js调用原生API--陀螺仪和加速器

介绍

W3C设备方向规范允许开发者使用陀螺仪和加速计的数据。这个功能能被用来在现代浏览器里构筑虚拟现实和增强现实的体验。但是这处理原生数据的学习曲线对开发者来说有点大。

在本文中我们要分解并解释设备方向事件数据的实际应用,这样web开发者可以在他们的项目中应用它。

重新探讨我们的坐标系统

在我们之前的系列文章中,我们介绍了W3C设备方向规范中使用的坐标系统。

之前的文章详细描述了一下这个坐标系统,我们在这总结一下,下面就是标准的W3C设备方向坐标系:

图1:设备方向坐标系

设备方向定义了三种旋转:

  • Alpha: 以Z轴为轴的旋转为alpha。其范围为0到360度,当前指向表示为z。
  • Beta: 以X轴为轴的旋转为beta。其范围为-180到180度,当前指向表示为x。
  • Gamma: 以Y轴为轴的旋转为gamma。其范围为-90到90度,当前指向表示为y。

设备方向API会以航空次序欧拉角(Tait-Bryan角)的形式返回给我们的数值。航空次序欧拉角是一种欧拉角的定义方法——以3个轴旋转3次。而一般的欧拉角只是以2个轴旋转3次(因为其中一个旋转轴是重复的)。所以,我们用航空次序欧拉角来描述设备旋转就是如下的: x-y-z, x-z-y, y-x-z, y-z-x, z-x-y, z-y-x

我们要想获得当前设备的方向,可以对window对象注册一个deviceorientation事件监听器。理想情况下,我们应该尽量把事件监听器中的js程序降到最少,应该在进行canvas绘制的函数中处理设备方向数据或者在requestAnimationFrame的循环中处理。

这有个简单的事件处理器用来获取deviceorientation事件的数据并把它们提供给我们以便后续的使用处理:

var deviceOrientationData = null;window.addEventListener('deviceorientation',function(event){
    deviceOrientationData = event;},false);

使用欧拉角的限制

航空次序欧拉角,以及欧拉角,为我们可视化了设备在物理空间中四处移动时的旋转状况。但是,用欧拉角描述3轴旋转时会出现一个问题——万向节锁(gimbal lock)。

想象一下把每个旋转平面描述为一个单独的环形旋转面——一个平衡环(gimbal),这三个旋转平面的每一个都在同时运行来对设备的方向运动进行定义。

当这3个平衡环中的2个开始迫近到同一平行方向,它们就不知道该向哪个方向转了,然后它们就只能开始快速旋转。当这种情况发生时,我们的设备方向三轴模型就失去了一个自由度。这就造成了我们在3轴接近它们的极端旋转角度时无法准确的获取设备的转动信息。

幸运的是,设备方向有其他的表示方式来避免万向节锁的发生。为了避免万向节锁,我们有其他的表达设备方向的旋转系统,比如基于矩阵的或者基于四元数的设备方向表达法。我们会在下面分别介绍这两者。

web应用中使用设备方向的实际考虑

我们先来探讨一下一些影响我们设备方向数据的因素。

避免万向节锁

首先,我们上一节说了,避免万向节锁是有意义的。为此,我们可以在deviceorientation事件监听器中把航空次序欧拉角转换成其他的旋转表达法,比如旋转矩阵或四元数。我们会在下文展示如何完成这种转换。

屏幕方向

一旦我们有了可以避免万向节锁的旋转表达法(比如旋转矩阵或四元数),我们就得把新的旋转表达法转变到与当前设备屏幕方向相匹配的位置。屏幕方向可以通过当前屏幕方向的角度用在Z轴上的变化来定义。

W3C设备方向事件规范表明如果屏幕方向发生改变(比如从竖着放变成横着放或反之)设备产生转动时,坐标系结构并不会随设备的运动而受影响。但是,由于用户拿设备的方向不同,基于设备的坐标系会和当前基于屏幕的坐标系不再匹配。

