You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

2D Perlin Noise算法实现出现水平伪影,求修复方案

修复2D Perlin Noise伪影问题

你的代码里有几个核心问题导致了水平线伪影,同时还有细节需要规范:

问题1:每次调用噪声函数都重新生成随机置换表,且种子随时间变化

你在perlinNoise函数里每次都重新创建并打乱置换数组,而且main循环里每次调用都传time(nullptr)当种子。time(nullptr)返回当前秒数,当循环执行到下一秒时,种子突变,导致整行噪声的随机基底完全不同,和上下行断开,出现明显的水平线。

问题2:网格坐标取整方式不规范

用glm::ivec2(point)是向零取整,对于负数坐标会出错,Perlin Noise需要的是每个点所在网格的左下角整数坐标,应该用floor取整。

问题3:置换表索引未做循环处理

当噪声频率足够大时,网格坐标可能超过256,直接用gridCoords.x作为索引会越界,需要用位运算限制在置换表的有效范围内。


修复后的代码

#include "lodepng/lodepng.h"

#include <vector>
#include <numeric>
#include <iostream>
#include <algorithm>
#include <random>
#include <ctime>

#include <glm/glm.hpp>

float lerp(float a, float b, float x)
{
    return a + x * (b - a);
}

float fade(float x)
{
    return x * x * x * (x * (6 * x - 15) + 10);
}

float gradient(glm::vec2 point, unsigned char seed)
{
    glm::vec2 randomVectors[] = {
        { 0.0f, 1.0f },
        { 0.0f, -1.0f },
        { 1.0f, 0.0f },
        { -1.0f, 0.0f }
    };

    return glm::dot(point, randomVectors[seed % 4]);
}

// 修改:接收预先生成的置换表,不再每次生成
float perlinNoise(glm::vec2 point, const std::vector<unsigned int>& permutations)
{
    // 修复:用floor获取网格左下角坐标
    glm::vec2 gridFloored = glm::floor(point);
    glm::ivec2 gridCoords = glm::ivec2(gridFloored);
    glm::vec2 pointCoords = point - gridFloored;
    glm::vec2 distanceCoords = { fade(pointCoords.x), fade(pointCoords.y) };

    // 修复:用位运算限制索引在0-255之间,保证置换表循环
    int gridX = gridCoords.x & 255;
    int gridY = gridCoords.y & 255;

    // 获取四个网格点的置换索引
    unsigned int idx00 = permutations[permutations[gridX] + gridY];
    unsigned int idx10 = permutations[permutations[gridX + 1] + gridY];
    unsigned int idx01 = permutations[permutations[gridX] + gridY + 1];
    unsigned int idx11 = permutations[permutations[gridX + 1] + gridY + 1];

    // 计算四个梯度点的值
    float val00 = gradient(pointCoords, idx00);
    float val10 = gradient(pointCoords - glm::vec2(1.0f, 0.0f), idx10);
    float val01 = gradient(pointCoords - glm::vec2(0.0f, 1.0f), idx01);
    float val11 = gradient(pointCoords - glm::vec2(1.0f, 1.0f), idx11);

    // 双线性插值
    float lerpX1 = lerp(val00, val10, distanceCoords.x);
    float lerpX2 = lerp(val01, val11, distanceCoords.x);
    float finalVal = lerp(lerpX1, lerpX2, distanceCoords.y);

    // 归一化到[0,1]范围
    return (finalVal + 1.0f) / 2.0f;
}

int main()
{
    glm::ivec2 size = { 512, 512 };
    float frequency = 16.0f;

    // 预先生成置换表,只生成一次
    std::vector<unsigned int> permutations(256);
    std::iota(permutations.begin(), permutations.end(), 0);
    std::mt19937 randomEngine(time(nullptr));
    std::shuffle(permutations.begin(), permutations.end(), randomEngine);
    // 扩展置换表,避免越界
    permutations.insert(permutations.end(), permutations.begin(), permutations.end());

    std::vector<unsigned char> noiseImage{};
    noiseImage.reserve(size.x * size.y);

    float scaleX = size.x / frequency;
    float scaleY = size.y / frequency;

    for (int y = 0; y < size.y; y++)
    {
        for (int x = 0; x < size.x; x++)
        {
            glm::vec2 point = { static_cast<float>(x) / scaleX, static_cast<float>(y) / scaleY };
            // 调用预先生成的置换表
            float noiseVal = perlinNoise(point, permutations);
            noiseImage.push_back(static_cast<unsigned char>(std::round(255 * noiseVal)));
        }
    }

    lodepng::encode("noise.png", noiseImage, size.x, size.y, LodePNGColorType::LCT_GREY);
    return 0;
}

关键修改点说明

  1. 置换表预生成:把置换表的创建和打乱移到main函数中,只执行一次,保证所有像素用同一个随机基底,避免因种子突变导致的行中断。
  2. 网格坐标取整:用glm::floor获取正确的网格左下角坐标,适配正负坐标场景。
  3. 索引循环处理:用&255限制网格坐标在0-255范围内,保证置换表的循环访问,避免越界。
  4. 代码可读性优化:拆分插值步骤,让逻辑更清晰。

内容的提问来源于stack exchange,提问作者Roman Leshchuk

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.17 12:22:08