The Book of Shaders by Patricio Gonzalez Vivo & Jen Lowe

Tiếng Việt - 日本語 - 中文版 - 한국어 - Español - Portugues - Français - Italiano - Deutsch - Русский - English


Biểu đồ màu của Paul Klee (1931)

Màu sắc

Ta chưa đề cập nhiều lắm về các loại vector trong GLSL. Trước khi đi tiếp thì việc hiểu các kiểu dữ liệu này rất quan trọng và màu sắc là chủ đề phù hợp tuyệt vời để giải thích cho các khái niệm đó.

Nếu bạn đã quen với mô hình lập trình hướng đối tượng thì có thể bạn đã chú ý tới việc ta có thể truy cập dữ liệu bên trong các vector như cách mà struct của ngôn ngữ C làm việc.

vec3 red = vec3(1.0,0.0,0.0);
red.x = 1.0;
red.y = 0.0;
red.z = 0.0;

Định nghĩa một màu sử dụng các ký hiệu x, yz có thể hơi khó hiểu nhỉ ? Đó cũng chính là lý do mà ta sẽ có nhiều cách khác để truy cập dữ liệu trong vector. Các ký hiệu x, yz có thể thay thế bằng .r, .g, .b, hoặc .s, .t, .p. (.s, .t.p sẽ được sử dụng ở các chương sau để truy cập các toạ độ trong không gian texture). Ngoài ra bạn cũng có thể truy cập các giá trị trong vector bằng vị trí trong array ([0], [1][2]).

Mỗi dòng code dưới đây đều truy cập một giá trị giống nhau trong vector:

vec4 vector;
vector[0] = vector.r = vector.x = vector.s;
vector[1] = vector.g = vector.y = vector.t;
vector[2] = vector.b = vector.z = vector.p;
vector[3] = vector.a = vector.w = vector.q;

Những cách truy cập khác nhau vào cùng 1 giá trị trong vector này sẽ giúp bạn viết code rõ ràng và dễ dàng hơn. Và sự linh hoạt này cũng sẽ khiến bạn dần dần xoá nhoà khoảng cách giữa không gian màu sắc và không gian vật lý, do chúng có cùng một cách lưu trữ dữ liệu.

Một tính năng khác cũng tuyệt vời không kém của vector trong GLSL là các giá trị bên trong có thể được tráo đổi (swizzle) vị trí theo bất kỳ trật tự nào bạn muốn, khiến cho việc xử lý chúng dễ dàng hơn bao giờ hết.

vec3 yellow, magenta, green;

// Tạo màu vàng (1., 1., 0.)
yellow.rg = vec2(1.0);  // Gán giá trị 1. cho kênh R và G
yellow[2] = 0.0;        // Gán giá trị 0. cho kênh B

// Tạo màu hồng (1., 0., 1.)
magenta = yellow.rbg;   // Đảo vị trí của 2 kênh G và B

// Tạo màu xanh lá (0., 1., 0.)
green.rgb = yellow.bgb; // Lấy giá trị ở kênh B của màu vàng để gán đồng thời cho cả kênh R và B của màu xanh lá

Tiện ích

Bạn có thể hiếm khi chọn màu bằng các con số vì nó không mấy trực quan, nhưng bắt buộc phải làm vậy trong GLSL. May thay, có rất nhiều tiện ích hỗ trợ thao tác này. Bạn có thể tuỳ ý lựa chọn tiện ích phù hợp nhất, miễn là kết quả trả về được lưu dưới dạng vec3 hoặc vec4. Ví dụ, tôi sử dụng các template sau trên trang Spectrum:

vec3({{rn}},{{gn}},{{bn}})
vec4({{rn}},{{gn}},{{bn}},1.0)

Trộn màu

Bạn đã biết cách định nghĩa các màu sắc rồi, giờ thì kết hợp nó với kiến thức đã có từ các chương trước nào. Trong GLSL có 1 hàm rất hữu ích, đó là mix(), giúp bạn trộn 2 màu với nhau theo 1 tỉ lệ nhất định. Và tỉ lệ đó cũng nằm trong khoảng [0.0, 1.0]. Hoàn hảo, đó chính là những gì mà ta đã học và luyện tập ở chương trước với việc sơn hàng rào, giờ thì lôi ra áp dụng thôi!

Hãy xem đoạn code dưới đây và chú ý vào dòng 18 vì tôi sẽ sử dụng giá trị tuyệt đối của đồ thị sóng hình sin làm tỉ lệ trộn 2 màu colorAcolorB.

Thử xem bạn thuần thục môn võ karate-shader đến đâu rồi nào:

Gradient

Hàm mix() còn nhiều vũ khí bí mật khác nữa. Thay vì truyền vào 1 số thực float để chỉ định tỉ lệ trộn 2 màu, bạn có thay nó bằng một vec3 (hoặc vec4 tuỳ vào định dạng của 2 màu gốc) để chỉ định tỉ lệ trộn màu cho từng kênh r, g, b (và cả a) riêng biệt.

Hãy xem đoạn code ví dụ dưới đây. Cũng tương tự như ở chương trước, tôi sẽ vẽ đồ thị của hàm số được dùng để chỉ định tỉ lệ trộn màu. Hiện tại thì tất cả các kênh đều dùng chung 1 hàm số nên sẽ có đồ thị giống nhau.

Bạn hãy thử uncomment dòng số 25 xem điều gì xảy ra. Rồi lần lượt cả dòng 26 và 27 nữa. Bạn sẽ thấy mỗi kênh của 2 màu colorAcolorB lại được trộn với nhau theo một tỉ lệ riêng.

Chắc hẳn bạn chưa quên 3 hàm số tôi dùng ở các dòng từ 25 tới 27 đâu nhỉ. Hãy thí nghiệm thoải mái với chúng đi. Kiến thức từ chương trước sẽ giúp bạn tạo nên rất nhiều dải màu gradient thú vị đó. Hãy thử:

Bức 'The Fighting Temeraire' của William Turner (1838)

HSB

Ta không thể học về màu sắc mà không đề cập tới không gian màu được. Có thể bạn đã biết, chúng ta có nhiều cách khác nhau để lưu trữ màu ngoài cách dùng 3 kênh đỏ, xanh lá, xanh dương.

HSB là viết tắt của Hue (sắc độ), Saturation (độ bão hoà màu) và Brightness (hoặc Value, độ sáng), là một cách định dạng màu khác, vốn có tổ chức và dễ hiểu hơn nhiều. Hãy dành vài phút để đọc hiểu 2 hàm rgb2hsv()hsv2rgb() trong đoạn code dưới đây.

Bằng cách ánh xạ vị trí trên trục X với Hue, vị trí trên trục Y với Brightness, ta có được dải phổ của những màu sắc có thể quan sát được bằng mắt thường. Cách phân bố màu trên không gian kiểu này của HSB giúp cho việc chọn màu dễ hơn cách dùng RGB nhiều.

HSB trong hệ toạ độ cực

HSB ban đầu được thiết kế để biểu diễn bằng hệ toạ độ cực (vị trí mỗi điểm được xác định bằng khoảng cách tới gốc toạ độ và góc phương vị - góc so với 1 trục toạ độ duy nhất), thay vì hệ toạ độ Đề-các. Giả sử toàn bộ canvas sẽ được dùng để biểu diễn hệ toạ độ cực. Để chuyển đổi màu ở định dạng HSB từ hệ toạ độ Đề-các sang hệ toạ độ cực thì ta cần xác định được khoảng cách từ 1 điểm ảnh trên canvas tới điểm chính giữa của canvas (chính là gốc toạ độ cực), rồi tính góc nghiêng của vector từ tâm tới điểm ảnh đó với trục hoành. Để làm được điều này thì ta cần dùng tới các hàm length()atan(y,x) (ở các ngôn ngữ shader khác thì người ta hay dùng hàm atan2(y,x) còn trong GLSL thì đó chính là hàm atan này, nhờ có overload).

Khi sử dụng các hàm vector và lượng giác, vec2, vec3vec4 sẽ chẳng khác gì vector thông thường, kể cả bạn có dùng nó để biểu diễn màu đi chăng nữa. Vì vậy ta sẽ coi như màu sắc và vector tương đương nhau, thực tế cho thấy lối suy nghĩ linh hoạt này sẽ rất hữu dụng.

Chú ý: Các hàm hình học, ngoài length ra thì còn rất nhiều: distance(), dot(), cross, normalize(), faceforward(), reflect()refract(). GLSL cũng có nhiều hàm đặc biệt dành riêng cho vector như: lessThan(), lessThanEqual(), greaterThan(), greaterThanEqual(), equal()notEqual().

Khi ta đã có góc và khoảng cách trong hệ toạ độ cực rồi, ta cần "chuẩn hoá" (normalize) các giá trị đó sao cho chúng nằm trong khoảng [0.0, 1.0]. Ở dòng 27, hàm atan(y,x) sẽ cho kết quả là góc tính bằng đơn vị radian, nằm trong khoảng [-PI, PI] tương đương với [-3.14, 3.14]. Vậy để chuẩn hoá về khoảng [0.0, 1.0], ta sẽ "remap" góc đó bằng cách chia cho 2 PI (dùng hằng số TWO_PI được định nghĩa trên đầu) rồi cộng kết quả thu được với 0.5. Còn với khoảng cách thì dễ rồi, giá trị lớn nhất mà nó có thể đạt được trong canvas là 0.5, cũng chính là bán kính đường tròn ngoại tiếp của canvas. Vậy nên để "remap" khoảng cách về khoảng [0.0, 1.0] thì ta chỉ cần x2.

Và đó cũng chính là phần chủ đạo trong đoạn code dưới đây.

Hãy thử sửa code sao cho:

Quang phổ tổng hợp và tách riêng tần số đỏ, vàng, xanh - William Home Lizars (1834)

Chú ý về hàm và tham số

Trước khi sang chương tiếp theo hãy cùng nhìn lại một chút bằng cách quan sát các hàm số đã dùng. Bạn sẽ thấy có từ khoá in được đặt trước kiểu dữ liệu của mỗi tham số. Từ khoá này là một trong những qualifier của GLSL và trong trường hợp này thì nó cho ta / GPU biết rằng mọi thay đổi trong hàm này sẽ không làm thay đổi giá trị của biến bên ngoài được truyền vào làm tham số. Ta sẽ còn thấy thêm các qualifier khác ở các chương tiếp theo như outinout. Qualifier inout, đại khái là sẽ khiến cho biến đó được truyền vào hàm với dạng tham chiếu (reference), nên mọi thay đổi trong hàm này đối với biến đó sẽ được giữ nguyên khi ra khỏi hàm.

int newFunction(in vec4 aVec4,      // read-only
                out vec3 aVec3,     // write-only
                inout int aInt);    // read-write

Ở chương tiếp theo ta sẽ dùng tất cả kiến thức đã học được trước đó để tạo nên các hình khối bằng cách pha trộn (blend) các không gian lại với nhau. Bạn không đọc nhầm đâu, ... pha trộn các không gian đó.