Rotate SKSpriteNode to Another SKSpriteNode - swift

I am trying to rotate one sprite node to another. It rotates but not well. How can I fix it?
I have tried:
let blaster = self.childNode(withName: blaster)
let currentBlasterPosition = blaster!.position
let angle = atan2(currentBlasterPosition.y - cubes[0].position.y, currentBlasterPosition.x - cubes[0].position.x)
let rotateAction = SKAction.rotate(toAngle: angle + 90, duration: 0.0)
blaster!.run(SKAction.sequence([rotateAction]))
The SKSpriteNode is rotating for about -30 to 30 degrees from the point it should be (depending on its position).

Instead of dealing with calculating the angles yourself, use SKConstraint for that task.
Assuming your cubes is an Array of SKNodes:
let constraint = SKConstraint.orient(to: cubes[0], offset: SKRange(constantValue:0))
blaster!.constraints = [constraint]
You will have to do this just once, instead of every frame. The constraint is automatically applied every frame.
To remove it, set the blaster's constraints back to nil:
blaster!.constraints = nil

Related

How to get the SCNVector3 position of the camera in relation to it's direction ARKit Swift

I am trying to attach an object in front of the camera, but the issue is that it is always in relation to the initial camera direction. How can I adjust/get the SCNVector3 position to place the object in front, even if the direction of the camera is up or down?
This is how I do it now:
let ballShape = SCNSphere(radius: 0.03)
let ballNode = SCNNode(geometry: ballShape)
let viewPosition = sceneView.pointOfView!.position
ballNode.position = SCNVector3Make(viewPosition.x, viewPosition.y, viewPosition.z - 0.4)
sceneView.scene.rootNode.addChildNode(ballNode)
Edited to better answer the question now that it's clarified in a comment
New Answer:
You are using only the position of the camera, so if the camera is rotated, it doesn't affect the ball.
What you can do is get the transform matrix of the ball and multiply it by the transform matrix of the camera, that way the ball position will be relative to the full transformation of the camera, including rotation.
e.g.
let ballShape = SCNSphere(radius: 0.03)
let ballNode = SCNNode(geometry: ballShape)
ballNode.position = SCNVector3Make(0.0, 0.0, -0.4)
let ballMatrix = ballNode.transform
let cameraMatrix = sceneView.pointOfView!.transform
let newBallMatrix = SCNMatrix4Mult(ballMatrix, cameraMatrix)
ballNode.transform = newBallMatrix
sceneView.scene.rootNode.addChildNode(ballNode)
Or if you only want the SCNVector3 position, to answer exactly to your question (this way the ball will not rotate):
...
let newBallMatrix = SCNMatrix4Mult(ballMatrix, cameraMatrix)
let newBallPosition = SCNVector3Make(newBallMatrix.m41, newBallMatrix.m42, newBallMatrix.m43)
ballNode.position = newBallPosition
sceneView.scene.rootNode.addChildNode(ballNode)
Old Answer:
You are using only the position of the camera, so when the camera rotates, it doesn't affect the ball.
SceneKit uses a hierarchy of nodes, so when a node is "child" of another node, it follows the position, rotation and scale of its "parent". The proper way of attaching an object to another object, in this case the camera, is to make it "child" of the camera.
Then, when you set the position, rotation or any other aspect of the transform of the "child" node, you are setting it relative to its parent. So if you set the position to SCNVector3Make(0.0, 0.0, -0.4), it's translated -0.4 units in Z on top of its "parent" translation.
So to make what you want, it should be:
let ballShape = SCNSphere(radius: 0.03)
let ballNode = SCNNode(geometry: ballShape)
ballNode.position = SCNVector3Make(0.0, 0.0, -0.4)
let cameraNode = sceneView.pointOfView
cameraNode?.addChildNode(ballNode)
This way, when the camera rotates, the ball follows exactly its rotation, but separated -0.4 units from the camera.

Aligning ARFaceAnchor with SpriteKit overlay

I'm trying to calculate SpriteKit overlay content position (not just overlaying visual content) over specific geometry points ARFaceGeometry/ARFaceAnchor.
I'm using SCNSceneRenderer.projectPoint from the calculated world coordinate, but the result is y inverted and not aligned to the camera image:
let vertex4 = vector_float4(0, 0, 0, 1)
let modelMatrix = faceAnchor.transform
let world_vertex4 = simd_mul(modelMatrix, vertex4)
let pt3 = SCNVector3(x: Float(world_vertex4.x),
y: Float(world_vertex4.y),
z: Float(world_vertex4.z))
let sprite_pt = renderer.projectPoint(pt3)
// To visualize sprite_pt
let dot = SKSpriteNode(imageNamed: "dot")
dot.size = CGSize(width: 7, height: 7)
dot.position = CGPoint(x: CGFloat(sprite_pt.x),
y: CGFloat(sprite_pt.y))
overlayScene.addChild(dot)
In my experience, the screen coordinates given by ARKit's projectPoint function are directly usable when drawing to, for example, a CALayer. This means they follow iOS coordinates as described here, where the origin is in the upper left and y is inverted.
SpriteKit has its own coordinate system:
The unit coordinate system places the origin at the bottom left corner of the frame and (1,1) at the top right corner of the frame. A sprite’s anchor point defaults to (0.5,0.5), which corresponds to the center of the frame.
Finally, SKNodes are placed in an SKScene which has its origin on the bottom left. You should ensure that your SKScene is the same size as your actual view, or else the origin may not be at the bottom left of the view and thus your positioning of the node from view coordinates my be incorrect. The answer to this question may help, in particular checking the AspectFit or AspectFill of your view to ensure your scene is being scaled down.
The Scene's origin is in the bottom left and depending on your scene size and scaling it may be off screen. This is where 0,0 is. So every child you add will start there and work its way right and up based on position. A SKSpriteNode has its origin in the center.
So the two basic steps to convert from view coordinates and SpriteKit coordinates would be 1) inverting the y-axis so your origin is in the bottom left, and 2) ensuring that your SKScene frame matches your view frame.
I can test this out more fully in a bit and edit if there are any issues
Found the transformation that works using camera.projectPoint instead of the renderer.projectPoint.
To scale the points correctly on the spritekit: set scaleMode=.aspectFill
I updated https://github.com/AnsonT/ARFaceSpriteKitMapping to demo this.
guard let faceAnchor = anchor as? ARFaceAnchor,
let camera = sceneView.session.currentFrame?.camera,
let sie = overlayScene?.size
else { return }
let modelMatrix = faceAnchor.transform
let vertices = faceAnchor.geometry.vertices
for vertex in vertices {
let vertex4 = vector_float4(vertex.x, vertex.y, vertex.z, 1)
let world_vertex4 = simd_mul(modelMatrix, vertex4)
let world_vector3 = simd_float3(x: world_vertex4.x, y: world_vertex4.y, z: world_vertex4.z)
let pt = camera.projectPoint(world_vector3, orientation: .portrait, viewportSize: size)
let dot = SKSpriteNode(imageNamed: "dot")
dot.size = CGSize(width: 7, height: 7)
dot.position = CGPoint(x: CGFloat(pt.x), y: size.height - CGFloat(pt.y))
overlayScene?.addChild(dot)
}

SceneKit applyForce in ARKit [duplicate]

I have the following code that creates a SCNBox and shoots it on the screen. This works but as soon as I turn the phone in any other direction then the force impulse does not get updated and it always shoots the box in the same old position.
Here is the code:
#objc func tapped(recognizer :UIGestureRecognizer) {
guard let currentFrame = self.sceneView.session.currentFrame else {
return
}
/
let box = SCNBox(width: 0.2, height: 0.2, length: 0.2, chamferRadius: 0)
let material = SCNMaterial()
material.diffuse.contents = UIColor.red
material.lightingModel = .constant
var translation = matrix_identity_float4x4
translation.columns.3.z = -0.01
let node = SCNNode()
node.geometry = box
node.geometry?.materials = [material]
print(currentFrame.camera.transform)
node.physicsBody = SCNPhysicsBody(type: .dynamic, shape: nil)
node.simdTransform = matrix_multiply(currentFrame.camera.transform, translation)
node.physicsBody?.applyForce(SCNVector3(0,2,-10), asImpulse: true)
self.sceneView.scene.rootNode.addChildNode(node)
}
Line 26 is where I apply the force but it does not take into account the user's current phone orientation. How can I fix that?
On line 26 you're passing a constant vector to applyForce. That method takes a vector in world space, so passing a constant vector means you're always applying a force in the same direction — if you want a direction that's based on the direction the camera or something else is pointing, you'll need to calculate a vector based on that direction.
The (new) SCNNode property worldFront might prove helpful here — it gives you the direction a node is pointing, automatically converted to world space, so it's useful with physics methods. (Though you might want to scale it.)

How to apply these transformations to a line in SpriteKit

I'm not sure what tools I should use for what I'm trying to do since I'm only really familiar with SKSpriteNodes and a little bit with SKShapeNodes.
My mission is as follows:
Add a line to the scene, SKShapeNode?
Rotate the line along it's bottom point (beginning point?) by some angle. Imagine a clock hand for this, rotating around the bottom point
Find the new point (x,y coord) of the top point (end point?) after the line has been translated
Does anyone know how I can accomplish this? I'm currently using an SKShapeNode for my line and rotating it with .zRotation but I can't seem to accomplish my goal. There doesn't seem to be an achorPoint property for SKShapeNodes, so I can't change the point of rotation. Also I'm clueless on how to find the position of the end point of my line AFTER it has been rotated, I created it as follows:
let linePath = CGMutablePath()
linePath.move(to: begin)
linePath.addLine(to: end)
let line = SKShapeNode()
line.path = linePath
line.strokeColor = UIColor.black
line.lineWidth = 5
SceneCoordinator.shared.gameScene.addChild(line)
I'm rotating using:
public func rotate(angle: Double) {
var transform = CGAffineTransform(rotationAngle: CGFloat(angle))
line.path = linePath.mutableCopy(using: &transform)
}
SKShapeNode can be pretty expensive if you notice your FPS dropping. Also, you can easily turn a shape into a sprite to get .anchorPoint, but be warned that anchorpoint's behavior is not always as expected and you may have bugs later on (especially with physics):
func shapeToSprite(_ shape: SKShapeNode) -> SKSpriteNode {
let sprite = SKSpriteNode(texture: SKView().texture(from: shape))
sprite.physicsBody = shape.physicsBody // Or create a new PB from alpha mask (may be slower, IDK)
shape.physicsBody = nil
return sprite
}
override func didMove(to view: SKView) {
let shape = SKShapeNode(circleOfRadius: 60)
let sprite = shapeToSprite(shape)
sprite.anchorPoint = CGPoint()
addChild(sprite)
}
Otherwise, you are going to have to either 1), redraw the line with the correct rotation, 2), rotate the shape then reposition it at a new location...
Both are going to be MATH :[ so hopefully the anchorppoint works for you

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.