threejs-gltf-loading

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

three.js glTF Loading

three.js glTF 加载

Load
.gltf
/
.glb
models and play their animations in three.js, including compressed geometry (DRACO/Meshopt) and textures (KTX2). Patterns target r165+, verified against r184.
在three.js中加载
.gltf
/
.glb
模型并播放其动画,包括支持压缩几何体(DRACO/Meshopt)和纹理(KTX2)。以下模式适用于 r165+ 版本,已在 r184 版本中验证。

When to use

使用场景

  • Use to import a 3D model, add it to the scene, inspect its node hierarchy, and play baked/skinned animation clips with an
    AnimationMixer
    .
  • Use when files are
    .gltf
    /
    .glb
    , or code imports
    GLTFLoader
    /
    DRACOLoader
    /
    KTX2Loader
    from
    three/addons/loaders/...
    .
When not to use: creating the renderer/camera/loop →
threejs-scene-setup
. Tuning surface look, lights, or shadows on the loaded model →
threejs-materials-lighting
. Authoring/exporting the model itself (Blender) is out of scope; prefer glTF over OBJ/FBX for runtime.
  • 用于导入3D模型、将其添加到场景中、检查节点层级结构,以及通过
    AnimationMixer
    播放烘焙/蒙皮动画片段。
  • 当文件格式为
    .gltf
    /
    .glb
    ,或代码从
    three/addons/loaders/...
    导入
    GLTFLoader
    /
    DRACOLoader
    /
    KTX2Loader
    时使用。
不适用场景:创建渲染器/相机/循环 → 使用
threejs-scene-setup
。调整已加载模型的表面外观、灯光或阴影 → 使用
threejs-materials-lighting
。模型的创作/导出(如Blender)不在本技能范围内;Web运行时优先使用glTF而非OBJ/FBX。

Core workflow

核心工作流程

  1. Why glTF. It's a transmission format: binary vertex data, PBR materials, and animations are ready to render with minimal parsing. Prefer it over OBJ (no scene graph, no animation) and FBX (heavy) for the web.
  2. Load with
    GLTFLoader
    .
    loader.load(url, onLoad, onProgress, onError)
    . The result
    gltf
    has
    gltf.scene
    (the
    Object3D
    root),
    gltf.animations
    (
    AnimationClip[]
    ),
    gltf.cameras
    , and
    gltf.asset
    .
  3. Add
    gltf.scene
    to your scene
    and frame it. Inspect the hierarchy with
    traverse
    /
    getObjectByName
    to find the parts you'll control.
  4. Play animations with an
    AnimationMixer
    .
    One mixer per animated root;
    mixer.clipAction(clip).play()
    ; advance with
    mixer.update(delta)
    every frame.
  5. Decode compressed assets. Attach a
    DRACOLoader
    (and/or
    KTX2Loader
    + Meshopt) so DRACO meshes and KTX2 textures load; point the decoders at their files.
  6. Verify what loaded — log the scene graph and
    gltf.animations
    , and confirm the model is visible (right scale, lit) and the clip actually plays.
  1. 为什么选择glTF:它是一种传输格式,包含二进制顶点数据、PBR材质和动画,只需极少解析即可渲染。在Web环境中,它比OBJ(无场景图、无动画)和FBX(体积大)更合适。
  2. 使用
    GLTFLoader
    加载
    :调用
    loader.load(url, onLoad, onProgress, onError)
    。返回的
    gltf
    对象包含
    gltf.scene
    Object3D
    根节点)、
    gltf.animations
    AnimationClip[]
    数组)、
    gltf.cameras
    gltf.asset
  3. gltf.scene
    添加到场景并调整视角
    :使用
    traverse
    /
    getObjectByName
    检查层级结构,找到需要控制的部分。
  4. 通过
    AnimationMixer
    播放动画
    :每个动画根节点对应一个mixer;调用
    mixer.clipAction(clip).play()
    ;每帧通过
    mixer.update(delta)
    更新动画进度。
  5. 解码压缩资源:附加
    DRACOLoader
    (和/或
    KTX2Loader
    + Meshopt)以加载DRACO网格和KTX2纹理;将解码器指向对应的文件。
  6. 验证加载结果 —— 打印场景图和
    gltf.animations
    ,确认模型可见(缩放正确、已打光)且动画片段正常播放。

Patterns

示例模式

1. Load a model and frame it

1. 加载模型并调整视角

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

