Three.js の浮島を静的サイトに載せる

浮島ページは自分のスクリプトだけを読み込みます。シーンは実行時に生成し、Three.js はこのページのチャンクにだけ入り、最初の HTML には空の舞台しかありません。

結論から。3D シーンはページ単位で読み込むなら、静的サイトにそのまま載せられます。浮島ページの HTML に Three.js のコードは一行もなく、高さの決まった空の舞台があるだけです。ブラウザが文書を解析し終えてから、シーンのチャンクをダウンロードし始めます。

空の舞台と、その下にあるもの

骨組みは三層です。墨線の枠と硬いオフセット影を持つ舞台、その中に絶対配置したマウント地点、そして読み込み中・準備完了・失敗の三状態。状態は data-state 属性で切り替え、骨組みもエラー文も再試行ボタンも最初から HTML にあります。スクリプトで生成しません。

これで二つ得します。第一に最初の描画が跳ねないこと。舞台の高さは clamp() で固定なので、スクリプトが届く前からページの高さは最終値です。第二に失敗を説明できること。WebGL のないブラウザでは、空白ではなく短い一文と再試行ボタンが出ます。

載せ方はサイト内のゲームやツールと同じです。静的の殻 + 動的 import() + createApp。

const loaders = import.meta.glob<{ default: Parameters<typeof createApp>[0] }>('./*/Scene.vue');

const loader = loaders['./' + slug + '/Scene.vue'];
if (!loader) throw new Error('isle component not found');
const mod = await loader();
createApp(mod.default, { locale }).mount(root);

import.meta.glob は飾りではありません。Vite がビルド時に独立したチャンクへの対応表へ展開するので、Three.js と五つのポストプロセス、シーン全体はこのページを開いた人だけがダウンロードします。他の 400 ページ余りの成果物には入りません。

すべて実行時に生成する

島にはテクスチャもモデルもありません。岩は手で三角分割した不規則なリングの積み重ねで、頂点色で明暗を付けています。水面と滝は time と night の uniform で動く二つの ShaderMaterial。草、花、苔は InstancedMesh で約千二百インスタンス。柔らかい光のスプライトすら、canvas に放射グラデーションを描いて作ります。

代わりに最初のフレームは重いので、主島の影マップは一度だけ計算して凍結します。

renderer.shadowMap.autoUpdate = false;
renderer.shadowMap.needsUpdate = true;

動くもの —— 衛星、蛍、花びら、雲 —— は影を落としません。1 フレームはおよそ三百回の描画呼び出しと五万三角形で、ノート PC でも最近のスマートフォンでも耐えます。

孔版套印の言語に合わせる

元は独立した暗いページで、そのまま持ってくると紙とインクの配色と衝突します。そこで持ち込んだのはシーンだけ。操作盤は描き直しました。直角、等幅大文字のラベル、ぼかしゼロの硬い影、すべて --c-* トークン経由です。ダークモードはサイトのテーマに従い、シーン自身の「夕暮れ / 夜」とは別物です。片方は紙の色、もう片方は島の空模様です。

左上の小さな一行は実測値です。描画呼び出し数、三角形数、準備状態を 900 ミリ秒ごとにレンダラから読みます。デバッグ用であると同時に、このページの仕様書でもあります。

代償

代償は正直に書きます。Three.js のぶん、このページのスクリプトは数十 KB から約六百 KB(gzip で約 150 KB)に増えます。払うのはこのページを開いた人だけですが、その人は確かに払います。

だからこのページはトップの主役には向きませんし、記事の挿絵に埋め込むものでもありません。通り道ではなく、目的地です。

まとめ

静的ホスティングと 3D は両立します。重いものを自分のルートに閉じ込め、三つの状態をスクリプトではなく HTML に持たせれば、あとはデザインの問題です。

← 記事一覧に戻る

コメント

…