OpenGL и первый цвет
Подключишь загрузчик glad, получишь функции OpenGL 4.6 и закрасишь окно своим цветом.
Откроем наш проект в Visual Studio. Сейчас программа открывает окно и крутит главный цикл, но внутри окна ничего не нарисовано. В этой главе программа закрасит окно тёмно-синим цветом через OpenGL. Цвет — самая простая картинка, но путь у цвета тот же, что будет у любого трёхмерного предмета в движке: программа вызывает функцию OpenGL, а рисует видеокарта.
Где живут функции OpenGL
OpenGL — это не библиотека, которую можно скачать, как GLFW, а описание набора функций: как функция называется, что принимает и что должна сделать. Код этих функций пишет производитель видеокарты и кладёт в glClearglClear программа вызовет в главном цикле, чтобы окно стало тёмно-синим. Имя сложено из двух частей: gl (функция OpenGL) и Clear («очистить»).LNK2019. Код лежит в драйвере видеокарты, а видеокарта, на которой программу запустят, может оказаться другой, чем на компьютере, где программу собирают. У разных видеокарт код функции свой, и при сборке ещё неизвестно, чей именно понадобится. Поэтому линкеру нечего связывать с вызовом такой функции, а где лежит код, программа выясняет сама, уже во время запуска, когда драйвер видеокарты известен.
Есть одно исключение: Windows даёт программам файл opengl32.dll, и через этот файл доступны самые старые функции OpenGL, версии 1.1 1997 года, например glClear. Для остальных функций, например glGenBuffersglGenBuffers заводит в памяти видеокарты место под данные.glGenBuffers» — и записывает полученный адрес в glfwSetErrorCallback превращалась в указатель на функцию.
glad в проекте
glad не скачивают готовым: сайт glad генерирует, то есть пишет, код загрузчика под нужную версию OpenGL, а собирать этот код потом будет CMake вместе с проектом. Мы возьмём OpenGL 4.6, последнюю версию, и профиль glBegin, в core убраны. Второй профиль, compatibility («совместимость»), оставляет и старые функции, но движку они не нужны.
Открой в браузере glad.sh с настройками курса: в поле Generator уже выбран C/C++, в строке gl — версия Version 4.6 и профиль Core.
В папке проекта D:\
- external\
glad\ — заголовок с объявлениями всех функций OpenGL;include\ glad\ gl.h - external\
glad\ — заголовок с типами вродеinclude\ KHR\ khrplatform.h khronos_int32_t, который подключает gl.h; - external\
glad\ — код glad на языке C: во время запуска программы он спрашивает у драйвера видеокарты адреса функций OpenGL.src\ gl.c
Потом допиши в CMakeLists.txt строки, отмеченные в карточке рядом: слово C в строке project, раздел «glad» после раздела «GLFW» и слово glad в конце target_link_libraries.
У GLFW цель glfw создавала команда add_libraryadd_library(glfw ${GLFW_LIBRARY_TYPE} init.c input.c window.c …)FetchContent не нужен: файлы glad glad ты описываешь сам, двумя строками раздела «glad».
add_library(glad STATIC external/glad/src/gl.c) создаёт glad:
target_include_directories(glad PUBLIC …) говорит, в какой папке лежат #include. Эта команда добавляет в список цели glad папку external\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\
C в строке project говорит CMake, что в проекте есть код на C, а не только на C++ (CXX): gl.c написан на C. Без этого слова проект тоже соберётся, но только потому, что язык C уже включила GLFW строкой project(GLFW VERSION 3.5.1 LANGUAGES C …) в файле D:\
Сохрани файл (Ctrl+S) и дождись в окне OutputВывод строки CMake generation finished..
Проверь, что цель появилась: нажми Кнопка Switch Views стоит в строке кнопок над деревом файлов (1). После нажатия в списке представлений появляется строка CMake Targets View (2):
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 работают не сами по себе, а внутри glfwCreateWindow вместе с окном, а до этого программа говорит GLFW, какую версию OpenGL просить у драйвера.
Допиши в main.cpp строки, отмеченные в карточке рядом. Главный цикл пока не трогай.
Строки, которые ты дописал, по порядку подключают заголовок glad, просят у драйвера версию OpenGL, выбирают контекст, загружают функции OpenGL и печатают версию, которую дал драйвер.
Строка В начале gl.h от glad стоит проверка: если метка Заголовок GLFW видит метку и не подключает системный заголовок OpenGL.#include <glad/gl.h> должна стоять выше строки, которая подключает заголовок GLFW, иначе сборка остановится. В glClear, объявлены как обычные функции. Заголовок GLFW подключает этот системный заголовок, если до него не подключён другой заголовок OpenGL. Заголовок glad объявляет все функции OpenGL 4.6, и те же имена, например glClear, объявлены в нём иначе, как указатели на функции. Одно имя с двумя разными объявлениями компилятор не принимает. Поэтому glad идёт первым и _gl_h уже есть, значит, кто-то подключил другой заголовок OpenGL раньше, и сборка останавливается. Если метки нет, glad ставит метку сам:#ifdef __gl_h_ #error OpenGL (gl.h) header already included (API: gl), remove previous include!#endif#define __gl_h_ 1#include <GLFW/glfw3.h> написать строку #define GLFW_INCLUDE_NONE, заголовок GLFW не подключит никакой заголовок OpenGL, и порядок двух строк #include перестанет иметь значение. Этот макрос можно задать и для всего проекта в CMake: target_compile_definitions(app PRIVATE GLFW_INCLUDE_NONE).
Три строки glfwWindowHint («подсказка для окна») настраивают следующее окно, которое создаст glfwCreateWindow: OpenGL версии 4 (MAJOR, старшая цифра) точка 6 (MINOR, младшая цифра), glfwSetErrorCallback напечатает причину, а проверка if (!window) завершит программу с кодом 1.
glfwMakeContextCurrent выбирает контекст, с которым будут работать функции OpenGL. Функции вроде glClear
Вызов glfwMakeContextCurrent(window) записывает в запись «текущий контекст» контекст окна window. После этого каждый вызов функции OpenGL, например закрашивание окна или смена цвета, относится к окну window. Запись хранится отдельно для каждого main. Программа может завести несколько потоков, и тогда у каждого потока свой текущий контекст.
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_castunsigned char читается как указатель на char.
Запусти программу без отладки (Ctrl+F5). Окно снова пустое, а в консоли появится строка с версией OpenGL. Вот так строка выглядит на ноутбуке автора:
OpenGL 4.6.0Версия OpenGL, которую дал драйвер. Мы просили 4.6, и драйвер дал ровно 4.6. Последняя цифра — номер исправления, для программы он ничего не меняет. - Build 30.0.101.1631Остальное после версии пишет производитель драйвера, как захочет. Так строку пишет драйвер Intel: номер сборки драйвера. Драйвер NVIDIA пишет иначе, например 4.6.0 NVIDIA 616.56, где 616.56 — версия драйвера. #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;}Закрасить окно
Теперь у программы есть функции OpenGL, и окно можно закрасить. Рисует OpenGL не прямо на экран, а в
Допиши в главный цикл три строки после glfwPollEvents(), отмеченные в карточке рядом.
Три новые строки стоят внутри цикла, поэтому выполняются на каждой
glClearColor запоминает цвет, которым OpenGL будет закрашивать буфер кадра. Четыре числа цвета записываются в контекст OpenGL, а на экране ничего не меняется: цвет лежит в контексте, пока программа не запишет другой.
glClear закрашивает буфер кадра, который сейчас не показан на экране. Цвет glClear среди аргументов не получает: функция берёт цвет из контекста OpenGL, тот самый, который туда записал glClearColor. Поэтому в цикле glClearColor стоит выше glClear.
glfwSwapBuffers меняет два буфера кадра местами. У окна два буфера кадра: передний, который сейчас показан в окне, и задний, который программа закрашивает. После вызова залитый задний буфер становится передним, и окно показывает его целиком, за один раз. Бывший передний буфер уходит назад, и на следующей итерации программа закрашивает этот буфер заново.
Запусти программу без отладки (Ctrl+F5). Окно закрасится тёмно-синим, почти чёрным цветом. Растяни окно: зажми левую кнопку мыши на краю окна и тяни, но кнопку не отпускай. Новая область окна остаётся тёмной, без цвета очистки. Когда ты отпустишь кнопку, цвет заполняет всё окно.
Так происходит потому, что на время растягивания Windows сама управляет окном внутри вызова glfwPollEvents и не возвращает управление в программу. Цикл стоит на этой строке, glClear и glfwSwapBuffers не выполняются, и новую область окна закрашивать некому. Когда ты отпускаешь кнопку, glfwPollEvents завершается, цикл делает следующую итерацию и закрашивает окно целиком.
while (!glfwWindowShouldClose(window)) { glfwPollEvents(); glClearColor(0.10f, 0.12f, 0.16f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); glfwSwapBuffers(window); }Схема ниже показывает, что эти три строки делают с контекстом OpenGL и с двумя буферами кадра: где хранится цвет очистки, какой буфер заливает glClear и как залитый буфер попадает на экран.
Зачем нужны glClear и glfwSwapBuffers
glClearColor («цвет очистки») записывает цвет в контекст, и больше ничего: ни один пиксель не меняется. Четыре числа — красный, зелёный, синий и 0.10, 0.12, 0.16 — тёмный, с синим оттенком: синего чуть больше, чем красного и зелёного. В привычной записи от 0 до 255 это примерно 26, 31, 41.glClear («очистить») заливает задний буфер цветом очистки из контекста. GL_COLOR_BUFFER_BIT говорит, что очистить: цвет каждого пикселя. У буфера кадра бывают и другие части, например glClear умеет чистить любые из частей, поэтому часть называют явно. Передний буфер в это время не трогают: на экране всё ещё мусор.glfwSwapBuffers («поменять буферы») меняет буферы местами: залитый буфер становится передним буфером, и окно показывает этот буфер целиком, за один раз. Бывший передний буфер становится задним буфером, и на следующей итерации программа будет рисовать в этот буфер. Так зритель никогда не видит буфер, в который программа ещё рисует.glfwSwapBuffers.В трёх строках главного цикла, которые ты дописал, функции В скобках у этих двух вызовов нет окна: Это функция GLFW, а не OpenGL. Библиотека GLFW просит у Windows показать буфер в окне, а Windows должна знать, в каком именно окне, поэтому окно передают аргументом.glClearColor и glClear окно не получаютglClearColor получает четыре числа цвета, а glClear — только флаг GL_COLOR_BUFFER_BIT. while (!glfwWindowShouldClose(window)) { glfwPollEvents(); glClearColor(0.10f, 0.12f, 0.16f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); glfwSwapBuffers(window); }glfwSwapBuffers(window) получает while (!glfwWindowShouldClose(window)) { glfwPollEvents(); glClearColor(0.10f, 0.12f, 0.16f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); glfwSwapBuffers(window); }glfwMakeContextCurrent выбирает контекст.
Что такое контекст OpenGL и зачем он нужен
glClear(GL_COLOR_BUFFER_BIT) из главного цикла. Единственный аргумент этой функции говорит только, что очистить: цвет каждого пикселя. Чтобы выполнить работу, драйверу видеокарты нужно знать больше: каким цветом заливать, в каком окне и какой из двух буферов этого окна, передний или задний. В скобках вызова этих сведений нет, значит, драйвер берёт сведения из другого места.glfwCreateWindow вместе с окном Engine. Сведения разложены на четыре группы. Настройки говорят, как рисовать: цвет очистки, в новом контексте это четыре нуля (красный, зелёный, синий и альфа), glClearColor(0.10f, 0.12f, 0.16f, 1.0f) записывает четыре числа в ряд «цвет очистки» и больше ничего не делает. Важно то, что происходит после вызова: цвет остаётся в контексте, пока программа не запишет другой. Поэтому строку glClearColor glClearColor стоит в цикле, чтобы все шаги кадра были рядом: сначала выбрать цвет, потом залить буфер, потом показать буфер.glClear находит всё нужное в контексте: берёт цвет очистки и сведения о том, куда рисовать, а затем заливает задний буфер окна Engine. Вот зачем нужен контекст: без контекста все эти сведения пришлось бы передавать в скобках при каждом вызове, как в воображаемой строке на схеме: окно window, буфер GL_BACK («задний») и четыре числа цвета. А чтобы нарисовать предмет, сведений нужно ещё больше: шейдер, текстура, буфер вершин. Контекст помнит всё это между вызовами: настройку достаточно записать один раз, и настройка действует на все следующие вызовы.glfwCreateWindow создаёт второй контекст, и значения в двух контекстах никак не связаны: цвет очистки в контексте окна Engine остался тёмно-синим, а цвет очистки в контексте второго окна ещё начальный, чёрный. У второго контекста и свой буфер, куда рисовать. Но в скобках у функции glClear контекста нет. Если программа снова вызовет glClear, с каким из двух контекстов будет работать этот вызов?main. Windows хранит запись о текущем контексте OpenGL отдельно для каждого потока, поэтому у двух потоков могут быть разные текущие контексты. Один контекст может быть текущим только в одном потоке за раз.glfwCreateWindow создаёт контекст, но текущим не делает. Строка glfwMakeContextCurrent(window) записывает в ячейку контекст окна Engine, и появляется стрелка. В main эта строка стоит один раз, перед главным циклом: вызовы OpenGL ячейку не меняют, и запись остаётся, пока программа не запишет другой контекст.glClear передаёт работу драйверу, драйвер читает ячейку, по стрелке находит контекст окна Engine и берёт там цвет очистки и сведения о том, куда рисовать. Так же начинает работу и glClearColor, только эта функция не читает цвет, а записывает: драйвер находит контекст по ячейке и кладёт четыре числа из скобок вызова в поле цвета очистки этого контекста. В коде драйвера это выглядит примерно как строка current->clearColor = color;. Здесь current («текущий») — контекст, найденный по ячейке, clearColor — поле с цветом очистки внутри контекста, а color — четыре числа, которые передали в glClearColor. Если ячейка пуста, функциям OpenGL не с каким контекстом работать, поэтому glfwMakeContextCurrent нужен и программе с одним окном. Так как драйвер сам находит контекст по ячейке, vkCreateBuffer получает VkDevice, объект выбранной видеокарты, а vkCmdDraw получает VkCommandBuffer, объект со списком команд для видеокарты. OpenGL придумали в начале 1990-х, и с тех пор функции OpenGL работают с контекстом, который выбран заранее.glfwMakeContextCurrent(window2) перезаписывает ячейку: теперь в ячейке контекст второго окна, и стрелка переходит на второй контекст. glClear(GL_COLOR_BUFFER_BIT) написан так же, как в строке выше, но теперь драйвер по ячейке находит второй контекст и заливает чёрным задний буфер второго окна. Контекст окна Engine при этом не исчез и не изменился: чтобы вернуться к этому контексту, программа снова вызовет glfwMakeContextCurrent(window). Ячейка только выбирает контекст, а настройки, данные и окно, в которое рисовать, записаны в самом контексте.Итог
Проект собирает загрузчик glad как свою цель, программа просит у
Так выглядят оба файла после главы: