three.ws

@three-ws/pose

Turn a phrase into a pose, a deterministic seed plus the full joint-rotation map for a rigged 3D avatar.

npm downloads license node

Install · Quick start · API · How it works · three.ws


@three-ws/pose is the official client for the three.ws Pose Studio, the engine behind three.ws/pose. It maps a natural-language pose description ("warrior stance", "wave hello", "sitting cross-legged") to a deterministic seed and the complete Euler joint-rotation map for the three.ws humanoid mannequin, picked from an in-repo library of named preset poses. The same prompt always yields the same pose, so it is a perfect way to seed or initialize a rigged character before you hand off to keyframes or live control. It runs the pose_model algorithm locally over the bundled preset library (pure, deterministic compute, no model inference, no network), and can optionally call the hosted tool on /api/mcp-3d instead. It pairs with @three-ws/forge (which makes & rigs the avatar) and @three-ws/avatar (which renders it).

Why

You have a rigged humanoid GLB and you want it to do something, not a baked clip, just a single, named static pose to start from. Hand-authoring Euler rotations per joint is tedious and error-prone: which axis bends the elbow, how far does the shoulder open, where does the root drop for a crouch? Pose Studio answers that once, for a curated library of real poses, and exposes it as a single call:

This is the SDK twin of the 3D Studio MCP server: the same preset engine, exposed as plain functions instead of an MCP tool.

Install

npm install @three-ws/pose

Zero runtime dependencies. Works in Node 18+ and the browser (uses WebCrypto, and fetch only on the optional hosted lane). To render or rig the avatar you pose, add @three-ws/avatar and @three-ws/forge.

Quick start

No key, no wallet, no network:

import { poseSeed } from '@three-ws/pose';

const pose = await poseSeed('wave hello');

console.log(pose.presetId);   // → 'wave'
console.log(pose.seed);       // → '4ae1977023a2568b' (stable sha256-derived)
console.log(pose.parameters); // → { shoulderR: { x: 0, y: 0, z: -2.45 }, elbowR: { x: -1.2, … }, … }
console.log(pose.previewUrl); // → https://three.ws/pose?seed=4ae1977023a2568b&preset=wave

Apply the rotations straight to a Three.js skeleton:

import { poseSeed } from '@three-ws/pose';

const { parameters } = await poseSeed('the thinker');

for (const [jointName, euler] of Object.entries(parameters)) {
  if (jointName === 'rootPosition') {
    rig.position.set(euler.x, euler.y, euler.z); // optional whole-figure offset
    continue;
  }
  const bone = rig.getObjectByName(jointName);
  if (bone) bone.rotation.set(euler.x, euler.y, euler.z); // radians
}

Same prompt, same pose, useful as a fixed initialization seed:

const a = await poseSeed('crouch');
const b = await poseSeed('crouch');
console.log(a.seed === b.seed); // → true, always

API

poseSeed(prompt, options?) → Promise<PoseResult>

Resolve a natural-language pose description to a deterministic seed and the full joint-rotation map. prompt is a string, 1-500 characters. Runs locally.

Per-call options

Option Type Default Notes
signal AbortSignal none Cancel the call.
headers Record<string,string> none Extra headers (hosted lane only).

createPose(options?) → PoseClient

Build a client with fixed configuration. With no transport option the client resolves poses locally, exactly like the top-level poseSeed. Passing any of baseUrl, apiKey, or fetch switches it to the hosted pose_model tool on POST /api/mcp-3d (one JSON-RPC tools/call per pose):

Option Type Default Notes
baseUrl string https://three.ws API origin. Selects the hosted lane.
fetch typeof fetch globalThis.fetch E.g. a payment-aware x402 fetch. Selects the hosted lane.
apiKey string none OAuth bearer; hosted calls run operator-funded. Selects the hosted lane.
previewBase string https://three.ws/pose Base URL for the returned previewUrl (both lanes).
headers Record<string,string> none Default headers on every hosted-lane request.

On the hosted lane a keyless call is x402-priced per call: the server answers 401/402 with a payment challenge, surfaced as PaymentRequiredError with the accepts array. Settle it with a payment-aware fetch (e.g. @three-ws/x402-fetch) or pass an apiKey to run operator-funded. The local lane never charges anything.

Returns PoseResult

Field Type Notes
seed string 16-hex stable id, sha256(prompt|presetId).slice(0,16).
presetId string The picked preset's id, e.g. 'wave', 'warrior2', 'crouch'.
presetLabel string Human label, e.g. 'Wave hello', 'Warrior II (yoga)'.
group string One of Standing, Action, Sitting & Floor, Expressive.
parameters Record<string, { x, y, z }> Joint → Euler rotation in radians. May include rootPosition (a translation, not a rotation).
previewUrl string Open the result on three.ws/pose with seed + preset params.
match { score: number, reason: string } reason is token-match or no-match-deterministic-pick.
groups string[] All four preset groups, for building a picker.

parameters follows the mannequin convention: in rest pose every rotation is 0, arms at the sides. shoulder.z opens an arm outward, shoulder.x is forward(−)/ back(+), elbow.x bends (negative bends the forearm up). Joints not present in a pose are at rest (0).

