波動方程式をブラウザで可視化する — 干渉・回折・共鳴の世界
このトピックを深掘りする
クリエイティブコーディング
ブラウザでの数学的シミュレーション・自然現象の可視化・生成アートの実装技法
全12本中 5 本目。
次に読む記事
N体問題をブラウザで — 万有引力シミュレーションの実装
NBody、SolarSystem、GravityWellコンポーネントで使った万有引力計算、ベクトル演算、積分法、軌道描画の実装詳細。
Reaction-Diffusionのパラメータ空間を探索する — Gray-Scottモデルの4つの顔
Gray-Scott Reaction-Diffusionモデルのfeed/kill率を変えると、斑点・縞模様・珊瑚・細胞分裂と全く異なるパターンが現れる。200×200グリッドでの実装と探索記録。
砂山モデルと自己組織化臨界 — BTWモデルをブラウザで動かす
Bak-Tang-Wiesenfeld砂山モデルの実装を通じて、自己組織化臨界現象をブラウザで可視化する。べき乗則に従う雪崩分布の計測とフラクタルパターンの生成過程を解説。
シリーズ全体の流れを見ながら、次に読む記事へ進めます。 初めての方はホームへ →
波動方程式の離散化
連続的な波動方程式 ∂²u/∂t² = c²∇²u を有限差分法で離散化する。時間と空間を格子に分割し、差分で微分を近似する。
1D波動方程式
const N = 500
const c = 2.0 // 波の伝播速度
const dt = 0.01
const dx = 1.0
const u = new Float32Array(N) // 現在
const uPrev = new Float32Array(N) // 1ステップ前
const uNext = new Float32Array(N) // 次のステップ
function step1D() {
const r = (c * dt / dx) ** 2
for (let i = 1; i < N - 1; i++) {
uNext[i] = 2 * u[i] - uPrev[i] + r * (u[i + 1] - 2 * u[i] + u[i - 1])
}
// 境界条件(固定端)
uNext[0] = 0
uNext[N - 1] = 0
// バッファ入れ替え
uPrev.set(u)
u.set(uNext)
}r = (c*dt/dx)² はCFLクーランド数。r ≤ 1 でないと数値的に不安定になる(解が発散する)。
2D波動方程式
WaveEquationとChladniPlateで使う:
const W = 200
const H = 200
const u2d = new Float32Array(W * H)
const u2dPrev = new Float32Array(W * H)
const u2dNext = new Float32Array(W * H)
const damping = 0.999
function step2D() {
const r = (c * dt / dx) ** 2
for (let y = 1; y < H - 1; y++) {
for (let x = 1; x < W - 1; x++) {
const idx = y * W + x
const laplacian =
u2d[idx + 1] + u2d[idx - 1] +
u2d[idx + W] + u2d[idx - W] -
4 * u2d[idx]
u2dNext[idx] = (2 * u2d[idx] - u2dPrev[idx] + r * laplacian) * damping
}
}
u2dPrev.set(u2d)
u2d.set(u2dNext)
}damping = 0.999 はエネルギー減衰。これがないと波が永遠に反射し続けてカオスになる。
WaveInterference — 干渉パターン
2つの波源から同心円状に波を放射し、干渉パターンを可視化する:
function addSource(u: Float32Array, x: number, y: number, amplitude: number, t: number) {
const freq = 5.0
const idx = y * W + x
u[idx] += amplitude * Math.sin(2 * Math.PI * freq * t * dt)
}2つの波源を近くに置くと、強め合いと弱め合いのストライプ(ヤングの干渉縞)が現れる。波源間の距離と波長の比で縞の間隔が決まる。
ChladniPlate — 共鳴パターン
クラドニ図形は薄板の共鳴モードを可視化したもの。2D波動方程式で特定の周波数で振動させると、振幅ゼロの線(節線)が現れる:
function renderChladni(ctx: CanvasRenderingContext2D, u: Float32Array) {
const imageData = ctx.createImageData(W, H)
for (let i = 0; i < W * H; i++) {
const amplitude = Math.abs(u[i])
// 節線(振幅ゼロ付近)を白く、腹を暗く
const brightness = amplitude < 0.01 ? 255 : Math.max(0, 255 - amplitude * 500)
const px = i * 4
imageData.data[px] = brightness
imageData.data[px + 1] = brightness
imageData.data[px + 2] = brightness
imageData.data[px + 3] = 255
}
ctx.putImageData(imageData, 0, 0)
}物理的なクラドニ実験では金属板に砂をまいて弓で弾く。砂が節線に集まり、美しい幾何学模様が現れる。それをブラウザで再現した。
SoundWavesとの関係
SoundWavesコンポーネントはWeb Audio APIの AnalyserNode でリアルタイムの波形データを取得して描画する。波動方程式のシミュレーションとは異なり、実際の音声信号の可視化:
function drawWaveform(ctx: CanvasRenderingContext2D, dataArray: Uint8Array) {
ctx.beginPath()
const sliceWidth = ctx.canvas.width / dataArray.length
for (let i = 0; i < dataArray.length; i++) {
const v = dataArray[i] / 128.0
const y = (v * ctx.canvas.height) / 2
if (i === 0) ctx.moveTo(0, y)
else ctx.lineTo(i * sliceWidth, y)
}
ctx.stroke()
}「波」という共通テーマでも、物理シミュレーション(WaveEquation)と信号処理(SoundWaves)では実装がまったく異なる。
パフォーマンス最適化
200×200グリッドの2Dシミュレーションは毎フレーム40,000セルを更新する。Float32Arrayを使う理由:
- メモリ連続性: TypedArrayはC言語の配列と同じメモリレイアウトで、キャッシュヒット率が高い
- 数値型固定: JITコンパイラが型推論を省略できる
- GC不要: サイズ固定でガベージコレクションが走らない
通常の number[] と比較して、ループ処理が2-3倍速くなるケースもある。
まとめ:波動方程式で使った技術と道具
波動現象の数学的背景を理解するなら、道具棚の書籍が助けになる。差分法の安定性条件やCFL条件は、シミュレーション全般で必須の知識。