Движок с нуля
Часть 2 · Первый треугольник · глава 2.3

OpenGL и первый цвет

Подключишь загрузчик glad, получишь функции OpenGL 4.6 и закрасишь окно своим цветом.

Откроем наш проект в Visual Studio. Сейчас программа открывает окно и крутит главный цикл, но внутри окна ничего не нарисовано. В этой главе программа закрасит окно тёмно-синим цветом через OpenGL. Цвет — самая простая картинка, но путь у цвета тот же, что будет у любого трёхмерного предмета в движке: программа вызывает функцию OpenGL, а рисует видеокарта.

Где живут функции OpenGL

OpenGL — это не библиотека, которую можно скачать, как GLFW, а описание набора функций: как функция называется, что принимает и что должна сделать. Код этих функций пишет производитель видеокарты и кладёт в драйвер. Сборка программы идёт в два шага. Сначала компилятор переводит каждый файл с кодом на C++ в машинный код и записывает результат в объектный файл. Если в файле вызвана функция, которую написали не здесь, компилятор оставляет в объектном файле отметку «тут нужна функция с таким именем»: где лежит её код, компилятор не знает. Потом линкер для каждой такой отметки находит код функции в объектных файлах и библиотеках и записывает всё вместе в .exe. Для вызова функции OpenGL, например glClear, линкер кода не найдёт ни в одном файле, и сборка остановится с ошибкой LNK2019. Код лежит в драйвере видеокарты, а видеокарта, на которой программу запустят, может оказаться другой, чем на компьютере, где программу собирают. У разных видеокарт код функции свой, и при сборке ещё неизвестно, чей именно понадобится. Поэтому линкеру нечего связывать с вызовом такой функции, а где лежит код, программа выясняет сама, уже во время запуска, когда драйвер видеокарты известен.

Есть одно исключение: Windows даёт программам файл opengl32.dll, и через этот файл доступны самые старые функции OpenGL, версии 1.1 1997 года, например glClear. Для остальных функций, например glGenBuffers, программа во время запуска спрашивает драйвер по имени: «дай адрес функции glGenBuffers» — и записывает полученный адрес в указатель на функцию. Так у программы появляется то, чего при сборке не было у линкера: адрес кода функции в драйвере. Функций в OpenGL 4.6 больше шестисот, и писать запрос для каждой вручную никто не хочет. Эту работу делает загрузчик — небольшой код, который для каждой функции OpenGL, и для старой тоже, заводит указатель и при запуске записывает в указатели адреса функций. Мы возьмём загрузчик glad.

glad в проекте

glad не скачивают готовым: сайт glad генерирует, то есть пишет, код загрузчика под нужную версию OpenGL, а собирать этот код потом будет CMake вместе с проектом. Мы возьмём OpenGL 4.6, последнюю версию, и профиль core.

Твой ход
Язык Visual Studio:

Открой в браузере glad.sh с настройками курса: в поле Generator уже выбран C/C++, в строке gl — версия Version 4.6 и профиль Core. Расширений и опций нет. Нажми Generate внизу страницы, а на следующей странице — ссылку glad.zip, и браузер скачает архив.

В папке проекта D:\dev\MyEngine создай папку external\glad и распакуй в эту папку содержимое архива: папки include и src. Должно получиться три файла:

  • external\glad\include\glad\gl.h — заголовок с объявлениями всех функций OpenGL;
  • external\glad\include\KHR\khrplatform.h — заголовок с типами вроде khronos_int32_t, который подключает gl.h;
  • external\glad\src\gl.c — код glad на языке C: во время запуска программы он спрашивает у драйвера видеокарты адреса функций OpenGL.

Потом допиши в CMakeLists.txt строки, отмеченные в карточке рядом: слово C в строке project, раздел «glad» после раздела «GLFW» и слово glad в конце target_link_libraries.

Объяснение

У GLFW цель glfw создавала команда add_library в собственном файле CMakeLists.txt из папки GLFW. У glad такого файла нет, и FetchContent не нужен: файлы glad лежат в проекте, а цель glad ты описываешь сам, двумя строками раздела «glad».

