CSS3Dカードが沈黙した midori233 のクリック迷子録
一度つまずいた。カードをクリックしても裏面が見えない。これは課題。midori233 は CSS3DRenderer(CSS3Dレンダラー)で「おしゃれなカード」を回転させる実験だったが、マウス操作を受け付ける設定(pointer-events)の初期値を none にしたまま放置したせいで HUD が完全に無反応になった。50歳で再挑戦中の身としては、こういう基礎実験でつまずいた理由を丁寧に洗い直したい。midori234 以降の改善につながった反省点を記録する。
作ったもの
Three.js の WebGL シーンに CSS3DRenderer を重ね、CSS3Dに追加する処理で 400px 四方のカードを 3D 空間へ配置した。カードは前面と背面を持ち、クリックするとカードのスタイルのtransform(変形)を rotateY(180deg) に切り替える構成だ。カード前面とカード背面の双方にテキストを入れ、CSS3Dオブジェクトを 0.5 にスケールして Z=-100 に浮かせている。WebGL 側には赤い球を追加し、背景にはグラデーションと星空フォールバックを描く。動画 01.mp4 では HUD と球体が同じ位置で重なり、不思議な陰影が付いている。だが実際に触るとカードは沈黙したままだ。CSS3Dレンダラーの DOM が入力を遮断しているため、どれだけ UI を作り込んでもユーザーは裏側へ辿り着けない。
クリックできない理由を掘り下げる
マウス操作の設定(pointer-events)が常に none
CSS3DレンダラーのDOM要素のスタイルでマウス操作を受け付けない設定(pointer-events: none)を初期化でセットしたうえ、その子要素である .css3d-container と .css3d-card もボタンが押されるまでマウス操作を受け付けない設定のままだ。つまり最初はもちろん、トグルしてもレンダラーの設定が邪魔をし続ける。注意が必要だ。
DOM を計測すると CSS3DRenderer の配下には 312 ノードがぶら下がっており、そのすべてにマウス操作の設定を配布しようとしていた。けれど親が none である限り、どれだけ再帰処理を回してもイベントは上層で食い止められる。結果としてカーディナルな UI 制御は一度も DOM イベントに届かず、HUD の実験はゼロ歩のまま止まった。
もともとレンダラーのマウス操作の設定を無効化したのは WebGL のカメラ制御(OrbitControls)を優先させるためだが、CSS3D を操作したい瞬間には逆に邪魔になる。理想は、HUD 操作中だけカメラ制御を無効にしてマウス操作の設定をレンダラー側へ戻すこと。そうしない限り、CSS3D と WebGL の共存はいつまでも実現しない。
初期状態で HUD が完全に遮断される
ボタンを押さない限り .css3d-container も .css3d-card もマウス操作の設定(pointer-events)を受け取らない。結果としてカードの裏面へ切り替える処理は一度も発火しない。デモを見ると分かる通り、HUD は最初から沈黙している。
しかもボタンが押されてもレンダラーが none のままなので、実際には状態が変わったように見えてもカードには一切届かない。ボタンのテキストが「インタラクション: ON」に変わるだけで、体験は何も改善しない。UI が反応しないと、ユーザーはページを閉じるしかない。HUD の最優先事項は最初のクリックが通ることだと痛感した。
CSS3Dに追加する処理の戻り値を破棄している
CSS3Dに追加する処理は CSS3DObject を返すのに、呼び出し側では値を受け取っていない。カードを複数追加したり削除したりする発想がそもそも組み込まれていないのだ。参照が無いので CSS3Dシーンからオブジェクトを削除することも、オブジェクトの要素を検索することもできない。HUD を差し替える余地が無いまま実験を終えてしまった。
カードのトグル処理は DOM を直接操作しているものの、CSS3DObject の transform(変形)や位置を変更したい場合は Three.js の API を呼ぶ必要がある。戻り値を握りつぶした瞬間に、その可能性をすべて捨ててしまった。
CSS3Dに追加する処理を改修するなら、返却値を配列へ積むコンポーネントマネージャー(ComponentManager)を導入し、カードの削除・入れ替え・アニメーション停止を一括で扱えるようにするのが良い。midori236 で同種の仕組みを作ったのは、この失敗を踏まえての判断だった。
赤い球が CSS3D を覆い隠す
原点に球を追加する処理は半径64のメッシュ(3Dオブジェクト)を Z=-100 に追加する。CSS3Dカードも Z=-100 にあるため、WebGL の球が真正面から HUD を覆い隠す。球はデバッグ用のはずが HUD の主役を奪ってしまった。
Promise.all(非同期処理の並列実行)に詰め込みすぎ
Promise.all(複数の非同期処理を並列実行する仕組み)へ 6 個の初期化タスクを一気に投入した結果、HUD の表示まで 1.2 秒ほど待たされる。環境マップのフォールバックが終わるまで CSS3DObject も表示されない。ブラウザのパフォーマンス記録を見ると、環境マップを設定する処理が完了するまで CSS3Dオブジェクトを表示する処理の結果が DOM に現れず、ユーザーには真っ暗な画面が表示され続ける。
理想は HUD の描画を最優先にし、環境マップの読込やドラッグ制御(DragControls)の導入は後から行うことだ。Promise.all を乱用すると失敗時にどのタスクが止まったのか判別しづらく、今回のように星空フォールバックが走り続けていることにも気付けなくなる。
環境マップが見つからず星空が10Hzで走る
環境マップを設定する処理で'images/black_back2.jpg'は存在しないファイルを参照し、CanvasTexture(キャンバステクスチャ)の星空が 100ms(0.1秒)ごとに更新フラグを呼び続ける。カードを置くだけの HUD にしては GPU も CPU も忙しすぎる。描画 1 回あたり平均 3.6ms(約0.004秒)、1 秒で 10 回、1 分で 600 回。これでは HUD の性能測定以前に負荷が天井へ張り付く。
Promise.all に初期化処理を詰め込み、HUD の表示が 1.2 秒近く遅延する。
タイマー(setInterval)を止める処理が無く、HUD を外してもタイマーが走り続ける。
デバイス最適化の距離がバラバラ
mobile は Z=15、desktop は Z=50。カードのサイズは固定なのに、デスクトップだけ妙に遠くへ飛ばされる。読みやすさを求める HUD では距離を揃えるべきだった。
ドラッグ制御(DragControls)/ GUI / Stats は未使用
ドラッグ制御(DragControls)、GUI(グラフィカルユーザーインターフェース)、Stats(統計表示)をインポートしているが、一度も呼ばれない。HUD は軽くしたかったのに、不要なモジュールまで読み込んでバンドルを太らせてしまった。
使ってみて
HUD の操作を有効にするには、renderer.domElement から pointer-events を戻すだけで良かった。グラフィックを盛り込む前に、最初のクリックが効くかどうかを確かめる。たったそれだけの確認を怠った結果、カードは沈黙した。次の段階では pointer の扱いとクリーンアップを最優先で直すと決めた。
まとめ
- CSS3DRenderer の DOM が常時マウス操作を受け付けない設定(pointer-events: none)で、HUD には一切操作が届かない。
.css3d-container と .css3d-card も初期値が none のため、ボタンを押すまでカードが不動のまま。
- WebGL の赤い球が CSS3Dカードと同じ位置に置かれ、HUD が視覚的に埋もれる。
- Promise.all に初期化処理を詰め込み、HUD の表示が 1.2 秒近く遅延する。
- 環境マップが見つからず、星空フォールバックが 100ms(0.1秒)ごとに CanvasTexture を更新し続ける。
- デバイス最適化は Z を 15 / 12 / 50 に分け、デスクトップでカードが小さくなり過ぎる。
- ドラッグ制御(DragControls)/ dat.GUI / Stats をインポートするだけで未使用。バンドルの無駄が増えている。
Stats だけでも 33KB、dat.GUI は 68KB。モバイル回線ではそれだけで 0.2 秒以上の遅延を生む。HUD 実験の段階で無駄な依存を増やすと、後のバージョンでも削除しにくくなる。
さらに深く学ぶには
最後まで読んでくださり、ありがとうございました。デモを触ると「クリックできない HUD」がどれほど致命的かが掴めます。次の実装では renderer と CSS3D の入力経路を最初に整え、HUD の基礎を固めてから表現を盛り込む方針で進めます。