Search Unity

  1. Megacity Metro Demo now available. Download now.
    Dismiss Notice
  2. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

Combining Meshes with Bones?

Discussion in 'Scripting' started by FGPArthurVII, Jul 16, 2020.

  1. FGPArthurVII

    FGPArthurVII

    Joined:
    Jan 5, 2015
    Posts:
    106
    So I've made a little script to Bake multiple meshes into one single combined mesh.

    It was meant to be used on a character's hair So instead of having a parent object holding 100 hair strands (or cards if you prefer calling like so) I'd have only a single Mesh for the entire hair.

    All good, but the hair uses rigging to make It look brushed and neat in place, of course, the end result looks like the character received a strong electrical jolt as her hair is completelly straightened up, because the bones are no longer affecting It. So, now I want to know how to change this script so the final mesh still mantains the link and weights to the original rigging?

    Code (CSharp):
    1. using UnityEngine;
    2.  
    3. public class MeshCombiner : MonoBehaviour
    4. {
    5.    public void CombineMeshes ()
    6.    {
    7.        Quaternion oldRot = transform.rotation;
    8.        Vector3 oldPos = transform.position;
    9.  
    10.        transform.rotation = Quaternion.identity;
    11.        transform.position = Vector3.zero;
    12.  
    13.        MeshFilter[] filters = GetComponentsInChildren<MeshFilter>();
    14.        Debug.Log(name + "is Combining " + filters.Length + " Meshes");
    15.        Mesh finalMesh = new Mesh();
    16.  
    17.        CombineInstance[] combiners = new CombineInstance[filters.Length];
    18.        for(int a = 0; a < filters.Length; a++)
    19.        {
    20.            if(filters[a].transform == transform)
    21.             continue;
    22.  
    23.            combiners[a].subMeshIndex = 0;
    24.            combiners[a].mesh = filters[a].sharedMesh;
    25.            combiners[a].transform = filters[a].transform.localToWorldMatrix;
    26.        }
    27.        finalMesh.CombineMeshes(combiners);
    28.        GetComponent<MeshFilter>().sharedMesh = finalMesh;
    29.  
    30.        transform.rotation = oldRot;
    31.        transform.position = oldPos;
    32.    }
    33. }
    34.  
    Code (CSharp):
    1. using UnityEngine;
    2. using UnityEditor;
    3.  
    4. [CustomEditor (typeof (MeshCombiner))]
    5. public class MeshCombinerEditor : Editor
    6. {
    7.     void OnSceneGUI()
    8.     {
    9.         MeshCombiner mc = target as MeshCombiner;
    10.         if(Handles.Button (mc.transform.position + Vector3.up * 1,
    11.         Quaternion.LookRotation(Vector3.up), 1, 1, Handles.CylinderCap))
    12.         {
    13.             mc.CombineMeshes ();
    14.         }
    15.     }
    16. }
    17.  
    Gratefully,
    Arthur
     
    tonytopper likes this.
  2. bentontr

    bentontr

    Joined:
    Jun 9, 2017
    Posts:
    39
    Hey Arthur,
    I did something similar some time ago. Basically I had a character skinned mesh and I would combine various components with it dynamically (such as hair, earrings, etc). Here is the script I used to do that:

    Code (CSharp):
    1. using System;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4.  
    5. public class CharacterMesh
    6. {
    7.     private class MeshComponent
    8.     {
    9.         public Vector3[] Verts;
    10.         public Vector3[] Normals;
    11.         public Vector2[] Uvs;
    12.         public int[] Triangles;
    13.         public BoneWeight[] Weights;
    14.     }
    15.  
    16.     public Mesh Mesh { get; }
    17.     public SkinnedMeshRenderer Renderer { get; }
    18.  
    19.     private readonly List<MeshComponent> _components = new List<MeshComponent>(8);
    20.     private readonly MeshComponent _baseMeshComponent;
    21.  
    22.     public CharacterMesh(SkinnedMeshRenderer renderer, Mesh baseMesh)
    23.     {
    24.         Renderer = renderer;
    25.  
    26.         if (baseMesh == null)
    27.         {
    28.             throw new ArgumentException("BaseMesh must not be null.", nameof(baseMesh));
    29.         }
    30.  
    31.         if (renderer == null)
    32.         {
    33.             throw new ArgumentException("Renderer must not be null.", nameof(renderer));
    34.         }
    35.  
    36.         Mesh = new Mesh
    37.         {
    38.             name = renderer.name,
    39.             vertices = baseMesh.vertices,
    40.             uv = baseMesh.uv,
    41.             triangles = baseMesh.triangles,
    42.             bindposes = baseMesh.bindposes,
    43.             boneWeights = baseMesh.boneWeights,
    44.             normals = baseMesh.normals,
    45.             colors = baseMesh.colors,
    46.             tangents = baseMesh.tangents
    47.         };
    48.         renderer.sharedMesh = Mesh;
    49.  
    50.         _baseMeshComponent = new MeshComponent
    51.         {
    52.             Verts = baseMesh.vertices,
    53.             Normals = baseMesh.normals,
    54.             Uvs = baseMesh.uv,
    55.             Triangles = baseMesh.triangles,
    56.             Weights = baseMesh.boneWeights
    57.         };
    58.  
    59. #if UNITY_EDITOR
    60.         Debug.Assert(Mesh.normals.Length == Mesh.vertices.Length, $"Normals and verts for {Mesh.name} do not match up.");
    61. #endif
    62.  
    63.         //if for some reason the weights aren't set, create a placeholder
    64.         if (_baseMeshComponent.Weights == null || _baseMeshComponent.Weights.Length == 0)
    65.         {
    66. #if UNITY_EDITOR
    67.             Debug.LogWarning($"No bone weights were set for combined mesh with name {Mesh.name}.");
    68. #endif
    69.             _baseMeshComponent.Weights = new BoneWeight[_baseMeshComponent.Verts.Length];
    70.         }
    71.     }
    72.  
    73.     public void CombineMesh(List<Mesh> meshes)
    74.     {
    75.         _components.Clear();
    76.  
    77.         int vertexCount = _baseMeshComponent.Verts.Length;
    78.         int uvCount = _baseMeshComponent.Uvs.Length;
    79.         int triangleCount = _baseMeshComponent.Triangles.Length;
    80.  
    81.         //calculate the total sizes
    82.         for (int i = 0; i < meshes.Count; i++)
    83.         {
    84.             var mesh = meshes[i];
    85.             if (mesh == null)
    86.             {
    87.                 continue;
    88.             }
    89.  
    90. #if UNITY_EDITOR
    91.             Debug.Assert(mesh.normals.Length == mesh.vertices.Length, $"Normals and verts for {mesh.name} do not match up.");
    92. #endif
    93.  
    94. #if UNITY_EDITOR
    95.             Debug.Assert(Mesh.bindposes.Length == mesh.bindposes.Length, $"Bone structure of {mesh.name} does not match base structure of {Mesh.name}.");
    96. #endif
    97.  
    98.             var meshComp = new MeshComponent
    99.             {
    100.                 Verts = mesh.vertices,
    101.                 Normals = mesh.normals,
    102.                 Triangles = mesh.triangles,
    103.                 Uvs = mesh.uv,
    104.                 Weights = mesh.boneWeights
    105.             };
    106.  
    107.             //if for some reason the weights aren't set, create a placeholder
    108.             if (meshComp.Weights == null || meshComp.Weights.Length == 0)
    109.             {
    110. #if UNITY_EDITOR
    111.                 Debug.LogWarning($"No bone weights were set for combined mesh with name {mesh.name}.");
    112. #endif
    113.                 meshComp.Weights = new BoneWeight[meshComp.Verts.Length];
    114.             }
    115.  
    116. #if UNITY_EDITOR
    117.             Debug.Assert(meshComp.Verts.Length == meshComp.Weights.Length, $"Bone weights must be the same length as the verts for {mesh.name}.");
    118. #endif
    119.  
    120.             vertexCount += meshComp.Verts.Length;
    121.             uvCount += meshComp.Uvs.Length;
    122.             triangleCount += meshComp.Triangles.Length;
    123.  
    124.             _components.Add(meshComp);
    125.         }
    126.  
    127.         var verts = new Vector3[vertexCount];
    128.         var normals = new Vector3[vertexCount];
    129.         var uvs = new Vector2[uvCount];
    130.         var triangles = new int[triangleCount];
    131.         var weights = new BoneWeight[vertexCount];
    132.  
    133.         //copy in base mesh
    134.         Array.Copy(_baseMeshComponent.Verts, verts, _baseMeshComponent.Verts.Length);
    135.         Array.Copy(_baseMeshComponent.Normals, normals, _baseMeshComponent.Normals.Length);
    136.         Array.Copy(_baseMeshComponent.Uvs, uvs, _baseMeshComponent.Uvs.Length);
    137.         Array.Copy(_baseMeshComponent.Triangles, triangles, _baseMeshComponent.Triangles.Length);
    138.         Array.Copy(_baseMeshComponent.Weights, weights, _baseMeshComponent.Verts.Length);
    139.  
    140.         int vertexOffset = _baseMeshComponent.Verts.Length;
    141.         int uvOffset = _baseMeshComponent.Uvs.Length;
    142.         int triangleOffset = _baseMeshComponent.Triangles.Length;
    143.  
    144.         //copy in sub meshes
    145.         for (int i = 0; i < _components.Count; i++)
    146.         {
    147.             var meshComp = _components[i];
    148.  
    149.             Array.Copy(meshComp.Verts, 0, verts, vertexOffset, meshComp.Verts.Length);
    150.             Array.Copy(meshComp.Normals, 0, normals, vertexOffset, meshComp.Normals.Length);
    151.             Array.Copy(meshComp.Uvs, 0, uvs, uvOffset, meshComp.Uvs.Length);
    152.  
    153.             //can't just copy triangles directly, need to offset
    154.             for (int j = 0; j < meshComp.Triangles.Length; j++)
    155.             {
    156.                 triangles[triangleOffset + j] = meshComp.Triangles[j] + vertexOffset;
    157.             }
    158.  
    159.             Array.Copy(meshComp.Weights, 0, weights, vertexOffset, meshComp.Verts.Length);
    160.  
    161.             vertexOffset += meshComp.Verts.Length;
    162.             uvOffset += meshComp.Uvs.Length;
    163.             triangleOffset += meshComp.Triangles.Length;
    164.         }
    165.  
    166.         //update the mesh components...
    167.         Mesh.Clear();
    168.         Mesh.vertices = verts;
    169.         Mesh.normals = normals;
    170.         Mesh.uv = uvs;
    171.         Mesh.triangles = triangles;
    172.         Mesh.boneWeights = weights;
    173.     }
    174. }
    175.  
    Note: It's possible to have the same rig for different components in your 3D editor - but when imported into unity different bones can be optimized out or re-ordered (thanks for that - very helpful, especially since it can't be turned off). So you can use this editor script to order the bones on the skinned meshes that are imported:
    Code (CSharp):
    1. //Sourced from: https://forum.unity.com/threads/assetpostprocessor-skinned-mesh-renderer-bone-index-reorder-solved.401872/
    2.  
    3. using System;
    4. using System.Collections.Generic;
    5. using System.Linq;
    6. using UnityEditor;
    7. using UnityEngine;
    8.  
    9. //sorts transform bone indexes in skinned mesh renderers so that we can swap skinned meshes at runtime
    10. public class AssetPostProcessorReorderBones : AssetPostprocessor
    11. {
    12.     protected void OnPostprocessModel(GameObject go)
    13.     {
    14.         Process(go);
    15.     }
    16.  
    17.     private void Process(GameObject go)
    18.     {
    19.         var renderers = go.GetComponentsInChildren<SkinnedMeshRenderer>(true);
    20.  
    21.         foreach (var renderer in renderers)
    22.         {
    23.             if (renderer == null)
    24.             {
    25.                 continue;
    26.             }
    27.  
    28.             //list of bones
    29.             var tList = renderer.bones.ToList();
    30.  
    31.             //sort alphabetically
    32.             tList.Sort(CompareTransform);
    33.  
    34.             //record bone index mappings (richardf advice)
    35.             //build a Dictionary<int, int> that records the old bone index => new bone index mappings,
    36.             //then run through every vertex and just do boneIndexN = dict[boneIndexN] for each weight on each vertex.
    37.             var remap = new Dictionary<int, int>();
    38.             for (var i = 0; i < renderer.bones.Length; i++)
    39.             {
    40.                 remap[i] = tList.IndexOf(renderer.bones[i]);
    41.             }
    42.  
    43.             //remap bone weight indexes
    44.             var bw = renderer.sharedMesh.boneWeights;
    45.             for (var i = 0; i < bw.Length; i++)
    46.             {
    47.                 bw[i].boneIndex0 = remap[bw[i].boneIndex0];
    48.                 bw[i].boneIndex1 = remap[bw[i].boneIndex1];
    49.                 bw[i].boneIndex2 = remap[bw[i].boneIndex2];
    50.                 bw[i].boneIndex3 = remap[bw[i].boneIndex3];
    51.             }
    52.  
    53.             //remap bindposes
    54.             var bp = new Matrix4x4[renderer.sharedMesh.bindposes.Length];
    55.             for (var i = 0; i < bp.Length; i++)
    56.             {
    57.                 bp[remap[i]] = renderer.sharedMesh.bindposes[i];
    58.             }
    59.  
    60.             //assign new data
    61.             renderer.bones = tList.ToArray();
    62.             renderer.sharedMesh.boneWeights = bw;
    63.             renderer.sharedMesh.bindposes = bp;
    64.         }
    65.     }
    66.  
    67.     private static int CompareTransform(Transform a, Transform b)
    68.     {
    69.         return string.Compare(a.name, b.name, StringComparison.Ordinal);
    70.     }
    71. }
    72.  
    Finally, I would suggest creating a .asset from the skinned mesh objects to avoid bones going missing / other issues (Unity won't optimize the .assets) so you can copy out the bones from the mesh that has all the bones such as your player character into the other meshes (such as hair) using this wizard:
    Code (CSharp):
    1. using System;
    2. using System.Collections.Generic;
    3. using System.IO;
    4. using UnityEditor;
    5. using UnityEngine;
    6.  
    7. public class SkinnedMeshComponentWizard : ScriptableWizard
    8. {
    9.     public SkinnedMeshRenderer BaseMesh;
    10.     public List<SkinnedMeshRenderer> Components;
    11.  
    12.     [MenuItem("Tools/Mesh/Generate Skinned Mesh Component")]
    13.     protected static void CreateWizard()
    14.     {
    15.         DisplayWizard<SkinnedMeshComponentWizard>("Skinned Mesh Component Generation", "Generate");
    16.     }
    17.  
    18.     protected void OnWizardCreate()
    19.     {
    20.         var path = EditorUtility.SaveFolderPanel("Export Meshes", string.Empty, string.Empty);
    21.         string relativePath = path.Substring(path.IndexOf("Assets/", StringComparison.Ordinal));
    22.         foreach (var component in Components)
    23.         {
    24.             var boneMapping = new Dictionary<int, int>();
    25.             for (int i = 0; i < component.bones.Length; i++)
    26.             {
    27.                 var compBone = component.bones[i];
    28.                 for (int j = 0; j < BaseMesh.bones.Length; j++)
    29.                 {
    30.                     var baseBone = BaseMesh.bones[j];
    31.                     if (compBone.name == baseBone.name)
    32.                     {
    33.                         boneMapping.Add(i, j);
    34.                         break;
    35.                     }
    36.                 }
    37.             }
    38.  
    39.             //remap the bones
    40.             var weights = component.sharedMesh.boneWeights;
    41.             for (int i = 0; i < weights.Length; i++)
    42.             {
    43.                 var weight = weights[i];
    44.                 weights[i].boneIndex0 = boneMapping[weight.boneIndex0];
    45.                 weights[i].boneIndex1 = boneMapping[weight.boneIndex1];
    46.                 weights[i].boneIndex2 = boneMapping[weight.boneIndex2];
    47.                 weights[i].boneIndex3 = boneMapping[weight.boneIndex3];
    48.             }
    49.  
    50.             var verticies = component.sharedMesh.vertices;
    51.             var scale = component.transform.lossyScale;
    52.             for (var i = 0; i < verticies.Length; i++)
    53.             {
    54.                 var vertex = verticies[i];
    55.                 vertex.x = vertex.x * scale.x;
    56.                 vertex.y = vertex.y * scale.y;
    57.                 vertex.z = vertex.z * scale.z;
    58.                 verticies[i] = vertex;
    59.             }
    60.  
    61.             var mesh = new Mesh
    62.             {
    63.                 name = component.sharedMesh.name,
    64.                 vertices = verticies,
    65.                 uv = component.sharedMesh.uv,
    66.                 triangles = component.sharedMesh.triangles,
    67.                 bindposes = BaseMesh.sharedMesh.bindposes,
    68.                 boneWeights = weights,
    69.                 normals = component.sharedMesh.normals,
    70.                 colors = component.sharedMesh.colors,
    71.                 tangents = component.sharedMesh.tangents
    72.             };
    73.             AssetDatabase.CreateAsset(mesh, Path.Combine(relativePath, $"{mesh.name}.asset"));
    74.         }
    75.     }
    76.  
    77.     protected void OnWizardUpdate()
    78.     {
    79.         helpString = "Creates a new mesh based off the bone structure of a base mesh and ensures everything is in the correct order in the new mesh.";
    80.         isValid = (BaseMesh != null) && (Components != null && Components.Count > 0);
    81.     }
    82. }
    83.  
     
  3. FGPArthurVII

    FGPArthurVII

    Joined:
    Jan 5, 2015
    Posts:
    106
    Thank You Very Much!
     
  4. FGPArthurVII

    FGPArthurVII

    Joined:
    Jan 5, 2015
    Posts:
    106
    Hello, I'm not so much of a programmer as I'm an artist, and I do not think I have enough the knowledge to fully comprehend what is going on on these scripts. Can you explain to me how to use It? What components do I need my separate meshes to have (for example, my scripts demands every separate object I want to combine have a Mesh Filter)? Where and wich script do I apply? where will my "baked" combined mesh will appear, this kind of stuff?

    Gratefully,
    Arthur