awolCZ
awolCZ

Reputation: 31

Getting Y rotation of ARKit pointOfView

I'm trying to get the real life angle of the point of view in ARKit scene (0 - 360 degrees). I'm using euler angles from SCNNode of pointOfView.

print("\(pointOfView.eulerAngles.y.radiansToDegrees)")

Problem is, that when looking north, I'm getting 0 as a result and when looking south, I'm also getting 0. When looking NE, I get -45 degrees and when looking SE, I also get -45 degrees. Seems like SCNNode can not determine between North and South, only between West and East. Any advice?

I generally need to implement radar view in my ARKit real world scene. And expected behavior is North: 0, East: 90, South: 180, West: 270.

Thanks in advance!

Upvotes: 2

Views: 850

Answers (1)

Hari Honor
Hari Honor

Reputation: 8914

I've just been working on a similar situation. What you are after I call the "heading" which isn't as easy to define cleanly as you might think.

Quick background: FYI, there are two kinds of rotation, "Euler" which are relative to the real world space but which suffer what they call Gimbal Lock at the "pitch" extremes. And then there are the rotation angles relative to the device's axis, held in the transform property of ARCamera.

To illustrate the difference euler.y alway means the way the device is facing (except when it is flat in which case gimbal lock mucks it up, hence our problem), whereas the transform y always means rotation around the vertical axis through the phone (which, just to make things extra confusing, is based on the device held landscape in ARKit).

(Side note: If you are used to CoreMotion, you may have notice that in ARKit, Gimbal Lock occurs when the device is held flat, whereas in CM it is upright).

So how do we get a "heading" that works whether the device is flat or upright? The solution below (sorry it's objective-c!) does the following:

  1. Take two normal vector, one along the phone's Z axis (straight out from the screen) and one that sticks out the bottom of the phone, which I call the -Y axis (though it's actually the +X axis when held landscape).

  2. Rotate the vector by the device's transform (not the Eulers), project onto the XZ plane and get the angle of the projected vectors wrt the Z-axis.

  3. When the phone is upright, the Z Normal will be the perfect heading, but when the phone is flat, the Y normal is the one to use. In between we'll "crossfade" based on the phone's "tilt", ie the euler.x.

  4. One small issue is the when user holds the phone slightly down past flat, the heading given by the Z Normal flips. We don't really want that (more from a UX perspective than a mathematical one) so let's detect this "downward tilt" and flip the zHeading 180˚ when it happens.

The end result is a consistent and smooth heading regardless of the device orientation. It even works when the device is changed moved between portrait and landscape...huzzah!

// Create a Quaternion representing the devices curent rotation (NOT the same as the euler angles!)
GLKMatrix3 deviceRotM = GLKMatrix4GetMatrix3(SCNMatrix4ToGLKMatrix4(SCNMatrix4FromMat4(camera.transform)));
GLKQuaternion Q = GLKQuaternionMakeWithMatrix3(deviceRotM);

// We want to use the phone's Z normal (in the phone's reference frame) projected onto XZ to get the angle when the phone is upright BUT the Y normal when it's horizontal. We'll crossfade between the two based on the phone tilt (euler x)...
GLKVector3 phoneZNormal = GLKQuaternionRotateVector3(Q, GLKVector3Make(0, 0, 1));
GLKVector3 phoneYNormal = GLKQuaternionRotateVector3(Q, GLKVector3Make(1, 0, 0)); // why 1,0,0? Rotation=(0,0,0) is when the phone is landscape and upright. We want the vector that will point to +Z when the phone is portrait and flat

float zHeading = atan2f(phoneZNormal.x, phoneZNormal.z);
float yHeading = atan2f(phoneYNormal.x, phoneYNormal.z);

// Flip the zHeading if phone is tilting down, ie. the normal pointing down the device suddenly has a +y component
BOOL isDownTilt = phoneYNormal.y > 0;
if (isDownTilt) {
    zHeading = zHeading + M_PI;
    if (zHeading > M_PI) {
        zHeading -= 2 * M_PI;
    }
}

float a = fabs(camera.eulerAngles.x / M_PI_2);
float heading = a * yHeading + (1 - a) * zHeading;

NSLog(@"euler: %3.1f˚   %3.1f˚   %3.1f˚    zHeading=%3.1f˚    yHeading=%3.1f˚    heading=%3.1f˚    a=%.2f    status:%li:%li  zNorm=(%3.2f, %3.2f, %3.2f)    yNorm=(%3.2f, %3.2f, %3.2f)", GLKMathRadiansToDegrees(camera.eulerAngles.x), GLKMathRadiansToDegrees(camera.eulerAngles.y), GLKMathRadiansToDegrees(camera.eulerAngles.z), GLKMathRadiansToDegrees(zHeading), GLKMathRadiansToDegrees(yHeading), GLKMathRadiansToDegrees(heading), a, camera.trackingState, camera.trackingStateReason, phoneZNormal.x, phoneZNormal.y, phoneZNormal.z, phoneYNormal.x, phoneYNormal.y, phoneYNormal.z);

Upvotes: 2

Related Questions