ShadowMap

创建阴影分为两步:
一:以光源为视点,正交投影渲染整个场景,得到深度图(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;
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值