About Procedural Mesh Component
See
UProceduralMeshComponent
A procedural mesh is composed of multiple sections (FProcMeshSection
).
Sections
Each section holds some data such as the vertices (FProcMeshVertex
), collisions (bEnableCollision
) and more.
Each material has its own section (you can get/set the material of a section by using the section index or by reading the value of MaterialIndex
on the FSkelMeshRenderSection
entry.
When you use the various CreateMeshSection
functions you might wonder what each param is.
Here is a description for the important ones:
- Vertices: All the vertices for the section you are making, since you are using triangles this array should always be a multiple of 3 (not enforced, you might want to have extra vertices for some other use).
- Triangles: For each triangle (3 entry per triangle, same as Vertices) tell what vertices should be used for each triangle corner. For example if you want 1 triangle and you have 3 entries in Vertices, your Triangles array should be
{0, 1, 2}
. - VertexColors: Tell the color to be used for each vertex. This should match the size of Vertices.
Order of vertices indexes/triangles corners important
The order of your vertices are important and must “match” to not have some inverted normals/faces. This depends on your normal values As you can see more clearly in the “simple triangle” and “reuse vertices” examples below, the triangle is expected to be built in a specific order (Corner 1 → Corner 2 → Corner 3). Corner 1 would be the origin point, it doesnt have any order conditions.
In case of the normal value being
0,0,1
Corner 2 is the “right” corner. Corner 3 is the “forward” corner. This means that Corner 3 should have the biggest relative X value, and Corner 2 should have the biggest relative Y value.In case of the normal value being
0,0,-1
Corner 2 is the “left” corner. Corner 3 is the “bottom” corner. This means that Corner 3 should have the lowest relative X value, and Corner 2 should have the lowest relative Y value.
Vertices
FProcMeshVertex
hold per vertex data such as the position, normal, UVs and more.
Examples
Simple triangle
I have a list of sections to create, in this example we only have one entry.
Those values were set by the default section constructor (a custom type I made)
// Relevant code \\
// our 3 vertices, representing the 3 points of our triangle
Vertices.Emplace(FVector(0, 0, 0));
Vertices.Emplace(FVector(0, 100, 0));
Vertices.Emplace(FVector(100, 0, 0));
// the indexes of the 3 vertices (inside Vertices) that will be used for this triangle
Triangles.Emplace(0);
Triangles.Emplace(1);
Triangles.Emplace(2);
// UVs for this triangle (optional in our case)
UVs.Emplace(FVector2D(0, 0));
UVs.Emplace(FVector2D(1, 0));
UVs.Emplace(FVector2D(0, 1));
// colors for each vertex
VertexColors.Emplace(FColor::Red);
VertexColors.Emplace(FColor::Blue);
VertexColors.Emplace(FColor::Green);
// The 3 normal array entries are set to 0,0,1
Then I call a function on BeginPlay that creates the new sections from the data.
void UPSMTestProceduralMeshComponent::CreateMeshFromData()
{
ClearAllMeshSections();
// Assign the geometry data to the mesh component
for (int i = 0; i < SectionsData.Num(); ++i)
{
auto& CurrentSection = SectionsData[i];
CurrentSection.CalcNormals();
CreateMeshSection_LinearColor(
0,
CurrentSection.Vertices,
CurrentSection.Triangles,
CurrentSection.Normals,
CurrentSection.UVs,
CurrentSection.VertexColors,
TArray<FProcMeshTangent>(),
CurrentSection.bCreateCollision
);
SetMaterial(i, CurrentSection.Material);
}
}
Why using a material ? You are already giving the colors with
VertexColors
.Well if you don’t supply any material the engine will default to the default grid material. You can use whatever material you want however if you want to see the values you set in
VertexColors
you should use theVertexColor
material Node like so:
And here is the result:
2 simple triangles
Using the same code with different values as showed in the simple triangle example:
Result:
Reusing vertex for close triangle (square case)
In some cases you want 2 (or more) triangles to be next to each other (for example to make a square).
In this square example we will add 1 vertex to our first 3 and reuse 2 of the previous vertices for our new triangle.
The data is the following:
Result:
Skeletal Mesh
You can generate a mesh looking the same as a Skeletal Mesh from a Skeletal Mesh Component.
Warning
Before doing further please note that this is some early testing code that “works” (correct visuals but seems to have incorrect triangle count and maybe broken collisions).
Code (primarily taken from this UE forum post):
void UPSMTestProceduralSkeletalMeshComponent::CreateProceduralSkeletalMeshFromData(USkeletalMeshComponent* SkeletalMeshComponent, int32 LODIndex)
{
if (!IsValid(SkeletalMeshComponent)) { return; }
FSkeletalMeshRenderData* SKMRenderData = SkeletalMeshComponent->GetSkeletalMeshRenderData();
const FSkeletalMeshLODRenderData& DataArray = SKMRenderData->LODRenderData[LODIndex];
FSkinWeightVertexBuffer& SkinWeights = *SkeletalMeshComponent->GetSkinWeightBuffer(LODIndex);
TArray<FVector> VerticesArray;
TArray<FVector> Normals;
TArray<FVector2D> UV;
TArray<FColor> Colors;
TArray<FProcMeshTangent> Tangents;
TArray<UMaterialInterface*> Materials;
for (int32 ReaderSectionIndex = 0; ReaderSectionIndex < DataArray.RenderSections.Num(); ReaderSectionIndex++)
{
const auto& SectionData = DataArray.RenderSections[ReaderSectionIndex];
// get num vertices and offset
const int32 NumSourceVertices = SectionData.NumVertices;
const int32 BaseVertexIndex = SectionData.BaseVertexIndex;
Materials.Emplace(SkeletalMeshComponent->GetMaterial(SectionData.MaterialIndex));
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.Emplace(SkinnedVectorPos.X, SkinnedVectorPos.Y, SkinnedVectorPos.Z);
// 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.Emplace(ZTangentStatic.X, ZTangentStatic.Y, ZTangentStatic.Z);
// add tangents
Tangents.Add(FProcMeshTangent(FVector(XTangentStatic.X, XTangentStatic.Y, XTangentStatic.Z), false));
// get UVs
const FVector2f SourceUVs = DataArray.StaticVertexBuffers.StaticMeshVertexBuffer.GetVertexUV(VertexIndex, 0);
UV.Emplace(SourceUVs);
// dummy vertex colors
Colors.Add(FColor::Red);
}
}
// get index buffer
FMultiSizeIndexContainerData IndicesData;
DataArray.MultiSizeIndexContainer.GetIndexBuffer(IndicesData.Indices);
for (int32 RenderSectionIndex = 0; RenderSectionIndex < DataArray.RenderSections.Num(); RenderSectionIndex++)
{
const auto& SectionData = DataArray.RenderSections[RenderSectionIndex];
TArray<int32> Tris;
// get number triangles and offset
const int32 SectionNumTriangles = SectionData.NumTriangles;
const int32 SectionBaseIndex = SectionData.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
CreateMeshSection(RenderSectionIndex, VerticesArray, Tris, Normals, UV, Colors, Tangents, bCreateCollision);
SetMaterial(RenderSectionIndex, Materials[RenderSectionIndex]);
}
if (bSimulatePhysics)
{
SetSimulatePhysics(true);
}
}