ARKIT - How to stick two objects - swift

I have two objects (two cubes). First, I add to the scene the first cube. Then I want to add the second one and I want it to be stuck to the first one, on one side of the first one - I will select which side by clicking on it (like in the image below). Is it possible to just click a face of the first cube and the second one to automatically appear into the scene and stick to the first cube? I cannot figure how to do this.
Photo

When you create an SCNBoxGeometry:
The SCNBox class automatically creates SCNGeometryElement objects as
needed to handle the number of materials.
As such in order to access these elements you would need to create an SCNMaterial for each face of the Box. Then you can perform an SCNHitTest to detect which face has been detected:
When you perform a hit-test search, SceneKit 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.
So how would we approach this?
Lets assume you have created to SCNNodes called:
var cubeOne = SCNNode()
var cubeTwo = SCNNode()
These are both assigned 6 different SCNMaterials (one for each face) like so:
override func viewDidLoad() {
super.viewDidLoad()
cubeOne.geometry = cubeGeometry()
cubeOne.position = SCNVector3(0, -0.5, -1.5)
cubeOne.name = "Cube One"
cubeTwo.geometry = cubeGeometry2()
cubeTwo.position = SCNVector3(30, 0, -1.5)
cubeTwo.name = "Cube Two"
self.augmentedRealityView.scene.rootNode.addChildNode(cubeOne)
self.augmentedRealityView.scene.rootNode.addChildNode(cubeTwo)
}
/// Returns The 6 Faces Of An SCNBox
///
/// - Returns: SCNGeometry
func cubeGeometry() -> SCNGeometry{
var colours: [UIColor] = [.red, .green, .cyan, .yellow, .purple, .blue]
var faces = [SCNMaterial] ()
let cubeGeometry = SCNBox(width: 0.2, height: 0.2, length: 0.2, chamferRadius: 0)
for faceIndex in 0..<5{
let material = SCNMaterial()
material.diffuse.contents = colours[faceIndex]
faces.append(material)
}
cubeGeometry.materials = faces
return cubeGeometry
}
Now you have assigned 6 materials to the faces, you will have six geometry elements which correspond to each side of your SCNBox.
Now having done this lets quickly create an enum which corresponds to the order of the faces:
enum BoxFaces: Int{
case Front, Right, Back, Left, Top, Botton
}
Now when we perform a hitTest we can log the location of the hit e.g:
/// Detects Which Cube Was Detected & Logs The Geometry Index
///
/// - Parameter gesture: UITapGestureRecognizer
#IBAction func cubeTapped(_ gesture: UITapGestureRecognizer){
//1. Get The Current Touch Location
let currentTouchLocation = gesture.location(in: self.augmentedRealityView)
//2. Perform An SCNHitTest
guard let hitTest = self.augmentedRealityView.hitTest(currentTouchLocation, options: nil).first else { return }
//3. If The Node In Cube One Then Get The Index Of The Touched Material
if let namedNode = hitTest.node.name{
if namedNode == "Cube One"{
//4. Get The Geometry Index
if let faceIndex = BoxFaces(rawValue: hitTest.geometryIndex){
print("User Has Hit \(faceIndex)")
//5. Position The Second Cube
positionStickyNode(faceIndex)
}
}
}
}
In part 5 you will notice the call to the function positionStickyNode which places the secondCube at the corresponding location of the 1st Cube:
/// Position The Second Cube Based On The Face Tapped
///
/// - Parameter index: BoxFaces
func positionStickyNode(_ index: BoxFaces){
let (min, max) = cubeTwo.boundingBox
let cubeTwoWidth = max.x - min.x
let cubeTwoHeight = max.y - min.y
switch index {
case .Front:
cubeTwo.simdPosition = float3(cubeOne.position.x, cubeOne.position.y, cubeOne.position.z + cubeTwoWidth)
case .Right:
cubeTwo.simdPosition = float3(cubeOne.position.x + cubeTwoWidth, cubeOne.position.y, cubeOne.position.z)
case .Back:
cubeTwo.simdPosition = float3(cubeOne.position.x, cubeOne.position.y, cubeOne.position.z - cubeTwoWidth)
case .Left:
cubeTwo.simdPosition = float3(cubeOne.position.x - cubeTwoWidth, cubeOne.position.y, cubeOne.position.z)
case .Top:
cubeTwo.simdPosition = float3(cubeOne.position.x, cubeOne.position.y + cubeTwoHeight, cubeOne.position.z)
case .Botton:
cubeTwo.simdPosition = float3(cubeOne.position.x, cubeOne.position.y - cubeTwoHeight, cubeOne.position.z)
}
This is a very crude example and will work when your cubes are the same size... You have more than enough however, to now figure out how this would work for different sizes etc.
Hope it helps...

Related

ARKit and RealityKit limitation Entity number

I am study RealityKit and I'm try to drop on the floor a set of red card divided in different row at the position tapped.
See the picture attached.
I notice that if the number of cards is to much AR kit display the cards for few second in the sene but then after a second the cards disappeared from a scene.
and on the console I get the following print out:
2021-10-10 15:31:31.851724+0800 indovinaWho[12166:4021700] [Session] ARSession <0x111038050>: ARSessionDelegate is retaining 14 ARFrames. This can lead to future camera frames being dropped.
Here my code how I drop the cards.
Basically with the following code I'm creating a AnchorEntity at location tapped and then append the entity "modCard" changing the position accordingly.
If for example I state 8 card per row in 3 row, I just see the card for few second and then suddently they disappeared from my scene.
Reality kit have a maximum number of entity can be attached to an AnchorEntity?
func createCard(raycast: ARRaycastResult, maxrow: Int, cardPerRow: Int){
let anchor = AnchorEntity(raycastResult: raycast)
var prPosMod = simd_make_float3(0,0,0) // prev position
let cardByside = cardPerRow/2
DispatchQueue.global().sync {
for _ in 1...maxrow {
for ncard in 1...cardPerRow {
let modEntity = self.modelCard()
if ncard < cardByside {
modEntity.position = simd_make_float3(prPosMod.x-0.1,prPosMod.y,prPosMod.z)
}else {
modEntity.position = simd_make_float3(prPosMod.x + 0.1,prPosMod.y,prPosMod.z)
}
prPosMod = modEntity.position
anchor.addChild(modEntity)
}
prPosMod = simd_float3(0,prPosMod.y,prPosMod.z-0.1)
}
self.arView.scene.addAnchor(anchor)
}
}
func modelCard() -> ModelEntity {
let mesh = MeshResource.generateBox(width: 0.08, height: 0.01, depth: 0.08, cornerRadius: 0.005, splitFaces: false)
let material = SimpleMaterial(color: .red, isMetallic: false)
return ModelEntity(mesh: mesh, materials: [material])
}

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.

ARSCNView unprojectPoint

I need to convert a point in the 2d coordinate space of my ARSCNView to a coordinate in 3d space. Basically a ray from the point of view to the touched location (up to a set distance away).
I wanted to use arView.unprojectPoint(vec2d) for that, but the point returned always seems to be located in the center of the view
vec2d is a SCNVector3 created from a 2d coordinate like this
SCNVector3(x, y, 0) // 0 specifies camera near plane
What am I doing wrong? How do I get the desired result?
I think you have at least 2 possible solutions:
First
Use hitTest(_:types:) instance method:
This method searches for real-world objects or AR anchors in the captured camera image corresponding to a point in the SceneKit view.
let sceneView = ARSCNView()
func calculateVector(point: CGPoint) -> SCNVector3? {
let hitTestResults = sceneView.hitTest(point,
types: [.existingPlane])
if let result = hitTestResults.first {
return SCNVector3.init(SIMD3(result.worldTransform.columns.3.x,
result.worldTransform.columns.3.y,
result.worldTransform.columns.3.z))
}
return nil
}
calculateVector(point: yourPoint)
Second
Use unprojectPoint(_:ontoPlane:) instance method:
This method returns the projection of a point from 2D view onto a plane in the 3D world space detected by ARKit.
#nonobjc func unprojectPoint(_ point: CGPoint,
ontoPlane planeTransform: simd_float4x4) -> simd_float3?
or:
let point = CGPoint()
var planeTransform = simd_float4x4()
sceneView.unprojectPoint(point,
ontoPlane: planeTransform)
Add a empty node infront of camera at 'x' cm offset and making it the child of camera.
//Add a node in front of camera just after creating scene
hitNode = SCNNode()
hitNode!.position = SCNVector3Make(0, 0, -0.25) //25 cm offset
sceneView.pointOfView?.addChildNode(hitNode!)
func unprojectedPosition(touch: CGPoint) -> SCNVector3 {
guard let hitNode = self.hitNode else {
return SCNVector3Zero
}
let projectedOrigin = sceneView.projectPoint(hitNode.worldPosition)
let offset = sceneView.unprojectPoint(SCNVector3Make(Float(touch.x), Float(touch.y), projectedOrigin.z))
return offset
}
See the Justaline GitHub implementation of the code here

Randomly place object within a 10metre radius within AR scene view

I want to be able to place an object randomly within a set radius(5-10 metres). the idea is that the user will walk around and eventually the object will come into view.
Here is an example which uses #Coeur's randomDistance Function:
/// Spawns 5 Nodes At Random Distances Away From The Camera
func spwanRandomNodes(){
//1. Create An Array Of Colours
var colourArray: [UIColor] = [.red, .green, .yellow, .cyan, .white]
//2. Create 5 Different Spheres With A Random Color & Position
for i in 0...4{
let nodeHolder = SCNNode()
let nodeGeometry = SCNSphere(radius: 0.2)
nodeGeometry.firstMaterial?.diffuse.contents = colourArray[i]
nodeHolder.geometry = nodeGeometry
//3. Create A Random Distance From 5 To 10 (Cœur's Answer)
let randomDistanceFrom5To10 = Float(arc4random()) / Float(UInt32.max) * 5 + 5
//4. Add The Node To The Scene Root
augmentedRealityView?.scene.rootNode.addChildNode(nodeHolder)
//5. Generate The Random SCNVector3
/* Here I Have Added An XSpacer Simply For Testing */
let xSpacer = Float(0.3 * Float(i))
let randomVector = SCNVector3 (xSpacer, 0, -(randomDistanceFrom5To10))
//6. Set THe Nodes Positon
nodeHolder.position = randomVector
}
}

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!