Adding SCNNode multiple time shows only once - swift

I am trying to add SCNNode mutiple time in a loop at different position but I can see same type of node at once with the last position.
Below is the code
let entityArray:[entity] = [.coin, .coin, .coin, .brick, .coin, .coin, .coin, .brick]
func setupworld() {
let scene = SCNScene(named: "art.scnassets/MainScene.scn")!
var zPosition = -10
var count = 0
let delta = -4
for entity in entityArray {
var node = SCNNode()
switch entity {
case .coin:
node = scene.rootNode.childNode(withName: "coin", recursively: true) ?? node
node.position = SCNVector3(0, -5, zPosition)
case .brick:
node = scene.rootNode.childNode(withName: "brick", recursively: true) ?? node
node.position = SCNVector3(0, 0, zPosition)
}
self.sceneView.scene.rootNode.addChildNode(node)
zPosition += delta
count += 1
}
}
It shows one coin and one brick at last positions.
I am new to scenekit so would be doing something wrong, Please help me.

Building on from the other comments and as #rmaddy has said an SCNNode has a clone() function (which is the approach you should take here) and which simply:
Creates a copy of the node and its children.
One thing to be aware of when using this however, is that each cloned Node will share the same geometry and materials.
That's to say that if you wanted at any point to have some bricks with a red colour and some with a green colour you wouldn't be able to do it with this method since:
changes to the objects attached to one node will affect
other nodes that share the same attachments.
To achieve this e.g. to render two copies of a node using different materials, you must copy both the node and its geometry before assigning a new material, which you can read more about here: Apple Discussion
The reason you are only ever seeing one instance of either the coin or brick is because each time you are iterating through your loop you are saying that the newly created node is equal to either the coin or the brick, so naturally the last element in that loop will be the one that references that element from your scene.
Putting this into practice and solving your issue therefor, your setupWorld function should look like something like this:
/// Sets Up The Coins & Bricks
func setupworld(){
//1. Get Our SCNScene
guard let scene = SCNScene(named: "art.scnassets/MainScene.scn") else { return }
//2. Store The ZPosition
var zPosition = -10
//3. Store The Delta
let delta = -4
//4. Get The SCNNodes We Wish To Clone
guard let validCoin = scene.rootNode.childNode(withName: "coin", recursively: true),
let validBrick = scene.rootNode.childNode(withName: "brick", recursively: true) else { return }
//5. Loop Through The Entity Array & Create Our Nodes Dynamically
var count = 0
for entity in entityArray {
var node = SCNNode()
switch entity{
case .coin:
//Clone The Coin Node
node = validCoin.clone()
node.position = SCNVector3(0, -5, zPosition)
case .brick:
//Clone The Brick Node
node = validBrick.clone()
node.position = SCNVector3(0, 0, zPosition)
}
//6. Add It To The Scene
self.sceneView.scene.rootNode.addChildNode(node)
//7. Adjust The zPosition
zPosition += delta
count += 1
}
}

Related

ARkit SCNMorpher isn't working for me. No errors, just no shape changes

I'm trying a simple example using SCNMorpher to blend between to poly spheres. They are identical in topology except for the position of the points
Each is stored in a .scn file and I get the shapes like:
sphereNode = SCNReferenceNode(named: "sphere")
sphereNode2 = SCNReferenceNode(named: "sphere2")
sphereNode?.morpher = SCNMorpher()
sphereNode!.morpher?.targets = [(sphereNode2?.childNodes.first!.geometry)!]
sphereNode!.name = "EFFECT"
I'm using the faceAnchor blend shapes to drive it
if let effectNode = sceneView?.scene.rootNode.childNode(withName: "EFFECT", recursively: true) {
let v = faceAnchor?.blendShapes[ARFaceAnchor.BlendShapeLocation.jawOpen]
effectNode.morpher?.setWeight(v as! CGFloat, forTargetNamed: "sphere2")
}
I've also tried:
...
effectNode.morpher?.setWeight(v as! CGFloat, forTargetAt: 0)
...
The code runs.. I can print values for v.. they change as I open/close my jaw and that value is passed to the morpher. I see the base sphere shape but it never deforms toward the sphere2 shape. Am I suppose to do anything else to force it to redraw or calc the deformation?
Hmm. looks like I was attaching the morpher to the parent of the shape, not the actual sphere.. funny how asking a question here sometimes creates that "Ah Ha" moment. Reading in my spheres like this fixed it:
sphereNode = SCNReferenceNode(named: "sphere").childNodes.first
sphereNode2 = SCNReferenceNode(named: "sphere2").childNodes.first
sphereNode?.morpher = SCNMorpher()
sphereNode!.morpher?.targets = [(sphereNode2.geometry)!]
sphereNode!.name = "EFFECT"

