Thứ Sáu, 18 tháng 5, 2012

OpenGL Tiếng Việt - Bài 1


OpenGL Tiếng Việt

Đây là bản dịch do mình tự dịch, có nhiều chỗ chưa được chính xác, vì vậy các bạn có thể đọc bài gốc tại http://nehe.gamedev.net/tutorial/creating_an_opengl_window_%28win32%29/13001/
Code minh họa bằng rất nhiều ngôn ngữ phổ biến các bạn cũng vào đó tải luôn.
Lưu ý đây là mình dịch từ trang nehe.gamedev.net . Mình không có tự viết cái gì cả.
Trước khi bắt đầu các bạn nên cài trình phát triển ứng dụng bằng một trong các ngôn ngữ mà họ đưa ra ví dụ ở trong cái link trên kia. Tác giả dùng VC++. Mình thì mình dùng devC++. Bạn nào dùng devC++ thì có thể thiết lập theo như trang http://www.cnt50dh1.net/4r/showthread.php?tid=39 để devC++ dùng được OpenGL.


Bài 1: Tạo cửa sổ OpenGL (Win32)

NOTE #1: Nhiều trình dịch không định nghĩa hằng CDS_FULLSCREEN. Nếu gặp thông báo lỗi biên dịch với CDS_FULLSCREEN thì bạn thêm dòng sau vào đầu chương trình
#define CDS_FULLSCREEN 4.

NOTE #2: Khi tôi viết chương 1 thì GLAUX vẫn xài được. Nhưng sau này thì GLAUX không được hỗ trợ nữa. Nhiều phần của tutorial vẫn dùng GLAUX. Nếu trình dịch của bạn không hỗ trợ GLAUX hoặc bạn không thích dùng thì bạn tải GLAUX REPLACEMENT CODE từ trang chính (menu trái). Trang của NeHe nhé không phải trang này.

Bốn dòng đầu là include các file header:
#include <windows.h>                              // Header File For Windows
#include <gl\gl.h>                                // Header File For The OpenGL32 Library
#include <gl\glu.h>                               // Header File For The GLu32 Library
#include <gl\glaux.h>                             // Header File For The GLaux Library

Tiếp là khai báo các biến dùng trong chương trình. Chương trình này tạo 1 cửa sổ OpenGL trống nên cũng chưa cần phải khai báo nhiều biến. Một vài biến rất là quan trọng và sẽ được dùng trong tất cả các chương trình OpenGL mà bạn viết theo cái khuôn mẫu của loạt bài này.
Dòng đầu khai báo Rendering Context. Tất cả chương trình OpenGL được liên kết tới một Rendering Context. Rendering Context là cái mà liên kết các lời gọi OpenGL với Device Context. Để mà chương trình vẽ vào một cửa sổ thì bạn phải tạo một cái Device Context, cái dòng thứ 2 chính là khai báo biến kiểu handle Device Context. DC kết nối cửa sổ với GDI (Graphics Device Interface). RC kết nối OpenGL với DC.
Dòng thứ 3, biến hWnd lưu handle của cửa sổ của chúng ta. Cuối cùng biến thứ 4 lưu giữ handle của Instance của chương trình.

HGLRC           hRC=NULL;                           // Permanent Rendering Context
HDC             hDC=NULL;                           // Private GDI Device Context
HWND            hWnd=NULL;                          // Holds Our Window Handle
HINSTANCE       hInstance;                          // Holds The Instance Of The Application

Dòng đầu trong các dòng dưới là để khai báo một mảng dùng để quản lý sự kiến nhấn phím. Có nhiều cách để quản lý sự kiện nhấn phím, và đây là cách mà mình xài. Cách này đáng tin phết, với lại có thể nhận biết được NHỮNG phím nào đang nhấn.
Biến active được dùng để lưu trạng thái cửa sổ: đã thu nhỏ xuống taskbar hay là chưa. If the Window has been minimized we can do anything from suspend the code to exit the program. I like to suspend the program. That way it won't keep running in the background when it's minimized.
Biến fullscreen: nếu chạy ở chế độ toàn màn hình, fullscreen sẽ bằng TRUE, nếu chạy ở chế độ cửa sổ thì fullscreen=FALSE. Biến này phải khái báo ở vùng global để các hàm, thủ tục biết được là chương trình đang chạy ở chế độ nào.

bool    keys[256];                              // Array Used For The Keyboard Routine
bool    active=TRUE;                                // Window Active Flag Set To TRUE By Default
bool    fullscreen=TRUE;                            // Fullscreen Flag Set To Fullscreen Mode By Default

Giờ ta phải khai báo hàm WndProc(). Hàm CreateGLWindow() có gọi đến hàm WndProc() nhưng mà hàm WndProc() được đặt phía sau hàm CreateGLWindow(). Trong C, một hàm nếu muốn gọi đến hàm, thủ tục, đoạn code ở phía sau mình thì cái hàm được gọi kia ta phải khai báo ở phía trên.   

LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);               // Declaration For WndProc

Đoạn code tiếp theo làm công việc thay đổi cỡ của OpenGL scene khi cửa sổ bị thay đổi kích cỡ (trong trường hợp chạy ở chế độ cửa sổ). Trong trường hợp mà cửa sổ không thay đổi kích cỡ (ví dụ như lúc chạy ở chế độ toàn màn hình), cái này vẫn được gọi ít nhất một lần khi chương trình lần đầu chạy để thiết lập perspective view. OpenGL scene sẽ được resize dựa vào độ rộng và chiều cao của cửa sổ.              

GLvoid ReSizeGLScene(GLsizei width, GLsizei height)             // Resize And Initialize The GL Window
{
    if (height==0)                              // Prevent A Divide By Zero By
    {
        height=1;                           // Making Height Equal One
    }
    glViewport(0, 0, width, height);                    // Reset The Current Viewport

Dòng tiếp sau đây thiết lập màn hình cho perspective (luật phối cảnh gần xa) view. Có nghĩa là vật càng ở xa thì nhìn nó càng nhỏ. Làm ta có cảm giác thực tế. Luật phối cảnh được tính toán với góc nhìn 45 độ dựa vào chiều rộng và cao của cửa sổ. 0.1f, 100.0f là điểm bắt đầu và kết thúc cho phần độ sâu màn hình mà chúng ta được phép vẽ vào.
glMatricMode(GL_PROJECTION)  chỉ ra rằng 2 dòng tiếp theo sẽ có tác dụng với ma trận hình chiếu. Ma trận hình chiếu chịu trách nhiệm thêm luật phối cảnh gần xa vào scene của chúng ta. glLoadIdentity() giống như kiểu reset. Nó khôi phục ma trận được chọn về trạng thái ban đầu. Sau khi gọi glLoadIdentity() chúng ta thiết lập phối cảnh gần xa cho scene. glMatrixMode(GL_MODELVIEW) chỉ ra rằng bất kỳ sự biến đổi mới nào cũng sẽ tác động vào modelview matrix. Modelview matrix là nơi lưu thông tin đối tượng của chúng ta. Cuối cùng reset cái modelview matric. Phần này mà không hiểu thì cũng không cần lo lắm, cái này tôi sẽ giải thích ở các tutorial sau. Chỉ cần biết là nó PHẢI có thì ta mới có một cái perspective scene (tạm dịch là quang cảnh có luật phối cảnh gần xa) đẹp.

    glMatrixMode(GL_PROJECTION);                        // Select The Projection Matrix
    glLoadIdentity();                           // Reset The Projection Matrix

    // Calculate The Aspect Ratio Of The Window
    gluPerspective(45.0f,(GLfloat)width/(GLfloat)height,0.1f,100.0f);

    glMatrixMode(GL_MODELVIEW);                     // Select The Modelview Matrix
    glLoadIdentity();                           // Reset The Modelview Matrix
}

Đoạn tiếp theo sẽ thiết lập cho OpenGL. Thiết lập màu làm màu xóa cho màn hình, bật depth buffer lên, bật smooth shading lên, v.v... Cái này sẽ không được gọi cho đến khi cửa sổ OpenGL được tạo. Hàm này trả về kết quả nhưng mà cái hàm khởi tạo này của chúng ta nó không phức tạp lắm nên ta chưa cần quan tâm đến giá trị trả về.

int InitGL(GLvoid)                              // All Setup For OpenGL Goes Here
{

Dòng sau bật smooth shading (dịch là làm mịn viền đi :) ). Smooth shading sẽ làm cho màu sắc đẹp hơn across a polygon, và làm mịn ánh sáng. Tôi sẽ trình bày kỹ hơn về smooth shading ở các phần sau.

glShadeModel(GL_SMOOTH);                        // Enables Smooth Shading

Dòng sau thiết lập màu xóa màn hình. Nếu bạn chưa rõ về màu sắc, để tôi nói thêm. Giá trị màu nằm từ 0.0f đến 1.0f. 0.0f là màu tối nhất còn 1.0f là màu sáng nhất. Tham số đầu của hàm glClearColor là mức màu đỏ, thứ 2 là mức màu lục và thứ ba là mức màu lam. Mức màu càng gần 1.0f thì màu càng sáng. Tham số cuối là mức trong suốt. Ta xóa màn hình nên cũng chả cần quan tâm đến tham số thứ 4, cứ để nó bằng 0.0f. Tôi sẽ nói về nó ở phần khác.
Bạn tạo ra các màu bằng cách phối 3 màu cơ bản là (đỏ, lục, lam). Nghĩa là nếu bạn gọi
glClearColor(0.0f,0.0f,1.0f,0.0f) thì màn hình sẽ được xóa bằng màu lam với mức sáng cao. glClearColor(0.5f,0.0f,0.0f,0.0f) -> xóa màn hình bằng màu đỏ tầm trung. Để làm nền đen thì cho tất cả bằng 0.0f, làm nền trắng thì cho tất cả về 0.0f.

glClearColor(0.0f, 0.0f, 0.0f, 0.0f);                   // Black Background

Ba dòng tiếp là ta làm việc với Depth Buffer. Depth buffer như kiểu các tầng, các lớp ở trên màn hình. Depth buffer keeps track of độ sâu của đối tượng đối với màn hình. Trong chương trình này ta không dùng đến depth buffer, nhưng mà tất cả các chương trình OpenGL 3D dùng depth buffer.  It sorts out which object to draw first so that a square you drew behind a circle doesn't end up on top of the circle. Depth buffer là một phần rất quan trọng trong OpenGL.
               
glClearDepth(1.0f);                         // Depth Buffer Setup
glEnable(GL_DEPTH_TEST);                        // Enables Depth Testing
glDepthFunc(GL_LEQUAL);                         // The Type Of Depth Test To Do

Tiếp theo ta bảo với OpenGL là ta muốn có sự phối cảnh xa gần chuẩn nhất. Việc này làm chương trình ngốn tài nguyên máy hơn nhưng mà làm perspective view nhìn đẹp hơn.

glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);          // Really Nice Perspective Calculations

Cuối cùng là return TRUE. Sau này ta viết hàm khởi tạo phức tạp hơn, ta xem nếu khởi tạo không vấn đề gì thì mới trả về TRUE, còn không thì trả về FALSE để các hàm khác gọi nó thì có thể xem giá trị trả về để biết là có lỗi gì không. Giờ thì không cần quan tâm.

    return TRUE;                                // Initialization Went OK
}

Phần code này là nơi mà bạn thực sự vẽ. Tất tật mọi thứ định vẽ lên màn hình là ở phần này. Mỗi bài sau sẽ thêm code vào phần này. Nếu bạn đã hiểu về OpenGL, bạn có thể thử tạo một hình đơn giản bằng cách thêm code vào sau glLoadIdentity() và trước return TRUE. Nếu chưa biết thì đợi bài sau. Giờ ta cứ xóa cái màn hình bằng màu mà ta đã chọn trước đó, xóa depth buffer và reset cái scene. Giờ chưa vẽ gì cả.
return TRUE nói cho chương trình biết là không gặp phải lỗi gì cả. Nếu bạn muốn chương trình dừng lại vì một lý do nào đó, thêm return FALSE vào đâu đó trước dòng return TRUE. Chương trình sau đó sẽ thoát.  

int DrawGLScene(GLvoid)                             // Here's Where We Do All The Drawing
{
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);         // Clear The Screen And The Depth Buffer
    glLoadIdentity();                           // Reset The Current Modelview Matrix
    return TRUE;                                // Everything Went OK
}

Đoạn code tiếp theo được gọi trước khi chương trình thoát ra. Công việc của KillGLWindow() là giải phóng Rendering Context, Device Context và cuối cùng là Window Handle. Tôi đã thêm nhiều đoạn kiểm lỗi. Nếu chương trình không thể hủy bất cứ phần nào của cửa sổ, một hộp thoại thông báo lỗi phựt ra. Thế này giúp bạn tìm lỗi trong chương trình dễ hơn.

GLvoid KillGLWindow(GLvoid)                         // Properly Kill The Window
{

Điều đầu tiên trong KillGLWindow() là kiểm tra xem đang ở chế độ toàn màn hình hay không. Nếu ở chế độ toàn màn hình, ta sẽ chuyển về màn desktop. Ta nên hủy cửa sổ trước khi tắt chế độ toàn màn hình, với một số card đồ họa nếu ta hủy cửa sổ trước khi tắt chế độ toàn màn hình thì desktop sẽ bị lỗi. Thế nên ta tắt chế độ toàn màn hình trước để tránh lỗi desktop.

if (fullscreen)                             // Are We In Fullscreen Mode?
{

Dùng hàm ChangeDisplaySettings(NULL,0) để đưa ta về với desktop gốc. NULL làm tham số đầu, 0 làm tham số thứ 2 sẽ khiến Windows dùng các thiết lập lưu trong registry (độ phân giải, độ sâu màu, tần số, v.v... mặc định). Sau đó ta hiện con trỏ chuột ra.

    ChangeDisplaySettings(NULL,0);                  // If So Switch Back To The Desktop
    ShowCursor(TRUE);                       // Show Mouse Pointer
}

Đoạn code dưới kiểm tra xem ta có Rendering Context không. Nếu không, chương trình sẽ nhảy đến đoạn code dưới kiểm tra xem ta có Device Context không.

if (hRC)                                // Do We Have A Rendering Context?
{

Nếu ta có Rendering Context, đoạn code dưới đây sẽ kiểm tra xem ta có thể giải phóng nó (tách hRC ra khỏi hDC) hay không. Chú ý cách tôi kiểm lỗi. Tôi chỉ đơn giản bảo chương trình giải phóng nó (bằng wglMakeCurrent(NULL,NULL)), sau đó kiểm tra xem giải phóng có thành công hay không.

if (!wglMakeCurrent(NULL,NULL))                 // Are We Able To Release The DC And RC Contexts?
{

Nếu không thể giải phóng DC và RC contexts, MessageBox() sẽ phựt ra thông báo lỗi để cho ta biết không thể giải phóng DC và RC. NULL có nghĩa là hộp thông báo không có cửa sổ cha. Đoạn text bên phải sau NULL là dòng chữ sẽ hiển thị ở hộp thông báo. "SHUTDOWN ERROR" là đoạn tiêu đề hộp thoại. MB_OK, hộp thông báo có dạng hộp thông báo với một nút OK. MB_ICONINFORMATION là để hộp thông báo có cái biểu tượng chữ i.

    MessageBox(NULL,"Release Of DC And RC Failed.","SHUTDOWN ERROR",MB_OK | MB_ICONINFORMATION);
}

Tiếp đến ta xóa Rendering Context. Nếu không thành công thì hộp thoại thông báo hiển thị.

if (!wglDeleteContext(hRC))                 // Are We Able To Delete The RC?
{

Không xóa được Rendering Context -> phựt thông báo lỗi rồi đặt hRC = NULL.

        MessageBox(NULL,"Release Rendering Context Failed.","SHUTDOWN ERROR",MB_OK | MB_ICONINFORMATION);
    }
    hRC=NULL;                           // Set RC To NULL
}

Kiểm tra xem chương trình có Device Context không, nếu có thì giải phóng nó. Nếu không giải phóng được thì phựt thông báo lỗi rồi đặt hDC = NULL.

if (hDC && !ReleaseDC(hWnd,hDC))                    // Are We Able To Release The DC
{
    MessageBox(NULL,"Release Device Context Failed.","SHUTDOWN ERROR",MB_OK | MB_ICONINFORMATION);
    hDC=NULL;                           // Set DC To NULL
}

Kiểm tra Window Handle, hủy cửa sổ bằng hàm DestroyWindow(hWnd). Nếu không hủy được thì phựt thông báo lỗi và hWnd được đặt bằng NULL.

if (hWnd && !DestroyWindow(hWnd))                   // Are We Able To Destroy The Window?
{
    MessageBox(NULL,"Could Not Release hWnd.","SHUTDOWN ERROR",MB_OK | MB_ICONINFORMATION);
    hWnd=NULL;                          // Set hWnd To NULL
}

Cuối cùng là hủy đăng ký Windows Class. Công việc này giúp ta thực sự hủy cửa sổ, như thế thì khi mở lại một cửa sổ khác ta không gặp lỗi "Windows Class already registered" Cái Windows Class này đã được đăng ký trước đó rồi

    if (!UnregisterClass("OpenGL",hInstance))               // Are We Able To Unregister Class
    {
        MessageBox(NULL,"Could Not Unregister Class.","SHUTDOWN ERROR",MB_OK | MB_ICONINFORMATION);
        hInstance=NULL;                         // Set hInstance To NULL
    }
}

Đoạn code tiếp tạo cửa sổ OpenGL. I spent a lot of time trying to decide if I should create a fixed fullscreen Window that doesn't require a lot of extra code, or an easy to customize user friendly Window that requires a lot more code. I decided the user friendly Window with a lot more code would be the best choice. I get asked the following questions all the time in email: How can I create a Window instead of using fullscreen? How do I change the Window's title? How do I change the resolution or pixel format of the Window? The following code does all of that! Therefore it's better learning material and will make writing OpenGL programs of your own a lot easier!
Như bạn thấy hàm trả về kiểu BOOL (TRUE hoặc FALSE), nó nhận 5 tham số: tiêu đề cửa sổ, độ rộng cửa sổ, chiều cao cửa sổ, số bit (16/24/32) và cuối cùng cờ toàn màn hình fullscreenflag, TRUE cho toàn màn hình, FALSE cho chế độ cửa sổ. Ta trả về một giá trị boolean nói cho chương trình biết là cửa sổ được tạo thành công.

BOOL CreateGLWindow(char* title, int width, int height, int bits, bool fullscreenflag)
{

Khi ta yêu cầu Windows tìm cho ta một pixel format như chúng ta muốn, số hiệu của chế độ mà WIndows tìm cho ta sẽ được lưu trong biến PixelFormat.
GLuint      PixelFormat;                        // Holds The Results After Searching For A Match
wc được dùng để lưu Window Class structure.  Window Class structure lưu thông tin về cửa sổ của chúng ta. Bằng việc thay đổi các trường khác nhau trong Class ta có thể thay đổi giao diện của cửa sổ. Cửa sổ nào cũng phải thuộc về một Window Class. Trước khi tạo một cửa sổ, bạn PHẢI đăng ký một Class cho cửa sổ đó.

WNDCLASS    wc;                         // Windows Class Structure

dwExStyle và dwStyle sẽ lưu thông tin về kiểu cửa sổ mở rộng và kiểu cửa sổ bình thường. Tôi dùng các biến lưu kiểu để tôi có thể thay đổi kiểu cửa sổ dựa trên việc tôi muốn tạo kiểu cửa sổ nào (popup window cho fullscreen hay một cửa sổ có viền cho chế dộ cửa sổ)
DWORD       dwExStyle;                      // Window Extended Style
DWORD       dwStyle;                        // Window Style
Năm dòng tiếp là để lưu giá trị trên-trái, dưới-phải của hình vuông. Ta dùng các giá trị này điều chỉnh cửa sổ để vùng ta vẽ có độ phân giải chính xác như ta muốn. Bình thường khi ta tạo cửa sổ 640x480, viền cửa sổ chiếm vài phần.

RECT WindowRect;                            // Grabs Rectangle Upper Left / Lower Right Values
WindowRect.left=(long)0;                        // Set Left Value To 0
WindowRect.right=(long)width;                       // Set Right Value To Requested Width
WindowRect.top=(long)0;                         // Set Top Value To 0
WindowRect.bottom=(long)height;                     // Set Bottom Value To Requested Height
Dòng code tiếp theo ta đặt biến fullscreen bằng fullscreenflag
fullscreen=fullscreenflag;                      // Set The Global Fullscreen Flag

Đoạn code tiếp theo, ta lấy một instance cho cửa sổ của ta, sau đó khai báo Window Class.
Kiểu CS_HREDRAW và CS_VREDROW bắt cửa sổ vẽ lại mỗi lần nó bị chỉnh cỡ. CS_OWNDC tạo DC riêng cho cửa sổ. Có nghĩa DC không được chia sẻ giữa các ứng dụng. WndProc là hàm theo dõi các thông điệp trong chương trình. Ta không dùng dữ liệu cửa sổ mở rộng nên trường thứ 2 ta cho zero. Sau đó ta đặt instance. Tiếp ta đặt hIcon = NULL -> cửa sổ không icon, con trỏ chuột thì ta dùng con trỏ mặt định. Màu nền không thành vấn đề (ta đặt màu nền trong GL). Cửa sổ này không cần menu nên ta đặt nó = NULL, và tên lớp (class) thích đặt là gì cũng được. Ta dùng tên "OpenGL" cho nó đơn giản.

hInstance       = GetModuleHandle(NULL);            // Grab An Instance For Our Window
wc.style        = CS_HREDRAW | CS_VREDRAW | CS_OWNDC;       // Redraw On Move, And Own DC For Window
wc.lpfnWndProc      = (WNDPROC) WndProc;                // WndProc Handles Messages
wc.cbClsExtra       = 0;                        // No Extra Window Data
wc.cbWndExtra       = 0;                        // No Extra Window Data
wc.hInstance        = hInstance;                    // Set The Instance
wc.hIcon        = LoadIcon(NULL, IDI_WINLOGO);          // Load The Default Icon
wc.hCursor      = LoadCursor(NULL, IDC_ARROW);          // Load The Arrow Pointer
wc.hbrBackground    = NULL;                     // No Background Required For GL
wc.lpszMenuName     = NULL;                     // We Don't Want A Menu
wc.lpszClassName    = "OpenGL";                 // Set The Class Name
Giờ ta đăng ký Class. Nếu có lỗi ta phựt thông báo lỗi, click OK xong là thoát chương trình.            
if (!RegisterClass(&wc))                        // Attempt To Register The Window Class
{
    MessageBox(NULL,"Failed To Register The Window Class.","ERROR",MB_OK|MB_ICONEXCLAMATION);
    return FALSE;                           // Exit And Return FALSE
}

Giờ kiểm tra chương trình chạy ở chế độ toàn màn hình hay là chế độ cửa sổ.
               
if (fullscreen)                             // Attempt Fullscreen Mode?
{

Đoạn tiếp theo có vẻ như nhiều người gặp vấn đề với việc chuyển sang chế độ toàn màn hình. Có một vài điều quan trọng bạn cần phải nhớ khi chuyển sang chế độ toàn màn hình. Phải chắc rằng chiều rộng và chiều cao mà bạn dùng trong chế độ toàn màn hình giống với chiều rộng và chiều cao bạn định dùng cho cửa sổ của mình, và quan trọng nhất, đặt chế độ toàn màn hình TRƯỚC KHI bạn tạo cửa sổ. Trong đoạn code này, bạn không cần lo về chiều rộng và chiều dài, cỡ toàn màn hình và cỡ cửa sổ được đặt giống nhau.

DEVMODE dmScreenSettings;                   // Device Mode
memset(&dmScreenSettings,0,sizeof(dmScreenSettings));       // Makes Sure Memory's Cleared
dmScreenSettings.dmSize=sizeof(dmScreenSettings);       // Size Of The Devmode Structure
dmScreenSettings.dmPelsWidth    = width;            // Selected Screen Width
dmScreenSettings.dmPelsHeight   = height;           // Selected Screen Height
dmScreenSettings.dmBitsPerPel   = bits;             // Selected Bits Per Pixel
dmScreenSettings.dmFields=DM_BITSPERPEL|DM_PELSWIDTH|DM_PELSHEIGHT;

Đoạn code trên đây ta xóa phòng để lưu thiết đặt cho hình ảnh. Ta đặt độ rộng, chiều cao và số bit của màn hình mà ta muốn. Đoạn code dưới đây ta yêu cầu chế độ toàn màn hình. Ta lưu tất cả thông tin về độ rộng, chiều cao, số bit trong dmScreenSettings. Dòng dưới dòng ChangeDisplaySettings thử chuyển sang chế độ phù hợp với những gì ta lưu trong dmScreenSettings. Tôi dùng tham số CDS_FULLSCREEN khi thay đổi chế độ, như thế nó sẽ không có cái taskbar ở dưới màn hình với lại nó không dịch chuyển hay đổi cỡ các cửa sổ trên desktop khi bạn chuyển sang chế độ toàn màn hình hay khi chuyển về.
// Try To Set Selected Mode And Get Results.  NOTE: CDS_FULLSCREEN Gets Rid Of Start Bar.
if (ChangeDisplaySettings(&dmScreenSettings,CDS_FULLSCREEN)!=DISP_CHANGE_SUCCESSFUL)
{
Nếu không thiết lập được chế độ, đoạn code dưới đây sẽ thực thi. Nếu không có chế độ toàn màn hình phù hợp, một hộp thoại thông báo phựt ra cho phép bạn có 2 lựa chọn: chạy trong chế độ cửa sổ hay là quit.

// If The Mode Fails, Offer Two Options.  Quit Or Run In A Window.
if (MessageBox(NULL,"The Requested Fullscreen Mode Is Not Supported By\nYour Video Card. Use Windowed Mode Instead?","NeHe GL",MB_YESNO|MB_ICONEXCLAMATION)==IDYES)
{
Nếu người dùng chọn chế độ cửa sổ, biến fullscreen được đặt bằng FALSE, và chương trình tiếp tục chạy.
    fullscreen=FALSE;               // Select Windowed Mode (Fullscreen=FALSE)
}
else
{

Nếu chọn vào quit, hộp thoại thông báo phựt ra bảo là sẽ đóng lại chương trình. FALSE sẽ được trả về để nói cho chương trình biết là không tạo được cửa sổ. Sau đó thì chương trình quit.

            // Pop Up A Message Box Letting User Know The Program Is Closing.
            MessageBox(NULL,"Program Will Now Close.","ERROR",MB_OK|MB_ICONSTOP);
            return FALSE;                   // Exit And Return FALSE
        }
    }
}

Bởi vì đoạn code fullscreen trên đây có thể lỗi và người dùng có thể là sẽ chọn chạy trong chế độ cửa sổ, ta kiểm tra lại xem biến fullscreen là TRUE hay FALSE trước khi thiết lập màn hình/ kiểu cửa sổ.
               
if (fullscreen)                             // Are We Still In Fullscreen Mode?
{

Nếu vẫn ở trong chế độ toàn màn hình ta sẽ đặt kiểu mở rộng là WS_EX_APPWINDOW -> cửa trên cùng thu nhỏ xuống taskbar khi cửa sổ của ta visible. Kiểu cửa sổ là WS_POPUP -> kiểu cửa sổ không viền -> tốt cho chế độ toàn màn hình.
Cuối cùng ẩn chuột. Nếu chương trình mình không xài chuột thì ẩn nó đi thì tốt hơn. Tùy bạn thôi.

    dwExStyle=WS_EX_APPWINDOW;                  // Window Extended Style
    dwStyle=WS_POPUP;                       // Windows Style
    ShowCursor(FALSE);                      // Hide Mouse Pointer
}
else
{

Nếu sử dụng cửa sổ thay vì chế độ toàn màn hình, ta sẽ đặt kiểu mở rộng là

WS_EX_WINDOWEDGE -> nhìn cho nó 3D tí. Kiểu thì là WS_OVERLAPPEDWINDOW thay vì WS_POPUP -> có tiêu đề, có viền có thể dùng chuột kéo chỉnh kích cỡ, có nút minimize/maximize.
    dwExStyle=WS_EX_APPWINDOW | WS_EX_WINDOWEDGE;           // Window Extended Style
    dwStyle=WS_OVERLAPPEDWINDOW;                    // Windows Style
}

Dòng dưới chỉnh cửa sổ phụ thuộc vào kiểu cửa sổ ta tạo. Sự điều chỉnh sẽ làm cửa sổ có độ phân giải như ý. Bình thường viền nó chiếm một phần. Bằng cách dùng lệnh AdjustWindowRectEx không phần nào của scene bị phủ bởi viền, thay vào đó cửa sổ nó sẽ to ra để đủ cho phần viền. Trong chế độ toàn màn hình lệnh này không tác dụng.
AdjustWindowRectEx(&WindowRect, dwStyle, FALSE, dwExStyle);     // Adjust Window To True Requested Size
Đoạn code tiếp theo, ta sẽ tạo cửa sổ và xem nó có gặp lỗi gì không. Gọi CreateWindowEx() và đưa nó các tham số cần thiết. Kiểu mở rộng mà ta chọn dùng. Tên lớp (class) (giống với tên mà bạn dùng khi đăng ký Window Class). Tiêu đề cửa sổ. Kiểu cửa sổ. Vị trí trên-trái cửa cửa sổ. Chiều rộng, chiều cao. Ta không cần cửa sổ cha, ta cũng không cần menu nên ta đặt cả 2 là NULL. Window instance, và cuối cùng là NULL cho tham số cuối.
Chú ý là cho cả kiểu WS_CLIPSIBLINGS và WS_CLIPCHILDREN theo với kiểu cửa sổ mà ta chọn. WS_CLIPSIBLINGS và WS_CLIPCHILDREN là bắt buộc để OpenGL hoạt động. Các kiểu này ngăn cấp các cửa sổ khác vẽ vào cửa sổ OpenGL.
if (!(hWnd=CreateWindowEx(  dwExStyle,              // Extended Style For The Window
                "OpenGL",               // Class Name
                title,                  // Window Title
                WS_CLIPSIBLINGS |           // Required Window Style
                WS_CLIPCHILDREN |           // Required Window Style
                dwStyle,                // Selected Window Style
                0, 0,                   // Window Position
                WindowRect.right-WindowRect.left,   // Calculate Adjusted Window Width
                WindowRect.bottom-WindowRect.top,   // Calculate Adjusted Window Height
                NULL,                   // No Parent Window
                NULL,                   // No Menu
                hInstance,              // Instance
                NULL)))                 // Don't Pass Anything To WM_CREATE
Tiếp ta kiểm tra xem cửa sổ có tạo được không. Nếu tạo được cửa sổ rồi, hWnd sẽ lưu window handle. Nếu không tạo được cửa sổ thì thông báo lỗi phựt ra và chương trình thoát.
{
    KillGLWindow();                         // Reset The Display
    MessageBox(NULL,"Window Creation Error.","ERROR",MB_OK|MB_ICONEXCLAMATION);
    return FALSE;                           // Return FALSE
}
Đoạn code tiếp theo mô tả một Pixel Format. Ta chọn một định dạng hỗ trợ OpenGL và bộ đệm đôi (double buffering), cùng với RGBA (red, green, blue, alpha). Ta tìm một pixel format phù hợp với số bit mà ta chọn (16bit, 24bit, 32bit). Cuối cùng ta thiết lập 16bit Z-Buffer. Các tham số còn lại không sử dụng đến hoặc là không quan trọng (aside from the stencil buffer and the (slow) accumulation buffer).
static  PIXELFORMATDESCRIPTOR pfd=                  // pfd Tells Windows How We Want Things To Be
{
    sizeof(PIXELFORMATDESCRIPTOR),                  // Size Of This Pixel Format Descriptor
    1,                              // Version Number
    PFD_DRAW_TO_WINDOW |                        // Format Must Support Window
    PFD_SUPPORT_OPENGL |                        // Format Must Support OpenGL
    PFD_DOUBLEBUFFER,                       // Must Support Double Buffering
    PFD_TYPE_RGBA,                          // Request An RGBA Format
    bits,                               // Select Our Color Depth
    0, 0, 0, 0, 0, 0,                       // Color Bits Ignored
    0,                              // No Alpha Buffer
    0,                              // Shift Bit Ignored
    0,                              // No Accumulation Buffer
    0, 0, 0, 0,                         // Accumulation Bits Ignored
    16,                             // 16Bit Z-Buffer (Depth Buffer)
    0,                              // No Stencil Buffer
    0,                              // No Auxiliary Buffer
    PFD_MAIN_PLANE,                         // Main Drawing Layer
    0,                              // Reserved
    0, 0, 0                             // Layer Masks Ignored
};

Nếu không có lỗi trong lúc tạo cửa sổ, ta sẽ tạo một OpenGL Device Context. Nếu không tạo được thì một thông báo lỗi phựt ra, và chương trình sẽ quit (return FALSE).
               
if (!(hDC=GetDC(hWnd)))                         // Did We Get A Device Context?
{
    KillGLWindow();                         // Reset The Display
    MessageBox(NULL,"Can't Create A GL Device Context.","ERROR",MB_OK|MB_ICONEXCLAMATION);
    return FALSE;                           // Return FALSE
}
Tiếp là tìm một pixel format phù hợp với cái ta mô tả ở trên. Nếu Windows mà không tìm được thì ta xuất thông báo lỗi rồi quit (return FALSE).
               
if (!(PixelFormat=ChoosePixelFormat(hDC,&pfd)))             // Did Windows Find A Matching Pixel Format?
{
    KillGLWindow();                         // Reset The Display
    MessageBox(NULL,"Can't Find A Suitable PixelFormat.","ERROR",MB_OK|MB_ICONEXCLAMATION);
    return FALSE;                           // Return FALSE
}

Tiếp là thiết lập pixel format. Nếu không được thì lại thông báo lỗi rồi quit (return FALSE).
               
if(!SetPixelFormat(hDC,PixelFormat,&pfd))               // Are We Able To Set The Pixel Format?
{
    KillGLWindow();                         // Reset The Display
    MessageBox(NULL,"Can't Set The PixelFormat.","ERROR",MB_OK|MB_ICONEXCLAMATION);
    return FALSE;                           // Return FALSE
}
Xong xuôi ta lấy một cái Rendering Context. Nếu không lấy được thì báo lỗi rồi quit (return FALSE).
               
if (!(hRC=wglCreateContext(hDC)))                   // Are We Able To Get A Rendering Context?
{
    KillGLWindow();                         // Reset The Display
    MessageBox(NULL,"Can't Create A GL Rendering Context.","ERROR",MB_OK|MB_ICONEXCLAMATION);
    return FALSE;                           // Return FALSE
}
Đến đây mà vẫn không có lỗi gì thì ta active cái Rendering Context. Nếu không active được nó thì thông báo lỗi rồi quit (return FALSE).

if(!wglMakeCurrent(hDC,hRC))                        // Try To Activate The Rendering Context
{
    KillGLWindow();                         // Reset The Display
    MessageBox(NULL,"Can't Activate The GL Rendering Context.","ERROR",MB_OK|MB_ICONEXCLAMATION);
    return FALSE;                           // Return FALSE
}

Đến đây mà không vấn đề gì thì ta hiện cửa sổ ra, set cho nó thành foreground window, setfocus cho nó. Sau đó gọi ReSizeGLScene với tham số rộng, cao để thiết lập perspective OpenGL screen.
               
ShowWindow(hWnd,SW_SHOW);                       // Show The Window
SetForegroundWindow(hWnd);                      // Slightly Higher Priority
SetFocus(hWnd);                             // Sets Keyboard Focus To The Window
ReSizeGLScene(width, height);                       // Set Up Our Perspective GL Screen

Cuối cùng ta InitGL(). Cái này sẽ thiết lập ánh sáng, texture, và bất kỳ cái gì mà cần thiết lập. Bạn có thể viết đoạn kiểm lỗi của riêng bạn ở bên trong hàm InitGL(), và trả về TRUE nếu thấy OK, trả về FALSE nếu thấy sai. Ví dụ, trong quá trình load texture mà gặp lỗi thì ta trả về FALSE cho chương trình nó dừng.
               
if (!InitGL())                              // Initialize Our Newly Created GL Window
{
    KillGLWindow();                         // Reset The Display
    MessageBox(NULL,"Initialization Failed.","ERROR",MB_OK|MB_ICONEXCLAMATION);
    return FALSE;                           // Return FALSE
}

Đến đây thì coi như việc tạo cửa sổ là xong rồi, ta trả về TRUE cho hàm WinMain() để nói rằng WinMain() không gặp lỗi, để chương trình không quit.
               
    return TRUE;                                // Success
}

Đây là nơi mà tất cả thông điệp cửa sổ được truyền vào. Khi ta đăng ký Window Class ta đã bảo Windows là đây là hàm nhận thông điệp cửa sổ.
               
LRESULT CALLBACK WndProc(   HWND    hWnd,                   // Handle For This Window
                UINT    uMsg,                   // Message For This Window
                WPARAM  wParam,                 // Additional Message Information
                LPARAM  lParam)                 // Additional Message Information
{
Đoạn code tiếp đây xem cái thông điệp cửa sổ mà Windows nó gửi vào là gì để ta xử lý.
               
switch (uMsg)                               // Check For Windows Messages
{

uMsg mà là WM_ACTIVE thì ta check xem cửa sổ có đang active không. Nếu cửa sổ đang thu nhỏ ở taskbar thì biến active sẽ là FALSE. Nếu cửa sổ đang active, biến active sẽ là TRUE.

case WM_ACTIVATE:                       // Watch For Window Activate Message
{
    if (!HIWORD(wParam))                    // Check Minimization State
    {
        active=TRUE;                    // Program Is Active
    }
    else
    {
        active=FALSE;                   // Program Is No Longer Active
    }

    return 0;                       // Return To The Message Loop
}

Nếu thông điệp là WM_SYSCOMMAND (system command - lệnh hệ thống) ta sẽ xem xét biến wParam. Nếu wParam = SC_SCREENSAVE hoặc SC_MONITORPOWER - screensaver định chạy hoặc màn hình đang vào chế độ tiết kiệm năng lượng, ta trả về 0 để ngăn cấm việc này.

case WM_SYSCOMMAND:                     // Intercept System Commands
{
    switch (wParam)                     // Check System Calls
    {
        case SC_SCREENSAVE:             // Screensaver Trying To Start?
        case SC_MONITORPOWER:               // Monitor Trying To Enter Powersave?
        return 0;                   // Prevent From Happening
    }
    break;                          // Exit
}

Nếu uMsg là WM_CLOSE có nghĩa là cửa sổ bị đóng. Ta gửi ra thông điệp thoát mà vòng lặp chính sẽ intercept. Biến done được set bằng TRUE, vòng lặp chính ở trong WinMain sẽ dừng và chương trình sẽ thoát.
               
case WM_CLOSE:                          // Did We Receive A Close Message?
{
    PostQuitMessage(0);                 // Send A Quit Message
    return 0;                       // Jump Back
}

Nếu là có phím đang được nhấn ta đọc wParam để biết nó là phím nào. Phần tử mảng keys[] tương ứng sẽ được đặt bằng TRUE. Theo cách đó ta sau này có thể đọc cái mảng đấy để biết được những phím nào đang được nhấn. Cách này cho phép ta nhấn nhiều phím cùng lúc.

case WM_KEYDOWN:                        // Is A Key Being Held Down?
{
    keys[wParam] = TRUE;                    // If So, Mark It As TRUE
    return 0;                       // Jump Back
}

Nếu thấy phím nào được nhả ra thì ta lại đọc wParam rồi đặt phần tử mảng tương ứng với phím đó là FALSE. Mỗi phím trên bàn phím được định bởi 1 số từ 0 đến 255. Khi ta nhấn 1 phím có mã 40 chẳng hạn, keys[40] sẽ được đặt bằng TRUE. Khi ta nhả ra, keys[40] sẽ được đặt bằng FALSE.

case WM_KEYUP:                          // Has A Key Been Released?
{
    keys[wParam] = FALSE;                   // If So, Mark It As FALSE
    return 0;                       // Jump Back
}

Khi ta chỉnh cỡ cửa sổ, thông điệp uMsg sẽ là WM_SIZE. Ta đọc LOWORD (từ thấp) và HIWORD (từ cao) của biến LParam để xem chiều rộng với lại chiều cao. Ta gọi hàm ReSizeGLScene và truyền vào chiều rộng, chiều cao mới để OpenGL Scene được chỉnh về cỡ mới.
               
    case WM_SIZE:                           // Resize The OpenGL Window
    {
        ReSizeGLScene(LOWORD(lParam),HIWORD(lParam));       // LoWord=Width, HiWord=Height
        return 0;                       // Jump Back
    }
}
Tất cả các thông điệp mà ta không quan tâm tới thì ta truyền cho hàm DefWindowProc để Windows xử lý nó.
               
    // Pass All Unhandled Messages To DefWindowProc
    return DefWindowProc(hWnd,uMsg,wParam,lParam);
}