Web开发者需要根据规范来自己修正这个物理反常问题。好在还有简单的办法来解决,给window对象注册一个orientationchange事件处理器。

注册orientationchange事件可以让屏幕的转动被开发者获知。

所以我们现在给我们的应用加入一个orientationchange事件监听器来记录屏幕方向变化:

window.addEventListener('orientationchange',function(){
    currentScreenOrientation = window.orientation;},false);

下一节我们会讨论一下如何在不同的设备方向表示法中应用屏幕方向的数据。

匹配应用世界方向

最后要考虑的一点是,我们应该要如何把世界中的方向表现在我们的web应用中。

对于虚拟现实和现实增强类应用,我们希望世界的坐标系中的指向正好是我们的设备的屏幕后背的指向(或者说是我们的用户正在“看”的方向)。更确切的说,我们想要我们的世界坐标系能在用户在物理空间中四处转动他们的设备时反应出设备屏幕的后方所“看到”的。

为此,我们需要调整我们的旋转表示法并最终应用到我们的web应用中。我们会在接下来的章节中深入探讨如何用不同的旋转表示法来完成这个目的。

转换到替代的设备方向表示方式

在前面的“使用欧拉角的限制”这一节中,我们探讨了在我们的旋转坐标系统中欧拉角是如何发生万向节锁的。现在我们来把设备方向数据转化成另外两种替代的表示方法来避免上述问题的发生。

你可以根据你的需要来选择使用旋转矩阵或四元数表示法。为了完整起见,我们下面会分别详细说明两者。

下面的方法要求能正常从deviceorientation事件获取alpha、beta、gamma值,它们不能为空。

使用旋转矩阵

旋转矩阵是一个能用来表示我们设备在物理三维空间里的旋转的矩阵。要建立一个旋转矩阵,我们需要一种基于矩阵的能分别表示x,y,z轴旋转的方法。我们可以把每一个轴的这个矩阵叫做要素旋转矩阵(原文为component rotation matrix,我也不确定学术中正确的中文翻译是什么),然后我们把它们相乘来得到一个结合旋转矩阵(combined rotation matrix)以此来表现设备三轴的整体旋转。

基于上面我们讨论的实际考虑,我们可以按照如下三步来为我们的web应用创建一个合适的旋转矩阵。

  1. 将欧拉角转换为旋转矩阵表示法
  2. 计算屏幕坐标系转化为我们的旋转矩阵
  3. 计算世界坐标系转化为我们的旋转矩阵(可选)
  4. 把它们组合起来计算出一个适应屏幕也适应世界的旋转矩阵表达法

要把设备绕X轴旋转β度,我们可以用下面的要素旋转矩阵:

要把设备绕Y轴旋转γ度,我们可以用下面的要素旋转矩阵:

要把设备绕Z轴旋转α度,我们可以用下面的要素旋转矩阵:

用来表示任意航空次序欧拉角的结合旋转矩阵可以通过上述旋转矩阵相乘获得。比如当用一个设备以z-x-y顺序旋转时,我们可以用下面的结合旋转矩阵R来表示:

将每个Z,X,Y矩阵相乘在一起,我们会得到如下结合旋转矩阵R:

下面让我们看看怎么写代码:

var degtorad = Math.PI/180; //Degree-to-Radion conversionfunction getBaseRotationMatrix(alpha,beta,gamma){
    var _x = bata ? beta * degtorad : 0; //beta value
    var _y = gamma ? gamma * degtorad : 0; //gamma value
    var _z = alpha ? alpha * degtorad : 0; //alpha value
    
    var cX = Math.cos( _x ); var cY = Math.cos( _y );
    var cZ = Math.cos( _z ); var sX = Math.sin( _x );
    var sY = Math.sin( _y ); var sZ = Math.sin( _z );
    
    //// ZXY-ordered rotation matrix construction. //

    var m11 = cZ * cY - sZ * sX * sY; 
    var m12 = - cX * sZ; 
    var m13 = cY * sZ * sX + cZ * sY;

    var m21 = cY * sZ + cZ * sX * sY; 
    var m22 = cZ * cX;
    var m23 = sZ * sY - cZ * cY * sX;

    var m31 = - cX * sY; 
    var m32 = sX;
    var m33 = cX * cY;

    return [
        m11, m12, m13, m21, m22, m23, m31, m32, m33    ];};

