Diffuse an animated SpriteKit Scene over an SCNPlane (ARKit) - swift

tl;dr I'm trying to animate an SKScene on a plane in ARKit (i.e. a table). I can get the SKScene to appear by texturing an SCNPlane with it, but it does not animate. How can I get the animations to work?
Hi,
I'm trying to make an ARKit game which is essentially just a 2D game rendered on a plane anchored in the 3D world. I have the game as an SKScene object. Usually when rendering an SKScene in ARKit I'll create an SCNPlane and set the material to be an SCNMaterial with the SKScene diffused. This is what my current code looks like (in my renderer(_:didAdd:for:):
guard let scene = SKScene(fileNamed: "GameScene") else { return }
let scnMaterial = SCNMaterial()
scnMaterial.diffuse.contents = scene
let plane = SCNPlane(width: 0.8, height: 0.6)
plane.firstMaterial = scnMaterial
let planeNode = SCNNode(geometry: plane)
This renders the SKScene fine, but the update(_:) function is never called. After some research, I found out that when diffusing an SKScene onto an SCNMaterial, it pauses the scene. So, I added self.isPaused = false to the sceneDidLoad() of the SKScene. This gets the update(_:) function to run, but the texture on the SCNPlane is never updated, so the SKScene maintains its starting look. When the SKScene is run on its own in a UIViewController it works fine.
Does anyone know how I can get an animated SKScene to animate on an SCNPlane? At the end of the day, I just want an SKScene to animate on a plane (i.e. a table) in ARKit, so any alternative methods would work.

A Scenekit view runs its own rendering loop
and it does not update/render those node who are not moving by actions or physics movement.
Scenekit automatically decides when to update/render the nodes and when to not.
Try to apply continuous SCNAction or CAAnimation to your SCNPlane node with some movement, and it the diffuse property animation should work.

Related

How am I able to create an overlay SCNScene on an SKScene in Swift using SpriteKit AR?

I already have a SpriteKit AR game and now I would like to add a SceneKit scene overlay on the SpriteKit scene. I have already created a plane detection code, that uses a SceneKit Node and replaces the plane with an image. I know that there is a function called "scene.overlaySKScene" but how am I able to do the opposite and overlay an SCNScene over an SKScene?
guard let scene = SKScene(fileNamed: "LevelOne") else { return }
for node in scene.children {
if let type = createAnchor(in: scene, with: frame, at: node),
type == .boss {
createAntiBossWeapon(in: frame)
}
}
}
Use SK3DNode.
SKView doesn't offer an overlay SCNScene analogous to SCNSceneRenderer's overlaySKScene. But you can place any number of SpriteKit nodes that render SceneKit content in your SpriteKit node hierarchy by using SK3DNode.

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.)

SpriteKit: Add a node to view?

I'm new to Xcode 7, how do I add a black node in SpriteKit?
Can anyone point me in the right direction or link me up with any tutorials please? Thank you
You want to create a new SKSpriteNode object, and add it to the view
class GameScene: SKScene {
var node: SKSpriteNode?
override func didMoveToView(view: SKView) {
let node = SKSpriteNode(imageNamed: "BlackNode.png")
node.position.x = self.size.width / 2
node.position.y = self.size.height / 2
addChild(node)
}
}
So in your GameScene class (or whatever SKScene class you are using), this creates a SKSpriteNode that is the image of whatever "BlackNode.png" is, and sets its position to the center of the screen, and adds it to scene
Assuming your question is asking how to add an all black node to the scene, try this:
override func didMoveTo(view: SKView) {
let node = SKSpriteNode(color: UIColor.black, size: CGSize(width: 100, height: 100)) // Declare and initialize node
addChild(node) // Function that adds node to scene
}
This uses the SKSpriteNode initializer init(color: UIColor, size: CGSize), which you would pass a color for the node and size. This is then used to create a rectangle, while the nodes texture property remains nil.
Note that in this example, no position is explicitly chosen, so the default (0,0) is chosen (centre of scene if scene anchorPoint is 0.5,0.5)
To get a further understanding of scenes vs views and SpriteKit vs UIKit (which I think is where you're having trouble) check out this answer: SpriteKit vs UIKit

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!)
}

Is there a way to include a SpriteKit scene in a SceneKit Scene?

I am wondering if it is possible to include a SpriteKit scene in a SceneKit scene, and if it is, how to do that ?
Yes there is!
You can assign a Sprite Kit scene (SKScene) as the contents of a material property (SCNMaterialProperty) in Scene Kit.
I've come across a few questions about this exact topic and there don't seem to be a whole lot of actual code examples, so here's another one. Hopefully this is helpful:
SKSpriteNode *someSKSpriteNode;
// initialize your SKSpriteNode and set it up..
SKScene *tvSKScene = [SKScene sceneWithSize:CGSizeMake(100, 100)];
tvSKScene.anchorPoint = CGPointMake(0.5, 0.5);
[tvSKScene addChild:someSKSpriteNode];
// use spritekit scene as plane's material
SCNMaterial *materialProperty = [SCNMaterial material];
materialProperty.diffuse.contents = tvSKScene;
// this will likely change to whereever you want to show this scene.
SCNVector3 tvLocationCoordinates = SCNVector3Make(0, 0, 0);
SCNPlane *scnPlane = [SCNPlane planeWithWidth:100.0 height:100.0];
SCNNode *scnNode = [SCNNode nodeWithGeometry:scnPlane];
scnNode.geometry.firstMaterial = materialProperty;
scnNode.position = tvLocationCoordinates;
// Assume we have a SCNCamera and SCNNode set up already.
SCNLookAtConstraint *constraint = [SCNLookAtConstraint lookAtConstraintWithTarget:cameraNode];
constraint.gimbalLockEnabled = NO;
scnNode.constraints = #[constraint];
// Assume we have a SCNView *sceneView set up already.
[sceneView.scene.rootNode addChildNode:scnNode];