Dynamically generating 3D meshes in Unity: what not to do

A simple 2D square generated in Unity. It's purple because we haven't assigned any shaders yet.
A simple 2D square generated in Unity. It’s purple because we haven’t assigned any shaders yet.

Recently in Unity, I’ve been playing with dynamic map generation. I wanted a hex-based map, so first things being first, I needed a hexagon. My hexagons would be pretty simple meshes, and I had to have all the points and calculations about their shape encoded into Unity anyway, since I would need that to put together the grid, so I decided to dynamically generate the mesh itself through Unity.

This page in the docs explains how to do it, and this example code shows how to generate a simple square:

using UnityEngine;
using System.Collections.Generic;
using System;

public class Tile : MonoBehaviour {
	public int x;
	public int y;

	public static Tile generate(int x, int y) {
		GameObject tObj = Instantiate(Resources.Load("Prefabs/TilePrefab")) as GameObject;
		tObj.transform.SetParent(GameObject.Find("InstancesTile").transform, false);
		Tile t = tObj.GetComponent<Tile>();
		t.x = x;
		t.y = y;
		t.GenerateSquareMesh();
		return t;
	}

	private static Vector3 GetSquareCorner (Vector3 center, int i) {
		int x = 0;
		int y = 0;

		if(i == 1 || i == 2){
			x = 1;
		}

		if(i==3 || i == 2){
			y = 1;
		}

		return new Vector3(x, y, 0);
	}

	public void GenerateSquareMesh() {
		Mesh mesh = new Mesh();

		Vector3 center = new Vector3(0,0,0);
		Vector3[] points = new Vector3[5];
		List<int> triangles = new List<int>();
		for(int i=0; i < 4; i++){
			Vector3 corner = GetSquareCorner(center, i);
			points[i] = corner;
			triangles.Add(i); // this point
			triangles.Add((i+1) % 4); // next point around circumference
			triangles.Add(4); // center point
		}
		points[4] = center;

		mesh.vertices = points;
		mesh.triangles = triangles.ToArray();

		// Not planning on using UVs yet so these are dummy values to make Unity happy
		Vector2 uv = new Vector2(0,0);
		List<Vector2> uvList = new List<Vector2>();
		foreach(Vector3 point in points){
			uvList.Add(uv);
		}

		mesh.RecalculateBounds();
		mesh.RecalculateNormals();

		GetComponent<MeshFilter>().mesh = mesh;
	}

}

So I wrote some code to generate my hexagon, which had 7 points (1 per corner plus a center point) and 6 triangles. Then I wrote some more code to build a grid of them. Generating a 100×100 grid of these simple hexagons was pretty fast. Then I realized that I wanted to have some hilly and mountainous terrain, in which case it’s nice to have some actual elevation. For this, I had to add some sides to my hex. No big deal, I thought. I beefed up my mesh generation to add sides. This added another 6 points (for each corner bottom) and 12 triangles (two per side), for a total of 13 points and 18 tris.

A simple 2d hexagon generated in Unity.
A simple 2d hexagon generated in Unity.

A 3D "hexagon with sides" (hexagonal prism) generated in Unity.
A 3D “hexagon with sides” (hexagonal prism) generated in Unity.

I reran my project and it ground to a halt. A 10×10 map was still doable, but the 100×100 map would freeze Unity. Not cool for what’s supposed to be one of the simplest parts of my map generation. Running the profiler on the 10×10 case revealed that the profiler would freeze while the map was generating. In other words, Unity was fine displaying and lighting all these meshes, but the actual mesh generation was a struggle. At the same time, Unity was only eating about 25% CPU on my quad-core machine. In retrospect the answer is pretty simple. While Unity’s mesh rendering is multithreaded, the Unity API isn’t threadsafe. In a nutshell, this means my Unity scripts are running at 1/4 efficiency on a quad-core machine, 1/8 on an 8-core machine, and so on, so optimization is crucial.

Since all my meshes are the same shape, the obvious fix is to only generate a single hex mesh. (The individual elevations can be done by scaling the transform along the height axis.) As applied to the simple square tiles:

using UnityEngine;
using System.Collections.Generic;
using System;

public class Tile : MonoBehaviour {
	public int x;
	public int y;
	public static Mesh mesh =  GenerateSquareMesh();

