Download

How to achieve destructible HLODs

Oct 24, 2020.Knowledge
You can achieve this by using vertex colors in the HLOD meshes to store the original objects IDs. Using a texture that serves as a “visibility buffer” you can then hide some parts of the mesh from the material.
How to achieve destructible HLODs 1 Adding vertex colors to the merged mesh 2 Creating the visibility buffer, texture & material 3 Updating the visibility buffer 5 Reading from the visibility buffer in the Material 5 Code Sample - Destruction Mesh Merge Extension 6

Adding vertex colors to the merged mesh
This can be done quite easily using a “mesh merge extension” which will get called when a merged mesh is created.

You must then ensure you register/unregister this merge extension at the editor startup/shutdown.
At startup:
// Register the merge extension
MergeExtension = new FDestructionMeshMergeExtension();
IMeshMergeModule& Module = FModuleManager::LoadModuleChecked(“MeshMergeUtilities”);
Module.GetUtilities().RegisterExtension(MergeExtension);

At shutdown:
// Unregister the merge extension
IMeshMergeModule& Module = FModuleManager::LoadModuleChecked(“MeshMergeUtilities”); Module.GetUtilities().UnregisterExtension(MergeExtension);

Creating the visibility buffer, texture & material
TArray VisibilityBuffer;
UMaterialInstanceDynamic* VisibilityMaterial;
UTexture2DDynamic* VisibilityTexture;
int32 ActorCount = LODActor->SubActors.Num();
VisibilityBuffer.SetNumUninitialized(FMath::RoundUpToPowerOfTwo(ActorCount));
FMemory::Memset(LODActorData.VisibilityBuffer.GetData(), 0xff,
LODActorData.VisibilityBuffer.Num());
// Retrieve base HLOD material (always go to the static mesh, as this component may have a previous override)
UMaterialInterface* HLODMaterial = LODActor->GetStaticMeshComponent()->GetStaticMesh()->GetMaterial(0);
// Retrieve number of instance stored inside of this LOD actor
float NumberOfInstances = 0.0f;
if(HLODMaterial->GetScalarParameterValue(FMaterialParameterInfo(“NumInstances”), NumberOfInstances, true))
{
// Create dynamic texture size of (NumInstances, 1) if required
uint32 TextureSize = FMath::TruncToInt(NumberOfInstances);
// The baked material instance count must equal the visibility buffer size
if(TextureSize == VisibilityBuffer.Num())
{
if(VisibilityTexture == nullptr)
{
FTexture2DDynamicCreateInfo CreateInfo;
CreateInfo.Format = PF_G8;
CreateInfo.Filter = TF_Nearest;
CreateInfo.SamplerAddressMode = AM_Clamp;
CreateInfo.bSRGB = false;
UTexture2DDynamic* DynamicInstanceTexture = UTexture2DDynamic::Create(TextureSize, 1, CreateInfo);
if(DynamicInstanceTexture)
{
VisibilityTexture = DynamicInstanceTexture;
}
}
if(VisibilityTexture)
{
// Create dynamic material instance if required and set it to us the dynamic
texture
if(VisibilityMaterial == nullptr)
{
UMaterialInstanceDynamic* MaterialInstance =
UMaterialInstanceDynamic::Create(HLODMaterial, LODActor);
MaterialInstance->SetTextureParameterValue(“InstanceVisibilityTexture”,
VisibilityTexture);
VisibilityMaterial = MaterialInstance;
}
if(VisibilityMaterial)
{
LODActor->GetStaticMeshComponent()->SetMaterial(0, VisibilityMaterial);
FTexture2DDynamicResource* TextureResource =
static_cast<FTexture2DDynamicResource*>(VisibilityTexture->Resource);
if(FApp::CanEverRender() && ensure(TextureResource))
{
// Enqueue initial update
ENQUEUE_RENDER_COMMAND(FSetupVisibilityTexture)(
[TextureResource, VisibilityBuffer =
VisibilityBuffer](FRHICommandListImmediate& RHICmdList)
{
WriteRawToTexture_RenderThread(TextureResource,
VisibilityBuffer);
});
}
else
{
VisibilityTexture = nullptr;
VisibilityMaterial = nullptr;
}
}
}
else
{
VisibilityTexture = nullptr;
VisibilityMaterial = nullptr;
}
}
}

Updating the visibility buffer
int32 ActorIndex = 0; for(AActor* HLODSubActor : LODActor->SubActors)
{
if(ADestructibleActor* DestructibleActor = Cast(HLODSubActor))
{
// Set percentage health as uint8 in the visibility texture const uint8 HealthInt = DestructibleActor->GetHealthPercent() * 0xff; // Record destroyed state LODActorData.VisibilityBuffer[ActorIndex] = !(DestructibleActor->WasDestroyed() || DestructibleActor->IsPendingKill() || DestructibleActor->IsActorBeingDestroyed()) ? HealthInt : 0x0;
}
else
{
LODActorData.VisibilityBuffer[ActorIndex] = (HLODSubActor != nullptr) ? 0xff : 0x00;
} ActorIndex++;
}
Reading from the visibility buffer in the Material

Code Sample - Destruction Mesh Merge Extension
// Use vertex color attributes to store component indices
// In the material, this allows us to mask destructed building parts
class FDestructionMeshMergeExtension : public IMeshMergeExtension
{
public:
virtual void OnCreatedMergedRawMeshes(
const TArray<UStaticMeshComponent*>& MergedComponents,
const class FMeshMergeDataTracker& DataTracker,
TArray& MergedMeshLODs) override
{
ALODActor* MediumLevelLODActor;
int32 MaxHLODLevelIndex;
GetMediumHLODActor(MergedComponents, MediumLevelLODActor,
MaxHLODLevelIndex);

        if (ShouldSetupForDestruction(MediumLevelLODActor, MaxHLODLevelIndex) 
            &&  MergedMeshLODs.Num() == 1) 
        { 
            // Create primitive to actor mapping, assume that order of building actors 
            // matches 
            LODActor->SubActors 
            TMap<uint32, uint32> ComponentToOwnerMapping; 

            for (int32 ComponentIndex = 0; 
                 ComponentIndex < MergedComponents.Num();
                 ++ComponentIndex) 
            { 
                UStaticMeshComponent* StaticMeshComponent = 
                MergedComponents[ComponentIndex]; 

                if (StaticMeshComponent->GetOwner()) 
                { 
                    uint32 Index = 
                        MediumLevelLODActor->SubActors.IndexOfByPredicate(
                            [StaticMeshComponent](AActor* InActor) -> bool 
                                  {  
                                          return InActor == StaticMeshComponent->GetOwner();
                                  }
                            );  

                    if (ensure(Index != INDEX_NONE)) 
                    { 
                        ComponentToOwnerMapping.Add(ComponentIndex, Index); 
                    } 
                } 
            } 

            // Clear vertex colors 
            FMeshDescription& MergedMesh = MergedMeshLODs[0]; 
            TVertexInstanceAttributesRef<FVector4> VertexInstanceColors = 
                MergedMesh.VertexInstanceAttributes().GetAttributesRef<FVector4> 
                (MeshAttribute::VertexInstance::Color);

            for (FVertexInstanceID VertexInstanceID : 
                    MergedMesh.VertexInstances().GetElementIDs()) 
            { 
                VertexInstanceColors[VertexInstanceID] = FVector4(0.0f, 0.0f, 0.0f, 0.0f); 
            } 

            TArray<uint32> ComponentToWedgeOffsets; 
            ComponentToWedgeOffsets.SetNumZeroed(MergedComponents.Num()); 

            for (int32 ComponentIndex = 0; 
                 ComponentIndex < MergedComponents.Num(); 
                 ++ComponentIndex) 
            { 
                const int32 LOD0 = 0; 
                ComponentToWedgeOffsets[ComponentIndex] = 
                DataTracker.GetComponentToWedgeMappng(ComponentIndex, LOD0); 
            } 

            // Store component index + 1 in the wedge colors for each part of the merged 
            //mesh 
            for (int32 ComponentIndex = 0; 
                 ComponentIndex < MergedComponents.Num(); ++ComponentIndex) 
            {
                const uint32 ComponentStart = 
                    ComponentToWedgeOffsets[ComponentIndex]; 
                const uint32 ComponentEnd = 
                    ComponentToWedgeOffsets.IsValidIndex(ComponentIndex + 1) ? 
                        ComponentToWedgeOffsets[ComponentIndex + 1] : 
                        MergedMesh.VertexInstances().Num(); 
                FColor PerComponentFillColor = FColor::Black; 
                const uint32* StoredIndex = 
                    ComponentToOwnerMapping.Find(ComponentIndex); 

                if (StoredIndex) 
                { 
                    PerComponentFillColor.DWColor() = (*StoredIndex) + 1; 
                } 

                for (uint32 Index = ComponentStart; Index < ComponentEnd; ++Index) 
                { 
                    FVertexInstanceID VertexInstanceID(Index); 
                    VertexInstanceColors[VertexInstanceID] = 
                        FLinearColor(PerComponentFillColor); 
                } 
            } 
        } 
    }

