Урок №3. Наш первый проект в OpenGL — Создание окна

  Дмитрий Бушуев  | 

  Обновл. 7 Сен 2021  | 

 43433

 ǀ   30 

На этом уроке мы создадим наш первый проект в OpenGL.

Подключаем GLFW

Давайте посмотрим, сможем ли мы запустить GLFW. Для начала создадим основной файл нашей программы, который будет называться main.cpp и подключим в нем два заголовочных файла:

Важное примечание: Заголовочный файл GLAD обязательно должен подключаться перед другими заголовочными файлами, которые требуют OpenGL (например, GLFW), так как внутри GLAD идет подключение файлов OpenGL (например, GL/gl.h).

Теперь нам нужно определить функцию main(), в которой будет создаваться экземпляр окна GLFW:

В начале функции main() мы инициализируем GLFW при помощи  функции glfwInit(). После этого, пользуясь функцией glfwWindowHint(), мы задаем конфигурацию GLFW:

   первый аргумент функции glfwWindowHint() указывает на то, какой из параметров GLFW мы хотим настроить, а все вместе они образуют большое множество опций, отличительной чертой которых является наличие префикса GLFW_;

   второй аргумент — это целое число, которое устанавливает значение нашего параметра. Со списком всех возможных опций и соответствующих им значений можно ознакомиться здесь.

Сейчас вы можете попробовать скомпилировать и запустить эту программу. Если в результате компилятор выдаст кучу ошибок типа undefined reference, то это означает, что вы неправильно подключили библиотеку GLFW.

Поскольку основное внимание на данном уроке уделяется OpenGL версии 3.3, то с помощью первых двух вызовов функции glfwWindowHint() мы сообщаем GLFW, что собираемся использовать именно эту версию OpenGL. Таким образом, GLFW сможет правильно организовать свою работу при создании контекста OpenGL. В случае, если на компьютере пользователя не установлена нужная версия OpengGL, GLFW не запустится. Мы также сообщаем GLFW, что хотим явно использовать core-profile. Это делается для того, чтобы не подгружать лишние для нас OpenGL-функции, предназначенные для поддержания обратной совместимости приложений.

Обратите внимание, если вы пользователь macOS, то вам необходимо раскомментировать следующую строку в вашей программе:

Примечание: Убедитесь, что ваша ОС и аппаратная часть компьютера поддерживают версию OpenGL 3.3 и выше. В противном случае, приложение может демонстрировать неопределенное поведение или же вообще перестать работать. Определить, какую версию OpenGL поддерживает ваш компьютер, можно с помощью программы OpenGL Extension Viewer (для Windows) или команды glxinfo (для Linux). Если в результате этого выяснится, что ваша версия OpenGL ниже рекомендованной, то попробуйте проверить, поддерживает ли ваша видеокарта OpenGL 3.3+ (если нет, то она действительно старая) и/или обновите драйверы.

Далее мы должны создать объект window, представляющий окно приложения. Присутствие данного объекта требуется для большинства других функций GLFW, т.к. именно через него происходит управление окном нашей программы:

Рассмотрим детально функцию glfwCreateWindow():

   первые два аргумента, которые она принимает, являются шириной и высотой окна;

   с помощью третьего аргумента мы указываем имя окну — "OpenGL for Ravesli.com" (вы можете установить любое другое имя);

   последние 2 параметра пока что можно проигнорировать.

Данная функция возвращает объект GLFWwindow, который позже понадобится нам для других операций GLFW. После этого, с помощью функции glfwMakeContextCurrent(window), мы сообщаем GLFW сделать контекст нашего окна основным контекстом в текущем потоке.

GLAD


На предыдущем уроке мы упоминали, что GLAD оперирует указателями на OpenGL-функции, поэтому мы должны сначала инициализировать GLAD, и только после этого можно пользоваться OpenGL-функциями:

В качестве параметра мы передаем GLAD-функцию, которая загружает адреса указателей на OpenGL-функции (которые могут отличаться, в зависимости от используемой вами ОС).

В то же время, библиотека GLFW содержит удобный инструмент в виде функции glfwGetProcAddress(), которая самостоятельно может определить нужные нам для работы функции, в зависимости от используемой операционной системы.

