Хабрахабр

hellOGL: OpenGL hello world

Сегодня я покажу, как открыть окно и создать контекст OpenGL. Это на удивление непростая задача, OpenGL до сих пор не имеет официальных кроссплатформенных средств создания контекста, поэтому будем опираться на сторонние библиотеки (в данном случае GLFW и glad). В интернете уже очень много подобных hello world, но всё, что я видел, мне не нравится: или оно очень навороченное, или картинки в примерах уж очень примитивные (либо и то, и другое!). Большое спасибо всем авторам, но я выкачу очередной туториал 🙂

Сегодня мы отрисуем вот такое:


Эта модель нарисована художником Сэмюэлем Шаритом (arshlevon), огромное ему спасибо за разрешение её использовать в рамках моего курса лекций по компьютерной графике!

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

Поэтому сегодня я покажу, как отрисовать диаблу с хардверным ускорением, причём я во многом воспользуюсь кодом из репозитория софтверного растеризатора. Удивительно, но многие мои студенты не понимают, что этот софтверный растеризатор — не просто игрушка, но самое настоящее введение в то, как работает OpenGL.

Мой код нужен лишь для того, чтобы знать, что именно в документации читать, и в каком порядке. Внимание, я не ставлю себе задачей объяснить каждую строчку кода, так как полагаюсь на то, что чтение документации — самый хороший способ всё понять. Я потратил очень много времени на tinyrenderer, где это всё разложено по полочкам. Более того, я не буду объяснять, что такое шейдеры, и я не буду объяснять, как считать карты нормалей.

Весь репозиторий живёт здесь; создал по одному коммиту на каждый шаг туториала, так как гитхаб даёт очень удобную просматривалку всех внесённых изменений. Начинаем вот отсюда, наша цель получить вот такое окошко:

Под линуксом последняя версия кода компилируется вот так: Компилируется код при помощи CMake; я проверял под линуксом (g++) и виндой (Visual Studio 2017).

git clone --recurse-submodules https://github.com/ssloy/hellOGL.git
cd hellOGL
mkdir build
cd build
cmake ..
make

Используйте `git checkout`, если хотите компилировать отдельный коммит, а не последнюю версию. Этот код подгружает glad и GLFW, создаёт окошко с необходимым клавиатурным коллбэком, и подгружает с диска пустые вершинный и пиксельный шейдеры.
Изменения в проекте, внесённые на этом этапе, смотреть здесь. На данном этапе наша цель распарсить файл 3Д модели, и нарисовать первые треугольники, не заботясь на данный момент об освещении:

Может, не так уж и бесполезен софтверный рендерер? Обратите внимание, что и саму модель, и библиотеку для работы с векторами, да и сам парсер модели я взял целиком из tinyrenderer.

Мы сначала подгрузили 3Д модель, а затем я создаю массив vertices размером 3 * 3 * (количество треугольников). Основная идея в современном OpenGL очень простая. Каждая вершина описывается тремя числами (x,y,z). Каждый треугольник имеет три вершины, так? Итого для описания всей модели нам достаточно 3*3*model.nfaces():

std::vector<GLfloat> vertices(3*3*model.nfaces(), 0); for (int i=0; i<model.nfaces(); i++) }

А затем мы скажем OpenGL, что вот массив, рисуй, родной!

while (!glfwWindowShouldClose(window)) {
[...]
 glDrawArrays(GL_TRIANGLES, 0, vertices.size());
[...]
}

Вершинный шейдер ничего интересного не делает, он просто передаёт фрагментному шейдеру данные как есть:

#version 330 core // Input vertex data, different for all executions of this shader
layout(location = 0) in vec3 vertexPosition_modelspace; void main() { gl_Position = vec4(vertexPosition_modelspace, 1); // Output position of the vertex, in clip space
}

Ну и фрагментный шейдер тоже незатейлив. Он просто рисует текущий пикесль красным цветом:

#version 330 core // Output data
out vec3 color; void main() { color = vec3(1,0,0);
}

Самое сложное сделано, теперь дело техники!
Изменения в проекте, внесённые на этом этапе, смотреть здесь. Мы должны получить вот такую картинку:

Поэтому вдобавок к массиву vertices я добавил ещё один массив normals. Диффузное освещение в модели Фонга, как известно, это простое скалярное произведение между
нормальным вектором и вектором освещения. Не глядя в код скажите, какого он размера?

Самое интересное происходит во фрагментном шейдере, в основном .cpp файле происходит лишь подгрузка данных:

#version 330 core // Output data
out vec3 color; // Interpolated values from the vertex shaders
in vec3 Normal_cameraspace;
in vec3 LightDirection_cameraspace; void main() { vec3 n = normalize(Normal_cameraspace); // Normal of the computed fragment, in camera space vec3 l = normalize(LightDirection_cameraspace); // Direction of the light (from the fragment to the light) float cosTheta = clamp(dot(n,l), 0, 1); // Cosine of the angle between the normal and the light direction, color = vec3(1,0,0)*(0.1 + // ambient lighting 1.3*cosTheta); // diffuse lighting
}

Изменения в проекте, внесённые на этом этапе, смотреть здесь. На этом этапе я закодил Model, View и Projection матрицы. В самом начале они просто единичные, но если вы нажмёте пробел, то модель начнёт вращаться: при каждой отрисовке картинки я поворачиваю матрицу Model вокруг оси z на 0.01 радиана:

{ // rotate the model around the z axis with each frame Matrix R = rot_z(0.01); if (animate) M = R*M; }

Здесь функция rot_z() возвращает матрицу вращения вокруг оси z на заданный угол. Поскольку OpenGL про мой класс матриц ничего не знает, пришлось добавить экспорт матриц void export_row_major() в простой указатель на float.

Изменения в проекте, внесённые на этом этапе, смотреть здесь. На этом этапе мы научимся накладывать текстуры. Поскольку обычная диффузная текстура — это скучно, то я сразу применю карту нормалей, да причём в касательном пространстве. Карты нормалей выглядят примерно вот так:

С точки зрения данных нужно добавить несколько буферов: координаты uv, и массивы касательных и бикасательных векторов. Соответствующие вычисления, мягко скажем, неочевидны, поэтому опять-таки, читайте объяснения в tinyrenderer.

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

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

В этом коде много что можно улучшить, да и визуальные эффекты можно наворачивать бесконечно. Но моя цель не в этом, моя цель показать, что абсолютно все техники, которые я затрагивал в софтверном рендеринге, применимы и в настоящем контексте OpenGL. И лично я по-прежнему считаю, что начинать знакомство с 3Д графикой нужно, рисуя картинки без использования магии графических библиотек.

В качестве продлжения попробуйте, например, добавить тени, или посчитать глобальное освещение, ну или, наконец, сделать glow map: ведь глаза и кристалл во лбу Диабло должны светиться!

Теги
Показать больше

Похожие статьи

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

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

Кнопка «Наверх»
Закрыть