一、OPENGL 概念与开发流程
1. 认识 GPU
OPENGL 的底层原理是采用 GPU 运行,来实现加速的目的。
那 GPU 是如何实现这个加速的目的呢,下面给大家进行讲解。
GPU 全称是GraphicProcessing Unit--图形处理器,其最大的作用就是进行各种绘制计算机图形所需的运算,包括顶点设置、光影、像素操作等。GPU实际上是一组图形函数的集合,而这些函数有硬件实现,只要用于 3D 游戏中物体移动时的坐标转换及光源处理。在很久以前,这些工作都是由 CPU 配合特定软件进行的,后来随着图像的复杂程度越来越高,单纯由 CPU 进行这项工作对于 CPU 的负荷远远
超出 CPU 的正常性能范围,这个时候就需要一个在图形处理过程中担当重任的角色,GPU 也就是从那时起正式诞生了。
从 GPU 的结构示意图上来看,一块标准的 GPU 主要包括通用计算单元、控制器和寄存器,从这些模块上来看,是不是跟和 CPU 的内部结构很像呢?
CPU 与 GPU 的结构对比
事实上两者的确在内部结构上有许多类似之处,但是由于 GPU 具有高并行结构(highly parallel structure),所以 GPU 在处理图形数据和复杂算法方面拥有比 CPU 更高的效率。上图展示了 GPU 和
CPU 在结构上的差异, CPU 大部分面积为控制器和寄存器,与之相比, GPU 拥有更多的 ALU(Arithmetic Logic Unit,逻辑运算单元)用于数据处理,而非数据高速缓存和流控制,这样的结构适合对密集
型数据进行并行处理。CPU 执行计算任务时,一个时刻只处理一个数据,不存在真正意义上的并行,而 GPU 具有多个处理器核,在一个时刻可以并行处理多个数据。
GPU 对图像的处理概览
GPU 采用流式并行计算模式,可对每个数据进行独立的并行计算,所谓“对数据进行独立计算”,即,流内任意元素的计算不依赖于其它同类型数据,例如,计算一个顶点的世界位置坐标,不依赖于其他顶点的位置。
而所谓“并行计算”是指“多个数据可以同时被使用,多个数据并行运算的时间和1个数据单独执行的时间是一样的”。
简而言之, GPU 的图形(处理)流水线完成如下的工作:
顶点处理:这阶段 GPU 读取描述 3D 图形外观的顶点数据并根据顶点数据确定 3D 图形的形状及位置关系,建立起 3D 图形的骨架。在现有的 GPU 中,这些工作由硬件实现的
Vertex Shader (顶点着色器)完成。
光栅化计算:显示器实际显示的图像是由像素组成的,我们需要将上面生成的图形上的点和线通过一定的算法转换到屏幕上相应的像素点。把一个矢量图形转换为一系列像素点的过程就称为光栅化。
例如,一条数学表示的斜线段,最终被转化成阶梯状的连续像素点。
纹理帖图:顶点单元生成的多边形只构成了 3D 物体的轮廓,而纹理映射(texture mapping)工作完成对多变形表面的帖图,通俗的说,就是将多边形的表面贴上相应的图片,从而生成“真实”的
图形。TMU (Texture mapping unit)即是用来完成此项工作。
像素处理:这阶段(在对每个像素进行光栅化处理期间)GPU 完成对像素的计算和处理,从而确定每个像素的最终属性。在支持 DX8 和 DX9 规格的 GPU 中,这些工作由硬件实现的
Pixel Shader(像素着色器)完成。
最终输出:由 ROP (光栅化引擎)最终完成像素的输出,1 帧渲染完毕后,被送到显存帧缓冲区。
GPU 的工作通俗的来说就是完成 3D 图形的生成,将图形映射到相应的像素点上,对每个像素进行计算确定最终颜色并完成输出。
不过需要注意的是,无论多牛的游戏家用显卡,光影都是 CPU 计算的,GPU 只有 2 个工作, 1 多边形生成。 2 为多边形上颜色。
实际应用中图像的生成流程大致如下:
首先从硬盘中读取模型,
CPU 分类后将多边形信息交给 GPU, GPU 再时时处理成屏幕上可见的多边形,但是没有纹理只有线框。CPU 计算出模型后,GPU 将模型数据放进显存,显卡同时也为模型贴材质,给模型上颜色。
CPU 相应从显存中获取多边形的信息。然后 CPU 计算光照后产生的影子的轮廓。等 CPU 计算出后,显卡的工作又有了,那就是为影子中填充深的颜色。周而复始,完成 CPU 与 GPU 之间的数据交换。
2. OPENGL 介绍
OPENGL 确切的说是一种规范 ,规定好某个函数名的功能 ,确定输入输出 ,而函数的内容由每个平台自己进行开发 ,而 OPENGL ES 就是针对嵌入式 ,移动端的 OPENGL ,而我们的芯片或者是手机游戏基本是使用了
OPENGL ES 2.0 或 3.0 等
而在 2015 年 ,新出了一种性能更高的 GPU 渲染 API ,“vulkan”,并且在 我们的 i.MX8 上就有驱动支持。假如你的设备开发时间比较新 ,有相关的 vulkan 支持驱动的话 ,并且是初学者 ,可以考虑直接学习 vulkan
但假如你在 windows 开发,依然会使用到下面提到的 glfw ,glm 这类工具和显示库,这两个非核心的库是通用的 。
在 GL3W,GLEW 后 ,更新的调用 OPENGL 的库是 glad 。也就是说 ,假如你在 windows 端进行一些简单的渲染操作 ,你只需要调用 glad 与 glfw 即可完成开发到显示的流程 。
而 GLFW 是windows 的 OPENGL 用于显示的窗口 ,也就是说 ,假如你想让你的3D 图形显示在windows 上 ,就可以用 GLFW 来实现这个功能 ,如果你想显示在芯片上的外接屏幕上就得使用另外的显示函数 ,
比如 EGL
对于嵌入式与 windows 端来说 ,内部的 GPU 渲染代码是可以通用的 ,只是显示出来的接口不一致 ,windows 端需要的是 glfw ,嵌入式是 openGV 等,只是 OPENGL ES 针对嵌入式端进行了缩小 ,
更适合嵌入式进行开发 。
而 GLM 是 OPENGL 的数学库 ,可以让你实现对图像的旋转 ,平移 ,比例变换甚至透视变换等矩阵操作 。
3. 为什么要用 OPENGL?
难道不能直接将数据从 CPU 跨到 GPU 处理?为什么要多此一举,出现 OPENGL 这个框架?
CPU 与 GPU 的数据处理
数据饥饿:从一块内存中将数据复制到另一块内存中,传递速度是非常慢的,内存复制数据时,CPU 和 GPU 都不能操作数据(避免引起错误)4. 了解客户端/服务端的概念
客户端/服务端
OPENGL 相当于把 CPU 当做了客户端,GPU 为服务端
5. OPENGL 的开发流程
OPENGL 是一个偏整体的开发框架 ,需要多个函数协同处理 ,不同于 OpenCV ,“你用到哪个函数 ,去了解这个函数即可 ”,OPENGL 需要你对开发的整体进行了解与编辑 ,所以即使写个三角形 ,也需要花费一定的时间 。
OPENGL 首先有多个概念 ,顶点 ,缓存 ,采样器 ,顶点着色器 ,片元着色器等等
这些先放在一边 ,先讲讲最简单的画三角形时 ,代码的流程 。① 顶点定义
GLfloat vVertices[] = { 0.0f, 0.5f, 0.0f, -0.5f, -0.5f, 0.0f, 0.5f, -0.5f, 0.0f }; |
前面有提到,我们需要将 CPU 上的数据先存入 OPENGL 的 BUFFER 中,再由 GPU 运行。
② 使用 VBO
2.1 获取 buffer ID
可知我们需要得到一个 OPENGL 的缓存,我们可以先通过以下函数让 OPENGL 反馈给我们一个对应缓存的唯一 ID,来使用这个缓存。
glGenBuffers ( 3, userData->vboIds ); |
glBindBuffer ( GL_ARRAY_BUFFER, userData->vboIds[0] ); |
2.3 将顶点数据传到 VBO
glBufferData ( GL_ARRAY_BUFFER, vtxStrides[0] * numVertices,vtxBuf[0], GL_STATIC_DRAW ); |
参数2:数组的大小
参数3:数组的地址
参数4:指定显卡要采用什么方式来管理我们的数据,GL_STATIC_DRAW 表示这些数据不会经常改变。
2.4 定义顶点着色器顶点着色器是负责将缓存中的数据传入 GPU
先放出一段代码(GLSL 语言编写,语法与 C 极为类似):
const char vShaderStr[] = "#version 300 es \n" "layout(location = 0) in vec4 a_position; \n" "layout(location = 1) in vec4 a_color; \n" "out vec4 v_color; \n" "void main() \n" "{ \n" " v_color = a_color; \n" " gl_Position = a_position; \n" "}"; |
第二行:指明了需要从上一个步骤中获取一个 vec3 类型的位置数据,数据
位置在输入数据的 0 偏移位置(类似于输入了一块数据,我们要的数据在头部)
第七行(main 函数内部):将顶点的位置直接赋值成输入的位置,gl_Position 是一个内置的变量,用来表示顶点位置的。
2.5 编译着色器
上面的 2.4 定义的着色器代码是字符串形式,需要对其进行编译后,OPENGL 才能直接使用这一部分的功能。
首先先创建一个着色器对象
// Create the shader object shader = glCreateShader ( type ); |
再将源码附在该对象上,并进行编译
// Load the shader source glShaderSource ( shader, 1, &shaderSrc, NULL );
// Compile the shader glCompileShader ( shader ); |
2.6 定义片元着色器
同样的,片元着色器主要决定每个像素的颜色
const char fShaderStr[] = "#version 300 es \n" "precision mediump float; \n" "in vec4 v_color; \n" "out vec4 o_fragColor; \n" "void main() \n" "{ \n" " o_fragColor = v_color; \n" "}" ; |
2.7 编译片元着色器
同样也需要进行代码附加和编译
③ 着色器程序对象
着色器程序对象与着色器对象是不同的,着色器程序对象主要是将顶点着色器与片元着色器编译后的附加在着色器程序对象上。
glAttachShader ( programObject, vertexShader ); glAttachShader ( programObject, fragmentShader ); |
使用着色器程序
glUseProgram ( userData->programObject ); |
使用完毕后,需要清理着色器程序对象
glDeleteShader ( vertexShader ); |
④ 指明顶点属性
我们需要先确定输入着色器的定点的属性,才能输入到顶点着色器中。
我们定义的顶点格式如下:
这里我们需要调用 API 函数:
// Load the vertex data glVertexAttribPointer ( 0, 3, GL_FLOAT, GL_FALSE, 0, vVertices ); |
参数 1:指明我们想要配置的顶点属性。类似编号的东西,之前我们设置了 location = 0,就是我们在这里用到的 0 .
参数 2:顶点属性的大小。我们用到的顶点是一个 vec3 的结果,所以大小为 3.
参数 3:数据的类型。我们使用的是 float 类型
参数 4:指明数据是否要被规范化。这里我们设置成 FALSE,不用规范化,因为我们已经规范化好了。
参数 5:表示属性的跨度。正如之前我们分析的,我们的跨度是 12,就是 3 倍的 float 类型。
参数 6:指明了数据的起始偏移量。这里转成了一个 void* 指针类型比较奇怪,我们以后再聊
⑤ 绘制三角形
当我们做完上面那么多步骤之后,我们只需要调用一行代码就可以了
glDrawArrays ( GL_TRIANGLES, 0, 3 ); |
综上,我们就完成了使用 OPENGL 进行三角形的渲染。
⑥ VAO
我们似乎已经完成了三角形的绘制,但我们还需要调用一下 VAO
这是 OPENGL 渲染必不缺少的一步
VAO 主要有以下两个功能:
- 对glEnableVertexAttribArray或者是DisableVertexAttribArray的调用
- 使用glVertexAttribPointer设置的顶点属性以及与顶点属性相关连的VBO
在我们调用 VAO 之前,因为没有使用我们设置的顶点,
所以无法显示三角形。
unsigned int VAO; glGenVertexArrays(1, &VAO); glBindVertexArray(VAO); |
⑦ 运行结果
不出所料,三角形如期显示出来
最后给大家展示一下 OPENGL ES3.0 的示范代码主文件的总体代码,
文件可从下面的链接下载,已进行保存分享:
链接:https://pan.baidu.com/s/1uNYxlk0qQBvBz49_9c6Ptg 提取码:dden |
6. 总结
这是一张顶点处理的流程图,我们所做的工作就是处理其中的一些阶段。今天我们处理的是顶点着色和片元着色,之后在光照和纹理的时候我们依旧是处理这些着色,可见这两个阶段是多么重要。
二、认识 API
1. OPENGL ES
109 个 API ,OPENGL ES 也可以绘制 buffer ,可以将这个备用 buffer 更新到 buffer 上 ,或者直接替换 手机屏幕的 buffer
① 7 个 API ,用于从手机获取绘制 buffer
② 9 个 API ,用于沟通 GPU 可编程模块 ,shader ,可通过 OPENGL ES 对 Shader 进行编译 ,绑定在 program 上
③ 26 个 API ,用于传入绘制信息
④ 27 个 API ,用于设置绘制状态
⑤ 3 个 API ,用于执行绘制命令
⑥ 33 个 API ,用于查询环境 ,清理状态
⑦ 4 个 API ,其他用处
EGL -> OPENGL ES -> GLES SL
手机像素的颜色范围为 ,0 ~ 255
人眼的范围为 0 ~ 10万
自然界中实际存在的颜色范围为 0 ~ 一个亿
在手游的引擎中 ,为了将图片绘制到手机上 ,OPENGL ES2.0 一共有 109 个 API
① 需要使用 EGL 作为接口 ,获取到手机屏幕的 handle ,并创建绘制所需要的环境
首先先获取手机屏幕这个棋盘 ,其次在 GPU 中绘制一个相同的棋盘 和一个本子,这个本子是用于记录这个棋盘的初始颜色等信息
② 需要使用 GL Shading Lauguage ,编写 GPU 可编程模块 ,用于在 GPU 上进行运算 。
确认 OPENGL ES 传入的这些点的位置 和颜色 ,对这些信息进行更新或者变换,然后根据 OPENGL ES 连成的点或者线 ,三角形的信息,通过插值方式在线上或者三角形上形成渐变色 ,也可不使用插值或
者渐变 ,可在 OPENGL ES 或者 GL SL 中进行设置 。最后通过 EGL 将新生成的棋盘与手机屏幕棋盘进行调换 。
当每秒超过 30FPS ,即可成像 。
更新像素的算法通过 GL SL 写在 Shader 里面 ,运行在 GPU 中 ,这个 Shader 又称为 vs ,针对所有传入的顶点进行运算,顶点颜色在 vs 中并没有发生变化
如果在 shader 中使用判断语句 ,会对 GPU 产生较大的负荷
③ 需要使用 OPENGL ES ,传入绘制的信息 ,设置绘制状态 ,并执行绘制命令 。
设置棋盘的初始信息等 ,且准备一些棋子 ,棋子带有颜色和位置等信息 ,OPENGL ES 把这些带有颜色的棋子放到对应的位置上 ,然后再把这些棋子按照一定的规律连成线或者三角形 ,
2. EGL
EGL 一共有 34 个 API ,
7 个 API 用于与手机关联并获取手机支持的配置信息
查询这款硬件支持多少种格式的绘制 buffer ,以及每种格式的 RGBA 是如何划分的 ,是否支持 depth ,stencil ,如果没有则不需要传递这个参数
14 个 API 用于根据需要生成手机支持的 surface 和 context ,并对surface 和 context 进行关联 ,surface 包含了 绘制 buffer 和 depth buffer 等。context 包含了状态值 ,一个 context 同时也只能被一个进
程启用
5 个 API ,用于指定使用哪个版本的 OPENGL ES ,并与 OPENGL ES 建立关联
因为 EGL 需要与 OPENGL ES 对接 ,所以需要指定一个版本来进行关联
6 个 API ,用于操作 EGL 上纹理 ,以及与多线程相关的高级功能
2 个 API ,其他用处
3. EGL API 详解
3.1 EGLint eglGetError(void)
功能: 用于返回当前 thread ,如果 EGL 的 API 出错的话 ,最近一个错误所对应的错误代码
输入: 空(在 GPU 中 ,如果使用到了这个函数 ,会去读取寄存器中的函数)
输出: 错误代码 ( 15 种错误代码)
3.2 EGLDisplay eglGetDisplay(EGLNativeDisplayType display_id);
功能: 从 EGL 运行的操作系统中获取一个 Display
输入: 从操作系统中 ,得知的 Display ID
输出: 用于显示图片绘制的 Display
3.3 EGLBoolean eglInitialize(EGLDisplay dpy, EGLint *major, EGLint *minor);
功能: 针对某 display 初始化一个某版本的 EGL
输入: 使用 display 的 handle 特质某个 display ,major 和 minor 共同用于指定 EGL 的版本 ,大版本 ,小版本 ,1.4,1,4
输出: EGL 初始化成功或失败
3.4 EGLBoolean eglGetConfigs(EGLDisplay dpy,EGLConfig *configs,EGLint config_size, EGLint *num_config);
功能: 获取某 display 支持的配置信息
输入: Display 的 handle ,一个用于保存配置信息的指针(假如没有存入指针 ,而是 null 就不会有配置信息返回) ,指针中存放的配置信息的数量 ,某 display 支持的配置信息数量(依然会返回支持
的个数)
两个输入参数 ,两个输出参数 ,第一个和第三个为输入参数,第二个和第四个为输出参数
输出: 配置信息获取成功或失败
3.5 EGLBoolean eglChooseConfig(EGLDisplay dpy, const EGLint *attrib_list ,EGLConfig *configs,EGLint config_size,EGLint *num_config);
功能: 获取与需求匹配 ,且某 display 支持的配置信息
输入: Display 的 handle ,用于匹配使用的需求信息 ,一个用于保存匹配的配置信息的指针 ,指针中存放的配置信息的数量 ,匹配配置信息的数量
三个输入参数,第一第二第四为输入 ,第三第五为输出参数
输出: 匹配的配置信息获取成功或者失败
3.6 EGLSurface eglCreateWindowSurface(EGLDisplay dpy ,EGLConfig config ,EGLNativeWindowType win ,const EGLint *attrib_list);
功能: 创建一个可以显示在屏幕上的 rendering surface ,surface 主要分为三种 ,这个只是其中一种 pixmap pixel 等等 ,这两种不可显示
输入: Display handle ,用于创建 surface 的配置信息 ,窗口信息的 handle ,额外的需求信息
输出: 创建的 rendering surface 的 handle
3.7 EGLBoolean eglBindAPI(EGLenum api);
功能: 设置当前 thread 的绘制 API
输入: 支持的绘制 API ,比如 EGL OPENGL ,OPENGL ES ,EGL OPENVG
从这里开始 ,OPENGL 与 OPENGL ES 将开始分离 ,OPENGL ES 主要用于移动端而不是 PC 端 ,所以这一般传入 OPENGL ES 参数
输出: API 设置成功或者失败
3.8 EGLContext eglCreateContext(EGLDisplay dpy,EGLConfig config,EGLContext share_context,const EGLint *attrib_list);
【 针对 OPENGL ES 】
功能: 针对当前的绘制 API 创建一个 rendering context
输入: Display 的 handle ,用于创建 context 的配置信息 ,指定一个 context 使得创建的 context 与其 share ,额外的需求信息
输出: 创建的 rendering context 的 handle
3.9 EGLBoolean eglMakeCurrent(EGLDisplay dpy, EGLSurface draw ,EGLSurface read ,EGLContext ctx);
功能: 将指定 context 绑定到当前 thread 以及读写的 surface 上
输入: Display 的 handle ,用于写入的 surface ,用于读取的 surface ,指定的 context
输出: 创建的 rendering context 的 handle
3.10 EGLBoolean eglSwapBuffers(EGLDisplay dpy, EGLSurface surface);
功能: 将 surface 中的 color buffer 显示到屏幕上
(depth buffer 和 color buffer ,frame buffer ,只有 color buffer 可以看到)
输入: Display 的 handle ,将会被展示的 surface
输出: 显示成功或者失败
3.11 EGLBoolean eglTerminate(EGLDisplay dpy);
功能: 将某 display 对应的 EGL 相关的资源释放
输入: 使用 Display 的 handle 特指某个 Display
输出: 释放 EGL 相关资源成功或者失败
三、 预告
经过这两篇博文,大家配置好了 OPENGL 的环境,并对最简单的图像渲染开发以及部分 API 有所了解。
接下来的博文,将对 OPENGL 的纹理绘制进行讲解。
敬请期待!
评论