Houdini技术体系 基础管线(四) :Houdini驱动的UE4植被系统 上篇
背景
之前在《Houdini技术体系 过程化地形系统(一):Far Cry5的植被系统分析》一文中已经对AAA游戏中过程化植被的需求有了一定的定义,后续工作就是如何用Houdini开发功能需求定义的节点,以及对应UE4的Houdiin Engine来制定过程化管线。Houdini的HDA的开发放在过程化地形系统部分讲解,这里主要是讲解工作流程的制定。FC5的分析之前,也大致介绍了UE4的植被系统。这里再确定下植被系统工作流方面的需求:
- Houdini Input:UE4 要输入什么给Houdini
- 所选择Landscape Component部分的地形信息
- 美术提供的选择区域,可以是绘制的Mask,也可以是Curve的选框
- 与HDA中对应的植被资源参数,简单的可以用Houdini Engine ,完善一些的话最好是一个json或xml的描述文件,或者UE4的DataTabel。
- 除了HeightMap外的Mask信息,还有峭壁,湖泊,电线杆,栅栏等不能摆放植被的区域Mask
- Houdini Ouptut:Houdini要输出什么给UE4
- Entity Point Cloud:每个点包含了对应的植被实体,以及实体的Positon,Rotation,Scale等Transform信息
- Terrain Data:例如树根对地形隆起的变化,树根部分对地表材质Mask的影响,地表的颜色等
- Mask Data:草体布局信息的Mask贴图
- UE4植被系统的支持
- Houdini Instance与UE4 Foliage Type的Instance的对应
- Houdini Biome Mask与UE4 Grass Type的对应
- Houdini闭环修改与UE4 Foliage System的对应。
那么,这里就以实现这些需求为目标,介绍下实现基于Houdini的UE4植被系统的基础管线所要注意的事项。
Houdini的Input设置
在之前FC5的植被系统的需求分析中也可以得知,最新的AAA游戏中,一平方公里场景里就有60w左右的植被实体,而最近的一些UE4大世界游戏和手游,也通常在6x6~8x8km左右。在策划和美术的迭代开发中,每次都要生成整个场景的植被再看效果的话,会极大的影响开发效率,对大团队的多人协作开发也很不友好。所以这里也要像之前的地形管线一样,以FC5的过程化系统为原型,可以支持美术或策划来选择和绘制生成区域,让Houdini的过程化生成只影响这一部分选择区,这样不但可以利用UE4的WorldComposition的功能多人工作,也可以通过UE4自带的绘制Selection Region Tool,让美术更进一步的控制过程化的影响区域,减少生成时间,提升迭代效率。
如下图所示,FC5的植被系统,支持类似UE4的Component和Paint Region来作为Terrain Data,
在上文中,也总结到UE4里FC5植被系统的Input,有以下几项:
- 所选择Landscape Component部分的地形信息
- 美术提供的选择区域,可以是绘制的Mask,也可以是Curve的选框
- 与HDA中对应的植被资源参数,简单的可以用Houdini Engine ,完善一些的话最好是一个json或xml的描述文件,或者UE4的DataTabel。
- 除了HeightMap外的Mask信息,还有峭壁,湖泊,电线杆,栅栏等不能摆放植被的区域Mask
关于如何确定Input Terrain Data,Landscape Component的选择在地形管线部分已经几次讲到了。幸运的是,UE4除了Component Select Tool之外,在Terrain Sculpt Tool里提供Region Selection Tool的功能,这个要比Component Select Tool更加灵活和便捷。但和Component Select Tool一样,这个功能对原生的Houdini Engine并不适用...
如上图所示,Region Selection Tool是Terrain Sculpt Tool里的功能,不过可以借用这个功能来作为Input的Mask Paint来使用。接下来看下怎么扩展Houdini Engine把这个Mask作为Input传到Houdini里去。
Region Selection Tool绘制的Mask的方式,当使用者绘制时,会在FLandscapeToolStrokeMask::Apply函数里根据笔触和权重值,把绘制值添加到class ULandscapeInfo的TMap<FIntPoint,float> SelectedRegion 里的,只有绘制过的区域才会保存在SelectedRegion里,其中FIntPoint代表的是在Landscape里的XY位置信息,作为整型保存,而float为绘制Mask的权重。
拿到了SelectedRegion后,就是要在Houdini Engine里把它作为Mask,输入到HDA中进行处理。Houdini Engine是在FHoudiniLandscapeUtils::CreateHeightfieldFromLandscapeComponent函数里对Height Data和Mask Data进行Input打包的,这里选择在这个函数里加入SelectedRegion的Mask的打包工作。
首先,是根据Houdini里一个Landscape Component的大小MaxX x MaxY,创建出对应的SelectedRegion大小的Mask数组。在LandscapeInfo的SelectedRegion里查找每个点的信息,如果有就复制到对应位置,没有则设置为0。这样,提供给Houdini使用的SelectedRegionData就完成了。
TArray<float> SelectedRegionData; for (int32 X = MinX; X <= MaxX; X++) { for (int32 Y = MinY; Y <= MaxY; Y++) { float RegionSelect = LandscapeInfo->SelectedRegion.FindRef(FIntPoint(X, Y)); SelectedRegionData.Add(RegionSelect); } }
接下里,跟LayerMask同样的方式,通过C++代码创建一个名为SelectedRegion的Mask节点,并跟其他的Volume Merge到一起。
FString LayerName = "SelectedRegion"; HAPI_NodeId LayerVolumeNodeId = -1; if (!CreateVolumeInputNode(LayerVolumeNodeId, LayerName, ParentId)) return false; HAPI_PartId CurrentPartId = 0; if (!SetHeighfieldData(LayerVolumeNodeId, CurrentPartId, SelectedRegionData, SelectedRegionLayerVolumeInfo, LayerName, ComponentIndex)) return false; if (!CommitVolumeInputNode(LayerVolumeNodeId, InputMergeNodeId, MergeInputIndex)) return false; MergeInputIndex++;
另外,ULandscapeInfo提供了把绘制的SelectedRegion转为Selected Component的功能,这样绘制过过程化的影响区域后,就不用再选择一次Landscpe Component了。这个修改也很简单,在FHoudiniEngineUtils::HapiCreateInputNodeForLandscape函数里,当没有selected components时,就把绘制的区域转换成SelectedComponents。
if ( LandscapeInfo ) { // Get the currently selected components SelectedComponents = LandscapeInfo->GetSelectedComponents(); // 如果没有selected components,则从绘制区域获取selected components if (SelectedComponents.Num() == 0) SelectedComponents = LandscapeInfo->GetSelectedRegionComponents(); }
把名为“SelectedRegion”的Mask作为Input输入到HDA后,需要在HDA里对应这个Mask Layer来识别。在HDA的Heightfield Noise节点里,把SelectedRegion作为Mask Layer来使用
这样HeightField只有在有Mask的部分会有Noise的效果,这个同样也可以用在植被的Entity Point Cloud的生成上。
下图的效果,就是在绘制的'X'的区域内,对9个Landscape Component产生噪声变化。
绘制选区通常是控制比较大的区域,而如果是要生成小范围的区域,建议像下图这样用Curve Input来控制区域了。
如何创建一个Curve Input的方法,在Houdini技术体系 基础管线(三) :UE4以选择区域的方式对地形做生成和更新 上篇 和官方文档 https://www.sidefx.com/docs/unreal/_curves.html里都有详细介绍,也是有创建SOP或添加Operate Path两种方法。这里就不多做叙述了。
除了HeightData选区外,Input还需要有摆放的UE4植被列表(Biomes List),植被列表通常是用xml,json,或者ue4的datatable来记录每种植被在HDA节点里属性以及在UE4中使用资源的对应关系。然后在HDA里通过Python脚本来加载读取,即便是比下图FC5用例更复杂的HDA节点串联和配置,也可以借助Python自动化,完全摆脱人力基于配置文件自动化的创建和连接。Houdini的Python脚本使用在后续的基础管线部分讲解,本节出于篇幅关系,使用Geometry Input作为简化版的Biome Input。
而类似各种像湖泊,峭壁,电线杆等的Mask过程化生成的Mask,这里假设你已经通过其他方式导出了Mask图,或者准备在Houdini里通过Mask By Feature或Mask By Object来生成。在管线部分也就不浪费篇幅了,会在之后具体的地形篇的植被制作部分再做详细介绍。
HDA的制作以及Ouput的对应
FC5的HDA的Output分为两大类
- Entity Point Cloud
- Terrain Data
基于Input生成Entity Point Cloud的原理非常简单,在去年的在 Houdini HIVE at SIGGRAPH 2017上,Procedural Scattering in Houdini Engine and Unity 的Talk上介绍的就涵盖了全部的基础知识。就如下图所示,根据Terrain,Curve以及资源实体的Inptu,按一定规则在引擎里进行过程化摆放。
视频链接:https://vimeo.com/228231127 ,对应hda的下载地址:https://www.dropbox.com/s/iv840ldw5tn4lw1/scatter_tool.hda?dl=0 。虽然跟FC5的实现相比还有不小的差距,在管线篇中作为基础参考绰绰有余,有兴趣的可以下载来看看。这里也借用它简单介绍下使用的相关节点和功能。
下图中,Setup中的三个Input,分别对应的Terrain Data,Curve Select和Biome Instance。
这里先创建个临时的Terrain和Curve来做测试使用。
结果就是在Curve选择范围的Terrain上,随机Scatter了一定数量的Entity,这里用红色的Box作为代理体显示。
这里参数不变,把scatter换成heightfield scatter
这样,只有绘制了Mask的红色区域才会被放置Entity。
但是示例的这个Scatter Tool也有不适用的地方。首先就是把物体赋予到Entity Point Cloud的Copy节点,并不能对应UE4的Instanced Mesh,这样Output到UE4里的话,每个树木实体都会创建为一个Static mesh,会让过程化生成和最后运行的效率都变得非常差。最好映照按照官方文档的建议,用支持Instance的Copy to Point节点替换掉Copy Stamp节点。
这里根据地形坡度生成mask,然后把Mask转为Point Cloud来摆放树木,来比较Copy to Point节点和Copy Stamp节点的区别。
这里使用heightfield_maskbyfeature,生成Slope为20~70范围的Mask。在一块小的地形上生成100颗树来对比下生成效果。
首先是用Copy Stamp节点,选择好地形和植被实体后,经过20~30秒的卡顿后,有90多个植被生成出来。这个生成的时间和植被数量比,使用体验很难让场景美术接受。
不论是Bake成BP还是Actor,所有的树都会被Batch成一个Static Mesh,这样对图形程序做植被渲染优化也非常不友好
和Copy Stamp那20多秒的处理时间相比。。替换为Copy to Point节点,在选择完Input的植被实体和地形后的瞬间(1秒内),几乎没有感觉到任何延迟的,就完成了100颗树木实例的摆放工作。
把植被Bake成一个BP后,可以看到所有的树木被Bake到一个Instanced Static Mesh组件里,而每颗树作为一个Instance保存。也正是因为它采用的Instance的方式,才会有那么快捷的生成速度。这个效率对场景美术迭代来说足够了,但InstancedStaticMesh并不支持LOD模式,用来批量放置的植被实体原有的LOD信息也丢失了。这里应该是创建HierarchicalInstancedStaticMeshComponent才能支持LOD。
如何生成HierarchicalInstancedStaticMeshComponent的Actor或BP,可以参考 void UHoudiniAssetInstanceInputField::AddInstanceComponent( int32 VariationIdx )的函数。当Static Mesh有多个LOD时,Houdini Engine会用HierarchicalInstancedStaticMeshComponent替换Instanced Static Mesh
UInstancedStaticMeshComponent * InstancedStaticMeshComponent = nullptr; if ( StaticMesh->GetNumLODs() > 1 ) { // If the mesh has LODs, use Hierarchical ISMC InstancedStaticMeshComponent = NewObject< UHierarchicalInstancedStaticMeshComponent >( RootComp->GetOwner(), UHierarchicalInstancedStaticMeshComponent::StaticClass(), NAME_None, RF_Transactional); } else { // If the mesh doesnt have LOD, we can use a regular ISMC InstancedStaticMeshComponent = NewObject< UInstancedStaticMeshComponent >( RootComp->GetOwner(),UInstancedStaticMeshComponent::StaticClass(), NAME_None, RF_Transactional ); }
这里保证树的实体有LOD,并且在HDA Input 勾选Export LODs
再次Bake,可以看到是HierarchicalInstancedStaticMeshComponent,每个Instance也有之前的LOD信息了。
姑且算是支持了最基础的Entity Cloud Point的Output,但距离实际项目需求还很遥远,就像Input需要支持Biomes List配置表的读取一样,Output出于维护和调试的考虑,最好也是能支持Biomes List的输出,而且,虽然提供给场景美术绘制影响区域的功能,但BP的保存方式,对Houdini的闭环迭代也并不足够的友好,当美术需要对一些细小地区做频繁迭代时,新生成的植被如何去替换BP里已经生成植被也是个问题,解决方法只能整个BP对应区域重新生成一次。这需要美术在设计初期就对植被布局策略和种类考虑清楚。还有就是UE4除了Foliage Type外,还有Grass Type的植被的支持。这些会放在之后如何UE4植被系统整合的部分来讲解。
FC5的植被系统除了Entity Cloud Point之外,还有以下的Terrain Data的Output。
其中Terrain HeightMap和Forest Mask,都可以通过之前地形管线中介绍的方法传递给UE4,而Texture ID,因为FC5的地形渲染有TextureArray支持,而UE4则受限于每个Component4个Landscape Layer的限制,所以Layer与Texture的对应又是在Mateiral里绑定的,需要修改UE4引擎源码才能支持,但这就超出Houdini技术体系范畴了。这里介绍一个临时的折中办法,通过直接去修改约定好的对应的Layer的Mask权重的方式,来实现UE4里要根据输出的Houdini Output的去改变Layer的Texture ID的需求。
首先,用RegionTool绘制一块区域来生成植被
这里就只简单的根据绘制的Mask部分Scatter生成Point Cloud,效果如下
接下来实现一下Terrain Data的Output的功能,不论是Terrain Texture还是Terrain Deformation的输出原理上都是在植被一定范围内生成Mask,再把这个Mask转化为Layer Data或者Height Data,传递给UE4。
出于跑通管线的目的,这里在HDA里做一个简单的实现。在houdini里根据Scatter生成的Point Cloud,使用Sphere来生成出Mask,再根据这个Mask来提升地形高度,并把Mask输出位树根部所对应的Landscape Material Layer的值。
在测试场景上选择一个生成区域
有摆放植被的周围地形改为了另外一种Layer。
树根附近的地表高度也被提升了。和FC5那种按照树根形状来提升高度的效果相比还是有差距。
总结
上篇中,简单的介绍了如何在UE4里实现类似FC5植被系统的管线,但还有问题等待解决
- 虽然植被实体可以被Bake为HierarchicalInstancedStaticMeshComponent或InstancedStaticMeshComponent来近似UE4Foliage Type的渲染方式,但并不支持GrassType的类型。
- Point Cloud生成的植被只能Bake为场景中的Actor或BP而不是Foliage Type,并不能与UE4的Foliage System融合,在迭代和修改中增加了额外的负担。
- 示例中的HDA只支持1种植被,生成策略也非常简单,而且也不支持BiomeList的读取,和FC5有相当大的差距。
对于前两点,会在Houdini驱动的UE4植被系统 下篇中有进一步解决方案的介绍,只有整条UE4的植被基础管线完成后,才能把鱼FC5近似的Houdini功能集成到UE4里,这部分HDA的制作会在地形系统篇中进行具体讲解。