前言
大家好,我是simple。我的理想是利用科技手段来解决生活中遇到的各种问题。
在鸿蒙应用开发中,三维模型的集成能极大提升用户交互体验,尤其在虚拟展示、互动教育等场景中不可或缺。本文基于一份实际的鸿蒙3D模型加载代码,详细解析从模型加载、光源配置、相机调优,到动画控制与资源管理的全流程。
一、核心框架与代码结构概览
本次示例基于鸿蒙ArkUI 3D图形框架,核心依赖@kit.ArkGraphics3D
提供的3D渲染能力,主要涉及Scene
(场景)、Camera
(相机)、Light
(光源)、Animation
(动画)等核心类。
二、模型加载:从资源到场景的桥梁
3D模型的加载是整个流程的基础,鸿蒙推荐使用glTF
格式(.gltf或.glb),因其轻量且适合移动端渲染。代码中通过Scene.load
实现模型加载,我们来拆解这一过程。
1. 资源准备与路径处理
加载时需确保模型关联的纹理、二进制文件(.bin)与gltf文件在同一目录,避免资源丢失。
2. 场景初始化流程
模型加载的核心逻辑在init
方法中,通过Scene.load
异步加载模型,并在加载完成后初始化场景参数:
typescript
init(): void {
if (this.scene == null) {
// 加载gltf模型(核心API)
const scene = Scene.load(this.model);
scene.then(async (result: Scene) => {
if (result) {
this.scene = result;
// 调整模型根节点位置(模型调整)
if (result.root) {
result.root.position = { x: 0, y: 0, z: 0 }; // 重置模型位置到原点
}
// 后续初始化:相机、光源、动画...
}
});
}
}
Scene.load
:异步加载模型资源,返回Promise<Scene>
,场景实例包含模型的所有节点、动画等信息。- 模型位置调整:通过
result.root.position
设置模型根节点坐标,确保模型在场景中的初始位置符合预期(此处重置到原点,避免模型偏移)。
三、光源配置:让模型"可见"的关键
3D模型默认处于"无光环境",若不添加光源,模型会因无法反射光线而完全黑屏。代码中通过创建平行光(DirectionalLight)为场景提供照明,我们来分析其实现。
1. 平行光的创建与参数
typescript
// 在模型加载完成后创建光源
let sceneFactory: SceneResourceFactory = this.scene.getResourceFactory();
let sceneLightParameter: SceneNodeParameters = { name: "light" };
// 创建平行光(DirectionalLight)
const light: Light = await sceneFactory.createLight(sceneLightParameter, LightType.DIRECTIONAL);
light.color = this.lightColor; // 从外部接收光源颜色
- 光源类型 :选择
LightType.DIRECTIONAL
(平行光),模拟太阳光效果,光线平行照射,适合大多数场景。 - 颜色控制 :通过
@Prop lightColor
接收外部传入的颜色,支持动态调整光源色调(如白色{r:1, g:1, b:1, a:1}
或暖黄色{r:1, g:0.8, b:0.6, a:1}
)。
目前只有平行光和点光源。
四、相机调整:定义"观看视角"
相机是3D场景的"眼睛",决定了用户从哪个角度观察模型。代码中创建了自定义相机,并通过参数调整优化视角,关键配置如下:
1. 相机创建与核心参数
typescript
// 创建相机
let rf: SceneResourceFactory = this.scene.getResourceFactory();
this.cam = await rf.createCamera({ "name": "Camera" });
// 相机启用
this.cam.enabled = true;
// 位置与旋转
this.cam.position = this.cameraPosition; // {x:0, y:0, z:0}(可外部调整)
this.cam.rotation = this.cameraRotation; // 初始旋转(四元数)
// 视场角与裁剪面
this.cam.fov = 75; // 视场角75度(值越大,视野越广)
this.cam.nearPlane = 0.1; // 近平面(距离相机过近的物体不渲染)
this.cam.farPlane = 1000; // 远平面(距离相机过远的物体不渲染)
- fov(视场角):75度是平衡视角广度与模型细节的常用值,值越大场景容纳内容越多,但模型可能显小。
- 近/远平面 :
nearPlane=0.1
避免近距离模型被裁剪,farPlane=1000
确保远距离模型可见,根据模型大小调整(如小模型可减小farPlane
提升性能),在这里切记nearPlane
必须比farPlane
要小,不然会有不可预料的错误。
2. 视角优化建议
- 若模型显示不全:调整相机
position.z
(沿Z轴后退,如z:5
),拉远与模型的距离。 - 若模型倾斜:通过
cameraRotation
调整旋转角度(如{x: -15, y: 30, z: 0, w:1}
实现俯视角)。 - 复杂场景可添加多个相机,通过切换
enabled
属性实现视角切换。
五、动画控制:让模型"动起来"
3D模型的动画(如机械臂运动、角色动作)是提升交互感的核心。代码中通过Animation
类管理模型动画,并实现了完整的启停控制逻辑。
1. 动画加载与初始化
模型加载完成后,通过result.animations
获取所有内置动画(gltf模型可包含多个动画轨道):
typescript
// 获取模型中的所有动画
let animations: Animation[] = result.animations;
if (animations.length) {
this.threeDModelAnimation = animations; // 保存动画列表
this.startAni(); // 启动动画
}
2. 动画启停与循环控制
代码封装了startAni
、stopAni
、pauseAni
三个方法,分别控制动画的启动、停止与暂停:
typescript
// 启动动画
startAni: () => void = () => {
if (this.scene && this.visible) {
this.threeDModelAnimation.forEach((ani) => {
ani.onStarted(() => {
// 绑定帧率同步,每帧渲染场景
this.backDisplaySyncRate?.on("frame", () => {
if (this.scene && this.cam) {
this.scene.renderFrame(); // 渲染一帧
}
});
this.backDisplaySyncRate?.start(); // 启动帧率同步
// 动画完成后循环播放
ani.onFinished(() => {
if (this.visible) {
ani.seek(0); // 回到起点
ani.stop();
ani.start(); // 重新启动
}
});
});
ani.start(); // 启动当前动画
});
}
};
// 停止动画
stopAni: () => void = () => {
this.threeDModelAnimation.forEach((ani) => {
ani.stop();
ani.seek(0); // 重置到初始状态
this.backDisplaySyncRate?.stop(); // 停止帧率同步,节省资源
});
};
- 帧率同步 :通过
displaySync.create()
创建60帧同步器(sync60
),确保动画流畅且不占用额外性能。 - 循环播放 :
onFinished
回调中重新启动动画,实现无缝循环。 - 资源释放 :停止动画时调用
backDisplaySyncRate.stop()
,避免后台无效渲染。
六、可见性管理:动态资源优化
当模型不可见时(如页面切换、组件隐藏),继续运行动画会浪费系统资源。代码通过visible
状态结合onVisibleAreaChange
实现动态管理:
1. 可见性监测与状态联动
typescript
build() {
Stack() {
// 监测可见区域变化(触发visible状态更新)
Row()
.width(5).height(5)
.onVisibleAreaChange([0.0, 1.0], (visible) => {
this.visible = visible; // 当可见比例在0~1之间时更新状态
});
// 3D场景渲染(仅当sceneOpt存在时显示)
if (this.sceneOpt) {
Component3D(this.sceneOpt);
}
}
}
onVisibleAreaChange
:监听组件可见比例,当完全不可见时visible
设为false
,触发动画停止。
2. 可见性变化时的动画控制
通过@Watch('changeVisible')
监听visible
状态变化,自动启停动画:
typescript
@Watch('changeVisible')
@State visible: boolean = false;
changeVisible() {
if (this.visible) {
// 可见时:若未初始化则初始化,否则启动动画
if (!this.scene) {
this.init();
} else {
this.startAni();
}
} else {
// 不可见时:停止动画
this.stopAni();
}
}
这一机制确保模型在隐藏时完全释放动画资源,显著提升应用后台性能。
七、代码封装与扩展:ThreeDController的作用
为了让动画控制逻辑更清晰,代码通过ThreeDController
封装了启停方法,方便外部调用:
typescript
// 控制器定义(简化)
export class ThreeDController {
stop?: () => void;
start?: () => void;
pause?: () => void;
}
// 组件中关联控制器
controller: ThreeDController = new ThreeDController();
aboutToAppear(): void {
this.controller.stop = this.stopAni;
this.controller.start = this.startAni;
this.controller.pause = this.pauseAni;
}
外部可通过获取controller
实例,灵活控制动画(如按钮点击暂停/继续):
typescript
// 外部调用示例
ThreeDModel({ model: $rawfile('model.gltf') })
.onReady((controller) => {
// 点击按钮暂停动画
Button('暂停').onClick(() => controller.pause());
});
八、常见问题
- 模型格式选择 :优先使用
glb
格式(二进制glTF),相比gltf
减少文件数量,加载更快。 - 常见问题排查 :
- 模型不显示:检查光源是否添加、相机位置是否正确(避免模型在相机后方);
- 动画不播放:确认gltf模型包含动画轨道(可通过Blender导出时勾选"动画"选项);
- 三维模型审核不通过
- 在不可见时,模型未被销毁但仍然在运行动画等会造成资源浪费,从而导致上架审核不通过,可参考第六点。