SceneKit cube rotation with multiple UIPanGestureRecognizers - ios

Thanks for taking a look at this. I'm sure it is something very basic. I am a beginner.
I am trying to rotate a cube in in a SceneKit view with pan gestures. I have so far been successful in load this sample app onto my iPad and panning my finger on the y axis to rotate the cube on its x axis, or panning along the screens x-axis to rotate the cube along its y-axis.
Currently, I've noticed that whichever gesture recognizer is added last to the sceneView is the one that works. My question is how can I have the cube respond to either a x pan gesture then a y pan gesture or vice versa.
Here is the code I have written so far:
import UIKit
import SceneKit
class ViewController: UIViewController {
//geometry
var geometryNode: SCNNode = SCNNode()
//gestures
var currentYAngle: Float = 0.0
var currentXAngle: Float = 0.0
override func viewDidLoad() {
super.viewDidLoad()
sceneSetup()
}
func sceneSetup () {
//setup scene
let scene = SCNScene()
let sceneView = SCNView(frame: self.view.frame)
self.view.addSubview(sceneView)
//add camera
let camera = SCNCamera()
let cameraNode = SCNNode()
cameraNode.camera = camera
cameraNode.position = SCNVector3(x: 0.0, y: 0.0, z: 5.0)
//add light
let light = SCNLight()
light.type = SCNLightTypeOmni
let lightNode = SCNNode()
lightNode.light = light
lightNode.position = SCNVector3(x: 1.5, y: 1.5, z: 1.5)
//add cube
let cubeGeometry = SCNBox(width: 1.0, height: 1.0, length: 1.0, chamferRadius: 0.0)
let boxNode = SCNNode(geometry: cubeGeometry)
scene.rootNode.addChildNode(lightNode)
scene.rootNode.addChildNode(cameraNode)
scene.rootNode.addChildNode(boxNode)
geometryNode = boxNode
//add recognizers
let panXRecognizer = UIPanGestureRecognizer(target: self, action: "rotateXGesture:")
let panYRecognizer = UIPanGestureRecognizer(target: self, action: "rotateYGesture:")
sceneView.addGestureRecognizer(panXRecognizer)
sceneView.addGestureRecognizer(panYRecognizer)
sceneView.scene = scene
}
func rotateXGesture (sender: UIPanGestureRecognizer) {
let translation = sender.translationInView(sender.view)
var newXAngle = (Float)(translation.y)*(Float)(M_PI)/180.0
newXAngle += currentXAngle
geometryNode.transform = SCNMatrix4MakeRotation(newXAngle, 1, 0, 0)
if(sender.state == UIGestureRecognizerState.Ended) {
currentXAngle = newXAngle
}
}
func rotateYGesture (sender: UIPanGestureRecognizer) {
let translation = sender.translationInView(sender.view)
var newYAngle = (Float)(translation.x)*(Float)(M_PI)/180.0
newYAngle += currentYAngle
geometryNode.transform = SCNMatrix4MakeRotation(newYAngle, 0, 1, 0)
if(sender.state == UIGestureRecognizerState.Ended) {
currentYAngle = newYAngle
}
}
}

Combine your current two gestures into one. Here's the relevant portion of the code I'm using:
func panGesture(sender: UIPanGestureRecognizer) {
let translation = sender.translationInView(sender.view!)
var newAngleX = (Float)(translation.y)*(Float)(M_PI)/180.0
newAngleX += currentAngleX
var newAngleY = (Float)(translation.x)*(Float)(M_PI)/180.0
newAngleY += currentAngleY
baseNode.eulerAngles.x = newAngleX
baseNode.eulerAngles.y = newAngleY
if(sender.state == UIGestureRecognizerState.Ended) {
currentAngleX = newAngleX
currentAngleY = newAngleY
}
}
Here's a gesture for zooming as well:
func pinchGesture(sender: UIPinchGestureRecognizer) {
let zoom = sender.scale
var z = cameraNode.position.z * Float(1.0 / zoom)
z = fmaxf(zoomLimits.min, z)
z = fminf(zoomLimits.max, z)
cameraNode.position.z = z
}
Edit: I found a better way to rotate the model. In the panGesture code at the top, the x-axis pivots as you rotate about the y. This means if you rotate 180 about the y, rotation about the x is opposite your finger motion. The method also restricts motion to two degrees of freedom. The method linked to below, even though it doesn't directly affect the z-axis, somehow seems to allow three degrees of freedom. It also makes all vertical swipes rotate about the x in the logical direction.
How to rotate object in a scene with pan gesture - SceneKit

