Hướng Dẫn Làm Game Với OpenGL - Phần 1 : Giới Thiệu OpenGL

I - Mở đầu

Series bài viết này sẽ cung cấp những kiến thức cần thiết để có thể phát triển 1 game trên thư viện Open GL, series gồm 3 phần:

Phần 1: Giới thiệu về Open GL

Phần 2 và 3: làm 1 game demo nhỏ trên Open GL.

II - Giới thiệu chung

Cùng với sự phát triển của các game engine hiện nay, việc làm game đã trở nên dễ dàng và nhanh chóng, tuy nhiên việc sử dụng các game engine sẽ dẫn đến sự lệ thuộc vào công cụ, sức mạnh và hạn chế của những game engine đó. Để thoát khỏi sự lệ thuộc vào hãng thứ 3 và hoàn toàn kiểm soát, tối ưu games của mình, hầu hết các studio games lớn đều tự xây dựng những game engine riêng của mình như idSoftware với Quake Engine, Doom Engine, Valve với Source Engine, hay gần đây có Konami với FoxEngine …

Các bạn có thể tham khảo thông tin về các engine này ở đây:

https://github.com/id-Software/Quake

https://developer.valvesoftware.com/wiki/SDK_Docs

https://www.youtube.com/watch?v=UeDutz5CN2k

Các game engines làm việc với các nền tảng khác nhau từ PC/Mac, các hệ máy consoles PS3, PS4, XBox 360, Xbox One, các thiết bị mobile Android, iOS … mỗi nền tảng lại có hệ thống phần cứng riêng ví dụ như nhân đồ hoạ (GPU) trên 1 PC có thể là của nVidia, AMD, Intel, trên mobile có thể của nVidia, Qualcomn …

Một số thông tin về gpu mời bạn đọc tham khảo ở các site sau:

http://www.nvidia.com/download/index.aspx

http://www.amd.com/en-us

https://www.qualcomm.com/products/snapdragon

Thành phần quan trọng nhất trong 1 game engine là Renderer - module để tạo ra hình ảnh, để làm việc với thiết bị khác nhau, họ phải sử dụng các API Render - những thư viện render chuẩn ví dụ như DirectX hoặc OpenGL … những API Render này trực tiếp làm việc với GPU(qua drivers) giúp chúng ta và phía user(các developers) không cần quan tâm đến những tác vụ ở tầng hardware, tiết kiệm được rất nhiều thời gian, công sức phải làm những công việc lặp lại.

Cả 2 API có những điểm giống, khác nhau, và trong quá trình lịch sử phát triển có sự học hỏi lẫn nhau, để có được một so sánh chung về DirectX và OpenGL bạn đọc có thể tham khảo bài viết khá đầy đủ ở đây:

https://en.wikipedia.org/wiki/Comparison_of_OpenGL_and_Direct3D

về căn bản, mình thấy có những điểm sau là khác biệt sâu sắc nhất, quan trọng nhất:

DirectX OpenGL
Phát triển bởi Microsoft, chỉ sử dụng được cho các hệ máy của Microsoft như PC, Xbox, Windows Phone. Khởi nguồn từ SGI hiện tại là Kronos Group https://en.wikipedia.org/wiki/OpenGL là API crossplatform - sử dụng cho hầu hết các platform hiện nay (PC, Mac, PS4, Steam, iOS, Android ...)
Là 1 phần mềm cài đặt lên trên hệ điều hành. Là 1 chuẩn (specification) để các hãng implement vào thư viện của mình (mỗi hãng có một phiên bản riêng)
Implement bên ngoài drivers (performance được nhiều review đánh giá là chậm hơn) Implement bên trong các drivers (sự khác biệt này dẫn đến cách thức optimize của 2 thư viện cũng khác nhau - kĩ thuật marshalling)
Build trên programming model OOP (hướng đối tượng) Build trên model Functional programming

Các function, feature cho đến thời điểm hiện tại của 2 API khá tương đồng, ở đây chúng ta chỉ tìm hiểu ở mức sử dụng được căn bản nên cũng không cần thiết phải đi sâu thêm.

