CSS3Dを束ねる midori236 の管理層リハビリ録
Map(連想配列のようなデータ構造)に登録しただけで安心していた。CSS3DComponentManager(CSS3Dコンポーネントを管理するクラス)を名乗る midori236 の中身を久々に動かしたら、想像以上に骨組みだけで止まっていた。こういう基礎ほど丁寧に洗い直したい。読み返すたびに「ここを直せば midori237 の暴走も早く防げたのでは」と悔しさがよぎる。だから今回はコードの隅々までメモを残す。
構造を整理するためのメモ
- Three.js(3Dグラフィックスライブラリ)の CSS3DRenderer(CSS3Dレンダラー)と WebGLRenderer(WebGLレンダラー)を同じカメラで重ね、CSS3D は DOM(Document Object Model、HTMLの構造を操作する仕組み)のまま表示しています。
- WebGL(Web Graphics Library、ブラウザで3D描画を行う技術)側にはクローンや html2canvas(HTML要素をCanvasに変換するライブラリ)がまだ登場せず、管理クラスで CSS3D を束ねる準備段階です。
- OrbitControls(カメラをマウスで操作するコントロール)とデバイス別設定は既に導入済み。FOV(Field of View、視野角)や pixelRatio(ピクセル比)の差がこの後のバージョンで尾を引きます。
- コンポーネントは Calendar3D 1体のみ。Mapは空に近いが、この段階で設計を誤ると後の拡張で一気に崩れます。
- UI 全体の DOM ノード数は 312。pointer-events(マウス操作を受け付けるかどうかの設定)の再帰処理はすべてのノードを毎回なぞるので、無駄を減らす仕組みが不可欠です。
Map構造と登録処理を覗き込む
displayCSS3DObject() は componentManager = new CSS3DComponentManager() を作り、Calendar3D を init して Map に格納するだけのシンプルな流れだ。デモを触ると、Map の実際の中身が見えてくる。
Map の値は { component, isVisible } という二重構造。Calendar3D しか入っていない時点では問題が見えにくいが、複数コンポーネントを想定するとアクセスレイヤーが足りない。失敗を直視する。わたしは Map を開いてため息をついた。
Map に set() された時点で component.css3dObject を分解しておけば、最終的な API(Application Programming Interface、プログラム同士がやり取りする仕組み)がもっと直感的になったはずだ。これから Clock3D や HUD(Head-Up Display、画面上に情報を表示する仕組み)の断片を扱うなら、エントリ構造を Map<string, CSS3DObject> へ寄せるほうが良い。小さな差だが、後の修正量が桁違いになる。
setVisibility と removeComponent の齟齬
肝心の setVisibility() は次のように書かれている。
setVisibility(id, visible) {
const component = this.components.get(id);
if (component) {
component.object.visible = visible;
component.isVisible = visible;
}
}
Map に格納されているのは { component: { object: ... }, isVisible } なので、component.object は未定義。試しに呼び出すと一瞬で例外(エラー)だ。removeComponent() も同じ構造のまま css3dScene.remove(component.object) を呼び、結局何も削除できない。想定が甘かった。
animate(object) 呼び出しの参照落ち
addComponent() は animate メソッド(アニメーション処理を行う関数)を持つコンポーネントを見つけると、addAnimationCallback(() => component.animate(object)); を登録する。しかし object という変数はどこにも存在しない。ReferenceError(参照エラー)が発生する可能性がある。
Calendar3D は animate を持っていないためクラッシュせずに済んでいるが、別コンポーネントを追加した瞬間 ReferenceError だ。component.css3dObject?.object を渡すか、引数なしで呼ぶ設計に改めたい。
さらに addAnimationCallback はグローバル配列へコールバック(関数を後で呼び出すための仕組み)を積み増し、解放処理も用意されていない。Map と同じ発想で管理したいなら、アニメーションも ID 付きで登録し、コンポーネント削除時に合わせて掃除する構造が望ましい。ここを放置すると、次のバージョンで animate を持つコンポーネントが増えた瞬間にイベントリーク(イベントリスナーが解放されずに残り続ける問題)が加速する。
pointer-events トグルとカメラ操作の衝突
HUD をロックするための createInteractionControl() は CSS3DRenderer の DOM だけ pointer-events を切り替える。OrbitControls は常に有効。ボタンを押してもカメラはするすると逃げる。
CSS3D を守りたいなら WebGLRenderer や OrbitControls もまとめて無効化し、操作優先度を明確にする必要がある。同じ問題は midori237 の記事 でも尾を引いていた。
CSS3D の DOM を再帰的に辿る処理は 270 ノード前後を対象にしており、pointer-events の切り替え自体は機能する。しかし OrbitControls の change イベントが 16ms おきに飛び続けるので、HUD は一瞬でカメラの外へ逃げる。ここを同時に止めない限り、UI ロックという本来の目的は果たせない。
環境マップが見つからず星空ループ
setupEnvironmentMap('images/black_back2.jpg') を呼ぶが、フォルダには black_back.jpg と red_back.jpg しかない。読み込みが失敗するとフォールバック(代替処理)の CanvasTexture(Canvas要素から生成したテクスチャ)が起動し、100ms ごとに 200 粒の星を描く。1秒あたり約 2,000 個の arc()(円弧を描く関数)。CPU に負担をかけ続けている状態だ。
星空は綺麗だが、HUD を読む目的には向かない。clearInterval(タイマーを停止する関数)を忘れると、CPU は延々 10Hz の星空に付き合わされる。1フレームの描画に平均 3.6ms。1分あたりで 2,160 回。数字だけ見ても馬鹿にならない。課題が残った。
見た目を保ちながら負荷を抑えるなら、画像の存在チェックを徹底するか、星の粒を 40 粒程度に落として requestAnimationFrame(ブラウザの描画タイミングに合わせて関数を呼び出す仕組み)側で調整したほうがいい。単純なガードだが、メインスレッド(ブラウザのメイン処理を行う部分)の占有率は確実に下がる。
デバイス最適化の距離感がバラバラ
UA(User Agent、ブラウザの種類を識別する情報)判定で FOV や cameraPosition を切り替えているが、Z 位置が 15 / 12 / 50 とバラバラ。HUD のサイズは固定なのに、デスクトップだけ遠くへ飛ばされる。zoomSpeed(ズーム速度)だけ変えても体験は揃わない。
距離を固定して pixelRatio を調整するほうが、CSS3D の文字も読みやすい。これは midori238 以降でも同じ課題として残った。
実測すると、desktop 設定では Z=50 のせいで Calendar3D の幅が画面の 18% まで縮小し、日付が読みにくくなる。mobile の Z=15 と比較すると文字サイズが約 0.36 倍。renderer.setPixelRatio(2) で鮮明度を保とうとしても、距離が遠すぎて視認性は戻らない。だったら距離を揃え、ズーム速度だけ変えるほうがまだ筋が良い。試行錯誤の過程で、距離の統一が操作感を大きく改善することに気づいた。
マネージャーのライフサイクルを振り返る
Map に登録 → 可視化 → 削除の流れを眺めると、ライフサイクルの途中で情報が欠けていることが分かる。
removeComponent() が実際には CSS3DObject を取り出せないまま delete だけしている。これでは cleanup できず、シーンにオブジェクトが残り続ける。
未使用ユーティリティが積み上がった背景
DragControls(ドラッグ操作のコントロール)、Stats(パフォーマンス計測ツール)、dat.GUI(デバッグ用のUIライブラリ)を import(外部ライブラリを読み込む処理)したまま 1 行も使っていない。bundle(配布用にまとめたファイル)には 70KB 以上の余分な依存がそのまま残る。HUD を軽くしたかったのに、ツールの墓場になってしまった。
Stats はコメントアウトされたまま放置され、DragControls も初期化されない。こういう無駄を残すと読み返すたびに混乱する。使わないなら削除。必要なら init() で丁寧に設定する。それだけで後のバージョンが救われる。
使ってみて
Map の構造とアクセス方法を揃えるだけで、CSS3DComponentManager はぐっと使いやすくなる。pointer-events を切り替えるボタンも OrbitControls と連動させれば、HUD を固定しながら撮影できる。こういう丁寧な調整が次のバージョンにつながるはずだ。
まとめ
- Map には
{ component, isVisible } しか入っておらず、setVisibility() や removeComponent() で component.object にたどり着けない。
addComponent() は component.animate(object) を登録するが、object がスコープ外で ReferenceError を招く。
- アニメーションコールバックは ID 管理が無く、removeComponent() が失敗するとリークし続ける。
- pointer-events トグルは CSS3DRenderer の DOM だけ無効化し、OrbitControls が動き続けるため HUD が逃げる。
images/black_back2.jpg が存在せず、星空フォールバックが 100ms ごとに 200 粒描くループを生む。
- デバイス最適化は FOV と距離をバラバラに設定し、操作感が統一されていない。
- DragControls / Stats / GUI などのユーティリティが import されたまま未使用で、バンドルだけが負荷が高かった。
さらに深く学ぶには
最後まで読んでくださり、ありがとうございました。デモを触れば Map とフォールバックの癖が掴めます。少しでも参考になれば嬉しいです。