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。
再通俗一点,纹理就是贴图,如下图:
所以,学习纹理,就是学习如何将图贴上去的问题。
1.1 纹理坐标
比如上章画了一个矩形,现在我们有一张图片,那怎么把这张图片纹理映射到矩形呢?答案就是点对点,每个顶点坐标都一一对应的;而这个坐标就叫做纹理坐标。
1.2 采样
纹理坐标在 x轴和 y轴上,范围是 0 到 1(这里讲的是二维纹理),而使用纹理坐标获取纹理颜色的方式,就叫做采样。
1.3 纹理坐标
纹理也有自己的坐标体系,范围在在(0,0)到(1,1)内,两个维度分别是S、T,所以一般称为ST纹理坐标。而有些时候也叫UV坐标。
而它是没有方向性的,因此我们可以随意指定,因为我们是搞安卓,所以就让纹理坐标的起始点为左上角:
1.4 文件加载
OpenGL 不能直接加载 JPG 或者 PNG 这种被编码过的格式,需要加载原始数据,如 Bitmap; 也不能数据被压缩,因此,图片应放在 xxx-nodpi 目录下,且使用 BtimapFactory 读取图片时,应设置 options.isScaled = false。
1.5 纹理过滤
当我们通过光栅化,把图片处理成一个个小片段,再进行采样渲染时,通过会遇到纹理像素和小片段并非一一对应的,就会出现压缩或者放大的情况,比如下面这张图:
本来应该点对点像素的,但是我们放得特别大,就会出现纹理像素和实际像素不对应的情况。
这个时候,OpenGL 就会纹理过滤和多级渐远纹理的处理方案。详细可参考:LearnOpenGl_Cn
这里,你可以理解为怎么让图片更顺滑更清晰,而需要配置的选项。
二. 加载纹理
刚才说道,纹理也是一个 OpenGL 的对象,所以它的创建,跟 VBO 这些差不多,就是换了 texture 的关键字。步骤如下:
- 创建纹理对象
- 绑定纹理到上下文
- 创建bitmap数据
- 绑定bitmap数据到纹理
- 解绑和释放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
)
看看是不是跟四种模式对应上了呢。
五. 多纹理
上面都是一张纹理,这里我们尝试使用多张纹理。
细心的你,可能发现了,片段着色器中的 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
5.2 设置纹理变量
由于多加了一个纹理变量,所以需要指定变量代表的意思。
GLES30.glUniform1i(GLES30.glGetUniformLocation(programId,"ourTexture"),0)
GLES30.glUniform1i(GLES30.glGetUniformLocation(programId,"ourTexture2"),1)
5.2 添加多一张图片
前面都不需要变,只需要加多已张图片加载到 纹理对象中即可,图片为:
texture = loadTexture(TAG,MainApplication.context, R.mipmap.wuliuqi)
texture2 = loadTexture(TAG,MainApplication.context, R.mipmap.wuliuqi2)
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)
}
效果:
纹理相乘:
FragColor = texture1 * texture2;
混合
FragColor = mix(texture1,texture2,0.5);
这样,我们就把纹理的知识学完了。
参考:
https://learnopengl-cn.github.io/01%20Getting%20started/06%20Textures/
https://www.jianshu.com/p/3659f4649f98
https://juejin.cn/post/7150869291208802341