SceneKit: move camera towards direction its facing with pan gesture - ios

I have set up some custom camera controls in my SceneKit game. I am having a problem with my pan gesture auto-adapting based on the cameras y euler angle. The pan gesture I have works by panning the camera on the x and z axis (by using the gestures translation) The problem is, despite the cameras rotation, the camera will continue to pan on the x and z axis. I want it so that the camera pans on the axis its facing.
here are my gestures I am using to pan/rotate:
panning:
var previousTranslation = SCNVector3(x: 0.0,y: 15,z: 0.0)
var lastWidthRatio:Float = 0
var angle:Float = 0
#objc func pan(gesture: UIPanGestureRecognizer) {
gesture.minimumNumberOfTouches = 1
gesture.maximumNumberOfTouches = 1
if gesture.numberOfTouches == 1 {
let view = self.view as! SCNView
let node = view.scene!.rootNode.childNode(withName: "Node", recursively: false)
let secondNode = view.scene!.rootNode.childNode(withName: "CameraHandler", recursively: false)
let translation = gesture.translation(in: view)
let constant: Float = 30.0
angle = secondNode!.eulerAngles.y
//these were the previous values I was using to handle panning, they worked but provided really jittery movement. You can change the direction they rotate by multiplying the sine/cosine .pi values by any integer.
//var translateX = Float(translation.y) * sin(.pi) / cos(.pi) - Float(translation.x) * cos(.pi)
//var translateY = Float(translation.y) * cos(.pi) / cos(.pi) + Float(translation.x) * sin(.pi)
//these ones work a lot smoother
var translateX = Float(translation.x) * Float(Double.pi / 180)
var translateY = Float(translation.y) * Float(Double.pi / 180)
translateX = translateX * constant
translateY = translateY * constant
switch gesture.state {
case .began:
previousTranslation = node!.position
break;
case .changed:
node!.position = SCNVector3Make((previousTranslation.x + translateX), previousTranslation.y, (previousTranslation.z + translateY))
break
default: break
}
}
}
rotation:
#objc func rotate(gesture: UIPanGestureRecognizer) {
gesture.minimumNumberOfTouches = 2
gesture.maximumNumberOfTouches = 2
if gesture.numberOfTouches == 2 {
let view = self.view as! SCNView
let node = view.scene!.rootNode.childNode(withName: "CameraHandler", recursively: false)
let translate = gesture.translation(in: view)
var widthRatio:Float = 0
widthRatio = Float(translate.x / 10) * Float(Double.pi / 180)
switch gesture.state {
case .began:
lastWidthRatio = node!.eulerAngles.y
break
case .changed:
node!.eulerAngles.y = lastWidthRatio + widthRatio
print(node!.eulerAngles.y)
break
default: break
}
}
}
the CameraHandler Node is the parent node of the Camera Node. It all works, it just doesnt work like I want it to. Hopefully this is clear enough for you guys to understand.

In Objective C. The key part is the last three lines and specifically the order of multiplication of the matrices in the last line (causing the movement to happen in local space). If the transmat and cammat are switched it would behave again like you have now (moving in world space). The refactor part is just something that works for my specific situation where both perspective and orthographic camera is possible.
-(void)panCamera :(CGPoint)location {
CGFloat dx = _prevlocation.x - location.x;
CGFloat dy = location.y - _prevlocation.y;
_prevlocation = location;
//refactor dx and dy based on camera distance or orthoscale
if (cameraNode.camera.usesOrthographicProjection) {
dx = dx / 416 * cameraNode.camera.orthographicScale;
dy = dy / 416 * cameraNode.camera.orthographicScale;
} else {
dx = dx / 720 * cameraNode.position.z;
dy = dy / 720 * cameraNode.position.z;
}
SCNMatrix4 cammat = self.cameraNode.transform;
SCNMatrix4 transmat = SCNMatrix4MakeTranslation(dx, 0, dy);
self.cameraNode.transform = SCNMatrix4Mult(transmat, cammat);
}

I figured it out, based off of what Xartec answered. I translated it into swift and retro-fitted it to work with what I needed. I'm not truly happy with it because the movement is not smooth. I will work on smoothing it out later today.
pan gesture: This gesture pans the cameras parent node around the scene in the direction that the camera is rotated. It works exactly like I wanted.
#objc func pan(gesture: UIPanGestureRecognizer) {
gesture.minimumNumberOfTouches = 1
gesture.maximumNumberOfTouches = 1
if gesture.numberOfTouches == 1 {
let view = self.view as! SCNView
let node = view.scene!.rootNode.childNode(withName: "CameraHandler", recursively: false)
let translation = gesture.translation(in: view)
var dx = previousTranslation.x - translation.x
var dy = previousTranslation.y - translation.y
dx = dx / 100
dy = dy / 100
print(dx,dy)
let cammat = node!.transform
let transmat = SCNMatrix4MakeTranslation(Float(dx), 0, Float(dy))
switch gesture.state {
case .began:
previousTranslation = translation
break;
case .changed:
node!.transform = SCNMatrix4Mult(transmat, cammat)
break
default: break
}
}
}
rotation gesture: This gesture rotates the camera with two fingers.
#objc func rotate(gesture: UIPanGestureRecognizer) {
gesture.minimumNumberOfTouches = 2
gesture.maximumNumberOfTouches = 2
if gesture.numberOfTouches == 2 {
let view = self.view as! SCNView
let node = view.scene!.rootNode.childNode(withName: "CameraHandler", recursively: false)
let translate = gesture.translation(in: view)
var widthRatio:Float = 0
widthRatio = Float(translate.x / 10) * Float(Double.pi / 180)
switch gesture.state {
case .began:
lastWidthRatio = node!.eulerAngles.y
break
case .changed:
node!.eulerAngles.y = lastWidthRatio + widthRatio
break
default: break
}
}
}
To get the same functionality that I have, you need to attach the cameraNode to a parent node. Like so:
//create and add a camera to the scene
let cameraNode = SCNNode()
//cameraHandler is declared outside viewDidLoad.
let cameraHandler = SCNNode()
cameraNode.camera = SCNCamera()
cameraNode.name = "Camera"
cameraNode.position = SCNVector3(x: 0.0,y: 10.0,z: 20.0)
//This euler angle only rotates the camera downward a little bit. It is not neccessary
cameraNode.eulerAngles = SCNVector3(x: -0.6, y: 0, z: 0)
cameraHandler.addChildNode(cameraNode)
cameraHandler.name = "CameraHandler"
scene.rootNode.addChildNode(cameraHandler)

Related

I need to move and rotate view around point in the screen with pan gesture

I want to make the view rotate and move around the center of the screen.
Here is visual representation of what I want to achieve. Image
And my current solution:
#objc func circleMoved(_ gesture: UIPanGestureRecognizer) {
switch gesture.state {
case .changed:
let translation = gesture.translation(in: view)
circle.layer.zPosition = 200
let spread = view.width
let angle: CGFloat = 100
let angleMoveX = angle * translation.x / spread
let angleMoveY = angle * translation.y / spread
let angleMoveXRadians = angleMoveX * CGFloat.pi/180
let angleMoveYRadians = angleMoveY * CGFloat.pi/180
var move = CATransform3DIdentity
move = CATransform3DRotate(move, angleMoveXRadians, 0, 1, 0)
move = CATransform3DRotate(move, -angleMoveYRadians, 1, 0, 0)
move = CATransform3DTranslate(move, translation.x, translation.y, 0)
circle.layer.transform = CATransform3DConcat(circle.layer.transform, move)
gesture.setTranslation(.zero, in: view) } }
But the problem is that it rotates more than I wanted. (60 degrees max)

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.

UIPanGestureRecognizer sometimes doesn't get into End State

