家具加到50个以后,为什么转视角开始卡了?
前言
前面六篇,SpaceRoom 从空房间做到了能选家具、能转视角。房间里放个十来件家具,跑起来很流畅。
然后我放了 50 件家具进去,问题来了:转视角的时候开始掉帧,拖一下卡一下,内存也涨了不少。
第一反应是:手机性能不够吧?然后我打印了一下帧率,发现还真不是 GPU 跑不动,而是很多地方做了不必要的工作。
这一篇就讲:3D 场景卡顿,不能只盯着 GPU。节点数量、纹理大小、重复材质、不可见对象、每帧的逻辑更新,这些都会影响性能。先把性能数据测出来,才知道该优化哪里。
一、先测数据:卡顿到底卡在哪
很多人做性能优化,上来就"模型减面、纹理压缩",结果做完发现帧率还是上不去。因为你都不知道瓶颈在哪,优化就是瞎猜。
先建一个性能监控,把关键指标打出来:
exportclassScenePerformanceMonitor{privateframeCount:number=0;privatelastTime:number=0;privatefps:number=0;// 统计数据privatestats={nodeCount:0,// 节点总数meshCount:0,// Mesh数量materialCount:0,// 材质数量textureCount:0,// 纹理数量memoryMB:0,// 内存占用initTimeMs:0,// 场景初始化耗时frameTimeMs:0// 每帧耗时};/** * 每帧调用,统计FPS */onFrame():void{this.frameCount++;constnow=Date.now();if(now-this.lastTime>=1000){this.fps=this.frameCount;this.frameCount=0;this.lastTime=now;console.info(`PerfMonitor: FPS=${this.fps}, nodes=${this.stats.nodeCount}, meshes=${this.stats.meshCount}`);}}/** * 收集场景统计 */collectStats(scene:scene.Scene):void{// 递归统计节点数this.stats.nodeCount=this.countNodes(scene.getRoot());console.info('PerfMonitor: stats collected',this.stats);}privatecountNodes(node:scene.Node):number{letcount=1;constchildren=node.getChildren();for(constchildofchildren){count+=this.countNodes(child);}returncount;}}先把数据打出来,才能知道问题在哪。常见的问题有:
| 指标 | 正常范围 | 异常表现 |
|---|---|---|
| FPS | 50~60 | 低于30就明显卡 |
| 节点数 | 几百个以内 | 上千个就要注意 |
| 纹理内存 | 几十MB | 超过100MB就吃紧 |
| 每帧耗时 | 16ms以内 | 超过33ms就是30帧 |
二、最常见的优化:不可见对象不渲染
第一个最容易做的优化:相机看不到的物体,就不要渲染了。
比如房间里有个柜子,柜子门是关着的,柜子里面的东西相机根本看不到。但如果柜子里面的模型也在渲染,就是浪费。
更常见的情况:用户视角转到另一边,左边的家具完全在视野外面,但还是每帧都在算。
这个叫视锥体裁剪(Frustum Culling):相机视野是个锥形,锥形外面的物体直接跳过渲染。
ArkGraphics 3D 应该会自动做这个,但我们自己也可以加一层:离相机太远的小物体,直接不渲染。
exportclassVisibilityManager{privatecameraPos:{x:number,y:number,z:number}={x:0,y:1.6,z:5};/** * 每帧更新可见性 */updateVisibility(nodes:scene.Node[]):void{for(constnodeofnodes){constpos=node.position;// 算一下离相机多远constdx=pos.x-this.cameraPos.x;constdy=pos.y-this.cameraPos.y;constdz=pos.z-this.cameraPos.z;constdist=Math.sqrt(dx*dx+dy*dy+dz*dz);// 超过30米的小物体,直接隐藏if(dist>30){if(node.visible){node.visible=false;}}else{if(!node.visible){node.visible=true;}}}}}这个优化很简单,但效果明显。尤其是大场景里,远处的小物体全部跳过渲染,帧率马上就上来了。
三、重复材质和纹理:别每个家具都来一份
第二个常见问题:10把椅子,每把椅子都加载了一份纹理。其实椅子都是一样的,纹理只需要一份,所有椅子共享。
这个和之前说的 Resource 缓存是一个道理,但材质和纹理也要做缓存:
exportclassMaterialCache{privatematerialMap:Map<string,scene.Material>=newMap();/** * 获取共享材质 */getMaterial(texturePath:string):scene.Material|null{// 已经有了直接返回if(this.materialMap.has(texturePath)){returnthis.materialMap.get(texturePath)!;}// 没有就创建constmaterial=...// 创建材质,加载纹理this.materialMap.set(texturePath,material);returnmaterial;}}10把椅子共享一份布料纹理,纹理内存直接省了 90%。这个优化在家具多的时候效果特别明显。
四、别每帧都更新不需要变的东西
第三个问题:每帧都在做不必要的逻辑更新。
比如:家具的位置从来没变过,但每帧都在重新算它的世界坐标;UI 状态从来没变过,但每帧都在刷新;动画已经停了,但每帧还在检查。
这些看起来都是小事,但每帧省一点,加起来就是帧率提升。
一个原则:只有真的会变的东西,才每帧更新。
| 东西 | 什么时候更新 |
|---|---|
| 相机位置 | 手势操作的时候更新,不是每帧 |
| 家具位置 | 用户拖动的时候更新 |
| 选中高亮 | 选中状态变化的时候更新 |
| 可见性 | 相机移动的时候更新,不是每帧 |
很多人写代码习惯了 onFrame() 里什么都做,结果就是每帧跑一大堆没用的逻辑。
五、几个最容易踩的性能坑
把这一篇遇到的坑总结一下:
| 坑 | 现象 | 解决办法 |
|---|---|---|
| 上来就减面压缩纹理 | 做了半天帧率没提升 | 先测数据,找到瓶颈再优化 |
| 所有物体都渲染 | 视野外的物体也在算 | 视锥体裁剪,远处的隐藏 |
| 每个家具一份纹理 | 纹理内存爆了 | 相同材质纹理共享缓存 |
| 每帧都跑所有逻辑 | CPU占用高,帧率上不去 | 只在状态变化的时候更新 |
| 退出页面不释放资源 | 退出再进,内存越来越大 | 页面销毁时释放所有资源 |
| 只看FPS不看内存 | 帧率还行,但App被系统杀了 | 内存和帧率都要监控 |
3D 性能优化不是玄学,是先测数据、再找瓶颈、然后针对性优化。不要上来就瞎优化。
总结
第七篇的核心就一句话:卡顿不能只怪 GPU,先测数据再优化。
- 先建性能监控,把 FPS、节点数、内存都打出来
- 相机看不到的物体,直接隐藏不渲染
- 相同的材质和纹理,所有家具共享,不要每个都来一份
- 只有真的会变的东西,才每帧更新
- 退出页面记得释放资源,不然内存越积越多
SpaceRoom 现在家具多了也能流畅跑了。最后一篇做工程收尾:加动画、管生命周期,把整个项目整理成一个可维护的工程。