Android显示系统(02)- OpenGL ES - 概述

一、前言:

为了介绍清楚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变种:

优点:

  1. 节省资源:
    • OpenGL ES针对嵌入式设备的特点进行了优化,以节省系统资源(如内存和处理器资源),能够在资源受限的设备上更高效地运行。
  2. 更好的移植性:
    • 由于OpenGL ES是为多种嵌入式平台设计的,因此具有更好的移植性,可以在不同类型的嵌入式系统上运行。
  3. 更好的性能:
    • 由于OpenGL ES专门针对嵌入式设备进行了优化,可以提供更好的性能和效率,使得图形渲染在移动设备和其他嵌入式系统上更加流畅。
  4. 低功耗:
    • OpenGL ES的设计考虑了嵌入式设备的功耗限制,因此在保持较低功耗的同时提供良好的图形渲染效果。

1、在显示系统中位置:

https://blog.csdn.net/Ziwubiancheng/article/details/144065680

我在这一片文章中已经说明白了。

2、GLSurfaceView:

比如我们要使用GLSurfaceView显示一张图片,如下所示:

  1. GLSurfaceView是SurfaceView的子类,而SurfaceView里面又拥有Surface;
  2. SurfaceView的主要作用就是将我们需要渲染的内容展示到屏幕上;里面会创建一个Surface;
  3. EGL主要作用就是加载OpenGL ES库;里面的EGLSurface会连接到上一步的具体Surface;
  4. EGL将内容通过GPU渲染出来,然后转给EGLSurface的Surface;
  5. 最终,将多个Surface通过BufferQueue传给SurfaceFlinger,通过SF进行合成;
  6. 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_TRUEEGL_FALSE,所以,开发人员要主动调用eglGetError去获取具体原因:

函数原型

cpp 复制代码
EGLint eglGetError(void);

返回值

cpp 复制代码
EGLint

:表示最近发生的 EGL 错误代码,是一个整数值。可能的错误代码有:

  • EGL_SUCCESS:没有错误发生。
  • 其他错误代码,如 EGL_NOT_INITIALIZEDEGL_BAD_ALLOCEGL_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_sizeconfigs 数组的大小,即最大可以存储的配置数量。
  • 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_SIZEEGL_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_sizeconfigs 数组的大小,即最大可以存储的配置数量。
  • 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选择一个ContextSurface绑定起来,从而使得后续的 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,后续文章再结合实例让大家更熟悉这些知识点。

相关推荐
画个太阳作晴天1 小时前
Android10 设备死机的问题分析和解决
android·framework·anr
SHUIPING_YANG4 小时前
Typora设置自动上传图片到图床
android
Ai 编码助手4 小时前
php多进程那点事,用 swoole 如何去解决呢
android·php·swoole
weixin_690654746 小时前
龙迅#LT8612UX适用于HDMI 转 HDMI&VGA应用领域,分辨率高达4K60HZ,内置程序,方便调试!
音视频·信号处理
alexhilton6 小时前
群星闪耀的大前端开发
android·kotlin·android jetpack
未来之窗软件服务7 小时前
android 开机启动—信发系统开发—未来之窗行业应用跨平台架构
android·智慧大屏幕·信发系统
不是AI7 小时前
【安卓开发】【Android Studio】项目构建(Build)时报错:Integer Overflow
android·ide·android studio
倾城半生-花落成殇7 小时前
Android Studio 右侧工具栏 Gradle 不显示 Task 列表
android·ide·gradle·android studio
李艺为7 小时前
Android Studio 历史版本下载
android·ide·android studio
图扑软件7 小时前
视频融合×室内定位×数字孪生
javascript·人工智能·音视频·智慧城市·可视化·室内定位·视频融合