Stitch & loop motions | Uthana API Docs

Preview API

This API is a preview and is unstable—it is expected to change in future releases. Reach out via support@uthana.com, Slack, or Discord to share your feedback.

Not yet in the official clients

The stitch & loop API is not yet covered by the official Python (uthana) or JavaScript (@uthana/client) clients. Use the raw GraphQL examples below.

Learn more about Stitching and Looping.

Stitch allows you to join two motion clips into a single animation. Using a leading prefix, a trailing suffix, and spatial data at the join points, the stitch model generates a smooth transition between the two motions.

Overview

You provide pose samples for each clip (pelvis/root position + rotation, plus forward-facing yaw) at the required time points. The stitch uses the prefix pose at at_upper_trim_time and the suffix pose at at_lower_trim_time as the connection points, aligns the motions in world space, and returns a new motion id synchronously.

You can use the Uthana web app Stitch UI to play around with stitch and better understand how to position characters for prefix/suffix alignment.

Key concepts and terminology

Before diving into the API, it helps to understand the core concepts and how they map to the stitch parameters.

Coordinate system and axes

Uthana uses a right-handed, Y-up coordinate system consistent with Three.js, GLM, and glTF:

Axis Direction Meaning
X Right Positive X points to the character's right when facing +Z
Y Up Positive Y points upward; the floor is at y = 0
Z Forward Positive Z is "forward" when the character has zero yaw

Right-handed Y-up axes

Pose properties and how they map to the API

The stitch model needs to know where each character is and how it's oriented at key moments. It uses this pose data to align the prefix and suffix spatially and generate a smooth transition. Positions are {x, y, z} in meters—e.g. {x: 0.1, y: 0.95, z: 0.5} means 0.1m to the right, pelvis height 0.95m above the floor, 0.5m forward. These properties are required for both the leading (prefix) and trailing (suffix) motion—each motion gets its own StitchParamsInput with full pose data.

You should get these values from your motion loader by evaluating the motion at each required time: seek to the frame, update the animation, and read the world-space transforms from your 3D engine (e.g. getWorldPosition, getWorldQuaternion in Three.js). Pelvis position and rotation come directly from the motion; hips_forward_facing_world_yaw is computed from the left and right hip bone positions (see How to obtain these values).

Suppose you want to space two motions 1 meter apart in a straight line. You would set the prefix pelvis at the last frame (at_upper_trim_time) to {x: 0, y: 0.95, z: 0} and the suffix pelvis at the first frame (at_lower_trim_time) to {x: 0, y: 0.95, z: 1}. Without this spatial data, the stitch model has no way to know where each motion sits in the world, and the transition will look wrong.

What each property is
Property Type What it represents
root_node_world_pos {x, y, z} (meters) Character root position in world space. x and z are horizontal; y is height above the floor.
root_node_world_rot {x, y, z, w} (quaternion) Character root orientation in world space. Identity (no rotation) is {x: 0, y: 0, z: 0, w: 1}.
pelvis_world_pos {x, y, z} (meters) Pelvis bone position in world space. Used in each of at_zero_time, at_lower_trim_time, at_upper_trim_time.
pelvis_world_rot {x, y, z, w} (quaternion) Pelvis bone orientation in world space.
hips_forward_facing_world_yaw radians Angle around the Y axis for hips facing. Computed as atan2(forward.x, forward.z). Examples:
- 0 = facing +Z
- π/2 = facing -X
- π = facing -Z
- -π/2 = facing +X

Quaternions use {x, y, z, w} with w as the scalar component. This matches Three.js, GLM, and common game engines. See Three.js Quaternion and Quaternions and spatial rotation for more on this format. For the coordinate system and glTF reference, see above.

What they're used for

The stitch model uses root and pelvis transforms to:

Without accurate pose data, the transition can look jarring. For production, always sample from your motion loader.

Example: a single pelvis state

Each of at_zero_time, at_lower_trim_time, and at_upper_trim_time is a PelvisStateInput:

{
  "pelvis_world_pos": { "x": 0.12, "y": 0.95, "z": 0.0 },
  "pelvis_world_rot": { "x": 0, "y": 0, "z": 0, "w": 1 },
  "hips_forward_facing_world_yaw": 0
}

Root node vs. pelvis

The root is the hierarchy root; the pelvis is a child bone. Their positions and rotations can differ when the character is posed.

Trim times and the three sample points

Each motion is sampled at three time points:

Sample When Purpose
at_zero_time t = 0 Start of the motion clip
at_lower_trim_time Start of the trimmed segment Where the "useful" part of the clip begins
at_upper_trim_time End of the trimmed segment Where the "useful" part ends

For the prefix, keep at_upper_trim_time slightly before the clip end (e.g. motion_duration - 0.051) to avoid sampling exactly at the boundary.

How to obtain these values

Motion loader required

You will need a motion loader (e.g. GLTF + Three.js, or your engine's equivalent) that can evaluate poses at specific times.

Summary. For programmatic use, the sampling process is:

  1. Load the motion (e.g. GLTF with animation) and the character skeleton.
  2. Seek to each time (0, lower_trim, upper_trim) and update the animation.
  3. Read world-space transforms for the pelvis and root from your 3D engine (e.g. getWorldPosition, getWorldQuaternion in Three.js).
  4. Compute hips_forward_facing_world_yaw: Get world positions of left and right hip bones. The "hips forward" direction is perpendicular to the hip line in the horizontal plane: forward = normalize(cross(left_hip - right_hip, up)). Then yaw = atan2(forward.x, forward.z).

Prerequisites

Before calling the stitch API, you need:

See How to obtain these values for the sampling process. The Uthana web app performs this automatically when you use the Stitch UI.

Step-by-step tutorial

Step 1: Authenticate your request

API_KEY="your-api-key"
API_URL="https://uthana.com/graphql"

Step 2: Build the stitch input

The MotionStitchInput requires character_id, prefix, and suffix. Each of prefix and suffix is a StitchParamsInput with motion metadata and pose samples at three time points.

If you don't yet have a motion loader, you can use these placeholder values for the pose fields to satisfy the schema and test the API flow. They are structurally valid but will not yield good stitch quality—for production, always sample from your motion data.

Property Placeholder value Notes
root_node_world_pos {x: 0, y: 0, z: 0} Origin. Replace with sampled data for real stitches.
root_node_world_rot {x: 0, y: 0, z: 0, w: 1} Identity (no rotation).
pelvis_world_pos (in each sample) {x: 0, y: 0, z: 0} Origin.
pelvis_world_rot (in each sample) {x: 0, y: 0, z: 0, w: 1} Identity.
hips_forward_facing_world_yaw 0 Facing +Z.

Recommended defaults

For stitch_duration, stitch_loop, prompt, and other optional fields, see Recommended defaults.

Required fields (must be derived from your motion data):

For prefix motions, keep the upper trim time slightly before the clip end (e.g. duration - 0.051) to avoid sampling at the exact boundary, which improves stitch quality.

Step 3: Call the mutation

curl -X POST "$API_URL" \
  -u "$API_KEY": \
  -H "Content-Type: application/json" \
  -d '{
    "query": "mutation CreateEnhancedStitchedMotion($stitch_input: MotionStitchInput!) { create_enhanced_stitched_motion(stitch_input: $stitch_input) { motion { id } } }",
    "variables": {
      "stitch_input": {
        "character_id": "'