UE5程序化内容生成框架(PCG)
程序化内容生成框架(PCG)是一个用于在虚幻引擎中创建你自己的程序化内容及工具的工具集。借助PCG,技术美术师、设计师和程序员能够构建任意复杂度的快速迭代式工具和内容,从资产工具(如建筑物或群系生成等)到整个世界,不一而足。
Point 是 PCG 中非常重要的概念。例如,要生成一片森林,我们首先需要确定的是生成对象的位置点,也就是“散布点”的操作,然后,只需要将 StaticMesh 放置在这些点所在的位置就行了。
Point 上可以携带各种各样的 Attribute,譬如大小,方向,位置,等等。在将 StaticMesh 放置到这些点的时候,可以利用这些 Attribute 来确定实际对象的大小,方向,位置,等等。
Attribute 可以跟随 Point 从上一个步骤流向下一个步骤,然后下一个步骤的节点再利用 Point 所携带的信息来处理自己的逻辑,因此,Attribute 是 PCGGraph 中信息传递的重要载体。
PCG 中注重 Point 的概念,暂时没有发现与 Houdini 中 Vertex,Edge,Primitive 对应的概念,虽然有名为 UPCGPrimitiveData 的类型,但看起来概念上似乎与 Houdini 中的 Primitive 有所区别。由于还没有系统地阅读 PCG 相关的代码,且 PCG 尚处于测试开发的阶段,如果有误或功能完善会回来更正。
要启用 PCG,需要确保引擎更新到 UE5.2 版本,并启用如下插件。

PCG GraphNode
此处介绍 PCG GraphNode。本节会尽量介绍 PCG Graph 中每个节点的使用方法。如需快速查阅可使用 Ctrl+F 搜索对应 “ * + 节点名称”关键字。
e.g.
查询 MeshSampler 可使用关键字 *MeshSampler。
[ Sampler ]
Sampler 分类下的节点提供采样并生成 Points 的功能。
*MeshSampler
MeshSampler 提供在 StaticMesh 上采样模型表面 Points 的功能,通常这个节点需要配合 CopyPoints 节点来使用,以使采样到的 Points 符合被采样物体的全局空间变换。

PointSampler 提供从一组给定的 Points 中随机筛选 Points 的功能,可以设定随机筛选的比例,图中是按照 10% 比例筛选上个示例的 Points 的效果。

SplineSampler 提供从给定的 PolyLineData 采样样条线上的 Points 的功能。

SplineSampler 可以选择多种生成方式,如果选择的是 Interior 方式生成,还可以通过曲线来控制优边缘到中心的 Density 变化。

SurfaceSampler 提供从给定的 SurfaceData 采样表面 Points 的功能。有效的数据是这种绿色的 SurfaceData,但也可接受 SpatialData,例如 GetActorData 节点提供的数据,但只会简单地采样 BoundBox 表面。

*VolumeSampler
VolumeSampler 提供从给定的 SpatialData 采样体积 Points 的功能。

*CopyPoints
CopyPoints 将源输入的点云复制到目标点云中每个点的位置。在复制的过程中会应用目标点云中点的变换,也可选择继承 Source 或 Target 其中之一的属性。与 Houdini 中的 CopytoPoints 不同,CopyPoints 只能接受 PointData,在使用上会带来一定的限制。

*CreatePoints
CreatePoints 用于手动创建 Points。示例图片中使用 CreatePoints 创建了三个 Point。

CreatePointsGird 创建一个 Points 网格阵列。

MeshSocketsToPoints 将 Mesh 上的插槽转换为 Points。

PointFromPlayerPawn 将 PlayerPawn 转换为一个 Point。

StaticMeshSpawner 利用给定的 Points 信息(位置,缩放,旋转)生成 StaticMesh。
SpawnActor 利用给定的 Points 信息(位置,缩放,旋转)生成 Actor。示例图片中白色元素为一个包含三角锥的蓝图。

[ Density ]
Density 是一个常用的 Attribute,它常被用来甄别 Points 是否应该存在。例如,通过比较 Density 是否大于或小于某一个值,来删除或保留 Points。

