My goal was to have a 3D scene where you can rotate, pinch and laterally move the camera left/right and up/down without removing both fingers from the screen. So while you are pinching for example, you can lift just one finger and then continue to rotate the scene. Simultaneous gestures was the solution to a point. About every second or third pinch, the scene would disappear completely. I think that is due to a bug where occasionally the pinch velocity would return nan or inf. After putting in a test for these conditions, the goal was achived.I have borrowed from the 3D Apple Games tutorial to smooth out the motions. There are four gestures, two pan, one pinch and one tap.
//
import UIKit
import SceneKit
class GameViewController: UIViewController, UIGestureRecognizerDelegate { // need delegate
var scnView = SCNView()
var scene = SCNScene()
//// camera
let camera = SCNCamera()
let cameraNode = SCNNode()
var cameraOrbitFinal = SCNNode() // final position of camera (smooth transition)
let cameraOrbitStart = SCNNode() // start position of camera
//// pan camera limits
var widthAngle: Float = 0.87 // initial angles
var heightAngle: Float = 0.20
var lastWidthAngle: Float = 0.87 /// in radians
var lastHeightAngle: Float = 0.20
var maxHeightAngleXUp: Float = 0.40 // up/down limits
var maxHeightAngleXDown: Float = 0.05
//// camera zoom limits
var cameraZoomScaleMax = 15.0
var cameraZoomScaleMin = 5.0
var cameraCurrentZoomScale = 10.0
//// camera limits up/down, left/right
var lastPositionX: Float = 0.0
var lastPositionY: Float = 0.0
var maxXPositionRight: Float = 4.0
var maxXPositionLeft: Float = -4.0
var maxYPositionUp: Float = 3.0
var maxYPositionDown: Float = -3.0
////// original settings, double tap to reset
var originalCameraZoomScale: Double!
var originalWidthAngle: Float!
var originalHeightAngle: Float!
var originalPositionX: Float!
var originalPositionY: Float!
/////// position of cameraOrbitNode, starts at center of scene
var positionX: Float = 0.0
var positionY: Float = 0.0
override func viewDidLoad() {
super.viewDidLoad()
scnView = self.view as! SCNView
scnView.scene = scene
scnView.delegate = self
cameraNode.camera = camera
cameraNode.position = SCNVector3(x: 0, y: 0, z: Float(cameraCurrentZoomScale))
cameraOrbitStart.position = cameraNode.position
//initial camera selfie stick setup
cameraOrbitFinal.addChildNode(cameraNode)
cameraOrbitFinal.position = SCNVector3(x: 0, y: 0, z: 0)
cameraOrbitFinal.eulerAngles.y = Float(-2 * M_PI) * lastWidthAngle // initial view angle around y
cameraOrbitFinal.eulerAngles.x = Float(-M_PI) * lastHeightAngle // initial view angle around x
cameraOrbitStart.eulerAngles.x = cameraOrbitFinal.eulerAngles.x
cameraOrbitStart.eulerAngles.y = cameraOrbitFinal.eulerAngles.y
scene.rootNode.addChildNode(cameraOrbitFinal)
scene.rootNode.addChildNode(cameraOrbitStart)
//// set original positions and camera angle, double tap to reset
originalCameraZoomScale = cameraCurrentZoomScale
originalWidthAngle = widthAngle
originalHeightAngle = heightAngle
originalPositionX = positionX
originalPositionY = positionY
/////// a blue box
let boxSize: CGFloat = 1.0
let box = SCNBox(width: boxSize, height: boxSize, length: boxSize, chamferRadius: 0.0)
let boxNode = SCNNode(geometry: box)
scene.rootNode.addChildNode(boxNode)
box.firstMaterial?.diffuse.contents = UIColor.blue
///// add omni light
let omniLight = SCNLight()
omniLight.type = SCNLight.LightType.omni
let omniLightNode = SCNNode()
omniLightNode.light = omniLight
omniLightNode.position = SCNVector3(x: -2.0, y: 5, z: 10.0)
cameraOrbitFinal.addChildNode(omniLightNode) // add light to camera orbit node
////// add gestures
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(self.panGestureRecognized(gesture:)) )
panGesture.delegate = self //// needed for simultaneous gestures
self.view.addGestureRecognizer(panGesture)
let pinchGesture = UIPinchGestureRecognizer(target: self, action: #selector(self.pinchGestureRecognized(gesture:)) )
pinchGesture.delegate = self //// needed for simultaneous gestures
self.view.addGestureRecognizer(pinchGesture)
let lateralGesture = UIPanGestureRecognizer(target: self, action: #selector(self.lateralGestureRecognized(gesture:)) )
lateralGesture.delegate = self //// needed for simultaneous gestures
self.view.addGestureRecognizer(lateralGesture)
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(self.tapGestureRecognized(gesture:)) )
tapGesture.delegate = self
tapGesture.numberOfTapsRequired = 2
self.view.addGestureRecognizer(tapGesture)
scnView.isPlaying = true // keeps render delegate running for smoother transitions
}
///////// enable simultaneous gestures
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
/////////////////////////////////////////////////
/////////////////////////////////////////////////
/// gestures
func panGestureRecognized(gesture: UIPanGestureRecognizer) {
if gesture.numberOfTouches == 1 {
let translation = gesture.translation(in: gesture.view!)
widthAngle = Float(translation.x) / Float(gesture.view!.frame.size.width) + lastWidthAngle
heightAngle = Float(translation.y) / Float(gesture.view!.frame.size.height) + lastHeightAngle
// limits
if (heightAngle >= maxHeightAngleXUp ) {
heightAngle = maxHeightAngleXUp
lastHeightAngle = heightAngle
///// reset translation when at max height so finger slide reacts immediately
gesture.setTranslation(CGPoint(x: translation.x, y: 0.0), in: self.view)
}
if (heightAngle <= maxHeightAngleXDown ) {
heightAngle = maxHeightAngleXDown
lastHeightAngle = heightAngle
///// reset translation when at min height so finger slide reacts immediately
gesture.setTranslation(CGPoint(x: translation.x, y: 0.0), in: self.view)
}
//// rotate camera smootly to new angle
cameraOrbitStart.eulerAngles.y = Float(-2 * M_PI) * widthAngle
cameraOrbitStart.eulerAngles.x = Float(-M_PI) * heightAngle
}
else { // when gesture ends or another finger touches screen, save the rotation
gesture.setTranslation(CGPoint(x: 0.0, y: 0.0), in: self.view)
lastWidthAngle = widthAngle
lastHeightAngle = heightAngle
}
}
func pinchGestureRecognized(gesture: UIPinchGestureRecognizer) {
if gesture.numberOfTouches == 2 {
var pinchVelocity = Double(gesture.velocity)
///// error check on multiple simultaneous gestures bug
if (pinchVelocity.isNaN) || (pinchVelocity.isInfinite) {
pinchVelocity = 0.0
}
cameraCurrentZoomScale -= pinchVelocity / 10.0
if cameraCurrentZoomScale <= cameraZoomScaleMin {
cameraCurrentZoomScale = cameraZoomScaleMin
}
if cameraCurrentZoomScale >= cameraZoomScaleMax {
cameraCurrentZoomScale = cameraZoomScaleMax
}
/////// move camera along selfie stick
cameraOrbitStart.position = SCNVector3(x: positionX, y: positionY, z: Float(cameraCurrentZoomScale))
}
}
func lateralGestureRecognized(gesture: UIPanGestureRecognizer) {
if gesture.numberOfTouches == 2 {
let translation = gesture.translation(in: gesture.view!)
positionX = Float((-translation.x / 30.0)) + lastPositionX
positionY = Float((translation.y / 30.0)) + lastPositionY
if positionX >= maxXPositionRight {
///// reset translation when at max so finger slide reacts immediately
gesture.setTranslation(CGPoint(x: 0.0, y: translation.y), in: self.view)
lastPositionX = maxXPositionRight
positionX = maxXPositionRight
}
if positionX <= maxXPositionLeft {
gesture.setTranslation(CGPoint(x: 0.0, y: translation.y), in: self.view)
lastPositionX = maxXPositionLeft
positionX = maxXPositionLeft
}
if positionY <= maxYPositionDown {
gesture.setTranslation(CGPoint(x: translation.x, y: 0.0), in: self.view)
lastPositionY = maxYPositionDown
positionY = maxYPositionDown
}
if positionY >= maxYPositionUp {
gesture.setTranslation(CGPoint(x: translation.x, y: 0.0), in: self.view)
lastPositionY = maxYPositionUp
positionY = maxYPositionUp
}
/////// move camera laterally up/down or left/right
cameraOrbitStart.position = SCNVector3(x: positionX, y: positionY, z: Float(cameraCurrentZoomScale))
}
else { // when gesture ends or a finger is lifted, save the position
gesture.setTranslation(CGPoint(x: 0.0, y: 0.0), in: self.view)
lastPositionX = Float(positionX)
lastPositionY = Float(positionY)
}
}
func tapGestureRecognized(gesture: UITapGestureRecognizer) {
////// double tap to reset scene to original view
cameraOrbitStart.eulerAngles.y = Float(-2 * M_PI) * originalWidthAngle
cameraOrbitStart.eulerAngles.x = Float(-M_PI) * originalHeightAngle
cameraOrbitStart.position = SCNVector3(x: originalPositionX, y: originalPositionY, z: Float(originalCameraZoomScale))
cameraCurrentZoomScale = originalCameraZoomScale
positionX = 0.0
positionY = 0.0
lastPositionX = 0.0
lastPositionY = 0.0
lastWidthAngle = originalWidthAngle
lastHeightAngle = originalHeightAngle
}
/////////////////////////////////////////////////
/////////////////////////////////////////////////
//smooth gestures, called by SCNSceneRendererDelegate
func updatePositions() {
/// pan
let lerpY = (cameraOrbitStart.eulerAngles.y - cameraOrbitFinal.eulerAngles.y) * 0.075
let lerpX = (cameraOrbitStart.eulerAngles.x - cameraOrbitFinal.eulerAngles.x) * 0.075
cameraOrbitFinal.eulerAngles.y += lerpY
cameraOrbitFinal.eulerAngles.x += lerpX
/// zooms
let lerpZ = (cameraOrbitStart.position.z - cameraNode.position.z) * 0.075
cameraNode.position.z += lerpZ
/// lateral moves
let lerpLX = (cameraOrbitStart.position.x - cameraNode.position.x) * 0.075
let lerpLY = (cameraOrbitStart.position.y - cameraNode.position.y) * 0.075
cameraNode.position.x += lerpLX
cameraNode.position.y += lerpLY
}
override var shouldAutorotate: Bool {
return true
}
override var prefersStatusBarHidden: Bool {
return true
}
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
if UIDevice.current.userInterfaceIdiom == .phone {
return .allButUpsideDown
} else {
return .all
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Release any cached data, images, etc that aren't in use.
}
}
extension GameViewController : SCNSceneRendererDelegate {
func renderer(_ renderer: SCNSceneRenderer, didApplyAnimationsAtTime time: TimeInterval) {
updatePositions()
}
}