前言:Unity中是内置了PerlinNoise的,之所以我想要自己实现是因为自己实现一次可以更深地理解其背后原理。
###(一)、Perlin噪声介绍:
柏林噪声是一个主要用于程序生成随机内容的算法,本质上噪声就是一个随机数生成器。 其在游戏开发领域,常用于生成波形,起伏不平的材质或纹理。例如用柏林噪声来生成我的世界的地形,水,火焰以及云。
说到随机可能你会想到Unity里不是有Random类可以给我们提供随机数吗,为什么还需要Perlin Noise来生成一个噪声值。这是因为通过Random类获取的随机数它太随机了,毫无规律可言,两个相邻产生的随机数之间没有联系从而可能造成两个值相差过大,这也让它在PCG领域的应用不如Perlin Noise。拿Perlin Noise最典型的应用举例,Minecraft里面的无限大地形就是通过Perlin Noise来实现的,正是其平滑的特点让生成的地形贴近于自然,并且其伪随机性也在游戏开发中大放异彩,Minecraft中你输入相同的Seed(种子)总会得到一样的地形。
(二)、Perlin噪声原理:
- 一维柏林噪声:在X轴向上每个整数坐标随机生成一个数(范围为-1~1),我们称这个数为Gradient,译为梯度或者斜率。然后我们对相邻两个整数之间使用梯度进行插值计算,使得相邻两点之间平滑过渡。平滑度取决于所选用的插值函数,老版的柏林噪声使用f(t)=3t^2-2t^3,改进后的柏林噪声使用f(t)=t^3(t(t6-15)+10)。
- 二维柏林噪声:获取一个点P(x,y),然后得到P点周围最近的四个点A(i, j)、B(i+1, j)、C(i, j+1)、D(i+1, j+1);随后获取ABCD四点的二维梯度值G(A)、G(B)、G(C)、G(D),并且算出ABCD到P点的向量AP、BP、CP以及DP。接着,将G(A)与AP进行点乘,计算出A点对于P点的梯度贡献值,然后分别算出其余三个点对P点的梯度贡献值,使用缓和曲线(ease curves)来计算它们的权重和。
- 三维柏林噪声:同二维,计算8个顶点对P点的梯度贡献值
- 二维下有4个,三维下有8个,n维下有2^n个,所以复杂度为O(2^n)
所以柏林噪声算法中一个重要部分就是梯度的计算。所以Perlin噪声也叫Gradient噪声(基于梯度的噪声)。
(三)、Perlin的代码实现:
using System.Collections.Generic;
using UnityEngine;
using System;
public static class Noise
{
/// <summary>
/// 用于获得伪随机梯度的排列表,有Perlin本人提供
/// </summary>
private static readonly int[] perm = {
151,160,137,91,90,15,
131,13,201,95,96,53,194,233,7,225,140,36,103,30,69,142,8,99,37,240,21,10,23,
190, 6,148,247,120,234,75,0,26,197,62,94,252,219,203,117,35,11,32,57,177,33,
88,237,149,56,87,174,20,125,136,171,168, 68,175,74,165,71,134,139,48,27,166,
77,146,158,231,83,111,229,122,60,211,133,230,220,105,92,41,55,46,245,40,244,
102,143,54, 65,25,63,161, 1,216,80,73,209,76,132,187,208, 89,18,169,200,196,
135,130,116,188,159,86,164,100,109,198,173,186, 3,64,52,217,226,250,124,123,
5,202,38,147,118,126,255,82,85,212,207,206,59,227,47,16,58,17,182,189,28,42,
223,183,170,213,119,248,152, 2,44,154,163, 70,221,153,101,155,167, 43,172,9,
129,22,39,253, 19,98,108,110,79,113,224,232,178,185, 112,104,218,246,97,228,
251,34,242,193,238,210,144,12,191,179,162,241, 81,51,145,235,249,14,239,107,
49,192,214, 31,181,199,106,157,184, 84,204,176,115,121,50,45,127, 4,150,254,
138,236,205,93,222,114,67,29,24,72,243,141,128,195,78,66,215,61,156,180,
151
};
/// <summary>
/// 余弦插值
/// </summary>
/// <param name="a"></param>
/// <param name="b"></param>
/// <param name="t"></param>
/// <returns></returns>
private static float Cos_Interpolate(float a, float b, float t)
{
double ft = t * Math.PI;
t = (float)((1 - Math.Cos(ft)) * 0.5);
return a * (1 - t) + t * b;
}
public static float perlinNoise(float x,float y)
{
List<Vector2> gradientList = CalculateGradient();
Vector2 pos = new Vector2(x, y);
Vector2 rightUp = new Vector2((int)x + 1, (int)y + 1);
Vector2 rightDown = new Vector2((int)x + 1, (int)y);
Vector2 leftUp = new Vector2((int)x, (int)y + 1);
Vector2 leftDown = new Vector2((int)x, (int)y);
float v1 = DotGridGradient(gradientList[GradientIndex((int)leftDown.x, (int)leftDown.y)], GetDisVec(pos, leftDown));
float v2 = DotGridGradient(gradientList[GradientIndex((int)rightDown.x, (int)rightDown.y)], GetDisVec(pos, rightDown));
float interpolation1 = Cos_Interpolate(v1, v2, x - (int)x);
float v3 = DotGridGradient(gradientList[GradientIndex((int)leftUp.x, (int)leftUp.y)], GetDisVec(pos, leftUp));
float v4 = DotGridGradient(gradientList[GradientIndex((int)rightUp.x, (int)rightUp.y)], GetDisVec(pos, rightUp));
float interpolation2 = Cos_Interpolate(v3, v4, x - (int)x);
return Cos_Interpolate(interpolation1, interpolation2, y - (int)y);
}
/// <summary>
/// 获取各个点的伪随机梯度
/// </summary>
/// <param name="x">点的X坐标</param>
/// <param name="y">点的Y坐标</param>
/// <returns></returns>
private static int GradientIndex(int x,int y)
{
return perm[(x + perm[y & 255]) & 255] & 7;//&7是为了保证取梯度向量时不越界
}
/// <summary>
/// 计算伪随机向量
/// </summary>
/// <returns></returns>
private static List<Vector2> CalculateGradient()
{
//可以采用Perlin在论文中给出的12个向量
//我们这里采用在单位圆上分布均匀的单位向量
List<Vector2> tempList = new List<Vector2>();
for(int i = 0; i < 8; i++)
{
//
Vector2 vec = new Vector2(Mathf.Cos(Mathf.PI/4 * i), Mathf.Sin(Mathf.PI/4 * i));
tempList.Add(vec);
}
return tempList;
}
/// <summary>
/// 获得各顶点到指定点的方向向量
/// </summary>
/// <param name="p1">指定点</param>
/// <param name="p2">分别为四个顶点</param>
/// <returns></returns>
private static Vector2 GetDisVec(Vector2 p1,Vector2 p2)
{
return p1 - p2;
}
/// <summary>
/// 将梯度向量和个方向向量进行点乘
/// </summary>
/// <param name="gradient"></param>
/// <param name="dirVec"></param>
/// <returns></returns>
private static float DotGridGradient(Vector2 gradient,Vector2 dirVec)
{
return Vector2.Dot(gradient, dirVec) / 2 + 0.5f;
}
}
读者可以将代码粘贴进Unity里面自行测试,得到的噪声值和Unity内置的进行比较。当然效果没有内置的好,毕竟内置的肯定是经过优化的,并且Perlin Noise本身就有很多的实现,插值方式,列表选择等都会影响其结果。
本文介绍了 Perlin Noise 的原理及用途,并提供了 Unity 中的代码实现。Perlin Noise 常用于游戏开发中生成自然景观,如地形、水、火焰等。
595

被折叠的 条评论
为什么被折叠?



