본문으로 건너뛰기

사람 에셋 만들기 ​

사람도 가구처럼 상자(파츠)로 만든 에셋이다. 몇 가지 약속만 지키면 직접 만든 사람이 공식 사람처럼 걷고, 아무 가구에나 앉고, 일하는 동작을 한다. 사람과 가구는 서로를 모르고 파츠 이름과 앉은 자세 틀(SITTING) 로만 맞물린다.

약속 ​

선 모습 ​

항목값
footprint[0.5, 0.5] — 길 찾기가 사람을 이 크기로 본다
anchor'floor' — 책상 위가 아니라 바닥재 위에 선다
방향0° 에서 +y 를 본다. 얼굴·앞가슴 파츠를 y 가 큰 쪽에 둔다
다리leg-l·leg-r(+ shoe-l·shoe-r, 반바지면 shorts-l·shorts-r). 걸을 때 앞뒤로 번갈아 흔든다
팔arm-l·arm-r(+ forearm-*·hand-*). 다리와 반대로 흔든다
나머지이름 자유. 걸을 때 몸 전체와 함께 들썩인다

-l·-r 은 좌우를 가르는 데만 쓴다 (어느 쪽이 실제 왼쪽인지는 상관없다).

앉은 자세 (poses.sit) ​

가구는 자리 가운데·등받이 앞면·앉는 면 높이만 알려 주고(seats), 사람은 이 틀에 맞춰 앉은 모습을 그린다.

          y=0     0.1(등)            0.46(무릎)      0.75
   z      │       │                    │              │
  1.36 ─  │     ┌─┴──┐ 머리            │              │
          │     │    │                 │              │
  0.62 ─  │     ├────┴────────── 허벅지 ┤              │
  0.49 ─  │ 엉덩이 아래 ────────────────┤ 정강이        │
          │                            │   │          │
  0.06 ─  │                            └───┴── 발 ─────┤
     0 ─  └───────────────────────────────────────────┘   (옆에서 본 모습, +y 를 본다)
SITTING값뜻
width×depth0.5 × 0.75앉은 자세 발자국. poses.sit.footprint 가 이것이어야 한다
back0.1등 뒷면 y. 가구 등받이 앞면이 여기 온다
knee0.46무릎 y. 이보다 앞으로 나온 다리는 앉는 면 앞으로 내려간다
hip0.49엉덩이 아래 z. 가구 앉는 면 높이가 여기 온다
  • 앉는 면이 0.49 보다 높거나 낮은 가구는 사람을 그만큼 올리고 내린다 (가구 바닥보다 내려가지는 않는다).
  • 발은 z 0 에 둔다. 손은 책상 윗판(0.75)보다 조금 위(0.8 안팎)에 두어야 책상 앞에 앉았을 때 윗판을 뚫지 않는다.

누운 자세 (poses.lie, 선택) ​

침대(seats[].pose: 'lie')에 누우려면 누운 자세가 있어야 한다. 없으면 침대 자리를 쓰지 않을 뿐이다.

LYING값뜻
width×length0.5 × 1.4누운 자세 발자국. poses.lie.footprint 가 이것이어야 한다
방향반듯이 누워 위(+z)를 본다. 머리 끝이 y = 0 쪽(머리맡), 발이 +y 쪽, 등이 z = 0
  • 공식 사람은 선 모습을 그대로 뒤로 눕혀 만든다: 선 파츠 (x, y, z) → 누운 파츠 (x, 머리맡 여유 + 키 − (z + h), y − 등). 같은 id 를 쓰므로 눕고 일어서는 동안 파츠가 호를 그리며 뒤로 넘어간다.
  • 누울 때 손에 든 짐·컵은 뺀다. sleep 상태에서 눈(eye-l·eye-r)을 opacity: 0 으로 감긴다.

앉을 때 이어지는 파츠 ​

앉기·일어서기 동안 엔진은 선 파츠를 앉은 파츠로 옮겨 간다 (움직임 엔진).

앉은 파츠어디서 나오나
선 모습과 같은 id그 파츠가 옮겨 간다 (몸통·머리·팔·소품)
thigh-l·thigh-rleg-l·leg-r 의 위 절반
shin-l·shin-rleg-l·leg-r 의 아래 절반
foot-l·foot-rshoe-l·shoe-r
그 밖제자리에서 점으로부터 자라난다 (앞으로 내민 아래팔)

선 모습에만 있는 파츠(손에 든 짐, 치마 자락)는 앉는 동안 줄어들어 사라진다. 앉은 신발에 shoe-* 를 그대로 쓰지 말고 foot-* 로 이름을 바꾼다 — 같은 이름이면 신발만 따로 미끄러져 다리에서 떨어져 보인다.

상태 ​

상태언제보통 하는 일
idle서 있을 때·기다릴 때없음 (기본 모습)
walk걸을 때있어야 걷는 사람으로 친다 (Studio 행동 목록, useObjectMotion). 파츠를 안 바꿔도 된다
work일하기·설비 조작손 type-a·type-b (번갈아 두드리기)
talk회의·식사머리 파츠 nod
rest쉬기윗몸 breathe
drink물·간식오른손·컵 sip (컵은 평소 opacity: 0)
sleep침대에서 자기윗몸 breathe, 눈 opacity: 0

없는 상태는 기본 모습으로 한다. 활동 → 상태는 ACTIVITIES[활동].state 다.

예제 ​

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: '정비사',
  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: '서 있기' },
    walk: { label: '걷기' },
    work: { label: '일하기', parts: { 'hand-l': { animation: 'type-a' }, 'hand-r': { animation: 'type-b' } } },
  },
  defaultState: 'idle',
})

이 예제는 테스트(packages/assets/src/motion.test.ts)에서 네 방향 책상 의자에 앉혀 보고 가구를 뚫지 않는지 확인한다.

name·label 은 기본 글이다. 이 예제처럼 한국어를 바로 써도 되지만, 공식 에셋은 name 에 영어를 두고 한국어는 옆의 names: { ko: '정비사' }·labels: { ko: '서 있기' } 에 둔다. assetName(asset, 'ko')·stateLabel(asset, state, 'ko') 로 읽는다 (언어).

점검: 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'],   // 점에서 자라난다
//     vanishing: [],                                              // 앉으면 사라진다
//   },
//   warnings: [],
// }

warnings 가 알려 주는 것: 다리 파츠가 없음·한쪽만 있음, walk 상태 없음, sit 자세 없음, 앉은·누운 발자국이 SITTING·LYING 과 다름. 경고 글은 기본 영어이고, describeRig(mechanic, { locale: 'ko' }) 면 한국어다. lies 는 누운 자세가 있는지다 (없어도 경고는 아니다). 발자국 밖으로 나간 파츠, 파츠 크기·재질 같은 기본 검사는 defineAsset 이 한다.

더 확인할 것 (공식 에셋 테스트가 하는 일):

  • 파츠끼리 부피가 겹치지 않는다 — 겹치면 앞뒤를 정할 축이 없어 그리는 순서가 흔들린다.
  • 공식 가구(의자·소파·벤치·휠체어)에 네 방향으로 앉혀 가구와 겹치지 않는다: seatedPatch(furniture, furnitureAsset, 0) 로 앉히고 objectPartBoxes 끼리 boxesIntersect.
  • 앉는 동작이 가구를 뚫지 않는다: sitPathClear(seated, [x, y, z], [asset], seatObstacles(scene, registry)).

팩으로 내기 ​

ts
import type { AssetPack } from '@isora/core'

export const myPeople: AssetPack = { id: 'me/people', name: '우리 회사 사람', version: '1.0.0', license: 'CC0-1.0', assets: [mechanic] }
  • createRegistry([...officialPacks, myPeople]) 로 레지스트리에 넣고 <IsoScene registry={registry}> 로 쓴다.
  • 에셋 id 는 제작자/팩/이름 이다. 데이터만 담은 팩 폴더로 배포하려면 pnpm isora pack build (README).
  • Studio 의 에셋 편집기에서 만든 '내 에셋'도 같은 형식이다.

MIT 라이선스로 배포합니다.