ETH官方钱包

前往
大廳
主題

【程式】OpenGL 上傳資料至buffer物件

Shark | 2022-06-08 11:41:02 | 巴幣 1228 | 人氣 620

前篇  OpenGL 3.3 使用貼圖
Direct3D篇  Direct3D 上傳資料至buffer物件

程式教學目錄

最近在Linux開發機總算把nVidia專有驅動程式搞定了,但還是不能動態調整(平時開內顯,執行程式時選擇要不要開獨顯),一次只能用一個晶片,想改用另一個晶片必須改設定後重新開機。所以平常還是只用內顯,要測試的時候才開獨顯。
不同晶片執行OpenGL的狀況不一樣,用nVidia晶片測試一次才發現初始化的步驟有錯誤,把初始化那篇改一下,其他篇之後找時間改。
OpenGL 3.3初始化(X Window)

另外,雖然很多Linux發行版將Firefox做為預設瀏覽器,但它的效能消耗很大,每次一開Firefox電腦就變得很燙,檢查後發現預設不開硬體視訊解碼是原因之一,研究了一下怎麼設定,寫在這篇最後面。
顯卡少女的第二個能力——視訊編解碼



這次要用的圖。

下載後把檔名改成remilia.png,放在與可執行檔相同資料夾。
來源:ヘポモチ! http://forest.her.jp/moricchi/dot.htm
原圖是GIF動畫,我把各個畫格拆開。

畫格是這樣,以中央為定位點。長度不是寫在程式裡,而是從圖檔寬高計算。

這次做了三種動態
  • 按WSAD鍵讓圖上下左右移動。
  • 按空白鍵不放時圖變成黑色,放開後恢復。
  • 每隔一段時間換畫格。
首先shader是這樣
//uploaddata.vert.glsl
#version 330
//uniform buffer,對應到struct UniformBuffer
layout(std140) uniform uniform0{
  vec4 globalColor;
  vec2 windowSizeRcp;
};
uniform sampler2D sampler0;

//頂點資料,對應到struct VertexData
layout(location=0) in vec2 inPos;
layout(location=1) in vec2 inTexCoord;

//vertex傳給fragment shader的資料
out vec2 varTexCoord;

void main(){
  vec2 outPos=inPos*vec2(2,-2)*windowSizeRcp+vec2(-1,1);
  gl_Position=vec4(outPos, 0, 1);
  ivec2 texSize=textureSize(sampler0, 0);
  varTexCoord=inTexCoord/texSize;
}
//uploaddata.frag.glsl
#version 330
//uniform buffer,對應到struct UniformBuffer
layout(std140) uniform uniform0{
  vec4 globalColor;
  vec2 windowSizeRcp;
};
uniform sampler2D sampler0;

//vertex傳給fragment shader的資料
in vec2 varTexCoord;

void main(){
  vec4 texColor=texture(sampler0, varTexCoord);
  gl_FragColor=texColor*globalColor;
}
邏輯跟使用貼圖篇差不多,顏色改放在uniform buffer而不是頂點資料。vertex shader把以像素為單位的位置和貼圖坐標換算成-1~1,fragment shader讀取貼圖、把顏色與globalColor相乘。

windowSizeRcp的Rcp=reciprocal=倒數。電腦算除法比乘法慢得多,寫程式一個優化技巧是儘量用乘法代替除法,雖然本篇只有4個頂點影響很小,示範一下這個方法。本來坐標要除以視窗寬高,這裡由CPU事先把寛高的倒數算出,shader裡用乘法,這樣只要做一次除法;而且主程式裡的「1.0/WINDOW_W、1.0/WINDOW_H」是常數,編譯時就會算出來,執行時連除法也不會有。反之如果在vertex shader裡算除法,n個頂點就要做n次除法。

再來是主程式
uploaddata.c
#define GL_GLEXT_PROTOTYPES
#define GLX_GLXEXT_PROTOTYPES
#include<GL/gl.h>
#include<GL/glx.h>

#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<linux/input-event-codes.h> //使用key code
#include<x86intrin.h>
//使用_rotr()和__bswapd()

const int WINDOW_W=400, WINDOW_H=400;

