Detect touch on child node of object in SceneKit - swift

Basically this question but for SceneKit.
I have a parent node with a few smaller nodes inside it, the parent node at a later point becomes transparent, (parent's diffuse material opacity is set to 0) after that i would like to get the node that was tapped inside the object, how should i go about doing that? The default hit testing returns the parent node, and since there are quiet a few smaller nodes inside the object, i need the precise one that was tapped.

To fix this issue I recommend to read next topic from Apple:
https://developer.apple.com/documentation/scenekit/scnhittestoption
General idea:
func registerGestureRecognizer() {
let tap = UITapGestureRecognizer(target: self, action: #selector(search))
self.sceneView.addGestureRecognizer(tap)
}
#objc func search(sender: UITapGestureRecognizer) {
let sceneView = sender.view as! ARSCNView
let location = sender.location(in: sceneView)
let results = sceneView.hitTest(location, options: [SCNHitTestOption.searchMode : 1])
guard sender.state == .ended else { return }
for result in results.filter( { $0.node.name == "Your node name" }) {
// do manipulations
}
}
Hope it helps!
Best regards.

Related

Scenekit: rotate camera to tap point (3D)

I have a camera node.
Around the camera node, there is another big node (.obj file) of a building.
User can move inside the building.
User can do LongPressGesture, and additional node (let's say a sphere) appears on the wall of the building. I want to rotate my camera to this new node (to tap location).
I don't know how to do it. Can someone help me?
Other answers are not correct for me. Camera just rotates in random directions.
I've found a way!
I take the location of a tap (or any coordinates you need to turn to)
#objc private func handleLongPress(pressRec: UILongPressGestureRecognizer) {
let arr: [UIGestureRecognizer.State] = [.cancelled, .ended, .failed]
if !arr.contains(pressRec.state) {
let touchPoint = pressRec.location(in: sceneView)
let hitResults = sceneView.hitTest(touchPoint, options: [:])
if let result: SCNHitTestResult = hitResults.first {
createAnnotation(result.worldCoordinates)
pressRec.state = .cancelled
}
}
}
func for turn camera:
func turnCameraTo(worldCoordinates: SCNVector3) {
SCNTransaction.begin()
SCNTransaction.animationDuration = C.hotspotAnimationDuration
cameraNode.look(at: worldCoordinates)
sceneView.defaultCameraController.clearRoll()
SCNTransaction.completionBlock = {
}
SCNTransaction.commit()
}

remove nodes from scene after use ARSCNView

I am making an app using ARKit to measure between two points. The goal is to be able to measure length, store that value, then measure width and store.
The problem I am having is disposing of the nodes after I get the measurement.
Steps so far:
1) added a button with a restartFunction. this worked to reset the measurements but did not remove the spheres from scene, and also made getting the next measurement clunky.
2) set a limit on > 2 nodes. This functionaly works best. But again the spheres just stay floating in the scene.
Here is a screen shot of the best result I have had.
#objc func handleTap(sender: UITapGestureRecognizer) {
let tapLocation = sender.location(in: sceneView)
let hitTestResults = sceneView.hitTest(tapLocation, types: .featurePoint)
if let result = hitTestResults.first {
let position = SCNVector3.positionFrom(matrix: result.worldTransform)
let sphere = SphereNode(position: position)
sceneView.scene.rootNode.addChildNode(sphere)
let tail = nodes.last
nodes.append(sphere)
if tail != nil {
let distance = tail!.position.distance(to: sphere.position)
infoLabel.text = String(format: "Size: %.2f inches", distance)
if nodes.count > 2 {
nodes.removeAll()
}
} else {
nodes.append(sphere)
}
}
}
I am new to Swift (coding in general) and most of my code has come from piecing together tutorials.
I think the issue here is that your are not actually removing the SCNNodes you have added to hierarchy.
Although you are removing the nodes from what I assume is an array of SCNNodes by calling: nodes.removeAll(), you first need to actually remove them from the scene hierarchy.
So what you need to do is call the following function on any node you wish to remove:
removeFromParentNode()
Which simply:
Removes the node from its parent’s array of child nodes.
As such you would do something like this which first removes the nodes from the hierarchy, and then removes them from the array:
for nodeAdded in nodesArray{
nodeAdded.removeFromParentNode()
}
nodesArray.removeAll()
So based on the code provided you could do the following:
if nodes.count > 2 {
for nodeAdded in nodes{
nodeAdded.removeFromParentNode()
}
nodes.removeAll()
}
For future reference, if you want to remove all SCNNodes from you hierarchy you can also call:
self.augmentedRealityView.scene.rootNode.enumerateChildNodes { (existingNode, _) in
existingNode.removeFromParentNode()
}
Whereby self.augmentedRealityView refers to the variable:
var augmentedRealityView: ARSCNView!
Here is a very basic working example based on (and modified from) the code you have provided:
/// Places A Marker Node At The Desired Tap Point
///
/// - Parameter sender: UITapGestureRecognizer
#objc func handleTap(_ sender: UITapGestureRecognizer) {
//1. Get The Current Tap Location
let currentTapLocation = sender.location(in: sceneView)
//2. Check We Have Hit A Feature Point
guard let hitTestResult = self.augmentedRealityView.hitTest(currentTapLocation, types: .featurePoint).first else { return }
//3. Get The World Position From The HitTest Result
let worldPosition = positionFromMatrix(hitTestResult.worldTransform)
//4. Create A Marker Node
createSphereNodeAt(worldPosition)
//5. If We Have Two Nodes Then Measure The Distance
if let distance = distanceBetweenNodes(){
print("Distance == \(distance)")
}
}
/// Creates A Marker Node
///
/// - Parameter position: SCNVector3
func createSphereNodeAt(_ position: SCNVector3){
//1. If We Have More Than 2 Nodes Remove Them All From The Array & Hierachy
if nodes.count >= 2{
nodes.forEach { (nodeToRemove) in
nodeToRemove.removeFromParentNode()
}
nodes.removeAll()
}
//2. Create A Marker Node With An SCNSphereGeometry & Add It To The Scene
let markerNode = SCNNode()
let markerGeometry = SCNSphere(radius: 0.01)
markerGeometry.firstMaterial?.diffuse.contents = UIColor.cyan
markerNode.geometry = markerGeometry
markerNode.position = position
sceneView.scene.rootNode.addChildNode(markerNode)
//3. Add It To The Nodes Array
nodes.append(markerNode)
}
/// Converts A matrix_float4x4 To An SCNVector3
///
/// - Parameter matrix: matrix_float4x4
/// - Returns: SCNVector3
func positionFromMatrix(_ matrix: matrix_float4x4) -> SCNVector3{
return SCNVector3(matrix.columns.3.x, matrix.columns.3.y, matrix.columns.3.z)
}
/// Calculates The Distance Between 2 Nodes
///
/// - Returns: Float?
func distanceBetweenNodes() -> Float? {
guard let firstNode = nodes.first, let endNode = nodes.last else { return nil }
let startPoint = GLKVector3Make(firstNode.position.x, firstNode.position.y, firstNode.position.z)
let endPoint = GLKVector3Make(endNode.position.x, endNode.position.y, endNode.position.z)
let distance = GLKVector3Distance(startPoint, endPoint)
return distance
}
For an example of a measuringApp which might help your development you can look here: ARKit Measuring Example
Hope it helps...
This looks like a logic issue. You're assigning nodes.last to tail just before checking if tail is not nil. So it will never be != nil so you'll never execute the nodes.append(sphere) in the else.
I agree with #dfd. Set a breakpoint to make sure the code is being executed before continuing.

ARKit dragging object changes scale

İ am trying to move the SCNNode object which i placed on to a surface. İt moves but the scale changes and it becomes smaller, when i first start to move.
This is what i did;
#IBAction func dragBanana(_ sender: UIPanGestureRecognizer) {
guard let _ = self.sceneView.session.currentFrame else {return}
if(sender.state == .began) {
let location = sender.location(in: self.sceneView)
let hitTestResult = sceneView.hitTest(location, options: nil)
if !hitTestResult.isEmpty {
guard let hitResult = hitTestResult.first else {return}
movedObject = hitResult.node
}
}
if (sender.state == .changed) {
if(movedObject != nil) {
let location = sender.location(in: self.sceneView)
let hitTestResult = sceneView.hitTest(location, types: .existingPlaneUsingExtent)
guard let hitResult = hitTestResult.first else {return}
let matrix = SCNMatrix4(hitResult.worldTransform)
let vector = SCNVector3Make(matrix.m41, matrix.m42, matrix.m43)
movedObject?.position = vector
}
}
if (sender.state == .ended) {
movedObject = nil
}
}
My answer is probably very late, but I faced this issue myself and it took me a while to kind of figure out why this might happen. I'll share my experience and maybe you can relate to it.
My problem was that I was trying to change the position of the node after changing its scale at runtime (most of my 3D assets were very large when added, I scale them down with a pinch gesture). I noticed changing the scale was the cause of the position change not working as expected.
I found a very simple solution to this. You simply need to change this line:
movedObject?.position = vector
to this:
movedObject?.worldPosition = vector
According to SCNNode documentation, the position property determines the position of the node relative to its parent. While worldPosition is the position of the node relative to the scene's root node (i.e. the world origin of ARSCNView)
I hope this answers your question.
It's because you're moving the object on the 3 axis and Z changes that's why it feels like it scales but it's only getting closer to you.

