Cg 编程/Unity/投影仪
本教程介绍了 **投影纹理映射投影仪**, 它是 Unity 的特殊渲染组件。
它基于 “Cookie”部分。如果你还没有阅读过该教程,请先阅读。
Unity 的投影仪有点类似于聚光灯。事实上,它们可以用于类似的应用。然而,存在着一个重要的技术差异:对于聚光灯,所有被照亮物体的着色器都需要计算聚光灯的照明,如 “Cookie”部分 中所述。如果物体的着色器忽略了聚光灯,它就不会被聚光灯照亮。投影仪则不同:每个投影仪都与一个材质关联,该材质包含一个着色器,该着色器应用于投影仪范围内的所有物体。因此,物体的着色器不需要处理投影仪;相反,投影仪将它的着色器应用于它范围内的所有物体,作为额外的渲染通道,以实现某些效果,例如添加投影图像的光线或衰减物体的颜色以模拟阴影。事实上,通过使用投影仪着色器的不同混合方程,可以实现各种效果。(混合方程在 “透明度”部分 中讨论。)
人们甚至可以将投影仪视为实现光线的更“自然”方式。但是,光与材料之间的相互作用通常是特定于每种材料的,而投影仪的单个着色器无法处理所有这些差异。这将投影仪的可能性限制为三种基本行为:将光线添加到物体,调制物体的颜色,或两者兼而有之,添加光线并调制物体的颜色。我们将以添加光线到物体和衰减物体的颜色为例来讨论调制颜色。
为了创建一个投影仪,从主菜单中选择 **GameObject > Create Empty**,然后(在选择新对象时)从主菜单中选择 **Component > Effects > Projector**。你现在拥有一个投影仪,可以像操作聚光灯一样操作它。投影仪在 **Inspector Window** 中的设置在 Unity 手册 中进行了讨论。这里,唯一重要的设置是投影仪的 **Material**,它将应用于它范围内的所有物体。因此,我们必须创建另一个材质,并为它分配一个合适的着色器。这个着色器通常无法访问它所应用的博弈对象的材质;因此,它无法访问它们的纹理等。它也无法访问任何有关光源的信息。但是,它可以访问博弈对象的顶点的属性和它自己的着色器属性。
用于将光线添加到物体的着色器可以用来将任何图像投影到其他物体上,类似于投影仪或电影投影仪。因此,它应该使用类似于聚光灯 Cookie 的纹理图像(参见 “Cookie”部分),只不过纹理图像的 RGB 颜色应该被添加以允许彩色投影。我们通过将片段颜色设置为纹理图像的 RGBA 颜色并使用混合方程来实现这一点
Blend One One
它只是将片段颜色添加到帧缓冲区中的颜色。(根据纹理图像,可能最好使用 Blend SrcAlpha One
来移除任何不透明度为零的颜色。)
与聚光灯的 Cookie 另一个不同之处在于,我们应该使用特定于 Unity 的统一矩阵 unity_Projector
将位置从对象空间转换为投影仪空间,而不是矩阵 _LightMatrix0
。但是,投影仪空间中的坐标与光线空间中的坐标非常类似——除了产生的 和 坐标在正确范围内;因此,我们不必担心添加 0.5。但是,我们必须执行除以 坐标的操作(如投影纹理映射中总是做的那样);通过显式地将 和 除以 还是通过使用 tex2Dproj
。
行 ZWrite Off
确保我们不会改变深度缓冲区,因为我们只是将光线添加到已经光栅化的网格。Offset -1, -1
稍微改变了深度,以假装我们稍微位于网格的前面,我们正在向其添加光线。这有助于确保我们现在光栅化的任何内容都不会被该网格遮挡。(当你复制粘贴下面的代码时,一些编辑器会在第一个 “-” 之后添加一个空格字符(“
”),这会产生语法错误。只需删除该空格字符。)
Shader "Cg projector shader for adding light" {
Properties {
_ShadowTex ("Projected Image", 2D) = "white" {}
}
SubShader {
Pass {
Blend One One
// add color of _ShadowTex to the color in the framebuffer
ZWrite Off // don't change depths
Offset -1, -1 // avoid depth fighting (should be "Offset -1, -1")
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// User-specified properties
uniform sampler2D _ShadowTex;
// Projector-specific uniforms
uniform float4x4 unity_Projector; // transformation matrix
// from object space to projector space
struct vertexInput {
float4 vertex : POSITION;
float3 normal : NORMAL;
};
struct vertexOutput {
float4 pos : SV_POSITION;
float4 posProj : TEXCOORD0;
// position in projector space
};
vertexOutput vert(vertexInput input)
{
vertexOutput output;
output.posProj = mul(unity_Projector, input.vertex);
output.pos = UnityObjectToClipPos(input.vertex);
return output;
}
float4 frag(vertexOutput input) : COLOR
{
if (input.posProj.w > 0.0) // in front of projector?
{
return tex2D(_ShadowTex ,
input.posProj.xy / input.posProj.w);
// alternatively: return tex2Dproj(
// _ShadowTex, input.posProj);
}
else // behind projector
{
return float4(0.0, 0.0, 0.0, 0.0);
}
}
ENDCG
}
}
Fallback "Projector/Light"
}
请注意,我们必须测试 是否为正值(即片段位于投影仪的前面,而不是后面)。如果没有此测试,投影仪也会向它后面的物体添加光线。此外,纹理图像必须是正方形的,并且通常最好使用纹理坐标设置为钳制的纹理。
以防你好奇:纹理的着色器属性称为 _ShadowTex
,以便与投影仪的内置着色器兼容。
如 “Cookie”部分 中所述,投影纹理映射有时会伴随一个令人不快的副作用:在投影的边缘,GPU 使用高 mip 级别,这会导致可见的边界(特别是对于纹理坐标被钳制的纹理映射)。避免此问题的最简单方法是禁用纹理图像的 mip 映射:在 **Project Window** 中找到并选择纹理图像;然后在 **Inspector Window** 中将 **Texture Type** 设置为 **Advanced**,并取消选中 **Generate Mip Maps**。不要忘记单击 **Apply** 按钮。
创建用于调制颜色的投影仪的基本步骤与上面相同。唯一的区别是着色器代码。以下示例通过衰减颜色(特别是地板的颜色)来添加阴影。请注意,在实际应用中,阴影投射者的颜色不应衰减。这可以通过将阴影投射者分配给特定层(在游戏对象的检查器窗口中)并在投影仪的检查器窗口中忽略层下指定此层来实现。
为了使阴影具有特定的形状,我们使用纹理图像的 Alpha 分量来确定阴影的深浅。(因此,我们可以使用标准资源中灯光用的 Cookie 纹理。)为了在帧缓冲区中衰减颜色,我们应该用 1 减去 Alpha(即 Alpha 等于 1 时的因子 0)来乘以它。因此,适当的混合方程是
混合 零 一减去源 Alpha
零
表示我们不添加任何光。即使阴影太暗,也不应添加光;相反,应在片段着色器中降低 Alpha 分量,例如,通过将其乘以小于 1 的因子。为了独立地调制帧缓冲区中的颜色分量,我们需要混合 零 源颜色
或混合 零 一减去源颜色
。
不同的混合方程实际上是着色器代码中与添加光版本相比的唯一变化。
Shader "Cg projector shader for drop shadows" {
Properties {
_ShadowTex ("Projected Image", 2D) = "white" {}
}
SubShader {
Pass {
Blend Zero OneMinusSrcAlpha // attenuate color in framebuffer
// by 1 minus alpha of _ShadowTex
ZWrite Off // don't change depths
Offset -1, -1 // avoid depth fighting (should be "Offset -1, -1")
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// User-specified properties
uniform sampler2D _ShadowTex;
// Projector-specific uniforms
uniform float4x4 unity_Projector; // transformation matrix
// from object space to projector space
struct vertexInput {
float4 vertex : POSITION;
float3 normal : NORMAL;
};
struct vertexOutput {
float4 pos : SV_POSITION;
float4 posProj : TEXCOORD0;
// position in projector space
};
vertexOutput vert(vertexInput input)
{
vertexOutput output;
output.posProj = mul(unity_Projector, input.vertex);
output.pos = UnityObjectToClipPos(input.vertex);
return output;
}
float4 frag(vertexOutput input) : COLOR
{
if (input.posProj.w > 0.0) // in front of projector?
{
return tex2D(_ShadowTex ,
input.posProj.xy / input.posProj.w);
// alternatively: return tex2Dproj(
// _ShadowTex, input.posProj);
}
else // behind projector
{
return float4(0.0, 0.0, 0.0, 0.0);
}
}
ENDCG
}
}
Fallback "Projector/Light"
}
恭喜,本教程到此结束。我们已经了解了
- Unity 投影仪的工作原理。
- 如何实现一个着色器,用于将光添加到投影仪上的对象。
- 如何实现一个着色器,用于衰减投影仪上的对象的颜色。
如果你还想了解更多
- 关于光空间(与投影空间非常相似),你应该阅读“Cookies”部分。
- 关于纹理映射,特别是 Alpha 纹理贴图,你应该阅读“透明纹理”部分。
- 关于固定功能 OpenGL 中的投影纹理映射,你可以阅读 NVIDIA 的白皮书“投影纹理映射”,作者 Cass Everitt(可在线获得在线)。
- 关于 Unity 投影仪,你应该阅读Unity 关于投影仪的文档以及 Unity 的“标准资源”中的示例(可在 Asset Store 获取)在“标准资源”>“效果”>“投影仪”>“着色器”。