Процедурно генерируемые карты мира на Unity C#, часть 4 (трафик)

от автора

image

Это последняя статья из цикла о процедурно генерируемых с помощью Unity и C# картах мира. Осторожно, под катом 7 МБ картинок.

Содержание

Часть 1:

Введение
Генерирование шума
Начало работы
Генерирование карты высот

Часть 2:

Свертывание карты на одной оси
Свертывание карты на обеих осях
Поиск соседних элементов
Битовые маски
Заливка

Часть 3:

Генерирование тепловой карты
Генерирование карты влажности
Генерирование рек

Часть 4 (эта статья):

Генерирование биомов
Генерирование сферических карт

Генерирование биомов

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

image

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

public enum BiomeType {     Desert,     Savanna,     TropicalRainforest,     Grassland,     Woodland,     SeasonalForest,     TemperateRainforest,     BorealForest,     Tundra,     Ice } 

Затем нужно создать таблицу, которая поможет нам определить тип биома на основании температуры и влажности. У нас уже есть HeatType и MoistureType. Каждое из этих перечислений содержит 6 определенных типов. Для сопоставления каждого из этих типов со схемой Уиттекера создана следующая таблица:

image

Для удобства поиска этих данных в коде преобразуем таблицу в двухмерный массив. Он будет таким:

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);         }     } } 

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

image

Значения цветов вставлены в класс 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; } 

При рендеринге карт биомов получаются красивые сворачиваемые карты мира.

image image

Генерирование сферических карт

До этого момента мы создавали миры, сворачиваемые по оси 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;     } } 

Мы получаем, соответственно, карту высот, тепловую карту, карту влажности и карту биомов:

image

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

image

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

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; } 

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

image

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

image

Чтобы еще улучшить изображение, мы добавим пару слоев облаков. Сгенерировать облака с помощью шума очень просто, так почему бы и нет. Мы используем модуль волнового (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; } 

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

image

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

image

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

image

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

ссылка на оригинал статьи https://habrahabr.ru/post/276551/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *