Procedural Mesh in UE4 #2 – Subdivided Plane
In the first part of this tutorial series, you’ve created a single triangle that can be textured. In this tutorial, you will use that knowledge and make a little bit more complex shape.
Subdivided plane
A subdivided plane is still pretty basic shape and as you’ll see in the moment, it’s not that hard to implement. However, the concepts, that you learn from it can be used when creating more complex shapes. Also, it is a good base for creating other more complicated shapes such as terrain.
Before you start coding the plane itself, let’s make few adjustment. Frist create new variable GenerateMesh which will be used as a button in the editor for manual control over the mesh generation. In constructor set that variable to false. In OnConstruction function, create if statement based on that variable. That’s where all mesh generation will happen. You can also delete the code from the previous tutorial since you will be implementing new functions for this. In the beginning of the if statement set GenerateMesh to false. This will basically allow you to use bool variable as a button, since every time you’ll check it, the construction script will run and immediately set it to false.
When creating a subdivided plane, you need to somehow set the resolution of the plane. For that reason create variables Width and Height. This will allow you to set the dimensions of the plane, but you still need to set the actual distance between vertices, since Width and Height variables are a number of vertices. To do that, create variable Spacing. Next, create two functions: GenerateVertices will be used to create vertices and everything vertex related (uvs, normals, colors, …), GenerateTriangles will be used to create the triangles. And that’s it for the header file.
#include "GameFramework/Actor.h" #include "ProceduralMeshComponent.h" #include "MyProceduralMesh.generated.h" UCLASS() class TUTORIALS_API AMyProceduralMesh : public AActor { GENERATED_BODY() UPROPERTY(VisibleAnywhere, Category = "MyProceduralMesh") UProceduralMeshComponent* pm; public: AMyProceduralMesh(); UPROPERTY() TArray<FVector> vertices; UPROPERTY() TArray<FVector> normals; UPROPERTY() TArray<int32> triangles; UPROPERTY() TArray<FVector2D> uvs; UPROPERTY() TArray<FLinearColor> vertexColors; UPROPERTY() TArray<FProcMeshTangent> tangents; UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuProceduralMesh") int32 height; UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuProceduralMesh") int32 width; UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuProceduralMesh") float spacing; UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuProceduralMesh") bool generateMesh; virtual void OnConstruction(const FTransform& Transform) override; void GenerateVertices(); void GenerateTriangles(); void ClearMeshData(); };
In the constructor, initialize newly created variables to some value (let’s say width and height to 4 and spacing to 50.0).
AMyProceduralMesh::AMyProceduralMesh() { PrimaryActorTick.bCanEverTick = true; pm = CreateDefaultSubobject<UProceduralMeshComponent>(TEXT("ProceduralMesh")); pm->AttachToComponent(GetRootComponent(), FAttachmentTransformRules::KeepRelativeTransform); width = 4; height = 4; spacing = 50.0f; generateMesh = false; }
Generate vertices function is pretty straightforward. Just create two for loops, one for height and one for width. Inside those loops add a new vertex. Location of that vertex is current x/y index times spacing.
Next, you need to set all the other arrays. You can set normals to vector (0.0, 0.0, 1.0f) vertex colors to black (0.0, 0.0, 0.0, 1,0) and tangent to (1.0, 0.0, 0.0). The uvs are a bit more complicated because it depends on your preference of unwrapping. Let’s just unwrap it, so the whole plane is within 0-1 range. First, you need to calculate the distance between each vertex in the UV space (uvSpacing). This can be easily done by dividing 1.0 by one of the plane dimensions (width or height). However, this will only work, if width and height are the same. If you make you plane rectangle, you need to divide by the longer side. Setting up the uvs is then easy. Just do the same as with vertex location, but instead of multiplying by spacing multiply by newly calculated uvSpacing. You can then easily change tiling inside material or modify the code as you see fit.
void AMyProceduralMesh::GenerateVertices() { float uvSpacing = 1.0f / FMath::Max(height, width); for (int32 y = 0; y < height; y++) { for (int32 x = 0; x < width; x++) { vertices.Add(FVector(x * spacing, y * spacing, 0.0f)); normals.Add(FVector(0.0f, 0.0f, 1.0f)); uvs.Add(FVector2D(x * uvSpacing, y * uvSpacing)); vertexColors.Add(FLinearColor(0.0f, 0.0f, 0.0f, 1.0f)); tangents.Add(FProcMeshTangent(1.0f, 0.0f, 0.0f)); } } }
GenerateTriangles function will assemble the individual triangles. Once again create two for loops, but this time the range will be one less than width/height. That’s because within each cycle a quad will be created (two triangles). There is a reason, why separating the generation of vertices and triangles is a good idea. While generating vertices creates sort of a “content” of the mesh, generating triangles creates the topology. You can run into a situation, where you want to regenerate a certain mesh over an over with some slight randomness. In that case, all you need is to generate the triangles once (since the topology will be the same) and only regenerate the vertices. And since triangles contain just indices to vertices you can delete and create the vertices at will. The only condition is, that the number of vertices must remain the same (otherwise you would have to regenerate triangles as well). There is even a function UpdateMeshSection, that takes all the arrays except the triangle array (because the topology must remain the same when updating the mesh). This function is recommended when updating procedural mesh without change of topology.
void AMyProceduralMesh::GenerateTriangles() { for (int32 y = 0; y < height - 1; y++) { for (int32 x = 0; x < width - 1; x++) { triangles.Add(x + (y * width)); //current vertex triangles.Add(x + (y * width) + width); //current vertex + row triangles.Add(x + (y * width) + width + 1); //current vertex + row + one right triangles.Add(x + (y * width)); //current vertex triangles.Add(x + (y * width) + width + 1); //current vertex + row + one right triangles.Add(x + (y * width) + 1); //current vertex + one right } } }
Conclusion
Subdivided plane demonstrates basic principles of generating mesh (and topology). The next part of this tutorial series will be about creating cube, sphere, and torus. There will also be a little bit of math, but nothing scary.
Source code
#include "GameFramework/Actor.h" #include "ProceduralMeshComponent.h" #include "MyProceduralMesh.generated.h" UCLASS() class TUTORIALS_API AMyProceduralMesh : public AActor { GENERATED_BODY() UPROPERTY(VisibleAnywhere, Category = "MyProceduralMesh") UProceduralMeshComponent* pm; public: AMyProceduralMesh(); UPROPERTY() TArray<FVector> vertices; UPROPERTY() TArray<FVector> normals; UPROPERTY() TArray<int32> triangles; UPROPERTY() TArray<FVector2D> uvs; UPROPERTY() TArray<FLinearColor> vertexColors; UPROPERTY() TArray<FProcMeshTangent> tangents; UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuProceduralMesh") int32 height; UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuProceduralMesh") int32 width; UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuProceduralMesh") float spacing; UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuProceduralMesh") bool generateMesh; virtual void OnConstruction(const FTransform& Transform) override; void GenerateVertices(); void GenerateTriangles(); void ClearMeshData(); };
#include "Tutorials.h" #include "MyProceduralMesh.h" AMyProceduralMesh::AMyProceduralMesh() { PrimaryActorTick.bCanEverTick = true; pm = CreateDefaultSubobject<UProceduralMeshComponent>(TEXT("ProceduralMesh")); pm->AttachToComponent(GetRootComponent(), FAttachmentTransformRules::KeepRelativeTransform); width = 4; height = 4; spacing = 50.0f; generateMesh = false; } void AMyProceduralMesh::OnConstruction(const FTransform& Transform) { if (generateMesh) { generateMesh = false; ClearMeshData(); GenerateVertices(); GenerateTriangles(); //Function that creates mesh section pm->CreateMeshSection_LinearColor(0, vertices, triangles, normals, uvs, vertexColors, tangents, false); } } void AMyProceduralMesh::GenerateVertices() { float uvSpacing = 1.0f / FMath::Max(height, width); for (int32 y = 0; y < height; y++) { for (int32 x = 0; x < width; x++) { vertices.Add(FVector(x * spacing, y * spacing, 0.0f)); normals.Add(FVector(0.0f, 0.0f, 1.0f)); uvs.Add(FVector2D(x * uvSpacing, y * uvSpacing)); vertexColors.Add(FLinearColor(0.0f, 0.0f, 0.0f, 1.0f)); tangents.Add(FProcMeshTangent(1.0f, 0.0f, 0.0f)); } } } void AMyProceduralMesh::GenerateTriangles() { for (int32 y = 0; y < height - 1; y++) { for (int32 x = 0; x < width - 1; x++) { triangles.Add(x + (y * width)); //current vertex triangles.Add(x + (y * width) + width); //current vertex + row triangles.Add(x + (y * width) + width + 1); //current vertex + row + one right triangles.Add(x + (y * width)); //current vertex triangles.Add(x + (y * width) + width + 1); //current vertex + row + one right triangles.Add(x + (y * width) + 1); //current vertex + one right } } } void AMyProceduralMesh::ClearMeshData() { vertices.Empty(); triangles.Empty(); uvs.Empty(); normals.Empty(); vertexColors.Empty(); tangents.Empty(); } <span id="mce_marker" data-mce-type="bookmark" data-mce-fragment="1"></span>
Leave a Reply