Tìm hiểu WebGL Phần 2: Đi sâu vào cách code WebGL appication
Bài đăng này đã không được cập nhật trong 3 năm
Lời Nói Đầu
Tiếp theo phần một về tìm hiểu về WebGL, ở bài này ta sẽ đi sâu và cách viết chương trình WebGL. Bắt đầu từ chuẩn bị canvas, context, chuẩn bị dữ liệu hình học cho các mô hình cần vẽ, viết các shader program để xử lý đỉnh, màu sắc, texture của mô hình, liên kết các shader program và vẽ các mô hình.
Lập Trình WebGL Application
I. WebGL Context
Để viết một ứng dụng WebGL, đầu tiên ta cần lấy được WebGL rendering context, một đối tượng giúp tương tác với buffer để vẽ trong WebGL và các hàm của WebGL. Để thu được WebGL context ta thực hiện các bước sau:
-
Tạo HTML 5 canvas: ở bước này chúng ta khai báo canvas trong HTML 5 bằng thẻ <canvas>. Cung cấp cho nó một cái id và có thể style để xác định kích thước của canvas.
<!DOCTYPE HTML> <html> <head> <style>
mycanvas{border:1px solid blue;}
</style>
</head>
<body>
<canvas id = "mycanvas" width = "300" height = "300"></canvas>
</body>
</html>
-
Một khi tạo xong canvas ta sẽ viết script để tương tác với canvas bằng javascript qua đó thu được WebGL context.
2.1 Lấy id của canvas sử dụng javascript và dom.
var canvas = document.getElementById('my_Canvas');
2.2 Thu lấy WebGL context thông hàm getContext(), với tham số truyền vào là experimental-webgl.
var canvas = document.getElementById('my_Canvas'); var gl = canvas.getContext('experimental-webgl');
Và như vậy ta đã thu được đối tượng WebGL để bắt đầu tạo ứng dụng WebGL.
II. Dữ liệu hình học cho WebGL
1. Lưu các dữ liệu bằng javascript
Các mô hình đối tượng 2D hoặc 3D phải có các dữ liệu hình học về: đỉnh, chỉ số các đỉnh, màu sắc, texture … đúng chuẩn. Trong WebGL các dữ liệu này được lưu trữ bằng mảng javascript. Các dữ liệu này được truyền cho các shader programs, các chương trình chạy trên GPU và sẽ tạo ra các đối tượng đồ họa.
Một mô hình 2D hoặc 3D được vẽ sử dụng các đỉnh gọi là mesh. Mỗi mặt trong mesh là môt hình đa giác, và các đa giác này được dựng bởi 3 hoặc nhiều đỉnh. Để vẽ một mô hình trong WebGL, ta phải định nghĩa các đỉnh và các chỉ số của đỉnh bằng mảng javascript. Ví dụ 3 đỉnh của một tam giác có thể khai báo như sau:
var vertices = [
0.5,0.5, //Vertex 1
0.5,-0.5, //Vertex 2
-0.5,-0.5, //Vertex 3
];
Ngoài ra tam giác còn có thể định nghĩa bằng chỉ số của các đỉnh, như tam giác sau:
Có thể được định nghĩa bằng các chỉ số của các đỉnh 0, 1, 2.
var indices = [ 0,1,2 ]
2. Buffer object
Đây là cơ chế được cung cấp bởi WebGL nhằm chỉ ra vùng bộ nhớ được cung cấp trong hệ thống. Các buffer object này được dung để lưu trữ dữ liệu của mô hình mà ta muốn vẽ, tương ứng với các đỉnh, các chỉ số đỉnh, màu sắc … Sử dụng buffer object cho phép chúng ta truyền nhiều dữ liệu cùng lúc vào các shader programs, thông qua biến attribute của shader programs. Vì các buffer object này được lưu trữ trong GPU, chúng có thể được render trực tiếp qua đó cải thiện hiệu năng của quá trình render các mô hình đồ họa. Để xử lý hình học có hai kiểu buffer object.
-
Vertex buffer object: chúng lưu trữ dữ liệu của các đỉnh trược khi chúng được render. Các dữ liệu có thể là: tọa độ đỉnh, màu sắc, texture.
-
Index buffer object: nó lưu trữ dữ liệu chỉ số đỉnh của các mô hình đồ họa. Khi đã có dữ liệu hình học của mô hình đồ họa cần vẽ.
Ta sẽ lưu trữ nó vào các buffer object theo các bước sau:
-
Tạo một buffer rỗng bằng createBuffer()
var vertex_buffer = gl.createBuffer();
-
Bind buffer với một mảng thích hợp: sau khi tạo buffer object ta cần bind một mảng thích hợp với nó sử dụng bindBuffer().
//bind vertex buffer gl.bindBuffer(gl.ARRAY_BUFFER, vertex_buffer); //bind index buffer gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, index_buffer);
Tham số đầu tiên là kiểu buffer ta muốn bind với buffer rỗng mà ta mới tạo ở trên. Có 2 kiểu đã được định nghĩa sẵn là:
- ARRAY_BUFFER: biểu diễn cho dữ liệu vertex.
- ELEMENT_ARRAY_BUFFER: biểu diễn cho dữ liệu index.
Tham số thứ 2 là biến buffer object mà ta tạo lúc trước.
-
Truyền data vào buffer object sử dụng bufferData().
//truyền data vào vertex buffer gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW); //truyền data vào index buffer gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW);
Tham số đầu tiên là: kiểu buffer ta đã sử dụng.
Tham số thứ 2 là data của buffer. Cần chú ý do data này lúc tạo sử dụng mảng javascript, ta cần chuyển đổi data này sang kiểu mà buffer của WebGL có thể hiểu.
Tham số thứ 3 đẻ chỉ ra cách sử dụng data của buffer object để vẽ gồm 3 cách:
- STATIC_DRAW: data được chỉ ra một lần nhưng sử dụng nhiều lần.
- STREAM_DRAW: data được chỉ ra một lần và được sử dụng vài lần.
- DYNAMIC_DRAW: data được lặp đi lặp lại và được sử dụng nhiều lần.
-
Khi sử dụng xong ta có thể unbind buffer để giải phóng nó bằng việc sử dụng hàm bindBuffer() nhưng tham số thứ 2 là null.
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null);
III. Shader
Shader là các chương trình được chạy trên GPU. Shader được viết bằng OpenGL Embedded System Shader Language. ES SL có các biến, kiểu dữ liệu, qualifiers, input và output dựng sẵn.
1. Kiểu dữ liệu
|_. Kiểu |_. Mô tả |
| void | biểu diễn cho giá trị rỗng |
| bool | true hoặc false |
| int | kiểu số nguyên |
| float | kiểu số thực |
| vec2, vec3, vec4 | vecto số thực n chiều |
| bvec2, bvec3, bvec4 | vecto boolen |
| ivec2, ivec3, ivec4 | vecto số nguyên có dấu |
| mat2, mat3, mat4 | ma trận số thực nxn |
| sampler2D | texture 2D |
| samplerCube | texture hình lập phương |
2. Qualifiers
|_. Qualifiers |_. Mô tả |
| attribute | qualifier này hoạt động như một liên kết giữa vertex shader và OpenGL ES để tiền xử lý dữ liệu của các đỉnh. Giá trị của attribute thay đổi khi vertex shader được chạy. |
| uniform | qualifier này liên kết shader program và ứng dụng WebGL. Giá trị của chúng không đổi và chỉ đọc (read-only) |
| varying | qualifier này liên kết giữa vertex shader và fragment shader biến đổi dữ liệu. |
3. Vertex shader
Là các chương trình được gọi trên mỗi đỉnh. Nó dùng để biến đổi hình học từ một vị trí này sang vị trí khác. Nó xử lý dữ liệu mỗi đỉnh như: tọa độ các đỉnh, màu sắc, và texture. Khi ES SL code của shader program, lập trình viên cần định nghĩa các attribute để xử lý dữ liệu. Những attribute này trỏ đến các đối tượng vertex buffer được viết bằng javascript. Trong vertex shader có các biến được định nghĩa trước:
|_. Biến |_. Mô tả |
| vec4 gl_Position; | Lưu vị trí của đỉnh. |
| float gl_PointSize; |Lưu kích thước của điểm |
Một đoạn code đơn giản của vertex shader.
attribute vec2 coordinates;
void main(void) {
gl_Position = vec4(coordinates, 0.0, 1.0);
};
Ở trên ta định nghĩa một attribute là coordinates kiểu vecto hai chiều. Tiếp đến một biến có sẵn của vertex shader được định nghĩa là gl_Position (biến này chỉ làm việc với vertex shader), biến này có kiểu là vec4, và ở trên attribute coordinates được truyền vào cùng với 2 giá trị nữa để hình thành gl_Position. Sau này gl_Position được dùng để nối các đỉnh, cắt gọt các mô hình, và các hoạt động khác trên các hình cơ bản (primitive) sau khi quá trình xử lý đỉnh kết thúc.
4. Fragment shader
Các mesh được tạo bởi nhiều hình tam giác, và mặt của mỗi hình tam giác này được hiểu là một fragment. Fragment shader program là đoạn code chạy trên mỗi điểm ảnh của mỗi fragment. Nó được sử dụng để tính toán và tô màu cho mỗi điểm ảnh. Trong fragment shader có các biến được định nghĩa trước:
|_. Biến |_. Mô tả |
| vec4 gl_FlagCoord; | Lưu vị trí của fragment cùng với buffer frame. |
| bool gl_FrontFacing; |Lưu fragment thuộc về các hình mặt trước |
| vec2 gl_PointCoord; |Lưu vị trí của fragment cùng với điểm |
| vec4 gl_FlagColor; |Lưu màu output của fragment |
| vec4 gl_FlagData[n]; |Lưu màu fragment cho màu thứ n |
Một đoạn code đơn giản của vertex shader.
void main(void) {
gl_FragColor = vec4(0, 0.8, 0, 1);
}
5. Lưu trữ và biên dịch các shader program
- Lưu trữ mã nguồn.
Các shader là các chương trình độc lập, ta có thể viết chúng như các kịch bản riêng biệt và sử dụng trong ứng dụng. Hoặc lưu mã nguồn của chúng vào một string.
var vertexCode =
'attribute vec2 coordinates;' +
'void main(void) {' +
' gl_Position = vec4(coordinates, 0.0, 1.0);' +
'}';
-
Biên dịch shader program:
-
Tạo đối tượng shader program bằng createShader().
var vertShader = gl.createShader(gl.VERTEX_SHADER);
Tham số truyền vào là kiểu shader:
- VERTEX_SHADER: tạo vertex shader.
- FRAGMENT_SHADER: tạo fragment shader.
-
Gắn mã nguồn cho các đối tượng shader sử dụng shaderSource().
gl.shaderSource(vertShader, vertCode);
Tham số đầu tiên là shader object mà ta tạo ở trên.
Tham số thứ hai là mã nguồn của shader dưới dạng một string.
-
Biên dịch chương trình sử dụng compileShader().
gl.compileShader(vertShader);
-
Ở trên là code để tạo và biên dịch vertex shader, với fragment shader cũng tạo tương tự chỉ khác ở fragment code.
-
Kết hợp các chương trình lại. Để tạo ứng dụng WebGL ta cần có cả vertex và fragment shader program. Sau khi tạo và biên dịch cả hai chương trình shader,ta cần kết hợp hai shader program bằng các bước sau:
-
Tạo đối tượng program bằng createProgram().
var shaderProgram = gl.createProgram();
-
Gắn các shader program và đối tượng program vừa tạo ở trên bằng attachShader().
gl.attachShader(shaderProgram, vertShader); gl.attachShader(shaderProgram, fragShader);
Tham số đầu tiên là đối tượng program đã được tạo.
Tham số thứ hai là shader program đã được biên dịch.
-
Liên kết các shader sử dụng linkProgram().
gl.linkProgram(shaderProgram);
-
Sử dụng program bằng useProgram().
gl.useProgram(shaderProgram);
-
Kết Luận
Mình vừa giới thiệu ba bước đầu tiên để lập trình một ứng dụng WebGL đó là tạo canvas là lấy WebGL context. Chuẩn bị giữ liệu hình học cho mô hình cần vẽ cùng với đó là các buffer. Cuối cùng là shader program, các chương trình chạy trên GPU giúp render từ các dữ liệu hình học ra các mô hình và vẽ lên WebGL context. Ở phần tiếp theo mình sẽ giới thiệu về cách liên kết giữa các shader program với các data đã lưu trong buffer để tạo ra ứng dụng WebGL, cùng với đó là các ví dụ cơ bản.
Tài Liệu Tham Khảo
All rights reserved