HarmonyOS 6.1 端侧 3DGS 重建实战:重建在 C 层,ArkTS 只管"看"和"改"

文章来源声明: 原文作者:威哥爱编程; 来源站点:掘金; 原文链接:https://juejin.cn/post/7685317187398549550; 本文基于上述来源整理/加工,觅优补充点评,仅供技术学习交流。版权归原作者所有。
觅优短评

最大价值在于分层认知与四条硬边界:把单会话当全局互斥锁、把温控订阅当准入项、把 1080×1440 当输入契约。适合端侧 3D 重建的架构师与 NDK 开发者做预研避坑。

> 本文涉及 HarmonyOS 6.1.0(23) 起的视觉输入重建与 7.0(26) 新增能力。文中代码是为说明问题编写的完整示例,不是官方示例的搬运;API 名称、枚举取值与版本号等事实性信息均标注官方出处;涉及真机表现的部分已明确标注,未做任何实测数据编造。

HarmonyOS 6.1 端侧 3DGS 重建实战

引子:一句"拍一圈就行",V哥调研了三天

产品在需求单上写了一句很轻的话:

用户拿着手机绕着东西走一圈,App 里就能 360° 转着看这个模型。

听起来像"调个相机 + 调个模型加载"的活。V哥打开官方文档准备抄一段示例,结果第一眼就发现事情不对:V哥在网上搜到的那些 ArkTS 重建代码,在官方文档里根本找不到对应接口。

这件事值得先说,因为它决定了你这三天的调研方向是不是从一开始就跑偏了。


一、先纠正一个认知:重建在 C 层,ArkTS 只做"看"和"改"

Spatial Recon Kit(空间建模套件)的官方 ArkTS API 文档下只有两个模块

模块干什么起始版本
`spatialRender`3DGS 模型的**加载与渲染**,含滤镜效果6.0.1(21)
`spatialEdit`3DGS 模型的**选择、上色、删除、导出**26.0.0

ArkTS API 总览

注意:这里没有"重建"模块。

重建管线是**纯 C/C++(NDK)**的,官方指南标题就写着「重建三维场景(C/C++)」,从 6.1.0(23) 开始支持通过视觉输入重建(重建三维场景(C/C++))。

所以整个能力是分层的,V哥用一张图说清楚:

C 层与 ArkTS 层的分工

  • C 层(NDK):检测能力 → 建会话 → 喂数据帧 → 启动重建 → 查进度 → 暂停/继续 → 保存结果 → 销毁会话。
  • ArkTS 层:把 C 层产出的模型文件(MP4 / PLY / GLB)加载进 ArkGraphics3D 场景,做滤镜、做编辑、做交互。

这就是第一个原创判断:你不可能只用 ArkTS 完成"端侧重建"。 但凡看到一个"纯 ArkTS 三行代码生成 3D 模型"的示例,先别急着抄——先去官方 API 总览里核对模块名。V哥这次核对的结论是:官方 ArkTS 侧只有 spatialRenderspatialEdit,没有重建入口。

另外两条 Kit 级约束必须提前知道(Spatial Recon Kit 简介):

  • 本 Kit 仅支持中国境内(香港特别行政区、澳门特别行政区、中国台湾除外);
  • 本 Kit 是 ArkGraphics 3D 模块的扩展,必须与它联合使用

二、第一关:设备门槛,三重限制叠在一起

在写第一行代码之前,先回答"这台设备配不配跑"。官方给了三重限制,任何一条不满足,后面全是白干:

限制维度具体要求
地域仅中国境内(不含港澳台)
设备形态Phone、Tablet、PC/2in1、TV
芯片仅保证旗舰芯片(Kirin 9020 / 9030S / 9030 / 9030 Pro 及以后)
模拟器**不支持**

四道门槛速查

芯片这一条官方说得很克制但很明确:由于空间重建对性能开销较大,当前仅保证旗舰芯片上的用户体验;在其他芯片上,即使查询接口返回"支持",也无法保证重建耗时和重建质量重建三维场景(C/C++))。

