
上周受到茜熙xixi大佬的鼓励,准备开一个PCG的新坑,本系列预期以建筑生成、街区生成为主线,记录在学习PCG生成现代城市中的一些经验,希望可以帮助到刚入门PCG的朋友。
本文内容使用5.6.1版本引擎(Bug比较多,谨慎使用),以官方案例的CitySample中/Building/CH/A建筑资产作为基础素材。
本文对应的[项目GitHub](https://github.com/jiadevr/PCGDemo)使用5.6.1源码编译版本创建,需要C++编译环境。中由于库大小的限制,对CitySample资产设置了gitignore,如果需要完整浏览请再打开项目前先下载CitySample内容,Migrate"Kit_Bldg_CHA.umap"及其关联资产到项目中,将GrammarLearning内容作为子关卡挂载。
5.6.1在PCG方面仍不是非常稳定,使用过程中请多保存。
黑客帝国的大型建筑都比较复杂,如果粗略划分我们可以分成常规建筑部分和顶部穹顶建筑,两部分建筑特性如下:
常规建筑:具有统一的横截面积,楼层模块较为规整,根据楼层模块不同可以分为入口、过渡层、标准层、装饰层、屋顶、屋顶装饰。
顶部穹顶建筑:内部多次向内挤压,具有复杂的界面类型,模块复杂度远多于常规建筑。

水平结构方面,部件类型较为相似,整体近似可以视作`[转角][窗,立柱][门]*[立柱,窗,立柱][窗,立柱]*[窗][转角]`

除垂直结构外还能看到水平方面根据建筑朝向有区别,仅在正面有和特殊窗

根据两部分建筑的不同特性,本文主要考虑矩形可缩放建筑,支持任意角度的边的内容仍在施工中,并对标准建筑和穹顶建筑两部进行分别构建。
在垂直方向上,两部分均使用垂直语句元素分配完成垂直方向分割。
在水平方向上,标准建筑部分使用水平语句元素分配完成分布,穹顶建筑使用常规PCG点操作方式完成。
同时,为了尽可能多地了解5.6的新特性,本文完全以PCG图表完成,主要尝试了以下内容:
直接使用CitySample资产,不对资产进行任何形式的修改。
使用DataTable作为数据驱动方式,在PCG中读取DataTable数据完成水平机构模型、Grammar语句的配置。
标准建筑部分使用新Loop节点、PCG子图进行嵌套,完成重复层级构造。
穹顶建筑部分使用最少的Spline构建,内部挤出、圆环等使用PCG图表以参数化形式创造,测试PCG支持的最大图表量。
不使用自定义PCG蓝图、C++、HLSL自定义节点,使用自带节点纯PCG图完成。
外层使用蓝图类挂载PCG组件,并为组件传入正方向用于划定门窗。
Grammar的使用是本文的基石,这方面可以从茜熙xixi-黑客帝国模块化建筑UE_PCG教程 | B01_官方案例之语法Grammar的视频获得很好的入门,在自行实践中参考PCG插件中提供的示例关卡(\Plugins\PCG\Content\SampleContent\Grammar)
参考官方示例关卡Grammar的使用流程是:
1. 获取Spline信息
如果直接读取SplineData,在作为多层生成子图时常用Input传入,以保持更大的灵活性,这也是官方案例中的写法。

2. 添加语法属性到Spline的Data中。
使用节点AddAttribute,应当注意Spline的数据分为两部分,其中@Data指的是整个曲线的通用属性,语法、挤出高度等通用数据多写在该分类中,也更加容易读取。而不带Data的指的是ControlPoint,对每一个点进行数据写入。

3. 对曲线进行垂直方向复制。
使用节点DuplicateCross-Section,该节点需要指定以下内容:
挤出矢量(`@Data.ExtrudeVector`):对建筑来说一般是(0,0,0标高到屋顶高度)。
ModulesInfo:每层信息,这部分和上层垂直结构内容对应,指定垂直结构中每一层的楼层高度和名称,注意名称需要严格对应,Size和模型的z轴方向高度对应。
ExtraOutputAttributes垂直复制产生的标志信息会创建到ControlPoint的哪个Attribute上,这边需要注意如果目标Attribute已经存在,不会覆写,因此如果对产生的曲线再次复制,需要指定不同的Attribute名称,考虑到一致性还需要将新属性复制到旧属性实现覆盖,这部分在穹顶建筑部分还会再次提到。
GrammarAttribute:在勾选GrammarAsAttribute后会从Spline的指定名称Attribute中读取分布语法。
4. 细分Spline将语法元素和附加信息写入每个细分点
有两种选择,可以直接使用SubdivideSpline或者使用SplineToSegment把样条边转换为点再用SubdivideSegment转换成分段。总之两种方式都是将Spline根据规则切分为Point。
5.6版本在读取Spline时会有错误(曲线不闭合、控制点重叠等),此时需要在SubdivideSpline中勾选AcceptIncompleteSubdivision。其他参数配置、注意内容和DuplicateCross-Section一致。
注意其中一个不起眼的选项ForwardAttributesfromModulesInfo,这个选项可以负载到ModuleInfo的信息在细分过程中一起匹配到生成的细分点,实现Mesh信息的匹配和写入,是后文数据驱动的关键。其中Mesh使用软引用(SoftObjectPath类型)。
5. 模型缩放调整
由于在配置时部分元素支持缩放适配长度等信息,这部分和模型建模有强绑定关系,不需要过多关注,在后续单层生成时我们会详细展开。
6. 生成Mesh
使用PCGMeshSelectorByAttribute,以之前细分时写入的Mesh属性作为数据来源。

从程序分解问题的角度,固定截面的多层建筑只是单层建筑的不断重复,我们只需要在单层中处理可能的情况,然后将它们有序重复就可以实现多层建筑。因此单层建筑的制作是常规建筑构建过程中的重点。在单层建筑中我们需要首先解决以下问题:
层内容的数据驱动配置:从内容复用角度来看,我们希望每个PCG建筑都是同一类的实例,而不是每个建筑都新建一个类,因此我们需要暴露成员变量、DataTable配表等方式实现内容的动态维护。
模型配置:在黑客帝国觉醒:创造世界(官方字幕)中提到黑客帝国的资产以左下角为枢轴点,而SubdivisionSegment等节点切分默认使用的是Point的中心位置,因此在使用时需要考虑设置偏移。
边角处理:本文首先以直角常规建筑入手,Grammar分割时如果每个面都带柱,会产生模型重叠,这方面又涉及两种不同解决方案。
正侧面差异:在上节中已经提到,建筑的正立面底层带门但侧面、后面不带,我们需要选择一种方法为各建筑立面指定“门蒙版”。
本部分对应PCG图表\Content\CityExampleLearning\SingleFloor\PCG_CHA_SingleFloor.uasset
在我之前的文章数据驱动实践中提到过各种数据驱动方式的比较,在PCG中我们可以使用的工具主要是PCG图表暴露出的参数、DataTable、读取父类(和组件)的成员变量。
其中PCG暴露图表在5.6中已经提供了单独面板进行管理,使用方式和蓝图成员变量类似,使用也较为简单这里不做赘述;读取父类(和组件)的成员变量的方式在后文中正侧面差异部分介绍;本小节主要介绍DataTable在PCG图表中的使用。
在PCG中引入DataTable内容有两个常用节点LoadDataTable和DataTableRowToAttributeSet,LoadDataTable又可以通过切换OutputType切换为Point和AttributeSet,前者常用于从CSV获取外部导入的点信息,本文使用后者,即创建PCG图中匹配的结构体,然后以AttributeSet的形式传入PCGGraph。

创建自定义结构
那么既然提到了匹配的结构体,首先就要明确我们在哪里需要使用:
水平方向:SubdiveSegment-ModulesInfo:之前我们提到了水平细分过程中需要指定Subdivde节点的ModulesInfo并使用ForwardAttributesfromModulesInfo值支持Mesh信息负载,那么我们这里可以根据ModulesInfo对应的数据格式PCGSubdivisionSubModule进行自定义结构体的创建。如果不想使用DataTable,也可以直接创建该类型的数组,实现在PCGComponent中以成员变量形式配置。

根据PCGSubdivisionSubModule结构体信息,我们创建如下自定义结构体,相比原结构体,增加了对Mesh的软引用,在使用SubdiveSegment切分模块时可以作为负载一同传入,实现对点Mesh的直接指定,避免后续再使用Symbol匹配Mesh。

将每层所使用的SubModulesInfo的信息保存在DataTable中,Symbol字段和Grammar中使用的元素一一对应。

将DataTable数据传入PCGGraph
数据载入方面,常见的有单个细分元素读取和全部元素读取两种情况:
需要生成单个元素时使用DataTableRollToAttributeSet,读取给定的Row(这边建议RowName和Symbol保持一致,再后续查找时更便捷),其他节点按照常规方法读取输出的AttributeSet即可。

传入SubdivideSegment时我们一般选择LoadDataTable,以AttributeSet形式输出、加工后作为ModulesInfo的传入参数(务必勾选ForwardAttributesfromModulesInfo),此时细分点在添加Symbol之外也会带有配置的Mesh信息。下图黄色框内为SubdivideSegment输出:

当我们把每层所用到的所有构件填入DataTable并配置了DataTable读取之后,我们就可以脱离PCG图表对引用的模型相关数据进行维护,实现数据和逻辑的解耦,在后续复用、多人协作版本控制方面发挥更大价值。
城市案例中的中模型枢轴点(PovitPoint)均在模型左下角、模型主方向为Y轴正方向,但节点生成的点默认枢轴点是底边中心,如果直接使用资产会出现布点方向错误、模型旋转方向错误的问题,如下图所示(红色Debug点为细分产生布点位置)。

因此我们需要在PCG图表中对生成点进行调整,参考Grammar关卡中的内容主要需要调整的内容包括:
模型旋转:使用Y轴为正方向的模型旋转90°(正负和曲线方向有关,对应SplineDirection节点)。
Scale调整:细分节点在读取到元素Scalable参数为ture后,会设置Scale.X参数进行长度适配,但我们的模型是Y方向为正方向,需要将Scale.X和Scale.Y交换。
枢轴点偏移:因为枢轴点位于下角落,对应的生成点应该向生成点更偏向起点的方向移动半个模型长度。
整体外推:这部分和下一小节边角处理有关,是为了避免边角模型重叠,将模型沿垂直样条线的发你想移动一个壁厚距离。
每一步骤执行后效果如下所示:

下面我们分别讲解每一步的操作方法。
模型旋转
PCG中的Rotation内部已经进行了标准化操作,我们不能像蓝图、CPP中直接设置某一个轴的数值(可能会直接触发断言导致编辑器关闭),常见的直接旋转节点有以下两种:
RotateAroundAxis:这是一个对向量旋转的节点,必须是向量,不能直接作用于Rotation,我们如果需要使用这个节点需要先拿到正方向、旋转正方向再将正方向转化为Rotation,可以说非常麻烦。同时该节点还有一个设计缺陷,当你首先连接了In时,其他三个wildcard节点会变为和In一样的数据类型,但Angle只支持一维数值类型,需要在上面右键ConvertToDouble才能正常使用,否则会一直报错,如下图所示:

TransformRotation:对旋转应用一个矩阵变换,正如其名称,这个节点直接影响旋转值,不需要进行其他转换,这个矩阵变换也非常简单,我们只需要在Transform中填入我们需要的旋转角度、设置InputSource1的操作值为点的`$Rotation`属性即可,如下图所示:

Scale调整和枢轴点偏移
由于我们只希望模型在单一维度上缩放,其他两边保持不变,所以Scale的调整非常简单,我们直接先调用AttributeCopy节点配置InputSource和OutputTarget,将`$Scale.X`拷贝到`$Scale.Y`,然后调用AttributeSet设置`$Scale.X=1.0`即可;但考虑到我们还需要设置枢轴点偏移,可缩放物体的偏移值和缩放值有关,因此我们还需要对偏移值进行Scale。
为了实现这一目的,我们首先需要把不缩放情况下偏移的值逐个取出来,在DataTable中我们记录了每一个模块的延伸方向长度(Size),这个值也是Modules排布的基础。对这种随点属性不同的值,我们将其作为传给SubdivisionSegment的AttributeSet的一个元素,利用其能力直接将其写入每个点属性中,名称为PovitOffset。

设定了偏移量之后我们还需要考虑到一个问题,偏移量作为一个矢量(左下角指向中点)那么点的旋转、缩放都需要应用,同时还需要将其应用在原始的位置上,实现整体适配,这次我们使用VectorTransform完成这一操作,但在对输入输出结果Debug时,无论放在Scale调整前后,只有Rotation的值应用到了PovitOffset。但没关系,它已经把最麻烦的工作做好了,剩下的我们可以手动完成。
对PovitOffset应用Scale之后,下一步我们再用Position减去这个值,得到新的位置,完成整个调整过程,此时我们已经获得了整齐排布的外墙,全程操作节点图如下:

整体外推
这部分和边角处理有很大的关系,我们放在下一节一起聊。
对于我们创建的直角建筑,如果在Grammar中指定每个边都放置转角立柱,那么角落位置会出现两个立柱模型,对于这种情况我们有两种解决办法:
把已经分开的分段再用Merge/Union合并到一起,应用SelfPurning删除重复点。合并后再次对每一边操作需要AttributePartition拆开处理。
直接变换思路,我们的目的是在角落放置立柱,角落的特点是Spline控制点所在位置,我们可以不在边上创建立柱,转而获取SplineControlPoint,单独在这个位置生成转角模型。这样的好处是保留了边的独立性,但在处理非直角转角时还需要做额外处理。
既然本文处理的都是直角转角,那我们不妨试试第二种,此时我们不在边语法中设置转角C,让剩余内容填充立面,然后在控制点生成转角点。此时由于相邻立面都是直接应用的语法,并没有给立柱空出距离。此时就需要将墙面整体向朝向方向偏移,空出转角位置,便宜的距离取决于转角Mesh尺寸,我们在DataTable中已经进行了配置,依然可以走数据驱动,那么我们只需要分别完成下面两部分即可:
转角立柱生成:生成逻辑和校准立面模块逻辑大致相同,其中参数和模型默认朝向有直接关系,不具备通用性,建议直接以90°的倍数试值。图中的CornerMeshInfo是NamedRerouteDecarationNode,在一处输出作为多出输入时,可以用类似局部变量的方式调用,简化复杂的连线。

墙壁向前避让:墙壁避让方面由于之前已经完成了点的旋转,里面上的点Y方向均与SPline重合,元素朝向方向为X方向,因此只需要按照之前的PovitOffset同样的方式,向X正方向偏移一个转角部件的Size再设置到`$Position`即可。

效果如下所示

在水平结构分析中我们可以看到正立面和其他立面在门窗布置上有所不同,体现为入口B层正立面有门,过渡层T0正立面有跨层窗。对于这种特殊立面我们可以通过蓝图Owner传入设置参数-PCGGraph读取-计算蒙版选择特殊Grammar实现。
在项目中提供了BP_BuildingWithDir蓝图,该蓝图在构造脚本中以ForwardVector驱动ArrowComponent的朝向同时作为传递。需要注意无论是PCGGraph还是蓝图都不能拿到ArrowComponent的Rotation数据,所以不能直接以ArrowComponent作为方向调整工具。

随后我们在PCG图表中使用GetActorProperty,获取ForwardVector,此时我们就拿到了蓝图传入的参数。下面我们可以使用类似材质中蒙版计算的方式利用点乘叉乘方式把该面所在的SplineSegment进行调整,具体来说分为几步:
计算边元素正方向矢量:我们已知SplineSegment可以获得位于相邻ControlPoint中点处、和两点距离等长的点信息。那么我们配合ControlPoint作为起点就可以获得“边走向”矢量,即下图中的箭头方向,世界正上方向和这个矢量叉乘获得垂直于世界正上方向和该向量组成平面的向量(这里我认为根据右手定则计算方向应该和红色ArrowComponent指示的向量相反,但实际Debug方向与红色向量同向......)。
通过点乘为朝向划分蒙版:将叉乘的计算结果再与外部传入的ArrowComponent正方向矢量点乘,获得`[-1,1]`范围的一维结果,因为我们这里需要对结果二值化,可以直接Clamp成`[0,1]`。如下图中Debug箭头的着色情况。在最简单的情况下我们已经获得了需要设置为特殊语法的面。

处理其他情况:如果我们仅匹配1,可能会导致斜边被漏掉,致使整栋楼没有带门的面,因此我们还需要一个`(0,1)`范围的阈值筛选面,这里挑选了0.85作为阈值。更进一步,我们更保险一点,我们设置即使没有点满足阈值时Index=0的面一定带门。

至此我们已经完成了特殊语法的写入,输出结果汇入前文的模型配置部分进行模块划分、朝向和枢轴点划分步骤,呈现效果如下图所示:

单层常规建筑部分的PCG图如下

已经有了单层建筑后,截面固定的多层常规建筑只需要在此基础上进行重复生成和模块替换。如同本文“Grammar的使用“一节,增加挤出矢量、垂直语句后使用DuplicateCross-Section创建带有Symbol标记的多条样条线,我们还需要解决以下几个重要问题:
多层建筑生成,根据DuplicateCross-Section生成的多样条线复用单层内容生成多层建筑。
配置多层数据驱动,让每一层的模型能自动化匹配。
跨层模块的配置。
生成简单的示意性屋顶。
本节内容对应的主PCGGraph为\Content\CityExampleLearning\MultiFloor\PCG_CHA_MultiFloor.uasset。
首先我们需要对单层生成进行一处改造和检查,让其支持多Spline输入而不是直接读取。在“Grammar的使用“我们提到了使用Input和GetSplineData两种方式获取Spline信息,以SubGraph形式把单层生成图嵌入多层生成逻辑时,推荐使用Input方式增加泛用性。
官方案例中这种这种方法是为了将PCG_BuildingSample.uasset(位于\Engine\Plugins\PCG\Content\SampleContent\Grammar\Graphs\)在PCG_PrimitiveCrossSection_Grammar.uasset(位于\Engine\Plugins\PCGInterops\PCGGeometryScriptInterop\Content\SampleContent\Grammar\)中嵌套使用。

随后我们检查一下DataTable、正侧面Grammar语法都已经被配置为GraphParameters,如下图所示:

此时我们把它作为SubGraph时就可以看到这些属性可以被覆盖(PCG_CHA_BaseFloor与上节单层生成内容为复制后改名,主体逻辑一致)

下一步我们需要把生成的多个样条以同样的方式生成,考虑到垂直结构具有多层,每层的配置不同,我们使用Loop节点进行嵌套,在Loop的执行体内匹配使用的该层的模型信息,然后调用单层子图生成。
Loop节点使用
根据前文所述我们,需要使用Loop节点为DuplicateCross-Section生成的多样条线分配不同的子模块信息并调用单层生成逻辑实现多层建筑生成。Loop节点的基础使用可以在ContentExamples官方示例工程的PCG_Maths.uasset和PCG_Loop_exponentiallyScale.uasset中看到。
在创建Loop节点后,需要向内填充用于循环的Graph,和官方案例不同,我们并不需要它返回什么数据,因此我们可以直接以下图中的_Default_Loop作为模板创建,这个蓝图会作为循环体多次执行,每次传入一条由DuplicateCross-Section分割的样条线,配合Symbol属性进行划分我们可以实现对样条线元素的准确控制。

循环体内的大体框架如下,我们可以从ControlPoint中取得Index=0的控制点中Symbol属性,然后根据Symbol筛选出记录模型信息的DataTable和Grammar,这样我们就可以实现每层模型和语法的分别控制。

循环体的核心在于如何正确匹配出对应的内容,这里我们有两个选择,分别是Switch节点和DataTable,Switch支持FString类型的分支切换但其内容固定,每次修改需要进入节点再进行编辑,对于我们拓展来说比较复杂,这部分会在穹顶建筑构建时展开。在本节中依然使用DataTable作为数据驱动的方式,将在下一小节详细展开。
上一小节中我们已经拿到了Symbol属性,根据这个属性DataTableRowToAttributeSet同样可以视作一种自动化配置的Switch节点。参考我们在水平方向指定了DataTable进行每一层元素的配置,我们这次可以在垂直方向用一个DataTable管理不同层的DataTable引用,使用和Symbol匹配的RowName进行对应索引。
根据上面的思路我们可以创建如下结构体,并依照结构体创建DataTable,DataTable中的RowName和DuplicateCross-Section中的ModulesInfo结构体中的Symbol保持对应。分别设置对每个楼层ModulesInfo信息DataTable的软引用、楼层正面的语法、楼层侧面和背面的语法。依次填入内容后,我们将其作为切换逻辑连入上一节的图表中。

除了上述内容,这里还将DataTable配置为输入参数,支持在其他节点中调用。在从AttributeSet中选择单个属性方面,目前没有找到更适宜的方法,这里使用DeleteAttributes并配置为KeepSelectedAttributes进行属性选择。

调用循环处如下(\Content\CityExampleLearning\MultiFloor\PCG_CHA_MultiFloor.uasset)

完成以上配置之后,我们再调整多层生成时就可以在PCG组件公开出的变量中配置Modules和垂直语法,在垂直DataTable中管理每一层的DataTable引用和语法,如果需要切换每一层的配置则对应转化到层DataTable中,实现整体的数据驱动控制。
完成整体配置之后我们开始处理比较复杂的跨层结构,即过渡层T0的跨层拱门状窗:

对于这种结构我们有增、删两种思路:其一是将其作为两层的一个整体,也就是说整个过渡层T0其实是一个高3m+3m的单层结构,我们拿到样条后将高度为3m的三页窗向上复制一份形成这样的结构;其二是将其作为特殊双层结构,使用点控制把上部跨层窗的模型生成点删除。
为了便于后期调整创建使用同样素材的不跨层结构或者多个连续跨层结构,这里选择了第二种也就是删除的思路。在具体操作上我们进行如下步骤:
筛选出有跨层结构标记的样条线。
统计样条线数目,当带有T0标记的样条线总数为偶数时就每两层生成跨层结构,当带有T0标记的样条线总数为奇数时,先尝试划分为偶数+1,对偶数生成跨层结构,奇数生成常规排列。
删除跨层造成的重复点。
同时为了保持逻辑之间的独立性,我们可以在常规楼层生成中设计过滤逻辑,让跨层T0和以后有相同设计的其他楼层不参与常规生成,并以单独分支存在,方便后期进行动态维护。
为了实现筛选我们可以在DuplicateCross-Section输出之后再新建一个分支,维护一个集合SpecialVModuleName记录跨层模块Symbol。然后分两步进行判断:首先检测生成Modules包含我们需要设置为跨层的内容,当包含时才进行后一步判断,这样当我们不需要跨层时仅需要清除SpecialVModuleName,即可避免后续运算。完成第一步检测后,再筛选传入的所有样条中包含SpecialVModuleName中Symbol标记的样条,把他们放入我们跨层专用的循环中。修改完成的全图如下,下面我依次介绍。

单层部分我们已经做了很好的模块化处理,可以很轻松的复用到生成逻辑中,同样创建Loop节点,输入外部汇总来的总层数、样条垂直分布配置DataTable和跨层名称,内部逻辑加入孤立奇数层判断、删除点即可。

随后我们还需要返回到常规生成逻辑中,把跨层的部分筛选去除,对原始逻辑进行如下更改:加入外部输入参数JumpModuleNames表示哪些样条跳过,对应传入SpecialVModuleName。在内部逻辑中使用Switch控制样条线是否传入子图表。这里要特别注意对于AttributeSet传出地参数,使用GetEntriesCount才能获取单个Collection中的全部属性条数,使用DataCount返回的是Collection数量。以及在我们的子图表中当传入数据为0时会切换成采样曲线,所以Select节点应该放在样条输出之后。

完成跨层设置后我们已经可以随心所欲地配置跨层内容、不会和原模型重叠。

屋顶方面由于在后续文章还会继续探讨使用程序化生成的方式创建,本文仅做示意性脚手架级生成。
在PCG_BuildingSample中的Debug内容中有一个CreateSurfaceFromSpline节点,可以根据Spline生成一个可以采样的Surface。对Surface采样之后就可以获取生成屋顶的大致点。
依照这个方法最关键的部分是筛选出合适的Spline,我们还可以按照跨层建筑、角落立柱的方式根据Symbol进行筛选,通过FilterDataByAttribute筛选出代表屋顶的样条线(Symbol=E)依此生成即可。同时,该节点支持对String使用Substring方式进行匹配,在后续顶部穹顶建筑生成时可以进行多样条匹配。

至此我们完成了常规建筑多层的生成。

相比常规建筑具有几乎一致的截面形状和大小,顶部穹顶建筑的生成更加复杂,具有多种收缩、变形结构。出于学习更多节点的目的,本文在后续章节尝试将顶部建筑作为一个PCGActor对象构建,在编写逻辑时可能并非出于最佳实践,希望有更好方法的朋友多提优化意见。
言归正传,我们开始分析顶部建筑。从之前垂直结构的分析上来看,顶部数目并不多余底部。从水平结构分析来看,顶部没有明显的方向特性,四边的语法内容相同。从下面这张图中我们可以看到,在矩形边缘部分,建筑分为变截面的三个部分:底部基座、标准层、顶部基座。这三部分分别来看和底部建筑构建方法类似,这意味着我们还可以继续复用我们的Loop节点和单层子图的组合,依照先前的逻辑就可以完成每部分的单独构建,因此其主要难点在于以下方面:
参数化平台内壁收缩,我希望以一个PCGActor对象制作该部分,那么DuplicateCross-Section垂直细分出的Spline就需要考虑进行变形控制,使得下图箭头所指的位置能形成正确的阶梯分布
穹顶建筑圆环细分生成,DuplicateCross-Section生成的垂直曲线组的默认形状是相同的,在指定的Spline形状为矩形时,我们需要在顶部生成圆形的分布点。

分析完毕,下面我们开始!
参数化内壁收缩在本文中指的是给定一个收缩矢量,在不创建新的SplineComponent的情况下控制上小节中三部分的生成轮廓。如果按照常规建筑的生成方式,我们首先给出总建筑高度,然后再划分每个SubModules生成整个一体的SplineArray,会导致后期需要控制的样条数目太多。因此本小节采用分段方式,即每次DuplicateCross-Section只创建等截面的一段,然后提取末端Spline进行收缩,并再次调用DuplicateCross-Section创建。这样的好处在于阶段划分清晰,便于控制,同时也遇到了之前提到过的DuplicateCross-Section的输出每次需要指定不同名称这小坑。
下面我们简略看一下具体如何实现:
最初始的1部分,我们直接读取样条信息,不需要进行缩放,采用和多层建筑部分同样的逻辑即可:

随后在生成屋顶的部分,我们除了将Spline转成采样平面,继续利用这条样条线为基础进行收缩,这边利用了最后一层的高度同时是屋顶、栏杆底部、下一级建筑底部的特性。图中红色框线的部分是之前多层常规建筑没有的部分。

前方红框中使用了自定义节点PCG_GetShrinkRectangleSpline,这个节点的主要内容是通过传入的四个SplineControlPoint找到中点指向四个顶点的矢量提供基础收缩方向,ShrinkValue指定具体的收缩矢量,对SplineControlPoint进行偏移。在计算矢量时需要以控制点为对象,但传出参数仍需要为Spline,所以在计算完控制点之后将控制点的`$Position`拷贝回Spline,是本文对Spline操作的主要逻辑。

后方红框中圈住了DuplicateCross-Section和后续的CopyAttribute,这是为了避免之前多次提到的DuplicateCross-Section的输出每次需要指定不同名称,本文通过每次为DuplicateCross-Section的输出参数指定不同名称属性,然后再将其拷贝回原始属性位置实现。
随着重复两次上述操作,矩形部分的建筑就已经生成完成了。
对圆形穹顶部分,我们依然可以提取末端的Spine进行收缩操作,但随后需要根据收缩后的矩形获得一个圆形的点排布。整体逻辑如下:

其中PCG_GetInCircle和PCG_DomeBuilding为这个环节中新出现的自定义节点,我们下面分别展开:
PCG_GetInCircle的目的就是获取圆形排布的点,其核心节点是CreatePointsSphere,从名字上来看这节点也提供类似CreatePointGrid等一致的标准图形点创建功能,使用这个节点可以避免手动计算Tangent获取标准化图形。使用这个节点时有两个注意事项,首先是输入Origin和Radius的参数在输入前必须设置成完全匹配的名称;其次5.6版本在调整该节点的SphereGeneration时不建议使用Segment模型,容易触发断言、编辑器退出,建议换算成角度以Angle模式输入。该自定义节点内容如下:

完成圆生成后再次一模块高度进行细分,由于整体内容固定,使用节点也较为常规这边直接展示PCG_DomeBuilding的内容。其中需要额外注意间隔立柱、立柱顶部塔尖装饰、顶部石墩等内容均需要使用选择点、反向收缩等功能实现。

目前PCG的Debug工具正逐步完善,但仍有一些刚刚上手的朋友不太清楚的点,这里初步分享一些我遇到的注意事项:
需要在DebugObject菜单中选中需要Debug的对象,再看Attribute信息。
对于子图表、GraphInstance在Debug时需要打开原始Graph,在下面选择对应。
循环体中会有多个输入的图表,需要选中对应的循环Index进行查看。
Switch节点在Debug过程中会有灰显和正常显示的区别,灰显的内容一般是当前循环、选择没有执行对应的节点。
本文对黑客帝国中Kit_Bldg_CHA关卡的建筑使用PCG节点进行了复刻,在复刻过程中也让我对PCG子的的各种图表有了更深的认识。预计后边的文章会继续围绕使用纯UEPCG复刻黑客帝国案例生成现代城市展开。由于想做的内容还有很多,规划的时间比较紧张文章的准备和写作都有些仓促,如有不足请各位朋友不吝指正。也希望本文能对正在学习的朋友有一些帮助。