Three.js 是一个强大的 JavaScript 3D 库,结合 WebVR API 可以轻松创建沉浸式 VR 体验。本教程将带你使用 Three.js 构建一个最小可行的 VR 场景,包含基础结构、交互控制和 VR 模式切换。
环境准备
首先创建基础 HTML 结构并引入 Three.js 和 WebXR polyfill(用于兼容不支持原生 WebXR 的浏览器):
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Three.js VR Demo</title>
<script src="https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/webxr-polyfill@latest/build/webxr-polyfill.min.js"></script>
</head>
<body style="margin: 0; overflow: hidden;">
<script>
// 这里将放置我们的Three.js代码
</script>
</body>
</html>
场景初始化
创建 Three.js 的基本组件:场景 (Scene)、相机 (Camera)、渲染器 (Renderer):
// 初始化场景、相机和渲染器
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x88ccff); // 设置天空颜色
// 创建透视相机(视场角、宽高比、近截面、远截面)
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 1.6, 3); // 设置相机位置(眼高约1.6米)
// 创建WebGL渲染器并启用VR支持
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.xr.enabled = true; // 启用VR支持
document.body.appendChild(renderer.domElement);
// 添加VR按钮
const vrButton = document.createElement('button');
vrButton.textContent = '进入VR';
vrButton.style.position = 'absolute';
vrButton.style.bottom = '20px';
vrButton.style.left = '50%';
vrButton.style.transform = 'translateX(-50%)';
vrButton.style.padding = '10px 20px';
vrButton.style.fontSize = '16px';
vrButton.style.cursor = 'pointer';
document.body.appendChild(vrButton);
vrButton.addEventListener('click', () => {
renderer.xr.setReferenceSpaceType('local').then(() => {
renderer.xr.getSession() ?
renderer.xr.endSession() :
renderer.xr.startSession('immersive-vr');
});
});
添加 3D 对象
在场景中添加一些基础 3D 对象作为示例:
// 添加地面
const groundGeometry = new THREE.PlaneGeometry(20, 20);
const groundMaterial = new THREE.MeshStandardMaterial({ color: 0x88aa66 });
const ground = new THREE.Mesh(groundGeometry, groundMaterial);
ground.rotation.x = -Math.PI / 2; // 使平面水平
scene.add(ground);
// 添加光源
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight.position.set(5, 10, 7);
scene.add(directionalLight);
// 添加一个立方体
const cubeGeometry = new THREE.BoxGeometry(1, 1, 1);
const cubeMaterial = new THREE.MeshStandardMaterial({
color: 0xff5533,
metalness: 0.2,
roughness: 0.7
});
const cube = new THREE.Mesh(cubeGeometry, cubeMaterial);
cube.position.set(0, 0.5, 0); // 放置在地面上
scene.add(cube);
添加控制器支持
为 VR 控制器添加支持,使其能够与场景交互:
// 添加VR控制器
const controller1 = renderer.xr.getController(0);
scene.add(controller1);
const controller2 = renderer.xr.getController(1);
scene.add(controller2);
// 为控制器添加可视化表示
function setupController(controller) {
const geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.Float32BufferAttribute([0, 0, 0, 0, 0, -1], 3));
geometry.setAttribute('color', new THREE.Float32BufferAttribute([0.5, 0.5, 0.5, 0, 0, 0], 3));
const material = new THREE.LineBasicMaterial({ vertexColors: true, linewidth: 2 });
const ray = new THREE.Line(geometry, material);
ray.scale.z = 5; // 射线长度
controller.add(ray);
// 控制器点击事件
controller.addEventListener('selectstart', () => {
ray.material.color.set(0xff0000); // 点击时变红
});
controller.addEventListener('selectend', () => {
ray.material.color.set(0x000000); // 松开时变黑
});
}
setupController(controller1);
setupController(controller2);
动画循环
创建渲染循环,使场景能够动态更新:
// 动画循环
function animate() {
renderer.setAnimationLoop(render);
}
function render() {
// 让立方体旋转
cube.rotation.x += 0.01;
cube.rotation.y += 0.01;
renderer.render(scene, camera);
}
// 处理窗口大小变化
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
// 启动动画循环
animate();
完整代码
将以上所有部分组合在一起,得到完整的最小 VR 场景代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Three.js VR Demo</title>
<script src="https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/webxr-polyfill@latest/build/webxr-polyfill.min.js"></script>
</head>
<body style="margin: 0; overflow: hidden;">
<script>
// 初始化场景、相机和渲染器
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x88ccff);
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 1.6, 3);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.xr.enabled = true;
document.body.appendChild(renderer.domElement);
// 添加VR按钮
const vrButton = document.createElement('button');
vrButton.textContent = '进入VR';
vrButton.style.position = 'absolute';
vrButton.style.bottom = '20px';
vrButton.style.left = '50%';
vrButton.style.transform = 'translateX(-50%)';
vrButton.style.padding = '10px 20px';
vrButton.style.fontSize = '16px';
vrButton.style.cursor = 'pointer';
document.body.appendChild(vrButton);
vrButton.addEventListener('click', () => {
renderer.xr.setReferenceSpaceType('local').then(() => {
renderer.xr.getSession() ?
renderer.xr.endSession() :
renderer.xr.startSession('immersive-vr');
});
});
// 添加地面
const groundGeometry = new THREE.PlaneGeometry(20, 20);
const groundMaterial = new THREE.MeshStandardMaterial({ color: 0x88aa66 });
const ground = new THREE.Mesh(groundGeometry, groundMaterial);
ground.rotation.x = -Math.PI / 2;
scene.add(ground);
// 添加光源
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight.position.set(5, 10, 7);
scene.add(directionalLight);
// 添加一个立方体
const cubeGeometry = new THREE.BoxGeometry(1, 1, 1);
const cubeMaterial = new THREE.MeshStandardMaterial({
color: 0xff5533,
metalness: 0.2,
roughness: 0.7
});
const cube = new THREE.Mesh(cubeGeometry, cubeMaterial);
cube.position.set(0, 0.5, 0);
scene.add(cube);
// 添加VR控制器
const controller1 = renderer.xr.getController(0);
scene.add(controller1);
const controller2 = renderer.xr.getController(1);
scene.add(controller2);
// 为控制器添加可视化表示
function setupController(controller) {
const geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.Float32BufferAttribute([0, 0, 0, 0, 0, -1], 3));
geometry.setAttribute('color', new THREE.Float32BufferAttribute([0.5, 0.5, 0.5, 0, 0, 0], 3));
const material = new THREE.LineBasicMaterial({ vertexColors: true, linewidth: 2 });
const ray = new THREE.Line(geometry, material);
ray.scale.z = 5;
controller.add(ray);
controller.addEventListener('selectstart', () => {
ray.material.color.set(0xff0000);
});
controller.addEventListener('selectend', () => {
ray.material.color.set(0x000000);
});
}
setupController(controller1);
setupController(controller2);
// 动画循环
function animate() {
renderer.setAnimationLoop(render);
}
function render() {
cube.rotation.x += 0.01;
cube.rotation.y += 0.01;
renderer.render(scene, camera);
}
// 处理窗口大小变化
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
// 启动动画循环
animate();
</script>
</body>
</html>
测试与部署
- 将上述代码保存为 HTML 文件(如vr-demo.html)
- 使用本地服务器运行(避免浏览器安全限制):
npx http-server .
- 在支持 WebXR 的浏览器中打开页面(如 Chrome、Firefox Reality)
- 连接 VR 头显(如 Meta Quest、HTC Vive 等)
- 点击 "进入 VR" 按钮体验场景
扩展建议
- 添加更多 3D 对象:使用不同几何体创建更丰富的场景
- 实现交互逻辑:添加拾取、移动、缩放等交互功能
- 优化性能:使用 LOD(细节级别)、实例化等技术优化性能
- 添加音效:使用 Web Audio API 为场景添加沉浸音效
- 物理模拟:集成 Cannon.js 等物理引擎实现真实物理效果
这个最小 VR 场景示例展示了 Three.js 构建 VR 应用的基本结构,你可以在此基础上进行扩展,创建更复杂、更丰富的 VR 体验。