- Home /
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?
A GameObject can only have one mesh (because it can only have one renderer). So you at least have to use shared mateirals.
It should be possible to make meshes from submeshes: Take a look at $$anonymous$$esh.sub$$anonymous$$eshCount and $$anonymous$$esh.GetTriangles.
I know that I will have to use separate game objects. That is not the issue. I can't find any documentation on getting the vertices and uvs for a specific submesh and only that submesh. I managed to copy the triangles, so the submesh looks correct, but the vertices and UVs are also copied in their entirety and I can't figure out how to only get the ones used by the submesh.
The result is a correct looking mesh with a bunch of extra unused vertices and uvs.
I don't know what's the problem with the documentation... It's all there in the Scripting API as far as I can see. Did you see this thread from last year?
It's not that the documentation isn't there, it's just that it's a little confusing and vague for someone that is a beginner and doesn't have quite a bit of pre existing knowledge on the topic.
Strangely, a Google search didn't yield that thread you posted. After reading the thread and the answer @Bunny83 posted, I feel that it makes a lot more sense.
I've edited my answer and included a $$anonymous$$esh extension method that can extract a submesh from a given mesh.
Answer by Bunny83 · Jul 08, 2016 at 12:16 AM
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 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[i];
int n;
if (!map.TryGetValue(o, out n))
{
n = dest.Add(source, o);
map.Add(o,n);
}
newIndices[i] = 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.
The private class "Vertices" is just a helper class which handles the "copying" of all vertex attributes at once but only those which are actually present.
Great answer and great example! I have yet to test it, as I won't be back on my computer until tomorrow, but this is amazing! $$anonymous$$uch thanks.
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
No, i can't really help you fix this with that little information. Your mesh seems to be quite detailed. $$anonymous$$aybe your mesh exceeds the 64k vertices? The $$anonymous$$esh class now supports more that 64k, but you have to explicitly set the $$anonymous$$esh.indexFormat to the 32 bit format if you have more than 64k vertices.
I'll edit my answer to add some automatic format setting inside the "AssignTo" method. However if that doesn't fix your problem you should post a seperate question and add more information. if you can't provide a copy of the model (copyright?) at least tell us it's stats (vertex / triangle count, submesh count, ...)
Thanks for the reply, @Bunny83 !
That fixed the problem!!! You saved me from a terrible headache... I didn't realize the mesh format index needed to be explicitly changed.
Francesco
Any clues on how to correctly copy over bind poses and blend shapes?. I am having a lot of issues with bind pose mismatch and animations.
What exact issues do you have? Keep in $$anonymous$$d that a Skinned$$anonymous$$eshRenderer has a reference to all the bone Transform components. So when you create seperate objects for each submesh, make sure you set the same bone array to all of them. Of course you must not remove or rearrange any bones from the bones array.
If you need further help your your specific issue, please ask a seperate question and add more details over there. If it's related to this question or answer you can always include a link to this question in your question.
Note that Blendshapes are a completely different beast. However it generally works the same way. Blendshapes have to use the same vertex order as the original vertex array. So if you're creating a new vertex list the way I do, you essentially need to use the index map to figure out which vertices to keep and in which order.
Answer by x4000 · Aug 20, 2017 at 06:09 PM
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[i] );
}
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<MeshFilter>();
if ( !meshFilterComponent )
{
Debug.LogError( "MeshFilter null for '" + go.name + "'!" );
return;
}
MeshRenderer meshRendererComponent = go.GetComponent<MeshRenderer>();
if ( !meshRendererComponent )
{
Debug.LogError( "MeshRenderer null for '" + go.name + "'!" );
return;
}
Mesh mesh = go.GetComponent<MeshFilter>().sharedMesh;
if ( !mesh )
{
Debug.LogError( "Mesh null for '" + go.name + "'!" );
return;
}
List<MeshFromSubmesh> meshFromSubmeshes = GetAllSubMeshAsIsolatedMeshes( mesh );
if ( meshFromSubmeshes == null || meshFromSubmeshes.Count == 0 )
{
Debug.LogError( "List<MeshFromSubmesh> 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<MeshFilter>();
meshFromSubmeshFilter.sharedMesh = meshFromSubmeshes[i].mesh;
MeshRenderer meshFromSubmeshMeshRendererComponent = meshFromSubmeshGameObject.AddComponent<MeshRenderer>();
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[i].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[i].mesh, path );
Debug.Log( "Created: " + path );
}
}
private static List<MeshFromSubmesh> GetAllSubMeshAsIsolatedMeshes( Mesh mesh )
{
List<MeshFromSubmesh> meshesToReturn = new List<MeshFromSubmesh>();
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<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 )
{
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[i];
int n;
if ( !map.TryGetValue( o, out n ) )
{
n = dest.Add( source, o );
map.Add( o, n );
}
newIndices[i] = n;
}
Mesh m = new Mesh();
dest.AssignTo( m );
m.triangles = newIndices;
return m;
}
}
Answer by ypoeymirou · Mar 23, 2017 at 03:23 PM
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[i].mesh;
MeshRenderer meshFromSubmeshMeshRendererComponent = meshFromSubmeshGameObject.AddComponent<MeshRenderer>();
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[i].id];
meshFromSubmeshMeshRendererComponent.sharedMaterials = new[] { material };
}
// Don't forget to save the newly created mesh in the asset database (on disk)
AssetDatabase.CreateAsset(meshFromSubmeshes[i].mesh, "*YourAssetFolder*/IsolatedSubmeshes/*YourAssetName*" + "@" + mesh.name + "@" + meshFromSubmeshName + ".asset");
}
}
Answer by ypoeymirou · Mar 25, 2017 at 08:52 AM
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.
Answer by pachermann · Jun 03, 2021 at 12:20 PM
This is amazing, thank you so much for sharing this!