目标
我们希望实现高速的,从纹理贴图到纹理贴图的部分拷贝。形式化地,将纹理贴图 \(A\) 的某个矩形区域 \((x_1,y_1)-(x_2,y_2)\) 拷贝到纹理贴图 \(B\) 的一个指定的等大小的矩形区域。
背景
缓存是 GPU 能够控制和管理的连续 RAM。我们希望,程序从内存复制数据到缓存后,CPU 尽可能不再接触这个缓存。通过控制独占的缓存,GPU 能够高效地读写内存。
生成缓存是为 GPU 控制的一块缓存生成一个独一无二的标识符。绑定时告诉图形库接下来的运算要使用某个缓存。加载缓存数据是为当前绑定的缓存初始化足够的显存空间并复制数据。
接收渲染结果的缓冲区叫帧缓存。面对在不同的缓存之间大量的迁移数据的情况,我们需要创建自己的帧缓存。FBO 本身没有图像存储区。我们必须帧缓存关联图像(纹理或渲染对象)关联到 FBO。这种机制允许 FBO 快速地切换(分离和关联)帧缓存关联图像。
方法
在这篇帖子 opengl - Best method to copy texture to texture - Stack Overflow 中给出了一组对不同方法的评测。
本文中我们选用 glCopyTexSubImage2D
来实现。这是效率很高,同时也较为简单的一种方法。
void glCopyTexSubImage2D(GLenum target,
GLint level,
GLint xoffset,
GLint yoffset,
GLint x,
GLint y,
GLsizei width,
GLsizei height);
实现
绑定源的帧缓存和目标的纹理贴图,调用 glCopyTexSubImage2D
来做区域复制,然后解绑。
我们可以把这个过程进一步封装一下。
glBindFramebuffer(GL_FRAMEBUFFER, fboSrc);
glBindTexture(GL_TEXTURE_2D, texDest);
glCopyTexSubImage2D(GL_TEXTURE_2D, 0, xo, yo, x, y, w, h);
glBindTexture(GL_TEXTURE_2D, 0);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
示例
一个具体的例子是实时动态的图像流,每节拍产生一竖条新图像被压进来。我们通过缓存之间的拷贝来实现这种操作。
首先是初始化部分。我们需要为图像源创建纹理贴图。
// create and load source texture
pTextureSource = new QOpenGLTexture(QOpenGLTexture::Target2D);
pTextureSource->create();
pTextureSource->setData(img.mirrored());
...
为两个 Buffer(实际上类似于前台和后台)创建纹理贴图。
width = img.width();
height = img.height();
QImage nullImage(width,height,QImage::Format_RGBA8888);
nullImage.fill(0);
// create old buffer texture
pTextureOld = new QOpenGLTexture(QOpenGLTexture::Target2D);
pTextureOld->create();
pTextureOld->setData(nullImage);
// create result buffer texture
pTextureResult = new QOpenGLTexture(QOpenGLTexture::Target2D);
pTextureResult->create();
pTextureResult->setData(nullImage);
为每个贴图创建帧缓存对象并绑定。
glGenFramebuffers(1, &fboSource);
glBindFramebuffer(GL_FRAMEBUFFER, fboSource);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, pTextureSource->textureId(), 0);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
...
装载着色器、顶点数据,解析顶点数据。
// create and load shader
m_shaderTextureShader.addShaderFromSourceFile(QOpenGLShader::Vertex, "./shader.vert");
m_shaderTextureShader.addShaderFromSourceFile(QOpenGLShader::Fragment, "./shader.frag");
m_shaderTextureShader.link();
// create and load vertex data
QOpenGLVertexArrayObject::Binder{&m_vaoVertexArrayObject};
m_vboVertexBufferObject.create();
m_vboVertexBufferObject.bind();
m_vboVertexBufferObject.allocate(vertices.data(), sizeof(float) * vertices.size());
// set attribute pointer
m_shaderTextureShader.setAttributeBuffer("aPos", GL_FLOAT, 0, 3, sizeof(GLfloat) * 5);
m_shaderTextureShader.enableAttributeArray("aPos");
m_shaderTextureShader.setAttributeBuffer("aTexCoord", GL_FLOAT, sizeof(GLfloat) * 3, 2, sizeof(GLfloat) * 5);
m_shaderTextureShader.enableAttributeArray("aTexCoord");
接下来是实时绘制部分。
我们在每次绘制时,要用上面封装好的拷贝函数,做三次拷贝。
CopyFromFramebufferToTexture(fboResult, pTextureOld->textureId(), 0, 0, 0, 0, width, height);
CopyFromFramebufferToTexture(fboOld, pTextureResult->textureId(), 0, 0, w0, 0, width - w0, height);
CopyFromFramebufferToTexture(rand() % 2 ? fboSource2: fboSource, pTextureResult->textureId(), width - w0, 0, 0, 0, w0, height
绑定并绘制。
// bind texture and connect to texture unit
pTextureResult->bind(0);
m_shaderTextureShader.setUniformValue("ourTexture", 0);
// bind vao and draw
QOpenGLVertexArrayObject::Binder{&m_vaoVertexArrayObject};
this->glDrawArrays(GL_POLYGON, 0, 4);
这个示例是完全基于复制的。实际上,也可以用渲染到纹理来实现类似的功能。
References
opengl - Best method to copy texture to texture - Stack Overflow
glCopyTexSubImage2D - OpenGL 4 Reference Pages (khronos.org)