	public static Tile generate(int x, int y) {
		GameObject tObj = Instantiate(Resources.Load("Prefabs/TilePrefab")) as GameObject;
		tObj.transform.SetParent(GameObject.Find("InstancesTile").transform, false);
		Tile t = tObj.GetComponent<Tile>();
		t.x = x;
		t.y = y;
		t.GetComponent<MeshFilter>().mesh = Tile.mesh;
		return t;
	}

	private static Vector3 GetSquareCorner (Vector3 center, int i) {
		int x = 0;
		int y = 0;

		if(i == 1 || i == 2){
			x = 1;
		}

		if(i==3 || i == 2){
			y = 1;
		}

		return new Vector3(x, y, 0);
	}

	public void GenerateSquareMesh() {
		Mesh mesh = new Mesh();

		Vector3 center = new Vector3(0,0,0);
		Vector3[] points = new Vector3[5];
		List<int> triangles = new List<int>();
		for(int i=0; i < 4; i++){
			Vector3 corner = GetSquareCorner(center, i);
			points[i] = corner;
			triangles.Add(i); // this point
			triangles.Add((i+1) % 4); // next point around circumference
			triangles.Add(4); // center point
		}
		points[4] = center;

		mesh.vertices = points;
		mesh.triangles = triangles.ToArray();

		// Not planning on using UVs yet so these are dummy values to make Unity happy
		Vector2 uv = new Vector2(0,0);
		List<Vector2> uvList = new List<Vector2>();
		foreach(Vector3 point in points){
			uvList.Add(uv);
		}

		mesh.RecalculateBounds();
		mesh.RecalculateNormals();

		GetComponent<MeshFilter>().mesh = mesh;
	}

}

And the results are pretty striking:

If you can't tell whether the horizontal line is for the 2D tiles or the new 3D tiles, it's because they overlap completely.
If you can’t tell whether the horizontal line is for the 2D tiles or the new 3D tiles, it’s because they overlap completely.

Lesson learned: mesh generation can be a major bottleneck in Unity, so don’t generate more meshes than you have to. In this case I could get away with making a single mesh, but in other situations you may need a lot of very different, complex meshes or a lot of very similar-but-different meshes. Some plausible but completely untested ideas for these cases:

  • If your generated meshes are the same on each playthrough, you may be able to import them as static assets. The Unify ObjExporter script can be used to export a Unity mesh, which can then be reimported as an asset.
  • If you are dynamically generating a lot of meshes, you may be able to spawn threads so that the bulk of your mesh creation can be done in parallel. The Unity API isn’t threadsafe, so the actual Mesh object creation and assignment to a MeshFilter would need to be done in the main thread, but calculation of vertices, triangles and UVs could be done in worker threads.
  • If your meshes are similar to each other, you may be able to have a static base mesh which you tweak a little bit for each new dynamic mesh, instead of calculating everything from scratch. This could save a fair whack of the mesh generation time.

I’ll be sure to update if I end up trying either of the above two methods for the rest of my meshes, but for now, this works pretty well for me!

Are you using the dynamic mesh generation/tweaking features in Maya? Do you have any other strategies to help keep it fast?

6 thoughts on “Dynamically generating 3D meshes in Unity: what not to do

  1. The last reason for creating meshes procedurally is that it is fun . And by creating scripts that generates meshes, you ll learn a lot about how Unity handles meshes internally.

    • Absolutely, I have ended up using some of the techniques I learned from this post in other 2D games. You can make some pretty interesting shapes with custom meshes and a math lib. 🙂

  2. Observe a sensible topology . The exact nature of a “sensible” structure for your mesh is rather subtle but generally, you should bear in mind how the vertices and triangles of the model will be distorted as it is animated. A poor topology will not allow the model to move without unsightly distortion of the mesh. A lot can be learned by studying existing 3D character meshes to see how the topology is arranged and why.

    • In my case there’s no need to watch the rig/topology so much as long as I got the look I wanted, since this were tiles for a board-style game and there was not going to be any animation applied to them. But I have definitely seen some ugly animations from poor topology. Good to keep in mind if you are going to animate, I agree. 😀

  3. I think this is among the most vital info for me. And i’m glad reading your article.
    But wanna remark on few general things, The web site style is wonderful,
    the articles is really nice : D. Good job, cheers

Leave a Reply

Your email address will not be published. Required fields are marked *