如果你想給游戲做個截圖功能,或者想把屏幕圖像弄成一個紋理,你就非常需要 PBO 了
通常情況下,你想把屏幕圖像的像素數據讀到內存需要用 glReadPixels 然后 pixels 參數傳進去一塊內存地址
這樣做是非常非常不好的,因為 glReadPixels 會把屏幕圖像的像素數據從顯卡的顯存復制到內存條,這個過程就非常非常的慢,特別是數據量大的時候
然后如果你要把像素數據再用 glTexImage2D 傳到紋理,數據就又要從內存條復制到顯存,這個過程也是非常非常慢的,特別是數據量大的時候
那么有沒有一種辦法,讓我們可以通過一個內存指針,直接訪問顯存的數據呢?當然是有的,那就是 OpenGL 的 Array Buffer
這個東西中文叫做 數組緩沖區 也可以直接省略成 緩沖區 因為它就是顯存里的一塊內存,所以我們下文就叫 緩沖區 吧
你可以用 glMapBuffer 得到它的內存指針,然后就可以為所欲為了,另外,OpenGL 很多用來返回數據的函數,都可以把數據寫到緩沖區里,而不是復制到內存條。
就比如說 glReadPixels 原本你是要傳一個內存指針進去的,但是有了緩沖區,它就可以把數據復制到緩沖區里而不是復制到內存條
因為,屏幕的像素數據是在顯存里的,緩沖區也是在顯存里的,所以,顯存->復制數據->顯存 速度就比 顯存->復制數據->內存條 快非常非常的多
然后我們直接用 glMapBuffer 來獲取緩沖區的內存地址,就能訪問到復制好的屏幕像素數據了,接着該干嘛干嘛。
而且,OpenGL 的一些函數可以把數據寫入到緩沖區里,還有些函數也可以從緩沖區里讀取數據來用,比如,glTexImage2D 什么的,如果你很聰明,你已經知道接下來要干嘛了
假如我們上一步把屏幕的像素數據讀取到緩沖區里了,我們就可以直接用 glTexImage2D、glTexSubImage2D 什么的函數把緩沖區里的數據傳給紋理了
這樣我們就把屏幕圖像存儲在紋理了,然后干嘛干嘛,並且這個過程完全不關內存條的事,所以速度也是非常非常的快
當然我們需要用來操作Buffer的函數。你也可以用GLEW庫來偷懶
#include "glext.h" PFNGLBINDBUFFERPROC glBindBuffer = NULL; PFNGLBUFFERDATAPROC glBufferData = NULL; PFNGLGENBUFFERSPROC glGenBuffers = NULL; PFNGLMAPBUFFERPROC glMapBuffer = NULL; PFNGLUNMAPBUFFERPROC glUnmapBuffer = NULL; void gl_init() { glBindBuffer = (PFNGLBINDBUFFERPROC)wglGetProcAddress("glBindBuffer"); glBufferData = (PFNGLBUFFERDATAPROC)wglGetProcAddress("glBufferData"); glGenBuffers = (PFNGLGENBUFFERSPROC)wglGetProcAddress("glGenBuffers"); glMapBuffer = (PFNGLMAPBUFFERPROC)wglGetProcAddress("glMapBuffer"); glUnmapBuffer = (PFNGLUNMAPBUFFERPROC)wglGetProcAddress("glUnmapBuffer"); }
那么上面我們已經BB了一堆,接下來就開始寫代碼了
GLuint Buffer; GLuint Texture; void init() { // 創建1個緩沖區 glGenBuffers(1, &Buffer); // 緩沖區剛創建出來的時候還沒有分配內存,所以我們要初始化一下它 // 先綁定.. glBindBuffer(GL_ARRAY_BUFFER, Buffer); // 這一步十分重要,第2個參數指定了這個緩沖區的大小,單位是字節,一定要注意 // 然后第3個參數是初始化用的數據,如果你傳個內存指針進去,這個函數就會把你的 // 數據復制到緩沖區里,我們這里一開始並不需要什么數據,所以傳個NULL就行了 // GL內部會給緩沖區分配內存,然后什么都不干,第4個參數可以優化顯存效率,指定 // 緩沖區中的數據讀寫頻繁程度,如果緩沖區中的數據不經常讀寫,可以傳入 GL_STATIC_**** // 這樣GL會把緩沖區放在內存數據不經常變動的區域,如果要經常讀寫緩沖區中的數據,可以傳 // 別的值,具體參考 @https://www.khronos.org/registry/OpenGL-Refpages/gl2.1/ 吧 // 注意這里的 BUFFER_SIZE 我們假設要復制整個屏幕的像素數據,格式為RGB就行了,那么 // 大小就是 屏幕寬度×屏幕高度×3,每個像素3字節 glBufferData(GL_ARRAY_BUFFER, BUFFER_SIZE, NULL, GL_STREAM_COPY); // 這樣我們的緩沖區就已經初始化好了,它現在已經有一塊可用的內存 // 隨時可以用 glMapBuffer 來訪問 // 初始化完了那么解綁吧 glBindBuffer(GL_ARRAY_BUFFER, 0); // 創建1個紋理,等會把屏幕復制到這個紋理 glGenTextures(1, &Texture); // 初始化紋理,不多解釋了 glBindTexture(GL_TEXTURE_2D, Texture); // 這里data參數傳NULL和上面緩沖區一樣,GL僅僅給紋理分配內存而已 // ScreenWide和ScreenTall是屏幕的寬度和高度 // 格式用RGB,因為屏幕不需要透明通道,所以紋理的像素數據大小是和上面的緩沖區大小一樣的 glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, ScreenWide, ScreenTall, 0, GL_RGB, GL_UNSIGNED_BYTE, NULL); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); // 初始化完了解綁 glBindTexture(GL_TEXTURE_2D, 0); } void draw() { // 假裝這里畫了游戲場景 // 首先我們要把緩沖區綁定到 GL_PIXEL_PACK_BUFFER 這個地方 glBindBuffer(GL_PIXEL_PACK_BUFFER, Buffer); // 這個函數會判斷 GL_PIXEL_PACK_BUFFER 這個地方有沒有綁定一個緩沖區,如果有,那就把數據寫入到這個緩沖區里 // 前4個參數就是要讀取的屏幕區域,不多解釋 // 格式是RGB,類型是BYTE,每個像素3字節 // 如果GL_PIXEL_PACK_BUFFER有綁定緩沖區,最后一個參數就作為偏移值來使用,這里不啰嗦沒用的東西了。 // 傳NULL就行 glReadPixels(0, 0, ScreenWide, ScreenTall, GL_RGB, GL_UNSIGNED_BYTE, NULL); // 好了我們已經成功把屏幕的像素數據復制到了緩沖區里 // 這時候,你可以用 glMapBuffer 得到緩沖區的內存指針,來讀取里面的像素數據,保存到圖片文件 // 完成截圖 /****** // 注意glMapBuffer的第1個參數不一定要是GL_PIXEL_PACK_BUFFER,你可以把緩沖區綁定到比如上面init函數的GL_ARRAY_BUFFER // 然后這里也傳GL_ARRAY_BUFFER,由於懶得再綁定一次,就接着用上面綁定的GL_PIXEL_PACK_BUFFER吧 void *data = glMapBuffer(GL_PIXEL_PACK_BUFFER, GL_READ_WRITE); if (data) { WriteTGA("screenshot.tga", ScreenWide, ScreenTall, data); glUnmapBuffer(GL_PIXEL_PACK_BUFFER); // 不要忘了解除Map } ******/ // 完事了把GL_PIXEL_PACK_BUFFER這個地方的緩沖區解綁掉,以免別的函數誤操作 qglBindBuffer(GL_PIXEL_PACK_BUFFER, 0); // 接着我們演示一下把緩沖區中的像素數據傳給紋理 // 首先我們把緩沖區綁定到 GL_PIXEL_UNPACK_BUFFER 這個地方。這里注意啊!GL_PIXEL_PACK_BUFFER 和 GL_PIXEL_UNPACK_BUFFER 是不同的! glBindBuffer(GL_PIXEL_UNPACK_BUFFER, Buffer); // 綁定紋理 glBindTexture(GL_TEXTURE_2D, Texture); // 這個函數會判斷 GL_PIXEL_UNPACK_BUFFER 這個地方有沒有綁定一個緩沖區 // 如果有,就從這個緩沖區讀取數據,而不是data參數指定的那個內存 // 前面參數很簡單就不解釋了,最后一個參數和上面glReadPixels同理,傳NULL就行 // 這樣glTexSubImage2D就會從我們的緩沖區中讀取數據了 // 這里為什么要用glTexSubImage2D呢,因為如果用glTexImage2D,glTexImage2D會銷毀紋理內存重新申請,glTexSubImage2D就僅僅只是更新紋理中的數據 // 這就提高了速度,並且優化了顯存的利用率 glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, ScreenWide, ScreenTall, GL_RGB, GL_UNSIGNED_BYTE, NULL); // 完事了把GL_PIXEL_UNPACK_BUFFER這個地方的緩沖區解綁掉,以免別的函數誤操作 glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0); // 這時候我們已經更新了紋理,我們可以把紋理畫出來看看 // 假裝這里有繪制紋理的代碼 }
代碼就這些了。如果你想了解上面用到的函數的詳細信息,一定要去看看 https://www.khronos.org/registry/OpenGL-Refpages/gl2.1 這個網頁
補充:GL_PIXEL_PACK_BUFFER 和 GL_PIXEL_UNPACK_BUFFER 統稱為 PBO(Pixel Buffer Object)因為這倆就是用來搞像素的,所以就叫 Pixel buffer 了唄
緩沖區什么東西都可以存,可不止像素,具體請搜索其它資料吧
這是紋理的樣子:
紋理中沒有看到HUD是因為我讀取屏幕像素的時候HUD還沒有繪制