在数字内容日益丰富的今天,沉浸式全景体验已成为展示空间、场景和环境的重要方式。无论是虚拟看房、线上博物馆,还是旅游预览,360° 全景图都能让用户仿佛身临其境。
借助 Three.js ------ 这个强大的 WebGL 3D 库,配合现代前端框架 Vue 3,我们可以在浏览器中轻松构建一个高性能、可交互的全景图查看器。本文将带你一步步实现这一功能,并提供完整可运行的代码。
一、技术原理:为什么用球体?
常见的全景图采用 等距柱状投影(Equirectangular Projection),即一张宽高比为 2:1 的矩形图像,包含了完整的 360° 水平视角和 180° 垂直视角。
要在 3D 空间中正确还原这种投影,最直观的方法是:
- 创建一个大型球体
- 将全景图作为纹理贴到球体内表面
- 将相机放置在球体中心
- 用户通过旋转视角,从内部观察球壁上的图像
✅ 关键技巧:使用
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