Окно просмотра

Прежде чем мы сможем начать рендеринг, нам нужно сообщить OpenGL размер видимой области окна, через которую пользователь сможет наблюдать за процессом рендеринга и отображения картинки. Эта область называется окном просмотра (англ. «viewport»). Его размеры задаются относительно основного окна с помощью функции glViewport():

Первыми двумя параметрами данной функции являются координаты нижнего левого угла окна просмотра. Третий и четвертый параметры устанавливают ширину и высоту окна для рендеринга в пикселях, которые мы устанавливаем равными размеру GLFW-окна: ширина окна составляет 800 пикселей, а высота — 600 пикселей.

Стоит заметить, что мы могли бы установить меньшие значения размеров окна просмотра относительно размеров GLFW-окна; в таком случае, весь OpenGL-рендеринг отображался бы в меньшем окне, и мы могли бы, например, отображать другие элементы вне окна просмотра OpenGL.

Примечание: На самом деле, OpenGL использует данные из функции glViewport(), чтобы преобразовать обработанные им 2D-координаты в координаты на вашем экране. Например, обработанная точка с координатами (-0.5; 0.5) будет (после финального преобразования) отображена в точку с координатами (200; 450) на экране. Обратите внимание, что обработанные координаты в OpenGL находятся в диапазоне от -1 до 1, поэтому мы отображаем диапазон обработанных координат (-1, 1) на соответствующем диапазоне координат (0, 800) и (0, 600) на экране.

Однако в тот момент, когда пользователь изменяет размеры окна, должен быть скорректирован и размер области окна просмотра. Для этого необходимо определить callback-функцию (или «функцию обратного вызова»), которая вызывается при каждом изменении размера окна. Её прототип показан ниже:

В качестве первого аргумента данная функция принимает указатель на объект GLFWwindow, а двумя следующими аргументами являются новые размеры области окна просмотра. Таким образом, всякий раз, когда изменяется размер окна приложения, GLFW вызывает данную функцию, передавая ей все необходимые для обработки аргументы:

Для того, чтобы сообщить GLFW, что мы хотим вызывать функцию framebuffer_size_callback() всякий раз, когда происходит изменение размеров окна, нам нужно прописать следующее:

Во время первого отображения окна также вызывается функция framebuffer_size_callback(), имеющая аналогичные размеры полученного окна. Стоит заметить, что для Retina-дисплеев ширина и высота, в конечном итоге, будут значительно больше исходных входных значений.

Существует обширное множество callback-функций, которые мы можем настроить для регистрации наших собственных функций. Например, мы можем определить callback-функцию для обработки изменений входных данных джойстика, обработки сообщений об ошибках и т.д. Регистрация callback-функций происходит после создания окна и до начала цикла рендеринга.

Цикл рендеринга


Мы не хотим, чтобы наше приложение нарисовало одну картинку, а затем сразу же закрыло окно. Мы хотим, чтобы наше приложение продолжало рисовать картинки и обрабатывать пользовательский ввод до тех пор, пока мы ему явно не сообщим остановиться. Для этого нам нужно создать цикл while, именуемый в дальнейшем циклом рендеринга, который будет работать до тех пор, пока мы сами не сообщим GLFW остановиться.

В следующем фрагменте кода показан пример довольно простого цикла рендеринга:

В начале каждой итерации цикла функция glfwWindowShouldClose() проверяет, сообщали ли мы GLFW закрыть приложение. Если это так, то функция возвращает true, и игровой цикл останавливается, после чего мы можем завершить выполнение нашего приложения. Функция glfwPollEvents() следит за тем, инициируются ли какие-либо события (например, ввод с клавиатуры или перемещение мышки), обновляет состояние окна и вызывает соответствующие функции (которые мы можем зарегистрировать с помощью callback-методов). Функция glfwSwapBuffers() меняет местами цветовой буфер (большой 2D-буфер, содержащий значения цвета для каждого пикселя в окне GLFW), который используется для рендеринга во время данной итерации рендеринга, и выводит его на экран.

