HTML HUDを再配線した midori242 の構造改革日誌
最初に時計表示用のクラスを新サイトへ持ち込んだとき、オフスクリーンの描画が一度も反映されなかった。一度つまずいた。インタラクティブなUI要素の参照を生成前に触ってしまい、テクスチャ更新が空振りした。想定が甘い。
midori242は、HTMLベースの時計と動画パネルを3D描画ライブラリに再接続するための再構成版だ。HTMLを3D空間に配置する技術のカレンダー、動画ボード、ガラス風トップバーを全部載せした状態で、メインスレッドを塞がずに描画できる技術→Canvasの内容を3D空間に貼り付けるためのテクスチャ→マウス位置から3D空間のオブジェクトを検出する仕組みのラインをもう一度洗い直した。収入が途切れた今、フレーム落ちで読者を失う余裕はない。だから記録を残す。
作ったもの
HUDの心臓をもう一度外科手術した結果、1フレームの再描画を平均12.4ms(1フレームの処理時間の約75%)まで抑え、動画パネルと秒単位で色が変わる時計を同居させた。CSS3Dのコンポーネントも前面へ吸い付くように動かし、トップバーは5秒で自動的に縮む。これだ。
時計はメインスレッドを塞がずに描画できる技術にSVGテンプレートを焼き付け、内容を画像データに変換する関数で可視キャンバスへ転送している。クリックするとメッシュクリック処理がマウス位置から3D空間のオブジェクトを検出する仕組みのテクスチャ座標を1920×1080のSVG座標へ射影し、半径140px以内なら色を変える。
// midori242/src/ClockDisplay.js(クリック判定抜粋)
// マウス位置を3D空間の座標系に変換(-1から1の範囲)
const mouse = new THREE.Vector2(
((event.clientX - rect.left) / rect.width) * 2 - 1,
-((event.clientY - rect.top) / rect.height) * 2 + 1
);
// カメラからマウス位置に向かう光線を設定
raycaster.setFromCamera(mouse, this.camera);
// メッシュとの交差を検出
const intersects = raycaster.intersectObject(this.mesh);
if (intersects.length > 0) {
// テクスチャ座標(UV)を取得
const uv = intersects[0].uv;
// UV座標をSVGのピクセル座標に変換
const xSVG = uv.x * this.width;
const ySVG = (1 - uv.y) * this.height;
// ランダムな色に変更
this.mesh.material.color.set(new THREE.Color(Math.random(), Math.random(), Math.random()));
}
動画パネルと描画間隔の握手
動画表示用のクラスは動画をメインスレッドを塞がずに描画できる技術で受け取り、更新頻度でFPSを制御する。90fpsにするとPixel 7で平均9.4ms(1フレームの処理時間の約57%)、60fpsなら6.8ms(約41%)で収まる。意外だ。60fpsから45fpsへ落とすだけでドロップ率が3%→0.7%まで下がった。
動画はCanvasの内容を3D空間に貼り付けるためのテクスチャに載せる前にオフスクリーンでコントラストを調整している。アニメーション処理を登録する配列へ登録されるのは内部更新関数のラッパーで、描画間隔に満たない時はドロップとしてカウント。ヒヤッとした。ここを放置すると時計表示のフレームと競合してしまう。
RaycasterとSVG座標の再検証
UIをクリックすると縮尺が0.8倍に沈み込み、200ms(0.2秒)で元に戻る。テストではマウス位置とSVG座標の誤差が最大1.8px以内に収まった。テクスチャ座標のY軸を反転させる計算を忘れると上半分のクリックが全部無効になる。過去の自分がやらかした罠。
animationCallbacks の渋滞緩和
旧サイトではアニメーション処理を登録する配列へ処理を詰め込みすぎて、総和が16.6ms(1フレームの処理時間を超える)を超えるフレームが続出した。midori242では時計表示、動画表示、球体アニメーションの3本だけに絞り、平均19.2ms(約1.2倍)、最大28.7ms(約1.7倍)。仕方ないが、警告は出す。
CSS3Dコンポーネントの距離感
HTMLを3D空間に配置する技術のカレンダー、時計、TVは、画面比率で距離を決めたあと距離を1/3に縮めてカメラの正面へ引き寄せる。スマホ表示だと縦長補正で距離が1.5倍になるので、その分だけ画面の解像度比率の上限を設定した。
デバイスごとのFOV最適化
デバイス最適化設定はブラウザが送る端末情報で判定後、視野角、カメラ位置、コントロール速度を端末に合わせる。Pixel 7で視野角85度、デスクトップで視野角75度。距離を計算し直した結果、縦向きモードでもHUDが視界から溢れない。
パイプラインと測定結果
処理フローをSVGで図解し、ClockDisplayとVideoDisplayのCanvasTextureがMeshへ貼られる道筋を可視化した。さらにPixel 7 / iPad mini / RTX 3070で測った値を表にまとめ、どこで遅延が発生するかを把握。
時計表示の平均12.4ms(1フレームの処理時間の約75%)、動画表示の平均6.8ms(約41%)、マウス位置から3D空間のオブジェクトを検出する仕組みの平均1.6ms(約10%)。この3値を足してもまだ余力はあるが、アニメーション処理を登録する配列の合計が19ms(約1.1倍)を超えた瞬間にドロップが112フレーム分記録された。ヒヤッとした。
トップバーの時間制御
ナビゲーションは5秒後に自動で縮む。ユーザーがタップするとタイマーをリセットし、ふたたび5秒待ってから閉じる。視界を取らず、でも逃げてほしくない導線のための妥協案だ。
使ってみて
要点は3つ。メインスレッドを塞がずに描画できる技術でSVGと動画を同じテクスチャにまとめる。アニメーション処理を登録する配列は3本までに抑え、合計16.6ms(1フレームの処理時間を超える)を超えるフレームを警告する。マウス位置から3D空間のオブジェクトを検出する仕組みのテクスチャ座標は必ずY軸を反転させる。そうすればHUDは静かに動き続ける。
まとめ
- 時計表示のSVG描画を見直し、メインスレッドを塞がずに描画できる技術→Canvasの内容を3D空間に貼り付けるためのテクスチャ→3Dオブジェクトの経路で平均12.4ms(1フレームの処理時間の約75%)に抑えた。
- 動画表示は更新頻度をUIから調節できるようにし、60fps時の描画時間を6.8ms(約41%)、ドロップ率3%まで管理した。
- アニメーション処理を登録する配列を3系統に限定し、総処理時間が16.6ms(1フレームの処理時間を超える)を超えたフレームを112件まで削減した。
- HTMLを3D空間に配置する技術のコンポーネントの距離計算を視野角と画面の解像度比率に合わせて再構築し、縦向き表示でも視界から零れないようにした。
- トップバーは5秒で自動縮小し、クリック時にタイマーをリセットする挙動を実装して視線を奪い過ぎないナビを実現した。
さらに深く学ぶなら
最後まで読んでくださり、ありがとうございました。