The Book of Shaders by Patricio Gonzalez Vivo & Jen Lowe

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


Giới thiệu cho người đã biết Javascript

Tác giả: Nicolas Barradeau

Nếu bạn là developer Javascript, khả năng cao là bạn sẽ thấy hoang mang một chút khi đọc quyển sách này. Thực tế là có rất nhiều điểm khác biệt khi code JavaScript vốn chỉ là bề nổi, so với việc phải đụng tới shader ở sâu bên dưới tảng băng chìm. Tuy nhiên, không giống với ngôn ngữ nền tảng là Assembly, GLSL rất gần với ngôn ngữ mà con người có thể hiểu được, và tôi tin rằng một khi bạn đã nắm được các đặc tính của nó thì bạn sẽ bắt kịp rất nhanh thôi.

Coi như bạn đã biết về Javascript và cả Canvas API đi. Mà nếu có chưa biết mấy thì cũng đừng lo, bạn vẫn sẽ hiểu được phần lớn nội dung của phần này thôi.

Tất nhiên tôi sẽ không đi sâu vào chi tiết và một vài điều tôi nói có thể không chính xác hoàn toàn, nên đừng kỳ vọng nó được như "cầm tay chỉ việc" mà hãy coi nó như

MỘT CÁI ÔM NỒNG ẤM

JavaScript rất thích hợp để thử nghiệm nhanh ; bạn chỉ việc viết vài hàm đơn giản, không có ràng buộc gì về kiểu dữ liệu, tuỳ ý thêm bớt các hàm của class, tải lại trang web là đã thấy ngay kết quả rồi, sau đó lại sửa một tí, tải lại trang, cứ thế lặp lại, dễ như ăn kẹo.

Thế GLSL thì có gì khác JavaScript chứ. Suy cho cùng thì cả 2 đều chạy trên trình duyệt mà, chúng đều vẽ một vài thứ lên màn hình đó thôi, mà riêng về khía cạnh đó thì dùng JavaScript dễ hơn.

Ừ thì, điểm khác biệt chính nằm ở chỗ Javascript là một ngôn ngữ thông dịch (interpreted) còn GLSL thì là một ngôn ngữ biên dịch (compiled). Một chương trình biên dịch được thực thi trực tiếp bởi hệ điều hành, là một chương trình bậc thấp và thường chạy rất nhanh. Còn một chương trình thông dịch thì lại cần một Máy ảo (Virtual Machine / VM) để thực thi, nó là một chương trình bậc cao và thường chậm hơn.

Khi một trình duyệt (chính xác phải là máy ảo JavaScript mới đúng) thực thi hoặc thông dịch một đoạn mã, nó chẳng biết biến nào có ý nghĩa gì hay hàm này sẽ cho kết quả gì (ngoại trừ TypedArrays). Vì thế nó chẳng thể tối ưu bất kỳ cái gì trước khi thực thi cả, nó còn cần thời gian để dịch code của bạn này, rồi thì đoán xem kiểu dữ liệu của các biến là gì này và nếu được thì cố gắng chuyển một phần sang dạng mã Assembly để code chạy nhanh hơn.

Đó là cả một quá trình cồng kềnh, phức tạp và có phần lề mề, nếu bạn hứng thú đi vào chi tiết thì tôi xin giới thiệu tìm hiểu cách mà trình thông dịch V8 của Chrome hoạt động. Điểm tệ nhất đó là, mỗi trình duyệt lại tự tối ưu mã JavaScript theo một cách riêng mà quá trình này hoàn toàn nằm ngoài tầm với của bạn.

Còn một chương trình biên dịch thì không như thế ; hệ điều hành thực thi nó, nếu chương trình không có lỗi biên dịch thì cứ thế mà chạy thôi. Nếu bạn quên một dấu chấm phẩy cuối dòng thì khả năng cao là bạn sẽ được thông báo còn code của bạn thì thậm chí còn chưa được biên dịch thành chương trình cơ.

