WWDC Quick Look 💓 By SwiftGGTeam
Explore immersive website environments in visionOS

Explore immersive website environments in visionOS

观看原视频

Highlight

Safari 在 visionOS 上新增 requestImmersive() API,网页中的 <model> 元素可以一键进入真实比例的沉浸式 3D 环境,网页 UI 仍悬浮可见,开发者无需编写 Swift 代码即可构建空间网站体验。

核心内容

网页也能做 VR?以前不行

以前要在 visionOS 上给用户沉浸式体验,只有两条路:写原生 App,或者用 WebXR 做兼容性补丁。Web 端最多放个 3D 模型让用户转着看,没法真正”走进”场景。

卖票网站想让用户预览座位视角?只能放几张静态图。游戏厂商想做个营销页面展示密室场景?只能放视频。用户永远隔着屏幕,没法感知空间。

现在:几行 JS 就能”破框”

Apple 在 Safari 中加入了沉浸式 API(Immersive API)。它的设计直接对标开发者熟悉的 Fullscreen API:

  • requestImmersive() — 让模型进入沉浸模式
  • document.immersiveEnabled — 检测浏览器是否支持
  • document.immersiveElement — 获取当前沉浸的元素
  • immersivechange 事件 — 监听状态变化
  • :immersive CSS 伪类 — 根据状态调整样式

和 Fullscreen API 的关键区别:Fullscreen 会替换整个页面内容,而 Immersive API 只把 <model> 元素拽出浏览器边界,铺满物理空间。Safari 窗口缩小并悬浮在场景中,网页 UI 依然可见。

这意味着用户站在 1:1 的剧院里看舞台时,选座按钮和支付表单就悬浮在旁边。不需要在”沉浸模式”和”平面模式”之间反复切换。

两个真实场景

剧院选座:用户点击座位后,网页内联显示该座位的视角预览。点击”沉浸式预览”按钮,用户直接站在那个位置上,环顾四周看舞台视野。(00:23

密室逃脱营销:游戏官网隐藏所有 3D 预览,用户点击”进入密室”后才加载场景。场景内有电视播放神秘视频,视频结束后大门滑开,引导用户下载 App。(00:50

详细内容

基础:HTML Model 元素

沉浸式 API 建立在 <model> 元素之上。这个元素已经在 iOS、macOS 和 visionOS 的 Safari 中可用。

最简单的用法:

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

加上环境光贴图(Environment Map)让反射更真实:

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

环境光贴图是一张 360 度 HDR 图像,捕捉场景周围的光照信息。有了它,金属和光滑表面会呈现逼真的反射效果。(01:52

内联预览:在网页里看 3D

剧院选座网站需要先在网页里显示座位视角。把 <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 米的 eye-level 平移
  • 沉浸模式的原点已在地板,不需要额外下移

监听 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 退出沉浸,所以必须监听这个事件

隐藏内联预览:密室逃脱场景

密室逃脱营销页面不需要在网页里展示 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> 元素”吸”到 3D 场景的电视屏幕上。需要在 USDZ 文件中加入 RealityKit 注释,标记电视屏幕为视频对接区域。

Apple 提供了 Blender 插件来添加这些注释。(12:59

网页中请求视频全屏:

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 组件。

建议单独创建一个低多边形(Low-poly)网格专门用于接收阴影,不要在复杂高模上计算阴影,否则性能开销很大。(14:49

性能优化

环境模型通常比简单物体模型更大更复杂。优化建议:

1. 减少顶点数

删除站在原点看不到的网格。比如密室地板下方的结构、被墙壁完全遮挡的物体背面。

2. 减少实体数

把桌面和上面的所有装饰品合并成一个 Mesh,避免大量独立 Entity。

3. 使用低多边形网格

阴影接收面用专门的低模,不要复用高模地板。

4. 简化 Shader

把光照烘焙到材质贴图中,使用 Unlit 材质跳过运行时的光照计算。

5. 压缩 USDZ

usdcrush 工具压缩纹理:

usdcrush model.usdz -o optimized.usdz

这个命令行工具在所有 Mac 上都可用,能显著减小模型体积,加快下载速度。(16:38

图像控件 API

除了模型元素,图像控件 API(Image Controls API)也能给网页增加空间维度。给 <img>controls 属性:

<img src="panorama.jpg" controls>

在 visionOS 上,用户可以把全景图全屏查看,图像会包裹在周围空间中。空间照片(Spatial Photos)也支持同样的交互。(17:28

核心启发

1. 电商产品预览:让用户”走进”你的商品

  • 做什么:家具、房产、汽车等网站可以让用户进入 1:1 的 3D 场景体验产品
  • 为什么值得做:沉浸式 API 把转化率高的网页渠道和空间体验结合,不需要用户下载 App
  • 怎么开始:用 Blender 制作 USDZ 场景,网页中放 <model> + requestImmersive(),参考剧院选座的 entityTransform 逻辑

2. 游戏营销页面:把官网变成游戏的第一关

  • 做什么:游戏官网隐藏 3D 预览,用户点击后直接进入游戏场景,播放剧情视频,最后引导下载
  • 为什么值得做:Session 中的密室逃脱 demo 证明了几行代码就能做出电影级的营销体验
  • 怎么开始:隐藏 <model> 节省加载,用 requestFullscreen() 做视频对接,视频结束后 play() 模型动画

3. 文旅导览:网页即导游

  • 做什么:博物馆、景区网站让用户”站”在展品或景点前,配合悬浮的网页 UI 展示文字介绍
  • 为什么值得做:Safari 窗口在沉浸模式下依然可见,网页 UI 和 3D 场景天然共存
  • 怎么开始:用 immersivechange 事件切换 UI 状态,内联时显示缩略图,沉浸时显示详细导览

4. 在线教育:把实验室搬到网页里

  • 做什么:化学、物理、生物课程网站让学生进入虚拟实验室,观察 3D 模型内部结构
  • 为什么值得做currentTime 可以控制动画时间轴,适合展示分步骤的实验过程
  • 怎么开始:在 USDZ 中预置分阶段动画,JS 中通过 currentTime 控制播放进度

5. 活动/演出票务:座位视角预览

  • 做什么:演唱会、剧院、体育场馆票务网站让用户在购买前”站”在座位上确认视角
  • 为什么值得做:Session 的剧院 demo 已经完整展示了实现方式,JSON 座位数据 + buildTransform 即可
  • 怎么开始:收集每个座位的坐标和朝向数据,内联预览用 entityTransform 定位,沉浸模式让用户真正”入座”

关联 Session

评论

GitHub Issues · utterances