什么是 OpenGL

为了调用 GPU 硬件进行高效的光栅化渲染,绘制图形,我们需要使用一些接口,来命令 GPU 帮助我们进行各种图形的绘制操作。

OpenGL(Open Graphics Library,开放图形库或者”开放式图形库“) 是用于渲染 2D、3D 矢量图形的跨语言、跨平台的应用程序编程接口(API)。这个接口由近 350 个不同的函数调用组成,用来从简单的二维图形绘制到复杂的三维景象。

OpenGL 的版本与驱动支持

就和 C++ 语言一样,OpenGL 的 API 是有许多版本的,每个版本 API 的制定,是由一个名叫 Khronos 的组织发布的。和 C++ 的 ISOCpp 一样,他们只负责制定 API,不负责实现,由各大硬件厂商的驱动来实现。

一般来说新版本都会尽量保留老的函数,但是 OpenGL 2.0 到 3.0 的跳跃是个例外,废除了大量的立即模式专用的一些过于迂腐的老函数,那些在引入新函数的同时依然兼容 2.0 老函数的称为兼容配置(compatible profile),而只保留 3.0 中那些现代 API 函数的则称为核心配置(core profile),意思是只保留核心功能。

OpenGL 版本历史变迁

版本

管线

模式

OpenGL 1.0

固定管线

立即模式

OpenGL 2.0

可编程管线

立即模式

OpenGL 3.0

可编程管线

现代模式

OpenGL 4.0

可编程管线

现代模式

GLSL 版本与 OpenGL 版本

OpenGL 2.0 的可编程管线,为了可编程,引入了着色器。其中片段着色器(fragment shader)允许用户自定义每个像素的颜色计算公式(故得名着色器),这个公式后来被人们称作 BRDF;而顶点着色器(vertex shader)允许用户自定义每个顶点的矩阵投影方式,修改顶点属性等。

GLSL

OpenGL

1.10

2.0

1.20

2.1

1.30

3.0

1.40

3.1

1.50

3.2

3.30

3.3

4.00

4.0

4.10

4.1

4.20

4.2

4.30

4.3

4.40

4.4

4.50

4.5

古代 OpenGL

OpenGL 的接口(API)是有许多版本的。OpenGL 2.0 及以前的版本,称之为古代 OpenGL,例如:

glBegin(GL_POINTS);
glVertex3f(0.0f, 0.5f, 0.0f);
glVertex3f(0.5f, 0.0f, 0.0f);
glEnd();

由于我们是一个一个函数调用来传入顶点的坐标,颜色等信息,这种模式下的 OpenGL 又被称为立即模式(intermediate mode),立即模式是古代 OpenGL 特有的,效率低,且灵活性差,难以自定义新的属性

glMatrixModeglPushMatrixglOrtho 等函数也是立即模式的 API 之一。

现代 OpenGL

OpenGL 3.0 及以后的版本,称之为现代 OpenGL,例如:

glGenVertexArrays(1, &vao);
glBindVertexArrays(vao);

特点是有非常多的各种 VAO,VBO,FBO,各种对象及其绑定(bind)操作。

安装 OpenGL

Windows 安装 OpenGL

安装 DirectX 运行时的同时会安装 OpenGL 驱动,只要安装了 DirectX 就安装了 OpenGL。

会在 C:\Windows\System32 中出现一个 opengl32.dll,这个就是 OpenGL 运行时,程序启动后会调用 LoadLibrary("opengl32.dll") 加载这个文件,并用 GetProcAddress 加载其中的 gl 函数。该 dll 文件只是运行时库,光这个文件没用,需要同时具有物理显卡驱动才能运行。

如果你没有显卡,想用低效的 mesa 软光栅,可以从网上下载另一个名叫 opengl32sw.dll 的文件,重命名以后覆盖这个 opengl32.dll 即可,但这样你将无法再使用物理显卡的硬件加速光栅化。

Linux 安装 OpenGL

Ubuntu:

sudo apt-get install libglu1-mesa-dev freeglut3-dev mesa-common-dev

Arch Linux:

sudo pacman -S freeglut glu libglvnd mesa glfw glm

OpenGL 头文件

古代 OpenGL 往往用这个系统自带的头文件:

#include <GL/gl.h>

这里面包含的都是些 OpenGL 2.0 以前版本的老函数,而不是具有现代 OpenGL 3.0 以上的新函数,如果我们想要用现代 OpenGL,就不能用这个头文件。

现代 OpenGL 没有头文件,而是必须使用 LoadLibraryA 手动去 OpenGL 的 DLL 里一个个加载那些函数,如果我们每用一个函数就得手动加载一下函数,那该多痛苦呀!

因此,有了 glad 和 glew 这样的第三方库,他们会在启动时加载所有的 OpenGL 函数,放到全局函数指针中去,这样当你使用时,就好像在调用一个普通的函数一样。

因此为了现代 OpenGL,我们必须选择一个 API 加载器,本课程选用了 glad,当你需要 OpenGL 函数时,不是去导入 <GL/gl.h> ,而是导入 <glad/glad.h> 作为替代:

#include <glad/glad.h>

glad 坑点

如果用 glad.h,就不要再导入古代的 gl.h 头文件了:

#include <GL/gl.h>  // 会和 glad 冲突

此外,glad 头文件必须在其他 gl 相关库的前面,例如:

#include <glad/glad.h>
#include <GLFW/glfw3.h>

建议自己的 glcommon.h 头文件里写这两行,然后用 gl 和 glfw 就导入这个 glcommon.h。

glad 介绍

glad 实际上是一个 python 包,他所做的是根据你指定的版本,生成加载 OpenGL 全部函数的 glad.c 和头文件 glad.h。

python -m pip install glad
​
python -m glad --out-path . --generator c --api gl=4.6 --profile compatibility

compatibility 是兼容版本。

glfw - 跨平台的 OpenGL 窗体管理库

https://github.com/glfw/glfw

GLFW 是配合 OpenGL 使用的轻量级工具程序库,缩写自 Graphics Library Framework(图形库框架)。GLFW 的主要功能是创建并管理窗口和 OpenGL 上下文,同时还提供了处理手柄、键盘、鼠标输入的功能。

注:实际上也支持创建 OpenGL ES 和 Vulkan 上下文。

glm - 仿 glsl 语法的数学矢量库

https://github.com/g-truc/glm

glm 和 glsl 的语法如出一辙,学会了 glsl 等于学会了 glm。此外,glm 还支持 SIMD,支持 CUDA。

术语

API:opengl、opengl es(嵌入式平台)、vulkan、metal、directx;

OpenGL 模式:compatible(兼容老功能)、core(只保留新功能);

上下文创建:wgl(Wendous)、glx(Linux)、egl(嵌入式平台);

窗口创建:glfw、glut;

API 加载器:glad、glew;

数学矢量库:glm;

着色器语言:glsl。

OpenGL 样板代码

初始化 GLFW

glfwInit();     // 初始化 GLFW 库

加上错误处理

if(!glfwInit()) {
  throw std::runtime_error("failed to initialize GLFW");
}

诸如 glfwInit 这样的 C 语言函数都约定,返回非 0 值表示正确。

创建一个窗口

GLFWwindow* window = glfwCreateWindow(640, 480, "Example", nullptr, nullptr);
if(!window) {
  glfwTerminate();
  throw std::runtime_error("GLFW failed to create window");
}

glfwMakeContextCurrent(window);	// 设置上下文

初始化 GLAD

有了上下文以后,就可以初始化 GLAD 这个库了。

// 初始化 GLAD,加载函数指针
gladLoadGL();

由于使用 glad/glad.h 而不是 GL/gl.h,必须先 gladLoadGL 后才能正常使用 gl 函数,如果不先 gladLoadGL 的话,gl 函数就还是空指针,试图调用他们就会直接崩溃。

注意:gladLoadGL 必须在 glfwMakeContextCurrent 之后,因为 GLAD 的初始化也是需要调用 gl 函数的,需要一个 OpenGL 上下文。

if(!gladLoadGL()) {
  glfwTerminate();    // 由于 glfwInit 在前,理论上是需要配套的 glfwTerminate 防止泄露
  throw std::runtime_error("GLAD failed to load GL functions");
}
​
printf("OpenGL version:", glGetString(GL_VERSION));  // 初始化完毕,打印一下版本号

检测窗口是否已经关闭

glfwInit();
glfwMakeContextCurrent(window);
gladLoadGL();

while(!glfwWindowShouldClose(window)) {
  // 画图
}

如果点击了 window 上的关闭按钮,glfwWindowShouldClose 就会返回 true。

拉去”最新事件“

加上了 glfwWindowShouldClose 判断也不会在点击关闭按钮时退出,因为 glfw 窗口有一个事件(event)系统,事件是从操作系统队列中取得的,如果不去主动获取,那么就不会知道一个"点击关闭按钮"的事件发生,因此我们必须在每次循环结束后使用 glfwPollEvents 从操作系统那里获取"最新事件",才能让 glfwWindowShouldClose 生效。

while(!glfwWindowShouldClose(window)) {
  // 画图
  glfwPollEvents();
}

画图指令提交

gl 的画图函数为了高效,是有一个命令队列的,只有在调用 glFlush 时,才会把命令提交到驱动中去,并在窗口中显示。

while(!glfwWindowShouldClose(window)) {
  // 画图
  glFlush();
  glfwPollEvents();
}

以前的窗口都是单缓冲的,由于时代的进步,现在的 glfw 创建窗口默认都会创建双缓冲的窗口,需要调用 glfwSwapBuffers 代替 glFlush

while(!glfwWindowShouldClose(window)) {
  // 画图
  glfwSwapBuffers(window);
  glfwPollEvents();
}

一般需要使用双缓冲(double buffer)的地方都是由于"生产者"和"消费者"供需不一致所造成的。在 OpenGL 中,生产者是我的画图函数,而消费者是显式图形的窗口。

空间坐标系

屏幕是二维的。计算机图形学普遍采用的是笛卡尔坐标系,其具有 x 轴和 y 轴两条相互垂直的直线。

1-zvew.webp

在这种坐标系下,要确定二维空间中的一个点,就需要 x, y 两个坐标。在计算机中,会用浮点数表示。

屏幕空间坐标的取值范围

由于计算机的显示器是一块固定大小的方形屏幕,x 和 y 坐标无法向两端无限延伸,是有范围的,不同的图形 API 对屏幕空间的 x 和 y 坐标范围规定也有所不同。OpenGL 中,规定 x 和 y 的取值为从 -1.0 到 1.0。x 轴正方向向右,y 轴正方向向上,屏幕中心为原点。

2-lwkh.webp

注意到我们计算机的显示器往往不是正方形的,通常是 16:9 的横向长方形(对于手机则是纵向长方形),这种情况下 x 和 y 坐标取值依然不变,还是 -1.0 到 1.0。只是整体被压扁了。

3-jmzw.webp

空间离散化

显示器有一个重要参数叫做分辨率,例如显示器的分辨率是 1920 * 1080,意味着宽度(x方向)上有 1920 个像素点,高度(y方向)上有 1080 个像素点。

OpenGL 在绘制一个点时,就把你提供的浮点坐标 (x, y) 转换成像素的坐标:

round(x * 1920), round(y * 1080)

其中 round 是取整函数,求最接近的整数。

颜色离散化

如果你近距离观察显示器,就会发现他实际上由一个个像素点组成,每一个像素点又都由红、绿、蓝三个 LED 灯组成,计算机通过调节三个 LED 灯不同亮度的组合,从远处看,就好像有各种不同的颜色一样。

这三个灯的亮度有 256 个档位可供调节,刚好对应于 8 位无符号整数 unsigned char 的表示范围 [0, 255],因此会有"颜色就是 RGB","RGB 就是三个 uchar" 的说法。

4-nbbg.webp

Z 方向

OpenGL 规定朝向屏幕里面为 z 轴正方向。z 坐标越小的值越靠近人眼或者摄像头。

这是为了服务 OpenGL 的"深度测试"(depth test)功能,z 坐标也叫"深度",点的 z 越大说明他"陷入屏幕越深",也就是离人眼更远。

深度测试

由于光学规律,远处的物体会被近处的物体遮挡,OpenGL 为了模拟现实中的这种遮挡效果,会给每个像素除了 RGB 值外,额外加一个深度值的信息。

每次绘制一个点时,会检测当前点的 z 值是否小于缓冲中的深度值,如果小于等于则成功绘制,覆盖旧物体;否则认为该点被前景遮挡,放弃绘制新物体。

这种格式的图像又称 RGBD 图,在计算机视觉中非常有用,由于现有的 png 格式支持 alpha 透明度通道,深度值 D 通常会夺舍保存到 alpha 通道里。

深度图

所有像素点的深度可以组成一副新的图像,称为深度图。启用深度测试后,OpenGL 缓冲区不仅包含颜色信息,还包含一张深度图,用于剔除遮挡在前景后方的物体。

5-bkdo.webp

没有深度缓冲,图形的相互覆盖只能由绘制先后顺序决定,后来者居上。

而启用了深度测试后,会根据像素点的 z 坐标来判断谁覆盖谁,例如图中的宇航员会覆盖他背后的月球表面,因为宇航员离摄像头更近,z 坐标更小。

