延迟渲染中的模板测试深度优化:光照体积(Light Volume)的内外表面双面剔除

📅 发布时间:2026/10/7 8:27:59
延迟渲染中的模板测试深度优化:光照体积(Light Volume)的内外表面双面剔除
延迟渲染中的模板测试深度优化光照体积Light Volume的内外表面双面剔除在很多刚搭好延迟渲染Deferred Shading的引擎里开发者常常会写出一个看似“优雅”却性能灾难的光照循环在光照阶段绘制一个铺满全屏的大三角形片元着色器里挂一个for (int i 0; i lightCount; i)对屏幕上的每一个像素都把场景里的上百个点光源挨个算一遍衰减。这种做法在光源只有十几个的时候还能跑动但如果关卡策划在赛博朋克街道上布置了 300 盏路灯和霓虹灯光照 Pass 的 GPU 耗时会直接飙升到 25ms 以上。因为在全屏 200 万个像素里某一个具体的路灯其实只照亮了地面上直径 5 米的一小块圆形区域其余 98% 的屏幕像素完全处于该光源的影响半径Radius之外。让全屏像素陪着这盏灯一起执行复杂的 PBR 高光与漫反射微积分是纯粹的 GPU 算力挥霍。将光源的影响范围具象化为一个三维的几何光照体积网格Light Volume Sphere Mesh是解决多光源裁剪的标准解法。然而单纯把光照体积扔进光栅化管线会立刻遭遇一个极其恶心的几何悖论如果摄像机走进了光照球体内部相机的近裁剪面会把球体的正面三角形全部切碎丢弃导致光照在一瞬间神秘熄灭解决这个悖论、并实现极致像素剔除的工业级王牌利器是基于硬件模板测试Stencil Buffer的双面体积剔除算法。光照球体的几何两难困境假设场景中有一盏影响半径为 $R$ 的点光源我们在世界空间以光源坐标为中心生成一个半径为 $R$ 的低多边形球体网格如 320 个三角面的二十面体细分球绘制正面Front-Facing Culling开启顺时针面剔除剔除背面只画正面。深度测试设为GL_LEQUAL表面在场景几何体前面时通过。致命死穴当玩家操纵角色一步步靠近这盏灯摄像机钻进了光照球的包围半径内。此时光照球的正面面片直接穿透了主相机的近裁剪面Near Plane被硬件裁剪管线完全丢弃。正面没了光栅化片元为 0这盏灯直接在玩家眼前原地消失绘制背面Back-Facing Culling翻转为只画背面。深度测试设为GL_GEQUAL球体背面在场景表面后面时通过。性能暗伤虽然相机进入球体内部不会熄灭但在大量空白天空背景或深度无穷远处背面三角形依然会穿透深度测试引发海量的无效片元光照计算。真正受光照影响的像素必须在空间中满足严密的集合交集关系场景物体的真实几何表面深度必须严格夹在光照球体的“正面外表面”与“背面内表面”之间硬件模板测试Stencil Test的双通道收敛法利用现代 GPU 硬件内置的 8 位 Stencil 模板缓冲区我们可以在不执行任何片元光照着色关闭色彩与深度写入像素填充速率拉满到极致的前提下以纳秒级速度标记出所有有效受光像素阶段一模板体积标记 PassDepth-Fail / Two-Sided Stencil关闭颜色写入Color Write Mask 0关闭深度写入开启双面模板测试正面多边形Front Faces若深度测试失败Depth Fail即球体正面在场景物体之后将模板值加 1VK_STENCIL_OP_INCREMENT_AND_WRAP背面多边形Back Faces若深度测试失败Depth Fail即球体背面在场景物体之后将模板值减 1VK_STENCIL_OP_DECREMENT_AND_WRAP。经过这套经典的“Carmacks Reverse”几何相交测试推演场景物体在光照球体前方正面和背面都在物体后面一加一减最终模板值归零场景物体在光照球体后方正面和背面都在物体前面都不触发 Depth Fail最终模板值依然为零唯独当场景表面正好被包裹在光照球体内部时正面在物体前不减背面在物体后加 1最终留在模板缓冲区中的像素值绝对非零Stencil ! 0阶段二光照着色 Pass绘制一个全屏四边形或者反向绘制球体开启颜色混合与写入。将模板测试配置为仅在Stencil ! 0时通过并顺手将模板值清零复原。只有真正被光芒笼罩的有效像素才会执行昂贵的 PBR 片元计算剔除率高达85% 到 95%生产级 Vulkan 1.3 模板光照管线配置核心代码下面展示配置双面模板标记管线的 C 生产级实现#include vulkan/vulkan.h // 创建模板体积标记专属 Pipeline VkPipeline CreateStencilVolumePipeline(VkDevice device, VkPipelineLayout layout) { // 1. 深度与模板状态配置 VkPipelineDepthStencilStateCreateInfo dsInfo{}; dsInfo.sType VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO; dsInfo.depthTestEnable VK_TRUE; dsInfo.depthWriteEnable VK_FALSE; // 绝对不污染主深度缓冲 dsInfo.depthCompareOp VK_COMPARE_OP_LESS_OR_EQUAL; dsInfo.stencilTestEnable VK_TRUE; // 正面多边形规则Depth Fail 时递增 dsInfo.front.failOp VK_STENCIL_OP_KEEP; dsInfo.front.passOp VK_STENCIL_OP_KEEP; dsInfo.front.depthFailOp VK_STENCIL_OP_INCREMENT_AND_WRAP; dsInfo.front.compareOp VK_COMPARE_OP_ALWAYS; dsInfo.front.compareMask 0xFF; dsInfo.front.writeMask 0xFF; // 背面多边形规则Depth Fail 时递减 dsInfo.back.failOp VK_STENCIL_OP_KEEP; dsInfo.back.passOp VK_STENCIL_OP_KEEP; dsInfo.back.depthFailOp VK_STENCIL_OP_DECREMENT_AND_WRAP; dsInfo.back.compareOp VK_COMPARE_OP_ALWAYS; dsInfo.back.compareMask 0xFF; dsInfo.back.writeMask 0xFF; // 2. 光栅化状态双面绘制 (禁用剔除) VkPipelineRasterizationStateCreateInfo rasterizer{}; rasterizer.sType VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO; rasterizer.cullMode VK_CULL_MODE_NONE; // 必须同时光栅化正面与背面 rasterizer.polygonMode VK_POLYGON_MODE_FILL; // 3. 颜色混合关闭所有颜色输出写掩码像素填充率狂飙 VkPipelineColorBlendAttachmentState colorBlendAttachment{}; colorBlendAttachment.colorWriteMask 0; // 零带宽开销 VkPipelineColorBlendStateCreateInfo colorBlending{}; colorBlending.sType VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO; colorBlending.attachmentCount 1; colorBlending.pAttachments colorBlendAttachment; // ... 组装生成 GraphicsPipeline ... return VK_NULL_HANDLE; }工业化落地的避坑指南光照球体多边形面数的黄金比例光照球只是一个用来圈定范围的粗糙包围体千万不要使用包含上万顶点的精细球体一个只有120 到 320 个多边形的低模球体足以覆盖几何边界面数过高会导致顶点着色器反客为主成为新瓶颈。与分块延迟渲染Tiled / Clustered Deferred的技术选型权衡模板体积剔除是极其经典的纯 GPU 硬件管线方案几乎能在任意支持硬件 Stencil 的设备上稳健运行。但当动态点光源超过 1000 盏时频繁切换模板状态和绘制球体会带来过多的 DrawCall。在现代 3A 引擎中更先进的路线是分簇延迟渲染Clustered Shading利用 Compute Shader 在三维视锥网格中提前计算光源索引列表。但在常规规模50 到 200 盏动态光源的场景中模板体积剔除依然是最简洁、最皮实且无额外显存消耗的高性价比方案。用硬件模板测试把光芒收敛进精确的物理体积消灭每一处多余的光照空转。深挖现代硬件底层的每一个寄存器潜能才能在群星璀璨的夜景中始终让帧率稳稳飞驰。