How to rotate a SCNSphere using a pan gesture recognizer - ios

I created an SCNSphere so now it looks like a planet kind of. This is exactly what I want. My next goal is to allow users to rotate the sphere using a pan gesture recognizer. They are allowed to rotate it around the X or Y axis. I was just wondering how I can do that. This is what I have so far.
origin = sceneView.frame.origin
node.geometry = SCNSphere(radius: 1)
node.geometry?.firstMaterial?.diffuse.contents = UIImage(named: "world.jpg")
let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(CategoryViewController.panGlobe(sender:)))
sceneView.addGestureRecognizer(panGestureRecognizer)
func panGlobe(sender: UIPanGestureRecognizer) {
// What should i put inside this method to allow them to rotate the sphere/ball
}

We have a ViewController that contains a node sphereNode that contains our sphere.
To rotate the sphere we could use a UIPanGestureRecognizer.
Since the recognizer reports the total distance our finger has traveled on the screen we cache the last point that was reported to us.
var previousPanPoint: CGPoint?
let pixelToAngleConstant: Float = .pi / 180
func handlePan(_ newPoint: CGPoint) {
if let previousPoint = previousPanPoint {
let dx = Float(newPoint.x - previousPoint.x)
let dy = Float(newPoint.y - previousPoint.y)
rotateUp(by: dy * pixelToAngleConstant)
rotateRight(by: dx * pixelToAngleConstant)
}
previousPanPoint = newPoint
}
We calculate dx and dy with how much pixel our finger has traveled in each direction since we last called the recognizer.
With the pixelToAngleConstant we convert our pixel value in an angle (in randians) to rotate our sphere. Use a bigger constant for a faster rotation.
The gesture recognizer returns a state that we can use to determine if the gesture has started, ended, or the finger has been moved.
When the gesture starts we save the fingers location in previousPanPoint.
When our finger moves we call the function above.
When the gesture is ended or canceled we clear our previousPanPoint.
#objc func handleGesture(_ gestureRecognizer: UIPanGestureRecognizer) {
switch gestureRecognizer.state {
case .began:
previousPanPoint = gestureRecognizer.location(in: view)
case .changed:
handlePan(gestureRecognizer.location(in: view))
default:
previousPanPoint = nil
}
}
How do we rotate our sphere?
The functions rotateUp and rotateRight just call our more general function, rotate(by: around:) which accepts not only the angle but also the axis to rotate around.
rotateUp rotates around the x-axis, rotateRight around the y-axis.
func rotateUp(by angle: Float) {
let axis = SCNVector3(1, 0, 0) // x-axis
rotate(by: angle, around: axis)
}
func rotateRight(by angle: Float) {
let axis = SCNVector3(0, 1, 0) // y-axis
rotate(by: angle, around: axis)
}
The rotate(by:around:) is in this case relative simple because we assume that the node is not translated/ we want to rotate around the origin of the nodes local coordinate system.
Everything is a little more complicated when we look at a general case but this answer is only a small starting point.
func rotate(by angle: Float, around axis: SCNVector3) {
let transform = SCNMatrix4MakeRotation(angle, axis.x, axis.y, axis.z)
sphereNode.transform = SCNMatrix4Mult(sphereNode.transform, transform)
}
We create a rotation matrix from the angle and the axis and multiply the old transform of our sphere with the calculated one to get the new transform.
This is the little demo I created:
This approach has two major downsides.
It only rotates around the nodes coordinate origin and only works properly if the node's position is SCNVector3Zero
It does takes neither the speed of the gesture into account nor does the sphere continue to rotate when the gesture stops.
An effect similar to a table view where you can flip your finger and the table view scrolls fast and then slows down can't be easily achieved with this approach.
One solution would be to use the physics system for that.

Below is what I tried, not sure whether it is accurate with respect to angles but...it sufficed most of my needs....
#objc func handleGesture(_ gestureRecognizer: UIPanGestureRecognizer) {
let translation = gestureRecognizer.translation(in: gestureRecognizer.view!)
let x = Float(translation.x)
let y = Float(-translation.y)
let anglePan = (sqrt(pow(x,2)+pow(y,2)))*(Float)(Double.pi)/180.0
var rotationVector = SCNVector4()
rotationVector.x = x
rotationVector.y = y
rotationVector.z = 0.0
rotationVector.w = anglePan
self.earthNode.rotation = rotationVector
}
Sample Github-EarthRotate

Related

Converting Radians to Degrees using UIRotationGestureRecognizer?

I am using UIRotationGestureRecognizer to rotate an image. I would like to convert the angle of rotation from Radians to Degrees becuase I can think better in degrees. I found a solution on stack and other sources but for some reason the solution does not seem to work
For example, when I rotate the image counter clockwise about 45 degrees am getting a degree value of approximately -0.15 from the formula???
#objc func handleImageRotation(sender:
UIRotationGestureRecognizer){
guard sender.view != nil else{return}
if sender.state == .began || sender.state == .changed {
// rotation enacted
imageView.transform = imageView.transform.rotated(by: sender.rotation)
rotationAngleRads = sender.rotation
rotationAngleDegs = rad2Deg(radians: rotationAngleRads)
print("Degrees: \(rotationAngleDegs!)")
sender.rotation = 0
}
}
// Convert Radians to Degress
private func rad2Deg(radians: CGFloat) -> Double{
let angle = Double(radians) * (180 / .pi)
return Double(angle)
}
Your main issue is that you are resetting the gesture's rotation property. You should not do that. From the documentation:
The rotation value is a single value that varies over time. It is not the delta value from the last time that the rotation was reported. Apply the rotation value to the state of the view when the gesture is first recognized—do not concatenate the value each time the handler is called.
So remove the sender.rotation = 0 line.
As a result of that, you need to fix how you apply the transform to the image view.
Replace:
imageView.transform = imageView.transform.rotated(by: sender.rotation)
with:
imageView.transform = CGAffineTransform(rotatationAngle: sender.rotation)
That applies the full rotation instead of trying to increment the current rotation.

ARKit - initially placing object in ARSKView at a certain heading/angle from camera

I'm creating my anchor and 2d node in ARSKView like so:
func displayToken(distance: Float) {
print("token dropped at: \(distance)")
guard let sceneView = self.view as? ARSKView else {
return
}
// Create anchor using the camera's current position
if let currentFrame = sceneView.session.currentFrame {
removeToken()
// Create a transform with a translation of x meters in front of the camera
var translation = matrix_identity_float4x4
translation.columns.3.z = -distance
let transform = simd_mul(currentFrame.camera.transform, translation)
// Add a new anchor to the session
let anchor = ARAnchor(transform: transform)
sceneView.session.add(anchor: anchor)
}
}
func view(_ view: ARSKView, nodeFor anchor: ARAnchor) -> SKNode? {
// Create and configure a node for the anchor added to the view's session.
if let image = tokenImage {
let texture = SKTexture(image: image)
let tokenImageNode = SKSpriteNode(texture: texture)
tokenImageNode.name = "token"
return tokenImageNode
} else {
return nil
}
}
This places it exactly in front of the camera at a given distance (z). What I want to also do is take a latitude/longitude for an object, calculate an angle or heading in degrees, and initially drop the anchor/node at this angle from the camera. I'm currently getting the heading by using the GMSGeometryHeading method, which takes users current location, and the target location to determine the heading. So when dropping the anchor, I want to put it in the right direction towards the target location's lat/lon. How can I achieve this with SpriteKit/ARKit?
Can you clarify your question a bit please?
Perhaps the following lines can help you as example. There the cameraNode moves using a basic geometry-formula, moving obliquely depending on the angle (in Euler) in both x and y coordinates
var angleEuler = 0.1
let rotateX = Double(cameraNode.presentation.position.x) - sin(degrees(radians: Double(angleEuler)))*10
let rotateZ = Double(cameraNode.presentation.position.z) - abs(cos(degrees(radians: Double(angleEuler))))*10
cameraNode.position = SCNVector3(x:Float(rotateX), y:0, z:Float(rotateX))
If you want an object fall in front of the camera, and the length of the fall depend on a degree just calculate the value of "a" in the geometry-formula "Tan A = a/b" and update the node's "presentation.position.y"
I hope this helps

