一.简介
目前的阴影算法大致可以分为以下三类:基于ray tracing,基于shadow volume,基于shadow map。shadow map因其易于实现,算法复杂度与场景复杂度无关等优点被广泛应用,但是shadow map有alias(走样、锯齿)的问题。为解决这个问题,产生了诸多对标准shadow map的改进办法。
Shadow map的alias问题可以分为两类:Perspective alias和Project alias。Project alias是因为当光照方向与物体表面夹角比较小时,使得多个pixel对应Shadow map中一个texel,产生alias问题,此类alias目前是个困难问题,针对这类alias的研究有Resolution-Matched SMs (Lefohn et al. 2007), Irregular Z-Buffer (Johnson et al.2005),但都需要承受很大性能损失。Perspective alias产生的原因是透视投影会产生近大远小的效果,这使得近处物体的多个pixel可能对应着Shadow map中一个texel, 产生alias问题。相比其他流行的shadow map技术,如perspective shadow maps (PSMs) (Stamminger and Drettakis 2002), light-space perspective shadow maps (LiSPSMs) (Wimmer et al. 2004) and trapezoidal shadow maps (TSMs) (Martin and Tan 2004),本文介绍的Parallel-Split Shadow Maps (PSSMs) (Zhang et al. 2007 and Zhang et al. 2006) 思想相对直观,实现也比较简单,并且不需要根据不同类型的关于和特殊的位置对映射进行任何特化处理,而且可以和其他的shadow map算法结合使用。
PSSMs的基本思路就是把摄像机的视域体按照Z的范围平行分割成多个部份,每个部分称为一个子视域体,分别为对每个子视域体生成Shaodw Map,在渲染场景时根据Z值选择最适合的Shaodw Map进行采样。
二.算法
PSSMs算法的主要步骤如下:
1) 将摄像机视域体按深度范围划分成多个子视域体{V1,V2,...,Vm}。
2) 根据各相机子视域体分别计算各光源子视域体{W1,W2,...,Wm}。
3) 生成阴影图,将Wi包含的阴影投射对象渲染到阴影图Ti中。
4) 综合场景阴影效果,Vi中的像素采样Ti进行阴影判断。
Step1. 分割摄像机视域体
如何分割视域体对PSSMs算法生成的阴影质量有着重要影响,若将视域体划分为m层,则摄像机空间下场景的划分平面深度值Ci可以通过
求得,其中f,n分别表示V的远近裁剪面,和代表2种划分策略,参数用以调节他们的比例,分层策略的详细解释见参考文献[1]。
Step2. 计算光源视域体及其变换矩阵
在计算子视域体Wi的view-projection矩阵之前,我们必须找到分割视域体Vi在光空间下的轴对齐包围盒。然后,我们引入cropMatrix的概念,cropMatrix是一个在投影空间内对规则观察体进行缩放和平移的矩阵,最终我们在以光源为视点进行阴影图Ti的渲染时使用的view-projection变换矩阵是
lightViewMatrix * lightProjMatrix * cropMatrix
当lightProjMatrix为正交投影时(对于平行光),cropMatrix的作用相当于在世界空间内平移和缩放以光源为视点的视域体;
当lightProjMatrix为透视投影时(对于点光源),cropMatrix的作用相当于在世界空间内绕光源旋转和缩放以光源为视点的视域体。
这正好符合平行光和点光源的特性,所以cropMatrix在平行光和点光源下都是正确的。
我们使用如下函数来计算cropMatrix,
Matrix4f CalcCropMatrix( Frustum splitFrustum, Matrix4f matViewProj);
其中,
参数 splitFrustum 是某个子视域体Vi。
参数 matViewProj 是一个未经修剪的view-projection矩阵(即视矩阵和投影矩阵的复合)
函数返回一个cropMatrix使得 matViewProj * cropMatrix所对应的规则观察体恰可以包住splitFrustum的轴对齐包围盒(AABB)。
函数实现如下:
Matrix4f CalcCropMatrix( Frustum splitFrustum, Matrix4f matViewProj)
{
// 计算视域体splitFrustum在matViewProj空间下的轴对齐包围盒
BoundingBox cropBB = CalcAABB(splitFrustum, matViewProj);
// 使用默认的近裁剪面
cropBB.min.z = 0.0f;
// 计算缩放和偏移值
float scaleX, scaleY, scaleZ;
float offsetX, offsetY, offsetZ;
scaleX = 2.0f / (cropBB.max.x - cropBB.min.x);
scaleY = 2.0f / (cropBB.max.y - cropBB.min.y);
offsetX = -0.5f * (cropBB.max.x + cropBB.min.x) * scaleX;
offsetY = -0.5f * (cropBB.max.y + cropBB.min.y) * scaleY;
scaleZ = 1.0f / (cropBB.max.z - cropBB.min.z);
offsetZ = -cropBB.min.z * scaleZ;
return Matrix4f(scaleX, 0.0f, 0.0f, 0.0f,
0.0f, scaleY, 0.0f, 0.0f,
0.0f, 0.0f, scaleZ, 0.0f,
offsetX, offsetY, offsetZ, 1.0f);
}
|
这里有几点需要说明的,cropBB.min.z = 0.0f表示使用默认的近裁剪面,即生成的cropMatrix不对参数matViewProj所定义的近裁剪面进行调整,如果使用由包围盒计算出的近裁剪面会导致从由matViewProj所定义的视点到包围盒之间的阴影投射体不会被渲染到阴影图上,那么其所产生的阴影就不会出现在包围盒内的物体上,如图2-3所示,Caster的阴影应该出现在Vi内的物体上,但Caster不在Vi所对应的AABB内。
还有一点需要注意的是应该保证splitFrustum 不会出现在matViewProj所对应的投影空间的Z轴的副半轴,否则生成cropMatrix将不正确。
Step3. 生成分段阴影图
在阴影图生成方面,在DX10上是可以优化到渲染一次的。而在DX9上如果将视域体分成N段的话需要渲染N次来生成N张不同子视域体的阴影图。如下图2-4:
在DX9上使用GPU加速的渲染流程大致如下:
Matrix4f lightViewProjMatrix[numSplits];
//生成光源深度图
for(int i=0;i<numSplits;i++)
{
//设置阴影图为RenderTarget
GetRenderDevice()->BeginRenderToTargets(m_pTexShadowMap[i]));
GetRenderDevice()->Clear();
//计算子视域体
splitFrustum = camera->CalculateFrustum(splitPos[i], splitPos[i+1]);
//计算cropMatrix
cropMatrix = CalcCropMatrix(splitFrustum, lightViewMatrix * lightProjMatrix);
lightViewProjMatrix[i] = lightViewMatrix * lightProjMatrix * cropMatrix;
//以lightViewMatrix * lightProjMatrix * cropMatrix为ViewProj变换渲染阴影投射体
RenderCasters(casters, lightViewProjMatrix[i]);
GetRenderDevice()->EndRenderToTargets();
}
|
Step4. 合成场景阴影效果
for(int i=0;i<numSplits;i++)
{
//设置各个子视域体的光源ViewProj矩阵
GetRenderDevice()->ShaderSetConstant(ShaderStage::VERTEX_SHADER_STAGE, LIGHT_VPMATRIX_REGISTER+i*4, &lightViewProjMatrix[i]);
//设置各个子视域体生成的阴影图
GetRenderDevice()->ShaderSetConstant(ShaderStage::PIXEL_SHADER_STAGE, SHADOWMAP_REGISTER+i, m_pTexShadowMap[i]);
}
//以摄像机的ViewProj变换渲染阴影接收体
RenderReivers(receivers, camera->viewMatrix * camera->projMatrix);
|
Vertex and Pixel Shaders for Synthesizing Shadows in DirectX 9
struct VS_IN
{
float3 ObjPos : POSITION;
float2 ObjUV : TEXCOORD2;
};
struct VS_OUT
{
float4 ProjPos : POSITION;
float2 ObjUV : TEXCOORD2;
float viewZ : TEXCOORD3;
float4 texCoordProj[3] : TEXCOORD4;
};
float4x4 matWorld : register(c0);
float4x4 matView : register(c4);
float4x4 matProj : register(c8);
float4x4 matLightVP[3] : register(c12);
VS_OUT main( VS_IN In )
{
float4 wPosition = mul(float4(In.ObjPos.xyz, 1.0f), matWorld);
float4 vPosition = mul(wPosition, matView);
VS_OUT Out;
Out.ProjPos = mul( vPosition, matProj );
Out.ObjUV = In.ObjUV;
Out.viewZ = vPosition.z;
float4x4 matLightViewport = float4x4(
0.5f, 0.0f, 0.0f, 0.0f,
0.0f, -0.5f, 0.0f, 0.0f,
0.0f, 0.0f, 1.0f, 0.0f,
0.5f + 0.5f / 512.0f, 0.5f + 0.5f / 512.0f, 0, 1.0f
);
for(int i=0;i<3;i++)
{
Out.texCoordProj[i] = mul( wPosition, matLightVP[i] );
Out.texCoordProj[i] = mul( Out.texCoordProj[i], matLightViewport);
}
return Out;
}
|
struct PS_IN
{
float2 ObjUV : TEXCOORD2;
float viewZ : TEXCOORD3;
float4 texCoordProj[3] : TEXCOORD4;
};
sampler2D g_Tex : register(s0);
sampler2D g_tShadowMap[3] : register(s1);
float4 main( PS_IN In ) : COLOR
{
float lightIntensity = 1.0;
for(int i=0;i<3;i++)
{
if(In.viewZ < splitPlane[i])
{
lightIntensity = tex2Dproj(g_tShadowMap[i], In.texCoordProj[i]).x;
}
}
float4 COLOR_IN_SHADOW, COLOR_IN_LIGHT;
COLOR_IN_SHADOW.rgba = float4(0.2,0.2,0.2,1.0);
COLOR_IN_LIGHT.rgba = float4(1.0,1.0,1.0,1.0);
float4 tex = tex2D(g_Tex, In.ObjUV);
tex *= lerp(COLOR_IN_SHADOW, COLOR_IN_LIGHT, lightIntensity);
return tex;
}
|
三.可能的优化
·结合使用Variance Shadow Maps和Gaussian Blur可进一步软化阴影边缘
·正反Culling渲染2次取平均深度可以帮助解决Shadow Acne & Peter Panning
·将阴影图纹理打包,使用TextureArray或者拼到一张2D Texture上,可以减少PS的分支
参考文献:
[1] Zhang F, Sun H Q, Xu L L, et al. Hardware-accelerated parallel-split shadow maps[J]. International Journal of Image and Graphics, 2008.8(2): 223-241
[2] Zhang F, Sun H Q, Nyman O. Parallel-Split Shadow Maps on Programmable GPUs. GPU Gems 3 - Chapter 10, 2007: 203-238