盒子
盒子
文章目录
  1. OpenGL ES 系列
  2. 着色器源码
  3. 纹理坐标
  4. 填充纹理坐标数据
  5. 创建纹理
  6. 纹理参数
  7. 填充与绑定纹理
  8. 渲染
  9. 优化
    1. 减少数据
    2. 防止变形
  10. 推荐

Android OpenGL ES 纹理

OpenGL ES 系列

Android OpenGL ES 基础原理

Android OpenGL ES 渲染模式

之前我们一直都是在绘制简单的图形与颜色,如果是一张图片该如何通过OpenGL ES进行渲染出来呢?

OpenGL ES的渲染方式是通过纹理来绘制出图片,通过纹理将图片像素值传递到对应位置,最终渲染出来。

着色器源码

还是老规矩,首先定义顶点与片段着色器源代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private const val VERTEX_SHADER_SOURCE =
"attribute vec4 a_Position;\n" +
"attribute vec2 a_textureCoordinate;\n" +
"varying vec2 v_textureCoordinate;\n" +
"void main() {\n" +
" v_textureCoordinate = a_textureCoordinate;\n" +
" gl_Position = a_Position;\n" +
"}"

private const val FRAGMENT_SHADER_SOURCE =
"precision mediump float;\n" +
"uniform sampler2D u_texture;\n" +
"varying vec2 v_textureCoordinate;\n" +
"void main() {\n" +
" gl_FragColor = texture2D(u_texture, v_textureCoordinate);\n" +
"}"

这里我们定义了a_textureCoordinate纹理的坐标,这个与之前颜色同理;然后是u_texture,代表二维纹理句柄,通过外界将纹理传递给GL程序,最终通过texture2D来转化成对应的颜色值。

纹理坐标

要绘制纹理,自然要有纹理坐标,要知道在哪些位置进行绘制。所以我们来定义纹理的坐标

1
2
3
4
5
6
7
8
9
10
11
12
13
// 整个视图
private val mVertexData = floatArrayOf(
-1.0f, 1.0f, -1.0f, -1.0f, 1.0f, -1.0f,
-1.0f, 1.0f, 1.0f, 1.0f, 1.0f, -1.0f
)

// 整个纹理
private val mTextureData = floatArrayOf(
0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f,
0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f
)

private const val TEXTURE_DIMENSION_SIZE = 2

之前我们提到顶点的坐标原点是在中心,且坐标范围是-1~1。而纹理的坐标原点是在左下角,且坐标范围是0~1。

所以你会发现顶点坐标与纹理坐标是上下颠倒的。

那么有的同学可能会有疑问,为什么要颠倒呢?都颠倒了绘制出来的图片不是也是颠倒的吗?

我可以很明确的说,不会的。让我先买个关子,原因后面再说。

有了坐标数据,接下来是干什么呢?如果你看了前面几篇文章就不陌生了。

将数据填充到Buffer中,并传递到GL程序中。

填充纹理坐标数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 加载纹理数据
val textureBuffer = ByteBuffer.allocateDirect(mTextureData.size * Float.SIZE_BYTES)
.order(ByteOrder.nativeOrder())
.asFloatBuffer()
textureBuffer.put(mTextureData)
textureBuffer.position(0)

// 获取对应纹理参数位置
val textureCoordinateLocation = GLES20.glGetAttribLocation(programId, "a_textureCoordinate")

// 启动对应纹理参数位置
GLES20.glEnableVertexAttribArray(textureCoordinateLocation)

// 填充对应顶点处纹理位置数据
GLES20.glVertexAttribPointer(textureCoordinateLocation, TEXTURE_DIMENSION_SIZE, GLES20.GL_FLOAT, false, 0, textureBuffer)

这样我们的纹理绘制区域就确定了。这一点跟顶点与颜色的数据填充完全相同,其实我们看它们在源码中的变量定义就能明白这一点。因为最终目的都是向GL程序进行填充数据。掌握到这一点以后任何数据的填充都是类似的,这就是GL程序的套路所在。

创建纹理

既然纹理区域确定了,现在我们就要来创建纹理。

1
2
3
4
// 创建纹理
val textures = IntArray(1)
GLES20.glGenTextures(textures.size, textures, 0)
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textures[0])

我们需要获得的是纹理句柄,所以我们将创建的纹理句柄保存到textures中,同时指定该纹理在GL程序中的通道索引,这里为0。

为什么要指定通道,因为纹理可以有多个,当你去绘制的时候需要选择指定的通道,才能绘制出自己想要的纹理。另外因为我们需要绘制的是二维图片,所以我们将纹理指定为GL_TEXTURE_2D二维。

纹理参数

创建纹理之后,我们继续设置纹理的参数。

1
2
3
4
5
// 设置纹理参数
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR)
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR)
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE)
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE)

其中GL_TEXTURE_MIN_FILTER与GL_TEXTURE_MAG_FILTER是纹理的过滤参数,作用是当纹理渲染时比原理的纹理小或者大时要如何处理,GL_LINEAR是线性处理方式,展示的效果是更平滑;还有一种是GL_NEAREST,它会选择与最近的像素,所以展示的效果有锯齿感。

下面我们将顶点坐标扩大5倍看下两种处理方式的效果。





GL_TEXTURE_WRAP_T与GL_TEXTURE_WRAP_S是纹理坐标超出纹理范围的处理参数。

GL_CLAMP_TO_EDGE以填充的方式进行处理。

GL_REPEAT以重复的方式进行处理。

下面我们将纹理坐标扩大5倍看下两种方式的处理效果。





填充与绑定纹理

我们通过加载本地的一种图片,将其通过OpenGL 进行渲染出来。

首先我们将本地的图片转化成Bitmap。

1
2
// 将资源图片解码成bitmap
val bitmap = BitmapFactory.decodeResource(context.resources, R.drawable.yaodaoji)

然后将Bitmap填充到Buffer中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 将bitmap填充到纹理中
val bitmapBuffer = ByteBuffer.allocateDirect(bitmap.width * bitmap.height * 4)
.order(ByteOrder.nativeOrder())
bitmap.copyPixelsToBuffer(bitmapBuffer)
bitmapBuffer.position(0)
GLES20.glTexImage2D(
GLES20.GL_TEXTURE_2D,
0,
GLES20.GL_RGBA,
bitmap.width,
bitmap.height,
0,
GLES20.GL_RGBA,
GLES20.GL_UNSIGNED_BYTE,
bitmapBuffer
)

这一步再熟悉不过了,将Bitmap数据填充到Buffer中,然后通过glTexImage2D将其填充到纹理中。

之前我们说到纹理坐标与顶点坐标上下颠倒的问题,为什么不会造成渲染出来的图片上下颠倒。

答案就在这里,我们渲染图片使用的是Bitmap的数据,而Bitmap的数据坐标是在左上角,所以此时加载的Bitmap数据是与纹理进行了上下颠倒的。而纹理又与顶点上下颠倒,这一来一回就刚好拨正了,所以我们真正渲染出来的图片就刚好是正确的方向。

纹理数据有了,再来将其绑定到对应的纹理上。

1
2
3
// 绑定特定索引纹理
val textureLocation = GLES20.glGetUniformLocation(programId, "u_texture")
GLES20.glUniform1i(textureLocation, 0)

注意这里的0,就是之前我们创建纹理时指定的通道,这样我们就能将其绑定到对应的纹理通道上。

渲染

1
2
3
4
5
6
7
8
// 设置清屏颜色
GLES20.glClearColor(1.0f, 1.0f, 1.0f, 1.0f)
// 清屏处理
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
// 设置视图大小
GLES20.glViewport(0, 0, mSurfaceViewWidth, mSurfaceViewHeight)
// 渲染
GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, mVertexData.size / VERTEX_DIMENSION_SIZE)

