Repository navigation
MMCP 1.4: joints that move, curves keyed where they bend (0.12.0) - #13
Merged
Merged
Conversation
A rig whose joints are turned is carried perfectly by rotations and a root. A rig that DEFORMS by moving joints is not: a face that opens by sliding its lips, a fin membrane, a tongue. Measured on one such rig against the pose its own pipeline renders, rotations alone leave the membrane 38.8 units out of a 223-unit shoulder and the mouth shut. The container was never the limit -- glTF has had a translation channel per node all along, and this writes one for any joint that does not simply sit at its rest offset. On a 357-joint rig that is a handful of them, so the file grows by what is used and no more. `MotionResult.translations` is (B, T, J, 3), a joint's offset from its parent in the parent's frame, which is what glTF means by a node's translation and what `rest_translation` is when nothing has moved; for the root it is the world position, which is the same thing. Opt-in at both ends. A model advertises `supports_joint_translation` and the server drops the channels for one that does not, so a backbone cannot ship them to clients that were never told to expect them. A reader that ignores them still gets a correct skeleton -- turned right, placed right -- and gets a stiffer one. Rebuilt the way a reader rebuilds it, the same take that was 38.8 units out on the membrane and 13.5 inside the mouth comes back at 0.0001, which is float32. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A key on every frame is exact and is what a take being retargeted wants. It is not what an animator wants: a curve keyed everywhere can be rebaked and nothing else. So `MotionResult.keyframes` lets a model name the frames each channel is worth keying, and the encoder gives those samplers their own input accessors. The frames are the model's to choose. Only the model knows what straightening its own curve costs -- an encoder sees quaternions and cannot tell which of them a long chain is about to multiply into a visible error. A model that says nothing stays dense, so this is additive: the same request against the same backbone returns the same document as before unless the backbone fills it in. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A key on every frame is exact, and exact is what a take being RETARGETED wants -- at one-frame spacing the tangents have nothing left to argue about. A thinned curve is what someone who has to OPEN the result wants: one keyed everywhere can be rebaked and nothing else. Those are opposite, the server cannot see which caller it has, and a model that picks for itself picks wrong half the time. So `options.keyframe_budget` asks: a tolerance in the model's own world units, omitted meaning key every frame. Omitted is the default because dense is the answer that loses nothing. A model advertises `supports_keyframe_reduction` and the server refuses the option where it is not advertised, rather than ignoring it -- a caller that sent a number and got a dense answer back has no way to tell that the number went nowhere. test_capabilities_unchanged_for_an_old_backbone pops the added capabilities instead of carrying them into the golden, which is what was already done for supports_motion_reference_mixed. The golden keeps saying what 0.9 said, and the list in the test is the whole of what has been added since. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`ts_acc` was assigned and never read: the call is there so the dense timeline is the document's first accessor, used or not. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
parse_gltf took the frame count from the first rotation track and copied every track into an array of that length. A channel keyed where it bends (MotionResult.keyframes) is shorter than that, so a document the server now writes crashed the SDK's own reader. And a translation channel was read as a world position wherever it appeared, which on any joint but the root it is not. Every track is now put on the sample's timeline -- the extension's fps and num_frames, or the longest input for a document without them -- the way a glTF reader plays it: slerp along the shorter arc for rotations, a line for translations, held outside the keyed range. Dense documents come out as before. Per-joint translations arrive as `local_translations` [T, J, 3]: the channel where there is one, the rest offset where there is not, the root's world position for the root. `posed_joints` keeps meaning world positions and so stays the root's. tests/test_joint_translation.py covers the three 1.4 additions through the server and back through this parser; two of its tests fail against the 0.11.0 parser. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… bend (0.12.0) Releases what pfx-motion has been running from a vendored build of feat/joint-translation, now on top of 1.3: - supports_joint_translation + MotionResult.translations: a translation channel for each joint that leaves its rest offset, the offset from its parent. The server drops them for a model that does not advertise the capability. - MotionResult.keyframes: channels keyed on the frames the model names, each sampler with its own input. - supports_keyframe_reduction + options.keyframe_budget: the caller asks for thinned curves; refused with invalid_options where not advertised. All three are opt-in on the model's side. A 1.3 client against a model that advertises none of them gets the same motion as before. Docs: versioning, capabilities, generate-request options, response channels, MotionResult fields, and what the client parser returns. test_ground_height stops pinning the protocol it shipped in; the smoke tests pin the current one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Releases
feat/joint-translationon top of 1.3 as MMCP 1.4 / motionmcp-sdk 0.12.0. pfx-motion has been running that branch from a vendored wheel (0.10.0 plus these features, minus 0.11.0). The Animatica API, on PyPI 0.11.0, rejects the capabilities those backbones publish (extra_forbidden), so their models drop out of the registry.What's in 1.4
All three are opt-in on the model's side:
supports_joint_translation+MotionResult.translations(B, T, J, 3): a glTFtranslationchannel for each joint that leaves its rest offset, giving its offset from the parent. This is for rigs that deform by sliding joints (face, fin membrane, tongue). The server drops the channels for a model that doesn't advertise the capability.MotionResult.keyframes: channels keyed only on the frames the model names, each sampler with its owninputaccessor. Anything left out stays dense.supports_keyframe_reduction+options.keyframe_budget(float > 0): the caller asks for thinned curves. A model that doesn't advertise reduction refuses the option withinvalid_options.Added on top of the branch
parse_gltftook the frame count from the first rotation track, so a document with sparse keys crashed the SDK's own reader. It also read any translation channel as a world position. Now every track is resampled onto the sample's timeline (slerp for rotations, held outside the keyed range). Per-joint offsets come back as a newlocal_translations, andposed_jointsstays world positions (the root's).tests/test_joint_translation.pycovers all three features through the server and back through the parser. Two of its tests fail against the 0.11.0 parser. The branch commits had no tests for translations or keyframes.MotionResult, client parsing).ts_accin the encoder was assigned for its side effect only; it's now a plain call with a comment.Verified
pytest, with the[server]extras).options.keyframe_budgetpassesfrom_wire.After merge: tag
v0.12.0on main, andrelease.ymlpublishes to PyPI.🤖 Generated with Claude Code