Alfie Hanssen Alfie Hanssen - 2 months ago 30
Swift Question

How to use a pan gesture to rotate a camera in SceneKit using quaternions

I'm building a 360 video viewer using the iOS SceneKit framework.

I'd like to use a UIPanGestureRecognizer to control the camera's orientation.

SCNNodes have several properties we can use to specify their rotation:

rotation
(a rotation matrix),
orientation
(a quaternion),
eulerAngles
(per axis angles).

Everything I've read says to avoid using euler angles in order to avoid gimbal lock.

I'd like to use quaternions for a few reasons which I won't go into here.

I'm having trouble getting this to work properly. Camera control is almost where I'd like it to be but there's something wrong. It looks as though the camera is being rotated about the Z axis despite my attempts to only influence the X and Y axes.

I believe the issue has something to do with my quaternion multiplication logic. I haven't done anything related to quaternion in years :( My pan gesture handler is here:

func didPan(recognizer: UIPanGestureRecognizer)
{
switch recognizer.state
{
case .Began:
self.previousPanTranslation = .zero

case .Changed:
guard let previous = self.previousPanTranslation else
{
assertionFailure("Attempt to unwrap previous pan translation failed.")

return
}

// Calculate how much translation occurred between this step and the previous step
let translation = recognizer.translationInView(recognizer.view)
let translationDelta = CGPoint(x: translation.x - previous.x, y: translation.y - previous.y)

// Use the pan translation along the x axis to adjust the camera's rotation about the y axis.
let yScalar = Float(translationDelta.x / self.view.bounds.size.width)
let yRadians = yScalar * self.dynamicType.MaxPanGestureRotation

// Use the pan translation along the y axis to adjust the camera's rotation about the x axis.
let xScalar = Float(translationDelta.y / self.view.bounds.size.height)
let xRadians = xScalar * self.dynamicType.MaxPanGestureRotation

// Use the radian values to construct quaternions
let x = GLKQuaternionMakeWithAngleAndAxis(xRadians, 1, 0, 0)
let y = GLKQuaternionMakeWithAngleAndAxis(yRadians, 0, 1, 0)
let z = GLKQuaternionMakeWithAngleAndAxis(0, 0, 0, 1)
let combination = GLKQuaternionMultiply(z, GLKQuaternionMultiply(y, x))

// Multiply the quaternions to obtain an updated orientation
let scnOrientation = self.cameraNode.orientation
let glkOrientation = GLKQuaternionMake(scnOrientation.x, scnOrientation.y, scnOrientation.z, scnOrientation.w)
let q = GLKQuaternionMultiply(combination, glkOrientation)

// And finally set the current orientation to the updated orientation
self.cameraNode.orientation = SCNQuaternion(x: q.x, y: q.y, z: q.z, w: q.w)
self.previousPanTranslation = translation

case .Ended, .Cancelled, .Failed:
self.previousPanTranslation = nil

case .Possible:
break
}
}


My code is open source here: https://github.com/alfiehanssen/360Player/

Check out the
pan-gesture
branch in particular:
https://github.com/alfiehanssen/360Player/tree/pan-gesture

If you pull the code down I believe you'll have to run it on a device rather than the simulator.

I posted a video here that demonstrates the bug (please excuse the low resness of the video file):
https://vimeo.com/174346191

Thanks in advance for any help!

Answer

I was able to get this working using quaternions. The full code is here: ThreeSixtyPlayer. A sample is here:

    let orientation = cameraNode.orientation

    // Use the pan translation along the x axis to adjust the camera's rotation about the y axis (side to side navigation).
    let yScalar = Float(translationDelta.x / translationBounds.size.width)
    let yRadians = yScalar * maxRotation

    // Use the pan translation along the y axis to adjust the camera's rotation about the x axis (up and down navigation).
    let xScalar = Float(translationDelta.y / translationBounds.size.height)
    let xRadians = xScalar * maxRotation

    // Represent the orientation as a GLKQuaternion
    var glQuaternion = GLKQuaternionMake(orientation.x, orientation.y, orientation.z, orientation.w)

    // Perform up and down rotations around *CAMERA* X axis (note the order of multiplication)
    let xMultiplier = GLKQuaternionMakeWithAngleAndAxis(xRadians, 1, 0, 0)
    glQuaternion = GLKQuaternionMultiply(glQuaternion, xMultiplier)

    // Perform side to side rotations around *WORLD* Y axis (note the order of multiplication, different from above)
    let yMultiplier = GLKQuaternionMakeWithAngleAndAxis(yRadians, 0, 1, 0)
    glQuaternion = GLKQuaternionMultiply(yMultiplier, glQuaternion)

    cameraNode.orientation = SCNQuaternion(x: glQuaternion.x, y: glQuaternion.y, z: glQuaternion.z, w: glQuaternion.w)