const loader = new GLTFLoader();
loader.load(
  'assets/robot.glb',
  (gltf) => {
    const root = gltf.scene;
    scene.add(root);
    // Inspect: gltf.animations is an array of AnimationClip.
    console.log('clips:', gltf.animations.map((c) => c.name));
  },
  (event) => console.log(`${(event.loaded / event.total) * 100}% loaded`),
  (error) => console.error('glTF load failed:', error)
);
js
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';

const loader = new GLTFLoader();
loader.load(
  'assets/robot.glb',
  (gltf) => {
    const root = gltf.scene;
    scene.add(root);
    // Inspect: gltf.animations is an array of AnimationClip.
    console.log('clips:', gltf.animations.map((c) => c.name));
  },
  (event) => console.log(`${(event.loaded / event.total) * 100}% loaded`),
  (error) => console.error('glTF load failed:', error)
);

2. Play a skinned animation with AnimationMixer

2. 使用AnimationMixer播放蒙皮动画

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

let mixer;                                  // declare outside so the loop can see it
const clock = new THREE.Clock();

new GLTFLoader().load('assets/character.glb', (gltf) => {
  scene.add(gltf.scene);
  mixer = new THREE.AnimationMixer(gltf.scene);          // one mixer per animated root
  const clip = THREE.AnimationClip.findByName(gltf.animations, 'Run')
            ?? gltf.animations[0];
  mixer.clipAction(clip).play();
});

renderer.setAnimationLoop(() => {
  const dt = clock.getDelta();
  if (mixer) mixer.update(dt);              // advance the animation by real seconds
  renderer.render(scene, camera);
});
js
import * as THREE from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';

let mixer;                                  // declare outside so the loop can see it
const clock = new THREE.Clock();

new GLTFLoader().load('assets/character.glb', (gltf) => {
  scene.add(gltf.scene);
  mixer = new THREE.AnimationMixer(gltf.scene);          // one mixer per animated root
  const clip = THREE.AnimationClip.findByName(gltf.animations, 'Run')
            ?? gltf.animations[0];
  mixer.clipAction(clip).play();
});

renderer.setAnimationLoop(() => {
  const dt = clock.getDelta();
  if (mixer) mixer.update(dt);              // advance the animation by real seconds
  renderer.render(scene, camera);
});

3. Cross-fade between two clips

3. 两个动画片段之间交叉淡入淡出

js
const actions = {};
mixer = new THREE.AnimationMixer(gltf.scene);
for (const clip of gltf.animations) {
  actions[clip.name] = mixer.clipAction(clip);
}
actions['Idle'].play();

function transitionTo(name, duration = 0.3) {
  const next = actions[name];
  next.reset().play();
  for (const [n, action] of Object.entries(actions)) {
    if (n !== name) action.crossFadeTo(next, duration, false);
  }
}
js
const actions = {};
mixer = new THREE.AnimationMixer(gltf.scene);
for (const clip of gltf.animations) {
  actions[clip.name] = mixer.clipAction(clip);
}
actions['Idle'].play();

function transitionTo(name, duration = 0.3) {
  const next = actions[name];
  next.reset().play();
  for (const [n, action] of Object.entries(actions)) {
    if (n !== name) action.crossFadeTo(next, duration, false);
  }
}

4. DRACO-compressed geometry

4. DRACO压缩几何体

js
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js';

const draco = new DRACOLoader();
// Point at the decoder files you ship (or a pinned CDN copy of the same version).
draco.setDecoderPath('https://cdn.jsdelivr.net/npm/three@0.184.0/examples/jsm/libs/draco/');

const loader = new GLTFLoader();
loader.setDRACOLoader(draco);
loader.load('assets/city-draco.glb', (gltf) => scene.add(gltf.scene));
js
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js';

const draco = new DRACOLoader();
// Point at the decoder files you ship (or a pinned CDN copy of the same version).
draco.setDecoderPath('https://cdn.jsdelivr.net/npm/three@0.184.0/examples/jsm/libs/draco/');

const loader = new GLTFLoader();
loader.setDRACOLoader(draco);
loader.load('assets/city-draco.glb', (gltf) => scene.add(gltf.scene));

5. Find and animate a named part

5. 查找并动画化指定名称的部件

