跳转到内容

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

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