《HarmonyOS 7 ArkGraphics 3D 空间设计开发实战》05:Camera控制、手势映射与空间浏览交互【鸿蒙心迹】

手指拖动200像素,相机为什么不是在世界里移动200个单位?

前言

前面四篇,3D 房间已经能看了:有家具、有材质、有灯光。但只能从一个固定视角看,用户没法自己转一转、看一看。

这一篇开始做交互:单指拖动转视角,双指缩放,双指平移。看起来是很常见的手势,但做起来有个很容易绕晕的问题:

ArkUI 手势给的是二维屏幕坐标,像素单位。Camera 在三维世界里工作,单位是世界坐标。手指拖动 200 像素,不是相机在世界里移动 200 个单位------中间需要一层映射。

很多人刚开始做的时候,直接把手势 delta 加到相机位置上,结果就是:手指动一点,相机飞出去十万八千里,或者手指拖半天相机纹丝不动。

这一篇就解决这个问题:怎么把二维手势输入,映射成三维空间里的相机旋转、缩放和平移。


一、先搞清楚:手势给的是什么,相机需要的是什么

先把两边的输入输出列清楚:

来源 给的是什么 单位
ArkUI 手势 手指在屏幕上移动了多少像素 屏幕像素(x, y)
Camera 需要的 绕 Y 轴转了多少度(Yaw)、绕 X 轴转了多少度(Pitch)、离目标点多远(Distance) 角度、世界单位

这两个东西量纲都不一样,不能直接加。需要一个转换系数:屏幕上拖多少像素,对应转多少度。

举个例子:如果用户手指在屏幕上从左到右拖满整个屏幕宽度(比如 1080 像素),我们希望相机正好绕 Y 轴转 360 度。那转换系数就是 360 / 1080 = 0.333 度每像素。

也就是说,手指拖 100 像素,相机就转 33.3 度。

这个系数不是固定的,可以根据手感调整。有人喜欢灵敏一点,有人喜欢稳一点。


二、写一个 CameraController,封装视角逻辑

先写 CameraController,把相机的状态和操作都封装进去:

typescript 复制代码
import { scene } from '@kit.ArkGraphics3DKit';

export class CameraController {
  // 相机状态
  private yaw: number = 0;        // 绕Y轴旋转角度
  private pitch: number = -30;    // 绕X轴俯仰角度
  private distance: number = 8;   // 离目标点的距离
  private target: {x: number, y: number, z: number} = {x: 0, y: 0.5, z: 0};  // 观察目标点

  // 手势灵敏度
  private rotateSensitivity: number = 0.3;  // 每像素对应多少度
  private zoomSensitivity: number = 0.01;   // 每像素对应缩放多少

  private cameraNode: scene.Node | null = null;

  constructor(private scene: scene.Scene) {}

  /**
   * 初始化相机
   */
  setupCamera(): void {
    this.cameraNode = this.scene.createNode('MainCamera');
    const camera = this.cameraNode.createComponent(scene.NodeComponentType.CAMERA) as scene.Camera;

    // 更新相机位置
    this.updateCameraTransform();

    this.scene.getRoot().addChild(this.cameraNode);
    console.info('CameraController: camera ready');
  }

  /**
   * 根据当前状态计算相机位置
   */
  private updateCameraTransform(): void {
    if (!this.cameraNode) return;

    // 球坐标转直角坐标:根据yaw、pitch、distance算相机位置
    const radYaw = this.yaw * Math.PI / 180;
    const radPitch = this.pitch * Math.PI / 180;

    const x = this.target.x + this.distance * Math.sin(radYaw) * Math.cos(radPitch);
    const y = this.target.y + this.distance * Math.sin(radPitch);
    const z = this.target.z + this.distance * Math.cos(radYaw) * Math.cos(radPitch);

    this.cameraNode.position = {x, y, z};

    // 相机始终看向目标点
    const camera = this.cameraNode.getComponent(scene.NodeComponentType.CAMERA) as scene.Camera;
    camera.lookAt = this.target;
  }

  /**
   * 旋转视角(单指拖动)
   */
  rotate(deltaX: number, deltaY: number): void {
    // deltaX 控制左右旋转(yaw)
    this.yaw += deltaX * this.rotateSensitivity;
    // deltaY 控制上下俯仰(pitch)
    this.pitch += deltaY * this.rotateSensitivity;

    // 限制俯仰角度,不要转到天上或地下
    this.pitch = Math.max(-80, Math.min(80, this.pitch));

    this.updateCameraTransform();
  }

  /**
   * 缩放(双指捏合)
   */
  zoom(delta: number): void {
    this.distance -= delta * this.zoomSensitivity;
    // 限制距离,不要太近也不要太远
    this.distance = Math.max(2, Math.min(20, this.distance));

    this.updateCameraTransform();
  }
}

核心逻辑就在 updateCameraTransform() 里:用 yaw(左右角度)、pitch(俯仰角度)、distance(距离)这三个参数,算出相机在空间里的位置,然后让相机看向目标点。

这就是轨道相机(Orbit Camera)的基本模型:相机绕着一个目标点转,不是相机自己乱飞。


三、把 ArkUI 手势接到 CameraController 上

CameraController 写好了,接下来接 ArkUI 的手势:

typescript 复制代码
@Entry
@Component
struct SpaceRoomPage {
  private cameraCtrl: CameraController | null = null;

