4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ブラウザだけで動くWebGL作品を一気に4つ作った — スクロール宇宙・自作GPU流体・物理カー・自作の海で踏んだ罠まとめ

4
Last updated at Posted at 2026-06-21

はじめに

勢いで、ブラウザで動くインタラクティブなWebGL作品を一気に4つ作った。せっかくなので、それぞれで踏んだ罠と「効いた一手」をまとめておく。方向性が全部違う(スクロール演出 / GPUシミュレーション / 物理エンジンありのゲーム / 物理エンジンなしのゲーム)ので、つまずきポイントもバラバラで面白い。

作品 何か 技術 リンク
🌌 VOYAGE(航宙) スクロールで宇宙を旅するシネマティック3D Three.js + GLSL + Lenis サイト / GitHub
🎨 INK(墨流し) ドラッグでかき混ぜるフル画面GPU流体 純WebGL2 + GLSL(ライブラリ不使用) サイト / GitHub
🚗 DRIVE(走) 低ポリの島をミニカーで走り回る物理ワールド Three.js + Rapier(WASM物理) サイト / GitHub
🌊 WAKE(航跡) スピードボートで自作の海を走る(物理エンジンなし) Three.js + GLSL(Gerstner波) サイト / GitHub

インタラクティブものは実物が一番なので、まず触ってから読むのがおすすめ。ソースは全部 Public にしてある。


作品1: VOYAGE — スクロール量で「宇宙の旅」を演出する

VOYAGE 大気圏

やったこと

Immersive Garden や EXP by Lusion みたいな「スクロールするとカメラが動いて、1ページが映像作品になる」系。
ポイントは、スクロール位置を正規化した進捗 p(0..1) に変換して、これ一つで全部を駆動すること。カメラのz軌道も、星雲/銀河の不透明度も、ワープ量も、bloomの強さも、章キャプションの表示も、全部 p から出す。

// スクロール量 → p(Lenisのスムーススクロールを利用)
const p = lenis.limit > 0 ? clamp(lenis.scroll / lenis.limit, 0, 1) : 0;

// pでカメラを軌道に沿わせる(piecewise-linear track: z=16 → -138)
const camZ = track(p, Z_TRACK);

// ワープ量も星雲/銀河の不透明度も、全部pから
const warp = smoothstep(0.64, 0.72, p) * (1 - smoothstep(0.82, 0.90, p));

5章構成:大気圏 → 軌道 → 星雲 → ワープ → 銀河

ハマり1:加算合成パーティクルの中を飛ぶと真っ白に飛ぶ

星雲を「数千の加算合成(additive)パーティクル」で作り、その中をカメラで通り抜ける演出にしたら、近づいたパーティクルが巨大スプライト化して画面が真っ白になった。additive は重なるほど明るくなるので、近接した点が累積して飽和する。

効いた対策はシェーダー側の2つ:

// ① 点サイズに上限を付ける
gl_PointSize = min(uSize * aScale * uPixelRatio * (300.0 / dist), 150.0 * uPixelRatio);

// ② 近接フェード:レンズに近すぎる点を溶かして「顔面に巨大スプライト」を防ぐ
vFade = smoothstep(1.5, 14.0, dist);

加えて、雲ごとに不透明度を下げ、星雲の中ではbloomを少しだけ下げると色が残るようになった。

ハマり2:銀河が画面外へ吹き飛ぶ(地味に1サイクル溶かした)

渦巻銀河を作って points.rotation で傾けたら、銀河が画面のどこにもいない。
原因は、銀河の頂点を中心 (0, 6, -160)ベイクしたままオブジェクト回転やシェーダー内スピンをかけたこと。回転はオブジェクトのローカル原点周りに効くので、原点から離れた場所にある頂点群が原点を中心に「公転」して、画面外へ飛んでいってしまう。

直し方は「頂点は原点中心で作って、配置は position で行う」:

// ❌ 頂点に中心をベイク → rotationで原点周りに公転して飛ぶ
positions[i*3] = center.x + Math.cos(a) * r;

// ✅ 原点中心で作り、配置はpositionで(spin/tiltが自分の中心周りに効く)
positions[i*3] = Math.cos(a) * r;
points.position.set(0, 6, -240);
points.rotation.x = -0.5;

