背景
上篇中,我们介绍了如何修改Houdini Enigne来设置单个Landscape Compnent的Height和Layer的数据,但原生Houdini Engine并不支持多选Component的写回功能,下篇中,我们来解决这个问题。
Component多选支持的修改
Houdini Engine虽然支持多个Landscape Component的选择,但是并不支持写回到
Landscape Component,需要自己来实现这个功能。单个Component的实现方法上文已经接受。
多选和单选区别只是在要把所选的Landscape Component按提交给Houdini的顺序来保存。
通过阅读Houdini Engine代码可以看到FHoudiniLandscapeUtils::CreateHeightfieldFromLandscapeComponentArray的参数LandscapeComponentArray里有所有提交的Landscape Component,不过LandscapeComponentArray中保存顺序并不是真正的提交顺序,而Houdini Engine Output出来的处理结果的顺序是Input的顺序的是一致的,如果直接用LandscapeComponentArray的结果,就会导致Component的不对应,所以这里我是在FHoudiniLandscapeUtils::CreateHeightfieldFromLandscapeComponent函数里把Input的Component保存起来,也保证了保存顺序。
然后在FHoudiniLandscapeUtils::CreateAllLandscapes函数中,就可以把每个FoundHeightfield和LandscapeComponent做对应,来调用LandscapeEdit.SetHeightData来更新这个
Component的Height Data了。
for ( TArray< const FHoudiniGeoPartObject* >::TConstIterator IterHeighfields ( FoundHeightfields ); IterHeighfields; ++IterHeighfields ) { SelectLandscapeComponent = SelectLandscapeComponentArray[ComponentIndex];
而Layer Data的保存方式和Height Data,所有Landscape Component的Layer都保存在一个ImportLayerInfos,比如选了4个
Component,每个Component有4个Layer,ImportLayerInfos里就有4x4 16个Layer Data的信息,这里也需要自己按Component和每个Component的Layer数量来提交。下面伪代码,LayerNum为每个Component的Layer的数量,ComponentIndex为处理的Component的编号,而实际开发情况下,可能每个Component的Layer的数量和命名都不一样,那就需要根据规则来定制这里的算法了。
// Set Current Component's Layer Data for (int32 LayerIndex = LayerNum * ComponentIndex; LayerIndex < LayerNum * (ComponentIndex + 1); LayerIndex++) { LandscapeEdit.SetAlphaData } ComponentIndex++;
这样修改后,Houdini Engine就可以支持多选Landscape Component的Input和Output了。这里使用上节用到HDA文件,选中4个Component做HeightField Noise的生成
但结果跟我们预想的并不一样,而且只有第一个Component被做了Noise处理。。。
这是UE4原生的Houdini Engine的Input的数据和我们的HDA的处理算法不匹配导致的。
修改HDA对Input的支持
造成这个结果的原因,要从Houdini Engine生成Input的函数FHoudiniEngineUtils::HapiCreateInputNodeForLandscape入手:
// 1. Create the heightfield input node. // We'll use its mergeId to connect all the landscape layers, // while it's displayId will be our connected asset ID FString LandscapeName = LandscapeProxy->GetName() + TEXT("_Merge"); HAPI_NodeId MergeId = -1; if ( !FHoudiniLandscapeUtils::CreateHeightfieldInputNode( ConnectedAssetId, MergeId, LandscapeName ) ) return false;
这里是通过Houdini Engine,直接创建了一个Houdini的Merge节点,然后把每个Component的Landscape Height Data和Layer Data转为HeightField的Height和Mask Volume,在Merge到一起,也就是用C++代码来生成HDA节点,
这样就保证了所有Input的整体处理,而且也可以程序化的去对应不同的Input情况,在后面的章节里,很多Input项目也是要使用C++或Python来生成HDA节点来节省开发成本。
下图就是程序生成HDA节点的效果示意:
再增加一个Heightfield noise 看下效果:和之前闭环测试效果一样,因为
默认的Houdini HeightField节点并不支持这种多个Volume Merge的处理。
这里介绍
三种解决方法:
方法一是直接修改或重写HeightField Noise节点来支持整个Merged Volume:
方法二:用Loop处理每个Height Volume
方法三:用tilesplice把Volume合并到一起做处理,然后再用split重新切开
如果不想自己修改或定制节点的话,方法二和三都可以,感觉方法三还更省事,但方法三有以下几个问题:
一个是Tile顺序的问题。UE4里的Tile和Houdini的Tile的行列是不同的,同样一个2x2的Compnent,他的Input和Output的顺序有所不同:
Input Output
1 2 1 3
3 4 2 4
这个需要自己开发功能调整,另外,方法三所选的Component也必须是Nx
M这种连续的Volume,否则tilesplice节点会像下图这样帮你补齐。所以方法三也有不少的限制。
如果时间允许,还是自己开发一套Heightfield的节点来定制需要的功能,原生的HeightField系列节点在内存上也有些浪费,限制也比较多,而方法二和三可以作为临时应急方法。
边缘法线问题处理
再运行一次HDA处理,2x2的4个Component的节点确实都做了处理,但是在Component边缘有很明显的接缝问题,在World Normal视图下更明显。
造成法线接缝是Component之间是共享边缘造成的,单个Landscape Component的LandscapeEdit.SetHeightData即便选择计算法线,也会导致边缘因为采样不到旁边Component的顶点,而导致两个Component的不连续,这里我暂时使用了比较暴力的方法,所有的Landscape Comonent 在
SetHeightData都不计算法线,而是在最后重新计算整个Landscape的Normal。
再看下修改后多选Component增加一个HeightField Noise的效果。
Height Data处理后,就是Layer Data的处理了,这里把HeightField Noise 改成 HeightField Mask Noise,对Landscpae的4个Layer的Mask做噪声处理,和之前Height Data导入时一样,也会有Component之间的接缝问题。
这种分割图接缝的问题,以前用WorldMachine做Tile Mask时也经常遇到,也就是Tile边缘之间共享顶点的问题。用WM可以少输出一圈边缘的Map的方法来解决,在UE4里也可以使用类似的方法。
在调用的LandscapeEdit.SetAlphaData参数上,把Stride比默认的宽度减少1(XSize - 1),就可以不传边缘的Mask Data进去了。
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(), XSize - 1);
再看一下效果,接缝的问题基本上已经解决了。
总结
至此,Houdini技术体系的几个问题的基础解决方案已经完成,后面的文章会逐渐倾向Houdini的地形实际制作部分。
而这些技术案例,大多要基于这个闭环+可选组件的方式来实现,随着技术介绍流程,我也会在Github上定制一个类似Far Cry5的UE4 Houdini Engine版本,希望大家多提宝贵意见。