1. 程式人生 > >第五章 Unity中的基礎光照(3)

第五章 Unity中的基礎光照(3)

lds 存儲 com code 原理 提醒 詳細 dot mat

目錄

  • 1. 在Unity Shader中實現高光反射光照模型
    • 1.1 實踐:逐頂點光照
    • 1.2 逐像素光照
    • 1.3 Blinn-Phong光照模型
  • 2. 召喚神龍:使用Unity內置的函數

@

1. 在Unity Shader中實現高光反射光照模型

在前面,我們給出了基本光照模型中高光反射部分的計算公式:
技術分享圖片
從公式可以看出,要計算高光反射需要知道4個參數:入射光線的顏色和強度Clight,材質的高光反射系數mspecular,視角方向v以及反射方向r。其中反射反射方向r可以由表面法線n和光源方向l計算而得:

技術分享圖片
上述公式很簡單,更幸運的是,Cg提供了計算反射方向的函數reflect。
函數:reflect(i,n)
參數:i,入射方向;n,法線方向。可以是float、float2、float3等類型。
描述:當給定入射方向i和法線方向n時,reflect函數可以返回反射方向。下圖給出了參數和返回值之間的關系。
技術分享圖片

1.1 實踐:逐頂點光照

我們首先來看如何實現一個逐頂點的高光反射光效果,在學習完本節後,我們會得到類似下圖的效果。
技術分享圖片
(1)為了在材質面板中能夠方便的控制高光反射屬性,我們在Shader的Properties語義塊中聲明了3個屬性:

Properties{
_Diffuse("Diffuse",Color)= (1,1,1,1)
_Specular("Specular",color)=(1,1,1,1)
_Gloss("Gloss",Range(8.0,256))=20
}

其中,新添加的_Specular用於控制材質的高光反射顏色,而_Gloss用於控制高光區域的大小。
(2)然後,我們在SubShader語義塊中定義了一個Pass語義塊,這是因為頂點/片元著色器的代碼需要寫在Pass語義塊,而非SubShader語義塊。而且,我們在Pass的第一行指明了該Pass的光照模式:

SubShader{
Pass{
Tags{"LightMode"="ForwardBase"}
}
}

LightMode標簽是Pass標簽的一種,它用於定義該Pass在Unity的光照流水線周明華的角色,在後面我們會更加詳細的解釋它。在這裏我們只需要知道,只有定義了正確的LightMode,我們才能得到一些Unity的內置光照變量,例如_LightColor0。

(3)然後我們使用CGPROGRAM和ENDCG來包圍CG代碼片,以定義最重要的頂點著色器和片元著色器代碼。首先我們使用#pragma指令來告訴Unity,我們定義的頂點著色器和片元著色器叫什麽名字。在本例中,它們的名字分別是vert和frag:

CGPROGRAM
#pragma vertex vert
#pragma fragment frag

(4)為了使用Unity內置的一些變量,如_LightColor0,還需要包含進Unity的內置文件Lighting.cginc:

#include "Lighting.cginc"

(5)為了在Shader中使用Properties語義塊中聲明的屬性,我們需要定義和這些屬性類型相匹配的變量:

fixed4 _Diffuse;
fixed4 _Specular;
float _Gloss;

由於顏色屬性的範圍在0到1之間,因此對於_Diffuse和_Specular屬性我們可以使用fixed精度的變量來存儲它。而_Gloss的範圍很大,因此我們使用float精度來存儲。
(6)然後,我們定義了頂點著色器的輸入和輸出結構體(輸出結構體同時也是片元著色器的輸入結構體):

struct a2v{
        float4 vertex:POSITION;
        float3 normal:NORMAL;
};
struct v2f{
float4 pos:SV_POSITION;
fixed3 color:COLOR;
};

(7)在頂點著色器中,我們計算了包含高光反射的光照模型

