I need a way to combine multiple bodyparts into a character controlled by a skeleton (or an alternative way of doing the customization process). Simple as that, really.
In my current project I have all the bodyparts moving with their own skeletons controlled by the same Animator (movement using NavMeshAgent and root motion), but they bump into eachother and get desynced, not to mention collisions with other objects and optimisation worries.
I’m not too experienced as I’m entirely self taught and prone to finding the easy way out or brute forcing things, so I’d prefer something simple I can start out with over endless lines of code, but at this point I just want anything that works. There has to be some way to get something as common as customization done. I’m on my knees, just… just give me something, please. Any functional tutorials out there? Any related terms I could google? Anything?
This is what I’m using
using UnityEngine;
using System.Collections.Generic;
using HierarchyDict = System.Collections.Generic.Dictionary<string, UnityEngine.Transform>;
using BoneTransformDict = System.Collections.Generic.Dictionary<string, utils.Tuple<UnityEngine.Transform, string>>;
namespace utils
{
public class MeshCombiner
{
#region Operations
//! Combine mesh.
/*!
\return combined mesh instance.
*/
public static GameObject Combine(List<SkinnedMeshRenderer> SkinnedRenderers)
{
// Generated GO
GameObject final_mesh_go = new GameObject("Mesh");
// Dummy parent holder
GameObject dummy_parent = new GameObject("DummyParent");
// All available bones
var all_bones = new BoneTransformDict();
// Traverse through all skinned mesh renderers
foreach(var renderer in SkinnedRenderers)
{
var renderer_bones = renderer.bones;
foreach (var bone in renderer_bones)
{
// Bone doesn't exist, add it
if (!all_bones.ContainsKey(bone.name))
all_bones[bone.name] = new utils.Tuple<Transform, string>(bone, bone.parent.name);
}
}
var combineInstanceArrays = new Dictionary<Material, List<CombineInstance>>();
var bone_weights = new Dictionary<Mesh, BoneWeight[]>();
// Map between bone name and index
var added_bones = new Dictionary<string, int>();
// List of child objects holding the skinned mesh renderers to be
// destroyed when finished
var child_objects_to_destroy = new List<GameObject>();
int bone_index = 0;
foreach(var renderer in SkinnedRenderers)
{
child_objects_to_destroy.Add(renderer.transform.parent.gameObject);
var renderer_bones = renderer.bones;
// Add all bones as first and save the indices of them
foreach (var bone in renderer_bones)
{
// Bone not yet added
if (!added_bones.ContainsKey(bone.name))
added_bones[bone.name] = bone_index++;
}
// Adjust bone weights indices based on real indices of bones
var bone_weights_list = new BoneWeight[renderer.sharedMesh.boneWeights.Length];
var renderer_bone_weights = renderer.sharedMesh.boneWeights;
for (int i = 0; i < renderer_bone_weights.Length; ++i)
{
BoneWeight current_bone_weight = renderer_bone_weights[i];
current_bone_weight.boneIndex0 = added_bones[renderer_bones[current_bone_weight.boneIndex0].name];
current_bone_weight.boneIndex2 = added_bones[renderer_bones[current_bone_weight.boneIndex2].name];
current_bone_weight.boneIndex3 = added_bones[renderer_bones[current_bone_weight.boneIndex3].name];
current_bone_weight.boneIndex1 = added_bones[renderer_bones[current_bone_weight.boneIndex1].name];
bone_weights_list[i] = current_bone_weight;
}
bone_weights[renderer.sharedMesh] = bone_weights_list;
// Handle bad input
if (renderer.sharedMaterials.Length != renderer.sharedMesh.subMeshCount)
{
Debug.LogError("Mismatch between material count and submesh count. Is this the correct MeshRenderer?");
continue;
}
// Prepare stuff for mesh combination with same materials
for (int i = 0; i < renderer.sharedMesh.subMeshCount; i++)
{
// Material not in dict, add it
if (!combineInstanceArrays.ContainsKey(renderer.sharedMaterials[i]))
combineInstanceArrays[renderer.sharedMaterials[i]] = new List<CombineInstance>();
var actual_mat_list = combineInstanceArrays[renderer.sharedMaterials[i]];
// Add new instance
var combine_instance = new CombineInstance();
combine_instance.transform = renderer.transform.localToWorldMatrix;
combine_instance.subMeshIndex = i;
combine_instance.mesh = renderer.sharedMesh;
actual_mat_list.Add(combine_instance);
}
// No need to use it anymore
renderer.enabled = false;
}
var bones_hierarchy = new HierarchyDict();
// Recreate bone structure
foreach (var bone in all_bones)
{
// Bone not processed, process it
if (!bones_hierarchy.ContainsKey(bone.Key))
AddParent(bone.Key, bones_hierarchy, all_bones, dummy_parent);
}
// Create bone array from preprocessed dict
var bones = new Transform[added_bones.Count];
foreach (var bone in added_bones)
bones[bone.Value] = bones_hierarchy[bone.Key];
// Get the root bone
Transform root_bone = bones[0];
while (root_bone.parent != null)
{
// Get parent
if (bones_hierarchy.ContainsKey(root_bone.parent.name))
root_bone = root_bone.parent;
else
break;
}
// Create skinned mesh renderer GO
GameObject combined_mesh_go = new GameObject("Combined");
combined_mesh_go.transform.parent = final_mesh_go.transform;
combined_mesh_go.transform.localPosition = Vector3.zero;
// Fill bind poses
var bind_poses = new Matrix4x4[bones.Length];
for (int i = 0; i < bones.Length; ++i)
bind_poses[i] = bones[i].worldToLocalMatrix * combined_mesh_go.transform.localToWorldMatrix;
// Need to move it to new GO
root_bone.parent = final_mesh_go.transform;
// Combine meshes into one
var combined_new_mesh = new Mesh();
var combined_vertices = new List<Vector3>();
var combined_uvs = new List<Vector2>();
var combined_indices = new List<int[]>();
var combined_bone_weights = new List<BoneWeight>();
var combined_materials = new Material[combineInstanceArrays.Count];
var vertex_offset_map = new Dictionary<Mesh, int>();
int vertex_index_offset = 0;
int current_material_index = 0;
foreach (var combine_instance in combineInstanceArrays)
{
combined_materials[current_material_index++] = combine_instance.Key;
var submesh_indices = new List<int>();
// Process meshes for each material
foreach (var combine in combine_instance.Value)
{
// Update vertex offset for current mesh
if (!vertex_offset_map.ContainsKey(combine.mesh))
{
// Add vertices for mesh
combined_vertices.AddRange(combine.mesh.vertices);
// Set uvs
combined_uvs.AddRange(combine.mesh.uv);
// Add weights
combined_bone_weights.AddRange(bone_weights[combine.mesh]);
vertex_offset_map[combine.mesh] = vertex_index_offset;
vertex_index_offset += combine.mesh.vertexCount;
}
int vertex_current_offset = vertex_offset_map[combine.mesh];
var indices = combine.mesh.GetTriangles(combine.subMeshIndex);
// Need to "shift" indices
for (int k = 0; k < indices.Length; ++k)
indices[k] += vertex_current_offset;
submesh_indices.AddRange(indices);
}
// Push indices for given submesh
combined_indices.Add(submesh_indices.ToArray());
}
combined_new_mesh.vertices = combined_vertices.ToArray();
combined_new_mesh.uv = combined_uvs.ToArray();
combined_new_mesh.boneWeights = combined_bone_weights.ToArray();
combined_new_mesh.subMeshCount = combined_materials.Length;
for (int i = 0; i < combined_indices.Count; ++i)
combined_new_mesh.SetTriangles(combined_indices[i], i);
// Create mesh renderer
SkinnedMeshRenderer combined_skin_mesh_renderer = combined_mesh_go.AddComponent<SkinnedMeshRenderer>();
combined_skin_mesh_renderer.sharedMesh = combined_new_mesh;
combined_skin_mesh_renderer.bones = bones;
combined_skin_mesh_renderer.rootBone = root_bone;
combined_skin_mesh_renderer.sharedMesh.bindposes = bind_poses;
combined_skin_mesh_renderer.sharedMesh.RecalculateNormals();
combined_skin_mesh_renderer.sharedMesh.RecalculateBounds();
combined_skin_mesh_renderer.sharedMaterials = combined_materials;
// Destroy children
foreach (var child in child_objects_to_destroy)
GameObject.DestroyImmediate(child);
// Destroy dummy parent
GameObject.DestroyImmediate(dummy_parent);
return final_mesh_go;
}
static void AddParent(string BoneName, HierarchyDict BoneHierarchy, BoneTransformDict AllBones, GameObject DummyParent)
{
Transform actual_bone = null;
// Must be bone
if (AllBones.ContainsKey(BoneName))
{
var bone_tuple = AllBones[BoneName];
// Add parent recursively if not added
if (!BoneHierarchy.ContainsKey(bone_tuple._2))
{
AddParent(bone_tuple._2, BoneHierarchy, AllBones, DummyParent);
// Unparent all children of parents
Unparent(BoneHierarchy[bone_tuple._2], DummyParent);
}
bone_tuple._1.parent = BoneHierarchy[bone_tuple._2];
actual_bone = bone_tuple._1;
}
BoneHierarchy[BoneName] = actual_bone;
}
static void Unparent(Transform Parent, GameObject DummyParent)
{
if (Parent != null)
{
var unparent_list = new List<Transform>();
foreach (Transform child in Parent.transform)
unparent_list.Add(child);
foreach (var child in unparent_list)
child.parent = DummyParent.transform;
}
}
#endregion
}
}
Really appreciate the help, like you don’t even know how much, but… I’m just having a really hard time using this, let alone getting a hang of how exactly it works.
I know you’ve already gone way beyond the call of duty, but could you maybe just throw me a one or two sentence tutorial of how I’m supposed to call this, and from where?
Sorry for the late reply,
the function you want to use is Combine(). It takes the list of skinned mesh renderers you want to combine in one. Maybe it’s not what you want. As first you must have some skinned meshes prepared. For instance if you want to combine the human torso and head, then you need to load appropriate prefabs (for head, torso), get the skinned mesh renderers from them and pass them to this function. The returned object is the GO with single skinned mesh renderer and also the bones as Transform GO.
Anyway, I forgot to include the Tuple class
public struct Tuple<T1, T2>
{
public readonly T1 _1;
public readonly T2 _2;
public Tuple(T1 T1_, T2 T2_)
{
_1 = T1_;
_2 = T2_;
}
}
Also I don’t know how good are you at programming but you should be at least past the beginning stage to use it correctly.
No problem dude, you’re already doing way more than could be asked of you.
I have a working base knowledge, but am completely clueless when it comes to all this rendering madness, and have some pretty big language barriers to overcome. At the moment I’m thinking something along the lines of
List<SkinnedMeshRenderer> meshRenderers = gameObject.GetComponentsInChildren<SkinnedMeshRenderer>();
utils.MeshCombiner.Combine(meshRenderers);
but, umm… that’s not right, is it? I’m getting a type conversion error, so I’m guessing GetComponentsInChildren returns an array, and arrays and lists don’t mix? Had to look into the whole List thing, so… would this work?
List<SkinnedMeshRenderer> meshRenderers = new List<SkinnedMeshRenderer> (gameObject.GetComponentsInChildren<SkinnedMeshRenderer>());
utils.MeshCombiner.Combine(meshRenderers);
Am I even close? Because, I mean… They did combine into one object, but it’s more of an amorphous blob combination of all the parts, with scales that I can’t even describe. The parts are connected to the bones, but the positioning is nothing like the original skeleton, not even close. Did I mess something up with the whole List thing, or…?
Yes I think that should work. I’m not primarly the C# developer but I think the List should have a constructor accepting the raw Array.
Also I forgot to mentioned the preconditions for the function.
All the parts MUST have same base skeleton (but could have some additional bones if needed). That is, the structure, the bone names, all must be same.
So maybe there could be the error.
Anyway, if you got some errors, please post the whole error message you get. If you got some screenshot of the output, please post it.
Does the combined mesh looks like the things from the “The Thing” movie?
The skeletons for each part are identical, yeah. I built all the meshes around the same base skeleton file.
No errors popping up on the console. All the meshes get stretched or shrunk in different dimensions and apparently move to the 0,0,0 location upon creation (not sure if intended or not).
See: video of mesh madness
Note: model was made as a placeholder out of boredom. Please don’t judge
Any idea what might be causing the weird stretching, scaling and whatnot?
If you could post me the mesh I could look at it. Is the base model somehow scaled? That is, some parts have some scale set, etc?
Went through the meshes for safe measure and found no scalings. I mean, some of the meshes aren’t exactly size 1,1,1, but changing these values doesn’t seem to change anything anyway, so I’m assuming they just take on the size of the bones and can’t be changed, yeah? (I did try changing them to 1,1,1 anyway. No difference). The objects themselves are 1,1,1 though.
Meshes are Blender files, uploaded them to MediaFire, left the .meta data in there in case that helps.
So I looked at your model and don’t know where the problem is. For me it looks like the scale issue. But it could by that my code isn’t working right, also.
Looking at your blend file in blender, there is some scale set. So I applied the scale and it looks like it is ok, only rotated. I’ve tried it only with legs.
So try to apply all the scales and rotations on the models.
Well s**t. Never even realized that’s how Blender handles sizes in object mode. That really seems like the issue, changing the object sizes in blender to 1 seems to produce very similar results to how the mesh combiner acts in Unity. After fixing things around in Blender I got the legs to work, I think. They look about right. Still getting some weird rotations and I need to figure out how to actually add components to the combined mesh object instead of the old object, but that’s a problem for Future-Scrawny.
Seriously though, can I like buy you a pizza or something? You’ve been a massive help.
Edit: Fixed all the parts in Blender and they look fine in Unity, so that’s definitely a thing. Rotations are still a bit strange, but meh
That’s ok
Don’t know why the rotation is wrong… maybe unity is somehow internally swapping some axis when importing from blender. You could try to export from blender as fbx to find out how it will looks like.
I did try that, but I ended up with a 1/100 scale version when imported to Unity, and the mesh combiner made the whole part apparently invisible, so… I decided not to even go there. At the moment I’m perfectly happy just turning the whole thing 270 degrees around X after combining the meshes
One more question if you don’t mind, how do you deal with actually doing things with the combined mesh? Do you just add components through code in runtime, or…?
For the scale, on the import tab you could change the “scale” of the imported object. So try this one. I would really like to know if this rotation bug is specific to blender files.
The second question, I do everything at runtime, the mesh combining etc. But I have prefab with animator, etc, and I just add the combined GO as the child to this prefab. But this is also consequence as whole game is “procedural”, that is everything is loaded, prepared and so on in the runtime, based on config files, custom scene files, maps, etc.
Well, after a quick test run I’m even more confused. Exported as an .fbx with scaling at 100. When added to the scene, the size is correct, but after running the mesh combiner, it shrinks back to what I assume is that same 1/100 scale. Wondering how the exporter actually handles the scaling.
Also, I noticed a little something that happens with the combiner. The bones in the combined mesh seem to be relative to where the combining objects are. As in, if you combine the objects at 0,0,0 the bones are where they’re supposed to be, but if you do it at 10,0,0 the mesh will be at 0,0,0 but the bones will be at 10,0,0. See: images. Not sure if intended or not
Edit: Looked into the rotation, and apparently it’s a Blender thing. Z axis is facing up when in most cases that’d be Y. “Blender uses the right hand coordinate system with the Z axis pointing upwards. This is common with the coordinate systems used by most common 3D CAD packages”. Exporting as .fbx should fix that problem I hope.
Edit2: The bones in the exported .fbx file have a scale of 100, meshes have a scale of 1. No idea what the exporter is up to. Go figure /shrug. I guess I could just export everything as an .fbx, then scale the combined mesh up? Seems a little cheap, but I mean… Got any other ideas?
Edit3: So yeah, scratch that. .fbx also seems to be rotated no matter what I set as the “up” direction while exporting (even defining different directions to different parts). Must be the skeleton then, yeah? Skeleton’s standing upright, mesh is lying on the floor face down. There are no answers, all I find are more questions lol
This mesh offset could probably happen. Haven’t tested it yet as all my combined meshes are initially at (0,0,0).
Regarding the rotation, be sure that all the rotation, scale is applied before exporting (Ctrl+A in blender I think). Also you could try with simple plane export from blender and experiment if the rotation change. If you change the axis orientation in export, it should change it in unity. Hope you’re using the .fbx in unity and not the .blend files
Regarding the scaling issue, don’t know where could the problem be. Anyway, scaling the objects in unity is in most cases not a good thing. So scale the object on import or in the 3D program.
Also you definitelly should have the 1:1 copy of the combined mesh (no additional scaling, rotating should be done). Othewise other problem could arise. It could be I have some bugs in code and would like to know. It also could be that the model got some issue. We should find out if the model is 100% correct. What about trying some other model?
I’m attaching the sample project, to be sure you’re using the functionality right.
2335923–157864–test_merge.zip (1020 KB)
I think I found the rotation problem. There is one Transform named “Armature” which is parent for the root bone Waist. But it is strange, that “Armature” isn’t part of the bone hierarchy. Need to look at this more closely.
Ran into some even stranger rotation problems where I had two instances of the same part/file, neither of which had any in-editor rotations on them, but they were facing different directions. At this point I don’t even, so I made a clean project with just the bare essentials to keep testing.
Yes, all rotations and whatnot are applied and I’m using the right files. Changing the up direction when exporting .fbx models does change the initial direction of the models in Unity, but has no apparent effect on the rotation that happens when the meshes are combined.
Scaling is still a total mystery, but assuming the rotation problem is not related to exporting as .blend files, I can just keep using those and scaling won’t be a problem.
So, I made a quick new model and bones, rigged it and exported. Shocking news: more weird things happened.
Note: irrelevant things, feel free to skip
I made the right side of the model, duplicated the arms and legs, and mirrored to the other side. Apparently Blender’s idea of mirroring is just setting an object’s scale to -1, meaning it fucks everything up, including normals. Slightly mad at whoever came up with this brilliant idea.
End irrelevance
Using the new rig (didn’t bother chopping it up, just a single skeleton) with the mesh combiner, it did not fall flat on it’s face, but on it’s side. Exporting it as an .fbx will result in it lying on it’s side from the moment he’s brought into the scene, regardless of what I put in as it’s exported up direction.
Just about to hit my boiling point, gonna take a break for a few hours.
Update: Interesting fact, I found this, and the mesh behaves the same way, falling on it’s face. So it’s definitely something on my end. Gonna dive back into google and see if I bump into anything.
Update2: Well, minor success. After messing with a ton of exporting, I found that by turning the whole thing 270 around the X axis in Blender, applying rotations, then exporting as .fbx and changing Y to be the up direction and Z to be the forward direction, I can actually get the model to stand upright ever after the mesh combining. Armature’s still sporting a 270 rotation around the X axis, but seems to be countered by the export settings. Similarly I can get the .blend files to work by rotating the model 90 degrees around X, then applying the rotation and saving. Sort of brute forcing it, but whatever works, right?
Another slightly interesting fact is that having an animator component on the parent object of the to-be-combined parts makes the combined mesh fall flat on it’s face again. So yeah, that sort of ties the whole thing into one big knot all over again. Adding animations messes things up, and I don’t know if you’re aware, but animations are sort of important.
Back to the drawing board.
I need to look at it. Don’t have a time now, maybe weekend. I could maybe fix it. The main problem is this “Armature” Transform which is exported as parent of the root bone, but isn’t referenced anywhere. So I’ll need to support it somehow and use it’s rotation as basis. Or just make it also parent for the combined bones. Need to look at it.
So I tried to play with it but, without success. I just don’t have enough time to debug it deeply. But it looks like that this “Armature” Transform is somehow causing the issue. I don’t know any workarounds now. Maybe unity has some internal states which aren’t exported through API.
The animations should work. But it could be related to that “Armature” transform.