手游MMORPG遮挡剔除:从零到实战优化

📅 发布时间:2026/10/21 3:05:03
手游MMORPG遮挡剔除:从零到实战优化
以Unity 移动端 MMORPG为主要场景。⚠️ 不同 GPU 架构、驱动和 Unity 版本的行为差异很大文中的数字仅为示意。是否值得做、做到哪一层以真机实测为准。先打个比方画室里被挡住的东西还是那间画室。经理派单时说画一座城画师们老老实实把城墙后面的每一栋房子、房子里的每一张桌子都画了出来最后城墙一盖全白画了。遮挡剔除就是在动笔之前先问一句“这东西会被挡住吗”问的时机不同省下的活也不同什么时候问谁来问省下了什么① 上色前逐个像素问GPU 硬件自动完成上色工人的活像素阶段② 派单前逐个物体问预计算数据、CPU 或 GPU 计算经理的工单 摆姿势工人的活 上色工人的活问得越早省得越多但问得越早判断本身也越贵、越容易出错。遮挡剔除优化的核心就是在判断成本和节省收益之间找到平衡。一、先分清三种剔除类型剔除什么成本引擎是否自动做视锥剔除镜头视野外的物体很低✅ 自动距离剔除太远、太小、看不清的物体很低需要手动设置遮挡剔除在视野内、但被别的东西挡住的物体中到高需要专门方案视野范围 ──────────────────── ╲ 被城墙挡住 → 遮挡剔除 ╲ ██ 城墙 ╲ 在视野内、未被挡 → 正常绘制 ╲ 太远 → 距离剔除 ╲ 视野外 → 视锥剔除优先级先把视锥剔除和距离剔除做到位几乎零成本再考虑遮挡剔除。二、先判断你的游戏值不值得做1. 手机 GPU 已经帮你省了一部分手机 GPU 本身有被挡住的像素不上色的能力下文第三部分详述所以对不透明物体来说像素阶段的浪费本来就有限。物体级遮挡剔除在手机上真正能省的主要是CPU剔除、排序、提交 Draw Call 的开销顶点阶段被挡住的模型依然要做顶点变换、骨骼蒙皮带宽分块渲染TBDR的 GPU 会把所有三角形的处理结果写回内存再分到各个小块三角形越多带宽越大阴影被遮挡的物体如果也不投射阴影需谨慎判断阴影阶段也能省。2. 场景类型决定收益场景遮挡关系收益主城、街道、城镇建筑密集、互相遮挡⭐⭐⭐ 高室内、地下城副本墙壁、房间分隔⭐⭐⭐ 高山谷、峡谷地形地形遮挡远处⭐⭐ 中开阔草原、海面几乎没有遮挡⭐ 很低甚至得不偿失俯视角45° 固定视角视角高很少被挡⭐ 很低算一笔账遮挡剔除本身要花时间。如果剔除花了 1.5ms只省下 1ms 的渲染时间那就是负优化。三、第一层保护好 GPU 内置的像素剔除免费的午餐这一层不需要写任何剔除代码只需要别把它破坏掉。1. 各家 GPU 的内置手段GPU技术原理简述对绘制顺序的依赖Apple、PowerVRHSR隐藏面消除一个小块内所有三角形先比完深度只给最前面的上色低MaliEarly-ZS FPK前向像素剔除提前做深度测试后来的像素如果更靠前可以杀掉还在排队的旧像素中AdrenoEarly-Z LRZ低分辨率深度先用一张粗糙的深度图快速剔除大块被挡住的区域较高从前往后画效果最好通用Early-Z上色之前先测深度被挡住就跳过高2. 哪些写法会让它们失效破坏者为什么对策Alpha Testclip / discardGPU 必须先执行 Shader 才知道这个像素要不要丢无法提前判断它会不会挡住后面的东西少用树叶用贴合形状的网格放到不透明物体之后绘制Unity 的 AlphaTest 队列默认在 2450在 Shader 中修改深度输出SV_Depth深度要算完 Shader 才知道移动端避免使用半透明物体不写深度无法遮挡别人还必须全部画控制面积和层数绘制顺序混乱后画的近处物体无法帮先画的远处物体省活不透明物体从近到远绘制Unity 默认会排序但合批、自定义队列可能打乱天空盒先画整个屏幕先被天空涂满天空盒在不透明物体之后绘制URP 默认如此中途切换渲染目标、回读深度打断分块渲染流程深度信息要写回内存减少渲染目标切换3. 深度预渲染Depth Prepass要不要开先只画一遍深度再正式上色确保每个像素只上一次色。桌面 GPU手机 GPU收益常常明显对 Apple/PowerVR/Mali 来说HSR/FPK 已经做了类似的事代价顶点多处理一遍顶点多处理一遍 额外带宽建议视情况开启默认不开只有 Alpha Test 植被特别多、像素 Shader 特别重时针对性地给这部分物体开️ 这一层就像把菜从近到远摆上桌不花钱只要别乱摆。四、第二层Unity 内置的预计算遮挡剔除Umbra1. 原理在编辑器里提前烘焙把场景切成很多格子计算站在这个格子里能看到哪些物体运行时直接查表。烘焙时编辑器 场景 ──▶ 切分空间格子 ──▶ 计算可见性 ──▶ 遮挡数据文件 运行时CPU 相机所在格子 ──▶ 查表 ──▶ 得到可见物体列表 ──▶ 只绘制这些2. 物体的两种身份身份含义适合Occluder Static遮挡者能挡住别人大型建筑、墙壁、山体、大石头Occludee Static被遮挡者能被挡住几乎所有静态物件只做被遮挡者不做遮挡者—小物件、镂空物体栅栏、树木、半透明物体动态物体角色、怪物不能参与烘焙但可以在 Renderer 上勾选Dynamic Occlusion作为被遮挡者查询。3. 关键参数参数含义调整建议Smallest Occluder小于这个尺寸的物体不作为遮挡者调大数据更小、运行更快但剔除更保守Smallest Hole小于这个尺寸的缝隙视为不透光太大会导致透过窗户、门缝本应看见的东西被错误剔除Backface Threshold用于剔除相机不可能到达的位置的数据场景封闭性好时可降低以减少数据量4. 配套工具Occlusion Area只在玩家可能到达的区域烘焙减少数据量Occlusion Portal门、闸门打开和关闭时切换遮挡状态适合副本中的可开关大门。5. 优缺点✅ 优点❌ 缺点开箱即用不用写代码只支持静态遮挡者可破坏的墙、移动的大门无法动态遮挡运行时查询相对便宜遮挡数据占内存大世界可能很大稳定、没有延迟穿帮烘焙时间长场景一改就要重新烘焙场景分块流式加载时各块数据要分别管理查询在 CPU 上进行物体数量极多时也有开销适用副本、主城等封闭场景。对开放大世界往往需要配合下面的方案。五、第三层硬件遮挡查询Occlusion Query——手机上慎用原理画一个物体的包围盒不上色问 GPU这个盒子有没有像素通过深度测试没有就说明整个物体被挡住了。为什么手机上不推荐CPU 第 N 帧 发出查询 ──────────────────────▶ 等结果…… GPU 分块渲染整帧完成后才有结果 CPU 第 N1 帧或 N2 帧 拿到结果问题说明结果延迟CPU 要等 GPU 画完才知道结果立即等待会让 CPU 和 GPU 互相卡住延迟使用会穿帮用上一两帧的结果镜头快速转动时会出现物体晚一拍才出现TBDR 不友好分块渲染要处理完整帧才能给出结果延迟更长每个查询本身就是一次绘制物体多时查询开销抵消了收益 Unity 也没有对外直接暴露通用的遮挡查询接口。手游中一般不作为主力方案。六、第四层CPU 软件光栅化遮挡剔除手游大世界的主力1. 原理在 CPU 上画一张很小的深度图只用来判断遮挡① 选出近处的大型遮挡者建筑、山体、城墙使用它们的简化代理模型 ② 在 CPU 上把代理模型光栅化到一张小深度图例如 256×128 ③ 对每个待检测物体把它的包围盒投影到屏幕上和深度图比较 ④ 包围盒覆盖区域全部比深度图更远 → 被遮挡不绘制CPU 深度缓冲低分辨率 ┌────────────────────────┐ │░░░░░░░████████░░░░░░░░░│ ████ 城墙代理模型 │░░░░░░░████████░░░░░░░░░│ │░░░░░░░██[]██░░░░░░░░░│ 的包围盒完全落在城墙后面 → 剔除 │░░░░░░░████████░░[]░░░│ 落在空白区域 → 绘制 └────────────────────────┘业界常见实现思路参考 Intel 的Masked Occlusion CullingSIMD 加速的分层深度缓冲。Unity 中可以用Job System Burst实现在多个核心上并行。2. 为什么适合手游大世界优点说明支持动态遮挡者和被遮挡者移动的大门、可破坏建筑都能参与当帧结果没有延迟穿帮不依赖 GPU 回读不需要烘焙适合分块流式加载的大世界不占 GPU手机上 GPU 往往比 CPU 更紧张、更容易发热结果还能用于其他系统例如跳过被遮挡角色的动画更新、特效更新3. 关键设计要点建议遮挡者代理模型由美术制作或工具自动生成几十到几百个三角形的简化盒体必须完全在原模型内部不能比原模型大否则会错误剔除遮挡者数量每帧只选离相机最近、屏幕占比最大的若干个如 30~100 个按屏幕面积 ÷ 距离排序深度图分辨率低分辨率即可过高会让 CPU 成本飙升被遮挡者检测用包围盒宁可保守误判为可见不要激进误判为不可见会穿帮分层组织先测试整个区块、建筑群的大包围盒被挡住就跳过其中所有物体线程放到工作线程并注意前面章节说的大小核调度跑在小核上可能来不及完成4. 成本控制剔除成本 光栅化遮挡者 测试被遮挡者 目标中端机上整体控制在 0.5~1ms工作线程示意值低端机可以降低深度图分辨率、减少遮挡者数量或者直接关闭如果低端机本来视距就很短。七、第五层GPU 驱动的 Hi-Z 遮挡剔除1. 原理利用上一帧的深度图在 GPU 上用计算着色器Compute Shader判断每个物体是否可见再用间接绘制Indirect Draw只画可见的物体。CPU 全程不需要知道结果。第 1 步构建 Hi-Z分层深度图把深度图逐级缩小每一级的一个像素保存下一级 2×2 区域中最远的深度Mip0全分辨率 Mip1 Mip2 ┌──┬──┬──┬──┐ ┌─────┬─────┐ ┌───────────┐ │ 3│ 5│ 9│ 9│ │ │ │ │ │ ├──┼──┼──┼──┤ ─▶ │ 5 │ 9 │ ─▶│ 9 │ │ 4│ 2│ 8│ 7│ │ │ │ │ (最远深度) │ └──┴──┴──┴──┘ └─────┴─────┘ └───────────┘第 2 步测试物体计算着色器每个线程处理一个物体 ① 把包围盒投影到屏幕得到屏幕矩形和最近深度 ② 根据矩形大小选择合适的 Mip 级别让矩形只覆盖 2×2 个像素 ③ 取出这几个像素的最远深度 ④ 物体最近点 比 遮挡物最远点 还远 → 被完全遮挡 ⑤ 可见的物体写入可见列表并累加间接绘制参数第 3 步间接绘制DrawMeshInstancedIndirect / Graphics.RenderMeshIndirect → GPU 根据可见列表决定画多少实例CPU 不参与2. 时间差问题上一帧的深度对不上这一帧用上一帧深度判断这一帧镜头移动后会出现刚从墙后转出来的物体晚一帧才出现。解决方式说明两阶段剔除① 用上一帧深度剔除并画出可见物体 → ② 用这一帧刚画出的深度构建新 Hi-Z → ③ 对第一阶段被剔除的物体再测一次补画误判的深度重投影把上一帧深度按相机运动变换到当前视角会有空洞保守放大包围盒简单但效果有限镜头切换时禁用传送、过场动画切镜头时当帧不剔除3. 手机上的现实问题问题说明读回深度要付带宽手机 GPU 通常把深度留在片上画完直接丢弃不写回内存。做 Hi-Z 就必须把深度写回内存以 2400×1080 分辨率、4 字节深度计算每帧约 10MB 的额外写入再加上构建 Mip 的读写计算着色器支持需要 OpenGL ES 3.1 / Vulkan / Metal老旧机型和部分驱动存在兼容问题间接绘制的驱动问题部分老旧 Adreno、Mali 驱动的间接绘制有性能或正确性问题合批体系要改造需要实例化渲染、统一的物体数据缓冲和常规 GameObject 渲染流程差异大发热GPU 本来就是手游最紧张的资源再加计算任务可能得不偿失4. Unity 6 的内置方案Unity 6 在 URP 中提供了GPU Resident Drawer GPU Occlusion Culling可以在不手写 Compute Shader 的情况下使用 GPU 遮挡剔除。它有前提条件例如需要 Forward 渲染路径、支持计算着色器的平台、物体使用兼容的渲染方式等并且在移动端的收益需要逐机型实测。具体要求以你使用的 Unity 版本文档为准。适用高端机档位、物体数量极大如大量植被、密集城市的场景。中低端机建议保留 CPU 方案。八、方案对比与选择方案剔除粒度动态遮挡者成本在哪延迟穿帮手游推荐度GPU 内置像素剔除像素✅免费无⭐⭐⭐ 必须保护Unity 预计算Umbra物体❌CPU 查询 内存无⭐⭐⭐ 封闭场景硬件遮挡查询物体✅GPU 同步等待有⭐ 不推荐CPU 软件光栅化物体✅CPU 工作线程无⭐⭐⭐ 大世界主力GPU Hi-Z物体/实例✅GPU 带宽有可修正⭐⭐ 高端机一个常见的组合┌───────────── 所有档位 ─────────────┐ │ 视锥剔除 距离剔除 保护像素剔除 │ └──────────────────┬────────────────┘ │ ┌──────────────────────────┼──────────────────────────┐ ▼ ▼ ▼ 副本 / 室内 主城 / 大世界 高端机 海量实例 Umbra 预计算剔除 CPU 软件光栅化剔除 GPU Hi-Z 间接绘制 可叠加 Umbra 处理静态部分 植被、密集物件九、MMO 特有场景场景建议主城挤满玩家角色作为被遮挡者参与剔除被建筑挡住的玩家不仅不画还可以跳过动画更新、特效、头顶名字和血条第三人称镜头快速旋转优先选择没有延迟的方案预计算、CPU 光栅化GPU 方案要做两阶段剔除镜头可拉远到高空视角越高遮挡越少剔除收益下降可以在高空视角时关闭遮挡剔除改用更激进的距离剔除和 LOD地形遮挡山脉是天然遮挡者可以生成地形的低精度代理网格参与 CPU 光栅化“地平线剔除”分块流式加载遮挡数据和代理模型跟随区块加载和卸载传送、过场动画镜头瞬移时当帧强制全部可见或重置剔除历史阴影被相机视角遮挡的物体仍可能投射可见的影子例如墙后的高塔把影子投到墙前的地面上。所以不能直接用相机的遮挡结果来剔除阴影投射者。稳妥的做法是阴影投射者按光源视角单独做视锥剔除相机遮挡结果只用于不投射阴影或阴影距离之外的物体角色可以用被遮挡 离可见区域足够远作为跳过阴影的条件十、美术与关卡配合遮挡剔除的效果一半靠内容剔除算法再好如果场景是一眼望到底也没有东西可剔。收益大小首先由关卡和资源决定。1. 关卡设计主动制造遮挡做法说明视线阻断街道设计成转弯、错位不要一条直线大道通到地图尽头天然隔断城门、山口、峡谷、隧道把区域分开过渡处正好可以做流式加载地形起伏小山丘、高低差挡住远处的建筑群室内外分离室内物件单独分组只在玩家靠近门窗或进入室内时才可能可见️ 很多经典主城的曲折小巷既是美术风格也是性能设计。2. 模型拆分粒度不能太大也不能太碎❌ 太大整条街合成一个网格 → 只要露出一个角整条街都要画剔除完全失效 ❌ 太碎每块砖、每个花盆都是独立物体 → 剔除测试次数暴涨CPU 开销超过收益 ✅ 适中以一栋建筑一个房间一组摆件为单位 → 剔除有效测试次数可控这里和第六章合批存在矛盾静态合批会把物体合成大网格削弱剔除效果。常见做法是按空间区块合批只合并相邻、通常同时可见的物体而不是全场景一把合并。3. 遮挡代理模型的制作规范规范说明必须在原模型内部代理模型比原模型大 → 会把原本可见的东西错误剔除造成物体闪烁消失面数极低一般几十到一两百个三角形用盒子、柱体拼出主体轮廓忽略镂空和细节窗户、栏杆、屋檐的镂空部分直接不做按不遮挡处理命名约定例如Building_A_OCC导入时由工具自动识别、隐藏、注册为遮挡者自动生成 人工检查工具可以按体素化或凸包自动生成再由美术检查是否超出原模型4. 包围盒要准确剔除依赖包围盒。包围盒虚大物体就永远剔不掉。常见问题对策骨骼动画角色包围盒过大例如包含了武器挥舞的最大范围使用合理大小的包围盒Skinned Mesh Renderer 不要随意开启Update When Offscreen粒子系统包围盒自动计算得非常大手动设置固定包围盒Custom Bounds美术导出时模型带了远离原点的游离顶点资源检查工具中检测包围盒与实际体积比例异常的模型5. 不适合做遮挡者的物体镂空的栅栏、树木、铁丝网、脚手架半透明的玻璃、水面、帷幕太薄或太小的路牌、灯柱、小石头会消失的可破坏物除非遮挡系统支持动态更新。十一、调试与验证确认剔对了、剔得值1. 可视化工具工具用途UnityOcclusion Culling 窗口 → Visualization在 Scene 视图中查看 Umbra 当前剔除结果冻结剔除Freeze Culling自己实现一个调试开关冻结当前剔除结果然后把相机飞到旁边看哪些被剔除一目了然深度图显示CPU 光栅化方案把低分辨率深度图画到屏幕角落检查遮挡者是否正确包围盒着色可见物体画绿框被剔除的画红框被视锥剔除的不画Frame Debugger / RenderDoc对比开关剔除前后的 Draw Call 列表2. 核心指标指标含义关注点剔除率被遮挡剔除的物体数 ÷ 视锥内物体数主城可能达到 40%70%开阔地带接近 0说明该区域可以关掉剔除耗时遮挡剔除本身在 CPU/GPU 上花的时间必须明显小于节省下来的时间节省量减少的 Draw Call、三角面、蒙皮角色数结合第二章的瓶颈判断看省下的是不是瓶颈所在最终帧时间开关对比唯一真正的判断标准3. CPU 方案的被遮挡者测试示意下面是一个简化的 Burst Job演示包围盒矩形 vs 低分辨率深度图的测试逻辑仅示意实际实现通常会用分层深度和 SIMD 进一步加速usingUnity.Burst;usingUnity.Collections;usingUnity.Jobs;publicstructScreenRect{publicintminX,minY,maxX,maxY;// 包围盒投影到深度图上的像素范围publicfloatminDepth;// 包围盒离相机最近点的线性深度publicboolcrossesNearPlane;// 包围盒是否跨过近裁剪面}[BurstCompile]publicstructOccludeeTestJob:IJobParallelFor{// 每个像素存遮挡者的最近深度线性深度越大越远没有遮挡者的像素为 ∞[ReadOnly]publicNativeArrayfloatdepthBuffer;[ReadOnly]publicNativeArrayScreenRectrects;publicintwidth;[WriteOnly]publicNativeArrayboolvisible;publicvoidExecute(inti){ScreenRectrrects[i];// 包围盒跨过近裁剪面时投影不可靠保守地视为可见if(r.crossesNearPlane){visible[i]true;return;}for(intyr.minY;yr.maxY;y){for(intxr.minX;xr.maxX;x){// 只要有一个像素上物体比遮挡者更近 → 可能可见if(r.minDepthdepthBuffer[y*widthx]){visible[i]true;return;}}}// 覆盖的所有像素都被更近的遮挡者挡住 → 剔除visible[i]false;}} 注意两个保守原则跨近裁剪面的物体直接视为可见只要有一个像素可能看得见就算可见。剔除宁可漏剔不可错剔。4. 必测的穿帮场景场景容易出现的问题快速旋转镜头GPU 方案延迟一帧物体晚一拍出现站在门口、窗边Smallest Hole 设置过大透过门窗本该看到的东西消失贴着薄墙、站在墙角代理模型过大或包围盒跨近平面处理不当从高处跳下、坐骑飞行视角高度剧烈变化预计算数据未覆盖该区域传送、切换场景、过场动画剔除历史没有重置区块流式加载边界新加载区块的遮挡数据尚未就绪建议把这些位置做成固定的相机回放路线加入第十三章所说的性能场景回归每次构建自动截图对比。十二、被剔除之后让其他系统也偷懒物体不画了但如果它的动画、脚本、特效还在跑只省了一半。遮挡结果应该广播给其他系统系统被遮挡时的处理AnimatorCulling Mode 设为Cull Update Transforms或Cull Completely注意根运动角色的位置同步粒子系统设置 Culling Mode 为Pause或Automatic并配合准确的包围盒头顶名字、血条被遮挡的玩家不显示或降低更新频率布料、头发物理暂停模拟音效被遮挡时做低通滤波还能顺便增加真实感自定义 MonoBehaviour用OnBecameVisible/OnBecameInvisible或自己的剔除回调降频⚠️OnBecameVisible/OnBecameInvisible也会被 Scene 视图相机和阴影相机影响在编辑器里测试时容易误判以真机结果为准。十三、常见误区误区真相“开了遮挡剔除就一定更快”开阔场景、俯视角游戏里剔除率很低剔除本身的开销可能让帧率下降“遮挡者越多越精确越好”每多一个遮挡者光栅化成本就上升只要近处的大块遮挡者“代理模型大一点更保险”恰恰相反代理模型比原模型大会导致错误剔除必须在原模型内部“手机 GPU 有 HSR不需要物体级剔除”HSR 只省像素阶段CPU 提交、顶点、蒙皮、TBDR 的几何带宽都省不了“被剔除的物体就不消耗性能了”动画、粒子、脚本、物理可能还在跑需要联动处理“相机看不见的物体就可以不投射阴影”墙后的物体影子可能投到墙前阴影要单独判断“GPU 剔除比 CPU 剔除先进全机型都上”手机上深度回读要付带宽计算着色器和间接绘制在中低端机上兼容性和收益都不稳定“编辑器里看起来没问题”编辑器的 CPU、GPU 和手机完全不同穿帮问题也要在真机上快速转镜头去找十四、落地路线第 1 步 测量用第二章的方法确认瓶颈在 CPU 提交 / 顶点 / 带宽而不是像素上色 │ 第 2 步 零成本手段视锥剔除 按物件大小的距离剔除 LOD │ 第 3 步 保护像素剔除控制 Alpha Test、半透明、绘制顺序移动端默认不开深度预渲染 │ 第 4 步 封闭场景副本、室内、主城静态部分Unity 预计算遮挡剔除 │ 第 5 步 大世界和动态遮挡CPU 软件光栅化剔除Job System Burst │ 第 6 步 高端机 海量实例评估 GPU Hi-Z / Unity 6 GPU Occlusion Culling │ 第 7 步 联动把剔除结果用于动画、特效、UI、物理的降频 │ 第 8 步 验证与回归可视化、剔除率与耗时统计、穿帮路线回放、热机复测按档位配置档位建议配置低端视锥 激进距离剔除 短视距雾效 预计算剔除CPU 光栅化可降分辨率或关闭中端预计算剔除 CPU 光栅化剔除高端以上全部 可选 GPU 剔除逐机型验证后开启一句话总结 遮挡剔除就是在画室里动笔前先问一句这东西会被挡住吗。手机 GPU 已经免费帮你在上色前问了一遍HSR / FPK / LRZ你要做的第一件事是别破坏它想在派单前就省下工单和顶点就要在预计算、CPU 光栅化、GPU Hi-Z 之间按场景和机型选择。⚖️ 判断永远只有一个标准剔除本身花的时间必须远小于它省下的时间并且宁可漏剔不可错剔。