VOYAGE 銀河

言われてみれば当たり前なんだけど、「メッシュは原点付近に作って position で置く」を徹底していないと普通に踏む。


作品2: INK — ライブラリ不使用でGPU流体を書く

INK

やったこと

Active Theory / Lusion 系の「フル画面でカーソルに即反応する」やつ。コンセプトはリアルタイム流体にした。Three.js すら使わず、生のWebGL2 + GLSL だけで Jos Stam の stable-fluids(Navier–Stokes)を実装している。やっていることは、フラグメントシェーダーのパスを float の FBO 間で ping-pong させるだけ。

毎フレームのステップはこの順番:

curl → 渦confinement → divergence → 圧力ソルバ(Jacobi ×20) → 勾配を引く
→ velocityを移流 → dyeを移流 → display

依存ライブラリ0、JSは約16KB(gzip 5KB)。生で書くと中身が全部見えて気持ちいい。

ハマり:かき混ぜると画面が真っ黒になる

速い・長い・交差するストロークで撹拌すると、対向する速度場で染料(dye)が分散して値が ~0 になり、画面が真っ黒に。DENSITY_DISSIPATION(拡散)や SPLAT_FORCE を調整してもイマイチ安定しない。

**決定打は「アンビエント自動スプラット」**だった。0.5秒ごとに、穏やかな色を1点だけ自動で注入する。

function ambientSplat() {
  const c = HSVtoRGB(autoHue, 1.0, 1.0);
  const a = Math.random() * Math.PI * 2;
  splat(rand(), rand(), Math.cos(a) * 180, Math.sin(a) * 180,
        { r: c.r * 4, g: c.g * 4, b: c.b * 4 });
}
// メインループ内
ambientTimer += dt;
if (ambientTimer > 0.5) { ambientTimer = 0; ambientSplat(); }

これで「無操作でも常時カラフルに流れている」状態になり、第一印象も安定性も一気に良くなった。ロード直後から絵があるのは正義。
ホバー時は色相 autoHue を時間で巡回させて、ただのマウス移動でも虹色に反応するようにしている。


作品3: DRIVE — Rapierで物理カーゲームを作る

DRIVE

やったこと

Bruno Simon 系の「操作して探索する3Dワールド」。Three.js でレンダリングして、物理は Rapier(Rust製、WASMでブラウザ動作)。車は Rapier の DynamicRayCastVehicleController(レイキャスト車両=後輪駆動・前輪ステア・サスペンション付き)。低ポリの島に、坂・ぶつかると散らばるクレート・木・岩・コインを置いた。

あとからタイムアタック(光るゲートを順に通る周回+ベストラップを localStorage 保存)、ニトロ、全コイン収集で紙吹雪、も足した。

Rapier vehicle controller で踏んだ罠まとめ

ここが一番ネットに情報が少なくてハマったので、未来の自分のために残す。

controller.indexForwardAxis は getter 専用
代入すると Cannot set property indexForwardAxis ... which has only a getter で例外が飛んで初期化が止まる。Rapierのデフォルト(up=Y, forward=Z)が普通の車のレイアウトに合うので、そもそも触らないのが正解。

body.setAdditionalMassProperties(...) で車が動かなくなる
重心を下げようとこれを呼んだら、車がピクリとも動かず、currentVehicleSpeed も NaN になった。撤去して、重心はシャシーの collider を下にオフセットして下げるだけにした。

// 追加質量APIは使わず、colliderを下げて重心を下げる(転倒対策)
RAPIER.ColliderDesc.cuboid(HX, HY, HZ).setTranslation(0, -0.22, 0)

controller.currentVehicleSpeed が不安定(0 / NaN を返す)
HUDの速度・最高速度キャップ・車輪の回転を全部これに頼ると壊れる。シャシーの body.linvel()(線速度)から出すのが確実だった。

speed: () => { const v = body.linvel(); return Math.hypot(v.x, v.z); }

④「速すぎ&アクセルを離すと前にひっくり返る」
テストで指摘されて、コーストブレーキ(自動ブレーキ)を廃止+最高速度キャップ+低重心+角ダンピングで安定化。減速時の前のめり転倒は、結局「重心の高さ × 急減速」なので、その両方を潰すと収まる。

