I have a line (UIView with width = screen width and height = 2), I need to move this line up and down the screen with forward tilt and backward tilt. I know I need to use Gyroscope, how can I achieve this using MotionManager()?
var motionManager = CMMotionManager()
private func getGyroUpdates() {
if motionManager.isDeviceMotionAvailable == true {
motionManager.deviceMotionUpdateInterval = 0.1
let queue = OperationQueue()
motionManager.startDeviceMotionUpdates(to: queue, withHandler: { [weak self] motion, error in
// Get the attitude of the device
guard let motion = motion else { return }
let pitch = Double(round(motion.attitude.pitch.rad2deg()))
let length = sqrt(motion.gravity.x * motion.gravity.x + motion.gravity.y * motion.gravity.y + motion.gravity.z * motion.gravity.z)
// how to i get the value to be plotted in Y? do i use gravity? or pitch ?
DispatchQueue.main.async {
// frontBackMovement is the line view here
self?.frontBackMovement.transform = CGAffineTransform(translationX: 0, y: "what should be the value here??")
}
})
print("Device motion started")
}else {
print("Device motion unavailable")
}
}
line needs to move in the frame which is a uiview, I need to get value to put in the place of Y to put it in CGAffineTransform. so basically how do I map value I get from Motion object to plot it in Y. I tried the radian value I get from attitude.pitch but how do I convert that into Y? If I use gravity value how do I use it?
Thank you very much for your help in advance.
Depending on the effect you are trying to achieve you would want to get the pitch angle from motion.attitude.pitch. Then you need to calculate the y offset based on how far you want the line to move relative to the pitch angle.
Let's say you want the line to move 100 points up or down as the device is tilted between -90º and 90º.
You had the angle:
let pitch = Double(round(motion.attitude.pitch.rad2deg()))
So now calculate the distance:
let maxDistance = 100.0
let currentDistance = pitch / 90.0 * maxDistance
where maxDistance is the furthest you want the line to move. You probably need some extra checks to ensure pitch is kept between -90 and 90.
A much simpler approach than using CMMotionManager is to use UIInterpolatingMotionEffect. First setup your line to be in the center of its parent view. Then use the following code:
let maxDistance: Float = 100 // how far do you want the line to move
let eff = UIInterpolatingMotionEffect(keyPath: "center.y", type: .tiltAlongVerticalAxis)
eff.maximumRelativeValue = maxDistance
eff.minimumRelativeValue = -maxDistance
lineView.addMotionEffect(eff)
where maxDistance is the max distance you want the line to move as the device is tilted. In your case this sounds like it should be half the height of the line's parent view.
Related
Suppose you are holding an iphone/ipad vertically in front of you with the screen facing you, in portrait orientation. You tilt the device forward and backward keeping the screen facing you. I have the pitch value from CMMotion manager. I am facing issue with plotting the value of Y-axis accurately to the movement.
func setUpGyro() {
if motionManager.isGyroAvailable {
motionManager.deviceMotionUpdateInterval = 0.1;
motionManager.startDeviceMotionUpdates(to: OperationQueue.main, withHandler: {[weak self] (deviceMotion, NSError) -> Void in
self?.updatePitchMotion(deviceMotion!)
})
} else {
// gyro data is not available
}
}
private func updatePitchMotion(_ motion: CMDeviceMotion) {
let pitch = Double(round(motion.attitude.pitch.rad2deg()))
let maxDistance: CGFloat = 75 // line will not go over 75, as height of the superview is 150
let currentDistance = pitch / 90.0 * maxDistance
DispatchQueue.main.async {
// plotGreen changes the color of the line to green as line is red by default
pitch == -7 ? self.plotGreen() : self.plotRed()
pitch == -8 ? self.plotGreen() : self.plotRed()
pitch == -6 ? self.plotGreen() : self.plotRed()
self.pitchIndicatorLine.transform = CGAffineTransform(translationX: 0, y: currentDistance)
print(pitch)
}
}
I'm getting results from the above code for pitchIndicatorLine but the screen is facing in the upward direction (horizontally) and not facing myself (vertically), can anyone please correct the code for the device to work vertically.
Note: I think its more of a logic issue.
I'm working with an AR app that uses Apple's Focus Square to guide user interaction on both vertical and horizontal surfaces. However, it's only designed to work on horizontal surfaces. I have modified the method that updates the square's rotation to account for vertical surfaces as well. However, using the method that they used only one rotation can be performed at a time, whereas in order to have the square positioned properly on a horizontal surface I need to have two rotations performed (one to apply the original yaw correction, and one to pitch the square vertically).
The relevant code is below, but the specific changes I made are only at the beginning and end.
// MARK: - Appearence
func update(for position: float3, planeAnchor: ARPlaneAnchor?, camera: ARCamera?) {
lastPosition = position
let thisAlignment = planeAnchor?.alignment ?? .horizontal
if thisAlignment != lastAlignment {
lastAlignment = thisAlignment
print("alignmentChanged")
recentFocusSquarePositions = []
}
if let anchor = planeAnchor {
close(flash: !anchorsOfVisitedPlanes.contains(anchor))
lastPositionOnPlane = position
anchorsOfVisitedPlanes.insert(anchor)
} else {
open()
}
updateTransform(for: position, alignment: thisAlignment, camera: camera)
}
private func updateTransform(for position: float3, alignment: ARPlaneAnchor.Alignment, camera: ARCamera?) {
// add to list of recent positions
recentFocusSquarePositions.append(position)
// remove anything older than the last 8
recentFocusSquarePositions.keepLast(8)
// move to average of recent positions to avoid jitter
if let average = recentFocusSquarePositions.average {
self.simdPosition = average
self.setUniformScale(scaleBasedOnDistance(camera: camera))
}
// Correct y rotation of camera square
if let camera = camera {
let tilt = abs(camera.eulerAngles.x)
let threshold1: Float = .pi / 2 * 0.65
let threshold2: Float = .pi / 2 * 0.75
let yaw = atan2f(camera.transform.columns.0.x, camera.transform.columns.1.x)
var angle: Float = 0
switch tilt {
case 0..<threshold1:
angle = camera.eulerAngles.y
case threshold1..<threshold2:
let relativeInRange = abs((tilt - threshold1) / (threshold2 - threshold1))
let normalizedY = normalize(camera.eulerAngles.y, forMinimalRotationTo: yaw)
angle = normalizedY * (1 - relativeInRange) + yaw * relativeInRange
default:
angle = yaw
}
if alignment == .vertical{
self.rotation = SCNVector4(0, 1, 1, Float.pi)
} else {
self.rotation = SCNVector4(0, 1, 0, angle)
}
}
}
My goal is to have both the rotations in the control statement at the end performed on the object. If the else statement is removed, only that rotation is applied and the square remains horizontal. I have tried making both modifications to the transform directly using SCNMatrix4MakeRotation(_:_:_:_:) but this caused the square to spin endlessly and position itself incorrectly. I also tried adjusting the node's Pivot but this resulted in no visible changes to the object.
I am trying to put SCNCylinder node in the scene on the touch point. I always want to show the cylinder shape diameter facing towards camera. Its working fine for horizontal scene but it have a problem in vertical scene. In vertical scene I can see the cylinder sides but I want to show the full diameter facing towards the camera no matter whats the camera orientation is. I know there is some transformation needs to be applied depending on the camera transform but don't know how. I am not using plane detection its the simple node which is directly added to the scene.
Vertical Image:
Horizontal Image:
The code to insert the node is as follows,
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else {
return
}
let result = sceneView.hitTest(touch.location(in: sceneView), types: [ARHitTestResult.ResultType.featurePoint])
guard let hitResult = result.last else {
print("returning because couldn't find the touch point")
return
}
let hitTransform = SCNMatrix4(hitResult.worldTransform)
let position = SCNVector3Make(hitTransform.m41, hitTransform.m42, hitTransform.m43)
let ballShape = SCNCylinder(radius: 0.02, height: 0.01)
let ballNode = SCNNode(geometry: ballShape)
ballNode.position = position
sceneView.scene.rootNode.addChildNode(ballNode)
}
Any help would be appreciated.
I'm not certain this is the right way to handle what you need but here is something which may help you.
I think CoreMotion could be useful to help you determine if the device is at a horizontal or vertical angle.
This class has a property called attitude, which describes the rotation of our device in terms of roll, pitch, and yaw. If we are holding our phone in portrait orientation, the roll describes the angle of rotation about the axis that runs through the top and bottom of the phone. The pitch describes the angle of rotation about the axis that runs through the sides of your phone (where the volume buttons are). And finally, the yaw describes the angle of rotation about the axis that runs through the front and back of your phone. With these three values, we can determine how the user is holding their phone in reference to what would be level ground (Stephan Baker).
Begin by importing CoreMotion:
import CoreMotion
Then create the following variables:
let deviceMotionDetector = CMMotionManager()
var currentAngle: Double!
We will then create a function which will check the angle of our device like so:
/// Detects The Angle Of The Device
func detectDeviceAngle(){
if deviceMotionDetector.isDeviceMotionAvailable == true {
deviceMotionDetector.deviceMotionUpdateInterval = 0.1;
let queue = OperationQueue()
deviceMotionDetector.startDeviceMotionUpdates(to: queue, withHandler: { (motion, error) -> Void in
if let attitude = motion?.attitude {
DispatchQueue.main.async {
let pitch = attitude.pitch * 180.0/Double.pi
self.currentAngle = pitch
print(pitch)
}
}
})
}
else {
print("Device Motion Unavailable");
}
}
This only needs to be called once for example in viewDidLoad:
detectDeviceAngle()
In your touchesBegan method you can add this to the end:
//1. If We Are Holding The Device Above 60 Degress Change The Node
if currentAngle > 60 {
//2a. Get The X, Y, Z Values Of The Desired Rotation
let rotation = SCNVector3(1, 0, 0)
let vector3x = rotation.x
let vector3y = rotation.y
let vector3z = rotation.z
let degreesToRotate:Float = 90
//2b. Set The Position & Rotation Of The Object
sphereNode.rotation = SCNVector4Make(vector3x, vector3y, vector3z, degreesToRotate * 180 / .pi)
}else{
}
I am sure there are better ways to achieve what you need (and I would be very interested in hearing them too), but I hope it will get you started.
Here is the result:
i am following this tutorial (i think it is written in JavaScript): 3D Terrain
Im trying it in Swift with SpriteKit but have a problem with the rotation at the x-axis.
So this is what it looks like in the tutorial:
And this is where i am now:
I have created this grid with following Code:
var shapePoints = [CGPoint]()
for i in 0...zeilen+1{
for j in 0...spalten{
shapePoints.append(CGPoint(x: zellenGroesse*j, y: zellenGroesse*i))
shapePoints.append(CGPoint(x: zellenGroesse*j, y: zellenGroesse*(i+1)))
}
}
let fertigeShape = SKShapeNode(points: &shapePoints, count: shapePoints.count)
self.addChild(fertigeShape)
Now i would like to rotate it by some degrees, but i can only rotate it at the z-axis, not the x-axis.
Is there a way in SpriteKit to rotate the node at the x-axis, too?
Thanks and best regards
You can since iOS11, with what's called an SKTransformNode. When you want to 3D perspective tilt a node, add it as a child to a SKTransformNode, and then set it's xRotation or yRotation, which is a value in radians.
let t = SKTransformNode()
t.addChild(someNode)
scene.addChild(t)
t.xRotation = CGFloat.pi // tilts someNode by 180 degrees
You can also animate the change, like this.
let degrees: CGFloat = 20
let radians = CGFloat.pi / 180 * degrees
let duration = 2.0
let tilt = SKAction.customAction(withDuration: duration) { (node, elapsed) in
let percent = elapsed / CGFloat(duration)
t.xRotation = percent * radians
}
tilt.timingMode = .easeOut
t.run(tilt)
No, it isn't possible, SpriteKit is 2d. You should look at SceneKit for 3d. https://developer.apple.com/reference/scenekit
Hopefully you found a solution, but now Apple has released SKTransformNode - you can put your node(s) inside an SKTransformNode and rotate them around X and Y in addition to the standard Z
I am really bad at explaining these situations so bear with me.
What I want is when the user taps the white annotation, that point will scroll to the center (along with the globe)
I would also like to be able to do this programmatically, scrolling to a point when i provide x/y coords for the globe
I am using the following function to calculate the SCNVector3 based on x/y coordinates
func positionForCoordinates(coordinates: CGPoint, radius: CGFloat) -> SCNVector3 {
let s = coordinates.x
let t = coordinates.y
let r = radius
let x = r * cos(s) * sin(t)
let y = r * sin(s) * sin(t)
let z = r * cos(t)
return SCNVector3(x: Float(x), y: Float(y), z: Float(z))
}
its the math that really is eluding me.
Let's assume according to your problem, we knew the following
coordinate (latitude/longitude) of annotation that you placed it on the globe, and you want to simulate rotating a globe model to center on it
a camera is facing at the center of the screen
we will use a camera to rotate around globe model instead of rotating the globe itself
We can take advantage of animatable properties of SCNNode and SCNCamera so this will make it easier for us to do animation and not to manually lerp values in render loop.
First - Set up camera
func setupCamera(scene: SCNScene) {
cameraOrbit = SCNNode()
cameraNode = SCNNode()
camera = SCNCamera()
// camera stuff
camera.usesOrthographicProjection = true
camera.orthographicScale = 10
camera.zNear = 1
camera.zFar = 100
// initially position is far away as we will animate moving into the globe
cameraNode.position = SCNVector3(x: 0, y: 0, z: 70)
cameraNode.camera = camera
cameraOrbit = SCNNode()
cameraOrbit.addChildNode(cameraNode)
scene.rootNode.addChildNode(cameraOrbit)
}
Camera node is set with SCNCamera instance via its camera property, and is wrapped inside another node as we will use to manipulate its rotation.
I left code for defining those variables in the class for brevity.
Second - Implement conversion method
We need a method that convert map coordinate (latitude/longitude) to rotation angles for us to plug it into SCNNode.eulerAngles in order to rotate our camera around the globe model.
/**
Get rotation angle of sphere along x and y direction from input map coordinate to show such location at the center of view.
- Parameter from: Map coordinate to get rotation angle for sphere
- Returns: Tuple of rotation angle in form (x:, y:)
*/
func rotationXY(from coordinate: CLLocationCoordinate2D) -> (x: Double, y: Double) {
// convert map coordiante to texture coordinate
let v = 0.5 - (coordinate.latitude / 180.0)
let u = (coordinate.longitude / 360.0) + 0.5
// convert texture coordinate to rotation angles
let angleX = (u-0.5) * 2 * Double.pi
let angleY = (0.5-v) * -Double.pi
return (x: angleX, y: angleY)
}
From the code, we need to convert from map coordinate to texture coordinate in which when you place a texture ie. diffuse texture onto a sphere to render normally without any modification of rotation angle of globe node, and camera's position is placed along z-axis (ie. x=0, y=0, z=N), the center of such texture will be shown at the camera. So in the equation, we take into account 0.5 to accommodate on this.
After that we convert results into angles (radians). Along x direction, we could rotate sphere for 360 degrees to wrap around it fully. For y direction, it takes 180 degrees to wrap around it.
Please note I didn't get rid of 0.5 for both case as to make it as-is in each conversion, and for clearer to see along with explanation. You can simplify it by removing from the code.
Third - Animate
As a plus, I will include zoom level as well.
Let's assume that you allow zooming in level of 0.0 to 10.0. We use orthographicScale property of SCNNode to simulate zooming in for orthographic type of camera we've set up above. In contrast, orthographicScale is inverse of zoom level in normal understanding. So at zoom level of 10.0, orthographicScale will be 0.0 in order to achieve zoom-in effect.
/**
Rotate camera around globe to specified coordinate.
- Parameter to: Location in coordinate (latitude/longitude)
- Parameter zoomLevel: Zoom level in range 0.0 - 10.0.
- Parameter duration: Duration for rotation
- Parameter completion: Delegate when rotation completes to notify back to user
*/
func flyGlobeTo(to location: CLLocationCoordinate2D, zoomLevel: Double, duration: Double, completion: (()->Void)?=nil) {
// make a call to our conversion method
let rotation = self.rotationXY(from: location)
SCNTransaction.begin()
SCNTransaction.animationDuration = duration
SCNTransaction.completionBlock = {
completion?()
}
self.cameraOrbit.eulerAngles.x = Float(rotation.y)
self.cameraOrbit.eulerAngles.y = Float(rotation.x)
self.camera.orthographicScale = 10.0 - zoomLevel // calculate value from inverse from orthographicScale
SCNTransaction.commit()
}
And that's it. Whenever you need to fly to a target coordinate on the map, just use flyGlobeTo() method. Make sure you call it in main thread. Animation is done via SCNTransaction and not through UIView.animate, obviously it's different technology but I note it here too as first time I did use the latter myself.