iT邦幫忙

第 12 屆 iT 邦幫忙鐵人賽

DAY 6
0
自我挑戰組

初見Unity Shader系列 第 6

漫反射

文章使用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上,會將結果用 https://chart.googleapis.com/chart?cht=tx&chl=max(0.0%2C%20x)https://chart.googleapis.com/chart?cht=tx&chl=saturate(x) 限制住。

半蘭伯特

半蘭伯特為一種視覺加強技術,由那家數不出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)

Reference


上一篇
一個三角形的誕生(下)
下一篇
Unity Frame Debugger
系列文
初見Unity Shader30

尚未有邦友留言

立即登入留言