How to move multiple nodes in ARSCNView - ios

My goal is to be able to move/rotate AR objects using the gestureRecognizer. While I got it working for a single AR cube, I cannot get it work for multiple cubes/objects.
Main part of the viewDidLoad:
let boxNode1 = addCube(position: SCNVector3(0,0,0), name: "box")
let boxNode2 = addCube(position: SCNVector3(0,-0.1,-0.1), name: "box2")
sceneView.scene.rootNode.addChildNode(boxNode1)
sceneView.scene.rootNode.addChildNode(boxNode2)
var nodes: [SCNNode] = getMyNodes()
var parentNode = SCNNode()
parentNode.name = "motherNode"
for node in nodes {
parentNode.addChildNode(node)
}
sceneView.scene.rootNode.addChildNode(parentNode)
// sceneView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(ViewController.handleTap(_:))))
sceneView.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(ViewController.handleMove(_:))))
sceneView.addGestureRecognizer(UIRotationGestureRecognizer(target: self, action: #selector(ViewController.handleRotate(_:))))
let configuration = ARWorldTrackingConfiguration()
sceneView.session.run(configuration)
Part of panGesture (works for each cube, but does not work if I change to nodeHit.Parent!) The parent node is detected correctly, but no change is made to it:
#objc func handleMove(_ gesture: UIPanGestureRecognizer) {
//1. Get The Current Touch Point
let location = gesture.location(in: self.sceneView)
//2. Get The Next Feature Point Etc
guard let nodeHitTest = self.sceneView.hitTest(location, options: nil).first else { print("no node"); return }
var nodeHit = nodeHitTest.node
// nodeHit = nodeHit.parent!
//3. Convert To World Coordinates
let worldTransform = nodeHitTest.simdWorldCoordinates
//4. Apply To The Node
nodeHit.position = SCNVector3(worldTransform.x, worldTransform.y, 0)
}
What I want to do is to be able to move both cube at once (so they all undergoes the same translation). It sounds possible from this post:
How to join multiple nodes to one node in iOS scene
However, at the same time this post also says I cannot do that for reason I do not understand yet:
SceneKit nodes aren't changing position with scene's root node
In the worst case I think it is possible to manually apply the transformation to every child node, however applying translation to one parent Node seems to be a much elegant way for doing this.
Edit: I tried this way and can get both moving nodes moving, however sometimes the position is reversed (sometimes the cube comes on top of the other when they should not):
#objc func handleMove(_ gesture: UIPanGestureRecognizer) {
//1. Get The Current Touch Point
let location = gesture.location(in: self.sceneView)
//2. Get The Next Feature Point Etc
guard let nodeHitTest = self.sceneView.hitTest(location, options: nil).first else { print("no node"); return }
// var nodeHit = nodeHitTest.node
let nodeHit = nodeHitTest.node
let original_x = nodeHitTest.node.position.x
let original_y = nodeHitTest.node.position.y
print(original_x, original_y)
// let nodeHit = sceneView.scene.rootNode.childNode(withName: "motherNode2", recursively: true)
//3. Convert To World Coordinates
let worldTransform = nodeHitTest.simdWorldCoordinates
//4. Apply To The Node
//// nodeHit.position = SCNVector3(worldTransform.x, worldTransform.y, 0)
nodeHit.position = SCNVector3(worldTransform.x, worldTransform.y, 0)
for node in nodeHit.parent!.childNodes {
if node.name != nil{
if node.name != nodeHit.name {
let old_x = node.position.x
let old_y = node.position.y
print(old_x, old_y)
node.position = SCNVector3((nodeHit.simdPosition.x + original_x - old_x), (nodeHit.simdPosition.y + original_y - old_y), 0)
}
}
}
Any ideas?

I swapped the plus minus sign and now it is working correctly. Here is the code:
/// - Parameter gesture: UIPanGestureRecognizer
#objc func handleMove(_ gesture: UIPanGestureRecognizer) {
//1. Get The Current Touch Point
let location = gesture.location(in: self.sceneView)
//2. Get The Next Feature Point Etc
guard let nodeHitTest = self.sceneView.hitTest(location, options: nil).first else { print("no node"); return }
// var nodeHit = nodeHitTest.node
let nodeHit = nodeHitTest.node
let original_x = nodeHitTest.node.position.x
let original_y = nodeHitTest.node.position.y
// let nodeHit = sceneView.scene.rootNode.childNode(withName: "motherNode2", recursively: true)
//3. Convert To World Coordinates
let worldTransform = nodeHitTest.simdWorldCoordinates
//4. Apply To The Node
//// nodeHit.position = SCNVector3(worldTransform.x, worldTransform.y, 0)
nodeHit.position = SCNVector3(worldTransform.x, worldTransform.y, 0)
for node in nodeHit.parent!.childNodes {
if node.name != nil{
if node.name != nodeHit.name {
let old_x = node.position.x
let old_y = node.position.y
node.position = SCNVector3((nodeHit.simdPosition.x - original_x + old_x), (nodeHit.simdPosition.y - original_y + old_y), 0)
}
}
}
The idea is even without grouping everything into a new node, I can access all the nodes using nodeHit.parent!.childNodes. This also contains other nodes created by default such as camera or light source so I added the condition to make sure it only selects the nodes with the names I have created. Ideally you just need to use some built in methods to move all the nodes in the scene but I cannot find such method.
So my method first keep track of the old position before moving, then if it is node hit by hit test, move it as before. However, if it is not the node hit by hit test, reposition it by the difference between the the 2 nodes. The relative position difference should be the same regardless of where you move the nodes.

Related

Is there anyway to identify the touched node in ARKit?

I am using ARKit to project a 3D file. In that 3D there are multiple sub nodes. When a user touches on any node we have to display some information about the touched node.
Is there any way we could identify on which node the user touched?
You can perform a hit test to identify which node user has touched. Assuming you have two nodes in your scene, for example:
override func viewDidLoad() {
...
let scene = SCNScene()
let node1 = SCNNode()
node1.name = "node1"
let node2 = SCNNode()
node2.name = "node2"
scene.rootNode.addChildNode(node1)
scene.rootNode.addChildNode(node2)
sceneView.scene = scene
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(tapped))
sceneView.addGestureRecognizer(tapGestureRecognizer)
}
In your tap handler, you can detect the touched node and perform any logic you need, like displaying some information about the node.
#objc func tapped(recognizer: UIGestureRecognizer) {
guard let sceneView = recognizer.view as? SCNView else { return }
let touchLocation = recognizer.location(in: sceneView)
let results = sceneView.hitTest(touchLocation, options: [:])
if results.count == 1 {
let node = results[0].node
print(node.name) // prints "node1" or "node2" if user touched either of them
if node.name == "node1" {
// display node1 information
} else if node.name == "node2" {
// display node2 information
}
}
}

