- Home /
CombineMeshes with Different Materials
Hi,
I need to use CombineMeshes() but there is something unclear about how it works.
My situation is this: I need to combine multiple meshes of different materials into one mesh, with (I'm assuming) one submesh per unique material. Sometimes there may be multiple instances of the same mesh using a different material, or multiple instances of the same mesh using the same material but for different submeshes.
The confusion is, the Mesh object doesn't contain information about which materials are being used for a particular instance, and CombineMesh doesn't request that information from a MeshRenderer object, which is the only thing that DOES contain that information. Two different meshes may have multiple submeshes, say 2 submeshes, but mesh1's submesh1's material might not be the same as mesh2's submesh1's material. How would it know to put them into different submeshes?
I read that you can specify that through CombineInstance.subMeshIndex:
ArrayList materials = new ArrayList();
ArrayList combineInstances = new ArrayList();
MeshFilter[] meshFilters = <Object>.GetComponentsInChildren<MeshFilter>();
foreach( MeshFilter meshFilter in meshFilters )
{
MeshRenderer meshRenderer = meshFilter.GetComponent<MeshRenderer>();
for(int s = 0; s < meshFilter.sharedMesh.subMeshCount; s++)
{
// !! Assumes that submesh index will always correspond to material index !!
int materialArrayIndex;
for(materialArrayIndex = 0; materialArrayIndex < materials.Count; materialArrayIndex++)
{
if(materials[materialArrayIndex] == meshRenderer.materials[s])
break;
}
if(materialArrayIndex == materials.Count)
materials.Add(meshRenderer.materials[s]);
CombineInstance combineInstance;
combineInstance.transform = meshRenderer.transform;
// ---------------------------------------------------------------
combineInstance.subMeshIndex = materialArrayIndex;
combineInstance.mesh = meshFilter.sharedMesh ??? .submesh[s] ???
// ---------------------------------------------------------------
combineInstances.Add( combineInstance );
}
}
But as you can see, meshFilter.sharedMesh doesn't give you access to the individual submeshes. So how is it supposed to know how to separate them?
Thanks for helping.
Answer by Bunny83 · Dec 19, 2011 at 12:24 PM
Mesh.CombineMeshes (or to be more precise the Mesh itself) can only use one material per submesh. The submesh index is the same as the material index. If you have more materials then submeshes Unity will apply those to one of the submeshes (can't remember which one, the first or the last, i guess to the last one).
Why do you need to combine them? Two or more different material means two or more drawcalls. So you don't have much benefit from combining the meshes except they come up as one.
If you combine meshes you usually use a texture atlas so you need only one material.
So, this is my understanding.
Suppose I wanted to combine these meshes:
$$anonymous$$esh1 ------
Submesh1 - uses $$anonymous$$aterial1
Submesh2 - uses $$anonymous$$aterial2
$$anonymous$$esh2 ------
Submesh1 - uses $$anonymous$$aterial2
Submesh2 - uses $$anonymous$$aterial3
Then would the result mesh be this?
Result $$anonymous$$esh ------
Submesh1 ($$anonymous$$esh1.Submesh1) - uses $$anonymous$$aterial1
Submesh2 ($$anonymous$$esh1.Submesh2 & $$anonymous$$esh2.Submesh1) - uses $$anonymous$$aterial2
Submesh3 ($$anonymous$$esh2.Submesh2) - uses $$anonymous$$aterial3
Note that $$anonymous$$esh1 and $$anonymous$$esh2 might be instances of the same mesh, not necessarily two different meshes altogether.
I know that at a $$anonymous$$imum you will need one draw call for each unique material. $$anonymous$$y intention is to accomplish just that - $$anonymous$$imizing the draw calls by combining all of the submeshes which use the same material into one big submesh, and therefore, using only one render call for that material. The problem is, as with the above example, this cannot be done considering the submesh index alone, unless Combine$$anonymous$$esh() is doing something I am not aware of.
Well, Combine$$anonymous$$eshes can either merge all submeshes into one or keep all individual submeshes. It's just a helper function. You can still combine them "manually". It's just a little bit of work to correct the indices.
Since all submeshes of one mesh use the same vertices (just different indices) you just have to concat all vertices into one vertex array. Remember the first index of the appended vertices. Just correct all indices of the second mesh by this offset. Now you can assign the indices the way you want. If you have 4 submeshes but two share the same material, just merge the two indices arrays into one so you have one big mesh. So you should end up with 3 submeshes and 3 materials like you mentioned.
$$anonymous$$esh1
vertices: v1_0, v1_1, v1_2, v1_3, v1_4, v1_5
submeshA: 0,1,2 --> $$anonymous$$aterial1
submeshB: 3,4,5 --> $$anonymous$$aterial2
$$anonymous$$esh2
vertices: v2_0, v2_1, v2_2, v2_3, v2_4, v2_5
submeshA: 0,1,2 --> $$anonymous$$aterial2
submeshB: 3,4,5 --> $$anonymous$$aterial3
___first vertex of second mesh (index 6)
// Now combined: /
Combined $$anonymous$$esh |
vertices: v1_0, v1_1, v1_2, v1_3, v1_4, v1_5, v2_0, v2_1, v2_2, v2_3, v2_4, v2_5
submeshA: 0,1,2 --> $$anonymous$$aterial1
submeshB: 3,4,5, 6,7,8 --> $$anonymous$$aterial2
submeshC: 9,10,11 --> $$anonymous$$aterial3
SubmeshB contains both indices of the first mesh and the shifted indices of the second mesh.
Okay, I will try this. :) I just hope this executes quickly enough to handle fairly high-polycount objects.
Thank you!
I created a script that combines the meshes but ins$$anonymous$$d of manually modifying the vertex data (for transforms) and index data, it just uses the Combine$$anonymous$$eshes() function multiple times. Is this faster than doing it manually? You seem to know your stuff so I thought I'd ask.
Answer by Bunzaga · Jan 08, 2016 at 08:40 AM
I've combined the Unity3d example, Zergling103, and added Benjamen247 modifications, to create a script which will combine sub meshes with different materials dynamically at runtime:
using UnityEngine;
using System.Collections;
public class MeshCombiner : MonoBehaviour
{
public void Awake()
{
ArrayList materials = new ArrayList();
ArrayList combineInstanceArrays = new ArrayList();
MeshFilter[] meshFilters = gameObject.GetComponentsInChildren<MeshFilter>();
foreach (MeshFilter meshFilter in meshFilters)
{
MeshRenderer meshRenderer = meshFilter.GetComponent<MeshRenderer>();
if (!meshRenderer ||
!meshFilter.sharedMesh ||
meshRenderer.sharedMaterials.Length != meshFilter.sharedMesh.subMeshCount)
{
continue;
}
for (int s = 0; s < meshFilter.sharedMesh.subMeshCount; s++)
{
int materialArrayIndex = Contains(materials, meshRenderer.sharedMaterials[s].name);
if (materialArrayIndex == -1)
{
materials.Add(meshRenderer.sharedMaterials[s]);
materialArrayIndex = materials.Count - 1;
}
combineInstanceArrays.Add(new ArrayList());
CombineInstance combineInstance = new CombineInstance();
combineInstance.transform = meshRenderer.transform.localToWorldMatrix;
combineInstance.subMeshIndex = s;
combineInstance.mesh = meshFilter.sharedMesh;
(combineInstanceArrays[materialArrayIndex] as ArrayList).Add(combineInstance);
}
}
// Get / Create mesh filter & renderer
MeshFilter meshFilterCombine = gameObject.GetComponent<MeshFilter>();
if (meshFilterCombine == null)
{
meshFilterCombine = gameObject.AddComponent<MeshFilter>();
}
MeshRenderer meshRendererCombine = gameObject.GetComponent<MeshRenderer>();
if (meshRendererCombine == null)
{
meshRendererCombine = gameObject.AddComponent<MeshRenderer>();
}
// Combine by material index into per-material meshes
// also, Create CombineInstance array for next step
Mesh[] meshes = new Mesh[materials.Count];
CombineInstance[] combineInstances = new CombineInstance[materials.Count];
for (int m = 0; m < materials.Count; m++)
{
CombineInstance[] combineInstanceArray = (combineInstanceArrays[m] as ArrayList).ToArray(typeof(CombineInstance)) as CombineInstance[];
meshes[m] = new Mesh();
meshes[m].CombineMeshes(combineInstanceArray, true, true);
combineInstances[m] = new CombineInstance();
combineInstances[m].mesh = meshes[m];
combineInstances[m].subMeshIndex = 0;
}
// Combine into one
meshFilterCombine.sharedMesh = new Mesh();
meshFilterCombine.sharedMesh.CombineMeshes(combineInstances, false, false);
// Destroy other meshes
foreach (Mesh oldMesh in meshes)
{
oldMesh.Clear();
DestroyImmediate(oldMesh);
}
// Assign materials
Material[] materialsArray = materials.ToArray(typeof(Material)) as Material[];
meshRendererCombine.materials = materialsArray;
foreach (MeshFilter meshFilter in meshFilters)
{
DestroyImmediate(meshFilter.gameObject);
}
}
private int Contains(ArrayList searchList, string searchName)
{
for (int i = 0; i < searchList.Count; i++)
{
if (((Material)searchList[i]).name == searchName)
{
return i;
}
}
return -1;
}
}
It works great for my needs (dungeon generator).
plzz help. object position change after combining. is there a way to keep the position same as primary object?
The script assumes the object its combining is at (0,0,0). Add this at the start of Awake():
Vector3 basePosition = transform.position;
Quaternion baseRotation = transform.rotation;
transform.position = Vector3.zero;
transform.rotation = Quaternion.identity;
and this at the end of Awake():
transform.position = basePosition;
transform.rotation = baseRotation;
This works great for a small number of objects but once you get into the 1000s it starts to fail. Any idea how to make it more robust for huge maps?
What do you mean by "fail"? If you use a $$anonymous$$esh with a 16 bit index buffer, yes it would fail at 64k vertices since that's the limit. In the past Unity only supported 16 bit index buffers however we now have 32 bit index buffer support. Though you have to switch to 32 bit yourself. See indexFormat for more details.
For anyone still finding this, I've rewritten the script above in a neater, safer fashion with the ability to ignore certain gameobjects, you can find it here.
Thanks Bunzaga.
Ha, with the new comment from Plapi100, and with the years of experience I've had with Unity since the original carnation of this, I also rewrote a similar implementation. :D I also added a secondary method, where you can pass in the $$anonymous$$eshFilter and $$anonymous$$eshRenderer arrays, and it will take those and generate a mesh for you. Essentially, I've cached some filters and renderers on a ScriptableObject, and can use them to generate meshes, without needing a source game object.
I like your options to ignore objects if wanted, and not to destroy the original.
I figured you would've. 2016 was a long time ago! Your old code saved me a bunch of time and effort, so I just thought I'd pay it forward as best I could.
Answer by aroha · May 07, 2013 at 05:39 AM
I used Zergling103's script (thanks by the way!) but I was having problems with the position of the generated mesh not being in the same location. This seemed to be only the case when the object was not at world coordinates (0,0,0).
I also removed the Objects[] variable and changed it so the script will just combine all meshes and materials on the object that is attached to.
You will need to manually call Combine() to execute it, or right-click the script in the inspector and choose Combine from the menu.
using UnityEngine; using System; using System.Collections; using System.Collections.Generic;
public class CombineMeshes : MonoBehaviour { [ContextMenu("Combine")] public void Combine () { // Find all mesh filter submeshes and separate them by their cooresponding materials ArrayList materials = new ArrayList (); ArrayList combineInstanceArrays = new ArrayList (); Matrix4x4 myTransform = transform.worldToLocalMatrix;
MeshFilter[] meshFilters = this.gameObject.GetComponentsInChildren<MeshFilter> ();
foreach (MeshFilter meshFilter in meshFilters) {
MeshRenderer meshRenderer = meshFilter.GetComponent<MeshRenderer> ();
// Handle bad input
if (!meshRenderer) {
Debug.LogError ("MeshFilter does not have a coresponding MeshRenderer.");
continue;
}
if (meshRenderer.materials.Length != meshFilter.sharedMesh.subMeshCount) {
Debug.LogError ("Mismatch between material count and submesh count. Is this the correct MeshRenderer?");
continue;
}
for (int s = 0; s < meshFilter.sharedMesh.subMeshCount; s++) {
int materialArrayIndex = 0;
for (materialArrayIndex = 0; materialArrayIndex < materials.Count; materialArrayIndex++) {
if (materials [materialArrayIndex] == meshRenderer.sharedMaterials [s])
break;
}
if (materialArrayIndex == materials.Count) {
materials.Add (meshRenderer.sharedMaterials [s]);
combineInstanceArrays.Add (new ArrayList ());
}
CombineInstance combineInstance = new CombineInstance ();
combineInstance.transform = myTransform * meshRenderer.transform.localToWorldMatrix;
combineInstance.subMeshIndex = s;
combineInstance.mesh = meshFilter.sharedMesh;
(combineInstanceArrays [materialArrayIndex] as ArrayList).Add (combineInstance);
}
}
// For MeshFilter
{
// Get / Create mesh filter
MeshFilter meshFilterCombine = gameObject.GetComponent<MeshFilter> ();
if (!meshFilterCombine)
meshFilterCombine = gameObject.AddComponent<MeshFilter> ();
// Combine by material index into per-material meshes
// also, Create CombineInstance array for next step
Mesh[] meshes = new Mesh[materials.Count];
CombineInstance[] combineInstances = new CombineInstance[materials.Count];
for (int m = 0; m < materials.Count; m++) {
CombineInstance[] combineInstanceArray = (combineInstanceArrays [m] as ArrayList).ToArray (typeof(CombineInstance)) as CombineInstance[];
meshes [m] = new Mesh ();
meshes [m].CombineMeshes (combineInstanceArray, true, true);
combineInstances [m] = new CombineInstance ();
combineInstances [m].mesh = meshes [m];
combineInstances [m].subMeshIndex = 0;
}
// Combine into one
meshFilterCombine.sharedMesh = new Mesh ();
meshFilterCombine.sharedMesh.CombineMeshes (combineInstances, false, false);
// Destroy other meshes
foreach (Mesh mesh in meshes) {
mesh.Clear ();
DestroyImmediate (mesh);
}
}
// For MeshRenderer
{
// Get / Create mesh renderer
MeshRenderer meshRendererCombine = gameObject.GetComponent<MeshRenderer> ();
if (!meshRendererCombine)
meshRendererCombine = gameObject.AddComponent<MeshRenderer> ();
// Assign materials
Material[] materialsArray = materials.ToArray (typeof(Material)) as Material[];
meshRendererCombine.materials = materialsArray;
}
}
}
A little rough around the edges, but it seems to have the necessary components. Need to talk to you about some alterations I have in $$anonymous$$d to see if you can help me with my algorithm.
Answer by Zergling103 · Dec 20, 2011 at 05:57 AM
Here is a component which combines meshes as I had originally intended - it works exactly as CombineMeshes but while taking into account different materials across different instances.
Note that it uses ArrayList instead of fixed sized arrays, which might be slower. But the alternative - getting the array size first - would require searching through the materials and submeshes twice, once for the array size, and again for filling the array. That probably would have wound up being slower.
Code is under construction - the only issue is that after you reference meshRenderer.materials[] or sharedMaterials[] the original instances are replaced with unique clones and thus materials[materialArrayIndex] == meshRenderer.sharedMaterials[s] will always be false. :(
using UnityEngine;
using System;
using System.Collections;
public class CombineMeshes : MonoBehaviour {
public GameObject[] Objects;
[ContextMenu("Combine")]
public void Combine()
{
// Find all mesh filter submeshes and separate them by their cooresponding materials
ArrayList materials = new ArrayList();
ArrayList combineInstanceArrays = new ArrayList();
foreach( GameObject obj in Objects )
{
if(!obj)
continue;
MeshFilter[] meshFilters = obj.GetComponentsInChildren<MeshFilter>();
foreach( MeshFilter meshFilter in meshFilters )
{
MeshRenderer meshRenderer = meshFilter.GetComponent<MeshRenderer>();
// Handle bad input
if(!meshRenderer) {
Debug.LogError("MeshFilter does not have a coresponding MeshRenderer.");
continue;
}
if(meshRenderer.materials.Length != meshFilter.sharedMesh.subMeshCount) {
Debug.LogError("Mismatch between material count and submesh count. Is this the correct MeshRenderer?");
continue;
}
for(int s = 0; s < meshFilter.sharedMesh.subMeshCount; s++)
{
int materialArrayIndex = 0;
for(materialArrayIndex = 0; materialArrayIndex < materials.Count; materialArrayIndex++)
{
if(materials[materialArrayIndex] == meshRenderer.sharedMaterials[s])
break;
}
if(materialArrayIndex == materials.Count)
{
materials.Add(meshRenderer.sharedMaterials[s]);
combineInstanceArrays.Add(new ArrayList());
}
CombineInstance combineInstance = new CombineInstance();
combineInstance.transform = meshRenderer.transform.localToWorldMatrix;
combineInstance.subMeshIndex = s;
combineInstance.mesh = meshFilter.sharedMesh;
(combineInstanceArrays[materialArrayIndex] as ArrayList).Add( combineInstance );
}
}
}
// For MeshFilter
{
// Get / Create mesh filter
MeshFilter meshFilterCombine = gameObject.GetComponent<MeshFilter>();
if(!meshFilterCombine)
meshFilterCombine = gameObject.AddComponent<MeshFilter>();
// Combine by material index into per-material meshes
// also, Create CombineInstance array for next step
Mesh[] meshes = new Mesh[materials.Count];
CombineInstance[] combineInstances = new CombineInstance[materials.Count];
for( int m = 0; m < materials.Count; m++ )
{
CombineInstance[] combineInstanceArray = (combineInstanceArrays[m] as ArrayList).ToArray(typeof(CombineInstance)) as CombineInstance[];
meshes[m] = new Mesh();
meshes[m].CombineMeshes( combineInstanceArray, true, true );
combineInstances[m] = new CombineInstance();
combineInstances[m].mesh = meshes[m];
combineInstances[m].subMeshIndex = 0;
}
// Combine into one
meshFilterCombine.sharedMesh = new Mesh();
meshFilterCombine.sharedMesh.CombineMeshes( combineInstances, false, false );
// Destroy other meshes
foreach( Mesh mesh in meshes )
{
mesh.Clear();
DestroyImmediate(mesh);
}
}
// For MeshRenderer
{
// Get / Create mesh renderer
MeshRenderer meshRendererCombine = gameObject.GetComponent<MeshRenderer>();
if(!meshRendererCombine)
meshRendererCombine = gameObject.AddComponent<MeshRenderer>();
// Assign materials
Material[] materialsArray = materials.ToArray(typeof(Material)) as Material[];
meshRendererCombine.materials = materialsArray;
}
}
}
:)
I managed to fix the issue you had.
for (int s = 0; s < meshFilter.shared$$anonymous$$esh.sub$$anonymous$$eshCount; s++) {
int materialArrayIndex = Contains (materials, meshRenderer.shared$$anonymous$$aterials [s].name);
if (materialArrayIndex == -1) {
materials.Add (meshRenderer.shared$$anonymous$$aterials [s]);
materialArrayIndex = materials.Count - 1;
}
combineInstanceArrays.Add (new ArrayList ());
CombineInstance combineInstance = new CombineInstance ();
combineInstance.transform = meshRenderer.transform.localToWorld$$anonymous$$atrix;
combineInstance.sub$$anonymous$$eshIndex = s;
combineInstance.mesh = meshFilter.shared$$anonymous$$esh;
(combineInstanceArrays [materialArrayIndex] as ArrayList).Add (combineInstance);
}
private int Contains (ArrayList searchList, string searchName)
{
for (int i = 0; i < searchList.Count; i++) {
if ((($$anonymous$$aterial)searchList [i]).name == searchName) {
return i;
}
}
return -1;
}
Hope this can be of some help to someone :)
@ Benja$$anonymous$$ 247 Hi, I'm trying to figure this out, where in the code is this supposed to go?
@Sondre.S Hi the for-loop in my comment is meant to replace the for-loop section starting at line 36 in the above answer. The method can go anywhere in your script.
Thanks, but I get this error(I know you get this if you access renderer.material in the editor, so how are you supposed to get this to work, I mean after all it is written as an editor script):
"Instantiating material due to calling renderer.material during edit mode. This will leak materials into the scene. You most likely want to use renderer.shared$$anonymous$$aterial ins$$anonymous$$d.
UnityEngine.Renderer:get_materials()
Combine$$anonymous$$eshes:Combine() (at Assets/Standard Assets/Scripts/Utility Scripts/Combine$$anonymous$$eshes.cs:48)"
Ins$$anonymous$$d of using renderer.materials you use renderer.shared$$anonymous$$aterials. If that does not fix your problem, post the line in your code that gives the problem (in your case line 48)
Answer by baha · May 04, 2013 at 09:22 PM
If you want a tool that do combine meshes and materials as well generating a texture atlas then GameDraw might be helpful to you!
Here is a video showing the combine meshes and materials feature in action: http://www.youtube.com/watch?v=glqbWsc4CXQ
and here is the link to the asset store: https://www.assetstore.unity3d.com/#/content/2811
Your answer
Follow this Question
Related Questions
Materials and Preformance on a Massive scale... 0 Answers
Why does the same material render differently here? 1 Answer
How do I use the same material on 2 meshes without distortion? 2 Answers
How to use multiple Unity materials on external meshes 0 Answers
The best way to have multiple models with only one material 2 Answers