OpenGL ES 2D纹理载入和处理

一、纹理

本文基于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_2DGL_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进行不同尺寸下所需的纹理级别判定,从而在不同尺寸下给出相应的纹理大小。

注意事项

  1. 如果纹理像素的尺寸不是2的幂,可能导致纹理处理时出现裂痕或闪光等;
  2. 上一帧影响下一帧的纹理处理需要更加关注纹理渲染的性能问题;
  3. 纹理映射可能因为纹理本身像素过大却要显示在很小的尺寸窗口上,导致出现像素损失;
  4. 慎重选择纹理过滤的算法,这将决定大纹理在小尺寸窗口显示或小纹理在大尺寸窗口显示的效果好坏;
  5. 合理调用渲染管线流程,防止频繁的调用导致性能问题。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,204评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,091评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,548评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,657评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,689评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,554评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,302评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,216评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,661评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,851评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,977评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,697评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,306评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,898评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,019评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,138评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,927评论 2 355