The way you set up your two gesture recognizers is identical, they will both fire for the same events (which is why the last one to be added predominates). There is no control within pan to specifically limit it to vertical or horizontal pans. Instead, consider analyzing the direction of the pan and then decide whether to rotate your gesture one way or the other, based upon which is greater.

Related

Load large 3d Object .scn file in ARSCNView Aspect Fit in to the screen ARKIT Swift iOS

I am developing ARKit Application using 3d models. So for that I have used 3d models & added gestures for move, rotate & zoom 3d models.
Now I am facing only 1 issue but I am not sure if this issue relates to what. Is there an issue in 3d model or if anything missing in my program.
Issue is the 3d model I am using shows very big & goes out of the screen. I am trying to scale it down size but its very big.
Here is my code :
#IBOutlet var mySceneView: ARSCNView!
var selectedNode = SCNNode()
var prevLoc = CGPoint()
var touchCount : Int = 0
override func viewDidLoad() {
super.viewDidLoad()
self.lblTitle.text = self.sceneTitle
let mySCN = SCNScene.init(named: "art.scnassets/\(self.sceneImagename).scn")!
self.mySceneView.scene = mySCN
let cameraNode = SCNNode()
cameraNode.camera = SCNCamera()
cameraNode.position = SCNVector3Make(0, 0, 0)
self.mySceneView.scene.rootNode.addChildNode(cameraNode)
self.mySceneView.allowsCameraControl = true
self.mySceneView.autoenablesDefaultLighting = true
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(detailPage.doHandleTap(_:)))
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(detailPage.doHandlePan(_:)))
let gesturesArray = NSMutableArray()
gesturesArray.add(tapGesture)
gesturesArray.add(panGesture)
gesturesArray.addObjects(from: self.mySceneView.gestureRecognizers!)
self.mySceneView.gestureRecognizers = (gesturesArray as! [UIGestureRecognizer])
}
//MARK:- Handle Gesture
#objc func doHandlePan(_ sender: UIPanGestureRecognizer) {
var delta = sender.translation(in: self.view)
let loc = sender.location(in: self.view)
if sender.state == .began {
self.prevLoc = loc
self.touchCount = sender.numberOfTouches
} else if sender.state == .changed {
delta = CGPoint(x: loc.x - prevLoc.x, y: loc.y - prevLoc.y)
prevLoc = loc
if self.touchCount != sender.numberOfTouches {
return
}
var rotMat = SCNMatrix4()
if touchCount == 2 {
rotMat = SCNMatrix4MakeTranslation(Float(delta.x * 0.025), Float(delta.y * -0.025), 0)
} else {
let rotMatX = SCNMatrix4Rotate(SCNMatrix4Identity, Float((1.0/100) * delta.y), 1, 0, 0)
let rotMatY = SCNMatrix4Rotate(SCNMatrix4Identity, Float((1.0/100) * delta.x), 0, 1, 0)
rotMat = SCNMatrix4Mult(rotMatX, rotMatY)
}
let transMat = SCNMatrix4MakeTranslation(selectedNode.position.x, selectedNode.position.y, selectedNode.position.z)
selectedNode.transform = SCNMatrix4Mult(selectedNode.transform, SCNMatrix4Invert(transMat))
let parentNodeTransMat = SCNMatrix4MakeTranslation((selectedNode.parent?.worldPosition.x)!, (selectedNode.parent?.worldPosition.y)!, (selectedNode.parent?.worldPosition.z)!)
let parentNodeMatWOTrans = SCNMatrix4Mult(selectedNode.parent!.worldTransform, SCNMatrix4Invert(parentNodeTransMat))
selectedNode.transform = SCNMatrix4Mult(selectedNode.transform, parentNodeMatWOTrans)
let camorbitNodeTransMat = SCNMatrix4MakeTranslation((self.mySceneView.pointOfView?.worldPosition.x)!, (self.mySceneView.pointOfView?.worldPosition.y)!, (self.mySceneView.pointOfView?.worldPosition.z)!)
let camorbitNodeMatWOTrans = SCNMatrix4Mult(self.mySceneView.pointOfView!.worldTransform, SCNMatrix4Invert(camorbitNodeTransMat))
selectedNode.transform = SCNMatrix4Mult(selectedNode.transform, SCNMatrix4Invert(camorbitNodeMatWOTrans))
selectedNode.transform = SCNMatrix4Mult(selectedNode.transform, rotMat)
selectedNode.transform = SCNMatrix4Mult(selectedNode.transform, camorbitNodeMatWOTrans)
selectedNode.transform = SCNMatrix4Mult(selectedNode.transform, SCNMatrix4Invert(parentNodeMatWOTrans))
selectedNode.transform = SCNMatrix4Mult(selectedNode.transform, transMat)
}
}
#objc func doHandleTap(_ sender: UITapGestureRecognizer) {
let p = sender.location(in: self.mySceneView)
var hitResults = self.mySceneView.hitTest(p, options: nil)
if (p.x > self.mySceneView.frame.size.width-100 || p.y < 100) {
self.mySceneView.allowsCameraControl = !self.mySceneView.allowsCameraControl
}
if hitResults.count > 0 {
let result = hitResults[0]
let material = result.node.geometry?.firstMaterial
selectedNode = result.node
SCNTransaction.begin()
SCNTransaction.animationDuration = 0.3
SCNTransaction.completionBlock = {
SCNTransaction.begin()
SCNTransaction.animationDuration = 0.3
SCNTransaction.commit()
}
material?.emission.contents = UIColor.white
SCNTransaction.commit()
}
}
My Question is :
Can we set any size of 3d object model Aspect fit in screen size in the centre of the screen ? Please suggest if there is some way for it.
Any guidence or suggestions will be highly appreciated.
What you need to is to use getBoundingSphereCenter to get the bounding sphere size, then can project that to the screen. Or alternatively get the ratio of that radius over the distance between scenekit camera and the object position. This way you will know how big the object will look on the screen. To the scale down, you simple set the scale property of your object.
For the second part, you can use projectPoint.
The way I handled this is making sure the 3D model always has a fixed size.
For example, if the 3D model is a small cup or a large house, I insure it always has a width of 25 cm on the scene's coordinate space (while maintaining the ratios between x y z).
You can calculate the width of the bounding box of the node like this:
let mySCN = SCNScene(named: "art.scnassets/\(self.sceneImagename).scn")!
let minX = mySCN.rootNode.boundingBox.min.x
let maxX = mySCN.rootNode.boundingBox.max.x
// change 0.25 to whatever you need
// this value is in meters
let scaleValue = 0.25 / abs(minX - maxX)
// scale all axes of the node using `scaleValue`
// this maintains ratios and does not stretch the model
mySCN.rootNode.scale = SCNVector3(scaleValue, scaleValue, scaleValue)
self.mySceneView.scene = mySCN
You can also calculate the scale value based on height or depth by using the y or z value of the bounding box.

