Сказ о том, как я свой воксельный 3д движок создавал

Дисклеймер: несмотря на многие ограничения и практически отсутствующий инструментарий, этот проект все-таки является 3д-движком, т. к. позволяет в режиме реального времени визуализировать трехмерную сцену. Unity3d используется исключительно как способ доступа к нужным мне технологиям. Штатный рендер Unity не используется.

Немного теории

Рендер трехмерного мира являет собой чрезвычайно сложную задачу, как с точки зрения объема работы, так и с используемого математического аппарата. В связи с этим проще и эффективнее доработать старое двигло с учетом новых технологий, чем писать с нуля движок. Даже самые новые игровые движки, типа CryEngeich, Unity 2015-2020, EU4-5 содержат в своей основе год бородатых годов. А может и не содержат, свечку не держал, исходники не видел. Итак, позволить себе создание нового 3д движка могут или крупные компании или, напротив, инди-студии, которым нечего терять можно и можно пуститься во все тяжкие

Самый распространенным способ описание трехмерной модели объекта является полигональная модель. Модель задается в виде массива вершин и отношений между ними:

  • пара вершин образует ребро
  • три ребра образуют треугольник
  • множество треугольников образуют трехмерную поверхность

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

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

  • затраты видеопамяти при рендере пропорциональны размеру сцены
  • фундаментальная угловатость мира, связанная с тем, что сцена задается в виде «кубиков»
  • невозможность текстурирования объектов
  • сохраненное на диске состояние сцены занимает большой объем памяти

Однако, воксели имеют уникальные положительные стороны:

  • возможность легко изменять объекты на сцене
  • физически корректная реализация распространения света в полупрозрачных телах
  • полный контроль за процессом рендеринга из-за его простоты
  • возможность создания физической симуляции мира на уровне поведения отдельных частиц

Как я дошел до жизни такой

Предистория разработки этого движка началась ещё в 2003 году, когда в журнале Game. exe была выпущена статья о разработке компьютерной игры «Периметр: Геометрия войны». В моей памяти навсегда остались описание деформируемого в реальном времени ландшафта, по которому медленно ползут танки, которые обстреливают гаубицы на искусственных насыпях. »Периметр» навеки стал для меня эталоном стратегии, пускай даже сама игра была не столько хороша, как в моих мечтах. Идея использовать ландшафт в качестве основного элемента игрового процесса крепко поселилась в голове. И, когда я в 2019 году узрел на Хабре статьи, посвященные использованию вычислительных шейдеров я понял — настало то самое время. Идея создать свой воксельный движок не имела конкретной цели (вроде создания игры), но начав я уже не мог остановится.

Базовый принцип работы

Сцена для рендера представляет собой Texture3D, т. е. трехмерный массив, где индекс обозначает положение вокселя в трехмерном пространстве, а хранится в нем его цвет в формате RGBA (Red, Green, Blue, Alpha). Размер сцены составляет 256x256x256 вокселей. Для каждого пикселя экрана из точки положения наблюдателя (камеры) выпускается луч, в направлении данного пикселя. Далее в цикле происходит «шагание» по лучу, где читается цвет из трехмерной текстуры, где индекс — это округленные координаты текущей точки. Если прозрачность цвета близка к нулю — то цикл продолжается дальше. Если прозрачность цвета больше нуля — то пиксель закрашивается в этот цвет и цикл прерывается. Вот такой простой алгоритм, дающий, тем не менее рендер в реальном времени. На самом деле я его позаимствовал его из этой статьи.

Так как я статью не понял, у меня получился свой собственный рабочий алгоритм.

Первые шаги

Что бы не тянуть интригу — мне удалось сделать с нуля рабочий рендер 3d сцены на Юнити, собрав при этом чуть ли не все возможные проблемы.

Первый сохранившийся пример рендера — холмистая равнина, цвет зависит от глубины
Первый сохранившийся пример рендера — холмистая равнина, цвет зависит от глубины

Самого старого варианта, с белым шаром в пустом пространстве, не сохранилось. Однако, и на этом скриншоте видны характерные черты воксельного рендера — угловатость пейзажа и его низкое разрешение. Но картинка есть! Я не знаю, кто будет читать эту статью и настолько мой читатель будет грамотен в вопросах компьютерной графики. Поэтому, я должен сказать, что сделанное мной даже на этапе скриншота — значительный успех. На каждом шагу меня подстерегали неразрешимые проблемы, любая из которых могла меня навсегда остановить, а хитроумным решениям для обхода глюков несть числа. Результатом является стабильный рендер картинки с частотой 30 герц и смутные перспективы на будущее технологии. Останавливаться на достигнутом было слишком просто, к тому же такой результат не годился для практического применения.

Реализация

Иметь алгоритм — это хорошо, но иметь конкретную реализацию — несравнимо лучше. Для простоты я использовал игровой движок Unity. Мой движок состоит из двух основных компонентов — скрипты для управления процессом рендера и compute shader (вычислительный шейдер) для самого процесса рендера. Вычислительный шейдер — это очень полезная и оригинальная технология, которая позволяет запускать на видеокарте любой выполнимый код и возвращать результат обратно на сторону процессора. В отличии от процессора, видеокарта при помощи compute shader может параллельно обрабатывать миллионы потоков, что идеально подходит под задачу. Однако, сложность заключается в том, что вычислительными шейдерами трудно управлять и ещё сложнее отлаживать.

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

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

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

Освещенная сцена из прошлого скриншота. Хорошо видна геометрическая тень и эффект муара из-за погрешностей округления
Освещенная сцена из прошлого скриншота. Хорошо видна геометрическая тень и эффект муара из-за погрешностей округления

Однако, данный метод можно улучшить, добавив отображение нескольких источников света.

Рендер с двумя цветными источниками света. Эффект муара жестоко подавлен.
Рендер с двумя цветными источниками света. Эффект муара жестоко подавлен.

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

Рендер с учетом нормалей и обратноквадратичного ослабления света. Эффект муара подавлен ещё более сурово.
Рендер с учетом нормалей и обратноквадратичного ослабления света. Эффект муара подавлен ещё более сурово.

На рендер на последнем скриншоте — это, пожалуй, предел того, что можно вытащить из расчет освещения в экранным пространстве. Этот метод очень производителен и дает красивую картинку. К моей великой печали, он не позволяет работать с полупрозрачными объектами, а это одно из главных потенциальных достоинств воксельной технологии. Я испробовал много СПОБСобов обойти это ограничение, но лишь доказал, что это невозможно. печальный результат можно увидеть на картинке ниже.

За пролупрозрачной розовой стеной видно часть шара, но тени на ней — не видны. Хорошо видна мягкая полутень на краю.
За пролупрозрачной розовой стеной видно часть шара, но тени на ней — не видны. Хорошо видна мягкая полутень на краю.

Объемное освещение

Когда я понял фундаментальные ограничения старой системы, то было сложно справится с печалью. Требовалось радикально отойти от старого алгоритма, что бы продвинутся дальше. Мой новый алгоритм (названный «Accumulation de perte de lumière» или «Pertalum») имитирует реальное распространение света. Суть этого алгоритма заключается в использовании особой трехмерной текстуры, названной картой светопотерь. При расчете лучей в эту текстуру сохраняется цвет, который равен потери яркости луча при прохождении через данный воксель. Эти вычисления применяются для всех вокселей сцены. Да, для всех 16 миллионов. Результатом была довольно красивая картинка ценой чудовищных вычислительных затрат.

На этой картинке хорошо видна тень за полупрозрачной стеной и колоризованная тень.
На этой картинке хорошо видна тень за полупрозрачной стеной и колоризованная тень.

При запуска получилась приятная картинка и неприятное явление в лице загрузки видеокарты на 100%. Я долго думал как уменьшить загрузку, пока не придумал поистине гениальный план.

Я купил новую видеокарту! RTX2060 (на то время она была весьма мощной).

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

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

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

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

Реализация и беды с ней

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

Разработка движка началась с Unity3d. Звучит немного абсурдно, но этой самый простой способ получить доступ к необходимым мне технологиям. Разумеется, я изучил возможности главного конкурента Unity — Unreal Ingeich (ну ладно, Engine) 4. В Анрыле написание, компиляция и запуск compute shader занимают гораздо больше усилий и сомнительных плюсах. Работа с вычислительными шейдерами в Юнити укладывается в простую последовательность:

  • написать шейдер
  • сохранить его в папке Resources
  • в скрипте загрузить его из этой папки
  • назначить переменные загруженному шейдеру
  • запустить его

В Анрыле для этого надо выполнить множество сложных и неочевидных операций и написать уйму кода, который делает неизвестно что (во всяком случае для меня) и неизвестно как (аналогично). Возможно, подход Анрыла имеет свой плюс. Или даже два плюса (++).

Как я уже писал в прошлой части, Compute Shader — это технология, которая позволяет выполнять на видеокарте любой произвольный код и даже возвращать его на сторону центрального процессора. При обычной работе видеокарта обрабатывает 3д-сцену, формирует картинку и отправляет её в кадровый буффер, после чего она оправляется на монитор. Использование вычислительных шейдеров превращает видеокарту в отдельное вычислительное устройство. Видеокарта из-за особенностей архитектуры идеально приспособлена для параллельного выполнения множества однотипных операций над данными. Например, обработка текстуры (фильтрация, наложение эффектов и т. д.). Возьмем текстуру размером 1024х1024. В этом случае ComputeShader (далее — CS) позволяет параллельно обрабатывать все пиксели, благодаря чему этот процесс завершается за считанные миллисекунды. Центральный процессор может обрабатывать текстуру в 5-10 потоков и завершит работу за несколько секунд, чего недостаточно для создания эффектов в режиме реального времени. Однако, даже CS будет тормозить при слишком большом количестве потоков.

