Three.js from Zero to Senior · Part 11 — Custom GLSL Shaders
Drop below the built-in materials: the GPU pipeline, vertex vs fragment shaders, attributes/uniforms/varyings, writing a ShaderMaterial, animating it from JS, and extending built-ins with onBeforeCompile — with a live shader playground.
Mười phần đầu đã cho bạn một checkpoint production với material có sẵn. Giờ ta đi xuống lớp GPU: tự viết chương trình GLSL, đồng thời ghi rõ đâu là đường WebGL-only và đâu cần TSL cho kiến trúc đa backend.
Điều khiển shader trực tiếp — đẩy displacement và frequency, xem vertex shader bóp méo mesh theo thời gian thực.
Material thực chất là gì, bên dưới
Mọi material bạn đã dùng — MeshStandardMaterial, MeshBasicMaterial — chỉ là bộ sinh ra hai chương trình nhỏ chạy trên GPU: một vertex shader và một fragment shader. Khi viết một ShaderMaterial, bạn bỏ qua bộ sinh và cấp thẳng hai chương trình đó.
GLSL là một ngôn ngữ nhỏ giống C với kiểu vector dựng sẵn (vec2, vec3, vec4, mat4) và toán (sin, mix, dot, normalize).
Hai shader, và việc của từng cái
GPU chạy chúng trong một pipeline, song song quy mô lớn:
- Vertex shader — chạy một lần mỗi đỉnh. Việc của nó là tính vị trí clip-space cuối (
gl_Position). Đây là nơi bạn di chuyển hình học — lượn, sóng, displacement. - Fragment shader — chạy một lần mỗi pixel tam giác phủ. Việc của nó là xuất một màu (
gl_FragColor). Đây là nơi bạn tô màu — gradient, toán ánh sáng, hoạ tiết.
Có nhiều fragment hơn vertex rất nhiều, nên toán đắt nên đặt ở vertex shader khi có thể.
Ba loại dữ liệu bạn truyền vào shader
Đây là từ vựng làm ai cũng rối lúc đầu:
attribute— dữ liệu mỗi đỉnh từ geometry:position,normal,uv. Three.js nối sẵn các cái chuẩn. Chỉ đọc, chỉ vertex shader.uniform— một giá trị giống nhau cho mọi đỉnh và pixel trong draw call này, đặt từ JS: thời gian, màu, texture, vị trí chuột. Đây là bảng điều khiển của bạn.varying— giá trị vertex shader ghi và fragment shader đọc, được nội suy qua tam giác ở giữa. Cách truyền kết quả mỗi đỉnh xuống tô màu mỗi pixel.
Một ShaderMaterial tối giản
const uniforms = {
uTime: { value: 0 },
uColor: { value: new THREE.Color(0xc8ff00) },
};
const material = new THREE.ShaderMaterial({
uniforms,
vertexShader: /* glsl */`
uniform float uTime;
varying vec2 vUv;
void main() {
vUv = uv; // pass uv down to the fragment shader
vec3 p = position;
p.z += sin(p.x * 4.0 + uTime) * 0.2; // ripple along x over time
gl_Position = projectionMatrix * modelViewMatrix * vec4(p, 1.0);
}`,
fragmentShader: /* glsl */`
uniform vec3 uColor;
varying vec2 vUv;
void main() {
gl_FragColor = vec4(uColor * vUv.y, 1.0); // fade the color along v
}`,
});
Lưu ý những gì đã được cấp sẵn: projectionMatrix, modelViewMatrix, position, normal, và uv. Three.js tự chèn chúng trong một ShaderMaterial; bạn không khai báo.
Animate từ JS — một con số, hàng nghìn nhân
Một shader không đọc được Date.now() — nó không có đồng hồ. Bạn cấp thời gian qua một uniform, cập nhật mỗi frame:
const timer = new THREE.Timer();
timer.connect(document);
function animate(timestamp) {
timer.update(timestamp);
material.uniforms.uTime.value = timer.getElapsed(); // ← the whole animation hook
renderer.render(scene, camera);
}
renderer.setAnimationLoop(animate);
Phép gán duy nhất đó điều khiển một chương trình chạy đồng thời trên hàng nghìn nhân GPU. uAmp, uFreq, uTime của demo đều chỉ là uniform cập nhật từ slider.
Seam WebGL-only: onBeforeCompile
Viết cả mô hình chiếu sáng PBR bằng GLSL thô là việc khổng lồ. Với WebGLRenderer, onBeforeCompile cho phép chèn GLSL vào shader dựng sẵn thay vì thay thế nó:
const mat = new THREE.MeshStandardMaterial({ color: 0x88aaff });
mat.onBeforeCompile = (shader) => {
shader.uniforms.uTime = { value: 0 };
shader.vertexShader = 'uniform float uTime;\n' + shader.vertexShader.replace(
'#include <begin_vertex>',
'vec3 transformed = position; transformed.y += sin(position.x*3.0 + uTime)*0.2;'
);
mat.userData.shader = shader; // keep a handle to update uTime each frame
};
Cách này giữ ánh sáng, bóng và environment map, nhưng đổi lại bạn phụ thuộc vào shader chunk nội bộ. Nó không được WebGPURenderer hỗ trợ và patch có thể vỡ khi nâng Three.js. Hãy coi đây là seam kế thừa có test visual, không phải default architecture mới.
Ghi chú về TSL
Three.js hiện đại dùng TSL — shader graph viết trong JavaScript/TypeScript, sinh GLSL hoặc WGSL theo backend. GLSL vẫn quan trọng để hiểu pipeline và debug generated shader; còn material mới cần chạy trên WebGPURenderer nên ưu tiên Node Material/TSL. Part 25 có migration matrix đầy đủ cho ShaderMaterial, onBeforeCompile và EffectComposer.
Lời cảnh báo của sư phụ
- Mesh đen, JS không lỗi? Lỗi biên dịch GLSL in ra console — đọc đi; số dòng chính xác.
- Quên
gl_Positionhaygl_FragColor? Mỗi shader phải ghi output, nếu không chẳng có gì. 1thay vì1.0? GLSL nghiêm về kiểu — literalfloatcần dấu thập phân.- Khai lại
position/uv? Three.js đã chèn sẵn trongShaderMaterial; khai lại là lỗi. ShaderMaterialkhông có ánh sáng? Mặc định không có — bạn tự tính; với code mới đa backend, cân nhắc Node Material/TSL thay vì patch built-in.
Luyện tập, không thì coi như chưa học
- Đọc gradient: trong demo, đặt displacement về 0 và xem gradient màu fragment thuần do
vDispđiều khiển. - Cố tình phá: trong code, đổi một
1.0thành1và đọc lỗi biên dịch trong console. - Hiệu ứng gió: thử cả
onBeforeCompilevà mộtpositionNodeTSL, rồi ghi rõ test nào bảo vệ mỗi implementation khi nâng renderer.
Phần tiếp theo
Shader điều khiển vật trông thế nào; biên giới tiếp theo điều khiển vật tự di chuyển ra sao. Ở Phần 12 ta thêm vật lý: ghép Three.js với một engine rigid-body (cannon-es), tách render-world / physics-world, collider, và vòng lặp fixed-timestep — với một sandbox cho bạn thả khối và cầu rồi xem chúng chồng lên nhau.