跳到主要内容

Noise 噪点

给画面加随机扰动,做胶片颗粒、老电视雪花、磨砂质感。

node.cache();
node.filters([Konva.Filters.Noise]);
node.noise(0.4); // 默认 0.2

扰动是对称的​

源码里的计算是:

const amount = this.noise() * 255;
const half = amount / 2;
data[i] += half - 2 * half * Math.random();

也就是每个通道加上一个 [-half, +half] 区间内的随机值。 noise(0.4) 对应 half = 51,扰动范围是 ±51。

因为是对称的,画面的平均亮度不变,只是方差变大。 这和"加噪让图变亮/变暗"的直觉不同——噪点只影响层次,不影响曝光。

三个颜色通道各自独立取随机数,所以噪点是彩色的, 不是灰度的颗粒。想要灰度噪点,得自己写 自定义滤镜,让三个通道共用同一个随机值。

alpha 通道不受影响(步长是 4,跳过了第四个字节)。

每次运行都会重新随机​

Math.random() 在滤镜函数里调用,意味着每次滤镜执行都会得到一组新的噪点。

静态场景下这没关系——滤镜只在 cache() 之后跑一次。 但在动画里,如果每帧都触发滤镜重跑,噪点会每帧都变, 画面看起来在剧烈闪烁。

这有时正是你要的(老电视雪花效果),有时则是个 bug (你只想要静态的胶片颗粒,结果它在抖)。区别在于你是否在每帧改属性。

想要静态颗粒:cache() 一次、设一次 noise(),之后别再碰它。

想要动态雪花:在动画里反复设置 noise() 触发重算, 或者干脆预生成几帧噪点贴图循环播放(性能好得多,见下)。

常见问题​

noise 的取值范围是多少?​

默认 0.2,没有硬性上限。实用范围:

  • 0.05 以下——很轻微,适合给纯色区域加一点质感避免色带
  • 0.2(默认)——明显但不喧宾夺主
  • 0.5 以上——画面开始被噪点主导
  • 1.0——扰动范围 ±127,原图基本被淹没

为什么加了噪点图片看起来发灰?​

不是发灰,是对比度的感知下降。噪点把原本平滑的区域打散, 边缘和细节被随机值淹没,视觉上就"糊"了。

如果想要颗粒感但不损失锐度,把 noise 调小(0.1 以下), 再配合 Contrast 补回一点对比度。

能只给暗部加噪点吗?​

内置滤镜不行,它对所有像素一视同仁。

真实胶片的颗粒在中间调最明显、在纯黑和纯白处最弱。 要模拟这个特性需要自定义滤镜,按像素亮度调制噪点强度:

function FilmGrain(imageData) {
const d = imageData.data;
for (let i = 0; i < d.length; i += 4) {
const lum = (d[i] + d[i + 1] + d[i + 2]) / 765; // 0 ~ 1
const weight = 4 * lum * (1 - lum); // 中间调最大
const n = (Math.random() - 0.5) * 60 * weight;
d[i] += n; d[i + 1] += n; d[i + 2] += n; // 共用随机值 = 灰度噪点
}
}

性能提示​

Noise 每个像素要调用三次 Math.random(), 这是它比其他逐像素滤镜贵的原因——随机数生成的成本远高于一次乘法。 一张 1000×1000 的图在 2x 缓存下是一千二百万次 Math.random() 调用。

因此有几条针对性的优化:

共用一个随机值。 如果不需要彩色噪点,三个通道用同一个随机数, 调用次数立刻降到三分之一,而且效果更接近真实胶片颗粒。

别在动画里重跑噪点滤镜。 这是最贵的用法:每帧一千多万次随机数生成。 需要动态噪点时,正确做法是预生成几张噪点贴图 (比如 5 张 256×256 的噪点 PNG), 用一个 Konva.Image 以 globalCompositeOperation: 'overlay' 叠在上层循环播放。 成本从"每帧一千万次随机"降到"每帧一次贴图绘制",差着几个数量级。

降低缓存分辨率。 噪点本身就是随机的, cache({ pixelRatio: 1 }) 造成的画质损失完全看不出来, 但工作量直接降到四分之一。这个滤镜是降分辨率收益最大的之一。