Short introduction to mesh generation in Unity

Kajetan Radulski
9 min readFeb 19, 2024

--

This will be a short introduction to mesh generation in unity, created mostly for the purposes of being reffered to in other graphics releated courses.

This is a humble cube. It is probably a sight you are familiar with by now. If you don’t have one and want to follow along from the very beginning create it now.

The cube looks like a cube because there is a cube mesh attached to the mesh filter component. In this tutorial we will lear how to make our own cube mesh and attach it to mesh filter.

Understanding the meshes

Vertices

Vertices are simply points in 3D space.

Triangles

Every mesh is constructed of triangles. Triangles are represented in class by an array of ints that are read in a group of three.

So for example.

//This array 
int[] exampleTriangleArray = new[] { 0, 1, 2, 1, 2, 3};
//Represents two triangles
exampleTriangleArray = new[] {
0, 1, 2
2, 3, 0
};

You can visualize it like this

Remember that in unity triangles are rendered based on order of their vertices . If triangles are rendered backwards, change vertex order so that vertices are ordered counter-clockwise from the direction from which they are seen

UV’s

Uv’s determine which part of the texture will be displayed on each triangle. Uv coordinates go from (0,0) — left-bottom corner, to (1,1) — right-top corner

This is how those UV’s look on our simple cube

Normals

Normals are 3D vectors that determine the way light shines on an object. In most lighting models how bright the object is is determined by how close angle between surface normal and light direction is to 90 degrees

Mesh generation

All the above elements are stored in arrays. To get one corner’s position, normal and uv we would have to visit several different arrays every time. Instead let’s create a struct top keep those data compact while we are working on them.

I personally created my struct in new file called VertexUtility and called my struct VertexData.

public struct VertexData
{
public Vector3 Postion;
public Vector2 Uv;
public Vector3 Normal;
public bool Side;
}

Let’s create an actual static VertexUtility class with following ComputeNormal() function. This function will allow us to create normals perpendicular to our triangle surface.

...
public static class VertexUtility
{
public static Vector3 ComputeNormal(VertexData vertexA, VertexData vertexB, VertexData vertexC)
{
Vector3 sideL = vertexB.Postion - vertexA.Postion;
Vector3 sideR = vertexC.Postion - vertexA.Postion;

Vector3 normal = Vector3.Cross(sideL, sideR);

return normal.normalized;
}
}

With that done, we have a struct to store our dingle vertex data, but we still need a class that will turn this data into a mesh. That will be our MeshConstructionHelper. It’s functions are pretty straightforward as they mostly manage our value lists in order to later turn them into a mesh.

public class MeshContructionHelper
{
//All the properites we need to contruct a mesh in the end
private List<Vector3> _vertices;
private List<int> _triangles;
private List<Vector2> _uvs;
private List<Vector3> _normals;

public MeshContructionHelper()
{
_triangles = new List<int>();
_vertices = new List<Vector3>();
_uvs = new List<Vector2>();
_normals = new List<Vector3>();
}

//This function will actually produce a mesh
//How exiting!
public Mesh ConstructMesh()
{
Mesh mesh = new Mesh();
mesh.vertices = _vertices.ToArray();
mesh.triangles = _triangles.ToArray();
mesh.normals = _normals.ToArray();
mesh.uv = _uvs.ToArray();
return mesh;
}

//Adds three new vertices and makes a trianglew out of them
public void AddMeshSection(VertexData vertexA, VertexData vertexB, VertexData vertexC)
{
Vector3 normal = VertexUtility.ComputeNormal(vertexA, vertexB, vertexC);

vertexA.Normal = normal;
vertexB.Normal = normal;
vertexC.Normal = normal;

int indexA = AddVertex(vertexA);
int indexB = AddVertex(vertexB);
int indexC = AddVertex(vertexC);

AddTriangle(indexA, indexB, indexC);
}


private void AddTriangle(int indexA, int indexB, int indexC)
{
_triangles.Add(indexA);
_triangles.Add(indexB);
_triangles.Add(indexC);
}

private int AddVertex(VertexData vertex)
{
_vertices.Add(vertex.Postion);
_uvs.Add(vertex.Uv);
_normals.Add(vertex.Normal);
return _vertices.Count - 1;
}
}

Now, let’s create a script that will actually create the mesh. For now we will only create a drawing gizmos function that will display our mesh properties.

public class MeshGeneratorExample : MonoBehaviour
{
[SerializeField]
private MeshFilter _meshFilter;

private void OnDrawGizmosSelected()
{
Mesh mesh = _meshFilter.sharedMesh;
Gizmos.matrix = transform.localToWorldMatrix;
for (int index = 0; index < mesh.vertices.Length; index++)
{
Vector3 vertex = mesh.vertices[index];
Vector3 normal = mesh.normals[index];
Vector3 normalColor = (normal + Vector3.one) / 2f;
Gizmos.color = new Color(normalColor.x, normalColor.y, normalColor.z) * 1.5f;
Gizmos.DrawLine(vertex, vertex + normal);
Gizmos.color = Color.green;
Gizmos.DrawSphere(vertex, 0.1f);
}
}
}

Now, attach this new component to your cube, and attach MeshFilter reference. Now, you should see something like this.

Enable wireframe view to be able to see triangles

I also use a very simple shader to display our current uv’s

Shader "Unlit/DebugUVShader"
{
Properties
{
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100

Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag

#include "UnityCG.cginc"

struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};

struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};


v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = v.uv;
return o;
}

fixed4 frag (v2f i) : SV_Target
{
return float4(i.uv.x, i.uv.y, 0, 1);
}
ENDCG
}
}
}

Create material that uses this shader by right click the shader and selecting Create -> Material. Then attach this material to our cube.

Now, we get a lot of information about our mesh!

In every graphics tutorial there comes a moment when a cube has to die. This moment is now. Let’s replace our mesh filter mesh with an empty one.

public class MeshGeneratorExample : MonoBehaviour
{
...
public void GenerateMesh()
{
MeshContructionHelper meshContructionHelper = new MeshContructionHelper();
_meshFilter.sharedMesh = meshContructionHelper.ConstructMesh();
}
}

Now, let’s call it using but in new custom inspector.

No one calls you anymore these days.

But calling a function from custom inspector is an easy way of testing your code!

[CustomEditor(typeof(MeshGeneratorExample))]
public class MeshGeneratorEditor : Editor
{
public override void OnInspectorGUI()
{
DrawDefaultInspector();
MeshGeneratorExample meshSlicer = target as MeshGeneratorExample;

if (GUILayout.Button("Generate mesh"))
{
Undo.RecordObject(meshSlicer, "Generate meshe");
meshSlicer.GenerateMesh();
EditorUtility.SetDirty(meshSlicer);
}
}
}

Our cube has vanished! Just like your savings, but never fear, it will be right back. The cube that is. You will never recover financially.

Let’s make a triangle first.

public class MeshGeneratorExample : MonoBehaviour
{
...
public void GenerateMesh()
{
MeshContructionHelper meshContructionHelper = new MeshContructionHelper();
VertexData vertexA = new VertexData()
{
Postion = new Vector3(0, 0, 0),
Uv = new Vector2(0, 0)
};
VertexData vertexB = new VertexData()
{
Postion = new Vector3(0, 1, 0),
Uv = new Vector2(0, 1)
};
VertexData vertexC = new VertexData()
{
Postion = new Vector3(1, 0, 0),
Uv = new Vector2(1, 0)
};
meshContructionHelper.AddMeshSection(vertexA, vertexB, vertexC);
_meshFilter.sharedMesh = meshContructionHelper.ConstructMesh();
}
}

Simple enough right? This is how it looks.

Now let’s try making a quad by joining two triangles together.

public class MeshGeneratorExample : MonoBehaviour
{
...
public void GenerateMesh()
{
MeshContructionHelper meshContructionHelper = new MeshContructionHelper();
VertexData vertexA = new VertexData()
{
Postion = new Vector3(0, 0, 0),
Uv = new Vector2(0, 0)
};
VertexData vertexB = new VertexData()
{
Postion = new Vector3(0, 1, 0),
Uv = new Vector2(0, 1)
};
VertexData vertexC = new VertexData()
{
Postion = new Vector3(1, 0, 0),
Uv = new Vector2(1, 0)
};
VertexData vertexD = new VertexData()
{
Postion = new Vector3(1, 1, 0),
Uv = new Vector2(1, 1)
};
meshContructionHelper.AddMeshSection(vertexA, vertexB, vertexC);
meshContructionHelper.AddMeshSection( vertexB, vertexC, vertexD);
_meshFilter.sharedMesh = meshContructionHelper.ConstructMesh();
}
}

