Cg 编程/Unity/平面阴影
本教程涵盖了将阴影投影到平面上。
它不基于任何特定的教程;但是,了解“顶点变换”部分是有用的。
实时计算逼真的阴影是困难的。但是,某些情况要容易得多。将硬阴影(即没有半影的阴影;参见“球体的软阴影”部分)投影到平面上就是其中一种情况。这个想法是通过渲染投射阴影的物体来渲染阴影,该物体的颜色为阴影的颜色,并且顶点被投影到接收阴影的平面正上方。
为了渲染投影阴影,我们必须将物体投影到平面上。为了指定平面,我们将使用默认平面游戏对象的局部坐标系。因此,我们可以通过编辑平面对象轻松修改平面的位置和方向。在该游戏对象的坐标系中,实际平面只是 平面,该平面由 和 轴跨越。
在顶点着色器中投影物体意味着投影每个顶点。这可以通过类似于“顶点变换”部分中讨论的投影矩阵来完成。但是,这些矩阵计算和调试起来有些困难。因此,我们将采用另一种方法,使用一些向量算术来计算投影。左侧的示意图显示了点P在光方向L上投影到接收阴影的平面上的情况。(注意,向量L与通常用于光照计算的光向量方向相反。)为了将点P移动到平面,我们添加了L的缩放版本。缩放因子原来是P到平面的距离除以L在平面法线方向上的长度(因为类似于灰色线所示的三角形)。在平面的坐标系中,法线向量只是 轴,我们也可以使用点P的 坐标除以向量L的负 坐标的比率。
因此,顶点着色器可能如下所示
uniform float4x4 _World2Receiver; // transformation from
// world coordinates to the coordinate system of the plane
[...]
float4 vert(float4 vertexPos : POSITION) : SV_POSITION
{
float4x4 modelMatrix = unity_ObjectToWorld;
float4x4 modelMatrixInverse = unity_WorldToObject;
float4 lightDirection;
if (0.0 != _WorldSpaceLightPos0.w)
{
// point or spot light
lightDirection = normalize(
mul(modelMatrix, vertexPos - _WorldSpaceLightPos0));
}
else
{
// directional light
lightDirection = -normalize(_WorldSpaceLightPos0);
}
float4 vertexInWorldSpace = mul(modelMatrix, vertexPos);
float distanceOfVertex =
mul(_World2Receiver, vertexInWorldSpace).y
// = height over plane
float lengthOfLightDirectionInY =
mul(_World2Receiver, lightDirection).y
// = length in y direction
lightDirection = lightDirection
* (distanceOfVertex / (-lengthOfLightDirectionInY));
return mul(UNITY_MATRIX_VP,
vertexInWorldSpace + lightDirection));
}
统一变量_World2Receiver
最好使用一个小的 C# 脚本来设置,该脚本应附加到投射阴影的物体上
using UnityEngine;
[ExecuteInEditMode, RequireComponent(typeof(Renderer))]
public class SetWorld2Receiver : MonoBehaviour {
public GameObject plane;
Renderer rend;
Renderer planeRend;
void Start() {
rend = GetComponent<Renderer>();
}
// Update is called once per frame
void Update () {
if (plane != null) {
planeRend = plane.GetComponent<Renderer>();
rend.sharedMaterial.SetMatrix("_World2Receiver",
planeRend.worldToLocalMatrix);
}
}
}
该脚本(应命名为SetWorld2Receiver
)要求用户指定接收阴影的平面对象,并相应地设置统一变量_World2Receiver
。
对于完整的着色器代码,我们通过注意到矩阵向量积的 坐标只是矩阵的第二行(即从 0 开始时的第一行)与向量的点积来提高性能。此外,我们通过在顶点位于平面下方时或光向上照射时不移动顶点来提高鲁棒性。此外,我们尝试通过以下指令确保阴影在平面的顶部
偏移 -1.0,-2.0
这会稍微降低光栅化三角形的深度,以使它们始终遮挡其他深度大致相同的三角形。
着色器的第一遍渲染投射阴影的物体,而第二遍渲染投影阴影。在实际应用中,第一遍可以用一个或多个遍来代替,以计算投射阴影的物体的照明。
Shader "Cg planar shadow" {
Properties {
_Color ("Object's Color", Color) = (0,1,0,1)
_ShadowColor ("Shadow's Color", Color) = (0,0,0,1)
}
SubShader {
Pass {
Tags { "LightMode" = "ForwardBase" } // rendering of object
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// User-specified properties
uniform float4 _Color;
float4 vert(float4 vertexPos : POSITION) : SV_POSITION
{
return mul(UNITY_MATRIX_MVP, vertexPos);
}
float4 frag(void) : COLOR
{
return _Color;
}
ENDCG
}
Pass {
Tags { "LightMode" = "ForwardBase" }
// rendering of projected shadow
Offset -1.0, -2.0
// make sure shadow polygons are on top of shadow receiver
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
// User-specified uniforms
uniform float4 _ShadowColor;
uniform float4x4 _World2Receiver; // transformation from
// world coordinates to the coordinate system of the plane
float4 vert(float4 vertexPos : POSITION) : SV_POSITION
{
float4x4 modelMatrix = unity_ObjectToWorld;
float4x4 modelMatrixInverse = unity_WorldToObject;
float4x4 viewMatrix =
mul(UNITY_MATRIX_MV, modelMatrixInverse);
float4 lightDirection;
if (0.0 != _WorldSpaceLightPos0.w)
{
// point or spot light
lightDirection = normalize(
mul(modelMatrix, vertexPos - _WorldSpaceLightPos0));
}
else
{
// directional light
lightDirection = -normalize(_WorldSpaceLightPos0);
}
float4 vertexInWorldSpace = mul(modelMatrix, vertexPos);
float4 world2ReceiverRow1 =
float4(_World2Receiver[1][0], _World2Receiver[1][1],
_World2Receiver[1][2], _World2Receiver[1][3]);
float distanceOfVertex =
dot(world2ReceiverRow1, vertexInWorldSpace);
// = (_World2Receiver * vertexInWorldSpace).y
// = height over plane
float lengthOfLightDirectionInY =
dot(world2ReceiverRow1, lightDirection);
// = (_World2Receiver * lightDirection).y
// = length in y direction
if (distanceOfVertex > 0.0 && lengthOfLightDirectionInY < 0.0)
{
lightDirection = lightDirection
* (distanceOfVertex / (-lengthOfLightDirectionInY));
}
else
{
lightDirection = float4(0.0, 0.0, 0.0, 0.0);
// don't move vertex
}
return mul(UNITY_MATRIX_VP,
vertexInWorldSpace + lightDirection);
}
float4 frag(void) : COLOR
{
return _ShadowColor;
}
ENDCG
}
}
}
有一些东西可以改进,特别是在片段着色器中
- 可以使用
discard
指令删除位于矩形平面对象之外的阴影片段,该指令在“切口”部分中进行了讨论。 - 如果平面是纹理化的,则可以通过仅使用局部顶点坐标进行纹理查找(也在平面对象的着色器中)并将平面的纹理指定为投射阴影的物体的着色器属性来集成此纹理化。
- 可以通过在此着色器中计算平面的照明并根据投射阴影的物体的表面法线向量与光方向的角度对其进行衰减来伪造软阴影,这类似于“轮廓增强”部分中的方法。
恭喜,本教程到此结束。我们已经看到
- 如何将顶点沿光方向投影到平面上。
- 如何实现此技术将阴影投影到平面上。
如果您还想了解更多
- 关于模型变换、视图变换和投影,您应该阅读“顶点变换”部分中的描述。
- 关于设置投影矩阵以投影阴影,你可以阅读 Tom McReynolds 组织的 SIGGRAPH '98 课程“使用 OpenGL 的高级图形编程技术”的第 9.4.1 节,该课程可以 在线 获取。