7/26/16

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

Nối tiếp phần 6 toàn lý thuyết về texture, phần này sẽ hướng dẩn bạn cách sử dụng những kiến thức đó vào code. Chú ý rằng texture, shader hay model đều được mình viết trong các class để dể quản lý sau này.

Textures Objects

Cũng giống với VBOs và VBIs, textures là những objects nên cũng cũng cần được khởi tạo bởi việc gọi hàm của OpenGL.
GLuint tex;
glGenTextures(1, &tex);
Textures thường sử dụng các bức ảnh để "đắp" lên vật thể 3D, nhưng thực ra ta có thể sử dụng nhiều loại dữ liệu khác nhau. Nó có thể là texture 1D, 2D hay 3D. Giống với những objects khác, textures cần phải được liên kết đến trước khi sử dụng.
glBindTexture(GL_TEXTURE_2D, tex);
Bước tiếp theo chúng ta sẽ cần load ảnh texture sử dụng hàm glTextImage2D như sau:
// Black/white checkerboard
float pixels[] = {
    0.0f, 0.0f, 0.0f,   1.0f, 1.0f, 1.0f,
    1.0f, 1.0f, 1.0f,   0.0f, 0.0f, 0.0f
};
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, 2, 2, 0, GL_RGB, GL_FLOAT, pixels);
Đối số thứ 2 xác định mức độ chi tiết của ảnh, 0 nghĩa là ảnh gốc. Đối số thứ 3 xác định loại ảnh của texture, có thể là ảnh RBG, ảnh ARBG,...Đối số thứ 4 và 5 xác định chiều rộng và chiều cao của ảnh. Đối số thứ 6 là border, không biết làm gì nhưng mà phải là 0. Đối số thứ 7 mô tả định dạng pixel, và đối số cuối cùng là con trỏ tới ảnh trong bộ nhớ. Các chi tiết khác bạn có thể xem thêm tại đây.

Texture Image

Vấn đề tiếp theo ở đây là làm sao để nạp được dữ liệu ảnh vào trong bộ nhớ. OpenGL không hổ trợ trực tiếp điều đó, ta phải sử dụng thư viện của bên thứ 3 như SOIL. Trong bài viết này mình sẽ làm việc với định dạng TGA, và sử dụng thư viện của người khác(chưa có tên :v, nó thực chất là 1 file cpp với 2 hàm load file TGA đơn giản mà bạn có thể dể dàng đọc hiểu nếu có thời gian), bạn có thể xem thư viện này trong source code project của bài này ở cuối bài viết(trong TGA.hTGA.cpp). Mình sẽ không tập trung vào "làm cách nào để nạp ảnh vào bộ nhớ" mà chỉ tập trung vào texture. Đây là mục đích của bài hôm nay. Dưới đây là cách sử dụng thư viện TGA:
// load TGA file from file path then store in memory which has been pointed by imageData.
// image width, height and bpp will be store in width, height and bpp variants.
const char * imageData = LoadTGA(mTgaFilePath, width, height, bpp);
Đây là toàn bộ source code để nạp một texture và chỉ cần lưu lại texture id để sử dụng sau này.

Các bước thực hiện như sau:
  1. Đầu tiên ta sẽ load ảnh texture TGA từ file vào bộ nhớ bằng hàm LoadTGA.
  2. Khởi tạo và kích hoạt texture bằng 2 hàm glGenTextures và glBindTexture.
  3. "Đẩy" dữ liệu ảnh texture trong bộ nhớ vào GPU bằng hàm glTexImage2D.
  4. Bước cuối cùng là cấu hình texture như wrapping và filtering.

Shaders

Bây giờ ta cần "lập trình" lại các shaders để nó có thể nhận diện và apply chính xác được texture lên vật thể.

Vertex Shader

Khai báo thêm một attribute để nhận lại các giá trị UV trong vertex shader sau đó chỉ đơn giản là đẩy giá trị này sang cho fragment shader như sau:
attribute vec3 a_position;
attribute vec2 a_uv;

varying vec2 v_uv;

void main()
{
    gl_Position = vec4(a_position, 1.0);
    v_uv = a_uv;
}
Để đẩy dữ liệu sang fragment shader bạn chỉ cần khai báo một varying như trên và truyền attribute cần đẩy sang fragment shader cho biến varying này.

Fragment Shader

Khi vertex shader đẩy dữ liệu UV sang thì ta cần nhận lại dữ liệu này bằng cách định nghĩa một biến varying cùng tên. Ta định nghĩa thêm một uniform kiểu sampler2D, nó có giá trị mặt định là 0. Chúng ta chỉ cần thay đổi giá trị này khi sử dụng nhiều textures. Tạm thời thì ta chỉ cần làm việc với 1 texture. Ta sử dụng tiếp hàm texture để lấy mẫu một pixel từ một texture 2D. Dĩ nhiên bạn sẽ cần cung cấp "id" của texture và "tọa độ" UV trên texture đó để lấy ra màu.
precision mediump float;
uniform sampler2D u_texture;
varying vec2 v_uv;

