사람 에셋 만들기
사람도 가구처럼 상자(파츠)로 만든 에셋이다. 몇 가지 약속만 지키면 직접 만든 사람이 공식 사람처럼 걷고, 아무 가구에나 앉고, 일하는 동작을 한다. 사람과 가구는 서로를 모르고 파츠 이름과 앉은 자세 틀(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×depth | 0.5 × 0.75 | 앉은 자세 발자국. poses.sit.footprint 가 이것이어야 한다 |
back | 0.1 | 등 뒷면 y. 가구 등받이 앞면이 여기 온다 |
knee | 0.46 | 무릎 y. 이보다 앞으로 나온 다리는 앉는 면 앞으로 내려간다 |
hip | 0.49 | 엉덩이 아래 z. 가구 앉는 면 높이가 여기 온다 |
- 앉는 면이 0.49 보다 높거나 낮은 가구는 사람을 그만큼 올리고 내린다 (가구 바닥보다 내려가지는 않는다).
- 발은 z 0 에 둔다. 손은 책상 윗판(0.75)보다 조금 위(0.8 안팎)에 두어야 책상 앞에 앉았을 때 윗판을 뚫지 않는다.
누운 자세 (poses.lie, 선택)
침대(seats[].pose: 'lie')에 누우려면 누운 자세가 있어야 한다. 없으면 침대 자리를 쓰지 않을 뿐이다.
LYING | 값 | 뜻 |
|---|---|---|
width×length | 0.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-r | leg-l·leg-r 의 위 절반 |
shin-l·shin-r | leg-l·leg-r 의 아래 절반 |
foot-l·foot-r | shoe-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 다.
예제
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
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)).
팩으로 내기
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 의 에셋 편집기에서 만든 '내 에셋'도 같은 형식이다.