第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 提供的基础手势事件接口,可以监听 DownMoveUp 等触摸事件。

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 模型的交互式旋转:

  1. onTouch 手势监听:通过 TouchType.Down 记录起始状态,TouchType.Move 计算位移并更新角度。
  2. 触摸坐标 → 旋转角度deltaX * 0.01 实现屏幕像素到弧度值的归一化转换。
  3. 四元数旋转:构建绕 Y 轴的 Quaternion,设置到 Node.rotation 完成旋转变换。
  4. 角度累加与状态保存:通过 rotationAngle 变量维持旋转状态,支持增量旋转。
  5. 按钮步进与复位:提供精确控制和一键还原能力。
  6. renderFrame 渲染更新:每次旋转后主动触发重绘。

拖拽交互让用户能够自由观察模型的每一个角度,是 3D 查看器最基础也是最重要的交互方式之一。


下一篇预告:第2.9篇:视频导出与本地保存——DocumentViewPicker

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