ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

HarmonyOS 7 Spatial Recon Kit + ArkGraphics 3D:端侧 3DGS 场景加载与分块渲染实战【鸿蒙心迹】

HarmonyOS 7 Spatial Recon Kit + ArkGraphics 3D:端侧 3DGS 场景加载与分块渲染实战【鸿蒙心迹】 这次我没有把 3DGS 当成“放一个模型到页面上”的普通 3D 展示而是把问题收缩到真正影响端侧体验的一段链路插件加载、场景装载、相机驱动、分块请求、状态可视化和调试闭环。Demo 项目名统一为GaussField场景文件为courtyard.scene.json。做 3DGS 相关 Demo 时很容易出现一种错觉模型文件能打开说明接入已经完成。真正把它放到手机上跑一段时间之后会发现问题并不在“能不能显示”而在“怎么稳定地显示”。一个小场景可以整包加载大场景就要考虑可见区域、瓦片、相机变化、资源占用以及请求节奏。只要其中一环没有做状态收口最终看到的现象往往很像渲染问题实际却可能是加载链路没有建立完整。HarmonyOS 的 Spatial Recon Kit 把 3DGS 相关的重建、渲染和编辑能力放进了统一能力域在渲染侧spatialRender可以加载普通 3DGS 节点也可以在较新的能力版本中加载 Tiled 3DGS 场景。后者的价值不是“换一种文件格式”而是允许渲染器围绕相机视口按需请求瓦片让大场景不必一次把全部数据压进内存。这篇文章记录的就是我把一个院落 3DGS 场景接到 HarmonyOS 7 Demo 中的过程。最终目标很具体进入页面后状态从READY走到INTERACTIVE场景文件固定为courtyard.scene.json调试页能够看到24 / 96的瓦片请求进度、当前58 FPS以及最近一次请求到的tile_024.sog。一、先把“显示模型”和“端侧场景系统”分开我最开始做的版本只有两个动作初始化 Scene然后加载一个 3DGS 模型。这个版本看起来很干净但它隐藏了一个问题——UI 完全不知道底层现在处于什么状态。页面出现了模型不等于链路没有问题。比如插件尚未完成初始化时就开始加载资源、页面退出后仍有异步结果回调、相机对象更换后分块节点还绑定着旧相机、路径错误时 UI 还保持“加载中”这些问题都不会在第一屏立刻暴露。所以我先给 GaussField 定了四个业务状态READY页面已创建尚未开始场景装载LOADING插件和场景正在建立INTERACTIVE场景可交互相机已能驱动瓦片选择ERROR初始化或资源加载失败需要把错误原因暴露给页面。这里有个工程上的取舍这些状态不是 Spatial Recon Kit 强制提供的枚举而是 Demo 自己的业务状态。这样做的好处是把底层能力和页面行为隔开。以后无论普通GSNode还是TiledGSNode页面只关心“现在能不能交互”。这段代码解决什么问题把插件加载、场景加载与 UI 状态放进同一条可观察链路。import{Scene,RenderContext}fromkit.ArkGraphics3Dimport{spatialRender}fromkit.SpatialReconKittypeSceneStateREADY|LOADING|INTERACTIVE|ERROREntryComponentstruct Index{StatesceneState:SceneStateREADYStateerrorMessage:stringprivaterenderContext:RenderContext|nullnullasyncprepareRenderContext():Promisevoid{this.sceneStateLOADINGthis.renderContextScene.getDefaultRenderContext()if(!this.renderContext){this.sceneStateERRORthis.errorMessageRenderContext unavailablereturn}try{this.renderContext.loadPlugin(spatialRender.GSPlugin.PLUGIN_ID)console.info([GS] plugin loaded)}catch(err){this.sceneStateERRORthis.errorMessageplugin load failed:${JSON.stringify(err)}}}}这段代码没有急着创建模型。原因是插件加载是后续 3DGS 节点加载的前置条件。把这一步拆出来之后日志里能够明确看到[GS] plugin loadedUI 也能在失败时结束“无限转圈”。实际项目里还要注意一个点不要把loadPlugin()放进会重复触发的普通构建逻辑里。页面状态变化会带来重新构建但渲染插件的初始化应该由清晰的生命周期入口控制而不是跟着 UI 重建次数走。二、小场景能整包大场景不要硬扛普通 3DGS 模型适合体量可控、资源边界明确的场景。它的使用思路很直准备Scene调用GSPlugin.loadGSNode()然后把节点挂到场景树里。这样的代码容易理解也适合验证模型是否能正确导入。但我这次更关心的是院落外扩以后怎么办。场景一旦从“一个房间”变成“院落 街区”最直接的问题就是资源规模。把全量高斯点一次性加载到端侧不仅首屏等待时间会增加内存峰值也会跟着放大。更麻烦的是大多数时候用户只看当前视口附近远处的数据即便已经加载也没有形成等比例的体验收益。Tiled 3DGS 正好适合把这个问题拆开。场景按空间切成多个瓦片渲染侧根据相机驱动瓦片选择再把需要的资源交给应用处理。官方能力中TiledGSNode需要通过setCamera()绑定相机并可以通过setTileRequestCallback()拿到渲染器当前请求的GSTile列表。我在 Demo 里把总瓦片数记为96不是说所有项目都应该切成 96 份而是为了让调试状态可视化。当前截图中请求到24 / 96代表视口变化后已经发生了 24 个瓦片请求。这段代码解决什么问题加载分块 3DGS 场景并把相机与瓦片请求链路真正连起来。import{Scene,Camera,Node}fromkit.ArkGraphics3Dimport{spatialRender}fromkit.SpatialReconKitclassTiledSceneController{privatetiledNode?:spatialRender.TiledGSNodeprivatecamera?:Cameraasyncload(scene:Scene,root:Node,camera:Camera):Promisevoid{this.cameracameraconstparams:spatialRender.TiledGSImportSettings{uri:file:///data/storage/el2/base/files/courtyard/courtyard.scene.json}this.tiledNodeawaitspatialRender.GSPlugin.loadTiledGSNode(scene,params,root)this.tiledNode.setCamera(camera)this.tiledNode.setTileRequestCallback((tiles:spatialRender.GSTile[]){for(consttileoftiles){console.info([GS] requested tile${tile.uri})}})}}真正容易写错的地方不是 API 名字而是调用顺序。TiledGSNode创建出来以后如果没有绑定能够驱动选择的 Camera那么“分块场景已经加载”与“瓦片会跟着视口请求”是两件事。也就是说看到节点存在不能直接判断按需加载已经工作。我会把相机绑定放在节点创建完成后立刻做并把瓦片请求回调当成验收点。只要移动视角后日志里没有任何新 URI就应该先检查 Camera 和回调链路而不是先怀疑模型质量。上图就是我希望保留的调试现场左侧工程目录、中间加载代码、右侧模拟器和底部日志同时出现。页面显示INTERACTIVE场景文件是courtyard.scene.json请求瓦片为24 / 96当前帧率58。这些值并不是为了做性能宣传而是为了让“UI、代码、日志”能够互相对上。三、瓦片请求不能只打日志要变成可诊断的数据只在 HiLog 里打印 URI调试几分钟还行真的遇到抖动、白块或视角切换延迟时就不够了。因为你需要知道请求发生了多少次、哪些瓦片在当前视口内、最近一块是什么、某块是否频繁重复请求、加载时长有没有突然升高。所以我在 GaussField 里又加了一层TileDebugStore。它不参与渲染决策只保存调试信息。这样即使以后切换到别的加载实现页面上的调试视图也不用跟着重写。这段代码解决什么问题把瓦片回调转换成页面可展示、可复盘的诊断状态。interfaceTileRequestRecord{uri:stringtime:numbercostMs:number}ObservedclassTileDebugStore{requestedCount:number0visibleCount:number0fps:number0cacheMb:number0latestTile:stringrecords:TileRequestRecord[][]push(uri:string,costMs:number):void{this.requestedCountthis.latestTileuri.substring(uri.lastIndexOf(/)1)this.records.unshift({uri,time:Date.now(),costMs})this.recordsthis.records.slice(0,20)}}这里我没有把“请求次数”和“已完成加载数”混在一起。请求回调描述的是渲染器此刻需要什么而业务真正完成资源读取、通知瓦片可用属于下一层。两者如果混成一个数字出现重试或缓存命中后很难解释。截图里为了让文章更容易对照我把调试口径固定为24 / 96表示已记录的请求数与场景总瓦片数18表示当前视口中参与展示的可见瓦片统计184 MB是 Demo 自己记录的本地瓦片缓存占用最近一次瓦片为tile_024.sog。这些都属于示例项目指标并不是 Spatial Recon Kit 自动返回的一组固定 UI 数据。四、从 READY 到 INTERACTIVE中间每一步都要有证据做图形类功能最怕“凭感觉”。页面不黑屏就觉得加载成功拖动不卡就觉得性能没问题。这样的验证方式太粗。我更愿意把一次场景进入拆成下面几个可观察节点READY → LOADING_PLUGIN → LOADING_SCENE → CAMERA_BOUND → TILE_REQUESTING → INTERACTIVE业务 UI 不一定需要展示这么细但调试日志最好能保留。这样当问题出现时可以很快判断停在哪一段。例如停在LOADING_PLUGIN优先看插件加载与能力支持停在LOADING_SCENE检查场景 URI、manifest 和资源可访问性已经CAMERA_BOUND但没有TILE_REQUESTING检查 Camera 是否为当前实际渲染视角有瓦片请求但画面缺块继续看瓦片获取、缓存与 ready 通知流程已经INTERACTIVE但帧率波动再去分析可见瓦片数量、视角变化速度和场景规模。这套判断顺序的价值是把“渲染有问题”拆成多个窄问题。问题越窄排查越快。手机页里我没有堆太多调试项只保留最适合日常判断的四个状态、文件、瓦片进度和 FPS。这里的INTERACTIVE表示页面已经允许用户旋转、缩放和移动观察24 / 96与 DevEco 截图保持一致方便从真机界面反查日志。五、相机是分块渲染里真正的“需求发生器”普通列表的懒加载很好理解滚动到哪里加载到哪里。Tiled 3DGS 的思路其实类似只不过“滚动位置”变成了三维相机的视锥和视口。这也解释了一个我一开始容易忽略的问题如果应用里有多个 Camera或者为了截图、预览、自由浏览分别创建了不同相机那么setCamera()绑定哪一个非常关键。绑定的是旧相机即使用户在 UI 里移动了当前视角瓦片选择仍可能依据另一个 Camera 的状态。因此我后来不再把 Camera 当成一个随处可拿的全局变量而是把它当成TiledSceneController的显式依赖。切换主相机时同步更新 TiledGSNode。这样代码多了一步但状态关系更清楚。另外一个边界是页面离开。3D 场景的异步过程可能比页面生命周期更长尤其是瓦片来自本地文件之外的自定义数据源时。工程里应该保证页面销毁后不再向已经失效的 UI 状态对象回写长期驻留的缓存也要有容量上限而不是把“按需加载”做成“最终还是全量常驻”。六、用最近一次瓦片请求定位“动一下才加载”的问题我专门做了一个“分块调试”页不是为了好看而是为了处理一种非常典型的现象场景刚进来时局部清晰旋转视角后某个方向短时间缺细节再停一会又恢复。这种问题只看最终画面很难判断。调试页里我会同时看四个数字请求瓦片24 / 96可见瓦片18当前 FPS58缓存占用184 MB。再往下看最近一条请求tile_024.sog。如果视角已经明显变化但最近瓦片一直不变优先检查 Camera 和选择逻辑。如果最新瓦片在快速变化但完成耗时越来越长就把注意力转向资源读取、缓存和 I/O。这里红圈标出tile_024.sog承担的是“证据”而不是装饰。文章正文写到最近瓦片请求时读者可以直接对照这一行而不需要在一堆 UI 元素里猜我要说明什么。七、不要把“请求瓦片”误写成“网络下载瓦片”还有一个表达上的坑值得单独说。setTileRequestCallback()告诉应用的是渲染器希望获得哪些瓦片。瓦片最终来自本地文件、应用缓存、局域网资源还是远端服务是应用自己的数据链路选择。所以我在文章和日志里统一使用“请求瓦片”而不是默认写成“下载瓦片”。否则读者很容易误以为这个能力天然绑定网络方案。如果业务确实是远端大场景工程上至少还要补三层URI 到缓存键的映射并发请求和取消策略瓦片到达后的可用通知与错误恢复。视角快速旋转时旧方向的请求很可能已经失去价值。网络层如果不做取消或优先级控制就会出现“用户看东边带宽还在给西边服务”的浪费。这个问题和渲染算法本身无关却会直接表现成端侧交互延迟。八、我最后保留的验收清单这次 Demo 最后没有以“场景能打开”作为完成条件而是留了一个很短的验收清单。第一冷启动进入页面READY必须在开始加载后切换为LOADING成功后进入INTERACTIVE失败必须落到ERROR不能无限等待。第二场景文件固定为courtyard.scene.json切换视角时setTileRequestCallback()能产生新的瓦片 URI。第三调试页数据必须与日志同源。UI 显示24 / 96时日志里能找到对应的请求累计最近瓦片为tile_024.sog时最近记录中也能找到它。第四连续旋转、缩放、返回页面再进入不能因为重复初始化让状态叠加。尤其要观察请求数是否异常增长、缓存是否只增不降以及旧页面回调是否继续打印。第五性能数字只用于当前 Demo 的相对观察不把一次58 FPS当成所有设备和场景的固定结论。换模型规模、设备、视口、瓦片切分方式结果都会变化。九、这次最大的收获不是“把 3DGS 跑起来”Spatial Recon Kit 把 3DGS 的能力入口提供出来以后开发者真正要做的工程工作是把它嵌进自己的状态系统、资源系统和调试系统里。普通 GSNode 解决的是“把模型放进场景”TiledGSNode 更进一步把大场景的资源需求跟相机视口关联起来。我现在更愿意把 GaussField 看成一个小型场景运行时而不是模型查看器。页面层有状态渲染层有节点相机产生可见需求瓦片层记录请求调试页把关键证据暴露出来。这样一旦出现缺块、延迟、异常占用至少知道应该从哪一层开始查。对 3DGS 这种看起来很“视觉化”的能力工程里最重要的反而是把不可见的状态写清楚。画面好看只是结果能解释这个结果是怎么来的才算真的接住了能力。十、普通 GSNode 和 TiledGSNode我怎么选如果项目只是展示一件商品、一个人物扫描件或一个小型室内空间我不会为了“技术更新”强行上分块方案。普通GSNode的优点是链路短资源边界明确加载完成以后状态也容易理解。场景体量稳定、用户视角范围受控时它反而更容易做出确定性。TiledGSNode 更适合另外一类需求模型规模会继续扩张用户可以大范围移动或者资源本身就天然适合按空间组织。这时候关键指标不再只是模型文件多大而是“当前视口真正需要多少数据”。分块之后应用可以围绕相机需求做缓存、优先级和释放策略端侧资源利用会更接近用户正在看的区域。我通常用三个问题做选择。第一首屏是否必须很快进入可交互状态第二用户有没有可能在大场景中连续移动第三模型未来会不会从一个固定资产扩张成可持续更新的空间数据。如果三个答案大多是“是”分块方案的工程价值就会逐渐超过它增加的复杂度。还有一点容易忽略分块并不会自动解决全部性能问题。瓦片过小会让调度与请求次数增多瓦片过大又会让按需加载失去意义。相机快速移动时资源优先级与取消策略也会直接影响体验。所以 Tiled 不是“性能开关”而是一套允许应用继续做资源治理的基础机制。十一、我会怎样构造一次故障验证正常流程跑通以后我会故意制造几种错误。把courtyard.scene.json改成不存在的路径看状态能不能从LOADING落到ERROR临时不调用setCamera()看调试页是否出现“场景已建但无瓦片请求”再把视角快速连续旋转观察最近请求 URI 是否随视口变化。这类故障注入很重要因为图形应用的 happy path 往往太顺。真正上线以后遇到的通常是资源缺失、缓存被清理、页面快速进出、前后台切换或设备负载升高。提前把这些路径走一遍才能知道日志有没有信息量。我还会记录一次基线同一设备、同一场景、同一初始机位下进入INTERACTIVE的时间、首次瓦片请求数量、稳定后的可见瓦片数量和内存区间。以后改了瓦片切分或缓存策略不需要凭肉眼说“好像更快”直接跟基线比较即可。因此这次 Demo 最终留下来的不是一张好看的院落图而是一套可重复的验证方法状态能解释日志能对应错误能复现数据能比较。对任何端侧 3D 能力来说这比单次跑通更有价值。参考资料华为开发者Spatial Recon Kit / spatialRender APIhttps://developer.huawei.com/consumer/en/doc/harmonyos-references/spatial-recon-spatialrender华为开发者Spatial Recon Kit 术语与 Tiled 3D Gaussian Splatting 说明https://developer.huawei.com/consumer/cn/doc/HarmonyOS-Guides/spatial-recon-glossary华为开发者重建三维场景C/C与 Spatial Recon Kit 相关开发指南https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/spatial-recon-c-spatial-recon-pipeline
返回列表