rotate object in ARkit

I've got a object in my scene that when I move my finger across the screen, I want the object to rotate in that direction. It's a cup on screen, and sliding my finger around on the screen should rotate the cube about the center point, but not move the cup's position. It should only rotate as long as they are actively swiping
Rotating an SCNNode is a fairly simple task.
You should begin by creating a variable to store the rotationAngle around the YAxis or any other that you wish to perform the rotation on e.g:
var currentAngleY: Float = 0.0
You will also need to have some way to detect the node that you want to rotate, which for this example we will call currentNode e.g.
var currentNode: SCNNode!
In this example I will just rotate around the YAxis.
If you want to use a UIPanGestureRecognizer you can do so like this:
/// Rotates An Object On It's YAxis
///
/// - Parameter gesture: UIPanGestureRecognizer
#objc func rotateObject(_ gesture: UIPanGestureRecognizer) {
guard let nodeToRotate = currentNode else { return }
let translation = gesture.translation(in: gesture.view!)
var newAngleY = (Float)(translation.x)*(Float)(Double.pi)/180.0
newAngleY += currentAngleY
nodeToRotate.eulerAngles.y = newAngleY
if(gesture.state == .ended) { currentAngleY = newAngleY }
print(nodeToRotate.eulerAngles)
}
Alternatively if you want to use a UIRotationGesture you can do something like this:
/// Rotates An SCNNode Around It's YAxis
///
/// - Parameter gesture: UIRotationGestureRecognizer
#objc func rotateNode(_ gesture: UIRotationGestureRecognizer){
//1. Get The Current Rotation From The Gesture
let rotation = Float(gesture.rotation)
//2. If The Gesture State Has Changed Set The Nodes EulerAngles.y
if gesture.state == .changed{
currentNode.eulerAngles.y = currentAngleY + rotation
}
//3. If The Gesture Has Ended Store The Last Angle Of The Cube
if(gesture.state == .ended) {
currentAngleY = currentNode.eulerAngles.y
}
}
Hope it helps...

Rotate sprite node with moving touch

