I'm currently developing an iOS app that uses ARKit and SceneKit for the augmented reality.
I'm having a problem while loading an .usdz model into the scene.
I can load it correctly into the scene, but, when I try to get the node name (in which I loaded the .usdz model) after tapping on it, it returns the .usdz name and not the name I gave to it.
The code I use to load the .usdz model is:
let mdlAsset = MDLAsset(url: urlPath)
mdlAsset.loadTextures()
let asset = mdlAsset.object(at: 0) // extract first object
var assetNode = SCNNode(mdlObject: asset)
assetNode = SCNNode(mdlObject: asset)
assetNode.name = "Node-2"
sceneView.scene.rootNode.addChildNode(assetNode)
To capture the tap on the node, the code is:
#objc func handleTap(recognizer: UITapGestureRecognizer){
let location = recognizer.location(in: sceneView)
let results = sceneView.hitTest(location, options: nil)
guard recognizer.state == .ended else { return }
if results.count > 0 {
let result = results[0] as SCNHitTestResult
let node = result.node
print(node.name)
}
}
As I mentioned before, when I tap on the object it prints
Optional("Sphere_0")
value that can be found in the top right corner, in the model details page.
The correct value that I expected was "Node-2".
The name of your USDZ model isn't overridden. It's the peculiarities of SceneKit hit-testing and scene hierarchy. When you perform a hit-test search, SceneKit looks for SCNGeometry objects (not a main node) along the ray you specify. So, all you need to do, once the hit-test is completed, is to find the corresponding parent nodes.
Try this code:
import SceneKit.ModelIO
class GameViewController: UIViewController {
var sceneView: SCNView!
override func viewDidLoad() {
super.viewDidLoad()
sceneView = (self.view as! SCNView)
let scene = SCNScene(named: "art.scnassets/ship.scn")!
sceneView.scene = SCNScene()
sceneView.backgroundColor = .black
let recog = UITapGestureRecognizer(target: self, action: #selector(tap))
sceneView.addGestureRecognizer(recog)
// ASSET
let mdlAsset = MDLAsset(scnScene: scene)
let asset = mdlAsset.object(at: 0)
let node = SCNNode(mdlObject: asset.children[0])
node.name = "Main-Node-Name" // former "ship"
node.childNodes[0].name = "SubNode-Name" // former "shipMesh"
node.childNodes[0].childNodes[0].name = "Geo-Name" // former "Scrap_MeshShape"
sceneView.scene?.rootNode.addChildNode(node)
}
}
And your hit-testing method:
extension GameViewController {
#objc func tap(recognizer: UITapGestureRecognizer) {
let location = recognizer.location(in: sceneView)
let results = sceneView.hitTest(location)
guard recognizer.state == .ended else { return }
if results.count > 0 {
let result = results[0] as SCNHitTestResult
let node = result.node
print(node.name!) // Geo-Name
print(node.parent!.name!) // SubNode-Name
print(node.parent!.parent!.name!) // Main-Node-Name
}
}
}
Related
I loaded into SceneKit a .usdz file which has an animation attached to it. I want to stop this animation to play.. but I can't find the right way.
I load the .usdz asset file with the following method:
func loadIdle() {
let urlfile = Bundle.main.path(forResource: "armi_idle",
ofType: "usdz",
inDirectory: "Asset.scnassets")!
let scene = try! SCNScene(url: URL(string: urlfile)!)
guard let findNode = scene.rootNode.childNode(withName: "armi_idle",
recursively: true)
else {
print("err finde idle")
return
}
// try to pause, but not work
findNode.isPaused = true
// add to main scene
self.scene.rootNode.addChildNode(findNode)
}
A picture of the asset:
Declare the following properties:
import SceneKit
#IBOutlet var sceneView: SCNView!
var notWalking: Bool = true
var animations = [String: CAAnimation]()
var node = SCNNode()
override func viewDidLoad() {
super.viewDidLoad()
sceneView.scene = SCNScene()
self.animation()
}
Then try the following logic (you need Idle and Walking files) :
fileprivate func loadAnimation(withKey: String, scene: String, id: String) {
let url = Bundle.main.url(forResource: scene, withExtension: "dae")!
let source = SCNSceneSource(url: url, options: nil)
guard let character = source?.entryWithIdentifier(id,
withClass: CAAnimation.self) else { return }
character.fadeInDuration = 0.5
character.fadeOutDuration = 0.5
self.animations[withKey] = character
}
then:
fileprivate func animation() {
let standStill = SCNScene(named: "art.scnassets/Idle")!
for childNode in standStill.rootNode.childNodes {
self.node.addChildNode(childNode)
}
sceneView.scene.rootNode.addChildNode(self.node)
self.loadAnimation(withKey: "walking", scene: "art.scnassets/Walking",
id: "Walking-1")
}
and then:
func playWalking(key: String) {
sceneView.scene.rootNode.addAnimation(animations[key]!, forKey: key)
}
func stopWalking(key: String) {
sceneView.scene.rootNode.removeAnimation(forKey: key,
blendOutDuration: 0.75)
}
and at last:
#IBAction func pressed(_ sender: UIButton) {
notWalking ? playWalking(key: "walking") : stopWalking(key: "walking")
notWalking.toggle()
}
I am attempting to delete the .scn objects I placed down. However, with my current code, it is just deleting individual nodes. Here is how I handle the tap delete.
#objc func Erase(sender: UITapGestureRecognizer){
print("rendering")
//sharedVM.count = sharedVM.count + 1
guard let pointOfView = sceneView.pointOfView else {return}
guard let cameraPosition = getCameraPosition(in: sceneView) else {
return
}
let location = sender.location(in: view)
let currentPositionOfCamera = cameraPosition + getRay(for: location, in: sceneView)
DispatchQueue.main.async{
//guard let location = touches.first?.location(in: sceneView) else { return }
let results = self.sceneView.hitTest(location, options: [SCNHitTestOption.searchMode : 1])
for result in results { /// See if the beam hit the cube
let Node = result.node
Node.enumerateChildNodes { (node, stop) in
node.removeFromParentNode() }
Node.removeFromParentNode()
}
}
}
Here is how I place the object:
var objecttest = VirtualObject(url: referenceURL)!
//var objecttest = VirtualObject(url: URL(string: "Models.scnassets/cup/cup.scn")!)
objecttest.load()
self.sceneView.scene.rootNode.addChildNode(objecttest)
class VirtualObject: SCNReferenceNode {
...
}
I've created an AR app that works pretty well, but I'm not a hug fan of the objects spawning in front of the camera every time. I would prefer to have them spawn in this field, further out for the camera, and facing predetermined directions. For example, I want a car to spawn in the same parking lot space every time, so when I walk out into the lot, I can see the car parked there like I left it, no matter which way I come at it from.
How can I spawn my objects based on their location? I would think it would have to do with replacing the plane detection with latitude and longitude coordinates, but I don't know how to go about this.
Any help is greatly appreciated!
import UIKit
import RealityKit
import ARKit
class ViewController: UIViewController {
#IBOutlet var arView: ARView!
override func viewDidLoad() {
super.viewDidLoad()
arView.session.delegate = self
showModel()
overlayCoachingView()
setupARView()
arView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(handleTap(recognizer:))))
}
func showModel(){
let anchorEntity = AnchorEntity(plane: .horizontal, minimumBounds:[0.2, 0.2])
let entity = try! Entity.loadModel(named: "COW_ANIMATIONS")
entity.setParent(anchorEntity)
arView.scene.addAnchor(anchorEntity)
}
func overlayCoachingView () {
let coachingView = ARCoachingOverlayView(frame: CGRect(x: 0, y: 0, width: arView.frame.width, height: arView.frame.height))
coachingView.session = arView.session
coachingView.activatesAutomatically = true
coachingView.goal = .horizontalPlane
view.addSubview(coachingView)
}
// Load the "Box" scene from the "Experience" Reality File
// let boxAnchor = try! Experience.loadBox()
// Add the box anchor to the scene
//arView.scene.anchors.append(boxAnchor)
func setupARView(){
arView.automaticallyConfigureSession = false
let configuration = ARWorldTrackingConfiguration()
configuration.planeDetection = [.horizontal, .vertical]
configuration.environmentTexturing = .automatic
arView.session.run(configuration)
}
//object placement
#objc
func handleTap(recognizer: UITapGestureRecognizer){
let location = recognizer.location(in:arView)
let results = arView.raycast(from: location, allowing: .estimatedPlane, alignment: .horizontal)
if let firstResult = results.first {
let anchor = ARAnchor(name: "COW_ANIMATIONS", transform: firstResult.worldTransform)
arView.session.add(anchor: anchor)
} else {
print("Object placement failed - couldn't find surface.")
}
}
func placeObject(named entityName: String, for anchor: ARAnchor) {
let entity = try! ModelEntity.loadModel(named: entityName)
entity.generateCollisionShapes(recursive: true)
arView.installGestures([.rotation, .translation], for: entity)
let anchorEntity = AnchorEntity(anchor: anchor)
anchorEntity.addChild(entity)
arView.scene.addAnchor(anchorEntity)
}
}
extension ViewController: ARSessionDelegate {
func session( session: ARSession, didAdd anchors: [ARAnchor]) {
for anchor in anchors {
if let anchorName = anchor.name, anchorName == "COW_ANIMATIONS" {
placeObject(named: anchorName, for: anchor)
} }
}
}
For geo location Apple made ARGeoTrackingConfiguration with corresponding ARGeoAnchors.
let location = CLLocationCoordinate2D(latitude: -18.9137, longitude: 47.5361)
let geoAnchor = ARGeoAnchor(name: "Tana", coordinate: location, altitude: 1250)
arView.session.add(anchor: geoAnchor)
let realityKitAnchor = AnchorEntity(anchor: geoAnchor)
arView.scene.anchors.append(realityKitAnchor)
At the moment it's working in the current cities and areas.
You can also use getGeoLocation(forPoint:completionHandler:) instance method that converts a position in the framework’s local coordinate system to GPS latitude, longitude and altitude.
arView.session.getGeoLocation(forPoint: xyzWorld) { (coord, alt, error) in
let anchor = ARGeoAnchor(coordinate: coord, altitude: alt)
}
I have a subclass "ExSCNNode" of SCNNode to add more properties and behavior to the SCNNode.
class ExSCNNode : SCNNode {
...
}
I than build a scene with a ExSCNNode.
let testnode = ExSCNNode()
When hittesting the scene:
// check what nodes are tapped
let p = gestureRecognize.location(in: scnView)
let hitResults = scnView.hitTest(p, options: [:])
// check that we clicked on at least one object
if hitResults.count > 0 {
for hit in hitResults {
let hitnode = hit.node
...
hitnode is a SCNNode not a ExSCNNode.
But I want to get the ExSCNNode to access the advance functionality.
How do I get access to the subclass instead of the SCNNode class ?
Just cast the object to your subclass:
// check what nodes are tapped
let p = gestureRecognize.location(in: scnView)
let hitResults = scnView.hitTest(p, options: [:])
for hit in hitResults {
if let hitnode = hit.node as? ExSCNNode {
…
}
I have a swift game that uses SpriteKit. I made a game but now I want to create a menu scene. So I changed the GameViewController to load MenuScene.swift like so
extension SKNode {
class func unarchiveFromFile(file : NSString) -> SKNode? {
if let path = NSBundle.mainBundle().pathForResource(file, ofType: "sks") {
var sceneData = NSData(contentsOfFile: path, options: .DataReadingMappedIfSafe, error: nil)!
var archiver = NSKeyedUnarchiver(forReadingWithData: sceneData)
archiver.setClass(self.classForKeyedUnarchiver(), forClassName: "SKScene")
let scene = archiver.decodeObjectForKey(NSKeyedArchiveRootObjectKey) as MenuScene
archiver.finishDecoding()
return scene
} else {
return nil
}
}
}
class GameViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
if let scene = MenuScene.unarchiveFromFile("MenuScene") as? MenuScene {
//if scene = MenuScene.unarchiveFromFile("MenuScene") as? MenuScene {
// Configure the view.
let skView = self.view as SKView
skView.showsFPS = true
skView.showsNodeCount = true
/* Sprite Kit applies additional optimizations to improve rendering performance */
skView.ignoresSiblingOrder = true
/* Set the scale mode to scale to fit the window */
scene.scaleMode = .AspectFill
scene.size = skView.bounds.size
skView.presentScene(scene)
}
}
It loads MenuScene just fine and I created a play button but now I want to transition to gamescene when clicked so I put in this code
override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
/* Called when a touch begins */
for touch: AnyObject in touches {
let location = touch.locationInNode(self)
if(play.containsPoint(location)) {
gamescene = GameScene(size: self.scene!.size)
self.view!.presentScene(gamescene, transition:reveal)
}
}
}
I have tried numerous other attempts but each time it has crashed into AppDelegate.swift with the error: Thread 1: EXC_BAD_ACCESS(code=EXC_I386_GPFLT)
So what am I doing wrong?
the problem is in sknode extension part.
extension SKNode {
class func unarchiveFromFile(file : NSString) -> SKNode? {
if let path = NSBundle.mainBundle().pathForResource(file, ofType: "sks") {
var sceneData = NSData(contentsOfFile: path, options: .DataReadingMappedIfSafe, error: nil)!
var archiver = NSKeyedUnarchiver(forReadingWithData: sceneData)
archiver.setClass(self.classForKeyedUnarchiver(), forClassName: "SKScene")
/*
Below Line is the problem. you create an object with gameScene object
type However in here you call it as MenuScene.
*/
let scene = archiver.decodeObjectForKey(NSKeyedArchiveRootObjectKey) as MenuScene
archiver.finishDecoding()
return scene
} else {
return nil
}
}
}
it will be fixed with creating new skNode extension and modified it . like as:
extension SKNode {
class func unarchiveFromFile2(file : NSString) -> SKNode? {
if let path = NSBundle.mainBundle().pathForResource(file, ofType: "sks") {
var sceneData = NSData(contentsOfFile: path, options: .DataReadingMappedIfSafe, error: nil)!
var archiver = NSKeyedUnarchiver(forReadingWithData: sceneData)
archiver.setClass(self.classForKeyedUnarchiver(), forClassName: "SKScene")
let scene = archiver.decodeObjectForKey(NSKeyedArchiveRootObjectKey) as gameScene
archiver.finishDecoding()
return scene
} else {
return nil
}
}
}
Its very late answer but there must be solution..
Use this code for change your scene :
func changeScene(){
let scene = NameScene(size: self.view!.bounds.size)
scene.backgroundColor = SKColor(red: 1, green: 1, blue: 1, alpha: 1)
scene.scaleMode = .AspectFill
scene.backgroundColor = UIColor.whiteColor()
let transition = SKTransition.pushWithDirection(SKTransitionDirection.Left, duration:0.5)
self.scene?.view?.presentScene(scene, transition: transition)
}
It turns out I had a particle emitter in the menu scene. I forgot to remove it when transitioning. So it was trying to emitting to a scene that did not exist anymore. Fixing it was as simple as adding this line before transitioning to the game scene.
myparticle.removeFromParent();