Three.js from Zero to Senior · Part 5 — Textures & PBR Materials
Dress your surfaces: UV mapping, the PBR map set (albedo, normal, roughness, metalness, AO), color management done right, and environment maps that make metal reflect — with a live material you can build one map at a time.
A solid-colour MeshStandardMaterial only goes so far. {Một MeshStandardMaterial màu đặc chỉ đi được tới đó.} Real surfaces — brick, brushed steel, scuffed leather — carry detail at a scale far finer than your geometry’s triangles. {Bề mặt thật — gạch, thép chải, da trầy — mang chi tiết ở mức nhỏ hơn nhiều so với tam giác của geometry.} Textures paint that detail on cheaply, and PBR maps drive how each pixel reacts to light. {Texture vẽ chi tiết đó lên một cách rẻ, và PBR map điều khiển cách từng pixel phản ứng với ánh sáng.}
Build the material one map at a time in the demo — turn the base color, normal, and roughness maps on and off, and toggle the environment to see reflections appear. {Dựng material từng map một trong demo — bật/tắt base color, normal, roughness, và toggle environment để thấy phản chiếu xuất hiện.}
Open the full demo {Mở demo đầy đủ}: /tools/threejs-pbr-textures-demo/.
Loading a texture {Nạp một texture}
A texture is an image wrapped onto geometry. {Texture là một ảnh bọc lên geometry.} Load it with TextureLoader (async — the image arrives over the network): {Nạp bằng TextureLoader (bất đồng bộ — ảnh tới qua mạng):}
const loader = new THREE.TextureLoader();
const albedo = loader.load('/textures/brick_color.jpg');
// CRITICAL: a color texture must be tagged as sRGB, or it looks washed out.
albedo.colorSpace = THREE.SRGBColorSpace;
const material = new THREE.MeshStandardMaterial({ map: albedo });
Tiling and offset live on the texture, not the material: {Lặp và lệch nằm trên texture, không phải material:}
albedo.wrapS = albedo.wrapT = THREE.RepeatWrapping;
albedo.repeat.set(4, 4); // tile 4×4 across the UVs
UV mapping — how a flat image wraps a 3D shape {Ánh xạ UV — ảnh phẳng bọc hình 3D thế nào}
Every vertex carries a 2D coordinate (u, v) in the range 0–1 that says “this point of the mesh maps to this point of the texture.” {Mỗi đỉnh mang một toạ độ 2D (u, v) trong khoảng 0–1, nói “điểm này của mesh ứng với điểm này của texture.”} Built-in geometries come with sensible UVs; imported models carry UVs authored in Blender or Maya. {Geometry dựng sẵn đã có UV hợp lý; model import mang UV được tạo trong Blender hay Maya.} When a texture looks stretched or seamed, the UVs are the suspect — not the texture. {Khi texture trông bị kéo hoặc lộ đường nối, hãy nghi UV — không phải texture.}
The PBR map set {Bộ map PBR}
Physically-based rendering describes a surface with a set of textures, each driving one property. {Render dựa trên vật lý mô tả bề mặt bằng một bộ texture, mỗi cái điều khiển một thuộc tính.} You rarely use all of them, but you should know each. {Hiếm khi dùng hết, nhưng nên biết từng cái.}
map(albedo / base color) — the raw colour, no lighting baked in. Tag itSRGBColorSpace. {map(albedo / màu nền) — màu thô, không nướng sẵn ánh sáng. GắnSRGBColorSpace.}normalMap— fakes bumps and grooves by perturbing the surface normal per pixel, so light reacts as if there were geometry that isn’t there. The cheapest way to add detail. Linear color space (it’s data, not colour). {normalMap— giả gồ ghề bằng cách bóp méo normal từng pixel, để ánh sáng phản ứng như có hình mà thực ra không có. Cách rẻ nhất để thêm chi tiết. Không gian màu Linear (nó là dữ liệu, không phải màu).}roughnessMap— grayscale; white = matte, black = glossy. Lets one surface mix polished and worn areas. {roughnessMap— thang xám; trắng = matte, đen = bóng. Cho một bề mặt trộn vùng bóng và vùng mòn.}metalnessMap— grayscale; white = metal, black = non-metal. {metalnessMap— thang xám; trắng = kim loại, đen = phi kim.}aoMap(ambient occlusion) — darkens crevices that ambient light can’t reach. Needs a second set of UVs (uv2). {aoMap(che khuất môi trường) — làm tối các khe mà ánh sáng môi trường không tới. Cần bộ UV thứ hai (uv2).}displacementMap— actually moves vertices (needs dense geometry); contrast with the fake normal map. {displacementMap— thật sự dời đỉnh (cần geometry dày); trái với normal map giả.}
const mat = new THREE.MeshStandardMaterial({
map: albedo, // sRGB
normalMap: normal, // linear
roughnessMap: rough, // linear
metalnessMap: metal, // linear
metalness: 1, // multiplied with the map
roughness: 1,
});
Note metalness/roughness values are multiplied with their maps. {Lưu ý giá trị metalness/roughness được nhân với map của chúng.} If you supply a roughnessMap, leave roughness: 1 so the map shows through. {Nếu bạn cấp roughnessMap, để roughness: 1 để map hiện ra.}
Color management — the bug that makes everything look “off” {Quản lý màu — lỗi khiến mọi thứ trông “sai sai”}
Modern Three.js renders in linear space and outputs sRGB. {Three.js hiện đại render trong không gian linear và xuất ra sRGB.} Two rules keep colours correct: {Hai quy tắc giữ màu đúng:}
// 1. Color textures (albedo, env) → sRGB. Data textures (normal, rough, metal) → linear (default).
albedo.colorSpace = THREE.SRGBColorSpace;
// 2. Add tone mapping for a filmic, non-clipped look.
renderer.toneMapping = THREE.ACESFilmicToneMapping;
Skip rule 1 and your textures look pale and washed out; tag a normal map as sRGB by mistake and your lighting goes subtly wrong. {Bỏ quy tắc 1 thì texture trông nhợt nhạt; lỡ gắn một normal map là sRGB thì ánh sáng sai một cách tinh vi.} This single concept separates renders that look “right” from ones that look “amateur.” {Riêng khái niệm này phân biệt render trông “đúng” với render trông “nghiệp dư.”}
Environment maps — reflections and free lighting {Environment map — phản chiếu và ánh sáng miễn phí}
A metal sphere with no environment is just a dark ball — there’s nothing for it to reflect. {Một quả cầu kim loại không có environment chỉ là quả bóng tối — chẳng có gì để phản chiếu.} An environment map is a 360° image of the surroundings; PBR materials sample it for reflections and image-based lighting. {Environment map là ảnh 360° của khung cảnh; material PBR lấy mẫu từ nó để phản chiếu và chiếu sáng dựa trên ảnh.} Toggle it in the demo — the mirror finish appears with no extra light. {Toggle nó trong demo — lớp bóng như gương xuất hiện mà không cần thêm đèn.}
The clean way, with no HDR file to ship, is RoomEnvironment + PMREMGenerator (what the demo uses): {Cách gọn, không cần ship file HDR, là RoomEnvironment + PMREMGenerator (cái demo dùng):}
import { RoomEnvironment } from 'three/addons/environments/RoomEnvironment.js';
const pmrem = new THREE.PMREMGenerator(renderer);
const envMap = pmrem.fromScene(new RoomEnvironment(), 0.04).texture;
material.envMap = envMap; // per-material
// or light the WHOLE scene at once:
scene.environment = envMap;
Setting scene.environment lights every PBR material in the scene — the single biggest “instant realism” upgrade you can make. {Đặt scene.environment chiếu sáng mọi material PBR trong scene — nâng cấp “chân thực tức thì” lớn nhất bạn có thể làm.}
The master’s warnings {Lời cảnh báo của sư phụ}
- Textures washed out? You forgot
colorSpace = SRGBColorSpaceon color maps. {Texture nhợt nhạt? Bạn quêncolorSpace = SRGBColorSpacecho color map.} roughnessMapignored? Leaveroughness: 1so the map isn’t multiplied down to nothing. {roughnessMapbị lờ? Đểroughness: 1để map không bị nhân về 0.}- Metal looks black? It has nothing to reflect — set
scene.environment. {Kim loại đen? Không có gì để phản chiếu — đặtscene.environment.} aoMapdoes nothing? It needs a second UV set:geometry.setAttribute('uv2', …). {aoMapkhông ăn? Cần bộ UV thứ hai.}- Texture stretched? Blame the UVs, not the image. {Texture bị kéo? Đổ lỗi cho UV, không phải ảnh.}
Practice, or it didn’t happen {Luyện tập, không thì coi như chưa học}
- One map at a time {Từng map một}: in the demo, start with only base color, then add normal, then roughness — name what each one changed. {trong demo, bắt đầu chỉ base color, rồi thêm normal, rồi roughness — gọi tên thứ mỗi cái đổi.}
- Make a mirror {Làm cái gương}: metalness 1, roughness 0, environment on. Then crank roughness to 0.5 — watch the reflection blur. {metalness 1, roughness 0, bật environment. Rồi tăng roughness lên 0.5 — xem phản chiếu nhoè.}
- Tile it {Lặp nó}: raise the tiling slider and watch the checker repeat — that’s
repeat.set()on the texture. {tăng slider tiling và xem ô cờ lặp — đó làrepeat.set()trên texture.}
What’s next {Phần tiếp theo}
You can now author believable surfaces from scratch. {Giờ bạn tạo được bề mặt đáng tin từ đầu.} But nobody models a character vertex-by-vertex in code — real assets come from Blender as glTF files, often with baked-in animation. {Nhưng không ai dựng nhân vật từng đỉnh bằng code — asset thật đến từ Blender dưới dạng file glTF, thường kèm animation nướng sẵn.} In Part 6 we load glTF models (with Draco compression), then play their clips through the AnimationMixer — and you’ll drive a live mixer that crossfades between animations. {Ở Phần 6 ta nạp model glTF (kèm nén Draco), rồi phát clip của chúng qua AnimationMixer — và bạn sẽ điều khiển một mixer trực tiếp crossfade giữa các animation.}