本教程涵盖了投射到平面上的阴影。
它并没有基于任何特别的教程;但是,理解章节“顶点变换”还是很有用的。
投射到平面上的硬阴影
实时计算真实的阴影是困难的。但是,有些情况就要容易得多了。向平面投影一个硬阴影(即没有半影的阴影,查看章节“球体的软阴影”)就是其中之一。这个想法是通过用阴影接收平面之上被投影的顶点在阴影的颜色中渲染阴影投射物体来渲染阴影。
投影物体到一个平面上
为了渲染投影的阴影,我们必须把物体向一个平面投影。为了指定这个平面,我们将会使用默认平面的局部坐标系。这样,我们可以通过编辑平面对象很容易地修改平面的位置和方向,实际的平面就是y=0的平面,它是由x和z轴组成的。
于是,顶点着色器就是这样的:
uniform float4x4 _World2Receiver; // transformation from
// world coordinates to the coordinate system of the plane
[...]
float4 vert(float4 vertexPos : POSITION) : SV_POSITION
{
float4x4 modelMatrix = _Object2World;
float4x4 modelMatrixInverse = _World2Object;
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));
}
uniform变量_World2Receiver
是通过附着在阴影投射对象上的脚本来设置的:
@script ExecuteInEditMode()
public var plane : GameObject;
function Update ()
{
if (null != plane)
{
GetComponent(Renderer).sharedMaterial.SetMatrix("_World2Receiver",
plane.GetComponent(Renderer).worldToLocalMatrix);
}
}
这个脚本需要用户指定阴影接收的平面对象并且设置相应的uniform _World2Receiver
。
完整的着色器代码
对于这段完整的着色器代码,我们通过注意矩阵向量乘法的y坐标正好是矩阵和向量第二行(即从0开始时的第一行)的点乘这点来提升代码的性能。此外,我们通过不移动平面下方的顶点来提升代码的鲁棒性(译者注:鲁棒是Robust的音译,也就是健壮和强壮的意思。),光线向上的时候也不移动。另外,我们要用以下的指令努力确保阴影是在平面的上面:
Offset -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 = _Object2World;
float4x4 modelMatrixInverse = _World2Object;
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
}
}
}
片元着色器的进一步优化
有一些东西可以改进,特别是在片元着色器中: