SceneKit: Create SCNNodes from models at runtime - ios

I am making a SceneKit game, and I have a folder with my models (file.obj, file.mtl, file.png). I can drag the models to the game.scn file.
let node = rootNode.childNode(withName: "boxTarget", recursively: true)!.flattenedClone()
node.isHidden = false
Then I look for the name of the node and I create a flattenedClone.
But I think that there will be a better way to create multiple SCNNodes with models, dynamically, at runtime, without adding them to the game.scn file.

If I understand the question correctly, I did mine something like this:
func initGameNodes() {
scene = SCNScene()
gameNodes = SCNNode()
gameNodes.name = "gameNodes"
scene.rootNode.addChildNode(gameNodes)
initLights()
scene.rootNode.addChildNode(camera.cameraEye)
scene.rootNode.addChildNode(camera.cameraFocus)
camera.reset()
}
func loadCollada(sceneName: String, objName: String) -> SCNNode {
let vScene = SCNScene(named: sceneName)!
let gObject = vScene.rootNode.childNode(withName: objName, recursively: true)!
return gObject
}
func createProjectileNodes(vMissile: weaponTypes) -> SCNNode {
switch vMissile
{
case .defenseMissile:
let vNode = loadCollada(sceneName: "art.scnassets/Models/missile.dae", objName: "Default")
vNode.scale = SCNVector3Make(0.05, 0.05, 0.05)
vNode.name = "Missile01"
return vNode
case . etc.
}
}

Related

Inverse Kinematics in Arkit

I have Scenekit model with 4 joints.
When I use it in SCNView with the code below and change the position of the parent node (Joint) I can see it animated according to the joints.
private lazy var scene: SCNScene = {
let scene = SCNScene(named: "art.scnassets/ears")!
return scene
}()
in viewDidLoad
let cameraNode = SCNNode()
cameraNode.camera = SCNCamera()
cameraNode.position = SCNVector3(x: 0, y: 0, z: 5)
rootNode.addChildNode(cameraNode)
scnView.scene = scene
scnView.allowsCameraControl = true
scnView.backgroundColor = .white
let joint = contentNode!.childNode(withName: "Joint", recursively: true)!
var ik:SCNIKConstraint = .inverseKinematicsConstraint(chainRootNode: joint)
joint.childNode(withName: "head", recursively: true)!.constraints = [ik]
The problem occurs when I use the same model in Arkit. In this case, I use a separate View Controller than above. I place it on the head. The model collapse when I show it in Arkit with the code below. I expect it to track head movement and bend according to it.
func renderer(_ renderer: SCNSceneRenderer, nodeFor anchor: ARAnchor) -> SCNNode? {
guard let sceneView = renderer as? ARSCNView,
anchor is ARFaceAnchor else { return nil }
faceGeometry = ARSCNFaceGeometry(device: sceneView.device!)!
contentNode = SCNReferenceNode(named: "art/ears")
//contentNode!.physicsBody = .kinematic()
let joint = contentNode!.childNode(withName: "Joint", recursively: true)!
var ik:SCNIKConstraint = .inverseKinematicsConstraint(chainRootNode: joint)
joint.childNode(withName: "head", recursively: true)!.constraints = [ik]
return contentNode
}
I have this extension
extension SCNReferenceNode {
convenience init(named resourceName: String, loadImmediately: Bool = true) {
let url = Bundle.main.url(forResource: resourceName, withExtension: "scn", subdirectory: "Models.scnassets")!
self.init(url: url)!
if loadImmediately {
self.load()
}
}
Normal look of model is like this
Correctly applied inverse kinematics snapshot
I tried setting setMaxAllowedRotationAngle(between 20-45 degrees) to the joints, this prevents collapse behavior but it also prevents bending.
y property of model gravity is -9.8, x and z are 0.
What might be the problem that I can't create the same effect in Arkit?

How to download ARImage (.SCN) file to display image in ARSCNView?

I am trying to download the ARImage(.scn file) In my application. And display in the ARSCNView
Below my code is working fine it's able to display the image. An image is also below
func addARObject(x: Float = 0, y: Float = 0, z: Float = -0.5) {
let aRUrl : URL = URL(string :arImageUrl2)!
do {
let arScene = try SCNScene(url: aRUrl , options: nil)
let arNode = arScene.rootNode.childNode(withName: "chair", recursively: false)
print("x,y,z",x,y,z)
arNode?.position = SCNVector3(x,y,z)
zAxis = z
if isNodeAvailable {
currentNode.position = SCNVector3(x,y,z)
}else{
isNodeAvailable = true
currentNode = arNode
sceneView.scene.rootNode.addChildNode(arNode!)
}
}
catch {
print("errrrrororororor")
}
}
And Output of this is 👇🏻
But chair color is Red and its showing surface white color. but actually, there is no surface.
If the same image I am using In My project folder Without download then chair color is Red.
So Can Anyone Explain to me what's wrong with my code or image issue?
Below Image is when I am using the local File in my Project.
You create a parent node and then you can add all your nodes as children.
var nodes: [SCNNode] = getMyNodes()
var parentNode = SCNNode()
parentNode.name = "chair"
for node in nodes {
parentNode.addChildNodes(node)
}
You can access a specific child using:
parentNode.childNode(withName: nameOfChildNode, recursively: true)
If you are importing from a scene file:
let scene = SCNScene(named: "myScene.scn")
func getMyNodes() -> [SCNNode] {
var nodes: [SCNNode] = [SCNNode]()
for node in scene.rootNode.childNodes {
nodes.append(node)
}
return nodes
}
After getting child node from parent node, you can add texture to the child node.
let childOne = parentNode.childNode(withName: nameOfChildNode, recursively: true)
childOne.?.firstMaterial?.diffuse.contents = UIColor.red

Adding a SCNBillboardConstraint makes the node dissapear

After what I've read in the documentation and on the internet a SCNBillboardConstraint would rotate a node to always face the pointOfView node - in the case of ARKit, the user's camera.
The thing is, when I add a SCNBillboardConstraint to a child node, it dissapears. The nodes are just some SCNTexts added as a subchild of a more complex model.
The hierarchy looks something like this: RootNode - > Text node (two of them).
Just after I added the root node to the scene's root node, I add this constraint in the following way:
updateQueue.async {
self.sceneView.scene.rootNode.addChildNode(virtualObject)
self.sceneView.addOrUpdateAnchor(for: virtualObject)
self.addBillboardContraintsToText(object: virtualObject)
}
func addBillboardContraintsToText(object: VirtualObject) {
guard let storeNode = object.childNodes.first else {
return
}
for node in storeNode.childNodes {
if let geometry = node.geometry, geometry.isKind(of: SCNText.self) {
let billboard = SCNBillboardConstraint()
node.constraints = [billboard]
}
}
}
The text nodes have their position set properly relative to their root node, so there's no problem with that. When I add a SCNLookAtConstraint though, it works just fine.
node.pivot = SCNMatrix4Rotate(node.pivot, Float.pi, 0, 1, 0)
let lookAt = SCNLookAtConstraint(target: sceneView.pointOfView)
lookAt.isGimbalLockEnabled = true
node.constraints = [lookAt]
Any ideas why the SCNBillboardConstraint might not work? Am I doing something wrong?
This Code (with apples CupScn) works just fine for me:
cupNode.position = SCNVector3(0.5,0,-0.5)
guard let virtualObjectScene = SCNScene(named: "cup.scn", inDirectory: "Models.scnassets/cup") else {
return
}
let wrapperNode = SCNNode()
for child in virtualObjectScene.rootNode.childNodes {
child.geometry?.firstMaterial?.lightingModel = .physicallyBased
wrapperNode.addChildNode(child)
}
cupNode.addChildNode(wrapperNode)
scene.rootNode.addChildNode(cupNode)
let billboardConstraint = SCNBillboardConstraint()
billboardConstraint.freeAxes = SCNBillboardAxis.Y
cupNode.constraints = [billboardConstraint]

Load .scn file as SCNNode LOSS animations

When I load Horse.scn to a SCNNode, the animations are lost.
func addHorse(for parentNode:SCNNode, postion:SCNVector3){
guard let virtualObjectScene = SCNScene(named: "Horse.scn") else {
return
}
let wrapperNode = SCNNode()
for child in virtualObjectScene.rootNode.childNodes {
child.geometry?.firstMaterial?.lightingModel = .physicallyBased
child.movabilityHint = .movable
wrapperNode.addChildNode(child)
}
wrapperNode.scale = SCNVector3(0.001,0.001,0.001)
wrapperNode.position = postion;
parentNode.addChildNode(wrapperNode)
}
If I load scene from Horse.dae file, everything is fine. Setting rootNode 's playing property to YES doesn't work.

SpriteKit reference nodes from level editor

I'm using the scene editor in SpriteKit to place color sprites and assign them textures using the Attributes Inspector. My problem is trying to figure out how to reference those sprites from my GameScene file. For example, I'd like to know when a sprite is a certain distance from my main character.
Edit - code added
I'm adding the code because for some reason, appzYourLife's answer worked great in a simple test project, but not in my code. I was able to use Ron Myschuk's answer which I also included in the code below for reference. (Though, as I look at it now I think the array of tuples was overkill on my part.) As you can see, I have a Satellite class with some simple animations. There's a LevelManager class that replaces the nodes from the scene editor with the correct objects. And finally, everything gets added to the world node in GameScene.swift.
Satellite Class
func spawn(parentNode:SKNode, position: CGPoint, size: CGSize = CGSize(width: 50, height: 50)) {
parentNode.addChild(self)
createAnimations()
self.size = size
self.position = position
self.name = "satellite"
self.runAction(satAnimation)
self.physicsBody = SKPhysicsBody(circleOfRadius: size.width / 2)
self.physicsBody?.affectedByGravity = false
self.physicsBody?.categoryBitMask = PhysicsCategory.satellite.rawValue
self.physicsBody?.contactTestBitMask = PhysicsCategory.laser.rawValue
self.physicsBody?.collisionBitMask = 0
}
func createAnimations() {
let flyFrames:[SKTexture] = [textureAtlas.textureNamed("sat1.png"),
textureAtlas.textureNamed("sat2.png")]
let flyAction = SKAction.animateWithTextures(flyFrames, timePerFrame: 0.14)
satAnimation = SKAction.repeatActionForever(flyAction)
let warningFrames:[SKTexture] = [textureAtlas.textureNamed("sat8.png"),
textureAtlas.textureNamed("sat1.png")]
let warningAction = SKAction.animateWithTextures(warningFrames, timePerFrame: 0.14)
warningAnimation = SKAction.repeatActionForever(warningAction)
}
func warning() {
self.runAction(warningAnimation)
}
Level Manager Class
import SpriteKit
class LevelManager
{
let levelNames:[String] = ["Level1"]
var levels:[SKNode] = []
init()
{
for levelFileName in levelNames {
let level = SKNode()
if let levelScene = SKScene(fileNamed: levelFileName) {
for node in levelScene.children {
switch node.name! {
case "satellite":
let satellite = Satellite()
satellite.spawn(level, position: node.position)
default: print("Name error: \(node.name)")
}
}
}
levels.append(level)
}
}
func addLevelsToWorld(world: SKNode)
{
for index in 0...levels.count - 1 {
levels[index].position = CGPoint(x: -2000, y: index * 1000)
world.addChild(levels[index])
}
}
}
GameScene.swift - didMoveToView
world = SKNode()
world.name = "world"
addChild(world)
physicsWorld.contactDelegate = self
levelManager.addLevelsToWorld(self.world)
levelManager.levels[0].position = CGPoint(x:0, y: 0)
//This does not find the satellite nodes
let satellites = children.flatMap { $0 as? Satellite }
//This does work
self.enumerateChildNodesWithName("//*") {
node, stop in
if (node.name == "satellite") {
self.satTuple.0 = node.position
self.satTuple.1 = (node as? SKSpriteNode)!
self.currentSatellite.append(self.satTuple)
}
}
The Obstacle class
First of all you should create an Obstacle class like this.
class Obstacle: SKSpriteNode { }
Now into the scene editor associate the Obstacle class to your obstacles images
The Player class
Do the same for Player, create a class
class Player: SKSpriteNode { }
and associate it to your player sprite.
Checking for collisions
Now into GameScene.swift change the updated method like this
override func update(currentTime: CFTimeInterval) {
/* Called before each frame is rendered */
let obstacles = children.flatMap { $0 as? Obstacle }
let player = childNodeWithName("player") as! Player
let obstacleNearSprite = obstacles.contains { (obstacle) -> Bool in
let distance = hypotf(Float(player.position.x) - Float(obstacle.position.x), Float(player.position.y) - Float(obstacle.position.y))
return distance < 100
}
if obstacleNearSprite {
print("Oh boy!")
}
}
What does it do?
The first line retrieves all your obstacles into the scene.
the second line retrieves the player (and does crash if it's not present).
Next it put into the obstacleNearSprite constant the true value if there is at least one Obstacle at no more then 100 points from Player.
And finally use the obstacleNearSprite to print something.
Optimizations
The updated method gets called 60 times per second. We put these 2 lines into it
let obstacles = children.flatMap { $0 as? Obstacle }
let player = childNodeWithName("player") as! Player
in order to retrieves the sprites we need. With the modern hardware it is not a problem but you should save references to Obstacle and Player instead then searching for them in every frame.
Build a nice game ;)
you will have to loop through the children of the scene and assign them to local objects to use in your code
assuming your objects in your SKS file were named Obstacle1, Obstacle2, Obstacle3
Once in local objects you can check and do whatever you want with them
let obstacle1 = SKSpriteNode()
let obstacle2 = SKSpriteNode()
let obstacle3 = SKSpriteNode()
let obstacle3Location = CGPointZero
func setUpScene() {
self.enumerateChildNodesWithName("//*") {
node, stop in
if (node.name == "Obstacle1") {
self.obstacle1 = node
}
else if (node.name == "Obstacle2") {
self.obstacle2 = node
}
else if (node.name == "Obstacle3") {
self.obstacle3Location = node.position
}
}
}

Resources