Houdini技术体系 基础管线(四) :Houdini驱动的UE4植被系统 下篇

背景

在上篇中,实现了使用Houdini在UE4里根据地形过程生成植被的最基本的原型。并且支持把植被在UE4里Bake成使用的HierarchicalInstancedStaticMeshComponent的BP形式,一定程度上解决了植被渲染效率的问题。
 
但这种方法在开发效率和运行效率上都还有他的问题:
  • 开发效率方面,这个方案并不支持UE4的Foliage Mode Editor:
    • 每个植被区域都被Bake成BP的形式,场景美术规划阶段就需要格外小心防止区域之间穿插造成植被之间的叠加
    • 当出现比较大的改动需求时,一个BP的范围发生改动就会造成大量BP重新生成的连锁反映。
    • 就像地形生成一样,完全的自动化并不现实,美术需要能通过UE4传统手绘方式来进行修改植被的方式。
    • 一个区域的BP植被需要重新生成时,还要重新绘制一遍之前的生成区域,这个过程除非预先保存,否则很难完全重现上次绘制的区域。
  • 运行效率优化方面,同一类的Instance并不能放到一个InstancedStaticMeshComponent 这样必定会造成一定程度的性能损耗
 
正因为如此,还有必要Houdini的植被管线与UE4的Foliage Mode编辑的植被系统串联起来。这样Hoduini生成后的内容,美术可以很方便的修改,也可以用Houdini来做二次修正,最终生成的植被也可以使用UE4的植被系统的优化方案。而UE4的Foliage System,其实就是每个Level里有一个AInstancedFoliageActor,每种Foliage Type对应的Instanc Mesh实例都保存在AInstancedFoliageActor的FFoliageMeshInfo里。
 
如果要把Houdini过程化生成与Foliage Mode衔接起来,那么就需要Houdini Engine Input和Output部分可以支持UE4的Foliage System。也就是每个Level的AInstancedFoliageActor里,概括来说就是:
  • Houdini Input要增加FoliageType的选项,生成实例的对象不再是用Statice Mesh,而是UE4的Foliage Type
  • Houdini Output直接输出植被实例不再Bake到BP里,而是直接Add到UE4的Foliage System的Foliage Instance里
 
接下来就讲解下如何通过只修改Houdini Engine,不需要触碰UE4引擎源码,来把Houdini植被管线与UE4的植被系统整合到一起的方法。

Houdini Input对FoliageType的选项支持

上篇中也提到过,原生的Houdini Engine的过程化实例放置功能,并没有把植被做特殊的Input处理,而是作为Geometry来对待。首要任务就是在Houdini Engine Input里可以支持Foliage Type。
 
先进入到HoudiniAssetInput.h里,在EHoudiniAssetInputType的Enum里增加FoliageTypeInput。
 
namespace EHoudiniAssetInputType
{
    enum Enum
    {
        GeometryInput = 0,
        AssetInput,
        CurveInput,
        LandscapeInput,
		FoliageTypeInput, // Add foliage type input
        WorldInput
    };
}

 

然后,在UHoudiniAssetInput类的CreateWidgetResources(),ChangeInputType(),CreateWidgetResources(),UploadParameterValue()的函数里,参考EHoudiniAssetInputType中其他的InputType的处理方式,加入对FoliageTypeInput的处理,此外,还要在FHoudiniParameterDetails类的CreateWidgetInput,加入针对FoliageTypeInput的菜单UI,这里可以参考
InParam.ChoiceIndex == EHoudiniAssetInputType::GeometryInput 

  

部分的代码给FoliageTypeInput实现一遍,但要自己实现一下Helper_CreateFoliageWidget
 
for ( int32 Ix = 0; Ix < NumInputs; Ix++ )
 {
    UObject* InputObject = InParam.GetInputObject( Ix );
    //Helper_CreateGeometryWidget( InParam, Ix, InputObject, AssetThumbnailPool, VerticalBox );
    Helper_CreateFoliageWidget(InParam, Ix, InputObject, AssetThumbnailPool, VerticalBox);
}

  

