Skip to content

Figures: humans, bipeds and quadrupeds ​

@bendyline/molen-figures renders stylized people and animals and moves them: a kernel half (/kernel: descriptors and presets, canonical rigs and sockets, deterministic gait and pose evaluation, locomotion state, attachments) and a client half (/client: the figure renderable kind that generates a skinned body from the descriptor and poses it every frame). The look is deliberately left of the uncanny valley: readable silhouettes, color blocks for clothing, disc eyes, no mouths or fingers, gently faceted surfaces (see the figure rules in 3D art guidelines).

Naming note: this is not @bendyline/molen-kernel/kinematics (the 2.5D collision layer) and not /character (the gravity/jump mover). Figures sit on top of whatever moves an entity.

Quick start ​

A human with a hat, walking under the character controller:

json
{
  "format": "molen/scene@3",
  "name": "hello-figures",
  "tickRate": 60,
  "physics": { "engine": "kinematics", "character": true },
  "entities": [
    {
      "id": "npc-1",
      "components": {
        "transform": { "pos": [0, 0, 0], "rot": [0, 0, 0, 1] },
        "renderable": { "kind": "figure", "ref": "procedural" },
        "figure": { "preset": "human.adult", "height": 1.7, "palette": { "top": "#3b6ea5" } },
        "character": { "speed": 1.4, "jumpSpeed": 6, "gravity": 20, "vy": 0, "grounded": true },
        "moveIntent": { "dir": [1, 0], "jump": false },
        "collider": { "shape": "circle", "radius": 0.35, "layer": 1, "mask": 1 },
        "kinematicBody": { "vel": [0, 0, 0], "slide": true }
      }
    },
    {
      "id": "npc-1-hat",
      "components": {
        "transform": { "pos": [0, 0, 0], "rot": [0, 0, 0, 1] },
        "renderable": { "kind": "primitive", "ref": "cylinder", "materialRef": "palette:#39747d", "primitive": { "size": [0.3, 0.12, 0.3] } },
        "parent": { "id": "npc-1" },
        "figureAttachment": { "socket": "head.top", "offset": { "pos": [0, 0.06, 0] } }
      }
    }
  ]
}

molen validate, molen sim run --ticks 120 --hash, and molen shot work unchanged: tooling installs the figures systems on every world, and the capture page renders the figure kind. In a browser host, pass the kind to the client and the systems to the world:

ts
import { mountExperience } from '@bendyline/molen-client';
import { figureKind } from '@bendyline/molen-figures/client';
await mountExperience({ link: worker, scene, canvas, kinds: [figureKind()] });

// worker.ts
import { buildWorld } from '@bendyline/molen-kernel';
import { figuresScriptApi, installFigures } from '@bendyline/molen-figures/kernel';
const world = buildWorld(scene, setup, {
  capabilities: [(w) => ({ figures: figuresScriptApi(installFigures(w)) })],
});

Components ​

ComponentOwnerPurpose
figureyouWhat the figure is: preset plus descriptor overrides, and how it faces (facing, turnRate, speedSource, blendTicks).
figureIntentyou (optional)Control: a forced mode, a lookAt target, joint overrides, ik goals.
figureStatekernelLocomotion state: mode, quantized speed, strideRate, tick-anchored phaseAt/phaseAtTick, modeAtTick, prevMode. Read it; never write it.
figureAttachmentyouOn a child entity with parent: which socket to follow, an offset, and which anchor of the child sits on the socket.

The renderable is { "kind": "figure", "ref": "procedural", "lod"?: "auto" | 0 | 1 | 2 }. materialRef is ignored for figures; colors come from the descriptor palette.

Descriptor and presets ​

A descriptor is a preset plus overrides. molen figure presets lists them; molen schema get figure prints the full shape (the molen/figure@1 file format wraps the same fields with a format envelope for molen validate and molen figure preview).

FieldMeaning
presethuman.adult, human.child, human.elder, dog, cat, horse, deer, cow, sheep
heightmeters; bipeds to the head top, quadrupeds to the withers
build-1 (slight) to 1 (heavy): limb and torso radii, shoulder width
agechild, adult, elder (elder adds a stoop)
proportionsmultipliers: legRatio, armRatio, torsoRatio, headScale, neckLength, neckPitch, shoulderWidth, hipWidth, bodyLength, tailLength, earSize, snoutLength
featureshair (none/cap/bob/long), hands, sleeves, legs, tail, ears, horns, feet, mane
palette#rrggbb: skin (fur/hide for animals), hair, eyes, top, bottom, shoes, accent, markings
seeddetail jitter only (symmetry-safe)

Every rig stands with its feet at y = 0 and its reference point exactly at height, whatever the overrides. Bodies come in three detail tiers (near about 1,000 to 1,500 triangles, medium, and a rigid distant tier); the client picks a tier by camera distance unless lod pins one.

Locomotion ​

The figures-locomotion system (late phase, before the hierarchy) reads whatever moves the entity: kinematicBody.vel, then character + moveIntent, platformBody, a rapier velocity, else transform deltas (speedSource: 'transform' forces the last; 'none' freezes it). It picks a mode (idle, walk, run, jump, fall; sit when mounted), a quadruped gait (walk, trot, gallop by Froude number), a stride rate from the leg length, and turns the figure toward its velocity (facing: 'velocity', the default) at turnRate rad/s. facing: 'manual' leaves transform.rot to you (rapier-driven bodies, strafing).

State is written only when something changes: speed is quantized to 0.05 m/s, the gait phase is anchored to a tick, so a steadily walking figure sends no deltas. figureIntent.mode forces a mode; lookAt aims the head at a point or an entity.

Pedestrians in ambient life are figures moved this way: the ambient kernel writes their transform along footways and speedSource: 'transform' animates the gait.

Sockets and attachments ​

Sockets are named points on the rig, evaluated under the current pose. An entity with parent: { id: <figure> } and figureAttachment: { socket } gets its localTransform from the socket each tick (the kernel hierarchy composes the world transform, and destroying the figure cascades). Anything renders there: a glTF hat, a primitive, another figure.

Biped socketsQuadruped sockets
head.top, face, eyes, eye, ear.l, ear.r, neck, chest, back, hip.l, hip.r, hand.l, hand.r, foot.l, foot.r, pelvishead.top, face, eyes, eye, mouth, neck, withers, saddle, back, tail.tip, foot.fl, foot.fr, foot.hl, foot.hr, pelvis

anchor chooses which point of the attached entity sits on the socket: origin (default), pelvis, or eye (for figures seated on saddles and vehicle seats). Riders use the vehicle mount contract: molen.figures.mount(rider, horse) adds a mountable with a saddle seat and seats the rider; the figures-riders system keeps the rider on the animated saddle.

Scripting: molen.figures.* ​

setMode(id, mode | null), lookAt(id, { pos } | { entity } | null), pose(id, overrides | null), ik(id, goals | null), attach(itemId, figureId, socket, offset?, anchor?), detach(itemId), mount(riderId, figureId, seat?), dismount(riderId), socket(figureId, name) (world pose), setDescriptor(id, patch), joints(id), sockets(id), presets().

Previews ​

sh
molen figure presets
molen figure preview --lineup all --angles review --mode walk --out review.png
molen figure preview my.figure.json --angles 4 --out turntable.png

The MCP tools figure_preview (returns images) and list_figure_presets mirror these. The review view set is the art-guideline review: front, three-quarter, rear, and a gameplay-distance view.

Determinism ​

The pose is a pure function evaluatePose(rig, figureState, tick) shared by both halves: the kernel evaluates it only where it needs a socket (attachments, riders, molen.figures.socket), the client at every frame. figureState and the attachment localTransforms are ordinary hashed components; rigs, meshes and poses are derived. The client keeps the previous state until the interpolated render tick reaches the tick a state changed on, so poses and transforms agree.

Limits ​

  • Hands are mittens (a thumb on the near tier with hands: 'fingers'); no faces beyond eyes and brows.
  • Foot planting on uneven ground and quadruped IK are not evaluated yet.
  • Imported skinned glTF figures and molen figure bake (skinned GLB export) are later phases.
  • Accessories follow kernel-evaluated sockets at tick rate; fast hand swings can lag a few centimeters between ticks.

37 guides · 36 schema formats · 41 API entry points · 10 samples. API, CLI and schema pages are generated from the shipped build.