ARkit image recognition and AR visualization of the image - ios

In the company where I work I need to do this application. I have to recognize an image of a painting, and to visualize it in AR once recognized (in practice I find the real picture and above the painting in AR) I visualize the text or the selectable points with various characteristics of the picture in question. At the moment I have this code for the AR that recognizes the image in question and I visualize a plan above it. can you help me to create maybe a view above the picture with the features listed above?
import ARKit
import SceneKit
import UIKit
class ViewController: UIViewController, ARSCNViewDelegate {
#IBOutlet var sceneView: ARSCNView!
#IBOutlet weak var blurView: UIVisualEffectView!
/// The view controller that displays the status and "restart experience" UI.
lazy var statusViewController: StatusViewController = {
return childViewControllers.lazy.compactMap({ $0 as? StatusViewController }).first!
}()
/// A serial queue for thread safety when modifying the SceneKit node graph.
let updateQueue = DispatchQueue(label: Bundle.main.bundleIdentifier! +
".serialSceneKitQueue")
/// Convenience accessor for the session owned by ARSCNView.
var session: ARSession {
return sceneView.session
}
// MARK: - View Controller Life Cycle
override func viewDidLoad() {
super.viewDidLoad()
sceneView.delegate = self
sceneView.session.delegate = self
// Hook up status view controller callback(s).
statusViewController.restartExperienceHandler = { [unowned self] in
self.restartExperience()
}
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// Prevent the screen from being dimmed to avoid interuppting the AR experience.
UIApplication.shared.isIdleTimerDisabled = true
// Start the AR experience
resetTracking()
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
session.pause()
}
// MARK: - Session management (Image detection setup)
/// Prevents restarting the session while a restart is in progress.
var isRestartAvailable = true
/// Creates a new AR configuration to run on the `session`.
/// - Tag: ARReferenceImage-Loading
func resetTracking() {
guard let referenceImages = ARReferenceImage.referenceImages(inGroupNamed: "AR Resources", bundle: nil) else {
fatalError("Missing expected asset catalog resources.")
}
let configuration = ARWorldTrackingConfiguration()
configuration.detectionImages = referenceImages
session.run(configuration, options: [.resetTracking, .removeExistingAnchors])
statusViewController.scheduleMessage("Look around to detect images", inSeconds: 7.5, messageType: .contentPlacement)
}
// MARK: - ARSCNViewDelegate (Image detection results)
/// - Tag: ARImageAnchor-Visualizing
func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
guard let imageAnchor = anchor as? ARImageAnchor else { return }
let referenceImage = imageAnchor.referenceImage
updateQueue.async {
// Create a plane to visualize the initial position of the detected image.
let plane = SCNPlane(width: referenceImage.physicalSize.width,
height: referenceImage.physicalSize.height)
let planeNode = SCNNode(geometry: plane)
planeNode.opacity = 0.25
/*
`SCNPlane` is vertically oriented in its local coordinate space, but
`ARImageAnchor` assumes the image is horizontal in its local space, so
rotate the plane to match.
*/
planeNode.eulerAngles.x = -.pi / 2
/*
Image anchors are not tracked after initial detection, so create an
animation that limits the duration for which the plane visualization appears.
*/
planeNode.runAction(self.imageHighlightAction)
// Add the plane visualization to the scene.
node.addChildNode(planeNode)
}
DispatchQueue.main.async {
let imageName = referenceImage.name ?? ""
self.statusViewController.cancelAllScheduledMessages()
self.statusViewController.showMessage("Detected image “\(imageName)”")
}
}
var imageHighlightAction: SCNAction {
return .sequence([
.wait(duration: 0.25),
.fadeOpacity(to: 0.85, duration: 0.25),
.fadeOpacity(to: 0.15, duration: 0.25),
.fadeOpacity(to: 0.85, duration: 0.25),
.fadeOut(duration: 0.5),
.removeFromParentNode()
])
}
}

One way to tackle your problem is to create an SKScene and render it as an SCNMaterial.
Here is a fully commented example for you which makes use of the following ARSCNViewDelegate method:
//--------------------------
// MARK: - ARSCNViewDelegate
//--------------------------
extension ViewController: ARSCNViewDelegate {
func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
//1. Check We Have An ARImageAnchor And Have Detected Our Reference Image
guard let imageAnchor = anchor as? ARImageAnchor else { return }
let referenceImage = imageAnchor.referenceImage
//2. Get The Physical Width & Height Of Our Reference Image
let width = CGFloat(referenceImage.physicalSize.width)
let height = CGFloat(referenceImage.physicalSize.height)
//3. Create An SKScene Which We Will Display Above Our Target
let overlayNode = SCNNode()
let spriteKitScene = SKScene(size: CGSize(width: 600, height: 300))
spriteKitScene.backgroundColor = .clear
let imageName = SKLabelNode(text: "Line Friends")
imageName.position = CGPoint(x: 600/2, y: 240)
imageName.horizontalAlignmentMode = .center
imageName.fontSize = 60
imageName.fontName = "San Fransisco"
spriteKitScene.addChild(imageName)
let artistName = SKLabelNode(text: "By Line Coorporation")
artistName.position = CGPoint(x: 600/2, y: 180)
artistName.horizontalAlignmentMode = .center
artistName.fontSize = 60
artistName.fontName = "San Fransisco"
spriteKitScene.addChild(artistName)
let creationDate = SKLabelNode(text: "Created 2011")
creationDate.position = CGPoint(x: 600/2, y: 120)
creationDate.horizontalAlignmentMode = .center
creationDate.fontSize = 60
creationDate.fontName = "San Fransisco"
spriteKitScene.addChild(creationDate)
let planeHeight = height/2
let overlayGeometry = SCNPlane(width: width, height: planeHeight)
overlayNode.geometry = overlayGeometry
//4. Add The SpriteKit Scene As The SCNPlane's Geometry
overlayGeometry.firstMaterial?.diffuse.contents = spriteKitScene
//5. Rotate The Material Contents So It Isn't Backwards
overlayGeometry.firstMaterial?.diffuse.contentsTransform = SCNMatrix4Translate(SCNMatrix4MakeScale(1, -1, 1), 0, 1, 0)
//6. Rotate The Node
overlayNode.transform = SCNMatrix4MakeRotation(-Float.pi / 2, 1, 0, 0)
//7. Place It About The Target
let zPosition = height - (planeHeight/2)
overlayNode.position = SCNVector3(0, 0, -zPosition)
//8. Add It To The Scene
node.addChildNode(overlayNode)
}
}
Which yields something like the following:
Obviously if you have multiple image targets, you would create a func to dynamically create your 'info' overlay...
Hope it points you in the right direction... And apologies for the heinous spelling of corporation! ^_______*

Related

Draw custom polygon dynamically from manually added verticies in real time

I want to create app where you can set points in real world in detected surface and after you have 4 or more points it creates plane/polygon between them with texture. Basic controller configuration:
#IBOutlet weak var sceneView: ARSCNView!
let configuration = ARWorldTrackingConfiguration()
var vectors: [SCNVector3] = []
var mostBottomYAxis: Float? {
didSet {
for (index, _) in vectors.enumerated() {
vectors[index].y = self.mostBottomYAxis ?? vectors[index].y
}
}
}
var identifier: UUID?
override func viewDidLoad() {
super.viewDidLoad()
self.sceneView.debugOptions = [ARSCNDebugOptions.showFeaturePoints, ARSCNDebugOptions.showWorldOrigin]
self.sceneView.showsStatistics = true
self.configuration.worldAlignment = .gravity // Y axis is Up and Down
self.configuration.planeDetection = .horizontal // Detect horizontal surfaces
self.sceneView.session.run(configuration)
self.sceneView.autoenablesDefaultLighting = true
self.sceneView.delegate = self
self.registerGestureRecognizers()
}
Tap gesture method for adding vertex to surface:
#objc func handleTap(sender: UITapGestureRecognizer) {
let sceneView = sender.view as! ARSCNView
let tapLocation = sender.location(in: sceneView)
if let raycast = sceneView.raycastQuery(from: tapLocation, allowing: .estimatedPlane, alignment: .horizontal),
let result = sceneView.session.raycast(raycast).first {
self.addItem(raycastResult: result)
}
}
func addItem(raycastResult: ARRaycastResult) {
let transform = raycastResult.worldTransform
let thirdColumn = transform.columns.3
let vector = SCNVector3(x: thirdColumn.x, y: mostBottomYAxis ?? thirdColumn.y, z: thirdColumn.z)
self.vectors.append(vector)
self.addItem(toPosition: vector)
}
and methods for drawing polygon:
func getGeometry(forVectors vectors: [SCNVector3]) -> SCNGeometry {
let polygonIndices = Array(0...vectors.count).map({ Int32($0) })
let indices: [Int32] = [Int32(vectors.count)] + /* We have a polygon with this count of points */
polygonIndices /* The indices for our polygon */
let source = SCNGeometrySource(vertices: vectors)
let indexData = Data(bytes: indices,
count: indices.count * MemoryLayout<Int32>.size)
let element = SCNGeometryElement(data: indexData,
primitiveType: .polygon,
primitiveCount: 1,
bytesPerIndex: MemoryLayout<Int32>.size)
// MARK: - Texture
let textureCoordinates = [
CGPoint(x: 0, y: 0),
CGPoint(x: 1, y: 0),
CGPoint(x: 0, y: 1),
CGPoint(x: 1, y: 1)
]
let uvSource = SCNGeometrySource(textureCoordinates: textureCoordinates)
let geometry = SCNGeometry(sources: [source, uvSource],
elements: [element])
return geometry
}
func getNode(forVectors vectors: [SCNVector3]) -> SCNNode {
let polyDraw = getGeometry(forVectors: vectors)
let material = SCNMaterial()
material.isDoubleSided = true
material.diffuse.contents = UIImage(named: "Altezo_colormix_brilant")
material.diffuse.wrapS = .repeat
material.diffuse.wrapT = .repeat
polyDraw.materials = [material]
let node = SCNNode(geometry: polyDraw)
return node
}
extension PlacePointsForPlaneViewController: ARSCNViewDelegate {
func renderer(_ renderer: SCNSceneRenderer, didUpdate node: SCNNode, for anchor: ARAnchor) {
guard let planeAnchor = anchor as? ARPlaneAnchor else { return }
let y = planeAnchor.transform.columns.3.y
if mostBottomYAxis == nil {
mostBottomYAxis = y
identifier = planeAnchor.identifier
}
if mostBottomYAxis! > y {
mostBottomYAxis = y
identifier = planeAnchor.identifier
}
// Remove existing plane nodes
node.enumerateChildNodes { (childNode, _) in
childNode.removeFromParentNode()
}
if planeAnchor.identifier == self.identifier {
if self.vectors.count > 3 {
let modelNode = getNode(forVectors: self.vectors)
node.addChildNode(modelNode)
}
}
}
}
I hope it's mostly self explained. I have vectors which contains manually added points. I want to be working only with one surface that is most bottom (thats what do mostBottomYAxis and identifier).
What am I doing wrong? Why polygon is not drawn exactly between points? When I tried to draw line between two points it's working.
One more problem. How to set texture coordinates to correctly draw texture in polygon and to get it working not only for 4 verticies but for more (dynamically as user add more points and can change their positions)?
Thanks for help
Edit: After I post question I tried to different nodes to add my custom polygon and it's working fine if I added to sceneView rootNode:
sceneView.scene.rootNode.addChildNode(modelNode)
So this helps with position of polygon. But still I have problems with texture. How to set texture offset/transform to get it working? To have texture fill this custom geometry and repeat texture to edges.

