이 문서는 현재 apps/dawnlight 구현 기준으로, 플레이어 아바타의 GLB 로딩 방식과 감정/모션 처리 방식을 정리합니다.
- 모델 GLB:
https://static.dawn.lt/glb/leafy-explorer/leafy-explorer-v2.glb - 애니메이션 GLB:
https://static.dawn.lt/glb/leafy-explorer/leafy-explorer-anim.glb - 스마일 텍스처:
https://static.dawn.lt/glb/leafy-explorer/smile.webp
코드 위치:
apps/dawnlight/src/game/engine/player-avatar.tsapps/dawnlight/src/components/dawnlight-ui/avatar-preview.tsx
- 게임 엔진 초기화 시
PlayerAvatar인스턴스를 생성합니다. - 즉시
playerAvatar.load()를 비동기로 호출합니다.
코드 위치:
apps/dawnlight/src/game/engine/game-engine.ts
PlayerAvatar.load() 내부 동작:
GLTFLoader+MeshoptDecoder설정
EXT_meshopt_compression압축된 GLB를 읽기 위해loader.setMeshoptDecoder(MeshoptDecoder)를 사용합니다.
- 모델/애니메이션 병렬 로딩
Promise.all([modelGLB, animationGLB])로 동시에 다운로드합니다.- 애니메이션 클립은
animationGLB.animations가 우선이며, 비어 있으면 모델의 내장 애니메이션을 fallback으로 사용합니다.
- 모델 준비(
prepareAvatarModel)
- 모든 Mesh를 순회하며 머티리얼 정규화(
MeshBasicMaterial -> MeshStandardMaterial변환 포함). - 투명 + 텍스처 조합은
alphaTest기반으로 정리. - emissive를 제어 가능한 상태로 초기화.
- 바운딩 박스 기준으로 아바타 높이를
PLAYER_HEIGHT * 1.02에 맞춰 스케일. - 바닥 기준으로 Y 오프셋 보정.
- Hip/Pelvis 본 탐색(루트 모션 보정용).
- 애니메이션 액션 구성(
setupAvatarAnimations)
AnimationMixer생성.- 클립별
AnimationAction등록(LoopRepeat, Infinity). - idle/crouch/sneak/walk/run/jump/swimForward/swimIdle 액션을 alias/패턴으로 탐색해 바인딩.
- 씬에 부착
- 로딩/설정 완료된
modelRoot를 씬에 추가.
프레임 업데이트는 AnimationLoop에서 updateLoopPlayerAvatar(...)를 통해 playerAvatar.update(...)를 매 틱 호출하는 구조입니다.
코드 위치:
apps/dawnlight/src/game/engine/animation-loop.tsapps/dawnlight/src/game/engine/animation-loop-camera.ts
update(...)의 상태 결정 우선순위:
- 강제 디버그/감정 액션이 있으면 해당 액션 고정 재생
- 수영 상태면
swimForward/swimIdle - 앉기(crouch) 상태면 crouch 액션
- 점프 조건 충족 시 jump 액션
- 이동 중이면 sneak/run/walk 중 하나
- 이동이 없으면 idle 사이클(여러 idle 액션 랜덤 순환)
추가 동작:
- 이동 방향(
intendedMoveYaw)에 따라 3인칭 아바타 Yaw를 보간 회전. - 클립 루트 모션이 캐릭터 위치를 밀어내지 않도록 수평 보정(
hipBone기준) 적용. - 야간 밝기 대응으로 emissive intensity를 태양 강도에 따라 보정.
현재는 클립 이름과 실제 동작이 1:1이 아닌 리타게팅/오염 데이터가 있어, alias 매핑 테이블(CLIP_ACTUAL_MOTION_BY_LABEL)을 통해 실제 의미를 재해석합니다.
핵심 함수:
findClipByAlias(...)findClip(...)findClipNameByAlias(...)findClipNameByPattern(...)
또한 crouch에 대해서는 createHorizontalRootLockedClip(...)으로 X/Z 이동 트랙을 고정한 클립을 만들어 사용합니다.
코드 위치:
apps/dawnlight/src/game/engine/player-avatar-setup.ts
- 모바일 UI 감정 버튼
EmotionButton->onTriggerEmotion->handleMobileEmotion- 동작:
playGreetingForDuration(2000)+playSmileExpressionForDuration(2000)
코드 위치:
apps/dawnlight/src/components/virtual-joystick/emotion-button.tsxapps/dawnlight/src/components/virtual-joystick/virtual-joystick.tsxapps/dawnlight/src/components/dawnlight.tsx
- 채팅 명령
/emotion smile: 스마일 표정만 2초 요청/hello: 웨이브 + 스마일 2초
코드 위치:
apps/dawnlight/src/components/dawnlight-chat.tsapps/dawnlight/src/hooks/use-chat-commands.ts
- Greeting(웨이브)
playGreetingForDuration이wave_for_help_1alias 또는(wave|greet|hello|hand)패턴 클립을 찾아 강제 재생합니다.- 강제 상태는 일정 시간 후 자동 해제되며, 이동/점프/특정 상태 변화 시 중간 해제될 수 있습니다.
- Smile(표정)
playSmileExpressionForDuration은 smile 텍스처 프리셋을 적용해 얼굴 텍스처를 교체합니다.- 최초 호출 시 비동기 프리로드 후 적용, 이후는 캐시된 프리셋 재사용.
- 만료 시간이 지나면 기본 텍스처 프리셋으로 자동 복원.
- 현재 구현상
playSmileExpressionForDuration(...)는 preload 성공/실패와 무관하게 즉시true를 반환합니다. - 따라서 채팅의 실패 문구 분기(
/emotion smile실패)는 사실상 잘 발생하지 않고, 실제 preload 실패는 콘솔 에러 로그로 확인하는 구조입니다.
- 랜덤 스마일
- 별도 트리거 없이도 5~15초 간격으로 1초짜리 랜덤 스마일이 자동 발생합니다.
코드 위치:
apps/dawnlight/src/game/engine/player-avatar.tsapps/dawnlight/src/game/engine/player-avatar-behavior.ts
UI 좌상단 아바타 프리뷰도 동일 GLB/애니메이션 GLB를 별도 로딩합니다.
- 런타임 아바타의 현재 클립 이름(
getCurrentClipName)을 읽어 동일 클립으로 동기화 재생. - 프리뷰에서도 hip 기반 수평 루트 모션 보정을 적용해 화면 내 위치 드리프트를 줄입니다.
코드 위치:
apps/dawnlight/src/components/dawnlight-ui/avatar-preview.tsx
개발 스크립트 scripts/optimize-gltf-assets.mjs는 asset-origin의 GLB를 asset-optimized로 변환합니다.
- glTF Transform CLI(
@gltf-transform/cli)로 meshopt 압축 적용 - 텍스처 포맷/크기 조정
- 파일명에
animation문자열이 포함된 경우에만 animation pack으로 분류하여 더 공격적인 텍스처 축소 정책 사용
코드 위치:
apps/dawnlight/scripts/optimize-gltf-assets.mjs