How to achieve destructible HLODs

,

Oct 24, 2020.Knowledge
Originally written by Ryan B.

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
  4. Reading from the visibility buffer in the Material
  5. Code Sample - Destruction Mesh Merge Extension

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<IMeshMergeModule>("MeshMergeUtilities"); 
Module.GetUtilities().RegisterExtension(MergeExtension);

At shutdown:

// Unregister the merge extension 
IMeshMergeModule& Module = FModuleManager::LoadModuleChecked<IMeshMergeModule>("MeshMergeUtilities"); Module.GetUtilities().UnregisterExtension(MergeExtension);

Creating the visibility buffer, texture & material

TArray<uint8> 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<ADestructibleActor>(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

HLOD

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<FMeshDescription>& 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; 
            }