前往
大廳
主題

【程式】OpenGL ES 3.0初始化(X Window)

Shark | 2024-09-20 20:56:42 | 巴幣 114 | 人氣 246

雖然OpenGL ES一般印象是給嵌入式裝置使用,以前貼過這張圖,這類3D API除了顯示晶片以外作業系統也要配合,只要作業系統有把它加入系統API就可以使用,Linux也有OpenGL ES的API所以也能用。

OpenGL ES把OpenGL一些以前有用但現在已過時的功能刪除,函式和常數名稱和OpenGL相同,所以大部分程式碼可以不用修改轉成OpenGL ES,除了跟作業系統打交道的部分。筆者有考慮把目前製作中遊戲從GLX改成EGL+GLES,改寫應該不難,程式會比GLX好看,而且以後轉移到Wayland或Android比較方便。

程式教學一覽



有些在「【程式】OpenGL 3.3初始化(X Window)」提過的就不重覆寫了。

Fedora 40要裝這個套件:mesa-libEGL-devel。Mint的套件名稱是libegl1-mesa-dev。
本篇目標版本是OpenGL ES 3.0,功能大致與OpenGL 3.3相同。
參考這篇的程式碼
https://registry.khronos.org/EGL/extensions/KHR/EGL_KHR_platform_x11.txt

#include<EGL/egl.h>
#include<GLES3/gl3.h>
#include<X11/Xutil.h> //使用XGetVisualInfo()
#include<stdio.h>
#include<unistd.h>
//使用usleep()

const int WINDOW_W=200,WINDOW_H=200;
//OpenGL ES必要物件
Display* dsp;
Window window;
Colormap cmap;
EGLDisplay eglDsp;
EGLContext context;
EGLSurface surface;
//畫面顏色
float color[]={0,0,0,1};

//這三個函式在下面說明
static Window initGLESAndCreateWindow();
static void nextFrame();
static void deinitGLES();

int main(){
  dsp = XOpenDisplay( NULL );
  window=initGLESAndCreateWindow();
  if(!window){
    printf("Can not initialize OpenGL ES\n");
    return 0;
  }

  //設標題
  XStoreName(dsp, window, "title");
  //設定事件mask
  Atom wmDelete = XInternAtom(dsp, "WM_DELETE_WINDOW", False);
  XSetWMProtocols(dsp, window, &wmDelete, 1);
  XMapWindow(dsp, window);

  //接收事件
  XEvent evt;
  int isEnd=0;
  while(!isEnd){
    while(XPending(dsp)){
      XNextEvent(dsp, &evt);
      switch(evt.type){
      case ClientMessage:
        if(evt.xclient.data.l[0]== wmDelete){ isEnd=1; }
        break;
      }
    }

    //正式寫遊戲時,遊戲邏輯放在此處

    nextFrame();
    usleep(16000);
  }

  deinitGLES();
  XDestroyWindow(dsp, window);
  XFreeColormap(dsp, cmap);
  XFlush(dsp);
  XCloseDisplay(dsp);
  return 0;
}
之前「建立視窗~把視窗指定為OpenGL畫布」要用GLX這個函式庫,本篇則是用EGL。

儲存global狀態的變數有EGLDisplay、EGLContext、EGLSurface三個,其中EGLDisplay和EGLSurface是把平台原生物件包裝一層,增加一些GLES用的屬性。在X Window這兩者是將Display*和Window包裝,在Windows則是HDC和HWND。
查egl.h會發現三者都是將void*取另一個名稱,這些是不透明的物件,外部看不到內容,只能呼叫函式由函式庫內部操作。

main()的內容跟GLX篇幾乎一樣,就不解釋了。

