GODRICH
#cssアニメーション#ボタン#ローディング#トランジション#スクロール#ダッシュボード#実用ui#デザイントレンド

レイマーチング9選 — WebGLなし、キャンバスのピクセル計算で描く3Dシーン

レイマーチングとは、画面のピクセルごとに視線を1本ずつ前へ進め、数式で定義した距離関数に初めて触れる点の色を決める3Dレンダリングの手法です。WebGLなし、キャンバスのピクセル計算だけで、球からフラクタル、雲まで9つの場面をこのページで動かしています。

自動再生 · タイルを押すとその項目へ · すべて1つのzip

なぜこの順番か

並びは見栄えの順ではありません。距離関数が複雑になっていく順です。まず球1つの距離式で前進の規則そのものを覚え(01)、同じ規則を部屋の内側とドーナツに適用します(02・03)。次は距離を正確に知っているのではなく推定する段階で、マンデルバルブとメンガーのスポンジがフラクタルの近似距離へ進みます(04・05)。06からは距離同士を混ぜる方法(06)、三角関数で書く無限構造(07)、表面を捨てて密度を積む方法(08)へと場面の定義自体が広がり、最後に影・霧・反射をひとつの舞台に載せて部品を完成品にまとめます(09)。9枚すべてで、全23回のフレーム遷移に動きがありました(実測)。ピクセルを直接焼く隣人としては、fbmノイズのキャンバス地形と反応拡散で模様を育てる盤が同じ系譜です。

項目コード

01球ひとつ — 最小マーチャーが作る最初の3D

ヒーロー背景に置かれる照らされた球体や、3D紹介画面の最初の場面に使う、床へ柔らかい影が落ちる最小のマーチャーです。

球ひとつ — 最小マーチャーが作る最初の3D
新しいタブで開く

01 · 球ひとつ — 最小マーチャーが作る最初の3D — 計測 累積面積 13.794% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

ImageDataputImageDatarequestAnimationFrame

距離の分だけ安全に前進するループ

// ── march ループ — 9項目の原型。距離の分だけ安全に前進する ──
var t = 0.0, hit = 0, px = ex, py = ey, pz = ez;
for (var s = 0; s < STEPS; s++) {
  var d = map(px, py, pz);
  if (d < HIT_EPS) { hit = 1; break; }      // 表面に到達
  if (t > 12.0) break;                      // 遠くなれば空へ放す
  t += d;
  px = ex + dx * t; py = ey + dy * t; pz = ez + dz * t;
}

場面は2つの距離の最小値

// 場面の距離 = 2つの物体距離の最小値(和集合)
function map(px, py, pz) {
  var dS = sdSphere(px, py, pz);
  var dF = sdFloor(py);
  return dS < dF ? dS : dF;
}

map が返す値 d はその点から最も近い表面までの距離なので、その長さだけ前に進んでもどの物体にも触れないことが保証されます。一歩進んで測り直し、d が判定値 HIT_EPS を下回れば、そこがそのピクセルの色になります(実測: 累積面積13.794パーセント、強度平均19.1、RT_W 64px・STEPS 64)。

02角丸箱の部屋 — 内側から見る交差シーン

内装のプレビューや製品の見え方ガイドに使う、カメラが箱の中に入って壁を見る室内レンダリングです。

角丸箱の部屋 — 内側から見る交差シーン
新しいタブで開く

02 · 角丸箱の部屋 — 内側から見る交差シーン — 計測 累積面積 52.589% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

ImageDataputImageDatarequestAnimationFrame

内側から外を測る交差の距離

// 室内距離 — カメラが箱の中にいるときの丸い壁までの距離。
// コア(半幅 - 丸み)の面までの余裕がそのまま安全な移動距離になる
function distToWall(px, py, pz) {
  var hx = 3.0 - ROUND, hy = 1.5 - ROUND, hz = 3.0 - ROUND;
  var dx = hx - Math.abs(px), dy = hy - Math.abs(py), dz = hz - Math.abs(pz);
  return Math.min(dx, Math.min(dy, dz));
}

