• [转]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");
    }

    那么上面我们已经讲了一堆,接下来就开始写代码了

    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,每个像素1字节
        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,每个像素1字节
        // 如果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还没有绘制

    原文链接额:

    OpenGL 使用 PBO 高速复制屏幕图像到内存或者纹理中

  • 相关阅读:
    图片不能显示
    Lambda表达式where过滤数据
    存储文本到一个文件里
    获取用户临时文件夹路径
    判断某一个字符串是否存在另一个字符串中
    使用反射为特性赋值
    字符串与数据流之间的转换
    控制台应用程序获取计算机名
    重复输出字符或字符串
    使用HashSet<>去除重复元素的集合
  • 原文地址:https://www.cnblogs.com/rainbow70626/p/8976095.html
Copyright © 2020-2023  润新知