6-bibx.webp

屏幕空间的裁剪

如果画了一个 x 或 y 坐标超出 [-1, 1] 区间的点,OpenGL 则会认为他已经不可能显示在屏幕上,所以会把这个点裁剪(clip)掉,不予显示,也不会对他进行任何着色器计算。

7-arni.webp

z 坐标超过 -1 到 1 范围的点会被"裁剪"掉。因为 OpenGL 的深度缓冲是有限精度的,通常是 24 位无符号整数,把 0 到 2^24 - 1 映射到 z 浮点坐标的 -1 到 1,如果 z 浮点坐标无限制,那么定点量化的深度缓冲就容纳不下了。

8-kqlb.webp

不同图形 API 规定的 Z 方向不同

9-wijw.webp

世界空间坐标系

三维空间同样有笛卡尔坐标系,确定三维空间中的一个点,就需要 x, y, z 三个坐标。

10-adtt.webp

绘制各种图形

三维世界到二维屏幕的转换

无论我们想要表现的图形空间是多么丰富多彩的三维世界,最终都需要投影到二维的屏幕上呈现给用户欣赏。

在三维世界坐标和二维屏幕(准确的说还是三维屏幕,其中 z 坐标用于深度检测)之间转换的,是一个矩阵,下一课中详细介绍投影矩阵等知识。

目前我们只考虑二维图形的绘制,在默认的配置下(没有手动指定过任何矩阵),OpenGL 会把我们输入的世界坐标直接当作屏幕空间坐标。

指定一个顶点坐标

要画一个点,可以用 glVertex3f 函数,他有三个参数,分别是你要画点的 x, y, z 坐标。

由于目前我们只是在二维屏幕上画图,也暂时不打算启用深度测试,z 坐标暂时没有意义,可以始终为 0。默认的配置下(也没有指定过矩阵),OpenGL 会把世界空间坐标直接当作屏幕空间坐标。

glVertex3f(0.0f, 0.0f, 0.0f);

在 (0, 0, 0) 坐标出绘制一个点,也就是在屏幕的中央绘制一个点。但是 0.0 这样的写法并不严谨,glVertex3f 的参数是 float 类型的,而 C 语言中带小数点的常量(如 3.14)默认是 double 类型的,此处发生了隐式类型转换,为了防止被转换(以及影响 auto 推导),建议在每个带小数点的常量后面加一个 f 后缀(如 3.14f),这样 C 语言就会把他视为 float 类型常数而不是 double 类型常数。

画一个点

光靠 glVertext3f 指定一个顶点坐标还没有用。OpenGL 支持多重绘图模式:

  1. 点 GL_POINTS

  2. 线 GL_LINES

  3. 面 GL_TRIANGLES

我们可以用 glBegin(GL_xxx) 来指定一个绘图模式,然后通过 glVertex3f 指定这种模式下的一些顶点坐标,最后调用 glEnd() 结束绘制。

glBegin(GL_POINTS);
glVertex3f(0.0f, 0.0f, 0.0f);
glEnd();

11-quyv.webp

默认只有一个像素。

glPointSize(64.0f);
glBegin(GL_POINTS);
glVertex3f(0.0f, 0.0f, 0.0f);
glEnd();

12-rpnn.webp

画一个直径 64 个像素,圆形的点

glEnable 函数可以启用 OpenGL 的一些特性,例如 GL_POINT_SMOOTH 会让点的形状变成圆形的(而不是默认的正方形)。

glEnable(GL_POINT_SMOOTH);
glPointSize(64.0f);
glBegin(GL_POINTS);
glVertex3f(0.0f, 0.0f, 0.0f);
glEnd();

13-bscp.webp

启用抗锯齿

glEnable(GL_POINT_SMOOTH);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glPointSize(64.0f);
glBegin(GL_POINTS);
glVertex3f(0.0f, 0.0f, 0.0f);
glEnd();

状态机模型

一次设置,永久生效,直到再次设置或取消。

glEnable(GL_POINT_SMOOTH);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glPointSize(64.0f);

这些东西可以放在 main 函数里,因为 OpenGL 使用的是"状态机模型",任何东西只需要设置一次,以后一直保持这个设置不变,直到你再调用或者 glDisable

