【转】重力感应和罗盘

iPad 的玩家大概都用过 StarWalk 这款应用——强力到无以附加的星图软件。StarWalk 里的世界其实就是虚拟了一个环绕用户的天球,当然还能够与真实的天球对应得上,当用户举着 iPad 对向不同方位时,能够从 iPad 屏幕上看到天球上对应的那一方向上的区域,就如同真的在用望远镜观察天球一样。这里面的原理其实很简单,整个虚拟的天球就是游戏世界,主摄像机就在这个游戏世界的正中心点,当 iPad 屏幕正对的方位发生变化时,主摄像机根据获取到的重力感应输入和罗盘输入计算出新的朝向,并转向正确的方向。

重力感应:

  iPad 重力感应的 X,Y,Z 三方向分量分布为:以 Home 按键在下的屏幕摆放方式为基准,垂直屏幕向外的方向为 +Z 轴,沿屏幕边缘向上的方向为 +Y 轴,沿屏幕边缘向右的方向为 +X 轴。想象在 iPad 屏幕正中央栓了一个重锤,重锤指向的方向自然永远是真实世界的重力方向,也是 iPad 重力感应器所接收到的重力方向。现在,把重力感应方向映射到前面重力感应三方向分量组成的坐标系中去,就可以用三分量来描述这个重力输入方向了,分量的取值范围是 (-1, 1)。

  当 iPad 屏幕朝上平躺放置时,重力方向描述为 (0,0,-1);当 iPad 的 Home 按键朝下,做屏幕竖立放置时,重力方向描述为 (0, -1, 0);当 iPad 的 Home 按键在左,做屏幕侧立放置时,重力方向描述为 (1,0,0)。如下图:


  同理,当 iPad 屏幕朝下平躺时,重力方向为 (0,0,1);当 iPad 倒竖放置(Home 键在上竖立放置)时,重力方向为 (0,1,0);当 iPad 侧立放置,Home 键在右时,重力方向为 (-1,0,0)。这六种摆放下重力与某一坐标轴重合,当 iPad 以其他角度摆放时,只需将重力输入看作一个大小为 1 单位的矢量来做坐标轴映射。 当然,这个映射不需要自己计算,因为我们可以——

获得重力输入:

  要获得重力输入很简单,使用 Input.acceleration 即可获得最近一次的重力输入,其值是一个 Vector3 型变量,也就是重力输入方向在上述坐标系下的映射三分量。我们可以通过一个简单的脚本来观察当 iPad 翻转时,这个重力输入的三分量数值会如何变化:

using UnityEngine;

using System.Collections;

using System;


public class ShowGSensorValue : MonoBehaviour

{


// Use this for initialization

void Start()

{


}


// Update is called once per frame

void Update()

{

}


void OnGUI()

{

GUI.Box(new Rect(5, 5, 100, 20), String.Format("{0:0.000}", Input.acceleration.x));

GUI.Box(new Rect(5, 30, 100, 20), String.Format("{0:0.000}", Input.acceleration.y));

GUI.Box(new Rect(5, 55, 100, 20), String.Format("{0:0.000}", Input.acceleration.z));

}

}

上面这个脚本中,通过在屏幕左上角绘制纵向排列的三个 Box 控件,每个空间显示一个分量,来实时显示当前游戏帧内重力输入方向的三个分量大小。如下图所示:

  仅仅是拿来观察重力输入分量,那么直接取 Input.acceleration 就足够了。但是,如果要利用重力输入来影响游戏世界里的物体,那么这里就产生了一个问题:重力输入的分量是以上面提到的用 iPad 屏幕为标准的坐标系进行表述的,但是游戏世界坐标系与 iPad 屏幕坐标系并不一致。就医上面这截图里的球来说,它所处的坐标系的三坐标轴方向是下面这个样子的:


绿色是 +Y 轴,红色是 +X 轴,蓝色是 +Z 轴。或者我们把游戏的主摄像机调整一个角度,好看得更清楚一点:


把摄像机挪到球的正上方,现在球的坐标系看起来跟 iPad 的屏幕坐标系很像了,但显然这里的 +Y 轴和 +Z 轴方向与 iPad 屏幕坐标系不一样。现在切换到摄像机:


其实 iPad 屏幕坐标系就是 Z 轴反向的主摄像机本地坐标系:


所以,如果直接取 Input.acceleration 的方向来对这个球施加作用力的话,它不会按照我们所预期的那样运动。这就需要做——

坐标变换:

  iPad 玩游戏时,是通过游戏里的主摄像机在观察游戏世界,而屏幕就是主摄像机的镜头,因此在主摄像机的指向方向没有发生变化时,可以认为整个游戏世界是“粘在” iPad 屏幕“后头”的,而屏幕就是一块透明玻璃,我们正透过这块玻璃观察粘在玻璃后头的游戏世界。


  而坐标变换就是把屏幕,也就是这块玻璃的坐标系换算成玻璃后面游戏世界的坐标系。最简单的一种坐标变换,就是当摄像机像上面例子中那样从游戏世界的正上方向下俯视,此时只需要简单的把 +Y 换成 +Z 轴,+Z 轴换成 +Y 轴即可。例如把上面的代码修改一下:


using UnityEngine;

using System.Collections;

using System;


public class ShowGSensorValue : MonoBehaviour

{

public Rigidbody Target = null;

public float ForceFactor = 10.0f;


// Use this for initialization

void Start()

{


}


// Update is called once per frame

void FixedUpdate()

{

if (Target != null)

{

Target.AddForce(new Vector3(Input.acceleration.x, Input.acceleration.z, Input.acceleration.y) * ForceFactor, ForceMode.Force);

}

}


void OnGUI()

{

GUI.Box(new Rect(5, 5, 100, 20), String.Format("{0:0.000}", Input.acceleration.x));

GUI.Box(new Rect(5, 30, 100, 20), String.Format("{0:0.000}", Input.acceleration.y));

GUI.Box(new Rect(5, 55, 100, 20), String.Format("{0:0.000}", Input.acceleration.z));

}

}

而更复杂的时候,屏幕坐标系与游戏世界坐标系之间有倾斜角,不能直接调换坐标分量完事,需要用三角函数计算;更更复杂的时候,屏幕坐标系和游戏世界坐标系之间的相对关系是时刻在变化的,因为,我们的主摄像机时刻在移动和旋转,这时需要更为复杂的坐标变换。


备注:iPad 用以支持重力感应的硬件设备是加速度计,当 iPad 除了重力加速度之外不承受其他加速度时,从加速度计上读取的数值可以认为是当前重力加速度,归一化之后即可认为是当前地理位置的重力方向。因此,如果 iPad 设备处于复杂加速环境中,那么从加速度计上读入的数据就不能作为判断重力方向的依据。Unity 3D 3.5 版本后开始支持陀螺仪,看 API 文档里专门有一项 gravity 属性,说明是重力加速度方向,但是手头没有 iPad 2 也没有 iPhone 4 无法实验。

------------------------------------------------------------------------------------

让世界旋转起来:

  游戏世界的主摄像机就是我们在游戏里的“眼睛”,为了让“眼睛”能够与手持 iPad 的我们保持协调,跟随着我们自己转身而转动,好像我们自己就站在游戏世界里用自己的眼睛观察游戏世界一样,这里就需要解决几个数学问题。

  既然要旋转主摄像机,那么首先需要找到旋转的参照,因为我们旋转主摄像机的目的是为了观察游戏世界,那么这个参照自然选取游戏世界坐标系。主摄像机的 +Z 轴朝向就是我们通过 iPad 屏幕观察游戏世界时“眼睛”的观察方向,因此,只要我们能够找到主摄像机 +Z 轴方向向量与游戏世界坐标系下某个固定方向向量之间的角度关系,我们就可以通过 Quaternion(四元数,Unity 3D 里用四元数来描述空间旋转)来将主摄像机旋转到我们所期望的任意方位。

