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,或者目标平台不支持,就会回退到代理网格。
flowchart TD
subgraph source ["源资产"]
src["Static Mesh"]
end
subgraph nanite ["Nanite 管线"]
builder["Nanite Builder"]
dag["Cluster DAG"]
proxy["Fallback Proxy"]
end
subgraph physics ["物理管线"]
col["Collision Mesh"]
phys["Physics Engine"]
end
subgraph render ["渲染管线"]
vis["VisBuffer BasePass"]
end
src --> builder
builder --> dag
builder --> proxy
src --> col
col --> phys
dag --> vis这张图把几条数据流画清楚了。代理网格和碰撞网格都从源网格派生,但各自为政。碰撞网格通常比代理网格更粗,因为物理不需要视觉细节,只需要一个合理的壳。
老周第一次看到这图时问:「那我把 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