The goal is to orbit around an arbitrary, but visible, point in a scene.
As the user pans, the camera should move and rotate around this anchor point.
This works wonderfully if the anchor point is in the middle of the screen.
However, if the anchor point is off-center -- say, at the left edge of the screen -- the camera jumps momentarily as it brings the anchor point into the center.
The code below fixed the jumping issue, but it disorients users in that the anchor point slowly shifts to the center.
The camera movement is connected to the pan gesture.
Any ideas on how to solve this problem?
let panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(didSceneViewPanOneFinger))
panRecognizer.minimumNumberOfTouches = 1
panRecognizer.maximumNumberOfTouches = 1
panRecognizer.delegate = self
sceneView.addGestureRecognizer(panRecognizer)
func didSceneViewPanOneFinger(_ sender: UIPanGestureRecognizer) {
// Pick any visible point as long as it's not in center
let anchorPoint = SCNVector(-15, 0, 0)
// Orbit camera
cameraNode.orbitPoint(anchorPoint: anchorPoint, translation: sender.translation(in: sender.view!), state: sender.state)
}
class CameraNode: SCNNode {
// Vars
let headNode = SCNNode()
var curXRadians = Float(0)
var curYRadians = Float(0)
var directLastTranslationY = Float(0)
var reach = Float(10)
var aimingPoint = SCNVector3()
var lastAnchor:SCNVector3!
init(reach: Float) {
super.init()
self.reach = reach
// Call <doInit> only after all properties set
doInit()
}
fileprivate func doInit() {
// Add head node
headNode.camera = SCNCamera()
headNode.camera!.zNear = Double(0.1)
headNode.position = SCNVector3(x: 0, y: 0, z: 0)
addChildNode(headNode)
// Position camera
position = SCNVector3(x: 0, y: minY, z: reach)
}
func orbitPoint(anchorPoint: SCNVector3, translation: CGPoint, state: UIGestureRecognizerState) {
// Get pan distance & convert to radians
var xRadians = GLKMathDegreesToRadians(Float(translation.x))
var yRadians = GLKMathDegreesToRadians(Float(translation.y))
// Get x & y radians, adjust values to throttle rotate speed
xRadians = (xRadians / 3) + curXRadians
yRadians = (yRadians / 3) + curYRadians
// Limit yRadians to prevent rotating 360 degrees vertically
yRadians = max(Float(-M_PI_2), min(Float(M_PI_2), yRadians))
// Save original position
if state == .began {
aimingPoint = lastAnchor ?? anchorPoint
} else {
aimingPoint = SCNVector3.lerp(vectorStart: anchorPoint, vectorEnd: aimingPoint, t: 0.99)
}
// Rotate around <anchorPoint>
// * Compute distance to <anchorPoint>, used as radius for spherical movement
let radius = aimingPoint.distanceTo(position)
var newPoint = getPointOnSphere(aimingPoint, hAngle: yRadians, vAngle: xRadians, radius: radius)
if newPoint.y < 0 {
yRadians = directLastTranslationY
newPoint = getPointOnSphere(aimingPoint, hAngle: yRadians, vAngle: xRadians, radius: radius)
}
// Set rotation values to avoid Gimbal Lock
headNode.rotation = SCNVector4(x: 1, y: 0, z: 0, w: yRadians)
rotation = SCNVector4(x: 0, y: 1, z: 0, w: xRadians)
print("cam pos: \(position). anchor point: \(anchorPoint). radius: \(radius).")
// Save value for next rotation?
if state == .ended {
curXRadians = xRadians
curYRadians = yRadians
lastAnchor = aimingPoint ?? anchorPoint
}
directLastTranslationY = yRadians
}
// Your position in 3d is given by two angles (+ radius, which in your case is constant)
// here, s is the angle around the y-axis, and t is the height angle, measured 'down' from the y-axis.
func getSphericalCoords(_ s: Float, t: Float, r: Float) -> SCNVector3 {
return SCNVector3(-(cos(s) * sin(t) * r),
sin(s) * r,
-(cos(s) * cos(t) * r))
}
fileprivate func getPointOnSphere(_ centerPoint: SCNVector3, hAngle: Float, vAngle: Float, radius: Float? = nil) -> SCNVector3 {
// Update <radius>?
var radius = radius
if radius == nil {
radius = reach
}
// Compute point & return result
let p = centerPoint - getSphericalCoords(hAngle, t: vAngle, r: radius!)
return p
}
}