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, và 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:}
AnimationClip— the recording. A named bundle of keyframe tracks (“Walk”, “Idle”, “Jump”). Reusable data, plays nothing on its own. {AnimationClip— bả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ì.}AnimationMixer— the player, bound to one model. It advances time and blends clips. One mixer per animated object. {AnimationMixer— má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— the play/pause/loop control for one clip on that mixer. Created viamixer.clipAction(clip). {AnimationAction— điều khiển play/pause/loop cho một clip trên mixer đó. Tạo quamixer.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 calledaction.play(). {Model nạp xong mà đứng im? Bạn quênmixer.update(delta)trong loop, hoặc chưa gọiaction.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.loadreturns nothing? It’s async — the model lives in the callback, not after the call. {loader.loadkhô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ùngQuaternionKeyframeTrack, đừ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ầnAnimationMixerriêng.}
Practice, or it didn’t happen {Luyện tập, không thì coi như chưa học}
- 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.}
- 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.}
- Author a clip {Tạo một clip}: build a
NumberKeyframeTrackon.rotation[y]and play a 360° spin. {dựng mộtNumberKeyframeTracktrê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.}