Đây là tâm điểm của chương trình Windows. Đây là nơi mà chúng ta gọi hàm tạo cửa sổ, xử lý thông điệp cửa sổ và tương tác với người dùng.
               
int WINAPI WinMain( HINSTANCE   hInstance,              // Instance
            HINSTANCE   hPrevInstance,              // Previous Instance
            LPSTR       lpCmdLine,              // Command Line Parameters
            int     nCmdShow)               // Window Show State
{

Ta khai báo 2 biến. msg dùng để kiểm tra xem có waitng message nào cần xử lý. Biến done khởi tạo bằng FALSE -> chương trình vẫn đang chạy. Khi mà done vẫn bằng FALSE thì chương trình vẫn chạy, khi nó bị đổi thành TRUE, chương trình sẽ thoát.

MSG msg;                                // Windows Message Structure
BOOL    done=FALSE;                         // Bool Variable To Exit Loop

Phần code này là tùy chọn. Nó hiện thông báo lên hỏi thích chạy ở chế độ toàn màn hình không. Nếu người dùng nhấn vào nút NO, biến fullscreen sẽ được đặt = FALSE và chương trình sẽ chạy ở chế độ cửa sổ thay vì chế độ fullscreen.
               
// Ask The User Which Screen Mode They Prefer
if (MessageBox(NULL,"Would You Like To Run In Fullscreen Mode?", "Start FullScreen?",MB_YESNO|MB_ICONQUESTION)==IDNO)
{
    fullscreen=FALSE;                       // Windowed Mode
}

Tạo cửa sổ OpenGL. Truyền vào tham số tiêu đề, độ rộng, chiều cao, độ sâu màu, và TRUE/FALSE cho chế độ màn hình. Nếu có cái lỗi gì xảy ra thì hàm nó trả về FALSE, chương trình ngay lập tức sẽ thoát.
               
// Create Our OpenGL Window
if (!CreateGLWindow("NeHe's OpenGL Framework",640,480,16,fullscreen))
{
    return 0;                           // Quit If Window Was Not Created
}

Bắt đầu vòng lặp. Lặp cho đến khi done nó bằng TRUE.
               
while(!done)                                // Loop That Runs Until done=TRUE
{

Đâu tiên là phải kiểm tra xem có thông điệp nào đang đợi được xử lý không. Sử dụng hàm PeekMessage() ta sẽ kiểm tra được mà không cần phải dừng chương trình. Rất nhiều chương trình dùng hàm GetMessage(). Nó hoạt động tốt, nhưng với GetMessage() chương trình của bạn sẽ không làm gì cho đến khi nó nhận được sự kiện paint (vẽ lại) hoặc thông điệp cửa sổ khác.
               
if (PeekMessage(&msg,NULL,0,0,PM_REMOVE))           // Is There A Message Waiting?
{

Đoạn code tiếp theo ta kiểm tra xem thông điệp gửi đến có phải là thông điệp quit chương trình không. Nếu thông điệp gửi đến là WM_QUIT được gây ra bởi PostQuitMessage(0) thì biến done được được đặt bằng TRUE, chương trình sẽ thoát.

               
if (msg.message==WM_QUIT)               // Have We Received A Quit Message?
{
    done=TRUE;                  // If So done=TRUE
}
else                            // If Not, Deal With Window Messages
{

Nếu không phải thông điệp quit thì ta translate thông điệp sau đó dispatch thông điệp để hàm WndProc() hoặc Windows nó xử lý.
               
        TranslateMessage(&msg);             // Translate The Message
        DispatchMessage(&msg);              // Dispatch The Message
    }
}
else                                // If There Are No Messages
{

Nếu không còn thông điệp nào thì ta sẽ vẽ OpenGL scene. Dòng code đầu kiểm tra xem cửa sổ có active không. Nếu phím ESC được nhấn thì biến done được đặt bằng TRUE để chương trình nó thoát.
               
// Draw The Scene.  Watch For ESC Key And Quit Messages From DrawGLScene()
if (active)                     // Program Active?
{
    if (keys[VK_ESCAPE])                // Was ESC Pressed?
    {
        done=TRUE;              // ESC Signalled A Quit
    }
    else                        // Not Time To Quit, Update Screen
    {

Nếu chương trình đang active và phím esc không được nhấn thì ta render cái scene và swap cái buffer (bằng cách dùng double buffernig ta sẽ có được smooth flicker free animation). Bằng việc sử dụng double buffering, ta vẽ mọi thứ vào màn hình ẩn. Khi ta swap (tráo đổi) buffer, màn hình ta thấy sẽ bị ẩn đi, màn hình ẩn sẽ hiện ra. Theo cách này ta sẽ không phải thấy cái cảnh scene bị vẽ lên mà sau khi vẽ xong rồi thì nó mới hiện ra.
               
        DrawGLScene();              // Draw The Scene
        SwapBuffers(hDC);           // Swap Buffers (Double Buffering)
    }
}

Đoạn tiếp theo mới được thêm vào cách đây không lâu (05-01-00). Nó cho phép ta nhấn F1 để chuyển từ chế độ toàn màn hình sáng chế độ cửa sổ và ngược lại.
               
        if (keys[VK_F1])                    // Is F1 Being Pressed?
        {
            keys[VK_F1]=FALSE;              // If So Make Key FALSE
            KillGLWindow();                 // Kill Our Current Window
            fullscreen=!fullscreen;             // Toggle Fullscreen / Windowed Mode
            // Recreate Our OpenGL Window
            if (!CreateGLWindow("NeHe's OpenGL Framework",640,480,16,fullscreen))
            {
                return 0;               // Quit If Window Was Not Created
            }
        }
    }
}

Nếu biến done không còn là FALSE nữa, chương trình sẽ thoát. Ta kill cái cửa sổ OpenGL để mọi thứ được giải phóng, sau đó là thoát.
               
    // Shutdown
    KillGLWindow();                             // Kill The Window
    return (msg.wParam);                            // Exit The Program
}


In this tutorial I have tried to explain in as much detail, every step involved in setting up, and creating a fullscreen OpenGL program of your own, that will exit when the ESC key is pressed and monitor if the window is active or not. I've spent roughly 2 weeks writing the code, one week fixing bugs & talking with programming gurus, and 2 days (roughly 22 hours writing this HTML file). If you have comments or questions please email me. If you feel I have incorrectly commented something or that the code could be done better in some sections, please let me know. I want to make the best OpenGL tutorials I can and I'm interested in hearing your feedback.

3 nhận xét:

  1. Mặc dù không phải là tác giả (đúng ra) của bài viết này, nhưng việc chủ thread làm cho cộng đồng rất đáng hưởng ứng. Dù nhận xét này từ 2 năm sau. Cám ơn!
    Không biết có thể có email của tiền bối không, để có thể học hỏi.

    Trả lờiXóa
  2. Cám ơn bạn rất nhiều. Nếu được phép mình sẽ tạo một mục riêng đăng lên 4mghc.com. Quá hay.

    Trả lờiXóa
  3. Không thể không bình luận nữa. Đáng lý phải có lời cám ơn từ hai năm trước để cổ vũ tinh thần vì cộng động của tác giả. Tôi là fan của tác giả blog này.

    Trả lờiXóa