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管線修改的細節上。
 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM