OpenGL 入门

OpenGL 入门(实际是看见大门)

一直对OpenGL感兴趣,但是之前了解过它两次,也写过例子,但是没弄懂,最近工作需要,学习了一下,找到了一个好的教程,才算弄明白了一点,这篇文章只是开个头,试着按照我的理解将OpenGL的基本原理讲明白,希望读者反馈,一块讨论:)

从哪开始

学习一个东西,最快的方式应该是先从宏观的角度去了解它,起点当然是官方网站和Wiki,这里是最正确的一手资料。

OpenGL 是 open graphics library 的缩写。从名字来看,它是一个库, 但是它只是一个 API(application programming interface),只是一套规范,用来渲染 2D 或者 3D 向量图形的。为什么是向量图形,以后会会讲到,先来把它的用途说完,它是通常用来与GPU交互,实现硬件加速。”通常”,你懂得他的意思吧?因为它就是一套接口,如果你要实现它不与GPU交互那也是可以的。要去实现它的是那些生产显卡GPU的厂商,这样,他的GPU被叫做支持OpenGL。

硬件加速是什么意思? 硬件加速是借助硬件模块来替代 运行在通用目的的CPU上的 软件算法。注意加粗的部分,没有这个修饰,就很难理解了,因为所有软件算法都是运行在硬件上的,是吧?这里的意思,CPU是通用目的的,而GPU是专门处理图像的,所以GPU的图像处理操作要比CPU块,不然也没它事了。

这样,我们知道OpenGL操作GPU, GPU在显卡上,显卡有现存,而我们平时编程只是跟内存打交道, 当然还有CPU,我们是摆脱不掉他的。所以我们在进行OpenGL开发时,就牵扯到CPU, GPU, 内存, 显存,还有文件存储系统。

状态机–给我发个红包,我笑给你看

OpenGL 本身是一个大的状态机,这只鸡有多大, 大到我们总是在它肚子里,哈哈,开个玩笑,不过也算贴切。

我们先来理解什么是状态机。 数学上有有限状态机, 用来描述有限个状态,而且这些状态之间会相互转换。 形象一点,一个烤面包机也算一个状态机,有没有插电两种状态,有没有打开开关两个状态, 有没有插入面包两个状态。这些状态,都会因为一些行为而改变,比如插入插销,烤面包机就由未插电状态变成插电状态。而某些状态会支持某些行为,如只有插电,插入面包,打开开关,这时才能够加热面包。
下面还有个图简单的不能再简单的状态机:


这是我画的,还挺有感觉:)

一个大开关,里边有三个按钮,这就表示了6种状态,而每个按钮的拨动,都会导致大开关的状态转换,这就是状态机。

OpenGL里有个Context的概念,而这个Context,中文译为上下文。我们在进行 OpenGL 开发时, 会设置某些选项,操作某些缓冲,这些操作并没有指明是对谁,但是实际上是改变了当前上下文的状态, 然后进行绘制,绘制出来的图像是那种状态下的样子。

然后,当切换了上下文时,之前的配置就都无效了。

这就像我们本来看着一个正在烤着面包的面包机,突然目光转向一个电都没插,开关没开的烤面包机,然后插电,插面包,打开,这一系列的操作的对象都不用指明是那个烤面包机,但是,读起来却明白是它,这就是上下文的作用。

对象–状态的集合,数据的容器

OpenGL 里也有对象, 但是此对象并不是面向对象编程语言里的对象,更不是你妈让你找的对象,而是一个可选项的集合,此集合是OpenGL状态的一个子集。如图:

我们可以把控制灯光的两个开关看做一个对象,控制风扇的一个开关看做一个对象,这样这个状态机就有两个对象。

无论何时何地,我们都像下面这样使用对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// OpenGL的状态
struct OpenGL_Context
{
...
object * object_Window_Target;
...
};

// 创建对象
GLuint objectId = 0;
glGenObject(1, &objectId);

// 把对象绑定到环境
glBindObject(GL_WINDOW_TARGET, objectId);