add_library(glad STATIC external/glad/src/gl.c) создаёт цель glad: статическую библиотеку из одного файла gl.c. Путь к файлу записан от папки проекта.

target_include_directories(glad PUBLIC …) говорит, в какой папке лежат заголовки glad. У каждой цели есть свой список папок, где компилятор ищет файлы для строк #include. Эта команда добавляет в список цели glad папку external\glad\include. Слово PUBLIC («общее») значит, что папка попадает в список не только цели glad, но и каждой цели, которая подключает glad, например цели app. В строках для app, например target_link_libraries(app PRIVATE glfw glad), стоит PRIVATE: настройка только для самой цели app. Здесь нужно PUBLIC, потому что строку #include <glad/gl.h> пишет не только gl.c, но и твой main.cpp: этот файл входит в цель app, как записано в строке add_executable(app main.cpp).

glad в target_link_libraries подключает библиотеку к app: компилятор получит папку external\glad\include, а линкер — готовый код glad.

C в строке project говорит CMake, что в проекте есть код на C, а не только на C++ (CXX): gl.c написан на C. Без этого слова проект тоже соберётся, но только потому, что язык C уже включила GLFW строкой project(GLFW VERSION 3.5.1 LANGUAGES C …) в файле D:\dev\MyEngine\out\build\vs2026\_deps\glfw-src\CMakeLists.txt. Проект не должен держаться на том, что написала чужая библиотека, поэтому язык C записываем сами.

Запуск

Сохрани файл (Ctrl+S) и дождись в окне OutputВывод строки CMake generation finished..

Проверь, что цель появилась: нажми Switch Views над деревом в окне Solution ExplorerОбозреватель решений, затем двойной щелчок по строке CMake Targets View. Под Engine Project рядом с app (executable) появилась строка glad (static library).

CMakeLists.txtстроки 6–42коммит 9ea057d
project(Engine LANGUAGES C CXX)# ============================================================================# Dependencies# ============================================================================include(FetchContent)# --- GLFW -------------------------------------------------------------------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 -------------------------------------------------------------------add_library(glad STATIC external/glad/src/gl.c)target_include_directories(glad PUBLIC external/glad/include)# ============================================================================# App# ============================================================================add_executable(app main.cpp)target_compile_features(app PRIVATE cxx_std_20)if(MSVC)    target_compile_options(app PRIVATE /W4)endif()target_link_libraries(app PRIVATE glfw glad)

Контекст и загрузка функций

Функции OpenGL работают не сами по себе, а внутри контекста OpenGL: это всё, что драйвер помнит об одном окне: настройки рисования, например каким цветом закрашивать окно, и данные, которые программа загрузила в видеокарту. Контекст не равен окну: окно видит пользователь, а контекст лежит в памяти драйвера. У каждого окна свой контекст, и между окном и контекстом такая же связь, как между холстом и записью о том, каким цветом на нём рисуют. Функция OpenGL меняет настройку в контексте или рисует по настройкам из контекста, а результат появляется в окне, которому этот контекст принадлежит. Контекст создаёт glfwCreateWindow вместе с окном, а до этого программа говорит GLFW, какую версию OpenGL просить у драйвера.

Твой ход

Допиши в main.cpp строки, отмеченные в карточке рядом. Главный цикл пока не трогай.

Объяснение

Строки, которые ты дописал, по порядку подключают заголовок glad, просят у драйвера версию OpenGL, выбирают контекст, загружают функции OpenGL и печатают версию, которую дал драйвер.

Строка #include <glad/gl.h> должна стоять выше строки, которая подключает заголовок GLFW, иначе сборка остановится. В Windows SDK есть собственный заголовок OpenGL: в нём имена старых функций, например glClear, объявлены как обычные функции. Заголовок GLFW подключает этот системный заголовок, если до него не подключён другой заголовок OpenGL. Заголовок glad объявляет все функции OpenGL 4.6, и те же имена, например glClear, объявлены в нём иначе, как указатели на функции. Одно имя с двумя разными объявлениями компилятор не принимает. Поэтому glad идёт первым и помечает, что заголовок OpenGL уже подключён. GLFW видит метку, системный заголовок не подключает, и у каждого имени остаётся одно объявление. Если поставить строки наоборот, первым подключится системный заголовок, и glad остановит сборку ошибкой, потому что имена уже объявлены по-другому. Проекту достаточно порядка «сначала glad», хотя есть и другой способ.