Displaying a SwiftUI View in an ARKit app

I have an app where I detect an Image and show UI on a plane over the detected Images.
Previously I have had success in displaying a UIViewController on a Plane in a SceneKit ARKit app. I am trying to show a SwiftUI view on a plane on the detected image.
Here is the code I have.
func renderer(_ renderer: SCNSceneRenderer, nodeFor anchor: ARAnchor) -> SCNNode? {
// After detecting the image marker
let node = SCNNode()
guard let imageAnchor = anchor as? ARImageAnchor else { return nil }
guard let imageName = imageAnchor.name else { return nil }
let plane = SCNPlane(width: imageAnchor.referenceImage.physicalSize.width,
height: imageAnchor.referenceImage.physicalSize.height)
let planeNode = SCNNode(geometry: plane)
planeNode.eulerAngles.x = -.pi / 2
arViewController = createARController(node: planeNode)
node.addChildNode(planeNode)
return node
}
func createARController(node: SCNNode) -> ARViewController {
let arVC = UIStoryboard(name: "ARStoryboard", bundle: nil).instantiateInitialViewController() as! ARViewController
DispatchQueue.main.async {
arVC.planeNode = node
arVC.willMove(toParent: self)
self.addChild(arVC)
arVC.view.frame = CGRect(x: 0, y: 0, width: 500, height: 500)
self.view.addSubview(arVC.view)
}
self.show(viewController: arVC, on: node)
return arVC
}
private func show(viewController: ARViewController, on node: SCNNode) {
let material = SCNMaterial()
material.diffuse.contents = viewController.view
node.geometry?.materials = [material]
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1)) {
// add child vc with swiftui view here.
viewController.addARView()
}
}
class ARViewController: UIViewController {
var planeNode: SCNNode?
#IBOutlet weak var containerView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
print("ARViewController loaded")
// Do any additional setup after loading the view.
}
func addARView() {
let controller = UIHostingController(rootView: SwiftUIARView())
self.addChild(controller)
controller.view.frame = self.view.frame
view.addSubview(controller.view)
controller.didMove(toParent: self)
}
}
If I have a UIKit button on the ARViewController, I can see the button, but when the addARView() function gets called to initialize the UIHostingController, then I just see a black view being rendered. The actual view never gets rendered. Am I doing something wrong? or it is just not possible for now?
Has anyone had any luck rendering SwiftUI Views in ARKit SceneKit app?

Calculate image size corresponding to wall size in Augmented Reality and Drag it smoothly

I'm creating an app that choose image from gallery and place it on wall using ARKit vertical plane detection.I'm also able to scale it properly but dragging is not as expected. Also, i'm also confused about image size. I want it like suppose if wall is of 10 feet and i want to place image of 3*2 feet.I referred many links and docs.Below is the code that renders and add place image:
func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for
anchor: ARAnchor) {
if nodeWeCanChange == nil{
guard let planeAnchor = anchor as? ARPlaneAnchor else { return
}
let width = CGFloat(planeAnchor.extent.x)
let height = CGFloat(planeAnchor.extent.z)
nodeWeCanChange = SCNNode(geometry: SCNPlane(width: width,
height: height))
nodeWeCanChange?.position = SCNVector3(planeAnchor.center.x, 0,
planeAnchor.center.z)
nodeWeCanChange?.transform = SCNMatrix4MakeRotation(-Float.pi / 2.0, 1.0, 0.0, 0.0)
nodeWeCanChange?.geometry?.firstMaterial?.diffuse.contents = UIColor.white
DispatchQueue.main.async(execute: {
node.addChildNode(self.nodeWeCanChange!)
self.zIndex = (self.nodeWeCanChange?.simdPosition.z)!})
} }
}
func placeImage(image : UIImage){
guard let pointsPerInch = UIScreen.pointsPerInch else{
return}
let pixelWidth = (image.size.width) * (image.scale)
let pixelHeight = (image.size.height) * (image.scale)
let inchWidth = pixelWidth/pointsPerInch
let inchHeight = pixelHeight/pointsPerInch
let widthInMetres = (inchWidth * 2.54) / 100
let heightInMeters = (inchHeight * 2.54) / 100
let photo = SCNPlane(width: (widthInMetres), height: (heightInMeters))
nodeWeCanChange?.geometry = photo
nodeWeCanChange?.geometry?.firstMaterial?.diffuse.contents = image}`
#objc func handleTap(_ gestureRecognize: ThresholdPanGesture)
{
// Function that handles drag
let position = gestureRecognize.location(in: augentedRealityView)
var foundNode:SCNNode? = nil
do {
if gestureRecognize.state == .began {
print("Pan state began")
//let hitTestOptions = [SCNHitTestOption: Any]()
let hitResult: [SCNHitTestResult] = (self.augentedRealityView?.hitTest(gestureRecognize.location(in: self.augentedRealityView), options: nil))!
guard let firstNode = hitResult.first else {
return
}
print("first node =\(firstNode.node)")
if firstNode.node.isEqual(nodeWeCanChange){
foundNode = nodeWeCanChange
print("node found")}
else if (firstNode.node.parent?.isEqual(nodeWeCanChange))!{ print("node found")}
else{ return print("node not found")}
latestTranslatePos = position
}
}
if gestureRecognize.state == .changed{
let deltaX = Float(position.x - latestTranslatePos.x)/700
let deltaY = Float(position.y - latestTranslatePos.y)/700
nodeWeCanChange?.localTranslate(by: SCNVector3Make(deltaX, -deltaY , zIndex))
latestTranslatePos = position
print("Pan State Changed")
}
if gestureRecognize.state == .ended {
print("Done moving object")
let deltaX = Float(position.x - latestTranslatePos.x)/700
let deltaY = Float(position.y - latestTranslatePos.y)/700
nodeWeCanChange?.localTranslate(by: SCNVector3Make(deltaX, -deltaY , zIndex))
latestTranslatePos = position
foundNode = nil
}
}`
Can any one please help me what i'm missing?
There are two questions here... ^__________^.
In regard to panning your SCNNode the easiest thing to do would be to use touches, although you can modify the example below quite easily.
Assuming your content is placed on ARPlaneAnchor and you want to move it on the plane then you can do something like this (assuming you have also selected an SCNNode to be your nodeToDrag) which in my example will be the picture:
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
//1. Get The Current Touch Point
guard let currentTouchPoint = touches.first?.location(in: self.augmentedRealityView),
//2. Get The Existing ARPlaneAnchor
let hitTest = augmentedRealityView.hitTest(currentTouchPoint, types: .existingPlane).first else { return }
//3. Convert To World Coordinates
let worldTransform = hitTest.worldTransform
//4. Set The New Position
let newPosition = SCNVector3(worldTransform.columns.3.x, worldTransform.columns.3.y, worldTransform.columns.3.z)
//5. Apply To The Node
nodeToDrag.simdPosition = float3(newPosition.x, newPosition.y, newPosition.z)
}
Alternatively you can try the following:
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
//1. Get The Current Touch Point
guard let currentTouchPoint = touches.first?.location(in: self.augmentedRealityView),
//2. Get The Existing ARPlaneAnchor
let hitTest = augmentedRealityView.hitTest(currentTouchPoint, types: .existingPlane).first else { return }
//3. Convert To Local Coordinates
nodeToDrag.simdPosition.x = Float(hitTest.localTransform.columns.3.x)
nodeToDrag.simdPosition.y = Float(-hitTest.localTransform.columns.3.z)
}
In regard to the sizing you it's not entirely clear what you are wanting to do. However here is a small example with comments that should help:
func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
//1. Check We Have An ARPlaneAnchor
guard let planeAnchor = anchor as? ARPlaneAnchor else { return }
//2. Get The Approximate Width & Height Of The Anchor
let width = CGFloat(planeAnchor.extent.x)
let height = CGFloat(planeAnchor.extent.z)
//3. Create A Dummy Wall To Illustrate The Vertical Plane
let dummyWall = SCNNode(geometry: SCNPlane(width: width, height: height))
dummyWall.geometry?.firstMaterial?.diffuse.contents = UIColor.black
dummyWall.opacity = 0.5
dummyWall.eulerAngles.x = -.pi/2
node.addChildNode(dummyWall)
//4. Create A Picture Which Will Be Half The Size Of The Wall
let picture = SCNNode(geometry: SCNPlane(width: width/2, height: height/2))
picture.geometry?.firstMaterial?.diffuse.contents = UIColor.white
//5. Add It To The Dummy Wall (As We Dont Assign A Position It Will Be Centered Automatically At SCNVector3Zero)
dummyWall.addChildNode(picture)
//6. To Prevent Z-Fighting Move The Picture Forward Slightly
picture.position.z = 0.0001
}
You could also create a helper function e.g:
//--------------------------
//MARK: - CGFloat Extensions
//--------------------------
extension CGFloat{
/// Desired Scale Factor
enum Scalar{
case Quarter
case Third
case Half
case ThreeQuarters
case FullSize
var value: CGFloat{
switch self {
case .Quarter: return 0.25
case .Third: return 0.33
case .Half: return 0.5
case .ThreeQuarters: return 0.75
case .FullSize: return 1
}
}
}
/// Returns A CGFloat Based On The Desired Scale
///
/// - Parameter size: Scalar
/// - Returns: CGFloat
func scaledToSize(_ size: Scalar) -> CGFloat {
return self * size.value
}
}
Which you can use to scale your picture based on the extent of the anchor e.g:
let width = CGFloat(planeAnchor.extent.x)
let height = CGFloat(planeAnchor.extent.z)
let picture = SCNNode(geometry: SCNPlane(width: width.scaledToSize(.ThreeQuarters), height: height.scaledToSize(.ThreeQuarters)))
Update:
Since you were unable to get the example to work, and have now been more specific as to you requirements here is a fully working example (which needs a little tweaking) but it should be more than enough to point you in the right direction:
//-----------------------
//MARK: ARSCNViewDelegate
//-----------------------
extension ARViewController: ARSCNViewDelegate{
func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
if wallAndPictureNode != nil { return }
//1. Check We Have An ARPlaneAnchor
guard let planeAnchor = anchor as? ARPlaneAnchor else { return }
//2. Get The Approximate Width & Height Of The Anchor
let width = CGFloat(planeAnchor.extent.x)
let height = CGFloat(planeAnchor.extent.z)
//3. Create A Dummy Wall To Illustrate The Vertical Plane
let dummyWall = SCNNode(geometry: SCNPlane(width: width, height: height))
dummyWall.geometry?.firstMaterial?.diffuse.contents = UIColor.black
dummyWall.opacity = 0.5
dummyWall.eulerAngles.x = -.pi/2
node.addChildNode(dummyWall)
//4. Create A Picture Which Will Be Half The Size Of The Wall
let picture = SCNNode(geometry: SCNPlane(width: width/2, height: height/2))
picture.geometry?.firstMaterial?.diffuse.contents = UIColor.white
//5. Add It To The Dummy Wall (As We Dont Assign A Position It Will Be Centered Automatically At SCNVector3Zero)
dummyWall.addChildNode(picture)
//6. To Prevent Z-Fighting Move The Picture Forward Slightly
picture.position.z = 0.0001
//7. Set The Wall & Picture Node
wallAndPictureNode = dummyWall
}
}
class ARViewController: UIViewController {
//1. Create A Reference To Our ARSCNView In Our Storyboard Which Displays The Camera Feed
#IBOutlet weak var augmentedRealityView: ARSCNView!
//2. Create Our ARWorld Tracking Configuration
let configuration = ARWorldTrackingConfiguration()
//4. Create Our Session
let augmentedRealitySession = ARSession()
//5. Create Variable To Store Our Wall With The Picture On It
var wallAndPictureNode: SCNNode?
//--------------------
//MARK: View LifeCycle
//--------------------
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewDidAppear(_ animated: Bool) { setupARSession() }
override func didReceiveMemoryWarning() { super.didReceiveMemoryWarning() }
//-------------------
//MARK: - Interaction
//-------------------
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
//1. Get The Current Touch Point, Check We Have Our WallAndPictureNode, & Then Check We Have A Valid ARPlaneAnchor
guard let nodeToMove = wallAndPictureNode,
let currentTouchPoint = touches.first?.location(in: self.augmentedRealityView),
let hitTest = augmentedRealityView.hitTest(currentTouchPoint, types: .existingPlane).first else { return }
//3. Move The Wall & Picture
nodeToMove .simdPosition.x = Float(hitTest.worldTransform.columns.3.x)
nodeToMove .simdPosition.z = Float(-hitTest.worldTransform.columns.3.y)
}
//---------------
//MARK: ARSession
//---------------
/// Sets Up The ARSession
func setupARSession(){
//1. Set The AR Session
augmentedRealityView.session = augmentedRealitySession
augmentedRealityView.delegate = self
//2. Conifgure The Type Of Plane Detection
configuration.planeDetection = [.vertical]
//3. Configure The Debug Options
augmentedRealityView.debugOptions = debug(.None)
//4. Run The Session
augmentedRealitySession.run(configuration, options: [.resetTracking, .removeExistingAnchors])
//5. Disable The IdleTimer
UIApplication.shared.isIdleTimerDisabled = true
}
}
Hope it helps...

Using Multiple AR Image Anchors to display dynamic 3D Overlay

I'm working on a project wherein we have to detect a certain number of custom QR codes (as ARImageAnchors) and then using the position of these anchors to dynamically display a 3D overlay. To be exact, we are planning to dynamically display a 3D model of human anatomy over the anchors which will be placed on a mannequin. For example, the mannequin we are placing the QR codes on is smaller or bigger than the default size of the 3D model, we would like it to adapt based on the distances between the images. Below is the sample code I'm thinking of working off from (source: https://www.appcoda.com/arkit-image-recognition/).
import UIKit
import ARKit
class ViewController: UIViewController {
#IBOutlet weak var sceneView: ARSCNView!
#IBOutlet weak var label: UILabel!
let fadeDuration: TimeInterval = 0.3
let rotateDuration: TimeInterval = 3
let waitDuration: TimeInterval = 0.5
lazy var fadeAndSpinAction: SCNAction = {
return .sequence([
.fadeIn(duration: fadeDuration),
.rotateBy(x: 0, y: 0, z: CGFloat.pi * 360 / 180, duration: rotateDuration),
.wait(duration: waitDuration),
.fadeOut(duration: fadeDuration)
])
}()
lazy var fadeAction: SCNAction = {
return .sequence([
.fadeOpacity(by: 0.8, duration: fadeDuration),
.wait(duration: waitDuration),
.fadeOut(duration: fadeDuration)
])
}()
lazy var treeNode: SCNNode = {
guard let scene = SCNScene(named: "tree.scn"),
let node = scene.rootNode.childNode(withName: "tree", recursively: false) else { return SCNNode() }
let scaleFactor = 0.005
node.scale = SCNVector3(scaleFactor, scaleFactor, scaleFactor)
node.eulerAngles.x = -.pi / 2
return node
}()
lazy var bookNode: SCNNode = {
guard let scene = SCNScene(named: "book.scn"),
let node = scene.rootNode.childNode(withName: "book", recursively: false) else { return SCNNode() }
let scaleFactor = 0.1
node.scale = SCNVector3(scaleFactor, scaleFactor, scaleFactor)
return node
}()
lazy var mountainNode: SCNNode = {
guard let scene = SCNScene(named: "mountain.scn"),
let node = scene.rootNode.childNode(withName: "mountain", recursively: false) else { return SCNNode() }
let scaleFactor = 0.25
node.scale = SCNVector3(scaleFactor, scaleFactor, scaleFactor)
node.eulerAngles.x += -.pi / 2
return node
}()
override func viewDidLoad() {
super.viewDidLoad()
sceneView.delegate = self
configureLighting()
}
func configureLighting() {
sceneView.autoenablesDefaultLighting = true
sceneView.automaticallyUpdatesLighting = true
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
resetTrackingConfiguration()
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
sceneView.session.pause()
}
#IBAction func resetButtonDidTouch(_ sender: UIBarButtonItem) {
resetTrackingConfiguration()
}
func resetTrackingConfiguration() {
guard let referenceImages = ARReferenceImage.referenceImages(inGroupNamed: "AR Resources", bundle: nil) else { return }
let configuration = ARWorldTrackingConfiguration()
configuration.detectionImages = referenceImages
let options: ARSession.RunOptions = [.resetTracking, .removeExistingAnchors]
sceneView.session.run(configuration, options: options)
label.text = "Move camera around to detect images"
}
}
extension ViewController: ARSCNViewDelegate {
func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
DispatchQueue.main.async {
guard let imageAnchor = anchor as? ARImageAnchor,
let imageName = imageAnchor.referenceImage.name else { return }
// TODO: Comment out code
// let planeNode = self.getPlaneNode(withReferenceImage: imageAnchor.referenceImage)
// planeNode.opacity = 0.0
// planeNode.eulerAngles.x = -.pi / 2
// planeNode.runAction(self.fadeAction)
// node.addChildNode(planeNode)
// TODO: Overlay 3D Object
let overlayNode = self.getNode(withImageName: imageName)
overlayNode.opacity = 0
overlayNode.position.y = 0.2
overlayNode.runAction(self.fadeAndSpinAction)
node.addChildNode(overlayNode)
self.label.text = "Image detected: \"\(imageName)\""
}
}
func getPlaneNode(withReferenceImage image: ARReferenceImage) -> SCNNode {
let plane = SCNPlane(width: image.physicalSize.width,
height: image.physicalSize.height)
let node = SCNNode(geometry: plane)
return node
}
func getNode(withImageName name: String) -> SCNNode {
var node = SCNNode()
switch name {
case "Book":
node = bookNode
case "Snow Mountain":
node = mountainNode
case "Trees In the Dark":
node = treeNode
default:
break
}
return node
}
}
I know that the 3D overlay is displayed in the renderer function above, but it is only displaying on top of a single detected image anchor. Now my question is, is it possible to reference multiple ARImage anchors to dynamically display a single 3D model?
Being a novice in ARKit and Swift in general, I'm not sure how to go about this problem yet. I'm hoping someone might have an idea of how to work around this and point me to the right direction. Any help will be greatly appreciated!
Thanks in advance!

