Как сделать воксельный движок

Воксельная графика своими руками — первые шаги

Знакомство с воксельной графикой

В процессе поиска алгоритмов расчета коллизий на сайте GameDev, я наткнулся на маленькую статью про движок idTech 6 и заинтересовался воксельной графикой, которую противопоставляют полигональной графике, на которой сейчас основана почти вся компьютерная графика.
Вообще, воксел расшифровывается как «объемный пиксель», однако сейчас под вокселом в основном понимается некий примитив, чаще всего куб или прямоугольный параллелепипед, который имеет определенный размер и цвет. В idTech 6 и в движке Кена Сильвермана Voxlap они хранятся в разреженном октодереве (SVO — sparse voxel octree), что позволяет экономить память и делает возможным простую реализацию «уровня детализации».

Как сделать воксельный движок. Смотреть фото Как сделать воксельный движок. Смотреть картинку Как сделать воксельный движок. Картинка про Как сделать воксельный движок. Фото Как сделать воксельный движок

Как сделать воксельный движок. Смотреть фото Как сделать воксельный движок. Смотреть картинку Как сделать воксельный движок. Картинка про Как сделать воксельный движок. Фото Как сделать воксельный движок
Как сделать воксельный движок. Смотреть фото Как сделать воксельный движок. Смотреть картинку Как сделать воксельный движок. Картинка про Как сделать воксельный движок. Фото Как сделать воксельный движок
Как сделать воксельный движок. Смотреть фото Как сделать воксельный движок. Смотреть картинку Как сделать воксельный движок. Картинка про Как сделать воксельный движок. Фото Как сделать воксельный движок

Первые попытки

Штурмовать сразу трехмерное пространство я не решился — слишком пугающе выглядел весь тот список формул, которые бы пришлось освоить (о них речь пойдет чуть ниже), и решил просто переписать код, чтобы платформер использовал «боксы» — квадраты, и соответственно хранились не в разреженном октодереве, а в разреженном квадродереве.
Первый код был ужасен — все функции работы с деревом, такие как удаление, обход дерева, создание узлов, были итеративными отдельными от класса QuadTree функциями. В прочем, даже добавление их в пределы класса особо не сыграло роли, так как в последствии выяснилось, что рекурсивные функции сильно выигрывают в данном случае. Единственное, что принесли полезного эти первые попытки — это четкая формулировка, какие функции мне нужны, а также основы реализации деревьев на С++, что в дальнейшем очень помогало и, я надеюсь, еще будет помогать. И, конечно же, именно те первые попытки подтолкнули изучать OpenGL (правда до этого прельстился Direct2D, но очень быстро разочаровался в нем).

Как сделать воксельный движок. Смотреть фото Как сделать воксельный движок. Смотреть картинку Как сделать воксельный движок. Картинка про Как сделать воксельный движок. Фото Как сделать воксельный движок
Как сделать воксельный движок. Смотреть фото Как сделать воксельный движок. Смотреть картинку Как сделать воксельный движок. Картинка про Как сделать воксельный движок. Фото Как сделать воксельный движок
Как сделать воксельный движок. Смотреть фото Как сделать воксельный движок. Смотреть картинку Как сделать воксельный движок. Картинка про Как сделать воксельный движок. Фото Как сделать воксельный движок

Переход в объем

OpenGL я начал изучать на NeHe gamedev и как то очень быстро втянулся в трехмерное пространство и начал планировать движок для Quake-подобной игры. Квадродерево было переписано в октодерево и начались первые сложности. Октодеревья потребляют памяти гораздо больше, и даже не смотря на то, что все основные функции стали рекурсивными, все равно они тратили слишком много времени и памяти. Для решения этой проблемы были реализованы следующие методы:

Оптимизация памяти

В октодереве очень часто приходится использовать операторы new/delete, которые выделяют для указателя место в динамической памяти (куче). Динамическая память медленнее, чем статическая (стек), а также сами функции new/delete выполнялись для меня слишком медленно. Из-за чего был написан собственный класс MemoryPool и шаблон mem_pool_tree.
mem_pool_tree был написан под впечатлением от BST-дерева, с которым я познакомился из книги Т. Кормана «Алгоритмы. Построение и анализ», и не работает напрямую с памятью, а только оперирует цифрами, которые в последствии используются для смещение указателя с начала массива в статической области памяти. Предугадать удаления не представлялось возможным, а вот выделять «правильные» куски памяти было реально, из-за чего я взял у BST дерева идею и повороты, и добавил «блочность» — mem_pool_tree хранит узлы, в котором две переменные хранят начало и конец блока, и еще две переменные — начало и конец занятого пространства. Если происходит попытка удалить кусок в середине занятого пространства, то узел делится, если вызывается функция выделения куска, то алгоритм ищет такой блок, где выделение пространства позволит ему объединится с соседним блоком. И периодически вызывается функция балансировки.

Многопоточность

Из-за строения дерева, в котором у родительского узла есть указатель на массив из восьми дочерних узлов, функции, где требуется полный обход дерева (такие, как удаление всего дерева, удаление лишних элементов, вычисление средних вокселов и т.д.), были написаны с возможностью включить многопоточность. Многопоточность была реализована с помощью OpenMP. К примеру, надо оптимизировать дерево (например, зачем хранить восемь дочерних узлов, если можно их цвет передать родительскому узлу, а их удалить). Реализуем:

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

Загрузка и сохранение вокселей

Долго пришлось искать оптимальный метод хранения вокселей в файле — ведь в условиях, когда оперативная память ценна, хранить лишние вокселы в оперативке является непозволительной роскошью. После долгих исканий, выбор остановился на SQLite3, в котором есть кэширование, а также возможность загрузить только те вокселы, которые требуются исходя из значений «уровня детализации». Самая быстрая работа с SQLite3 базами оказалось при встраивании в проект исходного кода sqlite3 и самостоятельной компиляции (точных цифр не помню, но что то вроде полмиллиона переменных за 200-250 ms, причем на нетбуке с Intel Atom). Естественно, в SQLite3 использовались для ускорения «Begin transaction;», «Commit transaction;», «PRAGMA journal_mode = MEMORY;», «PRAGMA synchronous = OFF;» и т.д.

Скриншоты

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

Как сделать воксельный движок. Смотреть фото Как сделать воксельный движок. Смотреть картинку Как сделать воксельный движок. Картинка про Как сделать воксельный движок. Фото Как сделать воксельный движок
Как сделать воксельный движок. Смотреть фото Как сделать воксельный движок. Смотреть картинку Как сделать воксельный движок. Картинка про Как сделать воксельный движок. Фото Как сделать воксельный движок
Как сделать воксельный движок. Смотреть фото Как сделать воксельный движок. Смотреть картинку Как сделать воксельный движок. Картинка про Как сделать воксельный движок. Фото Как сделать воксельный движок
Как сделать воксельный движок. Смотреть фото Как сделать воксельный движок. Смотреть картинку Как сделать воксельный движок. Картинка про Как сделать воксельный движок. Фото Как сделать воксельный движок
Как сделать воксельный движок. Смотреть фото Как сделать воксельный движок. Смотреть картинку Как сделать воксельный движок. Картинка про Как сделать воксельный движок. Фото Как сделать воксельный движок
Как сделать воксельный движок. Смотреть фото Как сделать воксельный движок. Смотреть картинку Как сделать воксельный движок. Картинка про Как сделать воксельный движок. Фото Как сделать воксельный движок

Текущие задачи и заключение

Полиморфизм

Сейчас октодерево в очередной раз переписывается с применением полиморфизма. Основная задача — чтобы дерево было не чистым октодеревом, а скрещением с kd-tree (дерево, в котором идет не разбиение воксела на 8 маленьких вокселей, а разбиение на два воксела с определенной пропорцией и по определенной оси), и еще другими модификациями.

RayCasting

Октодерево позволяет Ray Casting, алгоритм «бросания лучей», с помощью которого сейчас пишу растеризатор. Также реализации алгоритма используется OpenGL (генерация текстуры из массива и отображение его на полигоне), «групповая трассировка» и C++ AMP. В целом, эта тема хорошо раскрыта на ray-tracing.ru.

