- Home /
Combining Skinned Meshes
Hello Unity Answers! :) I'm having a lot of difficulty over the subject of skinned meshes, and haven't found a suitable answer anywhere yet.
My situation is:
I'm putting together a character customization system with different head, body and clothes meshes;
This will be animated using a common skeleton;
I want this to remain in one drawcall;
What I've done so far:
I've managed to do this on static (non-skinned) meshes; that is, I have successfully combined the 3 meshes if they are not skinned;
I've managed to create a custom atlas and change the UVs accordingly;
I've tried to use SkinnedMeshCombiner (available at the wiki here) but I can't make it work correctly;
My problems with SkinnedMeshCombiner:
the script seems to add every bone (in all 3 exported meshes' rigs) to the finished combined mesh. I have an AnimatorController that moves one of those rigs (avatars), so only parts of the combined SkinnedMesh move;
if I try to edit the script around to add only one set of bones to the combined SkinnedMesh, the skin weights are all over the place and the body does not distort properly. (see this image for the results and my code below, SkinnedCharacterCombiner.cs)
Can anyone shed some light on this? Basically what I want is:
In runtime, load 3 different meshes that share the same skeleton, combine them in one SkinnedMeshRenderer, then apply animation coming from the shared rig, so that the whole character remains one drawcall.
Thanks!
SkinnedCharacterCombiner.cs
using UnityEngine;
using System.Collections.Generic;
public class SkinnedCharacterCombiner : MonoBehaviour {
void Start() {
Vector3 originalPosition = this.transform.position;
this.transform.position = Vector3.zero;
SkinnedMeshRenderer[] smRenderers = GetComponentsInChildren<SkinnedMeshRenderer>();
int numSubs = 0;
List<Transform> bones = new List<Transform>();
List<BoneWeight> boneWeights = new List<BoneWeight>();
List<CombineInstance> combineInstances = new List<CombineInstance>();
foreach(SkinnedMeshRenderer smr in smRenderers)
numSubs += smr.sharedMesh.subMeshCount;
//int boneOffset = 0;
SkinnedMeshRenderer baseMR = smRenderers[0];
for( int s = 0; s < smRenderers.Length; s++ ) {
SkinnedMeshRenderer smr = smRenderers[s];
BoneWeight[] meshBoneweight = smr.sharedMesh.boneWeights;
// May want to modify this if the renderer shares bones as unnecessary bones will get added.
foreach( BoneWeight bw in meshBoneweight ) {
BoneWeight bWeight = bw;
//bWeight.boneIndex0 += boneOffset;
//bWeight.boneIndex1 += boneOffset;
//bWeight.boneIndex2 += boneOffset;
//bWeight.boneIndex3 += boneOffset;
boneWeights.Add( bWeight );
}
//boneOffset += smr.bones.Length;
CombineInstance ci = new CombineInstance();
ci.mesh = smr.sharedMesh;
ci.transform = smr.transform.localToWorldMatrix;
combineInstances.Add(ci);
Object.Destroy(smr.gameObject);
}
Transform[] meshBones = baseMR.bones;
foreach( Transform bone in meshBones )
bones.Add( bone );
List<Matrix4x4> bindposes = new List<Matrix4x4>();
for(int b = 0; b < bones.Count; b++) {
bindposes.Add(bones[b].worldToLocalMatrix * transform.worldToLocalMatrix);
}
SkinnedMeshRenderer r = gameObject.AddComponent<SkinnedMeshRenderer>();
r.sharedMesh = new Mesh();
r.sharedMesh.CombineMeshes(combineInstances.ToArray(), true, true);
Material combinedMat = new Material( Shader.Find( "Diffuse" ) );
r.sharedMaterial = combinedMat;
r.rootBone = bones[0];
r.bones = bones.ToArray();
r.sharedMesh.boneWeights = boneWeights.ToArray();
r.sharedMesh.bindposes = bindposes.ToArray();
r.sharedMesh.RecalculateBounds();
this.transform.position = originalPosition;
}
}
Answer by hsmanfroi · Mar 17, 2014 at 09:23 PM
*Ok... I couldn't edit it in an easy way but here is the code that did it for me.
From the 3d model, I had to create a "base" mesh which sole purpose was to bring from the rig all the bones - so that "base" mesh has some skin weights from all the bones in the rig, so that I can read all of the bones included in its skin modifier, and then delete it and use that bone information for the other meshes.
I apologize for not editing it into something that completely solves the problem (I'm sure I'm missing something), but I'm heading off to vacation and didn't want to leave you empty-handed.
I hope the script below helps. You won't be able to use it as is, but you can read through it and try to make the same functionality as I did.
Good luck! :)*
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
public class CharacterAssembler : MonoBehaviour {
//3d
public string headResource = "Heads/BaseHead";
//TODO remove this method and call ConstructCharacter() from the script that instantiates this object
void Start() {
ConstructCharacter();
}
/// <summary>
/// Character construction method.
/// </summary>
void ConstructCharacter() {
// 1. reset object to zero-position to avoid issues with world/local during combine
Vector3 originalPosition = this.transform.position;
this.transform.position = Vector3.zero;
// 2. instantiate head
GameObject headGO = Instantiate(Resources.Load(headResource)) as GameObject;
headGO.transform.parent = this.transform;
// 3. get base mesh by name
SkinnedMeshRenderer[] smRenderers = GetComponentsInChildren<SkinnedMeshRenderer>();
SkinnedMeshRenderer smrBase = FindByName(smRenderers, "base");
if(smrBase != null)
Debug.Log("Base mesh successfully loaded with " + smrBase.bones.Length + " bones.");
else
Debug.LogError("Base mesh is invalid.");
// 4. keep a list of objects to destroy (at the end of the script)
List<SkinnedMeshRenderer> toDestroy = new List<SkinnedMeshRenderer>();
// 5. get bone information from base and destroy it
List<Transform> bones = new List<Transform>();
Hashtable bonesByHash = new Hashtable();
List<BoneWeight> boneWeights = new List<BoneWeight>();
//keep bone info
int boneIndex = 0;
foreach(Transform bone in smrBase.bones) {
bones.Add(bone);
bonesByHash.Add(bone.name, boneIndex);
boneIndex++;
}
//keep bindposes
List<Matrix4x4> bindposes = new List<Matrix4x4>();
for(int b = 0; b < bones.Count; b++)
bindposes.Add(bones[b].worldToLocalMatrix * transform.worldToLocalMatrix);
//destroy
toDestroy.Add(smrBase);
// 6. Keep a list of combine instances as we start digging into objects.
List<CombineInstance> combineInstances = new List<CombineInstance>();
// 7. get body smr, alter UVs, insert into combine, delete
Vector2[] uvs;
List<Vector2> totalUVs = new List<Vector2>();
SkinnedMeshRenderer smrBody = FindByName(smRenderers, "body");
uvs = smrBody.sharedMesh.uv;
for(int n = 0; n < uvs.Length; n++)
uvs[n] = new Vector2(uvs[n].x * 0.5f, uvs[n].y);
//smrBody.sharedMesh.uv = uvs;
totalUVs.AddRange(uvs);
InsertSMRToCombine(smrBody, bonesByHash, boneWeights, combineInstances);
toDestroy.Add(smrBody);
// 8. get head, alter UVs, insert into combine, delete
SkinnedMeshRenderer smrHead = FindByName(smRenderers, "cabeca"); //TODO fbx should contain 'head'
uvs = smrHead.sharedMesh.uv;
for(int n = 0; n < uvs.Length; n++)
uvs[n] = new Vector2(uvs[n].x * 0.5f, uvs[n].y);
//smrHead.sharedMesh.uv = uvs;
totalUVs.AddRange(uvs);
InsertSMRToCombine(smrHead, bonesByHash, boneWeights, combineInstances);
toDestroy.Add(smrHead);
// 9. get uniform smr, alter UVs, insert into combine, delete
SkinnedMeshRenderer smrUniform = FindByName(smRenderers, "uniform");
uvs = smrUniform.sharedMesh.uv;
for(int n = 0; n < uvs.Length; n++)
uvs[n] = new Vector2(0.5f + uvs[n].x * 0.5f, uvs[n].y);
//smrUniform.sharedMesh.uv = uvs;
totalUVs.AddRange(uvs);
InsertSMRToCombine(smrUniform, bonesByHash, boneWeights, combineInstances);
toDestroy.Add(smrUniform);
// 10. get shirt smr, alter UVs, insert into combine, delete
SkinnedMeshRenderer smrShirt = FindByName(smRenderers, "shirt");
uvs = smrShirt.sharedMesh.uv;
for(int n = 0; n < uvs.Length; n++)
uvs[n] = new Vector2(0.5f + uvs[n].x * 0.5f, uvs[n].y);
//smrShirt.sharedMesh.uv = uvs;
totalUVs.AddRange(uvs);
InsertSMRToCombine(smrShirt, bonesByHash, boneWeights, combineInstances);
toDestroy.Add(smrShirt);
//combine
//add an empty skinned mesh renderer, and combine meshes into it
SkinnedMeshRenderer r = gameObject.AddComponent<SkinnedMeshRenderer>();
r.sharedMesh = new Mesh();
r.sharedMesh.CombineMeshes(combineInstances.ToArray());
r.sharedMesh.uv = totalUVs.ToArray();
r.bones = bones.ToArray();
r.rootBone = bones[0]; // TODO we can search bonehash for the name of the root node
r.sharedMesh.boneWeights = boneWeights.ToArray();
r.sharedMesh.bindposes = bindposes.ToArray();
//make shadermanager texture
//apply
//late destroy all skinnedmeshrenderers
//special destroy for head to get rid of the extra bip
Object.Destroy(smrHead.transform.parent.gameObject);
//then all smrs
foreach (SkinnedMeshRenderer t in toDestroy) {
// TODO destroy unnecessary bips
// Transform bipRoot = t.gameObject.transform.FindChild("Bip001");
//
// if(bipRoot != null)
// Object.Destroy(bipRoot);
Object.Destroy(t.gameObject);
}
//material
r.material = ShaderManager.Instance.GetCharacterMaterial(
teamString,
uniformIndex,
faceTextureName,
uniformNumber);
//recalculate bounds and return to original position
r.sharedMesh.RecalculateBounds();
this.transform.position = originalPosition;
}
#region extra methods
private void InsertSMRToCombine (SkinnedMeshRenderer smr, Hashtable boneHash,
List<BoneWeight> boneWeights, List<CombineInstance> combineInstances) {
BoneWeight[] meshBoneweight = smr.sharedMesh.boneWeights;
// remap bone weight bone indexes to the hashtable obtained from base object
foreach(BoneWeight bw in meshBoneweight) {
BoneWeight bWeight = bw;
bWeight.boneIndex0 = (int)boneHash[smr.bones[bw.boneIndex0].name];
bWeight.boneIndex1 = (int)boneHash[smr.bones[bw.boneIndex1].name];
bWeight.boneIndex2 = (int)boneHash[smr.bones[bw.boneIndex2].name];
bWeight.boneIndex3 = (int)boneHash[smr.bones[bw.boneIndex3].name];
boneWeights.Add(bWeight);
}
//add the smr to the combine list; also add to destroy list
CombineInstance ci = new CombineInstance();
ci.mesh = smr.sharedMesh;
ci.transform = smr.transform.localToWorldMatrix;
combineInstances.Add(ci);
}
/// <summary>
/// Finds a SkinnedMeshRenderer in the list.
/// </summary>
/// <returns>Found SMR.</returns>
/// <param name="source">Source array to search.</param>
/// <param name="name">Name of the SMR to be searched.</param>
private SkinnedMeshRenderer FindByName(SkinnedMeshRenderer[] source, string name) {
SkinnedMeshRenderer target = null;
foreach(SkinnedMeshRenderer s in source) {
if(s.name.Contains(name)) {
target = s;
break;
}
}
if(target == null)
Debug.LogError("SkinnedMeshRenderer " + name + " not found.");
return target;
}
#endregion
}
Dude, I'm using the same script, but my problem is when I start the animation, the mesh just explodes? I tried my best to implement your working script, but it's not to be working for my model?
Answer by NiloBR · Mar 11, 2014 at 02:24 AM
i am having the same problems with this script and i cant figure out what is happening... i will post my my setup to make the issue as clear as possible...
i will keep digging and i let u know if i find something that can help us...
Nilo, I have since posting this thread found a solution. I can look up my Unity Project later but as far as I can remember, the problem was around the bone weights - I had to manually index them. I'll try and post my solution ASAP.
Just had this same problem NiloBR mentioned. The issue with boneWeights is that you should not set the bone index and bone weight directly. You should, ins$$anonymous$$d, create a BoneWeight array, create each BoneWeight object, assign its values, and set the whole array back to the mesh.
Bonus explanation: BoneWeight itself is a struct, and as such, it shouldn't be mutable. Unity made boneIndex and boneWeight as properties, allowing a kind of mutable struct. It probably stores the values internally and allow the values to be changed. The problem is, the $$anonymous$$esh correctly assumes the BoneWeight struct won't be internally changed, even if it does, so changing the struct's internal values won't update the $$anonymous$$esh. Updating the BoneWeight array triggers a mesh update. Even if Unity decides to keep it this way, the should, at the very least, document it.
danilonishimura can you please elaborate more on how to fix this? I got the same problem like NiloBR. As soon as the animation plays, the whole mesh just explode?
What should I do with this part of script, as you told about bone Index and bone weight?
BoneWeight[] meshBoneweight = smr.shared$$anonymous$$esh.boneWeights;
// $$anonymous$$ay want to modify this if the renderer shares bones as unnecessary bones will get added.
foreach( BoneWeight bw in meshBoneweight ) {
BoneWeight bWeight = bw;
bWeight.boneIndex0 += boneOffset;
bWeight.boneIndex1 += boneOffset;
bWeight.boneIndex2 += boneOffset;
bWeight.boneIndex3 += boneOffset;
boneWeights.Add( bWeight );
}
boneOffset += smr.bones.Length;
@amiel_ace, $$anonymous$$odifying the values of the struct won't affetc the Skinned $$anonymous$$esh Renderer. Think this way: setting the boneWeights array back to the skinned mesh renderer will trigger the mesh renderer to read and apply the bone values, setting the internal values of the weights won't.
foreach (BoneWeight bw in meshBoneweight)
{
BoneWeight bWeight = new BoneWeight();
bWeight.boneIndex0 = bw.boneIndex0 + boneOffset;
bWeight.boneIndex1 = bw.boneIndex1 + boneOffset;
bWeight.boneIndex2 = bw.boneIndex2 + boneOffset;
bWeight.boneIndex3 = bw.boneIndex3 + boneOffset;
boneWeights.Add(bWeight);
}
boneOffset += smr.bones.Length;
smr.shared$$anonymous$$esh.boneWeights = boneWeights.ToArray();
Answer by marcos4503 · Jan 28, 2019 at 12:22 PM
It looks like the issue has already been resolved in this topic, but for anyone reading this in the future, you might consider using the "Skinned Mesh Combiner MT" asset. It is a very simple to use asset that has the function of combining skinned meshes. You can combine the meshes both at runtime and in the editor. You can see it in the Asset Store through this link: http://u3d.as/1pe2
The asset also has a protection against any artifacts generated during the merge!
Thank you!