为何我的Perlin Noise算法在网格单元间无法平滑过渡?
Pygame复现Perlin Noise的平滑过渡问题排查
问题背景
我尝试用Python的Pygame模块复现Perlin Noise,具体实现步骤如下:
- 窗口尺寸设为(1600, 1200),划分为40×40像素的网格单元(共40列30行),每个主单元再拆分为10×10像素的子单元,每个主单元包含16个子单元
- 为每个网格单元的角点分配随机梯度向量
- 遍历每个网格单元及内部子单元,计算子单元与所在主单元四个角点的偏移向量,再求偏移向量与对应角点梯度向量的点积
- 采用SmoothStep函数执行双线性插值:先对顶部、底部两个角点的点积分别线性插值,再对这两个结果插值得到最终噪声值
- 重复上述步骤,将噪声值映射为灰度值
当前遇到的问题:网格单元间颜色仍呈现随机性,相邻单元的子单元缺乏关联,无法实现平滑过渡。

核心代码片段
def CreateGradientVec(): theta = uniform(0, 2 * pi) gradient_vec = pygame.Vector2(cos(theta), sin(theta)) #Theta chooses a random angle from 0-360 degrees, since sin^2(theta) + cos^2(theta) = 1, so sin(theta) + cos(theta) = 1 #and the gradient vec will be a unit vector in a random resultant direction return gradient_vec def SmoothStep(t): #Formula for smoothstep function return 3*(t**2) - 2*(t**3) def BilinearInterpolation(d1, d2, d3, d4, point): point_normalized = point / CELL_SIZE #NORMALISING CANDIDATE POINT TO BE USED AS INPUT FOR SMOOTHSTEP #point_normalized *= 0.01 #Bilinear Interpolation Formula return lerp( lerp(d1, d2, SmoothStep(point_normalized.x)), lerp(d3, d4, SmoothStep(point_normalized.x)), SmoothStep(point_normalized.y)) def PerlinNoise(grid_cells, grid_map): noise_values = [] for cell in grid_cells: cell_topleft = cell.GetPos() cell_noise_values = [] #Finds all 4 corners of a cell, as well as the corresponding gradient vector of each corner corners = [(Vector2(cell_topleft), grid_map[cell_topleft]), (Vector2(cell_topleft[0] + CELL_SIZE, cell_topleft[1]), grid_map[(cell_topleft[0] + CELL_SIZE, cell_topleft[1])]), (Vector2(cell_topleft[0], cell_topleft[1] + CELL_SIZE), grid_map[(cell_topleft[0], cell_topleft[1] + CELL_SIZE)]), (Vector2(cell_topleft[0] + CELL_SIZE, cell_topleft[1] + CELL_SIZE), grid_map[(cell_topleft[0] + CELL_SIZE, cell_topleft[1] + CELL_SIZE)]) ] for subcell in cell.GetSubCells(): dot_values = [] subcell_pos = Vector2(subcell.GetPos()) for i in range(4): corner_pos = corners[i][0] gradient_vec = corners[i][1] offset_vec = (subcell_pos - corner_pos) / CELL_SIZE # Normalises offset vec so both gradient and offset vec are unit vectors dot_values.append(offset_vec.dot(gradient_vec)) noise_value = BilinearInterpolation(dot_values[0], dot_values[1], dot_values[2], dot_values[3], subcell_pos) noise_value = (noise_value + 1.5) / 3 #Transforms noise values, so they are not negative and greater than 1 cell_noise_values.append(noise_value) #Creates a 2D list, where each sub-list contains the noise values for all sub-cells in a single cell noise_values.append(cell_noise_values) return noise_values
问题根源与修复方案
1. 坐标归一化错误(核心问题)
BilinearInterpolation函数中,point_normalized = point / CELL_SIZE使用了子单元的全局坐标,而非其在当前主单元内的相对坐标。比如某个子单元在主单元内的位置是(10,10),全局坐标可能是(40,40),直接除以CELL_SIZE(40)得到(1,1),但实际单元内相对坐标应该是(0.25,0.25),这会导致SmoothStep的输入完全错误,插值逻辑失效。
修复代码:
def BilinearInterpolation(d1, d2, d3, d4, point, cell_topleft): # 计算子单元在当前主单元内的相对坐标 relative_pos = point - cell_topleft # 归一化到[0,1]范围 point_normalized = relative_pos / CELL_SIZE # 双线性插值:先x方向,再y方向 lerp_x_top = lerp(d1, d2, SmoothStep(point_normalized.x)) lerp_x_bottom = lerp(d3, d4, SmoothStep(point_normalized.x)) return lerp(lerp_x_top, lerp_x_bottom, SmoothStep(point_normalized.y))
2. 插值函数调用参数缺失
在PerlinNoise函数中调用BilinearInterpolation时,需要传入主单元的左上角坐标,确保相对坐标计算正确:
# 替换原有的noise_value计算行 noise_value = BilinearInterpolation(dot_values[0], dot_values[1], dot_values[2], dot_values[3], subcell_pos, cell_topleft)
3. 梯度向量共享验证
需确保grid_map中存储的角点梯度是全局唯一的——相邻主单元共享的角点(如单元A的右上角和单元B的左上角)必须使用同一个梯度向量,不能重复生成。如果grid_map初始化逻辑有误,会直接破坏相邻单元的平滑过渡。
4. 噪声值映射优化
Perlin Noise的理论输出范围是[-√2, √2]≈[-1.414,1.414],原映射公式(noise_value + 1.5) / 3存在偏移,建议改为:
noise_value = (noise_value + 1.414) / 2.828
这样可以将值准确映射到[0,1]区间,避免灰度值异常。
内容的提问来源于stack exchange,提问作者wert01




