大场景视距裁剪与 Hi-Z 遮挡剔除在移动端的实操与优化
在开放世界大场景游戏中,仅靠视锥体裁剪(Frustum Culling)已无法遏制渲染几何负载的膨胀。当玩家站在山谷或城镇街道中央时,视野前方虽然包含了数万个建筑、植被和道具,但其中超过 70% 的物体实际上完全被前方的大山或临街城墙遮挡。如果将这些完全不可见的物体依然送入渲染管线,会产生巨量的顶点吞吐浪费与无效的 Early-Z 深度测试开销。
层次深度缓冲(Hierarchical Z-Buffer, Hi-Z)遮挡剔除利用上一帧生成的深度金字塔(Depth Pyramid),在 GPU Compute Shader 中对候选包围盒(AABB)进行快速保守可见性测试,结合 GPU Driven 渲染架构的间接绘制(DrawMeshInstancedIndirect),可以在移动端将 Draw Call 与顶点处理压力削减 60% 以上。
Hi-Z 遮挡剔除工作流与原理解析
Hi-Z 的核心思想是构建一个类似 Mipmap 的深度纹理金字塔。在标准纹理采样中,Mipmap 生成采用平均值(Average Downsampling);但在 Hi-Z 深度金字塔中,每个降采样像素必须取其对应的 4 个子像素的最大深度值(在 Reverse-Z 规范下为最小深度值,代表最靠近相机的表面),以确保遮挡判定的严格保守性(Conservative Testing)。
[原始深度缓冲 (Depth Pre-Pass)] │ ▼ (Max/Min 降采样构建 Mipmap 链) [Hi-Z 深度金字塔 (1/2, 1/4, 1/8, 1/16, ...)] │ ▼ (在 Compute Shader 中输入候选包围盒 AABB) ┌──────────────────────────────────────────────┐ │ 1. 视锥体相交测试 (Frustum Test) │ │ 2. 计算 AABB 屏幕空间包围矩形与对应 Mip 级别 │ │ 3. 采样 Hi-Z 金字塔深度并与 AABB 最近深度比对 │ └──────────────────────────────────────────────┘ │ ▼ (通过测试的实例写入 Culled Instance Buffer) [DrawMeshInstancedIndirect 间接绘制命令触发]移动端 Hi-Z Compute Shader 核心实现
以下是基于 HLSL 实现的 GPU 实例剔除着色器代码,支持 Reverse-Z 模式:
#pragma kernel CSMain struct InstanceData { float4x4 objectToWorld; float3 boundsCenter; float3 boundsExtents; }; StructuredBuffer<InstanceData> _InstanceBuffer; AppendStructuredBuffer<uint> _VisibleInstanceArgsBuffer; Texture2D<float> _HiZDepthTexture; SamplerState sampler_HiZDepthTexture; float4x4 _ViewProjectionMatrix; float4 _CameraPositionWS; float4 _HiZTextureSize; // (Width, Height, 1/Width, 1/Height) float _MaxDrawDistance; // 判断 AABB 是否在视锥体外部 bool IsOutsideFrustum(float3 center, float3 extents) { // 标准 6 个视锥平面的快速点积判别 // 此处省略 6 平面判别循环,返回 true 表示在视野外 return false; } [numthreads(64, 1, 1)] void CSMain(uint3 id : SV_DispatchThreadID) { uint instanceIndex = id.x; InstanceData inst = _InstanceBuffer[instanceIndex]; float3 centerWS = mul(inst.objectToWorld, float4(inst.boundsCenter, 1.0)).xyz; float distToCamera = distance(_CameraPositionWS.xyz, centerWS); // 1. 距离裁剪 if (distToCamera > _MaxDrawDistance) return; // 2. 视锥体裁剪 if (IsOutsideFrustum(centerWS, inst.boundsExtents)) return; // 3. 计算 AABB 在屏幕空间的投影像素范围 float3 minWS = centerWS - inst.boundsExtents; float3 maxWS = centerWS + inst.boundsExtents; float3 boxCorners[8] = { float3(minWS.x, minWS.y, minWS.z), float3(maxWS.x, minWS.y, minWS.z), float3(minWS.x, maxWS.y, minWS.z), float3(maxWS.x, maxWS.y, minWS.z), float3(minWS.x, minWS.y, maxWS.z), float3(maxWS.x, minWS.y, maxWS.z), float3(minWS.x, maxWS.y, maxWS.z), float3(maxWS.x, maxWS.y, maxWS.z) }; float2 minScreen = float2(1.0, 1.0); float2 maxScreen = float2(-1.0, -1.0); float nearestDepth = 0.0; // Reverse-Z: 1.0 为近平面, 0.0 为远平面 [unroll] for (int i = 0; i < 8; i++) { float4 clipPos = mul(_ViewProjectionMatrix, float4(boxCorners[i], 1.0)); float3 ndc = clipPos.xyz / clipPos.w; minScreen = min(minScreen, ndc.xy); maxScreen = max(maxScreen, ndc.xy); nearestDepth = max(nearestDepth, ndc.z); // 找到离相机最近的深度 } // 映射到 [0, 1] 纹理坐标空间 float2 minUV = saturate(minScreen * 0.5 + 0.5); float2 maxUV = saturate(maxScreen * 0.5 + 0.5); float2 uvSize = (maxUV - minUV) * _HiZTextureSize.xy; // 根据包围盒像素占位计算最佳采样 Mip 级别 float maxDim = max(uvSize.x, uvSize.y); float mipLevel = ceil(log2(max(maxDim, 1.0))); // 采样 Hi-Z 金字塔的 4 个脚点深度 float4 depthSamples; depthSamples.x = _HiZDepthTexture.SampleLevel(sampler_HiZDepthTexture, minUV, mipLevel).r; depthSamples.y = _HiZDepthTexture.SampleLevel(sampler_HiZDepthTexture, maxUV, mipLevel).r; depthSamples.z = _HiZDepthTexture.SampleLevel(sampler_HiZDepthTexture, float2(minUV.x, maxUV.y), mipLevel).r; depthSamples.w = _HiZDepthTexture.SampleLevel(sampler_HiZDepthTexture, float2(maxUV.x, minUV.y), mipLevel).r; // 在 Reverse-Z 下,如果 Hi-Z 记录的最小深度大于等于物体的最近深度,说明被完全挡住 float maxOccluderDepth = min(min(depthSamples.x, depthSamples.y), min(depthSamples.z, depthSamples.w)); if (nearestDepth >= maxOccluderDepth) { // 可见:压入间接绘制索引缓冲 _VisibleInstanceArgsBuffer.Append(instanceIndex); } }移动端(TBDR)工程落地的关键避坑
- 上一帧深度缓冲的滞后性(Temporal Reprojection):直接使用上一帧生成的 Hi-Z 进行当前帧裁剪时,快速转动视角会导致视野边缘物体出现 1 帧的“闪烁空洞”。必须使用上一帧的 ViewProjection 矩阵逆变换重新投影 AABB,或者对画面边缘区域的包围盒适当外扩(Padding)。
- TBDR 深度解析开销(Depth Resolve):在移动端 Mali / Adreno 架构上,将 On-Chip Tile Memory 中的 Depth Buffer 拷贝到系统内存(Resolve)会消耗显存带宽。建议将 Hi-Z 金字塔的生成放在不透明物体渲染之后的低分辨率拷贝 Pass 中,限制金字塔尺寸为 512x256,既能保证剔除精度,又极大节省下采样带宽。
- 两阶段剔除(Two-Phase Culling):对于动态移动的物体,第一阶段使用上一帧 Hi-Z 进行保守裁剪并绘制已知可见物体;第二阶段在完成主要遮挡物绘制后,对第一阶段被剔除的疑似可见物体进行二次测试,彻底杜绝闪烁。