Chapters

Hide chapters

Apple Augmented Reality by Tutorials

Second Edition · iOS 15 · Swift 5.4 · Xcode 13

Section I: Reality Composer

Section 1: 5 chapters
Show chapters Hide chapters

16. Focus Nodes & Billboards
Written by Chris Language

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

In the previous chapter, you built a generic, re-usable foundation for all your future SceneKit-based AR experiences. The app operates in a few basic states and, as an added bonus, it also conforms to a standard onboarding process thanks to Apple’s AR Coaching Overlay view. This will make your users feel right at home when they pick up your app and play with the AR experiences you create.

In this chapter, you’ll continue to add more components to the reusable foundation. You’ll learn how to create and manage a focus node that helps the user know where content will be placed. You’ll also get to build the entire AR experience with some basic interaction.

Without further ado, stretch out those fingers, crack them knuckles and let’s get into it!

Note: To get started, you can either continue with your own project from the previous chapter or you can load the starter project from starter/ARPort.

Importing 3D Assets

In the previous chapter, you learned about the SceneKit Asset Catalog, which is a folder that your entire team of artists and developers can share. It keeps the graphics component of your app completely separate from the code. This allows you and your team to merge any graphical changes and additions into the app with minimal disruption.

Your first step to get started is to add the ready-made asset catalog to the project.

With your project open in Xcode on one side and Finder open on the other side, find art.scnassets inside starter/resources.

Drag and drop the art.scnassets folder into Xcode, placing it just above Assets.xcassets.

Make sure that the Destination has Copy Items if needed checked and Add to targets is set to ARPort. Select Finish to complete the process.

Excellent, you’ve successfully imported all of the 3D assets you’ll need to complete the AR experience.

Focus Nodes

The app already detects horizontal surfaces, so your goal now is to show the user exactly where on that surface they’re pointing. This is where a focus node comes in handy.

What is a Focus Node?

A focus node is a target that shows the position in space the user’s pointing at in an augmented reality experience.

Creating a Focus Point

Before you can create a focus point, you need to define the onscreen position to use for the ray cast. For this particular app, you’ll use the center point of the screen.

var focusPoint:CGPoint!
func initFocusNode() {
  focusPoint = CGPoint(x: view.center.x, 
    y: view.center.y + view.center.y * 0.1)
}

Handling Orientation Changes

To solve the problem, you need to update the focus point every time the user changes screen orientation.

@objc
func orientationChanged() {
  focusPoint = CGPoint(x: view.center.x,
    y: view.center.y  + view.center.y * 0.1)
}
NotificationCenter.default.addObserver(self,
  selector: #selector(ViewController.orientationChanged),
  name: UIDevice.orientationDidChangeNotification,
  object: nil)

Creating a Focus Node

To create a new focus node, you first need to create a new SceneKit scene.

Adding Billboard Constraints

It would look really cool if the focus node always faced the user. To achieve that effect, you can use a billboard constraint. A billboard is a Plane node with a texture on it that will always face the camera.

Loading the Focus Node

Now that the Focus scene is ready to go, you need to load it and add it to the main scene.

var focusNode: SCNNode!
// 1
let focusScene = SCNScene(
  named: "art.scnassets/Scenes/FocusScene.scn")!
// 2
focusNode = focusScene.rootNode.childNode(
  withName: "Focus", recursively: false)!
// 3
focusNode.isHidden = true
sceneView.scene.rootNode.addChildNode(focusNode)
self.initFocusNode()

Updating the Focus Node

Now that the focus node is ready to go, you need some code to manage the node’s visibility.

