タービュレントディスプレイスを JS で実現する - SVG フィルタとシェーダで作る乱流ゆがみ

タービュレントディスプレイスを JS で実現する - SVG フィルタとシェーダで作る乱流ゆがみ

作成日:
読了:14
更新日:

After Effects に タービュレントディスプレイス(Turbulent Displace)というエフェクトがあります。フラクタルノイズでレイヤーを波打たせ、揺らめく炎・水面・旗・グリッチのような有機的なゆがみを作るものです。これを Web/JavaScript でやりたい——という話、実はブラウザ標準でほぼ同じことができます

鍵は SVG フィルタの feTurbulence(ノイズ生成)と feDisplacementMap(そのノイズでピクセルをずらす)の組み合わせ。この記事では、AE のパラメータと SVG 属性の対応から、インタラクティブデモ、画像・DOM への適用、Evolution(時間変化)のアニメーション、そして自由度の高い WebGL シェーダ版までを、MDN を一次ソースに整理します。

仕組み: 「ノイズ」で「ピクセルをずらす」

ディスプレイス(変位)の考え方はシンプルです。ある画像のピクセルを、別の画像(ディスプレイスメントマップ)の明るさに応じて移動させる。マップにノイズを使えば、ノイズの形に沿ってグニャリと歪みます。

MDN によれば、feDisplacementMap の変位は次の式で定義されます。

feDisplacementMap の変位式(MDN)
P'(x,y) <- P( x + scale * (XC(x,y) - 0.5), y + scale * (YC(x,y) - 0.5) )

P が元画像、XC/YC がマップの色チャンネル値(0〜1)。- 0.5 で「中央を基準に ±方向へずらす」ようになり、scale がずれの強さです。このマップに feTurbulence のフラクタルノイズを流し込むのが、タービュレントディスプレイスの正体です。

After Effects のパラメータと SVG 属性の対応

AE を触ったことがある人向けに、対応関係を表にするとこうなります。

AE の Turbulent DisplaceWeb(SVG フィルタ)補足
Amount(量)feDisplacementMapscaleゆがみの強さ
Size(サイズ)feTurbulencebaseFrequency小さい値ほど大きな模様(逆相関)
Complexity(複雑さ)feTurbulencenumOctavesノイズの重ね数。1〜5 程度
Displacement 種別feTurbulencetypeturbulence(シャープ)/ fractalNoise(なめらか)
Evolution(変化)時間で属性をアニメSVG には時間軸が無いので JS/SMIL で擬似的に(後述)
Random SeedfeTurbulenceseed同じ seed なら同じ模様

type は MDN の定義で、turbulence が乱流的でシャープ、fractalNoise がブラウン運動的でなめらか。AE の質感に近いのは多くの場合 turbulence です。

まずは触ってみる

下のデモは、SVG の feTurbulence + feDisplacementMap をテキストとグリッドに適用したものです。Amount(scale)・Size(baseFrequency)・Complexity(numOctaves)を動かし、type を切り替え、Evolution の再生/停止ができます。

TURBULENTDISPLACE

Size を下げる(=大きな模様)と「うねり」、上げる(=細かい模様)と「ザワつき」になります。Complexity を上げるとディテールが増え、Amount でゆがみ量が決まる——AE の挙動とそのまま対応しているのが分かります。

SVG フィルタで実装する

デモの中身は、突き詰めればこれだけです。feTurbulence でノイズを作り、それを feDisplacementMapin2 に渡します。

タービュレントディスプレイスの最小実装
<svg viewBox="0 0 480 220" xmlns="http://www.w3.org/2000/svg">
  <defs>
    <filter id="turbulent" x="-20%" y="-20%" width="140%" height="140%">
      <!-- 1. フラクタルノイズを生成 -->
      <feTurbulence
        type="turbulence"
        baseFrequency="0.018"
        numOctaves="3"
        seed="4"
        result="noise" />
      <!-- 2. ノイズでソースを変位させる -->
      <feDisplacementMap
        in="SourceGraphic"
        in2="noise"
        scale="28"
        xChannelSelector="R"
        yChannelSelector="G" />
    </filter>
  </defs>
 
  <text x="240" y="130" text-anchor="middle" font-size="54"
        font-weight="800" fill="#268bd2" filter="url(#turbulent)">
    TURBULENT
  </text>
