前往
大廳
主題

【程式】OpenGL 3.3初始化 (Windows)

Shark | 2021-04-17 20:50:52 | 巴幣 226 | 人氣 1205

在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的函式和常數,寫程式前要做一點前置工作
  1. 去Khronos Group的網站,下載glext.h、wglext.h、khrplatform.h這三個檔案。
    https://www.khronos.org/registry/OpenGL/index_gl.php
  2. 將這三個檔放在適當地方
    方法一:放在Windows SDK資料夾,這樣你寫的所有程式都可以使用。
    例如筆者目前用的VC 2013,把glext.h和wglext.h放在這裡。
    C:\Program Files (x86)\Windows Kits\8.1\Include\um\gl\
    其他VC版本把8.1換成其他數字,自己找一下。

    然後要建一個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所在資料夾讀取。
GCC的話,筆者有一段時間沒用Windows版GCC所以不太確定,看它的include資料夾有沒有glext.h和wglext.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和兩個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也一樣。

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);
}
swap buffer的函式名稱改成SwapBuffers(),其餘跟X Window篇一樣,之前用GetDC()取得的DC在這裡會用到。這裡可看出為何說OpenGL是跨平台,除了初始化、結束和swap buffer以外,其餘操作的程式碼在各平台都一樣。

static void deinitOpenGL(){
  wglDeleteContext(hglrc);
}
刪除的步驟跟X Window篇不一樣,處理完WM_DESTROY訊息之後視窗就被刪除了,但之後呼叫SwapBuffers()也不會讓程式掛掉,只會傳回0代表失敗。如果在意這個傳回值也可以改在WM_CLOSE裡呼叫PostQuitMessage(0),刪除OpenGL context後再呼叫DestroyWindow()刪視窗。

至於釋放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的人請自行變通一下。

創作回應

相關創作

更多創作