//OpenGL global狀態
Display* dsp;
Window window;
Colormap cmap;
GLXContext context;
//建好後就不會修改的物件
uint32_t programID;
uint32_t sampler;
uint32_t vertexArrayObj;
//貼圖
uint32_t texture;
uint16_t imageW,imageH;
//每個frame會修改的資料
uint32_t uniformBuffer;
uint32_t vertexData;

//--系統物件
//這兩個struct是要上傳到GPU的資料

typedef struct {
  float globalColor[4];
  float windowSizeRcp[2];
} UniformBuffer;
//使用「頂點資料layout設定」裡VertexData2的方式,但顏色改放在uniform buffer
typedef struct {
  float pos[8];
  short texCoord[8];
} VertexData;

//按鈕狀態
union {
  uint8_t array[5];
  struct{
    uint8_t up;
    uint8_t down;
    uint8_t left;
    uint8_t right;
    uint8_t space;
  };
} keyState;
//將keyState.array對應到按鍵的對應表
const uint8_t LOOKUP_TABLE[] = {KEY_W+8,KEY_S+8,KEY_A+8,KEY_D+8,KEY_SPACE+8};
const int LOOKUP_TABLE_LEN = sizeof(LOOKUP_TABLE);

//--邏輯物件
struct {
  float x,y; //在畫面上的位置,單位為像素
  short texX,texY,texW,texH; //貼圖坐標
  int frameCounter; //換畫格的計時器
} myCharacter;
const float SPEED=4; //單位為pixel/frame
const int FRAME_TIME=6; //每秒10格
const int CELL_NUMBER=8;
const float BORDER=40;

//用來將圖變色
float globalColor[]={1,1,1,1};

//------
//這些函式在後面說明

static Window initGLAndCreateWindow();
static void deinitGL();
static void deinitSettings();
static char* loadWholeFile(const char* fileName, uint32_t* outFileSize);
static int loadShader(uint32_t shaderID, const char* fileName);
static uint32_t loadTexture(const char* fileName, uint16_t* outW, uint16_t* outH);
static uint32_t createProgram(uint32_t vsID, uint32_t fsID);

static int initSettings();
static void nextFrame();
static void drawScreen();

//------
//以下是main()


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

  //將視窗設成不可改大小
  XSizeHints* sizeHint=XAllocSizeHints();
  sizeHint->flags = PMaxSize|PMinSize;
  sizeHint->min_width = sizeHint->max_width = WINDOW_W;
  sizeHint->min_height = sizeHint->max_height = WINDOW_H;
  XSetWMNormalHints(dsp, window, sizeHint);
  XFree(sizeHint);

  //設標題
  XStoreName(dsp, window, "uploaddata");
  //設定事件mask
  const int eventMask=KeyPressMask|KeyReleaseMask;
  XSelectInput(dsp,window,eventMask);
  Atom wmDelete = XInternAtom(dsp, "WM_DELETE_WINDOW", False);
  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 KeyPress:
      case KeyRelease:{
        uint8_t flag= evt.type==KeyPress;
        uint32_t keyCode=evt.xkey.keycode;
        for(int i=0;i<LOOKUP_TABLE_LEN;i++){
          if(keyCode==LOOKUP_TABLE[i]){
            keyState.array[i]=flag;
            break;
          }
        }
        }break;
      case
ClientMessage:
        if(evt.xclient.data.l[0]== wmDelete){ isEnd=1; }
        break;
      }
    }

    nextFrame(); //處理邏輯
    drawScreen(); //更新畫面

    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);
    }
  }

  deinitSettings();
  deinitGL();
  XDestroyWindow( dsp, window );
  XFreeColormap(dsp, cmap);
  XFlush(dsp);
  XCloseDisplay( dsp );
  return 0;
}
主要邏輯跟D3D篇一樣:
程式開始宣告一些變數記錄狀態,並設定一些動畫相關數值。這裡速率、畫格數等數值都寫在程式裡,因為本篇想儘量簡化主題以外的部分,正式做軟體時可以看情況放在外部檔案。

程式結構方面,將邏輯和繪圖分開:用作業系統API讀取輸入,將按鍵狀態存在變數keyState;在nextFrame()把所有物體的坐標和顏色算好;然後drawScreen()才畫圖。GL的函式全放在drawScreen()裡面,而不是每算好一個物體就呼叫繪圖指令。

