Простой кроссплатформенный OpenGL-проект на C++

24 августа 2015

В свободное время я потихоньку раскуриваю OpenGL. Это у меня что-то вроде нового хобби. Как кто-то из вас может помнить, когда-то я уже пробовал играться с OpenGL на Haskell. Однако использовать тамошний ни на что не похожий и практически не документированный DSL оказалось слишком сложной задачей для меня, и от затеи пришлось отказаться.

Надо отметить, что в этот раз я чуть было не совершил ту же ошибку, выбрав поначалу язык Scala и библиотеку LWJGL. С документацией у LWJGL все отлично, никакого своего DSL не накручено, и примеров в сети очень много. Но проблема заключается в том, что в большинстве туториалов и книг по OpenGL все же используется язык C++. И если ты столкнешься с какой-то проблемой, никто ни в каком IRC или на StackOverflow не станет разбираться в твоем коде на Scala. Плюс неизвестно еще, ты просто неправильно используешь OpenGL, или может это какая-то ошибка в LWJGL, или может это какой-то хитрый баг, вызванный взаимодействием кода на Scala с LWJGL. Сообщество вокруг LWJGL достаточно велико само по себе, но чтобы кто-то из этого сообщества тебе помог, код должен быть на Java, а с Java связываться ну очень не хотелось. Кроме того, у LWJGL на данный момент есть стабильная, но постепенно теряющая свою актуальность версия 2.9, а также совершенно новая, ни с чем не совместимая, и все еще находящаяся в альфе версия 3.0.

В силу всех этих причин на время изучения OpenGL я решил использовать C++. Когда у меня будет больше опыта в самом OpenGL, можно будет попробовать использовать и альтернативные языки, например, Scala или Go. В качестве системы сборки был выбран CMake. Дело в том, что в Linux, активным пользователем которого я являюсь, по всей видимости, пока что наиболее приличной IDE для C++ является CLion. Я реально проводил серьезный ресерч на эту тему. А CLion ничего кроме CMake пока не поддерживает. Кроме того, у меня есть очень приятный опыт использования продукции JetBrains в прошлом, да и в настоящем.

Интересно, как культура и традиции могут определять выбор технологий, не правда ли?

Теперь, когда инструментарий выбран, можно создать новый Git-репозиторий, открыть CLion и создать в этом репозитории новый проект. В проекте нам понадобятся кое-какие зависимости. И тут начинается интересное, так как общепринятого способа управления зависимостями в C++ вроде как нет. Довольно перспективно выглядит Biicode, но, насколько я могу судить, пока что он не получил широкого распространения. Не то, чтобы я много работал с C++ в последнее время, но похоже, что неплохое решение заключается в следующем. Зависимости, которые можно поставить через менеджер пакетов ОС, например, apt-get, ставятся через него. Прочие же зависимости подтягиваются в виде исходных кодов через механизм сабмодулей в Git.

Например, нам в нашем проекте понадобятся библиотеки GLM и GLFW. Первая предназначена для работы с матрицами и векторами. Вторая — для рисования окошек, реагирования на движения мыши, нажатия клавиш клавиатуры и так далее. Еще для решения той же задачи есть GLUT. В одних туториалах используется GLFW, в других GLUT, и в чем принципиальное различие между ними на данный момент мне не до конца понятно. Так что, по сути, тут я выбирал наугад. Итак, используя сабмодули Git, подключаем эти библиотеки к проекту:

git submodule add https://github.com/glfw/glfw glfw
git submodule add https://github.com/Groovounet/glm glm

Если вы склонируете репозиторий на другую машину, подтянуть зависимости можно так:

git submodule init
git submodule update

Интересный факт о сабмодулях — они смотрят на конкретный коммит. Изменить его можно, перейдя в каталог сабмодуля и сделав checkout интересующей ветки или тэга. После этого git diff вашего собственного репозитория покажет, что в нем есть изменения, который можно закоммитить. Также в какой-то момент вы можете обнаружить, что удаляются сабмодули Git очень непросто. Делается это так (почти рабочий рецепт был найден на gist, приведенная ниже инструкция тестировалась на Git 1.9.1):

  1. Скажите git rm --cached имя_сабмодуля;
  2. Удалите соответствующие строчки из файла .gitmodules;
  3. Также грохните соответствующую секцию в .git/config;
  4. Сделайте коммит;
  5. Удалите файлы сабмодуля;
  6. Удалите каталог .git/modules/имя_сабмодуля;

Теперь правим файл CMakeList.txt нашего проекта:

cmake_minimum_required(VERSION 2.8)
project(red_triangle)

find_package(OpenGL REQUIRED)

add_subdirectory(glfw)

include_directories(glfw/include)
include_directories(glm)

set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O2 -Wall -Wextra -std=c++11")

set(SOURCE_FILES main.cpp)
add_executable(red_triangle ${SOURCE_FILES})

target_link_libraries(red_triangle
  glfw ${GLFW_LIBRARIES} ${OPENGL_LIBRARY})

Как видите, здесь просто указывается минимальная версия cmake, название проекта, его зависимости и флаги компиляции. Ничего сложного.

В main.cpp пишем следующий код:

#include <glm/glm.hpp>
#include <GLFW/glfw3.h>
#include <glm/gtc/type_ptr.hpp>
#include <glm/gtx/transform.hpp>
#include <iostream>

int main() {
  if(!glfwInit()) {
    std::cerr << "Failed to initialize GLFW" << std::endl;
    return -1;
  }

  glfwDefaultWindowHints();

  GLFWwindow* window = glfwCreateWindow(300, 300, "Red Triangle",
                                        nullptr, nullptr);
  if(window == nullptr) {
    std::cerr << "Failed to open GLFW window" << std::endl;
    glfwTerminate();
    return -1;
  }

  glfwMakeContextCurrent(window);
  glfwSwapInterval(1);
  glfwShowWindow(window);

  // Важно! Не эквивалентно glEnable(GL_DEPTH_TEST | GL_DOUBLEBUFFER)
  glEnable(GL_DEPTH_TEST);
  glEnable(GL_DOUBLEBUFFER);
  glDepthFunc(GL_LESS);

  glClearColor(0, 0, 0, 1);

  glm::mat4 m = glm::perspective(45.0f, 4.0f / 3.0f, 1.0f, 100.0f);
  glMatrixMode(GL_PROJECTION);
  glLoadMatrixf(glm::value_ptr(m));

  while(glfwWindowShouldClose(window) == GL_FALSE) {
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    glColor4f(1, 0, 0, 1);
    glBegin(GL_TRIANGLES);
    glVertex3f(   0,  0.5, -5);
    glVertex3f( 0.5, -0.5, -5);
    glVertex3f(-0.5, -0.5, -5);
    glEnd();

    glfwSwapBuffers(window);
    glfwPollEvents();
  }

  glfwDestroyWindow(window);
  glfwTerminate();
  return 0;
}

Важно! Имейте в виду, что все эти glBegin, glEnd и glVertex3f — это устаревший OpenGL. В новых проектах вы не должны их использовать. Здесь эти функции используются только для того, чтобы не уводить повествование в сторону VAO, VBO и шейдеров. Заинтересованные читатели могут ознакомиться с почти таким же проектом на новом OpenGL.

Код предположительно очень прост и на данном этапе не нуждается в особых пояснениях. Здесь вы видите пример создания окна при помощи GLFW, простой пример использования GLM, а также рисование красного треугольника на черном фоне при помощи устаревших функций OpenGL. Заинтересованные читатели могут найти больше теории в предыдущих постах этого блога, посвященных OpenGL, или дождаться будущих. Результат же будет выглядеть как-то так:

Рисуем красный треугольник на OpenGL

А сборка проекта «вручную», без использования CLion, осуществляется так:

mkdir build
cd build
cmake ..
make

Примечательно, что это же проект успешно компилируется и под Windows. Там есть свои сложности, связанные с необходимостью устанавливать MinGW и Git для Windows, но в целом все работает точно так же. Еще может потребоваться немного подправить идущий с MinGW файл math.h, так как в нем есть баг. Кроме того, код успешно собирается и под MacOS.

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

Дополнение: Продолжаем изучение OpenGL: VBO, VAO и шейдеры

Метки: , , .


Вы можете прислать свой комментарий мне на почту, или воспользоваться комментариями в Telegram-группе.