This does not look right… We messed up the order of vertices. Let’s try again.

    public void GenerateMesh()
{
MeshContructionHelper meshContructionHelper = new MeshContructionHelper();
VertexData vertexA = new VertexData()
{
Postion = new Vector3(0, 0, 0),
Uv = new Vector2(0, 0)
};
VertexData vertexB = new VertexData()
{
Postion = new Vector3(0, 1, 0),
Uv = new Vector2(0, 1)
};
VertexData vertexC = new VertexData()
{
Postion = new Vector3(1, 1, 0),
Uv = new Vector2(1, 1)
};
VertexData vertexD = new VertexData()
{
Postion = new Vector3(1, 0, 0),
Uv = new Vector2(1, 0)
};
meshContructionHelper.AddMeshSection(vertexA, vertexB, vertexC);
meshContructionHelper.AddMeshSection( vertexC, vertexD, vertexA);
_meshFilter.sharedMesh = meshContructionHelper.ConstructMesh();
}

Much better. Let’s create another quad on the other side.

public class MeshGeneratorExample : MonoBehaviour
{
...
public void GenerateMesh()
{
...
VertexData vertexE = new VertexData()
{
Postion = new Vector3(0, 0, 1),
Uv = new Vector2(1, 0)
};
VertexData vertexF = new VertexData()
{
Postion = new Vector3(0, 1, 1),
Uv = new Vector2(0, 1)
};
VertexData vertexG = new VertexData()
{
Postion = new Vector3(1, 1, 1),
Uv = new Vector2(1, 1)
};
VertexData vertexH = new VertexData()
{
Postion = new Vector3(1, 0, 1),
Uv = new Vector2(1, 0)
};
meshContructionHelper.AddMeshSection( vertexF,vertexE, vertexG);
meshContructionHelper.AddMeshSection( vertexH, vertexG, vertexE);
_meshFilter.sharedMesh = meshContructionHelper.ConstructMesh();
}
}

We are almost there. Now let’s connect existing points. But his whole thing get’s tedious quickly. Let’s help ourselves a bit with a CreateQuad function.

public class MeshGeneratorExample : MonoBehaviour
{
...
public void GenerateMesh()
{
...
private void CreateQuad(VertexData vertexA,
VertexData vertexB,
VertexData vertexC,
VertexData vertexD,
ref MeshContructionHelper meshContructionHelper)
{
meshContructionHelper.AddMeshSection(vertexA, vertexB, vertexC);
meshContructionHelper.AddMeshSection( vertexC, vertexD, vertexA);
}
}

Okay, let’s connect some points then!

public class MeshGeneratorExample : MonoBehaviour
{
...
public void GenerateMesh()
{
MeshContructionHelper meshContructionHelper = new MeshContructionHelper();
VertexData vertexA = new VertexData()
{
Postion = new Vector3(0, 0, 0),
Uv = new Vector2(0, 0)
};
VertexData vertexB = new VertexData()
{
Postion = new Vector3(0, 1, 0),
Uv = new Vector2(0, 1)
};
VertexData vertexC = new VertexData()
{
Postion = new Vector3(1, 1, 0),
Uv = new Vector2(1, 1)
};
VertexData vertexD = new VertexData()
{
Postion = new Vector3(1, 0, 0),
Uv = new Vector2(1, 0)
};
VertexData vertexE = new VertexData()
{
Postion = new Vector3(0, 0, 1),
Uv = new Vector2(1, 0)
};
VertexData vertexF = new VertexData()
{
Postion = new Vector3(0, 1, 1),
Uv = new Vector2(0, 1)
};
VertexData vertexG = new VertexData()
{
Postion = new Vector3(1, 1, 1),
Uv = new Vector2(1, 1)
};
VertexData vertexH = new VertexData()
{
Postion = new Vector3(1, 0, 1),
Uv = new Vector2(1, 0)
};
CreateQuad(vertexA, vertexB, vertexC, vertexD, ref meshContructionHelper);
CreateQuad(vertexF, vertexE,vertexH, vertexG, ref meshContructionHelper);
CreateQuad(vertexC, vertexB, vertexF, vertexG, ref meshContructionHelper);
_meshFilter.mesh = meshContructionHelper.ConstructMesh();
}

private void CreateQuad(VertexData vertexA,
VertexData vertexB,
VertexData vertexC,
VertexData vertexD,
ref MeshContructionHelper meshContructionHelper)
{
meshContructionHelper.AddMeshSection(vertexA, vertexB, vertexC);
meshContructionHelper.AddMeshSection( vertexC, vertexD, vertexA);
}
}

That’s about it, generating another three faces is an exercise for the reader, but I think you got a hang of it by now.

Here is a repo with essential scripts we ussed so far https://github.com/hesmeron/MeshGenerationEssentials

Thank you so much for reading, and good luck.

--

--