什么是 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 版本历史变迁
GLSL 版本与 OpenGL 版本
OpenGL 2.0 的可编程管线,为了可编程,引入了着色器。其中片段着色器(fragment shader)允许用户自定义每个像素的颜色计算公式(故得名着色器),这个公式后来被人们称作 BRDF;而顶点着色器(vertex shader)允许用户自定义每个顶点的矩阵投影方式,修改顶点属性等。
古代 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 特有的,效率低,且灵活性差,难以自定义新的属性。
glMatrixMode
、glPushMatrix
和 glOrtho
等函数也是立即模式的 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 窗体管理库
GLFW 是配合 OpenGL 使用的轻量级工具程序库,缩写自 Graphics Library Framework(图形库框架)。GLFW 的主要功能是创建并管理窗口和 OpenGL 上下文,同时还提供了处理手柄、键盘、鼠标输入的功能。
注:实际上也支持创建 OpenGL ES 和 Vulkan 上下文。
glm - 仿 glsl 语法的数学矢量库
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 轴两条相互垂直的直线。
在这种坐标系下,要确定二维空间中的一个点,就需要 x, y 两个坐标。在计算机中,会用浮点数表示。
屏幕空间坐标的取值范围
由于计算机的显示器是一块固定大小的方形屏幕,x 和 y 坐标无法向两端无限延伸,是有范围的,不同的图形 API 对屏幕空间的 x 和 y 坐标范围规定也有所不同。OpenGL 中,规定 x 和 y 的取值为从 -1.0 到 1.0。x 轴正方向向右,y 轴正方向向上,屏幕中心为原点。
注意到我们计算机的显示器往往不是正方形的,通常是 16:9 的横向长方形(对于手机则是纵向长方形),这种情况下 x 和 y 坐标取值依然不变,还是 -1.0 到 1.0。只是整体被压扁了。
空间离散化
显示器有一个重要参数叫做分辨率,例如显示器的分辨率是 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" 的说法。
Z 方向
OpenGL 规定朝向屏幕里面为 z 轴正方向。z 坐标越小的值越靠近人眼或者摄像头。
这是为了服务 OpenGL 的"深度测试"(depth test)功能,z 坐标也叫"深度",点的 z 越大说明他"陷入屏幕越深",也就是离人眼更远。
深度测试
由于光学规律,远处的物体会被近处的物体遮挡,OpenGL 为了模拟现实中的这种遮挡效果,会给每个像素除了 RGB 值外,额外加一个深度值的信息。
每次绘制一个点时,会检测当前点的 z 值是否小于缓冲中的深度值,如果小于等于则成功绘制,覆盖旧物体;否则认为该点被前景遮挡,放弃绘制新物体。
这种格式的图像又称 RGBD 图,在计算机视觉中非常有用,由于现有的 png 格式支持 alpha 透明度通道,深度值 D 通常会夺舍保存到 alpha 通道里。
深度图
所有像素点的深度可以组成一副新的图像,称为深度图。启用深度测试后,OpenGL 缓冲区不仅包含颜色信息,还包含一张深度图,用于剔除遮挡在前景后方的物体。
没有深度缓冲,图形的相互覆盖只能由绘制先后顺序决定,后来者居上。
而启用了深度测试后,会根据像素点的 z 坐标来判断谁覆盖谁,例如图中的宇航员会覆盖他背后的月球表面,因为宇航员离摄像头更近,z 坐标更小。
屏幕空间的裁剪
如果画了一个 x 或 y 坐标超出 [-1, 1] 区间的点,OpenGL 则会认为他已经不可能显示在屏幕上,所以会把这个点裁剪(clip)掉,不予显示,也不会对他进行任何着色器计算。
z 坐标超过 -1 到 1 范围的点会被"裁剪"掉。因为 OpenGL 的深度缓冲是有限精度的,通常是 24 位无符号整数,把 0 到 2^24 - 1 映射到 z 浮点坐标的 -1 到 1,如果 z 浮点坐标无限制,那么定点量化的深度缓冲就容纳不下了。
不同图形 API 规定的 Z 方向不同
世界空间坐标系
三维空间同样有笛卡尔坐标系,确定三维空间中的一个点,就需要 x, y, z 三个坐标。
绘制各种图形
三维世界到二维屏幕的转换
无论我们想要表现的图形空间是多么丰富多彩的三维世界,最终都需要投影到二维的屏幕上呈现给用户欣赏。
在三维世界坐标和二维屏幕(准确的说还是三维屏幕,其中 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 支持多重绘图模式:
点 GL_POINTS
线 GL_LINES
面 GL_TRIANGLES
我们可以用 glBegin(GL_xxx)
来指定一个绘图模式,然后通过 glVertex3f
指定这种模式下的一些顶点坐标,最后调用 glEnd()
结束绘制。
glBegin(GL_POINTS);
glVertex3f(0.0f, 0.0f, 0.0f);
glEnd();
默认只有一个像素。
glPointSize(64.0f);
glBegin(GL_POINTS);
glVertex3f(0.0f, 0.0f, 0.0f);
glEnd();
画一个直径 64 个像素,圆形的点
glEnable
函数可以启用 OpenGL 的一些特性,例如 GL_POINT_SMOOTH 会让点的形状变成圆形的(而不是默认的正方形)。
glEnable(GL_POINT_SMOOTH);
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);
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();
画三角形
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();
三角形的每个顶点若用 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();
单一颜色填充
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();
画多个三角形
要画 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();
画一个圆
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();
}
用一系列三角形实心地填充这个圆
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();
取点密度提升,用 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();
让圆半径为 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();
画一个扇形——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();
画一个同心圆
用 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();
作业
画一下 OpenCV 的 logo 吧!
答案
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());
}