Android OpenGL ES 学习(七) – 纹理

news2025/1/29 13:47:24

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])

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 提供了四种:
在这里插入图片描述
当纹理超过了范围,就会有不同的视觉效果,如下图:
在这里插入图片描述
这里我们先这样设置,后面我们再用代码验证。

接着设置纹理过滤,然后使用 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 关联顶点索引时,它的步长和偏移地址都发生了改变,如下:
在这里插入图片描述
所以,顶点数据修改为:

 //绘制位置
 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;

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

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
)

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

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/58467.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

第二证券|11天9板,“超级面料”概念火爆,高管却偷偷减持

自带抗病毒特点的“超级面料”炽热,多家上市公司发表相关状况。 安奈儿11天9板收重视函 早盘,安奈儿再度涨停,短短11个交易日9个涨停板,累计涨幅到达127%。 昨日晚间,安奈儿收到深交所重视函。在重视函中&#xff0c…

【计算机毕业设计】72.房屋出租出售系统源码

一、系统截图(需要演示视频可以私聊) 摘 要 随着科学技术的飞速发展,社会的方方面面、各行各业都在努力与现代的先进技术接轨,通过科技手段来提高自身的优势,房屋出租出售系统当然也不能排除在外。房屋出租出售系统是…

《绿色消费实施方案》的推出,释放出怎么样的信号,有无新的赛道

近日,国家发改委等七部门印发的《促进绿色消费实施方案》(以下简称《方案》)提出,探索实施全国绿色消费积分制度,鼓励地方结合实际建立本地绿色消费积分制度,以兑换商品、折扣优惠等方式鼓励绿色消费。鼓励各类销售平台制定绿色低…

C/C++语言 数据结构 创建邻接表存储的无向图及其邻接表的输出

目录 1.邻接表相关知识补充 2. 图的邻接存储表示 3.测试输入与输出样例 4.代码实现 4.1 创建无向图邻接表 4.2 输入无向图的邻接表 1.邻接表相关知识补充 定义: 对于图中每个顶点 vi,把所有邻接于 vi的顶点(对有向图是将从vi出发的弧的弧头顶…

windows/linux命令行操作快捷方式

命令行快捷键 Ctrla:光标回到命令行首。 (a:ahead) Ctrle:光标回到命令行尾。 (e:end) Ctrlb:光标向行首移动一个字符。 (b:backwards&#xff…

【模拟面试】23届本科生拿下字节/京东/网易研发offer,到底有多强?

这是一场模拟面试,面试选手是今年的应届生,拿下了字节/京东/网易的offer。 如果你想参加模拟面试欢迎私聊我,仅限后端,go语言更好。 下面可以看看这个同学的简历 我个人觉得挺不错的 总共问了几个问题 你们这个计费系统是干什么的…

想要精通算法和SQL的成长之路 - 判断子序列问题

想要精通算法和SQL的成长之路 - 判断子序列问题前言一. 判断子序列1.1 动态规划做法1.2 双指针二. 不同的子序列前言 想要精通算法和SQL的成长之路 - 系列导航 一. 判断子序列 原题链接 给定字符串 s 和 t ,判断 s 是否为 t 的子序列。 字符串的一个子序列是原始字…

阿里p8实战总结SpringCloud微服务分布式系统文档

前言 有人调侃我们说: 程序员不如送外卖。送外卖是搬运食物,自己是搬运代码,都不产出新的东西……透支体力,又消耗健康,可替代性极强,30岁之后就要面临被优化的危险……想跳槽,但是更高的平台…

Interval Envision图像库,非常强大的图像功能

Interval Envision图像库被认为具有增强Borland和Embarcadero Delphi的能力,并为用户和开发人员提供非常强大的图像功能。该库能够为用户提供一个图像文件I/O,用于文件的流行格式、扫描、打印过程、ocr的接口,以及最终的图片处理,…

Linux文件系统——文件系统、挂载点、目录结构

目录 一、目录结构 1.1 基本介绍 1.2 详细说明目录作用 二、挂载点 一、目录结构 1.1 基本介绍 Linux是一切皆文件,将所用的东西当做文件处理 目录结构就是一个单一的树状结构 整个的目录树只有一个树根:/ 根目录 文件夹分门别类的放到根目录…

vue配置环境变量

vue配置环境变量目录概述需求:设计思路实现思路分析1.URL管理2.网页3.加载.env4.分析参考资料和推荐阅读Survive by day and develop by night. talk for import biz , show your perfect code,full busy,skip hardness,make a better result,wait for c…

卷积神经网络kernel/filter/stride

【关于神经网络的学习】 【参考】:卷积核filter和kernal的区别 - 一杯明月 - 博客园 (cnblogs.com) 【参考】:(1条消息) 卷积神经网络的卷积核(kernel)、输入尺寸(input)、步长(stride&#x…

[附源码]计算机毕业设计springboot学生疫情防控信息填报系统

项目运行 环境配置: Jdk1.8 Tomcat7.0 Mysql HBuilderX(Webstorm也行) Eclispe(IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持)。 项目技术: SSM mybatis Maven Vue 等等组成,B/S模式 M…

使用frp进行内网穿透

frp概述 frp 是一个高性能的反向代理应用,可以帮助您轻松地进行内网穿透,对外网提供服务,支持 tcp, http, https 等协议类型,并且 web 服务支持根据域名进行路由转发。frp 采用 C/S 模式,将服务端部署在具有公网 IP 机…

Kafka - topic producer consumer 常用命令

Kafka常用命令 Topic相关 创建topic bin/kafka-topics.sh --create --zookeeper node02:2181 --replication-factor 1 --partitions 1 --topic testkafka3.3.1新版本就会提示不兼容,需要改用新版本 Exception in thread "main" joptsimple.Unrecogniz…

单链表初阶的两道基础题

初阶链表刷题翻转单链表(链接在末尾)链表的倒数第K个结点(链接在末尾)普通解法进阶解法注意!!!学习的是解题的思维! 翻转单链表(链接在末尾) 解题思路 如果给…

Java#31(不可变集合,Stream流和方法引用)

目录 一.创建不可变集合: 不可以被修改的集合 1.List接口创建不可变集合 2.Set接口创建不可变集合 3.Map接口创建不可变集合 二.Stream流 1.如何获取Stream流? 2.Stream流的中间方法 3.Stream流终结方法 三.方法引用 1.引用静态方法 2.引用成员方法 3.引用结构方法…

【Java学习】JavaWeb---Request Response

文章目录1. Request1. 1Request继承体系1.2 Request获取请求数据1.3 Request请求转发2. Response2. 1 Response设置响应数据功能介绍2. 2 Response完成重定向2. 3 Response响应字符数据2. 4 Response响应字节数据1. Request 1. 1Request继承体系 1.2 Request获取请求数据 1.3 …

制造企业如何借数字化能力进军万亿国际市场?

历时七十余载,中国建立了世界最完整的现代工业体系,实现了从“制造大国”向“制造强国”的历史性跨越,同时,这片土地也孕育了全球最庞大的自动化物流系统。 报告显示,从2012到2021年,中国社会物流总额和社…

转行软件测试我后悔了

很多时候,都在想当初做的转行软件测试行业的决定是对的吗?现在后悔还来得及吗? 记得在求职的时候,面试官经常问我:“为什么要选择软件测试工作?” 而我也会经常说一堆自己有的没的优势去应付。 工作这么久了&#x…