跳轉到內容

Cg 程式設計/Unity/Cookie

來自 Wikibooks,開放世界中的開放書籍
光源前方的 Gobo 的示意圖。
Cookie 的實際應用:類似於 Gobo 但離光源更遠。

本教程涵蓋了光空間中的投影紋理對映,這對實現聚光燈和方向光源的 Cookie 非常有用。(實際上,Unity 對任何聚光燈都使用內建 Cookie。)在 Unity 中,許多內建著色器可以處理 Cookie;本教程展示了它是如何工作的。

本教程基於“光滑鏡面高光”“透明紋理”部分的程式碼。如果你還沒有閱讀這些教程,建議先閱讀。

現實生活中的 Gobo 和 Cookie

[編輯 | 編輯原始碼]

在現實生活中,Gobo 是放置在光源前面的帶孔的固體材料(通常是金屬),用於控制光束或陰影的形狀。Cookie(或“cuculoris”)的作用類似,但它們放置在離光源更遠的地方,如左側影像所示。

Unity 的 Cookie

[編輯 | 編輯原始碼]

在 Unity 中,每個光源在Inspector 視窗(選擇光源時)中都可以指定一個Cookie。這個 Cookie 實際上是一個 Alpha 紋理貼圖(參見“透明紋理”),它被放置在光源前面並隨光源移動(因此它實際上類似於 Gobo)。它允許光線穿過紋理影像 Alpha 分量為 1 的區域,並在 Alpha 分量為 0 的區域阻擋光線。Unity 對聚光燈和方向光源的 Cookie 只是方形的二維 Alpha 紋理貼圖。另一方面,點光源的 Cookie 是立方體貼圖,這裡不再討論。

為了實現 Cookie,我們必須擴充套件任何應該受到 Cookie 影響的表面的著色器。(這與 Unity 的投影機工作方式截然不同;請參見“投影機”部分。)具體來說,我們必須根據著色器照明計算中光源的 Cookie 對每個光源的光線進行衰減。這裡,我們使用“光滑鏡面高光”部分中描述的逐畫素照明;但是,該技術可以應用於任何照明計算。

為了找到 Cookie 紋理中的相關位置,必須將表面柵格化點的座標轉換為光源的座標系。這個座標系非常類似於攝像頭的裁剪座標系,如“頂點變換”部分所述。實際上,理解光源座標系的最佳方式可能是將其視為攝像機。然後,xy 光座標與該假設攝像機的螢幕座標相關。從世界座標系到光座標系的點變換實際上非常容易,因為 Unity 提供了所需的 4×4 矩陣作為統一變數 _LightMatrix0。(否則,我們必須設定類似於檢視變換和投影矩陣的矩陣,這些矩陣在“頂點變換”部分中進行了討論。)

為了獲得最佳效率,應透過將 _LightMatrix0 乘以世界空間中的位置(在頂點著色器中)來執行表面點從世界空間到光空間的變換,例如:

         ...
         uniform float4x4 _LightMatrix0; // transformation 
            // from world to light space (from Autolight.cginc)
         ...

         struct vertexInput {
            float4 vertex : POSITION;
            float3 normal : NORMAL;
         };
         struct vertexOutput {
            float4 pos : SV_POSITION;
            float4 posWorld : TEXCOORD0;
               // position of the vertex (and fragment) in world space 
            float4 posLight : TEXCOORD1;
               // position of the vertex (and fragment) in light space
            float3 normalDir : TEXCOORD2;
               // surface normal vector in world space
         };
 
         vertexOutput vert(vertexInput input) 
         {
            vertexOutput output;
 
            float4x4 modelMatrix = unity_ObjectToWorld;
            float4x4 modelMatrixInverse = unity_WorldToObject;

            output.posWorld = mul(modelMatrix, input.vertex);
            output.posLight = mul(_LightMatrix0, output.posWorld);
            output.normalDir = normalize(
               mul(float4(input.normal, 0.0), modelMatrixInverse).xyz);
            output.pos = UnityObjectToClipPos(input.vertex);
            return output;
         }

