OffscreenCanvas一枚で押し切った midori241 の試運転記
時計HUDを新サイトに貼り付けた初日、動画の上にSVG時計(ベクター形式の画像で作った時計)を重ねたはずなのに、画面に現れたのは巨大な黒い円だけだった。一度つまずいた。動画の読み込みを待つ前に描画ループを回し始めたせいで、テクスチャ(画像を3D空間に貼り付けるためのデータ)は空っぽのまま回転していた。想定が甘い。
midori241は、クラス化する前の試運転版だ。OffscreenCanvas(メインスレッドを塞がずに描画できる技術)、背景動画、SVGテンプレート、CanvasTexture(Canvasの内容を3D空間に貼り付けるためのテクスチャ)、PlaneGeometry(平面の形状データ)をすべて一つの関数に押し込み、Promise(非同期処理を扱う仕組み)でまとめて返した。window.animationCallbacks(アニメーション処理を登録する配列)もまだ無く、配列はモジュールのローカル変数。収入が揺らぐなかで致命的な落ち込みは避けたかったので、全体像を改めて記録する。
作ったもの
巨大な初期化関数でHUD全体を生成し、シーンに1枚のプレーンとして浮かせるだけの構造だ。テクスチャが更新されればneedsUpdate(テクスチャの更新フラグ)が立ち、動画と時刻が同一キャンバスで同期する。
描画はupdateClock()で完結している。カラーテーマもクリックも存在せず、ひたすら動画を描き、その上にSVG文字盤を重ねるだけだった。
// midori241/webgl.js(updateClock抜粋)
// 時計を更新する関数
function updateClock() {
const now = new Date();
// 動画フレームをCanvasに描画
context.drawImage(video, 0, 0, width, height);
// SVGテンプレートを生成してData URLに変換
const svgUrl = `data:image/svg+xml;charset=utf-8,${generateSVGTemplate(time, date)}`;
const img = new Image();
// 画像の読み込みが完了したらCanvasに描画し、テクスチャを更新
img.onload = () => {
context.drawImage(img, 0, 0);
if (interactiveUI) {
interactiveUI.material.map.needsUpdate = true;
}
};
img.src = svgUrl;
}
Promiseで一発生成するフロー
初期化はPromiseにまとめ切っていた。動画がonloadeddata(動画データの読み込み完了イベント)を発火してからプレーン生成関数が呼ばれ、PlaneGeometryが返る。ClockDisplay(時計表示用のクラス)が無いので、非同期処理1本で全部を済ませるしかなかった。
animationCallbacks の未公開問題
midori241ではアニメーション処理を登録する配列がモジュール内に閉じていて、外部から解除する術がない。球体アニメーションや時計更新を突っ込むだけで、テスト用にコンソールから削除したくてもできない。ヒヤッとした。
CSS3Dはまだ軽いレイアウト
カレンダー、時計、TVの3D配置は距離調整を単純に3で割るだけだった。縦向き補正も画面の解像度比率の上限設定もなし。だから縦長端末で奥行きが足りず、HUDが顔に近づきすぎることがある。
デバイスプリセットの未調整ポイント
ブラウザが送る端末情報で判定してモバイル、タブレット、デスクトップを分岐する仕組みはすでにあったが、距離計算は固定式で、縦向き時の補正も距離を1.5倍にするだけ。midori242で本格的に手を入れるまで、視野角はその場しのぎ。
Statsオーバーレイの邪魔
パフォーマンス計測ツールを導入したのはいいが、固定化していないせいでHUDをクリックすると数値パネルが前面に躍り出る。マウス操作を受け付けるかどうかを制御するCSSの設定を切るのを忘れていた。意外だ。ちょっとした怠慢が操作性を壊す。
単一パイプラインの流れ
フロー図にしてみると、関数の肥大化が一目瞭然。メインスレッドを塞がずに描画できる技術の生成から3D空間に配置するオブジェクト追加までが1本の矢印で繋がり、差し戻しが効かない。midori242でクラス化する判断を背中から押してくれた。
スナップショット計測
midori241時点での計測値をまとめると、SVG描画が平均17.9ms(1フレームの処理時間を超える)、最大29.4ms(約1.8倍)という厳しい結果だった。動画と時計を同じキャンバスに描く限り、そこで詰まるのは当たり前だ。
Clock更新が平均17.9ms(1フレームの処理時間を超える)、最大29.4ms(約1.8倍)。プレーン生成関数のコストは平均4.6ms。animationCallbacksの合計は12.8msで、余裕があるように見えた。でもClockと動画の一体化がボトルネックになり、ステップアップが難しいと痛感した。
使ってみて
要点は3つ。メインスレッドを塞がずに描画できる技術の生成と3Dオブジェクト作成を一つの非同期処理で抱え込んだ。アニメーション処理を登録する配列はモジュール内に閉じ、windowへ露出しなかった。パフォーマンス計測ツールのオーバーレイがマウス操作を奪い、HUDを触れなくした。だから次のmidori242で全部分解した。
まとめ
- メインスレッドを塞がずに描画できる技術とSVGを単一関数で処理し、時計と動画を1枚のテクスチャにまとめたが、描画平均17.9ms(1フレームの処理時間を超える)・最大29.4ms(約1.8倍)という重さを招いた。
- アニメーション処理を登録する配列をローカル配列で運用した結果、テスト中に登録解除ができず、球体アニメーションの停止が困難になった。
- CSS3Dレイヤーは距離計算が単純で、縦向き端末ではHUDが近づき過ぎる問題が発生した。
- デバイス最適化設定はモバイル、タブレット、デスクトップの3プリセットのみで、画面の解像度比率の上限設定と縦向き補正が未完成だった。
- パフォーマンス計測ツールのオーバーレイをマウス操作設定無しで貼り付け、HUDのクリックを塞いでしまった。この反省がmidori242の再配線に繋がった。
さらに深く学ぶなら
最後まで読んでくださり、ありがとうございました。