Вооружившись игровым движком Unity, я написал техническое демо, в котором пытаюсь наложить кубические воксели Minecraft на шарообразное тело (планету). Планета генерируется процедурно, и её можно полностью разрушить. Игроки могут расставлять или убирать разные блоки, которых насчитывается более 20 типов.

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

Ответы на ваши вопросы

Как в это играть?

Последняя сборка, которую я сделал, бесплатно выложена на моей странице в Itch.io вот здесь.Притом, что она оптимизирована для нативной работы под Windows, есть и играбельная веб-сборка, которую вы можете запускать в браузере (не исключено, что она покажется вам слегка кустарной).

Зачем вы это сделали?

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

Собираетесь ли вы доводить это до полноценной игры?

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

Сколько времени ушло у вас на эту работу?

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

Чем вы пользовались, выполняя этот проект?

Проект собран в Unity 6 на языке C#. Притом, что я активно задействовал систему заданий Unity и компилятор Burst, я не стал класть все яйца в одну корзину и полностью полагаться на DOTS. В меру моего (весьма ограниченного) понимания, DOTS требует изрядно поработать, но даёт в лучшем случае умеренный, а то и минимальный выигрыш в производительности. В то же время, в нём недостаёт некоторых элементарных возможностей.

Можно посмотреть исходный код?

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

Из какого набора текстур вы делали блоки?

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

Добро пожаловать на шар!

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

Слева: выделены те 2D-блоки (квадраты), центры которых лежат в пределах красного диска. Справа: выделены те 3D-блоки (кубы), центры которых лежат в пределах красного шара.
Слева: выделены те 2D-блоки (квадраты), центры которых лежат в пределах красного диска.
Справа: выделены те 3D-блоки (кубы), центры которых лежат в пределах красного шара.

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

Слева: 2D-представление планеты, собранной из кубиков, которые выровнены по сетке. Справа: планета собрана из кубиков, которые выстроились под действием гравитации.
Слева: 2D-представление планеты, собранной из кубиков, которые выровнены по сетке.
Справа: планета собрана из кубиков, которые выстроились под действием гравитации.

Так вот что нам на самом деле требуется: блоки нужно расставить так, чтобы их вертикальные грани всегда были сориентированы по вектору гравитации (верхняя грань обращена к небу, а нижняя — к центру планеты).

В рамках этой задачи нужно решить две (2) подзадачи:

  1. Спроецировать на 3D-сферу плоскую сетку, состоящую из квадратов

  2. Двигаясь в стороны, сохранять ширину блока неизменной

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

Проклятье картографа

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

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

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

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

Шесть квадратов (слева) спроецированы на соответствующие им сектора квад-сферы (справа).
Шесть квадратов (слева) спроецированы на соответствующие им сектора квад-сферы (справа).

Как построить квад-сферу? А очень просто:

  1. Берём куб, центр которого расположен в начале координат

  2. Каждую его грань делим на квадратную сетку

  3. Каждую вершину (угол решётки) сдвигаем вовне ровно на один (1) единичный отрезок от начала координат

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

Процесс построения квад-сферы. Слева: куб, центр которого расположен в начале координат. Центр: куб, грани которого поделены на сетки. Справа: куб, спроецированный на единичную сферу путём нормализации каждой из вершин.
Процесс построения квад-сферы. Слева: куб, центр которого расположен в начале координат. Центр: куб, грани которого поделены на сетки. Справа: куб, спроецированный на единичную сферу путём нормализации каждой из вершин.

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

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

Слева: Проекция на квад-сферу, сделанная по умолчанию. Справа: улучшенная проекция, в которой снижено искажение квадов.
Слева: Проекция на квад-сферу, сделанная по умолчанию. Справа: улучшенная проекция, в которой снижено искажение квадов.

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

Слева: Паттерн блоков в случае нормализации заданной по умолчанию квад-сферы. Справа: паттерн блоков в проекции со сниженным искажением.
Слева: Паттерн блоков в случае нормализации заданной по умолчанию квад-сферы. Справа: паттерн блоков в проекции со сниженным искажением.

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

Просто для интереса — давайте рассмотрим небольшую «кубистую» (по аналогии со «скалистой») планету до и после того, как к ней применена какая-либо сферическая проекция. Слева показана та же самая маленькая кубистая планета до проецирования вершин на сферу, а справа — после.

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

Попробуем закопаться

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

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

Слева: 2D-столбик блоков из плоского мира. Справа: 2D-столбик блоков из шарообразного мира, где в каждый слой входит постоянное количество блоков.
Слева: 2D-столбик блоков из плоского мира. Справа: 2D-столбик блоков из шарообразного мира, где в каждый слой входит постоянное количество блоков.

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