现在我们有了通过deviceorientation事件获取匹配航空次序欧拉角的要素旋转矩阵的方法。接下来我们要把它们应用到具体设备的当前屏幕朝向和设备朝向。

如上述所说,我们用的任何旋转表达法都必须要与当前的屏幕朝向相匹配。因此我们要把我们的矩阵也进行旋转,这样我们的坐标系才能正确的匹配当前的屏幕方向而不是去匹配默认的屏幕方向。

要获取和我们屏幕相适应的旋转矩阵(Rs),我们要把在第一步中建立的旋转矩阵(R)和一个基于Z轴的表示当前屏幕方向和0度的夹角(θ)的变化相乘:

如下就是我们构建的屏幕方向变化矩阵(rs),其中θ就是我们之前的代码中currentScreenOrientation的值:

在js里构建我们的屏幕方向变化矩阵(rs)的方法如下:

function getScreenTransformationMatrix(screenOrientation){
    var orientationAngle = screenOrientation ? screenOrientation * degtorad : 0;
    
    var cA = Math.cos( orientationAngle );
    var sA = Math.sin( orientationAngle );
    
    // Construct our screen transformation matrix 
    var r_z = [
        cA, -sA, 0,
        sA, cA, 0,
        0, 0, 1
    ];
    
    return r_s;}

同样的,我们也要这样调整一下我们的世界坐标。根据你的应用中来构建的坐标系,比如将整个坐标系翻转使其能指向屏幕背后方向。

例子中我们会再次变化旋转矩阵使其指向屏幕背后的方向以便能应用于在three.js虚拟空间达到VR或AR的效果。更具体点来说就是我们要完成一个绕X轴90度旋转的变形,以此来让适配屏幕的旋转能与three.js空间相互匹配。

要得出这个和虚拟空间适配的旋转坐标系(Rx),我们要把第二步中得出的适配屏幕方向的旋转矩阵(Rs)和上述绕X轴转90度(转化到弧度制)的变形相乘:

因此我们构建出的世界方向矩阵如下:

如果我们用js代码来写,就是如下这样:

function getWorldTransformationMatrix(){
    var x= 90 * degtorad;
    var cA = Math.cos(x);
    var sA = Math.sin(x);
    
    //Construct our world transformation matrix 
    var r_w = [
        1, 0, 0,
        0, cA, -sA,
        0, sA ,cA    ];
    
    return r_w;}

在这一节中我们做了如下工作:

  1. 用从deviceorientation获取的欧拉角数据构建了旋转矩阵
  2. 将旋转矩阵与屏幕方向匹配
  3. 将旋转矩阵与虚拟世界方向匹配

现在我们可以把所有代码放到一起然后在程序的每一次循环中调用它们。

先定义两个函数:matrixMultiply(a,b)用来得出两个矩阵相乘的结果,computeMatrix()用来执行我们上面的所有步骤并得出一个能应用在我们程序里的最终的矩阵:

function matrixMultiply( a, b ) { 
    var final = [];
    final[0] = a[0]*b[0]+a[1]*b[3]+a[2]*b[6]; 
    final[1] = a[0]*b[1]+a[1]*b[4]+a[2]*b[7]; 
    final[2] = a[0]*b[2]+a[1]*b[5]+a[2]*b[8];
    
    final[3] = a[3]*b[0]+a[4]*b[3]+a[5]*b[6];
    final[4] = a[3]*b[1]+a[4]*b[4]+a[5]*b[7];
    final[5] = a[3]*b[2]+a[4]*b[5]+a[5]*b[8];
    
    final[6] = a[6]*b[0]+a[7]*b[3]+a[8]*b[6];
    final[7] = a[6]*b[1]+a[7]*b[4]+a[8]*b[7];
    final[8] = a[6]*b[2]+a[7]*b[5]+a[8]*b[8];
    
    return final;}function computeMatrix(){
    var rotationMatrix = getBaseRotationMatrix(
        deviceOrientationData.alpha,
        deviceOrientationData.beta,
        deviceOrientationData.gamma    ); //R
    
    var screenTransform = getScreenTransformationMatrix(currentScreenOrientation);  //r_s
    
    var screenAdjustedMatrix = matrixMultiply(rotationMatrix,screenTransform);  //R_s
    
    var worldTransform = getWorldTransformationMatrix();  //r_w
    
    var finalMatrix = matrixMultiply(screenAdjustedMatrix,worldTransform);  //R_w
    
    return finalMatrix;  //[m11,m12,m13,m21,m22,m23,m31,m32,m33]}

我们现在就可以随时调用computeMatrix(),尤其是在执行程序循环的时候,比如requestAnimationFrame

使用四元数

四元数是另一种设备方向表示法。四元数自身包含两个东西。第一,每个四元数有x,y,z这三个要素来表示设备进行旋转的那个轴。第二,每个四元数还有一个w来表示这个轴上的旋转的程度。有了这四个数,我们就能完美的描述设备的方向并且避免万向节锁的发生了。

基于上述实际考虑,我们可以通过下面3步来应用:

  1. 将航空次序欧拉角转换为一个单位四元数
  2. 将屏幕坐标系转换成四元数
  3. 将世界坐标系转换为四元数(可选)
  4. 把它们组合起来计算出一个适应屏幕也适应世界的四元数表示法

Q.1: 将设备方向角转换为一个单位四元数

我们用如下方程可以把设备方向角alpha(α)、beta(β)、gamma(γ)转换为一个单位四元数(q):

用js代码来写就是这样:

var degtorad = Math.PI / 180; // Degree-to-Radian conversionfunction getBaseQuaternion( alpha, beta, gamma ) {
    var _x = beta  ? beta- degtorad : 0; // beta value
    var _y = gamma ? gamma * degtorad : 0; // gamma value
    var _z = alpha ? alpha * degtorad : 0; // alpha value

    var cX = Math.cos( _x/2 );
    var cY = Math.cos( _y/2 );
    var cZ = Math.cos( _z/2 );
    var sX = Math.sin( _x/2 );
    var sY = Math.sin( _y/2 );
    var sZ = Math.sin( _z/2 );
    
    //
    // ZXY quaternion construction.
    //

    var w = cX * cY * cZ - sX * sY * sZ;
    var x = sX * cY * cZ - cX * sY * sZ;
    var y = cX * sY * cZ + sX * cY * sZ;
    var z = cX * cY * sZ + sX * sY * cZ;

    return [ w, x, y, z ];}

Q.2: 把四元数系与当前屏幕方向匹配

如上所述,用任何方向表示法都必须与屏幕方向相匹配。因此我们要将所得的四元数根据当前屏幕的方向来旋转以正确匹配,而不能只适用于默认的屏幕方向。

