
3DGS 重建结果几十 MB每次重建几分钟用户拍完等不起。商品展示场景里同一个展位会反复拍——换商品、调摆放、补拍角度每次全量重建 3~5 分钟用户第二次拍就烦了。我们做了本地缓存加增量更新首次全量重建存盘后续只重采变化区域合并回主场景。这篇把存储架构、增量合并方案和踩过的坑摊出来。能力面序列化存储与增量合并序列化存储重建结果落盘用 PLY 格式按 SKU ID 版本号组织目录。一个 SKU 的存储结构/data/storage/el2/base/files/3dgs/ └── sku_12345/ ├── base.ply # 主场景全量重建结果 ├── base.meta.json # 元数据点数、包围盒、重建时间、采集参数 ├── patch_001.ply # 增量补丁 1 ├── patch_002.ply # 增量补丁 2 └── current.ply # 当前生效版本base 补丁合并后current.ply是合并后的完整版本渲染时直接加载这个。base.ply和补丁是中间产物用于再合并。这个结构的好处是回滚容易——某个补丁合出来质量差删掉补丁重新合就行不用重拍。元数据base.meta.json存重建时的采集参数相机轨迹、光照环境、分辨率增量重建时要用这些参数做参考保证新补丁和 base 在同一参数空间下。格式选择上我们对比过 PLY 和 MP4。PLY 是点云原生格式读写直接但体量大96 MB/42 万点。MP4 是 Spatial Recon Kit 支持的压缩格式体量小约 PLY 的 1/4但读写要编解码加载耗时略长。商品展示场景里我们选 PLY——加载快比存储小重要存储可以外置。展厅大场景走 Tiled 时选 MP4因为大场景存储敏感、加载本来就是分块的。被放弃的方案差分存储我们试过只存变化部分的差分存储base.ply 加一串差分补丁不合并成 current.ply渲染时实时 apply 差分。想法是省掉 current.ply 的存储省一倍空间合并放到渲染时做。跑下来行不通。渲染时实时 apply 差分要在每帧把 base 和补丁的高斯点合并开销约 30 ms/帧帧率直接掉到 30 fps 以下。而且差分 apply 的结果不能缓存每帧都变没有优化空间。差分存储省了存储但把开销转嫁到渲染热路径得不偿失。这个方案放弃了回到预合并 current.ply 的方案。增量合并方案增量更新的思路是空间分块。把主场景按 AABB 划分成网格块用户标记变化区域比如展位右半边换了商品只对变化块重采重建再把重建结果合并回主场景。合并不是简单拼接。块边界处两边的高斯点要过渡平滑否则出现色差或密度跳变。我们做的合并步骤用户标记变化区域 AABB对变化区域重采重建产出局部 3DGS在块边界做点云融合边界附近两边各取一定宽度的过渡带按距离加权混合写入新补丁文件合并生成新的 current.ply缓存策略存储空间有限用户沙箱默认配额 1 GB3DGS 缓存不能占满。我们用 LRU 按场景分组每个 SKU 一个缓存组组内 base 补丁 current 算一个整体淘汰LRU 按最后查看时间淘汰最近看过的 SKU 优先保留设上限单 SKU 缓存不超过 300 MB总缓存不超过 500 MB超限时按 LRU 淘汰整个 SKU 组不拆着删拆了 base 删了补丁就没法合并LRU 的最近使用定义要小心。一开始我们用最后加载时间结果发现用户频繁切换的两个 SKU 把别的都挤掉了其实用户只是来回看没真在用。改成最后重建时间——只有触发过重建的 SKU 才算在用纯加载的不更新 LRU 时间戳。改完之后淘汰更合理长期不重建的 SKU 优先被淘汰。LRU 的时间戳定义要看业务加载和重建是两个不同的活跃信号。约束面存储预算与拼接误差约束表现我们的处理沙箱存储上限用户配额 1 GB3DGS 占 500 MBLRU 淘汰单 SKU 上限 300 MB增量拼接误差块边界色差、密度跳变过渡带加权混合带宽 5 cm重建 session 单例同一时刻只允许一个重建 session增量重建时主场景要先卸载缓存失效物理场景变了但缓存没更新采集时做特征比对差异超阈值失效补丁累积膨胀多次增量后补丁越来越多定期把补丁合进 base重置补丁链重建 session 单例这条是硬约束。Spatial Recon Kit 同一时刻只允许一个重建 session第二个 session 创建会失败或阻塞。增量重建时要启动新 session主场景必须先从渲染管线卸载——不能一边渲染主场景一边重建补丁。用户感知到的是增量重建那几秒画面会切到重建中占位重建完再切回来。这个体验不如理想中无感增量但 session 单例绕不过。拼接误差是另一道硬约束。块边界两边的高斯点是独立重建的光照环境微变会导致两边颜色有偏差。过渡带加权混合能缓解但不能消除——如果两边色差大比如一边窗光一边室内灯过渡带里会出现一条渐变带肉眼能看出这里颜色在过渡。增量合并的拼接误差在光照均匀的展位里几乎无感在光照不均的场景里明显。还有一道约束是补丁累积。每次增量加一个补丁文件补丁多了合并耗时线性增长。第 5 个补丁合并要 12 秒第 10 个要 22 秒。我们设了补丁数 3 触发合进 base的阈值把补丁链重置。阈值定 3 是权衡——合进 base 要 15 秒太频繁合本身也是开销太少补丁合并又慢。3 在我们场景里是个甜点更大场景可能要调。场景落地商品展位增量更新一个 1.5 m × 1 m 的商品展位首次全量重建 4 分 12 秒产出 42 万点 PLY 96 MB。后续换展位右半边的商品只重采右半边 0.5 m × 1 m 区域增量重建 1 分 38 秒产出补丁 18 万点 41 MB合并后 current.ply 51 万点 116 MB。首次 vs 增量耗时对比操作耗时产出点数文件体量用户等待体验首次全量重建4 分 12 秒42 万96 MB全程等待增量重建右半边1 分 38 秒18 万补丁41 MB等待画面切占位增量重建左上角小块42 秒6 万补丁14 MB短暂等待补丁合并8 秒-3 万重叠剔除current 重生成无感补丁合进 base定期15 秒base 更新补丁链重置无感测试机型 Pura 80 Pro麒麟 9030。增量重建比全量快 2.5~6 倍取决于变化区域占比。变化区域越小收益越大。跨设备增量耗时把同一展位的增量重建放到两台机器上对比机型首次全量增量右半边增量左上小块合并Pura 80 Pro (9030)4 分 12 秒1 分 38 秒42 秒8 秒Mate 70 Pro (9020)6 分 05 秒2 分 19 秒58 秒11 秒Mate 70 Pro 慢约 40%但增量收益比例一致——增量都是全量的 35%~40%。增量方案跨设备收益稳定慢机器也能享受加速。这个比例只在我们测的展位尺寸下成立更大场景的增量收益要重测。存储占用追踪同一个展位做了 5 轮增量更新存储占用变化轮次base.ply补丁数补丁总大小current.ply总占用首次96 MB0096 MB96 MB第 1 轮增量96 MB141 MB116 MB253 MB第 2 轮增量96 MB272 MB128 MB296 MB第 3 轮增量96 MB398 MB135 MB329 MB补丁合进 base135 MB00135 MB135 MB第 4 轮增量135 MB138 MB142 MB315 MB第 3 轮后总占用 329 MB 接近单 SKU 上限 300 MB触发补丁合进 base占用降到 135 MB。补丁链要定期合并否则存储膨胀失控。合进 base 的操作我们放在用户不活跃时应用后台、夜间自动做避免占用前台时间。缓存命中的真实分布我们上线了一周灰度统计缓存命中率。200 个 SKU、约 1500 次展示请求命中类型次数占比说明current.ply 直接命中118078.7%无需重建增量重建21014.0%用户标记变化区域全量重建956.3%内容特征比对判定大变LRU 淘汰后重建151.0%缓存被淘汰了78.7% 的请求直接命中缓存用户无感。14% 走增量等 1~2 分钟。6.3% 走全量等 4~6 分钟。LRU 淘汰导致的重建只有 1%说明 500 MB 总上限对 200 个 SKU 够用。缓存命中率的关键不是算法多聪明是上限设得够大——我们把总上限从 200 MB 调到 500 MB 后淘汰重建从 8% 降到 1%。存储换命中率的杠杆很划算。踩坑三个增量方案的坑坑一增量合并后拼接处出现色差第一次增量合并我们没做过渡带直接把补丁拼到 base 上。展位右半边新商品是冷色调左半边原场景是暖色调拼缝处一条明显的色差分界线。用户反馈像两张图拼起来的。加了 5 cm 宽过渡带后色差缓解成渐变带但没消除。根因是两边重建时的光照环境微变——右半边重采时是下午窗光偏暖base 是上午重建的窗光偏冷。这个色差在过渡带里表现为一条渐变比硬分界好但还能看出。最终的解法是重采时尽量对齐光照时间。我们加了重采时间窗提示增量重采要在和 base 重建同一时间段做上午对上午、下午对下午光照环境一致拼接误差最小。增量合并的拼接质量不只靠算法靠采集纪律。坑二缓存失效策略不当导致展示旧版本第二版我们用采集参数 hash做缓存失效 key——采集参数变了就判定缓存失效要全量重建。结果用户只是换了个商品摆位置采集参数相机轨迹、分辨率没变缓存没失效展示的还是旧版本。用户拍完新商品看到的还是旧商品。问题出在缓存失效 key 只看了采集参数没看场景内容。改方案是采集时做一帧特征提取SIFT 特征点和缓存的 base 特征比对差异超阈值才判定内容变了要重建。特征提取约 200 ms能接受。缓存失效要靠内容特征比对不能只靠采集参数。后来又发现一个反向问题用户只是调了一下灯光没换商品特征比对判定内容变了触发全量重建白等 4 分钟。我们把特征比对阈值放宽并加了一个用户手动标记变化区域的入口——用户知道自己换了什么让用户标比算法猜准。手动标记变化区域走增量重建没标记时才走特征比对。坑三重建 session 单例导致增量时主场景不能加载第三版增量方案我们想做得无感——边渲染主场景边重建补丁重建完无感替换。结果启动增量重建 session 时直接报错因为主场景的渲染 session 还在。Spatial Recon Kit 同一时刻只允许一个重建 session。这个约束文档里写了我们漏看了。改方案是增量重建前先把主场景从渲染管线卸载GSNode.visible false 卸载 PLY重建完再加载回来。用户感知到的是增量那 1 分 38 秒画面切到重建中占位图。session 单例让无感增量做不了只能做短暂切占位快速增量。这个体验不如理想但目前 API 没办法绕过。占位图的选择有讲究。一开始我们用纯色占位用户反馈以为应用崩了。改成用上一帧的截图做占位定格在重建前的画面用户感知是画面定住了在处理比纯色好。再后来在定格画面上叠一个进度条和正在更新场景文案用户接受度最高。占位图用上一帧截图进度文案比纯色或转圈都好这个细节是 QA 提了三次单才调出来的。存储架构与增量合并ArkTS 实现// entry/src/main/ets/storage/GsCacheManager.etsimport{spatialRender}fromkit.SpatialReconKit;import{fileIoasfs}fromkit.CoreFileKit;// 缓存管理LRU 按场景分组exportclassGsCacheManager{privatecacheDir:string/data/storage/el2/base/files/3dgs/;privatemaxTotalBytes:number500*1024*1024;// 500 MB 总上限privatemaxPerSkuBytes:number300*1024*1024;// 300 MB 单 SKU 上限// 取当前生效版本 uri没有返回 nullgetCurrentUri(skuId:string):string|null{constdir:stringthis.cacheDirsku_skuId/;constcurrentPath:stringdircurrent.ply;if(fs.accessSync(currentPath)){returnfile://currentPath;}returnnull;}// 增量重建标记变化区域 AABB只重采该区域asyncincrementalRebuild(skuId:string,changeAabb:spatialRender.AABB,captureSession:spatialRender.ReconSession,):Promisestring{constdir:stringthis.cacheDirsku_skuId/;// 1. 主场景必须先卸载session 单例约束// 调用方负责卸载这里只做重建// 2. 启动增量重建 session只处理 changeAabb 内的采集数据constpatchUri:stringdirpatch_this.nextPatchId(dir).ply;awaitcaptureSession.rebuildPartial({outputUri:patchUri,region:changeAabb,// 参照 base 的采集参数保证同一参数空间refMeta:this.loadMeta(dirbase.meta.json),});// 3. 合并补丁到 currentawaitthis.mergePatches(skuId);returnpatchUri;}// 合并所有补丁到 current.ply带 5 cm 过渡带privateasyncmergePatches(skuId:string):Promisevoid{constdir:stringthis.cacheDirsku_skuId/;constbaseUri:stringdirbase.ply;constcurrentUri:stringdircurrent.ply;constpatches:string[]this.listPatches(dir);constmerger:spatialRender.GSMergerspatialRender.GSPlugin.createMerger();awaitmerger.merge({baseUri:baseUri,patchUris:patches,outputUri:currentUri,transitionBand:0.05,// 5 cm 过渡带overlapPolicy:dedup,// 重叠点去重});}// 定期把补丁合进 base重置补丁链asynccompactPatches(skuId:string):Promisevoid{constdir:stringthis.cacheDirsku_skuId/;constcurrentUri:stringdircurrent.ply;constbaseUri:stringdirbase.ply;// current 变成新 basefs.copyFileSync(currentUri.replace(file://,),baseUri.replace(file://,));// 删除所有补丁for(constpofthis.listPatches(dir)){fs.unlinkSync(p.replace(file://,));}}// LRU 淘汰超总上限时删最久未访问的 SKU 组evictIfNeeded():void{lettotal:numberthis.calcTotalBytes();while(totalthis.maxTotalBytes){constoldest:string|nullthis.findLruSku();if(oldestnull){break;}total-this.deleteSku(oldest);}}privatenextPatchId(dir:string):number{returnthis.listPatches(dir).length1;}privatelistPatches(dir:string):string[]{return[];/* 省略 */}privateloadMeta(path:string):spatialRender.ReconMeta{return{}asspatialRender.ReconMeta;}privatecalcTotalBytes():number{return0;/* 省略 */}privatefindLruSku():string|null{returnnull;/* 省略 */}privatedeleteSku(sku:string):number{return0;/* 省略 */}}mergePatches里的transitionBand: 0.05就是 5 cm 过渡带overlapPolicy: dedup是重叠区域去重——两块边界处的点云有重叠去重避免密度跳变。这两个参数是踩坑后定下来的。存储架构流程否是无变化有变化是否是否用户触发重建有缓存?全量重建 → base.ply内容特征比对直接加载 current.ply用户标记变化区域?增量重建变化区域 → patch_N.ply写 current.ply合并补丁过渡带 → current.ply加载 current.ply 渲染补丁数 3?后台合进 base重置补丁链等下次增量总结一下下重建 session 单例增量重建前主场景必须先卸载无感增量做不了增量重建占位图用上一帧截图进度文案纯色或转圈用户以为崩了增量合并拼接误差靠采集纪律重采要在和 base 同一光照时间段做缓存失效靠内容特征比对不能只靠采集参数用户手动标记变化区域最准补丁链定期合进 base否则存储膨胀失控合操作放后台补丁数 3 触发合进 base阈值按场景调我们展位场景甜点是 3单 SKU 缓存上限 300 MB总缓存上限 500 MBLRU 按整组淘汰缓存命中率靠上限设得够大500 MB 对 200 SKU 够用命中率 78.7%过渡带 5 cm重叠点去重这两个参数是踩坑后定的current.ply 是渲染加载的版本base 补丁是中间产物回滚删补丁即可差分存储省存储但把开销转嫁渲染热路径已放弃PLY 选加载快MP4 选存储小商品展示走 PLY展厅大场景走 MP4LRU 时间戳用最后重建时间不是最后加载时间纯加载不更新活跃度补丁合进 base 的操作放后台/夜间避免占用前台时间影响用户体验增量重建占位图用上一帧截图进度文案纯色用户以为崩了过渡带 5 cm 经验值重叠点去重避免密度跳变踩坑后定的Spatial Recon Kit 体验保证仅覆盖麒麟 9020/9030S/9030/9030 Pro 及后续旗舰我们正在把用户标记变化区域做成更智能的版本采集时用端侧 AI 检测自动识别场景里变了什么物体自动框出变化 AABB不用用户手动标。这个方案要引入 AI 模型加载和 3DGS 渲染争资源要单独评估。端侧 AI 检测在 API 26 仍带 Beta 标记接口可能调整先用着试。第二个想验证的是拼接色差能不能在合并时做颜色对齐——把补丁的颜色直方图匹配到 base 的颜色直方图理论上能消色差。风险是颜色对齐会改变补丁本身的颜色可能引入新的失真。如果后续 API 不变的话先在光照均匀的展位场景试颜色对齐光照不均的场景还是靠采集纪律。第三个想做的是把缓存命中率接进监控面板。目前命中率是灰度一周的离线统计要做在线的——实时看命中率、淘汰次数、增量触发次数发现命中率掉了能及时调上限。运营那边也想看这个数据他们要算加多少缓存换多少用户体验的账有这个数据才好申请存储预算扩容。