Helper_CreateFoliageWidget和Helper_CreateGeometryWidget的区别就在与UI里对UObject子类的筛选,把UStaticMesh替换成UFoliageType
 SNew( SAssetDropTarget )
        .OnIsAssetAcceptableForDrop( SAssetDropTarget::FIsAssetAcceptableForDrop::CreateLambda(
                []( const UObject* InObject ) {
                    return InObject && InObject->IsA< /*UStaticMesh*/ UFoliageType >();

  

这样,HDA里就增加了FoliageTypeInput的选项,并添加到Input里了。这里不得不说Unity写工具界面比UE4的效率高太多了。
 
虽然通过修改Houdini Engine,把FoliageType的Input读入了,但是Houdini Engine的管线的实例化部分,还是只能对Geometry来进行处理,这里就需要在FHoudiniEngineUtils::HapiCreateInputNodeForObjects函数里,获取FoliageType对应的Static Mesh再输出给Houdini Input Node了。
 
if (UFoliageType_InstancedStaticMesh * InputFoliageType = Cast<UFoliageType_InstancedStaticMesh>(InputObjects[InputIdx]))
{
    UStaticMesh* InputStaticMesh = InputFoliageType->GetStaticMesh();
    // Creating an Input Node for Static Mesh Data
    if (!HapiCreateInputNodeForStaticMesh(InputStaticMesh, MeshAssetNodeId, OutCreatedNodeIds, nullptr, bExportAllLODs, bExportSockets))
    {
        HOUDINI_LOG_WARNING(TEXT("Error creating input index %d on %d"), InputIdx, ConnectedAssetId);
    }
    SelectInputFoliageTypeArray.Add(InputFoliageType);
}

  

这样改造Houdini Engine后,输入FoliageType以及Landscape的Draw SelectRegion,就可以和上篇一样输出植被了。
Houdini Input支持Foliage Type后,接下来要实现的就是Houdni Output到Foliage System的功能了。

Houdini Output与Foliage Editor的关联

Output与FoliageEditor关联方面最基础的需求有以下几点。
  • Houdini输出的Entity Point Cloud所对应的Instance可以直接Add到UE4的Foliage System里。
  • 美术可以通过绘制区域来对已经生成部分再次做过程化生成,或者直接利用FoliageEdit手绘的方式来进行迭代调整。
  • 手绘调整部分和Houdini自动化生成部分可以分Layer保存,可以根据情况选择自动生成部分是否影响到手工调整部分。
FC5里也没有提及第三项的实现方式,所以基础管线部分主要讲解前两项的实现方法,而第三条在后续文章里会参考GDC2017上GHOST RECON的地形工具的方法来实现。
 
这里先定位到Houdini Engine生成Output到UE4的类函数UHoudiniAssetComponent::CreateObjectGeoPartResources里
 
#if WITH_EDITOR
    if ( FHoudiniEngineUtils::IsHoudiniNodeValid( AssetId ) )
    {
        // Create necessary instance inputs.
        CreateInstanceInputs( FoundInstancers );

        // Create necessary curves.
        CreateCurves( FoundCurves );

        // Create necessary landscapes
        CreateAllLandscapes( FoundVolumes );
    }
#endif

  

其中CreateInstanceInputs函数功能会迭代关卡里的每一种Instancer,再通过UHoudiniAssetInstanceInput::CreateInstanceInput(),根据这个Instancer对应的Cloud Point,在UHoudiniAssetComponent::CreateInstanceInputs创建InstancedStaticMesh。
 
for ( const FHoudiniGeoPartObject& GeoPart : Instancers )
{    
     HoudiniAssetInstanceInput->CreateInstanceInput();
}

  

在UHoudiniAssetInstanceInput::CreateInstanceInput()里,参考FEdModeFoliage::AddInstancesImp的方法, 在当前Level的AInstancedFoliageActor中对应FoliageType的FFoliageMeshInfo里,根据植被在Entity Point Cloud的Tranform信息,来添加Instance。
 
UWorld* World = GEditor->GetEditorWorldContext().World();
ULevel* TargetLevel = World->GetCurrentLevel();

AInstancedFoliageActor* IFA = AInstancedFoliageActor::GetInstancedFoliageActorForLevel(TargetLevel, true);
FFoliageMeshInfo* MeshInfo;
UFoliageType* FoliageSettings = IFA->AddFoliageType(FoliageType, &MeshInfo);

GLevelEditorModeTools().ActivateMode(FBuiltinEditorModes::EM_Foliage);
FEdModeFoliage* FoliageEditMode = (FEdModeFoliage*)GLevelEditorModeTools().GetActiveMode(FBuiltinEditorModes::EM_Foliage);
			
for (int32 InstanceIdx = 0; InstanceIdx < InstancerPartTransforms.Num(); ++InstanceIdx)
{
    FTransform InstanceTransform;

    FHoudiniEngineUtils::TranslateHapiTransform(InstancerPartTransforms[InstanceIdx], InstanceTransform);
    FFoliageInstance Inst;
    Inst.Location = InstanceTransform.GetLocation();
    Inst.Rotation = InstanceTransform.GetRotation().Rotator();
    MeshInfo->AddInstance(IFA, FoliageSettings, Inst, nullptr, true);
}		

  

如下图所示,过程化植被也加入到了Level的AInstancedFoliageActor里。这样生成后也可以使用Foliage Editor来做二次修改。
 
当场景美术需要做二次修改时,那么首先需要把修改区域的植被先清除掉,再使用Houdini对这块绘制区域重新过程化生成植被。这就需要Houdini Engine可以支持移除掉绘制区域植被的功能。这个可以参考void FEdModeFoliage::ReapplyInstancesForBrush的方法。使用MeshInfo->InstanceHash->GetInstancesOverlappingBox来获取美术绘制范围的Instance,在利用MeshInfo->InstanceHash->RemoveInstance把范围内对应的Foliage Type移除,再使用HDA来重新生成。在上一篇中我们也讲到Select Tool其实绘制的是地表的Mask,也就是对应地形Tile的X,Y值,所以这里还需要把Landscape的X,Y值转成世界空间的Box,来做判断,这部分的实现代码如下:
 
ULandscapeComponent* SelectLandscapeComponent = FHoudiniLandscapeUtils::SelectLandscapeComponentArray[Index];
int32 MinX = MAX_int32;
int32 MinY = MAX_int32;
int32 MaxX = -MAX_int32;
SelectLandscapeComponent->GetComponentExtent(MinX, MinY, MaxX, MaxY);
					
ULandscapeInfo* LandscapeInfo = SelectLandscapeComponent->GetLandscapeProxy()->GetLandscapeInfo();
for (int32 X = MinX; X <= MaxX; X++)
{
    for (int32 Y = MinY; Y <= MaxY; Y++)
    {
        float RegionSelect = LandscapeInfo->SelectedRegion.FindRef(FIntPoint(X, Y));
        if (RegionSelect > 0)
        {
            SelectRegionNum++;
            FBoxSphereBounds ComponentBounds = 
            SelectLandscapeComponent->CalcBounds(SelectLandscapeComponent->GetComponentTransform());

            FBox CachedLocalBox;
            CachedLocalBox.Min = FVector(X, Y, 0);
            CachedLocalBox.Max = FVector(X+1, Y+1, 0);
            CachedLocalBox.IsValid = 1;
            FBox MyBounds = CachedLocalBox.TransformBy(SelectLandscapeComponent->GetLandscapeProxy()->
            GetLandscapeActor()->GetActorTransform());
            MyBounds.Max.Z = ComponentBounds.GetBox().Max.Z ;
            MyBounds.Min.Z = ComponentBounds.GetBox().Min.Z ;
            FBoxSphereBounds RegionBounds  = FBoxSphereBounds(MyBounds);
            auto TempInstances = MeshInfo->InstanceHash->GetInstancesOverlappingBox(RegionBounds.GetBox());
            for (int32 Idx : TempInstances)
            {
                if(InInstancesToRemove.Find(Idx) == INDEX_NONE)
                    InInstancesToRemove.Add(Idx);
			}
		}
	}
}
MeshInfo->RemoveInstances(IFA, InInstancesToRemove, true);

  

迭代SelectRegion的每一个绘制点,把这个绘制点根据地形世界变化转换为对应的Box,再判断Box里是否有Instance,再进行移除操作。如果是基于Landscape Component做再生成就简单很多了,获取这个Component的Box,移除掉Box范围内的植被实例。
分步的看一下修改改后的效果。首先Houdini Engine会把绘制区域的植被全部清除掉。
 
然后再根据HDA里的Scatter算法,来摆放Instance并加入到关卡的AInstancedFoliageActor里。
之前的代码示例只是移除其中一种FoliageType,如果需要删除掉绘制区域的所有Foliage Type的话,只需要迭代每个FoliageType对应的FFoliageMeshInfo,再进行删除Instance操作即可。
 
TMap<UFoliageType*, FFoliageMeshInfo*> InstancesFoliageType = IFA->GetAllInstancesFoliageType();

for (auto& MeshPair : InstancesFoliageType)
{
    FFoliageMeshInfo* MeshInfo = MeshPair.Value;
    UFoliageType* FoliageSettings = MeshPair.Key;
}

  

就此,一个最基本的Houdini驱动的UE4植被系统的FoliageType部分就完成了。而一些具体的细节改动和工具开发,会放在内容制作部分再做讲解。

GrassType的对应

上一节也讲到,UE4的植被系统除了Foliage Type外,还有Grass Type,Grass Type除了没有碰撞外,保存和生成方式也不一样,Foliage Type的植被是制作阶段就保存在AInstancedFoliageActor里,在游戏运行时跟随Level加载后作为Instance来渲染。而Grass Type虽然也是跟Foliage Type一样,使用的HierarchicalInstancedStaticMeshComponents来进行渲染,区别在于它是在游戏运行时根据相机的视角来生成的。而GrassType的分布信息,则是根据UE4的地形材质系统来输出的。就如下图所示,默认的哪种GrassType被布置地面的哪个位置,是通过采样Landsacpe Layer的信息来确定的。但这种方式就导致了Grass和Layer之前的强制绑定关系。在一些特殊需求上,比如在一些特定的地标区域生成某种特定的草,或者排除掉某些特定草的需求,使用默认的方案都很难解决。

一种折衷的方案,是像下图这样,自己输入一张全场景的植被布局Mask图,来作为GrassType的采样信息使用,但这受限于植被的种类,地图大小,很难保障精确,只能作为临时的方案。
 
    还有一种方法就是直接改引擎源码,void ALandscapeProxy::UpdateGrass(const TArray<FVector>& Cameras, bool bForceSync)的部分。这个就不是光修改Houdini Engine引擎就能解决的了,文章篇幅关系也只能放到后文单独挑出一章节来做讲解。

总结

至此,地形和植被相关的整个管线部分已经基本上打通,但和国外AAA级产品的过程化工具比,还是有很大的差距。
管线上的主要的差距还是在工具易用性和完善程度上,比如幽灵行动:荒野里,通过把过程化生成的地形,道路,铁路,以及手工修改的部分做分层保存,这样当一层做修改或恢复时,才不会影响到其他的层的修改。
 
后续的文章中,会逐步的深入到具体的场景地形和植被制作上,届时也会涉及到更多Houdini Enigne管线修改的细节上。
 
posted @ 2018-07-07 18:52  Trace0429  阅读(...)  评论(...编辑  收藏