Hơi phũ, nhưng đó chính là cách mà shader hoạt động: một chương trình được biên dịch để thực thi trên GPU. Đừng sợ! Một trình biên dịch sẽ là chiến hữu đáng tin cậy nhất của bạn. Các ví dụ trong quyển sách này và người đồng hành editor online rất thân thiện. Nó sẽ chỉ ra cho bạn thấy tại sao code của bạn không biên dịch được và bạn phải sửa chỗ nào, và nếu bạn làm đúng thì kết quả hiển thị ngay lập tức luôn. Đó là một cách tuyệt vời để học vì nó rất trực quan và bạn chẳng phải sợ sẽ làm hỏng cái gì cả.

Điểm lưu ý cuối cùng, một chương trình shader được tạo nên bởi 2 chương trình con, đó là vertex shaderfragment shader. Về cơ bản thì vertex shader sẽ nhận tham số đầu vào là các khối hình học rồi biến chúng thành các điểm ảnh (pixel) (hoặc fragment) rồi chuyển kết quả cho fragment shader xử lý tiếp, vốn công việc chính là tô màu từng điểm ảnh. Quyển sách này hầu như chỉ tập trung vào chương trình thứ hai. Trong tất cả các ví dụ, khối hình được sử dụng chỉ là một tứ giác lấp đầy cả màn hình.

Vậy! Bạn đã sẵn sàng chưa ? Tiếp tục nhé!

Quy định kiểu dữ liệu một cách chặt chẽ

Kết quả tìm kiếm đầu tiên với từ khoá 'strong type' trên Google Image, ngày 2016/05/20

Khi bạn đã quen với JavaScript hay các ngôn ngữ không quan trọng về kiểu dữ liệu, thì việc phải quy định kiểu dữ liệu cho mỗi biến là một khái niệm xa lạ, cũng khiến cho nó trở thành rào cản lớn nhất khi làm quen với GLSL. Kiểu dữ liệu, như chính cái tên của nó, có nghĩa là bạn phải chỉ định mỗi biến (và cả hàm nữa) sử dụng kiểu dữ liệu gì. Về cơ bản thì việc dùng chung một từ khoá var cho tất cả các biến đã không còn nữa. GLSL không cho phép điều đó xảy ra nên bạn có muốn cũng không được.

Thay vì dùng từ khoá var thần thánh, bạn sẽ phải chỉ đích danh kiểu dữ liệu cho từng biến một, sau đó thì trình biên dịch sẽ biết chính xác đang phải xử lý cái gì và làm thế nào thì hiệu quả nhất. Nhược điểm của việc này là bạn phải hiểu tất cả các kiểu dữ liệu, mà lại còn phải hiểu tường tận nữa cơ. May thay, chỉ có một vài kiểu dữ liệu thôi và cũng khá đơn giản nữa.

Nghe thì đáng sợ chứ thực ra nó không quá khác với code JavaScript mà bạn vẫn hay dùng đâu ; nếu một biến có kiểu boolean thì bạn sẽ trông đợi nó chỉ lưu trữ một trong hai giá trị true hoặc false mà thôi. Nếu một biến được khai báo là var uid = XXX;, thì có khả năng đó là một số nguyên còn nếu nó được khai báo là var y = YYY; có thể nó trỏ tới một số thực. Còn với ngôn ngữ quy định kiểu dữ liệu (strong type), bạn sẽ không phí thời gian đoán xem 2 biến đó có cùng kiểu không, bằng các biểu thức X == Y (hay typeof X == typeof Y ? .. hoặc typeof X !== null && Y... ...) ; bạn sẽ biết chắc điều đó đúng hay sai mà kể cả bạn không để ý thì trình biên dịch sẽ làm thay việc đó.

Đây là các kiểu dữ liệu đơn (scalar) trong GLSL: bool (Đúng sai), int(Số nguyên), float(Số thực). Còn vài kiểu nữa nhưng cứ từ từ, đoạn code mẫu dưới đây khai báo các biến (đừng quên var không tồn tại trong thế giới GLSL nhé):

// Khai báo một biến boolean
JavaScript: var b = true;               GLSL: bool b = true;

// Khai báo một số nguyên
JavaScript: var i = 1;                  GLSL: int i = 1;

// Khai báo một số thực
JavaScript: var f = 3.14159;            GLSL: float f = 3.14159;

Không có gì khó phải không ? Như đã nói ở trên, nó thậm chí còn giúp bạn đỡ đau đầu khi code ấy chứ. Nếu còn nghi ngờ về điều đó thì cứ tạm bỏ qua, chỉ cần biết nó giúp chương trình của bạn chạy nhanh hơn JavaScript nhiều là cũng đủ rồi.

void

Có kiểu void tương đương với null, nó được dùng khi hàm không trả về kết quả gì cả. Và bạn không thể khai báo biến kiểu này.

boolean

Các biến kiểu boolean hầu hết được sử dụng trong các câu lệnh điều kiện như ; if( myBoolean == true ){}else{}. Nếu các nhánh điều kiện rất hay gặp ở CPU, thì kiến trúc song song của GPU lại hạn chế đất diễn của chúng. Thậm chí việc sử dụng các lệnh điều kiện còn không được khuyến khích ở đa phần các trường hợp, trong quyển sách này có một vài kỹ thuật để xử lý các trường hợp đó.

Ép kiểu

Như Boromir đã nói, "One does not simply combine Typed primitives". Không như JavaScript, GLSL không cho phép thực hiện các phép toán giữa các toán hạng không cùng kiểu.

Ví dụ sau:

int     i = 2;
float   f = 3.14159;

// thử nhân một số nguyên với một số thực
float   r = i * f;

sẽ không cho kết quả tốt vì bạn đang cố lai con mèo với con hươu cao cổ. Giải pháp là ép kiểu (type casting) ; đoạn code sau sẽ giúp trình biên dịch tin rằng i cũng có kiểu floati vẫn giữ nguyên kiểu vốn có:

// ép biến `i` từ kiểu int sang float
float   r = float( i ) * f;

Điều này giống như việc cho con mèo mặc đồ của con hươu cao cổ vậy, và nó sẽ có hiệu quả (r sẽ kết quả của phép toán i x f).

Bạn có thể ép kiểu qua lại giữa tất cả các kiểu phía trên, chú ý là khi chuyển từ số thực sang số nguyên thì phần thập phân sẽ biến mất, tương đương với việc dùng hàm Math.floor(). Ép một số thực float hoặc một số nguyên int sang kiểu bool sẽ cho kết quả true nếu số đó khác 0.

Hàm khởi tạo (constructor)

Kiểu dữ liệu của biến cũng chính là hàm khởi tạo của class tương ứng ; thực tế thì một số thực float có thể coi là 1 instance của class Float.

Các lệnh sau đây đều hợp lệ và cho kết quả giống nhau

int     i = 1;
int     i = int( 1 );
int     i = int( 1.9995 );
int     i = int( true );

Trông thì không giống kiểu scalar lắm, và cũng na ná ép kiểu, nhưng mọi sự sẽ sáng tỏ ở phần overload.

OK, vậy là ta đã biết về ba kiểu dữ liệu cơ bản, những thứ mà bạn không thể sống nếu thiếu được và đương nhiên là GLSL còn nhiều kiểu khác.

Vector

Kết quả tìm kiếm đầu tiên với từ khoá 'vector villain' trên Google Image, ngày 2016/05/20

Trong cả JavaScript lẫn GLSL, bạn sẽ cần những cách tinh vi hơn để xử lý dữ liệu, và vectors khi đó rất hữu ích. Tôi cho rằng bạn đã sử dụng class Point trong JavaScript để lưu 2 giá trị xy cùng lúc rồi, code sẽ trông thế này:

// Khai báo 'class':
var Point = function( x, y ){
    this.x = x || 0;
    this.y = y || 0;
}

// và bạn sẽ tạo một instance mới như sau
var p = new Point( 100,100 );

Như ta thấy, có quá nhiều điểm không hợp lý. Từ khoá var vồn dùng cho biến thì lại được dùng để khai báo class rồi thì this chả hiểu ở đâu ra, xong lại x với y chả biết kiểu dữ liệu gì ... Kiểu này là không ổn với shader đâu.

Thay vào đó, GLSL có sẵn các cấu trúc dữ liệu để lưu trữ các biến đồng thời, có thể kể ra:

Bạn nhận ra là có đủ các loại vector cho mỗi kiểu dữ liệu cơ bản, tuyệt vời ông mặt giời. Từ giải thích trên đây, ta có thể suy luận được rằng bvec2 sẽ gồm 2 giá trị kiểu bool còn vec4 sẽ gồm 4 số thực kiểu float

Một điểm mới nữa từ vector là các chiều (dimension), không phải bạn dựng hình 2D thì dùng vector 2 chiều còn dựng hình 3D thì dùng vector 3 chiều đâu nhé. Nếu vậy thì vector 4 chiều dùng để dựng hình gì ? (Thực ra thì không gian 4 chiều có tên riêng đấy, là tesseract hoặc khối siêu lập phương - hypercube)

Nhưng không phải thế đâu nhé, từ chiều ở đây chỉ nói về số giá trị được lưu giữ trong mỗi vector thôi:

// tạo một vector boolean 2 chiều
bvec2 b2 = bvec2 ( true, false );

// tạo một vector số nguyên 3 chiều
ivec3 i3 = ivec3( 0,0,1 );

// tạo một vector số thực 4 chiều
vec4 v4 = vec4( 0.0, 1.0, 2.0, 1. );

b2 gồm 2 giá trị boolean, i3 gồm 3 số nguyên v4 gồm 4 số thực.

Làm thế nào để sử dụng từng giá trị trong vector? Với kiểu dữ liệu đơn thì quá đơn giản ; nếu ta có float f = 1.2; thì biến f sẽ có giá trị 1.2. Còn với vector thì hơi khác và cũng kỳ diệu hơn một chút.

Truy cập

Có nhiều cách để truy cập các giá trị bên trong vector

// Đầu tiên hãy tạo một vector số thực 4 chiều
vec4 v4 = vec4( 0.0, 1.0, 2.0, 3.0 );

Để lấy từng giá trị trong vector, ta có thể viết:

float x = v4.x;     // x = 0.0
float y = v4.y;     // y = 1.0
float z = v4.z;     // z = 2.0
float w = v4.w;     // w = 3.0

rất dễ dàng, nhưng còn nhiều cách khác cũng cho kết quả tương tự:

float x =   v4.x    =   v4.r    =   v4.s    =   v4[0];     // x = 0.0
float y =   v4.y    =   v4.g    =   v4.t    =   v4[1];     // y = 1.0
float z =   v4.z    =   v4.b    =   v4.p    =   v4[2];     // z = 2.0
float w =   v4.w    =   v4.a    =   v4.q    =   v4[3];     // w = 3.0

Nếu bạn tinh ý thì sẽ nhận ra:

Vậy nên tuỳ vào việc bạn đang xử lý các toạ độ 2D hay 3D, màu kèm theo alpha hoặc không, hay một vài con số bất kỳ, bạn có thể tuỳ chọn cách dùng vector bạn muốn. Thường thì các toạ độ 2 chiều và các vector sẽ được lưu bằng các cấu trúc vec2, vec3 hoặc vec4, màu sắc thì được lưu trong vec3 hoặc vec4 nếu bạn muốn lưu thêm kênh alpha nữa, không có ràng buộc nào cả. Thậm chí, nếu bạn dùng cả một vector bvec4 chỉ để lưu 1 giá trị boolean thì cũng được nốt, có điều hơi lãng phí bộ nhớ.

Chú ý: Trong shader, các giá trị của các kênh (R, G, B, A) đều được chuẩn hoá để nằm trong khoảng [0-1] chứ không phải [0x00-0xFF], vì thế tốt nhất là nên sử dụng vector số thực 4 chiều vec4 để lưu trữ giá trị màu.

Hay quá phải không, chưa hết đâu nhé!

Tráo đổi (swizzle)

Ta có thể lấy ra nhiều hơn một giá trị nữa cơ ; giả sử bạn chỉ cần 2 giá trị XY từ một vec4, thì trong JavaScript, bạn sẽ phải làm như sau:

var needles = [0, 1]; // vị trí của 'x' và 'y' trong array dưới đây
var a = [ 0,1,2,3 ]; // giả lập `vec4`
var b = a.filter( function( val, i, array ) {
return needles.indexOf( array.indexOf( val ) ) != -1;
});
// b = [ 0, 1 ]

// hoặc trực diện luôn
var needles = [0, 1];
var a = [ 0,1,2,3 ]; // giả lập `vec4`
var b = [ a[ needles[ 0 ] ], a[ needles[ 1 ] ] ]; // b = [ 0, 1 ]

Quá cồng kềnh. Hãy xem trong GLSL làm như thế nào:

// Tạo một `vec4`
vec4 v4 = vec4( 0.0, 1.0, 2.0, 3.0 );

// rồi chỉ lấy mỗi x và y ra
vec2 xy =   v4.xy; //   xy = vec2( 0.0, 1.0 );

Ủa cái gì vậy ?! Khi bạn truy cập liên tiếp (concatenate accessors), GLSL sẽ trả về tập con của các giá trị bạn muốn, gói gọn trong một vector khác. Thực ra thì vector là một cấu trúc dữ liệu cho phép truy cập ngẫu nhiên, nếu muốn bạn có thể tượng tượng nó giống array bên JavaScript vậy. Vì thế, bạn không chỉ lấy được 1 tập con mà còn có thể chỉ định thứ tự từng phần tử muốn lấy nữa cơ. Đoạn code sau sẽ tráo đổi giá trị của các vector theo thứ tự ngược lại:

// Tạo một vector R,G,B,A
vec4 color = vec4( 0.2, 0.8, 0.0, 1.0 );

// Truy cập các gía trị theo thứ tự ngược lại
vec4 backwards = v4.abgr; // backwards = vec4( 1.0, 0.0, 0.8, 0.2 );

Hơn thế nữa, chẳng ai ngăn bạn lấy một phần tử nhiều lần:

// Tạo một vector R,G,B,A
vec4 color = vec4( 0.2, 0.8, 0.0, 1.0 );

// Và tạo một vector mới chỉ dùng giá trị của kênh G (2 lần) và A
vec3 GAG = v4.gag; // GAG = vec4( 0.8, 1.0, 0.8 );

Khă năng này quá ngầu khi phải xử lý vector, ví dụ như khi chỉ muốn lấy các kênh RGB của một màu có đủ RGBA chẳng hạn.

Overload tất cả

Ở phần kiểu dữ liệu, tôi đã nhắc tới điều gì đó liên quan tới hàm khởi tạo (constructor) và đây lại là 1 tính năng tuyệt vời nữa của GLSL ; overload. Cho ai chưa biết, overload là một toán tử hoặc hàm số đại loại sẽ 'tự động thay đổi cách thực thi sao cho khớp với kiểu dữ liệu'. JavaScript không có overload, nên bạn có thể thấy nó hơi lạ lúc đầu, nhưng khi đã quen rồi thì bạn sẽ thắc mắc sao JavaScript lại không có tính năng này (ngắn gọn là do không ràng buộc kiểu dữ liệu đó).

Hãy xem ví dụ đơn giản nhất để hiểu overload là gì:

vec2 a = vec2( 1.0, 1.0 );
vec2 b = vec2( 1.0, 1.0 );
// overload phép cộng
vec2 c = a + b;     // c = vec2( 2.0, 2.0 );

HẢ ? Hai giá trị không phải số đơn thuần mà cũng cộng được ?!

Chính xác là thế đó. Tất nhiên là áp dụng cho toàn bộ các toán tử khác (+, -, * & /) nữa nhưng đây mới là mở đầu thôi. Hãy xem đoạn code sau:

vec2 a = vec2( 0.0, 0.0 );
vec2 b = vec2( 1.0, 1.0 );
// overload hàm khởi tạo
vec4 c = vec4( a , b );         // c = vec4( 0.0, 0.0, 1.0, 1.0 );

Ta vừa mới tạo nên một vec4 từ 2 vec2, bằng cách gán giá trị của a.xa.y cho X, Y của vec4, rồi lại gán tiếp b.xb.y cho Z, W của vec4

Đây là điều sẽ xảy ra khi một hàm được overload để chấp nhận các loại tham số khác nhau, cụ thể trong trường hợp này là hàm khởi tạo của vec4. Điều đó có nghĩa là rất nhiều phiên bản khác nhau của cùng 1 hàm có thể cùng tồn tại, ví dụ các lệnh khai báo sau hoàn toàn hợp lệ:

vec4 a = vec4(1.0, 1.0, 1.0, 1.0);
vec4 a = vec4(1.0);// cả 4 giá trị x, y, z, w đều bằng 1.0
vec4 a = vec4( v2, float, v4 );// vec4( v2.x, v2.y, float, v4.x );
vec4 a = vec4( v3, float );// vec4( v3.x, v3.y, v3.z, float );
etc.

Điều duy nhất bạn cần bận tâm là đảm bảo hàm tạo có đủ dữ liệu nó cần mà thôi.

Điều cuối cùng, bạn hoàn toàn có thể overload một hàm có sẵn bất kỳ sao cho nó phù hợp với yêu cầu của bạn (cũng không nên lạm dụng quá).

Các kiểu dữ liệu khác

Vector thật thú vị, và là vũ khí chính trong code shader. Còn các cấu trúc dữ liệu khác như Ma trận và Texture sampler sẽ được bàn tới ở phần sau của quyển sách.

Bạn cũng có thể dùng Array. Tất nhiên là phải quy định kiểu rồi và sau đây là một vài điểm đáng lưu tâm:

Code như sau sẽ không đúng:

int values[3] = [0,0,0];

phải như thế này cơ:

int values[3];
values[0] = 0;
values[1] = 0;
values[2] = 0;

Điều này cũng không quá tệ nếu bạn thật sự cần can thiệp từng phần tử trong array. Còn nếu muốn đa dạng hơn thì có thể dùng kiểu struct. Chúng giống như các object nhưng không có hàm đi kèm ; chúng chỉ cho phép đóng gói các biến vào bên trong thôi

struct ColorStruct {
    vec3 color0;
    vec3 color1;
    vec3 color2;
}

sau đó bạn có thể sử dụng như sau:

// Khởi tạo cấu trúc với giá trị nào đó
ColorStruct sandy = ColorStruct(    vec3(0.92,0.83,0.60),
                                    vec3(1.,0.94,0.69),
                                    vec3(0.95,0.86,0.69) );

// Truy cập biến trong cấu trúc
sandy.color0 // vec3(0.92,0.83,0.60)

Cú pháp kiểu này có thể hơi tự do chút, nhưng nó sẽ giúp bạn viết code rõ ràng hơn hay ít nhất là nhìn dễ hiểu hơn.

Các lệnh điều khiển

Cấu trúc dữ liệu hay thật đấy nhưng tới lúc nào đó ta vẫn có thể phải cần tới các lệnh điều khiển. May mán là cú pháp rất giống với JavaScript.

Ví dụ về lệnh điều kiện:

if( condition ){
    //true
}else{
    //false
}

Còn một vòng lặp thì thường giống như:

const int count = 10;
for( int i = 0; i <= count; i++){
    // làm gì đó
}

hoặc với iterator:

const float count = 10.;
for( float i = 0.0; i <= count; i+= 1.0 ){
    // làm gì đó
}

Chú ý là biến count phải được khai báo là một hằng số (constant). Điều đó có nghĩa là ta phải đặt thêm từ khoá const vào trước như ví dụ dưới đây.

Ngoài ra ta cũng có các lệnh breakcontinue:

const float count = 10.;
for( float i = 0.0; i <= count; i+= 1.0 ){
    if( i < 5. )continue;
    if( i >= 8. )break;
}

Chú ý là trên một vài nền tảng phần cứng, lệnh break không hoạt động giống nhau nên vòng lặp vẫn chạy.

Nhìn chung thì bạn nên giữ số vòng lặp càng ít càng tốt và tránh sử dụng các lệnh điều kiện càng nhiều càng tốt.

qualifiers

Không chỉ có kiểu dữ liệu mà GLSL còn dùng qualifier để giúp trình biên dịch biết mỗi biến có gì đặc biệt. Ví dụ có những dữ liệu chỉ được truyền từ CPU sang GPU gọi là attributeuniform. Từ khoá attribute được dùng trong vertex shader, còn uniform được dùng cho cả vertex shader và fragment shader. Từ khoá varying để đánh dấu các biến luân chuyển giữa vertex shader và fragment shader.

Tôi sẽ không đi vào chi tiết ở đây vì ta chủ yếu tập trung vào fragment shader nhưng ở phần sau của quyển sách, bạn sẽ thấy code tương tự như:

uniform vec2 u_resolution;

Tôi mới đặt từ khoá uniform vào trước kiểu dữ liệu của biến khi khai báo đó. Điều này có nghĩa là CPU sẽ gửi thêm thông tin cho shader thông qua biến này. Cụ thể đó là độ phân giải của canvas, chiều rộng và chiều cao của canvas sẽ được lưu vào biến x và y của một vector 2 chiều.

Khi trình biên dịch thấy một biến có đánh dấu qualifier này, nó sẽ đảm bảo bạn không thể thay đổi giá trị trong shader.

Điều tương tự cũng được áp dụng cho biến count được dùng để giới hạn số vòng lặp for:

const float count = 10.;
for( ... )

Khi ta dùng qualifier const, trình biên dịch sẽ đảm bảo rằng giá trị của biến này chỉ được khởi tạo một lần duy nhất, nếu không thì nó không phải là hằng số nữa rồi.

Còn 3 qualifier hay được dùng nữa cho các tham số của hàm là : in, outinout. Trong JavaScript, khi bạn truyền giá trị vào 1 hàm thì mọi thay đổi với giá trị đó chỉ có tác dụng trong hàm chứ không ảnh hưởng gì tới biến bên ngoài.

function banana( a ){
    a += 1;
}
var value = 0;
banana( value );
console.log( value );// > 0 ; ra khỏi hàm thì value vẫn giữ nguyên giá trị như trước khi gọi hàm

Ý nghĩa của 3 qualifier tham số:

Viết lại hàm banana trong GLSL:

void banana( inout float a ){
    a += 1.;
}
float A = 0.;
banana( A ); // lúc này A = 1.;

Điều này rất khác so với JavaScript và cũng rất tiện.

Không gian và toạ độ

Chú ý cuối cùng, trong DOM và Canvas 2D, trục Y hướng xuống dưới. Điều này có lý trong bối cảnh DOM dựng trang web có thể scroll được ; từ trên xuống dưới. Còn trong canvas của WebGL thì trục Y hướng lên trên.

Điều đó có nghĩa là gốc tọa độ, điểm (0, 0) nằm ở góc dưới cùng bên trái của WebGL canvas, chứ không phải góc trên cùng bên trái như 2D Canvas. Toạ độ của texture cũng vì thế mà có thể gây lú một chút nếu chưa quen.

Và thế là hết !

Tất nhiên ta có thể đi sâu hơn vào rất nhiều khái niệm phía trên, nhưng nhớ rằng đây chỉ là lời chào dành cho người mới. Đúng là có rất nhiều thứ phải tìm hiểu nhưng với sự kiên trì và chăm chỉ thì sẽ quen nhanh thôi.

Tôi hy vọng bạn thấy một vài phần hữu ích ở đây, giờ thì bạn sẵn sàng bắt đầu chuyển phiêu lưu vào quyển sách chưa ?