尧图精选

Three.js三维房间搭建实战:从渲染管线到交互与性能优化

🕒 发布时间:2026/9/9 0:18:07 📁 来源:尧图网络
简介以three.js为核心的Web3D三维房间构建项目面向前端开发者和三维可视化初学者重点解决从二维平面图到可交互三维空间的转换难题。资源采用模块化实现方式包含完整HTML页面、OrbitControls相机控制脚本和核心JavaScript逻辑同时配套docx说明书与txt操作指南系统讲解几何体建模、材质纹理贴图、光源布置与阴影计算以及基于Raycaster的事件交互等关键环节。整个压缩包共5个文件大小仅254KB结构简洁、即下即用适合课程设计、毕业设计或Web3D入门实践。目前已有2305人学习下载学习热度持续上升。通过研读代码与文档读者可快速掌握three.js构建室内场景的完整工作流既能独立实现房间搭建与视角漫游也能为后续虚拟现实、增强现实项目积累扎实基础。1. 为什么用Three.js做三维房间一次选型复盘1.1 Web 3D不是只有Three.js但它是综合成本最低的路线在动手写代码之前先把选型这件事讲透。市面上做Web 3D的路线不少Unity导出WebGL、Babylon.js、Three.js、纯手写WebGL还有这两年逐渐冒头的WebGPU方案。我在不同项目里轮着试过几条最终一直坚持下来的组合是Three.js打底。如果是做房间、展厅、家具展示这类偏静态或轻交互的场景Three.js的综合成本最低。这不是说其他路线不行而是它们各自的代价在你的项目里未必划得来。Unity导出WebGL有个老问题包体积太大一个空场景都动辄几十MB首屏加载慢移动端表现也不稳定。Babylon.js功能确实全但API的复杂度比Three.js高一个量级一个人维护项目时排坑效率会明显下降。纯手写WebGL适合想深入理解图形学的朋友但不适合做产品光是矩阵变换、着色器编译、视锥剔除这些基础工作就足够拖垮整个迭代节奏。Three.js恰好卡在“功能够用、抽象不过分”的位置文档和示例又多几乎每个问题都能搜到现成答案。房间这种高度标准化的场景选择Three.js基本不会被底层问题卡住。1.2 理清Three.js的渲染管线场景、相机、渲染器三件套开始写代码之前先建立整体认知。所有Three.js项目都绕不开三个核心对象Scene场景、Camera相机、Renderer渲染器。用电影打个比方Scene是舞台和所有演员Camera是你手里的取景镜头Renderer是最终把画面投到屏幕上的放映机。三者各司其职配合动画循环里反复调用的render方法完成输出。// 场景承载所有物体、灯光、相机 const scene new THREE.Scene(); // 相机透视相机四个参数是视场角、宽高比、近裁剪面、远裁剪面 const camera new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 100 ); // 渲染器负责把场景和相机视角渲染到canvas上 const renderer new THREE.WebGLRenderer({ antialias: true }); renderer.setSize(window.innerWidth, window.innerHeight); document.body.appendChild(renderer.domElement); function animate() { requestAnimationFrame(animate); renderer.render(scene, camera); } animate();这段代码是所有Three.js项目的骨架。不少人一开始就图省事把near设成0.001、far设成10000觉得范围大总没错。实际上深度缓冲的精度在近处集中、远处分散near太小而far太大时房间不同墙面之间的深度差值会被压缩得极小画面会出现墙面闪烁、物体前后穿插的z-fighting问题。做10米以内的房间场景near取0.1、far取100足够既能保证深度精度又能完整覆盖房间范围。这套参数不是随便拍的是经过多次调试验证过的稳妥值。1.3 我搭建房间前的坐标系与单位规划先说坐标系。Three.js默认采用右手坐标系X轴向右、Y轴向上、Z轴指向屏幕外。我做房间时有一套固定约定地面放在Y0平面Y轴正方向是房间高度方向房间的长度沿X轴、宽度沿Z轴。这个约定没有强制力但统一之后计算家具位置、相机高度、碰撞检测都会顺畅很多。团队协作时坐标系约定还能避免因为轴方向不同导致的模型旋转错乱。单位规划同样容易被忽略等到后患爆发才后悔。Three.js本身不强制单位1单位到底是1米还是1厘米完全由你定义。做室内房间展示我强烈建议约定1单位1米这样真实世界的尺寸可以直接填门高2米、床长2米、人眼高度1.6米零换算成本。如果你做的是小型零件再改成1单位1厘米做城市沙盘可以改成1单位1公里。单位问题在纯渲染阶段不会暴露等你加物理引擎、做尺寸测量标注时才会集中爆发到时候再改模型位置的代价非常高前期花十分钟把单位定下来后面会省下大把返工时间。2. 房间的“壳”怎么搭从地面到四面墙的具体顺序2.1 用BoxGeometry做墙体还是逐面拼BufferGeometry搭建房间的“壳”有三种常见做法我在项目里都用过说说各自的实际表现。第一种直接用BoxGeometry把每面墙拉成薄长方体四面墙加地板需要天花板就再加一面独立放置。第二种做一个足够大的BoxGeometry盒子材质面设为THREE.BackSide人直接站在盒子内部去看。第三种手写BufferGeometry逐面拼接顶点、法线和索引生成地面和墙面。直接说结论做内部展示时用独立BoxGeometry墙体是性价比最高的方案。虽然会多画几个看不见的外部面多消耗一点点性能但换来的是维护上的巨大便利——每面墙是独立对象改厚度、开个洞、挂装饰画都只动对应那面墙。BackSide方案看似省面实际上法线方向是反的光照计算和纹理方向都会出问题调试成本高得吓人。手动BufferGeometry性能天花板更高但要处理顶点顺序、法线方向、UV展开开发效率低适合有平台级需求的团队个人项目快速落地时不推荐。// 一面5米宽、3米高、0.2米厚的墙 const wallGeometry new THREE.BoxGeometry(5, 3, 0.2); const wallMaterial new THREE.MeshStandardMaterial({ color: 0xffffff }); const wall new THREE.Mesh(wallGeometry, wallMaterial); wall.position.set(0, 1.5, -2.5); // 放在房间的Z轴负方向 scene.add(wall);三种方式的差异可以类比成独立BoxGeometry是乐高积木拼装灵活但零件多一些BackSide是把纸箱拆开反过来用节省空间但图案和文字全反了BufferGeometry是3D打印精度上限高但需要图纸和机器。个人项目求快求稳多数时候选乐高积木就够了。2.2 地板和天花板的处理大平面铺贴与纹理重复地面和天花板的建法比较直接PlaneGeometry或者BoxGeometry都可以。如果考虑地板的实际厚度用0.1米厚的BoxGeometry会更有材质感也方便后续加踢脚线之类的细节。天花板同理厚度可以薄一点视觉上几乎看不出来但碰撞检测时能多一层保障。地板的大坑在纹理。一个6米乘6米的房间如果直接拿一张1024像素的瓷砖贴图铺满每块瓷砖会被拉伸得完全失真看起来像糊了一层带花纹的纸。正确做法是把纹理的wrapS、wrapT都设为THREE.RepeatWrapping再根据真实尺寸计算重复次数。const textureLoader new THREE.TextureLoader(); const floorTexture textureLoader.load(textures/floor.jpg); floorTexture.wrapS THREE.RepeatWrapping; floorTexture.wrapT THREE.RepeatWrapping; // 贴图假设是一平米一块的瓷砖6米x6米的地板需要重复6x6次 floorTexture.repeat.set(6, 6); const floorMaterial new THREE.MeshStandardMaterial({ map: floorTexture, });纹理重复以后还有一个很容易被忽略的细节各向异性过滤anisotropy。斜着看地面时远处纹理如果没有各向异性过滤会迅速糊成一片画面显得很脏。Three.js里一行代码就能解决直接把值拉满就行floorTexture.anisotropy renderer.capabilities.getMaxAnisotropy();这一行能让地面和墙面的斜视清晰度提升一个量级性能开销极小强烈建议在所有带纹理的物件上都养成这个习惯。2.3 门窗开洞的正确姿势与法线问题门窗开洞是房间搭建里最考验经验的一环。用BoxGeometry做好整面墙之后想直接“挖”一个门洞出来会发现它没有提供布尔运算接口。实际操作里有两种主流方案。方案一用三个长方体拼出门洞两侧的墙段再在门洞上方补一块短过梁。这种方式的代码量少不涉及复杂几何运算视觉上完全没问题缺点是墙体在逻辑结构上不是一个整体。方案二用THREE.CSG做布尔运算把墙体长方体和门洞长方体做减法能生成真正的“洞口”但布尔运算后的几何体三角面数量会明显增加而且几乎每次都要检查法线方向否则墙面会大面积发黑、光照完全错乱。我在实际项目中的选择标准是门洞位置固定、不需要参数化调整时就用拼块方案省心稳定要做“用户能自己改门洞大小高度”的可配置功能时才上CSG。很多人一开始就想用最“高级”的做法但房间场景的最终评价标准是稳定和好看不是算法复杂度。那两个洞最后看起来一样但拼块方案省下的调试时间足够你把整个房间的灯光调得更舒服。3. 光打对了房间才立得住光照方案与阴影配置3.1 三光源体系的搭建环境光、方向光、点光源房间模型建得再细致光照不对出来的效果都是“塑料感”。这是新手最容易困惑的地方明明材质没问题颜色也对看起来就是不舒服。绝大多数情况下原因出在光照方案上——要么只有一个环境光要么只有一个方向光明暗关系没有层次。我在房间项目里的标配是“环境光方向光点光源”的三光源体系。环境光AmbientLight给整个场景兜底照明保证背光面不会漆黑一片方向光DirectionalLight模拟太阳或主照明决定物体的体积感和明暗方向点光源PointLight负责模拟吸顶灯、台灯、筒灯这类局部光源制造近处的暖光层次。// 环境光整体提亮强度别太高 const ambientLight new THREE.AmbientLight(0xffffff, 0.4); scene.add(ambientLight); // 方向光模拟窗外进来的主光 const dirLight new THREE.DirectionalLight(0xffffff, 1.2); dirLight.position.set(5, 8, 3); dirLight.target.position.set(0, 0, 0); scene.add(dirLight); scene.add(dirLight.target); // 点光源模拟房间中央的吸顶灯 const pointLight new THREE.PointLight(0xfff5e6, 1.5, 10, 2); pointLight.position.set(0, 2.8, 0); scene.add(pointLight);方向光的target属性容易被漏掉。DirectionalLight有一个明确的照射目标如果不把target加入场景它默认指向原点但如果你把光的位置设得比较偏目标却不调整光照方向会变得很怪。上面代码里的scene.add(dirLight.target)一步都不能省。点光源的第四个参数decay是衰减指数。新版Three.js默认decay为2对应物理上真实的平方反比衰减。但配合强度较低的光源时画面可能显得很暗。想要室内设计渲染图那种均匀明亮的效果可以把decay调到1或者把强度调大。这个参数没有标准答案适合放进GUI面板里随时拖动边拖边看反馈。3.2 阴影的开关不是万能的shadow map参数调优阴影是房间立体感的重要来源也是性能杀手。新手常见操作是给所有灯都挂上castShadow结果画面要么整体变暗要么阴影边缘锯齿粗糙要么帧率崩掉。理解阴影的原理后你就知道该怎么做。Three.js默认的阴影方案是Shadow Map从光源视角渲染一张深度图再在相机渲染时比较每个片元的深度判断它是否被遮挡。Shadow Map本身没有方向性但它的分辨率直接决定阴影边缘的细腻程度。renderer.shadowMap.enabled true; renderer.shadowMap.type THREE.PCFSoftShadowMap; dirLight.castShadow true; dirLight.shadow.mapSize.width 2048; dirLight.shadow.mapSize.height 2048; dirLight.shadow.camera.near 0.5; dirLight.shadow.camera.far 20; dirLight.shadow.camera.left -5; dirLight.shadow.camera.right 5; dirLight.shadow.camera.top 5; dirLight.shadow.camera.bottom -5; // 地面和家具必须分别设置成可接收和可投射阴影 floorMesh.receiveShadow true; sofaMesh.castShadow true;mapSize从1024提到2048阴影边缘会有肉眼可见的改善但开销也明显上升。房间这种小场景2048到4096已经到顶了再往上性价比很低。另一个容易踩的坑是dirLight.shadow.camera的六个边界参数它们定义阴影计算的有效范围。如果范围远大于房间尺寸Shadow Map的像素会被浪费在一大堆空场景上阴影边缘自然就糊。实践中的做法是让这六个边界紧密贴合房间尺寸再用mapSize控制质量。还有个容易被忽视的小坑开启阴影后如果忘了给地面设置receiveShadow、给家具设置castShadow这些物体虽然渲染正常但完全没有投影效果。调阴影时务必逐个确认Mesh的castShadow和receiveShadow设置这个忘了排查起来很费时间。3.3 灯光颜色与色温对空间氛围的影响灯光颜色是空间氛围的点睛之笔也是“效果图”和“白模渲染”拉开差距的地方。Three.js中灯光的color属性直接决定空间冷暖基调。我用得比较多的预设包括暖白0xfff0e0模拟白天自然光暖黄0xffd9a0模拟晚上家居灯冷白0xe8f0ff模拟偏白的人工光。做客厅示例时我的习惯是主方向光用中性偏暖的白模拟白天光照中央点光源用暖黄色模拟吊灯电视墙附近加一盏淡蓝色点光源模拟屏幕辉光饱和度很低但能明显提升氛围。为了调试方便建议做三组预设——白天模式、夜晚模式、展示模式用GUI面板一键切换比反复改代码刷新高效得多。如果你是从老版本Three.js升级过来的项目要注意r155之后useLegacyLights的默认值从true改成了false。这意味着光照强度更接近真实物理单位同样的强度数值在老版本里刚刚好新版本里可能会过曝。迁移时要么把所有灯光强度统一调低要么设置renderer.useLegacyLights true避免大面积返工。4. 从白模到真实房间材质、纹理与贴图排布4.1 PBR材质的核心参数粗糙度、金属度、贴图通道让一个房间摆脱“塑料感”关键不是换一张更漂亮的图而是正确使用材质系统。Three.js的MeshStandardMaterial基于PBRPhysically Based Rendering基于物理的渲染实现核心参数是两个粗糙度roughness和金属度metalness。粗糙度控制物体表面散射程度取值范围0到1。0是完全镜面1是完全哑光。房间里的墙面、布艺沙发、木地板粗糙度一般在0.6到0.9之间金属灯罩、不锈钢桌腿粗糙度在0.1到0.3之间。金属度表示材质是否属于金属非金属统一设成0或很小的值金属材质设成1。很多新手把金属度拉得很高想做出“金属感”结果物体像涂了一层清漆的塑料表面出现强烈镜面反射。这是因为金属度改变了漫反射和高光的计算方式用错会让材质在物理上不成立。不同材质可以参考以下参数起步材质粗糙度金属度备注白墙0.850.0可加轻微法线贴图木地板0.60.05配木纹贴图更佳布艺沙发0.950.0高粗糙度无反射金属落地灯0.21.0配环境贴图才有反射感玻璃茶几0.050.0需要透明通道配合4.2 纹理平铺与UV重复让一面墙的花纹不糊材质参数之外贴图通道设计也是关键。一张完整PBR贴图组通常包含固有色albedo、法线normal、粗糙度roughness、金属度metalness和环境光遮蔽AO几个通道。项目早期如果没有完整通道可以只加载固有色贴图粗糙度和金属度用固定值写死之后再逐步补法线和AO。法线贴图是我在所有项目里都坚持加的一种贴图。它通过改变表面法线方向来模拟凹凸细节不增加几何面数就能让墙面、木纹、瓷砖获得很强立体感。Three.js用法线贴图非常简单const material new THREE.MeshStandardMaterial({ map: textureLoader.load(textures/wall_albedo.jpg), normalMap: textureLoader.load(textures/wall_normal.jpg), roughnessMap: textureLoader.load(textures/wall_roughness.jpg), normalScale: new THREE.Vector2(0.8, 0.8), });normalScale控制法线强度。远距离观察时1.0没问题近景展示时法线强度太高表面会出现“鼓包”感建议调到0.5到0.8之间。这个值同样适合放调试面板实时调。墙面纹理还有一个新手容易踩的坑瓷砖或木地板的纹理接缝。当纹理重复次数是整数倍时相邻两块瓷砖交界处的花纹如果对不上一眼就能看出破绽。解决办法有两个一是选择本身设计成“无缝拼接”的贴图素材二是用Photoshop或在线工具把贴图四边改造成可平铺状态。无缝贴图的质量直接决定房间质感这一块别省时间。4.3 从外部导入模型glTF格式与纹理路径坑房间里除了硬装墙面还要放沙发、床、灯、装饰画这些家具。有现成模型时导入外部模型是最快方案。我强烈建议使用glTF/GLB格式而不是传统OBJ或FBX。glTF是Khronos Group推动的3D传输格式原生支持PBR材质、骨骼动画、纹理引用和Three.js的配合基本是最优的。import { GLTFLoader } from three/examples/jsm/loaders/GLTFLoader.js; const loader new GLTFLoader(); loader.load( models/sofa.glb, (gltf) { const model gltf.scene; model.scale.set(1, 1, 1); model.position.set(1, 0, -1); scene.add(model); }, undefined, (error) { console.error(模型加载失败, error); } );导入模型有两个高频坑。第一个是纹理路径。glTF规范要求纹理和.gltf文件使用相对路径引用如果模型文件和纹理文件夹层级不对或者打包时改了静态资源路径加载时会报404。处理方式是模型和纹理放同一资源目录、保持相对路径或者加载后统一修正所有Mesh的纹理路径。第二个是模型单位。3D建模软件里常用厘米导出的模型在Three.js里会显得巨大无比——一个沙发可能变成一座山。加载后要先调整scale再放到房间坐标系里这个步骤几乎每次都要做。5. 让用户“走”进房间观察控制与交互反馈5.1 OrbitControls最短路径获得可旋转的房间视图绝大多数房间展示项目第一版交互只需要一个功能让用户围着房间转着看。Three.js官方提供的OrbitControls几乎是为这个需求设计的支持鼠标拖拽旋转、滚轮缩放、右键平移。import { OrbitControls } from three/examples/jsm/controls/OrbitControls.js; const controls new OrbitControls(camera, renderer.domElement); controls.target.set(0, 1.5, 0); // 看向房间中心 controls.enableDamping true; // 阻尼让旋转顺滑 controls.dampingFactor 0.08; controls.maxPolarAngle Math.PI / 2.1; // 限制俯仰角防止穿到地板下 controls.minDistance 0.5; controls.maxDistance 15; controls.update();用OrbitControls有几个细节值得记住。enableDamping建议一直开着否则鼠标松开的一瞬间画面会突然停住手感很生硬。maxPolarAngle限制相机绕水平面的俯仰角度通常限制在90度以内避免用户把相机转到房间正下方、穿到地板底下。minDistance和maxDistance限制缩放范围防止相机拉进墙体内部或者退得太远看不到房间整体。记得在动画循环里持续调用controls.update()否则阻尼效果不会生效。5.2 第一人称漫游用PointerLockControls控制行走当需求进一步升级希望用户像游戏里那样在房间里自由走动就需要第一人称控制了。Three.js官方提供PointerLockControls基于浏览器的Pointer Lock API实现鼠标视角控制再配合键盘的WASD做位移。import { PointerLockControls } from three/examples/jsm/controls/PointerLockControls.js; const controls new PointerLockControls(camera, renderer.domElement); document.getElementById(enterBtn).addEventListener(click, () { controls.lock(); });第一人称漫游的核心是每一帧读取键盘状态并更新相机位置。移动方向要基于相机当前朝向不能按固定世界坐标方向走。比如按W时应该沿着相机面朝的那条水平线前进而不是沿着世界Z轴方向。const keys {}; document.addEventListener(keydown, (e) (keys[e.code] true)); document.addEventListener(keyup, (e) (keys[e.code] false)); const walkSpeed 0.08; function animate() { requestAnimationFrame(animate); if (controls.isLocked) { // 获取相机朝向在水平面的投影 const direction new THREE.Vector3(); camera.getWorldDirection(direction); direction.y 0; direction.normalize(); // 计算右方向向量 const right new THREE.Vector3(); right.crossVectors(direction, new THREE.Vector3(0, 1, 0)); if (keys[KeyW]) camera.position.addScaledVector(direction, walkSpeed); if (keys[KeyS]) camera.position.addScaledVector(direction, -walkSpeed); if (keys[KeyA]) camera.position.addScaledVector(right, -walkSpeed); if (keys[KeyD]) camera.position.addScaledVector(right, walkSpeed); } renderer.render(scene, camera); } animate();第一人称漫游最典型的坑就是穿墙。纯演示项目可以无所谓但正经产品必须有碰撞检测。最简单方案是用Raycaster射线检测朝移动方向发射一条短射线与墙体Mesh相交检测如果交点距离小于碰撞半径比如0.3米就禁止移动。用墙面数组加射线检测实现代码量不大效果立竿见影。const raycaster new THREE.Raycaster(); const wallMeshes [wall1, wall2, wall3, wall4]; function canMove(direction) { raycaster.set(camera.position, direction); const intersects raycaster.intersectObjects(wallMeshes); return intersects.length 0 || intersects[0].distance 0.3; }5.3 点击检测Raycaster拾取家具的完整流程房间展示的另一个常见需求是点击家具弹详情。Three.js里的标准做法是Raycaster拾取先把鼠标屏幕坐标转换成标准化设备坐标NDC然后从相机位置朝该方向发射射线检测射线与场景中Mesh的交点。const raycaster new THREE.Raycaster(); const mouse new THREE.Vector2(); renderer.domElement.addEventListener(click, (event) { // 屏幕坐标转NDC坐标范围是[-1, 1] mouse.x (event.clientX / window.innerWidth) * 2 - 1; mouse.y -(event.clientY / window.innerHeight) * 2 1; raycaster.setFromCamera(mouse, camera); const intersects raycaster.intersectObjects(furnitureGroup.children, true); if (intersects.length 0) { const obj intersects[0].object; // 向上查找绑定了业务数据的根节点 let target obj; while (target !target.userData.furnitureId) { target target.parent; } if (target) { showFurnitureDetail(target.userData.furnitureId); } } });intersectObjects的第二个参数传true表示递归检测所有子节点。这一步对导入的glTF模型尤其重要——模型往往是一个多层级的场景树真正的几何Mesh在叶子节点不递归检测就点不中。拿到命中对象后向上遍历parent找userData里的业务标识可以避免在每一个子Mesh上重复绑定数据。这套NDC坐标转换公式建议背下来后面做拾取、标注、画布交互都会反复用到。6. 性能优化与实测踩坑移动端不掉帧的底线6.1 Draw Call合并为什么房间一复杂就卡一个房间四面墙、地板、天花板、五六把椅子、一张桌子、几个灯具如果每个Mesh各自独立渲染一次一帧的Draw Call很容易冲到40到50次。移动端的老型号GPU在这个量级就开始吃力帧率掉得很明显。优化Draw Call是移动端不掉帧的第一道防线。最常用手段是合并静态几何体。Three.js官方提供了BufferGeometryUtils.mergeGeometries可以把一组几何体合并成一个同一个材质的多个物体因此只需要一次Draw Call。import { mergeGeometries } from three/examples/jsm/utils/BufferGeometryUtils.js; const merged mergeGeometries([ tableTopGeometry, tableLeg1.geometry, tableLeg2.geometry, ]); const table new THREE.Mesh(merged, woodMaterial);但合并不是万能的它有一个副作用合并之后每个零件作为独立物体被点击的能力就丢失了。想实现点击桌面弹出桌子的详情命中检测返回的会是合并后的整个Mesh你反而需要额外维护子区域信息复杂度不降反升。我的实践原则是纯静态、不需要交互的构件尽量合并比如天花板、整面墙、地板需要交互的家具保持独立Mesh通过控制家具数量来控制总Draw Call。通常房间场景把Draw Call控制在30以内移动端就不会有明显卡顿。6.2 纹理压缩与内存控制手机端加载的常见问题房间项目的资源大头基本压在纹理上。一张2048x2048的JPG贴图解码后占用的GPU内存大约是2048x2048x4字节也就是16MB。一个房间如果只有10张贴图就是160MB中低端手机很容易因此被系统强制回收甚至白屏。缓解办法有三条按优先级排序。第一控制单张贴图分辨率2048对多数室内场景已经足够没必要无脑上4096第二使用压缩纹理格式WebP等格式能在不太损失观感的前提下把解码后内存降到原来四分之一左右第三引入LOD思路远处物体用低分辨率贴图用户走近后再替换高清版本。Three.js没有内置一个开箱即用的LOD纹理方案但可以用距离检测或区域切换手动实现。项目早期做到前两点已经能规避大部分内存问题LOD可以等确实有需要再上。6.3 我在Three.js房间项目中踩过的三个必现Bug最后分享三个我反复踩过的坑它们几乎在每个新项目里都会重新登场一次。踩坑场景典型现象根因处理方式透明物件隔断遮挡后方物体、边缘黑线透明混合渲染顺序不正确材质depthWrite设为false透明物体手动排renderOrder窗口缩放圆变椭圆、比例失真投影矩阵宽高比未更新resize事件里同时更新camera.aspect并调用updateProjectionMatrixCDN纹理模型贴图不显示、控制台CORS报错浏览器同源策略拦截加载前设置loader.setCrossOrigin(anonymous)服务端配置CORS头第一个是透明物件渲染顺序错乱。Three.js默认按物体到相机的距离排序渲染但透明物体的混合模式需要特殊顺序。当房间里放了玻璃茶几、透明隔断这类物件时经常出现透明物体遮住后面的物体或者边缘出现黑色轮廓。解决办法是给透明材质设置depthWrite: false并对多个透明物体会显式管理renderOrder保证从远到近按序渲染。第二个是窗口缩放后的画面畸变。窗口大小一变化如果只调renderer.setSize而不更新camera的aspect画面就会拉伸或挤压。必须在resize事件里同时做两件事window.addEventListener(resize, () { camera.aspect window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); });怎么判断是不是犯了这个问题典型症状就是缩放窗口后圆形物体变成椭圆模型比例变得奇怪。原因就是投影矩阵没有随宽高比更新。第三个是纹理加载的跨域问题。模型或纹理放在CDN上时TextureLoader默认遵循浏览器的同源策略会直接抛CORS错误。这问题在本机开发时不出现往往部署上线后才暴露排查起来非常隐蔽。解法是加载前设置textureLoader.setCrossOrigin(anonymous)同时CDN服务端配置好CORS响应头两者缺一不可。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联 返回资讯列表 →