Three.js Vue.js VR全景看房系统详解

54 阅读5分钟

概述

本文将详细介绍如何使用 Three.js 和 Vue.js 构建一个 VR 全景看房系统。我们将学习如何创建 360 度全景房间、实现房间间的切换、添加交互式标签以及创建流畅的相机动画效果。

screenshot_2026-01-29_11-21-10.gif

准备工作

首先,我们需要引入必要的库和组件:

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 全景看房系统广泛应用于:

  1. 房地产行业: 在线展示房屋内部结构
  2. 旅游行业: 景点虚拟游览
  3. 教育领域: 虚拟校园参观
  4. 商业展示: 商店/展厅虚拟体验

扩展建议

  1. 多层建筑: 支持楼层切换
  2. 热点导航: 添加更多交互热点
  3. VR支持: 集成 WebVR API
  4. 移动端适配: 优化触摸交互
  5. 音频导览: 添加语音讲解功能

总结

通过这个项目,我们学习了如何使用 Three.js 和 Vue.js 创建一个功能完整的 VR 全景看房系统:

  1. 如何使用立方体贴图技术创建 360 度全景环境
  2. 如何实现流畅的相机控制和视角切换
  3. 如何创建可交互的 3D 标签系统
  4. 如何使用 GSAP 实现平滑动画效果
  5. 如何管理多个场景间的切换

这套系统为用户提供了沉浸式的虚拟看房体验,展示了现代 Web 技术在房地产领域的巨大潜力。