根据前文所述,Worley噪声代码实现比较简单,我们着重讲解一下2D噪声的算法,3D、4D实现基本一致,不作详细描述。
一、Worley Noise 2D实现
还是与以前一样,我们分步来实现Worley噪声,根据前文步骤,Worley噪声实现共分六步:
第一步、确定输入点所在的晶胞
这与Perlin噪声确定输入点所在晶格是一样的,代码如下:
int evalCubeX = GetFloor(x);
int evalCubeY = GetFloor(y);
第二步、对该晶胞产生可重复的随机数生成器
在我们的算法里,我们使用了FNV Hash算法,这样可以保证种子的唯一性,然后我们使用之前学习的线性同余算法(LCG random number generator)作为我们随机数生成器。在使用线性同余算法前使用的Hash共两个作用,第一是生成LCG算法的输入值,第二是为下步确定日胞内特征点数量作准备。代码如下:
lastRandom = lcgRandom(hash((uint)(cubeX + seed), (uint)(cubeY)));
numberFeaturePoints = probLookup(lastRandom);
我们使用用晶胞左下角顶点坐标与一上种子作为Hash函数的输入,得到唯一确定的随机值,然后用这个值作为LCG的输入,保证同一个输入点得到同样的输出,然后我们还随机生成了[1,9]个特征点数量。
第三步、确定在该晶胞内生成特征点的数量
numberFeaturePoints = probLookup(lastRandom);
第四步、随机的将3所生成的特征点放置到晶胞中
对每一个特征点,我们求出特征点的坐标值,某个特征点的坐标值就是(featurePointX,featurePointY),代码如下:
lastRandom = lcgRandom(lastRandom);
randomDiffX = (float)lastRandom / 0x100000000;
lastRandom = lcgRandom(lastRandom);
randomDiffY = (float)lastRandom / 0x100000000;
featurePointX = randomDiffX + (double)cubeX;
featurePointY = randomDiffY + (double)cubeY;
第五步、计算输入点到该晶胞内所有特征点的距离最小值
我们并没有保存特征点的坐标,而是直接求出特征点与输入点的距离,并将这个值与我们保存的最小值比较,如果小于之前的最小值,则更新最小值。
tempDistance = EuclidianDistance(x, y, featurePointX, featurePointY);
if (tempDistance < closestDistance)
closestDistance = tempDistance;
第六步、查找并计算输入点到所在晶胞直接相邻晶胞内特征点的距离最小值,并与5中生成的最小值比较,返回最小值
在计算完同一晶胞内的所有特征点距离后,我们还要与输入点直接相邻的晶胞进行比较,这就是嵌套的两个for语句实现的。
for (int i = -1; i < 2; ++i)
for (int j = -1; j < 2; ++j)
{......}
至此,我们就完成了Worley噪声的计算。这是C#实现的方案,在Cg中,我们更是直接一步利用hash函数得到输入点与特征点的距离差值,代码更简洁。生成的Worley噪声如下图所示:
上图的单形噪声
上图分形噪声
二、Worley NOise 3D实现
3D Worley 噪声实现与2D基本一致,不同之处在于:一是3D生成的特征点有三个坐标分量面2D是二个,另一个3D生成需要检查周边的晶胞比2D多,因此for循环多了一个。代码如下:
uint lastRandom, numberFeaturePoints;
Vector3 randomDiff = new Vector3();
Vector3 featurePoint;
int cubeX, cubeY,cubeZ;
double closestDistance = 1000000.0, tempDistance = 0.0;
int evalCubeX = GetFloor(x);
int evalCubeY = GetFloor(y);
int evalCubeZ = GetFloor(z);
for (int i = -1; i < 2; ++i)
for (int j = -1; j < 2; ++j)
{
for (int k = -1; k < 2; ++k)
{
cubeX = evalCubeX + i;
cubeY = evalCubeY + j;
cubeZ = evalCubeZ + k;
lastRandom = lcgRandom(hash((uint)(cubeX + seed), (uint)(cubeY),(uint)(cubeZ)));
numberFeaturePoints = probLookup(lastRandom);
for (uint l = 0; l < numberFeaturePoints; ++l)
{
lastRandom = lcgRandom(lastRandom);
randomDiff.x = (double)lastRandom / 0x100000000;
lastRandom = lcgRandom(lastRandom);
randomDiff.y = (double)lastRandom / 0x100000000;
lastRandom = lcgRandom(lastRandom);
randomDiff.z = (double)lastRandom / 0x100000000;
featurePoint = new Vector3(randomDiff.x + (double)cubeX, randomDiff.y + (double)cubeY,randomDiff.z + (double)cubeZ) ;
tempDistance = EuclidianDistance(new Vector3(x,y,z), featurePoint);
if (tempDistance < closestDistance)
closestDistance = tempDistance;
}
}
}
return closestDistance;
下图是我们将第3维作为时间输入产生的2D平面动画:
基于CPU产生的动画
基于GPU产生的动画
三、Worley Noise 4D实现
4D Worley 噪声实现与3D基本一致,不再赘述。代码如下:
uint lastRandom, numberFeaturePoints;
Vector4 randomDiff = new Vector4();
Vector4 featurePoint;
int cubeX, cubeY, cubeZ,cubeW;
double closestDistance = 1000000.0, tempDistance = 0.0;
int evalCubeX = GetFloor(x);
int evalCubeY = GetFloor(y);
int evalCubeZ = GetFloor(z);
int evalCubeW = GetFloor(w);
for (int i = -1; i < 2; ++i)
for (int j = -1; j < 2; ++j)
{
for (int k = -1; k < 2; ++k)
{
for (int n = -1; n < 2; ++n)
{
cubeX = evalCubeX + i;
cubeY = evalCubeY + j;
cubeZ = evalCubeZ + k;
cubeW = evalCubeW + n;
lastRandom = lcgRandom(hash((uint)(cubeX + seed), (uint)(cubeY), (uint)(cubeZ), (uint)(cubeW)));
numberFeaturePoints = probLookup(lastRandom);
for (uint l = 0; l < numberFeaturePoints; ++l)
{
lastRandom = lcgRandom(lastRandom);
randomDiff.x = (double)lastRandom / 0x100000000;
lastRandom = lcgRandom(lastRandom);
randomDiff.y = (double)lastRandom / 0x100000000;
lastRandom = lcgRandom(lastRandom);
randomDiff.z = (double)lastRandom / 0x100000000;
lastRandom = lcgRandom(lastRandom);
randomDiff.w = (double)lastRandom / 0x100000000;
featurePoint = new Vector4(randomDiff.x + (double)cubeX, randomDiff.y + (double)cubeY, randomDiff.z + (double)cubeZ,randomDiff.w + (double)cubeW);
tempDistance = EuclidianDistance(new Vector4(x, y, z,w), featurePoint);
if (tempDistance < closestDistance)
closestDistance = tempDistance;
}
}
}
}
return closestDistance;
下图是GPU上的4D实现,将第4维作为时间输入产生的3D动画(在cpu上计算时间成本太高,没有做动画)。
四、CPU与GPU实现时的差异
Worley噪声在CPU与GPU上实现时我们采用了不同的方法,CPU实现我们完全按照Worley论文使用的方法设计算法,在GPU上,出于以下两个方面原因,我们变更了原算法的实现:一是GPU不支持变量循环,在第三步中,worley原设计是产生[1,9]个随机特征点,在GPU中我们限定只产生1个特征点,二是在GPU中我们使用了更简单的hash函数,并直接得到输入点与特征点的距离值。虽然实现方式有些许不同,但实现的效果基本一致。
五、代码下载
VS2015平台下C#实现的WorleyNoise:
VS2015_C#_WorleyNoise
Unity2017.1.1f1平台下Cg实现的WorleyNoise
Unity2017.1.1f1_Cg_WorleyNoise
参考文献
1、An In Depth Cell Noise Tutorial
2、Cell (Worley) noise in Unity3D