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ả.
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.
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!
Trả lờiXóaKhông biết có thể có email của tiền bối không, để có thể học hỏi.
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óaKhô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