DEV Community

orca forge
orca forge

Posted on Originally published at forge.workstyle.tech

Why a VRM Right Hand Disappeared: Empty Cuff Ring and NaN

📝 Originally published (in Japanese) at forge.workstyle.tech.

TL;DR A VRM export made only the right hand disappear, even across edits and poses. The shared cuff position found no right-hand vertices, leaving an empty ring. Its mean produced NaN values that spread to the pivot and finger bones.

Conclusion First

The disappearance of the right hand in the VRM was caused by the right hand's vertices failing to reach the common cuff position for both arms, which allowed NaN (Not a Number) values to propagate from an empty loop. We fixed this by ensuring that scaling transforms are not baked into the vertices, and by adding a safeguard that halts the process if NaN values are detected in empty loops or bones.

Was the Disappearing Right Hand a Display or Pose Issue?

On October 5 at 17:01 UTC, a user reported an issue: only the right hand was missing on screen, both in an unedited state and regardless of the pose. My first thought was that it was a visibility toggle or posing issue.

When I inspected the VRM exported at 17:11 UTC, the Hands mesh had 1,462 vertices. Of those, 711 vertices—accounting for the entire right hand—had their positions set to NaN. The renderer simply doesn't draw vertices with NaN coordinates. The invisible hand wasn't a rendering bug after all.

Initially, I checked component visibility toggles, edit operations, weight sources, poses, and culling. (Weights define how much a bone influences vertices.) The hand's scale was also set to 1. None of these were the culprit.

At which stage was NaN born?

I investigated the pipeline—the processing flow up until export—stage by stage. The files were normal up until the manual adjustment stage. Starting from the files in the synchronization stage, the rest matrix (a matrix representing the reference pose of the bones) and lengths for the 15 bones of the right finger were NaN.

NaN did not suddenly appear during export. Even before the synchronization stage, the input for the coordinate generation process was already corrupted.

Why the Ring at the Cuff's Vertex Became Empty

The data before modification was normal. The change began with the influence of the 3D editing done afterwards.

In the modification, a shrink transformation was attached to the Hands object. In world coordinates (coordinates based on the entire scene), it changed from -0.586 to +0.662, and the right hand was shifted about 7cm inward.

After that, step 1 of replacing the head bakes the transformation of all objects into the vertices. Baking refers to the process of reflecting the transformation attached to an object into the vertex coordinates. Although step 2 of synchronization also has a process called matrix_world = Identity, it is after baking, so the transformation cannot be restored here.

The problematic calculation was in step 6. The cuff position was determined by a common value for both left and right sides.

cuff = max|x|  # approximately 0.63
x_start = approximately 0.61
Enter fullscreen mode Exit fullscreen mode

The right hand only reaches up to -0.586. Even if we search for vertices on the cuff side from approximately 0.61, there are no corresponding vertices for the right hand. The ring (a collection of vertices along the cuff) on the right became empty.

Taking the average of an empty ring results in NaN. The pivot (the reference point for transformation) becomes NaN from that value, and NaN spreads to the 15 bones of the right fingers as well. The direct cause of the destruction of 711 vertices was handling different ranges on the left and right with a common cuff value.

How to Stop the Empty Loop and NaN in Bones

The correction was done in two stages.

In the stage of replacing the head, the hands are excluded from the conversion target. The reduction that was attached to the hands was prevented from being baked into the vertices.

In the synchronization stage, a process was added to raise a RuntimeError if the loop is empty. Furthermore, if there is an NaN in the bones, it is stopped by an assert (a check that confirms the condition and stops the process if it is not met). This is to detect abnormal values before proceeding and exporting VRM.

Re-execution took real 0m21.657s and exited with 0. The log output included "hands Ă—0.924 (from sleeve to fingertips 0.134 m)". In the confirmation output, the number of NaNs in the 1462 vertices of the Hands mesh was 0.

It was confirmed that the hands were restored. However, this alone did not reveal other problems in the processing system.

Why Python Errors Don’t Stop Execution in Our Pipeline

In a related log from another day, a 3D editing application was returning exit code 0 despite encountering Python errors. Relying solely on the exit code meant the calling process couldn’t detect the anomaly.

On 10-06, I added --python-exit-code 1 to some steps. By 10-07, all steps were routed through a common function, using both --python-exit-code 1 and set -e -o pipefail. The grep command for log verification was wrapped in { grep … || true; } to prevent the entire process from halting if no results were found.

Additionally, I introduced a FAIL_AT=<step> option for testing. Setting FAIL_AT=ring intentionally triggered a failure, confirming that the process stopped with exit code 1 and subsequent steps were skipped.

This exit code issue is separate from the problem of the missing right hand. However, if a pipeline detects an anomaly but continues processing, corrupted values will propagate downstream. It’s essential not only to log errors but also to ensure the process halts at the correct point when anomalies occur.

How to Verify Values Are Created at the Right Stage

Simply chasing NaN vertices didn't lead to the root cause of the disappearing right hand. It was only after comparing each stage in order—applying transforms, baking transforms, and defining the cuffs—that I finally tracked it down to an empty loop.

When processing values shared between the left and right sides, you can't assume both sides will have the same range of data. To fix this, I made sure the calculation aborts if the loop is empty and added validation for the bone values as well.

Finally, I intentionally induced failures to confirm whether the pipeline truly stops when a step returns an error. The fix wasn't complete just by bringing the right hand back; it was about ensuring we never miss an anomaly the next time one occurs.

Related Articles

Key takeaways

  • Check whether the issue affects only the right hand and reproduces across edits and poses.
  • Check the exported VRM Hands mesh for NaN values.
  • Compare files before hand editing, after transform baking, and at the sync stage.
  • Stop when the cuff ring is empty, and check bones for NaN values.
  • Make Python errors return exit code 1 and confirm later pipeline steps do not run.

Top comments (0)