- Published on
練習實作 Phong Model 光照
Unity 對圖形底層進行包裝,並定義一系列的 Render Pipeline,使用者可使用 Cg/HLSL 語言在各渲染階段操作物體頂點、像素等,這次使用 Cg 語言實作光照模型 Phong Model,包括 Ambient、Diffuse 和 Specular。
漫反射 (Diffuse)
實作漫反射須知道物體表面是否正向光源,因此可使用物體表面與光源的向量 ,並與表面法向量 做內積,若光源在物件表面正上方,則 為 1,若光源與表面平行或在背後,則 (前提 、 均是 Normalize),計算內積後即可知道光源與表面漫反射的相關性,最後加入光源強度和 Diffuse 程度,整合公式如下:

預覽結果

大功告成!但仔細看會發現,沒照到光的部分通通是黑色的,因為 ,這不符合現實光照,因此須對物件增加一層 Ambient Light。
加入環境光 (Diffuse + Ambient)
現實中,若沒照到光的部分仍然會有其他物件反射後的環境光,所以不可能是全黑的,但早期的電腦很難計算所有光的反射,因此公式很簡單,只需要乘上光照強度和Ambient程度就好。
關於較新的技術 PBR(基於物理的渲染)採用光線追蹤模擬光的反射,這樣可以達到更真實的渲染效果,推薦一本講解 PBR 的書籍 Physically Based Rendering: From Theory to Implementation,這本書的特色是實作內容完全由文言程式語言(Literate Programming)組成,以白話文的方式解釋程式碼,大幅降低理解的難易度。
Ambient 的計算方式如下:
預覽結果

加上 Ambient 後,沒有光源的地方就不會是全黑了!最後只剩下 Specular 需要實作,Specular 的目的是模擬物體的反射光。
加入高光 (Diffuse + Ambient + Specular)
Phong Model 中的 Specular,先獲得光源經過表面反射的反射 ,接著與相機視角 內積,目的是獲得反射光打到眼睛的相關程度,若反射出來的光不直視眼睛,反射量則遞減。
之後可以進一步模擬物體粗糙程度,設置粗糙度 ,若物體越粗糙,反射程度會指數遞減(高光會不明顯)。

實作結果

程式碼 (CG)
Shader "Unlit/Phong"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_Color("Color", Color) = (0.25, 0.5, 0.5, 1)
_Diffuse("Diffuse", float) = 1
_Gloss("Gloss", float) = 1
_Specular("Specular", float) = 1
}
SubShader
{
Tags { "RenderType"="Opaque" }
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
float3 normal : NORMAL;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
float4 worldPos : TEXCOORD1;
float3 normal : NORMAL;
};
sampler2D _MainTex;
float4 _MainTex_ST;
float4 _Color;
float _Gloss;
float _Diffuse;
float _Specular;
v2f vert (appdata v)
{
v2f o;
// 將物件座標轉為螢幕座標
o.vertex = UnityObjectToClipPos(v.vertex);
// 取得texture的UV,之後要貼貼圖在像素上
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
// 計算Diffuse需要頂點世界座標,所以拿世界座標轉換矩陣跟頂點座標相乘
o.worldPos = mul(unity_ObjectToWorld, v.vertex);
// 獲得頂點法向量
o.normal = v.normal;
return o;
}
fixed4 frag(v2f i) : SV_Target
{
/* Ambient */
fixed4 ambient = UNITY_LIGHTMODEL_AMBIENT;
/* Diffuse */
// 獲得點的法向量 (normalize)
fixed3 worldNormalDir = normalize(i.normal);
// 獲得點對光源的向量 (normalize)
fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
// 取光向量與表面法向量夾角,並算出強度
fixed4 diffuse = saturate(dot(worldNormalDir, worldLightDir)) * _Diffuse;
/* Specular */
// 計算光對表面的反射向量
// (worldLightDir是負的,因為是從光為起點到表面的向量)
fixed3 reflectDir = normalize(reflect(-worldLightDir, worldNormalDir));
// 算出視角與表面的向量
fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos);
// dot算出specular
fixed4 specular = pow(saturate(dot(reflectDir, viewDir)), _Gloss) * _Specular;
// 取得貼圖在該UV下的顏色
fixed4 textureColor = tex2D(_MainTex, i.uv) * _Color;
return textureColor * (diffuse + ambient + specular);
}
ENDCG
}
}
}