ARKit ImageDetection - get reference image when tapping 3D object - ios

I want to create an App, that detects reference images, then a 3D (SCNScene) object appears (multiple images / objects in 1 Camera is possible). This is already running.
Now, when the user taps on the object, I need to know the file-name of the referenceImage, because the image should be shown.
import UIKit
import SceneKit
import ARKit
class ViewController: UIViewController, ARSCNViewDelegate {
#IBOutlet var sceneView: ARSCNView!
private var planeNode: SCNNode?
private var imageNode: SCNNode?
private var animationInfo: AnimationInfo?
private var currentMediaName: String?
override func viewDidLoad() {
super.viewDidLoad()
let scene = SCNScene()
sceneView.scene = scene
sceneView.delegate = self
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// Load reference images to look for from "AR Resources" folder
guard let referenceImages = ARReferenceImage.referenceImages(inGroupNamed: "AR Resources", bundle: nil) else {
fatalError("Missing expected asset catalog resources.")
}
// Create a session configuration
let configuration = ARWorldTrackingConfiguration()
// Add previously loaded images to ARScene configuration as detectionImages
configuration.detectionImages = referenceImages
// Run the view's session
sceneView.session.run(configuration)
let tap = UITapGestureRecognizer(target: self, action: #selector(handleTap(rec:)))
//Add recognizer to sceneview
sceneView.addGestureRecognizer(tap)
}
//Method called when tap
#objc func handleTap(rec: UITapGestureRecognizer){
//GET Reference-Image Name
loadReferenceImage()
if rec.state == .ended {
let location: CGPoint = rec.location(in: sceneView)
let hits = self.sceneView.hitTest(location, options: nil)
if !hits.isEmpty{
let tappedNode = hits.first?.node
}
}
}
func loadReferenceImage(){
print("CLICK")
}
func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
guard let imageAnchor = anchor as? ARImageAnchor else {
return
}
currentMediaName = imageAnchor.referenceImage.name
// 1. Load plane's scene.
let planeScene = SCNScene(named: "art.scnassets/plane.scn")!
let planeNode = planeScene.rootNode.childNode(withName: "planeRootNode", recursively: true)!
// 2. Calculate size based on planeNode's bounding box.
let (min, max) = planeNode.boundingBox
let size = SCNVector3Make(max.x - min.x, max.y - min.y, max.z - min.z)
// 3. Calculate the ratio of difference between real image and object size.
// Ignore Y axis because it will be pointed out of the image.
let widthRatio = Float(imageAnchor.referenceImage.physicalSize.width)/size.x
let heightRatio = Float(imageAnchor.referenceImage.physicalSize.height)/size.z
// Pick smallest value to be sure that object fits into the image.
let finalRatio = [widthRatio, heightRatio].min()!
// 4. Set transform from imageAnchor data.
planeNode.transform = SCNMatrix4(imageAnchor.transform)
// 5. Animate appearance by scaling model from 0 to previously calculated value.
let appearanceAction = SCNAction.scale(to: CGFloat(finalRatio), duration: 0.4)
appearanceAction.timingMode = .easeOut
// Set initial scale to 0.
planeNode.scale = SCNVector3Make(0.0, 0.0, 0.0)
// Add to root node.
sceneView.scene.rootNode.addChildNode(planeNode)
// Run the appearance animation.
planeNode.runAction(appearanceAction)
self.planeNode = planeNode
self.imageNode = node
}
func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor, updateAtTime time: TimeInterval) {
guard let imageNode = imageNode, let planeNode = planeNode else {
return
}
// 1. Unwrap animationInfo. Calculate animationInfo if it is nil.
guard let animationInfo = animationInfo else {
refreshAnimationVariables(startTime: time,
initialPosition: planeNode.simdWorldPosition,
finalPosition: imageNode.simdWorldPosition,
initialOrientation: planeNode.simdWorldOrientation,
finalOrientation: imageNode.simdWorldOrientation)
return
}
// 2. Calculate new animationInfo if image position or orientation changed.
if !simd_equal(animationInfo.finalModelPosition, imageNode.simdWorldPosition) || animationInfo.finalModelOrientation != imageNode.simdWorldOrientation {
refreshAnimationVariables(startTime: time,
initialPosition: planeNode.simdWorldPosition,
finalPosition: imageNode.simdWorldPosition,
initialOrientation: planeNode.simdWorldOrientation,
finalOrientation: imageNode.simdWorldOrientation)
}
// 3. Calculate interpolation based on passedTime/totalTime ratio.
let passedTime = time - animationInfo.startTime
var t = min(Float(passedTime/animationInfo.duration), 1)
// Applying curve function to time parameter to achieve "ease out" timing
t = sin(t * .pi * 0.5)
// 4. Calculate and set new model position and orientation.
let f3t = simd_make_float3(t, t, t)
planeNode.simdWorldPosition = simd_mix(animationInfo.initialModelPosition, animationInfo.finalModelPosition, f3t)
planeNode.simdWorldOrientation = simd_slerp(animationInfo.initialModelOrientation, animationInfo.finalModelOrientation, t)
//planeNode.simdWorldOrientation = imageNode.simdWorldOrientation
guard let currentImageAnchor = anchor as? ARImageAnchor else { return }
let name = currentImageAnchor.referenceImage.name!
print("TEST")
print(name)
}
func refreshAnimationVariables(startTime: TimeInterval, initialPosition: float3, finalPosition: float3, initialOrientation: simd_quatf, finalOrientation: simd_quatf) {
let distance = simd_distance(initialPosition, finalPosition)
// Average speed of movement is 0.15 m/s.
let speed = Float(0.15)
// Total time is calculated as distance/speed. Min time is set to 0.1s and max is set to 2s.
let animationDuration = Double(min(max(0.1, distance/speed), 2))
// Store animation information for later usage.
animationInfo = AnimationInfo(startTime: startTime,
duration: animationDuration,
initialModelPosition: initialPosition,
finalModelPosition: finalPosition,
initialModelOrientation: initialOrientation,
finalModelOrientation: finalOrientation)
}
}

Since your ARReferenceImage is stored within the Assets.xcassets catalogue you can simply load your image using the following initialization method of UIImage:
init?(named name: String)
For your information:
if this is the first time the image is being
loaded, the method looks for an image with the specified name in the
application’s main bundle. For PNG images, you may omit the filename
extension. For all other file formats, always include the filename
extension.
In my example I have an ARReferenceImage named TargetCard:
So to load it as a UIImage and then apply it as an SCNNode or display it in screenSpace you could so something like so:
//1. Load The Image Onto An SCNPlaneGeometry
if let image = UIImage(named: "TargetCard"){
let planeNode = SCNNode()
let planeGeometry = SCNPlane(width: 1, height: 1)
planeGeometry.firstMaterial?.diffuse.contents = image
planeNode.geometry = planeGeometry
planeNode.position = SCNVector3(0, 0, -1.5)
self.augmentedRealityView.scene.rootNode.addChildNode(planeNode)
}
//2. Load The Image Into A UIImageView
if let image = UIImage(named: "TargetCard"){
let imageView = UIImageView(frame: CGRect(x: 10, y: 10, width: 300, height: 150))
imageView.image = image
imageView.contentMode = .scaleAspectFill
self.view.addSubview(imageView)
}
In your context:
Each SCNNode has a name property:
var name: String? { get set }
As such I suggest that when you create content in regard to your ARImageAnchor you provide it with the name of your ARReferenceImage e.g:
//---------------------------
// MARK: - ARSCNViewDelegate
//---------------------------
extension ViewController: ARSCNViewDelegate{
func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
//1. Check We Have Detected An ARImageAnchor & Check It's The One We Want
guard let validImageAnchor = anchor as? ARImageAnchor,
let targetName = validImageAnchor.referenceImage.name else { return}
//2. Create An SCNNode With An SCNPlaneGeometry
let nodeToAdd = SCNNode()
let planeGeometry = SCNPlane(width: 1, height: 1)
planeGeometry.firstMaterial?.diffuse.contents = UIColor.cyan
nodeToAdd.geometry = planeGeometry
//3. Set It's Name To That Of Our ARReferenceImage
nodeToAdd.name = targetName
//4. Add It To The Hierachy
node.addChildNode(nodeToAdd)
}
}
Then it is easy to get a reference to the Image later e.g:
/// Checks To See If We Have Hit A Named SCNNode
///
/// - Parameter gesture: UITapGestureRecognizer
#objc func handleTap(_ gesture: UITapGestureRecognizer){
//1. Get The Current Touch Location
let currentTouchLocation = gesture.location(in: self.augmentedRealityView)
//2. Perform An SCNHitTest To See If We Have Tapped A Valid SCNNode & See If It Is Named
guard let hitTestForNode = self.augmentedRealityView.hitTest(currentTouchLocation, options: nil).first?.node,
let nodeName = hitTestForNode.name else { return }
//3. Load The Reference Image
self.loadReferenceImage(nodeName, inAR: true)
}
/// Loads A Matching Image For The Identified ARReferenceImage Name
///
/// - Parameters:
/// - fileName: String
/// - inAR: Bool
func loadReferenceImage(_ fileName: String, inAR: Bool){
if inAR{
//1. Load The Image Onto An SCNPlaneGeometry
if let image = UIImage(named: fileName){
let planeNode = SCNNode()
let planeGeometry = SCNPlane(width: 1, height: 1)
planeGeometry.firstMaterial?.diffuse.contents = image
planeNode.geometry = planeGeometry
planeNode.position = SCNVector3(0, 0, -1.5)
self.augmentedRealityView.scene.rootNode.addChildNode(planeNode)
}
}else{
//2. Load The Image Into A UIImageView
if let image = UIImage(named: fileName){
let imageView = UIImageView(frame: CGRect(x: 10, y: 10, width: 300, height: 150))
imageView.image = image
imageView.contentMode = .scaleAspectFill
self.view.addSubview(imageView)
}
}
}
Important:
One thing I have just discovered is that if we load the the ARReferenceImage e.g:
let image = UIImage(named: "TargetCard")
Then the image is displayed is in GrayScale, which is properly what you dont want!
As such what you probably need to do is to copy the ARReferenceImage into the Assets Catalogue and give it a prefix e.g. ColourTargetCard...
Then you would need to change the function slightly by naming your nodes using a prefix e.g:
nodeToAdd.name = "Colour\(targetName)"
Hope it helps...

