【共创稿事节】HarmonyOS 7是3DGS 端侧重建:空间建模类应用的设计机会

做 3D 内容最劝退的一环,从来不是渲染,是"把真实世界搬进屏幕"。建模师在 DCC 里拉几天面片,导出几十兆模型,到手机上还要重新减面、重烘焙贴图。3DGS(3D Gaussian Splatting)绕开了这条路径:拍一段视频,用一组带属性的高斯点去逼近真实场景,光影和纹理几乎是采集来的。HarmonyOS 7(API 26)把这件事放到了端侧,Spatial Recon Kit 提供加载与渲染能力,设计侧要回答的问题随之变了------不再是"怎么把模型做小",而是"拿到真实空间之后,应用还能做什么"。

3DGS 和传统建模,描述的是两种东西

传统建模在描述表面。顶点、三角面、UV、贴图组成一层壳,光照由材质和灯光实时计算,模型是"空心的形状"。3DGS 描述的是体积:几十万到几百万个高斯点(Gaussian Point)填充空间,每个点带位置、颜色、透明度、旋转和缩放,渲染时投影到屏幕,按深度做 alpha 混合,叠加出完整的场景。

这个差别会传导到工作流上。传统流程的成本大头在建模和贴图;3DGS 的成本大头在采集和算力。前者考验美术水平,后者考验数据质量。

维度 传统多边形建模 3DGS 重建
数据来源 人工建模 + 贴图绘制 拍摄采集 + 算法重建
典型体量 单模型数 MB 到数十 MB 单场景数十 MB 到数百 MB
结构 有拓扑,可编辑、可动画 点云式,无拓扑,编辑受限
真实感 依赖美术能力 依赖采集质量,光影天然保留
视角表现 视差正常 视差正常,非全景"单点环视"
文件格式 glTF / GLB / FBX MP4 / PLY / GLB
渲染开销 受面数、材质影响 受同屏高斯点数量影响(fillrate 敏感)

看清这张表,后面的设计取舍就有依据了:3DGS 换来了真实感,代价是体积和渲染压力都更大。

端侧重建改掉的三个前提

真实资产不用再"造"

家具应用要给用户看一张真实沙发,过去只能买模型、或者扫描后云端重建再下载。端侧重建把采集、重建、渲染放进同一台设备,用户拍到的就是拿到的。对设计来说,这意味着"内容供给"不再是瓶颈------产品可以先做互动,再补资产。

迭代周期从"天"压到"小时"

内容团队改一个展厅布局,2D 时代要重新出图、切图、发版。3DGS 场景可以整段替换,重建一次就能看效果。评审从"看图"变成"进场景走一遍",反馈更直接,返工更少。

数据不出设备

卧室、店铺内部、工厂车间这类空间,拍完上传到云端本身就涉及隐私与合规。端侧完成重建,原始采集数据可以只留在本地,重建结果也由用户决定是否分享。这不只是技术选项,是很多场景能不能做的前提。

把模型加载进场景

Spatial Recon Kit 的入口是 spatialRender,它依赖 @kit.ArkGraphics3D 的 Scene 与渲染上下文。顺序不能乱:先注册插件 ID,再加载场景,最后挂 GS 节点。

typescript 复制代码
// entry/src/main/ets/entryability/EntryAbility.ets
import { Scene, RenderContext, Node } from '@kit.ArkGraphics3D';
import { spatialRender } from '@kit.SpatialReconKit';

@Entry
@Component
struct GsViewer {
  private scene: Scene | null = null;

  aboutToAppear(): void {
    // 1. 拿到默认渲染上下文,后续插件注册都挂在它上面
    const renderContext: RenderContext | null = Scene.getDefaultRenderContext();
    if (renderContext === null) {
      console.error('no render context, abort');
      return;
    }
    // 2. 必须先注册 GSPlugin,否则调用 GSPlugin 的接口属于未定义行为
    renderContext.loadPlugin(spatialRender.GSPlugin.PLUGIN_ID);

    // 3. 加载场景,成功后把 3DGS 节点挂到根节点下
    Scene.load().then(async (scene: Scene) => {
      this.scene = scene;
      if (!scene.root) {
        return;
      }
      const root: Node = scene.root as Node;
      // uri 支持 OhosRawFile:// 与 file:// 两种前缀;格式支持 mp4 / ply / glb
      const uri = 'OhosRawFile://assets/gltf/room.glb';
      const gsNode: spatialRender.GSNode =
        await spatialRender.GSPlugin.loadGSNode(scene, { uri, offset: 0 }, root);
      // GSNode 继承自 Node,可直接做位移、缩放、可见性控制
      gsNode.position = { x: 0, y: -0.2, z: 0 };
      gsNode.scale = { x: 1, y: 1, z: 1 };
      gsNode.visible = true;
    }).catch((err: string) => {
      console.error('load 3dgs failed: ' + err);
    });
  }

  build() {
    Column() {
      if (this.scene) {
        // Component3D 负责把 Scene 渲染出来,尺寸交给布局
        Component3D({ scene: this.scene })
          .width('100%')
          .height('100%')
      }
    }.width('100%').height('100%')
  }
}

一个房间用 loadGSNode 就够了。但一栋楼、一个园区不行------几百 MB 的高斯点不可能一次性进内存。API 26 引入的分块 3DGS(Tiled 3D Gaussian Splatting)解决的就是这个:把场景按空间切成瓦片,渲染器在相机移动时按需请求当前视口附近的瓦片。

typescript 复制代码
// 大场景按需加载:相机移到哪,渲染器就请求哪一片
const params: spatialRender.TiledGSImportSettings = {
  uri: 'file:///data/storage/el2/base/files/campus/campus.scene.json' // 清单文件描述瓦片层级
};
const tiled: spatialRender.TiledGSNode =
  await spatialRender.GSPlugin.loadTiledGSNode(scene, params, root);

// 指定驱动分块选择的相机:相机的位置决定了要加载哪些瓦片
tiled.setCamera(camera);

// 渲染器需要数据时回调,应用负责把瓦片拉到本地再通知就绪
tiled.setTileRequestCallback((tiles: spatialRender.GSTile[]) => {
  for (const tile of tiles) {
    // 远端拉取 -> 落盘到 tile.uri 指向的相对路径 -> 通知渲染器可读
    void downloadTile(tile.uri).then(() => {
      tiled.notifyTileReady(tile);
    });
  }
});

分块加载把"一次性几百 MB"变成"首屏几 MB + 走到哪加载到哪"。设计上要多考虑一件事:瓦片加载是有延迟的,远景区域会先以低精度出现,再逐步清晰。这不是 bug,是设计要接受的渐进呈现。

设计机会不止"看模型"

刚上手容易把 3DGS 当成"更真实的模型预览"。真正有意思的是它上面叠的那一层交互。

  • 场景标注层:高斯点是死的、不可拾取的,手势和视线需要一个可命中的目标。做法是在场景里挂普通 Node 作为热点,用 Raycast 拾取这些 Node,而不是去点高斯点本身。
  • 虚拟陈设:真实房间重建后叠加 glTF 家具,用位移和缩放调整位置,做"摆放前先看效果"。
  • 空间导览:Tiled 场景配合相机路径,做展馆、厂区的走动式浏览。
  • 隐私敏感建模:卧室、试衣间、店铺内部,端侧完成,原始数据不外传。
  • 记录与回访:装修前拍一遍留底,之后随时进入对比。

约束在哪里,设计就得在哪里让步

约束 具体表现 设计对策
模型体量 单场景可达数百 MB 大场景走 Tiled 分块,小场景控制采集范围
渲染性能 同屏高斯点过多会掉帧 限制相机拉近距离,默认视距保持中景
加载延迟 瓦片按需拉取,远景先模糊 接受渐进呈现,用进度提示代替"转圈等待"
交互命中 高斯点不可直接拾取 叠加可拾取 Node 作为交互代理
设备支持 暂不支持模拟器,仅境内设备 降级方案要准备,开发期用真机验证
数据格式 MP4 / PLY / GLB 三种 按用途选:PLY 保真、MP4 体积小、GLB 便于混合管线

