OpenGL 按照三角形仿射变换并贴图渲染(正常渲染或离屏渲染以及异步优化)

时间:2021-09-09 18:34:42

标签:CG opengl

转载请说明:http://blog.csdn.net/hust_sheng/article/details/75268410


需求

  在2转8路或者4转64路虚拟视点合成的项目中,需要根据真实相机的真实视点合成虚拟位置(虚拟相机)的虚拟视点。最后一步的绘制过程大致如下:

OpenGL 按照三角形仿射变换并贴图渲染(正常渲染或离屏渲染以及异步优化)

  其实就是将图像以三角形为单位,从原始图像(左侧)向目标图像(右侧)映射,也即warp。本质是仿射变换的过程。


怎么用OpenGL实现上述过程?

  OpenGL的贴图渲染会基于GPU使用shader进行,效率较高,对于加速来说比较合适,贴图渲染也是OpenGL较为核心的功能。下面进行大致的介绍:

  • 环境
    VS2015
    glew/freeglut
    API:OpenGL2.0

    • 安装VS之后默认会包含OpenGL,不需要重新安装,但是如果需要其他第三方库,需要下载或者编译,相关内容见:windows下配置OpenGL 32位/64位环境(glut、freeglut、glew等工具)
    • 遗憾的是使用的是OpenGL2.0,貌似有点落伍,还是推荐3.0,之后也会补充3.0的实现思路。但是两个版本的OpenGL原理是一样的。

常见的渲染一般会分为两种:(1)渲染至窗口(2)离屏渲染

  • 头文件
