How to implement a 3D compass with SceneKit - ios

Currently I am confused in terms of making a functionality of 3D compass. With SceneKit, I loaded an arrow object and then fetched sensor data like the code below (The effect is this video)
let motionManager = CMMotionManager()
motionManager.deviceMotionUpdateInterval = 1.0/60.0
if motionManager.isDeviceMotionAvailable {
motionManager.startDeviceMotionUpdates(to: OperationQueue.main, withHandler: { (devMotion, error) -> Void in
arrow.orientation = SCNQuaternion(-CGFloat((motionManager.deviceMotion?.attitude.quaternion.x)!), -CGFloat((motionManager.deviceMotion?.attitude.quaternion.y)!), -CGFloat((motionManager.deviceMotion?.attitude.quaternion.z)!), CGFloat((motionManager.deviceMotion?.attitude.quaternion.w)!))
})}
After done this, if the user rotate the device in any axis, the arrow can be rotated as well. Since this is a 3D compass, I need the arrow to point the direction. So I fetched the heading data via CLLocationManager, and then the problem is coming.
let gq1: GLKQuaternion = GLKQuaternionMakeWithAngleAndAxis(GLKMathDegreesToRadians(heading), 0, 0, 1)
arrow.orientation = SCNVector4(gq1.x,gq1.y,gq1.z,gq1.w)
The above code is doing perfectly in 2D like the pic below, it points the east direction correctly. But I want it in 3D environment, so I did like below
motionManager.deviceMotionUpdateInterval = 1.0/60.0
if motionManager.isDeviceMotionAvailable {
motionManager.startDeviceMotionUpdates(to: OperationQueue.main, withHandler: { (devMotion, error) -> Void in
arrow.orientation = orient(q: (motionManager.deviceMotion?.attitude.quaternion)!, angle: heading)
}
func orient(q:CMQuaternion,angle:Float) -> SCNQuaternion{
GLKQuaternionMakeWithAngleAndAxis(GLKMathDegreesToRadians(-angle), 0, 0, 1)
let gq1: GLKQuaternion = GLKQuaternionMakeWithAngleAndAxis(GLKMathDegreesToRadians(-angle), 0, 0, 1)
// add a rotation in z axis
let gq2: GLKQuaternion = GLKQuaternionMake(Float(q.x), Float(q.y), Float(q.z), Float(q.w))
// the current orientation
let qp: GLKQuaternion = GLKQuaternionMultiply(gq1, gq2)
// get the "new" orientation
var rq = CMQuaternion()
rq.x = Double(qp.x)
rq.y = Double(qp.y)
rq.z = Double(qp.z)
rq.w = Double(qp.w)
return SCNVector4(-Float(rq.x), -Float(rq.y), -Float(rq.z), Float(rq.w))
}
There is a bug(this video shows), if I rotate the device around z axis, the arrow still rotates around y axis. Did I do anything wrong?

If I rotate the device around z axis by this method, the direction data comes out continuously. As a result, I can't use CLLocationManager to achieve my goal. This thread is what I want.

Related

SCNNode orientation instead of eulerAngles

I am trying to rotate SCNNode with UIRotationGestureRecognizer and changing of eulerAngles. I switch between rotation around y and around z axis. When I rotate only one of them everything is fine. But problem appears when I rotate both. It no longer rotate around axis itself but it rotates randomly.
I have read many answers especially from ARGeo like this one: SCNNode Z-rotation axis stays constant, while X and Y axes change when node is rotated , and I understand problem of eulerAngles and gimbal lock.
But I don't understand how to correctly use orientation ant that w component. Is here someone who successfully used UIRotationGestureRecognizer and orientation instead of eulerAngles?
Here's one of an examples how to implement UIRotationGestureRecognizer.
I copied it from How can I rotate an SCNNode.... SO post.
private var startingOrientation = GLKQuaternion.identity
private var rotationAxis = GLKVector3Make(0, 0, 0)
#objc private func handleRotation(_ rotation: UIRotationGestureRecognizer) {
guard let node = sceneView.hitTest(rotation.location(in: sceneView), options: nil).first?.node else {
return
}
if rotation.state == .began {
startingOrientation = GLKQuaternion(boxNode.orientation)
let cameraLookingDirection = sceneView.pointOfView!.parentFront
let cameraLookingDirectionInTargetNodesReference = boxNode.convertVector(cameraLookingDirection,
from: sceneView.pointOfView!.parent!)
rotationAxis = GLKVector3(cameraLookingDirectionInTargetNodesReference)
} else if rotation.state == .ended {
startingOrientation = GLKQuaternionIdentity
rotationAxis = GLKVector3Make(0, 0, 0)
} else if rotation.state == .changed {
let quaternion = GLKQuaternion(angle: Float(rotation.rotation), axis: rotationAxis)
node.orientation = SCNQuaternion((startingOrientation * quaternion).normalized())
}
}
Hope this helps.

Rotate camera around itself

I am using two virtual joysticks to move my camera around the scene. The left stick controls the position and the right one controls the rotation.
When using the right stick, the camera rotates, but it seems that the camera rotates around the center point of the model.
This is my code:
fileprivate func rotateCamera(_ x: Float, _ y: Float)
{
if let cameraNode = self.cameraNode
{
let moveX = x / 50.0
let rotated = SCNMatrix4Rotate(cameraNode.transform, moveX, 0, 1, 0)
cameraNode.transform = rotated
}
}
I have also tried this code:
fileprivate func rotateCamera(_ x: Float, _ y: Float)
{
if let cameraNode = self.cameraNode
{
let moveX = x / 50.0
cameraNode.rotate(by: SCNQuaternion(moveX, 0, 1, 0), aroundTarget: cameraNode.transform)
}
}
But the camera just jumps around. What is my error here?
There are many ways to handle rotation, some are very suitable for giving headaches to the coder.
It sounds like the model is at 0,0,0, meaning it’s in the center of the world, and the camera is tranformed to a certain location. In the first example using matrices, you basically rotate that transformation. So you transform first, then rotate, which yes will cause it to rotate around the origin (0,0,0).
What you should do instead, to rotate the camera in local space, is rotate the camera first in local space and then translate it to its position in world space.
Translation x rotation matrix results in rotation in world space
Rotation x translation matrix results in rotation in local space
So a solution is to remove the translation from the camera first (moving it back to 0,0,0), then apply the rotation matrix, and then reapply the translation. This comes down to the same result as starting with an identity matrix. For example:
let rotated = SCNMatrix4Rotate(SCNMatrixIdentity, moveX, 0, 1, 0)
cameraNode.transform = SCNMatrix4Multiply(rotated, cameraNode.transform)

How can I use sensor data to affect the object in SceneKit?

I am implementing an AR app in iOS platform with SceneKit. I wanna my object to rotate followed by mobile rotation. For the mobile rotation side, I found there is a parameter called quaternion under CMAttitude class but I am not sure how can I use this parameter to rotate the object I loaded in the scene. Any ideas?
let motionManager = CMMotionManager()
motionManager.deviceMotionUpdateInterval = 1.0 / 60.0
if motionManager.isDeviceMotionAvailable {
motionManager.startDeviceMotionUpdates(to: OperationQueue.main, withHandler: { (devMotion, error) -> Void in
//change the left camera node euler angle in x, y, z axis
cameraNode.eulerAngles = SCNVector3(
-Float((devMotion?.attitude.roll)!) - Float(M_PI_2),
Float((motionManager.deviceMotion?.attitude.yaw)!),
-Float((motionManager.deviceMotion?.attitude.pitch)!)
)
})}
I tried this with Playground app in iPad.
I have tried using Core Motion, but what I do is rotate the scnCamera

How to rotate an SCNBox

I'm trying to rotate an SCNBox I created using swipe gestures. For example, when I swipe right the box should rotate 90degs in the Y-axis and -90degs when I swipe left. To achieve this I have been using the node's SCNAction.rotateByX method to perform the rotation animation. Now the problem I'm having is when rotating along either the X-axis or Z-axis after a rotation in the Y-axis and vice-versa is that the positions of the axes change.
What I have notice is that any rotation perform on either of the X,Y,Z axes changes the direction in which the other axes point.
Example: Default position
Then after a rotation in the Z-axis:
Of course this pose a problem because now when I swipe left or right I no longer get the desire effect because the X-axis and Y-axis have now swapped positions. What I would like to know is why does this happen? and is there anyway to perform the rotation animation without it affecting the other axes?
I apologize for my lack of understanding on this subject as this is my first go at 3d graphics.
Solution:
func swipeRight(recognizer: UITapGestureRecognizer) {
// rotation animation
let action = SCNAction.rotateByX(0, y: CGFloat(GLKMathDegreesToRadians(90)), z: 0, duration: 0.5)
boxNode.runAction(action)
//repositoning of the x,y,z axes after the rotation has been applied
let currentPivot = boxNode.pivot
let changePivot = SCNMatrix4Invert(boxNode.transform)
boxNode.pivot = SCNMatrix4Mult(changePivot, currentPivot)
boxNode.transform = SCNMatrix4Identity
}
I haven't ran into any problems yet but it may be safer to use a completion handler to ensure any changes to X,Y,Z axes are done before repositioning them.
I had the same issue, here's what I use to give the desired behavior:
func panGesture(sender: UIPanGestureRecognizer) {
let translation = sender.translationInView(sender.view!)
let pan_x = Float(translation.x)
let pan_y = Float(-translation.y)
let anglePan = sqrt(pow(pan_x,2)+pow(pan_y,2))*(Float)(M_PI)/180.0
var rotVector = SCNVector4()
rotVector.x = -pan_y
rotVector.y = pan_x
rotVector.z = 0
rotVector.w = anglePan
// apply to your model container node
boxNode.rotation = rotVector
if(sender.state == UIGestureRecognizerState.Ended) {
let currentPivot = boxNode.pivot
let changePivot = SCNMatrix4Invert(boxNode.transform)
boxNode.pivot = SCNMatrix4Mult(changePivot, currentPivot)
boxNode.transform = SCNMatrix4Identity
}
}

Core Motion: how to tell which way is "up"?

I'm trying to duplicate the functionality in the Compass app - and I'm stuck on a particular bit: how do I figure out which way is "up" in the interface?
I've got a label onscreen, and I've got the following code that orients it to remain horizontal as the device moves around:
self.motionManager = CMMotionManager()
self.motionManager?.gyroUpdateInterval = 1/100
self.motionManager?.startDeviceMotionUpdatesToQueue(NSOperationQueue.mainQueue(), withHandler: { (deviceMotion, error) -> Void in
let roll = -deviceMotion.attitude.roll
self.tiltLabel?.transform = CGAffineTransformRotate(CGAffineTransformIdentity, CGFloat(roll))
})
This effect is pretty good, but it's got a few states where it's wrong - for example, the label flips erratically when the iPhone's lightning connector is pointed up.
How do I consistently tell which direction is up using CoreMotion?
UPDATE: Apparently, roll/pitch/yaw are Euler angles, which suffer from gimbal lock - so I think the correct solution might involve using quaternions, which don't suffer from this issue, or perhaps the rotationMatrix on CMAttitude might help: https://developer.apple.com/library/ios/documentation/CoreMotion/Reference/CMAttitude_Class/index.html
It doesn't need to be quite so complicated for the 2D case. "Up" means "opposite gravity", so:
motionManager.startDeviceMotionUpdatesToQueue(NSOperationQueue.mainQueue()) { (motion, error) in
// Gravity as a counterclockwise angle from the horizontal.
let gravityAngle = atan2(Double(motion.gravity.y), Double(motion.gravity.x))
// Negate and subtract π/2, because we want -π/2 ↦ 0 (home button down) and 0 ↦ -π/2 (home button left).
self.tiltLabel.transform = CGAffineTransformMakeRotation(CGFloat(-gravityAngle - M_PI_2))
}
But simply "opposite gravity" has less meaning if you're trying to do this in all 3 dimensions: the direction of gravity doesn't tell you anything about the phone's angle around the gravity vector (if your phone is face-up, this is the yaw angle). To correct in three dimensions, we can use the roll, pitch, and yaw measurements instead:
// Add some perspective so the label looks (roughly) the same,
// no matter what angle the device is held at.
var t = self.view.layer.sublayerTransform
t.m34 = 1/300
self.view.layer.sublayerTransform = t
motionManager.startDeviceMotionUpdatesToQueue(NSOperationQueue.mainQueue()) { (motion, error) in
let a = motion.attitude
self.tiltLabel.layer.transform =
CATransform3DRotate(
CATransform3DRotate(
CATransform3DRotate(
CATransform3DMakeRotation(CGFloat(a.roll), 0, -1, 0),
CGFloat(a.pitch), 1, 0, 0),
CGFloat(a.yaw), 0, 0, 1),
CGFloat(-M_PI_2), 1, 0, 0) // Extra pitch to make the label point "up" away from gravity
}

Resources