main()增加了讀取輸入的部分,用到的函式和常數見這篇:讀取鍵盤與滑鼠輸入 (X Window)
LOOKUP_TABLE是把要用的五個按鍵的X Window keycode寫成一個陣列,收到按鍵事件時檢查keycode有沒有在這個陣列裡,然後把按鍵是否按下存在keyState。這裡用union,array[5]和up等五個欄位佔用相同空間,可以用陣列也可以用struct的語法存取資料。

以下前5個函式和以前一樣,loadTexture()只有小幅改變,就不解釋了。
loadWholeFile()的作業系統API說明在此:檔案操作—Linux篇
gdk-pixbuf的用法見這篇:讀取圖檔的方法-Linux篇
如果有多個program物件,產生program的程式碼是固定的pattern,拿出來放在另一個函式。
//成功傳回Window,失敗傳回0
static Window initGLAndCreateWindow(){
  const int fbConfigAttr[]={
    GLX_X_RENDERABLE,True,
    GLX_DOUBLEBUFFER,True,
    GLX_X_VISUAL_TYPE, GLX_TRUE_COLOR,
    GLX_RED_SIZE, 8, GLX_GREEN_SIZE, 8, GLX_BLUE_SIZE, 8, GLX_ALPHA_SIZE,8,
    None
  };
  int fbCount;
  GLXFBConfig* fbc = glXChooseFBConfig(dsp, DefaultScreen(dsp),fbConfigAttr,&fbCount);
  if(fbc==NULL){ return 0; }

  //產生OpenGL 3.3 context
  const int contextAttribs[] = {
    GLX_CONTEXT_MAJOR_VERSION_ARB,3,
    GLX_CONTEXT_MINOR_VERSION_ARB,3,
    GLX_CONTEXT_PROFILE_MASK_ARB, GLX_CONTEXT_CORE_PROFILE_BIT_ARB,
    None,
  };
  context = glXCreateContextAttribsARB(dsp, fbc[0], 0,True, contextAttribs);
  if(context==0){
    XFree(fbc);
    return 0;
  }

  //取得這個fbConfig的visual,用它建立視窗
  XVisualInfo* vInfo=glXGetVisualFromFBConfig(dsp, fbc[0]);
  XSetWindowAttributes windowAttr;
  cmap = XCreateColormap(dsp, DefaultRootWindow(dsp), vInfo->visual,AllocNone);
  windowAttr.colormap = cmap;
  windowAttr.background_pixel = 0;
  windowAttr.border_pixel = 0;
  Window window = XCreateWindow(dsp, DefaultRootWindow(dsp),
    0,0,WINDOW_W,WINDOW_H,
    0, vInfo->depth, InputOutput, vInfo->visual,
    CWBackPixel|CWBorderPixel|CWColormap, &windowAttr);

  XFree(vInfo);
  XFree(fbc);
  glXMakeCurrent(dsp, window, context);

//設定Vsync
  PFNGLXSWAPINTERVALMESAPROC glXSwapIntervalMESA=
    (PFNGLXSWAPINTERVALMESAPROC)glXGetProcAddress("glXSwapIntervalMESA");
  if(glXSwapIntervalMESA){
    glXSwapIntervalMESA(0);
  }
  PFNGLXSWAPINTERVALSGIPROC glXSwapIntervalSGI=
    (PFNGLXSWAPINTERVALSGIPROC)glXGetProcAddress("glXSwapIntervalSGI");
  if(glXSwapIntervalSGI){
    glXSwapIntervalSGI(0);
  }
  return window;
}

static void deinitGL(){
  glXDestroyContext(dsp, context);
}

static void deinitSettings(){
  glDeleteProgram(programID);
  glDeleteSamplers(1, &sampler);
  glDeleteVertexArrays(1, &vertexArrayObj);

  glDeleteTextures(1, &texture);
  uint32_t buffers[]={vertexData, uniformBuffer};
  glDeleteBuffers(2, buffers);
}

//傳回來的指標要用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;
}

//loadTexture()小幅改變,除了OpenGL識別碼以外也傳回寬高
static uint32_t loadTexture(const char* fileName, uint16_t* outW, uint16_t* outH){
  //用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);
  *outW=w; *outH=h;
  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_RGBA8, w,h,0, GL_BGRA,GL_UNSIGNED_BYTE, pixels);
  glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAX_LEVEL, 0); //設mipmap層數
  free(pixels);
  return textureID;
}

