Swift Add Button to Scnnode

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

added button to sceneKit view but it has a lag

Okay I solved it. The problematic piece of code is

starButton.addTarget(self, action: "starButtonClicked", forControlEvents: UIControlEvents.TouchUpInside)

UIControlEvent.TouchUpInside gives the illusion of lag. Changing it to .TouchDown is much better.

SwiftUI with SceneKit: How to use button action from view to manipulate underlying scene

I found a solution using @EnvironmentalObject but I am not completely sure, if this is the right approach. So comments on this are appreciated.

First, I moved the SCNScene into it’s own class and made it an OberservableObject:

class Scene: ObservableObject {
@Published var scene: SCNScene

init(_ scene: SCNScene = SCNScene()) {
self.scene = scene
self.scene = setup(scene: self.scene)
}

// code omitted which deals with setting the scene up and adding/removing the box

// method used to determine if the box node is present in the scene -> used later on
func boxIsPresent() -> Bool {
return scene.rootNode.childNode(withName: "box", recursively: true) != nil
}
}

I inject this Scene into the app as an .environmentalObject(), so it is available to all views:

class SceneDelegate: UIResponder, UIWindowSceneDelegate {

func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {

// Create the SwiftUI view that provides the window contents.
let sceneKit = Scene()
let mainView = MainView().environmentObject(sceneKit)

// Use a UIHostingController as window root view controller.
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: mainView)
self.window = window
window.makeKeyAndVisible()
}
}
}

MainView is slightly altered to call SceneView (a UIViewRepresentable) with the separate Scene for the environment:

struct MainView: View {
@EnvironmentObject var scene: Scene

var body: some View {
ZStack {
SceneView(scene: self.scene.scene)
HUDView()
}
}
}

Then I make the Scene available to the HUDView as an @EnvironmentalObject, so I can reference the scene and its methods and call them from the Button action. Another effect is, I can query the Scene helper method to determine, if a Button should be active or not:

struct HUDView: View {
@EnvironmentObject var scene: Scene
@State private var canAddBox: Bool = false
@State private var canRemoveBox: Bool = true

var body: some View {
VStack {
HStack(alignment: .center, spacing: 0) {
Spacer ()

ButtonView(
action: {
self.scene.addBox()
if self.scene.boxIsPresent() {
self.canAddBox = false
self.canRemoveBox = true
}
},
icon: "plus.square.fill",
isActive: $canAddBox
)

ButtonView(
action: {
self.scene.removeBox()
if !self.scene.boxIsPresent() {
self.canRemoveBox = false
self.canAddBox = true
}
},
icon: "minus.square.fill",
isActive: $canRemoveBox
)

}
.background(Color.white.opacity(0.2))

Spacer()
}
}
}

