Tìm hiểu về OpenGL ES 2.0(tiếp)
Bài đăng này đã không được cập nhật trong 3 năm
Ở phần trước chúng ta đã tìm hiểu sơ lược về OpenGL ES là gì và các khái niệm cơ bản của OpenGL ES 2.0 như Vertex Shader, Primitive Assembly, Rasterization, Fragment Shader.Trong phần tiếp theo này chúng ra sẽ tìm hiểu một ví dụ nho nhỏ có tên "Hello Triangle" nhé.
1.Hello Triangle
Dưới đây là mã nguồn đầy đủ cho ví dụ Hello Tam giác mà chúng ta sẽ cùng phân tích.Đối với các bạn đã quen thuộc với thư viện OpenGL trên nền Desktop hay các máy tính để bàn,các bạn có thể bạn sẽ nghĩ rằng có rất nhiều cách, nhiều đoạn mã để làm công việc đơn giản này đó là chỉ vẽ một hình tam giác đơn giản.Mặc dù vậy, OpenGL ES 2.0 hoàn toàn dựa trên cơ chế shader, có nghĩa là bạn không thể vẽ bất kỳ hình học nào mà không dựa trên cơ chế shaders với những ràng buộc và giới hạn thích hợp. Và cũng có nghĩa là chúng ta cần implement một đoạn sourcecode dài hơn để hình ảnh bắt mắt hơn so với việc sử dụng cơ chế "fixed function processing" trên nền desktop OpenGL.Hãy cũng theo dõi đoạn code này và giải thích từng vấn đề nhé:
typedef struct {
// Handle to a program object
GLuint programObject;
} UserData;
///
// Create a shader object, load the shader source, and
// compile the shader.
GLuint LoadShader(const char *shaderSrc, GLenum type) {
GLuint shader;
GLint compiled;
// Create the shader object
shader = glCreateShader(type);
if(shader == 0)
return 0;
// Load the shader source
glShaderSource(shader, 1, &shaderSrc, NULL);
// Compile the shader
glCompileShader(shader);
// Check the compile status
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
if(!compiled) {
GLint infoLen = 0;
glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &infoLen);
if(infoLen > 1) {
char* infoLog = malloc(sizeof(char) * infoLen);
glGetShaderInfoLog(shader, infoLen, NULL, infoLog);
esLogMessage("Error compiling shader:\n%s\n", infoLog);
free(infoLog);
}
glDeleteShader(shader);
return 0;
}
return shader;
}
///
// Initialize the shader and program object
//
int Init(ESContext *esContext) {
UserData *userData = esContext->userData;
GLbyte vShaderStr[] =
"attribute vec4 vPosition; \n"
"void main() { \n"
" gl_Position = vPosition; \n"
"} \n";
GLbyte fShaderStr[] =
"precision mediump float; \n"
"void main() { \n"
" gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); \n"
"} \n";
GLuint vertexShader;
GLuint fragmentShader;
GLuint programObject;
GLint linked;
// Load the vertex/fragment shaders
vertexShader = LoadShader(GL_VERTEX_SHADER, vShaderStr);
fragmentShader = LoadShader(GL_FRAGMENT_SHADER, fShaderStr);
// Create the program object
programObject = glCreateProgram();
if(programObject == 0)
return 0;
glAttachShader(programObject, vertexShader);
glAttachShader(programObject, fragmentShader);
// Bind vPosition to attribute 0
glBindAttribLocation(programObject, 0, "vPosition");
// Link the program
glLinkProgram(programObject);
// Check the link status
glGetProgramiv(programObject, GL_LINK_STATUS, &linked);
if(!linked) {
GLint infoLen = 0;
glGetProgramiv(programObject, GL_INFO_LOG_LENGTH, &infoLen);
if(infoLen > 1) {
char* infoLog = malloc(sizeof(char) * infoLen);
glGetProgramInfoLog(programObject, infoLen, NULL, infoLog);
esLogMessage("Error linking program:\n%s\n", infoLog);
free(infoLog);
}
glDeleteProgram(programObject);
return FALSE;
}
// Store the program object
userData->programObject = programObject;
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
return TRUE;
}
///
// Draw a triangle using the shader pair created in Init()
//
void Draw(ESContext *esContext) {
UserData *userData = esContext->userData;
GLfloat vVertices[] = {0.0f, 0.5f, 0.0f,
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f};
// Set the viewport
glViewport(0, 0, esContext->width, esContext->height);
// Clear the color buffer
glClear(GL_COLOR_BUFFER_BIT);
// Use the program object
glUseProgram(userData->programObject);
// Load the vertex data
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, vVertices);
glEnableVertexAttribArray(0);
glDrawArrays(GL_TRIANGLES, 0, 3);
eglSwapBuffers(esContext->eglDisplay, esContext->eglSurface);
}
int main(int argc, char *argv[]) {
ESContext esContext;
UserData userData;
esInitialize(&esContext);
esContext.userData = &userData;
esCreateWindow(&esContext, "Hello Triangle", 320, 240,
ES_WINDOW_RGB);
if(!Init(&esContext))
return 0;
esRegisterDrawFunc(&esContext, Draw);
esMainLoop(&esContext);
}
2.Building and Running
Chương trình "Hello Triangle" ở trên chạy trên nền trình giả lập "AMD’s OpenGL ES 2.0".Trình giả lập này cung cấp một EGL 1.3 và OpenGL ES 2.0 APIs.Chuẩn GL2 and EGL lần đầu được cung cấp bởi Khronos và được sử dụng như là interface của giả lập.Các giả lập(emulator) như là một full-implement của OpenGL ES 2.0.Điều đó cũng có nghĩa là graphics code được viết trên emulator như được kết nối liền mạch với thiết bị thực.Lưu ý rằng emulator yêu cầu phải có desktop GPU và support cho desktop OpenGL 2.0 API.Build and run đoạn code ở trên với Visual Studio 2005 chúng tasẽ có được kết quả sau
Tiếp theo chúng ta sẽ phân tích xem đoạn code ở trên làm gì nhé
3.Using the OpenGL ES 2.0 Framework
Trong main function của đoạn code trên chúng ta đã gọi một số ES utility functions.Đầu tiên chúng ta đã khai báo ESContext và khởi tạo chúng như sau:
ESContext esContext;
UserData userData;
esInitialize(&esContext);
esContext.userData = &userData;
ESContext xuất hiện ở trong tất cả các ES framework utility functions, nó bao gồm tất các thông tin cần thiết về chương trình mà ES framework cần.Lý do của việc này là ES code framework không cần tới dữ liệu toàn cục(global data).
Nhiều nền tảng trên các thiết bị cầm tay không cho phép việc ứng dụng khai báo các dữ liệu toàn cục tĩnh, chúng ta có thể kể đến các nền tảng như BREW và Symbian(có vẻ hơi cũ rồi nhỉ).Như vậy chúng ta tránh việc khai báo dữ liệu toàn cục trong ví dụ mà ta sử dụng.
ESContext có một biến thành phần(member varialble) có tên là userData .Mỗi một chương trình sẽ lưu trữ một số dữ liệu cần thiết cho chương trình đó trong userData .Hàm esInitialize được gọi bởi ứng dụng để khởi tạo một trạng thái và khởi tạo ES code framework.Một yếu tố khác ở trong cấu trúc của ESContext được miêu tả ở trong header file là dự định chỉ được đọc bởi các ứng dụng người dùng.Dữ liệu khác ở trong cấu trúc của ESContext là độ dài, rộng của cửa sổ ứng dụng(window width, height), trạng thái EGL, con trỏ callback function.
Phần còn lại của main function chịu trách nhiệm tạo ra cửa sổ chương trình, khởi tạo draw callback function và bước vào vòng lặp chính.
esCreateWindow(&esContext, "Hello Triangle", 320, 240, ES_WINDOW_RGB);
if(!Init(&esContext))
return 0;
esRegisterDrawFunc(&esContext, Draw);
esMainLoop(&esContext);
Khi ta gọi hàm esCreateWindow sẽ tạo ra một cửa sổ, như ở trên có kích thước là weight x height = (320x240).Tham số cuối cùng là 1 bít xác định tùy chọn cho việc tạo cửa sổ .Trong trường hợp này tùy chọn của chúng ta là một RGB framebuffer, chúng ta sẽ tìm hiểu kĩ hơn khi nghiên cứu về EGL.Function này sử dụng EGL để khởi tạo một bề mặt trên màn hình được gắn vào một cửa sổ.EGL là một nền tảng độc lập API cho việc dựng hình vào bối cảnh.
Sau khi gọi esCreateWindow function, tiếp theo trong main function chúng ta sẽ gọi Init function để khởi tạo tất cả mọi thứ cần thiết cho việc run ứng dụng.Bước cuối cùng là sử dụng một callback function có tên Draw, nó sẽ được gọi để render ra các frame.esMainLoop được gọi cuối cùng để đi vào xử lí vòng lặp cho đến khi cửa sổ windows được đóng lại
4.Creating a Simple Vertex and Fragment Shader
Trong OpenGL ES 2.0, không thứ gì có thể được vẽ trừ khi vertext và fragment shader được load hợp lệ.Ở trong phần trước chúng ta đã thảo luận cơ bản về OpenGL ES 2.0 pipeline, các khái niệm chung về vertext và fragment.Như vậy để render ra bất kì hình ảnh nào đó trong OpenGL ES 2.0 thì nhất định phải có cả vertex và fragment shader.
Nhiệm vụ lớn nhất của Init function ở trên là load vertext và fragment.Vertext shader được đưa ra rất đơn giản như sau:
GLbyte vShaderStr[] =
"attribute vec4 vPosition; \n;"
"void main() { \n;"
" gl_Position = vPosition; \n;"
"} \n;"
Trong đoạn khai báo shader ở trên có 1 giá trị đầu vào attribute, đó là 1 trong 4 vector thành phần tên vPosition.Sau đó Draw function sẽ gửi các vị trí cho mỗi đỉnh và đặt trong biến này.Shader khai báo một main function và đánh dấu cho sự khởi đầu của việc thực thi shader.Phần thân của shader khá đơn giản, nó copy thuộc tính đầu vào vPosition và0 trong một biến đầu ra đặc biệt có tên là gl_Position.Mỗi vertex shader phải xuất ra một vị trí tương ứng vào trong biến gl_Position.Biến này định nghĩa vị trí sẽ được truyền qua các trạng thái tiếp theo trong đường ống pipeline
Fragment shader trong ví dụ của chúng ta cũng khá dễ hiểu:
GLbyte fShaderStr[] =
"precision mediump float; \n"
"void main() { \n"
" gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); \n"
"} \n;
Trạng thái đầu tiên trong fragment shader khai báo chính xác mặc định cho biến float của shader.Bây giờ chúng ta sẽ chú ý vào main function có giá trị output là (1.0, 0.0, 0.0, 1.0) được gán vào gl_FragColor.Các gl_FragColor là biến built-in đặc biệt nó bao gồm các màu đầu ra cho fragment shader.Trong trường hợp này, shader xuất ra các màu đỏ cho tất cả các fragment.
Trong hầu hết các game hoặc ứng dụng thì shader source sẽ không được nạp trực tiếp trên một dòng như ví dụ trên, thực tế thì chúng ta sẽ nạp vào từ các file text rồi sau đó load vào API.Tuy nhiên để có một ứng dụng đơn giản khép kín chúng ta sử dụng shader source trực tiếp ở trong mã nguỗn chương trình.
5.Compiling and Loading the Shaders
Như vậy là chúng ta đã khai báo shader source, tiếp theo sẽ là việc load shaders vào trong OpenGL ES.LoadShader function ở trong ví dụ trên chịu trách nhiệm cho việc load shader source, compiling, và checking để chắc chắc rằng không có lỗi nào xảy ra.Sau đó nó sẽ trả về một shader objects, một OpenGL ES 2.0 mà sau này có thể được sử dụng để gắn vào một đối tượng chương trình. Hãy cùng xem xét cách mà LoadShader function làm việc.Các đối tượng shader được khởi tạo lần đầu sử dụng glCreateShader.
GLuint LoadShader(GLenum type, const char *shaderSrc) {
GLuint shader;
GLint compiled;
// Create the shader object
shader = glCreateShader(type);
if(shader == 0)
return 0;
Shader source code tự load chính nó vào shader object sử dụng glShaderSource.Sau đó shader được compile sử dụng glCompileShader function.
// Load the shader source
glShaderSource(shader, 1, &shaderSrc, NULL);
// Compile the shader
glCompileShader(shader);
Sau khi compile shader, trạng thái của compile được xác định và xuất ra một số lỗi (nếu có)
// Check the compile status
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
if(!compiled) {
GLint infoLen = 0;
glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &infoLen);
if(infoLen > 1) {
char* infoLog = malloc(sizeof(char) * infoLen);
glGetShaderInfoLog(shader, infoLen, NULL, infoLog);
esLogMessage("Error compiling shader:\n%s\n", infoLog);
free(infoLog);
}
glDeleteShader(shader);
return 0;
}
return shader;
}
Nếu shader được compile thành công, một đối tượng mới shader được trả về và sẵn sàng được gọi trong các đoạn chương trình sau.
6.Creating a Program Object and Linking the Shaders
Một khi ứng dụng đã tạo ra một shader object cho vertex and fragment shader, nó cần phải tạo ra một đối tượng là một chương trình(program object).Đối tượng chương trình ở đây được hiểu là liên kết cuối cùng tới chương trình.Mỗi khi shader được compile vào trong shader object, nó phải được gắn vào một program object và liên kết giữa chúng trước khi được vẽ
Sâu về quá trình tạo ra program objects, và các liên kết chúng ta sẽ tìm hiểu sau, tạm thời chúng ra sẽ phân tích tổng quát ngắn gọn về quá trình này.Bước đầu tiên để tạo ra program object và gắn chúng vào vertex shader và fragment shader.
// Create the program object
programObject = glCreateProgram();
if(programObject == 0)
return 0;
glAttachShader(programObject, vertexShader);
glAttachShader(programObject, fragmentShader);
Khi shader đã được đính kèm, tiếp theo ứng dụng sẽ set vị trí cho thuộc tính vertex shader vPosition:
// Bind vPosition to attribute 0
glBindAttribLocation(programObject, 0, "vPosition");
Chúng ta cần lưu ý rằng khi gọi glBindAttribLocation sẽ bind ra thuộc tính vPosition khai báo ở trong vertex shader tới vị trí 0.Sau đó khi chúng ra xác định dữ liệu vertex, vùng đó được sử dụng để xác định vị trí.Cuối cùng chúng ta đã sẵn sàng để liên kết chương trình và check lỗi phát sinh
// Link the program
glLinkProgram(programObject);
// Check the link status
glGetProgramiv(programObject, GL_LINK_STATUS, &linked);
if(!linked) {
GLint infoLen = 0;
glGetProgramiv(programObject, GL_INFO_LOG_LENGTH, &infoLen);
if(infoLen > 1) {
char* infoLog = malloc(sizeof(char) * infoLen);
glGetProgramInfoLog(programObject, infoLen, NULL, infoLog);
esLogMessage("Error linking program:\n%s\n", infoLog);
free(infoLog);
}
glDeleteProgram(programObject);
return FALSE;
}
// Store the program object
userData->programObject = programObject;
Sau tất cả các bước trên, chúng ta có bước compile shaders cuối cùng, kiểm tra lỗi compile, tạo ra program object, đính kèm shaders, liên kết chương trình và kiểm tra các lỗi liên kết.Sau khi liên kết thành công program object chúng ta có thể sử dụng program object để render, để làm được việc đó chúng ta sẽ binding nó sử dụng glUseProgram
// Use the program object
glUseProgram(userData->programObject);
Sau khi gọi glUseProgram với program object handle, tất cả các bước rendering tiếp theo sẽ sử dụng vertex và fragment shaders kèm theo program object.
7.Setting the Viewport and Clearing the Color Buffer
Bây giờ chúng ta đã render một bề mặt với EGL và khởi tạo, load shaders, chúng ta đã sẵn sàng để có thể vẽ nên các hình ảnh.Hàm Draw callback vẽ nên một frame(khung).Lệnh đầu tiên chúng ra thực thi ở trong Draw là glViewport, lệnh này đã thông báo cho OpenGL ES về nguồn gốc, chiều rộng, chiều cao của bề mặt dựng hình 2D sẽ được render khi nó được vẽ.Trong OpenGL ES khung nhìn định nghĩa hình chữ nhật 2D, trong đó tất cả các hoạt động rendering OpenGL ES cuối cùng sẽ được hiển thị.
// Set the viewport
glViewport(0, 0, esContext->width, esContext->height);
Sau khi cài đặt khung nhìn, bước tiếp theo là clear màn hình.Trong OpenGL ES, có rất nhiều kiểu buffers tham gia vào trong việc dựng hình: màu sắc, độ sâu..Ở trong ví dụ này chúng ta chỉ sử dụng color buffer để vẽ.Sau khi bắt đầu với mỗi frame chúng ta sẽ clear color buffer sử dụng glClear function.
// Clear the color buffer
glClear(GL_COLOR_BUFFER_BIT);
Buffer sẽ clear các màu sắc được chỉ định với glClearColor function.Trong ví dụ ở đoạn cuối của Init function clear color được set là (0.0, 0.0, 0.0, 1.0), vì vậy màn hình sau khi clear sẽ có màu đen.Clear color có thể được thiết lập bởi ứng dụng trước khi gọi glClear function trên bộ color buffer.
Kết luận: hy vọng bài viết phân tích cụ thể mã nguồn của ví dụ mang lại nhiều điều bổ ích cho bạn đọc!
All rights reserved