18
votes

I am having a hard time mapping device motion (sensor fusion) to SceneKit node rotation. The premise of the problem is as follows,

I have sphere, and the camera is positioned to be inside the the sphere such that the geometric center of sphere and camera node's center coincide. What i want to achieve is when i rotate physically around a point, the motion needs to be mapped accurately on the camera as well.

The implementaion details are as follows:

I have a node, with a sphere as geomertry and is a child to root node. I am using sensor fusion to get attitude quternions and then converting them to euler angles. The code for that is:

- (SCNVector3)eulerAnglesFromCMQuaternion:(CMQuaternion) {
    GLKQuaternion gq1 =  GLKQuaternionMakeWithAngleAndAxis(GLKMathDegreesToRadians(90), 1, 0, 0);
    GLKQuaternion gq2 =  GLKQuaternionMake(q.x, q.y, q.z, q.w);
    GLKQuaternion qp  =  GLKQuaternionMultiply(gq1, gq2);
    CMQuaternion rq =   {.x = qp.x, .y = qp.y, .z = qp.z, .w = qp.w};
    CGFloat roll = atan2(-rq.x*rq.z - rq.w*rq.y, .5 - rq.y*rq.y - rq.z*rq.z);
    CGFloat pitch = asin(-2*(rq.y*rq.z + rq.w*rq.x));
    CGFloat yaw = atan2(rq.x*rq.y - rq.w*rq.z, .5 - rq.x*rq.x - rq.z*rq.z);
    return SCNVector3Make(roll, pitch, yaw);
}

The conversion equation is from 3D Math Primer for Graphics and Game Development. I have used to book as a reference, haven't read it. But definitely on my reading list.

Now to simulate the physical rotation in SceneKit, am rotating my node containing SCNCamera. This is a child to rootNode. And am using .rotation property to do the same. One thing to note here is that Now my device motion update thread looks something like this

[self.motionManager startDeviceMotionUpdatesUsingReferenceFrame:CMAttitudeReferenceFrameXArbitraryCorrectedZVertical toQueue:mq withHandler:^(CMDeviceMotion *motion, NSError *error) {
    CMAttitude *currentAttitude = motion.attitude;
    [SCNTransaction begin];
    [SCNTransaction setDisableActions:YES];
    SCNVector3 v = [self eulerAnglesFromCMQuaternion:currentAttitude.quaternion];
    self.cameraNode.eulerAngles = SCNVector3Make( v.y, -v.x, 0); //SCNVector3Make(roll, yaw, pitch)
    [SCNTransaction commit];
}];

Something of caution here is the euler angles are different for device and for SceneKit node. The links explain them.

SceneKit Euler Angles

CMAttitude Euler Angles

And in my understanding the mapping between two goes something like this.

[Image]

Now the problem I am facing is, the camera's 360 degree rotation is over before my physical rotation about a point is. Which am trying to dpecit via the following image.

[Image2]

I doubt the quaternion -> euler angle conversion is causing the error, and quaternion math is way over my head as of now.

I hope i have provided all the information, if anything more is required am only glad to add.

2
This answer solves for all device orientations: stackoverflow.com/questions/31823012/…Alfie Hanssen

2 Answers

11
votes

Thanks for your description of your problem and the beginning of a solution! I am quite sure that this can not be done with the Euler angles, because of the gimbal lock (loose of 1 degree of freedom).

The solution that worked for is to get the attitude and use the quaternion to set the orientation of the camera:

[self.motionManager startDeviceMotionUpdatesUsingReferenceFrame:CMAttitudeReferenceFrameXArbitraryCorrectedZVertical toQueue:myQueue withHandler:^(CMDeviceMotion *motion, NSError *error) {
    CMAttitude *currentAttitude = motion.attitude;
    [SCNTransaction begin];
    [SCNTransaction setDisableActions:YES];

    SCNQuaternion quaternion = [self orientationFromCMQuaternion:currentAttitude.quaternion];
    _cameraNode.orientation = quaternion;

    [SCNTransaction commit];
}];
- (SCNQuaternion)orientationFromCMQuaternion:(CMQuaternion)q
{
    GLKQuaternion gq1 =  GLKQuaternionMakeWithAngleAndAxis(GLKMathDegreesToRadians(-90), 1, 0, 0); // add a rotation of the pitch 90 degrees
    GLKQuaternion gq2 =  GLKQuaternionMake(q.x, q.y, q.z, q.w); // the current orientation
    GLKQuaternion qp  =  GLKQuaternionMultiply(gq1, gq2); // get the "new" orientation
    CMQuaternion rq =   {.x = qp.x, .y = qp.y, .z = qp.z, .w = qp.w}; 

    return SCNVector4Make(rq.x, rq.y, rq.z, rq.w);
}
3
votes

If CoreMotion and SceneKit don't use the same convention for euler angles then you can probably use intermediate nodes to simplify the conversion. For example by having a hierarchy like:

YawNode
  |
  |___PitchNode
      |
      |___RollNode
          |
          |___CameraNode

YawNode, PitchNode, RollNode would rotate only around one single axis. Then by playing on the hierarchy order you can reproduce CoreMotion's convention.

That being said, I'm not sure the conversion quaternion -> euler is a great idea. I suspect that you won't get a continuous/smooth transition at angles like 0 / PI / 2PI... I would try quaternion->matrix4 instead and update the "transform" of your node.