wandres.dev
FILTROS SVG · El pipeline de primitivas

feTurbulence, feDisplacementMap y feMorphology

El ruido de Perlin dentro de SVG, sus cuatro parámetros, cómo se usa como mapa de desplazamiento, la erosión y dilatación morfológicas, y cuatro texturas completas.

⏱ 20 min

feTurbulence es la única primitiva de SVG que genera contenido en lugar de transformarlo, y produce ruido de Perlin con la implementación fijada en la propia especificación, lo que significa que el mismo seed da el mismo ruido en todos los navegadores. Combinada con feDisplacementMap, que empuja píxeles según los valores de otra imagen, y con feMorphology, que engorda o adelgaza siluetas, cubre casi todo el terreno de la textura procedural en la web.

🎯 Al terminar esta lección sabrás
  • Controlar los cuatro parámetros de feTurbulence y predecir el aspecto del ruido.
  • Distinguir turbulence de fractalNoise y elegir según el efecto.
  • Usar feDisplacementMap con los selectores de canal correctos.
  • Aplicar feMorphology para engordar, adelgazar y crear un contorno.

feTurbulence y sus cuatro parámetros

<feTurbulence type="fractalNoise" baseFrequency="0.02" numOctaves="4" seed="7" />

baseFrequency es la frecuencia espacial del ruido, en ciclos por unidad de usuario. Valores bajos (0,005 a 0,02) dan manchas grandes; valores altos (0,5 a 2) dan grano fino. Acepta dos valores para anisotropía: baseFrequency="0.01 0.4" produce vetas horizontales, que es lo que da aspecto de madera o de agua.

numOctaves es cuántas capas de ruido se suman, cada una al doble de frecuencia y a la mitad de amplitud. Con 1 el ruido es suave; con 5 tiene detalle a todas las escalas. Cada octava duplica el coste, así que 4 o 5 es el techo razonable y 8 es una forma segura de bloquear el hilo principal.

seed inicializa el generador. Cambiarlo da un ruido distinto con las mismas características. La especificación fija el algoritmo del generador pseudoaleatorio, así que el mismo seed produce el mismo ruido en todos los motores. Eso es una garantía real y poco común.

type acepta turbulence (el valor por omisión) y fractalNoise. La diferencia: turbulence suma los valores absolutos de cada octava, lo que produce discontinuidades en las derivadas y da un aspecto nervioso, de humo o de llama. fractalNoise suma los valores con signo, lo que da un ruido suave y equilibrado alrededor del gris medio, adecuado para nubes y para mapas de desplazamiento.

Una advertencia importante: la salida de feTurbulence tiene los cuatro canales con ruido, incluido el alfa. Si la usas directamente verás un patrón de colores translúcidos, no una escala de grises. Para convertirlo en gris hay que desaturar, y para hacerlo opaco hay que forzar el alfa:

<filter id="grano" x="0" y="0" width="100%" height="100%">
  <feTurbulence type="fractalNoise" baseFrequency="0.8" numOctaves="3" seed="2" />
  <feColorMatrix type="matrix" values="0 0 0 0 0.5
                                       0 0 0 0 0.5
                                       0 0 0 0 0.5
                                       0 0 0 1 0" />
</filter>

Esa matriz merece una lectura: las tres primeras filas ponen los canales de color a la constante 0,5 (todo el trabajo lo hace la quinta columna) y la cuarta deja el alfa como estaba. El resultado es un gris uniforme con la transparencia moteada del ruido, que es la forma de usar el ruido como textura de opacidad.

feDisplacementMap

Toma dos entradas: in es la imagen que se va a deformar, in2 es el mapa. Para cada píxel, lee dos canales del mapa y los usa como desplazamiento.

<feDisplacementMap in="SourceGraphic" in2="ruido" scale="20"
                   xChannelSelector="R" yChannelSelector="G" />

La fórmula, con los valores del mapa normalizados a 0 a 1:

P'(x,y) = P( x + scale * (canalX - 0.5), y + scale * (canalY - 0.5) )

El menos 0,5 es lo que centra el desplazamiento: un valor de 0,5 en el mapa significa desplazamiento nulo, valores menores empujan en un sentido y mayores en el otro. Por eso fractalNoise, que se centra en 0,5, es el tipo correcto para un mapa de desplazamiento, y turbulence, que se acumula hacia arriba, produce un desplazamiento sesgado.

xChannelSelector e yChannelSelector aceptan R, G, B y A. El valor por omisión de ambos es A, lo que casi nunca es lo que quieres: si dejas los dos en alfa, el desplazamiento es igual en las dos direcciones y el resultado es un cizallado diagonal, no una deformación orgánica. Pon siempre R y G explícitamente.

Textura 1: papel rugoso

<filter id="rugoso" x="-10%" y="-10%" width="120%" height="120%"
        color-interpolation-filters="sRGB">
  <feTurbulence type="fractalNoise" baseFrequency="0.015" numOctaves="4"
                seed="11" result="ruido" />
  <feDisplacementMap in="SourceGraphic" in2="ruido" scale="9"
                     xChannelSelector="R" yChannelSelector="G" />
</filter>

Aplicado a un text o a un path de contorno, produce el borde tembloroso de un dibujo a mano. scale controla cuánto tiembla; baseFrequency controla el tamaño de las ondulaciones. Con frecuencia baja y escala alta sale un trazo ondulado; con frecuencia alta y escala baja, un borde vibrado.

Es la técnica detrás del aspecto «dibujado a mano» de las bibliotecas de diagramas, con la diferencia de que ellas perturban la geometría y esto perturba los píxeles. La ventaja de hacerlo con filtro: funciona sobre cualquier contenido, incluido texto y bordes redondeados. La desventaja: es un efecto de rasterizado, así que no escala.

Textura 2: grano de película

<filter id="pelicula" x="0" y="0" width="100%" height="100%"
        color-interpolation-filters="sRGB">
  <feTurbulence type="fractalNoise" baseFrequency="0.9" numOctaves="2"
                seed="4" result="grano" />
  <feColorMatrix in="grano" type="saturate" values="0" result="gris" />
  <feComponentTransfer in="gris" result="suave">
    <feFuncA type="linear" slope="0.12" intercept="0" />
  </feComponentTransfer>
  <feBlend in="SourceGraphic" in2="suave" mode="overlay" />
</filter>

El feComponentTransfer sobre el alfa con pendiente 0,12 es lo que baja la intensidad del grano a un nivel creíble. overlay como modo de fusión mantiene el contraste original y solo añade textura.

Este filtro tiene un uso muy concreto que ya se mencionó en el nivel 20: romper las bandas de un degradado. Superpuesto a un degradado grande, el ruido desplaza el redondeo de cada píxel y las bandas desaparecen. Con slope a 0,03 el grano es imperceptible y las bandas se van igual.

feMorphology

Dilata o erosiona una imagen: para cada píxel, toma el máximo (dilatar) o el mínimo (erosionar) de sus vecinos dentro de un radio.

<feMorphology operator="dilate" radius="2" in="SourceAlpha" result="gordo" />
<feMorphology operator="erode"  radius="2" in="SourceAlpha" result="flaco" />

radius acepta uno o dos valores. Y hay que saber que la implementación usa un kernel rectangular, no circular, así que las esquinas de una forma dilatada salen cuadradas. Con radios pequeños no se nota; con radios grandes, sí, y no hay forma de evitarlo dentro de SVG.

Textura 3: contorno exterior

Restar el original de su versión dilatada da un contorno de grosor exacto, que es algo que el trazo no puede hacer sobre una silueta compuesta:

<filter id="contorno" x="-20%" y="-20%" width="140%" height="140%"
        color-interpolation-filters="sRGB">
  <feMorphology in="SourceAlpha" operator="dilate" radius="3" result="gordo" />
  <feComposite in="gordo" in2="SourceAlpha" operator="out" result="anillo" />
  <feFlood flood-color="#f9e2af" result="tinta" />
  <feComposite in="tinta" in2="anillo" operator="in" result="borde" />
  <feMerge>
    <feMergeNode in="borde" />
    <feMergeNode in="SourceGraphic" />
  </feMerge>
</filter>

Es la manera de poner un contorno a un texto con varias palabras, a un icono con varias piezas, o a una imagen con transparencia, sin que el contorno se meta entre las piezas. Un stroke haría el contorno de cada letra por separado; esto lo hace de la silueta conjunta.

Textura 4: cristal líquido

Combinando desplazamiento con un ruido de baja frecuencia y anisotrópico:

<filter id="liquido" x="-15%" y="-15%" width="130%" height="130%"
        color-interpolation-filters="sRGB">
  <feTurbulence type="fractalNoise" baseFrequency="0.008 0.03"
                numOctaves="2" seed="19" result="ondas" />
  <feDisplacementMap in="SourceGraphic" in2="ondas" scale="28"
                     xChannelSelector="R" yChannelSelector="G" result="deformado" />
  <feGaussianBlur in="deformado" stdDeviation="0.6" />
</filter>

La frecuencia anisotrópica (0.008 0.03) da ondas alargadas en horizontal, que es lo que sugiere agua. El desenfoque final de 0,6 suaviza los saltos que deja el desplazamiento en las zonas de gradiente fuerte.

feTurbulence no se puede animar de forma barata, y el truco del seed no es el que parece

La tentación al ver el ruido es animarlo. Y hay dos maneras, las dos con trampa.

Animar seed parece lo obvio y es lo peor: cada valor de seed produce un campo de ruido completamente distinto, sin ninguna continuidad con el anterior. El resultado no es un ruido que fluye, es un ruido que parpadea. Además, seed se especifica como número entero a efectos del generador, así que los valores intermedios de una interpolación se truncan y el parpadeo va a saltos. Es la razón de que las animaciones de ruido copiadas de internet se vean como ruido de televisión y no como humo.

Animar baseFrequency da continuidad, porque el campo se estira y se encoge de forma suave. Es lo que se usa para el efecto de fuego o de agua, y funciona. El coste es que cada fotograma regenera el ruido entero, con todas sus octavas, sobre toda la región del filtro. Es de las cosas más caras que se pueden pedir a un navegador: una animación de ruido a pantalla completa con 4 octavas puede consumir el presupuesto de un fotograma varias veces.

Lo que sí es barato y da un resultado convincente: generar el ruido una vez y animar el desplazamiento. Se deja el feTurbulence fijo y se anima scale de feDisplacementMap, o se anima un transform sobre el elemento filtrado. El campo de ruido se cachea, y lo único que se recalcula por fotograma es la lectura del mapa.

Y la alternativa que gana a todas cuando el efecto es importante: generar el ruido una sola vez, exportarlo a PNG, y usarlo como feImage. Se pierde la independencia de resolución y se gana un orden de magnitud de rendimiento. Si el ruido es un fondo estático, esa es la respuesta correcta y no hay debate.

⚔️ Reto práctico

Construye un efecto de humo animado con el enfoque barato: un feTurbulence fijo de tipo fractalNoise, un feDisplacementMap sobre una forma, y una animación que mueva el elemento en lugar de regenerar el ruido. Compáralo, con el panel de rendimiento abierto, con la versión que anima baseFrequency. Anota los milisegundos por fotograma de cada uno y decide cuál puede ir en producción.