7/14/16

Lâp trình OpenGLES - Phần 4

Sau khi tìm hiểu sơ lượt về shader và vertex trong phần 3, phần tiếp theo này sẽ hướng dẩn các bạn cách vẽ một hình đơn giản bằng opengl. Thực ra để vẽ hình đơn giản này thì bạn không cần dùng đến opengl, chỉ cần sử dụng một thư viện đơn giản như winbgim để vẽ. Nhưng để xây dựng một game đủ lớn với các nhân vật được nạp từ file thì bạn không thể sử dụng winbgim được nữa, lúc này opengl đủ sức để "cân" các nhân vật game này. Trong bài viết này mình chỉ hướng dẩn cách vẽ hình bằng VBO vì nó chỉ cần đẩy dữ liệu vertex đi một lần duy nhất nhờ đó tăng tốc độ vẽ.

Vertex Buffer Object(VBO)

Như bạn đã biết, dữ liệu vertex được lưu trong RAM của hệ thống, khi bạn vẽ hình(gọi các hàm glDrawArray hoặc glDrawElements) thì dữ liệu này sẽ được đẩy sang GPU(RAM của card màng hình). Việc đẩy dữ liệu mỗi lần vẽ hình như thế sẽ làm tăng thời gian, nếu số lượng vertex lớn thì sẽ là một vấn đề nghiêm trọng đấy. Giải pháp đưa ra ở đây đó là chỉ cache dữ liệu trong GPU, vì thế nên ta chỉ cần đẩy vertex đi một lần duy nhất, các lần sau muốn vẽ thì chỉ cần trỏ đến các vertex này thôi. Lợi ích của việc này khá lớn:
  • Nâng cao hiệu suất rendering.
  • Giảm băng thông bộ nhớ.
  • Giảm tiêu hao năng lượng.
Đầu tiên bạn cần tạo một buffer bằng hàm glGenBuffers, hàm này sẽ cung cấp cho bạn một giá trị integer để xác định buffer đã được tạo và sử dụng buffer đó sau này.
GLuint vbo;
glGenBuffers(1, &vbo); // Generate 1 buffer
Bước tiếp theo bạn cần kích hoạt buffer vừa được tạo bằng hàm glBindBuffer để xác định vbo nào được kích hoạt. Tại sao vậy? đơn giản là bạn có thể tạo ra nhiều vbo cho nhiều mục đích khác nhau, và khi bạn muốn sử dụng chúng bạn phải nói cho opengl biết rằng bạn muốn sử dụng vbo nào. Vì thế nên cần phải bind vbo trước khi sử dụng để thông báo vbo vừa bind sẽ được sử dụng.
glBindBuffer(GL_ARRAY_BUFFER, vbo);
Tiếp theo ta cần đẩy dữ liệu vertex tới buffer vừa được tạo. Để làm được điều đó, bạn cần sử dụng hàm glBufferData.
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
Chú ý rằng hàm này nó không tham chiếu đến bất kỳ vbo nào, nhưng thay vào đó nó sẽ tham chiếu đến vbo đang được kích hoạt bởi hàm glBindBuffer. Đối số thứ 1 chúng ta sẽ bàn sau, đối số thứ 2 sẽ là tổng kích thước của vertex cần đẩy đi(bytes). Đối số thứ 3 thì đơn giản nó chính là con trỏ đến vùng nhớ chứa vertex cần đẩy đi. Đối số cuối cùng phụ thuộc vào cách dùng các vertex này như thế nào, cụ thể như sau:
  • GL_STATIC_DRAW: Dữ liệu vertex sẽ được đẩy đi một lần duy nhất và sẽ sử dụng dữ liệu đã đẩy đi này để vẽ nhiều lần sau này.
  • GL_DYNAMIC_DRAW: Dữ liệu vertex sẽ bị thay đổi theo thời gian nhưng mà vẫn được vẽ nhiều hơn. Nghĩa là số lần vẽ nhiều hơn số lần thay đổi vertex.
  • GL_STREAM_DRAW: Số lần thay đổi dữ liệu vertex sẽ nhiều hơn số lần vẽ.
Dựa vào đối số cuối cùng này, opengl sẽ lựa chọn loại bộ nhớ để lưu trữ vertex data trong GPU một cách thích hợp nhất.

Drawing

Sau các bước bên trên, các bạn đã cung cấp dữ liệu vertex cho GPU. Giờ để vẽ hình ra bạn chỉ cần gọi hàm glDrawArrays như sau:
glDrawArrays(GL_TRIANGLES, 0, 3);
Chú ý rằng quá trình vẽ cần được gọi trong sự kiện Draw. Bây giờ ta cũng tìm hiểu về các đối số của hàm glDrawArrays.

  • Đối số đầu tiên xác định loại hình cần vẽ ra như tam giác, tứ giác, đường thẳng....
  • Đối số tiếp theo xác định số lượng vertex cần bỏ qua(tính từ vị trí đầu tiên).
  • Đối số cuối cùng xác định số lượng vertex cần vẽ.
