jvinhit//lab

Search posts

Type to search across journal entries.

navigate open esc close

Three.js from Zero to Senior · Bonus Part 14 — Blender → glTF → Three.js: the Import Workflow

Stop building everything from primitives. Model in Blender, export glTF/GLB, and load it with GLTFLoader. Export settings, scene-graph mapping, scale, units, PBR materials, and the pitfalls that bite — with a live model loader.

Part 13 made furniture out of boxes and cylinders. {Part 13 dựng đồ nội thất từ box và cylinder.} That’s the right tool for simple, configurable props — but the moment you need a damaged sci-fi helmet, a rigged character, or a scanned avocado, you don’t model it in code. {Đó là công cụ đúng cho vật đơn giản, cấu hình được — nhưng khi bạn cần một cái mũ sci-fi sờn rách, một nhân vật có xương, hay một quả bơ scan, bạn không dựng nó bằng code.} You build it in a real DCC tool — Blender, the free industry standard — export to glTF, and load it. {Bạn dựng nó trong một công cụ DCC thật — Blender, chuẩn ngành miễn phí — xuất ra glTF, rồi nạp vào.} This is how 95% of production Three.js scenes get their content. {Đây là cách 95% scene Three.js production có nội dung.}

The demo below is a model loader. {Demo bên dưới là một trình nạp model.} Switch between four real assets and watch the readout: how many meshes, materials and triangles the importer produced, and whether the file carries animation. {Đổi giữa bốn asset thật và xem chỉ số: importer tạo ra bao nhiêu mesh, material, triangle, và file có mang animation không.} Toggle wireframe to see topology, and the environment to see how PBR materials need light to look right. {Bật wireframe để thấy lưới, và environment để thấy material PBR cần ánh sáng mới đẹp.}

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

Why glTF, and glb vs gltf {Vì sao glTF, và glb khác gltf}

glTF is the “JPEG of 3D” — an open Khronos format designed to be transmitted and rendered, not edited. {glTF là “JPEG của 3D” — định dạng mở của Khronos, thiết kế để truyền tải và render, không phải để chỉnh sửa.} It stores meshes, PBR materials, textures, cameras, skins and animation in a layout that maps almost 1:1 onto Three.js objects, which is why GLTFLoader is first-class and fast. {Nó lưu mesh, material PBR, texture, camera, skin và animation theo bố cục ánh xạ gần như 1:1 sang object Three.js, nên GLTFLoader là công dân hạng nhất và nhanh.} Prefer it over OBJ/FBX for the web. {Hãy ưu tiên nó hơn OBJ/FBX cho web.}

Two flavours: {Hai biến thể:}

  • .glb — one binary file with geometry, textures and JSON all packed in. Ship this. One request, nothing to misplace. {.glb — một file nhị phân gói cả hình học, texture và JSON. Hãy ship cái này. Một request, không lạc file.}
  • .gltf — JSON that references external .bin buffers and image files. Readable and diff-able, but it’s many files. {.gltf — JSON tham chiếu tới các buffer .bin và file ảnh bên ngoài. Dễ đọc, dễ diff, nhưng là nhiều file.}

Modeling in Blender so it survives export {Dựng trong Blender để xuất ra không hỏng}

The export is only as clean as the file. {Bản xuất chỉ sạch bằng đúng file gốc.} A short pre-flight checklist saves hours of “why is it rotated / huge / black?”: {Một checklist ngắn trước khi xuất giúp tiết kiệm hàng giờ “sao nó bị xoay / khổng lồ / đen?”:}

  • Apply transforms (Ctrl+A → All Transforms). Unapplied rotation/scale ships into the runtime and confuses everything. {Apply transform (Ctrl+A → All Transforms). Rotation/scale chưa apply sẽ theo vào runtime và làm rối mọi thứ.}
  • Model at real-world scale in metres. Three.js is unit-agnostic, but matching 1 unit = 1 metre keeps lights, shadows and physics sane. {Dựng theo tỉ lệ thật bằng mét. Three.js không quy định đơn vị, nhưng để 1 unit = 1 mét giúp ánh sáng, bóng, vật lý hợp lý.}
  • Unwrap UVs for anything textured — no UVs, no texture maps. {Unwrap UV cho mọi thứ có texture — không UV thì không có texture map.}
  • Use the Principled BSDF for materials. It’s the one shader glTF maps cleanly to MeshStandardMaterial. {Dùng Principled BSDF cho material. Đây là shader duy nhất glTF ánh xạ gọn sang MeshStandardMaterial.}
  • Name your objects. Names become mesh.name — that’s how you’ll find the door to animate it. {Đặt tên object. Tên trở thành mesh.name — đó là cách bạn tìm cánh cửa để animate.}

