文章使用Unity 2019 LTS
在現實世界中,我們能看到的任何東西都是由光打在物體上,最後反射至我們眼睛的結果。本章會透過Unity來介紹基本的光源模型,初探光在三維電腦圖學的世界裡是怎麼運作的。
從國中物理到大學學到的物理告訴我們,我們能看到物體的顏色是因為光照到物體後,反射的結果,然而我們都知道物體的表面不可能全是光滑的,則光照到凹凸不平的表面後,無序地向四面八方反射的現象,就是漫反射
from wiki
首先我們想想根據了解的漫反射的情形,列出需要甚麼參數:
「嗯...想看到物體的顏色,所以第一,我們需要物體的顏色,然後這跟光有關,所以我們需要光的入射角以及顏色,顏色可以不用,但入射角是必要的,然後對於表面的反射,我們需要表面的法向量計算出反射的角度,然後我們還要挑選出物體的哪一塊被光照到,哪一塊沒有,所以可以利用角度來進行篩選...,阿!可以用點積這個做法」
於是我們整理出來的結果有:
以下會有兩種不同的寫法,逐頂點與逐像素,就是將漫反射的計算分別放到vertex shader和fragment shader。
讀者可以先將以下的Shader複製到Unity裡面,可以發現程式碼機幾乎沒變,只是換了不同的位置寫而已,但看到的結果會有很大的區別。
Shader "Learning/Diffuse-Per-Vertex"
{
Properties
{
// 定義一個顏色屬性於Unity Editor中調整
_Diffuse ("Diffuse Color", Color) = (1, 1, 1, 1)
}
SubShader
{
Tags { "LightMode" = "ForwardBase" }
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "Lighting.cginc"
// 來自上方定義的_Diffuse屬性
fixed4 _Diffuse;
struct v2f
{
float4 pos : SV_POSITION;
fixed3 color : COLOR;
};
v2f vert(appdata_base v)
{
v2f o;
// 將物件由模型空間轉換至裁剪空間
o.pos = UnityObjectToClipPos(v.vertex);
// 獲得環境光
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
// 將物件的法向量轉換至世界空間,記得線性代數的計算是由右至左
fixed3 worldNormalDir = normalize(mul(v.normal, (float3x3)unity_WorldToObject));
// 獲得光的方向向量
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
// 計算漫反射係數
// saturate(x): 將x擷取至[0, 1],我們不希望獲得負值的結果
fixed diffIntensity = saturate(dot(worldNormalDir, worldLightDir));
// 將光的顏色、物件的顏色、反射的係數"混合"
// Diffuse = I * C * dot(L, N)
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * diffIntensity;
// 將環境光與漫反射進行"疊加"
o.color = ambient + diffuse;
return o;
}
fixed4 frag(v2f f) : SV_Target
{
// 取得由上一階段vertex shader所計算的color
return fixed4(f.color, 1.0);
}
ENDCG
}
}
Fallback "Specular"
}
// 與逐頂點並無太大的差異,只是將光源計算至fragment(pixel) shader中
Shader "Learning/Diffuse-Per-Pixel"
{
Properties
{
_Diffuse("Diffuse Color", Color) = (1, 1, 1, 1)
}
SubShader
{
Tags { "LightMode" = "ForwardBase" }
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "Lighting.cginc"
struct v2f
{
float4 pos : SV_POSITION;
float3 normal : NORMAL;
};
fixed4 _Diffuse;
v2f vert (appdata_base v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.normal = mul(v.normal, (float3x3)unity_WorldToObject);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT;
fixed3 worldNormalDir = normalize(i.normal);
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
fixed3 diffIntensity = saturate(dot(worldNormalDir, worldLightDir);
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * diffIensity);
return fixed4(ambient + diffuse, 1);
}
ENDCG
}
}
Fallback "Diffuse"
}
圖(1)左邊為逐頂點,右邊為逐像素。
圖(1)
由圖(1)放大看會發現,逐頂點在陰影的部分呈現一塊一塊的樣子,逐像素則是滑順的,請見圖(2)。
圖(2)
注意: 點積後可能會產生負值,我們並不希望使用他,因為(R, G, B)的範圍為0到1,所以在撰寫Shader上,會將結果用 或 限制住。
半蘭伯特為一種視覺加強技術,由那家數不出3的遊戲公司 - Valve所提出的,應用在《Half Life》上。
上面兩種範例會遇到一種情況,即是光無法照射到模型的部分,該部分會呈現出全黑的模樣,這會使畫面像一個平面,無法呈顯模型細節,Valve提出了一個方法,那就是半蘭伯特,以下為進行修改後的公式:
多了A和B這兩個變數可以幫助我們調整對於完全沒照到光的部分。
Shader "Learning/Ch6/half_lambert"
{
Properties
{
_Diffuse("Diffuse Color", Color) = (1, 1, 1, 1)
// 新增兩個屬性,對漫反射進行調整
_A ("Half Lambert Scale", Range(0.1, 1.0)) = 0.5
_B ("Half Lambert Offset", Range(0.1, 1.0)) = 0.5
}
SubShader
{
Tags { "LightMode" = "ForwardBase" }
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "Lighting.cginc"
struct v2f
{
float4 pos : SV_POSITION;
float3 normal : NORMAL;
};
fixed4 _Diffuse;
fixed _A;
fixed _B;
v2f vert (appdata_base v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.normal = mul(v.normal, (float3x3)unity_WorldToObject);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT;
fixed3 worldNormalDir = normalize(i.normal);
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
// 由saturate改為,由兩個變數進行調整
fixed3 half_lambert = dot(worldNormalDir, worldLightDir) * _A + _B;
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * half_lambert;
return fixed4(ambient + diffuse, 1);
}
ENDCG
}
}
}
比較一下三種光源模型 圖(3),有左至右為逐頂點、逐像素、半蘭伯特
圖(3)