Skip to content

Fix repeated armature bind-pose round trips - #3007

Open
toaster0123 wants to merge 1 commit into
jMonkeyEngine:masterfrom
toaster0123:fix/bind-pose-2383-20261003
Open

toaster0123 wants to merge 1 commit into
jMonkeyEngine:masterfrom
toaster0123:fix/bind-pose-2383-20261003

Conversation

@toaster0123

Copy link
Copy Markdown
Contributor

Fixes #2383.

Repeatedly restoring and saving the same bind pose can stretch or distort a rotated joint hierarchy, even when the original scale is 1. The added test reproduces this with an eight-joint chain: save its bind pose, then call applyBindPose() and saveBindPose() 1,000 times without changing the pose.

Three details caused the saved pose to drift:

  • The separate-transform strategy did not correctly undo the parent's translation, rotation, and scale. It now reverses those components directly.
  • Saving the matrix-based bind pose discarded shear produced by rotated, nonuniformly scaled joints. It now keeps the complete model matrix.
  • Reused matrices retained small errors in their bottom row after inversion. That storage is now reset before rebuilding the matrix.

The regression tests cover both transform strategies, unit and nonuniform scales, identity skinning at the saved pose, and reused bind-matrix storage.

Validation:

  • Seven of the eight new cases fail on unchanged master; all eight pass with this fix.
  • 176 animation, math, and bounds tests pass on Java 21 and Java 25, with one existing disabled test.
  • All 691 core Java sources compile with --release 8 and the repository's strict unchecked-warning options.
  • A separate randomized check passed for 50 seeds, 16-joint chains and branching hierarchies, both strategies, and 1,000 restore/save cycles.

These checks used cached JUnit 5.13.4. The full Gradle build with the project's configured Java 25/JUnit 6 toolchain and renderer/GPU tests have not been run. Coverage uses positive scales; reflection and zero-scale limitations remain. Small floating-point round-off is still expected.

Original discussion: https://hub.jmonkeyengine.org/t/issue-calling-armature-savebindpose/48046

Fixes jMonkeyEngine#2383. Restore separate parent transforms componentwise, preserve the
complete matrix bind pose, and reset reused affine matrix storage.
Add regression coverage for both joint transform strategies.

Forum: https://hub.jmonkeyengine.org/t/issue-calling-armature-savebindpose/48046
@riccardobl
riccardobl marked this pull request as ready for review October 3, 2026 20:06
Comment on lines +182 to +183
inverseModelBindMatrix.loadIdentity();
jointModelTransform.getOffsetTransform(inverseModelBindMatrix, Matrix4f.IDENTITY);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, focused fix — going through getOffsetTransform() instead of decomposing into T/R/S is the right call, and keeping the full matrix for shear is a solid catch.

Two things I'd like you to double-check here:

  1. Freshness of the cached local transform. getOffsetTransform() reads the transform cached inside the JointModelTransform, which is only refreshed by updateModelTransform(). The previous code handed initialTransform straight to JointModelTransform.saveBindPose(...). If the local transform changed since the last model-transform update, the new path could save a slightly stale pose. Your round-trip test never changes the pose, so it can't distinguish the two — worth either confirming that Armature.saveBindPose()/applyBindPose() refresh the model transforms, or adding a case that moves a joint before saving.

  2. Custom JointModelTransform implementations. This call no longer goes through the interface's saveBindPose(Transform, Matrix4f, Joint). If a third-party implementation overrides only that method, this path now skips it. Worth a javadoc note or keeping the delegation if the method is meant to stay as an extension point.

@jaime-jmebot jaime-jmebot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really solid diagnosis and a tidy, minimal fix — I like that the shear case is handled by keeping the whole model matrix rather than trying to recover it.

  • Routing the bind pose through getOffsetTransform() instead of a T/R/S decomposition is the right call, and the loadIdentity() reset for reused matrices is a good catch.
  • The separate-transform fix looks correct: reversing the parent's T/R/S components directly is exactly what the inverse composition needs for a TRS chain.
  • The regression tests cover both strategies and both scale flavours, and the fact that 7 of 8 fail on master is convincing evidence they actually pin the bug.
  • Only open point is in the inline comment: whether the model transform cached inside the JointModelTransform is guaranteed fresh at saveBindPose() time, plus the small API note about custom implementations.

Good luck with the full Gradle run when you can.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

armature.saveBindPose() corrupts model

2 participants