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 andskins[]definition mirroring the request's skeleton (joint names preserved). - One
animations[]entry per generated sample, namedsample_0,sample_1, … Each animation has dense per-frame channels withLINEARinterpolation 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_motionblock carrying motion metadata that doesn't fit cleanly in glTF. The correspondingextensionsUsedentry is"MMCP_motion".
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]
}
}
]
}
| Field | Type | Notes |
|---|---|---|
version | string | Protocol version that produced this response |
model | string | Echoed model id |
fps | number | Frame rate of the animation |
canonical_to_request | { canonical_joint: request_joint } | null | Present 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[].name | string | Matches the corresponding animations[].name |
samples[].num_frames | int | Frame count |
samples[].chunk_boundaries | int[] | 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 } | null | Per-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[].trajectory | object | null | The 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, ...]
}
| Field | Type | Notes |
|---|---|---|
model | string | The 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 |
loop | bool | The request's options.loop |
params | object | The 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 |
yaw | number[] | 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 |
heading | string | How 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) |
markers | object | Foot 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.