おまけ:ニトロ

Spaceをブレーキからニトロ(ブースト)に変えた。エンジン力と速度上限を一時的に上げて、ゲージを消費/回復させるだけ。

boosting = !!input.nitro && nitroFuel > 0;
if (boosting && sp2 < NITRO_SPEED_2) engine = ENGINE_FORCE * NITRO_MULT;

排気炎のメッシュを出して、ブースト中だけカメラのFOVを少し上げると、一気に「速い感」が出る。地味だけどFOVの演出は効く。


作品4: WAKE — 物理エンジンを捨てて「海そのもの」を自作する

WAKE ブースト走行

やったこと

DRIVE が気持ちよかったので、勢いで船バージョンも作った。ミニカーを海に浮かべる…のではなく、今度は Rapier(物理エンジン)をあえて捨てて、海面と船の挙動を全部自前で書いた。海は Gerstner 波(複数のサイン波+横方向の変位を重ねた、いわゆる「波っぽい」やつ)。

操作感は DRIVE と同じ(スロットル / 操舵 / Space ブースト / タイムアタック / 収集)だけど、Rapier を外したのでバンドルは 約2.5MB → gzip 146KB に激減した。

キモ:同じ波を「GPU(見える海)」と「CPU(船が乗る海)」で共有する

波を2か所で別々に計算すると、船が水面から浮いたりめり込んだりする。なので波のパラメータは1か所だけで定義して、GPU頂点シェーダーにベイクする式と、JS側の高さ/法線サンプラーを同じ係数から生成する。これで船は「描かれているまさにその水面」に乗る。

// 同じ WAVES 配列から CPU 側の高さを出す(GPU頂点シェーダーも同じ式をベイク)
function sampleHeight(x, z, t) {
  let y = 0;
  for (const w of WAVES) y += w.amp * Math.sin(w.k * (w.dx*x + w.dz*z) + w.omega * t);
  return y;
}
// 船は y = 水面の高さ + 喫水。姿勢は波の法線に向けて傾ける
boat.position.y = water.height(x, z) + RIDE;

物理エンジンなしで「それっぽい」船挙動を作る

舵は速度(=舵に当たる水の流れ)に比例して効くようにして、後進では反転させる。これだけで「止まっているとハンドルが効かない、走ると曲がる」という船らしさが出る。

const flow = Math.min(1, Math.abs(vel) / TURN_FLOW); // 水流=舵の効き
yaw += steer * TURN_RATE * flow * Math.sign(vel || 1) * dt;

罠:海を広く取ると重い → メッシュを船に追従+波グリッドにスナップ

巨大な平面を細かく分割すると重い。なので海面メッシュは船を中心に追従させて狭く保ち、波の位相はワールド座標基準uOffset で渡す)にして「メッシュは動くけど波模様は世界に固定」にする。さらに頂点が視覚的に這うのを防ぐため、メッシュ位置を波のセル幅にスナップする。これで“無限の海”の見た目になる。

航跡(船尾から伸びる泡のリボン)は、船尾の点を毎フレーム履歴に積んで、頂点ごとに alpha をフェードさせた細い帯を張っているだけ。物理エンジンがないぶん、こういう「演出」に手を回せる。


まとめ

  • 4作品とも DNS登録 → Vite静的ビルド → nginx + Let's Encrypt → GitHub Public という同じ型で量産できた。静的サイトなら本当に軽い。
  • ブラウザのWebGLだけで、スクロール演出も、自作の流体シミュレーションも、WASM物理のゲームも、物理エンジンなしの海ゲームも、十分に動く。
  • 物理は「使う / 使わない」どちらもアリ。DRIVE は Rapier に任せて速く形にできたし、WAKE は逆に物理エンジンを捨てたことで挙動も海も自分のコントロール下に置けて、バンドルも軽くなった。題材しだい。
  • そして一番のポイントは、ハマりどころは大体ドキュメントに載っていない細部だったこと。原点周りの公転、加算合成の白飛び、vehicle controller の getter 罠、GPUとCPUで波を共有する……。同じ轍を踏む人の時短になれば嬉しい。

気になった実装があれば、ソースは全部覗けます:

4
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?