js
new GLTFLoader().load('assets/car.glb', (gltf) => {
  scene.add(gltf.scene);
  const wheels = [];
  gltf.scene.traverse((node) => {
    if (node.name.startsWith('Wheel')) wheels.push(node);
  });
  renderer.setAnimationLoop(() => {
    const dt = clock.getDelta();
    for (const w of wheels) w.rotation.x += dt * 4;
    renderer.render(scene, camera);
  });
});
js
new GLTFLoader().load('assets/car.glb', (gltf) => {
  scene.add(gltf.scene);
  const wheels = [];
  gltf.scene.traverse((node) => {
    if (node.name.startsWith('Wheel')) wheels.push(node);
  });
  renderer.setAnimationLoop(() => {
    const dt = clock.getDelta();
    for (const w of wheels) w.rotation.x += dt * 4;
    renderer.render(scene, camera);
  });
});

Pitfalls

常见陷阱

  • Model loads but is invisible → it has lit (PBR) materials and the scene has no light or environment. Add a light or
    scene.environment
    (see
    threejs-materials-lighting
    ), and check scale — glTF is in metres, so a 0.01-scaled asset is tiny.
  • load
    is async
    gltf
    only exists inside the callback; declare
    mixer
    /refs outside and assign them in the callback, or use
    await loader.loadAsync(url)
    .
  • Animation never moves → you didn't call
    mixer.update(delta)
    each frame, or you passed milliseconds instead of seconds (use
    clock.getDelta()
    ), or you forgot
    action.play()
    .
  • DRACO/KTX2 model fails → the decoder/transcoder path is wrong or version- mismatched.
    setDecoderPath
    /
    setTranscoderPath
    must point at files matching your three.js version.
  • Multiple mixers fighting → use one
    AnimationMixer
    per animated root and create all actions from it; don't make a new mixer per clip.
  • Baked-in transforms surprise you → exporters sometimes bake scale/rotation onto child nodes. Dump the hierarchy (names + position/rotation/scale) before relying on a node's local transform; re-export from the source if the rig is unusable.
  • Origins are off → re-parent a part under a fresh
    Object3D
    to give it a clean pivot rather than fighting baked offsets.
  • 模型已加载但不可见 → 模型使用了受光照影响的(PBR)材质,但场景中没有灯光或环境。添加灯光或设置
    scene.environment
    (参考
    threejs-materials-lighting
    ),并检查缩放比例——glTF使用米作为单位,所以缩放0.01的资产会非常小。
  • load
    是异步操作
    gltf
    仅在回调函数内存在;在外部声明
    mixer
    /引用并在回调中赋值,或使用
    await loader.loadAsync(url)
  • 动画始终不播放 → 未在每帧调用
    mixer.update(delta)
    ,或传入的是毫秒而非秒(使用
    clock.getDelta()
    ),或忘记调用
    action.play()
  • DRACO/KTX2模型加载失败 → 解码器/转码器路径错误或版本不匹配。
    setDecoderPath
    /
    setTranscoderPath
    必须指向与你的three.js版本匹配的文件。
  • 多个mixer冲突 → 每个动画根节点使用一个
    AnimationMixer
    ,并从中创建所有动作;不要为每个动画片段创建新的mixer。
  • 烘焙变换导致意外问题 → 导出器有时会将缩放/旋转烘焙到子节点上。在依赖节点的局部变换之前,先导出层级结构(名称 + 位置/旋转/缩放);如果绑定不可用,从源文件重新导出。
  • 原点偏移 → 将部件重新父化到新的
    Object3D
    下,以获得干净的轴心点,而非修改烘焙的偏移量。

References

参考资料

  • For the full decode/transcode setup (DRACO + Meshopt + KTX2 together),
    loadAsync
    • a
      LoadingManager
      progress bar, reusing models with
      SkeletonUtils.clone
      , and exporter guidance (apply transforms, one clean root), read
      references/loaders-and-animation.md
      .
  • 如需完整的解码/转码设置(同时使用DRACO + Meshopt + KTX2)、
    loadAsync
    +
    LoadingManager
    进度条、使用
    SkeletonUtils.clone
    复用模型,以及导出指南(应用变换、单一干净根节点),请阅读
    references/loaders-and-animation.md

Related skills

相关技能

  • threejs-scene-setup
    — the renderer, camera, and loop this model renders into.
  • threejs-materials-lighting
    — lighting/environment so PBR models look right.
  • fps-shooter
    — a 3D genre that composes three.js skills.
  • threejs-scene-setup
    —— 模型渲染所需的渲染器、相机和循环。
  • threejs-materials-lighting
    —— 使PBR模型显示正常的灯光/环境设置。
  • fps-shooter
    —— 组合多种three.js技能的3D游戏类型。