老周负责的项目最近进入铺量阶段。关卡里要放一片山地森林,几千棵树、几百块岩石,还有几处残破的石墙废墟。美术同事手动摆了一周,树的位置总像阅兵,石头分布又太平均,玩家跑两遍就觉得假。老周想找个办法,把重复布置工作自动化,同时保留手工调整的空间,避免生成结果千篇一律。老周试了几套脚本工具,最后把目光投向 UE5.8 的 PCG Framework。他想知道这东西到底能不能把看起来自然这件事自动化,同时又不失控。
PCG Framework 是什么
PCG 全名 Procedural Content Generation,在 UE5 里是一套以节点图驱动的程序化内容生成框架。它把生成规则抽象成可编辑、可复用、可调试的资产,而不是直接在场景里随机撒模型。你画一张图,描述输入数据经过哪些节点处理,最终变成什么输出。
它的核心价值在于规则可沉淀。美术只管调参数,程序只管写节点,策划可以只关心结果。生成过程是确定性的,同一个图、同一个种子,反复执行得到的结果完全一致。这对版本控制和回归测试很友好。
UE5.8 里 PCG 插件已经脱离实验性标签的束缚,API 更稳定,性能也更扎实。下面这张架构图能帮你先建立整体印象。
flowchart TD
subgraph world ["世界/关卡"]
landscape["Landscape"]
mesh["Static Mesh"]
end
subgraph pcg ["PCG 运行时"]
component["UPCGComponent"]
graph["UPCGGraph"]
nodes["PCG Nodes"]
end
subgraph data ["PCG 数据"]
end
landscape --> component
mesh --> component
component --> graph
graph --> nodes
nodes --> point
nodes --> spline
nodes --> surface
nodes --> volume世界提供输入,组件持有图,图调度节点,节点产出数据。看着简单,真正用起来你会发现每个层级都有自己的脾气。
UPCGComponent、UPCGGraph、UPCGData
这三个类是 PCG 的骨架。
UPCGComponent 继承自 UActorComponent,通常挂在 Actor 上。它负责三件事:保存当前激活的 UPCGGraph、触发生成、把生成的结果同步到渲染层。你可以在编辑器里点击 Generate 按钮,也可以在运行时调用 Generate 接口。组件还会管理种子、属性覆盖、调试绘制。
UPCGGraph 是真正的规则资产。它内部维护一个节点列表,每个节点都是 UPCGNode 实例。节点之间通过输入输出引脚连接。图本身不保存生成的网格实例,只保存如何生成。这让一个图可以被多个组件复用,也可以被打包成子图嵌套到更大的图里。
UPCGData 是数据抽象基类。节点处理的是数据,而不是直接操作场景对象。Point、Spline、Surface、Volume 都继承自它。把数据和生成逻辑拆开,是 PCG 设计里最聪明的一步。你可以把中间结果缓存下来,也可以把数据导出去给别的系统用。
组件触发生成时,会先把 Graph 编译成执行计划,再逐节点调用 Execute。每个节点只读输入、写输出,副作用被限制在组件内部。这种隔离让多线程化变得可行,也是 UE5.8 性能提升的关键。
Point / Spline / Surface / Volume 四种数据类型
PCG 里所有能被节点处理的数据,最终都落到这四类里。别被名字唬住,它们其实是不同粒度的空间信息。
Point 是最常用的原子数据。一个 Point 不只包含 Transform,还携带一组 Attributes,比如密度、法线、颜色、随机值。密度是 Point 的灵魂,取值范围通常是 ,用来控制在这里放东西的概率。如果你有一千个点,每个点密度 ,那么总密度 ,某个点被选中时贡献的概率可以写成:
Spline 是曲线数据。做道路、河流、藤蔓、城墙残骸时很有用。节点可以沿 Spline 等距采样出一组 Point,再在这些点上生成模型。样条本身也能携带属性,比如宽度、优先级。
Surface 是表面数据。最常见的来源是 Landscape 或静态网格的三角面。Surface 节点会把表面离散成点云,同时记录每个点的法线、曲率、材质层权重。植被撒点通常从 Surface 开始。
Volume 是体积数据。它描述一个三维区域,常用来做剔除、遮罩或局部密度控制。比如你想让废墟内部不生树,就可以在废墟周围放一个 Volume,用 Difference 节点把内部点全部移除。
它们的关系可以画成下面这张图:
graph LR
A[PCGData] --> B[Point]
A --> C[Spline]
A --> D[Surface]
A --> E[Volume]
B --> F["Transform + Attributes"]
C --> G["Curve + Sampling"]
D --> H["Triangles + Layers"]
E --> I["Bounds + Mask"]下面这张图用简单的点云示意 Point 在 Landscape 上的分布。
实际工作中,一个典型的森林生成流程是这样:从 Landscape 拿到 Surface,按坡度、朝向、材质层筛选后得到候选 Point;再用 Volume 把建筑周围和道路两侧剃掉;接着给每个 Point 赋予随机缩放、旋转、种类;最后交给 Mesh Spawner 节点生成 HISM 实例。岩石和废墟同理,只是输入换成 Spline 或手工放置的 Volume。
Mesh Spawner 会根据 Point 的属性决定放哪个 Static Mesh、用哪个材质变体。你可以用一个属性叫 MeshIndex,每个值对应一种树木模型。0 可能是 oak,1 可能是 birch,2 可能是 pine。这样一片森林里几种树会自然混种,而不是单一树种排成阵列。
生成流程与执行顺序
了解数据类型之后,再看生成流程就顺了。UE5.8 的生成流程大致分为五步:输入收集、图编译、节点执行、数据到实例、结果提交。
下面这张图把关键节点串起来:
flowchart LR
A["输入数据"] --> B["Graph 编译"]
B --> C["节点 A 执行"]
C --> D["节点 B 执行"]
D --> E["生成 ISM/HISM"]
E --> F["提交到场景"]输入收集阶段,组件会 gather 所有和当前 Actor 绑定的输入:Landscape、Spline、Volume、Mesh 等。然后图编译阶段会把节点图转成执行顺序。节点之间的连接决定依赖关系,没有依赖的节点可以并行执行。UE5.8 在这部分做了优化,子图会被实例化,重复使用的子图只编译一次。
节点执行阶段是最耗时的。每个节点拿到输入数据,执行自己的算法,输出新的数据。节点执行不是黑盒,你可以在编辑器里看到每个节点产出了多少 Point、耗时多少毫秒。调试信息对定位性能瓶颈很有帮助。
数据到实例阶段,Mesh Spawner 节点会把 Point 转成 Instanced Static Mesh 或 Hierarchical Instanced Static Mesh。HISM 适合大面积植被,因为支持 LOD 和裁剪。最后结果提交到场景,编辑器下是持久化到 Actor 的子对象,运行时则是挂载到组件的实例化组件。
这里有一个常见误区:节点的视觉左右顺序并不等于执行顺序。真正决定先后的是数据依赖。如果你把两个互不相干的节点摆成上下两行,它们完全可能同时跑。反过来,如果一个节点依赖上一个节点的输出,即使它们在图里挨得很近,也必须等前者完成。
种子与随机性
程序化生成最怕两种结果:一是每次生成都一样,看起来死板;二是每次生成都不一样,调好的参数没法复现。PCG 用种子解决了这个矛盾。
每个 UPCGComponent 都有一个 Seed。生成开始时,这个种子会作为随机数流的起点。每个节点在内部根据父种子和自身 Node ID 再派生一个子种子。于是同一张图、同一个组件种子,输出永远稳定;换一个种子,分布就换一版。这种层级种子链可以写成:
节点里的随机操作,比如随机缩放、随机种类、Perlin 噪声,都从这个种子流取数。设计师只需要调一个 Seed 滑条,就能遍历多种变体,找到最喜欢的一版。
调整 Seed 时别指望每个种子都好看。我一般会批量生成十个版本,让策划和美术一起挑。挑中某一版后把 Seed 记下来写进组件默认值,这样全团队打开关卡看到的都是同一版。
种子不是万能的。有些节点依赖外部状态,比如时间、玩家位置、运行时生成的输入数据。这类节点在运行时每帧都可能变化,输出不再完全确定。UE5.8 给这类节点做了标记,方便你识别哪些部分是可缓存的,哪些必须实时算。
还有个细节:子图继承父图种子时,Epic 在 5.8 里把哈希算法从简单的 FMath::Rand 改成了更稳定的 CityHash 变种。这意味着跨版本的种子值行为更一致,项目升级后不会因为种子变了导致场景大变。
与 Landscape 的集成
植被生成最自然的输入就是 Landscape。PCG 提供了 Landscape 采样节点,可以直接读取地形高度、法线、曲率、材质层权重。你不需要自己写射线检测,也不需要手工烘焙高度图。
老周的做法是:先用 Landscape Surface 节点把地形转成 Point 云,每个点自带地形法线和材质层信息。然后用 Attribute 节点计算坡度:
接着用 Filter 节点去掉坡度过陡的点。灌木喜欢缓坡,岩石可以容忍陡坡,树木要避开悬崖。不同植被类型用不同阈值,就得到分层分布。
材质层权重也重要。草地层的权重高,就多长草和矮灌木;泥土层权重高,可以裸露地面并放大石头;雪线以上直接清空植被。所有这些判断都发生在图节点里,策划改的是数值,不是位置。
UE5.8 的 Landscape 采样节点新增了对 Physical Material 的读取。你可以根据物理材质决定生成什么,比如只在 Rock 物理材质上长苔藓,只在 Grass 上长花。这比按材质层判断更稳定,因为材质层可能随着地形绘制被美术改来改去,物理材质相对固定。
老周会把 Physical Material 和材质层权重混着用。比如只在 Grass 物理材质且草层权重大于 0.6 的区域生成花丛,两层筛选叠加后分布会更有层次。你也可以把坡度、朝向、高度三个条件串起来,只在向阳缓坡且海拔适中的地方生成特定树种。
编辑器生成与运行时生成
PCG 既能当编辑器工具,也能当运行时系统。两者的差别不只是调用时机。
编辑器生成通常在关卡编辑阶段完成。设计师调好图、生成满意后,可以把结果烘焙成普通的 Static Mesh 实例或 HISM 组件。好处是运行时不承担生成开销,场景里看到的就是最终结果。坏处是占用磁盘和内存,而且变体固定。
运行时生成在 BeginPlay 或按需触发。适合开放世界、Roguelike、每次开局都不同的关卡。生成逻辑和 assets 一起打包,玩家进入时再算。代价是 CPU 和内存峰值,以及首次生成时的卡顿。
两者的调用路径可以用一张序列图表示:
sequenceDiagram
participant 设计师
participant 组件 as UPCGComponent
participant 图 as UPCGGraph
participant 数据 as UPCGData
participant 实例 as ISM/HISM
设计师->>组件: 点击 Generate
组件->>图: 编译并执行
图->>数据: 生成 Point/Spline/Volume
数据-->>实例: 转换
实例-->>组件: 持久化到 Actor
Note right of 组件: 编辑器生成
玩家->>组件: BeginPlay
组件->>图: Execute
图->>数据: 生成数据
数据-->>实例: 转换
实例-->>组件: 运行时挂载
Note right of 组件: 运行时生成实际项目里,我倾向把大场景拆成两部分:地形植被在编辑器生成并烘焙,小道具和每次随机废墟在运行时生成。这样既能保证核心场景的稳定,又能保留一些变化。
烘焙大面积植被时,不需要给每棵树都生成精确碰撞。玩家能到达的小范围里保留胶囊体碰撞就够了,远处森林直接用 Landscape 碰撞或者简化包围盒。省下的物理开销很可观。
UE5.8 的改进
5.4 到 5.8 之间,PCG 的改动不在外观上,而在骨子里。
编译阶段更快了。子图实例化和节点依赖分析在 5.8 里被集中处理,重复子图不再反复编译。大图的第一次 Generate 时间有明显下降。
执行阶段更敢用多线程。以前很多节点在 GameThread 上跑,节点一多编辑器会卡。5.8 把大量纯计算节点移到 TaskGraph,CPU 占用更平滑。你打开 Stat PCG 面板,能看到每个节点分别在哪个线程上执行。
调试工具也更顺手。节点会显示输入输出 Point 数量、执行耗时、内存占用。新版还加了 Graph Insights,能高亮出数据依赖链。排查为什么这片区域没树变得简单。
API 层面,自定义节点的门槛降低了不少。Settings 和 Element 的绑定更清晰,属性反射自动同步到节点面板。下面是一个最简的自定义 Settings 示例:
// 来源:MyProject/PCG/MyPCGDensityFilter.h
UCLASS()
class UMyPCGDensityFilter : public UPCGSettings
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, Category = "PCG")
float MinSlope = 0.0f;
UPROPERTY(EditAnywhere, Category = "PCG")
float MaxSlope = 45.0f;
UPROPERTY(EditAnywhere, Category = "PCG")
bool bInvert = false;
};这个类被图节点引用后,蓝图和 C++ 都能编辑参数。真正执行过滤的逻辑在对应的 FPCGElement 里实现,和 Settings 分离。5.8 的文档把这层关系讲得更清楚,新手不容易把两者混在一起。
体验上还有一个改进:编辑器里 Generate 按钮支持部分节点重新执行。你只改了一个 Filter 阈值,不必全图重跑,PCG 会只重算依赖链上的节点。以前改一个阈值要重跑全图,现在只重算依赖链,响应速度快了很多。
几个容易踩的坑
第一个坑是把 Point 的 Density 当成布尔值用。Density 为 0 不意味着绝对不放,很多节点会用它做概率加权。如果你只想剔除某些点,应该用 Filter 节点,而不是把 Density 全部写成 0 或 1。
第二个坑是忽略 Bounds。PCG 的节点在处理大数据集时会根据 Bounds 做空间分区。如果你的输入 Surface 没有正确 Bounds,节点会认为数据无穷大,导致内存暴涨或者生成失败。遇到生成卡住,先看 Bounds 对不对。
第三个坑是过度依赖运行时生成。很多玩家第一次进入关卡时,如果 PCG 要算几千个实例,帧时间会很难看。建议把高频出现、数量大的内容烘焙掉,只把需要随机变化的小部分留给运行时。
第四个坑是子图递归过深。子图能嵌套,但嵌套层级深了会让编译时间变长,调试也困难。我的经验是一般不要超过三层,超过三层就考虑拆成多个组件或者预处理数据。
写在最后
老周后来把山地森林拆成三层:底层用 Landscape Surface 生成大片松树和草;中层沿 Spline 布置石径和倒木;顶层用手工 Volume 控制废墟和营地的范围。每次他调整种子,就会得到一版新的森林。策划挑了三版最自然的,美术再 hand polish 局部。
PCG 不是要让机器替代美术,而是把重复劳动前置,让创意留在最后。理解它的核心概念后,你就能决定哪些东西应该交给图,哪些东西必须人工把控。
记住下面这几点:
- UPCGComponent 负责触发,UPCGGraph 负责规则,UPCGData 负责数据。
- Point、Spline、Surface、Volume 是四种基本数据,理解它们才能选对输入节点。
- 节点执行顺序由数据依赖决定,不是视觉顺序。
- 种子保证可复现,层级哈希链是稳定随机的基础。
- 与 Landscape 集成时,优先用法线和物理材质做筛选。
- 编辑器生成适合烘焙大场景,运行时生成适合变化内容。
- UE5.8 的编译、多线程、调试和 API 都比早期版本扎实很多。
把这套框架用熟,山地里会长出真正的森林,而不是排列整齐的树。