How to draw line node keep same size in camera as Measure App in iPhone? - swift

I try make an AR app as a Measure default app in iPhone. ( I base on project TBXark/Ruler on github)
I draw startNode, endNode, cylinder line, and SCNText. But I can't manage the scale of size, it only readable in near, and so small when measure far plane detect.
I have 2 question:
How to keep size node, cylinder and text same when draw near or far as Measure App
How to draw scntext with background and align the same direction cylinder line as Measure App.
Here is my Line Node class:
class LineNode: NSObject {
let startNode: SCNNode
let endNode: SCNNode
var lineNode: SCNNode?
let textNode: SCNNode
let sceneView: ARSCNView?
// init func
init(startPos: SCNVector3,
sceneV: ARSCNView,
color: (start: UIColor, end: UIColor) = (UIColor(hexCss: 0xF1B426), UIColor(hexCss: 0xD43278)),
font: UIFont = UIFont.boldSystemFont(ofSize: 8) ) {
sceneView = sceneV
let scale = 1 / 400.0
let scaleVector = SCNVector3(scale, scale, scale)
func buildSCNSphere(color: UIColor) -> SCNSphere {
let dot = SCNSphere(radius: 1)
dot.firstMaterial?.diffuse.contents = color
dot.firstMaterial?.lightingModel = .constant
dot.firstMaterial?.isDoubleSided = true
return dot
}
// startNode
startNode = SCNNode(geometry: buildSCNSphere(color: color.start))
startNode.scale = scaleVector
startNode.position = startPos
sceneView?.scene.rootNode.addChildNode(startNode)
// endNode
endNode = SCNNode(geometry: buildSCNSphere(color: color.end))
endNode.scale = scaleVector
// line with start to end
lineNode = CylinderLine(parent: sceneView!.scene.rootNode,
v1: startNode.position,
v2: endNode.position,
radius: 0.001,
radSegmentCount: 16,
color: UIColor.white)
sceneView?.scene.rootNode.addChildNode(lineNode!)
// text show measure line length
let text = SCNText (string: "--", extrusionDepth: 0.1)
text.font = font
text.firstMaterial?.diffuse.contents = UIColor(hexCss: 0xffa800)
text.firstMaterial?.lightingModel = .constant
text.alignmentMode = CATextLayerAlignmentMode.center.rawValue
text.truncationMode = CATextLayerTruncationMode.middle.rawValue
text.firstMaterial?.isDoubleSided = true
textNode = SCNNode(geometry: text)
textNode.scale = SCNVector3(1 / 500.0, 1 / 500.0, 1 / 500.0)
super.init()
}
// update end node realtime
public func updatePosition(pos: SCNVector3, camera: ARCamera?, unit: MeasurementUnit.Unit = MeasurementUnit.Unit.centimeter) -> Float {
// update endNode
let posEnd = updateTransform(for: pos, camera: camera)
if endNode.parent == nil {
sceneView?.scene.rootNode.addChildNode(endNode)
}
endNode.position = posEnd
// caculate new mid
let posStart = startNode.position
let middle = SCNVector3((posStart.x + posEnd.x) / 2.0, (posStart.y + posEnd.y) / 2.0 + 0.002, (posStart.z + posEnd.z) / 2.0)
// update text measure
let text = textNode.geometry as! SCNText
let length = posEnd.distanceFromPos(pos: startNode.position)
text.string = MeasurementUnit(meterUnitValue: length).roundUpstring(type: unit)
text.materials.first?.diffuse.contents = UIColor.orange
textNode.setPivot()
textNode.position = middle
if textNode.parent == nil {
sceneView?.scene.rootNode.addChildNode(textNode)
}
lineNode?.removeFromParentNode()
lineNode = lineBetweenNodeA(nodeA: startNode, nodeB: endNode)
sceneView?.scene.rootNode.addChildNode(lineNode!)
return length
}
}

I use this to update scale if even if you stay far away it still readable
func updateScaleFromCameraForNodes(_ nodes: [SCNNode], fromPointOfView pointOfView: SCNNode , useScaling: Bool){
nodes.forEach { (node) in
//1. Get The Current Position Of The Node
let positionOfNode = SCNVector3ToGLKVector3(node.worldPosition)
//2. Get The Current Position Of The Camera
let positionOfCamera = SCNVector3ToGLKVector3(pointOfView.worldPosition)
//3. Calculate The Distance From The Node To The Camera
let distanceBetweenNodeAndCamera = GLKVector3Distance(positionOfNode, positionOfCamera)
let a = distanceBetweenNodeAndCamera*1.75
if(useScaling) {
node.simdScale = simd_float3(a,a,a)
}
}
SCNTransaction.flush()
}
then called it in the renderer updateAtTime
self.updateScaleFromCameraForNodes(self.nodesAdded, fromPointOfView:
self.cameraNode, useScaling: true)

Related

Place node in front of the camera and rotate

I want to show a pointer when a user double taps on a sceneView. For this, I want to use SCNTorus because at some angle it is a circle.
let geometry = SCNTorus(ringRadius: 0.01, pipeRadius: 0.001)
let node = SCNNode(geometry: geometry)
I already get a vector from hitTest that I use as a position for the node
private func getVector(for point: CGPoint) -> SCNVector3? {
guard let hitTest = self.sceneView.hitTest(point, types: .featurePoint).first else {
return nil
}
let transform = SCNMatrix4.init(hitTest.worldTransform)
let vector = SCNVector3Make(transform.m41, transform.m42, transform.m43)
return vector
}
How can I transform it to always appear as a circle when the node added.
For vertical device position rotating by .pi/2 along X-axis works great but it breaks when devices move in a space, so I can not hardcode the angle
node.eulerAngles.x = .pi / 2
sceneView.scene.rootNode.addChildNode(node)
I believe I need to sceneView.pointOfView somehow to apply the transformation. But I stuck here.
Here is the full code
#objc private func didDoubleTap(_ sender: UITapGestureRecognizer) {
let point = sender.location(in: sceneView)
guard let vector = getVector(for: point) else { return }
guard let pov = sceneView.pointOfView else { return }
guard let camera = sceneView.session.currentFrame?.camera else { return }
let geometry = SCNTorus(ringRadius: 0.01, pipeRadius: 0.001)
let node = SCNNode(geometry: geometry)
node.position = vector
node.eulerAngles.x = .pi / 2
sceneView.scene.rootNode.addChildNode(node)
animatePointingNode(node)
}
I was able to solve the issue by setting eulerAngles.y to be equal to camera's Y and adjusting camera's X by .pi/2
guard let camera = sceneView.session.currentFrame?.camera else { return }
pointerNode.position = vector
pointerNode.eulerAngles.x = camera.eulerAngles.x + .pi / 2
pointerNode.eulerAngles.y = camera.eulerAngles.y

How to draw a line between two points in SceneKit?

If I have two points in SceneKit (e.g. (1,2,3) and (-1,-1,-1)). How do I draw a line between the two?
I see that there is a SCNBox object I may be able to use, but that only allows me to specify the center (e.g. via simdPosition). The other ways to modify it are the transform (which I don't know how to use), or the Euler angles (which I'm not sure how to calculate which ones I need to use).
You can draw a line between two points using the following approach:
import SceneKit
extension SCNGeometry {
class func line(vector1: SCNVector3,
vector2: SCNVector3) -> SCNGeometry {
let sources = SCNGeometrySource(vertices: [vector1,
vector2])
let index: [Int32] = [0,1]
let elements = SCNGeometryElement(indices: index,
primitiveType: .line)
return SCNGeometry(sources: [sources],
elements: [elements])
}
}
...and then feed it to addLine function in ViewController:
class ViewController: UIViewController {
// Some code...
func addLine(start: SCNVector3, end: SCNVector3) {
let lineGeo = SCNGeometry.line(vector1: start,
vector2: end)
let lineNode = SCNNode(geometry: lineGeo)
sceneView.scene.rootNode.addChildNode(lineNode)
}
}
As we all know line's width can't be changed (cause there's no property to do it), so you can use cylinder primitive geometry instead of a line:
extension SCNGeometry {
class func cylinderLine(from: SCNVector3,
to: SCNVector3,
segments: Int) -> SCNNode {
let x1 = from.x
let x2 = to.x
let y1 = from.y
let y2 = to.y
let z1 = from.z
let z2 = to.z
let distance = sqrtf( (x2-x1) * (x2-x1) +
(y2-y1) * (y2-y1) +
(z2-z1) * (z2-z1) )
let cylinder = SCNCylinder(radius: 0.005,
height: CGFloat(distance))
cylinder.radialSegmentCount = segments
cylinder.firstMaterial?.diffuse.contents = UIColor.green
let lineNode = SCNNode(geometry: cylinder)
lineNode.position = SCNVector3(x: (from.x + to.x) / 2,
y: (from.y + to.y) / 2,
z: (from.z + to.z) / 2)
lineNode.eulerAngles = SCNVector3(Float.pi / 2,
acos((to.z-from.z)/distance),
atan2((to.y-from.y),(to.x-from.x)))
return lineNode
}
}
...then feed it the same way to ViewController:
class ViewController: UIViewController {
// Some code...
func addLine(start: SCNVector3, end: SCNVector3) {
let cylinderLineNode = SCNGeometry.cylinderLine(from: start,
to: end,
segments: 3)
sceneView.scene.rootNode.addChildNode(cylinderLineNode)
}
}
First you'll need to calculate the heading and pitch between the two points. Full post is here and this answer explains how to do it between any arbitrary two points.
Once you have your two angles, if you attempt to use the Euler angles on an SCNBox, you'll notice that when you only modify the pitch (eulerAngles.x), or only modify the heading (eulerAngles.y), everything works fine. However, the moment you try to modify both, you'll run into issues. One solution is to wrap one node inside another.
This seemed like such a useful suggestion, that I create a handy wrapper node that should handle rotations upon all 3 axes:
import Foundation
import SceneKit
struct HeadingPitchBank {
let heading: Float
let pitch: Float
let bank: Float
/// returns the heading and pitch (bank is 0) represented by the vector
static func from(vector: simd_float3) -> HeadingPitchBank {
let heading = atan2f(vector.x, vector.z)
let pitch = atan2f(sqrt(vector.x*vector.x + vector.z*vector.z), vector.y) - Float.pi / 2.0
return HeadingPitchBank(heading: heading, pitch: pitch, bank: 0)
}
}
class HeadingPitchBankWrapper: SCNNode {
init(wrappedNode: SCNNode) {
headingNode = SCNNode()
pitchNode = SCNNode()
bankNode = SCNNode()
_wrappedNode = wrappedNode
super.init()
addChildNode(headingNode)
headingNode.addChildNode(pitchNode)
pitchNode.addChildNode(bankNode)
bankNode.addChildNode(wrappedNode)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
var heading: Float {
get {
return headingNode.eulerAngles.y
}
set {
headingNode.eulerAngles.y = newValue
}
}
var pitch: Float {
get {
return pitchNode.eulerAngles.x
}
set {
pitchNode.eulerAngles.x = newValue
}
}
var bank: Float {
get {
return bankNode.eulerAngles.z
}
set {
bankNode.eulerAngles.z = newValue
}
}
var headingPitchBank: HeadingPitchBank {
get {
return HeadingPitchBank(heading: heading, pitch: pitch, bank: bank)
}
set {
heading = newValue.heading
pitch = newValue.pitch
bank = newValue.bank
}
}
var wrappedNode: SCNNode {
return _wrappedNode
}
private var headingNode: SCNNode
private var pitchNode: SCNNode
private var bankNode: SCNNode
private var _wrappedNode: SCNNode
}
You could then use this to easily draw a line between two points:
func createLine(start: simd_float3 = simd_float3(), end: simd_float3, color: UIColor, opacity: CGFloat? = nil, radius: CGFloat = 0.005) -> SCNNode {
let length = CGFloat(simd_length(end-start))
let box = SCNNode(geometry: SCNBox(width: radius, height: radius, length: length, chamferRadius: 0))
box.geometry!.firstMaterial!.diffuse.contents = color
let wrapper = HeadingPitchBankWrapper(wrappedNode: box)
wrapper.headingPitchBank = HeadingPitchBank.from(vector: end - start)
wrapper.simdPosition = midpoint(start, end)
if let opacity = opacity {
wrapper.opacity = opacity
}
return wrapper
}
Just build a custom geometry using SCNGeometryPrimitiveType.line:
let vertices: [SCNVector3] = [
SCNVector3(1, 2, 3),
SCNVector3(-1, -1, -1)
]
let linesGeometry = SCNGeometry(
sources: [
SCNGeometrySource(vertices: vertices)
],
elements: [
SCNGeometryElement(
indices: [Int32]([0, 1]),
primitiveType: .line
)
]
)
let line = SCNNode(geometry: linesGeometry)
scene.rootNode.addChildNode(line)

Get Size of image in SCNNode / ARKit Swift

I'm trying to scan a Reference-Image an then display the image itself above the printed reference-image. The "virutal" image size should be the same like the printed size.
My idea: get the size of the printed Reference-Image, then scale the image in the SCNNode to this size (or scale the SCNNode to this size?)
But: 1-> How to get the size of the printed image, 2-> for scaling the SCNNode I need the size of this node, too. How to get it?
import UIKit
import SceneKit
import ARKit
import AVKit
import AVFoundation
class ViewController: UIViewController, ARSCNViewDelegate {
#IBOutlet var sceneView: ARSCNView!
private var planeNode: SCNNode?
private var imageNode: SCNNode?
private var animationInfo: AnimationInfo?
private var currentMediaName: String?
private var scrollView: UIScrollView!
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){
let location: CGPoint = rec.location(in: sceneView)
let hits = self.sceneView.hitTest(location, options: nil)
if !hits.isEmpty{
let tappedNode = hits.first?.node
if tappedNode != nil && tappedNode?.name != nil{
let stringArr = tappedNode?.name?.components(separatedBy: "-")
let name = stringArr! [0]
let size = stringArr! [1].components(separatedBy: ",")
let width = Float(size [0])
let height = Float(size [1])
loadReferenceImage(tappedNode: tappedNode!, name: (name), width: width!, height: height!)
}
}
}
private func playVideo() {
guard let path = Bundle.main.path(forResource: "video", ofType:"m4v") else {
debugPrint("video.m4v not found")
return
}
let player = AVPlayer(url: URL(fileURLWithPath: path))
let playerController = AVPlayerViewController()
playerController.player = player
present(playerController, animated: true) {
player.play()
}
}
func loadReferenceImage(tappedNode: SCNNode, name: String, width: Float, height: Float){
print("TAP")
print(name)
let currentNode = tappedNode.parent
if let image = UIImage(named: "col" + name){
let childNodes = currentNode?.childNodes
for node in (childNodes)!{
node.removeFromParentNode()
}
let newImage = UIImage(named: "col" + name)
let newnode = SCNNode(geometry: SCNPlane(width: CGFloat(width), height: CGFloat(height)))
newnode.geometry?.firstMaterial?.diffuse.contents = newImage
newnode.scale = SCNVector3(x: 10, y: 10, z: 10)
currentNode?.removeAnimation(forKey: "spin_around")
let rotation = SCNVector3((currentNode?.eulerAngles.x)!-0.95,(currentNode?.eulerAngles.y)!,(currentNode?.eulerAngles.z)!)
currentNode?.eulerAngles = rotation
//SIZE??????
let nodex = currentNode?.scale.x
let nodey = currentNode?.scale.y
let nodez = currentNode?.scale.z
let factorx = width / nodex!
let factory = height / nodey!
currentNode?.addChildNode(newnode)
}
}
func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
guard let imageAnchor = anchor as? ARImageAnchor else {
return
}
// 1. Load 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)/1.2
let heightRatio = Float(imageAnchor.referenceImage.physicalSize.height)/1.2
let width = imageAnchor.referenceImage.physicalSize.width
let height = imageAnchor.referenceImage.physicalSize.height
let prefix = "-"
let imageSize = width.description + "," + height.description
let targetName = imageAnchor.referenceImage.name! + prefix + imageSize
// 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)
//test
appearanceAction.timingMode = .easeOut
// Set initial scale to 0.
planeNode.scale = SCNVector3Make(0 , 0, 0)
//rotate y
let spin = CABasicAnimation(keyPath: "rotation")
spin.fromValue = NSValue(scnVector4: SCNVector4(x: 0, y: 1, z: 0, w: 0))
spin.toValue = NSValue(scnVector4: SCNVector4(x: 0, y: 1, z: 0, w: Float(CGFloat(2 * Double.pi))))
spin.duration = 4
spin.repeatCount = .infinity
planeNode.addAnimation(spin, forKey: "spin_around")
// Add to root node.
sceneView.scene.rootNode.addChildNode(planeNode)
// Run the appearance animation.
planeNode.runAction(appearanceAction)
planeNode.name = targetName
let nodes = planeNode.childNodes
for node in nodes{
node.name = targetName
}
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 }
}
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)
}
}
In order to do this I believe that first you need to get the size in Pixels of the UIImage by
multiplying the size values by the value in the scale property to get
the pixel dimensions of the image.
As such an example would be something like so:
guard let image = UIImage(named: "launchScreen") else { return }
let pixelWidth = image.size.width * image.scale
let pixelHeight = image.size.height * image.scale
print(pixelWidth, pixelHeight)
The size of my image when made in Adobe Illustrator was 3072 x 4099, and when I logged the results in the console the dimensions were also the same.
Now the tricky part here is calculating the pixels to a size we can use in ARKit, remembering that different devices have a different PPI (Pixels Per Inch) density.
In my example I am just going to use the PPI of an iPhone7Plus which is 401.
//1. Get The PPI Of The iPhone7Plus
let iphone7PlusPixelsPerInch: CGFloat = 401
//2. To Get The Image Size In Inches We Need To Divide By The PPI
let inchWidth = pixelWidth/iphone7PlusPixelsPerInch
let inchHeight = pixelHeight/iphone7PlusPixelsPerInch
//3. Calculate The Size In Metres (There Are 2.54 Cm's In An Inch)
let widthInMetres = (inchWidth * 2.54) / 100
let heightInMeters = (inchHeight * 2.54) / 100
Now we have the size of our Image in Metres it is simple to create an SCNNode of that size e.g:
//1. Generate An SCNPlane With The Same Size As Our Image
let realScaleNode = SCNNode(geometry: SCNPlane(width: widthInMetres, height: heightInMeters))
realScaleNode.geometry?.firstMaterial?.diffuse.contents = image
realScaleNode.position = SCNVector3(0, 0, -1)
//2. Add It To Our Hierachy
self.augmentedRealityView.scene.rootNode.addChildNode(realScaleNode)
Hope it helps...
P.S: This may be useful for helping you get the PPI of the Screen (marchv/UIScreenExtension)

SceneKit - Placing SCNCylinder between 2 SCNNodes

I have 2 SCNSphere and I want to "link" them with a SCNCylinder (something like this O<->O) but I cannot achieve it I tried using SCNNode pivot property applying rotations, the lookAt(_:) but none of them allow me to achieve what I want.
As a constraint, I want the cylinder to be child of the first sphere for architecture reasons.
Let's say we have a trivial case, first and second sphere have a radius of 0.1 and no rotation is applied to them, only the position is relevant :
static func createSphere(position: SCNVector3, color: UIColor = .red) -> SCNNode {
let shape = SCNSphere(radius: 0.1)
shape.firstMaterial?.diffuse.contents = color
let node = SCNNode(geometry: shape)
node.position = position
return node
}
And to create the "link node" :
static func createCylinder(position: SCNVector3, color: UIColor = .green) -> SCNNode {
let shape = SCNCylinder(radius: 0.2, height: 1.0)
shape.firstMaterial?.diffuse.contents = color
let node = SCNNode(geometry: shape)
node.position = position
return node
}
All being used like this :
/// Create a sphere and the related link if there is a next sphere
static func createPOI(position: SCNVector3, nextPosition: SCNVector3? = nil) -> SCNNode {
var node: SCNNode = createSphere(position: position, color: .black)
if let nextPosition = nextPosition {
let lineNode = createLine(position: SCNVector3.zero, targetPosition: nextPosition)
node.addChildNode(lineNode)
}
return node
}
static func createLine(position: SCNVector3, targetPosition: SCNVector3) -> SCNNode {
let node = createCylinder(position: position)
//step 1: handle rotation
node.position = position
//node.look(at: targetPosition)
node.eulerAngles.x = Float.pi/2
node.eulerAngles.y = atan2f(targetPosition.y, targetPosition.magnitude)
//step 2: handle scaling and positioning
node.pivot = SCNMatrix4MakeTranslation(-0.5, 0, 0)
let width = CGFloat((targetPosition - position).magnitude)
(node.geometry as? SCNCylinder)?.height = width
//TODO: Cylinder should link source and target nodes
//Tried a lot of things above but didn't manage to do it
return node
}

Swift: ARKit place a rectangle based on 2 nodes

My idea is that i want to place 2 sphere nodes at selected locations. From that point i basically want to draw a rectangle that will adjust the height with a slider. Basically that means that the 2 spheres will represent all 4 corners in the beginning. But when testing the code i use 1 meter height as a test.
The problem i have is that i cant seem to place the rectangle at the correct location as illustrated in the image below:
the rectangle in the image has a higher y-point than the points, it's rotated slightly and its center point is above node 2 and not in between the nodes. I don't want to use node.rotation as it wont work dynamically.
This is the code i use to place the 2 nodes and draw + add the rectangle.
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let location = touches.first?.location(in: sceneView) else {return}
let hitTest = sceneView.hitTest(location, types: [ARHitTestResult.ResultType.featurePoint])
guard let result = hitTest.last else {return}
// Converts the matrix_float4x4 to an SCNMatrix4 to be used with SceneKit
let transform = SCNMatrix4.init(result.worldTransform)
// Creates an SCNVector3 with certain indexes in the matrix
let vector = SCNVector3Make(transform.m41, transform.m42, transform.m43)
let node = addSphere(withPosition: vector)
nodeArray.append(node)
sceneView.scene.rootNode.addChildNode(node)
if nodeArray.count == 2 {
let node1 = sceneView.scene.rootNode.childNodes[0]
let node2 = sceneView.scene.rootNode.childNodes[1]
let bezeierPath = UIBezierPath()
bezeierPath.lineWidth = 0.01
bezeierPath.move(to: CGPoint(x: CGFloat(node1.position.x), y: CGFloat(node1.position.y)))
bezeierPath.addLine(to: CGPoint(x: CGFloat(node2.position.x), y: CGFloat(node2.position.y)))
bezeierPath.addLine(to: CGPoint(x: CGFloat(node2.position.x), y: CGFloat(node2.position.y+1.0)))
bezeierPath.addLine(to: CGPoint(x: CGFloat(node1.position.x), y: CGFloat(node1.position.y+1.0)))
bezeierPath.close()
bezeierPath.fill()
let shape = SCNShape(path: bezeierPath, extrusionDepth: 0.02)
shape.firstMaterial?.diffuse.contents = UIColor.red.withAlphaComponent(0.8)
let node = SCNNode.init(geometry: shape)
node.position = SCNVector3(CGFloat(abs(node1.position.x-node2.position.x)/2), CGFloat(abs((node1.position.y)-(node2.position.y))/2), CGFloat(node1.position.z))
sceneView.scene.rootNode.addChildNode(node)
}
}
Also note that this is not my final code. It will be refactored once i get everything working :).
This is currently working. I used the wrong calculation to set it in the middle of the 2 points.
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
// Check if a window is placed already and fetch location in sceneView
guard debugMode, let location = touches.first?.location(in: sceneView) else {return}
// Fetch targets at the current location
let hitTest = sceneView.hitTest(location, types: [ARHitTestResult.ResultType.featurePoint])
guard let result = hitTest.last else {return}
// Create sphere are position. getPosition() is an extension
createSphere(withPosition: result.getPosition())
// When 2 nodes has been placed, create a window and hide the spheres
if nodeArray.count == 2 {
let firstNode = nodeArray[nodeArray.count-1]
let secondNode = nodeArray[nodeArray.count-2]
firstNode.isHidden = true
secondNode.isHidden = true
createWindow(firstNode: firstNode, secondNode: secondNode)
sceneView.debugOptions = []
}
}
func createSphere(withPosition position: SCNVector3) {
// Create an geometry which you will apply materials to and convert to a node
let object = SCNSphere(radius: 0.01)
let material = SCNMaterial()
material.diffuse.contents = UIColor.red
object.materials = [material]
let node = SCNNode(geometry: object)
node.position = position
nodeArray.append(node)
sceneView.scene.rootNode.addChildNode(node)
}
func createWindow(firstNode: SCNNode, secondNode: SCNNode) {
// Create an geometry which you will apply materials to and convert to a node
let shape = SCNBox(width: CGFloat(abs(firstNode.worldPosition.x-secondNode.worldPosition.x)), height: 0.01, length: 0.02, chamferRadius: 0)
let material = SCNMaterial()
material.diffuse.contents = UIColor.red.withAlphaComponent(0.8)
// Each side of a cube can have different materials
// https://stackoverflow.com/questions/27509092/scnbox-different-colour-or-texture-on-each-face
shape.materials = [material]
let node = SCNNode(geometry: shape)
let minX = min(firstNode.worldPosition.x, secondNode.worldPosition.x)
let maxX = max(firstNode.worldPosition.x, secondNode.worldPosition.x)
// The nodes pivot Y-axis should be split in half of the nodes height, this will cause the node to
// only scale in one direction, when scaling it
// https://stackoverflow.com/questions/42568420/scenekit-understanding-the-pivot-property-of-scnnode/42613002#42613002
node.position = SCNVector3(((maxX-minX)/2)+minX, firstNode.worldPosition.y, firstNode.worldPosition.z)
node.pivot = SCNMatrix4MakeTranslation(0, 0.005, 0)
node.scale = SCNVector3Make(1, -50, 1)
node.name = "window"
sceneView.scene.rootNode.addChildNode(node)
}
Also added:
#objc func resetSceneView() {
// Clear all the nodes
sceneView.scene.rootNode.enumerateHierarchy { (node, stop) in
node.childNodes.forEach({
$0.removeFromParentNode()
})
node.removeFromParentNode()
}
// Reset the session
sceneView.session.pause()
sceneView.debugOptions = [ARSCNDebugOptions.showFeaturePoints]
sceneView.session.run(configuration, options: .resetTracking)
let matrix = sceneView.session.currentFrame!.camera.transform
sceneView.session.setWorldOrigin(relativeTransform: matrix)
nodeArray = []
}
To make sure that the world alignment is reset to your camera position