Scaling SCNNode and its children - swift

I am making a simple measuring app. Currently I place spheres as SCNNodes around the place and between nodes appears a label that displays the length of the line from node 1 to node 2.
This is how the labels are created:
func addLabel() {
let plane = SCNPlane(width: 0.07, height: 0.02)
plane.cornerRadius = plane.height / 10
let sks = SKScene(size: CGSize(width: plane.width * 10e3, height: plane.height * 10e3))
sks.backgroundColor = UIColor(red: 0.1, green: 0.1, blue: 0.1, alpha: 0.7)
currentLbl = SKLabelNode(text: "")
currentLbl.fontSize = 110
currentLbl.fontName = "Helvetica"
currentLbl.verticalAlignmentMode = .center
currentLbl.position = CGPoint(x: sks.frame.midX, y: sks.frame.midY)
currentLbl.fontColor = .white
sks.addChild(currentLbl)
let material = SCNMaterial()
material.isDoubleSided = true
material.diffuse.contents = sks
material.diffuse.contentsTransform = SCNMatrix4Translate(SCNMatrix4MakeScale(1, -1, 1), 0, 1, 0)
plane.materials = [material]
let node = SCNNode(geometry: plane)
node.constraints = [SCNBillboardConstraint()]
node.position = SCNVector3Make(0, 0, 0)
let (minBound, maxBound) = node.boundingBox
node.pivot = SCNMatrix4MakeTranslation( (maxBound.x + minBound.x)/2, minBound.y, 0.02/2)
lblNodes.append(node)
currentLblNode = node
sceneView.scene.rootNode.addChildNode(node)
}
I would like to apply a mathematical equation to the scale of these label nodes (in my update function) to maintain readability from a couple of metres.
var myNodes: [SCNNode] = []
let s = getMagicScalingNumber()
Say I obtained my scale factor as above and I have an array of SCNNodes, how can I scale all the nodes and their respective children so they stay visually proportional.
If SCNTransformConstraint() is an option for this, I would appreciate an example of how to implement it.
Edit: Just to clarify, I have tried
currentLblNode.scale = SCNVector3Make(s, s, s)
which does not seem to work.

