从白噪声到柏林噪声

以前很好奇,为什么我的世界游戏本身就那么小,但是你却可以根据一个种子得到一个固定的地图,总不可能作者把每个种子都手动生成了地图然后保存吧,但是我随机输入一个种子都可以,那工作量也太大了,而且跟游戏大小相悖,后来了解到他可能就是使用了柏林噪声加随机种子生成的。

下面是从白噪声到柏林噪声的过程:

每个像素点完全随机生成的噪声就是白噪声

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的基础上多加了一个维度而已。

Leave a Comment