Генерация ландшафтов в Unity3d

в 12:15, , рубрики: game development, Gamedev, unity3d, Алгоритмы, метки: , ,

Генерация ландшафтов в Unity3d
Думаю все заметили, что сейчас стало появляться множество всяких бродилок с выживанием в стиле Minecraft. Сделать такую решился и я. Начало было лёгким — Unity3d имеет огромный функционал для сознания простеньких игр (и не только). Персонаж, игровые объекты, в общем основу сделать быстро. Но какой minecraft без рандомно генерируемого мира? Это стало первой трудной задачей. И думаю не только для меня. Просмотрев весь гугл и потратив кучу времени на эту бесполезную вещь я решил написать эту статью дабы сократить страдания других.

Дальше вас ждёт описание алгоритмов (и код) создания более менее реалистичных ландшафтов. Уточню, что все примеры на C#.

План действия

Для начала хорошо бы было разобраться, что подразумевается под генерацией ландшафта:

  1. Генерация карты высот. Это самая главная часть, по карте высот строится terrain (или mesh). Её можно также использовать для окрашивания terrain в зависимости от высоты Генерация ландшафтов в Unity3d
    и для расстановки игровых объектов.
  2. Построение ландшафта. Есть два способа выполнения этого пункта, в зависимости от того не хотите ли вы сложностей и используете ли вы unity3d, или же вам плевать на производительность, но вам важно, чтоб было красиво. В первом случае советую использовать встроенный в unity3d редактор ландшафта (terrain).
    Простенький код для этого:

    	Terrain terrain = FindObjectOfType<Terrain> (); // Находи наш terrain
    	float[,] heights = new float[resolution,resolution]; // Создаём массив вершин
    
    	// ...
    	// Делаем с heights всё, что хотим
    	// ...
    
    	terrain.terrainData.size = new Vector3(width,height,length); // Устанавливаем размер нашей карты
    	terrain.terrainData.heightmapResolution = resolution; // Задаём разрешение (кол-во высот)
    	terrain.terrainData.SetHeights(0, 0, heights); // И, наконец, применяем нашу карту высот (heights)
    

    Второй способ заключается в создании mesh. Данный метод даёт больший простор действий над ландшафтом, но он и сложнее: вам придётся создавать mesh, далее разбивать его на треугольникии и трудиться над шейдерами для покраски. Разобраться во 2 способе вам поможет эта статья.

  3. Наложение текстур.Конечный этап в генерации ландшафта. Здесь опять нам пригодится карта высот из первого пункта. Для наложения и смешивания текстур мы будем использовать простой шейдер.
    Shader "Custom/TerrainShader" {
        Properties {
            _HTex ("heightMap texture", 2D) = "white" {}
            _GTex ("grass texture", 2D) = "white" {}
            _RTex ("rock texture", 2D) = "white" {}
        }
        SubShader {
            Tags { "RenderType"="Opaque" }
            LOD 2048
            
            CGPROGRAM
            #pragma surface surf Lambert
            sampler2D _GrassTex;
            sampler2D _RockTex;
            sampler2D _HeightTex;
            struct Input {
                float2 uv_GTex;
                float2 uv_RTex;
                float2 uv_HTex;
            };
    
            void surf (Input IN, inout SurfaceOutput o) {
                
                float4 grass = tex2D(_GTex, IN.uv_GTex);
                float4 rock = tex2D(_RTex, IN.uv_RTex);
                float4 height = tex2D(_HTex, IN.uv_HTex);
    
                o.Albedo = lerp(grass, rock, height);
                 
            }
            ENDCG
        } 
        FallBack "Diffuse"
    }
    

    Здесь мы получили на вход 3 текстуры: карта высот, текстуры травы и камня. Далее мы смешиваем текстуры камня и травы по карте высот, используя функцию lerp(). И на выход мы подаём нашу карту высот но окрашенную в нужные текстуры.

Итак, поняв общий план действий, надо приступать к делу.

Частые ошибки

С самого начала я думал, что всё будет очень просто и для рандомной генерации ландшафта можно обойтись обычной функцией Random(). Но это самый неправильный способ. Его результат это вовсе не красивая карта, а расчёска в приближении.
Генерация ландшафтов в Unity3d

Шум Перлина

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

Perlin noise (Шум Перлина, также иногда Классический шум Перлина) — математический алгоритм по генерированию процедурной текстуры псевдо-случайным методом. Используется в компьютерной графике для увеличения реализма или графической сложности поверхности геометрических объектов. Также может использоваться для генерации эффектов дыма, тумана и т.д.

