Community코딩 & 개발github.com

threejs-gltf-loading

`.gltf`/`.glb` 모델을 three.js r187에서 로드하고 애니메이션을 재생합니다: GLTFLoader, DRACO, Meshopt 및 KTX2 디코더, AnimationMixer 크로스페이드 및 SkeletonUtils 클론. 모델이 보이지 않거나 너무 작을 때, 디코더 파일 404, 애니메이션이 재생되지 않거나 클론이 하나의 스켈레톤을 공유할 때 사용하세요. 씬 설정(`threejs-scene-setup`)이나 조명(`threejs-materials-lighting`)에는 해당되지 않습니다.

threejs-gltf-loading란 무엇인가요?

이 스킬은 three.js r187 환경에서 `.gltf` 또는 `.glb` 포맷의 3D 모델을 로드하고 애니메이션을 제어하는 방법을 설명합니다. DRACO, Meshopt, KTX2와 같은 압축된 지오메트리와 텍스처 디코더를 지원하며, `AnimationMixer`를 사용한 클립 재생, 크로스페이드 전환, `SkeletonUtils.clone`을 통한 복제 문제 해결 방법을 제공합니다. 모델이 로드되지 않거나, 애니메이션이 작동하지 않거나, 복제된 캐릭터의 스켈레톤이 공유되는 문제를 해결하는 데 유용합니다. Cursor, Claude Code, Codex와 같은 AI 에이전트가 three.js 기반 웹 3D 프로젝트에서 모델 관련 작업을 수행할 때 참고할 수 있는 실용적인 패턴과 문제 해결 방법을 제시합니다.

지원 대상~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/mext-ai/elearning-agent-skills/tree/HEAD/third-party/gamedev/threejs-gltf-loading

Installed? Explore more 코딩 & 개발 skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

즐겨 사용하는 AI에게 물어보기

이 에이전트 스킬이 미리 로드된 새 채팅을 엽니다.

문서

three.js glTF 로딩

.gltf / .glb 모델(압축된 지오메트리(DRACO, Meshopt) 및 텍스처(KTX2) 포함)을 로드하고 애니메이션 클립을 재생합니다. 패턴은 r187을 대상으로 합니다. 기존 프로젝트는 사용자가 업그레이드를 요청하지 않는 한 고정된 릴리스를 유지합니다. glTF는 기본 런타임 포맷입니다. OBJ는 씬 그래프나 애니메이션이 없고, FBX는 브라우저에서 파싱하기에 무겁습니다.

사용 시기

  • 모델이나 캐릭터 가져오기, 노드 계층 검사, 클립 재생 또는 블렌딩, 애니메이션 캐릭터의 여러 복사본 생성, 또는 나타나지 않는 모델 수정.
  • 파일이 .gltf / .glb이거나, 코드가 three/addons/loaders/...에서 GLTFLoader, DRACOLoader 또는 KTX2Loader를 가져오는 경우.
  • 렌더러, 카메라 및 루프(threejs-scene-setup), 모델의 조명 및 재질(threejs-materials-lighting), 또는 DCC 도구의 모델링 및 내보내기 설정에는 해당되지 않습니다.

워크플로

  1. 로더 코드를 작성하기 전에 파일 검사 (gltf-transform을 사용할 수 있을 때): npx @gltf-transform/cli inspect model.glb는 메시, 확장(예: KHR_draco_mesh_compression, EXT_meshopt_compression, KHR_texture_basisu), 클립 및 텍스처 크기를 나열합니다. 나열된 각 압축 확장은 디코더를 연결해야 합니다.
  2. GLTFLoader로 로드 (비동기 코드에서는 loadAsync). 결과에는 gltf.scene(루트 Object3D)과 gltf.animations(AnimationClip[])가 있습니다.
  3. gltf.scene을 추가하고 프레이밍합니다. glTF 단위는 미터입니다. 이름과 경계를 기록하여 코드가 제어할 부분을 찾습니다.
  4. 애니메이션 루트마다 하나의 AnimationMixer, mixer.clipAction(clip)으로 액션을 생성하고, 매 프레임마다 mixer.update(seconds)를 호출합니다.
  5. 1단계에서 찾은 확장에 대한 디코더를 연결합니다. r185부터 DRACOLoader와 KTX2Loader는 경로 없이 번들 디코더 파일을 찾습니다.
  6. Verify로 확인합니다.

패턴

1. 모델 로드 및 프레이밍

import * as THREE from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';

const gltf = await new GLTFLoader().loadAsync('assets/robot.glb');
const root = gltf.scene;
scene.add(root);

const box = new THREE.Box3().setFromObject(root);
console.log('size (m):', box.getSize(new THREE.Vector3()).toArray());
console.log('clips:', gltf.animations.map((c) => c.name));

2. 클립 재생

const timer = new THREE.Timer();            // THREE.Clock은 r183부터 폐기됨
const mixer = new THREE.AnimationMixer(gltf.scene);
const run = THREE.AnimationClip.findByName(gltf.animations, 'Run') ?? gltf.animations[0];
mixer.clipAction(run).play();

renderer.setAnimationLoop(() => {
  timer.update();
  mixer.update(timer.getDelta());           // 초 단위, 밀리초 아님
  renderer.render(scene, camera);
});

3. 클립 간 크로스페이드

const actions = Object.fromEntries(gltf.animations.map((c) => [c.name, mixer.clipAction(c)]));
let current = actions.Idle;
current.play();

function transitionTo(name, duration = 0.3) {
  const next = actions[name];
  if (!next || next === current) return;
  next.reset().play();                      // 되감기 및 이전 페이드 제거
  current.crossFadeTo(next, duration, false);
  current = next;
}

4. 압축된 지오메트리 및 텍스처

import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { DRACOLoader, DRACO_GLTF_CONFIG } from 'three/addons/loaders/DRACOLoader.js';
import { KTX2Loader } from 'three/addons/loaders/KTX2Loader.js';
import { MeshoptDecoder } from 'three/addons/libs/meshopt_decoder.module.js';

const loader = new GLTFLoader()
  .setDRACOLoader(new DRACOLoader().setDecoderPath(DRACO_GLTF_CONFIG)) // glTF 전용 WASM 빌드 (r185+)
  .setKTX2Loader(new KTX2Loader().detectSupport(renderer))             // 렌더러 필요
  .setMeshoptDecoder(MeshoptDecoder);
// 디코더와 워커가 한 번만 생성되도록 모든 모델에 대해 이 로더 하나를 공유합니다.

r185 이전 릴리스나 호스트가 번들 파일을 제공하지 않는 경우, 설치된 three에서 examples/jsm/libs/draco/ 및 libs/basis/를 복사하여 setDecoderPath / setTranscoderPath에 해당 폴더를 전달합니다.

5. 이름이 지정된 부품 찾기

const wheels = [];
gltf.scene.traverse((node) => {
  if (node.name.startsWith('Wheel')) wheels.push(node);
});
// 루프에서: for (const w of wheels) w.rotation.x += dt * 4;

6. 코드가 필요한 부품을 유지하면서 모델 축소

# Meshopt 압축, 최대 1024px WebP 텍스처, 이름이 지정된 노드와 계층 유지.
npx @gltf-transform/cli optimize in.glb out.glb \
  --compress meshopt --texture-compress webp --texture-size 1024 \
  --flatten false --join-named false
npx @gltf-transform/cli validate out.glb

optimize는 기본적으로 이름이 지정된 노드를 포함하여 계층을 평탄화하고 메시를 결합합니다. 이렇게 하면 드로우 콜이 줄어들지만, Wheel_FL을 찾거나 도어 노드를 애니메이션하는 코드는 아무것도 찾지 못하게 됩니다. 코드가 접근하는 모든 모델에 대해 위의 두 플래그를 유지하세요. 출력에는 setMeshoptDecoder(패턴 4)가 필요합니다.

확인

  1. 파일 확인: 각 새 파일이나 변경된 파일에 대해 npx @gltf-transform/cli validate model.glb를 실행하고, 로더 코드를 디버깅하기 전에 보고된 오류를 수정합니다.
  2. 빌드: npm run build (또는 npx tsc --noEmit)가 통과합니다.
  3. 런타임 프로브: scene과 mixer를 window에 노출시킵니다(Object.assign(window, { scene, mixer })). 이 표현식을 Playwright의 page.evaluate에 문자열로 전달하여 Node가 아닌 페이지에서 실행되도록 하거나(threejs-scene-setup의 스모크 스크립트가 작동함), 사용자에게 브라우저 콘솔에 await을 앞에 붙여 붙여넣고 출력을 보고하도록 요청합니다:
(async () => {
  const before = window.mixer?.time ?? null;
  await new Promise((r) => setTimeout(r, 500));
  const meshes = [];
  window.scene.traverse((o) => { if (o.isMesh) meshes.push(o.name || o.type); });
  return { meshes: meshes.length, mixerAdvanced: before !== null && window.mixer.time > before };
})();

0이 아닌 메시 개수와 mixerAdvanced: true를 기대하고, 스크린샷으로 스케일과 방향을 확인합니다.

주의사항

  • 모델이 로드되지만 아무것도 표시되지 않음 → PBR 재질에 빛이나 환경이 없음(threejs-materials-lighting 참조) 또는 스케일이 100배 차이(센티미터 단위 내보내기). 패턴 1의 경계 상자를 로그로 확인합니다.
  • 콜백 외부에서 gltf가 정의되지 않음 → load는 비동기입니다. loadAsync를 사용하거나 콜백 내부에서 공유 참조를 할당합니다.
  • 애니메이션이 움직이지 않음 → mixer.update가 매 프레임 호출되지 않거나, 밀리초 단위로 전달되거나, play()가 호출되지 않았습니다.
  • draco_* 또는 basis_* 파일 404 또는 디코딩 오류 → 디코더 파일이 제공되지 않거나 다른 three.js 릴리스에서 왔습니다. 설치된 three에서 복사본을 제공하고 로더가 이를 가리키도록 합니다.
  • setKTX2Loader must be called before loading KTX2 textures 또는 setMeshoptDecoder must be called before loading compressed files → 파일이 KHR_texture_basisu 또는 meshopt 압축을 사용합니다. 디코더를 연결합니다(패턴 4).
  • .hasFeature() called before the backend is initialized → KTX2Loader.detectSupport가 await renderer.init() 완료 전에 WebGPURenderer에서 실행되었습니다.
  • 동일한 모델을 두 번 추가하면 이동함 → Object3D는 하나의 부모를 가집니다. 복제하고, 스킨 메시의 경우 SkeletonUtils.clone을 사용합니다(참고자료 참조).
  • 클론이 함께 애니메이션되거나 하나만 움직임 → 일반 .clone()은 스켈레톤을 공유합니다. SkeletonUtils.clone과 클론당 별도의 mixer를 사용합니다.
  • gltf-transform optimize 후 이름이 지정된 부품이 사라짐 → 기본 평탄화 및 결합이 병합했습니다. --flatten false --join-named false로 다시 실행합니다.
  • 베이크된 오프셋이 런타임 제어와 충돌 → 내보내기 도구가 자식 노드에 스케일이나 회전을 베이크할 수 있습니다. 먼저 이름과 로컬 변환을 덤프합니다. 깨끗한 피벗을 위해 새 Object3D 아래에 부품을 다시 부모로 지정하거나, 변환을 적용하여 다시 내보내도록 요청합니다.

참고자료

  • 여러 로더에 걸친 LoadingManager 진행률 표시줄, SkeletonUtils.clone을 사용한 여러 애니메이션 적 생성, AnimationAction 루핑 및 블렌딩 세부 정보, 디코더 호스팅 참고 사항, 아티스트에게 전달할 내보내기 체크리스트에 대해서는 references/loaders-and-animation.md를 읽어보세요.

관련 스킬

  • threejs-scene-setup: 렌더러, 카메라 및 루프.
  • threejs-materials-lighting: PBR 모델이 올바르게 보이도록 조명 및 환경.
  • create-game-assets: 3D 에셋 자체의 브리핑, 소싱 및 검사.
  • fps-shooter: 이러한 스킬을 기반으로 하는 3D 장르.

버전 정보

최종 확인: 2026-11-11, three r187 (npm [email protected], 2026-10-30 게시) 및 @gltf-transform/cli 4.5.1 기준.

  • r185: DRACOLoader와 KTX2Loader가 기본적으로 번들 디코더 폴더를 사용하도록 변경됨; DRACO_GLTF_CONFIG 추가됨; DRACOLoader.setDecoderConfig() 폐기됨(r194에서 제거 예정). r183: EXT_meshopt_compression 외에 KHR_meshopt_compression 지원. r181: KTX2Loader.detectSupportAsync() 폐기됨.
  • gltf-transform 4.5.1 optimize 기본값: --compress meshopt, --flatten true, --join true, --join-named true, --simplify true, --texture-compress auto. --texture-compress ktx2는 PATH에 KTX-Software 4.4.0 이상의 ktx 명령이 필요합니다.
  • 출처: r187 릴리스 노트, r187 DRACOLoader.js, r187 GLTFLoader.js, glTF Transform CLI, npx @gltf-transform/cli optimize --help.

Individual skills in this repo

This repo contains 1 individual skill — each has its own dedicated page.

관련 스킬