jvinhit//lab

Search posts

Type to search across journal entries.

navigate open esc close

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_Position or gl_FragColor? Each shader must write its output, or you get nothing. {Quên gl_Position hay gl_FragColor? Mỗi shader phải ghi output, nếu không chẳng có gì.}
  • 1 instead of 1.0? GLSL is strict about types — float literals need the decimal. {1 thay vì 1.0? GLSL nghiêm về kiểu — literal float cần dấu thập phân.}
  • Redeclared position/uv? Three.js already injects them in a ShaderMaterial; declaring them again is an error. {Khai lại position/uv? Three.js đã chèn sẵn trong ShaderMaterial; khai lại là lỗi.}
  • No lighting on your ShaderMaterial? It has none by default — you compute it yourself, or use onBeforeCompile to keep the built-in lighting. {ShaderMaterial không có ánh sáng? Mặc định không có — bạn tự tính, hoặc dùng onBeforeCompile.}

Practice, or it didn’t happen {Luyện tập, không thì coi như chưa học}

  1. 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 do vDisp điều khiển.}
  2. Break it on purpose {Cố tình phá}: in code, change a 1.0 to 1 and read the compile error in the console. {trong code, đổi một 1.0 thành 1 và đọc lỗi biên dịch trong console.}
  3. Wind effect {Hiệu ứng gió}: use onBeforeCompile on a MeshStandardMaterial to sway a tall box and keep its shadows. {dùng onBeforeCompile trên MeshStandardMaterial để 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.}