Here is the ButtonView code, which used a @Bindingto set its active state (not sure about the correct order for this with the@State property inHUDView`):

struct ButtonView: View {
let action: () -> Void
var icon: String = "square"
@Binding var isActive: Bool

var body: some View {
Button(action: action) {
Image(systemName: icon)
.font(.title)
.accentColor(self.isActive ? Color.white : Color.white.opacity(0.5))
}
.frame(width: 44, height: 44)
.disabled(self.isActive ? false: true)

}
}

Anyway, the code works now. Any thoughts on this?

create 3 buttons or interactions ARKit swift

I'm not certain this is the most robust nor most scalable way to tackle your problem but I managed to achieve it fairly simply.

First I got an image which I knew was 10cm x 10cm:
Sample Image

I then added it as an ARReferenceImage and set it's physicalWidth and height to (you guessed it) 0.1m x 0.1m.

Now since we know in advance the size of our detectable image, I created a simple SCNScene.

Sample Image

I made an SCNPlane with the same dimensions as the image, and set it's diffuse contents to our targetImage.

I then added three SCNTorus's at the places I wanted to detect. Obviously this is me just being silly, but you will get the idea.

I then set each each of the pseudo buttons with a name:
(a) Spike (Top),
(b) Hand,
(c) Tail.

I then set the SCNPlane to hidden, as we dont actually need to see it.

After having setting up my ARImageTrackingConfiguration I used the delegate callback to overlay my scene:

//---------------------------
// MARK: - ARSCNViewDelegate
//---------------------------

extension ViewController: ARSCNViewDelegate{

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

//1. Check We Have Detected An ARImageAnchor & Check It's The One We Want
guard let validImageAnchor = anchor as? ARImageAnchor,
let targetName = validImageAnchor.referenceImage.name, targetName == "TargetImage" else { return}

//2. Since We Know The Size Of Our Target & Have Created Our Overlay Simply Add It To The Node
guard let interactiveTarget = SCNScene(named: "art.scnassets/Overlay.scn") else { return }
let overlayNode = interactiveTarget.rootNode
overlayNode.eulerAngles.x = -.pi / 2
node.addChildNode(overlayNode)

}
}

That aligned very well, with the target image so, now I had to handle the interaction.

I simply used touchesBegan to detect if we had hit any of our desired SCNNodes:

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

//1. Get The Current Touch Location
//2. Check We Have Hit An SCNNode By Performing An SCNHitTest
//3. Check Our HitNode Has A Name & It Isn't The Target
guard let currentTouchLocation = touches.first?.location(in: self.augmentedRealityView),
let hitTestNode = self.augmentedRealityView.hitTest(currentTouchLocation, options: nil).first?.node,
let name = hitTestNode.name, name != "Target" else { return }

//4. Update The UI That We Have Touched An Area
infoLabel.text = "Ya! Don't Touch My \(name)"

//5. Reset The Text
DispatchQueue.main.asyncAfter(deadline: .now() + 2) { self.infoLabel.text = "" }
}

Which yielded something like this:

Sample Image

And here is the full code:

//---------------------------
// MARK: - ARSCNViewDelegate
//---------------------------

extension ViewController: ARSCNViewDelegate{

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

//1. Check We Have Detected An ARImageAnchor & Check It's The One We Want
guard let validImageAnchor = anchor as? ARImageAnchor,
let targetName = validImageAnchor.referenceImage.name, targetName == "TargetImage" else { return}

//2. Since We Know The Size Of Our Target & Have Created Our Overlay Simply Add It To The Node
guard let interactiveTarget = SCNScene(named: "art.scnassets/Overlay.scn") else { return }
let overlayNode = interactiveTarget.rootNode
overlayNode.eulerAngles.x = -.pi / 2
node.addChildNode(overlayNode)

}
}

class ViewController: UIViewController {

@IBOutlet var augmentedRealityView: ARSCNView!
@IBOutlet weak var infoLabel: UILabel!

//-----------------------
// MARK: - View LifeCycle
//-----------------------

override func viewDidLoad() {
super.viewDidLoad()

augmentedRealityView.delegate = self

infoLabel.text = ""
}

override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)

//1. Get The Images We Wish To Track
guard let imagesToTrack = ARReferenceImage.referenceImages(inGroupNamed: "AR Resources", bundle: nil) else {
fatalError("Missing Reference Images")
}

//2. Set Up Our ARTracking Configuration
let configuration = ARImageTrackingConfiguration()
configuration.maximumNumberOfTrackedImages = 1

//3. Assign Our Detection Images
configuration.trackingImages = imagesToTrack

//4. Run The Session
augmentedRealityView.session.run(configuration)
}

override func viewWillDisappear(_ animated: Bool) {

super.viewWillDisappear(animated)

augmentedRealityView.session.pause()
}

//--------------------------
// MARK: - Touch Interaction
//--------------------------

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

//1. Get The Current Touch Location
//2. Check We Have Hit An SCNNode By Performing An SCNHitTest
//3. Check Our HitNode Has A Name & It Isn't The Target
guard let currentTouchLocation = touches.first?.location(in: self.augmentedRealityView),
let hitTestNode = self.augmentedRealityView.hitTest(currentTouchLocation, options: nil).first?.node,
let name = hitTestNode.name, name != "Target" else { return }

//4. Update The UI That We Have Touched An Area
infoLabel.text = "Ya! Don't Touch My \(name)"

//5. Reset The Text
DispatchQueue.main.asyncAfter(deadline: .now() + 2) { self.infoLabel.text = "" }
}

}

As I said perhaps not the most robust or scalable solution but it will definitely point you in the right direction...

How to use *.scnp file in SwiftUI for button click (iOS)?

Here is modified code with demo. Tested with Xcode 11.4 / macOS 10.15.4

demo

Note: as I don't know your project structure, all dependent resource files were added as Resources (not in Assets). Just in case.

resources

struct DemoSceneKitParticles: View {
@State private var exploding = false
var body: some View {
VStack {
ScenekitView(exploding: $exploding)
Button("BOOM") { self.exploding = true }
}
}
}

struct ScenekitView : NSViewRepresentable {
@Binding var exploding: Bool
let scene = SCNScene(named: "ship.scn")!

func makeNSView(context: NSViewRepresentableContext<ScenekitView>) -> SCNView {
// create and add a camera to the scene
let cameraNode = SCNNode()
cameraNode.camera = SCNCamera()
scene.rootNode.addChildNode(cameraNode)

// place the camera
cameraNode.position = SCNVector3(x: 0, y: 0, z: 15)

// create and add a light to the scene
let lightNode = SCNNode()
lightNode.light = SCNLight()
lightNode.light!.type = .omni
lightNode.position = SCNVector3(x: 0, y: 10, z: 10)
scene.rootNode.addChildNode(lightNode)

// create and add an ambient light to the scene
let ambientLightNode = SCNNode()
ambientLightNode.light = SCNLight()
ambientLightNode.light!.type = .ambient
ambientLightNode.light!.color = NSColor.darkGray
scene.rootNode.addChildNode(ambientLightNode)

// retrieve the ship node
let ship = scene.rootNode.childNode(withName: "ship", recursively: true)!

// animate the 3d object
ship.runAction(SCNAction.repeatForever(SCNAction.rotateBy(x: 0, y: 2, z: 0, duration: 1)))

// retrieve the SCNView
let scnView = SCNView()
return scnView
}

func updateNSView(_ scnView: SCNView, context: Context) {
scnView.scene = scene

// allows the user to manipulate the camera
scnView.allowsCameraControl = true

// show statistics such as fps and timing information
scnView.showsStatistics = true

// configure the view
scnView.backgroundColor = NSColor.black

if exploding {
if let ship = scene.rootNode.childNode(withName: "ship", recursively: true),
let particles = SCNParticleSystem(named: "Explosion", inDirectory: nil) {

let node = SCNNode()
node.addParticleSystem(particles)
node.position = ship.position
scnView.scene?.rootNode.addChildNode(node)
ship.removeFromParentNode()
}
}
}
}

Update: variant for iOS

Tested with Xcode 11.4 / iOS 13.4

demo3

Full module code (resource files as before at top level)

import SwiftUI
import SceneKit

struct DemoSceneKitParticles: View {
@State private var exploding = false
var body: some View {
VStack {
ScenekitView(exploding: $exploding)
Button("BOOM") { self.exploding = true }
}
}
}

struct ScenekitView : UIViewRepresentable {
@Binding var exploding: Bool
let scene = SCNScene(named: "ship.scn")!

func makeUIView(context: Context) -> SCNView {
// create and add a camera to the scene
let cameraNode = SCNNode()
cameraNode.camera = SCNCamera()
scene.rootNode.addChildNode(cameraNode)

// place the camera
cameraNode.position = SCNVector3(x: 0, y: 0, z: 15)

// create and add a light to the scene
let lightNode = SCNNode()
lightNode.light = SCNLight()
lightNode.light!.type = .omni
lightNode.position = SCNVector3(x: 0, y: 10, z: 10)
scene.rootNode.addChildNode(lightNode)

// create and add an ambient light to the scene
let ambientLightNode = SCNNode()
ambientLightNode.light = SCNLight()
ambientLightNode.light!.type = .ambient
ambientLightNode.light!.color = UIColor.darkGray
scene.rootNode.addChildNode(ambientLightNode)

// retrieve the ship node
let ship = scene.rootNode.childNode(withName: "ship", recursively: true)!

// animate the 3d object
ship.runAction(SCNAction.repeatForever(SCNAction.rotateBy(x: 0, y: 2, z: 0, duration: 1)))

// retrieve the SCNView
let scnView = SCNView()
return scnView
}

func updateUIView(_ scnView: SCNView, context: Context) {
scnView.scene = scene

// allows the user to manipulate the camera
scnView.allowsCameraControl = true

// show statistics such as fps and timing information
scnView.showsStatistics = true

// configure the view
scnView.backgroundColor = UIColor.black

if exploding {
if let ship = scene.rootNode.childNode(withName: "ship", recursively: true),
let particles = SCNParticleSystem(named: "Explosion", inDirectory: nil) {

let node = SCNNode()
node.addParticleSystem(particles)
node.position = ship.position
scnView.scene?.rootNode.addChildNode(node)
ship.removeFromParentNode()
}
}
}
}

struct DemeSKParticles_Previews: PreviewProvider {
static var previews: some View {
DemoSceneKitParticles()
}
}

Is it possible to change the ARKit scene's object when clicking on a button Swift

What I was missing in my above code is that I had to make the deletion and add two synchronize tasks. Since the Delete function is into a closure( asynchronous task). so the add function will be executed before the deletion.
And by then the error will be gone.



Related Topics



Leave a reply



Submit