GLFW入门教程02.02:空白界面

目的

显示一个最基本的窗口界面。

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

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

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

上下文(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的成员函数就行了,这样能理解起来是不是很简单。

状态机

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

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

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

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

油画算法/画家算法

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

painter

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

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

源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include <stdio.h>
#include <GLFW/glfw3.h>

static void glfw_error_callback(int error, const char* description)
{
fprintf(stderr, "Glfw Error %d: %s\n", error, description);
}

int main(int, char**)
{
// Setup window
glfwSetErrorCallback(glfw_error_callback);
if (!glfwInit())
return 1;

GLFWwindow* window = glfwCreateWindow(1280, 720, "GLFW Window", NULL, NULL);
if (window == NULL)
return 1;

glfwMakeContextCurrent(window);
glfwSwapInterval(1); // Enable vsync

// Main loop
while (!glfwWindowShouldClose(window))
{
// Poll and handle events (inputs, window resize, etc.)
glfwPollEvents();

int display_w, display_h;
glfwGetFramebufferSize(window, &display_w, &display_h);
glViewport(0, 0, display_w, display_h);
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);

//display func

glfwMakeContextCurrent(window);
glfwSwapBuffers(window);
}

glfwDestroyWindow(window);
glfwTerminate();

return 0;
}

此代码将作为后续开发的模板。

主要流程为

flowchart TB

A(Start) --> B[初始化]
B --> C[创建窗口]
C --> D[上下文设置]
D --> E[显示循环]
E --> F[释放资源]
F --> G(End)

函数调用图为

flowchart TB

A(Start) --> B[glfwInit]
B --> C[glfwCreateWindow]
C --> D[glfwMakeContextCurrent]
D --> E{glfwWindowShouldClose}
E --No--> F[glfwPollEvents]
F --> G[glfwGetFramebufferSize]
G --> H[glViewport]
H --> I[glClearColor]
I --> J[glClear]
J --> L[glfwMakeContextCurrent]
L --> M[glfwSwapBuffers]
M --> E
E --Yes--> O[glfwDestroyWindow]
O --> P[glfwTerminate]
P --> Q(End)

编译

1
2
all:
gcc main.c -o window -lglfw -lGL

结果

窗口


GLFW入门教程02.02:空白界面
https://blog.jackeylea.com/glfw/simple-window-of-glfw/
作者
JackeyLea
发布于
2024年8月4日
许可协议