除了統一變數 _LightMatrix0 的定義,以及新的輸出引數 posLight 和計算 posLight 的指令之外,這與“光滑鏡面高光”部分中的頂點著色器相同。

方向光源的 Cookie

[編輯 | 編輯原始碼]

對於方向光源的 Cookie,我們可以直接在片段著色器中使用 posLight 中的 xy 光座標作為 Cookie 紋理 _LightTexture0 的紋理座標進行查詢。然後,將得到的 Alpha 分量乘以計算出的照明;例如:

            // compute diffuseReflection and specularReflection

            float cookieAttenuation = 1.0;
            if (0.0 == _WorldSpaceLightPos0.w) // directional light?
            {
               cookieAttenuation = 
                  tex2D(_LightTexture0, input.posLight.xy).a;
            }
            // compute cookieAttenuation for spotlights here

            return float4(cookieAttenuation 
               * (diffuseReflection + specularReflection), 1.0);

聚光燈的 Cookie

[編輯 | 編輯原始碼]

對於聚光燈,必須將 posLight 中的 xy 光座標除以 w 光座標。這種除法是投影紋理對映的特徵,與攝像機的透視除法相對應,如“頂點變換”部分所述。Unity 定義矩陣 _LightMatrix0 使我們必須在除法後將兩個座標都加上

               cookieAttenuation = tex2D(_LightTexture0, 
                  input.posLight.xy / input.posLight.w 
                  + float2(0.5, 0.5)).a;

對於某些 GPU,使用內建函式 tex2Dproj 可能更有效,它接受 float3 中的三個紋理座標,並在紋理查詢之前將前兩個座標除以第三個座標。這種方法的一個問題是,我們必須在除以 posLight.w 後加上;但是,tex2Dproj 不允許我們在內部除以第三個紋理座標之後新增任何內容。解決方案是在除以 posLight.w 之前加上 0.5 * input.posLight.w,這相當於在除法之後加上

               float3 textureCoords = float3(
                  input.posLight.x + 0.5 * input.posLight.w, 
                  input.posLight.y + 0.5 * input.posLight.w,
                  input.posLight.w);
               cookieAttenuation = 
                  tex2Dproj(_LightTexture0, textureCoords).a;

請注意,方向光源的紋理查詢也可以透過將 textureCoords 設定為 float3(input.posLight.xy, 1.0) 來使用 tex2Dproj 實現。這將允許我們對方向光源和聚光燈使用同一個紋理查詢,在某些 GPU 上效率更高。

有時,投影紋理對映會產生一個令人不快的副作用:在投影的邊緣,GPU 使用高 mip 級別,這會導致可見的邊界(尤其是對於具有鉗位紋理座標的紋理貼圖)。避免這種情況最簡單的方法是停用紋理影像的 mip 對映:在Project 視窗中找到並選擇紋理影像;然後在Inspector 視窗中將紋理型別設定為高階,並取消選中生成 mip 對映。不要忘記點選應用按鈕。

完整的著色器程式碼

[編輯 | 編輯原始碼]

對於完整的著色器程式碼,我們使用“光滑鏡面高光”部分中 ForwardBase 通道的簡化版本,因為 Unity 只在 ForwardBase 通道中使用沒有 Cookie 的方向光源。所有帶有 Cookie 的光源都由 ForwardAdd 通道處理。我們忽略了點光源的 Cookie,因為 _LightMatrix0[3][3]1.0(但我們將其包含在下一部分中)。聚光燈始終具有 Cookie 紋理:如果使用者沒有指定 Cookie,則 Unity 會提供一個 Cookie 紋理來生成聚光燈的形狀;因此,始終應用 Cookie 是可以的。方向光源並非始終具有 Cookie;但是,如果只有一個沒有 Cookie 的方向光源,則它已在 ForwardBase 通道中處理。因此,除非有多個沒有 Cookie 的方向光源,否則我們可以假設 ForwardAdd 通道中的所有方向光源都有 Cookie。在這種情況下,完整的著色器程式碼可能是