How to scale and move all nodes at once? ARKit Swift

I got these functions that scale and move the 3D object displayed on the screen when tapping on it. The problem consists that if I move or scale the node (3D object) it will only make bigger or smaller one part of the node and I want to scale everything.
I know one solution is to join the 3D object in Blender as one but, later on, I want to add specific animation to specific nodes and that's why I don't want to join them.
Here are the code that I'm using for scaling and moving the objects:
#objc func panned(recognizer :UIPanGestureRecognizer)
{
//This function is for scaling
if recognizer.state == .changed
{
guard let sceneView = recognizer.view as? ARSCNView
else
{
return
}
let touch = recognizer.location(in: sceneView)
let translation = recognizer.translation(in: sceneView)
let hitTestResults = self.sceneView.hitTest(touch, options: nil)
if let hitTest = hitTestResults.first
{
let planeNode = hitTest.node
self.newAngleY = Float(translation.x) * (Float) (Double.pi)/180
self.newAngleY += self.currentAngleY
planeNode.eulerAngles.y = self.newAngleY
}
else if recognizer.state == .ended
{
self.currentAngleY = self.newAngleY
}
}
}
And this one:
#objc func pinched(recognizer :UIPinchGestureRecognizer)
{
if recognizer.state == .changed
{
guard let sceneView = recognizer.view as? ARSCNView
else
{
return
}
}
let touch = recognizer.location(in: sceneView)
let hitTestResults = self.sceneView.hitTest(touch, options: nil)
if let hitTest = hitTestResults.first
{
let planeNode = hitTest.node
let pinchScaleX = Float(recognizer.scale) * planeNode.scale.x
let pinchScaleY = Float(recognizer.scale) * planeNode.scale.y
let pinchScaleZ = Float(recognizer.scale) * planeNode.scale.z
planeNode.scale = SCNVector3(pinchScaleX, pinchScaleY, pinchScaleZ)
recognizer.scale = 1
}
}
I don't know if this helps, but here is an image of the nodes:
Image of the nodes
Thanks in advance!
In order to achieve the "grouping" you are looking for you have to create an empty scene node that will be the root to all other nodes.
let myRootNode : SCNNode = SCNNode()
sceneView.scene.rootNode.addChildNode(myRootNode)
Next append all new nodes in the scene to myRootNode so all your models are parented to the same node.
let newChildNode : SCNNode = SCNNode()
myRootNode.addChildNode(newChildNode)
Then you can apply translations to the myRootNode to move it along with all child nodes. Also the child nodes remain separate models that can be moved (translated/rotated) individually or as a single group.
Within your hit test you should be scaling and rotating the myRootNode instead of the individual model.

ios - ARKit - How to change object position to another surface

