CSS3Dを前面に押し出した midori240 の粒子実験録
midori240は2025-01-28に「CSS3D(HTMLを3D空間に配置する技術)を主役にしてWebGL(3D描画のための技術)は背景装飾」という方針でリリースした。最初のデバッグで15,000個の粒子で作った球体を呼び出した瞬間、Pixel 7のFPS(1秒間に描画できるフレーム数)が41まで崩落。一度つまずいた。CSS3Dに寄りかかり過ぎてGPU(画像処理を担当する部品)とCPU(計算を担当する部品)の役割配分を見失っていた。想定が甘かった。
CSS3Dパネルとコントロールを見せるために、あえてOffscreenCanvas(メインスレッドを塞がずに描画できる技術)を捨て、Promise.all(複数の非同期処理を同時に実行する仕組み)で初期化を一気に走らせた。だがpointer-events(マウス操作を受け付けるかどうかを制御するCSSの設定)を切り忘れたStats.js(パフォーマンス計測ツール)がHUD(画面上に表示される情報パネル)を塞ぎ、操作テストは何度も中断した。収入が不安定な中で、ここまで効率の悪い初期化を続ける余裕は無い。だから傷跡を記録する。
TODOリスト
- 粒子15,000個を抱えた球体生成処理の挙動を再現し、負荷の境界を測る。
- CSS3Dコンポーネントの距離を単純除算で決めた時の歪みを可視化し、後継の改善動機を整理する。
- デバイス最適化設定が縦向き補正を欠いたまま動くリスクを数字で残す。
- Statsオーバーレイがマウス操作を奪う現象をデモ化し、再発防止メモにする。
粒子球体が描いた無駄な豪華さ
CSS3Dを際立たせるための背景なのに、粒子は主役を奪った。初期化と更新に最大33.7ms(1フレームの処理時間の約2倍)。Pixel 7でGPU温度が6℃上がった。やりすぎだ。
粒子生成は波×ねじれ×加算合成を全部盛りした。自分でも制御不能。やりすぎた。
// midori240/webgl.js(createAnimatedSphere抜粋)
// 粒子の数を15,000個に設定
const particleCount = 15000;
// 3D空間の形状データを格納するためのオブジェクトを作成
const geometry = new THREE.BufferGeometry();
// 各粒子の位置情報(x, y, zの3つの値)を格納する配列
const positions = new Float32Array(particleCount * 3);
// 各粒子の色情報(赤、緑、青の3つの値)を格納する配列
const colors = new Float32Array(particleCount * 3);
// 各粒子のサイズを格納する配列
const sizes = new Float32Array(particleCount);
// 各粒子の位置を計算して配列に格納
for (let i = 0; i < particleCount * 3; i += 3) {
const t = i / (particleCount * 3);
// 波のような動きを加えた半径を計算
const radius = 200 + 150 * Math.sin(t * Math.PI * 6);
const theta = t * Math.PI * 12;
const phi = t * Math.PI * 3;
// さらに波の動きを追加
const wave = Math.sin(theta * 5) * 80 + Math.cos(phi * 2.5) * 40;
// 3D空間での位置を計算(球面座標から直交座標へ変換)
positions[i] = (radius + wave) * Math.sin(phi) * Math.cos(theta);
positions[i + 1] = (radius + wave) * Math.sin(phi) * Math.sin(theta);
positions[i + 2] = (radius + wave) * Math.cos(phi);
// 粒子のサイズも波のように変化させる
sizes[i/3] = 2.0 + Math.sin(t * Math.PI * 6) * 3;
}
Promise.allで一気に走らせた初期化
シーン設定、コントロール設定、デバイス最適化の3つの処理をまとめて投げた。環境マップ(背景の光の情報)生成とCSS3D登録も並走。最大680ms(約0.7秒)かかった。時間がかかった。
Promiseが一つでも詰まると起動全体が停止する。ログも遅延も拾いづらい。ヒヤッとした。
距離を除算しただけのCSS3D配置
距離は単純に3で割っただけ。視野角の補正も縦向き補正も無し。縦長端末でHUDが顔にめり込む。力業だ。
pointer-eventsだけで切り替えたインタラクション
HTMLを3D空間に配置する描画エンジンに、マウス操作を受け付けない設定を強制した。ONにすると再帰で全要素へスタイルを適用する。カメラを回転・ズームできるコントロールと取り合いになる。
操作系を切り替えるたびにCSS3Dが再描画され、平均6.8msの追加コスト。地味に痛い。
デバイスプリセットの過信
UA判定(ブラウザが送る端末情報で判定)でmobile / tablet / desktopの3択。portrait倍率は距離を1.5倍にするだけ。Pixel 7でFOV85°、DesktopでFOV75°。中間が無い。
距離計算を固定化した結果、Pixel 7縦持ちでHUD外縁が視界から溢れ、ユーザーは操作バーを探せない。悔しい。
画像無しの環境マップ生成
環境マップ設定は画像が無いと描画用の領域でグラデーションを生成する。100ミリ秒ごとに実行するタイマーでテクスチャの更新フラグを立てる。毎秒10回の更新が描画負荷を押し上げる。
Statsオーバーレイが奪った操作権
パフォーマンス計測ツールを右上へ貼っただけ。マウス操作の設定を切らなかった。カメラコントロールに触る前に計測ツールが反応する。操作検証が進まない。
パフォーマンス計測と反省
Pixel 7で33.7ms(1フレームの処理時間の約2倍)、iPad miniで28.3ms(約1.7倍)、RTX 3070でも14.6ms(約0.9倍)。OffscreenCanvasを捨てた代償は大きかった。
粒子球体の初期化24.6ms、CSS3Dレンダリング12.1ms、カメラコントロール更新6.8ms。全部足すと60ms近い(1フレームの処理時間の約3.6倍)。没にしなかったのが不思議なくらい。
使ってみて
要点は3つ。粒子球体生成は背景に徹するはずが主役を奪い、1フレーム33msまで落ちた。CSS3D距離を単純除算したせいで縦向き端末の没入感が崩壊する。パフォーマンス計測ツールのマウス操作設定を切らず、検証そのものが止まった。midori241以降の全面再設計は、この失敗の積み重ね。
まとめ
- 粒子1.5万個の加算合成(複数の画像を重ねて明るくする処理)を背景に置いた結果、Pixel 7で平均33.7ms(1フレームの処理時間の約2倍)、ドロップ率12%まで悪化した。
- 複数の非同期処理を同時に実行する仕組みで初期化を並列化したが、最長680ms(約0.7秒)のブロックが発生し、起動時のハング要因になった。
- CSS3Dの距離設計を単純に3で割るだけに固定したため、縦向き端末でHUDが視界から溢れた。
- マウス操作の設定を切り忘れたパフォーマンス計測ツールがHUD操作を妨害。UI評価が進まず、検証日が2日も延びた。
- 環境グラデーションを100ms周期で更新し続けたせいで、描画更新コストがフレームごとに0.8ms積み上がった。
さらに深く学ぶなら
最後まで読んでくださり、ありがとうございました。