Skip to content

Latest commit

 

History

History
681 lines (389 loc) · 40.8 KB

1.OpenGL简介.md

File metadata and controls

681 lines (389 loc) · 40.8 KB

1.OpenGL简介

OpenGL(Open Graphics Library开放图形库)是用于渲染2D、3D矢量图形的跨语言、跨平台的应用程序编程接口

然而,OpenGL本身并不是一个API,它仅仅是一个由Khronos组织制定并维护的规范(Specification)

OpenGL规范严格规定了每个函数该如何执行,以及它们的输出值。至于内部具体每个函数是如何实现(Implement)的,将由编写OpenGL库的人自行决定。

OpenGL官网

OpenGL一般用于在图形工作站,PC端使用,由于性能各方面原因,在移动端使用OpenGL基本带不动。
为此,Khronos公司就为OpenGL提供了一个子集,OpenGL ES(OpenGL for Embedded System)。

后面文章所有的源码都在Github上 且本文章使用的OpenGL版本为OpenGL 3.3(对应OpenGL ES 版本为OpenGL ES 3.3)。

OpenGL ES

OpenGL ES 官网

OpenGL ES是免费的跨平台的功能完善的2D/3D图形库接口的API, 是OpenGL的一个子集,主要针对手机、Pad和游戏主机等嵌入式设备而设计。

移动端使用到的基本上都是OpenGL ES,当然Android开发下还专门为OpenGL提供了android.opengl包,并且提供了GlSurfaceView, GLU, GlUtils等工具类。

OpenGL ES由OpenGL裁剪而来,因此有必要了解一下两者版本的对应关系:

image

OpenGL的作用

在手机或电脑上有两大元件,一个是CPU,一个是GPU。
显示图形界面也有两种方式,一个是使用CPU渲染,一个是使用GPU渲染。
但是目前为止最高效的方法就是有效的使用图形处理单元(GPU),图像的处理和渲染就是在将要渲染到窗口上的像素上做很多的浮点运算,而GPU可以并行的做浮点运算,所以用GPU来分担CPU的部分,可以提高效率,可以说GPU渲染其实是一种硬件加速。

  • 图片处理:比如图片色调转换、美颜等。
  • 摄像头预览效果处理。比如美颜相机、恶搞相机等。
  • 视频处理。视频播放的时候增加一些滤镜效果。
  • 3D游戏。比如神庙逃亡、都市赛车等。

Android中的OpenGL ES

Android中OpenGL ES的版本支持如下:

  • OpenGL ES 1.0 和 1.1 - 此 API 规范受 Android 1.0 及更高版本的支持。
  • OpenGL ES 2.0 - 此 API 规范受 Android 2.2(API 级别 8)及更高版本的支持。
  • OpenGL ES 3.0 - 此 API 规范受 Android 4.3(API 级别 18)及更高版本的支持。
  • OpenGL ES 3.1 - 此 API 规范受 Android 5.0(API 级别 21)及更高版本的支持。

OpenGL ES是一套绘制3D图形的API,OpenGL ES是和平台无关的一套API,主要是为了rendering 2D和3D图形等。
OpenGL就是一组函数名,并不能直接用,需要底层硬件GPU的支持。

OpenGL ES的实现有2中方式:

  • 第一种:是软件实现,用cpu去实现算法模拟GPU渲染、合成工作,就是agl(libGLES_android.so),路径是frameworks/native/opengl/libagl,即the software OpenGL ES library。

  • 第二种:是硬件厂商根据自己GPU提供的实现,一般都不开放源代码,就是/vendor/lib/egl或/system/lib/egl目录下的库。

OpenGL状态机(State Machine)

As long as you keep in mind that OpenGL is basically one large state machine,most of its functionality will make more sense.

  • OpenGL自身是一个巨大的状态机: 描述该如何操作的所有变量的大集合
  • OpenGL的状态通常被称为上下文(Context)
  • 状态设置函数(State-changing Function)
  • 状态应用的函数(State-using Function)

image

我们通过改变一些上下文变量来改变OpenGL的状态,从而告诉OpenGL如何去绘图。

既然OpenGL是一个大的状态机,那状态机中肯定会有两种函数:

  • 设置状态的函数,例如glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
  • 使用状态的函数,例如glClear(GL_COLOR_BUFFER_BIT);

OpenGL Context

OpenGL是一个仅仅关注图像渲染的图像接口库,在渲染过程中它需要将顶点信息、纹理信息、编译好的着色器等渲染状态信息存储起来,而存储这些信息调用任何OpenGL函数前,必须先创建OpenGL Context,它存储了OpenGL的状态变量以及其他渲染有关的信息。
OpenGL是个状态机,有很多状态变量,是个标准的过程式操作过程,改变状态会影响后续所有的操作,这和面向对象的解耦原则不符,毕竟渲染本身就是个复杂的过程。
OpenGL采用Client-Server模型来解释OpenGL程序,即Server存储OpenGL Context,Client提出渲染请求,Server给予响应。之后的渲染工作就要依赖这些渲染状态信息来完成,当一个上下文被销毁时,它所对应的OpenGL渲染工作也将结束。

OpenGL ES API没有提及如何创建渲染上下文,或者渲染上下文如何连接到原生窗口系统。

EGL是Khronos渲染API(如OpenGL ES)和原生窗口系统之间的接口。

对象

OpenGL在开发的时候引入了一些抽象层,对象(Object)就是其中的一个。

一个对象是指一些选项的集合,代表OpenGL状态的一个子集。

可以将OpenGL中的对象看作是C语言中的结构体。
例如下面可以用一个对象代表绘制窗口的设置,之后就可以设置它的大小、窗口支持的颜色位数等值。

struct object_Window_Target {
    float set_window_size;    // 这里调用的是OpenGL提供的设置窗口大小的某个方法
    float set_window_color;   // 这里调用的是OpenGL提供的设置窗口颜色位数的某个方法
    ....
}

通常把OpenGL上下文比作一个大的结构体,包含很多子集。

// OpenGL的状态
struct OpenGL_Context {
    ....
    object *object_Window_Target;  // 设置窗口大小、颜色位数等的对象
    ....
}

但是,这样也有问题:

  • 当前状态只有一份,如果每次显示不同的效果,都重新配置会很麻烦。
  • 这时候我们就需要一些小助理(对象),来帮忙记录某些状态信息,以便复用。

如果我们有10种子集,那就需要10个小助理(对象),而当前状态(Context,只有一份)可以通过装配这些对象来完成。

image

渲染管线

在OpenGL中,任何事物都在3D空间中,而屏幕和窗口却是2D像素数组,这导致OpenGL的大部分工作都是关于把3D坐标转变为适应你屏幕的2D像素。

3D坐标转为2D坐标的处理过程是由OpenGL的图形渲染管线(Graphics Pipeline,大多译为管线,实际上指的是一堆原始图形数据途经一个输送管道,期间经过各种变化处理最终出现在屏幕的过程)管理的。

图形渲染管线可以被划分为两个主要部分:

  • 第一部分把你的3D坐标转换为2D坐标
  • 第二部分是把2D坐标转变为实际的有颜色的像素

图形渲染管线接收一组3D坐标,然后把它们转变为你屏幕上的有色2D像素输出。

图形渲染管线可以被划分为几个阶段,每个阶段将会把前一个阶段的输出作为输入。
所有这些阶段都是高度专门化的(它们都有一个特定的函数),并且很容易并行执行。
正是由于它们具有并行执行的特性,当今大多数显卡都有成千上万的小处理核心,它们在GPU上为每一个(渲染管线)阶段运行各自的小程序,从而在图形渲染管线中快速处理你的数据。这些小程序叫做着色器(Shader)

简单来说,渲染管线就是一个顶点在呈现为像素之前经过的全部过程。

  • OpenGL 1.x系列采用的是固定功能管线。
  • OpenGL ES 2.0开始采用了可编程图形管线。
  • OpenGL ES 3.0兼容了2.0,并丰富了更多功能。

OpenGL渲染管线的流程为:

顶点数据 -> 顶点着色器 -> 图元装配 -> 几何着色器 -> 光栅化 -> 片段着色器 -> 逐片段处理 -> 帧缓冲

OpenGL ES 3.0实现了具有可编程着色功能的图形管线,有两个规范组成:

  • OpenGL ES3.0 API规范
  • OpenGL ES着色语言3.0规范(OpenGL ES SL)。

如下图,展示了OpenGL ES 3.0的图形管线。阴影的表示OpenGL ES 3.0管线中可编程阶段。

下面,你会看到一个图形渲染管线的每个阶段的抽象展示。
要注意蓝色部分代表的是我们可以注入自定义的着色器的部分:

首先,我们以数组的形式传递3个3D坐标作为图形渲染管线的输入,用来表示一个三角形,这个数组叫做顶点数据(Vertex Data);
顶点数据是一系列顶点的集合。
一个顶点(Vertex)是一个3D坐标的数据的集合。
而顶点数据是用顶点属性(Vertex Attribute)表示的,它可以包含任何我们想用的数据,但是简单起见,我们还是假定每个顶点只由一个3D位置和一些颜色值组成的吧。

立即渲染模式(Immediate mode) && 核心模式(Core-profile)

早期的OpenGL使用立即渲染模式(Immediate mode,也就是固定渲染管线),这个模式下绘制图形很方便。OpenGL的大多数功能都被库隐藏起来,开发者很少有控制OpenGL如何进行计算的自由。而开发者迫切希望能有更多的灵活性。随着时间推移,规范越来越灵活,开发者对绘图细节有了更多的掌控。立即渲染模式确实容易使用和理解,但是效率太低。因此从OpenGL3.2开始,规范文档开始废弃立即渲染模式,并鼓励开发者在OpenGL的核心模式(Core-profile)下进行开发,这个分支的规范完全移除了旧的特性。

