Creating an object placement indicator similar to Apple's focus square - swift

Working on my first ARKit project which I use horizontal plane detection to place a 3d model onto the horizontal plane.
I'm trying to use the focus square but as a Swift newbie, I couldn't figure how to use it. So I want to create my own static indicator.
My approach is to create a SCNPlane as an indicator. However, my SCNPlane does not move quickly like Apple's focus square so I need help with my code.
Here's my code:
func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
guard let planeAnchor = anchor as? ARPlaneAnchor else { return }
// Create a SceneKit plane to visualize the node using its position and extent.
let plane = SCNPlane(width: CGFloat(0.1), height: CGFloat(0.1))
let planeNode = SCNNode(geometry: plane)
let planeMaterial = SCNMaterial()
planeMaterial.diffuse.contents = UIColor.gray.withAlphaComponent(0.3)
plane.materials = [planeMaterial]
planeNode.position = SCNVector3Make(planeAnchor.center.x, 0, planeAnchor.center.z)
// SCNPlanes are vertically oriented in their local coordinate space.
// Rotate it to match the horizontal orientation of the ARPlaneAnchor.
planeNode.transform = SCNMatrix4MakeRotation(-Float.pi / 2, 1, 0, 0)
// ARKit owns the node corresponding to the anchor, so make the plane a child node.
node.addChildNode(planeNode)
}
func renderer(_ renderer: SCNSceneRenderer, didUpdate node: SCNNode, for anchor: ARAnchor) {
guard let planeAnchor = anchor as? ARPlaneAnchor,
let planeNode = node.childNodes.first,
let plane = planeNode.geometry as? SCNPlane
else { return }
let x = CGFloat(planeAnchor.center.x)
let y = CGFloat(planeAnchor.center.y)
let z = CGFloat(planeAnchor.center.z)
planeNode.position = SCNVector3(x, y, z)
}
Not sure if this is the right approach? If you guys have the right approach, please let me know

I suppose it must approximately work this way:
class ViewController: UIViewController {
#IBOutlet var sceneView: ARSCNView!
var focusSquare = FocusSquare() // custom class instantiation
// ............................
var session: ARSession {
return sceneView.session
}
override func viewDidLoad() {
super.viewDidLoad()
sceneView.delegate = self
sceneView.session.delegate = self
sceneView.scene.rootNode.addChildNode(focusSquare)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
resetTracking()
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
session.pause()
}
func resetTracking() {
virtualObjectInteraction.selectedObject = nil
let configuration = ARWorldTrackingConfiguration()
configuration.planeDetection = [.horizontal, .vertical]
session.run(configuration, options: [.resetTracking, .removeExistingAnchors])
statusViewController.scheduleMessage("PLEASE, FIND A SURFACE", inSeconds: 5.0, messageType: .planeEstimation)
}
func updateFocusSquare(isObjectVisible: Bool) {
if isObjectVisible {
focusSquare.hide()
} else {
focusSquare.unhide()
statusViewController.scheduleMessage("MOVE LEFT-RIGHT", inSeconds: 5.0, messageType: .focusSquare)
}
if let camera = session.currentFrame?.camera, case .normal = camera.trackingState,
let result = self.sceneView.smartHitTest(screenCenter) {
updateQueue.async {
self.sceneView.scene.rootNode.addChildNode(self.focusSquare)
self.focusSquare.state = .detecting(hitTestResult: result, camera: camera)
}
addObjectButton.isHidden = false
statusViewController.cancelScheduledMessage(for: .focusSquare)
} else {
updateQueue.async {
self.focusSquare.state = .initializing
self.sceneView.pointOfView?.addChildNode(self.focusSquare)
}
addObjectButton.isHidden = true
}
}
}
You can download Apple's a sample project here, where you can see how to create a FocusSquare() class.

Apple's focus square smooth behavior is due to the "virtualObjectLoader" class in conjunction with the "updateAtTime" and "didUpdate" implementation of renderer functions
Just give a look to those three elements and you will find the right approach
Please post your code after adding those concepts if any trouble

Related

RealityKit ARkit : find an anchor (or entity) from raycast - always nil

