Copying SCNParticleSystem doesn't seem to work well - swift

I'm trying to use an SCNParticleSystem as a "template" for others. I basically want the exact same properties except for the color animation for the particles. Here's what I've got so far:
if let node = self.findNodeWithName(nodeName),
let copiedParticleSystem: SCNParticleSystem = particleSystemToCopy.copy() as? SCNParticleSystem,
let colorController = copiedParticleSystem.propertyControllers?[SCNParticleSystem.ParticleProperty.color],
let animation: CAKeyframeAnimation = colorController.animation as? CAKeyframeAnimation {
guard animation.values?.count == animationColors.count else {
return nil
}
// Need to copy both the animations and the controllers
let copiedAnimation: CAKeyframeAnimation = animation.copy() as! CAKeyframeAnimation
copiedAnimation.values = animationColors
let copiedController: SCNParticlePropertyController = colorController.copy() as! SCNParticlePropertyController
copiedController.animation = copiedAnimation
// Finally set the new copied controller
copiedParticleSystem.propertyControllers?[SCNParticleSystem.ParticleProperty.color] = copiedController
// Add the particle system to the desired node
node.addParticleSystem(copiedParticleSystem)
// Some other work ...
}
I copy not only the SCNParticleSystem, but also SCNParticlePropertyController and CAKeyframeAnimation just to be safe. I've found that I've had to manually do these "deep" copies "manually" since a .copy() on SCNParticleSystem doesn't copy the animation, etc.
When I turn on the copied particle system on the node it was added to (by setting the birthRate to a positive number), nothing happens.
I don't think the problem is with the node that I've added it to, since I've tried adding the particleSystemToCopy to that node and turning that on, and the original particle system becomes visible in that case. This seems to indicate to me that the the node I've added the copied particle system to is OK in terms of its geometry, rendering order, etc.
Something else perhaps worth mentioning: the scene is loaded from a .scn file and not created programmatically in code. In theory that shouldn't affect anything, but who knows ...
Any ideas on why this copied particle system doesn't do anything when I turn it on?

Do not use copy() method for particle systems !
copy() method does not allow copy particles' color (copied particles will be default white).
You can test it with the following code:
let particleSystem01 = SCNParticleSystem()
particleSystem01.birthRate = 2
particleSystem01.particleSize = 0.5
particleSystem01.particleColor = .systemIndigo // INDIGO
particleSystem01.emitterShape = .some(SCNSphere(radius: 2.0))
let particlesNode01 = SCNNode()
particlesNode01.addParticleSystem(particleSystem01)
particlesNode01.position.y = -3
sceneView.scene.rootNode.addChildNode(particlesNode01)
let particleSystem02 = particleSystem01.copy() // WHITE
let particlesNode02 = SCNNode()
particlesNode02.addParticleSystem(particleSystem02 as! SCNParticleSystem)
particlesNode02.position.y = 3
sceneView.scene.rootNode.addChildNode(particlesNode02)
Use clone() method for nodes instead !
clone() method works more consistently for 3d objects and particle systems and it can help you save particles' color but, of course, doesn't allow save a position for each individual particle.
let particlesNode02 = particlesNode01.clone() // INDIGO
particlesNode02.position.y = 3
sceneView.scene.rootNode.addChildNode(particlesNode02)

Related

Animating SCN Objects with CAKeyframeAnimation in Swift SceneKit

