В прошлом уроке мы подготовили рабочее пространство и теперь мы полностью готовы создать окно.

Данный перевод подготовлен совместно с FERusM за что ему большое спасибо.

Заинтересовавшихся прошу под кат.

Меню


1. Начинаем

  1. OpenGL
  2. Создание окна
  3. Hello Window
  4. Hello Triangle


Часть 1.3. Hello Window


После установки GLFW самое время сделать простенькую программку, как это принято в подобных материалах, пусть это будет Hello World. Для начала нужно создать .cpp файл и подключить несколько заголовочников, также необходимо установить переменную GLEW_STATIC, которая указывает на то, что мы будем использовать статическую версию библиотеки GLEW.

// GLEW нужно подключать до GLFW.
// GLEW
#define GLEW_STATIC
#include <GL/glew.h>
// GLFW
#include <GLFW/glfw3.h>

Убедитесь в том, что подключение GLEW происходит раньше GLFW. Заголовочный файл GLEW содержит в себе подключение всех необходимых заголовочных файлов OpenGL, таких как GL/gl.h

Заметка от переводчика
Как заметил TrueBers это, предположительно, просто устаревший костыль и современные версии GLFW сами подключают требуемые библиотеки, правда если не установлен флаг GLFW_INCLUDE_NONE, а по умолчанию он не объявлен.

Далее напишем функцию main, пока что в ней будет создаваться окно GLFW. Она будет иметь следующий вид:

int main()
{	
	//Инициализация GLFW
	glfwInit();
	//Настройка GLFW
	//Задается минимальная требуемая версия OpenGL. 
	//Мажорная 
	glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
	//Минорная
	glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
	//Установка профайла для которого создается контекст
	glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
	//Выключение возможности изменения размера окна
	glfwWindowHint(GLFW_RESIZABLE, GL_FALSE);

	return 0;
}

В данной функции мы сначала инициализируем GLFW вызывом функции glfwInit, после чего приступаем к его настройке, используя функцию glfwWindowHint. glfwWindowHint имеет очень простую сигнатуру, первым аргументом необходимо передать идентификатор параметра, который подвергается изменению, а вторым параметром передается значение, которое устанавливается соответствующему параметру. Идентификаторы параметров, а также некоторые их значения находятся в общем перечислении с префиксом GLFW_. Больше подробностей о настройке контекста GLFW можно найти в официальной документации GLFW. Если при запуске этого примера вы получаете ошибки, сильно похожие на неопределенное поведение, это значит то, что вы неправильно подключили библиотеку GLFW.

Поскольку в статьях будет использоваться OpenGL версии 3.3, то необходимо сообщить GLFW то что мы используем именно эту версию, что происходит в результате вызова метода glfwWindowHint c аргументами:

GLFW_CONTEXT_VERSION_MAJOR, 3
GLFW_CONTEXT_VERSION_MINOR, 3

Таким образом, GLFW производит все необходимые действия при создании OpenGL контекста. Это гарантирует то, что если у пользователя нет необходимой версии OpenGL (в данном случае рассматривается версия 3.3), то GLFW просто не запустится. Помимо установки версии, мы явно указали на то, что будем использовать профиль GLFW_OPENGL_CORE_PROFILE. Это приведет к ошибке в случае использования устаревших функций OpenGL. Если вы используете Mac OS X, то необходимо добавить следующий вызов функции glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE) в код инициализации GLEW.
Убедитесь в наличии поддержки OpenGL версии 3.3 и выше вашим железом и наличие установленного OpenGL соответствующей версии в ОС. Для того, чтобы узнать версию OpenGL на вашем компьютере под Linux используйте glxinfo в консоли. Для Windows можно использовать программу OpenGL Extension Viewer. Если версия OpenGL ниже необходимой убедитесь в том что ваше железо поддерживает его и/или попробуйте обновить драйвера.

Теперь нужно создать объект окна. Этот объект содержит всю необходимую информацию об окне и используется функциями GLFW.

GLFWwindow* window = glfwCreateWindow(800, 600, "LearnOpenGL", nullptr, nullptr);
if (window == nullptr)
{
	std::cout << "Failed to create GLFW window" << std::endl;
	glfwTerminate();
	return -1;
}
glfwMakeContextCurrent(window);

Сигнатура функции glfwCreateWindow требует следующие аргументы: “Высота окна”, “Ширина окна”, “Название окна” (оставшиеся аргументы нам не понадобятся). Возвращает указатель на объект типа GLFWwindow, который нам потом понадобится. Далее мы создаем контекст окна, который будет основным контекстом в данном потоке.

GLEW


В прошлом уроке мы говорили, что GLEW управляет указателями на функции OpenGL, соответственно мы должны инициализировать GLEW, перед тем как вызывать какие либо функции OpenGL.

glewExperimental = GL_TRUE;
if (glewInit() != GLEW_OK)
{
    std::cout << "Failed to initialize GLEW" << std::endl;
    return -1;
}

Заметьте, что мы установили переменную glewExperimental в GL_TRUE, перед тем как инициализировать GLEW. Установка значения glewExperimental в GL_TRUE позволяет GLEW использовать новейшие техники для управления функционалом OpenGL. Также, если оставить эту переменную со значением по умолчанию, то могут возникнуть проблемы с использованием Core-profile режима.

Viewport


Прежде чем мы начнем что-либо отрисовывать нам надо еще кое что сделать. Нам нужно сообщить OpenGL размер отрисовываемого окна, чтобы OpenGL знал, как мы хотим отображать данные и координаты относительно окна. Мы можем установить эти значения через функцию glViewport.

int width, height;
glfwGetFramebufferSize(window, &width, &height);
  
glViewport(0, 0, width, height);

Первые 2 аргумента функции glViewport — это позиция нижнего левого угла окна. Третий и четвертый — это ширина и высота отрисовываемого окна в px, которые мы получаем напрямую из GLFW. Вместо того, чтобы руками задавать значения ширины и высоты в 800 и 600 соответственно мы будем использовать значения из GLFW, поскольку такой алгоритм также работает и на экранах с большим DPI (как Apple Retina).

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

Подготавливаем двигатели


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

while(!glfwWindowShouldClose(window))
{
    glfwPollEvents();
    glfwSwapBuffers(window);
}

Функция glfwWindowShouldClose проверяет в начале каждой итерации цикла, получил ли GLFW инструкцию к закрытию, если так — то функция вернет true и игровой цикл перестанет работать, после чего мы сможем закрыть наше приложение.

Функция glfwPollEvents проверяет были ли вызваны какие либо события (вроде ввода с клавиатуры или перемещение мыши) и вызывает установленные функции (которые мы можем установить через функции обратного вызова (callback)). Обычно мы вызываем функции обработки событий в начале итерации цикла.

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

Еще кое что


Как только мы вышли из игрового цикла, надо очистить выделенные нам ресурсы. Делается это функцией glfwTerminate в конце main функции.

glfwTerminate();
return 0;

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


Если у вас отобразилась скучнейшая черная картинка — то вы все сделали правильно! Если у вас отрисовывается что-то другое или у вас возникли проблемы с соединением всех примеров в уроке, то попробуйте этот исходный код.

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

Ввод


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

void key_callback(GLFWwindow* window, int key, int scancode, int action, int mode);

Эта функция принимает первым аргументом указатель на GLFWwindow, далее идет число описывающее нажатую клавишу, действие осуществляемое над клавишей и число описывающее модификаторы (shift, control, alt или super). Когда будет нажата клавиша, GLFW вызовет эту функцию и передаст в нее требуемые аргументы.

void key_callback(GLFWwindow* window, int key, int scancode, int action, int mode)
{
    // Когда пользователь нажимает ESC, мы устанавливаем свойство WindowShouldClose в true, 
    // и приложение после этого закроется
    if(key == GLFW_KEY_ESCAPE && action == GLFW_PRESS)
    	glfwSetWindowShouldClose(window, GL_TRUE);
}

В нашей (новой) key_callback функции мы проверяем является ли нажатая клавиша клавишей ESC и если на нее нажали (а не отпустили) — то мы закрываем GLFW устанавливая свойство WindowShouldClose в true используя glfwSetWindowShouldClose. Следующая проверка состояния в игровом цикле прервет цикл и приложение закроется.

Осталось только передать это функцию в GLFW. Делается это следующим образом:

glfwSetKeyCallback(window, key_callback);  

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

Отрисовка


Нам хотелось бы разместить все команды отрисовки в игровом цикле, так как мы хотим, чтобы отрисовка происходила на каждой итерации цикла. Это должно выглядеть как-то так:

// Игровой цикл
while(!glfwWindowShouldClose(window))
{
    // Проверяем события и вызываем функции обратного вызова.
    glfwPollEvents();

    // Команды отрисовки здесь
    ...

    // Меняем буферы местами
    glfwSwapBuffers(window);
}

Чтобы просто удостовериться в том, что все работает как надо мы будем очищать экран, заливая его своим цветом. В начале каждой итерации отрисовки зачастую надо очищать экран, иначе мы будем видеть результаты прошлой отрисовки (иногда действительно надо добиться такого эффекта, но зачастую это не так). Мы можем с легкостью очистить буфер, использовав glClear, в которую мы передадим специальные биты, чтобы указать какие конкретно буферы надо очистить. Биты, которые мы можем сейчас установить — это GL_COLOR_BUFFER_BIT, GL_DEPTH_BUFFER_BIT и GL_STENCIL_BUFFER_BIT. Сейчас нам надо очистить только цветовой буфер.

glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);

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

Как вы могли понять, glClearColor — это функция устанавливающая состояние, а glClear — это функция использующая состояние, которая использует состояние для определения цвета заполнения экрана.


Полный исходный код урока можно найти здесь.

