OpenGL 4.6 без glBind: Direct State Access на C++23

Почти каждый, кто учил OpenGL, начинал с одного и того же ритуала: glGenBuffers, glBindBuffer, glBufferData. Чтобы изменить объект, его сначала нужно привязать. Так написаны самые известные учебники, потому что они рассчитаны на OpenGL 3.3.
В OpenGL 4.5 этот ритуал стал необязательным. В стандарт вошёл Direct State Access, сокращённо DSA: функции, которые получают объект прямо в аргументе и ничего не привязывают. В статье я соберу на DSA квадрат с текстурой и индексами и по дороге покажу схемами, что происходит внутри. Код написан на C++23: std::expected, std::print, std::mdspan и несколько диапазонов оказались здесь к месту.
Всё, что есть в статье, собрано компилятором MSVC из Visual Studio 2026 с флагом /W4 без предупреждений и запущено на OpenGL 4.6. Вот что рисует программа:

Привяжи, потом меняй
Напомню, как выглядит заполнение буфера вершин в стиле OpenGL 3.3:
GLuint vbo = 0;
glGenBuffers(1, &vbo);
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);Посмотрите на последнюю строку. В вызове glBufferData нет переменной vbo. Функция получает константу GL_ARRAY_BUFFER, а это не буфер, а название гнезда в контексте OpenGL. Контекст хранит таблицу таких гнёзд, и в каждом гнезде записано имя одного объекта. Функция glBufferData заглядывает в гнездо GL_ARRAY_BUFFER и меняет тот буфер, имя которого там сейчас записано.
Получается, что строка кода не говорит, какой объект она изменит. Ответ зависит от того, какой glBindBuffer выполнился последним, а этот вызов может стоять в другой функции и в другом файле.
Пока программа состоит из одного main, такое неудобство незаметно. Оно становится ошибкой, когда появляются свои функции. Вот типичный случай с текстурами:

Программа привязала текстуру стены, чтобы настроить ей фильтрацию. Между привязкой и настройкой она вызвала loadIcon, а функция loadIcon внутри привязывает свою текстуру, потому что по-другому загрузить картинку в OpenGL 3.3 нельзя. После возврата в гнезде GL_TEXTURE_2D записана иконка, и настройка фильтрации достаётся иконке.
OpenGL об ошибке не сообщит: все три вызова правильные. Чтобы защититься, каждую такую функцию приходится писать по схеме «запомни, что было в гнезде, сделай своё, верни как было», то есть перед работой вызывать glGetIntegerv(GL_TEXTURE_BINDING_2D, …).
Есть и вторая известная ловушка. Гнездо GL_ELEMENT_ARRAY_BUFFER принадлежит не контексту, а привязанному сейчас VAO. Поэтому аккуратная на вид строка glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0), выполненная до glBindVertexArray(0), отцепляет буфер индексов от VAO, и glDrawElements потом рисует пустоту. Запомнить это правило можно, но вывести его из кода нельзя.
DSA: имя объекта первым аргументом
Идея Direct State Access помещается в одну строку: функция получает имя объекта первым аргументом и работает с этим объектом, а гнёзда не читает и не меняет.

Тот же буфер вершин на DSA:
GLuint vbo = 0;
glCreateBuffers(1, &vbo);
glNamedBufferStorage(vbo, sizeof(vertices), vertices, 0);Вызова glBindBuffer нет, константы GL_ARRAY_BUFFER тоже нет. Буферу вообще не нужно знать, будет он хранить вершины, индексы или данные для шейдера: назначение появляется позже, когда буфер подключают к VAO.
Функция loadIcon из примера выше на DSA не трогает ни одного гнезда. Её можно вызвать в любом месте программы, и состояние контекста останется прежним.
glCreate вместо glGen
Первое отличие в коде — glCreateBuffers на месте glGenBuffers. Разница не только в названии.