SceneKit Basics: move node

I have a simple scene with a centered object and the camera positioned on a virtual sphere around the center (inspired from Rotate SCNCamera node looking at an object around an imaginary sphere).
Now I want to be able to move the object with a two finger pan gesture. My code works well as long as I don't rotate the camera first. If I first rotate the camera by 180° (so that the camera looks at the object from behind), then the movement is in the wrong direction (which makes sense to me). It gets worse with other camera positions. However, I cannot find how I must convert or project the x/y translations from the recognizer to the plane parallel to the camera. The object should move under the fingers..
This is the setup of my scene:
let scene = SCNScene()
//add camera
let camera = SCNCamera()
camera.usesOrthographicProjection = true
camera.orthographicScale = 30
camera.zNear = 1.0
camera.zFar = 1000
cameraNode = SCNNode()
cameraNode.camera = camera
cameraNode.position = SCNVector3(x: 0.0, y: 0.0, z: 50.0)
cameraOrbit = SCNNode()
cameraOrbit.addChildNode(cameraNode)
scene.rootNode.addChildNode(cameraOrbit)
boxNode.geometry = SCNBox(width: 10, height: 40, length: 10, chamferRadius: 0.2)
scene.rootNode.addChildNode(boxNode)
let panMoveGestureRecognizer = UIPanGestureRecognizer(target: self, action:#selector(panMoveGesture))
panMoveGestureRecognizer.minimumNumberOfTouches = 2
sceneView.addGestureRecognizer(panMoveGestureRecognizer)
And the gesture recognizers code:
func panMoveGesture(recognizer: UIPanGestureRecognizer) {
//move
let translation = recognizer.translation(in: self.view.superview)
var newXMovement = -Float(translation.x) * 0.1
var newYMovement = Float(translation.y) * 0.1
switch recognizer.state {
case .began:
currentXMovement = cameraOrbit.position.x
currentYMovement = cameraOrbit.position.y
case .changed:
newXMovement += currentXMovement
newYMovement += currentYMovement
cameraOrbit.position.x = newXMovement
cameraOrbit.position.y = newYMovement
case .ended:
currentXMovement = newXMovement
currentYMovement = newYMovement
default: break
}
}
Please help ;-)

