7/14/16

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

Ở phần trước bạn đã tìm hiểu xong về cách tạo một cửa sổ cũng nhu một số sự kiện trong game, đọc lại ở đây. Ở phần này sẽ khái quát qua 2 khái niệm cực kỳ quan trọng trong lập trình opengl đó là shader và vertex. Vậy shader là gì? Cách định nghĩa vertex như thế nào?...tất cả sẽ được giải đáp ngay bên dưới.

Shader

Shader định nghĩa cách xử lý các vertex và fragment trong rendering pipeline. Vì thế tương ứng với việc xử lý vertex hay fragment mà sẽ có 2 loại shaders khác nhau là vertex shader và fragment shader. Shader thực ra là một chương trình nhỏ và bạn có thể lập trình chúng để xử lý các vertex hoặc fragment...theo ý đồ của mình. Ngôn ngử của nó là GLSL(OpenGL Shading Language) dựa trên cú pháp của C.

Trước khi sử dụng shaders bạn cần phải thực hiện 4 bước sau:
  1. Biên dịch shader: shader được viết như một chương trình vì thế nên ta cần biên dịch từ source code trước khi sử dụng chúng. Thường thì bạn sẽ viết shader vào file .vs cho vertex shader và .fs cho fragment shader sau đó biên dịch từ các file này khi cần sử dụng.
  2. Đính shader tới program.
  3. Liên kết program. Cần chỉ rỏ cho opengl biết rằng ta cần sử dụng program nào.
  4. Sử dụng program để gửi những thông tin cần thiết tới cho GPU.
Quá trình làm việc của shaders như sau:
Đầu vào cho vertex shader đó là các attributes, uniforms. Sau khi xử lý các dữ liệu này thì đến lượt fragment shader hoạt động, các trường được khai báo là varying sẽ được pass qua cho fragment shader. Tại đây fragment shader tiếp tục xử lý để cho ra các pixels. Shader có xây dựng sẵn các biến như gl_Position(xác địn vị trí), gl_FragColor(xác định màu)...

Ở đây bạn cần phân biệt rỏ 2 loại dữ liệu là attribute và uniform, chú ý rằng đây là 2 loại dữ liệu duy nhất mà ta có thể gửi đến GPU RAM.
  • Attribute: bạn có thể xem nó như các biến và có thể thay đổi lúc runtime.
  • Uniform: bạn có thể xem nó là hằng số và không thể bị thay đổi.
Đây là quá trình biên dịch từ source code và link vào program của shader.
Như bạn thấy thì game của ta sẽ sử dụng các hàm của đọc file cùng với các hàm của opengl để load, compile và đính vào program.

Đây là một vertex shader đơn giản:

Còn đây là một fragment shader đơn giản:

Cấu trúc chương trình shader khá giống với C, với các trường hợp đơn giản ta chỉ cần sử dụng hàm main để xử lý. Ở đây các biến không được sử dụng sẽ bị remove khi shader biên dịch.

Để nạp một shader bạn làm như sau:

Đầu vào bao gồm 2 thông tin là loại shader(vertex hay fragment) và source code của shader. Source code có thể được nạp từ file cho tiện hoặc bạn có thể hardcode vô luôn cũng được.

Ở đây program không phải là chương trình hay game của ta mà là một chương trình của opengl, mỗi chương trình sẽ gồm một vertex shader và một fragment shader. Hiểu nôm na là một program sẽ quản lý một cặp vertex shader và fragment shader. Để tạo một program trong opengl bạn làm như sau:

Sau khi chương trình link thành công, bạn cần sử dụng hàm glGetAttribLocation để lấy về giá trị location của attribute trong shader và glGetUniformLocation để lấy về giá trị location của uniform trong shader. Sau khi có các giá trị location này rồi thì bạn mới có thể làm việc với attribute/uniform. Ví dụ để lấy location của "a_position" và "a_color" trong shader bạn làm như sau:
GLint position = glGetAttribLocation(programHandle,"a_position");
GLint color    = glGetAttribLocation(programHandle,"a_color");// this return -1
Bạn chú ý rằng biến "a_color" được định nghĩa trong shader nhưng lại trả về -1 khi lấy location vì nó không được sử dụng nên sẻ bị remove lúc compile.

Sau khi lấy được giá trị location bạn cần config cho nó đồng thời enable để có thể sử dụng được(vì mặt định các giá trị này bị disable). Bạn sử dụng lần lượt 2 hàm sau để thiết đặt cho attribute và enable nó:
void glVertexAttribPointer( GLuint index,
    GLint size,
    GLenum type,
    GLboolean normalized,
    GLsizei stride,
    const GLvoid * pointer);