Функция glGenBuffers выдаёт свободное имя, то есть число, и больше ничего не делает. Самого объекта ещё нет: OpenGL создаёт объект в момент первой привязки. В старом стиле это не мешало, потому что привязка всё равно шла следующей строкой.
DSA ничего не привязывает, поэтому объект должен существовать сразу. Функции семейства glCreate* выдают имя и тут же создают объект.
Смешивать два стиля нельзя. Я проверил: если получить имя через glGenBuffers и сразу передать это имя в glNamedBufferStorage, вызов завершается ошибкой GL_INVALID_OPERATION, и память буферу не выделяется. То же самое происходит с glGenTextures и glTextureParameteri.
У каждого вида объектов есть своя создающая функция: glCreateBuffers, glCreateVertexArrays, glCreateTextures, glCreateFramebuffers, glCreateRenderbuffers, glCreateSamplers.
Буфер: память выделяется один раз
Вторая строка — glNamedBufferStorage. Эта функция выделяет буферу память и заполняет память данными. Аргументов четыре: имя буфера, размер в байтах, указатель на данные и флаги.
От привычной glBufferData она отличается тем, что память неизменяемая по размеру. Повторный вызов glNamedBufferStorage для того же буфера даёт GL_INVALID_OPERATION. Буфер другого размера — это новый буфер.
Флаги в последнем аргументе говорят, что программа собирается делать с содержимым дальше. Ноль означает «ничего»: данные загружены один раз, и менять содержимое со стороны процессора нельзя. Вызов glNamedBufferSubData для такого буфера тоже даёт GL_INVALID_OPERATION. Чтобы обновлять данные, при создании нужен флаг GL_DYNAMIC_STORAGE_BIT, а чтобы отображать буфер в память программы через glMapNamedBufferRange — флаги GL_MAP_READ_BIT или GL_MAP_WRITE_BIT.
Вместо расплывчатой подсказки GL_STATIC_DRAW драйвер получает точное обещание и может выбрать для буфера подходящую память.
В программе статьи два буфера, вершины и индексы, и оба создаёт одна функция:
GLuint createBuffer(std::span<const std::byte> bytes)
{
GLuint buffer = 0;
glCreateBuffers(1, &buffer);
glNamedBufferStorage(buffer, static_cast<GLsizeiptr>(bytes.size()), bytes.data(), 0);
return buffer;
}std::span<const std::byte> передаёт указатель и размер одной вещью, поэтому перепутать sizeof массива с sizeof элемента при вызове не получится:
const GLuint vertexBuffer = createBuffer(std::as_bytes(std::span(vertices)));
const GLuint indexBuffer = createBuffer(std::as_bytes(std::span(indices)));std::span и std::as_bytes появились ещё в C++20. Конструктор std::span(vertices) сам выводит из массива тип элемента и количество.
VAO: формат отдельно, буфер отдельно
В OpenGL 3.3 вершины описывает функция glVertexAttribPointer. Она делает два дела сразу: записывает формат атрибута и запоминает буфер, который в этот момент лежит в гнезде GL_ARRAY_BUFFER. Буфера в аргументах снова нет.
DSA разводит эти два дела по разным функциям, и устройство VAO становится видно в коде:

Внутри VAO лежат записи двух видов.
Точка привязки отвечает на вопрос «откуда брать байты». В ней записаны буфер, начало данных в буфере и шаг — расстояние в байтах от одной вершины до следующей.
Атрибут отвечает на вопрос «как понимать байты». В нём записаны количество чисел, тип чисел и смещение от начала вершины. Кроме того, атрибут хранит номер точки привязки, из которой он читает.
Отдельно в VAO есть одно место для буфера индексов.
Вершина в программе статьи состоит из позиции, текстурных координат и цвета:
struct Vertex
{
float position[2];
float uv[2];
float color[3];
};Так четыре вершины лежат в буфере:

Сначала точка привязки и буфер индексов:
GLuint vao = 0;
glCreateVertexArrays(1, &vao);
glVertexArrayVertexBuffer(vao, 0, vertexBuffer, 0, sizeof(Vertex));
glVertexArrayElementBuffer(vao, indexBuffer);Функция glVertexArrayVertexBuffer принимает пять аргументов: VAO, номер точки привязки, буфер, начало данных в буфере и шаг. Шаг равен sizeof(Vertex), то есть 28 байт.
Функция glVertexArrayElementBuffer подключает буфер индексов. Ловушки с glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0) здесь нет, потому что привязки нет.
Теперь атрибуты. Каждому атрибуту нужны три вызова: включить, описать формат, указать точку привязки. Три атрибута дают девять почти одинаковых строк, поэтому я описал раскладку вершины таблицей и прошёл по таблице циклом:
struct Attribute
{
GLint count;
GLuint offset;
};
constexpr std::array layout{
Attribute{ 2, offsetof(Vertex, position) },
Attribute{ 2, offsetof(Vertex, uv) },
Attribute{ 3, offsetof(Vertex, color) },
};
for (const auto [index, attribute] : std::views::enumerate(layout))
{
const auto location = static_cast<GLuint>(index);
glEnableVertexArrayAttrib(vao, location);
glVertexArrayAttribFormat(vao, location, attribute.count, GL_FLOAT, GL_FALSE, attribute.offset);
glVertexArrayAttribBinding(vao, location, 0);
}std::views::enumerate из C++23 выдаёт пары «номер, элемент», и отдельный счётчик не нужен. Номер элемента в таблице совпадает с location атрибута в шейдере.
Обратите внимание на последний аргумент glVertexArrayAttribFormat. Смещение передаётся обычным числом. В glVertexAttribPointer на этом месте стоял указатель, и число приходилось превращать в указатель записью (void*)offsetof(…).
Разделение формата и буфера даёт и практическую выгоду. Чтобы нарисовать тем же VAO другую модель с такой же раскладкой вершины, достаточно одного вызова glVertexArrayVertexBuffer с другим буфером: атрибуты заново описывать не надо.
Текстура без glActiveTexture
Картинку для текстуры программа создаёт сама, чтобы статье не понадобилась библиотека загрузки изображений. Это шахматная доска 64 на 64 пикселя:
constexpr int size = 64;
constexpr int cell = 8;
std::vector<std::uint32_t> pixels(size * size);
const std::mdspan image(pixels.data(), size, size);
const auto range = std::views::iota(0, size);
for (const auto [y, x] : std::views::cartesian_product(range, range))
{
const bool light = (x / cell + y / cell) % 2 == 0;
image[y, x] = light ? 0xFFE8E8E8 : 0xFF505050;
}Здесь сразу три вещи из C++23. std::mdspan смотрит на одномерный std::vector как на двумерную таблицу и сам считает y * size + x. Запись image[y, x] с двумя индексами в квадратных скобках стала законной тоже в C++23. std::views::cartesian_product заменяет два вложенных цикла одним.
Теперь объект текстуры:
GLuint texture = 0;
glCreateTextures(GL_TEXTURE_2D, 1, &texture);
glTextureStorage2D(texture, 1, GL_RGBA8, size, size);
glTextureSubImage2D(texture, 0, 0, 0, size, size, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
glTextureParameteri(texture, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTextureParameteri(texture, GL_TEXTURE_MAG_FILTER, GL_NEAREST);Функция glCreateTextures получает вид текстуры первым аргументом: вид задаётся при создании и потом не меняется. В остальных вызовах константы GL_TEXTURE_2D уже нет, везде стоит имя texture.
Работа с памятью устроена так же, как у буфера. glTextureStorage2D один раз выделяет память: второй аргумент — количество уровней детализации, третий — формат хранения, дальше ширина и высота. Повторный вызов даёт GL_INVALID_OPERATION. glTextureSubImage2D копирует пиксели в уже выделенную память.
Осталось передать текстуру шейдеру. В старом стиле для этого нужны два вызова и ещё одно скрытое состояние: glActiveTexture выбирает текстурный блок, а glBindTexture кладёт текстуру в выбранный блок. В DSA номер блока передаётся аргументом:

glBindTextureUnit(0, texture);Шейдер находит текстуру по номеру блока, который записан в описании сэмплера:
layout(binding = 0) uniform sampler2D checker;Номер блока стоит в двух местах, в вызове и в шейдере, и оба места видны глазами. Вызов glUniform1i для сэмплера не нужен.
Шейдеры: std::expected вместо молчаливой ошибки
Функции шейдеров и программ, glShaderSource, glCompileShader, glLinkProgram, с самого начала принимали имя объекта аргументом, так что менять в них нечего. Зато здесь пригодился C++23.
Обычная compileShader из учебника печатает ошибку и всё равно возвращает имя неработающего шейдера. В C++23 функция может вернуть либо результат, либо ошибку:
enum class ShaderStage : GLenum
{
Vertex = GL_VERTEX_SHADER,
Fragment = GL_FRAGMENT_SHADER,
};
std::expected<GLuint, std::string> compileShader(ShaderStage stage, const char* source)
{
const GLuint shader = glCreateShader(std::to_underlying(stage));
glShaderSource(shader, 1, &source, nullptr);
glCompileShader(shader);
GLint success = GL_FALSE;
glGetShaderiv(shader, GL_COMPILE_STATUS, &success);
if (!success)
{
std::string message(1024, '\0');
GLsizei length = 0;
glGetShaderInfoLog(shader, static_cast<GLsizei>(message.size()), &length, message.data());
message.resize(length);
glDeleteShader(shader);
return std::unexpected(std::move(message));
}
return shader;
}std::expected<GLuint, std::string> хранит либо имя шейдера, либо текст ошибки. Вызывающий код не может случайно взять имя и забыть про проверку: значение достаётся через * только после if.
enum class ShaderStage не даёт передать в функцию постороннюю константу OpenGL вроде GL_TEXTURE_2D: такой вызов не скомпилируется. std::to_underlying превращает значение перечисления обратно в GLenum без static_cast с повтором типа.
У std::expected есть методы, которые меняют содержимое, не разворачивая его. transform_error дописывает к тексту ошибки, какой именно шейдер не собрался:
const auto vertexShader = compileShader(ShaderStage::Vertex, vertexSource)
.transform_error([](std::string message) { return "vertex shader: " + message; });
if (!vertexShader)
{
return vertexShader;
}В main проверка занимает одно условие, а сообщение печатает std::println:
const auto program = createProgram(vertexShaderSource, fragmentShaderSource);
if (!program)
{
std::println(stderr, "{}", program.error());
// …закрыть окно и выйти
}std::println из заголовка <print> заменяет связку std::cerr << std::format(…): формат тот же, перевод строки функция добавляет сама.
С uniform-переменными DSA появился ещё раньше остального, в OpenGL 4.1. Классическая glUniform1f записывает значение в программу, которая сейчас включена через glUseProgram. У glProgramUniform1f программа стоит первым аргументом:
glProgramUniform1f(*program, timeLocation, static_cast<float>(glfwGetTime()));Номер timeLocation равен нулю, потому что в шейдере он назначен явно: layout(location = 0) uniform float time;. Вызов glGetUniformLocation не нужен.
Что всё-таки привязывается
Привязка из OpenGL не исчезла. Изменилась её роль: объект привязывают, чтобы им рисовать, а не чтобы его менять. Цикл кадра выглядит так:
glProgramUniform1f(*program, timeLocation, static_cast<float>(glfwGetTime()));
glUseProgram(*program);
glBindVertexArray(vao);
glBindTextureUnit(0, texture);
glDrawElements(GL_TRIANGLES, static_cast<GLsizei>(std::size(indices)), GL_UNSIGNED_INT, nullptr);Три строки перед glDrawElements отвечают на три вопроса команды рисования: какой программой, из каких вершин, с какой текстурой. Так же для рисования привязывается кадровый буфер через glBindFramebuffer. Всё остальное программа настроила один раз до цикла и ни разу не обратилась к гнёздам.
Шпаргалка
Привязка, OpenGL 3.3 | DSA, OpenGL 4.5 и новее |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Правило для имён простое: у буферов в название добавляется Named, у текстур Tex превращается в Texture, у VAO появляется VertexArray.
Когда DSA не подойдёт
DSA требует OpenGL 4.5. На Windows и Linux такую версию дают драйверы NVIDIA, AMD и Intel для видеокарт примерно последних десяти лет. Есть три случая, когда DSA недоступен:
macOS. Apple остановила OpenGL на версии 4.1. Из всего описанного там работает только
glProgramUniform*.OpenGL ES и WebGL. В мобильных и браузерных версиях DSA нет.
Старое железо, у которого драйвер не дотягивает до 4.5.
И о скорости. DSA убирает лишние вызовы привязки, но заметного ускорения от этого ждать не стоит. Выгода в другом: код говорит, какой объект он меняет, а функции перестают портить друг другу состояние.
Что пригодилось из C++23
Возможность | Заголовок | Что заменяет |
|---|---|---|
|
| код ошибки и вывод в консоль внутри функции |
|
|
|
|
|
|
|
| ручной расчёт |
|
| отдельный счётчик в цикле |
|
| вложенные циклы |
Я проверял код только на MSVC из Visual Studio 2026 с флагом /std:c++latest (CMake 4.4 ставит этот флаг по строке target_compile_features(app PRIVATE cxx_std_23)). На GCC и Clang не собирал: поддержку <mdspan>, <print> и новых диапазонов в вашей версии стандартной библиотеки стоит сверить с таблицей на cppreference.
Полный код
Скрытый текст
#include <array>
#include <cstddef>
#include <cstdint>
#include <cstdio>
#include <expected>
#include <mdspan>
#include <print>
#include <ranges>
#include <span>
#include <string>
#include <utility>
#include <vector>
#include <glad/gl.h>
#include <GLFW/glfw3.h>
const char* vertexShaderSource = R"(
#version 460 core
layout(location = 0) in vec2 inPosition;
layout(location = 1) in vec2 inUV;
layout(location = 2) in vec3 inColor;
out vec2 vertexUV;
out vec3 vertexColor;
void main()
{
gl_Position = vec4(inPosition, 0.0, 1.0);
vertexUV = inUV;
vertexColor = inColor;
}
)";
const char* fragmentShaderSource = R"(
#version 460 core
in vec2 vertexUV;
in vec3 vertexColor;
layout(binding = 0) uniform sampler2D checker;
layout(location = 0) uniform float time;
out vec4 color;
void main()
{
float pulse = 0.5 + 0.5 * sin(time * 2.0);
vec3 tint = mix(vec3(1.0), vertexColor, pulse);
color = vec4(texture(checker, vertexUV).rgb * tint, 1.0);
}
)";
struct Vertex
{
float position[2];
float uv[2];
float color[3];
};
struct Attribute
{
GLint count;
GLuint offset;
};
enum class ShaderStage : GLenum
{
Vertex = GL_VERTEX_SHADER,
Fragment = GL_FRAGMENT_SHADER,
};
std::expected<GLuint, std::string> compileShader(ShaderStage stage, const char* source)
{
const GLuint shader = glCreateShader(std::to_underlying(stage));
glShaderSource(shader, 1, &source, nullptr);
glCompileShader(shader);
GLint success = GL_FALSE;
glGetShaderiv(shader, GL_COMPILE_STATUS, &success);
if (!success)
{
std::string message(1024, '\0');
GLsizei length = 0;
glGetShaderInfoLog(shader, static_cast<GLsizei>(message.size()), &length, message.data());
message.resize(length);
glDeleteShader(shader);
return std::unexpected(std::move(message));
}
return shader;
}
std::expected<GLuint, std::string> linkProgram(GLuint vertexShader, GLuint fragmentShader)
{
const GLuint program = glCreateProgram();
glAttachShader(program, vertexShader);
glAttachShader(program, fragmentShader);
glLinkProgram(program);
GLint success = GL_FALSE;
glGetProgramiv(program, GL_LINK_STATUS, &success);
if (!success)
{
std::string message(1024, '\0');
GLsizei length = 0;
glGetProgramInfoLog(program, static_cast<GLsizei>(message.size()), &length, message.data());
message.resize(length);
glDeleteProgram(program);
return std::unexpected(std::move(message));
}
return program;
}
std::expected<GLuint, std::string> createProgram(const char* vertexSource, const char* fragmentSource)
{
const auto vertexShader = compileShader(ShaderStage::Vertex, vertexSource)
.transform_error([](std::string message) { return "vertex shader: " + message; });
if (!vertexShader)
{
return vertexShader;
}
const auto fragmentShader = compileShader(ShaderStage::Fragment, fragmentSource)
.transform_error([](std::string message) { return "fragment shader: " + message; });
if (!fragmentShader)
{
glDeleteShader(*vertexShader);
return fragmentShader;
}
const auto program = linkProgram(*vertexShader, *fragmentShader)
.transform_error([](std::string message) { return "program: " + message; });
glDeleteShader(*vertexShader);
glDeleteShader(*fragmentShader);
return program;
}
GLuint createBuffer(std::span<const std::byte> bytes)
{
GLuint buffer = 0;
glCreateBuffers(1, &buffer);
glNamedBufferStorage(buffer, static_cast<GLsizeiptr>(bytes.size()), bytes.data(), 0);
return buffer;
}
GLuint createCheckerTexture()
{
constexpr int size = 64;
constexpr int cell = 8;
std::vector<std::uint32_t> pixels(size * size);
const std::mdspan image(pixels.data(), size, size);
const auto range = std::views::iota(0, size);
for (const auto [y, x] : std::views::cartesian_product(range, range))
{
const bool light = (x / cell + y / cell) % 2 == 0;
image[y, x] = light ? 0xFFE8E8E8 : 0xFF505050;
}
GLuint texture = 0;
glCreateTextures(GL_TEXTURE_2D, 1, &texture);
glTextureStorage2D(texture, 1, GL_RGBA8, size, size);
glTextureSubImage2D(texture, 0, 0, 0, size, size, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
glTextureParameteri(texture, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTextureParameteri(texture, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
return texture;
}
int main()
{
glfwSetErrorCallback([](int code, const char* description) {
std::println(stderr, "GLFW error {}: {}", code, description);
});
if (!glfwInit())
{
return 1;
}
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 6);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
GLFWwindow* window = glfwCreateWindow(960, 540, "DSA", nullptr, nullptr);
if (!window)
{
glfwTerminate();
return 1;
}
glfwMakeContextCurrent(window);
if (!gladLoadGL(glfwGetProcAddress))
{
std::println(stderr, "Failed to load OpenGL functions");
glfwDestroyWindow(window);
glfwTerminate();
return 1;
}
std::println("OpenGL {}", reinterpret_cast<const char*>(glGetString(GL_VERSION)));
const auto program = createProgram(vertexShaderSource, fragmentShaderSource);
if (!program)
{
std::println(stderr, "{}", program.error());
glfwDestroyWindow(window);
glfwTerminate();
return 1;
}
const Vertex vertices[] = {
{ { -0.5f, -0.5f }, { 0.0f, 0.0f }, { 1.0f, 0.3f, 0.3f } },
{ { 0.5f, -0.5f }, { 1.0f, 0.0f }, { 0.3f, 1.0f, 0.3f } },
{ { 0.5f, 0.5f }, { 1.0f, 1.0f }, { 0.3f, 0.3f, 1.0f } },
{ { -0.5f, 0.5f }, { 0.0f, 1.0f }, { 1.0f, 1.0f, 0.3f } },
};
const GLuint indices[] = { 0, 1, 2, 2, 3, 0 };
const GLuint vertexBuffer = createBuffer(std::as_bytes(std::span(vertices)));
const GLuint indexBuffer = createBuffer(std::as_bytes(std::span(indices)));
GLuint vao = 0;
glCreateVertexArrays(1, &vao);
glVertexArrayVertexBuffer(vao, 0, vertexBuffer, 0, sizeof(Vertex));
glVertexArrayElementBuffer(vao, indexBuffer);
constexpr std::array layout{
Attribute{ 2, offsetof(Vertex, position) },
Attribute{ 2, offsetof(Vertex, uv) },
Attribute{ 3, offsetof(Vertex, color) },
};
for (const auto [index, attribute] : std::views::enumerate(layout))
{
const auto location = static_cast<GLuint>(index);
glEnableVertexArrayAttrib(vao, location);
glVertexArrayAttribFormat(vao, location, attribute.count, GL_FLOAT, GL_FALSE, attribute.offset);
glVertexArrayAttribBinding(vao, location, 0);
}
const GLuint texture = createCheckerTexture();
const GLint timeLocation = 0;
while (!glfwWindowShouldClose(window))
{
glfwPollEvents();
glClearColor(0.10f, 0.12f, 0.16f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
glProgramUniform1f(*program, timeLocation, static_cast<float>(glfwGetTime()));
glUseProgram(*program);
glBindVertexArray(vao);
glBindTextureUnit(0, texture);
glDrawElements(GL_TRIANGLES, static_cast<GLsizei>(std::size(indices)), GL_UNSIGNED_INT, nullptr);
glfwSwapBuffers(window);
}
glDeleteTextures(1, &texture);
glDeleteVertexArrays(1, &vao);
glDeleteBuffers(1, &indexBuffer);
glDeleteBuffers(1, &vertexBuffer);
glDeleteProgram(*program);
glfwDestroyWindow(window);
glfwTerminate();
return 0;
}Скрытый текст
cmake_minimum_required(VERSION 3.28)
project(DsaDemo LANGUAGES C CXX)
include(FetchContent)
set(GLFW_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)
set(GLFW_BUILD_TESTS OFF CACHE BOOL "" FORCE)
set(GLFW_BUILD_DOCS OFF CACHE BOOL "" FORCE)
set(GLFW_INSTALL OFF CACHE BOOL "" FORCE)
FetchContent_Declare(
glfw
GIT_REPOSITORY https://github.com/glfw/glfw.git
GIT_TAG 3.5.1
GIT_SHALLOW TRUE
)
FetchContent_MakeAvailable(glfw)
# glad 2, сгенерированный для OpenGL 4.6 core: https://gen.glad.sh
add_library(glad STATIC external/glad/src/gl.c)
target_include_directories(glad PUBLIC external/glad/include)
add_executable(app main.cpp)
target_compile_features(app PRIVATE cxx_std_23)
if(MSVC)
target_compile_options(app PRIVATE /W4)
endif()
target_link_libraries(app PRIVATE glfw glad)Файлы glad для OpenGL 4.6 core генерируются на gen.glad.sh и кладутся в папку external/glad.
Вместо заключения
DSA не меняет того, что OpenGL умеет. Он меняет то, как читается программа: объект назван в каждой строке, которая с ним работает, а привязка осталась только там, где она имеет смысл, — перед рисованием.
Статья выросла из курса «Движок с нуля», в котором я по шагам пишу 3D-движок на C++ и OpenGL 4.6 — сразу на DSA, без привязок ради настройки. Схемы там интерактивные: Хабр не позволяет встроить в статью свой скрипт, а на сайте схему можно пройти по шагам. Код движка открыт под лицензией MIT: github.com/chiter28/game-engine-for-site.
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.