基于 HarmonyOS SDK +Flutter+C++的 3D 重建接入实践
基于 HarmonyOS SDK +Flutter+C++的 3D 重建接入实践
这次做的重点,
不是单独做一个 3D 展示页。
真正落地的是一条完整链路。
这条链路把
Flutter、
HarmonyOS ArkTS、
C++
三层真正接通。
最后形成的是
入口调起、
原生采集、
CSDK 重建、
本地模型保存、
3DGS 预览、
模型列表管理
这一整套流程。
这篇文档不讲泛泛概念。
只围绕当前项目里
已经实现的 3D 功能来写。
重点放在三件事:
第一,
整体架构怎么拆。
第二,
关键代码怎么落。
第三,
页面和流程怎么跑起来。
一、项目里这套 3D 功能到底做了什么
现在这套能力,
不是拍一张图然后做个假预览。
而是基于 HarmonyOS
Spatial Recon CSDK
接入了真实的空间重建流程。
在当前工程里,
用户先从 Flutter 页面进入。
然后通过 MethodChannel
切到 HarmonyOS 原生页面。
原生页面负责权限申请、
会话初始化、
采集状态切换、
重建状态轮询。
底层 C++ 负责和 CSDK 直接交互。
包括:
AR Frame 输入、
会话启动、
进度查询、
模型保存。
模型生成完成以后,
结果不上传服务器。
而是直接保存在应用本地目录。
接着模型列表页读取本地文件。
预览页再通过官方
3DGS 加载组件
把模型接入渲染场景。
这就是当前实现的主线。
二、整体架构
这次实现不是一层代码写到底。
而是明确拆成三层。
1. Flutter 层
Flutter 层负责业务入口。
它不直接处理重建细节。
只负责:
展示入口,
触发能力调用,
给用户兜底提示。
2. ArkTS 层
ArkTS 层负责原生页面。
它承担的是页面状态编排。
包括:
权限申请,
本地目录准备,
采集页生命周期,
重建进度轮询,
模型列表跳转,
模型预览页跳转。
3. C++ 层
C++ 层负责底层能力。
这里直接对接的是
HarmonyOS Spatial Recon CSDK。
真正和会话、
帧输入、
模型文件、
重建回调
打交道的地方都在这里。
三、架构流程图
下面这张图,
就是当前项目里实际跑通的结构。
Flutter 首页入口
|
v
NativeService / MethodChannel
|
v
HarmonyOS NativePlugin.ets
|
v
PetReconstructionPage.ets
|
v
libpetrecon.so
|
v
pet_reconstruction.cpp
|
v
Spatial Recon CSDK
|
v
本地 PLY 模型文件
|
v
PetReconstructionModelsPage.ets
|
v
PetReconstructionModelViewerPage.ets
|
v
loadGSNode 加载 3DGS 模型
这张图里最重要的点,
不是层数多。
而是边界清楚。
Flutter 不碰底层重建。
ArkTS 不自己生成模型。
C++ 不关心页面布局。
这样后面继续调优时,
就不容易互相牵连。
四、演示流程图
如果从用户操作角度看,
当前的演示流程是下面这样。
点击 3D 功能入口
|
v
检查设备 API 能力
|
v
打开原生采集页
|
v
申请相机 / 陀螺仪 / 加速度计权限
|
v
初始化重建工作目录
|
v
开始采集有效 AR Frame
|
v
启动 Spatial Recon 会话
|
v
生成 PLY 模型
|
v
模型保存到本地目录
|
v
进入本地模型列表
|
v
点击模型进入 3DGS 预览页
五、页面效果截图
当前生成的模型,
会进入本地模型列表。
这张图对应的是列表页效果。

下面这张图,
对应官方文档里的 3DGS 加载效果参考。

下面这张图,
对应官方支持格式说明。