Примечание: Когда приложение выполняет отрисовку сцены с использованием одного единственного буфера, то может появиться проблема в виде мерцающего изображения. Это происходит потому, что итоговое отображаемое изображение не создается мгновенно, а рисуется пиксель за пикселем слева направо и сверху вниз. Поскольку это изображение не появляется в одно мгновение для пользователя, то результат может содержать артефакты сжатия (глюки, искажения и т.д.). Чтобы избавиться от данных проблем, оконные приложения используют технологию двойного буфера: front-буфер содержит итоговое выходное изображение, которое пользователь видит на своем экране, в то время как все команды рисования/рендеринга выполняются в back-буфере. Как только все команды рендеринга закончат свою работу, мы меняем содержимое front-буфера с содержимым back-буфера, чтобы изображение можно было отобразить без выполнения его рендеринга, обходя тем самым вышеупомянутую проблему появления артефактов.

Последние штрихи

Как только мы выйдем из цикла рендеринга, нам нужно будет очистить/удалить все выделенные для GLFW ресурсы. Это можно сделать с помощью функции glfwTerminate(), которую необходимо вызвать в конце функции main():

Благодаря этому мы очистим все задействованные ранее ресурсы и корректно завершим выполнение приложения.

На данный момент полный исходный код нашего приложения выглядит следующим образом:

Теперь попробуйте скомпилировать приложение. Результат должен быть следующим:

Если у вас получилось очень скучное и унылое черное изображение, то вы всё сделали правильно!

Если же возникли проблемы с компиляцией приложения, то сначала убедитесь, что установлены все нужные параметры компоновщика и что вы правильно подключили нужные каталоги в свою IDE (как описано на предыдущем уроке). Кроме того, убедитесь, что ваш код не содержит ошибок; вы можете легко проверить его, сравнив с полным исходным кодом, расположенным выше. Если же у вас все равно остались какие-либо проблемы — вы можете задать свой вопрос в комментариях к этой статье.

Пользовательский ввод


Неплохо было бы реализовать возможность обрабатывать пользовательский ввод (например, нажатия клавиш клавиатуры, движения курсора мыши и т.п.). К счастью, в GLFW уже есть несколько функций, предназначенных для решения подобных задач. Одна из них — это GLFW-функция glfwGetKey(), принимающая в качестве первого аргумента объект пользовательского ввода (в нашем случае этим объектом будет window, т.е. само окно), а в качестве второго аргумента — какую клавишу клавиатуры требуется отслеживать. В результате функция возвращает ответ, была ли нажата в данный момент эта клавиша. Чтобы не допускать бардака в организации исходного кода нашей программы, мы определим новую функцию processInput(GLFWwindow *window), внутрь которой и поместим вызов функции glfwGetKey():

Как вы видите, в ней мы проверяем, нажал ли пользователь клавишу Escape. В случае положительного ответа функция glfwGetKey() возвращает значение GLFW_PRESS, если же нажатия не было, функция возвращает GLFW_RELEASE. Если пользователь действительно нажал клавишу Escape, то мы закрываем GLFW, установив с помощью функции glfwSetwindowShouldClose() значение true для свойства WindowShouldClose. Благодаря этому цикл рендеринга в блоке кода функции main() прервется, и наше приложение закроется.

Функция processInput() будет вызываться каждую итерацию нашего цикла рендеринга:

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

Рендеринг проекта

Все команды, так или иначе связанные с рендерингом, мы поместим в цикл рендеринга, чтобы они вызывались каждую итерацию цикла (или, как мы уже говорили, каждый фрейм цикла):

Чтобы проверить, действительно ли всё работает так, как нужно, мы попробуем закрасить наше окно в какой-нибудь произвольный цвет при помощи функции очистки экрана glClear(), которую поместим в начало нашего фрейма. Если данную функцию расположить в другом месте, то можно столкнуться с негативным эффектом, когда на экране будет виден результат отрисовки предыдущего фрейма.

