Unity中的光源型別(向前渲染路徑進行光照計算)

畅知發表於2024-09-23

Unity中的光源型別

Unity中共支援4種光源型別:

  • 平行光
  • 點光源
  • 聚光燈
  • 面光源(在光照烘焙時才可以發揮作用)

光源的屬性:

  • 位置
  • 方向(到某個點的方向)
  • 顏色
  • 強度
  • 衰減(到某個點的衰減)
  1. 平行光

    平行光的幾何定義是最簡單的,平行光可以照亮的範圍是無限遠的,且對與場景中的各個點的方向和強度都是一致的。在場景中作為太陽這樣的角色出現。

    img

  2. 點光源

    點光源照亮的空間是有限的,它是由空間中的一個球體定義的。其可以表示由一個點發出的、向所有方向延伸的光。

    img

    需要注意的是點光源的方向屬性是由某個點減去點光源位置所得出的向量,表示點光源在該點的光照方向。點光源會衰減,隨著物體逐漸原理點光源,其接收到的光照強度也會逐漸減小。

  3. 聚光燈

    聚光燈是這3種光源型別中最複雜的一種。它的照亮空間同樣是有限的,但不再是簡單的球體,而是由空間中的一塊錐形區域定義的。聚光燈可以用於表示由一個特定位置出發、向特定方向延伸的光。

    img

​ 這塊錐形區域的半徑由皮膚中的Range屬性決定,而錐體的張開角度由Spot Angle屬性決定。我們同樣也可以在 Scene檢視中直接拖拉聚光燈的線框(如中間的黃色控制點以及四周的黃色控制點)來修改它的屬性。聚光燈的位置同樣是由Transform元件中的Position屬性定義的。對於方向屬性,我們需要用聚光燈的位置減去某點的位置來得到它到該點的方向。聚光燈的衰減也是隨著物體逐漸遠離點光源而逐漸減小,在錐形的頂點處光照強度最強,在錐形的邊界處強度為0。其中間的衰減值可以由一個函式定義,這個函式相對於點光源衰減計算公式要更加複雜,因為我們需要判斷一個點是否在錐體的範圍內。

在向前渲染中處理不同的光照型別

Shader "Custom/ForwardRanderingLearn"
{
    Properties{
        _Diffuse("Diffuse", Color) = (1,1,1,1) //漫反射顏色
        _Specular("Specular",Color) = (1,1,1,1)//高光反射顏色
        _Gloss("Gloss",Range(8.0,256)) = 20 //高光反射強度
    }
    
    SubShader{
        Tags { "RenderType" ="Opaque" }
        
        Pass
        {
            //設定渲染模式
            Tags{ "LightMode"="ForwardBase" }
            
            CGPROGRAM
            //新增宏引用
            #pragma multi_compile_fwdbase

            #pragma  vertex vert
            #pragma  fragment frag

            #include "Lighting.cginc"

            fixed4 _Diffuse;
            fixed4 _Specular;
            float _Gloss;

            struct a2v
            {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
            };

            struct v2f
            {
                float4 pos:SV_POSITION;
                float3 worldNormal : TEXCOORD0;
                float3 worldPos : TEXCOORD1;
            };

            v2f vert(a2v v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.worldNormal = UnityObjectToWorldNormal(v.normal);
                o.worldPos = mul(unity_ObjectToWorld,v.vertex).xyz;
                return o;
            }

            fixed4 frag(v2f i) : SV_Target
            {
                fixed3 worldNormal = normalize(i.worldNormal);
                fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz); //平行光的方向

                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz; //環境光

                fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * max(0,dot(worldNormal,worldLightDir));

                //計算高光反射
                fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);
                fixed3 halfDir = normalize(worldLightDir + viewDir);
                fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0,dot(worldNormal,halfDir)),_Gloss);

                //平行光的衰減因子
                fixed atten = 1.0;

                return fixed4(ambient + (diffuse + specular) * atten,1.0);
            }
            ENDCG
        }
        
        Pass
        {
            Tags {"LightMode" = "ForwardAdd"}
            
            //開啟混合模式
            Blend One One
            
            CGPROGRAM
            
            #pragma multi_compile_fwdadd

            #pragma vertex vert
            #pragma fragment frag

            #include "Lighting.cginc"
            #include "AutoLight.cginc"

            fixed4 _Diffuse;
            fixed4 _Specular;
            float _Gloss;

            struct a2v
            {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
            };

            struct v2f
            {
                float4 pos : SV_POSITION;
                float3 worldNormal : TEXCOORD0;
                float3 worldPos : TEXCOORD1;
            };

            v2f vert(a2v v)
            {
                v2f o;

                o.pos = UnityObjectToClipPos(v.vertex);

                o.worldNormal = UnityObjectToWorldNormal(v.normal);

                o.worldPos = mul(unity_ObjectToWorld,v.vertex).xyz;

                return o;
            }

            fixed4 frag(v2f i):SV_Target
            {
                fixed3 worldNormal = normalize(i.worldNormal);

                //根據光照型別確定光源方向
                #ifdef USING_DIRECTIONAL_LIGHT
                //平行光
                    fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
                #else
                //非平行光
                    fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz - i.worldPos.xyz);
                #endif

                //漫反射光
                fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * max(0,dot(worldNormal,worldLightDir));

                fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);
                fixed3 halfDir = normalize(worldLightDir + viewDir);
                fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0,dot(worldNormal,halfDir)),_Gloss);

               
                //根據光源型別來設定衰減函式
                #ifdef  USING_DIRECTIONAL_LIGHT
                    fixed atten = 1.0;
                #else
                    #if defined(POINT)
                         float3 lightCoord = mul(unity_WorldToLight,float4(i.worldPos,1.0)).xyz;
                         fixed atten = tex2D(_LightTexture0,dot(lightCoord,lightCoord).rr).UNITY_ATTEN_CHANNEL;
                    #elif defined (SPOT)
                         float4 lightCoord = mul(unity_WorldToLight,float4(i.worldPos,1.0)).xyz;
                         fixed atten = (lightCoord.z > 0) * tex2D(_LightTexture0, lightCoord.xy / lightCoord.w + 0.5).w * tex2D(_LightTextureB0, dot(lightCoord, lightCoord).rr).UNITY_ATTEN_CHANNEL;
                    #else
                        fixed atten = 1.0;
                    #endif

                #endif

                return fixed4((diffuse + specular) * atten,1.0);
            }
            
            ENDCG
        }
    }
    FallBack "Specular"
   
}

