标签: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,之后也会补充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。整个流程简单来说如下图所示:
-
准备数据
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保存在本地,具体的过程见 链接。
需要说明的是:
调用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渲染的图片保存到本地,见链接