#include <GL/glew/glew.h> // 必须在glut.h之前include
//
#include <GL/glut.h>
#include <GL/freeglut.h>
#include <GL/freeglut_ext.h>
#include <GL/freeglut_std.h>
  • 渲染至窗口
    使用glut即可

    • 初始化包括两个部分

      • 初始化窗口
      • 设置纹理对象(数据来源)以及相关参数
      void initOpenGL(int argc, char* argv[])
      {
      // GLUT 初始化
      glutInit(&argc, argv);

      glutInitDisplayMode(GLUT_RGB | GLUT_DOUBLE); // 双缓冲
      glutInitWindowPosition(0, 0);
      glutInitWindowSize(640, 480);
      glutCreateWindow("X-project");

      glEnable(GL_TEXTURE_2D);

      int texture_ID;
      glGenTextures(1, &texture_ID); // 分配一个新的纹理编号
      if (texture_ID == 0) {
      return;
      }

      glBindTexture(GL_TEXTURE_2D, texture_ID);
      glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); // 双线性插值
      glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
      glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER); // 对应原图像中超出边缘的像素按照边界扩展(黑边)
      glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
      glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);

      float border_color[4] = { 1.0f, 1.0f, 1.0f, 1.0f };
      glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, border_color);

      return ;
      }

    需要说明的是,如果我们没有指定渲染的缓冲区,OpenGL默认的渲染缓冲区是窗口绑定的Frame Buffer Object。整个流程简单来说如下图所示:

    OpenGL 按照三角形仿射变换并贴图渲染(正常渲染或离屏渲染以及异步优化)

    • 准备数据

      glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0,
      GL_BGR_EXT, GL_UNSIGNED_BYTE, tlImage->imageData);

      除了最后一个参数之外,其他参数是为了配置最后一个参数对应的图像数据(如果我们设置的是纹理的话,第一个参数是固定的)。最后一个参数:tlImage->imageData 表示贴图渲染的原图像,或者说是参考图像,我们的目的是得到贴图之后的图像。

    • 设置贴图渲染的像素点的对应关系(关键的一步)

    // 开始绑定
    glBegin(GL_TRIANGLES); // 指定映射方法:三角形或者矩形

    ... // 设置原图像和贴图之后的目标图像对应的顶点

    glEnd();

    设置原图像和贴图之后的目标图像对应的顶点 这句话的意思是什么呢?OpenGL需要知道 srcImage -> dstImage 之间的映射关系,才能进行“贴图”操作,有下面几种常见映射方法:

    1. 矩形映射
      首先将映射方式设置为:GL_QUADS,之后可以直接设置当前图像和目标图像的四个图像顶点,即依次矩形映射即可完成贴图过程,我们也可以设置多组小矩形之间的映射类似于下图。
    2. 三角形映射
      如果将映射方式设置为:GL_TRIANGLES,之后可以直接设置当前图像和目标图像按照三角形换分之后的顶点,如下图所示:
      OpenGL 按照三角形仿射变换并贴图渲染(正常渲染或离屏渲染以及异步优化)

      给一个例子:

      // 开始绑定
      glBegin(GL_TRIANGLES);

      // 上三角
      glTexCoord2d(u1_up, v1_up); // src
      glVertex3d(x1_up, y1_up, 1); // dst

      glTexCoord2d(u2_up, v2_up);
      glVertex3d(x2_up, y2_up, 1);

      glTexCoord2d(u3_up, v3_up);
      glVertex3d(x3_up, y3_up, 1);

      glEnd();

      glTexCoord2d 函数(纹理坐标,即原图像坐标)用于设置srcImage图像的三角形顶点,三组(x, y);
      glVertex3d 函数(顶点坐标,目标图像坐标)用于设置dstImage图像的三角形顶点,三组(x, y, 1),第三个参数表示法向量,2D设置为1即可。

      • 显示
      glutSwapBuffers();

      该函数的目的就是交换缓冲区,我们一开始会设置单缓冲或这是双缓冲(效率更高一点),上述函数就是将缓冲区中地数据交换到绑定至窗口的FBO中,我们就可以看到相应的图片了。

      • 数据读取

        pPixelData = (GLubyte*)malloc(PixelDataLength);
        if (pPixelData == 0)
        exit(0);

        ...

        // 读取像素
        glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
        glReadPixels(0, 0, width, height, GL_BGR_EXT, GL_UNSIGNED_BYTE, pPixelData);

        基本思路是通过glReadPixels函数读取GPU中对应缓冲区的数据,将其保存在预先设计好的buffer(pPixelData)里面,之后将buffer保存在本地,具体的过程见 链接

        需要说明的是:
          调用glReadPixels函数之前,需要加上 glReadBuffer(GL_FRONT); ,因为对于渲染到窗口的情况来说,窗口相当于FRONT,这句话相当于说,我们读取的对象是“前端”的数据。现在思考另一个问题,既然我们设置的是双缓冲!那么,glReadBuffer(GL_BACK); 是否有用呢?事实证明是的,这里也有 离屏渲染 的思想(注意思想)
          在调用glutSwapBuffers()函数之前,数据应该是在“后端”的,所以我们在此处就进行数据读取也是可以的,那么就需要设置 glReadBuffer(GL_BACK);


  • 离屏渲染

    相对于窗口渲染来说,头文件是一致的,主要基于glew。

    • 初始化包括四个部分

      • glut初始化

        // GLUT 初始化 
        glutInit(&argc, argv);
        glutInitDisplayMode(GLUT_RGB | GLUT_DOUBLE);
        glutInitWindowPosition(-100, -100);
        glutInitWindowSize(1, 1);
        glutCreateWindow("glew test");
      • glew初始化

        // GLEW 初始化
        GLenum err;
        err
        = glewInit();
      • 创建原图像数据纹理

        glEnable(GL_TEXTURE_2D);    // 使用纹理之前需要开启

        glGenTextures(1, &m_srctex);
        glBindTexture(GL_TEXTURE_2D, m_srctex);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
        glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);
      • 创建目的图像纹理并与FBO(也要创建)绑定

        glGenTextures(1, &m_dstfbotex);
        glBindTexture(GL_TEXTURE_2D, m_dstfbotex);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
        //glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, m_width, m_height, 0, GL_BGR, GL_UNSIGNED_BYTE, NULL);

        glEnable(GL_FRAMEBUFFER);
        glGenFramebuffers(1, &m_FboID);
        glBindFramebuffer(GL_FRAMEBUFFER, m_FboID);
        glFramebufferTexture2D(GL_FRAMEBUFFER,
        GL_COLOR_ATTACHMENT0, // 把纹理对象绑定到FBO的0号绑定点
        GL_TEXTURE_2D, m_dstfbotex, 0); // 0 表示使用原图像

        GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
        if (status != GL_FRAMEBUFFER_COMPLETE){
        exit(-1);
        }

        注意
          前面也说,OpenGL会将一个默认的FBO绑定到显示的窗口上面。这里我们自己创建一个FBO和一个纹理,再将两者绑定,这样系统就会使用我们自己创建的FBO,避免了屏幕渲染的过程,这就是离屏渲染

      • 保存状态变量

        glGetIntegerv(GL_FRAMEBUFFER_BINDING_EXT, &m_curbuff);

        glPushAttrib(GL_VIEWPORT_BIT); // 保存状态变量
        glMatrixMode(GL_PROJECTION);
        glPushMatrix();
        glMatrixMode(GL_MODELVIEW);
        glPushMatrix();

        glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT);

        glViewport(0, 0, m_width, m_height);

        glMatrixMode(GL_PROJECTION);
        glLoadIdentity();
        glMatrixMode(GL_MODELVIEW);
        glLoadIdentity();
        gluOrtho2D(0.0, 1.0, 0.0, 1.0);
      • 准备数据

        非常关键!

        glBindTexture(GL_TEXTURE_2D, m_srctex);         // 纹理选择绑定m_srctex纹理
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0,
        GL_BGR_EXT, GL_UNSIGNED_BYTE, tlImage->imageData); // 将原数据加载到m_srctex纹理

        ... // 设置贴图渲染的像素点的对应关系(关键的一步)
      • 渲染至我们自己设置的FBO

          采用离屏渲染不需要 glutSwapBuffers(); 但是需要注意的是,在读取FBO中数据的时候需要加上下面代码,表示读取的是我们自己的m_FboID对应的FBO中的数据,很重要!

        glBindFramebuffer(GL_READ_FRAMEBUFFER, m_FboID);
      • 恢复状态变量

        glMatrixMode(GL_MODELVIEW);
        glPopMatrix();
        glMatrixMode(GL_PROJECTION);
        glPopMatrix();
        glPopAttrib();

性能优化(异步操作)

  针对上述离屏渲染过程进行性能分析,发现数据量大的时候,渲染过程和GPU->CPU的数据传输过程(glReadPixels函数)都会有较大的耗时,可以采取的解决方案是渲染过程和数据处理过程采用异步处理,查阅相关资料发现glReadPixels函数本身不支持异步操作,但是我们通过采用PBO可以实现类似的异步操作。下面简单介绍:

  • 初始化

    创建PBO:

    glGenBuffersARB(PBO_COUNT, pboIds);
    glBindBufferARB(GL_PIXEL_PACK_BUFFER_ARB, pboIds[0]);
    glBufferDataARB(GL_PIXEL_PACK_BUFFER_ARB, DATA_SIZE, 0, GL_STREAM_READ_ARB);
  • 数据读取操作

    glBindBufferARB(GL_PIXEL_PACK_BUFFER_ARB, pboIds[0]);
    // 注意glReadPixels函数的最后一个参数是0
    glReadPixels(0, 0, width, height * OPENGL_FBO_PARALLELNUM, GL_BGR_EXT, GL_UNSIGNED_BYTE, 0);

    // 异步读取
    GLubyte* image_src = (GLubyte*)glMapBufferARB(GL_PIXEL_PACK_BUFFER_ARB, GL_READ_ONLY_ARB);

    if (image_src){
    glUnmapBufferARB(GL_PIXEL_PACK_BUFFER_ARB);
    }

关于如何将OpenGL渲染的图片保存到本地,见链接