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; }
关键修改点说明
- 置换表预生成:把置换表的创建和打乱移到
main函数中,只执行一次,保证所有像素用同一个随机基底,避免因种子突变导致的行中断。 - 网格坐标取整:用
glm::floor获取正确的网格左下角坐标,适配正负坐标场景。 - 索引循环处理:用
&255限制网格坐标在0-255范围内,保证置换表的循环访问,避免越界。 - 代码可读性优化:拆分插值步骤,让逻辑更清晰。
内容的提问来源于stack exchange,提问作者Roman Leshchuk
相关产品推荐
相关产品推荐

