
简介基于vue3、TypeScript与Three.js打造的全景家居VR看房系统面向需要实现360°全景展示的Web前端开发者与室内设计演示场景。系统支持全景拖拽旋转与自由视角通过房间名称热点即可切换不同空间快速查看房屋整体布局适合房地产、装修设计及虚拟展厅等应用。压缩包共125个文件约75.54MB主要包含JPG全景图、PNG界面素材、HDR环境贴图、Vue组件与JS脚本等既提供可用的场景图像也包含核心交互逻辑与工程配置。目前已有557人学习下载资源附带完整可运行的前端工程配置了npm依赖与启动环境方便开发者借助源码理解Three.js全景渲染机制、场景切换思路并在此基础上二次开发出更丰富的家居展示系统。1. 全景看房系统拆解Vue3 Three.js 让浏览器变身 VR 展厅一个 2:1 比例的全景 JPG拖进浏览器页面后只需要两三秒用户按住鼠标左键一拖就能环视整个客厅滚轮一推能看到吊顶细节点一下右下角按钮就切到卧室。这个项目做的就是这样一件事——Vue3threejs全景看房把传统地产销售必须到场才能完成的 VR 全景展示压缩到一个链接里。它适合两类人一类是正在帮房产、酒店、展厅类客户做数字化展示的开发者想找一套能直接改、能上线交付的全景代码另一类是想研究 Three.js 和 Vue3 组合式 API 是怎么协作的进阶学习者。整套代码的技术栈非常集中Vue3 TypeScript Three.js Vite核心业务全围绕全景图渲染和交互展开没有冗余依赖值得花一个下午拆开看。2. 项目结构解析Vue3 工程与 Three.js 场景的分层边界2.1 全景看房的整体分层思路拿到源码后不要急着跑先把分层关系理清楚。这个系统在架构上分了三层最外层是 Vue3 组件层负责页面布局、房间切换按钮、加载进度提示这些 UI 交互中间层是 Three.js 渲染核心层负责场景、相机、渲染器、轨道控制器的初始化全景球体的创建和贴图更新最底层是数据资源层也就是 public 目录下的全景图和房间配置文件。三层的依赖方向是单向的Vue 组件只调用渲染层暴露的方法渲染层只处理 Three.js 相关逻辑不碰 DOM 结构和路由这样双方谁出问题都不至于连坐。这种分层方式和我以前做的很多二开项目思路一致如果你把 Three.js 代码直接写在 Vue 组件里前期确实省事但一旦全景图数量多起来组件会膨胀到上千行改渲染参数要翻组件改 UI 布局也要翻组件越改越难受。当前项目的封装方式把渲染相关全部收敛到src/panorama目录里组件只保留模板和事件绑定这正是 Vue3 Three.js 项目比较推荐的落地姿势。2.2 目录结构与文件职责解压源码包后的工程目录大致如下先拿它对照自己手里的代码避免后面改的时候找错文件。vue3-threejs-vr-house/ ├── public/ │ ├── panoramas/ │ │ ├── living-room.jpg # 客厅全景图 │ │ ├── bedroom.jpg # 卧室全景图 │ │ └── kitchen.jpg # 厨房全景图 │ └── rooms.config.json # 房间配置列表 ├── src/ │ ├── components/ │ │ ├── PanoramaViewer.vue # 全景容器组件 │ │ └── RoomSwitcher.vue # 房间切换组件 │ ├── panorama/ │ │ ├── core/ │ │ │ ├── sceneManager.ts # 场景/相机/渲染器/控制器封装 │ │ │ └── sphericalRenderer.ts # 全景球体渲染与贴图更新 │ │ └── utils/ │ │ ├── textureLoader.ts # 纹理加载封装含进度回调 │ │ └── device.ts # 设备类型与性能判断 │ ├── views/ │ │ └── home.vue # 首页布局组装所有组件 │ ├── router/ │ │ └── index.ts # 路由入口 │ ├── stores/ │ │ └── viewer.ts # Pinia 状态保存当前房间与加载状态 │ ├── App.vue │ └── main.ts ├── package.json ├── vite.config.ts └── tsconfig.jsonsceneManager.ts是全景渲染的发动机包含场景初始化、相机透视参数、控制器配置、渲染循环启动sphericalRenderer.ts负责创建球体、贴图更新和旧纹理的释放textureLoader.ts把 Three.js 的 TextureLoader 包了一层让调用方可以拿到加载进度和错误事件。2.3 房间配置与数据流rooms.config.json是这个项目里容易被忽略但很关键的文件它决定了房间切换按钮长什么样、点完切到哪张图{ rooms: [ { id: living, name: 客厅, image: /panoramas/living-room.jpg }, { id: bedroom, name: 卧室, image: /panoramas/bedroom.jpg }, { id: kitchen, name: 厨房, image: /panoramas/kitchen.jpg } ] }每个房间对象有id、name、image三个字段。id用于路由或状态管理里标记当前房间name直接渲染到切换按钮文本上image是全景图路径。数据流是全项目学习成本最低的一条线Vue 组件从 Pinia store 读取房间列表 → 渲染RoomSwitcher按钮 → 点击后调用viewerstore 的switchRoomaction → action 里触发sceneManager.switchScene(image)→ 渲染层加载新贴图并更新球体材质。整套链路没有多余的中间层理解之后改起来很轻松。3. 核心模块拆解一张全景图从加载到渲染的完整链路3.1 场景、相机与渲染器初始化打开sceneManager.ts最先看到的是构造函数里对 Three.js 三大件的初始化。我拆过的项目里很多都把这段写在组件内但这个项目把它抽成了独立类这样单元测试和复用都方便。// src/panorama/core/sceneManager.ts import * as THREE from three import { OrbitControls } from three/examples/jsm/controls/OrbitControls.js export class SceneManager { private scene: THREE.Scene private camera: THREE.PerspectiveCamera private renderer: THREE.WebGLRenderer private controls: OrbitControls private sphere: THREE.Mesh constructor(container: HTMLElement) { // 创建场景对象所有 3D 元素都挂载在它下面 this.scene new THREE.Scene() // 透视相机视角 60 度宽高比跟随容器尺寸近裁剪面 0.1远裁剪面 1000 this.camera new THREE.PerspectiveCamera( 60, container.clientWidth / container.clientHeight, 0.1, 1000 ) // 开启抗锯齿的 WebGL 渲染器 this.renderer new THREE.WebGLRenderer({ antialias: true }) this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)) this.renderer.setSize(container.clientWidth, container.clientHeight) container.appendChild(this.renderer.domElement) // 轨道控制器实现鼠标拖拽旋转视角、滚轮缩放 this.controls new OrbitControls(this.camera, this.renderer.domElement) this.controls.enableDamping true // 开启惯性阻尼拖拽结束后视角缓慢停稳 this.controls.rotateSpeed 0.8 // 旋转灵敏度数值越大拖拽越敏感 this.controls.minDistance 0.1 // 最近观察距离避免穿模 this.controls.maxDistance 3 // 最远观察距离防止拉出球体 this.controls.enablePan false // 禁用平移全景场景里平移没有意义 // 相机放在原点也就是球体的圆心位置 this.camera.position.set(0, 0, 0) // 启动渲染循环 this.animate() } private animate() { requestAnimationFrame(() this.animate()) // 阻尼效果需要在每帧渲染前更新控制器状态 this.controls.update() this.renderer.render(this.scene, this.camera) } }几个关键参数说明PerspectiveCamera的第三个和第四个参数0.1、1000决定了可视裁剪范围在全景场景下相机永远在球心实际能看到的最远距离是球体半径所以这个数值不需要调大。setPixelRatio(Math.min(window.devicePixelRatio, 2))是我一直沿用的习惯——如果直接设置为设备像素比在部分高清屏上渲染压力会翻好几倍限制到 2 是画质和性能的平衡点。enableDamping配合controls.update()是官方推荐写法漏掉update()的话拖拽会变得非常生硬。3.2 全景球体创建与贴图映射全景看房的核心机制是把一张宽高比 2:1 的等距柱状投影图Equirectangular贴到一个球体内表面然后把相机放到球心。用户看到的是球体内壁的局部区域旋转视角就等于在环顾整个空间。// src/panorama/core/sphericalRenderer.ts import * as THREE from three export class SphericalRenderer { // 创建全景球体 public createSphere(texture: THREE.Texture): THREE.Mesh { // 半径 1横向 64 分段纵向 32 分段 const geometry new THREE.SphereGeometry(1, 64, 32) const material new THREE.MeshBasicMaterial({ map: texture, side: THREE.BackSide // 关键配置只渲染球体内表面 }) return new THREE.Mesh(geometry, material) } // 更新贴图并回收旧纹理 public updateTexture(sphere: THREE.Mesh, newTexture: THREE.Texture) { const oldMap (sphere.material as THREE.MeshBasicMaterial).map if (oldMap) oldMap.dispose() // 手动释放旧贴图防止内存持续上涨 ;(sphere.material as THREE.MeshBasicMaterial).map newTexture ;(sphere.material as THREE.MeshBasicMaterial).needsUpdate true } }SphereGeometry(1, 64, 32)的半径是 1相机在原点所以观察距离就是 1。分段数量 64 和 32 在全景图分辨率不高时足够如果用的是 8K 全景图建议把宽度分段提升到 128否则在边缘能看到轻微棱角。side: THREE.BackSide是全景渲染的灵魂配置它让材质的可视面变成内表面配合相机在球心才能实现人在球中看四周的效果。MeshBasicMaterial不受光照影响这点很重要。全景图本身已经包含了环境光照信息如果换成MeshStandardMaterial再加入光源颜色会双重叠加要么过曝要么偏色。全景场景里永远用基础材质。3.3 纹理加载与进度回调贴图加载在这个项目里是独立的工具模块我拆代码时重点关注了它的错误处理因为全景图文件通常体积不小弱网环境下加载失败是常见问题。// src/panorama/utils/textureLoader.ts import * as THREE from three export function loadPanoramaTexture( url: string, onProgress?: (percent: number) void ): PromiseTHREE.Texture { return new Promise((resolve, reject) { const loader new THREE.TextureLoader() loader.load( url, (texture) { // 设置纹理的采样方式减少拉伸时的模糊锯齿 texture.minFilter THREE.LinearFilter resolve(texture) }, (xhr) { // xhr.loaded / xhr.total 计算百分比供 UI 层展示进度 if (xhr.total 0) { const percent (xhr.loaded / xhr.total) * 100 onProgress?.(Math.round(percent)) } }, (error) { reject(new Error(全景图加载失败: ${url})) } ) }) }这里把 TextureLoader 的第四参数错误回调转成了 Promise 的 reject调用方就可以用async/await加try/catch处理失败场景不用再嵌套回调。minFilter设置成LinearFilter是因为全景图经常被缩小显示默认的MipmapLinearFilter在部分安卓 WebGL 设备上会导致贴图发虚。3.4 场景切换与状态管理多房间切换是看房系统区别于普通全景展示的关键。实现思路很直接保留同一个球体网格只替换其材质上的 map 贴图。// src/stores/viewer.ts 中的核心切换逻辑 import { defineStore } from pinia export const useViewerStore defineStore(viewer, { state: () ({ currentRoomId: living, loading: false, loadingPercent: 0, }), actions: { async switchRoom(roomId: string, imageUrl: string) { this.loading true this.loadingPercent 0 try { // 加载新贴图同时更新进度 const texture await loadPanoramaTexture(imageUrl, (p) { this.loadingPercent p }) // 调用渲染层的贴图更新方法 sphericalRenderer.updateTexture(sphere, texture) this.currentRoomId roomId } catch (e) { console.error(切换房间失败, e) // 此处可以接 UI 层的错误提示 } finally { this.loading false this.loadingPercent 0 } }, }, })切换场景时最容易翻车的点是内存管理。Three.js 的TextureLoader每加载一张图都会在 GPU 上创建新的纹理对象如果不主动 dispose 旧的连续切换十几次房间后浏览器显存占用会明显上涨弱机型的页面会越来越卡。这个项目在updateTexture里先取旧贴图调用dispose()这个血泪经验直接决定了我后来所有全景项目的资源回收写法。4. 运行与配置从克隆到上线的完整流程4.1 环境要求与依赖安装这个项目基于 Vite 构建所以对 Node 版本有要求建议直接上 Node 18 及以上npm 8 或 pnpm 7 都行。拿到源码后第一步是解压到本地目录然后安装依赖。# 查看当前 node 版本低于 16 需要先升级 node -v # 进入项目根目录 cd vue3-threejs-vr-house # 使用 npm 安装依赖如果项目里有 pnpm-lock.yaml优先用 pnpm install npm install如果安装过程出现ERR! ERESOLVE这类依赖树冲突多半是本地 npm 版本和项目锁文件版本不匹配删掉node_modules和package-lock.json后重新 install 一般能解决。这是我常在群里看到别人问的问题实际处理过一次后就不觉得玄学了。4.2 启动开发服务器npm run dev执行完终端会打印本地地址默认是http://localhost:5173。打开浏览器后如果看到加载进度条然后出现全景观房页面说明环境没问题。Vite 的 HMR 热更新是默认开启的改sceneManager.ts里的rotateSpeed之类的参数保存后页面会无刷新更新这个特性在全景调参时非常好用。如果页面白屏优先打开浏览器开发者工具的 Console 面板看有没有红色报错。最常见的两种情况一是端口被占用Vite 会自动切换端口但containerWidth拿到的是 0二是容器的height没设置导致渲染器大小异常。这个项目根组件里给PanoramaViewer设置了width: 100vw; height: 100vh如果二次开发时把这个容器嵌到其他布局里一定要确认容器有明确高度否则clientHeight为 0渲染器会创建一个 0 像素高的画布。4.3 构建生产包npm run build npm run preview # 本地预览构建产物构建完成后会在dist/目录输出静态文件包括index.html、assets/js和assets/img。npm run preview是 Vite 自带的本地静态预览服务用来在部署前确认构建产物没有路径问题。部署时有个注意点如果项目部署在服务器二级路径下比如https://example.com/vr-house/需要在vite.config.ts里设置base: /vr-house/否则 index.html 里引用的 JS 资源路径会从根目录开始找导致 404。这个项目默认是部署在根目录二开部署到子目录时最容易忘的就是这一步。4.4 全景图资源的准备与替换跑通后想替换成自己的房源照片只需要替换public/panoramas/目录下的 JPG 文件并同步修改rooms.config.json。全景图不是随便一张照片都能用必须是 2:1 比例的等距柱状投影图常见尺寸是 4096×2048 或 8192×4096。用普通单反拍的照片直接贴上去会让画面严重变形需要用全景拼接软件或 360 相机拍摄也可以先从一些免费全景图网站下载测试图。我一般会建一个脚本检查图片宽高比是否符合 2:1不合法的直接报警而不是等到运行时才发现图被拉伸变形。运行时调试时可以打开 Three.js 的开发模式在sceneManager.ts里临时加上console.log(this.renderer.info.memory.textures)就能看到纹理数量和显存占用变化排查加载和释放问题。5. 避坑指南全景看房开发中我踩过的四个坑5.1 现象一球体渲染出来是黑色或者全白原因球体材质用错了光照模式。全景图自身的像素已经包含了光照信息如果使用MeshStandardMaterial这类 PBR 材质在没有加光源的场景下会渲染成黑色如果在场景里加一个强光又会把全景图洗白。解决换回MeshBasicMaterial并且确认side: THREE.BackSide设置成功。如果既有材质又有光源的问题删掉Scene里所有Light对象。5.2 现象二全景图左右边缘有一条接缝原因等距柱状投影图的首尾其实是同一个水平位置当球体的宽度分段数是奇数或贴图 UV 采样在边缘不重合时就会出现一条肉眼可见的经线。更常见的原因是SphereGeometry的宽度分段数太少导致整张图被拉扯产生形变。解决把分段数调整到偶数并适当提高new THREE.SphereGeometry(1, 64, 32)的 64 是满足 2:1 全景图的最低配置如果图片边缘有严重扭曲提高到 128 分段。检查贴图的wrapS和wrapT设置为THREE.ClampToEdgeWrapping避免边缘像素重复造成的错位。5.3 现象三手机端旋转视角时画面不跟手原因部分安卓机的 WebGL 渲染性能不足加上没有对像素比做限制导致帧率低、触摸事件积压。另外 OrbitControls 的rotateSpeed在触摸设备上的敏感度和鼠标不同默认值偏高。解决在SceneManager构造器里对设备类型做判断安卓中低端机把setPixelRatio降到 1.5同时把rotateSpeed调到 0.5。device.ts工具里用navigator.userAgent和navigator.hardwareConcurrencyCPU 核数小于 4 视为低性能来区分设备档位。5.4 现象四频繁切换房间后页面越来越卡原因每次TextureLoader.load都会传入新的纹理对象旧的 GPU 纹理没有被释放。Three.js 的Material.map更换不会自动 dispose 被替换的旧纹理需要手动处理。解决在updateTexture方法里先保存旧贴图更换后调用oldMap.dispose()。同时建议对全景 JPG 做压缩单张控制在 3MB 以内否则低端机在切换时会出现明显的纹理加载卡顿。我一般会在线性图片压缩工具里把 8K 图压到 4K 输出视觉差异很小体感流畅度提升明显。6. 二次开发技巧把全景 Demo 改造成可交付的产品6.1 配置驱动把房间数据彻底 JSON 化现在的rooms.config.json只描述了房间和图片的对应关系。要交付给客户时我通常会把它扩展成包含户型信息、热点坐标、默认视角的完整配置文件。每个房间增加defaultLookAt字段进入房间时把相机旋转到指定视角而不是每次进入都朝向正前方{ id: living, name: 客厅, image: /panoramas/living-room.jpg, defaultLookAt: { x: 0.2, y: 0.3 }, hotspots: [ { position: [0.15, 0.1, -0.98], label: 真皮沙发, targetRoom: bedroom } ] }代码里在switchScene完成后调用controls.setLookAt或者直接修改相机的 Euler 角就能实现进房自动朝向卖点方向的效果。6.2 热点标注给全景图加点交互有了hotspots配置下一步是用 Three.js 的射线检测实现可点击标签。在全景模式下用Raycaster把屏幕坐标转换成球面上的 3D 点然后在这个位置放置一个圆形贴花的 Mesh点击它触发切换房间或弹出产品详情。这也是市面上商业看房系统中空间热点跳转的通用方案基于这个项目的代码框架改造不算大核心是维护一组热点 Mesh 并在每个renderer.render()之后把 3D 坐标投影为屏幕坐标让标签始终贴在正确位置。6.3 基于这套代码的交付习惯我每次用这套全景看房框架做交付时第一件事是先把全景图资源整理规范按房源ID/房间类型.jpg的目录结构放进 public然后写一个简单的静态资源配置校验脚本检查图片比例和 JSON 字段是否匹配防止交付前环节出幺蛾子。从那以后每一套项目的配置校验都强制走一遍脚本没有一次因为图片路径写错在客户现场翻车。这个框架本身不复杂但把数据层和渲染层分离的原则已经帮我省掉了不知道多少二次开发的返工时间希望帮到你。本文还有配套的精品资源点击获取