以前很好奇,为什么我的世界游戏本身就那么小,但是你却可以根据一个种子得到一个固定的地图,总不可能作者把每个种子都手动生成了地图然后保存吧,但是我随机输入一个种子都可以,那工作量也太大了,而且跟游戏大小相悖,后来了解到他可能就是使用了柏林噪声加随机种子生成的。
下面是从白噪声到柏林噪声的过程:
每个像素点完全随机生成的噪声就是白噪声
import matplotlib.pyplot as plt
import numpy as np
pic = np.random.randint(0, 255, [256, 256])
plt.imshow(pic, cmap='gray')

这种噪声形成的图像色块之间的过渡十分生硬,一个办法就是将图像分块,只对每块的四个角随机生成灰度值,而块中间部分则使用双线性插值法(距离加权均值)填充灰度值
import matplotlib.pyplot as plt
import cv2
import numpy as np
pic = np.random.randint(0, 255, [8, 8], dtype=np.uint8)
pic = cv2.resize(pic, dsize=[256, 256], interpolation=cv2.INTER_LINEAR) # cv2.INTER_LINEAR表示使用双线性插值的方式扩展
plt.imshow(pic, cmap='gray')

但是这种噪声看起来仍然非常生硬,特别是里面的高光部分,很规整,于是就想办法改进。
之前的做法就是在四个角生成一个随机数作为灰度值,然后在中间插入均值,这也是为什么这种噪声晶格感严重的原因,因为在单个晶格中,所有灰度值变化的梯度方向是相同的,就好像在这个格子中拉了一条渐变线一样。
解决的办法也是让各个灰度值梯度方向不同,现在,在每个顶点上随机生成一个向量表示颜色梯度方向。
可以将这4个颜色梯度方向的模长都设定为1,在计算晶格中间某个像素点的灰度值时,将P和Q两个向量(从同一个顶点出发的两个向量)计算内积,四个顶点都计算完后再累加就是该点的灰度值了,其中Q表示四个顶点到目标点的向量。
如此一来,噪声就不会像之前那样有很直的横线或者竖线

但晶格感还是很严重,所以真正给柏林噪声注入灵魂的是对原始坐标进行变换的“激活函数”(缓动曲线),它的作用就是让晶格的坐标点过渡更自然(见下方代码)

完整的柏林噪声代码:
import numpy as np
import cv2
import matplotlib.pyplot as plt
def interpolant(t):
"""
缓动曲线
"""
return t*t*t*(t*(t*6 - 15) + 10)
def generate_perlin_noise_2d(
shape, res, tileable=(False, False), interpolant=interpolant
):
delta = (res[0] / shape[0], res[1] / shape[1]) # 8 / 256
d = (shape[0] // res[0], shape[1] // res[1])
grid = np.mgrid[0:res[0]:delta[0], 0:res[1]:delta[1]].transpose(1, 2, 0) % 1 # 只取小数部分,生成每个像素点坐标特征量
# Gradients
angles = 2*np.pi*np.random.rand(res[0]+1, res[1]+1)
gradients = np.dstack((np.cos(angles), np.sin(angles)))
if tileable[0]:
gradients[-1,:] = gradients[0,:]
if tileable[1]:
gradients[:,-1] = gradients[:,0]
gradients = gradients.repeat(d[0], 0).repeat(d[1], 1)
g00 = gradients[ :-d[0], :-d[1]]
g10 = gradients[d[0]: , :-d[1]]
g01 = gradients[ :-d[0],d[1]: ]
g11 = gradients[d[0]: ,d[1]: ]
# Ramps
n00 = np.sum(np.dstack((grid[:,:,0] , grid[:,:,1] )) * g00, 2)
n10 = np.sum(np.dstack((grid[:,:,0]-1, grid[:,:,1] )) * g10, 2)
n01 = np.sum(np.dstack((grid[:,:,0] , grid[:,:,1]-1)) * g01, 2)
n11 = np.sum(np.dstack((grid[:,:,0]-1, grid[:,:,1]-1)) * g11, 2)
# Interpolation
t = interpolant(grid) # 使用缓动曲线映射每个像素点坐标(不是真的坐标,只是坐标的一个特征量)
n0 = n00*(1-t[:,:,0]) + t[:,:,0]*n10
n1 = n01*(1-t[:,:,0]) + t[:,:,0]*n11
return np.sqrt(2)*((1-t[:,:,1])*n0 + t[:,:,1]*n1)
pic = generate_perlin_noise_2d((256, 256), [8,8])
plt.imshow(pic,cmap='gray')
其中,缓动曲线的图形为(也可以选择其他的缓动曲线):

这是2D柏林噪声的生成过程,像我的世界地图、游戏中的火焰效果等都可以使用3D柏林噪声来生成。3D柏林噪声也只是在2D的基础上多加了一个维度而已。