HarmonyOS 7 / API 26 3DGS 端侧重建接入边界:Spatial Recon Kit 与 ArkGraphics 3D 分工实战
2026/8/8 13:47:23 网站建设 项目流程

HarmonyOS 7 / API 26 3DGS 端侧重建接入边界:Spatial Recon Kit 与 ArkGraphics 3D 分工实战

先把问题摆出来

HarmonyOS 7 / API 26 把 3DGS 端侧重建放到了新能力列表里,这个方向很有吸引力:空间建模、商品展示、文旅展陈、室内空间预览,都可以把二维素材变成更接近真实空间的 3D 内容。

但真到开发侧,最容易出错的地方不是“会不会写一个 3D 页面”,而是把几个能力混成一个东西:

  • 以为 Spatial Recon Kit 负责所有 3D 渲染;
  • 以为 ArkGraphics 3D 可以直接完成重建;
  • 以为 3DGS 模型加载成功,就等于完整接入成功;
  • 以为模拟器能跑通,真机就一定没问题;
  • 以为只要页面能显示模型,性能、降级、失败恢复就可以后面再补。

我的处理方式是把链路分成五段:能力检测、重建会话、模型产物、3D 场景、失败兜底。每段只负责自己的事,后面才好排查。

这几个官方能力分别管什么

这篇按 HarmonyOS 7 / API 26 的能力范围来理解,核心参考的是 HarmonyOS 7 新能力、Spatial Recon Kit、ArkGraphics 3D 和 AR Engine 的官方说明。

能力更适合负责什么不适合负责什么
Spatial Recon Kit3DGS 相关的重建、模型加载、模型处理、滤镜和空间重建会话页面布局、普通 3D 场景组织、业务状态管理
ArkGraphics 3D3D 场景、节点、相机、灯光、材质、动画、后处理、模型展示从视频或空间数据生成 3DGS 产物
AR Engine需要把虚拟物体放到真实世界坐标里时,处理现实空间对齐普通模型预览页、离线模型列表展示
ArkUI 页面展示状态、按钮、错误提示、进度、结果入口重建算法本身、渲染引擎内部细节

所以一个比较稳的接入方式是:Spatial Recon Kit 产出或加载模型,ArkGraphics 3D 负责把模型放进场景里展示,ArkUI 页面只观察状态。需要和现实世界对齐时,再考虑 AR Engine。不要让一个页面组件同时承担采集、重建、加载、渲染和错误恢复。

我会先做能力检测,而不是先写页面

3DGS 这种能力和普通 UI 组件不一样,它受设备、区域、系统版本、GPU、模型体积和运行环境影响。官方文档也明确提到了一些约束,比如 Spatial Recon Kit 的地区、设备和模拟器支持范围,ArkGraphics 3D 也依赖图形能力。

开发时我会先把能力检测做成一个单独模块。页面进来以后,先问一句:当前设备到底能不能跑?不能跑,就给用户一个正常的降级页面,而不是让页面白屏。

interface SpatialCapabilityResult { canPreview: boolean; canReconstruct: boolean; reason: string; } export class SpatialCapabilityGuard { async check(): Promise<SpatialCapabilityResult> { const apiLevel = this.readApiLevel(); const gpuReady = await this.checkGpuAbility(); const reconReady = this.canUseSystemCapability('SystemCapability.Graphics.SpatialRecon'); if (apiLevel < 26) { return { canPreview: false, canReconstruct: false, reason: '需要 HarmonyOS 7 / API 26 或更高版本' }; } if (!gpuReady) { return { canPreview: false, canReconstruct: false, reason: '当前设备图形能力不足,先展示普通图片预览' }; } return { canPreview: true, canReconstruct: reconReady, reason: reconReady ? '支持 3DGS 预览和端侧重建' : '支持 3D 预览,但端侧重建不可用' }; } private readApiLevel(): number { return 26; } private async checkGpuAbility(): Promise<boolean> { return true; } private canUseSystemCapability(name: string): boolean { return name.length > 0; } }

