Move camera in Scenekit with Swift - swift

I want to move the camera every frame automatic in the z-axis in Scenekit. I have write this code in Swift:
func renderer(_ renderer: SCNSceneRenderer,
updateAtTime time: TimeInterval) {
cameraNode.position.z = cameraNode.position.z + 2
}
This works only for 3 frames, after that the camera doesn't move anymore. Can someone give me the correct code so the camera moves automatic every frame?

According to SceneKit documentation:
SceneKit calls this method exactly once per frame, so long as the SCNView object (or other SCNSceneRenderer object) displaying the scene is not paused.
If nothing else in your view is moving, the scene is โ€˜pausedโ€™ and does not render. According to this question, in order to force the render every frame, the workaround should be:
sceneView.loops = true
However, updating the position every frame is not the best practice because FPS can change, which would affect the camera speed. But you can keep a consistent camera speed using SCNAction without using the render function:
let move = SCNAction.moveBy(x: 0, y: 0, z: 120, duration: 1.0)
let moveForever = SCNAction.repeatActionForever(move)
cameraNode.runAction(moveForever)
This will also make controlling your camera easier if it needs to stop, and it does not force the render if it is unnecessary.

Related

SpriteKit - change animation speed dynamically

I am new to the SpriteKit framework and cannot figure out how to do the following:
In the middle of the scene I have a sprite to which I apply an .animateWithTextures SKAction. Now I simply want to increase the speed of that animation, or decrease its duration for the same effect.
I made the SKAction a property of my GameScene class so I can access its properties from everywhere, but neither changing its speed nor its duration affects the animation.
I read several threads, the closest to my problem being this one:
How to change duration of executed SpriteKit action
which is from seven years ago, and none of the answers is working for me.
Here is basically what I do:
class GameScene: SKScene {
var thePlayer: SKSpriteNode = SKSpriteNode()
var playerAnimation: SKAction?
...
...
func setInitialAnimation() {
let walkAnimation = SKAction.repeatForever(SKAction(named: "PlayerWalk", duration: 1)!)
self.playerAnimation = walkAnimation
self.thePlayer.run(walkAnimation)
}
func changeAnimationDuration(to duration: CGFloat) {
self.playerAnimation?.duration = duration
}
}
The animation starts when the setInitialAnimation method is called, but changing the action's duration has no effect. The only thing that works so far is removing the old action from the player sprite and running a new one.
Should it not be possible to change the properties of a running SKAction or is that some kind of fire and forget mechanism?
Play around with the timePerFrame to get the correct animation speed.
var frames = [SKTexture]()
for i in 1...10 {
frames.append(SKTexture(imageNamed: "YourTextureAnimationFrame\(i)"))
}
let animate = SKAction.animate(with: frames, timePerFrame: 0.025)
//timePerFrame setting the speed of the animation.
node.run(animate)

Viewport? or Camera? of Field of View? changes on rotating iOS device

I have a 3d world using a SceneKit view in my app.
I need to support all iOS devices and orientations.
I do not understand 3d maths, matrices nor really understand cameras and stuff, yet I've built a 3d world, and I have a camera that works, and orbits with a Pan Gesture an object at the centre of the world.
My problem is that when I rotate the device from portrait to landscape the SceneKit view automatically changes it's perspective (of what I'm not sure - the camera? or the world itself?) and the object that looked like it was close, now goes way back into the distance.
What I want to happen is that regardless of rotation the 'viewport'? or camera? or world perspective? stays the same and the distance the camera is from the object doesn't "look" different i.e. the object visually stays at the same place.
I have not done any matrix manipulations or set up anything complicated.
I understand that the units of SceneKit are metres, so my object is in the correct size, and the camera is the right number of metres from it... it looks 'correct'
What I don't understand is why it all changes in landscape.
Yes, the SceneKit view is using AutoLayout constraints to hug the edges of the screen.
Here is my camera setup:
private func setupCamera() -> SCNNode {
let camera = SCNCamera()
camera.usesOrthographicProjection = false
camera.orthographicScale = 9
camera.zNear = 0.01
camera.zFar = 9500
camera.focalLength = 50
camera.focusDistance = 33
camera.fieldOfView = 100
let cameraNode = SCNNode()
cameraNode.position = SCNVector3(x:0, y:0, z: 500.0)
cameraNode.camera = camera
self.cameraNode = cameraNode
let cameraOrbit = SCNNode()
cameraOrbit.addChildNode(self.cameraNode!)
return cameraOrbit
}
The only way I've found to 'fix' this is to make the SceneKit view always be a square and sized so that it's always the largest of the devices width or height and keep it's centre in the screen's centre.
But this approach isn't working with other parts of the app, so instead I know I need to do this 'properly'.
So would someone please provide the code or tell me what I need to understand so that in the function that gets called when the device rotates i.e. override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) then I can correct this automatic change and keep the view's 'perspective' the same.
Thank you.
After 4 days of playing with constraints, manually moving the frame and all sorts of stuff, I have fixed it.
I constrained the scenekitview to the superview with 0 constants, so it's always the size of the screen.
Then ALL I had to do to solve this is ONE LINE... sigh.
I just had to halve the 'sensorHeight' and then it worked perfectly.
if UIDevice.current.orientation == .landscapeLeft ||
UIDevice.current.orientation == .landscapeRight
{
self.cameraNode?.camera?.sensorHeight = 12
}
else
{
self.cameraNode?.camera?.sensorHeight = 24
}
EDIT:
Because of the constraints to the screen size, the default Field of View meant that you have a fish-eye effect.
Dropping the FoV to almost half cured this and now objects rotate without distortions:
camera.fieldOfView = 50
From ios11 you can also tell scenekit which projection to use.
This solved it for me and is aspect ratio independent.
camera.projectionDirection = isPortrait ? .vertical : .horizontal

How to update the pointOfView in ARKit

I'm trying to build my first ARKit app. The purpose of the app is to shoot little blocks in the direction that the camera is facing. Right now, here is the code I have.
sceneView.scene.physicsWorld.gravity = SCNVector3(x: 0, y: 0, z: -9.8)
#IBAction func tapScreen() {
if let camera = self.sceneView.pointOfView {
let sphere = NodeGenerator.generateCubeInFrontOf(node: camera, physics: true)
self.sceneView.scene.rootNode.addChildNode(sphere)
var isSphereAdded = true
print("Added box to scene")
}
}
The gravity works fine, whenever I tap on the screen the block shoots out each and every time I tap. However, they all shoot to the same point, no matter which direction the camera is facing. I'm trying to understand how pointOfView works, would I need to re-render the whole scene? Something else that I can't quite think of? Thanks for any help!
Change this line from
self.sceneView.scene.rootNode.addChildNode(sphere)
to
self.sceneView.pointOfView?.addChildNode(sphere)

How do I programmatically move an ARAnchor?

I'm trying out the new ARKit to replace another similar solution I have. It's pretty great! But I can't seem to figure out how to move an ARAnchor programmatically. I want to slowly move the anchor to the left of the user.
Creating the anchor to be 2 meters in front of the user:
var translation = matrix_identity_float4x4
translation.columns.3.z = -2.0
let transform = simd_mul(currentFrame.camera.transform, translation)
let anchor = ARAnchor(transform: transform)
sceneView.session.add(anchor: anchor)
later, moving the object to the left/right of the user (x-axis)...
anchor.transform.columns.3.x = anchor.transform.columns.3.x + 0.1
repeated every 50 milliseconds (or whatever).
The above does not work because transform is a get-only property.
I need a way to change the position of an AR object in space relative to the user in a way that keeps the AR experience intact - meaning, if you move your device, the AR object will be moving but also won't be "stuck" to the camera like it's simply painted on, but moves like you would see a person move while you were walking by - they are moving and you are moving and it looks natural.
Please note the scope of this question relates only to how to move an object in space in relation to the user (ARAnchor), not in relation to a plane (ARPlaneAnchor) or to another detected surface (ARHitTestResult).
Thanks!
You don't need to move anchors. (hand wave) That's not the API you're looking for...
Adding ARAnchor objects to a session is effectively about "labeling" a point in real-world space so that you can refer to it later. The point (1,1,1) (for example) is always the point (1,1,1) โ€” you can't move it someplace else because then it's not the point (1,1,1) anymore.
To make a 2D analogy: anchors are reference points sort of like the bounds of a view. The system (or another piece of your code) tells the view where it's boundaries are, and the view draws its content relative to those boundaries. Anchors in AR give you reference points you can use for drawing content in 3D.
What you're asking is really about moving (and animating the movement of) virtual content between two points. And ARKit itself really isn't about displaying or animating virtual content โ€” there are plenty of great graphics engines out there, so ARKit doesn't need to reinvent that wheel. What ARKit does is provide a real-world frame of reference for you to display or animate content using an existing graphics technology like SceneKit or SpriteKit (or Unity or Unreal, or a custom engine built with Metal or GL).
Since you mentioned trying to do this with SpriteKit... beware, it gets messy. SpriteKit is a 2D engine, and while ARSKView provides some ways to shoehorn a third dimension in there, those ways have their limits.
ARSKView automatically updates the xScale, yScale, and zRotation of each sprite associated with an ARAnchor, providing the illusion of 3D perspective. But that applies only to nodes attached to anchors, and as noted above, anchors are static.
You can, however, add other nodes to your scene, and use those same properties to make those nodes match the ARSKView-managed nodes. Here's some code you can add/replace in the ARKit/SpriteKit Xcode template project to do that. We'll start with some basic logic to run a bouncing animation on the third tap (after using the first two taps to place anchors).
var anchors: [ARAnchor] = []
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
// Start bouncing on touch after placing 2 anchors (don't allow more)
if anchors.count > 1 {
startBouncing(time: 1)
return
}
// Create anchor using the camera's current position
guard let sceneView = self.view as? ARSKView else { return }
if let currentFrame = sceneView.session.currentFrame {
// Create a transform with a translation of 30 cm in front of the camera
var translation = matrix_identity_float4x4
translation.columns.3.z = -0.3
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)
anchors.append(anchor)
}
}
Then, some SpriteKit fun for making that animation happen:
var ballNode: SKLabelNode = {
let labelNode = SKLabelNode(text: "๐Ÿ€")
labelNode.horizontalAlignmentMode = .center
labelNode.verticalAlignmentMode = .center
return labelNode
}()
func startBouncing(time: TimeInterval) {
guard
let sceneView = self.view as? ARSKView,
let first = anchors.first, let start = sceneView.node(for: first),
let last = anchors.last, let end = sceneView.node(for: last)
else { return }
if ballNode.parent == nil {
addChild(ballNode)
}
ballNode.setScale(start.xScale)
ballNode.zRotation = start.zRotation
ballNode.position = start.position
let scale = SKAction.scale(to: end.xScale, duration: time)
let rotate = SKAction.rotate(toAngle: end.zRotation, duration: time)
let move = SKAction.move(to: end.position, duration: time)
let scaleBack = SKAction.scale(to: start.xScale, duration: time)
let rotateBack = SKAction.rotate(toAngle: start.zRotation, duration: time)
let moveBack = SKAction.move(to: start.position, duration: time)
let action = SKAction.repeatForever(.sequence([
.group([scale, rotate, move]),
.group([scaleBack, rotateBack, moveBack])
]))
ballNode.removeAllActions()
ballNode.run(action)
}
Here's a video so you can see this code in action. You'll notice that the illusion only works as long as you don't move the camera โ€” not so great for AR. When using SKAction, we can't adjust the start/end states of the animation while animating, so the ball keeps bouncing back and forth between its original (screen-space) positions/rotations/scales.
You could do better by animating the ball directly, but it's a lot of work. You'd need to, on every frame (or every view(_:didUpdate:for:) delegate callback):
Save off the updated position, rotation, and scale values for the anchor-based nodes at each end of the animation. You'll need to do this twice per didUpdate callback, because you'll get one callback for each node.
Work out position, rotation, and scale values for the node being animated, by interpolating between the two endpoint values based on the current time.
Set the new attributes on the node. (Or maybe animate it to those attributes over a very short duration, so it doesn't jump too much in one frame?)
That's kind of a lot of work to shoehorn a fake 3D illusion into a 2D graphics toolkit โ€” hence my comments about SpriteKit not being a great first step into ARKit.
If you want 3D positioning and animation for your AR overlays, it's a lot easier to use a 3D graphics toolkit. Here's a repeat of the previous example, but using SceneKit instead. Start with the ARKit/SceneKit Xcode template, take the spaceship out, and paste the same touchesBegan function from above into the ViewController. (Change the as ARSKView casts to as ARSCNView, too.)
Then, some quick code for placing 2D billboarded sprites, matching via SceneKit the behavior of the ARKit/SpriteKit template:
// in global scope
func makeBillboardNode(image: UIImage) -> SCNNode {
let plane = SCNPlane(width: 0.1, height: 0.1)
plane.firstMaterial!.diffuse.contents = image
let node = SCNNode(geometry: plane)
node.constraints = [SCNBillboardConstraint()]
return node
}
// inside ViewController
func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
// emoji to image based on https://stackoverflow.com/a/41021662/957768
let billboard = makeBillboardNode(image: "โ›น๏ธ".image())
node.addChildNode(billboard)
}
Finally, adding the animation for the bouncing ball:
let ballNode = makeBillboardNode(image: "๐Ÿ€".image())
func startBouncing(time: TimeInterval) {
guard
let sceneView = self.view as? ARSCNView,
let first = anchors.first, let start = sceneView.node(for: first),
let last = anchors.last, let end = sceneView.node(for: last)
else { return }
if ballNode.parent == nil {
sceneView.scene.rootNode.addChildNode(ballNode)
}
let animation = CABasicAnimation(keyPath: #keyPath(SCNNode.transform))
animation.fromValue = start.transform
animation.toValue = end.transform
animation.duration = time
animation.autoreverses = true
animation.repeatCount = .infinity
ballNode.removeAllAnimations()
ballNode.addAnimation(animation, forKey: nil)
}
This time the animation code is a lot shorter than the SpriteKit version.
Here's how it looks in action.
Because we're working in 3D to start with, we're actually animating between two 3D positions โ€” unlike in the SpriteKit version, the animation stays where it's supposed to. (And without the extra work for directly interpolating and animating attributes.)

Centering the camera on a node in swift spritekit

I am creating a Terraria-style game in Swift. I want to have it so the player node is always in the center of the screen, and when you move right the blocks go left like in Terraria.
I am currently trying to figure out how to keep the view centered on the character. Does anyone know of a good way of accomplishing this?
Since iOS 9 / OS X 10.11 / tvOS, SpriteKit includes SKCameraNode, which makes a lot of this easier:
positioning the camera node automatically adjusts the viewport
you can easily rotate/zoom the camera by transform in the camera node
you can fix HUD elements relative to the screen by making them children of the camera node
the scene's position stays fixed, so things like physics joints don't break the way they do when you emulate a camera by moving the world
It gets even better when you combine camera nodes with another new feature, SKConstraint. You can use a constraint to specify that the camera's position is always centered on a character... or add extra constraints to say, for example, that the camera's position must stay within some margin of the edge of the world.
The below will center the camera on a specific node. It can also smoothly transition to the new position over a set time frame.
class CameraScene : SKScene {
// Flag indicating whether we've setup the camera system yet.
var isCreated: Bool = false
// The root node of your game world. Attach game entities
// (player, enemies, &c.) to here.
var world: SKNode?
// The root node of our UI. Attach control buttons & state
// indicators here.
var overlay: SKNode?
// The camera. Move this node to change what parts of the world are visible.
var camera: SKNode?
override func didMoveToView(view: SKView) {
if !isCreated {
isCreated = true
// Camera setup
self.anchorPoint = CGPoint(x: 0.5, y: 0.5)
self.world = SKNode()
self.world?.name = "world"
addChild(self.world)
self.camera = SKNode()
self.camera?.name = "camera"
self.world?.addChild(self.camera)
// UI setup
self.overlay = SKNode()
self.overlay?.zPosition = 10
self.overlay?.name = "overlay"
addChild(self.overlay)
}
}
override func didSimulatePhysics() {
if self.camera != nil {
self.centerOnNode(self.camera!)
}
}
func centerOnNode(node: SKNode) {
let cameraPositionInScene: CGPoint = node.scene.convertPoint(node.position, fromNode: node.parent)
node.parent.position = CGPoint(x:node.parent.position.x - cameraPositionInScene.x, y:node.parent.position.y - cameraPositionInScene.y)
}
}
Change whatโ€™s visible in the world by moving the camera:
// Lerp the camera to 100, 50 over the next half-second.
self.camera?.runAction(SKAction.moveTo(CGPointMake(100, 50), duration: 0.5))
Source: swiftalicio - 2D Camera in SpriteKit
For additional information, look at Apple's SpriteKit Programming Guide (Example: Centering the Scene on a Node).
You have to create World node that contains nodes. And you should put anchorPoint for example (0.5,0.5). Center on your player. And then you should move your player.
func centerOnNode(node:SKNode){
let cameraPositionInScene:CGPoint = self.convertPoint(node.position, fromNode: world!)
world!.position = CGPoint(x:world!.position.x - cameraPositionInScene.x, y: world!.position.y - cameraPositionInScene.y)
}
override func didSimulatePhysics() {
self.centerOnNode(player!)
}