Слева: 2D-блочный столбик из плоского мира. Справа: 2D-блочный столбик из шарообразного мира, в котором на каждый слой выделяется «идеальное» количество блоков.
Слева: 2D-блочный столбик из плоского мира. Справа: 2D-блочный столбик из шарообразного мира, в котором на каждый слой выделяется «идеальное» количество блоков.

Можно пойти на компромисс и добавлять блоки в уровень лишь в тех случаях, когда искажение становится слишком велико. Кроме того, чтобы обеспечить плотное прилегание стыков между предыдущим и последующим слоем, нужно увеличивать количество блоков, умножая имеющееся значение на некий целочисленный коэффициент. Здесь подошло бы любое целое число ≥ 2, однако, пользуясь минимальным значением, то есть, 2, можно было бы добиться минимального искажения.

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

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

Из чего состоит планета

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

В плоском блочном мире структура для хранения блоков весьма проста. Есть мир, разделённый на столбчатые фрагменты, для каждого из которых существует минимальная и максимальная высота. По вертикали каждый из столбчатых фрагментов подразделяется на отображаемые фрагменты. Например, в Minecraft стандартный столбчатый фрагмент имеет размер 16x320x16 блоков и, в свою очередь, подразделяется на отображаемые фрагменты размером 16x16x16 блоков каждый.

Слева: Плоский блочный мир, разделённый на столбчатые фрагменты. Центр: одиночный столбчатый фрагмент. Справа: отдельно взятый отображаемый фрагмент (увеличено).
Слева: Плоский блочный мир, разделённый на столбчатые фрагменты. Центр: одиночный столбчатый фрагмент. Справа: отдельно взятый отображаемый фрагмент (увеличено).

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

Далее каждый сектор «по вертикали» подразделяется на оболочки. По мере того, как мы удаляемся от начала координат планеты, эти оболочки увеличиваются как по количеству слоёв, так и по количеству блоков в слое. Оболочек может быть сколько угодно, то есть, такой структурой можно покрыть всё доступное 3D-пространство (пока я всё-таки задал максимальную высоту, но в самой структуре планеты этот параметр никак не ограничен).

Ради эффективности далее разделю эти оболочки (размеры которых варьируются) на равномерные фрагменты величиной 16x16x16 блоков.

Почему удобно таким образом сгруппировывать блоки во фрагменты?

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

Вот почему почти во всех воксельных играх воксели обычно объединяют в группы, чтобы способствовать их пакетной обработке в тех случаях, когда один или более вокселей будут изменены. Обычно эти фрагменты бывают примерно одинакового размера, поэтому и поддаются идентичной обработке.

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

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

Слева: Шарообразный кубистый мир, разделённый на шесть секторов. Центр: отдельно взятый сектор, разделённый на оболочки (здесь показаны три). Справа: Отдельно взятый фрагмент из внешней оболочки (увеличен).
Слева: Шарообразный кубистый мир, разделённый на шесть секторов. Центр: отдельно взятый сектор, разделённый на оболочки (здесь показаны три). Справа: Отдельно взятый фрагмент из внешней оболочки (увеличен).

Чтобы было понятнее, как всё это выглядит в игре, вот два скриншота из ранней версии игры «Blocky Planet», где показано, как выглядит «лопнувший» мир.

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

Каков мой адрес?

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

Этот адрес указывает, где именно находятся данные по конкретному блоку в общей структуре планеты. Он во многом подобен почтовому адресу, который постепенно уточняется, например, Страна → Регион → Город → Дом. В случае с адресом блока соответствующая последовательность имеет вид Сектор → Оболочка → Фрагмент → Блок.

Если вам удобнее посмотреть код, то вот как можно представить этот адрес в виде структуры на C#:

struct BlockAddress
{
    public int sectorIndex; // 0 - 5
    public int shellIndex;  // 0 - +бесконечность
    public int3 chunkIndex; // [0-n, 0-n, 0-n]
    public int3 blockIndex; // [0-15, 0-15, 0-15]
}

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

Работа с соседними блоками

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

Цель: зная адрес блока и одно из шести (6) направлений по осям, вернуть адрес следующего соседнего блока в том же направлении.

Эти направления осей соответствуют положительным и отрицательным полуосям в трёхмерных декартовых координатах, где Влево = -X, Вправо = +X, Вниз = -Y, Вверх = +Y, Назад = -Z и Вперёд = +Z.

Схема шести направлений по осям, где рядом с блоком может располагаться соседний блок.
Схема шести направлений по осям, где рядом с блоком может располагаться соседний блок.

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

Вертикальные границы оболочек

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

