概述
本文将详细介绍如何使用 Three.js 和 Vue.js 构建一个 VR 全景看房系统。我们将学习如何创建 360 度全景房间、实现房间间的切换、添加交互式标签以及创建流畅的相机动画效果。
准备工作
首先,我们需要引入必要的库和组件:
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
import { RGBELoader } from "three/examples/jsm/loaders/RGBELoader";
import { ref, onMounted } from "vue";
import gsap from "gsap";
import SpriteCanvas from "./three/SpriteCanvas";
场景初始化
首先,我们需要创建一个基本的 Three.js 场景:
// 初始化场景
const scene = new THREE.Scene();
// 初始化相机
const camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
1000
);
// 设置相机位置
camera.position.set(0, 0, 0);
// 初始化渲染器
const renderer = new THREE.WebGLRenderer({
antialias: true,
alpha: true,
logarithmicDepthBuffer: true,
});
renderer.setSize(window.innerWidth, window.innerHeight);
渲染循环
设置基本的渲染循环:
const render = () => {
renderer.render(scene, camera);
requestAnimationFrame(render);
};
鼠标交互控制
实现鼠标拖拽来控制相机旋转:
onMounted(() => {
container.value.appendChild(renderer.domElement);
render();
let isMouseDown = false;
// 监听鼠标按下事件
container.value.addEventListener(
"mousedown",
() => {
isMouseDown = true;
},
false
);
container.value.addEventListener(
"mouseup",
() => {
isMouseDown = false;
},
false
);
container.value.addEventListener("mouseout", () => {
isMouseDown = false;
});
let clock = new THREE.Clock();
clock.start();
// 是否按下鼠标,移动鼠标
container.value.addEventListener("mousemove", (event) => {
camera.rotation.order = "YXZ";
let delta = clock.getDelta();
if (isMouseDown) {
gsap.to(camera.rotation, {
y: camera.rotation.y + event.movementX * 0.001,
x: camera.rotation.x + event.movementY * 0.001,
duration: delta,
});
}
});
});
全景房间类
创建一个 Room 类来管理全景房间:
class Room {
constructor(
name,
roomIndex,
textureUrl,
position = new THREE.Vector3(0, 0, 0),
euler = new THREE.Euler(0, 0, 0)
) {
this.name = name;
// 创建立方体
const geometry = new THREE.BoxGeometry(10, 10, 10);
geometry.scale(1, 1, -1);
var arr = [
`${roomIndex}_l`, // 左
`${roomIndex}_r`, // 右
`${roomIndex}_u`, // 上
`${roomIndex}_d`, // 下
`${roomIndex}_b`, // 后
`${roomIndex}_f`, // 前
];
let boxMaterials = [];
arr.forEach((item) => {
// 纹理加载
const texture = new THREE.TextureLoader().load(
textureUrl + item + ".jpg"
);
if (item === `${roomIndex}_d` || item === `${roomIndex}_u`) {
texture.rotation = Math.PI;
texture.center = new THREE.Vector2(0.5, 0.5);
}
boxMaterials.push(
new THREE.MeshBasicMaterial({
map: texture,
transparent: true,
opacity: 0.8,
depthWrite: true,
depthTest: true,
})
);
});
const cube = new THREE.Mesh(geometry, boxMaterials);
cube.position.copy(position);
cube.rotation.copy(euler);
scene.add(cube);
}
}
交互式标签精灵
创建 SpriteText 类用于创建可交互的标签:
class SpriteText {
constructor(text, position) {
this.callbacks = [];
const canvas = document.createElement("canvas");
canvas.width = 1024;
canvas.height = 1024;
const context = canvas.getContext("2d");
context.fillStyle = "rgba(100, 100, 100, 0.7)";
context.fillRect(0, 256, 1024, 512);
context.textAlign = "center";
context.textBaseline = "middle";
context.font = "bold 200px Arial";
context.fillStyle = "white";
context.fillText(text, 512, 512);
let texture = new THREE.CanvasTexture(canvas);
const material = new THREE.SpriteMaterial({
map: texture,
transparent: true,
depthWrite: true,
});
const sprite = new THREE.Sprite(material);
sprite.scale.set(0.5, 0.5, 0.5);
sprite.position.copy(position);
this.sprite = sprite;
sprite.renderOrder = 1;
scene.add(sprite);
let mouse = new THREE.Vector2();
let raycaster = new THREE.Raycaster();
window.addEventListener("click", (event) => {
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
let intersects = raycaster.intersectObject(sprite);
if (intersects.length > 0) {
this.callbacks.forEach((callback) => {
callback();
});
}
});
}
onClick(callback) {
this.callbacks.push(callback);
}
}
创建房间和标签
创建多个房间并设置房间间的切换:
// 创建客厅
let liveroom = new Room("客厅", 0, "./img/livingroom/");
// 创建厨房
let kitchenPostion = new THREE.Vector3(-5, 0, -10);
let kitEuler = new THREE.Euler(0, -Math.PI / 2, 0);
let kitchen = new Room("厨房", 3, "./img/kitchen/", kitchenPostion, kitEuler);
// 创建厨房精灵文字
let kitchenTextPosition = new THREE.Vector3(-1, 0, -3);
let kitchenText = new SpriteText("厨房", kitchenTextPosition);
kitchenText.onClick(() => {
// 让相机移动到厨房
gsap.to(camera.position, {
duration: 1,
x: kitchenPostion.x,
y: kitchenPostion.y,
z: kitchenPostion.z,
});
moveTag("厨房");
});
// 创建厨房回客厅精灵文字
let kitchenBackTextPosition = new THREE.Vector3(-4, 0, -6);
let kitchenBackText = new SpriteText("客厅", kitchenBackTextPosition);
kitchenBackText.onClick(() => {
// 让相机移动到客厅
gsap.to(camera.position, {
duration: 1,
x: 0,
y: 0,
z: 0,
});
moveTag("客厅");
});
// 创建阳台
let balconyPosition = new THREE.Vector3(0, 0, 15);
let balcony = new Room("阳台", 8, "./img/balcony/", balconyPosition);
// 创建阳台精灵文字
let balconyTextPosition = new THREE.Vector3(0, 0, 3);
let balconyText = new SpriteText("阳台", balconyTextPosition);
balconyText.onClick(() => {
// 让相机移动到阳台
gsap.to(camera.position, {
duration: 1,
x: balconyPosition.x,
y: balconyPosition.y,
z: balconyPosition.z,
});
moveTag("阳台");
});
// 创建阳台回客厅精灵文字
let balconyBackTextPosition = new THREE.Vector3(-1, 0, 11);
let balconyBackText = new SpriteText("客厅", balconyBackTextPosition);
balconyBackText.onClick(() => {
// 让相机移动到客厅
gsap.to(camera.position, {
duration: 1,
x: 0,
y: 0,
z: 0,
});
moveTag("客厅");
});
标签动画
实现标签的平滑移动动画:
function moveTag(name) {
let positions = {
客厅: [100, 110],
厨房: [180, 190],
阳台: [50, 50],
};
if (positions[name]) {
gsap.to(tagDiv.value, {
duration: 0.5,
x: positions[name][0],
y: positions[name][1],
ease: "power3.inOut",
});
}
}
加载进度管理
使用 Three.js 的加载管理器来显示加载进度:
THREE.DefaultLoadingManager.onProgress = function (item, loaded, total) {
console.log(item, loaded, total);
console.log("进度:", new Number((loaded / total) * 100).toFixed(2));
progress.value = new Number((loaded / total) * 100).toFixed(2);
};
技术要点详解
1. 全景立方体贴图
使用立方体贴图(Cube Map)技术创建 360 度全景环境。将 6 张图片分别对应立方体的 6 个面(左、右、上、下、前、后)。
2. 相机控制
- 使用鼠标拖拽实现 360 度视角旋转
- 通过 GSAP 实现平滑的相机旋转动画
- 采用 YXZ 旋转顺序避免万向锁问题
3. 交互式精灵标签
- 使用 CanvasTexture 动态创建文本纹理
- 通过射线检测实现点击交互
- 使用 Sprite 对象在 3D 空间中显示标签
4. 房间切换机制
- 每个房间使用独立的空间坐标
- 通过 GSAP 平滑过渡相机位置实现房间切换
- 添加返回按钮实现房间间的往返切换
5. 性能优化
- 使用透明度控制优化渲染性能
- 合理设置深度测试参数
- 使用 GSAP 优化动画性能
应用场景
VR 全景看房系统广泛应用于:
- 房地产行业: 在线展示房屋内部结构
- 旅游行业: 景点虚拟游览
- 教育领域: 虚拟校园参观
- 商业展示: 商店/展厅虚拟体验
扩展建议
- 多层建筑: 支持楼层切换
- 热点导航: 添加更多交互热点
- VR支持: 集成 WebVR API
- 移动端适配: 优化触摸交互
- 音频导览: 添加语音讲解功能
总结
通过这个项目,我们学习了如何使用 Three.js 和 Vue.js 创建一个功能完整的 VR 全景看房系统:
- 如何使用立方体贴图技术创建 360 度全景环境
- 如何实现流畅的相机控制和视角切换
- 如何创建可交互的 3D 标签系统
- 如何使用 GSAP 实现平滑动画效果
- 如何管理多个场景间的切换
这套系统为用户提供了沉浸式的虚拟看房体验,展示了现代 Web 技术在房地产领域的巨大潜力。