SCNCamera moving camera's pivot

I need to focus to certain node, for example a pyramid. Then apply distance to the camera, then move the camera based on user click. My approach is like this :
import SceneKit
class GameViewController: UIViewController {
let scene = SCNScene()
override func viewDidLoad() {
super.viewDidLoad()
let camera = SCNCamera()
camera.usesOrthographicProjection = true
camera.orthographicScale = 4
camera.zNear = 1
camera.zFar = 100
let cameraNode = SCNNode()
cameraNode.position = SCNVector3(x: 0, y: 0, z: 6)
cameraNode.camera = camera
let cameraOrbit = SCNNode()
cameraOrbit.name = "orbit"
cameraOrbit.addChildNode(cameraNode)
scene.rootNode.addChildNode(cameraOrbit)
let Py = SCNPyramid(width: 2, height: 3, length: 2)
Py.firstMaterial?.diffuse.contents = UIColor.purple()
let P = SCNNode(geometry: Py)
P.position = SCNVector3(x:0,y:0,z:2) //see the note
scene.rootNode.addChildNode(P)
/* N O T E :
the position of the pyramid must not be changed
as my intention is to rotate the camera
not the pyramid node
I repeat, I don't want to rotate the pyramid
*/
let scnView = self.view as! SCNView
scnView.scene = scene
scnView.allowsCameraControl = false
scnView.backgroundColor = UIColor.black()
// user rotates the camera by tapping
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTap(_:)))
scnView.addGestureRecognizer(tapGesture)
}
//the function which does camera rotation :
func handleTap(_ gestureRecognize: UIGestureRecognizer) {
//I guess the solution is around here
//like modify the cameraOrbit.position ?
//or cameraNode.position ?
//tried both but doesn't work
//or I used them wrong
let cameraOrbit = scene.rootNode.childNode(withName: "orbit", recursively: true)!
SCNTransaction.begin()
SCNTransaction.animationDuration = 2
cameraOrbit.eulerAngles.z += Float(M_PI_2) //see below
SCNTransaction.commit()
/*
I realise that by performing rotation on the camera
(the camera position is unchanged) works for z rotation,
but this is not what I want. What I want is a solution,
which also works for eulerAngles.x and eulerAngles.y.
I used eulerAngles.z as example since it's easier to observe.
I guess the true solution involves moving the camera
with specific trajectory, not only rotation of "anchored" camera.
*/
}
//...
}
The result is :
What I want to achieve is to make the rotation relative to its centre :
My question is, how to adjust the pivot so I can achieve the rotation relative to the pyramid's centre?
Note : I don't want to rotate the pyramid.
I found the solution, but I can't explain why. Please comment if you can explain.
The camera orbit position must be set to match the object's location (in this case, the pyramid). So
cameraOrbit.position = P.position
Then here comes the mystery solution, add cameraOrbit.position.y by half of pi :
cameraOrbit.position.y += Float(M_PI_2)
//therefore we have the final cameraOrbit.position as (0, pi/2, 2)
Tested and works perfectly for all cameraOrbit.eulerAngles.
But I don't have a clue why it works. If pi/2 is coming from some projection thingy, then why it's tucked on y only? I mean, when I do any of the cameraOrbit.eulerAngles, I don't need to assign this pi/2 to either x or z.
Here it is the complete code
import SceneKit
class GameViewController: UIViewController {
let scene = SCNScene()
override func viewDidLoad() {
super.viewDidLoad()
let camera = SCNCamera()
camera.usesOrthographicProjection = true
camera.orthographicScale = 4
camera.zNear = 1
camera.zFar = 100
let cameraNode = SCNNode()
cameraNode.position = SCNVector3(x: 0, y: 0, z: 6)
cameraNode.camera = camera
let cameraOrbit = SCNNode()
cameraOrbit.name = "orbit"
cameraOrbit.addChildNode(cameraNode)
scene.rootNode.addChildNode(cameraOrbit)
let Py = SCNPyramid(width: 2, height: 3, length: 2)
Py.firstMaterial?.diffuse.contents = UIColor.purple()
let P = SCNNode(geometry: Py)
P.position = SCNVector3(x:0,y:0,z:2)
scene.rootNode.addChildNode(P)
// S O L U T I O N :
cameraOrbit.position = P.position
cameraOrbit.position.y += Float(M_PI_2)
//therefore we have the final cameraOrbit.position as (0, pi/2, 2)
let scnView = self.view as! SCNView
scnView.scene = scene
scnView.allowsCameraControl = false
scnView.backgroundColor = UIColor.black()
// user rotates the camera by tapping
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTap(_:)))
scnView.addGestureRecognizer(tapGesture)
}
//the function which does camera rotation :
func handleTap(_ gestureRecognize: UIGestureRecognizer) {
//I was wrong, the solution is not here
let cameraOrbit = scene.rootNode.childNode(withName: "orbit", recursively: true)!
SCNTransaction.begin()
SCNTransaction.animationDuration = 2
cameraOrbit.eulerAngles.z += Float(M_PI_2) //works also for x and y
SCNTransaction.commit()
}
...
}

