跳轉到內容

GLSL 程式設計/Unity/Cookie

來自華夏公益教科書
光源前 Gobo 的示意圖。
Cookie 在實際應用中:類似於 Gobo 但距離光源更遠。

本教程涵蓋了光空間中的投影紋理對映,它有助於為聚光燈和方向光源實現 Cookie。(事實上,Unity 對任何聚光燈都使用內建 Cookie。)

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

現實生活中的 Gobo 和 Cookie

[編輯 | 編輯原始碼]

在現實生活中,Gobo 是放置在光源前方的帶有孔洞的材料(通常是金屬),用於操縱光束或陰影的形狀。Cookie(或“cuculoris”)的功能類似,但放置在遠離光源的位置,如左側影像所示。

Unity 的 Cookie

[編輯 | 編輯原始碼]

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

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

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

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

         uniform mat4 _Object2World; // model matrix
         uniform mat4 _World2Object; // inverse model matrix
         uniform vec4 _WorldSpaceLightPos0; 
            // direction to or position of light source
         uniform vec4 _LightColor0; 
            // color of light source (from Lighting.cginc)
         uniform mat4 _LightMatrix0; // transformation 
            // from world to light space (from Autolight.cginc)
 
         varying vec4 position; 
            // position of the vertex (and fragment) in world space 
         varying vec4 positionInLightSpace; 
            // position of the vertex (and fragment) in light space
         varying vec3 varyingNormalDirection; 
            // surface normal vector in world space
 
         #ifdef VERTEX
 
         void main()
         {                                
            mat4 modelMatrix = _Object2World;
            mat4 modelMatrixInverse = _World2Object; // unity_Scale.w 
               // is unnecessary because we normalize vectors
 
            position = modelMatrix * gl_Vertex;
            positionInLightSpace = _LightMatrix0 * position;
            varyingNormalDirection = normalize(vec3(
               vec4(gl_Normal, 0.0) * modelMatrixInverse));
            
            gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
         }
 
         #endif

除了統一變數_LightMatrix0和變化量positionInLightSpace的定義以及計算positionInLightSpace的指令外,這與“平滑鏡面高光”部分中的頂點著色器相同。

用於方向光源的 Cookie

[編輯 | 編輯原始碼]

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

            // compute diffuseReflection and specularReflection

            float cookieAttenuation = 1.0;
            if (0.0 == _WorldSpaceLightPos0.w) // directional light?
            {
               cookieAttenuation = texture2D(_LightTexture0, 
                  vec2(positionInLightSpace)).a;
            }
            // compute cookieAttenuation for spotlights here

            gl_FragColor = vec4(cookieAttenuation 
               * (diffuseReflection + specularReflection), 1.0);

除了vec2(positionInLightSpace),我們還可以使用positionInLightSpace.xy來獲取一個二維向量,其中包含光空間中的xy 座標。

用於聚光燈的 Cookie

[編輯 | 編輯原始碼]

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

               cookieAttenuation = texture2D(_LightTexture0, 
                  vec2(positionInLightSpace) / positionInLightSpace.w 
                  + vec2(0.5)).a;

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

               vec3 textureCoords = vec3(vec2(positionInLightSpace) 
                  + vec2(0.5 * positionInLightSpace.w), 
                  positionInLightSpace.w);
               cookieAttenuation = 
                  texture2DProj(_LightTexture0, textureCoords).a;

請注意,透過將textureCoords設定為vec3(vec2(positionInLightSpace), 1.0),也可以使用texture2DProj實現方向光的紋理查詢。這將允許我們對方向光和聚光燈使用相同的紋理查詢,這在某些 GPU 上更高效。

完整著色器程式碼

[編輯 | 編輯原始碼]

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

