jvinhit//lab

Search posts

Type to search across journal entries.

navigate open esc close

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.

5 MIN READ Updated JUL 12, 2026

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.

Mở demo toàn màn hình

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.

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, onBeforeCompileEffectComposer.

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_Position hay gl_FragColor? Mỗi shader phải ghi output, nếu không chẳng có gì.
  • 1 thay vì 1.0? GLSL nghiêm về kiểu — literal float cần dấu thập phân.
  • Khai lại position/uv? Three.js đã chèn sẵn trong ShaderMaterial; khai lại là lỗi.
  • ShaderMaterial khô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

  1. Đọc gradient: trong demo, đặt displacement về 0 và xem gradient màu fragment thuần do vDisp điều khiển.
  2. Cố tình phá: trong code, đổi một 1.0 thành 1 và đọc lỗi biên dịch trong console.
  3. Hiệu ứng gió: thử cả onBeforeCompile và một positionNode TSL, 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.