次の方法で共有


快適な動作

Windows Phone のコンパスに親しむ

Charles Petzold

コード サンプルのダウンロード

Charles Petzold人間は、五感をまったく別々に使用することはほとんどありません。周囲の状況を思い浮かべるときは、周囲に目を配り、耳を研ぎ澄まします。味わい、匂いを嗅ぎ、触れることによって食事を楽しみます。楽器を演奏するときは、楽器に触れ、楽譜を読み、音に耳を傾けます。

これは、スマートフォンにも当てはまります。スマートフォンは、カメラのレンズを通して "見"、マイクを通して "聴き"、タッチスクリーンを使って "触れ"、GPS や方向センサーを使って地球上での位置を把握します。しかし、このようなセンサーからの入力を組み合わせるとどうなるかをきちんと説明するのは難しく、相乗効果があるとしか言いようがありません。

加速度計の問題

Windows Phone の加速度計は、重要な情報を提供するセンサーの優れた例ですが、別のセンサー、とりわけコンパスと組み合わせるとさらに威力を発揮します。

加速度計のハードウェア自体は、実際には力を測定しています。しかし、物理学からわかるように、力は質量と加速度の積で求められるため、加速度計はあらゆる加速度に反応します。携帯電話を静止させると、加速度計は重力を測定し、3D ベクトルは地球の中心を指します。このベクトルは、3D 座標系に相対です (図 1 参照)。Silverlight プログラム と XNA プログラムのどちらをコーディングしているか、または縦長モードと横長モードのどちらを実行しているかにかかわらず、座標系は同じです。

The Phone’s Sensor Coordinate System
図 1 携帯電話組み込みのセンサーの座標系

Accelerometer クラスは、加速度ベクトルを Vector3 値の形式で提供します。この値は XNA 型なので、Silverlight プログラムでこの値を使用する必要がある場合は、Microsoft.Xna.Framework アセンブリへの参照が必要です。

加速度ベクトルを使用すると、携帯電話でプログラムを実行して地球に対する携帯電話の向きを特定できますが、重要な情報がいくつか抜け落ちています。これを実演してみましょう。

このコラムのダウンロード可能なコード (archive.msdn.microsoft.com/mag201206TouchAndGo (英語) から入手できます) は、(やや似通った) 4 つの XNA プロジェクトが同梱された、3DPointers という Visual Studio ソリューションです。Accelerometer 3D プログラムでは、加速度ベクトルの方向を向き、空間に浮遊する 3 次元の "ピン" が描画されます (図 2 参照)。

The Accelerometer 3D Display
図 2 Accelerometer 3D の表示

Windows Phone センサーを使用するすべてのプログラムは、Microsoft.Devices.Sensors アセンブリへの参照が必要です。通常、携帯電話の XNA プログラムは横長モードで実行しますが、横長モードはセンサーで使用する座標系と一致していないため、問題が発生することがあります。作業を行いやすくするために、ここではプログラムの Game 派生クラスのコンストラクターで、XNA の座標系を縦長モードに向きを設定し直します。

graphics.IsFullScreen = true;
graphics.PreferredBackBufferWidth = 480;
graphics.PreferredBackBufferHeight = 800;

Windows Phone 7.1 では、多種多様なセンサー間での一貫性を確保するため、センサー API に少々変更が加えられています。Accelerometer 3D プログラムのコンストラクターでは、この新しい API を使用して、フィールドとして保存される Accelerometer インスタンスを作成します。

if (Accelerometer.IsSupported)
{
  accelerometer = new Accelerometer
  {
    TimeBetweenUpdates = this.TargetElapsedTime
  };
}

TimeBetweenUpdates プロパティの既定値は 25 ミリ秒ですが、今回はプログラムのフレーム レートの 33 ミリ秒に設定しています。

Accelerometer を開始するには、プログラムで OnActivated メソッドのオーバーライドを使用します。

if (accelerometer != null)
{
  try { accelerometer.Start(); }
  catch { }
}

この時点では Start メソッドがエラーになるシナリオを想像しにくいのですが、Start メソッドを try ブロックに含めることをお勧めします。コンパスは OnDeactivated で停止します。