Bây giờ ta sẽ vẽ một hình tam giác màu trắng bằng opengl, các bước làm như sau.
Đầu tiên hàm Init(sẽ được gọi khi sự kiện Init xảy ra) sẽ thực hiện load các shaders, tạo vbo và đẩy các vertex ra GPU đồng thời cấu hình cho positioin attibute của vertex.

Đây mới chỉ là bước chuẩn bị, hình tam giác vẩn chưa hiện ra. Ta cần gọi hàm glDrawArrays để vẽ hình này ra trong hàm Draw(sẽ được gọi khi sự kiện Draw xảy ra).
void Draw()
{
    glDrawArrays(GL_TRIANGLES, 0, 3);
}
Thế là xong, chỉ cần sử dụng 2 hàm Init và Draw là ta đã vẽ được một hình tam giác đơn giản như thế này rồi.
Source code đoạn này tại đây.

Uniform

Hiện tại thì chúng ta mới chỉ vẽ được một hình tam giác màu trắng và màu này được hardcoded ngay trong shader, nhưng nếu ta muốn thay đổi màu của nó trong lúc chạy chương trình thì sao. Các attributes không phải là cách duy nhất để truyền dữ liệu tới shader. Cách khác ở đây chính là sử dụng uniforms. Về cơ bản thì uniforms chỉ là các biến toàn cục và giá trị của chúng được dùng chung cho vertex shader và fragment shader, nó như là biến read-only trong shaders ấy. Để hình dung rỏ hơn về cách thức hoạt động của uniforms ta sẽ đi vào thực hiện ví dụ: Thay đổi màu của hình tam giác theo thời gian.

Ta tiến hành thay đổi 2 điểm trong chương trình đó là fragment shader và main.
Với fragment shader, bạn cần định nghĩa một uniform vec3 để truyền thông tin màu vào cho shader.
precision mediump float;
uniform vec3 u_color;

void main()
{
    gl_FragColor = vec4(u_color, 1.0);
}
u_color sẽ là cầu nối giữa shader và chương trình của ta, phía hàm main bạn cần lấy location của u_color bằng hàm glGetUniformLocation như sau:
GLint u_color = glGetUniformLocation(mProgram, "u_color");
Để đẩy dữ liệu màu cho shader bạn sử dụng hàm glUniform3f như sau:
glUniform3f(u_color, r, b, g);
glUniform3f cho phép bạn truyền 3 giá trị float cho uniform, tương tự nếu bạn muốn truyền 2 giá trị float thì sử dụng glUniform2f...
Nếu bạn muốn thay đổi màu sắt cho tam giác một lần duy nhất thì bạn có thể gọi hàm glUniform3f để truyền dữ liệu màu cho shader ngay tại Init. Trong ví dụ này mình sẽ cho tam giác nháy nháy(blink). Để làm được điều đó trong hàm Update(được gọi khi sự kiện Update xảy ra) bạn tiến hành thay đổi giá trị màu cho tam giác tương tự như sau:
void Update()
{
    long t_now = clock();
    double dental = t_now - t_start;
    glUniform3f(shader->m_u_color, sin(dental * 0.01f), 0.0f, 0.0f);
}
Và đây là kết quả:
Source code đoạn này tại đây.

Attribute

Với uniform bạn hoàn toàn có thể thay đổi màu sắt của toàn bộ tam giác một cách dể dàng. Nhưng bây giờ ta cần mỗi góc của tam giác một màu thì làm sao nhỉ. Giải pháp ở đây chính là attribute, mỗi góc một attribute, mỗi màu riêng.

Để làm được điều này ta lần lượt chỉnh sửa chương trình như sau.
Step 1: Thêm trường color vào struct Vertex để lưu giá trị màu cho mỗi vertex.
struct Vertex 
{
    Vector3 position;
    Vector3 color;// this line
    Vector2 uv;
    Vector3 normal;
};
Step 2: Chỉnh sửa 2 shaders, đối với vertex shader bạn cần thêm 1 attribute để truyền color vào và 1 varying để đẩy giá trị color đó qua cho fragment shader. Phía fragment shader cần khai báo một varying cùng tên để nhận lại giá trị color mà vertex shader vừa đẩy sang.

Step 3: Lấy giá trị location, setting và anable cho color attribute. Bước này khá dể, làm y chan như với position attribute thôi. Chú ý rằng bước lấy giá trị location mình không viết ở Init mà ngay tại Shader class.
Step 4: Thêm màu vào cho các đỉnh của tam giác. Bước này cũng không có gì khó khăng cả, làm như sau:

Và đây là thành quả:
Source code đoạn này tại đây.

Destroying

Sau khi game kết thúc, ta cần phải giải phóng các tài nguyên mà nó đã chiếm giữ như files, bộ nhớ, âm thanh,...Quá trình "thu dọn" diễn ra trong hàm Destroy(sẽ được gọi khi sự kiện destroy xảy ra) một lần duy nhất ứng với khi game kết thúc. Ví dụ như sau:
void Destroy()
{
    MEM_FREE(shader);
}

Như thế là ta vừa kết thúc phần 4 của loạt bài viết về opengl, toàn bộ source code của phần này ở đây: https://github.com/sontx/opengles/tree/day4

0 nhận xét :

Post a Comment