Related

How to play a video using on Camera (SCNNode) Image Tracking using Swift

I am new to ARKit . I am using Image Tracking to detect the image and dislpay the content beside that Image ..
On the left side of the image I am displaying some information of the image and on the right side I am showing the Web View .
I just want to display some video over the image (top).
Can you guys please help me to display a video over the image (top). I have attached the code for the Info and webview similary i want display to the video
func displayDetailView(on rootNode: SCNNode, xOffset: CGFloat) {
let detailPlane = SCNPlane(width: xOffset, height: xOffset * 1.4)
detailPlane.cornerRadius = 0.25
let detailNode = SCNNode(geometry: detailPlane)
detailNode.geometry?.firstMaterial?.diffuse.contents = SKScene(fileNamed: "DetailScene")
// Due to the origin of the iOS coordinate system, SCNMaterial's content appears upside down, so flip the y-axis.
detailNode.geometry?.firstMaterial?.diffuse.contentsTransform = SCNMatrix4Translate(SCNMatrix4MakeScale(1, -1, 1), 0, 1, 0)
detailNode.position.z -= 0.5
detailNode.opacity = 0
rootNode.addChildNode(detailNode)
detailNode.runAction(.sequence([
.wait(duration: 1.0),
.fadeOpacity(to: 1.0, duration: 1.5),
.moveBy(x: xOffset * -1.1, y: 0, z: -0.05, duration: 1.5),
.moveBy(x: 0, y: 0, z: -0.05, duration: 0.2)
])
)
}
func displayWebView(on rootNode: SCNNode, xOffset: CGFloat) {
// Xcode yells at us about the deprecation of UIWebView in iOS 12.0, but there is currently
// a bug that does now allow us to use a WKWebView as a texture for our webViewNode
// Note that UIWebViews should only be instantiated on the main thread!
DispatchQueue.main.async {
let request = URLRequest(url: URL(string: "https://www.youtube.com/watch?v=QvzVCOiC-qs")!)
let webView = UIWebView(frame: CGRect(x: 0, y: 0, width: 400, height: 672))
webView.loadRequest(request)
let webViewPlane = SCNPlane(width: xOffset, height: xOffset * 1.4)
webViewPlane.cornerRadius = 0.25
let webViewNode = SCNNode(geometry: webViewPlane)
webViewNode.geometry?.firstMaterial?.diffuse.contents = webView
webViewNode.position.z -= 0.5
webViewNode.opacity = 0
rootNode.addChildNode(webViewNode)
webViewNode.runAction(.sequence([
.wait(duration: 3.0),
.fadeOpacity(to: 1.0, duration: 1.5),
.moveBy(x: xOffset * 1.1, y: 0, z: -0.05, duration: 1.5),
.moveBy(x: 0, y: 0, z: -0.05, duration: 0.2)
])
)
}
}
I had called this methods in the below function ..
func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
guard let imageAnchor = anchor as? ARImageAnchor else { return }
updateQueue.async {
let physicalWidth = imageAnchor.referenceImage.physicalSize.width
let physicalHeight = imageAnchor.referenceImage.physicalSize.height
// Create a plane geometry to visualize the initial position of the detected image
let mainPlane = SCNPlane(width: physicalWidth, height: physicalHeight)
mainPlane.firstMaterial?.colorBufferWriteMask = .alpha
// Create a SceneKit root node with the plane geometry to attach to the scene graph
// This node will hold the virtual UI in place
let mainNode = SCNNode(geometry: mainPlane)
mainNode.eulerAngles.x = -.pi / 2
mainNode.renderingOrder = -1
mainNode.opacity = 1
// Add the plane visualization to the scene
node.addChildNode(mainNode)
// Perform a quick animation to visualize the plane on which the image was detected.
// We want to let our users know that the app is responding to the tracked image.
self.highlightDetection(on: mainNode, width: physicalWidth, height: physicalHeight, completionHandler: {
// Introduce virtual content
self.displayDetailView(on: mainNode, xOffset: physicalWidth)
// Animate the WebView to the right
self.displayWebView(on: mainNode, xOffset: physicalWidth)
})
}
}
Any help is Appreciated ..
The effect you are trying to achieve is to play a video on a SCNNode. To do this, in your renderer func, you need to create an AVPlayer and use it to create a SKVideoNode. Then you need to create an SKScene, add the SKVideoNode. Next set the texture of your plane to that SKScene.
so this in the context of your code above:
func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
guard let imageAnchor = anchor as? ARImageAnchor else { return }
updateQueue.async {
let physicalWidth = imageAnchor.referenceImage.physicalSize.width
let physicalHeight = imageAnchor.referenceImage.physicalSize.height
// Create a plane geometry to visualize the initial position of the detected image
let mainPlane = SCNPlane(width: physicalWidth, height: physicalHeight)
mainPlane.firstMaterial?.colorBufferWriteMask = .alpha
// Create a SceneKit root node with the plane geometry to attach to the scene graph
// This node will hold the virtual UI in place
let mainNode = SCNNode(geometry: mainPlane)
mainNode.eulerAngles.x = -.pi / 2
mainNode.renderingOrder = -1
mainNode.opacity = 1
// Add the plane visualization to the scene
node.addChildNode(mainNode)
// Perform a quick animation to visualize the plane on which the image was detected.
// We want to let our users know that the app is responding to the tracked image.
self.highlightDetection(on: mainNode, width: physicalWidth, height: physicalHeight, completionHandler: {
// Introduce virtual content
self.displayDetailView(on: mainNode, xOffset: physicalWidth)
// Animate the WebView to the right
self.displayWebView(on: mainNode, xOffset: physicalWidth)
// setup AV player & create SKVideoNode from avPlayer
let videoURL = URL(fileURLWithPath: Bundle.main.path(forResource: videoAssetName, ofType: videoAssetExtension)!)
let player = AVPlayer(url: videoURL)
player.actionAtItemEnd = .none
videoPlayerNode = SKVideoNode(avPlayer: player)
// setup SKScene to hold the SKVideoNode
let skSceneSize = CGSize(width: physicalWidth, height: physicalHeight)
let skScene = SKScene(size: skSceneSize)
skScene.addChild(videoPlayerNode)
videoPlayerNode.position = CGPoint(x: skScene.size.width/2, y: skScene.size.height/2)
videoPlayerNode.size = skScene.size
// Set the SKScene as the texture for the main plane
mainPlane.firstMaterial?.diffuse.contents = skScene
mainPlane.firstMaterial?.isDoubleSided = true
// setup node to auto remove itself upon completion
NotificationCenter.default.addObserver(forName: .AVPlayerItemDidPlayToEndTime, object: player.currentItem, queue: nil, using: { (_) in
DispatchQueue.main.async {
if self.debug { NSLog("video completed") }
// do something when the video ends
}
})
// play the video node
videoPlayerNode.play()
})
}
}
For 2022 ..
This is now (conceptually) simple,
guard let url = URL(string: " ... ") else { return }
let vid = AVPlayer(url: url)
.. some node .. .geometry?.firstMaterial?.diffuse.contents = vid
vid.play()
It's that easy.
You'll spend a lot of time monkeying with the mesh, buffers etc to get a good result, depending on what you're doing.

