一、前言:
为了介绍清楚Android显示系统,得从实战的目的花几篇文章介绍下OpenGL ES,对OpenGL ES熟悉的可以直接跳过这几章。OpenGL ES是OpenGL的精简版,专门用于嵌入式平台,因为嵌入式平台硬件资源有限,做了定向的优化。而OpenGL就是定义了一套渲染API标准(注意不是库),方便不同操作系统,不同硬件对渲染进行统一。目的就是一个:将3D世界转换到2D屏幕显示出来,比如以前小学课文中有一篇叫做《画杨桃》,3D空间中的杨桃就像我们设计的一个模型,但是,从不同视角去看,在2D屏幕上形成的一张画是不一样的:
二、OpenGL渲染管线:
OpenGL 1.0之前是固定管线,从2.0版本开始就成为可编程(写Shader程序)了,更灵活了。从3D世界转换为2D数据的过程,也就是渲染的过程,在OpenGL中前一个步骤输出结果又可以作为下一个步骤的输入,像工厂流水线一样,我们叫做渲染管线
,如下图所示:
这是对GPU渲染管线各个阶段做了抽象,其中蓝色的三个部分我们可以自己写一段程序控制(着色器程序,英文:Shader)。由于现代GPU基本上有成百上千的核,因此,我们这种Shader
就可以并行运行在GPU上,充分利用GPU资源。
同时,我们也可以看出,渲染管线做的事情可以粗略总结为:将三维模型转换为二维模型(几何阶段),然后,给模型中每个像素填充颜色(光栅化阶段),其中几何阶段就是上面渲染管线的前三步,光栅化阶段就是后三步,分解看看:
1、模型数据(Vertex Data)
就是将三维顶点数据二维化,顶点数据包括:顶点的(x, y, z)坐标,顶点的RGB颜色,还有一些顶点的法线、uv、切线等等。针对坐标系,以及数据生成的过程,推荐阅读:
。总结起来就是,3D到2D的过程要经过:物体坐标系→世界坐标系→摄像机坐标系→屏幕坐标系变换。最终就在屏幕坐标系形成一幅画。
就像一个杨桃放到那里,我们可能画一张之后,我们人可能走动再画另外一个视角,或者走进走远再画,还有可能在旁边放几片叶子再画。但是,无论你怎么走动,杨桃本身还在那里,不增不减。也就是物体坐标系
当中的不会变换。
三维变换主要有:模型变换、视图变换、投影变换三种。下面我以一个最简单的模型--三角形为例子说明下:
a)模型变换:
就是对模型进行:平移,缩放,旋转。
比如,刚开始三角形在世界坐标系
中是这样:
我们进行平移下:
旋转缩放大家自己脑补下。
b)视图变换:
我们希望这个模型不再是以世界坐标系为中心了,而是以摄像机(眼睛)为中心的坐标系去观察这个世界。所以,在摄像机的地方建立一个右手坐标系:
然后,以摄像机为中心的坐标系,进行视图变换:
这样,就拿到了摄像机坐标系
当中的坐标。
c)投影变换:
将我们前面摄像机角度看到的像投影到标准屏幕坐标系
当中:
蓝色的就是屏幕啦!实际投影变换其实还要复杂一些,需要压缩成模盒,后面遇到再解释,先有点印象。
2、顶点着色器(Vertex Shader)
前面步骤形成的图片是线性的,并不能显示在屏幕上,要想显示,必须像素化。我们现在以三角形为例子在说明,实际OpenGL中任何图形都可以使用图元(点、线、三角形)来拼出来,比如:
上面就将一个人像模型用很多三角形表示了,复杂的地方(或者想更细腻)我们可以使用更多三角形表示。因此,我们为了更直观介绍,只介绍三角形这个最简单的模型,其他复杂模型就得继续深造。
所以,目前我们需要提取这个三角形的顶点数据。这些顶点数据会通过顶点数组对象(VAO)告诉OpenGL,通过索引缓冲对象(EBO)告诉OpenGL,这些顶点数据是如何绘制构成图形的。像这样:
cpp
float vertices[] = {
-0.5f, -0.5f, 0.0f, // 左下
0.5f, -0.5f, 0.0f, // 右下
0.0f, 0.5f, 0.0f // 正上
};
3、图元装配(Shape Assembly)
前面都是针对顶点的一些变换,变换完成之后,就需要将这些顶点有序的安装连起来,就是图元装配;
比如下图:
4、几何着色器(Geometry Shader)
像上面流水管线图中画的一样,将三角形加一条线就变成两个三角形了,比如,处理一些曲面的时候会用到。
5、裁剪剔除:
其实上图渲染管线的图(官网的图)并没有提到这一步,做裁剪剔除可以提高我们渲染效率,就像我们打CS游戏一样,游戏地图肯定是提前建模好的,但是我们视图只能看到一部分,看不到的部分我们不会傻傻地跑去渲染,你和敌人面对面交火,敌人的后背你肯定也不需要去渲染了。
那么都是基本图元,我们怎么知道要剔除谁呢?
- 假如我们规定逆时针是正面的,那么顺时针的三角形我们就不显示;
- 视口以外的也不显示;
这样,最终我们看到的就是:
6、光栅化(Rasterization)
前面都是对顶点着色器进行处理,从光栅化开始就是对片元着色器进行处理了。
我们知道屏幕都是由很多像素拼成的,因此,我们要渲染图片,就得对每一个像素进行染色,光栅化就是将我们上面三角形三个顶点围起来的像素计算出来:
但是,请注意,光栅化并没有真正染色,只是确定范围。
7、片元着色(Fragment Shader)
这一步就是为每一个像素染色,片元着色器包含3D场景的数据,比如(光照,阴影,光的颜色等等),这些数据可以被用来计算最终的像素的颜色。
8、测试和混合(Tests and Blending)
由OpenGL自动完成,用户只要调用:
cpp
glEnable(GL_DEPTH_TEST);
使用相应的深度来检查片段是在其他对象的前面还是后面------丢弃未看到/使用的片段。在此步骤中,alpha(不透明度)值也会与场景中的对象混合。
像上图一样,将一个蓝色三角形和一个红色三角形进行混合,明显的蓝色三角形在前面(距离人眼更近)。
三、OpenGL ES:
Android当中使用了OpenGL ES,OpenGL ES(OpenGL for Embedded Systems)是专为嵌入式系统设计的OpenGL变种:
优点:
- 节省资源:
- OpenGL ES针对嵌入式设备的特点进行了优化,以节省系统资源(如内存和处理器资源),能够在资源受限的设备上更高效地运行。
- 更好的移植性:
- 由于OpenGL ES是为多种嵌入式平台设计的,因此具有更好的移植性,可以在不同类型的嵌入式系统上运行。
- 更好的性能:
- 由于OpenGL ES专门针对嵌入式设备进行了优化,可以提供更好的性能和效率,使得图形渲染在移动设备和其他嵌入式系统上更加流畅。
- 低功耗:
- OpenGL ES的设计考虑了嵌入式设备的功耗限制,因此在保持较低功耗的同时提供良好的图形渲染效果。
1、在显示系统中位置:
https://blog.csdn.net/Ziwubiancheng/article/details/144065680
我在这一片文章中已经说明白了。
2、GLSurfaceView:
比如我们要使用GLSurfaceView
显示一张图片,如下所示:
- GLSurfaceView是SurfaceView的子类,而SurfaceView里面又拥有Surface;
- SurfaceView的主要作用就是将我们需要渲染的内容展示到屏幕上;里面会创建一个Surface;
- EGL主要作用就是加载OpenGL ES库;里面的EGLSurface会连接到上一步的具体Surface;
- EGL将内容通过GPU渲染出来,然后转给EGLSurface的Surface;
- 最终,将多个Surface通过BufferQueue传给SurfaceFlinger,通过SF进行合成;
- SF整合之后可以通过GPU(或者HWC)显示出来;
3、EGL:
1)EGL和OpenGL ES关系:
上面提到了EGL,它在应用层和Native层都可以被调用,它属于是将OpenGL ES和本地窗口(Android就是Surface)联系起来。这样我们就可以通过EGL来获取Surface,Surface里面又有Buffer,我们就可以通过OpenGL ES往这个Buffer中绘制数据了。如下图所示:
简而言之,EGL保证了OpenGL ES的平台独立性,具体功能如下:
- 加载OpenGL ES库;
- 创建Surface;
- 创建图形环境(Graphics Context),就是OpenGL ES这个渲染管线的上下文环境(状态机);
- 提供了对显示设备访问的API;
- 提供了对渲染配置的管理;
2)egl.cfg:
到底采用软件还是硬件进行渲染(优先选择硬件),在系统启动之后才能确定,系统启动之后,读取system/lib/egl/egl.cfg
这个文件,如果这个文件存在就采用硬件渲染,如果不存在就加载软件库libagl进行渲染。
3) EGL API介绍:
Android中我们经常要使用EGL的API,我们对他们功能进行介绍:
eglGetDisplay :
我们前面说过Android中通过EGL来管理OpenGL ES和Surface,将他们俩联系起来,但是Surface肯定属于某个显示器,因此,我们需要先获取显示器对象EGLDisplay,作为后续大多数操作的句柄,另外,这个接口中做了一件特别重要的事情就是加载OpenGL ES库
;
函数原型:
cpp
EGLDisplay eglGetDisplay(EGLNativeDisplayType display_id);
参数:
display_id
:要获取的显示器的标识符。在 Android 中,通常可以传入EGL_DEFAULT_DISPLAY
表示默认显示器。
返回值:
EGLDisplay
:表示显示器的句柄,是一个指向 EGLDisplay 结构体的指针。
功能:
eglGetDisplay
用于获取一个显示器的句柄,这个显示器句柄是后续 EGL 操作的关键。显示器句柄表示一个可用的显示设备,它是 EGL 与底层图形系统进行通信的接口。
工作原理:
eglGetDisplay
在调用时传入显示器的标识符,通常是EGL_DEFAULT_DISPLAY
,表示获取默认显示器。通过这个函数获取的EGLDisplay
句柄可以在后续的 EGL 初始化过程中使用。
eglGetError:
用于获取最近发生的 EGL 错误代码,因为大多数EGL接口都是直接返回EGL_TRUE
和EGL_FALSE
,所以,开发人员要主动调用eglGetError去获取具体原因:
函数原型:
cpp
EGLint eglGetError(void);
返回值:
cpp
EGLint
:表示最近发生的 EGL 错误代码,是一个整数值。可能的错误代码有:
EGL_SUCCESS
:没有错误发生。- 其他错误代码,如
EGL_NOT_INITIALIZED
、EGL_BAD_ALLOC
、EGL_BAD_CONFIG
等,表示不同类型的错误。
示例:
cpp
EGLint error = eglGetError();
if (error != EGL_SUCCESS) {
// 处理发生的 EGL 错误
switch (error) {
case EGL_NOT_INITIALIZED:
// 处理未初始化错误
break;
case EGL_BAD_ALLOC:
// 处理内存分配错误
break;
// 其他错误处理
default:
// 默认处理
break;
}
}
eglInitialize:
用于初始化 EGL 系统,并获取与指定显示器相关的 EGL 版本信息。
函数原型:
cpp
EGLBoolean eglInitialize(EGLDisplay display, EGLint *major, EGLint *minor);
参数:
display
:要初始化的显示器的句柄。major
:指向一个整数变量的指针,用于存储 EGL 主版本号。minor
:指向一个整数变量的指针,用于存储 EGL 次版本号。
返回值:
EGLBoolean
:表示初始化是否成功。EGL_TRUE
表示成功,EGL_FALSE
表示失败。
eglGetConfigs:
用于获取最佳的Surface,我们可以手动指定,也可以表明我们的要求(如颜色格式、深度缓冲区、模板缓冲区等),让EGL推荐一个最佳的;
函数原型:
cpp
EGLBoolean eglGetConfigs(EGLDisplay display, EGLConfig *configs, EGLint config_size, EGLint *num_config);
参数:
display
:要查询的显示器的句柄。configs
:用于存储配置列表的数组。config_size
:configs
数组的大小,即最大可以存储的配置数量。num_config
:指向一个整数变量的指针,用于存储实际获取到的配置数量。
返回值:
EGLBoolean
:表示获取配置列表是否成功。EGL_TRUE
表示成功,EGL_FALSE
表示失败。
eglGetConfigAttrib:
用于查询某个具体Surface的某个具体属性。如颜色格式、深度缓冲区大小等,在进行图形渲染前,了解配置的属性可以帮助开发人员优化渲染流程,确保图形渲染效果的质量和性能。
函数原型:
cpp
EGLBoolean eglGetConfigAttrib(EGLDisplay display, EGLConfig config, EGLint attribute, EGLint *value);
参数:
display
:显示器的句柄。config
:要查询的配置句柄。attribute
:要查询的属性类型,如EGL_BUFFER_SIZE
、EGL_RED_SIZE
等。value
:指向一个整数变量的指针,用于存储查询到的属性值。
返回值:
EGLBoolean
:表示查询属性是否成功。EGL_TRUE
表示成功,EGL_FALSE
表示失败。
eglChooseConfig:
前面都是逐个查新配置EGLConfig,我们也可以填写一个期望的配置清单,让EGL匹配一个最佳配置:
函数原型:
cpp
EGLBoolean eglChooseConfig(EGLDisplay display, const EGLint *attrib_list, EGLConfig *configs, EGLint config_size, EGLint *num_config);
参数:
display
:显示器句柄。attrib_list
:属性列表,用于指定选择配置的条件,以EGL_NONE
结束。configs
:用于存储选择到的配置的数组。config_size
:configs
数组的大小,即最大可以存储的配置数量。num_config
:指向一个整数变量的指针,用于存储实际选择到的配置数量。
返回值:
EGLBoolean
:表示选择配置是否成功。EGL_TRUE
表示成功,EGL_FALSE
表示失败。
功能:
eglChooseConfig
用于根据属性列表选择与之匹配的配置。属性列表中包含了选择配置的条件,如颜色格式、深度缓冲区大小等。函数将从显示器支持的配置列表中选择最符合条件的配置。
示例:
cpp
EGLDisplay display;
EGLConfig configs[10]; // 假设最多选择 10 个配置
EGLint num_config;
EGLint attrib_list[] = {
EGL_RED_SIZE, 8,
EGL_GREEN_SIZE, 8,
EGL_BLUE_SIZE, 8,
EGL_NONE
};
if (eglChooseConfig(display, attrib_list, configs, 10, &num_config) == EGL_FALSE) {
// 选择配置失败的处理
}
在调用 eglChooseConfig
时,传入显示器句柄、属性列表、用于存储选择到的配置的数组、数组大小以及用于存储实际选择到的配置数量的变量指针。函数成功返回后,configs
数组中将存储选择到的配置,num_config
中存储实际选择到的配置数量。
eglCreateWindowSurface:
前面已经选择了最佳配置EGLConfig
,下面就开始创建Surface
了;eglCreateWindowSurface
用于创建一个Surface
,该Surface
可以用于在窗口或屏幕上进行 OpenGL ES 渲染。Surface
通常与特定的窗口系统相关联,用于将图形内容绘制到屏幕上。
函数原型:
cpp
EGLSurface eglCreateWindowSurface(EGLDisplay display, EGLConfig config, EGLNativeWindowType win, const EGLint *attrib_list);
参数:
display
:显示器句柄。config
:用于创建Surface
表面的配置。win
:与Surface
关联的本地窗口类型,通常是窗口句柄或者原生窗口类型。attrib_list
:属性列表,用于指定Surface
的属性,通常以EGL_NONE
结束。
返回值:
EGLSurface
:表示创建的Surface
句柄(其实里面含有原生的surface),如果创建失败,会返回EGL_NO_SURFACE
。
eglCreatePbufferSurface:
前面创建的Surface
直接显示在屏幕上,我们这个接口创建的Surface
是在离屏渲染区
,离屏渲染(Off-screen Rendering)是指在不直接显示在屏幕上的情况下进行图形渲染的过程。通常,图形渲染会将结果直接绘制到屏幕上,但在某些情况下,我们希望进行渲染的结果不直接显示在屏幕上,而是用于其他用途,比如生成纹理、进行后期处理、计算光照等。这时就可以使用离屏渲染技术。
函数原型:
cpp
EGLSurface eglCreatePbufferSurface(EGLDisplay display, EGLConfig config, const EGLint *attrib_list);
参数:
display
:EGL 显示器句柄。config
:用于创建 Pbuffer surface 的配置。attrib_list
:可选的属性列表,用于指定 Pbuffer surface 的属性,通常以EGL_NONE
结束。
返回值:
EGLSurface
:表示创建的 Pbuffer surface 句柄,如果创建失败,会返回EGL_NO_SURFACE
。
eglCreateContext:
用于创建一个 OpenGL 上下文(Context)。
函数原型:
cpp
EGLContext eglCreateContext(EGLDisplay display, EGLConfig config, EGLContext share_context, const EGLint *attrib_list);
参数:
display
:EGL 显示器句柄。config
:用于创建上下文的配置。share_context
:可选的共享上下文,如果需要多个上下文共享资源,则传入共享的上下文。attrib_list
:属性列表,用于指定上下文的属性,通常以EGL_NONE
结束。
返回值:
EGLContext
:表示创建的 OpenGL 上下文句柄,如果创建失败,会返回EGL_NO_CONTEXT
。
eglMakeCurrent:
我们每个进程可以创建多个Context
,我们调用这个API选择一个Context
和Surface
绑定起来,从而使得后续的 OpenGL 渲染操作可以在这个上下文和绘图表面上进行。
函数原型:
cpp
EGLBoolean eglMakeCurrent(EGLDisplay display, EGLSurface draw, EGLSurface read, EGLContext context);
参数:
display
:EGL 显示器句柄。draw
:要绑定的绘图表面(可以是窗口表面、Pbuffer 表面等)。read
:要绑定的读取表面(通常与绘图表面相同)。context
:要绑定的 OpenGL 上下文。
返回值:
EGLBoolean
:表示操作是否成功,成功时返回EGL_TRUE
,失败时返回EGL_FALSE
。
注意:参数2和参数3我们通常填同一个,不知道为啥这么设计。
eglSwapBuffers:
用于交换前后缓冲,将当前绘图表面(Surface)上的渲染结果显示到屏幕上。在使用 OpenGL 进行图形渲染时,通常会在完成一帧的渲染之后调用 eglSwapBuffers 函数来触发显示系统将渲染结果呈现在屏幕上。
eglSwapBuffers:
用于交换前后缓冲,将当前绘图表面(Surface)上的渲染结果显示到屏幕上。在使用 OpenGL 进行图形渲染时,通常会在完成一帧的渲染之后调用 eglSwapBuffers 函数来触发显示系统将渲染结果呈现在屏幕上。函数原型:
cpp
EGLBoolean eglSwapBuffers(EGLDisplay display, EGLSurface surface);
参数:
display
:EGL 显示器句柄。surface
:要交换缓冲的绘图表面。
返回值:
EGLBoolean
:表示操作是否成功,成功时返回EGL_TRUE
,失败时返回EGL_FALSE
。
功能:
eglSwapBuffers
函数用于交换前后缓冲,将当前绘图表面上的渲染结果显示到屏幕上。
工作原理:
- 当调用
eglSwapBuffers
函数时,会通知显示系统将当前绘图表面的渲染结果显示到屏幕上。 - 在双缓冲模式下,交换缓冲会将当前绘图表面的后备缓冲(已经渲染完成的缓冲)呈现在屏幕上,同时将前缓冲切换为后备缓冲用于下一帧的渲染。
- 这个函数通常在完成一帧的渲染后被调用,触发显示系统将渲染结果显示出来
四、小结:
本文主要介绍了渲染管线,并且介绍了Android平台的OpenGL ES,以及非常重要的EGL,后续文章再结合实例让大家更熟悉这些知识点。