Создание Планеты - часть 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; } }
Далее
На этом все. В будущих материалах мы рассмотрим вопрос освещения, атмосферного рассеивания, генерацию биомов, света для планет и уровень детализации.
- Старый Лагерь на новом скриншоте ремейка "Готики"
- Дизайнер Halo, Rainbow Six Siege и Fortnite присоединился к Bungie для работы над Marathon
- Нарративный директор Fable покидает Playground Games после года работы в должности