嘿,各位未来的元宇宙建筑师们,欢迎来到今天的 WebXR 开发速成班!我是你们的导游,将带你们用 Vue.js 和 WebXR API 打造一个属于自己的沉浸式 AR/VR 体验。
准备好了吗?让我们开始构建属于自己的现实扭曲器吧!
第一站:WebXR API 基础认知
首先,我们要认识一下今天的主角——WebXR API。 简单来说,WebXR API 是一组 JavaScript 接口,它允许我们在浏览器中创建和管理虚拟现实(VR)和增强现实(AR)体验。
把它想象成一个万能遥控器,你可以用它来控制浏览器理解并渲染你的3D场景,并将其呈现在VR头显或AR设备上。
核心概念:
- XRSystem: 这是整个 WebXR 体验的入口点。 你可以通过它请求 XR 会话(session) 等。
- XRSession: 代表一个活动的 AR 或 VR 会话。 在会话中,你可以访问设备的位置、方向、以及绘制场景所需的信息。
- XRReferenceSpace: 定义坐标系,场景中的所有物体都相对于这个坐标系定位。常见的类型有
local
,local-floor
,viewer
,unbounded
。 - XRFrame: 代表一个渲染帧。 它包含渲染场景所需的所有信息,例如设备姿态和视图。
- XRViewerPose: 描述用户在虚拟或增强现实世界中的头部位置和方向。
- XRView: 代表一个单独的视图,用于渲染到屏幕或头显上(例如,左眼和右眼)。
- XRWebGLLayer: 将 WebXR 渲染内容输出到 WebGL 上下文。
第二站:Vue.js 项目搭建与配置
既然是 Vue.js 开发,那我们先创建一个 Vue 项目。
vue create my-xr-app
cd my-xr-app
选择你喜欢的预设。我这里选择默认的 Vue 3 + TypeScript。
接下来,我们需要安装一些必要的依赖:
npm install three @types/three
three
是一个流行的 JavaScript 3D 库,我们将使用它来创建 3D 场景。 @types/three
提供 TypeScript 类型定义,让我们的代码更健壮。
第三站:WebXR 会话启动
现在,让我们开始编写代码,创建一个 WebXR.vue
组件,用于初始化 WebXR 会话。
<template>
<canvas ref="canvas" />
</template>
<script lang="ts">
import { defineComponent, ref, onMounted } from 'vue';
import * as THREE from 'three';
export default defineComponent({
name: 'WebXR',
setup() {
const canvas = ref<HTMLCanvasElement | null>(null);
let renderer: THREE.WebGLRenderer;
let scene: THREE.Scene;
let camera: THREE.PerspectiveCamera;
let xrSession: XRSession | null = null;
let xrRefSpace: XRReferenceSpace | null = null;
const init = async () => {
if (!navigator.xr) {
alert("WebXR not supported");
return;
}
renderer = new THREE.WebGLRenderer({
antialias: true,
canvas: canvas.value!,
});
renderer.xr.enabled = true; // 启用 WebXR
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
scene = new THREE.Scene();
camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
1000
);
// 创建一个简单的立方体
const geometry = new THREE.BoxGeometry();
const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);
// 设置相机初始位置
camera.position.z = 5;
// WebXR 相关
try {
const supported = await navigator.xr.isSessionSupported('immersive-vr'); // 'immersive-ar' for AR
if (supported) {
const button = document.createElement('button');
button.textContent = 'Enter VR';
button.addEventListener('click', async () => {
try {
await startXRSession();
} catch (error) {
console.error("Failed to start XR session:", error);
}
});
document.body.appendChild(button); // 添加按钮到页面
} else {
alert('VR not supported on this device.');
}
} catch (error) {
console.error("Error checking XR support:", error);
alert("Error checking XR support.");
}
};
const startXRSession = async () => {
try {
xrSession = await navigator.xr.requestSession('immersive-vr'); // 'immersive-ar' for AR
xrSession.onend = () => {
xrSession = null;
};
xrRefSpace = await xrSession.requestReferenceSpace('local'); // 或者 'local-floor', 'viewer'
await renderer.xr.setSession(xrSession);
xrSession.requestAnimationFrame(animate);
} catch (error) {
console.error("Error starting XR session:", error);
alert("Error starting XR session.");
}
};
const animate = (time: number, frame?: XRFrame) => {
if (!frame || !xrSession) {
renderer.render(scene, camera);
requestAnimationFrame(animate);
return;
}
const pose = frame.getViewerPose(xrRefSpace!);
if (pose) {
const glLayer = renderer.xr.getLayer(xrSession);
if(glLayer) {
renderer.setSize(glLayer.framebufferWidth, glLayer.framebufferHeight);
}
renderer.render(scene, camera);
}
xrSession.requestAnimationFrame(animate);
};
onMounted(() => {
init();
});
return {
canvas,
};
},
});
</script>
<style scoped>
canvas {
width: 100%;
height: 100%;
display: block;
}
</style>
代码解释:
template
: 包含一个canvas
元素,我们将使用它来渲染 3D 场景。ref
: 使用ref
创建canvas
的引用。onMounted
: 在组件挂载后,调用init
函数。init
:- 检查浏览器是否支持 WebXR。
- 创建
WebGLRenderer
,Scene
,Camera
。 - 创建一个简单的立方体并添加到场景中。
- 设置相机初始位置。
- 检测设备是否支持VR,创建按钮启动会话。
startXRSession
:- 请求一个
immersive-vr
(或者immersive-ar
)会话。 - 请求一个
local
参考空间。 - 将 WebXR 会话设置到渲染器。
- 开始动画循环。
- 请求一个
animate
:- 如果没有
frame
,则使用普通方式渲染。 - 获取用户的姿势(位置和方向)。
- 根据用户姿势更新相机。
- 渲染场景。
- 请求下一帧动画。
- 如果没有
第四站:一些调试技巧
-
WebXR 模拟器: 如果你没有 VR 头显或 AR 设备,可以使用 WebXR 模拟器进行调试。Chrome 和 Firefox 都有 WebXR 模拟器插件。
-
控制台输出: 在代码中添加
console.log
语句,以便在控制台中查看变量的值。 -
错误处理: 使用
try...catch
语句来捕获错误,并提供有用的错误消息。
第五站:进阶 AR/VR 开发
好的,现在我们已经掌握了 WebXR 的基础知识。 接下来,我们可以探索一些更高级的技术。
- 模型加载: 使用
THREE.GLTFLoader
加载 3D 模型。 - 光照和阴影: 添加光照和阴影,使场景更逼真。
- 交互: 使用射线投射(Raycasting)检测用户与 3D 对象的交互。
- AR 平面检测: 在 AR 场景中,使用
XRSession.requestHitTest()
检测真实世界的平面。 - 性能优化: 使用 LOD (Level of Detail) 技术和模型简化来提高性能。
案例分享: 增强现实测量工具
假设我们要开发一个简单的 AR 测量工具,用户可以在真实世界中测量物体之间的距离。
以下是实现步骤:
- 平面检测: 使用
XRSession.requestHitTest()
检测真实世界的平面。 - 锚点创建: 当用户点击屏幕时,在检测到的平面上创建一个锚点。
- 距离计算: 计算两个锚点之间的距离,并在屏幕上显示结果。
- 渲染线条: 在两个锚点之间渲染一条线,可视化测量结果。
示例代码片段 (基于上面的 WebXR.vue 修改):
// ... (之前的代码)
let hitTestSource: XRHitTestSource | null = null;
let hitTestSourceInitialized: boolean = false;
let anchors: THREE.Mesh[] = [];
let line: THREE.Line | null = null;
const initAR = async () => {
try {
const supported = await navigator.xr.isSessionSupported('immersive-ar');
if (supported) {
const button = document.createElement('button');
button.textContent = 'Enter AR';
button.addEventListener('click', async () => {
try {
await startXRSessionAR();
} catch (error) {
console.error("Failed to start XR session:", error);
}
});
document.body.appendChild(button); // 添加按钮到页面
} else {
alert('AR not supported on this device.');
}
} catch (error) {
console.error("Error checking XR support:", error);
alert("Error checking XR support.");
}
};
const startXRSessionAR = async () => {
try {
xrSession = await navigator.xr.requestSession('immersive-ar', {
requiredFeatures: ['hit-test'],
optionalFeatures: ['dom-overlay'],
});
xrSession.onend = () => {
xrSession = null;
hitTestSourceInitialized = false;
anchors = [];
if(line) {
scene.remove(line);
line = null;
}
};
xrRefSpace = await xrSession.requestReferenceSpace('local');
// 创建 hit test source
hitTestSource = await xrSession.requestHitTestSource({ space: xrRefSpace });
hitTestSourceInitialized = true;
await renderer.xr.setSession(xrSession);
xrSession.requestAnimationFrame(animateAR);
//添加点击事件
canvas.value!.addEventListener('click', onCanvasClick);
} catch (error) {
console.error("Error starting XR session:", error);
alert("Error starting XR session.");
}
};
const onCanvasClick = async (event: MouseEvent) => {
if (!xrSession || !hitTestSourceInitialized) return;
const hitTestResults = await hitTestSource!.getHitTestResults(xrRefSpace!);
if (hitTestResults.length > 0) {
const hit = hitTestResults[0];
const hitPose = hit.getPose(xrRefSpace!);
if (hitPose) {
// 创建一个简单的球体作为锚点
const geometry = new THREE.SphereGeometry(0.05, 32, 32);
const material = new THREE.MeshBasicMaterial({ color: 0xff0000 });
const sphere = new THREE.Mesh(geometry, material);
sphere.position.set(hitPose.transform.position.x, hitPose.transform.position.y, hitPose.transform.position.z);
sphere.quaternion.set(hitPose.transform.orientation.x, hitPose.transform.orientation.y, hitPose.transform.orientation.z, hitPose.transform.orientation.w);
scene.add(sphere);
anchors.push(sphere);
if(anchors.length >= 2) {
//计算距离
const distance = anchors[0].position.distanceTo(anchors[1].position);
console.log("Distance:", distance);
//渲染线条
if(line){
scene.remove(line);
}
const materialLine = new THREE.LineBasicMaterial({ color: 0x0000ff });
const points = [
anchors[0].position,
anchors[1].position
];
const geometryLine = new THREE.BufferGeometry().setFromPoints( points );
line = new THREE.Line( geometryLine, materialLine );
scene.add( line );
}
}
}
};
const animateAR = (time: number, frame?: XRFrame) => {
if (!frame || !xrSession) {
renderer.render(scene, camera);
requestAnimationFrame(animateAR);
return;
}
const pose = frame.getViewerPose(xrRefSpace!);
if (pose) {
const glLayer = renderer.xr.getLayer(xrSession);
if(glLayer) {
renderer.setSize(glLayer.framebufferWidth, glLayer.framebufferHeight);
}
renderer.render(scene, camera);
}
xrSession.requestAnimationFrame(animateAR);
};
// ... (onMounted 和 return 部分)
onMounted(() => {
initAR(); // 初始化 AR
});
在这个例子中,我们使用了 hit-test
特性来检测真实世界的平面,并在点击屏幕时创建锚点。 然后,我们计算两个锚点之间的距离,并在它们之间渲染一条线。
代码注释
- 我们请求
immersive-ar
会话,并要求hit-test
特性。 requestHitTestSource
创建一个 hit test source,用于检测真实世界的平面。getHitTestResults
返回一个 hit test 结果数组。- 使用 hit test 结果的
getPose
方法获取锚点的位置和方向。 - 通过计算两个锚点坐标的差值,得到距离。
- 使用
THREE.Line
渲染一条线,连接两个锚点。
一些需要注意的点:
注意点 | 描述 |
---|---|
权限请求 | AR/VR 需要访问设备摄像头和传感器,因此浏览器会提示用户授权。 |
性能优化 | AR/VR 应用对性能要求很高,需要尽可能优化代码和资源。 |
用户体验 | 良好的用户体验至关重要。 提供清晰的指示和反馈,避免让用户感到不适。 |
设备兼容性 | WebXR API 的支持程度因设备和浏览器而异。 需要进行充分的测试,确保应用在不同设备上都能正常运行。 |
参考空间选择 | 选择合适的参考空间(local , local-floor , viewer , unbounded )对于获得正确的姿态信息非常重要。 |
第六站:总结与展望
恭喜你!现在你已经掌握了使用 Vue.js 和 WebXR API 开发 AR/VR 应用的基础知识。
WebXR 的未来充满无限可能。 随着技术的不断发展,我们可以期待更多令人惊叹的 AR/VR 体验。 勇敢地探索,大胆地创新,让我们一起用 WebXR 创造一个更美好的世界吧!
记住,编码的道路永无止境,不断学习,不断实践,你就能成为真正的 WebXR 大师! 祝你编码愉快!