各位屏幕前的靓仔俊女们,大家好! 今天咱们来聊点刺激的——如何在 Vue 这位前端小甜甜的帮助下,打造一个沉浸式的 AR/VR 体验。准备好了吗?系好安全带,我们要发车啦!
一、WebXR API:通往虚拟现实的大门
首先,我们要认识一位新朋友——WebXR API。 别看名字挺唬人,其实它就是浏览器提供的一套接口,专门用来搞 AR/VR 的。 简单来说,有了它,我们就能让浏览器理解你的头显(VR 头盔)或者手机摄像头,然后把虚拟世界叠加到真实世界中,或者把你完全拉进一个虚拟的世界。
WebXR API 的核心概念有几个:
- XRSystem: 这是整个 WebXR 的入口,你可以用它来检查浏览器是否支持 WebXR,以及请求 XR 会话。
- XRSession: 代表一个 AR/VR 会话。所有渲染、交互都发生在这个会话里。
- XRReferenceSpace: 定义了一个坐标系,用来定位虚拟物体和用户的视角。常用的有
local
,viewer
,local-floor
,bounded-floor
,unbounded
这些类型。 - XRFrame: 每一帧的快照,包含设备姿态、输入等信息。
- XRViewerPose: 描述了用户的视角,包含位置和方向。
- XRInputSource: 代表一个输入设备,比如 VR 手柄或者触摸屏。
为了更清晰地理解这些概念,咱们用一张表格来总结一下:
概念 | 描述 |
---|---|
XRSystem |
WebXR API 的入口点。 负责检查 WebXR 支持情况,请求会话等。 |
XRSession |
代表一个 AR/VR 会话。所有的渲染、交互都发生在这个会话中。 |
XRReferenceSpace |
定义了一个坐标系,用于定位虚拟物体和用户的视角。常见的类型包括: local (相对于会话开始时的位置), viewer (相对于用户的头部), local-floor (相对于地面), bounded-floor (限定在一个区域内的地面), unbounded (没有边界)。 |
XRFrame |
每一帧的快照,包含设备姿态、输入等信息。是渲染循环的核心。 |
XRViewerPose |
描述了用户的视角,包含位置和方向。通过它可以知道用户在虚拟世界中的位置。 |
XRInputSource |
代表一个输入设备,例如 VR 手柄或触摸屏。通过它可以获取用户的输入操作。 |
二、Vue + WebXR:珠联璧合,干活不累
Vue 擅长管理 UI 状态和组件,而 WebXR 负责处理底层的 AR/VR 交互。 它们俩配合起来,简直是天作之合!
接下来,我们一步一步地用 Vue 来实现一个简单的 AR 应用:在你的桌面上放一个虚拟的茶杯。
- 环境搭建
首先,你需要一个支持 WebXR 的浏览器,比如 Chrome Canary 或者 Edge Canary。 并且你需要启用 WebXR 的实验特性。在 Chrome Canary 中,你可以在地址栏输入 chrome://flags
,然后搜索 "WebXR",启用相关的 flag。
然后,创建一个 Vue 项目。你可以使用 Vue CLI 或者 Vite。
# Vue CLI
vue create my-ar-app
# Vite
npm create vite my-ar-app --template vue
- 初始化 WebXR
在你的 Vue 组件中,我们需要初始化 WebXR。
<template>
<canvas ref="xrCanvas" />
<div v-if="!xrSupported">
您的浏览器不支持 WebXR.
</div>
<div v-if="!xrSession">
<button @click="startXR">开始 WebXR</button>
</div>
</template>
<script>
import * as THREE from 'three';
import { ARButton } from 'three/examples/jsm/webxr/ARButton.js';
export default {
data() {
return {
xrSupported: false,
xrSession: null,
renderer: null,
scene: null,
camera: null,
cube: null,
};
},
async mounted() {
this.xrSupported = navigator.xr !== undefined;
if (this.xrSupported) {
// 使用 ARButton 而不是手动创建按钮
document.body.appendChild( ARButton.createButton( this.renderer ) );
try {
const session = await navigator.xr.requestSession('immersive-ar', {
requiredFeatures: ['hit-test'], // 开启 hit-test
optionalFeatures: ['dom-overlay'],
});
this.xrSession = session;
this.startXR();
} catch (error) {
console.error('WebXR 初始化失败:', error);
this.xrSupported = false; // 禁用 XR
}
}
},
methods: {
async startXR() {
try {
// 创建 Three.js 场景
this.scene = new THREE.Scene();
this.camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.01, 20);
// 创建 WebGL 渲染器
this.renderer = new THREE.WebGLRenderer({
antialias: true,
canvas: this.$refs.xrCanvas,
});
this.renderer.setSize(window.innerWidth, window.innerHeight);
this.renderer.setPixelRatio(window.devicePixelRatio);
this.renderer.xr.enabled = true;
this.renderer.xr.setReferenceSpaceType('local'); // 使用 'local' 或 'local-floor'
// 创建一个立方体 (我们的茶杯)
const geometry = new THREE.BoxGeometry(0.1, 0.1, 0.1);
const material = new THREE.MeshNormalMaterial();
this.cube = new THREE.Mesh(geometry, material);
this.scene.add(this.cube);
// 启动 XR 会话
this.xrSession = await navigator.xr.requestSession('immersive-ar', {
requiredFeatures: ['hit-test', 'dom-overlay'], // 开启 hit-test 和 dom-overlay
optionalFeatures: ['dom-overlay'],
});
this.xrSession.updateWorldTrackingState({ planeDetectionState: { enabled: true } }); // 开启平面检测
this.xrSession.addEventListener('end', this.onXRSessionEnd);
this.renderer.xr.setSession(this.xrSession);
// 设置渲染循环
this.renderer.setAnimationLoop(this.render);
// 设置 hit-test
this.xrSession.requestReferenceSpace('viewer').then((refSpace) => {
this.viewerSpace = refSpace;
this.xrSession.requestHitTestSource({ space: this.viewerSpace }).then((source) => {
this.hitTestSource = source;
});
});
} catch (error) {
console.error('启动 WebXR 失败:', error);
}
},
render(time, frame) {
if (frame) {
const session = frame.session;
// hit test
if (this.hitTestSource) {
const hitTestResults = frame.getHitTestResults(this.hitTestSource);
if (hitTestResults.length > 0) {
const hit = hitTestResults[0];
const pose = hit.getPose(this.renderer.xr.getReferenceSpace());
this.cube.position.copy(pose.transform.position);
this.cube.quaternion.copy(pose.transform.orientation);
}
}
const referenceSpace = this.renderer.xr.getReferenceSpace();
const viewerPose = frame.getViewerPose(referenceSpace);
if (viewerPose) {
this.renderer.render(this.scene, this.camera);
}
}
},
onXRSessionEnd() {
this.xrSession = null;
this.renderer.setAnimationLoop(null);
},
},
beforeUnmount() {
if (this.xrSession) {
this.xrSession.end();
}
},
};
</script>
<style scoped>
canvas {
width: 100%;
height: 100%;
display: block;
}
</style>
这段代码做了这些事情:
- 检查浏览器是否支持 WebXR。
- 如果支持,就显示一个 "开始 WebXR" 按钮。
- 点击按钮后,初始化 Three.js 场景和 WebXR 会话。
- 创建一个立方体,作为我们的虚拟茶杯。
- 设置一个渲染循环,每一帧都更新立方体的位置和方向。
- 让茶杯听话:Hit Test
现在,茶杯已经在场景里了,但是它还不知道该放在哪里。我们需要用到 Hit Test API,让茶杯能够 "听话" 地放在我们点击的位置。
在上面的代码中,我们已经开启了hit-test
特性,并且在startXR
方法中请求了hitTestSource
。在render
方法中,我们使用frame.getHitTestResults
来获取 hit test 的结果,然后根据结果来更新立方体的位置和方向。
简单解释下 Hit Test 的原理:
- 我们从用户的视角发射一条 "射线"。
- 这条射线会和现实世界中的平面(比如桌面)相交。
- Hit Test API 会返回射线和平面相交的位置和方向。
- 我们把虚拟物体放在这个位置,就实现了 "放置" 的效果。
三、进阶:让 AR 应用更上一层楼
上面的例子只是一个简单的演示。要打造一个真正的 AR 应用,我们还需要考虑很多问题。
- 模型加载
用立方体当茶杯太 low 了!我们要加载真实的 3D 模型。 Three.js 提供了很多模型加载器,比如 GLTFLoader
。
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
// ...
const loader = new GLTFLoader();
loader.load(
'path/to/your/model.gltf',
(gltf) => {
this.teaCup = gltf.scene;
this.scene.add(this.teaCup);
},
(xhr) => {
console.log((xhr.loaded / xhr.total * 100) + '% loaded');
},
(error) => {
console.log('An error happened');
}
);
- 光照和阴影
没有光照和阴影,虚拟物体看起来会很假。 Three.js 提供了各种光照类型,比如 AmbientLight
, DirectionalLight
, PointLight
。
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
this.scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(1, 1, 1);
this.scene.add(directionalLight);
- 交互
AR 应用不能只是看看而已,还要能交互。 我们可以监听 WebXR 的输入事件,比如手柄的按钮点击或者触摸屏的滑动。
this.xrSession.addEventListener('selectstart', (event) => {
// 处理点击事件
console.log('点击事件', event);
});
- 性能优化
AR 应用对性能要求很高。 如果帧率太低,用户会感到眩晕。
一些优化技巧:
- 减少场景中的三角形数量。
- 使用纹理压缩。
- 避免频繁的内存分配。
- 使用 Web Workers 来处理耗时的计算。
- 平面检测
平面检测是 AR 应用中非常重要的功能。 它可以让应用自动识别现实世界中的平面,比如桌面或者墙壁。
在上面的代码中,我们已经开启了平面检测:
this.xrSession.updateWorldTrackingState({ planeDetectionState: { enabled: true } }); // 开启平面检测
然后,我们可以监听 planeadded
和 planeremoved
事件,来获取检测到的平面信息。
this.xrSession.addEventListener('planeadded', (event) => {
const plane = event.plane;
console.log('检测到平面', plane);
});
- DOM Overlay
DOM Overlay 是一个非常有用的特性,它允许我们将 HTML 元素覆盖在 AR 场景之上。这对于创建 UI 界面、显示信息等非常方便。
首先,需要在请求会话时启用dom-overlay
特性:
this.xrSession = await navigator.xr.requestSession('immersive-ar', {
requiredFeatures: ['hit-test', 'dom-overlay'], // 开启 hit-test 和 dom-overlay
optionalFeatures: ['dom-overlay'],
domOverlay: { root: document.getElementById('overlay') }
});
然后,创建一个 HTML 元素,并将其设置为 DOM Overlay 的根元素:
<div id="overlay" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none;">
<h1>Hello AR!</h1>
<p>This is an overlay.</p>
</div>
四、总结:AR/VR 的未来,由你创造
好了,今天的讲座就到这里。 我们一起学习了 WebXR API 的基本概念,以及如何在 Vue 中使用 WebXR API 来创建一个简单的 AR 应用。
当然,AR/VR 的世界远不止这些。 还有很多有趣的技术等待我们去探索,比如:
- WebRTC: 实现多人 AR/VR 体验。
- WebAssembly: 用高性能的 C/C++ 代码来加速 AR/VR 应用。
- AI: 让 AR/VR 应用更加智能。
希望今天的讲座能给你带来一些启发。 记住,AR/VR 的未来,由你创造! 让我们一起用代码,构建一个更加美好的虚拟世界吧! 谢谢大家!