В этом цикле статей мы научимся создавать процедурно генерируемые карты мира с помощью Unity и C#. Цикл будет состоять из четырех статей.
Содержание
Часть 1 (эта статья):
Введение
Генерирование шума
Начало работы
Генерирование карты высот
Часть 2:
Поворот карты по одной оси
Поворот карты по обеим осям
Поиск соседних элементов
Битовые маски
Заливка
Часть 3:
Генерирование тепловой карты
Генерирование карты воды
Генерирование рек
Часть 4:
Генерирование биомов
Генерирование сферических карт
Введение
В этих обучающих статьях мы создадим процедурно генерируемые карты, похожие на такие:
Здесь представлены следующие карты:
- тепловая карта (левая верхняя)
- карта высот (правая верхняя)
- карта воды (правая нижняя)
- карта биомов (левая нижняя)
В следующих статьях этой серии мы узнаем, как управлять данными этих карт. Также мы рассмотрим способ проецирования карт на сферические поверхности.
Генерирование шума
В Интернете есть множество различных генераторов шума, большинство из них имеют открытые исходники, поэтому здесь не нужно изобретать велосипед. Я позаимствовал портированную версию библиотеки 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; } } } }
После запуска кода мы получим следующую текстуру:
Выглядит пока не очень интересно, но начало положено. У нас есть массив данных, содержащий значения от 0 до 1, с очень интересным рисунком.
Теперь нам нужно придать значимости нашим данным. Например, пусть все, что меньше 0,4, будет считаться водой. Мы можем изменить следующее в нашем TextureGenerator, назначив все значения ниже 0,4 синими, а выше — белыми:
if (value < 0.4f) pixels[x + y * width] = Color.blue; else pixels[x + y * width] = Color.white;
После этого мы получил следующее конечное изображение:
У нас уже что-то получается. Появляются фигуры, соответствующие этому простому правилу. Давайте сделаем следующий шаг.
Добавим других настраиваемых переменных в наш класс 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);
Добавив таким образом новые правила, мы получим следующие результаты:
У нас получилась интересная карта вершин с представляющей ее текстурой.
Исходники кода первой части вы можете скачать отсюда: World Generator Part1.
ссылка на оригинал статьи https://habrahabr.ru/post/276251/
Добавить комментарий