//X11必須先建EGLContext再建視窗
static Window initGLESAndCreateWindow(){
  eglDsp=eglGetDisplay(dsp);
  if(eglDsp==EGL_NO_DISPLAY){ return 0; }
  eglInitialize(eglDsp,NULL,NULL);
  const int configAttrib[]={
    EGL_RED_SIZE,8, EGL_GREEN_SIZE,8, EGL_BLUE_SIZE,8,EGL_ALPHA_SIZE,8,
    EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT,
    EGL_SURFACE_TYPE, EGL_WINDOW_BIT, //這個跟預設值相同,可不填
    EGL_NONE,
  };
  int numConfig;
  EGLConfig eglConfig;
  EGLBoolean ret=eglChooseConfig(eglDsp, configAttrib, &eglConfig,1,&numConfig);
  if(ret==EGL_FALSE){
    eglTerminate(eglDsp);
    return 0;
  }

  //印出EGLConfig編號
  EGLint configID;
  eglGetConfigAttrib(eglDsp, eglConfig, EGL_CONFIG_ID, &configID);
  printf("config ID %x\n",configID);

  //產生OpenGLES 3.0 context
  const int contextAttrib[]={
    EGL_CONTEXT_MAJOR_VERSION, 3,EGL_CONTEXT_MINOR_VERSION,0,
    EGL_NONE,
  };
  context=eglCreateContext(eglDsp, eglConfig, NULL, contextAttrib);
  if(context==0){
    eglTerminate(eglDsp);
    return 0;
  }

  //取得這個EGLConfig的visual,用它建立視窗
  EGLint visualID;
  eglGetConfigAttrib(eglDsp, eglConfig, EGL_NATIVE_VISUAL_ID, &visualID);
  XVisualInfo vInfoTemplate;
  vInfoTemplate.visualid=visualID;
  int numVisual;
  XVisualInfo* vInfo=XGetVisualInfo(dsp, VisualIDMask,&vInfoTemplate,&numVisual);
  XSetWindowAttributes windowAttr;
  cmap = XCreateColormap(dsp, DefaultRootWindow(dsp), vInfo->visual,AllocNone);
  windowAttr.colormap = cmap;
  window = XCreateWindow(dsp, DefaultRootWindow(dsp),
    0,0,WINDOW_W,WINDOW_H,
    0, vInfo->depth, InputOutput, vInfo->visual,
    CWColormap, &windowAttr);
  XFree(vInfo);

  //印出visual編號
  int defaultVisualID=XVisualIDFromVisual(DefaultVisual(dsp, DefaultScreen(dsp)));
  printf("defaultVisual:%x windowVisual:%x\n",defaultVisualID,visualID);

  surface=eglCreateWindowSurface(eglDsp,eglConfig, window, NULL);
  eglMakeCurrent(eglDsp, surface,surface, context);

  //設定Vsync
  eglSwapInterval(eglDsp, 0);
  return window;
}
initGLESAndCreateWindow()是跟GLX不同的主要部分。EGL的目標是不同平台儘量使用相同的函式和常數名稱。相較於glX很多函式需要傳入X Window的Display*和Window,EGL只有eglGetDisplay()和eglCreateWindowSurface()需要傳入平台相關變數,產生出EGLDisplay和EGLSurface之後就使用這兩個。

第一步是用eglGetDisplay()和eglInitialize()產生EGLDisplay物件。eglInitialize()第二、三參數傳回這台裝置上的EGL版本,不需要的話可填NULL。

第二步類似GLX的fbConfig,要設定framebuffer的格式,如RGBA各幾bits、有沒有depth buffer和stencil buffer。用一個整數陣列設定格式,鍵與值交替,最後用一個鍵EGL_NONE結束。把陣列傳入eglChooseConfig(),符合條件的EGLConfig會用第三參數傳回,可能有不只一個,第三參數填一個陣列,第四參數填陣列長度就能取得多個,但本篇只取一個。
有哪些屬性可以設定看eglChooseConfig()的說明。
eglChooseConfig - EGL Reference Pages

EGLConfig是不透明物件,用eglGetConfigAttrib()取得config ID並用printf()印出,config ID是什麼意思在下面說明。第三步用eglCreateContext()產生EGLContext,跟eglChooseConfig()一樣用EGL_NONE結束的陣列指定GLES版本。

建視窗時跟GLX一樣要讓三個物件的framebuffer格式配合:OpenGL ES、視窗、螢幕。視窗的格式必須跟GLES相同,方法是從EGLContext取得visual ID並用它產生一個XVisualInfo物件。螢幕的格式不一定和視窗相同,要產生Colormap物件用來轉換。最後把visual和Colormap傳入XCreateWindow()產生視窗。Colormap物件必須一直留著,等視窗被刪除後才能刪除。

最後用eglCreateWindowSurface()產生EGLSurface物件,第四參數用整數陣列設定屬性,但本篇沒用到。用eglMakeCurrent()設定目前要用的context和surface(GLES可以建立複數個context或surface看情況切換)。設定vsync的函式是eglSwapInterval()。

