Published on

練習實作 Phong Model 光照

Unity 對圖形底層進行包裝,並定義一系列的 Render Pipeline,使用者可使用 Cg/HLSL 語言在各渲染階段操作物體頂點、像素等,這次使用 Cg 語言實作光照模型 Phong Model,包括 Ambient、Diffuse 和 Specular。

漫反射 (Diffuse)

實作漫反射須知道物體表面是否正向光源,因此可使用物體表面與光源的向量 R\vec{R},並與表面法向量 N\vec{N} 做內積,若光源在物件表面正上方,則 RN\vec{R} \cdot \vec{N} 為 1,若光源與表面平行或在背後,則 RN<=0\vec{R} \cdot \vec{N} <= 0(前提 R\vec{R}N\vec{N} 均是 Normalize),計算內積後即可知道光源與表面漫反射的相關性,最後加入光源強度和 Diffuse 程度,整合公式如下:

Diffuse=ILight×IDiffuse×(RN)Diffuse = I_{Light} \times I_{Diffuse} \times ( \vec{R} \cdot \vec{N} )

預覽結果

大功告成!但仔細看會發現,沒照到光的部分通通是黑色的,因為 RN<=0\vec{R} \cdot \vec{N} <= 0,這不符合現實光照,因此須對物件增加一層 Ambient Light。

加入環境光 (Diffuse + Ambient)

現實中,若沒照到光的部分仍然會有其他物件反射後的環境光,所以不可能是全黑的,但早期的電腦很難計算所有光的反射,因此公式很簡單,只需要乘上光照強度和Ambient程度就好。

關於較新的技術 PBR(基於物理的渲染)採用光線追蹤模擬光的反射,這樣可以達到更真實的渲染效果,推薦一本講解 PBR 的書籍 Physically Based Rendering: From Theory to Implementation,這本書的特色是實作內容完全由文言程式語言(Literate Programming)組成,以白話文的方式解釋程式碼,大幅降低理解的難易度。

Ambient 的計算方式如下:

Ambient=ILight×IAmbientAmbient = I_{Light} \times I_{Ambient}

預覽結果

加上 Ambient 後,沒有光源的地方就不會是全黑了!最後只剩下 Specular 需要實作,Specular 的目的是模擬物體的反射光。

加入高光 (Diffuse + Ambient + Specular)

Phong Model 中的 Specular,先獲得光源經過表面反射的反射 R\vec{R},接著與相機視角 V\vec{V} 內積,目的是獲得反射光打到眼睛的相關程度,若反射出來的光不直視眼睛,反射量則遞減。

之後可以進一步模擬物體粗糙程度,設置粗糙度 nn,若物體越粗糙,反射程度會指數遞減(高光會不明顯)。

Specular=ILight×ISpecular×(VR)nSpecular = I_{Light} \times I_{Specular} \times (\vec{V} \cdot \vec{R})^n

實作結果

程式碼 (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
        }
    }
}