08 August 2014

Combine meshes in Unity

If there are a lot of objects in a scene that share the same material but do not share the same meshes you can lower the number of draw calls by combining them into one single mesh.

Unity has a "MeshCombineUtility" in the standard assets that is supposed to do exactly that, but several of our combined meshes were missing parts in an unpredictable way. It uses a hashtable to index the used materials. I replaced it with a typed dictionary and that fixed it. Somehow we must have had hash collisions.

In the documentation of Mesh.Combinemeshes there is a script that intends to do the same, with simpler code. Oddly, that script does combine multiple meshes into one but it generates an object without materials.

So I wrote my own version of the CombineChildren script that combines both into one, working script:

using UnityEngine;
using System.Collections.Generic;

[AddComponentMenu("Mesh/Combine Children")]
public class CombineChildren : MonoBehaviour {

void Start()
{
 Matrix4x4 myTransform = transform.worldToLocalMatrix;
 Dictionary<string, List<CombineInstance>> combines = new Dictionary<string, List<CombineInstance>>();
 Dictionary<string , Material> namedMaterials = new Dictionary<string, Material>();
 MeshRenderer[] meshRenderers = GetComponentsInChildren<MeshRenderer>();
 foreach (var meshRenderer in meshRenderers)
 {
  foreach (var material in meshRenderer.sharedMaterials)
    if (material != null && !combines.ContainsKey(material.name)) {
   combines.Add(material.name, new List<CombineInstance>());
   namedMaterials.Add(material.name, material);
    }
 }

 MeshFilter[] meshFilters = GetComponentsInChildren<MeshFilter>();
 foreach(var filter in meshFilters)
 {
  if (filter.sharedMesh == null)
   continue;
  var filterRenderer = filter.GetComponent<Renderer>();
  if (filterRenderer.sharedMaterial == null)
   continue;
  if (filterRenderer.sharedMaterials.Length > 1)
   continue;
  CombineInstance ci = new CombineInstance
  {
   mesh = filter.sharedMesh,
   transform = myTransform*filter.transform.localToWorldMatrix
  };
  combines[filterRenderer.sharedMaterial.name].Add(ci);

  Destroy(filterRenderer);
 }

 foreach (Material m in namedMaterials.Values)
 {
  var go = new GameObject("Combined mesh");
  go.transform.parent = transform;
  go.transform.localPosition = Vector3.zero;
  go.transform.localRotation = Quaternion.identity;
  go.transform.localScale = Vector3.one;

  var filter = go.AddComponent<MeshFilter>();
  filter.mesh.CombineMeshes(combines[m.name].ToArray(), true, true);

  var arenderer = go.AddComponent<MeshRenderer>();
  arenderer.material = m;
 }
}}

25 comments:

Steve Swink said...

Oh man, perfect. I was *just* about to write my own version of this after being frustrated with the shite Unity example. As always a quick Google finds someone with a great solution.

Thanks for this!

-S

krishx007 said...

Thanks for sharing..!!!

Hristoz Stefanov said...

Thanks a bunch, Alex, this post is a real time saver as this is a very common optimization scenario.

Would you mind adding an open source license, so that people can copy-paste it with clear conscience :)

Alex Vanden Abeele said...

Copy paste away, all code on my blog may be used at will :)

Merc Simos said...

Hi. Dunno if what I m trying to do is what your script does. I have a model split into 3 meshes, i turn it into a ragdoll with ragdoll wizard, but when i try to move it ,it tears at the joint parts, so I thought the problem is that i must make it 1 mesh. I dropped the script into the parent Highrezmeshes(with children highrez1 highrez2 and highrez3) but nothing changes.

Alex Vanden Abeele said...

That won't work, no. The combine meshes script will only work for meshrenderers. For skinned mesh renderers you need to use other scripts, that also assign the bones correctly and stuff.
This is a good example: http://unity3d.com/showcase/live-demos#character-customization

Mark T said...

HI! Thanks for sharing this code, ever so awesomely.
I'm having a couple of problems, however. Using Unity 5, filter.renderer.sharedMaterial and filter.renderer.enabled don't seem to be recognized attributes of the class MeshFilter. I made sure to include the using statements as shown, but no luck.. maybe it's a change in Unity5? Anyway, do you know of a workaround for those 2 statements that would still allow the code to work? Or am I maybe doing something wrong (anyone else have this issue?)?

Thanks so much in advance. You've got a new fan.

Alex Vanden Abeele said...

Yeah, you're right. IN Unity 5 they removed the "renderer" and other properties on GameObjects. See: http://docs.unity3d.com/ScriptReference/GameObject-renderer.html

I've updated the script with the version that we have in game now.

Mark T said...

Awesome, Alex. Thank you so much!
Having an issue with "ResetLocal()" now, though. Not recognized as a function in Transform. Could it be a custom function you created elsewhere?

Thanks again!

Mark T said...

Ok, Got it! I just replaced "go.transform.ResetLocal();" with the 3 transform calls from the earlier version:

go.transform.localPosition = Vector3.zero;
go.transform.localRotation = Quaternion.identity;
go.transform.localScale = Vector3.one;

This should still work as expected, yeah?

Thanks again, so very very much!

Alex Vanden Abeele said...

Ugh, indeed, that was an extension method defined in our game, thx for the heads up!

Ulises González Zúñiga said...
This comment has been removed by the author.
Ulises González Zúñiga said...

Sorry for the noob question, but, how do I use this script?

Ulises González Zúñiga said...

Sorry for the spam, I figured how to use it. Just one more question. I'm seeing that the script creates a new combined mesh, but leaves all the individual children alive. Is there a way to delete the original children and work only with the combined mesh?

Mark T said...

Yes, you'll need to cycle through and delete child objects that are not your new combined mesh. I do this:
var children = new List();
foreach (Transform child in transform) {
if (child.name != "Combined Mesh")
children.Add (child.gameObject);
}
children.ForEach(child => Destroy(child));

Alex Vanden Abeele said...

Mark is right :) The reason why I don't remove the children in this script is that you might have behaviours on the children that should keep running.

Ulises González Zúñiga said...

Oh, ok, awesome. And just one last question. I'm working with a lot of game objects, which are often too many to be combined into just one mesh, leaving me with, say half of the object combined. Is there a way to modify the script to keep it combining into new meshes as long as there are still gameObjects? I know that a mesh cannot have more than 65000 (or so) vertexes in it. I could force it to keep combining gameobjects by tracking how many vertexes I have, right?

Alex Vanden Abeele said...

You could do that, yes. You'd have to track the amount of vertices and create multiple combined objects according to that.
But I'd rather parent the children manually to separate root game objects and add a CombineChildren to them. That way you can organize the children a bit conform their location in the scene, which will be good for culling.

Ulises González Zúñiga said...

Thank you fir the tip. I've been tinkering with the code a bit, I'm close on getting it to work on combining a lot of game objects. Thanks!!

David H said...

(Origonal Code)
People like you, fueling the unity community, is exactly why I love using Unity 3D. Thank you for the help.

f84a5490-b3ec-11e5-bebd-7731f2ae3390 said...

I'm not sure how to get this code to work. I was able to get the original unity example to work, but for me, this code creates many "Combined mesh" objects, which isn't what I need.

Alex Vanden Abeele said...

The script will create a combined mesh for every material it finds in the underlying hierarchy. Normally you'd want to combine a lot of meshes that share the same material resulting in only one or a few combined meshes. But if the meshes all have different materials than there's no point in trying to combine them. Batching won't work on them either.

Alex Vanden Abeele said...

I just noticed a case in our game where there were indeed many combined meshes generated. It must be something that has been recently changed in Unity (I think) since i haven't seen this behaviour before. I started a thread about it on the Unity forum in the hope it gets resolved: http://forum.unity3d.com/threads/shared-materials-in-assetbundles-become-unique.379375/

In the meantime I changed the script to cope with this weird behaviour.

Dpaint said...

Thanks for posting this code, Unity docs still dont provide a robust example. Can you add in a check for static objects that dont need combining?
if(!filter.gameObject.isStatic)

I was also wondering if you could transfer the shadow casting enums over with the material, similar to the material key pair values:
Dictionary string, List ShadowCastingMode scm = new Dictionary string, List ShadowCastingMode ();

and then storing the filterRenderer.shadowCastingMode for re-applying to the groups meshfilter at the end. Theres other values such as Recieve Shadows, Use LightProbes and the Reflection probe usage data that needs transfering to the Combined Mesh parent.

(An easier way would be to serialize it and apply to arenderer, but this would apply to all materials and groups found).
[SerializeField]
private ShadowCastingMode shadowCastingMode = ShadowCastingMode.Off;
public ShadowCastingMode SCM { get { return shadowCastingMode; } }

Alex Vanden Abeele said...

The code is free to change at your will, so feel free to make these changes. I myself prefer to keep the code clean and simple. For all the situations you mention I just wouldn't put the script on those gameobjects.