</svg>

xChannelSelector / yChannelSelector は「マップのどの色チャンネルで縦横をずらすか」。R と G を使うのが定番です。フィルタ領域は歪みで要素がはみ出すため、x/y/width/height を広げて(例: -20%140%)クリップされないようにします。

画像や DOM 要素にも効く

このフィルタは SVG 内の図形だけでなく、CSS の filter: url() で HTML 要素にも適用できます。画像をゆがませるのも一行です。

画像・DOM への適用
.distort {
  filter: url(#turbulent);
}
img でも div でも
<img src="/photo.jpg" class="distort" alt="" />
<div class="distort">ゆがむテキストや UI</div>

ページ内のどこかに <svg> でフィルタ定義(<defs> 内)を置いておけば、url(#turbulent) で参照できます。

Evolution(時間変化)を JS でアニメーションする

AE の Evolution は「ノイズを時間方向に進める」パラメータですが、feTurbulence には時間軸(z)がありません。Web で「動く乱流」を作る代表的な手は2つです。

1. baseFrequency / seed を揺らす(requestAnimationFrame)

baseFrequency を小刻みに変化させると、模様がうねって動いて見えます。属性を直接書き換えるのが軽量です。

requestAnimationFrame で baseFrequency を揺らす
const turb = document.querySelector('#turbulent feTurbulence')
const base = 0.018
let t = 0
 
function tick() {
  t += 0.016
  const f = Math.max(0.001, base + Math.sin(t) * base * 0.4)
  turb.setAttribute('baseFrequency', f.toFixed(5))
  requestAnimationFrame(tick)
}
requestAnimationFrame(tick)

seed を一定間隔で変える手もありますが、seed は変えるたびに模様が切り替わる(連続的でない)ため、なめらかな流れには baseFrequency の方が向きます。上のデモの Evolution もこの方式です。

2. SMIL の animate で完結させる(JS なし)

JS を使わず、SVG だけで動かすこともできます。

SMIL で baseFrequency をアニメ
<feTurbulence type="turbulence" baseFrequency="0.018" numOctaves="3">
  <animate
    attributeName="baseFrequency"
    dur="8s"
    values="0.018; 0.03; 0.018"
    repeatCount="indefinite" />
</feTurbulence>

手軽ですが、再生制御やインタラクションが絡むなら JS(上の方式)が扱いやすいです。

もっと自由に: WebGL / GLSL シェーダ版

SVG フィルタは手軽で標準・GPU 合成も効きますが、「真の Evolution(ノイズの第3次元=時間)」や「画像・動画テクスチャを自在に歪める」「歪みを部分的にマスクする」といった要求にはシェーダが向きます。フラグメントシェーダでノイズ分だけ UV をずらしてサンプリングすれば、それがタービュレントディスプレイスです。

フラグメントシェーダ(抜粋・概念)
precision highp float;
uniform sampler2D uTex;   // 歪ませる画像
uniform float uTime;      // Evolution
uniform float uAmount;    // Amount
uniform float uSize;      // Size
varying vec2 vUv;
 
// 3D simplex noise(snoise)は別途用意する想定
float snoise(vec3 p);
 
void main() {
  // 時間を3次元目に使うと「連続的に変化する乱流」になる
  float nx = snoise(vec3(vUv * uSize, uTime));
  float ny = snoise(vec3(vUv * uSize + 31.4, uTime));
  vec2 displaced = vUv + vec2(nx, ny) * uAmount;
  gl_FragColor = texture2D(uTex, displaced);
}

ポイントは、ノイズ関数の引数に uTime を渡して第3次元(時間)でノイズを進めること。これで SVG では難しかった「途切れず流れ続ける乱流」が自然に書けます。Three.js / React Three Fiber を使うと、テクスチャ・ユニフォーム・描画ループの管理が楽になります(React Three Fiber 入門)。snoise(simplex noise)の実装は Ashima/webgl-noise などの定番 GLSL がそのまま使えます。

参考: Canvas 2D で手で書く場合

仕組みの理解には、Canvas 2D で1ピクセルずつ変位させる素朴な実装も有効です。ノイズ値で参照元座標をずらして ImageData を組み替えます。

Canvas 2D(概念・低速)
const src = ctx.getImageData(0, 0, w, h)
const dst = ctx.createImageData(w, h)
 
for (let y = 0; y < h; y++) {
  for (let x = 0; x < w; x++) {
    const n = noise2D(x * size, y * size)      // -1..1 のノイズ
    const sx = Math.round(x + n * amount)
    const sy = Math.round(y + n * amount)
    // (sx,sy) の画素を (x,y) へコピー(境界チェックは省略)
    const di = (y * w + x) * 4
    const si = (clamp(sy, 0, h - 1) * w + clamp(sx, 0, w - 1)) * 4
    dst.data[di] = src.data[si]
    dst.data[di + 1] = src.data[si + 1]
    dst.data[di + 2] = src.data[si + 2]
    dst.data[di + 3] = src.data[si + 3]
  }
}
ctx.putImageData(dst, 0, 0)

毎フレーム全画素を CPU で回すため重いです。学習やオフライン処理には良いですが、リアルタイム用途では SVG フィルタかシェーダを選びます。

使い分けと注意点

方式長所短所・向かない用途
SVG feTurbulence + feDisplacementMap標準・手軽、画像/DOM に CSS で適用可、GPU 合成Evolution は擬似的、細かい制御は限界
WebGL / GLSL シェーダ真の時間変化、テクスチャ/動画、マスク等自在、高速実装コスト、WebGL の知識が要る
Canvas 2D 手書き仕組みが分かる、依存なし低速。リアルタイム不向き

実務上の注意:

  • フィルタ領域のはみ出し: 歪みで要素が領域外に出ると切れる。x/y/width/height を広げる
  • 色空間: feTurbulence は既定で linearRGB。見た目を合わせたいときは color-interpolation-filters="sRGB" を指定
  • パフォーマンス: 大きな要素・高 numOctaves・毎フレーム属性更新は重くなりうる。モバイルでは baseFrequency の更新頻度や要素サイズを抑える
  • アクセシビリティ: 強い揺れは prefers-reduced-motion を尊重して Evolution を止める配慮を

まとめ

  • タービュレントディスプレイスは「ノイズでピクセルをずらす」エフェクト。Web では SVG の feTurbulence + feDisplacementMap がほぼ等価
  • AE のパラメータは SVG 属性に対応: Amount=scale / Size=baseFrequency(逆相関)/ Complexity=numOctaves / 種別=type / Seed=seed
  • filter: url(#id)画像や DOM にも適用可能
  • Evolution は feTurbulence に時間軸が無いため、baseFrequencyrequestAnimationFrame か SMIL で揺らして擬似的に表現する
  • 真の時間変化・テクスチャ歪み・マスクが要るなら WebGL/GLSL シェーダ(ノイズの第3次元に時間を使う)が本命
  • 理解には Canvas 2D の手書きも有効だが低速。リアルタイムは SVG かシェーダ

「After Effects のあのエフェクト、Web では無理かな?」と思いがちですが、タービュレントディスプレイスは標準機能の組み合わせで驚くほど近づけます。まずは SVG フィルタで触り、物足りなくなったらシェーダへ——という順で広げるのがおすすめです。

参考リンク