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

в 10:01, , рубрики: C#, game development, unity3d, процедурная генерация, процедурные текстуры

image

В этом цикле статей мы научимся создавать процедурно генерируемые карты мира с помощью Unity и C#. Цикл будет состоять из четырех статей.

Содержание

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

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

Часть 2:

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

Часть 3:

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

Часть 4:

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

Введение

В этих обучающих статьях мы создадим процедурно генерируемые карты, похожие на такие:

image

Здесь представлены следующие карты:

  • тепловая карта (левая верхняя)
  • карта высот (правая верхняя)
  • карта воды (правая нижняя)
  • карта биомов (левая нижняя)

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

Генерирование шума

В Интернете есть множество различных генераторов шума, большинство из них имеют открытые исходники, поэтому здесь не нужно изобретать велосипед. Я позаимствовал портированную версию библиотеки Accidental Noise.

Портирование на C# выполнено Nikolaj Mariager.

Для правильной работы в Unity в портированную версию внесены незначительные изменения.

Вы можете использовать любой понравившийся генератор шума. Все техники, перечисленные в статье, могут применяться к другим источникам шума.

Начало работы

Сначала нам необходимо создать контейнер для хранения данных, которые мы будем генерировать.

Начнем с создания класса MapData. Переменные Min и Max нужны для отслеживания нижнего и верхнего пределов генерируемых значений.

public class MapData { 
    public float[,] Data;
    public float Min { get; set; }
    public float Max { get; set; }
 
    public MapData(int width, int height)
    {
        Data = new float[width, height];
        Min = float.MaxValue;
        Max = float.MinValue;
    }
}

Также мы создадим класс Tile, который будет позже использоваться для создания игровых объектов Unity из генерируемых данных.

public class Tile
{
    public float HeightValue { get; set; }
    public int X, Y;
         
    public Tile()
    {
    }
}

Чтобы посмотреть, что происходит, нам необходимо графическое представление данных. Для этого мы создадим новый класс TextureGenerator.

Пока этот класс будет создавать черно-белое отображение наших данных.

using UnityEngine;
 
public static class TextureGenerator {
         
    public static Texture2D GetTexture(int width, int height, Tile[,] tiles)
    {
        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++)
            {
                float value = tiles[x, y].HeightValue;
 
                //Set color range, 0 = black, 1 = white
                pixels[x + y * width] = Color.Lerp (Color.black, Color.white, value);
            }
        }
         
        texture.SetPixels(pixels);
        texture.wrapMode = TextureWrapMode.Clamp;
        texture.Apply();
        return texture;
    }
     
}

Скоро мы расширим этот класс.

Генерирование карты высот

Я решил, что карты будут фиксированного размера, поэтому нужно указать ширину (Width) и высоту (Height) карты. Также нам понадобятся настраиваемые параметры для генератора шума.

Мы сделаем эти данные отображаемыми в Unity Inspector, чтобы настройка карт была намного проще.

Класс Generator инициализирует модуль Noise, генерирует данные карты высот, создает массив тайлов, а затем генерирует текстурное представление этих данных.

Вот код с комментариями:

using UnityEngine;
using AccidentalNoise;
 
public class Generator : MonoBehaviour {
 
    // Настраиваемые переменные для Unity Inspector
    [SerializeField]
    int Width = 256;
    [SerializeField]
    int Height = 256;
    [SerializeField]
    int TerrainOctaves = 6;
    [SerializeField]
    double TerrainFrequency = 1.25;
 
    // Модуль генератора шума
    ImplicitFractal HeightMap;
     
    // Данные карты высот
    MapData HeightData;
 
    // Конечные объекты
    Tile[,] Tiles;
     
    // Вывод нашей текстуры (компонент unity)
    MeshRenderer HeightMapRenderer;
 