iOS UIView 3D border

I have been working on coin like UIView, which has 3d rotation.
I was able to implement rotate effect with CATransform3dRotate, based on the gesture. Now I need to implement shadow like effect, when image rotates.
Please refer to the image attached herewith.
Any suggestions? recommendations? or sample code would be great
Sample Requirement
You might want to look at SceneKit. I've put together a quick demo, which you can download here, of a 3D coin object which rotates horizontally left or right by a swipe gesture, with a realistic light source and shadowing. The relevant code follows:
import UIKit
import SceneKit
class ViewController: UIViewController {
#IBOutlet weak var sceneView: SCNView!
let rotate90AboutZ = SCNAction.rotateByX(0.0, y: 0.0, z: CGFloat(M_PI_2), duration: 0.0)
var currentAngle: Float = 0.0
var coinNode = SCNNode()
override func viewDidLoad() {
super.viewDidLoad()
sceneView.userInteractionEnabled = true
sceneView.backgroundColor = UIColor.blackColor()
sceneView.autoenablesDefaultLighting = true
}
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(true)
coinSceneSetup()
}
func coinSceneSetup() {
let coinScene = SCNScene()
//coin is rendered as a cylinder with a very small height
let coinGeometry = SCNCylinder(radius: 50, height: 2)
coinNode = SCNNode(geometry: coinGeometry)
coinNode.position = SCNVector3Make(0.0, 25.0, 25.0)
coinScene.rootNode.addChildNode(coinNode)
//rotate coin 90 degrees about the z axis so that it stands upright
coinNode.runAction(rotate90AboutZ)
let shinyCoinMaterial = SCNMaterial()
shinyCoinMaterial.diffuse.contents = UIColor.lightGrayColor()
shinyCoinMaterial.specular.contents = UIColor.whiteColor()
shinyCoinMaterial.shininess = 1.0
coinGeometry.firstMaterial = shinyCoinMaterial
sceneView.scene = coinScene
let panRecognizer = UIPanGestureRecognizer(target: self, action: "panGesture:")
sceneView.addGestureRecognizer(panRecognizer)
}
//allows for coin to spin with a right or left finger swipe, while still keeping it rotated 90 degrees about z axis
func panGesture(sender: UIPanGestureRecognizer) {
let translation = sender.translationInView(sender.view!)
var newAngle = (Float)(translation.x)*(Float)(M_PI)/180.0
newAngle += currentAngle
coinNode.runAction(SCNAction.rotateToX(CGFloat(newAngle), y: 0.0, z: CGFloat(M_PI_2), duration: 0.0))
if(sender.state == UIGestureRecognizerState.Ended) {
currentAngle = newAngle
}
}
}
Several excellent introductory tutorials are available, for example here, here, and here. weheartswift.com put out a three part tutorial in Swift that I personally find especially useful. Here are parts 1, 2 and 3.
Jeff Lamarche's introduction to SceneKit on OS X, along with his accompanying source code is a valuable resource as well.

