I would like to continually output the updated eye transform matrices for an ARFaceGeometry in an ARSession. I have written the following so far and instead of updating the position continually, it simply outputs the initial position and continues to repeat those values. I have tried implementing an updateFeatures(for node: SCNNode,using anchor: ARFaceAnchor) for eye position and was not able to yield a different result. Would you have any tips/ glaring mistakes I failed to notice that could be causing the eyeTransform to not update?
func renderer(_ renderer: SCNSceneRenderer, didUpdate node: SCNNode, for anchor: ARAnchor) {
var leftEyeTransform: simd_float4x4 { get {
return currentFaceAnchor!.leftEyeTransform}
}
print("Left Eye: \(currentFaceAnchor!.leftEyeTransform)")
guard anchor == currentFaceAnchor,
let contentNode = selectedContentController.contentNode,
contentNode.parent == node
else { return }
selectedContentController.renderer(renderer, didUpdate: contentNode, for: anchor)
}
In the above, currentFaceAnchor is a let constant defined as an ARFaceAnchor in the renderer(didAdd:) method and contentNode is a let constant defined as contentController.renderer(nodeFor: faceAnchor).
It looks like you're not using the anchor that the renderer function gives you as a parameter. That anchor contains all of the updated information about the face/eye. The currentFaceAnchor that you're referring to only contains the information about the face anchor from when you first initialized it. If you were to add what's below you should see updating values from the leftEyeTransform.
func renderer(_ renderer: SCNSceneRenderer, didUpdate node: SCNNode, for anchor: ARAnchor) {
guard #available(iOS 12.0, *), let faceAnchor = anchor as? ARFaceAnchor else { return }
print("\(faceAnchor.leftEyeTransform)")
}
Related
I am trying to get real-world human body height from ARBodyAnchor. I understand that I can get real-world distance between body joints. For example hip to foot-joint as in code below. But how do I get distance from top of head to bottom of foot?
func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
if anchor is ARBodyAnchor {
let footIndex = ARSkeletonDefinition.defaultBody3D.index(for: .leftFoot)
let footTransform = ARSkeletonDefinition.defaultBody3D.neutralBodySkeleton3D!.jointModelTransforms[footIndex]
let distanceFromHipOnY = abs(footTransform.columns.3.y)
print(distanceFromHipOnY)
}
}
The default height of ARSkeleton3D from right_toes_joint (or if you wish left_toes_joint) to head_joint is 1.71 meters. And since head_joint in Apple's skeletal system's definition is the upmost skeleton's point, you can use the common skull's height – from eye line to crown.
In other words, the distance from neck_3_joint to head_joint in virtual model's skeleton is approximately the same as from head_joint to crown.
There are 91 joint in ARSkeleton3D:
print(bodyAnchor.skeleton.jointModelTransforms.count) // 91
Code:
extension ViewController: ARSessionDelegate {
func session(_ session: ARSession, didUpdate anchors: [ARAnchor]) {
for anchor in anchors {
guard let bodyAnchor = anchor as? ARBodyAnchor
else { return }
let skeleton = bodyAnchor.skeleton
for (i, joint) in skeleton.definition.jointNames.enumerated() {
print(i, joint)
// [10] right_toes_joint
// [51] head_joint
}
let toesJointPos = skeleton.jointModelTransforms[10].columns.3.y
let headJointPos = skeleton.jointModelTransforms[51].columns.3.y
print(headJointPos - toesJointPos) // 1.6570237 m
}
}
}
However, we have a compensator:
bodyAnchor.estimatedScaleFactor
ARKit must know the height of a person in the camera feed to estimate an accurate world position for the person's body anchor. ARKit uses the value of estimatedScaleFactor to correct the body anchor's position in the physical environment.
The default real-world body is 1.8 meters tall. (some kind of mismatch...)
The default value of estimatedScaleFactor is 1.0.
If you set:
let config = ARBodyTrackingConfiguration()
config.automaticSkeletonScaleEstimationEnabled = true
arView.session.run(config, options: [])
ARKit sets this property to a value between 0.0 and 1.0.
I have a list of images coming from server and stored in gallery. I want to pick any image and place on live wall using ARKit and want to convert images into 3d images tp perform operation like zooming , moving image etc.
Can anybody please guide how can I create custom object in AR?
To detect vertical surfaces (e.g walls) in ARKit you need to firstly set up ARWorldTrackingConfiguration and then enable planeDetection within your app.
So under your Class Declaration you would create the following variables:
#IBOutlet var augmentedRealityView: ARSCNView!
let augmentedRealitySession = ARSession()
let configuration = ARWorldTrackingConfiguration()
And then initialise your ARSession and in ViewDidLoad for example e.g:
override func viewDidLoad() {
super.viewDidLoad()
//1. Set Up Our ARSession
augmentedRealityView.session = augmentedRealitySession
//2. Assign The ARSCNViewDelegate
augmentedRealityView.delegate = self
//3. Set Up Plane Detection
configuration.planeDetection = .vertical
//4. Run Our Configuration
augmentedRealitySession.run(configuration, options: [.resetTracking, .removeExistingAnchors])
}
Now that you are all set to detected vertical planes you need to hook into the following ARSCNViewDelegate Method:
func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) { }
Which simply:
Tells the delegate that a SceneKit node corresponding to a new AR
anchor has been added to the scene.
In this method we are going to explicitly look for any ARPlaneAnchors which have been detected which provide us with:
Information about the position and orientation of a real-world flat
surface detected in a world-tracking AR session.
As such placing an SCNPlane onto a detected vertical plane is a simple as this:
//-------------------------
//MARK: - ARSCNViewDelegate
//-------------------------
extension ViewController: ARSCNViewDelegate{
func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
//1. Check We Have Detected An ARPlaneAnchor
guard let planeAnchor = anchor as? ARPlaneAnchor else { return }
//2. Get The Size Of The ARPlaneAnchor
let width = CGFloat(planeAnchor.extent.x)
let height = CGFloat(planeAnchor.extent.z)
//3. Create An SCNPlane Which Matches The Size Of The ARPlaneAnchor
let imageHolder = SCNNode(geometry: SCNPlane(width: width, height: height))
//4. Rotate It
imageHolder.eulerAngles.x = -.pi/2
//5. Set It's Colour To Red
imageHolder.geometry?.firstMaterial?.diffuse.contents = UIColor.red
//4. Add It To Our Node & Thus The Hiearchy
node.addChildNode(imageHolder)
}
}
Applying This To Your Case:
In your case we need to do some additional work, as you want to be able to allow the user to apply an image to the vertical plane.
As such your best bet is to make the node you have just added a variable e.g.
class ViewController: UIViewController {
#IBOutlet var augmentedRealityView: ARSCNView!
let augmentedRealitySession = ARSession()
let configuration = ARWorldTrackingConfiguration()
var nodeWeCanChange: SCNNode?
}
As such your Delegate Callback might look like so:
//-------------------------
//MARK: - ARSCNViewDelegate
//-------------------------
extension ViewController: ARSCNViewDelegate{
func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
//1. If We Havent Create Our Interactive Node Then Proceed
if nodeWeCanChange == nil{
//a. Check We Have Detected An ARPlaneAnchor
guard let planeAnchor = anchor as? ARPlaneAnchor else { return }
//b. Get The Size Of The ARPlaneAnchor
let width = CGFloat(planeAnchor.extent.x)
let height = CGFloat(planeAnchor.extent.z)
//c. Create An SCNPlane Which Matches The Size Of The ARPlaneAnchor
nodeWeCanChange = SCNNode(geometry: SCNPlane(width: width, height: height))
//d. Rotate It
nodeWeCanChange?.eulerAngles.x = -.pi/2
//e. Set It's Colour To Red
nodeWeCanChange?.geometry?.firstMaterial?.diffuse.contents = UIColor.red
//f. Add It To Our Node & Thus The Hiearchy
node.addChildNode(nodeWeCanChange!)
}
}
}
Now you have a reference to the nodeWeCanChange setting it's image at anytime is simple!
Each SCNGeometry has a set of Materials which are a:
A set of shading attributes that define the appearance of a geometry's
surface when rendered.
In our case we are looking for the materials diffuse property which is:
An object that manages the material’s diffuse response to lighting.
And then the contents property which are:
The visual contents of the material property—a color, image, or source
of animated content.
Obviously you need to handle the full logistics of this, however a very basic example might look like so assuming you stored your Images into an Array of UIImage e.g:
let imageGallery = [UIImage(named: "StackOverflow"), UIImage(named: "GitHub")]
I have created an IBAction which will change the image of our SCNNode's Geometry based on the tag of the UIButton pressed e.g:
/// Changes The Material Of Our SCNNode's Gemeotry To The Image Selected By The User
///
/// - Parameter sender: UIButton
#IBAction func changeNodesImage(_ sender: UIButton){
guard let imageToApply = imageGallery[sender.tag], let node = nodeWeCanChange else { return}
node.geometry?.firstMaterial?.diffuse.contents = imageToApply
}
This is more than enough to point you in the right direction... Hope it helps...
I would like to check whether the ARReferenceImage is no longer visible in the camera's view. At the moment I can check if the image's node is in the camera's view, but this node is still visible in the camera's view when the ARReferenceImage is covered with another image or when the image is removed.
func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) {
guard let node = self.currentImageNode else { return }
if let pointOfView = sceneView.pointOfView {
let isVisible = sceneView.isNode(node, insideFrustumOf: pointOfView)
print("Is node visible: \(isVisible)")
}
}
So I need to check if the image is no longer visible instead of the image's node visibility. But I can't find out if this is possible. The first screenshot shows three boxes that are added when the image beneath is found. When the found image is covered (see screenshot 2) I would like to remove the boxes.
I managed to fix the problem! Used a little bit of Maybe1's code and his concept to solving the problem, but in a different way. The following line of code is still used to reactivate the image recognition.
// Delete anchor from the session to reactivate the image recognition
sceneView.session.remove(anchor: anchor)
Let me explain. First we need to add some variables.
// The scnNodeBarn variable will be the node to be added when the barn image is found. Add another scnNode when you have another image.
var scnNodeBarn: SCNNode = SCNNode()
// This variable holds the currently added scnNode (in this case scnNodeBarn when the barn image is found)
var currentNode: SCNNode? = nil
// This variable holds the UUID of the found Image Anchor that is used to add a scnNode
var currentARImageAnchorIdentifier: UUID?
// This variable is used to call a function when there is no new anchor added for 0.6 seconds
var timer: Timer!
The complete code with comments below.
/// - 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
// The following timer fires after 0.6 seconds, but everytime when there found an anchor the timer is stopped.
// So when there is no ARImageAnchor found the timer will be completed and the current scene node will be deleted and the variable will set to nil
DispatchQueue.main.async {
if(self.timer != nil){
self.timer.invalidate()
}
self.timer = Timer.scheduledTimer(timeInterval: 0.6 , target: self, selector: #selector(self.imageLost(_:)), userInfo: nil, repeats: false)
}
// Check if there is found a new image on the basis of the ARImageAnchorIdentifier, when found delete the current scene node and set the variable to nil
if(self.currentARImageAnchorIdentifier != imageAnchor.identifier &&
self.currentARImageAnchorIdentifier != nil
&& self.currentNode != nil){
//found new image
self.currentNode!.removeFromParentNode()
self.currentNode = nil
}
updateQueue.async {
//If currentNode is nil, there is currently no scene node
if(self.currentNode == nil){
switch referenceImage.name {
case "barn":
self.scnNodeBarn.transform = node.transform
self.sceneView.scene.rootNode.addChildNode(self.scnNodeBarn)
self.currentNode = self.scnNodeBarn
default: break
}
}
self.currentARImageAnchorIdentifier = imageAnchor.identifier
// Delete anchor from the session to reactivate the image recognition
self.sceneView.session.remove(anchor: anchor)
}
}
Delete the node when the timer is finished indicating that there was no new ARImageAnchor found.
#objc
func imageLost(_ sender:Timer){
self.currentNode!.removeFromParentNode()
self.currentNode = nil
}
In this way the currently added scnNode wil be deleted when the image is covered or when there is found a new image.
This solution does unfortunately not solve the positioning problem of images because of the following:
ARKit doesn’t track changes to the position or orientation of each detected image.
I don't think this is currently possible.
From the Recognizing Images in an AR Experience documentation:
Design your AR experience to use detected images as a starting point for virtual content.
ARKit doesn’t track changes to the position or orientation of each detected image. If you try to place virtual content that stays attached to a detected image, that content may not appear to stay in place correctly. Instead, use detected images as a frame of reference for starting a dynamic scene.
New Answer for iOS 12.0
ARKit 2.0 and iOS 12 finally adds this feature, either via ARImageTrackingConfiguration or via the ARWorldTrackingConfiguration.detectionImages property that now also tracks the position of the images.
The Apple documentation to ARImageTrackingConfiguration lists advantages of both methods:
With ARImageTrackingConfiguration, ARKit establishes a 3D space not by tracking the motion of the device relative to the world, but solely by detecting and tracking the motion of known 2D images in view of the camera. ARWorldTrackingConfiguration can also detect images, but each configuration has its own strengths:
World tracking has a higher performance cost than image-only tracking, so your session can reliably track more images at once with ARImageTrackingConfiguration.
Image-only tracking lets you anchor virtual content to known images only when those images are in view of the camera. World tracking with image detection lets you use known images to add virtual content to the 3D world, and continues to track the position of that content in world space even after the image is no longer in view.
World tracking works best in a stable, nonmoving environment. You can use image-only tracking to add virtual content to known images in more situations—for example, an advertisement inside a moving subway car.
The correct way to check if an image that you are tracking is not currently tracked by ARKit is by using the "isTracked" property in the ARImageAnchor on the didUpdate node for anchor function.
For that, I use the next struct:
struct TrackedImage {
var name : String
var node : SCNNode?
}
And then an array of that struct with the name of all the images.
var trackedImages : [TrackedImage] = [ TrackedImage(name: "image_1", node: nil) ]
Then in the didAdd node for anchor, set the new content to the scene and also add the node to the corresponding element in the array of trackedImages
func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
// Check if the added anchor is a recognized ARImageAnchor
if let imageAnchor = anchor as? ARImageAnchor{
// Get the reference ar image
let referenceImage = imageAnchor.referenceImage
// Create a plane to match the detected image.
let plane = SCNPlane(width: referenceImage.physicalSize.width, height: referenceImage.physicalSize.height)
plane.firstMaterial?.diffuse.contents = UIColor(red: 1, green: 1, blue: 1, alpha: 0.5)
// Create SCNNode from the plane
let planeNode = SCNNode(geometry: plane)
planeNode.eulerAngles.x = -.pi / 2
// Add the plane to the scene.
node.addChildNode(planeNode)
// Add the node to the tracked images
for (index, trackedImage) in trackedImages.enumerated(){
if(trackedImage.name == referenceImage.name){
trackedImage[index].node = planeNode
}
}
}
}
Finally in the didUpdate node for anchor function we search for the anchor name in our array and check if the property isTracked is false.
func renderer(_ renderer: SCNSceneRenderer, didUpdate node: SCNNode, for anchor: ARAnchor) {
var trackedImages : [TrackedImage] = [ TrackedImage(name: "image_1", node: nil) ]
if let imageAnchor = anchor as? ARImageAnchor{
// Search the corresponding node for the ar image anchor
for (index, trackedImage) in trackedImages.enumerated(){
if(trackedImage.name == referenceImage.name){
// Check if track is lost on ar image
if(imageAnchor.isTracked){
// The image is being tracked
trackedImage.node?.isHidden = false // Show or add content
}else{
// The image is lost
trackedImage.node?.isHidden = true // Hide or delete content
}
break
}
}
}
}
This solution works when you want to tracked multiple images at the same time and know when any of them is lost.
Note: For this solution to work the maximumNumberOfTrackedImages in the AR configuration must be set to a nonzero number.
For what its worth, I spent hours trying to figure out how to constantly check for image references. The didUpdate function was the answer. Then you just need to test of the reference image is being tracked using the .isTracked property. At that point, you can set the .isHidden property to true or false. Heres my example:
func renderer(_ renderer: SCNSceneRenderer, didUpdate node: SCNNode, for anchor: ARAnchor) {
let trackedNode = node
if let imageAnchor = anchor as? ARImageAnchor{
if (imageAnchor.isTracked) {
trackedNode.isHidden = false
print("\(trackedNode.name)")
}else {
trackedNode.isHidden = true
//print("\(trackedImageName)")
print("No image in view")
}
}
}
I'm not entirely sure I have understood what your asking (so apologies), but if I have then perhaps this might help...
It seems that for insideOfFrustum to work correctly, that their must be some SCNGeometry associated with the node for it to work (an SCNNode alone will not suffice).
For example if we do something like this in the delegate callback and save the added SCNNode into an array:
func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
//1. If Out Target Image Has Been Detected Than Get The Corresponding Anchor
guard let currentImageAnchor = anchor as? ARImageAnchor else { return }
//2. Print The Anchor ID & It's Associated Node
print("""
Anchor With ID Has Been Detected \(currentImageAnchor.identifier)
Associated Node Details = \(node)
""")
//3. Store The Node
imageTargets.append(node)
}
And then use the insideOfFrustum method, 99% of the time it will say that the node is in view even when we know it shouldn't be.
However if we do something like this (whereby we create a transparent marker node e.g. one that has some geometry):
func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
//1. If Out Target Image Has Been Detected Than Get The Corresponding Anchor
guard let currentImageAnchor = anchor as? ARImageAnchor else { return }
//2. Print The Anchor ID & It's Associated Node
print("""
Anchor With ID Has Been Detected \(currentImageAnchor.identifier)
Associated Node Details = \(node)
""")
//3. Create A Transpanrent Geometry
node.geometry = SCNSphere(radius: 0.1)
node.geometry?.firstMaterial?.diffuse.contents = UIColor.clear
//3. Store The Node
imageTargets.append(node)
}
And then call the following method, it does detect if the ARReferenceImage is inView:
func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) {
//1. Get The Current Point Of View
guard let pointOfView = augmentedRealityView.pointOfView else { return }
//2. Loop Through Our Image Target Markers
for addedNode in imageTargets{
if augmentedRealityView.isNode(addedNode, insideFrustumOf: pointOfView){
print("Node Is Visible")
}else{
print("Node Is Not Visible")
}
}
}
In regard to your other point about an SCNNode being occluded by another one, the Apple Docs state that the inViewOfFrostrum:
does not perform occlusion testing. That is, it returns
true if the tested node lies within the specified viewing frustum
regardless of whether that node’s contents are obscured by other
geometry.
Again, apologies if I haven't understood you correctly, but hopefully it might help to some extent...
Update:
Now I fully understand your question, I agree with #orangenkopf that this isn't possible. Since as the docs state:
ARKit doesn’t track changes to the position or orientation of each
detected image.
From the Recognizing Images in an AR Experience documentation:
ARKit adds an image anchor to a session exactly once for each
reference image in the session configuration’s detectionImages array.
If your AR experience adds virtual content to the scene when an image
is detected, that action will by default happen only once. To allow
the user to experience that content again without restarting your app,
call the session’s remove(anchor:) method to remove the corresponding
ARImageAnchor. After the anchor is removed, ARKit will add a new
anchor the next time it detects the image.
So, maybe you can find a workaround for your case:
Let's say we are that structure which saves our ARImageAnchor detected and the virtual content associated:
struct ARImage {
var anchor: ARImageAnchor
var node: SCNNode
}
Then, when the renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) is called, you save the image detected into a temporary list of ARImage:
...
var tmpARImages: [ARImage] = []
func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
guard let imageAnchor = anchor as? ARImageAnchor else { return }
let referenceImage = imageAnchor.referenceImage
// If the ARImage does not exist
if !tmpARImages.contains(where: {$0.anchor.referenceImage.name == referenceImage.name}) {
let virtualContent = SCNNode(...)
node.addChildNode(virtualContent)
tmpARImages.append(ARImage(anchor: imageAnchor, node: virtualContent))
}
// Delete anchor from the session to reactivate the image recognition
sceneView.session.remove(anchor: anchor)
}
If you understood, while your camera's view point out of the image/marker, the delegate function will loop endlessly... (because we removed the anchor from the session).
The idea will be to combine the image recognition loop, the image detected saved into the tmp list and the sceneView.isNode(node, insideFrustumOf: pointOfView) function to determine if the image/marker detected is no longer view.
I hope it was clear...
This code works only if You hold the device strictly horizontally or vertically. If You hold iPhone tilted or starting to tilt if, this code doesn't work:
func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) {
//1. Get The Current Point Of View
guard let pointOfView = augmentedRealityView.pointOfView else { return }
//2. Loop Through Our Image Target Markers
for addedNode in imageTargets{
if augmentedRealityView.isNode(addedNode, insideFrustumOf: pointOfView){
print("Node Is Visible")
}else{
print("Node Is Not Visible")
}
}
}
I'm fairly new to iOS Swift programming. I'm using ARKit to build a very basic app to detect a horizontal plane and place,translate,rotate,modify or delete an object on it.
My main concern is to differential between the plane detected by ARKit and a digital object that I've placed. My thinking was to use hitTest(:options:) to select the object (if any) and hitTest(:types:) to select the plane through a tap gesture. I'm attaching the relevant code snippet below.
#objc func tapped(_ gesture: UITapGestureRecognizer){
let sceneView = gesture.view as! ARSCNView
let location = gesture.location(in: sceneView)
let hitTestOptions: [SCNHitTestOption: Any] = [.boundingBoxOnly: true]
let existingNodeHitTest = sceneView.hitTest(location, options: hitTestOptions)
if let existingNode = existingNodeHitTest.first?.node {
// Move, rotate, modify or delete the object
} else {
// Option to add other objects
let hitTest = sceneView.hitTest(location, types: .existingPlaneUsingExtent)
if !hitTest.isEmpty {
let node = findNode(at: location)
if node !== selectedNode {
self.addItems(hitTestResult: hitTest.first!)
}
}
}
}
func addItems(hitTestResult: ARHitTestResult) {
let scene = SCNScene(named: "BuildingModels.scnassets/model/model.scn")
let itemNode = (scene?.rootNode.childNode(withName: "SketchUp", recursively: false))!
let transform = hitTestResult.worldTransform
let position = SCNVector3(transform.columns.3.x,transform.columns.3.y,transform.columns.3.z)
itemNode.position = position
// self.sceneView.scene.lightingEnvironment.contents = scene.lightingEnvironment.contents
self.sceneView.scene.rootNode.addChildNode(itemNode)
selectedNode = itemNode
}
func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
guard let planeAnchor = anchor as? ARPlaneAnchor else {return}
let gridNode = createGrid(planeAnchor: planeAnchor)
node.addChildNode(gridNode)
}
func renderer(_ renderer: SCNSceneRenderer, didUpdate node: SCNNode, for anchor: ARAnchor) {
guard let planeAnchor = anchor as? ARPlaneAnchor else {return}
node.enumerateChildNodes { (childNode, _) in
childNode.removeFromParentNode()
}
let gridNode = createGrid(planeAnchor: planeAnchor)
node.addChildNode(gridNode)
}
When I run the code, the hitTest(_:options:) returns the plane detected. Are there any ways to select only the SCNNodes (objects) that I place and not the plane detected. Am I missing something? Any help is highly appreciated.
Thanks,
Sourabh.
Looking at your question you are already half way there.
The way to handle this in it's entirety, is to make use of the following HitTest functions within your UITapGestureRecognizer function:
(1) An ARSCNHitTest which:
Searches for real-world objects or AR anchors in the captured camera image corresponding to a point in the SceneKit view.
(2) AnSCNHitTest which:
Looks for SCNGeometry objects along the ray you specify. For each intersection between the ray and and a geometry, SceneKit creates a hit-test result to provide information about both the SCNNode object containing the geometry and the location of the intersection on the geometry’s surface.
Using your UITapGestureRecognizer as an example therefore, you can differentiate between an ARPlaneAnchor (detectedPlane) and any SCNNode within your scene like so:
#objc func handleTap(_ gesture: UITapGestureRecognizer){
//1. Get The Current Touch Location
let currentTouchLocation = gesture.location(in: self.augmentedRealityView)
//2. Perform An ARNSCNHitTest To See If We Have Hit An ARPlaneAnchor
if let planeHitTest = augmentedRealityView.hitTest(currentTouchLocation, types: .existingPlane).first,
let planeAnchor = planeHitTest.anchor as? ARPlaneAnchor{
print("User Has Tapped On An Existing Plane = \(planeAnchor.identifier)")
return
}
//3. Perform An SCNHitTest To See If We Have Hit An SCNNode
if let nodeHitTest = augmentedRealityView.hitTest(currentTouchLocation, options: nil).first {
let nodeTapped = nodeHitTest.node
print("An SCNNode Has Been Tapped = \(nodeTapped)")
return
}
}
If you make use of the name property for any of your SCNNode’s this will also help you further e.g:
if let name = nodeTapped.name{
print("An SCNNode Named \(name) Has Been Tapped")
}
Additionally, if you ONLY want to detect objects you have added e.g SCNNodes then you can simply remove part two of the getureRecognizer function.
Hope it helps...
To fix this issue, you should loop through your scene nodes, after that you can manipulate with your wanted node. Example:
for node in sceneView.scene.rootNode.childNodes {
if node.name == "yorNodeName" {
// do your manipulations
}
}
Don't forget to add name to your nodes. Example:
node.name = "yorNodeName"
I hope it helped!
As soon as a node starts moving, for some reason, it stops tracking the position of the node. and here's what I mean:
internal func launchNode(force:Float) {
let force = force * 0.004
currNode.physicsBody = SCNPhysicsBody(type: .dynamic, shape: nil)
currNode.physicsBody?.mass = 0.05
let direction = currNode.worldFront + SCNVector3(force, -force, -force)
currNode.physicsBody?.applyForce(direction, asImpulse: true)
}
The Node is moving, which is great, but I'd like to track the current position of the node while it's moving:
func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) {
guard currNode != nil else { return }
print("simdWorldPosition: \(currNode.simdWorldPosition.y) position: \(currNode.position.y) simWorldPosition: \(currNode.simdWorldPosition.y) simPosition: \(currNode.simdPosition.y)")
}
None of these properties is updating the location, so I know I'm missing something. I'd like to stop it from moving when it gets to a certain position (in y coordinate). If anyone had a success tracking the location of a node after .applyForce, I'd much appreciate it if you could point out what I did wrong, and perhaps what worked for you. thanks.
UPDATE
Here's some code I'm using to test it.
I'm declaring the node at the beginning of this ViewController:
var currNode = SCNNode()
You can try the following for testing to track the location of a node with the ARKit default project:
//call this once your scene is set
func addTapGestureToSceneView() {
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.didTap(withGestureRecognizer:)))
sceneView.addGestureRecognizer(tapGestureRecognizer)
}
#objc func didTap(withGestureRecognizer recognizer: UIGestureRecognizer) {
let tapLocation = recognizer.location(in: sceneView)
let hitTestResults = sceneView.hitTest(tapLocation)
guard let node = hitTestResults.first?.node else { return }
if node.name == "shipMesh" {
currNode = node
moveShip()
}
}
internal func moveShip() {
let force = 2 * 0.004
currNode.physicsBody = SCNPhysicsBody(type: .dynamic, shape: nil)
currNode.physicsBody?.mass = 0.05
let direction = currNode.worldFront + SCNVector3(force, -force, -force)
currNode.physicsBody?.applyForce(direction, asImpulse: true)
}
func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) {
print("simdWorldPosition: \(currNode.simdWorldPosition.y) position: \(currNode.position.y) simWorldPosition: \(currNode.simdWorldPosition.y) simPosition: \(currNode.simdPosition.y)")
}
So Answer of your question is in one line
use node.presentation property
func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) {
print("simdWorldPosition: \(currNode.presentation.simdWorldPosition.y) position: \(currNode.presentation.position.y) simWorldPosition: \(currNode.presentation.simdWorldPosition.y) simPosition: \(currNode.presentation.simdPosition.y)")
}
From Apple Docs
When you use implicit animation (see SCNTransaction) to change a node’s properties, those node properties are set immediately to their target values, even though the animated node content appears to transition from the old property values to the new. During the animation SceneKit maintains a copy of the node, called the presentation node, whose properties reflect the transitory values determined by any in-flight animations currently affecting the node.
Hope it is helpful