//成功傳回programID,失敗傳回0
static uint32_t createProgram(uint32_t vsID, uint32_t fsID){
  programID=glCreateProgram();
  glAttachShader(programID, vsID);
  glAttachShader(programID, fsID);
  glLinkProgram(programID);
  glDetachShader(programID, vsID);
  glDetachShader(programID, fsID);

  int ret;
  glGetProgramiv(programID,GL_LINK_STATUS, &ret);
  if(ret){
    glUseProgram(programID);
    return programID;
  }
  //print program error
  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 0;
}

initSettings()裡,shader、program、sampler物件,以及各種設定值跟以前大致相同,也不再解釋。
static void initSettings(){
  //compile vertex shader
  uint32_t vsID=glCreateShader(GL_VERTEX_SHADER);
  if(loadShader(vsID, "uploaddata.vert.glsl")){
    return 1;
  }
  //compile fragment shader
  uint32_t fsID=glCreateShader(GL_FRAGMENT_SHADER);
  if(loadShader(fsID, "uploaddata.frag.glsl")){
    return 1;
  }

  //create program object
  programID=createProgram(vsID, fsID);
  glDeleteShader(vsID); //之後不會再用到shader物件,可刪除
  glDeleteShader(fsID);
  if(!programID){
    return 1;
  }

  //rasterizer
  glDisable(GL_CULL_FACE);
  glDisable(GL_MULTISAMPLE);
  glViewport(0, 0, WINDOW_W, WINDOW_H);
  //depth and stencil
  glDisable(GL_DEPTH_TEST);
  glDisable(GL_STENCIL_TEST);
  //blend
  glEnable(GL_BLEND);
  glBlendEquation(GL_FUNC_ADD);
  glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ONE_MINUS_SRC_ALPHA);

  //sampler
  glGenSamplers(1, &sampler);
  glSamplerParameteri(sampler, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
  glSamplerParameteri(sampler, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
  glSamplerParameteri(sampler, GL_TEXTURE_WRAP_S, GL_REPEAT);
  glSamplerParameteri(sampler, GL_TEXTURE_WRAP_T, GL_REPEAT);
  //將這個物件與shader裡的sampler物件對應
  int location=glGetUniformLocation(programID, "sampler0");
  glUniform1i(location, 0);
  glBindSampler(0, sampler);

  //設定清除畫面的顏色
  const float color[]={1,1,1,1};
  glClearColor(color[0],color[1],color[2],color[3]);

貼圖、buffer和VAO是資料,比較需要在執行途中建立或刪除。這三樣其實不要用全域變數寫死,用物件管理比較好,但本篇不希望篇幅過長,所以用全域變數且在程式開始時一併建立物件,動態管理等以後有需要再用。
之前說過「glBindBuffer(GL_ARRAY_BUFFER, vertexData);」跟其他bind函式不一樣,此函式不是把物件放到顯卡少女的工作臺上,只是把物件ID指定給一個叫GL_ARRAY_BUFFER的變數,呼叫glVertexAttribPointer()才把「資料來自哪個buffer物件」存進VAO。
  //texture,產生物件同時bind texture
  glActiveTexture(GL_TEXTURE0);
  texture = loadTexture("remilia.png", &imageW, &imageH);

  //本章重點之一:vertex data
  glGenBuffers(1, &vertexData);
  glBindBuffer(GL_ARRAY_BUFFER, vertexData);
  glBufferData(GL_ARRAY_BUFFER, sizeof(VertexData),NULL, GL_STREAM_DRAW);

  //vertex array object
  glGenVertexArrays(1,&vertexArrayObj);
  glBindVertexArray(vertexArrayObj);
  glEnableVertexAttribArray(0);
  glVertexAttribPointer(0,2, GL_FLOAT,0,0,0);
  glEnableVertexAttribArray(1);
  glVertexAttribPointer(1,2, GL_SHORT,0,0, (void*)offsetof(VertexData, texCoord));

  //本章重點之二:uniform buffer
  glGenBuffers(1, &uniformBuffer);
  glBindBuffer(GL_UNIFORM_BUFFER, uniformBuffer);
  glBufferData(GL_UNIFORM_BUFFER, sizeof(UniformBuffer), NULL, GL_STREAM_DRAW);
  //將這個物件與shader裡的buffer物件對應
  uint32_t uniformIndex=glGetUniformBlockIndex(programID, "uniform0");
  glUniformBlockBinding(programID, uniformIndex, 0);
  glBindBufferBase(GL_UNIFORM_BUFFER, 0, uniformBuffer);

  //邏輯物件的初值
  memset(&myCharacter, 0, sizeof(myCharacter));
  myCharacter.x=WINDOW_W-BORDER; //把位置設在右邊中央
  myCharacter.y=WINDOW_H/2;
  myCharacter.texW=imageW/CELL_NUMBER; //一個畫格的大小
  myCharacter.texH=imageH;
  return 0;
}
glBufferData()第三參數填NULL,代表此時只設定預留的byte數,之後再上傳資料。
OpenGL wiki: glBufferData
OpenGL wiki: Buffer Object Usage
第4參數是我們要如何使用這個buffer,之前已經看過一個GL_STATIC_DRAW,全部有9個值可以填。
先考慮CPU是否存取這個buffer,有三種情況
DRAW:CPU會上傳但不會讀取
READ:CPU會讀取
COPY:只被GPU讀寫,CPU不會存取
再來是內容被修改的頻率,STATIC<DYNAMIC<STREAM。
這兩種flag排列組合就形成9個常數。

看起來跟D3D的Usage有點像,不同的是這只是hint而不會限制buffer可以做何用途,把一個buffer設成STATIC_COPY之後還是可以從CPU上傳資料,這是讓驅動程式和顯示晶片根據hint選擇儲存資料的方式,以提升效能。

最後是邏輯物件初值,本篇寫在初始化函式裡,但正式做遊戲時這種物件都是途中動態產生和刪除。

nextFrame()是邏輯,跟D3D篇完全一樣,把系統和邏輯分離的好處之一是在不同平臺邏輯部分的code可以共用。
static float clamp(float value, float min, float max){
  if(value<=min){ return min; }
  if(value>=max){ return max; }
  return value;
}

static void nextFrame(){
  //計算圖位置
  float v[2]={0,0};
  if(keyState.up){
    v[1]=-SPEED;
  }else if(keyState.down){
    v[1]=SPEED;
  }
  if(keyState.left){
    v[0]=-SPEED;
  }else if(keyState.right){
    v[0]=SPEED;
  }
  myCharacter.x+=v[0];
  myCharacter.y+=v[1];
  //防止圖跑到畫面外
  myCharacter.x=clamp(myCharacter.x, BORDER, WINDOW_W-BORDER);
  myCharacter.y=clamp(myCharacter.y, BORDER, WINDOW_H-BORDER);

  //換畫格
  myCharacter.frameCounter++;
  if(myCharacter.frameCounter == FRAME_TIME){
    myCharacter.frameCounter=0;
    myCharacter.texX+=myCharacter.texW;
    if(myCharacter.texX >= imageW){
      myCharacter.texX=0;
    }
  }

  //修改顏色
  if(keyState.space){
    memset(globalColor, 0, sizeof(float)*3);
  }else{
    for(int i=0;i<3;i++){
      globalColor[i]=1.0;
    }
  }
}
前面用struct keyState把按鈕狀態記錄下來,這裡用keyState來判斷。

換畫格的做法是用一個計數器,每個frame增加1,加到一定數量就歸零並修改貼圖坐標。

drawScreen()是呼叫OpenGL的函式繪圖。
static void drawScreen(){
  glClear(GL_COLOR_BUFFER_BIT);
  //畫出物體,先算出4個頂點的坐標
  //4個點分別是左上、左下、右上、右下

  float pos[8];
  short texCoord[8];
  pos[0]= myCharacter.x-myCharacter.texW/2.0;
  pos[1]= myCharacter.y-myCharacter.texH/2.0;
  pos[6]= myCharacter.x+myCharacter.texW/2.0;
  pos[7]= myCharacter.y+myCharacter.texH/2.0;
  pos[2]= pos[0];
  pos[3]= pos[7];
  pos[4]= pos[6];
  pos[5]= pos[1];
  texCoord[0]= myCharacter.texX;
  texCoord[1]= myCharacter.texY;
  texCoord[6]= myCharacter.texX+myCharacter.texW;
  texCoord[7]= myCharacter.texY+myCharacter.texH;
  texCoord[2]= texCoord[0];
  texCoord[3]= texCoord[7];
  texCoord[4]= texCoord[6];
  texCoord[5]= texCoord[1];
  //將頂點坐標上傳到vertexData
  glBindBuffer(GL_ARRAY_BUFFER, vertexData);
  VertexData* destPtr1=glMapBufferRange(GL_ARRAY_BUFFER,0,sizeof(VertexData),
    GL_MAP_WRITE_BIT|GL_MAP_INVALIDATE_BUFFER_BIT);
  memcpy(destPtr1->pos, pos, sizeof(pos));
  memcpy(destPtr1->texCoord, texCoord, sizeof(texCoord));
  glUnmapBuffer(GL_ARRAY_BUFFER);

  //將globalColor與視窗大小上傳到uniform buffer
  glBindBuffer(GL_UNIFORM_BUFFER, uniformBuffer);
  UniformBuffer* destPtr2=glMapBufferRange(GL_UNIFORM_BUFFER,0,sizeof(UniformBuffer),
    GL_MAP_WRITE_BIT|GL_MAP_INVALIDATE_BUFFER_BIT);
  memcpy(destPtr2->globalColor, globalColor, sizeof(globalColor));
  destPtr2->windowSizeRcp[0]= 1.0/WINDOW_W;
  destPtr2->windowSizeRcp[1]= 1.0/WINDOW_H;
  glUnmapBuffer(GL_UNIFORM_BUFFER);

  glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
  glXSwapBuffers(dsp, window);
}
myCharacter裡記錄的是圖的xy坐標、貼圖的xywh,要先算出矩形4個頂點的坐標才能給GL畫圖。
把中心點坐標與畫格寬高的一半相加或相減,算出四個頂點的位置。

「texW/2.0」程式寫成除法,但編譯器也知道除法比乘法慢得多,會儘量用乘法代替除法,像這樣除以浮點常數的時候,編譯時會轉換成*0.5。

上傳資料到buffer物件有3種方法
  1. 再呼叫一次glBufferData(),這會把之前的資料清除。
    看起來會把buffer空間釋放再重建,但size和usage參數跟之前相同的話,系統有可能繼續用原本的空間。
  2. glBufferSubData(),可以只修改buffer的一部分。
    OpenGL wiki: glBufferSubData
  3. map、unmap
    呼叫glMapBufferRange(),系統會配置一塊記憶體並把指標傳回,你在這塊空間填資料,填好後呼叫glUnmapBuffer()把資料上傳。
    OpenGL wiki: glMapBufferRange
    OpenGL wiki: glUnmapBuffer
    另有一個函式叫glMapBuffer(),這好像是比較早的規格,access參數能用的值比較少。
    OpenGL wiki: glMapBuffer
我有一次量一下執行時間,三種方法差不了多少,看網路上別人的筆記,哪個方法快每個人測的結果也不一樣,實際使用就看哪個方法程式碼比較簡短,或自己量量看執行時間吧。

指定buffer物件的方法是傳統的bind:glBindBuffer()把物件ID指定給內部的全域變數,這些函式用target參數得知要操作哪個buffer,用OpenGL要習慣這種方式。
本篇GL_ARRAY_BUFFER與GL_UNIFORM_BUFFER在initSettings()裡設定好之後就不再改變,把drawScreen()裡兩行glBindBuffer()拿掉不影響結果。

如果上傳資料時之前的資料完全不需要保留,將這一點告訴OpenGL可能可以提升效能。
GPU與CPU並不是同步執行,CPU上傳資料時有可能GPU正在使用這個buffer,此時系統可以配置另一塊空間放資料,等GPU用完buffer後把前一塊空間釋放,叫GPU改用新的空間,這樣CPU就不用等待GPU。
glBufferData()的size和usage參數填跟初始化一樣的值可以達到效果,也可以glMapBufferRange()第四參數包含GL_MAP_INVALIDATE_BUFFER_BIT。

本篇用map的方法。前面宣告兩個struct:VertexData和UniformBuffer就是要填入的資料,宣告一個指標指向Map()傳回的空間,填入struct各個欄位。

build的指令跟前篇一樣
gcc uploaddata.c -o uploaddata -s -Os -lX11 -lGL `pkg-config --cflags --libs gdk-pixbuf-2.0`

執行的樣子
(此圖檔是30fps,是實際程式的一半)


其實還有一些可以改進的地方,請參照Direct3D篇最後面。
Direct3D篇

創作回應

更多創作