DensityNoise 用于随机化 Point 上的 Density 值。

DensityRemap 用于将 Density 的值从一个区间映射到另一个区间。

相比于 DensityRemap,CurveRemapDensity 提供更自由的映射形式,可以通过曲线来控制 Density 值的映射。

DistanceToDensity 计算一个指定坐标点到每个 Point 的距离,并直接将计算的值赋值给 Density 属性。

在测试的过程中发现 DistanceToNeighbors 的行为比较奇怪,查看实现逻辑和关键代码,发现只有一个临时的替代方案。DistanceToNeighbors 的意图是将 TargetPoints(input0)投影到 OpitionalSamplePoints(intput1)所代表的空间平面上,然后计算投影点与 OpitionalSamplePoints 间的距离。
*DensityFilter
DensityFilter 筛选并输出 Density 值为特定区间的 Points。示例图片中红色部分为被筛选的 Points,白色部分为未被筛选的 Points。注意未被筛选的 Points 不会输出。

FilterByTag 用于二次筛选。假设有这样一种情况,我们的某个对象都诞生自同一个蓝图类型,但具备不同的 Tag 来区分彼此,我希望获取 World 中所有的蓝图对象,然后在 PCGGraph 中分门别类对它们进行不同的处理。这时我需要获取所有的对象,然后进行二次筛选。
图片示例中的三个对象属于同一种蓝图,但右侧一个具有“TagA”标签,于是可以使用 FilterByTag 过滤“TagA”将它筛选出来。

IntersectWithTaggedActorsGeometry 计算给定 Points 中的 Point 是否在一 TaggedActors 内部或外部。

PointFilter 根据输入的 Filter 属性值从给定的 Points 中筛选符合要求的 Points。

SelfPruning 剔除范围重合的点。

*BoundsModifier
BoundsModifier 用于修改 Point 上的 Bounds 属性。
Bounds 是 PointData 的固有属性,用于确定 Point 在空间中所占的体积。一些节点会以 Bounds 为依据来执行逻辑,譬如 SelfPruning。

CreateSpline 将给定的 PointData 转换为 PolyLineData。

Difference 计算 Source 输入与 Differences 输入的差集。目前尚只有 Binary 一种 DensityFunction 可以正常使用。

Difference 在进行计算时会参考 Differences 输入的 Stepness 属性来决定计算范围。

Distance 计算从 Source 输入的 Points 到 Target 输入 Points 的最小距离和方向。

ExtentsModifier 与 BoundsModifier 都可以修改 Points 上的 Bounds 属性,它们之间有概念上的区别。从它们的执行模式上也能看出一定的区别。
Extents 是一个三维向量。表示空间数据的尺寸,也就是宽度、高度和深度。Extents 的中心坐标永远处于 Extents 所表示范围的中心,因此定义 Extents 只需要一个三维向量即可。
Bounds 是一个轴对齐的包围盒。表示空间数据的位置和大小。Bounds 的中心坐标可以不在 Bounds 所表示范围的中心,因此定义 Bounds 需要两个三维向量。
Extents 和 Bounds 之间有一定的关系,但不一定相等。比如,如果空间数据的原点是(0, 0, 0),那么它的Bounds 就等于它的 Extents(在 PCGPointData 概念中)。但是,如果空间数据的原点不是(0, 0, 0),那么它的 Bounds 就要根据原点的偏移来计算。

三个节点功能相似,都用于合并或分离数据。
Merge 与 Union 功能相似,用于合并 MultipleData,Merge 只能合并 PointData 类型的数据,而 Union 没有限制。
Gather 节点用于分离 MultipleData,但还没有开发完全。

GetData 是一个系列。GetActorData 是父对象,其他几个都继承自 GetActorData 是子对象,它们与父对象的区别在于将父对象提供的数据过滤为了某个特定的类别。

以 GetLandscapeData 为例,它判断 InDataType 是否包含 EPCGDataType::PolyLine 这个枚举值,以决定是否输出到 FPCGDataCollection。
因此,你会发现有时下面的用法也是可以正确执行的。