Shader "GLSL 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
 
         GLSLPROGRAM
 
         // User-specified properties
         uniform vec4 _Color; 
         uniform vec4 _SpecColor; 
         uniform float _Shininess;
 
         // The following built-in uniforms (except _LightColor0) 
         // are also defined in "UnityCG.glslinc", 
         // i.e. one could #include "UnityCG.glslinc" 
         uniform vec3 _WorldSpaceCameraPos; 
            // camera position in world space
         uniform mat4 _Object2World; // model matrix
         uniform mat4 _World2Object; // inverse model matrix
         uniform vec4 _WorldSpaceLightPos0; 
            // direction to or position of light source
         uniform vec4 _LightColor0; 
            // color of light source (from Lighting.cginc)
 
         varying vec4 position; 
            // position of the vertex (and fragment) in world space 
         varying vec3 varyingNormalDirection; 
            // surface normal vector in world space
 
         #ifdef VERTEX
 
         void main()
         {                                
            mat4 modelMatrix = _Object2World;
            mat4 modelMatrixInverse = _World2Object; // unity_Scale.w 
               // is unnecessary because we normalize vectors
 
            position = modelMatrix * gl_Vertex;
            varyingNormalDirection = normalize(vec3(
               vec4(gl_Normal, 0.0) * modelMatrixInverse));
 
            gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
         }
 
         #endif
 
         #ifdef FRAGMENT
 
         void main()
         {
            vec3 normalDirection = normalize(varyingNormalDirection);
 
            vec3 viewDirection = 
               normalize(_WorldSpaceCameraPos - vec3(position));
            vec3 lightDirection = 
               normalize(vec3(_WorldSpaceLightPos0));

            vec3 ambientLighting = 
               vec3(gl_LightModel.ambient) * vec3(_Color);
 
            vec3 diffuseReflection = vec3(_LightColor0) * vec3(_Color) 
               * max(0.0, dot(normalDirection, lightDirection));
 
            vec3 specularReflection;
            if (dot(normalDirection, lightDirection) < 0.0) 
               // light source on the wrong side?
            {
               specularReflection = vec3(0.0, 0.0, 0.0); 
                  // no specular reflection
            }
            else // light source on the right side
            {
               specularReflection = vec3(_LightColor0) 
                  * vec3(_SpecColor) * pow(max(0.0, dot(
                  reflect(-lightDirection, normalDirection), 
                  viewDirection)), _Shininess);
            }
 
            gl_FragColor = vec4(ambientLighting 
               + diffuseReflection + specularReflection, 1.0);
         }
 
         #endif
 
         ENDGLSL
      }
 
      Pass {      
         Tags { "LightMode" = "ForwardAdd" } 
            // pass for additional light sources
         Blend One One // additive blending 
 
         GLSLPROGRAM
 
         // User-specified properties
         uniform vec4 _Color; 
         uniform vec4 _SpecColor; 
         uniform float _Shininess;
 
         // The following built-in uniforms (except _LightColor0) 
         // are also defined in "UnityCG.glslinc", 
         // i.e. one could #include "UnityCG.glslinc" 
         uniform vec3 _WorldSpaceCameraPos; 
            // camera position in world space
         uniform mat4 _Object2World; // model matrix
         uniform mat4 _World2Object; // inverse model matrix
         uniform vec4 _WorldSpaceLightPos0; 
            // direction to or position of light source
         uniform vec4 _LightColor0; 
            // color of light source (from Lighting.cginc)
         
         uniform mat4 _LightMatrix0; // transformation 
            // from world to light space (from Autolight.cginc)
         uniform sampler2D _LightTexture0; 
            // cookie alpha texture map (from Autolight.cginc)
 
         varying vec4 position; 
            // position of the vertex (and fragment) in world space 
         varying vec4 positionInLightSpace; 
            // position of the vertex (and fragment) in light space
         varying vec3 varyingNormalDirection; 
            // surface normal vector in world space
 
         #ifdef VERTEX
 
         void main()
         {                                
            mat4 modelMatrix = _Object2World;
            mat4 modelMatrixInverse = _World2Object; // unity_Scale.w 
               // is unnecessary because we normalize vectors
 
            position = modelMatrix * gl_Vertex;
            positionInLightSpace = _LightMatrix0 * position;
            varyingNormalDirection = normalize(vec3(
               vec4(gl_Normal, 0.0) * modelMatrixInverse));
            
            gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
         }
 
         #endif
 
         #ifdef FRAGMENT
 
         void main()
         {
            vec3 normalDirection = normalize(varyingNormalDirection);
 
            vec3 viewDirection = 
               normalize(_WorldSpaceCameraPos - vec3(position));
            vec3 lightDirection;
            float attenuation;
 
            if (0.0 == _WorldSpaceLightPos0.w) // directional light?
            {
               attenuation = 1.0; // no attenuation
               lightDirection = normalize(vec3(_WorldSpaceLightPos0));
            } 
            else // point or spot light
            {
               vec3 vertexToLightSource = 
                  vec3(_WorldSpaceLightPos0 - position);
               float distance = length(vertexToLightSource);
               attenuation = 1.0 / distance; // linear attenuation 
               lightDirection = normalize(vertexToLightSource);
            }
 
            vec3 diffuseReflection = 
               attenuation * vec3(_LightColor0) * vec3(_Color) 
               * max(0.0, dot(normalDirection, lightDirection));
 
            vec3 specularReflection;
            if (dot(normalDirection, lightDirection) < 0.0) 
               // light source on the wrong side?
            {
               specularReflection = vec3(0.0, 0.0, 0.0); 
                  // no specular reflection
            }
            else // light source on the right side
            {
               specularReflection = attenuation * vec3(_LightColor0) 
                  * vec3(_SpecColor) * pow(max(0.0, dot(
                  reflect(-lightDirection, normalDirection), 
                  viewDirection)), _Shininess);
            }
 
            float cookieAttenuation = 1.0;
            if (0.0 == _WorldSpaceLightPos0.w) // directional light?
            {
               cookieAttenuation = texture2D(_LightTexture0, 
                  vec2(positionInLightSpace)).a;
            }
            else if (1.0 != _LightMatrix0[3][3]) 
               // spotlight (i.e. not a point light)?
            {
               cookieAttenuation = texture2D(_LightTexture0, 
                  vec2(positionInLightSpace) / positionInLightSpace.w 
                  + vec2(0.5)).a;
            }
            gl_FragColor = vec4(cookieAttenuation 
               * (diffuseReflection + specularReflection), 1.0);
        }
 
         #endif
 
         ENDGLSL
      }
   } 
   // The definition of a fallback shader should be commented out 
   // during development:
   // Fallback "Specular"
}