ARKit limit display distance of a node

I would like to create a node in sceneView, that is displayed at normal position in the scene, until user get too close or too far from it. Then it should be displayed at the same direction from the user, but with restricted distance. So far best I found is SCNDistanceConstraint, which limits this distance, but the problem is, that this constraint after it moved the node, this node stays in this new place. So for example, I want to limit the node to be displayed not closer then one meter from camera. I'm getting closer to the node, and it's being pushed away, but then when I get camera back, this node should return to it's original position - for now it stays where it was pushed. Is there some easy way to get such behavior?
Im not entirely sure I have understood what you mean, but it seems you always want your SCNNode to be positioned 1m away from the camera, but keeping its other x, y values?
If this is the case then you can do something like this:
func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) {
//1. Get The Current Node On Screen & The Camera Point Of View
guard let nodeToPosition = currentNode, let pointOfView = augmentedRealityView.pointOfView else { return }
//2. Set The Position Of The Node 1m Away From The Camera
nodeToPosition.simdPosition.z = pointOfView.presentation.worldPosition.z - 1
//3. Get The Current Distance Between The SCNNode & The Camera
let positionOfNode = SCNVector3ToGLKVector3(nodeToPosition.presentation.worldPosition)
let positionOfCamera = SCNVector3ToGLKVector3(pointOfView.presentation.worldPosition)
let distanceBetweenNodeAndCamera = GLKVector3Distance(positionOfNode, positionOfCamera)
print(distanceBetweenNodeAndCamera)
}
I have added in part three, so you could use the distance to do some additional calculations etc.
Hope this points you in the right direction...
The answer above is not exactly what I need - I want object to be displayed like it was just placed in normal position, so I can get closer and farer to/from it, but limit how close/far I can get from that object. When I'm beyond that limit, object should start move to be always within given distance range from camera. Anyway I think I have found some right direction in this. Instead of assigning position, I'm creating a constraint that constantly updates position of my node to be either in given position if it's in given range from user, or if not, adjusts this position to fit in that range:
private func setupConstraint() {
guard let mainNodeDisplayDistanceRange = mainNodeDisplayDistanceRange else {
constraints = nil
position = requestedPosition
return
}
let constraint = SCNTransformConstraint.positionConstraint(inWorldSpace: true) { (node, currentPosition) -> SCNVector3 in
var cameraPositionHorizontally = (self.augmentedRealityView as! AugmentedRealityViewARKit).currentCameraPosition
cameraPositionHorizontally.y = self.requestedPosition.y
let cameraToObjectVector = self.requestedPosition - cameraPositionHorizontally
let horizontalDistanceFromCamera = Double((cameraToObjectVector).distanceHorizontal)
guard mainNodeDisplayDistanceRange ~= horizontalDistanceFromCamera else {
let normalizedDistance = horizontalDistanceFromCamera.keepInRange(mainNodeDisplayDistanceRange)
let normalizedPosition = cameraPositionHorizontally + cameraToObjectVector.normalizeHorizontally(toDistance: normalizedDistance)
return normalizedPosition
}
return self.requestedPosition
}
constraints = [constraint]
}
internal var requestedPosition: SCNVector3 = .zero {
didSet {
setupConstraint()
}
}
This starts to work fine, but I still need to find a way to animate this.

Run SCNActions sequence with different nodes in SceneKit

I know how to create a sequence of SCNActions in a single node, one by one, in SceneKit. But I would like to know, how can I make a sequence of SCNActions with different nodes?
For example
Move forward node A
Move forward node B
Wait 1 second
Move backward node A
I found an example with SpriteKit but I can not use it, this...
Run SKActions sequence with different nodes
The code of a sequence is as follows
var sequence = [SCNAction] ()
let force = SCNVector3(0.0, 0.0, -1.0)
let move = SCNAction.move(by: force!, duration: 1.5)
squence.append(move)
let actions = SCNAction.sequence(squence)
nodeSelected?.runAction(actions)
you can use customAction.
let actionA = SCNAction.customAction(duration: 1.5) { (node, elapsedTime) in
nodeA.position.z -= 1.0
}
let actionB = SCNAction.customAction(duration: 1.5) { (node, elapsedTime) in
nodeB.position.z -= 1.0
}
let sequence = SCNAction.sequence([actionA,actionB])
anyNode.runAction(sequence)
Running action with any node will first move nodeA then nodeB.

