I've got an SCNCamera at position(30,30,30) with a SCNLookAtConstraint on an object located at position(0,0,0). I'm trying to get the camera to rotate around the object on an imaginary sphere using A UIPanGestureRecognizer, while maintaining the radius between the camera and the object. I'm assuming I should use Quaternion projections but my math knowledge in this area is abysmal. My known variables are x & y translation + the radius I am trying to keep. I've written the project in Swift but an answer in Objective-C would be equally accepted (Hopefully using a standard Cocoa Touch Framework).
Where:
private var cubeView : SCNView!;
private var cubeScene : SCNScene!;
private var cameraNode : SCNNode!;
Here's my code for setting the scene:
// setup the SCNView
cubeView = SCNView(frame: CGRectMake(0, 0, self.width(), 175));
cubeView.autoenablesDefaultLighting = YES;
self.addSubview(cubeView);
// setup the scene
cubeScene = SCNScene();
cubeView.scene = cubeScene;
// setup the camera
let camera = SCNCamera();
camera.usesOrthographicProjection = YES;
camera.orthographicScale = 9;
camera.zNear = 0;
camera.zFar = 100;
cameraNode = SCNNode();
cameraNode.camera = camera;
cameraNode.position = SCNVector3Make(30, 30, 30)
cubeScene.rootNode.addChildNode(cameraNode)
// setup a target object
let box = SCNBox(width: 10, height: 10, length: 10, chamferRadius: 0);
let boxNode = SCNNode(geometry: box)
cubeScene.rootNode.addChildNode(boxNode)
// put a constraint on the camera
let targetNode = SCNLookAtConstraint(target: boxNode);
targetNode.gimbalLockEnabled = YES;
cameraNode.constraints = [targetNode];
// add a gesture recogniser
let gesture = UIPanGestureRecognizer(target: self, action: "panDetected:");
cubeView.addGestureRecognizer(gesture);
And here is the code for the gesture recogniser handling:
private var position: CGPoint!;
internal func panDetected(gesture:UIPanGestureRecognizer) {
switch(gesture.state) {
case UIGestureRecognizerState.Began:
position = CGPointZero;
case UIGestureRecognizerState.Changed:
let aPosition = gesture.translationInView(cubeView);
let delta = CGPointMake(aPosition.x-position.x, aPosition.y-position.y);
// ??? no idea...
position = aPosition;
default:
break
}
}
Thanks!
It might help to break down your issue into subproblems.
First, think about how to organize your scene to enable the kind of motion you want. You talk about moving the camera as if it's attached to an invisible sphere. Use that idea! Instead of trying to work out the math to set your cameraNode.position
to some point on an imaginary sphere, just think about what you would do to move the camera if it were attached to a sphere. That is, just rotate the sphere.
If you wanted to rotate a sphere separately from the rest of your scene contents, you'd attach it to a separate node. Of course, you don't actually need to insert a sphere geometry into your scene. Just make a node whose position
is concentric with the object you want your camera to orbit around, then attach the camera to a child node of that node. Then you can rotate that node to move the camera. Here's a quick demo of that, absent the scroll-event handling business:
let camera = SCNCamera()
camera.usesOrthographicProjection = true
camera.orthographicScale = 9
camera.zNear = 0
camera.zFar = 100
let cameraNode = SCNNode()
cameraNode.position = SCNVector3(x: 0, y: 0, z: 50)
cameraNode.camera = camera
let cameraOrbit = SCNNode()
cameraOrbit.addChildNode(cameraNode)
cubeScene.rootNode.addChildNode(cameraOrbit)
// rotate it (I've left out some animation code here to show just the rotation)
cameraOrbit.eulerAngles.x -= CGFloat(M_PI_4)
cameraOrbit.eulerAngles.y -= CGFloat(M_PI_4*3)
Here's what you see on the left, and a visualization of how it works on the right. The checkered sphere is cameraOrbit
, and the green cone is cameraNode
.
There's a couple of bonuses to this approach:
cameraNode
is a child node of cameraOrbit
, its own position stays constant -- the camera moves due to the rotation of cameraOrbit
. Now that you've got your scene architected for camera rotation, turning input events into rotation is pretty easy. Just how easy depends on what kind of control you're after:
GLKQuaternion
. (UPDATE: GLK types are "sorta" available in Swift 1.2 / Xcode 6.3. Prior to those versions you can do your math in ObjC via a bridging header.)Either way, you can skip some of the gesture recognizer boilerplate and gain some handy interactive behaviors by using UIScrollView
instead. (Not that there isn't usefulness to sticking with gesture recognizers -- this is just an easily implemented alternative.)
Drop one on top of your SCNView
(without putting another view inside it to be scrolled) and set its contentSize
to a multiple of its frame size... then during scrolling you can map the contentOffset
to your eulerAngles
:
func scrollViewDidScroll(scrollView: UIScrollView) {
let scrollWidthRatio = Float(scrollView.contentOffset.x / scrollView.frame.size.width)
let scrollHeightRatio = Float(scrollView.contentOffset.y / scrollView.frame.size.height)
cameraOrbit.eulerAngles.y = Float(-2 * M_PI) * scrollWidthRatio
cameraOrbit.eulerAngles.x = Float(-M_PI) * scrollHeightRatio
}
On the one hand, you have to do a bit more work for infinite scrolling if you want to spin endlessly in one or both directions. On the other, you get nice scroll-style inertia and bounce behaviors.