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事件 — 监听状态变化:immersiveCSS 伪类 — 根据状态调整样式
和 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
- 在 visionOS 中设计沉浸式环境 — 学习创建逼真 3D 环境的设计原则
- 空间 Web 的新特性 — 深入了解模型动画时间轴和更多 Web 空间 API
- 优化 visionOS 自定义环境 — 3D 资产优化的进阶指南
- HTML Model 元素入门 —
<model>元素的完整用法和各平台行为
评论
GitHub Issues · utterances