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,后续文章再结合实例让大家更熟悉这些知识点。

相关推荐
百锦再19 分钟前
Android Studio开发 SharedPreferences 详解
android·ide·android studio
青春给了狗31 分钟前
Android 14 修改侧滑手势动画效果
android
CYRUS STUDIO38 分钟前
Android APP 热修复原理
android·app·frida·hotfix·热修复
火柴就是我2 小时前
首次使用Android Studio时,http proxy,gradle问题解决
android
limingade2 小时前
手机打电话时电脑坐席同时收听对方说话并插入IVR预录声音片段
android·智能手机·电脑·蓝牙电话·电脑打电话
浩浩测试一下2 小时前
计算机网络中的DHCP是什么呀? 详情解答
android·网络·计算机网络·安全·web安全·网络安全·安全架构
青春给了狗4 小时前
Android 14 系统统一修改app启动时图标大小和圆角
android
pengyu4 小时前
【Flutter 状态管理 - 柒】 | InheritedWidget:藏在组件树里的"魔法"✨
android·flutter·dart
居然是阿宋6 小时前
Kotlin高阶函数 vs Lambda表达式:关键区别与协作关系
android·开发语言·kotlin
凉、介6 小时前
PCI 总线学习笔记(五)
android·linux·笔记·学习·pcie·pci