Place Multiple Scn Objects in Touchesbegan Method

Place multiple SCN objects in touchesBegan method

Tip: If you use RealityKit, read this post.

Solution 1

Adding 3D models using touchesBegan(:with:)

Use the following code to get a desired effect (place as many objects into a scene as you want):

Sample Image

At first create an extension for your convenience:

import ARKit

extension SCNVector3 {

static func + (lhs: SCNVector3, rhs: SCNVector3) -> SCNVector3 {
return SCNVector3(lhs.x + rhs.x, lhs.y + rhs.y, lhs.z + rhs.z)
}
}

Then use it in your ViewController for adding a pointOfView.position to desiredVector:

class ViewController: UIViewController {

@IBOutlet var sceneView: ARSCNView!

override func touchesBegan(_ touches: Set<UITouch>,
with event: UIEvent?) {

sceneView.isMultipleTouchEnabled = true

guard let pointOfView = sceneView.pointOfView // Camera of SCNScene
else { return }

let cameraMatrix = pointOfView.transform

let desiredVector = SCNVector3(cameraMatrix.m31 * -0.5,
cameraMatrix.m32 * -0.5,
cameraMatrix.m33 * -0.5)

// What the extension SCNVector3 is for //
let position = pointOfView.position + desiredVector

let sphereNode = SCNNode()
sphereNode.geometry = SCNSphere(radius: 0.05)
sphereNode.geometry?.firstMaterial?.diffuse.contents = UIColor.green
sphereNode.position = position
sceneView.scene.rootNode.addChildNode(sphereNode)
}
override func viewDidLoad() {
super.viewDidLoad()
let scene = SCNScene(named: "art.scnassets/myScene.scn")!
sceneView.scene = scene
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
let config = ARWorldTrackingConfiguration()
sceneView.session.run(config)
}
}

And if you want to retrieve a 3D model from .scn file use the following code:

(Instead of the sphereNode):

var model = SCNNode()
let myScene = SCNScene(named: "art.scnassets/ship.scn")

// Model's name in a Scene graph hierarchy.
// Pay particular attention – it's not a name of .scn file.
let nodeName = "ship"

model = (myScene?.rootNode.childNode(withName: nodeName, recursively: true))!
model.position = position
sceneView.scene.rootNode.addChildNode(model)

Sample Image


Solution 2

Adding 3D models using Plane Detection + Hit-Testing

Use the following code if you want to add models using plane detection and Hit-testing :

At first create an extension for your convenience:

extension float4x4 {
var simdThree: SIMD3<Float> {
let translation = self.columns.3
return SIMD3<Float>(translation.x, translation.y, translation.z)
}
}

Then use it in the ViewController:

class ViewController: UIViewController {

@IBOutlet weak var sceneView: ARSCNView!

override func viewDidLoad() {
super.viewDidLoad()
addGesture()
}

override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
sceneView.delegate = self // for ARSCNViewDelegate

let config = ARWorldTrackingConfiguration()
config.planeDetection = [.horizontal]
sceneView.session.run(config)
}

func addGesture() {
let tapGesture = UITapGestureRecognizer(target: self,
action: #selector(addModel))
sceneView.addGestureRecognizer(tapGesture)
}

// Don't forget to drag-and-drop TapGestureRecognizer object from library
@objc func addModel(recognizer: UIGestureRecognizer) {

let tap: CGPoint = recognizer.location(in: sceneView)

let results: [ARHitTestResult] = sceneView.hitTest(tap,
types: .existingPlaneUsingExtent)

guard let hitTestResult = results.first
else { return }

let translation = hitTestResult.worldTransform.simdThree
let x = translation.x
let y = translation.y
let z = translation.z

guard let scene = SCNScene(named: "art.scnassets/myScene.scn"),
let robotNode = scene.rootNode.childNode(withName: "robot",
recursively: true)
else { return }

robotNode.position = SCNVector3(x, y, z)
robotNode.scale = SCNVector3(0.02, 0.02, 0.02)
sceneView.scene.rootNode.addChildNode(robotNode)
}
}

And, you have to implement a logic inside two renderer() methods for ARPlaneAnchors:

extension ViewController: ARSCNViewDelegate {

func renderer(_ renderer: SCNSceneRenderer,
didAdd node: SCNNode,
for anchor: ARAnchor) { // your logic here.... }

func renderer(_ renderer: SCNSceneRenderer,
didUpdate node: SCNNode,
for anchor: ARAnchor) { // your logic here.... }

}

How to detect which SCNNode has been touched ARKit

There are multiple problems here, and the existing answers are addressing only some of them.

  1. It's unclear from the code you've posted whether bottleNode can be nil when this method runs. Calling a method through the optional (the ? in bottle?.contains) when its value is nil would fail silently — causing the entire expression result to wrap in an Optional whose value is nil — but you've got parens and a force unwrap around the whole expression, so the nil-unwrap would crash.

  2. contains(_:) is not a method on SCNNode. It's unclear what type your bottleNode could be that you could even write this method call without getting compiler errors... but if bottleNode actually is an SCNNode and you've done some type-erasing/Any-casting goop to allow the call to compile, the call would fail at runtime due to the non-existent method.

If your goal with the bottleNode.contains line is to determine whether the hit test result is either bottleNode itself or a child node thereof, I'd recommend defining and using an extension method like this:

extension SCNNode {
func hasAncestor(_ node: SCNNode) -> Bool {
if self === node {
return true // this is the node you're looking for
}
if self.parent == nil {
return false // target node can't be a parent/ancestor if we have no parent
}
if self.parent === node {
return true // target node is this node's direct parent
}
// otherwise recurse to check parent's parent and so on
return self.parent.hasAncestor(node)
}
}

// in your touchesBegan method...
if let bottleNode = bottleNode, result.node.hasAncestor(bottleNode) {
print("match")
}

If instead your goal is to determine whether result.node lies within or overlaps the bounding box of bottleNode (regardless of node hierarchy), answering that question is a little more complicated. A simple position within boundingSphere check is pretty easy, or if you're looking for containment/overlap, an octree might help.

swift/scenekit problems getting touch events from SCNScene and overlaySKScene

This is "lifted" straight out of Xcode's Game template......

Add a gesture recognizer in your viewDidLoad:

       // add a tap gesture recognizer
let tapGesture = UITapGestureRecognizer(target: self, action:
#selector(handleTap(_:)))
scnView.addGestureRecognizer(tapGesture)

func handleTap(_ gestureRecognize: UIGestureRecognizer) {
// retrieve the SCNView
let scnView = self.view as! SCNView

// 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 {
// retrieved the first clicked object
let result: AnyObject = hitResults[0]

// result.node is the node that the user tapped on
// perform any actions you want on it

}
}

Adding SCNNode multiple time shows only once

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
}

}

