【共创稿事节】HarmonyOS ArkGraphics 3D实操——做一个可暂停、可拖动的音箱场景动画

这一次不再只旋转模型,而是让智能音箱自己完成一次装配:底座先归位,网罩和机壳随后从不同空间方向回到中心,灯环与顶盖最后合上。当前正在运动的零件会被薄荷色高亮,拖动时间线可以停在任意装配阶段,再上下左右环绕检查空间关系。

暂停在 43% 时,画面保留机壳、网罩和底座,顶盖与灯环仍在待装配位置;点击播放后继续向 100% 归位。拖动时间线到中段会记录 SCENE_ANIM_SEEK progress=0.490000,节点立即跳到对应装配状态。

先看装配过程

页面把结构模型的五个节点映射到一条时间轴。播放时,各节点按照"底座、网罩、机壳、灯环、顶盖"的顺序使用平滑插值回到原位;当前阶段节点通过 Geometry.mesh.materialOverride 套用薄荷色材质,读者可以直接判断哪一件正在装配。暂停不会清除当前姿态,重播会从0%重新开始。

这段动画真正展示的是三个 3D 能力:第一,零件不是贴在画面上的平面,而是带有 X/Y/Z 坐标的独立空间节点;第二,相机可以在装配过程中上下左右环绕,观察零件之间的前后遮挡和间隙;第三,材质覆盖会随当前阶段改变,让正在装配的零件从整机中被识别出来。把时间线停在 42% 时,可以看到机壳已经靠近中心,底座和网罩仍然保持分层,灯环与顶盖还在上方等待归位。

如果只看完整态,所有零件会重新重合,动画价值很难被看出来。阅读这篇文章时建议先看 GIF 的爆炸态,再看暂停截图,最后拖到 100% 对比完整态。

一、节点动画怎么来的

案例复用前文的本地 orbitdesk_structure.glb,只改变节点的 position,不复制几何体和材质。每个零件保存一条初始偏移量:

复制代码
const partNames: string[] = [
  'MetalBaseLower', 'SpeakerGrille', 'SpeakerBody',
  'TopRing', 'TopInset'
];
const xOffsets: number[] = [-0.48, 0.58, -0.42, 0.64, -0.52];
const yOffsets: number[] = [-1.15, -0.55, 0, 0.55, 1.10];
const zOffsets: number[] = [0.42, -0.26, 0.34, -0.32, 0.26];
const nodes: Node[] = [];
const baseX: number[] = [];
const baseY: number[] = [];
const baseZ: number[] = [];

for (const name of partNames) {
  const node = find(scene.root, name);
  if (node) {
    nodes.push(node);
    baseX.push(node.position.x);
    baseY.push(node.position.y);
    baseZ.push(node.position.z);
  }
}

baseX/baseY/baseZ 是装配完成后的坐标,三组 offset 是爆炸状态的空间偏移。这样同一个节点既能被拆开,也能准确回到原来的位置。网罩不是一张 2D 图片,五个部件都是真实 GLB 节点。

复制代码
const phase = Math.max(0, Math.min(1, progress * 1.32 - index * 0.11));
const eased = phase * phase * (3 - 2 * phase);
node.position = {
  x: baseX[index] + xOffsets[index] * (1 - eased),
  y: baseY[index] + yOffsets[index] * (1 - eased),
  z: baseZ[index] + zOffsets[index] * (1 - eased)
};

progress 从0到1变化,offset 决定节点从哪里开始,smoothstep 让开始和结束都更柔和。返工后的每个零件同时拥有 X/Y/Z 偏移,例如网罩从侧后方回到中心,顶盖从上方回位。动画每一帧都根据初始状态重新计算,不累加位移,因此拖动时间线后不会产生漂移。

3. 用材质覆盖告诉读者当前装到哪一件

只移动零件仍然容易看漏当前阶段,所以页面为当前节点临时套用一套薄荷色材质:

复制代码
if (node.nodeType === NodeType.GEOMETRY) {
  const geometry = node as Geometry;
  geometry.mesh.materialOverride = index === currentPart
    ? assemblyHighlight
    : undefined;
}

这个覆盖只改变当前节点的显示材质,GLB 原本的网孔、灯环和金属材质仍然保留。动画播放时,标题、阶段文字和模型高亮指向同一个零件。

4. 相机也参与动画观察

手指拖动改变的是相机球面位置,而不是整体旋转模型:

复制代码
const yaw = this.yaw * Math.PI / 180;
const pitch = this.pitch * Math.PI / 180;
const radius = 7.8;
camera.position = {
  x: radius * Math.cos(pitch) * Math.sin(yaw),
  y: 0.55 + radius * Math.sin(pitch),
  z: radius * Math.cos(pitch) * Math.cos(yaw)
};

因此可以从正面、侧面和俯视角度观察零件之间的前后关系;模型本身不需要被整体旋转,也不会因为父节点旋转造成网格错位。

二、播放、暂停与时间线

播放按钮只切换 playing 状态,定时器根据时间差推进进度;暂停时保留进度和节点位置。速度按钮改变推进系数,时间线改变进度并立即调用同一套插值函数。

5. 播放循环只负责推进时间

复制代码
if (this.playing) {
  this.progress = Math.min(1, this.progress + dt * this.speed / 4);
  this.apply();
}

在实际页面中,dt 来自两次定时器回调之间的秒数,避免把设备刷新频率写死。apply() 负责把当前进度转换成五个节点的新坐标;因此播放、拖动和重播共享同一条空间计算路径。

6. 时间线拖动是一次可回看的空间取样

复制代码
Slider({ value: this.progress, min: 0, max: 1, step: 0.01 })
  .onChange((value: number, mode: SliderChangeMode) => {
    this.playing = false;
    this.progress = value;
    this.current = Math.min(4, Math.floor(value * 5));
    this.apply();
    if (mode === SliderChangeMode.End) {
      hilog.info(0, 'AssemblyAnimation',
        'SCENE_ANIM_SEEK progress=%{public}d', value);
    }
  });

例如拖到 49%,模型会停在机壳阶段;此时相机仍然可以环绕,读者能从侧面看出网罩和机壳的前后间距。它不是把 GIF 快进到某一帧,而是重新计算真实节点的三维姿态。

在手机上可以看到五个零件从分离状态逐步回到装配位置;暂停后当前空间关系保持不变,恢复播放才继续移动。实际运行还覆盖了时间线跳到 49% 的中段状态和退出释放。GIF来自真实设备页面采帧,用来展示状态变化,不代表渲染帧率。

三、为什么要单独处理退出

页面退出时先停止定时器,再释放场景引用;加载代次递增后,尚未完成的旧加载结果会被销毁。这样快速返回首页、重复进入和重复释放都不会把旧场景带回页面。

复制代码
this.generation++;
this.playing = false;
if (this.timer !== -1) {
  clearInterval(this.timer);
  this.timer = -1;
}
this.scene?.destroy();
this.scene = undefined;

四、复现步骤

从"ArkGraphics 3D 实验空间"进入"装配动画实验室"。先点击"重播"回到爆炸态,再点击"播放";当机壳高亮、标题显示约 40% 时点击"暂停",观察零件之间的空间间隙。随后点击"播放"继续完成装配,拖动时间线可以直接跳到任意阶段,点击"1.5x"可以加快预览。案例使用本地模型,不需要网络和相机权限。

你可以按"爆炸态 → 中段暂停 → 完整态"的顺序阅读结果:爆炸态用于看清零件层级,中段用于看变换和高亮,完整态用于确认所有节点回到同一套装配坐标。

相关推荐
anthonyzhu1 小时前
MacOS27 x86限制引发的pod的问题处理
华为·harmonyos
贾伟康1 小时前
【HarmonyOS 7新能力|030】沉浸光感工程封装:把接入逻辑放进可维护的分层结构
harmonyos·arkts·arkui·harmonyos 7·交互动效
万物智能信息科技1 小时前
GPIO控制状态灯—【万物智能之开源鸿蒙OpenHarmony系统实战开发系列教程】
人工智能·华为·开源·harmonyos·鸿蒙
威哥爱编程1 小时前
HarmonyOS 7 平行视界 EasyGo 实战:配置文件接入应用内分屏,1:2/2:1 随意切
harmonyos·arkts
威哥爱编程1 小时前
HarmonyOS 7 空间重建实战:3DGS 端侧重建从建模到商品展示
harmonyos·arkts
jiao000012 小时前
【鸿蒙实战】从零撸一个「华为商城」App
华为·harmonyos·arkts
李游Leo10 小时前
HarmonyOS 7 端侧 AI 视觉能力实战 04:给图片增加人脸检测能力
harmonyos
ChinaDragon11 小时前
HarmonyOS:应用程序包管理模块(获取当前应用信息)
harmonyos
Latte Moments开发13 小时前
Harmony鸿蒙实战开发-事件提醒app「事件名称/描述、提醒日期、提醒时长、延迟提醒次数、延迟提醒事件间隔」➕⏰声音【源码在文末】
华为·harmonyos