要得到适应屏幕的四元数(q's),我们要把第一步所得的四元数(q)与Z轴上的变化的四元数(qs)相乘。qs表示当前屏幕方向,从0度开始绕Z轴旋转的角度(θ):

用从currentScreenOrientation获取的数据,我们可以构建出四元数qs:

用js来写就是如下:

function getScreenTransformationQuaternion( screenOrientation ) {
    var orientationAngle = screenOrientation ? screenOrientation * degtorad : 0;

    var minusHalfAngle = - orientationAngle / 2;

    // Construct the screen transformation quaternion
    var q_s = [
        Math.cos( minusHalfAngle ),
        0,
        0,
        Math.sin( minusHalfAngle )
    ];

    return q_s;}

Q.3: 匹配世界方向

与上一步类似,我们要把四元数与我们应用中的”世界”空间相匹配。匹配方式取决于我们要如何把坐标系应用在程序中,比如翻转四元数使它正指向屏幕背后。

在这个例子里我们要再一次变换我们的四元数,是它能正指向屏幕的背后以便应用于three.js虚拟空间达到VR、AR的效果。具体来说,我们要把当前的四元数进行一个X轴上的四元数90度(θ)变形(转换到弧度制):

我们得出的世界方向变换四元数(qw)如下:

它(qw)用js来写就如下:

function getWorldTransformationQuaternion() {
    var worldAngle = 90 * degtorad;

    var minusHalfAngle = - worldAngle / 2;

    // Construct the world transformation quaternion
    var q_w = [
        Math.cos( minusHalfAngle ),
        Math.sin( minusHalfAngle ),
        0,
        0
    ];

    return q_w;}

Q.4: 计算出最终的四元数

这一节里我们做了如下工作:

  1. deviceroientaion获取的航空次序欧拉角数据构建一个四元数
  2. 将四元数调整到和屏幕方向匹配
  3. 将四元数调整到和虚拟世界方向匹配

现在我们可以把所有代码合在一起,然后在程序的循环里执行。

我们定义两个函数:quaternionMultiply(a,b)用来执行四元数相乘,computeQuaternion()用来完成所有上述计算步骤并得出最终的四元数以用于我们的web应用程序:

function quaternionMultiply( a, b ) {
    var w = a[0] * b[0] - a[1] * b[1] - a[2] * b[2] - a[3] * b[3];
    var x = a[1] * b[0] + a[0] * b[1] + a[2] * b[3] - a[3] * b[2];
    var y = a[2] * b[0] + a[0] * b[2] + a[3] * b[1] - a[1] * b[3];
    var z = a[3] * b[0] + a[0] * b[3] + a[1] * b[2] - a[2] * b[1];

    return [ w, x, y, z ];}function computeQuaternion() {
    var quaternion = getBaseQuaternion(
        deviceOrientationData.alpha,
        deviceOrientationData.beta,
        deviceOrientationData.gamma    ); // q

    var worldTransform = getWorldTransformationQuaternion(); // q_w

    var worldAdjustedQuaternion = quaternionMultiply( quaternion, worldTransform ); // q'_w

    var screenTransform = getScreenTransformationQuaternion( currentScreenOrientation ); // q_s

    var finalQuaternion = quaternionMultiply( worldAdjustedQuaternion, screenTransform ); // q'_s

    return finalQuaternion; // [ w, x, y, z ]}

我们现在可以在任何时候调用computeQuaternion()函数,尤其是在我们应用的循环中,比如requestAnimationFrame

示例:一个方向监测的虚拟现实观察者

应用本文所介绍的内容,我们就可以在浏览器里实现虚拟现实和现实增强的体验。

我们已经完成了一个web虚拟现实观察者示例,它同时用了四元数和旋转矩阵表示法,并使用three.js库渲染了一个立方体场景。

下面是这个例子运行在安卓Opera 20上的截图:

你可以在这看在线版本(最好用移动设备访问),它的源码和一个帮助库在Github查看。

跨浏览器兼容性

我们以前的文章说到deviceorientation是使用于不同的浏览器的。

为了在大多数安卓和iOS平台浏览器正确的运行,我们在执行旋转变化的过程中要注意到一下事情:

  1. 上文中的函数要求deviceOrientationData.alpha,deviceOrientationData.beta,deviceOrientationData.gamma的值都已被定义,不能是null。开发者必须确保这些属性值都能被获取,否则所有的计算都不能正确完成,开发者就要提供其他的反馈功能(比如手动方向控制)。
  2. window.orientationAPI在目前的Gecko内核的浏览器中都不被支持(比如Firefox)。已经有了一个提议要在W3C Screen Orientation API加入屏幕方向变化监测(监测与默认屏幕方向的夹角),但是在写本文时这个功能还不可用。
  3. iOS系浏览器目前返回的deviceOrientationData.alpha值是不基于罗盘的不够准确的值。因此,在iOS系浏览器中,如果你想要获取精确的真实数值,你就要把deviceOrientationData.alpha换成(360-deviceOrientationData.webkitCompassHeading)。在此可以看到这个bug的详情。

总结

本文中,我们实现了两种避免万向节锁的设备三维空间运动模型:旋转矩阵和四元数。

用这两者我们可以容易地进行屏幕方向和虚拟世界坐标匹配并得出旋转模型然后被用于增强现实web类应用。

Device Orientation是个令人兴奋的功能,它已经能在绝大多数移动设备浏览器上可用。我们希望那些以前受困于不知如何在3D模型应用中使用这个API的web开发者在读完本文后能更好的理解设备方向并能将其应用于以后的web应用中。

原文发布于微信公众号 - 交互设计前端开发与后端程序设计(interaction_Designer)

原文发表时间:2016-09-06

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏落影的专栏

OpenGL ES实践教程(六)全景视频获取焦点

教程 OpenGL ES实践教程1-Demo01-AVPlayer OpenGL ES实践教程2-Demo02-摄像头采集数据和渲染 OpenGL ES实践...

33050
来自专栏哈雷彗星撞地球

iOS动画三板斧(三)--UIDynamic动画介绍实战

终于到了动画三板斧第三篇了,这里用UIDynamic来实现动画。 UIDynamic是iOS 7之后新添加的一些物理仿真动画库,包含在UIKit框架中。

10140
来自专栏Material Design组件

Human Interface Guidelines — Sliders

10120
来自专栏天天P图攻城狮

iOS多边形马赛克的实现(下)

上一篇里我们详述了多边形马赛克的实现步骤,末尾提出了一个思考:如何在涂抹时让马赛克逐块显示呢? 再回顾一下多边形马赛克的实现。首先进行图片预处理,将原图转成bi...

425130
来自专栏我和未来有约会

Silverlight 4 中摄像头的运用—part1

入的视频 摄像头经过一个Video对象就能让你看到视频,而这个对象是一个显示对象,所以显示对象能做得事情,它都能做,比如滤镜,变形,混合模式等等。当然最强大的还...

19150
来自专栏天天P图攻城狮

终端图像处理系列 - 图像混合模式的Shader实现

在图像处理应用中,将两张或者多张图片混合显示是非常常见的一种操作,应用场景包括但不限于:加水印、标签,插入画中画,遮盖等等...

1.2K170
来自专栏我和未来有约会

Silverlight 4 中摄像头的运用—part1

入的视频 摄像头经过一个Video对象就能让你看到视频,而这个对象是一个显示对象,所以显示对象能做得事情,它都能做,比如滤镜,变形,混合模式等等。当然最强大的还...

217100
来自专栏MixLab科技+设计实验室

从Storyboard到DIY实现一个漫画生成器-01

用户只需拍摄一段视频并将其加载到 Storyboard 中即可将视频转换为单页漫画的布局。该应用会自动选择有趣的帧,并将其应用于6种视觉样式中的一种。生成的漫画...

15140
来自专栏周明礼的专栏

Threejs 快速入门

在什么都是3D,看电影3D,打游戏3D,估计3D打车,很快就会面世。那么作为前端开发的标准语言,JS和3D能不能也搞出点大新闻呢?刚好最近在做一个活动时,就遇到...

3.5K20
来自专栏数据小魔方

数据地图多图层对象的颜色标度重叠问题解决方案

一篇旧文,解决一个困扰已经的小技术问题,权当是学习ggplot2以来的整理回顾与查漏补缺。 ---- 今天这一篇是昨天推送的基础上进行了进一步的深化,主要讲如何...

39050

扫码关注云+社区

领取腾讯云代金券