I'm developing a card view like in Tinder. When cards X origin is bigger than a value which I declare, It moves out the screen. Otherwise, It sticks to center again. I'm doing all of these things inside UIPanGestureRecognizer function. I can move the view in Change state. However, It sometimes doesn't get into end state so card is neither moves out of the screen or stick to center again. It just stays in some weird place.
So My problem is that card should go out of the screen like in below screenshot or stick into center.
I tried solutions in below post but nothing worked:
UIPanGestureRecognizer not calling End state
UIPanGestureRecognizer does not switch to state "End" or "Cancel" if user panned x and y in negative direction
/// This method handles the swiping gesture on each card and shows the appropriate emoji based on the card's center.
#objc func handleCardPan(sender: UIPanGestureRecognizer) {
// Ensure it's a horizontal drag
let velocity = sender.velocity(in: self.view)
if abs(velocity.y) > abs(velocity.x) {
return
}
// if we're in the process of hiding a card, don't let the user interace with the cards yet
if cardIsHiding { return }
// change this to your discretion - it represents how far the user must pan up or down to change the option
// distance user must pan right or left to trigger an option
let requiredOffsetFromCenter: CGFloat = 80
let panLocationInView = sender.location(in: view)
let panLocationInCard = sender.location(in: cards[0])
switch sender.state {
case .began:
dynamicAnimator.removeAllBehaviors()
let offset = UIOffsetMake(cards[0].bounds.midX, panLocationInCard.y)
// card is attached to center
cardAttachmentBehavior = UIAttachmentBehavior(item: cards[0], offsetFromCenter: offset, attachedToAnchor: panLocationInView)
//dynamicAnimator.addBehavior(cardAttachmentBehavior)
let translation = sender.translation(in: self.view)
print(sender.view!.center.x)
sender.view!.center = CGPoint(x: sender.view!.center.x + translation.x, y: sender.view!.center.y)
sender.setTranslation(CGPoint(x: 0, y: 0), in: self.view)
case .changed:
//cardAttachmentBehavior.anchorPoint = panLocationInView
let translation = sender.translation(in: self.view)
print(sender.view!.center.x)
sender.view!.center = CGPoint(x: sender.view!.center.x + translation.x, y: sender.view!.center.y)
sender.setTranslation(CGPoint(x: 0, y: 0), in: self.view)
case .ended:
dynamicAnimator.removeAllBehaviors()
if !(cards[0].center.x > (self.view.center.x + requiredOffsetFromCenter) || cards[0].center.x < (self.view.center.x - requiredOffsetFromCenter)) {
// snap to center
let snapBehavior = UISnapBehavior(item: cards[0], snapTo: CGPoint(x: self.view.frame.midX, y: self.view.frame.midY + 23))
dynamicAnimator.addBehavior(snapBehavior)
} else {
let velocity = sender.velocity(in: self.view)
let pushBehavior = UIPushBehavior(items: [cards[0]], mode: .instantaneous)
pushBehavior.pushDirection = CGVector(dx: velocity.x/10, dy: velocity.y/10)
pushBehavior.magnitude = 175
dynamicAnimator.addBehavior(pushBehavior)
// spin after throwing
var angular = CGFloat.pi / 2 // angular velocity of spin
let currentAngle: Double = atan2(Double(cards[0].transform.b), Double(cards[0].transform.a))
if currentAngle > 0 {
angular = angular * 1
} else {
angular = angular * -1
}
let itemBehavior = UIDynamicItemBehavior(items: [cards[0]])
itemBehavior.friction = 0.2
itemBehavior.allowsRotation = true
itemBehavior.addAngularVelocity(CGFloat(angular), for: cards[0])
dynamicAnimator.addBehavior(itemBehavior)
showNextCard()
hideFrontCard()
}
default:
break
}
}
I was checking If I'm swiping in horizontal with:
let velocity = sender.velocity(in: self.view)
if abs(velocity.y) > abs(velocity.x) {
return
}
For some reason, It was getting in to return while I'm swiping horizontal. When I comment this block of code, everything started to work :)

SceneKit: Using pan gesture to move a node that's not at origin

I have a SCNNode that I've set at a position of SCNVector3(0,0,-1). The following code moves the node forward or backward along the Z-axis, however, the initial pan gesture moves the node to (0,0,0) and then moves the node in line with the pan gesture.
let currentPositionDepth = CGPoint()
#objc func handlePan(sender: UIPanGestureRecognizer) {
let translation = sender.translation(in: sceneView)
var newPos = CGPoint(x: CGFloat(translation.x), y: CGFloat(translation.y))
newPos.x += currentPositionDepth.x
newPos.y += currentPositionDepth.y
node.position.x = Float(newPos.x)
node.position.z = Float(newPos.y)
if(sender.state == .ended) { currentPositionDepth = newPos }
}
I'd like the node to move from it's set position of (0,0,-1). I've tried setting currentPositionDepth.y to -1, however it does not achieve the desired effect. How can I achieve this?
Try something like this:
var previousLoc = CGPoint.init(x: 0, y: 0)
#objc func panAirShip(sender: UIPanGestureRecognizer){
var delta = sender.translation(in: self.view)
let loc = sender.location(in: self.view)
if sender.state == .changed {
delta = CGPoint.init(x: 2 * (loc.x - previousLoc.x), y: 2 * (loc.y - previousLoc.y))
airshipNode.position = SCNVector3.init(airshipNode.position.x + Float(delta.x * 0.02), airshipNode.position.y + Float(-delta.y * (0.02)), 0)
previousLoc = loc
}
previousLoc = loc
}
I have multiplied the 0.02 factor to make the translation smoother and in turn easier for the end user. You may change that factor to anything else you like.

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