当使用OpenGL的核心模式时,OpenGL迫使我们使用现代的函数。当我们试图使用一个已废弃的函数时,OpenGL会抛出一个错误并终止绘图。现代函数的优势是更高的灵活性和效率,然而也更难于学习。立即渲染模式从OpenGL实际运作中抽象掉了很多细节,因此它在易于学习的同时,也很难让人去把握OpenGL具体是如何运作的。现代函数要求使用者真正理解OpenGL和图形编程,它有一些难度,然而提供了更多的灵活性,更高的效率,更重要的是可以更深入的理解图形编程。

这也是为什么我们的教程面向OpenGL3.3的核心模式。虽然上手更困难,但这份努力是值得的。

现今,更高版本的OpenGL已经发布(写作时最新版本为4.5),你可能会问:既然OpenGL 4.5 都出来了,为什么我们还要学习OpenGL 3.3?答案很简单,所有OpenGL的更高的版本都是在3.3的基础上,引入了额外的功能,并没有改动核心架构。新版本只是引入了一些更有效率或更有用的方式去完成同样的功能。因此,所有的概念和技术在现代OpenGL版本里都保持一致。当你的经验足够,你可以轻松使用来自更高版本OpenGL的新特性。

依赖库

OpenGL实际上并不是把图像直接绘制到计算机屏幕上,而是将之渲染到一个帧缓冲区。
然后由计算机来负责把帧缓冲区中的内容绘制到屏幕上的一个窗口中。有不少库都可以支持这一部分工作:

  • 使用操作系统提供的窗口管理功能,比如Microsoft Windows API。但这通常不实用,因为需要很多底层的编码工作。
  • FreeGLUT、GLOW、GLFW等库

简要来说,GLAD、GLEW都是一个图形库,可以理解为是在显卡驱动上给渲染用户一个统一的API;
而GLUT、FreeGLUT、GLFW这三个是用于图形开发的辅助工具库,主要用于创建和管理OpenGL环境、操作窗口等。 OpenGL只是一个标准/规范,具体的实现是由驱动开发商针对特定显卡实现的。但是在你真正能够在程序中使用OpenGL之前,你需要对他进行初始化,但是由于OpenGL是跨平台的,所以也没有一个标准的方式进行初始化。OpenGL初始化分为两个阶段:

  • 第一个阶段,你需要创建一个OpenGL上下文环境,这个上下文环境存储了所有与OpenGL相关的状态(OpenGL是一个状态机),上下文位于操作系统中某个进程中,一个进程可以创建多个上下文,每一个上下文都可以描绘一个不同的可视界面,就像应用程序中的窗口;简单来理解就是为了创建一个窗口;而GLUT、FreeGLUT、GLFW库就是用于干这件事的。

  • 第二个阶段,你需要定位所有需要在OpenGL中使用的函数(由于OpenGL驱动版本众多,它大多数函数的位置都无法在编译时确定下来,需要在运行时查询。任务就落在了开发者身上,开发者需要在运行时获取函数地址并将其保存在一个函数指针中供以后使用),而GLEW、GLAD就是干这件事的。

GLFW

GLFW是一个专门针对OpenGL的C语言库,它提供了一些渲染物体所需的最低限度的接口。它允许用户创建OpenGL上下文,定义窗口参数以及处理用户输入。
简单来说,GLFW负责创建窗口,处理窗口相关的事件(如键盘和鼠标输入),并提供一个OpenGL上下文供你的程序使用。 这些库节省了我们书写操作系统相关代码的时间,提供给我们一个窗口和一个OpenGL上下文用来渲染。

GLAD

然后,我们有GLAD。由于OpenGL驱动版本众多,它大多数函数的位置都无法在编译时确定下来,需要在运行时查询。所以任务就落在了开发者身上,开发者需要在运行时获取函数地址并将其保存在一个函数指针中供以后使用。取得地址的方法因平台而异,代码非常复杂,而且很繁琐,我们需要对每个可能使用的函数都要重复这个过程。幸运的是,有些库能简化此过程,其中GLAD是目前最新,也是最流行的库。GLAD是用来管理OpenGL的函数指针的,所以在调用任何OpenGL的函数之前我们需要初始化GLAD。GLAD也可以使OpenGL基础渲染变得简单。

到了这些感觉不想学了,又是GLFW、又是GLAD,每个都要看,每个都要学,太麻烦了。好在Android上已经帮我们处理好了,我们只要使用GLSurfaceView即可。

着色器

着色器(shader)是在GPU上运行的小程序。
从名称可以看出,可通过处理它们来处理顶点。
此程序使用OpenGL ES SL语言来编写。
它是一个描述顶点或像素特性的简单程序。
OpenGL最本质的概念之一就是着色器,它是图形硬件设备所执行的一类特殊函数。
理解着色器最好的办法就是把它看做是专为图形处理单元(即GPU)编译的一种小型程序。

任何一种OpenGL程序本质上都可以被分为两部分:

  • CPU端运行的部分,采用C++、Java之类的语言编写
  • GPU端运行的部分,使用GLSL语言编写

顶点着色器

图形渲染管线的第一个部分是顶点着色器,他是用来渲染图形顶点的OpenGL ES代码。
它把一个单独的顶点作为输入。顶点着色器主要的目的是把3D坐标转为另一种3D坐标(后面会解释)从而生成每个顶点的最终位置,同时顶点着色器允许我们对顶点属性进行一些基本处理。

所有顶点着色器的主要目标都是将顶点发送给管线(正如之前所说的它会对每个顶点进行处理)。

顶点着色器可以操作的属性有: 位置、颜色、纹理坐标,但是不能创建新的顶点。最终产生纹理坐标、颜色、点位置等信息送往后续阶段。

顶点着色器会在GPU上创建内存用于存储我们的顶点数据,还要配置OpenGL如何解释这些内存,并且指定其如何发送给显卡。顶点着色器接着会处理我们在内存中指定数量的顶点。

我们通过顶点缓冲对象(Vertex Buffer Object, VBO)管理这个内存,它会在GPU内存(通常被称为显存)中储存大量顶点。

顶点缓冲对象是OpenGL中的一个对象(小助手),就像OpenGL中的其它对象一样,这个缓冲有一个独一无二的ID,所以我们可以使用glGenBuffers()函数和一个缓冲ID生成一个VBO对象:

OpenGL文档

glGenBuffers函数的定义为:

// glGenBuffers — glGenBuffers returns n buffer object names in buffers. 
void glGenBuffers(GLsizei n,
    GLuint *buffers);
// n : Specifies the number of buffer object names to be generated.
// buffers : Specifies an array in which the generated buffer object names are stored. 

C使用:

GLuint vertex_buffer;
// 创建一个小助理(对象),并且给小助理起一个名字
glGenBuffers(1, &vertex_buffer);

Java使用:

// 下面只生成一个vbo对象,所以这里申请一个容量为1的int型缓冲区就可以
IntBuffer vbo = IntBuffer.allocate(1);
// 该方法有两个参数,第一个是生成的数量,然后把它们的id储存在第二个参数的IntBuffer数组中
GLES30.glGenBuffers(1, vbo);

在OpenGL中有很多缓冲对象类型,顶点缓冲对象的缓冲类型是GL_ARRAY_BUFFER。
OpenGL允许我们同时绑定多个缓冲,只要它们是不同的缓冲类型。

我们可以使用glBindBuffer函数把新创建的缓冲绑定到GL_ARRAY_BUFFER目标上,glBindBuffer函数的定义为:

// glBindBuffer — bind a named buffer object

void glBindBuffer(GLenum target,
    GLuint buffer);
// target : Specifies the target to which the buffer object is bound. The symbolic constant must be GL_ARRAY_BUFFER, GL_COPY_READ_BUFFER, GL_COPY_WRITE_BUFFER, GL_ELEMENT_ARRAY_BUFFER, GL_PIXEL_PACK_BUFFER, GL_PIXEL_UNPACK_BUFFER, GL_TEXTURE_BUFFER, GL_TRANSFORM_FEEDBACK_BUFFER, or GL_UNIFORM_BUFFER.
// buffer: Specifies the name of a buffer object.

C使用:

// 让上下文给这个小助理分配工作任务,让小助理明确这次的工作内容 (绑定小助理(对象)到上下文)
glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer);

Java使用:

GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER, vbo.get(0));

从调用glBindBuffer()这一刻起,我们使用的任何(在GL_ARRAY_BUFFER目标上的)缓冲调用都会用来配置当前绑定的缓冲(VBO)。
然后我们可以调用glBufferData()函数,它会把之前定义的顶点数据复制到缓冲的内存中(小助理把对应的数据单独记录下来了)。

// glBufferData creates a new data store for the buffer object currently bound to target. 
// glBufferData为当前绑定到的目标缓冲区对象创建一个新的数据存储
void glBufferData(GLenum target,
    GLsizeiptr size,
    const GLvoid *data,
    GLenum usage);

// target : Specifies the target buffer object. The symbolic constant must be GL_ARRAY_BUFFER, GL_COPY_READ_BUFFER, GL_COPY_WRITE_BUFFER, GL_ELEMENT_ARRAY_BUFFER, GL_PIXEL_PACK_BUFFER, GL_PIXEL_UNPACK_BUFFER, GL_TEXTURE_BUFFER, GL_TRANSFORM_FEEDBACK_BUFFER, or GL_UNIFORM_BUFFER.
// size : Specifies the size in bytes of the buffer object's new data store.
// data : Specifies a pointer to data that will be copied into the data store for initialization, or NULL if no data is to be copied.
// usage : Specifies the expected usage pattern of the data store. The symbolic constant must be GL_STREAM_DRAW, GL_STREAM_READ, GL_STREAM_COPY, GL_STATIC_DRAW, GL_STATIC_READ, GL_STATIC_COPY, GL_DYNAMIC_DRAW, GL_DYNAMIC_READ, or GL_DYNAMIC_COPY.

C使用:

GLuint vertex_buffer; // Save this for later rendering
glGenBuffers(1, &vertex_buffer);
glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer);
glBufferData(GL_ARRAY_BUFFER, data_size_in_bytes, NULL, GL_STATIC_DRAW);

Java使用:

GLES30.glBufferData(GLES30.GL_ARRAY_BUFFER, POSITION_VERTEX.size * 4,
                vertexBuffer, GLES30.GL_STATIC_DRAW);

至此,已经把顶点数据储存在显卡的内存中,用创建的这个VBO顶点缓冲对象管理

glBufferData是一个专门用来把用户定义的数据复制到当前绑定缓冲的函数。也就是让小助理来记录需要的数据。

  • 第一个参数是目标缓冲的类型:这里顶点缓冲对象当前绑定到GL_ARRAY_BUFFER目标上。
  • 第二个参数是指定传输数据的大小(以字节为单位),c++中用一个简单的sizeof计算顶点数据的大小就可以,这里float的大小为4个字节。
  • 第三个参数是我们希望发送的实际数据。
  • 第四个参数指定了我们希望显卡如何管理给定的数据。它有三种常用的形式:
    • GL_STATIC_DRAW :数据不会或几乎不会改变。
    • GL_DYNAMIC_DRAW:数据会被改变很多。
    • GL_STREAM_DRAW :数据每次绘制时都会改变。

使用这些缓冲对象的好处是我们可以一次性的发送一大批数据到显卡上,而不是每个顶点发送一次。从CPU把数据发送到显卡相对较慢,所以只要可能我们都尝试尽量一次性发送尽可能多的数据。

当数据发送至显卡的内存中后,顶点着色器几乎能立即访问顶点,这个是非常快的过程。

整体总结为:

  • 它会在GPU上创建内存,用于存储我们的顶点数据

    • 通过顶点缓冲对象(Vertex Buffer Objects, VBO)来管理数据,顶点缓冲对象的缓冲类型是GL_ARRAY_BUFFER。
    • OpenGL允许我们同时绑定多个缓冲,只要它们是不同的缓冲类型(每一个缓冲类型类似于前面说的子集,每个VBO是一个小助理)。
  • 配置OpenGL如何解释这些内存

通过顶点数组对象(Vertex Array Objects, VAO)来管理,数组中的每一个项都对应一个属性的解析。

VAO(Vertex Array Object)是指顶点数组对象,主要用于管理 VBO 或 EBO ,减少 glBindBuffer 、glEnableVertexAttribArray、 glVertexAttribPointer 这些调用操作,高效地实现在顶点数组配置之间切换。

注意: VAO并不保存实际数据,而是放顶点结构的定义

image

OpenGLES2.0编程中,用于绘制的顶点数组数据首先保存在CPU内存,在调用glDrawArrays或者glDrawElements等进行绘制时,需要将顶点数组数据从CPU内存拷贝到显存。

但是很多时候我们没必要每次绘制的时候都去进行内存拷贝,如果可以在显存中缓存这些数据,就可以在很大程度上降低内存拷贝带来的开销。

OpenGLES3.0 VBO和EBO的出现就是为了解决这个问题。VBO和EBO的作用是在显存中提前开辟好一块内存,用于缓存顶点数据或者图元索引数据,从而避免每次绘制时的CPU与GPU之间的内存拷贝,可以提升渲染性能,降低内存带宽和功耗。

把数据发送给OpenGL管线还要更加复杂一点,有两种方式:

  • 通过顶点属性的缓冲区。
  • 直接发送给统一变量。

理解这两种方式的机制非常重要,因为这样我们才能为每个要发送的项目选取合适的方式。

缓冲区和顶点属性

想要绘制一个对象,它的顶点数据需要发送给顶点着色器。
通常会把顶点数据在放入一个缓冲区,并把这个缓冲区和着色器中声明的顶点属性相关联。
要完成这件事,有好几个步骤需要做,有些步骤只需要做一次,而对于动画场景,一些步骤则需要每帧做一次。只做一次的步骤如下,它们一般包含在init()中:

  • 创建缓冲区。
  • 将顶点数据复制到缓冲区。
    • 每帧都要做的步骤如下,它们一般包含在display()中。
  • 启用包含顶点数据的缓冲区。
  • 将这个缓冲区和一个顶点属性相关联。
  • 启用这个顶点属性。
  • 使用glDrawArrays()绘制对象。

所有缓冲区通常都在程序开始的时候统一创建,可以在init()中,或者在被init()调用的函数中。

在OpenGL中,缓冲区被包含在顶点缓冲对象(Vertex Buffer Object, VBO)中,VBO在OpenGL应用程序中被声明和实例化。

一个场景可能需要很多VBO,所以我们常常会在init()中生成并填充若干个VBO,以备程序需要时直接使用。

图元装配(Primitive Assembly)

顶点组合成图元的过程叫做图元装配,这里的图元就是指点、线、三角。
图元装配阶段将顶点着色器输出的所有顶点作为输入(如果是GL_POINTS,那么就是一个顶点),并将所有的点装配成指定的图元的形状。
也就是说图元装配是将顶点着色器中设置的顶点数据装配成指定图元的形状。

OpenGL中最基础且唯一的多边形就是三角形,所有更复杂的图形都是由三角形组成的。复杂的图形都可以拆分成多个三角形。比如OpenGL提供给开发者的绘制方法glDrawArrays,这个方法的第一个参数就是指定绘制方式,可选值有:

  • GL_POINTS:以点的形式绘制
  • GL_LINES:以线的形式绘制
  • GL_TRIANGLE_STRIP:以三角形的形式绘制,所有二维图像的渲染都会使用这种方式。

