Remove complete .scn instead of individual nodes Swift ARKit - swift

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 {
...
}

Related

How to change model's color on tap and then change it back to original?

Im trying to create a functionality in ARKit where if the user taps on the modelEntity it changes its colour to lets say blue so it indicates that it's selected. But then if the user taps on another entity, the previously selected entity's material changes back to what it was before it was selected.
So i am able to change its colour with this code:
let selectedMaterial = SimpleMaterial(color: .link, isMetallic: false)
selectedEntity.model?.materials[0] = selectedMaterial
But how do I change it back after I 'deselect' it, when i select another modelEntity?
Because I've been trying to save it's material to a variable, but I'm having a problem with it, because lets say there are two modelEntites, A and B. When I tap the "A" entity it changes colour, then I tap on the "B" entity then "B" entity's colour changes and the "A" entity's material changes back to the original (like how it should be working), but when I tap again on the "A" entity the "B" entity's material goes back to the original but the "A" entity's colour doesn't change.
This is how I'm trying to make it work:
enum EntityState {
case unselected
case selected
case correctName
case wrongName
}
private var entitiesState: [String: EntityState] = [String: EntityState]()
private var modelEntities: [ModelEntity] = [ModelEntity]()
private var modelEntitiesMaterials: [String: [Material]] = [String: [Material]]()
//This is how i place a modelEntity
#objc private func placeObject() {
let modelName = self.model.name ?? ""
let entity = try! Entity.load(named: modelName)
let geomChildrens = entity.findEntity(named: "Geom")
if let geomChildrens = geomChildrens {
for children in geomChildrens.children {
let childModelEntity = children as! ModelEntity
childModelEntity.collision = CollisionComponent(shapes: [ShapeResource.generateConvex(from: childModelEntity.model!.mesh)])
entitiesState[childModelEntity.name] = EntityState.unselected
modelEntities.append(childModelEntity)
}
}
let modelEntity = ModelEntity()
modelEntity.addChild(entity)
let anchorEntity = AnchorEntity(.plane(.horizontal, classification: .any, minimumBounds: .zero))
anchorEntity.addChild(modelEntity)
arView.installGestures([.all],for: modelEntity)
arView.scene.addAnchor(anchorEntity)
}
private func selectEntity(withSelectedEntity: ModelEntity?) {
modelInformationView.isHidden = false
//If we didnt hit any modelEntity
guard let selectedEntity = withSelectedEntity else {
//Unselect the selected entity if there is one.
for entity in modelEntities {
if(entitiesState[entity.name] == .selected) {
entitiesState[entity.name] = .unselected
}
}
colorModelEntities()
return
}
if(entitiesState[selectedEntity.name] == .selected) {
// If its already selected, just unselect
entitiesState[selectedEntity.name] = .unselected
} else {
//First unselect the previously selected entity.
for entity in modelEntities {
if(entitiesState[entity.name] == .selected) {
entitiesState[entity.name] = .unselected
}
}
//Select the entity.
entitiesState[selectedEntity.name] = .selected
}
colorModelEntities()
}
private func colorModelEntities() {
let selectedMaterial = SimpleMaterial(color: .link, isMetallic: false) //Blue
for entity in modelEntities {
let keyExists = modelEntitiesMaterials[entity.name] != nil
if keyExists {
entity.model!.materials = modelEntitiesMaterials[entity.name]!
}
if(entitiesState[entity.name] == .selected) {
//Color blue the selected item
entity.model?.materials[0] = selectedMaterial
}
}
}
#objc private func handleTap(sender: UITapGestureRecognizer) {
let tapLocation: CGPoint = sender.location(in: arView)
let result: [CollisionCastHit] = arView.hitTest(tapLocation)
guard let hitTest: CollisionCastHit = result.first, hitTest.entity.name != "Ground Plane"
else {
selectEntity(withSelectedEntity: nil)
return
}
let entity: ModelEntity = hitTest.entity as! ModelEntity
let keyExists = modelEntitiesMaterials[entity.name] != nil
if !keyExists {
modelEntitiesMaterials[entity.name] = entity.model!.materials
}
selectEntity(withSelectedEntity: entity)
}
Solution for a single model
🥧 It's easy as Apple pie 🥧
I used a regular single box scene built in Reality Composer.
import UIKit
import RealityKit
class GameViewController: UIViewController {
#IBOutlet var arView: ARView!
var me: ModelEntity? = nil
var counter = 0
override func viewDidLoad() {
super.viewDidLoad()
let scene = try! Experience.loadBox()
print(scene)
me = scene.findEntity(named: "simpBld_root") as? ModelEntity
me?.model?.materials[0] = UnlitMaterial(color: .red)
me?.generateCollisionShapes(recursive: true)
arView.scene.anchors.append(scene)
}
override func touchesBegan(_ touches: Set<UITouch>,
with event: UIEvent?) {
guard let point = touches.first?.location(in: arView) else { return }
let ray = arView.ray(through: point)
let castHits = arView.scene.raycast(origin: ray?.origin ?? [0,0,0],
direction: ray?.direction ?? [0,0,0])
if castHits.first != nil {
counter += 1
if counter % 2 == 1 {
me?.model?.materials[0] = UnlitMaterial(color: .green)
} else {
me?.model?.materials[0] = UnlitMaterial(color: .red)
}
}
}
}
Solution for multiple models
Here I used three boxes scene built in Reality Composer (I merely copy-pasted original box).
import UIKit
import RealityKit
class GameViewController: UIViewController {
#IBOutlet var arView: ARView!
typealias ME = ModelEntity
var me: [ModelEntity] = [ME(),ME(),ME()]
override func viewDidLoad() {
super.viewDidLoad()
let scene = try! Experience.loadBox()
for ind in 0...2 {
me[ind] = scene.children[0].children[0]
.children[ind].children[0] as! ModelEntity
me[ind].model?.materials[0] = UnlitMaterial(color: .red)
me[ind].generateCollisionShapes(recursive: true)
me[ind].name = "box \(ind + 1)"
}
arView.scene.anchors.append(scene)
print(scene)
}
override func touchesBegan(_ touches: Set<UITouch>,
with event: UIEvent?) {
guard let point = touches.first?.location(in: arView) else { return }
let ray = arView.ray(through: point)
let castHits = arView.scene.raycast(origin: ray?.origin ?? [0,0,0],
direction: ray?.direction ?? [0,0,0])
guard let name = castHits.first?.entity.name else { return }
if castHits.first != nil {
switch name {
case "box 1": changeTo(0, .green);
changeTo(1, .red); changeTo(2, .red)
case "box 2": changeTo(1, .green);
changeTo(0, .red); changeTo(2, .red)
case "box 3": changeTo(2, .green);
changeTo(0, .red); changeTo(1, .red)
default: break
}
print(name)
}
}
func changeTo(_ element: Int, _ color: UIColor) {
me[element].model?.materials[0] = UnlitMaterial(color: color)
}
}
This answer might be helpful as well.

How to get a tap position in ARKit?

I am trying to tap the screen and get tap position then I can fire the bullet from the position I tap.
I searched and tried many ways such as below but it doesn't work
guard let pointOfView = sceneView.pointOfView else { return }
let transform = pointOfView.transform
let orientation = SCNVector3(-transform.m31, -transform.m32, -transform.m33)
let scnView = sender.view as! ARSCNView
let holdLocation = sender.location(in: scnView)
let estimatedPlane: ARRaycastQuery.Target = .existingPlaneInfinite
let alignment: ARRaycastQuery.TargetAlignment = .any
guard let query = scnView.raycastQuery(from: holdLocation,
allowing: estimatedPlane,
alignment: alignment)
else { return }
let results = scnView.session.raycast(query)
guard let pointResult = results.first else { return }
let pointTransform = SCNMatrix4(pointResult.worldTransform)
let pointLocation = SCNVector3(pointTransform.m41,
pointTransform.m42,
pointTransform.m43)
let pointPosition = orientation + pointLocation
Can anyone help to tell me how to do it? Highly appreciate it.

Node name was overwritten by USDZ model

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
}
}
}

How to Zoom .DAE 3D model using ARKit iOS

I have a 3D model with .scn extension. How to zoom it with pinch gesture without virtualobject file from iOS sample Placing Objects application.
Pinch gesture works well with .scn's if its converted from .obj file. But its not working with .dae model.
func addPinchGestureToSceneView() {
pinchGesture = UIPinchGestureRecognizer(target: self, action: #selector(scale))
pinchGesture.scale = 1.0;
pinchGesture.delegate = self
self.sceneView.addGestureRecognizer(pinchGesture)
}
private func node(at position: CGPoint) -> SCNNode? {
var hitTestOptions = [SCNHitTestOption: Any]()
hitTestOptions[SCNHitTestOption.boundingBoxOnly] = true
return sceneView.hitTest(position, options: hitTestOptions)
.first(where: { self.getOnlyModelName(name: $0.node.name ?? "") == self.currentmodel.modelname})?
.node
}
#objc func scale(_ gesture: UIPinchGestureRecognizer) {
if self.currentmodel.isZoomEnabled == false{
return
}
let location = gesture.location(in: sceneView)
guard let node = node(at: location)else{return}
// guard let node = node(at: location) else { return }
switch gesture.state {
case .began:
originalScale = node.scale
gesture.scale = CGFloat(node.scale.x)
print("Begin:: \(originalScale)")
case .changed:
guard var originalScale = originalScale else { return }
if gesture.scale > 2.0{
return
}
originalScale.x = Float(gesture.scale)
originalScale.y = Float(gesture.scale)
originalScale.z = Float(gesture.scale)
node.scale = originalScale
case .ended:
guard var originalScale = originalScale else { return }
if gesture.scale > 2.0{
return
}
originalScale.x = Float(gesture.scale)
originalScale.y = Float(gesture.scale)
originalScale.z = Float(gesture.scale)
node.scale = originalScale
gesture.scale = CGFloat(node.scale.x)
default:
gesture.scale = 1.0
originalScale = nil
}
When it's a dae, you might need to try to grab the parent of the node caught in the hit test. I had a similar issue with dae that got solved by getting the parent of the child, or even sometimes the grandparent of the child.

Can I get 3d models from web servers on Swift?

I'm working on an application with Arkit. There are many 3D models and the size is big in my app. Can I get these models out of another server (outside sites)? I'm new on swift, I can't seem to find anything on loading a 3d model from a web server.
is it enough to change the model path there? Thank you
func loadModel() {
guard let virtualObjectScene = SCNScene(named: "\(modelName).\(fileExtension)", inDirectory: "Models.scnassets/\(modelName)") else {
return
}
let wrapperNode = SCNNode()
for child in virtualObjectScene.rootNode.childNodes {
let defaults = UserDefaults.standard
wrapperNode.addChildNode(child)
}
self.addChildNode(wrapperNode)
}
All code:
import UIKit
import SceneKit
import ARKit
class VirtualObject: SCNNode {
var modelName: String = ""
var fileExtension: String = ""
var thumbImage: UIImage!
var title: String = ""
var viewController: ViewController?
override init() {
super.init()
self.name = "Virtual object root node"
}
init(modelName: String, fileExtension: String, thumbImageFilename: String, title: String) {
super.init()
self.name = "Virtual object root node"
self.modelName = modelName
self.fileExtension = fileExtension
self.thumbImage = UIImage(named: thumbImageFilename)
self.title = title
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func loadModel() {
guard let virtualObjectScene = SCNScene(named: "\(modelName).\(fileExtension)", inDirectory: "Models.scnassets/\(modelName)") else {
return
}
let wrapperNode = SCNNode()
for child in virtualObjectScene.rootNode.childNodes {
let defaults = UserDefaults.standard
wrapperNode.addChildNode(child)
}
self.addChildNode(wrapperNode)
}
func unloadModel() {
self.removeFromParentNode()
for child in self.childNodes {
child.removeFromParentNode()
}
}
func translateBasedOnScreenPos(_ pos: CGPoint, instantly: Bool, infinitePlane: Bool) {
guard let controller = viewController else {
return
}
let result = controller.worldPositionFromScreenPosition(pos, objectPos: self.position, infinitePlane: infinitePlane)
controller.moveVirtualObjectToPosition(result.position, instantly, !result.hitAPlane)
}
}
extension VirtualObject {
static func isNodePartOfVirtualObject(_ node: SCNNode) -> Bool {
if node.name == "Virtual object root node" {
return true
}
if node.parent != nil {
return isNodePartOfVirtualObject(node.parent!)
}
return false
}
static let availableObjects: [VirtualObject] = [
Anatomy()
]
}
you can load an scn file from a webserver with ip addresses like this (i used a fake ip below)
let myURL = NSURL(string: “http://110.151.153.202:80/scnfiles/myfile.scn”)
let scene = try! SCNScene(url: myURL! as URL, options:nil)
Edit:
Here’s a simple Swift PlayGrounds which pulls a test cube scn file from my github repo. You just tap anywhere and the cube loads.
import ARKit
import SceneKit
import PlaygroundSupport
class ViewController: NSObject {
var sceneView: ARSCNView
init(sceneView: ARSCNView) {
self.sceneView = sceneView
super.init()
self.setupWorldTracking()
self.sceneView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(ViewController.handleTap(_:))))
}
private func setupWorldTracking() {
if ARWorldTrackingConfiguration.isSupported {
let configuration = ARWorldTrackingConfiguration()
configuration.planeDetection = .horizontal
configuration.isLightEstimationEnabled = true
self.sceneView.session.run(configuration, options: [])
}
}
#objc func handleTap(_ gesture: UITapGestureRecognizer) {
let results = self.sceneView.hitTest(gesture.location(in: gesture.view), types: ARHitTestResult.ResultType.featurePoint)
guard let result: ARHitTestResult = results.first else {
return
}
// pulls cube.scn from github repo
let myURL = NSURL(string: "https://raw.githubusercontent.com/wave-electron/scnFile/master/cube.scn")
let scene = try! SCNScene(url: myURL! as URL, options: nil)
let node = scene.rootNode.childNode(withName: "SketchUp", recursively: true)
node?.scale = SCNVector3(0.01,0.01,0.01)
let position = SCNVector3Make(result.worldTransform.columns.3.x, result.worldTransform.columns.3.y, result.worldTransform.columns.3.z)
node?.position = position
self.sceneView.scene.rootNode.addChildNode(node!)
}
}
let sceneView = ARSCNView()
let viewController = ViewController(sceneView: sceneView)
sceneView.autoenablesDefaultLighting = true
PlaygroundPage.current.needsIndefiniteExecution = true
PlaygroundPage.current.liveView = viewController.sceneView