I cant scale a Virtual Object in an other Class swift

Im trying to figure out how to scale a Virtual object in an other class
I tried to make a function in the extension of the Virtual object that looks like this
public func setSize1(_ size: Float, node: SCNNode) -> VirtualObject? {
if let virtualObjectRoot = node as? VirtualObject {
return virtualObjectRoot
}
guard let parent = node.parent else { return nil }
node.scale = SCNVector3(x: size, y: size, z: size)
node.position.y = (0)
return VirtualObject.existingObjectContainingNode(parent)
}
And then i call it with
#IBAction func sliderValueChanged(_ sender: UISlider) {
let newValue = Float(sender.value)
VirtualObject().setSize1(newValue, node: SCNNode)
}
But every time i do this, i get an error like: "Cannot convert value of type 'SCNNode.Type' to expected argument type 'SCNNode'" What is the best way to edit a Node in a other class?
The problem is here:
VirtualObject().setSize1(newValue, node: SCNNode)
You're not passing an object, but a type (SCNNode is the name of a class). You have to pass the SCNNode instance instead.
Edit
To retrieve the node that you want to use:
guard let node = self.childNode(withName: myCachedNodeName, recursively: true) else { return }
VirtualObject().setSize1(newValue, node: node)
Edit 2
Given your needs, I suggest to scale the node using a pinch gesture recognizer. This is how to add it to your scene view controller:
let pinch = UIPinchGestureRecognizer(target: self, action: #selector(pinch(:)))
self.view.addGestureRecognizer(pinch)
Then you have to declare a method that will be executed whenever a pinch gesture is detected:
func pinch(gesture:UIPinchGestureRecognizer) {
let scale = gesture.scale
if gesture.state == .ended {
// Use the scale value to scale you SCNNode
// Alternatively, you could scale your node continuously and not
// only when the gesture is ended. But in this case, remember to
// reset the pinch gesture recognizer's scale value
}
}

Add SCNNode after rotating rootNode

I'm trying to add a node (a sphere) to a body model but it doesn't work properly after I rotate the model through a pan gesture.
Here's how I'm adding the node (using a long tap gesture):
func addSphere(sender: UILongPressGestureRecognizer) {
switch sender.state {
case .Began:
let location = sender.locationInView(bodyView)
let hitResults = bodyView.hitTest(location, options: nil)
if hitResults.count > 0 {
let result = hitResults.first!
let secondSphereGeometry = SCNSphere(radius: 0.015)
secondSphereGeometry.firstMaterial?.diffuse.contents = UIColor.redColor()
let secondSphereNode = SCNNode(geometry: secondSphereGeometry)
let vpWithZ = SCNVector3(x: Float(result.worldCoordinates.x), y: Float(result.worldCoordinates.y), z: Float( result.worldCoordinates.z))
secondSphereNode.position = vpWithZ
bodyView.scene!.rootNode.addChildNode(secondSphereNode)
}
break
default:
break
}
}
Here is how I rotate the view:
func rotateGesture(sender: UIPanGestureRecognizer) {
let translation = sender.translationInView(sender.view)
var newZAngle = (Float)(translation.x)*(Float)(M_PI)/180.0
newZAngle += currentZAngle
bodyView.scene!.rootNode.transform = SCNMatrix4MakeRotation(newZAngle, 0, 0, 1)
if sender.state == .Ended {
currentZAngle = newZAngle
}
}
And to load the 3D model I just do:
bodyView.scene = SCNScene(named: "male_body.dae") // bodyView is a SCNView in the storyboard
I found something related to the worldTransform property and also the function convertPosition:toNode: but couldn't find an example that works well.
The problem is that, if I rotate the model, the sphere are not positioned properly. They're always positioned as if the model was in its initial state.
If I turn the body and add long tap his arm (on the side), the sphere is added somewhere floating in front of the body, as you can see above.
I don't know how to fix this. Appreciate if someone can help me. Thanks!