Это последняя статья из цикла о процедурно генерируемых с помощью Unity и C# картах мира. Осторожно, под катом 7 МБ картинок.
Содержание
Введение
Генерирование шума
Начало работы
Генерирование карты высот
Свертывание карты на одной оси
Свертывание карты на обеих осях
Поиск соседних элементов
Битовые маски
Заливка
Генерирование тепловой карты
Генерирование карты влажности
Генерирование рек
Часть 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.
Автор: PatientZero