在此shader中,在Base Pass中處理場景中最重要的平行光。

本場景中只有一個平行光,因此Base Pass只會執行一次。如果場景中包含多個平行光,Unity則會選擇最亮的平行光傳遞給Base Pass進行逐畫素處理,其它平行光會按照逐頂點或在Additional Pass中按照逐畫素方式處理。

如果場景中沒有任何平行光,那麼Base Pass會當成全黑的光源處理。

對於Base Pass來說,它處理的逐畫素光源型別一定是平行光。我們可以使用__WorldSpaceLightPos0來得到這個平行光的方向(位置對平行光來說沒有意義),使用_LightColor0來得到它的顏色和強度(_LightColor0已經是顏色和強度相乘後的結果),由於平行光可以認為是沒有衰減的,因此這裡我們直接令衰減值為1.0。相關程式碼如下:

        //  Compute  diffuse  term
        fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * max(0, dot(worldNormal, worldLightDir));

        ...

        //  Compute  specular  term
        fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(worldNormal, halfDir)),
        _Gloss);

        //  The  attenuation  of  directional  light  is  always  1
        fixed  atten  =  1.0;

        return  fixed4(ambient  +  (diffuse  +  specular)  *  atten,  1.0);

接下來,我們需要為場景中其他逐畫素光源定義Additional Pass。為此,我們首先需要設定Pass的渲染路徑標籤:

        Pass  {
            //  Pass  for  other  pixel  lights
            Tags  {  "LightMode"="ForwardAdd"  }

            Blend  One  One

            CGPROGRAM

            //  Apparently  need  to  add  this  declaration
            #pragma  multi_compile_fwdadd

與Base Pass不同的是,我們還使用Blend命令開啟和設定了混合模式。這是因為,我們希望Additional Pass計算得到的光照結果可以在幀快取中與之前的光照結果進行疊加。如果沒有使用Blend命令的話,Additional Pass會直接覆蓋掉之前的光照結果。在本例中,我們選擇的混合係數是Blend One One,這不是必需的,我們可以設定成Unity支援的任何混合係數。常見的還有Blend SrcAlpha One。

通常來說,Additional Pass的光照處理和Base Pass的處理方式是一樣的,因此我們只需要把Base Pass的頂點和片元著色器程式碼貼上到Additional Pass中,然後再稍微修改一下即可。這些修改往往是為了去掉Base Pass中環境光、自發光、逐頂點光照、SH光照的部分,並新增一些對不同光源型別的支援。因此,在Additional Pass的片元著色器中,我們沒有再計算場景中的環境光。

因此在計算光源的5個屬性——位置、方向、顏色、強度以及衰減時,顏色和強度我們仍然可以使用_LightColor0來得到,但對於位置、方向和衰減屬性,我們就需要根據光源型別分別計算。首先,我們來看如何計算不同光源的方向:

        #ifdef  USING_DIRECTIONAL_LIGHT
              fixed3  worldLightDir  =  normalize(_WorldSpaceLightPos0.xyz);
        #else
              fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz - i.worldPosition.xyz);
        #endif

處理不同光源的衰減:

        #ifdef  USING_DIRECTIONAL_LIGHT
            fixed  atten  =  1.0;
        #else
            float3  lightCoord  =  mul(_LightMatrix0,  float4(i.worldPosition,  1)).xyz;
            fixed atten = tex2D(_LightTexture0, dot(lightCoord, lightCoord).rr).UNITY_ATTEN_CHANNEL;
        #endif

我們同樣透過判斷是否定義了USING_DIRECTIONAL_LIGHT來決定當前處理的光源型別。如果是平行光的話,衰減值為1.0。如果是其他光源型別,那麼處理更復雜一些。儘管我們可以使用數學表示式來計算給定點相對於點光源和聚光燈的衰減,但這些計算往往涉及開根號、除法等計算量相對較大的操作,因此Unity選擇了使用一張紋理作為查詢表(Lookup Table, LUT),以在片元著色器中得到光源的衰減。我們首先得到光源空間下的座標,然後使用該座標對衰減紋理進行取樣得到衰減值。

image-20240923080110029

注:本文為馮樂樂《Unity Shader入門精要讀書筆記》

相關文章