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
- Adding vertex colors to the merged mesh
- Creating the visibility buffer, texture & material
- Updating the visibility buffer
- Reading from the visibility buffer in the Material
- 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
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;
}