结合你整套 Cesium 轴拖拽 + 旋转圈拖拽 代码,我把里面用到的全部核心数学知识,按「概念→几何意义→代码对应→为什么这么算」完整讲一遍,不讲虚的,全部和代码一一绑定,学完就能彻底看懂每一行向量 / 矩阵运算的来历。
一、先统一基础:空间点与向量(Cesium 里一切的基础)
1. 点 vs 向量
- 点
Cartesian3:空间里一个固定位置,如center、currentPoint。 - 向量
Cartesian3:有方向、有长度的箭头,描述 "从哪指向哪"。
在 Cesium 里共用同一个类 Cartesian3,但意义不同:
p:点a - b:从点b指向点a的向量
2. 最基础运算(代码里到处都是)
(1)向量减法:两点之间的向量
// 从 center 指向 currentPoint 的向量
const v = Cesium.Cartesian3.subtract(currentPoint, center, new Cesium.Cartesian3());
- 数学:v=Pcurrent−Pcenter
- 几何:从中心点拉一条箭头到当前点
(2)向量加法:点的平移
Cesium.Cartesian3.add(p, translation, result)
- 数学:Pnew=Pold+t
- 几何:把点 Pold 沿着 t 方向移动一段距离
(3)数乘:缩放向量长度
Cesium.Cartesian3.multiplyByScalar(axis, delta, result)
- 数学:vnew=k⋅vold
- 几何:保持方向不变,把向量拉长 / 缩短
k倍
二、点乘(Dot Product)------ 你代码里的 "投影神器"
点乘是你轴拖拽的灵魂,所有 "只沿轴移动" 都靠它。
1. 数学定义
对两个三维向量 a=(ax,ay,az)、b=(bx,by,bz):a⋅b=axbx+ayby+azbz
2. 几何意义(最关键)
a⋅b=∥a∥∥b∥cosθ
- θ:两向量夹角
- ∥a∥:向量长度(模)
两个极端情况:
- 两向量同向平行:θ=0, cosθ=1 → 点乘 = 长度乘积
- 两向量垂直:θ=90∘, cosθ=0 → 点乘 = 0
3. 代码里的核心用法:投影
当 b 是单位向量 (长度 = 1,你代码里都做了 normalize):a⋅b=向量 a 在 b 方向上的投影长度
对应你代码:
const currentScalar = Cesium.Cartesian3.dot(this.dragAxis, v);
dragAxis:单位化轴向量(东 / 北 / 天)v:从中心到当前点的向量currentScalar:v 在轴方向上的投影长度→ 也就是「只看这个轴方向,偏移了多少米」
这就是为什么拖 Z 轴只会改高度,X/Y 分量被完全过滤掉。
4. 另一个用途:求夹角余弦
const cos = Cesium.Cartesian3.dot(this.dragStartVector, currentVector);
- 两个都是单位向量
- 直接得到夹角的 cosθ,用于后面算旋转角度
三、叉乘(Cross Product)------ 你旋转圈的灵魂
叉乘专门用来:
- 求垂直于两个向量的新方向
- 计算旋转方向(顺时针 / 逆时针)
- 构造平面法向量
1. 数学定义
a×b=aybz−azbyazbx−axbzaxby−aybx
2. 几何意义
- 结果向量 a×b 同时垂直于 a 和 b
- 长度:∥a×b∥=∥a∥∥b∥sinθ
- 方向满足右手定则,决定旋转正负方向。
3. 你代码里的两大用途
(1)构造约束平面的法向量(轴拖拽)
let normal = Cesium.Cartesian3.cross(axis, cameraDir, result);
- 想要平面包含轴 axis,且尽量面向相机
- 平面法向量必须同时垂直轴和相机方向 → 只能用叉乘
(2)求旋转方向与正弦值(旋转圈)
const cross = Cesium.Cartesian3.cross(this.dragStartVector, currentVector, result);
const sin = Cesium.Cartesian3.dot(this.dragAxis, cross);
cross垂直于起始向量和当前向量- 再和旋转轴点乘,得到 sinθ
- 同时携带正负号,表示顺时针 / 逆时针
四、单位向量与归一化
Cesium.Cartesian3.normalize(v, v);
数学:v^=∥v∥v,∥v^∥=1
为什么你代码里处处归一化?
- 点乘结果直接 = 投影长度,不用再除以长度
- 旋转角度计算只关心方向,不关心长度
- 平面、矩阵计算不会引入缩放误差
你代码里
getAxisVector、getAxisDragPlane、prepareRingDrag全都做了这一步,是工程上必须的规范。
五、平面方程与射线 - 平面求交(拖拽 "不飘" 的数学保证)
1. 平面一般方程
Ax+By+Cz+D=0
- n=(A,B,C):单位法向量
- 平面上任意一点 (x,y,z) 都满足方程
2. Cesium.Plane 构造
new Cesium.Plane(normal, D);
代码里:
return new Cesium.Plane(normal, -Cesium.Cartesian3.dot(normal, center));
- 令 D=−n⋅Pcenter
- 代入平面方程:n⋅P−n⋅Pcenter=0⇒n⋅(P−Pcenter)=0
- 几何意义:平面必定经过 center 点
3. 射线 - 平面求交
const currentPoint = Cesium.IntersectionTests.rayPlane(ray, plane);
- 射线:从相机出发,穿过鼠标屏幕点的一条直线
- 求它和约束平面的交点,就把鼠标的 "屏幕 2D 运动" 映射成 "平面上的 3D 点"
- 这一步保证:所有拖拽都被限制在平面内,不会乱跑
六、旋转全套数学:从轴角 → 四元数 → 矩阵
你旋转代码是标准、工程化最强的 3D 旋转写法,我把链条完整拆开。
1. 旋转描述方式:轴角表示
绕单位向量 u 旋转 θ 角,这是最直观的旋转描述。
对应代码:
const q = Cesium.Quaternion.fromAxisAngle(axis, angle);
2. 为什么用四元数?
- 避免万向锁(Gimbal Lock):欧拉角会出现旋转自由度丢失
- 插值平滑、数值稳定
- Cesium 内部大量使用四元数
3. 四元数转旋转矩阵
const m = Cesium.Matrix3.fromQuaternion(q);
- 四元数便于计算与插值
- 矩阵便于批量点旋转
4. 空间点绕任意轴旋转的标准三步法(你代码的核心)
对任意点 P,绕轴 u、中心 C 旋转 θ:
第 1 步:平移到局部原点(以中心为原点)
Plocal=P−C
const local = Cesium.Cartesian3.subtract(p, center, res);
第 2 步:绕原点旋转
Prot=M⋅Plocal
const rotated = Cesium.Matrix3.multiplyByVector(m, local, res);
第 3 步:平移回世界中心
Pnew=Prot+C
return Cesium.Cartesian3.add(rotated, center, res);
这就是 applyRotation 里 rotatePoint 函数的完整数学含义。
七、旋转角度计算:atan2(sin, cos) 完整版数学
你旋转圈角度计算是图形学标准写法,很多人只会抄不懂原理,这里彻底拆开:
已知:
- v0:起始方向(单位)
- v1:当前方向(单位)
- u:旋转轴(单位)
1. 求 cos
cosθ=v0⋅v1
const cos = Cesium.Cartesian3.dot(this.dragStartVector, currentVector);
2. 求 sin(带方向)
v0×v1=usinθ再点乘旋转轴:sinθ=u⋅(v0×v1)
const cross = Cesium.Cartesian3.cross(v0, v1, res);
const sin = Cesium.Cartesian3.dot(axis, cross);
3. 求带符号的真实角度
θ=atan2(sinθ, cosθ)
const angle = Math.atan2(sin, cos);
意义:
- 同时得到角度大小 和旋转方向(正负)
- 范围:−π∼π,完美对应拖拽顺时针 / 逆时针
- 这是 3D 旋转交互里最稳健、无歧义的角度计算方式
八、ENU 坐标系数学(为什么不用世界坐标系)
1. ENU:East-North-Up
以地面点 P 为原点:
- E(X):切平面内向东
- N(Y):切平面内向北
- U(Z):沿椭球法向量向上(垂直当地地面)
2. 变换矩阵
const enuTransform = Cesium.Transforms.eastNorthUpToFixedFrame(center);
这是一个 4x4 齐次变换矩阵,含义:
- 列 0:E 方向单位向量
- 列 1:N 方向单位向量
- 列 2:U 方向单位向量
- 列 3:平移分量(原点 center)
3. 提取轴向量
const east = Cesium.Matrix4.getColumn(enuTransform, 0, res); // X
const north = Cesium.Matrix4.getColumn(enuTransform, 1, res); // Y
const up = Cesium.Matrix4.getColumn(enuTransform, 2, res); // Z
这就是 getAxisVector 的全部数学来源。
九、把数学串回你整套代码(一张总表)
| 代码片段 | 对应数学 | 作用 |
|---|---|---|
subtract(a,b) |
v=a−b | 两点间向量 |
dot(axis, v) |
投影长度 | 提取沿轴偏移,过滤其他方向 |
cross(a,b) |
垂直于 a、b 的向量 | 构造平面法向量、求旋转方向 |
normalize(v) |
单位化 | 点乘 = 投影、角度计算无缩放误差 |
Plane(normal, D) |
平面方程 | 约束拖拽范围 |
rayPlane |
射线与平面求交 | 鼠标 2D→平面上 3D 点 |
fromAxisAngle |
轴角→四元数 | 描述绕轴旋转 |
Matrix3.multiplyByVector |
矩阵 × 向量 | 点旋转 |
atan2(sin,cos) |
带符号夹角 | 旋转角度与方向 |
eastNorthUpToFixedFrame |
ENU 局部坐标系 | 贴合地形的轴方向 |