一、纹理
本文基于OpenGL ES介绍2D纹理创建和处理,包括纹理映射和渲染的方案。本文所指的纹理是通过2D图片生成的,大小一般为2的幂,其基本单位是纹理像素。纹理像素包含颜色、深度、Alpha值等信息。需要特别注意,纹理坐标和顶点坐标不是同一个概念。纹理坐标的取值范围是0到1,左上角为(0,0),右下角为(1,1),通过纹理坐标可以获取在该坐标下的纹理像素信息,这个过程称为纹理采样。
二、纹理绘制步骤
完整代码:
glGenTextures(1, &lastFrameTextureId);
glBindTexture(GL_TEXTURE_2D, lastFrameTextureId);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, lastFrameImage->width, lastFrameImage->height, 0, GL_RGBA, GL_UNSIGNED_BYTE,
lastFrameImage->ppPlane[0]);
glBindTexture(GL_TEXTURE_2D, GL_NONE);
纹理绘制过程可以简化为以下步骤:
1. 创建纹理对象 void glGenTextures(GLsizei n, GLuint *textures)
-
n
指的事需要创建纹理个数; -
textures
指的纹理对象ID数组。
2. 绑定纹理对象 void glBindTexture(GLenum target, GLuint texture)
-
target
指将纹理绑定到GL_TEXTURE_2D
、GL_TEXTURE_3D
等目标; -
texture
指要绑定的纹理对象句柄。
3. 纹理过滤
纹理过滤的目的是尽可能解决针对不同尺寸纹理贴图时存在大尺寸到小尺寸或小尺寸到大尺寸的伪像和性能问题。
void glTexParameteri(GLenum target, GLenum pname, GLint param)
void glTexParameteriv(GLenum target, GLenum pname, const GLint *params)
void glTexParameterf(GLenum target, GLenum pname, GLfloat param)
void glTexParameterfv(GLenum target, GLenum pname, const GLfloat *params)
-
target
同绑定纹理的target
; -
pname
设置在不同方向上的参数、或支持设置的插值算法 -
params
针对不同pname设置参数。
4. 纹理图像加载void glTexImage2D(GLenum target, GLint level, GLenum internalFormat, GLsizei width, GLsizei height, GLint border, GLenum format, GLenum types, const void* pixels)
-
target
同绑定纹理的target
; -
level
指定加载的mip级别。未设置Mipmap则设置为0,设置了则依据需要增大; -
internalFormat
纹理存储的内存格式,可选择设置为GL_RBGA、GL_DEPTH_COMPONENT24等; -
width
图像像素宽度; -
height
图像像素高度; -
border
通常设置为0,为了与桌面OpenGL接口兼容; -
format
输入的纹理数据格式,可以是GL_RGB、GL_RGBA、GL_ALPHA -
type
输入的像素数据类型,可以是GL_UNSIGNED_BYTE、GL_BYTE、GL_SHORT等; -
pixels
图像像素数据,包含(widthheight高度)个像素。
三、FBO渲染到纹理
在实际使用过程中,对纹理的处理肯定不仅仅局限于给定一张图片渲染显示出来就够了,还需要对给定的图片进行纹理像素的操作。因此在上述通过载入图片获取像素创建纹理进行绘制的基础上,如果需要对纹理进行后处理,可以通过帧缓冲区(FBO)渲染到纹理的方式对纹理进行修改。
完整代码
glGenTextures(1, &lastFrameTextureId);
glBindTexture(GL_TEXTURE_2D, lastFrameTextureId);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
// 创建并初始化 FBO
glGenFramebuffers(1, &lastFrameBuffer);
glBindFramebuffer(GL_FRAMEBUFFER, lastFrameBuffer);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, lastFrameTextureId, 0);
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
LOGD("FBOSample::CreateTempFrameBufferObj glCheckFramebufferStatus status != GL_FRAMEBUFFER_COMPLETE");
return false;
}
glBindTexture(GL_TEXTURE_2D, GL_NONE);
创建过程可以简化为以下步骤:
1. 创建帧缓冲区对象(FBO)void glGenFramebuffers(GLsizei n, GLuint *ids)
-
n
帧缓冲区对象数量; -
*ids
指向有n个元素的数组指针,帧缓冲区对象为该数组中的对象。
2. 绑定当前帧缓冲区对象void glBindFramebuffer(GLenum target, GLuint framebuffer)
-
target
可以设置为GL_READ_FRAMEBUFFER、GL_DRAW_FRAMEBUFFER或GL_FRAMEBUFFER; -
framebuffer
帧缓冲区对象名称。
3.连接一个2D纹理作为附着void glFramebufferTexture2D(GLenum target, GLenum attachment, GLenum textarget, GLuint texture, GLint level)
-
target
同板顶帧缓冲区对象中的target
; -
attachment
可以设置为GL_COLOR_ATTACHMENTi(颜色附着)、GL_DEPTH_ATTACHMENT(深度附着)、GL_STENCIL_ATTACHMENT(模板附着)、GL_DEPTH_STENCIL_ATTACHMENT(深度模板附着); -
textarget
指定纹理目标,为需要绑定纹理中glTexImage2D
中的target参数所指定的值; -
texture
指定纹理对象; -
level
指定纹理对象mip级别,若未设置mipmap则为0。
绘制过程代码
绘制过程可以分为两步,首先在帧缓冲区对象中进行离屏渲染,该部分内容不会显示在屏幕上,渲染的同时会将结果渲染到先前帧缓冲区对象附着的纹理。其次在默认缓冲区中进行渲染并显示,在此只需要激活并绑定离屏渲染时帧缓冲区对象附着的纹理,将其加载到着色器中即可完成整个绘制过程。
具体到编码,我们可以写两段着色器代码,第一段为离屏渲染时对载入的纹理进行放大缩小拉伸裁剪等操作,得到我们想要的纹理。因为该操作是在创建的帧缓冲区对象中,得到的纹理会自动渲染到附着的纹理中,因此可以把得到的纹理再通过普通渲染显示出来。同理,通过普通渲染显示出来的过程中也可以对此处的着色器进行修改,再一次对纹理进行后处理。
//离屏渲染,使用FBO
glBindFramebuffer(GL_FRAMEBUFFER, lastFrameBuffer);
glUseProgram(m_FboProgramObj);
glBindVertexArray(m_VaoIds[1]);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, m_ImageTextureId);
glUniform1i(m_FboSamplerLoc, 0);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, (const void *)0);
glBindVertexArray(0);
glBindTexture(GL_TEXTURE_2D, 0);
// 普通渲染
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glViewport(0, 0, screenW, screenH);
glUseProgram(m_ProgramObj);
glBindVertexArray(m_VaoIds[0]);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, lastFrameTextureId);
glUniform1i(m_SamplerLoc, 0);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, (const void *)0);
glBindTexture(GL_TEXTURE_2D, GL_NONE);
glBindVertexArray(GL_NONE);
四、读取颜色缓冲区
纹理像素的获取过程还可以通过直接读取帧缓冲区像素,OpenGL提供了void glReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, GLvoid *pixels)
接口直接可直接调用,如果开发过程中渲染管线的调用频率不高,且无需担心性能问题,那么glReadPixels()
能够直接获取当前帧缓冲区的像素数据进行下一步操作。
此外,还能通过PBO、双PBO的方式提升效率。
五、纹理映射
纹理映射是纹理后处理的一大解决方案。直接通过着色器对纹理进行映射可以极大提升运行效率和处理效率。纹理映射可以简单理解为通过不同的矩阵变换转换纹理的坐标和像素,从而展现不同的纹理结果。纹理映射主要体现在平移、旋转、缩放等,常见的纹理映射可以通过以下两种方法实现:
- 顶点着色器顶点变换
- 片段着色器纹理变换
1.顶点着色器顶点变换
可以假象一个纹理附着在一个物体上,在我们的相机视角内可以看到物体的全部,但如果我们移动物体(平移)、拉进或放远物体(缩放),则我们看到物体上的纹理也会因此发生变换。通过平移改变纹理显示在该视角下的位置,通过缩放改变纹理的放大或缩小。当然整个过程还包含OpenGL整个坐标体系变换,在此不做扩展。
glm::mat4 Model = glm::mat4(1.0f);
Model = glm::translate(Model, glm::vec3(tempX, tempY, 0.0f));
Model = glm::scale(Model, glm::vec3(scaleX, scaleY, 1.0f));
Model = glm::translate(Model, glm::vec3(tempX, tempY, 0.0f));
将需要变换的矩阵传入顶点着色器中:
char vShaderStr[] =
"#version 300 es\n"
"layout(location = 0) in vec4 a_position;\n"
"layout(location = 1) in vec2 a_texCoord;\n"
"uniform mat4 u_MVPMatrix;\n"
"out vec2 v_texCoord;\n"
"void main()\n"
"{\n"
" gl_Position = u_MVPMatrix * a_position;\n"
" v_texCoord = a_texCoord;\n"
"}";
其中u_MVPMatrix即为变换矩阵,由此渲染结果的纹理即为变换过的纹理。
2.片段着色器纹理变换
片段着色器的纹理变换方式更加多样,除了对纹理坐标的变换(特别注意顶点坐标和纹理坐标是完全不同的)还可以针对特定区域纹理进行裁剪、像素处理等。
- 纹理坐标变换同顶点着色器中的处理方式,通过绑定Uniform变量,传入需要变换的矩阵,或者将矩阵变换在着色器代码中完成,即可完成纹理坐标的变换;
- 纹理裁剪可以理解为针对特定纹理区域需要通过裁剪得到最终纹理图形(如将透明度小于0.5的区域裁剪舍弃),可以通过灵活运用着色器中的discard内建函数和纹理坐标完成;
char fShaderStr[] =
"#version 300 es\n"
"precision mediump float;\n"
"in vec2 v_texCoord;\n"
"layout(location = 0) out vec4 outColor;\n"
"uniform sampler2D s_TextureMap;\n"
"void main()\n"
"{\n"
" vec4 tempColor = texture(s_TextureMap, v_texCoord);\n"
" if(tempColor.a < 0.5){\n"
" discard;\n"
" }else{ \n"
" outColor = tempColor;\n"
" } \n"
"}";
- 像素处理可以做不同的混合算法处理,在此给出简单的示例,将所有纹理像素加上某一颜色像素进行输出。
char fShaderStr[] =
"#version 300 es\n"
"precision mediump float;\n"
"in vec2 v_texCoord;\n"
"layout(location = 0) out vec4 outColor;\n"
"uniform sampler2D s_TextureMap;\n"
"void main()\n"
"{\n"
" vec4 tempColor = texture(s_TextureMap, v_texCoord);\n"
" outColor = tempColor + vec4(0.2,0.2,0.2,0.2);\n"
"}";
纹理映射部分使用场景
- 大尺寸纹理需要裁剪出其中的一部分作为纹理进行渲染;
- 纹理叠加时颜色混合处理;
- 纹理尺寸与窗口尺寸不匹配时的简单映射;
- 纹理像素过大但需要在不同小尺寸下进行渲染。
针对上述第四点的优化,可以使用Mipmap进行不同尺寸下所需的纹理级别判定,从而在不同尺寸下给出相应的纹理大小。
注意事项
- 如果纹理像素的尺寸不是2的幂,可能导致纹理处理时出现裂痕或闪光等;
- 上一帧影响下一帧的纹理处理需要更加关注纹理渲染的性能问题;
- 纹理映射可能因为纹理本身像素过大却要显示在很小的尺寸窗口上,导致出现像素损失;
- 慎重选择纹理过滤的算法,这将决定大纹理在小尺寸窗口显示或小纹理在大尺寸窗口显示的效果好坏;
- 合理调用渲染管线流程,防止频繁的调用导致性能问题。