Why is SCNNode "jiggling" when dropped onto SCNPlane?

I have a SCNPlane that is added to the scene when a sufficient area is detected for a horizontal surface. The plane appears to be placed in a correct spot, according to the floor/table it's being placed on. The problem is when I drop a SCNNode(this has been consistent whether it was a box, pyramid, 3D-model, etc.) onto the plane, it will eventually find a spot to land and 99% start jiggling all crazy. Very few times has it just landed and not moved at all. I also think this may be cause by the node being dropped and landing slightly below the plane surface. It is not "on top" neither "below" the plane. Maybe the node is freaking out because it's kind of teetering between both levels?
Here is a video of what's going on, you can see at the beginning that the box is below and above the plane and the orange box does stop when it collides with the dark blue box, but does go back to its jiggling ways when the green box collides with it at the end:
The code is here on github
I will also show some of the relevant parts embedded in code:
I just create a Plane class to add to the scene when I need to
class Plane: SCNNode {
var anchor :ARPlaneAnchor
var planeGeometry :SCNPlane!
init(anchor :ARPlaneAnchor) {
self.anchor = anchor
super.init()
setup()
}
func update(anchor: ARPlaneAnchor) {
self.planeGeometry.width = CGFloat(anchor.extent.x)
self.planeGeometry.height = CGFloat(anchor.extent.z)
self.position = SCNVector3Make(anchor.center.x, 0, anchor.center.z)
let planeNode = self.childNodes.first!
planeNode.physicsBody = SCNPhysicsBody(type: .static, shape: SCNPhysicsShape(geometry: self.planeGeometry, options: nil))
}
private func setup() {
//plane dimensions
self.planeGeometry = SCNPlane(width: CGFloat(self.anchor.extent.x), height: CGFloat(self.anchor.extent.z))
//plane material
let material = SCNMaterial()
material.diffuse.contents = UIImage(named: "tronGrid.png")
self.planeGeometry.materials = [material]
//plane geometry and physics
let planeNode = SCNNode(geometry: self.planeGeometry)
planeNode.physicsBody = SCNPhysicsBody(type: .static, shape: SCNPhysicsShape(geometry: self.planeGeometry, options: nil))
planeNode.physicsBody?.categoryBitMask = BodyType.plane.rawValue
planeNode.position = SCNVector3Make(anchor.center.x, 0, anchor.center.z)
planeNode.transform = SCNMatrix4MakeRotation(Float(-Double.pi / 2.0), 1, 0, 0)
//add plane node
self.addChildNode(planeNode)
}
This is the ViewController
enum BodyType: Int {
case box = 1
case pyramid = 2
case plane = 3
}
class ViewController: UIViewController, ARSCNViewDelegate, SCNPhysicsContactDelegate {
//outlets
#IBOutlet var sceneView: ARSCNView!
//globals
var planes = [Plane]()
var boxes = [SCNNode]()
//life cycle
override func viewDidLoad() {
super.viewDidLoad()
//set sceneView's frame
self.sceneView = ARSCNView(frame: self.view.frame)
//add debugging option for sceneView (show x, y , z coords)
self.sceneView.debugOptions = [ARSCNDebugOptions.showFeaturePoints, ARSCNDebugOptions.showWorldOrigin]
//give lighting to the scene
self.sceneView.autoenablesDefaultLighting = true
//add subview to scene
self.view.addSubview(self.sceneView)
// Set the view's delegate
sceneView.delegate = self
//subscribe to physics contact delegate
self.sceneView.scene.physicsWorld.contactDelegate = self
//show statistics such as fps and timing information
sceneView.showsStatistics = true
//create new scene
let scene = SCNScene()
//set scene to view
sceneView.scene = scene
//setup recognizer to add scooter to scene
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(tapped))
sceneView.addGestureRecognizer(tapGestureRecognizer)
}
//MARK: helper funcs
#objc func tapped(recognizer: UIGestureRecognizer) {
let scnView = recognizer.view as! ARSCNView
let touchLocation = recognizer.location(in: scnView)
let touch = scnView.hitTest(touchLocation, types: .existingPlaneUsingExtent)
//take action if user touches box
if !touch.isEmpty {
guard let hitResult = touch.first else { return }
addBox(hitResult: hitResult)
}
}
private func addBox(hitResult: ARHitTestResult) {
let boxGeometry = SCNBox(width: 0.1,
height: 0.1,
length: 0.1,
chamferRadius: 0)
let material = SCNMaterial()
material.diffuse.contents = UIColor(red: .random(),
green: .random(),
blue: .random(),
alpha: 1.0)
boxGeometry.materials = [material]
let boxNode = SCNNode(geometry: boxGeometry)
//adding physics body, a box already has a shape, so nil is fine
boxNode.physicsBody = SCNPhysicsBody(type: .dynamic, shape: nil)
//set bitMask on boxNode, enabling objects with diff categoryBitMasks to collide w/ each other
boxNode.physicsBody?.categoryBitMask = BodyType.plane.rawValue | BodyType.box.rawValue
boxNode.position = SCNVector3(hitResult.worldTransform.columns.3.x,
hitResult.worldTransform.columns.3.y + 0.3,
hitResult.worldTransform.columns.3.z)
self.sceneView.scene.rootNode.addChildNode(boxNode)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
let configuration = ARWorldTrackingConfiguration()
configuration.planeDetection = .horizontal
//track objects in ARWorld and start session
sceneView.session.run(configuration)
}
//MARK: - ARSCNViewDelegate
func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
//if no anchor found, don't render anything!
if !(anchor is ARPlaneAnchor) {
return
}
DispatchQueue.main.async {
//add plane to scene
let plane = Plane(anchor: anchor as! ARPlaneAnchor)
self.planes.append(plane)
node.addChildNode(plane)
//add initial scene object
let pyramidGeometry = SCNPyramid(width: CGFloat(plane.planeGeometry.width / 8), height: plane.planeGeometry.height / 8, length: plane.planeGeometry.height / 8)
pyramidGeometry.firstMaterial?.diffuse.contents = UIColor.white
let pyramidNode = SCNNode(geometry: pyramidGeometry)
pyramidNode.name = "pyramid"
pyramidNode.physicsBody = SCNPhysicsBody(type: .dynamic, shape: nil)
pyramidNode.physicsBody?.categoryBitMask = BodyType.pyramid.rawValue | BodyType.plane.rawValue
pyramidNode.physicsBody?.contactTestBitMask = BodyType.box.rawValue
pyramidNode.position = SCNVector3(-(plane.planeGeometry.width) / 3, 0, plane.planeGeometry.height / 3)
node.addChildNode(pyramidNode)
}
}
func renderer(_ renderer: SCNSceneRenderer, didUpdate node: SCNNode, for anchor: ARAnchor) {
let plane = self.planes.filter {
plane in return plane.anchor.identifier == anchor.identifier
}.first
if plane == nil {
return
}
plane?.update(anchor: anchor as! ARPlaneAnchor)
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
//pause session
sceneView.session.pause()
}
}
I think i followed the same tutorial. I also had same result. Reason is because when the cube drops from higher place, it accelerates and doesnot exactly hit on the plane but passes through. If you scale down the cube to '1 mm' you can see box completely passes through plane and continue falling below plane. You can try droping cube from nearer to the plane, box drops slower and this 'jiggling' will not occur. Or you can try with box with small height instead of plane.
I had the same problem i found out one solution.I was initializing the ARSCNView programmatically.I just removed those code and just added a ARSCNView in the storyboard joined it in my UIViewcontroller class using IBOutlet it worked like a charm.
Hope it helps anyone who is going through this problem.
The same code is below.
#IBOutlet var sceneView: ARSCNView!
override func viewDidLoad() {
super.viewDidLoad()
self.sceneView.debugOptions = [ARSCNDebugOptions.showFeaturePoints,ARSCNDebugOptions.showWorldOrigin]
sceneView.delegate = self
sceneView.showsStatistics = true
let scene = SCNScene()
sceneView.scene = scene
}
The "jiggling" is probably caused by an incorrect gravity vector. Try experimenting with setting the gravity of your scene.
For example, add this to your viewDidLoad function:
sceneView.scene.physicsWorld.gravity = SCNVector3Make(0.0, -1.0, 0.0)
I found that setting the gravity - either through code, or by loading an empty scene - resolves this issue.

Resources