该过程还有两个重要操作,裁剪和淘汰:

  • 裁剪是指对于不在视椎体(屏幕上可见的3D区域)内的图元进行裁剪。
  • 淘汰是指根据图元面向前方或后方选择抛弃它们(如事物内部的点)。

几何着色器(Geometry Shader)

图元装配阶段的输出会传递给几何着色器(Geometry Shader)。

几何着色器把图元形式的一系列顶点的集合作为输入,它可以通过产生新顶点构造出新的图元来生成其他形状。例如它生成了另一个三角形。

几何着色器的输出会被传入光栅化阶段(Rasterization Stage),这里它会把图元映射为最终屏幕上相应的像素,生成供片段着色器(Fragment Shader)使用的片段(Fragment)。

在片段着色器运行之前会执行裁切(Clipping)。裁切会丢弃超出你的视图以外的所有像素,用来提升执行效率。

光栅化阶段(Rasterization Stage)

这里会把图元映射为最终屏幕上相应的像素,生成供片段着色器(fragment shader)使用的片段。
在图元装配后传递过来的图元数据中,这些图元信息只是顶点而已。
顶点处都还没有像素点,直线段端点之间是空的、多边形的边和内部也是空的,光栅化的任务就是构造这些。将图片转化为片段(fragment)的过程叫做光栅化。

这个阶段会将图元数据分解成更小的单元并对应于帧缓冲区的各个像素,这些单元成为片元,一个片元可能包含窗口颜色、纹理坐标等属性。

光栅化其实是一种将几何图元变成二维图像的过程。在这里,虚拟3D世界中的物体投影到平面上,并生成一系列的片段。

片段着色器或片元着色器(Fragment Shader)

使用颜色或纹理(texture)渲染图形表面的OpenGLES代码。

我们3D世界中的点、三角形、颜色等全都需要展现在一个2D显示器上。
这个2D屏幕由栅格(即矩形像素阵列)组成。当3D物体栅格化后,OpenGL会将物体中的图元(通常是三角形)转化为片段。
片段拥有关于像素的信息。栅格化过程确定了为了显示由3个顶点确定的三角形需要绘制的所有像素的位置。片段着色器用于为栅格化的像素指定颜色。

主要目的是计算一个像素的最终颜色。
所有片段着色器的目的都是给为要展示的像素赋予颜色。

光栅化操作构造了像素点(图元(点或三角形)转换成了像素的集合),这个阶段就是处理这些像素点,根据自己的业务,例如高亮、饱和度调节、高斯模糊等来变化这个片元的颜色。
为组成点、直线和三角形的每个片元生成最终颜色/纹理,针对每个片元都会执行一次,一个片元是一个小的、单一颜色的长方形区域,类似于计算机屏幕上的一个像素。
一旦最终颜色生成,OpenGL就会把它们写到一块称为帧缓冲区的内存块中,然后Android就会把这个帧缓冲区显示到屏幕上。

通常在这里对片段进行处理(纹理采样、颜色汇总等),将每个片段的颜色等属性计算出来并送给后续阶段。

可以看到,图形渲染管线非常复杂,它包含很多可配置的部分。然而,对于大多数场合,我们只需要配置顶点和片段着色器就行了。几何着色器是可选的,通常使用它默认的着色器就行了。

在现代OpenGL中,我们必须定义至少一个顶点着色器和一个片段着色器(因为GPU中没有默认的顶点/片段着色器)。

光栅化后产生了多少个片元,就会插值计算出多少套in变量。同时,渲染管线就会调用多少次片元着色器。可以看出,一般情况下对一个3D物体的渲染中,片元着色器执行的次数会大大超过顶点着色器。 因此,GPU硬件中配置的片元着色器硬件数量往往多于顶点着色器硬件数量,通过这些硬件单元的并行执行,提高渲染速度。

整个处理过程又分为以下几个部分:

  • 逐片段操作 具体的细分步骤又分为:

    • 像素归属测试

      判断当前像素是否归OpenGL所有,即OpenGLES帧缓冲区窗口的部分是否被另一个窗口所遮蔽,被遮挡像素不属于OpenGL上下文。

    • 裁剪测试

      判断当前像素是否位于裁剪矩形范围内,如果位于裁剪区域外则被抛弃。

    • 模板测试

      模板测试主要将绘制区域限定在一定范围内,一般用在湖面倒影、镜像等场合。

    • 深度测试

      深度测试是将输入片元的深度与帧缓冲区中对应片元的深度进行比较,确定片段是否应该被拒绝。

    • 混合

      将新生成的片段和保存在缓冲区的片段进行混合。

    • 抖动

      用于最小化,因为使用有限精度在帧缓冲区中保存颜色值而产生的伪像,使用少量颜色模拟更宽的颜色范围。

帧缓冲区

