Locomotion New
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.
- Shell
- Python
- TypeScript
- React
- C#
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:
strides:4(number of full stride pairs; allowed1to5)move_speed:1.2m/s, relative to the character's height and stride (valid range0.25to8.0)style_id:"neutral_male_a"travel_angle:0degrees —0is North (forward),90is East (right),-90is West (left),180/-180is South (backward); values beyond this range are reduced via modulo 360.
- Shell
- Python
- TypeScript
- React
- C#
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}")
# Using the UthanaCharacters enum for built-in characters
styles = await client.motions.list_locomotion_styles()
result2 = await client.motions.create_locomotion(
UthanaCharacters.tar,
strides=2,
move_speed=1.3,
style_id=styles[0] if styles else "neutral_male_a",
travel_angle=0,
)
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}`);
// Using the UthanaCharacters enum for built-in characters
const result2 = await client.motions.createLocomotion(UthanaCharacters.tar, {
strides: 2,
move_speed: 1.3,
style_id: styles[0] ?? "neutral_male_a",
travel_angle: 0,
});
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>
);
}
var locoQuery = @"
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
}
}
}";
var locoRequest = new
{
query = locoQuery,
variables = new
{
character_id = CharacterId,
strides = 2,
move_speed = 1.3,
style_id = "neutral_male_a",
travel_angle = 0
}
};
var locoJson = JsonSerializer.Serialize(locoRequest);
var locoContent = new StringContent(locoJson, Encoding.UTF8, "application/json");
// POST with the same Basic auth as other GraphQL calls
var locoResponse = await _httpClient.PostAsync(ApiUrl, locoContent);
locoResponse.EnsureSuccessStatusCode();
Parameters
character_id:String!— The character the motion is generated and retargeted for.strides:Int— Count of full stride pairs (1 to 5). Default 4. Each full stride is a left+right or right+left step pair.move_speed:Float— Travel speed in m/s (relative to the character; recommended roughly 0.25 to 6.0, allowed up to 8.0). Default 1.2.style_id:String— Locomotion style from List locomotion styles or the optional query in Step 2 below. Resolves case-insensitively to a known Uthana style. Defaultneutral_male_a.travel_angle:Float— Travel direction in degrees on the ground plane, normalized to -180 to 180.0is forward (north in Uthana's mapping),90is to the right,-90to the left,180or-180is backward. Default 0.
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
- Generate or pick clips — Create one motion per combination you need (same character
character_id), varyingstyle_id,move_speed, andtravel_angleas needed. - Export for your engine — Download GLB or FBX from Download a motion using your motion id and character id.
- Choose root motion vs in-place — Either let the clip move the skeleton root (root motion), or download with
in_place=trueso the character stays under the origin while you move the character transform in code in the direction that matchestravel_angle. See In-place motion. - Loop and switch — Play clips with seamless looping where possible; swap or cross-fade clips when input direction or speed changes. For authored loops and progressive vs cyclical behavior after export, see Stitch and loop motions.
- Three.js
- Unity
Load a retargeted GLB (with animation), drive it with AnimationMixer, and repeat the clip. Update the mixer each frame.
If you used in-place exports, apply world translation yourself: each frame, move your character root along the same planar direction implied by travel_angle (for example 0° forward on +Z in many setups—match your scene convention). If you keep root motion in the clip, translation usually comes from the animated root; align gameplay collision and camera with that motion.
import * as THREE from "three";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
const clock = new THREE.Clock();
let mixer: THREE.AnimationMixer | undefined;
new GLTFLoader().load("/locomotion.glb", (gltf) => {
const root = gltf.scene;
scene.add(root);
const clip = gltf.animations[0];
if (!clip) return;
mixer = new THREE.AnimationMixer(root);
const action = mixer.clipAction(clip);
action.setLoop(THREE.LoopRepeat, Infinity);
action.play();
});
function onFrame() {
requestAnimationFrame(onFrame);
const dt = clock.getDelta();
mixer?.update(dt);
// If you exported with in_place=true, move `root` (or a parent group) each frame
// along the ground-plane direction that matches your locomotion travel_angle,
// e.g. root.position.addScaledVector(desiredWorldDir, speed * dt);
renderer.render(scene, camera);
}
Import the FBX or GLB, put an Animator on your character, and assign your locomotion clip to a state. Call Animator.Move / root motion only when the clip is supposed to drive translation.
Root motion from the clip — Enable root motion on the animator so baked translation in the animation moves the character:
using UnityEngine;
[RequireComponent(typeof(Animator))]
public class LocomotionPlayback : MonoBehaviour
{
Animator animator;
void Awake()
{
animator = GetComponent<Animator>();
animator.applyRootMotion = true;
}
void OnAnimatorMove()
{
// Root motion from the locomotion clip drives transform when applyRootMotion is true
transform.rotation = animator.rootRotation;
transform.position += animator.deltaPosition;
}
}
In-place clip + controller-driven translation — Export with in_place=true, set applyRootMotion = false, and move with CharacterController (or your physics body) in the direction that matches the clip's travel_angle when you pick which clip to play:
using UnityEngine;
[RequireComponent(typeof(Animator), typeof(CharacterController))]
public class LocomotionInPlace : MonoBehaviour
{
public float moveSpeed = 2f;
Animator animator;
CharacterController controller;
void Awake()
{
animator = GetComponent<Animator>();
controller = GetComponent<CharacterController>();
animator.applyRootMotion = false;
}
void Update()
{
var input = new Vector3(Input.GetAxis("Horizontal"), 0f, Input.GetAxis("Vertical"));
if (input.sqrMagnitude > 1f) input.Normalize();
// Drive planar motion in code; pick Animator parameters / states by direction + speed
// so they match the `travel_angle` and `move_speed` used when generating each clip.
controller.Move(transform.TransformDirection(input) * moveSpeed * Time.deltaTime);
}
}
Map travel_angle (Uthana ground-plane degrees: 0 forward, 90 right, etc.) to the same local or world direction you use when selecting which locomotion clip plays.
Error handling
- Shell
- Python
- TypeScript
- React
- C#
# Check for errors in response
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())
import { UthanaError } from "@uthana/client";
try {
const result = await client.motions.createLocomotion(CHARACTER_ID, { strides: 2 });
} catch (err) {
if (err instanceof UthanaError) {
console.error(err.statusCode, err.apiMessage);
}
}
const createLoco = useUthanaCreateLocomotion();
if (createLoco.isError) {
console.error(createLoco.error);
}
if (result.Errors != null && result.Errors.Length > 0)
{
foreach (var error in result.Errors)
{
Console.WriteLine($"Error: {error.Message}");
if (error.Extensions != null && error.Extensions.ContainsKey("code"))
{
Console.WriteLine($"Code: {error.Extensions["code"]}");
}
}
}
Next steps
- See Text to motion for open-ended, prompt-driven generation
- See Stitch and loop motions for post-processing, including progressive root motion vs in-place looping
- Download a motion to export assets
- Explore Retargeting for applying motions to other characters on download
- Check the API reference for full schema, including
locomotion_stylesandcreate_locomotion