WWDC Quick Look 💓 By SwiftGGTeam
visionOS で没入型 Web サイト環境を探る

visionOS で没入型 Web サイト環境を探る

元の動画を見る

ハイライト

visionOS の Safari に requestImmersive() API が追加され、Web ページ内の <model> 要素をワンタップで実寸大の没入型 3D 環境へ移せるようになります。Web UI は浮かんだまま表示され、開発者は Swift コードを書かなくても空間 Web 体験を構築できます。

主要内容

Web ページでも VR ができるのか。以前は難しかった

以前 visionOS でユーザーに没入体験を提供するには、実質的に2つの道しかありませんでした。ネイティブ App を書くか、WebXR で互換レイヤーを作るかです。Web 側ではせいぜい 3D モデルを置いて回転して見せる程度で、ユーザーが本当にその場へ「入り込む」ことはできませんでした。

チケット販売サイトが座席からの見え方を見せたい場合、静止画を数枚置くしかありません。ゲーム会社が脱出ゲームの部屋を見せるマーケティングページを作りたい場合も、動画に頼るしかありません。ユーザーは常に画面の外側にいて、空間として体験できませんでした。

今は、数行の JS でフレームを越えられる

Apple は Safari に Immersive API を追加しました。設計は開発者がすでに知っている Fullscreen API にかなり近いものです。

  • requestImmersive():モデルを没入モードに入れる
  • document.immersiveEnabled:ブラウザが対応しているかを検出する
  • document.immersiveElement:現在没入している要素を取得する
  • immersivechange イベント:状態変化を監視する
  • :immersive CSS 疑似クラス:状態に応じてスタイルを調整する

Fullscreen API との大きな違いは、Fullscreen がページ全体を置き換えるのに対し、Immersive API は <model> 要素だけをブラウザの境界から取り出し、物理空間いっぱいに広げることです。Safari ウィンドウは小さくなってシーン内に浮かび、Web UI はそのまま見えます。

つまり、ユーザーが 1:1 の劇場に立ってステージを見ている時も、座席選択ボタンや決済フォームは隣に浮かべておけます。「没入モード」と「平面モード」を何度も行き来する必要はありません。

2つの実例

劇場の座席選択:ユーザーが座席をクリックすると、Web ページ内にその座席からの眺めがインラインで表示されます。「没入プレビュー」ボタンを押すと、ユーザーはその場所に直接立ち、周囲を見回してステージの見え方を確認できます。(00:23)

脱出ゲームのマーケティング:ゲーム公式サイトはすべての 3D プレビューを隠しておき、ユーザーが「脱出部屋に入る」をクリックしてからシーンを読み込みます。シーン内ではテレビが謎めいた映像を再生し、映像が終わると扉が開いて App のダウンロードへ誘導します。(00:50)

詳細

基礎:HTML Model 要素

Immersive API は <model> 要素の上に作られています。この要素はすでに iOS、macOS、visionOS の Safari で利用できます。

最も簡単な使い方です。

<model src="teapot.usdz">
</model>

Environment Map を追加すると、反射がよりリアルになります。

<model src="teapot.usdz"
       environmentmap="kitchen.hdr">
</model>

Environment Map は 360 度の HDR 画像で、シーン周囲の光の情報を捉えます。これがあると、金属や光沢のある表面にリアルな反射が現れます。(01:52)

インラインプレビュー:Web ページ内で 3D を見る

劇場の座席選択サイトでは、まず Web ページ内で座席からの眺めを表示する必要があります。<model> を div に入れます。

<div class="seat-preview">
    <model id="theater"
           src="theater-model.usdz"
           environmentmap="theater-lighting.hdr">
    </model>
</div>

デフォルトでは、モデルは要素の境界に収まるようにスケールされます。そのためユーザーは劇場全体を外側から見下ろすことになり、座席からの視点にはなりません。entityTransform プロパティを調整して、モデルの位置、回転、スケールを変える必要があります。(04:40)

まず変換行列をリセットします。

const theater = document.getElementById("theater");

async function updateModelTransform() {
    await theater.ready;
    const identity = new DOMMatrix();
    theater.entityTransform = identity;
}

updateModelTransform();

重要な点:

  • await theater.ready によってモデルアセットの読み込み完了を保証します
  • DOMMatrix() は単位行列を作り、デフォルト変換をすべて消します
  • entityTransform はインラインレイヤー内でのモデルの空間姿勢を制御します

リセット後、劇場の床はレイヤーの中心にあります。しかし人の目の高さはおよそ 1 メートルなので、モデルを下に動かす必要があります。

const theater = document.getElementById("theater");

async function updateModelTransform() {
    await theater.ready;
    const transform = new DOMMatrix();
    transform.translateSelf(0, -1.0, 0);
    theater.entityTransform = transform;
}

updateModelTransform();

重要な点:

  • translateSelf(x, y, z) の y 値 -1.0 は、下方向へ 1 メートル移動することを表します
  • 座標系は右手系の Y-up 規約で、これは Web プラットフォームの標準です