I'm writing an application that displays chemical reactions and molecules in 3D. I read in all the values and positions of each atom from a text file and I am creating each atom shape with SCNSpheres. I have all the other values I need read in properly, but I can't figure out how to add keyframe animations to each node object in my scene.
I set up the molecules like this in ViewController.swift
func makeAtom(atomName: String, coords: [Double], scene: SCNScene) {
guard let radius = atomRadii[atomName]?.atomicRadius else { return }
atoms.append(Atom(name: atomName, x: coords[0], y: coords[1], z: coords[2], radius: radius, positions: []))
let atomGeometry = SCNSphere(radius: CGFloat(radius))
let atomNode = SCNNode(geometry: atomGeometry)
atomNode.position = SCNVector3(coords[0], coords[1], coords[2])
scene.rootNode.addChildNode(atomNode)
atomNodes.append(atomNode)
}
I know that the CAKeyframeAnimations are supposed to be set up like this
let animation = CAKeyframeAnimation()
animation.keyPath = "position.y"
animation.values = [0, 300, 0]
animation.keyTimes = [0, 0.5, 1]
animation.duration = 2
animation.isAdditive = true
vw.layer.add(animation, forKey: "move")
I just don't know where I should be declaring these animations and how the layers factor into all this. What layer should I be adding the animations to? And how can I trigger them to play? I've been searching all over the internet for help with this but I can't find anything that just shows a simple implementation.
I can provide more code if need be, I'm pretty new to StackOverflow and want to make sure I'm doing this right.
You can do it different ways, but I like this method: 58001288 (my answer here) as you can pre-build some animations using scenekit and then run them as a sequence.
Per the comment.. needed more room.
A sequence is a fixed thing. You can start, repeat, and stop it. However, it's hard to interact with it during phases.
If you really need to do that, then one way is to break up your sequence into its parts and call the next one yourself after a completion handler of the current one. I keep an array and a counter so that I know where I am. So basically it's just a queue of actions that I manage - if I'm on a certain step and the button is pressed, then I can cancel all current actions, set the desired effect, and restart it.
Edit:
The completion handler calls itself at the end of the function and advances its own array count so that the next one in the list can be called. This is obviously a bit dangerous, so I would use sparingly, but that's how I did it. I started mine on a timer, then don't forget to clean it up. I had a global GAME_ACTIVE switch and within the code I checked for it before calling myself again.
Edit2: This is actually a moveTo, but it's still just a custom set of SCNActions that calls itself when complete based on duration so that it immediately goes to the next one without a delay.
func moveTo()
{
let vPanelName = moves[moveCount]
let vLaneNode = grid.gridPanels[vPanelName]!.laneNodes[lane]
let vAction = SCNAction.move(to: vLaneNode.presentation.position, duration: TimeInterval(data.getAttackSpeed(vGameType: gameType)))
node.runAction(vAction, completionHandler:
{
self.moveCount += 1
if(self.moveCount >= self.moves.count - 1)
{
self.killMe(vRealKill: false)
return
}
else
{
self.moveTo()
}
})
}

ARKit Adding node causes frame drop even after using `prepare`

