OpenGL 使用 PBO 高速復制屏幕圖像到內存或者紋理中


如果你想給游戲做個截圖功能,或者想把屏幕圖像弄成一個紋理,你就非常需要 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還沒有繪制

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM