Separating submeshes into unique meshes?

When I import a model, I use OnPostprocessModel to modify the meshes. Some of the meshes will have submeshes. As I am unable to edit these submeshes outside of Unity, I would like to be able to make each submesh its own individual mesh with only one material. Is there any way to do this?

Submeshes still share the same vertices. A submesh simply has a seperate indices list. The easiest solution is to create a seperate gameobject / mesh for each submesh and assign each the same vertices array. Now you only need to call [GetTriangles][1] for each submesh and assign the resulting arrays to each mesh instance.

The problem here is that each mesh might have some overhead vertices which aren’t used in the individual mesh instances.

To solve that you would need to “remap” the indices and only copy the used vertices.

So as a simple example if a sub mesh is a single triangle with the indices 20, 25 and 5 in the original mesh, you would need to create a new vertices array with 3 members and copy those 3 vertex elements over. The new indices / triangle array would need to be adjusted. Since the element “0” in the new vertices array is the old element 20, element “1” is the old “25” and “2” is the old “5”, Your indices / triangle array would simply be “0, 1, 2”.

This remapping is a bit tricky but not too complicated. A Dictionary would be handy to do the remapping. It basically works like this:

  • Get the triangle list for your submesh
  • iterate through all indices and for each, check if the index is already in the dictionary.
  • If it is already known, use the dictionary to get the “new index” for that vertex.
  • If it’s not in the dictionary, copy the vertex from the original array into your new vertex list. In addition you store the old index into the dictionary along with the new one. The new index is the position of that vertex in the new list.
  • Finally you create a new index / triangle list and use the same dictionary to map the indices.

edit
I’ve just written a Mesh class extentions that can extract a submesh with all it’s properties. It does only copy the relevant vertices and only the used vertex attributes.

using UnityEngine;
using System.Collections.Generic;

public static class MeshExtension
{
    private class Vertices
    {
        List<Vector3> verts = null;
        List<Vector2> uv1 = null;
        List<Vector2> uv2 = null;
        List<Vector2> uv3 = null;
        List<Vector2> uv4 = null;
        List<Vector3> normals = null;
        List<Vector4> tangents = null;
        List<Color32> colors = null;
        List<BoneWeight> boneWeights = null;

        public Vertices()
        {
            verts = new List<Vector3>();
        }
        public Vertices(Mesh aMesh)
        {
            verts = CreateList(aMesh.vertices);
            uv1 = CreateList(aMesh.uv);
            uv2 = CreateList(aMesh.uv2);
            uv3 = CreateList(aMesh.uv3);
            uv4 = CreateList(aMesh.uv4);
            normals = CreateList(aMesh.normals);
            tangents = CreateList(aMesh.tangents);
            colors = CreateList(aMesh.colors32);
            boneWeights = CreateList(aMesh.boneWeights);
        }

        private List<T> CreateList<T>(T[] aSource)
        {
            if (aSource == null || aSource.Length == 0)
                return null;
            return new List<T>(aSource);
        }
        private void Copy<T>(ref List<T> aDest, List<T> aSource, int aIndex)
        {
            if (aSource == null)
                return;
            if (aDest == null)
                aDest = new List<T>();
            aDest.Add(aSource[aIndex]);
        }
        public int Add(Vertices aOther, int aIndex)
        {
            int i = verts.Count;
            Copy(ref verts, aOther.verts, aIndex);
            Copy(ref uv1, aOther.uv1, aIndex);
            Copy(ref uv2, aOther.uv2, aIndex);
            Copy(ref uv3, aOther.uv3, aIndex);
            Copy(ref uv4, aOther.uv4, aIndex);
            Copy(ref normals, aOther.normals, aIndex);
            Copy(ref tangents, aOther.tangents, aIndex);
            Copy(ref colors, aOther.colors, aIndex);
            Copy(ref boneWeights, aOther.boneWeights, aIndex);
            return i;
        }
        public void AssignTo(Mesh aTarget)
        {
            if (verts.Count > 65535)
                aTarget.indexFormat = UnityEngine.Rendering.IndexFormat.UInt32;
            aTarget.SetVertices(verts);
            if (uv1 != null) aTarget.SetUVs(0, uv1);
            if (uv2 != null) aTarget.SetUVs(1, uv2);
            if (uv3 != null) aTarget.SetUVs(2, uv3);
            if (uv4 != null) aTarget.SetUVs(3, uv4);
            if (normals != null) aTarget.SetNormals(normals);
            if (tangents != null) aTarget.SetTangents(tangents);
            if (colors != null) aTarget.SetColors(colors);
            if (boneWeights != null) aTarget.boneWeights = boneWeights.ToArray();
        }
    }

    public static Mesh GetSubmesh(this Mesh aMesh, int aSubMeshIndex)
    {
        if (aSubMeshIndex < 0 || aSubMeshIndex >= aMesh.subMeshCount)
            return null;
        int[] indices = aMesh.GetTriangles(aSubMeshIndex);
        Vertices source = new Vertices(aMesh);
        Vertices dest = new Vertices();
        Dictionary<int, int> map = new Dictionary<int, int>();
        int[] newIndices = new int[indices.Length];
        for (int i = 0; i < indices.Length; i++)
        {
            int o = indices*;*

int n;
if (!map.TryGetValue(o, out n))
{
n = dest.Add(source, o);
map.Add(o,n);
}
newIndices = n;
}
Mesh m = new Mesh();
dest.AssignTo(m);
m.triangles = newIndices;
return m;
}
}
With that extention somewhere in your project you can simply do
Mesh subMesh = someMesh.GetSubmesh(0);
Note: I’ve written that extension from scratch. I haven’t tested it yet but it’s syntax-checked and should work. If you find any error / misbehaviour, please leave a comment.
edit
I just added an automatic index format selecting into the “AssignTo” method in order to support Unity’s new 32 bit index buffers if necessary. Note that i just put the limit to the usual 65535. However as far as i remember Unity had a slightly smaller limit (at least when there was only a 16bit index buffer in the past). This should work in most cases, if you want to be sure you may want to run some tests on a recent Unity version to test when the 16bit index buffer starts to throw errors.
_*[1]: https://docs.unity3d.com/ScriptReference/Mesh.GetTriangles.html*_

Expanding on this a bit, here’s the combined version that I wound up using for my own purposes:

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

public class ArcenSubmeshSplitter
{
    //http://answers.unity3d.com/questions/1213025/separating-submeshes-into-unique-meshes.html
    [MenuItem( "Arcen/Submesh Splitter" )]
    public static void BuildWindowsAssetBundle()
    {
        GameObject[] objects = Selection.gameObjects;
        for ( int i = 0; i < objects.Length; i++ )
        {
            ProcessGameObject( objects *);*

}
Debug.Log( "Done splitting meshes into submeshes! " + System.DateTime.Now );
}

public class MeshFromSubmesh
{
public Mesh mesh;
public int id; // Represent the ID of the sub mesh from with the new ‘mesh’ has been created
}

private static void ProcessGameObject( GameObject go )
{
// Isolate Sub Meshes
MeshFilter meshFilterComponent = go.GetComponent();
if ( !meshFilterComponent )
{
Debug.LogError( “MeshFilter null for '” + go.name + “'!” );
return;
}
MeshRenderer meshRendererComponent = go.GetComponent();
if ( !meshRendererComponent )
{
Debug.LogError( “MeshRenderer null for '” + go.name + “'!” );
return;
}
Mesh mesh = go.GetComponent().sharedMesh;
if ( !mesh )
{
Debug.LogError( “Mesh null for '” + go.name + “'!” );
return;
}
List meshFromSubmeshes = GetAllSubMeshAsIsolatedMeshes( mesh );
if ( meshFromSubmeshes == null || meshFromSubmeshes.Count == 0 )
{
Debug.LogError( “List empty or null for '” + go.name + “'!” );
return;
}
string goName = go.name;
for ( int i = 0; i < meshFromSubmeshes.Count; i++ )
{
string meshFromSubmeshName = goName + “sub” + i;
GameObject meshFromSubmeshGameObject = new GameObject();
meshFromSubmeshGameObject.name = meshFromSubmeshName;
meshFromSubmeshGameObject.transform.SetParent( meshFilterComponent.transform );
meshFromSubmeshGameObject.transform.localPosition = Vector3.zero;
meshFromSubmeshGameObject.transform.localRotation = Quaternion.identity;
MeshFilter meshFromSubmeshFilter = meshFromSubmeshGameObject.AddComponent();
meshFromSubmeshFilter.sharedMesh = meshFromSubmeshes*.mesh;*
MeshRenderer meshFromSubmeshMeshRendererComponent = meshFromSubmeshGameObject.AddComponent();
if ( meshRendererComponent != null )
{
// To use the same mesh renderer properties of the initial mesh
EditorUtility.CopySerialized( meshRendererComponent, meshFromSubmeshMeshRendererComponent );
// We just need the only one material used by the sub mesh in its renderer
Material material = meshFromSubmeshMeshRendererComponent.sharedMaterials[meshFromSubmeshes*.id];*
meshFromSubmeshMeshRendererComponent.sharedMaterials = new[] { material };
}
// Don’t forget to save the newly created mesh in the asset database (on disk)
string path = “Assets/_Meshes/Split/” + meshFromSubmeshName + “.asset”;
AssetDatabase.CreateAsset( meshFromSubmeshes*.mesh, path );*
Debug.Log( "Created: " + path );
}
}

private static List GetAllSubMeshAsIsolatedMeshes( Mesh mesh )
{
List meshesToReturn = new List();
if ( !mesh )
{
Debug.LogError( “No mesh passed into GetAllSubMeshAsIsolatedMeshes!” );
return meshesToReturn;
}
int submeshCount = mesh.subMeshCount;
if ( submeshCount < 2 )
{
Debug.LogError( “Only " + submeshCount + " submeshes in mesh passed to GetAllSubMeshAsIsolatedMeshes” );
return meshesToReturn;
}
MeshFromSubmesh m1;
for ( int i = 0; i < submeshCount; i++ )
{
m1 = new MeshFromSubmesh();
m1.id = i;
m1.mesh = mesh.GetSubmesh( i );
meshesToReturn.Add( m1 );
}
return meshesToReturn;
}
}

