Mesh Batcher

Here’s another snippet of code from Look Out Below!

Look Out Below! is built using Unity Free, which is a great tool for indies with zero budget but it has some limitations. One of those is that we couldn’t use static batching, a process that combines meshes to reduce draw calls and increase performance.

We got around this by rolling out our own mesh batching, MeshBatcher, this script is put on a parent object and combines all its children. Currently it supports lightmaps, multiple materials and childen with a combined vertex count greater 65536 (Unity’s limit). Meshes are broken up by lightmap index first, then by material as submeshes.

There is also the option to build on Start() or via a PostProcess script to batch the mesh during the build phase (which is how static batching works). These have pros and cons, batching on Start() results in a smaller installer size but a longer load time, and batching on build gives a very fast load time but a larger installer, so it’s going to depend on how you want to use it.

Here’s MeshBatcher.cs

//----------------------------------------------
// MeshBatcher
// Mark Hogan
// www.popupasylum.co.uk
//----------------------------------------------

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

public class MeshBatcher : MonoBehaviour {

	class MeshStrip
	{
		public List<int> strip = new List<int>();
	}

	public List<GameObject> batchedResults = new List<GameObject>();

	public bool batchOnStart = false;

	/// <summary>
	/// Batchs the index of the lightmap.
	/// Meshes are divided into mesh strips (one strip per material)
	/// Meshes are given a lightmap index;
	/// Meshes with the same lightmap index and strips that use the same material can have thier strips batched.
	/// Lightmapped meshes have a second (full 01) uv set and renderers contain the offset/scale of it in the lightmap
	/// UV2s must therefore be adjusted to account for the fact that we can only have one offset
	/// </summary>
	/// <param name='lightmapIndex'>
	/// Lightmap index.
	/// </param>
	void BatchLightmapIndex(int lightmapIndex)
	{
		MeshRenderer[] meshRenderers = GetComponentsInChildren<MeshRenderer>();

		List<Material> materials = UniqueMaterialsForRenderers(meshRenderers);

		MeshFilter[] meshFilters = GetComponentsInChildren<MeshFilter>();
		meshFilters = RemoveDisabledRenderersAndNoneLightmap(meshFilters, lightmapIndex);

		List<Vector3> verts = new List<Vector3>();
		List<Vector3> norms = new List<Vector3>();
		List<Vector2> uv = new List<Vector2>();
		List<Vector2> uv1 = new List<Vector2>();
		List<Color> col = new List<Color>();

		List<MeshStrip> strips = new List<MeshStrip>();
		for (int i = 0; i<materials.Count; i++){
			strips.Add(new MeshStrip());
		}

		foreach (MeshFilter mf in meshFilters)
		{
			int vertCount = verts.Count;

			// if the mesh filter has no mesh ignore it
			if (!mf || !mf.sharedMesh){
				break;
			}

			//if the result of adding this mesh would produce a mesh with a vertex count thats too high, create the current mesh and begin a new one
			if (mf.sharedMesh.vertexCount + vertCount > 65000)
			{
				MakeMeshChild("MeshLightmapIndex_" + lightmapIndex + "_" + vertCount,
				              verts.ToArray(),
				              norms.ToArray(),
				              uv.ToArray(),
				              uv1.ToArray(),
				              col.ToArray(),
				              strips.ToArray(),
				              materials.ToArray(),
				              lightmapIndex);

				verts.Clear();
				norms.Clear();
				uv.Clear();
				uv1.Clear();
				col.Clear();

				foreach (MeshStrip ms in strips)
				{
					ms.strip.Clear();
				}

				vertCount = 0;
			}

			//ADDING VERTICIES
			Vector3[] newVerts = new Vector3[mf.sharedMesh.vertices.Length];
			for (int i = 0; i<mf.sharedMesh.vertices.Length; i++)
			{
				newVerts[i] = transform.InverseTransformPoint(mf.transform.TransformPoint(mf.sharedMesh.vertices[i]));
			}
			verts.AddRange(newVerts);

			//ADDING STANDARD NORMALS
			norms.AddRange(mf.sharedMesh.normals);

			//ADDING STANDARD UVS
			uv.AddRange(mf.sharedMesh.uv);

			//CONVERTING INDIVIDUAL LIGHTMAP UVS TO THIS RENDERERS LIGHTMAP COORDS
			if (lightmapIndex>=0 && lightmapIndex<255){
				Vector2[] lightmapUVs = mf.sharedMesh.uv2;
				Vector4 lightmapTilingOffset = mf.renderer.lightmapTilingOffset;
				Vector2 uvscale = new Vector2( lightmapTilingOffset.x, lightmapTilingOffset.y );
				Vector2 uvoffset = new Vector2( lightmapTilingOffset.z, lightmapTilingOffset.w );
				for ( int j = 0; j < lightmapUVs.Length; j++ ) {
					lightmapUVs[j] = uvoffset + new Vector2( uvscale.x * lightmapUVs[j].x, uvscale.y * lightmapUVs[j].y );
				}
				uv1.AddRange(lightmapUVs);
			}

			//ACCOUNTING FOR MESHES WITHOUT VERTEX COLORS
			if (mf.sharedMesh.colors.Length == 0){
				Color[] replacementColors = new Color[mf.sharedMesh.vertexCount];
				for (int i = 0; i < mf.sharedMesh.vertexCount; i++){replacementColors[i] = Color.white;}
				col.AddRange(replacementColors);
			}
			else{
				col.AddRange(mf.sharedMesh.colors);
			}

			//ASSEMBLING TRIANGLE STRIPS
			for (int subMeshIndex = 0; subMeshIndex < mf.sharedMesh.subMeshCount && subMeshIndex < mf.renderer.sharedMaterials.Length; subMeshIndex++)
			{
				int[] mfStrip = mf.sharedMesh.GetTriangles(subMeshIndex);
				if (MeshIsFlipped(mf)){
					mfStrip = ReverseTriangleWinding(mfStrip);
				}
				for(int i = 0; i < mfStrip.Length; i++)
				{
					mfStrip[i] = mfStrip[i] + vertCount;
				}
				int stripIndex = materials.IndexOf(mf.renderer.sharedMaterials[subMeshIndex]);
				strips[stripIndex].strip.AddRange(mfStrip);
			}
		}

		MakeMeshChild("MeshLightmapIndex_" + lightmapIndex + "_"  + verts.Count,
		              verts.ToArray(),
		              norms.ToArray(),
		              uv.ToArray(),
		              uv1.ToArray(),
		              col.ToArray(),
		              strips.ToArray(),
		              materials.ToArray(),
		              lightmapIndex);

		foreach (MeshFilter r in meshFilters){
			DestroyImmediate(r.renderer);
			DestroyImmediate(r);
		}
	}

