OpenGL ES
OpenGL ES 简介
OpenGL ( Open Graphics Library ) 是一种跨平台的图形API,用于为 3D 图形处理硬件指定标准的软件接口。
OpenGL ES (OpenGL for Embedded System) 是 OpenGL 规范的一种形式,适用于嵌入式设备。
OpenGL ES的世界里面只有点、线、三角形,其它更为复杂几何形状都是由三角形构成的
Android 可通过开放图形库 OpenGL ES API 来支持高性能 2D 和 3D 图形。
Android 支持多版 OpenGL ES API | |
---|---|
OpenGL ES 1.0 和 1.1 | Android 1.0 及更高版本的支持 |
OpenGL ES 2.0 | Android 2.2(API 级别 8)及更高版本的支持 |
OpenGL ES 3.0 | Android 4.3(API 级别 18)及更高版本的支持 |
OpenGL ES 3.1 | Android 5.0(API 级别 21)及更高版本的支持 |
一、相同点
相比于 OpenGL ES 1.x 系列的固定功能管线,OpenGL ES 2.0 和 OpenGL ES 3.0 都是可编程图形管线。开发者可以自己编写图形管线中的 顶点着色器 和 片段着色器 两个阶段的代码。
二、不同点
1. 兼容性
OpenGL ES 3.0 是向后兼容 OpenGL ES 2.0 的。也就是说使用 2.0 编写的应用程序是可以在 3.0 中继续使用的。
2. 新特性
采用阴影贴图、体渲染(volume rendering)、基于 GPU 的粒子动画、几何形状实例化、纹理压缩和伽马校正等技术
图形管线 (Graphics pipeline)
OpenGLES3.0 新增一个新功能—变换反馈,使顶点着色器输出可以选择性地写入一个输出缓冲区
1、Vertex Buffer/ArrayObjects 顶点缓冲区/数组对象
顶点数据来源,即渲染管线的顶点输入,通常使用Buffer object效率更好。
2、Vertex Shader 顶点着色器
顶点着色器,该模块为可编程模块,可以通过API来对顶点进行操作。顶点着色器是以顶点为目标来进行处理的,如通过矩阵变换位置,根据光源生成每个顶点的颜色数据,以及计算生成或移动纹理的坐标。
3、Primitive Assembly 图元装配
图元(Primitive)是一个可以使用合适OpenGL ES指令进行渲染的几何结构。图元装配(Primitive Assembly),顾名思义就是组装图元的意思,也就是说它会把顶点组装成图元,同时它也会对它组装的图元进行一个简单的处理以使得在后续流程中只处理可以在屏幕中显示的图元。图元装配首先会将顶点着色器处理过的顶点组装成一个一个独特的可以被渲染的几何图元,如三角形、线、点块纹理。在组装好图元之后,它会判断该图元是否处于屏幕的可显示的范围内,如果图元完全不在屏幕的可显示范围内,那么它就会丢弃该图元,如果图元有一部分在可显示的范围内,则裁切图元,丢弃不在屏幕显示范围内的部分。除此之外,图元装配也会去判断图元的朝向是面向正面还是背面,如果图元是面向背面的,那么该图元也会被丢弃。
4、Rasterization 光栅化
在图元装配之后就是光栅化(Rasterization)图元了,它是将上一步装配好的图元(点块、线、三角形)转换成可以画到屏幕上的二维片(two-dimensional fragments),以便家下来的片元着色器(fragmentsshader)对它进行处理
5、Fragment Shader 片段着色器
片段着色器(Fragment Shader)也是一个可编程模块,我们可以通过OpenGL ES的对应api来对片元进行处理。片元着色器可以丢弃片元(fragment)也可以为片元生成一个颜色值储存在内置变量gl_FragColor中。光栅化阶段生成的颜色、深度、模板和屏幕坐标将成为下一个阶段——逐个片源处理(Per-Fragment operations)的输入。
6、Per-FragmentOperations
在逐个片元处理阶段,一个经由光栅化阶段生成的片元,假如它对应的屏幕坐标为(x,y),那么在这个阶段,在该片元处理过程中,只能改动framebuff中坐标为(x,y)的像素
着色器和程序
OpenGL ES中,每个程序对象必须连接一个顶点着色器和一个片段着色器。
程序对象被链接为用于渲染的最后可执行程序
顶点着色器
可以通过API来对顶点进行操作。顶点着色器是以顶点为目标来进行处理的,如通过矩阵变换位置,根据光源生成每个顶点的颜色数据,以及计算生成或移动纹理的坐标。
顶点着色器的输入通常为:
1) Attributes:来自顶点数组中每个顶点的数据
2) Uniforms:顶点着色器的常量数据,不能被着色器修改,一般用于对同一组顶点组成的3D物体中所有顶点都相同的变量,如光源的位置。
3) Samplers:一种特殊的Uniforms,顶点着色器使用的纹理,这个输入是可选的。
4) Shader program:这个是顶点着色器上要执行的处理的代码。
顶点着色器的输出称为可变变量(varying variables),在图元光栅化阶段,varying变量的值为每个生成的原片进行计算(这个计算过程称为插值),然后作为输入数据输入到片元着色器(fragment shader)。
片段着色器
计算每个像素的颜色和其它属性
片元着色器(Fragment Shader)也是一个可编程模块,我们可以通过OpenGL ES的对应api来对片元进行处理。
片元着色器可以丢弃片元(fragment)也可以为片元生成一个颜色值储存在内置变量gl_FragColor中。
光栅化阶段生成的颜色、深度、模板和屏幕坐标将成为下一个阶段——逐个片源处理(Per-Fragment operations)的输入。
1) Varying vriables:顶点着色器计算出来的Varying vriables经过光栅化模块对每个片进行插值计算之后的值
2) Uniforms:片着色器模块使用的常量数据
3) Samplers:一种特殊的uniforms类型,供片着色器使用的纹理
4) Shader program:实现片着色器里相关处理/操作的代码
着色器语言 GLSL
着色器语言(Shading Language)是一种高级的图形化编程语言,其源自应用广泛的C语言。
提供了更加丰富的针对于图像处理的原生类型,诸如向量、矩阵之类。
主要包含以下特性:
1.GLSL是一种面向过程的语言
2.GLSL的基本语法与C/C++基本相同
3.完美的支持向量和矩阵操作
4.通过限定符操作来管理输入输出类型的
5.GLSL提供了大量的内置函数来提供丰富的扩展功能
获得链接后的着色器对象的一般过程包括6个步骤:
1、创建一个顶点着色器对象和一个片段着色器对象。
2、将源代码连接到每个着色器对象。
3、编译着色器对象。
4、创建一个程序对象。
5、将编译后的着色器对象连接到程序对象。
6、链接程序对象。
坐标系
安卓屏幕坐标系
屏幕左上点为原点,x轴向右,y轴向下,跟随屏幕方向变化而变化。
世界坐标系
World coordinates 右手三维坐标系,屏幕中心点位原点x轴向右,y轴向上,z轴向屏幕外。
用来描述物体或者光源的位置,跟随屏幕方向变化而变化。
OpenGL设备标准化坐标系
Normalized device coordinates(NDC坐标系),采用的是左手三维坐标系,x、y、z各个坐标都在[-1,1]之间,z轴是指向屏幕里的,跟随屏幕方向变化而变化。
NDC坐标系 是OpenGL唯一理解的内部坐标系
坐标变换
坐标变换的目标,就是把一个3D空间中的对象最终投射到2D的屏幕上去。
坐标变换就是要解决在给定的观察视角下,3D世界的每个点最终对应到屏幕上的哪个像素上去。
1.首先,一个3D对象的模型被创建(使用某种建模软件)出来之后,是以本地坐标(local coordinates)来表达的,坐标原点(0, 0, 0)一般位于3D对象的中心。
2.3D对象的本地坐标经过一个model变换,就变换到成了世界坐标(world coordinates)。一般来说,model变换又包含三种可能的变换:缩放(scaling)、旋转(rotation)、平移(translation)。在计算机图形学中,一个变换通常使用矩阵乘法来计算完成,因此这里的model变换相当于给本地坐标左乘一个model矩阵,就得到了世界坐标。
3.在同一个世界坐标系内的各个3D对象共同组成了一个场景(scene),对于这个场景,我们可以从不同的角度去观察。当观察角度不同的时候,我们眼中看到的也不同。为了表达这个观察视角,我们会再建立一个相机坐标系从世界坐标系到相机坐标系的转换,我们称之为view变换。相机相当于眼睛,执行一个view变换,就相当于我们把眼睛调整到了我们想要的一个观察视角上。
4.对相机坐标执行一个投影变换(projection),就变换成了裁剪坐标(clip coordinates)。在裁剪坐标系(clip space)下,x、y、z各个坐标轴上会指定一个可见范围,坐标超过可见范围的顶点(vertex)就会被裁剪掉,这样,3D场景中超出指定范围的部分最终就不会被绘制。这个投影变换,是从3D变换到2D的关键步骤。
5.裁剪坐标(clip coordinates)经过一个特殊的透视划分过程,就变换成了NDC坐标(Normalized Device Coordinates)。由于这个过程在OpenGL ES中是自动进行的,我们不需要针对它来编程。NDC是真正的由OpenGL ES来定义的坐标。在NDC的定义中,x、y、z各个坐标都在[-1,1]之间。因此,NDC定义了一个边长为2的立方体,每个边从-1到1,NDC中的每个坐标都位于这个立方体内(落在立方体外的顶点在前一步已经被裁剪掉了)。值得注意的是,虽然NDC包含x、y、z三个坐标轴,但它主要表达了顶点在xOy平面内的位置,x和y坐标它们最终会对应到屏幕的像素位置上去。而z坐标只是为了表明深度关系,谁在前谁在后(前面的挡住后面的),因此z坐标只是相对大小有意义,z的绝对数值是多大并不具有现实的意义。
6.NDC坐标每个维度的取值范围都是[-1,1],但屏幕坐标并不是这样,而是大小不一。这样NDC坐标就需要一个变换,才能变换到屏幕坐标(screen coordinates),这个变换被称为视口变换(viewport transform)。在OpenGL ES中,这个变换也是自动完成的,但需要我们通过glViewport接口来指定绘制视口(屏幕)的大小。这里还需要注意的一点是,屏幕坐标(screen coordinates)与屏幕的像素(pixel)还不一样。屏幕坐标(screen coordinates)是屏幕上任意一个点的精确位置,简单来说就是可以是任意小数,但像素的位置只能是整数了。
7.这里的视口变换是从NDC坐标变换到屏幕坐标,还没有到最终的像素位置。再从屏幕坐标对应到像素位置,是后面的光栅化完成的。
三角形
坐标的绘制顺序定义了该形状的环绕方向。
默认情况下,在 OpenGL 中,沿逆时针方向绘制的面为正面,而另一面是背面。
面剔除是 OpenGL 环境的一个选项,允许渲染管道忽略(不计算或不绘制)形状的背面,从而节省时间和内存并缩短处理周期
纹理 (texture)
当把纹理按照特定的方式映射到物体表面上,能使物体看上去更加真实。
纹理只表示对象表面的彩色图案,它不能改变对象的几何形式。
纹理坐标系 (UV coordinates) 2D纹理坐标系也就是二维的UV坐标系,水平方向是U,垂直方向是V,范围[0,1] 。
在理解纹理映射时,可以将纹理看做应用在物体表面的像素颜色。
位移 缩放 旋转
OpenGL ES 中使用四个分量(x, y, z, w)来定义空间一个点.
齐次坐标就是将一个原本是n维的向量用一个 n+1 维向量来表示,多出一个分量w, w分量的用处是来创造3D视觉效果的, 根据w分量的大小对物体进行拉伸,最后将w=1的截面进行展示
三维坐标 表示的变换存在一种不便:需要乘法和加法两次计算才能完成转换。回想多项式乘法会发现,(a+b)^n 的展开式相当之长,这样,当变换的次数很多时,这样的计算就会很复杂且不直观
引入了齐次坐标的概念:对于某个三维坐标点(x, y, z),增加一维 w != 0,并对原三维坐标进行同样的缩放,形成新的四维坐标(wx, wy, wz, w),只需要用乘法就可以完成所有的任务了,即是所谓齐次坐标。
光照
物体会吸收某些颜色的光,没有被吸收的光会反射出来,就是物体的颜色。颜色可以数字化的将颜色由红色、绿色和蓝色三个分量组成。
在OpenGL ES 中,光照模型分为三个部分,分别为环境光、漫反射光、镜面光
直接给出颜色的方式来对3D场景中的物体进行着色渲染,很难使3D场景拥有较强的真实感。在片段着色器中计算光照会获得更好更真实的光照效果,但是会比较耗性能
环境光照:即使在黑暗的情况下,世界上通常也仍然有一些光亮(月亮、远处的光),所以物体几乎永远不会是完全黑暗的。为了模拟这个,我们会使用一个环境光照常量,它永远会给物体一些颜色。
漫反射光照:模拟光源对物体的方向性影响(Directional Impact)。物体的某一部分越是正对着光源,它就会越亮。
1.法向量:一个垂直于顶点表面的向量。
2.定向的光线:作为光源的位置与物体各个表面位置之间向量差的方向向量
两个向量之间的角度能够通过点乘计算出来,
点乘返回一个标量,可以用它计算光线对片段颜色的影响。不同片段朝向光源的方向的不同,这些片段被照亮的情况也不同。
镜面光照:模拟有光泽物体上面出现的亮点。镜面光照的颜色相比于物体的颜色会更倾向于光的颜色。
和漫反射光照一样,镜面光照也是依据光的方向向量和物体的法向量来决定的,也依赖于观察方向
镜面光照是基于光的反射特性。如果反射光直射入眼此时的光照强度就是最大的,而随着角度的偏离,光照强度渐渐变小。
Vulkan
Vulkan是一个跨平台的2D和3D绘图应用程序接口。基于Mantle构建,AMD将其Mantle API捐赠给科纳斯组织(Khronos),给予该组织开发底层API的基础。
Vulkan作为OpenGL的替代者,诞生的最重要理由是性能,以最小化驱动器中的 CPU 开销,并能让应用更直接地控制 GPU 操作。 Vulkan 还允许多个线程同时执行工作,以获得更好的并行化。
自 Android 7.0 开始,Google便在系统平台中添加了对Vulkan的API支持。
基于OpenGL的图形引擎,其渲染过程粗略可分为主机端资源加载,设备端数据交互与管线准备 及 每帧循环的渲染三个部分。
Vulkan 优势
1.效率上的提升::Vulkan在效率上的提升主要是它天然支持多线程
2.异步数据交互:使用OpenGL时,如果把数据交互放到另一个独立线程中完成,将会引起冲突,这个原因是上传资源和进行绘制时都需要改变上下文,而用Vulkan则没有这个问题
3.并行绘制:由于绘制时要改变上下文,OpenGL的并行绘制无疑也不可能了,Vulkan可以并行创建Command Buffer,Command Buffer 提交后就都是GPU驱动怎么执行的事了,执行的过程没必要也没可能用多线程加速。
4.复用Command Buffer: OpenGL 每帧绘制时,都需要在驱动层重新建一个Command Buffer 传递下去,而 Vulkan 是在应用层建好 Command Buffer,每帧绘制时Sub上去。
5.便于模块化: 虽然初看上去Vulkan比OpenGL复杂了许多,需要多写不少代码,但Vulkan更容易封装,各子模块之间互不影响,软件架构设计会轻松不少,开发维护起来更为方便。
Vulkan 编程流程
这个流程和OpenGL的使用流程很像,就是找到设备——创建上下文——创建命令队列——准备任务——发送执行。
Vulkan 命令缓冲区,诸如绘制和内存操作相关命令,在Vulkan中不是通过函数直接调用的。我们需要在命令缓冲区对象中记录我们期望的任何操作。这样做的优点是可以提前在多线程中完成所有绘制命令相关的装配工作,并在主线程循环结构中通知Vulkan执行具体的命令。
将渲染图像提交到屏幕的基本机制。这种机制称为交换链,并且需要在Vulkan上下文中被明确创建。从屏幕的角度观察,交换链本质上是一个图像队列。应用程序作为生产者会获取图像进行绘制,然后将其返还给交换链图像队列,等待屏幕消费。交换链的具体配置信息决定了应用程序提交绘制图像到队列的条件以及图像队列表现的效果,但交换链的通常使用目的是使绘制图像的最终呈现与屏幕的刷新频率同步。可以简单将交换链理解为一个队列,同步从生产者,即应用程序绘制图像,到消费者,屏幕刷新的Produce-Consume关系。
Vulkan中的Shader只支持spv的标准二进制格式,我们所写的glsl都必须通过官方的一个转换器转换为二进制格式。这样做就不需要GPU驱动去做语法解析等编译前端工作了。