Unreal Engine destructible skeletal mesh

Tanks in Tank Brawl 2 use skeletal meshes with shooting, moving, braking ..etc animations to make them good. When shooting at those tanks, part of the skeletal mesh fall off and the rest still animate correctly.

Unreal Engine doesn’t support this natively so after experimenting with many different techniques we finally settle with using a dynamic texture system which is fast and relatively simple to implement. In this technique, we make the  skeletal mesh use a material that have opacity channel controlled by a 10×10  grey scale dynamic texture, so we can turn off part of it when a piece is chucked off, we also need to create a separate actor with a static mesh component of that part at same location to replace turned off part of the skeletal mesh but since it is a separate actor, it can be physically simulated ( fall and bounce on ground ). Below is the part the skeletal mesh’s material showing its Opacity setup:

You can see that the texture is a Param2D type which when the game run, we will replace the default greyscale white texture tenTenWhite.png  it with a dynamic 10×10 grey scale texture that we will create in code. This dynamic texture is looked up by a separate UV channel  ( Shown here as TexCoord(1) instead of TexCoord(0)) of the skeletal mesh. When creating the skeletal mesh asset in 3D modelling model, you need to setup this  UV1 channel so that each individual part of the mesh is mapped into a single cell inside the 10×10 grid like below:

Here you can see that the whole skeletal mesh have 36 parts, each have uv setup so each  is whole inside a cell within a 10×10 grid, so they can be individually turn off/on by changing the alpha value of the corresponding pixel inside the 10×10 dynamic texture.

Initially the dynamic texture is all white which means the skeletal mesh will be rendered with all the part fully visible. When we want to make a part fall off, we do 2 things:

– Modify the dynamic texture so that the pixel at the part location (inside the UV map above ) change from 255 to 0. This will make the part disappeared from the skeletal mesh.

– Create a separate physic simulated actor with a static mesh represent the fall off part at the exact location that match its previous location in the skeletal mesh. This make the part appear to fall off and bouncing on the ground. So in addition to the 3D model for the whole tank ( skeletal mesh ) you also need one model (static mesh) for each part that can be fall off.

Below are c++ code for turning off parts of the skeletal mesh

Code TankActor.h


class ATankActor
{
	static const int m_textureSize = 10;

	UPROPERTY()
	UTexture2D* m_dynamicPartMaskTexture;

	UPROPERTY()
	UMaterialInstanceDynamic *m_dynamicMaterialInstance;

	FUpdateTextureRegion2D m_wholeTextureReagion;
	uint8 m_pixelArray[m_textureSize * m_textureSize] = { 255 };
}

Code TankActor.cpp

In our system, each part is identified by an index, and our array  m_cluster->m_parts[index].m_indexIntoPixelArray  tell us which index UV location ( the cell ) the part locate inside the 10×10 dynamic texture.  The m_indexIndoPixelArray is just computed by U*10 + V . Which you need to setup according to your need.



void ATank::turnOffPart(int index, class USkeletalMeshComponent* skelMesh)
{	
	// Create the dynamic texture if it wasn't created
	if (!m_dynamicPartMaskTexture)
	{
		first = true;
		m_dynamicPartMaskTexture = UTexture2D::CreateTransient(m_textureSize, m_textureSize, PF_G8);
		m_dynamicPartMaskTexture->CompressionSettings = TextureCompressionSettings::TC_Grayscale;
		m_dynamicPartMaskTexture->SRGB = 0;
		m_dynamicPartMaskTexture->UpdateResource();
		m_dynamicPartMaskTexture->Filter = TF_Nearest;

		for (int i = 0; i < m_textureSize * m_textureSize; ++i) m_pixelArray[i] = 255; } // Create the dynamic material instance if it wasn't created and // aslo set the material to use the dynamic texture created above if (!m_dynamicMaterialInstance) { m_dynamicMaterialInstance = skelMesh->CreateAndSetMaterialInstanceDynamic(1);
		m_dynamicMaterialInstance->SetTextureParameterValue("componentMaskTxt", m_dynamicPartMaskTexture);

	}

	// Mark the pixel to be invisible
	m_pixelArray[m_cluster->m_parts[index].m_indexIntoPixelArray] = 0;

	// Copy data over to the dynamic texture
	UpdateTextureRegions(m_dynamicPartMaskTexture, 0, 1, &m_wholeTextureReagion, m_textureSize, 1, m_pixelArray, false);
}

namespace
{
	void UpdateTextureRegions(UTexture2D* Texture, int32 MipIndex, uint32 NumRegions, FUpdateTextureRegion2D* Regions, uint32 SrcPitch, uint32 SrcBpp, uint8* SrcData, bool bFreeData)
	{
		if (Texture->Resource)
		{
			struct FUpdateTextureRegionsData
			{
				FTexture2DResource* Texture2DResource;
				int32 MipIndex;
				uint32 NumRegions;
				FUpdateTextureRegion2D* Regions;
				uint32 SrcPitch;
				uint32 SrcBpp;
				uint8* SrcData;
			};

			FUpdateTextureRegionsData* RegionData = new FUpdateTextureRegionsData;

			RegionData->Texture2DResource = (FTexture2DResource*)Texture->Resource;
			RegionData->MipIndex = MipIndex;
			RegionData->NumRegions = NumRegions;
			RegionData->Regions = Regions;
			RegionData->SrcPitch = SrcPitch;
			RegionData->SrcBpp = SrcBpp;
			RegionData->SrcData = SrcData;

			ENQUEUE_UNIQUE_RENDER_COMMAND_TWOPARAMETER(
				UpdateTextureRegionsData,
				FUpdateTextureRegionsData*, RegionData, RegionData,
				bool, bFreeData, bFreeData,
				{
					for (uint32 RegionIndex = 0; RegionIndex < RegionData->NumRegions; ++RegionIndex)
					{
						int32 CurrentFirstMip = RegionData->Texture2DResource->GetCurrentFirstMip();
						if (RegionData->MipIndex >= CurrentFirstMip)
						{
							RHIUpdateTexture2D(
								RegionData->Texture2DResource->GetTexture2DRHI(),
								RegionData->MipIndex - CurrentFirstMip,
								RegionData->Regions[RegionIndex],
								RegionData->SrcPitch,
								RegionData->SrcData
								+ RegionData->Regions[RegionIndex].SrcY * RegionData->SrcPitch
								+ RegionData->Regions[RegionIndex].SrcX * RegionData->SrcBpp
							);
						}
					}
			if (bFreeData)
			{
				FMemory::Free(RegionData->Regions);
				FMemory::Free(RegionData->SrcData);
			}
			delete RegionData;
				});
		}
	}
}