Stop sharing node's geometry with its clone programmatically - swift

When you create a copy of an object, geometry and its properties (materials...) are shared with that object.In Xcode Scene Editor you can easily disable that by setting Geometry Sharing (under Attributes Inspector) to Unshare.
I want to achieve the same thing programmatically, but can't find any similar properties in SceneKit documentation.I have found a similar post where someone proposed copying the object, its geometry and its material. I tried doing so but had no success.
This is the relevant part of my code:
let randomColors: [UIColor] = [UIColor.blue, UIColor.red, UIColor.yellow, UIColor.gray]
let obstacleScene = SCNScene(named: "art.scnassets/Scenes/obstacleNormal.scn")
let obstacle = obstacleScene?.rootNode.childNode(withName: "obstacle", recursively: true)
for i in 1...15 {
let randomPosition = SCNVector3(x: Float(i) * 3.5, y: 0.15, z: sign * Float(arc4random_uniform(UInt32(Int(playgroundZ/2 - 2.0))) + 1))
let randomColor = randomColors[Int(arc4random_uniform(UInt32(3)))]
let obstacleCopy = obstacle?.clone()
obstacleCopy?.position = randomPosition
obstacleCopy?.geometry?.materials.first?.diffuse.contents = randomColor
obstacleCopy?.eulerAngles = SCNVector3(x: 10.0 * Float(i), y: Float(30 - i), z: 5.0 * Float(i) * sign) //malo na random
obstacleCopy?.physicsBody = SCNPhysicsBody(type: .dynamic, shape: nil)
obstacleCopy?.physicsBody?.isAffectedByGravity = false
obstacleCopy?.physicsBody?.categoryBitMask = PhysicsCategory.obstacle
obstacleCopy?.physicsBody?.collisionBitMask = PhysicsCategory.none
obstacleCopy?.physicsBody?.contactTestBitMask = PhysicsCategory.car1 | PhysicsCategory.car2 | PhysicsCategory.barrier
obstacleArray.append(obstacleCopy!)
raceScene!.rootNode.addChildNode(obstacleCopy!)
}
I want to set different attributes on those objects but can't, because their geometry is shared.I tried doing this with copying the object and cloning it, but I couldn't see any differences with copying or cloning it.
Is there a property which you can use to achieve geometry unsharing, similar to the option in the Scene Editor, OR should the method of also copying object's geometry and its materials work?

According to the clone() API reference, you can copy the geometry after cloning, this will create a new unshared geometry for your node.
let newNode = node.clone()
newNode.geometry = node.geometry?.copy() as? SCNGeometry
The material attached to the copied geometry is still the same one being used on the original, so any changes will still affect both.
Either create a new material, or make a copy.
if let newMaterial = newNode.geometry?.materials.first.copy() as? SCNMaterial {
//make changes to material
newNode.geometry?.materials = [newMaterial]
}

You could deep clone a SCNNode, which creates an unshared geometry and unshared materials with the original.
fileprivate func deepCopyNode(node: SCNNode) -> SCNNode {
let clone = node.clone()
clone.geometry = node.geometry?.copy() as? SCNGeometry
if let g = node.geometry {
clone.geometry?.materials = g.materials.map{ $0.copy() as! SCNMaterial }
}
return clone
}

Related

I want to reduce the code in methods that are basically identical and pass in the parameters but don't know where to begin?