    virtual void OnCreatedProxyMaterial(const TArray<UStaticMeshComponent*>& 
            MergedComponents, UMaterialInterface* ProxyMaterial) override 
    { 
        UMaterialInstanceConstant* Instance = 
            Cast<UMaterialInstanceConstant>(ProxyMaterial); 

        if (!Instance) 
        { 
            return; 
        }

        ALODActor* MediumLevelLODActor; 
        int32 MaxHLODLevelIndex; 

        GetMediumHLODActor(MergedComponents, MediumLevelLODActor, 
                           MaxHLODLevelIndex);

        // Enforce dithering materials for all HLODs 
        if (MaxHLODLevelIndex != INDEX_NONE) 
        { 
            Instance->BasePropertyOverrides.bOverride_DitheredLODTransition = true; 
            Instance->BasePropertyOverrides.DitheredLODTransition = true; 
        }

        // Setup medium HLODs for destruction 
        if (ShouldSetupForDestruction(MediumLevelLODActor, MaxHLODLevelIndex)) 
        { 
            // Ensure a destructible material is used 
            const static FName 
                EnableInstanceDestroyingParamName(TEXT("EnableInstanceDestroying")); 
            FGuid ParamGUID; 
            bool bEnableInstanceDestroying = false; 
            bool bFoundParam = 
                Instance->GetStaticSwitchParameterValue(
                                EnableInstanceDestroyingParamName, 
                                bEnableInstanceDestroying,
                                ParamGUID); 

            if (bFoundParam && bEnableInstanceDestroying) 
            { 
                Instance->SetScalarParameterValueEditorOnly( 
                    FMaterialParameterInfo("NumInstances"), 
                    FMath::RoundUpToPowerOfTwo(
                            MediumLevelLODActor->SubActors.Num())); 

                Instance->BasePropertyOverrides.TwoSided = false; 
                Instance->BasePropertyOverrides.bOverride_TwoSided = true; 
                Instance->InitStaticPermutation(); 
            } 
        } 
    }

    private: 

        static const uint32 MediumHLODLevelIndex = 1; 

        static void GetMediumHLODActor(const TArray<UStaticMeshComponent*>& 
                                       MergedComponents, ALODActor*& MediumLevelLODActor, 
                                       int32& MaxHLODLevelIndex) 
        { 
            MaxHLODLevelIndex = INDEX_NONE; 
            MediumLevelLODActor = nullptr; 

            for (UPrimitiveComponent* PrimitiveComponent : MergedComponents) 
            { 
                if (PrimitiveComponent && 
                    PrimitiveComponent->GetLODParentPrimitive()) 
                { 
                    ALODActor* ParentActor = 
                        Cast<ALODActor>(PrimitiveComponent->GetLODParentPrimitive()
                        ->GetOwner()); 

                    if (ParentActor) 
                    { 
                        if (ParentActor->LODLevel == MediumHLODLevelIndex) 
                        { 
                            MediumLevelLODActor = ParentActor; 
                        }

                        MaxHLODLevelIndex = FMath::Max(MaxHLODLevelIndex, 
                        ParentActor->LODLevel); 
                    } 
                } 
            } 
        }

        static bool ShouldSetupForDestruction(ALODActor* MediumLevelLODActor, 
                                              int32 MaxHLODLevelIndex) 
        { 
            if (MaxHLODLevelIndex != MediumHLODLevelIndex) 
            { 
                return false; 
            } 

            if (MediumLevelLODActor == nullptr) 
            { 
                return false; 
            }

            AWorldSettings* WorldSettings = 
                MediumLevelLODActor->GetLevel()->GetWorldSettings(); 
                
            return WorldSettings->GetNumHierarchicalLODLevels() == 2; 
        }