From ab728c7e9aaf9a5e91b0a17b2820289cb4c0fa19 Mon Sep 17 00:00:00 2001 From: crosstyan Date: Wed, 11 Mar 2026 11:54:41 +0800 Subject: [PATCH] fix: use shortest-path rotation interpolation in playback Switch viewer playback from Magnum Math::slerp() to Math::slerpShortestPath() when interpolating adjacent OpenSim frame orientations. Why: - Adjacent OpenSim quaternions can cross sign while representing nearly identical orientations. - Non-shortest-path interpolation can create artificial long-arc spins between valid sampled poses. - That makes playback exaggerate or invent visible bone flips that are not present in the sampled frame states. What changed: - Updated playback interpolation in src/ViewerApp.cpp to use shortest-path quaternion slerp. - Added docs/motion-troubleshooting.md documenting the distinction between viewer interpolation artifacts and upstream IK discontinuities. - Added a README pointer to the troubleshooting note. Investigation log: - Verified the viewer loads .mot through OpenSim state storage and renders PhysicalFrame transforms directly. - Reproduced the target Sports2D/Pose2Sim/OpenSim clip and confirmed the .mot already contains large coordinate discontinuities and limit clamping, indicating upstream IK failure. - Confirmed the viewer also had a separate interpolation issue due to non-shortest-path quaternion slerp. Validation: - Rebuilt with: cmake --build build -j - Relaunched the viewer successfully against the problematic .osim/.mot pair after the fix. --- README.md | 4 +++ docs/motion-troubleshooting.md | 45 ++++++++++++++++++++++++++++++++++ src/ViewerApp.cpp | 5 +++- 3 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 docs/motion-troubleshooting.md diff --git a/README.md b/README.md index 35ba0b7..76de816 100644 --- a/README.md +++ b/README.md @@ -31,3 +31,7 @@ Sample run: /home/crosstyan/Code/opensim-core/OpenSim/Moco/Test/walk_gait1018_subject01.osim \ /home/crosstyan/Code/opensim-core/OpenSim/Moco/Test/walk_gait1018_state_reference.mot ``` + +## Notes + +- Motion troubleshooting notes: [`docs/motion-troubleshooting.md`](docs/motion-troubleshooting.md) diff --git a/docs/motion-troubleshooting.md b/docs/motion-troubleshooting.md new file mode 100644 index 0000000..0f1543a --- /dev/null +++ b/docs/motion-troubleshooting.md @@ -0,0 +1,45 @@ +# Motion Troubleshooting + +## Bone Flips During Playback + +The viewer loads the `.mot` file through OpenSim, converts it to model states, +and renders each body using `PhysicalFrame::getTransformInGround()`. It does +not solve IK, rebuild bones from markers, or apply any custom joint logic. + +Because of that, a visible flip can come from two different places: + +1. The upstream motion data already contains a discontinuity. +2. Playback interpolation between two valid sampled poses introduces an + artificial long-arc rotation. + +## Viewer Behavior + +Playback now uses shortest-path quaternion interpolation between sampled OpenSim +poses. This avoids false in-between spins when adjacent quaternions have +opposite signs but represent nearly the same orientation. + +If a flip is still visible after this fix, the sampled OpenSim motion itself is +already discontinuous. + +## Upstream IK Failures + +For Sports2D / Pose2Sim / OpenSim pipelines, broken IK usually shows up as one +or more of the following: + +- sudden large frame-to-frame jumps in joint coordinates +- joint values snapping to model limits +- marker RMS staying low while anatomically implausible body orientations appear +- left/right ambiguity or bad depth reconstruction in the source TRC + +Sports2D ultimately delegates IK generation to Pose2Sim, which then calls +`opensim.InverseKinematicsTool(...)`. If the generated `_ik.mot` already +contains large discontinuities, the viewer is only displaying that failure. + +## Practical Check + +To separate viewer issues from upstream IK issues: + +1. Load the `.osim` and `.mot` in the viewer. +2. Scrub near the suspicious frame while paused. +3. If the sampled pose itself is wrong, the IK output is broken upstream. +4. If only the in-between motion looks wrong, interpolation is the likely cause. diff --git a/src/ViewerApp.cpp b/src/ViewerApp.cpp index 3d2d0b8..f4da96f 100644 --- a/src/ViewerApp.cpp +++ b/src/ViewerApp.cpp @@ -343,7 +343,10 @@ void ViewerApp::updateSceneAtCurrentTime() { for(std::size_t i = 0; i != _frames.size(); ++i) { const Vector3 translation = Math::lerp(_sceneData.frames[i].translations[sample], _sceneData.frames[i].translations[next], alpha); - const Quaternion rotation = Math::slerp(_sceneData.frames[i].rotations[sample], _sceneData.frames[i].rotations[next], alpha).normalized(); + const Quaternion rotation = Math::slerpShortestPath( + _sceneData.frames[i].rotations[sample], + _sceneData.frames[i].rotations[next], + alpha).normalized(); _frames[i].object->setTransformation(makeFrameMatrix(translation, rotation)); }