Three.js看房案例实战:GLTF加载、几何合并与高亮拾取全解析
简介这是一套基于Three.js实现的三维看房完整案例资源面向Web前端开发、三维可视化与智慧展厅方向的学习者和开发者解决从零搭建可交互看房场景、实现房间间无缝跳转等实际问题。资源共1252个文件压缩包21.44MB以js源码为主体同时包含ts类型文件、json配置文件、md说明文档、cjs模块以及字体、模型等静态资源目录结构完整便于直接运行、调试和二次开发。已有237人学习下载。案例覆盖三维场景构建的核心流程场景对象创建、透视相机设置、多类型灯光布置、几何体与材质搭配并通过关键帧动画实现从大厅到厨房的平滑过渡还使用加载器引入外部模型与纹理增强真实感。适合希望掌握Three.js实际项目架构、快速搭建看房Demo或深入了解Web3D交互细节的开发者参考。 做Three.js看房这个方向前两年就有人在技术群里问但真正让我决定把这套案例好好整理出来的是上个月帮朋友改一个二手房在线看房需求。购房者对“只看照片不放心、视频又没法自己操控”这件事很敏感中介和销售也只能一遍遍补发图片、录短视频沟通效率特别低。而Three.js这套Web端三维方案恰好能补上这块短板在浏览器里直接打开用户自己拖动视角、切换楼层、查看重点家具和户型信息整个体验比看图直观得多。如果你正在调研“Three.js到底能不能扛住看房场景”或者已经上手试了但发现模型加载慢、交互卡顿、高亮选择不好做那这篇内容应该能帮你省不少试错时间。我会把整套看房案例拆开来聊覆盖选型逻辑、GLTF资源管线、合并几何体、射线拾取高亮、性能优化和常见坑点尽量落成可以直接抄走的方案。1. 看房案例的整体思路与技术选型1.1 看房业务的真实需求拆解先别急着写代码先想清楚看房业务里用户最关心什么。我自己归了一下核心就三类一是“这是什么房”户型结构、面积、朝向、楼层位置要让用户一眼看懂二是“房间里到底长什么样”用户要能自由旋转视角、走进房间、看装修细节和家具摆放三是“感兴趣了怎么深入了解”点某个沙发、某面墙、某个户型能弹出对应的面积、材质、价格等说明。对应到Three.js技术方案上这三类需求分别落地为场景沙盘与户型标注、室内相机漫游、以及点击交互与信息展示。其中第一个相对静态重点是场景组织和可视化层次第二个重点在相机控制和碰撞限制第三个则是典型的高亮拾取与UI联动。搞清楚这些后面所有技术选型就有依据了不是越炫越好而是每个功能都服务于“让用户高效看懂房子”这个目标。1.2 Three.js和Unity怎么选这个问题的热度一直不低。我的观点是两者定位不同没有绝对优劣。Unity的优势在于引擎级渲染管线和资产体系适合做大体量数字孪生城市、重度游戏化场景但如果在Web端做Unity的WebGL导出包通常比较大首屏加载和内存占用也偏高和Three.js相比部署链路长不少。Three.js的优势是轻。它本身就是浏览器原生扛WebGL开发完打一个静态包扔到CDN就能跑不需要额外安装插件微信内置浏览器、H5分享链接都能直接打开。对看房这种“用户点开链接就要能看到”的轻交互场景Three.js的交付效率要高很多。另外更新迭代也方便改个户型、换套家具前端发版就完事。我用一张表总结下两者的适配场景对比维度Three.jsUnity运行环境浏览器原生无需安装WebGL导出或客户端安装首屏加载模型压缩后相对可控包体偏大加载时间长更新迭代前端发版链路短需重新构建发布渲染能力适合轻量级场景适合重度渲染与物理模拟适用场景H5看房、线上展厅、电商3D大型数字孪生、复杂仿真如果你只是想快速做一套“能在手机浏览器里跑的看房应用”直接选Three.js就行如果你的目标是高精度整楼BIM加复杂交互再认真考虑Unity。2. 场景搭建与资源管线处理2.1 从建模软件到Three.js的资源规范化看房案例的模型来源一般是Blender、3ds Max或者SketchUp导出格式优先选GLB/GLTF。这个格式在Three.js里支持最好材质、动画、节点层级都能完整保留。我踩过最狠的一个坑是从3ds Max导出时没有注意单位模型导进Three.js后大了100倍相机被模型整个包住转视角直接黑屏。后来养成了习惯DCC软件建模时统一用米做单位导出后先在gltf-viewer里看一眼再接到项目里。另一个关键是压缩。看房场景模型很可能动辄几百MB不压缩直接加载移动端基本会白屏或卡死。现在比较成熟的方案是几何数据用Draco压缩纹理用KTX2格式Basis Universal压缩。Draco能把几何信息压掉70%以上KTX2能把贴图从几MB压到几百KB两者组合下来一个100MB的户型场景能压到20MB左右。加载器写法很简单import { GLTFLoader } from three/examples/jsm/loaders/GLTFLoader.js; import { DRACOLoader } from three/examples/jsm/loaders/DRACOLoader.js; const loader new GLTFLoader(); const dracoLoader new DRACOLoader(); dracoLoader.setDecoderPath(/draco/); loader.setDRACOLoader(dracoLoader); loader.load(/models/house.glb, (gltf) { const model gltf.scene; setupLights(model); fitCameraToScene(model); scene.add(model); }, undefined, (err) { console.error(模型加载失败, err); });注意Draco的decoder文件夹一定要部署到可访问的静态资源目录否则浏览器控制台会报404模型就加载不出来了。2.2 合并几何体的核心细节与收益看房场景里墙体、地板、天花、固定的柜体这些物体通常数量很大——一个普通户型有几百上千个独立Mesh是很正常的。每一个Mesh如果都单独提交渲染就会有对应的draw call手机上性能很容易被拖垮。解决方案就是“合并几何体”把大量不参与复杂交互的静态Mesh合并成一个或少数几个Mesh从根源上减少draw call。我习惯把需要合并的物体分成几类按材质合并墙体和墙体合并木地板和同材质柜体合并互不干扰。直接贴一段常用的合并逻辑import { mergeGeometries } from three/examples/jsm/utils/BufferGeometryUtils.js; function mergeStaticMeshes(meshes) { const geometryList meshes.map((mesh) { // 关键克隆一份几何体再应用世界矩阵避免污染原模型 const geo mesh.geometry.clone(); geo.applyMatrix4(mesh.matrixWorld); return geo; }); const merged mergeGeometries(geometryList); if (!merged) return null; // 合并后原Mesh可以移除保留一个Mesh承载合并结果 const mergedMesh new THREE.Mesh(merged, mergedMaterial); return mergedMesh; }这里有个很容易忽略的坑mergeGeometries对每个几何体的属性有要求如果有的Mesh有UV、有的没有UV或者法线缺失合并过程会报错或者出现黑面。所以合并前最好先对几何体做一个标准化比如统一给geometry.computeVertexNormals()确保数据结构一致。合并时材质问题也要注意。不同材质不能随便合进一个Mesh除非你使用材质组的机制。简单场景下我建议先按材质名称分组再对每组分别合并这样代码简单也直观。还有一点合并后原Mesh的坐标信息就丢了所以后续需要单独高亮的物体一定要先摘出来留作独立Mesh别一股脑全合并进去。2.3 材质分组和合并场景的取舍有些开发者会把“能合并的都合并”这其实不完全是好事。合并后整块几何体成了一个整体无法再对其中单个柜子做高亮、变色、点击等操作。看房场景中床、沙发、书桌、智能马桶这类用户可能会点击的家具必须保留成独立物体才能配合射线拾取。我常用的策略是渲染对象划分为两组——“交互组”和“静态组”。交互组保持原Mesh参与射线的intersectObjects检测静态组做完合并后只负责渲染不参与交互。这样既保证了draw call可控又不牺牲交互能力。3. 看房核心交互实现3.1 相机漫游与楼层切换交互看房案例里最常用的是轨道控制OrbitControls但需要把参数调好。用户如果误操作把相机拉到地板下面或穿出墙外体验会很差必须要限制相机活动范围。一般我会限制maxPolarAngle不超过90度防止相机钻到地面以下同时设置minDistance和maxDistance保证视角在合理范围。楼层切换是另一个高频交互。实现起来比较简单把每层楼放入一个Group切换时控制Group的visible即可。但要特别注意灯光和阴影处理——如果你用了实时阴影被隐藏楼层的castShadow属性依然可能会影响渲染结果。我在一个项目里遇到的现象是隐藏了楼上楼层楼上打下来的阴影却还留在当前楼层上看起来很怪。后来处理方式是切换楼层时同步遍历该楼层所有Mesh把castShadow和receiveShadow一起关掉问题才消失。对于更进阶的自动漫游我一般会预定义一条沿房间核心动线的路径用三次贝塞尔曲线让相机沿着路径移动。整个过程类似“飞机沿着航道巡航”用户能看清楚每一间房的结构又不会因为手动拖动而迷失方向。3.2 选择物体的高亮与Raycaster拾取“Threejs选择物体的高亮”是看房场景里最常被搜索的问题之一核心其实就两步第一步用Raycaster算出鼠标点中了哪个物体第二步给这个物体加高亮效果。Raycaster的实现很多人写过但真正写对的人不多。最容易出问题的是坐标转换鼠标事件拿到的clientX、clientY要先转换成WebGL归一化设备坐标NDC转换时一定要考虑canvas元素本身的位置偏移不能直接用window.innerWidth去算。我之前就是没减掉canvas距离页面顶部的偏移导致在带导航栏的页面上点击偏差越来越大。一个相对完整的写法是这样的const raycaster new THREE.Raycaster(); const pointer new THREE.Vector2(); let currentHighlight null; function onPointerMove(event) { const rect renderer.domElement.getBoundingClientRect(); pointer.x ((event.clientX - rect.left) / rect.width) * 2 - 1; pointer.y -((event.clientY - rect.top) / rect.height) * 2 1; raycaster.setFromCamera(pointer, camera); const hits raycaster.intersectObjects(interactGroup.children, true); if (hits.length 0) { clearHighlight(); return; } // 沿父级链找到可交互节点 let target hits[0].object; while (target !target.userData.interactive) { target target.parent; } if (target) setHighlight(target); }高亮方式我尝试过三种直接改材质颜色、加发光材质、用后处理描边。改颜色最简单但会把原材质覆盖掉还原的时候容易残留问题。加发光材质emissive效果温和不改变底色对用户来说视觉干扰小我比较推荐。后处理描边OutlinePass视觉最突出但会拉高整体渲染开销低端手机上不建议默认开启可以让用户手动切换“高亮模式”。实际项目中我采用的折中方案是把原材质的emissiveIntensity临时调高配合一个亮色结束后再恢复到原值这样既不用复制材质效果也足够明显。3.3 点击交互与信息展示的细节处理看房场景里点击一个热点户型、家具、楼栋弹出信息面板是标准的交互闭环。但不能忽略一个细节用户可能是在“拖拽旋转视角”而不是在“点击”。如果手指一碰就弹窗旋转视角时会误触得很烦。解决办法是区分点击和拖拽。我在pointerdown时记录起始坐标在pointerup时计算位移距离如果小于5px才认为是点击再走射线拾取和弹窗逻辑否则判定为拖拽继续旋转视角。这个阈值亲测在移动端体验很好。信息展示层我有两种做法。轻量情况下可以直接用HTML绝对定位弹窗和Three.js canvas叠加代码最简单如果想要3D空间里的标签跟随物体可以用CSS2DRenderer把标签挂在物体节点上相机转动时标签保持面向用户。看房案例我建议先用HTML弹窗实用性优先后续再做3D标签也不迟。4. 性能优化与移动端适配4.1 合并几何体对draw call的改善上面说了合并几何体的原理这里补一个实际案例数据。某次优化一个两室户型场景原始模型大概1200个独立Meshdraw call接近1300中端安卓手机FPS只有20上下转动视角明显卡顿。把墙面、地板、天花板、固定柜体分组合并后draw call降到40左右手机FPS稳定在55到60。记住一个粗略的量化结论移动端尽量把draw call控制在100以内场景越大越要狠心合并。除了合并几何体还可以把相同材质的Mesh批量合并成组或者使用InstancedMesh渲染重复的桌椅、灯具等实例收益都非常显著。4.2 纹理压缩、裁剪与像素比控制性能优化不止几何体。纹理这块KTX2压缩是必须的如果业务场景里没法上KTX2至少把纹理压缩成WebP或JPEG尺寸控制在不超过模型实际需要的大小。一张4096的贴图在手机上可能就占几十MB内存压缩到1024后体感和观感差别不大但内存占用和加载耗时都会明显下降。相机裁剪平面也值得注意。far值不要一味设很大几层楼的看房场景far设到200米足够。near和far的比值如果太悬殊会出现深层场景的深度精度问题——远处墙体和家具互相乱闪这种问题排查起来很费神不如一开始就把范围控制好。渲染器初始化时有一个参数很多人不在意但移动端特别关键renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));部分安卓机设备像素比高达3甚至4不做限制的话渲染分辨率几倍增长GPU压力呈指数上升。压到2之后肉眼几乎看不出清晰度差异但流畅度提升非常明显。4.3 按需加载与场景分块较大的看房项目比如一整栋楼几十层一次性加载全部模型基本不可行。我习惯用两个方案一层一买的项目就把整层模型拆成独立GLB切换楼层时按需加载小户型项目则把家具和硬装拆成两个资源先加载硬装让用户看到房间框架家具资源后台加载完成后渐显。这种“先结构后软装”的策略能极大改善首屏体验。楼层之间还可以用LOD思想用户在楼栋外观视角时使用低面数模型展示拉近进入室内后再替换高精度模型。用Three.js的LOD对象或者手动切换组都可以看项目复杂度选择。5. 常见问题与排查技巧5.1 高频问题速查表我整理了做Three.js看房案例时最常遇到的一批问题直接放表问题现象可能原因排查思路与解法模型加载白屏GLB路径不对或Draco decoder未部署打开控制台看网络请求确认是否404检查decoder路径配置高亮点击位置不准屏幕坐标转换时没减去canvas偏移使用getBoundingClientRect计算NDC不要直接用innerWidth合并后出现黑面几何体法线缺失或UV不统一merge前统一执行computeVertexNormals并检查UV层是否齐全合并后无法单独高亮原Mesh被合并对象丢失交互物体单独放入interactGroup只合并静态组切换楼层后阴影残留被隐藏楼层的Mesh仍参与阴影计算切换时同步遍历并关闭对应Mesh的castShadow/receiveShadow手机端旋转卡顿draw call过高或pixelRatio未限制合并几何体、压缩纹理并将pixelRatio限制到2以下点击会误触弹窗拖拽和点击未区分用pointerdown/up位移距离判断小于阈值才算点击5.2 容易忽略的Raycaster性能与准确性细节射线拾取如果场景里交互物体特别多性能也可能成为瓶颈。比如整栋楼每层100个可点击的家具5层就是500个每次鼠标移动都做完整射线检测在手机上是有压力的。优化思路是“降低检测频率”和“缩小检测范围”鼠标移动时不实时检测改为“节流”每80毫秒检测一次或者只有当射线指向某个楼层Group时才检测该组的子物体。更精细的做法是给每个交互物体维护一个粗糙的包围盒先用射线和包围盒做粗筛通过后再做精确Mesh相交。这样能把Raycaster的计算量降低一个量级。5.3 模型加载与显示的常见问题模型加载这块如果只看到白屏没有报错第一反应看控制台的Network面板确认GLB文件有没有真正到达浏览器。如果用了Draco压缩还要确认decoder路径对不对常见的错误是只用GLTFLoader加载忘了把DRACOLoader塞进去或者decoder目录根本没有拷贝到静态服务器上。还有一个模型显示上的经典问题导出时忘记应用旋转和缩放Apply Transform导致模型进入Three.js后旋转轴混乱或尺寸不对。解决方法是在Blender或3ds Max里先CtrlA应用全部变换再导出这能省掉后期大量代码层面的修正。一些实际操作后的体会做了几个看房项目之后我的体感非常明确这种Web 3D应用真正烧时间的从来不是“让立方体转起来”这种入门效果而是资源管线和交互状态管理这两大块。特别是看房这种对加载速度、拾取精度和交互反馈都很敏感的场景模型不压缩、几何体不合并、点击不区分拖拽随便哪一项没处理好都可能在手机上直接劝退用户。所以我现在的习惯是每次动手前先画一个“交互清单”哪些物体需要独立拾取、哪些可以合并、哪些用后处理高亮、哪些用发光材质。把这些边界理清楚了后面代码基本不会乱。这个看房案例里合并几何体和高亮拾取是连接最紧密的两个模块正好对应“性能”和“交互”两个核心命题。再往后扩展你还可以在基础上做AR家具摆放、日照模拟、多人实时同看直播带看但不管上层功能怎么长根基始终是用好GLTF资源管线、控制好draw call、把拾取和高亮交互做扎实。这套思路换到线上展厅、电商3D展示、虚拟售楼处也都是一样的。本文还有配套的精品资源点击获取