来看下渲染出来的效果



优化

有几个点我们可以进行优化。

减少数据

其中一个是我们使用的是GL_TRIANGLES方式进行渲染,在之前的文章我们知道这种渲染方式是不会共用顶点的,会导致顶点数量过多,占用的内存过大。

为了减少内存,我们可以使用GL_TRIANGLE_STRIP方式。这样我们就需要调整顶点与纹理坐标了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 整个视图
// --->
// \
// \
// --->
private val mVertexData = floatArrayOf(
-1.0f, -1.0f,
1.0f, -1.0f,
-1.0f, 1.0f,
1.0f, 1.0f
)

// 整个纹理
// --->
// /
// /
// --->
private val mTextureData = floatArrayOf(
0.0f, 1.0f,
1.0f, 1.0f,
0.0f, 0.0f,
1.0f, 0.0f
)

之前都是6个顶点,现在将它们都修改成4个即可。

通过GL_TRIANGLE_STRIP的特性,合理定义四个顶点的数据,就可以完全覆盖整个屏幕,即四个顶点绘制两个三角形,而这两个三角形刚好能够组合成屏幕的全部内容。

注意视图与纹理的坐标颠倒性,还有绘制的方向性。

运行之后,效果还是一致的,这里就不再展示了。

防止变形

而另一个是我们发现效果图片变形了。

这是由于原图片的宽高尺寸比例与展示的屏幕宽高尺寸比例不一致。

知道原因解决方案就有了,我们将图片纹理与渲染的屏幕进行一个宽高缩放,这样就能保证渲染出来的图片能够不变形展示。这一点与我们正常为一个ImageView设置不变形的做法一致。只不过是ImageView内部提供了设置ScaleType的方式。

具体的缩放处理方式就不贴源代码了,感兴趣的可以自己去查看源码。

我这里只说一下最终的处理位置,我们只需将Bitmap加载到纹理的时候,进行纹理缩放处理即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 加载纹理数据
val textureBuffer = ByteBuffer.allocateDirect(mTextureData.size * Float.SIZE_BYTES)
.order(ByteOrder.nativeOrder())
.asFloatBuffer()
// textureBuffer.put(mTextureData)
textureBuffer.put(
// 根据图片进行纹理缩放
Utils.adapterCoordinate(
mTextureData,
glSurfaceView.width,
glSurfaceView.height,
bitmap.width,
bitmap.height,
ScaleType.CENTER_FIT
)
)
textureBuffer.position(0)

这里使用的是居中等比缩放,再来看下处理后的效果。



纹理处理的方式也并不难,相信一路走下来的同学都有所体会

这也进一步说明OpenGL ES也没有很难,只是我们开始对它的使用方式不熟悉,因为它与我们正常的展示一张图片的方式完全不同,但明白它的处理方式之后,就会发现它也是有规律可循的,毕竟对应的API是不会出现太多的差异的。也希望能够帮助大家对OpenGL ES有一个全面的了解。

最后附上源码地址:OpenGL ES

推荐

android_startup: 提供一种在应用启动时能够更加简单、高效的方式来初始化组件,优化启动速度。不仅支持Jetpack App Startup的全部功能,还提供额外的同步与异步等待、线程控制与多进程支持等功能。

AwesomeGithub: 基于Github的客户端,纯练习项目,支持组件化开发,支持账户密码与认证登陆。使用Kotlin语言进行开发,项目架构是基于JetPack&DataBinding的MVVM;项目中使用了Arouter、Retrofit、Coroutine、Glide、Dagger与Hilt等流行开源技术。

flutter_github: 基于Flutter的跨平台版本Github客户端,与AwesomeGithub相对应。

android-api-analysis: 结合详细的Demo来全面解析Android相关的知识点, 帮助读者能够更快的掌握与理解所阐述的要点。

daily_algorithm: 每日一算法,由浅入深,欢迎加入一起共勉。

支持一下
赞赏是一门艺术