  build() {
    Stack() {
      scene.SceneView({ scene: undefined })
        .width('100%')
        .height('100%')
        .gesture(
          // 单指拖动旋转
          PanGesture()
            .onActionUpdate((event: GestureEvent) => {
              this.cameraCtrl?.rotate(event.offsetX, event.offsetY);
            })
        )
        .gesture(
          // 双指捏合缩放
          PinchGesture()
            .onActionUpdate((event: GestureEvent) => {
              // 双指距离变化来控制缩放
              this.cameraCtrl?.zoom(event.scale * 10);
            })
        )
    }
    .width('100%')
    .height('100%')
  }
}

这里有个坑:PanGesture 的 event.offsetX 是本次手势开始到现在的总偏移,不是每次回调的增量。

也就是说,手指从左边拖到右边,拖了 500 像素,第一次回调 offsetX 是 10,第二次是 30,第三次是 60......不是每次给你这次动了多少,而是从手势开始到现在累计动了多少。

所以正确的做法是记录上一次的位置,每次算增量:

typescript 复制代码
private lastX: number = 0;
private lastY: number = 0;

// onActionStart 的时候记录初始位置
.onActionStart(() => {
  this.lastX = 0;
  this.lastY = 0;
})

.onActionUpdate((event: GestureEvent) => {
  // 本次增量 = 本次总偏移 - 上次总偏移
  const deltaX = event.offsetX - this.lastX;
  const deltaY = event.offsetY - this.lastY;

  this.cameraCtrl?.rotate(deltaX, deltaY);

  // 更新上次位置
  this.lastX = event.offsetX;
  this.lastY = event.offsetY;
})

这个坑特别常见。我第一次做的时候,直接把 event.offsetX 加进去,结果就是:手指刚拖一下,相机瞬间就转了 180 度,因为 offsetX 是从手势开始累计的,不是增量。


四、加边界限制:不要让相机穿墙

开放的 3D 场景,相机是可以随便跑的。但我们这个是房间内部,相机不能穿墙,不能转到房间外面去。

两个限制:

  1. 俯仰角不要太极端,不要转到天花板上面或者地板下面
  2. 距离不要太近,不要贴到家具里面

刚才代码里已经加了:

typescript 复制代码
// 限制俯仰角度在 -80 到 80 度之间
this.pitch = Math.max(-80, Math.min(80, this.pitch));

// 限制距离在 2 到 20 之间
this.distance = Math.max(2, Math.min(20, this.distance));

这样用户再怎么拖,相机也不会飞到离谱的位置。


五、几个最容易踩的交互坑

把这一篇遇到的坑总结一下:

坑 现象 解决办法
手势坐标直接当世界坐标 手指动一点相机飞出去 加灵敏度系数,像素转角度
PanGesture 用了累计偏移不是增量 刚开始拖一下相机转半圈 记录上次位置,每次算增量
没有俯仰角限制 相机转到天上或者地下 pitch 限制在 -80 到 80 度
没有距离限制 缩放太近贴到模型里,太远看不到 distance 限制在 2 到 20
拖动方向反了 手指向右拖,场景向左转 deltaX 正负号调整一下
手势太灵敏或者太迟钝 手感不对 调整 rotateSensitivity 参数

这些问题都不是技术上做不到,是手感调优。3D 交互的难点从来不是"能不能转",而是"转起来是不是自然"。


总结

第五篇的核心就一句话:二维手势和三维相机之间,必须有一层映射。

  1. ArkUI 手势给的是屏幕像素,相机需要的是角度和距离,不能直接加
  2. 用轨道相机模型:yaw(左右转)、pitch(上下俯仰)、distance(距离)三个参数控制相机
  3. PanGesture 给的是累计偏移,要自己算每次的增量
  4. 加边界限制:俯仰角、距离都要有上下限,不然相机乱飞
  5. 灵敏度参数根据手感调,没有标准答案

SpaceRoom 现在用户可以自己转着看了。下一篇做更有意思的交互:用户点一下沙发,能选中它、移动它、删除它------也就是 3D 拾取。

相关推荐
沫璃染墨1 小时前
《Qt从零入门系列(十一):Qt事件机制详解——从QEvent到鼠标、键盘与定时器事件》
c++·qt·ui·硬件工程·交互·个人开发·qt5
resh_people2 小时前
开源鸿蒙平台 KMP/CMP 三方库「Okio」适配全流程
华为·开源·harmonyos
3D阅阅三维模型轻量化及浏览平台2 小时前
非标设计提效:3D 阅图实现模型自检与出图的实战指南
3d·模具·模型比对·模型差异化·版本比对·机加工·审图
李游Leo2 小时前
《HarmonyOS 7 ArkGraphics 3D 空间设计开发实战》08:动画、生命周期与SpaceRoom工程化收尾【鸿蒙心迹】
3d·harmonyos
轻口味3 小时前
HarmonyOS 7 新特性2:音频编创——轻音台里的降噪、环绕与格式转换
华为·音视频·harmonyos·鸿蒙·音频编创
m0_738185823 小时前
Flutter 鸿蒙化实战:flutter_blue_plus 适配 OpenHarmony,蓝牙扫描连接开箱即用
flutter·华为·harmonyos·鸿蒙
李游Leo3 小时前
HarmonyOS 7 + ArkUI + Adaptive Layout 学习笔记:折叠屏多形态布局适配与窗口状态响应机制【鸿蒙心迹】
笔记·学习·harmonyos
贾伟康4 小时前
【HarmonyOS 7新能力|067】LTPO可变帧率实战:让动画流畅而静态页面省电
harmonyos·arkts·低功耗·ltpo·可变帧率
LucianaiB4 小时前
华为云码道 AI 编程实战:我用 CodeArts 智能体打造了一款 HarmonyOS 游戏化专注应用「智办 ZhiBan」
人工智能·ai·华为云·harmonyos·codearts·材料