Ali Şahan Yalçın

The Skin-Weight Bug That Survived Six Solvers

·5 min read
Unreal EngineC++RiggingSkinning
The Skin-Weight Bug That Survived Six Solvers

This is a bug story. A rigging bug made my meshes tear apart every time I rotated a bone. I went through six different skin weight methods to fix it, and in the end the bug was in none of them.

The problem

Every rig I made had the same issue. Attach the mesh to the skeleton, rotate an arm or turn the head, and the surface would come apart at the joint. A gap would open in the mesh. That is what bad skin weights look like, so I tried better skin weights.

Six methods

Over about a week I made, in order:

  • Geodesic voxel binding (the Maya and Mixamo way). Distance is measured through a voxel version of the body, so weights do not jump between limbs that are close in space but far apart on the body. The engine version breaks on hollow, double walled game meshes, so I wrote my own solid fill voxel solver.
  • Direct distance, the simple falloff version.
  • Bone heat (Blender style auto weights). Each bone acts like a heat source and the heat spreads across the surface to set the weights. I used the Eigen math library to solve it.
  • libigl's bounded biharmonic weights, a mesh-weighting method from a math library. I copied the whole library into the project, got it working, then removed it.
  • Direct Manny weight transfer. Copy Manny's weights by nearest vertex.
  • Template transfer. Reshape the weighted Manny mesh onto my fitted skeleton and copy its weights over with UE's FTransferBoneWeights, filling in the gaps where the two meshes do not line up. This one was clearly the best.
  • I also pulled in EA's Dem Bones as a seventh option and drove it with generated poses.

    And they all still tore. Some worse than others, but the arm always came apart.

    The test that was wrong

    I wanted to measure the tearing, not just see it, so I made a stress test. Regenerate a rig, pose the mesh into a hard rotation (raise the arm 45 degrees, bend the knee 60 degrees), and measure how far each edge of the mesh stretches past its resting length. Good weights keep edges near 1.0x (no stretch). Bad weights stretch them 30x or 100x.

    The problem: the test kept saying the elbow was 1.00x, perfectly clean, on a mesh I could see tearing. For a few days I trusted the number instead of what I was looking at.

    What was actually wrong

    The stress test measures stretch along triangle edges. But a UV seam or a hard normal split puts two separate vertices at the exact same spot. They are not joined by a triangle edge, so if the solver gives those two vertices different bone weights, they fly apart when you pose the mesh. My edge test sees nothing, because no edge crosses the gap.

    I wrote a quick check for it. Find every pair of vertices sitting on top of each other and compare their weights. On the mannequin there were 1,933 pairs with different weights. The worst ones were split all the way, one vertex on the hand and its twin on a different finger. So of course it tore, and of course every method did it, because each one weights every vertex on its own.

    The fix

    The fix is not in the solver. Before binding, I weld the duplicate vertices that sit on the same spot (FMergeCoincidentMeshEdges) so every point on the surface is a single vertex with a single weight. The UV and normal seams are kept on the side, so the mesh still looks exactly the same. One point, one weight. On the mannequin that merged 5,142 seam edges and dropped the pairs with different weights from 1,933 to 0.

    The tearing was gone, in every method at once.

    After that

    With the seams fixed I could trust my stress numbers, and they showed a few more things:

  • Dem Bones was the worst option. Its re-solve makes the good transfer weights worse. Template transfer beat it at every joint (shoulder 12x vs 33x edge stretch), so I kept template transfer as the default and left Dem Bones as an experimental option.
  • The head was still weak. Spine weight was bleeding all the way up into the skull (a chest bone carrying 40 to 80 percent at head height), so turning the head left half the skull behind. I added a pass that moves that weight onto the neck and head bones, blended by height so it does not make a new seam.
  • For clean retargeting I made the fitter bend the spine and place the clavicles with Manny's own curve instead of a straight line between markers, clamped so a bone never pokes out through the skin on a slim character.
  • Six methods, and the bug was in none of them.