Skip to content

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 ​

ItemValue
footprint[0.5, 0.5] — pathfinding treats a person as this size
anchor'floor' — stands on floor tiles, not on top of desks
FacingFaces +y at 0°. Put face and chest parts on the larger-y side
Legsleg-l and leg-r (+ shoe-l and shoe-r; shorts-l and shorts-r for shorts). They swing back and forth alternately when walking
Armsarm-l and arm-r (+ forearm-* and hand-*). They swing opposite to the legs
Everything elseAny 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)
SITTINGValueMeaning
width×depth0.5 × 0.75Sitting footprint. poses.sit.footprint must be this
back0.1y of the back of the body. The front of the furniture's backrest goes here
knee0.46y of the knees. Leg parts in front of this hang down in front of the seat
hip0.49z 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.

LYINGValueMeaning
width×length0.5 × 1.4Lying footprint. poses.lie.footprint must be this
OrientationLies 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 sleep state, close the eyes (eye-l, eye-r) with opacity: 0.

Parts carried over when sitting ​

While sitting down and standing up, the engine moves standing parts to seated parts (Motion engine).

Seated partComes from
Same id as in the standing poseThat part moves over (torso, head, arms, props)
thigh-l · thigh-rUpper half of leg-l · leg-r
shin-l · shin-rLower half of leg-l · leg-r
foot-l · foot-rshoe-l · shoe-r
Anything elseGrows 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 ​

StateWhenUsual effect
idleStanding, waitingNone (default look)
walkWalkingRequired to count as a walking person (Studio behavior list, useObjectMotion). It does not have to change any parts
workWorking, operating equipmentHands type-a and type-b (alternate tapping)
talkMeetings, mealsHead part nod
restRestingUpper body breathe
drinkWater, snacksRight hand and cup sip (the cup is normally opacity: 0)
sleepSleeping in bedUpper body breathe, eyes opacity: 0

Missing states use the default look. The state for an activity is ACTIVITIES[activity].state.

Example ​

ts
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 ​

ts
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 test objectPartBoxes pairs with boxesIntersect.
  • The sitting motion does not pass through furniture: sitPathClear(seated, [x, y, z], [asset], seatObstacles(scene, registry)).

Publishing as a pack ​

ts
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, use pnpm isora pack build (README).
  • "My assets" made in Studio's asset editor use the same format.

Released under the MIT License.