Create procedural mesh from animated skeletal mesh?

Is there a way to do this in C++?

I’ve found a couple of things that explain something similar but can’t quite work it out.

Basically I want to have a procedural mesh that is an exact copy of the skeletal mesh, including animations and morphs, and have it updating at runtime.

It should also have collision.

I don’t mind if this will be expensive, it will only be used for one or 2 meshes.

1 Like

This should help, however it doesn’t work with morph targets.

void CopySkeletalMeshToProcedural(USkeletalMeshComponent* SkeletalMeshComponent, int32 LODIndex, UProceduralMeshComponent* ProcMeshComponent)
{

	FSkeletalMeshRenderData* SkMeshRenderData = SkeletalMeshComponent->GetSkeletalMeshRenderData();
	const FSkeletalMeshLODRenderData& DataArray = SkMeshRenderData->LODRenderData[LODIndex];
    FSkinWeightVertexBuffer& SkinWeights = *SkeletalMeshComponent->GetSkinWeightBuffer(LODIndex);

	TArray<FVector> VerticesArray;
	TArray<FVector> Normals;
	TArray<FVector2D> UV;
	TArray<int32> Tris;
	TArray<FColor> Colors;
	TArray<FProcMeshTangent> Tangents;

	
	//get num vertices
	int32 NumSourceVertices = DataArray.RenderSections[0].NumVertices;

	for (int32 i = 0; i < NumSourceVertices; i++)
	{
		//get skinned vector positions
		FVector SkinnedVectorPos = USkeletalMeshComponent::GetSkinnedVertexPosition(SkeletalMeshComponent, i, DataArray, SkinWeights);
		VerticesArray.Add(SkinnedVectorPos);

		//Calc normals and tangents from the static version instead of the skeletal one
		FVector ZTangentStatic = DataArray.StaticVertexBuffers.StaticMeshVertexBuffer.VertexTangentZ(i);
		FVector XTangentStatic = DataArray.StaticVertexBuffers.StaticMeshVertexBuffer.VertexTangentX(i);

		//add normals from the static mesh version instead because using the skeletal one doesn't work right.
		Normals.Add(ZTangentStatic);

		//add tangents
		Tangents.Add(FProcMeshTangent(XTangentStatic, false));

		//get UVs
		FVector2D uvs = DataArray.StaticVertexBuffers.StaticMeshVertexBuffer.GetVertexUV(i, 0);
		UV.Add(uvs);

		//dummy vertex colors
		Colors.Add(FColor(0, 0, 0, 255));
	}


	//get index buffer
	FMultiSizeIndexContainerData indicesData;
	DataArray.MultiSizeIndexContainer.GetIndexBuffer(indicesData.Indices);

	//iterate over num indices and add traingles
	for (int32 i = 0; i < indicesData.Indices.Num(); i++)
	{
		uint32 a = 0;
		a = indicesData.Indices[i];
		Tris.Add(a);
	}

	//Create the procedural mesh
	ProcMeshComponent->CreateMeshSection(0, VerticesArray, Tris, Normals, UV, Colors, Tangents, true);
}
2 Likes

Hi, tofmedPOST:
thank you so much for your greatest work done here, but there is a little problem in your code: error C2065: ‘SkinWeights’: undeclared identifier, how can I get the value of the SkinWeights variable?

Oh, I figure it out now:
add FSkinWeightVertexBuffer* SkinWeights = SkeletalMeshComponent->GetSkinWeightBuffer(LODIndex); before line 21 and it works fine. thanks a lot for your amazing work!

Oops, looks like i forgot a line - I’ve updated my answer, so you shouldn’t be seeing errors now :slight_smile:

This is cool! How would I go about implementing this code? I’m not very keen on C++ just yet. Would I make a Blueprint Function Library ? What would I include ? Thanks!

I would also like to know how this is implemented in blueprints, please

Works like a charm, thank you very much for sharing your knowledge!

Hello!
Thanks for the excellent solution; it helped me a lot!

A small addition: if the mesh has multiple render sections, this code will copy only the first one. It is easily solved by adding a loop and iterating through all DataArray.RenderSections.
And then offset the vertex indexes by DataArray.RenderSections[SectionCount].BaseVertexIndex.

But I have a different but related question: does anybody know how to pick only the vertexes, which have weights associated with some particular bone?
I try to create a dynamic dismemberment system, which would work on with “solid” skeletal meshes without any pre-cutting.
It already works, but it is not very efficient because each time, I have to copy a lot of “unused” vertices and slice them afterwards, but I don’t like this.
FSkinWeightVertexBuffer’s method GetBoneIndex(VertexIndex, InfluenceIndex) seems to be the solution. However, due to a lack of documentation, I am not sure that I use it properly (e.g. I still do not completely get what InfluenceIndex is).
Any ideas?

3 Likes

Hello dear anonymous user:) It’s great that you help people so much! You write “This should help, however it doesn’t work with morph targets.” I want to ask you if it’s possible to make it work with morph targets? Or should I not try?

I had some problems with a skeletal mesh that contain multiple RenderSections and also different vector types in UE5 (i guess?)
here’s updated example code that accounts for multiple render section and properly copies all render sections

To make different section with different materials to work properly you also need to fill in “triangles” array properly accounting for each section BaseIndex and keeping in mind that index buffer array is multiple of 3 respective to “triangle index” of a section

Here is updated code that work for me.

void CopySkeletalMeshToProcedural(USkeletalMeshComponent* SkeletalMeshComponent, int32 LODIndex, UProceduralMeshComponent* ProcMeshComponent)
{
	FSkeletalMeshRenderData* SkMeshRenderData = SkeletalMeshComponent->GetSkeletalMeshRenderData();
	const FSkeletalMeshLODRenderData& DataArray = SkMeshRenderData->LODRenderData[LODIndex];
	FSkinWeightVertexBuffer& SkinWeights = *SkeletalMeshComponent->GetSkinWeightBuffer(LODIndex);

	TArray<FVector> VerticesArray;
	TArray<FVector> Normals;
	TArray<FVector2D> UV;
	TArray<FColor> Colors;
	TArray<FProcMeshTangent> Tangents;

	for (int32 j = 0; j < DataArray.RenderSections.Num(); j++)
	{
		//get num vertices and offset 
		const int32 NumSourceVertices = DataArray.RenderSections[j].NumVertices;
		const int32 BaseVertexIndex = DataArray.RenderSections[j].BaseVertexIndex;
		
		for (int32 i = 0; i < NumSourceVertices; i++)
		{
			const int32 VertexIndex = i + BaseVertexIndex;

			//get skinned vector positions
			const FVector3f SkinnedVectorPos = USkeletalMeshComponent::GetSkinnedVertexPosition(
				SkeletalMeshComponent, VertexIndex, DataArray, SkinWeights);
			VerticesArray.Add(fromFVector3f(SkinnedVectorPos));

			//Calc normals and tangents from the static version instead of the skeletal one
			const FVector3f ZTangentStatic = DataArray.StaticVertexBuffers.StaticMeshVertexBuffer.VertexTangentZ(
				VertexIndex);
			const FVector3f XTangentStatic = DataArray.StaticVertexBuffers.StaticMeshVertexBuffer.VertexTangentX(
				VertexIndex);

			//add normals from the static mesh version instead because using the skeletal one doesnt work right.
			Normals.Add(fromFVector3f(ZTangentStatic));

			//add tangents
			Tangents.Add(FProcMeshTangent(fromFVector3f(XTangentStatic), false));

			//get UVs
			const FVector2f SourceUVs = DataArray.StaticVertexBuffers.StaticMeshVertexBuffer.
			                                      GetVertexUV(VertexIndex, 0);
			FVector2d ResUVs;
			ResUVs.X = SourceUVs.X;
			ResUVs.Y = SourceUVs.Y;
			UV.Add(ResUVs);

			//dummy vertex colors
			Colors.Add(FColor(0.0, 0.0, 0.0, 255));
		}
	}

	//get index buffer
	FMultiSizeIndexContainerData IndicesData;
	DataArray.MultiSizeIndexContainer.GetIndexBuffer(IndicesData.Indices);


	for (int32 j = 0; j < DataArray.RenderSections.Num(); j++)
	{
		TArray<int32> Tris;

		// get number triangles and offset
		const int32 SectionNumTriangles = DataArray.RenderSections[j].NumTriangles;
		const int32 SectionBaseIndex = DataArray.RenderSections[j].BaseIndex;
		
		//iterate over num indices and add traingles
		for (int32 i = 0; i < SectionNumTriangles; i++)
		{
			int32 TriVertexIndex = i*3 + SectionBaseIndex;
			Tris.Add(IndicesData.Indices[TriVertexIndex]);
			Tris.Add(IndicesData.Indices[TriVertexIndex + 1]);
			Tris.Add(IndicesData.Indices[TriVertexIndex + 2]);
		}

		//Create the procedural mesh section
		ProcMeshComponent->CreateMeshSection(j, VerticesArray, Tris, Normals, UV, Colors, Tangents, true);
	}
}

1 Like

Is there any additional info regarding how to implement this? I don’t have any real knowledge with c++ but this solution seems to be the best avenue

Seems that the code causes a crash with
Assertion failed: (Index >= 0) & (Index < ArrayNum) [File:D:\build\++UE5\Sync\Engine\Source\Runtime\Core\Public\Containers\Array.h] [Line: 758] error. Did anybody experience this and knows what could be the cause?

Incase someone don‘’t know how to deal with multiple RenderSections

void UProceduralMeshComponent::CopySkeletalMesh(USkeletalMeshComponent* SkeletalMeshComponent, int32 LODIndex)
{
FSkeletalMeshRenderData* SkMeshRenderData = SkeletalMeshComponent->GetSkeletalMeshRenderData();
const FSkeletalMeshLODRenderData& LODRenderData = SkMeshRenderData->LODRenderData[LODIndex];
FSkinWeightVertexBuffer& SkinWeights = *SkeletalMeshComponent->GetSkinWeightBuffer(LODIndex);

TArray VerticesArray;
TArray Normals;
TArray UV;
TArray Tris;
TArray Colors;
TArray Tangents;

for (int SectionId = 0; SectionId < LODRenderData.RenderSections.Num(); ++SectionId)
{
const FSkelMeshRenderSection& SkmSection = LODRenderData.RenderSections[SectionId];
VerticesArray.Empty();
Normals.Empty();
UV.Empty();
Tris.Empty();
Colors.Empty();
Tangents.Empty();
for (int32 i = SkmSection.BaseVertexIndex, MaxIndex = SkmSection.BaseVertexIndex + SkmSection.NumVertices; i < MaxIndex; i++)
{
FVector SkinnedVectorPos = FVector(USkeletalMeshComponent::GetSkinnedVertexPosition(SkeletalMeshComponent, i, LODRenderData, SkinWeights));
VerticesArray.Add(SkinnedVectorPos);
FVector ZTangentStatic = FVector(LODRenderData.StaticVertexBuffers.StaticMeshVertexBuffer.VertexTangentZ(i));
FVector XTangentStatic = FVector(LODRenderData.StaticVertexBuffers.StaticMeshVertexBuffer.VertexTangentX(i));
Normals.Add(ZTangentStatic);
Tangents.Add(FProcMeshTangent(XTangentStatic, false));
FVector2D uvs = FVector2D(LODRenderData.StaticVertexBuffers.StaticMeshVertexBuffer.GetVertexUV(i, 0));
UV.Add(uvs);
Colors.Add(FColor(0, 0, 0, 255));
}
FMultiSizeIndexContainerData indicesData;
LODRenderData.MultiSizeIndexContainer.GetIndexBuffer(indicesData.Indices);
for (int32 i = SkmSection.BaseIndex, MaxIndex = SkmSection.BaseIndex + SkmSection.NumTriangles * 3; i < MaxIndex; i++)
{
uint32 a = 0;
a = indicesData.Indices[i] - SkmSection.BaseVertexIndex;
Tris.Add(a);
}

  CreateMeshSection(SectionId, VerticesArray, Tris, Normals, UV, Colors, Tangents, true);

}
}

(post deleted by author)

@SavvySouls @NfrancisJ @Zarrar2802
For us less cpp-ically inclined, I’ll list down the instructions to implement this code.

You will need Visual Studio 2022. You can use this tutorial to install and set up VS2022 for Unreal https://www.youtube.com/watch?v=HQDskHVw1to

We will create a Blueprint Function Library where we will store our code and make it callable anywhere.

Instructions are for UE5 but will work for UE4 too.

These are the steps I took to make it work, some of these might be redundant but I will post them nevertheless for sake of completeness.

  • First we need to ensure Procedural Mesh Component is enabled through Plugins. Restart the editor if prompted.

  • Second, we close the project, and open the .uproject file in notepad and add the plugin’s entry separately too. Normally this happens automatically but I’m not sure if it works for Unreal-provided plugins too so just in case.

  • Save and close the .uproject file and right click the .uproject file, click on “Generate Visual Studio Project Files”.

  • This should create a .sln file of the same name in the same folder as the .uproject file.
    image

  • Now, open the .uproject file again in Unreal Editor and add a C++ class. Note that the method to add a C++ class in UE4 is different, the option is located somewhere else, I don’t remember where, quick google search should fix that.

  • Go to All Classes, search for Blueprint Function Library and select the corresponding class of the same name, click Next.

  • Set Class Type to Public, click on the Private button then click Public again, it adds a “Public” folder to the file path, not sure if that’s important but that’s what I did. Give your shiny new BFL a name and then “Create Class”

  • Now save everything and close the editor, and - Save and right click the .uproject file, click on “Generate Visual Studio Project Files”, just like last time, for good measure. I’m just a guy trying not to break stuff.

  • Now launch your .sln file in Visual Studio 2022.
    image

  • It should launch in VS2022 like this:

  • In the Solution Explorer to the right, locate your shiny new BFL header file under Games/Source/Public/“YOURBFLNAME.h” and the cpp file at Games/Source/Private/“YOURBFLNAME.cpp”

  • Now comes the fun (idkwtfisgoingon) stuff. Copy the above cpp code from Create procedural mesh from animated skeletal mesh? - #11 by mrz_lxa (Thanks @mrz_lxa !) and paste it in the .cpp file at the bottom, after the include lines.

  • Now go up to the include line and under it, add an extra line: include “ProceduralMeshComponent.h”. The goal of this line is to include the Procedural Mesh Component library so that we can use the UProceduralMeshComponent class in our code below. If the previous steps were correct, this should all be clear, otherwise every instance of UProceduralMeshComponent in the below code will be highlighted and give the error " identifier UProceduralMeshComponent is undefined". Try the above steps with the Procedural Mesh Component Plugin again.

  • Add this little snippet to the function name. Note that the BFL name after “U” and before “::” will correspond to your own BFL.

  • Now copy the function name and arguments:

  • Go to the .h file and paste it within the curly braces “{ }” and under “GENERATED_BODY()”

  • Add “UFUNCTION(BlueprintCallable, Category = “Skeletal Mesh”)” above the copied line, Add "static void " before the copied line and add a “;” after the copied line to complete the header definition.

  • That’s about it, I guess. Right click your project name in the Solution Explorer and “Clean” cause why not. Then right click again and “Rebuild”.


  • Hopefully, it should compile. Once it does, launch your project and go to a blueprint graph, RMB and search for “Copy Skeletal Mesh To Procedural” and the node should pop up


  • If you get errors, chatgpt is able to guess fairly well why it happened, ask it.

  • If someone more adept than me at cpp corrects this post, listen to them. I’ll try to update it if I get a chance.

this is what I use:

