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 shader và fragment 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ẽ
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 float
dù i
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
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ị x
và y
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:
bvec2
: Vector boolean 2 chiều,bvec3
: Vector boolean 3 chiều,bvec4
: Vector boolean 4 chiềuivec2
: Vector số nguyên 2 chiều,ivec3
: Vector số nguyên 3 chiều,ivec4
: Vector số nguyên 4 chiềuvec2
: Vector số thực 2 chiều,vec3
: Vector số thực 3 chiều,vec4
: Vector số thực 4 chiều
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:
X
,Y
,Z
vàW
được dùng để mô tả các vector trong đồ hoạ 3DR
,G
,B
vàA
được dùng để mô tả các kênh màu và alpha[0]
,[1]
,[2]
và[3]
được dùng như một mảng cố định
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ị X
và Y
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.x
và a.y
cho X
, Y
của vec4
, rồi lại gán tiếp b.x
và b.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:
- Kích thước Array cố định
- Không thể dùng các hàm push(), pop(), splice() vân vân và cũng không có thuộc tính
length
luôn nha - Không thể gán giá trị hàng loạt khi khởi tạo mà phải gán giá trị từng phần tử một
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 break
và continue
:
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à attribute và uniform.
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
, out
và inout
.
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ố:
in
không thay đổi giá trị của biến bên ngoài (mặc định cho mọi tham số)out
chỉ dùng để lưu giá trị mới mà vẫn giữ nguyên khi đã ra khỏi hàminout
tuỳ ý đọc ghi
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 ?