I am adding a 3D model containing animations to the scene that I previously download from the internet. Before adding this node I use prepare function on it because I wan't to avoid frame drop. But still I get a very short frame drop to about 47 fps. This is caused by executing this prepare function. I also tried using prepare(_:, shouldAbortBlock:) on other dispatch queue, but this still didn't help. Can someone help me resolve this or tell me why there is this happening?
arView.sceneView.prepare([mediaNode]) { [mediaNode, weak self] (success) in
guard let `self` = self else { return }
guard
let currentMediaNode = self.mediaNode as? SCNNode,
currentMediaNode === mediaNode,
!self.mainNode.childNodes.contains(mediaNode)
else { return }
self.mainNode.addChildNode(mediaNode)
}
By the way this is a list of files I'm using to load this model:
https://www.dropbox.com/s/7968fe5wfdcxbyu/Serah-iOS.dae?dl=1
https://www.dropbox.com/s/zqb6b6rxynnvc5e/0001.png?dl=1
https://www.dropbox.com/s/hy9y8qyazkcnvef/0002.tga?dl=1
https://www.dropbox.com/s/fll9jbjud7zjlsq/0004.tga?dl=1
https://www.dropbox.com/s/4niq12mezlvi5oz/0005.png?dl=1
https://www.dropbox.com/s/wikqgd46643327i/0007.png?dl=1
https://www.dropbox.com/s/fioj9bqt90vq70c/0008.tga?dl=1
https://www.dropbox.com/s/4a5jtmccyx413j7/0010.png?dl=1
DAE file is already compiled by Xcode tools so that it can be loaded after being downloaded from the internet. And this is the code I use to load it after it's downloaded:
class func loadModel(fromURL url: URL) -> SCNNode? {
let options = [SCNSceneSource.LoadingOption.animationImportPolicy : SCNSceneSource.AnimationImportPolicy.playRepeatedly]
let sceneSource = SCNSceneSource(url: url, options: options)
let node = sceneSource?.entryWithIdentifier("MDL_Obj", withClass: SCNNode.self)
return node
}
I was experiencing the same issue. My nodes were all taking advantage of physically-based rendering (PBR) and the first time I added a node to the scene, the frame rate dropped significantly, but was fine after that. I could add as many other nodes without a frame rate drop.
I figured out a work around to this issue. What I do is after I create my ARConfiguration and before I call session.run(configuration) I add a test node with PBR to the scene. In order for that node to not appear, I set the node's material's colorBufferWriteMask to an empty array (see this answer: ARKit hide objects behind walls) Then before I add my content I remove that node. Adding and removing this test node does the trick for me.
Here is an example:
var pbrTestNode: SCNNode!
func addPBRTestNode() {
let testGeometrie = SCNBox(width: 0.5, height: 0.5, length: 0.5, chamferRadius: 0)
testGeometrie.materials.first?.diffuse.contents = UIColor.blue
testGeometrie.materials.first?.colorBufferWriteMask = []
testGeometrie.materials.first?.lightingModel = .physicallyBased
pbrTestNode = SCNNode(geometry: testGeometrie)
scene.rootNode.addChildNode(pbrTestNode)
}
func removePBRTestNode() {
pbrTestNode.removeFromParentNode()
}
func startSessionWithPlaneDetection() {
// Create a session configuration
let configuration = ARWorldTrackingConfiguration()
if #available(iOS 11.3, *) {
configuration.planeDetection = [.horizontal, .vertical]
} else {
configuration.planeDetection = .horizontal
}
configuration.isLightEstimationEnabled = true
// this prevents the delay when adding any nodes with PBR later
sceneController.addPBRTestNode()
// Run the view's session
sceneView.session.run(configuration)
}
Call removePBRTestNode() when you add your content to the scene.
Firstly
Get 3D model for AR app with no more than 10K polygons and a texture of 1K x 1K. The best result can be accomplished with 5K...7K polygons per each model. Totally, SceneKit's scene may contain not more than 100K polygons. This recommendation helps you considerably improve rendering performance and, I suppose, you'll have a minimal drop frame.
Secondly
The simplest way to get rid of drop frame in ARKit/SceneKit/AVKit is to use Metal framework. Just imagine: a simple image filter can be more than a hundred times faster to perform on the GPU than an equivalent CPU-based filter. The same things I could say about realtime AV-video and 3D animation – they perform much better on GPU.
For instance, you can read this useful post about using Metal rendering for AVCaptureSession. There's awesome workflow how to use Metal.
P.S. Check your animated object/scene in 3D authoring tool (if it's OK) before writing a code.

Making a node's sprite change as an animation (spriteKit)

im very new to swift, i have made a sprite kit game with a coin sprite. I want to make it spin so ive made 6 sprites in total. Im trying to get a continuous loop of spinning by quickly changing the sprites. I have tried to do this with the code below.
//This will hold all of the coin spinning sprites
let coinTextures : NSMutableArray = []
//There are 6 in total, so loop through and add them
for i in 0..<6 {
let texture : SKTexture = SKTexture(imageNamed: "Coin\(i + 1)")
coinTextures.insert(texture, at: i)
}
//When printing coinTextures, they have all been added
//Define the animation with a wait time of 0.1 seconds, also repeat forever
let coinAnimation = SKAction.repeatForever(SKAction.animate(with: coinTextures as! [SKTexture], timePerFrame: 0.1))
//Get the coin i want to spin and run the action!
SKAction.run(coinAnimation, onChildWithName: "coin1")
As i said im very new so im not sure what ive done wrong here.
Also the name of the coin i want to spin is "coin1" and the sprites so from coin1 to coin 6
You are almost there.
The problem is that your final line creates an action, but not running it on anything...
You got two alternatives:
1) Run your action on your scene
// Create an action that will run on a child
let action = SKAction.run(coinAnimation, onChildWithName: "coin1")
scene?.run(action)
or
2) Run the action directly on the child
// Assuming that you have a reference to coin1
coin1.run(coinAnimation)
As a sidenote, your array could be declared as var coinTextures: [SKTexture] = [], you can use append to add items to it and avoid the casting when you pass the textures to the action.
Or you can use a more compact form to construct your textures array:
let coinTextures = (1...6).map { SKTexture(imageNamed: "Coin\($0)") }
I hope that this makes sense

SpriteKit, copying a physics body does NOT copy the settings?

