© Wallace Wang 2018
Wallace WangBeginning ARKit for iPhone and iPadhttps://doi.org/10.1007/978-1-4842-4102-8_6

6. Positioning Objects

Wallace Wang1 
(1)
San Diego, CA, USA
 

Virtual objects appear in an augmented reality view when you specify their x, y, and z coordinates. Such virtual objects remain fixed in place unless the user resets the world origin so they appear based on the new world origin’s location.

However, sometimes you may not want to specify exact coordinates to display a virtual object. Instead, you might want to place one virtual object a specific distance based on the current position of another virtual object. Rather than define exact coordinates, you really want to define relative coordinates such as always placing a second virtual object a fixed distance to the left of another virtual object.

Often times you’ll need to combine multiple virtual objects to create a single image, such as a box with a pyramid on top to create a house image. When working with multiple virtual objects that create a single object, relative positioning makes it easy to display all virtual objects correctly so they create a unified visual appearance.

Defining Relative Positions

Normally when you place a virtual object in an augmented reality view, you need to define two items. First, you need to define the virtual object’s x, y, and z coordinates like this:
node.position = SCNVector3(0, 0, -0.3)
This code places a virtual object 0 distance along the x-axis, 0 distance along the y-axis, and -0.3 meters along the z-axis. Once you define the x, y, and z coordinates of a virtual object, the second step is to place that virtual object based on the position of the rootnode like this:
sceneView.scene.rootNode.addChildNode(node)

The rootnode appears at the world origin (0, 0, 0) so the addChildNode command simply adds a node based on its position of the rootnode.

To learn how to place two virtual objects in an augmented reality view, let’s start by creating a new Xcode project by following these steps:
  1. 1.

    Start Xcode. (Make sure you’re using Xcode 10 or greater.)

     
  2. 2.

    Choose File ➤ New ➤ Project. Xcode asks you to choose a template.

     
  3. 3.

    Click the iOS category.

     
  4. 4.

    Click the Single View App icon and click the Next button. Xcode asks for a product name, organization name, organization identifiers, and content technology.

     
  5. 5.

    Click in the Product Name text field and type a descriptive name for your project such as Positioning. (The exact name does not matter.)

     
  6. 6.

    Click the Next button. Xcode asks where you want to store your project.

     
  7. 7.

    Choose a folder and click the Create button. Xcode creates an iOS project.

     
Now modify the Info.plist to allow access to the camera and to use ARKit by following these steps:
  1. 1.

    Click the Info.plist file in the Navigator pane. Xcode displays a list of keys, types, and values.

     
  2. 2.

    Click the disclosure triangle to expand the Required Device Capabilities category to display Item 0.

     
  3. 3.

    Move the mouse pointer over Item 0 to display a plus (+) icon.

     
  4. 4.

    Click this plus (+) icon to display a blank Item 1.

     
  5. 5.

    Type arkit under the Value category in the Item 1 row.

     
  6. 6.

    Move the mouse pointer over the last row to display a plus (+) icon.

     
  7. 7.

    Click on the plus (+) icon to create a new row. A popup menu appears.

     
  8. 8.

    Choose Privacy – Camera Usage Description.

     
  9. 9.

    Type AR needs to use the camera under the Value category in the Privacy – Camera Usage Description row.

     
Now it’s time to modify the ViewController.swift file to use ARKit and SceneKit by following these steps:
  1. 1.

    Click on the ViewController.swift file in the Navigator pane.

     
  2. 2.
    Edit the ViewController.swift file so it looks like this:
    import UIKit
    import SceneKit
    import ARKit
    class ViewController: UIViewController, ARSCNViewDelegate {
        let configuration = ARWorldTrackingConfiguration()
        override func viewDidLoad() {
            super.viewDidLoad()
            // Do any additional setup after loading the view, typically from a nib.
        }
    }
     
To view augmented reality in our app, add a single ARKit SceneKit View (ARSCNView) for displaying augmented reality with the camera view, as shown in Figure 6-1. The exact size of the ARSCNView doesn’t matter.
../images/469983_1_En_6_Chapter/469983_1_En_6_Fig1_HTML.jpg
Figure 6-1

