1. 为什么选择Vue+Three.js做VR看房?
这两年VR看房突然火了起来,很多房产平台都在用。作为前端开发者,我发现用Vue+Three.js这套组合拳来实现VR看房特别顺手。先说Vue,它那个响应式数据绑定简直是为交互场景量身定做的,Three.js又是WebGL的顶级封装,两者配合起来就像咖啡配奶泡——完美。
我去年做过一个中介平台的VR看房项目,客户要求能在网页里自由走动看房。当时试过纯Three.js方案,发现状态管理特别麻烦。后来改用Vue+Three.js,把场景状态都存在Vuex里,开发效率直接翻倍。比如用户点击"下一间房"按钮,Vue这边改个数据,Three.js场景就自动更新了,这种开发体验不要太爽。
2. 环境搭建与基础配置
2.1 创建Vue项目
先来个标准的Vue项目初始化:
npm init vue@latest vr-house-viewer cd vr-house-viewer npm install重点来了,Three.js的安装有讲究。我建议用官方包而不是某些第三方封装:
npm install three @types/three踩坑提醒:记得在vite.config.ts里加个配置,否则Three.js的示例模型可能加载失败:
export default defineConfig({ optimizeDeps: { exclude: ['three'] } })2.2 初始化Three.js场景
在components文件夹新建VRViewer.vue,先写个基础架子:
<script setup> import * as THREE from 'three' import { onMounted, ref } from 'vue' const canvasRef = ref(null) onMounted(() => { // 初始化场景 const scene = new THREE.Scene() const camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 ) const renderer = new THREE.WebGLRenderer({ canvas: canvasRef.value, antialias: true }) // 后续代码... }) </script> <template> <canvas ref="canvasRef" /> </template>3. 全景图处理与球体创建
3.1 获取全景图素材
找全景图是个技术活。我常用的免费资源站有:
- Poly Haven(CC0协议可商用)
- Texture Haven(建筑类素材多)
- 各大相机厂商的样张库
专业项目建议用Insta360这类设备拍摄,手机拍的全景图往往顶部/底部有畸变。有个取巧办法:用Matterport扫描的房屋,导出等距柱状投影图。
3.2 创建球体模型
重点代码来了,这里有几个关键参数要注意:
const geometry = new THREE.SphereGeometry( 500, // 半径 60, // 宽度分段数 40, // 高度分段数 Math.PI, // 水平起始角度 Math.PI * 2, // 水平扫描角度 Math.PI / 2, // 垂直起始角度 Math.PI // 垂直扫描角度 )为什么半径要设500?实测发现:
- 值太小:用户稍微移动就会穿模
- 值太大:浮点数精度问题导致闪烁
- 500-1000这个范围最合适
4. 材质应用与相机设置
4.1 加载全景贴图
用TextureLoader加载图片时,一定要处理加载状态:
const textureLoader = new THREE.TextureLoader() const texture = textureLoader.load( '/panorama.jpg', (texture) => { texture.mapping = THREE.EquirectangularReflectionMapping texture.colorSpace = THREE.SRGBColorSpace }, undefined, (err) => { console.error('图片加载失败:', err) } )4.2 相机定位技巧
相机位置设置有个小窍门:
camera.position.set(0, 1.6, 0) // 1.6米是成年人平均视高 camera.lookAt(0, 1.6, -1) // 看向正前方建议加上轨道控制器方便调试:
import { OrbitControls } from 'three/addons/controls/OrbitControls.js' const controls = new OrbitControls(camera, renderer.domElement) controls.enableZoom = true controls.target.set(0, 1.6, 0)5. 交互功能实现
5.1 热点标记
在墙上添加可点击的热点:
const hotspotGeometry = new THREE.SphereGeometry(0.2, 16, 16) const hotspotMaterial = new THREE.MeshBasicMaterial({ color: 0xff0000 }) const hotspot = new THREE.Mesh(hotspotGeometry, hotspotMaterial) hotspot.position.set(3, 1.5, -4) // 放在墙面位置 // 添加点击事件 window.addEventListener('click', (event) => { const mouse = new THREE.Vector2( (event.clientX / window.innerWidth) * 2 - 1, -(event.clientY / window.innerHeight) * 2 + 1 ) const raycaster = new THREE.Raycaster() raycaster.setFromCamera(mouse, camera) const intersects = raycaster.intersectObjects([hotspot]) if (intersects.length > 0) { console.log('点击了热点') } })5.2 多房间切换
用Vue的响应式特性管理房间状态:
const rooms = ref([ { id: 1, panorama: '/room1.jpg', hotspots: [...] }, { id: 2, panorama: '/room2.jpg', hotspots: [...] } ]) const currentRoom = ref(0) function switchRoom(index) { currentRoom.value = index loadPanorama(rooms.value[index].panorama) }6. 性能优化方案
6.1 图片压缩技巧
全景图体积大,建议:
- 使用JPEG格式,质量设70-80%
- 分辨率控制在8000x4000以内
- 启用渐进式加载
可以用sharp库预处理图片:
npm install sharp然后写个转换脚本:
const sharp = require('sharp') sharp('input.jpg') .resize(8000, 4000) .jpeg({ quality: 80, progressive: true }) .toFile('output.jpg')6.2 内存管理
Three.js容易内存泄漏,记得在组件卸载时清理:
onBeforeUnmount(() => { renderer.dispose() geometry.dispose() material.dispose() texture.dispose() })7. 移动端适配要点
7.1 陀螺仪控制
加上设备方向检测:
if (window.DeviceOrientationEvent) { window.addEventListener('deviceorientation', (event) => { const alpha = event.alpha ? THREE.MathUtils.degToRad(event.alpha) : 0 const beta = event.beta ? THREE.MathUtils.degToRad(event.beta) : 0 const gamma = event.gamma ? THREE.MathUtils.degToRad(event.gamma) : 0 camera.rotation.set(beta, alpha, -gamma) }) }7.2 触摸事件处理
实现双指缩放:
let initialDistance = 0 canvasRef.value.addEventListener('touchstart', (e) => { if (e.touches.length === 2) { initialDistance = Math.hypot( e.touches[0].pageX - e.touches[1].pageX, e.touches[0].pageY - e.touches[1].pageY ) } }) canvasRef.value.addEventListener('touchmove', (e) => { if (e.touches.length === 2) { const distance = Math.hypot( e.touches[0].pageX - e.touches[1].pageX, e.touches[0].pageY - e.touches[1].pageY ) const zoom = distance / initialDistance camera.fov = initialFov / zoom camera.updateProjectionMatrix() } })8. 常见问题排查
8.1 图片显示黑色
可能原因及解决方案:
- 图片路径错误 - 用require()包裹路径
- 跨域问题 - 确保图片服务器配置CORS
- 纹理未翻转 - 记得设置geometry.scale(1,1,-1)
8.2 性能卡顿
优化策略:
- 降低球体分段数(但不要低于32)
- 使用低分辨率贴图预览,高清图延迟加载
- 启用renderer.outputColorSpace = THREE.SRGBColorSpace
最后提醒下,正式项目建议用PhotoSphereViewer这类成熟库,但理解底层原理很重要。我有个项目开始时直接用现成库,后来要加特殊效果时不得不重写,早知道不如一开始就用Three.js自己实现。