Заключение

В целом, тема интересная, и можно много интересного найти по ней. Например: статья на хабре про движок Atomontage и презентация технологии SVO с SIGGRAPH 2012.

Написанный мною класс распределения памяти с использованием массива в статической памяти, после замеров, выдал следующие данные:

Источник

Пишем собственный воксельный движок

Как сделать воксельный движок. Смотреть фото Как сделать воксельный движок. Смотреть картинку Как сделать воксельный движок. Картинка про Как сделать воксельный движок. Фото Как сделать воксельный движок

Примечание: полный исходный код этого проекта выложен здесь: [source].

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

После выпуска первоначального концепта Task-Bot [перевод на Хабре] я почувствовал, что меня ограничивает двухмерное пространство, в котором я работал. Казалось, что оно сдерживает возможности емерджентного поведения ботов.

Предыдущие неудачные попытки изучения современного OpenGL поставили передо мной мысленный барьер, но в конце июля я каким-то образом наконец пробил его. Сегодня, в конце октября, у меня уже достаточно уверенное понимание концепций, поэтому я выпустил собственный простой воксельный движок, который будет средой для жизни и процветания моих Task-Bots.

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

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

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

Концепция движка

Движок полностью написан с нуля на C++ (за некоторыми исключениями, например, поиска пути). Для рендеринга контекста и обработки ввода я использую SDL2, для отрисовки 3D-сцены — OpenGL, а для управления симуляцией — DearImgui.

Я решил использовать воксели в основном потому, что хотел работать с сеткой, которая имеет множество преимуществ:

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

Класс World

Класс мира служит базовым классом для хранения всей информации мира. Он обрабатывает генерацию, загрузку и сохранение данных блоков.

Данные блоков хранятся во фрагментах (chunks) постоянного размера (16^3), а мир хранит вектор фрагментов, загруженный в виртуальную память. В больших мирах практически необходимо хранить в памяти только определённую часть мира, поэтому я и выбрал такой подход.

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

Если я когда-нибудь реализую многопоточное сохранение и загрузку фрагментов, то преобразование плоского массива в разреженное октодерево и обратно может быть вполне возможным вариантом для экономии памяти. Здесь ещё есть пространство для оптимизации!

Моя реализация разреженного октодерева сохранилась в коде, поэтому можете спокойно ею воспользоваться.

Хранение фрагментов и работа с памятью

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

Фрагменты сериализованы при помощи библиотеки boost, а данные мира хранятся как простой текстовый файл, в котором каждый фрагмент — это строка файла. Они генерируются в определённом порядке, чтобы их можно было «упорядочить» в файле мира. Это важно для дальнейших оптимизаций.

В случае большого размера мира основным узким местом является считывание файла мира и загрузка/запись фрагментов. В идеале нам нужно выполнять только одну загрузку и передачу файла мира.

Для этого метод World::bufferChunks() удаляет фрагменты, которые находятся в виртуальной памяти, но невидимы, и интеллектуально загружает новые фрагменты из файла мира.

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

Пример загрузки фрагментов при малом расстоянии рендеринга. Артефакты искажения экрана вызваны ПО записи видео. Иногда возникают заметные пики загрузок, в основном вызванные созданием мешей

Кроме того, я задал флаг, сообщающий, что рендерер должен заново создать меш загруженного фрагмента.

Класс Blueprint и editBuffer

editBuffer — это сортируемый контейнер bufferObjects, содержащий информацию о редактировании в мировом пространстве и пространстве фрагментов.

Если при внесении изменений в мир записывать их в файл сразу же после внесения изменения, то нам придётся передавать весь текстовый файл целиком и записывать КАЖДОЕ изменение. Это ужасно с точки зрения производительности.

Поэтому сначала я записываю все изменения, которые нужно внести, в editBuffer при помощи метода addEditBuffer (который также вычисляет позиции изменений в пространстве фрагментов). Прежде чем записывать их в файл, я сортирую изменения по порядку фрагментов, которым они принадлежат по расположению их в файле.

