• OpenGL进阶(二)之像素缓冲PixelBuffer


    本系列文章为Learn OpenGL个人学习总结!
    OpenGL入门(一)之认识OpenGL和创建Window
    OpenGL入门(二)之渲染管线pipeline,VAO、VBO和EBO
    OpenGL入门(三)之着色器Shader
    OpenGL入门(四)之纹理Texture
    OpenGL入门(五)之Matrix矩阵操作和坐标系统
    OpenGL进阶(一)之帧缓冲FrameBuffer
    OpenGL进阶(二)之像素缓冲PixelBuffer

    PBO

    说起PBO,不得不提一篇文章:
    http://www.songho.ca/opengl/gl_pbo.html

    本篇文章,更多的是对该文章的翻译和提炼总结!另外文章提到的PBO当时还是OpenGL中的一个拓展,在v2.1版本开始已经是核心功能了!

    什么是PBO

    PBO(Pixel Buffer Object)非常类似VBO,以便于将顶点数据和像素数据存储到缓冲对象中,这种存储像素数据的缓冲区对象称为像素缓冲区对象 (PBO)。
    另外,添加了2个额外的“目标”标志。这些标志协助绑定的PBO内存管理器(OpenGL驱动)决定缓冲对象存储的最好的位置:系统内存,共享内存或者显存
    另外,目标标志清楚的指出PBO绑定将用于2种操作:GL_PIXEL_PACK_BUFFER用于将像素数据传送到PBO,或者GL_PIXEL_UNPACK_BUFFER将PBO传输到像素数据。
    在这里插入图片描述
    举个例子,glReadPixels()glGetTexImage()是“打包(pack)”像素操作, 而glDrawPixels(), glTexImage2D()glTexSubImage2D()是“解压(unpack)”操作.当一个PBO绑定到GL_PIXEL_PACK_BUFFER标志上时,glReadPixels()从OpenGL的帧缓冲(framebuffer)读取像素数据(pixel data),然后把数据画(打包pack)到PBO中。当一个PBO绑定到GL_PIXEL_UNPACK标志上时,glDrawPixels()从PBO读取(unpack)或复制像素数据到OpenGL帧缓冲(framebuffer)中。

    为什么要用PBO

    PBO的主要优势是通过直接的内存访问(Direct Memory Access,DMA)而不用涉及到CPU周期就可以快速的将像素数据传输到或者传输出显卡。PBO的另一个优势是异步的直接内存访问传输,让我们比较下传统的纹理传输方法和使用像素缓冲对象。下面左图是用传统的方式从图像源(图像文件或者视频流)去加载图片数据.图像源数据首先加载到系统内存中,然后,通过glTexImage2D()从系统内存里面拷贝到OpenGL纹理对象。这2个传输步骤(加载和拷贝)都是CPU执行的。
    没有PBO的纹理加载使用PBO加载纹理
    相反,在右侧图中,图像源可以直接加载到由 OpenGL 控制的 PBO 中。CPU 仍然涉及将图像源加载到 PBO,但不用于将像素数据从 PBO 传输到纹理对象。相反,GPU(OpenGL 驱动程序)管理将数据从 PBO 复制到纹理对象。这意味着 OpenGL 在不浪费 CPU 周期的情况下执行 DMA 传输操作。此外,OpenGL 可以安排异步 DMA 传输以供以后执行。因此,glTexImage2D() 立即返回,CPU 可以执行其他操作,而无需等待像素传输完成。

    有两种主要的 PBO 方法可以提高像素数据传输的性能:流式纹理更新从帧缓冲区异步回读

    创建PBO

    开头提到PBO和VBO非常类似,不同的是多了两个额外标志GL_PIXEL_PACK_BUFFERGL_PIXEL_UNPACK_BUFFERGL_PIXEL_PACK_BUFFER是用来将像素数据从OpenGL传输到你的应用的,而GL_PIXEL_UNPACK_BUFFER是用来将像素数据从应用传输到OpenGL中的。OpenGL参考这些标志决定PBO的最佳内存空间,例如,用于上传(解包unpacking)纹理的视频内存,或用于读取(打包packing)帧缓冲区的系统内存。然后,这些目标标志只是用来提示而已。OpenGL驱动会为你决定合适的位置。

    创建 PBO 需要 3 个步骤;

    • 使用glGenBuffers()生成一个新的缓冲区对象。
    • 使用glBindBuffer()绑定缓冲区对象。
    • 使用glBufferData()将像素数据复制到缓冲区对象。

    如果在 glBufferData() 中指定指向源数组的 NULL 指针,则 PBO 仅分配具有给定数据大小的内存空间。glBufferData() 的最后一个参数是 PBO 提供如何使用缓冲区对象的另一个性能提示。GL_STREAM_DRAW用于流式纹理上传,GL_STREAM_READ用于异步帧缓冲区回读

    映射PBO(Mapping PBO)

    PBO 提供了一种内存映射机制,将 OpenGL 控制的缓冲区对象映射到客户端的内存地址空间(GPU—>CPU)。因此,客户端可以使用glMapBuffer()glUnmapBuffer()修改缓冲区对象的一部分或整个缓冲区。

    void* glMapBuffer(GLenum target, GLenum access)
    
    GLboolean glUnmapBuffer(GLenum target)
    
    • 1
    • 2
    • 3

    如果成功,glMapBuffer() 返回指向缓冲区对象的指针。否则返回 NULL。目标参数是GL_PIXEL_PACK_BUFFERGL_PIXEL_UNPACK_BUFFER 。第二个参数,access指定如何处理映射的缓冲区;从 PBO 读取数据 (GL_READ_ONLY),将数据写入 PBO (GL_WRITE_ONLY),或两者兼而有之 (GL_READ_WRITE)。

    请注意,如果 GPU 仍在使用缓冲区对象,则glMapBuffer()将不会返回,直到 GPU 完成其与相应缓冲区对象的工作。为了避免这种停顿(等待),请在glMapBuffer()之前使用 NULL 指针调用 glBufferData()。然后,OpenGL 会丢弃旧的缓冲区,并为缓冲区对象分配新的内存空间。

    使用 PBO 后,必须使用 glUnmapBuffer() 取消映射缓冲区对象。如果成功,glUnmapBuffer() 返回 GL_TRUE。否则,它返回 GL_FALSE。

    两个使用PBO提高像素传输性能的例子

    流式纹理更新

    纹理源在 PBO 模式下的每一帧都直接写入映射的像素缓冲区。然后,使用 glTexSubImage2D() 将这些数据从 PBO 传输到纹理对象。通过使用 PBO,OpenGL 可以在 PBO 和纹理对象之间执行异步 DMA 传输。它显着提高了纹理上传性能。如果支持异步 DMA 传输,glTexSubImage2D() 应该立即返回,CPU 可以处理其他作业而无需等待实际的纹理复制。
    在这里插入图片描述
    为了最大限度地提高流传输性能,您可以使用多个像素缓冲区对象。上图显示同时使用了 2 个 PBO;glTexSubImage2D() 从一个 PBO 复制像素数据,同时将纹理源写入另一个 PBO。

    对于第n帧,PBO 1用于 glTexSubImage2D() 并且PBO 2用于获取新的纹理源。对于第n+1帧,2 个像素缓冲区正在切换角色并继续更新纹理。由于异步 DMA 传输,可以同时执行更新和复制过程。CPU 将纹理源更新为 PBO,而 GPU 从另一个 PBO 复制纹理。

    下边我们使用前边文章中的木箱纹理,来模拟一下两个PBO更新纹理的流程:

        //加载图片
        int width, height, nrChannels;
        stbi_set_flip_vertically_on_load(true); // OpenGL要求y轴0.0坐标是在图片的底部的,但是图片的y轴0.0坐标通常在顶部。
        //这里会拿到图像的宽高和颜色通道个数  3
        unsigned char *data = stbi_load("../dependency/stb/container.jpg", &width, &height, &nrChannels, 0);
    
        int data_size = width*height*3;
    
        unsigned int pboIds[2]; // IDs of PBO
        // create 2 pixel buffer objects, you need to delete them when program exits.
        // glBufferData() with NULL pointer reserves only memory space.
        // 第三个参数data传NULL,只保留内存空间
        glGenBuffers(2, pboIds);
        glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pboIds[0]);
        glBufferData(GL_PIXEL_UNPACK_BUFFER, data_size, 0, GL_STREAM_DRAW);
    
        glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pboIds[1]);
        glBufferData(GL_PIXEL_UNPACK_BUFFER, data_size, 0, GL_STREAM_DRAW);
    
        glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
    
        //生成纹理
        unsigned int texture1;
        glGenTextures(1, &texture1);
        glBindTexture(GL_TEXTURE_2D, texture1);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); 
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
        //data传0
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, 0);
    
        //传统的直接上传纹理
        // glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
        glGenerateMipmap(GL_TEXTURE_2D);
    
        ....
        
        //渲染
        while (!glfwWindowShouldClose(window))
        {
            processInput(window);
    
            glClearColor(0.2f, 0.3f, 0.3f, 1.0f);               
            glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    
            static int index = 0;
            int nextIndex = 0; // pbo index used for next frame
            // In dual PBO mode, increment current index first then get the next index
            index = (index + 1) % 2;
            nextIndex = (index + 1) % 2;
    
            // bind the texture and PBO
            glBindTexture(GL_TEXTURE_2D, texture1);
            glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pboIds[index]);
            /*________________________________*/
    
            // ----start----
            // copy pixels from PBO to texture object
            // Use offset instead of ponter.
            glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width, height, GL_RGB, GL_UNSIGNED_BYTE, 0);
            
            // bind PBO to update pixel values
            glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pboIds[nextIndex]);
    
            // map the buffer object into client's memory
            // Note that glMapBuffer() causes sync issue.
            // If GPU is working with this buffer, glMapBuffer() will wait(stall)
            // for GPU to finish its job. To avoid waiting (stall), you can call
            // first glBufferData() with NULL pointer before glMapBuffer().
            // If you do that, the previous data in PBO will be discarded and
            // glMapBuffer() returns a new allocated pointer immediately
            // even if GPU is still working with the previous data.
            glBufferData(GL_PIXEL_UNPACK_BUFFER, data_size, 0, GL_STREAM_DRAW);
            unsigned char *ptr = (unsigned char*)glMapBuffer(GL_PIXEL_UNPACK_BUFFER, GL_WRITE_ONLY);
            if (ptr)
            {
                // update data directly on the mapped buffer
                memcpy(ptr, data, data_size);
                glUnmapBuffer(GL_PIXEL_UNPACK_BUFFER); // release pointer to mapping buffer
            }
            // ----end----
            // it is good idea to release PBOs with ID 0 after use.
            // Once bound with 0, all pixel operations behave normal ways.
            glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
    
            glBindVertexArray(VAO);
            glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
    
            ....
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91

    上边几个地方需要注意:

    1. glBufferData(GL_PIXEL_UNPACK_BUFFER, data_size, 0, GL_STREAM_DRAW);中第三个参数传0,只保留内存空间
    2. glTexImage2D()生成纹理时,最后一个参数data传0,同样是保留内存空间
    3. 在渲染中glTexSubImage2D()即通知GPU将当前绑定的PBO中数据上传到纹理对象中。因为绑定了PBO,所以最后一个参数data也传0。
      题外话:使用glTexSubImage2D()要求宽高和通道数都要与glTexImage2D()中的相同!
    4. 绑定另外一个PBO,将图像数据更新到另一个PBO中。因为这里使用了PBO的内存映射机制,所以在此之前glBufferData(GL_PIXEL_UNPACK_BUFFER, data_size, 0, GL_STREAM_DRAW);第三个参数data也传0。如果不用下边的内存映射,可以直接传图像数据data。

    从帧缓冲区异步回读

    传统的 glReadPixels() 会阻塞管道并等待所有像素数据传输完毕。然后,它将控制权返回给应用程序。相反,带有 PBO 的 glReadPixels() 可以安排异步 DMA 传输并立即返回而不会停顿。因此,应用程序(CPU)可以立即执行其他进程,同时通过 OpenGL(GPU)使用 DMA 传输数据。
    在这里插入图片描述
    上图使用 2 个像素缓冲区。在第 n帧,应用程序使用 glReadPixels()将像素数据从 OpenGL 帧缓冲区读取到PBO 1 ,并处理PBO 2中的像素数据。这些读取和处理可以同时执行,因为对PBO 1的 glReadPixels() 会立即返回,并且 CPU 会立即开始处理PBO 2中的数据。而且,我们在每一帧上 交替使用PBO 1和PBO 2 。

    关于FBO的内容,可以参考上一篇文章:OpenGL进阶(一)之帧缓冲FrameBuffer

        glBindFramebuffer(GL_FRAMEBUFFER, fbo);
        
        //初始化
        unsigned int pboIds[2]; // IDs of PBO
        // create 2 pixel buffer objects, you need to delete them when program exits.
        // glBufferData() with NULL pointer reserves only memory space.
        // 第三个参数data传NULL,只保留内存空间
        glGenBuffers(2, pboIds);
        glBindBuffer(GL_PIXEL_PACK_BUFFER, pboIds[0]);
        glBufferData(GL_PIXEL_PACK_BUFFER, data_size, 0, GL_STREAM_READ);
    
        glBindBuffer(GL_PIXEL_PACK_BUFFER, pboIds[1]);
        glBufferData(GL_PIXEL_PACK_BUFFER, data_size, 0, GL_STREAM_READ);
    
        glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
    
        ...
        
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    首先创建没有什么好说的,跟上边上传纹是一样的,只不过类型修改一下GL_PIXEL_PACK_BUFFERGL_STREAM_READ

        //绑定到第一个PBO
        glBindBuffer(GL_PIXEL_PACK_BUFFER, mPboIds[index]);
        //调用glReadPixels通知GPU把数据拷贝到第一个pbo里
        glReadPixels(0, 0, width, height, GL_RGB, GL_UNSIGNED_BYTE, 0);
        
        //绑定到第二个PBO
        glBindBuffer(GL_PIXEL_PACK_BUFFER, mPboIds[mPboIdNewIndex]);
    
        //映射内存,pbo->cpu
        void *pixelsPtr = glMapBufferRange(GL_PIXEL_PACK_BUFFER, 0, data_size, GL_MAP_READ_BIT);
        if (pixelsPtr) {
            memcpy(data, static_cast<unsigned char *>(pixelsPtr), data_size);
            glUnmapBuffer(GL_PIXEL_PACK_BUFFER);
        }
        
        glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
        glBindFramebuffer(GL_FRAMEBUFFER, 0);
    
        index = (index + 1) % 2;
        nextIndex = (nextIndex + 1) % 2;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    首先glReadPixels()函数:
    参数1,2:要从frame buffer中读取的第一个像素的坐标
    参数3:指定像素数据的格式
    参数4:指定像素数据的数据类型
    参数5:具体的像素数据指针。这里使用了FBO,所以传0

    然后,glMapBufferRange()和上边glMapBuffer()相同,使用内存映射,将PBO2中的数据读取复制出来!
    最后交换索引!

  • 相关阅读:
    人力资源小程序
    使用C语言实现并查集
    TortoiseGit安装教程(Windows)
    将nestjs项目迁移到阿里云函数
    33.CSS发光按钮的悬停效果
    store redux在项目中的应用
    MySQL 数据库(DBMS)安装教程图文详解
    VScode插件汇总
    C++基础从0到1入门编程(三)
    HJ16 购物单
  • 原文地址:https://blog.csdn.net/aiynmimi/article/details/126767018