Android OpenGL ES 学习(七) – 纹理

时间:2022-12-05 07:14:26

OpenGL 学习教程
Android OpenGL ES 学习(一) – 基本概念
Android OpenGL ES 学习(二) – 图形渲染管线和GLSL
Android OpenGL ES 学习(三) – 绘制平面图形
Android OpenGL ES 学习(四) – 正交投屏
Android OpenGL ES 学习(五) – 渐变色
Android OpenGL ES 学习(六) – 使用 VBO、VAO 和 EBO/IBO 优化程序
Android OpenGL ES 学习(七) – 纹理
代码工程地址: https://github.com/LillteZheng/OpenGLDemo.git

上一章中 Android OpenGL ES 学习(六) – 使用 VBO、VAO 和 EBO/IBO 优化程序,我们已经学习了 VBO、VAO 和 EBO/IBO 的知识,这一章,一起来学习 OpenGL 纹理相关的只是。今天要完成的效果,加载一张图片:
Android OpenGL ES 学习(七) – 纹理
Android OpenGL ES 学习(七) – 纹理
Android OpenGL ES 学习(七) – 纹理

一. 基本原理

可能第一印象是一张二维图片,如下图:
Android OpenGL ES 学习(七) – 纹理
但在OpenGL的世界里,这里有点不一样,它与光栅化有点像,光栅化过程中,会切成一片片小片段,然后片段着色器中把颜色值赋给图元表面。
Android OpenGL ES 学习(七) – 纹理

纹理也相似,它包含一张或多张图片信息(也可以是其他数据)的一个 OpenGL 对象,在光栅化的时候,计算当前小片段在纹理上的坐标位置,然后在片段着色器中,根据这些纹理坐标,去纹理中取出对应的颜色值。

纹理有一维,二维和三维三种类型,但我们这里只讲 二维图片 GL_TEXTURE_2D。

再通俗一点,纹理就是贴图,如下图:
Android OpenGL ES 学习(七) – 纹理

所以,学习纹理,就是学习如何将图贴上去的问题。

1.1 纹理坐标

比如上章画了一个矩形,现在我们有一张图片,那怎么把这张图片纹理映射到矩形呢?答案就是点对点,每个顶点坐标都一一对应的;而这个坐标就叫做纹理坐标

1.2 采样

纹理坐标在 x轴和 y轴上,范围是 0 到 1(这里讲的是二维纹理),而使用纹理坐标获取纹理颜色的方式,就叫做采样

1.3 纹理坐标

纹理也有自己的坐标体系,范围在在(0,0)到(1,1)内,两个维度分别是S、T,所以一般称为ST纹理坐标。而有些时候也叫UV坐标。
而它是没有方向性的,因此我们可以随意指定,因为我们是搞安卓,所以就让纹理坐标的起始点为左上角:
Android OpenGL ES 学习(七) – 纹理

1.4 文件加载

OpenGL 不能直接加载 JPG 或者 PNG 这种被编码过的格式,需要加载原始数据,如 Bitmap; 也不能数据被压缩,因此,图片应放在 xxx-nodpi 目录下,且使用 BtimapFactory 读取图片时,应设置 options.isScaled = false。

1.5 纹理过滤

当我们通过光栅化,把图片处理成一个个小片段,再进行采样渲染时,通过会遇到纹理像素和小片段并非一一对应的,就会出现压缩或者放大的情况,比如下面这张图:
Android OpenGL ES 学习(七) – 纹理
本来应该点对点像素的,但是我们放得特别大,就会出现纹理像素和实际像素不对应的情况。

这个时候,OpenGL 就会纹理过滤和多级渐远纹理的处理方案。详细可参考:LearnOpenGl_Cn

这里,你可以理解为怎么让图片更顺滑更清晰,而需要配置的选项。

二. 加载纹理

刚才说道,纹理也是一个 OpenGL 的对象,所以它的创建,跟 VBO 这些差不多,就是换了 texture 的关键字。步骤如下:

  1. 创建纹理对象
  2. 绑定纹理到上下文
  3. 创建bitmap数据
  4. 绑定bitmap数据到纹理
  5. 解绑和释放bitmap

2.1 创建和绑定纹理对象

创建和绑定非常简单,使用的是 glGenTextures 和 glBindTexture:

val buffer = IntArray(1)
 //创建纹理对象
 GLES30.glGenTextures(1,buffer,0)

 if (buffer[0] == 0){
     Log.e(TAG, "创建对象失败")
     return null
 }
 //绑定纹理到上下文
 GLES30.glBindTexture(GLES30.GL_TEXTURE_2D,buffer[0])

2.2 创建 bitmap 数据

这里在 xxx-nodpi 中导入一张图片,然后使用 BitmapFactory 加载

BitmapFactory.Options().apply {
        //不允许放大
        inScaled = false
        val bitmap = BitmapFactory.decodeResource(context.resources, resId, this)
        if (bitmap == null) {
            //删除纹理对象
            GLES30.glDeleteTextures(1,buffer,0)
            Log.d(TAG, "loadTexture fail,bitmap is null ")
            return null
        }
}

2.3 绑定 bitmap 数据到纹理和解绑

绑定之前,先设置纹理过滤,先设置纹理环绕模式

        //纹理环绕
        GLES30.glTexParameteri(GLES30.GL_TEXTURE_2D,GLES30.GL_TEXTURE_WRAP_S,GLES30.GL_REPEAT)
        GLES30.glTexParameteri(GLES30.GL_TEXTURE_2D,GLES30.GL_TEXTURE_WRAP_T,GLES30.GL_REPEAT)

什么意思呢?刚才说道纹理坐标时 (0,0) 到 (1,1),那超过的部分是怎么呈现方式呢?OpenGL 提供了四种:
Android OpenGL ES 学习(七) – 纹理
当纹理超过了范围,就会有不同的视觉效果,如下图:
Android OpenGL ES 学习(七) – 纹理
这里我们先这样设置,后面我们再用代码验证。

接着设置纹理过滤,然后使用 GLUtils.texImage2D 绑定数据即可。

        //纹理环绕
        GLES30.glTexParameteri(GLES30.GL_TEXTURE_2D,GLES30.GL_TEXTURE_WRAP_S,GLES30.GL_REPEAT)
        GLES30.glTexParameteri(GLES30.GL_TEXTURE_2D,GLES30.GL_TEXTURE_WRAP_T,GLES30.GL_REPEAT)

        //纹理过滤
        GLES30.glTexParameteri(GLES30.GL_TEXTURE_2D,GLES30.GL_TEXTURE_MIN_FILTER,GLES30.GL_NEAREST)
        GLES30.glTexParameteri(GLES30.GL_TEXTURE_2D,GLES30.GL_TEXTURE_MAG_FILTER,GLES30.GL_LINEAR)

        //绑定数据
        GLUtils.texImage2D(GLES30.GL_TEXTURE_2D,0,bitmap,0)

        //生成 mip 位图 多级渐远纹理
        GLES30.glGenerateMipmap(GLES30.GL_TEXTURE_2D)

        //回收bitmap
         bean.id = buffer[0]
        bean.width = bitmap.width
        bean.height = bitmap.height

        //解绑纹理对象
        GLES30.glBindTexture(GLES30.GL_TEXTURE_2D,0)

这里可以封装成一个工具类,完成代码为:

data class TextureBean(var id: Int, var width: Int,var height: Int) {
    constructor():this(-1,0,0)
}


fun loadTexture(TAG:String,context: Context,resId:Int):TextureBean?{
    val bean = TextureBean()
    val buffer = IntArray(1)
    //创建纹理对象
    GLES30.glGenTextures(1,buffer,0)

    if (buffer[0] == 0){
        Log.e(TAG, "创建对象失败")
        return null
    }
    //绑定纹理到上下文
    GLES30.glBindTexture(GLES30.GL_TEXTURE_2D,buffer[0])

    BitmapFactory.Options().apply {
        //不允许放大
        inScaled = false
        val bitmap = BitmapFactory.decodeResource(context.resources, resId, this)
        if (bitmap == null) {
            //删除纹理对象
            GLES30.glDeleteTextures(1,buffer,0)
            Log.d(TAG, "loadTexture fail,bitmap is null ")
            return null
        }

        //纹理环绕
        GLES30.glTexParameteri(GLES30.GL_TEXTURE_2D,GLES30.GL_TEXTURE_WRAP_S,GLES30.GL_REPEAT)
        GLES30.glTexParameteri(GLES30.GL_TEXTURE_2D,GLES30.GL_TEXTURE_WRAP_T,GLES30.GL_REPEAT)

        //纹理过滤
        GLES30.glTexParameteri(GLES30.GL_TEXTURE_2D,GLES30.GL_TEXTURE_MIN_FILTER,GLES30.GL_NEAREST)
        GLES30.glTexParameteri(GLES30.GL_TEXTURE_2D,GLES30.GL_TEXTURE_MAG_FILTER,GLES30.GL_LINEAR)

        //绑定数据
        GLUtils.texImage2D(GLES30.GL_TEXTURE_2D,0,bitmap,0)

        //生成 mip 位图 多级渐远纹理
        GLES30.glGenerateMipmap(GLES30.GL_TEXTURE_2D)

        //回收bitmap
         bean.id = buffer[0]
        bean.width = bitmap.width
        bean.height = bitmap.height

        //解绑纹理对象
        GLES30.glBindTexture(GLES30.GL_TEXTURE_2D,0)

    }

    return bean
}

三. 编写纹理顶点

刚才说道,纹理可以简单理解成贴图,那么就需要点对点,所以,我们需要把纹理坐标也对上矩形的坐标,在上章的基础上,顶点数据为:

private val POINT_RECT_DATA2 = floatArrayOf(
     // positions         //color              // texture coords
     0.8f,  0.8f, 0.0f,   1.0f, 0.0f, 0.0f,   1.0f, 0.0f, // top right
     0.8f, -0.8f, 0.0f,   1.0f, 0.0f, 1.0f,   1.0f, 1.0f, // bottom right
    -0.8f, -0.8f, 0.0f,   1.0f, 0.0f, 1.0f,   0.0f, 1.0f, // bottom left
    -0.8f,  0.8f, 0.0f,   0.0f, 0.5f, 1.0f,   0.0f, 0.0f  // top left
 )

3.1 编写着色器代码

为了把顶点数据传递过去,我们需要在顶点着色器上,添加一个变量,表现纹理顶点数据,然后传递给片段着色器:

private const val VERTEX_SHADER = """#version 300 es
        uniform mat4 u_Matrix;
        layout(location = 0) in vec4 a_Position;
        layout(location = 1) in vec4 a_Color;
        layout(location = 2) in vec2 aTexture;
        out vec4 vTextColor;
        out vec2 vTexture;
        void main()
        {
            // 矩阵与向量相乘得到最终的位置
            gl_Position = u_Matrix * a_Position;
            //传递给片段着色器的颜色
            vTextColor = a_Color;
            vTexture = aTexture;
        
        }
"""

可以看到,添加了一个 aTexture,因为是二维图片,所以分量类型是 vec2 ,并设置 out 类型的 vTexture ,给片段着色器。

但是我们怎样能把纹理对象传给片段着色器呢?GLSL有一个供纹理对象使用的内建数据类型,叫做采样器(Sampler),它以纹理类型作为后缀,比如sampler1D、sampler3D,或在我们的例子中的sampler2D。我们可以简单声明一个uniform sampler2D把一个纹理添加到片段着色器中,稍后我们会把纹理赋值给这个uniform。

/**
 * 片段着色器
 */
private const val FRAGMENT_SHADER = """#version 300 es
        precision mediump float;
        out vec4 FragColor;
        in vec4 vTextColor;
        in vec2 vTexture;
        uniform sampler2D ourTexture;
        void main()
        {
          FragColor = texture(ourTexture,vTexture) ;
        }
"""

弄完之后,使用 texture 这个内置函数,来取 纹理的颜色,第一个是参数是纹理数据,第二个是顶点数据。

3.2 加载数据

同 VBO 的操作,首先加载好纹理的数据,然后管理纹理坐标。注意,由于我们增加了 纹理坐标,所以,OpenGL 关联顶点索引时,它的步长和偏移地址都发生了改变,如下:
Android OpenGL ES 学习(七) – 纹理
所以,顶点数据修改为:

 //绘制位置
 GLES30.glVertexAttribPointer(
     0, 3, GLES30.GL_FLOAT,
     false, 8 * 4, 0
 )
 GLES30.glEnableVertexAttribArray(0)

 //绘制颜色,颜色地址偏移量从3开始,前面3个为位置
 vertexData.position(3)
 GLES30.glVertexAttribPointer(
     1, 3, GLES30.GL_FLOAT,
     false, 8 * 4, 3*4 //需要指定颜色的地址 3 * 4
 )
 GLES30.glEnableVertexAttribArray(1)

 texture = loadTexture(TAG,MainApplication.context, R.mipmap.wuliuqi)
 //纹理在位置和颜色之后,偏移量为6
 vertexData.position(6)
 GLES30.glVertexAttribPointer(
     2, 2, GLES30.GL_FLOAT,
     false, 8 * 4, 6*4 //需要指定颜色的地址 3 * 4
 )
 GLES30.glEnableVertexAttribArray(2)

3.3 绘制

绘制就比较简单了,在使用之前,调用一下纹理数据就可以了:

texture?.apply {
    GLES30.glBindTexture(GLES30.GL_TEXTURE_2D,id)
}

GLES30.glBindVertexArray(vao[0])
GLES30.glDrawElements(GLES30.GL_TRIANGLE_STRIP, 6, GLES30.GL_UNSIGNED_INT, 0)

这样,我们就绘制好了。

四. 其他效果

上面的代码中,你可能会觉得顶点颜色好像没啥用?
那如果把纹理颜色和顶点颜色混合呢,如修改成:

FragColor = texture(ourTexture,vTexture) * vTextColor;

就会出现混合色:
Android OpenGL ES 学习(七) – 纹理

4.1 环绕模式

刚才说道,如果超过纹理坐标时 (0,0) 到 (1,1),那超过的部分是怎么呈现方式呢?我们修改一下纹理坐标,让它超过 1,模式为GL_REPEAT :

private val POINT_RECT_DATA2 = floatArrayOf(
    // positions         //color              // texture coords
    0.8f,  0.8f, 0.0f,   1.0f, 0.0f, 0.0f,   1.5f, 0.0f, // top right
    0.8f, -0.8f, 0.0f,   1.0f, 0.0f, 1.0f,   1.5f, 1.5f, // bottom right
   -0.8f, -0.8f, 0.0f,   1.0f, 0.0f, 1.0f,   0.0f, 1.5f, // bottom left
   -0.8f,  0.8f, 0.0f,   0.0f, 0.5f, 1.0f,   0.0f, 0.0f  // top left
)

Android OpenGL ES 学习(七) – 纹理
看看是不是跟四种模式对应上了呢。

这样,我们就把纹理的知识学完了。

参考:
https://learnopengl-cn.github.io/01%20Getting%20started/06%20Textures/
https://www.jianshu.com/p/3659f4649f98
https://juejin.cn/post/7150869291208802341