使用 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

相关推荐
鬼手点金3 小时前
nanoGPT/nanochat 能做出 DeepSeek/Qwen 这样的大模型吗?
服务器·前端·javascript·bug·openclaw
软件技术NINI4 小时前
美妆商城html+Css+js 4页
javascript·css·html
蛋蛋的就会顺顺的4 小时前
hot100 p0——栈,贪心算法,动态规划
java·前端·javascript·算法·leetcode·力扣
码艺-Alimjan5 小时前
Tauri 源码备份 · 打包 & 解压 总结,源码收集之路
前端·javascript·vue.js
死磕音视频5 小时前
让小模型做选择题:限定候选项并读取 logits 概率的入门实践(附 Python 代码)
开发语言·python·大模型·文本分类·transformers
顽疲7 小时前
Java vue 养老系统源码详解:月账单 care_month_bill 与 Quartz 扣费 Job 实践
java·开发语言·vue.js·vue养老·springboot养老·java养老
DevUp7 小时前
标着 JavaScript 的 Wiki 与知识库:既不是 CMS 也不是 SSG(番外三)
javascript·node.js·cms
Wang's Blog8 小时前
Java 项目实战: 交友项目 - 安装开发工具与环境准备
java·开发语言·交友
kyriewen8 小时前
我上手了 Vite+ 1.0:Node 不用自己装,lint 却翻车了
前端·javascript·程序员
鹏多多9 小时前
前端是如何死去的?| 完整时间线
前端·javascript·程序员