presetPose(presetId, options?) → Promise<PoseResult>

Skip selection and resolve a specific preset by id, handy once a user has chosen one from a picker. The local lane looks the preset up directly (never through prompt scoring, so the id you ask for is always the preset you get, with match.reason === 'preset-id') and seeds with the preset id as the prompt, so the same preset always returns the same seed.

listPresetGroups() → string[]

The four pose groups, returned synchronously for menu scaffolding: ['Standing', 'Action', 'Sitting & Floor', 'Expressive'].

How it works

poseSeed() runs the pose_model algorithm in-process, over the preset library bundled with the package (the same library the hosted tool serves). It is pure deterministic computation: it scores your prompt against the named presets, picks one, derives a seed, and returns the preset's pre-authored rotation map. No image model, no GPU, no state, no network.

prompt ("warrior stance")
        │
        ▼
  tokenize → score every preset by token overlap
  (preset id + label + group all contribute vocabulary)
        │
        ├─ best score > 0 ──▶ token-match  ─────────┐
        └─ no overlap ──────▶ sha256(prompt) % N ───┤  (deterministic fallback)
                                                     ▼
                         picked preset { id, label, group, pose }
                                                     │
        seed = sha256(prompt | presetId).slice(0,16) ▼
                          { seed, presetId, parameters, previewUrl, … }

The preset library spans four groups: Standing (T-pose, A-pose, relaxed, contrapposto, arms-up, wave, hands-on-hips, salute), Action (walk-step, run, jump, punch, archery, superhero-landing, fighting-stance), Sitting & Floor (chair, floor, kneel, crouch, thinker), and Expressive (praying, meditate, warrior II, arabesque, flex, point, facepalm, bow). It is the same data the public /pose page renders; enumerate it at runtime from the exported PRESETS array rather than hardcoding.

The hosted lane: the raw HTTP

The same engine is hosted as the pose_model tool on the 3D Studio MCP server. The SDK calls it only when you configure a transport (see createPose above); the wire call is a standard MCP tools/call, x402-priced for keyless callers:

const res = await fetch('https://three.ws/api/mcp-3d', {
  method: 'POST',
  headers: {
    'content-type': 'application/json',
    accept: 'application/json, text/event-stream',
  },
  body: JSON.stringify({
    jsonrpc: '2.0',
    id: 1,
    method: 'tools/call',
    params: { name: 'pose_model', arguments: { prompt: 'warrior stance' } },
  }),
});

const { result } = await res.json();
const pose = result.structuredContent;
// → { seed, preset_id, preset_label, group, parameters, preview_url, match, groups }

The HTTP tool returns snake_case keys (preset_id, preview_url); the SDK normalizes them to the camelCase PoseResult shape above (both lanes go through the same normalization, so results are interchangeable).

Pricing

The default local lane is free: pure in-process compute, no payment, no key, no wallet, nothing to meter.

The hosted pose_model tool on POST /api/mcp-3d is priced per call over x402 for keyless callers, alongside the rest of the 3D Studio tools. The authoritative price is the one in the server's 402 payment challenge (surfaced by this SDK as PaymentRequiredError.accepts); read it at runtime rather than hardcoding. Calls authenticated with a three.ws OAuth apiKey run operator-funded instead of paying x402.

Errors & edge cases

poseSeed() rejects with a typed PoseError carrying a code:

code Meaning Recovery
invalid_prompt Prompt empty or over 500 chars (the tool requires 1-500). Trim to a short phrase.
network_error Hosted lane only: the endpoint was unreachable. Retry; honour the signal.
tool_error Hosted lane only: the MCP server returned a JSON-RPC error. Inspect error.data; retry.
payment_required Hosted lane only, keyless: the x402 challenge (a PaymentRequiredError). Pay via accepts, pass an apiKey, or use the free local lane.

Designed states, not crashes:

Examples

Build a pose picker from the live groups, then resolve a chosen preset:

import { poseSeed, listPresetGroups } from '@three-ws/pose';

for (const group of listPresetGroups()) renderGroupHeader(group);

button.onclick = async () => {
  const pose = await poseSeed(input.value || 'relaxed stand');
  applyToRig(pose.parameters);
  shareLink.href = pose.previewUrl;
};

Seed a freshly forged avatar, generate, rig, then drop into a starting pose:

import { forge, rig } from '@three-ws/forge';
import { poseSeed } from '@three-ws/pose';

const base = await forge('a cartoon astronaut, full body');
const rigged = await rig(base.glbUrl);          // animation-ready humanoid
const start = await poseSeed('superhero landing'); // initial pose
// load rigged.glbUrl, then apply start.parameters to its skeleton

Agent / MCP: the same capability is the pose_model tool on the hosted 3D Studio MCP server. An agent that already holds an MCP session can call it with { prompt: 'kneeling' } and receive the identical preset + seed this package computes locally.

Related


Built by three.ws · The only coin is $THREE

Security and contributing

See SECURITY.md, CONTRIBUTING.md and AGENTS.md (for AI agents). Part of three.ws.