8
votes

I have a scene setup with SCNCamera that rotates around an object.

What would be the best way to limit the extents of rotation the camera can achieve around the object?

Example: instead of being able to rotate around a whole sphere, how would I limit rotation to a single hemisphere?

My first attempt was to see if there was any clamps for .allowsCameraControl. Could not find anything.

I then tried adapting c# Unity : mouse orbit script, no luck.

Some pointers on how to approach or solve this would great.

Boilerplate Arcball thanks to this answer.

var lastWidthRatio: Float = 0
var lastHeightRatio: Float = 0

let camera = SCNCamera()
let cameraNode = SCNNode()
let cameraOrbit = SCNNode()

override func viewDidLoad() {
    super.viewDidLoad()

    // create a new scene
    let scene = SCNScene(named: "art.scnassets/ship.scn")!

    // create and add a camera to the scene

    camera.usesOrthographicProjection = true
    camera.orthographicScale = 9
    camera.zNear = 0
    camera.zFar = 100

    cameraNode.position = SCNVector3(x: 0, y: 0, z: 50)
    cameraNode.camera = camera

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

    // retrieve the ship node
    let ship = scene.rootNode.childNodeWithName("ship", recursively: true)!

    // retrieve the SCNView
    let scnView = self.view as! SCNView

    // set the scene to the view
    scnView.scene = scene

    // add a tap gesture recognizer
    let gesture = UIPanGestureRecognizer(target: self, action: "panDetected:");
    scnView.addGestureRecognizer(gesture);
}


func panDetected(sender: UIPanGestureRecognizer) {
    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

    print(Float(-2 * M_PI) * widthRatio)
    if (sender.state == .Ended) {
        lastWidthRatio = widthRatio % 1
        lastHeightRatio = heightRatio % 1
    }
}
2
I don't think the modulus 1 operation is going to have quite the result desired (widthRatio % 1). The widthRatio will vary from -1 to 1, but when a user rotates past the limit, the value might change from -0.9 to, say, -1.2. The modulus gets applied, resulting in a value of 0.2, causing an abrupt jump to the other side of the sphere. One way around that would be to add 2 if the value falls below -1, and subtract 2 if the end of the rotation is above 1. Using that method, moving from the -0.9 position to -1.2 would result in a value of 0.8, which I believe is the desired result.Dave Ruske

2 Answers

4
votes

It looks like you're almost there, using just the @Rickster code from the answer you cited.

The change you could make would be in these lines:

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

which implicitly allow pitch and yaw to cover the entire sphere. That's where you can do your limiting. For instance, instead of allowing the pitch (eulerAngles.x) to vary from 0 to -π, you could do

self.cameraOrbit.eulerAngles.x = Float(-M_PI_2) + Float(-M_PI_2) * heightRatio

to vary smoothly between -π/2 and -π, using full screen vertical scrolling to cover that range. Or you could put hard min/max limits/checks in those two lines to constrain to a particular area of the globe.

(Edit to address the inertia comment)

For rotational damping, or inertia, I'd approach it by using the built in SceneKit Physics, and perhaps put the camera on an invisible (no geometry) SCNNode. That camera node becomes a gimbal, similar to the approach taken in this project: An interactive seven-foot globe created entirely in RubyMotion and SceneKit.

