ThreeJS的DeviceOrientationControls源代码解析

https://github.com/newbieYoung/NewbieWebArticles/blob/master/webgl-threejs-DeviceOrientationControls.html(2017-06-04T08:54:28Z)

© Young 2017-06-02 11:04
Welcome to My GitHub

概述

在学习ThreeJS和WebGL的过程中想着实现一个基于手机陀螺仪的3D环绕视角的例子,遇到了很多困难,一直都没有完成;后来发现ThreeJS中已经有了类似的例子,在这个例子里边作者封装了一个叫作DeviceOrientationControls的控制器,专门处理基于手机陀螺仪的相机的方位变化,所以我就想仔细了解一下这个控制器的工作原理。

分析

使用方法

该控制器使用方法很简单;

  • 首先引入代码;

  • 然后通过相机构建;

  • 最后在渲染方法里边不断的调用该控制器的update方法即可;

核心代码

这个控制器代码量不多,只有短短的100行,逻辑看起来也很简单,只需要监听手机陀螺仪事件然后不断改变相机方位即可,核心代码如下:

42行和第50行初始化了一个Euler对象;

    var euler = new THREE.Euler();
    euler.set(beta,alpha,-gamma,'YXZ');//'ZXY' for the device, but 'YXZ' for us

Euler就是欧拉角,简单来说欧拉角的基本思想就是任何角位移都可以分解为绕三个互相垂直的轴的三个旋转组成,任意三个轴和任意顺序都可以,但最有意义的是使用笛卡尔坐标系(旋转物体自身的坐标系,而不是世界坐标系)并按一定顺序所组成的旋转序列;

手机陀螺仪的deviceorientation事件返回的数据,就是一个欧拉角位移数据;

alpha表示设备绕Z轴旋转的角度,范围为0~360;
beta表示设备绕X轴旋转的角度,范围为-180~180;
gamma表示设备绕Y轴旋转的角度,范围为-90~90;

这里存在几个问题:

其一:为什么每个角度的范围不一致?

首先得明确一点为什么要有范围,主要是因为如果没有范围,比如alpha为10和370其实是等价的,一个角位移存在多个描述会导致一些麻烦(可以思考下会有什么麻烦?);再来就是为什么不同方向的角位移限制不一致,其实原因还是基于上述理由,如果都限制为0~360,还是有些角位移有多种表示,导致连一些基本的问题,比如两组欧拉角代表的角位移是否相同都很难回答。
至于为什么范围分别是0~360、-180~180、-90~90,这里就不过多展开了,同时也压根不准备证明欧拉角定理(欧拉定理是以瑞士数学家莱昂哈德·欧拉命名,于1775年,欧拉使用简单的几何论述证明了这定理)。

其二:怎么判断手机陀螺仪的顺序?

很简单,范围越大表示次序越靠前,因为最外层的运动范围更大一些,也就是说手机陀螺仪欧拉角的顺序是ZXY

在解释42行和50行的代码之前,我们先用已经了解的知识制作一个例子来演示手机陀螺仪,

请使用支持陀螺仪的移动设备浏览。

在这个例子里边我创建了一个和手机外型类似的长方体,然后让其方位跟随手机设备方位的变化而变化,核心代码如下:

在这个方法里边用了三种方式来实现旋转;

第一种方式就是分别调用物体的rotateZrotateXrotateY方法(因为任何角位移都可以分解为绕三个互相垂直的轴的三个旋转组成);

第二种方式是使用矩阵,同样是基于上述原理,唯一要注意的是角度要取负值(简单理解为物体实际没有动,变换的是坐标系);

第三种方式就是使用ThreeJS框架实现的欧拉角;

    /**
     *  注意下边不要想当然为啥set方法的前三个参数和后边的顺序不是一一对应;
     *  比如如果顺序为ZXY,那么前边参数顺序应该为alpha、beta、gamma;
     *  ThreeJS代码实现就是前边参数的顺序固定为XYZ,和后边的顺序是没有关系的;
     *  因为这个想当然的问题困扰我很久。
     */
    var euler = new THREE.Euler();
    euler.set( beta,gamma,alpha,  'ZXY' );

