• Android OpenGL ES 学习(七) – 纹理


    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 纹理相关的只是。今天要完成的效果,加载一张图片:
    在这里插入图片描述

    一. 基本原理

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

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

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

    再通俗一点,纹理就是贴图,如下图:
    图片来源:https://juejin.cn/post/7150869291208802341

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

    1.1 纹理坐标

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

    1.2 采样

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

    1.3 纹理坐标

    纹理也有自己的坐标体系,范围在在(0,0)到(1,1)内,两个维度分别是S、T,所以一般称为ST纹理坐标。而有些时候也叫UV坐标。
    而它是没有方向性的,因此我们可以随意指定,因为我们是搞安卓,所以就让纹理坐标的起始点为左上角:
    图片来源(https://www.jianshu.com/p/3659f4649f98)

    1.4 文件加载

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

    1.5 纹理过滤

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

    这个时候,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])
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    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
            }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    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)
    
    • 1
    • 2
    • 3

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

    接着设置纹理过滤,然后使用 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)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

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

    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
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55

    三. 编写纹理顶点

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

    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
     )
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    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;
            
            }
    """
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    可以看到,添加了一个 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) ;
            }
    """
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

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

    3.2 加载数据

    同 VBO 的操作,首先加载好纹理的数据,然后管理纹理坐标。注意,由于我们增加了 纹理坐标,所以,OpenGL 关联顶点索引时,它的步长和偏移地址都发生了改变,如下:
    在这里插入图片描述
    所以,顶点数据修改为:

     //绘制位置
     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)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    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)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    这样,我们就绘制好了。

    四. 其他效果

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

    FragColor = texture(ourTexture,vTexture) * vTextColor;
    
    • 1

    就会出现混合色:
    在这里插入图片描述

    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
    )
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    在这里插入图片描述
    看看是不是跟四种模式对应上了呢。

    五. 多纹理

    上面都是一张纹理,这里我们尝试使用多张纹理。
    细心的你,可能发现了,片段着色器中的 sampler2D ourTexture,命名没有赋值,为啥能正确能正确加载图片。
    原因是当你没设置时,默认使用第一个纹理 GLES30.GL_TEXTURE0 ,OpenGL共支持16个纹理。
    这里,我们使用两种图片,即使用 GLES30.GL_TEXTURE0 和 GLES30.GL_TEXTURE1。

    5.1 多纹理片段着色器

    我们新建多一个 sampler2D 纹理变量:

    /**
     * 片段着色器
     */
    private const val FRAGMENT_SHADER = """#version 300 es
            precision mediump float;
            out vec4 FragColor;
            in vec4 vTextColor;
            in vec2 vTexture;
            uniform sampler2D ourTexture;
            uniform sampler2D ourTexture2;
            void main()
            {
              vec4 texture1 = texture(ourTexture,vTexture);
              vec4 texture2 = texture(ourTexture2,vTexture);
              FragColor = texture1 + texture2;
            }
    """
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    让片段着色器 = 纹理1+纹理2

    5.2 设置纹理变量

    由于多加了一个纹理变量,所以需要指定变量代表的意思。

    GLES30.glUniform1i(GLES30.glGetUniformLocation(programId,"ourTexture"),0)
    GLES30.glUniform1i(GLES30.glGetUniformLocation(programId,"ourTexture2"),1)
    
    • 1
    • 2

    5.2 添加多一张图片

    前面都不需要变,只需要加多已张图片加载到 纹理对象中即可,图片为:
    在这里插入图片描述

    texture = loadTexture(TAG,MainApplication.context, R.mipmap.wuliuqi)
    texture2 = loadTexture(TAG,MainApplication.context, R.mipmap.wuliuqi2)
    
    • 1
    • 2

    5.4 渲染

    override fun onDrawFrame(gl: GL10?) {
        //步骤1:使用glClearColor设置的颜色,刷新Surface
        GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT)
    
    
        //useVaoVboAndEbo
        texture?.apply {
            GLES30.glActiveTexture(GLES30.GL_TEXTURE0)
            GLES30.glBindTexture(GLES30.GL_TEXTURE_2D,id)
        }
        texture2?.apply {
            GLES30.glActiveTexture(GLES30.GL_TEXTURE1)
            GLES30.glBindTexture(GLES30.GL_TEXTURE_2D,id)
        }
    
        GLES30.glBindVertexArray(vao[0])
        GLES30.glDrawElements(GLES30.GL_TRIANGLE_STRIP, 6, GLES30.GL_UNSIGNED_INT, 0)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    效果:
    在这里插入图片描述
    纹理相乘:

    FragColor = texture1 * texture2;
    
    • 1

    在这里插入图片描述
    混合

    FragColor = mix(texture1,texture2,0.5);
    
    • 1

    在这里插入图片描述

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

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

  • 相关阅读:
    虹科分享 | 近距离接触最新的3个勒索软件
    HTTP与HTTPS的区别及HTTPS如何安全的传输数据
    U-Boot常用命令
    在nodejs中实现实时通信的几种方式
    Java之IO流详解(二)——字节流
    RabbitMQ整理
    Spring框架系列(4) - 深入浅出Spring核心之面向切面编程(AOP)
    什么是DCS系统?DCS和SCADA的区别
    Python股票量化投资课学习—小市值策略测试
    路由不变,页面刷新
  • 原文地址:https://blog.csdn.net/u011418943/article/details/128163235