브라우저에서 오픈 월드 만들기, 17부: 리타기팅이 필요 없던 애니메이션, 그리고 실시간 에셋 검색
글쓴이 Oleg Sidorkin, Cinevva CTO 겸 공동 창업자
처음 오셨나요? 시리즈 가이드를 보세요. spike가 무엇인지 설명하고 모든 부분을 링크해 둡니다.
16부에서 우리는 물체를 배치할 수 있는 세계를 얻었습니다. 이번 부분은 그 안에서 플레이어가 할 수 있는 더 나은 것을 줍니다. 전투급 애니메이션 세트, 그리고 검색을 입력해서 수천 개의 CC0 모델 중 무엇이든 세계로 끌어오는 방법입니다.
262개 클립, 하나의 스켈레톤, 세 개의 리타기팅 수정
플레이어 릭은 CC4 스타일의 3MIKE 스켈레톤으로 213개의 본을 가지고 있고, 80개가 넘는 본의 얼굴 릭과 완전한 손가락 관절을 포함합니다. 애니메이션 소스는 여기에 맞지 않습니다. 첫 번째 버전은 손으로 고른 Mixamo 전투 클립을 리타기팅했지만, 소스 풀이 얇았고 이동 애니메이션은 Mixamo와 몇 개의 Kimodo BVH 대기 동작 사이에 어색하게 쪼개져 있었습니다. 진짜 성과는 소스 라이브러리를 Quaternius의 Universal Animation Library 두 팩으로 바꿨을 때 나왔습니다. 하나의 일관된 UE5 스타일 65본 마네킹 위에 총 262개의 클립입니다. 검 콤보, 활쏘기, 등반, 벽 달리기, 회피, 피격 반응, 이모트, 그리고 훨씬 깔끔한 이동 애니메이션 세트가 있습니다.
이건 65본 소스에서 213본 타깃으로 가는 것이고, 공유된 본 이름도, T-포즈 방향도, 팔다리 비율도 없습니다. 모든 클립은 세 단계로 리매핑됩니다. 본 이름 맵, 두 릭이 기준 방향을 공유하도록 하는 바인드 포즈 정렬, 그리고 더 짧은 소스 릭이 캐릭터를 바닥에 무릎까지 박아버리지 않도록 하는 위치 트랙 스케일링입니다. 이걸 제대로 맞추는 일은 각각 구체적인 무언가를 가르쳐 준 일련의 버그를 쫓는 것을 뜻했습니다.
캐릭터가 너무 비틀린 채로 나왔고, 모든 어깨와 팔꿈치가 약 30° 정도 과하게 회전했습니다. 원인은 포즈 불일치였습니다. UAL 소스는 진짜 T-포즈로 배포되고, CC4의 바인드는 A-포즈인데, 리타기터는 두 릭이 일치하는 기준 포즈에 있다고 가정했습니다. 그래서 A에서 T로의 차이가 매 프레임 델타마다 더해졌습니다. 수정은 바인드를 잡기 위해 CC4의 팔 체인을 진짜 T-포즈로 강제한 다음 리타기팅하는 것입니다. 그러면 델타가 작게 유지됩니다.
그다음에는 몸이 이동을 멈췄습니다. 넉백이 상체를 뒤로 젖혔지만 발은 바닥에 박힌 채로 남았습니다. UAL은 변위를 골반이 아니라 root 본에 둡니다. 그래서 골반 위치를 읽으면 거의 0에 가까운 움직임이 나왔습니다. 수정은 root.position과 pelvis.position을 더하고, 수평 부분을 스케일링한 다음, 하나의 엉덩이 위치 트랙으로 쓰는 것입니다. 이걸 하면서 우리는 수직 오프셋이 약 12% 어긋나 있는 것을 발견했습니다. 골반 높이는 엉덩이-바닥 비율이 필요한데 Y축에 전역 팔다리 비율을 썼기 때문이었습니다. 두 개의 비율, 두 개의 축입니다. Y에는 RL_BoneRoot.position NaN 경고가 줄줄이 나왔는데, UAL의 root(
가장 작은 수정이 보기에는 가장 만족스러웠습니다. 캐릭터가 축 늘어진 바인드 포즈 손가락으로 검을 쥐고 있었는데, 본 맵에 손가락 항목이 없었고 리타기터는 매핑된 본에 대해서만 트랙을 쓰기 때문이었습니다. 손가락 본 30개(다섯 손가락, 세 마디, 두 손, 변형되지 않는 UAL의 네 번째 손끝 헬퍼는 제외)를 추가하니 손이 손잡이를 잡을 때 오므라지고 놓을 때 펴졌습니다.
262개 클립을 보지 않고 262개 클립을 증명하기
262개 클립 라이브러리를 눈으로 검증할 수는 없습니다. 그래서 우리는 오프라인 궤적 일치 테스트를 만들었습니다. 각 팩을 로드하고, 소스 스켈레톤을 60Hz로 샘플링하고, 리타기팅 파이프라인을 돌린 다음, 스케일링 후의 본별 월드 위치와 회전을 소스와 비교하는 헤드리스 Node 스크립트입니다. 골반 Y 드리프트는 최대 0.003 m로 나왔습니다. 손은 일정한 2.5° 오프셋을 보였는데, 처음에는 "손가락이 추적되지 않는다"로 읽혔습니다. 하지만 일정한 오프셋은 평평한 UAL 손과 약간 오므라진 CC4 바인드 사이의 바인드 델타이고, 클립 전체에서 변하지 않습니다. 진짜 애니메이션 오류는 프레임마다 달라지는 드리프트로 나타납니다. 이게 분명해지자 테스트는 한 방 회귀 검사가 되었습니다. 어떤 클립의 드리프트가 그 일정한 기준선을 유지하지 못하면, 최근의 변경이 리타기팅을 망가뜨린 것입니다.
나란히 놓인 참조 마네킹은 디버깅의 시각적 절반을 결정적으로 만들었습니다. 백슬래시를 누르면 현재 클립을 소유한 소스 릭이 플레이어 옆에 나타납니다. 그래서 "어깨가 비틀렸다"라는 질문이 "비틀림이 소스에 있나, 아니면 리타기팅이 더한 건가?"로 바뀝니다. 수치 테스트는 회귀를 잡고, 시각 참조는 숫자로 드러나지 않는 바인드 포즈 실수를 잡습니다. 둘이 함께 추측 게임을 은퇴시켰습니다.
파이프라인이 단단해지자, WASD/점프/수영 기준선을 Mixamo에서 UAL로 바꾸는 것은 얇은 별칭 맵에 불과했습니다. FSM은 여전히 idle이나 walk 같은 일반 상태 이름을 말하고, 재생 시점에 UAL 클립 이름으로 해석됩니다. 수영은 클립별 릭 오프셋이 필요했습니다. 자유형과 선헤엄 포즈가 골반을 서로 다른 해부학적 높이에 고정하기 때문입니다. 그래서 능동적으로 수영할 때는 릭을 반 미터 가라앉히고 선헤엄칠 때는 더 깊게 가라앉히면서, 부드러운 5Hz로 둘 사이를 블렌딩합니다.
단어를 입력하면 모델이 나온다
Spike 34는 CC0 팩을 손으로 큐레이션하느라 하루를 잡아먹었습니다. 장기적인 답은 검색 상자입니다. Polyhaven은 너그러운 JSON API와 결정론적 CDN 뒤에 약 1,100개의 CC0 모델을 게시합니다. 그리고 이 spike는 빌드 단계 없이 정적 페이지에서 전체 경로(쿼리, 썸네일, 로드, 렌더)를 약 300줄의 바닐라 JS에 three.js를 더해 연결합니다.
부팅할 때 전체 카탈로그를 한 번 가져옵니다. 약 600 KB입니다. 검색은 순수 클라이언트 측 점수 매기기(이름이 id를 이기고, id가 카테고리를 이기고, 카테고리가 태그를 이김)에 120 ms 디바운스를 걸고 상위 60개 카드를 렌더링합니다. 썸네일은 IntersectionObserver를 통해 지연 로드되므로 타이핑이 한 번에 60개 요청을 쏘지 않습니다. 흥미로운 부분은 로딩입니다. Polyhaven의 파일 엔드포인트는 GLB가 아니라 멀티파일 glTF를 노출하고, 텍스처는 해상도 간에 공유되며 별도 파일로 쪼개져 있고, 상대 경로에서 절대 CDN URL로 가는 include 맵을 돌려줍니다. JSON을 우리가 직접 내려받아 패치하는 대신, 그 맵을 LoadingManager.setURLModifier에 먹입니다. 이건 로더가 필요로 하는 모든 의존성(.bin, 각 텍스처)마다 발동되어 CDN을 통해 해석합니다. 한 번의 클릭, 겉보기엔 하나의 파일입니다. API와 CDN 둘 다 너그러운 CORS를 설정했고, 어떤 클라이언트 코드보다 먼저 curl로 검증했습니다. 그래서 프록시가 필요 없습니다. PBR 머티리얼은 RoomEnvironment와 ACES 톤 매핑 기본값으로 에셋별 보정 없이 올바르게 렌더링되고, 1k 텍스처는 일반적인 모델을 4k의 20에서 40 MB 대신 2에서 5 MB로 유지합니다.
meshoptimizer WASM 패스가 비파괴적 리듀서로 마무리합니다. 각 메시는 원본 지오메트리의 복제본을 보관하고, 비율을 바꾸면 누적적으로 단순화하는 대신 그 복제본에서 인덱스 버퍼를 다시 빌드합니다. 멀티 머티리얼 지오메트리는 그룹별로 단순화하고 geometry.groups를 다시 빌드해서 머티리얼 슬롯이 무너지지 않습니다. 안락의자 하나가 전체 정밀도 5,626 삼각형에서 절반 정밀도 2,812로 갑니다.
이 장에서 다룬 기술
바인드 포즈 정렬을 동반한 스켈레톤 리타기팅. 본 이름, 비율, 기준 포즈가 다른 한 스켈레톤에서 다른 스켈레톤으로 애니메이션을 매핑하려면 세 가지 보정이 필요합니다. 본 이름 맵, 두 릭이 기준 방향을 공유하도록 하는 바인드 포즈 정렬(타깃의 A-포즈 팔 체인을 소스의 T-포즈로 강제), 그리고 위치 트랙 스케일링입니다. 포즈 불일치는 A에서 T로의 회전을 매 프레임 델타에 더해 관절 회전을 두 배로 만듭니다. 매핑되지 않은 헬퍼 본은 반드시 버려야 합니다.
한 릭을 위한 두 개의 스케일 비율. 수평 변위는 전역 팔다리 비율(전체 스켈레톤 크기)을 쓰지만, 수직 골반 오프셋은 엉덩이-바닥 비율 root 본에 살기 때문에, 넉백, 등반, 이동 클립 전반에서 움직임을 보존하려면 둘 다 하나의 엉덩이 위치 트랙으로 더해야 합니다.
오프라인 궤적 일치 테스트. 헤드리스 스크립트가 소스 스켈레톤을 60Hz로 샘플링하고, 리타기팅 파이프라인을 돌리고, 본별 월드 변환을 소스와 비교합니다. 일정한 프레임별 오프셋은 무해한 바인드 델타이고, 프레임마다 달라지는 드리프트는 진짜 오류입니다. 그래서 테스트는 변경이 트랙을 떨어뜨리거나 왜곡할 때 발동되는 회귀 검사가 됩니다. 팩별 GLB 격리(새로 로드하고, 다음 것 전에 해제)는 전체 스윕에 걸친 캐시 경합을 피합니다.
CDN glTF 그래프를 위한 LoadingManager.setURLModifier. CDN이 glTF를 상대 URI 그래프로, 동반하는 include 맵(상대 경로에서 절대 URL로)과 함께 배포할 때, setURLModifier는 JSON을 다시 쓰지 않고도 로더가 요청하는 모든 의존성을 CDN을 통해 해석합니다. 이것은 멀티파일, 멀티해상도 배포를 한 번의 클릭 로드로 접어 넣습니다. 다음 로드 전에 이전 모델의 지오메트리, 머티리얼, 텍스처 맵을 해제하면 탐색 세션 동안 수백 MB의 GPU 메모리가 쌓이는 것을 막습니다.
비파괴적 메시 단순화. 각 메시의 원본 지오메트리 복제본을 저장하고 단순화 비율마다 인덱스 버퍼만 다시 빌드하면 변경이 빠르게 유지되고 반복 단순화로 인한 누적 손상을 피합니다. geometry.groups 슬라이스별로 돌리고 그룹을 재구성하면 멀티 머티리얼 할당이 보존됩니다. 이것이 거리 기반 LOD에 어떻게 공급되는지는 LOD와 meshoptimizer를 보세요.
29부 중 17부. 이전: 16부 - 계속 자라나는 세계를 위한 구조 다음: 18부 - AI가 배치한 듯한 스캐터 브러시 시리즈 가이드: /ko/blog/2026-02-25-open-world-browser-series-guide