- PVSM.RU - https://www.pvsm.ru -

hellOGL: OpenGL hello world

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

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

hellOGL: OpenGL hello world - 1

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

Этап 0: читаем tinyrenderer

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

hellOGL: OpenGL hello world - 2

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

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

Этап первый, самый сложный: создание окна

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

hellOGL: OpenGL hello world - 3

Компилируется код при помощи 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Д модель

Изменения в проекте, внесённые на этоп этапе, смотреть здесь [9]. На данном этапе наша цель распарсить файл 3Д модели, и нарисовать первые треугольники, не заботясь на данный момент об освещении:

hellOGL: OpenGL hello world - 4

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

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

    std::vector<GLfloat> vertices(3*3*model.nfaces(), 0);
    for (int i=0; i<model.nfaces(); i++) {
        for (int j=0; j<3; j++) {
            for (int k=0; k<3; k++) vertices[(i*3+j)*3 + k] = model.point(model.vert(i, j))[k];
        }
    }

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

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

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

#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
}

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

#version 330 core

// Output data
out vec3 color;

void main() {
    color = vec3(1,0,0);
}

Самое сложное сделано, теперь дело техники!

Этап третий: диффузное освещение

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

hellOGL: OpenGL hello world - 5

Диффузное освещение в модели Фонга, как известно, это простое скалярное произведение между
нормальным вектором и вектором освещения. Поэтому вдобавок к массиву 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
}

Этап четвёртый: матрицы преобразований

Изменения в проекте, внесённые на этоп этапе, смотреть здесь [13]. На этом этапе я закодил 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.

hellOGL: OpenGL hello world - 6

Этап пятый: карты нормалей

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

hellOGL: OpenGL hello world - 7

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

hellOGL: OpenGL hello world - 8

Этап пятый: диффузная текстура

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

hellOGL: OpenGL hello world - 9

Этап шестой: блики

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

hellOGL: OpenGL hello world - 10

Заключение

В этом коде много что можно улучшить, да и визуальные эффекты можно наворачивать бесконечно. Попробуйте, например, добавить тени [17], или посчитать глобальное освещение [18], ну или, наконец, сделать glow map: ведь глаза и кристалл во лбу Диабло должны светиться!

Автор: haqreu

Источник [19]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/opengl/311113

Ссылки в тексте:

[1] либо и то, и другое: http://www.opengl-tutorial.org/

[2] Сэмюэлем Шаритом (arshlevon): https://arshlevon.cgsociety.org/

[3] tinyrenderer: https://github.com/ssloy/tinyrenderer/wiki

[4] на хабре: https://habr.com/ru/post/248153/

[5] что такое шейдеры: https://github.com/ssloy/tinyrenderer/wiki/Lesson-6:-Shaders-for-the-software-renderer

[6] как считать карты нормалей: https://github.com/ssloy/tinyrenderer/wiki/Lesson-6bis:-tangent-space-normal-mapping

[7] здесь: https://github.com/ssloy/hellOGL

[8] вот отсюда: https://github.com/ssloy/hellOGL/commit/4b71a567418570ca415754bceab8c7038ac599ce

[9] смотреть здесь: https://github.com/ssloy/hellOGL/commit/e72d42ea7a4e5f88a40e0f5854b39a98417e83ff

[10] Вершинный шейдер: https://github.com/ssloy/hellOGL/blob/e72d42ea7a4e5f88a40e0f5854b39a98417e83ff/src/vertex.glsl

[11] фрагментный шейдер: https://github.com/ssloy/hellOGL/blob/e72d42ea7a4e5f88a40e0f5854b39a98417e83ff/src/fragment.glsl

[12] смотреть здесь: https://github.com/ssloy/hellOGL/commit/aa0c3f40ac46f589894edf292ae0745f0bc28cd4

[13] смотреть здесь: https://github.com/ssloy/hellOGL/commit/6f24b0bddc0dedb0c4b1a39af0b531899e51ec03

[14] смотреть здесь: https://github.com/ssloy/hellOGL/commit/7e0eb761e68abd27034f05cf2fb110ee6650fcd3

[15] смотреть здесь: https://github.com/ssloy/hellOGL/commit/4f9006f99668a87e71f81cfa8e96ce2fc6972fb2

[16] смотреть здесь: https://github.com/ssloy/hellOGL/commit/0c931c86e4306c22691a7f55473ced8c64c34c04

[17] добавить тени: https://github.com/ssloy/tinyrenderer/wiki/Lesson-7:-Shadow-mapping

[18] глобальное освещение: https://github.com/ssloy/tinyrenderer/wiki/Lesson-8:-Ambient-occlusion

[19] Источник: https://habr.com/ru/post/443174/?utm_source=habrahabr&utm_medium=rss&utm_campaign=443174