Skip to main content

Response

The response body is a glTF 2.0 document.

Content-Type is model/gltf+json for JSON glTF or model/gltf-binary for .glb, per Accept negotiation.

What's in it​

  • A nodes[] hierarchy and skins[] definition mirroring the request's skeleton (joint names preserved).
  • One animations[] entry per generated sample, named sample_0, sample_1, … Each animation has dense per-frame channels with LINEAR interpolation for joint rotations and the root translation.
  • Buffers / bufferViews / accessors backing the animation data. In the JSON variant, buffers are embedded as data: URIs; in the binary variant they live in the GLB binary chunk (no base64 overhead).
  • An extensions.MMCP_motion block carrying motion metadata that doesn't fit cleanly in glTF. The corresponding extensionsUsed entry is "MMCP_motion".
Joint set scope

Clients SHOULD treat the response's joint set as authoritative for the joints listed and leave any additional bones in the user's actual rig (IK controls, twist bones, fingers, face) to client-side rig logic. The response only animates joints that were sent in the request's skeleton.joints.

extensions.MMCP_motion​

{
"version": "1.0",
"model": "kimodo-soma-rp",
"fps": 30.0,
"canonical_to_request": {
"Hips": "mixamorig:Hips",
"LeftFoot": "mixamorig:LeftFoot"
},
"samples": [
{
"name": "sample_0",
"num_frames": 150,
"chunk_boundaries": [],
"foot_contacts": {
"LeftHeel": [true, true, false],
"LeftToe": [true, true, true],
"RightHeel": [false, false, true],
"RightToe": [false, true, true]
}
}
]
}
FieldTypeNotes
versionstringProtocol version that produced this response
modelstringEchoed model id
fpsnumberFrame rate of the animation
canonical_to_request{ canonical_joint: request_joint } | nullPresent only when the server retargeted the canonical skeleton to the client's skeleton. Maps the canonical joint name (used internally) to the request's joint name (used in samples[].foot_contacts). Servers with supports_retargeting: false omit this field
samples[].namestringMatches the corresponding animations[].name
samples[].num_framesintFrame count
samples[].chunk_boundariesint[]Frame indices at which the server stitched internal chunks (relevant when the model declares chunking: "stitched"). Empty array when the request fit in one chunk
samples[].foot_contacts{ joint_name: bool[] } | { "$accessor": int } | nullPer-frame contact state, keyed by joint names from the request's skeleton.joints. For long clips, MAY reference an accessor (uint8 booleans, length num_frames × len(joints)) via { "$accessor": <accessor_index> }; the joint-name → channel-index mapping then lives in samples[].foot_contact_joints: string[]
samples[].trajectoryobject | nullThe travel trajectory: the path the motion moves along, without the body's own sway, surge, height or twist (see below). Sent by servers with supports_trajectory: true; null when the body could not be read

Clients that don't need the metadata can ignore the extension entirely and still get a valid glTF.

The travel trajectory​

A client that plays a motion in place (a game cycle, a character driven by a controller) must take its travel out. Pinning the root on the ground takes out too much: the pelvis surges forward and back and sways side to side with every step, and a game moving the character at the walk's speed then sees the feet slide. samples[].trajectory is the travel alone, found from the centre of mass with the gait taken out and fitted with the simplest model a game can re-apply:

"trajectory": {
"model": "line",
"loop": true,
"params": {"speed": 1.052, "heading_deg": 0.1, "off_cm": 0.9},
"heading": "fixed",
"markers": {"LeftFootDown": [0, 31], "RightFootDown": [15, 46]},
"position": [[0.0, 0.021], [0.0, 0.056], ...],
"yaw": [0.0, 0.0, ...]
}
FieldTypeNotes
modelstringThe shape: still, line, arc, or bezier, the simplest within tolerance across the travel path. Along it the trajectory moves with the body (its centre of mass projected onto the shape), not at a constant speed, so played in place the body does not float forward and back with each stride. A loop is only ever still, line or arc, and closes on exactly its root's travel over the cycle, so it repeats
loopboolThe request's options.loop
paramsobjectThe shape's numbers: speed (mean, m/s) and speed_range [min, max], heading_deg (a line's), radius_m, turned_deg, turn_deg_s (an arc's), control_points_xz (a Bezier's); off_cm is its RMS distance across the travel path
position[x, z][]Per frame, on the ground plane, metres, in the response's frame
yawnumber[]Per frame, radians about +Y, 0 at frame 0: the heading, where the body faces (square to its hips, averaged over a stride), not where its path goes. A strafe keeps facing forward; a turn on the spot turns; a twist that comes back is not a turn. A loop's closes on its turn over the cycle
headingstringHow yaw was fitted: fixed, steady (a constant turn rate), keyed (a curve that only turns one way), or path (no hips to read a facing from: the path's direction)
markersobjectFoot sync markers: LeftFootDown / RightFootDown, the frames each foot comes down on

To play a sample in place, move frame t by the inverse of T(position[t]) · Ry(yaw[t]), then by T(position[0]): the root ends where it began, facing the way it began, and every other part of the motion is untouched. For root motion, put T(position[t]) · Ry(yaw[t]) (relative to frame 0) on a ground root bone and parent the hips to it: the same clip then plays with root motion, or in place by ignoring the root.