glfwMakeContextCurrent(window);
​
if(!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) {
  glfwTerminate();
  std::cerr << "GLAD failed to load GL functions\n";
  return -1;
}
​
std::cerr << "OpenGL version: " << glGetString(GL_VERSION) << '\n';
​
CHECK_GL(glClear(GL_COLOR_BUFFER_BIT));
CHECK_GL(glEnable(GL_POINT_SMOOTH));
CHECK_GL(glEnable(GL_BLEND));
CHECK_GL(glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA));
CHECK_GL(glPointSize(64.0f));
​
while(!glfwWindowShouldClose(window)) {
  render();
  
  glwSwapBuffers(window);
  glfwPollEvents();
}

每个顶点指定不同的颜色

每个顶点可以通过 glColor3f 分别指定颜色,以 (R, G, B) 三个 0 到 1 之间的浮点数来指定:

glBegin(GL_POINTS);
glColor3f(1.0f, 0.0f, 0.0f);
glVertex3f(0.0f, 0.5f, 0.0f);
glColor3f(0.0f, 1.0f, 0.0f);
glVertext3f(-0.5f, -0.5f, 0.0f);
glColor3f(1.0f, 1.0f, 0.0f);
glVertex3f(0.5f, -0.5f, 0.0f);
glEnd();

14-zdak.webp

画三角形

GL_POINTS 把每个坐标当作单独的点来绘制,而 GL_TRIANGLES 则是把三个坐标点连成一个三角形来绘制:

glBegin(GL_TRIANGLES);
glVertex3f(0.0f, 0.5f, 0.0f);
glVertex3f(-0.5f, -0.5f, 0.0f);
glVertex3f(0.5f, -0.5f, 0.0f);
glEnd();

15-nrgy.webp

三角形的每个顶点若用 glColor3f 指定了颜色,则三角形会呈现渐变的效果。

glBegin(GL_TRIANGLES);
glColor3f(1.0f, 0.0f, 0.0f);
glVertex3f(0.0f, 0.5f, 0.0f);
glColor3f(0.0f, 1.0f, 0.0f);
glVertext3f(-0.5f, -0.5f, 0.0f);
glColor3f(1.0f, 1.0f, 0.0f);
glVertex3f(0.5f, -0.5f, 0.0f);
glEnd();

16-ywcu.webp

单一颜色填充

glBegin(GL_TRIANGLES);
glColor3f(0.5f, 0.5f, 1.0f);
gVertex3f(0.0f, 0.5f, 0.0f);
glVertex3f(-0.5f, -0.5f, 0.0f);
glVertex3f(0.5f, -0.5f, 0.0f);
glEnd();

17-nxme.webp

画多个三角形

要画 n 个三角形则指定 3n 个顶点坐标即可。GL_TRIANGLES 可以绘制多个三角形,但要求 glBegin 和 glEnd 之间的 glVertex3f 调用的数量必须是 3 的整数倍。

glBegin(GL_TRIANGLES);
glVertex3f(0.0f, 0.0f, 0.0f);
glVertex3f(0.0f, 1.0f, 0.0f);
glVertex3f(1.0f, 0.0f, 0.0f);
glVertex3f(-0.5f, 0.0f, 0.0f);
glVertex3f(-1.0f, -1.0f, -0.0f);
glVertex3f(0.0f, -1.0f, 0.0f);
glEnd();

18-tyiv.webp

画一个圆

static void render() {
  glBegin(GL_POINTS);
  constexpr int n = 20;
  constexpr float pi = 3.1415926535897f;
  
  for(int i = 0; i < n; ++i) {
    float angle = i / (float)n * pi * 2;
    glVertex3f(sinf(angle), cosf(angle), 0.0f);
  }
  
  glEnd();
}

19-kfdm.webp

用一系列三角形实心地填充这个圆

glBegin(GL_TRIANGLES);
constexpr int n = 20;
constexpr float pi = 3.1415926535897f;
for(int i = 0; i < n; ++i) {
  float angle = i / (float)n * pi * 2;
  float angle_next = (i + 1) / (float)n * pi * 2;
  glVertex3f(0.0f, 0.0f, 0.0f);
  glVertex3f(sinf(angle), cosf(angle), 0.0f);
  glVertex3f(sinf(angle_next), cosf(angle_next), 0.0f);
}
glEnd();

20-wgpk.webp

取点密度提升,用 100 边形画近似圆

glBegin(GL_TRIANGLES);
constexpr int n = 100;
constexpr float pi = 3.1415926535897f;
for(int i = 0; i < n; ++i) {
  float angle = i / (float)n * pi * 2;
  float angle_next = (i + 1) / (float)n * pi * 2;
  glVertex3f(0.0f, 0.0f, 0.0f);
  glVertex3f(sinf(angle), cosf(angle), 0.0f);
  glVertex3f(sinf(angle_next), cosf(angle_next), 0.0f);
}
glEnd();

