前往
大廳
主題

【程式】將「OpenGL 3.3 使用貼圖」改寫成OpenGL ES

Shark | 2024-10-02 00:21:36 | 巴幣 6 | 人氣 144

將「OpenGL 3.3 使用貼圖」改成用EGL+OpenGL ES,看看和OpenGL有什麼差異。

程式教學目錄



首先是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沒有。
還有需要precision qualifier。
參考資料:
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
主要是fragment shader的浮點數必須手動指定,包括向量和矩陣。本篇shader開頭的「precision lowp float;」設定沒寫時精度就是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
整數則分別是9-bit、16-bit、32-bit。原則是估計各個變數的數值範圍和0與1之間等分數,來設定精度,本篇的話顏色切255等分夠用,用lowp,貼圖坐標需要切400等分以上,用mediump。

主程式除了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;
}
第一是要手寫這三個常數。這三個本來是在/usr/include/GLES2/gl2ext.h裡定義,但GLES3的.h檔沒有定義。
#define GL_BGR_EXT 0x80E0
#define GL_BGRA_EXT 0x80E1
#define GL_BGRA8_EXT 0x93A1
顏色的byte順序GLES沒有把BGRA列入標準規格,只有RGBA。但從筆者收集的資料,大概可以賭幾乎所有晶片都有這個擴充:GL_EXT_texture_format_BGRA8888
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;
}
第二是因應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我還沒試。

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));
}
第三也是因為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)。
//要傳給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`

創作回應

相關創作

更多創作