	void MakeMeshChild(string name, Vector3[] verts, Vector3[] norms, Vector2[] uv0s, Vector2[] uv1s, Color[] cols, MeshStrip[] subMeshes, Material[] materials, int lightmapIndex)
	{
		//MAKE NEW MESH
		Mesh newMesh = new Mesh();
		newMesh.name = name;

		//APPLYING VERTICIES< NORMALS< UVS< UV2S< COLORS
		newMesh.vertices = verts;
		newMesh.normals = norms;
		newMesh.uv = uv0s;
		if (uv1s.Length == verts.Length){
			newMesh.uv2 = uv1s;
		}
		newMesh.colors = cols;

		//APPLYING SUBMESH TRIANGLE STRIPS
		newMesh.subMeshCount = materials.Length;
		for (int i = 0; i<subMeshes.Length; i++)
		{
			newMesh.SetTriangles(subMeshes[i].strip.ToArray(), i);
		}

		GameObject childMesh = new GameObject(name);
		Transform cT = childMesh.transform;
		cT.parent = transform;
		cT.localPosition = Vector3.zero;
		cT.localScale = Vector3.one;
		cT.localRotation = Quaternion.identity;

		MeshFilter mfr = childMesh.AddComponent<MeshFilter>();
		mfr.sharedMesh = newMesh;

		MeshRenderer mr = childMesh.AddComponent<MeshRenderer>();
		mr.sharedMaterials = materials;
		mr.lightmapIndex = lightmapIndex;

		childMesh.layer = this.gameObject.layer;

		batchedResults.Add(childMesh);
	}

	int[] ReverseTriangleWinding(int[] triangleStrip)
	{
		int[] copy = new int[triangleStrip.Length];
		for (int i = 0; i<triangleStrip.Length; i+=3)
		{
			copy[i] = triangleStrip[i];
			copy[i+1] = triangleStrip[i+2];
			copy[i+2] = triangleStrip[i+1];
		}

		return copy;
	}

	bool MeshIsFlipped(MeshFilter m)
	{
		Vector3 mScale = m.transform.lossyScale;
		return (mScale.x*mScale.y*mScale.z < 0);
	}

	MeshFilter[] RemoveDisabledRenderersAndNoneLightmap(MeshFilter[] mfs, int removeMeshesWithLightmapIndexesThatDontMatchThisValue)
	{
		List<MeshFilter> filterList = new List<MeshFilter>();
		filterList.AddRange(mfs);

		List<MeshFilter> dupFilterList = new List<MeshFilter>();
		dupFilterList.AddRange(mfs);

		for (int i = 0; i < mfs.Length; i++){
			if (dupFilterList[i].renderer.enabled == false || dupFilterList[i].renderer.lightmapIndex!=removeMeshesWithLightmapIndexesThatDontMatchThisValue)	{
				filterList.Remove(dupFilterList[i]);
			}
		}

		return filterList.ToArray();
	}

	List<Material> UniqueMaterialsForRenderers(MeshRenderer[] meshRenderers)
	{
		List<Material> materials = new List<Material>();

		foreach (MeshRenderer mr in meshRenderers){
			foreach (Material mat in mr.sharedMaterials){
				if (mat && !materials.Contains(mat)){
					materials.Add(mat);
				}
			}
		}

		return materials;
	}

	/// <summary>
	/// Gets the lightmap indicies of mesh renderers in children
	/// </summary>
	/// <returns>
	/// The lightmap indicies.
	/// </returns>
	int[] GetLightmapIndicies()
	{
		MeshRenderer[] mrs = gameObject.GetComponentsInChildren<MeshRenderer>();

		List<int> lightmapIndexes = new List<int>();

		foreach (MeshRenderer mr in mrs){
			if (!lightmapIndexes.Contains(mr.lightmapIndex)){
				lightmapIndexes.Add(mr.lightmapIndex);
			}
		}

		return lightmapIndexes.ToArray();
	}

	public void Start()
	{
		if (batchOnStart && (batchedResults == null || batchedResults.Count == 0)){
			Batch();
		}
	}

	public void Batch()
	{
		//if (DuplicateBatch()){return;}

		foreach (int index in GetLightmapIndicies())
		{
			if (index>=-1 && index<=255){ BatchLightmapIndex(index);}
		}
	}

	public bool DuplicateBatch()
	{
		foreach (MeshBatcher mb in FindObjectsOfType(typeof(MeshBatcher)) as MeshBatcher[])	{
			if (mb.name == gameObject.name && mb.batchedResults.Count>0){
				foreach (GameObject r in mb.batchedResults){
					GameObject doop = Instantiate(r, this.transform.position, this.transform.rotation) as GameObject;
					doop.transform.parent = this.transform;
				}
				return true;
			}
		}

		return false;
	}
}

and BatchOnBuild.cs which must be put in an Editor folder

//----------------------------------------------
// BatchOnBuild
// Mark Hogan
// www.popupasylum.co.uk
//----------------------------------------------

using UnityEngine;
using UnityEditor;
using UnityEditor.Callbacks;

public class BatchOnBuild  {

	[PostProcessScene]
	public static void OnPostprocessScene() {
		MeshBatcher[] meshBatcher = GameObject.FindObjectsOfType(typeof(MeshBatcher)) as MeshBatcher[];

		foreach (MeshBatcher mb in meshBatcher)
		{
			if (!mb.batchOnStart){
				mb.Batch();
			}
		}
	}

}

1 Comment

Leave a comment

Your email address will not be published.