Три строки glfwWindowHint («подсказка для окна») настраивают следующее окно, которое создаст glfwCreateWindow: OpenGL версии 4 (MAJOR, старшая цифра) точка 6 (MINOR, младшая цифра), профиль core. Если драйвер не умеет OpenGL 4.6, окно не создастся: лямбда из glfwSetErrorCallback напечатает причину, а проверка if (!window) завершит программу с кодом 1.

glfwMakeContextCurrent выбирает контекст, с которым будут работать функции OpenGL. Функции вроде glClear не получают окно среди аргументов, в них нет параметра «для какого окна». Поэтому Windows хранит запись «текущий контекст»: код каждой функции OpenGL в драйвере смотрит в эту запись и работает с тем контекстом, на который запись указывает. Пока запись пуста, у функций OpenGL нет контекста, и вызовы не сработают: записать цвет и закрасить окно негде.

Вызов glfwMakeContextCurrent(window) записывает в запись «текущий контекст» контекст окна window. После этого каждый вызов функции OpenGL, например закрашивание окна или смена цвета, относится к окну window. Запись хранится отдельно для каждого потока программы, поэтому вызов действует в том потоке, где он сделан. Наша программа работает в одном потоке, и одного вызова хватает. Окон может быть несколько, и у каждого свой контекст: тогда тем же вызовом программа выбирает, с каким из них работать.

gladLoadGL заполняет указатели на функции OpenGL. Способ спросить адрес функции свой в каждой операционной системе. В коде glad, который мы сгенерировали, этих способов нет, а в GLFW есть. Поэтому в gladLoadGL ты передаёшь функцию из GLFW, которая умеет спрашивать адрес: glfwGetProcAddress («получить адрес процедуры»). После имени glfwGetProcAddress нет круглых скобок: со скобками функция вызвалась бы прямо в этой строке, а без скобок в gladLoadGL передаётся сама функция, то есть адрес кода функции. Так же в начале main ты передал лямбду в glfwSetErrorCallback: функцию написал или выбрал ты, а вызывает библиотека. Код glad вызывает glfwGetProcAddress для каждого из шестисот имён. Если не получилось, gladLoadGL возвращает 0, и программа выходит с кодом 1. Загружать функции можно только после glfwMakeContextCurrent: адреса функций драйвер выдаёт для текущего контекста, а без текущего контекста glfwGetProcAddress вернёт пустой адрес, и лямбда напечатает ошибку GLFW о том, что текущего контекста нет.

glGetString(GL_VERSION) — первый вызов функции OpenGL в программе. Функция glGetString возвращает строку со сведениями об OpenGL, а аргумент GL_VERSION, константа из заголовка glad, выбирает, какую именно строку вернуть: с версией OpenGL, которую дал драйвер. По этой строке видно, что драйвер действительно дал 4.6. glGetString возвращает строку как const GLubyte*, то есть указатель на unsigned char, а std::format ждёт const char*. reinterpret_cast говорит компилятору: «читай эти байты как обычные символы».

Запуск

Запусти программу без отладки (Ctrl+F5). Окно снова пустое, а в консоли появится строка с версией OpenGL. Вот так строка выглядит на ноутбуке автора:

Консоль app
OpenGL 4.6.0 - Build 30.0.101.1631
main.cpp
#include <format>#include <iostream>#include <glad/gl.h>#include <GLFW/glfw3.h>int main(){    glfwSetErrorCallback([](int code, const char* description) {        std::cerr << std::format("GLFW error {}: {}\n", 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(1280, 720, "Engine", nullptr, nullptr);    if (!window)    {        glfwTerminate();        return 1;    }    glfwMakeContextCurrent(window);    if (!gladLoadGL(glfwGetProcAddress))    {        std::cerr << "Failed to load OpenGL functions\n";        glfwDestroyWindow(window);        glfwTerminate();        return 1;    }    std::cout << std::format("OpenGL {}\n", reinterpret_cast<const char*>(glGetString(GL_VERSION)));    while (!glfwWindowShouldClose(window))    {        glfwPollEvents();    }    glfwDestroyWindow(window);    glfwTerminate();    return 0;}
Если не получилось2
C1189 #error: OpenGL (gl.h) header already included (API: gl), remove previous include! Ошибка на вкладке Error List в файле gl.h, сборка остановилась.
Почему
Строка #include <GLFW/glfw3.h> стоит в main.cpp выше, чем #include <glad/gl.h>. Заголовок GLFW сам подключает системный заголовок OpenGL из Windows SDK, если до этого не подключён другой заголовок OpenGL. Системный заголовок объявляет функции версии 1.1, например glClear, как обычные функции, а glad объявляет те же имена по-своему, как замену имени на указатель. Одно имя с двумя разными объявлениями компилятор не примет, поэтому glad заранее останавливает сборку строкой #error с понятным текстом.
Что сделать

Поменяй две строки местами: сначала #include <glad/gl.h>, потом #include <GLFW/glfw3.h>, как в карточке выше, и снова запусти программу.

GLFW error 65543: WGL: Driver does not support OpenGL version 4.6 Строка в консоли, а окно не появилось.
Почему
Драйвер видеокарты не умеет OpenGL 4.6: устарел драйвер или сама видеокарта. Три строки glfwWindowHint попросили версию 4.6, драйвер отказал, и glfwCreateWindow вернула nullptr. Строку в консоли напечатала лямбда из glfwSetErrorCallback, а проверка if (!window) завершила программу.
Что сделать

Установи свежий драйвер с сайта производителя видеокарты: NVIDIA, AMD или Intel. Если строка осталась, видеокарта не умеет OpenGL 4.6. Тогда в первых двух строках glfwWindowHint поменяй цифры 4 и 6 на версию, которую видеокарта умеет, например 4 и 1.

Что делает gladLoadGL с функцией glClear

Возьмём функцию glClear. Эта функция старая и доступна через opengl32.dll, но glad заводит указатели и для старых функций, чтобы все функции OpenGL вызывались одинаково. В заголовке external\glad\include\glad\gl.h имя glClear — не функция, а замена имени: везде, где ты пишешь glClear, компилятор видит glad_glClear. Переменная glad_glClear — указатель на функцию, и при запуске программы этот указатель пустой, в указателе лежит NULL.

external\glad\include\glad\gl.hстрока 2403
#define glClear glad_glClear

Заполняет указатель gladLoadGL. Для каждой функции OpenGL код gladLoadGL выполняет одну такую строку, вот строка для glClear:

external\glad\src\gl.cстрока 710
    glad_glClear = (PFNGLCLEARPROC) load(userptr, "glClear");

load(userptr, "glClear") — это вызов функции load. Внутри себя функция load вызывает функцию glfwGetProcAddress, которую ты передал в gladLoadGL, а glfwGetProcAddress находит адрес функции с именем glClear: сначала спрашивает драйвер, а старые функции, которых драйвер не выдаёт, ищет в opengl32.dll. (PFNGLCLEARPROC) говорит компилятору, что полученный адрес — адрес функции нужного типа, а glad_glClear = записывает адрес в указатель.

Теперь вызов glClear в твоём коде переходит по адресу из указателя glad_glClear. Если вызвать glClear до gladLoadGL, указатель ещё пустой, и программа упадёт.

Закрасить окно

Теперь у программы есть функции OpenGL, и окно можно закрасить. Рисует OpenGL не прямо на экран, а в буфер кадра: таблицу пикселей размером с окно. Буферов у окна два, и это называется двойная буферизация: в один буфер программа рисует, другой в это время показан на экране.

Откуда у окна два буфера

В main нет ни одной строки, которая просит создать буферы. Буферы появляются во время вызова glfwCreateWindow, и работу делят библиотека GLFW и драйвер видеокарты.

У GLFW есть настройка окна GLFW_DOUBLEBUFFER («двойной буфер»). Настройку задают той же функцией glfwWindowHint, которой программа просит версию OpenGL, но в нашем коде такой строки нет: двойной буфер включён значением по умолчанию. Значения по умолчанию GLFW записывает так:

glfw-src\src\window.cстрока 288
    _glfw.hints.framebuffer.doublebuffer = true;

Когда программа вызывает glfwCreateWindow, GLFW читает эту настройку и просит у драйвера окно с двумя буферами кадра. Память под оба буфера выделяет драйвер видеокарты, а GLFW только говорит, что буферов нужно два.

В OpenGL у заднего буфера есть имя, константа GL_BACK («задний»). Константа буфер не создаёт, а только называет буфер в функциях OpenGL. В новом контексте окна с двумя буферами рисование идёт в буфер GL_BACK, поэтому glClear заливает задний буфер, хотя в скобках вызова о буфере ничего не сказано.

Твой ход

Допиши в главный цикл три строки после glfwPollEvents(), отмеченные в карточке рядом.

Объяснение

Три новые строки стоят внутри цикла, поэтому выполняются на каждой итерации.

glClearColor запоминает цвет, которым OpenGL будет закрашивать буфер кадра. Четыре числа цвета записываются в контекст OpenGL, а на экране ничего не меняется: цвет лежит в контексте, пока программа не запишет другой.

glClear закрашивает буфер кадра, который сейчас не показан на экране. Цвет glClear среди аргументов не получает: функция берёт цвет из контекста OpenGL, тот самый, который туда записал glClearColor. Поэтому в цикле glClearColor стоит выше glClear.

glfwSwapBuffers меняет два буфера кадра местами. У окна два буфера кадра: передний, который сейчас показан в окне, и задний, который программа закрашивает. После вызова залитый задний буфер становится передним, и окно показывает его целиком, за один раз. Бывший передний буфер уходит назад, и на следующей итерации программа закрашивает этот буфер заново.

Запуск

Запусти программу без отладки (Ctrl+F5). Окно закрасится тёмно-синим, почти чёрным цветом. Растяни окно: зажми левую кнопку мыши на краю окна и тяни, но кнопку не отпускай. Новая область окна остаётся тёмной, без цвета очистки. Когда ты отпустишь кнопку, цвет заполняет всё окно.

Так происходит потому, что на время растягивания Windows сама управляет окном внутри вызова glfwPollEvents и не возвращает управление в программу. Цикл стоит на этой строке, glClear и glfwSwapBuffers не выполняются, и новую область окна закрашивать некому. Когда ты отпускаешь кнопку, glfwPollEvents завершается, цикл делает следующую итерацию и закрашивает окно целиком.

main.cppстроки 42–50коммит 9ea057d
    while (!glfwWindowShouldClose(window))    {        glfwPollEvents();        glClearColor(0.10f, 0.12f, 0.16f, 1.0f);        glClear(GL_COLOR_BUFFER_BIT);        glfwSwapBuffers(window);    }

Схема ниже показывает, что эти три строки делают с контекстом OpenGL и с двумя буферами кадра: где хранится цвет очистки, какой буфер заливает glClear и как залитый буфер попадает на экран.

Зачем нужны glClear и glfwSwapBuffers

Сверху — контекст OpenGL, в нём настройки рисования, и пока важна одна: цвет очистки. Ниже — окно Engine, в окне показан передний буфер. Рядом задний буфер: на экране задний буфер не виден. Сразу после создания окна в обоих буферах мусор: память буферов ещё никто не заливал, и что в ней лежит, заранее неизвестно.
glClearColor («цвет очистки») записывает цвет в контекст, и больше ничего: ни один пиксель не меняется. Четыре числа — красный, зелёный, синий и альфа, каждое от 0.0 до 1.0. Наш цвет 0.10, 0.12, 0.16 — тёмный, с синим оттенком: синего чуть больше, чем красного и зелёного. В привычной записи от 0 до 255 это примерно 26, 31, 41.
glClear («очистить») заливает задний буфер цветом очистки из контекста. GL_COLOR_BUFFER_BIT говорит, что очистить: цвет каждого пикселя. У буфера кадра бывают и другие части, например глубина, и функция glClear умеет чистить любые из частей, поэтому часть называют явно. Передний буфер в это время не трогают: на экране всё ещё мусор.
glfwSwapBuffers («поменять буферы») меняет буферы местами: залитый буфер становится передним буфером, и окно показывает этот буфер целиком, за один раз. Бывший передний буфер становится задним буфером, и на следующей итерации программа будет рисовать в этот буфер. Так зритель никогда не видит буфер, в который программа ещё рисует.
Дальше это повторяется на каждой итерации цикла: залить задний буфер, поменять буферы. Пока картинка — один цвет, и кадры одинаковые. Когда в окне появятся предметы, которые двигаются, каждая итерация будет рисовать новый кадр в задний буфер, а передний буфер будет показывать прошлый, уже готовый кадр. Поэтому кадр, который программа рисует, зритель сразу не видит: на экране в это время прошлый кадр, то есть картинка всегда запаздывает на один кадр. Новый кадр появится в окне только после glfwSwapBuffers.

В трёх строках главного цикла, которые ты дописал, функции glClearColor и glClear окно не получают, а glfwSwapBuffers(window) получает. Откуда OpenGL знает, с каким окном работать, если окно ему не передали? Схема ниже показывает, что лежит в контексте OpenGL, зачем контекст нужен и как glfwMakeContextCurrent выбирает контекст.

Что такое контекст OpenGL и зачем он нужен

Возьмём строку glClear(GL_COLOR_BUFFER_BIT) из главного цикла. Единственный аргумент этой функции говорит только, что очистить: цвет каждого пикселя. Чтобы выполнить работу, драйверу видеокарты нужно знать больше: каким цветом заливать, в каком окне и какой из двух буферов этого окна, передний или задний. В скобках вызова этих сведений нет, значит, драйвер берёт сведения из другого места.
Сведения, которых нет в скобках вызова, лежат в контексте OpenGL: структуре в памяти драйвера, где собрано всё, что драйверу нужно знать, чтобы рисовать в одном окне. Контекст создаёт glfwCreateWindow вместе с окном Engine. Сведения разложены на четыре группы. Настройки говорят, как рисовать: цвет очистки, в новом контексте это четыре нуля (красный, зелёный, синий и альфа), область рисования и проверка глубины. Куда рисовать — задний буфер окна Engine: кадр рисуют в буфер, который не виден на экране.
Кроме настроек контекст хранит то, из чего складывается картинка с предметами. Созданные данные — то, что программа загрузила в память видеокарты: буферы вершин, шейдеры, текстуры. Контекст помнит, какие данные есть и где эти данные лежат. Выбрано сейчас — какой из созданных шейдеров и какая из созданных текстур пойдут в рисование. Программа пока ничего не создала и не выбрала, поэтому в группах «Созданные данные» и «Выбрано сейчас» пусто.
glClearColor(0.10f, 0.12f, 0.16f, 1.0f) записывает четыре числа в ряд «цвет очистки» и больше ничего не делает. Важно то, что происходит после вызова: цвет остаётся в контексте, пока программа не запишет другой. Поэтому строку glClearColor можно было бы вызвать один раз перед циклом.
glClear находит всё нужное в контексте: берёт цвет очистки и сведения о том, куда рисовать, а затем заливает задний буфер окна Engine. Вот зачем нужен контекст: без контекста все эти сведения пришлось бы передавать в скобках при каждом вызове, как в воображаемой строке на схеме: окно window, буфер GL_BACK («задний») и четыре числа цвета. А чтобы нарисовать предмет, сведений нужно ещё больше: шейдер, текстура, буфер вершин. Контекст помнит всё это между вызовами: настройку достаточно записать один раз, и настройка действует на все следующие вызовы.
Допустим, программа открыла второе окно. Вместе с этим окном glfwCreateWindow создаёт второй контекст, и значения в двух контекстах никак не связаны: цвет очистки в контексте окна Engine остался тёмно-синим, а цвет очистки в контексте второго окна ещё начальный, чёрный. У второго контекста и свой буфер, куда рисовать. Но в скобках у функции glClear контекста нет. Если программа снова вызовет glClear, с каким из двух контекстов будет работать этот вызов?
Драйвер узнаёт контекст из ячейки «текущий контекст»: так на схеме нарисована запись, которую Windows хранит для каждого потока. В ячейке лежит один контекст или ничего. В начале программы ячейка пуста: glfwCreateWindow создаёт контекст, но текущим не делает. Строка glfwMakeContextCurrent(window) записывает в ячейку контекст окна Engine, и появляется стрелка. В main эта строка стоит один раз, перед главным циклом: вызовы OpenGL ячейку не меняют, и запись остаётся, пока программа не запишет другой контекст.
Каждая функция OpenGL начинает работу с ячейки. Вызов glClear передаёт работу драйверу, драйвер читает ячейку, по стрелке находит контекст окна Engine и берёт там цвет очистки и сведения о том, куда рисовать. Так же начинает работу и glClearColor, только эта функция не читает цвет, а записывает: драйвер находит контекст по ячейке и кладёт четыре числа из скобок вызова в поле цвета очистки этого контекста. В коде драйвера это выглядит примерно как строка current->clearColor = color;. Здесь current («текущий») — контекст, найденный по ячейке, clearColor — поле с цветом очистки внутри контекста, а color — четыре числа, которые передали в glClearColor. Если ячейка пуста, функциям OpenGL не с каким контекстом работать, поэтому glfwMakeContextCurrent нужен и программе с одним окном. Так как драйвер сам находит контекст по ячейке, передавать контекст аргументом не нужно.
Вызов glfwMakeContextCurrent(window2) перезаписывает ячейку: теперь в ячейке контекст второго окна, и стрелка переходит на второй контекст. glClear(GL_COLOR_BUFFER_BIT) написан так же, как в строке выше, но теперь драйвер по ячейке находит второй контекст и заливает чёрным задний буфер второго окна. Контекст окна Engine при этом не исчез и не изменился: чтобы вернуться к этому контексту, программа снова вызовет glfwMakeContextCurrent(window). Ячейка только выбирает контекст, а настройки, данные и окно, в которое рисовать, записаны в самом контексте.

Итог

Проект собирает загрузчик glad как свою цель, программа просит у драйвера OpenGL 4.6 core, загружает функции OpenGL и на каждой итерации цикла заливает задний буфер цветом и показывает буфер в окне.

Твой ход · коммит

В окне Git ChangesИзменения Git дважды щёлкни каждый файл из списка ChangesИзменения и прочитай изменения. Если всё устраивает, закоммить и отправь на GitHub с сообщением Load OpenGL 4.6 core with glad and clear the window.

Так выглядят оба файла после главы:

CMakeLists.txt
CMakeLists.txtкоммит 9ea057d
cmake_minimum_required(VERSION 3.28)# ============================================================================# Project# ============================================================================project(Engine LANGUAGES C CXX)# ============================================================================# Dependencies# ============================================================================include(FetchContent)# --- GLFW -------------------------------------------------------------------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 -------------------------------------------------------------------add_library(glad STATIC external/glad/src/gl.c)target_include_directories(glad PUBLIC external/glad/include)# ============================================================================# App# ============================================================================add_executable(app main.cpp)target_compile_features(app PRIVATE cxx_std_20)if(MSVC)    target_compile_options(app PRIVATE /W4)endif()target_link_libraries(app PRIVATE glfw glad)
main.cpp
main.cppкоммит 9ea057d
#include <format>#include <iostream>#include <glad/gl.h>#include <GLFW/glfw3.h>int main(){    glfwSetErrorCallback([](int code, const char* description) {        std::cerr << std::format("GLFW error {}: {}\n", 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(1280, 720, "Engine", nullptr, nullptr);    if (!window)    {        glfwTerminate();        return 1;    }    glfwMakeContextCurrent(window);    if (!gladLoadGL(glfwGetProcAddress))    {        std::cerr << "Failed to load OpenGL functions\n";        glfwDestroyWindow(window);        glfwTerminate();        return 1;    }    std::cout << std::format("OpenGL {}\n", reinterpret_cast<const char*>(glGetString(GL_VERSION)));    while (!glfwWindowShouldClose(window))    {        glfwPollEvents();        glClearColor(0.10f, 0.12f, 0.16f, 1.0f);        glClear(GL_COLOR_BUFFER_BIT);        glfwSwapBuffers(window);    }    glfwDestroyWindow(window);    glfwTerminate();    return 0;}