I have a spriteKit project where I have many characters across several scenes.
As a beginner I just built each one individually for each scene - which makes for a ton of extra code.
I know I could clean this up with a "Build character class" or something like that...
I am just not sure where to begin.
Here is code from two of the characters in one scene...but imagine 5-10 characters per scene?
Also is there a way a property list could be useful for storing these type of properties?
//BEAR
func buildBear() {
let bearAnimatedAtlas = SKTextureAtlas(named: "Bear")
var bearFrames: [SKTexture] = []
let numImages = bearAnimatedAtlas.textureNames.count
for i in 1...numImages {
let bearTextureName = "bear\(i)"
bearFrames.append(bearAnimatedAtlas.textureNamed(bearTextureName))
}
animatedBear = bearFrames
let firstFrameTexture = animatedBear[0]
bear = SKSpriteNode(texture: firstFrameTexture)
bear.size.height = 370
bear.size.width = 370
bear.position = CGPoint(x: 295, y: 25)
bear.zPosition = 1
bear.name = "bear"
isUserInteractionEnabled = true
addChild(bear)
}
//CAT
func buildCat() {
let catAnimatedAtlas = SKTextureAtlas(named: "Cat")
var catFrames: [SKTexture] = []
let numImages = catAnimatedAtlas.textureNames.count
for i in 1...numImages {
let catTextureName = "cat\(i)"
catFrames.append(catAnimatedAtlas.textureNamed(catTextureName))
}
animatedCat = catFrames
let firstFrameTexture = animatedCat[0]
cat = SKSpriteNode(texture: firstFrameTexture)
cat.size.height = 240
cat.size.width = 240
cat.position = CGPoint(x: 134, y: -38)
cat.zPosition = 2
cat.name = "cat"
isUserInteractionEnabled = true
addChild(cat)
}
How could I clean up something like this - I need different position/size per scene but I imagine I could just override that per scene?
I know I know how to do this! - just not where to start?
Gimme a nudge please!
One of the confusing things about the existence of so many languages is that they each have their own jargon, and their own conventions. The root of your problem, however, has nothing to do with Swift or Sprite Kit. When I read your question, I see code that could use some Abstract Data Types. In Java, you would create an Interface, in C++ you would create a "pure virtual" class. Well a rose by any other name still gets the job done. I recommend creating a Protocol, perhaps called Spritable, to define the types of objects that you intend to build into sprites. It would probably be as simple as this:
protocol Spritable {
var species: String { get }
var height: Int { get }
var width: Int { get }
}
The only other thing that differs between your two functions appears to be the starting position. Since this is not inherent in the meaning of a Spritable object, I would package that data separately. A tuple should do the job. With these revisions, your two functions can be merged into one:
func buildSprite(of creature: Spritable, at position: (x: Int, y: Int, z: Int)) {
let spriteAnimatedAtlas = SKTextureAtlas(named: creature.species)
var spriteFrames: [SKTexture] = []
let numImages = spriteAnimatedAtlas.textureNames.count
for i in 1...numImages {
let spriteTextureName = "\(creature.species.lowercased())\(i)"
spriteFrames.append(spriteAnimatedAtlas.textureNamed(spriteTextureName))
}
animatedSprite = spriteFrames
let firstFrameTexture = animatedSprite[0]
sprite = SKSpriteNode(texture: firstFrameTexture)
sprite.size.height = creature.height
sprite.size.width = creature.width
sprite.position = CGPoint(x: position.x, y: position.y)
sprite.zPosition = position.z
sprite.name = creature.species
isUserInteractionEnabled = true
addChild(sprite)
}
To build a bear, aside from a workshop, you will need to define a struct that implements Spritable:
struct Bear: Spritable {
var species: String { return "Bear" }
var height: Int
var width: Int
init(height: Int, width: Int) {
self.height = height
self.width = width
}
}
Then here would be your function call:
buildSprite(of: Bear(height: 370, width: 370), at: (295, 25, 1))
This is a pretty simple example, and could be solved in a few simpler ways than this. However, I find that the larger a project gets, the greater the benefits of organizing code around Abstract Data Types become, so it's worth taking that approach even in a simple case like this.
I ended up not using a protocol on this.
I simply built a method to construct the sprites - similar to what #TallChuck suggested.
func buildCharacter(name:String, height: CGFloat, width: CGFloat, position: CGPoint, zPosition: CGFloat) {
let animatedAtlas = SKTextureAtlas(named: name)
var animationFrames: [SKTexture] = []
let numImages = animatedAtlas.textureNames.count
for i in 1...numImages {
let textureName = "\(name)\(i)"
animationFrames.append(animatedAtlas.textureNamed(textureName))
}
animatedCharacter = animationFrames
let firstFrameTexture = animatedCharacter[0]
builtCharacter = SKSpriteNode(texture: firstFrameTexture)
builtCharacter.size.height = height
builtCharacter.size.width = width
builtCharacter.position = position
builtCharacter.zPosition = zPosition
builtCharacter.name = name
isUserInteractionEnabled = true
addChild(builtCharacter)
}
It works perfect for building and adding to the scenes - had some issues accessing the nodes names for touch detection but got it sorted. Now trying to figure out how to call actions on the nodes - its all different than the normal way. So I will prolly ask that question next. But overall reduced a ton of repeated code! Thanks for the help.
None of this has to be done in code. With Sprite Kit, you create your bear and your cat via the sprite kit editor, and then you load in the sks file by using the constructor that loads by filename.
This is a similar behavior to how game objects work in unity.

