4

I am using SceneKit and allowing users to rotate what they're seeing inside a sphere by using a pan gesture. This is working fine - except, I'd like to have the sphere keep rotating slightly in the direction of the pan gesture, to feel a bit more reactive.

Here's my setup for the scene:

    // Create scene
    let scene = SCNScene()

    sceneView.scene = scene

    let panRecognizer = UIPanGestureRecognizer(target: self,
                                               action: #selector(ViewController.handlePanGesture(_:)))

    sceneView.addGestureRecognizer(panRecognizer)

    //Create sphere
    let sphere = SCNSphere(radius: 50.0)

    // sphere setup

    sphereNode = SCNNode(geometry: sphere)

    sphereNode!.position = SCNVector3Make(0,0,0)
    scene.rootNode.addChildNode(sphereNode!)

    // Create camera
    let camera = SCNCamera()

    // camera setup

    cameraNode = SCNNode()

    cameraNode!.camera = camera
    cameraNode!.position = SCNVector3Make(0, 0, 0)

    cameraOrbit = SCNNode()

    cameraOrbit!.addChildNode(cameraNode!)
    scene.rootNode.addChildNode(cameraOrbit!)

    let lookAtConstraint = SCNLookAtConstraint(target: sphereNode!)
    lookAtConstraint.gimbalLockEnabled = true
    cameraNode!.constraints = [lookAtConstraint]

    sceneView.pointOfView = cameraNode

And here is how the pan gesture is currently handled (courtesy of SCNCamera limit arcball rotation):

    let translation = sender.translationInView(sender.view!)
    let widthRatio = Float(translation.x) / Float(sender.view!.frame.size.width) + lastWidthRatio
    let heightRatio = Float(translation.y) / Float(sender.view!.frame.size.height) + lastHeightRatio

    self.cameraOrbit?.eulerAngles.y = Float(-2 * M_PI) * widthRatio
    self.cameraOrbit?.eulerAngles.x = Float(-M_PI) * heightRatio

    if (sender.state == .Ended) {
        lastWidthRatio = widthRatio
        lastHeightRatio = heightRatio
    }

I'm not sure where to begin in adding in a slight continued rotation motion. Maybe adding a physicsBody and applying force? Maybe animating the change in eulerAngles?

Community
  • 1
  • 1
H K
  • 1,215
  • 2
  • 16
  • 29

1 Answers1

5

The way I've handled things like this before is to split the gesture handler into sections for the gesture state beginning, changing and ending. If the rotation is working for you all you need to do is add code to the bottom of your if (sender.state == .Ended) statement.

After lastHeightRatio = heightRatio you can find the velocity of the pan in the view using sender.velocityInView(sender.view!) and then find the horizontal component as before, then add an SCNAction to rotate the node with an EaseOut timing mode.

You'll likely need a multiplier to translate the velocity in the view (measured in points) to the angle you wish to rotate through (measured in radians). A velocity of 10 points, which is relatively small, would result in a very fast rotation.

Also you'll need to remove all actions from the node when the gesture begins again if you want a touch to stop the residual rotation.

Some sampel code would be:

if (sender.state == .Ended) {
  lastWidthRatio = widthRatio
  lastHeightRatio = heightRatio

  // Find velocity
  let velocity = sender.velocityInView(sender.view!)

  // Create Action for whichever axis is the correct one to rotate about.
  // The 0.00001 is just a factor to provide the rotation speed to pan
  // speed ratio you'd like. Play with this number and the duration to get the result you want
  let rotate = SCNAction.rotateByX(velocity.x * 0.00001, y: 0, z: 0, duration: 2.0)

  // Run the action on the camera orbit node (if I understand your setup correctly)
  cameraOrbit.runAction(rotate)
}

This should be enough to get you started.

Andy Heard
  • 1,715
  • 1
  • 15
  • 25
  • i ended up doing something similar - thanks! my issue now is that the `lastRatio` value is now off, because the final resting position of the sphere has rotated further than the last time that value was set. so, when the user goes to make the next pan gesture, the camera "jumps" to the position before the drift was started as the `eulerAngles` are reset. if you have any thoughts on that one, i'm all ears. – H K May 18 '16 at 15:36
  • Hmm, maybe try adding a completion handler to `runAction` that updates the lastRatio values again when the action is complete? – Andy Heard May 19 '16 at 11:01
  • @AndyHeard I tried that, but its not working and I really don't understand why. Here's my code snippet ```lastWidthRatio += (((-velocity.x * 0.001)*2) / sender.view.frame.size.width);``` – Kevin Goedecke Jul 06 '16 at 12:04