创建阴影分为两步:
一:以光源为视点,正交投影渲染整个场景,得到深度图(shadow map)并保存变换矩阵。深度图中记录了以光源为视点时,所有的可视点的深度。
二:以相机为视点渲染场景,对于场景中的每个顶点,将其变换到以光源为视点的空间,若其深度大于shadow map中对应点的深度值,则说明光源射来的光线被物体遮蔽了。则该点处于阴影中
由于光源发出的光是平行光,因此我们需要建立一个正交投影矩阵
// ----------------计算以光源为视点的视图矩阵--------------
LightDir = XMLoadFloat3(&mLights[0].Dir);
LightPos = XMVectorMultiply(XMVectorReplicate(-2.0f * mSceneBound.Radius) , LightDir);
TargetPos = XMLoadFloat3(&mSceneBound.Center);
Up = XMVectorSet(0.0f , 1.0f , 0.0f , 1.0f);
XMMATRIX V = XMMatrixLookAtLH(LightPos , TargetPos , Up);
// ----------由于是平行光,所以采用正交投影------------
// 将包围盒中心变换到视图空间
XMFLOAT3 SphereCenterLS;
XMStoreFloat3(&SphereCenterLS , XMVector3TransformCoord(TargetPos , V));
float l = SphereCenterLS.x - mSceneBounds.Radius;
float r = SphereCenterLS.x + mSceneBounds.Radius;
float t = SphereCenterLS.y + mSceneBounds.Radius;
float b = SphereCenterLS.y - mSceneBounds.Radius;
float f = SphereCenterLS.z + mSceneBounds.Radius;
float n = SphereCenterLS.z - mSceneBounds.Radius;
XMMATRIX P = XMMatrixOrthographicOffCenterLH(l , r , b , t , n , f);
// ----------------变换到纹理空间---------------------
XMMATRIX T =
(
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 , 0.0f , 1.0f
);
// mShadowTransform可以将顶点从局部空间变换到纹理空间
// 当然,该变换是以光源是视点,采用正交投影,并且为了采样
// 我们将其变换到了纹理空间
XMMATRIX S = V * P * T;
XMStoreFloat4x4(&mLightView , V);
XMStoreFloat4x4(&mLightProj , P);
XMStoreFloat4x4(&mShadowTransform , S);
下面我们介绍下如何生成Shadow Map
Shadow Map中记录的是以光源为视点的最近可视点的深度值,并不需要render target,所以只需要绑定DepthStencilView
ID3D11RenderTargetView* RTVArray[1] = {nullptr};
Context->OMSetRenderTargets(1 , RTVArray , mShadowMapDSV);
Context->ClearDepthStencilView(mShadowMap , D3D11_CLEAR_DEPTH , 1.0f , 0);
Shader代码如下:
// ----------------------Vertex Shader--------------------
cbuffer Transform
{
// 以光源为视点,正交投影
float4x4 WorldViewProj;
float4x4 TexTransform;
};
struct VertexIn
{
float3 PosL : POSITION;
float3 NormalL : NORMAL;
float2 Tex : TEXCOORD;
};
struct VertexOut
{
float4 PosH : SV_POSITION;
float2 Tex : TEXCOORD;
};
VertexOut main(VertexIn input)
{
VertexOut output;
output.PosH = mul(float4(input.PosW , 1.0f) , WorldViewProj);
output.Tex = mul(float4(input.Tex , 0.0f , 1.0f) , TexTransform).xy;
}
//--------------------Pixel Shader---------------------
Texture2D DiffuseMap;
SamplerState SampleLinear;
void main(VertexOut input)
{
float4 texColor = DiffuseMap.Sample(SampleLinear , input.Tex);
clip(texColor.a - 0.1f);
}
上面这段shader仅仅是将场景渲染到指定空间,得到以光源为视点时的最近可视点的深度。
我们得到ShadowMap后,我们可以将其用于场景渲染,以判断哪些是阴影
Shadow acne
shadow map的分辨率有限,因此每个shadow map texel对应于场景中一块区域,很容易产生shadow acne的现象即有一个阶梯状的边缘
如:
下图阐述了这个现象产生的原因:
由于shadow map分辨率有限,因此每个texel对应于场景中一个区域,如图中的区域1。点
p1
和
p2
对应于屏幕上的不同像素点,由于我们判断一个像素是否位于阴影中,是通过比较该点在以光源为视点的空间中的深度和对应texel中储存的深度值。
在这种情况下
d(p1)>s
,
d(p2)<s
, 因此
p1
将会被认为是在阴影中,但其实应该都不在阴影中,因此将产生shadow acne
最简单的解决方案是给shadow map中储存的深度值一个常量偏移。
如下图所示:
在这种情况下,
d(p1)<s
,
d(p2)<s
,不会出现shadow acne
Peter-pinning
然而,一个固定的偏移并不是万能的,当三角形相对于光源而言,斜率过大时,我们需要一个非常大的偏移,但这样的话,会导致出现peter-panning , 由于偏移过大,使得很多本该在阴影中的像素也被判断为不在阴影中。
如图:
原因如下:
因此我们希望可以测量多边形相对于光源的斜率,斜率越大,则使用越大的偏移量。恰好,图形显卡通过一种叫做slope-scaled-bias的光栅化状态进行内置支持。
struct D3D11_RASTERIZER_DESC
{
...
INT DepthBias;
FLOAT DepthBiasClamp;
FLOAT SlopeScaledDepthBias;
...
};
DepthBias:一个固定的偏移量
DepthBiasClamp:最大的偏移量,防止多边形相对于光源过陡时,产生过大的偏移量从而导致peter-pinning
SlopeScaledDepthBias:一个缩放因子,基于多边形的斜率控制偏移量。
由于我们想要基于相对于光源的多边形斜率进行偏移,因此我们在将场景渲染到shadow map上时,偏移shadow map的值,这样我们在渲染场景时,shadow map中对应的值就是已经偏移过的了。
PCF
用于采样shadow map的投影纹理坐标
(u,v)
并不总是在shadow map的texel上,通常会在四个texel之间,因此我们需要对其进行双线性插值。然而我们不应该插值shadow map对应的深度值,关于像素是否处于阴影中将得到一个不正确的结果。我们应该插值比较的结果(四个texel处是否处于阴影),这叫做百分比渐进过滤。我们使用点过滤(MIN_MAG_MIP_POINT)来采样
(u,v)
,
(u+Δx,v)
,
(u,v+Δx)
,
(u+Δx,v+Δx)
,
Δx=1SHADOW_MAP_SIZE
。由于采用的是点采样 , 这四个采样点将对应于最近的四个texel。如下图:
我们将对
s0,s1,s2,s3
每个都做shadow map测试,然后双线性插值比较结果。
float s0 = ShadowMap.Sample(SamplePoint , ProjTex.xy).r;
float s1 = ShadowMap.Sample(SamplePoint , ProjTex.xy + float2(dx , 0)).r;
float s2 = ShadowMap.Sample(SamplePoint , ProjTex.xy + float2(0 , dx)).r;
float s3 = ShadowMap.Sample(SamplePoint , ProjTex.xy + float2(dx , dx)).r;
// 不在阴影中为1,在阴影中为0
float reslut0 = depth <= s0;
float result1 = depth <= s1;
float result2 = depth <= s2;
float result3 = depth <= s3;
// 计算在texel space中的坐标
float2 texPos = SHADOW_MAP_SIZE * ProjTex.xy;
// 然后获取其小数部分,小数部分代表s0~s1 , s0~s2这段占了多少比例
float2 t = frac(texPos);
// 双线性插值比较结果
return lerp( lerp(result0 , result1 , t.x) , lerp(result2 , result3) , t.y );
然而正如上面所描述的,我们需要四次纹理采样,GPU上纹理采样是比较耗性能的。不过,DirectX11通过SampleCmpLevelZero(…)内置支持PCF
SamplerComparisonState samShadow
{
Filter = COMPARISION_MIN_MAG_LINEAR_MIP_POINT;
// 上面代码中的比较操作
Comparison = LESS_EQUAL;
...
};
// 用法,depth是归一化后的SamplePosH.z
// 自动实现4-tap PCH
// 即我们上面实现的功能
percentLit = ShadowMap.SampleCmpLevelZero(samShadow , shadowPosH.xy , depth).r;