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

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

Для сохранения будущей детализации я решила разделить мою геометрию на шесть разных вершинных буфферов, которые позволяют игнорировать невидимые индивидуальные плоскости и ввести алгоритм LOD для каждой из плоскостей словно она является полностью отделенным куском.
for ( int j = 0; j < numRows; ++j )
{
for ( int i = 0; i < numCols; ++i )
{
vertices[ j * width + i ].UV.x = (float)i / (float)(numCols - 1);
vertices[ j * width + i ].UV.y = (float)j / (float)(numRows - 1);
}
}
Теперь у нас есть UV развертка, можно накладывать цвет и карту высот
Генерация карты высот
Как я упоминала в прошлом материале, одна из причин по которой я выбрала процедурную генерацию является мое отсутствие художественного таланта. Другими словами, наилучший результат при создании текстур для планет я получу если только создам программу генерирующую эти текстуры за меня. Кто сказал, что программистам не нужно опасаться потери работ в угоду ИИ?
У нас есть 6 сеток для каждой из которых требуется карта высот, одним важным условием являются плавные переходы между двумя разными сетками. Прошлый опыт с генерацией контента привел меня к поискам в Google информации по генерации поверхностей с использованием Шумов Перлина. Которые привели меня к этому туториалу.
Libnose - это портативная, открытая, понятная библиотека по генерации шумов в C++. Она неплохо выглядит для моих нужд, потому что я запускаю код на моем телефоне Nexus 5 и библиотека поддерживает функцию SetSeed позволяющую указывать зерно генерации, позволяющее при одном и том же вводе получать один и тот же результат.
Сгенерированная карта может быть легко превращена в кубическую карту или в куски отдельных текстур для каждой из сторон сферы. Вот пример карты из туториала выше:

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

К сожалению, после экспериментов он оказался слишком медленным и поэтому я занялась поисками альтернативы. Это привело меня к Simplex Noise - более быстрый метод с результатом походим на шумы Перлина. А еще этот метод умеет работать с графическим процессором.
Использование Simlex Noise
Теперь, когда я остановилась на алгоритме генерации шумов мне требуется внедрить его с учетом моих требований. Я нашла одним из его вариантов для C++ и решила поэкспериментировать с ним.
Подходящей функцией в файле simplexnoise.h для нас стала scaled_octave_noise_3d, которая позволяет создавать шум для 3D координат, что соответствует нашей сфере и стыкам разных сторон.
float scaled_octave_noise_3d( const float octaves,
const float persistence,
const float scale,
const float loBound,
const float hiBound,
const float x,
const float y,
const float z );
Для каждой вершины, после проецирования позиции на сферу, я указала смещение используя следующую функцию:
void CalculateHeight( Vector3& vPosition )
{
// Вектор направления из центра сферы
Vector3 vNormalFromCenter = vPosition;
vNormalFromCenter.Normalize();
// Переменные для шумов
static const float HEIGHT_MAX = 24.5f; // Planet Radius is 1,000.
static const float HEIGHT_MIN = -31.0f;
static const float NOISE_PERSISTENCE = 0.6f;
static const float NOISE_OCTAVES = 8.0f;
static const float NOISE_SCALE = 1.0f;
// Генерация шума для позиции
float fNoise = scaled_octave_noise_3d( HEIGHT_OCTAVES, HEIGHT_PERSISTENCE, NOISE_SCALE, HEIGHT_MIN, HEIGHT_MAX, vPosition.x, vPosition.y, vPosition.z );
// Установка уровня окаена как базового
if ( fNoise <= -0.0f )
fNoise = 0.0f;
// Вытеснение позиции
vPosition += vNormalFromCenter * fNoise;
}
Это привело к результату на первом изображении в материале. Я наложила цвета RGB(28,107,160) для всех высот 0.0f и ниже и RGB(61,82,29) для всех высот выше уровня моря.
Обратите внимание на переменные для минимальной и максимальной высоты. Так как две трети земли покрыта водой и одна треть - почва, я решила поэкспериментировать с переменными приводящими к 33%-40% почвы. Я обнаружила, что при указании чисел от -31.0 до 24.5 дает наилучшие результаты. Вы можете подобрать свои собственные варианты. Радиус моей планеты составляет 1,000, поэтому эти числа имеют определенный смысл при взгляде на соотношение.
Это достаточно удовлетворительное решение для начала. В будущем я надеюсь увеличить количество деталей накладывая вторую карту шумов с меньшими числами и/или увеличу размер высот выше уровня моря. Это должно создать дополнительные горы и меньшее равномерную сушу.
Вот пример планеты с переменной NOISE_SCALE = 2.5f:

Применение зерна
Как упоминалось в начале материала, одним из требований генератора являлась возможность репликации результата с ипользованием зерна. У libnoise есть функция SetSeed, к сожалению, у SimplexNoise такой нет.
Впрочем, ее не сложно написать самому. В начале файла simplexnoise.h есть массив с таблицей изменений.
static const int perm[512] = { ... }
Массив содержит числа от 0 до 255 вперемешку. Далее массив копируется один раз.
Изменяя ключевое слово const и применяя следующую функцию вы можете легко задать зерно и повторить один результат для планеты:
// Переменные
// perm - таблица изменений SimplexNoise
void set_seed( unsigned int seed )
{
// Указание зерна для генератора
srand( seed );
// СОздание таблицы числел
for ( int i = 0; i < 256; ++i )
{
perm[ i ] = i;
}
// Рандомизация таблицы случайных чисел
for ( int i = 0; i < 256; ++i )
{
// Замена нынешнего числа на число из таблицы
int k = perm[ i ];
// Случайная ячейка
int j = (int)random( 256 );
// Замена
perm[ i ] = perm[ j ];
perm[ j ] = k;
// Таблица повторяется. Указываем те же числа
perm[ 256 + i ] = perm[ j ];
perm[ 256 + j ] = k;
}
}
Далее
На этом все. В будущих материалах мы рассмотрим вопрос освещения, атмосферного рассеивания, генерацию биомов, света для планет и уровень детализации.
- Playground Games набирает команду для финальной стадии разработки Fable
- Инди-разработчик показал симуляцию 250 тысяч зомби в реальном времени на движке Godot
- Delos: Space Traffic Control предлагает стать диспетчером космической станции на орбите Марса