盒子
盒子
文章目录
  1. 基本概念
    1. 坐标
    2. 形状与方向
  2. GL程序
    1. 着色器源码
    2. 创建GL程序
    3. 添加顶点与片段着色器
    4. 链接与应用
  3. 数据填充
  4. 渲染
  5. 推荐

Android OpenGL ES 基础原理

由于5G的发展,现在音视频越来越流行,我们的生活已经完全被抖音、视频号、B站等视频应用所包围。从这一点也能看到音视频的重要性。

而作为一名Android开发者,是时候来了解一下关于Android方面渲染方面的知识。音视频的应用都离不开OpenGL ES的处理。对于视频的高效渲染与融合操作是至关重要的。

上面的这种动画相信大家都很熟悉,类似的动画在各大直播间都会出现。那么这炫酷的原理实现内部都离不开OpenGL ES的高效渲染与更高级的融合处理。

多的就先不说了,现在我们就来认识一下OpenGL ES。

基本概念

Android可以通过OpenGL来支持高效的2D和3D图形,同时OpenGL是一种跨平台的图形API。其中OpenGL ES是OpenGL规范的一种形式,适用于嵌入式设备。

Android支持多种版本的OpenGL ES API:

  1. 1.0&1.1 Android1.0及以上
  2. 2.0 Android2.2及以上
  3. 3.0 Android4.3及以上
  4. 3.1 Android5.0及以上

我们的内容主要是基于OpenGL ES 2.0来进行,也就是进行二维的图形渲染。

坐标

在Android中通过Canvas进行绘制的坐标原点是在屏幕的左上角,同时它的坐标范围都是以屏幕的宽高来定义。

OpenGL ES则不同,它是以绘制区域的中心为原点,同时它的坐标范围是-1.0 ~ 1.0。也就是说它的坐标都是基于可绘制区域进行比例换算。并不是真正的值。

形状与方向

在OpenGL ES中,绘制的形状都是以三角形为基础,也就是说它必须由3个或者以上的点来进行绘制。所以它是由多个三角形进行组合成特定的形状,进过不同程度的交叉与重叠来达到不同的形状。

例如以二维空间来定义

同时还存在绘制顺序,所谓的绘制顺序也是以三角形为基础,通过三角形的三个顶点进行环绕绘制。默认是以逆时针进行绘制。

对于二维图像可能绘制顺序没那么重要,但是对于三维图像就很重要了。三维图像是由于视角的问题,会存在正反面的关系。

例如一款3D游戏,游戏中有一辆汽车,正对我们的为正面,我们看不到的一面为反面,虽然反面看不到,但OpenGL ES还是会进行绘制。为了对反面不做无用的绘制,可以使用OpenGL的面剔除操作,该操作允许渲染管道忽略形状的反面,这样就可以节约时间与内存并缩短处理周期。

那么这里的正面就是沿逆时针绘制的面。

GL程序

OpenGL ES渲染需要借助GL程序,通过创建GL程序、顶点与片段着色器、加载着色器代码、编译代码、应用、数据填充,最终进行渲染。

在创建GL程序之前,我们先来了解顶点着色器与片段着色器。

着色器源码

GL程序渲染的过程中需要确认顶点位置与对应的颜色,而这两个部分分别借助于顶点与片段着色器来实现。

1
2
3
4
5
6
7
8
9
10
11
12
// 顶点
private const val VERTEX_SHADER_SOURCE =
"attribute vec4 a_Position;\n" +
"void main() {\n" +
" gl_Position = a_Position;\n" +
"}"
// 片段
private const val FRAGMENT_SHADER_SOURCE =
"precision mediump float;\n" +
"void main() {\n" +
" gl_FragColor = vec4(0.0, 0.0, 1.0, 1.0);\n" +
"}"

上面分别是顶点着色器与片段着色器的源码。attribute是变量修饰符,用的比较多的是以下三种。

  1. attribute:表示只读的顶点数据,应用在顶点着色器中。可修饰声明顶点、颜色等数据
  2. uniform:顶点着色器与片段着色器的共享数据,在程序中值的不变的,初始值由程序外部传入
  3. varying:顶点着色器输入,片段着色器输出;由顶点着色器传输给片段着色器中的插值数据

vec4是变量类型,变量主要有以下几种

除此之外还有数组与结构体,用来实现复杂的数据类型。

我们将定义的a_Position赋值给gl_Position,这样GL程序就会使用定义的顶点数据进行渲染。

同理gl_FragColor也是一样,代表对应渲染顶点位置时的颜色,这里直接写死了一个蓝色。

创建GL程序

首先我们要创建GL程序

1
2
// 创建GL程序
val programId = GLES20.glCreateProgram()

拿到programId,为之后的程序操作做准备

