OpenGL名词解释

buffer

一段连续的内存区域

data buffer

将本地图片*.jpg数据加载至内存,即data buffer。

它的大小和图片存储在磁盘中文件大小一致,但其数据并不直接描述像素(因为我们常用的图片数据本身就是压缩后的)。

image buffer

Image Buffers代表图片元数据Data Buffers在内存中被解码后的表示,每个元素代表一个像素点的颜色,即常说的位图。

其大小与Data Buffers大小成正比例。

framebuffer

Frame Buffer 存储了程序的每帧的实际渲染输出(actual rendered output)。GPU会根据Frame Buffer 的内容按一定帧率显示在屏幕上。

你可以这样理解,显示器刷新频率为60Hz,即GPU每16.6ms从framebuffer里面获取数据并显示在屏幕上。

Vertex

原指(三角形或锥形的)角顶、顶点、至高点,引申为OpenGL中的任一顶点。

绘制一个三角形,给定三个顶点,vertex程序将顶点坐标、纹理坐标传递给fragment程序

在传递时,系统会对顶点组成的区域进行插值,使其形成面。

fragment

原指片元,OpenGL中指的是一片区域。

fragment程序根据插值结果计算最后显示在屏幕上的像素值

vao /vertex array object

vbo/ vertex buffer object

顶点缓冲对象

VBO其实没有用到GPU运算,也就是说他不用写着色语言,直接用opengl函数就可以调用,主要目的是用于加快渲染的速。
我们都知道,受到总线带宽的限制,把顶点数据从CPU传到GPU,是比较花费时间的,特别当我们有上百万个顶点要频繁地传送到GPU的时候。

为了解决这个瓶颈问题,于是便有了VBO,VBO的应用就是在显卡中分配一个显存空间,然后一次性把所有的顶点数据发送到这块显存中保存起来,这样,当我们要刷新3D场景的时候,我们就不需要每次都把上百万个顶点从CPU传送到GPU了,只要直接从VBO对像中读取就行了。数据从CPU到GPU大概只有8G的带宽,而由VBO直接读取,速度是50G以上,几十倍的差别。

C/C++通过指针进行间接寻址,而OpenGL中是通过各种类型object id进行寻址,在创建一个opengl对象时系统会给它分配一个id,我们要间接寻址都是这个id,比如RC(渲染上下文)中的一个状态就保存着当前VAO的Id,而VAO中又保存了各种缓存的id(VBO、EBO…)

需要注意的是,不要在VAO.release()之前调用VBO或EBO的release()函数

VAO也是一个状态机,Qt中的代码并不能很好的体现这一特点,因此很容易出错。

比如这样的代码,看上去似乎没错

1
2
3
4
5
6
7
VAO.create();
VAO.bind();
VBO.create();
VBO.bind();
//do something...
VBO.release();
VAO.release();

其实是大错特错,假设VAO是一个这样的结构体(其实本质上差不多的)

1
2
3
4
5
6
struct VAO{
int ID;
int VBO_ID;
int EBO_ID;
//...
}

而上下文中则存储着当前VAO的Id;

再来看上面的代码

1
2
3
4
5
6
7
VAO.create();       创建一个VAO对象,OpenGL会给它(顶点数组缓存对象)分配一个id
VAO.bind(); 将RC中的当前顶点数组缓存对象Id设置为VAO的id
VBO.create(); 创建一个VBO对象,OpenGL会给它(缓存对象)分配一个id
VBO.bind(); 将当前顶点数组中的VBO_ID设置为此对象的Id
//do something...
VBO.release(); 将当前顶点数组中的VBO_ID设置为0
VAO.release(); 将RC中的当前顶点数组缓存对象ID设置为0

这样一看,我们是把VBO”绑定”到VAO上,又把0绑定到VAO上替换了VBO,这就导致了VBO并没有正确的绑定到VAO上

因此,我们需要把VBO解绑放到VAO解绑之后,或者直接不解绑

fbo/fragment buffer object

FBO与VBO虽然只有一字之差,就它们的意义却大有不同。FBO的主要作用就是改变当前帧缓存的输出路径,除了自身之外,它并不分配内存。默认情况下,显卡的图像数据是输出到帧缓存和深度缓存中去的,帧缓存的数据会直接显示到显示器上。但是用了FBO之后,我们可在改变这一默认的输出方向,把原来要输送到帧缓存或深度缓存的数据输送到一个纹理对像中去,而这个纹理则可以用于后面的运算。

要注意,这里的纹理对像是标准的纹理,要求程序员为其分配显存空间,而FBO只是一个桥梁,起到连接的作用。

pbo/pixel buffer object

PBO设计的目的就是快速地向显卡传输数据,或者从显卡读取数据,我们可以使用它更加高效的读取屏幕数据。

单个PBO读取屏幕数据效率大概和 glReadPixels() 差不多,双PBO交换读取效率会很高。原因是使用PBO时,屏幕上的数据不是读取到内存,而是从显卡读到PBO中,或者如果内部机制是读取到内存中,但这也是由DMA 控制器来完成的,而不是cpu指令来做的,再加上两个PBO交换使用,所以读取效率很高。

还可以使用PBO 高效读取内存数据(比如纹理)到显存中。在没有FBO之前,就是用来它做离屏渲染的。

油画算法/画家算法

所谓画家算法就是像画家画画一样的流程。

painter

先画山,再画草地(草地挡住了一部分山),再画树(树挡住了一部分草地)。

然而有些场景并没有那么简单。作为“画家”的GPU虽然可以一层一层往画布上进行输出,但是无法在某一层渲染完成之后,再回过头来擦除/改变其中的某个部分——因为在这一层之前的若干层layer像素数据,已经在渲染中被永久覆盖了。这就意味着,对于每一层layer,要么能找到一种通过单次遍历就能完成渲染的算法,要么就不得不另开一块内存,借助这个临时中转区域来完成一些更复杂的、多次的修改/剪裁操作。

离屏渲染(offscreen render)

当image buffer需要进行一些额外处理(如圆角、毛玻璃或其他滤镜)并且进行额外处理后无法直接将数据传递至frame buffer进行显示,需要将处理后的数据暂存至offscreen buffer中,再由offscreen buffer传递至frame buffer,最终显示在屏幕上,这个过程就称为离屏渲染。

offscreen buffer同为内存中的一块连续区域。在对图片进行额外处理时用于存放中间合成数据的区域。

render

因此,不一定执行圆角操作(额外处理)就一定会触发离屏渲染,还需要image buffer暂存至offscreen buffer这一过程。

综上,离屏渲染触发条件有两个:

  • 图片(图层)需要额外处理
  • 数据需要暂存至offscreen buffer

如果图片进行额外处理时导致image buffer暂存至offscreen buffer,那么就会触发离屏渲染。可以理解为,图像额外处理过程较复杂,渲染流水线无法找到单次遍历就能完成渲染的算法,需要暂存中间数据至offscreen buffer,待所有操作处理完成后再传递至frame buffer。

即触发数据需要暂存至offscreen buffer的条件是:渲染流水线无法找到单次遍历就能完成渲染的算法,需要开辟新的内存区域(offscreen buffer)保存中间值。

offscreen

上下文(Context)

如果你使用过Qt的QPainter类进行过绘图的话,可能理解起来会简单一点,QPainter需要一个PaintDevice(绘图设备)参数,

而我们的QWidget,QImage,QPixmap就继承自它,因此可以这样使用:

1
2
Qimage img(400,400);
QPainter painter(&img);

也可以在QWidget的绘图函数中这样:

1
2
3
void paintEvent(QPaintEvent*){
QPainter painter(this);
}

上面的代码主要是创建了一个QPainter对象,并且设置了该对象操作的渲染设备,之后我们可以调用QPainter的各种成员方法在该设备进行绘图。

而OpenGL中进行绘图也需要一个这样的东西——Render Context (渲染上下文),简单来说,它也是一个绘图设备。回顾一下我们是如何在Qt中创建OpenGL窗口,首先创建一个QWidget,然后修改继承自QOpenGLWidget和QOpenGLFunctions,继承QOpenGLWidget是为了创建窗口,而继承QOpenGLFunctions只是为了让我们能够少写一点代码(你应该知道Qt把所有OpenGL函数封装为QOpenGLFunctions的成员函数),我们完全可以在QOpenGLWidget中创建一个 QOpenGLFunctions变量,只不过可能要这样来使用它:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Test:public QOpenGLWidget{

private:
QOpenGLFunctions func;

public:
void initializeGL(){
func.initializeOpenGLFunctions();
}
void paintGL(){
//...
func.glDrawArray(...);
}
}

