HarmonyOS 6.1端侧3DGS重建实战:C层核心+ArkTS交互

发布时间:2026/9/17 5:57:12
HarmonyOS 6.1端侧3DGS重建实战:C层核心+ArkTS交互 HarmonyOS 6.1 上跑通端侧 3DGS 重建是今年我们团队最折腾也最有成就感的项目之一。先说明结论重建逻辑全部放在 C 层ArkTS 只负责把中间结果画出来、把参数改回去。这个分层决定了整个项目的性能上限也决定了我们后面踩坑的数量级。如果你准备在 HarmonyOS 设备上做类似的 3D 重建或者你手上正好有一台跑 HarmonyOS 6.1 的机器想试试 3DGS这篇文章值得你读完。我会从架构选型、重建主流程、ArkTS 接口设计讲到调优和排坑尽量把关键参数和实现思路都贴出来。先说清楚 3DGS 是什么3D Gaussian Splatting本质是用一堆带颜色、透明度、形状的三维高斯体去拟合真实场景。传统重建输出网格模型3DGS 输出的是“点云 参数包”。它渲染质量接近 NeRF但速度可以做到实时很适合端侧做“扫描一个物体生成可交互 3D 场景”这类体验。但“适合渲染”不代表“适合在端侧训练”训练部分的计算压力非常大。1. 为什么重建必须放在 C 层ArkTS 不可能干这件事1.1 先拆任务3DGS 重建到底有哪些计算量你拿起手机对着一个杯子转一圈想要生成这个杯子的三维高斯表示。整个过程大概分成四步从视频帧里提取特征点做帧间匹配估算每帧相机位姿SfM / SLAM 部分初始化一批三维高斯然后迭代优化位置、尺度、旋转、透明度、颜色对每一帧做可微渲染计算损失反向传播。这里面随便挑一个步骤都是百万级浮点运算。特征点提取要对每帧图像做金字塔缩放、梯度计算、描述子统计位姿估计要求解非线性最小二乘每一轮都要做雅可比矩阵分解高斯优化更是夸张一个中等场景十几万个高斯体每个高斯对屏幕上的多个像素都有贡献投影、排序、blend、反向传播一轮下来就是上亿次浮点运算。这套计算如果放在 ArkTS 里跑会遇到几个非常现实的问题TypeScript 运行时是解释执行加 JIT没有足够高效的 SIMD 指令集抽象对象数组访问会带来大量边界检查和 GC 压力一个Float32Array虽然能存数据但要做复杂矩阵运算时每步都要建中间变量内存拷贝次数多到能把 L2 Cache 打穿。我自己早期做实验用 ArkTS 写了一个简化版高斯投影单帧 1280x720 分辨率、五万高斯一次前向渲染花了 900 多毫秒。而同一个算法用 C 加 NEON 优化降到 47 毫秒。这个差距不是靠手机硬件提升能弥补的完全是语言运行时和内存模型的问题。所以“重建在 C 层”不是偏好问题是能不能跑起来的问题。1.2 HarmonyOS 6.1 给 Native 层提供了什么能力HarmonyOS 6.1 的 Native 开发体系已经相当完整。通过 NDK 可以直接使用 C/C 写核心逻辑通过 N-API 和 ArkTS 互相调用。针对图形计算OpenGL ES 3.2 和 Vulkan 都能用Vulkan 可以拿来做 compute shader 加速可微渲染部分。系统还提供了native_window和surface相关的接口可以绕过 ArkTS 直接把渲染结果输出到显示 Surface减少一次纹理拷贝。我在项目里用的是 OpenGL ES 3.2 做光栅化渲染Compute Shader 做高斯参数更新的一部分操作。选 OpenGL ES 而不是 Vulkan主要原因是团队对 OpenGL ES 更熟而且 HarmonyOS 6.1 上 GLES 的驱动稳定性比 Vulkan 要好尤其是多线程同时做渲染和训练时GLES 的同步语义更简单。C 层负责的工作清单大致是图像采集通过 Camera API 拿到 YUV 帧转成 RGB再生成图像金字塔特征提取与匹配用轻量 ORB 类特征提取器MTK/海思都不挑兼容性好位姿估计跑一个简化版 bundle adjustment输出当前帧的 4x4 相机矩阵高斯场景更新新增高斯、位置优化、协方差约束、透明度剪枝实时渲染把当前高斯集合渲染成一幅彩色图和一幅 alpha 图给 ArkTS 显示快照导出把全部高斯参数二进制导出方便后续加载或分享。ArkTS 侧只保留三件事显示渲染结果、接收手势、修改参数。说白了UI 是遥控器C 层才是发动机。1.3 为什么不让 ArkTS 也参与一部分计算你可能想问ArkTS 也不是完全不能算能不能让它做点轻量的活比如特征匹配排序、训练 Loss 曲线绘制我的建议是除 UI 外尽量别碰计算。原因有三点。第一ArkTS 和 C 层通过 N-API 传参每次调用都有序列化和跨语言边界成本。如果频繁把小数组传过去传回来性能损耗比例极高。第二ArkTS 的数组和对象生命周期由运行时管理长耗时循环里容易触发后台 GCGC 发生时会暂停所有 JS 线程导致画面掉帧。第三HarmonyOS 6.1 的 TaskPool/Worker 虽然能做后台任务但 Worker 之间复制数据是深拷贝对ArrayBuffer还好对对象数组会有明显开销。所以我们的原则是数据进门就转成 C 端的vector或裸指针后续所有计算都在 C 内存空间完成ArkTS 只拿结果引用不复制数据。这个原则执行得越彻底后面性能问题越少。我见过太多项目口口声声说“C 加速”结果每次迭代都从 JS 层把数据拷贝到 C 层算完再拷贝回来整体比纯 JS 还慢。做端侧 3DGS 这种高频迭代场景拷贝次数是第一位要消灭的东西。2. 端侧 3DGS 重建主流程从图像序列到高斯场景2.1 输入数据与预处理绕开“先拍视频再导入”的坑很多人拿到 3DGS 项目第一反应是“我调用相机录像录完一段视频再丢给算法”。这个思路在 PC 上没问题在端侧行不通。因为端侧内存有限一段 10 秒的 4K 视频解码出来能占掉 1.5GB 内存再加上高斯参数和中间特征缓存直接 OOM。实际操作时我们采用“边采集边抽帧”的策略。相机实时预览走 CameraKit 的PhotoOutput或PreviewOutput在预览回调里每隔 N 帧取一帧做处理。选帧要满足两个条件和上一帧的位姿变化不能太小否则没有新信息也不能太大否则特征匹配容易失败。我们用一个简单指标上一帧和当前帧的重合区域面积占比。占比在 60% 到 85% 之间就保留这一帧。帧率上我们取 30fps 视频流实际抽帧约 8fps 到 12fps。采集 90 帧左右足够重建一个圆形物体的 360 度视角。这个数量比 PC 端 200-300 帧少很多代价是重建细节会差一点但端侧体验优先。预处理部分有个容易忽略的点相机内参。3DGS 重建对相机内参误差非常敏感。如果直接用预览流的宽高比去计算内参重建出来的几何会扭曲。我们会在初始化时用系统相机 API 查询LensInfo拿到焦距、主点再根据裁剪关系换算成像素坐标系下的内参矩阵。这一步一定要做否则后面 SfM 算出来的位姿会带系统性偏差。2.2 稀疏重建与高斯初始化先有骨架再长肉3DGS 的输入不是原始图像序列直接进网络而是需要一个稀疏点云和每帧相机位姿作为“初始化引导”。这里有两个选择一是用完整 COLMAP二是在端侧写一个轻量 SfM 流程。COLMAP 在 PC 上很成熟但它的特征提取、匹配、全局优化在端侧慢得离谱而且内存峰值高。我们选择自己写轻量 SfM。流程是对抽出的帧提取 ORB 特征点降采样到 640x360 分辨率上提特征用汉明距离做暴力匹配再用 RANSAC 过滤误匹配从第一帧建立初始坐标系后续每帧用 PnP 求解位姿三角化新观察点局部 bundle adjustment 只优化最近 20 帧。这个流程精度比 COLMAP 低但好处是内存可控、可流式处理。实测 90 帧输入SfM 总耗时约 4 秒稀疏点云大概 8000 到 15000 个点。这个数量级做高斯初始化完全够用。初始化高斯时我们把每个三角化点作为高斯中心尺度初始化为该点到最近三个点的平均距离旋转初始化为单位四元数透明度初始化为 0.6颜色取该点被观察像素颜色的均值。这样一个稀疏点云大概生成 1 到 2 万个高斯后续训练时再通过 densification 增加到需要的密度。2.3 可微光栅化训练循环端侧训练的重头戏3DGS 训练过程就是不断调整高斯参数让渲染结果逼近真实图像。核心公式上每个高斯在屏幕空间形成一个椭圆斑颜色和透明度通过排序后 alpha blending 合成最终像素。损失函数我们复用了论文里的组合L1 颜色损失 0.2 倍的 D-SSIM 损失后面这个 D-SSIM 用来保持边缘细节。学习率设置很关键位置学习率 0.001尺度学习率 0.005旋转学习率 0.001透明度学习率 0.01。这个组合在端侧收敛稳定。稠密化和剪枝也做了简化。原始 3DGS 论文里会根据梯度分裂和克隆高斯我们前端侧版本只在每 10 轮迭代中执行一次检查且设置高斯总数上限。分裂操作会用两个高斯替换一个高斯尺度减半克隆操作会把高斯复制一份位置朝向梯度方向偏移。剪枝则把透明度低于 0.05 且位置相关梯度小的高斯移出场景。跑了 100 轮迭代端侧 90 帧输入高斯基数约 8 万训练总耗时约 65 秒。这个速度在裸 C NEON 优化下实现如果用 GPU Compute Shader 可以压到 45 秒以内。作为对比PC 上 1080Ti 跑同样数据 90 轮只要 15 秒。差距能接受毕竟端侧功耗有限。2.4 渲染与导出不是开发板演示是真的能交互训练完成后实时渲染 8 万个高斯在 1280x720 分辨率下C 层用 OpenGL ES 3.2 排序 光栅化耗时约 23 毫秒也就是 40 帧左右。如果抽到 960x540 分辨率能跑到 50 帧以上。这个帧率足够支撑“拿着手机绕着物体看”的体验。导出格式我们用了自定义二进制文件头记录高斯数量、相机数量、SHS 阶数接下来每个高斯写位置、尺度、旋转四元数、透明度、球谐参数最后写相机内参和位姿列表。这样 ArkTS 端的“模型文件”就是一个ArrayBuffer可以直接存到沙箱目录或分享。导出前做一次全局量化把float32压成float16体积直接减半。视觉上 16 位精度对位置和颜色影响不大对旋转四元数略微有差异但端侧展示场景用户基本感知不到。3. 实操ArkTS 只做“看”和“改”的接口设计3.1 N-API 接口设计最少暴露四个接口就够ArkTS 和 C 层的通信我用 N-API 做了四个接口接口作用入参返回initSession初始化重建会话相机内参、最大高斯数无processFrame输入一帧图像和位姿ArrayBuffer 图像、宽高、相机矩阵当前进度getRenderResult拿渲染结果目标宽高ArrayBuffer 颜色图updateParams修改训练参数学习率、迭代轮数、剪枝阈值无设计原则是“大块数据走 Buffer小数数据走 number”。不要每次传一个对象也不要传一个字符串表示参数。ArkTS 对象传进 N-API 后每一次属性访问都要走napi_get_value_*系列函数性能开销非常大。我们实测如果每帧传一个{ width: 1920, height: 1080, data: arrayBuffer }这样的对象帧率高时 N-API 的打解包开销能占掉 8% CPU。改成只传四个基本参数加一个 ArrayBuffer开销降到 1% 以下。代码上initSession的 C 实现大概长这样#include napi/native_api.h static ReconstructSession *g_session nullptr; static napi_value InitSession(napi_env env, napi_callback_info info) { size_t argc 2; napi_value args[2]; napi_get_cb_info(env, info, argc, args, nullptr, nullptr); size_t len 0; napi_get_arraybuffer_info(env, args[0], nullptr, len); // 第一个参数是 Float32Array 格式的内参矩阵 float *intrinsics nullptr; napi_get_typedarray_info(env, args[0], nullptr, nullptr, (void **)intrinsics, nullptr, nullptr); int32_t maxSplats 0; napi_get_value_int32(env, args[1], maxSplats); if (!g_session) { g_session new ReconstructSession(); g_session-Init(intrinsics, maxSplats); } return nullptr; }ArkTS 侧调用非常简单import gs3d from libgs3d.so; const intrinsics new Float32Array(4); // fx, fy, cx, cy gs3d.initSession(intrinsics, 200000);这里有个细节HarmonyOS 里import一个 native 模块模块名要和在CMakeLists.txt里add_library产出的 so 文件名对应。调试时如果老是提示undefined先确认 so 有没有真打进 HAP别一上来就怀疑接口。3.2 ArkTS 的“看”Canvas 拿到 C 层图像做展示“看”分两个层面一是训练过程中看中间结果二是训练完成后做交互式展示。中间结果我们用最朴素的方式C 层每迭代 5 轮把当前渲染结果编码成 RGBA 字节数组通过getRenderResult返回。ArkTS 侧用CanvasRenderingContext2D的drawImage或者ImageBitmap直接绘制。这里强烈建议返回ImageBitmap之前先把数据从 ArrayBuffer 拷贝到一个PixelMap而不是直接拿原始 Buffer 画。原因很简单Canvas 绘制需要一个ImageData对象而ImageData的构造过程会复制数据。如果你每次从 C 层拿 ArrayBuffer再 new 一个ImageData等于多了一次全帧拷贝。改成让 C 层直接把渲染数据写进一个由 ArkTS 预分配的PixelMap的 buffer 里绘制时零拷贝。交互式展示则用 Surface 或者 XComponent 接 native 渲染窗口。最省事的方式是 ArkTS 双指缩放和拖动时把手势参数传给 C 层C 层改相机视角矩阵重新渲染渲染结果再回传。走这种“渲染在 C 层 按键在 ArkTS”的模式整个交互延迟可以压到 50ms 以内用户体感基本是实时的。3.3 ArkTS 的“改”Slider 调参和点选删除用户想改什么最常见的是调学习率、调透明度、删掉不要的高斯。调参用Slider组件最简单Slider({ value: this.learnRate * 1000, min: 0.1, max: 10, step: 0.1 }) .onChange((val: number) { this.learnRate val / 1000; gs3d.updateParams({ learnRate: this.learnRate }); })注意我这里把学习率乘了 1000 再显示因为学习率比较小直接用会让人感觉调了半天没反应。这是交互设计上的细节但很影响体验。点选删除高斯的实现路径是用户在 ArkTS 端点击画面某个点得到屏幕 UV 坐标传给 C 层后C 层用当前视角把每个高斯的中心投影到屏幕找到距离点击点最近的一个或几个高斯标记为“待删除”下一次训练迭代前移除。这个逻辑在 C 层做很快一百万个高斯线性扫一遍也就几十毫秒不需要建立空间索引。3.4 序列化与内存安全别让 ArkTS 和 C 层共同拥有同一块内存N-API 里最危险的事情之一是 ArkTS 持有一个ArrayBuffer同时把它的底层指针交给了 C 层。如果 ArkTS 端后续把它传给别的 API或者触发 GC 移动内存C 这边拿到的指针就悬空了。HarmonyOS 的ArrayBuffer是固定内存不会移动但你不能保证 ArkTS 端不会提前释放。安全做法是C 层自己开内存保存数据副本ArkTS 只负责传入原始数据。回传数据时用napi_create_arraybuffer新建 Buffer 返回C 端不要再持有这块内存的所有权。这样有性能损耗但换来了稳定。我用过更方便的方案在 C 端用napi_ref保存一个 ArkTS 传来的ArrayBuffer引用这样只要 ArkTS 侧引用不释放底层内存就一直有效。但这要求 ArkTS 端和 C 端严格遵守生命周期约定。生产环境里这种隐式共享很容易埋雷。我的建议是新手做 N-API 通信永远用拷贝别用引用先跑通再优化。4. 性能调优和问题排查实录4.1 实测性能数据与瓶颈以 HarmonyOS 6.1 设备 8 核 CPU 支持 OpenGL ES 3.2 的 GPU 为参考跑 90 帧 1280x720 图像序列重建总耗时和资源占用如下阶段平均耗时CPU占用内存峰值抽帧与图像预处理1.8s320%220MB特征提取与匹配2.2s340%180MB位姿估计与调整1.1s260%90MB高斯初始化0.3s150%60MB训练100轮65s410%780MB实时渲染预测23ms/帧45%350MB瓶颈清晰训练阶段占了整个流程 90% 以上的时间。而且训练阶段 CPU 占用长期在 400% 附近功耗也会飙升。如果你做产品这里需要考虑“后台训练 前台看结果”的双线程模型不然用户会等到怀疑人生。4.2 内存吃紧高斯数量上限和量化端侧内存只有几个 GB训练时如果高斯数量失控很快就会 OOM。我们策略有三个一是限制最大高斯数。初始化最大 20 万但默认场景超过 10 万就停止 densification只做参数优化和剪枝。二是角度筛选。不是所有相机位姿都对某个高斯有贡献训练时只对与当前相机视角相关的子集做反向传播。用“高斯中心是否落在相机视锥体内”做一个粗筛能把参加计算的高斯数量砍掉一半。三是 float16 压缩。位置、尺度、旋转用 float16 存储球谐系数用 int8 量化。精度会掉一点但显存带宽压力大幅降低。特别是在实时渲染时float16 能显著减少 GPU 显存带宽占用。4.3 崩溃排查N-API 类型转换和 Buffer 生命周期“一调用就崩溃”是我们调试初期遇到最多的问题。绝大多数原因是类型没对内核传Float32ArrayArkTS 传的是普通Array内核用napi_get_arraybuffer_info取数据但 ArkTS 传进来的是ArrayBuffer的某个DataView内核假设宽高参数是int32ArkTS 传进来的是浮点数。遇到崩溃最快的定位方式是在 N-API 入口函数最开头把所有入参类型检查一遍napi_is_arraybuffer(env, args[0], isBuffer); if (!isBuffer) { napi_throw_type_error(env, EINVAL, args[0] must be ArrayBuffer); return nullptr; }不要省这几行检查。端侧开发流程里ArkTS 和 C 不是一个人写的话类型不一致几乎是必定发生的。另一个崩溃点是内存释放。C 层 new 出来的对象析构时机如果绑定在模块生命周期上热加载 HAP 时可能导致 double free。我们在module_unregister或者Finalizer里统一清理避免在单个接口调用里释放全局会话对象。4.4 热降频和续航不能让训练跑死主线程3DGS 训练是长任务如果直接跑在 UI 线程的调用链里屏幕会卡到完全无法交互而且手机快速发热。正确做法是放到独立的std::thread或者系统TaskPool的 background 优先级里行程负责训练主线程只做轻量调度。我们还做了一个“温度护栏”机制每训练 5 轮读一次电池温度超过 40 度就手动降频学习率减半迭代轮数加长超过 45 度暂停训练提示用户休息。这个条件虽然会拖慢训练但避免了“手机烫到手握不住”的灾难现场。实时渲染也有功耗问题。如果用户只是在观察模型而不是移动视角我们就把渲染帧率限制到 15fps等有手势输入再提升到 30fps。毕竟端侧产品续航和温度是体验的一部分不能只顾性能。4.5 常见问题速查表现象可能原因处理方案processFrame闪退ArkTS 传入的 Buffer 不是 ArrayBuffer入口处检查类型并抛出明确错误重建出来模型变形相机内参不对或位姿漂移重新利用LensInfo校正内参首帧位姿做固定训练 Loss 不降学习率过大或图像亮度剧烈变化调低学习率对帧做全局亮度归一化高斯数量爆炸关闭了 densification 上限设置最大高斯数训练中定期剪枝渲染画面闪烁高斯基数过少或透明度剪枝过激进降低剪枝阈值减少单次剪枝数量N-API 调用偶发崩溃Buffer 生命周期冲突改为内部拷贝不共享内存所有权这些坑不是文档里写出来的全是跑真实设备踩回来的。希望这张表能帮你少走几圈弯路。5. 从“训练演示”到“能用的端侧产品”还差什么5.1 可交互的编辑能力远比重建本身重要用户拿到一个重建好的 3D 高斯场景不会只想转圈看。他想删除桌面上的杂物想换成白色背景拍照想让某个区域更亮一点。这些编辑动作看起来是 UI 功能做起来全是 C 层能力。我们实现了一个“高斯拾取”操作渲染时在 shader 里输出一个额外的 id buffer每个像素记录它来自哪个高斯。抓屏时把这个 id buffer 一并返回。用户在 ArkTS 端点中某个像素就知道了对应的高斯 ID接着可以删除、变透明、改色。这个功能很实用但代价是渲染时多一次 render target 输出GPU 耗时多 3ms 左右可以接受。5.2 个人对这套分层的一点体会回看整个过程我最大的感悟是“边界感”。架构上把“重建”和“展示修改”彻底分开不只是技术选择更是团队协作上的解放。写 ArkTS 的同事不必理解高斯协方差矩阵只需要面对稳定的 Buffer 接口写 C 的同事也不必关心手势和动画。所有复杂的地方都藏在 so 里ArkTS 侧看起来就是“调一个函数拿一张图发一个参数”。这也意味着如果你打算复现这条路别一开始就贪多。先把四个 N-API 接口跑通再逐步往 C 层塞功能。很多时候问题不是算法不够好而是 ArkTS 和 C 层的脏数据在互相打架。最后一定提醒一句做端侧 3DGS 千万别照搬 PC 端的工作流。PC 上你能靠显存堆规模端侧就得靠剪枝、量化、降帧率、降分辨率一点点抠。性能调优没有银弹唯有一个一个瓶颈去消。这套方案目前在我们的 HarmonyOS 6.1 测试机上运行稳定后续我们还在尝试把它扩展到视频流连续重建以及把重建结果导出为 glTF 给普通 3D 查看器用。等跑通了再回来汇报。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询