Character meshes in our game Academia are always the hardest to optimize. There are many of them and they hardly batch even if I had combined all character sprites in one single texture. A character in the game has two quads, the body and the head. I had given up on batching multiple characters but what frustrated me is that these two meshes are always rendered in separate draw calls. This is because the head sprite has transparency and it should be rendered on top of the body sprite. The two can’t be batched. What’s a motivated guy got to do? I combined them into a single mesh.

It’s easy to combine meshes. There are already existing tools out there for this purpose. However, I wanted something different. I want to be able to change the UV coordinates of some parts of the combined mesh during runtime. For example, when the character blinks, I want to change the UV coordinates of the face quad. I also wanted it to be lightweight. There are asset products like Mesh Baker that I find too bloated for my needs.

Time for some code! Here’s the MeshPartHandle. It handles the minimal data to represent a “part” of the combined mesh. It just contains a starting index and the vertex count of the mesh part. I’ll show the usage later.

public class MeshPartHandle { private int startIndex; private readonly int vertexCount; public MeshPartHandle(int vertexCount) { this.vertexCount = vertexCount; } public int VertexCount { get { return vertexCount; } } public int StartIndex { get { return startIndex; } set { startIndex = value; } } }

The following is the mesh combiner itself:

[RequireComponent(typeof(MeshFilter))] [RequireComponent(typeof(MeshRenderer))] public class CombinedMesh : MonoBehaviour { private Mesh mesh; private MeshFilter meshFilter; private MeshRenderer meshRenderer; private readonly Dictionary<Transform, Mesh> meshMap = new Dictionary<Transform, Mesh>(); private readonly Dictionary<Transform, MeshPartHandle> handleMap = new Dictionary<Transform, MeshPartHandle>(); private Transform selfTransform; public void Clear() { this.meshMap.Clear(); this.handleMap.Clear(); } public MeshPartHandle Add(Transform owner, Mesh mesh) { Assertion.Assert(!this.meshMap.ContainsKey(owner)); // Should not contain the specified owner yet this.meshMap[owner] = mesh; MeshPartHandle handle = new MeshPartHandle(mesh.vertices.Length); this.handleMap[owner] = handle; return handle; } private List<Vector3> vertices = new List<Vector3>(); private List<Color> colors = new List<Color>(); private List<Vector3> normals = new List<Vector3>(); private List<Vector2> uvs = new List<Vector2>(); private List<Vector2> uvs2 = new List<Vector2>(); private List<int> triangles = new List<int>(); // Cache array so we could easily set new UV values private Vector2[] uvArray; private Vector2[] uv2Array; public void Build() { this.vertices.Clear(); this.colors.Clear(); this.normals.Clear(); this.uvs.Clear(); this.uvs2.Clear(); this.triangles.Clear(); foreach (KeyValuePair<Transform, Mesh> entry in this.meshMap) { AddToBuild(entry.Key, entry.Value); } this.mesh = new Mesh(); this.mesh.vertices = this.vertices.ToArray(); this.mesh.colors = this.colors.ToArray(); this.mesh.triangles = this.triangles.ToArray(); this.mesh.normals = this.normals.ToArray(); this.uvArray = this.uvs.ToArray(); this.mesh.uv = this.uvArray; this.uv2Array = this.uvs2.ToArray(); this.mesh.uv2 = this.uv2Array; this.meshFilter = GetComponent<MeshFilter>(); Assertion.AssertNotNull(this.meshFilter); this.meshFilter.mesh = this.mesh; this.meshRenderer = GetComponent<MeshRenderer>(); Assertion.AssertNotNull(this.meshRenderer); } private void AddToBuild(Transform owner, Mesh mesh) { MeshPartHandle handle = this.handleMap[owner]; handle.StartIndex = this.vertices.Count; this.colors.AddRange(mesh.colors); this.normals.AddRange(mesh.normals); this.uvs.AddRange(mesh.uv); // Special case for UV2 // Other meshes don't have it so we use zeroes if(mesh.uv2.Length == 0) { for(int i = 0; i < mesh.vertices.Length; ++i) { this.uvs2.Add(VectorUtils.ZERO_2D); } } else { Assertion.Assert(mesh.uv.Length == mesh.uv2.Length); this.uvs2.AddRange(mesh.uv2); } // Adjust the triangle indeces for(int i = 0; i < mesh.triangles.Length; ++i) { this.triangles.Add(mesh.triangles[i] + handle.StartIndex); } if(this.selfTransform == null) { this.selfTransform = this.transform; // Cache } // Transform the vertices from its owner for(int i = 0; i < mesh.vertices.Length; ++i) { Vector3 transformedVertex = this.selfTransform.InverseTransformPoint(owner.TransformPoint(mesh.vertices[i])); this.vertices.Add(transformedVertex); } } public void SetMaterial(Material material) { this.meshRenderer.material = material; } public void SetSortingLayer(string sortingLayerName) { this.meshRenderer.sortingLayerName = sortingLayerName; } public void SetUvs(MeshPartHandle handle, Vector2[] uvs) { for(int i = 0; i < handle.VertexCount; ++i) { this.uvArray[handle.StartIndex + i] = uvs[i]; } this.meshFilter.mesh.uv = this.uvArray; } public void SetUvs2(MeshPartHandle handle, Vector2[] uvs) { for (int i = 0; i < handle.VertexCount; ++i) { this.uv2Array[handle.StartIndex + i] = uvs[i]; } this.meshFilter.mesh.uv2 = this.uv2Array; } public Transform SelfTransform { get { if(this.selfTransform == null) { this.selfTransform = this.transform; } return selfTransform; } } }

This is then how it is used:

CombinedMesh combinedMesh = GetComponent<CombinedMesh>(); // Or any other way of getting this instance MeshPartHandle headHandle = combinedMesh.Add(this.transform, this.headMesh); MeshPartHandle bodyHandle = combinedMesh.Add(this.transform, this.bodyMesh); combinedMesh.Build(); // Builds the combined mesh // During gameplay, say we want the character to use the sprite that's facing left Vector2[] leftHeadUvs = GetUvs("Head", Orientation.LEFT); combinedMesh.SetUvs(headHandle, leftHeadUvs); Vector2[] leftBodyUvs = GetUvs("Body", Orientation.LEFT); combinedMesh.SetUvs(bodyHandle, leftBodyUvs);

The combiner class is very straightforward. It just maintains a list of mesh data like vertices, colors, triangle indeces, normals, and UVs. Whenever a mesh is added through CombinedMesh.Add(), we also add the data of that mesh to the locally maintained lists. Each MeshPartHandle remembers how many vertices it has and where its starting index is. The handles are then used to change the parts of the mesh it represents. For now, it can only change UVs. It’s certainly possible to allow changes to colors, or normals, etc.

By using this, Unity now renders a character in a single draw call. An added effect to this is that batching multiple characters is now possible in certain conditions. For example, characters that lie in the same horizontal line can now be batched.

This is just one use. I think I’m going to use this to combine other objects. Our rendering is still terrible. Here’s hoping that I can improve it by using this simple mesh combiner.