The user interface just needs a single ARSCNView

After you’ve added a single ARKit SceneView to the user interface, you need to put constraints on those user interface items. To add constraints, choose Editor ➤ Resolve Auto Layout Issues ➤ Reset to Suggested Constraints at the bottom half of the menu under the All Views in Container category.

After designing the user interface, the next step is to connect the user interface items to the Swift code in the ViewController.swift file. To do this, follow these steps:
  1. 1.

    Click the Main.storyboard file in the Navigator pane.

     
  2. 2.

    Click the Assistant Editor icon or choose View ➤ Assistant Editor ➤ Show Assistant Editor to display the Main.storyboard and the ViewController.swift file side by side.

     
  3. 3.

    Move the mouse pointer over the ARSCNView, hold down the Control key, and Ctrl-drag under the class ViewController line.

     
  4. 4.

    Release the Control key and the left mouse button. A popup menu appears.

     
  5. 5.
    Click in the Name text field and type sceneView, then click the Connect button. Xcode creates an IBOutlet, as shown here:
    @IBOutlet var sceneView: ARSCNView!
     
  6. 6.
    Edit the viewDidLoad function so it looks like this:
        override func viewDidLoad() {
            super.viewDidLoad()
            // Do any additional setup after loading the view, typically from a nib.
            sceneView.delegate = self
            sceneView.showsStatistics = true
            sceneView.debugOptions = [ARSCNDebugOptions.showWorldOrigin, ARSCNDebugOptions.showFeaturePoints]
            showShape()
        }
     
  7. 7.
    Edit the viewWillAppear function so it looks like this:
        override func viewWillAppear(_ animated: Bool) {
            super.viewWillAppear(animated)
            sceneView.session.run(configuration)
        }

    This viewDidLoad function calls on a showShape function. The showShape function displays a sphere.

     
  8. 8.
    Type the following underneath the viewWillAppear function:
        func showShape() {
            let sphere = SCNSphere(radius: 0.05)
            sphere.firstMaterial?.diffuse.contents = UIColor.orange
            let node = SCNNode()
            node.geometry = sphere
            node.position = SCNVector3(0.2, 0.1, -0.1)
            sceneView.scene.rootNode.addChildNode(node)
        }
     
The entire ViewController.swift file should look like this:
import UIKit
import SceneKit
import ARKit
class ViewController: UIViewController, ARSCNViewDelegate  {
    @IBOutlet var sceneView: ARSCNView!
    let configuration = ARWorldTrackingConfiguration()
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
        sceneView.delegate = self
        sceneView.showsStatistics = true
        sceneView.debugOptions = [ARSCNDebugOptions.showWorldOrigin, ARSCNDebugOptions.showFeaturePoints]
        showShape()
    }
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        sceneView.session.run(configuration)
    }
    func showShape() {
        let sphere = SCNSphere(radius: 0.05)
        sphere.firstMaterial?.diffuse.contents = UIColor.orange
        let node = SCNNode()
        node.geometry = sphere
        node.position = SCNVector3(0.2, 0.1, -0.1)
        sceneView.scene.rootNode.addChildNode(node)
    }
}
This app places an orange sphere at (0.2, 0.1, -0.1). To run this app, follow these steps:
  1. 1.

    Connect an iOS device to your Macintosh through its USB cable.

     
  2. 2.

    Click the Run button or choose Product ➤ Run.

     
  3. 3.

    When the app runs, an orange sphere appears.

     
  4. 4.

    Click the Stop button or choose Product ➤ Stop.

     

Now let’s add a second virtual object, such as a green box, except we want it to appear 0.4 meters to the left (the x-axis), 0.3 meters lower (the y-axis), and 0.2 meters in front (the z-axis). To do this, we have to use math.

To make a virtual object appear 0.4 meters to the left of the first virtual object, we need to use a value of -.0.2 (0.2 – 0.4). To make it appear 0.3 meters lower, we need to use a value of -0.2 (0.1 – 0.3), and to make it appear 0.2 meters in front, we need to use a value of 0.1 (-0.1 + 0.2).

Edit the showShape function so it looks like this:
func showShape() {
    let sphere = SCNSphere(radius: 0.05)
    sphere.firstMaterial?.diffuse.contents = UIColor.orange
    let box = SCNBox(width: 0.2, height: 0.2, length: 0.2, chamferRadius: 0.0)
    box.firstMaterial?.diffuse.contents = UIColor.green
    let boxNode = SCNNode()
    boxNode.geometry = box
    boxNode.position = SCNVector3(-0.2, -0.2, 0.1) //0.4 meters to the left (the x-axis), 0.3 meters lower (the y-axis), and 0.2 meters in front (the z-axis)
    sceneView.scene.rootNode.addChildNode(boxNode)
    let node = SCNNode()
    node.geometry = sphere
    node.position = SCNVector3(0.2, 0.1, -0.1)
    sceneView.scene.rootNode.addChildNode(node)
}

If you run this modified showShape function , your app should show an orange sphere and a green box. The huge problem with this method of positioning a second virtual object based on the position of another virtual object is that you must calculate the distances between the two virtual objects manually, which makes this method error-prone.

Rather than calculate distances manually and hope you did it right, a far better solution is to simply define the distance you want one virtual object to appear away from another one.

Remember, when you define a position for a node (virtual object), using the SCNVector3 command , you’re just defining the virtual object’s position based on the rootnode’s position, which is the world origin (0, 0, 0) like this:
node.position = SCNVector3(0.2, 0.1, -0.1)
sceneView.scene.rootNode.addChildNode(node)

Rather than place a virtual object (node) in relation to the rootnode (world origin), you can just place a virtual object in relation to another virtual object. Doing so avoids the hassle of doing the math to determine x, y, and z coordinates solely based on the world origin.

So if we want to place the box 0.4 meters to the left, 0.3 meters lower, and 0.2 meters in front of a virtual object, we can simply use those numbers like this:
boxNode.position = SCNVector3(-0.4, -0.3, 0.2)
After defining the position of this virtual object, we can then place it relative to another virtual object. Instead of adding the node to the rootnode, we can add it to the other virtual object like this:
node.addChildNode(boxNode)
Modify the showShape function like this:
    func showShape() {
        let sphere = SCNSphere(radius: 0.05)
        sphere.firstMaterial?.diffuse.contents = UIColor.orange
        let box = SCNBox(width: 0.2, height: 0.2, length: 0.2, chamferRadius: 0.0)
        box.firstMaterial?.diffuse.contents = UIColor.green
        let boxNode = SCNNode()
        boxNode.geometry = box
        boxNode.position = SCNVector3(-0.4, -0.3, 0.2)
        let node = SCNNode()
        node.geometry = sphere
        node.position = SCNVector3(0.2, 0.1, -0.1)
        sceneView.scene.rootNode.addChildNode(node)
        node.addChildNode(boxNode)
    }

This creates the exact same result of drawing a green box a specific distance from an orange sphere. The big difference is that instead of adding the green box to the rootnode and calculating its distance from the orange sphere, we just added the green box to the orange sphere and defined the distance along the x, y, and z axis to place the green box.

Relative positioning makes it easy to place virtual objects a fixed distance from another virtual object. Initially, you’ll need to place one virtual object relative to the rootnode (the world origin of 0, 0, 0), but after that, you can place virtual objects relative to the location of other virtual objects.

Combining Geometric Shapes

Once you know how to position virtual objects relative to one another, you can easily combine multiple geometric shapes to create interesting objects that you could never create using a single geometric shape. For example, you could create a pyramid on top of a box to create a house object, multiple cylinders with a sphere to create a stick figure, or a plane with a torus to create a basketball hoop.

To see how to use relative positioning to combine multiple geometric shapes together, let’s create a snowman. The snowman will consist of three spheres stacked on top of each other with a hat on top, which we’ll create with two cylinders.

First, we’ll need to create three spheres stacked on top of each other. Let’s start with one large sphere that will be positioned relative to the rootnode (the world origin at 0, 0, 0). Then we’ll add progressively smaller spheres on top using relative positioning.

The first sphere gets positioned based on the rootnode 0.05 meters on the x-axis, 0.05 on the y-axis, and -0.05 on the z-axis like this:
let sphere = SCNSphere(radius: 0.04)
sphere.firstMaterial?.diffuse.contents = UIColor.red
let node = SCNNode()
node.geometry = sphere
node.position = SCNVector3(0.05, 0.05, -0.05)
sceneView.scene.rootNode.addChildNode(node)

This creates a red sphere positioned based on the rootnode, which appears at the world origin (0, 0, 0). Now let’s create a second sphere in blue that appears on top of the red sphere. Instead of positioning this blue sphere in the augmented reality view by the rootnode (world origin), we’ll use relative positioning and place it based on the location of the red sphere.

When placing virtual objects relative to another virtual object, you need to use trial and error to define specific distances until the virtual objects appear the way you want in relation to one another.

To place this second, blue sphere on top of the current red sphere, we can use the following code:
let middleSphere = SCNSphere(radius: 0.03)
middleSphere.firstMaterial?.diffuse.contents = UIColor.blue
let middleNode = SCNNode()
middleNode.geometry = middleSphere
middleNode.position = SCNVector3(0, 0.06, 0)
node.addChildNode(middleNode)

This code creates a sphere with a radius of 0.03 meters and colors it blue. Then it positions it at 0, 0.06, 0 based on the relative position of the red sphere. That means it appears 0.06 meters above the center of the red sphere.

Next, we need to create the third and top sphere. This top sphere has a radius of 0.02 meters and is colored white. We’ll place this sphere on top of the middle sphere 0.04 meters above the middle sphere’s center like this:
let topSphere = SCNSphere(radius: 0.02)
topSphere.firstMaterial?.diffuse.contents = UIColor.white
let topNode = SCNNode()
topNode.geometry = topSphere
topNode.position = SCNVector3(0, 0.04, 0)
middleNode.addChildNode(topNode)
Finally, let’s add a black hat to the three spheres that make up a virtual snowman. The hat will consist of two cylinders. A wide, flattened cylinder will create the hat brim and a narrower and taller cylinder will make up the rest of the hat. The hat rim needs to be positioned relative to the top sphere like this:
let hatRim = SCNCylinder(radius: 0.03, height: 0.002)
hatRim.firstMaterial?.diffuse.contents = UIColor.black
let rimNode = SCNNode()
rimNode.geometry = hatRim
rimNode.position = SCNVector3(0, 0.016, 0)
topNode.addChildNode(rimNode)

The radius of the hat rim is 0.03 meters and its height is just 0.002 meters tall. This code colors the cylinder black and places it 0.016 meters above the center of the top sphere.

Finally, we need to complete the hat with a second black cylinder. This cylinder needs to be 0.01 meters above the hat rim with a smaller radius of 0.015 and a taller height of 0.025 meters like this:
let hatTop = SCNCylinder(radius: 0.015, height: 0.025)
hatTop.firstMaterial?.diffuse.contents = UIColor.black
let hatNode = SCNNode()
hatNode.geometry = hatTop
hatNode.position = SCNVector3(0, 0.01, 0)
rimNode.addChildNode(hatNode)
To see how to create a virtual snowman out of three spheres and two cylinders, follow these steps:
  1. 1.

    Click the ViewController.swift file in the Navigator pane.

     
  2. 2.
    Edit the showShape function so it looks like this:
    func showShape() {
        let sphere = SCNSphere(radius: 0.04)
        sphere.firstMaterial?.diffuse.contents = UIColor.red
        let node = SCNNode()
        node.geometry = sphere
        node.position = SCNVector3(0.05, 0.05, -0.05)
        sceneView.scene.rootNode.addChildNode(node)
        let middleSphere = SCNSphere(radius: 0.03)
        middleSphere.firstMaterial?.diffuse.contents = UIColor.blue
        let middleNode = SCNNode()
        middleNode.geometry = middleSphere
        middleNode.position = SCNVector3(0, 0.06, 0)
        node.addChildNode(middleNode)
        let topSphere = SCNSphere(radius: 0.02)
        topSphere.firstMaterial?.diffuse.contents = UIColor.white
        let topNode = SCNNode()
        topNode.geometry = topSphere
        topNode.position = SCNVector3(0, 0.04, 0)
        middleNode.addChildNode(topNode)
        let hatRim = SCNCylinder(radius: 0.03, height: 0.002)
        hatRim.firstMaterial?.diffuse.contents = UIColor.black
        let rimNode = SCNNode()
        rimNode.geometry = hatRim
        rimNode.position = SCNVector3(0, 0.016, 0)
        topNode.addChildNode(rimNode)
        let hatTop = SCNCylinder(radius: 0.015, height: 0.025)
        hatTop.firstMaterial?.diffuse.contents = UIColor.black
        let hatNode = SCNNode()
        hatNode.geometry = hatTop
        hatNode.position = SCNVector3(0, 0.01, 0)
        rimNode.addChildNode(hatNode)
    }
     
  3. 3.

    Connect an iOS device to your Macintosh through its USB cable.

     
  4. 4.

    Click the Run button or choose Product ➤ Run. Notice that the virtual snowman appears in the augmented reality view, as shown in Figure 6-2.

     
