Hello folks! I’m working on a voxel/cube character creator where after building your desired character you can combine the cubes together into one mesh, attach it to a skinned mesh renderer and preview animations on it. The cube prefab is the default cube model that comes with the engine just with a custom material. Each cube is attached to one body part (Head, torso right hand etc.) and each body part has exactly one “bone” transform.
[SerializeField] private Transform rig;
[SerializeField] private Animator animator;
[ContextMenu("Preview Animations")]
private void CombineCubesIntoSkinnedMesh()
{
Transform[] bones = new Transform[bodyPartAreas.Count];
List<MeshFilter> allCubeMeshFilters = new List<MeshFilter>();
for(int bodyPartIndex = 0; bodyPartIndex < bodyPartAreas.Count; bodyPartIndex++)
{
bones[bodyPartIndex] = bodyPartAreas[bodyPartIndex].bone;
List<GameObject> cubes = bodyPartAreas[bodyPartIndex].GetCubes();
for (int cubeIndex = 0; cubeIndex < cubes.Count; cubeIndex++)
{
MeshFilter cubeMeshFilter = cubes[cubeIndex].GetComponent<MeshFilter>();
allCubeMeshFilters.Add(cubeMeshFilter);
// Assign bone weights to each vertex of the mesh
BoneWeight1[] boneWeights = new BoneWeight1[cubeMeshFilter.mesh.vertexCount];
for (int i = 0; i < cubeMeshFilter.mesh.vertexCount; i++)
{
boneWeights[i].weight = 1;
boneWeights[i].boneIndex = bodyPartIndex;
}
Unity.Collections.NativeArray<BoneWeight1> weights = new Unity.Collections.NativeArray<BoneWeight1>(boneWeights, Unity.Collections.Allocator.Temp);
byte[] bonesPerVertexArray = new byte[cubeMeshFilter.mesh.vertexCount];
for (int i = 0; i < cubeMeshFilter.mesh.vertexCount; i++)
{
bonesPerVertexArray[i] = 1;
}
Unity.Collections.NativeArray<byte> bonesPerVertex = new Unity.Collections.NativeArray<byte>(bonesPerVertexArray, Unity.Collections.Allocator.Temp);
cubeMeshFilter.mesh.SetBoneWeights(bonesPerVertex, weights);
// Create a bind pose for the bone
Matrix4x4[] bindPoses = new Matrix4x4[1] { bones[bodyPartIndex].worldToLocalMatrix * rig.localToWorldMatrix };
cubeMeshFilter.mesh.bindposes = bindPoses;
cubes[cubeIndex].SetActive(false);
}
}
CombineInstance[] combine = new CombineInstance[allCubeMeshFilters.Count];
int combineIndex = 0;
foreach (MeshFilter cubeMeshFilter in allCubeMeshFilters)
{
combine[combineIndex].mesh = cubeMeshFilter.mesh;
combine[combineIndex].transform = cubeMeshFilter.transform.localToWorldMatrix;
cubeMeshFilter.gameObject.SetActive(false);
combineIndex++;
}
Mesh combinedMesh = new Mesh();
combinedMesh.CombineMeshes(combine);
SkinnedMeshRenderer skinnedMeshRenderer = rig.gameObject.AddComponent<SkinnedMeshRenderer>();
skinnedMeshRenderer.bones = bones;
skinnedMeshRenderer.rootBone = rig.GetChild(0);
skinnedMeshRenderer.sharedMesh = combinedMesh;
// Reset the rig transform so the same transform isn't applied to each vertice twice
rig.transform.position = Vector3.zero;
rig.transform.localScale = Vector3.one;
rig.transform.rotation = Quaternion.identity;
animator.enabled = true;
canPlaceCubes = false;
}
This is the part of the code where I’m combining the meshes into a skinned mesh renderer and assigning the bones My problem is that I’m getting a “bone weights don’t match the bones” error. I don’t see the reason for the disparity. When debugging the code I confirmed that the bones of the skinned mesh renderer are correctly assigned. All the expected bones, in the expected order. I’m not sure if its the mesh combination process messing up the bone weights and bind poses or something else entirely.
In the attached file you can see body part areas and the rig, visualized by the Animation Rigging package.
Might be a silly question, but is there a reason you’re bothering to make a skinned mesh, given that all the boxes are only influenced by one bone each anyway?
The character can consist of dozens, maybe hundreds of individual cube meshes, I’m combining them into a skinned mesh to optimize the number of draw calls necessary. There can be multiple characters like this present in a scene. Maybe entire structures too. Otherwise yeah I could’ve just parented each cube to the respective bone.
Well, according to the results of this thread Unity assumes that when combining skinned meshes, that each mesh has its own set of bones (which probably makes sense in some combine cases) and that those bones are also simply appended in the bones array, one after the other. However since you probably want to use the same bones for each mesh, you have to adjust the offset that Unity assumes. As you can see in his solution, he subracts the bones array length multiplied by the index of the mesh to map all the individual bone indices that Unity assumes are one after the other to the same single range of the one single set of bones.
I was never a fan of the “CombineMeshes” function. While it does simplify combining meshes a bit, it still requires quite a bit of setup and the actual actions performed are essentially just appending all the arrays one after another and adding offsets to indices. So the proper setup is actually the main work. The actual combining is “kinda” trivial.
The way you setup your meshes however is a bit of a problem as you have multiple meshes per bone with a variing count. So in your case fixing the bone indices would not be that trivial after the combination. One way would be a two step combination. So first create a combined mesh for each bone where you essentially combine all cubes of one body part so you have only one mesh per bone. Finally just combine those into one skinnedmesh.
What Bunny83 says translates in your case to setting the bone index of every vertex to 0, and giving each mesh one bone, because when the mesh is combined, the bones are combined into a single array and the bone indices from each mesh automatically remapped.
Right, if you do the two step combination after the first step each mesh would just have a single bone and each has an index of 0 and since CombineMeshes does assume you put those bones in that order into the bones array, the offsets would be correct.
Wow you’re right @Bunny83 , looking at the final combined mesh’s bone indices, they’re completely off. For example the first body part’s first cube has correct bone indexes but the second body part’s indices are all incremented by 1 extra. Once I limited the number of body parts to one, a single cube was successfuly attached to the head bone. I’ll have to devise a solution to fix these offsets. Thanks a lot for pointing at the right direction, I’ll update the post with the final code for posterity once I fix the issue.
Thanks for the added clarification @Peeling ! Its working now.
For reference this is what a body part is:
public class BodyPartArea : MonoBehaviour
{
public int id { get { return _id; } }
[SerializeField] private int _id;
public Transform bone { get { return _bone; } }
[SerializeField] private Transform _bone;
[SerializeField] private BoxCollider area;
[SerializeField] [Tooltip("Serialized for debug")] private List<GameObject> cubes;
private bool highlightGizmoActive;
private Color gizmoColor;
private void Awake()
{
cubes = new List<GameObject>();
}
private void Start()
{
gizmoColor = new Color(1, 1, 1, 0.3f);
}
public void AddCube(GameObject cube)
{
cubes.Add(cube);
}
public void RemoveCube(GameObject cube)
{
cubes.Remove(cube);
}
public List<GameObject> GetCubes()
{
return cubes;
}
public void SetHighlightGizmoActive(bool active)
{
highlightGizmoActive = active;
}
public void OnDrawGizmos()
{
if (highlightGizmoActive)
{
Gizmos.color = gizmoColor;
Gizmos.DrawCube(transform.position, area.bounds.size);
}
}
}
Here is the final code:
[SerializeField] private Transform rig;
[SerializeField] private Animator animator;
[SerializeField] private List<BodyPartArea> bodyPartAreas;
[ContextMenu("Preview Animations")]
private void CombineCubesIntoSkinnedMesh()
{
int cubeVertexCount = cubePrefab.GetComponent<MeshFilter>().sharedMesh.vertexCount;
Transform[] bones = new Transform[bodyPartAreas.Count];
Mesh[] bodyPartMeshes = new Mesh[bodyPartAreas.Count];
List<MeshFilter> allCubeMeshFilters = new List<MeshFilter>();
for (int bodyPartIndex = 0; bodyPartIndex < bodyPartAreas.Count; bodyPartIndex++)
{
bones[bodyPartIndex] = bodyPartAreas[bodyPartIndex].bone;
List<GameObject> bodyPartCubes = bodyPartAreas[bodyPartIndex].GetCubes();
CombineInstance[] bodyPartCubesCombine = new CombineInstance[bodyPartCubes.Count];
for (int cubeIndex = 0; cubeIndex < bodyPartCubes.Count; cubeIndex++)
{
MeshFilter cubeMeshFilter = bodyPartCubes[cubeIndex].GetComponent<MeshFilter>();
allCubeMeshFilters.Add(cubeMeshFilter);
// Create a combine instance to create a final combined mesh for each body part
bodyPartCubesCombine[cubeIndex].mesh = cubeMeshFilter.mesh;
bodyPartCubesCombine[cubeIndex].transform = cubeMeshFilter.transform.localToWorldMatrix;
bodyPartCubes[cubeIndex].SetActive(false); // We no longer need to see this cube
}
Mesh bodyPartMesh = new Mesh();
bodyPartMesh.CombineMeshes(bodyPartCubesCombine);
bodyPartMesh.RecalculateBounds();
bodyPartMeshes[bodyPartIndex] = bodyPartMesh;
// Assign bone weights to each vertex of the body part mesh
BoneWeight1[] boneWeights = new BoneWeight1[bodyPartMesh.vertexCount];
for (int i = 0; i < bodyPartMesh.vertexCount; i++)
{
boneWeights[i].weight = 1;
boneWeights[i].boneIndex = 0;
}
Unity.Collections.NativeArray<BoneWeight1> weights = new Unity.Collections.NativeArray<BoneWeight1>(boneWeights, Unity.Collections.Allocator.Temp);
// How many bones should each vertex be affected by? Since each body area has one bone the answer is 1
byte[] bonesPerVertexArray = new byte[bodyPartMesh.vertexCount];
for (int i = 0; i < bodyPartMesh.vertexCount; i++)
{
bonesPerVertexArray[i] = 1;
}
Unity.Collections.NativeArray<byte> bonesPerVertex = new Unity.Collections.NativeArray<byte>(bonesPerVertexArray, Unity.Collections.Allocator.Temp);
bodyPartMesh.SetBoneWeights(bonesPerVertex, weights);
// Create a bind pose for the bone
Matrix4x4[] bindPoses = new Matrix4x4[1] { bones[bodyPartIndex].worldToLocalMatrix * rig.localToWorldMatrix };
bodyPartMesh.bindposes = bindPoses;
}
// Combine all body part meshes into one mesh, as one body
CombineInstance[] bodyPartsCombine = new CombineInstance[bodyPartMeshes.Length];
for (int bodyPartIndex = 0; bodyPartIndex < bodyPartMeshes.Length; bodyPartIndex++)
{
bodyPartsCombine[bodyPartIndex].mesh = bodyPartMeshes[bodyPartIndex];
bodyPartsCombine[bodyPartIndex].transform = bodyPartAreas[bodyPartIndex].transform.localToWorldMatrix;
}
Mesh combinedMesh = new Mesh();
combinedMesh.CombineMeshes(bodyPartsCombine);
combinedMesh.RecalculateBounds();
SkinnedMeshRenderer skinnedMeshRenderer = rig.gameObject.AddComponent<SkinnedMeshRenderer>();
skinnedMeshRenderer.bones = bones;
skinnedMeshRenderer.rootBone = rig.GetChild(0); // First child of rig is the pelvis
skinnedMeshRenderer.sharedMesh = combinedMesh;
// Reset the rig transform so the same transform isn't applied to each vertex twice
rig.transform.position = Vector3.zero;
rig.transform.localScale = Vector3.one;
rig.transform.rotation = Quaternion.identity;
animator.enabled = true;
canPlaceCubes = false;
}
The main issue turned out to be that the CombineMeshes method incremented bone indexes by 1 for each mesh. For 10 body parts and each with 10 cubes, the final vertex group’s bone index would be off by 90. Body parts list is in the attachment. The solution was to combine each body part individually, setting the bone index of each vertex to 0 so the final step of combining the meshes of each body would increment the bone indexes correctly. My sincerest thanks for the help and I hope this can be useful to someone in the future.