目前 EPCGDataType 有以下一些类别。

GetTextureData 是较为特殊的 GetData,它采样一个 Texture,作用范围是当前 PCGVolume,值为 0 的部分不会产生 Points。

GetTextureData 会将 Texture 的通道值输出到 Density,就像下面这样。

Intersection 计算输入数据之间的交集。

NormalToDensity 将 Points 朝向转换为 Density 参数,用于识别 NormalToDensity 所处的坡度。

Projection 用于将 Source Points 投影到 Target 表面之上。Target 输入需要 SurfaceData 类型的数据,因此经常需要与 WorldRayHitQuery 节点联用。
WorldRayHitQuery 用于场景的射线追踪,可以精确捕获场景中的 SurfaceData 数据。

WorldVolumetricQuery 用于场景的体积查询,可以精确捕获场景中的 VolumeData 数据。此处捕获了一个外部 StaticMesh 的体积数据。

ToPoint 将一个 SpatialData 数据转换为特定的 PointData 类型数据。

TransformPoints 可以改变 Points 上的 Transform 的相关属性。它还可以用来随机化 Transform 属性。

[ Metadata ]
关于 Metadata
Metadata 节点大多与 Attributes 有关,Attributes 是每个 Data 元素上具有的数据,在 AttributesList 中代表一整行。

0号元素上的 Attributes
在 PCGGraph 中,GraphData 携带的原生数据被称为 Property,例如 PointData 上的 Position 和 Rotation。由用户或节点在后期附加的数据被称为 Attribute。Property 与 Attribute 的不同是在调用时,需要在名称前加上“$”符号。

在 PCGGraph 中,会看到很多不同类别的数据端口,从图标的样式上可以将它们归纳为三种类别。
第一类是下面这种只有一个圈和纵向排列多个圈的形式,表明这个端口表示单对象概念还是多对象概念。

在 PCGGraph 中,所谓“PCGData”,通常表示的是一个数据的合集。
例如“PCGPointData”表示的不是“一个点”,而是“一个对象上的所有点”。

PCGData 在节点间通常以 PCGData 数组的形式输出传递,如果是多对象的类型输入,则标志这个输入有能力处理 PCGData 数组,而不是仅处理 PCGData 数组中的第一个元素。
第二类是下面这种无纵深和有纵深的形式,表明这个端口可否接受多个输入。这方面目前做得还不够完善,理论上,我们应该可以通过 Gather 节点来自由地选择多输出中的单个输出。

还有一种白色的端口,在概念上有些不同,它是 SpatialData 类型。基本上,有色的端口一般代表着“某种”数据类型,而白色的端口就像一个大篮子,它可以装几乎所有 PCGGraph 中的数据类型,因为大部分数据都是诞生自它的子类。SpatialData 端口无法通过样式来识别 MutipleData 或 MutipleInput,需要通过鼠标提示来仔细甄别。

第三类就是我们开头提到的 Attribute,你可以看到它没有纵深,也没有纵向排列,它看上去像一个“条目”。

一个 Attribute (注意此处不同于 Attributes)代表某一个单独的数据,如果我们要将这个单独的数据赋予给“成组”的数据要将如何呢?那就要给这个组里的每个数据各分一个,于是在 AttributesList 中,一个 Attribute 占有一列。

一个 Attribute 存在于每个数据元素上
*CreateAttribute / *AddAttribute
CreateAttribute 用于创建一个属性,AddAttribute 用于向目标数据中添加一个属性。

AppendAttributeSet 用于将多个 Attribute 合并为 AttributeSet。

AttributeBitwiseOp 用于属性之间的位运算,支持 int32 或 int64 类型的属性。

AttributeBooleanOp 用于属性之间的布尔运算,输出 bool 值,支持 bool 类型的属性。

AttributeCompareOp 用于属性之间的比较运算,输出 bool 值。InputSurceA 和 InputSurceB 的类型不一定需要一致,但类型不一致时会遵循一些特殊的规则。

AttributeMathOp 用于属性之间的数学运算。InputSurceA 和 InputSurceB 的类型不一定需要一致,但类型不一致时会遵循一些特殊的规则。