Then export with File → Export → glTF 2.0, format glTF Binary (.glb), “+Y Up” on, “Apply Modifiers” on. {Sau đó xuất bằng File → Export → glTF 2.0, định dạng glTF Binary (.glb), bật “+Y Up”, bật “Apply Modifiers”.} Leave compression off for now — we add Draco in Part 15. {Tạm tắt nén — Part 15 mới thêm Draco.}

Loading it: GLTFLoader in ~10 lines {Nạp nó: GLTFLoader trong ~10 dòng}

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

const loader = new GLTFLoader();
loader.load(
  '/models/helmet.glb',
  (gltf) => {
    scene.add(gltf.scene);              // the imported subtree
    // gltf.animations  → AnimationClip[]
    // gltf.cameras     → authored cameras
  },
  (e) => console.log((e.loaded / e.total * 100).toFixed(0) + '%'), // progress
  (err) => console.error('load failed', err),                       // error
);

load() is asynchronous — everything that touches the model goes inside the success callback, never on the next line. {load() là bất đồng bộ — mọi thứ chạm vào model phải nằm trong callback thành công, đừng để ở dòng kế tiếp.} The progress callback drives the loading bar you see in the demo. {Callback progress điều khiển thanh loading bạn thấy trong demo.}

What the importer actually hands you {Importer thật sự đưa cho bạn cái gì}

gltf.scene is an ordinary THREE.Group. {gltf.scene là một THREE.Group bình thường.} Everything you learned about the scene graph applies — you can traverse it, re-parent nodes, swap materials, or find a mesh by name. {Mọi thứ bạn học về scene graph đều áp dụng — có thể traverse, đổi cha, thay material, hay tìm mesh theo tên.} The demo’s readout is just a traversal that counts what came in: {Chỉ số trong demo chỉ là một lần traverse đếm những gì nạp vào:}

let meshes = 0, tris = 0; const materials = new Set();
gltf.scene.traverse((o) => {
  if (!o.isMesh) return;
  meshes++;
  materials.add(o.material.uuid);
  o.castShadow = o.receiveShadow = true;     // importers don't set this
  const g = o.geometry;
  tris += (g.index ? g.index.count : g.attributes.position.count) / 3;
});

Note that last line in spirit: the importer never enables shadows for you. {Để ý dòng cuối: importer không bật bóng đổ giúp bạn.} If your model is flat and shadowless, this traversal is almost always the fix. {Nếu model phẳng và không bóng, lần traverse này gần như luôn là cách sửa.}

The two pitfalls everyone hits {Hai cái bẫy ai cũng dính}

Scale. {Tỉ lệ.} The Avocado in the demo is authored in centimetres — load it raw and it’s a speck; another model might fill the screen. {Quả bơ trong demo dựng theo centimet — nạp thô thì nó bé tí; model khác lại chiếm hết màn hình.} Don’t hand-tweak numbers. Measure the bounding box and fit it: {Đừng chỉnh số bằng tay. Đo bounding box rồi fit:}

const box = new THREE.Box3().setFromObject(model);
const size = box.getSize(new THREE.Vector3());
model.scale.setScalar(2.6 / Math.max(size.x, size.y, size.z)); // fit ~2.6u
box.setFromObject(model);                                       // re-measure
const c = box.getCenter(new THREE.Vector3());
model.position.sub(c).setY(model.position.y - box.min.y);       // center + ground

Materials look black or flat. {Material đen hoặc phẳng.} A MeshStandardMaterial is physically based — with no light and no environment it has nothing to reflect. {MeshStandardMaterialphysically based — không đèn, không environment thì chẳng có gì để phản chiếu.} Give the scene an image-based environment and metals/roughness suddenly read correctly: {Cho scene một environment dạng ảnh thì kim loại/độ nhám bỗng hiện đúng:}