// 设置当前绑定到GL_WINDOW_TARGET的对象的选项
glSetObjectOption(GL_WINDOW_TARGET, GL_OPTION_WINDOW_WIDTH, 800);
glSetObjectOption(GL_WINDOW_TARGET, GL_OPTION_WINDOW_HEIGHT, 600);

// 把上下文目标重设为默认
glBindObject(GL_WINDOW_TARGET, 0);

(代码来自:五十弦翻译的教程,啥叫禁止演义)

OpenGL如何工作的

前面已经提到过,OpenGL总共牵扯到计算机的5个大部分,它们分别是:
CPU,内存,GPU,显存,外存, 主要干了三个大事:

  1. CPU将数据从内存复制到显存
  2. CPU将着色器程序上传到GPU
  3. 通知GPU运行着色器程序,处理数据

下面一个图来表示 OpenGL 是如何工作的:

根据以上我们说的三个大事,和图片所示,我们就可以解释几个语句块

上传顶点数据的代码:

1
2
3
4
5
6
7
8
9
10
11
12
// 这是在内存中的数据
GLfloat vertices[] = {
-0.5f, -0.5f, 0.0f, // Left
0.5f, -0.5f, 0.0f, // Right
0.0f, 0.5f, 0.0f // Top
};
GLuint VBO; // Vertex Buffer Object, 这是一个缓存对象
glGenBuffers(1, &VBO); // 这时候才真正在显存创建了一个对象,VBO是它在内存中的一个引用,这时候,也分配了空间也有引用,但是依然不能操作它,因为,它跟OpenGLContext还没有关系
glBindBuffer(GL_ARRAY_BUFFER, VBO); // 这里真正产生了关系,将缓存对象绑定到了GL_ARRAY_BUFFER这么一个数组缓存目标上,为什么是目标,因为,之后我们就通过操作它来影响显存中的缓存对象。
// 这里数据传送路径是这样的:内存->CPU->GPU->显存
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
glBindBuffer(GL_ARRAY_BUFFER, 0); // 这里解绑刚才的缓存对象,之后的对GL_ARRAY_BUFFER的操作就不会影响上边创建的缓存对象了。

上面我们上传了顶点数据到显存,下面我们上传着色器程序到GPU,着色器程序是什么,我们暂时只需要知道,它会在GPU执行,处理我们上传到显存的数据就行了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 通知GPU分配一个顶点类型的着色器
GLuint vertexShader = glCreateShader(GL_VERTEX_SHADER);
// 将内存里的源代码指定给它(不知道有没有上传到显存)
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
// 编译着色器
glCompileShader(vertexShader);

...省略颜色着色器的代码,一般都会有这两个着色器,也可能有其他

// 创建一个着色器程序
GLuint shaderProgram = glCreateProgram();
// 将顶点着色器跟颜色着色器添加到着色器程序
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
// 链接着色器程序
glLinkProgram(shaderProgram);
// 这样就准备好了着色器程序

下面是绘制代码部分:

1
2
3
4
5
6
glUseProgram(shaderProgram); // 通知GPU使用shaderProgram着色器程序, 这里算是切换上下文,之后的GPU运行的都是这个Program
glBindBuffer(GL_ARRAY_BUFFER, VBO); // 设置当前的GL_ARRAY_BUFFER数组缓存对象, 这里也是切换上下文,之后使用的数组数据,来自VBO
glDrawArrays(GL_TRIANGLES, 0, 3); // 这时候,它会自动的去拿GL_ARRAY_BUFFER里的数据,使用shaderProgram这个着色器程序,进行绘制
glBindBuffer(GL_ARRAY_BUFFER, 0); // 解绑顶点数据
// 不在使用shaderProgram着色器程序
glUseProgram(shaderProgram);

当然,OpenGL的基本概念还有很多,这里这篇文章知识让你远远的看一看它的全貌,如果喜欢,请继续关注, 如果您能提出宝贵的意见,不胜感激。