CAAnimation on multiple SceneKit nodes simultaneously - ios

I am creating an application wherein I am using SceneKit contents in AR app. I have multiple nodes which are being placed at different places in my scene. They may or may not be necessarily be inside one parent node. The user has to choose a correct node, as per challenge set by the application. If the user chooses correct node, the correct node goes through one kind of animation and incorrect ones (may be several) undergo another set of animation. I am accomplishing animations using CAAnimation directly, which is all good. Basically to accomplish this, I am creating an array of all nodes and using them for animation.
DispatchQueue.global(qos: .userInteractive).async { [weak self] in
for node in (self?.nodesAddedInScene.keys)! {
for index in 1...node.childNodes.count - 1 {
if node.childNodes[index].childNodes.first?.name == "target" {
self?.riseUpSpinAndFadeAnimation(on: node.childNodes[index])
} else {
self?.fadeAnimation(on: node.childNodes[index])
}
}
}
}
When user chooses "target" node, that node goes through one set of animation and others go through another set of animations.
RiseUpSpinAndFadeAnimation:
private func riseUpSpinAndFadeAnimation(on shape: SCNNode) {
let riseUpAnimation = CABasicAnimation(keyPath: "position")
riseUpAnimation.fromValue = SCNVector3(shape.position.x, shape.position.y, shape.position.z)
riseUpAnimation.toValue = SCNVector3(shape.position.x, shape.position.y + 0.5, shape.position.z)
let spinAnimation = CABasicAnimation(keyPath: "eulerAngles.y")
spinAnimation.toValue = shape.eulerAngles.y + 180.0
spinAnimation.autoreverses = true
let fadeAnimation = CABasicAnimation(keyPath: "opacity")
fadeAnimation.toValue = 0.0
let riseUpSpinAndFadeAnimation = CAAnimationGroup()
riseUpSpinAndFadeAnimation.animations = [riseUpAnimation, fadeAnimation, spinAnimation]
riseUpSpinAndFadeAnimation.duration = 1.0
riseUpSpinAndFadeAnimation.fillMode = kCAFillModeForwards
riseUpSpinAndFadeAnimation.isRemovedOnCompletion = false
shape.addAnimation(riseUpSpinAndFadeAnimation, forKey: "riseUpSpinAndFade")
}
FadeAnimation:
private func fadeAnimation(on shape: SCNNode) {
let fadeAnimation = CABasicAnimation(keyPath: "opacity")
fadeAnimation.toValue = 0.0
fadeAnimation.duration = 0.5
fadeAnimation.fillMode = kCAFillModeForwards
fadeAnimation.isRemovedOnCompletion = false
shape.addAnimation(fadeAnimation, forKey: "fade")
}
I expect animations to work out, which they are actually. However, the issue is since the nodes are in an array animation is not being done at the same time for all nodes. There are minute differences in start of animation which actually is leading to not so good UI.
What I am looking for is a logic wherein I can attach animations on all nodes and call these animations together later when let's say the user taps correct node. Arrays don't seem to be a wise choice to me. However, I am afraid if I make all of these nodes child nodes of an empty node and the run animation on that empty node, possibly it would be difficult to manage placement of these child nodes in the first place since they supposed to be kept at random distances and not necessarily close together. Given that this ultimately drives AR experience, it more so becomes a bummer.
Requesting some inputs whether there are methods to attach animation to multiple (selected out of those) object (even if sequentially) but RUN them together. I used shape.addAnimation(fadeAnimation, forKey: "fade") "forKey", can that be made of use in such use case? Any pointers appreciated.

I've had up to fifty SCNNodes animating in perfect harmony by using CAKeyframe animations that are paused (.speed = 0) and setting the animation's time offset (.timeOffset) inside a SCNSceneRendererDelegate "renderer(updateAtTime:)" function.
It's pretty amazing how you can add a paused animation with an time offset every 1/60th of a second for a large number of nodes. Hats off to the SceneKit developers for having so little overhead on adding and removing CAAnimations.
I tried many different CAAnimation/SCNAction techniques before settling on this. In other methods the animations would drift out of sync over time.

