Custom people
A person is an asset made of boxes (parts), just like furniture. Follow a few conventions and your own person walks like the official ones, sits on any furniture, and plays the work animations. People and furniture know nothing about each other; they fit together only through part names and the sitting template (SITTING).
Conventions
Standing pose
| Item | Value |
|---|---|
footprint | [0.5, 0.5] — pathfinding treats a person as this size |
anchor | 'floor' — stands on floor tiles, not on top of desks |
| Facing | Faces +y at 0°. Put face and chest parts on the larger-y side |
| Legs | leg-l and leg-r (+ shoe-l and shoe-r; shorts-l and shorts-r for shorts). They swing back and forth alternately when walking |
| Arms | arm-l and arm-r (+ forearm-* and hand-*). They swing opposite to the legs |
| Everything else | Any name. Bobs along with the whole body when walking |
-l and -r only tell the two sides apart (which one is actually the left does not matter).
Sitting pose (poses.sit)
Furniture only tells where the seat center, the front of the backrest and the seat height are (seats); the person draws its seated shape to fit this template.
y=0 0.1 (back) 0.46 (knee) 0.75
z │ │ │ │
1.36 ─ │ ┌─┴──┐ head │ │
│ │ │ │ │
0.62 ─ │ ├────┴───────── thigh ─┤ │
0.49 ─ │ under hips ────────────────┤ shin │
│ │ │ │
0.06 ─ │ └───┴── foot ──┤
0 ─ └───────────────────────────────────────────┘ (side view, facing +y)SITTING | Value | Meaning |
|---|---|---|
width×depth | 0.5 × 0.75 | Sitting footprint. poses.sit.footprint must be this |
back | 0.1 | y of the back of the body. The front of the furniture's backrest goes here |
knee | 0.46 | y of the knees. Leg parts in front of this hang down in front of the seat |
hip | 0.49 | z of the underside of the hips. The furniture's seat height goes here |
- On furniture whose seat is higher or lower than 0.49, the person is raised or lowered by the difference (never below the furniture's base).
- Put the feet at z 0. Put the hands a little above the desk top (0.75), around 0.8, so they do not pass through the top when seated at a desk.
Lying pose (poses.lie, optional)
To lie on a bed (seats[].pose: 'lie'), the person needs a lying pose. Without one, it simply does not use bed spots.
LYING | Value | Meaning |
|---|---|---|
width×length | 0.5 × 1.4 | Lying footprint. poses.lie.footprint must be this |
| Orientation | Lies flat on the back, facing up (+z). Top of the head at y = 0 (the head end), feet toward +y, back at z = 0 |
- The official people are made by tipping the standing pose straight back: standing part
(x, y, z)→ lying part(x, head margin + height − (z + h), y − back). They use the same ids, so parts swing back in an arc while lying down and getting up. - Leave out items carried in the hands, such as bags and cups, when lying down. In the
sleepstate, close the eyes (eye-l,eye-r) withopacity: 0.
Parts carried over when sitting
While sitting down and standing up, the engine moves standing parts to seated parts (Motion engine).
| Seated part | Comes from |
|---|---|
| Same id as in the standing pose | That part moves over (torso, head, arms, props) |
thigh-l · thigh-r | Upper half of leg-l · leg-r |
shin-l · shin-r | Lower half of leg-l · leg-r |
foot-l · foot-r | shoe-l · shoe-r |
| Anything else | Grows from a point where it is (forearms reaching forward) |
Parts that exist only in the standing pose (a bag in hand, a skirt hem) shrink away while sitting down. Do not reuse shoe-* for seated shoes; rename them to foot-* — with the same name, the shoes would slide separately and look detached from the legs.
States
| State | When | Usual effect |
|---|---|---|
idle | Standing, waiting | None (default look) |
walk | Walking | Required to count as a walking person (Studio behavior list, useObjectMotion). It does not have to change any parts |
work | Working, operating equipment | Hands type-a and type-b (alternate tapping) |
talk | Meetings, meals | Head part nod |
rest | Resting | Upper body breathe |
drink | Water, snacks | Right hand and cup sip (the cup is normally opacity: 0) |
sleep | Sleeping in bed | Upper body breathe, eyes opacity: 0 |
Missing states use the default look. The state for an activity is ACTIVITIES[activity].state.
Example
import { box, defineAsset, type Vec3 } from '@isora/core'
const skin = '#eec3a0'
const shirt = '#2f9e44'
const pants = '#343a40'
const shoes = '#1b1b1f'
const part = (id: string, at: Vec3, size: Vec3, material: string) => box({ id, at, size, material })
export const mechanic = defineAsset({
id: 'me/people/mechanic',
name: 'Mechanic',
names: { ko: '정비사' }, // optional translations; assetName(mechanic, 'ko') → '정비사'
footprint: [0.5, 0.5],
height: 1.2,
anchor: 'floor',
tags: ['person'],
parts: [
part('shoe-l', [0.1, 0.14, 0], [0.135, 0.24, 0.06], shoes),
part('shoe-r', [0.265, 0.14, 0], [0.135, 0.24, 0.06], shoes),
part('leg-l', [0.1, 0.16, 0.06], [0.135, 0.18, 0.38], pants),
part('leg-r', [0.265, 0.16, 0.06], [0.135, 0.18, 0.38], pants),
part('torso', [0.08, 0.14, 0.44], [0.34, 0.22, 0.46], shirt),
part('arm-l', [0, 0.17, 0.56], [0.08, 0.16, 0.32], shirt),
part('arm-r', [0.42, 0.17, 0.56], [0.08, 0.16, 0.32], shirt),
part('head', [0.11, 0.13, 0.9], [0.28, 0.24, 0.28], skin),
],
poses: {
sit: {
footprint: [0.5, 0.75],
height: 1.4,
parts: [
part('foot-l', [0.1, 0.48, 0], [0.135, 0.18, 0.06], shoes),
part('foot-r', [0.265, 0.48, 0], [0.135, 0.18, 0.06], shoes),
part('shin-l', [0.1, 0.46, 0.06], [0.135, 0.12, 0.43], pants),
part('shin-r', [0.265, 0.46, 0.06], [0.135, 0.12, 0.43], pants),
part('thigh-l', [0.1, 0.14, 0.49], [0.135, 0.44, 0.13], pants),
part('thigh-r', [0.265, 0.14, 0.49], [0.135, 0.44, 0.13], pants),
part('torso', [0.08, 0.1, 0.62], [0.34, 0.22, 0.46], shirt),
part('arm-l', [0, 0.13, 0.88], [0.08, 0.16, 0.18], shirt),
part('arm-r', [0.42, 0.13, 0.88], [0.08, 0.16, 0.18], shirt),
part('forearm-l', [0, 0.29, 0.8], [0.08, 0.23, 0.08], shirt),
part('forearm-r', [0.42, 0.29, 0.8], [0.08, 0.23, 0.08], shirt),
part('hand-l', [0, 0.52, 0.8], [0.08, 0.08, 0.08], skin),
part('hand-r', [0.42, 0.52, 0.8], [0.08, 0.08, 0.08], skin),
part('head', [0.11, 0.09, 1.08], [0.28, 0.24, 0.28], skin),
],
},
},
states: {
idle: { label: 'Standing', labels: { ko: '서 있기' } },
walk: { label: 'Walking', labels: { ko: '걷기' } },
work: { label: 'Working', labels: { ko: '일하기' }, parts: { 'hand-l': { animation: 'type-a' }, 'hand-r': { animation: 'type-b' } } },
},
defaultState: 'idle',
})A test (packages/assets/src/motion.test.ts) seats this example in desk chairs facing all four directions and checks that it never passes through furniture.
name and label are the default (English) text; translations go next to them in names and labels and are read with assetName(asset, locale) and stateLabel(asset, state, locale) (Languages).
Checking: describeRig
import { describeRig } from '@isora/core'
describeRig(mechanic)
// {
// legs: ['shoe-l', 'shoe-r', 'leg-l', 'leg-r'], arms: ['arm-l', 'arm-r'],
// walks: true, sits: true,
// sitting: {
// sources: { 'foot-l': 'shoe-l', 'shin-l': 'leg-l', 'thigh-l': 'leg-l', torso: 'torso', … },
// appearing: ['forearm-l', 'forearm-r', 'hand-l', 'hand-r'], // grow from a point
// vanishing: [], // disappear when seated
// },
// warnings: [],
// }What warnings reports: no leg parts or legs on one side only, no walk state, no sit pose, and sitting or lying footprints that differ from SITTING or LYING. Warnings are in English; describeRig(mechanic, { locale: 'ko' }) returns them in Korean. lies tells whether there is a lying pose (missing it is not a warning). Basic checks such as parts sticking out of the footprint, part sizes and materials are done by defineAsset.
More to check (what the official asset tests do):
- Parts do not overlap in volume — if they do, there is no axis to decide front and back, and the draw order flickers.
- Seated on official furniture (chairs, sofas, benches, wheelchairs) in four directions, the person does not overlap the furniture: seat with
seatedPatch(furniture, furnitureAsset, 0)and testobjectPartBoxespairs withboxesIntersect. - The sitting motion does not pass through furniture:
sitPathClear(seated, [x, y, z], [asset], seatObstacles(scene, registry)).
Publishing as a pack
import type { AssetPack } from '@isora/core'
export const myPeople: AssetPack = {
id: 'me/people', name: 'Our people', names: { ko: '우리 회사 사람' }, version: '1.0.0', license: 'CC0-1.0', assets: [mechanic],
}- Add it to a registry with
createRegistry([...officialPacks, myPeople])and use it with<IsoScene registry={registry}>. - Asset ids are
author/pack/name. To ship a data-only pack folder, usepnpm isora pack build(README). - "My assets" made in Studio's asset editor use the same format.