零、前言
今天开始以我的视角分享 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( https://github.com/memononen/nanovg ),一个小型的图形渲染库。
-
skia( https://github.com/google/skia ),google 开发的 2D 图形库,在 Flutter 早期版本中作为主要的渲染引擎,底层便有使用到 OpenGL ES 进行渲染。
Impeller Availability:https://docs.google.com/spreadsheets/d/1AebMvprRkxP-D6ndx920lbvDBbhg-sNNRJ64XY2P2t0/edit?gid=0#gid=0
后面分享的文章会介绍如何在 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)
- 抖动
剪裁测试
开启裁剪测试之后,后续的绘制操作只会在裁剪区域中,而不再是整个视口区域。
模板测试
根据模板缓冲中存储的数值以及预先设置的比较条件,决定是否允许当前片元写入帧缓冲。如果不允许写入则片元会被抛弃。一般用在湖面倒影、镜像等场景。
深度测试
这个阶段比较片元的远近,会保留近的片元,丢弃远的片元。
混合
将新片元的颜色与帧缓冲中已有颜色进行 “加权” 或 “数学操作” 后,写回帧缓冲中。可以做透明度等效果。