WebGLとHTML/CSSの融合システムを完成させた
3D空間にHTMLコンテンツを表示するシステムの総合版を作った。
midori258までの技術を統合。画像80枚以上、動画3本、複数のHTMLパネルを同時に表示できるようにした。
処理は負荷が高かった。でも、動く。
なぜ作ったか
これまで、3D空間にHTMLを表示する実装を繰り返してきた(midori257、midori258)。
各バージョンで、特定の機能を試していた。でも、バラバラ。
統合したかった。全部の機能を1つのシステムで動かしたい。
写真展示で使うことを想定していた。大量の写真を3D空間に配置して、HTMLのキャプションや説明を表示する。
そのためには、複数のHTMLパネル、大量の画像、動画コントローラーが必要だった。
システム構成
3つのHTMLディスプレイモジュールを統合した。
HtmlDisplayOriginal
基本的なHTML表示モジュール。
画像、動画、Canvasを表示できる。インタラクティブ性も対応。
3000×5000pxの大きなパネル。写真ギャラリーのメインビューとして使う。
HtmlDisplayOriginal2
動画コントローラー付きのモジュール。
動画の再生、停止、シークバーを実装。
3000×6000pxのパネル。動画を大きく表示するため。
staticHtmlDisplay
静的なHTMLコンテンツ専用。
アニメーションなし。軽量。テキストや画像を静的に表示する。
1920×1080pxの標準サイズ。
画像の管理
80枚以上の画像を3D空間に配置した。
最初、全部の画像を最初に読み込もうとした。一度つまずいた。
ブラウザのメモリが足りない。80枚×5MBで、400MB以上。
遅延読み込みに変更した。
表示されるパネルの画像だけを読み込む。非表示のパネルは、画像を読まない。
if (placeholder.offsetParent !== null) {
// 表示されている場合のみ画像を読み込む
loadImage(config.url);
}
これで、メモリ使用量が150MB程度に抑えられた。
動画コントローラー
動画の再生をコントロールする機能を追加した。
シークバー
動画の進行状況を表示するバー。クリックで任意の位置にジャンプできる。
seekbar.addEventListener('click', (e) => {
const rect = seekbar.getBoundingClientRect();
const clickX = e.clientX - rect.left;
const ratio = clickX / rect.width;
videoElement.currentTime = videoElement.duration * ratio;
});
動画の長さに対する割合で、再生位置を設定。
再生/停止ボタン
ボタンをクリックで、再生と停止を切り替え。
playButton.addEventListener('click', () => {
if (videoElement.paused) {
videoElement.play();
playButton.textContent = '⏸️';
} else {
videoElement.pause();
playButton.textContent = '▶️';
}
});
シンプル。でも、最初は実装を間違えた。
動画要素がDOM外(-9999px)に配置されているため、直接操作できなかった。
プレースホルダーのIDから動画要素を取得する仕組みを追加。正しく動くようになった。
SVG変換の最適化
HTMLをSVGに変換して、テクスチャとして3Dメッシュに適用している。
この変換が負荷が高かった。大きなパネル(3000×6000px)だと、1フレームあたり200ms以上。
5fpsになってしまう。使えない。
最適化した。
変更検出
HTMLが変わっていない時は、変換をスキップ。
const rawSvg = await this._generateRawSVG();
if (this._lastRawSvg && rawSvg === this._lastRawSvg) return;
this._lastRawSvg = rawSvg;
静的なコンテンツなら、1回変換するだけ。その後は何もしない。
フレームレート制限
15fpsに制限した。
const now = performance.now();
if (this.lastUpdate && now - this.lastUpdate < 1000 / this.fps) return;
this.lastUpdate = now;
60fpsは無理。15fpsなら、動画や時計の更新も何とか動く。
でも、やっぱりカクつく。
完璧じゃない。でも、実用レベルにはなった。
3Dオブジェクトとの統合
HTMLパネルだけだと味気ない。
3Dオブジェクトも追加した。トーラス、多面体、クリスタル。
クリックで断片化して散らばるエフェクトも実装。
function fragmentMesh(mesh) {
const fragments = [];
const geometry = mesh.geometry;
// 各面を独立したメッシュに分割
for (let i = 0; i < geometry.attributes.position.count; i += 3) {
const fragment = createFragmentFromFace(geometry, i);
fragments.push(fragment);
scene.add(fragment);
}
// 断片を散らばらせるアニメーション
fragments.forEach(fragment => {
const velocity = new THREE.Vector3(
(Math.random() - 0.5) * 10,
(Math.random() - 0.5) * 10,
(Math.random() - 0.5) * 10
);
fragment.userData.velocity = velocity;
});
// 元のメッシュを削除
scene.remove(mesh);
}
見た目が派手になった。でも、処理が重くなった。
断片の数を制限(50個まで)して、何とか動くようにした。
ハマったポイント
メモリ不足
画像80枚を全部読み込もうとして、ブラウザがクラッシュした。
「Out of memory」エラー。
遅延読み込みに変更したけど、それでも時々クラッシュする。
画像のサイズを調整した。元のサイズ(7000×5000px)は大きすぎる。
1920×1080pxにリサイズしてから読み込むようにした。
これで、安定した。
SVG変換の失敗
大きなHTMLパネルをSVGに変換する時、たまに失敗する。
「Failed to execute 'drawImage' on 'CanvasRenderingContext2D'」エラー。
原因は、画像の読み込みが完了する前にSVGを生成していた。
画像の読み込み完了を待つ処理を追加。
await new Promise(resolve => {
img.addEventListener('load', resolve);
});
これで、エラーが出なくなった。
パフォーマンスの悪化
全部を統合したら、10fpsまで落ちた。
重すぎる。
各モジュールのフレームレートを個別に制限した。
- HtmlDisplayOriginal: 10fps
- HtmlDisplayOriginal2: 15fps(動画があるため)
- staticHtmlDisplay: 5fps(静的だから更新頻度低い)
これで、全体で平均12fps程度になった。
滑らかではない。でも、使える。
試してみた結果
写真80枚のギャラリー
写真を10枚ずつグループ化して、8つのパネルに分散。
カメラを動かして、各パネルを見て回る。
負荷が高かった。カクつく。でも、一応動く。
メモリ使用量は約200MB。ブラウザの限界に近い。
動画3本の同時再生
3つの動画を同時に再生してみた。
処理が重すぎた。5fpsまで落ちた。
動画は1本ずつ再生することにした。他の動画は停止。
これで、12fps程度に回復した。
HTMLと3Dの混在
HTMLパネルと3Dオブジェクトを同じ空間に配置。
視覚的に面白い。HTMLの情報性と、3Dの表現力が融合する。
でも、バランスが難しい。HTMLが多すぎると、3Dが見えない。3Dが多すぎると、HTMLが埋もれる。
試行錯誤の結果、HTMLパネル3枚、3Dオブジェクト5個までが限界だと分かった。
使ってみて
実際に統合システムを動かしてみて、限界を感じた。
処理が重すぎる。スペックの高いPCでないと、使えない。
でも、技術的には可能だと証明できた。WebGLとHTMLの融合は、ブラウザで実現できる。
ポイントは以下の3つ:
- 3つのHTMLディスプレイモジュールを統合(HtmlDisplayOriginal、HtmlDisplayOriginal2、staticHtmlDisplay)
- 遅延読み込みでメモリ使用量を抑制(全画像200MB以下)
- フレームレート個別制限で平均12fps維持(10fps、15fps、5fpsの組み合わせ)
実用性は低い。でも、実験としては面白かった。
同じような大規模な3D+HTML統合を試している方の参考になれば嬉しいです。
まとめ
今回は、WebGLとHTML/CSSの融合システムを完成させました。
ポイントは以下の4つ:
- 3つのHTMLモジュールを統合(基本、動画コントローラー、静的表示)
- 画像80枚以上を管理(遅延読み込み、1920×1080pxにリサイズ)
- 動画コントローラー実装(シークバー、再生/停止ボタン)
- フレームレート最適化(モジュールごとに5fps、10fps、15fpsと差別化)
処理は重いけど、技術的には実現できた。
次のステップは、パフォーマンス改善。より軽量な実装を模索する必要がある。
さらに深く学ぶには
この記事で興味を持った方におすすめのリンク:
自分の関連記事:
最後まで読んでくださり、ありがとうございました。