Класс blueprint содержит editBuffer, а также несколько методов, позволяющих создавать editBuffers конкретных объектов (деревьев, кактусов, хижин, и т.д.). Затем blueprint можно преобразовать в позицию, в которую нужно поместить объект, а далее просто записать его в память мира.

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

Я активно использую его на этапе генерации мира, чтобы расширить «бутылочное горлышко» записи изменений в файл.

Класс world хранит собственный blueprint изменений, внесённых в мир, чтобы при вызове bufferChunks() все изменения записывались на жёсткий диск за один проход, а затем удалялись из виртуальной памяти.

Рендеринг

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

Так как симуляция происходит не от первого лица, я выбрал ортографическую проекцию. Её можно было реализовать в формате псевдо-3D (т.е. предварительно спроецировать тайлы и наложить их в программном рендерере), но это показалось мне глупым. Я рад, что перешёл к использованию OpenGL.

Как сделать воксельный движок. Смотреть фото Как сделать воксельный движок. Смотреть картинку Как сделать воксельный движок. Картинка про Как сделать воксельный движок. Фото Как сделать воксельный движок

Базовый класс для рендеринга называется View, он содержит большинство важных переменных, управляющих визуализацией симуляции:

Как сделать воксельный движок. Смотреть фото Как сделать воксельный движок. Смотреть картинку Как сделать воксельный движок. Картинка про Как сделать воксельный движок. Фото Как сделать воксельный движок

Высокая глубина резкости (DOF). При больших расстояниях рендеринга может быть тормозной, но я всё это делал на своём ноутбуке. Возможно, на хорошем компьютере тормоза будут незаметны. Я понимаю, что это напрягает глаза и сделал так просто ради интереса.

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

Создание мешей фрагментов

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

Основной проблемой было эффективное создание из фрагментов рендерящихся VBO, но мне удалось реализовать на C++ собственную версию «жадного создания мешей» (greedy meshing), совместимую с OpenGL (не имеющую странных структур с циклами). Можете с чистой совестью пользоваться моим кодом.

В целом, переход к greedy meshing снизил количество отрисовываемых четырёхугольников в среднем на 60%. Затем, после дальнейших мелких оптимизаций (индексирования VBO) количество удалось снизить ещё на 1/3 (с 6 вершин на грань до 4 вершин).

При рендеринге сцены из 5x1x5 фрагментов в окне, не развёрнутом на весь экран, я получаю в среднем около 140 FPS (с отключенным VSYNC).

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

Шейдеры и выделение вокселей

Реализация GLSL-шейдеров — одна из самых интересных, и в то же время самых раздражающих частей написания движка из-за сложности отладки на GPU. Я не специалист по GLSL, поэтому многому приходилось учиться на ходу.

Реализованные мной эффекты активно используют FBO и сэмплирование текстур (например, размытие, наложение теней и использование информации о глубинах).

Мне всё ещё не нравится текущая модель освещения, потому что она не очень хорошо обрабатывает «темноту». Надеюсь, это будет исправлено в дальнейшем, когда я буду работать над циклом смены дня и ночи.

Также я реализовал простую функцию выбора вокселей при помощи модифицированного алгоритма Брезенхэма (это ещё одно преимущество использования вокселей). Она полезна для получения пространственной информации в процессе работы симуляции. Моя реализация работает только для ортографических проекций, но можете ею воспользоваться.

Как сделать воксельный движок. Смотреть фото Как сделать воксельный движок. Смотреть картинку Как сделать воксельный движок. Картинка про Как сделать воксельный движок. Фото Как сделать воксельный движок

Игровые классы

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

Мой обработчик событий (event handler) некрасив, зато функционален. С радостью приму рекомендации по его улучшению, особенно по использованию SDL Poll Event.

Последние примечания

Сам движок — это просто система, в которую я помещаю своих task-bots (подробно о них я расскажу в следующем посте). Но если вам показались интересными мои методы, и вы хотите узнать больше, то напишите мне.