It seems almost incredible but,
let pb = model.physicsBody?.copy() as! SKPhysicsBody
print("orig .. \(model.physicsBody!.linearDamping)")
print("pb .. \(pb.linearDamping)")
they are NOT the same. No really.
This is so bizarre that I must be doing something wrong. What am I doing wrong?
Other than just manually copying all the properties, and maintaining the code forever as that evolves, how to do this??
Here's a dupe-qualities function if anyone needs it, to save typing. (This of 2017 - of course it has to be maintained forever as Apple add qualities.)
extension SKSpriteNode
{
func dupe() -> Any {
// annoyingly, Apple does not provide a dupe function as you would use
// when spawning from a model.
let c = self.copy() as! SKSpriteNode
c.physicsBody = self.physicsBody!.copy() as? SKPhysicsBody
c.physicsBody?.affectedByGravity = self.physicsBody!.affectedByGravity
c.physicsBody?.allowsRotation = self.physicsBody!.allowsRotation
c.physicsBody?.isDynamic = self.physicsBody!.isDynamic
c.physicsBody?.pinned = self.physicsBody!.pinned
c.physicsBody?.mass = self.physicsBody!.mass
c.physicsBody?.density = self.physicsBody!.density
c.physicsBody?.friction = self.physicsBody!.friction
c.physicsBody?.restitution = self.physicsBody!.restitution
c.physicsBody?.linearDamping = self.physicsBody!.linearDamping
c.physicsBody?.angularDamping = self.physicsBody!.angularDamping
c.physicsBody?.categoryBitMask = self.physicsBody!.categoryBitMask
c.physicsBody?.collisionBitMask = self.physicsBody!.collisionBitMask
c.physicsBody?.fieldBitMask = self.physicsBody!.fieldBitMask
c.physicsBody?.contactTestBitMask = self.physicsBody!.contactTestBitMask
return c
}
}
(Just one man's opinion, I feel it's better to not just override the NSCopy, since, the whole thing is a touchy issue and it's probably better to simply be explicit for the sake of the next engineer. It's very common to "dupe" the qualities of a game object to another, so, this is fine.)
It has to do with SKPhysicsBody being a wrapper class to PKPhysicsBody
Essentially what is going on is when you create a copy of SKPhysicsBody, it creates a new instance of PKPhysicsBody, not a copy of it.
To get around this, you need to write an extension that fills in the values for you:
extension SKPhysicsBody
{
override open func copy() -> Any {
guard let body = super.copy() as? SKPhysicsBody else {fatalError("SKPhysicsBody.copy() failed")}
body.affectedbyGravity = affectedByGravity
body.allowsRotation= allowsRotation
body.isDynamic= isDynamic
body.mass= mass
body.density = density
body.friction = friction
body.restitution = restitution
body.linearDamping = linearDamping
body.angularDamping = angularDamping
return body
}
}
Note, I typed this by hand, I do not have XCode available at this time to test if it does work.

SpriteKit SKEmitterNode particleAction not working in Xcode 8 / iOS 10

I have an iOS 10 SpriteKit project where I'm trying to put actions on particles from a basic particle emitter created from the "snow" particle template in Xcode 8:
let snowPath = Bundle.main.path(forResource: "Snow", ofType: "sks")!
snowEmitter = NSKeyedUnarchiver.unarchiveObject(withFile: snowPath) as! SKEmitterNode
snowEmitter.position = CGPoint(x: 0, y: size.height / 2)
snowEmitter.particlePositionRange = CGVector(dx: size.width, dy: 0)
snowEmitter.particleAction = SKAction.scale(to: 3, duration: 3)
effectLayer.addChild(snowEmitter) // effectLayer is a SKNode on the scene
The emitter works as it should, but no matter what kind of SKAction I set particleAction to it gets ignored. Has anyone else experienced this?
Update: Doesn't work with Xcode 7 and iOS 9 either.
I think this still might be a leftover iOS 9 bug, not 100% sure. I just tried myself and I cannot get it to work as well.
SKEmitterNode particleAction not working iOS9 Beta
Can you not achieve the same effect using the particles settings directly in snow.sks in the inspector on the right?
You are probably looking at †hose two settings and its subsettings.
1) Particle life cycle (start, range)
2) Particle scale (start, range, speed)
This article has a nice description of each setting.
http://www.techotopia.com/index.php/An_iOS_8_Sprite_Kit_Particle_Emitter_Tutorial#Particle_Birthrate
As a general tip
Your code is not very safe in the first 2 lines because you force unwrapped the snow particle.
If you ever change the name and forget about it or the file becomes corrupted than you will crash. You should change it to something like this
guard let snowPath = Bundle.main.path(forResource: "Snow", ofType: "sks") else { return } // or if let snowPath = ...
snowEmitter = NSKeyedUnarchiver.unarchiveObject(withFile: snowPath) as? SKEmitterNode
...
You can also simply this code a lot by simple saying this where you define your snowEmitter property
let snowEmitter = SKEmitterNode(fileNamed: "Snow")
This will return an optional as well, just like your old code. Than in your method where you set up the emitter say something like this (dont use !)
if let snowEmitter = snowEmitter {
snowEmitter.position =
...
}
Hope this helps
There's another way to achieve the goal.
Make two or more particle systems.
Create two or more noise fields.
Bitmask match one each of the particle systems to one of the noise fields.
Put the noise fields down the bottom, where you want the wiggles to happen
Adjust the noise fields to taste.
As a 2021 datapoint, whilst adding particle emitters to Touchgram, I spent a couple of days exploring this. I came to the conclusion that they were broken in iOS9 and never fixed.