WebGLとHTMLを束ねた三層ショーケースの総仕上げ
3D空間にHTMLを並べる実験のまとめ版だ。動画と写真と静的パネルを同じ座標系に詰め込んだ。一度つまずいた。最初はコントロールできずに酔った。それでも仕上げ切った。
入口で押さえておきたい要素
- Three.js:
CatmullRomCurve3 と複数ライトで空間を構成する土台
- OffscreenCanvas: SVGをキャンバス化し、CanvasTextureに載せる経路
- Raycaster: UVからSVG座標を逆算してボタンの矩形へマッピング
新しい方のサイト記事だけを読んでも理解できるように、体験デモを冒頭から置いてある。
三枚のパネルをPromiseで同時起動する
setupInteractiveUI() は Photo_html_Display、mp4_control_Display、statichtml_Display を Promise.all で一気に初期化する。1024px幅にはしない。planeWidth=150 のプレーンに scale=2 を掛けているから、等倍の写真でも余裕がある。
配置は (0,0,0)、(50,350,-100)、(0,-150,30) の三点だ。CatmullRomの基準点と同じ座標を使っているので、球体がパネルの間を漂っても干渉しない。最初は await を入れずに書いてしまい、動画パネルだけ遅れて真っ黒のまま残った。ここは課題。描画前に必ず await Promise.all でテクスチャ更新が完了するまで待つように直した。
デモを全画面で試す と三パネルの起動シーケンスがそのまま見える。
写真ギャラリーは300pxでBASE64化する
写真表示用のクラスは画像をBASE64形式に変換する関数で1920pxの写真も幅300pxに落としてエンコードする。画像の影を 20→100px に広げるアニメーションはアニメーションクラスで3000msずつ往復。想定が甘かった。シャッフル中フラグを付け忘れて二重再生になり、ボタンを連打するとGPUが悲鳴を上げた。フラグで二度目の押下を無視するようにして収まった。
画像は4枚とも images/20220703-*。BASE64化しても一枚あたり約95KB。写真展示に使うつもりなので、解像感が落ちるのは承知で帯域優先にした。輪郭が分かるギリギリのライン。
動画の描画範囲をSVGから逆算する
動画プレイヤーはSVG内に本物の<video>を置かず、動画コンテナの矩形を取得する関数で .video-container の矩形をDOMから拾っている。画像を描画する関数の引数は x=768、y=162、幅864、高さ486。意外だ。もっと左寄りかと思っていた。
クリック判定は interactiveElements の矩形とRaycasterのUV。contains(xSVG, ySVG) に入る前に this.videoElement.currentTime ±= 10 を実行している。巻き戻しは Math.max(currentTime-10, 0)。10秒戻しすぎるとフリーズするバグがあったが、現在時刻が0.1秒以下のときは 0 に固定することで回避できた。
タイムライン演出をアニメーションクラスで管理する
写真表示用のクラスのボタンを押すとアニメーションが二段重ねで入る。線形イージングしか使っていないが、画像の角丸を0→150pxに膨らませて戻す往復をここに仕込んだ。3000ms×2で合計6秒。最初はアニメーションフレームを直接呼ぶ関数が増殖して破綻したので、アニメーションの進捗更新で揃えた。
アニメーション間隔を 16ms 未満に詰めようとしても this.fps=15 の制限でドロップする。lastUpdate との比較で 66ms ごとにしか更新しないようになっているためだ。短くすれば滑らかになるが、OffscreenCanvasの再描画コストが一気に跳ね上がったので15fpsに留めた。
FPS計測はStatsを一時的に復帰させて実測した
パフォーマンス計測ツールの初期化はコメントアウトされている。そこで検証時だけ復帰させて平均フレーム時間を取った。デスクトップ(GeForce RTX 2060)では平均16.1msで約62fps、CPUは24%。モバイル(iPhone 13)は32.4msで約31fpsまで落ちる。想定が甘かった。動画パネルとギャラリーを同時に動かすとHTML化テクスチャがボトルネックになる。
画像を描画する関数の回数を制限し、動画がデータ準備完了になるまで return するようにしたことで、モバイルの平均は40fpsまで回復した。ヨシ。
CatmullRomCurve3 で球体を漂わせて視点誘導する
球体を配置する関数では球体数20個が速度0.001〜0.003で曲線上を回る。基準点は3つの基準点と乱数で追加した3点の合計6点。曲線から位置を取得し、パラメータが1を超えたときに0に戻してループさせる。半径10の球体がプレーンの前面で揺れるので、静的HTMLが奥に引っ込んでいても存在感が出る。
乱数幅を ±200 にした結果、球がプレーンに突き刺さることがあった。一度つまずいた。アニメーションコールバックを追加する関数の中で、球体のZ座標を -20 以上に制限して衝突を避けた。
デバイス別にズームと操作感を切り替える
デバイス最適化設定関数はユーザーエージェントでモバイル・タブレット・デスクトップを判定する。PCは視野角75、カメラ位置Z=50、画面の解像度の比率2。モバイルは視野角85、Z=15、画面の解像度の比率1.5。パンとズーム速度も0.7〜1.0で切り替える。タッチ操作の設定を1本指で回転、2本指でズーム・パンに固定したのでスマホ操作でも破綻しない。
リサイズイベントでは camera.aspect と renderer.setSize を更新しているが、renderer.setPixelRatio は初期値のままだ。ブラウザを拡大してもボケないが、超高解像環境では伸びる余地がある。
全体の流れをフロー図で俯瞰する
初期化→UI生成→曲線アニメーション→操作イベントという流れを demo8 にまとめた。stepNodes を配列で保持し、updateView() でアクティブなステップだけ背景色を変える。setInterval で activeIndex を巡回させ、二周(steps.length*2) で自動停止する。
中堅の開発者でも流れを見失わないように、失敗した分岐は透明度を上げずにテキストで書き残した。
実際に触ってみたケース
- RTX 2060 / 1920×1080: FPSは62前後。動画をポーズするとCPU負荷が18%まで下がる。光源4灯でもまだ余裕がある。
- iPhone 13 Safari: 30fps付近まで落ちたが、
play/pause ボタンは指先でも押しやすい。動画がシーク完了するまでギャラリーのアニメは止まる。
- 低速ノートPC(Core i5-8250U):
pixelRatio を1.0に固定すると平均48fps。OffscreenCanvasがない環境では自動的に <canvas> を生成して問題なく動いた。
使ってみて
実際に操作して違和感がないかを確認しながら使ってほしい。全画面モードで最初にボタンを押すと、アニメーションのテンポが掴みやすい。
デモを全画面で起動(動画のコントローラーが反応する)
ポイントはこの3つ。
- 写真は300pxでBASE64化し、OffscreenCanvas経由でCanvasTextureに転送する
- 動画はSVGから算出した領域へ
drawImage で重ね、押下時に10秒単位でシークする
CatmullRomCurve3 とデバイス設定が視線誘導と操作感を両立させる
同じようにHTMLと3Dを混在させたい人の参考になれば嬉しい。
まとめ
今回は、WebGLとHTMLを束ねた三層ショーケースの全体の流れを説明しました。
ポイントは以下の4つ:
- 三枚のHTMLパネルの同時初期化:Promiseで同時初期化し、scale=2の平面に貼って統一感を出した
- ギャラリーの最適化:300pxに縮小してBASE64化、二段アニメーションで光量を調整した
- 動画プレイヤーの実装:SVGレイアウトから描画範囲を逆算し、巻き戻し・早送りをマウス位置から3D空間のオブジェクトを検出する仕組みで拾った
- 球体とカメラ設定:CatmullRomCurve3の球体と機種別カメラ設定で、位置関係と操作感を柔らかく整えた
まずは全体の流れを理解することが重要です。同じようにHTMLと3Dを混在させたい人の参考になれば嬉しいです。
さらに深く学ぶには
最後まで読んでくださり、ありがとうございました。