UE5 移动端 HZB Occlusion Culling
概述
在 UE5 移动端渲染管线中启用 HZB(Hierarchical Z-Buffer)遮挡剔除,替代传统的 OcclusionFeedback / GPU Query 方案。
- 适用平台: Android (Vulkan / GLES ES 3.1)、iOS (Metal)
- 预期收益: 遮挡率高场景 GPU 耗时降低 10-25%,Draw Call 减少 15-30%
- 延迟: 1 帧(与桌面端一致)
- 改动范围: 9 个文件,~500 行净增
如何开启
编辑器预览
- 编译编辑器后启动
- 视口工具栏 → Settings → Preview Rendering Level → 选择 Android Vulkan 或 Android OpenGL
- 控制台输入:
1 | r.HZBOcclusion 1 # 开启 HZB Occlusion |
真机部署
已通过配置文件默认开启:
| 文件 | 配置 |
|---|---|
Engine/Config/Android/AndroidEngine.ini |
r.HZBOcclusion=1 |
Engine/Config/IOS/BaseIOSEngine.ini |
r.HZBOcclusion=1 |
Engine/Config/BaseDeviceProfiles.ini |
Android 基类: r.HZB.BuildUseCompute=0Android_High: r.HZB.BuildUseCompute=1(覆盖为 compute shader)Android_Preview_Vulkan_SM5: r.HZB.BuildUseCompute=1 |
注意:
AndroidEngine.ini在编辑器预览模式下不会加载。编辑器预览通过 C++ 默认值 (GHZBOcclusion=1) 生效。
改动清单
核心修改
| # | 文件 | 改动 |
|---|---|---|
| 1 | SceneVisibility.cpp |
GHZBOcclusion 默认值改为 1 |
| 2 | SceneVisibility.cpp |
!IsOpenGLPlatform() || IsMobilePlatform() 放行移动端 GLES |
| 3 | SceneVisibility.cpp |
bUseOcclusionFeedback 增加 && !bHZBOcclusion,HZB 优先 |
| 4 | MobileShadingRenderer.cpp |
ShouldRenderHZB() 增加 r.HZBOcclusion 检查 |
| 5 | MobileShadingRenderer.cpp |
新增 SubmitHZBOcclusionTests(),在 4 处 RenderHZB() 后调用 |
| 6 | MobileShadingRenderer.cpp |
6 处 GPU Query 路径增加 !IsHZBOcclusionEnabled() 互斥 |
| 7 | SceneOcclusion.cpp |
FHZBTestPS::ShouldCompilePermutation 放宽到 IsMobilePlatform |
| 8 | SceneOcclusion.cpp |
InitRHI / ReleaseRHI 阈值从 SM5 → ES3_1 |
| 9 | SceneOcclusion.cpp |
Bounds 纹理去掉 TexCreate_RenderTargetable(ES3.1 兼容) |
| 10 | SceneRendering.h |
SubmitHZBOcclusionTests 函数声明 + 类成员 |
| 11 | SceneTextureReductions.cpp |
HZBBuildUseCompute() 增加 FeatureLevel 自动检测 |
非阻塞 Readback 优化
| # | 文件 | 改动 |
|---|---|---|
| 12 | SceneRendering.h |
FHZBOcclusionTester 新增 4 槽环形缓存 + bLockCalled |
| 13 | SceneOcclusion.cpp |
MapResults() 改为 IsReady() 预检 + 环形缓存回退 |
| 14 | SceneOcclusion.cpp |
UnmapResults() 增加 bLockCalled 保护 |
| 15 | MetalRenderTarget.cpp |
Metal RHI RHIMapStagingSurface 非阻塞优化 |
架构设计
完整帧流程
1 | Frame N: |
关键 CVars
| CVar | 默认 | 说明 |
|---|---|---|
r.HZBOcclusion |
1 (原 0) | 0=GPU Query, 1=HZB, 2=强制 HZB |
r.HZB.BuildUseCompute |
1 | 0=pixel shader, 1=compute shader |
r.OcclusionFeedback.Enable |
1 | 移动端 OcclusionFeedback 系统 |
HZB 构建模式
| 预览模式 | FeatureLevel | HZB 构建 |
|---|---|---|
| Android OpenGL/Vulkan Low/Mid | ES3.1 | pixel shader (DownsampleHZB, 8 pass) |
| Android Vulkan High | ES3.1 | pixel shader (继承基类) |
| Android VK SM5 | SM5 | compute shader (ReduceHZB, 2 pass) |
| iOS | Metal | compute shader |
非阻塞 Readback 设计
问题
FHZBOcclusionTester::MapResults() 调用 FRHIGPUTextureReadback::Lock() 时,如果 GPU 未完成写入,会阻塞 RenderThread 等待。
方案
利用 FRHIGPUMemoryReadback::IsReady() 进行非阻塞轮询:
1 | // IsReady() 底层实现: |
Lock() 行为(IsReady()=true 时调用):
| RHI | Lock() 是否阻塞 |
|---|---|
| OpenGL ES | ImmediateFlush(RHI 命令刷新,不等 GPU) |
| Vulkan | GetMappedPointer() 立即返回 |
| Metal | 改前:SubmitAndBlockUntilGPUIdle() 无条件阻塞;改后:Fence 已 signal 时跳过,直接 Lock Surface |
| D3D12 | Fence::Wait + Map,Fence 已 signal 时 Wait 不阻塞 |
Metal 改动(MetalRenderTarget.cpp):
1 | // 改前: 无条件阻塞 |
- GPU 就绪 → Lock() 读回(非阻塞) → 写入环形缓存 → 1 帧延迟
- GPU 未就绪 → 从环形缓存取最近有效结果 → 2~4 帧延迟但不阻塞
环形缓存
1 | static constexpr int32 MaxCachedResults = 4; |
IsValidFrame(PrimitiveOcclusionHistory->LastTestFrameNumber) 确保只有被缓存帧测试过的 primitive 才会用缓存结果,防止 index 映射错误。
验证方法
编辑器内
1 | # 1. 确认 HZB 已启用 |
RenderDoc 抓帧
在 Event Browser 搜索关键字:
| Pass 名称 | 含义 |
|---|---|
HZB |
HZB 构建 group |
BuildHZB(ViewId=0) |
HZB 金字塔逐 mip 构建 |
DownsampleHZB(mip=N) |
pixel shader mip 降采样 |
ReduceHZB(mips=[N;M]) |
compute shader 批处理 mip |
SubmitHZB |
HZB 遮挡测试提交 |
UpdateTextures |
bounds 数据 CPU→GPU |
TestHZB |
FHZBTestPS 全屏测试 pass |
真机
- 打包到 Android/iOS 设备
- RenderDoc 无线抓帧
- ARM Streamline / Snapdragon Profiler 分析 GPU 耗时
stat unit+stat gpu对比
平台兼容性
| 平台 | ShaderPlatform | HZB Culling | HZB 构建 |
|---|---|---|---|
| Android GLES ES 3.1 | OPENGL_ES3_1_ANDROID |
✅ | pixel shader |
| Android Vulkan ES 3.1 | VULKAN_ES3_1_ANDROID |
✅ | pixel shader |
| Android Vulkan SM5 | VULKAN_SM5_ANDROID |
✅ | compute shader |
| iOS Metal ES 3.1 | METAL_ES3_1_IOS |
✅ | pixel shader |
| iOS Metal SM5+ | METAL_SM5_IOS / METAL_SM6_IOS |
✅ | compute shader |
| Desktop OpenGL | — | ❌ 保持禁用 | — |
已知问题 & 注意事项
- 首帧无遮挡: 环形缓存为空时回退到
FirstFrameBuffer(全可见),首帧不剔除。 - OpenGL ES readback:
ImmediateFlush+glMapBufferRange在 GLES 上有固有开销,通过IsReady()预检 + 环形缓存缓解。 - Index 映射: 每帧 primitive→index 映射会变,通过
IsValidFrame(FrameNumber)确保只有匹配帧的 primitive 使用缓存。 - Vulkan ES 3.1: 编辑器预览中
PF_A32B32G32R32F格式通过去掉TexCreate_RenderTargetable兼容。