v2f vert(a2v v){
v2f o;
//Transform the vertex from object space to projection space
o.pos = mul(UNITY_MATRIX_MVP,v.vertex);
//Get ambient term
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
//Transform the normal from object space to world space
fixed3 worldNormal = normalize(mul(v.normal,(float3)_World2Object));
//Get the light direction in world space
fixed3 worldLightDir = normalize(_WorldSpaceLightPos.xyz);
//Compute diffuse term
fixed3 diffuse = _LightColor0.rgb*__Diffuse.rgb*saturate(dot(worrldNormal,worldLightDir));
//Get the reflect direction in world space
fixed3 reflectDir = normalize(reflect(-worldLightDir,worldNormal));
//Get the view direction in world space
fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz-mul(_Object2World,v.vertex).xyz);
//Compute specular term
fixed3 specular = _LightColor0.rgb*_Sprecular.rgb*pow(saturate(dot(reflectDir,viewDir)),_Gloss)
o.color = ambient+diffuse+specular;
return o;
}

對於高光反射部分,我們首先計算了入射光線方向關於表面法線的反射方向reflectDir。由於Cg的reflect函數的入射方向要求是由光源指向焦點處的,因此我們需要對_worldLightDir去反後再傳給reflect函數。然後我們通過_WorldSpaceCameraPos得到了世界空間中的攝像機的位置,再把頂點位置從模型空間變換到世界空間下,再通過和_WorldSpaceCameraPos相減即可得到世界空間下的視角方向。
由此,我們已經得到了所有的4個參數,代入公式即可得到高光反射的光照部分。最後,再和環境光、漫反射光相加存儲到最後的顏色中。
(8)片元著色器的代碼非常簡單,我們只需要直接返回頂點顏色即可:

fixed4 frag(v2f i):SV_Target{
return fixed4(i.color,1.0);
}

(9)最後,我們需要把這個Unity Shader的回調Shader設置為內置的Specular:

Fallback"Specular"

使用逐頂點的方法得到的高光效果有比較大問題,我們可以在上圖中看出高光部分明顯不平滑。這主要是因為,高光反射部分的計算是非線性的,而在頂點著色器中計算光照再進行插值的過程是線性的,破壞了原計算的非線性關系,就會出現較大的視覺問題。因此,我們就需要使用逐像素的方法來計算高光反射。

1.2 逐像素光照

我們可以使用逐像素光照來得到更加平滑的高光效果,如圖所示:
技術分享圖片
(1)修改頂點著色器的輸出結構v2f:

struct v2f{
float4 pos:SV_POSITION;
float3 worldNormal:TEXCOORD0;
float3 worldPos:TEXCOORD1;
}

(2)頂點著色器只需要計算世界空間下的法線方向和頂點坐標,並把它們傳遞給片元著色器即可:

v2f vert(a2v v){
v2f o;
// Transform the vertex from object space to projection space
o.pos=mul(UNITY_MATRIX_MVP,v.vertex);
//Transform the normal fram object space to world space
o.worldnormal = mul(v.normal,(float3×3)_world2Object);
//Transform the vertex from object space to world space
o.worldPos = mul(_Object2Word,v.vertex).xyz;
return 0;
}

(3)片元著色器需要計算關鍵的光照模型:

fixed4 frag(v2f i):SV_Target{
//Get ambient term
fixed3 ambient:UNITY_LIGHTMODEL_AMBIENT.xyz;
fixed3 worldNormal=normalize(i.worldNormal);
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
//Compute diffuse term
fixed3 diffuse = _LightColor0.rgb*_Diffuse.rgb*saturate(dot(worldNormal,worldLightDir));
//Get the reflect direction in world space
fixed3 reflectDir = normalize(_WorldSpaceCameraPos.xyz-i.worldPos.xyz);
//Compute specular term
fixed3 specular = _LightColor0.rgb*_Specular.rgb*pow(saturate(dot(reflectDir,viewDir)),_Gloss);
return fixed4(ambient+diffuse+specular+1.0);
}

可以看出,按逐像素的方式處理光照可以得到更加平滑的高光效果。至此我們就實現了一個完整的Phong光照模型。

1.3 Blinn-Phong光照模型

在前面我們還提出了另一種高光反射的方法——Blinn光照模型。Blinn光照模型沒有使用反射方向,而是引入了一個新的矢量h,它是通過對視角方向v和光照方向l相加後再歸一化得到的。即:
技術分享圖片
而Blinn模型計算高光反射的公式如下:
技術分享圖片
修改後的代碼如下:

fixed4 frag(v2f i):SV_Target{
...
//Get the view direction in world space
fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz-i.worldPos.xyz)
//Get the half direction in world space
fixed3 halfDir = normalize(worldLightDir+vieDir);
//Compute specular term
fixed3 specular = _LightColor0.rgb*_Specular.rgb*pow(max(0,dot(worldNormal,halfDir)),_Gloss);
return fixed4(ambient+diffuse+specular,1.0);
}

下圖給出了逐頂點的高光反射效果,逐像素的高光反射效果(Phong模型)和Blinn-Phong高光反射光照的對比效果。
技術分享圖片

可以看出,Blinn-Phong光照模型的高光反射部分看起來更大、更亮一些。在實際渲染中,絕大多數情況我們都會選擇Blinn-Phong光照模型。需要再次提醒的是,這兩種光照模型都是經驗模型,也就是說我們不應該認為Blinn-Phong模型是對“正確的”Phong模型的近似。實際上,在一些情況下,Blinn-Phong模型更符合實驗結果。

2. 召喚神龍:使用Unity內置的函數

讀者可以發現,計算光照模型的時候,我們往往需要得到光源方向、視角方向這兩個基本信息。在上面的例子中,我們都是自行在代碼裏面計算的,例如使用normalize(_WorldSpaceLightPos0.xyz)來得到光源方向(這種方法只適用於平行光),使用normalize(_WorldSpaceCameraPos.xyz-i.worldPosition.xyz)來得到視角方向。但如果要處理更復雜的光照類型,如點光源和聚光燈,我們計算光源的方法就是錯誤的。這需要我們在代碼中先判斷光源類型,在計算它的光源信息。具體方法會在後面講到。
手動計算這些光源信息的過程相對比較麻煩(但並不意味著你不需要了解它們的原理)。幸運的是,Unity提供了一些內置函數幫我們計算這些信息。下表給出了計算光照模型時,我們常常使用的一些內置函數。
技術分享圖片

註意,類似Unityxxx的幾個函數是Unity5中新添加的內置函數。這些幫助函數使得我們不需要跟各種變換矩陣、內置變量打交道,也不需要考慮各種不同的情況(例如使用了哪種光源),而僅僅調用一個函數就可以得到需要的信息。上面的9個幫助函數中,有五個我們已掌握了其內部實現,例如WorldSpaceViewDir函數實現如下:

//Compute world space view direction ,from object space position
inline float3 UnityWorldSpaceViewDir(in float3 worldPos)
{
return _WorldSpaceCamearPos.xyz-worldpos;
}

可以看出,這與之前計算視角的方向一致。需要註意的是,這些函數都沒有保證得到的方向矢量是單位矢量,因此,我們需要在使用前把它們歸一化。
而計算光源方向的3個函數:WorldSpaceLightDir、UnityWorldSpaceLightDir和ObjSpaceLightDir,稍微復雜一些,這是因為Unity幫我們處理了不同種類光源的情況。需要註意的是,這三個函數僅可用於前向渲染(關於什麽是前向渲染我們後面會講到)。這是因為只有在前向渲染時,這三個函數裏使用的內置變量_WorldSpaceLightPos0才會被正確賦值。關於哪些內置變量只會在前向渲染中被正確賦值,我們後面會說到。
下面介紹使用內置函數改寫UnityShader。我們將使用這些內置函數來改寫Blinn-Phong光照模型的UnityShader。
(1)在頂點著色器中,我們使用內置的UnityObjectToWorldNormal函數來計算世界空間下的法線方向:

v2f vert(a2v v){
v2f o;
...
//Use the bulid-in function to compute the normal in world space
o.worldNormal = UnityObjectToWorldNormal(v.normal);
...
return o;
}

(2)在片元著色器中,我們使用內置的UnityWorldSpaceLightDir函數和UnityWorldSpaceViewDir函數來分別計算世界空間的光照方向和視角方向:

fixed4 frag(v2f i):SV_Target{
...
fixed3 worldNormal = normalize(i.worldNormal);
//Use the build-in function tocompute the light direction in world space
// Rember to normalize the result
fixed3  worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
...
//Use the build-in function tocompute the light direction in world space
// Rember to normalize the result
fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
}

需要註意的是,由於內置函數得到的方向是沒有歸一化的,因此我們需要使用normalize函數來對結果進行歸一化,再進行光照模型的計算。

第五章 Unity中的基礎光照(3)