I am new to Swift and SpriteKit and am learning to understand the control in the game "Fish & Trip". The sprite node is always at the center of the view and it will rotate according to moving your touch, no matter where you touch and move (hold) it will rotate correspondingly.
The difficulty here is that it is different from the Pan Gesture and simple touch location as I noted in the picture 1 and 2.
For the 1st pic, the touch location is processed by atan2f and then sent to SKAction.rotate and it is done, I can make this working.
For the 2nd pic, I can get this by setup a UIPanGestureRecognizer and it works, but you can only rotate the node when you move your finger around the initial point (touchesBegan).
My question is for the 3rd pic, which is the same as the Fish & Trip game, you can touch anywhere on the screen and then move (hold) to anywhere and the node still rotate as you move, you don't have to move your finger around the initial point to let the node rotate and the rotation is smooth and accurate.
My code is as follow, it doesn't work very well and it is with some jittering, my question is how can I implement this in a better way? and How can I make the rotation smooth?
Is there a way to filter the previousLocation in the touchesMoved function? I always encountered jittering when I use this property, I think it reports too fast. I haven't had any issue when I used UIPanGestureRecoginzer and it is very smooth, so I guess I must did something wrong with the previousLocation.
func mtoRad(x: CGFloat, y: CGFloat) -> CGFloat {
let Radian3 = atan2f(Float(y), Float(x))
return CGFloat(Radian3)
}
func moveplayer(radian: CGFloat){
let rotateaction = SKAction.rotate(toAngle: radian, duration: 0.1, shortestUnitArc: true)
thePlayer.run(rotateaction)
}
var touchpoint = CGPoint.zero
var R2 : CGFloat? = 0.0
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
for t in touches{
let previousPointOfTouch = t.previousLocation(in: self)
touchpoint = t.location(in: self)
if touchpoint.x != previousPointOfTouch.x && touchpoint.y != previousPointOfTouch.y {
let delta_y = touchpoint.y - previousPointOfTouch.y
let delta_x = touchpoint.x - previousPointOfTouch.x
let R1 = mtoRad(x: delta_x, y: delta_y)
if R2! != R1 {
moveplayer(radiant: R1)
}
R2 = R1
}
}
}
This is not an answer (yet - hoping to post one/edit this into one later), but you can make your code a bit more 'Swifty' by changing the definition for movePlayer() from:
func moveplayer(radian: CGFloat)
to
rotatePlayerTo(angle targetAngle: CGFloat) {
let rotateaction = SKAction.rotate(toAngle: targetAngle, duration: 0.1, shortestUnitArc: true)
thePlayer.run(rotateaction)
}
then, to call it, instead of:
moveplayer(radiant: R1)
use
rotatePlayerTo(angle: R1)
which is more readable as it describes what you are doing better.
Also, your rotation to the new angle is constant at 0.1s - so if the player has to rotate further, it will rotate faster. it would be better to keep the rotational speed constant (in terms of radians per second). we can do this as follows:
Add the following property:
let playerRotationSpeed = CGFloat((2 *Double.pi) / 2.0) //Radian per second; 2 second for full rotation
change your moveShip to:
func rotatePlayerTo(angle targetAngle: CGFloat) {
let angleToRotateBy = abs(targetAngle - thePlayer.zRotation)
let rotationTime = TimeInterval(angleToRotateBy / shipRotationSpeed)
let rotateAction = SKAction.rotate(toAngle: targetAngle, duration: rotationTime , shortestUnitArc: true)
thePlayer.run(rotateAction)
}
this may help smooth the rotation too.

Rotating SKNode to point at touch contact point

Context: I'm using Xcode, Swift, and SpriteKit
I've created a separate class file "Ship.swift" and within it I've got:
let RAD_PER_SEC: CGFloat = 0.1
var direction = 0
//Updates object value for determining how much to rotate object
func spin {
}
Ideally I want to make an app sort of like the old game Astroids. For now, I just a ship that rotates in the center of the screen and points towards the touch-contact point.
If you want to rotate node towards the touch location you need to know the angle to rotate to. Because in Spritekit angles are measured in radians there are some conversions that have to be done:
let Pi = CGFloat(M_PI)
let DegreesToRadians = Pi / 180
After that you have to use atan2() to calculate the angle...Here is simple example which rotates sprite using touchesMoved method:
override func touchesMoved(touches: NSSet, withEvent event: UIEvent) {
let curTouch = touches.anyObject() as UITouch
let curPoint = curTouch.locationInNode(self)
let deltaX = self.yourNode.position.x - curPoint.x
let deltaY = self.yourNode.position.y - curPoint.y
let angle = atan2(deltaY, deltaX)
self.yourNode.zRotation = angle + 90 * DegreesToRadians
}
As of iOS8 Apple added SKConstraints which can be added to SKNodes. Here's the documentation: https://developer.apple.com/library/mac/documentation/SpriteKit/Reference/SKConstraint_Ref/index.html#//apple_ref/occ/cl/SKConstraint
In your case you'll want to create the constraint when the user touches the screen - SKConstraint.orientToPoint(touchLocation, offset: SKRange.rangeWithConstantValue(0))
And the add it to your sprite node using the constraints property.

Resources