Создание Планеты - часть 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; } }

Далее

На этом все. В будущих материалах мы рассмотрим вопрос освещения, атмосферного рассеивания, генерацию биомов, света для планет и уровень детализации.

Больше статей на Shazoo
Тэги:
Источники: