在Windows初始化OpenGL大概像這兩篇各取一部分合在一起,這兩篇講過的觀念就不贅述了。
【程式】Direct3D 11初始化
【程式】OpenGL 3.3初始化(X Window)
筆者的程式教學一覽
想用D3D和OpenGL的功能,除了顯卡和驅動程式以外,作業系統API也要配合,這個觀念講過很多次了。
Windows內建的OpenGL API一直停留在1.1版(原因……筆者也不知道),想用1.2以後的功能就得照X Window篇提過的手動取出函式指標,Windows用來取得函式指標的函式是wglGetProcAddress()。
使用VC的話,Windows SDK裡的header也只有OpenGL 1.1的函式和常數,寫程式前要做一點前置工作
OpenGL on Windows初始化比在X Window麻煩很多。wglGetProcAddress()必須先建立一個OpenGL context才能使用(X Window的glXGetProcAddress不需要這麼做),可是想建立core profile context要用到wglGetProcAddress()取出的函式,變成循環依存。
再加上後述的SetPixelFormat(),過程中必須呼叫這個函式,但一個視窗只能使用一次,如果建第一個context的時候對主視窗使用,第二個context就不能用了(會傳回0代表沒有成功)。
因此要這樣做:
1. 建立一個dummy window。
2. 利用dummy window建立一個dummy context。
3. 用wglGetProcAddress()取得必要的函式指標。
4. 把dummy context和dummy window刪除。
5. 建立實際要用的context。
參考OpenGL wiki的這一篇
Creating an OpenGL Context (WGL)
大部分跟「Direct3D 11初始化」相同,事件處理和Sleep()怎麼用就請參照那篇。
主要改變的只有一個地方,建立Window class的時候增加「wndclass.style=CS_OWNDC;」這一行,DC=device context,是Windows的一個系統:GDI的東西,主要用在繪製視窗,有興趣的話就自己研究一下GDI是什麼。
(「如何建一個視窗—Windows API篇」有講到,視窗不只是有標題列、有最小化和關閉按鈕的那種東西,所有GUI元件都是HWND型態)
要加CS_OWNDC的原因據說是95、98的時代由於記憶體和CPU速度很吃緊,DC並不是每個視窗都配置一個,而是由作業系統準備若干個重覆使用,每次繪圖時呼叫GetDC()取得一個,畫完後立刻呼叫ReleaseDC()釋放,但OpenGL程式必須取得DC之後將它一直保留,要加CS_OWNDC叫Windows把這個視窗視為特例。現在的硬體比較沒有資源限制,但各地方的教學還是建議你加上CS_OWNDC。
本篇最複雜的部分,如一開始所說要建一個dummy window和兩個context。
建dummy window用最省事的方法:建立成主視窗的子視窗,位置大小隨便設。其他方法如建立另一個主視窗,或建立記憶體內的點陣圖DC都比較麻煩。
視窗程式的教學還沒有寫到建子視窗和UI元件的方法,本篇是第一次用到,CreateWindow()第一參數決定建立何種元件,第八參數填親視窗,詳細用法等哪天寫到視窗元件的教學再介紹。
在Windows使用OpenGL需要取得這個視窗的DC,用GetDC()取得。
第一個context使用Windows內建API建立,是比較舊的方法,用struct PIXELFORMATDESCRIPTOR指定framebuffer格式,由於是dummy context,格式隨便填一個所有電腦都支援的。
詳細用法看MSDN的文件。
PIXELFORMATDESCRIPTOR說明
ChoosePixelFormat()說明
再來用wglGetProcAddress()取得三個函式指標,前兩個是初始化會用到,wglSwapIntervalEXT是用來設vsync。
接下來建立真正要用的context,跟「OpenGL 3.3初始化(X Window)」一樣使用0結束的陣列設定格式。
所有能填的值與函式的用法請看擴充功能的說明,學OpenGL多少要會看擴充功能的文件。
WGL_ARB_pixel_format說明
WGL_ARB_create_context說明
兩種choose pixel format方法的差別是,PIXELFORMATDESCRIPTOR+ChoosePixelFormat()不能支援1.2版以後追加的功能,multisample之類的功能要用陣列+wglChoosePixelFormatARB()才做得到。
最後一個工作:設vsync,X Window篇有提到OpenGL可以調整系統設定強制開或關vsync,且OpenGL程式最好明確呼叫函式設定vsync,在Windows也一樣。
swap buffer的函式名稱改成SwapBuffers(),其餘跟X Window篇一樣,之前用GetDC()取得的DC在這裡會用到。這裡可看出為何說OpenGL是跨平台,除了初始化、結束和swap buffer以外,其餘操作的程式碼在各平台都一樣。
刪除的步驟跟X Window篇不一樣,處理完WM_DESTROY訊息之後視窗就被刪除了,但之後呼叫SwapBuffers()也不會讓程式掛掉,只會傳回0代表失敗。如果在意這個傳回值也可以改在WM_CLOSE裡呼叫PostQuitMessage(0),刪除OpenGL context後再呼叫DestroyWindow()刪視窗。
至於釋放DC,這個視窗有CS_OWNDC屬性,視窗被刪除時也會把DC刪除,不用呼叫ReleaseDC()。
假設檔名是simplegl_w.c,用這個指令build
執行的樣子,順便把pixel format印出來看看。
pixel format類似X Window的FBConfig,每個號碼代表什麼意思要用看OpenGL資訊的軟體看,例如這是OpenGL Extension Viewer,左邊選「Display modes & pixel formats」。
右下列出各種framebuffer格式:有沒有depth buffer和stencil buffer、有沒有multisample等等。
OpenGL on Windows的教學只會寫到這一篇而已。我的前兩款自製遊戲:Monster Musume Hunter和Cyber Sprite在W、L兩平台都用OpenGL,隨後我在Windows把D3D和OpenGL都試過一次,發現Windows的OpenGL驅動程式經常沒有認真寫,常常效能比較差或是有bug,所以決定之後作品的Windows版改用Direct3D了。以後筆者的OpenGL範例程式都會在Linux製作,想在Windows用OpenGL的人請自行變通一下。
【程式】Direct3D 11初始化
【程式】OpenGL 3.3初始化(X Window)
筆者的程式教學一覽
想用D3D和OpenGL的功能,除了顯卡和驅動程式以外,作業系統API也要配合,這個觀念講過很多次了。
Windows內建的OpenGL API一直停留在1.1版(原因……筆者也不知道),想用1.2以後的功能就得照X Window篇提過的手動取出函式指標,Windows用來取得函式指標的函式是wglGetProcAddress()。
使用VC的話,Windows SDK裡的header也只有OpenGL 1.1的函式和常數,寫程式前要做一點前置工作
- 去Khronos Group的網站,下載glext.h、wglext.h、khrplatform.h這三個檔案。
https://www.khronos.org/registry/OpenGL/index_gl.php - 將這三個檔放在適當地方
方法一:放在Windows SDK資料夾,這樣你寫的所有程式都可以使用。
例如筆者目前用的VC 2013,把glext.h和wglext.h放在這裡。其他VC版本把8.1換成其他數字,自己找一下。C:\Program Files (x86)\Windows Kits\8.1\Include\um\gl\
然後要建一個KHR資料夾把khrplatform.h放在裡面,如下,因為glext.h裡有一行「#include <KHR/khrplatform.h>」。C:\Program Files (x86)\Windows Kits\8.1\Include\um\KHR\khrplatform.h
方法二:跟C程式碼放在同一資料夾,這個方法只有同一資料夾的.c檔可以用。
然後打開glext.h找到「#include <KHR/khrplatform.h>」這一行,將它改成「#include "khrplatform.h"」。本來是從標準include資料夾讀取,改成從glext.h所在資料夾讀取。
OpenGL on Windows初始化比在X Window麻煩很多。wglGetProcAddress()必須先建立一個OpenGL context才能使用(X Window的glXGetProcAddress不需要這麼做),可是想建立core profile context要用到wglGetProcAddress()取出的函式,變成循環依存。
再加上後述的SetPixelFormat(),過程中必須呼叫這個函式,但一個視窗只能使用一次,如果建第一個context的時候對主視窗使用,第二個context就不能用了(會傳回0代表沒有成功)。
因此要這樣做:
1. 建立一個dummy window。
2. 利用dummy window建立一個dummy context。
3. 用wglGetProcAddress()取得必要的函式指標。
4. 把dummy context和dummy window刪除。
5. 建立實際要用的context。
參考OpenGL wiki的這一篇
Creating an OpenGL Context (WGL)
#define UNICODE #include<windows.h> #include<GL/gl.h> #include<GL/glext.h> #include<GL/wglext.h> #include<stdio.h> const int WINDOW_W=200, WINDOW_H=200; //OpenGL必要物件 HDC hdc; HGLRC hglrc; //畫面顏色 float color[]={0,0,0,1}; //這三個函式在下面說明 static int initGL(HWND window){ …… } static void nextFrame(){ …… } static void deinitGL(){ …… } LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam){ switch(message){ case WM_DESTROY: PostQuitMessage(0); //跳出訊息迴圈 return 0; } return DefWindowProc(hwnd,message,wparam,lparam); } int main(){ WNDCLASS wndclass; ZeroMemory(&wndclass, sizeof(WNDCLASS)); wndclass.lpfnWndProc = WndProc; wndclass.hCursor = LoadCursor(NULL, IDC_ARROW); wndclass.lpszClassName = L"window"; wndclass.style = CS_OWNDC; //增加這一行 RegisterClass(&wndclass); RECT rect={0,0,WINDOW_W,WINDOW_H}; AdjustWindowRect(&rect, WS_CAPTION|WS_SYSMENU|WS_VISIBLE, 0); HWND window=CreateWindow(L"window", L"title", WS_OVERLAPPED|WS_SYSMENU|WS_VISIBLE,CW_USEDEFAULT,CW_USEDEFAULT, rect.right-rect.left, rect.bottom-rect.top, NULL,NULL,NULL,NULL); if(initGL(window)){ printf("Can not initialize OpenGL\n"); return 0; } //開始訊息迴圈 timeBeginPeriod(1); MSG msg; int isEnd=0; while(!isEnd){ while(PeekMessage(&msg,NULL,0,0,PM_REMOVE)){ if(msg.message==WM_QUIT){ isEnd=1; } DispatchMessage(&msg); } //正式寫遊戲時,遊戲邏輯放在此處 nextFrame(); //更新畫面 Sleep(16); } timeEndPeriod(1); deinitGL(); return 0; } |
大部分跟「Direct3D 11初始化」相同,事件處理和Sleep()怎麼用就請參照那篇。
主要改變的只有一個地方,建立Window class的時候增加「wndclass.style=CS_OWNDC;」這一行,DC=device context,是Windows的一個系統:GDI的東西,主要用在繪製視窗,有興趣的話就自己研究一下GDI是什麼。
(「如何建一個視窗—Windows API篇」有講到,視窗不只是有標題列、有最小化和關閉按鈕的那種東西,所有GUI元件都是HWND型態)
要加CS_OWNDC的原因據說是95、98的時代由於記憶體和CPU速度很吃緊,DC並不是每個視窗都配置一個,而是由作業系統準備若干個重覆使用,每次繪圖時呼叫GetDC()取得一個,畫完後立刻呼叫ReleaseDC()釋放,但OpenGL程式必須取得DC之後將它一直保留,要加CS_OWNDC叫Windows把這個視窗視為特例。現在的硬體比較沒有資源限制,但各地方的教學還是建議你加上CS_OWNDC。
//傳回0代表成功,非0代表失敗 static int initOpenGL(HWND window){ //dummy context //建立子視窗 HWND dummyWindow=CreateWindow(L"STATIC",L"",WS_CHILD,0,0,1,1, window,NULL,NULL,NULL); HDC dummyDC=GetDC(dummyWindow); PIXELFORMATDESCRIPTOR pfd; ZeroMemory(&pfd, sizeof(PIXELFORMATDESCRIPTOR)); pfd.nSize=sizeof(PIXELFORMATDESCRIPTOR); pfd.nVersion=1; pfd.dwFlags=PFD_DRAW_TO_WINDOW|PFD_SUPPORT_OPENGL|PFD_DOUBLEBUFFER; pfd.iPixelType=PFD_TYPE_RGBA; pfd.cColorBits=24; pfd.iLayerType=PFD_MAIN_PLANE; int pixelFormat=ChoosePixelFormat(dummyDC, &pfd); printf("context 1 pixelformat: %d\n",pixelFormat); int ret=SetPixelFormat(dummyDC,pixelFormat,NULL); //第三參數型態是PIXELFORMATDESCRIPTOR*,但其實程式執行不會用到,可填NULL if(ret==0){ return 1; } hglrc=wglCreateContext(dummyDC); if(!hglrc){ return 1; } wglMakeCurrent(dummyDC,hglrc); //load extension PFNWGLCHOOSEPIXELFORMATARBPROC wglChoosePixelFormatARB= (PFNWGLCHOOSEPIXELFORMATARBPROC)wglGetProcAddress("wglChoosePixelFormatARB"); PFNWGLCREATECONTEXTATTRIBSARBPROC wglCreateContextAttribsARB= (PFNWGLCREATECONTEXTATTRIBSARBPROC)wglGetProcAddress("wglCreateContextAttribsARB"); PFNWGLSWAPINTERVALEXTPROC wglSwapIntervalEXT= (PFNWGLSWAPINTERVALEXTPROC)wglGetProcAddress("wglSwapIntervalEXT"); //刪除OpenGL context、DC和dummy window wglDeleteContext(hglrc); ReleaseDC(dummyWindow, dummyDC); DestroyWindow(dummyWindow); //real context hdc=GetDC(window); const int pixelFormatAttr[]={ WGL_DRAW_TO_WINDOW_ARB, GL_TRUE, WGL_SUPPORT_OPENGL_ARB, GL_TRUE, WGL_DOUBLE_BUFFER_ARB, GL_TRUE, WGL_PIXEL_TYPE_ARB, WGL_TYPE_RGBA_ARB, WGL_COLOR_BITS_ARB, 24, WGL_ALPHA_BITS_ARB, 8, 0, }; UINT numFormats; pixelFormat=0; wglChoosePixelFormatARB(hdc, pixelFormatAttr, NULL,1,&pixelFormat, &numFormats); printf("context 2 pixelformat: %d\n",pixelFormat); ret=SetPixelFormat(hdc,pixelFormat,NULL); if(ret==0){ return 1; } //產生OpenGL 3.3 context const int contextAttr[]={ WGL_CONTEXT_MAJOR_VERSION_ARB, 3, WGL_CONTEXT_MINOR_VERSION_ARB, 3, WGL_CONTEXT_PROFILE_MASK_ARB, WGL_CONTEXT_CORE_PROFILE_BIT_ARB, 0, }; hglrc=wglCreateContextAttribsARB(hdc,0,contextAttr); if(!hglrc){ return 1; } wglMakeCurrent(hdc,hglrc); //設定Vsync wglSwapIntervalEXT(0); return 0; } |
建dummy window用最省事的方法:建立成主視窗的子視窗,位置大小隨便設。其他方法如建立另一個主視窗,或建立記憶體內的點陣圖DC都比較麻煩。
視窗程式的教學還沒有寫到建子視窗和UI元件的方法,本篇是第一次用到,CreateWindow()第一參數決定建立何種元件,第八參數填親視窗,詳細用法等哪天寫到視窗元件的教學再介紹。
在Windows使用OpenGL需要取得這個視窗的DC,用GetDC()取得。
第一個context使用Windows內建API建立,是比較舊的方法,用struct PIXELFORMATDESCRIPTOR指定framebuffer格式,由於是dummy context,格式隨便填一個所有電腦都支援的。
詳細用法看MSDN的文件。
PIXELFORMATDESCRIPTOR說明
ChoosePixelFormat()說明
再來用wglGetProcAddress()取得三個函式指標,前兩個是初始化會用到,wglSwapIntervalEXT是用來設vsync。
接下來建立真正要用的context,跟「OpenGL 3.3初始化(X Window)」一樣使用0結束的陣列設定格式。
所有能填的值與函式的用法請看擴充功能的說明,學OpenGL多少要會看擴充功能的文件。
WGL_ARB_pixel_format說明
WGL_ARB_create_context說明
兩種choose pixel format方法的差別是,PIXELFORMATDESCRIPTOR+ChoosePixelFormat()不能支援1.2版以後追加的功能,multisample之類的功能要用陣列+wglChoosePixelFormatARB()才做得到。
最後一個工作:設vsync,X Window篇有提到OpenGL可以調整系統設定強制開或關vsync,且OpenGL程式最好明確呼叫函式設定vsync,在Windows也一樣。
static void nextFrame(){ color[0]+=1.0/60; //修改顏色的R分量 if(color[0]>=1){ color[0]=0; } glClearColor(color[0],color[1],color[2],color[3]); glClear(GL_COLOR_BUFFER_BIT); SwapBuffers(hdc); } |
static void deinitOpenGL(){ wglDeleteContext(hglrc); } |
至於釋放DC,這個視窗有CS_OWNDC屬性,視窗被刪除時也會把DC刪除,不用呼叫ReleaseDC()。
假設檔名是simplegl_w.c,用這個指令build
VC: cl simplegl_w.c /Fesimplegl_w.exe /O2 /MD /link user32.lib opengl32.lib winmm.lib gdi32.lib GCC: gcc simplegl_w.c -o simplegl_w.exe -Os -s -luser32 -lopengl32 -lwinmm -lgdi32 |
執行的樣子,順便把pixel format印出來看看。
pixel format類似X Window的FBConfig,每個號碼代表什麼意思要用看OpenGL資訊的軟體看,例如這是OpenGL Extension Viewer,左邊選「Display modes & pixel formats」。
右下列出各種framebuffer格式:有沒有depth buffer和stencil buffer、有沒有multisample等等。
OpenGL on Windows的教學只會寫到這一篇而已。我的前兩款自製遊戲:Monster Musume Hunter和Cyber Sprite在W、L兩平台都用OpenGL,隨後我在Windows把D3D和OpenGL都試過一次,發現Windows的OpenGL驅動程式經常沒有認真寫,常常效能比較差或是有bug,所以決定之後作品的Windows版改用Direct3D了。以後筆者的OpenGL範例程式都會在Linux製作,想在Windows用OpenGL的人請自行變通一下。