GLSL 编程/Unity/Cookie
本教程涵盖了光空间中的投影纹理映射,它有助于为聚光灯和方向光源实现 Cookie。(事实上,Unity 对任何聚光灯都使用内置 Cookie。)
本教程基于“平滑镜面高光”部分和“透明纹理”部分的代码。如果你还没有阅读这些教程,你应该先阅读它们。
在现实生活中,Gobo 是放置在光源前方的带有孔洞的材料(通常是金属),用于操纵光束或阴影的形状。Cookie(或“cuculoris”)的功能类似,但放置在远离光源的位置,如左侧图像所示。
在 Unity 中,可以在选择光源时在检查器视图中为每个光源指定一个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 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,我们可以直接在片段着色器中使用positionInLightSpace
中的x 和 y 光坐标作为 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
来获取一个二维向量,其中包含光空间中的x 和 y 坐标。
对于聚光灯,必须将positionInLightSpace
中的x 和 y 光坐标除以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
传递的着色器代码。 每次编译都由以下符号之一的定义来区分:DIRECTIONAL
、DIRECTIONAL_COOKIE
、POINT
、POINT_NOATT
、POINT_COOKIE
、SPOT
。 着色器代码应该检查哪个符号已定义(使用指令 #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(可以在 网上 获取)。