一、前言
其实点光源的阴影我上周已经搞好了,只是在忙些其他事情,没来得及写文章总结,目前稍微闲了一点,就来说说我是如何实现的点光源的阴影。
先来看看效果:
本篇文章你将了解到:
- 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)
二、实现阴影的基本想法
我们首先从光源位置看向场景,记下此时距离最近的距离。比如对于上图的两个方向,上面那条记录的是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}; 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++) { view_create_info.subresourceRange.baseArrayLayer = i; 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()); } }
|
这里的精髓在A处相当于这个View对应哪个面,此外B处components只是有了R通道,因为我们本来也不需要其他通道。
第二歩,准备好Shader
下面是阴影渲染的Shader
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| #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;
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() { 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: view = Math::LookAt(light_pos, light_pos + Vector3(1.0f, 0.0f, 0.0f), Vector3(0.0f, -1.0f, 0.0f)); break; case 1: view = Math::LookAt(light_pos, light_pos + Vector3(-1.0f, 0.0f, 0.0f), Vector3(0.0f, -1.0f, 0.0f)); break; case 2: view = Math::LookAt(light_pos, light_pos + Vector3(0.0f, 1.0f, 0.0f), Vector3(0.0f, 0.0f, 1.0f)); break; case 3: view = Math::LookAt(light_pos, light_pos + Vector3(0.0f, -1.0f, 0.0f), Vector3(0.0f, 0.0f, -1.0f)); break; case 4: view = Math::LookAt(light_pos, light_pos + Vector3(0.0f, 0.0f, 1.0f), Vector3(0.0f, -1.0f, 0.0f)); break; case 5: view = Math::LookAt(light_pos, light_pos + Vector3(0.0f, 0.0f, -1.0f), Vector3(0.0f, -1.0f, 0.0f)); break; }
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); 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; view.light[0] = Math::ToVector4(light->GetWorldPosition()); view.light[1] = Vector4(near, far, 0, 0); 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();
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(); 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()); 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),这些东西我实现过后会为大家分享,要一步一步来,毕竟一口吃不成胖子。
其实还有一些遗留问题,例如当光源比较远时会有很严重的锯齿:
这里光源其实离两把AK很远,墙壁上的锯齿已经非常明显了。
我会在后续更新中解决这个问题。
五、参考