Cg 编程/Unity/Cookie
本教程涵盖了光空间中的投影纹理映射,这对实现聚光灯和方向光源的 Cookie 非常有用。(实际上,Unity 对任何聚光灯都使用内置 Cookie。)在 Unity 中,许多内置着色器可以处理 Cookie;本教程展示了它是如何工作的。
本教程基于“光滑镜面高光”和“透明纹理”部分的代码。如果你还没有阅读这些教程,建议先阅读。
在现实生活中,Gobo 是放置在光源前面的带孔的固体材料(通常是金属),用于控制光束或阴影的形状。Cookie(或“cuculoris”)的作用类似,但它们放置在离光源更远的地方,如左侧图像所示。
在 Unity 中,每个光源在Inspector 窗口(选择光源时)中都可以指定一个Cookie。这个 Cookie 实际上是一个 Alpha 纹理贴图(参见“透明纹理”),它被放置在光源前面并随光源移动(因此它实际上类似于 Gobo)。它允许光线穿过纹理图像 Alpha 分量为 1 的区域,并在 Alpha 分量为 0 的区域阻挡光线。Unity 对聚光灯和方向光源的 Cookie 只是方形的二维 Alpha 纹理贴图。另一方面,点光源的 Cookie 是立方体贴图,这里不再讨论。
为了实现 Cookie,我们必须扩展任何应该受到 Cookie 影响的表面的着色器。(这与 Unity 的投影机工作方式截然不同;请参见“投影机”部分。)具体来说,我们必须根据着色器照明计算中光源的 Cookie 对每个光源的光线进行衰减。这里,我们使用“光滑镜面高光”部分中描述的逐像素照明;但是,该技术可以应用于任何照明计算。
为了找到 Cookie 纹理中的相关位置,必须将表面栅格化点的坐标转换为光源的坐标系。这个坐标系非常类似于摄像头的裁剪坐标系,如“顶点变换”部分所述。实际上,理解光源坐标系的最佳方式可能是将其视为摄像机。然后,x 和 y 光坐标与该假设摄像机的屏幕坐标相关。从世界坐标系到光坐标系的点变换实际上非常容易,因为 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,我们可以直接在片段着色器中使用 posLight
中的 x 和 y 光坐标作为 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);
对于聚光灯,必须将 posLight
中的 x 和 y 光坐标除以 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
通道的着色器代码。每次编译都通过定义以下符号之一来区分:DIRECTIONAL
、DIRECTIONAL_COOKIE
、POINT
、POINT_NOATT
、POINT_COOKIE
、SPOT
。着色器代码应检查定义了哪个符号(使用指令 #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(可 在线获取)。