func updateFocusNode() {
  // 1   
  guard appState != .Started else { 
    focusNode.isHidden = true
    return
  }
  // 2
  if let query = self.sceneView.raycastQuery(
    from: self.focusPoint,
    allowing: .estimatedPlane,
    alignment: .horizontal) {
    // 3
    let results = self.sceneView.session.raycast(query)
    if results.count == 1 {
      if let match = results.first {
        // 4
        let t = match.worldTransform
        // 5
        self.focusNode.position = SCNVector3(
          x: t.columns.3.x, y: t.columns.3.y, z: t.columns.3.z)
        self.appState = .TapToStart
        focusNode.isHidden = false
      }
    } else {
      // 6
      self.appState = .PointAtSurface
      focusNode.isHidden = true
    }
  }
}
self.updateFocusNode()

Creating the Scene

Now that you know where you want to place your virtual content, it’s time to create some cool content to actually place. :]

Building the Scene

Create a new blank scene named ARPortScene.scn by right-clicking on the art.scnassets/Scenes folder and selecting New File. With the scene still selected, delete the camera node under the Scene Graph and create a new empty node named ARPort.

Adding Lights & Shadows

Create a new empty node as a child of ARPort and name it Lights & Shadows. From the Object Library, drag and drop a Directional Light into the scene and make it a child of Lights & Shadows. Rename it to DirectionalLight, too.

Adding a Shadow Catcher

To push the realism factor of your AR experience a bit, it would look amazing if the tall control tower would drop a shadow on top of the ground surface below it.

Loading the Scene

With the scene built, you now need to do two things: First, load the scene and then, when the user taps to start the AR experience, place the ARPort at the focus node’s location.

var arPortNode: SCNNode!
// 1
let arPortScene = SCNScene(
  named: "art.scnassets/Scenes/ARPortScene.scn")!
// 2
arPortNode = arPortScene.rootNode.childNode(
  withName: "ARPort", recursively: false)!
// 3
arPortNode.isHidden = true
sceneView.scene.rootNode.addChildNode(arPortNode)

Presenting the Scene

With the ARPort node ready and waiting to display, there’s one thing left to do: Display the node when the user taps the screen.

// 1
guard appState == .TapToStart else { return }
// 2
self.arPortNode.isHidden = false
self.focusNode.isHidden = true
// 3
self.arPortNode.position = self.focusNode.position
// 4
appState = .Started

Adding Interaction

Your app is shaping up nicely, and you’re almost done. But first, you’ll make it a little more useful by giving the user some elements to interact with.

Adding the Billboards

To speed things up, there’s a ready-made scene for you to use.

Handling Touch Input

In ViewController.swift, add the following code under the Scene Management section:

override func touchesBegan(_ touches: Set<UITouch>, 
  with event: UIEvent?) {
  DispatchQueue.main.async {
    // 1
    if let touchLocation = touches.first?.location(
      in: self.sceneView) {
      if let hit = self.sceneView.hitTest(touchLocation,
        options: nil).first {
        // 2
        if hit.node.name == "Touch" {
          // 3
          let billboardNode = hit.node.childNode(
            withName: "Billboard", recursively: false)
          billboardNode?.isHidden = false
        }
    // 4
        if hit.node.name == "Billboard" {
          hit.node.isHidden = true
        }
      }
    }
  }
}

Enabling Statistics & Debugging (Optional)

When dealing with problems, it’s extremely helpful to enable the scene statistics and debugging information.

// 1
sceneView.showsStatistics = true
// 2
sceneView.debugOptions = [
  ARSCNDebugOptions.showFeaturePoints,
  ARSCNDebugOptions.showCreases,
  ARSCNDebugOptions.showWorldOrigin,
  ARSCNDebugOptions.showBoundingBoxes,
  ARSCNDebugOptions.showWireframe]

Adding the Final Touches

You’re basically done, there are just a few tiny housecleaning issues that need to be done to ensure you handle every situation correctly.

self.arPortNode.isHidden = true
self.focusNode.isHidden = true
self.arPortNode.isHidden = true

Key Points

Congratulations, you’ve reached the end of this chapter and section, and you’ve created a super cool AR experience using SceneKit with ARKit.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now