Seeking Assistance to MeshCombineUtility

Hi All

I’m new to Unity3D and happened to use the “improved meshcombineutility” that superpig have posted on 2011, may I check if there’s any update to the code other than the changing of Get/SetTriangleStripes to Get/SetTriangles that have been highlighted from 2015?

I have changed that in my current project and am facing the error of

“Failed setting triangles. The number of supplied triangle indices must be a multiple of 3. UnityEngine.Mesh:SetTriangles (int[ ],int)”

This points to the following places:
“MeshCombineUtility:CreateCombinedMesh () (At line 187 of MeshCombineUtility)
MeshCombineUtility:Combine (System.Collections.Generic.IEnumerable`1<MeshCombineUtility/MeshInstance>,bool) (At line 208 of MeshCombineUtility)
CombineChildren.Combine () (Line 92 CombineChildren)
CombineChildren:Start () (Line 23 CombineChildren)”

As well as

“Non-convex MeshCollider with non-kinematic RigidBody is no longer supported since Unity 5. If you want to use a non-convex mesh either make the Rigidbody kinematic or remove the Rigidbody component.”

Thanks in advance!

MeshCombineUtility.cs

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

public class MeshCombineUtility
{
    private readonly List<Color> _colors = new List<Color>();
    private readonly bool _generateStrips;
    private readonly List<Vector3> _normals = new List<Vector3>();
    private readonly Dictionary<int, List<int>> _strip = new Dictionary<int, List<int>>();
    private readonly List<Vector4> _tangents = new List<Vector4>();
    private readonly Dictionary<int, List<int>> _triangles = new Dictionary<int, List<int>>();
    private readonly List<Vector2> _uv = new List<Vector2>();
    private readonly List<Vector2> _uv1 = new List<Vector2>();
    private readonly List<Vector3> _vertices = new List<Vector3>();
   
    /// <summary>
    /// Creates a new, empty MeshCombineUtility object for combining meshes.
    /// </summary>
    /// <param name="generateStrips">true if the meshes you're going to combine are all triangle-strip based; false if you just want to use triangle lists.</param>
    public MeshCombineUtility(bool generateStrips)
    {
        _generateStrips = generateStrips;
    }
   
    /// <summary>
    /// Allocate space for adding a load more vertex data.
    /// </summary>
    /// <param name="numVertices">The number of vertices you're about to add.</param>
    public void PrepareForAddingVertices(int numVertices)
    {
        int shortfall = numVertices - (_vertices.Capacity - _vertices.Count);
       
        _vertices.Capacity += shortfall;
        _normals.Capacity += shortfall;
        _tangents.Capacity += shortfall;
        _uv.Capacity += shortfall;
        _uv1.Capacity += shortfall;
        _colors.Capacity += shortfall;
    }
   
    /// <summary>
    /// Allocate space for adding a load more triangles to the triangle list.
    /// </summary>
    /// <param name="targetSubmeshIndex">The index of the submesh that you're going to add triangles to.</param>
    /// <param name="numIndices">The number of triangle indicies (number of triangles * 3) that you want to reserve space for.</param>
    public void PrepareForAddingTriangles(int targetSubmeshIndex, int numIndices)
    {
        if (!_triangles.ContainsKey(targetSubmeshIndex))
            _triangles.Add(targetSubmeshIndex, new List<int>());
       
        int shortfall = numIndices - (_triangles[targetSubmeshIndex].Capacity - _triangles[targetSubmeshIndex].Count);
        _triangles[targetSubmeshIndex].Capacity += shortfall;
    }
   
    /// <summary>
    /// Allocate space for adding a load more triangle strips to the combiner.
    /// </summary>
    /// <param name="targetSubmeshIndex">The index of the submesh you're going to add strips to.</param>
    /// <param name="stripLengths">A sequence of strip lengths (in number-of-indices). These numbers will be used to automatically calculate how many bridging indices need to be added.</param>
    public void PrepareForAddingStrips(int targetSubmeshIndex, IEnumerable<int> stripLengths)
    {
        if (!_strip.ContainsKey(targetSubmeshIndex))
            _strip.Add(targetSubmeshIndex, new List<int>());
       
        int requiredCapacity = _strip[targetSubmeshIndex].Count;
       
        foreach (int srcStripLength in stripLengths)
        {
            int adjStripLength = srcStripLength;
            if (requiredCapacity > 0)
                if ((requiredCapacity & 1) == 1)
                    adjStripLength += 3;
            else
                adjStripLength += 2;
            requiredCapacity += adjStripLength;
        }
       
        if (_strip[targetSubmeshIndex].Capacity < requiredCapacity)
            _strip[targetSubmeshIndex].Capacity = requiredCapacity;
    }
   
    /// <summary>
    /// Add a mesh instance to the combiner.
    /// </summary>
    /// <param name="instance">The mesh instance to add.</param>
    public void AddMeshInstance(MeshInstance instance)
    {
        int baseVertexIndex = _vertices.Count;
       
        PrepareForAddingVertices(instance.mesh.vertexCount);
       
        _vertices.AddRange(instance.mesh.vertices.Select(v => instance.transform.MultiplyPoint(v)));
        _normals.AddRange(
            instance.mesh.normals.Select(n => instance.transform.inverse.transpose.MultiplyVector(n).normalized));
        _tangents.AddRange(instance.mesh.tangents.Select(t =>
                                                         {
            var p = new Vector3(t.x, t.y, t.z);
            p =
                instance.transform.inverse.transpose.
                    MultiplyVector(p).normalized;
            return new Vector4(p.x, p.y, p.z, t.w);
        }));
        _uv.AddRange(instance.mesh.uv);
        _uv1.AddRange(instance.mesh.uv2);
        _colors.AddRange(instance.mesh.colors);
       
        if (_generateStrips)
        {
            int[] inputstrip = instance.mesh.GetTriangles(instance.subMeshIndex);
            PrepareForAddingStrips(instance.targetSubMeshIndex, new[] {inputstrip.Length});
            List<int> outputstrip = _strip[instance.targetSubMeshIndex];
            if (outputstrip.Count != 0)
            {
                if ((outputstrip.Count & 1) == 1)
                {
                    outputstrip.Add(outputstrip[outputstrip.Count - 1]);
                    outputstrip.Add(inputstrip[0] + baseVertexIndex);
                    outputstrip.Add(inputstrip[0] + baseVertexIndex);
                }
                else
                {
                    outputstrip.Add(outputstrip[outputstrip.Count - 1]);
                    outputstrip.Add(inputstrip[0] + baseVertexIndex);
                }
            }
           
            outputstrip.AddRange(inputstrip.Select(s => s + baseVertexIndex));
        }
        else
        {
            int[] inputtriangles = instance.mesh.GetTriangles(instance.subMeshIndex);
            PrepareForAddingTriangles(instance.targetSubMeshIndex, inputtriangles.Length);
            _triangles[instance.targetSubMeshIndex].AddRange(inputtriangles.Select(t => t + baseVertexIndex));
        }
    }
   
    /// <summary>
    /// Add multiple mesh instances to the combiner, allocating space for them all up-front.
    /// </summary>
    /// <param name="instances">The instances to add.</param>
    public void AddMeshInstances(IEnumerable<MeshInstance> instances)
    {
        instances = instances.Where(instance => instance.mesh);
       
        PrepareForAddingVertices(instances.Sum(instance => instance.mesh.vertexCount));
       
        foreach (var targetSubmesh in instances.GroupBy(instance => instance.targetSubMeshIndex))
        {
            if (_generateStrips)
            {
                PrepareForAddingStrips(targetSubmesh.Key,
                                       targetSubmesh.Select(instance => instance.mesh.GetTriangles(instance.subMeshIndex).Length));
            }
            else
            {
                PrepareForAddingTriangles(targetSubmesh.Key,
                                          targetSubmesh.Sum(instance => instance.mesh.GetTriangles(instance.subMeshIndex).Length));
            }
        }
       
        foreach (MeshInstance instance in instances)
            AddMeshInstance(instance);
    }
   
    /// <summary>
    /// Generate a single mesh from the instances that have been added to the combiner so far.
    /// </summary>
    /// <returns>A combined mesh.</returns>
    public Mesh CreateCombinedMesh()
    {
        var mesh = new Mesh
        {
            name = "Combined Mesh",
            vertices = _vertices.ToArray(),
            normals = _normals.ToArray(),
            colors = _colors.ToArray(),
            uv = _uv.ToArray(),
            uv2 = _uv1.ToArray(),
            tangents = _tangents.ToArray(),
            subMeshCount = (_generateStrips) ? _strip.Count : _triangles.Count
        };
       
        if (_generateStrips)
        {
            foreach (var targetSubmesh in _strip)
                mesh.SetTriangles(targetSubmesh.Value.ToArray(), targetSubmesh.Key); /* */
        }
        else
        {
            foreach (var targetSubmesh in _triangles)
                mesh.SetTriangles(targetSubmesh.Value.ToArray(), targetSubmesh.Key);
        }
       
        return mesh;
    }
   
    /// <summary>
    /// Combine the given mesh instances into a single mesh and return it.
    /// </summary>
    /// <param name="instances">The mesh instances to combine.</param>
    /// <param name="generateStrips">true to use triangle strips, false to use triangle lists.</param>
    /// <returns>A combined mesh.</returns>
    public static Mesh Combine(IEnumerable<MeshInstance> instances, bool generateStrips)
    {
        var processor = new MeshCombineUtility(generateStrips);
        processor.AddMeshInstances(instances);
        return processor.CreateCombinedMesh(); /* */
    }
   
    #region Nested type: MeshInstance
   
    public class MeshInstance
    {
        /// <summary>
        /// The source mesh.
        /// </summary>
        public Mesh mesh;
       
        /// <summary>
        /// The submesh from the source mesh that you want to add to the combiner.
        /// </summary>
        public int subMeshIndex;
       
        /// <summary>
        /// The submesh that you want this instance to be combined into. Group instances that should
        /// share a material into the same target submesh index.
        /// </summary>
        public int targetSubMeshIndex;
       
        /// <summary>
        /// The instance transform.
        /// </summary>
        public Matrix4x4 transform;
    }
   
    #endregion
}

CombineChildren.cs

using UnityEngine;
using System.Collections;


/*
Attach this script as a parent to some game objects. The script will then combine the meshes at startup.
This is useful as a performance optimization since it is faster to render one big mesh than many small meshes. See the docs on graphics performance optimization for more info.

Different materials will cause multiple meshes to be created, thus it is useful to share as many textures/material as you can.
*/
//[ExecuteInEditMode()]
[AddComponentMenu("Mesh/Combine Children")]
public class CombineChildren : MonoBehaviour {
   
    /// Usually rendering with triangle strips is faster.
    /// However when combining objects with very low triangle counts, it can be faster to use triangles.
    /// Best is to try out which value is faster in practice.
    public int frameToWait = 0;
    public bool generateTriangleStrips = true, combineOnStart = true, destroyAfterOptimized = false, castShadow = true, receiveShadow = true, keepLayer = true, addMeshCollider = false;
   
    void Start()
    {
        if (combineOnStart && frameToWait == 0) Combine();
        else StartCoroutine(CombineLate());
    }
   
    IEnumerator CombineLate()
    {
        for (int i = 0; i < frameToWait; i++ ) yield return 0;
        Combine();
    }
   
    [ContextMenu("Combine Now on Childs")]
    public void CallCombineOnAllChilds()
    {
        CombineChildren[] c = gameObject.GetComponentsInChildren<CombineChildren>();
        int count = c.Length;
        for (int i = 0; i < count; i++) if(c[i] != this)c[i].Combine();
        combineOnStart = enabled = false;
    }
   
    /// This option has a far longer preprocessing time at startup but leads to better runtime performance.
    [ContextMenu ("Combine Now")]
    public void Combine () {
        Component[] filters  = GetComponentsInChildren(typeof(MeshFilter));
        Matrix4x4 myTransform = transform.worldToLocalMatrix;
        Hashtable materialToMesh= new Hashtable();
       
        for (int i=0;i<filters.Length;i++) {
            MeshFilter filter = (MeshFilter)filters[i];
            Renderer curRenderer  = filters[i].GetComponent<Renderer>();
            MeshCombineUtility.MeshInstance instance = new MeshCombineUtility.MeshInstance ();
            instance.mesh = filter.sharedMesh;
            if (curRenderer != null && curRenderer.enabled && instance.mesh != null) {
                instance.transform = myTransform * filter.transform.localToWorldMatrix;
               
                Material[] materials = curRenderer.sharedMaterials;
                for (int m=0;m<materials.Length;m++) {
                    instance.subMeshIndex = System.Math.Min(m, instance.mesh.subMeshCount - 1);
                   
                    ArrayList objects = (ArrayList)materialToMesh[materials[m]];
                    if (objects != null) {
                        objects.Add(instance);
                    }
                    else
                    {
                        objects = new ArrayList ();
                        objects.Add(instance);
                        materialToMesh.Add(materials[m], objects);
                    }
                }
                if (Application.isPlaying && destroyAfterOptimized && combineOnStart) Destroy(curRenderer.gameObject);
                else if (destroyAfterOptimized) DestroyImmediate(curRenderer.gameObject);
                else curRenderer.enabled = false;
            }
        }
       
        foreach (DictionaryEntry de  in materialToMesh) {
            ArrayList elements = (ArrayList)de.Value;
            MeshCombineUtility.MeshInstance[] instances = (MeshCombineUtility.MeshInstance[])elements.ToArray(typeof(MeshCombineUtility.MeshInstance));
           
            // We have a maximum of one material, so just attach the mesh to our own game object
            if (materialToMesh.Count == 1)
            {
                // Make sure we have a mesh filter & renderer
                if (GetComponent(typeof(MeshFilter)) == null)
                    gameObject.AddComponent(typeof(MeshFilter));
                if (!GetComponent("MeshRenderer"))
                    gameObject.AddComponent<MeshRenderer>();
               
                MeshFilter filter = (MeshFilter)GetComponent(typeof(MeshFilter));
                if (Application.isPlaying) filter.mesh = MeshCombineUtility.Combine(instances, generateTriangleStrips);
                else filter.sharedMesh = MeshCombineUtility.Combine(instances, generateTriangleStrips);
                GetComponent<Renderer>().material = (Material)de.Key;
                GetComponent<Renderer>().enabled = true;
                if (addMeshCollider) gameObject.AddComponent<MeshCollider>();
                GetComponent<Renderer>().castShadows = castShadow;
                GetComponent<Renderer>().receiveShadows = receiveShadow;
            }
            // We have multiple materials to take care of, build one mesh / gameobject for each material
            // and parent it to this object
            else
            {
                GameObject go = new GameObject("Combined mesh");
                if (keepLayer) go.layer = gameObject.layer;
                go.transform.parent = transform;
                go.transform.localScale = Vector3.one;
                go.transform.localRotation = Quaternion.identity;
                go.transform.localPosition = Vector3.zero;
                go.AddComponent(typeof(MeshFilter));
                go.AddComponent<MeshRenderer>();
                go.GetComponent<Renderer>().material = (Material)de.Key;
                MeshFilter filter = (MeshFilter)go.GetComponent(typeof(MeshFilter));
                if(Application.isPlaying)filter.mesh = MeshCombineUtility.Combine(instances, generateTriangleStrips);
                else filter.sharedMesh = MeshCombineUtility.Combine(instances, generateTriangleStrips);
                go.GetComponent<Renderer>().castShadows = castShadow;
                go.GetComponent<Renderer>().receiveShadows = receiveShadow;
                if (addMeshCollider) go.AddComponent<MeshCollider>();
            }
        }   
    }   
}

No idea who superpig is but 2015 and 2011 were a long time ago.

You’re welcome to see my take on mesh combinations using that API in my MakeGeo project.

MakeGeo is presently hosted at these locations:

https://bitbucket.org/kurtdekker/makegeo

Otherwise, if you want to debug superpig’s code for him, be my guest. Here’s how to get started:

What is often happening in these cases is one of the following:

  • the code you think is executing is not actually executing at all
  • the code is executing far EARLIER or LATER than you think
  • the code is executing far LESS OFTEN than you think
  • the code is executing far MORE OFTEN than you think
  • the code is executing on another GameObject than you think it is
  • you’re getting an error or warning and you haven’t noticed it in the console window

To help gain more insight into your problem, I recommend liberally sprinkling Debug.Log() statements through your code to display information in realtime.

Doing this should help you answer these types of questions:

  • is this code even running? which parts are running? how often does it run? what order does it run in?
  • what are the values of the variables involved? Are they initialized? Are the values reasonable?
  • are you meeting ALL the requirements to receive callbacks such as triggers / colliders (review the documentation)

Knowing this information will help you reason about the behavior you are seeing.

You can also supply a second argument to Debug.Log() and when you click the message, it will highlight the object in scene, such as Debug.Log("Problem!",this);

If your problem would benefit from in-scene or in-game visualization, Debug.DrawRay() or Debug.DrawLine() can help you visualize things like rays (used in raycasting) or distances.

You can also call Debug.Break() to pause the Editor when certain interesting pieces of code run, and then study the scene manually, looking for all the parts, where they are, what scripts are on them, etc.

You can also call GameObject.CreatePrimitive() to emplace debug-marker-ish objects in the scene at runtime.

You could also just display various important quantities in UI Text elements to watch them change as you play the game.

If you are running a mobile device you can also view the console output. Google for how on your particular mobile target, such as this answer or iOS: How To - Capturing Device Logs on iOS or this answer for Android: How To - Capturing Device Logs on Android

Another useful approach is to temporarily strip out everything besides what is necessary to prove your issue. This can simplify and isolate compounding effects of other items in your scene or prefab.

Here’s an example of putting in a laser-focused Debug.Log() and how that can save you a TON of time wallowing around speculating what might be going wrong:

When in doubt, print it out!™

Note: the print() function is an alias for Debug.Log() provided by the MonoBehaviour class.

You must find a way to get the information you need in order to reason about what the problem is.

I think he means this .
I don’t know if that’s the same person (sure it has Unity badge), but a superpig seems to have become a principal engineer at unity.

2 Likes

Hi, thanks for that, will take a look through and hopefully resolve my issue. Will update again if I faced any doubts during the troubleshoots.

Indeed, I meant for him, I was planning to directly DM him to see if there’s any support for it but unable to do it.

Seeing this error, I gotta ask… did you copy / paste this code or retype it? If you retyped it, just delete what you got and go get the code afresh from copy / paste, because it seems unlikely an engine change would ever result in the above bug.

I didn’t copy/paste or retyped it since it was an imported project, but with old unity version that’s initially used for it and since Unity forces people to upgrade to Unity 5, it got upgraded but didn’t specifically update the respective API appropriately I supposed.

Will also try out the copy/paste method as well and provide an update. Note that this error only comes up when I tried to “Run” the environment.

The error is a bookkeeping or counting or organizational error…

That means this code is counting or enumerating differently, assuming that the code USED to work.

It seems a stretch how a Unity API change could cause a bookkeeping problem, but perhaps. I just can’t imagine how.

This however is something that changed. I remember this was because of performance, people abused the non-convex colliders a lot and then blamed the engine for being slow. I’m not sure what’s the current state of this, can’t remember.

1 Like

True but I think that only requires tick-marking the convex bool in the MeshCollider, correct?

I was just saying I don’t think a triangle count mistake could happen from an engine upgrade, that’s all.

A triangle count bug sounds like my own code, or a typo. :slight_smile: (yes I have seen the triangle count error a LOT)

2 Likes

True true, that stands alright. It’s hardly an upgrade error.

1 Like

Right, I went to tick that bool for Convex and it fix(?) the below stated error. (At least it doesn’t show up anymore during the “Run” time.

So now I’m currently left with this.

I have tried the copy/pasting method that Kurt mentioned but to no avail.

The number of triangle indices must be evenly divisible by 3

Go to where that property is set and find out why. See notes above, recopied her for you to get started. This is debugging.

You must find a way to get the information you need in order to reason about what the problem is.

What is often happening in these cases is one of the following:

  • the code you think is executing is not actually executing at all
  • the code is executing far EARLIER or LATER than you think
  • the code is executing far LESS OFTEN than you think
  • the code is executing far MORE OFTEN than you think
  • the code is executing on another GameObject than you think it is
  • you’re getting an error or warning and you haven’t noticed it in the console window

To help gain more insight into your problem, I recommend liberally sprinkling Debug.Log() statements through your code to display information in realtime.

Doing this should help you answer these types of questions:

  • is this code even running? which parts are running? how often does it run? what order does it run in?
  • what are the values of the variables involved? Are they initialized? Are the values reasonable?
  • are you meeting ALL the requirements to receive callbacks such as triggers / colliders (review the documentation)

Knowing this information will help you reason about the behavior you are seeing.

You can also supply a second argument to Debug.Log() and when you click the message, it will highlight the object in scene, such as Debug.Log("Problem!",this);

If your problem would benefit from in-scene or in-game visualization, Debug.DrawRay() or Debug.DrawLine() can help you visualize things like rays (used in raycasting) or distances.

You can also call Debug.Break() to pause the Editor when certain interesting pieces of code run, and then study the scene manually, looking for all the parts, where they are, what scripts are on them, etc.

You can also call GameObject.CreatePrimitive() to emplace debug-marker-ish objects in the scene at runtime.

You could also just display various important quantities in UI Text elements to watch them change as you play the game.

If you are running a mobile device you can also view the console output. Google for how on your particular mobile target, such as this answer or iOS: How To - Capturing Device Logs on iOS or this answer for Android: How To - Capturing Device Logs on Android

Another useful approach is to temporarily strip out everything besides what is necessary to prove your issue. This can simplify and isolate compounding effects of other items in your scene or prefab.

Here’s an example of putting in a laser-focused Debug.Log() and how that can save you a TON of time wallowing around speculating what might be going wrong:

When in doubt, print it out!™

Note: the print() function is an alias for Debug.Log() provided by the MonoBehaviour class.

I have sort of found out the issue already that pin-point to the Mesh used namely “Capsule”, it contains 832 triangle that is not divisible by 3. However, the Mesh “Capsule” comes from the default resources within Unity itself. Does that meant that I would have to change the Mesh material(? unsure of the naming convention) itself to something that’s divisible by 3?

Add on: Guess not… Changing to something that contains multiple of 3 triangles doesnt solve the issue.

do you know how meshes work in general? you don’t need the number of triangles to be divisible by 3. triangles consist of 3 vertices each, as per their geometric definition, thus a “triangle list” (a list of indices referring to individual vertices from which the triangles are created) must be divisible by 3.

and if this is something Unity reports as an error, then it’s a hard fact that this list somehow has not enough information, it has nothing to do with any capsule, especially not default models. something is w-r-o-n-g with your data. period.

I’d go with Kurt’s suggestion and roll up the sleeves and seek the source of this error. there is no other magical way of knowing what went awry and everything suggests it’s the data you’re working with. it could be a deep error, a malfunction of sorts, but most likely it’s some silly mistake. isolate the unknowns, use debugging tools as well as deduction to narrow down a potential problem. in programming, the most common error is by assuming that something works when it doesn’t. BY FAR the most common error. in my experience, that’s the case 80%+ of the time, I am almost pleasantly surprised when the other 20% turns up (I’m not actually, it’s usually some horrible Cthulhu mystery, but you get the point; your case doesn’t smell like a mystery to me).

And in this pursuit, make two ULTRA simple one-triangle models and try to merge them.

That way you literally can step through six vertices, two triangles using 6 vertex entries, etc

1 Like

Bookkeeping is an important consideration when working with MeshCombineUtility, as it can help you to make sure that your meshes are correctly combined and integrated. Here are some tips for best bookkeeping practices when using MeshCombineUtility:

  1. Keep a log: Make sure to keep a log of the meshes you have combined with MeshCombineUtility, as it can help you to easily keep track of them. Logging the date when you combined the meshes, and noting any potential problems, can provide valuable context if you need to go back and make changes.

  2. Label your meshes: Labeling your meshes with a unique identifier when you combine them can help you to easily distinguish them from other meshes. This can be especially helpful if you are combining multiple meshes, so that you can keep track of each one.

  3. Test and troubleshoot: Testing and troubleshooting your meshes after combining them with MeshCombineUtility can help to identify any potential issues that might arise. This can help you to ensure that your meshes are correctly combined and integrated.

Following these tips for best bookkeeping practices can help to ensure that you have a successful experience when using MeshCombineUtility. If you have any further questions or need any additional assistance, please don’t hesitate to reach out.

That sounds like an AI generated essay about the MeshCombineUtility… What’s the point of this nonsense?

2 Likes