How do i detect multiple images in AR?

I am creating this live newspaper app with ARKit which transforms images on newspaper into videos.I am able to detect one image and play a video on it but when i try doing it on two images and play a corresponding video on that images i get an error like
Attempted to add a SKNode which already has a parent
I tried by checking tracked images and comparing them to reference images but i think something is wrong with my logic.
This is my ViewDidAppear() Method
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// Create a session configuration
let configuration = ARImageTrackingConfiguration()
if let trackedImages = ARReferenceImage.referenceImages(inGroupNamed: "NewsPaperImages", bundle: Bundle.main) {
configuration.trackingImages = trackedImages
configuration.maximumNumberOfTrackedImages = 20
}
// Run the view's session
sceneView.session.run(configuration)
}
func renderer(_ renderer: SCNSceneRenderer, nodeFor anchor: ARAnchor) -> SCNNode? {
let node = SCNNode()
if let imageAnchor = anchor as? ARImageAnchor {
if(imageAnchor.referenceImage == UIImage(named: "Image2_River")) {
self.videoNode = SKVideoNode(fileNamed: "riverBeauty.mp4")
}
print("Yes it is an image")
self.videoNode.play()
let videoScene = SKScene(size: CGSize(width: 480, height: 360))
videoNode.position = CGPoint(x: videoScene.size.width/2, y: videoScene.size.height/2)
videoNode.yScale = -1.0
videoScene.addChild(videoNode)
let plane = SCNPlane(width: imageAnchor.referenceImage.physicalSize.width, height: imageAnchor.referenceImage.physicalSize.height)
plane.firstMaterial?.diffuse.contents = videoScene
let planeNode = SCNNode(geometry: plane)
planeNode.eulerAngles.x = -.pi/2
node.addChildNode(planeNode)
}
return node
}
it should be playing video on every detected image but instead it crashes.
Looking at your code, and from what you have said I think that you need to change your logic, to create a Video Node using the name property of your ARReferenceImage instead.
When you create an ARReferenceImage either statically (within the ARResourceBundle) or dynamically you can assign it a name e.g:
Then you can use the names to add logic to assign a different video to each referenceImage detected.
And in order to keep your code DRY you could create a reusable function to create your video node.
As such a simple example might look something like so:
//-------------------------
//MARK: - ARSCNViewDelegate
//-------------------------
extension ViewController: ARSCNViewDelegate{
func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
//1. Check We Have Detected An ARImageAnchor
guard let validAnchor = anchor as? ARImageAnchor else { return }
//2. Create A Video Player Node For Each Detected Target
node.addChildNode(createdVideoPlayerNodeFor(validAnchor.referenceImage))
}
/// Creates An SCNNode With An AVPlayer Rendered Onto An SCNPlane
///
/// - Parameter target: ARReferenceImage
/// - Returns: SCNNode
func createdVideoPlayerNodeFor(_ target: ARReferenceImage) -> SCNNode{
//1. Create An SCNNode To Hold Our VideoPlayer
let videoPlayerNode = SCNNode()
//2. Create An SCNPlane & An AVPlayer
let videoPlayerGeometry = SCNPlane(width: target.physicalSize.width, height: target.physicalSize.height)
var videoPlayer = AVPlayer()
//3. If We Have A Valid Name & A Valid Video URL The Instanciate The AVPlayer
if let targetName = target.name,
let validURL = Bundle.main.url(forResource: targetName, withExtension: "mp4", subdirectory: "/art.scnassets") {
videoPlayer = AVPlayer(url: validURL)
videoPlayer.play()
}
//4. Assign The AVPlayer & The Geometry To The Video Player
videoPlayerGeometry.firstMaterial?.diffuse.contents = videoPlayer
videoPlayerNode.geometry = videoPlayerGeometry
//5. Rotate It
videoPlayerNode.eulerAngles.x = -.pi / 2
return videoPlayerNode
}
}
As you can see I have opted to use an AVPlayer as my video content, although you can continue to use your videoScene should you so desire.
Hope it points you in the right direction...

Add plane nodes to ARKit scene vertically and horizontally

I want my app to lay the nodes on the surface, which can be vertical or horizontal. However, the node is always vertical. Here's a pic, these nodes aren't placed correctly.
#objc func didTapAddButton() {
let screenCentre = CGPoint(x: self.sceneView.bounds.midX, y: self.sceneView.bounds.midY)
let arHitTestResults: [ARHitTestResult] = sceneView.hitTest(screenCentre, types: [.featurePoint]) // Alternatively, we could use '.existingPlaneUsingExtent' for more grounded hit-test-points.
if let closestResult = arHitTestResults.first {
let transform: matrix_float4x4 = closestResult.worldTransform
let worldCoord: SCNVector3 = SCNVector3Make(transform.columns.3.x, transform.columns.3.y, transform.columns.3.z)
if let node = createNode() {
sceneView.scene.rootNode.addChildNode(node)
node.position = worldCoord
}
}
}
func createNode() -> SCNNode? {
guard let theView = myView else {
print("Failed to load view")
return nil
}
let plane = SCNPlane(width: 0.06, height: 0.06)
let imageMaterial = SCNMaterial()
imageMaterial.isDoubleSided = true
imageMaterial.diffuse.contents = theView.asImage()
plane.materials = [imageMaterial]
let node = SCNNode(geometry: plane)
return node
}
The app is able to see the ground but the nodes are still parallel to us. How can I fix this?
Edit: I figured I can use node.eulerAngles.x = -.pi / 2, this makes sure that the plane is laid down horizontally but it's still horizontal on vertical surfaces as well.
Solved! Here's how to make the view "parallel" to the camera at all times:
let yourNode = SCNNode()
let billboardConstraint = SCNBillboardConstraint()
billboardConstraint.freeAxes = [.X, .Y, .Z]
yourNode.constraints = [billboardConstraint]
Or
guard let currentFrame = sceneView.session.currentFrame else {return nil}
let camera = currentFrame.camera
let transform = camera.transform
var translationMatrix = matrix_identity_float4x4
translationMatrix.columns.3.z = -0.1
let modifiedMatrix = simd_mul(transform, translationMatrix)
let node = SCNNode(geometry: plane)
node.simdTransform = modifiedMatrix

ARKit uses "UIView" as contents in "SCNNode"

I have an odd problem, or maybe what I am doing is odd.
I have an ARKit app that displays a SCNNode with a data view embedded when a barcode is detected. When the data view is tapped I want to detach it from the node and add it as a subview to the main view. When tapped again I want to re attach it to the SCNNode.
I found this code that does at least part of what I want.
My app works well the first time. The data appears in the SCNNode:
{
let transform = anchor.transform
let label = SimpleValueViewController(nibName: "SimpleValueViewController", bundle: nil)
let plane = SCNPlane(width: self.sceneView.bounds.width/2000, height: self.sceneView.bounds.height/2000/2) //was sceneView
label.update(dataItem.title!, andValue: dataItem.value!, withTrend: dataItem.trend)
label.delegate = self
plane.firstMaterial?.blendMode = .alpha
plane.firstMaterial?.diffuse.contents = label.view //self.contentController?.view
let planeNode = SCNNode(geometry: plane)
planeNode.opacity = 0.75
planeNode.simdPosition = float3(transform.columns.3.x + ( base * count), 0, transform.columns.3.z)
node.addChildNode(planeNode)
childControllers[label] = planeNode
label.myNode = planeNode
label.parentNode = node
if("Speed" == anchorLabels[anchor.identifier]!) {
let constraint = SCNBillboardConstraint()
planeNode.constraints = [constraint]
}
count += 1
}
SimpleValueViewController has a tap recognizer that toggles between being a subview and being inside the node.
Here is the problem.
It comes up fine. The view is inside the node.
When tapped it gets attached to the main view.
When tapped again the node just shows a white square and taps no longer work.
I finally gave up and recreate a new viewController, but it still occasionally fails with only a white square:
Here is the code to detach/attach It is pretty messy as I have been pounding on it for a while:
func detach(_ vc:Detachable) {
if let controller = vc as? SimpleValueViewController {
controller.view.removeFromSuperview();
self.childControllers.removeValue(forKey: controller)
let node = controller.myNode
let parent = controller.parentNode
let newController = SimpleValueViewController(nibName: "SimpleValueViewController", bundle: nil)
if let v = newController.view {
print("View: \(v)")
newController.myNode = node
newController.parentNode = parent
newController.update(controller.titleString!, andValue: controller.valueString!, withTrend: controller.trendString!)
newController.delegate = self
newController.scaley = vc.scaley
newController.scalex = vc.scalex
newController.refreshView()
let plane = node?.geometry as! SCNPlane
plane.firstMaterial?.blendMode = .alpha
plane.firstMaterial?.diffuse.contents = v
// newController.viewWillAppear(false)
newController.parentNode?.addChildNode(newController.myNode!)
newController.attached = false;
self.childControllers[newController] = node
}
} else {
print("Not a know type of controller")
}
}
func attach(_ vc:Detachable) {
DispatchQueue.main.async {
if let controller = vc as? SimpleValueViewController {
self.childControllers.removeValue(forKey: controller)
let newController = SimpleValueViewController(nibName: "SimpleValueViewController", bundle: nil)
let node = controller.myNode
let parent = controller.parentNode
let scaleX = vc.scalex!/self.view.frame.size.width
let scaleY = vc.scaley!/self.view.frame.size.height
let scale = min(scaleX, scaleY)
let transform = CGAffineTransform(scaleX: scale, y: scale)
newController.view.transform = transform
newController.myNode = node
newController.parentNode = parent
newController.update(controller.titleString!, andValue: controller.valueString!, withTrend: controller.trendString!)
newController.delegate = self
newController.scaley = vc.scaley
newController.scalex = vc.scalex
node?.removeFromParentNode()
self.childControllers[newController] = node
newController.attached = true
self.view.addSubview(newController.view)
} else {
print("Not a know type of controller to attach")
}
}
}
I was thinking it was a timing issue as loading a view from a nib is done lazily so I put in a sleep for 1 second after the SimpleValueViewController(nibName: "SimpleValueViewController", bundle: nil) but that did not help.
Anything else I can try? I am way off base? I would like not to have to recreate the view controller every time the data is toggled.
Thanks in Advance.
Render functions:
func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
// func renderer(_ renderer: SCNSceneRenderer, nodeFor anchor: ARAnchor) -> SCNNode? {
// updateQueue.async {
//Loop through the near and far data and add stuff
let data = "Speed" == anchorLabels[anchor.identifier]! ? nearData : farData
let base = "Speed" == anchorLabels[anchor.identifier]! ? 0.27 : 0.2 as Float
print ("Getting Node for: " + self.anchorLabels[anchor.identifier]!)
var count = 0.0 as Float
for dataItem in data {
let transform = anchor.transform
let label = SimpleValueViewController(nibName: "SimpleValueViewController", bundle: nil)
let plane = SCNPlane(width: self.sceneView.bounds.width/2000, height: self.sceneView.bounds.height/2000/2) //was sceneView
label.update(dataItem.title!, andValue: dataItem.value!, withTrend: dataItem.trend)
label.delegate = self
plane.firstMaterial?.blendMode = .alpha
plane.firstMaterial?.diffuse.contents = label.view //self.contentController?.view
let planeNode = SCNNode(geometry: plane)
planeNode.opacity = 0.75
planeNode.simdPosition = float3(transform.columns.3.x + ( base * count), 0, transform.columns.3.z)
node.addChildNode(planeNode)
childControllers[label] = planeNode
label.myNode = planeNode
label.parentNode = node
if("Speed" == anchorLabels[anchor.identifier]!) {
let constraint = SCNBillboardConstraint()
planeNode.constraints = [constraint]
}
count+=1
}
and update
func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) {
guard currentBuffer == nil else {
return
}
self.updateFocusSquare(isObjectVisible: false)
// Retain the image buffer for Vision processing.
self.currentBuffer = sceneView.session.currentFrame?.capturedImage
classifyCurrentImage()
}
ClassifyCurrentImage is where the barcode recognizer lives. If a barcode is found it adds an anchor at the spot in the scene.

ARKit Moving Horizontal Plane Visualization

I'm able to create a horizontal plane the moment I detect a flat surface, but I am wondering if there is a way to display it while moving around similarly to the feature point debugging option?
Essentially, I want the plane to move with you, not add on to the plane.
Assuming I have interpreted your question correctly, you are essentially asking how can I update the visualisation of a detected plane as it increases in size etc.
There are a number of different ways that this can be achieved, but in this instance I will provide an example using a subclass of SCNNode which will update as the plane increases.
class PlaneNode: SCNNode {
let DEFAULT_IMAGE: String = "defaultGrid"
let NAME: String = "PlaneNode"
var planeGeometry: SCNPlane
var planeAnchor: ARPlaneAnchor
var widthInfo: String!
var heightInfo: String!
var alignmentInfo: String!
//---------------
//MARK: LifeCycle
//---------------
/// Inititialization
///
/// - Parameters:
/// - anchor: ARPlaneAnchor
/// - node: SCNNode
/// - node: Bool
init(anchor: ARPlaneAnchor, node: SCNNode, image: Bool, identifier: Int, opacity: CGFloat = 0.25){
//1. Create The SCNPlaneGeometry
self.planeAnchor = anchor
self.planeGeometry = SCNPlane(width: CGFloat(anchor.extent.x), height: CGFloat(anchor.extent.z))
let planeNode = SCNNode(geometry: planeGeometry)
super.init()
//2. If The Image Bool Is True We Use The Default Image From The Assets Bundle
let planeMaterial = SCNMaterial()
if image{
planeMaterial.diffuse.contents = UIImage(named: DEFAULT_IMAGE)
}else{
planeMaterial.diffuse.contents = UIColor.cyan
}
//3. Set The Geometries Contents
self.planeGeometry.materials = [planeMaterial]
//4. Set The Position Of The PlaneNode
planeNode.simdPosition = float3(self.planeAnchor.center.x, 0, self.planeAnchor.center.z)
//5. Rotate It On It's XAxis
planeNode.eulerAngles.x = -.pi / 2
//6. Set The Opacity Of The Node
planeNode.opacity = opacity
//7. Add The PlaneNode
node.addChildNode(planeNode)
//8. Set The Nodes ID
node.name = "\(NAME) \(identifier)"
}
required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") }
/// Updates The Size Of The Plane As & When The ARPlaneAnchor Has Been Updated
///
/// - Parameter anchor: ARPlaneAnchor
func update(_ anchor: ARPlaneAnchor) {
self.planeAnchor = anchor
self.planeGeometry.width = CGFloat(anchor.extent.x)
self.planeGeometry.height = CGFloat(anchor.extent.z)
self.position = SCNVector3Make(anchor.center.x, 0.01, anchor.center.z)
}
}
Having created our subclass we then need to use it within our ViewController.
You will normally get more than one ARPlaneAnchor and would probably store these in a dictionary, however in this example we just assume there will be one.
As such we will create a variable which will reference our PlaneNode:
var planeNode:PlaneNode?
Then in the ARSCNViewDelegate we will create our PlaneNode like so:
func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
//1. Get The Current ARPlaneAnchor
guard let anchor = anchor as? ARPlaneAnchor else { return }
//2. Create The PlaneNode
if planeNode == nil{
planeNode = PlaneNode(anchor: anchor, node: node, image: true, identifier: 0, opacity: 1)
node.addChildNode(planeNode!)
planeNode?.name = String("Detected Plane")
}
}
Then all you need to do is track the updating of the PlaneNode so that the visualization keeps updating e.g:
func renderer(_ renderer: SCNSceneRenderer, didUpdate node: SCNNode, for anchor: ARAnchor) {
guard let anchor = anchor as? ARPlaneAnchor, let existingPlane = planeNode else { return }
existingPlane.update(anchor)
}

Resources