在Vue 3中集成Three.js,首先需要创建一个Vue 3项目并安装相关依赖。我们可以使用Vite或Vue CLI来搭建项目骨架。安装Three.js非常简单,只需在项目根目录下执行安装命令即可。Three.js本身是一个独立的3D JavaScript库,并不强依赖于任何特定的前端框架,因此将其与Vue 3的响应式系统结合时,需要特别注意生命周期的管理。

Vue 3 与 Three.js 集成基础环境搭建
Vue 3的Composition API为我们提供了更加灵活的逻辑组织方式。在实现全景图预览组件时,我们通常会使用onMounted生命周期钩子来初始化Three.js的场景、相机和渲染器。这是因为在onMounted执行时,Vue已经完成了DOM的挂载,此时我们可以安全地获取到用于渲染3D内容的容器DOM元素。如果在更早的生命周期(如setup函数执行期间)尝试获取DOM元素,将会得到null,从而导致初始化失败。
除了初始化场景,组件的销毁过程同样关键。当Vue 3组件被卸载时,如果不手动清理Three.js创建的WebGL上下文、几何体、材质和纹理等资源,会导致严重的内存泄漏问题。浏览器中的WebGL上下文数量是有限制的,频繁地创建和销毁组件而不释放资源,最终会耗尽WebGL上下文池,导致页面无法再渲染任何3D内容。因此,在onBeforeUnmount钩子中,我们需要编写专门的清理逻辑。
在具体实现上,我们首先需要安装Three.js库。可以通过npm或yarn等包管理工具进行安装。安装完成后,在Vue组件中引入Three.js的核心模块。为了更好地管理状态,我们可以使用ref来定义容器引用,并使用let关键字声明Three.js相关的全局变量,如场景、相机、渲染器等。这些变量不应该放在响应式对象中,因为Three.js的内部状态变化不需要触发Vue的重新渲染,将其纳入响应式系统反而会导致严重的性能开销。
构建球体几何模型与全景纹理映射
全景图预览的核心原理是将观察者置于一个巨大的球体内部,并将全景图片作为纹理贴在球体的内表面。这样,当观察者在球体内部旋转视角时,就能看到全景图的不同部分,从而产生360度沉浸式的视觉体验。在Three.js中,我们可以使用SphereGeometry来创建这个球体几何模型。
创建球体几何体时,需要指定半径、水平分段数和垂直分段数等参数。为了获得平滑的视觉体验,通常会设置较高的分段数,但这也会增加顶点的数量,从而影响渲染性能。在实际应用中,需要根据目标设备的性能在画质和性能之间找到平衡点。此外,由于我们需要观察球体内部,因此必须通过设置THREE.BackSide来渲染球体的内表面,或者直接对几何体进行反向缩放操作,否则观察者将看不到任何内容。
纹理映射是另一个关键环节。Three.js提供了TextureLoader用于加载图片纹理。在加载全景图片时,需要注意图片的跨域问题。如果全景图片托管在不同的域名下,服务器必须配置正确的CORS响应头,否则浏览器会阻止纹理的加载,导致球体表面显示为黑色。加载完成后,将纹理对象赋值给MeshBasicMaterial的map属性,即可完成全景图的贴图操作。这种材质不受光照影响,非常适合用于呈现纯图片的全景场景。
实现鼠标交互与视角控制
仅有静态的全景图展示是不够的,用户需要通过鼠标拖拽来自由旋转视角,甚至通过滚轮来缩放画面。Three.js官方提供了一个强大的控制器组件OrbitControls,它可以非常方便地实现这些交互功能。需要注意的是,OrbitControls并不在Three.js的核心包中,需要从three/examples/jsm/controls/OrbitControls路径下单独引入。
引入OrbitControls后,我们需要将相机和用于交互的DOM元素传递给它。控制器会自动监听该DOM元素上的鼠标事件,并据此更新相机的位置和朝向。对于全景图预览场景,我们通常需要限制相机的缩放距离,防止用户将相机拉出球体表面。通过设置minDistance和maxDistance属性,可以将相机限制在一个合理的球壳范围内。
此外,OrbitControls还支持自动旋转功能,这对于展示类应用非常有用。通过设置autoRotate为true,并调整autoRotateSpeed参数,可以让全景图缓慢自动旋转,吸引用户的注意力。在交互逻辑处理完毕后,必须在渲染循环中调用控制器的update方法,以确保控制器内部的状态能够正确同步到相机上。下面是一个完整的Vue 3组件代码示例,展示了上述所有功能的集成:
<template>
<div class="panorama-container" ref="containerRef"></div>
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue';
import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';
const containerRef = ref(null);
let scene, camera, renderer, controls, animationId;
onMounted(() => {
initPanorama();
animate();
});
onBeforeUnmount(() => {
cleanup();
});
function initPanorama() {
const container = containerRef.value;
const width = container.clientWidth;
const height = container.clientHeight;
scene = new THREE.Scene();
camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000);
camera.position.set(0, 0, 0.1);
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(width, height);
container.appendChild(renderer.domElement);
const geometry = new THREE.SphereGeometry(500, 60, 40);
geometry.scale(-1, 1, 1);
const textureLoader = new THREE.TextureLoader();
const texture = textureLoader.load('https://ipipp.com/panorama.jpg');
const material = new THREE.MeshBasicMaterial({ map: texture });
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
controls = new OrbitControls(camera, renderer.domElement);
controls.enableZoom = true;
controls.minDistance = 1;
controls.maxDistance = 200;
controls.autoRotate = true;
controls.autoRotateSpeed = 0.5;
}
function animate() {
animationId = requestAnimationFrame(animate);
controls.update();
renderer.render(scene, camera);
}
function cleanup() {
cancelAnimationFrame(animationId);
if (controls) {
controls.dispose();
}
scene.traverse((child) => {
if (child.isMesh) {
child.geometry.dispose();
if (child.material.map) {
child.material.map.dispose();
}
child.material.dispose();
}
});
if (renderer) {
renderer.dispose();
if (renderer.domElement && renderer.domElement.parentNode) {
renderer.domElement.parentNode.removeChild(renderer.domElement);
}
}
}
</script>
<style scoped>
.panorama-container {
width: 100%;
height: 100vh;
overflow: hidden;
}
</style>性能优化与内存释放最佳实践
在Vue 3组件中运行Three.js场景,通常需要一个持续的渲染循环。使用requestAnimationFrame是执行渲染循环的最佳实践,因为它能够与浏览器的重绘频率保持同步,并在页面不可见时自动暂停渲染,从而节省CPU和GPU资源。在每一帧的渲染回调中,我们需要调用渲染器的render方法,并更新控制器状态。
然而,如果不加控制地使用requestAnimationFrame,在组件卸载时可能会导致回调函数仍在执行,从而引发报错或内存泄漏。因此,我们需要保存requestAnimationFrame返回的ID,并在onBeforeUnmount钩子中调用cancelAnimationFrame来取消渲染循环。这是确保组件安全销毁的第一步。
内存释放是Three.js应用中最容易被忽视的环节。在清理阶段,除了取消渲染循环和移除事件监听器外,还必须遍历场景中的所有对象,调用几何体和材质的dispose方法。对于纹理对象,同样需要调用其dispose方法来释放显存。最后,还需要调用渲染器的dispose方法来释放WebGL上下文。只有完成这一系列清理工作,才能确保组件在多次挂载和卸载后,系统资源不会被耗尽,应用始终保持流畅运行。