Swift SpriteKit Rotate Node at x-axis - swift

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

Related

how to plot forward/backward tilt on y axis?

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.

Swift: Calculate how much to rotate an SKSpriteNode to have it pointing at a CGPoint

I'm making a SpriteKit game in Playgrounds based on shooting space rocks. In the game, there is a gun, which is a SKSpriteNode and I want it to turn to the point that I pass into a function (CGPoint). What I would like to know is how would I calculate how much to turn the gun to face the given point? Thanks in advance!
Long ago, after a lot of mind tweaking math and a sleepless night, I came up with this function:
func gunAngle(gunPosition: CGPoint, targetPosition: CGPoint) -> CGFloat {
let deltaX = Float(targetPosition.x - gunPosition.x)
let deltaY = Float(targetPosition.y - gunPosition.y)
let pi = CGFloat(M_PI)
let angle = CGFloat(atan2f(deltaY, deltaX))
var newAngle = angle
if angle < (-pi / 2) {
newAngle += 2 * pi
}
return newAngle - pi/2 // Subtracting 90 degrees out of the resulting angle, as in SpriteKit 0 degrees faces left, unless you rotate your gun in the sprite accordingly
}
I realize this may not be the best method but it works for me. Some math gurus could probably come up with something really brilliant here. I'm not yet any of those.
Thanks to Ray Wenderlich, on his website there is a tutorial on that topic that helped me a lot in putting the foundation of that math.
Maybe,something like
func aim(node:SKSpriteNode, point:CGPoint){
let angle = atan2(point.y - node.position.y, point.x - node.position.x)
node.run(SKAction.rotate(byAngle: angle, duration: 0.5))
}
An alternative solution (this may not be applicable to you), is to create an SKNode (called target perhaps) and then to set up an SKContraint so that your gun always faces the target. You can then move target when required to wherever you want the gun to face and the gun will turn accordingly.
let gun = SKNSpritenode...
let target = SKNode...
let orientRange = SKRange(lowerLimit: 0.0, upperLimit: 0.0)
let orientConstraint = SKConstraint.orientToNode(target, offset: orientRange)
gun.constraints = [orientConstraint]

I can't understand SpriteKit's rotation system

I'm trying to rotate sprites with SKAction.rotateToAngle. For example:
rotate = SKAction.rotateToAngle(desiredAngle, duration: 1.0)
sprite.runAction(rotate)
But it didn't work as expected. The sprites rotates in a different way. By trying and trying I've found out that this is the rotation system adopted by SpriteKit:
Now I don't know why they decided to adopt this coordinate system but it's pretty counter-intuitive. How can I convert between this system and the normal one?
And by the way how can I rotate the sprites clockwise?
From the docs:
An angle of 0 radians specifies the positive x axis. A positive angle
is in the counterclockwise direction.
To rotate an object by 90 degrees counterclockwise, do this:
let action = SKAction.rotateToAngle(CGFloat(M_PI/2.0), duration: 5.0)
To rotate an object by 90 degrees clockwise, do this:
let action = SKAction.rotateToAngle(CGFloat(-M_PI/2.0), duration: 5.0)
Or instead of using negatives (-90 degrees), you can specify the angle as a positive value(270 degrees), and use rotateToAngle(_:duration:shortestUnitArc:) method which rotates the object in direction which results in the smallest rotation.
let action = SKAction.rotateToAngle(CGFloat(2 * M_PI - M_PI/2.0), duration: 5, shortestUnitArc: true)
Swift 5.2
To rotate an object by 180 degree clockwise:
let spin = SKAction.rotate(toAngle: -.pi, duration: 10)
To rotate an object by 180 degree counterclockwise:
let spin = SKAction.rotate(toAngle: .pi, duration: 10)
To 90 degrees, divide by 2:
let spin = SKAction.rotate(toAngle: -.pi/2, duration: 10)
let spin = SKAction.rotate(toAngle: .pi/2, duration: 10)

Swift - Use motionManager Accelerometer, Gyroscope and Magnetometer to move SceneKit node like the iPhone’s movements

I have a SceneKit SCNBox Cube that should follow all the movements of my iphone. So If my iphone rotate 90 or 360 degrees sideways should the cube also rotate 90 or 360 degrees and if I spin my phone forward the cube should spin like the phone.
I have tried with startDeviceMotionUpdatesUsingReferenceFrame, but it doesn't match the movements of my phone :(
motionManager.deviceMotionUpdateInterval = 1.0 / 60.0
motionManager.startDeviceMotionUpdatesUsingReferenceFrame(
CMAttitudeReferenceFrame.XArbitraryZVertical,
toQueue: NSOperationQueue.mainQueue(),
withHandler: { (motion: CMDeviceMotion!, error: NSError!) -> Void in
let currentAttitude = motion.attitude
let roll = Float(currentAttitude.roll)
let pitch = Float(currentAttitude.pitch)
let yaw = Float(currentAttitude.yaw)
self.arrRoll.append(roll)
self.arrPitch.append(pitch)
self.arrYaw.append(yaw)
cubeNode.eulerAngles = SCNVector3Make(roll, 0.0, 0.0)
cubeNode.eulerAngles = SCNVector3Make(0.0, 0.0, pitch)
cubeNode.eulerAngles = SCNVector3Make(0.0, yaw, 0.0)
})
Your code snippet is a little out of context, but one thing that strikes me off the bat is that you are (most likely unintentionally) overwriting the eulerAngles property of your node.
Why do you have 3 separate assignments (only the last of which will have an effect), instead of:
cubeNode.eulerAngles = SCNVector3Make(roll, yaw, pitch)
?

Rotate an object so that a tapped point faces camera

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.