Về directX - API phát triển bởi Microsoft, bạn đọc muốn tham khảo về directX có thể tìm thấy các tutorials ở đây:

http://www.directxtutorial.com/

Bài viết lần này sẽ chỉ tập trung vào OpenGL - chúng ta sẽ cùng tìm hiểu những kiến thức tối thiểu về OpenGL để có thể phát triển 1 game trong các bài viết tiếp theo.

III - Giới thiệu sơ lược về OpenGL và 3D Graphics

Một số khái niệm cơ bản

OpenGL là gì?

OpenGL là 1 crossplatform API, dùng để render 2D và 3D graphics. API tương tác trực tiếp với GPU (nhân xử lí đồ họa) để tăng tốc render cho phần cứng.

OpenGL định nghĩa, cung cấp các function để draw 2D và 3D Graphics, bao gồm khoảng 250 functions khác nhau.

vd về 1 function

glDrawArrays(GL_TRIANGLES, 0, 3); // draw a triangle by avertice array from 0 - > 3

Hiện nay OpenGL đã phát triển đến phiên bản 4.5, hỗ trợ cho các dòng GPU mới nhất. để xem phiên bản OpenGL mà GPU của bạn hỗ trợ, có thể tải phần mềm sau (dành cho máy Windows):

http://www.geeks3d.com/20130618/gpu-caps-viewer-1-18-1-videocard-information-utility-opengl-opencl-geforce-radeon-gpu/

techblog9_anh1.png

Rendering pipeline là gì?

Đây là 1 khái niệm căn bản trong 3D Graphics, cần hiểu rõ trước khi viết bất kì chương trình 3D nào Renderring pipeline/Graphics pipeline là 1 quá trình gồm 1 chuỗi các bước để tạo ra ảnh bitmap(2D) của 1 scene 3D. Chúng ta biết rằng hình ảnh hiển thị trên màn hình thực chất là 1 ảnh 2 chiều gồm nhiều pixels, tương ứng với số pixels của màn hình (vd 1920x1080 pixels), màn hình có 1 tần số quét(vd 60hz) tương ứng với số lần refresh trong 1s, trong mỗi lần refresh thì ảnh bitmap trên màn hình được cập nhật và chúng ta có được hình ảnh liên tục không bị ngắt quãng. hình ảnh sau cho chúng ta graphics pipeline của OpenGL 4:

pipeline-v4.png

Như trên chúng ta thấy có rất nhiều bước thực hiện trong graphics pipeline tuy nhiên hầu hết các bước chúng ta đều gọi các câu lệnh mà APi cung cấp, điều quan trọng nhất cần quan tâm đó là input & output của quá trình.

Ta có thể thấy input gồm có Input geometry + attributes ( ? ) và output là Framebuffers Input geometry gồm các vertex (đỉnh) của 1 scene

Mỗi scene 3D bao gồm nhiều mô hình 3D, tất cả đều được tạo ra từ các vertex có ví dụ về 1 mô hình như sau:

model_coords.png

Có rất nhiều thuộc tính ta có thể định nghĩa cho 1 vertex, ví dụ như position, color, vector n(pháp tuyến), trong 1 chương trình 3D chúng ta thường thấy 1 đoạn mã định nghĩa vertex như sau:

struct VERTEX { FLOAT X, Y, Z; // position D3DXCOLOR Color; // color }; VERTEX OurVertices[] = { {0.0f, 0.5f, 0.0f, D3DXCOLOR(1.0f, 0.0f, 0.0f, 1.0f)}, {0.45f, -0.5, 0.0f, D3DXCOLOR(0.0f, 1.0f, 0.0f, 1.0f)}, {-0.45f, -0.5f, 0.0f, D3DXCOLOR(0.0f, 0.0f, 1.0f, 1.0f)} };

Các thông tin của vertex được truyền vào API, ví dụ như ở OpenGL chúng ta có lệnh:

static const GLfloat g_vertex_buffer_data[] = { -1.0f, -1.0f, 0.0f, 1.0f, -1.0f, 0.0f, 0.0f, 1.0f, 0.0f, }; ………………… //send our vertices for OpenGL glBufferData(GL_ARRAY_BUFFER, sizeof(g_vertex_buffer_data), g_vertex_buffer_data, GL_STATIC_DRAW);

Dữ liệu vertices sẽ được truyền xuống GPU xử lý bằng 2 chương trình thường được gọi là vertex shader và fragment shader (với DX thì fragment được gọi là pixel shader), đây là 2 chương trình trong card đồ họa có nhiệm vụ tính toán xử lý dữ liệu đầu vào, tuy nhiên chúng ta cần chỉ cho nó cách tính toán, xử lý như thế nào, điều đó dẫn đến việc tạo ra các mã nguồn shader (source code shader) các mã nguồn này chạy trong lúc runtime và cũng được truyền xuống như 1 loại dữ liệu, ví dụ chúng ta có thể tạo ra 2 file như sau:

#version 330 core // vertexshader.shader // Input vertex data, different for all executions of this shader. layout(location = 0) in vec3 vertexPosition_modelspace; void main(){ gl_Position.xyz = vertexPosition_modelspace; gl_Position.w = 1.0; } #version 330 core //fragmentshader.shader // Ouput data out vec3 color; void main() { // Output color = red color = vec3(1,0,0); }

Sau khi thực hiện các bước API tạo cho chúng ta 1 mảng 2 chiều 32 bit (RGBA) lưu trữ các pixel color được gọi là FrameBuffer, GPU thực hiện việc swap buffer và đưa hình ảnh lên hiển thị

Trên đây là những lý thuyết căn bản nhất, chúng ta sẽ cùng tìm hiểu cụ thể thông qua 1 ví dụ kinh điển khi làm việc với DirectX/OpenGL, chương trình vẽ tam giác bằng OpenGL.

IV - Vẽ tam giác sử dụng OpenGL

Ở đây mình sẽ sử dụng IDE phát triển là Visual Studio 2013 trên Windows, việc develop & build trên Mac & Linux có thể thực hiện hoàn toàn tương tự, tuy nhiên do kiến thức về Unix có hạn nên mình sẽ không đi tìm hiểu chi tiết mà dành công việc đó cho bạn đọc.

Để chạy chương trình ta cần 1 số thư viện ngoài cho việc khởi tạo 1 cửa sổ, handle event trên windows và load OpenGL đó là GLFW và GLEW, chúng ta có thể download từ trên trang chủ

http://www.glfw.org/ http://glew.sourceforge.net/

Và include các thư viện này vào project (cách thức include các thư viện này vào Visual Studio bạn đọc có thể tham khảo ở đây với

GLEW http://in2gpu.com/2014/10/15/setting-up-opengl-with-visual-studio/

GLFW làm hoàn toàn tương tự )

Đây là source code của chương trình(Visual Studio 2013) trong đó có chứa các thư viện trong thư mục ../dependencies/, bạn đọc có thể download về tham khảo

https://github.com/TienHP/TechblogSep2015/tree/develop

Sau đây là các file mã nguồn chính

MainSource.cpp ----------------chứa hàm main thực hiên các công việc như khởi tạo, handle event trên windows và vẽ tam giác

Shader.cpp ---------------------load vertext shader và fragment shader source code, compile và truyền cho API

Quan sát MainSource.cpp ta thấy function main thực hiện những công việc chính sau:

int main() // Open a window and create its OpenGL context window = glfwCreateWindow(1024, 768, "Tutorial 02 - Red triangle", NULL, NULL); if (window == NULL){ fprintf(stderr, "Failed to open GLFW window. If you have an Intel GPU, they are not 3.3 compatible. Try the 2.1 version of the tutorials.\n"); glfwTerminate(); return -1; } glfwMakeContextCurrent(window);

=> Công việc của GLFW khởi tạo cho chúng ta 1 cửa sổ window và khởi tạo Context OpenGL cho nó Để kiểm tra công việc load OpenGL và nhận biết các extensions sử dụng cho platform hiện tại chúng ta có thể sử dụng thư viện GLEW như sau:

int main() //================ before // Initialize GLEW glewExperimental = true; // Needed for core profile if (glewInit() != GLEW_OK) { fprintf(stderr, "Failed to initialize GLEW\n"); return -1; }

Bước tiếp theo là cần tạo 1 vertexarray object, đây là đối tượng lưu trữ format data của vertex, chúng ta cần tạo theo code mẫu sau:

GLuint VertexArrayID; glGenVertexArrays(1, &VertexArrayID); glBindVertexArray(VertexArrayID);

Tiếp theo cần định nghĩa tam giác của chúng ta:

static const GLfloat g_vertex_buffer_data[] = { -1.0f, -1.0f, 0.0f, 1.0f, -1.0f, 0.0f, 0.0f, 1.0f, 0.0f, };

Tọa độ các vertex được lưu trong 1 buffer là tọa độ viewport, 1 hệ tọa độ trong đồ họa 3D để định nghĩa vị trí trên 1 viewport, ở đây viewport là toàn bộ cửa sổ do đó tam giác sẽ có dạng như sau:

triangle.png

Bước tiếp theo các lệnh để vẽ tam giác như sau: Tạo 1 vertex buffer, đây là đối tượng chứa vertices data được truyền trực tiếp cho GPU xử lý

// This will identify our vertex buffer GLuint vertexbuffer; // Generate 1 buffer, put the resulting identifier in vertexbuffer glGenBuffers(1, &vertexbuffer); // The following commands will talk about our 'vertexbuffer' buffer glBindBuffer(GL_ARRAY_BUFFER, vertexbuffer); // Give our vertices to OpenGL. glBufferData(GL_ARRAY_BUFFER, sizeof(g_vertex_buffer_data), g_vertex_buffer_data, GL_STATIC_DRAW);

Tiếp theo là câu lệnh main - loop vòng lặp để vẽ tam giác :

do{ // Clear the screen glClear(GL_COLOR_BUFFER_BIT); // Use our shader glUseProgram(programID); // 1rst attribute buffer : vertices glEnableVertexAttribArray(0); glBindBuffer(GL_ARRAY_BUFFER, vertexbuffer); glVertexAttribPointer( 0, // attribute 0. No particular reason for 0, but must match the layout in the shader. 3, // size GL_FLOAT, // type GL_FALSE, // normalized? 0, // stride (void*)0 // array buffer offset ); // Draw the triangle ! glDrawArrays(GL_TRIANGLES, 0, 3); // 3 indices starting at 0 -> 1 triangle glDisableVertexAttribArray(0); // Swap buffers glfwSwapBuffers(window); glfwPollEvents(); } // Check if the ESC key was pressed or the window was closed while (glfwGetKey(window, GLFW_KEY_ESCAPE) != GLFW_PRESS && glfwWindowShouldClose(window) == 0);

Những câu lệnh này thường xuyên lặp lại trong các chương trình OpenGL và ở đây chúng ta chỉ cần chú ý tới 2 function là:

void glVertexAttribPointer(GLuint index, GLint size, GLenum type, GLboolean normalized, GLsizei stride, const GLvoid * pointer);

Function này định nghĩa dữ liệu cho thuộc tính ví dụ thuộc tính ở đây là position có 3 component x, y và z do vậy truyền index = 0 (trùng với trong shader với layout = 0 ta sẽ lấy đó là position), size = 3 với 3 component kiểu GL_FLOAT, và GL_FALSE cho normalized vì đây là vector position không phải vector pháp tuyến (normalized), 2 tham số còn lại tạm thời ko quá quan trọng.

một function quan trọng khác ở đây là

glfwSwapBuffers(window)

ý nghĩa của hàm này là thực hiện việc swap (đổi) giữa front buffer và back buffer để chuyển hình ảnh mới lên monitor.

swapbuffer.png

Công việc tiếp theo đó là Load, Compile Shader và truyền nó cho GPU để xử lý input Như mình đã trình bày ở trên, Shader là 1 chương trình ở trong card đồ họa, nhiệm vụ của nó là xử lý vertices input để tạo ra back buffers, vấn đề của chúng ta là xây dựng function cho việc tính toán xử lý đó. Ở đây chúng ta thấy có 2 file .shader trong project.

#version 330 core // Input vertex data, different for all executions of this shader. layout(location = 0) in vec3 vertexPosition_modelspace; void main(){ gl_Position.xyz = vertexPosition_modelspace; gl_Position.w = 1.0; } #version 330 core // Ouput data out vec3 color; void main() { // Output color = red color = vec3(1,0,0); }

shader trong OpenGL được viết bằng ngôn ngữ GLSL (GL Shader Language) Shader thứ nhất là vertex shader dùng để transform các vertices dòng đầu tiên #version 330 core cho biết chúng ta sử dụng syntax của GLSL version 3 lệnh: layout(location = 0) in vec3 vertexPosition_modelspace; cho biết ta sử dụng layout = 0 cho position của vertex qua từ khóa vertexPosition_modelspace, với layout = 0 position của vertex sẽ được đọc từ index = 0 của vertex trong array ví dụ ở đây size = 3 như vậy 0,1,2 sẽ là của vertex 1 và 3,4,5 của vertex 2 …. gl_Position.w = 1.0 tạm thời không quan trọng bạn đọc muốn tìm hiều có thể đọc thêm ở đây:

https://en.wikipedia.org/wiki/Homogeneous_coordinates

Shader thứ 2 là pixel shader dùng để đổ màu cho mỗi pixel ở đây đơn giản ta muốn màu đỏ cho tất cả các pixel nên gán:

color = vec3(1,0,0);

Công việc cuối cùng là cần load các file shader source này và compile chúng, được thực hiện trong file Shader.cpp, các bước thực hiện như sau

Khởi tạo 2 Shader Object:

// Create the shaders GLuint VertexShaderID = glCreateShader(GL_VERTEX_SHADER); GLuint FragmentShaderID = glCreateShader(GL_FRAGMENT_SHADER); Đọc nội dung 2 file shader std::string VertexShaderCode; std::ifstream VertexShaderStream(vertex_file_path, std::ios::in); if (VertexShaderStream.is_open()){ std::string Line = ""; while (getline(VertexShaderStream, Line)) VertexShaderCode += "\n" + Line; VertexShaderStream.close(); } else{ printf("Impossible to open %s. Are you in the right directory ? Don't forget to read the FAQ !\n", vertex_file_path); getchar(); return 0; } //Compile shader printf("Compiling shader : %s\n", vertex_file_path); char const * VertexSourcePointer = VertexShaderCode.c_str(); glShaderSource(VertexShaderID, 1, &VertexSourcePointer, NULL); glCompileShader(VertexShaderID);

và gắn shader đó vào chương trình(mục đích để truyền shader xuống như 1 dữ liệu thông thường)

glAttachShader(ProgramID, VertexShaderID);

Các bước thực hiện khá đơn giản và dễ hiểu.

Chúng ta Build project bằng lệnh Build => Build Project hoặc click chuột phải vào project => build. Sau đó vào thư mục debug/release để chạy file .exe

(nếu chương trình báo thiếu file glew32.dll bạn đọc vui lòng copy file đó trong thư mục ../dependencies/glew/glew32.dll vào cùng thư mục với file .exe!)

Và kết quả thu được:

final_triangle.png

Trên đây là những kiến thức căn bản nhất về OpenGL và một số hiểu biết của mình về 3D Graphics. Hi vọng bài viết sẽ cung cấp cho các bạn những kiến thức cần thiết để áp dụng trong công việc và để chuẩn bị cho các bài viết sắp tới. Thanks everyone!

Nguồn tham khảo:

https://en.wikipedia.org/wiki/Comparison_of_OpenGL_and_Direct3D https://en.wikipedia.org/wiki/Graphics_pipeline http://www.directxtutorial.com/LessonList.aspx?listid=11 http://www.opengl-tutorial.org/

Từ khóa » Thư Viện Glfw