😉

[Swift]端末のPitch角を-180°~180°で取得する

2022/10/20に公開

はじめに

SwiftでCoreMotionを使用すると、Pitch角(端末のx軸回りの回転)が取得可能なのですが、-90°~90°の範囲でしか取得できず困ったので、Quaternionを使用して-180°~180°の範囲でPitch角を取得しました。

環境

  • Xcode 14.0.1
  • Swift 5.7

問題

前提

端末の角度を取得したい場面があり、以下の公式ドキュメントなどを参考にして端末の角度を取得してみました。
https://developer.apple.com/documentation/coremotion/getting_processed_device-motion_data/understanding_reference_frames_and_device_attitude
これを読んでみると、Pitch、Roll、Yawを-180°~180°の範囲で取得できそうです。
ただし、Yawはそこまで精度が高くないと思われる(Yawは地磁気から推定可能だが、地磁気以外にも残留磁気などが存在し、地磁気のみを正確に取得できないから?)ため、今回はYawについては触れません。

実装してみる

CoreMotionを使用して、端末の角度を取得してみます。以下のコードは、CoreMotionからPitch、Rollを取得するコードの一部です。
ゆっきぶろぐさんのサイトが大変参考になりました。

let motionManager = CMMotionManager()
    
    func start() {
        if motionManager.isDeviceMotionAvailable {
            motionManager.deviceMotionUpdateInterval = 1 //最大値100Hz(1 / 100)
            motionManager.startDeviceMotionUpdates(using: [.xMagneticNorthZVertical], to: OperationQueue.current!, withHandler: {(motion:CMDeviceMotion?, error:Error?) in
                        self.updateMotionData(deviceMotion: motion)
                    })
        }
    }
    
    private func updateMotionData(deviceMotion: CMDeviceMotion?) { 
        guard let attitude = deviceMotion?.attitude else { return }

        //radian -> degree
        let pitch = attitude.pitch * 180 / Double.pi //x
        let roll = attitude.roll * 180 / Double.pi  //y
        //let yaw = attitude.yaw * 180 / Double.pi   //z
    }

取得してみる

Rollについては、-180°~180°の範囲で取得することができましたが、Pitchについては-90°~90°(0°~90°、90°~0°、0°~-90°、-90°~0°)の範囲でしか取得できないことがわかりました。
これはオイラー角のジンバルロックが関係していそうですが詳しくわかりません。。。
とにかく結果としてCMDeviceMotion.attitude.pitchを使用するとPitchを-180°~180°の範囲で取得できないことがわかりました。

解決策

クォータニオンを使用する

そこで、クォータニオンからPitchを取得してみます。クォータニオンの説明については省略するので、詳しく知りたい方は調べてみてください。
クォータニオンからPitchへの変換は、式(1)で変換することができます。(参考:Conversion between quaternions and Euler angles

arctan{\cfrac{2(q_wq_x+q_yq_z)}{1-2(q_x^2+q_y^2)}}\space\space\space\space\space(1)

また、arctanは-\frac{\pi}{2}~\frac{\pi}{2}の範囲のみであるため、式(2)のatan2を用います。(式1でも取得可能かも知れませんが試していません)

atan2(2(q_wq_x+q_yq_z),\space 1-2(q_x^2+q_y^2))\space\space\space\space\space(2)

実装してみる

クォータニオンは、Pitch、Roll、Yawと同様にdeviceMotion.attitudeから取得する可能です。

        guard let attitude = deviceMotion?.attitude else { return }
        
        let qw = attitude.quaternion.w
        let qx = attitude.quaternion.x
        let qy = attitude.quaternion.y
        let qz = attitude.quaternion.z

        let qpitch = atan2((2 * (qw * qx + qy * qz)), 1 - 2 * (qx * qx + qy * qy))

終わり

クォータニオンを使用することで無事Pitchを-180°~180°の範囲で取得できるようになりました!

参考文献

Discussion