环境混合模式
一个属性 —— session.environmentBlendMode —— 告诉你用户能不能看见内容背后的真实房间。而在它的三个取值之一里,黑色不是一种颜色,是你场景上的一个洞。
同一帧画面,在三种显示器上
渲染一次,用三种方式合成。把物体亮度往下拖,盯住中间那块:在加色显示器上,暗像素不发光,所以物体不是变暗 —— 是不再存在。
这个演示需要 WebGL,你的浏览器没有提供。下面的正文独立成篇,不看演示也能读完。
一个属性,三种截然不同的显示器
每个 XRSession 都有 environmentBlendMode,一个只有三种取值的字符串。它描述的是硬件的物理特性:这块显示器是挡住真实世界、往上加光、还是与之混合。这不是偏好设置,也不是你能设的东西 —— 它是用户眼前那块屏的性质,你只能读。
大多数开发者从来没读过它,因为大多数开发都在同一台头显上进行,答案永远不变。结果是一类要等到别人用不同硬件打开你的页面才会出现的 bug,而它一出现就是灾难性的:内容整个看不见,或者本该是房间的地方是一块黑板。
这个值在会话一开始就可读,所以可以在初始化时分支一次,不必每帧判断。要分支的东西主要是三样:清除色、光照模型,以及**要不要画背景**。
三个取值各自是什么意思
这些名字描述的是合成操作,按合成来理解,后果就一目了然了。
- opaque
- 显示器完全挡住真实世界。你这一帧就是用户看到的全部,alpha 被忽略,而且你必须画背景,否则用户看到的是缓冲区里残留的东西。所有 VR 头显在普通 VR 模式下都是这个 —— Quest、Index、Vive。
- additive
- 显示器只能往真实世界上**加**光,减不了。合成器算的是「真实 + 你的」。黑色加不了任何东西,所以黑色就是透明;也因此没有任何办法画阴影或压暗什么。光波导显示器就是这样工作的,比如 HoloLens 和 Magic Leap。
- alpha-blend
- 合成器按你的 alpha 通道,把你这一帧与房间的摄像头画面混合。黑色是真的黑,alpha 为 0 就露出房间,一切都符合你对普通合成的预期。这是摄像头透视 AR —— Quest 3、Vision Pro、手机 AR。
分支一次就够
整套适配通常就三四行。问题不在于难,在于**太容易一直不写**。
const session = await navigator.xr.requestSession('immersive-ar', {
optionalFeatures: ['local-floor'],
});
// 这是硬件的属性,不是设置项。启动时读一次。
switch (session.environmentBlendMode) {
case 'opaque':
// 什么都透不过来,所以要画出一个世界。alpha 会被完全忽略。
renderer.setClearColor(0x101018, 1);
scene.background = skybox;
scene.add(environmentLighting);
break;
case 'additive':
// 清成黑色 —— 在这种显示器上,黑色的意思是「不发光」,
// 而那恰恰就是你想要的透明。
renderer.setClearColor(0x000000, 1);
scene.background = null;
// 阴影和深色材质画不出来:你只能加光。
// 改用明亮、高对比、以线条为主的内容。
disableShadows();
break;
case 'alpha-blend':
// 清成全透明,好让摄像头画面透过来。
renderer.setClearColor(0x000000, 0);
scene.background = null;
// 真正的遮挡需要深度感知;没有它,
// 你的内容永远浮在房间前面。
break;
}在 three.js 里,让 alpha-blend 这一支真正生效的是 renderer.setClearAlpha(0) 加上一个 alpha:true 的上下文。没开 alpha 创建的 renderer 会静默产出一帧不透明的黑,在透视设备上那就是把整个房间遮住了。
为什么 additive 坏得悄无声息
意外都出在 additive 这一支,因为它推翻了几乎每个渲染器里都默认成立的一条假设:你可以把一个像素变暗。你不能。显示器是在往已经进入眼睛的光上继续加光子,而没有任何机制能把光子拿走。
后面的一切都由此而来。阴影不可能 —— 阴影就是变暗。深色 UI 面板是隐形的。边缘抗锯齿渐变到黑就等于渐变到无,所以细的深色线条会消失。天空盒不只是浪费,而是主动做错事:它等于往整个房间泼一层光。而且对比度是在跟环境竞争 —— 房间越亮,你的内容活下来的部分越少。
能在加色硬件上成立的内容,看起来往往像平视显示器(HUD):明亮、饱和、高对比、线条多于填充,不依赖深色来传达任何含义。这是**设计约束**,不是一个渲染设置 —— 所以晚发现的代价很大。
上面这个演示在做什么
场景只渲染了一次,渲进一张带 alpha 通道的纹理 —— 那正是一个 WebXR 帧的样子。三块面板随后用规范里写的三条公式,把这同一张纹理合成到一张房间图上:opaque 那块无视房间,additive 那块做加法,alpha-blend 那块按 alpha 混合。没有任何一块是单独伪造的,它们只在混合方式上不同。
值得拖的是物体亮度。当它接近 0,opaque 面板显示的是深色背景上的深色物体 —— 还看得见,还在那儿。additive 面板则**什么都没有**,因为黑色像素贡献不了光。alpha-blend 面板里物体仍然可见,是一个真正的深色形状 —— 那里 alpha 和颜色是分开的。
环境亮度是第二课。往上拖,additive 面板会比另外两块早得多地被冲淡 —— 这就是光波导头显在户外吃力的原因,也是它们的内容都设计得明亮而稀疏的原因。
哪些硬件报告什么
同一台设备在不同会话模式下报告的值不同,这是最常被忽略的细节:Quest 3 在 VR 下是 opaque,在 AR 下是 alpha-blend。
| 设备 | immersive-vr | immersive-ar | 显示方式 |
|---|---|---|---|
| Meta Quest 2 | opaque | alpha-blend | LCD + 摄像头透视 |
| Meta Quest 3 / Pro | opaque | alpha-blend | 彩色摄像头透视 |
| Apple Vision Pro | opaque | alpha-blend | 摄像头透视 |
| HoloLens 2 | — | additive | 光波导,只能加光 |
| Magic Leap 2 | — | additive | 光波导 + 分区调光 |
| Android 手机 AR | — | alpha-blend | 摄像头画面在 canvas 之后 |
数据核对于 2026-09。Magic Leap 2 的分区调光能缓解但不能取消加色这一限制。依赖任何一行之前,请对照 WebXR 规范与各厂商文档复核。
最费时间的几个坑
这些在你自己开发用的那台头显上全都能过 —— 这才是它们代价高的原因,而不只是「写错了」。
- 从不读 environmentBlendMode
- 默认假设就是「我这台头显是怎样的」。下面每一条都是这一个疏忽的后果。
- 在 AR 里画天空盒
- 在 alpha-blend 上它把房间替换成你的背景;在 additive 上它往用户视野里灌一片光。两种情况下 AR 都没有了。
- 透视会话里用不透明的清除色
- 没开 alpha 创建的 renderer,或者 clear alpha 为 1,产出的是一帧实心画面。摄像头画面在它后面,用户看到的是一堵黑墙。
- 在 additive 上依赖阴影或深色 UI
- 这两样都是减法。在光波导显示器上,投影是隐形的,深色面板是个洞,所以任何靠明暗区分元素的布局都会垮掉。
- 在昏暗房间里测对比度
- 加色内容是在跟环境光竞争。深夜在书桌前完美可读的东西,中午在窗边可能什么都看不清。
延伸阅读
这一条的规范原文很短,而且写得意外清楚 —— 值得读全文,不要只看摘要。
- W3C — XRSession.environmentBlendMode — 规范性定义,含三条确切的合成公式。
- MDN — XRSession.environmentBlendMode — 实用参考,逐个取值给了处理建议。
- AR 命中测试 — AR 的另一半:把内容放到运行时已识别出的表面上。
- WebXR 会话 — 会话从哪来,以及为什么 immersive-ar 会改变你能画什么。
- 光照估计 — 先把光算对,再由混合模式决定这些像素有多少能到达眼睛。
- 深度感知 — 真实表面到底能不能挡在你的内容前面。
- 立体渲染 — 你正在合成的那一帧究竟是什么:视图、视口、一张 framebuffer。