动图魔方技术拆解 18:ArkGraphics3D 预览、能力检测与 3DGS 降级路线

SEO 信息

  • SEO 标题:动图魔方技术拆解 18:ArkGraphics3D 预览、能力检测与 3DGS 降级路线
  • SEO 摘要 :基于 HarmonyOS NEXT / ArkTS 项目"动图魔方",拆解 GIF 工具如何把 Model3DView.etsCapabilityService.etsRecon3DService.etsExportService.ets 串成可交付的 3D 路线:当前用 ArkGraphics3D 做真实模型预览,用 deviceInfo.sdkApiVersion 判断设备能力,并在 3DGS 未接入时稳定回退到合成导出。
  • 关键词:HarmonyOS, ArkTS, ArkGraphics3D, Component3D, CapabilityService, Recon3DService, 3DGS, Spatial Recon Kit, 能力检测, 降级路线
  • 文章封面doc/csdn-series/covers/cover-18-arkgraphics3d-capability-route.jpg
  • 投稿方向:普通技术拆解 / HarmonyOS 本地 3D 预览与降级工程
  • 项目环境 :HarmonyOS SDK 6.1.0(23)、ArkTS、DevEco Studio、GIFRubiksCube
  • 验证时间2026-06-27
  • 验证对象entry/src/main/ets/features/threeD/components/Model3DView.etsentry/src/main/ets/features/threeD/services/CapabilityService.etsentry/src/main/ets/features/threeD/services/Recon3DService.etsentry/src/main/ets/features/gif/services/ExportService.etsentry/src/main/ets/products/main/Index.etsentry/src/main/ets/entryability/EntryAbility.etsentry/src/main/resources/base/profile/main_pages.json

第 05 篇讨论的是"为什么一个 GIF 工具要给 HarmonyOS 7.0 的 3D 能力预埋结构",第 18 篇则把镜头拉回当前工程本身:既然项目还在 6.1.0(23),又不想在页面里夸大"真实 3DGS 已可用",那就必须把预览、能力判断、导出分流和失败回退做成一条可复验的工程链路。否则 3D 入口只会变成演示文案,而不是可以交付的产品能力。

一、真实工程问题背景

在工具类 App 里,"支持 3D"最容易被写成一句很虚的话:页面上放一个 3D 卡片、写两句未来能力说明、再放一个实验标签,看起来像已经有路线,实际上用户和测试都无法判断当前到底支持到哪一步。

"动图魔方"里这类风险主要体现在四个层面:

  1. 现象 :编辑页已经能看到旋转模型,团队就容易口头说成"3D 已经支持了"。
    原因 :把 ArkGraphics3D 实时渲染和 3DGS 重建混写成同一个能力名词。
    影响 :测试会误以为当前版本已经具备"从单图/视频到真实 3D 场景"的完整能力。
    验证路径 :必须把 Model3DView 的实时渲染、CapabilityService.route 的能力分层、Recon3DService 的占位状态同时拿出来核对。
  2. 现象 :设备 API 版本足够高,但当前安装包仍然只能走合成路线。
    原因 :设备支持和构建支持是两套条件,当前包未必真的带了 3DGS 执行体。
    影响 :如果 UI 只看 sdkApiVersion 开入口,就会出现"设备支持,但包不支持"的伪成功。
    验证路径 :同时检查 deviceSupportsReconbuildSupportsRecon 和最终 route
  3. 现象 :3DGS 执行体尚未完成时,3D 导出仍必须能继续工作。
    原因 :GIF 工具主线是可导出、可回看、可分享,不能让高阶实验能力拖垮当前版本。
    影响 :如果没有合成降级路线,3D 模式会变成只能看不能导的半成品。
    验证路径 :检查 ExportService.buildFromSynthetic() 是否在 Recon3DService 不可用时自动回退到 FrameProcessor.buildSyntheticGifFrames()
  4. 现象 :页面文案和真实能力边界不一致。
    原因 :功能入口、能力卡、导出提示没有围绕同一套路由结果收口。
    影响 :运营、测试和用户会拿着互相冲突的口径做判断。
    验证路径 :把 phase: '实验'真实3DGS:预留 和导出区提示文案逐项映射到源码。

所以第 18 篇要解决的不是"怎么把 3D 做炫",而是一个更朴素的问题:

如何在 6.1 可交付版本里,给 3D 预览和未来 3DGS 留出正式入口,同时保证当前用户拿到的是可运行、可解释、可降级的结果。

这个问题表面上是在讲 3D,实质上是在讲交付口径。只要入口说明、能力判断和导出路径三者中有一个说不清,测试结论就会摇摆,发布说明也会跟着失真。

换句话说,第 18 篇不是在证明"我们已经完成了 3DGS",而是在证明"当前版本把能做的、不能做的、未来准备怎么接,都写成了可核对的工程结构"。

二、目标与边界

这篇文章的目标很明确:

  1. 说明 Model3DView 当前已经落地的是哪种真实能力。
  2. 说明 CapabilityService 如何把设备能力和构建能力拆开判断。
  3. 说明 Recon3DService 为什么必须作为正式接入点存在,即便当前仍是占位实现。
  4. 说明 ExportService 怎样把 3dgs / render / synthetic 三条路线统一收口。
  5. 说明页面文案怎样避免把"实验预览"误写成"已完成重建"。

边界同样需要讲清楚:

  1. 本文不重复第 17 篇已经写过的"真实素材验证闭环",只聚焦 3D 路线。
  2. 本文不把 ArkGraphics3D 预览说成 3DGS 重建结果,两者在能力层级上不同。
  3. 本文里的 3D 导出当前仍以图片合成转动动效为主,不虚构尚未确认的 API 26 重建接口。
  4. 本文只引用项目现有代码和官方文档,不编造"未来可用"的 SDK 调用签名。

这组边界先写清楚,后面的代码证据才不会被误读成营销文案。尤其是 3D 相关话题,最容易出现的偏差就是把"预览已经跑起来"误写成"重建能力已经接入完成"。

对读者来说,边界越明确,后面每一段代码就越容易看懂。因为大家会知道本文到底在验证什么,也知道哪些能力仍然只是正式预留而非当前可用。

三、源码对象与官方依据

这一篇实际对应的源码对象如下:

  1. entry/src/main/ets/features/threeD/components/Model3DView.ets
  2. entry/src/main/ets/features/threeD/services/CapabilityService.ets
  3. entry/src/main/ets/features/threeD/services/Recon3DService.ets
  4. entry/src/main/ets/features/gif/services/ExportService.ets
  5. entry/src/main/ets/products/main/Index.ets
  6. entry/src/main/ets/entryability/EntryAbility.ets
  7. entry/src/main/resources/base/profile/main_pages.json
  8. doc/screenshots_current/gifrubik_3d.jpeg
  9. doc/screenshots_current/gifrubik_discover_expanded.jpeg
  10. doc/screenshots_current/gifrubik_editor.jpeg
  11. doc/screenshots_current/gifrubik_editor_export_blocked.jpeg
  12. doc/真实3D渲染落地记录.md

这些对象分别负责不同层级的事情:

对象 责任 为什么是第 18 篇主角
Model3DView.ets 加载 glTF、初始化相机、驱动模型自转、实时渲染或失败回退 证明项目当前已经有真实 3D 预览,不只是文案占位
CapabilityService.ets 判断 sdkApiVersion、构建能力与路由结果 决定 3D 路线到底走 3dgsrender 还是 synthetic
Recon3DService.ets 为 API 26+ 的真实 3DGS 重建保留接入口 决定"未来能力"是否有正式挂点,而不是散落在页面判断里
ExportService.ets 按路由分发 3D 导出实现,并在失败时自动回退 决定当前产品是否可交付
Index.ets 承接页面文案、能力卡、导出提示和用户可见状态 决定用户看到的能力表述是否准确
EntryAbility.ets 通过 windowStage.loadContent('products/main/Index') 装载主页面 证明第 18 篇分析的是正式产品入口,而不是独立 demo
main_pages.json 声明主页面资源索引包含 products/main/Index 证明 3D 路线已经进入正式页面路由,而不是临时调试页

把入口层也纳入证据链,能补上一块很容易被忽略的拼图。很多文章会直接从功能页开始讲,但真正决定"这是不是项目主线"的,是入口和页面注册是否已经接到正式产品结构里。