AttributeOperation 用于将一个来源属性上的数据复制到目标属性,如果目标属性不存在则会创建目标属性。

AttributePartition 根据一个指定的属性,将输入的数据元素分组。节点目前只会输出与第0号元素属性相同的有效分组,尚无法输出其他分组,也没有节点来使用分组。图片示例中,可以看到 AttributePartition 应该输出两个分组,但它将每一个 PositionX = -500 的元素都作为一个单独的分组输出。
如果进需要按属性对 Points 进行筛选,可以使用 DensityFilter 或 PointFilter 节点替代。

AttributeReduce 计算所有元素中某个 Attribute 的最大值,最小值或平均值。

AttributeRename 用于重命名某个 Attribute。

AttributeRename 计算与空间旋转相关的一些操作。根据不同的执行功能,它最多接受三个输入。

AttributeSelect 从一维或多维 Attribute 中的某个维度中选择具有最大,最小或中间值的那个元素,并输出这个元素具有的 Attribute 值或这个元素。

AttributeRename 计算与空间变换相关的一些操作。根据不同的执行功能,它最多接受三个输入。操作符似乎尚不完全,无法应用矩阵。

AttributeTrigOp 执行与三角函数相关的计算。

AttributeVectorOp 执行与向量相关的计算。

MakeTransformAttribute 生成一个 Transform 类型的 Attribute。
BreakTransformAttribute 将一个 Transform 类型转换为 Vector3 类型。

MakeVectorAttribute 生成一个 Vector2 / Vector / Vector4 类型的 Attribute。
BreakVectorAttribute 将一个 Vector2 / Vector / Vector4 类型转换为分量所携带的类型。

DataTableRowToAttributeSet 将数据表中的某一行转换为 AttributeSet。

FilterAttribute 筛选某一个特定的 Attribute,可以选择只保留或删除这个 Attribute。

GetAttributeFromPointInde 输出某个特定元素及其 AttributeSet。

PointMatchAndSet 对于所有点,如果找到匹配(例如某个属性等于某个值),则在该点上设置一个值(例如另一个属性)。图片示例中,将 TempB 值为 5 的元素的 TempA 属性值都修改为 0。

TransferAttribute 将 Source 中的属性传递给 Target。

*SetPointColor
SetPointColor 可以设置 Point 上的 Color 属性。

[ Blueprint ]
*ExecuteBlueprint
ExecuteBlueprint 调用一个 PCGBlueprintElement 类型的蓝图,用于扩展 PCGGraph 中的节点。

[ Param ]
*GetActorProperty
GetActorProperty 用于 PCGGraph 与其他 Actor 类型之间的通信。

[ Subgraph ]
*Subgraph
PCGGraph 之间可以相互嵌套,一个 PCGGraph 可以作为另一个 PCGGraph 的子图参与流程。

[ Transform ]
*ResetTransform
ResetTransform 用于重置 Transform 属性数据。

RebuildPointRotationFromNormal 通过 Normal 朝向重新计算 Point 旋转,这可能改变原有的 Point 方向。

ApplyScaleToBounds 目前是一个无效节点。

PCG Subgraph
PCGGraph 有相互嵌套的能力,一个 PCGGraph 可以是其他 PCGGraph 的 Subgraph,为 PCGGraph 的复用和复杂 PCGGraph 的构建提供了可能性。
图间数据传递
图间数据传递通过 Input 和 Output 的 CustomPin 实现。
打开一个 PCGGraph,选择 Input 节点或者 Output 节点,可以通过添加节点上的 CustomPin 来添加自定义输入和输出。

有时我们希望从外部向 PCGGraph 内部传递数据,就像 PCGVolume 细节面板里 Setting 参数做的那样,通过更改外部的选择来控制 PCGGraph 的行为。

控制 Input 输入的内容
PCGGraph 提供创建 Parameters 用于与外部交互。打开 GraphSetting,我们可以创建 Parameters,并在外部的细节面板中设置 Parameters 的值。

