基于 Three.js 构建极简 VR 场景教程

539 阅读4分钟

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>

测试与部署

  1. 将上述代码保存为 HTML 文件(如vr-demo.html)
  1. 使用本地服务器运行(避免浏览器安全限制):
npx http-server .
  1. 在支持 WebXR 的浏览器中打开页面(如 Chrome、Firefox Reality)
  1. 连接 VR 头显(如 Meta Quest、HTC Vive 等)
  1. 点击 "进入 VR" 按钮体验场景

扩展建议

  1. 添加更多 3D 对象:使用不同几何体创建更丰富的场景
  1. 实现交互逻辑:添加拾取、移动、缩放等交互功能
  1. 优化性能:使用 LOD(细节级别)、实例化等技术优化性能
  1. 添加音效:使用 Web Audio API 为场景添加沉浸音效
  1. 物理模拟:集成 Cannon.js 等物理引擎实现真实物理效果

这个最小 VR 场景示例展示了 Three.js 构建 VR 应用的基本结构,你可以在此基础上进行扩展,创建更复杂、更丰富的 VR 体验。