Вызывая функцию glClear(), мы хотим, чтобы она очистила цветовой буфер экрана. Используя в качестве аргументов так называемые «буферные биты», мы можем указать нашей функции glClear() какой именно буфер требуется очистить:

   GL_COLOR_BUFFER_BIT — очистка буфера цвета;

   GL_DEPTH_BUFFER_BIT — очистка буфера глубины;

   GL_STENCIL_BUFFER_BIT — очистка буфера трафарета.

В данный момент нас интересуют значения цвета, поэтому мы будем очищать цветовой буфер:

Обратите внимание, что мы также задаем и цвет для очистки экрана с помощью функции glClearColor(). Всякий раз, когда мы вызываем glClear() и, тем самым, очищаем цветовой буфер, весь цветовой буфер будет заполнен цветом, заданным при помощи функции glClearColor(). В результате мы получим темно-зелено-голубоватый цвет.

Примечание: OpenGL-функция glClearColor() относится к классу функций изменения контекста. А функция glClear() — к классу функций использования контекста. Простыми словами — функция glClear() использует текущие настройки цвета, установленные при помощи функции glClearColor().

Результат должен быть следующим:

Итак, сейчас у нас уже всё готово для заполнения цикла рендеринга множеством различных команд, связанных с отрисовкой изображения на экране пользователя. Этим мы как раз и займемся на следующем уроке.

  GitHub / Исходный код. Урок №3. Наш первый проект в OpenGL — Создание окна

Оценить статью:

Звёзд: 1Звёзд: 2Звёзд: 3Звёзд: 4Звёзд: 5 (65 оценок, среднее: 4,94 из 5)
Загрузка...

Комментариев: 30

  1. Даниил:

    Спасибо за урок!
    Хотелось бы узнать, как можно отключить отображение консоли при запуске программы?

    1. Сергей Федоров:

      Привет, читайте все комментарии, снизу вверх. Даже если кто-то пишет про ошибки, которых у вас не было.

      …..
      Название Проекта->Свойства->Компоновщик->Система->Подсистема
      Project Name->Properties->Linker->System->SubSystem
      и выбираем Windows (/SUBSYSTEM:WINDOWS).

      Далее идём в:
      Название Проекта->Свойства->Компоновщик->Дополнительно->Точка входа
      Project Name->Properties->Linker->Advanced->Entery Point
      и вписываем mainCRTStartup.

  2. Владимир:

    Нынче изменили инициализацию библиотеки GLAD с    

    на 

    1. Владимир:

      Сразу хочу извинится что предоставил немного некорректную информацию.
      В общем есть 2 библиотеки glad, первая только для работы с OpenGL и там инициализация как указанно в данном уроке.
      Вторая библиотека содержит в себе все тоже что и первая плюс инструментарий для работы с Vulkan, вот там инициализация как я написал выше, все остальные функции(для OpenGL) во второй библиотеки идентичны первой. В интернете эту библиотеку можно найти как Glad2

  3. Михаил:

    Почему функция processInput не относится к callback-функциям?

  4. RomanIT:

    Настойка для VS 2019 Руская версия предполагается что вы распаковали так : C:\glfw-3.3.7, C:\glfw-3.3.7\build\src\Debug\glad

    (main.cpp)

    (glad.h и glfw3.h, добавить в проект)

    (main.h)

    Свойства-> Каталоги VC++ -> каталоги библиотек:
    C:\glfw-3.3.7\include\GLFW;C:\glfw-3.3.7\build\src\Debug;$(LibraryPath)

    Свойства-> C/C++ -> Дополнительные коталоги вклчаетмых файлов:
    C:\glfw-3.3.7\build\src\Debug;C:\glfw-3.3.7\include\GLFW;%(AdditionalIncludeDirectories)

    Свойства-> Компоновщик -> Ввод:
    glfw3.lib;OpenGL32.Lib;%(AdditionalDependencies)

    Что бы оно не отображалось идём в настройки проекта:
    Название Проекта->Свойства->Компоновщик->Система->Подсистема
    Project Name->Properties->Linker->System->SubSystem
    и выбираем Windows (/SUBSYSTEM:WINDOWS).

    Далее идём в:
    Название Проекта->Свойства->Компоновщик->Дополнительно->Точка входа
    Project Name->Properties->Linker->Advanced->Entery Point
    и вписываем mainCRTStartup.

  5. Владислав:

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

  6. Сергей:

    Там где мы используем callback функцию лучше всего использовать лямбду, т.к. функцию framebuffer_size_callback мы используем только один раз для ее передачи в параметр:

  7. Александр Корсак:

    1 вопрос:

    Я запустил вашу проверку на то, подключилась ли библиотека GLAD:

    выдало в консоли "Failed to initialize GLAD", хотя, сама библиотека запустилась без ошибок.

    2 вопрос:

    У меня поставлен цикл while, но почему то у меня окно открывается и закрывается через несколько секунд. В то время, как на следующей вашей статье такой проблемы я не наблюдал

    1. 7IMB01:

      Вы наверно забыли добавить в основной цикл эту строчку:

  8. Anton:

    Всем привет!

    1. Почему мы не задаем: glViewport(0, 0, SCR_WIDTH, SCR_HEIGHT);
    в теле main, ведь функция framebuffer_size_callback вызывается только в случае изменения размера окна. Эти значения по умолчанию соответствуют окну в GLFW?

    2. Попробовал изменить все 4 аргумента, и не увидел изменений ни для левого верхнего угла, ни для размера окна отрисовки. Что делаю или понимаю не верно?

    1. Фото аватара Дмитрий Бушуев:

      >>Почему мы не задаем: glViewport(0, 0, SCR_WIDTH, SCR_HEIGHT); в теле main?
      1. В данном уроке мы, по большому счёту, еще ничего толком не рендерим (нет вызовов функций типа glDraw*), а только лишь заливаем фон выбранным цветом :). Но если хочется, то вы можете открыть исходники Урока №4, вставить вызов функции glViewport() внутрь функции main() (до цикла рендеринга while()) и получить желаемый результат.

      >>Эти значения по умолчанию соответствуют окну в GLFW?
      2. Да.

      >>2. Попробовал изменить все 4 аргумента, и не увидел изменений ни для левого верхнего угла, ни для размера окна отрисовки. Что делаю или понимаю не верно?
      3. См. п.1 и п.2. 🙂

  9. Демьян:

    На данный момент полный исходный код нашего приложения выглядит следующим образом:

    В этом моменте код опережает события, он выглядит в этот момент несколько иначе всё же.

    1. Фото аватара Дмитрий Бушуев:

      Спасибо за подсказку. Поправим 🙂

  10. Сергей:

    Дмитрий, спасибо за уроки.
    у меня проблема, все сделал по инструкции — при сборке выдает ошибку
    "Ошибка LNK2019 ссылка на неразрешенный внешний символ gladLoadGLLoader в функции main."
    такое ощущение что он библиотеку не подключил.

    а если я вставляю

    то пишет
    Ошибка LNK1104 не удается открыть файл "GLFW.lib"

    Windows 10.
    Visual Studio 2019
    в свойствах проекта вроде все прописал.
    режим debug x64

    1. Данила:

      Нашел решение проблемы: нужно в обозревателе решений, в исходные файлы добавить glad.c

      1. Алексей:

        спасибо, помогло

  11. misha:

    Здравствуйте, я скопировал готовый код в этом уроке чтобы проверить как работают библиотеки и у меня не создается и в консоли пишет
    Failed to create GLFW window. Получается переменная window равно нулю (Null), я не знаю как это решить. Кто-нибудь может помочь?
    Да кстати я работаю в Visual Studio 2019 на windows

    1. misha:

      Я уже решил проблему. Сначала я почитал интернете что нужно обновить драйвера,я их обновил, но ничего не вышло. Потом я заметил что используется не дискретная(на которую я установил драйвера) а встроенная и я отключил в диспетчере устройств встроенную и о чудо! Всё заработало

  12. Евгений:

    Добрый день! Та же проблема, что и у Ильи. ОС(Win10 PRO), IDE Visual Studio 2019.

    при попытке подключения
    #pragma comment ( lib, "opengl32.lib" )
    #pragma comment ( lib, "glu32.lib" )
    #pragma comment ( lib, "GLFW.lib" )
    #pragma comment ( lib, "GLFWDLL.lib" )

    Заявляет, что не может обнаружить GLFW.lib и GLFWDLL.lib

    Если я правильно помню, в самом уроке данные файлы не указывались и не создавались.

    1. Евгений:

      Вопрос разрешился очень просто: при компиляции не был выбран режим дебага 64, а библиотеку GLFW мы делали именно 64-битную.

  13. Алексей:

    Решение проблемы в CodeBlocks (20.03) по поводу ошибок undefined reference, а также корректная установка GLFW3.
    1. Скачиваем архив GLFW3 и распаковываем в любую папку. Внутри будут папки include, lib_mingw-w64.
    2. Из папки include кидаем всё содержимое по пути установки CodeBlocks\MinGW\x86_64-w64-mingw32\include\
    3. Из папки lib_mingw-w64 копируем все файлы с расширением .а по пути установки CodeBlocks\MinGW\x86_64-w64-mingw32\lib
    4. Из папки lib_mingw-w64 копируем файл glwf3.dll в папку C:\windows\System32
    5. Запускаем CodeBlocks, создаём ПУСТОЙ проект на основе шаблона Empty Project.
    6. Выбираем меню Project -> Build Options…
    7. В настройках проекта слева выбираем самый верхний уровень (НЕ release или debug), который обычно называется именем вашего проекта.
    8. Переходим на закладку Search directories (тут программа может перескочить снова на уровень release или debug, поэтому ещё раз выбираем самый верхний уровень!!!).
    9. На закладке Search directories переходим на закладку Compiler, жмём кнопку Add и выбираем путь, куда вы в самом начале копировали файлы из папки include (что-то типа CodeBlocks\MinGW\x86_64-w64-mingw32\include\GLFW).
    10. На закладке Linker повторяем то же самое, только указываем папку lib (CodeBlocks\MinGW\x86_64-w64-mingw32\lib\)
    11. Переходим на закладку Linker settings (она на одном уровне с закладкой Search directories). Жмём кнопку Add в области Link libraries, вписываем в поле glfw3 и сразу жмём кнопку ОК. Повторяем то же самое с параметрами opengl32 и gdi32. В итоге у вас должен получиться список из трёх библиотек:
    glfw3
    opengl32
    gdi32
    12. Жмём кнопку ОК в настройках сборки и на этом настройка GLFW3 в CodeBlocks 20.03 завершена.
    Удачи!!!

  14. Sergey:

    Здравствуйте.
    Скажите, куда именно добавить файл glad.c в свой проект?
    У меня CodeBlocks. Не компилируется.
    Пишет undefined reference to Glad…

    1. Фото аватара Дмитрий Бушуев:

      Добрый день.
      Какую ОС вы используете (Windows/Linux)?
      Можете привести скриншот ошибки?

      1. Sergey:

        Здравствуйте.
        Использую Windows. Проблема решилась. Просто добавить файл надо было в проекте. Извините за глупый вопрос.

        1. Фото аватара Дмитрий Бушуев:

          Хорошо, что всё разрешилось 🙂

  15. Илья:

    Здравствуйте, всё подключил, но выводит ошибки:
    ссылка на неразрешённый внешний символ _glfwInit в функции _main
    ссылка на неразрешённый внешний символ _glfwWindowHint в функции _main

    1. Фото аватара Дмитрий Бушуев:

      Добрый день.
      Судя по всему заголовочные файлы glfw подключены, а вот библиотеки — нет.
      Какая ОС/IDE используются (и их версии)?

      P.S.: Попробуйте в своей программе после подключения заголовочных файлов дописать следующие строки:
      […]
      #include <glad/glad.h>
      #include <GLFW/glfw3.h>
      #pragma comment ( lib, "opengl32.lib" )
      #pragma comment ( lib, "glu32.lib" )
      #pragma comment ( lib, "GLFW.lib" )
      #pragma comment ( lib, "GLFWDLL.lib" )

  16. Кирилл:

    Спасибо за очень подробный урок!

    1. Фото аватара Дмитрий Бушуев:

      Всегда пожалуйста. 🙂

Добавить комментарий для Владимир Отменить ответ

Ваш E-mail не будет опубликован. Обязательные поля помечены *