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
}
Related
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)
I’m using ARKit with SceneKit. When user presses a button I create an anchor and to the SCNNode corresponding to it I add a 3D object (loaded from a .scn file in the project).
The 3D object is placed facing the camera, with the same orientation the camera has. I would like to make it look like the object is laying on a plane surface and not inclined if it is that way. So, if I got it right, I’d need to apply a rotation transformation so that it’s rotation around the X and Z axis become 0.
My attempt at this is: take the node’s x and z eulerAngles, invert them, and rotate that amount around each axis
let rotationZ = rotationMatrixAroundZ(radians: -node.eulerAngles.z)
let rotationX = rotationMatrixAroundX(radians: -node.eulerAngles.x)
let rotationTransform = simd_mul(rotationTransformX, rotationTransformZ)
node.transform = SCNMatrix4(simd_mul(simd_float4x4(node.transform), rotationTransform))
This works all right for most cases, but in some the object is rotated in completely strange ways. Should I be setting the
rotation angle to anything else than just the inverse of the current Euler Angle? Setting the angles to 0 directly did not work at all.
I've come across this and figured out I was running into gimbal lock. The solution was to rotate the node around one axis, parent it to another SCNNode(), then rotate the parent around the other axis. Hope that helps.
You don't have to do the node transform on a matrix, you can simply rotate around a specific axis and that might be a bit simpler in terms of the logic of doing the rotation.
You could do something like:
node.runAction(SCNAction.rotateBy(x: x, y: y, z: z, duration: 0.0))
Not sure if this is the kind of thing you're looking for, but it is simpler than doing the rotation with the SCNMatrix4
Well, I managed a workaround, but I'm not truly happy with it, so I'll leave the question unanswered. Basically I define a threshold of 2 degrees and keep applying those rotations until both Euler Angles around X and Z are below the aforementioned threshold.
func layDownNode(_ node: SCNNode) {
let maxErrDegrees: Float = 2.0
let maxErrRadians = GLKMathDegreesToRadians(maxErrDegrees)
while (abs(node.eulerAngles.x) > maxErrRadians || abs(node.eulerAngles.z) > maxErrRadians) {
let rotationZ = -node.eulerAngles.z
let rotationX = -node.eulerAngles.x
let rotationTransformZ = rotationMatrixAroundZ(radians: rotationZ)
let rotationTransformX = rotationMatrixAroundX(radians: rotationX)
let rotationTransform = simd_mul(rotationTransformX, rotationTransformZ)
node.transform = SCNMatrix4(simd_mul(simd_float4x4(node.transform), rotationTransform))
}
}
I'm setting my SceneKit camera to the current CMDeviceMotion attitude using the CMDeviceMotion extension described in this answer:
func deviceDidMove(motion: CMDeviceMotion?, error: NSError?) {
if let motion = motion {
let orientation = motion.gaze(atOrientation: UIApplication.sharedApplication().statusBarOrientation)
cameraNode.orientation = orientation
}
}
This works beautifully, however, I'd like to block the rotation (roll) and only allow the camera to turn around (yaw) and pitch.
I tried to convert the quaternion back to Euler angles and simply leave roll at 0:
cameraNode.eulerAngles = SCNVector3(
x: orientation.pitch(),
y: orientation.yaw(),
z: 0)
However, this works for only one half of the yaw movement. For the other half, the camera is turned upside down. I suspect this is a Gimbal Lock issue involving Euler angles.
Quaternions are a bit like black magic for me, but is there a way to remove the roll component directly on the quaternion level so I can avoid Euler angles?
I try use lookat function rotate camera with pan gesture. I use swift and Metal(in this case Metal work the same with OpenGLES). Here is my code
The lookat function:
let kEye = V3f(0.0, 0.0, -2.0)
var ktarget = V3f(0.0, 0.0, 0.0)
let kUp = V3f(0.0, 1.0, 0.0)
var viewMatrix = lookAt(kEye, center: ktarget, up: kUp)
The pan gesture:
func pan(panGesture: UIPanGestureRecognizer){
if panGesture.state == UIGestureRecognizerState.Changed{
let pointInView = panGesture.locationInView(self.view)
let xDelta = (lastPanLocation.x - pointInView.x)/self.view.bounds.width * panSensivity
let yDelta = (lastPanLocation.y - pointInView.y)/self.view.bounds.height * panSensivity
lastPanLocation = pointInView
var viewDirection = rotationM3f(kUp, angle: Float(-xDelta)) * viewDirection
var toRotateAround = Cross(viewDirection, b: kUp)
viewDirection = rotationM3f(toRotateAround, angle: Float(-yDelta)) * viewDirection
ktarget = kEye + viewDirection
} else if panGesture.state == UIGestureRecognizerState.Began {
lastPanLocation = panGesture.locationInView(self.view)
}
}
At the beginning, it works fine, pan the camera after a while, the viewDirection and toRotateAround vector will become -0.0,-0.0,0.0, when finger move vertically but the camera does not look up and down, Anyone knows what is wrong in the code? Thanks~~~
You only modify the view direction (ktarget in the end) but forget about kUP. Once these 2 vectors become parallel the cross product is zero and everything breaks.
The solution you are looking for is recomputing the kUp vector by using a cross product of viewDirection and toRotateAround.
When using rotations like this you need to think of your data as base vectors and a position (location=kEye, forward=ktarget-keye, up=kUp, right=cross(forward, up)). The base vectors are always perpendicular to each other (I suggest them to be normalized as well) and when you rotate you always rotate one of these vectors around another base vectors and after the rotation you need to recompute the 3rd vector by using a cross product.
So to rotate left or right you would rotate the forward around up and then use a cross product between forward and up to get the right vector. (The right vector here is optional since you do not use it)
To rotate up or down you rotate forward vector around right vector and use a cross product between the forward and right to get the new top vector.
Then for tilting left or right you rotate up around forward and get the right vector with cross product of the 2 used vectors.
If you see the logic you will find out there are always 2 ways of rotating along one axis. For instance to rotate left or right you might as well rotate the right vector around up and find the new forward vector by using a cross product of right and up.
There is a trick though. The procedure described here works great for a free movement such as a flight simulation where you can "tilt". It is not appropriate for a movement such as for a first person shooter where up is always in the center of the screen horizontally (I hope you see the difference). To create this FPS way you actually do need to keep up as (0,1,0) but then forward must never be (0,1,0) but it can be (0.001, 0.09, 0) which is pretty close to looking directly upwards. So as long as you limit the upwards angle to some value you should be fine. There are other ways as well...
I'm trying to make a game where the sprite will always move to the right when hit by an object. However since the Sprite rotates constantly and the zero radians rotates with the Sprite causes my calculated magnitude to go the opposite direction if the sprite is facing left and hits the object. Is there a way to keep the direction of the magnitude always pointing to the right even if the zero is facing left?
// referencePoint = upper right corner of the frame
let rightTriangleFinalPoint:CGPoint = CGPoint(x: referencePoint.x, y: theSprite.position.y)
let theSpriteToReferenceDistance = distanceBetweenCGPoints(theSprite.position, b: referencePoint)
let theSpriteToFinalPointDistance = distanceBetweenCGPoints(theSprite.position, b: rightTriangleFinalPoint)
let arcCosineValue = theSpriteToFinalPointDistance / theSpriteToReferenceDistance
let angle = Double(acos(arcCosineValue))
let xMagnitude = magnitude * cos(angle)
let yMagnitude = (magnitude * sin(angle)) / 1.5
Not sure if this works for you:
I would use an orientation constraint to rotate the sprite. The movement can be done independent from the orientation in that case.
I made an tutorial some time ago: http://stefansdevplayground.blogspot.de/2014/09/howto-implement-targeting-or-follow.html
So I figured out what was going on.
It seems like the angle doesn't rotate with the Sprite like I originally thought and the vector that I am making is working with the code above. THE problem that I had was that I also set the collision bit for the objects which is wrong. If I only set the contact bit for the objects against the sprite the my desired outcome comes true.