@
统一管理光照衰减和阴影
在前面,我们已经讲过如何在UnityShader的前向渲染路径中计算光照衰减——在Base Pass中,平行光的衰减因子总是等于1,而在Additional Pass中,我们需要判断该Pass处理的光源类型,再使用内置变量和宏计算衰减因子。实际上,光照衰减和阴影对物体最终的渲染结果的影响本质上是相同的——我们都是把光照衰减因子和阴影值及光照结果相乘得到最终的渲染结果。那么是不是有一个方法可以同时计算两个信息呢?好消息是,Unity在Shader里提供了这样的功能,这主要是通过内置的UNITY_LIGHT_ATTENUATION宏来实现的。
关键代码如下:
(1)首先包含进需要的头文件。
//Need these files to get built-in macros
#include "Lighting.cginc"
#include "Autolight.cginc"
(2)在v2f结构体中使用内置宏SHADOW_COORDS声明阴影坐标:
struct v2f{
float4 pos:SV_POSITION
float3 worldNormal:TEXCOORD0
float3 worldPos:TEXCOORD1
SHADOW_COORDS(2)
};
(3)在顶点着色器中使用内置宏TRANSFER_SHADOW计算并向片元着色器传递阴影坐标:
v2f vert(a2v v){
v2f o;
...
TRANSFER_SHADOW(o);
return o;
}
(4)和之前的方式不同,这次我们在片元着色器中使用内置宏UNITY_LIGHT_ATTENUATION来计算光照衰减和阴影
fixed4 frag(v2f i):SV_Target
...
//UNITY_LIGHT_ATTENUATION not only compute attenuation,but also shadow infos
return fixed4(ambient+(diffuse+specular)*atten,1.0);
UNITY_LIGHT_ATTENUATION是Unity内置的用于计算光照衰减和阴影的宏,我们可以在内置的AutoLight.cginc里找到它们的相关声明。它接受3个参数,它会将光照衰减和阴影值相乘后的结果存储到第一个参数中。注意到,我们并没有在代码中声明第一个参数atten,这是因为UNITY_LIGHT_ATTENUATION会帮我们声明这个变量。它的第二个参数是结构体v2f,这个参数会传递给SHADOW_ATTENUATION,用来计算阴影值。而第三个参数是世界空间的坐标,正如我们在前面讲的那样,这个参数会用于计算光源空间下的坐标,再对光照衰减纹理采样得到的光照衰减。我们强烈建议读者查阅AutoLight.cginc中UNITY_LIGHT_ATTENUATION的声明,读者可以发现,Unity针对不同光源类型、是否启用cookie等不同情况声明了多个版本的UNITY_LIGHT_ATTENUATION。这些不同版本的声明是保证我们可以通过这样一个简单的代码来得到正确结果的关键。
(5)由于我们使用了UNITY_LIGHT_ATTENUATION,我们的Base Pass和Additional Pass的代码得以统一——我们不需要在Base Pass里单独处理阴影,也不需要在Additional Pass中判断光源类型来处理光照衰减,一切都只需要通过UNITY_LIGHT_ATTENUATION来完成即可。这正是Unity内置文件的魅力所在。如果我们希望可以在Additional Pass中添加阴影效果,就需要使用#pragma multi_compile_fwdadd_fullshadows编译指令来代替Additional Pass中的#pragma multi_compile_fwdadd指令。这样一来,Unity也会为这些额外的逐像素光源计算阴影,并传递给Shader。
透明物体的阴影
我们从一开始就强调,想要在Unity里让物体能够向其它物体投射阴影,一定要在它使用的Unity Shader中提供一个LightMode为ShadowCaster的Pass。在前面的例子中,我们使用内置的VertexLit中提供的ShadowCaster来投射阴影。VertexLit中的ShadowCaster实现很简单,它会正常渲染整个物体,然后把深度结果输出到一张深度图或阴影映射纹理中。读者可以在内置文件中找到相关文件。
对于大多数不透明物体来说,把Fallback设为VertexLit就可以得到正确的阴影。但对于透明物体来说,我们就需要小心处理它的阴影。透明物体的实现通常会使用透明度测试或透明度混合,我们要小心设置这些物体的Fallback。
透明度测试的处理比较简单,但如果我们仍然直接使用VertexLit、Diffuse、Specular等做为回调,往往无法得到正确的阴影。这是因为透明度测试需要在片元着色器中舍弃某些片元,而VertexLit中的阴影投射纹理并没有进行这样的操作。
(1)首先包含进需要的头文件:
#include ''Lighting.cginc''
#include "AutoLight.cginc"
(2)在v2f中使用内置宏SHADOW_COORDS声明阴影纹理坐标:
struct v2f{
float4 pos:SV_POSITION;
float3 worldNormal:TEXCOORD0;
float3 worldPos:TEXCOORD1;
float2 uv:TEXCOORD2;
SHADOW_COORDS(3)
};
注意到,由于我们已经占用了3个插值寄存器(使用TEXCOORD0、TEXCOORD1和TEXCOORD2修饰的变量),因此SHADOW_COORDS传入的参数是3,这意味着阴影纹理坐标将占用第四个插值寄存器TEXCOORD3。
(3)然后,在顶点着色器中使用内置宏TRANSFER_SHADOW计算阴影纹理坐标后传递给片元着色器:
v2f vert(a2v v){
v2f o;
...
//Pass shadow coordinates to pixel shade
TRANSFER_SHADOW(o);
return o;
}
(4)在片元着色器中,使用内置宏UNITY_LIGHT_ATTENUATION计算阴影和光照衰减:
fixed4 frag(v2f i):SV_Target{
...
//UNITY_LIGHT_ATTENUATION not only compute attenuation,but also shadow infos
UNITY_LIGHT_ATTENUATION(atten,i,i.worldPos);
return fixed(ambient+diffuse*atten,1.0);
}
(5)这次,我们更改它的Fallback,使用VertexLit作为它的回调Shader:
Fallback"VertexLit"
我们可以得到类似下图的效果:
细心的读者可以发现,镂空区域出现了不正常的阴影,看起来就像这个正方体是一个普通的正方体一样。而这并不是我们想要得到的,我们希望有些光应该是可以通过这些镂空区域透过来的,这些区域不应该有阴影。出现这样的情况是因为,我们使用的是内置的VertexLit中提供的ShadowCaster来投射阴影,而这个Pass中没有进行任何透明度测试计算,因此,它会把整个物体的深度信息渲染到深度图和阴影映射纹理中。因此,如果我们想要得到经过透明度测试后的阴影效果,就需要提供一个有透明度测试功能的ShadowCaster Pass。当然,我们可以自行编写一个这样的Pass,但这里我们仍然选择使用内置的UnityShader来减少代码量。
为了让使用透明度测试的物体得到正确的阴影效果,我们只需要在Unity Shader中更改一行代码,即把Fallback设置为Transparent/Cutout/VertexLit。读者可以在内置文件中找到该Unity Shader的代码,它的ShadowCaster Pass也计算了透明度测试,因此会把裁剪后的物体深度信息写入深度图和阴影映射纹理中。但需要注意的是,由于Transparent/Cutout/VertexLit中计算透明度测试时,使用了名为_Cutoff的属性来进行透明度测试,因此,这要求我们的Shader中也必须提供名为_Cutoff的属性。否则,同样无法得到正确的阴影效果。
在更改了Fallback后,我们可以得到下图的效果:
但是,这样的结果仍然有一些问题,例如出现了一些不应该透过光的部分。出现这种情况的原因是,默认情况下把物体渲染到深度图和阴影映射纹理中仅考虑物体的正面。但对于本例的正方体来说,由于一些面完全背对光源,因此这些面的深度信息没有加入到阴影映射纹理的计算中。为了得到正确的结果,我们可以将正方体的Mesh Renderer组件中的Cast Shadows属性设置为Two Sided,强制Unity在计算阴影映射纹理时计算所有面的深度信息。下图给出了正确设置后的渲染结果。
与透明度测试的物体相比,想要为使用透明度混合的物体添加阴影是一件比较复杂的事情。事实上,所有内置的透明度混合的Unity Shader,如Transparent/VertexLit等,都没有包含阴影投射的Pass。这意味着,这些半透明物体不会参与深度图和阴影映射纹理的计算,也就是说,它们不会向其它物体投射阴影,同样它们也不会接收来自其它物体的阴影。我们使用之前学习的透明度混合+阴影的方法来渲染一个正方体,添加关于阴影的计算,并且它的Fallback是内置的Transparent/VertexLit,下图显示了渲染的结果:
Unity会这样处理半透明物体是有它的原因的。由于透明度混合需要关闭深度写入,由此带来的问题也影响了阴影的生成。总体来说,要想为这些透明半透明物体产生正确的阴影,需要在每个光源空间下仍然严格按照从后往前的顺序进行渲染,这会让阴影处理变得非常复杂,而且会影响性能。因此,在Unity中,所有内置的半透明Shader是不会产生任何阴影效果的。当然,我们可以使用一些dirty trick来强制为半透明物体生成阴影,这可以通过把它们的Fallback设置为VertexLit、Diffuse这些不透明物体使用的UnityShader,这样Unity就会在它的Fallback找到一个阴影投射的Pass,然后我们可以通过物体的Mesh Render组件上的Cast Shadows和Receive Shadows选项来控制是否需要向其他物体投射或接收阴影。下图显示了把Fallback设置为VertexLit并开启阴影投射和接收阴影后的半透明物体的渲染效果。
可以看出,此时右侧平面的阴影投射到了半透明的立方体上,但它不会再穿透立方体把阴影投射到下方的平面上,这其实是不正确的。同时,立方体也可以把自身的阴影投射到下面的平面上。