标签: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 <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。整个流程简单来说如下图所示:
-
准备数据
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
之间的映射关系,才能进行“贴图”操作,有下面几种常见映射方法:-
矩形映射
首先将映射方式设置为:GL_QUADS
,之后可以直接设置当前图像和目标图像的四个图像顶点,即依次矩形映射即可完成贴图过程,我们也可以设置多组小矩形之间的映射类似于下图。 -
三角形映射
如果将映射方式设置为:GL_TRIANGLES
,之后可以直接设置当前图像和目标图像按照三角形换分之后的顶点,如下图所示:
给一个例子:
// 开始绑定 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)。 <font color="red">需要说明的是:</font>   调用glReadPixels函数之前,需要加上 `glReadBuffer(GL_FRONT);` ,因为对于渲染到窗口的情况来说,窗口相当于FRONT,这句话相当于说,我们读取的对象是“前端”的数据。现在思考另一个问题,既然我们设置的是双缓冲!那么,`glReadBuffer(GL_BACK);` 是否有用呢?事实证明是的,这里也有 `离屏渲染` 的思想<font color="red">(注意思想)</font>:   *在调用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渲染的图片保存到本地,见链接