Почти каждый, кто учил 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.
