將「OpenGL 3.3 使用貼圖」改成用EGL+OpenGL ES,看看和OpenGL有什麼差異。
程式教學目錄
首先是shader:
參考資料:
https://registry.khronos.org/OpenGL/specs/es/3.0/GLSL_ES_Specification_3.00.pdf 這篇的「4.5 Precision and Precision Qualifiers」
https://developer.arm.com/documentation/102502/0101/Shader-precision
嵌入式裝置的晶片效能比不上桌上型電腦,對電池續航力也有要求,所有浮點數都用32-bit計算的話,速度和耗電量表現會比較差,所以要告訴GPU哪些數值可以用低精度算。因為fragment shader的執行次數比vertex shader多很多,fragment shader的精度比較重要。
指定精度的保留字有lowp、mediump、highp三個。按照Khronos的文件,以下資料型態有預設精度:
主要是fragment shader的浮點數必須手動指定,包括向量和矩陣。本篇shader開頭的「precision lowp float;」設定沒寫時精度就是lowp,如果需要其他精度則在宣告變數時指定。
文件裡有寫到一堆目前還沒用到的sampler也必須手動指定,如sampler2DShadow。
按照那兩篇文件,浮點數三種精度的特性如下:
整數則分別是9-bit、16-bit、32-bit。原則是估計各個變數的數值範圍和0與1之間等分數,來設定精度,本篇的話顏色切255等分夠用,用lowp,貼圖坐標需要切400等分以上,用mediump。
主程式除了glX的函式、常數和資料型態改成對應的egl以外,大致有四個地方要改。
第一是要手寫這三個常數。這三個本來是在/usr/include/GLES2/gl2ext.h裡定義,但GLES3的.h檔沒有定義。
PC的話,支援Direct3D和OpenGL表示晶片有BGRA的功能,只要驅動程式沒有偷工減料,OpenGL ES沒理由不支援。Android我查到的說法是98%以上的機器都有支援。
筆者使用BGRA的理由是多數繪圖軟體都用BGRA,可以直接把十六進位數值貼到程式碼或程式的外部資料。
第二是因應BGRA格式而做的改變。glTexImage2D()第7參數(你傳入的點陣圖格式)從GL_BGRA改成GL_BGRA_EXT。
此時第三參數(顯示記憶體內部格式)要是填GL_RGBA8或GL_BGRA8_EXT在有些Linux發行版會視為error,填GL_RGBA或GL_BGRA_EXT才能正常作用。
(這我確定是發行版的差異,在分別是Intel和AMD晶片的兩台電腦上試,Mint 20.3都不能填GL_RGBA8和GL_BGRA8_EXT,但是Fedora 40可以)
GL/GLES較新版有另一個產生貼圖的函式glTexStorage2D(),能不能填GL_BGRA8_EXT我還沒試。
第三也是因為BGRA格式。這一段第三個glVertexAttribPointer(),第二參數從GL_BGRA改成GL_BGRA_EXT。這個就不知道是哪個擴充提供的。OpenGL是這個:ARB_vertex_array_bgra,在3.2被加入標準,GLES沒有同名的擴充,但實測可以用BGRA。
第四是glEnable()/glDisable()沒有GL_MULTISAMPLE的項目。好像只要framebuffer被建立成有multisample就一定啟用multisample,不像OpenGL可以啟用或停用。
傳給GPU的資料,以及剩下七個函式只有glX和egl的差別,以及initSettings()裡的glDisable(GL_MULTISAMPLE)。
用這個指令build
程式教學目錄
首先是shader:
#version 300 es //usetexture.vert.glsl layout(std140) uniform uniform1{ vec2 windowSize; }; uniform sampler2D sampler1; //GLSL的sampler物件包含sampler和貼圖 //頂點資料,對應到struct VertexData1 layout(location=0) in vec2 inPos; layout(location=1) in vec2 inTexCoord; layout(location=2) in vec4 inColor; //vertex傳給fragment shader的資料 out vec2 varTexCoord; out vec4 varColor; void main(){ vec2 outPos=inPos*vec2(2,-2)/windowSize+vec2(-1,1); gl_Position=vec4(outPos, 0, 1); ivec2 texSize=textureSize(sampler1, 0); varTexCoord=inTexCoord/vec2(texSize); varColor=inColor; } |
#version 300 es //usetexture.frag.glsl precision lowp float; //設定預設浮點數精度 uniform sampler2D sampler1; //從vertex shader內插的資料 in mediump vec2 varTexCoord; in vec4 varColor; //輸出到畫面的顏色 layout(location=0) out vec4 outColor0; void main(){ vec4 texColor=texture(sampler1, varTexCoord); outColor0=texColor*varColor; } |
- 開頭的「#version 330」改成「#version 300 es」。
- vertex shader傳給fragment shader的資料不能用varying保留字,且fragment shader的輸出不能用內建變數gl_FragColor,要用in, out。
- 整數不會自動轉成浮點數,要寫轉型語法,如「varTexCoord=inTexCoord/vec2(texSize);」這一行的vec2()。
- 到GLES 3.2也沒有把中間碼SPIR-V納入標準,好像Vulkan出來之後就全力發展Vulkan,SPIR-V移到Vulkan再支援。我手上兩台電腦的OpenGL有SPIR-V的擴充,但OpenGL ES沒有。
參考資料:
https://registry.khronos.org/OpenGL/specs/es/3.0/GLSL_ES_Specification_3.00.pdf 這篇的「4.5 Precision and Precision Qualifiers」
https://developer.arm.com/documentation/102502/0101/Shader-precision
嵌入式裝置的晶片效能比不上桌上型電腦,對電池續航力也有要求,所有浮點數都用32-bit計算的話,速度和耗電量表現會比較差,所以要告訴GPU哪些數值可以用低精度算。因為fragment shader的執行次數比vertex shader多很多,fragment shader的精度比較重要。
指定精度的保留字有lowp、mediump、highp三個。按照Khronos的文件,以下資料型態有預設精度:
vertex shader | |
float | highp |
int | highp |
sampler2D | lowp |
samplerCube | lowp |
fragment shader | |
int | mediump |
sampler2D | lowp |
samplerCube | lowp |
文件裡有寫到一堆目前還沒用到的sampler也必須手動指定,如sampler2DShadow。
按照那兩篇文件,浮點數三種精度的特性如下:
內部型態 | 數值範圍 | 0與1之間幾等分 | |
lowp | 10-bit定點數 | -2 ~ 2 | 2^8 |
mediump | 16-bit浮點數 | -2^14 ~ 2^14 | 2^10 |
highp | 32-bit浮點數 | -2^126 ~ 2^127 | 2^24 |
主程式除了glX的函式、常數和資料型態改成對應的egl以外,大致有四個地方要改。
#include<EGL/egl.h> #include<GLES3/gl3.h> #include<X11/Xutil.h> //使用XGetVisualInfo() #define GL_BGR_EXT 0x80E0 #define GL_BGRA_EXT 0x80E1 #define GL_BGRA8_EXT 0x93A1 #include<stdio.h> #include<time.h> //使用clock_gettime()和nanosleep() #include<fcntl.h> //使用open() #include<sys/stat.h> //使用fstat() #include<gdk-pixbuf/gdk-pixbuf.h> //讀取圖檔 #include<x86intrin.h> //使用_rotr()和__bswapd() const int WINDOW_W=400,WINDOW_H=400; //OpenGL ES global狀態 Display* dsp; Window window; Colormap cmap; EGLDisplay eglDsp; EGLContext context; EGLSurface surface; //GLES物件 uint32_t programID; uint32_t vertexData; uint32_t vertexArrayObj; //本篇新增的物件 uint32_t uniformBuffer; uint32_t texture; uint32_t sampler; //要傳給GPU的資料在下面說明 //這些函式在下面說明 static uint32_t loadTexture(const char* fileName); static void initVertexData1(uint32_t* outVertexData, uint32_t* outVao); static Window initGLESAndCreateWindow(); static void deinitGLES(); static void nextFrame(); static void deinitSettings(); static char* loadWholeFile(const char* fileName, uint32_t* outFileSize); static int loadShader(uint32_t shaderID, const char* fileName);我手上的兩台電腦。 static int initSettings(); int main(){ dsp = XOpenDisplay(NULL); window = initGLESAndCreateWindow(); if(!window){ printf("Can not initialize OpenGL ES\n"); return 0; } if(initSettings()){ return 0; } //設標題 XStoreName(dsp, window, "usetexture"); //設定事件mask Atom wmDelete = XInternAtom(dsp, "WM_DELETE_WINDOW", True); XSetWMProtocols(dsp, window, &wmDelete, 1); XMapWindow(dsp, window); XEvent evt; int isEnd=0; struct timespec prevTime,nextTime; while(!isEnd){ clock_gettime(CLOCK_MONOTONIC, &prevTime); //接收事件 while(XPending(dsp)){ XNextEvent(dsp, &evt); switch(evt.type){ case ClientMessage: if(evt.xclient.data.l[0]== wmDelete){ isEnd=1; } break; } } //正式寫遊戲時,遊戲邏輯放在此處 nextFrame(); //更新畫面 clock_gettime(CLOCK_MONOTONIC, &nextTime); //單位為奈秒(10^-9秒) int64_t elapsedTime = (nextTime.tv_sec-prevTime.tv_sec)*1000000000 +(nextTime.tv_nsec-prevTime.tv_nsec); //求出經過的奈秒數 struct timespec sleepTime={0, 16000000-elapsedTime}; if(sleepTime.tv_nsec>0){ nanosleep(&sleepTime, NULL); } } XDestroyWindow(dsp, window); XFreeColormap(dsp, cmap); XFlush(dsp); deinitSettings(); deinitGLES(); XCloseDisplay(dsp); return 0; } |
#define GL_BGR_EXT 0x80E0顏色的byte順序GLES沒有把BGRA列入標準規格,只有RGBA。但從筆者收集的資料,大概可以賭幾乎所有晶片都有這個擴充:GL_EXT_texture_format_BGRA8888。
#define GL_BGRA_EXT 0x80E1
#define GL_BGRA8_EXT 0x93A1
PC的話,支援Direct3D和OpenGL表示晶片有BGRA的功能,只要驅動程式沒有偷工減料,OpenGL ES沒理由不支援。Android我查到的說法是98%以上的機器都有支援。
筆者使用BGRA的理由是多數繪圖軟體都用BGRA,可以直接把十六進位數值貼到程式碼或程式的外部資料。
//讀取圖檔、解碼、產生texture物件 //成功傳回OpenGL物件識別碼,失敗傳回0 static uint32_t loadTexture(const char* fileName){ //用gdk-pixbuf讀取圖檔,把RGBA值存在一個叫pixels的變數 GError* error=NULL; GdkPixbuf* pixbuf = gdk_pixbuf_new_from_file(fileName,&error); if(pixbuf==NULL){ return 0; } int w = gdk_pixbuf_get_width(pixbuf); int h = gdk_pixbuf_get_height(pixbuf); uint32_t* pixels = (uint32_t*)malloc(w*h*4); GdkPixbuf* outPixbuf=gdk_pixbuf_new_from_data((uint8_t*)pixels, GDK_COLORSPACE_RGB, 1,8,w,h,w*4,NULL,0); gdk_pixbuf_copy_area(pixbuf,0,0,w,h,outPixbuf,0,0); g_object_unref(pixbuf); g_object_unref(outPixbuf); //RGBA轉BGRA uint32_t* tempP=pixels; for(int i=0; i<w*h; i++,tempP++){ *tempP=_rotr(__bswapd(*tempP), 8); } //建立texture物件 uint32_t textureID; glGenTextures(1, &textureID); glBindTexture(GL_TEXTURE_2D, textureID); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, w,h,0, GL_BGRA_EXT,GL_UNSIGNED_BYTE, pixels); glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAX_LEVEL, 0); //設mipmap層數 free(pixels); return textureID; } |
此時第三參數(顯示記憶體內部格式)要是填GL_RGBA8或GL_BGRA8_EXT在有些Linux發行版會視為error,填GL_RGBA或GL_BGRA_EXT才能正常作用。
(這我確定是發行版的差異,在分別是Intel和AMD晶片的兩台電腦上試,Mint 20.3都不能填GL_RGBA8和GL_BGRA8_EXT,但是Fedora 40可以)
GL/GLES較新版有另一個產生貼圖的函式glTexStorage2D(),能不能填GL_BGRA8_EXT我還沒試。
static void initVertexData1(uint32_t* outVertexData, uint32_t* outVao){ //vertex buffer glGenBuffers(1, outVertexData); glBindBuffer(GL_ARRAY_BUFFER, *outVertexData); glBufferData(GL_ARRAY_BUFFER, sizeof(VERTEX_DATA1),VERTEX_DATA1, GL_STATIC_DRAW); //vertex array object glGenVertexArrays(1,outVao); glBindVertexArray(*outVao); glEnableVertexAttribArray(0); glVertexAttribPointer(0, 2, GL_FLOAT,0, sizeof(VertexData1), 0); glEnableVertexAttribArray(1); glVertexAttribPointer(1, 2, GL_SHORT,0, sizeof(VertexData1), (void*)offsetof(VertexData1, texCoord)); glEnableVertexAttribArray(2); glVertexAttribPointer(2, GL_BGRA_EXT, GL_UNSIGNED_BYTE,1, sizeof(VertexData1), (void*)offsetof(VertexData1, color)); } |
第四是glEnable()/glDisable()沒有GL_MULTISAMPLE的項目。好像只要framebuffer被建立成有multisample就一定啟用multisample,不像OpenGL可以啟用或停用。
傳給GPU的資料,以及剩下七個函式只有glX和egl的差別,以及initSettings()裡的glDisable(GL_MULTISAMPLE)。
//要傳給GPU的資料 //uniform buffer const float WINDOW_SIZE[]={WINDOW_W, WINDOW_H}; //頂點資料 typedef struct{ float pos[2]; short texCoord[2]; uint32_t color; } VertexData1; VertexData1 VERTEX_DATA1[4]={ {100,0, 0, 0, 0xffffffff}, {100,400,0, 400,0xffffffff}, {400,0, 300,0, 0xffffffff}, {400,400,300,400,0xffffffff}, }; //傳回0代表失敗 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; } //產生OpenGL ES 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); surface=eglCreateWindowSurface(eglDsp,eglConfig, window, NULL); eglMakeCurrent(eglDsp, surface,surface, context); //設定Vsync eglSwapInterval(eglDsp, 0); return window; } static void deinitGLES(){ eglDestroyContext(eglDsp, context); eglDestroySurface(eglDsp, surface); eglTerminate(eglDsp); } static void nextFrame(){ glClear(GL_COLOR_BUFFER_BIT); glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); eglSwapBuffers(eglDsp, surface); } static void deinitSettings(){ glDeleteProgram(programID); uint32_t buffers[]={vertexData, uniformBuffer}; glDeleteBuffers(2, buffers); glDeleteVertexArrays(1, &vertexArrayObj); glDeleteTextures(1, &texture); glDeleteSamplers(1, &sampler); } //傳回來的指標要用free()釋放 static char* loadWholeFile(const char* fileName, uint32_t* outFileSize){ int file=open(fileName, O_RDONLY); if(file==-1){ *outFileSize=0; return NULL; } struct stat buf; fstat(file, &buf); long fileSize=buf.st_size; char* data=(char*)malloc(fileSize); read(file, data, fileSize); close(file); *outFileSize=fileSize; return data; } //傳回0代表成功、非0代表失敗 static int loadShader(uint32_t shaderID, const char* fileName){ uint32_t fileSize; const char* shaderCode=loadWholeFile(fileName, &fileSize); if(shaderCode==NULL){ return 1; } glShaderSource(shaderID, 1, &shaderCode, &fileSize); free((void*)shaderCode); glCompileShader(shaderID); //檢查shader錯誤 int ret; glGetShaderiv(shaderID, GL_COMPILE_STATUS, &ret); //ret=0代表有錯誤 if(!ret){ int infoLen; glGetShaderiv(shaderID, GL_INFO_LOG_LENGTH, &infoLen); //取得訊息長度 char info[infoLen]; //配置空間 glGetShaderInfoLog(shaderID, infoLen, NULL, info); //取得訊息 printf("%s:\n%s",fileName, info); } return !ret; } //傳回0代表成功,非0代表失敗 static int initSettings(){ //compile vertex shader uint32_t vsID=glCreateShader(GL_VERTEX_SHADER); loadShader(vsID, "usetexture.vert.glsl"); //compile fragment shader uint32_t fsID=glCreateShader(GL_FRAGMENT_SHADER); loadShader(fsID, "usetexture.frag.glsl"); //create program object programID=glCreateProgram(); glAttachShader(programID, vsID); glAttachShader(programID, fsID); glLinkProgram(programID); glDetachShader(programID, vsID); glDetachShader(programID, fsID); glDeleteShader(vsID); //之後不會再用到shader物件,可刪除 glDeleteShader(fsID); //check program error int ret; glGetProgramiv(programID,GL_LINK_STATUS, &ret); if(!ret){ int infoLen; glGetProgramiv(programID, GL_INFO_LOG_LENGTH, &infoLen); char info[infoLen]; glGetProgramInfoLog(programID, infoLen, NULL, info); printf("program linking:\n%s", info); glDeleteProgram(programID); return 1; } glUseProgram(programID); //vertex data & vertex array object initVertexData1(&vertexData, &vertexArrayObj); //uniform buffer glGenBuffers(1, &uniformBuffer); glBindBuffer(GL_UNIFORM_BUFFER, uniformBuffer); glBufferData(GL_UNIFORM_BUFFER, sizeof(WINDOW_SIZE), WINDOW_SIZE, GL_STATIC_DRAW); //將這個物件與shader裡的buffer物件對應 uint32_t uniformIndex=glGetUniformBlockIndex(programID, "uniform1"); glUniformBlockBinding(programID, uniformIndex, 2); glBindBufferBase(GL_UNIFORM_BUFFER, 2, uniformBuffer); //sampler glGenSamplers(1, &sampler); glSamplerParameteri(sampler, GL_TEXTURE_MAG_FILTER, GL_LINEAR); //default glSamplerParameteri(sampler, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glSamplerParameteri(sampler, GL_TEXTURE_WRAP_S, GL_REPEAT); //default glSamplerParameteri(sampler, GL_TEXTURE_WRAP_T, GL_REPEAT); //default //將這個物件與shader裡的sampler物件對應 int location=glGetUniformLocation(programID, "sampler1"); glUniform1i(location, 2); glBindSampler(2, sampler); //texture glActiveTexture(GL_TEXTURE2); //目前要操作2號slot texture = loadTexture("char1.png"); //rasterizer glDisable(GL_CULL_FACE); //default glViewport(0, 0, WINDOW_W, WINDOW_H); //少了glDisable(GL_MULTISAMPLE); //depth and stencil glDisable(GL_DEPTH_TEST); //default glDisable(GL_STENCIL_TEST); //default //blend glEnable(GL_BLEND); glBlendEquation(GL_FUNC_ADD); //default glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ONE_MINUS_SRC_ALPHA); const float color[]={1,1,1,1}; glClearColor(color[0],color[1],color[2],color[3]); return 0; } //initSettings()結束 |
用這個指令build
gcc usetexture.c -o usetexture -s -Os -lX11 -lEGL -lGLESv2 `pkg-config --cflags --libs gdk-pixbuf-2.0` |