V哥把这句话翻译成工程语言:"支持"是一个三态,不是布尔值。 于是能力检测要这样写:

<span>// spatial_gate.h —— V哥自己的能力检测封装</span>
<span>#<span>include</span> <span>"spatial/spatial_recon_interface.h"</span></span>

<span>enum class</span> <span>ReconGate</span> {
    UNSUPPORTED,   <span>// 设备根本不支持</span>
    RISKY,         <span>// 接口说支持,但芯片不在保证名单里,可跑但别承诺效果</span>
    READY          <span>// 支持且芯片在保证名单里</span>
};

<span>// 只做接口层判断,芯片分档由业务层结合机型名单决定</span>
<span>ReconGate <span>checkReconGate</span><span>()</span> </span>{
    HMS_SpatialReconStatus ret = <span>HMS_SpatialRecon_IsSupport</span>(SPATIAL_RECON_MODEL_TYPE_GS);
    <span>if</span> (ret != SPATIAL_RECON_STATUS_SUCCESS) {
        <span>return</span> ReconGate::UNSUPPORTED;
    }
    <span>return</span> ReconGate::READY;
}

HMS_SpatialRecon_IsSupport 的返回值只有两种可能:SPATIAL_RECON_STATUS_SUCCESS(支持)或 SPATIAL_RECON_STATUS_DEVICE_NOT_SUPPORT(不支持)——这一点在管理 Spatial Recon Kit 会话里有明确说明。

V哥建议把"不支持"做成一条完整的降级路径,而不是弹个 Toast 就完事。 因为按上面的限制,你的用户里必然有一大批设备跑不了。可行的降级是:换成本地预置的轻量模型、或者直接展示多角度实拍图,让功能"还在",只是效果降级。


三、第二关:数据输入,1.333 这个比例要背下来

重建要的不是"一段视频",而是一系列图像 + 每张图对应的相机内参和位姿。喂数据有两条路:

  1. 用 AR Engine 的数据结构——先更新一次 AR 引擎的计算结果,再把 ARSession / ARFrame 推进来;
  2. HMS_SpatialRecon_DataFrame 结构体自己组装——适合你已经有现成图像和内参的场景。

这里埋着整条链路最容易被忽略的一颗地雷

为保证重建效果和鲁棒性,不论使用何种格式,当前仅支持输入宽度 1080 像素、高度 1440 像素的图像。输入其余尺寸,结果是未定义的。

1080×1440,宽高比正好 1 : 1.3333。记住这个数字,因为你的相机预览、相册取图、缩放开销全都要围着它转——相机默认给你的是 1920×1080 或者 4:3,都不是这个比例,必须自己裁或缩。

第二条:仅支持 RGB 格式输入SPATIAL_RECON_IMAGEDATA_FORMAT_RGB)。

下面是V哥自己组装的推帧示例:

<span>// recon_frame.cpp —— 把一张图组装成 DataFrame 推进会话</span>
<span>#<span>include</span> <span>"spatial/spatial_recon_interface.h"</span></span>

<span>// 只推"有效帧",无效输入会直接返回 SPATIAL_RECON_STATUS_FAILED</span>
<span>HMS_SpatialReconStatus <span>pushOneFrame</span><span>(HMS_SpatialRecon_Session* session,
                                    <span>const</span> <span>uint8_t</span>* rgb, <span>uint32_t</span> w, <span>uint32_t</span> h,
                                    <span>float</span> fx, <span>float</span> fy, <span>float</span> cx, <span>float</span> cy)</span> </span>{
    <span>// 地雷一:尺寸不对,结果未定义,这里直接拦掉</span>
    <span>if</span> (w != <span>1080</span> || h != <span>1440</span>) {
        <span>return</span> SPATIAL_RECON_STATUS_FAILED;
    }

    HMS_SpatialRecon_DataFrame frame;
    frame.focalX = fx;          <span>// 相机内参</span>
    frame.focalY = fy;
    frame.principalX = cx;      <span>// 主点</span>
    frame.principalY = cy;
    frame.imageWidth = w;
    frame.imageHeight = h;
    frame.format = SPATIAL_RECON_IMAGEDATA_FORMAT_RGB;   <span>// 地雷二:仅 RGB</span>
    frame.imageData = <span>const_cast</span><<span>uint8_t</span>*>(rgb);

    <span>return</span> <span>HMS_SpatialRecon_PushFrame</span>(session, &frame);
}