// ── march ループ(室内版) — 境界までの距離の分だけ内側から前進する ──
var t = 0.06, hit = 0, px = ex, py = ey, pz = ez;
for (var s = 0; s < STEPS; s++) {
  px = ex + dx * t; py = ey + dy * t; pz = ez + dz * t;
  var d = distToWall(px, py, pz);
  if (d < HIT_EPS) { hit = 1; break; }      // 壁に到達
  t += d;
  if (t > 9.0) break;
}

大きな面は歩数が少なくて済む

外から見る物体の距離が min なのに対し、室内では3つの面までの余裕のうち最も小さい値が安全な移動距離になる点だけが違い、ループは同じ形です。天井のランプが半径0.85の円を描いて回って壁の明るさが流れ、面が大きくて1ステップの移動距離が長いため、56ステップ・56pxの予算でも累積面積52.589パーセント・強度平均37.1を記録しました(実測)。

03トーラスリング — ドーナツの距離式と縁の光

ロゴリングやローディング場面の回転ドーナツに使う、2つの半径の差だけで定義される立体です。

トーラスリング — ドーナツの距離式と縁の光
新しいタブで開く

03 · トーラスリング — ドーナツの距離式と縁の光 — 計測 累積面積 11.927% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

ImageDataputImageDatarequestAnimationFrame

2つの半径の差が作る立体

// ドーナツ(トーラス)距離 — XY平面に寝かせた。2つの半径の差がすべて
function sdTorus(px, py, pz) {
  var q = Math.sqrt(px * px + pz * pz) - TOR_R;   // 中心軸までの距離 - R
  return Math.sqrt(q * q + py * py) - TOR_r;      // チューブ中心から r だけ内側
}

// rim — 法線が視線と垂直に近いほど(縁)1 に近づく
var facing = Math.max(0, n[0] * -dx + n[1] * -dy + n[2] * -dz);
var rimK = Math.pow(1 - facing, 2.2);
var rimLit = Math.max(0, n[0] * rbx + n[1] * rby + n[2] * rbz);
var rim = rimK * (0.35 + 0.65 * rimLit);

穴の内側はステップを増やす

中央の軸までの距離から大きな半径 TOR_R(1.05) を引き、改めて縦成分と束ねてチューブ半径 TOR_r(0.42) を引くと、ドーナツ全体がわずか2行になります。穴の内側の谷に踏み込むピクセルが多いためステップは72まで上げており、画面を占める面積は11.927パーセントと狭くても、rim 光が縁を掠め続けて強度平均22.9を保ちました(実測: RT_W 48px)。

04マンデルバルブ — 距離推定が開くフラクタル

ヒーロー背景のフラクタル造形や表紙アートの種に使う、複素平面のマンデルブロを3次元に持ち上げた冪乗反復です。

マンデルバルブ — 距離推定が開くフラクタル
新しいタブで開く

04 · マンデルバルブ — 距離推定が開くフラクタル — 計測 累積面積 33.917% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

ImageDataputImageDatarequestAnimationFrame

冪乗反復と距離推定式

var zx = dx, zy = dy, zz = dz;                 // z = c から出発
var dr = 1.0, r = bc, trap = 2.0;
for (var i = 0; i < ITER; i++) {
  r = Math.sqrt(zx * zx + zy * zy + zz * zz);
  if (r < trap) trap = r;
  if (r > 1.6) break;                          // 脱出 — この時点の推定値を使う
  // 球面座標で8乗 — 3つの軸が一緒にねじれる
  var theta = Math.acos(zy / r);
  var phi = Math.atan2(zx, zz);
  var zr = Math.pow(r, POWER);
  var drn = Math.pow(r, POWER - 1) * POWER * dr + 1.0;
  var t8 = theta * POWER, p8 = phi * POWER;
  var st = Math.sin(t8);
  zx = zr * st * Math.sin(p8) + dx;
  zy = zr * Math.cos(t8) + dy;
  zz = zr * st * Math.cos(p8) + dz;
  dr = drn;
  if (r < 0.01) break;                         // 原点付近の収束 — 理論上無限の深さ
}
return 0.5 * Math.log(Math.max(r, 1.0001)) * r / dr;   // DE = 0.5·r·log(r)/|dr|

推定距離には安全係数を掛ける

反復ごとに点を球面座標へ移して8乗でねじり、元の点を足し直しますが、ここでは距離を知るのではなく微分値 dr で推定します — 最終行の 0.5·r·log(r)/|dr| がその推定式です。推定は過大に出がちなので SAFETY 0.9 を掛けて前進させ、48px・80ステップの予算で累積面積33.917パーセント・強度平均44.6でした(実測)。

05メンガーのスポンジ都市 — 規則的な穴の再帰

ゲーム背景のブロック都市や幾何パターン背景に使う、立方体を9分割して中央を貫く規則の反復です。

メンガーのスポンジ都市 — 規則的な穴の再帰
新しいタブで開く

05 · メンガーのスポンジ都市 — 規則的な穴の再帰 — 計測 累積面積 52.859% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

ImageDataputImageDatarequestAnimationFrame

座標を折り畳んで管を重ねる再帰

function map(px, py, pz) {
  var cy = py - 1.0;            // スポンジの中心 (0,1,0)・半幅 1
  var d = sdBox(px, cy, pz, 1.0);
  var s = 1.0;
  var cell = 0;
  for (var n = 0; n < LEVELS; n++) {
    var ax = mod2(px * s) - 1.0;
    var ay = mod2(cy * s) - 1.0;
    var az = mod2(pz * s) - 1.0;
    s *= 3.0;
    var rx = Math.abs(1.0 - 3.0 * Math.abs(ax));
    var ry = Math.abs(1.0 - 3.0 * Math.abs(ay));
    var rz = Math.abs(1.0 - 3.0 * Math.abs(az));
    var da = Math.max(rx, ry);              // 十字の管の3方向成分
    var db = Math.max(ry, rz);
    var dc = Math.max(rz, rx);
    var c = (Math.min(da, Math.min(db, dc)) - 1.0) / s;
    d = Math.max(d, c);
    cell = (ax + 1) * 4 + (ay + 1) * 2 + (az + 1);   // 0..8 セルコード — 色変調用
  }
  lastCell = cell;
  return d;
}

近似でも保守的なら形は正確

各段階で座標を mod 2 で折り、十字の管の距離を max で重ねますが、この値は実際の距離より小さいか等しい保守的な近似なので、安全に使う限り形は正確です。セルコードでブロックごとに色が分かれて団地のようになり、48pxの低解像度でも穴が鮮明で、累積面積52.859パーセント・強度平均51.6と9枚の中で最も明るく生きています(実測: STEPS 72・SAFETY 0.85)。

06メタボール融合 — 滑らかな和集合

ロゴアニメーションや有機的な形のバッジに使う、2つの塊が触れる瞬間に首が伸びて混ざる場面です。

メタボール融合 — 滑らかな和集合
新しいタブで開く

06 · メタボール融合 — 滑らかな和集合 — 計測 累積面積 32.783% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

ImageDataputImageDatarequestAnimationFrame

滑らかな最小値が首を作る

// 多項式 smooth min — 半径 k の中で2つの距離をつなぐ(首が作られる)
function smin(a, b, k) {
  var h = 0.5 + 0.5 * (b - a) / k;
  if (h < 0) h = 0; else if (h > 1) h = 1;
  return b + (a - b) * h - k * h * (1 - h);
}

// 場面の距離 = 3つの球の smin 和集合(床を含む)
function map(px, py, pz) {
  var dA = sdSphere(px, py, pz, centers[0], centers[1], centers[2], 0.72);
  var dB = sdSphere(px, py, pz, centers[3], centers[4], centers[5], 0.72);
  var dC = sdSphere(px, py, pz, centers[6], centers[7], centers[8], 0.42);
  lastMix = dA < dB ? 0 : 1;
  var dAB = smin(dA, dB, K);
  var dS = smin(dAB, dC, K * 0.8);           // 小さい球はより細い首で繋がる
  var dF = py;                               // 床平面 y=0
  return dS < dF ? dS : dF;
}

smin の距離も正確な距離