Думаю многих испугала приставка псевдо, но от неё легко избавиться. Далее представлен способ реализации шума в Unity3d:

  using UnityEngine;
  using System.Collections;
 
    public class PerlinNoisePlane : MonoBehaviour {
        public float power = 3.0f;
        public float scale = 1.0f;
        private Vector2 startPoint = new Vector2(0f, 0f);
 
        void Start () {
            MakeNoise ();
        }
 
        void MakeNoise() {
            MeshFilter mf = GetComponent<MeshFilter>(); // Ищем mesh
            Vector3[] vertices = mf.mesh.vertices; // Получаем его вершины
            for (int i = 0; i < vertices.Length; i++) {    
            float x = startPoint.x + vertices[i].x  * scale; // X координата вершины
            float z = startPoint.y + vertices[i].z  * scale; // Z координата вершины
                vertices[i].y = (Mathf.PerlinNoise (x, z) - 0.5f) * power;  // Задаём высоту для точки с вышеуказанными координатами
            }
            mf.mesh.vertices = vertices; // Присваиваем вершины
            mf.mesh.RecalculateBounds(); // Обновляем вершины
            mf.mesh.RecalculateNormals(); // Обновляем нормали
        }
    } 

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

Алгоритм diamond-square

После долгих часов скитаний по интернету наткнулся на этот алгоритм, и он оправдал все мои ожидания. Он даёт прекрасные результаты. Для расчитывания вершин есть очень простая формула.
Представим себе плоскость, её 4 вершины и точку по центру. Её высота будет равна сумме высот 4 вершин, делённая на их кол-во и некого случайного числа с коэффициентом. Вот код для unity3d (халява копи-пастерам):

using UnityEngine;
using System.Collections;

public class TerrainGenerator : MonoBehaviour {

	public float R; // Коэффициент скалистости
	public int GRAIN=8; // Коэффициент зернистости
	public bool FLAT = false; // Делать ли равнины
	public Material material; 

	private int width=2048;
	private int height=2048;
	private float WH;
	private Color32[] cols;
	private Texture2D texture;

	
	void Start () 
	{
		int resolution = width;
		WH = (float)width+height;

		// Задаём карту высот
		Terrain terrain = FindObjectOfType<Terrain> ();
		float[,] heights = new float[resolution,resolution]; 

		// Создаём карту высот
		texture = new Texture2D(width, height);
		cols = new Color32[width*height];
		drawPlasma(width, height);
		texture.SetPixels32(cols);
		texture.Apply();

		// Используем шейдер (смотри пункт 3 во 2 части)
		material.SetTexture ("_HeightTex", texture);

		// Задаём высоту вершинам по карте высот
		for (int i=0; i<resolution; i++) {
			for (int k=0;k<resolution; k++){
				heights[i,k] = texture.GetPixel(i,k).grayscale*R;
			}
		}

		// Применяем изменения
		terrain.terrainData.size = new Vector3(width, width, height);
		terrain.terrainData.heightmapResolution = resolution;
		terrain.terrainData.SetHeights(0, 0, heights);
	}

        // Считаем рандомный коэффициент смещения для высоты
	float displace(float num)
	{
		float max = num / WH * GRAIN;
		return Random.Range(-0.5f, 0.5f)* max;
	}
	
        // Вызов функции отрисовки с параметрами
	void drawPlasma(float w, float h) 
	{
		float c1, c2, c3, c4;
		
		c1 = Random.value;
		c2 = Random.value;
		c3 = Random.value;
		c4 = Random.value;
		
		divide(0.0f, 0.0f, w , h , c1, c2, c3, c4);
	}
	
        // Сама рекурсивная функция отрисовки
	void divide(float x, float y, float w, float h, float c1, float c2, float c3, float c4)
	{
		
		float newWidth = w * 0.5f;
		float newHeight = h * 0.5f;
		
		if (w < 1.0f || h < 1.0f)
		{
			float c = (c1 + c2 + c3 + c4) * 0.25f;
			cols[(int)x+(int)y*width] = new Color(c, c, c);
		}
		else
		{
			float middle =(c1 + c2 + c3 + c4) * 0.25f + displace(newWidth + newHeight);
			float edge1 = (c1 + c2) * 0.5f;
			float edge2 = (c2 + c3) * 0.5f;
			float edge3 = (c3 + c4) * 0.5f;
			float edge4 = (c4 + c1) * 0.5f;

			if(!FLAT){
				if (middle <= 0)
				{
					middle = 0;
				}
			else if (middle > 1.0f)
				{
					middle = 1.0f;
				}
			}
			divide(x, y, newWidth, newHeight, c1, edge1, middle, edge4);
			divide(x + newWidth, y, newWidth, newHeight, edge1, c2, edge2, middle);
			divide(x + newWidth, y + newHeight, newWidth, newHeight, middle, edge2, c3, edge3);
			divide(x, y + newHeight, newWidth, newHeight, edge4, middle, edge3, c4);
		}
	}
}

Материалы по теме

Автор: Reide740

Источник

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


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