../images/469983_1_En_6_Chapter/469983_1_En_6_Fig2_HTML.jpg
Figure 6-2

Three spheres and two cylinders create a virtual snowman

  1. 5.

    Click the Stop button or choose Product ➤ Stop

     
The entire ViewController.swift file should look like this:
import UIKit
import SceneKit
import ARKit
class ViewController: UIViewController, ARSCNViewDelegate  {
    @IBOutlet var sceneView: ARSCNView!
    let configuration = ARWorldTrackingConfiguration()
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
        sceneView.delegate = self
        sceneView.showsStatistics = true
        sceneView.debugOptions = [ARSCNDebugOptions.showWorldOrigin, ARSCNDebugOptions.showFeaturePoints]
        showShape()
    }
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        sceneView.session.run(configuration)
    }
    func showShape() {
        let sphere = SCNSphere(radius: 0.04)
        sphere.firstMaterial?.diffuse.contents = UIColor.red
        let node = SCNNode()
        node.geometry = sphere
        node.position = SCNVector3(0.05, 0.05, -0.05)
        sceneView.scene.rootNode.addChildNode(node)
        let middleSphere = SCNSphere(radius: 0.03)
        middleSphere.firstMaterial?.diffuse.contents = UIColor.blue
        let middleNode = SCNNode()
        middleNode.geometry = middleSphere
        middleNode.position = SCNVector3(0, 0.06, 0)
        node.addChildNode(middleNode)
        let topSphere = SCNSphere(radius: 0.02)
        topSphere.firstMaterial?.diffuse.contents = UIColor.white
        let topNode = SCNNode()
        topNode.geometry = topSphere
        topNode.position = SCNVector3(0, 0.04, 0)
        middleNode.addChildNode(topNode)
        let hatRim = SCNCylinder(radius: 0.03, height: 0.002)
        hatRim.firstMaterial?.diffuse.contents = UIColor.black
        let rimNode = SCNNode()
        rimNode.geometry = hatRim
        rimNode.position = SCNVector3(0, 0.016, 0)
        topNode.addChildNode(rimNode)
        let hatTop = SCNCylinder(radius: 0.015, height: 0.025)
        hatTop.firstMaterial?.diffuse.contents = UIColor.black
        let hatNode = SCNNode()
        hatNode.geometry = hatTop
        hatNode.position = SCNVector3(0, 0.01, 0)
        rimNode.addChildNode(hatNode)
    }
}

Summary

When placing virtual objects in an augmented reality view, each virtual object must be assigned to a node and that node needs x, y, and z coordinates to define its location. At least one virtual object needs to be located based on its distance from the rootnode, which represents the world origin (0, 0, 0). After placing at least one virtual object based on the rootnode, you can place additional virtual objects based on either the rootnode or any existing virtual objects.

Placing virtual objects based on the rootnode lets you define their location independent of any other virtual objects. However, if you want one virtual object to appear a fixed distance from a second virtual object, it’s easier to use relative positioning. Instead of defining a virtual object based on its location to the rootnode, you define it based on its location to another virtual object.

Without relative positioning, you would have to calculate distances based on the rootnode, which can be inaccurate and cumbersome to do. Relative positioning makes it easy to define virtual objects a fixed distance from one another.