本教程涵盖了光照空间中的投影纹理映射,它对实现聚光灯和方向光下的cookies很有用。(实际上,Unity会为任何聚光灯使用一个内置cookie。)
本教程是基于章节“光滑镜面高光”和“透明纹理”的代码。如果你没有阅读过这些章节,你应该先阅读一下。
现在生活中的镜头挡光板和Cookies
在现实生活中,镜头挡光板是一件有孔的材料(通常是块金属),它被放置在光源前面来操纵光束或阴影的形状。Cookies(或叫“cuculoris”)也具有类似用途,如图所示,它被放置在距离光源很远的地方。
Unity中的Cookies
在Unity中,当光源被选中时在Inspector Window中可以为每个光源指定一个cookie。这个cookie基本上就是一张alpha纹理贴图(查看章节“透明纹理”),它被放置在光源的前面并且沿着它移动(因此这跟镜头挡光板类似)。它使得光可以穿过纹理贴图的alpha分量为1的地方并且在alpha分量为0的地方屏蔽它。Unity关于聚光灯和方向光的cookies是正方形、二维的alpha纹理贴图。另一方面,点光源的cookies是立方体纹理,这个我们就不在这边介绍了。
为了实现一个cookie,我们必须扩展被cookie影响的任意表面的着色器。(这跟Untiy的投影工作是完全不一样的;参考章节“投影”。)我们必须根据着色器中光照计算的cookie来衰减每个光源的光。这里,我们使用章节“光滑镜面高光”中提到的逐像素光照;但是,这个技术可以被用到任何光照计算中。
为了找到cookie纹理中的相关位置,一个曲面光栅化点的位置被变换到光源的坐标系中。这个坐标系类似于摄像机的裁剪坐标系,它在章节“顶点变换”中有所描述。实际上,考虑坐标系的最好方法就是把光源看到摄像机。x和y光坐标跟这个假想摄像机的屏幕坐标相关。从世界坐标变换一个点到光源坐标实际非常简单,因为Unity提供了所需的4×4矩阵作为uniform变量_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 = _Object2World;
float4x4 modelMatrixInverse = _World2Object;
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 = mul(UNITY_MATRIX_MVP, input.vertex);
return output;
}
除了uniform_LightMatrix0
的定义,新的输出参数posLight
以及计算posLight
的指令,其它跟章节“光滑镜面高光”中的顶点着色器一样。
方向光源的Cookies
对于方向光源的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);
聚光灯的cookie
对于聚光灯来说,posLight
中的x和y光坐标必须除以w光坐标。这个除法的特点是投影纹理映射以及相应于摄像机透视除法,它在章节“顶点变换”中有所描述。Unity定义了矩阵_LightMatrix0
,这样我们就可以在除法之后把0.5加到两个坐标上去:
cookieAttenuation = tex2D(_LightTexture0,input.posLight.xy / input.posLight.w
+ float2(0.5, 0.5)).a;
对于一些GPU来说,使用内置函数tex2Dproj
会更有效率,它使用三个float3的纹理坐标并且在纹理查询前把前面两个坐标除以第三个坐标。这个方法的问题在于我们必须在除以posLight.w
之后加上0.5;但是,tex2Dproj
并不允许我们在内部除以第三个纹理坐标后加入任何东西。解决方案就是在除以posLight.w
后加入0.5 * input.posLight.w
,它对应于在除法后加入0.5:
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 map层级,它会导致一个可见的边界(特别对于被限制纹理坐标的纹理贴图)。避免这个最简单的办法就是不激活纹理贴图的mip map:在Project Window中找到并且选中纹理贴图;然后在Inspector Window中设置Texture Type为Advanced,并且不勾选Generate Mip Maps。不要忘记点击Apply按钮。
完整的着色器代码
对于完整的着色器代码我们使用了章节“光滑镜面高光”的ForwardBase
通道的简单版本,因为Unity在ForwardBase
通道中只使用没有cookie的方向光。所有有cookies的光源会被ForwardBase
通道处理。我们忽略了聚光灯的cookies,_LightMatrix0[3][3]
等于1.0(但是我们会在下章中介绍)。聚光灯总会有一张cookie纹理;如果用户没有指定,Unity会提供一张cookie纹理来生成聚光灯的形状;于是,应用cookie总是没问题的。方向光不总是有cookie;但是,如果只有一个方向光源没有cookie,它会在ForwardBase
通道中被处理。于是,除非有超过一个光源没有cookies,我们能够假设所在在ForwardAdd
通道中的方向光源都有cookies。在这种情况下,完整的着色器代码如下:
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 = _Object2World;
float4x4 modelMatrixInverse = _World2Object;
output.posWorld = mul(modelMatrix, input.vertex);
output.normalDir = normalize(
mul(float4(input.normal, 0.0), modelMatrixInverse).xyz);
output.pos = mul(UNITY_MATRIX_MVP, 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 = _Object2World;
float4x4 modelMatrixInverse = _World2Object;
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 = mul(UNITY_MATRIX_MVP, 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是使用立方体贴图的。这种类型的贴图会在章节“反射曲面”中讨论。
总结
恭喜,你学习了投影纹理映射中最重要的部分。我们学到了:
- 如何实现方向光源的cookies。
- 如何实现聚光灯的cookies(有和没有用户自定义cookies)。
- 如何对不同的光源实现不同的着色器。