void CopySkeletalMeshToProceduralAllSections(USkeletalMeshComponent* SkeletalMeshComponent, int32 LODIndex, UProceduralMeshComponent* ProcMeshComponent, bool CopyMaterials)
{
	if (!IsValid(SkeletalMeshComponent) || !IsValid(ProcMeshComponent))
	{
		return;
	}
	if (SkeletalMeshComponent->bHiddenInGame)
	{
		return;
	}
	if (!SkeletalMeshComponent->IsVisible())
	{
		return;
	}

	FSkeletalMeshRenderData* SkMeshRenderData = SkeletalMeshComponent->GetSkeletalMeshRenderData();
	if (!SkMeshRenderData)
	{
		UE_LOG(LogTemp, Error, TEXT("CopySkeletalMeshToProcedural SkeletalMeshComponent->GetSkeletalMeshRenderData() return null"));
		return;
	}

	const FSkeletalMeshLODRenderData& LODData = SkMeshRenderData->LODRenderData[LODIndex];
	FSkinWeightVertexBuffer& SkinWeights = *SkeletalMeshComponent->GetSkinWeightBuffer(LODIndex);
	FMultiSizeIndexContainerData IndicesData;
	LODData.MultiSizeIndexContainer.GetIndexBuffer(IndicesData.Indices);
	ProcMeshComponent->ClearAllMeshSections();
	TArray<FFinalSkinVertex> SkinnedVertices;	
	SkeletalMeshComponent->GetCPUSkinnedVertices(SkinnedVertices, 0);
	
	for (int SectionIndex = 0; SectionIndex < LODData.RenderSections.Num(); ++SectionIndex)
	{
		const FDuplicatedVerticesBuffer& DuplicatedVertices = LODData.RenderSections[SectionIndex].DuplicatedVerticesBuffer;
		const FSkelMeshRenderSection& RenderSection = LODData.RenderSections[SectionIndex];

		TMap<FVector3f, int32> VertexToIndexMap;
		const uint32 BaseVertexIndex = RenderSection.BaseVertexIndex;
		const uint32 NumSectionVerts = RenderSection.NumVertices;
		const uint32 NumTriangles = RenderSection.NumTriangles;
		TArray<FVector> Vertices;
		TArray<FColor> Colors;
		TArray<FVector> Normals;
		TArray<FProcMeshTangent> Tangents;
		TArray<FVector2D> UV0;
		TArray<FVector2D> UV1;
		TArray<FVector2D> UV2;
		TArray<FVector2D> UV3;		
		TArray<int32> Tris;

		for (int32 i = 0; i < RenderSection.GetNumVertices(); ++i)
		{
			const uint32 VertexIndex = BaseVertexIndex + i; //Vertex index within the entire mesh's vert list (RenderSection Start index + current i)
			Vertices.Add(FVector(USkeletalMeshComponent::GetSkinnedVertexPosition(SkeletalMeshComponent, VertexIndex, LODData, SkinWeights)));

			const FVector& ZTangentStatic = SkinnedVertices[VertexIndex].TangentZ.ToFVector(); //FVector(LODData.StaticVertexBuffers.StaticMeshVertexBuffer.VertexTangentZ(VertexIndex));
			const FVector& XTangentStatic = SkinnedVertices[VertexIndex].TangentX.ToFVector(); //FVector(LODData.StaticVertexBuffers.StaticMeshVertexBuffer.VertexTangentX(VertexIndex));

			if (LODData.StaticVertexBuffers.ColorVertexBuffer.GetNumVertices() > VertexIndex)
			{
				Colors.Add(LODData.StaticVertexBuffers.ColorVertexBuffer.VertexColor(VertexIndex));
			}

			Normals.Add(ZTangentStatic);
			if (LODData.GetNumTexCoords() >= 1)
				UV0.Add(FVector2D(LODData.StaticVertexBuffers.StaticMeshVertexBuffer.GetVertexUV(VertexIndex, 0)));
			if (LODData.GetNumTexCoords() >= 2)
				UV1.Add(FVector2D(LODData.StaticVertexBuffers.StaticMeshVertexBuffer.GetVertexUV(VertexIndex, 1)));
			if (LODData.GetNumTexCoords() >= 3)
				UV2.Add(FVector2D(LODData.StaticVertexBuffers.StaticMeshVertexBuffer.GetVertexUV(VertexIndex, 2)));
			if (LODData.GetNumTexCoords() == 4)
				UV3.Add(FVector2D(LODData.StaticVertexBuffers.StaticMeshVertexBuffer.GetVertexUV(VertexIndex, 3)));

			Tangents.Add(FProcMeshTangent(XTangentStatic, false));
		}		
		const int32 SectionNumTriangles = LODData.RenderSections[SectionIndex].NumTriangles;
		const int32 SectionBaseIndex = LODData.RenderSections[SectionIndex].BaseIndex;
		const int32 SectionNumIndices = SectionNumTriangles * 3;  

		for (int32 i = SectionBaseIndex; i < SectionBaseIndex + SectionNumTriangles * 3; i++)
		{
			Tris.Add(IndicesData.Indices[i] - RenderSection.BaseVertexIndex);		
		}
		ProcMeshComponent->CreateMeshSection(SectionIndex, Vertices, Tris, Normals, UV0, UV1, UV2, UV3, Colors, Tangents, true);
		if (CopyMaterials && SkeletalMeshComponent->GetMaterial(SectionIndex))
		{
			UMaterialInterface* Material = SkeletalMeshComponent->GetMaterial(SectionIndex);
			ProcMeshComponent->SetMaterial(SectionIndex, Material);
		}
	}		
	return;
}