public static class MeshExtension
{
private class Vertices
{
List verts = null;
List uv1 = null;
List uv2 = null;
List uv3 = null;
List uv4 = null;
List normals = null;
List tangents = null;
List colors = null;
List boneWeights = null;

public Vertices()
{
verts = new List();
}
public Vertices( Mesh aMesh )
{
verts = CreateList( aMesh.vertices );
uv1 = CreateList( aMesh.uv );
uv2 = CreateList( aMesh.uv2 );
uv3 = CreateList( aMesh.uv3 );
uv4 = CreateList( aMesh.uv4 );
normals = CreateList( aMesh.normals );
tangents = CreateList( aMesh.tangents );
colors = CreateList( aMesh.colors32 );
boneWeights = CreateList( aMesh.boneWeights );
}

private List CreateList( T[] aSource )
{
if ( aSource == null || aSource.Length == 0 )
return null;
return new List( aSource );
}
private void Copy( ref List aDest, List aSource, int aIndex )
{
if ( aSource == null )
return;
if ( aDest == null )
aDest = new List();
aDest.Add( aSource[aIndex] );
}
public int Add( Vertices aOther, int aIndex )
{
int i = verts.Count;
Copy( ref verts, aOther.verts, aIndex );
Copy( ref uv1, aOther.uv1, aIndex );
Copy( ref uv2, aOther.uv2, aIndex );
Copy( ref uv3, aOther.uv3, aIndex );
Copy( ref uv4, aOther.uv4, aIndex );
Copy( ref normals, aOther.normals, aIndex );
Copy( ref tangents, aOther.tangents, aIndex );
Copy( ref colors, aOther.colors, aIndex );
Copy( ref boneWeights, aOther.boneWeights, aIndex );
return i;
}
public void AssignTo( Mesh aTarget )
{
aTarget.SetVertices( verts );
if ( uv1 != null ) aTarget.SetUVs( 0, uv1 );
if ( uv2 != null ) aTarget.SetUVs( 1, uv2 );
if ( uv3 != null ) aTarget.SetUVs( 2, uv3 );
if ( uv4 != null ) aTarget.SetUVs( 3, uv4 );
if ( normals != null ) aTarget.SetNormals( normals );
if ( tangents != null ) aTarget.SetTangents( tangents );
if ( colors != null ) aTarget.SetColors( colors );
if ( boneWeights != null ) aTarget.boneWeights = boneWeights.ToArray();
}
}

public static Mesh GetSubmesh( this Mesh aMesh, int aSubMeshIndex )
{
if ( aSubMeshIndex < 0 || aSubMeshIndex >= aMesh.subMeshCount )
return null;
int[] indices = aMesh.GetTriangles( aSubMeshIndex );
Vertices source = new Vertices( aMesh );
Vertices dest = new Vertices();
Dictionary<int, int> map = new Dictionary<int, int>();
int[] newIndices = new int[indices.Length];
for ( int i = 0; i < indices.Length; i++ )
{
int o = indices*;*
int n;
if ( !map.TryGetValue( o, out n ) )
{
n = dest.Add( source, o );
map.Add( o, n );
}
newIndices = n;
}
Mesh m = new Mesh();
dest.AssignTo( m );
m.triangles = newIndices;
return m;
}
}

