HarmonyOS APP《画伴梦工厂》开发第16篇:手势交互——拖拽旋转 3D 模型
第2.8篇:手势交互——拖拽旋转 3D 模型
难度:⭐⭐⭐ 高级
前置知识:2.7 3D 模型渲染入门
涉及源文件:products/default/src/main/ets/pages/ModelViewerPage.ets

前言
在上一篇中,我们成功使用 ArkGraphics3D 加载并渲染了一个 3D 模型。但静态的模型只能展示一个角度,在实际应用中,用户通常需要从各个方向观察模型。
本篇将在模型渲染的基础上,实现两种交互方式:手指拖拽旋转 和 按钮步进旋转,并深入理解四元数(Quaternion)在 3D 旋转中的运用。
1. 旋转状态管理
首先定义三个关键变量来追踪旋转状态:
private rotationAngle: number = 0; // 当前旋转角度(弧度)
private dragStartX: number = 0; // 拖拽起始 X 坐标
private dragStartAngle: number = 0; // 拖拽起始时的旋转角度
rotationAngle:模型绕 Y 轴的总旋转量,单位为弧度。dragStartX:用户手指按下时的屏幕 X 坐标。dragStartAngle:手指按下时当前的旋转角度,用于增量计算。
弧度与角度换算:
角度 = 弧度 × 57.3(或弧度 × 180 / π)。代码中用Math.round(rotationAngle * 57.3)将弧度转换为角度显示。
2. onTouch 手势监听
onTouch 是 ArkUI 提供的基础手势事件接口,可以监听 Down、Move、Up 等触摸事件。
private handleRenderTouch(event: TouchEvent): void {
if (event.touches.length === 0) {
return;
}
if (event.type === TouchType.Down) {
// 记录触摸起点和当前角度
this.dragStartX = event.touches[0].x;
this.dragStartAngle = this.rotationAngle;
} else if (event.type === TouchType.Move) {
// 计算水平滑动的偏移量
const deltaX: number = event.touches[0].x - this.dragStartX;
// 偏移量 → 旋转角度(乘以 0.01 做归一化)
this.rotationAngle = this.dragStartAngle + deltaX * 0.01;
// 应用旋转到模型
this.applyModelRotation();
// 更新状态显示
this.status = '拖动旋转:' + Math.round(this.rotationAngle * 57.3).toString() + '°';
}
}
手势逻辑拆解
| 事件类型 | 行为 |
|---|---|
| TouchType.Down | 记录起始 X 坐标和当前角度,作为增量计算的基准 |
| TouchType.Move | 计算水平位移 deltaX,累加到起始角度上 |
| TouchType.Up | 无需处理,角度已实时更新 |
参数归一化
deltaX * 0.01 中的 0.01 是一个归一化因子,它将屏幕像素差值映射到弧度值:
- 屏幕滑动 100px → 旋转 1 弧度(约 57.3°)
- 屏幕滑动 360px → 旋转 3.6 弧度(约 206°)
这个值可以根据交互体验需求调整,数值越大,同样滑动距离产生的旋转角度越大。
3. 四元数(Quaternion)旋转变换
3.1 为什么用四元数?
在 3D 图形学中,表示旋转有三种主要方式:
| 方式 | 优点 | 缺点 |
|---|---|---|
| 欧拉角(Euler) | 直观,三个角度 | 万向锁问题 |
| 旋转矩阵 | 通用 | 占用空间大,插值困难 |
| 四元数(Quaternion) | 无万向锁,插值平滑 | 不够直观 |
ArkGraphics3D 的 Node.rotation 属性使用四元数,避免了欧拉角的万向锁问题。
3.2 绕 Y 轴旋转的四元数
private applyModelRotation(): void {
if (this.modelRoot === null) {
return;
}
const halfAngle: number = this.rotationAngle / 2;
const rotation: Quaternion = {
x: 0,
y: Math.sin(halfAngle), // 绕 Y 轴旋转
z: 0,
w: Math.cos(halfAngle)
};
this.modelRoot.rotation = rotation;
this.requestRenderFrame();
}
四元数原理
绕任意轴旋转的四元数公式为:
Q = (cos(θ/2), axis_x * sin(θ/2), axis_y * sin(θ/2), axis_z * sin(θ/2))
当绕 Y 轴旋转时,axis = (0, 1, 0),因此:
Q = (cos(θ/2), 0, sin(θ/2), 0)
对应到代码中:
| 分量 | 公式 | 代码 |
|---|---|---|
w |
cos(θ/2) |
Math.cos(halfAngle) |
x |
0 |
0 |
y |
sin(θ/2) |
Math.sin(halfAngle) |
z |
0 |
0 |
注意:如果希望绕 X 轴(上下旋转),将
x设为sin(halfAngle),y设为0;绕 Z 轴同理。
3.3 触发渲染更新
应用旋转后,调用 renderFrame 通知渲染引擎刷新画面:
private requestRenderFrame(): void {
if (this.activeScene === null) {
return;
}
const params: RenderParameters = {
alwaysRender: true
};
this.activeScene.renderFrame(params);
}
4. 按钮步进旋转与复位
除了拖拽,还提供了按钮操作方式,实现固定角度的步进旋转和一键复位。
4.1 步进旋转
private rotateByStep(deltaAngle: number): void {
this.rotationAngle += deltaAngle;
this.applyModelRotation();
this.status = '已旋转:' + Math.round(this.rotationAngle * 57.3).toString() + '°';
}
按钮调用示例(每次旋转约 20°):
// 左转 -0.35 弧度(约 -20°)
this.rotateByStep(-0.35);
// 右转 +0.35 弧度(约 +20°)
this.rotateByStep(0.35);
4.2 复位
private resetRotation(): void {
this.rotationAngle = 0;
this.applyModelRotation();
this.status = '视角已复位';
}
4.3 按钮 UI(示意代码)
Row() {
Button('左转').onClick(() => { this.rotateByStep(-0.35); })
Button('复位').onClick(() => { this.resetRotation(); })
Button('右转').onClick(() => { this.rotateByStep(0.35); })
}
5. 交互流程图
┌─────────────────────────────────────────┐
│ 交互流程总览 │
├─────────────────────────────────────────┤
│ │
│ 手指拖拽: │
│ ┌─────────┐ ┌──────────────┐ │
│ │ Down │ → │ 记录起始状态 │ │
│ └─────────┘ └──────────────┘ │
│ ┌─────────┐ ┌──────────────┐ │
│ │ Move │ → │ deltaX * 0.01│ │
│ └─────────┘ └──────┬───────┘ │
│ ▼ │
│ ┌──────────────────────────────────┐ │
│ │ rotationAngle = start + deltaX │ │
│ └──────────────┬───────────────────┘ │
│ ▼ │
│ ┌──────────────────────────────────┐ │
│ │ applyModelRotation() │ │
│ │ → Quaternion(0, sin(θ/2), 0, │ │
│ │ cos(θ/2)) │ │
│ │ → modelRoot.rotation = Q │ │
│ │ → renderFrame() │ │
│ └──────────────────────────────────┘ │
│ │
│ 按钮步进: │
│ rotateByStep(deltaAngle) │
│ → rotationAngle += deltaAngle │
│ → applyModelRotation() │
│ │
│ 复位: │
│ resetRotation() │
│ → rotationAngle = 0 │
│ → applyModelRotation() │
│ │
└─────────────────────────────────────────┘
6. 关键参数调优指南
| 参数 | 作用 | 推荐值范围 | 调大效果 |
|---|---|---|---|
deltaX * 0.01 |
触摸灵敏度 | 0.005 ~ 0.02 | 滑动更灵敏,旋转更快 |
rotateByStep(-0.35) |
步进角度 | 0.17 ~ 0.70 | 每次旋转角度更大 |
Quaternion.y = sin(θ/2) |
旋转轴 | 设为 1 为绕 Y 轴 |
改为 X 轴实现上下旋转 |
小结
本篇我们实现了 3D 模型的交互式旋转:
- onTouch 手势监听:通过
TouchType.Down记录起始状态,TouchType.Move计算位移并更新角度。 - 触摸坐标 → 旋转角度:
deltaX * 0.01实现屏幕像素到弧度值的归一化转换。 - 四元数旋转:构建绕 Y 轴的
Quaternion,设置到Node.rotation完成旋转变换。 - 角度累加与状态保存:通过
rotationAngle变量维持旋转状态,支持增量旋转。 - 按钮步进与复位:提供精确控制和一键还原能力。
- renderFrame 渲染更新:每次旋转后主动触发重绘。
拖拽交互让用户能够自由观察模型的每一个角度,是 3D 查看器最基础也是最重要的交互方式之一。
下一篇预告:第2.9篇:视频导出与本地保存——DocumentViewPicker
更多推荐


所有评论(0)