OpenGL实际上并不是把图像直接绘制到计算机屏幕上,而是将之渲染到一个帧缓冲区,然后由计算机来负责把帧缓冲区中的内容绘制到屏幕上的一个窗口中。

OpenGL管线的最终渲染目的地被称为帧缓存(framebuffer也被记做FBO)

着色器语言

GLSL(OpenGL Shading Language)是OpenGL着色语言。

在图形卡的GPU上执行,代替了固定的渲染管线的一部分,是渲染管线中不同层次具有可编程性,例如:视图转换、投影转换等。GLSL的着色器代码分为两个部分:

  • 顶点着色器(Vertex Shader)
  • 片段着色器(Fragment Shader)

通常在CPU上编译、链接成二进制数据,然后将它拷贝到GPU上运行,在GPU上运行起来的就是渲染、合成的命令。它的编译是通过OpenGL API操作的。

坐标系

OpenGL ES是一个3D的世界,由x、y、z坐标组成顶点坐标。采用虚拟的右手坐标。

OpenGL采用的是右手坐标,选取屏幕中心为原点,从原点到屏幕边缘默认长度为1,也就是默认情况下,从原点到x左边的1和到y左边的1的位置在屏幕上显示的并不相同。

形状面和缠绕

在OpenGL的世界里,我们只能画点、线、三角形,图元装配中说到所有复杂的图形都是由三角形组成。

三角形的点按顺序定义,使得它们以逆时针方向绘制。绘制这些坐标的顺序定义了形状的缠绕方向。默认情况下,在OpenGL中,逆时针绘制的面是正面。

程式(Program)

一个OpenGL对象,包含了你所希望用来绘制图形所要用到的着色器,最后顶点着色器和片元着色器都要放入到程式中,然后才可以使用,简单的说就是将两个着色器变为一个程序对象。

如果想要绘制图像,需要至少一个顶点着色器来定义一个图形顶点,以及一个片元着色器为该图形上色。这些着色器必须被编译然后再添加到一个OpenGL Program中,并利用这个Program来绘制形状。

glUseProgram()用于将含有两个已编译着色器的程序载入OpenGL管线阶段(在GPU上)。
注意,glUseProgram()并没有运行着色器,它只是将着色器加载进硬件。

EGL

在OpenGL的设计中,OpenGL是不负责管理窗口的,窗口的管理交由各个设备自己完成。具体的来说就是iOS平台上使用EAGL提供本地平台对OpenGL的实现,在Android平台上使用EGL提供本地平台对OpenGL的实现。OpenGL其实是通过GPU进行渲染。但是我们的程序是运行在CPU上,要与GPU关联,这就需要通过EGL,它相当于Android上层应用于GPU通讯的中间层。

EGL提供了OpenGL ES和运行于计算机上的原生窗口系统(如Windows、Mac OSX)之间的一个“结合”层次。
在EGL能够确定可用的绘制表面类型(或者底层系统的其他特性)之前,它必须打开和窗口系统的通信渠道。注意。Apple提供自己的EGL API的iOS实现,称为EAGL。
因为每个窗口系统都有不同的语言,所以EGL提供基本的不透明类型--EGLDisplay,该类型封装了所有系统相关性,用于和原生窗口系统接口。任何使用EGL的应用程序必须执行的第一个操作就是创建和初始化与本地EGL显示的连接。

EGL是OpenGL和本地窗口系统(Native Window System)之间的通信接口,它的主要作用:

  • 与设备的原生窗口系统通信;
  • 查询绘图表面的可用类型和配置;
  • 创建绘图表面;
  • 在OpenGL ES 和其他图形渲染API之间同步渲染;
  • 管理纹理贴图等渲染资源。 OpenGL ES 的平台无关性正是借助 EGL 实现的,EGL 屏蔽了不同平台的差异。

image

上图中:

  • Display(EGLDisplay) 是对实际显示设备的抽象;
  • Surface(EGLSurface)是对用来存储图像的内存区域 FrameBuffer 的抽象,包括 Color Buffer(颜色缓冲区), Stencil Buffer(模板缓冲区) ,Depth Buffer(深度缓冲区);
  • Context (EGLContext) 存储 OpenGL ES 绘图的一些状态信息; 在 Android 平台上开发 OpenGL ES 应用时,类 GLSurfaceView 已经为我们提供了对 Display , Surface , Context 的管理,即 GLSurfaceView 内部实现了对 EGL 的封装,可以很方便地利用接口 GLSurfaceView.Renderer 的实现,使用 OpenGL ES API 进行渲染绘制,很大程度上提升了 OpenGLES 开发的便利性。

本地窗口相关的 API 提供了访问本地窗口系统的接口,而 EGL 可以创建渲染表面 EGLSurface ,同时提供了图形渲染上下文 EGLContext,用来进行状态管理,接下来 OpenGL ES 就可以在这个渲染表面上绘制。

EGL为双缓冲工作模式(Double Buffer),既有一个Back Frame Buffer和一个Front Frame Buffer,正常绘制的目标都是Back Frame Buffer,绘制完成后再调用eglSwapBuffer API,将绘制完毕的FrameBuffer交换到Front Frame Buffer并显示出来。

