零、前言
今天开始以我的视角分享 OpenGL ES 知识,主要包括以下几个小点:
- 分享理论知识和相应的 API 使用,以及 EGL 环境搭建等,并结合例子进行展示。
- 在 Android、iOS、鸿蒙、mac 和 windows 多个平台上运行同一套底层代码,达到一处编写多端使用。
- 分享期间会不断的完善 EglBox 项目,使其满足日常开发,在需要用到 GL 时,可以开箱即用。
- 基于 OpenGL ES 封装一些好玩的项目,例如:相机、音视频播放、裁剪工具等。
话不多说,开始属于 "OpenGL ES" 的 Hello World 吧!
一、OpenGL ES 能做什么?
1-1、2D 图形界面
基于 OpenGL ES 可以开发我们自己的 2D 图形界面,例如:
-
nanovg( github.com/memononen/n… ),一个小型的图形渲染库。
-
skia( github.com/google/skia ),google 开发的 2D 图形库,在 Flutter 早期版本中作为主要的渲染引擎,底层便有使用到 OpenGL ES 进行渲染。
Impeller Availability:docs.google.com/spreadsheet…
后面分享的文章会介绍如何在 Flutter 上使用 OpenGL ES 渲染,让我们一起期待吧。
1-2、渲染 3D 效果
可以在移动设备上渲染 3D 效果,例如:
-
3D 胶卷渲染,最近的胶卷相机 APP 很多,在自己的 App 中加入胶卷的渲染可以丰富体验。
-
3D 字体渲染,结合 freetype 可以渲染不同字体的 3D 效果。
-
3D 场景渲染
这些都会在后续的文章中进行分享,一起期待吧
二、OpenGL ES 是什么?
OpenGL ES( OpenGL for Embedded Systems )是 OpenGL 的剪裁版本,专门针对嵌入式设备而设计,去除了许多不必要的特性以优化性能和内存占用。
它是一种跨平台的图形 API,不属于特定操作系统,可以在 Android、iOS、鸿蒙(HarmonyOS) 等多个移动平台以及智能电视、可穿戴设备、汽车信息娱乐系统等嵌入式系统上使用。
三、OpenGL ES 的版本
目前使用的 OpenGL ES 版本有 2.0 和 3.x (包括 3.0、3.1、3.2 ),他们之间从渲染表现和代码使用都有些许不同,接下来会一一阐述。
至于如何创建 2.0 和 3.x 的渲染环境(我们称之为 EGL ,全称 Embedded Graphics Library ),后面会单独一篇文章进行分享。
3-1、OpenGL ES 3.x 和 OpenGL ES 2.0
两者均支持 自定义渲染管线,都需要 GPU 硬件支持 ,不可以用软件模拟,所以开发 OpenGL ES 应用建议使用真机运行,避免一些不必要的问题。
现在市面上的机型基本上有 GPU 硬件,并不用担心 GPU 硬件的缺失。
3-2、OpenGL ES 3.x 相较于 OpenGL ES 2.0 的区别
- OpenGL ES 3.x 性能更好;
- OpenGL ES 3.x 光影效果更加真实,画质更加逼真、细腻;
- OpenGL ES 3.x 渲染技术有更多的缓冲区对象,增加了 GLSL ES 3.x 着色语言和计算着色器( Compute Shaders )的支持;
3-3、OpenGL ES 3.x 版本
- OpenGL ES 3.0 是基于 OpenGL 3.x 规范的子集。
- OpenGL ES 3.1 是基于 OpenGL 4.x 规范的子集。
- OpenGL ES 3.1 向下兼容,可以在原来的 OpenGL ES 2.0 和 OpenGL ES 3.0 的基础上增加新特性。
- OpenGL ES 3.0 对应 Android 4.3 版本以上, OpenGL ES 3.1 对应 Android 5.0 版本以上。
四、OpenGL ES 渲染管线
4-1、什么是渲染管线
OpenGL ES 渲染管线由 GPU 内部 处理图形信号的并行处理单元 组成,这些并行处理单元相互独立而且同时处理。
相较于 CPU 串行处理 而言,会大大提升渲染效率。越高端的 GPU 并行处理单元数量会更多,效率会更好。
4-2、渲染管线的作用
管线会将输入的描述数据(例如:顶点数据、颜色、矩阵),经过处理后,输出一帧图像,呈现给用户。
多帧图像以一定的顺序和时间间隔,最终形成我们所看到的连贯动画。
4-3、OpenGL ES 渲染管线处理流程
OpenGL ES 2.0 和 OpenGL ES 3.x 在流程上是一样的,但 “顶点着色器” 和 “片元着色器” 的 glsl 写法有区别,在 4-3-2 和 4-3-5 中进行分享。
这里的概念会较多,如果是初学者只需要有一个大致了解,后续的文章会进行深入分享,最后再回来看这张图会有不一样的想法。
4-3-1、输入装配(Input Assembly)
主要将以下数据从 CPU 传到 GPU:
1、顶点数据,例如:顶点位置、法线、纹理坐标、颜色等; 2、绘制方式,图元类型(点、线、面); ....
4-3-2、顶点着色器(Vertex Shader)
对 “输入装配” 传入的每个顶点都会执行一次 “顶点着色器” 代码。 会对每个顶点进行以下处理:
- 模型变换、视图变换、投影变换等操作;
- 顶点属性计算,例如:颜色、法线方向变换、纹理坐标变换等;
顶点着色器在 OpenGL ES 2.0 的工作原理:
- attribute 变量:每个顶点需要的属性,例如顶点的位置、颜色、法向量等信息。
- uniform 变量: 对于同一组顶点组成的单个物体中所有顶点都相同的变量。例如:投影矩阵、摄像机位置、光源位置等。
- varying 变量: 从顶点着色器计算产生并传递到片元着色器的数据变量。
- 内建变量: gl_Position: 经过变换矩阵变换、投影后顶点的最终位置。 gl_FrontFacing: 片元所在面的朝向。 gl_PointSize: 点的大小。
值得注意:
当我们在顶点着色器中给 varying 变量赋值后,这些值并不会直接传递给片元着色器。实际上,图形渲染管线会先执行光栅化处理。在光栅化阶段,渲染管线会根据以下信息为每个片元计算出 varying 变量的值:
- 图元各个顶点的 varying 变量值(顶点着色器的输出);
- 当前片元在图元内的相对位置;
通过这种插值机制,片元着色器最终接收到的 varying 变量值是根据片元在图元中的位置,由相邻顶点的值按比例计算得出的结果。确保了图形表面的颜色、纹理坐标等属性能够平滑过渡。
varying 变量的插值处理过程:
此处以颜色为例,其他属性也是一样的计算规则。
顶点着色器在 OpenGL 3.0 的工作原理
- in 变量: 等同于 OpenGL ES 2.0 中的 attribute 。每个顶点需要的属性变量,例如顶点的位置、颜色、法向量等信息。
- uniform 变量: 和 OpenGL 2.0 中的 uniform 一样。对于同一组顶点组成的单个物体中所有顶点都相同的变量。例如:投影矩阵、摄像机位置、光源位置等。
- out 变量: 从顶点着色器计算产生并传递到片元着色器的数据变量。可以通过以下写法控制进行插值或是不插值。 smooth out(默认值): 等同于 OpenGL ES 2.0 中的 varying ,会进行插值,插值规则完全相同。 flat out : 不会进行插值,使用图元中的最后一个顶点(例如三角形的第三个顶点)的值,所以此模式下图元每个片元的值都相同。
- 内建输出变量: gl_Position: 是经过变换矩阵变换、投影后的顶点的最终位置。和 OpenGL ES 2.0 中的 gl_Position 完全相同。 gl_PointSize: 点的大小。和 OpenGL ES 2.0 中的 gl_PointSize 完全相同。
- 内建输入变量: gl_VertexID: 存储当前被处理顶点的整数索引,类型是 int 。 gl_InstanceID: 实例化渲染中的实例索引,从 0 开始,每绘制一个新的实例就递增 1 。
4-3-3、图元装配(Primitive Assembly)
图元装配有两个步骤:图元组装 和 图元处理。
(1)图元组装:将顶点数据按照设置的绘制方式进行结合成完整的图元。
- 点绘制时,则一个点为一个图元。
- 线绘制时,则两个顶点结合为一个图元。
- 三角形绘制时,则将三个顶点结合为一个图元。
(2)图元处理:会经历三个过程
- 裁剪:判断图元是否在视景体中,有三种可能:
- 图元完全在视景体中,则整个图元保留;
- 图元完全不在视景体中,则整个图元抛弃;
- 图元部分在视景体中,则会进行增添顶点,保证视景体中的部分保留,超出的部分则抛弃;
- 透视除法:将裁剪空间坐标(x, y, z, w)转换为归一化设备坐标 (NDC,Normalized Device Coordinates) ,会进行 (x/w, y/w, z/w) 计算,让坐标影射到 [-1, 1] 区间
- 视口变换:根据
glViewport
设置的视口大小,将 NDC 坐标映射为窗口坐标。
一图胜千言
4-3-4、光栅化(Rasterization)
因为移动设备是通过像素来进行展示,所以需要将连续数学量表示的几何图元进行离散为一个个片元(此时不是像素)。“光栅化” 则是将连续数学量分解为离散化片元的这个动作。
值得注意的是,此过程会根据顶点数据(例如:顶点坐标、纹理坐标、法向量、颜色等)进行插值计算得到每个片元的属性。
片元和像素的区别
图元的处理是并发的,所以片元的产生顺序并不固定的,产生的片元都会暂时送入对应的帧缓冲位置,当出现同位置的片元时,靠近观察点的片元覆盖远的片元(具体的处理会在深度测试阶段进行),所以片元不一定是像素,只能说是候补像素。
4-3-5、片元着色器
光栅化得到的每个片元都会进行一次片元着色器的运算。 主要功能是计算片元的最终颜色和其他属性。
片元着色器在 OpenGL ES 2.0 的处理原理:
- varying 变量: 从顶点着色器传递到片元着色器的值。系统会在顶点着色器后的光栅化阶段自动插值产生,并不是顶点着色器直接产出的值。
- 内建变量: gl_FragColor: 片元的最终颜色。一般在片元着色器的最后会对其赋值。
片元着色器在 OpenGL ES 3.0 的处理原理:
- in 变量: 和 OpenGL ES 2.0 的 varying 一样,只是换了名称。
- out 变量: 由片元着色器写入计算完成的片元颜色值的变量。值得注意的是,OpenGL ES 2.0 中的 gl_FragColor 在 OpenGL ES 3.0 中被移除了。 需要输出片元颜色时,则定义一个
out vec4
类型的变量即可,变量名称没有规定。 - 内建输入变量: gl_FrontFacing: 用于确定当前片元所属的三角形是正面还是背面。是一个布尔类型的变量。如果值为 true ,表示是正面;如果值为 false ,表示是背面。 gl_FragCoord: 获取当前片元在帧缓冲中的屏幕坐标位置。是一个 vec4 类型的变量。gl_FragCoord.x 和 gl_FragCoord.y 表示片元在帧缓冲的二维屏幕坐标的位置。gl_FragCoord.z 表示片元的深度值,即深度坐标。而 gl_FragCoord.w 则表示透视除法的缩放因子( 1.0 / gl_FragCoord.w )。 gl_PointCoord: 是一个 vec2 类型的变量,表示当前片元在点精灵上的纹理坐标位置。gl_PointCoord.x 和 gl_PointCoord.y 的取值范围是从 0.0 到 1.0 ,表示纹理坐标的范围。
- 内建输出变量: gl_FragDepth: 设置片元的深度值,一个浮点数变量。
性能的优化点:
尽量减少片元着色器的运算量,将复杂的运算尽可能由顶点着色器来处理,因为顶点着色器的运行次数远远小于片元着色器的执行次数。
4-3-6、片元级测试与操作(Per-fragment Operations)
此阶段会对片元按以下顺序执行操作:
- 裁剪测试(Scissor Test)
- 模版测试(Stencil Test)
- 深度测试(Depth Test)
- 混合(Blending)
- 抖动
剪裁测试
开启裁剪测试之后,后续的绘制操作只会在裁剪区域中,而不再是整个视口区域。
模板测试
根据模板缓冲中存储的数值以及预先设置的比较条件,决定是否允许当前片元写入帧缓冲。如果不允许写入则片元会被抛弃。一般用在湖面倒影、镜像等场景。
深度测试
这个阶段比较片元的远近,会保留近的片元,丢弃远的片元。
混合
将新片元的颜色与帧缓冲中已有颜色进行 “加权” 或 “数学操作” 后,写回帧缓冲中。可以做透明度等效果。
抖动
允许只使用少量的颜色模拟出更宽的颜色显示范围,从而使颜色视觉效果更加丰富。例如,可以使用白色以及黑色模拟出一种过渡的灰色。缺点是会损失一部分分辨率。
对于现在主流的原生颜色就很丰富的显示设备一般不需要启用抖动。
4-3-7、帧缓冲
OpenGL ES 中的物体绘制并不是直接在屏幕上进行的,而是预先在帧缓冲区中进行绘制,每绘制完一帧再将绘制的结果交换到屏幕上。
这阶段将通过所有测试(模板测试、深度测试等)的片元,记录到用于显示的颜色缓冲中,为最终呈现在屏幕上做准备。
帧缓冲由以下三个组件构成:
- 颜色缓冲:存储每个片元的颜色值,每个颜色值包括 RGBA 4 个色彩通道,应用程序运行时在屏幕上看到的就是颜色缓冲中的内容。
- 深度缓冲:存储每个片元的深度值,深度值是指以特定的内部格式表示从片元处到观察点(摄像机)的距离。在启用深度测试的情况下,新片元想进入帧缓冲时需要将自己的深度值与帧缓冲中对应位置片元的深度值进行比较,若结果为小于才有可能进入缓冲,否则被丢弃。(在进行 3D 渲染时,深度测试则尤为重要)
- 模板缓冲:存储每个片元的模板值,供模板测试使用。
至此对渲染管线就大致的了解,对于新手这么多的概念会有一时消化不了,但不用担心,先有一定的概念作为支撑,后续分享的文章会进行讲解这些概念并结合实战。
五、Hello World —— OpenGL ES
经过漫长的理论,接下来编写一个最为简单、也最为常见的 OpenGL ES 程序 —— “在 OpenGL ES 中渲染一个三角形” ,作为开篇的一个结束语。
第一步,编写 “顶点着色器” 和 “片元着色器”
以 OpenGL ES 3.X 进行编写着色器
顶点着色器
#version 300 es
uniform mat4 uMVPMatrix;
in vec3 aPosition;
in vec4 aColor;
out vec4 vColor;
void main() {
gl_Position = uMVPMatrix * vec4(aPosition, 1.0);
vColor = aColor;
}
可以看到输入的顶点坐标 aPosition 和顶点颜色 aColor 使用了 in 进行描述,因为会根据不同顶点而变动。
偏移矩阵 uMVPMatrix 使用 uniform 进行描述,因为对于整个三角形偏移矩阵是一致的。
传递给片元着色器的颜色 vColor 使用 out 描述,默认为 smooth ,所以会在光栅话阶段进行插值后再作为输入传递给片元着色器。
片元着色器
#version 300 es
precision mediump float;
in vec4 vColor;
out vec4 fragColor;
void main() {
fragColor = vColor;
}
输入颜色 vColor 则为顶点着色器插值后的值,这里的着色器较为简单,直接作为输出颜色。
OpenGL ES 3.X 的片元着色器通过一个 out vec4
类型的变量来进行输出颜色,此处将它命名为 fragColor
。
第二步,加载着色器和获取对应的属性值
加载着色器的操作流程是固定的,可以将其封装起来,我将其封装为 GLProgram
。
GLProgram 传送门:github.com/zincPower/E…
abstract class GLProgram {
// ... 省略属性
override fun init() {
if (isInit()) {
Logger.e(TAG, "Program has been initialized. 【init】id=$id")
return
}
createProgram()
if (id != 0) onInit()
}
private fun createProgram() {
val vertexShaderSource = getVertexShaderSource()
if (vertexShaderSource.isEmpty()) {
Logger.e(TAG, "VertexShaderSource is empty.")
return
}
val fragmentShaderSource = getFragmentShaderSource()
if (fragmentShaderSource.isEmpty()) {
Logger.e(TAG, "FragmentShaderSource is empty.")
return
}
// 加载顶点着色器
mVertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertexShaderSource)
if (mVertexShader == 0) {
Logger.e(TAG, "Vertex shader load failure. ")
releaseResource()
return
}
// 加载片元着色器
mFragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentShaderSource)
if (mFragmentShader == 0) {
Logger.e(TAG, "Fragment shader load failure. VertexShader=$mVertexShader")
releaseResource()
return
}
// 创建程序
id = GLES20.glCreateProgram()
if (id == 0) {
Logger.e(TAG, "Create program failure.")
releaseResource()
} else { // 若程序创建成功则向程序中加入顶点着色器与片元着色器
// 向程序中加入顶点着色器
GLES20.glAttachShader(id, mVertexShader)
// 向程序中加入片元着色器
GLES20.glAttachShader(id, mFragmentShader)
// 链接程序
GLES20.glLinkProgram(id)
// 存放链接成功 program 数量的数组
val linkStatus = IntArray(1)
// 获取 program 的链接情况
GLES20.glGetProgramiv(id, GLES20.GL_LINK_STATUS, linkStatus, 0)
// 若链接失败则报错并删除程序
if (linkStatus[0] != GLES20.GL_TRUE) {
Logger.e(TAG, "Link program failure. id=$id, errorLog= \n ${GLES20.glGetProgramInfoLog(id)}")
releaseResource()
} else {
Logger.i(TAG, "Create program success. id=$id")
}
}
}
private fun releaseResource() {
Logger.i(TAG, "Release program. ProgramId=$id, VertexShaderId=$mVertexShader, FragmentShaderId=$mFragmentShader")
if (mVertexShader != 0) {
if (id != 0) GLES20.glDetachShader(id, mVertexShader)
GLES20.glDeleteShader(mVertexShader)
mVertexShader = 0
}
if (mFragmentShader != 0) {
if (id != 0) GLES20.glDetachShader(id, mFragmentShader)
GLES20.glDeleteShader(mFragmentShader)
mFragmentShader = 0
}
if (id != 0) {
GLES20.glDeleteProgram(id)
id = 0
}
}
// ... 省略其他方法
}
createProgram
方法会加载顶点着色器和片元着色器,这一过程会检测是否有异常,如果有异常则释放相应的着色器和 Program ,否则将 Program id 进行保存,后续可以使用。
abstract class GLProgram : GLObject {
// ... 省略 init 和 createProgram 方法
override fun isInit(): Boolean = (id != 0)
override fun release() {
if (isInit()) {
onRelease()
val currentProgram = EglBox.getCurrentProgram()
if (currentProgram == id) GLES20.glUseProgram(0)
releaseResource()
}
}
fun bind() {
if (!isInit()) {
Logger.e(TAG, "Program id is invalid. Please call create method first.")
return
}
GLES20.glUseProgram(id)
}
fun unbind() {
GLES20.glUseProgram(0)
}
protected fun getUniformLocation(uniformName: String): Int {
if (!isInit()) {
Logger.e(TAG, "Program isn't initialized. Please call init function first. uniformName=$uniformName")
return 0
}
return GLES20.glGetUniformLocation(id, uniformName)
}
protected fun getAttribLocation(attributeName: String): Int {
if (!isInit()) {
Logger.e(TAG, "Program isn't initialized. Please call init function first. attributeName=$attributeName")
return 0
}
return GLES20.glGetAttribLocation(id, attributeName)
}
fun draw() {
if (isInit()) {
GLES20.glUseProgram(id)
onDraw()
GLES20.glUseProgram(0)
} else {
Logger.e(TAG, "Program hasn't initialized. 【draw】")
}
}
}
剩余的 GLProgram 代码功能有:
- 将 Program 绑定
bind
和解绑unbind
。 - 将 Program 释放的
release
方法。 - 获取 Program 绑定的着色器中的属性
getAttribLocation
和一致变量getUniformLocation
。 - 调用 Program 绘制的
draw
方法。
abstract class GLProgram : GLObject {
// ... 省略其他方法
protected abstract fun onInit()
protected abstract fun onDraw()
protected abstract fun onRelease()
protected abstract fun getVertexShaderSource(): String
protected abstract fun getFragmentShaderSource(): String
}
剩余的还有抽象方法,用于给到真正的 Program 进行继承实现,例如我们这里需要实现三角形的 Program ,我们创建一个类 TriangleProgram
并继承 GLProgram
。
/**
* @author jiang peng yong
* @date 2024/6/15 13:05
* @email 56002982@qq.com
* @des 绘制三角形
* 第一个点 第二个点
* 红色 绿色
* ***********
* *********
* **原点**
* *****
* ***
* *
* 第三个点
* 蓝色
*/
class TriangleProgram : GLProgram() {
private val mVertexBuffer = allocateFloatBuffer(
floatArrayOf(
-0.5F, 0.5F, 0.0F,
0.5F, 0.5F, 0.0F,
0.0F, -0.5F, 0.0F
)
)
private val mColorBuffer = allocateFloatBuffer(
floatArrayOf(
1F, 0F, 0F, 1F,
0F, 1F, 0F, 1F,
0F, 0F, 1F, 1F
)
)
private var mMVPMatrixHandle = 0
private var mPositionHandle = 0
private var mColorHandle = 0
private val mVertexCount = 3
private val mMatrix = ModelMatrix()
override fun onInit() {
mMVPMatrixHandle = getUniformLocation("uMVPMatrix")
mPositionHandle = getAttribLocation("aPosition")
mColorHandle = getAttribLocation("aColor")
}
override fun onDraw() {
GLES20.glUniformMatrix4fv(mMVPMatrixHandle, 1, false, mMatrix.matrix, 0)
GLES20.glVertexAttribPointer(mPositionHandle, 3, GLES20.GL_FLOAT, false, 3 * 4, mVertexBuffer)
GLES20.glVertexAttribPointer(mColorHandle, 4, GLES20.GL_FLOAT, false, 4 * 4, mColorBuffer)
GLES20.glEnableVertexAttribArray(mPositionHandle)
GLES20.glEnableVertexAttribArray(mColorHandle)
GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, mVertexCount)
GLES20.glDisableVertexAttribArray(mPositionHandle)
GLES20.glDisableVertexAttribArray(mColorHandle)
}
override fun onRelease() {
mMVPMatrixHandle = 0
mPositionHandle = 0
mColorHandle = 0
}
override fun getVertexShaderSource(): String = """
#version 300 es
uniform mat4 uMVPMatrix;
in vec3 aPosition;
in vec4 aColor;
out vec4 vColor;
void main() {
gl_Position = uMVPMatrix * vec4(aPosition, 1.0);
vColor = aColor;
}
""".trimIndent()
override fun getFragmentShaderSource(): String = """
#version 300 es
precision mediump float;
in vec4 vColor;
out vec4 fragColor;
void main() {
fragColor = vColor;
}
""".trimIndent()
}
可以看到 getVertexShaderSource
和 getFragmentShaderSource
返回的是一开始编写的着色器。并且在 onInit
的时候获取了着色器中 in
和 uniform
属性的句柄,在 onDraw
的时候可以进行传值顶点坐标和顶点颜色,最后进行绘制。
第三步,使用 GLSurfaceView 提供 EGL 环境进行承载后运行
GLSurfaceView
是 Android 提供的带有 EGL 环境的 View ,后续会有文章分享如何构建自己的 EGL 环境。
class TriangleActivity : AppCompatActivity() {
private lateinit var mRenderView: RenderView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mRenderView = RenderView(this)
setContentView(mRenderView)
}
override fun onResume() {
super.onResume()
mRenderView.onResume()
}
override fun onPause() {
super.onPause()
mRenderView.onPause()
}
class RenderView(context: Context?) : GLSurfaceView(context) {
private val mRenderer = Renderer()
init {
setEGLContextClientVersion(3)
setRenderer(mRenderer)
renderMode = RENDERMODE_WHEN_DIRTY
}
private class Renderer : GLSurfaceView.Renderer {
private val mTriangleProgram = TriangleProgram()
override fun onSurfaceCreated(gl: GL10?, config: EGLConfig?) {
mTriangleProgram.init()
}
override fun onSurfaceChanged(gl: GL10?, width: Int, height: Int) {
GLES20.glViewport(0, 0, width, height)
}
override fun onDrawFrame(gl: GL10?) {
GLES20.glClear(GLES20.GL_DEPTH_BUFFER_BIT or GLES20.GL_COLOR_BUFFER_BIT)
mTriangleProgram.draw()
}
}
}
}
在 RenderView
中可以看到使用了 TriangleProgram
,进行创建和驱动渲染。
但是因为 GLSurfaceView
并没有提供一个释放的回调,所以并没有调用 TriangleProgram
的释放,后续我们创建自己的 EGL 环境就可以很好的控制 EGL 的生命周期,所以暂时忽略这一点。
小结
至此你已经知道怎么编写一个 OpenGL ES 程序了,但你会发现中间很多陌生的 API 和参数,不必担心,带着这份好奇继续后面的学习,这些都会一一解开。
来个 “课后作业” 吧~(是不是有种回到学校的感觉了,哈哈),如果让你绘制一个五角星,你需要怎么编写呢?效果如下图
六、写在最后
EglBox-Android 项目地址:github.com/zincPower/E… (如果对你有所帮助或喜欢的话,赏个 star 吧,码字不易,请多多支持)
如果觉得本篇博文对你有所启发或是解决了困惑,点个赞或关注我呀。
公众号搜索 “江澎涌”,更多优质文章会第一时间分享与你。