资讯动态

Vue3+TS+Three.js全景看房系统:核心渲染与避坑指南

发布时间:2026/10/26 15:31:10 来源:尧图企业网站定制
简介这是一套基于 Vue3、TypeScript 与 Three.js 构建的全景家居 VR 看房系统源码面向具备一定前端基础、希望掌握全景展示与交互开发的前端工程师、WebGL 学习者及房产数字化项目开发者。项目实现 360° 全景自由视角拖拽旋转与房间热点场景切换可完整呈现整屋布局是理解全景图拼接原理与 Three.js 场景渲染的实用范例。压缩包共 125 个文件以 90 张 jpg 与 16 张 png 全景贴图及界面素材为主另含 3 个 hdr 环境光照文件、2 个 vue 组件、4 个 js 脚本与 json 配置等整体约 75.54MB素材与代码分层清晰便于按模块查阅。目前已有 558 人学习下载。读者可从中获得可直接运行的全景看房工程结构、HDR 环境贴图与全景素材、热点交互与场景切换的实现思路适合作为房地产、室内设计、展览展示等全景应用开发的参考模板。1. 全景看房不是把图片塞进立方体这套 Vue3 TS Three.js 系统到底解决什么去年帮一个做本地房产中介的朋友看他们的小程序客户抱怨最多的不是房源少而是“图看着挺大进去发现客厅只有巴掌宽”。传统九宫格户型图加几张广角照片买家根本建立不起空间感带看十次有八次是白跑。这套基于 Vue3 TypeScript Three.js 的全景家居 VR 看房系统干的就是把一张张等距柱状投影的全景图拼成一个可以在浏览器里拖动、缩放、切换房间的沉浸式空间。它适合两类人手里有全景相机素材、想快速上线看房功能的中小团队以及想拿一个完整 Three.js 场景练手、把 Vue3 响应式和 3D 渲染揉在一起学的开发者。整套东西跑在浏览器里不需要装插件手机端也能拖着看这一点对中介门店那种老旧安卓平板特别友好。2. 技术选型与工程骨架为什么是 Vue3 TS Three.js 这套组合2.1 三个技术各自扛什么活先把分工说清楚不然后面看代码容易乱。Vue3 负责的是“壳”和状态当前在哪个房间、热点是否可见、加载进度多少、移动端还是桌面端这些都用ref/reactive管着模板里做条件渲染。TypeScript 负责的是“契约”全景图的配置对象长什么样、热点数据结构、相机参数全部定义成 interface改配置的时候编辑器直接给你报错这在房间数量一多的时候是救命的东西。Three.js 负责真正的渲染场景Scene、相机PerspectiveCamera、渲染器WebGLRenderer、球体几何SphereGeometry加纹理这套是全景播放器的标准骨架。为什么不用 CSS3D 或者纯background-image加拖拽因为纯 CSS 方案做不了真正的透视校正视野边缘会拉伸变形而且没法在同一场景里叠加 3D 热点标记。Three.js 把全景图贴在一个翻转法线的球体内壁上相机放在球心转动相机就等于转动视角几何上是对的边缘不会畸变。这是这个项目最核心的一个选择后面所有代码都围绕它展开。2.2 目录结构长什么样一个能跑起来的工程骨架大致是这样组织的我按实际拆过的项目习惯列一下panorama-vr/ ├── public/ │ └── panoramas/ # 全景图资源按房间分目录 │ ├── living/ │ │ ├── 0.jpg # 等距柱状投影图2:1 比例 │ │ └── 1.jpg │ └── bedroom/ ├── src/ │ ├── components/ │ │ ├── PanoramaViewer.vue # 核心渲染组件 │ │ ├── HotspotLayer.vue # 热点叠加层 │ │ └── LoadingMask.vue # 加载遮罩 │ ├── composables/ │ │ ├── useThreeScene.ts # 场景初始化与销毁 │ │ └── usePanorama.ts # 全景图切换逻辑 │ ├── types/ │ │ └── panorama.d.ts # 类型定义 │ ├── config/ │ │ └── rooms.ts # 房间与热点配置 │ └── App.vue ├── vite.config.ts └── tsconfig.jsonpublic/panoramas放静态资源Vite 不会处理它直接按 URL 引用省得打包时把几十兆的图塞进 bundle。composables目录是关键设计把 Three.js 那套命令式的初始化、动画循环、销毁逻辑抽成组合式函数组件里只负责挂载和传参。这样做的直接好处是同一个页面里想放两个全景窗口比如对比两个户型直接调两次useThreeScene就行不会互相污染。2.3 依赖与启动命令依赖不多核心就三个# 初始化工程如果从零搭 npm create vitelatest panorama-vr -- --template vue-ts cd panorama-vr # 核心依赖 npm install three npm install -D types/three # 启动开发服务器 npm run devthree是运行时依赖types/three必须装成开发依赖否则 TS 里import * as THREE from three会一片红。这里有个血泪经验types/three的版本要和three主版本对齐差一个大版本就可能出现SphereGeometry参数类型对不上的报错装的时候看一眼package.json里两个版本号的前两位是否一致。提示全景图必须是 2:1 比例的等距柱状投影图equirectangular普通手机拍的广角照片贴上去会严重变形这不是代码问题是素材问题。3. 核心渲染链路从一张全景图到可拖动的球体场景3.1 场景初始化球体、相机、渲染器三件套这是整个项目的心脏我把它单独抽在useThreeScene.ts里。先看代码再逐段说// src/composables/useThreeScene.ts import * as THREE from three import { ref, onBeforeUnmount } from vue export function useThreeScene(container: RefHTMLElement | null) { const scene new THREE.Scene() // 相机放在球心fov 75 是全景观看比较舒服的值 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 }) renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)) renderer.setSize(window.innerWidth, window.innerHeight) // 球体几何半径 500分段数 60/40 保证纹理不糊 const geometry new THREE.SphereGeometry(500, 60, 40) // 关键一步法线翻转让纹理贴在球体内壁 geometry.scale(-1, 1, 1) const material new THREE.MeshBasicMaterial() const sphere new THREE.Mesh(geometry, material) scene.add(sphere) let animationId 0 const animate () { animationId requestAnimationFrame(animate) renderer.render(scene, camera) } onBeforeUnmount(() { cancelAnimationFrame(animationId) geometry.dispose() material.dispose() renderer.dispose() }) return { scene, camera, renderer, sphere, animate } }逐段拆。PerspectiveCamera的四个参数分别是视野角度、宽高比、近裁剪面、远裁剪面。fov 设 75 是经验值太小视野窄像望远镜太大边缘拉伸明显。近裁剪面 0.1、远裁剪面 1000 是为了把半径 500 的球体完整包住如果远裁剪面设成 100球体背面直接被裁掉你会看到一片黑。SphereGeometry(500, 60, 40)里 500 是半径60 是水平分段、40 是垂直分段。分段数低了纹理会有棱角感高了浪费性能60/40 在手机上也扛得住。真正让全景成立的是geometry.scale(-1, 1, 1)这一行——它把球体法线翻到内侧相机在球心往外看看到的就是贴在内壁上的图。很多人第一次写全景忘了这行结果看到的是一片纯色因为默认法线朝外相机在球内看到的是背面被剔除后的空场景这个坑后面还会提。renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))是移动端适配的关键高分屏上不限制像素比会让渲染压力翻倍限制到 2 是画质和性能的平衡点。onBeforeUnmount里的 dispose 是必须的Three.js 的几何体和材质不会自动回收单页应用里反复进出看房页不 dispose 就是内存泄漏切十几个房间后页面直接卡死。3.2 纹理加载与房间切换场景搭好后换房间本质就是换球体材质上的贴图。这里用TextureLoader异步加载加载完再替换避免白屏// src/composables/usePanorama.ts import * as THREE from three const loader new THREE.TextureLoader() const cache new Mapstring, THREE.Texture() export function loadPanorama( sphere: THREE.Mesh, url: string, onProgress?: (percent: number) void ): Promisevoid { return new Promise((resolve, reject) { // 命中缓存直接换切回看过的房间是秒开的 if (cache.has(url)) { const mat sphere.material as THREE.MeshBasicMaterial mat.map cache.get(url)! mat.needsUpdate true resolve() return } loader.load( url, (texture) { texture.colorSpace THREE.SRGBColorSpace cache.set(url, texture) const mat sphere.material as THREE.MeshBasicMaterial mat.map texture mat.needsUpdate true resolve() }, (event) { if (event.total onProgress) { onProgress(Math.round((event.loaded / event.total) * 100)) } }, (err) reject(err) ) }) }texture.colorSpace THREE.SRGBColorSpace这行不能省。新版 Three.js 默认按线性空间处理纹理不设这个全景图会整体发灰发暗很多人以为是显示器问题其实是色彩空间没对上。cache用 Map 存已加载的纹理切回看过的房间不用重新下载这是体验上最划算的一个优化代价是内存房间特别多的时候要加个 LRU 淘汰我一般限制缓存 8 张。onProgress回调把加载百分比抛给 Vue 层LoadingMask.vue里绑一个进度条用户不会对着白屏干等。这里注意event.total在部分服务器没返回Content-Length时是 0要做个兜底不然进度条永远停在 0。3.3 拖拽交互鼠标和触摸统一处理全景的“看”靠拖拽转相机桌面端是鼠标、移动端是手指逻辑要统一。核心是记录按下时的坐标移动时算差值转成相机的经纬度// 在 PanoramaViewer.vue 的 setup 中 let isDragging false let prevX 0 let prevY 0 let lon 0 // 水平角度 let lat 0 // 垂直角度 function onPointerDown(e: PointerEvent) { isDragging true prevX e.clientX prevY e.clientY } function onPointerMove(e: PointerEvent) { if (!isDragging) return const deltaX e.clientX - prevX const deltaY e.clientY - prevY prevX e.clientX prevY e.clientY lon - deltaX * 0.1 lat deltaY * 0.1 // 垂直角度夹紧防止翻过头看到球体极点 lat Math.max(-85, Math.min(85, lat)) const phi THREE.MathUtils.degToRad(90 - lat) const theta THREE.MathUtils.degToRad(lon) camera.lookAt( 500 * Math.sin(phi) * Math.cos(theta), 500 * Math.cos(phi), 500 * Math.sin(phi) * Math.sin(theta) ) }用PointerEvent而不是分开写mousedown/touchstart是因为 Pointer Events 在桌面和移动端统一省掉一套兼容代码。0.1是灵敏度系数数值越大转得越快手机上可以调到 0.15 左右手感更好。lat夹在 -85 到 85 度之间是必须的不夹的话拖到极点附近会出现万向节式的翻转画面会突然倒过来这是全景播放器最典型的翻车点。camera.lookAt的参数是把球面坐标转成笛卡尔坐标半径用 500 和球体一致保证视线始终落在球面上。这套经纬度转笛卡尔的公式建议直接背下来Three.js 全景项目里到处用。4. 热点与房间跳转让全景图“可点”4.1 热点数据结构设计全景看房和单纯看全景图的区别就在于图上有可点的热点点客厅的箭头进卧室点沙发弹出商品信息。数据结构先定清楚// src/types/panorama.d.ts export interface Hotspot { id: string type: nav | info // nav 跳房间info 弹信息 position: { x: number; y: number } // 经纬度角度 targetRoomId?: string // nav 类型必填 title?: string content?: string } export interface Room { id: string name: string panoramaUrl: string hotspots: Hotspot[] }position用经纬度角度而不是 3D 坐标是因为配置的时候人脑更容易理解“往左 30 度、往上 10 度”而且和相机的经纬度体系一致渲染时直接套同一个转换公式。type用联合类型TS 会在你写nav却没填targetRoomId时直接报错这种约束在房间多了以后能省掉大量调试。4.2 热点投影到屏幕热点是 3D 空间里的一个方向要显示成屏幕上的一个按钮得做投影。每帧把热点的 3D 坐标用相机投影到屏幕坐标function updateHotspotScreenPos(hotspot: Hotspot) { const phi THREE.MathUtils.degToRad(90 - hotspot.position.y) const theta THREE.MathUtils.degToRad(hotspot.position.x) const vec new THREE.Vector3( 500 * Math.sin(phi) * Math.cos(theta), 500 * Math.cos(phi), 500 * Math.sin(phi) * Math.sin(theta) ) // 投影到标准化设备坐标 vec.project(camera) const x (vec.x * 0.5 0.5) * window.innerWidth const y (-vec.y * 0.5 0.5) * window.innerHeight // z 大于 1 说明在相机背后要隐藏 return { x, y, visible: vec.z 1 } }vec.project(camera)把世界坐标转成 -1 到 1 的标准化设备坐标再映射到屏幕像素。vec.z 1这个判断是隐藏相机背后的热点不判断的话你背对热点时它还会显示在屏幕上点下去跳到背后的房间用户直接懵。这个函数要放在渲染循环里每帧调用热点才会跟着视角实时移动。4.3 房间跳转与过渡点 nav 热点跳房间直接换图会很生硬加个淡入淡出async function goToRoom(roomId: string) { const room rooms.find(r r.id roomId) if (!room) return // 先淡出 await fadeTo(0, 300) await loadPanorama(sphere, room.panoramaUrl) currentRoom.value room // 重置视角到正前方 lon 0 lat 0 await fadeTo(1, 300) }fadeTo用一个覆盖层 div 改 opacity 实现比在 Three.js 里做后期简单得多。跳转后把lon/lat重置到 0让用户每次进新房间都从正前方开始看不重置的话会继承上个房间的视角有时候一进去就是天花板体验很怪。5. 避坑与排查这几个坑我踩过不止一次5.1 全景图一片黑或纯色现象场景初始化完屏幕上什么都没有或者只有背景色。原因九成是球体法线没翻转相机在球内看到的是被剔除的背面。解决确认geometry.scale(-1, 1, 1)这行在new SphereGeometry之后、new Mesh之前执行。另一个可能是纹理还没加载完就渲染了检查material.map是否已赋值、needsUpdate是否置了 true。5.2 移动端拖动时页面跟着滚动现象手指在全景上拖动画面转了但整个页面也在上下滚。原因touchmove的默认行为没阻止。解决给容器加touch-action: none的 CSS或者在pointermove里对拖拽状态调e.preventDefault()。我一般两个都加CSS 那层更稳不依赖 JS 执行时机。5.3 切几个房间后页面卡顿甚至崩溃现象反复切换房间帧率越来越低最后白屏。原因每次loadPanorama都新建纹理旧的没释放GPU 显存被吃光。解决用 Map 缓存纹理复用同时在组件卸载时遍历缓存调texture.dispose()。缓存也要设上限超过就淘汰最久没用的那张dispose之后再从 Map 里删掉。5.4 全景图发灰、颜色不对现象图能显示但整体偏暗偏灰和原图差很多。原因Three.js 新版默认线性色彩空间纹理没标SRGBColorSpace。解决加载完纹理立刻设texture.colorSpace THREE.SRGBColorSpace渲染器的outputColorSpace也确认是 sRGB。这两个地方有一个没对上颜色就不对。5.5 热点位置偏移视角一转就飘现象热点按钮初始位置对但转动视角后位置越来越偏。原因投影用的相机矩阵没更新或者投影时用的窗口尺寸和实际渲染尺寸不一致。解决确保updateHotspotScreenPos在camera.updateMatrixWorld()之后调用窗口尺寸用renderer.getSize()拿而不是直接读window.innerWidth有容器内嵌的场景两者会不一样。6. 二次开发把看房系统改成你自己的业务工具这套骨架的可扩展性比看上去强因为渲染层和业务层是分开的。想加功能基本不用动useThreeScene改配置和加组件就行。第一个改造方向是热点类型扩展。现在只有nav和info加一个product类型点沙发弹出商品卡片带购买链接数据结构里加个productId字段HotspotLayer.vue里多一个分支渲染半小时能搞定。第二个方向是多层全景比如同一个房间白天和夜晚两套图给Room加个variants数组切换时换 URL 就行缓存机制天然支持。验证改造是否成功我习惯用一套固定流程先在桌面 Chrome 里拖一圈看有没有畸变再用 DevTools 的设备模拟切到 iPhone 尺寸测触摸最后开 Performance 面板录一段切房间的操作看内存曲线是否持续上涨。内存那条线如果一直往上走不回落就是 dispose 没做干净回去查缓存淘汰逻辑。有个具体的技巧值得说房间配置别硬编码在rooms.ts里改成从后端接口拉 JSON前端只负责渲染。这样运营改房间、加热点不用重新发版中介门店自己就能维护。接口返回的结构和Roominterface 对齐TS 类型直接复用联调成本很低。从那以后我每次接全景类需求都强制先把“法线翻转、色彩空间、dispose”这三件事在最小 demo 里跑通再往上堆业务不然写到一半发现底子是歪的返工比重写还难受。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