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

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

  Обновл. 8 Июн 2020  | 

 8469

 ǀ   19 

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

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

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

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


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

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

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

  1. Аватар 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. 🙂

  2. Аватар Демьян:

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

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

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

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

  3. Аватар Сергей:

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

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

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

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

    1. Аватар Данила:

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

  4. Аватар misha:

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

    1. Аватар misha:

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

  5. Аватар Евгений:

    Добрый день! Та же проблема, что и у Ильи. ОС(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-битную.

  6. Аватар Алексей:

    Решение проблемы в 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 завершена.
    Удачи!!!

  7. Аватар Sergey:

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

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

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

      1. Аватар Sergey:

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

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

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

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

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

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

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

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

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

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

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