smin は2つの距離の最小値を半径 K(0.35) の中で多項式につなぎ、物体が触れる前に首を生やさせる1行の技法です。2つの大きな球が3.2秒周期で寄ったり離れたりする間に首が繋がったり切れたりし、smin の距離も正確なSDFなので安全係数なしでそのまま前進できます — 累積面積32.783パーセント・強度平均35.6(実測: RT_W 56px・STEPS 64)。

07ジャイロイド殻 — 三角関数で書く無限構造

テクスチャ背景や造形アートシーンに使う、サインとコサインの積が作る襞の殻です。

ジャイロイド殻 — 三角関数で書く無限構造
新しいタブで開く

07 · ジャイロイド殻 — 三角関数で書く無限構造 — 計測 累積面積 33.915% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

ImageDataputImageDatarequestAnimationFrame

等値面を2枚で挟んで切る殻

// ジャイロイド密度 — 3軸の sin·cos 積の和。値が 0 の面が無限の襞の等値面
function gyroid(px, py, pz) {
  return Math.sin(px) * Math.cos(py) + Math.sin(py) * Math.cos(pz) + Math.sin(pz) * Math.cos(px);
}

function map(px, py, pz) {
  var cy = py - 1.0;
  // 等値面の位相移動 — 座標を丸ごと流すと襞が這っていく
  var g = gyroid(px + phase, cy * 1.0, pz - phase * 0.6);
  lastBand = g / 0.5;
  var lip = (Math.abs(g) - SHELL_W) / 1.8;   // 殻の厚さで切る — 近似距離
  var dR = Math.sqrt(px * px + cy * cy + pz * pz) - 1.78;
  return lip > dR ? lip : dR;                // max = 交差(殻を球の中へ切り抜く)
}

勾配が急な面は前進を半分に

3つの積の和が0になる面が無限に襞れた等値面で、絶対値から殻の厚さ SHELL_W(0.34) を引いて1.8で割ったものを近似距離にし、半径1.78の球で切り抜きます。等値面の勾配の大きさは最大で約1.8に達するため真の距離へ換算するにはその逆数より小さくする必要があり、この盤は SAFETY を0.5まで押し下げて前進速度を半分にしています — 累積面積33.915パーセント・最大面積22.628パーセントで、画面のあちこちが動き続けます(実測: RT_W 56px・STEPS 68)。

08雲の体積 — 面ではなく積もる霧

空の背景や幻想的なヒーロー場面に使う、境界なく綿のように重なる雲です。

雲の体積 — 面ではなく積もる霧
新しいタブで開く

08 · 雲の体積 — 面ではなく積もる霧 — 計測 累積面積 45.965% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

ImageDataputImageDatarequestAnimationFrame

固定ステップで積む密度の累積

// 雲の密度 — 高さで整え(上下へ滲む)fbm と掛ける。雲の下は空いている
function density(px, py, pz, tSec) {
  var cy = py - 1.55;                       // 雲の層の中心の高さ
  var hFade = 1.0 - (cy * cy) / 0.85;       // 高さの耳(0 以下は雲なし)
  if (hFade <= 0) return 0;
  var n = fbm(px * 0.9 + tSec * DRIFT, cy * 1.5, pz * 0.9 - tSec * DRIFT * 0.7);
  var d = (n - 0.44) * hFade * DENSITY;     // 閾値 0.44 で綿のかたまりだけ残す
  return d > 0 ? d : 0;
}

当たり判定を捨てた体積マーチ

表面を探さず、レイが通る道筋ごとに密度を量ります。40個の固定区間で経路を分け、各区間の不透明度 1 - Math.exp(-d * STEP_LEN) を前が開いている分だけ足すfront-to-back合成で、前が0.98以上埋まったピクセルは奥を計算しません — 最も柔らかく動くため強度平均は17.3と低いものの、累積面積45.965パーセントを維持しました(実測: RT_W 52px・固定40ステップ)。

09チェッカーボード — 影・霧・反射をひとつのシーンに

製品のステージやインタラクティブアートシーンに使う、これまでの部品を全部載せた完成形の舞台です。