案例:家具摆放应用

设想一个"先看再买"的家居应用:用户拍一段客厅视频,端侧重建成 3DGS 场景,再从商品库选沙发、茶几拖进去,看比例、看风格,满意了直接下单。

流程拆开看是这样:
#mermaid-svg-aykylbs17rFoV2Bv{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-aykylbs17rFoV2Bv .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-aykylbs17rFoV2Bv .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-aykylbs17rFoV2Bv .error-icon{fill:#552222;}#mermaid-svg-aykylbs17rFoV2Bv .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-aykylbs17rFoV2Bv .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-aykylbs17rFoV2Bv .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-aykylbs17rFoV2Bv .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-aykylbs17rFoV2Bv .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-aykylbs17rFoV2Bv .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-aykylbs17rFoV2Bv .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-aykylbs17rFoV2Bv .marker{fill:#333333;stroke:#333333;}#mermaid-svg-aykylbs17rFoV2Bv .marker.cross{stroke:#333333;}#mermaid-svg-aykylbs17rFoV2Bv svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-aykylbs17rFoV2Bv p{margin:0;}#mermaid-svg-aykylbs17rFoV2Bv .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-aykylbs17rFoV2Bv .cluster-label text{fill:#333;}#mermaid-svg-aykylbs17rFoV2Bv .cluster-label span{color:#333;}#mermaid-svg-aykylbs17rFoV2Bv .cluster-label span p{background-color:transparent;}#mermaid-svg-aykylbs17rFoV2Bv .label text,#mermaid-svg-aykylbs17rFoV2Bv span{fill:#333;color:#333;}#mermaid-svg-aykylbs17rFoV2Bv .node rect,#mermaid-svg-aykylbs17rFoV2Bv .node circle,#mermaid-svg-aykylbs17rFoV2Bv .node ellipse,#mermaid-svg-aykylbs17rFoV2Bv .node polygon,#mermaid-svg-aykylbs17rFoV2Bv .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-aykylbs17rFoV2Bv .rough-node .label text,#mermaid-svg-aykylbs17rFoV2Bv .node .label text,#mermaid-svg-aykylbs17rFoV2Bv .image-shape .label,#mermaid-svg-aykylbs17rFoV2Bv .icon-shape .label{text-anchor:middle;}#mermaid-svg-aykylbs17rFoV2Bv .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-aykylbs17rFoV2Bv .rough-node .label,#mermaid-svg-aykylbs17rFoV2Bv .node .label,#mermaid-svg-aykylbs17rFoV2Bv .image-shape .label,#mermaid-svg-aykylbs17rFoV2Bv .icon-shape .label{text-align:center;}#mermaid-svg-aykylbs17rFoV2Bv .node.clickable{cursor:pointer;}#mermaid-svg-aykylbs17rFoV2Bv .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-aykylbs17rFoV2Bv .arrowheadPath{fill:#333333;}#mermaid-svg-aykylbs17rFoV2Bv .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-aykylbs17rFoV2Bv .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-aykylbs17rFoV2Bv .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-aykylbs17rFoV2Bv .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-aykylbs17rFoV2Bv .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-aykylbs17rFoV2Bv .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-aykylbs17rFoV2Bv .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-aykylbs17rFoV2Bv .cluster text{fill:#333;}#mermaid-svg-aykylbs17rFoV2Bv .cluster span{color:#333;}#mermaid-svg-aykylbs17rFoV2Bv div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-aykylbs17rFoV2Bv .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-aykylbs17rFoV2Bv rect.text{fill:none;stroke-width:0;}#mermaid-svg-aykylbs17rFoV2Bv .icon-shape,#mermaid-svg-aykylbs17rFoV2Bv .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-aykylbs17rFoV2Bv .icon-shape p,#mermaid-svg-aykylbs17rFoV2Bv .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-aykylbs17rFoV2Bv .icon-shape .label rect,#mermaid-svg-aykylbs17rFoV2Bv .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-aykylbs17rFoV2Bv .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-aykylbs17rFoV2Bv .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-aykylbs17rFoV2Bv :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 引导用户环绕拍摄
端侧重建 3DGS 场景
加载 GSNode 进入场景
按地面重建锚点平面
拖入 glTF 家具模型
Raycast 拾取 + 位移缩放
截图 / 录像分享

这个案例里,设计上最容易被忽略的是 D 这一步。重建出来的场景没有"地面"这个概念,是用户把家具放到半空中、还是贴到地上,全靠应用自己建立一个参考平面。常见做法是从点云的低处拟合一个水平面,或者让用户点两下地面手动定标。定标准不准,直接决定整个摆放体验可不可信。

第二个坑是拾取。用户觉得自己在点沙发,实际点的是空气------因为高斯点不参与拾取。所以每个家具都要挂一个不可见的包围盒 Node 作为代理,视觉上是 3DGS 家具,交互上命中的是代理盒。

几条小经验总结哦

  • 插件注册是硬前提。loadPlugin 漏掉或顺序反了,后面所有 GSPlugin 调用都可能拿到未定义行为,而且不一定报错,排查起来很费时间。
  • 采集质量直接决定上限。晃动、强反光、纯色墙面都是重建杀手,引导页要把"慢走、平稳、覆盖多角度"讲清楚。
  • GSNode 是普通 Node,位移缩放照常用,但别指望它像 glTF 那样按子节点拆开编辑。
  • 大场景一律先想 Tiled,不要等到内存爆了再改架构。
  • 模拟器跑不了这个 Kit,早点上真机,别把时间花在环境上。
  • uri 前缀写错。OhosRawFile:// 读的是 rawfile,file:// 读的是应用沙箱路径,两者不能混用。
  • 忘记判断 scene.root 是否为空,后续 as Node 会直接崩。
  • 把 Tiled 场景的相机忘了设置,渲染器只能默认用当前场景相机,分块选择可能不符合预期。
  • 在瓦片回调里同步做网络请求,阻塞渲染线程;应该异步拉取、落盘后再 notifyTileReady。
  • 采集数据和重建结果都放沙箱,没做清理策略,用几次就把用户存储占满。
相关推荐
传奇开心果编程1 小时前
【ArkUI进阶练中学】第5课:编译优化与包体积进
学习·ui·华为·harmonyos
m0_738185821 小时前
Flutter 鸿蒙化实战:flutter_quill 适配 OpenHarmony,富文本编辑器
flutter·华为·harmonyos·鸿蒙
I Am a robert girl1 小时前
从频率到几何:GA-EIRFS 如何重新思考长尾 3D 检测的采样逻辑
3d·3d目标检测·nuscenes·lidar·数据采样·长尾分布·类别不平衡
m0_738185821 小时前
Flutter 鸿蒙化实战:flutter_quick_video_encoder 适配 OpenHarmony,逐帧编码视频
flutter·华为·音视频·harmonyos·鸿蒙
HwJack202 小时前
【共创稿事节】HarmonyOS 7手势识别交互:空中手势的语义与容错设计
人工智能·深度学习·华为·交互·harmonyos
传奇开心果编程2 小时前
【ArkUI进阶练中学】第10课:架构设计与工程化最佳实践
学习·ui·华为·harmonyos
李游Leo3 小时前
HarmonyOS 7 + Node.js-JSON Schema:审核测试路径与版本事实的一致性门禁【鸿蒙心迹】
node.js·json·harmonyos
传奇开心果编程3 小时前
【ArkUI提高练中学】第7课:性能调优与稳定性治理
学习·ui·华为·harmonyos
李游Leo16 小时前
HarmonyOS 7 + Spatial Recon Kit-Core File Kit:3DGS 重建产物的原子发布与中断恢复【鸿蒙心迹】
3d·华为·harmonyos