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

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

  Обновл. 7 Апр 2020  | 

 1464

 ǀ   4 

В этом уроке мы создадим наш первый проект в 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():

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

Стоит заметить, что мы могли бы установить меньшие значения размеров окна просмотра, относительно размеров 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().

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

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


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

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

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

  1. Аватар Илья:

    Здравствуйте, всё подключил, но выводит ошибки:
    ссылка на неразрешённый внешний символ _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" )

  2. Аватар Кирилл:

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

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

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

Добавить комментарий

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