以下內容轉至https://blog.csdn.net/puppet_master/article/details/53074789
簡介
學了一段時間shader,然而一直在玩後處理,現在終於下定決心鑽研一下真正的帶光照的shader。從Diffuse到Specular。一個遊戲的畫面好壞,很大程度上取決於光照和貼圖。現實世界中,我們之所以能看見東西,是因爲他們要麼反射了光源發出的光,要麼是自身能夠發光。而在遊戲世界中,如果沒有了光,我們雖然可以直接根據貼圖顯示物體的材質,但是少了很多細節光影效果,遊戲顯得不真實。但是,真實的光照計算是一個非常複雜的過程,對於遊戲這種至少30FPS的程序來說是完全不可能的,所以我們必須要使用一種近似的光照算法,來模擬光照效果。本篇文章就來學習一下基本的光照模型以及其在unity下的shader實現。
漫反射和鏡面反射
我們觀察世界是因爲有光進入我們的眼睛,光在世界中主要有反射和折射兩種屬性,當光照在某種介質表面時,一部分光發生反射,另一部分光進入介質,發生折射,也有轉化爲其他能量的光。本篇文章只討論反射,折射等其他現象以後再學習。光的反射分爲兩種,漫反射和鏡面反射。
漫反射,是投射在粗糙表面上的光向各個方向反射的現象。當一束平行的入射光線射到粗糙的表面時,表面會把光線向着四面八方反射,所以入射線雖然互相平行,由於各點的法線方向不一致,造成反射光線向不同的方向無規則地反射,這種反射稱之爲“漫反射”或“漫射”。這種反射的光稱爲漫射光。很多物體,如植物、牆壁、衣服等,其表面粗看起來似乎是平滑,但用放大鏡仔細觀察,就會看到其表面是凹凸不平的,所以本來是平行的太陽光被這些表面反射後,瀰漫地射向不同方向。
鏡面反射,是指若反射面比較光滑,當平行入射的光線射到這個反射面時,仍會平行地向一個方向反射出來,這種反射就屬於鏡面反射,其反射波的方向與反射平面的法線夾角(反射角),與入射波方向與該反射平面法線的夾角(入射角)相等,且入射波、反射波,及平面法線同處於一個平面內。
蘭伯特光照模型
先來學習一個最簡單的光照模型,蘭伯特光照模型。蘭伯特光照模型是目前最簡單通用的模擬漫反射的光照模型,定義如下:模型表面的明亮度直接取決於光線向量(light vector)和表面法線(normal)兩個向量將夾角的餘弦值。光線向量是指這個點到光從哪個方向射入,表面法線則定義了這個表面的朝向。
如果漫反射光強設置爲Diffuse,入射光光強爲I,光方向和法線夾角爲θ,那麼蘭伯特光照模型可以用下面的公式表示:Diffuse = I * cosθ
進一步地,我們可以通過點乘來求得兩個方向向量之間的夾角,入射光方向設置爲L,法線方向設置爲N,如果光方向向量和法線方向向量都爲單位向量(這就是爲什麼我們在寫shader的時候需要normalize操作的原因),那麼它們之間的夾角餘弦值就可以表示爲:cosθ = dot(L,N),最終漫反射光強公式,也就是蘭伯特光照模型可以表示爲:Diffuse = I * dot(L,N)
逐頂點計算着色shader
我們在shader中需要計算輸出的顏色,逐頂點着色也就是說我們的計算主要放在了vertex shader中,根據頂點來計算,每個頂點中計算出了該點的顏色,直接作爲vertex shader的輸出,pixel(fragment) shader的輸入,當到達pixel階段時,直接輸出頂點shader的結果。比如一個三角形面片,在vertex階段,分別計算了每個頂點的顏色值,在pixel階段時,這個面片經過投影,最終顯示在屏幕上的像素,會根據該像素周圍的頂點來插值計算像素的最終顏色,這種着色方式也叫做高洛德着色。
下面看一下unity shader實現的逐頂點着色:
Shader "ApcShader/DiffusePerVetex"
{
//屬性
Properties{
_Diffuse("Diffuse", Color) = (1,1,1,1)
}
//子着色器
SubShader
{
Pass
{
//定義Tags
Tags{ "RenderType" = "Opaque" }
CGPROGRAM
//引入頭文件
#include "Lighting.cginc"
//定義Properties中的變量
fixed4 _Diffuse;
//定義結構體:應用階段到vertex shader階段的數據,如果定義了
struct a2v
{
float4 vertex : POSITION;
float3 normal : NORMAL;
};
//定義結構體:vertex shader階段輸出的內容
struct v2f
{
float4 pos : SV_POSITION;
fixed4 color : COLOR;
};
//定義頂點shader
v2f vert(a2v v)
{
v2f o;
o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
//把法線轉化到世界空間
float3 worldNormal = mul(v.normal, (float3x3)_World2Object);
//歸一化法線
worldNormal = normalize(worldNormal);
//把光照方向歸一化
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
//根據蘭伯特模型計算頂點的光照信息,dot可能有負值,小於0的部分可以理解爲看不見,直接取0
fixed3 lambert = max(0.0, dot(worldNormal, worldLightDir));
//最終輸出顏色爲lambert光強*材質diffuse顏色*光顏色
o.color = fixed4(lambert * _Diffuse.xyz * _LightColor0.xyz, 1.0);
return o;
}
//定義片元shader
fixed4 frag(v2f i) : SV_Target
{
return i.color;
}
//使用vert函數和frag函數
#pragma vertex vert
#pragma fragment frag
ENDCG
}
}
//前面的Shader失效的話,使用默認的Diffuse
FallBack "Diffuse"
}
Phong光照模型的Shader實現
Shader "lijia/Phong"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_Specular("Specular", Range(1, 20)) = 1
_SpecColor("SpecColor", Color) = (1,1,1,1)
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
float3 normal : TEXCOORD1;
float3 lightDir : TEXCOORD2;
float4 objPos : TEXCOORD3;
};
sampler2D _MainTex;
float4 _MainTex_ST;
float4 _LightColor0;
float _Specular;
float4 _SpecColor;
v2f vert (appdata_full v)
{
v2f o;
o.objPos = v.vertex;
o.vertex = mul(UNITY_MATRIX_MVP, v.vertex);
o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
o.normal = v.normal;
o.lightDir = ObjSpaceLightDir(v.vertex);//把光向量從世界空間轉成模型空間
return o;
}
fixed4 frag (v2f i) : SV_Target
{
float3 L = normalize(i.lightDir);
float3 N = normalize(i.normal);
float3 viewDir = normalize(ObjSpaceViewDir(i.objPos));//計算出視線
float diff = saturate(dot(L, N));
float3 reflection = normalize(2.0 * N * diff - L);//反射向量
float spec = pow(max(0, dot(reflection, viewDir)), _Specular);
float3 finalSpec = _SpecColor.rgb * spec;
//漫反射+鏡面高光+環境光
float3 finalLight = diff * _LightColor0 + finalSpec + UNITY_LIGHTMODEL_AMBIENT;
fixed4 col = tex2D(_MainTex, i.uv);
return col * float4(finalLight, 1);
}
ENDCG
}
}
}