背景
前一节里,解决了Houdini地形无缝导入到UE4的流程问题。但这种方法也有它的局限性,在实际游戏项目里,LA和LD还是偏向在游戏引擎编辑器里工作,他们的一些设计也会影响到地形的信息,那么就需要Houdini对已经导入UE4中并Bake成Landscape的地形资源做二次修改。通常会选择两种方案:
- 方案一:把整个地形和建筑都导回到Houdini里,重新过程化和调整生成后,再全部导入回UE4做处理。
- 方案二:使用HDA节点的Input和Output,通过调用Houdini Engine API,直接在UE4里完成调用Houdini过程化节点对地形做修改。
这里方案一不但要求美术和策划对Houdini有一定了解,而且因为Houdini里和引擎的渲染效果不一致。可能还需要导入到UE4里才能看到最终效果,大地形还要做WorldCompositon和LandscapeStreamingProxy的生成。除了地形以外的的场景和建筑部分,还会和GamePlay以及优化显示逻辑相关,通常会包装成BP或Prefab的形式,这些东西要导入Houdini再导回也不仅仅是资源处理的工作。这么看方法一是非常费时费力的方法。这也导致了国内一部分项目虽然是把Houdini的地形引入到制作管线里,但也仅仅是作为WordMachined的替代品,并不能完全发挥Houdini的全部功能。
所以,我们的目标还是方案二的开发方式,除了第一次在Houdini里做完初始地形导入到UE4里生成WorldCompositon后,就不再需要重新导回到Houdini,而是在UE4里调用预先封装好过程化功能的HDA节点来完成功能。这也是近年来Ubisoft在Tom Clancy’s Ghost Recon: Wildlands和Far Cry 5里使用Houdini的方案。具体案例在GDCVault上有GDC2017和GDC2018的相关视频,这里的目标也是要UE4里用类似他们的方法来实现功能。
图:Far Cry 5的地形编辑工具示例。可以直接在编辑器里调用Houdini功能对地形做修改。
但是使用原生的UE4 Houdini Engine的前提下,无缝大地型的UE4内部修改还是会有以下几个问题:
- 虽然Houdini Engine支持Landscape的读回到Houdini,但他Output只支持使用回读的Landacape Data信息创建一个新的Landscape:
- 每次修改,都要重新跑一遍上节提到的生成WorldCompositon和LandscapeStreamingProxy的过程,重新对地形对切割,还是非常耽误时间
- 虽然提供了基于Landscape Component的Input,但Output时每个Component会Cook成一个landscape,导致生成多个Component。
- 在原生配置下,即便只处理一部分地形的迭代也必须把整个Landsacape Data通过HDA Input到Houdini来进行处理:
- 假如使用Houdini里读入8x8k的地形,内存占用和过程化处理和交换数据量都会变大,从而导致Cook时间变长,降低迭代的速度。
- 不能基于Componment的控制生成范围,那就需要给HDA额外加入一个选择区域的Input,导致美术工作上变的更加繁琐。
- 很难在UE里做资源的版本管理,也不方便多人合作地形
对前面提到的一些概念和方法不了解也没关系,我接下来会用示例还原这些过程,直到最终的目标方案的原型,也就是Far Cry 5的功能效果。
图 Far Cry 5 地形编辑器,可以选择一个Terrain的Section或Sector区域来做过程化生成,极大的减小了处理和引擎与Houdini交换的资源量。
时间和篇幅的缘故,本节会分为上下篇,上半部分主要是在如何修改Houdini Engine源码,可以在UE4里基于Landscape Component进行小规模的迭代迭代的功能,下篇则是实现Houdini Engine管线的基础上,如何制作HDA节点,实现各种不同的编辑效果。
使用Houdini制作闭环
本节继续使用上一节的场景资源做作为测试用例,讲解一些HDA Input基础知识,让没有Houdini开发经验的程序人员也能很快接手Houdini Engine的改造。首先创建一个用来开发调试用的HDA节点,它的功能就是可以选中UE4场景里的一个Landscape作为Input,通过Houdini Engine把UE4 Landscape Height Data和Layer Data 转换为Heightfiled Height和Mask Data传入到HDA,在HDA里不做任何处理直接输出原始的Height和Mask,再次经过Houdini Engine生成UE4的Landscape Data。
下图就是一个Houdini闭环处理地形的流程展示,不需要打开Houdini,在UE4里就能完成闭环的操作。
如何创建一个SOP(Surface OPerators or geometry nodes )类型的Houdini Node Input的流程,Houdini Engine的官方文档里也有讲解 https://www.sidefx.com/docs/unreal/_inputs.html
方法一,Houdini里File->New Asset,按下图的创建一个新的Operator
方法二 创建一个Gemotry节点,Create Digital Asset,在HDA的Parameter里添加一个Operate Path的参数,把这个参数的与Object_Merge的Object做关联。
不论用这两种哪个方法制作都可以得到这个HDA文件,把它加入到UE资源并拖入到Level的话,按下图那样把Input类型选择为Landscape Input,就可以选择要处理LandscapeStreamingProxy。而勾选上最下面的“Export Selected Landscape Components Only”,就可以把Landscape Component作为Input输入给Houdini。但就像一开始提到的,原生Houdini Engine的这个功能并不能满足我们的需求。
基于component的更新的问题
如下图所示,原生UE4的Landscape的是支持多选Component Selection的,Houdini Engine也是支持多个Landscape Component 的Input。
选择4x4个Component作为要处理的Landscape信息,然后用Recommit看下结果.
如下图所示,虽然在效果面上,Houdini Enine把读入的LandScape Data转成HeightField Data 输入给Houdini又没有丝毫误差的的Output后转为LandScape Data,但Houdini Engine把这16个Component创建成了16个Landscape,这明显不是想要的结果。
另外要注意的是,新创建的Landscape的Transform和老的Landscape的Transform也不一样,这是Houdini和UE4的高度信息单位不同导致,这个问题也会在后面修改Houdini Engine时造成一定的困扰。而且原生的Component多选功能在Height和Mask更新上也会有一些问题。另外预先提到的一点,虽然Houdini Engine的Landscape的更新上有以上各种问题,但类似读取Landscape的信息来动态摆放,生成植物生态系统等不会修改LandscapeData的功能并不会受影响,这个具体的HDA开发也会在下篇里涉及到。
图:类似FarCry5根据选择地块的Mask信息生成植被的管线,原生的Houdini Engine也是可以胜任的。只需要一些Houdini HDA的功能开发就可以了。
定制Houdini Engine支持基于Component的生成和更新
因为时间和篇幅关系,这里提供一个不需要修改UE4源码,只少量修改Houdini Engine就可以解决问题的方法,先进入到UE4的Houdini Engine Plugin工程代码里。
和Landscape相关的功能,是在HoudiniLandscapeUtils和HoudiniEngineUtils里,建议有时间还是全看一遍,这里用注释简单描述下整个数据流程方便定位问题。
Input部分:
调用FHoudiniLandscapeUtils::CreateHeightfieldFromLandscapeComponentArray函数,把选择的Landscape Component的Height Data信息转为Houdini的HeightField Data。
1. Extracting the height data
2. Convert the height uint16 data to float
3. Set the HeightfieldData in Houdini
4. Extract and convert all the layers
// 1. Extract the uint8 values from the layer
// 2. Convert unreal uint8 to float
// 3. Set the heighfield data in Houdini
Output部分:
当数据在Houdini里处理完成后,调用FHoudiniLandscapeUtils::CreateAllLandscapes基于Houdini的volume数据生成Landscape。
// First, we need to extract proper height data from FoundVolumes
// Check that all layers/mask have not changed too
// Extract the Float Data from the Heightfield
// Convert the height data from Houdini's heightfield to Unreal's Landscape
// Look for all the layers/masks corresponding to the current heightfield
// Extract and convert the Landscape layers
// Create the actual Landscape
定位到FHoudiniLandscapeUtils::CreateLandscape函数里,它的核心功能把转换后的的uint16的Landsacpe的Height信息(TArray< uint16 >& IntHeightData)和Layer信息(TArray< FLandscapeImportLayerInfo >& ImportLayerInfos),通过调用UE4的ALandscapeProxy::Import来生成一个全新的Landscape。
这里选择的解决方案是不创建新的Landscape,把HeightData和LayerData做适当的封装,直接使用FLandscapeEditDataInterface的SetHeightData和SetAlphaData输入到需要修改的Landscape的对应Component的数据做更新。
Layer Data的处理方法
为了讲解简单起见,直接在CreateAllLandscapes函数的后面加上这部分功能。其中的Height Layer的更新相对简单,把之前Import用的TArray< FLandscapeImportLayerInfo > ImportLayerInfos的数据对应的用LandscapeEdit.SetAlphaData传给老的Landscape就可以了。
// Set Layer Data for (int32 LayerIndex = 0; LayerIndex < ImportLayerInfos.Num(); LayerIndex++) { LandscapeEdit.SetAlphaData(ImportLayerInfos[LayerIndex].LayerInfo, SelectLandscapeComponent->GetSectionBase().X, SelectLandscapeComponent->GetSectionBase().Y, SelectLandscapeComponent->GetSectionBase().X + SelectLandscapeComponent->ComponentSizeQuads, SelectLandscapeComponent->GetSectionBase().Y + SelectLandscapeComponent->ComponentSizeQuads, (uint8*)ImportLayerInfos[LayerIndex].LayerData.GetData(), 0); }
然后选中一个Component,给HDA配置上Landscape材质,以及一个HeightField Mask Noise节点,给地形的Groud信息图层信息增加一些噪声 。
左边是处理前的效果,右边是增加噪声后的效果。
可以看到,这里已经把Houdini处理过的Height Mask信息写回到了UE4原本的Landscape Layer上,实现了目标的效果。
Height Data的处理办法
和处理Layer Data的方法类似,把Houdini Engine Output的(TArray< uint16 >& IntHeightData)用LandscapeEdit.SetHeightData函数传回给Landscape Component。
ULandscapeInfo* LandscapeInfo = SelectLandscapeComponent->GetLandscapeProxy()->GetLandscapeInfo(); FLandscapeEditDataInterface LandscapeEdit(LandscapeInfo); int Num = IntHeightData.Num(); for (int i = 0; i < Num; i++) { // Convert Transform IntHeightData[i] = (IntHeightData[i] - ZeroValueInDigit)* SelectLandscapeScale.Z / OldLandscapeScale.Z + 32768.f; } // Set HeightData LandscapeEdit.SetHeightData(SelectLandscapeComponent->GetSectionBase().X, SelectLandscapeComponent->GetSectionBase().Y, SelectLandscapeComponent->GetSectionBase().X + SelectLandscapeComponent->ComponentSizeQuads , SelectLandscapeComponent->GetSectionBase().Y + SelectLandscapeComponent->ComponentSizeQuads , (uint16*)IntHeightData.GetData(), 0, false );
Convert Transform注释部分的处理,是因为经过重新处理后Houdini的Height Field的Data Range和之前的发生了变化,导致ConvertHeightfieldDataToLandscapeData里生成的HeightData和Transform信息和Input时的Transform信息不匹配,ZeroValueInDigit和OldLandscapeScale.Z分别代表了新地形的Transform信息。需要把两个Transform信息的差异对HeigtData做修正,才能把正确的Height Data写回到Landscape。如果你发现写回的地形整体高了一块或低了一块,或者高度比例和原来不一致,那通常就是这个HeightData的还原处理出错了。所以原始的Landscape Transform也要尽量标准,例如本节示例里初始Landscape的Transform就做的尽量正规化
看下修改代码后的效果,选择一个Landscape Component地块,在HDA节点里增加一个Heightfiled Noise的节点,对Landscape Height Data做一些轻微的噪声修改:
左侧是未处理的,右侧是处理完的。可以看到地块有了噪声的高低差的效果。
继续做一个Height Field Erode的测试,用TimeShift来控制Height Field Erode的演算帧数。
下图结果是TimeShift = 30 和 TimeShift = 60的效果对比。基本上实现了用Houdini Engine对一个Landsape Component的修改功能。但是问题也很明显。处理的Component和未处理的Component的边缘高度无法很好的衔接。距离推上生产线,还有不少功能需要开发和支持。
图:Height Field Erode效果生成,相比整个Landscape的演算,一个Component只要几秒内就能完成效果计算。
总结
Houdini Engine基于Landscape Component的过程化生成,确实可以大幅度的提升生成效率和速度,但是Houdini Engine和HDA制作都还需要一系列的定制开发
- 对多选Landscape Component的支持,并且解决多个Component之间的接缝问题。
- Input不能只有Landscape Component来控制范围,还需要为美术提供选区的功能来控制生成范围,避免Component边界问题。
<wiz_tmp_tag id="wiz-table-range-border" contenteditable="false" style="display: none;">