我们调用了 func.initializeOpenGLFunctions();这个函数,它的作用是将func的渲染上下文对象设置为当前的上下文对象。

QOpenGLWidget中的 void initializeGL() / void paintGL()这两个函数,在调用之前会使用成员函数makeCurrent(),将自己的上下文设置整个程序的当前上下文,并且在函数调用结束会使用doneCurrent()将程序当前上下文设为空。

是不是感觉跟QPainter painter(this)一样,我们之后绘图只需要调用func的成员函数就行了,这样能理解起来是不是很简单。

API

OpenGL一般被认为是一个API(Application Programming Interface, 应用程序编程接口),包含了一系列可以操作图形、图像的函数。

OpenGL本身并不是一个API,它仅仅是一个由Khronos组织制定并维护的规范(Specification)。一般是由显卡生产商根据这个规范来实现OpenGL,OpenGL的存在使得各个类型的显卡可以使用同一套API进行图形开发

OpenGL规范严格规定了每个函数该如何执行,以及它们的输出值。至于内部具体每个函数是如何实现(Implement)的,将由OpenGL库的开发者自行决定(译注:这里开发者是指编写OpenGL库的人)。因为OpenGL规范并没有规定实现的细节,具体的OpenGL库允许使用不同的实现,只要其功能和结果与规范相匹配(亦即,作为用户不会感受到功能上的差异)。

实际的OpenGL库的开发者通常是显卡的生产商。你购买的显卡所支持的OpenGL版本都为这个系列的显卡专门开发的。当你使用Apple系统的时候,OpenGL库是由Apple自身维护的。在Linux下,有显卡生产商提供的OpenGL库,也有一些爱好者改编的版本。这也意味着任何时候OpenGL库表现的行为与规范规定的不一致时,基本都是库的开发者留下的bug。

由于OpenGL的大多数实现都是由显卡厂商编写的,当产生一个bug时通常可以通过升级显卡驱动来解决。这些驱动会包括你的显卡能支持的最新版本的OpenGL,这也是为什么总是建议你偶尔更新一下显卡驱动。

所有版本的OpenGL规范文档都被公开的寄存在Khronos那里。有兴趣的读者可以找到OpenGL3.3(我们将要使用的版本)的规范文档。如果你想深入到OpenGL的细节(只关心函数功能的描述而不是函数的实现),这是个很好的选择。如果你想知道每个函数具体的运作方式,这个规范也是一个很棒的参考。

核心模式与立即渲染模式

早期的OpenGL使用立即渲染模式(Immediate mode,也就是固定渲染管线),这个模式下绘制图形很方便。

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

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

现在的很多教程依旧使用的是早期OpenGL的立即渲染(固定渲染管线)模式,你可能见过,它就是通过gl_Begin(图元),glVertex3D(…)…来绘制一些基本图形。

而现代openGL使用的是核心模式,它是通过VAO,VBO…等,使用drawElements(…),或者drawArray(…)来绘制基本图形。

为什么要使用核心模式?

  • 性能方面,固定渲染管线的渲染比较慢,它在绘制图形的时候,由于顶点数据是存在CPU中,一次只能通过glVertex3D来发送一个顶点数据到GPU中,而核心模式是可以使用顶点数组来一次传输大量的顶点数据。
  • 应用方面,核心模式可以直接编写着色器程序,扩展性有了很大提升,很多特效的制作,依赖于着色器程序。

现在网上能找到的教程基本上都是两种模式混合使用,为了适应未来,本系列及子系列的所有文章都是用核心模式。

扩展

OpenGL的一大特性就是对扩展(Extension)的支持,当一个显卡公司提出一个新特性或者渲染上的大优化,通常会以扩展的方式在驱动中实现。如果一个程序在支持这个扩展的显卡上运行,开发者可以使用这个扩展提供的一些更先进更有效的图形功能。通过这种方式,开发者不必等待一个新的OpenGL规范面世,就可以使用这些新的渲染特性了,只需要简单地检查一下显卡是否支持此扩展。通常,当一个扩展非常流行或者非常有用的时候,它将最终成为未来的OpenGL规范的一部分。

使用扩展的代码大多看上去如下:

1
2
3
4
5
if(GL_ARB_extension_name){
// 使用硬件支持的全新的现代特性
}else{
// 不支持此扩展: 用旧的方式去做
}

状态机

OpenGL自身是一个巨大的状态机(State Machine):一系列的变量描述OpenGL此刻应当如何运行。OpenGL的状态通常被称为OpenGL上下文(Context)。我们通常使用如下途径去更改OpenGL状态:设置选项,操作缓冲。最后,我们使用当前OpenGL上下文来渲染。

假设当我们想告诉OpenGL去画线段而不是三角形的时候,我们通过改变一些上下文变量来改变OpenGL状态,从而告诉OpenGL如何去绘图。一旦我们改变了OpenGL的状态为绘制线段,下一个绘制命令就会画出线段而不是三角形。

当使用OpenGL的时候,我们会遇到一些状态设置函数(State-changing Function),这类函数将会改变上下文。以及状态使用函数(State-using Function),这类函数会根据当前OpenGL的状态执行一些操作。只要你记住OpenGL本质上是个大状态机,就能更容易理解它的大部分特性。

OpenGL的绘图方式有点类似于工厂的流水线,这个工厂有很多模式,我们只需更改流水线中的某个状态就可以在不同模式间进行切换。

对象

OpenGL库是用C语言写的,同时也支持多种语言的派生,但其内核仍是一个C库。由于C的一些语言结构不易被翻译到其它的高级语言,因此OpenGL开发的时候引入了一些抽象层。“对象(Object)”就是其中一个。

在OpenGL中一个对象是指一些选项的集合,它代表OpenGL状态的一个子集。比如,我们可以用一个对象来代表绘图窗口的设置,之后我们就可以设置它的大小、支持的颜色位数等等。可以把对象看做一个C风格的结构体(Struct):

1
2
3
4
5
struct object_name {
float option1;
int option2;
char[] name;
};

当我们使用一个对象时,通常看起来像如下一样(把OpenGL上下文看作一个大的结构体):

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

// 创建一个unsigned int对象,用于存储openGL对象的id
unsigned int objectId = 0;

// 此处创建opengl对象,并把这个对象的id赋值给[objectId]
glGenObject(1, &objectId);

// 将GL_WINDOW_TARGET状态的当前操作对象,设置为[objectId]所指的对象
glBindObject(GL_WINDOW_TARGET, objectId);

// 设置 GL_WINDOW_TARGET 的一些状态,本质上是对当前对象[objectId]设置状态
glSetObjectOption(GL_WINDOW_TARGET, GL_OPTION_WINDOW_WIDTH, 800);
glSetObjectOption(GL_WINDOW_TARGET, GL_OPTION_WINDOW_HEIGHT, 600);

// 通过将GL_WINDOW_TARGET状态的当前操作对象为0,进行解绑操作
glBindObject(GL_WINDOW_TARGET, 0);

这一小段代码展现了你以后使用OpenGL时常见的工作流。我们首先创建一个对象,然后用一个id保存它的引用(实际数据被储存在后台)。然后我们将对象绑定至上下文的目标位置(例子中窗口对象目标的位置被定义成GL_WINDOW_TARGET)。接下来我们设置窗口的选项。最后我们将目标位置的对象id设回0,解绑这个对象。设置的选项将被保存在objectId所引用的对象中,一旦我们重新绑定这个对象到GL_WINDOW_TARGET位置,这些选项就会重新生效。

目前提供的示例代码只是OpenGL如何操作的一个大致描述,通过阅读以后的教程你会遇到很多实际的例子。

使用对象的一个好处是在程序中,我们不止可以定义一个对象,并设置它们的选项,每个对象都可以是不同的设置。在我们执行一个使用OpenGL状态的操作的时候,只需要绑定含有需要的设置的对象即可。比如说我们有一些作为3D模型数据(一栋房子或一个人物)的容器对象,在我们想绘制其中任何一个模型的时候,只需绑定一个包含对应模型数据的对象就可以了(当然,我们需要先创建并设置对象的选项)。拥有数个这样的对象允许我们指定多个模型,在想画其中任何一个的时候,直接将对应的对象绑定上去,便不需要再重复设置选项了

立体坐标

纹理坐标


OpenGL名词解释
https://feater.top/opengl/opengl-keyword-explaination/
作者
JackeyLea
发布于
2022年3月11日
许可协议