I know this is very late, however I have put together an example which should point you in the right direction.
In your example you are essentially creating a holder SCNNode which contains your labels etc.
You can store these into an array of [SCNNode] and then transform the scale of these like so:
/// Updates The Contents Of Each Node Added To Our NodesAdded Array
///
/// - Parameters:
/// - nodes: [SCNNode]
/// - pointOfView: SCNNode
func updateScaleFromCameraForNodes(_ nodes: [SCNNode], fromPointOfView pointOfView: SCNNode){
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)
//4. Animate Their Scaling & Set Their Scale Based On Their Distance From The Camera
SCNTransaction.begin()
SCNTransaction.animationDuration = 0.5
switch distanceBetweenNodeAndCamera {
case 0 ... 0.5:
node.simdScale = simd_float3(0.25, 0.25, 0.25)
case 0.5 ... 1:
node.simdScale = simd_float3(0.5, 0.5, 0.5)
case 1 ... 1.5:
node.simdScale = simd_float3(1, 1, 1)
case 1.5 ... 2:
node.simdScale = simd_float3(1.5, 1.5, 1.5)
case 2 ... 2.5:
node.simdScale = simd_float3(2, 2, 2)
case 2.5 ... 3:
node.simdScale = simd_float3(2.5, 2.5, 2.5)
default:
print("Default")
}
SCNTransaction.commit()
}
}
Here I am just setting 'random' values to illustrate the concept of scale depending on the distance from the camera.
To put this better into context here is a little demo I have put together:
//--------------------------
// MARK: - ARSCNViewDelegate
//--------------------------
extension ViewController: ARSCNViewDelegate{
func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) {
if !nodesAdded.isEmpty, let currentCameraPosition = self.sceneView.pointOfView {
updateScaleFromCameraForNodes(nodesAdded, fromPointOfView: currentCameraPosition)
}
}
}
class ViewController: UIViewController {
#IBOutlet var sceneView: ARSCNView!
var nodesAdded = [SCNNode]()
//-----------------------
// MARK: - View LifeCycle
//-----------------------
override func viewDidLoad() {
super.viewDidLoad()
//1. Generate Our Three Box Nodes
generateBoxNodes()
//2. Run The Session
let configuration = ARWorldTrackingConfiguration()
sceneView.session.run(configuration)
sceneView.delegate = self
}
/// Generates Three SCNNodes With An SCNBox Geometry & Places Them In A Holder Node
func generateBoxNodes(){
//1. Create An SCNNode To Hold All Of Our Content
let holderNode = SCNNode()
//2. Create An Array Of Colours For Each Face
let colours: [UIColor] = [.red, .green, .blue, .purple, .cyan, .black]
//3. Create An SCNNode Wih An SCNBox Geometry
let boxNode = SCNNode()
let boxGeometry = SCNBox(width: 0.1, height: 0.1, length: 0.1, chamferRadius: 0.01)
boxNode.geometry = boxGeometry
//4. Create A Different Material For Each Face
var materials = [SCNMaterial]()
for i in 0..<5{
let faceMaterial = SCNMaterial()
faceMaterial.diffuse.contents = colours[i]
materials.append(faceMaterial)
}
//5. Set The Geometries Materials
boxNode.geometry?.materials = materials
//6. Create Two More Nodes By Cloning The First One
let secondBox = boxNode.flattenedClone()
let thirdBox = boxNode.flattenedClone()
//7. Position Them In A Line & Add To The Scene
boxNode.position = SCNVector3(-0.2, 0, 0)
secondBox.position = SCNVector3(0, 0, 0)
thirdBox.position = SCNVector3(0.2, 0, 0)
holderNode.addChildNode(boxNode)
holderNode.addChildNode(secondBox)
holderNode.addChildNode(thirdBox)
holderNode.position = SCNVector3(0, 0, -1)
self.sceneView.scene.rootNode.addChildNode(holderNode)
nodesAdded.append(holderNode)
}
/// Updates The Contents Of Each Node Added To Our NodesAdded Array
///
/// - Parameters:
/// - nodes: [SCNNode]
/// - pointOfView: SCNNode
func updateScaleFromCameraForNodes(_ nodes: [SCNNode], fromPointOfView pointOfView: SCNNode){
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)
//4. Animate Their Scaling & Set Their Scale Based On Their Distance From The Camera
SCNTransaction.begin()
SCNTransaction.animationDuration = 0.5
switch distanceBetweenNodeAndCamera {
case 0 ... 0.5:
node.simdScale = simd_float3(0.25, 0.25, 0.25)
case 0.5 ... 1:
node.simdScale = simd_float3(0.5, 0.5, 0.5)
case 1 ... 1.5:
node.simdScale = simd_float3(1, 1, 1)
case 1.5 ... 2:
node.simdScale = simd_float3(1.5, 1.5, 1.5)
case 2 ... 2.5:
node.simdScale = simd_float3(2, 2, 2)
case 2.5 ... 3:
node.simdScale = simd_float3(2.5, 2.5, 2.5)
default:
print("Default")
}
SCNTransaction.commit()
}
}
override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) }
override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) }
override func didReceiveMemoryWarning() { super.didReceiveMemoryWarning() }
}
Hopefully it helps...

Related

How to change SCNView pivot point

Question
How does the SceneView/camera work out around which point to rotate, and how do I force the scene to rotate around the red sphere (-2, 0, 0)?
Demo
In this example SceneView I have placed
a camera at (-2, 0, 5)
a red sphere at (-2, 0, 0)
a blue sphere at (-1, 0, 0)
a white sphere at (0, 0, 0)
So the camera is in the same position along the x-axis as the red sphere
In the image you see the scene rotates around the blue sphere.
Playground example
Here is a working playground to produce the above example
import UIKit
import SceneKit
import PlaygroundSupport
var scene = SCNScene()
var sceneView: SCNView = {
let s = SCNView(
frame: CGRect(x: 0, y: 0, width: 600, height: 600)
)
s.scene = scene
s.backgroundColor = UIColor.black
s.allowsCameraControl = true
return s
}()
PlaygroundPage.current.liveView = sceneView
let redXPosition: simd_float1 = -2.0
let blueXPosition: simd_float1 = -1.0
let originXPosition: simd_float1 = 0.0
// MARK: Scene Nodes
let cameraNode: SCNNode = {
let n = SCNNode()
n.camera = SCNCamera()
n.camera?.contrast = 0.0
n.camera?.wantsHDR = false
return n
}()
cameraNode.simdPosition = simd_float3(redXPosition, 0, 5)
scene.rootNode.addChildNode(cameraNode)
let ambientLightNode: SCNNode = {
let n = SCNNode()
n.light = SCNLight()
n.light!.type = SCNLight.LightType.ambient
n.light!.color = UIColor(white: 0.75, alpha: 1.0)
return n
}()
ambientLightNode.simdPosition = simd_float3(0,5,0)
scene.rootNode.addChildNode(ambientLightNode)
// MARK: Spheres
// MARK: Origin - White
let originNode: SCNNode = {
let sphere = SCNSphere(radius: 0.5)
let node = SCNNode(geometry: sphere)
let mat = SCNMaterial()
mat.diffuse.contents = UIColor.white
sphere.materials = [mat]
return node
}()
// MARK: Red
let redNode: SCNNode = {
let sphere = SCNSphere(radius: 0.3)
let node = SCNNode(geometry: sphere)
let mat = SCNMaterial()
mat.diffuse.contents = UIColor.red
sphere.materials = [mat]
return node
}()
// MARK: Blue
let blueNode: SCNNode = {
let sphere = SCNSphere(radius: 0.3)
let node = SCNNode(geometry: sphere)
let mat = SCNMaterial()
mat.diffuse.contents = UIColor.blue
sphere.materials = [mat]
return node
}()
// MARK: Place nodes in scene
originNode.simdPosition = simd_float3(originXPosition,0,0)
redNode.simdPosition = simd_float3(redXPosition,0,0)
blueNode.simdPosition = simd_float3(blueXPosition,0,0)
scene.rootNode.addChildNode(originNode)
scene.rootNode.addChildNode(redNode)
scene.rootNode.addChildNode(blueNode)

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

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)

Room texture isn't fully rendered when I move SCNCamera around SCNBox inside the virtual room using SceneKit

I have tried to create a virtual room using SceneKit and put into it a box.
Also I have created a separate node with camera called ‘cameraOrbitNode’ (camera locates between the wall of room and the box). The main goal is to move ‘cameraOrbitNode’ around the box inside the room. I implemented it via pan gesture recognizer.
import UIKit
import SceneKit
enum WallType {
case left
case right
case top
case bottom
case forward
case back
}
class ViewController: UIViewController {
//MARK: - Outlets
#IBOutlet fileprivate weak var sceneView: SCNView!
//MARK: - Properties
fileprivate let roomLength: CGFloat = 12
fileprivate let cubeLength: CGFloat = 1
fileprivate var cameraOrbitNode: SCNNode?
fileprivate let panModifier: CGFloat = 100
//MARK: - Life cycle
override func viewDidLoad() {
super.viewDidLoad()
setupSceneView()
setupRoom()
setupCameraOrbitNode()
setupGestures()
}
func setupSceneView() {
sceneView.scene = SCNScene()
sceneView.autoenablesDefaultLighting = true
}
func setupRoom() {
let roomNode = SCNNode()
roomNode.position = SCNVector3(0, 0, 0)
for wallType in Array<WallType>([.left, .right, .top, .bottom, .forward, .back]) {
roomNode.addChildNode(getWallNodeWith(wallType: wallType))
}
roomNode.addChildNode(getCubeNode())
sceneView.scene?.rootNode.addChildNode(roomNode)
}
func getWallNodeWith(wallType: WallType) -> SCNNode {
let wallGeometrySize = getWallSize()
let wallGeometry = SCNPlane(width: wallGeometrySize.width, height: wallGeometrySize.height)
wallGeometry.firstMaterial?.diffuse.contents = UIColor.darkGray
let wallNode = SCNNode(geometry: wallGeometry)
wallNode.position = getWallPositionWith(wallType: wallType)
wallNode.eulerAngles = getWallEulerAnglesWith(wallType: wallType)
return wallNode
}
func getWallSize() -> CGSize {
return CGSize(width: roomLength, height: roomLength)
}
func getWallPositionWith(wallType: WallType) -> SCNVector3 {
switch wallType {
case .left:
return SCNVector3(-roomLength / 2, 0, 0)
case .right:
return SCNVector3(roomLength / 2, 0, 0)
case .forward:
return SCNVector3(0, 0, -roomLength / 2)
case .back:
return SCNVector3(0, 0, roomLength / 2)
case .top:
return SCNVector3(0, roomLength / 2, 0)
case .bottom:
return SCNVector3(0, -roomLength / 2, 0)
}
}
func getWallEulerAnglesWith(wallType: WallType) -> SCNVector3 {
switch wallType {
case .left, .right:
return SCNVector3(0, CGFloat.pi / 2, 0)
case .forward, .back:
return SCNVector3(0, 0, 0)
case .top, .bottom:
return SCNVector3(CGFloat.pi / 2, 0, 0)
}
}
func getCubeNode() -> SCNNode {
let cubeGeometry = SCNBox(width: cubeLength, height: cubeLength, length: cubeLength, chamferRadius: 0)
cubeGeometry.firstMaterial?.diffuse.contents = UIColor.blue
let cubeNode = SCNNode(geometry: cubeGeometry)
cubeNode.position = SCNVector3(0, 0, 0)
return cubeNode
}
func setupCameraOrbitNode() {
let camera = SCNCamera()
camera.zNear = 0
camera.zFar = Double(roomLength)
let cameraNode = SCNNode()
cameraNode.position = SCNVector3(0, 0, (roomLength + cubeLength) / 4)
cameraNode.camera = camera
cameraOrbitNode = SCNNode()
guard let cameraOrbitNode = cameraOrbitNode else { return }
cameraOrbitNode.addChildNode(cameraNode)
cameraOrbitNode.position = SCNVector3(0, 0, 0)
sceneView.scene?.rootNode.addChildNode(cameraOrbitNode)
}
#objc func actionPan(gesture: UIPanGestureRecognizer) {
let translation = gesture.velocity(in: gesture.view)
cameraOrbitNode?.eulerAngles.y -= Float(translation.x/CGFloat(panModifier)) * .pi / 180
cameraOrbitNode?.eulerAngles.x -= Float(translation.y/CGFloat(panModifier)) * .pi / 180
}
func setupGestures() {
sceneView.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(actionPan(gesture:))))
}
}
But in some cases you can see transparent walls of the room (basically room texture isn't fully rendered when I move the camera around the box). I was changing some camera properties (zNear, zFar) but nothing helped. Please see the links on images attached below. Thanks a lot for any help.
https://www.dropbox.com/s/ihxjrsgjfzidoff/IMG_0187.PNG?dl=0
https://www.dropbox.com/s/wd23xepcir3wggk/IMG_0188.PNG?dl=0
set isDoubleSdided to true
node.geometry?.firstMaterial?.isDoubleSided = true

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)

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