CombineMeshes - Creating a submesh for each unique material

So, I’m using Mesh.CombineMeshes to combine all child meshes into one mesh. However, not all of the meshes have the same material, so I would like to create a submesh for each unique material. I’ve spent hours googling this and have not been able to find a working example.

I tried changing the mergeSubMeshes to false in the CombineMeshes() function, but that doesn’t combine meshes at all (if I display submesh count, it is equal to the amount of meshes I’m combining). Also, all meshes turn invisible except for the very first one in the array.

Can someone please help me adjust my code so that it will create a submesh for each unique material?

using UnityEngine;
using System.Collections;

[RequireComponent(typeof(MeshFilter))]
[RequireComponent(typeof(MeshRenderer))]

public class scr_TerrainCombiner : MonoBehaviour {
	public MeshFilter[] meshFilters;
	public CombineInstance[] combine;

	public List<Material> materialList = new List<Material>();

	void Start() {

		foreach(Transform child in transform){
			child.position += transform.position;
		}
		transform.position = Vector3.zero;
		transform.rotation = Quaternion.identity;

		meshFilters = GetComponentsInChildren<MeshFilter>();

		combine = new CombineInstance[meshFilters.Length-1];

		int index = 0;

		for (int i = 0; i < meshFilters.Length; i++)
		{
			if (meshFilters[i].sharedMesh == null){
				continue;
			}

			combine[index].mesh = meshFilters[i].sharedMesh;
			combine[index].transform = meshFilters[i].transform.localToWorldMatrix;
			meshFilters[i].renderer.enabled = false;
			index++;
			
		}
		GetComponent<MeshFilter>().mesh = new Mesh();
		GetComponent<MeshFilter>().mesh.CombineMeshes (combine, true, true);
		renderer.material = meshFilters[1].renderer.sharedMaterial;


		Debug.Log(GetComponent<MeshFilter>().mesh.subMeshCount);

	}
}

Ugh, so I finally got it to properly create submeshes, but I end up getting almost double the vertices for one reduced draw call in my test scene!
Apparently my regular scene is too big to combine, as it throws an error saying I’ve reached the max amount of vertices.

I assumed that combining the meshes would also optimize the vertices by removing any unneeded ones (ie. if it was a plane, it could remove the vertices in the center, as they were unneeded). Is this not possible in any way?

EDIT: Nevermind, when I re-enable all my lighting and shadows, I drop from 159 draw calls to 11…
I just need to figure out how to split the mesh when the max amount of vertices have been reached then, I guess.
Or possibly some way to auto-chunk the world, then I can just have one mesh per chunk.

I put some code together to do this kind of thing trying to get something close to static batching for Unity Free.

Basically this script combines all its children into a single mesh with submeshes based on their lightmap index, up to the vertex limit with submeshes for materials. I also had a script in the Editor/ folder which would prebatch objects when building (I was using this on android and it was a lot faster to batch during the build phase than to try do it a runtime).

Also on low memory devices as the memory the meshes take can get pretty high, but if 2 mesh batchers have the same name then it can reuse the mesh from the one thats already been batched (this may or may not be desirable depending on your lighting ), just uncomment the commented line in Batch().

MeshBatcher.cs goes on the parent to combine children of

//----------------------------------------------
// 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;
	}
}

Editor/BatchOnBuild.cs

//----------------------------------------------
// 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 Like

Awesome, thanks a lot for the script!