前面章节我们学习了控制模型旋转的各种算法,所展示的效果都是通过程序设定的,实际上,我们往往需要人为的通过鼠标、触摸等方式实现对模型的旋转、位移等变换的控制。本节我们利用前面的知识实现用鼠标或者触摸的方式对模型进行控制。
原理分析
利用鼠标控制模型旋转的本质是求出鼠标在按下后并进行移动的轨迹,如何对应到 3D 空间中的旋转矩阵。
鼠标滑动
通常情况下,我们会在鼠标按下后,捕捉起始位置P0,由于屏幕是 2D 的,所以我们只能捕捉到屏幕的 X、Y 轴的坐标信息,所以我们的 P0
是一般是这样的:
var start = {x: 30, y: 50};
当鼠标滑动或者触摸 move 时,我们能够监听到鼠标或者触摸点在屏幕的当前坐标:
var current = {x: 300, y: 200};
我们拿到了鼠标滑动的轨迹坐标,那么这两个坐标值能不能和模型的旋转进行对应呢?如果可以对应,这中间的对应模型该如何建立呢?
我们从实际场景分析一下:
- 当鼠标向右或者向左做水平滑动时,我们期望的是能够让模型向右或者向左旋转,也就是绕 Y 轴旋转。
- 当鼠标向上或者向下做垂直滑动时,我们期望的是能够让模型向上或者向下旋转,也就是绕 X 轴旋转。
- 当鼠标做上述两种滑动时,滑动的距离与旋转的角度成正比,即距离越大,角度越大。
上面是三个很重要的建模依据。
滑动距离与旋转的映射关系
通过之前章节的学习,我们知道旋转的表示有如下三种:
- 欧拉角。
- 四元数。
- 轴角。
那么,按照上面的建模思路,能够直观的表示角度的只有欧拉角和轴角。
欧拉角旋转
我们先使用最直观的欧拉角来看下如何将鼠标的移动关联到模型的旋转。
很容易想到的策略是:
- 鼠标沿着 X 轴移动一像素时,绕 Y 轴旋转 1 度。
- 鼠标沿着 Y 轴移动一像素时,绕 X 轴旋转 1 度。
按照这个策略实现一下,看看是否符合我们的期望。
首先定义一个旋转矩阵currentMatrix
,用于保存模型渲染时采用的变换矩阵。 接着定义一个临时矩阵tempMatrix
,用于保存鼠标滑动时的临时矩阵。 最后定义一个最近一次的变换矩阵lastMatrix
,用于保存上一次的变换。
var currentMatrix = matrix.identity();
var tempMatrix = matrix.identity();
var lastMatrix = matrix.identity();
其次,我们需要监听鼠标或者触摸屏的坐标。
// 判断是否支持触摸事件。
var supportTouchEvent = 'ontouchstart' in window;
var dragStartEvent = supportTouchEvent? 'touchstart': 'mousedown';
var dragMoveEvent = supportTouchEvent? 'touchmove': 'mousemove';
var dragEndEvent = supportTouchEvent? 'touchend': 'mouseup';
var startX = 0, startY = 0, currentX = 0, currentY = 0;
//绑定拖拽开始事件
document.body.addEventListener(dragStartEvent, function dragStart(e){
e = supportTouchEvent ? e.changedTouches[0] : e;
startX = e.clientX;
startY = e.clientY;
});
// 绑定拖拽事件
document.body.addEventListener(dragMoveEvent, function dragMove(e){
e = supportTouchEvent ? e.changedTouches[0] : e;
currentX = e.clientX;
currentY = e.clientY;
rotate();
});
// 绑定拖拽结束事件
document.body.addEventListener(dragEndEvent, function dragEnd(e){
matrix.clone(currentMatrix, lastMatrix);
});
在拖拽结束事件中,我们将currentMatrix 矩阵拷贝给 lastMatrix。
你还会发现在拖拽事件中,我们执行了一个函数 rotate
,这个函数的作用是根据起始坐标与当前坐标,算出旋转矩阵,我们看下如何实现这个函数。
var euler = {x:0, y:0, z:0};
var radian = Math.PI / 180;
function rotate(){
var dx = currentX - startX;
var dy = currentY - startY;
euler.x = dy * radian;
euler.y = dx * radian;
// 本次拖拽的临时矩阵
tempMatrix = matrix.getMatrixFromEuler(euler, tempMatrix);
// 最近一次变换矩阵与临时矩阵的乘积,得出最终渲染矩阵。
currentMatrix = matrix.multiply(tempMatrix, lastMatrix);
render();
}
看下效果吧:
可能你会觉得旋转的灵敏度太大了,那我们可以定义一个系数,用来设置鼠标移动距离与旋转角度的比例。
var rate = 0.6;
euler.x = rate * dy * radian;
euler.y = rate * dx * radian;
大家可以根据自己的需要调整这个系数,直到符合自己的直觉为止。
看到这里,你会不会觉得太简单了。是的,当你掌握了之前章节的内容之后,写这个交互确实是很简单,无非就是和矩阵打交道。
接下来,我们看一下如何利用轴角的方式实现鼠标移动距离和旋转角度之间的映射关系。
轴角
轴角轴角,肯定要有一个旋转轴,外加旋转角度。我们就是要根据在 X 轴和 Y 轴的移动距离,找出对应的旋转轴和旋转角度。
我们看分别看下鼠标运动时的旋转轴。
- 沿着 X 轴移动时。
下图中 $\vec{P_0P_1}$ 为鼠标移动轨迹,$\vec{P_0R_1}$ 是对应的旋转轴,很容易看出旋转轴和移动轨迹垂直,为 Y 轴上的单位向量。
- 沿着 Y 轴移动时,旋转轴为 X 轴方向的单位向量。
- 沿着 X 轴和 Y 轴移动时。
当同时沿着 X 轴和 Y 轴移动时,轴向量和运动轨迹垂直,如上图 $\vec{P_0R_1}$,通过分解,我们可以求出旋转轴向量。
- X 轴分量为:
$ \vec{rx} = \frac{dy} {|\vec{P_0R_1}|} $
- Y 轴分量为:
$ \vec{ry} = \frac{dx}{ |\vec{P_0R_1}|} $
- Z 轴分量为0:
$ \vec{rz} = 0 $
其中$\vec{P_0R_1}$为鼠标移动的距离。
既然有了轴向量,我们还需要绕轴向量旋转的角度θ。旋转角度的选取也是一个经验值,在此我们以$\vec{P_0R_1}$的长度作为旋转的角度,大家可以根据自己的感觉适当调整。
有了轴向量和旋转角度,记下来就可以计算旋转矩阵了:
var l = Math.sqrt(dx * dx + dy * dy);
if(l <= 0)return;
var x = dx / l, y = dy / l;
var axis = {x:x, y:y, z:0};
tempMatrix = matrix.axisRotation(axis, l);
currentMatrix = matrix.multiply(tempMatrix, lastMatrix, currentMatrix);
render();
我们看下效果:
可见,利用轴角和欧拉角都能够实现利用鼠标控制模型的旋转。
那么,观察上面这个轴角计算方式,你会发现,我们在计算 tempMatrix 和 currentMatrix 的时候,计算量比较多,能不能优化一下呢?
嗯,我们可以采用四元数,利用四元数相乘来取代矩阵相乘,毕竟四元数的乘法运算量比矩阵乘法运算 量要小。
四元数
通过前面章节的学习,我们知道四元数可以由轴角转化而成,利用下面的公式:
$ \begin{aligned} q &= |q|[cos\theta, \vec{n}\cdot sin\theta] \end{aligned}
$
其中 $\vec{n}$ 为轴向量,$\theta$为旋转角度的一半。
根据这个公式,我们可以很方便的将轴角转化为四元数。
function fromAxisAndAngle(axis, angle, target){
let halfAngle = angle / 2,
s = Math.sin(halfAngle);
target = target || {};
target.x = axis.x * s;
target.y = axis.y * s;
target.z = axis.z * s;
target.w = Math.cos(halfAngle);
return target;
}
有了轴角转化为四元数的方法,改造一下 rotate 方法。
var currentQ = {x:0, y:0, z:0, w:1};
var lastQ = {x:0, y:0, z:0, w:1};
var l = Math.sqrt(dx * dx + dy * dy);
if(l <= 0)return;
var x = dx / l, y = dy / l;
var axis = {x:x, y:y, z:0};
var q = matrix.fromAxisAndAngle(axis, l);
currentQ = matrix.multiplyQuaternions(q, lastQ);
currentMatrix = matrix.makeRotationFromQuaternion(currentQ);
render();
当然拖拽结束事件我们也要修改一下,我们将不再保存上一次的旋转矩阵 lastMatrix,而是保存上一次的旋转四元数 lastQ。
// 绑定拖拽结束事件
document.body.addEventListener(dragEndEvent, function dragEnd(e){
Object.assign(lastQ, currentQ);
});
改造完毕,旋转控制效果和欧拉角与轴角一致,但是计算量和存储量少了很多。
回顾
本节我们学习了利用欧拉角、四元数、轴角对模型进行控制的原理,学完之后,你会发现只要考虑好映射模型,其余的就很简单了,无非就是对四元数或者矩阵的操作。
接下来,我们先搁置 WebGL 的学习,探讨 CSS 中的 3D 属性以及如何将数学算法应用到 CSS 的 3D 属性中。