ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

HarmonyOS 7 Spatial Recon Kit:3DGS 分块渲染与端侧重建状态治理【鸿蒙心迹】

HarmonyOS 7 Spatial Recon Kit:3DGS 分块渲染与端侧重建状态治理【鸿蒙心迹】 我这次没有把重点放在“把一个 3DGS 模型显示出来”而是把一次真实的空间重建任务拆成了可追踪、可恢复、可诊断的工程链路任务有 ID进度有来源状态有边界分块渲染有视口驱动异常也能知道停在了哪一步。HarmonyOS 7 对应 API 26.0.0空间计算这一块最让我感兴趣的变化之一就是 Spatial Recon Kit 不再只是“做个三维效果看看”。官方能力说明已经把 3DGS 端侧重建、渲染和编辑放进了一条完整链路里在渲染侧API 26 还提供了面向大场景的TiledGSNode核心思路是把完整 3DGS 场景切成瓦片根据当前相机视口按需加载而不是把所有高斯点一次性压进内存。我用一个叫GaussRoom的小工程做验证。场景很简单用户围着客厅走一圈采集素材任务进入重建重建结束后进入 3DGS 预览如果模型规模较大预览阶段切换为分块加载。最终我保留了一组固定数据作为调试样本任务 ID 为GS-20260930-017重建进度停在 68% 时关键帧数量是142 / 208进入分块渲染后可见瓦片36 / 52、已请求瓦片 44、当前 LOD 为 2、相机距离约 3.8 m。这些数字不是官方性能指标也不代表设备上限它们只是本文 Demo 的一次调试快照。真正有价值的是当这些数字被纳入状态模型之后很多以前只能靠“感觉卡了一下”的问题开始可以被复现和解释。一、我先改掉了“页面就是任务”的写法最早的版本里我在页面aboutToAppear()里直接开始重建页面销毁时再尝试停止任务。代码看起来很省事问题也很快出现用户切到别的页面再回来页面对象换了但底层任务未必结束一次失败后重新开始上一轮回调还可能继续更新 UI进度条看起来在动却不知道当前进度属于哪一个任务。这类问题在普通列表页面里不算大事在三维重建里就会被放大。因为重建不是一次几十毫秒的函数调用它更像一个有生命周期的长任务。页面只是观察者任务本身应该有独立身份。所以我先定义了一套很克制的状态IDLE尚未开始CAPTURING正在采集输入RECONSTRUCTING端侧正在计算READY模型已经可用于预览TILED_RENDERING分块模型正在按视口加载FAILED任务失败需要保留错误上下文。这段代码解决什么问题把任务状态和页面生命周期拆开并且保证旧任务回调不能污染新任务。// entry/src/main/ets/model/ReconState.etsexportenumReconState{IDLEIDLE,CAPTURINGCAPTURING,RECONSTRUCTINGRECONSTRUCTING,READYREADY,TILED_RENDERINGTILED_RENDERING,FAILEDFAILED}exportinterfaceReconSnapshot{taskId:stringstate:ReconState progress:numberkeyFrames:numberexpectedFrames:numberoutputFile?:stringerrorCode?:numbererrorMessage?:string}exportclassReconTaskStore{privatecurrentTaskId:stringprivatesnapshot?:ReconSnapshotbegin(taskId:string):void{this.currentTaskIdtaskIdthis.snapshot{taskId,state:ReconState.CAPTURING,progress:0,keyFrames:0,expectedFrames:208}}update(next:ReconSnapshot):boolean{if(next.taskId!this.currentTaskId){returnfalse}this.snapshotnextreturntrue}getSnapshot():ReconSnapshot|undefined{returnthis.snapshot}}这里最关键的并不是枚举而是taskId。每次启动新任务都生成新的任务 ID底层回调带着同一个 ID 回来页面收到回调后先核对 ID再决定是否更新状态。这样用户快速退出、重进、重新开始旧任务即使晚到几百毫秒也不会把新任务的状态覆盖掉。我后来发现这个做法还顺手解决了日志难追的问题。以前 HiLog 里只有progress68现在会打印taskGS-20260930-017 progress68 stateRECONSTRUCTING。一条日志本身没变复杂多少但排查时终于能把同一次任务串起来。二、68% 必须有业务含义不能只是一个进度条动画重建页面最容易做成“看起来很忙”一个百分比、一条进度条、一个旋转动画。真正调试时却会问三个问题68% 是哪一步的 68%这个值多久没变化了它和采集帧数有没有关系我的处理方式是把 UI 上的百分比只当作ReconSnapshot的一个投影不在页面里自己累加。底层重建层给什么页面就显示什么如果 15 秒没有新的进度回调页面单独进入“进度停滞”提示而不是偷偷把数字从 68 补到 69。另外关键帧数量要和进度并排看。比如本文这次快照是142 / 208进度 68%。如果关键帧仍在增长但重建进度暂时不动说明问题可能还在输入阶段如果关键帧已经稳定进度也长时间不变就该去看重建线程、设备负载或错误事件。一个单独的进度条给不了这种判断。我还给状态转换加了最小约束CAPTURING可以进入RECONSTRUCTING或FAILEDRECONSTRUCTING可以进入READY或FAILEDREADY才能进入TILED_RENDERING。这样 UI 就不会出现“模型还没准备好渲染按钮已经能点”的半成品状态。三、渲染阶段真正的变化是 Tiled 3DGS普通 3DGS 模型可以通过GSPlugin.loadGSNode()载入。到了 API 26大场景还可以使用loadTiledGSNode()创建TiledGSNode。官方 API 说明里明确提到这类节点面向大规模 3DGS 场景瓦片选择由相机驱动setCamera()的作用就是把当前相机交给分块节点让它知道视口在哪里。这里有两个工程细节值得先记住。一个是插件加载。调用 GS 相关加载能力前需要先把对应插件加载到渲染上下文另一个是模型层级。TiledGSNode属于 Stage 模型场景能力项目结构和页面上下文不要再按旧 FA 模型思路套。这段代码解决什么问题在 ArkGraphics 3D 场景中加载分块 3DGS并把相机交给瓦片选择逻辑。// entry/src/main/ets/pages/SpatialPreviewPage.etsimport{Scene,Camera,RenderContext}fromkit.ArkGraphics3Dimport{spatialRender}fromkit.SpatialReconKitexportclassSpatialPreviewController{privatescene?:Sceneprivatecamera?:CameraprivatetiledNode?:spatialRender.TiledGSNodeasyncinit():Promisevoid{constrenderContext:RenderContext|nullScene.getDefaultRenderContext()if(!renderContext){thrownewError(RenderContext unavailable)}renderContext.loadPlugin(spatialRender.GSPlugin.PLUGIN_ID)this.sceneawaitScene.load()this.cameraawaitthis.createSceneCamera(this.scene)consturiOhosRawFile://assets/room/tiles/manifest.jsonthis.tiledNodeawaitspatialRender.GSPlugin.loadTiledGSNode(this.scene,{uri},this.scene.root)this.tiledNode.setCamera(this.camera)}privateasynccreateSceneCamera(scene:Scene):PromiseCamera{// 项目中由实际 ArkGraphics 3D 相机创建逻辑返回 Camera。returnscene.root.getComponent(MainCamera)asCamera}}上面的createSceneCamera()是工程包装层重点不是这一行怎么取相机而是TiledGSNode最后必须拿到真正驱动当前视图的 Camera。不要创建一个“专门给 TiledGSNode 的假相机”UI 又用另一台相机渲染否则瓦片选择会和用户眼前看到的视口错开。图里的 DevEco Studio 我故意把几个信息放在一起左边是tiles/manifest.json和瓦片资源中间是loadTiledGSNode()与setCamera()右侧是正在重建的 GaussRoom 页面底部 HiLog 则记录requested44、ready36、total52。它们不是四张拼图而是一次真实调试时应该同时观察的四个区域。四、不要把“分块”理解成简单的文件切片一开始我也容易把 Tiled 3DGS 理解成“模型大了拆成几十个文件就行”。真正接入后才意识到工程层关注的是空间组织、视口、LOD 和加载节奏。官方术语对 Tiled 3D Gaussian Splatting 的描述很直接完整模型按空间划分为多个瓦片渲染时按需加载当前视口需要的数据以降低内存和渲染开销。也就是说瓦片只是载体真正让它有价值的是“只加载现在值得加载的那部分”。这也是为什么相机状态会从 UI 层一路进入渲染层。用户转身、拉近、拉远看见的空间发生变化瓦片请求集合跟着变化LOD 也可能变化。要是把这套链路当成普通分页加载就会设计出很奇怪的缓存策略——比如按文件序号预加载相邻瓦片但这些“相邻序号”在空间里未必相邻。我在 Demo 里没有自己干预底层瓦片选择而是把可观测数据做出来当前可见瓦片、已请求瓦片、LOD、相机距离、平均瓦片就绪时间。这样性能出现波动时先判断“请求是否合理”再决定要不要优化应用层资源组织而不是一上来就调线程数。五、手机上的 68% 页面其实是给开发者看的状态面板这张运行图里有几个我刻意保留的字段GS-20260930-017是任务 IDRECONSTRUCTING是当前状态142 / 208是关键帧数量room_017.ply是当前任务的输出文件名68% 是底层回调上来的重建进度。在正式产品里普通用户当然不需要看这么多技术字段。但开发阶段最好先把它们显示出来。很多问题只有“同屏”才能看出关系。例如用户说“到 68% 卡住了”我不再只问“卡了多久”而会继续看关键帧是否还增长、任务 ID 有没有变化、输出文件是否已经创建、页面是否发生过重建。等链路稳定之后这些字段可以移到 Debug 面板或仅在测试包打开。我的经验是太早把调试信息藏起来后面每次复现都要重新加日志反而浪费时间。六、相机更新要区分“渲染需要”和“日志需要”相机每一帧都可能发生细微变化。setCamera()负责告诉分块节点使用哪台相机真正的瓦片选择由能力内部完成但如果应用自己还要把相机位置、瓦片数量、内存等信息写入日志就不能每帧都打印一大串字符串。我最后把“相机驱动渲染”和“相机驱动诊断”拆成两条节奏。渲染保持正常更新诊断采样则控制在一个较低频率只在相机位移或朝向变化达到阈值时记录一次。这段代码解决什么问题给分块渲染增加轻量诊断采样而不是让 HiLog 反过来拖慢渲染。// entry/src/main/ets/common/RenderTelemetry.etsexportinterfaceRenderMetrics{visibleTiles:numberrequestedTiles:numberlod:numbermemoryMB:numbercameraDistance:numberaverageTileReadyMs:number}exportclassRenderTelemetry{privatelastReportAt:number0report(taskId:string,metrics:RenderMetrics):void{constnowDate.now()if(now-this.lastReportAt1000){return}this.lastReportAtnowconsole.info([GaussRoom] task${taskId}visible${metrics.visibleTiles}requested${metrics.requestedTiles}lod${metrics.lod}memory${metrics.memoryMB}MBcamera${metrics.cameraDistance.toFixed(1)}mtileReady${metrics.averageTileReadyMs}ms)}}这段代码没有控制TiledGSNode的内部调度它只负责应用自己的可观测性。两者要分清官方能力决定“怎么渲染”业务诊断决定“我怎么知道它现在发生了什么”。七、36 / 52 比“画面有点卡”有用得多这张诊断页对应的是同一个任务GS-20260930-017但状态已经进入TILED_RENDERING。我保留的快照是可见瓦片36 / 52、已请求 44、LOD 2、相机距离 3.8 m、内存占用约 1.42 GB、平均瓦片加载 28 ms。再强调一次这不是 Spatial Recon Kit 的官方基准只是我这次 Demo 的观测值。它的意义在于建立排查顺序。如果用户旋转视角后画面出现空洞我会先看请求瓦片有没有增加请求增加但 ready 长时间不变再看 IO 或模型资源ready 正常但画面仍缺失就检查瓦片内容、相机和场景变换。如果内存持续上涨才进一步判断是瓦片缓存、业务对象还是其他资源没有释放。这种排查和传统“掉帧了就开 Profiler”不冲突但顺序更合理。3D 场景的问题往往有业务语义视口、LOD、瓦片、场景节点。先把这些语义数据补齐再进性能工具定位速度会快很多。八、我给异常链路留了四种出口三维重建和普通页面最大的区别是它非常依赖设备能力、输入质量和长任务稳定性。因此我没有设计一个万能的catch而是把错误按处理方式拆开。第一类是设备不支持。官方 Spatial Recon Kit 专题材料明确提醒要对不支持设备做好判断和降级真机能力也要以当前文档和设备要求为准。遇到这类错误页面不要给“重试”按钮骗用户应该直接说明当前设备不可执行此任务并提供替代流程。第二类是输入不足。例如关键帧不足、相机运动覆盖不够。这时保留已采集数据比直接清空更重要让用户能补拍而不是重新走一圈。第三类是模型加载失败。uri为空、manifest 不存在、资源不完整都应该在进入渲染页之前被发现。我的页面只有在READY状态且模型资源校验通过后才允许进入预览。第四类是运行期渲染异常。某个瓦片失败时不要立即把整场景判死可以记录瓦片 ID 和错误并根据能力实际行为决定重试或提示。应用层至少要做到错误属于哪个任务、哪个阶段、哪个资源日志里能看出来。九、真机调试比“模拟器里能跑”更重要Spatial Recon Kit 涉及真实设备图形和重建能力。官方专题材料目前明确提示空间重建相关验证需要使用满足要求的真机或远程真机环境不能把模拟器截图当作最终验证依据。具体支持范围、设备芯片要求和版本限制仍应以你开发时的最新官方文档为准因为这类能力迭代很快。所以我把验证拆成两层页面状态、任务模型、日志格式可以在普通工程环境里先完成真正的重建、模型生成、分块渲染性能和设备兼容性再上真机确认。这能避免一个常见误区因为真机环境准备麻烦就把所有逻辑都拖到最后一起测。结果一旦出错你很难知道是 UI 状态机、桥接层、模型资源还是设备能力的问题。十、任务生命周期、页面生命周期和模型生命周期必须分开做到后面我又发现一个很容易被忽略的问题虽然我已经把任务从页面里拆出来但如果模型资源仍然跟着页面创建和销毁切一次前后台、旋转一次页面结构还是可能出现重复加载。于是我又把生命周期拆成了三套。页面生命周期最短。它负责订阅ReconTaskStore展示进度和诊断数据页面消失时只取消订阅不自动把任务杀掉。任务生命周期稍长从begin()开始到成功、失败或用户明确取消结束。模型生命周期则取决于预览需求任务已经重建完成以后模型可能还要被用户反复查看所以不能因为重建任务结束就立刻释放。这三个生命周期如果绑成一条线最典型的现象就是“返回列表再进详情模型又重新加载一次”。如果设备内存比较紧第二次加载甚至还没等第一次完全释放就出现瞬时内存峰值。我的做法是给模型仓库维护modelKey和引用计数同一个room_017已经存在就复用最后一个预览页面离开后再进入可释放状态。真正释放之前还要确认没有导出、截图或分享任务在引用它。这里我没有把缓存策略写成某个固定数字因为不同模型差异太大。小型物体扫描和整间客厅的资源占用完全不是一个量级。工程上更重要的是先定义清楚“谁拥有资源”“谁有权释放资源”再根据真机数据确定缓存上限。还有一个细节是失败任务的资源。FAILED不等于所有东西都应该立刻删除。如果失败发生在 80% 以后前面已经采集的关键帧、错误码、设备信息和最后一次进度快照都很有价值。我会保留一个很小的诊断摘要让用户可以重新开始同时开发者还能知道上一轮为什么失败。十一、性能回归不要只看平均帧率3DGS 场景接入完成以后我给自己加了一份很朴素的回归表。每次改瓦片资源、相机逻辑或 UI我都用同一条测试路径打开room_017从默认视角旋转 90 度前进到 3.8 m再退回初始位置。这样不同版本的日志可以横向比较。我关注的也不只有 FPS。至少还会记录首批瓦片可见时间、一次视角变化后新请求的瓦片数量、ready/requested的收敛速度、内存峰值以及页面退出后内存能否回落。平均帧率看起来很漂亮但如果每次转身都要等两秒模型才补齐用户依然会觉得卡反过来短时帧率波动如果发生在后台预取阶段实际感知可能并不明显。我还会故意做几次“不友好操作”快速拖动视角、连续进出预览页、重建完成后立即打开模型、应用切后台再回来。稳定的空间应用不能只在一条温柔的 Demo 路径里表现正常。等这些指标稳定后才适合去做更激进的优化比如调整资源组织、压缩包策略、预加载范围或诊断采样频率。否则很容易出现一种错觉某次改动让单个指标变好了却把另一个阶段的等待时间转移到了用户更敏感的位置。十二、这次最大的收获不是“3D 更炫”做完 GaussRoom 之后我对 Spatial Recon Kit 的理解反而更朴素了。3DGS 当然很吸引人点云逐渐变成完整场景的过程也很有视觉冲击但一个真正能交付的空间应用核心仍然是工程治理。任务什么时候开始、什么时候算结束68% 从哪里来用户退出后任务怎么办模型准备好之前能不能点预览大场景为什么要分块相机和瓦片之间是什么关系某次掉帧到底是内存、IO 还是视口请求异常——这些问题如果没有答案模型再漂亮也只适合做演示。我现在更愿意把 3DGS 的接入顺序写成一句话先把任务变成可观察的状态再把模型变成可控制的资源最后才谈视觉效果。当状态、日志、资源和渲染指标都能对上同样一个 68%就不再是进度条上的数字而是一次可以被解释、被复现、也可以被优化的工程过程。参考资料HarmonyOS 7 新能力一览https://developer.huawei.com/consumer/cn/features/Spatial Recon Kit / spatialRender APIhttps://developer.huawei.com/consumer/en/doc/harmonyos-references/spatial-recon-spatialrenderSpatial Recon Kit 术语https://developer.huawei.com/consumer/cn/doc/HarmonyOS-Guides/spatial-recon-glossary3DGS 端侧重建相关官方专题与文档入口https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/spatial-recon-c-spatial-recon-pipeline
返回列表