DX12龙书系列十五:阴影
shadowmap的基本原理就不再介绍了,直接看我们要怎么修改实现阴影。
我们把ShadowMap的逻辑分离出来,单独新建一个类。我们需要创建一个默认堆,用来存储对应的阴影深度图。
void ShadowMap::BuildResource()
D3D12_RESOURCE_DESC texDesc;
ZeroMemory(&texDesc, sizeof(D3D12_RESOURCE_DESC));
texDesc.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D;
texDesc.Alignment = 0;
texDesc.Width = mWidth;
texDesc.Height = mHeight;
texDesc.DepthOrArraySize = 1;
texDesc.MipLevels = 1;
texDesc.Format = mFormat;
texDesc.SampleDesc.Count = 1;
texDesc.SampleDesc.Quality = 0;
texDesc.Layout = D3D12_TEXTURE_LAYOUT_UNKNOWN;
texDesc.Flags = D3D12_RESOURCE_FLAG_ALLOW_DEPTH_STENCIL;
D3D12_CLEAR_VALUE optClear;
optClear.Format = DXGI_FORMAT_D24_UNORM_S8_UINT;
optClear.DepthStencil.Depth = 1.0f;
optClear.DepthStencil.Stencil = 0;
ThrowIfFailed(md3dDevice->CreateCommittedResource(
&CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_DEFAULT),
D3D12_HEAP_FLAG_NONE,
&texDesc,
D3D12_RESOURCE_STATE_GENERIC_READ,
&optClear,
IID_PPV_ARGS(&mShadowMap)));
}
同样的,就像深度图一样,我们也需要创建对应的DSV,因为还需要在shader里面访问,所以还要创建对应的SRV。
void ShadowMap::BuildDescriptors()
// Create SRV to resource so we can sample the shadow map in a shader program.
D3D12_SHADER_RESOURCE_VIEW_DESC srvDesc = {};
srvDesc.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING;
srvDesc.Format = DXGI_FORMAT_R24_UNORM_X8_TYPELESS;
srvDesc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D;
srvDesc.Texture2D.MostDetailedMip = 0;
srvDesc.Texture2D.MipLevels = 1;
srvDesc.Texture2D.ResourceMinLODClamp = 0.0f;
srvDesc.Texture2D.PlaneSlice = 0;
md3dDevice->CreateShaderResourceView(mShadowMap.Get(), &srvDesc, mhCpuSrv);
// Create DSV to resource so we can render to the shadow map.
D3D12_DEPTH_STENCIL_VIEW_DESC dsvDesc;
dsvDesc.Flags = D3D12_DSV_FLAG_NONE;
dsvDesc.ViewDimension = D3D12_DSV_DIMENSION_TEXTURE2D;
dsvDesc.Format = DXGI_FORMAT_D24_UNORM_S8_UINT;
dsvDesc.Texture2D.MipSlice = 0;
md3dDevice->CreateDepthStencilView(mShadowMap.Get(), &dsvDesc, mhCpuDsv);
void ShadowMapApp::UpdateShadowTransform(GameTimer& gt)
// Only the first "main" light casts a shadow.
XMVECTOR lightDir = XMLoadFloat3(&mRotatedLightDirections[0]);
XMVECTOR lightPos = -2.0f * mSceneBounds.Radius * lightDir;
XMVECTOR targetPos = XMLoadFloat3(&mSceneBounds.Center);
XMVECTOR lightUp = XMVectorSet(0.0f, 1.0f, 0.0f, 0.0f);
XMMATRIX lightView = XMMatrixLookAtLH(lightPos, targetPos, lightUp);
XMStoreFloat3(&mLightPosW, lightPos);
// Transform bounding sphere to light space.
XMFLOAT3 sphereCenterLS;
XMStoreFloat3(&sphereCenterLS, XMVector3TransformCoord(targetPos, lightView));
// Ortho frustum in light space encloses scene.
float l = sphereCenterLS.x - mSceneBounds.Radius;
float b = sphereCenterLS.y - mSceneBounds.Radius;
float n = sphereCenterLS.z - mSceneBounds.Radius;
float r = sphereCenterLS.x + mSceneBounds.Radius;
float t = sphereCenterLS.y + mSceneBounds.Radius;
float f = sphereCenterLS.z + mSceneBounds.Radius;
mLightNearZ = n;
mLightFarZ = f;
XMMATRIX lightProj = XMMatrixOrthographicOffCenterLH(l, r, b, t, n, f);
// Transform NDC space [-1,+1]^2 to texture space [0,1]^2
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);
XMMATRIX S = lightView * lightProj * T;
XMStoreFloat4x4(&mLightView, lightView);
XMStoreFloat4x4(&mLightProj, lightProj);
XMStoreFloat4x4(&mShadowTransform, S);
}
我们根据灯光的位置和朝向,可以算出灯光空间下的中心点坐标,然后把对应的正交矩阵算出来,因为我们要把最终的值存进贴图里,所以要从[-1,1]转到[0,1]的范围。
void ShadowMapApp::UpdateShadowPassCB(GameTimer& gt)
XMMATRIX view = XMLoadFloat4x4(&mLightView);
XMMATRIX proj = XMLoadFloat4x4(&mLightProj);
XMMATRIX viewProj = XMMatrixMultiply(view, proj);
XMMATRIX invView = XMMatrixInverse(&XMMatrixDeterminant(view), view);
XMMATRIX invProj = XMMatrixInverse(&XMMatrixDeterminant(proj), proj);
XMMATRIX invViewProj = XMMatrixInverse(&XMMatrixDeterminant(viewProj), viewProj);
UINT w = mShadowMap->Width();
UINT h = mShadowMap->Height();
XMStoreFloat4x4(&mShadowPassCB.view, XMMatrixTranspose(view));
XMStoreFloat4x4(&mShadowPassCB.invView, XMMatrixTranspose(invView));
XMStoreFloat4x4(&mShadowPassCB.proj, XMMatrixTranspose(proj));
XMStoreFloat4x4(&mShadowPassCB.invProj, XMMatrixTranspose(invProj));
XMStoreFloat4x4(&mShadowPassCB.viewProj, XMMatrixTranspose(viewProj));
XMStoreFloat4x4(&mShadowPassCB.invViewProj, XMMatrixTranspose(invViewProj));
mShadowPassCB.eyePos = mLightPosW;
mShadowPassCB.RenderTargetSize = XMFLOAT2((float)w, (float)h);
mShadowPassCB.InvRenderTargetSize = XMFLOAT2(1.0f / w, 1.0f / h);
mShadowPassCB.nearZ = mLightNearZ;
mShadowPassCB.farZ = mLightFarZ;
auto currPassCB = currentFrameRes->passUploader.get();
currPassCB->CopyData(1, mShadowPassCB);
void ShadowMapApp::DrawSceneToShadowMap(ID3D12GraphicsCommandList* mCommandList)
mCommandList->RSSetViewports(1, &mShadowMap->Viewport());
mCommandList->RSSetScissorRects(1, &mShadowMap->ScissorRect());
// Change to DEPTH_WRITE.
mCommandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(mShadowMap->Resource(),
D3D12_RESOURCE_STATE_GENERIC_READ, D3D12_RESOURCE_STATE_DEPTH_WRITE));
UINT passCBByteSize = DXUtil::calcConstantBufferByteSize(sizeof(PassConstants));
// Clear the back buffer and depth buffer.
mCommandList->ClearDepthStencilView(mShadowMap->Dsv(),
D3D12_CLEAR_FLAG_DEPTH | D3D12_CLEAR_FLAG_STENCIL, 1.0f, 0, 0, nullptr);
// Set null render target because we are only going to draw to
// depth buffer. Setting a null render target will disable color writes.
// Note the active PSO also must specify a render target count of 0.
mCommandList->OMSetRenderTargets(0, nullptr, false, &mShadowMap->Dsv());
// Bind the pass constant buffer for the shadow map pass.
auto passCB = currentFrameRes->passUploader->getRes();
D3D12_GPU_VIRTUAL_ADDRESS passCBAddress = passCB->GetGPUVirtualAddress() + 1 * passCBByteSize;
mCommandList->SetGraphicsRootConstantBufferView(1, passCBAddress);
mCommandList->SetPipelineState(psoMap["shadow_opaque"].Get());
DrawRenderItems(mCommandList, renderItemLayer[(int)RenderLayer::Opaque]);
// Change back to GENERIC_READ so we can read the texture in a shader.
mCommandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(mShadowMap->Resource(),
D3D12_RESOURCE_STATE_DEPTH_WRITE, D3D12_RESOURCE_STATE_GENERIC_READ));
}
渲染的时候,我们把RenderTarget设置为null,因为我们只需要depth的信息就好了,这样性能会更好。
然后看一下shader,首先是阴影:
struct VertexIn
float3 PosL : POSITION;
float2 TexC : TEXCOORD;
struct VertexOut
float4 PosH : SV_POSITION;
float2 TexC : TEXCOORD;
VertexOut VS(VertexIn vin)
VertexOut vout = (VertexOut)0.0f;
MaterialData matData = gMaterialData[gMaterialIndex];
// Transform to world space.
float4 posW = mul(float4(vin.PosL, 1.0f), gWorld);
// Transform to homogeneous clip space.
vout.PosH = mul(posW, gViewProj);
// Output vertex attributes for interpolation across triangle.
float4 texC = mul(float4(vin.TexC, 0.0f, 1.0f), gTexTransform);
vout.TexC = mul(texC, matData.MatTransform).xy;
return vout;
// This is only used for alpha cut out geometry, so that shadows
// show up correctly. Geometry that does not need to sample a
// texture can use a NULL pixel shader for depth pass.
void PS(VertexOut pin)
// Fetch the material data.
MaterialData matData = gMaterialData[gMaterialIndex];
float4 diffuseAlbedo = matData.DiffuseAlbedo;
uint diffuseMapIndex = matData.DiffuseMapIndex;
// Dynamically look up the texture in the array.
diffuseAlbedo *= gTextureMaps[diffuseMapIndex].Sample(gsamAnisotropicWrap, pin.TexC);
#ifdef ALPHA_TEST
// Discard pixel if texture alpha < 0.1. We do this test as soon
// as possible in the shader so that we can potentially exit the
// shader early, thereby skipping the rest of the shader code.
clip(diffuseAlbedo.a - 0.1f);
#endif
}
这里要注意,PS我们不需要任何返回值,但如果你有alpha_test,那么我们可以通过clip防止他写入深度图。
VertexOut VS(VertexIn vin)
VertexOut vout = (VertexOut)0.0f;
// Fetch the material data.
MaterialData matData = gMaterialData[gMaterialIndex];
// Transform to world space.
float4 posW = mul(float4(vin.PosL, 1.0f), gWorld);
vout.PosW = posW.xyz;
// Assumes nonuniform scaling; otherwise, need to use inverse-transpose of world matrix.
vout.NormalW = mul(vin.NormalL, (float3x3)gWorld);
vout.TangentW = mul(vin.TangentU, (float3x3)gWorld);
// Transform to homogeneous clip space.
vout.PosH = mul(posW, gViewProj);
// Output vertex attributes for interpolation across triangle.
float4 texC = mul(float4(vin.TexC, 0.0f, 1.0f), gTexTransform);
vout.TexC = mul(texC, matData.MatTransform).xy;
// Generate projective tex-coords to project shadow map onto scene.
vout.ShadowPosH = mul(posW, gShadowTransform);
return vout;
float4 PS(VertexOut pin) : SV_Target
// Fetch the material data.
MaterialData matData = gMaterialData[gMaterialIndex];
float4 diffuseAlbedo = matData.DiffuseAlbedo;
float3 fresnelR0 = matData.FresnelR0;
float roughness = matData.Roughness;
uint diffuseMapIndex = matData.DiffuseMapIndex;
uint normalMapIndex = matData.NormalMapIndex;
// Dynamically look up the texture in the array.
diffuseAlbedo *= gTextureMaps[diffuseMapIndex].Sample(gsamAnisotropicWrap, pin.TexC);
#ifdef ALPHA_TEST
// Discard pixel if texture alpha < 0.1. We do this test as soon
// as possible in the shader so that we can potentially exit the
// shader early, thereby skipping the rest of the shader code.
clip(diffuseAlbedo.a - 0.1f);
#endif
// Interpolating normal can unnormalize it, so renormalize it.
pin.NormalW = normalize(pin.NormalW);
float4 normalMapSample = gTextureMaps[normalMapIndex].Sample(gsamAnisotropicWrap, pin.TexC);
float3 bumpedNormalW = NormalSampleToWorldSpace(normalMapSample.rgb, pin.NormalW, pin.TangentW);
// Uncomment to turn off normal mapping.
//bumpedNormalW = pin.NormalW;
// Vector from point being lit to eye.
float3 toEyeW = normalize(gEyePosW - pin.PosW);
// Light terms.
float4 ambient = gAmbientLight*diffuseAlbedo;
// Only the first light casts a shadow.
float3 shadowFactor = float3(1.0f, 1.0f, 1.0f);
shadowFactor[0] = CalcShadowFactor(pin.ShadowPosH);
const float shininess = (1.0f - roughness) * normalMapSample.a;
Material mat = { diffuseAlbedo, fresnelR0, shininess };
float4 directLight = ComputeLighting(gLights, mat, pin.PosW,
bumpedNormalW, toEyeW, shadowFactor);
float4 litColor = ambient + directLight;
// Add in specular reflections.
float3 r = reflect(-toEyeW, bumpedNormalW);
float4 reflectionColor = gCubeMap.Sample(gsamLinearWrap, r);
float3 fresnelFactor = SchlickFresnel(fresnelR0, bumpedNormalW, r);
litColor.rgb += shininess * fresnelFactor * reflectionColor.rgb;
// Common convention to take alpha from diffuse albedo.
litColor.a = diffuseAlbedo.a;
return litColor;
float CalcShadowFactor(float4 shadowPosH)
// Complete projection by doing division by w.
shadowPosH.xyz /= shadowPosH.w;
// Depth in NDC space.
float depth = shadowPosH.z;
uint width, height, numMips;
gShadowMap.GetDimensions(0, width, height, numMips);
// Texel size.
float dx = 1.0f / (float)width;
float percentLit = 0.0f;
const float2 offsets[9] =
float2(-dx, -dx), float2(0.0f, -dx), float2(dx, -dx),
float2(-dx, 0.0f), float2(0.0f, 0.0f), float2(dx, 0.0f),
float2(-dx, +dx), float2(0.0f, +dx), float2(dx, +dx)
[unroll]
for(int i = 0; i < 9; ++i)