21-lymx.webp

让圆半径为 0.5(单位:半屏宽度)

glBegin(GL_TRIANGLES);
constexpr int n = 100;
constexpr float pi = 3.1415926535897f;
float radius = 0.5f;
for(int i = 0; i < n; ++i) {
  float angle = i / (float)n * pi * 2;
  float angle_next = (i + 1) / (float)n * pi * 2;
  glVertex3f(0.0f, 0.0f, 0.0f);
  glVertex3f(radius * sinf(angle), radius * cosf(angle), 0.0f);
  glVertex3f(radius * sinf(angle_next), radius * cosf(angle_next), 0.0f);
}
glEnd();

22-uigb.webp

画一个扇形——1/3 个圆

glBegin(GL_TRIANGLES);
constexpr int n = 100;
constexpr float pi = 3.1415926535897f;
float radius = 0.5f;
for(int i = 0; i < n / 3; ++i) {
  float angle = i / (float)n * pi * 2;
  float angle_next = (i + 1) / (float)n * pi * 2;
  glVertex3f(0.0f, 0.0f, 0.0f);
  glVertex3f(radius * sinf(angle), radius * cosf(angle), 0.0f);
  glVertex3f(radius * sinf(angle_next), radius * cosf(angle_next), 0.0f);
}
glEnd();

23-faea.webp

画一个同心圆

用 100 个四边形拼接在一起即可

glBegin(GL_TRIANGLES);
constexpr int n = 100;
constexpr float pi = 3.1415926535897f;
float radius = 0.5f;
float inner_radius = 0.25f;

for(int i = 0; i < n; ++i) {
  float angle = i / (float)n * pi * 2;
  float angle_next = (i + 1) / (float)n * pi * 2;
  glVertex3f(radius * sinf(angle), radius * cosf(angle), 0.0f);
  glVertex3f(radius * sinf(angle_next), radius * cosf(angle_next), 0.0f);
  glVetext3f(inner_radius * sinf(angle), inner_radius * cosf(angle), 0.0f);
  
  glVertex3f(inner_radius * sinf(angle_next), inner_radius * cosf(angle_next), 0.0f);
  glVertex3f(inner_radius * sinf(angle), inner_radius * cosf(angle), 0.0f);
  glVertex3f(radius * sinf(angle_next), radius * cosf(angle_next), 0.0f);
}

glEnd();

24-dpne.webp

作业

画一下 OpenCV 的 logo 吧!

25-vooe.webp

答案

1-ulqs.webp

static void paint_sector(int start, int end, float x, float y) {
    
    constexpr float pi = 3.1415926535897f;
    constexpr int n = 100;
    float radius = 0.4f;
    float inner_radius = 0.15f;

    for (; start < end; ++start) {
        float angle = start / (float)n * pi * 2;
        float angle_next = (start + 1) / (float)n * pi * 2;
        glVertex3f(radius * sinf(angle) + x, radius * cosf(angle) + y, 0.0f);
        glVertex3f(radius * sinf(angle_next) + x, radius * cosf(angle_next) + y, 0.0f);
        glVertex3f(inner_radius * sinf(angle) + x, inner_radius * cosf(angle) + y, 0.0f);

        glVertex3f(inner_radius * sinf(angle_next) + x, inner_radius * cosf(angle_next) + y, 0.0f);
        glVertex3f(inner_radius * sinf(angle) + x, inner_radius * cosf(angle) + y, 0.0f);
        glVertex3f(radius * sinf(angle_next) + x, radius * cosf(angle_next) + y, 0.0f);
    }
}

static void render() {
    glBegin(GL_TRIANGLES);
    glColor3f(1.0f, 0.0f, 0.0f);
   
    paint_sector(60, 100, 0.0f, 0.35f);
    paint_sector(0, 40, 0.0f, 0.35f);

    glColor3f(0.0f, 0.0f, 1.0f);
    paint_sector(10, 50, 0.515f, -0.4f);
    paint_sector(50, 90, 0.515f, -0.4f);

    glColor3f(0.0f, 1.0f, 0.0f);
    paint_sector(0, 10, -0.515f, -0.4f);
    paint_sector(25, 100, -0.515f, -0.4f);

    CHECK_GL(glEnd());
}