这段代码的重点不是具体接口名,而是流程:先判版本,再判图形能力,再判 Spatial Recon 能力。很多 3D 页面出问题,就是因为把这些判断放到了渲染失败以后才补。那时候用户已经看到空页面了,日志也很难对应到是哪一段出错。

案例一:离线 3DGS 模型预览

第一个案例先不做端侧重建,只做一个离线模型预览。比如产品同学给了一个已经处理好的 3DGS 模型文件,开发者要在应用里做预览、旋转、切换视角和失败兜底。

这个场景最适合拿来验证接入边界,因为它可以绕开采集和重建,先证明“模型产物到页面展示”这段链路是稳定的。

复现步骤

  1. 准备一个体积可控的 3DGS 或 3D 模型资源,放在沙箱目录或应用资源目录;
  2. 页面进入时先跑能力检测;
  3. 能力通过后,创建模型资源描述;
  4. 交给 ArkGraphics 3D 场景控制器加载;
  5. 加载成功后允许用户旋转、缩放、切换视角;
  6. 加载失败时显示封面图、错误原因和重试按钮。
  7. type SpatialAssetFormat = 'PLY' | 'GLB' | 'MP4_3DGS'; type PreviewPhase = 'idle' | 'checking' | 'loading' | 'ready' | 'fallback' | 'failed'; interface SpatialAsset { id: string; name: string; format: SpatialAssetFormat; localUri: string; posterUri: string; byteSize: number; } interface PreviewState { phase: PreviewPhase; assetId: string; message: string; progress: number; } export class SpatialPreviewStore { private state: PreviewState = { phase: 'idle', assetId: '', message: '', progress: 0 }; getState(): PreviewState { return { ...this.state }; } async open(asset: SpatialAsset, guard: SpatialCapabilityGuard, scene: ThreeDSceneController): Promise<PreviewState> { this.state = { phase: 'checking', assetId: asset.id, message: '正在检查设备能力', progress: 5 }; const ability = await guard.check(); if (!ability.canPreview) { this.state = { phase: 'fallback', assetId: asset.id, message: ability.reason, progress: 100 }; return this.getState(); } this.state = { phase: 'loading', assetId: asset.id, message: '正在加载 3D 模型', progress: 30 }; try { await scene.loadAsset(asset); await scene.applyDefaultCamera(); await scene.applySoftLight(); this.state = { phase: 'ready', assetId: asset.id, message: '模型已就绪', progress: 100 }; } catch (error) { this.state = { phase: 'failed', assetId: asset.id, message: '模型加载失败:' + String(error), progress: 100 }; } return this.getState(); } }

    这里我把页面状态写成了 PreviewState,不是在页面里散落一堆布尔值。原因很简单:3D 能力的失败原因很多,如果只写 isLoading、isError,后面根本看不出到底是能力不支持、文件不存在、模型太大,还是场景初始化失败。

    场景控制器只管 3D 展示

    export class ThreeDSceneController { private sceneReady: boolean = false; async init(surfaceId: string): Promise<void> { if (!surfaceId) { throw new Error('缺少 3D 渲染承载节点'); } this.sceneReady = true; } async loadAsset(asset: SpatialAsset): Promise<void> { if (!this.sceneReady) { throw new Error('3D 场景还没有初始化'); } if (!asset.localUri || asset.byteSize <= 0) { throw new Error('模型文件不存在或大小异常'); } } async applyDefaultCamera(): Promise<void> {} async applySoftLight(): Promise<void> {} dispose(): void { this.sceneReady = false; } }

    这个控制器故意不碰 UI 状态,也不碰业务数据。它只负责三件事:初始化场景、加载资源、释放场景。这样后面如果模型显示不出来,排查顺序就很明确:先看资源,再看场景,再看相机和灯光。

    案例二:端侧重建会话兜底

    第二个案例再进入端侧重建。端侧重建比离线预览更容易出问题,因为它多了采集、计算、进度、暂停、失败恢复和产物落盘。

    我会把它当成长任务处理,而不是当成一个按钮点击事件。

    复现步骤

    1. 用户进入重建页,先跑能力检测;
    2. 能力不满足时,直接切换到“上传已有模型”或“查看示例模型”;
    3. 能力满足时,创建重建会话;
    4. 推送输入素材或采集帧;
    5. 监听进度、失败、完成;
    6. 完成后把产物写入资源仓库,再进入 ArkGraphics 3D 预览;
    7. 页面退出或应用进后台时,能取消、暂停或恢复,不留下半截状态。
    8. type ReconPhase = 'idle' | 'preparing' | 'running' | 'saving' | 'done' | 'cancelled' | 'failed'; interface ReconProgress { phase: ReconPhase; percent: number; tips: string; outputUri: string; } interface ReconInput { sourceUri: string; expectedFormat: SpatialAssetFormat; maxDurationSeconds: number; } export class SpatialReconSessionGuard { private phase: ReconPhase = 'idle'; private cancelRequested: boolean = false; async run(input: ReconInput, guard: SpatialCapabilityGuard): Promise<ReconProgress> { const ability = await guard.check(); if (!ability.canReconstruct) { return { phase: 'failed', percent: 0, tips: ability.reason, outputUri: '' }; } this.phase = 'preparing'; this.cancelRequested = false; if (!input.sourceUri) { return { phase: 'failed', percent: 0, tips: '没有可用于重建的输入素材', outputUri: '' }; } try { this.phase = 'running'; await this.waitOrCancel(800); this.report('extractFrames', 15, '正在抽取关键帧'); await this.waitOrCancel(1200); this.report('matchFeature', 40, '正在匹配空间特征'); await this.waitOrCancel(1500); this.report('buildGaussian', 70, '正在生成 3DGS 表示'); this.phase = 'saving'; const outputUri = await this.persistOutput(input); this.phase = 'done'; return { phase: 'done', percent: 100, tips: '重建完成,可以进入 3D 预览', outputUri }; } catch (error) { if (this.cancelRequested) { return { phase: 'cancelled', percent: 100, tips: '用户已取消重建', outputUri: '' }; } return { phase: 'failed', percent: 100, tips: '重建失败:' + String(error), outputUri: '' }; } } cancel(): void { this.cancelRequested = true; } private async waitOrCancel(delay: number): Promise<void> { if (this.cancelRequested) { throw new Error('cancelled'); } await new Promise<void>((resolve) => setTimeout(resolve, delay)); } private report(stage: string, percent: number, tips: string): void { console.info('[SpatialRecon] ' + stage + ' ' + percent + '% ' + tips); } private async persistOutput(input: ReconInput): Promise<string> { return input.sourceUri + '.3dgs.' + input.expectedFormat.toLowerCase(); } }

      第一,重建会话要能取消。用户退出页面、应用进后台、设备发热、空间不足,都可能让重建不适合继续跑。没有取消逻辑,页面看起来只是一个进度条,实际上后台状态已经乱了。

      第二,重建完成后不要直接把产物塞给页面。先落到资源仓库,再由预览页面加载。这样“重建”和“展示”是两条链路。重建失败,不影响已有模型预览;预览失败,也不需要重新跑一遍重建。

      为什么不建议把所有逻辑塞进一个 ArkUI 页面

      3DGS 接入里,最危险的写法是页面里既管按钮、又管采集、又管重建、又管模型加载、又管错误提示。代码一开始看着直观,后面会出现几个问题:

      写法短期效果后期风险
      页面里直接写重建逻辑Demo 快页面退出后任务状态难处理
      页面里直接写模型加载少建一个类预览失败原因混在 UI 里
      页面同时管重建和渲染看起来链路短后面没法单独复用离线预览
      没有资源仓库少写落盘代码重建结果丢失或无法恢复
      只做成功态截图好看真机异常时用户只能看到空页面

      更稳的结构是下面这样:

      class SpatialFeatureEntry { private capabilityGuard = new SpatialCapabilityGuard(); private previewStore = new SpatialPreviewStore(); private reconSession = new SpatialReconSessionGuard(); private sceneController = new ThreeDSceneController(); async previewExistingAsset(asset: SpatialAsset): Promise<PreviewState> { return this.previewStore.open(asset, this.capabilityGuard, this.sceneController); } async reconstructThenPreview(input: ReconInput): Promise<PreviewState | ReconProgress> { const progress = await this.reconSession.run(input, this.capabilityGuard); if (progress.phase !== 'done') { return progress; } const asset: SpatialAsset = { id: 'asset-' + Date.now(), name: '端侧重建结果', format: input.expectedFormat, localUri: progress.outputUri, posterUri: '', byteSize: 1 }; return this.previewStore.open(asset, this.capabilityGuard, this.sceneController); } }

      这个入口类把两种场景放在一起:已有模型预览、重建后预览。页面不需要知道底层到底是离线资源还是刚重建出来的资源。页面只拿到状态,然后决定展示进度、错误、封面图还是 3D 场景。

      真机验证时我会看这几个结果

      验证点通过标准失败时先查哪里
      能力检测不支持设备能看到降级说明API 版本、设备能力、地区限制
      离线模型预览模型能显示,旋转缩放不卡死模型路径、模型体积、场景初始化
      相机和灯光首屏能看到模型主体默认相机距离、模型包围盒、灯光方向
      端侧重建会话有进度、可取消、失败有原因会话状态机、输入素材、存储空间
      产物落盘重启页面后还能打开结果沙箱路径、资源索引、文件大小
      后台恢复切后台再回来不会卡在旧进度生命周期、取消逻辑、状态快照

      这里面最容易被忽略的是“产物落盘”。如果重建结果只存在内存里,页面一退出就没了。用户看到的是“刚才明明成功了,回来怎么没了”。这类问题看起来像 UI bug,实际是资源生命周期没设计好。

      图片和封面也要按技术链路来做

      3DGS 文章的图不能只放一张随便截的编辑器图。读者点进来之前,封面就应该告诉他这篇在讲什么。我的封面会把链路画成五段:

      1. Capability Gate:先判设备和版本;
      2. Spatial Recon Session:处理重建会话;
      3. 3DGS Asset:保存模型产物;
      4. ArkGraphics 3D Scene:负责场景展示;
      5. Fallback:不支持或失败时可降级。
      6. 这样读者不用先读一大段文字,也能大概知道文章重点是“边界”和“接入链路”,不是单纯介绍名词。

        我自己的实现取舍

        如果只是写一个演示页面,我可以把所有逻辑放在一个页面里,半天就能看到效果。但如果要放进正式项目,我会接受多写几个类,把边界分开。

        原因有三个:

        • 3DGS 能力本身有设备和环境限制,能力检测必须独立;
        • 端侧重建是长任务,不能跟页面生命周期绑死;
        • 3D 展示会继续扩展相机、灯光、材质、动画和后处理,不能被重建流程拖住。

        这不是为了把代码写复杂,而是为了后面能排查、能复用、能降级。尤其是 HarmonyOS 7 / API 26 这类新能力,刚接入时更应该把失败路径放到台面上。只有成功截图,没有失败路径,项目里不一定稳。

        最后总结

        3DGS 端侧重建不要当成一个“页面组件”来接。更合理的理解是:Spatial Recon Kit 处理重建和 3DGS 资源相关能力,ArkGraphics 3D 负责把资源放进 3D 场景里展示,ArkUI 负责状态和交互,AR Engine 只在需要现实空间对齐时加入。

        开发者真正要补的是边界意识:先检测能力,再跑会话,再保存产物,再进入场景,失败时给降级。这样写出来的 3DGS 功能不会只停留在 Demo 截图,而是能经得住真机、弱设备、后台恢复和资源异常这些情况。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询