次に、プレビュー視点をユーザーが選んだ座席に合わせます。各座席の位置と向きを記録した JSON ファイルがあるとします。

function buildTransform(seat) {
    const transform = new DOMMatrix();
    const { x, y, z, ry } = seat;
    transform.rotateSelf(0, -ry, 0);
    transform.translateSelf(-x, -y, -z);
    transform.translateSelf(0, -1.0, 0);
    return transform;
}

重要な点:

  • rotateSelf(0, -ry, 0) は座席の向きに合わせてモデルを回転します
  • translateSelf(-x, -y, -z) はモデルを座席位置へ動かします。符号が反転している点に注意してください。動かしているのはカメラではなくモデルです
  • 変換順序は重要です。先に回転してから平行移動しないと、回転の中心がずれます

没入モードに入る

まずブラウザが対応しているかを検出します。

if (document.immersiveEnabled) {
    immersiveButton.hidden = false;
}

ユーザーがボタンをクリックしたら没入を開始します。

immersiveButton.addEventListener("click", async () => {
    await model.requestImmersive();
});

requestImmersive()click などのユーザー操作イベント内で呼び出す必要があります。(07:16)

座標系の切り替え:インライン vs 没入

インラインモードと没入モードでは、参照フレームが異なります。

モード原点スケール
インラインレイヤーの中心CSS ピクセル
没入ユーザーの足元、床の位置現実世界のメートル

没入環境は Safari ウィンドウの背後から開くため、モデルの主要な内容はユーザーの前方に置き、ウィンドウに隠れないようにします。(07:48)

buildTransform 関数を変更し、没入モードを処理します。

function buildTransform(seat, immersive) {
    const transform = new DOMMatrix();
    const { x, y, z, ry } = seat;
    transform.rotateSelf(0, -ry, 0);
    transform.translateSelf(-x, -y, -z);

    if (immersive) {
        transform.rotateSelf(0, 45, 0);
    } else {
        transform.translateSelf(0, -1.0, 0);
    }

    return transform;
}

重要な点:

  • 没入モードでは 45 度の回転を追加し、ステージが Safari ウィンドウの真後ろに来ないようにします
  • -1.0 メートルの目線高さ補正が必要なのはインラインモードだけです
  • 没入モードの原点はすでに床にあるため、追加の下方向移動は不要です

immersivechange イベントを監視し、状態が変わるたびに再計算します。

theater.addEventListener("immersivechange", () => {
    const isImmersive = !!document.immersiveElement;
    const transform = buildTransform(currentSeat, isImmersive);
    theater.entityTransform = transform;
    document.body.classList.toggle("immersive", isImmersive);
});

重要な点:

  • document.immersiveElement は現在没入モードにある要素を指します
  • 同時に body の CSS class を切り替え、終了ボタンの表示などページレイアウトを調整します
  • ユーザーは Digital Crown でいつでも没入を終了できるため、このイベント監視は必須です

インラインプレビューを隠す:脱出ゲームのシーン

脱出ゲームのマーケティングページでは、Web ページ内に 3D プレビューを出す必要はありません。謎めいた雰囲気を保つため、<model> を直接 display: none にします。

<model id="escapeRoom"
       src="escape-room.usdz"
       environmentmap="room-lighting.hdr"
       style="display: none">
</model>

この方法には実用的な利点があります。アセットが事前にダウンロードおよびデコードされません。大きな環境モデルでは、帯域とメモリを大きく節約できます。(10:53)

ボタンをクリックして脱出部屋に入ります。

const enterButton = document.getElementById("enterButton");
const escapeRoom = document.getElementById("escapeRoom");

enterButton.addEventListener("click", async () => {
    showLoadingAnimation();
    try {
        await escapeRoom.requestImmersive();
    } catch (error) {
        console.log(error);
    } finally {
        hideLoadingAnimation();
    }
});

重要な点:

  • 非表示状態のモデルはその場で読み込む必要があるため、loading アニメーションを表示します
  • try/catch でリクエスト失敗を処理します
  • finally で loading アニメーションが必ず非表示になるようにします

Video Docking

Video Docking は、Web ページ内の <video> 要素を 3D シーン内のテレビ画面へ「吸着」させる機能です。USDZ ファイルに RealityKit 注釈を追加し、テレビ画面を video docking 領域としてマークする必要があります。

Apple はこれらの注釈を追加するための Blender プラグインを提供しています。(12:59)

Web ページ側で動画のフルスクリーンを要求します。

const trailerVideo = document.getElementById("trailerVideo");
const demoButton = document.getElementById("demoButton");

demoButton.addEventListener("click", async () => {
    await trailerVideo.requestFullscreen();
});

動画は自動的に 3D シーン内のテレビ画面に表示されます。光のにじみ効果によって、動画の光が床や壁を自然に照らします。(13:16)

モデルアニメーション

動画の再生が終わったら、フルスクリーンを終了してモデルアニメーション、たとえば扉が開く動きを再生します。

const trailerVideo = document.getElementById("trailerVideo");
const escapeRoom = document.getElementById("escapeRoom");

trailerVideo.addEventListener("ended", async () => {
    await document.exitFullscreen();
    escapeRoom.play();
});

重要な点:

  • document.exitFullscreen() は動画をテレビ画面から切り離します
  • escapeRoom.play() は USDZ に組み込まれたアニメーションを再生します
  • アニメーションは Blender で作成し、書き出し時にタイムラインを保持できます

モデルアニメーションは完全なタイムライン制御に対応します。currentTime プロパティで、動画の再生位置を制御するようにジャンプできます。

// 5 秒時点のアニメーション状態へジャンプ
escapeRoom.currentTime = 5.0;

影の投射

Safari ウィンドウが 3D の床へ影を落とすようにすると、ユーザーは空間内でのウィンドウ位置を理解しやすくなります。これには、USDZ 内で影を受けるメッシュに Scene Understanding コンポーネントを追加する必要があります。

影を受ける専用の低ポリゴンメッシュを別に作ることが推奨されています。複雑な高ポリゴンモデルで影を計算すると、性能コストが大きくなります。(14:49)

パフォーマンス最適化

環境モデルは、単純な物体モデルより大きく複雑になりがちです。最適化のポイントは次のとおりです。

1. 頂点数を減らす

原点から見えないメッシュを削除します。たとえば、脱出部屋の床下構造や、壁に完全に隠れる物体の背面です。

2. エンティティ数を減らす

テーブルとその上の装飾品を1つの Mesh に結合し、多数の独立 Entity を避けます。

3. 低ポリゴンメッシュを使う

影を受ける面には専用の低ポリゴンモデルを使い、高ポリゴンの床を流用しないようにします。

4. Shader を単純化する

ライティングをマテリアルテクスチャへベイクし、Unlit マテリアルを使って実行時のライティング計算を省きます。

5. USDZ を圧縮する

usdcrush ツールでテクスチャを圧縮します。

usdcrush model.usdz -o optimized.usdz

このコマンドラインツールはすべての Mac で利用でき、モデルサイズを大きく減らし、ダウンロード速度を改善できます。(16:38)

Image Controls API

モデル要素以外にも、Image Controls API は Web ページに空間的な広がりを加えられます。<img>controls 属性を追加します。

<img src="panorama.jpg" controls>

visionOS では、ユーザーがパノラマ画像をフルスクリーンで表示でき、画像が周囲の空間を包み込みます。Spatial Photos も同じインタラクションに対応します。(17:28)

重要な示唆

1. EC の商品プレビュー:ユーザーを商品に「入り込ませる」

  • 何を作るか:家具、不動産、自動車などの Web サイトで、ユーザーが 1:1 の 3D シーンに入り商品を体験できるようにします
  • なぜ価値があるか:Immersive API はコンバージョンの高い Web チャネルと空間体験を結びつけ、ユーザーに App のダウンロードを求めません
  • どう始めるか:Blender で USDZ シーンを作り、Web ページに <model> + requestImmersive() を置き、劇場座席選択の entityTransform ロジックを参考にします

2. ゲームのマーケティングページ:公式サイトをゲームの第一ステージにする

  • 何を作るか:ゲーム公式サイトで 3D プレビューを隠し、クリック後にゲームシーンへ直接入り、ストーリー動画を再生し、最後にダウンロードへ誘導します
  • なぜ価値があるか:Session の脱出ゲーム demo は、数行のコードで映画的なマーケティング体験が作れることを示しています
  • どう始めるか<model> を隠して読み込みを節約し、requestFullscreen() で video docking を行い、動画終了後に play() でモデルアニメーションを再生します

3. 文化観光ガイド:Web ページがそのままガイドになる

  • 何を作るか:博物館や観光地の Web サイトで、ユーザーが展示物や名所の前に「立ち」、浮遊する Web UI で説明文を表示します
  • なぜ価値があるか:没入モードでも Safari ウィンドウは見え続けるため、Web UI と 3D シーンが自然に共存します
  • どう始めるかimmersivechange イベントで UI 状態を切り替え、インライン時はサムネイル、没入時は詳しいガイドを表示します

4. オンライン教育:実験室を Web ページへ移す

  • 何を作るか:化学、物理、生物の学習サイトで、学生が仮想実験室へ入り、3D モデルの内部構造を観察できるようにします
  • なぜ価値があるかcurrentTime でアニメーションのタイムラインを制御でき、段階的な実験手順の表示に向いています
  • どう始めるか:USDZ に段階別アニメーションを仕込み、JS の currentTime で再生位置を制御します

5. イベントや公演のチケット販売:座席からの眺めをプレビューする

  • 何を作るか:コンサート、劇場、スタジアムのチケットサイトで、購入前にユーザーが座席に「立って」視界を確認できるようにします
  • なぜ価値があるか:Session の劇場 demo は、JSON の座席データ + buildTransform で実装できることをすでに示しています
  • どう始めるか:各座席の座標と向きのデータを集め、インラインプレビューでは entityTransform で位置決めし、没入モードで本当に「着席」させます

関連セッション

コメント

GitHub Issues · utterances