多 pass 渲染就像制作一部电影的后期制作流程,每一步都专门处理一种效果,最后合成最终画面。
每个 pass 专注一种效果,互不干扰:阴影 pass 只关心深度;几何 pass 只关心基础着色;后处理 pass 只关心特效。
ShadowMap
阴影贴图,通过深度信息来判断物体距离光源的前后遮挡关系,
ShadowMapPass(); SpotShadowMapPass();
|
PreDepth
预深度:确定哪些像素是可见的,先画一遍,只记录深度,不进行着色。
目的: 解决传统绘制重复渲染的问题, 比如一个像素的位置上,先绘制了远处的山,再绘制近处的树,这样远处的山的着色计算就浪费了。
解决方案: 第一遍,只记录深度信息,确定每个像素最终显示哪个物体;后续只给真正可见的像素上色。
void SceneRenderer::Init() { FramebufferSpecification preDepthFramebufferSpec; preDepthFramebufferSpec.Width = m_Specification.ViewportWidth; preDepthFramebufferSpec.Height = m_Specification.ViewportHeight; preDepthFramebufferSpec.DebugName = "PreDepth-Opaque"; preDepthFramebufferSpec.Attachments = { ImageFormat::DEPTH32FSTENCIL8UINT }; preDepthFramebufferSpec.DepthClearValue = 0.0f; Ref<Framebuffer> clearFramebuffer = Framebuffer::Create(preDepthFramebufferSpec); preDepthFramebufferSpec.ClearDepthOnLoad = false; Ref<Framebuffer> loadFramebuffer = Framebuffer::Create(preDepthFramebufferSpec); }
|
预深度管线:
PipelineSpecification pipelineSpec; pipelineSpec.DebugName = "PreDepth-Opaque"; pipelineSpec.TargetFramebuffer = clearFramebuffer; pipelineSpec.Shader = Renderer::GetShaderLibrary()->Get("PreDepth"); pipelineSpec.Layout = vertexLayout; pipelineSpec.InstanceLayout = instanceLayout; m_PreDepthPipeline = Pipeline::Create(pipelineSpec);
pipelineSpec.DebugName = "PreDepth-Anim"; pipelineSpec.Shader = Renderer::GetShaderLibrary()->Get("PreDepth_Anim"); pipelineSpec.BoneInfluenceLayout = boneInfluenceLayout; m_PreDepthPipelineAnim = Pipeline::Create(pipelineSpec);
|
预深度渲染:
void SceneRenderer::PreDepthPass() { uint32_t frameIndex = Renderer::GetCurrentFrameIndex(); m_GPUTimeQueries.DepthPrePassQuery = m_CommandBuffer->BeginTimestampQuery(); Renderer::BeginRenderPass(m_CommandBuffer, m_PreDepthPass); for (auto& [mk, dc] : m_StaticMeshDrawList) { const auto& transformData = m_MeshTransformMap.at(mk); Renderer::RenderStaticMeshWithMaterial( m_CommandBuffer, m_PreDepthPipeline, dc.StaticMesh, dc.MeshSource, dc.SubmeshIndex, m_SubmeshTransformBuffers[frameIndex].Buffer, transformData.TransformOffset, dc.InstanceCount, m_PreDepthMaterial ); } for (auto& [mk, dc] : m_DrawList) { const auto& transformData = m_MeshTransformMap.at(mk); if (!dc.IsRigged) Renderer::RenderMeshWithMaterial(..., m_PreDepthMaterial); } Renderer::EndRenderPass(m_CommandBuffer); Renderer::BeginRenderPass(m_CommandBuffer, m_PreDepthAnimPass); for (auto& [mk, dc] : m_DrawList) { if (dc.IsRigged) { const auto& boneTransformsData = m_MeshBoneTransformsMap.at(mk); Renderer::RenderMeshWithMaterial( m_CommandBuffer, m_PreDepthPipelineAnim, dc.Mesh, dc.MeshSource, dc.SubmeshIndex, m_SubmeshTransformBuffers[frameIndex].Buffer, transformData.TransformOffset, boneTransformsData.BoneTransformsBaseIndex, boneTransformsData.BoneTransformsStride, dc.InstanceCount, m_PreDepthMaterial ); } } Renderer::EndRenderPass(m_CommandBuffer); m_CommandBuffer->EndTimestampQuery(m_GPUTimeQueries.DepthPrePassQuery); }
|
预深度 Early-Z 优化:
pipelineSpecification.DepthOperator = DepthCompareOperator::Equal; pipelineSpecification.DepthWrite = false;
|
也就是说,预深度 Pass 会写一张帧缓冲,此缓冲只有深度信息,即最前面的(需要被着色的)物体的深度信息,计算着色时,将物体的深度值与预深度贴图对应的深度值进行匹配,如果匹配成功,则进行着色,匹配失败,跳过着色。
预深度 Pass 的核心思想就是用少量的计算(只算深度)换取大量的节省(避免无效的复杂光照计算),这在复杂场景中效果非常明显,特别是有很多重叠物体的情况下。
HZB(Hierarchical Z Buffer)
HZB 的核心思想是用 空间换时间:预先计算好多个分辨率的深度信息,让后续的遮挡查询、光线追踪等操作能够快速跳过大块的空白区域,大幅提升渲染效率。
绘制不同比例的深度图,这是查询一个物体是否被遮挡时,可以先根据物体的大小选择合适的 mip 级别,查询该区域的最远深度与物体的最近深度进行对比,如果物体的最近深度比该区域的最远深度大,说明物体被完全遮挡,否则使用更精细的 mip 进一步检查。
int mipLevel = log2(400) = 8; float mountainDepth = SampleHZB(mountainCenter, 8); if (houseDepth > mountainDepth) skipRendering(house);
|
HZB 的核心思想就是分而治之:
- 大区域用粗糙的信息快速判断
- 小区域用精细的信息准确判断
- 根据需要动态选择精度级别
这样既保证了准确性,又大幅提升了性能。就像你看地图时,先看全国地图找到大概方向,再看省级地图,最后看详细街道图一样。
PreIntergration
想象你要在一面湖水中看倒影,但湖面有波纹:
- 传统方法:从每个角度都重新计算光线如何在波纹表面反射
- 预积分方法:预先计算好不同粗糙度表面的 “平均反射效果”,需要时直接查表
SSR(Screen Square Reflection)
传统反射渲染从镜子的角度将场景渲染一遍,性能消耗翻倍,每个镜子都要这么做,场景渲染次数一直增加。
屏幕空间反射: 利用已经渲染的场景(所有像素的颜色和深度),对于需要反射的表面,计算反射光方向,沿着反射方向在已渲染的图像中查找对应的颜色,如果找到就作为反射的颜色,如果没找到,就显示天空的颜色。
SSR 的局限性,如果需要计算反射的像素在屏幕空间外,则无法拿到,需要用一些策略缓解:
- 智能 Fallback - 在 SSR 失效时平滑过渡到其他技术
- 扩展数据 - 渲染比显示更多的区域
- 时域复用 - 利用前几帧的信息
- 分层策略 - 重要的反射用高端技术,次要的用简单方法
- 预计算置信度 - 提前知道哪里 SSR 会失效
Light Culling
传统光照的性能灾难:如果场景中有一千盏灯,即使你无法看到所有的灯,也会计算这一千盏灯对场景的影响。
问题的本质:
(1)距离衰减: 现实中 光照的强度 = 光源强度 / (距离的平方)
,距离 50m 时,强度 = 100 / (50²) = 0.04
,基本上看不出来了,但传统方法还是会计算这个量
(2)屏幕空间局限性: 一个像素实际上只会收到附近几盏灯的影响
光照剔除的基本原理
核心思想: 分块处理,将屏幕切成小格子,每个格子只关心影响到它的灯光。
两阶段处理:
- 光照剔除:着色器中,先确定当前工作组负责的屏幕块,计算这个块在世界空间中的范围,构建该块的 Frustum,测试每盏灯是否与其相交,将测试结果写入全局缓冲区;
- 像素着色:在像素着色器中,先确定当前像素属于哪个块,获取该块的灯光列表,只计算列表中灯光的贡献。
屏幕空间块到世界空间有一个转换,即通过近平面的范围,构建一个视锥体,该视锥体内的灯光才影响此屏幕空间块的着色。
深度范围的计算方法
float CalculateTileDepthRange(ivec2 tileID) { float minDepth = 1000.0; float maxDepth = 0.0; for (int y = 0; y < 16; y++) { for (int x = 0; x < 16; x++) { ivec2 pixelCoord = tileID * 16 + ivec2(x, y); float pixelDepth = SampleDepthBuffer(pixelCoord); minDepth = min(minDepth, pixelDepth); maxDepth = max(maxDepth, pixelDepth); } } return TileDepthRange(minDepth, maxDepth); }
float CalculateTileDepthRangeOptimized(ivec2 tileID) { int mipLevel = CalculateMipForTileSize(16); vec2 hzbUV = (tileID + 0.5) / numTiles; float blockDepth = textureLod(HZBTexture, hzbUV, mipLevel).r; return blockDepth; }
|