Пишем примитивную 3D игру. Расширяем пространство. Запускаем корабль. Часть 2

Всем добрый вечер! Материал к данному посту уже давно был готов, но не получилось выпустить его так быстро, как хотелось. Но, тем не менее.
Часть 1. Введение, строим 2D объект

Пишем примитивную 3D игру. Расширяем пространство. Запускаем корабль. Часть 2 Программирование, ЭВМ, Разработка, Игры, Урок, Длиннопост, Видео, Без звука, Пятничный тег моё

Я прочитал некоторые отзывы на первый пост, особенно, касательно программной части. Ещё раз акцентирую внимание - данные примеры не являются идеальными в плане кода, так как написаны исключительно для демонстрации алгоритма. Рекомендую не использовать их, а попытаться самостоятельно разработать их аналоги.

Итак, в прошлый раз мы остановились на том, что научились поворачивать 2D-область с помощью матриц. Некоторые просили рассказать подробнее о том, как их решать. Думаю, в этом посте завеса тайны будет чуть приоткрыта. Давайте начнём операции по расширению пространства до трёхмерного и добавим z-ось. Для этого мы сделаем следующее:

Задача 1. Изменим структуру нашего объекта, позволив хранить трёхмерные координаты.

Было:

Пишем примитивную 3D игру. Расширяем пространство. Запускаем корабль. Часть 2 Программирование, ЭВМ, Разработка, Игры, Урок, Длиннопост, Видео, Без звука, Пятничный тег моё

Стало:

Пишем примитивную 3D игру. Расширяем пространство. Запускаем корабль. Часть 2 Программирование, ЭВМ, Разработка, Игры, Урок, Длиннопост, Видео, Без звука, Пятничный тег моё

Для того, чтобы понять, почему количество значений увеличилось на столько порядков,  сразу же небольшая беседа о структуре 3D объекта. По-научному это называется "полигональная сетка".
Если кратко, то мы можем хранить описание 3D объекта в виде:
1. Списка вершин (Vertices)
2. Списка ребёр (Edges)
3. Списка, ну скажем так, граней (Faces)

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

Пишем примитивную 3D игру. Расширяем пространство. Запускаем корабль. Часть 2 Программирование, ЭВМ, Разработка, Игры, Урок, Длиннопост, Видео, Без звука, Пятничный тег моё

Рассмотрим куб. Куб имеет 8 крайних точек. Описываются их координаты по 3 осям, скажем так, в явно заданном виде (обычно они задаются в виде векторов, т.е. {0.5f, 0.23f, -0.09f} и т.п.). Далее следует порядок их объединения.

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

