资讯动态

SpatialReconKit + ArkGraphics 3D:分块 3DGS 的请求去重、并发背压与相机切换【鸿蒙心迹】

发布时间:2026/10/2 17:33:36 来源:尧图企业网站定制
分块 3DGS 最容易让人误判的时刻是场景已经显示出来了。模型有轮廓相机也能转似乎剩下的只是把网络请求写完。真正开始追踪 tile 请求后问题才变得具体相机轻轻回摆同一块数据可能再次进入回调下载任务跑得比存储慢队列会持续拉长页面已经离开较晚返回的任务仍在改状态。这篇文章用一个演示工程TileWatch把这些边界摊开。页面名是TiledScenePage任务编号gs_20261001_04清单文件为files/gs/courtyard/courtyard.scene.json主相机叫GSMainCamera。演示页固定显示请求 36 个 tile已就绪 24 个下载中 3 个队列 9 个总进度 67%当前观察项L2_014.sog。这些数据用于说明调度关系不是设备跑分或真实项目实测。一、tile 回调不是下载按钮而是渲染侧的需求信号官方接口提供TiledGSNode应用加载分块模型清单后可以设置驱动 tile 选择的相机并通过 tile 请求回调取得当前需要的GSTile。GSTile的核心信息是.sog文件 URI。这个回调表达的是“渲染器现在需要哪些块”而不是“请无条件创建一批永不取消的下载任务”。两者差别很大。相机移动时可见范围和细节层级会变化同一 URI 可能在不同批次出现。若每次回调都直接Promise.all()同一 tile 会被重复下载如果回调密集到达瞬时并发会由相机动作决定而不是由设备与网络能力决定。TileWatch因此把链路拆成四层SpatialReconKit 只产生请求TileRequestCoordinator做 URI 去重与状态转换下载器负责把远端内容写入清单约定的位置ArkUI 只显示调度快照。这样出现卡顿时可以先看是渲染请求多、队列积压、下载失败还是 UI 更新过密而不是把所有问题都叫作“3DGS 加载慢”。二、先让 Scene、插件、相机与分块节点形成闭环这段代码解决什么问题加载 3DGS 渲染插件、场景、相机与分块节点并把 tile 选择明确绑定到同一台相机。import{Scene,RenderContext,Camera,Node}fromkit.ArkGraphics3D;import{spatialRender}fromkit.SpatialReconKit;privateasyncopenScene():Promisevoid{constcontext:RenderContext|nullScene.getDefaultRenderContext();if(contextnull){thrownewError(default RenderContext unavailable);}context.loadPlugin(spatialRender.GSPlugin.PLUGIN_ID);constscene:SceneawaitScene.load();if(!scene.root){thrownewError(scene root unavailable);}constrootscene.rootasNode;constfactoryscene.getResourceFactory();constcamera:Cameraawaitfactory.createCamera({name:GSMainCamera,path:root.path});camera.enabledtrue;constmanifestfile:///data/storage/el2/base/files/gs/courtyard/courtyard.scene.json;constnodeawaitspatialRender.GSPlugin.loadTiledGSNode(scene,{uri:manifest},root);node.setCamera(camera);this.bindTileRequests(node);}这里没有把Scene.load()、相机创建与节点加载塞进一串没有中间检查的then()。默认渲染上下文为空、场景根节点不可用、清单路径为空都应该在靠近发生点的位置中止。否则错误会在后面的setCamera()或回调注册处表现出来日志看起来像 tile 调度失败根因却在更前面。TiledGSImportSettings的 URI 指向清单 JSON。文中的路径是 Demo 约定并不代表平台要求固定目录。官方资料说明分块导入能力和GSTile属于较新的接口项目必须按目标设备、Stage 模型限制及实际 SDK 声明确认可用性不能只因为编辑器能补全类型就推断所有设备都支持。还有一个容易遗漏的关系tile 选择由相机驱动。应用切换观察相机时必须同步更新TiledGSNode使用的相机。如果 UI 显示的是 B 相机节点仍由 A 相机驱动队列会请求用户看不到的区域画面与网络日志看起来像彼此矛盾。三、用四个集合说明一个 URI 当前到底在哪请求协调器不需要理解 3DGS 数学它只维护 URI 的所有权。ready表示本地已经可用queued表示等待下载inflight表示正在执行failed保存可重试项。一个 URI 在同一时刻只能属于一个主要状态。这段代码解决什么问题对回调里的 URI 去重并把新请求压入有上限的队列而不是直接启动无限并发。typeTileStateQUEUED|DOWNLOADING|READY|FAILED;classTileRequestCoordinator{privateready:SetstringnewSet();privatequeued:SetstringnewSet();privateinflight:SetstringnewSet();privatequeue:string[][];privatedisposed:booleanfalse;privatereadonlymaxConcurrent:number3;accept(tiles:spatialRender.GSTile[]):void{if(this.disposed)return;for(consttileoftiles){consturitile.uri;if(!uri||this.ready.has(uri)||this.queued.has(uri)||this.inflight.has(uri)){continue;}this.queued.add(uri);this.queue.push(uri);}this.pump();}privatepump():void{while(!this.disposedthis.inflight.sizethis.maxConcurrentthis.queue.length0){consturithis.queue.shift()!;this.queued.delete(uri);this.inflight.add(uri);voidthis.downloadOne(uri);}}}并发上限固定为 3只是 Demo 的调度参数不是 SpatialReconKit 推荐值。真实项目需要结合 tile 大小、网络、写入速度、内存与渲染帧稳定性测量。这里的关键不是数字 3而是并发权归协调器所有。相机回调可以一次带来 2 个或 20 个 URI都不会把同时执行数推到上限之外。集合比一个Mapstring, boolean更啰嗦却更容易查错。queued与数组队列保持一致inflight可以直接生成诊断页ready用于抑制回摆时的重复请求。实际实现还应处理本地文件已经存在但记录丢失的情况启动时扫描缓存、验证文件后再恢复ready不能把“文件名存在”直接当成完整内容可用。图中的 DevEco Studio 是演示配图。左侧工程目录、中间协调器代码、右侧模拟器和底部 HiLog 使用同一组数据taskgs_20261001_04 stateSTREAMING ready24 inflight3 queued9。红圈只标并发上限和去重集合它不构成真机性能证据。四、完成、失败与重试必须触发同一个泵如果只在accept()末尾调用pump()首批三个任务结束后队列不会继续推进。下载完成和失败都必须归还并发槽位再触发下一轮调度。失败项还要决定立即重试、延迟重试还是等待相机再次请求。这段代码解决什么问题让每个下载任务在成功或失败后归还槽位并用有限重试避免队列被单个坏 tile 卡住。privateretryCount:Mapstring,numbernewMap();privateasyncdownloadOne(uri:string):Promisevoid{try{awaitthis.downloader.fetchAndPersist(uri);if(this.disposed)return;this.inflight.delete(uri);this.ready.add(uri);this.onSnapshot?.(this.snapshot());}catch(error){this.inflight.delete(uri);if(!this.disposed){constcount(this.retryCount.get(uri)??0)1;this.retryCount.set(uri,count);if(count2){constdelayMscount1?420:1200;setTimeout(()this.requeue(uri),delayMs);}else{this.onFailed?.(uri,error);}}}finally{this.pump();}}L2_009.sog在演示里第一次失败420 ms 后回到队列。这个退避时间同样是示例参数不应被写成平台结论。有限重试的意义是给瞬时网络波动机会同时阻止永久失败项占满所有槽位。第二次仍失败时进入FAILED诊断页保留 URI、次数与阶段用户移动回该区域时再决定是否重新触发。注意finally里仍然调用pump()。即使状态快照回调抛出异常也不应该永久冻结后续队列。生产代码还应隔离观察者错误避免 UI 日志函数影响下载调度。下载器的文件写入应采用临时文件与校验后发布避免渲染器读到正在增长的.sog。11:18 的运行页显示STREAMING、67%、24/36、下载中 3、队列 9并把当前 tileL2_014.sog圈出。这里的 67% 是按ready / requested四舍五入后的演示口径它描述请求集合完成度不代表整个场景的几何质量或视觉完整度。五、相机切换时不清空 ready只重估尚未开始的请求相机从庭院入口切到屋顶观察位时已有 tile 缓存仍然有效直接清空ready会产生重复网络开销。真正需要重新判断的是尚未开始的队列旧视角排队的远处 tile 可能已经不再紧急新视角中心区域应该获得更高优先级。简单版本可以在每次回调到达时把新 URI 放到队首更完整的版本需要给请求附带层级、屏幕贡献或距离信息再由调度器排序。本文没有假设GSTile暴露这些额外字段因此只用 URI 做去重不编造优先级 API。若需要更精细的顺序应以当前接口提供的信息和业务清单元数据为准。TileWatch的相机切换流程是先让 UI 激活GSMainCamera或备用相机再调用节点的setCamera()增加一次cameraEpoch最后对后续状态快照附带 epoch。旧下载可以继续落盘但较晚返回的 UI 快照若 epoch 不匹配就不更新“当前视角请求”计数。这与取消下载不是一回事。已经进行到一半的大 tile 是否中断需要考虑浪费的流量和很快切回视角的可能性。演示选择“下载继续、UI 按 epoch 过滤、未开始队列可重新排序”这是相对克制的策略。六、页面离开后最先停止的是状态写入这段代码解决什么问题页面销毁或场景切换时使协调器失效拒绝晚到回调继续更新 ArkUI。classTileRequestCoordinator{// 前文成员省略dispose():void{this.disposedtrue;this.queue.length0;this.queued.clear();this.onSnapshotundefined;this.onFailedundefined;}}aboutToDisappear():void{this.viewEpoch;this.coordinator?.dispose();this.coordinatorundefined;this.pageStateIDLE;}这段代码只声明了协调器自己的边界清空待启动任务、断开 UI 观察者、让完成回调不再发布状态。它没有声称取消底层网络也没有假造一个 SpatialReconKit 销毁接口。正在执行的下载是否可取消要由下载实现提供信号场景、相机和节点的资源释放则应遵循实际承载组件与当前 ArkGraphics 3D 对象的生命周期。若页面频繁进入退出最危险的是把旧TiledGSNode、旧相机和新协调器混在一起。建议让一次场景会话拥有独立 epoch创建场景时生成所有异步结果都带回该值值不匹配就只做资源收尾不碰当前页面。诊断页与运行页不同。它展示REQUESTED → QUEUED → DOWNLOADING → READY列出L2_009.sog的 420 ms 重试以及 3 个并发槽位并在生命周期末尾标出“观察者已断开”。红色标注解释背压和晚到回调不是为了装饰完成状态。七、用日志回答“慢在哪里”而不是只记录百分比单一进度数字无法定位问题。建议至少记录五类事件tile 批次到达、去重后新增数量、队列等待长度、下载耗时与落盘结果、相机 epoch 变化。每条日志都带任务 ID但 URI 可以只保留文件名或哈希避免把完整远端地址写入日志。当queued持续增长、inflight稳定为 3说明下载或落盘吞吐跟不上请求当回调批次很大但去重后新增接近 0说明相机抖动或重复需求被缓存吸收当ready增长而画面不变化需要检查相机绑定、文件发布位置和渲染侧读取而不是继续提高并发。还要把 UI 刷新节流。每个 tile 完成就触发一整页重绘在快速网络下反而会制造主线程压力。协调器可以高频维护内部集合但以 100250 ms 合并一次快照给 ArkUI。诊断页需要精确事件时从环形缓冲读取不要让主页面承担完整日志列表。八、这条链路目前能证明什么本文能证明的是一种工程分层官方 3DGS 分块节点与相机负责产生需求应用调度层控制重复、并发、重试和页面状态文件层保证 tile 完整发布。它不能证明并发 3 最优也不能证明 67% 时画面达到某个质量更没有声称在特定机型完成性能测试。接入真实项目时还需要验证清单与.sog文件关系、远端缓存策略、失败码、网络切换、前后台行为、内存峰值和多相机切换。尤其是较新的接口要以目标 SDK 的 API 声明、系统能力和设备支持范围为准。真正值得保留的结论只有三个回调是需求信号不是无限并发指令URI 状态要有唯一所有者页面生命周期与下载生命周期必须分开。把这三件事做清楚3DGS 的“偶尔糊、偶尔卡、偶尔重复下载”才会变成可观察、可调节的工程问题。九、缓存不是越多越好淘汰也要服从场景会话分块模型一旦能稳定下载下一个问题通常是缓存。最直接的实现是所有.sog永久保留短期看命中率很好长期却会让应用目录不可控。另一种极端是页面退出立即删除本次会话刚结束用户返回就要重新拉取同样不合理。比较稳妥的做法是把缓存分成“会话热集”和“可复用冷集”。当前相机附近、正在下载和刚完成的 tile 属于热集不能被清理线程碰其他已校验文件进入冷集根据总大小、最后访问时间和模型版本淘汰。清单版本变化时不要只凭同名 URI 判断可复用还要核对清单标识、文件长度或内容摘要。缓存索引本身也可能损坏。启动时应允许从磁盘文件重建基础索引而不是因为一份 JSON 解析失败就删除全部模型。重建过程先把文件标为“待验证”通过长度与摘要后才进入ready。若只看文件存在之前异常中断留下的半成品会被误当成命中最终表现为渲染缺块却没有任何网络错误。并发下载与清理必须共享所有权信息。某个 URI 在inflight时清理器不能根据旧访问时间删除它的临时文件清理开始后也不能让新的下载立即写入同一路径。可以为每个模型目录设置轻量会话锁或让所有磁盘操作通过同一个仓储对象串行决策。这里不要求所有 I/O 真正串行而是要求“能不能删、能不能发布”只有一个判断者。存储空间不足时调度器不应继续接受几十个新请求再一起失败。仓储层返回空间压力后协调器可以暂停pump()先淘汰冷集再恢复队列若无法释放足够空间页面进入PAUSED_STORAGE保留相机与已就绪 tile让用户至少能查看当前质量。把这类错误归入普通网络重试只会反复写盘和清理。十、验收要故意制造相机抖动与页面离开顺着正常路径转一圈看见模型逐渐清晰只能证明最乐观的情况。调度器真正的验收应该围绕边界动作设计。第一组用例是重复请求。同一批 URI 连续调用accept()三次队列长度只能增加一次其中一个 URI 已进入inflight后再次出现也不能生成第二个下载。完成后再请求同一 URI应由ready直接吸收。报告记录三次输入数量与实际新增数量便于发现集合状态失配。第二组是并发背压。一次放入 50 个不同 URI观察任何时刻inflight.size都不超过 3。让前两个任务成功、第三个失败确认三个槽位都能归还后续队列继续前进。尤其要测试观察者抛错避免 UI 代码阻断finally里的泵。第三组是相机快速切换。A、B 相机交替五次旧 epoch 的状态快照不得覆盖当前页面已经完成的 tile 仍保留在缓存新回调只增加尚未出现的 URI。若队列支持重新排序还要确认重排没有同时把 URI 留在旧位置导致同一项执行两次。第四组是页面离开。三个下载进行中时触发aboutToDisappear()页面状态立刻回到IDLE后续完成回调不再改 ArkUI。若下载实现支持取消检查临时文件被关闭并按策略删除若不支持允许任务完成落盘但不能复活旧页面。第五组是坏文件。下载器返回成功但长度或摘要不符URI 不能进入ready临时文件不能改成最终名诊断页应显示“校验失败”而不是笼统的FAILED。下一次重试要从新临时文件开始不能续写损坏内容。第六组是空间压力。预留空间不足时暂停调度冷缓存淘汰完成后恢复无法恢复则稳定停在可解释状态。此时画面仍可使用已有 tile不应该因为后台存储错误把整个 Scene 立即销毁。这些用例大部分可以在协调器和仓储层用可控下载器完成不需要每次依赖真实 3DGS 服务。真机阶段再集中验证回调批次、相机驱动、插件加载、文件落盘与渲染读取之间的实际关系。这样测试失败时更容易判断是平台接入、调度逻辑还是数据文件问题。十一、从一次诊断快照反推优化顺序假设诊断页长期显示ready24、inflight3、queued9不能立刻得出“把并发改成 6”。先看三个进行中任务的阶段如果主要时间花在网络等待提高并发可能有效如果都卡在写盘或摘要计算提高并发会让 I/O 竞争更严重如果下载很快但渲染仍缺块瓶颈可能根本不在下载器。再看队列年龄。9 个等待项都刚刚进入属于相机移动的正常波动若最老项等待数秒仍未开始才说明吞吐跟不上。队列只显示长度而不显示最老等待时间会把短峰值与持续积压混为一谈。还要看去重率。请求 36 个、实际新增 12 个说明缓存和集合吸收了大量重复需求请求 36 个、实际新增也是 36 个则要确认是否换了模型、缓存键是否包含了不稳定参数或相机进入了全新区域。去重率高不是坏事它说明回调在表达动态需求而协调层没有把动态性放大成网络风暴。最后看帧体验。下载完成数增长只是后台指标用户在意的是相机操作是否连贯、中心区域是否先清晰、页面退出是否迅速。优化顺序应由可感知问题决定先阻止主线程更新过密再处理存储争用然后根据网络与 tile 大小调整并发。只盯着总耗时很容易用更高资源消耗换来一个并不明显的数字改善。这些指标必须放在同一次场景会话里解释跨模型比较时还要注明清单规模、缓存状态与网络条件避免把不可比的快照放到同一条趋势线上。十二、参考资料华为开发者文档3DGS 空间重建流程https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/spatial-recon-c-spatial-recon-pipeline华为开发者 APISpatialReconKit 空间渲染https://developer.huawei.com/consumer/cn/doc/harmonyos-references/spatialrender-api华为开发者 APIArkGraphics 3Dhttps://developer.huawei.com/consumer/cn/doc/harmonyos-references/arkgraphics3d-api

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