When I touch the screen I Use the raycast to place a object (entity and anchor) to it's worldTransform
Then I try to touch this object to get it's anchor (or it's own entity)
I am trying to find previously placed anchor with raycast (or hitTest ) but every things returns nil
like that :
This is my onTap code :
#IBAction func onTap(_ sender: UITapGestureRecognizer){
let tapLocation = sender.location(in: arView)
guard let result = arView.raycast(from: tapLocation, allowing: .estimatedPlane, alignment: .any).first else { return }
print("we have anchor??")
print(result.anchor) // always nil
// never hit
if let existResult = arView.hitTest(tapLocation).first {
print("hit test trigger")
if let entity = existResult.entity as Entity? {
NSLog("we have an entity \(entity.name)")
...
}
}
}
And this is the way I create some object and anchors :
let anchor = AnchorEntity(world: position)
anchor.addChild(myObj)
arView.scene.anchors.append(anchor)
// my obj is now visible
Do you have any idea why I can't manage to get the anchor I touch ?
EDIT :
ARview configuration :
arView.session.delegate = self
arView.automaticallyConfigureSession = true
let config = ARWorldTrackingConfiguration()
config.planeDetection = [.horizontal, .vertical]
NSLog("FINISHED INIT")
if ARWorldTrackingConfiguration.supportsSceneReconstruction(.mesh) {
config.sceneReconstruction = .mesh // .meshWithClassification
arView.environment.sceneUnderstanding.options.insert([.occlusion])
arView.debugOptions.insert(.showSceneUnderstanding)
NSLog("FINISHED with scene reco")
} else {
NSLog("no not support scene Reconstruction")
}
let tapGesture = UITapGestureRecognizer(target: self, action:#selector(onTap))
arView.addGestureRecognizer(tapGesture)
arView.session.run(config)
I finally manage to found a solution :
My ModelEntity (anchored) had to have a collision shape !
So after adding simply entity.generateCollisionShapes(recursive: true).
This is how I generate a simple box :
let box: MeshResource = .generateBox(width: width, height: height, depth: length)
var material = SimpleMaterial()
material.tintColor = color
let entity = ModelEntity(mesh: box, materials: [material])
entity.generateCollisionShapes(recursive: true) // Very important to active collisstion and hittesting !
return entity
and so after that we must tell the arView to listen to gestures :
arView.installGestures(.all, for: entity)
and finally :
#IBAction func onTap(_ sender: UITapGestureRecognizer){
let tapLocation = sender.location(in: arView)
if let hitEntity = arView.entity(
at: tapLocation
) {
print("touched")
print(hitEntity.name)
// touched !
return ;
}
}
Raycasting is only possible after ARKit detects planes. It can only raycast to planes or feature points. So make sure you run the AR configuration with plane detection (vertical or horizontal depending on your case)
let configuration = ARWorldTrackingConfiguration()
configuration.planeDetection = .horizontal
You can check whether plane anchor added here in renderer didAdd delegate of ARSCNViewDelegate
func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
if let planeAnchor = anchor as? ARPlaneAnchor {
//plane detected
}
}

How to rotate a 3D model in a USDZ file programmatically in Swift?

I currently have my app detecting a postcard picture. Once detected I place a 3D model of a dog on top of it. Problem is, the model is currently having the back of the dog sit sitting the card rather than it’s feet.
Is there a way to programmatically rotate the model so that the feet is standing on the postcard?
class ViewController: UIViewController, ARSCNViewDelegate {
#IBOutlet var sceneView: ARSCNView!
override func viewDidLoad() {
super.viewDidLoad()
sceneView.delegate = self
}
//MARK-: WHERE I CONFIGURE MY APP TO DETECT AN IMAGE
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
let configuration = ARImageTrackingConfiguration()
guard let trackedImages = ARReferenceImage.referenceImages(inGroupNamed: "Photos", bundle: Bundle.main) else {
print("No images available")
return
}
configuration.trackingImages = trackedImages
configuration.maximumNumberOfTrackedImages = 7
sceneView.session.run(configuration)
}
//MARK-: WHERE I PLACE A PLANE/DOG MODEL (named: shipScene) OVER THE DETECTED IMAGE
func renderer(_ renderer: SCNSceneRenderer, nodeFor anchor: ARAnchor) -> SCNNode? {
let node = SCNNode()
if let imageAnchor = anchor as? ARImageAnchor {
let plane = SCNPlane(width: imageAnchor.referenceImage.physicalSize.width, height: imageAnchor.referenceImage.physicalSize.height)
plane.firstMaterial?.diffuse.contents = UIColor(white: 1, alpha: 0.8)
let planeNode = SCNNode(geometry: plane)
planeNode.eulerAngles.x = -.pi / 2
guard let url = Bundle.main.url(forResource: "shipScene", withExtension: "usdz") else { fatalError() }
let mdlAsset = MDLAsset(url: url)
let dogScene = SCNScene(mdlAsset: mdlAsset)
let dogNode = shipScene.rootNode.childNodes.first!
shipNode.position = SCNVector3Zero
shipNode.position.z = 0.15
planeNode.addChildNode(shipNode)
node.addChildNode(planeNode)
}
return node
}
}
I figure I can use a rendition of
shipNode.position.x
To move the model, but any attempts has just moved the model around (but not rotating it).
At first you have to retrieve a model node from scene. Take into account that node's name is not the same as scene's name! You can find your node in Scene graph hierarchy.
let model = scene.rootNode.childNode(withName: "myModel", recursively: true)!
Then you can rotate it using rotation property:
model.rotation.y = -CGFloat.pi / 2
or using eulerAngles property:
model.eulerAngles = SCNVector3(0, -CGFloat.pi/2, 0)
or using orientation property:
model.orientation = SCNVector4(0, 1, 0, -CGFloat.pi/2)
Pay attention where a pivot point of your node is. A rotation should be around the pivot point.
Swift version 5.7
The accepted answer is already solving the issue.
Also, for those who need some spice, here is how you can rotate+scale together with animation.
let node = scene.rootNode.childNode(withName: "Cube_001", recursively: true)!
node.runAction(SCNAction.rotateBy(x: 1, y: 1, z: 3, duration: 3.1))
node.runAction(SCNAction.scale(by: 2, duration: 3.1))

How to add a cube in the center of the screen, and so that it never leaves?

Good afternoon!
I'm new to SceneKit. And I can not solve this problem.
I need to get the cube has always been in the center of the screen and followed the camera until it finds a horizontal and stand on it.
And I have it stays in one place
Here is my simple code right now.
import UIKit
import SceneKit
import ARKit
class ViewController: UIViewController, ARSCNViewDelegate {
#IBOutlet var sceneView: ARSCNView!
override func viewDidLoad() {
super.viewDidLoad()
let scene = SCNScene()
let boxGeometry = SCNBox(width: 0.2, height: 0.2, length: 0.2, chamferRadius: 0)
let material = SCNMaterial()
material.diffuse.contents = UIColor.blue
material.specular.contents = UIColor(white: 0.6, alpha: 1.0)
let boxNode = SCNNode(geometry: boxGeometry)
boxNode.geometry?.materials = [material]
boxNode.position = SCNVector3(0,0,-1.0)
scene.rootNode.addChildNode(boxNode)
sceneView.scene = scene
//------------------------------------
// Set the view's delegate
sceneView.delegate = self
// Show statistics such as fps and timing information
sceneView.showsStatistics = true
// Set the scene to the view
sceneView.scene = scene
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// Create a session configuration
let configuration = ARWorldTrackingConfiguration()
// Run the view's session
sceneView.session.run(configuration)
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
// Pause the view's session
sceneView.session.pause()
}
}
Help me please. Do not swear)
Here is an update to your code that ensure's the box is always at the center of the screen. Once the code detects a plane, it set's the box's parent as the plane anchor.
This is all very primitive, but should help you. If you want the node to float in the center of the screen, uncomment the SCNTransactions within the willRenderScene callback. If you want the box to always face the user, you can add a lookAtConstraint
import UIKit
import SceneKit
import ARKit
class ViewController: UIViewController, ARSCNViewDelegate {
#IBOutlet var sceneView: ARSCNView!
var boxNode: SCNNode? // keep a reference to the cube
override func viewDidLoad() {
super.viewDidLoad()
let scene = SCNScene()
let boxNode = createBox()
scene.rootNode.addChildNode(boxNode)
self.boxNode = boxNode
sceneView.scene = scene
//------------------------------------
// Set the view's delegate
sceneView.delegate = self
// Show statistics such as fps and timing information
sceneView.showsStatistics = true
// Set the scene to the view
sceneView.scene = scene
}
func createBox() -> SCNNode {
let boxGeometry = SCNBox(width: 0.2, height: 0.2, length: 0.2, chamferRadius: 0)
let material = SCNMaterial()
material.diffuse.contents = UIColor.blue
material.specular.contents = UIColor(white: 0.6, alpha: 1.0)
let boxNode = SCNNode(geometry: boxGeometry)
boxNode.geometry?.materials = [material]
return boxNode;
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// Create a session configuration
let configuration = ARWorldTrackingConfiguration()
configuration.planeDetection = .horizontal
// Run the view's session
sceneView.session.run(configuration)
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
// Pause the view's session
sceneView.session.pause()
}
// on willRender update the cube's position.
func renderer(_ renderer: SCNSceneRenderer, willRenderScene scene: SCNScene, atTime time: TimeInterval) {
// get camera translation and rotation
guard let pointOfView = sceneView.pointOfView else { return }
let transform = pointOfView.transform // transformation matrix
let orientation = SCNVector3(-transform.m31, -transform.m32, -transform.m33) // camera rotation
let location = SCNVector3(transform.m41, transform.m42, transform.m43) // camera translation
let currentPostionOfCamera = orientation + location
// SCNTransaction.begin()
if let boxNode = self.boxNode {
boxNode.position = currentPostionOfCamera
}
// SCNTransaction.commit()
}
// detect plances
func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
guard anchor is ARPlaneAnchor else { return }
if let boxNode = self.boxNode {
let newBoxNode = createBox() // create a new node for the center of the screen
self.boxNode = newBoxNode
SCNTransaction.begin()
boxNode.removeFromParentNode()
node.addChildNode(boxNode) // set the current box to the plane.
sceneView.scene.rootNode.addChildNode(newBoxNode) // add the new box node to the scene
SCNTransaction.commit()
}
}
}
extension SCNVector3 {
static func + (left: SCNVector3, right: SCNVector3) -> SCNVector3 {
return SCNVector3Make(left.x + right.x, left.y + right.y, left.z + right.z)
}
}

how to find centre of a plane found in a hit test ARKit SceneKit

In an image detection app, the image is recognised, then an opaque overlay plane is created so when the user taps on the screen a hit test finds the overlay plane, and a new object can be created. But I want to position the object exactly at the centre of the underlying image. How can I get it to be always at the centre of the image / plane, and to have the same orientation. Can this be got from a hit test result? Thanks for any advice!
#objc func handleScreenTap(sender: UITapGestureRecognizer) {
let tappedSceneView = sender.view as! ARSCNView
let tapLocation = sender.location(in: tappedSceneView)
let planeIntersections = tappedSceneView.hitTest(tapLocation, types: [.estimatedHorizontalPlane, .estimatedVerticalPlane])
if !planeIntersections.isEmpty {
addSceneAtPositionOnPlane(hitTestResult: planeIntersections.first!)
}
func addSceneAtPositionOnPlane(hitTestResult: ARHitTestResult) {
let transform = hitTestResult.worldTransform
let positionColumn = transform.columns.3
let initialPosition = SCNVector3(positionColumn.x,
positionColumn.y,
positionColumn.z)
let node = self.createScene(for: initialPosition)
sceneView.scene.rootNode.addChildNode(node)
}
func createScene(for position: SCNVector3) -> SCNNode {
let box = SCNNode(geometry: SCNBox(width: 0.1, //x
height: 0.1, //y
length: 0.1, //z
chamferRadius: 0))
box.geometry?.firstMaterial?.diffuse.contents = UIColor.red
box.geometry?.firstMaterial?.isDoubleSided = true
box.opacity = 0.8
box.position = position
return box
}
if you already added a SCNNode to render a plane on top of the detected image, then you could just use the SceneKit hitTest method that returns a SceneKit node vs. trying to hit test against ARKit geometry.
Once you have the plane you added to the scene you can just add your new geometry as a child of that node.
Here is an example where once the image is detected a plane is drawn on top of it, then when the user clicks on the plane a box is added as a child, the box will then follow the tracked image around and have the correct position and orientation.
import UIKit
import SceneKit
import ARKit
class ViewController: UIViewController, ARSCNViewDelegate {
#IBOutlet var sceneView: ARSCNView!
override func viewDidLoad() {
super.viewDidLoad()
sceneView.delegate = self
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(onTap))
sceneView.addGestureRecognizer(tapGesture)
}
#objc func onTap(_ recognizer: UITapGestureRecognizer) {
let point = recognizer.location(in: sceneView)
guard let hit = sceneView.hitTest(point, options: nil).first else {
return
}
let box = SCNBox(width: 0.02, height: 0.02, length: 0.02, chamferRadius: 0)
let node = SCNNode(geometry: box)
node.position = SCNVector3(0, 0, 0.01)
box.materials.first?.diffuse.contents = UIColor.red
hit.node.addChildNode(node)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
let configuration = ARWorldTrackingConfiguration()
guard let images = ARReferenceImage.referenceImages(inGroupNamed: "ARTest", bundle: nil) else {
return
}
configuration.detectionImages = images
configuration.maximumNumberOfTrackedImages = 1
sceneView.session.run(configuration)
}
func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
guard let imageAnchor = anchor as? ARImageAnchor else {
return
}
let size = imageAnchor.referenceImage.physicalSize
let plane = SCNPlane(width: size.width, height: size.height)
let planeNode = SCNNode(geometry: plane)
planeNode.eulerAngles.x = -Float.pi / 2
planeNode.opacity = 0.9
node.addChildNode(planeNode)
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
sceneView.session.pause()
}
}

