1

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
    }
}
Crashalot
  • 33,605
  • 61
  • 269
  • 439

1 Answers1

1

If you want to rotate the camera around a point that's not the centre of the camera's view, and keep that point not at the centre of the view, you're best off using a triangle arrangement of constraints.

At the centre of the camera's view, adjacent to the rotation point, fix a dummy object that's used as a camera "look at" constraint. This dummy object is affixed to the rotation point, so the camera rotates as you desire.

I hope this diagram explains better than my words:

enter image description here

Confused
  • 6,048
  • 6
  • 34
  • 75
  • No, it's constraints, to a dummy object, then to the node that's the rotationPoint. No maths required. If maths were needed, i couldn't do it! @Fluidity – Confused Dec 08 '16 at 19:17
  • constraints are a SpriteKit and SceneKit fixed connection (mostly). They're sort of somewhat useful for locking things together and creating positional relationships... @Fluidity – Confused Dec 08 '16 at 19:18
  • still, your brain had to think in trig in order to come up with the idea for constraints such as this. you are better at math than you think – Fluidity Dec 08 '16 at 19:21
  • 2
    I beg to disagree, @Fluidity. I had to think in terms of dummies: something I'm profoundly, personally, deeply familiar with. – Confused Dec 08 '16 at 19:23
  • And the "Path of a Dummy" is the title of my upcoming autobiography @Fluidity – Confused Dec 08 '16 at 19:25