跳转到内容

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

除非另有说明,否则本页面上的所有示例源代码都归属公共领域。
华夏公益教科书