Manganese,
I am just taking a guess here or could spark an idea for you :-)
I am focusing on this part of your question:
"What I am looking for is a logic wherein I can attach animations on all nodes and call these animations together later when let's say the user taps correct node."
I wonder if SCNTransaction:
[https://developer.apple.com/documentation/scenekit/scntransaction][1]
might do the trick
or maybe dispatch.sync or async (totally guessing...but could help)
[https://developer.apple.com/documentation/dispatch][1]
or I am way off the mark :-)
just trying to help out....
We all learn by sharing what we know
RAD

Related

Animate several properties of several views with Core Animation

I want to animate several properties of several subviews using Core Animations in Swift. I'm looking for a way to group the animations together and handle them as one, mainly to be able to synchronise them altogether and pause and resume all of them together, but also in order to avoid code duplication and make the code cleaner.
I tried to use other kind of animations, such as UIView.animate or UIViewPropertyAnimator, but neither of them fit my needs because they don't allow the customization needed for my case.
For now, I'm using CATransaction to ensure that the animation changes are committed to Core Animation at the same time:
CATransaction.begin()
CATransaction.setAnimationDuration(1.5)
CATransaction.setAnimationTimingFunction(CAMediaTimingFunction(controlPoints: 0.85, 0, 0.15, 1))
let opacityAnimation = CABasicAnimation(keyPath: "opacity")
opacityAnimation.fromValue = 0
opacityAnimation.toValue = 1.0
opacityAnimation.repeatCount = .infinity
opacityAnimation.autoreverses = true
self.lable.layer.add(beforeOpacityAnimation, forKey: "opacityAninmation")
let scaleAnimation = CABasicAnimation(keyPath: "transform.scale")
scaleAnimation.fromValue = 1.5
scaleAnimation.toValue = 1.0
scaleAnimation.repeatCount = .infinity
scaleAnimation.autoreverses = true
self.imageView.layer.add(targetImageScaleAnimation, forKey: "scaleAnimator")
CATransaction.commit()
So far so good. But now I want to be able to pause and resume all animations together. From what I see here, in order to achieve this one would need to iterate over all animated layers (e.g. self.lable.layer, self.imageView.layer) to modify their speed, timeOffset and beginTime properties (this is kinda dirty and nonatomic).
Is there a way to group these animations or refer to them as one in order to pause and resume them altogether?
If not, should I at least put these iterations over layers inside a CATransaction as well?

How to add SCNNodes without blocking main thread?

I'm creating and adding a large number of SCNNodes to a SceneKit scene, which causes the app to freeze for a second or two.
I thought I could fix this by putting all the action in a background thread using DispatchQueue.global(qos: .background).async(), but no dice. It behaves exactly the same.
I saw this answer and put the nodes through SCNView.prepare() before adding them, hoping it would slow down the background thread and prevent blocking. It didn't.
Here's a test function that reproduces the problem:
func spawnNodesInBackground() {
// put all the action in a background thread
DispatchQueue.global(qos: .background).async {
var nodes = [SCNNode]()
for i in 0...5000 {
// create a simple SCNNode
let node = SCNNode()
node.position = SCNVector3(i, i, i)
let geometry = SCNSphere(radius: 1)
geometry.firstMaterial?.diffuse.contents = UIColor.white.cgColor
node.geometry = geometry
nodes.append(node)
}
// run the nodes through prepare()
self.mySCNView.prepare(nodes, completionHandler: { (Bool) in
// nodes are prepared, add them to scene
for node in nodes {
self.myRootNode.addChildNode(node)
}
})
}
}
When I call spawnNodesInBackground() I expect the scene to continue rendering normally (perhaps at a reduced frame rate) while new nodes are added at whatever pace the CPU is comfortable with. Instead the app freezes completely for a second or two, then all the new nodes appear at once.
Why is this happening, and how can I add a large number of nodes without blocking the main thread?
I don't think this problem is solvable using the DispatchQueue. If I substitute some other task instead of creating SCNNodes it works as expected, so I think the problem is related to SceneKit.
The answers to this question suggest that SceneKit has its own private background thread that it batches all changes to. So regardless of what thread I use to create my SCNNodes, they all end up in the same queue in the same thread as the render loop.
The ugly workaround I'm using is to add the nodes a few at a time in SceneKit's delegated renderer(_:updateAtTime:) method until they're all done.
I poked around on this and didn't solve the freeze (I did reduce it a bit).
I expect that prepare() is going to exacerbate the freeze, not reduce it, because it's going to load all resources into the GPU immediately, instead of letting them be lazily loaded. I don't think you need to call prepare() from a background thread, because the doc says it already uses a background thread. But creating the nodes on a background thread is a good move.
I did see pretty good performance improvement by moving the geometry outside the loop, and by using a temporary parent node (which is then cloned), so that there's only one call to add a new child to the scene's root node. I also reduced the sphere's segment count to 10 (from the default of 48).
I started with the spinning spaceship sample project, and triggered the addition of the spheres from the tap gesture. Before my changes, I saw 11 fps, 7410 draw calls per frame, 8.18M triangles. After moving the geometry out of the loop and flattening the sphere tree, I hit 60 fps, with only 3 draw calls per frame and 1.67M triangles (iPhone 6s).
Do you need to build these objects at run time? You could build this scene once, archive it, and then embed it as an asset. Depending on the effect you want to achieve, you might also consider using SCNSceneRenderer's present(_:with:incomingPointOfView:transition:completionHandler) to replace the entire scene at once.
func spawnNodesInBackgroundClone() {
print(Date(), "starting")
DispatchQueue.global(qos: .background).async {
let tempParentNode = SCNNode()
tempParentNode.name = "spheres"
let geometry = SCNSphere(radius: 0.4)
geometry.segmentCount = 10
geometry.firstMaterial?.diffuse.contents = UIColor.green.cgColor
for x in -10...10 {
for y in -10...10 {
for z in 0...20 {
let node = SCNNode()
node.position = SCNVector3(x, y, -z)
node.geometry = geometry
tempParentNode.addChildNode(node)
}
}
}
print(Date(), "cloning")
let scnView = self.view as! SCNView
let cloneNode = tempParentNode.flattenedClone()
print(Date(), "adding")
DispatchQueue.main.async {
print(Date(), "main queue")
print(Date(), "prepare()")
scnView.prepare([cloneNode], completionHandler: { (Bool) in
scnView.scene?.rootNode.addChildNode(cloneNode)
print(Date(), "added")
})
// only do this once, on the simulator
// let sceneData = NSKeyedArchiver.archivedData(withRootObject: scnView.scene!)
// try! sceneData.write(to: URL(fileURLWithPath: "/Users/hal/scene.scn"))
print(Date(), "queued")
}
}
}
I have an asteroid simulation with 10000 nodes and ran into this issue myself. What worked for me was creating the container node, then passing it to a background process to fill it with child nodes.
That background process uses an SCNAction on that container node to add each of the generated asteroids to the container node.
let action = runBlock {
Container in
// generate nodes
/// then For each node in generatedNodes
Container.addChildNode(node)
}
I also used a shared level of detail node with an uneven sided block as its geometry so that the scene can draw those nodes in a single pass.
I also pre-generate 50 asteroid shapes that get random transformations applied during the background generation process. That process simply has to grab at random a pregen block apply a random simd transformation then stored for adding scene later.
I’m considering using a pyramid for the LOD but the 5 x 10 x 15 block works for my purpose. Also this method can be easily throttled to only add a set amount of blocks at a time by creating and passing multiple actions to the node. Initially I passed each node as an action but this way works too.
Showing the entire field of 10000 still affects the FPS slightly by 10 a 20 FPS but At that point the container nodes own LOD comes into effect showing a single ring.
Add all of them when application started but position them where camera dont see. When you need them change their position where they should be.

How start a animation in a certain time?

I have a CABasicAnimation involving multiple shapes that shows a transition between two different navigations. It's essentially a morph with an x duration.
However, there are a couple of requirements that I don't know how to deal with (and my googlefu isn't helping)
I need to be able to show an arbitrary point of the animation. For instance, if I'm mid-swipe between two views I need to be able to show the animation at 50%.
I need to be able to start the animation at an arbitrary point. In the previous case, if I'm mid-swipe and release the view I need to be able to complete the animation from 50% to 100%.
I gave it a shot with timeSincePause (code) but I can't get anywhere with the animation.
As an aid I have these two methods,
If the method of Resume an animation is an implicit way of continuing the same from a certain point with difference that is continuous from a time X which is defined by the pause method, how can I get it to NOT be the pause method that defines Continue beginTime if not already determined
#IBAction func tapPause() {
let pausetime = shape.convertTime(CACurrentMediaTime(), from: nil)
shape.speed = 0.0
shape.timeOffset = pausetime
}
#IBAction func tapResume() {
let pausedTime = shape.timeOffset
shape.speed = 1.0
shape.timeOffset = 0.0
shape.beginTime = 0.0
let timeSincePause = shape.convertTime(CACurrentMediaTime(), from: nil) - pausetime
shape.beginTime = timeSincePause
}
Which methods should I research to get this done?

continus animation with google maps

I have this method:
private func setupCameraPositionForMovement(markerPos: CLLocationCoordinate2D, secondPos: CLLocationCoordinate2D) {
let bearing = bearingBetweenLatLngs(markerPos, end: secondPos)
let cameraPosition = GMSCameraPosition(target: markerPos, zoom: 16, bearing: bearing + 20, viewingAngle: 90)
map.animateToCameraPosition(cameraPosition)
}
I am using this method to animate movement from markerPos to secondPos, And this work fine when I call it once like:
CATransaction.begin()
CATransaction.setAnimationDuration(1)
setupCameraPositionForMovement(markers[selectedIndex - 1].position, secondPos: markers[selectedIndex].position)
CATransaction.commit()
Now, I want to call this animation multiple times to display a continues movement,
So I tried this code:
for i in 1..<markers.count {
CATransaction.begin()
CATransaction.setAnimationDuration(1)
setupCameraPositionForMovement(markers[i - 1].position, secondPos: markers[i].position)
CATransaction.commit()
}
But without any benefit.
When I searched for this problem I found something like CAAnimationGroup, But I did not know how to use it in this situation.
Can any one help me ?
CAAnimationGroup allows multiple animations to be grouped and run concurrently. The grouped animations run in the time space specified by the CAAnimationGroup instance.
The duration of the grouped animations are not scaled to the duration of their CAAnimationGroup. Instead, the animations are clipped to the duration of the animation group.
CAAnimationGroup has only one property: animations.
This property is intended to add an array of animations to a layer. Suppose, for example, you want to change the transparency of a layer while your are moving it on the screen. If you simply create the two animations and add them to a layer one by one, you loose the control of the synchronization and depending on what your app is doing, you could have some visible delay between the 2 animations.
For more information check this page.
You can also check this SO question for more information.

