ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

UE4半透明排序错乱?Instanced Mesh的根源与解决方案

UE4半透明排序错乱?Instanced Mesh的根源与解决方案 这问题我太熟了。项目里做了一大片参数化生成的灌木丛叶片用的是半透明贴图卡片效果单看一张没问题摆进场景一俯视远处叶子直接穿到近处叶子前面半透明叠加区域跟马蜂窝似的。一开始我也以为是材质问题改混合模式、开关双面、调 Sort Priority折腾一晚上毫无起色。后来才想明白这是 Instanced Mesh 的半透明排序机制在作怪。这篇就把我从“排序错乱”到“彻底解决”的完整过程写出来。不只是给答案会把引擎这层排序机制掰开讲清楚再给出几种可行解法以及它们的适用边界。搞植被、搞批量装饰物、搞能量屏障这类半透明实例化物件的朋友这篇应该能帮你省下好几个晚上的排查时间。1. 先说结论Instanced Mesh 的半透明排序问题根源不在材质而在代理层1.1 引擎的半透明排序到底怎么排的要理解这个坑得先搞清楚 UE4 半透明物体的绘制流程。整个半透明渲染Translucent Pass走的是前向渲染路径本质上就是经典的画家算法先把场景里所有半透明 Primitive 收集起来按从远到近的顺序排好挨个画上去。在代码层面引擎在FPrimitiveSceneProxy生成FPrimitiveSceneInfo时会为每个 Primitive 记录一个 Bounds包围盒和一个 Sort Priority。渲染线程会拿相机位置去和 Bounds 中心点算距离再结合 Sort Priority 给半透明物体排序。这里的关键是排序的单位是 Primitive不是单个 Mesh 实例更不是单个三角形。普通的一个 Static Mesh Actor 摆在场景里它的 Bounds 就是自身网格的包围盒算出来的距离就是物体本身到相机的大致距离排序自然准。但如果这个“Primitive”是一大片 Instanced Mesh 呢1.2 HISM 和普通 Instance 的区别一个 Bounds 引发的血案Instanced Static MeshISM和 Hierarchical Instanced Static MeshHISM在场景里虽然挂着几十上百个实例但在渲染代理层面它们永远是一个FPrimitiveSceneProxy、一个Primitive 项。对应的它们也只有一个Bounds——这个 Bounds 是包住所有实例的巨型包围盒。问题就在这儿。假设我在 100 米见方的区域里种了 400 棵灌木每棵灌木的 HISM 组件如果跨越几十米那它的 Bounds 中心可能离哪棵树都不近、也不远。渲染线程算距离的时候拿着这个“假中心”去算整片灌木林被当成一个整体排在队伍里。里面的每一片半透明叶子呢压根没有单独参与排序的机会。于是画面就变成近处的叶子因为整体 Bounds 被排到了“较远”的位置远处的叶子反而先画了混合结果全乱。还有一个更隐蔽的问题即便整个 HISM 和场景里其他半透明物体的相对顺序排对了HISM 内部的一次 Draw Call 里几十上百个实例也是按空间组织结构的先后顺序一股脑画出来的互相之间的前后遮挡关系完全没人管。这才是 Instanced Mesh 半透明排序乱的完整解释。如果你搜过这个问题会看到不少人建议调TranslucencySortPriority。这个参数确实存在但它的作用是调整一个 Primitive 相对其他 Primitive 的排序位置对 HISM 内部的实例排序毫无办法。这一点后面专门讲。2. 定位过程复盘我是怎么一步步确认是 HISM 排序机制的锅如果你遇到的是类似症状先别急着改材质按下面这套链路走一遍能快速锁定问题真凶。很多“排序错误”实际上根本是别的原因。2.1 排查前要先排除掉的干扰项第一件事确认材质本身没问题。把 HISM 里的 Mesh 单独拖一个 Static Mesh Actor 放到场景里用同一个材质看渲染是否正常。如果单实例正常、多个普通 Actor 放在一起也正常那材质混合模式、双面设置、Depth 写入这些基础项基本可以排除。顺带说两个常见干扰项半透明材质的Two Sided开关如果打开了同一张面片的正面和背面会在两个 Draw Call 里分别绘制正反面绘制的顺序还固定是先背面再正面或反之容易造成一种“半透明内外面交叉乱序”的假象。单看一个物体可能不明显实例一多就乱上加乱。渲染特性里如果开了Screen Space Reflections半透明物体会拿场景深度去算反射排序一乱反射也会跟着闪烁错乱容易被误判成材质问题。2.2 复现步骤与观察方法确认基础项没问题之后用下面的步骤复现锁因在场景里放置一个 HISM 组件手动添加 3 个实例让它们前后交叉排列。相机从侧面观察交叉区域截图记录排序错误。把 HISM 换成 3 个普通的 Static Mesh Actor相同 mesh、相同材质观察同一位置。对比两者差异。正常的半透明单面片从侧面看近处物体会正确遮挡远处物体。HISM 的交叉区域则是随机错乱而且旋转相机视角时错乱模式会剧烈变化因为排序距离是拿 Bounds 中心算的视角一变距离就变。你也可以打开Stat GPU或RenderDoc抓帧看 Draw Call 结构。半透明 Pass 里 HISM 的多个实例往往在同一个 Draw Call 里批次没问题但实例顺序固定而普通 Actor 是每个 Actor 单独一个 Draw Call且 Draw Call 顺序严格的从远到近。看到这个差异基本就实锤了。表现现象可能原因是否与 HISM 排序相关单实例正常HISM 多实例交叉错乱HISM 实例内部无排序是单个物体自身上下两面混叠Two Sided 正反面绘制顺序否所有物体整体被错误遮挡Bounds 中心计算距离偏差是且优先级最高半透明物体和半透明物体交界闪烁排序策略不稳定部分相关粒子与 HISM 半透明交叉错乱粒子系统和 HISM 都只按 Primitive 排序是3. 解法一推荐把半透明物件从实例化容器中拆出去3.1 拆分思路与实现最干净、最彻底的方案就是不要在半透明材质上使用 Instanced Mesh。这不是我拍脑袋说的是引擎渲染机制决定的。HISM 的定位本来就是给高频复用的不透明物件做批量优化它为了性能牺牲掉了单实例排序能力而半透明材质恰恰是排序敏感的东西两者本质冲突。具体做法根据你的项目形态来如果这类半透明物件数量不大几十到几百个直接删掉 HISM 组件在需要的位置 Spawn 普通Static Mesh Actor或者用蓝图在启动时循环生成。如果还需要保留实例化管理的便利性比如一些逻辑需要统一控制实例的动画、显隐可以把“半透明材质的面片”单独拆成一个 Static Mesh Actor位置和旋转同步到对应 HISM 实例上HISM 本体只保留不透明的支撑结构。拆的时候要注意 Mesh 的坐标轴和缩放是否一致。HISM 的实例 Transform 可以拿到手直接传给 ActorUHierarchicalInstancedStaticMeshComponent* HISM ...; int32 InstanceCount HISM-GetInstanceCount(); for (int32 i 0; i InstanceCount; i) { FTransform InstanceTransform; HISM-GetInstanceTransform(i, InstanceTransform, true); AStaticMeshActor* SMActor World-SpawnActorAStaticMeshActor( SMActorClass, InstanceTransform); SMActor-GetStaticMeshComponent()-SetStaticMesh(LeafCardMesh); }这是半伪代码实际用 C 还是蓝图 Trigger 看你习惯。核心就是把排序敏感的半透明面片从 HISM 里“摘”出来让每个面片拥有独立的 Bounds 参与排序。3.2 同步实例位置与生命周期拆出来之后如果 HISM 的实例还会动态变化比如运行时增删、移动你需要在修改 HISM 实例的同时同步更新拆分出去的 Actor 的 Transform。工程上通常维护一张实例索引 - Actor的映射表HISM 实例发生变化时统一刷一遍。这一步的维护成本不算低但换来的是渲染顺序绝对正确。如果你的项目里这类半透明实例数量不会频繁变化我更推荐这个方案因为它把渲染正确性彻底锁死了后续不会再冒出新坑。3.3 性能账本值得吗拆分的代价显而易见少了 HISM 的实例化合并Draw Call 会增多。但你可以算一笔账一个 Draw Call 的成本大约在几微秒到几十微秒级别如果你的半透明面片只有几十上百个多出来的 Draw Call 对帧耗时的影响通常不到 1 毫秒远小于一次复杂后处理特效的开销。为了这个可控的小代价换取正确的混合顺序绝对划算。反过来讲如果你的半透明实例数量是几千上万个——比如整片森林的树叶都用半透明——那拆分就不现实了这时候需要的是下一个方案。4. 解法二常用用 Masked Dithered Opacity 骗过排序问题4.1 原理用不透明通道模拟透明这是被植被渲染广泛验证过的方案也是大多数商业项目处理树叶、草叶的方式材质混合模式不设 Translucent改用 Masked并勾选 Dithered Opacity。混合模式为 Masked 的材质压根不会进入半透明排序队列。引擎在 Pixel Shader 阶段直接丢弃亮度低于阈值的像素GPU 硬件测试和深度缓冲按不透明物体来走。深度测试是逐像素的不涉及“谁先画谁后画”所以排序问题凭空消失了——因为不存在多物体混合顺序的概念了。Dithered Opacity抖动不透明度是在 Masked 的基础上用一个屏幕空间噪声图案去决定像素的取舍让原本硬邦邦的裁剪边缘变成点状小洞。远看就像半透明近看是密集噪点但交给 TAA 时间抗锯齿收敛之后视觉上非常接近真实的半透明。4.2 参数配置和常见细节在实际项目中我会这样配置材质混合模式Masked勾选Dithered OpacityOpacity Mask Clip Value设到 0.3~0.5 之间具体看贴图灰度分布阴影设置Shadow Pass里的 Mask 阈值保持一致否则阴影会变得特别硬或缺失需要强调Dithered Opacity 并不是没有代价必须有 TAA没有 TAA 或关掉抗锯齿噪点会非常明显甚至闪烁。低分辨率移动端慎用屏幕空间噪声图案在低分辨率下会糊成一团效果很差。移动端我建议直接上普通的 Masked用更硬朗的裁剪边缘换稳定性。无法表现折射如果半透明材质里用了折射节点Dithered Opacity 直接放弃这部分效果。阴影会有颗粒感因为噪声是每一帧重新采样的时间上会有轻微变化阴影边缘可能看到蠕动的噪点。这套方案特别适合植被叶片、头发卡片的远距离表现不适合玻璃、水、能量护盾这类需要真实混合的材质。如果你的半透明东西是“小面积高密度透光”的形态比如铁丝网、网格栅栏、粒子的光斑Dithered Opacity 基本是完美替代。如果是大面积的实心半透明比如一块磨砂玻璃挡板那还是得靠拆分方案。5. 解法三缓解排序策略、Sort Priority 与它们的边界5.1 半透明排序策略与优先级参数UE4 的 Project Settings - Rendering 里有一个Translucent Sort Policy默认是Sort by Distance。它计算的是 Primitive Bounds 中心点到相机位置的距离。除此之外还有Sort by View Space Depth之类的选项本质都是换一种距离度量方式但作用范围依然是 Primitive 级别。每个 Primitive 的TranslucencySortPriority参数官方定义是越小越先绘制即越靠后。在蓝图里Static Mesh Component 的 Details 面板就有这个值。通过把它设成一个很大的负数可以让某些物体批次被强制排到所有半透明物体之后绘制。5.2 为什么这些在 HISM 里形同虚设在普通 Actor 上Sort Priority 确实能有效调整半透明物体的前后层叠关系。但回到 HISM整个组件只有一个FPrimitiveSceneInfo、一个TranslucencySortPriority。你把它设成任何值改变的都是“这整片 HISM 相对其他半透明物体”的位置HISM 内部那几百个实例依然是按顺序一股脑画完。也就是说如果你场景里有两个 HISM 半透明系统比如一片灌木HISM A和一排窗户HISM BSort Priority 可以调整“灌木整体排在窗户前面还是后面”但灌木 A 内部的叶子之间照样乱。5.3 什么场景下这个方案有效单体多面片组合比如一个树冠由十几张卡片组成但整棵树就是一个 Actor通过 Sort Priority 让树冠永远排在玩家角色后面可以消除遮挡人物时的穿帮。大面积半透明远景比如远处的雾面罩板调整优先级让远处罩板整体排到后方可以避免和不透明物体穿插闪烁。HISM 内部排序问题已通过别的手段解决Sort Priority 只用来调整系统间顺序。把这个方案当作“浅水位补丁”比较合适。它治标不治本但某些时候能让你省掉一次大重构。6. 解法四折中普通 Actor Use Instancing 的替代方案6.1 GPU Instancing 的另一个入口很多人不知道UE4 的 Static Mesh Actor 在满足一定条件时会自动使用 GPU Instancing。条件包括多个 Actor 使用同一个 Static Mesh使用同一个材质包括同一种材质实例Actor 的 Static Mesh Component 勾选了Use Instancing这种情况下渲染线程会把这些 Actor 的渲染批次合并成一个实例化调用Draw Call 数量大幅下降但每个 Actor 依然保留着独立的FPrimitiveSceneInfo和独立的 Bounds。翻译成人话就是批次性能你照样拿排序正确性也保住了。我做了个简单对比测试场景里摆 500 棵相同的灌木每种方式的半透明材质的 Draw Call 情况大致如下方案Draw Call半透明 Pass排序正确性备注HISM Translucent1~2错乱Bounds 整体参与排序Actor Use Instancing Translucent约 30~60正确逐 Actor渲染线程自动合并批次拆分 Actor无 Instancing500正确极端情况Draw Call 最高HISM Masked Dithered1~2无排序问题但需要 TAA 配合这里要说明自动实例化的批次并不会像 HISM 那样按层级裁剪也就是说不管相机角度怎么样所有实例都参与裁剪判断视锥剔除的效率略低但大部分情况下完全够用。6.2 批量转换技巧把已有 HISM 批量转成 Actor Instancing工程上可以写个编辑器脚本遍历 HISM 的所有实例。取得每个实例的 World Transform。用同样的 Transform Spawn 一个 Static Mesh Actor。设置好 Mesh、材质、碰撞、光照通道。勾选新 Actor 的 StaticMeshComponent 的Use Instancing。删除原 HISM。如果你担心生成的 Actor 过多导致场景臃肿可以只对“需要正确排序的半透明实例”做转换不透明部分继续留在 HISM 里。这样往往是最优解大量不透明瓦片照旧实例化少量半透明卡片变成独立 Actor性能和正确性两头都占。7. 不同场景下的方案选型参考没有万能解每个项目情况不一样。这里给一个选型矩阵可以直接对着抄作业。场景实例数量推荐方案茂密植被半透明树叶非移动端数千到数万Masked Dithered Opacity保留 HISM植被中低端设备数千Masked不开启 Dithered保留 HISM能量护盾 / 光墙 / 玻璃围栏数十到数百拆分半透明面片为独立 Actor大量半透明宝石 / 收集品数百到数千Actor Use Instancing Translucent混合场景大量不透明结构 少量半透明点缀不等不透明部分 HISM半透明部分拆分或 Actor Instancing半透明物体之间还需要正确遮挡关系如玻璃碎片互相遮挡数百以内独立 Actor 逐实例排序再补充一个实用判定标准如果你发现自己开始调 HISM 实例里的单个实例位置来缓解排序穿帮那就说明该上拆分方案了。绑定单个实例的位置去骗排序是一时之策早晚会随着相机角度变化穿帮还不如一开始就拆干净。7.1 项目里的实际取舍我这边的最终落地是这样的灌木的骨架和枝条留在 HISM不透明性能吃满叶子卡片改成了独立 Actor Use Instancing半透明排序正确同时在材质里做了一层深度淡化让叶片和枝条交接的地方不至于出现硬边。整体 Draw Call 增加大概 40 个但 1080p 下帧耗时几乎没变化。这个结果我认为是当前项目架构下最平衡的。调试半透明排序问题最大的障碍其实是心态一旦怀疑是材质问题你会在混合模式、深度写入、双面开关这些参数里反复打转。但本质问题在代理层——Instanced Mesh 作为一种 Primitive只能整体参与排序这个约束是写死在引擎设计里的。理解了这一层后续选方案就轻松了要么绕过排序Dithered Opacity要么绕过实例化拆分 / Actor Instancing要么绕过 Primitive 的限制暂时没有完全两全的办法。根据你的性能和设备目标选一条路走下去就行。
返回列表