Skip to content
本文总阅读量

Three.js 示例

本项目包含多个 Three.js 示例,涵盖 360° 全景浏览、雨景效果等功能。

示例列表

1. 360° 全景浏览 - 球形全景

使用 Three.js 实现的球形 360° 全景浏览,支持多个房间切换、热点点击交互。

标题:
说明:

功能特性:

  • 球形全景图展示
  • 多个房间场景切换(客厅、厨房等)
  • 热点标记(点击进入其他房间)
  • 鼠标悬停显示提示框
  • OrbitControls 自由视角控制

核心代码逻辑:

javascript
// 1. 创建球形几何体,贴上全景图作为纹理
const geometry = new THREE.SphereGeometry(500, 60, 40)
// 反转几何体,使贴图在内部显示
geometry.scale(-1, 1, 1)

// 2. 使用 Raycaster 检测鼠标与热点的交互
const raycaster = new THREE.Raycaster()
const mouse = new THREE.Vector2()
// 关键:使用 getBoundingClientRect 计算正确的 NDC 坐标
const rect = this.renderer.domElement.getBoundingClientRect()
mouse.x = ((e.clientX - rect.left) / rect.width) * 2 - 1
mouse.y = -((e.clientY - rect.top) / rect.height) * 2 + 1
raycaster.setFromCamera(mouse, this.camera)

注意事项:

  • Vue 3 中需用 markRaw() 包裹 Three.js 对象,避免响应式代理导致报错
  • 射线检测坐标必须基于画布的 getBoundingClientRect() 计算

2. 360° 全景浏览 - 立方体天空盒

使用立方体 6 个面贴图实现的 360° 全景浏览。

功能特性:

  • 立方体 6 面全景图
  • OrbitControls 视角控制
  • 自动适应窗口大小

核心代码逻辑:

javascript
// 导入 6 张方向图片
import leftImg from '@/assets/home/left.png'
import rightImg from '@/assets/home/right.png'
import topImg from '@/assets/home/top.png'
import bottomImg from '@/assets/home/bottom.png'
import frontImg from '@/assets/home/front.png'
import backImg from '@/assets/home/back.png'

// 创建立方体几何体
const boxGeometry = new THREE.BoxGeometry(10, 10, 10)
// 翻转 Z 轴使贴图在内部显示
boxGeometry.scale(10, 10, -10)

// 为每个面创建材质
const boxMaterials = picList.map((img) => {
  const texture = new THREE.TextureLoader().load(img)
  return new THREE.MeshBasicMaterial({ map: texture })
})

this.box = new THREE.Mesh(boxGeometry, boxMaterials)

4. 雨景效果

实现下雨天气的 3D 场景效果,包含云层、雨滴、闪电等元素。

功能特性:

  • 飘动的云层(使用烟雾纹理)
  • 下落的雨滴粒子系统
  • 随机闪电效果
  • 雾效氛围

核心代码结构:

rain/
├── index.vue              # 入口组件
├── Template.js            # 基础模板类(场景、相机、渲染器初始化)
├── scene/
│   └── director.js        # 导演类(场景编排、动画循环)
└── objects/
    ├── Cloud.js           # 云朵类
    └── RainDrop.js        # 雨滴粒子类

核心代码逻辑:

javascript
// 云朵动画
animate() {
  this.instance.rotation.z -= 0.003
}

// 雨滴粒子动画
animate() {
  const positions = this.geom.attributes.position.array
  for (let i = 0; i < this.drops * 3; i += 3) {
    this.velocityY[i / 3] += Math.random() * 0.05
    positions[i + 1] -= this.velocityY[i / 3]
    // 雨滴落地后重置到顶部
    if (positions[i + 1] < -200) {
      positions[i + 1] = 200
      this.velocityY[i / 3] = 0.5 + Math.random() / 2
    }
  }
  this.geom.attributes.position.needsUpdate = true
}

// 闪电随机效果
if (Math.random() > 0.93 || this.lightning.power > 100) {
  this.lightning.power = 50 + Math.random() * 500
}

常见问题

1. Vue 3 中 Three.js 对象报 Proxy 错误

问题: Error: Invalid typed array view: undefined 或黑屏

原因: Vue 3 的响应式系统会代理 data 中的对象,Three.js 内部使用的 typed arrays 被代理后会导致错误

解决: 使用 markRaw() 包裹 Three.js 对象

javascript
import { markRaw } from 'vue'

export default {
  data() {
    return {
      scene: null,
      camera: null,
      renderer: null
    }
  },
  methods: {
    initScene() {
      this.scene = markRaw(new THREE.Scene())
    }
  }
}

2. require() 在 Vite 中不可用

问题: require is not defined

原因: Vite 使用 ES Module,不支持 CommonJS 的 require()

解决: 使用 ES6 import

javascript
// 错误
const texture = new THREE.TextureLoader().load(require('@/assets/image.png'))

// 正确
import imageUrl from '@/assets/image.png'
const texture = new THREE.TextureLoader().load(imageUrl)

3. 射线检测点击位置偏移

问题: 点击热点没有反应,或点击位置与实际位置不符

原因: 鼠标坐标未转换为相对于画布的 NDC 坐标

解决: 使用 getBoundingClientRect() 计算

javascript
const rect = this.renderer.domElement.getBoundingClientRect()
mouse.x = ((e.clientX - rect.left) / rect.width) * 2 - 1
mouse.y = -((e.clientY - rect.top) / rect.height) * 2 + 1

4. 组件销毁后内存泄漏

问题: 切换页面后 Three.js 渲染循环仍在运行

解决: 在 beforeUnmount 中清理资源

javascript
beforeUnmount() {
  // 取消动画循环
  if (this.timer) {
    cancelAnimationFrame(this.timer)
  }
  // 移除事件监听
  window.removeEventListener('resize', this.onResize)
  // 清理渲染器
  if (this.renderer) {
    this.renderer.dispose()
  }
}

依赖安装

bash
npm install three panolens gsap

路由配置

javascript
{
  path: '/threeDemo/360/round',
  name: 'ThreeDemoRound',
  component: () => import('@/views/threeDemo/360/round.vue')
},
{
  path: '/threeDemo/360/skyBox',
  name: 'ThreeDemoSkyBox',
  component: () => import('@/views/threeDemo/360/skyBox.vue')
},
{
  path: '/threeDemo/rain',
  name: 'ThreeDemoRain',
  component: () => import('@/views/threeDemo/rain/index.vue')
}