前篇 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動畫,我把各個畫格拆開。
畫格是這樣,以中央為定位點。長度不是寫在程式裡,而是從圖檔寬高計算。
這次做了三種動態
邏輯跟使用貼圖篇差不多,顏色改放在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
主要邏輯跟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,拿出來放在另一個函式。
initSettings()裡,shader、program、sampler物件,以及各種設定值跟以前大致相同,也不再解釋。
貼圖、buffer和VAO是資料,比較需要在執行途中建立或刪除。這三樣其實不要用全域變數寫死,用物件管理比較好,但本篇不希望篇幅過長,所以用全域變數且在程式開始時一併建立物件,動態管理等以後有需要再用。
之前說過「glBindBuffer(GL_ARRAY_BUFFER, vertexData);」跟其他bind函式不一樣,此函式不是把物件放到顯卡少女的工作臺上,只是把物件ID指定給一個叫GL_ARRAY_BUFFER的變數,呼叫glVertexAttribPointer()才把「資料來自哪個buffer物件」存進VAO。
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可以共用。
前面用struct keyState把按鈕狀態記錄下來,這裡用keyState來判斷。
換畫格的做法是用一個計數器,每個frame增加1,加到一定數量就歸零並修改貼圖坐標。
drawScreen()是呼叫OpenGL的函式繪圖。
myCharacter裡記錄的是圖的xy坐標、貼圖的xywh,要先算出矩形4個頂點的坐標才能給GL畫圖。
把中心點坐標與畫格寬高的一半相加或相減,算出四個頂點的位置。
「texW/2.0」程式寫成除法,但編譯器也知道除法比乘法慢得多,會儘量用乘法代替除法,像這樣除以浮點常數的時候,編譯時會轉換成*0.5。
上傳資料到buffer物件有3種方法
指定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的指令跟前篇一樣
執行的樣子
(此圖檔是30fps,是實際程式的一半)
其實還有一些可以改進的地方,請參照Direct3D篇最後面。
Direct3D篇
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鍵讓圖上下左右移動。
- 按空白鍵不放時圖變成黑色,放開後恢復。
- 每隔一段時間換畫格。
//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; } |
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; } |
程式開始宣告一些變數記錄狀態,並設定一些動畫相關數值。這裡速率、畫格數等數值都寫在程式裡,因為本篇想儘量簡化主題以外的部分,正式做軟體時可以看情況放在外部檔案。
程式結構方面,將邏輯和繪圖分開:用作業系統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; } |
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; } } } |
換畫格的做法是用一個計數器,每個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); } |
把中心點坐標與畫格寬高的一半相加或相減,算出四個頂點的位置。
「texW/2.0」程式寫成除法,但編譯器也知道除法比乘法慢得多,會儘量用乘法代替除法,像這樣除以浮點常數的時候,編譯時會轉換成*0.5。
上傳資料到buffer物件有3種方法
- 再呼叫一次glBufferData(),這會把之前的資料清除。
看起來會把buffer空間釋放再重建,但size和usage參數跟之前相同的話,系統有可能繼續用原本的空間。 - glBufferSubData(),可以只修改buffer的一部分。
OpenGL wiki: glBufferSubData - 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篇