很久没有更新文章了,最近开始学习OpenGL的相关知识,记录一些学习笔记。大部分可能都是舶来品,但尽量带一些自己的理解。
学习资料:《Learn OpenGL - Graphics Programming》- Joey de Vries

OpenGL的入门内容其实可以分为两个部分,1、图像映射理论,2、构建逻辑与数据模型。前者偏理论,后者偏应用,按照知识的递进关系先有前者再有后者,但这么安排讲解,无法让读者短时间得到有效反馈。所以我所看的这本书很聪明先去讲实现,让大家从demo入手,而后再展开讲理论,可以说做到了循序渐进。这里就尽量不复述书上的内容了,作为这个系列学习笔记的第一篇,尝试提炼下第一部分,即OpenGL构建逻辑与数据模型。

Shader

shader是OpenGL的核心之一,顶点数据最终得到成型的图形需要经过多个步骤,其中包含多个shader。顶点数据包括规则形状的顶点坐标、对应顶点坐标的色彩数组或者材质素材对应坐标等等。一套完整成像过程所涉及的shader包括

  1. vertex shader - 建立外围输入数据与顶点属性的对应关系,尤其是顶点坐标gl_Position的对应关系,这里顶点坐标指的不是渲染物在物理空间坐标,而是最终映射在屏幕的相对坐标,因此一般会涉及到多个转换矩阵的相乘,相关内容争取在下一篇笔记中写一下。 同时还可以声明输出变量,通常包括对应顶点坐标的色彩数组或者材质素材对应坐标,以作为fragment shader的输入。
  2. geometry shader - 在建立好输入数据与图形坐标对应关系后,进入几何构建阶段。例如调用glDrawArrays函数第一个参数为GL_TRIANGLES,则每三个顶点构建为一个三角形,两个三角可构建一个多边形,多个三角形构建为其他几何形状,如立方体等等。
  3. fragment shader - 计算每个像素的最终色彩,比如通过插值计算补全图形色彩、通过顶点材质坐标补全图形材质、其他更高级的光影渲染等等。

vertex shader、geometry shader、fragment shader都可以手动定义。使用语言为类C语法的GLSL。所定义的shader是在运行时进行编译与使用。

VBO,VAO与EBO

OpenGL所提供出来的接口可以说是经典面向过程的范式,一方面它是函数式编程,同时我们还能接触到面向内存编程这种古老而有意思的开发模式。这基本只有c/c++才能提供的乐趣。

VBO

上述提到vertex shader需要输入数据,输入数据在运行时存储在显卡缓存中。我们知道处理器在进行计算时,首先要将内存数据搬移至寄存器,这个过程会带来一定耗时。数据存储在显卡缓存中,省去渲染阶段显卡将内存区域的数据搬移至显卡所带来耗时,侧面说明显卡缓存并非等同于内存,更像是内存与寄存器的结合。所绑定的缓存区间由某个非负整形ID代表,亦将对应的区域称之为VBO(vertex buffer objects)。

1
2
3
4
unsigned int VBO;
glGenBuffers(1, &VBO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

上述操作划分了对应的缓存区域,将内存数据搬移其中,但并未建立vertex shader中顶点属性与这些数据的对应关系,因此要通过以下步骤建立对应关系,或者说是明确Vertex Attribute Pointer顶点属性指针的指向。

1
glVertexAttribPointer(N, 3, GL_FLOAT,GL_FALSE, 3 *sizeof(float),(void*)0);

这行代码含义为:指针编号为N,输入数据中连续三个浮点型为一组,每组间隔为三个浮点型的长度(即组与组之间是连续的),起始偏移量为0。
在vertex shader中,于顶点属性声明之前加上layout (location = N) 即可明确顶点属性指针编号为N。比如

1
layout (location = N) in vec3 aPos;

稍早OpenGL版本稍有出入,vertex shader只能进行简单变量的声明,而明确指针编号需要在c/c++层面进行。

1
glBindAttribLocation(shaderProgram, N, "aPos");

VAO

VBO展现了OpenGL数据模型与数据通信的基本脉络。在极其简单的场景好像已经够用——用VBO1存储一个或一类几何图形的顶点坐标,用VBO2存储另一个或一类几何图形的顶点坐标,诸如此类。现实情况为,一个稍微复杂一点的几何图形,除了顶点,还需要对应顶点色彩数据,或者材质素材坐标数据,再复杂一点,比如进行光影处理时需要对应顶点的单位法向量等等。一个VBO存储多类顶点属性时,需要额外的数据对象,存储VBO的顶点属性指针,因此引入VAO,即Vertex Array Objects

VAO与VBO是一对一关系。这张图很好展示了VAO与VBO的关系。

http://captainwz.com/img/opengl1.png

VAO的代码相对简单,在绑定VBO的同时绑定VAO即可。

1
2
3
unsigned int VAO;
glGenVertexArrays(1, &VAO);
glBindVertexArray(VAO);

EBO

EBO,Element Buffer Objects,主要用于解决在表达几何图形时存在的顶点数据重复问题。比如四边形由两个三角形所表达,共六个顶点,但由两个顶点数据是重复的。有了EBO后,重复顶点数据只需一份,由EBO存储几何图形所需顶点数据的索引值。EBO也可像顶点属性指针一样在VAO中被指向。VBO、VAO、EBO三者关系如下图。

http://captainwz.com/img/opengl2.png

大致过程

总结一下,OpenGL中从数据到成像大致可以分为如下几个步骤:

  1. 顶点数据准备阶段,至少包括顶点坐标,还可能包括顶点坐标对应的色彩值、材质坐标、单位法向量等等。
  2. 数据由内存搬移至显卡缓存中,对应区域称之为VBO(Vertex Buffer Objects)。每类数据的起始指针存储在VAO中(Vertex Array Objects)。存在重复数据可利用索引值来表示每个图形包含的顶点,以降低存储空间,该信息存储于EBO(Element Buffer Objects),VAO包含指针指向对应EBO。
  3. 应用运行时对手动定义shader进行编译。通常,Vertex Shader将以上述显卡缓存中数据作为主要输入,以部分其他内存数据(uniform声明的变量)为辅助,计算出顶点对应的视图坐标,一些尚未被使用到的数据(如色彩等)可继续作为Fragment Shader输入。
  4. 在渲染逻辑主体中明确顶点所构建基础图形后(比如glDrawArrays(GL_TRIANGLES, 0, 36)明确构建的基础图形为三角形),Geometry Shader将这些基础图形构建为更复杂的几何图形,即最终图像的基本轮廓。
  5. 对几何图形进行像素化处理(可理解为将其分解为一个个细小颗粒),Fragment Shader将利用输入数据对每一个像素进行上色,完成图像的光影效果。
  6. 进行深度测试、模板测试、色彩混合等等操作(深度测试即是实现视觉遮挡效果,其他几个争取在后续学习笔记中写一下)。完成以上步骤,得到最终图像。

以上是OpenGL学习系列的第一篇内容,若有纰漏,还望指正。