Three.js from Zero to Senior · Bonus 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.
The ten core parts gave you everything to build real scenes with the materials Three.js ships. {Mười phần cốt lõi đã cho bạn mọi thứ để dựng scene thật với các material Three.js có sẵn.} This bonus arc goes to the senior frontier — and it starts at the deepest layer of all: writing the GPU program yourself, in GLSL. {Loạt bonus này tới biên giới senior — và bắt đầu ở lớp sâu nhất: tự viết chương trình GPU, bằng GLSL.}
Drive the shader live — push the displacement and frequency and watch the vertex shader deform the mesh in real time. {Đ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.}
Open the full demo {Mở demo đầy đủ}: /tools/threejs-shaders-demo/.
What a material is, underneath {Material thực chất là gì, bên dưới}
Every material you’ve used — MeshStandardMaterial, MeshBasicMaterial — is just a generator for two small programs that run on the GPU: a vertex shader and a fragment shader. {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.} When you write a ShaderMaterial, you skip the generator and provide those two programs directly. {Khi viết một ShaderMaterial, bạn bỏ qua bộ sinh và cấp thẳng hai chương trình đó.}
GLSL (OpenGL Shading Language) is a small C-like language with built-in vector types (vec2, vec3, vec4, mat4) and math (sin, mix, dot, normalize). {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).}
The two shaders, and what each one’s job is {Hai shader, và việc của từng cái}
The GPU runs them in a pipeline, massively in parallel: {GPU chạy chúng trong một pipeline, song song quy mô lớn:}
- Vertex shader — runs once per vertex. Its job is to compute the final clip-space position (
gl_Position). This is where you move geometry — wobble, wave, displacement. {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 — runs once per pixel the triangle covers. Its job is to output a color (
gl_FragColor). This is where you do coloring — gradients, lighting math, patterns. {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.}
There are far more fragments than vertices, which is why expensive math belongs in the vertex shader when you can move it there. {Có nhiều fragment hơn vertex rất nhiều, nên toán đắt nên đặt ở vertex shader khi có thể.}
The three data types you pass to shaders {Ba loại dữ liệu bạn truyền vào shader}
This is the vocabulary that confuses everyone at first: {Đây là từ vựng làm ai cũng rối lúc đầu:}
attribute— per-vertex data from the geometry:position,normal,uv. Three.js wires the standard ones in for you. Read-only, vertex shader only. {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— a value that’s the same for every vertex and pixel this draw call, set from JS: time, color, a texture, mouse position. This is your control panel. {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— a value the vertex shader writes and the fragment shader reads, interpolated across the triangle in between. How you pass per-vertex results down to per-pixel coloring. {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.}
A minimal ShaderMaterial {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
}`,
});
Note what’s already provided: projectionMatrix, modelViewMatrix, position, normal, and uv. {Lưu ý những gì đã được cấp sẵn: projectionMatrix, modelViewMatrix, position, normal, và uv.} Three.js injects these for you in a ShaderMaterial; you don’t declare them. {Three.js tự chèn chúng trong một ShaderMaterial; bạn không khai báo.}
Animating from JS — one number, thousands of cores {Animate từ JS — một con số, hàng nghìn nhân}
A shader can’t read Date.now() — it has no clock. {Một shader không đọc được Date.now() — nó không có đồng hồ.} You feed it time through a uniform, updated every frame: {Bạn cấp thời gian qua một uniform, cập nhật mỗi frame:}
function animate() {
requestAnimationFrame(animate);
material.uniforms.uTime.value = clock.getElapsedTime(); // ← the whole animation hook
renderer.render(scene, camera);
}
That single assignment drives a program executing on thousands of GPU cores simultaneously. {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.} The demo’s uAmp, uFreq, and uTime are all just uniforms updated from slider inputs. {uAmp, uFreq, uTime của demo đều chỉ là uniform cập nhật từ slider.}
The senior shortcut: extend a built-in, don’t rewrite it {Lối tắt senior: mở rộng built-in, đừng viết lại}
Writing a full PBR lighting model in raw GLSL is a huge job. {Viết cả mô hình chiếu sáng PBR bằng GLSL thô là việc khổng lồ.} Most of the time you want MeshStandardMaterial’s lighting plus one custom tweak — and onBeforeCompile lets you inject GLSL into the built-in shader instead of replacing it: {Phần lớn thời gian bạn muốn ánh sáng của MeshStandardMaterial cộng một chỉnh tuỳ biến — và onBeforeCompile cho bạn 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
};
This gives you free lighting, shadows, and environment maps while still bending the geometry your way — exactly how production effects (wind on foliage, water) are usually built. {Cách này cho bạn ánh sáng, bóng, environment map miễn phí mà vẫn uốn hình theo ý — đúng cách hiệu ứng production (gió trên cây, nước) thường được dựng.}
The TSL note {Ghi chú về TSL}
Modern Three.js (with WebGPU) introduces TSL (Three.js Shading Language) — shaders written in JavaScript-like node syntax that compile to both WebGL (GLSL) and WebGPU (WGSL). {Three.js hiện đại (với WebGPU) giới thiệu TSL — shader viết bằng cú pháp node giống JavaScript, biên dịch ra cả WebGL (GLSL) và WebGPU (WGSL).} GLSL is still the foundation and what every tutorial and Shadertoy uses, so learn it first — but know TSL is where the ecosystem is heading. {GLSL vẫn là nền tảng và là thứ mọi tutorial và Shadertoy dùng, nên học nó trước — nhưng biết TSL là hướng hệ sinh thái đang đi.}
The master’s warnings {Lời cảnh báo của sư phụ}
- Black mesh, no errors in JS? GLSL compile errors print to the console — read them; the line numbers are exact. {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.}
- Forgot
gl_Positionorgl_FragColor? Each shader must write its output, or you get nothing. {Quêngl_Positionhaygl_FragColor? Mỗi shader phải ghi output, nếu không chẳng có gì.} 1instead of1.0? GLSL is strict about types —floatliterals need the decimal. {1thay vì1.0? GLSL nghiêm về kiểu — literalfloatcần dấu thập phân.}- Redeclared
position/uv? Three.js already injects them in aShaderMaterial; declaring them again is an error. {Khai lạiposition/uv? Three.js đã chèn sẵn trongShaderMaterial; khai lại là lỗi.} - No lighting on your
ShaderMaterial? It has none by default — you compute it yourself, or useonBeforeCompileto keep the built-in lighting. {ShaderMaterialkhông có ánh sáng? Mặc định không có — bạn tự tính, hoặc dùngonBeforeCompile.}
Practice, or it didn’t happen {Luyện tập, không thì coi như chưa học}
- Read the gradient {Đọc gradient}: in the demo, set displacement to 0 and watch the pure fragment-color gradient driven by
vDisp. {trong demo, đặt displacement về 0 và xem gradient màu fragment thuần dovDispđiều khiển.} - Break it on purpose {Cố tình phá}: in code, change a
1.0to1and read the compile error in the console. {trong code, đổi một1.0thành1và đọc lỗi biên dịch trong console.} - Wind effect {Hiệu ứng gió}: use
onBeforeCompileon aMeshStandardMaterialto sway a tall box and keep its shadows. {dùngonBeforeCompiletrênMeshStandardMaterialđể lay một khối cao mà vẫn giữ bóng.}
What’s next {Phần tiếp theo}
Shaders control how things look; the next frontier controls how things move on their own. {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.} In Bonus Part 12 we add physics: pairing Three.js with a rigid-body engine (cannon-es), the render-world / physics-world split, colliders, and the fixed-timestep loop — with a sandbox where you drop boxes and spheres and watch them stack. {Ở Bonus 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.}