Multiple nodes in .scn model causing iMessage extension to crash

from the App Extension Programming Guide we learn that

Memory limits for running app extensions are significantly lower than the memory limits imposed on a foreground app. On both platforms, the system may aggressively terminate extensions because users want to return to their main goal in the host app. Some extensions may have lower memory limits than others: For example, widgets must be especially efficient because users are likely to have several widgets open at the same time.

Your app extension doesn’t own the main run loop, so it’s crucial that you follow the established rules for good behavior in main run loops. For example, if your extension blocks the main run loop, it can create a bad user experience in another extension or app.

Keep in mind that the GPU is a shared resource in the system. App extensions do not get top priority for shared resources; for example, a Today widget that runs a graphics-intensive game might give users a bad experience. The system is likely to terminate such an extension because of memory pressure. Functionality that makes heavy use of system resources is appropriate for an app, not an app extension.

Swift Add Button to SCNNode

There are a few ways you can approach this:

1: Standard Approach:

In your delegate method, assign each node a name as per below (clearly if you have lots of nodes you would want to store them in an Array or Dictionary depending upon your needs):

 func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {

//1. Check We Have The Image Anchor
guard let imageAnchor = anchor as? ARImageAnchor else { return }

//2. Get The Reference Image
let referenceImage = imageAnchor.referenceImage

//1. Create The Plane Geometry With Our Width & Height Parameters
let planeGeometry = SCNPlane(width: referenceImage.physicalSize.width,
height: referenceImage.physicalSize.height)

//2. Create A New Material
let material = SCNMaterial()

material.diffuse.contents = UIColor.red

//3. Create The Plane Node
let planeNode = SCNNode(geometry: planeGeometry)

planeNode.geometry?.firstMaterial = material

planeNode.opacity = 0.25

planeNode.eulerAngles.x = -.pi / 2

//4. Add A Name To The Node
planeNode.name = "I Was Clicked"

//5. Add It To The Scene
node.addChildNode(planeNode)

}

Then using touchesBegan, perform a hitTest and handle your touch accordingly:

   override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {

/*
1. Get The Current Touch Location
2. Check That We Have Touched A Valid Node
3. Check If The Node Has A Name
4. Handle The Touch
*/

guard let touchLocation = touches.first?.location(in: augmentedRealityView),
let hitNode = augmentedRealityView?.hitTest(touchLocation, options: nil).first?.node,
let nodeName = hitNode.name
else {
//No Node Has Been Tapped
return

}
//Handle Event Here e.g. PerformSegue
print(nodeName)

}

2: An Interesting Approach:

UIKit elements can actually be added as an SCNGeometry's Material. I personally haven't seen many people use this approach, but it may prove useful to anyone who wants to incorporate UIKit with ARKit.

Create A Custom UIButton e.g:

/// Clickable View
class ClickableView: UIButton{

override init(frame: CGRect) {

super.init(frame: frame)

self.addTarget(self, action: #selector(objectTapped(_:)), for: .touchUpInside)

self.backgroundColor = .red

}

required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

/// Detects Which Object Was Tapped
///
/// - Parameter sender: UIButton
@objc func objectTapped(_ sender: UIButton){

print("Object With Tag \(tag)")

}

}

And in your delegate method do the following:

  func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {

//1. Check We Have The Image Anchor
guard let imageAnchor = anchor as? ARImageAnchor else { return }

//2. Get The Reference Image
let referenceImage = imageAnchor.referenceImage

//1. Create The Plane Geometry With Our Width & Height Parameters
let planeGeometry = SCNPlane(width: referenceImage.physicalSize.width,
height: referenceImage.physicalSize.height)

//2. Create A New Material
let material = SCNMaterial()

DispatchQueue.main.async {
//3. Create The New Clickable View
let clickableElement = ClickableView(frame: CGRect(x: 0, y: 0,
width: 300,
height: 300))
clickableElement.tag = 1

//4. Add The Clickable View As A Materil
material.diffuse.contents = clickableElement
}

//5. Create The Plane Node
let planeNode = SCNNode(geometry: planeGeometry)

planeNode.geometry?.firstMaterial = material

planeNode.opacity = 0.25

planeNode.eulerAngles.x = -.pi / 2

//6. Add It To The Scene
node.addChildNode(planeNode)
}

This should get you started...



Related Topics



Leave a reply



Submit