Всё дело в том, что мы пытаемся минимизировать искажение в глубину, добавляя всё больше блоков в последующие оболочки. Это означает, что один блок в оболочке N будет соответствовать четырём  отдельным блокам в оболочке N+1, расположенной на уровень выше. Верно и обратное: у этих четырёх  блоков будет один общий сосед снизу.

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

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

А вот, если вам интересно, как эти вертикальные границы оболочек выглядят в игре:

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

Границы секторов

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

Шесть квадратов в секторе, локальные координатные системы которых выражаются через (u, v).Слева направо и сверху вниз: Вперёд, Вверх, Вниз, Влево, Вправо, Назад.
Шесть квадратов в секторе, локальные координатные системы которых выражаются через (u, v).Слева направо и сверху вниз: Вперёд, Вверх, Вниз, Влево, Вправо, Назад.

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

  1. Оси неправильно расположены (ось u прилипла к оси v)

  2. Оси инвертированы (направлены в противоположные стороны относительно тех, куда должны)

На этой схеме показаны все четыре комбинации вариантов выравнивания относительно граней секторов.
На этой схеме показаны все четыре комбинации вариантов выравнивания относительно граней секторов.

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

Прочая функциональность

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

Игрок и гравитация

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

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

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

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

А как обрабатывать повороты самого игрока?

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

Генерация ландшафта

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

1.     Задать все блоки как пустые (воздух)

2.     Если блок оказывается ниже самой высокой точки ландшафта — сделать его каменным

3.     Преобразовывать блоки в траву или грунт в зависимости от того, насколько они удалены от вершины своего столбца

4.     Если блок оказывается ниже планетарного уровня моря, то применяем к нему следующие преобразования:

o    Трава → Песок

o    Воздух → Вода

Процесс генерации фрагментов в зависимости от высоты ландшафта
Процесс генерации фрагментов в зависимости от высоты ландшафта

Так мы показываем, как именно высота ландшафта генерируется для всех столбцов, и именно этим шарообразные миры отличаются от плоских. В играх с плоскими мирами (таковых большинство) карта высот генерируется при помощи 2D-шума. Далее можно заглянуть на вершину интересующего вас столбца, указав координаты (x,z).

Есть две причины, по которым такой механизм очень сложно применить в сферическом мире:

  1. Значения шума должны гладко замощать всю сферу (т.е., не оставлять видимых швов)

  2. При проецировании 2D-шума на сферу будут возникать определённые искажения (о чём рассказано в первой половине этого поста)

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

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

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

Блочные структуры

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

Пример двух типов блочных структур: дубрава и построенный пользователем домик.
Пример двух типов блочных структур: дубрава и построенный пользователем домик.

Эти структуры сохраняются как 3D-массивы блоков (ниже именуемые исходной зоной). Теоретически должно быть совсем просто их размещать:

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

2. Скопировать все нужные блоки из исходной зоны в соответствующие позиции в зоне назначения

Здесь показано, как исходная зона (синяя) проецируется на соответствующую зону назначения (оранжевую).
Здесь показано, как исходная зона (синяя) проецируется на соответствующую зону назначения (оранжевую).

Но, как мы уже не раз убедились, в шарообразном мире из кубиков всё непросто.

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

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

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

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

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

Здесь показано, как блочные структуры хранятся в виде относительных направлений от исходного блока. В данном случае игроку потребуется поставить в точке назначения только тот блок, который станет исходным, а остальная структура вырастет вокруг него.
Здесь показано, как блочные структуры хранятся в виде относительных направлений от исходного блока. В данном случае игроку потребуется поставить в точке назначения только тот блок, который станет исходным, а остальная структура вырастет вокруг него.

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

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

Работа на будущее

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

Многопланетная система/спутники: начиная этот проект, я собирался создать солнечную систему в стиле Outer Wilds, полную кубистых планет, между которыми игрок мог бы путешествовать. Эти планеты могли бы двигаться по траекториям, напоминающим часовой механизм, либо можно было бы в более полной мере сымитировать гравитацию (как, например, в проекте солнечной системы от Себастьяна Лага).

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

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

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

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

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

Комментарии (4)


  1. koreychenko
    05.09.2025 16:11

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


  1. Jijiki
    05.09.2025 16:11

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

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


  1. tux32
    05.09.2025 16:11

    Любопытно было бы посмотреть, но увы - винды нет, в принципе. Собрал бы сам, но нет исходников. Жаль.


  1. JerryI
    05.09.2025 16:11

    Очень классное исследование. А квад сферы это прям хороший компромисс. Я как-то делал подобное на Хабре и сильно плохо получалось, пока в комментариях не подсказали как лучше