先有重力:

  上一节里说到重力感应的方向分量,iPad 接受到的重力输入是永远指向真实世界重力方向的,那么,假如我们创造的是一个正常的游戏世界的话,重力输入的方向其实就是游戏世界的 -Y 坐标轴方向,那么其反方向就是 +Y 轴方向。顺理成章,我们旋转主摄像机所需要的“游戏世界坐标系下某个固定方向向量”就是这个重力输入方向了。但是在计算主摄像机 +Z 轴方向与重力输入方向之间角度关系之前,还需要一个小换算,因为重力输入方向是以 iPad 屏幕坐标系度量的,而 iPad 屏幕坐标系与主摄像机自身坐标系之间 Z 轴是反向的,所以需要进行一次坐标变换,将重力输入方向向量从 iPad 屏幕坐标系变换到主摄像机坐标系。变换方法很简单:

Vector3 g = new Vector3(Input.acceleration.x, Input.acceleration.y, -Input.acceleration.z);


现在我们可以计算主摄像机 +Z 轴方向向量(0, 0, 1)与 g 之间的夹角,进而计算出主摄像机 +Z 轴方向向量变换到游戏世界坐标系下时的方向向量。这一扒拉计算需要用到球坐标系,寻找球坐标系坐标(r, θ, φ)对应直角坐标系下坐标点的步骤为:从原点出发,向 +Z 方向前进 r,然后依据右手定则,大拇指指向 +Y 轴方向,向其他四指的握转方向旋转 θ 角度,再依据右手定则,大拇指指向 +Z 轴方向,向其他四指握转方向旋转 φ 角度。如下图:


主摄像机 +Z 轴方向向量与 g 的夹角就是 π-θ,换言之 θ=π-夹角。现在这里出现了一个问题:θ 是知道了,那 φ 在哪?非常遗憾,如果我们用来定位的依据只有重力输入方向这一项数据的话,无法确定 φ。其实 θ 就是“天顶角”,也就是俯仰角,φ 是“方位角”,重力方向只能帮助我们确定俯仰角,方位角它是无能为力的,这需要借助罗盘或者陀螺仪。这在后文中会进一步解说,现在我们只需要先给 φ 定一个定值即可,比如 240度。

  仔细观察上图中的坐标系,又会发现一个问题:这个坐标系与我们使用的游戏世界坐标系长的不一样啊?没错,这个坐标系的 +Z 轴是游戏世界坐标系的 +Y 轴,它的 +Y 轴是游戏世界坐标系的 +Z 轴,刚好 +Z 和 +Y 轴调了一个个儿。没有关系,只需要将 x,y,z 的计算公式调整一下就行:

x = r sinθ cosφ
y = r cosθ
z = r sinθ sinφ

 代码也很简单:

using UnityEngine;
using System.Collections;
using System;

public class GSensorContoller : MonoBehaviour
{
private Transform myTransform = null;
private float x = 0.0f;
private float y = 0.0f;
private float z = 0.0f;
private Vector3 g;
private float theta = 0.0f;
private float phi = 0.0f;

void Awake()
{
myTransform = transform;
}

// Use this for initialization
void Start()
{
}

// Update is called once per frame
void Update()
{
Vector3 g = new Vector3(Input.acceleration.x, Input.acceleration.y, -Input.acceleration.z);
theta = Mathf.PI - Mathf.PI * Vector3.Angle(g, Vector3.forward) / 180.0f;
phi = Mathf.PI * 240.0f / 180.0f;
x = Mathf.Sin(theta) * Mathf.Cos(phi);
y = Mathf.Cos(theta);
z = Mathf.Sin(theta) * Mathf.Sin(phi);
Quaternion targetRotation = Quaternion.LookRotation(new Vector3(x, y, z), Vector3.up);

myTransform.rotation = targetRotation;
}

void OnGUI()
{
GUI.Box(new Rect(5, 5, 500, 25), String.Format("G  x:{0:0.000},y:{1:0.000},z:{2:0.000}", g.x, g.y, g.z));
GUI.Box(new Rect(5, 35, 500, 25), String.Format("theta:{0:0.000},phi:{1:0.000}", theta, phi));
GUI.Box(new Rect(5, 65, 500, 25), String.Format("Camera face  x:{0:0.000},y:{1:0.000},z:{2:0.000}", x, y, z));
GUI.Box(new Rect(5, 95, 500, 25), String.Format("Camera rotate x:{0:0.000},y:{1:0.000},z:{2:0.000}",
myTransform.rotation.eulerAngles.x, myTransform.rotation.eulerAngles.y, myTransform.rotation.eulerAngles.z));
}
}

在这段代码的作用下,已经能够让游戏里的主摄像机响应 iPad 的方位变化,虽然只是俯仰方向。但是在折腾 iPad 的时候会发现,只有抬起和放下 iPad 让俯仰角发生变化时,游戏画面会有响应,而像手握方向盘那样旋转 iPad 时,游戏画面保持不变,这看上去就比较怪异。我们期望的是:像手握方向盘那样旋转 iPad 时,画面向反方向旋转,不管 iPad 转成什么样,画面里的游戏世界始终与手持 iPad 的人保持相对静止。这一点,即便是只有重力输入,也能做到,只需要在 iPad 旋转时,将主摄像机向反方向绕 Z 轴旋转即可,空间中绕 Z 轴的旋转被称为 Roll,所以——

Let's ROLL:

  首先需要知道的是 iPad 到底旋转了多少角度,这同样需要找一个相对固定不变的参照。我们同样可以利用重力输入方向向量,只不过这一次需要的不只是一个向量,而是重力输入向量与屏幕坐标系 Z 轴形成的平面。因为这里处理的情况是 iPad 绕屏幕坐标系的 Z 轴旋转,因此 Z 轴方向和重力输入方向都可以认为是恒定不变的,也就是说由这两个方向向量决定的平面 GOZ 是不变的。那么 iPad 旋转的角度就是屏幕坐标系 Z 轴方向向量和 Y 轴方向向量决定的平面 YOZ 与参照平面 GOZ 的夹角。


  然后我们需要做的就是数学。希望看到这里的时候你还能够回想起高中和大学学过的空间解释几何。两个平面之间的夹角大小等于两个平面法向量之间的夹角大小。一个直角坐标方程为 ax + by + cz + d = 0 的平面,其法向量之一就是 (a, b, c)。经过不共线三点 (x0, y0, z0) (x1, y1. z1) (x2, y2. z2) 的平面的直角坐标方程可以通过下图的行列式求得:


至于如何求两向量的夹角,可以利用 Unity 3D 提供的函数:Vector3.Angle():

Vector3.Angle  

static function Angle (from : Vector3, to : Vector3) : float

Description

Returns the angle in degrees between from and to.

也可以自己用下图的公式求解:


平面 GOZ 可以认为它经过三个点 Z(0, 0, 1),O(0, 0, 0),G(x1, y1, z1),通过行列式计算得到平面的直角坐标方程 y1x - x1y = 0,其法向量为 (y1, -x1, 0);平面 YOZ 可以认为它经过三个点 Z(0, 0, 1),O(0, 0, 0),Y(0, 1, 0),通过行列式计算得到平面的直角坐标方程 x = 0,其法向量为 (1, 0, 0)。进一步算得 cosα = y1 / sqrt(y1 * y1 + (-x1) * (-x1))。这里 α 的取值范围是 [0, π],而我们求两个平面的夹角取值范围应该是 [0, π/2],所以我们想要的是 α = arccos(|y1 / sqrt(y1*y1 + x1 * x1)|)。接下来转换为代码:


using UnityEngine;

using System.Collections;

using System;


public class GSensorContoller : MonoBehaviour

{

private Transform myTransform = null;

private float x = 0.0f;

private float y = 0.0f;

private float z = 0.0f;

private Vector3 g;

private float theta = 0.0f;

private float phi = 0.0f;

private float turnAngle = 0.0f;


void Awake()

{

myTransform = transform;

}


// Use this for initialization

void Start()

{

}


// Update is called once per frame

void Update()

{

Vector3 g = new Vector3(Input.acceleration.x, Input.acceleration.y, -Input.acceleration.z);

theta = Mathf.PI - Mathf.PI * Vector3.Angle(g, Vector3.forward) / 180.0f;

phi = Mathf.PI * 240.0f / 180.0f;

x = Mathf.Sin(theta) * Mathf.Cos(phi);

y = Mathf.Cos(theta);

z = Mathf.Sin(theta) * Mathf.Sin(phi);

Quaternion targetRotation = Quaternion.LookRotation(new Vector3(x, y, z), Vector3.up);


float temp = Mathf.Sqrt(g.x * g.x + g.y * g.y);

if (temp != 0) turnAngle = Mathf.Acos(Mathf.Abs(g.y) / temp);

else turnAngle = 0.0f;

Quaternion targetTurn = Quaternion.AngleAxis(- turnAngle, Vector3.forward);


myTransform.rotation = targetRotation * targetTurn;

}


void OnGUI()

{

GUI.Box(new Rect(5, 5, 500, 25), String.Format("G  x:{0:0.000},y:{1:0.000},z:{2:0.000}", g.x, g.y, g.z));

GUI.Box(new Rect(5, 35, 500, 25), String.Format("theta:{0:0.000},phi:{1:0.000},turn:{2:0.000}", theta, phi, turnAngle));

GUI.Box(new Rect(5, 65, 500, 25), String.Format("Camera face  x:{0:0.000},y:{1:0.000},z:{2:0.000}", x, y, z));

GUI.Box(new Rect(5, 95, 500, 25), String.Format("Camera rotate x:{0:0.000},y:{1:0.000},z:{2:0.000}",

myTransform.rotation.eulerAngles.x, myTransform.rotation.eulerAngles.y, myTransform.rotation.eulerAngles.z));

}


}

上面这段代码当我们手持 iPad 在屏幕坐标系 +X 轴一侧旋转时一切正常,但到了 -X 轴一侧时却出现了问题——摄像机转向反了。这是因为主摄像机的转向也是有方向的,虽然在 -X 侧也是 GOZ 面和 YOZ 面夹角增大,但转向却与 +X 侧是相反的。要解决这个问题有多种办法,可以在计算两平面夹角时采用转角而不是夹角,更简单的办法是在 -X 侧和 +X 侧时分别对角度的正负做修正。代码调整如下:
using UnityEngine;
using System.Collections;
using System;

public class GSensorContoller : MonoBehaviour
{
private Transform myTransform = null;
private float x = 0.0f;
private float y = 0.0f;
private float z = 0.0f;
private Vector3 g;
private float theta = 0.0f;
private float phi = 0.0f;
private float turnAngle = 0.0f;

void Awake()
{
myTransform = transform;
}

// Use this for initialization
void Start()
{
}

// Update is called once per frame
void Update()
{
Vector3 g = new Vector3(Input.acceleration.x, Input.acceleration.y, -Input.acceleration.z);
theta = Mathf.PI - Mathf.PI * Vector3.Angle(g, Vector3.forward) / 180.0f;
phi = Mathf.PI * 240.0f / 180.0f;
x = Mathf.Sin(theta) * Mathf.Cos(phi);
y = Mathf.Cos(theta);
z = Mathf.Sin(theta) * Mathf.Sin(phi);
Quaternion targetRotation = Quaternion.LookRotation(new Vector3(x, y, z), Vector3.up);

float temp = Mathf.Sqrt(g.x * g.x + g.y * g.y);
if (temp != 0) turnAngle = Mathf.Acos(Mathf.Abs(g.y) / temp);
else turnAngle = 0.0f;
turnAngle = turnAngle * 180.0f / Mathf.PI * (g.x > 0.0f ? 1.0f : -1.0f);
Quaternion targetTurn = Quaternion.AngleAxis(- turnAngle, Vector3.forward);

myTransform.rotation = targetRotation * targetTurn;
}

void OnGUI()
{
GUI.Box(new Rect(5, 5, 500, 25), String.Format("G  x:{0:0.000},y:{1:0.000},z:{2:0.000}", g.x, g.y, g.z));
GUI.Box(new Rect(5, 35, 500, 25), String.Format("theta:{0:0.000},phi:{1:0.000},turn:{2:0.000}", theta, phi, turnAngle));
GUI.Box(new Rect(5, 65, 500, 25), String.Format("Camera face  x:{0:0.000},y:{1:0.000},z:{2:0.000}", x, y, z));
GUI.Box(new Rect(5, 95, 500, 25), String.Format("Camera rotate x:{0:0.000},y:{1:0.000},z:{2:0.000}",
myTransform.rotation.eulerAngles.x, myTransform.rotation.eulerAngles.y, myTransform.rotation.eulerAngles.z));
}
}
转向基本上正确了,但是画面抖动得很厉害。仔细观察重力输入的数值可以发现,数值在小数点后第二位之后很不稳定,即便是将 iPad 静置数值的变化也不会停止,这就导致后续计算的转角也频繁发生变化,摄像机抖动。要解决这个问题可以从两个方向出发,第一是降低对重力输入取值的精度,只有当重力输入数值该变量达到一定程度时才更新重力方向数值;第二是在摄像机旋转过程中加入线性插值,减缓摄像机旋转的速度,虽然这样会导致摄像机的反应延时,但同时能平稳摄像机的旋转。这两种方法互补干扰,可以同时采用:
UnityEngine;
using System.Collections;
using System;

public class GSensorContoller : MonoBehaviour
{
public float RotateSpeed = 1.0f;
private Transform myTransform = null;
private float x = 0.0f;
private float y = 0.0f;
private float z = 0.0f;
private Vector3 g;
private float theta = 0.0f;
private float phi = 0.0f;
private float turnAngle = 0.0f;

void Awake()
{
myTransform = transform;
}

// Use this for initialization
void Start()
{
}

// Update is called once per frame
void Update()
{
Vector3 g1 = new Vector3(Input.acceleration.x, Input.acceleration.y, -Input.acceleration.z);
if (Vector3.Angle(g, g1) > 1.0f) g = g1;
theta = Mathf.PI - Mathf.PI * Vector3.Angle(g, Vector3.forward) / 180.0f;
phi = Mathf.PI * 240.0f / 180.0f;
x = Mathf.Sin(theta) * Mathf.Cos(phi);
y = Mathf.Cos(theta);
z = Mathf.Sin(theta) * Mathf.Sin(phi);
Quaternion targetRotation = Quaternion.LookRotation(new Vector3(x, y, z), Vector3.up);

float temp = Mathf.Sqrt(g.x * g.x + g.y * g.y);
if (temp != 0) turnAngle = Mathf.Acos(- g.y / temp);
else turnAngle = 0.0f;
turnAngle = turnAngle * 180.0f / Mathf.PI * (g.x > 0.0f ? 1.0f : -1.0f);
Quaternion targetTurn = Quaternion.AngleAxis(- turnAngle, Vector3.forward);

myTransform.rotation = Quaternion.Slerp(myTransform.rotation, targetRotation * targetTurn, Time.deltaTime * rotateSpeed);
}

void OnGUI()
{
GUI.Box(new Rect(5, 5, 500, 25), String.Format("G  x:{0:0.000},y:{1:0.000},z:{2:0.000}", g.x, g.y, g.z));
GUI.Box(new Rect(5, 35, 500, 25), String.Format("theta:{0:0.000},phi:{1:0.000},turn:{2:0.000}", theta, phi, turnAngle));
GUI.Box(new Rect(5, 65, 500, 25), String.Format("Camera face  x:{0:0.000},y:{1:0.000},z:{2:0.000}", x, y, z));
GUI.Box(new Rect(5, 95, 500, 25), String.Format("Camera rotate x:{0:0.000},y:{1:0.000},z:{2:0.000}",
myTransform.rotation.eulerAngles.x, myTransform.rotation.eulerAngles.y, myTransform.rotation.eulerAngles.z));
}

}
上面代码中判断当两次重力输入方向的夹角超过 1 度时才更新重力输入数据,而且通过球形插值来平滑摄像机的转动。现在感觉就好多了,虽然当 iPad 转过天顶和脚底这两个特殊位置时摄像机的旋转与预想的有出入,但基本上达成了我们的目的。更细致的调整可以放在加入了罗盘数据输入之后由罗盘数据替代重力感应数据完成转角的计算。

Reference: http://blog.csdn.net/lijing_hi/article/details/7272086
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值