Following a path in Spritekit EXC_BAD_ACCESS

I have a simple Snake game where the head draws a UIBezier path. That part works fine:
func addLineToSnakePath(snakeHead: SnakeBodyUnit) {
//add a new CGPoint to array
activeSnakePathPoints.append(CGPoint(x: snakeHead.partX, y: snakeHead.partY))
let index = activeSnakePathPoints.count-1
if (index == 1) {
path.moveToPoint(activeSnakePathPoints[index-1])
}
path.addLineToPoint(activeSnakePathPoints[index])
shapeNode.path = path.CGPath
}
The path generates with swipes as the Head moves around the screen. Now I add a body unit to follow the UIBezier path and I get a bad access error.
func addBodyPart() {
let followBody = SKAction.followPath(path.CGPath, asOffset: true, orientToPath: false, duration: 1.0)
snakePart.runAction(followBody)
}
Crash at:
0 SKCFollowPath::cpp_willStartWithTargetAtTime(SKCNode*, double)
Thread 1 EXC_BAD_ACCESS
Looking at the code I would rather strengthen my code this way:
func addLineToSnakePath(snakeHead: CGPoint) {
let count = activeSnakePathPoints.count
if count == 0 { return } // The snake don't have an head..
//add a new CGPoint to array
activeSnakePathPoints.append(CGPoint(x: snakeHead.x, y: snakeHead.y))
guard count > 1 else {
// There are only two element (the head point and the new point) so:
path = UIBezierPath.init()
path.moveToPoint(activeSnakePathPoints[count-1])
shapeNode.path = path.CGPath
return
}
// There are some elements:
path.addLineToPoint(activeSnakePathPoints[count-1])
shapeNode.path = path.CGPath
}
Next, when you make the followPath action I've seen you use offset value to true ( I don't know if this is wanted from you..):
asOffset : If YES, the points in the path are relative offsets to the
node’s starting position. If NO, the points in the node are absolute
coordinate values.
In other words true means the path is relative from the sprite's starting position, false means absolute
Finally in the line:
snakePart.runAction(followBody)
I don't know what is snakePart but you use shapeNode in your function
Update:
Thinking about your crash, seems snakePart is not a valid SKNode (like when you try to use a sprite or shape without initialize itself)

How to combine to two or more SCNGeometry / SCNNode into one

As if I would like to create some custom shape by combining some SCNBox and SCNPyramid etc, I can put them together by setting the right positions and geometries. How ever I just can not find a way to combine them as a single unit which can be modified or reacted in physical world.
The code below that I would like create a simple house shape SCNNode, and I would like the node attached each other when affected by any collisions and gravity.
Can anyone give some hints?
let boxGeo = SCNBox(width: 5, height: 5, length: 5, chamferRadius: 0)
boxGeo.firstMaterial?.diffuse.contents = UIColor.blueColor()
let box = SCNNode(geometry: boxGeo)
box.position = SCNVector3Make(0, -2.5, 0)
scene.rootNode.addChildNode(box)
let pyramidGeo = SCNPyramid(width: 7, height: 7, length: 7)
pyramidGeo.firstMaterial?.diffuse.contents = UIColor.greenColor()
let pyramid = SCNNode(geometry: pyramidGeo)
pyramid.position = SCNVector3Make(0, 0, 0)
scene.rootNode.addChildNode(pyramid)
Make a container node, simply an empty node without any geometry. Let's call it "houseNode" since that's what it looks like you're building.
let houseNode = SCNNode()
Now make your other two nodes children of this.
houseNode.addChildNode(pyramid)
houseNode.addChildNode(box)
Now use the container node anytime you want to act on the two combined nodes.
Edit: You can effect changes to the geometry of the objects in your container by enumeration:
houseNode.enumerateChildNodesUsingBlock({
node, stop in
// change the color of all the children
node.geometry?.firstMaterial?.diffuse.contents = UIColor.purpleColor()
// I'm not sure on this next one, I've yet to use "physics".
houseNode.physicsBody?.affectedByGravity = true
})
Thanks bpedit!
With the childNodes method and the following code to set it a physicsShape, I found the solution for it.
houseNode.physicsBody = SCNPhysicsBody(type: .Dynamic,
shape: SCNPhysicsShape(node: houseNode,
options: [SCNPhysicsShapeKeepAsCompoundKey: true]))