Затем я портировал систему task-bot (настоящее сердце этого проекта) в 3D-мир и значительно расширил её возможности, но подробнее об этом позже (однако код уже выложен онлайн)!

Источник

Пишем собственный воксельный движок

Как сделать воксельный движок. Смотреть фото Как сделать воксельный движок. Смотреть картинку Как сделать воксельный движок. Картинка про Как сделать воксельный движок. Фото Как сделать воксельный движок

Примечание: полный исходный код этого проекта выложен здесь: [source].

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

После выпуска первоначального концепта Task-Bot [перевод на Хабре] я почувствовал, что меня ограничивает двухмерное пространство, в котором я работал. Казалось, что оно сдерживает возможности емерджентного поведения ботов.

Предыдущие неудачные попытки изучения современного OpenGL поставили передо мной мысленный барьер, но в конце июля я каким-то образом наконец пробил его. Сегодня, в конце октября, у меня уже достаточно уверенное понимание концепций, поэтому я выпустил собственный простой воксельный движок, который будет средой для жизни и процветания моих Task-Bots.

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

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

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

Концепция движка

Движок полностью написан с нуля на C++ (за некоторыми исключениями, например, поиска пути). Для рендеринга контекста и обработки ввода я использую SDL2, для отрисовки 3D-сцены — OpenGL, а для управления симуляцией — DearImgui.

Я решил использовать воксели в основном потому, что хотел работать с сеткой, которая имеет множество преимуществ:

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

Класс World

Класс мира служит базовым классом для хранения всей информации мира. Он обрабатывает генерацию, загрузку и сохранение данных блоков.

Данные блоков хранятся во фрагментах (chunks) постоянного размера (16^3), а мир хранит вектор фрагментов, загруженный в виртуальную память. В больших мирах практически необходимо хранить в памяти только определённую часть мира, поэтому я и выбрал такой подход.

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

Если я когда-нибудь реализую многопоточное сохранение и загрузку фрагментов, то преобразование плоского массива в разреженное октодерево и обратно может быть вполне возможным вариантом для экономии памяти. Здесь ещё есть пространство для оптимизации!

Моя реализация разреженного октодерева сохранилась в коде, поэтому можете спокойно ею воспользоваться.

Хранение фрагментов и работа с памятью

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

Фрагменты сериализованы при помощи библиотеки boost, а данные мира хранятся как простой текстовый файл, в котором каждый фрагмент — это строка файла. Они генерируются в определённом порядке, чтобы их можно было «упорядочить» в файле мира. Это важно для дальнейших оптимизаций.

В случае большого размера мира основным узким местом является считывание файла мира и загрузка/запись фрагментов. В идеале нам нужно выполнять только одну загрузку и передачу файла мира.

Для этого метод World::bufferChunks() удаляет фрагменты, которые находятся в виртуальной памяти, но невидимы, и интеллектуально загружает новые фрагменты из файла мира.

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

Пример загрузки фрагментов при малом расстоянии рендеринга. Артефакты искажения экрана вызваны ПО записи видео. Иногда возникают заметные пики загрузок, в основном вызванные созданием мешей

Кроме того, я задал флаг, сообщающий, что рендерер должен заново создать меш загруженного фрагмента.

Класс Blueprint и editBuffer

editBuffer — это сортируемый контейнер bufferObjects, содержащий информацию о редактировании в мировом пространстве и пространстве фрагментов.

Если при внесении изменений в мир записывать их в файл сразу же после внесения изменения, то нам придётся передавать весь текстовый файл целиком и записывать КАЖДОЕ изменение. Это ужасно с точки зрения производительности.

Поэтому сначала я записываю все изменения, которые нужно внести, в editBuffer при помощи метода addEditBuffer (который также вычисляет позиции изменений в пространстве фрагментов). Прежде чем записывать их в файл, я сортирую изменения по порядку фрагментов, которым они принадлежат по расположению их в файле.

Класс blueprint содержит editBuffer, а также несколько методов, позволяющих создавать editBuffers конкретных объектов (деревьев, кактусов, хижин, и т.д.). Затем blueprint можно преобразовать в позицию, в которую нужно поместить объект, а далее просто записать его в память мира.

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

Я активно использую его на этапе генерации мира, чтобы расширить «бутылочное горлышко» записи изменений в файл.

Класс world хранит собственный blueprint изменений, внесённых в мир, чтобы при вызове bufferChunks() все изменения записывались на жёсткий диск за один проход, а затем удалялись из виртуальной памяти.

Рендеринг

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

Так как симуляция происходит не от первого лица, я выбрал ортографическую проекцию. Её можно было реализовать в формате псевдо-3D (т.е. предварительно спроецировать тайлы и наложить их в программном рендерере), но это показалось мне глупым. Я рад, что перешёл к использованию OpenGL.

Как сделать воксельный движок. Смотреть фото Как сделать воксельный движок. Смотреть картинку Как сделать воксельный движок. Картинка про Как сделать воксельный движок. Фото Как сделать воксельный движок

Базовый класс для рендеринга называется View, он содержит большинство важных переменных, управляющих визуализацией симуляции:

Как сделать воксельный движок. Смотреть фото Как сделать воксельный движок. Смотреть картинку Как сделать воксельный движок. Картинка про Как сделать воксельный движок. Фото Как сделать воксельный движок

Высокая глубина резкости (DOF). При больших расстояниях рендеринга может быть тормозной, но я всё это делал на своём ноутбуке. Возможно, на хорошем компьютере тормоза будут незаметны. Я понимаю, что это напрягает глаза и сделал так просто ради интереса.

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

Создание мешей фрагментов

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

Основной проблемой было эффективное создание из фрагментов рендерящихся VBO, но мне удалось реализовать на C++ собственную версию «жадного создания мешей» (greedy meshing), совместимую с OpenGL (не имеющую странных структур с циклами). Можете с чистой совестью пользоваться моим кодом.

В целом, переход к greedy meshing снизил количество отрисовываемых четырёхугольников в среднем на 60%. Затем, после дальнейших мелких оптимизаций (индексирования VBO) количество удалось снизить ещё на 1/3 (с 6 вершин на грань до 4 вершин).

При рендеринге сцены из 5x1x5 фрагментов в окне, не развёрнутом на весь экран, я получаю в среднем около 140 FPS (с отключенным VSYNC).

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

Шейдеры и выделение вокселей

Реализация GLSL-шейдеров — одна из самых интересных, и в то же время самых раздражающих частей написания движка из-за сложности отладки на GPU. Я не специалист по GLSL, поэтому многому приходилось учиться на ходу.

Реализованные мной эффекты активно используют FBO и сэмплирование текстур (например, размытие, наложение теней и использование информации о глубинах).

Мне всё ещё не нравится текущая модель освещения, потому что она не очень хорошо обрабатывает «темноту». Надеюсь, это будет исправлено в дальнейшем, когда я буду работать над циклом смены дня и ночи.

Также я реализовал простую функцию выбора вокселей при помощи модифицированного алгоритма Брезенхэма (это ещё одно преимущество использования вокселей). Она полезна для получения пространственной информации в процессе работы симуляции. Моя реализация работает только для ортографических проекций, но можете ею воспользоваться.

Как сделать воксельный движок. Смотреть фото Как сделать воксельный движок. Смотреть картинку Как сделать воксельный движок. Картинка про Как сделать воксельный движок. Фото Как сделать воксельный движок

Игровые классы

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

Мой обработчик событий (event handler) некрасив, зато функционален. С радостью приму рекомендации по его улучшению, особенно по использованию SDL Poll Event.

Последние примечания

Сам движок — это просто система, в которую я помещаю своих task-bots (подробно о них я расскажу в следующем посте). Но если вам показались интересными мои методы, и вы хотите узнать больше, то напишите мне.

Затем я портировал систему task-bot (настоящее сердце этого проекта) в 3D-мирр и значительно расширил её возможности, но подробнее об этом позже (однако код уже выложен онлайн)!

Источник

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

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