PCGBlueprintElement 用于创建 PCGGraph 中的自定义节点。
PCGDataCollection
PCGDataCollection 是 PCGGraphPin 之间的数据传输类型,也就是 PCGBlueprintElement 的输入和输出。
一个 PCGDataCollection 可以包含多个 PCGTaggedData,一个 PCGTaggedData 包含端口名称(PinLabel),标签(Tags)和实际数据(PCGData)。

当我们需要从 PCGDataCollection 中读取数据时,可以使用它提供的几种函数。这些函数会根据不同的要求,将符合的 PCGTaggedData 打包为一个 PCGTaggedData 数组输出。
PinLabel 指定要获取哪个端口的数据,Tags 则为 PCGData 的分组处理提供了可能。要是有 GetTaggedParamsByPin 就更好些了。

获取 Input 数据的方式大略形式如下。

输出数据时,我们首先将带有 PinLabel 和 Tags 的 PCGTaggedData 全部放入一个数组中,然后交给 MakePCGDataCollection。

PCGGraph 有 AttributeSet 的概念,但 AttributeSet 是虚幻引擎中的一个已有类型了,所以 PCGGraph 中的 AttributeSet 实际是 PCGParamData 类型。PCGParamData 包装了一个 PCGMetaData。

PCGMetaData 是一个较为特殊的东西,它在 PCGSpatialData 中就存在,是一个单数而非数组。这似乎意味着它的元素(条目)管理与 SpatialData 中的元素管理是分离的。

仔细想一想就能理解为什么要这么麻烦。因为 MetaData 可以不依赖于 SpatialData 而存在,在 PCGGraph 中。MetaData 可以作为 PCGBlueprintElement 的输入和输出,因此需要 PCGParamData 来包装一下。

将 PCGParamData 转换为 PCGDataCollection
刚刚接触 PCGBlueprintElement 时就颇为困惑,为什么有些地方仅使用 PCGMetaData 就可以,而有些地方非要使用 PCGParamData 通过 MultableMetaData 节点转换一下。
因为如果要将 MetaData 输出,就必须使用 PCGParamData,如果只是局部使用,就可以直接使用 PCGMetaData。
下面的示例将 PCGBlueprintElement 的第一个 Input 和 Output 端口名字分别输出到名为 InputName 和 OutputName 的单一条目属性中。

当新建一个 PCGBlueprintElement 时,有一些虚函数可以被我们重写。

除了一些能够让我们改变外观的函数,还有一些事比较重要的。
Execute() / ExecuteWithContext()
Execute() 和 ExecuteWithContext() 是同一处调用,ExecuteWithContext() 默认直接调用一次 Execute()。这里是实际的执行函数,我们的逻辑入口一般都写在这里。触发函数会被自动调用,不需要再手动调用。

PointLoopBody()
PointLoopBody() 是非常常用的需要重写的函数,当使用 PointLoop 节点时,默认会执行这个函数中的逻辑。它是特殊的,会做异步处理。

下面的示例在 PCGGraph 中创建了一个值为“Hello”名为“CustomAttribute”的属性,并在名为“InputSample”的 PCGBlueprintElement 中读取 CustomAttribute,将每个 Point 的 Seed 与其合并储存在名为“Copy”的属性中输出。

PCG Blueprint
向图传递数据(蓝图)
向图传递数据可以使用 GetActorProperty 节点,搜索本文“*GetActorProperty”可以找到相关的方法。如果是在运行时改变数据,可以配合 NotifyPropertiesChangedFromBlueprint 节点使用。
从图获取数据
能够从 PCGGraph 中获取的数据是该图的 Output,可以使用 GetGeneratedGraphOutput 节点。

GetGeneratedGraphOutput 获取到的是我们的老朋友 PCGDataCollection。

示例中,我们在 PCGGraph 中创建了两个自定义输出,并分别输出了一个 PCGParamData 和一个 PCGPointData。接着将该 PCGGraph 添加到蓝图中,蓝图里执行两条路线,分别获取 PCGParamData 的数据和 PCGPointData 中的 Transform 属性。

运行关卡,打印如下数据。