I'm working with ARKit, I have a function to move the object like this:
var currentNode = SCNNode()
#objc func moveNode(_ gesture: UIPanGestureRecognizer) {
if !isRotating{
//1. Get The Current Touch Point
let currentTouchPoint = gesture.location(in: self.sceneView)
if gesture.state == .began {
guard let nodeHitTest = self.sceneView.hitTest(currentTouchPoint, options: nil).first else {return}
let nodeHit = nodeHitTest.node
currentNode = nodeHit
}
if gesture.state == .changed {
//2. Get The Next Feature Point Etc
guard let hitTest = self.sceneView.hitTest(currentTouchPoint, types: .existingPlane).first else { return }
//3. Convert To World Coordinates
let worldTransform = hitTest.worldTransform
//4. Set The New Position
let newPosition = SCNVector3(worldTransform.columns.3.x, worldTransform.columns.3.y, worldTransform.columns.3.z)
currentNode.simdPosition = float3(newPosition.x, newPosition.y, newPosition.z)
}
}
}
If my camera detect two surface, how can I move the object from current surface to another surface? Thanks all.

Adding a SCNBillboardConstraint makes the node dissapear

After what I've read in the documentation and on the internet a SCNBillboardConstraint would rotate a node to always face the pointOfView node - in the case of ARKit, the user's camera.
The thing is, when I add a SCNBillboardConstraint to a child node, it dissapears. The nodes are just some SCNTexts added as a subchild of a more complex model.
The hierarchy looks something like this: RootNode - > Text node (two of them).
Just after I added the root node to the scene's root node, I add this constraint in the following way:
updateQueue.async {
self.sceneView.scene.rootNode.addChildNode(virtualObject)
self.sceneView.addOrUpdateAnchor(for: virtualObject)
self.addBillboardContraintsToText(object: virtualObject)
}
func addBillboardContraintsToText(object: VirtualObject) {
guard let storeNode = object.childNodes.first else {
return
}
for node in storeNode.childNodes {
if let geometry = node.geometry, geometry.isKind(of: SCNText.self) {
let billboard = SCNBillboardConstraint()
node.constraints = [billboard]
}
}
}
The text nodes have their position set properly relative to their root node, so there's no problem with that. When I add a SCNLookAtConstraint though, it works just fine.
node.pivot = SCNMatrix4Rotate(node.pivot, Float.pi, 0, 1, 0)
let lookAt = SCNLookAtConstraint(target: sceneView.pointOfView)
lookAt.isGimbalLockEnabled = true
node.constraints = [lookAt]
Any ideas why the SCNBillboardConstraint might not work? Am I doing something wrong?
This Code (with apples CupScn) works just fine for me:
cupNode.position = SCNVector3(0.5,0,-0.5)
guard let virtualObjectScene = SCNScene(named: "cup.scn", inDirectory: "Models.scnassets/cup") else {
return
}
let wrapperNode = SCNNode()
for child in virtualObjectScene.rootNode.childNodes {
child.geometry?.firstMaterial?.lightingModel = .physicallyBased
wrapperNode.addChildNode(child)
}
cupNode.addChildNode(wrapperNode)
scene.rootNode.addChildNode(cupNode)
let billboardConstraint = SCNBillboardConstraint()
billboardConstraint.freeAxes = SCNBillboardAxis.Y
cupNode.constraints = [billboardConstraint]

ARKit : Handle tap to show / hide a node

I am new to ARKit , and i am trying an example to create a SCNBox on tap location. What i am trying to do is on initial touch i create a box and on the second tap on the created box it should be removed from the scene. I am doing the hit test. but it keeps on adding the box. I know this is a simple task, but i am unable to do it
#objc func handleTap(sender: UITapGestureRecognizer) {
print("hande tapp")
guard let _ = sceneView.session.currentFrame
else { return }
guard let scnView = sceneView else { return }
let touchLocation = sender.location(in: scnView)
let hitTestResult = scnView.hitTest(touchLocation, types: [ .featurePoint])
guard let pointOfView = sceneView.pointOfView else {return}
print("point \(pointOfView.name)")
if hitTestResult.count > 0 {
print("Hit")
if let _ = pointOfView as? ARBox {
print("Box Available")
}
else {
print("Adding box")
let transform = hitTestResult.first?.worldTransform.columns.3
let xPosition = transform?.x
let yPosition = transform?.y
let zPosition = transform?.z
let position = SCNVector3(xPosition!,yPosition!,zPosition!)
basketCount = basketCount + 1
let newBasket = ARBox(position: position)
newBasket.name = "basket\(basketCount)"
self.sceneView.scene.rootNode.addChildNode(newBasket)
boxNodes.append(newBasket)
}
}
}
pointOfView of a sceneView, is the rootnode of your scene, which is one used to render your whole scene. For generic cases, it usually is an empty node with lights/ camera. I don't think you should cast it as ARBox/ or any type of SCNNode(s) for that matter.
What you probably can try is the logic below (hitResults are the results of your hitTest):
if hitResults.count > 0 {
if let node = hitResults.first?.node as SCNNode? (or ARBox) {
// node.removeFromParentNode()
// or make the node opaque if you don't want to remove
else {
// add node.

Resources