ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

《HarmonyOS 7 ArkGraphics 3D 空间设计开发实战》03:glTF模型加载、实例复用与资源生命周期【鸿蒙心迹】

《HarmonyOS 7 ArkGraphics 3D 空间设计开发实战》03:glTF模型加载、实例复用与资源生命周期【鸿蒙心迹】 同一把椅子放三次为什么加载了三次模型前言前两篇搭好了工程骨架也搞清楚了坐标体系。这一篇开始往房间里放真正的家具模型沙发、桌子、椅子、台灯、绿植、柜子。然后马上就遇到一个问题我在房间里放了三把一样的椅子放在不同位置。跑起来一看内存涨了不少加载时间也变长了。我当时第一反应是三把椅子而已至于这么慢吗然后打印日志才发现每放一把椅子模型都重新加载了一遍。三个椅子节点对应了三份完全一样的模型资源。这就是 3D 开发里另一个常见误区把 Resource模型资源和 Instance场景实例搞混了。一个椅子模型文件只需要加载一次到内存里然后可以创建出任意多个实例每个实例有自己的位置、旋转和缩放但共享同一份模型数据。这一篇就解决这个问题怎么建立模型资源缓存怎么区分 Resource 和 Instance以及模型异步加载回来的时候页面已经退出了怎么办。一、先搞清楚Resource 和 Instance 不是一回事很多初学者刚开始做 3D 的时候以为创建一个模型就是加载一次文件。放一把椅子加载一次放两把椅子再加载一次。这在家具少的时候看不出来家具一多内存和加载时间就都上去了。先把概念拆开概念是什么有几个举例Resource资源模型文件本身包含Mesh、Material、纹理同一个模型文件只加载一次chair.gltfInstance实例场景里的一个节点引用Resource同一个Resource可以有多个Instance三把椅子就是三个InstanceNode节点场景树里的对象有Position、Rotation、Scale每个Instance对应一个Node每个椅子有自己的位置打个比方Resource 就像是椅子的设计图纸Instance 就像是按照图纸做出来的真实椅子。图纸只需要一份但可以做出很多把椅子每把椅子可以放在不同的位置。如果每次放一把椅子都重新读一遍图纸那就太浪费了。二、第一版直接加载放一个加载一个先写第一版代码看看问题出在哪import{scene}fromkit.ArkGraphics3DKit;import{SceneManager}from./SceneManager;exportclassModelManager{constructor(privatesceneMgr:SceneManager){}// 加载椅子模型并放到指定位置addChair(position:{x:number,y:number,z:number}):void{constscenethis.sceneMgr.getScene();if(!scene)return;// 每次都重新加载模型文件constchairNodescene.createNode(Chair);// 加载 chair.gltfchairNode.loadModel(resources/models/chair.gltf);chairNode.positionposition;scene.getRoot().addChild(chairNode);console.info(ModelManager: chair added at,position);}}这个代码看起来没问题但如果我调用三次 addChair()放三把椅子就会加载三次 chair.gltf。这在小模型的时候可能感觉不出来但如果是沙发、床这种大模型每个都几MB甚至几十MB放几个就把内存占满了。更麻烦的是模型加载是异步的。如果用户连续点了五次添加椅子就会触发五次模型加载请求就算后面取消了加载还是会继续。三、第二版加资源缓存Resource 只加载一次改进一下加一个资源缓存表记录已经加载过的模型。同一个模型文件只加载一次后面直接复用。import{scene}fromkit.ArkGraphics3DKit;import{SceneManager}from./SceneManager;// 模型资源缓存interfaceModelResource{resource:scene.ModelResource|null;// 模型资源refCount:number;// 引用计数loading:boolean;// 是否正在加载}exportclassModelResourceManager{privateresourceCache:Mapstring,ModelResourcenewMap();constructor(privatesceneMgr:SceneManager){}/** * 加载模型资源带缓存 */asyncloadModelResource(modelPath:string):Promisescene.ModelResource|null{// 1. 先查缓存constcachedthis.resourceCache.get(modelPath);if(cachedcached.resource){cached.refCount;console.info(ModelResourceManager: cache hit,modelPath);returncached.resource;}// 2. 正在加载中等待加载完成if(cachedcached.loading){// 简单处理轮询等待实际工程里用Promise队列awaitnewPromise(resolvesetTimeout(resolve,100));returnthis.loadModelResource(modelPath);}// 3. 开始加载this.resourceCache.set(modelPath,{resource:null,refCount:0,loading:true});try{constscenethis.sceneMgr.getScene();if(!scene)returnnull;// 异步加载模型资源constresourceawaitscene.loadModelResource(modelPath);// 更新缓存this.resourceCache.set(modelPath,{resource:resource,refCount:1,loading:false});console.info(ModelResourceManager: model loaded,modelPath);returnresource;}catch(e){console.error(ModelResourceManager: load failed,modelPath,e);this.resourceCache.delete(modelPath);returnnull;}}/** * 创建模型实例 */createInstance(modelPath:string,position:{x:number,y:number,z:number}):scene.Node|null{constscenethis.sceneMgr.getScene();if(!scene)returnnull;constnodescene.createNode(modelPath_Date.now());// 实例使用已经加载好的资源node.setModelResource(this.resourceCache.get(modelPath)?.resource??null);node.positionposition;scene.getRoot().addChild(node);returnnode;}}现在放三把椅子只会加载一次 chair.gltf。后面两把椅子直接复用缓存的资源。这里有个关键概念引用计数。每创建一个实例refCount 加1删除一个实例refCount 减1。当 refCount 降到 0 的时候说明这个资源没人用了可以释放掉。四、模型异步加载回来页面已经退出了怎么办模型加载是异步的这个过程中可能发生很多事用户点了添加椅子然后马上退出了页面模型还在加载中Scene 已经被释放了加载完成的回调里sceneMgr 已经是空的了如果不处理这种情况就会出现页面都退出了日志还在打印模型加载完成甚至还会有内存泄漏。处理办法加载的时候记录一个是否有效的标记加载完成以后先检查一下asyncloadModelResource(modelPath:string):Promisescene.ModelResource|null{// ...前面的加载逻辑// 开始加载前记录当前是否有效constisValidtrue;try{constresourceawaitscene.loadModelResource(modelPath);// 加载完成后检查一下页面是不是还在if(!isValid||!this.sceneMgr.getScene()){console.warn(ModelResourceManager: page already closed, discard loaded resource);// 直接释放刚加载的资源resource?.release();returnnull;}// ...正常更新缓存}}这个问题在真实工程里特别常见。我第一次做的时候用户快速进出页面好几次内存涨得特别快就是因为每次加载的模型都没释放。五、删除实例的时候记得释放引用创建实例的时候 refCount 加了1删除实例的时候就得减1不然资源永远释放不掉。/** * 删除模型实例 */releaseInstance(node:scene.Node,modelPath:string):void{constscenethis.sceneMgr.getScene();if(!scene)return;// 从场景树移除节点node.removeFromParent();// 引用计数减1constcachedthis.resourceCache.get(modelPath);if(cached){cached.refCount--;console.info(ModelResourceManager: refCount,modelPath,cached.refCount);// 引用计数为0释放资源if(cached.refCount0){cached.resource?.release();this.resourceCache.delete(modelPath);console.info(ModelResourceManager: resource released,modelPath);}}}这个逻辑很简单但特别容易忘。很多人只记得创建实例不记得删除的时候要减引用计数结果就是退出页面以后所有模型资源都还留在内存里。六、几个最容易踩的资源坑把这一篇遇到的坑总结一下坑现象解决办法每个实例都重新加载模型放N个家具就加载N次文件加资源缓存Resource只加载一次忘了引用计数退出页面后内存不释放创建实例加1删除实例减1为0时释放异步加载回来页面已退出页面都没了还在加载模型加载完成后检查页面状态无效就丢弃用户连续点击创建触发重复加载请求加载中的模型不再发新请求等待即可Node删了但资源没释放节点从场景树移除了资源还在删除节点时同步减引用计数这些问题在家具少的时候都看不出来一旦房间里有几十上百个模型内存和性能问题就全暴露出来了。这也是为什么 Resource 和 Instance 要分开管理——不是为了写代码好看是为了真的能在手机上跑起来。总结第三篇的核心就一句话Resource 和 Instance 是两回事别把它们混在一起。同一个模型文件只需要加载一次多个实例共享同一份资源用引用计数管理资源生命周期实例创建加1实例删除减1为0时释放模型是异步加载的加载完成后要检查页面是不是还在节点从场景树移除的时候记得同步释放资源引用资源缓存不是什么高级优化是 3D 应用能跑起来的基本要求SpaceRoom 现在能加载多个家具了而且不会重复加载。下一篇解决视觉效果为什么东西都有了但看起来像塑料——材质、纹理和灯光到底是怎么决定 3D 场景质感的。
返回列表