How to place 3D object at GPS coordinates with ARkit

Working with Project Dent trying to put 3d object at absolute GPS coordinates.
Readme shows how to put a 2D information annotation object into AR space, but I can't get it to place 3D object at GPS coordinates
Project Dent doesn't use standard SceneView, which makes it hard to try and do this based on a lot of the tutorials out there. It uses SceneLocationView based on ARCL
Here's the sample code for a 2D annotation
let coordinate = CLLocationCoordinate2D(latitude: 51.504571, longitude: -0.019717)
let location = CLLocation(coordinate: coordinate, altitude: 300)
let view = UIView() // or a custom UIView subclass
let annotationNode = LocationAnnotationNode(location: location, view: view)
sceneLocationView.addLocationNodeWithConfirmedLocation(locationNode: annotationNode)
Here's what I've been trying to do to get it to work with a 3D object
let coordinate = CLLocationCoordinate2D(latitude: 51.504571, longitude: -0.019717)
let location = CLLocation(coordinate: coordinate, altitude: 300)
let box = SCNBox(width: 0.1, height: 0.1, length: 0.1, chamferRadius: 0)
let objectNode = LocationNode(location: location, SCNbox: box)
sceneLocationView.addLocationNodeWithConfirmedLocation(locationNode: objectNode)
Ideally, I'd like this code to simply place 3d box at these GPS coordinates in AR space.
Sadly, I can't even get it to build at present.
As an update to this, I've done the following. Create a new class in Nodes, based on LocationNode, called ThreeDNode -
open class ThreeDNode: LocationNode {
// Class for placing 3d objects in AR space
public let threeDObjectNode: LocationNode
public init(location: CLLocation?, scene: SCNScene) {
let boxGeometry = SCNBox(width: 1, height: 1, length: 1, chamferRadius: 0)
//let boxNode = SCNNode(geometry: boxGeometry)
threeDObjectNode = LocationNode(location: location)
threeDObjectNode.geometry = boxGeometry
threeDObjectNode.removeFlicker()
super.init(location: location)
addChildNode(threeDObjectNode)
}
required public init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
and then in POIViewController, tried to place 3d object in AR space with following code -
//example using 3d box object
let coordinate2 = CLLocationCoordinate2D(latitude: 52.010339, longitude: -8.351157)
let location2 = CLLocation(coordinate: coordinate2, altitude: 300)
let asset = SCNScene(named: "art.scnassets/ship.scn")!
let object = ThreeDNode(location: location2, scene: asset)
//add to scene with confirmed location
sceneLocationView.addLocationNodeWithConfirmedLocation(locationNode: object)
No joy :( Any help, much appreciated.
You'll want to create a location node, and add your box as its geometry, or as a child node:
let boxNode = SCNNode(geometry: box)
let locationNode = LocationNode(location: location)
locationNode.addChildNode(boxNode)
sceneLocationView.addLocationNodeWithConfirmedLocation(locationNode: locationNode)
I didn't run this code in Xcode so you may need to tweak.

SceneKit Particle Systems in a Swift Playground

Usually I instantiate SCNParticleSystems by using the file initializer like this:
var stars = SCNParticleSystem(named: "Stars.sncp", inDirectory: nil)
However this project requires a Swift Playground and when I try to use that init function with systems stored in the playground's Resources folder it returns nil (even if I change the specified directory to "Resources" or "/Resources" etc. etc. ).
Are Playground resource paths handled differently to normal apps or am I making a really stupid filenaming mistake?
In Xcode 11 and later there's no preconfigured .scnp Particle System file. Instead you can use Particle System object coming directly from Xcode library.
Or, as always, you can create a particle system in Xcode Playground programmatically.
About Swift Playgrounds for iPad read here.
Here's a code:
import PlaygroundSupport
import SceneKit
let rectangle = CGRect(x: 0, y: 0, width: 1000, height: 200)
var sceneView = SCNView(frame: rectangle)
var scene = SCNScene()
sceneView.scene = scene
sceneView.backgroundColor = .black
let cameraNode = SCNNode()
cameraNode.camera = SCNCamera()
cameraNode.position.z = 70
sceneView.scene!.rootNode.addChildNode(cameraNode)
let particleSystem = SCNParticleSystem()
particleSystem.birthRate = 500
particleSystem.particleLifeSpan = 0.5
particleSystem.particleColor = .systemIndigo
particleSystem.speedFactor = 7
particleSystem.emittingDirection = SCNVector3(1,1,1)
particleSystem.emitterShape = .some(SCNSphere(radius: 15))
let particlesNode = SCNNode()
particlesNode.scale = SCNVector3(2,2,2)
particlesNode.addParticleSystem(particleSystem)
sceneView.scene!.rootNode.addChildNode(particlesNode)
PlaygroundPage.current.liveView = sceneView
I think you're doing a mistake in filename extension. It is .scnp and not .sncp.
Either try without any extension -
var stars = SCNParticleSystem(named: "Stars", inDirectory: nil)
or try with correct extension -
var stars = SCNParticleSystem(named: "Stars.scnp", inDirectory: nil)

ARKIT - How to stick two objects

I have two objects (two cubes). First, I add to the scene the first cube. Then I want to add the second one and I want it to be stuck to the first one, on one side of the first one - I will select which side by clicking on it (like in the image below). Is it possible to just click a face of the first cube and the second one to automatically appear into the scene and stick to the first cube? I cannot figure how to do this.
Photo
When you create an SCNBoxGeometry:
The SCNBox class automatically creates SCNGeometryElement objects as
needed to handle the number of materials.
As such in order to access these elements you would need to create an SCNMaterial for each face of the Box. Then you can perform an SCNHitTest to detect which face has been detected:
When you perform a hit-test search, SceneKit looks for SCNGeometry
objects along the ray you specify. For each intersection between the
ray and and a geometry, SceneKit creates a hit-test result to provide
information about both the SCNNode object containing the geometry and
the location of the intersection on the geometry’s surface.
So how would we approach this?
Lets assume you have created to SCNNodes called:
var cubeOne = SCNNode()
var cubeTwo = SCNNode()
These are both assigned 6 different SCNMaterials (one for each face) like so:
override func viewDidLoad() {
super.viewDidLoad()
cubeOne.geometry = cubeGeometry()
cubeOne.position = SCNVector3(0, -0.5, -1.5)
cubeOne.name = "Cube One"
cubeTwo.geometry = cubeGeometry2()
cubeTwo.position = SCNVector3(30, 0, -1.5)
cubeTwo.name = "Cube Two"
self.augmentedRealityView.scene.rootNode.addChildNode(cubeOne)
self.augmentedRealityView.scene.rootNode.addChildNode(cubeTwo)
}
/// Returns The 6 Faces Of An SCNBox
///
/// - Returns: SCNGeometry
func cubeGeometry() -> SCNGeometry{
var colours: [UIColor] = [.red, .green, .cyan, .yellow, .purple, .blue]
var faces = [SCNMaterial] ()
let cubeGeometry = SCNBox(width: 0.2, height: 0.2, length: 0.2, chamferRadius: 0)
for faceIndex in 0..<5{
let material = SCNMaterial()
material.diffuse.contents = colours[faceIndex]
faces.append(material)
}
cubeGeometry.materials = faces
return cubeGeometry
}
Now you have assigned 6 materials to the faces, you will have six geometry elements which correspond to each side of your SCNBox.
Now having done this lets quickly create an enum which corresponds to the order of the faces:
enum BoxFaces: Int{
case Front, Right, Back, Left, Top, Botton
}
Now when we perform a hitTest we can log the location of the hit e.g:
/// Detects Which Cube Was Detected & Logs The Geometry Index
///
/// - Parameter gesture: UITapGestureRecognizer
#IBAction func cubeTapped(_ gesture: UITapGestureRecognizer){
//1. Get The Current Touch Location
let currentTouchLocation = gesture.location(in: self.augmentedRealityView)
//2. Perform An SCNHitTest
guard let hitTest = self.augmentedRealityView.hitTest(currentTouchLocation, options: nil).first else { return }
//3. If The Node In Cube One Then Get The Index Of The Touched Material
if let namedNode = hitTest.node.name{
if namedNode == "Cube One"{
//4. Get The Geometry Index
if let faceIndex = BoxFaces(rawValue: hitTest.geometryIndex){
print("User Has Hit \(faceIndex)")
//5. Position The Second Cube
positionStickyNode(faceIndex)
}
}
}
}
In part 5 you will notice the call to the function positionStickyNode which places the secondCube at the corresponding location of the 1st Cube:
/// Position The Second Cube Based On The Face Tapped
///
/// - Parameter index: BoxFaces
func positionStickyNode(_ index: BoxFaces){
let (min, max) = cubeTwo.boundingBox
let cubeTwoWidth = max.x - min.x
let cubeTwoHeight = max.y - min.y
switch index {
case .Front:
cubeTwo.simdPosition = float3(cubeOne.position.x, cubeOne.position.y, cubeOne.position.z + cubeTwoWidth)
case .Right:
cubeTwo.simdPosition = float3(cubeOne.position.x + cubeTwoWidth, cubeOne.position.y, cubeOne.position.z)
case .Back:
cubeTwo.simdPosition = float3(cubeOne.position.x, cubeOne.position.y, cubeOne.position.z - cubeTwoWidth)
case .Left:
cubeTwo.simdPosition = float3(cubeOne.position.x - cubeTwoWidth, cubeOne.position.y, cubeOne.position.z)
case .Top:
cubeTwo.simdPosition = float3(cubeOne.position.x, cubeOne.position.y + cubeTwoHeight, cubeOne.position.z)
case .Botton:
cubeTwo.simdPosition = float3(cubeOne.position.x, cubeOne.position.y - cubeTwoHeight, cubeOne.position.z)
}
This is a very crude example and will work when your cubes are the same size... You have more than enough however, to now figure out how this would work for different sizes etc.
Hope it helps...

Import 3D model in SceneKit - Swift

I'm creating a game where the player can control a cube.
He could buy new cubes.
I created the basic cube like that :
// MAIN CUBE
mainCubeGeometry = SCNBox(width: 0.2, height: 0.2, length: 0.2, chamferRadius: 0.0)
mainCubeGeometry.firstMaterial?.diffuse.contents = UIColor.redColor()
mainCubeNode = SCNNode(geometry: mainCubeGeometry)
mainCubeNode.position = SCNVector3(x: 0.0, y: 0.0, z: 0.0)
scene.rootNode.addChildNode(mainCubeNode)
I would like to know how use 3D models (dae) of cubes created on any software (SketchUp per exemple). I don't really understand how to load the 3D file when the scene is created (scene = SCNScene(named: "3d.scnassets/cube.dae")) because it is a scene, so it import a scene into my original scene, while I just want to import a 3D model in a node to use it like my basic cube.
Thanks for your help !
SCNScene(named:) returns an SCNNode with all of the objects from that DAE file as child nodes. Load the file, retrieve the bodies you're interested in, and add them to your scene's root node.
Here are a couple of snippets from the Fox sample app from WWDC 2015. The Character class has a node property initialized like this:
let characterScene = SCNScene(named: "game.scnassets/panda.scn")!
let characterTopLevelNode = characterScene.rootNode.childNodes[0]
node.addChildNode(characterTopLevelNode)
And then add the character to the scene like this:
// Add the character to the scene.
scene.rootNode.addChildNode(character.node)
SOLUTION
I found a solution on this post : How do you load a .dae file into an SCNNode in IOS SceneKit?
class func collada2SCNNode(filepath:String) -> SCNNode {
var node = SCNNode()
let scene = SCNScene(named: filepath)
var nodeArray = scene!.rootNode.childNodes
for childNode in nodeArray {
node.addChildNode(childNode as SCNNode)
}
return node
}
Use :
let myNode = collada2SCNNode("nodes.scnassets/node.dae")
scene.rootNode.addChildNode(myNode)