- 教程
- 文档
- 示例工程
【免费下载链接】website-archive
Archive of the Coding Train website (first version)
Perlin 噪声(Perlin Noise)是创意编程中生成平滑、自然、非随机抖动效果的核心算法,而二维噪声则把这种平滑性从一条曲线扩展到一个平面。本篇文章以 Coding Train(The Coding Train)Perlin 噪声系列第 0.5 节 "2D Noise" 为骨架,结合仓库中可运行的 p5.js 源码,讲解如何在 p5.js 中通过noise(xoff, yoff)逐像素绘制一幅灰度噪声图,并给出inc、xoff、yoff等关键参数的取值规律,以及从 2D 升级到 3D 地形渲染的延伸思路。读完本文,你将能够独立写出「像素级二维噪声纹理」,并理解它为什么是云雾、地形、流体等效果的基础。
本讲在系列中的位置:从一维噪声到二维噪声
0.5-2d-noise.md是仓库 Perlin 噪声学习系列 的第 5 讲(编号 0.5),该系列还包括:
- 0.2 引言:Perlin 噪声是什么、如何产生以及历史背景;
- 0.3 noise() 与 random() 对比:说明 p5.js 中两个「返回随机数」的函数本质差异;
- 0.4 一维噪声作图:把一维噪声值画成平滑曲线并与纯随机对比;
- 0.5 二维噪声(本文主题):把一维噪声拓展到二维平面,逐像素生成噪声纹理。
理解 2D 噪声的关键在于先理解 1D:一维噪声相当于沿着一条直线(x 轴方向)连续采样noise()的值,得到的曲线是平滑波动的;二维噪声则是在一个平面(x-y 网格)上连续采样,每个采样点都拥有「相邻点相似」的特性,从而形成连续渐变的空间纹理。
配套的官方示例代码位于 learning/noise/0.5-2d-noise/P5/,含 sketch.js 与 index.html;仓库的旧版(ES5 语法)对应文件在 Tutorials/perlin_noise/04_perlin_noise_2D/。另外,0.5-2d-noise.md的 front matter 中还记录了本讲的 p5.js Web Editor 草图 id:2_hBcOBrF(即线上可运行版本)。
核心思路:用 xoff 与 yoff 遍历整个像素平面
二维噪声相比一维噪声,最大的变化是采样坐标从「一个递增的游标」变成「一组双游标」:
xoff(x-offset):沿画布 x 方向移动的噪声采样偏移量,控制噪声在水平方向的变化快慢;yoff(y-offset):沿画布 y 方向移动的噪声采样偏移量,控制噪声在垂直方向的变化快慢。
绘制策略是:外层循环逐行扫描 y,内层循环逐列扫描 x,对每个像素调用noise(xoff, yoff)得到 0~1 之间的平滑值,再乘以 255 映射为灰度写入像素数组。每扫描完一列,xoff增加inc;每完成一行,yoff也增加inc。这样 x-y 平面上每个相邻像素对应的噪声采样点都「相邻」,于是灰度是连续渐变的,而不是 random() 那种杂乱跳变。
这就是本讲最核心的算法骨架:双层循环 + 双偏移游标 + 像素数组写入。
逐行解读官方示例源码
以下为 learning/noise/0.5-2d-noise/P5/sketch.js 的完整 ES6 版本(该版本较旧版做了语法升级,逻辑一致):
// 2D Noise // The Coding Train / Daniel Shiffman // This example has been updated to use es6 syntax. let inc = 0.01; function setup() { createCanvas(200, 200); pixelDensity(1); } function draw() { let yoff = 0; loadPixels(); for (let y = 0; y < height; y++) { let xoff = 0; for (let x = 0; x < width; x++) { let index = (x + y * width) * 4; // let r = random(255); let r = noise(xoff, yoff) * 255; pixels[index + 0] = r; pixels[index + 1] = r; pixels[index + 2] = r; pixels[index + 3] = 255; xoff += inc; } yoff += inc; } updatePixels(); //noLoop(); }关键点逐行拆解:
let inc = 0.01;——噪声增量,是整个效果的「旋钮」。inc越小,相邻采样点越近,噪声越平滑、细节越少(更像大片云层);inc越大,噪声越细碎、颗粒感越强。官方示例取0.01,在 200×200 画布上意味着整个采样区间只有约 2 个单位(200 × 0.01),整体非常平滑。createCanvas(200, 200); pixelDensity(1);——创建 200×200 的画布,并把像素密度强制设为 1。pixelDensity(1)在这里很重要:若显示器为高分屏(Retina 等),p5.js 默认像素密度可能大于 1,导致pixels数组长度与width * height不匹配、出现采样错位;设为 1 可保证「每个像素恰好对应一个噪声采样点」。loadPixels()/updatePixels()——p5.js 像素操作的固定配对。loadPixels()把画布像素读入全局pixels数组,updatePixels()在修改完毕后把数组写回画布。像素索引公式
let index = (x + y * width) * 4;——p5.js 的pixels数组按 RGBA 排列,每个像素占 4 个连续元素:index+0为 R(红)、index+1为 G(绿)、index+2为 B(蓝)、index+3为 A(透明度)。let r = noise(xoff, yoff) * 255;——二维噪声的核心调用。noise()返回 0~1 之间的平滑值,乘 255 映射到 0~255 的灰度区间。将同一个r同时赋给 R、G、B 三分量,即得到灰度图(pixels[index + 3] = 255保证完全不透明)。游标推进——内层循环每处理一个像素
xoff += inc,外层循环每换一行yoff += inc。注意两个方向使用相同的inc,因此噪声在 x、y 两个方向上的「缩放」一致;若想让水平/垂直方向细节密度不同,可分别使用incX与incY(例如地形示例中xoff += 0.2、yoff += 0.2)。被注释掉的
// let r = random(255);——官方刻意保留这行注释,用于对比:换成random(255)后每个像素的灰度完全随机、相邻像素毫无关联,画面变成雪花噪点;而noise()版本则是连续渐变纹理。这正是系列第 0.3 讲 noise() 与 random() 对比 在二维场景下的直观印证。被注释掉的
//noLoop();——若取消注释,draw()只执行一次,得到一帧静态噪声图;保留注释则每一帧重绘。由于yoff每帧都从 0 重新开始(没有像一维作图那样维护全局start),画面本身是静止的。
运行入口 index.html 通过 CDN 引入p5.min.js(仓库内为p5@1.4.1)后加载sketch.js,直接在浏览器打开即可看到 200×200 的灰度噪声纹理。
仓库内新旧版本对照
仓库中保存了本讲的两个历史版本,适合对照学习:
| 版本 | 位置 | 语法 | 差异要点 |
|---|---|---|---|
| 新版本 | learning/noise/0.5-2d-noise/P5/sketch.js | ES6(let/const) | 注释更丰富,含//noLoop()说明 |
| 旧版本 | Tutorials/perlin_noise/04_perlin_noise_2D/sketch.js | ES5(var) | 逻辑相同,是 2016 年视频录制时的原始形态 |
旧版 HTML(Tutorials/perlin_noise/04_perlin_noise_2D/index.html)还额外引入了p5.sound.min.js音频插件,与噪声绘制无关,仅说明当时教程的运行环境。两版核心算法完全一致,从源码结构可以推断:该讲重点始终是「用双层循环把二维噪声写进像素数组」这一个模式。
关键参数调优:inc、画布尺寸与灰度映射
二维噪声的效果由三个因素决定,官方示例均给出明确取值:
inc = 0.01(官方默认):采样增量。建议尝试区间 0.005 ~ 0.05。经验规律:inc越小 → 采样越密集、过渡越平滑 → 大块「云团」式纹理;inc越大 → 采样越稀疏、细节越丰富 → 细碎「沙粒」式纹理;- 若
inc过大(如 > 0.1),相邻像素噪声值几乎无关,视觉上退化为随机噪点,失去 Perlin 噪声的意义。
- 画布尺寸 200×200:官方选择小画布,一是便于观察全貌,二是在
inc = 0.01下采样区间约为[0, 2],恰好落在噪声函数平滑变化的核心区间内。若改为 600×600,建议同步调小inc(如 0.005),以保持「整幅画面只有少数几片大云层」的观感。 - 灰度映射
noise(...) * 255:noise()输出 0~1,乘以 255 得到 0~255 灰度。若想观察更极端的明暗对比,可用map(noise(xoff, yoff), 0, 1, 50, 200)压缩灰度范围;若想得到彩色纹理,可分别用三个不同偏移(如noise(xoff, yoff)、noise(xoff + 100, yoff)、noise(xoff, yoff + 100))赋给 R/G/B。
延伸:从 2D 噪声到 3D 地形(仓库源码佐证)
二维噪声最著名的进阶应用是「Perlin 噪声地形」,仓库中就有完整的可运行实现:CodingChallenges/CC_011_PerlinNoiseTerrain/P5/sketch.js。该示例与本讲一脉相承,把二维噪声从「像素平面」升级为「顶点高度场」:
var yoff = flying; // 第三维:随时间变化的起点 for (var y = 0; y < rows; y++) { var xoff = 0; for (var x = 0; x < cols; x++) { terrain[x][y] = map(noise(xoff, yoff), 0, 1, -100, 100); xoff += 0.2; } yoff += 0.2; }对照本讲可以清楚看到三个升级点:
- 同样的双游标双层循环:
xoff每列递增、yoff每行递增,采样结构与 2D 噪声完全一致; - 用
map()而非* 255:把噪声值 0~1 映射到 -100~100,作为地形顶点高度; - 第三维动画:
yoff不再每帧从 0 开始,而是以全局变量flying为起点并每帧flying -= 0.1,于是整块噪声场沿 y 方向「流动」,形成飞越地形的效果——这就是「3D 噪声 = 2D 噪声切片随时间推移」的直观演示。
反过来,如果在 0.5 讲中取消//noLoop()注释并让yoff每帧偏移,同样能让 2D 噪声纹理动起来,得到类似流云的效果。系列其他相关实现还包括 CC_036_Blobby、CC_060_Butterfly_Wings、CC_103_Flames 等(均可在仓库中检索到noise(xoff, yoff)的同类用法),可作为进一步研究二维/三维噪声的素材。
常见问题排查
结合源码可以归纳本讲最易出现的三个问题:
- 画面出现条纹或错位:通常是没有调用
pixelDensity(1),高分屏下pixels数组长度超过width * height,导致采样索引错位。解决方案即官方写法pixelDensity(1)。 - 忘了
loadPixels()/updatePixels():直接读写pixels数组而不配对调用这两个函数,画面不会更新或直接报错。 - 看起来像随机噪点:
inc取值过大导致相邻采样点失去连续性。回到inc = 0.01附近,并检查是否误把noise()写成了random()(官方注释中特意保留了random(255)的对比行)。
小结
0.5-2d-noise.md这一讲的技术要点可以概括为一句话:用noise(xoff, yoff)配合双层循环,把二维 Perlin 噪声逐像素写入pixels数组,即可得到平滑渐变的灰度纹理。核心参数inc控制平滑度与细节密度,pixelDensity(1)保证像素映射正确,xoff/yoff双游标则是从一维到二维的关键跳跃。掌握这一模式后,向上可扩展出 3D 地形(见 CC_011),向旁可延伸出流动纹理与彩色噪声,是进入 Perlin 噪声创意编程的重要一步。完整代码与运行入口请查看 learning/noise/0.5-2d-noise/P5/。
- 教程
- 文档
- 示例工程
【免费下载链接】website-archive
Archive of the Coding Train website (first version)
相关推荐
用 Perlin 噪声打造二维流场:Coding Challenge 24 的 p5.js 与 Processing 完整实现
用 Perlin 噪声打造二维流场:Coding Challenge 24 的 p5.js 与 Processing 完整实现 本篇技术指南围绕 The Cod
教程文档示例工程OpenSimplex2:二维至四维的噪声生成库
OpenSimplex2:二维至四维的噪声生成库 基础介绍 OpenSimplex2 是一个开源的噪声生成库,由 KdotJPG 开发,旨在提供一种改进的 Si
探索OpenSimplex2:下一代二维与三维噪声库
探索OpenSimplex2:下一代二维与三维噪声库 是一个开源的、跨平台的噪音生成库,由K. Jason GS(KdotJPG)开发。如果你是游戏开发者、图形
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考