void main()
{
    gl_FragColor = texture2D(u_texture, v_uv);
}

Using Texture

Bạn vẩn cần phải load Shader Model như ở phần trước bằng 2 class được cung cấp là Shader và Model. Tiếp theo bạn load Texture từ một file TGA bằng Texture class.
shader = new Shader("Basic.vs", "Basic.fs");
shader->Init();
model = new Model("../res/models/Woman2.nfg");
model->Init();
texture = new Texture("../res/textures/Woman2.tga");
texture->Init();
Các tài nguyên đã được nạp vào GPU, bây giờ bạn cần kích hoạt chúng để thông báo cho OpenGL biết rằng bạn muốn sử dụng các tài nguyên này.
glUseProgram(shader->mProgram);
glBindBuffer(GL_ARRAY_BUFFER, model->mVBO);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, model->mIBO);
glBindTexture(GL_TEXTURE_2D, texture->mTextureId);
Bước tiếp theo là cấu hình và kích hoạt các attributes như ở bài trước. Ở đây ta vừa thêm một attribute là a_uv vì thế nên bạn cần cấu hình và kích hoạt attribute này như với a_position.
glVertexAttribPointer(shader->m_a_uv, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void *)offsetof(Vertex, uv));
glEnableVertexAttribArray(shader->m_a_uv);
Xong, bây giờ chỉ cần gọi hàm glDrawElements như ở bài trước nữa là xong.
glDrawElements(GL_TRIANGLES, model->mNumberOfIndices, GL_UNSIGNED_INT, 0);
Mọi thứ vẩn như phần trước, ở đây bạn chỉ phải nạp và kích hoạt texture sau đó cấu hình và kích hoạt thêm một attribute cho texture là a_uv nữa. Đây là hình ảnh sau khi chạy.
Woww, trông đẹp hơn rồi :)) nhưng mà chỉ thấy chân em nó thôi. Để nhìn thấy toàn thân thì chỉ cần thay đổi tọa độ là được. Từ từ rồi ta sẽ bàn về vấn đề này.

Texture Units

Bạn thấy rằng uniform trong fragment shader u_texture luôn có giá trị là 0. Ở đây ta có khái niệm là texture units(thôi thì khỏi dịch, để thế có khi lại hay hơn) là những tham chiếu tới texture objects trong một shader. Bạn có thể hiểu nôm na rằng nó sẽ xác định việc ta sẽ lấy mẫu ở texture nào(vì thực tế ta có thể nạp rất nhiều textures, lúc này cần xác định rỏ texture nào được sử dụng). Texture được liên kết tới texture unit bằng hàm glBindTexture, chúng ta đã sử dụng ở trên.
Hàm glActiveTexture định nghĩa texture unit nào sẽ được liên kết tới texture object khi hàm glBindTexture được gọi. Tổng số lượng texture units được hổ trợ thì khác nhau đối với từng loại card màng hình khác nhau. Nhưng nó ít hơn 48, thật ra thì ngang đây là quá đủ cho chúng ta, có khi cả đời chúng ta cũng không sử dụng hết con số này.

Để hiểu về cách thức hoạt động của texture units, mình sẽ lấy một ví dụ của open.gl như sau.
Ta sẽ "trộn" 2 ảnh(một ảnh con chó và 1 ảnh con mèo) bằng 2 bước như sau.
Đầu tiên ta cần chỉnh sửa fragment shader như bên dưới, 2 giá trị uniform tương ứng cho 2 textures.

Hàm mix được cung cấp sẵn(tương tự như hàm texture). Nó sẽ nội suy tiến tuyến 2 biến đầu dựa trên biến thứ 3. Bạn có thể hiểu nôm na rằng nó sẽ "trộn" 2 giá trị(2 màu) lại với nhau dựa trên 1 chỉ số.
Hai samplers của chúng ta đã được chuẩn bị xong, bây giờ ta sẽ phải gán 2 samplers này tới 2 textures unit và liên kết texture objects tới những texture units này. Đọc thì hơi hại não, xem code sẽ hiểu.

Hàm glUniform1i sẽ đặt texture unit cho sampler, đơn giản là bạn cần xác định location của sampler và truyền cho nó giá trị texture unit. Đây là kết quả.
Source code của đoạn này ở đây: https://open.gl/content/code/c3_multitexture.txt

Xong! vậy là chúng ta đã tìm hiểu sơ lượt về các khái niệm xung quanh texture và cách để load và "đắp" nó lên một vật thể.

Source code cho phần này tại đây: https://github.com/sontx/opengles/tree/day7

3 nhận xét :

  1. A ơi. Seri này hay quá! bào giờ thì có phần 8 vậy a? Cảm ơn a nhiều!!!

    ReplyDelete
  2. mình đổi tọa độ như thế nào để nó load được hết toàn bộ model vậy anh?

    ReplyDelete
  3. mình đổi tọa độ như thế nào để nó hiển thị nguyên hình woman được anh?

    ReplyDelete