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

标签:CG opengl

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


需求

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

  其实就是将图像以三角形为单位,从原始图像(左侧)向目标图像(右侧)映射,也即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。但是两个版本的OpenGL原理是一样的。
    • 基于shader实现的三角形仿射变换并贴图渲染已经给出了一个较为简化的补充(附代码),见链接

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

  • 头文件
#include        // 必须在glut.h之前include
//
#include 
#include 
#include 
#include 
  • 渲染至窗口
    使用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 按照三角形仿射变换并贴图渲染(正常渲染或离屏渲染以及异步优化)(一)_第1张图片

    • 准备数据

      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 按照三角形仿射变换并贴图渲染(正常渲染或离屏渲染以及异步优化)(一)_第2张图片

      给一个例子:

      // 开始绑定
      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保存在本地,具体的过程见 [链接](http://blog.csdn.net/hust_sheng/article/details/75268056)。
        
        需要说明的是:
          调用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渲染的图片保存到本地,见链接

你可能感兴趣的:(OpenGL,图形学)