3D数学基础 第二章:向量/矢量(续)

Drag

计算Collider到中心的偏移量

  1. 首先3D应用不同于2D,多了一个方向之后很多情况都不太一样,需要考虑的更细致一点
  2. 当拖拽开始时,计算并记录一个初始的偏移量 mBeginDrag,用于后续在拖拽过程中计算物体位置的变化。计算方法是将物体的世界位置减去碰撞器的世界位置,这个偏移量表示物体初始位置与碰撞点的偏移
  3. 获取当前碰撞器的世界位置 worldPos
  4. worldPos 与初始偏移量 mBeginDrag 相加,得到更新后的物体位置 updatePos
    1
    const updatePos = glvec3.add([], worldPos, this.mBeginDrag) as vec3;
  5. 更新物体位置

手柄到物体的距离固定 防止物体远离

  1. onDragBegin标志位,记录设备位置与碰撞点位置之间的距离 mDistance,以及碰撞盒(Collider)到物体中心点的方向向量 mDiffVec3

    1. 只有在 Begin设置 isDrag为true,让物体在这一帧的 update更新坐标
    1
    2
    3
    4
    5
    6
    7
    8
    const deviceObj = EventSystem.current().getInputDeviceObject(args.device());

    this.mDistance = glm.vec3.distance(deviceObj.worldPosition(), collider.worldPosition);
    this.mDiffVec3 = glm.vec3.subtract(
    [],
    collider.worldPosition,
    this.mSceneObj.worldPosition(),
    ) as vec3;
  2. 使用输入设备的旋转信息将初始方向向量 [0, 0, -1] 转换为世界坐标系中的方向向量。这个方向向量表示了物体应该移动的方向。

    1
    const dir = glm.vec3.transformQuat([], [0, 0, -1], deviceRotation);
  3. 计算更新后的物体位置。首先,将设备位置和方向向量的缩放(根据 mDistance)相加,以计算出物体在输入设备方向上移动的距离。然后,从这个总的位置变量中减去之前计算得到的差向量 mDiffVec3,这样就得到了物体的新位置。

    1. glm.vec3.scale([], dir, this.mDistance): 这部分代码使用 glm.vec3.scale 函数,将方向向量 dir 缩放(按照 this.mDistance 的大小),以计算在输入设备方向上移动的距离。这里的 dir 是表示拖拽方向的一个单位向量。
    2. glm.vec3.add([], devicePosition, ...):这部分代码将 devicePosition(输入设备的位置)和上一步中计算得到的移动距离相加。这样,我们得到了物体应该移动到的位置。
    3. glm.vec3.subtract([], ..., this.mDiffVec3):最后,这部分代码使用 glm.vec3.subtract 函数,将上一步计算得到的新位置和之前计算得到的差向量 this.mDiffVec3 相减。这个差向量表示了物体相对于拖拽点的初始偏移。通过将新位置和差向量相减,我们就得到了物体的最终更新位置。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      const updatePosition = glm.vec3.subtract(
      [],
      glm.vec3.add(
      [],
      devicePosition,
      glm.vec3.scale([], dir, this.mDistance),
      ),
      this.mDiffVec3,
      ) as vec3;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
import { UIObject } from '@irisview/components';
import {
SceneObject,
SphereCollider,
Interactable,
DragEventArgs,
DragEventType,
vec3,
EventSystem,
InputDevice,
} from '@irisview/engine';
import * as glm from 'gl-matrix-ts';
import { vec3type } from 'gl-matrix-ts/dist/common';

class Drag {
private node:UIObject;

private mSceneObj: SceneObject;

private mDistance: number;

private mDiffVec3:vec3type;

private mDevice:null | InputDevice;

private radius: number;

private isDrag:boolean;

constructor() {
this.radius = 2.5;
this.isDrag = false;
}

init(node: UIObject) {
this.node = node;
this.node.on('update', () => { this.initUpdate(); });
this.mSceneObj = node.getSceneObject();
this.initListener();
}

private initListener() {
SphereCollider.create(this.mSceneObj, { radius: this.radius });
const interat = Interactable.create(this.mSceneObj);
interat.OnDrag.add((_, args: DragEventArgs) => {
try {
const collider = args.colliderResult();
if (!collider) {
return;
}
this.mDevice = args.device();
const eventType = args.eventType();
switch (eventType) {
case DragEventType.Begin: {
this.isDrag = true;
const deviceObj = EventSystem.current().getInputDeviceObject(args.device());

this.mDistance = glm.vec3.distance(deviceObj.worldPosition(), collider.worldPosition);
console.log('distance', this.mDistance, this.isDrag);

this.mDiffVec3 = glm.vec3.subtract(
[],
collider.worldPosition,
this.mSceneObj.worldPosition(),
) as vec3;
break;
}
case DragEventType.Update: {
break;
}
default:
this.isDrag = false;
break;
}
} catch (e) {
console.error(e);
}
});
}

initUpdate() {
if (this.isDrag) {
const deviceObj = EventSystem.current().getInputDeviceObject(this.mDevice);
const devicePosition = deviceObj.worldPosition();
const deviceRotation = deviceObj.worldRotation();
const dir = glm.vec3.transformQuat([], [0, 0, -1], deviceRotation);
const updatePosition = glm.vec3.subtract(
[],
glm.vec3.add(
[],
devicePosition,
glm.vec3.scale([], dir, this.mDistance),
),
this.mDiffVec3,
) as vec3;
this.mSceneObj.setWorldPosition(updatePosition);
}
}
}

export default new Drag();

Rotation

四元数进行旋转

  1. 监听 onDrag事件
  2. onDragBegin中获取当前元素的 localPosition记做 startPosition
  3. onDragUpdate/End中获取当前元素的 localPosition记做 endPosition
  4. 计算两个向量夹角的四元数表示形式
  5. 获取到上一帧的 rotation四元数
  6. 将两个四元数进行叠加得到新的四元数

优点很明显:四元数本身自带方向,不需要额外计算旋转方向;但是需要理解旋转四元数的概念

欧拉角进行旋转

  1. 监听 onDrag事件
  2. onDragBegin中获取当前元素的 localPosition记做 startPosition
  3. onDragUpdate/End中获取当前元素的 localPosition记做 endPosition
  4. 计算两个向量的点积得到两个向量的 cosθ然后反解出对应的角度 θ 从而计算出欧拉角
  5. 根据两个向量的叉积计算出旋转方向
  6. 拿到上一帧的 euler乘上计算出的旋转方向,进行更新

缺点很多:比如不能一次更新x,y,z三个轴的欧拉角,同时可能会有卡顿(亲测),而且需要计算旋转方向

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
import { Container, UIObject } from '@irisview/components';
import {
SceneObject,
SphereCollider,
Interactable,
DragEventArgs,
DragEventType,
vec3,
vec4,
} from '@irisview/engine';
import { vec3 as glVec3, quat } from 'gl-matrix-ts';
import { vec3type } from 'gl-matrix-ts/dist/common';

class CuteRotation {
private mHolder: Container;

private mSceneObj: SceneObject;

private mBeginPos: vec3type | null;

private radius: number;

constructor() {
this.mBeginPos = null;
this.radius = 12 / 0.065;
}

initLayout(node: UIObject) {
const parent = node.parent as UIObject;
this.mSceneObj = node.getSceneObject();
this.mHolder = new Container({
parent,
});
this.initListener();
}

private initListener() {
SphereCollider.create(this.mHolder.sceneObject, { radius: this.radius });
const interactable = Interactable.create(this.mHolder.sceneObject);
interactable.OnDrag.add(this.handleDragEvent);
}

private handleDragEvent = (_: any, args: DragEventArgs) => {
try {
const collider = args.colliderResult();
if (!collider) {
return;
}
const eventType = args.eventType();
if (eventType === DragEventType.Begin) {
this.mBeginPos = glVec3.clone(collider.localPosition);
} else if (eventType === DragEventType.Cancel) {
this.mBeginPos = null;
} else {
const result = this.rotateModel(collider.localPosition);
if (result) {
this.mBeginPos = glVec3.clone(collider.localPosition);
}
}
} catch (e) {
console.error(e);
}
};

private rotateModel(endPos: vec3): boolean {
if (!this.mBeginPos) {
return false;
}
// 标准化起始和终止方向向量
const startDirection = glVec3.normalize(glVec3.create(), this.mBeginPos);
const endDirection = glVec3.normalize(glVec3.create(), endPos);
// 计算旋转四元数
const nowQuat = quat.rotationTo(quat.create(), startDirection, endDirection);
const lastQuat = this.mSceneObj.rotation();
const updateQuat = quat.multiply(quat.create(), nowQuat, lastQuat);

this.mSceneObj.setRotation([...updateQuat] as vec4);
return true;
}
}

export default new Rotation();

3D数学基础 第二章:向量/矢量(续)
https://jing-jiu.github.io/jing-jiu/2023/08/26/notebooks/3D数学基础:图形和游戏开发/第二章:向量(续)/
作者
Jing-Jiu
发布于
2023年8月26日
许可协议