How to detect touch and show new SCNPlane using ARKit?

Now I am able to show different SCNPlane, when card detected. After displaying SCNPlanes, the user touches any plane to show new SCNPlane. But right now touch is working properly but new SCNPlane is not showing.
Here is the code I've tried:
var cake_1_PlaneNode : SCNNode? = nil
func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
guard let imageAnchor = anchor as? ARImageAnchor else { return }
if let imageName = imageAnchor.referenceImage.name {
print(imageName)
if imageName == "menu" {
// Check To See The Detected Size Of Our menu Card (Should By 5cm*3cm)
let menuCardWidth = imageAnchor.referenceImage.physicalSize.width
let menuCardHeight = imageAnchor.referenceImage.physicalSize.height
print(
"""
We Have Detected menu Card With Name \(imageName)
\(imageName)'s Width Is \(menuCardWidth)
\(imageName)'s Height Is \(menuCardHeight)
""")
//raspberry
//cake 1
let cake_1_Plane = SCNPlane(width: 0.045, height: 0.045)
cake_1_Plane.firstMaterial?.diffuse.contents = UIImage(named: "france")
cake_1_Plane.cornerRadius = 0.01
let cake_1_PlaneNode = SCNNode(geometry: cake_1_Plane)
self.cake_1_PlaneNode = cake_1_PlaneNode
cake_1_PlaneNode.eulerAngles.x = -.pi/2
cake_1_PlaneNode.runAction(SCNAction.moveBy(x: 0.15, y: 0, z: -0.125, duration: 0.75))
node.addChildNode(cake_1_PlaneNode)
self.sceneView.scene.rootNode.addChildNode(node)
}
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
let touch = touches.first as! UITouch
if(touch.view == self.sceneView){
//print("touch working")
let viewTouchLocation:CGPoint = touch.location(in: sceneView)
guard let result = sceneView.hitTest(viewTouchLocation, options: nil).first else {
return
}
if let planeNode = cake_1_PlaneNode, cake_1_PlaneNode == result.node{
print("match")
cake_1()
}
}
}
func cake_1() {
let plane = SCNPlane(width: 0.15 , height: 0.15)
plane.firstMaterial?.diffuse.contents = UIColor.black.withAlphaComponent(0.75)
let planeNodee = SCNNode(geometry: plane)
planeNodee.eulerAngles.x = -.pi / 2
planeNodee.runAction(SCNAction.moveBy(x: 0.21, y: 0, z: 0, duration: 0))
} //cake_1
Follow this link: Detect touch on SCNNode in ARKit.
Looking at your code I can see several issues (not to mention the naming conventions for your variables and methods).
Firstly, you are creating a Global Variable which you have declared like so:
var cake_1_PlaneNode : SCNNode? = nil
However you use both a Local and Global Variable for your cake_1_PlaneNode in yourDelegate Callback:
let cake_1_PlaneNode = SCNNode(geometry: cake_1_Plane)
self.cake_1_PlaneNode = cake_1_PlaneNode
Which should simply read like so:
self.cake_1_PlaneNode = SCNNode(geometry: cake_1_Plane)
Secondly, you are adding your cake_1_PlaneNode to the rootNode of your ARSCNView rather than your detected ARImageAnchor which is probably what you don't want to do, since when an ARAnchor is detected:
You can provide visual content for the anchor by attaching geometry
(or other SceneKit features) to this node or by adding child nodes.
As such, this method (unless you actually want to do it like this) is unnecessary.
The remaining issues lie within your cake_1 function itself.
Firstly you are not actually adding your planeNodee to your sceneHierachy.
Since you haven't specified whether or not the newly initialised planeNode should be added directly to your ARSCNView or as a childNode of your cake_1_planeNode your function should include one of the following:
self.sceneView.scene.rootNode.addChildNode(planeNodee)
self.cake_1_planeNode.addChildNode(planeNodee)
In addition there is probably also no need to rotate your planeNodee since by default an SCNPlane is rendered vertically.
Since you haven't stipulated where you will be placing your content, it could be that using -.pi / 2 is unnecessary, since this could make it virtually invisible to the naked eye.
One other issue which could also account for you not seeing your node, when you actually add it, is the Z position.
If you set 2 nodes at the same position you will likely experience an issue know as Z-fighting (which you can read more about here). As such you should probably move your added node forward slightly when adding it e.g. SCNVector3 (0,0,0.001)to account for this.
Based on all of these points, I have provided a fully working and commented example below:
import UIKit
import ARKit
//-------------------------
//MARK: - ARSCNViewDelegate
//-------------------------
extension ViewController: ARSCNViewDelegate {
func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
//1. Check We Have An ARImageAnchor, Then Get It's Reference Image & Name
guard let imageAnchor = anchor as? ARImageAnchor else { return }
let detectedTarget = imageAnchor.referenceImage
guard let detectedTargetName = detectedTarget.name else { return }
//2. If We Have Detected Our Virtual Menu Then Add The CakeOnePlane
if detectedTargetName == "cakeMenu" {
let cakeOnePlaneGeometry = SCNPlane(width: 0.045, height: 0.045)
cakeOnePlaneGeometry.firstMaterial?.diffuse.contents = UIColor.cyan
cakeOnePlaneGeometry.cornerRadius = 0.01
let cakeOnPlaneNode = SCNNode(geometry: cakeOnePlaneGeometry)
cakeOnPlaneNode.eulerAngles.x = -.pi/2
//3. To Allow Us To Easily Keep Track Our Our Currently Added Node We Will Assign It A Unique Name
cakeOnPlaneNode.name = "Strawberry Cake"
node.addChildNode(cakeOnPlaneNode)
cakeOnPlaneNode.runAction(SCNAction.moveBy(x: 0.15, y: 0, z: 0, duration: 0.75))
}
}
}
class ViewController: UIViewController {
#IBOutlet var augmentedRealityView: ARSCNView!
let augmentedRealitySession = ARSession()
let configuration = ARWorldTrackingConfiguration()
//------------------
//MARK: - Life Cycle
//------------------
override func viewDidLoad() {
super.viewDidLoad()
setupARSession()
}
//-----------------
//MARK: - ARSession
//-----------------
/// Runs The ARSession
func setupARSession(){
//1. Load Our Detection Images
guard let detectionImages = ARReferenceImage.referenceImages(inGroupNamed: "AR Resources", bundle: nil) else { return }
//2. Configure & Run Our ARSession
augmentedRealityView.session = augmentedRealitySession
augmentedRealityView.delegate = self
configuration.detectionImages = detectionImages
augmentedRealitySession.run(configuration, options: [.resetTracking, .removeExistingAnchors])
}
//--------------------
//MARK: - Interaction
//--------------------
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
//1. Get The Current Touch Location & Perform An ARSCNHitTest To Check For Any Hit SCNNode's
guard let currentTouchLocation = touches.first?.location(in: self.augmentedRealityView),
let hitTestNode = self.augmentedRealityView.hitTest(currentTouchLocation, options: nil).first?.node else { return }
//2. If We Have Hit Our Strawberry Cake Then We Call Our makeCakeOnNode Function
if let cakeID = hitTestNode.name, cakeID == "Strawberry Cake"{
makeCakeOnNode(hitTestNode)
}
}
/// Adds An SCNPlane To A Detected Cake Target
///
/// - Parameter node: SCNNode
func makeCakeOnNode(_ node: SCNNode){
let planeGeometry = SCNPlane(width: 0.15 , height: 0.15)
planeGeometry.firstMaterial?.diffuse.contents = UIColor.black.withAlphaComponent(0.75)
let planeNode = SCNNode(geometry: planeGeometry)
planeNode.position = SCNVector3(0, 0, 0.001)
planeNode.runAction(SCNAction.moveBy(x: 0.21, y: 0, z: 0, duration: 0))
node.addChildNode(planeNode)
}
}
Which yields the following on my device:
For your information, this seems to show that your calculations for placing your content are off (unless of course this is the desired result).
As you can see, all of content rendered correctly, however the spacing of these was quite large, and as such you will likely need to pan your device somewhat to see it all when testing and developing further.
Hope it helps...
***Please use descriptive and clear names for your variables and functions, it is very hard to read and understand your code. You can read more about swift styling guidelines here: https://github.com/raywenderlich/swift-style-guide#naming
You are creating a new plane when the user touches the screen, but you are not adding that plane to the scene, therefore your "cake_1()" function only creates a new plane.
When ARKit detects an image, it automatically creates an empty node and adds it to our scene, at the center of the detected image. We must first keep a reference to the node ARKit has added for us when the image is detected.
Add this variable to the top of your class:
var detectedImageNode: SCNNode?
Then in func renderer(renderer: didAdd node:, for anchor:) add the following line:
detectedImageNode = node
Now that we have a reference to the node, we can easily add and remove other nodes.
Add the following line at the end of cake_1():
if let detectedImageNode = detectedImageNode {
cake_1_PlaneNode?.removeFromParentNode()
detectedImageNode.addChildNode(planeNodee)
}
Your final code should look like this:
func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
guard let imageAnchor = anchor as? ARImageAnchor else { return }
if let imageName = imageAnchor.referenceImage.name {
print(imageName)
if imageName == "menu" {
let cake_1_Plane = SCNPlane(width: 0.045, height: 0.045)
cake_1_Plane.firstMaterial?.diffuse.contents = UIImage(named: "france")
cake_1_Plane.cornerRadius = 0.01
let cake_1_PlaneNode = SCNNode(geometry: cake_1_Plane)
self.cake_1_PlaneNode = cake_1_PlaneNode
cake_1_PlaneNode.eulerAngles.x = -.pi/2
cake_1_PlaneNode.runAction(SCNAction.moveBy(x: 0.15, y: 0, z: -0.125, duration: 0.75))
node.addChildNode(cake_1_PlaneNode)
// No need to add the following line. The node is already added to the scene
//self.sceneView.scene.rootNode.addChildNode(node)
detectedImageNode = node
}
}
}
func cake_1() {
let plane = SCNPlane(width: 0.15 , height: 0.15)
plane.firstMaterial?.diffuse.contents = UIColor.black.withAlphaComponent(0.75)
let planeNodee = SCNNode(geometry: plane)
planeNodee.eulerAngles.x = -.pi / 2
if let detectedImageNode = detectedImageNode {
cake_1_PlaneNode?.removeFromParentNode()
detectedImageNode.addChildNode(planeNodee)
}
}
Alternative solution
If you are just trying to change the image of the plane then an easier way to approach this is to just change the texture of the plane.
Replace the contents of cake_1() with:
if let planeGeometry = cake_1_PlaneNode?.geometry {
planeGeometry.firstMaterial?.diffuse.contents = UIImage(named: "newImage")
}