Shader "Cg per-pixel lighting with cookies" {
   Properties {
      _Color ("Diffuse Material Color", Color) = (1,1,1,1) 
      _SpecColor ("Specular Material Color", Color) = (1,1,1,1) 
      _Shininess ("Shininess", Float) = 10
   }
   SubShader {
      Pass {    
         Tags { "LightMode" = "ForwardBase" } // pass for ambient light 
            // and first directional light source without cookie
 
         CGPROGRAM
 
         #pragma vertex vert  
         #pragma fragment frag 
 
         #include "UnityCG.cginc"
         uniform float4 _LightColor0; 
            // color of light source (from "Lighting.cginc")
 
         // User-specified properties
         uniform float4 _Color; 
         uniform float4 _SpecColor; 
         uniform float _Shininess;
 
         struct vertexInput {
            float4 vertex : POSITION;
            float3 normal : NORMAL;
         };
         struct vertexOutput {
            float4 pos : SV_POSITION;
            float4 posWorld : TEXCOORD0;
            float3 normalDir : TEXCOORD1;
         };
 
         vertexOutput vert(vertexInput input) 
         {
            vertexOutput output;
 
            float4x4 modelMatrix = unity_ObjectToWorld;
            float4x4 modelMatrixInverse = unity_WorldToObject; 
 
            output.posWorld = mul(modelMatrix, input.vertex);
            output.normalDir = normalize(
               mul(float4(input.normal, 0.0), modelMatrixInverse).xyz);
            output.pos = UnityObjectToClipPos(input.vertex);
            return output;
         }
 
         float4 frag(vertexOutput input) : COLOR
         {
            float3 normalDirection = normalize(input.normalDir);
 
            float3 viewDirection = normalize(
               _WorldSpaceCameraPos - input.posWorld.xyz);
            float3 lightDirection = 
               normalize(_WorldSpaceLightPos0.xyz);
 
            float3 ambientLighting = 
               UNITY_LIGHTMODEL_AMBIENT.rgb * _Color.rgb;
 
            float3 diffuseReflection = 
               _LightColor0.rgb * _Color.rgb
               * max(0.0, dot(normalDirection, lightDirection));
 
            float3 specularReflection;
            if (dot(normalDirection, lightDirection) < 0.0) 
               // light source on the wrong side?
            {
               specularReflection = float3(0.0, 0.0, 0.0); 
                  // no specular reflection
            }
            else // light source on the right side
            {
               specularReflection = _LightColor0.rgb 
                  * _SpecColor.rgb * pow(max(0.0, dot(
                  reflect(-lightDirection, normalDirection), 
                  viewDirection)), _Shininess);
            }
 
            return float4(ambientLighting + diffuseReflection 
               + specularReflection, 1.0);
         }
 
         ENDCG
      }

      Pass {    
         Tags { "LightMode" = "ForwardAdd" } 
            // pass for additional light sources
         Blend One One // additive blending 
 
         CGPROGRAM
 
         #pragma vertex vert  
         #pragma fragment frag 
 
         #include "UnityCG.cginc"
         uniform float4 _LightColor0; 
            // color of light source (from "Lighting.cginc")
         uniform float4x4 _LightMatrix0; // transformation 
            // from world to light space (from Autolight.cginc)
         uniform sampler2D _LightTexture0; 
            // cookie alpha texture map (from Autolight.cginc)
 
         // User-specified properties
         uniform float4 _Color; 
         uniform float4 _SpecColor; 
         uniform float _Shininess;
 
         struct vertexInput {
            float4 vertex : POSITION;
            float3 normal : NORMAL;
         };
         struct vertexOutput {
            float4 pos : SV_POSITION;
            float4 posWorld : TEXCOORD0;
               // position of the vertex (and fragment) in world space 
            float4 posLight : TEXCOORD1;
               // position of the vertex (and fragment) in light space
            float3 normalDir : TEXCOORD2;
               // surface normal vector in world space
         };
 
         vertexOutput vert(vertexInput input) 
         {
            vertexOutput output;
 
            float4x4 modelMatrix = unity_ObjectToWorld;
            float4x4 modelMatrixInverse = unity_WorldToObject;

            output.posWorld = mul(modelMatrix, input.vertex);
            output.posLight = mul(_LightMatrix0, output.posWorld);
            output.normalDir = normalize(
               mul(float4(input.normal, 0.0), modelMatrixInverse).xyz);
            output.pos = UnityObjectToClipPos(input.vertex);
            return output;
         }
 
         float4 frag(vertexOutput input) : COLOR
         {
            float3 normalDirection = normalize(input.normalDir);
 
            float3 viewDirection = normalize(
               _WorldSpaceCameraPos - input.posWorld.xyz);
            float3 lightDirection;
            float attenuation;
 
            if (0.0 == _WorldSpaceLightPos0.w) // directional light?
            {
               attenuation = 1.0; // no attenuation
               lightDirection = normalize(_WorldSpaceLightPos0.xyz);
            } 
            else // point or spot light
            {
               float3 vertexToLightSource = 
                  _WorldSpaceLightPos0.xyz - input.posWorld.xyz;
               float distance = length(vertexToLightSource);
               attenuation = 1.0 / distance; // linear attenuation 
               lightDirection = normalize(vertexToLightSource);
            }
 
            float3 diffuseReflection = 
               attenuation * _LightColor0.rgb * _Color.rgb
               * max(0.0, dot(normalDirection, lightDirection));
 
            float3 specularReflection;
            if (dot(normalDirection, lightDirection) < 0.0) 
               // light source on the wrong side?
            {
               specularReflection = float3(0.0, 0.0, 0.0); 
                  // no specular reflection
            }
            else // light source on the right side
            {
               specularReflection = attenuation * _LightColor0.rgb 
                  * _SpecColor.rgb * pow(max(0.0, dot(
                  reflect(-lightDirection, normalDirection), 
                  viewDirection)), _Shininess);
            }
 
            float cookieAttenuation = 1.0;
            if (0.0 == _WorldSpaceLightPos0.w) // directional light?
            {
               cookieAttenuation = tex2D(_LightTexture0, 
                  input.posLight.xy).a;
            }
            else if (1.0 != _LightMatrix0[3][3]) 
               // spotlight (i.e. not a point light)?
            {
               cookieAttenuation = tex2D(_LightTexture0, 
                  input.posLight.xy / input.posLight.w 
                  + float2(0.5, 0.5)).a;
            }

            return float4(cookieAttenuation 
               * (diffuseReflection + specularReflection), 1.0);
         }
 
         ENDCG
      }
   }
   Fallback "Specular"
}