チェッカーボード — 影・霧・反射をひとつのシーンに
新しいタブで開く

09 · チェッカーボード — 影・霧・反射をひとつのシーンに — 計測 累積面積 52.859% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

ImageDataputImageDatarequestAnimationFrame

影レイは同じループの再利用

// 柔らかい影 — 表面の点から光源方向へもう一度マーチする(01 と同じ構造)。
// 掠めるように近いときはクランプ緩和で半影になる
function softShadow(px, py, pz, lx, ly, lz) {
  var t = 0.02, res = 1.0;
  for (var i = 0; i < SHADOW_STEPS; i++) {
    var d = map(px + lx * t, py + ly * t, pz + lz * t);
    if (d < -0.001) return 0.0;
    res = Math.min(res, 10.0 * d / t);      // 縁が滑らかに — 係数 10 が緩和の幅
    if (res < 0.005 || t > 7.0) break;
    t += Math.max(d, 0.02);
  }
  return res < 0 ? 0 : (res > 1 ? 1 : res);
}

反射も霧も同じ距離関数から

床の点から光源へ向かって同じマーチループをもう一周すると影になり、掠める距離は 10.0 * d / t で緩和されて半影になります。濡れた床の反射も法線で跳ね返したレイを32ステップだけ追加マーチして0.38の割合で混ぜ、地平線は exp2 減衰の霧で溶かし、画面全体が均等に動いて累積面積52.859パーセントです(実測: RT_W 56px・STEPS 64・最大面積5.162パーセント)。

レイマーチングはどう3Dになるのか — 距離場を歩く視点

符号距離関数が場面を定義する

場面は多角形のリストではなく関数1つです。点 (x, y, z) を渡すとその点から最も近い表面までの距離が戻り、物体の内側では負の値になるものが符号距離関数(SDF)です。球は length(p - c) - r のみで、2物体の和集合は距離の min、交差は max で組み上がります — 02の部屋が min の室内版、07の殻が max で切り抜いた交差です。距離関数の目録がこの組み立て方の辞書の役割を果たします。

前進規則の数値反復

目から画面のピクセル1つへ向かう線に沿って、この規則だけを反復します: その場で距離を測り、測った分だけ前に進む — t += d。01の球の場面で画面中央のピクセルを追うと、目は球の中心から4.2離れた軌道にいて半径が1なので最初の測定は d が3.2、一歩で t が3.2まで進み、その場所はすでに表面の上なので次の測定が判定値0.005を下回り、2、3歩で色が決まります。角を斜めに掠めるピクセルは d が小さな正の値しか返さず数十歩歩いた末にステップ上限で空へ放たれ、遠いピクセルほど歩数が増えるこの性質が、解像度とステップ数を予算として扱う理由です。

法線は距離場の勾配

距離が0の場所が表面なので、距離場の勾配は表面で外を向く矢印、すなわち法線になります。9枚すべてが同じ前進差分で軸ごとに2回 map を呼んで勾配を測り、正規化して使います。照明も rim も反射もすべてこの法線1つから出てくるので、場面を新しく作ることは map を1つ新しく書くことと同じです。

低解像度バッファと拡大補間

ピクセルごとにステップを踏む計算は高くつくため、9枚は48~64px幅の小さなバッファに ImageData でピクセルを焼き、putImageData で移してから画面サイズへ滑らかに拡大します。計算量がウィンドウの大きさから切り離されて、このcanvas レンダリングの予算が固定される構造で、フレームは requestAnimationFrame が渡す時刻だけで流れます。ここが、マウス追従のポリゴン3Dシーンとは技法の軸が違うところです — あちらは頂点を遠近投影して多角形を重ねるのに対し、こちらは多角形なしで距離1つに場面全体を定義し、目からシーンへ入っていく深さを持ちます。

どこで壊れるか — 落とし穴

近似距離が実際より大きいと起きるトンネリング