Here is some code for my previous post.

The code has to be adapted, this is a general example, an extract from my custom importation process.

public class MeshFromSubmesh
{
    public Mesh mesh;
    public int id; // Represent the ID of the sub mesh from with the new 'mesh' has been created
}

private void OnPostprocessModel(GameObject go)
{
    // Isolate Sub Meshes
    MeshFilter meshFilterComponent = go.GetComponent<MeshFilter>();
    MeshRenderer meshRendererComponent = go.GetComponent<MeshRenderer>();
    Mesh mesh = go.GetComponent<MeshFilter>().sharedMesh;

    List<MeshFromSubmesh> meshFromSubmeshes = GetAllSubMeshAsIsolatedMeshes(mesh);
    for (int i = 0; i < meshFromSubmeshes.Count; i++)
    {
        string meshFromSubmeshName = "SubMesh_" + i;

        GameObject meshFromSubmeshGameObject = new GameObject();
        meshFromSubmeshGameObject.name = meshFromSubmeshName;
        meshFromSubmeshGameObject.transform.SetParent(meshFilterComponent.transform);
        meshFromSubmeshGameObject.transform.localPosition = Vector3.zero;
        meshFromSubmeshGameObject.transform.localRotation = Quaternion.identity;

        MeshFilter meshFromSubmeshFilter = meshFromSubmeshGameObject.AddComponent<MeshFilter>();
        meshFromSubmeshFilter.sharedMesh = meshFromSubmeshes*.mesh;*

MeshRenderer meshFromSubmeshMeshRendererComponent = meshFromSubmeshGameObject.AddComponent();
if (meshRendererComponent != null)
{
// To use the same mesh renderer properties of the initial mesh
EditorUtility.CopySerialized(meshRendererComponent, meshFromSubmeshMeshRendererComponent);
// We just need the only one material used by the sub mesh in its renderer
Material material = meshFromSubmeshMeshRendererComponent.sharedMaterials[meshFromSubmeshes*.id];*
meshFromSubmeshMeshRendererComponent.sharedMaterials = new[] { material };
}
// Don’t forget to save the newly created mesh in the asset database (on disk)
AssetDatabase.CreateAsset(meshFromSubmeshes_.mesh, “YourAssetFolder/IsolatedSubmeshes/YourAssetName” + “@” + mesh.name + “@” + meshFromSubmeshName + “.asset”);
}
}_

Excellent solution thanks !

However, don’t forget to set the materials correctly on the new meshes’ renderers.

For example, if the initial mesh had 2 materials in its mesh renderer (aka 2 IDs in 3DMax), you must setup the mesh renderer for the first created mesh (from first submesh) to use ONLY the 1st material. And the mesh renderer for the second created mesh (from second submesh) to use ONLY the 2nd material.

Hi, @Bunny83 !

I need a “split submeshes” function for a texture baking system I’m developing, and I find your script very useful for this purpose.

But I have a problem with it.
As you can see in the following image, the right model (generated with your script by splitting the left one) has some wrong triangles. Could you please help me to fix this?

Thanks,
Francesco

This is amazing, thank you so much for sharing this!