jvinhit//lab

Search posts

Type to search across journal entries.

navigate open esc close

Three.js from Zero to Senior · Part 6 — Loading Models & the Animation System

Bring in real assets and make them move: loading glTF with Draco, the AnimationMixer / AnimationClip / AnimationAction trio, crossfading between clips, and frame-rate-independent playback — with a live mixer you can drive.

4 MIN READ Updated JUL 12, 2026

Tới giờ mọi vật thể đều sinh ra trong code. Dự án thật nạp asset tạo trong Blender, Maya, hay mua trên chợ — nhân vật, đạo cụ, cả môi trường — và nhiều cái đến kèm animation nướng sẵn. Phần này nói về đưa các asset đó vào và phát animation của chúng.

Demo nối đúng hệ animation bạn sẽ dùng cho nhân vật glTF — nhưng với clip dựng tay để chạy không cần tải gì. Crossfade giữa các clip và chỉnh time scale:

Mở demo toàn màn hình

glTF — “JPEG của 3D”

glTF (.gltf / .glb) là định dạng phân phối chuẩn cho web: gọn, parse nhanh, và mang mesh, material PBR, camera, animation trong một file. Ưu tiên .glb nhị phân cho production.

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

const loader = new GLTFLoader();
loader.load('/models/robot.glb', (gltf) => {
  scene.add(gltf.scene);          // the model's root (a Group)
  // gltf.animations → an array of AnimationClips, if any
});

gltf.scene là một Group scene-graph bình thường — mọi thứ ở Phần 3 đều áp dụng. gltf.animations là mảng clip ta sắp phát.

Draco — đừng ship mesh khổng lồ

Model chi tiết thì nặng. Draco nén geometry mạnh; bạn nối một DRACOLoader vào GLTFLoader và nó tự giải mã khi nạp.

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

const draco = new DRACOLoader();
draco.setDecoderPath('https://www.gstatic.com/draco/v1/decoders/');
loader.setDRACOLoader(draco);

Senior luôn kiểm cân nặng asset: một .glb 40 MB chưa nén có thể còn vài MB với Draco + nén texture (KTX2/Basis).

Bộ ba animation

Animation của Three.js có ba lớp, và tên gọi làm ai cũng rối lúc đầu. Mô hình tư duy:

  • AnimationClipbản thu. Một gói track keyframe có tên (“Walk”, “Idle”, “Jump”). Dữ liệu tái dùng, tự nó không phát gì.
  • AnimationMixermáy phát, gắn với một model. Nó đẩy thời gian và trộn clip. Một mixer cho mỗi vật thể có animation.
  • AnimationActionđiều khiển play/pause/loop cho một clip trên mixer đó. Tạo qua mixer.clipAction(clip).
const mixer = new THREE.AnimationMixer(gltf.scene);
const action = mixer.clipAction(gltf.animations[0]);
action.play(); // starts; you still need to advance the mixer each frame

Đẩy mixer — dòng ai cũng quên

action.play() tự nó không làm gì animate. Mixer cần delta time mỗi frame — và đây chính là lý do ta dùng Timer từ Phần 1.

const timer = new THREE.Timer();
timer.connect(document);

function animate(timestamp) {
  timer.update(timestamp);
  const delta = timer.getDelta();   // seconds since last frame
  mixer.update(delta);              // ← without this, nothing moves
  renderer.render(scene, camera);
}
renderer.setAnimationLoop(animate);

Truyền delta (không phải bước cố định) là thứ khiến phát lại độc lập với frame rate: một clip kéo dài đúng thời gian thật ở 30 fps hay 144 fps.

Crossfade — trộn, đừng giật

Đổi cứng từ “Idle” sang “Walk” trông như robot. crossFadeTo giảm trọng số một action xuống trong khi tăng cái khác lên qua một khoảng thời gian — nút “crossfade” của demo làm đúng vậy.

function switchTo(next, duration = 0.4) {
  next.reset().play();
  current.crossFadeTo(next, duration, false);
  current = next;
}

Các cần khác bạn sẽ cần:

action.setLoop(THREE.LoopRepeat);     // or LoopOnce
action.timeScale = 0.5;               // half speed; negative = reverse
action.setEffectiveWeight(0.5);       // blend multiple clips at once
mixer.timeScale = 1;                  // global speed for the whole mixer

Dựng một clip bằng tay

Khi không có clip tạo sẵn — chuyển động UI, hiệu ứng thủ tục, chính demo này — bạn có thể ráp một AnimationClip từ KeyframeTrack. Tên track là tênĐốiTượng.thuộcTính:

// times (s), then values flattened: [x,y,z, x,y,z, …] for a VectorKeyframeTrack
const bounce = new THREE.VectorKeyframeTrack(
  '.position',                       // '.' = the object the mixer is bound to
  [0, 0.5, 1],                       // keyframe times
  [0, 0, 0,  0, 2, 0,  0, 0, 0],     // positions at each time
);
const clip = new THREE.AnimationClip('bounce', 1, [bounce]);
mixer.clipAction(clip).play();

Có track theo kiểu cho mỗi dạng dữ liệu: NumberKeyframeTrack, VectorKeyframeTrack, QuaternionKeyframeTrack (luôn dùng cái này cho xoay, không phải Euler), và ColorKeyframeTrack.

Lời cảnh báo của sư phụ

  • Model nạp xong mà đứng im? Bạn quên mixer.update(delta) trong loop, hoặc chưa gọi action.play().
  • Animation nhanh lên trên màn hình tần số cao? Bạn truyền bước cố định thay vì delta từ Timer.
  • loader.load không trả gì? Nó bất đồng bộ — model nằm trong callback, không phải sau lời gọi.
  • Animation xoay giật/lật? Dùng QuaternionKeyframeTrack, đừng animate góc Euler trực tiếp.
  • Nhiều nhân vật độc lập, một mixer? Thường nên dùng một mixer cho mỗi root được điều khiển độc lập; optionalRootAnimationObjectGroup là ngoại lệ có chủ đích.

Luyện tập, không thì coi như chưa học

  1. Cảm giác crossfade: trong demo, đổi clip với crossfade dài và ngắn, cảm nhận khác biệt.
  2. Time scale: kéo time scale về 0.25 và -1 — slow-mo và tua ngược.
  3. Tạo một clip: dựng một NumberKeyframeTrack trên .rotation[y] và phát một vòng quay 360°.

Phần tiếp theo

Giờ bạn nạp được asset thật có animation và điều khiển phát lại như một đạo diễn. Điều đó khép lại vòng “dựng một scene”. Nửa sau của series hướng tới làm nó nhanh và đẹp: Phần 7 đào vào post-processing và EffectComposer — bloom, độ sâu trường ảnh, và các pass shader tuỳ biến tạo nét điện ảnh cuối cùng cho bản render.