
Это последняя статья из цикла о процедурно генерируемых с помощью Unity и C# картах мира. Осторожно, под катом 7 МБ картинок.
Содержание
Часть 1:
Введение
Генерирование шума
Начало работы
Генерирование карты высот
Часть 2:
Свертывание карты на одной оси
Свертывание карты на обеих осях
Поиск соседних элементов
Битовые маски
Заливка
Часть 3:
Генерирование тепловой карты
Генерирование карты влажности
Генерирование рек
Часть 4 (эта статья):
Генерирование биомов
Генерирование сферических карт
Генерирование биомов
Биомы — это способ классификации типов земной поверхности. Наш генератор биомов будет основан на популярной модели Уиттекера, в которой биомы классифицируются по количеству осадков и температуре. Мы уже сгенерировали тепловую карту и карту влажности для нашего мира, поэтому определение биомов будет довольно просто выполнить. Схема классификации Уиттекера представлена на следующей иллюстрации:

Мы можем разделить различные типы биомов по заданной температуре и уровню влажности. Сначала создадим новое перечисление, в котором будут храниться эти типы биомов:
public enum BiomeType { Desert, Savanna, TropicalRainforest, Grassland, Woodland, SeasonalForest, TemperateRainforest, BorealForest, Tundra, Ice }
Затем нужно создать таблицу, которая поможет нам определить тип биома на основании температуры и влажности. У нас уже есть HeatType и MoistureType. Каждое из этих перечислений содержит 6 определенных типов. Для сопоставления каждого из этих типов со схемой Уиттекера создана следующая таблица:

Для удобства поиска этих данных в коде преобразуем таблицу в двухмерный массив. Он будет таким:
BiomeType[,] BiomeTable = new BiomeType[6,6] { //COLDEST //COLDER //COLD //HOT //HOTTER //HOTTEST { BiomeType.Ice, BiomeType.Tundra, BiomeType.Grassland, BiomeType.Desert, BiomeType.Desert, BiomeType.Desert }, //DRYEST { BiomeType.Ice, BiomeType.Tundra, BiomeType.Grassland, BiomeType.Desert, BiomeType.Desert, BiomeType.Desert }, //DRYER { BiomeType.Ice, BiomeType.Tundra, BiomeType.Woodland, BiomeType.Woodland, BiomeType.Savanna, BiomeType.Savanna }, //DRY { BiomeType.Ice, BiomeType.Tundra, BiomeType.BorealForest, BiomeType.Woodland, BiomeType.Savanna, BiomeType.Savanna }, //WET { BiomeType.Ice, BiomeType.Tundra, BiomeType.BorealForest, BiomeType.SeasonalForest, BiomeType.TropicalRainforest, BiomeType.TropicalRainforest }, //WETTER { BiomeType.Ice, BiomeType.Tundra, BiomeType.BorealForest, BiomeType.TemperateRainforest, BiomeType.TropicalRainforest, BiomeType.TropicalRainforest } //WETTEST };
Чтобы еще больше упростить поиск, добавим новую функцию, возвращающую тип биома любого тайла. Эта часть довольно проста, ведь каждому тайлу уже назначен тип тепла и влажности.
public BiomeType GetBiomeType(Tile tile) { return BiomeTable [(int)tile.MoistureType, (int)tile.HeatType]; }
Эта проверка выполняется для каждого тайла и устанавливает области биомов для всей карты.
private void GenerateBiomeMap() { for (var x = 0; x < Width; x++) { for (var y = 0; y < Height; y++) { if (!Tiles[x, y].Collidable) continue; Tile t = Tiles[x,y]; t.BiomeType = GetBiomeType(t); } } }
Отлично, мы определили все биомы. Однако пока мы не можем их визуализировать. Следующим шагом будет назначение цвета каждому типу. Это позволит нам наглядно отобразить область каждого биома на изображении. Я выбрал следующие цвета:

Значения цветов вставлены в класс TextureGenerator вместе с кодом генерирования текстуры биомов:
//карта биомов private static Color Ice = Color.white; private static Color Desert = new Color(238/255f, 218/255f, 130/255f, 1); private static Color Savanna = new Color(177/255f, 209/255f, 110/255f, 1); private static Color TropicalRainforest = new Color(66/255f, 123/255f, 25/255f, 1); private static Color Tundra = new Color(96/255f, 131/255f, 112/255f, 1); private static Color TemperateRainforest = new Color(29/255f, 73/255f, 40/255f, 1); private static Color Grassland = new Color(164/255f, 225/255f, 99/255f, 1); private static Color SeasonalForest = new Color(73/255f, 100/255f, 35/255f, 1); private static Color BorealForest = new Color(95/255f, 115/255f, 62/255f, 1); private static Color Woodland = new Color(139/255f, 175/255f, 90/255f, 1); public static Texture2D GetBiomeMapTexture(int width, int height, Tile[,] tiles, float coldest, float colder, float cold) { var texture = new Texture2D(width, height); var pixels = new Color[width * height]; for (var x = 0; x < width; x++) { for (var y = 0; y < height; y++) { BiomeType value = tiles[x, y].BiomeType; switch(value){ case BiomeType.Ice: pixels[x + y * width] = Ice; break; case BiomeType.BorealForest: pixels[x + y * width] = BorealForest; break; case BiomeType.Desert: pixels[x + y * width] = Desert; break; case BiomeType.Grassland: pixels[x + y * width] = Grassland; break; case BiomeType.SeasonalForest: pixels[x + y * width] = SeasonalForest; break; case BiomeType.Tundra: pixels[x + y * width] = Tundra; break; case BiomeType.Savanna: pixels[x + y * width] = Savanna; break; case BiomeType.TemperateRainforest: pixels[x + y * width] = TemperateRainforest; break; case BiomeType.TropicalRainforest: pixels[x + y * width] = TropicalRainforest; break; case BiomeType.Woodland: pixels[x + y * width] = Woodland; break; } // Тайлы воды if (tiles[x,y].HeightType == HeightType.DeepWater) { pixels[x + y * width] = DeepColor; } else if (tiles[x,y].HeightType == HeightType.ShallowWater) { pixels[x + y * width] = ShallowColor; } // рисуем реки if (tiles[x,y].HeightType == HeightType.River) { float heatValue = tiles[x,y].HeatValue; if (tiles[x,y].HeatType == HeatType.Coldest) pixels[x + y * width] = Color.Lerp (IceWater, ColdWater, (heatValue) / (coldest)); else if (tiles[x,y].HeatType == HeatType.Colder) pixels[x + y * width] = Color.Lerp (ColdWater, RiverWater, (heatValue - coldest) / (colder - coldest)); else if (tiles[x,y].HeatType == HeatType.Cold) pixels[x + y * width] = Color.Lerp (RiverWater, ShallowColor, (heatValue - colder) / (cold - colder)); else pixels[x + y * width] = ShallowColor; } // добавляем контур if (tiles[x,y].HeightType >= HeightType.Shore && tiles[x,y].HeightType != HeightType.River) { if (tiles[x,y].BiomeBitmask != 15) pixels[x + y * width] = Color.Lerp (pixels[x + y * width], Color.black, 0.35f); } } } texture.SetPixels(pixels); texture.wrapMode = TextureWrapMode.Clamp; texture.Apply(); return texture; }
При рендеринге карт биомов получаются красивые сворачиваемые карты мира.

Генерирование сферических карт
До этого момента мы создавали миры, сворачиваемые по оси X и Y. Такие карты отлично подходят для игр, потому что данные легко рендерятся в игровую карту.
Если попытаться спроектировать такие сворачиваемые текстуры на сферу, они будут выглядеть странно. Чтобы наш мир мог накладываться на сферу, необходимо написать генератор сферических текстур. В этой части мы добавим такую функцию для сгенерированных нами миров.
Сферическое генерирование немного отличается от генерирования свертываемых карт, потому что требует других шумовых схем и наложения текстур. По этой причине мы разделим класс генератора на две ветви подклассов: WrappableWorldGenerator и SphericalWorldGenerator. Каждый из них будет наследовать базовый класс Generator.
Это позволит нам иметь общее функциональное ядро, предоставляющее расширенные возможности каждому типу генератора.
Исходный класс Generator, а также некоторые его функции станут абстрактными:
protected abstract void Initialize(); protected abstract void GetData(); protected abstract Tile GetTop(Tile tile); protected abstract Tile GetBottom(Tile tile); protected abstract Tile GetLeft(Tile tile); protected abstract Tile GetRight(Tile tile);
Имеющиеся у нас функции Initialize() и GetData() были созданы для сворачиваемых миров, поэтому для сферического генератора нужно написать новые. Также мы создадим новые классы получения тайлов, потому что свертывание будет происходит на оси X со сферическим проецированием.
Инициализация шума происходит способом, похожим на ранее описанный, за исключением одного главного отличия. Тепловая карта в новом генераторе не будет свертываться на оси Y. Поэтому мы не можем создать правильный градиент для умножения. Нам придется делать это вручную во время генерирования данных.
protected override void Initialize() { HeightMap = new ImplicitFractal (FractalType.MULTI, BasisType.SIMPLEX, InterpolationType.QUINTIC, TerrainOctaves, TerrainFrequency, Seed); HeatMap = new ImplicitFractal(FractalType.MULTI, BasisType.SIMPLEX, InterpolationType.QUINTIC, HeatOctaves, HeatFrequency, Seed); MoistureMap = new ImplicitFractal (FractalType.MULTI, BasisType.SIMPLEX, InterpolationType.QUINTIC, MoistureOctaves, MoistureFrequency, Seed); }
Функция GetData изменится значительно. Мы вернемся к сэмплированию трехмерного шума. Шум будет сэмплироваться на основании системы координат с широтой и долготой.
Я посмотрел, как выполнили сферическое проецирование в libnoise, и использовал ту же концепцию. Основной код, преобразующий координаты широты и долготы в декартовы координаты трехмерной сферической карты, будет следующим:
void LatLonToXYZ(float lat, float lon, ref float x, ref float y, ref float z) { float r = Mathf.Cos (Mathf.Deg2Rad * lon); x = r * Mathf.Cos (Mathf.Deg2Rad * lat); y = Mathf.Sin (Mathf.Deg2Rad * lon); z = r * Mathf.Sin (Mathf.Deg2Rad * lat); }
Функция GetData циклически переберет все координаты, используя этот способ преобразования для генерирования данных карты. С помощью этого способа мы создаем данные тепла, высоты и влажности. Карта биомов генерируется так же, как и раньше — из конечных тепловой карты и карты влажности.
protected override void GetData() { HeightData = new MapData (Width, Height); HeatData = new MapData (Width, Height); MoistureData = new MapData (Width, Height); // Указываем область нашей карты по широте/долготе float southLatBound = -180; float northLatBound = 180; float westLonBound = -90; float eastLonBound = 90; float lonExtent = eastLonBound - westLonBound; float latExtent = northLatBound - southLatBound; float xDelta = lonExtent / (float)Width; float yDelta = latExtent / (float)Height; float curLon = westLonBound; float curLat = southLatBound; // Циклически перебираем все тайлы с помощью их координат широты/долготы for (var x = 0; x < Width; x++) { curLon = westLonBound; for (var y = 0; y < Height; y++) { float x1 = 0, y1 = 0, z1 = 0; // Преобразуем широту и долготу в x, y, z LatLonToXYZ (curLat, curLon, ref x1, ref y1, ref z1); // Тепловые данные float sphereValue = (float)HeatMap.Get (x1, y1, z1); if (sphereValue > HeatData.Max) HeatData.Max = sphereValue; if (sphereValue < HeatData.Min) HeatData.Min = sphereValue; HeatData.Data [x, y] = sphereValue; // Настройка тепла на основании широты float coldness = Mathf.Abs (curLon) / 90f; float heat = 1 - Mathf.Abs (curLon) / 90f; HeatData.Data [x, y] += heat; HeatData.Data [x, y] -= coldness; // Данные высоты float heightValue = (float)HeightMap.Get (x1, y1, z1); if (heightValue > HeightData.Max) HeightData.Max = heightValue; if (heightValue < HeightData.Min) HeightData.Min = heightValue; HeightData.Data [x, y] = heightValue; // Данные влажности float moistureValue = (float)MoistureMap.Get (x1, y1, z1); if (moistureValue > MoistureData.Max) MoistureData.Max = moistureValue; if (moistureValue < MoistureData.Min) MoistureData.Min = moistureValue; MoistureData.Data [x, y] = moistureValue; curLon += xDelta; } curLat += yDelta; } }
Мы получаем, соответственно, карту высот, тепловую карту, карту влажности и карту биомов:

Заметьте, что карты изгибаются возле углов. Это сделано специально, так работает сферическое проецирование. Давайте применим текстуру биомов для сферы и посмотрим, что получится:

Неплохое начало. Обратите внимание, наша карта высот стала черно-белой. Мы сделали это для того, чтобы использовать карту высот в качестве шейдера сферы. Для лучшего эффекта нам необходимо рельефная текстура, поэтому мы сначала отрендерим черно-белую текстуру, отображающую нужные нам смещения. Эта текстура затем будет преобразована в рельефную текстуру с помощью следующего кода:
public static Texture2D CalculateBumpMap(Texture2D source, float strength) { Texture2D result; float xLeft, xRight; float yUp, yDown; float yDelta, xDelta; var pixels = new Color[source.width * source.height]; strength = Mathf.Clamp(strength, 0.0F, 10.0F); result = new Texture2D(source.width, source.height, TextureFormat.ARGB32, true); for (int by = 0; by < result.height; by++) { for (int bx = 0; bx < result.width; bx++) { xLeft = source.GetPixel(bx - 1, by).grayscale * strength; xRight = source.GetPixel(bx + 1, by).grayscale * strength; yUp = source.GetPixel(bx, by - 1).grayscale * strength; yDown = source.GetPixel(bx, by + 1).grayscale * strength; xDelta = ((xLeft - xRight) + 1) * 0.5f; yDelta = ((yUp - yDown) + 1) * 0.5f; pixels[bx + by * source.width] = new Color(xDelta, yDelta, 1.0f, yDelta); } } result.SetPixels(pixels); result.wrapMode = TextureWrapMode.Clamp; result.Apply(); return result; }
Передав этой функции левую текстуру, мы получим рельефную текстуру, изображенную справа:

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

Чтобы еще улучшить изображение, мы добавим пару слоев облаков. Сгенерировать облака с помощью шума очень просто, так почему бы и нет. Мы используем модуль волнового (billow) шума для создания облаков.
Добавим два слоя облаков, чтобы придать им глубины. Код генератора облачного шума представлен ниже:
Cloud1Map = new ImplicitFractal(FractalType.BILLOW, BasisType.SIMPLEX, InterpolationType.QUINTIC, 5, 1.65f, Seed); Cloud2Map = new ImplicitFractal (FractalType.BILLOW, BasisType.SIMPLEX, InterpolationType.QUINTIC, 6, 1.75f, Seed);
Мы используем данные таким же способом. Генератор текстур облаков — это простой линейный интерполятор (lerp) от белого до прозрачного белого. Мы отсекаем облака до установленного значения, делая все остальное прозрачным. Код генератора текстур облаков имеет следующий вид:
public static Texture2D GetCloudTexture(int width, int height, Tile[,] tiles, float cutoff) { var texture = new Texture2D(width, height); var pixels = new Color[width * height]; for (var x = 0; x < width; x++) { for (var y = 0; y < height; y++) { if (tiles[x,y].CloudValue > cutoff) pixels[x + y * width] = Color.Lerp(new Color(1f, 1f, 1f, 0), Color.white, tiles[x,y].CloudValue); else pixels[x + y * width] = new Color(0,0,0,0); } } texture.SetPixels(pixels); texture.wrapMode = TextureWrapMode.Clamp; texture.Apply(); return texture; }
Создадим с его помощь�� две различные текстуры облаков. Эти текстуры тоже создаются для сферического проецирования, поэтому имеют изгибы по краям:

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

В конце я привожу скриншот всех сгенерированных текстур, использованных для создания финального рендера планеты:

На этом серия статей заканчивается. Исходный код всего проекта на github: World Generator Final.
