ARKit uses "UIView" as contents in "SCNNode" - ios

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.

Related

Scenekit - Add child node (Plane node) to the parent node (sphere node) in front of camera

I am trying to add child node to the parent node which have it's own camera. on tap of the user on scene, using hit test I am adding child node (i.e plane node) to the the parent node (sphere node). But I don't know why the child node is not facing properly to the camera. it's looking different on different location. I wanted it to fix looking at the camera.
Code :
// Creating sphere node and adding camera to it
func setup() {
let sphere = SCNSphere(radius: 10)
sphere.segmentCount = 360
let material = getTextureMaterial()
material.diffuse.contents = UIImage(named: "sample")
sphere.firstMaterial = material
let sphereNode = SCNNode()
sphereNode.geometry = sphere
sceneView.scene?.rootNode.addChildNode(sphereNode)
}
//Creating texture material to show 360 degree image...
func getTextureMaterial() -> SCNMaterial {
let material = SCNMaterial()
material.diffuse.mipFilter = .nearest
material.diffuse.magnificationFilter = .linear
material.diffuse.contentsTransform = SCNMatrix4MakeScale(-1, 1, 1)
material.diffuse.wrapS = .repeat
material.cullMode = .front
material.isDoubleSided = true
return material
}
#objc func handleTap(rec: UITapGestureRecognizer){
if rec.state == .ended {
let location: CGPoint = rec.location(in: sceneView)
let hits = sceneView.hitTest(location, options: nil)
if !hits.isEmpty {
let result: SCNHitTestResult = hits[0]
createPlaneNode(result: result)
}
}
}
func createPlaneNode(result : SCNHitTestResult) {
let plane = SCNPlane(width: 5, height: 5)
plane.firstMaterial?.diffuse.contents = UIColor.red
plane.firstMaterial?.isDoubleSided = true
let planeNode = SCNNode()
planeNode.name = "B"
planeNode.geometry = plane
planeNode.position = result.worldCoordinates
result.node.addChildNode(planeNode)
}
Results I am getting using above code:
Added Plane node is looking weird
I don't know if you ever found your answer but, it would be done freeing the node axes with SCNBillboardConstraint :)
let billboardConstraint = SCNBillboardConstraint()
billboardConstraint.freeAxes = [.all]
node.constraints = [billboardConstraint]

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

How to add node relative to tracked image position with ARKit?

When an image is detected, I add a node in front of it.
Image - what i need garantee
The node is this picture and in relation to this picture I add other nodes, one above (1) of the picture and another next (2). Node 1 always gets in the correct position, but node 2 sometimes gets in front of the image, which is wrong, and sometimes gets in the right position that it's like the photo.
Image - wrong
for anchor in sceneView.session.currentFrame!.anchors {
let imageAnchor = anchor as! ARImageAnchor
nodeInformation.position = SCNVector3Make(imageAnchor.transform.columns.3.x/100, imageAnchor.transform.columns.3.y/100 + 0.3, imageAnchor.transform.columns.3.z/100) //node 1
nodeAuthorInformation.position = SCNVector3Make(imageAnchor.transform.columns.3.x/100 + 0.3, imageAnchor.transform.columns.3.y/100, imageAnchor.transform.columns.3.z/100) //node 2
This is the code that I'm using to set the initial position of my two nodes.
So i would like to know how i can garantee that my second node will stay always in the right position, that it is next to picture.
How I add the nodes:
let tappedNode = self.sceneView.hitTest(gesture.location(in: gesture.view), options: [:])
if let result = tappedNode.first{
nodeInformation.transform = result.modelTransform
nodeAuthorInformation.transform = result.modelTransform
}
print("TAP PLACE = ", gesture.location(in: gesture.view))
if !tappedNode.isEmpty{
let node = tappedNode[0].node
print("NODE TAP = ", node.position)
let artPlane = SCNPlane(width: 0.3, height: 0.3)
artPlane.firstMaterial?.diffuse.contents = arArtDetailsScene
artPlane.firstMaterial?.isDoubleSided = true
let planeNode = SCNNode(geometry: artPlane)
planeNode.eulerAngles.x = .pi
authorPlane = SCNPlane(width: 0.2, height: 0.3)
authorPlane.firstMaterial?.diffuse.contents = arAuthorDetailsScene
authorPlane.firstMaterial?.isDoubleSided = true
let planeAuthorNode = SCNNode(geometry: authorPlane)
planeAuthorNode.eulerAngles.x = .pi
nodeInformation.addChildNode(planeNode)
nodeAuthorInformation.addChildNode(planeAuthorNode)
changeArtDetails(artId: artIndex + 1)
sceneView.scene.rootNode.addChildNode(nodeInformation)
sceneView.scene.rootNode.addChildNode(nodeAuthorInformation)
if sceneView.session.currentFrame != nil {
for anchor in sceneView.session.currentFrame!.anchors {
let imageAnchor = anchor as! ARImageAnchor
nodeInformation.position = SCNVector3Make(imageAnchor.transform.columns.3.x/100, imageAnchor.transform.columns.3.y/100 + 0.3, imageAnchor.transform.columns.3.z/100) // initial position node 1
nodeAuthorInformation.position = SCNVector3Make(imageAnchor.transform.columns.3.x/100 + 0.3, imageAnchor.transform.columns.3.y/100, imageAnchor.transform.columns.3.z/100) // initial position node 2
}
}
And I update the position of the node like this:
func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) {
if sceneView.session.currentFrame != nil {
for anchor in sceneView.session.currentFrame!.anchors {
if let imageAnchor = anchor as? ARImageAnchor{
DispatchQueue.main.async {
self.nodeInformation.position = SCNVector3Make(imageAnchor.transform.columns.3.x/100, imageAnchor.transform.columns.3.y/100 + 0.3, imageAnchor.transform.columns.3.z/100)
self.nodeAuthorInformation.position = SCNVector3Make(self.nodeInformation.position.x + 0.3, self.nodeInformation.position.y - 0.3, self.nodeInformation.position.z)//SCNVector3Make(imageAnchor.transform.columns.3.x/100 + 0.3, imageAnchor.transform.columns.3.y/100, imageAnchor.transform.columns.3.z/100)
}
}
}
}
}
ARKit updates position of anchors, not nodes. You will need to provide a node with three custom nodes as children in ARSCNViewDelegate.renderer(_ renderer: SCNSceneRenderer, nodeFor anchor: ARAnchor) -> SCNNode? if anchor is ARImageAnchor instead of adding it manually to the scene.

ARKit ImageDetection - get reference image when tapping 3D object

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

Scene Kit SCNMorpher not working with primitives like SCNSphere etc

Has anybody got the SCNMorpher to work? If so, what exactly have you done?
I'm down now to a small test program, that should show a red cone and when I tap on the screen it should (should!) morphe that into a sphere/ball. But the only thing that happens is, that the first geometry goes away (get very small) and the second one (Morphers first target) never appears.
I must miss something very simple. Can anybody get me on the horse, please?
import SceneKit
private let DISTANCE_FAKTOR = CGFloat(sqrt(3.0)/2.0)
class GameViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let (scene, view) = configScene(self.view.frame)
self.view = view
// Position camera to see picture full screen and light at same position
let pov = SCNVector3(x: 0.0, y: 0.0, z: Float(max(view.frame.width, view.frame.height) * DISTANCE_FAKTOR))
let pol = SCNVector3(x: 0.0, y: 0.0, z: Float(max(view.frame.width, view.frame.height) * DISTANCE_FAKTOR))
scene.rootNode.addChildNode(makeShapes()) // Create and add background plane
scene.rootNode.addChildNode(makeCamera(pov)) // Add camera to scene
scene.rootNode.addChildNode(makeLight(pol)) // Add light to scene
// add a tap gesture recognizer
view.gestureRecognizers = [UITapGestureRecognizer(target: self, action: "handleTap:")]
}
func handleTap(gestureRecognize: UIGestureRecognizer) {
NSLog("Tapped")
if let effectNode = (view as? SCNView)?.scene?.rootNode.childNodeWithName("EFFECT", recursively: true) {
NSLog("Animating")
animate(effectNode)
}
}
func makeShapes() -> SCNNode {
// First shape is a cone
let torus = SCNCone(topRadius: 0.0, bottomRadius: view.frame.width/2.0, height: view.frame.width/4.0)
torus.firstMaterial = SCNMaterial()
torus.firstMaterial?.diffuse.contents = UIColor.redColor()
// Now an additional sphere/ball
let sphere = SCNSphere(radius: view.frame.width/2.0)
sphere.firstMaterial = SCNMaterial()
sphere.firstMaterial?.diffuse.contents = UIColor.greenColor()
// Put all in the node
let node = SCNNode()
node.geometry = torus
node.morpher = SCNMorpher()
node.morpher?.targets = [sphere]
node.name = "EFFECT"
// I would expect now something between a ball and a torus in red/green
node.morpher?.setWeight(0.5, forTargetAtIndex: 0)
return node
}
func animate(node: SCNNode) {
SCNTransaction.begin()
SCNTransaction.setAnimationDuration(5.0)
node.morpher?.setWeight(1.0, forTargetAtIndex: 0) // From torus to ball
SCNTransaction.setCompletionBlock {
NSLog("Transaction completing")
SCNTransaction.begin()
SCNTransaction.setAnimationDuration(2.5)
node.morpher?.setWeight(0.0, forTargetAtIndex: 0) // And back
SCNTransaction.commit()
}
SCNTransaction.commit()
}
func configScene(frame: CGRect) -> (scene: SCNScene, view: SCNView) {
let view = SCNView()
view.frame = frame
view.autoresizingMask = UIViewAutoresizing.allZeros
view.backgroundColor = UIColor.blueColor()
let scene = SCNScene()
view.scene = scene
return (scene, view)
}
func makeCamera(pov: SCNVector3) -> SCNNode {
let camera = SCNCamera()
camera.zFar = Double(pov.z)
let node = SCNNode()
node.position = pov
node.camera = camera
return node
}
func makeLight(pol: SCNVector3) -> SCNNode {
let light = SCNLight()
light.type = SCNLightTypeOmni
light.zFar = CGFloat(pol.z)
let node = SCNNode()
node.position = pol
node.light = light
return node
}
override func shouldAutorotate() -> Bool {
return true
}
override func prefersStatusBarHidden() -> Bool {
return true
}
override func supportedInterfaceOrientations() -> Int {
if UIDevice.currentDevice().userInterfaceIdiom == .Phone {
return Int(UIInterfaceOrientationMask.AllButUpsideDown.rawValue)
} else {
return Int(UIInterfaceOrientationMask.All.rawValue)
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Release any cached data, images, etc that aren't in use.
}
}
When I exchange the ball and the cone in the code, I can see the ball and it disappears when tapping the screen, but the cone never appears.
If you want to run this code, just create a new GameScene project in Xcode and copy this code into GameViewController.swift
Has anybody got the SCNMorpher to work?
Yes, I've gotten SCNMorpher to work between both custom geometry and geometry that has been loaded from a file.
The piece that you are missing is that all the morph targets need to have the same number of vertices and the vertices need to have the same arrangement. This is discussed in the documentation for the targets property:
The base geometry and all target geometries must be topologically identical — that is, they must contain the same number and structural arrangement of vertices.
Your example (morning between a sphere and a cone) doesn't fulfill this requirement and is not expected to work.
You can try and morph between two different spheres (with the same number of segments!) to see that it does work.

Resources