You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

为何我的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

火山引擎 最新活动