Swift: ARKit place a rectangle based on 2 nodes - swift

My idea is that i want to place 2 sphere nodes at selected locations. From that point i basically want to draw a rectangle that will adjust the height with a slider. Basically that means that the 2 spheres will represent all 4 corners in the beginning. But when testing the code i use 1 meter height as a test.
The problem i have is that i cant seem to place the rectangle at the correct location as illustrated in the image below:
the rectangle in the image has a higher y-point than the points, it's rotated slightly and its center point is above node 2 and not in between the nodes. I don't want to use node.rotation as it wont work dynamically.
This is the code i use to place the 2 nodes and draw + add the rectangle.
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let location = touches.first?.location(in: sceneView) else {return}
let hitTest = sceneView.hitTest(location, types: [ARHitTestResult.ResultType.featurePoint])
guard let result = hitTest.last else {return}
// Converts the matrix_float4x4 to an SCNMatrix4 to be used with SceneKit
let transform = SCNMatrix4.init(result.worldTransform)
// Creates an SCNVector3 with certain indexes in the matrix
let vector = SCNVector3Make(transform.m41, transform.m42, transform.m43)
let node = addSphere(withPosition: vector)
nodeArray.append(node)
sceneView.scene.rootNode.addChildNode(node)
if nodeArray.count == 2 {
let node1 = sceneView.scene.rootNode.childNodes[0]
let node2 = sceneView.scene.rootNode.childNodes[1]
let bezeierPath = UIBezierPath()
bezeierPath.lineWidth = 0.01
bezeierPath.move(to: CGPoint(x: CGFloat(node1.position.x), y: CGFloat(node1.position.y)))
bezeierPath.addLine(to: CGPoint(x: CGFloat(node2.position.x), y: CGFloat(node2.position.y)))
bezeierPath.addLine(to: CGPoint(x: CGFloat(node2.position.x), y: CGFloat(node2.position.y+1.0)))
bezeierPath.addLine(to: CGPoint(x: CGFloat(node1.position.x), y: CGFloat(node1.position.y+1.0)))
bezeierPath.close()
bezeierPath.fill()
let shape = SCNShape(path: bezeierPath, extrusionDepth: 0.02)
shape.firstMaterial?.diffuse.contents = UIColor.red.withAlphaComponent(0.8)
let node = SCNNode.init(geometry: shape)
node.position = SCNVector3(CGFloat(abs(node1.position.x-node2.position.x)/2), CGFloat(abs((node1.position.y)-(node2.position.y))/2), CGFloat(node1.position.z))
sceneView.scene.rootNode.addChildNode(node)
}
}
Also note that this is not my final code. It will be refactored once i get everything working :).

This is currently working. I used the wrong calculation to set it in the middle of the 2 points.
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
// Check if a window is placed already and fetch location in sceneView
guard debugMode, let location = touches.first?.location(in: sceneView) else {return}
// Fetch targets at the current location
let hitTest = sceneView.hitTest(location, types: [ARHitTestResult.ResultType.featurePoint])
guard let result = hitTest.last else {return}
// Create sphere are position. getPosition() is an extension
createSphere(withPosition: result.getPosition())
// When 2 nodes has been placed, create a window and hide the spheres
if nodeArray.count == 2 {
let firstNode = nodeArray[nodeArray.count-1]
let secondNode = nodeArray[nodeArray.count-2]
firstNode.isHidden = true
secondNode.isHidden = true
createWindow(firstNode: firstNode, secondNode: secondNode)
sceneView.debugOptions = []
}
}
func createSphere(withPosition position: SCNVector3) {
// Create an geometry which you will apply materials to and convert to a node
let object = SCNSphere(radius: 0.01)
let material = SCNMaterial()
material.diffuse.contents = UIColor.red
object.materials = [material]
let node = SCNNode(geometry: object)
node.position = position
nodeArray.append(node)
sceneView.scene.rootNode.addChildNode(node)
}
func createWindow(firstNode: SCNNode, secondNode: SCNNode) {
// Create an geometry which you will apply materials to and convert to a node
let shape = SCNBox(width: CGFloat(abs(firstNode.worldPosition.x-secondNode.worldPosition.x)), height: 0.01, length: 0.02, chamferRadius: 0)
let material = SCNMaterial()
material.diffuse.contents = UIColor.red.withAlphaComponent(0.8)
// Each side of a cube can have different materials
// https://stackoverflow.com/questions/27509092/scnbox-different-colour-or-texture-on-each-face
shape.materials = [material]
let node = SCNNode(geometry: shape)
let minX = min(firstNode.worldPosition.x, secondNode.worldPosition.x)
let maxX = max(firstNode.worldPosition.x, secondNode.worldPosition.x)
// The nodes pivot Y-axis should be split in half of the nodes height, this will cause the node to
// only scale in one direction, when scaling it
// https://stackoverflow.com/questions/42568420/scenekit-understanding-the-pivot-property-of-scnnode/42613002#42613002
node.position = SCNVector3(((maxX-minX)/2)+minX, firstNode.worldPosition.y, firstNode.worldPosition.z)
node.pivot = SCNMatrix4MakeTranslation(0, 0.005, 0)
node.scale = SCNVector3Make(1, -50, 1)
node.name = "window"
sceneView.scene.rootNode.addChildNode(node)
}
Also added:
#objc func resetSceneView() {
// Clear all the nodes
sceneView.scene.rootNode.enumerateHierarchy { (node, stop) in
node.childNodes.forEach({
$0.removeFromParentNode()
})
node.removeFromParentNode()
}
// Reset the session
sceneView.session.pause()
sceneView.debugOptions = [ARSCNDebugOptions.showFeaturePoints]
sceneView.session.run(configuration, options: .resetTracking)
let matrix = sceneView.session.currentFrame!.camera.transform
sceneView.session.setWorldOrigin(relativeTransform: matrix)
nodeArray = []
}
To make sure that the world alignment is reset to your camera position

Related

Place node in front of the camera and rotate

I want to show a pointer when a user double taps on a sceneView. For this, I want to use SCNTorus because at some angle it is a circle.
let geometry = SCNTorus(ringRadius: 0.01, pipeRadius: 0.001)
let node = SCNNode(geometry: geometry)
I already get a vector from hitTest that I use as a position for the node
private func getVector(for point: CGPoint) -> SCNVector3? {
guard let hitTest = self.sceneView.hitTest(point, types: .featurePoint).first else {
return nil
}
let transform = SCNMatrix4.init(hitTest.worldTransform)
let vector = SCNVector3Make(transform.m41, transform.m42, transform.m43)
return vector
}
How can I transform it to always appear as a circle when the node added.
For vertical device position rotating by .pi/2 along X-axis works great but it breaks when devices move in a space, so I can not hardcode the angle
node.eulerAngles.x = .pi / 2
sceneView.scene.rootNode.addChildNode(node)
I believe I need to sceneView.pointOfView somehow to apply the transformation. But I stuck here.
Here is the full code
#objc private func didDoubleTap(_ sender: UITapGestureRecognizer) {
let point = sender.location(in: sceneView)
guard let vector = getVector(for: point) else { return }
guard let pov = sceneView.pointOfView else { return }
guard let camera = sceneView.session.currentFrame?.camera else { return }
let geometry = SCNTorus(ringRadius: 0.01, pipeRadius: 0.001)
let node = SCNNode(geometry: geometry)
node.position = vector
node.eulerAngles.x = .pi / 2
sceneView.scene.rootNode.addChildNode(node)
animatePointingNode(node)
}
I was able to solve the issue by setting eulerAngles.y to be equal to camera's Y and adjusting camera's X by .pi/2
guard let camera = sceneView.session.currentFrame?.camera else { return }
pointerNode.position = vector
pointerNode.eulerAngles.x = camera.eulerAngles.x + .pi / 2
pointerNode.eulerAngles.y = camera.eulerAngles.y

How to draw line node keep same size in camera as Measure App in iPhone?

