文章目錄
BRDF(基本光照模型)
基本光照模型一共有四種。分別爲環境光,自發光,漫反射,高光反射,我們先來實現漫反射
實現漫反射
光線強度的計算
在開始實現漫反射之前,我們需要了解一個概念:輻射度
下圖使用蘭伯特模型進行漫反射光計算(並且只進行了漫反射光計算),正面對着光線方向的部分最亮,然後逐漸變淡,
蘭伯特模型
反射光線的強度與表面法線和光源方向之間夾角的餘弦值成正比
(也就是使用我們剛剛的“輻射度”作爲漫反射光的強度)
好現在開始寫Shader
我們按書裏的流程寫一個頂點/片元着色器來實現漫反射
新建Shader
首先新建一個Unity Shader,把原有代碼全部刪除,然後給shader起個名字
Shader "Diffuse-Lambert" {
}
添加一個Properties語義塊
爲Shader添加一個Properties 語義塊,聲明我們需要的屬性。
Properties 語義塊是材質和Unity Shader的橋樑,它包含了一系列屬性,這些屬性會出現在檢查器窗口的材質面板中。【candycat】
Shader "Diffuse-Lambert" {
Properties {
//_Color 爲屬性標識符,我們會在稍後的Shader編寫中使用這個名字
//"Color Tint" 爲在檢查器窗口中顯示的屬性名稱
//Color 爲屬性的類型
//等號右邊的是屬性的默認值,在這裏四個1代表爲“白色”
_Color ("Color Tint", Color) = (1, 1, 1, 1) //漫反射底色
}
}
此時在Unity中隨意新建一個材質,並把我們的Diffuse-Lambert着色器拖到材質上,就可以在檢查器面板看到我們剛剛聲明的屬性了
添加SubShader和Pass。
每一個Unity Shader可以定義多個SubShader,但最少要有一個。當Unity需要加載這個UnityShader時,Unity會掃描所有的SubShader語義塊,並選出當前顯卡能夠支持的第一個SubShader運行。【candycat】
Shader "RefShader" { //屬性 Properties { } //顯卡A使用的子着色器 SubShader { } //顯卡B使用的子着色器 SubShader { } }
Shader "Diffuse-Lambert"{
Properties{
_Color("Color Tint", Color) = (1, 1, 1, 1) //漫反射底色
}
SubShader {
//如果支持的話,當前顯卡會使用這個子着色器
//不支持就靠 Fallback了
}
}
SubShader中定義了一系列Pass以及可選的狀態和標籤,每個Pass定義了一次完整的渲染流程,所以我們應該儘量使用最小數目的Pass。
Shader "Diffuse-Lambert"{
Properties{
_Color("Color Tint", Color) = (1, 1, 1, 1) //漫反射底色
}
SubShader {
Pass {
//在這裏會進行一次完整的渲染
}
}
}
在Pass中,我們設置標籤LightMode爲ForwardBase(向前渲染),這是爲了Unity能夠按向前渲染路徑的方式爲我們正確提供各個光照變量
Shader "Diffuse-Lambert"{
Properties{
_Color("Color Tint", Color) = (1, 1, 1, 1) //漫反射底色
}
SubShader {
Pass {
Tags { "LightMode"="ForwardBase" } //向前渲染
}
}
}
使用CG/HLSL語言來編寫頂點/片元着色器
我們使用 CGPROGRAM作爲CG/HLSL語言的開始符,而ENDCG是結束符。
在CG開始後,我們先來申明vert函數(頂點着色器,逐頂點渲染)和frag函數(片元着色器,逐片元渲染)
Shader "Diffuse-Lambert"{
Properties{
_Color("Color Tint", Color) = (1, 1, 1, 1) //漫反射底色
}
SubShader {
Pass {
Tags { "LightMode"="ForwardBase" } //向前渲染
CGPROGRAM //CG開始
#pragma vertex vert
#pragma fragment frag
//app to vert,頂點着色器的輸入類型,獲取當前頂點的信息(由程序給出的)
struct a2v {
};
//vert to frag,片元着色器的輸入類型,獲取當前片元的信息(由頂點着色器輸出的數據再插值得到的)
struct v2f {
};
//頂點着色器,逐頂點運行,輸入a2v是當前頂點的信息,輸出v2f給片元着色器
v2f vert(a2v v) {
}
//片元着色器,逐片元運行,輸入v2f是當前片元的信息,輸出顏色
fixed4 frag(v2f i) : SV_Target {
}
ENDCG //CG結束
}
}
}
這個流水線大約是這樣子的:
定義a2v和v2f結構體
現在我們的a2v還沒有任何字段,也就是說我們的頂點着色器(vert)什麼輸入都沒有,讓我們使用語義來給它增加一些數據。
Shader "Diffuse-Lambert"{
Properties{
_Color("Color Tint", Color) = (1, 1, 1, 1) //漫反射底色
}
SubShader {
Pass {
Tags { "LightMode"="ForwardBase" } //向前渲染
CGPROGRAM //CG開始
#pragma vertex vert
#pragma fragment frag
struct a2v {
//POSITION語義告訴Unity,用模型空間中當前頂點的座標填充vertex變量
float4 vertex : POSITION;
//NORMAL語義告訴Unity,用模型空間中當前頂點法的線方向填充normal變量
float3 normal : NORMAL;
};
struct v2f {
};
v2f vert(a2v v) {
}
fixed4 frag(v2f i) : SV_Target {
}
ENDCG //CG結束
}
}
}
接着給我們的v2f定義結構體,以確保我們在稍後的片元着色器中能夠有足夠的數據進行運算
我們回顧一下蘭伯特模型:
反射光線的強度與表面法線和光源方向之間夾角的餘弦值成正比
也就是說,只要得到了片元的表面法線和光源方向,我們就能得到該片元漫反射光線的強度。
Shader "Diffuse-Lambert"{
Properties{
_Color("Color Tint", Color) = (1, 1, 1, 1) //漫反射底色
}
SubShader {
Pass {
Tags { "LightMode"="ForwardBase" } //向前渲染
CGPROGRAM //CG開始
#pragma vertex vert
#pragma fragment frag
struct a2v {
float4 vertex : POSITION;
float3 normal : NORMAL;
};
struct v2f {
//SV_POSITION語義告訴Unity,pos裏包含了頂點在裁剪空間中的位置信息
//這也是頂點着色器最重要的一個工作:將頂點座標從模型空間轉換到裁剪空間
float4 pos : SV_POSITION;
//TEXCOORD0語義表示worldNormal變量佔用了TEXCOORD0插值寄存器
//每個插值寄存器可以存儲4個浮點值(float)
float3 worldNormal : TEXCOORD0; //世界空間下的頂點法線向量
float3 worldLightDir : TEXCOORD1; //世界空間下的光源位置
};
v2f vert(a2v v) {
}
fixed4 frag(v2f i) : SV_Target {
}
ENDCG //CG結束
}
}
}
包含頭文件以及聲明屬性變量
在正式開始計算之前,爲了使用一些Unity內置的變量和函數,我們需要包含進內置文件 "UnityCG.cginc"
Shader "Diffuse-Lambert"{
Properties{
_Color("Color Tint", Color) = (1, 1, 1, 1)
}
SubShader {
Pass {
Tags { "LightMode"="ForwardBase" }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc" //包含內置文件"UnityCG.cginc"
struct a2v {
float4 vertex : POSITION;
float3 normal : NORMAL;
};
struct v2f {
float4 pos : SV_POSITION;
float3 worldNormal : TEXCOORD0;
float3 worldLightDir : TEXCOORD1;
};
v2f vert(a2v v) {
}
fixed4 frag(v2f i) : SV_Target {
}
ENDCG
}
}
}
而爲了可以使用我們在Properties 語義塊中定義的屬性(那個_Color),我們需要在CG中對屬性進行申明
Shader "Diffuse-Lambert"{
Properties{
_Color("Color Tint", Color) = (1, 1, 1, 1)
}
SubShader {
Pass {
Tags { "LightMode"="ForwardBase" }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
fixed4 _Color; //這裏的變量名需要和屬性的名字完全一致
struct a2v {
float4 vertex : POSITION;
float3 normal : NORMAL;
};
struct v2f {
float4 pos : SV_POSITION;
float3 worldNormal : TEXCOORD0;
float3 worldLightDir : TEXCOORD1;
};
v2f vert(a2v v) {
}
fixed4 frag(v2f i) : SV_Target {
}
ENDCG
}
}
}
編寫頂點着色器
好了,開始寫我們的頂點着色器(vert函數)。首先申明一個v2f類型的變量,對結構體中的字段依次賦值,最後將其返回。
v2f vert(a2v v) {
//申明返回值v2f
v2f o;
//這是頂點着色器最重要的一個任務,將頂點座標從模型空間轉換到裁剪空間
//UnityObjectToClipPos函數接受一個模型空間的座標,返回該座標在裁剪空間的座標
o.pos = UnityObjectToClipPos(v.vertex);
//UnityObjectToWorldNormal函數接受一個模型空間的法線向量,將其轉換到世界空間中並返回
o.worldNormal = UnityObjectToWorldNormal(v.normal);
//WorldSpaceLightDir函數接受一個模型空間中的頂點位置,並返回世界空間中從該點到光源的光照方向。未被歸一化。(由於是平行光,任何點的光照方向都是一樣的,參數填fixed(0)都可以)
o.worldLightDir = WorldSpaceLightDir(v.vertex);
return o;
}
經過頂點着色器的處理,我們的每個片元中已經包含我們需要的兩個信息:法線向量和光源方向。現在,讓我們在片元着色器中爲每個片元計算他們的光線強度
編寫片元着色器
將法線向量和光源方向歸一化(轉爲長度爲1的單位向量)
由於我們稍後要用點積來求得兩向量夾角的cos值,而點積的公式是a·b=|a||b|cosθ(θ爲a和b的夾角),很明顯,只有當a和b均爲單位向量時,點積的結果纔會是我們要的兩向量的cos值。
fixed4 frag(v2f i) : SV_Target
{
//使用normalize()函數對向量進行歸一化
fixed3 worldNormal = normalize(i.worldNormal);
fixed3 worldLightDir = normalize(i.worldLightDir);
}
計算光線強度並返回顏色
fixed4 frag(v2f i) : SV_Target
{
fixed3 worldNormal = normalize(i.worldNormal);
fixed3 worldLightDir = normalize(i.worldLightDir);
//saturate()函數可以將值截取到[0,1],而dot()函數用於計算兩向量間的點積
//計算出光線強度後,和我們的_Color屬性相乘,就能實現一個亮度更改了
fixed3 diffuse = saturate(dot(worldNormal, worldLightDir)) * _Color;
//返回計算完成的顏色
return fixed4(diffuse, 1.0);
}
增加對燈光顏色的考慮
剛剛的計算我們實際上忽略了燈光的顏色,現在我們獲取燈光顏色並加入計算。
包含頭文件
爲了獲取燈光顏色,我們需要先在CG中包含頭文件 "Lighting.cginc"
Shader "Diffuse-Lambert"{
Properties{
_Color("Color Tint", Color) = (1, 1, 1, 1)
}
SubShader {
Pass {
Tags { "LightMode"="ForwardBase" }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc" //額外包含一個"Lighting.cginc"
#include "UnityCG.cginc"
fixed4 _Color;
struct a2v {
float4 vertex : POSITION;
float3 normal : NORMAL;
};
struct v2f
{
float4 pos : SV_POSITION;
float3 worldNormal : TEXCOORD0;
float3 worldLightDir : TEXCOORD1;
};
v2f vert(a2v v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldLightDir = WorldSpaceLightDir(v.vertex);
return o;
}
fixed4 frag(v2f i) : SV_Target
{
fixed3 worldNormal = normalize(i.worldNormal);
fixed3 worldLightDir = normalize(i.worldLightDir);
fixed3 diffuse = saturate(dot(worldNormal, worldLightDir)) * _Color;
return fixed4(diffuse, 1.0);
}
ENDCG
}
}
}
修改片元着色器
fixed4 frag(v2f i) : SV_Target
{
fixed3 worldNormal = normalize(i.worldNormal);
fixed3 worldLightDir = normalize(i.worldLightDir);
//直接乘上燈光顏色 _LightColor0
fixed3 diffuse = saturate(dot(worldNormal, worldLightDir)) * _Color * _LightColor0;
return fixed4(diffuse, 1.0);
}
另一種實現,半蘭伯特模型
在蘭伯特模型中,光照無法到達的區域,模型的外觀通常是全黑的,沒有任何明暗變化,這會使模型的背光區域看起來就像一個平面一樣,失去了模型細節表現。【candycat】
Valve公司在開發《半條命》時提出了一個新技術,被稱爲半蘭伯特光照模型。在半蘭伯特模型中,我們不將片元上法線向量和光源方向的cos值截取到[0, 1],而是爲這個cos值乘上α倍的縮放然後加上一個β大小的位移,通常,α和β都是0.5。也就是說,半蘭伯特模型將法線向量和光源方向的cos值從[-1, 1]映射到了[0, 1]。
修改片元着色器
fixed4 frag(v2f i) : SV_Target
{
fixed3 worldNormal = normalize(i.worldNormal);
fixed3 worldLightDir = normalize(i.worldLightDir);
//蘭伯特模型
//fixed3 diffuse = saturate(dot(worldNormal, worldLightDir)) * _Color * _LightColor0;
//半蘭伯特模型
fixed3 diffuse = (dot(worldNormal, worldLightDir) * 0.5 + 0.5) * _Color* _LightColor0;
return fixed4(diffuse, 1.0);
}