一、前言

其实点光源的阴影我上周已经搞好了,只是在忙些其他事情,没来得及写文章总结,目前稍微闲了一点,就来说说我是如何实现的点光源的阴影。

先来看看效果:

output1080vid

本篇文章你将了解到:

  • Vulkan中的Push Constant
  • Cubemap
  • 我如何实现的点光源阴影

本篇文章的代码在RickSchanze/ElbowEngine at 点光源的阴影 (github.com)

上一篇:点光源(Specialization Constant)以及变换 | 喜多喜多のBlog (kita-blog.vercel.app)

本篇:点光源的阴影 | 喜多喜多のBlog (kita-blog.vercel.app)

下一篇:天空盒,以及新的Shader编译流程 | 喜多喜多のBlog (kita-blog.vercel.app)

二、实现阴影的基本想法

image-20240818125311722

我们首先从光源位置看向场景,记下此时距离最近的距离。比如对于上图的两个方向,上面那条记录的是C到光源的距离,下面记录的是B到光源的距离。

然后在渲染物体时,假如我现在在渲染A点,那么计算A到光源的距离,同时取A到光源这个方向的、刚才提到的记录的最短距离,进行比较,我们发现还是刚才记录的C点离光照的距离短一点,那么就说明A点在阴影中。同理对于B点,我们发现与记录的一致,那么说明这个没有在阴影中。

对于点光源,它的光是向四面八方发散的,所以从需要从六个面渲染场景,即渲染六次。这也是实时点光源消耗非常大的原因。

三、实现

接下来逐一介绍我如何实现。

第一歩,准备好Image

点光源的阴影需要Cubemap来呈现,因此我新增了一个Cubemap,它里面存储了Cubemap需要生成的六个ImageView(六个面)。

1
2
3
4
5
6
7
8
9
10
11
12
class Cubemap : public Image
{
public:
ImageView* GetFaceView(ECubemapFace face);
ImageView* GetView() const { return cubemap_image_view_; }

protected:
TArray<ImageView*> cubemap_image_face_views_;
TArray<AnsiString> cubemap_face_view_names_;
ImageView* cubemap_image_view_;
AnsiString cubemap_image_view_name_;
};

对于生成的Image的信息,由于我们只需要存储一个距离,因此格式设置成了eR32Sfloat,并且因为需要渲染六个面,因此它的arrayLayer为6。

那么如何创建ImageView呢?很简单,只需要循环即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

void Cubemap::CreateCubemapImageViews(const AnsiString& name)
{
VulkanContext& context = *VulkanContext::Get();
vk::ImageViewCreateInfo view_create_info = {};
view_create_info.image = GetHandle();
view_create_info.viewType = vk::ImageViewType::e2D;
view_create_info.format = GetFormat();
view_create_info.components = {vk::ComponentSwizzle::eR}; // B处
view_create_info.subresourceRange = {vk::ImageAspectFlagBits::eColor, 0, GetMipLevel(), 0, 1};
cubemap_image_face_views_.resize(6);
cubemap_face_view_names_.resize(6);
for (int i = 0; i < 6; i++)
{
// clang-format off
view_create_info.subresourceRange.baseArrayLayer = i; // A处
cubemap_face_view_names_[i] = name + "_" + std::to_string(i);
cubemap_image_face_views_[i] = new ImageView(context.GetLogicalDevice()->GetHandle().createImageView(view_create_info), cubemap_face_view_names_[i].c_str());
// clang-format on
}
}

这里的精髓在A处相当于这个View对应哪个面,此外B处components只是有了R通道,因为我们本来也不需要其他通道。

第二歩,准备好Shader

下面是阴影渲染的Shader

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// vert shader
#version 450

...
layout(location = 0) out vec4 outWorldPos;
layout(location = 1) out vec3 outLightPos;
...
layout(push_constant) uniform CameraView {
mat4 view;
} camera_view; // 传入的视图矩阵
...
void main() {
gl_Position = ubo_view.projection * camera_view.view * ubo_instance.model * vec4(inPosition, 1.0);

outWorldPos = ubo_instance.model * vec4(inPosition, 1.0); // 传出这个片元的世界坐标
outLightPos = ubo_view.light[0].xyz;
}

这里只写了比较重要的部分。我们输出片元世界坐标和光源的位置坐标。

对于摄像机的视图矩阵,我们使用push constant传入,对于push constant,我们需要再管线创建时指定:

1
2
3
TArray<vk::PushConstantRange> push_constant_ranges;
// 设置push constant参数...
pipeline_layout_info.setPushConstantRanges(push_constant_ranges);

想传入数据时使用vkCmdPushConstants(3) (khronos.org)传入。我这里使用了vulkancpp所以是

1
cb.pushConstants(pipeline_->GetPipelineLayout(), ShaderStage2VKShaderStage(stage), offset, size, data);

本质是一样的。

1
2
3
4
5
6
7
8
9
10
11
#version 450

layout(location = 0) out float outDepth;

layout(location = 0) in vec4 inPos;
layout(location = 1) in vec3 inLightPos;

void main() {
vec3 lightVec = inPos.xyz - inLightPos;
outDepth = length(lightVec);
}

这里计算了点到光源的距离,并作为输出。

对于物体shader,我们也需要进行修改

1
2
3
4
5
6
7
8
9
#version 450 vert shader
#extension GL_EXT_debug_printf : enable
...
layout(location = 4) out vec4 outWorldPosition;

void main() {
outWorldPosition = ubo_instance.model * vec4(inPosition, 1.0);
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#version 450 frag shader
layout(binding = 3) uniform samplerCube shadowCubeMap;

layout(location = 0) out vec4 outColor;

void main() {
计算outColor...
// 阴影
vec3 lightVec = inWorldPosition.xyz - ubo_point_lights.lights[0].position.xyz;
float sampledDist = texture(shadowCubeMap, lightVec).r;
float dist = length(lightVec);
float shadow = (dist <= sampledDist + 0.1f) ? 1.0f : 0.1f;

outColor.rgb *= shadow;
}

就是比较距离来判断是不是阴影。

第三歩,准备好Render Pass

这里创建了一个PointLightShadowPass,下面列出了比较重要函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class PointLightShadowPass : public RHI::Vulkan::RenderPass
{
public:
void SetupCubemap();
void CleanCubemap();

Matrix4x4 GetFaceViewMatrix(Comp::Light* camera, int index);
void BeginDrawFace(vk::CommandBuffer cb, Material* mat, Comp::Light* light, int index, float near, float far);
void EndDrawFace(vk::CommandBuffer cb);

RHI::Vulkan::ImageView* GetOutputCubemapView() const;

private:
RHI::Vulkan::Image* color_ = nullptr;
RHI::Vulkan::Image* depth_ = nullptr;
RHI::Vulkan::ImageView* color_view_ = nullptr;
RHI::Vulkan::ImageView* depth_view_ = nullptr;

TStaticArray<RHI::Vulkan::Framebuffer*, 6> cubemap_framebuffers_;

RHI::Vulkan::Cubemap* shadow_map_;
};

为每一个framebuffer设置好image view:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void PointLightShadowPass::SetupFramebuffer()
{
// 这里创建了Cubemap
SetupCubemap();
...

vk::FramebufferCreateInfo fb;
fb.renderPass = handle_;
fb.width = width_;
fb.height = height_;
fb.layers = 1;
for (int i = 0; i < 6; i++)
{
attachments[0] = shadow_map_->GetFaceView(static_cast<Cubemap::ECubemapFace>(i))->GetHandle();
fb.setAttachments(attachments);
cubemap_framebuffers_[i] = new Framebuffer(fb);
}
}

计算每一个面的View Matrix:

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
26
27
28
29
30
31
32
33
34
35
Matrix4x4 PointLightShadowPass::GetFaceViewMatrix(Comp::Light* light, int index)
{
if (index < 0 || index > 5)
{
LOG_ERROR_ANSI_CATEGORY(Vulkan, "Index {} out of range when getting face view matrix for point light shadow pass", index);
return GetMatrix4x4Identity();
}
using namespace RHI::Vulkan;
auto view = glm::mat4(1.0f);
Vector3 light_pos = light->GetWorldPosition();
switch (index)
{
case 0: // POSITIVE_X
view = Math::LookAt(light_pos, light_pos + Vector3(1.0f, 0.0f, 0.0f), Vector3(0.0f, -1.0f, 0.0f));
break;
case 1: // NEGATIVE_X
view = Math::LookAt(light_pos, light_pos + Vector3(-1.0f, 0.0f, 0.0f), Vector3(0.0f, -1.0f, 0.0f));
break;
case 2: // POSITIVE_Y
view = Math::LookAt(light_pos, light_pos + Vector3(0.0f, 1.0f, 0.0f), Vector3(0.0f, 0.0f, 1.0f));
break;
case 3: // NEGATIVE_Y
view = Math::LookAt(light_pos, light_pos + Vector3(0.0f, -1.0f, 0.0f), Vector3(0.0f, 0.0f, -1.0f));
break;
case 4: // POSITIVE_Z
view = Math::LookAt(light_pos, light_pos + Vector3(0.0f, 0.0f, 1.0f), Vector3(0.0f, -1.0f, 0.0f));
break;
case 5: // NEGATIVE_Z
view = Math::LookAt(light_pos, light_pos + Vector3(0.0f, 0.0f, -1.0f), Vector3(0.0f, -1.0f, 0.0f));
break;
}

// clang-format on
return view;
}

准备开始渲染:

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
26
27
28
29
void PointLightShadowPass::BeginDrawFace(vk::CommandBuffer cb, Material* mat, Comp::Light* light, int index, float near, float far)
{
vk::ClearValue clear_value[2];
clear_value[0].color = {0.0f, 0.0f, 0.0f, 1.0f};
clear_value[1].depthStencil = {{1.0f, 0}};

vk::RenderPassBeginInfo begin_info;
begin_info.renderPass = handle_;
begin_info.framebuffer = cubemap_framebuffers_[index]->GetHandle();
begin_info.renderArea.extent.width = width_;
begin_info.renderArea.extent.height = height_;
begin_info.setClearValues(clear_value);
glm::mat4 viewMatrix = GetFaceViewMatrix(light, index);
cb.beginRenderPass(begin_info, vk::SubpassContents::eInline);
// 更新view matrix
mat->PushConstant(cb, 0, sizeof(Matrix4x4), RHI::Vulkan::EShaderStage::Vertex, &viewMatrix);
mat->Use(cb, width_, height_);
struct UboView
{
Matrix4x4 proj;
Matrix4x4 light;
} view;
view.proj = Math::Perspective((float)(Constant::PI / 2.0), 1.0f, 0.1f, 1024.0f);
view.proj[1][1] *= -1; // TODO:
view.light[0] = Math::ToVector4(light->GetWorldPosition());
view.light[1] = Vector4(near, far, 0, 0);
// 更新projection矩阵和light light矩阵第一列是光源的位置
mat->Set("ubo_view", &view, sizeof(UboView));
}

第四歩,加入到Render Pipeline

这里直接看代码就行:

1
2
3
4
5
6
7
8
9
10
11
12
13
void LiteForwardRenderPipeline::Build()
{
forward_pass_ = RenderPassManager::GetOrCreateRenderPass<SimpleObjectShadingPass>(0, 0, "SimpleForwardPass");
RegisterRenderPass(forward_pass_);
shadow_pass_ = RenderPassManager::GetOrCreateRenderPass<PointLightShadowPass>(800, 800, "PointLightPass");
RegisterRenderPass(shadow_pass_);

Shader* shadow_vert = Shader::Create<PointLightShadowPassVertShader>(L"Shaders/PointLightShadowVert.spv", "PointLightShadowVert");
Shader* shadow_frag = Shader::Create<PointLightShadowPassFragShader>(L"Shaders/PointLightShadowFrag.spv", "PointLightShadowFrag");
shadow_material_ = MaterialManager::CreateMaterial(shadow_vert, shadow_frag, shadow_pass_, L"PointLightShadowMaterial");

AddImGuiGraphicsPipeline();
}

上面的代码加载了绘制阴影需要使用的vert shader和frag shader,并准备好了pass

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
void LiteForwardRenderPipeline::DrawBackbuffer(const RenderContextDrawParam& draw_param)
{
Comp::Camera* main = Comp::Camera::Main;
Super::DrawBackbuffer(draw_param);
auto cb = draw_param.command_buffer;
auto meshes_to_draw = CollectMeshesWithMaterial();

// 走一遍shadow pass
auto light = Comp::LightManager::Get()->GetLights();
if (!light.empty())
{
shadow_material_->SetModel(model_instances_.models, model_instances_.size);
for (int i = 0; i < 6; i++)
{
shadow_pass_->BeginDrawFace(cb, shadow_material_, light[0], i, main->near_plane, main->far_plane);
for (auto& meshes: meshes_to_draw | std::views::values)
{
for (int j = 0; j < meshes.size(); j++)
{
TArray dynamic_offsets = {j * static_cast<uint32_t>(GetDynamicUniformModelAligment())};
shadow_material_->DrawMesh(cb, *meshes[j], dynamic_offsets);
}
}
shadow_pass_->EndDrawFace(cb);
}
}
auto* out_view = shadow_pass_->GetOutputCubemapView();
// 走渲染pass
forward_pass_->Begin(cb, main->background_color);
for (auto& [material, meshes]: meshes_to_draw)
{
material->Use(cb);
material->SetPostionViewProjection(main);
material->SetCubeTexture("shadowCubeMap", *out_view, Sampler::GetDefaultSampler()); // A
material->SetModel(model_instances_.models, model_instances_.size);
for (int i = 0; i < meshes.size(); i++)
{
TArray dynamic_offsets = {i * static_cast<uint32_t>(GetDynamicUniformModelAligment())};
material->DrawMesh(cb, *meshes[i], dynamic_offsets);
}
}

forward_pass_->End(cb);
}

上面的代码实际进行了渲染,注意A处将上一个Pass的输出当做了渲染Pass的一个输入。

四、结语

本篇文章相对来说文字讲解的部分不多,基本都是代码(因为思想很简单),因此可能有点水

阴影这部分除了点光源阴影,其实还有非常非常多可以深入探索的地方,比如软阴影(PCF、PCSS),直射光的阴影(CSM),这些东西我实现过后会为大家分享,要一步一步来,毕竟一口吃不成胖子。

其实还有一些遗留问题,例如当光源比较远时会有很严重的锯齿:

image-20240818140021929

这里光源其实离两把AK很远,墙壁上的锯齿已经非常明显了。

我会在后续更新中解决这个问题。

五、参考