SVG 滤镜里有一组效果,刚接触的人往往觉得它“玄学”——feTurbulence就是其中最典型的一个。你写下一行标签,屏幕上就冒出一片云雾、木纹、大理石或者水面波纹,参数稍微动一动,纹理的疏密、走向、颗粒感全变了,但你说不清它到底按什么规律在变。我第一次用它的时候,纯粹是瞎调baseFrequency和numOctaves,调到看着顺眼就收手,结果换一个尺寸的画布,纹理立刻崩掉,完全没法复用。
这篇内容就是把我这些年折腾feTurbulence的经验摊开讲清楚。它适合已经会用 SVG 基本图形、想进一步做纹理和质感的前端开发者,也适合做视觉设计、想把噪声纹理做成可参数化组件的朋友。核心关键词是feTurbulence、湍流滤镜、噪声纹理、baseFrequency、numOctaves、feDisplacementMap。读完之后你应该能做到:不再靠试参数碰运气,而是知道每个参数在控制什么,能稳定地做出云、雾、木纹、水面、磨砂玻璃这几类常见效果,并且知道它在性能和兼容性上有哪些坑。
1. 先搞清楚 feTurbulence 到底在生成什么
很多人把feTurbulence当成一个“纹理生成器”,这个理解不算错,但太模糊,导致调参时没有方向。更准确的说法是:它在为画布上的每一个像素计算一个伪随机的数值,然后把这个数值映射成颜色。你看到的云雾和木纹,本质上是“一张由算法算出来的噪声图”。
1.1 从“每个像素一个随机数”说起
如果只是给每个像素一个完全独立的随机数,你得到的是电视雪花那种纯白噪声,颗粒感极强,没有任何结构。feTurbulence高明的地方在于,它生成的随机数是有空间相关性的——相邻像素的数值彼此接近,距离越远差异越大。这种“平滑过渡的随机”就是所谓的噪声(noise),它才是云雾、木纹这些自然纹理的数学基础。
理解这一点非常关键:你调的所有参数,本质上都在控制“相邻像素之间的相关性有多强、在几个尺度上叠加”。相关性强的区域看起来就是大块的云,相关性弱的区域看起来就是细密的沙。所以后面讲baseFrequency时,你会发现它控制的就是这个相关性的尺度。
1.2 两种噪声类型:fractalNoise 与 turbulence
feTurbulence的type属性只有两个取值,但它们的视觉差异非常大,这是新手最容易忽略的一个分叉点。
| type 取值 | 数学特征 | 视觉表现 | 典型用途 |
|---|---|---|---|
fractalNoise | 多个倍频噪声直接叠加,值域集中在中间 | 柔和、连续、对比度低 | 云雾、烟雾、磨砂质感、纸张纹理 |
turbulence | 叠加时对绝对值做处理,产生尖锐的“脊线” | 对比强烈、有沟壑感、边缘锐利 | 木纹、大理石、火焰、水面波纹、闪电 |
我一开始默认用turbulence,做云雾效果怎么调都显得脏、显得硬,后来换成fractalNoise立刻就柔和了。反过来做木纹时用fractalNoise,纹理糊成一团没有年轮的锐利边界。这个选择基本决定了你后续调参的方向,选错了再怎么调都是事倍功半。
提示:如果你不确定该用哪个,先做云雾类效果就用
fractalNoise,做有“纹路边界”的效果就用turbulence,这个经验判断在绝大多数场景下都成立。
1.3 噪声图是怎么变成颜色的
feTurbulence输出的其实是一个 RGBA 四通道的图像,每个通道都是独立的噪声值。默认情况下,R、G、B 三个通道的噪声是各自独立计算的,所以直接看输出会是一张彩色的噪点图。这也是为什么很多人第一次单独用feTurbulence时,看到的是花花绿绿的杂色,而不是想象中的灰度纹理。
要把它变成可用的灰度纹理,通常要配合feColorMatrix把彩色压成灰度,或者用feComponentTransfer调整通道。这个组合套路后面会专门讲。先记住一个结论:feTurbulence单独用几乎没意义,它永远是滤镜链里的一环,前后通常要接颜色处理或位移处理。
2. baseFrequency:控制纹理“颗粒有多大”
如果只能记住一个参数,那就是baseFrequency。它决定了噪声的基础频率,频率越高,单位距离内变化越快,纹理越细密;频率越低,纹理越舒展、块面越大。这是调参时第一个要动的旋钮。
2.1 单值和双值的区别
baseFrequency可以写一个值,也可以写两个值,用空格分隔。这个细节很多人没注意,但它直接决定了纹理是不是“各向同性”。
- 写一个值,比如
baseFrequency="0.05":X 和 Y 方向频率相同,纹理在各个方向均匀分布,适合云雾、磨砂这类无方向性的效果。 - 写两个值,比如
baseFrequency="0.01 0.2":X 方向频率低、Y 方向频率高,纹理在水平方向被拉长,形成横向的条纹感。做木纹、拉丝金属、水波纹时,这个双值写法是核心技巧。
我做过一个横向木纹的效果,一开始用单值怎么调都是均匀颗粒,后来把 X 设成 0.008、Y 设成 0.15,纹理立刻被拉成横向的细长纹路,再配合turbulence类型的脊线,年轮的感觉就出来了。这个“方向性靠双值频率制造”的思路,是feTurbulence里最实用的技巧之一。
2.2 频率和画布尺寸的耦合关系
这里有个大坑,我踩过不止一次:baseFrequency是相对于滤镜坐标系的,而滤镜默认使用objectBoundingBox作为primitiveUnits的参考。这意味着同一个频率值,作用在 100×100 的小图上和 1000×1000 的大图上,视觉颗粒大小完全不同——大图上的纹理会被“放大”,看起来更粗。
解决办法有两个,我一般按场景选:
- 把
filterUnits和primitiveUnits都设成userSpaceOnUse,这样频率就对应绝对像素坐标,纹理密度和画布尺寸解耦,换尺寸不会崩。 - 保持默认的
objectBoundingBox,但把频率值按画布尺寸做换算,比如目标是在 800px 宽上得到合适颗粒,就按比例调整。
<filter id="grain" filterUnits="userSpaceOnUse" x="0" y="0" width="800" height="800"> <feTurbulence type="fractalNoise" baseFrequency="0.9" numOctaves="3" result="noise"/> </filter>上面这段用userSpaceOnUse后,baseFrequency="0.9"就稳定对应“每像素级别的高频颗粒”,做磨砂质感时非常好复用。这个设置是我现在做任何纹理组件的默认起点。
2.3 频率取值的经验区间
调了这么多效果,我大致总结出一个频率区间参考,能帮你少走很多弯路:
| 效果目标 | baseFrequency 参考 | type 建议 |
|---|---|---|
| 大块云雾、烟雾 | 0.005 ~ 0.02 | fractalNoise |
| 中等云层、纸张纹理 | 0.02 ~ 0.08 | fractalNoise |
| 木纹、大理石纹路 | 0.005 ~ 0.03(配合双值) | turbulence |
| 水面波纹 | 0.01 ~ 0.05(双值拉方向) | turbulence |
| 细密磨砂、颗粒噪点 | 0.5 ~ 1.2 | fractalNoise |
这些数值不是绝对标准,但作为起点非常靠谱。我的习惯是先按目标效果选一个区间中值,再微调,而不是从 0 开始瞎试。
3. numOctaves 与 seed:叠加层数与随机起点
baseFrequency决定了“基础颗粒”,但自然纹理很少是单一尺度的——云有大团也有细边,木纹有粗纹也有细丝。numOctaves就是用来叠加多个尺度的,而seed决定这堆随机数的起点。
3.1 numOctaves 是“倍频叠加”的层数
numOctaves的每个“八度”(octave)会把频率翻倍、振幅减半,然后叠加到结果上。层数越多,纹理的细节越丰富,但同时计算量也线性增长。这个机制和音频里的泛音叠加是一个道理:基频决定音高,泛音决定音色。
实际调参时我的经验是:
numOctaves="1":只有基础层,纹理干净但单调,适合做纯色块上的轻微质感。numOctaves="2"到3":最常用的区间,既有细节又不至于太脏,云雾和木纹的甜点区。numOctaves="4"以上:细节非常丰富,但容易显得“噪”,而且性能开销明显上升,移动端要谨慎。
我做过对比测试,在同一个 800×800 画布上,numOctaves从 1 加到 5,渲染耗时大概翻了三倍多。所以除非你确实需要极细腻的纹理,否则 3 层基本够用。这个“够用就好”的判断,比盲目堆参数重要得多。
3.2 seed 让同一套参数产出不同纹理
seed是个很实用的参数,它改变随机数的起始值,从而在参数完全不变的情况下得到不同的纹理分布。做卡片背景、头像底纹这类需要“同风格但不同图案”的场景时,seed就是你的批量生成开关。
<filter id="cloud-a"> <feTurbulence type="fractalNoise" baseFrequency="0.02" numOctaves="3" seed="7"/> </filter> <filter id="cloud-b"> <feTurbulence type="fractalNoise" baseFrequency="0.02" numOctaves="3" seed="42"/> </filter>两个滤镜参数一模一样,只有seed不同,出来的云纹分布就完全不一样。我一般会准备一组 seed 值(比如 3、7、13、42),配合 CSS 变量切换,就能低成本地做出“每张卡片纹理都不同”的效果,而不需要准备多张图片。
注意:
seed的取值在不同渲染引擎下的具体分布可能有细微差异,所以不要指望“seed=42 在所有浏览器里长得一模一样”。它适合做“同风格随机”,不适合做“像素级精确复现”。
3.3 stitchTiles:平铺时的接缝处理
如果你打算把噪声纹理做成可平铺的背景(比如background-repeat),stitchTiles="stitch"这个属性就派上用场了。它会让噪声在瓦片边界处做特殊处理,使得相邻瓦片拼接时接缝不明显。
默认值是noStitch,平铺时接缝处会有明显的断裂感。做无缝纹理背景时,记得加上stitchTiles="stitch"。不过它也不是万能的,频率特别低、块面特别大的时候,接缝依然可能看得出来,这时候要么提高频率,要么干脆用一张足够大的纹理不做平铺。
4. 把噪声变成可用纹理:颜色与位移的组合拳
前面说过,feTurbulence单独输出是彩色噪点,没法直接用。真正让它发挥价值的,是它和feColorMatrix、feComponentTransfer、feDisplacementMap的组合。这一节讲几套我反复用、稳定可靠的组合套路。
4.1 用 feColorMatrix 压成灰度并调对比
最基础的组合是把彩色噪声转成灰度,并拉开对比度。feColorMatrix的type="saturate"设成 0 就能去色,再用type="matrix"调整对比和亮度。
<filter id="paper"> <feTurbulence type="fractalNoise" baseFrequency="0.04" numOctaves="4" result="noise"/> <feColorMatrix in="noise" type="saturate" values="0" result="gray"/> <feComponentTransfer in="gray"> <feFuncR type="linear" slope="0.6" intercept="0.2"/> <feFuncG type="linear" slope="0.6" intercept="0.2"/> <feFuncB type="linear" slope="0.6" intercept="0.2"/> </feComponentTransfer> </filter>这里feComponentTransfer的slope控制对比度、intercept控制整体亮度偏移。slope大于 1 拉大对比,小于 1 压缩对比。做纸张纹理时我一般用 0.5~0.7 的 slope,让纹理若隐若现,不会喧宾夺主。这个“压低对比”的思路很关键——很多新手做出来的纹理太抢眼,盖过了主体内容,就是因为没做这一步。
4.2 feDisplacementMap:让噪声去“扭曲”别的图形
这是feTurbulence最有价值的用法,也是它从“纹理生成”升级为“质感工具”的关键。feDisplacementMap会读取噪声图的通道值,用它来偏移另一张图的像素位置,从而产生扭曲、波动、毛边效果。
它的核心参数是scale(偏移强度)和xChannelSelector/yChannelSelector(用噪声的哪个通道控制 X/Y 方向偏移)。做水面倒影、旗帜飘动、毛玻璃边缘时,这套组合几乎是标配。
<filter id="water"> <feTurbulence type="turbulence" baseFrequency="0.01 0.06" numOctaves="2" result="noise"/> <feDisplacementMap in="SourceGraphic" in2="noise" scale="18" xChannelSelector="R" yChannelSelector="G"/> </filter>scale="18"表示最大偏移 18 像素,这个值直接决定扭曲的剧烈程度。我调这个参数的经验是:做轻微的水面波动用 5~15,做强烈的扭曲变形用 30 以上,超过 50 基本就面目全非了。baseFrequency的双值写法在这里制造了“横向拉长的波纹”,配合位移就有了水面的感觉。
4.3 组合顺序为什么不能随便换
滤镜链是有顺序的,in和result把各个原语串起来。顺序错了,效果完全不对。比如上面水面那个例子,如果先做feDisplacementMap再生成噪声,位移就没有噪声可读,直接失效。
我总结的通用顺序是:先生成噪声 → 再处理噪声(去色/调对比)→ 最后用噪声去影响目标图形。这个顺序符合数据流的方向,也最好调试。调试时我习惯给每个原语加result命名,这样在浏览器开发者工具里能逐段检查中间输出,定位问题快很多。
5. 五类高频效果的完整参数配方
光讲原理不够,这一节直接给配方。下面每个效果都是我在实际项目里用过、验证过可复用的参数组合,你可以直接抄,再按需微调。
5.1 云雾与烟雾
<filter id="fog" x="-20%" y="-20%" width="140%" height="140%"> <feTurbulence type="fractalNoise" baseFrequency="0.012" numOctaves="4" seed="5" result="noise"/> <feColorMatrix in="noise" type="saturate" values="0"/> <feComponentTransfer> <feFuncA type="linear" slope="0.7" intercept="0"/> </feComponentTransfer> </filter>要点是低频率(0.01 左右)加fractalNoise,再通过feFuncA控制透明度,让云雾有浓淡层次。x/y/width/height扩大滤镜区域是为了避免云雾被裁切——这是新手常犯的错,滤镜默认区域只有图形包围盒,云雾超出部分会被切掉。
5.2 木纹
<filter id="wood"> <feTurbulence type="turbulence" baseFrequency="0.006 0.14" numOctaves="3" seed="11" result="noise"/> <feColorMatrix in="noise" type="saturate" values="0"/> <feComponentTransfer> <feFuncR type="linear" slope="1.4" intercept="-0.2"/> <feFuncG type="linear" slope="1.4" intercept="-0.2"/> <feFuncB type="linear" slope="1.4" intercept="-0.2"/> </feComponentTransfer> </filter>双值频率0.006 0.14是灵魂,X 方向极低、Y 方向较高,纹理被拉成横向细纹。turbulence类型提供锐利的年轮边界,slope拉到 1.4 增强对比让纹路更清晰。
5.3 大理石
大理石和木纹思路类似,但频率更均匀、对比更柔和,且常配合位移做出“脉络”感。我一般用fractalNoise打底,再用一层turbulence做细脉络叠加,两层不同 seed 混合,出来的纹路更自然。
5.4 水面波纹
前面 4.2 已经给了核心配方,补充一点:做静态水面时scale小一点(8~12),做动态波动时可以配合 SMIL 或 CSS 动画去改baseFrequency或seed,但要注意改seed会导致纹理跳变,动画不连续,所以动态效果更适合改baseFrequency或直接对feDisplacementMap的scale做动画。
5.5 磨砂玻璃
磨砂玻璃的关键是“高频细颗粒 + 轻微位移”。用baseFrequency="0.8"左右的高频fractalNoise,配合feDisplacementMap的scale="2"到4",让背后的内容产生细微模糊扭曲,再叠一层半透明白色,磨砂感就出来了。这个效果在卡片 UI 上很出彩,但性能开销要留意,后面会讲。
6. 性能、兼容与调试:那些文档不会告诉你的坑
feTurbulence是计算密集型滤镜,用不好会直接拖垮页面。这一节讲我踩过的性能和兼容坑,以及一套实用的调试方法。
6.1 性能开销的真实来源
噪声计算是逐像素的,画布越大、numOctaves越高、滤镜区域越大,开销越大。我实测过一个全屏(1920×1080)的feTurbulence滤镜,numOctaves="4"时在某些集成显卡上会掉到 30fps 以下。
降低开销的几个实操手段:
- 把滤镜应用在小尺寸元素上,再用 CSS
transform: scale()放大,比直接在大元素上算噪声便宜得多。 - 控制
numOctaves不超过 3,除非确实需要。 - 用
x/y/width/height精确限定滤镜区域,别用默认的 120% 包围盒去算一大片空白。 - 静态纹理考虑“算一次存成图片”,而不是每次渲染都实时计算。
注意:
feTurbulence的结果在页面滚动、重绘时可能被反复计算,如果发现滚动卡顿,优先怀疑它。
6.2 浏览器差异与降级思路
主流现代浏览器对feTurbulence的支持都不错,但不同引擎在seed的具体分布、stitchTiles的接缝处理上会有细微差异。我的做法是:不追求跨浏览器像素级一致,只保证“风格一致”。如果某个环境完全不支持 SVG 滤镜,就降级成一张预渲染的纹理图片,用@supports或特性检测来切换。
6.3 一套可复现的调试流程
调feTurbulence最怕瞎试。我固定用这套流程:
- 先只保留
feTurbulence,把type、baseFrequency、numOctaves定下来,看灰度噪声形态对不对。 - 加
feColorMatrix去色,确认对比度合适。 - 需要扭曲时再加
feDisplacementMap,从scale="0"慢慢往上加,观察变化。 - 最后调滤镜区域
x/y/width/height,确保不被裁切。
每一步都给result命名,用开发者工具的滤镜检查器逐段看输出。这套流程能让你在几分钟内定位到是哪个参数出了问题,而不是对着最终效果猜。
7. 我个人的几条实战心得
最后分享几条纯经验的东西,都是文档里不会写的。
第一,先定 type 再调频率。很多人一上来就调baseFrequency,但 type 选错了方向就错了。我现在的习惯是先花十秒判断这个效果该用fractalNoise还是turbulence,定下来再动频率,效率高很多。
第二,双值频率是被低估的技巧。大部分教程只讲单值,但木纹、水波、拉丝这些有方向性的纹理,双值频率才是关键。记住“X 低 Y 高是横向纹理,X 高 Y 低是纵向纹理”这个口诀。
第三,滤镜区域一定要显式设置。默认区域会裁掉超出包围盒的纹理,云雾类效果尤其明显。养成写x="-20%" y="-20%" width="140%" height="140%"的习惯,能省掉大量“为什么纹理被切了”的困惑。
第四,静态纹理优先预渲染。如果一个纹理在页面上是固定的,别让它每次渲染都实时算。用一次性的 canvas 或离屏渲染生成图片,性能收益非常明显,尤其是在列表、卡片这种重复出现的场景里。
第五,seed 是批量生成的利器,但别依赖它的精确值。做同风格不同图案时用 seed 切换,但不要写死“seed=42 必须是这个图案”这种假设,跨引擎会翻车。
feTurbulence这个滤镜,入门时觉得它不可控,用熟了会发现它其实是 SVG 里少数能“用代码生成自然质感”的工具。参数不多,但每个都值得吃透。把 type、baseFrequency、numOctaves、seed 这四个搞明白,再配上颜色和位移的组合,你能做出的效果远比想象中多。