void glEnableVertexAttribArray( GLuint index);
Các đối số của hàm glVertexAttribPointer lần lượt như sau:
  • index: giá trị location mà ta đã lấy được từ hàm glGetAttribLocation ở trên.
  • size: kích thước của attribute. Phải là 1, 2, 3, 4 hoặc GL_BGRA
  • type: loại của attribte như GL_BYTE, GL_INT, GL_FLOAT...
  • normalized: cũng chả biết nó làm gì nữa :|
  • stride: giá trị byte offset giữa 2 vertex attribute liên tiếp.
  • pointer: vị trí của thành phần mà location trỏ tới trong vertex.
Với hàm glEnableVertexAttribArray thì bạn chỉ cần truyền giá trị location cho nó để enable thôi.

Vertex

Mỗi vertex thường có 4 thuộc tính cơ bản đó là:
  • position(x, y, z) hoặc (x, y, z, w). Giá trị w ở trường hợp 2 thì cũng chưa biết tác dụng là gì, đọc trên mạng thấy hoang mang quá :|
  • color(r, g, b) hoặc (a, r, g, b).
  • texture(s, t) hoặc là (u, v).
  • normal vertor(x, y, z).
Đối số cuối cùng của glVertexAttribPointer là pointer cần vị trí của thuộc tính mà location trỏ đến trong vertex. Ví dụ như  vertex của ta chứa full 4 thuộc tính như trên và location trỏ đến color, lúc này ta cần xác định vị trí của color trong "struct" vertex.

Chúng ta có 2 cách lưu một vertex:
  • Lưu position, color, normal trong các mảng độc lập.
  • Tạo struct vertex để lưu chung cho dể nhìn.
Thường thì ta sẽ sử dụng cách 2 kết hợp với load dữ liệu từ file vì với một nhân vật có khá nhiều chi tiết(nhiều vertex cấu thành) thì ta không thể hardcode trong mảng được rồi.
Dưới đây là ví dụ của 2 cách lưu:
Cách 1:
pVertex = { 0, 1, 1, //position of vertex 0 
            2, 4, 2, //position of vertex 1 
            1, 4, 6, //position of vertex 2 
            5, 1, 5  //position of vertex 3 
          }

pTexCor = { 0.1, 0.1, //tex-coord of vertex 0
            0.2, 0.5, //tex-coord of vertex 1
            0.1, 0.7, //tex-coord of vertex 2
            0.9, 0.1  //tex-coord of vertex 3
          }
Cách 2:
struct Vertex
 {
     float x,y,z;     // Vector3 pos
     float u,v;       // Vector2 uv
     float r,g,b,a;   // Vector4 color
     float x, y, z;   // Vector3 normal
}
Bây giờ ta sẽ sử dụng cách 2 để tính toán giá trị một số đối số của hàm glVertexAttribPointer.

  • position bao gồm 3 giá trị float -> tổng = 3 * sizeof(float) = 12 bytes.
  • textcoord bao gồm 2 giá trị float -> tổng = 2 * sizeof(float) = 8 bytes.
  • color bao gồm 4 giá trị float -> tổng = 4 * sizeof(float) = 16 bytes.
  • normal bao gồm 3 giá trị float -> tổng = 3 * sizeof(float) = 12 bytes.
Cuối cùng tổng giá trị của stride = 12 + 8 + 16 + 12 = sizeof(Vertex) = 48.

Như đã nói ở trên thì giá trị pointer chính là vị trí của phần tử property trong vertex.
Tương ứng như hình thì ta sẽ có giá trị pointer lần lượt như sau:
  • position: 0.
  • UV(textcoord): 0 + 12 bytes = 0 + sizeof(Vector3).
  • Color: 0 + 12 bytes + 8 bytes = 0 + sizeof(Vector3) + sizeof(Vector2).
  • Normal: 0 + 12 bytes + 8 bytes + 16 bytes = 0 + sizeof(Vector3) + sizeof(Vector2) + sizeof(Vector4).
Như vậy là bạn đã tìm hiểu khái quát về shader và vertex trong opengl rồi. Cụ thể lập trình shader như thế nào thì mình sẽ hướng dẩn thông qua các ví dụ cụ thể sau này.

Trong project mình sẽ quản lý shader bằng một class là Shader. Bạn cần cung cấp đường dẩn đến vertex shader và fragment shader cho Shader class. Cụ thể sử dụng class Shader này như thế nào, sử dụng ở đây và tác dụng của nó là gì thì mình sẽ đề cập ở phần tiếp theo của loạt bài viết này.

Đây là source code cho bài hôm nay: https://github.com/sontx/opengles/tree/day3

0 nhận xét :

Post a Comment