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

в 13:02, , рубрики: C#, game development, unity3d, Алгоритмы, процедурная генерация, процедурные текстуры, Работа с анимацией и 3D-графикой

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.

Автор: PatientZero

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js