点光源阴影

TODO…

点光源阴影

概述

shadow map是一项使用非常普遍的技术,这里不再赘述。

点光源阴影,英文omnidirectional shadow maps。由于点光源是向空间中四面八方照射,光线方向的范围是球面,因此很容发现,点光源如果要产生阴影,也需要获取四面八方的shadow map。

这里借用learnopengl的图

learnopengl提到了CubeMap用来做这件事很棒,采样6张shadow map的事也可以交给显卡去做!

这里我们使用DX12实现

创建CubeMap相关资源

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Eigen::Vector2i depSize{512, 512};
// 一些其他代码...
D3D12_RESOURCE_DESC depthDesc{};
depthDesc.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D;
depthDesc.Alignment = 0;
depthDesc.Width = depSize.x();
depthDesc.Height = depSize.y();
depthDesc.DepthOrArraySize = 6;
depthDesc.MipLevels = 1;
depthDesc.Format = DXGI_FORMAT_R32_TYPELESS;
depthDesc.SampleDesc.Count = 1;
depthDesc.SampleDesc.Quality = 0;
depthDesc.Layout = D3D12_TEXTURE_LAYOUT_UNKNOWN;
depthDesc.Flags = D3D12_RESOURCE_FLAG_ALLOW_DEPTH_STENCIL;
D3D12_CLEAR_VALUE clearDesc{};
clearDesc.Format = DXGI_FORMAT_D32_FLOAT;
clearDesc.DepthStencil.Depth = 1.0f;
clearDesc.DepthStencil.Stencil = 0;
D3D12MA::ALLOCATION_DESC allocDesc{.HeapType = D3D12_HEAP_TYPE_DEFAULT};
ThrowIfFailed(_context->Allocator()->CreateResource(
&allocDesc, &depthDesc,
D3D12_RESOURCE_STATE_GENERIC_READ,
&clearVal,
&_resAlloc,
IID_PPV_ARGS(&_res)));

由于只有要将纹理交给像素着色器使用,还要作为深度图交给管线去写入,因此这里资源的格式指定为R32_TYPELESS,精度也指定为32位,反正是学习和实验无所谓(

初始状态指定为GENERIC_READ,指定为COMMON应该也没差,之后反正要切换状态

有了Resource,我们还需要为之后给着色器采样创建SRV

1
2
3
4
5
6
7
8
D3D12_SHADER_RESOURCE_VIEW_DESC srvDesc{};
srvDesc.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING;
srvDesc.Format = DXGI_FORMAT_R32_FLOAT;
srvDesc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURECUBE;
srvDesc.TextureCube.MostDetailedMip = 0;
srvDesc.TextureCube.MipLevels = 1;
srvDesc.TextureCube.ResourceMinLODClamp = 0.0f;
// 以下省略...

读取时的纹理格式指定为R32_FLOAT

接着为CubeMap的6张纹理创建生成各自的DSV,之后在渲染时传给管线。

1
2
3
4
5
6
7
8
9
for (size_t i = 0; i < 6; i++) {
D3D12_DEPTH_STENCIL_VIEW_DESC dsvDesc{};
dsvDesc.ViewDimension = D3D12_DSV_DIMENSION_TEXTURE2DARRAY;
dsvDesc.Format = DXGI_FORMAT_D32_FLOAT;
dsvDesc.Texture2DArray.MipSlice = 0;
dsvDesc.Texture2DArray.FirstArraySlice = i;
dsvDesc.Texture2DArray.ArraySize = 1;
// 以下省略...
}

也没什么特殊的,唯一需要注意的是格式要指定为D32_FLOAT,不是R32_FLOAT

阴影Shader

learnopengl介绍了用几何着色器一次性处理6次场景绘制,也描述了不一定能加速的理由,那我们在这里就不用几何着色器,而是最naive的真的画6次吧(主要是懒

阴影shader里要做的事情很少,而且由于只写入深度信息,连颜色都不需要输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
cbuffer _PreObject : register(b0) {
float4x4 g_model;
float4x4 g_mvp;
float4x4 g_invModel;
};

struct VertexIn {
float3 Pos : POSITION;
float3 Nor : NORMAL;
float2 UV0 : TEXCOORD0;
float4 Tan : TANGENT;
};

float4 VS(VertexIn vin) : SV_POSITION {
return mul(g_mvp, float4(vin.Pos, 1.0f));
}

void PS() {}

绘制阴影Pass

和定向光的阴影pass一样,把相机摆在光源坐标处看向场景,并记录深度信息。

这里我们以CubeMap在纹理数组里保存的顺序(就是X+,X-,Y+,Y-,Z+,Z-)来依次绘制。

定向光的相机使用的正交投影,因为定向光的光线间都是平行的照射到场景里。依此类推,点光源就应该使用透视投影,而且fov是90度,就可以生成无缝的CubeMap

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Eigen::Vector3f target[6] = {
Eigen::Vector3f{lightPos.x() + 1, lightPos.y(), lightPos.z()},
Eigen::Vector3f{lightPos.x() - 1, lightPos.y(), lightPos.z()},
Eigen::Vector3f{lightPos.x(), lightPos.y() + 1, lightPos.z()},
Eigen::Vector3f{lightPos.x(), lightPos.y() - 1, lightPos.z()},
Eigen::Vector3f{lightPos.x(), lightPos.y(), lightPos.z() + 1},
Eigen::Vector3f{lightPos.x(), lightPos.y(), lightPos.z() - 1}};
Eigen::Vector3f up[6] = {
Eigen::Vector3f{0, 1, 0},
Eigen::Vector3f{0, 1, 0},
Eigen::Vector3f{0, 0, -1},
Eigen::Vector3f{0, 0, 1},
Eigen::Vector3f{0, 1, 0},
Eigen::Vector3f{0, 1, 0}};

首先提前写好透视投影相机的参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
CD3DX12_VIEWPORT viewport(0.0f, 0.0f, depSize.x(), depSize.y());
cmdList->RSSetViewports(1, &viewport);
CD3DX12_RECT rect(0, 0, depSize.x(), depSize.y());
cmdList->RSSetScissorRects(1, &rect);
auto toWrite = CD3DX12_RESOURCE_BARRIER::Transition(depthMap->GetResource(), depthMap->GetInitState(), D3D12_RESOURCE_STATE_DEPTH_WRITE);
cmdList->ResourceBarrier(1, &toWrite);
for (size_t i = 0; i < 6; i++) {
// 以上省略 depthView 的获取...
cmdList->ClearDepthStencilView(depthView, D3D12_CLEAR_FLAG_DEPTH, 1.0f, 0, 0, nullptr);
cmdList->OMSetRenderTargets(0, nullptr, true, &depthView);
GlobalCBuffer global{};
global.View = LookAtLH(lightPos, target[i], up[i]);
global.Proj = PerspectiveLH(Radian(90.0f), 1, 0.1f, 1000.0f);
global.VP = global.Proj * global.View;
// 上传参数到 cbuffer...
// 遍历物体绘制...
}
auto toInit = CD3DX12_RESOURCE_BARRIER::Transition(depthMap->GetResource(), D3D12_RESOURCE_STATE_DEPTH_WRITE, depthMap->GetInitState());
cmdList->ResourceBarrier(1, &toInit);

绘制过程也很简单,首先别忘了设置视口,也别忘了把CubeMap的状态转换为DEPTH_WRITE。

相机参数在CPU计算好后传到cbuffer

最后也别忘了把CubeMap的状态转换回去

绘制着色Pass

和普通着色的区别基本只在着色器里,CPU端唯一要做的只有绑定CubeMap的SRV到管线上

1
2
3
4
5
6
7
8
9
10
11
12
13
float3 toLight = light.WorldPos - posW;
float dist = length(toLight);
float3 dirL = toLight / dist;
float vis = 0;

float depth = _PointShadow.Sample(g_Sampler, -dirL).r;
float far = POINT_LIGHT_FARZ;
float near = POINT_LIGHT_NEARZ;
float bias = POINT_LIGHT_BIAS;
depth = depth * (far - near);
vis = (dist <= depth + bias) ? 1 : 0;

// 后续着色...

点光源阴影
https://ksgfk.github.io/2023/09/08/D3D12-点光源阴影/
作者
ksgfk
发布于
2023年9月8日
许可协议