特定光源的著色器程式

[編輯 | 編輯原始碼]

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

#pragma multi_compile_lightpass

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

Shader "GLSL 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
 
         GLSLPROGRAM
 
         // User-specified properties
         uniform vec4 _Color; 
         uniform vec4 _SpecColor; 
         uniform float _Shininess;
 
         // The following built-in uniforms (except _LightColor0) 
         // are also defined in "UnityCG.glslinc", 
         // i.e. one could #include "UnityCG.glslinc" 
         uniform vec3 _WorldSpaceCameraPos; 
            // camera position in world space
         uniform mat4 _Object2World; // model matrix
         uniform mat4 _World2Object; // inverse model matrix
         uniform vec4 _WorldSpaceLightPos0; 
            // direction to or position of light source
         uniform vec4 _LightColor0; 
            // color of light source (from Lighting.cginc)
 
         varying vec4 position; 
            // position of the vertex (and fragment) in world space 
         varying vec3 varyingNormalDirection; 
            // surface normal vector in world space
 
         #ifdef VERTEX
 
         void main()
         {                                
            mat4 modelMatrix = _Object2World;
            mat4 modelMatrixInverse = _World2Object; // unity_Scale.w 
               // is unnecessary because we normalize vectors
 
            position = modelMatrix * gl_Vertex;
            varyingNormalDirection = normalize(vec3(
               vec4(gl_Normal, 0.0) * modelMatrixInverse));
 
            gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
         }
 
         #endif
 
         #ifdef FRAGMENT
 
         void main()
         {
            vec3 normalDirection = normalize(varyingNormalDirection);
 
            vec3 viewDirection = 
               normalize(_WorldSpaceCameraPos - vec3(position));
            vec3 lightDirection = 
               normalize(vec3(_WorldSpaceLightPos0));
 
            vec3 ambientLighting = 
               vec3(gl_LightModel.ambient) * vec3(_Color);
 
            vec3 diffuseReflection = vec3(_LightColor0) * vec3(_Color) 
               * max(0.0, dot(normalDirection, lightDirection));
 
            vec3 specularReflection;
            if (dot(normalDirection, lightDirection) < 0.0) 
               // light source on the wrong side?
            {
               specularReflection = vec3(0.0, 0.0, 0.0); 
                  // no specular reflection
            }
            else // light source on the right side
            {
               specularReflection = vec3(_LightColor0) 
                  * vec3(_SpecColor) * pow(max(0.0, dot(
                  reflect(-lightDirection, normalDirection), 
                  viewDirection)), _Shininess);
            }
 
            gl_FragColor = vec4(ambientLighting 
               + diffuseReflection + specularReflection, 1.0);
         }
 
         #endif
 
         ENDGLSL
      }
 
      Pass {      
         Tags { "LightMode" = "ForwardAdd" } 
            // pass for additional light sources
         Blend One One // additive blending 
 
         GLSLPROGRAM
 
         #pragma multi_compile_lightpass
 
         // User-specified properties
         uniform vec4 _Color; 
         uniform vec4 _SpecColor; 
         uniform float _Shininess;
 
         // The following built-in uniforms (except _LightColor0) 
         // are also defined in "UnityCG.glslinc", 
         // i.e. one could #include "UnityCG.glslinc" 
         uniform vec3 _WorldSpaceCameraPos; 
            // camera position in world space
         uniform mat4 _Object2World; // model matrix
         uniform mat4 _World2Object; // inverse model matrix
         uniform vec4 _WorldSpaceLightPos0; 
            // direction to or position of light source
         uniform vec4 _LightColor0; 
            // color of light source (from Lighting.cginc)
 
         uniform mat4 _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
 
         varying vec4 position; 
            // position of the vertex (and fragment) in world space 
         varying vec4 positionInLightSpace; 
            // position of the vertex (and fragment) in light space
         varying vec3 varyingNormalDirection; 
            // surface normal vector in world space
 
         #ifdef VERTEX
 
         void main()
         {                                
            mat4 modelMatrix = _Object2World;
            mat4 modelMatrixInverse = _World2Object; // unity_Scale.w 
               // is unnecessary because we normalize vectors
 
            position = modelMatrix * gl_Vertex;
            positionInLightSpace = _LightMatrix0 * position;
            varyingNormalDirection = normalize(vec3(
               vec4(gl_Normal, 0.0) * modelMatrixInverse));
 
            gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
         }
 
         #endif
 
         #ifdef FRAGMENT
 
         void main()
         {
            vec3 normalDirection = normalize(varyingNormalDirection);
 
            vec3 viewDirection = 
               normalize(_WorldSpaceCameraPos - vec3(position));
            vec3 lightDirection;
            float attenuation = 1.0; 
               // by default no attenuation with distance
 
            #if defined DIRECTIONAL || defined DIRECTIONAL_COOKIE
               lightDirection = normalize(vec3(_WorldSpaceLightPos0));
            #elif defined POINT_NOATT
               lightDirection = 
                  normalize(vec3(_WorldSpaceLightPos0 - position));
            #elif defined POINT || defined POINT_COOKIE || defined SPOT
               vec3 vertexToLightSource = 
                  vec3(_WorldSpaceLightPos0 - position);
               float distance = length(vertexToLightSource);
               attenuation = 1.0 / distance; // linear attenuation 
               lightDirection = normalize(vertexToLightSource);
            #endif
 
            vec3 diffuseReflection = 
               attenuation * vec3(_LightColor0) * vec3(_Color) 
               * max(0.0, dot(normalDirection, lightDirection));
 
            vec3 specularReflection;
            if (dot(normalDirection, lightDirection) < 0.0) 
               // light source on the wrong side?
            {
               specularReflection = vec3(0.0, 0.0, 0.0); 
                  // no specular reflection
            }
            else // light source on the right side
            {
               specularReflection = attenuation * vec3(_LightColor0) 
                  * vec3(_SpecColor) * pow(max(0.0, dot(
                  reflect(-lightDirection, normalDirection), 
                  viewDirection)), _Shininess);
            }
 
            float cookieAttenuation = 1.0; 
               // by default no cookie attenuation
            #if defined DIRECTIONAL_COOKIE
               cookieAttenuation = texture2D(_LightTexture0, 
                  vec2(positionInLightSpace)).a;
            #elif defined POINT_COOKIE
               cookieAttenuation = textureCube(_LightTexture0, 
                  vec3(positionInLightSpace)).a;
            #elif defined SPOT
               cookieAttenuation = texture2D(_LightTexture0, 
                  vec2(positionInLightSpace) 
                  / positionInLightSpace.w + vec2(0.5)).a;
            #endif
            gl_FragColor = vec4(cookieAttenuation * 
               (diffuseReflection + specularReflection), 1.0);
         }
 
         #endif
 
         ENDGLSL
      }
   } 
   // The definition of a fallback shader should be commented out 
   // during development:
   // Fallback "Specular"
}

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

摘要

[edit | edit source]

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

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

進一步閱讀

[edit | edit source]

如果您仍然想了解更多

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


< GLSL Programming/Unity

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