除了eglChooseConfig()以外,本段提到的函式的說明文件:
eglGetDisplay()
eglInitialize()
eglGetConfigAttrib()
eglCreateContext()
eglCreateWindowSurface()
eglMakeCurrent()
eglSwapInterval()

此外還有這兩個函式存在。
eglGetPlatformDisplay()
eglCreatePlatformWindowSurface()
是EGL 1.5版新增的,eglGetDisplay()和eglCreateWindowSurface()則是EGL最初的版本就有。

static  void nextFram(){
  color[0]+=1.0/60;
  if (color[0]>1){
    color[0]=0;
  }
  glClearColor(color[0],color[1],color[2],color[3]);
  glClear(GL_COLOR_BUFFER_BIT);
  eglSwapBuffers(eglDsp, surface);
}

static void deinitGLES(){
  eglDestroyContext(eglDsp, context);
  eglDestroySurface(eglDsp, surface);
  eglTerminate(eglDsp);
}
這兩個做的事跟GLX版一樣,只是函式名稱不同。
本篇還沒有寫shader,GLES的shader要把開頭的「#version 330」改成「#version 300 es」。

用這個指令build:
gcc simplegles.c -o simplegles -Os -s -lX11 -lEGL -lGLESv2
用命令列執行才看得到prinf()的輸出。執行的樣子:

這個例子GLES的config ID是0x01,視窗與螢幕的visual ID是0x49(十六進位),代表什麼意思在下面「eglinfo」一節說明。



vsync與擴充函式的注意事項跟GLX那篇相同故不再贅述。EGL用來取得擴充函式的函式是eglGetProcAddress()。
想查OpenGL ES哪一版有哪些功能,方法一是在以下網址查。
OpenGL ES 3.0 Reference Pages
OpenGL ES 3.1 Reference Pages
OpenGL ES 3.2 Reference Pages
方法二是查自己電腦裡的C語言header。例如筆者寫這篇時用的Fedora 40是「/usr/include/GLES3」裡的gl3.h、gl31.h和gl32.h。
看函式或常數名稱有沒有列在裡面,有就是有功能,沒有就是沒功能。例如看glCreateShader()的說明,3.0版參數可填GL_VERTEX_SHADER和GL_FRAGMENT_SHADER,3.1版增加了GL_COMPUTE_SHADER,可得知compute shader是在3.1版加入的。

GLES最終版本是3.2,功能接近OpenGL 4.3,之後就用Vulkan取代。

- eglinfo -

查看EGL與GLES資訊的軟體,Fedora 40要安裝這個套件:egl-utils。
此套件有另一個程式es2_info,但它只能取得目前平台的GLES擴充字串,用途跟eglinfo重複。
在命令列直接打eglinfo會列出很大量的資訊,用「eglinfo>a.txt」把資訊輸出到文字檔比較好查。「eglinfo -h」可以查命令列參數的用法。

首先它會列出很多平台(platform),像筆者的情況有這些:
GBM platform
Wayland platform
X11 platform
Surfaceless platform
Device platform

其中GBM platform只有這兩行,表示這台電腦不支援GBM平台。
GBM platform:
eglinfo: eglInitialize failed
本篇用的是X11平台,先找到「X11 platform:」的部分再看內容。
OpenGL core、OpenGL compatibility、OpenGL ES這些的extensions內容跟glxinfo顯示的一樣,就請看GLX那篇。

新的東西是Configurations。
Configurations:
     bf lv colorbuffer dp st  ms    vis   cav bi  renderable  supported
  id sz  l  r  g  b  a th cl ns b    id   eat nd gl es es2 vg surfaces
---------------------------------------------------------------------
0x01 32  0  8  8  8  8  0  0  0 0 0x49TC      a  y  y  y     win,pb,pix
0x02 32  0  8  8  8  8 16  0  0 0 0x49TC      a  y  y  y     win,pb,pix
0x03 32  0  8  8  8  8 24  0  0 0 0x49TC      a  y  y  y     win,pb,pix
  ……
程式執行時印出config ID是0x01,對照這個表就能查到framebuffer格式。

這個程式不會列出visual資訊,要用glxinfo才看得到。

創作回應

相關創作

更多創作