25、WebRTC与AR/VR:WebXR与WebRTC结合、空间音频、3D场景共享
说实话,WebRTC 能做到音视频通话,大家已经不稀奇了。但要是把实时通信塞进 AR/VR 里呢?你想想看,两个人戴着眼镜,在同一个虚拟空间里走动,声音会随着距离变化,还能一起操作一个 3D 模型——这才是真正的“面对面”。
这一章,我们就来聊聊 WebRTC 和 WebXR 怎么结合,空间音频怎么落地,以及 3D 场景共享的那些坑。我踩过的坑不少,希望能帮你省点时间。
WebXR 与 WebRTC:两个世界的握手
WebXR 是浏览器里的 AR/VR 标准,它管的是“渲染”和“追踪”。WebRTC 管的是“传输”。两者结合,说白了就是:把 3D 场景里的位置、姿态、音视频流,实时同步给远端用户。
我在项目里遇到过最典型的需求:两个人在 VR 里开会,A 能看到 B 的虚拟化身,B 能听到 A 的声音从正确方向传来。这背后就是 WebXR 提供位置数据,WebRTC 负责传数据通道和音频流。
position 和 quaternion,通过 WebRTC 的 DataChannel 广播出去。远端收到后,更新本地 3D 场景里的化身位置。
具体怎么做?我习惯用 requestAnimationFrame 循环里读取 XR 帧的 viewerPose,然后通过 DataChannel 发送。代码大概长这样:
// 在 XR 帧循环中
function onXRFrame(time, xrFrame) {
const pose = xrFrame.getViewerPose(xrReferenceSpace);
if (pose) {
const pos = pose.transform.position;
const rot = pose.transform.orientation;
const data = {
type: 'pose',
x: pos.x, y: pos.y, z: pos.z,
qx: rot.x, qy: rot.y, qz: rot.z, qw: rot.w
};
dataChannel.send(JSON.stringify(data));
}
xrSession.requestAnimationFrame(onXRFrame);
}
嗯,这里要注意:不要每帧都发。我一开始就这么干,结果 DataChannel 直接爆了。后来改成每 50ms 发一次,或者只在位置变化超过 1cm 时发送,效果就好多了。
空间音频:让声音有“位置感”
空间音频,说白了就是让声音像在真实世界里一样——人站在你左边,声音就从左边来;人走远了,声音就变小。WebRTC 原生支持 RTCRtpSender 的 setTrack 配合 PannerNode 实现。
我建议直接用 Web Audio API 的 PannerNode。把远端用户的音频流接到 MediaStreamSource,然后连到 PannerNode,再连到 AudioContext.destination。每次收到远端位置更新,就设置 panner.positionX/Y/Z。
const audioCtx = new AudioContext();
const panner = audioCtx.createPanner();
panner.panningModel = 'HRTF'; // 更真实
// 远端音频流
const remoteStream = new MediaStream([track]);
const source = audioCtx.createMediaStreamSource(remoteStream);
source.connect(panner).connect(audioCtx.destination);
// 每次收到位置更新
function updateAudioPosition(x, y, z) {
panner.positionX.value = x;
panner.positionY.value = y;
panner.positionZ.value = z;
}
PannerNode 的朝向和头显的旋转同步。否则你转头时,声音方向不会变,体验很出戏。
我曾经在项目里忘了同步朝向,测试时总觉得声音“粘在墙上”。后来加了一行 panner.orientationX/Y/Z 的更新,问题就解决了。这个坑,你大概率也会遇到。
3D 场景共享:不只是传个模型
3D 场景共享,不是把整个模型文件传过去——那太慢了。真正的做法是:只同步状态。比如模型的位置、旋转、缩放、动画状态。模型本身可以提前加载,或者用 glTF 的 URL 引用。
我习惯用 DataChannel 传 JSON 格式的状态更新。结构大概是这样:
{
"type": "objectUpdate",
"id": "chair_01",
"position": [1.2, 0.5, -0.3],
"rotation": [0, 1.57, 0],
"scale": [1, 1, 1],
"animation": "idle"
}
远端收到后,在 Three.js 或 A-Frame 里找到对应对象,直接更新 transform。注意:要加插值。直接赋值会导致物体“跳变”。我一般用 lerp 做平滑过渡。
知识体系与核心逻辑
下面这张图,是我自己梳理的 WebRTC + WebXR 结合的核心流程。你看完应该能明白数据是怎么流转的。
实践中的几个关键点
- 带宽控制: 3D 场景里的位置更新很频繁。我建议用
RTCDataChannel的send之前做一次差分比较,只发送变化的部分。 - 坐标系对齐: WebXR 使用右手坐标系,Three.js 也是。但如果你混用 A-Frame(左手系),记得做转换。我吃过这个亏,模型在本地正常,远端却倒立了。
- 音频同步: 空间音频的延迟要控制在 50ms 以内。如果网络不好,可以降低位置更新频率,但音频流不能断。我一般把音频流优先级设高,DataChannel 设低。
说实话,WebRTC + WebXR 的组合现在还处于早期,但潜力巨大。我见过有人用它做远程手术指导,也有人做虚拟演唱会。你想想看,未来每个人都能在浏览器里进入一个共享的 3D 空间,声音、位置、交互都实时同步——这不就是元宇宙的雏形吗?
嗯,这一章的内容就到这里。代码示例和流程图我都放在上面了,你照着跑一遍应该就能跑通。如果遇到问题,多半是坐标系或者音频上下文没配对,检查一下这两点就好。