Сам CS представляет собой код, написанный на языке HLSL (англ. высокоуровневый язык шейдеров). Это один из диалектов си, с вырезанной работой с памятью и добавленной векторной арифметикой. CS состоит из глобальных переменных и ядер (kernel). Кернель — это обычная функция, которая имеет параметр id, который имеет уникальное значение для каждого потока. Шейдер запускается на стороне скрипта с указанным количеством потоков. Подробная информация о этой технологии тайной не является и доступна по первым ссылкам в гугле.

Беды

Если бы у меня CS и другие технологии работали точно так же, как и в руководстве — я бы добился бы такого же результата за две недели, а не за три месяца. Среди самых неожиданных проблем:

  • Нельзя в скрипте создать трехмерную текстуру (Texture3D) размером больше чем 256х256х170. В противном случае движок будет выдавать черный экран вместо рендера, даже если эта текстура НИГДЕ не используется. При этом никаких ошибок и предупреждений Unity не выдает и посему решить эту проблему не удалось, лишь обойти.
  • Для использования трехмерной текстуры необходимо дополнительно указать, что она именно трехмерная, причем в руководстве это нигде не указано. В противном случае шейдер будет выдавать ошибку, что нет третьего измерения у текстуры.
  • В CS в текстуру нельзя сохранять отрицательные значения цвета.
  • Полностью отсутствуют средства отладки шейдера, поэтому для определения проблем приходится создавать свои инструменты
  • Юнити последних версий имеет дурную привычку обновлятся, что приводит к крушению всего проекта. Решилось переходом на старую версию.
  • На ноль делить нельзя. Даже если очень хочется.

Физическая симуляция

Воспоминания о разрушаемом ландшафте не давали мне покоя и я решил воплотить физическую симуляцию мира на уровне отдельных частиц. Я знал что задача была решаема и с головой пустился в разработку. Для этого пришлось отказаться от представления мира в виде обычной трехмерной текстуры. Вместо сцену я представил в виде списка частиц длиной 256х256х256. Частица представляет собой структуру, которая содержит в себе:

  • Цвет (куда уж без него?)
  • Позицию в пространстве
  • Текущую скорость
  • Массу

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

Принцип работы физической симуляции был таков:

  • в отдельном кернеле каждая частица меняет свою позицию на величину, равную своей скорости
  • Частица передает импульс другим частицам, которые она могла задеть.

Я смело предположил, что это должно работать. А на самом деле — нет. Все, что у меня получилось — это сделать падение под действием гравитации и обнаружение столкновений.

Падаем под музыку Эньи. Не спрашивайте, откуда опять взялся муар. Если знаете — никому не говорите.

Редактор

В процессе работы я сталкивался с тем, что мне надо прямо во время работы добавить или удалить объекты на сцене. Это значит, что пришло время создать 3д-редактор на основе моего движка! На этот раз дело шло безо всяких эпических превозмоганий, некоторую сложность составило создание отката рисования (undo-redo, как в любом редакторе). Захват всей сцены требовам большого объема памяти и на несколько секунд тормозил рендер. Я решил эту проблему сохраняя в памяти только измененный фрагмент сцены.

Сказ о том, как я свой воксельный 3д движок создавал

Сейчас редактор имеет следующие функции:

  • Рисование на сцене/добавление новых вокселей.
  • Изменение режимов рисования (глобальное/экранное, непрерывное/дискретное).
  • Отмена изменений
  • Сохранение в файл (расширение. tempete) и загрузка из файла сцены.
Цмок)
Цмок)

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

И главное: время танков, ползущих по динамически изменяемому ландшафту к гаубицам на искусственных насыпях, обязательно придет.
#лонгрид #движок #воксели #разработка

193193
29 комментариев

свой воксельный 3д движок

23
Ответить

В Анриле кстати реймарчинг имеется. В 4 версии можно включить тени и АО на основе Distant Fields, а в 5 весь Люмен на этом основан. То есть где-то в памяти есть копия сцены, в которой для объектов созданы их воксельные копии и там летают лучи, которым не нужны RT ядра.

А еще есть технология VXGI в этих ваших Крайэнджинах.

То есть разрушаемости все равно нет, полигонаж все равно есть. Но где-то там в отражении можно разглядеть что-то воксельное, если очень сильно постараться

11
Ответить

Ну разрушаемость есть, не такая, конечно, как в Teardown/Minecraft, но как в хорошо проработанных шутерах.

Ответить

Ясное дело, что исходники нужны!

7
Ответить

Могу в личку дать ссылку

1
Ответить

Комментарий недоступен

8
Ответить

Комментарий недоступен

5
Ответить