添加顶点与片段着色器

首先创建顶点与片段着色器

1
2
3
// 创建顶点与片段着色器
val vertexShader = GLES20.glCreateShader(GLES20.GL_VERTEX_SHADER)
val fragmentShader = GLES20.glCreateShader(GLES20.GL_FRAGMENT_SHADER)

将之前定义的着色器源码加载到着色器中

1
2
3
// 加载顶点与片段着色器代码
GLES20.glShaderSource(vertexShader, VERTEX_SHADER_SOURCE)
GLES20.glShaderSource(fragmentShader, FRAGMENT_SHADER_SOURCE)

通过GL程序进行编译

1
2
3
// 编译顶点与片段着色器代码
GLES20.glCompileShader(vertexShader)
GLES20.glCompileShader(fragmentShader)

最后将编译完的顶点与片段着色器添加到指定的GL程序中,也就是我们第一步创建的GL程序

1
2
3
// 添加到GL程序中
GLES20.glAttachShader(programId, vertexShader)
GLES20.glAttachShader(programId, fragmentShader)

链接与应用

着色器装载完毕之后,剩下的就是将我们创建的GL程序进行链接与应用

1
2
3
4
// 链接GL程序
GLES20.glLinkProgram(programId)
// 应用GL程序
GLES20.glUseProgram(programId)

这样我们的GL程序才算真正的完成了,下面就是数据的填充与渲染操作。

数据填充

在顶点着色器源码定义中,我们定义了a_Position变量,需要我们从外部将数据添加到a_Position,这样才能真正应用到gl_Position中。

下面我们来进行数据的填充

首先我们定义一个填充的顶点数据

1
2
3
private val mVertexData = floatArrayOf(0.0f, 0.5f, -0.5f, -0.5f, 0.5f, -0.5f, -0.5f, 1.0f, -1.0f, 0.5f, 0f, 0.5f)

private const val VERTEX_DIMENSION_SIZE = 2

顶点维度是二维,所以这里mVertexData中定义了6个顶点数据,也就是2个三角形的数据。第一个在左上角,第二个在中间。

我们将数据添加到Buffer中,并将索引位置定义到开始位置0

1
2
3
4
5
6
// 加载顶点数据
val vertexBuffer = ByteBuffer.allocateDirect(mVertexData.size * Float.SIZE_BYTES)
.order(ByteOrder.nativeOrder())
.asFloatBuffer()
vertexBuffer.put(mVertexData)
vertexBuffer.position(0)

继续获取a_Position在GL程序中的参数位置

1
2
// 获取对应参数位置
val positionLocation = GLES20.glGetAttribLocation(programId, "a_Position")

这一点与我们平常的编程不同,在GL程序中,如果要获取其中的变量,我们并不是直接拿到这个变量的本身,而是通过拿到它在GL中对应的位置索引,然后通过位置索引进行变量操作。

获取之后还要进行启动激活

1
2
// 启动对应参数位置
GLES20.glEnableVertexAttribArray(positionLocation)

最后就是填充

1
2
// 填充顶点数据
GLES20.glVertexAttribPointer(positionLocation, VERTEX_DIMENSION_SIZE, GLES20.GL_FLOAT, false, 0, vertexBuffer)

VERTEX_DIMENSION_SIZE代表的是填充一个二维的顶点数据,类型为GLES20.GL_FLOAT。

渲染

GL程序与顶点数据都已经准备完毕,接下来是最后一步渲染。

在渲染之前我们需要对屏幕进行清屏操作,默认屏幕是黑色,我们可以指定需要的清屏后的颜色

1
2
3
4
// 设置清屏颜色
GLES20.glClearColor(1.0f, 1.0f, 1.0f, 1.0f)
// 清屏处理
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)

这里指定清屏颜色为白色

在渲染之前设置渲染的视图位置与大小,最后再进行渲染。

1
2
3
4
// 设置视图大小
GLES20.glViewport(0, 0, mSurfaceViewWidth, mSurfaceViewHeight)
// 渲染
GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, mVertexData.size / VERTEX_DIMENSION_SIZE)

在渲染操作中使用了GLES20.GL_TRIANGLES,这是一种渲染方式,它代表会以每3个顶点为一组的方式进行三角形渲染,所以我们运行之后就能看到2个三角形。

参数0与mVertexData.size / VERTEX_DIMENSION_SIZE代表有6个顶点且从第0个位置开始,也就是第一个顶点位置。

最后我们再来看下运行后的效果



大功告成,与我们的预期完全一样。

后续会继续聊聊颜色的动态填充、渲染的三种方式与纹理的操作,敬请期待。

源码地址: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: 每日一算法,由浅入深,欢迎加入一起共勉。

支持一下
赞赏是一门艺术