最开始我是在一个产品原型里要用三维模型,项目周期压得紧,美术那边给了个 GLB 文件,我当时脑子里的第一反应是:HarmonyOS 上到底怎么把 GLB 玩意儿渲染出来?翻了一圈资料,大部分都是介绍概念,能直接跑通的示例少得可怜。后来花了两个晚上,把 ArkGraphics3D 的 Scene 初始化、模型加载、节点挂载、生命周期管理整条链路啃了一遍,才把那个灰模小房子稳稳地渲染到屏幕上。
如果你也正卡在“GLB 文件下载下来了,但不知道在鸿蒙应用里怎么把它显示出来”这一步,这篇东西就是为你写的。它会带你走完从模型准备、工程配置、Scene 初始化到 GLB 资源加载与节点挂载的完整流程,最后还会把我在真机上踩过的白屏、缺贴图、内存翻倍这几个坑一并拆开讲。
1. 为什么我用 ArkGraphics3D 做鸿蒙 3D 渲染:选型逻辑与能力边界
先说结论:如果你要在 HarmonyOS 原生应用里展示 GLB 模型,而且不想引入 Unity、Cocos 这类重型引擎,ArkGraphics3D 是目前最合适的官方方案。它不是 WebView 里跑 Three.js,也不是拿 OpenGL ES 从零手写渲染管线,而是鸿蒙系统自带的、面向应用层开放的 3D 图形能力。
1.1 为什么不用 Web 方案或者自研渲染
很多做跨端的同学第一反应是上 WebView 跑 Three.js,因为 GLB 解析、PBR 材质、动画播放这些 Three.js 全都有,生态也成熟。但 WebView 的问题在于:它和原生 UI 的交互链路长,内存占用高,而且渲染性能受 WebView 内核调度影响很大。一个复杂的 GLB 模型在 WebView 里可能只有几十帧,但同样的模型走 ArkGraphics3D 的 GPU 管线能稳定跑满 60 帧。另外,如果你的应用里本来就用了 ArkUI 的原生组件,ArkGraphics3D 可以直接和这些组件做层级混排和事件互操作,这是 WebView 做不到的。
还有人会想自己封装 OpenGL ES 或者对接 Vulkan。那种方案自由度确实最大,但代价也最重:你得自己处理 GLB 的解析(glTF 2.0 规范里有 JSON 结构、二进制 buffer、纹理图像三个大部分),自己管理 GPU 资源、纹理上传、渲染循环、相机矩阵。这个工作量在一般业务项目里完全不可接受。ArkGraphics3D 把这些都封装好了,暴露给开发者的是一套场景图模型(Scene、Node、Mesh、Material 这些概念),你只需要关心“什么物体摆在什么位置”,不用关心“显卡怎么画出这个物体”。
1.2 ArkGraphics3D 能做什么、不能做什么
我把它接进项目之后,梳理了一下它的能力边界,方便你判断合不合适:
| 能力 | 支持情况 | 说明 |
|---|---|---|
| GLB / glTF 模型加载 | 支持 | 文本格式 .gltf 和二进制格式 .glb 都能解析 |
| PBR 材质渲染 | 支持 | 金属度、粗糙度、法线贴图、环境贴图都有效果 |
| 骨骼动画 / 顶点动画 | 支持 | 但需要确认动画节点是否被正确加载 |
| 多相机、多光源 | 支持 | 适合做产品展示、场景漫游 |
| 与 ArkUI 混排 | 支持 | 通过 XComponent 承载渲染区域 |
| 粒子、后处理特效 | 部分支持 | 能接入但生态较弱,复杂特效还是建议专用引擎 |
| 物理引擎 | 不支持 | 需要自己集成第三方物理库 |
提示:如果你的目标是做一个完整游戏(带物理碰撞、Npc AI、大世界关卡),ArkGraphics3D 目前还不合适;但如果你做的是“产品展示、家装预览、工业模型查看器”这类应用,它正好合适。
1.3 版本兼容性:HarmonyOS 6 和 API 版本的关系
ArkGraphics3D 是伴随着 SDK 演进逐步开放的,在我的工程里,我用的是 HarmonyOS 6 配套的 SDK,特性开关默认开启,没有额外需要勾选的权限。只要你用的是 DevEco Studio 里较新版本的 SDK,创建工程后直接在代码里 import 对应的 Kit 就能用。如果你的项目还在老的 API 版本上(比如 API 9 或更早),那就别想了,得先升级工程。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. GLB 模型源:下载、格式转换、浏览器预览校验一条龙
很多人在代码上栽跟头之前,先在模型上栽了跟头。GLB 文件看着是一个 .glb,但里面封装的质量参差不齐,有的模型贴图是外部引用的、有的模型包含了动画、有的模型尺寸巨大或轴线歪了。所以第一步是把模型源打理干净。
2.1 GLB 和 glTF 的区别:什么时候选哪个
glTF 2.0 是一种面向 GPU 的 3D 模型格式标准,它有两种主要分发形态:
.gltf:JSON 文本文件,外加一堆外部引用的.bin(几何数据)和.png / .jpg(贴图)文件。好处是文本可读、容易调试,坏处是文件多、传输慢、容易漏文件。.glb:把 JSON、二进制几何数据、贴图全部打包进一个二进制文件。好处是单文件、方便分发、加载性能更好。
所以项目里我推荐一律使用 .glb,因为应用安装包里面放一个文件比放二十个文件优雅得多,下载和拷贝也不容易坏。
2.2 从哪下载靠谱的 GLB 模型
常见渠道有这么几个:
- 官方示例仓库:KhronosGroup 维护的 glTF-Sample-Models,里面有大量标准测试模型,适合做功能验证。
- 一些公开的 3D 模型站:Sketchfab 上很多模型支持直接下载 GLB,注意看它标注的许可协议,有的是免费可商用,有的仅供学习。
- 自己用建模软件导出:Blender、3ds Max、Cinema 4D 都支持导出 GLB,这种情况下模型质量完全可控。
在实际生产里我更喜欢“自己导出”而不是“下载现成”,因为下载来的模型经常带着一堆用不到的高精度贴图和一个奇怪的原点坐标,后续反而要花时间去清理。
2.3 skp 转 GLB:SketchUp 模型进鸿蒙的实用路线
有朋友问过我,SketchUp 做的模型(.skp)怎么变成 GLB。SketchUp 本身不支持直接导出 GLB,我试过的可行路线有两条:
- 用 Blender 中转:Blender 直接导入
.skp文件需要安装插件(比如 SketchUp Importer 插件),导入后用 blender 的 “导出为 glTF 2.0” 功能,格式选.glb。这条路最通用,也方便顺手做减面。 - 使用 SketchUp 的 glTF 导出插件:部分商业插件(比如 Exporter for glTF)可以一键导出,但免费版往往有模型面数限制。
Blender 中转最大的好处是中间多了一次“修模型”的机会。比如 SketchUp 里模型单位是英寸、Blender 里单位是米,导出之前可以用 Blender 的比例设置统一单位,再用 Ctrl+A 应用缩放,保证导出的 GLB 在鸿蒙端尺寸是正常的。
2.4 网页 GLB 查看器:加载到真机前先做一次体检
代码还没跑之前,我强烈建议你把 GLB 文件拖到网页查看器里看一眼。比如 gltf-viewer 这样的工具,直接在浏览器里打开,把 .glb 拖进去就能看到渲染结果、模型层级、动画列表、相机参数。
为什么要做这一步?因为你一旦写完了 ArkGraphics3D 的加载代码,发现屏幕上黑乎乎一片,你会很难判断到底是“模型文件本身坏了”还是“场景初始化写错了”。提前在网页查看器里确认模型能正常显示,就排除了一个最大的变量。我习惯检查三件事:
- 模型是否正常显示,有没有缺贴图导致紫红色。
- 模型的尺寸和中心点在哪,很多模型原点不在几何中心,会导致加载后偏到画面外。
- 动画和控制器的参数,避免鸿蒙端动画播放异常。
2.5 一个能跑通的 GLB 文件长什么样
到这一步,你手里的 GLB 应该满足这些条件:
- 是二进制
.glb格式,不是.gltf加一堆外部资源。 - 贴图是嵌入在文件内部的,不依赖外部图片路径。
- 模型尺寸适中,比如单位是米,一个物体在 0.1 到 10 之间比较合理。
- 它在网页查看器里能正常渲染出来。
如果你已经有这个文件了,下面就可以进工程了。
3. 工程初始化:从 Device 到 Scene 再到 XComponent 的最小骨架
ArkGraphics3D 的架构有点像一个“微缩引擎”,你要按顺序把渲染环境搭起来。我用一个类比帮你理解:Device 是显卡驱动的抽象,Scene 是一个舞台,Node 是台上的演员,Camera 是观众的视角,Light 是舞台灯光。缺了任何一个,表演要么看不见、要么看不清。
3.1 架构概览:Device、Scene、Node、Camera、Light
Device:代表 GPU 设备和上下文,是创建所有其他资源的前提。它管理底层图形 API 的上下文、着色器编译、纹理上传这些事的生命周期。Scene:场景图的根节点,包含所有渲染对象、相机、光源。每次帧更新都会遍历这个图。Node:场景中的实体节点,可以嵌套。模型加载进来之后就是一堆 Node 组成的层级树,每个 Node 上可能挂着一个 Mesh(网格)。Camera:定义观察者的位置、朝向、视野角度。没有 Camera,场景里的物体不会参与投影计算,渲染出来就是黑的或者空白。Light:定义光照环境。没有 Light,模型看起来是黑的(除非模型本身用了不受光照影响的材质)。
3.2 创建 XComponent:3D 渲染区域要嵌在哪里
ArkGraphics3D 的渲染结果要显示到屏幕上,需要通过 ArkUI 的 XComponent 组件来承载。它的本质是给原生渲染模块提供一个纹理或者 Surface 的挂载点。
在 ArkUI 页面里,你要做的第一件事是声明一块 XComponent 区域:
typescript复制@Entry
@Component
struct ModelViewerPage {
private modelViewId: string = 'model_view_3d'
build() {
Stack({ alignContent: Alignment.Center }) {
XComponent({
id: this.modelViewId,
type: XComponentType.TEXTURE,
libraryname: ''
})
.width('100%')
.height('100%')
}
}
}
这里注意 type: XComponentType.TEXTURE,它表示这个 XComponent 以纹理模式工作,适合被图形接口直接渲染。如果你需要嵌入的是摄像头预览这类场景,可能要换 SURFACE 模式,但加载 3D 模型用 TEXTURE 就够了。
提示:XComponent 的 id 是全局标识,后面 ArkGraphics3D 初始化的时候需要根据这个 id 找到对应的渲染区域。id 写错了,后面怎么创建都是白搭。
3.3 初始化 Device 并创建 Scene
在组件 aboutToAppear 之后、页面显示之前,最好把渲染环境初始化好。这里有一个容易混淆的点:XComponent 的 onLoad 事件是在 XComponent 创建完成后触发的,在这个回调里去做渲染上下文的绑定最安全。
基本的初始化顺序:
typescript复制import { arkui3d } from '@kit.ArkGraphics3D'
// 1. 创建设备
let device = arkui3d.Device.create()
// 2. 创建场景
let scene = arkui3d.Scene.create(device)
// 3. 给场景添加一个相机
let camera = scene.createNode('camera')
camera.position = { x: 0, y: 2, z: 5 }
camera.lookAt({ x: 0, y: 0, z: 0 })
scene.camera = camera
// 4. 给场景添加一个平行光
let light = scene.createNode('light')
light.position = { x: 3, y: 5, z: 4 }
scene.addNode(light)
顺序千万别搞反,先有 Device 才能创建 Scene,先有 Scene 才能往它身上挂节点。我之前偷懒,先在 aboutToAppear 里创建了 Scene,再在 onLoad 里绑定 XComponent,结果运行的时候直接报设备上下文为空的错。
3.4 把 XComponent 和 Scene 绑定起来
这一步是把“画面输出到屏幕”的最后一道桥梁。不同版本 API 名字可能有差异,但思路是一致的:用 XComponent 的 id 去拿到渲染 surface,然后把它作为 Scene 的输出目标。
typescript复制XComponent({
id: this.modelViewId,
type: XComponentType.TEXTURE,
libraryname: ''
})
.onLoad(() => {
// 拿到 XComponent 的渲染上下文
let ctx = this.getXComponentContext(this.modelViewId)
// 把 scene 输出到这块纹理上
scene.attachSurface(ctx)
})
这里能跑通,后面加载模型才有意义。我见过不少新手在 scene 没绑定 surface 之前就急急忙忙加载 GLB,模型加载成功了,但屏幕上始终什么都没有——因为画面根本没输出到界面。
3.5 渲染循环和动画帧:谁来让画面动起来
ArkGraphics3D 的场景默认会持续渲染,不需要你像 OpenGL 那样手动调 requestAnimationFrame。但如果你想做自转、位移、缩放这类动画,需要注册一个帧回调,在每一帧更新节点的 transform 属性。
typescript复制let angle = 0
function onFrame() {
angle += 0.01
modelNode.rotate = { x: 0, y: angle, z: 0 }
// 继续请求下一帧
}
这个回调的频率和屏幕刷新率一致(通常 60Hz),在真机上做展示类应用完全够用。如果你要做复杂的骨骼动画播放,ArkGraphics3D 也提供了动画组件,但它是基于模型动画片段(clips)驱动的,后续讲到模型加载时再展开。
4. 加载 GLB 的三条通道与模型节点挂载细节
现在进入标题里的核心环节:把 GLB 文件加载进来。这一步坑最多,我拆开来讲。
4.1 GLB 文件放在哪里、怎么引入工程
在 HarmonyOS 工程里,GLB 模型文件一般放在 resources/rawfile 目录下,因为它属于“原始文件”,不会被编译器做特殊处理,读取的时候需要通过资源接口按文件名访问。
rawfile 的优点是通过资源路径直接访问,不会打包成二进制资源索引;缺点是它只能按路径读取,不允许列出目录。所以模型引用路径要写对,大小写也要敏感。
4.2 加载方式一:从 rawfile 读取并解析为场景节点
最常用的方式是从 rawfile 读取 GLB 资源,然后交给 ArkGraphics3D 解析。我以伪代码的形式给出参考流程:
typescript复制import { resourceManager } from '@kit.AbilityKit'
let resMgr = getContext(this).resourceManager
// 1. 打开 rawfile 文件
let file = resMgr.getRawFileContent('models/demo.glb')
// 2. 把它转换成 ArkGraphics3D 的资源对象
let glbResource = arkui3d.Resource.create(file)
// 3. 加载该资源,拿到场景节点
scene.loadModel(glbResource).then((modelNode) => {
// 4. 模型加载成功后挂到场景根节点上
scene.addNode(modelNode)
}).catch((err) => {
console.error(`[ArkGraph 3D] load failed, code=${err.code}, msg=${err.message}`)
})
这里的关键点是:loadModel 是异步的,GLB 文件的解析、GPU 纹理上传、网格提交都发生在这个异步过程中。不要在 then 回调之外访问 modelNode,否则你拿到的一定是 undefined。
4.3 加载方式二:从网络下载再加载
如果你的 GLB 文件不在本地,需要先从服务端下载。注意这需要在 module.json5 里申请网络权限:
json复制{
"module": {
"requestPermissions": [
{
"name": "ohos.permission.INTERNET"
}
]
}
}
下载完后保存到应用私有目录,然后再读取文件字节流喂给资源对象。这种方式适合做“模型热更新”,比如线上商品模型库,不需要发版就能上新模型。
4.4 模型节点添加到场景:位置、缩放、旋转
GLB 文件加载进来之后,它自带一个局部坐标系。如果模型在建模软件里没有对齐原点,你需要在场景里手动调整它的位置和姿态。
typescript复制modelNode.position = { x: 0, y: -1, z: 0 }
modelNode.scale = { x: 1, y: 1, z: 1 }
modelNode.rotate = { x: 0, y: 0, z: 0 } // 弧度制
有一个比较隐蔽的坑:大部分建模软件的坐标轴是 Z 轴朝上,而 OpenGL 系(包括 ArkGraphics3D)默认 Y 轴朝上。如果你发现模型加载出来后是“躺倒”的,把它绕 X 轴旋转 -Math.PI / 2 就会立起来:
typescript复制modelNode.rotate = { x: -Math.PI / 2, y: 0, z: 0 }
如果你的模型里自带了多个子节点(比如一个产品模型包含外壳、内部零件、Logo 等),加载后它们会按原有层级结构挂在模型节点的子节点下。这种情况下不要随意修改父节点的 scale,否则容易出现子节点变形。
4.5 相机参数调优:为什么模型加载了却看不见
这是所有 3D 应用新手都会遇到的问题:模型加载成功、场景也绑定成功,但屏幕上一片黑。原因大概率是相机位置不对。
GLB 模型在建模软件里常见的尺寸是“小物体 0.5 米,大物体 5 米”,但有些模型你根本不知道它的真实尺寸。加载后第一件事,我建议打印模型的包围盒信息:
typescript复制let bounds = modelNode.getBounds()
console.info(`[bounds] ${JSON.stringify(bounds)}`)
拿到包围盒的中心点和尺寸后,把相机摆到能覆盖这个包围盒的距离:
typescript复制let radius = bounds.size.length()
camera.position = { x: bounds.center.x, y: bounds.center.y, z: bounds.center.z + radius * 2 }
camera.lookAt(bounds.center)
这个方法非常实用。你会发现“看不见模型”很多时候根本不是模型的问题,而是相机离模型太近或者离得太远。
4.6 动画模型的特殊处理:glTF clips 加载
你的 GLB 如果带了动画(比如模型里有骨骼动画、机械结构转动),加载后 modelNode 下会包含动画组件。播放动画的基本流程是先取到动画片段列表,再选择一个片段播放:
typescript复制let animator = modelNode.getAnimator()
let clips = animator.getClips()
if (clips.length > 0) {
animator.play(clips[0].name, { loop: true })
}
这里容易出问题的是:动画片段的名称可能包含特殊字符(比如空格、中文),建议在导出 GLB 时就把动画命名规范化。我在 Blender 里导出前都会把 Action 命名为 idle、walk、attack 这种纯英文短单词,到了代码里处理起来干净利落。
4.7 资源释放:别再让每次退出页面内存翻倍
GLB 加载进去之后会占据显存和内存,包括网格顶点缓冲、索引缓冲、纹理缓存等。如果用户在 A 页面反复进出,模型重复创建但不释放,内存会一路涨上去。实测下来,一个 20MB 的 GLB,光纹理就可能占 50MB 以上内存,反复进出几次,低内存设备就直接被系统回收了。
在页面 aboutToDisappear 或者组件销毁时,必须把场景节点和资源显式释放:
typescript复制aboutToDisappear() {
if (this.modelNode) {
scene.removeNode(this.modelNode)
this.modelNode.destroy()
this.modelNode = null
}
// 如果要彻底释放,还要销毁 Scene 本身
scene.destroy()
}
有一些模型加载的是同一份资源文件,如果两个页面要共享,建议做一个资源缓存单例:同一个 GLB 只解析一次,后续页面加载同一份资源时直接复用解析后的节点对象,而不是再走一遍文件读取和解析。这是我目前项目里收益最大的一次优化,内存从 400MB 降到了 180MB 左右。
5. 真实环境里最容易翻车的五个场景分析与排查路径
到这里,功能基本能跑通了。但真实项目不会这么顺利,我在接入过程中遇到过不少诡异现象,这里挑五个最有代表性的拆开讲。这些坑对新手来说特别隐蔽,你不踩一次很难意识到。
5.1 白屏:只有清屏色,没有模型
现象:XComponent 区域有背景色,但模型完全渲染不出来,控制台也没有任何报错。
排查路径:先确认场景有没有绑定到 XComponent surface;再确认相机是否在场景中;再确认模型节点是否添加成功。我那次白屏原因很蠢——loadModel 还没执行完,页面就做了跳转,模型节点没来得及挂到场景上。后来我加了加载状态提示,等异步回调完成后再切换 UI,问题解决。
建议:模型加载通常有几百毫秒延迟,一定要加 loading 状态,否则用户会以为应用卡死了。白屏时先在回调里打日志,确认 modelNode 非空。
5.2 模型黑乎乎的:没有光或者材质异常
现象:场景能显示,但模型是全黑的,像个黑色剪影。
排查路径:多因场景里没有光源。GLB 模型自带 PBR 材质,有金属度和粗糙度,无光环境下等于在暗房里看黑色雕塑,什么都看不清。给场景添加 DirectionalLight 和 AmbientLight 后,模型立刻有了明暗和颜色。
还有一个原因是贴图路径问题。如果 GLB 虽然是单文件,但贴图是外部引用的 .KTX2 或者 .webp,解析失败的贴图会显示为黑色。要确认 GLB 里贴图是否内嵌,用前面说的网页查看器转一圈就知道。
5.3 模型躺倒了:坐标系不一致
现象:模型能显示,但它是横躺的,和预期朝向差了 90 度。
原因:Blender、SketchUp 等软件习惯 Z 轴朝上,而实时渲染引擎通常 Y 轴朝上。导出的 GLB 如果没做轴向转换,进入 ArkGraphics3D 就会躺倒。
修复:在 Blender 导出设置里旋转 X 轴 -90,或者加载后在代码里把模型节点绕 X 轴旋转 -π/2,两种方法都行。更推荐在导出时处理,因为这样模型在场景里的逻辑坐标更直观。
5.4 纹理模糊或紫红色:贴图格式和尺寸问题
现象:模型能显示,但表面的纹理很模糊,甚至变成紫红色。
原因:GLB 里面的贴图尺寸过大时,会被 GPU 降采样;贴图格式如果非 GPU 直接支持的压缩格式,可能需要转码,转码失败就会出现紫红色。
排查:用图片工具查看 GLB 内嵌贴图的格式。WebP、PNG、JPEG 基本没问题;如果贴图用了罕见的格式或者超大的 16K 纹理,建议先用工具统一压缩到 2K 左右再导出。
5.5 频繁进出页面后卡顿掉帧
现象:第一次进入场景很流畅,第二次就明显掉帧,第三次直接 ANR。
原因:资源泄漏,模型节点每次进入都重新加载,退出时未释放。
修复:遵循上面说的资源释放流程,并做一份模型缓存。另外,如果模型文件本身太大(超过 50MB),即便没有泄漏,每次加载的解析时间也很可观。建议在资源管理阶段就把模型减面压缩,后面会具体说。
6. 从能显示到跑得顺:模型压缩与渲染优化清单
模型能在真机上显示出来,只是迈出了第一步。一个生产级的 3D 展示应用,还要考虑安装包体积、内存占用量、首帧渲染时长。这一节是一个可以直接“抄作业”的优化清单,我把它按照优先级排了序。
6.1 用 Draco 压缩几何数据:体积最多能缩 90%
GLB 文件里占用最大头的通常是顶点几何数据。Draco 是 Google 开源的网格压缩算法,可以把顶点位置、法线、UV 这些数据压缩掉大部分,而且 GPU 在渲染时能直接解码,无需在 CPU 侧解压成原始数据再上传。
具体操作,我推荐使用 gltf-transform 这个命令行工具:
bash复制npx @gltf-transform/cli optimize in.glb out.glb --compress draco
实测下来,一个导出自 Blender 的 15MB 模型,经过 Draco 压缩后能降到 4.8MB,而且渲染质量几乎无损。注意 Draco 压缩会轻微增加 GPU 解码耗时,所以不要对那种面数本身就很低的模型做压缩,收益不大反而增加开销。
6.2 纹理压缩与尺寸控制:显存省一半
GLB 里的 PNG 贴图是未压缩格式,RGBA 32 位,一张 2048x2048 的纹理在显存里占 16MB。如果你一个模型有 5 张贴图,光贴图就占 80MB 显存。
建议:
- 贴图尺寸统一降到 2048 或 1024。展示类应用看整体效果,1024 通常够了。
- 用
gltf-transform把 PNG 转成 WebP,可以显著减小文件体积。 - 如果目标设备明确支持 ASTC / ETC2 压缩纹理,可以在导出时把这些纹理转换。ARK3D 对 GPU 纹理格式的支持里,ASTC 是主流。
bash复制npx @gltf-transform/cli optimize in.glb out.glb --texture-compress webp
6.3 模型减面:用 Blender 的 Decimate 修改器
如果你的模型是从 SketchUp 或者高精度建模软件里拿过来的,面数可能动辄上百万,这对于实时渲染来说完全是浪费。在 Blender 里导入模型后,选中模型添加 Decimate(减面)修改器,把比例调到 0.2-0.3,肉眼几乎看不出区别,但渲染压力能降到原来的五分之一。
我一般在 Blender 里这么处理:
- 导入 GLB。
- 切到编辑模式,选中网格,添加 Decimate 修改器。
- 将 Ratio 设为 0.2,勾选 Collapse 模式。
- 应用修改器,导出 GLB。
6.4 首帧优化:让用户少等 500ms
加载大模型时,首帧时间往往被文件读取和解析占用。两个技巧:
- 模型文件不要放在启动时就加载,等用户真正进入 3D 页面再异步加载。
- 在加载过程中先显示一个轻量级占位场景(比如一个简单的立方体),让用户感觉到“3D 环境已经活了”,一旦模型加载完成再替换节点。这个体验提升非常明显。
6.5 渲染质量与性能的取舍
在真机上调试时,务必关注帧率和功耗。DevEco Studio 自带的 Profiler 可以查看 GPU 占用、表层渲染耗时。我个人的基准值是:
| 场景 | FPS | 内存增量 |
|---|---|---|
| 单模型展示,500k 面 | 60 | <80MB |
| 多模型场景,100 万面 | 30-45 | <150MB |
| 有动画循环播放 | 30+ | 基线上浮 10% |
如果达不到,优先检查贴图大小,然后再检查模型面数。很多时候贴图减半就能救回来,根本不值得去动模型。
7. 从 Demo 到生产:我踩完坑后沉淀的最终模板
最后分享一套我目前在真机项目里稳定使用的初始化模板。它不一定覆盖你所有的业务场景,但作为最基础的三维模型展示框架,完全可以作为起点。
typescript复制@Entry
@Component
struct ModelViewerPage {
private xComponentId: string = 'ag3d_xcomponent'
private scene: arkui3d.Scene | null = null
build() {
Stack({ alignContent: Alignment.Center }) {
XComponent({ id: this.xComponentId, type: XComponentType.TEXTURE, libraryname: '' })
.width('100%')
.height('100%')
.onLoad(() => {
this.initScene()
})
}
}
private initScene() {
let device = arkui3d.Device.create()
this.scene = arkui3d.Scene.create(device)
let camera = this.scene.createNode('camera')
camera.position = { x: 0, y: 2, z: 6 }
camera.lookAt({ x: 0, y: 0, z: 0 })
this.scene.camera = camera
let light = this.scene.createNode('light')
light.intensity = 1.2
this.scene.addNode(light)
// 这里把 scene 挂到 XComponent 上
this.scene.attachSurface(this.getXComponentContext(this.xComponentId))
this.loadModel('models/demo.glb')
}
private loadModel(path: string) {
let resMgr = getContext(this).resourceManager
resMgr.getRawFileContent(path).then((file) => {
let resource = arkui3d.Resource.create(file)
this.scene?.loadModel(resource).then((node) => {
node.scale = { x: 1, y: 1, z: 1 }
this.scene?.addNode(node)
})
})
}
aboutToDisappear() {
this.scene?.destroy()
this.scene = null
}
}
这套模板我用了几个月,稳定性很好。唯一要注意的是 API 细节可能随版本迭代有调整,你把思路理解透了,遇到变化改起来也就几分钟的事。我个人在接入 ArkGraphics3D 的过程中最大的体会是:它虽然是操作系统级能力,但如果你按照“建模工具导出—浏览器预览—工程初始化—场景组装—资源释放”这条链路一步一步来,踩坑是可以被系统性规避的,而不是靠运气碰对。最后再分享一个小技巧:当你怀疑模型加载不出来是文件问题而不是代码问题时,别急着查日志,先用网页查看器看看模型本身,这一步能替你省下至少 30 分钟的排查时间。
