形を写すだけでは足りない。遮蔽を装いながら呼吸するカレンダーを、CSS3D(CSS3Dレンダラー)とWebGL(Web Graphics Library、ブラウザで3D描画を行う技術)の二重構造で縫い直した記録です。平滑化の遅れ、1,000ms更新の負荷、pointer-events(マウス操作を受け付けるかどうかの設定)の連打を数字で追いかけました。
対象読者
- CSS3DRenderer(CSS3Dレンダラー)とWebGLRenderer(WebGLレンダラー)を同居させる設計に悩んでいる方
- html2canvas(HTML要素をCanvasに変換するライブラリ)の更新頻度とGPU(Graphics Processing Unit、画像処理を行う装置)転送量のバランスを測りたいThree.js(3Dグラフィックスライブラリ)ユーザー
- マルチレンダラー環境でインタラクション(操作)を制御する具体例を探している人
記事に書いてあること
- CSS3Dカレンダーを1000pxのPlaneGeometry(平面の形状)へ写し取る際の回転同期と追従ラグ(遅延)
- 400〜800pxの球殻へ900粒子を散布し、点光源3本と柱の装飾を脈動させる仕掛け
- html2canvas(scale=2)を1000ms/1500msで回したときのピクセル転送量と負荷の試算
- createInteractionControl()がpointer-eventsを全DOM(Document Object Model、HTMLの構造を操作する仕組み)へ再帰適用する影響範囲
- Hybridパネルのオフセット(位置のずれ)とQuaternion(四元数、3D回転を表現する数学的な仕組み)でのチルト角をどう決めたか
前提を軽く整える
- Three.js 0.170のモジュール読み込みとCSS3DRendererの使い分けが分かる
- html2canvasのscaleパラメータ(拡大率)が描画コストに与える影響を把握している
- WebGLでShaderMaterial(シェーダーを使ったマテリアル)を用いたCanvasTexture(Canvas要素から生成したテクスチャ)の貼り替え経験がある
作ったもの
ヘッダーの動画はCalendar3DをCSS3Dで操作しつつ、WebGL側にCanvasTextureのクローンを貼ったものです。1000pxのPlane(平面)を0.15係数でslerp(球面線形補間、滑らかな回転を実現する数学的な手法)し、リングとリップルで疑似遮蔽(見た目上の遮蔽効果)を補っています。最初のデモで角度追従のラグを体験してから読み進めると理解が早いです。
CSS3Dを複製するとどう歪むか
CSS3Dカレンダーは300×300pxです。html2canvasでscale=2を掛け、360,000ピクセルのCanvasTextureへ変換し、ShaderMaterialのtDiffuse(拡散反射テクスチャ)に渡しています。これを1000ms間隔で更新しても秒間4.0MB弱ですが、500msまで縮めると8MB/sを超え、私のGPUだと顕著に重くなりました。負荷が高すぎる。
滑らかさを欲張ってinterval(更新間隔)を500msに詰めるとテクスチャ更新が描画を奪い、Planeのslerpがワンテンポ止まりました。改めて1000msに戻し、Hybridパネル側は1500msで落ち着かせています。試行錯誤の過程で、更新間隔の調整がパフォーマンスに大きく影響することに気づいた。
粒子と点光がつくる空間の厚み
createAmbientParticles()で900個のPoints(点の集合)を球殻400〜800pxへ散らし、opacity(透明度)を0.45±0.15で揺らしています。加えてcreatePulsatingLight()を3本起動し、baseIntensity(基本光量)=1.6/1.3/1.2にrange(範囲)を足してsin波(正弦波)で揺らす設計です。点滅周期は0.018〜0.027rad/frame。数字で見るとかなり緩やかでした。
光量の揺れはcreateCalendarHalo()にも波及します。リングはlerp(target.position, 0.1)(線形補間、滑らかな移動を実現する数学的な手法)で追従し、ripples(波紋)は3枚のRingGeometry(リングの形状)を周期2.5で膨張させているので、Planeが瞬間移動しても数フレーム遅れて追いつく構造です。
pointer-eventsの再帰トグルで起きたこと
createInteractionControl()は右下ボタンでCSS3DRenderer.domElementのpointer-eventsをON/OFFし、すべてのCSS3DObject.elementに再帰でスタイルを流します。Calendar3Dだけでも12ノード以上を一気に書き換えるため、短い間隔で連打するとDOM計測が跳ねました。
実プロジェクトではON/OFFボタンを押した瞬間にStats(パフォーマンス計測ツール)で計測し、1回の切り替えにつき約0.9ms(Chrome 120、RTX 3060)で収まることを確認。これなら操作を遮断したい場面でも許容できます。
Hybridパネルのオフセット調整
CSS3D側のラベルをそのままWebGL空間へ持ち込みたい。そこでoffsetLocal = (-60, 20, -120)をcssObject.localToWorld()(ローカル座標をワールド座標に変換する関数)に渡し、tiltQuaternion(-8°, 0, 0)(チルト角を四元数で表現)でわずかに前傾させています。depth(奥行き)を大きくしすぎるとPlaneとの遮蔽が破綻するので、scale(拡大率)は1±0.2へ制限しました。
処理全体の流れはPromise.all(複数の非同期処理を並列実行する仕組み)で並列化していますが、順序を把握しないとデバッグが大変なのでフローを整理しました。中盤でCSS3D→WebGLクローン→InteractionControlの順番になっている理由が分かります。
参考になったコード
createCalendarWebGLClone()でやっていることを10行ずつ眺めると、Texture更新と追従処理がどう分離されているかが見えてきます。
// webgl.js(抜粋)
async function updateCalendarTexture() {
const clone = calendarElement.cloneNode(true);
tempContainer.innerHTML = '';
tempContainer.appendChild(clone);
clone.style.transform = 'none';
clone.style.width = '300px';
clone.style.height = '300px';
const canvas = await html2canvas(clone, {
backgroundColor: null,
logging: false,
useCORS: true,
scale: 2,
allowTaint: true
});
const texture = new THREE.CanvasTexture(canvas);
texture.needsUpdate = true;
webglPlane.material.uniforms.tDiffuse.value = texture;
}
updatePlaneBillboard()は同じlerp/slerpを0.15で回し、位置・回転・スケールを同期させています。これらの処理が揃って初めて、CSS3DとWebGLの見た目が破綻せずに同期します。支えるのは小さなlerp(線形補間)とslerp(球面線形補間)です。
実際に試した例
- ネオンテーマではhtml2canvasが1.4ms→2.3msへ増加。filter(フィルター効果)を外すだけで戻りました。
- pointer-eventsトグルは5秒間隔なら安定。100msで叩くとPointsのopacityが跳ねました。
- VideoTexture(動画テクスチャ)とCanvasTextureを同時に回すとGPU待機が増加。順番に処理する設計へ戻しました。
使ってみて
実際に動きを確かめたい方へ。
ビルボード追従デモを開く(全画面表示)
ポイントは以下の2つです。
- CSS3D→WebGLの角度同期はslerp(0.15)で十分。0.3にするとビルボードが揺れます。
- html2canvasのintervalは1000msで安定。500msだとGPU転送が8MB/sを超え、描画が止まります。
同じようにCSS3Dを写し取りたい方は、上記の制約を踏まえて調整してみてください。
まとめ
- CSS3DカレンダーをCanvasTexture化し、1000px Planeへ貼りながら0.15のlerp/slerpで追従させた
- html2canvas(scale=2)の転送量を試算し、500msでは8MB/sを超えて失速することを確認
- 900粒子+点光源3本+リング&リップルで空間の奥行きを補強
- pointer-events再帰とHybridパネルのパラメータを整理し、二重レンダラー構成を安定化
さらに深く学ぶには
最後まで読んでくださり、本当にありがとうございました。今回の数字が誰かの空間演出を少しでも助ければ嬉しいです。