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.

So far every object was born in code. {Tới giờ mọi vật thể đều sinh ra trong code.} Real projects load assets authored in Blender, Maya, or bought off a marketplace — characters, props, whole environments — and many arrive with animation already baked in. {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.} This part is about getting those assets in and playing their animations. {Phần này nói về đưa các asset đó vào và phát animation của chúng.}

The demo wires up the exact same animation system you’ll use for a glTF character — but with hand-built clips so it runs with zero downloads. {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 between clips and scrub the time scale: {Crossfade giữa các clip và chỉnh time scale:}

Open the full demo {Mở demo đầy đủ}: /tools/threejs-animation-mixer-demo/.

glTF — the JPEG of 3D {glTF — “JPEG của 3D”}

glTF (.gltf / .glb) is the standard delivery format for the web: compact, fast to parse, and it carries meshes, PBR materials, cameras, and animation in one file. {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.} Prefer the binary .glb for production. {Ư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 is a normal scene-graph Group — everything from Part 3 applies. {gltf.scene là một Group scene-graph bình thường — mọi thứ ở Phần 3 đều áp dụng.} gltf.animations is the array of clips we’re about to play. {gltf.animations là mảng clip ta sắp phát.}

Draco — don’t ship huge meshes {Draco — đừng ship mesh khổng lồ}

Detailed models are heavy. {Model chi tiết thì nặng.} Draco compresses mesh geometry dramatically; you wire a DRACOLoader into the GLTFLoader and it transparently decodes on load. {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);

A senior always checks asset weight: a 40 MB uncompressed .glb can drop to a few MB with Draco + texture compression (KTX2/Basis). {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).}

The animation trio {Bộ ba animation}

Three.js animation has three layers, and the names confuse everyone at first. {Animation của Three.js có ba lớp, và tên gọi làm ai cũng rối lúc đầu.} Here’s the mental model: {Mô hình tư duy:}

  • AnimationClipthe recording. A named bundle of keyframe tracks (“Walk”, “Idle”, “Jump”). Reusable data, plays nothing on its own. {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ì.}
  • AnimationMixerthe player, bound to one model. It advances time and blends clips. One mixer per animated object. {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.}
  • AnimationActionthe play/pause/loop control for one clip on that mixer. Created via mixer.clipAction(clip). {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

Advancing the mixer — the line everyone forgets {Đẩy mixer — dòng ai cũng quên}

action.play() doesn’t animate anything by itself. {action.play() tự nó không làm gì animate.} The mixer needs delta time every frame — and this is exactly why we used clock.getDelta() back in Part 1. {Mixer cần delta time mỗi frame — và đây chính là lý do ta dùng clock.getDelta() từ Phần 1.}

const clock = new THREE.Clock();

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

Feeding delta (not a fixed step) is what makes playback frame-rate independent: a clip lasts the same wall-clock time at 30 fps or 144 fps. {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.}

Crossfading — blend, don’t snap {Crossfade — trộn, đừng giật}

Hard-switching from “Idle” to “Walk” looks robotic. {Đổi cứng từ “Idle” sang “Walk” trông như robot.} crossFadeTo blends the weight of one action down while ramping another up over a duration — the demo’s “crossfade” buttons do exactly this. {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;
}

Other levers you’ll reach for: {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

Building a clip by hand {Dựng một clip bằng tay}

When you don’t have an authored clip — UI motion, a procedural effect, the demo itself — you can assemble an AnimationClip from KeyframeTracks. {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.} The track name is targetName.property: {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();

There are typed tracks for each data shape: NumberKeyframeTrack, VectorKeyframeTrack, QuaternionKeyframeTrack (always use this for rotation, not Euler), and ColorKeyframeTrack. {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.}

The master’s warnings {Lời cảnh báo của sư phụ}

  • Model loaded but frozen? You forgot mixer.update(delta) in the loop, or never called action.play(). {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 speeds up on a fast monitor? You passed a fixed step instead of clock.getDelta(). {Animation nhanh lên trên màn hình tần số cao? Bạn truyền bước cố định thay vì clock.getDelta().}
  • loader.load returns nothing? It’s async — the model lives in the callback, not after the call. {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.}
  • Rotation animation glitches/flips? Use a QuaternionKeyframeTrack, never animate Euler angles directly. {Animation xoay giật/lật? Dùng QuaternionKeyframeTrack, đừng animate góc Euler trực tiếp.}
  • Multiple models, one mixer? Don’t — each animated model needs its own AnimationMixer. {Nhiều model, một mixer? Đừng — mỗi model có animation cần AnimationMixer riêng.}

Practice, or it didn’t happen {Luyện tập, không thì coi như chưa học}

  1. Crossfade feel {Cảm giác crossfade}: in the demo, switch clips with a long vs. short crossfade duration and feel the difference. {trong demo, đổi clip với crossfade dài và ngắn, cảm nhận khác biệt.}
  2. Time scale {Time scale}: scrub the mixer time scale to 0.25 and -1 — slow motion and reverse. {kéo time scale về 0.25 và -1 — slow-mo và tua ngược.}
  3. Author a clip {Tạo một clip}: build a NumberKeyframeTrack on .rotation[y] and play a 360° spin. {dựng một NumberKeyframeTrack trên .rotation[y] và phát một vòng quay 360°.}

What’s next {Phần tiếp theo}

You can now load real, animated assets and control their playback like a director. {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.} That closes the “build a scene” arc. {Điều đó khép lại vòng “dựng một scene”.} The back half of this series turns toward making it fast and beautiful: Part 7 dives into post-processing and the EffectComposer — bloom, depth of field, and custom shader passes that give your render its final cinematic polish. {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.}