Bootstrap

Unity 使用柏林噪声程序化生成地形

参考教程链接
项目链接

柏林噪声函数简述

👇对噪声和柏林噪声不了解的可以看下面这个讲解。
柏林函数简介

  简单来说柏林噪声是一种连续的、渐变的噪声,不理解原理也无所谓,unity自带有Mathf.PerlinNoise(X-coordinate,Y-coordinate);我们可以根据这个来制作更有层次性的柏林噪声。你可以把这个函数理解为Unity提供了一张很大的随机平滑噪声图,我们可以通过(x,y)来在上面采样。

带有平滑的值噪声
带有平滑的值噪声
Unity自带的柏林噪声
在这里插入图片描述

在这里插入图片描述

  当我们形容一段波函数,振幅(Amplitude)指的是y轴,频率(Frequency)指的是x轴。

  如果只是这样一段是不够丰富,我们可以多设几段波,让他们在不同的频率下相加,更加丰富一点。例如下图,我们把原始的波函数看作主要的山脉轮廓,把2倍频率的看作巨石,把4倍频率的看作小石头,这样就可以做出主轮廓变化平缓,巨石出现频繁,小石头出现更加频繁的效果,最后把这三个参数相加就可以得到一个变化更加丰富的波形了。
  如果你理解了这点,你就懂了间隙度(lacunarity) 这个参数,通过这个参数,我们可以控制山脉高低变化的频率,把频率变成1 2 4,1 3 9 等等。

在这里插入图片描述
  但又有一个问题,我们大石头、小石头对波的影响太大了,应该区分开,让大石头、小石头的占比更小一点。我们可以修改振幅来处理,例如大石头的振幅只有0.5,小石头的振幅只有0.25。
  如下图,可以看出大石头、小石头振幅发生了明显的变化,把他们相加,我们就得到了一条保留原来大致轮廓,又更有层次感的波,相对应的,我们引入一个新的参数 持久性(persistance) 来控制振幅。。
  注意间隙度(lacunarity)应该是大于1的,保证乘方后的频率是递增的,变化越来越快,
  持久性(persistance)是小于1的,保证乘方后振幅是递减的,对整体的影响越来越小。
在这里插入图片描述
  你可能注意到图中还有另外一个参数 度(octave) ,在上述的例子中,我们用主轮廓、大石头、小石头三个波来解释对应的变化,但实际的时候,我们可以设置更多不同的波,就像中石头、更小的石头,当我们把度设为3的时候就是上述的例子,当我们增加度,就可以具体出更多的波来叠加。
  理解了lacunarity、persistance、octave这三个参数,就懂了我们对原始柏林函数的修改。cheer!

初始准备

  先来看看我们的起始项目
项目链接跳转

Noise:是我们后续写柏林噪声的主要地方,核心代码区,先写一个值噪声理解下原理,生成一个width*height的(0,1)随机数组。
MapDisplay:将上一步生成的二位噪声数组从(0,1)lerp转换到(0,0,0)到(255,255,255),转成纹理,赋值到材质球上。
MapGenerate:承载后续的各种参数,然后调用我们写的各种函数。
MapGeneratorEditor: 编辑器扩展,当我们点击Generate按钮,或者有参数改动时,调用GenerateMap函数来重新生成。

  这样我们就获得了一个可以实时展示生成噪声的Plane。
在这里插入图片描述
  下面的注释中有,是使用Unity自带柏林噪声的版本
在这里插入图片描述
  可以看出原始柏林函数已经做到了随机和平滑,但我们要做的不止于此,而是这样、这样的更丰富的噪声。甚至引入随机种子来让噪声“彻底”随机。

在这里插入图片描述
在这里插入图片描述

代码实现部分

  回忆一下上面的内容,我们要做的是分octave次级,每次计算出新的noiseValue,然后叠加计算出最后的高度;
  所以我们noise的新框架大概是这样的

       for (int y = 0; y < mapHeight; y++)
       {
           for (int x = 0; x < mapWidth; x++)
           {
          	   	float noiseHeight = 0;  //高度,即最终该点的颜色值,将每一度的振幅相加来获得
               	
               	//分octaves次级
               for (int i = 0; i < octaves; i++)
               {
               	//1.根据persistance、lacunarity计算出新的 频率、振幅
   				//2.通过频率、振幅计算出新的噪声值noiseValue
   				
   				noiseHeight += noiseValue; //3.把每一度的值叠加起来,得到最终颜色
               }
               
               noiseMap[x, y] = noiseHeight;		//获取结果
           }
       }

  现在的柏林噪声生成代码中,Mathf.PerlinNoise(x,y),(x,y)就是代表频率,返回值代表振幅,加入振幅amplitude和频率frequency就转化成 Mathf.PerlinNoise(x * frequency, y * frequency) * amplitude 。

代码如下

        for (int y = 0; y < mapHeight; y++)
        {
            for (int x = 0; x < mapWidth; x++)
            {
                //2.使用unity的柏林函数
                float amplitude = 1;    //振幅
                float frequency = 1;    //频率
                float noiseHeight = 0;  //高度,即最终该点的颜色值,将每一度的振幅相加来获得
                //分octaves次级
                for (int i = 0; i < octaves; i++)
                {
                    float sampleX = x / scale * frequency;
                    float sampleY = y / scale * frequency;  //用频率影响采样点间隔

                    float perlinValue = Mathf.PerlinNoise(sampleX, sampleY);
                    noiseHeight += perlinValue * amplitude; //把每一度的值叠加起来,得到最终颜色

                    amplitude *= persistance;	//每次要对振幅和频率进行开方
                    frequency *= lacunarity;
                }
                noiseMap[x, y] = noiseHeight;	
            }
        }

现在我们来解决另一个问题,因为我们的噪声生成是随机的,可能出现这种情况
在这里插入图片描述
噪声并没有占满[0,1],我们希望像下面这样把范围归一化到[0,1],保证不会出现过于明显的极端情况
在这里插入图片描述
最终代码如下
noise.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public static class Noise 
{
    //octaves:度
    //persistance:持久性,控制振幅
    //lacunarity: 间隙度,控制频率
    public static float[,] GenerateNoiseMap(int mapWidth, int mapHeight,float scale, int octaves, float persistance, float lacunarity)
    {
        float[,] noiseMap = new float[mapWidth, mapHeight];

        //防止除以0,除以负数
        if (scale <= 0)
        {
            scale = 0.0001f;
        }

        float maxNoiseHeight = float.MinValue;
        float minNoiseHeight = float.MaxValue;

        for (int y = 0; y < mapHeight; y++)
        {
            for (int x = 0; x < mapWidth; x++)
            {
                //使用unity的柏林函数
                float amplitude = 1;    //振幅
                float frequency = 1;    //频率
                float noiseHeight = 0;  //高度,即最终该点的颜色值,将每一度的振幅相加来获得
                //分octaves次级
                for (int i = 0; i < octaves; i++)
                {
                    float sampleX = x / scale * frequency;
                    float sampleY = y / scale * frequency;  //用频率影响采样点间隔

                    float perlinValue = Mathf.PerlinNoise(sampleX, sampleY);
                    noiseHeight += perlinValue * amplitude; //振幅影响

                    amplitude *= persistance;	//更换新的频率和振幅
                    frequency *= lacunarity;
                }
                
                if (noiseHeight > maxNoiseHeight)
                {
                    maxNoiseHeight = noiseHeight;
                }
                else if (noiseHeight < minNoiseHeight)
                {
                    minNoiseHeight = noiseHeight;
                }
                noiseMap[x, y] = noiseHeight;
            }
        }
		
		//把噪声的范围从[minNoiseHeight,maxNoiseHeight]归一化到[0,1]
        for (int y = 0; y < mapHeight; y++)
        {
            for (int x = 0; x < mapWidth; x++)
            {
                noiseMap[x, y] = Mathf.InverseLerp(minNoiseHeight, maxNoiseHeight, noiseMap[x, y]);
            }
        }

        return noiseMap;
    }
}

  具体操作为,定义minNoiseHeight,maxNoiseHeight两个参数来记录整张噪声图的最大、最小值, 每计算一个像素点后,我们检查是否更新。最后使用Mathf.InverseLerp 函数来对所有点归一化,靠近minNoiseHeight的点结果靠近0,靠近maxNoiseHeight的更靠近1。

  至此,我们已经应用好了前面提到的理论,再更改下MapGenerate.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class MapGenerate : MonoBehaviour
{
	public int mapWidth;
	public int mapHeight;
	public float noiseScale = 10f;

	[Range(1, 100)]
	public int octaves;			//度,需要大于1
	[Range(0, 1)]
	public float persistance;	//持久度,需要大于0,小于1
	[Range(1, 20)]
	public float lacunarity;    //间隙度,控制频率,需要大于1

	//是否开启在编辑器中自动更新
	public bool autoUpdate = true;
	public void GenerateMap()
	{
		//生成噪声
		float[,] noiseMap = Noise.GenerateNoiseMap(mapWidth, mapHeight,noiseScale,octaves,persistance,lacunarity);

		//显示在Plane上
		MapDisplay display = FindObjectOfType<MapDisplay>();
		display.DrawNoiseMap(noiseMap);
	}
}

  至此,我们就可以获得不错的噪声了(右侧是可参考的参数)
在这里插入图片描述

添加随机性

  随机的思路是对采样加上随机偏移,我们之前用这样的方式来采样

        for (int y = 0; y < mapHeight; y++)
        {
            for (int x = 0; x < mapWidth; x++)
            {
				float perlinValue = Mathf.PerlinNoise(x, y);
			}
		}

  结果就是永远是在[0,0] 到 [mapWidth,mapHeight]这个范围进行采样,如果我们这样修改

float perlinValue = Mathf.PerlinNoise(x + RandomOffset.x, y + RandomOffset.y);

  就可以把采样范围从[0,0] ~ [mapWidth,mapHeight] 随机到 [RandomOffset.x,RandomOffset.y] ~ [mapWidth + RandomOffset.x, mapHeight + RandomOffset.y]上。

  在此基础上,我们可以添加一个手动的Vector2 offset,令RandomOffset += offset,这样我们也可以手动简单地偏移噪声。
  代码大致如下

		//引入两个新变量 随机种子seed ,还有自定义偏移offset
		System.Random prng = new System.Random(seed);
		
		//即上文提到的RandomOffset,我们需要对每一个度都随机一个新的采样点
		Vector2[] octaveOffsets = new Vector2[octaves];	
		
		for (int i = 0; i < octaves; i++)  
		{
			float offsetX = prng.Next(-100000, 100000) + offset.x;	//RandomOffset += offset
			float offsetY = prng.Next(-100000, 100000) + offset.y;
			octaveOffsets[i] = new Vector2(offsetX, offsetY);
		}
		
		for (int y = 0; y < mapHeight; y++)
        {
            for(int x = 0; x < mapWidth; x++)
            {
				float amplitude = 1;
				float frequency = 1;
				float noiseHeight = 0;

				for (int i = 0; i < octaves; i++)
				{
					float sampleX = x / scale * frequency + octaveOffsets[i].x;
					float sampleY = y / scale * frequency + octaveOffsets[i].y;
					
					float perlinValue = Mathf.PerlinNoise(sampleX, sampleY);	
				}
			}
		}

项目链接跳转

这里我们还更改了另一个地方,x - halfWidth

float sampleX = (x-halfWidth) / scale * frequency + octaveOffsets[i].x; 

我们之前定义了一个NoiseScale参数来对噪声图进行缩放,但是以右上角为锚点进行缩放的。
这是因为x / scale ,是以(0,0)点(即右上角)为锚点进行缩放,而(x-halfWidth) / scale 相当于把(halfWidth,halfHeight)偏移到(0,0)点,即图像中点作为(0,0)锚点。
在这里插入图片描述
在这里插入图片描述

  那么用这个噪声可以做什么呢,我们只要添加一点点小功能,就可以实现各种效果。
在这里插入图片描述
在这里插入图片描述
具体做法可参考教程源文件也放到github上了。

;