UE5.8 Nanite 代理、碰撞与位移:别让视觉盛宴变成物理噩梦
引子:老周的悬崖,和那个掉下去的玩家
老周是美术组长。上个月他拿着一组 Photogrammetry 扫描的悬崖资产来找我,脸上带着一种「我终于不用再雕 LOD」的释然。
那块悬崖源网格有两百三十万三角形。启用 Nanite 之后,远景是粗糙石块,近景能看到岩层纹理和细小裂缝。站在关卡里看,确实震撼。策划在旁边直点头:「玩家会沿着这条栈道走,到这里会触发剧情。」
两天后 QA 发了一张截图。角色站在栈道边缘,脚底下是空的,整个人正在做自由落体。
老周一脸懵:「我模型明明有地面啊。」
我打开 Static Mesh Editor,指着 Collision 栏说:「模型是有了,可物理引擎看不到。Nanite 渲染的是一套数据,碰撞用的是另一套。你只开了 Nanite,没给物理留门。」
这就是本文想聊的事。Nanite 解放了美术,但它在物理世界里是隐形的。要把视觉上的壮观变成玩家脚下的实地,中间隔着代理网格、碰撞体、复杂度设置、通道配置、物理资产、NavMesh,还有位移与顶点动画的一堆限制。
一、为什么 Nanite 网格不能直接当碰撞用
Nanite 的核心价值是「按需渲染三角形集群」。导入时,源网格被切分成最多 128 个三角形一组的 cluster,再构建成层级 DAG。运行时 GPU 根据屏幕大小和遮挡关系,决定画哪些 cluster、画多细。
这套机制对渲染极好,对物理却是灾难。
物理引擎需要稳定、连续、可查询的几何。它要回答的问题很简单:一个点在哪一侧?两个凸包是否相交?一条射线打到了哪个面?这些问题要求碰撞几何在内存里是完整的,边界是准确的,不会因为相机拉远就突然少掉一半三角形。
Nanite 的数据会流送、会简化、会按视角变化。同一个岩石,远看可能只有几百个三角形,近看是两百万。物理引擎不能容忍这种不确定性。所以它干脆不用 Nanite 数据做碰撞。
flowchart TD
A[源网格 Source Mesh] --> B{启用 Nanite}
B --> C[Nanite Cluster DAG]
C --> D[GPU VisBuffer 渲染]
B --> E[保留源网格副本]
E --> F[生成碰撞网格]
F --> G[PhysX / Chaos 物理求解]上图里最关键的一点是:源网格被分成了两条路。一条进 Nanite 管线,用于画面;另一条进物理管线,用于碰撞、射线、NavMesh。两条路起点相同,但之后各走各的。渲染再漂亮,也不会自动反馈到物理。
我在团队里经常说一句话:Nanite 解决的是「看得见」的问题,物理解决的是「摸得着」的问题。两码事。
二、代理网格:Nanite 的「替身」
除了碰撞,还有一些系统也不直接消费 Nanite 数据。比如某些动态阴影路径、距离场、HLOD 代理、Lightmap(Nanite 本身不支持 lightmap,但 fallback 可以参与部分烘焙流程)。这些场景下,引擎会用到 Nanite 的 fallback mesh,也叫 proxy mesh,中文常叫代理网格。
代理网格是源网格的一个低精度版本。导入 Nanite 时,引擎默认会自动生成一个 fallback。你可以在 Static Mesh Editor 的 Nanite Settings 里调整:
- Fallback Target:Auto / Triangle Percent / Relative Error;
- Fallback Triangle Percent:保留百分之多少的源三角形;
- Fallback Relative Error:允许的最大相对误差。
代理网格不是给物理用的。它的主要任务是当 Nanite 数据不可用、不支持或被显式禁用时,提供一个能画的网格。比如你在某个 Actor 上勾了 Disallow Nanite,或者目标平台不支持,就会回退到代理网格。
architecture-beta
group source[源资产]
group nanite[Nanite 管线]
group physics[物理管线]
group render[渲染管线]
service src as Static Mesh in source
service builder as Nanite Builder in nanite
service dag as Cluster DAG in nanite
service proxy as Fallback Proxy in nanite
service col as Collision Mesh in physics
service phys as Physics Engine in physics
service vis as VisBuffer BasePass in render
src:R --> builder:L
builder:R --> dag:L
builder:B --> proxy:T
src:B --> col:T
col:R --> phys:L
dag:R --> vis:L这张图把几条数据流画清楚了。代理网格和碰撞网格都从源网格派生,但各自为政。碰撞网格通常比代理网格更粗,因为物理不需要视觉细节,只需要一个合理的壳。
老周第一次看到这图时问:「那我把 fallback 精度调高点,能不能直接当碰撞用?」答案是最好别。fallback 关注的是「看起来像」,碰撞关注的是「相交对」。两者的生成目标不同,混在一起只会埋下隐患。
三、生成碰撞网格的三条路子
Static Mesh Editor 的 Collision 菜单提供了几种生成碰撞的方式:
3.1 自动凸包
Collision > Auto Convex Collision 会用 V-HACD 算法把网格拆成一组凸包。你可以控制最大凸包数量、凸包顶点和凹陷度。对于形状不规则的岩石,这是最快的方法。
缺点也明显:凸包数量多了物理开销大;数量少了细节丢失。一块两百三十万面的悬崖,如果直接 auto convex,可能要几十上百个凸包才能贴近形状,运行时并不便宜。
3.2 简单图元
Collision > Add Box / Sphere / Simplified Convex 适合规则物体。箱子、门板、石柱用 box 或胶囊最划算。组合几个简单图元,往往比一个复杂凸包性能更好。
3.3 从 DCC 导入 UCX
最可控的方式是在 Maya / Blender / 3ds Max 里命名碰撞体为 UCX_模型名,随 FBX 一起导入。这样美术可以精确控制哪些地方要碰撞、哪些地方要留空。英雄级岩石、建筑结构我通常推荐这条路线。
对于老周的悬崖,我们的最终方案是:
- 栈道和主体地面用 DCC 做的简模作为复杂碰撞;
- 突出的石块用十几个手动凸包;
- 远处不可达的山体用 box 粗略包围。
四、Collision Complexity:四个档位怎么选
Static Mesh Editor 的 Collision 分类下有个 Collision Complexity 选项,默认是 Project Default。它决定物理引擎在查询时怎么选形状。四个档位的区别如下:
| 档位 | 含义 | 适用场景 |
|---|---|---|
| Project Default | 按项目物理设置,简单查询走 simple,复杂查询走 complex | 大多数静态物体 |
| Simple And Complex | 同时保留两种形状,simple 用于常规物理,complex 用于复杂查询 | 需要精确射线检测的静态物体 |
| Use Simple Collision As Complex | 复杂查询也走 simple 形状,忽略三角网格 | 对精度要求低、想省内存的物体 |
| Use Complex Collision As Simple | 简单查询也走 complex 三角网格 | 需要精确地面、但不能做物理模拟 |
这里有个常见误解:选了 Use Complex Collision As Simple,物体就能像传统 mesh 一样被物理推着滚。错了。复杂碰撞是三角网格,PhysX 不支持用三角网格做刚体模拟。你可以站在上面、可以射线打到它,但不能让它受重力影响翻滚。
简单碰撞和复杂碰撞的查询代价可以粗略写成:
是简单碰撞的凸包数量, 是复杂碰撞的三角形数量。查询越频繁, 和 的影响越大。把两百三十万面的悬崖直接设成 Use Complex Collision As Simple,射线检测确实准,但每个 query 都要扫大量三角形,得不偿失。
我的习惯是:能走 simple 就不走 complex;必须走 complex 的地方,确保三角形数量经过控制,或者只在静态环境 ground 上用。
五、碰撞通道与 Physics Asset
5.1 自定义通道
项目里默认的通道(WorldStatic、WorldDynamic、Pawn、PhysicsBody 等)常常不够用。比如你需要一个只检测「可行走地面」的射线,而不想被装饰物、植被触发器干扰。
去 Project Settings > Engine - Collision 添加自定义 Object Channel 和 Trace Channel。假设新增一个 GroundTrace:
#define ECC_GroundTrace ECC_GameTraceChannel1
UStaticMeshComponent* GroundMesh = Cast<UStaticMeshComponent>(...);
GroundMesh->SetCollisionProfileName(TEXT("BlockAllDynamic"));
GroundMesh->SetCollisionObjectType(ECC_WorldStatic);
GroundMesh->SetCollisionResponseToChannel(ECC_Pawn, ECR_Block);
GroundMesh->SetCollisionResponseToChannel(ECC_GroundTrace, ECR_Block);
FHitResult Hit;
FVector Start = GetActorLocation();
FVector End = Start + FVector(0.0f, 0.0f, -10000.0f);
FCollisionQueryParams Params(SCENE_QUERY_STAT(GroundTrace), false, this);
GetWorld()->LineTraceSingleByChannel(Hit, Start, End, ECC_GroundTrace, Params);这段代码做两件事:一是把地面静态网格设置为只对 Pawn 和自定义 GroundTrace 通道产生 Block 响应;二是用 LineTraceSingleByChannel 向下打一条长射线,专门用来判断脚下是否有地。
通道设计要在项目早期定好。拖到后期再改,已放置的几千个 Actor 都要重新调预设,非常痛苦。
5.2 Physics Asset
角色和可动物体通常用 Skeletal Mesh,它们的碰撞由 Physics Asset 管理。Physic Body 一般用胶囊、球体、凸包组合,再加约束(Constraint)。
有个坑要避开:别给角色的 Physics Asset 用复杂碰撞。身体四肢用胶囊近似就行。我曾经见过一个项目给角色穿上扫描衣服的三角网格当物理碰撞,结果角色倒地时 PhysX 直接卡成 PPT。三角网格不能参与刚体模拟,这是物理引擎的硬限制。
六、位移与顶点动画:Nanite 的短板
Nanite 可以移动、旋转、缩放,甚至做非均匀缩放。但它不支持一般性的网格形变。
6.1 World Position Offset 能用,但有限制
材质里的 World Position Offset(WPO)在 Nanite 上是受支持的,但行为和传统网格不同。Nanite 会把使用 WPO 的 mesh 拆成更小的 cluster,每个 cluster 都有自己的 bounds,然后独立做剔除。
这意味着如果 WPO 位移幅度太大,cluster 的 bounds 会被撑开,远处看可能出现异常剔除,或者像素突然消失。 foliage 比较宽容,因为叶子本身有很多洞,自遮挡少。但一块实心的岩石如果被 WPO 顶出去半米,玩家就会看到石头边缘「缺一块」。
sequenceDiagram
participant M as 材质 WPO / Displacement
participant N as Nanite Cluster Builder
participant B as Cluster Bounds
participant H as HZB 遮挡剔除
M->>N: 提交最大偏移幅度
N->>B: 扩展每个 Cluster 的 Bounds
B->>H: 参与视锥与遮挡剔除
H->>H: 位移过大时边界外像素被错误剔除解决思路只有一个:把位移限制在可控范围内。别指望用 WPO 做大幅波动的水面或布料。
6.2 真正的顶点动画走不通
骨骼动画、Morph Target、Spline Mesh、逐实例顶点绘制,这些 Nanite 基本都不支持。如果你需要做水面、旗帜、藤蔓摆动、角色表情,请用传统网格,或者在 5.7+ 的实验性功能里谨慎尝试 Nanite Foliage / Nanite Skinning。
老周团队曾经想在 Nanite 岩石上做点「风化剥落」的小动画,用了 WPO 驱动顶点抖动。结果镜头拉远时岩石整片消失,因为 cluster bounds 被抖出视锥。最后改成在世界位置偏移里 clamp 偏移量,并且只在镜头很近时生效。
七、NavMesh:Nanite 不能替你走路
AI 能不能走,取决于 NavMesh。NavMesh 的生成依赖碰撞几何,而不是渲染几何。一个 Nanite 岩石再精致,如果没有碰撞,Recast 也不会在上面生成可行走区域。
对于 Nanite Landscape,情况稍微复杂。启用 Nanite 的地形依然保留了一份非 Nanite 数据,Recast 用这份数据生成 NavMesh。地形碰撞精度由 Landscape 的 Collision Mip Level 控制,0 最精确,5 最粗糙。
graph LR
A[Nanite Landscape] --> B[非 Nanite 地形数据]
B --> C[Landscape Collision]
C --> D[Recast NavMesh]
A --> E[Nanite 渲染]
D --> F[AI Controller 寻路]开放世界项目要特别注意 NavMesh 的内存和更新开销。大场景全图烘焙 NavMesh 可能吃掉几百 MB,而且运行时动态更新非常贵。配合 World Partition,用数据分层和局部 NavMesh 流送,比一张大图要靠谱。
老周的悬崖关卡在启用了 Landscape Nanite 之后,AI 全部站在原地发呆。查了一下午,发现是 Landscape 的 Collision Mip Level 被美术顺手改成了 5,地形碰撞比视觉地形低太多,NavMesh 以为脚下是平地,实际角色已经悬空。改成 2 之后恢复正常。
八、Runtime Virtual Texture 位移:给地面加细节的另一条路
如果位移只是为了给地面增加凹凸细节,不一定非要在几何层面折腾。Runtime Virtual Texture(RVT)可以把一张 displacement 纹理动态投影到地形上,再在地形材质里采样,输出到 Displacement 节点。
基本流程是:
- 创建 RVT Texture,Virtual Texture Content 设为 Displacement;
- 在 Landscape 材质里添加 Runtime Virtual Texture Sample,读取 displacement;
- 把采样结果连到
MakeMaterialAttributes的 Displacement 输入; - 开启材质的
Enable Tessellation,用 Nanite Tessellation 把位移变成真实几何起伏; - 绘制 Landscape Layer,RVT 会按区域更新。
在 DefaultEngine.ini 里需要先打开开关:
[/Script/Engine.RendererSettings]
r.Nanite.AllowTessellation=1
r.Nanite.Tessellation=1这里有几个坑要避开:
- Nanite Landscape 和非 Nanite 数据会同时存在内存里,数据量翻倍;
- RVT 采样依赖非 Nanite 地形数据,水渲染、Runtime Virtual Texture 等系统仍然要走老管线;
- 重新绘制地形时如果 Nanite 正在重建,有概率崩溃。稳妥做法是画 layer 前临时关闭 Nanite,画完再重建。
RVT 位移的好处是:细节来自纹理,不需要把每块石头都雕进地形网格。坏处是:它只影响视觉和简单碰撞(如果 complex collision 用得细),对简单碰撞图元没有影响。所以栈道、台阶这些玩家必须精确站立的地方,还是要靠碰撞体解决。
九、团队故事:裂谷关卡的三天调试
老周的悬崖属于一个更大的裂谷关卡。我们花了三天才把物理和视觉对齐。
第一天,美术把两百三十万面的悬崖导进来,开了 Nanite,没管碰撞。玩家走到栈道尽头直接穿模坠落。QA 报了十七个穿模 bug。
第二天,程序用 Auto Convex Collision 生成了四十多个凸包。角色不掉下去了,但在凸包缝隙处会卡住,镜头也会轻微抖动。profiler 显示物理查询时间从 0.8ms 涨到 2.4ms。
第三天,我们重新分工:
- 栈道和主体地面保留一份低模,作为复杂碰撞源;
- 英雄级岩石用手动 UCX 凸包;
- 远景山体用简单 box;
- 碰撞复杂度统一用 Simple And Complex;
- 新增 GroundTrace 通道,专门用于角色脚下检测;
- Landscape Nanite 的 Collision Mip Level 固定为 2;
- 地面细碎凹凸用 RVT displacement 表现,不再雕进地形。
改完之后,物理查询时间回到 0.9ms,穿模 bug 清零。老周看着屏幕说:「原来 Nanite 只负责好看,走路还得靠这些丑盒子。」
这句话我记了很久。
十、实操检查清单
给程序:
- 所有 Nanite 静态网格都要检查是否有碰撞;
- 根据物体用途选择 Collision Complexity;
- 自定义 Trace Channel,避免所有射线都走 BlockAll;
- 角色 Physics Asset 只用简单图元,禁用复杂碰撞;
- 用
stat physics、stat collision监控开销; - 大场景规划 NavMesh 分区和流送。
给美术:
- 英雄级物体尽量在 DCC 里做 UCX 碰撞体;
- 复杂地面如需精确行走,提供一份低精度碰撞源 mesh;
- 别让 Landscape Collision Mip Level 和视觉地形脱节;
- 用 RVT displacement 表现细碎凹凸,但别指望它修正碰撞;
- 大幅 WPO / 顶点动画请交给传统网格。
十一、几个容易踩的小坑
最后列几个我在项目里见过不止一次的低级错误。
开了 Nanite,忘了看碰撞。 美术导入高模后点一下 Enable Nanite,以为万事大吉。测试时角色穿墙,才发现 Static Mesh Editor 里 Collision 栏空空如也。建议团队定一条规则:导入 Nanite 资产后必须先做一次碰撞检查,再提交版本控制。
把复杂碰撞当成万能药。 有些同学遇到穿模就顺手改成 Use Complex Collision As Simple。短期问题消失,长期性能爆炸。复杂碰撞的查询开销随三角形数线性涨,只该用在真正需要精确形状的地方。
通道响应默认全 Block。 默认预设 BlockAll 很方便,但会让不必要的物体参与射线检测。自定义通道初期多花十分钟,后期能省下几个小时调试。
Landscape Nanite 和 Collision Mip Level 不匹配。 这是老周踩过的坑。视觉地形很陡,碰撞地形很平,AI 和玩家都会以为脚下有地。启用 Landscape Nanite 后,一定要打开 Player Collision 视图核对。
RVT displacement 和 Nanite 重建冲突。 边画 layer 边重建 Nanite,编辑器可能直接崩溃。养成习惯:大改地形前临时关掉 Nanite,改完再重建。
写在最后
Nanite 改变了美术做资产的方式,但它没有取消物理规则。渲染管线和物理管线仍然是两个世界,中间需要开发者手动搭桥。
这篇文章里反复出现的几个关键词,代理网格、碰撞网格、Collision Complexity、通道、Physics Asset、NavMesh、RVT,其实就是那座桥。桥搭得好,玩家既能享受两百万面岩石的视觉冲击,也能稳稳地站在上面,并感受到真实的空间层次。
桥搭不好,画面再震撼,也只是一个好看的空壳。
老周现在每个 Nanite 资产导入后都会先问一句:「碰撞做了吗?复杂碰撞还是简单碰撞?要走哪个通道?」这些问题听起来很程序,但美术越早参与,后面返工越少。
工具再聪明,也替代不了判断力。希望老周那三天的调试,能让你少花两天。
参考与延伸阅读
- Nanite Virtualized Geometry - Unreal Engine 5.8 Documentation
- Nanite Technical Details - Unreal Engine 5.8 Documentation
- Simple versus Complex Collision - Unreal Engine 5.8 Documentation
- Collision Response Reference - Unreal Engine 5.8 Documentation
- Using Nanite with Landscapes - Unreal Engine 5.8 Documentation
- Nanite Displacement & Runtime Virtual Textures - Brushify
- Nanite Dynamic Displacement - Unreal Engine Public Roadmap