使用 Three.js 构建沉浸式全景图AR

在数字内容日益丰富的今天,沉浸式全景体验已成为展示空间、场景和环境的重要方式。无论是虚拟看房、线上博物馆,还是旅游预览,360° 全景图都能让用户仿佛身临其境。

借助 Three.js ------ 这个强大的 WebGL 3D 库,配合现代前端框架 Vue 3,我们可以在浏览器中轻松构建一个高性能、可交互的全景图查看器。本文将带你一步步实现这一功能,并提供完整可运行的代码。


一、技术原理:为什么用球体?

常见的全景图采用 等距柱状投影(Equirectangular Projection),即一张宽高比为 2:1 的矩形图像,包含了完整的 360° 水平视角和 180° 垂直视角。

要在 3D 空间中正确还原这种投影,最直观的方法是:

  1. 创建一个大型球体
  2. 将全景图作为纹理贴到球体内表面
  3. 将相机放置在球体中心
  4. 用户通过旋转视角,从内部观察球壁上的图像

✅ 关键技巧:使用 geometry.scale(-1, 1, 1) 反转球体法线方向,确保从内部可见。


二、核心组件与交互设计

我们的查看器包含以下核心要素:

  • Three.js Scene:3D 场景容器
  • PerspectiveCamera:透视相机,模拟人眼视角
  • WebGLRenderer:负责将 3D 渲染为 2D 图像
  • OrbitControls:提供鼠标拖拽旋转、滚轮缩放等自然交互
  • MeshBasicMaterial:基础材质,无需光照即可显示贴图,避免黑屏问题

此外,我们还启用了:

  • 自动旋转:页面加载后缓慢转动,提示用户可交互
  • 阻尼效果:松开鼠标后惯性滑动,提升操作手感
  • 抗锯齿与 sRGB 色彩空间:确保图像清晰、颜色准确

完整代码实现

以下是完整的 Vue 3 单文件组件代码,可直接复制使用:

javascript 复制代码
<template>
  <!--
    Three.js 的渲染结果(WebGL canvas)会被挂载到这个容器里。
    由于样式使用了 position: fixed + inset: 0,因此它会覆盖整个视口。
  -->
  <div ref="containerRef" class="viewer"></div>
</template>

<script setup>
// Vue3 组合式 API:组件挂载/卸载时创建与释放 Three.js 资源
import { onBeforeUnmount, onMounted, ref } from 'vue'

// Three.js 核心库
import * as THREE from 'three'

// 轨道控制器:用于鼠标拖拽旋转/滚轮缩放等交互
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'

// Vite 中推荐使用 import 引入静态资源,这样打包后路径不会丢
import panoUrl from '../assets/qj.png'

// 容器 DOM 引用(Three.js 的 renderer.domElement 会 append 到这里)
const containerRef = ref(null)

// Three.js 相关实例(放在模块作用域,便于在 onBeforeUnmount 中释放)
let renderer
let scene
let camera
let controls
let frameId = 0

// 动画循环:每帧更新控制器并渲染
function animate() {
  frameId = requestAnimationFrame(animate)
  // OrbitControls 开启阻尼后必须每帧 update
  controls?.update?.()
  renderer.render(scene, camera)
}

// 组件挂载:初始化 Three.js 场景/相机/渲染器,并加载全景贴图
onMounted(() => {
  // 1) 场景
  scene = new THREE.Scene()

  // 2) 相机
  // fov: 视角;near/far: 近远裁剪面
  // 注意:这里将相机放在球体内部,所以 position 不能是 (0,0,0)(会出现裁剪/闪烁),给一个很小的 z 偏移即可。
  camera = new THREE.PerspectiveCamera(75, 1, 0.1, 1000)
  camera.position.set(0, 0, 0.1)

  // 3) 渲染器(真正把画面画出来的东西)
  renderer = new THREE.WebGLRenderer({ antialias: true })

  // 颜色空间:贴图通常是 sRGB,设置后颜色更准确
  renderer.outputColorSpace = THREE.SRGBColorSpace

  // 4) 把 renderer 的 canvas 挂到 DOM
  if (!containerRef.value) return
  containerRef.value.appendChild(renderer.domElement)
  renderer.domElement.style.width = '100%'
  renderer.domElement.style.height = '100%'
  renderer.domElement.style.display = 'block'

  // 5) 设置初始尺寸(仅一次,不监听 resize)
  const w = containerRef.value.clientWidth
  const h = containerRef.value.clientHeight
  camera.aspect = w / h
  camera.updateProjectionMatrix()
  renderer.setSize(w, h, false)
  renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2))

  // 6) 交互控制器:鼠标拖拽旋转,滚轮缩放
  controls = new OrbitControls(camera, renderer.domElement)
  controls.enableDamping = true
  controls.dampingFactor = 0.08
  controls.enablePan = false
  controls.rotateSpeed = 0.5
  controls.zoomSpeed = 0.8

  // 自动旋转:你一打开就能看出是"可转动的全景"
  controls.autoRotate = true
  controls.autoRotateSpeed = 0.6

  // 7) 创建一个球体,并把贴图贴到球体内壁
  const geometry = new THREE.SphereGeometry(50, 64, 64)
  geometry.scale(-1, 1, 1) // 反转以从内部观看

  // 8) 加载贴图
  const texture = new THREE.TextureLoader().load(panoUrl)
  texture.colorSpace = THREE.SRGBColorSpace

  // 9) 材质(无需光照)
  const material = new THREE.MeshBasicMaterial({ map: texture })
  const mesh = new THREE.Mesh(geometry, material)
  scene.add(mesh)

  // 10) 开始渲染(不再监听 resize)
  animate()
})

// 组件卸载:释放 GPU/事件监听,避免内存泄漏
onBeforeUnmount(() => {
  if (frameId) cancelAnimationFrame(frameId)

  try {
    controls?.dispose?.()
  } catch {
    // ignore
  }

  try {
    renderer?.dispose?.()
  } catch {
    // ignore
  }

  if (renderer?.domElement?.parentNode) {
    renderer.domElement.parentNode.removeChild(renderer.domElement)
  }

  renderer = undefined
  scene = undefined
  camera = undefined
})
</script>

<style scoped>
.viewer {
  position: fixed;
  inset: 0;
  width: 100vw;
  height: 100vh;
  overflow: hidden;
}
</style>

QQ20260130-171045

相关推荐
浅念-2 小时前
C语言——动态内存管理
c语言·开发语言·c++·笔记·学习
草履虫建模8 小时前
力扣算法 1768. 交替合并字符串
java·开发语言·算法·leetcode·职场和发展·idea·基础
naruto_lnq10 小时前
分布式系统安全通信
开发语言·c++·算法
Mr Xu_10 小时前
告别冗长 switch-case:Vue 项目中基于映射表的优雅路由数据匹配方案
前端·javascript·vue.js
前端摸鱼匠10 小时前
Vue 3 的toRefs保持响应性:讲解toRefs在解构响应式对象时的作用
前端·javascript·vue.js·前端框架·ecmascript
学嵌入式的小杨同学10 小时前
【Linux 封神之路】信号编程全解析:从信号基础到 MP3 播放器实战(含核心 API 与避坑指南)
java·linux·c语言·开发语言·vscode·vim·ux
sleeppingfrog10 小时前
zebra通过zpl语言实现中文打印(二)
javascript
Re.不晚10 小时前
Java入门17——异常
java·开发语言
精彩极了吧11 小时前
C语言基本语法-自定义类型:结构体&联合体&枚举
c语言·开发语言·枚举·结构体·内存对齐·位段·联合