Rotate SCNCamera node looking at an object around an imaginary sphere

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.
Setting the Scene
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:
You don't have to set the initial camera position in Cartesian coordinates. Just place it at whatever distance you want along the z-axis. Since cameraNode is a child node of cameraOrbit, its own position stays constant -- the camera moves due to the rotation of cameraOrbit.
As long as you just want the camera pointed at the center of this imaginary sphere, you don't need a look-at constraint. The camera points in the -Z direction of the space it's in -- if you move it in the +Z direction, then rotate the parent node, the camera will always point at the center of the parent node (i.e. the center of rotation).
Handling Input
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:
Looking for arcball rotation? (It's great for direct manipulation, since you can feel like you're physically pushing a point on the 3D object.) There are some questions and answers about that already on SO -- most of them use 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.)
For a simpler alternative, you can just map the x and y axes of your gesture to the yaw and pitch angles of your node. It's not as spiffy as arcball rotation, but it's pretty easy to implement -- all you need to do is work out a points-to-radians conversion that covers the amount of rotation you're after.
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.
Hey I ran into the problem the other day and the solution I came up with is fairly simple but works well.
First I created my camera and added it to my scene like so:
// create and add a camera to the scene
cameraNode = [SCNNode node];
cameraNode.camera = [SCNCamera camera];
cameraNode.camera.automaticallyAdjustsZRange = YES;
[scene.rootNode addChildNode:cameraNode];
// place the camera
cameraNode.position = SCNVector3Make(0, 0, 0);
cameraNode.pivot = SCNMatrix4MakeTranslation(0, 0, -15); //the -15 here will become the rotation radius
Then I made a CGPoint slideVelocity class variable. And created a UIPanGestureRecognizer and a and in its callback I put the following:
-(void)handlePan:(UIPanGestureRecognizer *)gestureRecognize{
slideVelocity = [gestureRecognize velocityInView:self.view];
}
Then I have this method that is called every frame. Note that I use GLKit for quaternion math.
-(void)renderer:(id<SCNSceneRenderer>)aRenderer didRenderScene:(SCNScene *)scenie atTime:(NSTimeInterval)time {
//spin the camera according the the user's swipes
SCNQuaternion oldRot = cameraNode.rotation; //get the current rotation of the camera as a quaternion
GLKQuaternion rot = GLKQuaternionMakeWithAngleAndAxis(oldRot.w, oldRot.x, oldRot.y, oldRot.z); //make a GLKQuaternion from the SCNQuaternion
//The next function calls take these parameters: rotationAngle, xVector, yVector, zVector
//The angle is the size of the rotation (radians) and the vectors define the axis of rotation
GLKQuaternion rotX = GLKQuaternionMakeWithAngleAndAxis(-slideVelocity.x/viewSlideDivisor, 0, 1, 0); //For rotation when swiping with X we want to rotate *around* y axis, so if our vector is 0,1,0 that will be the y axis
GLKQuaternion rotY = GLKQuaternionMakeWithAngleAndAxis(-slideVelocity.y/viewSlideDivisor, 1, 0, 0); //For rotation by swiping with Y we want to rotate *around* the x axis. By the same logic, we use 1,0,0
GLKQuaternion netRot = GLKQuaternionMultiply(rotX, rotY); //To combine rotations, you multiply the quaternions. Here we are combining the x and y rotations
rot = GLKQuaternionMultiply(rot, netRot); //finally, we take the current rotation of the camera and rotate it by the new modified rotation.
//Then we have to separate the GLKQuaternion into components we can feed back into SceneKit
GLKVector3 axis = GLKQuaternionAxis(rot);
float angle = GLKQuaternionAngle(rot);
//finally we replace the current rotation of the camera with the updated rotation
cameraNode.rotation = SCNVector4Make(axis.x, axis.y, axis.z, angle);
//This specific implementation uses velocity. If you don't want that, use the rotation method above just replace slideVelocity.
//decrease the slider velocity
if (slideVelocity.x > -0.1 && slideVelocity.x < 0.1) {
slideVelocity.x = 0;
}
else {
slideVelocity.x += (slideVelocity.x > 0) ? -1 : 1;
}
if (slideVelocity.y > -0.1 && slideVelocity.y < 0.1) {
slideVelocity.y = 0;
}
else {
slideVelocity.y += (slideVelocity.y > 0) ? -1 : 1;
}
}
This code gives infinite Arcball rotation with velocity, which I believe is what you are looking for. Also, you don't need the SCNLookAtConstraint with this method. In fact, that will probably mess it up, so don't do that.
If you want to implement rickster's answer using a gesture recognizer, you have to save state information as you'll only be given a translation relative to the beginning of the gesture. I added two vars to my class
var lastWidthRatio: Float = 0
var lastHeightRatio: Float = 0
And implemented his rotate code as follows:
func handlePanGesture(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
if (sender.state == .Ended) {
lastWidthRatio = widthRatio % 1
lastHeightRatio = heightRatio % 1
}
}
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 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!)
var widthRatio = Float(translation.x) / Float(gestureRecognize.view!.frame.size.width) + lastWidthRatio
var heightRatio = Float(translation.y) / Float(gestureRecognize.view!.frame.size.height) + lastHeightRatio
if (numberOfTouches==fingersNeededToPan) {
// 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
}
}
override func supportedInterfaceOrientations() -> UIInterfaceOrientationMask {
return .Landscape
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Release any cached data, images, etc that aren't in use.
}
}
There's no need to save the state anywhere but the node itself.
The code which uses some sort of width ratio behaves weirdly when you scroll back and forth repeatedly, and other code here looks overcomplicated.
I came up with a different (and I believe a better one) solution for gesture recognizers, based on #rickster's approach.
UIPanGestureRecognizer:
#objc func handlePan(recognizer: UIPanGestureRecognizer) {
let translation = recognizer.velocity(in: recognizer.view)
cameraOrbit.eulerAngles.y -= Float(translation.x/CGFloat(panModifier)).radians
cameraOrbit.eulerAngles.x -= Float(translation.y/CGFloat(panModifier)).radians
}
UIPinchGestureRecognizer:
#objc func handlePinch(recognizer: UIPinchGestureRecognizer) {
guard let camera = cameraOrbit.childNodes.first else {
return
}
let scale = recognizer.velocity
let z = camera.position.z - Float(scale)/Float(pinchModifier)
if z < MaxZoomOut, z > MaxZoomIn {
camera.position.z = z
}
}
I used velocity, as with translation when you slow down the touch it would still be the same event, causing the camera to whirl very fast, not what you'd expect.
panModifier and pinchModifier are simple constant numbers which you can use to adjust responsiveness. I found the optimal values to be 100 and 15 respectively.
MaxZoomOut and MaxZoomIn are constants as well and are exactly what they appear to be.
I also use an extension on Float to convert degrees to radians and vice-versa.
extension Float {
var radians: Float {
return self * .pi / 180
}
var degrees: Float {
return self * 180 / .pi
}
}
After trying to implement these solutions (in Objective-C) I realized that Scene Kit actually makes this a lot easier than doing all of this. SCNView has a sweet property called allowsCameraControl that puts in the appropriate gesture recognizers and moves the camera accordingly. The only problem is that it's not the arcball rotation that you're looking for, although that can be easily added by creating a child node, positioning it wherever you want, and giving it a SCNCamera. For example:
_sceneKitView.allowsCameraControl = YES; //_sceneKitView is a SCNView
//Setup Camera
SCNNode *cameraNode = [[SCNNode alloc]init];
cameraNode.position = SCNVector3Make(0, 0, 1);
SCNCamera *camera = [SCNCamera camera];
//setup your camera to fit your specific scene
camera.zNear = .1;
camera.zFar = 3;
cameraNode.camera = camera;
[_sceneKitView.scene.rootNode addChildNode:cameraNode];

Resources