柏林噪声函数简述
👇对噪声和柏林噪声不了解的可以看下面这个讲解。
柏林函数简介
简单来说柏林噪声是一种连续的、渐变的噪声,不理解原理也无所谓,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上了。