接下来回头再看控制器里边的代码,就会有几个问题了:

其一:为什么欧拉角的顺序变成了YXZ而不是ZXY?

其实控制器代码里边作者是写了注释的,ZXY for the device, but YXZ for us,简单来说摄像机坐标系和我们人眼的坐标系是不一样的,比如摄像机还存在一个正方向的问题,你在设置顺序为ZXY时就会发现在旋转到某个角度时整个页面的图像会发生颠倒,至于为啥设置顺序为YXZ就不会发生这种情况,我也不是很清楚。

其二:为什么gamma和alpha的位置发生了交换?

其三:为什么gamma值为负数?

第二和第三个问题会在第46行和第54行中得到解释。

46行和第54行把相机绕X轴旋转了负90度

    var q1 = new THREE.Quaternion( - Math.sqrt( 0.5 ), 0, 0, Math.sqrt( 0.5 ) ); // - PI/2 around the x-axis
    quaternion.multiply( q1 ); // camera looks out the back of the device, not the top

Quaternion就是四元数,在了解四元数所代表的几何意义之前,我们简单了解下为什么会有四元数;

主要是因为欧拉角存在一些问题,比如万向节死锁

简单来说就是在旋转过程中有两个轴重合导致,失去一个维度,导致此后的角位移没办法用欧拉角表示,也就是说欧拉角失效了,在欧拉角进行插值操作时大多数情况下会导致抖动、路径错误,物体会突然飘起来。
优酷里边有个视频教程讲的挺清楚的欧拉旋转
为什么用三个数来表达3D方位一定会导致如万向锁这样的问题?这是有数学原因的,它涉及到一些非常高级的数学概念,如“簇”;而四元数通过使用四个数来表达方位,从而避免了这些问题。

另外还涉及到复数,复数为实数的延伸,它使任一方程式都有根;

任意复数都可以表示为x+y*i,i是复数当中的虚数单位,它是-1的平方根。

数学家们慢慢接受复数之后,发现复数集存在于一个2D平面上,该平面有两个轴,实轴和虚轴;这样就能将复数(x+y*i)解释为2D向量,也能用来旋转2D中的向量;然后很自然的数学家们就想找到一种方法将复数从2D扩展到3D,刚开始他们认为这种新的复数应该有一个实部和两个虚部,然而这种方案一直没有进展,直到后来意识到应该有三个虚部和一个实部,就这样四元数诞生了。

老实说这是一个比较复杂的概念(至少当我写这篇博客的时候感觉如此),一个四元数包含一个标量分量和一个3D向量分量,经常记标量分量为w,记向量分量为单一的v或者分开的x,y,z。

    [w,v]
    [w,(x,y,z)]

四元数能被解释为角位移的绕某个轴n旋转a角度,公式如下:

    q = [cos(a/2),sin(a/2)*n]

因此可以知道第46行就是一个表示绕X轴旋转负90度的四元数了,那么现在的问题就是为什么要把摄像机绕X轴旋转负90度?

主要是因为手机陀螺仪的初始状态是在手机平放于水平面的时候,但是我们处理3D环绕视角场景时是希望在手机竖直时的状态为初始状态,因此需要把坐标系绕X轴旋转负90度,旋转负90度之后,现在Z轴相当于以前的负Y轴,现在的Y轴相当于以前的Z轴,因此也就不难理解为什么gamma和alpha的位置发生了交换?为什么gamma值为负数?这两个问题了。

40行和第56行处理了手机横屏和竖屏的问题

    var zee = new THREE.Vector3( 0, 0, 1 );
    quaternion.multiply( q0.setFromAxisAngle( zee, - orient ) ); // adjust for screen orientation

结语

由于数学基础薄弱,上述内容存在很多一笔带过甚至是强行解释的现象,欢迎大家纠正;

主要参考资料为《3D数学基础:图形与游戏开发》。

发表评论

电子邮件地址不会被公开。 必填项已用*标注