The Jerusalem PostUkraine moving toward Israeli-Italian satellite terminals instead of Starlink - reportPunchPolice probe claims of extortion by unidentified officers in ImoInquirerTaal Volcano records spike in quakes after days of low activityCNN TürkGalatasaray harekete geçti! Christ Oulai için temaslar sıklaştıDaily MaverickOCEAN PLUNDER: Relentless fishing and dodgy science — Spain and Taiwan’s blue shark trade한겨레“트럼프, AI칩 수출규제 풀기 직전 엔비디아 주식 샀다”Inquirer EntertainmentSandara Park eyes mature acting roles for her Filipino movie comebackגיקטייםמיליוני משתמשים עצבניים ואלפי טיסות מקורקעות: מה לעזאזל קרה בסופ"ש למיקרוסופט ולשדות התעופה בארה"ב?ZDF heuteAktuelle Pressemitteilungen des ZDFCNN بالعربيةقاليباف: إيران لن تبقل حالة "اللا حرب واللا سلم".. وأي مواجهة جديدة ستطال "أطرافا كثيرة"경향신문가스라이팅에 더 취약한 ‘가정·학교 밖 청소년’…범죄 경계선에 보호체계 빈틈없나Vanguard2027: Inuwa Yahaya flags off APC campaign, targets 1m votes for Tinubu
The Daily Newsstand · Free, Always
Sunday, November 15, 2026

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

Translate

Почти каждый, кто учил 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 меняет содержимое гнезда, и настройка попадает не в ту текстуру

Программа привязала текстуру стены, чтобы настроить ей фильтрацию. Между привязкой и настройкой она вызвала 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 выдаёт только имя, glCreateBuffers выдаёт имя и создаёт объект

glGenBuffers выдаёт только имя, glCreateBuffers выдаёт имя и создаёт объект

Функция 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: атрибуты ссылаются на точку привязки, точка привязки — на буфер

Внутри VAO лежат записи двух видов.

Точка привязки отвечает на вопрос «откуда брать байты». В ней записаны буфер, начало данных в буфере и шаг — расстояние в байтах от одной вершины до следующей.

Атрибут отвечает на вопрос «как понимать байты». В нём записаны количество чисел, тип чисел и смещение от начала вершины. Кроме того, атрибут хранит номер точки привязки, из которой он читает.

Отдельно в VAO есть одно место для буфера индексов.

Вершина в программе статьи состоит из позиции, текстурных координат и цвета:

struct Vertex
{
    float position[2];
    float uv[2];
    float color[3];
};

Так четыре вершины лежат в буфере:

Байты буфера вершин: шаг 28, смещения 0, 8 и 16

Байты буфера вершин: шаг 28, смещения 0, 8 и 16

Сначала точка привязки и буфер индексов:

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 кладёт текстуру в блок, сэмплер читает блок по номеру binding

glBindTextureUnit кладёт текстуру в блок, сэмплер читает блок по номеру binding

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 и новее

glGenBuffers + glBindBuffer

glCreateBuffers

glBufferData

glNamedBufferStorage

glBufferSubData

glNamedBufferSubData

glMapBufferRange

glMapNamedBufferRange

glGenVertexArrays + glBindVertexArray

glCreateVertexArrays

glEnableVertexAttribArray

glEnableVertexArrayAttrib

glVertexAttribPointer

glVertexArrayVertexBuffer + glVertexArrayAttribFormat + glVertexArrayAttribBinding

glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, …)

glVertexArrayElementBuffer

glGenTextures + glBindTexture

glCreateTextures

glTexImage2D

glTextureStorage2D + glTextureSubImage2D

glTexParameteri

glTextureParameteri

glGenerateMipmap

glGenerateTextureMipmap

glActiveTexture + glBindTexture

glBindTextureUnit

glUseProgram + glUniform*

glProgramUniform*

glGenFramebuffers + glBindFramebuffer

glCreateFramebuffers

glFramebufferTexture

glNamedFramebufferTexture

glCheckFramebufferStatus

glCheckNamedFramebufferStatus

glBlitFramebuffer

glBlitNamedFramebuffer

Правило для имён простое: у буферов в название добавляется 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

Возможность

Заголовок

Что заменяет

std::expected, transform_error

<expected>

код ошибки и вывод в консоль внутри функции

std::print, std::println

<print>

std::cerr << std::format(…)

std::to_underlying

<utility>

static_cast<GLenum>(…)

std::mdspan, запись image[y, x]

<mdspan>

ручной расчёт y * size + x

std::views::enumerate

<ranges>

отдельный счётчик в цикле

std::views::cartesian_product

<ranges>

вложенные циклы

Я проверял код только на 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.

View the original on Хабр →

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.