Теперь у нас есть все, чтобы начать заполнять игровой цикл вызовами функций отрисовки, но мы прибережем это для следующего урока.
Поделиться с друзьями
-->

Комментарии (23)


  1. VGrabko
    30.09.2016 04:39
    +5

    как по мне то все три урока можно было собрать в 1


    1. Hithroc
      30.09.2016 13:11
      +1

      Это же просто перевод learnopengl.com. Переводчик сохраняет структуру (и правильно делает, по-моему).


    1. Megaxela
      30.09.2016 19:43

      Да, я говорил об этом в первой статье, но решил все таки сохранить иерархию оригинала


  1. Ti_Fix
    30.09.2016 10:32

    Вот и окно появилось. Замечательно. Жду продолжения и шейдеров побольше!


  1. iperov
    30.09.2016 10:49
    -5

    Смысл этих статей в век бесплатных графических движков?


    1. GoldRenard
      30.09.2016 16:27
      +2

      В том, чтобы разобраться как оно все действительно работает изнутри, а не брать бесплатный движок и «шмяк-шмяк и в продакшен».


      1. iperov
        30.09.2016 16:29
        -1

        так продакшен в готовых движках на 4 порядка лучше будет, чем рисование полигонов на экране уровня конца 90х


        1. GoldRenard
          30.09.2016 16:33
          +1

          Еще раз, ключевое слово разобраться. Никто не предлагает тут писать свой «Crysis за три вечера».


        1. Jester92
          30.09.2016 17:34
          +1

          Но кто-то же должен писать готовые движки? Для этого нужно разбираться, как это делать. Если раньше только иногда вспоминалась «Академия» Азимова, то сейчас, при прочтении подобных комментариев или статей, прямо уже вижу картину, как физики-ядерщики возводятся в ряд жрецов.


          1. iperov
            30.09.2016 17:56
            -1

            поверьте, компании создающие игровые движки не нуждаются в хабрах


            1. Jester92
              30.09.2016 18:08
              +1

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


            1. playermet
              02.10.2016 19:27
              +1

              И откуда они по вашему берутся? Самозарождаются из воздуха сразу с топовыми специалистами?


        1. lgorSL
          01.10.2016 14:44
          +1

          так продакшен в готовых движках на 4 порядка лучше будет, чем рисование полигонов на экране уровня конца 90х

          Ага, а после такого выходят игры на юнити, в которых "пиксельный" спрайт в 16*16 крупных пикселей рисуется как здоровенная текстура типа 128*128, потому что разработчик не осилил параметры фильтрации текстур.


          1. iperov
            01.10.2016 15:20
            -1

            чего там осиливать? это выбирается в импорте текстуры.

            glfw и прочее это прошлый век, никто сейчас не теряет на это время.


            1. RussDragon
              02.10.2016 12:04
              +2

              Смелое заявление. Это либо какой-то толстый троллинг, видимо полнейшее непонимание предназначения GLFW/SDL/SFML и прочих OGL-библиотек.
              Я искренне не вижу смысла тащить чужую архитектура движка и ненужные модули для каких-то небольших проектов.
              И тем более глупо использовать Unity/UE для какой-нибудь 2D игры, где от OpenGL'а требуется лишь отрисовка текстур и, может быть, освещения. В таких случаях небольшой самописный рендер уменьшит размер и увеличит скорость загрузки игры на порядок.


              1. iperov
                02.10.2016 13:31
                -1

                предлагаешь писать велосипед? на хабре велосипеды жутко караются вместе с авторами


                1. RussDragon
                  02.10.2016 15:53
                  +1

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


            1. FoxCanFly
              05.10.2016 19:04

              И чем же кроссплатформенно работают с OGL контекстом в веке нынешнем по-вашему?


  1. RussDragon
    30.09.2016 19:05

    игровой цикл

    Мне кажется, понятие «игровой цикл» значит чуть другое. Я понимаю, что это перевод и в оригинале написано именно так, но, поправьте если ошибаюсь, game loop – это основной цикл обработки всего. Начиная с графики и заканчивая network-манипуляциями.


    1. Megaxela
      30.09.2016 19:38

      Да, вы правы.


  1. TrueBers
    01.10.2016 07:58
    +1

    Убедитесь в том, что подключение GLEW происходит раньше GLFW. Заголовочный файл GLEW содержит в себе подключение всех необходимых заголовочных файлов OpenGL, таких как GL/gl.h

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


    1. Megaxela
      01.10.2016 11:41

      Хм, хорошо, спасибо. Как только появится возможность — сделаю сноску по этому поводу.


  1. konshyn
    07.10.2016 12:15

    Вот написал я программу по открытию окна.
    Создал окно 720x720. Но почему-то glfwGetFramebufferSize() возвращает мне 1440x1440. Хотя окно выглядит как 720x720, но OpenGL рисует правильно только при glViewport(1440x1440), а при 720х720 смещается к началу (в левый нижний угол окна).
    Может кто знает, в чем дело?