The virtual gimbal then gets an SCNPhysicsBody (you add that, it doesn't come with one by default) with some damping. Or perhaps you put the physics on your central object, and give that object some angularDamping.

5
votes

Maybe this could be useful for readers.

    class GameViewController: UIViewController {

    var cameraOrbit = SCNNode()
    let cameraNode = SCNNode()
    let camera = SCNCamera()


    //HANDLE PAN CAMERA
    var lastWidthRatio: Float = 0
    var lastHeightRatio: Float = 0.2
    var WidthRatio: Float = 0
    var HeightRatio: Float = 0.2
    var fingersNeededToPan = 1
    var maxWidthRatioRight: Float = 0.2
    var maxWidthRatioLeft: Float = -0.2
    var maxHeightRatioXDown: Float = 0.02
    var maxHeightRatioXUp: Float = 0.4

    //HANDLE PINCH CAMERA
    var pinchAttenuation = 20.0  //1.0: very fast ---- 100.0 very slow
    var lastFingersNumber = 0

    override func viewDidLoad() {
        super.viewDidLoad()

        // create a new scene
        let scene = SCNScene(named: "art.scnassets/ship.scn")!

        // create and add a light to the scene
        let lightNode = SCNNode()
        lightNode.light = SCNLight()
        lightNode.light!.type = SCNLightTypeOmni
        lightNode.position = SCNVector3(x: 0, y: 10, z: 10)
        scene.rootNode.addChildNode(lightNode)

        // create and add an ambient light to the scene
        let ambientLightNode = SCNNode()
        ambientLightNode.light = SCNLight()
        ambientLightNode.light!.type = SCNLightTypeAmbient
        ambientLightNode.light!.color = UIColor.darkGrayColor()
        scene.rootNode.addChildNode(ambientLightNode)

    //Create a camera like Rickster said
        camera.usesOrthographicProjection = true
        camera.orthographicScale = 9
        camera.zNear = 1
        camera.zFar = 100

        cameraNode.position = SCNVector3(x: 0, y: 0, z: 50)
        cameraNode.camera = camera
        cameraOrbit = SCNNode()
        cameraOrbit.addChildNode(cameraNode)
        scene.rootNode.addChildNode(cameraOrbit)

        //initial camera setup
        self.cameraOrbit.eulerAngles.y = Float(-2 * M_PI) * lastWidthRatio
        self.cameraOrbit.eulerAngles.x = Float(-M_PI) * lastHeightRatio

        // retrieve the SCNView
        let scnView = self.view as! SCNView

        // set the scene to the view
        scnView.scene = scene

        //allows the user to manipulate the camera
        scnView.allowsCameraControl = false  //not needed

        // add a tap gesture recognizer
        let panGesture = UIPanGestureRecognizer(target: self, action: "handlePan:")
        scnView.addGestureRecognizer(panGesture)

        // add a pinch gesture recognizer
        let pinchGesture = UIPinchGestureRecognizer(target: self, action: "handlePinch:")
        scnView.addGestureRecognizer(pinchGesture)
    }

    func handlePan(gestureRecognize: UIPanGestureRecognizer) {

        let numberOfTouches = gestureRecognize.numberOfTouches()

        let translation = gestureRecognize.translationInView(gestureRecognize.view!)

        if (numberOfTouches==fingersNeededToPan) {

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

            //  HEIGHT constraints
            if (heightRatio >= maxHeightRatioXUp ) {
                heightRatio = maxHeightRatioXUp
            }
            if (heightRatio <= maxHeightRatioXDown ) {
                heightRatio = maxHeightRatioXDown
            }


            //  WIDTH constraints
            if(widthRatio >= maxWidthRatioRight) {
                widthRatio = maxWidthRatioRight
            }
            if(widthRatio <= maxWidthRatioLeft) {
                widthRatio = maxWidthRatioLeft
            }

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

            print("Height: \(round(heightRatio*100))")
            print("Width: \(round(widthRatio*100))")


            //for final check on fingers number
            lastFingersNumber = fingersNeededToPan
        }

        lastFingersNumber = (numberOfTouches>0 ? numberOfTouches : lastFingersNumber)

        if (gestureRecognize.state == .Ended && lastFingersNumber==fingersNeededToPan) {
            lastWidthRatio = widthRatio
            lastHeightRatio = heightRatio
            print("Pan with \(lastFingersNumber) finger\(lastFingersNumber>1 ? "s" : "")")
        }
    }

    func handlePinch(gestureRecognize: UIPinchGestureRecognizer) {
        let pinchVelocity = Double.init(gestureRecognize.velocity)
        //print("PinchVelocity \(pinchVelocity)")

        camera.orthographicScale -= (pinchVelocity/pinchAttenuation)

        if camera.orthographicScale <= 0.5 {
            camera.orthographicScale = 0.5
        }

        if camera.orthographicScale >= 10.0 {
            camera.orthographicScale = 10.0
        }

    }