I try make an AR app as a Measure default app in iPhone. ( I base on project TBXark/Ruler on github)
I draw startNode, endNode, cylinder line, and SCNText. But I can't manage the scale of size, it only readable in near, and so small when measure far plane detect.
I have 2 question:
How to keep size node, cylinder and text same when draw near or far as Measure App
How to draw scntext with background and align the same direction cylinder line as Measure App.
Here is my Line Node class:
class LineNode: NSObject {
let startNode: SCNNode
let endNode: SCNNode
var lineNode: SCNNode?
let textNode: SCNNode
let sceneView: ARSCNView?
// init func
init(startPos: SCNVector3,
sceneV: ARSCNView,
color: (start: UIColor, end: UIColor) = (UIColor(hexCss: 0xF1B426), UIColor(hexCss: 0xD43278)),
font: UIFont = UIFont.boldSystemFont(ofSize: 8) ) {
sceneView = sceneV
let scale = 1 / 400.0
let scaleVector = SCNVector3(scale, scale, scale)
func buildSCNSphere(color: UIColor) -> SCNSphere {
let dot = SCNSphere(radius: 1)
dot.firstMaterial?.diffuse.contents = color
dot.firstMaterial?.lightingModel = .constant
dot.firstMaterial?.isDoubleSided = true
return dot
}
// startNode
startNode = SCNNode(geometry: buildSCNSphere(color: color.start))
startNode.scale = scaleVector
startNode.position = startPos
sceneView?.scene.rootNode.addChildNode(startNode)
// endNode
endNode = SCNNode(geometry: buildSCNSphere(color: color.end))
endNode.scale = scaleVector
// line with start to end
lineNode = CylinderLine(parent: sceneView!.scene.rootNode,
v1: startNode.position,
v2: endNode.position,
radius: 0.001,
radSegmentCount: 16,
color: UIColor.white)
sceneView?.scene.rootNode.addChildNode(lineNode!)
// text show measure line length
let text = SCNText (string: "--", extrusionDepth: 0.1)
text.font = font
text.firstMaterial?.diffuse.contents = UIColor(hexCss: 0xffa800)
text.firstMaterial?.lightingModel = .constant
text.alignmentMode = CATextLayerAlignmentMode.center.rawValue
text.truncationMode = CATextLayerTruncationMode.middle.rawValue
text.firstMaterial?.isDoubleSided = true
textNode = SCNNode(geometry: text)
textNode.scale = SCNVector3(1 / 500.0, 1 / 500.0, 1 / 500.0)
super.init()
}
// update end node realtime
public func updatePosition(pos: SCNVector3, camera: ARCamera?, unit: MeasurementUnit.Unit = MeasurementUnit.Unit.centimeter) -> Float {
// update endNode
let posEnd = updateTransform(for: pos, camera: camera)
if endNode.parent == nil {
sceneView?.scene.rootNode.addChildNode(endNode)
}
endNode.position = posEnd
// caculate new mid
let posStart = startNode.position
let middle = SCNVector3((posStart.x + posEnd.x) / 2.0, (posStart.y + posEnd.y) / 2.0 + 0.002, (posStart.z + posEnd.z) / 2.0)
// update text measure
let text = textNode.geometry as! SCNText
let length = posEnd.distanceFromPos(pos: startNode.position)
text.string = MeasurementUnit(meterUnitValue: length).roundUpstring(type: unit)
text.materials.first?.diffuse.contents = UIColor.orange
textNode.setPivot()
textNode.position = middle
if textNode.parent == nil {
sceneView?.scene.rootNode.addChildNode(textNode)
}
lineNode?.removeFromParentNode()
lineNode = lineBetweenNodeA(nodeA: startNode, nodeB: endNode)
sceneView?.scene.rootNode.addChildNode(lineNode!)
return length
}
}
I use this to update scale if even if you stay far away it still readable
func updateScaleFromCameraForNodes(_ nodes: [SCNNode], fromPointOfView pointOfView: SCNNode , useScaling: Bool){
nodes.forEach { (node) in
//1. Get The Current Position Of The Node
let positionOfNode = SCNVector3ToGLKVector3(node.worldPosition)
//2. Get The Current Position Of The Camera
let positionOfCamera = SCNVector3ToGLKVector3(pointOfView.worldPosition)
//3. Calculate The Distance From The Node To The Camera
let distanceBetweenNodeAndCamera = GLKVector3Distance(positionOfNode, positionOfCamera)
let a = distanceBetweenNodeAndCamera*1.75
if(useScaling) {
node.simdScale = simd_float3(a,a,a)
}
}
SCNTransaction.flush()
}
then called it in the renderer updateAtTime
self.updateScaleFromCameraForNodes(self.nodesAdded, fromPointOfView:
self.cameraNode, useScaling: true)

Collision between two nodes not detected ARKit

I created two nodes: a sphere and a box:
var sphere = SCNNode(geometry: SCNSphere(radius: 0.005))
//I get the box node from scn file
let boxScene = SCNScene(named: "art.scnassets/world.scn")!
var boxNode: SCNNode?
I want two nodes or physicsBody's to interact, so I created a category for categoryBitMask and contactTestBitMask:
struct CollisionCategory: OptionSet {
let rawValue: Int
static let box = CollisionCategory(rawValue: 1)
static let sphere = CollisionCategory(rawValue: 2)
}
Here I set the box node as a physics body:
self.boxScene.rootNode.enumerateChildNodes { (node, _) in
if node.name == "box" {
boxNode = node
let boxBodyShape = SCNPhysicsShape(geometry: SCNBox(width: 0.1, height: 0.1, length: 0.1, chamferRadius: 0.1), options: nil)
let physicsBody = SCNPhysicsBody(type: .static, shape: boxBodyShape)
boxNode!.physicsBody = physicsBody
boxNode!.physicsBody?.categoryBitMask = CollisionCategory.box.rawValue
boxNode!.physicsBody?.contactTestBitMask = CollisionCategory.sphere.rawValue
boxNode!.physicsBody?.collisionBitMask = boxNode!.physicsBody!.contactTestBitMask
}
}
Here I set the sphere node in the render function, which you can move around the view:
func setUpSphere() {
let sphereBodySphere = SCNPhysicsShape(geometry: SCNSphere(radius: 0.005))
let physicsBody = SCNPhysicsBody(type: .kinematic, shape: sphereBodySphere)
sphere.physicsBody = physicsBody
sphere.physicsBody?.categoryBitMask = CollisionCategory.sphere.rawValue
sphere.physicsBody?.contactTestBitMask = CollisionCategory.box.rawValue
sphere.geometry?.firstMaterial?.diffuse.contents = UIColor.blue
sphere.physicsBody?.collisionBitMask = sphere.physicsBody!.contactTestBitMask
previousPoint = currentPosition
}
///It Adds a sphere and changes his position
func renderer(_ renderer: SCNSceneRenderer, willRenderScene scene: SCNScene, atTime time: TimeInterval) {
guard let pointOfView = sceneView.pointOfView else { return }
let mat = pointOfView.transform
let dir = SCNVector3(-1 * mat.m31, -1 * mat.m32, -1 * mat.m33)
let currentPosition = pointOfView.position + (dir * 0.185)
if buttonPressed {
if let previousPoint = previousPoint {
sphere.position = currentPosition
sceneView.scene.rootNode.addChildNode(sphere)
}
}
}
I added the protocol SCNPhysicsContactDelegate to the ViewController,
and I set in ViewDidLoad():
override func viewDidLoad() {
super.viewDidLoad()
sceneView.delegate = self
sceneView.scene.physicsWorld.contactDelegate = self
///I correctly see the shapes of the sphere and the box physics bodies using
sceneView.debugOptions = .showPhysicsShapes
createBox()
setUpSphere()
sceneView.scene = boxScene
sceneView.scene.physicsWorld.contactDelegate = self
}
Then I added that function:
func physicsWorld(_ world: SCNPhysicsWorld, didEnd contact: SCNPhysicsContact) {
print("Collision!")
}
This is what happens.
When the two nodes collide nothing happens, so I can't know if the two bodies are touching. Could the problem be about .kinematic, .static or about the function render()?
I followed step by step different tutorials about collisions in ARKit: Tutorial 1, Tutorial 2.
I have no idea why it doesn't work like expected.
Is there something wrong in my code?
Download file code link: https://ufile.io/20sla
willRenderScene is called uptown 60 times a second for every time the scene is going to be rendered. Since you're recreating the physics body every time it's probably messing up the physics engine determine collisions.
Try changing your code to only create the physics body once during setup.

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

SCNBox make the front side rotated to the camera in ARKit

I have a simple ARKit app. When the user touches the screen
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
let result = sceneView.hitTest(touch.location(in: sceneView), types: [ARHitTestResult.ResultType.featurePoint])
guard let hitResult = result.last else { return }
let hitTrasform = SCNMatrix4(hitResult.worldTransform)
let hitVector = SCNVector3Make(hitTrasform.m41, hitTrasform.m42, hitTrasform.m43)
createBox(position: hitVector)
}
I add a cube
func createBox(position: SCNVector3) {
let box = SCNBox(width: 0.1, height: 0.1, length: 0.1, chamferRadius: 1)
box.firstMaterial?.diffuse.contents = UIColor.black
let sides = [
UIColor.red, // Front
UIColor.black, // Right
UIColor.black, // Back
UIColor.black, // Left
UIColor.black, // Top
UIColor.black // Bottom
]
let materials = sides.map { (side) -> SCNMaterial in
let material = SCNMaterial()
material.diffuse.contents = side
material.locksAmbientWithDiffuse = true
return material
}
box.materials = materials
let boxNode = SCNNode(geometry: box)
boxNode.position = position
sceneView.scene.rootNode.addChildNode(boxNode)
}
When I add it for the first time, I see the front side and a bit of the right side. if I go around the cube on the other side and add another cube, I see the back side of the cube. So, can I do it so that the user sees only the front side whatever how the user goes around.
Thank you.
You can use SCNBillboardConstraint to make a given node face the camera. Just add boxNode.constraints = [SCNBillboardConstraint()] before adding it to the rootNode.