@
让物体接收阴影
为了让阴影出现在正方体上,我们对代码做一些修改。
(1)首先,我们在Base Pass中包含进一个新的内置文件
#include "AutoLight.cginc"
这是因为,我们下面计算阴影时所用的宏都是在这个文件中声明的。
(2)首先,我们在顶点着色器的输出结构体v2f中添加一个内置宏SHADOW_COORDS;
struct v2f{
float4 pos:SV_POSITION;
float3 worldNormal:TEXCOORD0;
float3 worldPos:TEXCOORD1;
SHADOW_COORDS(2);
};
这个宏的作用很简单,就是声明一个用于对阴影纹理采样的坐标。需要注意的是,这个宏的参数需要是下一个可用的插值寄存器的索引值,在上面的例子中就是2。
(3)然后,我们在顶点着色器返回之前添加另一个内置宏TRANSFER_SHADOW:
v2f vert(a2v v){
v2f o;
...
//Pass shadow coordinates to pixel shader
TRANSFER_SHADOW(o);
return o;
};
这个宏用于在顶点着色器中计算上一步中声明的阴影纹理坐标。
(4)接着,我们在片元着色器中计算阴影值,这同样使用了一个内置宏SHADOW_ATTENUATION:
//Use shadow coordinates to sample shadow map
fixed shadow=SHADOW_ATTENUATION(i);
SHADOW_COORDS、TRANSFER_SHADOW和SHADOW_ATTENUATION是计算阴影时的“三剑客”。我们可以在AutoLight.cginc中找到它们的声明:
// ----------
//Shadow helpers
//--------
//------Screen space shadows
#if defined(SHADOWS_SCREEN)
UNITY_DECLARE_SHADOWMAP(_ShadowMapTexture);
#define SHADOW_COORDS(idx1) unityShadowCoord4 _ShadowCoord:TEXCOORD##idx1;
#if defined (UNITY_NO_SCREENSPACE_SHADOWS)
#define TRANSFER_SHADOW_SHADOW(a)a._ShadowCoord=mul(unity_World2Shadow[0],mul(_Object2World,v.vertex));
inline fixed unitySampleShadow (unityShadowCoord4 shadowCoord)
{
...
}
#else//UNITY_NO_SCREENSPACE_SHADOWS
#define TRANSFER_SHADOW(a) a._ShadowCoord=ComputeScreenPos(a.pos);
inline fixed unitySampleShadow(unityShadowCoord4 ShadowCoord)
{
fixed shadow=tex2DProj(_ShadowMapTexture,UNITY_PROJ_COORD(shadowCoord)).r;
return shadow;
}
#endif
#define SHADOW_ATTENUATION(a)unitySampleShadow(a._ShadowCoord)
#endif
//---Spot light shadows
#if defined (SHADOWS_DEPTH)&&defined(SPOT)
...
#endif
//---Point light shadows
#if defined(SHADOWS_CUBE)
...
#endif
//---Shadows off
#if !defined(SHADOWS_SCREEN)&&!defined(SHADOWS_DEPTH)&&!defined(SHADOWS_CUBE)
#define SHADOW_COORDS(idx1)
#define TRANSFER_SHADOW(a)
#define SHADOW_ATTENUATION(a)1.0
#endif
上面的代码看起来很多很复杂,实际上只是Unity为了处理不同的光源类型、不同平台而定义了多个版本的宏。在前向渲染中,宏SHADOW_COORDS实际上就是声明了一个名为_ShadowCoord的阴影纹理坐标变量。而TRANSFER_SHADOW的实现会根据平台不同而有所差异。如果当前平台可以使用屏幕空间的阴影映射技术(通过判断是否定义了UNITY_NO_SCREENSPACE_SHADOWS来得到),TRANSFER_SHADOW会调用内置的ComputePos函数来计算_ShadowCoord;如果该平台不支持屏幕空间的阴影映射技术,就会使用传统的阴影映射技术,TRANSFER_SHADOW会把顶点坐标从模型空间变换到光源空间后存储到_ShadowCoord中。然后SHADOW_ATTENUATION负责使用_ShadowCoord对相关纹理进行采样,得到阴影信息。
注意到,上面的内置代码的最后定义了在关闭阴影时的处理代码,可以看出,当关闭阴影后,SHADOW_COORDS和TRANSFER_SHADOW实际没有任何作用,而SHADOW_ATTENUATION会直接等于数值1。
需要读者注意的是,由于这些宏会使用上下文变量来进行相关计算,例如TRANSFER_SHADOW会使用v.vertex或a.pos来计算坐标,因此为了能够让这些宏正确工作,我们需要保证自定义的变量名和这些宏中使用的变量名相匹配。我们需要保证:a2f结构体中顶点坐标变量名必须是vertex,顶点着色器的输出结构体v2f必须命名为v,且v2f中的顶点位置变量必须命名为pos。
(5)在完成了上面的所有操作后,我们只需要把阴影值shadow和漫反射和漫反射以及高光颜色相乘即可。得到的结果如下图所示。
需要注意的是,在上面的代码里我们只更改了Base Pass中的代码,使其可以得到阴影效果,而没有对Additional Pass做任何更改。大体上,Additional Pass的阴影处理和Base Pass是一样的。我们将在后面看到如何处理这些阴影。本节实现的代码仅是为了解释如何让物体接收阴影,但不可以直接应用到项目中。我们会在后面给出包含了完整的光照处理的Unity Shader。
使用帧调试器查看阴影绘制过程
尽管我们在上面描述了阴影的产生过程,但如果有直观的方式看到阴影一步步的绘制过程那就太好了。幸运的是,Unity5添加了新的调试工具——帧调试器。我们曾在前面利用它查看过Pass的绘制过程,本节我们会通过它查看阴影的绘制过程。
首先,我们要在Window->Frame Debugger中打开帧调试器。下图给出了在帧调试器中的分析结果
在上图中可以看出,绘制该场景共需要花费20个渲染事件。这些渲染事件可以分为4个部分:UpdateDepthTexture,即更新摄像机的深度纹理;RenderShadowmap,即渲染的得到平行光的阴影映射纹理;CollectShadows,即根据深度纹理和阴影映射纹理得到屏幕空间的阴影图;最后绘制渲染结果。
我们首先来看第一个部分:更新摄像机的深度纹理,这是前4个渲染事件的工作。我们可以单击这些事件查看它们的绘制结果。下图给出了正方体对深度纹理的更新结果。
从调试器右侧的面板我们可以了解这一渲染事件的详细信息。从上图我们可以发现,Unity调用了Shader来更新深度纹理,即上一节中的第三个Pass。尽管上一节只定义了两个Pass,但正如我们之前所说,Unity会在它的Fallback中找到第三个Pass,即LightMode为ShadowCaster的Pass来更新摄像机的深度纹理。同样,在第二个部分,即渲染得到平行光的阴影映射纹理的过程中,Unity也是调用了这个Pass来得到光源的阴影映射纹理。
在第三个部分中,Unity会根据之前两部的结果得到屏幕空间的阴影图,如下图所示:
这张图已经包含了最终屏幕上所有阴影区域的阴影。在最后一个部分中,如果物体所使用的的Shader包含了对这张阴影图的采样就会得到阴影效果。下图给出了这个部分Unity是如何一步步绘制出有阴影的画面效果的。