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管線修改的細節上。
相關文章
相關標籤/搜索