    void Start()
    {
        // Получаем меш, в который будут рендериться выходные данные
        HeightMapRenderer = transform.Find ("HeightTexture").GetComponent ();
 
        // Инициализируем генератор
        Initialize ();
         
        // Создаем карту высот
        GetData (HeightMap, ref HeightData);
         
        // Создаем конечные объекты на основании наших данных
        LoadTiles();
 
        // Рендерим текстурное представление нашей карты
        HeightMapRenderer.materials[0].mainTexture = TextureGenerator.GetTexture (Width, Height, Tiles);
    }
 
    private void Initialize()
    {
        // Инициализируем генератор карты высот
        HeightMap = new ImplicitFractal (FractalType.MULTI, 
                                       BasisType.SIMPLEX, 
                                       InterpolationType.QUINTIC, 
                                       TerrainOctaves, 
                                       TerrainFrequency, 
                                       UnityEngine.Random.Range (0, int.MaxValue));
    }
     
    // Извлекаем данные из модуля шума
    private void GetData(ImplicitModuleBase module, ref MapData mapData)
    {
        mapData = new MapData (Width, Height);
 
        // циклично проходим по каждой точке x,y - получаем значение высоты
        for (var x = 0; x < Width; x++)
        {
            for (var y = 0; y < Height; y++)
            {
                //Сэмплируем шум с небольшими интервалами
                float x1 = x / (float)Width;
                float y1 = y / (float)Height;
 
                float value = (float)HeightMap.Get (x1, y1);
 
                //отслеживаем максимальные и минимальные найденные значения
                if (value > mapData.Max) mapData.Max = value;
                if (value < mapData.Min) mapData.Min = value;
 
                mapData.Data[x,y] = value;
            }
        }   
    }
     
    // Создаем массив тайлов из наших данных
    private void LoadTiles()
    {
        Tiles = new Tile[Width, Height];
         
        for (var x = 0; x < Width; x++)
        {
            for (var y = 0; y < Height; y++)
            {
                Tile t = new Tile();
                t.X = x;
                t.Y = y;
                 
                float value = HeightData.Data[x, y];
                 
                //нормализуем наше значение от 0 до 1
                value = (value - HeightData.Min) / (HeightData.Max - HeightData.Min);
                 
                t.HeightValue = value;
 
                Tiles[x,y] = t;
            }
        }
    }
 
}

После запуска кода мы получим следующую текстуру:

image

Выглядит пока не очень интересно, но начало положено. У нас есть массив данных, содержащий значения от 0 до 1, с очень любопытным рисунком.

Теперь нам нужно придать значимости нашим данным. Например, пусть все, что меньше 0,4, будет считаться водой. Мы можем изменить следующее в нашем TextureGenerator, назначив все значения ниже 0,4 синими, а выше — белыми:

if (value < 0.4f)
    pixels[x + y * width] = Color.blue;
else
    pixels[x + y * width] = Color.white;

После этого мы получил следующее конечное изображение:

image

У нас уже что-то получается. Появляются фигуры, соответствующие этому простому правилу. Давайте сделаем следующий шаг.

Добавим других настраиваемых переменных в наш класс Generator. Они будут указывать на параметры, с которыми связаны значения высот.

float DeepWater = 0.2f;
float ShallowWater = 0.4f;  
float Sand = 0.5f;
float Grass = 0.7f;
float Forest = 0.8f;
float Rock = 0.9f;
float Snow = 1;

Также добавим новые цвета в генератор текстур:

private static Color DeepColor = new Color(0, 0, 0.5f, 1);
private static Color ShallowColor = new Color(25/255f, 25/255f, 150/255f, 1);
private static Color SandColor = new Color(240 / 255f, 240 / 255f, 64 / 255f, 1);
private static Color GrassColor = new Color(50 / 255f, 220 / 255f, 20 / 255f, 1);
private static Color ForestColor = new Color(16 / 255f, 160 / 255f, 0, 1);
private static Color RockColor = new Color(0.5f, 0.5f, 0.5f, 1);            
private static Color SnowColor = new Color(1, 1, 1, 1);

Добавив таким образом новые правила, мы получим следующие результаты:

image

У нас получилась интересная карта вершин с представляющей ее текстурой.

Исходники кода первой части вы можете скачать отсюда: World Generator Part1.

Автор: PatientZero

Источник

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


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