六、Flutter 层怎么接入
Flutter 层这里,
我故意做得很轻。
目的只有一个:
不要让业务层直接碰原生重建细节。
关键代码 1:业务页打开 3D 功能
Future<void> _openPetReconstruction() async {
final opened = await NativeService.openPetReconstruction();
if (!opened && mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('当前设备暂不支持三维重建')),
);
}
}
代码解析:
这里第一行是入口调用。
它不是直接开相机。
而是先走原生能力桥接。
第二段判断是兜底逻辑。
如果设备不支持,
或者原生页面没有成功打开,
Flutter 会直接给用户提示。
这个设计的价值在于:
业务层只做入口。
不承担原生能力细节。
这样 Flutter 页面会更稳定。
关键代码 2:Flutter 和原生的桥接服务
static Future<bool> openPetReconstruction() async {
try {
final result = await _channel.invokeMethod('openPetReconstruction');
return result == true;
} catch (e) {
print('NativeService: openPetReconstruction error: $e');
return false;
}
}
代码解析:
这里核心不是调用方法本身,
而是把 Flutter 和 HarmonyOS 原生能力之间,
固定成一个清晰的 MethodChannel 协议。
这样以后如果原生侧继续增加参数,
或者更换打开逻辑,
修改点也非常集中。
七、HarmonyOS ArkTS 层怎么承上启下
ArkTS 页面层,
在这套方案里不是装饰层。
它是真正的状态调度层。
一旦进入原生页面,
大部分重建流程控制都从这里开始。
关键代码 3:能力判断和页面跳转
private getPetReconstructionCapability(result: MethodResult): void {
const apiVersion: number = deviceInfo.sdkApiVersion;
let capability: Map<string, Object> = new Map<string, Object>();
capability.set('apiVersion', apiVersion);
capability.set('supported', apiVersion >= 23);
result.success(capability);
}
private openPetReconstruction(result: MethodResult): void {
if (deviceInfo.sdkApiVersion < 23) {
result.success(false);
return;
}
router.pushUrl({ url: 'pages/PetReconstructionPage' }).then(() => {
Log.i(TAG, 'Pet reconstruction page opened in EntryAbility');
result.success(true);
}).catch((error: BusinessError) => {
Log.e(TAG, `openPetReconstruction failed: ${error.code}, ${error.message}`);
result.error('OPEN_RECONSTRUCTION_FAILED', error.message, error.code);
});
}
代码解析:
第一段做的是 API 能力判断。
这里直接把
HarmonyOS API 23
作为能力门槛。
第二段做的是原生页面跳转。
也就是说,
Flutter 只负责发起调用,
真正的页面切换是在 HarmonyOS 侧完成的。
这种方式比在 Flutter 里自己猜设备能力更稳。
八、ArkTS 采集页具体做了什么
当前的采集页,
不是一个纯 UI 页面。
它真正负责的是:
准备本地目录,
申请权限,
恢复已有状态,
初始化原生管线,
轮询重建进度。
关键代码 4:页面准备逻辑
private async prepare(): Promise<void> {
if (!this.apiSupported) {
this.errorMessage = '三维重建需要 HarmonyOS API 23 或更高版本';
return;
}
this.ensureModelDirectory();
const existingState: PetReconState = getState();
if (existingState.reconstructing || existingState.finished) {
this.permissionGranted = true;
this.hardwareSupported = existingState.supported;
this.applyNativeState(existingState);
if (existingState.reconstructing) {
setRunningMode(true);
this.startPolling();
}
return;
}
const granted: boolean = await this.requestReconstructionPermissions();
this.permissionGranted = granted;
if (!granted) {
this.errorMessage = '需要相机和运动传感器权限才能进行空间重建';
return;
}
this.hardwareSupported = isSupported();
if (!this.hardwareSupported) {
this.errorMessage = '当前设备不支持 AR Engine SLAM 或 Spatial Recon Kit';
}
}
代码解析:
这里第一步,
先拦 API 版本。
第二步,
创建本地模型目录。
第三步,
检查当前是不是已经有正在跑的重建任务。
如果有,
页面会恢复状态,
而不是重新起一套流程。
第四步,
再去申请权限。
这个顺序很重要。
因为 3D 重建不是一次性按钮动作,
它是一个持续会话。
页面如果每次都重置,
状态一定会乱。
关键代码 5:权限申请
private async requestReconstructionPermissions(): Promise<boolean> {
try {
const manager = abilityAccessCtrl.createAtManager();
const permissions: Permissions[] = [
'ohos.permission.CAMERA',
'ohos.permission.GYROSCOPE',
'ohos.permission.ACCELEROMETER'
];
const result = await manager.requestPermissionsFromUser(this.context, permissions);
return result.authResults.length === permissions.length &&
result.authResults.every((authResult: number) => authResult === 0);
} catch (error) {
return false;
}
}
代码解析:
这里申请的是当前实现真正需要的三项权限。
相机负责图像输入。
陀螺仪和加速度计负责设备姿态感知。
权限范围越清楚,
后面页面行为越稳定。
而且也更容易和官方能力要求保持一致。
关键代码 6:初始化原生重建管线
private initializeNativePipeline(): void {
try {
this.ensureModelDirectory();
this.workDirectory = `${this.context.filesDir}/pet_reconstruction/work/session_${Date.now()}`;
if (!fileIo.accessSync(this.workDirectory)) {
fileIo.mkdirSync(this.workDirectory, true);
}
const existingState: PetReconState = getState();
if (existingState.reconstructing || existingState.finished) {
this.applyNativeState(existingState);
if (existingState.reconstructing) {
this.startPolling();
} else {
this.refreshLocalModelState();
}
return;
}
const result: PetReconActionResult = initialize(this.workDirectory);
if (!result.success) {
this.errorMessage = result.message;
return;
}
this.startPolling();
} catch (error) {
this.errorMessage = `初始化三维服务失败: ${error}`;
}
}
代码解析:
这里主要解决的是
“工作目录”和“重建状态恢复”。
每次新任务都会准备新的 work 目录。
如果底层已经在重建,
页面就直接恢复。
如果没有,
再真正初始化 native pipeline。
九、C++ 层为什么是这套方案的核心
如果说 Flutter 层是入口,
ArkTS 层是编排,
那 C++ 层就是重建本体。
因为真正和 Spatial Recon CSDK
直接对接的,
就是这一层。
关键代码 7:底层能力装载
CreateSession createSession = nullptr;
PushARFrame pushARFrame = nullptr;
StartSession startSession = nullptr;
SaveResultToFile saveResultToFile = nullptr;
GetProgress getProgress = nullptr;
代码解析:
这几组函数指针,
对应的是整个重建链路里最核心的几步。
会话创建,
输入 AR Frame,
正式开始重建,
查询进度,
保存结果。
这也说明当前实现不是自己拼一个伪流程,
而是明确建立在官方 CSDK 的会话模型之上。
十、模型输出格式为什么统一成 PLY
这次实现里,
我把模型格式统一固定成 PLY。
原因很实际。
如果输出格式不统一,
后面列表页、
缩略图、
预览页
都要分别做兼容。
而统一成官方支持格式以后,
整个链路会干净很多。
关键代码 8:设置输出模型格式
writeInfo_.modelFile = modelPath_.c_str();
writeInfo_.modelFormat = SPATIAL_RECON_OUTPUT_FORMAT_PLY;
代码解析:
第一行决定输出路径。
第二行决定输出格式。
这两行虽然不长,
但实际上把生成端和预览端绑定成了一条稳定链路。
后面模型管理页读本地文件时,
就不需要再做复杂的格式分支。
十一、真正启动重建的地方
模型格式定好以后,
还不能立刻进入预览。
中间最重要的一步,
是正式启动 Spatial Recon 会话。
关键代码 9:启动重建任务
HMS_SpatialReconStatus status = spatialApi_.startSession(
spatialSession_, nullptr, OnReconstructionFinished);
if (status != SPATIAL_RECON_STATUS_SUCCESS) {
return Failure(status, SpatialStatusMessage(status, "无法启动三维重建"));
}
代码解析:
这里是真正把输入帧推进到建模阶段的地方。
如果 startSession 没有成功,
后面所有流程都不该继续。
所以当前实现里,
这里做了明确的状态判定和失败返回。
这种写法的好处是,
失败边界清晰。
后面查问题时,
也更容易知道是会话没起来,
还是保存环节有问题。
十二、为什么保存模型也要单独处理
很多时候,
大家更关注模型什么时候开始生成。
但实际上,
模型什么时候真正“可读”,
同样关键。
因为文件路径出现了,
不代表预览层就一定能正确加载。
关键代码 10:保存模型文件
const HMS_SpatialReconStatus saveStatus = spatialApi_.saveResultToFile(
spatialSession_, &writeInfo_, OnModelSaved);
if (saveStatus != SPATIAL_RECON_STATUS_SUCCESS) {
currentStatus_ = saveStatus;
reconstructionStarted_ = false;
reconstructionCompleted_ = false;
saveStarted_ = false;
finished_ = false;
errorMessage_ = SpatialStatusMessage(saveStatus, "三维模型保存失败");
return;
}
代码解析:
这段代码的重点,
不是单纯把文件写出去。
而是明确把“重建完成”和“模型保存完成”拆成两个阶段。
这样做的收益很大。
因为模型只有在真正保存成功以后,
才应该被列表页读取,
才应该进入缩略图生成,
也才应该进入预览页加载。
十三、本地模型管理页解决了什么问题
如果只有重建完成页,
那整个功能更像是一个 Demo。
但当前实现里,
模型生成完成以后会进入本地模型列表。
这一层让 3D 功能从“临时演示”
变成了“可继续使用的能力”。
模型列表页目前已经具备这些能力:
读取本地文件,
展示模型名称,
展示文件大小,
展示创建时间,
支持重命名,
支持删除,
支持进入预览页。
这意味着模型结果不是一次性的。
它已经具备持续管理的基础。
十四、预览页为什么坚持走官方 3DGS 组件
当前预览页没有自定义解析 PLY。
而是直接走官方 loadGSNode。
这样做不是为了省事,
而是为了把边界分清楚。
生成端负责输出模型。
预览端负责加载模型。
这样后面一旦出现问题,
就能更明确地区分是:
采集问题,
重建问题,
还是加载问题。
关键代码 11:加载 3DGS 模型
const modelUri: string = `file://${renderModelPath}`;
const loadedNode: spatialRender.GSNode = await spatialRender.GSPlugin.loadGSNode(
loadedScene,
{ uri: modelUri, offset: 0 },
loadedScene.root
);
loadedNode.position = { x: 0, y: 0, z: 0 };
loadedNode.scale = { x: 1, y: 1, z: 1 };
loadedNode.visible = true;
代码解析:
第一行先把本地路径转成可加载的 URI。
第二段通过官方 GSPlugin
把模型接入场景树。
后面 position、
scale、
visible
这些设置,
则是为了让模型在场景中以稳定状态呈现出来。
也就是说,
预览页的核心职责不是重建模型,
而是把本地生成好的模型
稳定接入渲染场景。
十五、当前实现已经落下来的能力
从现在这套代码来看,
已经真正落下来的能力并不少。
第一,
Flutter 入口已经打通。
第二,
HarmonyOS 原生桥接已经打通。
第三,
ArkTS 页面已经能接管权限、
状态和页面流转。
第四,
C++ 已经和 Spatial Recon CSDK
直接对接完成。
第五,
本地模型文件已经能生成并保存。
第六,
本地模型页已经可以管理这些结果。
第七,
预览页已经可以通过官方 3DGS 组件完成加载。
从这个角度看,
现在这套 3D 功能已经不是一个“样子工程”。
它已经具备继续演进的底座。
十六、后面最值得继续优化的地方
如果继续往下做,
后面最值得投入的方向主要有三块。
第一块是采集质量。
也就是怎样让输入 AR Frame
更稳定,
更有效。
第二块是重建稳定性。
也就是会话状态、
保存边界、
恢复逻辑
要更稳。
第三块是预览体验。
包括:
拖动,
缩放,
缩略图生成,
默认视角,
模型清晰度。
但这些事情之所以现在有地方可改,
前提就是这次已经把
Flutter、
ArkTS、
C++、
HarmonyOS CSDK
这四块真正连起来了。
十七、总结
这次 3D 重建接入,
最重要的成果,
不是页面多了几个按钮。
而是把一条真正的工程链路落下来了。
现在这套结构里,
Flutter 负责入口,
HarmonyOS ArkTS 负责页面和状态,
C++ 负责底层会话和重建,
官方 3DGS 组件负责本地预览。
这种拆法,
让每一层的职责都很清楚。
后面不管继续做画质优化,
还是继续做模型管理能力,
都会比把所有逻辑堆在一起容易得多。
如果从工程视角看,
这次最有价值的地方,
就是把 3D 功能真正做成了一个可以继续扩展的模块,
而不是一次性的效果演示。
更多推荐


所有评论(0)