补充说明: 应用程序使用单缓冲绘图时可能会存在图像闪烁的问题。这是因为生成的图像不是一下子被绘制出来的,而是按照从左到右,由上到下逐像素的绘制而成的。最终图像不是在瞬间显示给用户,而是通过一步一步生成的,这会导致渲染的结果很不真实。为了规避这些问题,就应用了双缓冲渲染模式,前缓冲保存着最终输出的图像,它会在屏幕上显示;
而所有的渲染指令都会在后缓冲上绘制。当所有的渲染指令执行完毕后,我们交换前缓冲和后缓冲,这样图像就立即呈现出来,刚才提到的不真实感就消除了。

要在Android平台实现OpenGL渲染,需要完成一系列的EGL操作,主要为下面几步(后面分析GLSurfaceView源码的时候也是这样来实现的):

  1. 获取显示设备(EGL Display)

    获取将要用于显示的设备,有些系统具有多个显示器,会存在多个display,在Android上通过调用EGL10的eglGetDisplay(Object native_display)方法获得EGLDisplay对象,通常传入的参数为EGL10.EGL_DEFAULT_DISPLAY。

  2. 初始化EGL

    调用EGL10的egInitialize(EGLDisplay display, int[] major_minor)方法完成初始化操作。display参数即为上一步获取的对象,major_minor传入的是一个int数据,通常传入的是一个大小为2的数据。

  3. 选择Config配置

    调用EGL10的eglChooseConfig(EGLDisplay display, int[] attire_list, EGLConfig[] configs, int config_size, int[] num_config)方法,参数3用于存放输出的configs,参数4指定最多输出多少个config,参数5由EGL系统写入,表明满足attributes的config一共有多少个。

  4. 创建EGL Context

    eglCreateContext(EGLDisplay display, EGLConfig config, EGLContext share_context, int[] attrib_list);参数1即为上面获取的Display,参数2为上一步chooseConfig传入的configs,share_context,是否有context共享,共享的contxt之间亦共享所有数据,通常设置为EGL_NO_CONTEXT代表不共享。attrib_list为int数组 {EGL_CONTEXT_CLIENT_VERSION, 2,EGL10.EGL_NONE };中间的2代表的是OpenGL ES的版本。

  5. 创建EGLSurface

    eglCreateWindowSurface(EGLDisplay display, EGLConfig config, Object native_window, int[] attrib_list);参数1、2均为上述步骤得到的结果,参数3为上层创建的用于绘制内容的surface对象,参数4常设置为null。

  6. 设置OpenGL的渲染环境

    eglMakeCurrent(EGLDisplay display, EGLSurface draw, EGLSurface read, EGLContext context);该方法的参数意义很明确,该方法在异步线程中被调用,该线程也会被成为GL线程,一旦设定后,所有OpenGL渲染相关的操作都必须放在该线程中执行。

通过上述操作,就完成了EGL的初始化设置,便可以进行OpenGL的渲染操作。所有EGL命令都是以egl前缀开始,对组成命令名的每个单词使用首字母大写(如eglCreateWindowSurface)。

创建屏幕外渲染区域: EGL Pbuffer

除了可以用OpenGL在屏幕上的窗口渲染之外,还可以渲染称作pbuffer(像素缓冲区Pixel buffer的缩写)的不可见屏幕外表面。
和窗口一样,Pbuffer可以利用OpenGL ES3.0中的任何硬件加速。
Pbuffer最常用于生成纹理贴图。如果你想要做的是渲染到一个纹理,那么我们建议使用帧缓冲区对象代替Pbuffer,因为帧缓冲区更高效。不过,在某些帧缓冲区对象无法使用的情况下,Pbuffer仍然有用,例如用OpenGL ES在屏幕外表面上渲染,然后将其作为其他API(如OpenVG)中的纹理。
创建Pbuffer和创建EGL窗口非常相似,只有少数微小的不同。为了创建Pbuffer,需要和窗口一样找到EGLConfig,并作一处修改:我们需要扩增EGL_SURFACE_TYPE的值,使其包含EGL_PBUFFER_BIT。

OpenGL纹理

纹理(Texture)是一个2D图片(也有1D和3D纹理),他可以用来添加物体的细节,你可以想象纹理是一张绘有砖块的纸,无缝折叠贴合到你的3D房子上,这样你的房子看起来就有砖墙的外表了。

视频数据流

视频从视频文件到屏幕显示出来,Surface起到了非常重要的作用,在视频解码时,需要一个Surface作为已解码数据的载体,保存播放器解码后的数据。

在显示时,需要另一个Surface作为已渲染数据的载体,保存OpenGL渲染后的数据,最后通过EGL显示在屏幕上。

数据流如下图所示:

image

  • 解码器(MediaCodec或FFmpeg等)将视频文件中的视频编码数据解码成图像流放到Surface中。
  • SurfaceTexture把Surface生成的图像流,转换为纹理
  • OpenGL ES渲染纹理生成图形数据,放到GLSurfaceView中的Surface中
  • EGL从Surface中取出图形数据,显示到屏幕上

参考