SKEffectNode - CIFilter Blur Size Limit - Big Black Box

I am trying to blur multiple SKNode objects. I do this by having a parent SKEffectNode with a CIFilter set to #"CIGaussianBlur". Like so:
- (SKEffectNode *)createBlurNode
{
SKEffectNode *blurNode = [[SKEffectNode alloc] init];
blurNode.shouldRasterize = YES;
[blurNode setShouldEnableEffects:NO];
[blurNode setFilter:[CIFilter filterWithName:#"CIGaussianBlur"
keysAndValues:#"inputRadius", #10.0f, nil]];
return blurNode;
}
This works fine for a bunch of nodes currently onscreen. But when I space these notes far away from each other (about 3000 pixels), the blurring no longer happens and I get a big black box. This happens regardless of whether the SKNodes I'm blurring are SKShapeNodes or SKSpriteNodes. Here's a sample project with this issue: Sample Project. (By the way, thanks to BobMoff for the initial version found here):
Here's happy blur (when nodes are less than 3000 pixels away from each other):
Sad blur (when nodes are more than 3000 pixels away from each other):
UPDATE
This behavior occurs whenever an SKEffectNode is the parent. It doesn't matter if it's enabling effects, blurring, etc. If the parent node is an SKNode, it's fine. i.e. Even if the parent blur node is created like it is below, you will get the blackness:
- (SKEffectNode *)createBlurNode
{
SKEffectNode *blurNode = [[SKEffectNode alloc] init];
// blurNode.shouldRasterize = YES;
// [blurNode setShouldEnableEffects:NO];
// [blurNode setFilter:[CIFilter filterWithName:#"CIGaussianBlur"
// keysAndValues:#"inputRadius", #10.0f, nil]];
return blurNode;
}
I had a similar problem, with a very wide, panning scene that I wanted to blur.
To get the blur effect to work, I removed any nodes that were sticking out too far past the edges of the scene:
// Property declarations, elsewhere in the class:
var blurNode: SKEffectNode
var mainScene: SKScene
var exParents: [SKNode : SKNode] = [:]
/**
* Remove outlying nodes from the scene and activate the SKEffectNode
*/
func blurScene() {
let FILTER_MARGIN: CGFloat = 100
let widthMax: CGFloat = mainScene.size.width + FILTER_MARGIN
let heightMax: CGFloat = mainScene.size.height + FILTER_MARGIN
// Recursively iterate through all blurNode's children
blurNode.enumerateChildNodesWithName(".//*", usingBlock: {
[unowned self]
node, stop in
if node.parent != nil && node.scene != nil { // Ignore nodes we already removed
if let sprite = node as? SKSpriteNode {
// Calculate sprite node position in scene coordinates
let sceneOrig = sprite.scene!.convertPoint(sprite.position, fromNode: sprite.parent!)
// Find left, right, bottom and top edges of sprite
let l = sceneOrig.x - sprite.size.width*sprite.anchorPoint.x
let r = l + sprite.size.width
let b = sceneOrig.y - sprite.size.height*sprite.anchorPoint.y
let t = b + sprite.size.height
if l < -FILTER_MARGIN || r > widthMax || b < -FILTER_MARGIN || t > heightMax {
self.exParents[sprite] = sprite.parent!
sprite.removeFromParent()
}
}
}
})
blurNode.shouldEnableEffects = true
}
/**
* Disable blur and reparent nodes we removed earlier
*/
func removeBlur() {
self.blurNode.shouldEnableEffects = false
for (kid, parent) in exParents {
parent.addChild(kid)
}
exParents = [:]
}
NOTES:
This does remove content from your effect node, so extremely wide nodes won't show up in the final result:
You can see the mountain highlighted in red stuck out too far and was removed from the resulting blur.
This code only considers SKSpriteNodes. Empty SKNodes don't seem to break the effect node, but if you're using other visible nodes like SKShapeNodes or SKLabelNodes, you'll have to modify this code to include them.
If you have ignoreSiblingOrder = false, this code might mess up your z-ordering since you can't guarantee what order the nodes are added back to the scene.
Stuff I tried that didn't work
Simply saying node.hidden = true instead of using removeFromParent() doesn't work. That would be WAY too easy ;)
Using an SKCropNode to crop out outlying content didn't work for me. I tried having the SKEffectNode parent the SKCropNode and the other way around, but the black square appeared no matter how small I made the cropped area. This might still be worth looking into if you're desperate for a cleaner solution.
As noted here, SKScenes are secretly SKEffectNodes and you can set their filter just like our blurNode above. SKScenes don't show a black screen when their content is too big. Unfortunately, they seem to just silently disable the filter instead. Again, I might have missed something, so you could explore this option further if you're trying to apply an effect across the entire scene.
Alternate Solutions
You can capture an image of the whole screen and apply a filter to that, as suggested here. I ended up going with an even simpler solution; I took a generic screenshot of the stuff I wanted to blur, then applied a very heavy blur so you can't see the precise details. I used that as the blurred background and you can hardly tell it's not the real thing ;) This also saves a healthy chunk of memory and avoids a small UI hiccup.
Musings
This is a pretty nasty bug, and I hope Apple comes up with a solution soon. You can click this cute picture of a camera to get a GPU trace and some insight on what's happening:
The device seems to be discarding the framebuffer for the effect node because it takes up too much memory. This is affirmed by the fact that when there's more memory pressure on the device, it's easier to get the 'black square' on smaller content in the SKEffectNode.
I used a method that worked for my game but it requires the blurred area to be static without movement.
On iOS 10 using Swift 3 I used SKSpriteNode, SKView, SKEffectNode, CIFilter. I created a sprite from a texture returned from the SKView method "texture from node" and passed the current scene as the parameter because it inherits from SKNode. So essentially I was taking a "screenshot" of the scene and creating a sprite from it. I then put it in an SKEffectNode with a blur filter. (set "should rasterize" to true for better performance as I only needed to blur once). Finally I added the new sprite to the scene. From there you could add sprites to the scene and place them above the new blurred node.
let blurFilter = CIFilter(name: "CIGaussianBlur")!
let blurAmount = 15.0
blurFilter.setValue(blurAmount, forKey: kCIInputRadiusKey)
let blurEffect = SKEffectNode()
blurEffect.shouldRasterize = true
let screenshotNode = SKSpriteNode(texture: gameScene.view!.texture(from: gameScene))
blurEffect.addChild(screenshotNode)
blurEffect.filter = blurFilter
gameScene.addChild(blurEffect)
Possible workaround for the bug:
Use a camera, zoom WAY out, so you can see most everything of your background, take a screenshot style rendering of this image. Crop it to your needs, and then blur it. Then rasterise this.
Then scale this image back up, and slice it up if needs be, and place accordingly.
SKEffectNode renders into a texture. In most iOS systems the maximum size for a texture is 2048x2048. If an SKEffectNode is trying to render content larger than that, it will just use a 2048x2048 texture and anything outside of it will just not appear in the texture. It won't give you any error or warning about this happening; it simply does it silently.
And no, there is no way to tell SKEffectNode to use a texture of a specific size, and pan&clamp the content into it. It always uses a texture that will cover all the child nodes, and if the texture would be too large, it just silently uses that 2048x2048 texture.

Resources