特定光源的著色器程式

[編輯 | 編輯原始碼]

之前的著色器程式碼僅限於最多包含一個無 Cookie 的方向光源的場景。此外,它沒有考慮點光源的 Cookie。編寫更通用的著色器程式碼需要為不同的光源使用不同的 ForwardAdd 通道。(請記住,ForwardBase 通道中的光源始終是無 Cookie 的方向光源。)幸運的是,Unity 提供了一種使用以下 Unity 特定指令生成多個著色器的方法(在 ForwardAdd 通道的 CGPROGRAM 之後)

#pragma multi_compile_lightpass

使用此指令,Unity 將為不同型別的光源多次編譯 ForwardAdd 通道的著色器程式碼。每次編譯都透過定義以下符號之一來區分:DIRECTIONALDIRECTIONAL_COOKIEPOINTPOINT_NOATTPOINT_COOKIESPOT。著色器程式碼應檢查定義了哪個符號(使用指令 #if defined ... #elif defined ... #endif)幷包含相應的指令。例如

Shader "Cg per-pixel lighting with cookies" {
   Properties {
      _Color ("Diffuse Material Color", Color) = (1,1,1,1) 
      _SpecColor ("Specular Material Color", Color) = (1,1,1,1) 
      _Shininess ("Shininess", Float) = 10
   }
   SubShader {
      Pass {    
         Tags { "LightMode" = "ForwardBase" } // pass for ambient light 
            // and first directional light source without cookie
 
         CGPROGRAM
 
         #pragma vertex vert  
         #pragma fragment frag 
 
         #include "UnityCG.cginc"
         uniform float4 _LightColor0; 
            // color of light source (from "Lighting.cginc")
 
         // User-specified properties
         uniform float4 _Color; 
         uniform float4 _SpecColor; 
         uniform float _Shininess;
 
         struct vertexInput {
            float4 vertex : POSITION;
            float3 normal : NORMAL;
         };
         struct vertexOutput {
            float4 pos : SV_POSITION;
            float4 posWorld : TEXCOORD0;
            float3 normalDir : TEXCOORD1;
         };
 
         vertexOutput vert(vertexInput input) 
         {
            vertexOutput output;
 
            float4x4 modelMatrix = unity_ObjectToWorld;
            float4x4 modelMatrixInverse = unity_WorldToObject;
 
            output.posWorld = mul(modelMatrix, input.vertex);
            output.normalDir = normalize(
               mul(float4(input.normal, 0.0), modelMatrixInverse).xyz);
            output.pos = UnityObjectToClipPos(input.vertex);
            return output;
         }
 
         float4 frag(vertexOutput input) : COLOR
         {
            float3 normalDirection = normalize(input.normalDir);
 
            float3 viewDirection = normalize(
               _WorldSpaceCameraPos - input.posWorld.xyz);
            float3 lightDirection = 
               normalize(_WorldSpaceLightPos0.xyz);
 
            float3 ambientLighting = 
               UNITY_LIGHTMODEL_AMBIENT.rgb * _Color.rgb;
 
            float3 diffuseReflection = 
               _LightColor0.rgb * _Color.rgb
               * max(0.0, dot(normalDirection, lightDirection));
 
            float3 specularReflection;
            if (dot(normalDirection, lightDirection) < 0.0) 
               // light source on the wrong side?
            {
               specularReflection = float3(0.0, 0.0, 0.0); 
                  // no specular reflection
            }
            else // light source on the right side
            {
               specularReflection = _LightColor0.rgb 
                  * _SpecColor.rgb * pow(max(0.0, dot(
                  reflect(-lightDirection, normalDirection), 
                  viewDirection)), _Shininess);
            }
 
            return float4(ambientLighting + diffuseReflection 
               + specularReflection, 1.0);
         }
 
         ENDCG
      }

      Pass {    
         Tags { "LightMode" = "ForwardAdd" } 
            // pass for additional light sources
         Blend One One // additive blending 
 
         CGPROGRAM
 
         #pragma multi_compile_lightpass
 
         #pragma vertex vert  
         #pragma fragment frag 
 
         #include "UnityCG.cginc"
         uniform float4 _LightColor0; 
            // color of light source (from "Lighting.cginc")
         uniform float4x4 _LightMatrix0; // transformation 
            // from world to light space (from Autolight.cginc)
         #if defined (DIRECTIONAL_COOKIE) || defined (SPOT)
            uniform sampler2D _LightTexture0; 
               // cookie alpha texture map (from Autolight.cginc)
         #elif defined (POINT_COOKIE)
            uniform samplerCUBE _LightTexture0; 
               // cookie alpha texture map (from Autolight.cginc)
         #endif
 
         // User-specified properties
         uniform float4 _Color; 
         uniform float4 _SpecColor; 
         uniform float _Shininess;
 
         struct vertexInput {
            float4 vertex : POSITION;
            float3 normal : NORMAL;
         };
         struct vertexOutput {
            float4 pos : SV_POSITION;
            float4 posWorld : TEXCOORD0;
               // position of the vertex (and fragment) in world space 
            float4 posLight : TEXCOORD1;
               // position of the vertex (and fragment) in light space
            float3 normalDir : TEXCOORD2;
               // surface normal vector in world space
         };
 
         vertexOutput vert(vertexInput input) 
         {
            vertexOutput output;
 
            float4x4 modelMatrix = unity_ObjectToWorld;
            float4x4 modelMatrixInverse = unity_WorldToObject;

            output.posWorld = mul(modelMatrix, input.vertex);
            output.posLight = mul(_LightMatrix0, output.posWorld);
            output.normalDir = normalize(
               mul(float4(input.normal, 0.0), modelMatrixInverse).xyz);
            output.pos = UnityObjectToClipPos(input.vertex);
            return output;
         }
 
         float4 frag(vertexOutput input) : COLOR
         {
            float3 normalDirection = normalize(input.normalDir);
 
            float3 viewDirection = normalize(
               _WorldSpaceCameraPos - input.posWorld.xyz);
            float3 lightDirection;
            float attenuation = 1.0;
               // by default no attenuation with distance

            #if defined (DIRECTIONAL) || defined (DIRECTIONAL_COOKIE)
               lightDirection = normalize(_WorldSpaceLightPos0.xyz);
            #elif defined (POINT_NOATT)
               lightDirection = normalize(
                  _WorldSpaceLightPos0 - input.posWorld.xyz);
            #elif defined(POINT)||defined(POINT_COOKIE)||defined(SPOT)
               float3 vertexToLightSource = 
                  _WorldSpaceLightPos0.xyz - input.posWorld.xyz;
               float distance = length(vertexToLightSource);
               attenuation = 1.0 / distance; // linear attenuation 
               lightDirection = normalize(vertexToLightSource);
            #endif
 
            float3 diffuseReflection = 
               attenuation * _LightColor0.rgb * _Color.rgb
               * max(0.0, dot(normalDirection, lightDirection));
 
            float3 specularReflection;
            if (dot(normalDirection, lightDirection) < 0.0) 
               // light source on the wrong side?
            {
               specularReflection = float3(0.0, 0.0, 0.0); 
                  // no specular reflection
            }
            else // light source on the right side
            {
               specularReflection = attenuation * _LightColor0.rgb 
                  * _SpecColor.rgb * pow(max(0.0, dot(
                  reflect(-lightDirection, normalDirection), 
                  viewDirection)), _Shininess);
            }
 
            float cookieAttenuation = 1.0; 
               // by default no cookie attenuation
            #if defined (DIRECTIONAL_COOKIE)
               cookieAttenuation = tex2D(_LightTexture0, 
                  input.posLight.xy).a;
            #elif defined (POINT_COOKIE)
               cookieAttenuation = texCUBE(_LightTexture0, 
                  input.posLight.xyz).a;
            #elif defined (SPOT)
               cookieAttenuation = tex2D(_LightTexture0, 
                  input.posLight.xy / input.posLight.w 
                  + float2(0.5, 0.5)).a;
            #endif

            return float4(cookieAttenuation 
               * (diffuseReflection + specularReflection), 1.0);
         }
 
         ENDCG
      }
   }
   Fallback "Specular"
}

請注意,點光源的 Cookie 使用立方體紋理貼圖。這種紋理貼圖在 “反射表面”部分 中討論。

總結

[edit | edit source]

恭喜,您已經瞭解了投影紋理對映最重要的方面。我們已經看到了

  • 如何為方向光源實現 Cookie。
  • 如何實現聚光燈(有和沒有使用者指定的 Cookie)。
  • 如何為不同的光源實現不同的著色器。

進一步閱讀

[edit | edit source]

如果您還想了解更多

  • 關於沒有 Cookie 的光源的著色器版本,您應該閱讀 “平滑鏡面高光”部分
  • 關於紋理對映,特別是 alpha 紋理貼圖,您應該閱讀 “透明紋理”部分
  • 關於固定功能 OpenGL 中的投影紋理對映,您可以閱讀 NVIDIA 的白皮書“投影紋理對映”,作者是 Cass Everitt(可 線上獲取)。

< Cg Programming/Unity

除非另有說明,否則本頁上的所有示例原始碼均授予公共領域。
華夏公益教科書