import { RoomEnvironment } from 'three/addons/environments/RoomEnvironment.js';
const pmrem = new THREE.PMREMGenerator(renderer);
scene.environment = pmrem.fromScene(new RoomEnvironment(), 0.04).texture;

GLTFLoader already sets the correct color space on textures and wires base-color/normal/roughness/metalness/AO/emissive maps — so once lighting exists, you rarely touch the materials. {GLTFLoader đã đặt đúng color space cho texture và nối base-color/normal/roughness/metalness/AO/emissive — nên khi đã có ánh sáng, bạn hiếm khi phải động vào material.} Toggle the environment in the demo to see the difference. {Bật/tắt environment trong demo để thấy khác biệt.}

Animation comes along for free {Animation đi kèm sẵn}

If the file has animation (the Animated Box does), it arrives as gltf.animations. {Nếu file có animation (Animated Box có), nó đến dưới dạng gltf.animations.} Play it with an AnimationMixer — the same one we’ll go deep on in Part 17: {Phát nó bằng AnimationMixer — chính cái ta sẽ đào sâu ở Part 17:}

const mixer = new THREE.AnimationMixer(gltf.scene);
gltf.animations.forEach((clip) => mixer.clipAction(clip).play());
// in the loop:  mixer.update(clock.getDelta());

Swap models without leaking {Đổi model không rò rỉ}

Loaders allocate GPU geometry and textures. {Loader cấp phát geometry texture trên GPU.} When you replace a model, dispose both — textures are the part people forget: {Khi thay model, dispose cả hai — texture là phần người ta hay quên:}

old.traverse((o) => {
  if (!o.isMesh) return;
  o.geometry.dispose();
  for (const m of [].concat(o.material)) {
    for (const k in m) if (m[k]?.isTexture) m[k].dispose(); // every map
    m.dispose();
  }
});
scene.remove(old);

Master’s warnings {Lời cảnh báo của bậc thầy}

  • Don’t await a CDN and assume it’s there. Always wire the error callback — networks fail, and a silent black canvas is the worst UX. {Đừng tin CDN luôn sẵn sàng. Luôn nối callback lỗi — mạng có thể hỏng, và một canvas đen im lặng là UX tệ nhất.}
  • One mega-mesh ≠ fast. A model exported as a single 200k-tri mesh can’t be culled or instanced per-part. Keep logical objects separate in Blender. {Một mega-mesh không có nghĩa là nhanh. Model xuất thành một mesh 200k-tri không thể cull hay instance theo bộ phận. Hãy giữ các object logic tách biệt trong Blender.}
  • Triangulate intentionally. glTF stores triangles; let the exporter triangulate so you see in Blender exactly what ships. {Tam giác hoá có chủ đích. glTF lưu tam giác; để exporter tam giác hoá để bạn thấy trong Blender đúng cái sẽ ship.}
  • Watch the texture budget. A 5 MB .glb is usually 90% textures. Resize maps to what the screen needs before you blame the mesh. {Để ý ngân sách texture. Một .glb 5 MB thường 90% là texture. Hãy resize map về đúng nhu cầu màn hình trước khi đổ lỗi cho mesh.}

Practice {Thực hành}

  1. Export a cube from Blender as .glb, load it, and confirm scene.environment changes how it looks. {Xuất một cube từ Blender thành .glb, nạp vào, và xác nhận scene.environment đổi cách nó hiển thị.}
  2. Add a fifth model to the demo by URL and make the fit/center logic handle it without code changes. {Thêm model thứ năm vào demo bằng URL và để logic fit/center xử lý nó mà không sửa code.}
  3. Find a mesh by name (scene.getObjectByName) and toggle its visibility from a button. {Tìm mesh theo tên (scene.getObjectByName) và bật/tắt hiển thị từ một nút.}

What’s next {Tiếp theo}

That 3.7 MB helmet is fine on Wi-Fi and brutal on mobile data. {Cái mũ 3.7 MB ổn trên Wi-Fi nhưng tàn nhẫn với mạng di động.} In Part 15 we shrink it: Draco and Meshopt for geometry, KTX2 for textures, all with gltf-transform — often a 5–10× smaller file with no visible quality loss. {Ở Part 15 ta làm nó nhỏ lại: DracoMeshopt cho hình học, KTX2 cho texture, tất cả với gltf-transform — thường nhỏ hơn 5–10× mà không mất chất lượng thấy được.}