Giới thiệu về Shaders
Đã đến lúc khám phá thế giới của shaders. Chúng là phần không thể thiếu để tạo ra mọi loại hiệu ứng hình ảnh. Trong chương này, chúng ta sẽ tìm hiểu về shaders, những gì có thể đạt được với chúng và cách sử dụng chúng trong React Three Fiber.
Khởi đầu
Trước khi bắt đầu, tôi muốn nhấn mạnh rằng shaders có thể cần thời gian để bạn làm quen. Chúng hoạt động khác biệt so với phần còn lại của mã mà chúng ta đã viết cho đến nay. Đây là một cách suy nghĩ và tạo hiệu ứng hình ảnh mới. Nhưng đừng lo, tôi sẽ hướng dẫn bạn qua quá trình này, chúng ta sẽ bắt đầu với những điều cơ bản và dần dần tiến đến các chủ đề nâng cao hơn.
Đừng cảm thấy nản lòng nếu lúc đầu bạn không hiểu hết. Điều đó là bình thường. Đây là một khái niệm mới và cần luyện tập để trở nên thoải mái hơn với nó. Tôi khuyên bạn nên dành thời gian, thử nghiệm, và thực hành, nhưng tôi hứa với bạn, đó là xứng đáng! Shaders rất mạnh mẽ và sẽ cho bạn quyền kiểm soát để tạo ra bất kỳ hiệu ứng hình ảnh nào bạn có thể tưởng tượng.
Thêm vào đó, mỗi người có cách học khác nhau. Một số người học tốt hơn khi đọc, số khác khi xem video, hoặc khi thực hành. Đặc biệt với chủ đề này, tham khảo chéo các nguồn khác nhau có thể rất hữu ích. Tôi sẽ chia sẻ với bạn các tài nguyên vào cuối chương này để củng cố kiến thức của bạn và vượt xa những gì chúng tôi đề cập ở đây.
Hy vọng tôi không làm bạn sợ hãi và bạn đang háo hức muốn học về shaders. Hãy bắt đầu nào!
Shaders là gì?
Shaders là các chương trình nhỏ chạy trên GPU (Graphics Processing Unit). Chúng được viết bằng một ngôn ngữ gọi là GLSL (OpenGL Shading Language), tương tự như C.
Chúng được sử dụng để định vị các đỉnh của một mesh (Vertex Shader) và để tô màu từng pixel của các mặt (Fragment Shader).
Thực ra, chúng ta đã sử dụng shaders ngay từ đầu. Khi tạo một material, chúng ta đang sử dụng một shader. Ví dụ, khi tạo MeshBasicMaterial
, chúng ta đang sử dụng một shader để tô màu mesh với một màu duy nhất. Khi tạo MeshStandardMaterial
, chúng ta đang sử dụng một shader mô phỏng ánh sáng, bóng và phản chiếu.
Vertex Shader
Vertex shader là một chương trình được thực thi cho mỗi đỉnh của một hình học. Trách nhiệm chính của nó là chuyển đổi các đỉnh từ không gian 3D (thế giới 3D của chúng ta) sang không gian 2D (màn hình hoặc viewport của chúng ta). Nó đạt được sự chuyển đổi này bằng cách sử dụng nhiều ma trận:
- View Matrix: Ma trận này đại diện cho vị trí và hướng của camera trong cảnh. Nó chuyển đổi các đỉnh từ không gian thế giới sang không gian camera.
- Projection Matrix: Ma trận này, có thể là perspective hoặc orthographic, chuyển đổi các đỉnh từ không gian camera sang tọa độ thiết bị chuẩn hóa (NDC), chuẩn bị cho việc chiếu cuối cùng lên màn hình 2D.
- Model Matrix: Ma trận này bao gồm vị trí, xoay và tỉ lệ của từng đối tượng riêng lẻ trong cảnh. Nó chuyển đổi các đỉnh từ không gian đối tượng sang không gian thế giới.
Ngoài ra, vertex shader cũng tích hợp vị trí gốc của đỉnh và bất kỳ attributes nào khác liên quan đến nó.
Với mỗi đỉnh của hình học, vertex shader sẽ được thực thi.
Cuối cùng, vị trí đã chuyển đổi của đỉnh trong không gian 2D được trả về thông qua biến gl_Position
được định nghĩa trước. Sau khi tất cả các đỉnh được chuyển đổi, GPU nội suy các giá trị giữa chúng để tạo ra các mặt của hình học, sau đó sẽ được chuyển hóa và hiển thị lên màn hình.
Fragment Shader
Fragment shader, còn gọi là pixel shader, là một chương trình được thực thi cho mỗi fragment (hoặc pixel) được tạo ra bởi quá trình rasterization. Nhiệm vụ chính của nó là xác định màu sắc cuối cùng của mỗi pixel trên màn hình.
Đối với mỗi fragment được tạo ra trong quá trình rasterization, fragment shader sẽ được thực thi.
Fragment shader nhận các giá trị nội suy từ vertex shader, như màu sắc, tọa độ texture, normal, và bất kỳ thuộc tính nào khác liên quan đến các đỉnh của hình học. Những giá trị nội suy này được gọi là varyings, cung cấp thông tin về thuộc tính bề mặt tại mỗi vị trí fragment.
Ngoài các giá trị nội suy, fragment shader cũng có thể lấy mẫu texture và truy cập các biến uniform, những biến mà không đổi qua tất cả các fragments. Các biến uniform này có thể đại diện cho các tham số như vị trí ánh sáng, thuộc tính material, hay bất kỳ dữ liệu nào khác cần cho các tính toán shading.
Chúng ta sẽ quay lại với attributes và uniforms ở phần sau của bài học này.
Sử dụng các dữ liệu đầu vào, fragment shader thực hiện các tính toán khác nhau để xác định màu cuối cùng của fragment. Điều này có thể bao gồm các tính toán ánh sáng phức tạp, ánh xạ texture, hiệu ứng shading, hoặc bất kỳ hiệu ứng thị giác nào được mong muốn trong cảnh.
Khi việc tính toán màu hoàn tất, fragment shader đưa ra màu cuối cùng của fragment sử dụng biến gl_FragColor
được định nghĩa trước.
Tôi đã cố gắng giải thích shader đơn giản nhất có thể và có bỏ qua một số chi tiết kỹ thuật, nhưng tôi hiểu rằng nó vẫn có thể hơi trừu tượng. Hãy tạo một shader đơn giản để xem nó hoạt động như thế nào trên thực tế.
Shader Đầu Tiên Của Bạn
Hãy chạy gói khởi động. Bạn sẽ thấy khung này với một mặt phẳng màu đen ở giữa màn hình:
Mở tệp ShaderPlane.jsx
, nó chứa một mesh đơn giản với hình học mặt phẳng và một material cơ bản. Chúng ta sẽ thay thế material này bằng một custom shader material.
shaderMaterial
Để tạo một shader material, chúng ta sử dụng hàm shaderMaterial
từ Drei library.
Hàm này nhận 3 tham số:
uniforms
: Một đối tượng chứa các biến uniform được sử dụng trong shader. Giữ nó trống trong lúc này.vertexShader
: Một chuỗi chứa mã GLSL cho vertex shader.fragmentShader
: Một chuỗi chứa mã GLSL cho fragment shader.
Trên đầu tệp của chúng ta, hãy khai báo một shader material mới có tên MyShaderMaterial
:
import { shaderMaterial } from "@react-three/drei"; const MyShaderMaterial = shaderMaterial( {}, ` void main() { gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }`, ` void main() { gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0); } ` );
Chúng ta sẽ đi sâu vào mã shader ngay sau đây.
Để có thể sử dụng nó một cách khai báo với React Three Fiber, chúng ta sử dụng phương thức extend
:
import { extend } from "@react-three/fiber"; // ... extend({ MyShaderMaterial });
Bây giờ chúng ta có thể thay thế <meshBasicMaterial>
bằng shader material mới của chúng ta:
import { shaderMaterial } from "@react-three/drei"; import { extend } from "@react-three/fiber"; const MyShaderMaterial = shaderMaterial( {}, ` void main() { gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }`, ` void main() { gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0); } ` ); extend({ MyShaderMaterial }); export const ShaderPlane = ({ ...props }) => { return ( <mesh {...props}> <planeGeometry args={[1, 1]} /> <myShaderMaterial /> </mesh> ); };
Bạn sẽ thấy cùng một mặt phẳng màu đen như trước. Chúng ta chưa thay đổi gì, nhưng giờ chúng ta đang sử dụng một custom shader material.
Để kiểm tra xem nó có hoạt động không, hãy thay đổi màu sắc mà chúng ta đang trả về trong fragment shader. Thay dòng gl_FragColor
bằng dòng sau:
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
gl_FragColor
là một biến được định nghĩa trước đại diện cho màu của fragment. Nó là một vec4
(một vector với 4 thành phần) đại diện cho các kênh red, green, blue, và alpha của màu. Mỗi thành phần là một float giữa 0 và 1.
Bằng cách đặt thành phần đầu tiên thành 1.0
, chúng ta đang đặt kênh red đến giá trị tối đa của nó, sẽ hiển thị màu đỏ.
Bạn sẽ thấy một mặt phẳng màu đỏ ở giữa màn hình:
Chúc mừng! Bạn vừa tạo ra shader material đầu tiên của mình. Nó khá đơn giản, nhưng đây là bước đầu tiên.
Mã Shader
Trước khi chúng ta tiếp tục, hãy thiết lập môi trường phát triển để viết shader thoải mái hơn.
Bạn có hai lựa chọn để viết mã shader:
- Inline: Bạn có thể viết mã shader trực tiếp trong tập tin JavaScript.
- External: Bạn có thể viết mã shader trong một tập tin riêng có phần mở rộng
.glsl
và import nó vào tập tin JavaScript của bạn.
Tôi thường ưa chuộng cách tiếp cận inline trong các tập tin material phù hợp, bằng cách đó mã shader gần với khai báo material.
Nhưng để dễ viết và đọc hơn, tôi khuyên bạn nên sử dụng công cụ tô sáng cú pháp cho GLSL. Bạn có thể sử dụng extension Comment tagget templates cho Visual Studio Code. Nó sẽ làm nổi bật mã GLSL bên trong các chuỗi mẫu.
Sau khi cài đặt, để kích hoạt công cụ tô sáng cú pháp, bạn cần thêm comment sau vào đầu mã shader:
const MyShaderMaterial = shaderMaterial( {}, /* glsl */ ` void main() { gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }`, /* glsl */ ` void main() { gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); } ` );
Bạn sẽ thấy mã GLSL được tô sáng trong template strings:
Shader vertex phía trên hiện đã có tô sáng cú pháp đúng và dễ đọc hơn.
Đó là tất cả những gì bạn cần cho mã shader inline. Bạn vẫn có thể quyết định sử dụng tập tin external nếu bạn muốn giữ mã shader tách biệt. Hãy xem cách thực hiện điều đó.
Nhập các tệp GLSL
Đầu tiên, tạo một thư mục mới tên là shaders
trong thư mục src
. Trong thư mục này, tạo hai tệp: myshader.vertex.glsl
và myshader.fragment.glsl
và sao chép mã shader tương ứng vào từng tệp.
myshader.vertex.glsl
:
void main() { gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }
myshader.fragment.glsl
:
void main() { gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); }
Bạn có thể thoải mái sử dụng quy tắc đặt tên mà bạn thích, và nhóm các shader trong các thư mục con nếu bạn có nhiều shader.
Sau đó, để có thể nhập các tệp này vào tệp JavaScript của chúng ta, chúng ta cần cài đặt plugin vite-plugin-glsl như một phụ thuộc phát triển:
yarn add vite-plugin-glsl --dev
Sau đó, trong tệp vite.config.js
của bạn, nhập plugin và thêm nó vào mảng plugins
:
import react from "@vitejs/plugin-react"; import { defineConfig } from "vite"; import glsl from "vite-plugin-glsl"; // https://vitejs.dev/config/ export default defineConfig({ plugins: [react(), glsl()], });
Bây giờ bạn có thể nhập các tệp GLSL trong tệp JavaScript của mình và sử dụng chúng như mã shader:
import myShaderFragment from "./shaders/myshader.fragment.glsl"; import myShaderVertex from "./shaders/myshader.vertex.glsl"; const MyShaderMaterial = shaderMaterial({}, myShaderVertex, myShaderFragment);
Bây giờ chúng ta có một cách tiện lợi để viết và nhập mã shader, chúng ta có thể bắt đầu khám phá các phần khác nhau của mã shader.
GLSL
Mã shader được viết bằng GLSL (Ngôn ngữ Đổ bóng OpenGL). Nó là một ngôn ngữ giống C, hãy cùng xem các kiến thức cơ bản nhất.
Kiểu dữ liệu
GLSL có một số kiểu dữ liệu, nhưng phổ biến nhất là:
- bool: Một giá trị boolean (
true
hoặcfalse
). - int: Một số nguyên.
- float: Một số dấu phẩy động.
- vectors: Một tập hợp các số.
vec2
là một tập hợp 2 số dấu phẩy động (x
vày
),vec3
là một tập hợp 3 số dấu phẩy động (x
,y
, vàz
), vàvec4
là một tập hợp 4 số dấu phẩy động (x
,y
,z
, vàw
). Thay vì sử dụngx
,y
,z
, vàw
, bạn cũng có thể sử dụngr
,g
,b
, vàa
cho màu sắc, chúng có thể hoán đổi cho nhau. - matrices: Một tập hợp các vectors. Ví dụ,
mat2
là một tập hợp 2 vectors,mat3
là một tập hợp 3 vectors, vàmat4
là một tập hợp 4 vectors.
Swizzling và thao tác
Bạn có thể truy cập các thành phần của một vector bằng cách sử dụng swizzling. Ví dụ, bạn có thể tạo một vector mới bằng cách sử dụng các thành phần của một vector khác:
vec3 a = vec3(1.0, 2.0, 3.0); vec2 b = a.xy;
Trong ví dụ này, b
sẽ là một vector chứa các thành phần x
và y
của a
.
Bạn cũng có thể sử dụng swizzling để thay đổi thứ tự của các thành phần:
vec3 a = vec3(1.0, 2.0, 3.0); vec3 b = a.zyx;
Trong ví dụ này, b
sẽ bằng vec3(3.0, 2.0, 1.0)
.
Để tạo một vector mới với tất cả các thành phần giống nhau, bạn có thể sử dụng constructor:
vec3 a = vec3(1.0);
Trong ví dụ này, a
sẽ bằng vec3(1.0, 1.0, 1.0)
.
Toán tử
GLSL có các toán tử số học thông dụng: +
, -
, *
, /
, +=
, /=
, *=
và các toán tử so sánh thông dụng: ==
, !=
, >
, <
, >=
, <=
.
Chúng cần được sử dụng với các kiểu dữ liệu đúng. Ví dụ, bạn không thể cộng một số nguyên với một float, bạn cần chuyển đổi số nguyên thành float trước:
int a = 1; float b = 2.0; float c = float(a) + b;
Bạn có thể thực hiện các phép toán trên vectors và matrices như sau:
vec3 a = vec3(1.0, 2.0, 3.0); vec3 b = vec3(4.0, 5.0, 6.0); vec3 c = a + b;
Điều này tương tự như:
vec3 c = vec3(a.x + b.x, a.y + b.y, a.z + b.z);
Hàm
Điểm bắt đầu của vertex và fragment shaders là hàm main
. Đây là hàm sẽ được thực thi khi shader được gọi.
void main() { // Your code here }
void là kiểu trả về của hàm. Nó có nghĩa là hàm không trả về bất kỳ giá trị nào.
Bạn cũng có thể định nghĩa các hàm của riêng bạn:
float add(float a, float b) { return a + b; }
Sau đó bạn có thể gọi hàm này trong hàm main
:
void main() { float result = add(1.0, 2.0); // ... }
GLSL cung cấp nhiều hàm có sẵn cho các phép toán thông thường như sin
, cos
, max
, min
, abs
, round
, floor
, ceil
và nhiều hàm hữu ích khác như mix
, step
, length
, distance
, và nhiều hơn nữa.
Chúng ta sẽ khám phá các hàm cần thiết và thực hành với chúng trong bài học tiếp theo.
Vòng lặp và Điều kiện
GLSL hỗ trợ vòng lặp for
và câu lệnh if
. Chúng hoạt động tương tự như trong JavaScript:
for (int i = 0; i < 10; i++) { // Mã của bạn ở đây } if (condition) { // Mã của bạn ở đây } else { // Mã của bạn ở đây }
Ghi log / Debugging
Vì các chương trình shader chạy song song cho mỗi đỉnh và mảnh, nên không thể sử dụng console.log
để debug mã của bạn hoặc thêm breakpoints. Đây là điều làm cho việc debug shaders trở nên khó khăn.
Một cách phổ biến để debug shaders là sử dụng gl_FragColor
để trực quan hóa các giá trị của biến của bạn.
Lỗi biên dịch
Nếu bạn mắc lỗi trong mã shader của mình, bạn sẽ thấy một lỗi biên dịch trong console. Nó sẽ cho bạn biết dòng nào và loại lỗi nào. Không phải lúc nào cũng dễ hiểu, nhưng đó là cách tốt để biết nơi cần tìm kiếm vấn đề.
Hãy loại bỏ kênh alpha khỏi gl_FragColor
và xem điều gì xảy ra:
void main() { gl_FragColor = vec4(1.0, 0.0, 0.0); }
Bạn sẽ thấy một lỗi biên dịch trong console:
Nói cho chúng ta biết rằng gl_FragColor
yêu cầu 4 thành phần, nhưng chúng ta chỉ cung cấp 3.
Đừng quên khôi phục kênh alpha về 1.0
để loại bỏ lỗi.
Uniforms
Để truyền dữ liệu từ mã JavaScript vào shader, chúng ta sử dụng uniforms. Chúng là các hằng số trong tất cả các vertices và fragments.
projectionMatrix
, modelViewMatrix
, và position
là những ví dụ của các uniforms tích hợp sẵn tự động được truyền vào shader.
Hãy tạo một uniform tùy chỉnh để truyền một màu vào shader. Chúng ta sẽ sử dụng nó để tô màu cho plane. Chúng ta sẽ gọi nó là uColor
. Một thói quen tốt là đặt tiền tố u
cho tên của uniform để làm rõ ràng đó là một uniform trong mã của chúng ta.
Đầu tiên, hãy khai báo nó trong đối tượng uniforms của shaderMaterial
:
import { Color } from "three"; // ... const MyShaderMaterial = shaderMaterial( { uColor: new Color("pink"), } // ... ); // ...
Sau đó, chúng ta có thể sử dụng nó trong fragment shader:
uniform vec3 uColor; void main() { gl_FragColor = vec4(uColor, 1.0); }
Bạn nên thấy plane được tô màu hồng:
Ở đây, màu hồng là giá trị mặc định của uniform. Chúng ta có thể thay đổi nó trực tiếp trên material:
<MyShaderMaterial uColor={"lightblue"} />
Mặt phẳng bây giờ đã được tô màu xanh nhạt.
Cả vertex và fragment shaders đều có thể truy cập vào uniforms. Hãy thêm thời gian như một uniform thứ hai vào vertex shader để di chuyển plane lên và xuống:
React Three Fiber: The Ultimate Guide to 3D Web Development
✨ You have reached the end of the preview ✨
Go to the next level with Three.js and React Three Fiber!
Get full access to this lesson and the complete course when you enroll:
- 🔓 Full lesson videos with no limits
- 💻 Access to the final source code
- 🎓 Course progress tracking & completion
- 💬 Invite to our private Discord community
One-time payment. Lifetime updates included.