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

和自甴很熟 提交于 2020-01-23 11:04:10

问题


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!


回答1:


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)



回答2:


Sorry, this uses SCNVector4 instead of quaternions but works well for my use. I apply it to my root-most geometry nodes container ("rotContainer") instead of the camera but a brief test seems to indicate it will work for camera use as well.

func panGesture(sender: UIPanGestureRecognizer) {
    let translation = sender.translationInView(sender.view!)

    let pan_x = Float(translation.x)
    let pan_y = Float(-translation.y)
    let anglePan = sqrt(pow(pan_x,2)+pow(pan_y,2))*(Float)(M_PI)/180.0
    var rotationVector = SCNVector4()

    rotationVector.x = -pan_y
    rotationVector.y = pan_x
    rotationVector.z = 0
    rotationVector.w = anglePan

    rotContainer.rotation = rotationVector

    if(sender.state == UIGestureRecognizerState.Ended) {
        let currentPivot = rotContainer.pivot
        let changePivot = SCNMatrix4Invert(rotContainer.transform)
        rotContainer.pivot = SCNMatrix4Mult(changePivot, currentPivot)
        rotContainer.transform = SCNMatrix4Identity
    }
}



回答3:


bbedit's solution combines well with Rotating a camera on an orbit. Set up the camera as suggested in the linked answer then rotate the "orbit" node using bbedit's ideas. I have modified the code for Swift 4 version of his code that did work:

@IBAction func handlePan(_ sender: UIPanGestureRecognizer) {
    print("Called the handlePan method")
    let scnView = self.view as! SCNView
    let cameraOrbit = scnView.scene?.rootNode.childNode(withName: "cameraOrbit", recursively: true)
    let translation = sender.translation(in: sender.view!)
    let pan_x = Float(translation.x)
    let pan_y = Float(-translation.y)
    let anglePan = sqrt(pow(pan_x,2)+pow(pan_y,2))*(Float)(Double.pi)/180.0
    var rotationVector = SCNVector4()
    rotationVector.x = -pan_y
    rotationVector.y = pan_x
    rotationVector.z = 0
    rotationVector.w = anglePan
    cameraOrbit!.rotation = rotationVector
    if(sender.state == UIGestureRecognizerState.ended) {
        let currentPivot = cameraOrbit!.pivot
        let changePivot = SCNMatrix4Invert(cameraOrbit!.transform)
        cameraOrbit!.pivot = SCNMatrix4Mult(changePivot, currentPivot)
        cameraOrbit!.transform = SCNMatrix4Identity
    }
}


来源:https://stackoverflow.com/questions/38319565/how-to-use-a-pan-gesture-to-rotate-a-camera-in-scenekit-using-quaternions

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!