Locomotion | Uthana API Docs

Generate Consistent Walking-Style Locomotion

Generate consistent walking-style locomotion for a character in a specified direction with controllable style, speed, and stride count. Use locomotion when you need predictable, repeatable, looptable travel motion for gameplay, background NPCs, or any workflow where a text prompt is too open-ended to rely on the same path and style every time.

Overview

Text-to-motion is ideal when you want creative motion from a natural language description. Locomotion is different: you choose where the character should travel, how fast, how many stride pairs to use, and which style to apply, so clips stay stable and well-suited for looping and stitching in your own pipeline. Results are returned immediately, like text-to-motion, with no job polling.

Call locomotion_styles to list the style_id values accepted by create_locomotion (for example neutral_male_a).

Step-by-step tutorial

Step 1: Set up the client

Install your client library and authenticate using your API key. See the quickstart for setup instructions for each language.

Step 2: Query locomotion styles (optional)

Uthana exposes a query that returns every style_id you can pass to create_locomotion, for example neutral_male_a, aeroplane.

Example Queries

curl -sS -X POST "https://uthana.com/graphql" \
  -u "$API_KEY:" \
  -H "Content-Type: application/json" \
  -d '{"query":"query { locomotion_styles }"}'
async def main():
    styles = await client.motions.list_locomotion_styles()
    print(styles)

asyncio.run(main())
const styles = await client.motions.listLocomotionStyles();
console.log(styles);
import { useUthanaLocomotionStyles } from "@uthana/react";

function StyleList() {
  const { styles } = useUthanaLocomotionStyles();
  return <ul>{styles?.map((s) => <li key={s}>{s}</li>)}</ul>;
}
var styleBody = @"{"query":"query { locomotion_styles }"}";
var styleContent = new StringContent(styleBody, Encoding.UTF8, "application/json");
_httpClient.DefaultRequestHeaders.Authorization = new(
    "Basic", Convert.ToBase64String(Encoding.UTF8.GetBytes($"{_apiKey}:"))
);
var styleResp = await _httpClient.PostAsync(ApiUrl, styleContent);
var styleJson = await styleResp.Content.ReadAsStringAsync();
// Parse locomotion_styles from `styleJson` as needed

Step 3: Generate locomotion

Call create_locomotion with the character to retarget to (required character_id) and the motion parameters. If you omit optional fields, the API uses these defaults:

Example Queries

curl -X POST "https://uthana.com/graphql" \
  -u "$API_KEY:" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "mutation CreateLocomotion($character_id: String!, $strides: Int, $move_speed: Float, $style_id: String, $travel_angle: Float) { create_locomotion(character_id: $character_id, strides: $strides, move_speed: $move_speed, style_id: $style_id, travel_angle: $travel_angle) { motion { id name } } } }",
    "variables": {
      "character_id": "'$CHARACTER_ID'",
      "strides": 2,
      "move_speed": 1.3,
      "style_id": "neutral_male_a",
      "travel_angle": 0
    }
  }'
async def main():
    result = await client.motions.create_locomotion(
        CHARACTER_ID,
        strides=2,
        move_speed=1.3,
        style_id="neutral_male_a",
        travel_angle=0,
    )
    print(f"Created motion: {result.motion_id}")

asyncio.run(main())
const result = await client.motions.createLocomotion(CHARACTER_ID, {
  strides: 2,
  move_speed: 1.3,
  style_id: "neutral_male_a",
  travel_angle: 0,
});
console.log(`Created motion: ${result.motion_id}`);
import { useUthanaCreateLocomotion, useUthanaLocomotionStyles } from "@uthana/react";

function LocomotionPanel({ characterId }: { characterId: string }) {
  const { styles } = useUthanaLocomotionStyles();
  const createLoco = useUthanaCreateLocomotion();

return (
    <div>
      <p>Styles: {styles?.join(", ")}</p>
      <button
        onClick={() =>
          createLoco.mutate({
            character_id: characterId,
            strides: 2,
            move_speed: 1.3,
            style_id: "neutral_male_a",
            travel_angle: 0,
          })
        }
        disabled={createLoco.isPending}
      >
        {createLoco.isPending ? "Generating..." : "Generate locomotion"}
      </button>
      {createLoco.data && <p>Motion ID: {createLoco.data.motion_id}</p>}
    </div>
  );
}

Step 4: Handle the response

The mutation returns a motion with id and name immediately, similar to text-to-motion. Use the motion id in retargeting and downloading flows. When listing motions, filter with motions(methods: ["Locomotion"]) where supported.

Step 5: Integrate with a motion controller

Locomotion clips are a good fit for motion controllers because each clip encodes a fixed travel direction (travel_angle), speed (move_speed), and style (style_id). You can build a small library of clips (cardinal directions, a few speeds, one or more styles) and select or blend between them from gameplay input instead of relying on a single open-ended prompt.

Controller checklist

  1. Generate or pick clips — Create one motion per combination you need (same character character_id), varying style_id, move_speed, and travel_angle as needed.
  2. Export for your engine — Download GLB or FBX from Download a motion using your motion id and character id.
  3. Choose root motion vs in-place — Either let the clip move the skeleton root ( root motion), or download with in_place=true so the character stays under the origin while you move the character transform in code in the direction that matches travel_angle. See In-place motion.
  4. Loop and switch — Play clips with seamless looping where possible; swap or cross-fade clips when input direction or speed changes.

Parameters

Error handling

if echo "$RESPONSE" | jq -e '.errors' > /dev/null; then
    echo "Error occurred:";
    echo "$RESPONSE" | jq '.errors';
fi
from uthana import UthanaError

async def main():
    try:
        result = await client.motions.create_locomotion(CHARACTER_ID, strides=2)
    except UthanaError as e:
        print(e.status_code, e.api_message)

asyncio.run(main())

Next steps