zoukankan      html  css  js  c++  java
  • Cardboard Talk02 Accelerometer

    操作系统:Windows8.1

    显卡:Nivida GTX965M

    开发工具:Android studio 3.0.0 | Cardboard 1.0


    在深入讨论具体实现之前,有必要了解一下Android系统的 IMU 传感器与坐标系之间的关系及关系转换,才不至于在后面实现、应用中搞混坐标系。在此借机会翻译 NVDIA 的一篇文章 Tegra Android Accelerometer Whitepaper ,内容如有不当之处请指教。

    Introduction


    移动便携设备可以进行多方向的旋转。很多情景下,应用程序需要考虑通过何种方式影响他们,比如自定义用户UI时的 landscape 风景模式 或者 portriat 肖像模式时,或者解读原始的传感器数据时。应用程序在使用加速度计 accelerometer 获取原始数据必须要经过特殊处理,换句话说为了获得良好的用户体验,还要加入设备及操作系统的考虑因素。

    虽然加速度计 accelerometer 是许多软件应用重要的输入设备,但是有一些开发者正在使用错误的方式来处理加速度计的数据。更重要的是,这些应用程序没有考虑到设备的方向,这会直接导致糟糕的用户体验。处理加速度计正确的方向是一件简单、容易的事情,我们将在接下来的章节中讨论。

    Coordinate Space Glossary


    Android系统会以绝对值的形式上报加速度计的值。最初上报的数值始终是物理传感器的数据,经过调整为 canonical 规范格式,以便所有设备以相同形式上报、处理数据。Android 不会根据设备方向将加速度计数据转换。应用程序必须自己执行转换过程。

    为说明转换过程,本文档定义坐标空间概念以及各个坐标空间之间的转换关系。

    Space  Description
    设备坐标空间     加速度计设备可以按照各种方式输出加速度值,并且不受到任何标准的限制。
    规范坐标空间

    为规范设备每帧输出的坐标数据,Android要求必须重新对设备坐标系 Device Raw 进行映射。所以,当设备朝向右移动时,正X轴增加,当设备朝上移动时,正Y轴增加,当设备朝向屏幕方向移动时,正Z轴增加,反之亦然。

    有关 Canonical 加速度计各轴的布局结构请参阅 Accelerometer Canonical Axes 。

    屏幕坐标空间 Android窗体管理器的屏幕坐标系原点在左上角,最大坐标在右下角,即增加X是向右,增加Y是向下。Android的 Display 管理器会根据传感器读取的数据改变屏幕方向。屏幕坐标系总是相对于当前的旋转角度。
    世界坐标空间

    这个坐标空间是特定于 OpenGLES 应用程序的。应用程序开发人员可能需要修改代码以适应本文档中的假设条件,默认使用右手坐标系,up可以是任意的矢量。

    下表显示了需要做的转换关系,并为本文定义了一个词汇表。OpenGL应用程序通常会使用 canonToWorld 。基于Android窗体系统的应用程序将使用 canonToScreen。混合开发的应用程序,例如基于Android窗体系统且应用OpenGL渲染内容的将需要两者。

    需要注意的是,加速度计设备驱动程序会处理 Canonical Space,即处理 deviceToCanon 转换。本文档着重于 canonToScreencanonToWorld 转换。

    Accelerometer Canonical Axes


    以下图示显示了基于给定设备和方向的加速度计值的变化。它们包括一个基于垂直 portrait 肖像布局的原生设备,一个旋转到横向的 portriat 肖像设备,以及一个横向 landscape 风景布局原生设备,规范化 Canonical x/y/z 加速度计 轴/值。

    移动设备原生肖像模式  Orientation 0

    移动设备原生肖像模式 Orientation 90

    移动设备原生风景模式 Orientation 0

    Working with Acclerometer Data


    加速度计的值必须根据 Display 对象返回的方向进行旋转,才可能在屏幕上获取期望的结果。Android获取屏幕旋转朝向的API为 getOrientation() 或者 getRotation() ,他们都会返回同样的结果,但是前者不建议在继续使用。

    这些函数返回的值对应 Android.view.Surface 类中带有 ROTATION_ 前缀的整数和常量。下面是调用其中一个函数的例子。其中 this 代表 Activity 类型对象。

    WindowManager windowMgr =
         (WindowManager)this.getSystemService(WINDOW_SERVICE);
    int rotationIndex = windowMgr.getDefaultDisplay().getRotation();
    

    返回常量为:

    Constant Name Index/Value
    ROTATION_0 0
    ROTATION_90 1
    ROTATION_180 2
    ROTATION_270 3

    应用程序可以使用旋转值构建转换矩阵,该矩阵将会转换 Android Canonical 加速度计数据到其他的坐标系空间。为了将标准的 Canonical 加速度计数值转换为屏幕或者世界坐标系下的数值。一个基于 rotationIndex 索引的 canonAccel 向量需要按照90度的增量旋转,其中 ROTATION_0 意味着不需要旋转。

    为了进行 canonToScreen 转换,定义旋转方程为:

    screenAccel[0] = canonAccel[0] * cos⁡null - canonAccel[1] * sin⁡null 
    screenAccel[1] = -canonAccel[0] * sin⁡null - canonAccel[1] * cos⁡null 
    screenAccel[2] = canonAccel[2]

    其中:

          canonToScreen formula

    一个 canonToScreen 转换的函数实现如下:

    static void canonicalToScreen(int     displayRotation,
                                  float[] canVec,
                                  float[] screenVec)
    {
         struct AxisSwap
         {
              signed char negateX;
              signed char negateY;
              signed char xSrc;
              signed char ySrc;
         };
         static const AxisSwap axisSwap[] = {
              { 1, -1, 0, 1 },   // ROTATION_0
              {-1, -1, 1, 0 },   // ROTATION_90
              {-1,  1, 0, 1 },   // ROTATION_180
              { 1,  1, 1, 0 } }; // ROTATION_270
    
         const AxisSwap& as = axisSwap[displayRotation];
         screenVec[0] = (float)as.negateX * canVec[ as.xSrc ];
         screenVec[1] = (float)as.negateY * canVec[ as.ySrc ];
         screenVec[2] = canVec[2]; 
    }

    对于 canonToWorld 转换,旋转按照如下公式进行:

    screenAccel[0] = canonAccel[0] * cos⁡null - canonAccel[1] * sin⁡null 
    screenAccel[1] = canonAccel[0] * sin⁡null + canonAccel[1] * cos⁡null 
    screenAccel[2] = canonAccel[2]

    轴对称的转换相关数据可以预置在静态数组中,如下列函数 canonicalToWorld(),将标准空间加速度向量转换成OpenGL风格的世界空间向量时,使用简单的整数查找数组避免昂贵的三角函数计算代价。

    static void canonicalToWorld( int           displayRotation,
                                  const float*  canVec,
                                  float*        worldVec)
    {
         struct AxisSwap
         {
              signed char negateX;
              signed char negateY;
              signed char xSrc;
              signed char ySrc;
         };
         static const AxisSwap axisSwap[] = {
              { 1,  1, 0, 1 },   // ROTATION_0
              {-1,  1, 1, 0 },   // ROTATION_90
              {-1, -1, 0, 1 },   // ROTATION_180
              { 1, -1, 1, 0 } }; // ROTATION_270
    
         const AxisSwap& as = axisSwap[displayRotation];
         worldVec[0] = (float)as.negateX * canVec[ as.xSrc ];
         worldVec[1] = (float)as.negateY * canVec[ as.ySrc ];
         worldVec[2] = canVec[2];
    }
    

    下一个函数将根据对应的坐标轴计算旋转角度,该转换会参考加速度计上方向 localUp 。函数以向量的形式返回 旋转轴 rotAxis 和 对应的旋转角度 angles ,获得以上数据后就可以针对世界空间进行转换矩阵、四元数的创建。

    void computeAxisAngle(const float* localUp, const float* worldVec,
                          float* rotAxis, float* ang)
    {
         const Vec3& lup  =  *(Vec3*)localUp;
         Vec3 nTarget     =  normalize(*(Vec3*)worldVec);
         *rotAxis         =  cross(lup, nTarget);
         *rotAxis         =  normalize(*rotAxis);
         *ang             = -acosf(dot(lup, nTarget));
    }
    

    Power Conservation


    为了节省设备电量,应用程序应该选择低级别的加速度计更新频率。可以选择的更新频率级别均定义在 android.hardware.sensormanager 中,下表显示了按照降序排列的加速度计更新频率级别。

    Constant Name Relative Speed
    SENSOR_DELAY_FASTEST fastest
    SENSOR_DELAY_GAME faster
    SENSOR_DELAY_NORMAL slower
    SENSOR_DELAY_UI slowest

    一个设置传感器更新频率的示例如下:

    if (mSensorManager == null)
        mSensorManager = (SensorManager)getSystemService(SENSOR_SERVICE);
    if (mSensorManager != null)
        mSensorManager.registerListener(
             this,
             mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER),
             SENSOR_DELAY_GAME );  

    额外需要注意的是 delay 值是抽象的,该值与具体的设备有关。因此同样的更新级别在不同手机表现会有所不同。保证设备更新频率唯一的方法就是在运行时测量返回的速率。

    Supporting Older OS Versions


    为了支持比较老的Android系统比如 Froyo / v2.2。可能需要使用比较旧的API功能 getOrientation()。下面演示动态绑定的例子,需要注意的是 getOrientation() 可能会在未来Android版本中消失。

    WindowManager wm;
    Method getRotation;
    
    wm = (WindowManager)this.getSystemService(WINDOW_SERVICE);
    Class<Display> c = (Class<Display>)wm.getDefaultDisplay().getClass();
    Method[] methods = c.getDeclaredMethods();
    String rotFnName = new String("getRotation");
    for( Method method : methods )
    {
         Log.d("Methods", method.getName());
         if( method.getName().equals( rotFnName ))
         {
             getRotation = method;
             break;
         }
    }
    
    int orientation;
    Display display = wm.getDefaultDisplay();
    
    if( getRotation != null )
    {
         try
         {
             Integer i = (Integer)getRotation.invoke(d);
             orientation = i.intValue();
         }
         catch(Exception e) {}
    }
    else
    {
         orientation = display.getOrientation();
    }
    

    需要补充的是NDK并没有直接获取 Display 的方式,但是可以间接的传递 Context 对象,通过反射获取 WindowManagerDisplay 对象,从而调用 getOrientation()getRotation() 。但是该方法性能比较堪忧,毕竟使用了反射。另一个方式是从上层将 getOrientation() 或者 getRotation() 的值传递到 native层使用。

  • 相关阅读:
    Job流程:Shuffle详解
    学Python Django学得很迷茫,怎么办?-转自知乎
    URL补充
    创建多对多以及增加示例
    Day20-初识Ajax
    笔记-自己看Day20-待续
    Day20-单表中获取表单数据的3种方式
    Day19内容回顾
    一点疑惑的解释
    python os.path模块常用方法详解
  • 原文地址:https://www.cnblogs.com/heitao/p/7992291.html
Copyright © 2011-2022 走看看