if (accelerometer != null)
    accelerometer.Stop();

プログラムで LoadContent メソッドを使用して "ピン" の 3D 頂点を作成し、カメラとライティングの情報を格納する BasicEffect を定義します。ピンは、原点を基準に、Y 軸の正方向に伸びるよう定義します。カメラは、正の Z 軸から直接原点を指します。

次に、プログラムの Update メソッドで、加速度ベクトルを使ってワールド トランスフォームを定義します。このワールド トランスフォーム では、事実上、原点を基準にピンを動かすことができます。図 3 にそのコードを示します。

図 3 Accelerometer 3D の Update メソッド

protected override void Update(GameTime gameTime)
{
  if (GamePad.GetState(PlayerIndex.One).Buttons.Back 
    == ButtonState.Pressed)
      this.Exit();
  if (accelerometer != null && accelerometer.IsDataValid)
  {
    Vector3 acceleration = accelerometer.CurrentValue.Acceleration;
    text = String.Format("X = {0:F3}\nY = {1:F3}\nZ = {2:F3}",
                            acceleration.X,
                            acceleration.Y,
                            acceleration.Z);
    textPosition = new Vector2(0, this.GraphicsDevice.Viewport.Height -
                                    segoe24Font.MeasureString(text).Y);
    acceleration.Normalize();
    Vector3 axis = Vector3.Cross(acceleration, Vector3.UnitY);
    // Special case for magnetometer equal to (0, 1, 0) or (0, -1, 0)
    if (axis.LengthSquared() == 0)
        axis = Vector3.UnitX;
    else
        axis.Normalize();
    float angle = -(float)Math.Acos(Vector3.Dot(Vector3.UnitY, 
      acceleration));
    basicEffect.World = Matrix.CreateFromAxisAngle(axis, angle);
  }
  else
  {
    basicEffect.World = Matrix.Identity;
    text = "";
  }
  base.Update(gameTime);
}

IsDataValid プロパティが true の場合、このメソッドは、Accelerometer オブジェクトから直接 Acceleration 値を取得します。ピンは、加速度ベクトルと正の Y 軸との間の角度に基づいて回転させる必要があります。この角度は、これら 2 ベクトルのドット積によって算出し、回転軸はクロス積によって算出します。

しかし、次のことを試してみてください。立ち上がり、携帯電話を一定の角度で手に持ちます。ピンは下方向を指します。次に、その場に立ったまま、360 度回転します。回転している間、ピンにはまったく (またはほとんど) 変化がありません。Accelerometer クラスでは、携帯電話が加速度ベクトルに平行な軸を中心とした動きは認識できません。携帯電話の完全な 3 次元の向きを把握する必要のあるアプリケーションを作成している場合、加速度計だけでは必要な情報をすべて把握することはできません。

救いのコンパス

Windows Phone が最初にリリースされたときは、コンパスが携帯電話に組み込まれているかどうかもよく知られていませんでした。決定的な答えはだれも持っていないように見えましたが、アプリケーション プログラマにしてみれば、この状況は実に単純で、当時はコンパスのプログラミング インターフェイスがなかったため、コンパスが存在したとしても利用することはできませんでした。

Windows Phone 7.1 のリリースによってこの問題が明らかになりました。Windows Phone デバイスにコンパスは必須ではありませんが、必ずコンパスが組み込まれている Windows Phone デバイスもありました。2010 年の 12 月に私が購入した、Windows Phone 搭載のスマートフォンもその 1 つです。何よりすばらしいのは、このコンパスをプログラマが実際に使用できることです。Windows Phone 7.1 には新しく Compass クラスが導入されました。アプリケーション プログラマはこのクラスを使い、(Compass.IsSupported 静的プロパティによって) コンパスの有無を調べることができ、コンパスが読み取る情報にアクセスできます。Windows Phone エミュレーターでは、Compass.IsSupported が false を返します。

携帯電話のコンパスの基盤となっているのは、磁力計と呼ばれる 1 つのハードウェアです。この磁力計は、携帯電話近くのあらゆる磁力の影響を受けます (コンピューター デスク上のスピーカーから発せられる磁力が影響を及ぼすこともあります)。磁力を帯びたものを携帯電話から遠ざけると、磁力計は地球の磁場の強さと方向を測定します。

Compass クラスは、CompassReading 構造体という形式でデータを提供します。何も手を加えない磁力計データそのものは、MagnetometerReading プロパティで使用できます。このプロパティは、図 1 で示した座標系を基準とする別の Vector3 になります。磁力を帯びたものが近くに存在しなければ、このベクトルは地球の磁場の方向を指します。ベクトルは、当然北方向を指しますが、北半球のほとんどの場所では地球内部を指し示します。画面を上に向けて携帯電話を持つと、このベクトルの -Z 成分は非常に大きくなります。

3DPointers ソリューションには、Magnetometer 3D というプロジェクトが含まれています。Accelerometer クラスではなく Compass クラスを使用するという点以外は、Accelerometer 3D プロジェクトに似たプロジェクトです。図 4 は、マンハッタンにある私のアパートで携帯電話を持ち、画面を上にして先端を "アップタウン" (マンハッタンの 59 番通りから北の地域) に向けると、このプログラムがどのように表示されるかを示してます。つまり、携帯電話の両側を、ニューヨークを南北に走る大通りと (できる範囲で) 平行にします (地図を見ると、これらの大通りは北から 30 度東に傾いていることがわかります)。


図 4 Magnetometer 3D の表示

資料によると、MagnetometerReading ベクトルの単位はマイクロテスラです。私が所有する市販の Windows Phone デバイスのうち 2 台では、このベクトルはだいたい 30 度台の角度を示します。この値はおおよそ正確なものです (図 4 で示しているベクトルの値は、43 度という複合的な角度になっています)。しかし、3 台目の携帯電話では、MagnetometerReading のベクトルが正規化され、常に 1 度という角度を示します。

では、今度は次の動作を試してみます。Magnetometer 3D のベクトルが、図 1 の正の Y 軸とほぼ一直線上になるよう携帯電話を持ちます。次に、そのベクトルと平行な軸を中心に携帯電話を回転させます。ベクトルはまったく (またはほとんど) 変化せず、磁力計でも携帯電話の向きに関する完全な情報を把握できないことを表します。

コンパスの方位

通常、コンパスの使用中に、3D ベクトルが地球の磁場と平行になることは好ましくありません。これでは地表に接する 2D ベクトルの方が、はるかに役に立つでしょう。

おそらくご存知でしょうが、地球の磁場と自転軸は同じではありません。地軸が向いている方向は、北極または真北と呼ばれます。この方向は、地図など、事実上すべての目的で使用される北に当たります。2 次元平面上の角度は、北の方向を表すのによく使用されます。この方向は、多くの場合、方位または方角と呼ばれます。

磁北と真北の差は、世界中のすべての場所で異なります。ニューヨークでは、真方位を求めるのに磁方位から約 13 度を差し引く必要がありますが、シアトルでは、磁方位に 21 度を加えます。Compass クラスは、携帯電話の位置を基にこれらの計算を実行します。CompassReading には、MagnetometerReading のベクトル以外に、MagneticHeading と TrueHeading という 2 つの double 型のプロパティが用意されています。これらはどちらも、図 1 で示している正の Y 軸から反時計回りに測定された 0 ~ 360 度の角度の値です。

TrueHeading は、常に近似値として解釈します。また、近似値としても完全には信頼しないようにします。TrueHeading は、私が所有する携帯電話のうちの 2 台では多くの場合ほぼ正確ですが、残りの 1 台では 70 度もの誤差があります。

MagneticHeading 値の意味については、理解できたためしがありません。特定の場所では、TrueHeading と MagneticHeading の値の差は一定です。たとえば、私の居住地では、TrueHeading 値から MagneticHeading 値を引くと約 –13 度になります。3 台すべての携帯電話では、TrueHeading と MagneticHeading との間の差は、携帯電話の向きに基づいて散発的に 2 つの値の間を遷移します。差は、(ほぼ正確な) –12 になることもありますが、ほとんどの場合は 92 になります。この 2 つ以外の値は目にしたことがありません。私の携帯電話のいずれにおいても、MagneticHeading は、MagnetometerReading ベクトルの X 値と Y 値から算出された角度とは一致しません。

既に説明したように、XNA プログラムでは、Update メソッド内でセンサーから簡単に現在値を取得することができます。Silverlight プログラムで Sensor クラスを使用する場合は、CurrentValueChanged イベントのハンドラーを設定することをお勧めします。その後、センサーの読み取りオブジェクトをイベント引数から取得します。

このコラムのダウンロード可能なコードには、2 つの Silverlight プログラム (Arrow Compass と Dial Compass) を含めています。これらのプログラムでは、北方向を示すのに TrueHeading プロパティを使用します。グラフィックは、すべて XAML で定義しています。これらの Silverlight プログラムでは、XNA プログラムと同様にコンストラクターで Compass オブジェクトを作成しますが、CurrentValueChanged プロパティのハンドラーも設定します。

if (Compass.IsSupported)
{
  compass = new Compass();
  compass.TimeBetweenUpdates = TimeSpan.FromMilliseconds(33);
  compass.CurrentValueChanged += OnCompassCurrentValueChanged;
}

Arrow Compass では、このハンドラーを使って、矢印のグラフィックに関連付けられている RotateTransform オブジェクトの角度を設定します。

this.Dispatcher.BeginInvoke(() =>
{
  arrowRotate.Angle = -args.SensorReading.TrueHeading;
  accuracyText.Text = String.Format("±{0}°",
                      args.SensorReading.HeadingAccuracy);
});

CurrentValueChanged ハンドラーは別のスレッドで呼び出されるため、Dispatcher を使用してすべての UI オブジェクトを更新することが必要になります。TrueHeading の角度が反時計回りによる誤差を示すのに対して、Silverlight での回転は時計回りです。したがって、コードでは、回転における TrueHeading の方位に関する角度に負の値を使用します。

結果は図 5 のようになり、携帯電話が再びニューヨークのアップタウンを指します。

The Arrow Compass Display
図 5 Arrow Compass の表示

Dial Compass ではダイヤルが回転し、矢印は方向を示すために固定されたままになります (図 6 参照)。この方法は、携帯電話から見た北の方角を把握するのではなく、携帯電話の先端が指している方向を把握する場合に使用します。

The Dial Compass Display
図 6 Dial Compass の表示

これら 2 つのプログラムのどちらかを実行し、携帯電話を持って画面を下向きにすると、コンパスは正確に機能しなくなります。回転が本来の回転と逆向きになるためです。これを修正することが必要な場合は、加速度ベクトルの Z 値が正のときに、TrueHeading 値の正の値を使用します。

コンパスを調整する

Arrow Compass プログラムでは、右下隅に CompassReading 値の HeadingAccuracy プロパティを表示します。理論的には、これにより方位の値の正確性が確保されます。実際には、5% ~ 30% の HeadingAccuracy 値が表示されます (ただし、携帯電話で 5% 以上外れたのは見たことがありません)。

また、Compass クラスでは、Calibrate というイベントも定義します。このイベントは、HeadingAccuracy 値が 20% を超えると発生します。

この HeadingAccuracy は、調整操作を実行することで減らすことができます。携帯電話を体から離して持ち、画面を左右どちらかに向けて、無限大のマークを描きながら腕を複数回横に動かします。MSDN のチュートリアル (bit.ly/yYrHrL) で提供されているサンプル コードの一部には、コンパスに調整が必要であることをユーザーに表示して通知できるグラフィックも用意されています。

コンパスと加速度計を組み合わせる

携帯電話のコンパスは、(特に森で道に迷った場合に) コンパス自体による実用性がいくらか備わっているセンサーとして、申し分のない例です。しかし、このセンサーは他のセンサー、特に加速度計と組み合わせるとさらなる実力を発揮します。この 2 つのセンサーは、一緒に使用することで携帯電話の完全な向きを 3D 空間に示すことができます。

実際、Windows Phone 7.1 では、まさにこの処理を実行できる新しいクラスが定義されています。携帯電話の向きを特定する 3 種類の方法以外にも、Motion クラスでは新しい Gyroscope クラスが携帯電話上に存在する場合、Gyroscope クラスからも情報を取得することができます。

さらに、Motion クラスでは、Accelerometer と Compass からのデータに修正を加えることで、可能な処理を増やすことができます。これまで説明したプログラムを実行している場合は、これらのクラスから取得したデータに大きなジッターがあることにお気付きかもしれません。このジッターは、Motion クラスからすべて生じます。

しかし、私は前向きな難題を楽しむ方なので、Accelerometer と Compass のデータを "手動で" 組み合わせてみようと考えました。そしてこの経験により、Motion クラスに関する理解をさらに深めることができたと認めざるを得ません。

Compass 3D プログラムでは、北 (銀色)、東 (赤色)、南 (緑色)、および西 (青色) を指し示すよう、4 色で色付けしたピンを円状に配置して表示します。このプログラムでは、地表と平行に、コンパスの 4 点を正確に指すピンの平面表示を試みます。

ここでは、オイラー角を求める方法を採用しました。オイラー角は、X、Y、および Z 軸を中心とした回転を表す 3 つの角度です。これらの角度を組み合わせて使用すると、3D 空間での向きを求めることができます。飛行力学では、この 3 つの角度をピッチ、ロール、およびヨーと呼びます。航空機の観点から見ると、ピッチは機首が上下どちらを向いているかを示し、ロールは機体の左右の傾きを示します。これらの回転角は、2 つの軸相対に表示することができます。ピッチは、翼から伸びる軸を中心とした回転です。ロールは、機体の前方から後方に向かう軸を基準とします。ヨーは、地表に垂直な軸を中心とした回転で、機体のコンパス方位を示します。

図 1 に示した携帯電話の座標系に関連してこれらの角度を表示するには、先端を前に、3 つのボタンを背にして、携帯電話に魔法のじゅうたんのように乗り、画面に座っているところを想像してみてください。ピッチは X 軸を中心とした回転、ロールは Y 軸を中心とした回転、ヨーは Z 軸を中心とした回転になります。

加速度ベクトルからのロールとピッチの算出はきわめて簡単で、基本的な公式で求めることができます。

float roll = (float)Math.Asin(-acceleration.X);
float pitch = (float)Math.Atan2(acceleration.Y, -acceleration.Z);

携帯電話の画面を上にしてテーブルに置いた状態では、ロールもピッチも 0 です。ロールは –π/2 ~ π/2 の範囲で推移し、携帯電話の画面を下に向けると、値は 0 に戻ります。ピッチは、–π ~ π の範囲で推移し、携帯電話の画面を下に向けると、値は最大値になります。画面を上に向けると、ヨーは Compass の TrueHeading とだいたい同じ値になりますが、XNA で使用するためラジアンに変換します。

float yaw = MathHelper.ToRadians((float)compass.CurrentValue.TrueHeading);

しかし、コンパスを使用した Silverlight の 2 つのプログラムからわかるように、携帯電話の画面が地面に向くと、TrueHeading は正常に動作しなくなります。したがって、これに対する修正をヨーで行う必要があります。この主題に関する理論と実験を行った後、私はこれをそのまま残し、3 つの角度からワールド トランスフォームを作成しました。

basicEffect.World = Matrix.CreateRotationZ(yaw) *
                    Matrix.CreateRotationY(roll) *
                    Matrix.CreateRotationX(pitch);

結果を図 7 に示します。

The Compass 3D Program
図 7 Compass 3D プログラム

また、この 3 つの角度を Motion クラスから取得する Orientation 3D プログラムも含めました。(携帯電話が裏返しになっているときの動作が向上するだけでなく) いかに円滑な結果が得られるかおわかりいただけると思います。

Motion クラスは、このたった 1 つのプログラムでは、センサー API の追加よりも重要です。次号のコラムでわかるように、このクラスは実際に 3 次元世界への入り口として機能します。

Charles Petzold は MSDN Magazine の記事を長期にわたって担当している寄稿者です。彼の Web サイトは charlespetzold.com (英語) です。

この記事のレビューに協力してくれた技術スタッフの Drew Batchelor に心より感謝いたします。