这里补上 EntryAbility.etsmain_pages.json 之后,文章证据链就从"功能实现"延伸到了"应用入口"。这对解释 3D 路线是否真的进入交付面非常关键。

对应的官方依据建议同时核对:

  1. ArkGraphics 3D 场景构建与 Component3D 渲染说明
  2. deviceInfo.sdkApiVersion 兼容性检查说明
  3. @ohos.deviceInfo 接口参考
  4. Spatial Recon Kit 简介
  5. 加载 3DGS 模型能力说明
  6. spatialRender 参考

这里我做一个明确判断:

根据官方文档,ArkGraphics3D 实时场景渲染Spatial Recon / 3DGS 数据加载或重建属于不同能力层次。项目现在已经真实接入的是前者;后者在当前工程里是"有正式接入口但未在本构建里启用"的状态。

四、先把"真实 3D 预览"定义清楚,避免和 3DGS 混写

第 18 篇首先要做的一件事,就是把"当前到底实现了什么"讲清楚。

Model3DView.ets 不是在做伪 3D 图层平移,而是直接使用 @kit.ArkGraphics3D

复制代码
import { Scene, SceneResourceFactory, Camera, Node, Quaternion } from '@kit.ArkGraphics3D';

@Component
export struct Model3DView {
  @Prop viewSize: number = 220;
  @State sceneOpt: SceneOptions | null = null;
  @State failed: boolean = false;
  private scene: Scene | null = null;
  private node: Node | null = null;

组件初始化主线也很直接:

复制代码
Scene.load($rawfile('model3d/cube.gltf'))
  .then(async (result: Scene) => {
    this.scene = result;
    const rf: SceneResourceFactory = this.scene.getResourceFactory();
    const cam: Camera = await rf.createCamera({ name: 'GifRubikCam' });
    cam.enabled = true;
    cam.position.z = 3.2;
    this.node = this.scene.getNodeByPath('CubeRoot');
    const opt: SceneOptions = { scene: this.scene, modelType: ModelType.SURFACE };
    this.sceneOpt = opt;
    this.startSpin();
  })

这里至少能说明三件事情:

  1. 项目当前已经在加载真实 glTF 模型,而不是把 3D 预览写成静态示意图。
  2. 预览区里的旋转来自场景节点四元数更新,不是把一张图片做左右位移假装 3D。
  3. 当前落地的是真实 3D 模型渲染预览,不是"从单图或视频重建 3DGS 场景"。

这一段的工程价值,在于它把"渲染"这个词钉死在真实场景对象上。只要 Scene.loadCameraComponent3D 同时出现,就能判断这不是简单的位移动画或伪 3D 过场。

这也解释了为什么第 18 篇必须单独写。因为一旦团队内部把这一步叫成"3D 已经打通",后面的能力验收就会从第一天开始跑偏。

自转逻辑同样是证据:

复制代码
private startSpin(): void {
  this.timerId = setInterval(() => {
    if (this.node === null) {
      return;
    }
    this.angle += 0.03;
    const half = this.angle / 2;
    const rot: Quaternion = { x: 0, y: Math.sin(half), z: 0, w: Math.cos(half) };
    this.node.rotation = rot;
  }, 32);
}

这段代码非常适合拿来解释"为什么第 18 篇和第 05 篇不同":第 05 篇强调 7.0 路线预埋;第 18 篇强调当前代码里已经有一个真实 3D 渲染执行体,而且它的职责边界明确。

五、能力检测不能只看设备,还要看当前构建是否真的带上执行体

很多团队写"能力检测"时,只做一层 sdkApiVersion >= X 判断,然后在页面里直接开入口。这样最容易出现的问题是:

  • 设备看起来支持。
  • 页面文案也写成支持。
  • 但当前安装包根本没带对应实现。

项目里这一层被 CapabilityService.ets 拆开了:

复制代码
const API_ARKGRAPHICS3D = 12;
const API_3DGS_RECON = 26;

const deviceSupportsRecon = sdk >= API_3DGS_RECON;
const buildSupportsRecon = Recon3DService.isBuildSupported();
const supportRecon = deviceSupportsRecon && buildSupportsRecon;
const supportRender = sdk >= API_ARKGRAPHICS3D;

let route = 'synthetic';
if (supportRecon) {
  route = '3dgs';
} else if (supportRender) {
  route = 'render';
}

这一层设计的价值非常高:

  1. 设备支持构建支持被拆成两套条件,不再混为一谈。
  2. 真正给下游用的不是一堆零散布尔值,而是收口成 route
  3. UI、导出服务和后续 3DGS 接入都只需要消费统一路由结果。

更关键的是,这里把用户能看到的解释文案也一起收口了:

复制代码
if (supportRecon) {
  message = `检测到 API ${sdk}:启用真实 3DGS 端侧重建路线。`;
} else if (deviceSupportsRecon && !buildSupportsRecon) {
  message = `设备支持 3DGS(API ${sdk}),但当前构建尚未接入 3DGS 重建模块(Recon3DService);接入后即启用真实重建。当前走实时渲染 + 合成导出。`;
} else if (supportRender) {
  message = `检测到 API ${sdk}:支持 ArkGraphics3D 实时渲染;3DGS 重建需 API 26+。当前走实时渲染 + 合成导出。`;
} else {
  message = `当前设备 API ${sdk} 较低:走单图合成伪 3D 路线。`;
}

这比只在页面上写一个"实验功能"强太多,因为它把下面四种状态分开了:

  1. 真正可走 3dgs
  2. 设备条件到了,但当前构建还没带 Recon3DService 执行体。
  3. 只支持 ArkGraphics3D 预览。
  4. 连实时渲染都不支持,只能走合成。

从发布和测试角度看,这种拆分还有一个直接收益:问题单可以落到明确责任层。是设备不支持,还是 SDK 构建没带上执行体,还是仅支持预览不支持重建,都能从同一套路由结果里看出来。

如果没有这层路由收口,页面上出现的每一条提示文案都可能变成新的分叉逻辑。短期看似灵活,长期会让 3D 路线越来越难维护。

这四种状态如果不拆,后续的测试、运营文案、用户反馈都会乱。

六、Recon3DService 的意义不是"现在就能跑",而是让未来能力有正式接入口

第 18 篇最容易被误解的一点是:既然 Recon3DService 当前还是占位,为什么还值得单独写?

原因很简单:如果这里没有正式接入口,未来 3DGS 接入只会变成一堆散落在页面和导出服务里的临时判断。

项目里的 Recon3DService.ets 已经把这层结构搭出来了:

复制代码
export const BUILD_API_SUPPORTS_3DGS = false;

export class Recon3DService {
  static isBuildSupported(): boolean {
    return BUILD_API_SUPPORTS_3DGS;
  }

  static async reconstruct(uri: string, frameCount: number, delayCs: number, signal: ExportSignal): Promise<GifFrameBuildResult> {
    throw new Error('3DGS reconstruction not available in this build (requires API 26 SDK)');
  }
}

它现在虽然不执行真实重建,但已经提前固定了三件重要事情:

  1. 构建层开关在哪里控制。
  2. 真实重建服务未来以什么函数入口被调用。
  3. 返回结果 必须收口到 GifFrameBuildResult,复用下游量化和编码链路。

这类"先把空接口立住"的做法,真正解决的不是今天能不能跑,而是明天升级时改动面会不会失控。只要返回结果不变,下游导出链路就不用陪着一起重写。

对于工具类 App 来说,这比"先做一个能演示的实验接口"更靠谱。因为演示版本最怕的不是不能展示,而是后面接正式实现时把已有主链路拆散。

这就是标准工程思路:

先把接口位置、返回结构和回退规则固定,再等官方 API 或构建条件成熟时补执行体。这样升级到 API 26 时,动的是一个服务文件,而不是整条产品主流程。

七、导出分流必须可交付:3D 路线不成熟,也不能拖垮 GIF 主链路

如果 3D 路线只有预览,没有导出兜底,那它仍然只是实验页,不算真实功能。

ExportService.ets 里这部分做得很像一个真正可交付的产品路由器:

复制代码
if (preset.editorType === 'threeD') {
  return await ExportService.buildFromSynthetic(preset, 'rotate3d', signal);
}
if (preset.editorType === 'depth') {
  return await ExportService.buildFromSynthetic(preset, 'parallax', signal);
}

再往下看,rotate3d 的分流逻辑已经被能力路由接管:

复制代码
if (mode === 'rotate3d' && CapabilityService.detect3D().route === '3dgs' && Recon3DService.isBuildSupported()) {
  try {
    const reconResult = await Recon3DService.reconstruct(preset.sourceUris[0], frameCount, delayCs, signal);
    return await ExportService.encodeResult(reconResult, preset);
  } catch (err) {
    // 3DGS 不可用时回退合成路线
  }
}

const result = await FrameProcessor.buildSyntheticGifFrames(
  preset.sourceUris[0],
  frameCount,
  delayCs,
  mode,
  ExportService.editOptions(preset),
  signal
);

这段代码真正体现了第 18 篇的主题:

  1. 3dgs 不是一个页面概念,而是导出服务里的正式路线。
  2. 即便未来走进了 Recon3DService,最后仍然回到统一的 encodeResult() 下游。
  3. 只要 3DGS 当前不可用,就自动回退到合成路线,不让整个导出能力失效。

也就是说,项目没有因为"真实 3DGS 还没接完"就把 3D 功能整体锁死,而是先交付实时渲染预览 + 合成导出这条当前可稳定跑通的主线。

这就是第 18 篇最核心的取舍。当前版本不追求把未来能力一次性讲满,而是先保证用户今天打开页面、预览模型、点击导出时,拿到的都是一致且可解释的结果。

从工程治理角度看,这种取舍也更利于持续演进。以后无论接的是 Spatial Recon、3DGS 还是别的重建执行体,都可以在既有导出入口上增量替换,而不是推翻整条产品链路。

八、页面文案必须跟真实能力一致,不能把实验入口包装成完成态

如果代码里已经做了路由和降级,但页面仍写成"真实 3D 已完成",用户照样会被误导。

项目当前在 Index.ets 里把这个边界写得比较克制:

复制代码
{ id: 'threeD', title: '3D转动图', desc: '当前为图片合成转动动效', badge: '3D', tint: '#5C8DFF', phase: '实验' },
{ id: 'depth', title: '单图浅3D', desc: '本地视差合成动效', badge: '浅3D', tint: '#FF9B45', phase: '实验' }

设备卡里也没有故意夸大:

复制代码
Text(`当前路线:${this.routeLabel()} · 真实3DGS:${this.support3DRecon ? '可用' : '预留'}`)

导出区提示更直接:

复制代码
Text(
  this.route3D === '3dgs'
    ? '提示:检测到支持,将走真实 3DGS 端侧重建导出'
    : '提示:当前导出走图片合成转动动效;视频转真实 3D 需要 3DGS/NeRF 等重建能力。'
)

这类文案的工程意义很大:

  1. 用户知道当前拿到的是哪种结果。
  2. 测试知道当前验收口径是什么。
  3. 未来升级到 API 26 时,只需要改路由结果和执行体,不用推翻整套交互结构。

很多项目的问题不是没有降级,而是降级只存在于代码里,用户看不到,测试也看不到。等到有人拿着页面截图来问"为什么这里没有真 3D 导出"时,团队才发现对外口径根本没对齐。

把这层解释提前写进页面,本质上是在降低沟通成本。它让 3D 路线从"开发自己知道的内部事实",变成"用户和测试都能看到的公开规则"。

九、命中结果:先把源码定位、命令输出和真实行号钉住

为了避免这篇文章继续停留在"概念上说得通",我先把本地命令命中的关键结果列出来。实际定位时,主要用了下面这组命令:

复制代码
rg -n "Model3DView|CapabilityService|Recon3DService|buildFromSynthetic|route3D|真实3DGS|ArkGraphics3D" entry/src/main/ets -S

Get-ChildItem doc/screenshots_current -File |
  Where-Object { $_.Name -match "3d|discover_expanded|editor_export_blocked" } |
  Select-Object Name, Length

命中的关键结果如下:

复制代码
entry/src/main/ets/products/main/Index.ets:22:  { id: 'threeD', title: '3D转动图', desc: '当前为图片合成转动动效', badge: '3D', tint: '#5C8DFF', phase: '实验' },
entry/src/main/ets/products/main/Index.ets:1121: Text(`当前路线:${this.routeLabel()} · 真实3DGS:${this.support3DRecon ? '可用' : '预留'}`)
entry/src/main/ets/products/main/Index.ets:1142: Text(this.route3D === '3dgs' ? '提示:检测到支持,将走真实 3DGS 端侧重建导出' : '提示:当前导出走图片合成转动动效;视频转真实 3D 需要 3DGS/NeRF 等重建能力。')
entry/src/main/ets/features/threeD/components/Model3DView.ets:34:      Scene.load($rawfile('model3d/cube.gltf'))
entry/src/main/ets/features/threeD/components/Model3DView.ets:73:        Component3D(this.sceneOpt)
entry/src/main/ets/features/threeD/services/CapabilityService.ets:26:    const deviceSupportsRecon = sdk >= API_3DGS_RECON;
entry/src/main/ets/features/threeD/services/CapabilityService.ets:27:    const buildSupportsRecon = Recon3DService.isBuildSupported();
entry/src/main/ets/features/threeD/services/CapabilityService.ets:31:    let route = 'synthetic';
entry/src/main/ets/features/threeD/services/CapabilityService.ets:35:      route = 'render';
entry/src/main/ets/features/threeD/services/Recon3DService.ets:17:export const BUILD_API_SUPPORTS_3DGS = false;

doc/screenshots_current/gifrubik_3d.jpeg
doc/screenshots_current/gifrubik_discover_expanded.jpeg
doc/screenshots_current/gifrubik_editor.jpeg
doc/screenshots_current/gifrubik_editor_export_blocked.jpeg

这些命中结果把第 18 篇真正要讲的四个层级串起来了:

  1. 入口文案已经把 3D 标成实验态。
  2. 页面能力卡已经展示"路线"和"真实3DGS 是否可用"。
  3. 预览层已经真实落地 Scene.load + Component3D
  4. 服务层已经把 BUILD_API_SUPPORTS_3DGS 固定成一个可升级、可回退的接入口。

十、截图与证据:要证明"实时渲染、能力检测、降级提示"同时存在

第 18 篇不能只贴代码,因为 3D 路线最大的风险恰恰来自"页面说一套,底层做一套"。

这次我把截图和源码规则一一对应起来:

截图 需要证明的规则 对应源码位置
gifrubik_discover_expanded.jpeg 发现页把 3D 路线标成实验能力,而不是主线已完成功能 Index.ets:22-231333-1341
gifrubik_editor.jpeg 编辑器存在统一的 3D 入口,说明 3D 路线已经进入实际工作台结构 Index.ets:383-404
gifrubik_3d.jpeg 3D 编辑页同时展示预览、参数和说明,证明这不是独立 Demo 页面 Index.ets:888-9141118-1145
gifrubik_editor_export_blocked.jpeg 导出区提示当前走合成路线,说明页面文案遵守实际路由结果 Index.ets:1141-1145

10.1 发现页必须把 3D 标成实验路线,而不是完成态

这张图对应的是:

复制代码
{ id: 'threeD', title: '3D转动图', desc: '当前为图片合成转动动效', badge: '3D', tint: '#5C8DFF', phase: '实验' }

它证明项目没有把 3D 功能包装成"当前已完整闭环",而是明确告诉用户:当前仍是实验路线。

10.2 编辑器主入口必须把 3D 路线纳入统一工作台

这张图的意义在于证明 3D 并不是一个游离在工程外的独立样例,而是和视频转 GIF、图片拼 GIF 一起进入了同一套编辑页状态机。

10.3 3D 编辑页要同时承接预览、能力卡和导出提示

这张图至少可以验证四件事:

  1. 3D 模式是独立编辑入口,而不是被塞进普通 GIF 模式的隐藏分支。
  2. 页面上明确区分了 3D 路线、导出参数和能力提示,不是只给一个模糊入口。
  3. 用户看到的不是"已完成 3DGS 重建",而是当前工程下真实可交付的 3D 预览与导出说明。
  4. 预览区和导出区被放在同一条工作链上,说明这不是单独的图形实验页。

10.4 导出区文案必须告诉用户"当前为什么走合成"

这张图最重要的价值不是"页面看起来可导出",而是它能证明:

  1. 导出提示会跟着当前路由一起变化。
  2. 当前版本没有把"实时预览可用"偷换成"真实 3DGS 可导出"。
  3. 降级路线是用户可见、测试可见、验收可见的,不是藏在代码注释里。

十一、回归命令与验收清单

我这次定位和校验第 18 篇主线时,主要用了下面几类命令:

复制代码
rg -n "Model3DView|CapabilityService|Recon3DService|buildFromSynthetic|route3D|真实3DGS|ArkGraphics3D" entry doc -S

Get-Content entry/src/main/ets/features/threeD/components/Model3DView.ets
Get-Content entry/src/main/ets/features/threeD/services/CapabilityService.ets
Get-Content entry/src/main/ets/features/threeD/services/Recon3DService.ets
Get-Content entry/src/main/ets/features/gif/services/ExportService.ets

node tools/check_csdn_article_quality.js 18

本地质检第一轮的结果也值得保留下来,因为它正好反向证明第 18 篇应该补哪些证据:

复制代码
Article: doc\csdn-series\18-ArkGraphics3D预览能力检测与3DGS降级路线.md
Score: 76/100
Pass: NO (target >= 90)

当时主要缺的是:工程问题不够具体、截图与源码映射不足、命令与行号证据不够硬。也正因为如此,第 18 篇后续才会专门补上"命中结果"和"四张截图映射"这两层。

把第一次 76 分的结果放在这里,不是为了展示分数,而是为了说明这篇稿件是怎么从"概念介绍"被逼回"工程证据"的。这个修稿过程本身,就是系列文章里很值得保留的方法论。

如果一篇技术拆解连自己补过哪些证据都说不清,那它最后就算能发出去,也很难在系列里形成稳定复用的方法。

这篇的工程验收清单建议按下面的口径看:

验收项 结果 证据
当前工程已接入 ArkGraphics3D 实时模型预览 通过 Model3DView.etsScene.load + Component3D
3DGS 与 ArkGraphics3D 预览被明确区分 通过 CapabilityService.etsroutemessage
设备能力与构建能力未被混写 通过 deviceSupportsRecon / buildSupportsRecon 拆分
Recon3DService 已有正式接入口 通过 isBuildSupported()reconstruct() 占位
导出服务可按 3dgs / render / synthetic 分流 通过 ExportService.buildFromSynthetic()
3DGS 不可用时不会拖垮主流程 通过 catch 后自动回退 buildSyntheticGifFrames()
页面文案未把实验能力夸大为已完成能力 通过 Index.etsphase: '实验'真实3DGS:预留

十二、小结

第 18 篇真正想说明的是:

一个工具类 App 里最重要的,不是抢先把"3DGS"三个字写进页面,而是把当前可运行的能力未来待接入的能力不可用时的降级路线拆成一条清晰的工程链路。

"动图魔方"当前这条 3D 路线已经具备三个很关键的特征:

  1. 有真实执行体:Model3DView 已落地 ArkGraphics3D 模型预览。
  2. 有正式路由器:CapabilityService 把设备与构建条件拆开判断。
  3. 有稳定兜底:ExportService 在 3DGS 不可用时自动回退到合成导出。

这意味着未来即便要升级到 API 26、接入 Spatial Recon 或真实 3DGS 执行体,变动的也只是能力执行层,而不是整套页面、导出和验收结构。

十三、下一篇衔接

第 19 篇继续拆 《动图魔方技术拆解 19:HarmonyOS SDK 配置修正与 Hvigor 构建排查》,重点会转向:

  1. 6.1.0(23) 这类 HarmonyOS SDK 标识为什么经常在构建配置里写错。
  2. build-profile.json5、Hvigor 命令与本地环境变量怎样共同决定构建能否稳定通过。
  3. 当项目同时预埋高阶能力、又要保证当前版本可交付时,构建排查文档为什么必须和功能文档一起维护。

如果把第 18 篇看作"能力边界怎么收口",那第 19 篇就是"构建边界怎么收口"。前者解决的是页面和导出不说大话,后者解决的是本地和 CI 不因为 SDK 标识、构建脚本或环境变量反复翻车。

这里顺带保留一个系列锚点,方便统一回看老稿件:动图魔方技术拆解 17:清除虚拟数据后如何用真实素材验证 GIF 工具。第 17 篇解决真实素材验收,第 18 篇解决 3D 路线边界,第 19 篇则继续把构建稳定性补齐。