Представим куб в виде пар координат. Две крайние точки, соединяются в ребро, которое будет отображено в виде линии на экране. Тогда одно такое ребро будет задано координатами (x1, y1, z1, x2, y2, z2). Логичнее разместить их попарно ({x1,y1,z1}, {x2,y2,z2}, но, для упрощения алгоритма, они будут размещены в линию. У куба 8 вершин. А рёбер уже 12. Поэтому и получаем такую картину:

Пишем примитивную 3D игру. Расширяем пространство. Запускаем корабль. Часть 2 Программирование, ЭВМ, Разработка, Игры, Урок, Длиннопост, Видео, Без звука, Пятничный тег моё

Если 2D прямоугольник в виде точек описывался массивом [4][2], то куб в виде рёбер уже [12][6].
Также, к центру объекта добавляется третье измерение: float center[3] = { 0.f, 0.f, 0.f };

Задача 2. Организуем проекцию трёхмерного пространства на двумерное
Как я уже упоминал в первом посте, который некоторые незаслуженно назвали "бредом", модель находится где-то там, в невидимом пространстве, а вот практическую реализацию мы можем увидеть. Проекция двумерных координат очень проста по своей сути: x точки отображаем как x на экране, а y, как y. Здесь же мы имеем дело так же с x и y, но при этом куда-то нужно ещё вставить зависимость от координаты z. Можно сделать это также несколькими способами:

1. Построить ортогональную проекцию,  например, представив x как x+ k*cos(a)*z, но этот вариант рассматривать мы сейчас не будем.

2. Непосредственно разделить на z: x'=kx/z и y'=ky/z
Второй вариант, на самом деле, очень прост. формулы расскажут его лучше, чем я:

Пишем примитивную 3D игру. Расширяем пространство. Запускаем корабль. Часть 2 Программирование, ЭВМ, Разработка, Игры, Урок, Длиннопост, Видео, Без звука, Пятничный тег моё

Для получения двумерной координаты достаточно лишь сделать вот так. WW/2 - половина ширины экрана (x), FOV - коэффициент поля видимости, x - координата из массива (value[0]), z - то же самое (value[2]).

Здесь вводится коэффициент FOV, характеризующий глубину поля зрения. Если кто-то когда-либо интересовался анаглифными 3D-очками, то он мог заметить, что при изменении FOV на рендерере изменяется нагрузка на глаза. При сильных значениях изображение вообще перестаёт сходится. Проще показать наглядно, что я и сделаю в примере. В нашем случае примем его как константу 200-300 единиц, и более не будем трогать. Получив все координаты x' и y', осуществляем рендеринг, аналогично 2D из первого поста.

Так как я демонстрирую алгоритм, а не работу с библиотекой, я не использую вспомогательные средства SFML, а только лишь отображение объектов по явным координатам, подобно тому, как это делалось на древних компьютерах типа ZX Spectrum.

Ии.. мы получили вот такую картину:

Пишем примитивную 3D игру. Расширяем пространство. Запускаем корабль. Часть 2 Программирование, ЭВМ, Разработка, Игры, Урок, Длиннопост, Видео, Без звука, Пятничный тег моё

Четыре точки на поле. А чем она отличается от той, что была в первом посте, спросите вы? Не торопитесь. Давайте теперь включим отображение линий:

Пишем примитивную 3D игру. Расширяем пространство. Запускаем корабль. Часть 2 Программирование, ЭВМ, Разработка, Игры, Урок, Длиннопост, Видео, Без звука, Пятничный тег моё

Что-то начинает проясняться.. Мы находимся в центре куба, и смотрим прямо. Потому и видим четыре точки одной из его граней. Так и должно быть. При таком способе проекции наш экран соответствует координате z = 0. Таким образом, остальные 4 точки остались за нашей спиной, и к ним уходят соответствующие рёбра. Так же, чтобы избежать вылета приложения при делении на z = 0, нужно перед операцией деления явно указать: if (z=0) z=(0.01f). На этом задача по рендерингу окончена.

Задача 3. Расширяем функции поворота для 3D пространства
Вот здесь мы снова сталкиваемся с матрицами. Только для двумерного прямоугольника они были второго порядка, а сейчас нас ожидают матрицы 3 порядка (иллюстрация с сайта Wikipedia):

Пишем примитивную 3D игру. Расширяем пространство. Запускаем корабль. Часть 2 Программирование, ЭВМ, Разработка, Игры, Урок, Длиннопост, Видео, Без звука, Пятничный тег моё

Не стоит пугаться этих матриц. Разрешаются они стандартно, умножением на вектор-столбец с координатами:

Пишем примитивную 3D игру. Расширяем пространство. Запускаем корабль. Часть 2 Программирование, ЭВМ, Разработка, Игры, Урок, Длиннопост, Видео, Без звука, Пятничный тег моё

(цифры выше для примера). В нашем столбце будут координаты трёх вершин 1-й точки. Для линии, соответственно, делается два умножения. Более подробно для людей, мало знакомых с матрицами:
1. Сначала составляется матрица Mx, My, Mz в зависимости от того, вращение вокруг какой оси вам потребовалось: x,y,z.
2. Затем полученная матрица векторно перемножается на данные координаты
3. Полученные координаты записываются в новый объект, который будет являться уже повёрнутой копией старого объекта (в ООП), либо просто в поля (область памяти) старого объекта, как делалось на старинных компьютерах.

Пишем примитивную 3D игру. Расширяем пространство. Запускаем корабль. Часть 2 Программирование, ЭВМ, Разработка, Игры, Урок, Длиннопост, Видео, Без звука, Пятничный тег моё

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

Также, процедура умножения матрицы на вектор-столбец (это не самый красивый вариант, как её реализовать, но рабочий):

Пишем примитивную 3D игру. Расширяем пространство. Запускаем корабль. Часть 2 Программирование, ЭВМ, Разработка, Игры, Урок, Длиннопост, Видео, Без звука, Пятничный тег моё

newValue[i] - новое значение x[0], y[1], z[2] для первой точки линии. Для второй точки - аналогично.
oldData[i] - старое значение x[0], y[1], z[2] для первой точки линии. Аналогично newValue
center[i] - аналогично заданные координаты центра объекта.

Именно здесь нам и пригодился центр объекта. При повороте относительно произвольной оси, если объект уже был сдвинут, при неверно заданном центре объект начнёт вращаться либо с большим радиусом, либо вообще непредсказуемо. Поэтому при операции сдвига, например, +100 по z, в center так же записывается z: 0,0,100.

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

Давайте теперь испытаем полученные функции, и попробуем повернуть куб (как я и говорил, поворот осуществляется по прерыванию, т.к. игровой таймер всё ещё отсутствует):

Я кружусь в фантастическом танце. Я почти итальянец!

Также, демонстрация изменения константы FOV из задачи 2 на развёрнутом кубе:

Т.е. изменение FOV как бы искажает отклонение осью z оси x. Проявляется это увеличением количества видимых на экране объектов.

Задача 4. Элементы "гейм-дизайна"
Давайте теперь попробуем реализовать элемент игры, а именно, космический корабль, которым мы управляем. Нам понадобится модель корабля. Так как пишем мы примитивную игру, так сказать, "следуя по стопам" за гигантами прошлого века, модель тоже должна быть несложной в исполнении. Я смоделировал её в 3D редакторе:

Пишем примитивную 3D игру. Расширяем пространство. Запускаем корабль. Часть 2 Программирование, ЭВМ, Разработка, Игры, Урок, Длиннопост, Видео, Без звука, Пятничный тег моё

Теперь нужно получить её рёбра. Сделать это явно мы не сможем, поэтому получим хотя бы вершины. Экспортируем модель в формате .obj. На примере куба:

Пишем примитивную 3D игру. Расширяем пространство. Запускаем корабль. Часть 2 Программирование, ЭВМ, Разработка, Игры, Урок, Длиннопост, Видео, Без звука, Пятничный тег моё

Как мы можем видеть, содержимое файла, если открыть его в редакторе, похоже на упомянутое в первой задаче. Но есть небольшие отличия:
v - непосредственно вершины, в виде нормализованного к единице вектора
vn - нормали (нам не нужны)
f - грани (faces). Они нам не помогут. На первый взгляд сложно. Рассмотрим ещё более простую модель: грань.

Пишем примитивную 3D игру. Расширяем пространство. Запускаем корабль. Часть 2 Программирование, ЭВМ, Разработка, Игры, Урок, Длиннопост, Видео, Без звука, Пятничный тег моё

4 вершины, 1 "face": задан как 1//1 2//1 4//1 3//1. Но наша программа устроена по другому принципу. Возьмём отсюда только лишь вершины. Так как писать автоматизированное ПО для одной модели нецелесообразно, порядок соединения выставим вручную. Воссоздаём модель корабля:

Пишем примитивную 3D игру. Расширяем пространство. Запускаем корабль. Часть 2 Программирование, ЭВМ, Разработка, Игры, Урок, Длиннопост, Видео, Без звука, Пятничный тег моё

Вот, как будет выглядеть модель, если неправильно были выставлены оси координат. Помните ложку из 1 части поста? А это в подобном представлении наш корабль..

Пишем примитивную 3D игру. Расширяем пространство. Запускаем корабль. Часть 2 Программирование, ЭВМ, Разработка, Игры, Урок, Длиннопост, Видео, Без звука, Пятничный тег моё

Но, тем не менее, каркасная модель выполнена и состоит из 36 рёбер:

Пишем примитивную 3D игру. Расширяем пространство. Запускаем корабль. Часть 2 Программирование, ЭВМ, Разработка, Игры, Урок, Длиннопост, Видео, Без звука, Пятничный тег моё

Давайте запустим программу:

Пишем примитивную 3D игру. Расширяем пространство. Запускаем корабль. Часть 2 Программирование, ЭВМ, Разработка, Игры, Урок, Длиннопост, Видео, Без звука, Пятничный тег моё

В движении смотрится более эффектно:

К полёту готов! Также сразу же можно выполнить некоторую "постановку" сцены: корабль сдвигается вместо центра примерно на 0,7 экрана по y. Однако, конечно, пока это ещё не игра, а всего лишь просмотрщик 3D модели. Но, всё это, как и прежде, поправимо. Вопрос только, когда..

Всем спасибо, с вами был Kekovsky, надеюсь, пост был для вас информативным, комментируйте, поддерживайте автора и т.д.

Лига Разработчиков Видеоигр

6.8K постов22.2K подписчика

Добавить пост

Правила сообщества

ОБЩИЕ ПРАВИЛА:

- Уважайте чужой труд и используйте конструктивную критику

- Не занимайтесь саморекламой, пишите качественные и интересные посты

- Никакой политики


СТОИТ ПУБЛИКОВАТЬ:

- Посты о Вашей игре с историей её разработки и описанием полученного опыта

- Обучающие материалы, туториалы

- Интервью с опытными разработчиками

- Анонсы бесплатных мероприятий для разработчиков и истории их посещения;
- Ваши работы, если Вы художник/композитор и хотите поделиться ими на безвозмездной основе

НЕ СТОИТ ПУБЛИКОВАТЬ:

- Посты, содержащие только вопрос или просьбу помочь
- Посты, содержащие только идею игры

- Посты, единственная цель которых - набор команды для разработки игры

- Посты, не относящиеся к тематике сообщества

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

ЗАПРЕЩЕНО:

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

- Выдавать чужой труд за свой

Подобные посты будут перемещены из сообщества в общую ленту, а их авторы по решению администрации могут быть внесены в игнор-лист сообщества.


О РАЗМЕЩЕНИИ ССЫЛОК:

Ссылка на сторонний ресурс, связанный с игрой, допускается только при следующих условиях:

- Пост должен быть содержательным и интересным для пользователей, нести пользу для сообщества

- Ссылка должна размещаться непосредственно в начале или конце поста и только один раз

- Cсылка размещается в формате: "Страница игры в Steam: URL"