map が返す値が真の距律より大きくなった瞬間、レイは表面を突き抜けます。t += d は d が真であるときだけ安全だという保証なので、正確な距離を知らない場面では安全係数を掛けて前進しなければなりません — マンデルバルブは推定式なので0.9を、メンガーのスポンジは保守的な近似なので余裕を見た0.85を掛けます。ジャイロイドが最も極端で、等値面の勾配の大きさは最大約1.8のため真の距離へ換算するにはその逆数より小さくする必要があり、07は SAFETY を0.5まで押し下げます。係数を1に近づけると襞や穴の中へレイが瞬間移動して、表面に穴が開いた絵になるので、フラクタルの盤を直すたびにこの値から見直しました。

表面のない場面の固定ステップ

08の雲には触れる表面がないため、t += d の規則自体が成立しません。密度が0より大きい領域は境界ではなく霧のように広がっているので、この盤は測定を諦めて STEP_LEN 0.16 の固定区間40個で経路を切ります。同じ理由で、他の盤の技巧である判定値を締めることやステップ上限の調整はここでは何の役にも立たず、区間の長さと密度の倍率だけが画面を変えます。9つの場面のバニラ版とReact版は記事の下のパスワード付きzipにまるごと入っていて、開くパスワードはx8z9uagaです。

3つのバリエーションとアクセシビリティ

値1つを変えた3つの変種

名前 変える値 一言の感想
ステップ数 01 球の場面の最大ステップ 48 → 96 縁のシルエットが厚くなりフレームが重くなる
解像度 03 トーラスのレンダーターゲット 48px → 96px 同じ場面が鮮明になる代わりに計算量は4倍
色 $rm-lilac → $rm-peach 冷たいライラックの照明が暖かいピーチのトーンへ

3つともソース先頭の変化定数ブロックで値1つだけを変える軸です。03は画面内のボタンで48/96をその場で切り替えられて、計算量の違いを目で確かめられます。

動きを減らした利用者に残る1枚

prefers-reduced-motion: reduce を設定した利用者には requestAnimationFrame のループを始めず、各場面の構造が最も読める角度の静的な1枚だけを描きます — 01は影が読める heading 0.55・pitch 0.3、06は首が繋がる瞬間、09は影と反射が同時に見える角度です。動きを最小化するメディアクエリがオフのときと同じ計算で描いた1枚なので品質は変わりません。canvas アニメーション 作り方を探している方には、この静止フレームをそのまま背景画像として使う道もあります。

キーボードの矢印で回すカメラ

9枚すべてが role="application" と tabindex="0" を持つ実験盤で、ポインタと矢印キーを一緒に受け付けます。画面をタブで移して焦点を与えると矢印キーで heading と pitch を動かせ、reduced-motion の利用者はキーを押すたびにその角度の1枚だけを描き直して、カメラを回しながら構造を調べられます。

FAQ

レイマーチングとはレイトレーシングと何が違うのですか?

レイトレーシングが鏡面やガラスの反射・屈折を物理式で追うのに対し、レイマーチングは距離関数で定義した場面へ向かって視線を距離の分だけずつ進めて、最初に触れる表面を探す手法です。多角形のモデルを用意しなくてもフラクタル・無限構造・霧の体積が数行で現れる代わりに、正確なSDFでなければトンネリングという固有の落とし穴を管理する必要があります。

html5 canvas でもこの計算量は持ちこたえますか?

持ちこたえます。9枚はバッファの幅を48~64pxに固定して、計算するピクセル数をウィンドウの大きさと無関係な定数にまとめ、拡大は補間に任せます。実測では9枚すべてで23回のフレーム遷移に動きが確認できており、フレームは時刻の値で再生されるため、遅い端末ではゆっくり再生されるだけで計算が積み上がることはありません。

canvas アニメーション サンプルとして自分のページに貼っても良いですか?

良いです。記事の下のパスワード付きzipに9つの場面のバニラ版とReact版が入っていて、貼り付ければすぐ動きます。色は各盤のscss先頭にあるパステルトークン変数で入れ替えられ、変化定数ブロックの値を1つ変えるだけで、解像度・ステップ数・雲の濃さなどの個性も変えられます。

さらに読む

解凍パスワードを入力してください

パスワードはこの記事の本文の中にあります。読み進めると出てきます。