有个细节能省你不少事:关键帧是系统自动选取的。PushFrame / PushARFrame 时,系统会自己挑关键帧保存用于后续重建——你不需要(也不应该)自己去算哪一帧是关键的。


四、第三关:会话管理,一段三段式状态机

重建不是"一个函数调用完就出结果",它是一段有状态的生命周期。V哥把它归纳成三段:

① 采集阶段:CreateSession → PushFrame × N      (<span>Stage</span> = INIT)
② 重建阶段:StartSession → GetProgress / Pause / Resume   (<span>Stage</span> = BUILDING)
③ 保存阶段:SaveResultToFile 或 StartSession 时传入 writeInfo   (<span>Stage</span> = FINISHED)
                 ↓
              DestroySession

对应到接口,这一段是自写的完整骨架:

<span>// recon_session.cpp —— 三段式会话骨架</span>
<span>#<span>include</span> <span>"spatial/spatial_recon_interface.h"</span></span>

<span>// ① 创建会话:工作目录必须是应用内部文件目录的子目录</span>
<span>const</span> <span>char</span>* kWorkDir = <span>"/data/storage/el1/base/spatial_recon_files/"</span>;
HMS_SpatialRecon_Session* session = <span>nullptr</span>;
HMS_SpatialReconStatus ret =
    <span>HMS_SpatialRecon_CreateSession</span>(SPATIAL_RECON_MODEL_TYPE_GS, kWorkDir, &session);

<span>// ② 启动重建:writeInfo 非空 → 重建完成后自动保存;为空 → 稍后手动保存</span>
HMS_SpatialRecon_ModelWriteInfo info;
info.modelFormat = SPATIAL_RECON_OUTPUT_FORMAT_MP4;   <span>// 也可保存为 PLY 点云</span>
<span>auto</span> onFinished = [](HMS_SpatialReconStatus status) {
    <span>// 重建结束回调:这里只做通知,别在回调里做重活</span>
    <span>return</span>;
};
<span>HMS_SpatialRecon_StartSession</span>(session, &info, onFinished);

<span>// ③ 运行模式:必须按应用是否在前台设置,否则可能性能/功耗劣化</span>
<span>HMS_SpatialRecon_SetRunningMode</span>(SPATIAL_RECON_RUNNING_FOREGROUND_MODE);

<span>// 进度查询:第二个出参还能带出当前 Stage</span>
<span>float</span> progress = <span>0.0f</span>;
<span>HMS_SpatialRecon_GetProgress</span>(session, &progress, <span>nullptr</span>);

<span>// 结束后销毁</span>
<span>HMS_SpatialRecon_DestroySession</span>(session);

三个必须记住的点:

  1. 工作目录有硬要求CreateSession 时指定的工作目录必须已存在,且必须是应用内部文件目录的子目录(例如 /data/storage/el1/base/ 下面)。传个外部路径或者不存在的目录,创建就失败。
  2. SetRunningMode 不是可选项。官方原话是"此标志位如未正确设置,可能导致性能或者功耗劣化"。前台就设前台模式,切后台就设后台模式——这是让系统给你分配计算资源的依据。
  3. 销毁是有前提的:会话不保证并发安全。官方明确说明,在会话还在执行任务(重建或保存)时请求销毁,会导致未定义行为;一旦销毁,就不能再对该会话做任何操作。

保存结果也有两种姿势:StartSession 时把 writeInfo 传进去(重建完自动存),或者重建结束后手动调 SaveResultToFile。输出格式支持 PLY(点云)MP4(运镜视频)


五、第四关:两条硬边界——温度与串行

这一节是全文最该抄进架构评审的部分。

硬边界一:必须订阅温度事件

空间重建对系统资源的消耗量级,从官方这句话就能看出来:

由于空间重建计算量较大,强烈建议开发者通过 HMS 的公共事件接口,订阅热公共事件 COMMON_EVENT_THERMAL_LEVEL_CHANGED。当检测到设备温度过高时,自动暂停重建并提示用户,防止过热导致卡顿。

注意官方的用词是"强烈建议"——翻译过来就是"这是准入项,不是优化项"。这是V哥见过的第一个把"温控"写进主流程的 Kit。 实现上就是三步:订阅事件 → 温度超阈值 → 调 HMS_SpatialRecon_PauseSession,等温度回落再 HMS_SpatialRecon_ResumeSession

<span>// 过热保护:暂停与继续</span>
<span>HMS_SpatialRecon_PauseSession</span>(session);    <span>// 温度过高时</span>
<span>HMS_SpatialRecon_ResumeSession</span>(session);   <span>// 温度回落、用户确认后</span>

官方还补了一句很实用的建议:在应用里提供开关,让用户自己控制何时暂停、何时继续。

硬边界二:同一时刻只有一个会话

这条是整个能力最硬的边界,官方说得斩钉截铁:

由于重建过程中对系统资源消耗较大,Spatial Recon Kit 仅支持同一时刻只有一个 session 正在进行重建。如果同一时刻有多个 session 同时进行重建,会导致未定义行为

而且保存 MP4 也是串行的——同一时刻只能有一个会话在保存 MP4。

这不是性能建议,这是架构约束。 它的直接后果是:你不能让"相机页"和"模型页"各开一个会话,也不能让用户连点两次触发两轮重建。正确做法是把重建做成全局单例队列

  • 会话管理器全局唯一,同一时刻只有一个活跃会话;
  • 重建请求进队列,前一个没结束时,后来的请求排队而不是并发;
  • 页面销毁不等于会话结束,会话的生死必须由管理器统一管。

第二个原创判断:把"单会话"当成一个全局互斥锁来设计,而不是当成一个参数来传。 V哥在调研时看到过不少示例把 CreateSession 写在页面里——按官方这条约束,那种写法在真实场景里迟早撞车。


六、重建完怎么"看":ArkTS 侧的加载、滤镜与编辑

C 层把模型文件吐出来之后,剩下的事全在 ArkTS 层。

加载:先给渲染上下文装上 GSPlugin,再把模型加载成节点。支持 MP4 / PLY / GLB 三种格式(加载 3DGS 模型)。

<span>// ModelStage.ets —— 自写的加载封装</span>
<span>import</span> { spatialRender } <span>from</span> <span>'@kit.SpatialReconKit'</span>;
<span>import</span> { <span>Scene</span>, <span>RenderContext</span> } <span>from</span> <span>'@kit.ArkGraphics3D'</span>;

<span>export</span> <span>async</span> <span>function</span> <span>mountGSModel</span>(<span>uri: <span>string</span></span>): <span>Promise</span><spatialRender.<span>GSNode</span> | <span>null</span>> {
  <span>const</span> <span>ctx</span>: <span>RenderContext</span> | <span>null</span> = <span>Scene</span>.<span>getDefaultRenderContext</span>();
  <span>if</span> (ctx === <span>null</span>) {
    <span>return</span> <span>null</span>;
  }
  <span>// 1. 先注册 GSPlugin,不注册则场景不认识 3DGS 数据</span>
  ctx.<span>loadPlugin</span>(spatialRender.<span>GSPlugin</span>.<span>PLUGIN_ID</span>);

  <span>// 2. 加载场景</span>
  <span>const</span> <span>scene</span>: <span>Scene</span> = <span>await</span> <span>Scene</span>.<span>load</span>();

  <span>// 3. 加载 3DGS 节点:offset 是数据在文件中的偏移量,一般传 0</span>
  <span>const</span> <span>node</span>: spatialRender.<span>GSNode</span> =
    <span>await</span> spatialRender.<span>GSPlugin</span>.<span>loadGSNode</span>(scene, { uri, <span>offset</span>: <span>0</span> }, scene.<span>root</span>);

  <span>// GSNode 继承自 Node,可以像普通节点一样摆位置、缩放、控可见性</span>
  node.<span>position</span> = { <span>x</span>: <span>0</span>, <span>y</span>: <span>0</span>, <span>z</span>: -<span>3</span> };
  node.<span>scale</span> = { <span>x</span>: <span>1</span>, <span>y</span>: <span>1</span>, <span>z</span>: <span>1</span> };
  node.<span>visible</span> = <span>true</span>;
  <span>return</span> node;
}

这里有个顺序坑loadPlugin 必须在 loadGSNode 之前。少了这一步,GSNode 拿不到,场景也渲染不出高斯数据。

滤镜spatialRender 提供了几套现成的风格化效果——RetroEffect(复古)、ComicEffect(漫画)、ObraDinnEffect(黑白 bit 风)、ColorEditingEffect(颜色编辑),参数类从 6.1.0(23) 起提供(spatialRender API)。对商品展示、文博复刻这类场景,"一键换风格"是很实用的差异点。

编辑:7.0(26) 新增的 spatialEdit 才是真正把"重建"变成"可再创作"的一环(spatialEdit API)。核心是 GSEdit 类:

  • 选择:selectBy2DBox / selectBy3DBox / selectByIndex / selectBy2DMask,选中结果都追加到当前选区;
  • 变换与上色:transform(matrix)paint(color, mode),其中 PaintModeREPLACE / MULTIPLY / ADD 三种混合模式;
  • 删除与撤销:remove()undo()
  • 导出:saveToPLY(uri),把编辑后的模型存回 PLY;
  • 还有一个V哥觉得很实用的 extract3DMainBody(pressPoint)——按一个点把 3D 主体抠出来,相当于给模型做"抠图"。

大场景怎么办?TiledGSNode26.0.0 起),它是专门为大规模 3DGS 场景设计的分块渲染对象。模型一大就上分块,别指望单个 GSNode 硬扛。


七、上线自检清单

  • 确认过 ArkTS 侧没有重建接口,重建代码写在 C/C++ 层了吗?
  • HMS_SpatialRecon_IsSupport 调了吗?不支持时的完整降级路径做了吗?
  • 芯片不在保证名单(Kirin 9020 / 9030S / 9030 / 9030 Pro 及以后)时,有没有对用户降低效果预期的提示?
  • 输入图像是 1080×1440 吗?不是的话有没有做裁切/缩放?格式是 RGB 吗?
  • CreateSession 的工作目录存在、且在应用内部文件目录下吗?
  • 每一次 StartSession 之后都紧跟了 SetRunningMode 吗?切前后台有同步更新吗?
  • 订阅 COMMON_EVENT_THERMAL_LEVEL_CHANGED 了吗?过热能自动暂停吗?有用户手动暂停/继续的开关吗?
  • 重建和保存 MP4 都做了全局串行吗?会不会出现两个会话同时跑?
  • DestroySession 之前,确认重建/保存任务都已经结束了吗?
  • loadPlugin(GSPlugin.PLUGIN_ID)loadGSNode 之前调了吗?
  • 大场景用了 TiledGSNode 吗?
  • 真机上验证过重建耗时、发热与渲染帧率吗?(别在模拟器上验收——本 Kit 不支持模拟器

参考与出处

本文涉及的事实性信息(API 名称、枚举取值、版本号、官方约束)来自以下官方文档,文中的结构、代码示例、决策流程与自检清单为本人整理编写:


最后一句:这个能力最反直觉的地方在于——你以为难点是"算法",其实算法系统都封装好了;真正的难点是承认它有多"重":重到要限定芯片、重到要盯温度、重到同一时刻只能跑一个。想清楚这三件事,剩下的就是按状态机把接口串起来。