ARKit by Tutorials Table of Contents: Overview About This Book Sample ........................................................ 5 What You Need ....................................................................... 12 Book License ............................................................................ 13 Book Source Code & Forums ............................................. 14 About the Cover ..................................................................... 15 Chapter 7: Building a Portal ............................................... 16 Chapter 8: Adding Objects to Your World ................... 28 Chapter 9: Geometry, Textures & Lighting................... 44 Where to Go From Here? .................................................... 64 raywenderlich.com 2 ARKit by Tutorials Table of Contents: Extended About This Book Sample ........................................................ 5 About the Authors ............................................................................................................... 9 About the Editors............................................................................................................... 10 About the Artist.................................................................................................................. 11 What You Need ....................................................................... 12 Book License ............................................................................ 13 Book Source Code & Forums ............................................. 14 About the Cover ..................................................................... 15 Chapter 7: Building a Portal ............................................... 16 The portal app ..................................................................................................................... 16 Getting started ................................................................................................................... 17 Setting up ARKit ................................................................................................................. 18 Plane detection and rendering ..................................................................................... 21 Where to go from here? .................................................................................................. 27 Chapter 8: Adding Objects to Your World ................... 28 Getting started ................................................................................................................... 28 Hit testing ............................................................................................................................. 33 Adding crosshairs .............................................................................................................. 34 Adding a state machine ................................................................................................... 39 Where to go from here? .................................................................................................. 43 Chapter 9: Geometry, Textures & Lighting................... 44 Getting started ................................................................................................................... 44 Building the portal ............................................................................................................. 46 Adding the doorway ......................................................................................................... 58 Placing lights ........................................................................................................................ 61 Where to go from here? .................................................................................................. 63 raywenderlich.com 3 ARKit by Tutorials Where to Go From Here? .................................................... 64 raywenderlich.com 4 A About This Book Sample ARKit is Apple’s mobile AR development framework. With it, you can create an immersive, engaging experience, mixing virtual 2D and 3D content with the live camera feed of the world around you. You may be surprised to learn that, at this very moment, millions of Apple users already have a sophisticated AR device right in their pockets! We are pleased to offer you this sample from the full ARKit by Tutorials book. The three chapters that follow will take you through building a complete app: You'll implement a sci-fi portal right before your eyes and learn some important AR concepts along the way. The three chapters include: • Chapter 7: Building a Portal: In this chapter, you’ll explore an app to review the basics of ARKit development. You’ll set up an ARSession and add plane detection and other functions so that the app can render horizontal planes using the ARSCNViewDelegate protocol. • Chapter 8: Adding Objects to Your World: Over the course of building your portal, you’ll learn how to handle ARSession interruptions elegantly when your app goes to the background. You’ll then explore how hit-testing with an ARSCNView works as you start adding objects to the detected horizontal plane in the device’s surroundings. For adding virtual objects, you’ll use ARAnchors and SCNNode objects to define their position and geometry. • Chapter 9: Geometry, Textures & Lighting: In the final chapter of this section, you’ll first dive deeper to understand SceneKit’s coordinate system and materials. Next, you’ll use SCNNode objects with different geometries and attach textures to them to create the walls, floor and ceiling of your portal. Finally, you’ll make your portal appear realistic by adding lighting. raywenderlich.com 5 ARKit by Tutorials About This Book Sample We hope this hands-on look inside the book will give you a good idea of what's to come in the full version and show you why this book is a must-have for any app developer. In the full version of this book, you’ll create five immersive and engaging apps: a tabletop poker dice game, an immersive sci-fi portal, a 3D face-tracking mask app, a location-based AR ad network, and monster truck simulation with realistic vehicle physics. The full book is now available for purchase: • www.store.raywenderlich.com/products/arkit-by-tutorials. Enjoy! The ARKit by Tutorials team raywenderlich.com 6 ARKit by Tutorials About This Book Sample ARKit by Tutorials By Chris Language, Namrata Bandekar, Antonio Bello & Tammy Coron Copyright ©2018 Razeware LLC. Notice of Rights All rights reserved. No part of this book or corresponding materials (such as text, images, or source code) may be reproduced or distributed by any means without prior written permission of the copyright owner. Notice of Liability This book and all corresponding materials (such as source code) are provided on an “as is” basis, without warranty of any kind, express of implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and noninfringement. In no event shall the authors or copyright holders be liable for any claim, damages or other liability, whether in action of contract, tort or otherwise, arising from, out of or in connection with the software or the use of other dealing in the software. Trademarks All trademarks and registered trademarks appearing in this book are the property of their own respective owners. raywenderlich.com 7 ARKit by Tutorials About This Book Sample Dedications "To my wife Corné and my daughter Marizé. Thank you for your patience, support, belief, and most of all, your love. Everything I do, I do for you." — Chris Language "To my parents and my husband, for supporting me and to Ginger, my puppy, for all the smiles. :)" — Namrata Bandekar "For my evil cat, Jetsam." — Tammy Coron raywenderlich.com 8 ARKit by Tutorials About This Book Sample About the Authors Chris Language is a seasoned coder with 20+ years of experience, and the author of 3D Apple Games by Tutorials. He has fond memories of his childhood and his Commodore 64; more recently he started adding more good memories of life with all his Apple devices. By day, he fights for survival in the corporate jungle of Johannesburg, South Africa. By night he fights demons, dragons and zombies! For relaxation, he codes. You can find him on Twitter @ChrisLanguage Forever Coder, Artist, Musician, Gamer and Dreamer. Namrata Bandekar is an author of this book. Namrata is a Principal Software Engineer at Zynga building addictive games. She is also a member of the Ray Wenderlich Tutorial Team. She is the co-author of ARKit by Tutorials and one of the Tech Editors on the Android Apprentice book. Apart from building apps, she is passionate about travelling, scuba diving and hiking with her dog.Say hi to Namrata on Twitter: @NamrataCodes Antonio Bello is an author of this book. Antonio has spent most of his life writing code, and he’s gained a lot of experience in several languages and technologies. A few years ago he fell in love with iOS development, and that’s what he mostly works on since then, although he’s always open for challenges and for playing with new toys. He believes that reputation is the most important skill in his job, and that “it cannot be done” actually means “it can be done, but it’s not economically convenient." When he’s not working, he’s probably playing drums or making songs in his small, but well fitted, home recording studio. Tammy Coron is an independent creative professional and the host of Roundabout: Creative Chaos. She’s also the founder of Just Write Code. Find out more at tammycoron.com. raywenderlich.com 9 ARKit by Tutorials About This Book Sample About the Editors Jerry Beers is a tech editor of this book. Jerry is co-founder of Five Pack Creative, a mobile development company specializing in iOS development. He is passionate about creating well-crafted code and teaching others. You can find him on Twitter @jerrybeers or his company's site fivepackcreative.com. Joey deVilla is a tech editor on this book. Joey is a developer turned accordionist turned tech evangelist turned developer cat-herder. A Canadian turned Tampanian, he works at Sourcetoad, blogs at globalnerdy.com and joeydevilla.com, and does what he can to avoid becoming a “Florida Man” news story. Morten Faarkrog is a tech editor on this book. Morten is an app developer from Denmark who works at the digital agency Adapt. When not developing apps he enjoys reading, travelling, and running. To connect with Morten you can find him on LinkedIn. Matthew Morey is a tech editor on this book. Matt is an engineer, author, hacker, creator and tinkerer. As an active member of the iOS community and CTO at MJD Interactive he has led numerous successful mobile projects worldwide. When not developing apps he enjoys traveling, snowboarding, and surfing. He blogs about technology and business at matthewmorey.com. Wendy Lincoln is an editor of this book. She is driven by an insatiable curiosity about technology, and as a result she manages technical projects, does technical editing, and squanders her spare time learning about tech. When she’s not geeking out, you’ll find her in the waves or doing home-improvement projects with her husband. Chris Belanger is the editor of this book. Chris is the Editor in Chief at raywenderlich.com, and was a developer for nearly 20 years in various fields from e-health to aerial surveillance to industrial controls. If there are words to wrangle or a paragraph to ponder, he’s on the case. When he kicks back, you can usually find Chris with guitar in hand, looking for the nearest beach. Twitter: @crispytwit. raywenderlich.com 10 ARKit by Tutorials About This Book Sample About the Artist Vicki Wenderlich is the designer and artist of the cover of this book. She is Ray’s wife and business partner. She is a digital artist who creates illustrations, game art and a lot of other art or design work for the tutorials and books on raywenderlich.com. When she’s not making art, she loves hiking, a good glass of wine and attempting to create the perfect cheese plate. raywenderlich.com 11 W What You Need To follow along with this book, you'll need the following: • A Mac running macOS 10.13.4, at a minimum, with the latest point release and security patches installed. This is so you can install the latest version of the required development tool: Xcode. • Xcode 10 or later. Xcode is the main development tool for writing code in Swift. You need Xcode 10 at a minimum, since that version supports iOS 12.0 and ARKit 2.0, which is what we use in the book. You can download the latest version of Xcode for free from the Mac App Store, here: apple.co/1FLn51R. If you haven't installed the latest version of Xcode, be sure to do that before continuing with the book. The code covered in this book depends on Swift 4.2 and Xcode 10 — you may get lost if you try to work with an older version. raywenderlich.com 12 L Book License By purchasing ARKit by Tutorials, you have the following license: • You are allowed to use and/or modify the source code in ARKit by Tutorials in as many apps as you want, with no attribution required. • You are allowed to use and/or modify all art, images and designs that are included in ARKit by Tutorials in as many apps as you want, but must include this attribution line somewhere inside your app: “Artwork/images/designs: from ARKit by Tutorials, available at www.raywenderlich.com”. • The source code included in ARKit by Tutorials is for your personal use only. You are NOT allowed to distribute or sell the source code in ARKit by Tutorials without prior authorization. • This book is for your personal use only. You are NOT allowed to sell this book without prior authorization, or distribute it to friends, coworkers or students; they would need to purchase their own copies. All materials provided with this book are provided on an “as is” basis, without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose and noninfringement. In no event shall the authors or copyright holders be liable for any claim, damages or other liability, whether in an action of contract, tort or otherwise, arising from, out of or in connection with the software or the use or other dealings in the software. All trademarks and registered trademarks appearing in this guide are the properties of their respective owners. raywenderlich.com 13 B Book Source Code & Forums This book comes with the source code for the starter and completed projects for each chapter. These resources are shipped with the digital edition you downloaded from store.raywenderlich.com. We’ve also set up an official forum for the book at forums.raywenderlich.com. This is a great place to ask questions about the book or to submit any errors you may find. raywenderlich.com 14 A About the Cover Plesiosaurs were ancient marine reptiles; there are records of Plesiosaur fossils being studied as far back as the 1600’s, which makes them one of the first orders of extinct reptiles to be recognized as such. By the mid 1800’s, researchers had a pretty good idea of what Plesiosaurs looked like and how they behaved. But a string of recent discoveries of new types of Plesiosaurs have helped scientists clarify how they looked, how they evolved, and even how they hunted prey. Just as paleontologists bring the past alive through their discoveries, ARKit brings things real or imagined into reality through your smartphone. Whether you’re bringing dinosaurs to life in your dining room, creating portals to play in, or simply want to play Pac-Man on your city streets, ARKit can help make sure your great idea for an AR app will never become extinct! raywenderlich.com 15 7 Chapter 7: Building a Portal By Namrata Bandekar In this sample chapter, you’ll implement a portal app using ARKit and SceneKit. Portal apps can be used for educational purposes, like a virtual tour of the solar system from space, or for more leisurely activities, like enjoying a virtual beach vacation. The portal app The portal app you’ll build lets you place a virtual doorway to a futuristic room, somewhere on a horizontal plane in the real world. You can walk in and out of this virtual room, and you can explore what’s inside. In this chapter, you’ll set up the basics for your portal app. By the end of the chapter, you’ll know how to: • Set up an ARSession • Detect and render horizontal planes using ARKit Are you ready to build a gateway into another world? Perfect! raywenderlich.com 16 ARKit by Tutorials Chapter 7: Building a Portal Getting started In Xcode, open the starter project, Portal.xcodeproj. Build and run the project, and you’ll see a blank white screen. Ah, yes, the blank canvas of opportunity! Open Main.storyboard and expand the Portal View Controller Scene. raywenderlich.com 17 ARKit by Tutorials Chapter 7: Building a Portal The PortalViewController is presented to the user when the app is launched. The PortalViewController contains an ARSCNView that displays the camera preview. It also contains two UILabels that provide instructions and feedback to the user. Now, open PortalViewController.swift. In this file, you’ll see the following variables, which represent the elements in the storyboard: // 1 @IBOutlet var sceneView: ARSCNView? // 2 @IBOutlet weak var messageLabel: UILabel? // 3 @IBOutlet weak var sessionStateLabel: UILabel? Let’s take a look at what each one does: 1. sceneView is used to augment the camera view with 3D SceneKit objects. 2. messageLabel, which is a UILabel, will display instructional messages to the user. For example, telling them how to interact with your app. 3. sessionStateLabel, another UILabel, will inform the user about session interruptions, such as when the app goes into the background or if the ambient lighting is insufficient. Note: ARKit processes all of the sensor and camera data, but it doesn’t render any of the virtual content. To render content in your scenes, there are various renderers you can use alongside ARKit, such as SceneKit or SpriteKit. ARSCNView is a framework provided by Apple which you can use to integrate ARKit data with SceneKit easily. There are many benefits to using ARSCNView, which is why you’ll use it in this chapter’s project. In the starter project, you’ll also find a few utility classes in the Helpers group. You’ll be using these as you develop the app further. Setting up ARKit As you learned earlier in the book, the first step to setting things up is to capture the device’s video stream using the camera. For that, you’ll be using an ARSCNView object. raywenderlich.com 18 ARKit by Tutorials Chapter 7: Building a Portal Open PortalViewController.swift and add the following method: func runSession() { // 1 let configuration = ARWorldTrackingConfiguration.init() // 2 configuration.planeDetection = .horizontal // 3 configuration.isLightEstimationEnabled = true // 4 sceneView?.session.run(configuration) } // 5 #if DEBUG sceneView?.debugOptions = [ARSCNDebugOptions.showFeaturePoints] #endif Let’s take a look at what’s happening with this code: 1. You first instantiate an ARWorldTrackingConfiguration object. This defines the configuration for your ARSession. There are two types of configurations available for an ARSession: ARSessionConfiguration and ARWorldTrackingConfiguration. Using ARSessionConfiguration is not recommended because it only accounts for the rotation of the device, not its position. For devices that use an A9 processor, ARWorldTrackingSessionConfiguration gives the best results, as it tracks all degrees of movement of the device. 2. configuration.planeDetection is set to detect horizontal planes. The extent of the plane can change, and multiple planes can merge into one as the camera moves. It can find planes on any horizontal surface such as a floor, table or couch. 3. This enables light estimation calculations, which can be used by the rendering framework to make the virtual content look more realistic. 4. Start the session’s AR processing with the specified session configuration. This will start the ARKit session and video capturing from the camera, which is displayed in the sceneView. 5. For debug builds, this adds visible feature points; these are overlaid on the camera view. Now it’s time to set up the defaults for the labels. Replace resetLabels() with the following: func resetLabels() { messageLabel?.alpha = 1.0 messageLabel?.text = raywenderlich.com 19 ARKit by Tutorials } Chapter 7: Building a Portal "Move the phone around and allow the app to find a plane." + "You will see a yellow horizontal plane." sessionStateLabel?.alpha = 0.0 sessionStateLabel?.text = "" This resets the opacity and text of messageLabel and sessionStateLabel. Remember, messageLabel is used to display instructions to the user, while sessionStateLabel is used to display any error messages, in the case something goes wrong. Now, add runSession() to viewDidLoad() of PortalViewController: override func viewDidLoad() { super.viewDidLoad() resetLabels() runSession() } This will run the ARKit session when the app launches and loads the view. Next, build and run the app. Don’t forget — you’ll need to grant camera permissions to the app. ARSCNView does the heavy lifting of displaying the camera video capture. Because you’re in debug mode, you can also see the rendered feature points, which form a point cloud showing the intermediate results of scene analysis. raywenderlich.com 20 ARKit by Tutorials Chapter 7: Building a Portal Plane detection and rendering Previously, in runSession(), you set planeDetection to .horizontal, which means your app can detect horizontal planes. You can obtain the captured plane information in the delegate callback methods of the ARSCNViewDelegate protocol. Start by extending PortalViewController so it implements the ARSCNViewDelegate protocol: extension PortalViewController: ARSCNViewDelegate { } Add the following line to the very end of runSession(): sceneView?.delegate = self This sets the ARSCNViewDelegate delegate property of the sceneView as the PortalViewController. ARPlaneAnchors are added automatically to the ARSession anchors array, and ARSCNView automatically converts ARPlaneAnchor objects to SCNNode nodes. Now, to render the planes, all you need to do is implement the delegate method in the ARSCNViewDelegate extension of PortalViewController: // 1 func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) { // 2 DispatchQueue.main.async { // 3 if let planeAnchor = anchor as? ARPlaneAnchor { // 4 #if DEBUG // 5 let debugPlaneNode = createPlaneNode( center: planeAnchor.center, extent: planeAnchor.extent) // 6 node.addChildNode(debugPlaneNode) #endif // 7 self.messageLabel?.text = "Tap on the detected horizontal plane to place the portal" } } } raywenderlich.com 21 ARKit by Tutorials Chapter 7: Building a Portal Here’s what’s happening: 1. The delegate method, renderer(_:didAdd:for:), is called when ARSession detects a new plane, and the ARSCNView automatically adds an ARPlaneAnchor for the plane. 2. The callbacks occur on a background thread. Here, you dispatch the block to the main queue because any operations updating the UI should be done on the main UI thread. 3. This checks if the ARAnchor that was added is an ARPlaneAnchor. 4. This checks to see if you’re in debug mode. 5. If so, create the plane SCNNode object by passing in the center and extent coordinates of the planeAnchor detected by ARKit. The createPlaneNode() is a helper method which you’ll implement shortly. 6. The node object is an empty SCNNode that’s automatically added to the scene by ARSCNView; its coordinates correspond to the ARAnchor’s position. Here, you add the debugPlaneNode as a child node, so that it gets placed in the same position as the node. 7. Finally, regardless of whether or not you’re in debug mode, you update the instructional message to the user to indicate that the app is now ready to place the portal into the scene. Now it’s time to set up the helper methods. Create a new Swift file named SCNNodeHelpers.swift in the Helpers group. This file will contain all of the utility methods related to rendering SCNNode objects. Import SceneKit into this file by adding the following line: import SceneKit Now, add the following helper method: // 1 func createPlaneNode(center: vector_float3, extent: vector_float3) -> SCNNode { // 2 let plane = SCNPlane(width: CGFloat(extent.x), height: CGFloat(extent.z)) // 3 let planeMaterial = SCNMaterial() planeMaterial.diffuse.contents = UIColor.yellow.withAlphaComponent(0.4) // 4 plane.materials = [planeMaterial] // 5 raywenderlich.com 22 ARKit by Tutorials } Chapter 7: Building a Portal let planeNode = SCNNode(geometry: plane) // 6 planeNode.position = SCNVector3Make(center.x, 0, center.z) // 7 planeNode.transform = SCNMatrix4MakeRotation(-Float.pi / 2, 1, 0, 0) // 8 return planeNode Let’s go through this step-by-step: 1. The createPlaneNode method has two arguments: the center and extent of the plane to be rendered, both of type vector_float3. This type denotes the coordinates of the points. The function returns the SCNNode object created for the plane. 2. You instantiate the SCNPlane by specifying the width and height of the plane. You get the width from the x coordinate of the extent and the height from its z coordinate. 3. You initialize and assign the diffuse content for the SCNMaterial object. The diffuse layer color is set to a translucent yellow. 4. The SCNMaterial object is then added to the materials array of the plane. This defines the texture and color of the plane. 5. This creates an SCNNode with the geometry of the plane. The SCNPlane inherits from the SCNGeometry class, which only provides the form of a visible object rendered by SceneKit. You specify the position and orientation of the geometry by attaching it to an SCNNode object. Multiple nodes can reference the same geometry object, allowing it to appear at different positions in a scene. 6. You set the position of the planeNode. Note that the node is translated to coordinates (center.x, 0, center.z) reported by ARKit via the ARPlaneAnchor instance. 7. Planes in SceneKit are vertical by default, so you need to rotate the plane by 90 degrees to make it horizontal. 8. This returns the planeNode object created in the previous steps. raywenderlich.com 23 ARKit by Tutorials Chapter 7: Building a Portal Build and run the app. If ARKit can detect a suitable surface in your camera view, you’ll see a yellow horizontal plane. Move the device around, and you’ll notice the app sometimes shows multiple planes. As it finds more planes, it adds them to the view. Existing planes, however, do not update or change size as ARKit analyzes more features in the scene. raywenderlich.com 24 ARKit by Tutorials Chapter 7: Building a Portal ARKit constantly updates the plane’s position and extents based on new feature points it finds. To receive these updates in your app, add the following renderer(_:didUpdate:for:) delegate method to PortalViewController.swift: // 1 func renderer(_ renderer: SCNSceneRenderer, didUpdate node: SCNNode, for anchor: ARAnchor) { // 2 DispatchQueue.main.async { // 3 if let planeAnchor = anchor as? ARPlaneAnchor, node.childNodes.count > 0 { // 4 updatePlaneNode(node.childNodes[0], center: planeAnchor.center, extent: planeAnchor.extent) } } } Here’s what’s happening: 1. renderer(_:didUpdate:for:) is called when the corresponding ARAnchor updates. 2. Operations that update the UI should be executed on the main UI thread. 3. Check that the ARAnchor is an ARPlaneAnchor and make sure it has at least one child node that corresponds to the plane’s SCNNode. 4. updatePlaneNode(_:center:extent:) is a method that you’ll implement shortly. It updates the coordinates and size of the plane to the updated values contained in the ARPlaneAnchor. Open SCNNodeHelpers.swift and add the following code: func updatePlaneNode(_ node: SCNNode, center: vector_float3, extent: vector_float3) { // 1 let geometry = node.geometry as? SCNPlane // 2 geometry?.width = CGFloat(extent.x) geometry?.height = CGFloat(extent.z) // 3 node.position = SCNVector3Make(center.x, 0, center.z) } raywenderlich.com 25 ARKit by Tutorials Chapter 7: Building a Portal Going through this code step-by-step: 1. Check if the node has SCNPlane geometry. 2. Update the node geometry using the new values that are passed in. Use the extent or size of the ARPlaneAnchor to update the width and height of the plane. 3. Update the position of the plane node with the new position. Now that you can successfully update the position of the plane, build and run the app. You’ll see that the plane’s size and position shifts as it detects new feature points. There’s still one problem that needs to be solved. Once the app detects the plane, if you exit the app and come back in, you’ll see that the previously detected plane is now on top of other objects within the camera view; it no longer matches the plane surface it previously detected. To fix this, you need to remove the plane node whenever the ARSession is interrupted. You’ll handle that in the next chapter. raywenderlich.com 26 ARKit by Tutorials Chapter 7: Building a Portal Where to go from here? You may not realize it, but you have come a long way in building your portal app! Sure, there’s more to do, but you’re well on your way to traveling to another virtual dimension. Here’s a quick summary of what you did in this chapter: • You explored the starter project and reviewed the basics of ARKit. • You configured an ARSession so that it displays camera output within the app. • You added plane detection and other functions so that the app can render horizontal planes using the ARSCNViewDelegate protocol. In the next chapter, you’ll learn how to handle session interruptions and place rendered 3D objects in the view using SceneKit. raywenderlich.com 27 8 Chapter 8: Adding Objects to Your World By Namrata Bandekar In the previous chapter, you learned how to set up your iOS app to use ARKit sessions and detect horizontal planes. In this chapter, you’re going to build up your app and add 3D virtual content to the camera scene via SceneKit. By the end of this chapter, you’ll know how to: • Handle session interruptions • Place objects on a detected horizontal plane Before jumping in, load the starter project from the starter folder. Getting started Now that you can detect and render horizontal planes, you need to reset the state of the session if there are any interruptions. ARSession is interrupted when the app moves into the background or when multiple applications are in the foreground. Once interrupted, the video capture will fail, and the ARSession will be unable to do any tracking as it will no longer receive the required sensor data. When the app returns to the foreground, the rendered plane will still be present in the view. However, if your device has changed its position or rotation, the ARSession tracking will not work anymore. This is when you need to restart the session. The ARSCNViewDelegate implements the ARSessionObserver protocol. This protocol contains the methods that are called when the ARSession detects interruptions or session errors. raywenderlich.com 28 ARKit by Tutorials Chapter 8: Adding Objects to Your World Open PortalViewController.swift and add the following implementation for the delegate methods to the existing extension. // 1 func session(_ session: ARSession, didFailWithError error: Error) { // 2 guard let label = self.sessionStateLabel else { return } showMessage(error.localizedDescription, label: label, seconds: 3) } // 3 func sessionWasInterrupted(_ session: ARSession) { guard let label = self.sessionStateLabel else { return } showMessage("Session interrupted", label: label, seconds: 3) } // 4 func sessionInterruptionEnded(_ session: ARSession) { // 5 guard let label = self.sessionStateLabel else { return } showMessage("Session resumed", label: label, seconds: 3) } // 6 DispatchQueue.main.async { self.removeAllNodes() self.resetLabels() } // 7 runSession() Let’s go over this step-by-step. 1. session(_:, didFailWithError:) is called when the session fails. On failure, the session is paused, and it does not receive sensor data. 2. Here, you set the sessionStateLabel text to the error message that was reported as a result of the session failure. showMessage(_:, label:, seconds:) shows the message in the specified label for the given number of seconds. 3. The sessionWasInterrupted(_:) method is called when the video capture is interrupted as a result of the app moving to the background. No additional frame updates are delivered until the interruption ends. Here you display a "Session interrupted" message in the label for 3 seconds. 4. The sessionInterruptionEnded(_:) method is called after the session interruption has ended. A session will continue running from the last known state once the interruption has ended. If the device has moved, any anchors will be misaligned. To avoid this, you restart the session. 5. Show a "Session resumed" message on the screen for three seconds. raywenderlich.com 29 ARKit by Tutorials Chapter 8: Adding Objects to Your World 6. Remove previously rendered objects and reset all labels. You will implement these methods soon. These methods update the UI, so they need to be called on the main thread. 7. Restart the session. runSession() resets the session configuration and restarts the tracking with the new configuration. You will notice there are some compiler errors. You’ll resolve these errors by implementing the missing methods. Place the following variable in PortalViewController below the other variables: var debugPlanes: [SCNNode] = [] You’ll use debugPlanes, which is an array of SCNNode objects that keep track of all the rendered horizontal planes in debug mode. Then, place the following methods below resetLabels(): // 1 func showMessage(_ message: String, label: UILabel, seconds: Double) { label.text = message label.alpha = 1 } DispatchQueue.main.asyncAfter(deadline: .now() + seconds) { if label.text == message { label.text = "" label.alpha = 0 } } // 2 func removeAllNodes() { removeDebugPlanes() } // 3 func removeDebugPlanes() { for debugPlaneNode in self.debugPlanes { debugPlaneNode.removeFromParentNode() } } self.debugPlanes = [] raywenderlich.com 30 ARKit by Tutorials Chapter 8: Adding Objects to Your World Take a look at what’s happening: 1. You define a helper method to show a message string in a given UILabel for the specified duration in seconds. Once the specified number of seconds pass, you reset the visibility and text for the label. 2. removeAllNodes() removes all existing SCNNode objects added to the scene. Currently, you only remove the rendered horizontal planes here. 3. This method removes all the rendered horizontal planes from the scene and resets the debugPlanes array. Now, place the following line in renderer(_:, didAdd:, for:) just before the #endif of the #if DEBUG preprocessor directive: self.debugPlanes.append(debugPlaneNode) This adds the horizontal plane that was just added to the scene to the debugPlanes array. Note that in runSession(), the session executes with a given configuration: sceneView?.session.run(configuration) Replace the line above with the code below: sceneView?.session.run(configuration, options: [.resetTracking, .removeExistingAnchors]) Here you run the ARSession associated with your sceneView by passing the configuration object and an array of ARSession.RunOptions, with the following run options: 1. resetTracking:The session does not continue device position and motion tracking from the previous configuration. 2. removeExistingAnchors: Any anchor objects associated with the session in its previous configuration are removed. raywenderlich.com 31 ARKit by Tutorials Chapter 8: Adding Objects to Your World Run the app and try to detect a horizontal plane. Now send the app to the background and then re-open the app. Notice that the previously rendered horizontal plane is removed from the scene and the app resets the label to display the correct instructions to the user. raywenderlich.com 32 ARKit by Tutorials Chapter 8: Adding Objects to Your World Hit testing You are now ready to start placing objects on the detected horizontal planes. You will be using ARSCNView’s hit testing to detect touches from the user’s finger on the screen to see where they land in the virtual scene. A 2D point in the view’s coordinate space can refer to any point along a line segment in the 3D coordinate space. Hit-testing is the process of finding objects in the world located along this line segment. Open PortalViewController.swift and add the following variable. var viewCenter: CGPoint { let viewBounds = view.bounds return CGPoint(x: viewBounds.width / 2.0, y: viewBounds.height / 2.0) } In the above block of code, you set the variable viewCenter to the center of the PortalViewController’s view. Now add the following method: // 1 override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) { // 2 if let hit = sceneView?.hitTest(viewCenter, types: [.existingPlaneUsingExtent]).first { // 3 sceneView?.session.add(anchor: ARAnchor.init(transform: hit.worldTransform)) } } Here’s what’s happening: 1. ARSCNView has touches enabled. When the user taps on the view, touchesBegan() is called with a set of UITouch objects and a UIEvent which defines the touch event. You override this touch handling method to add an ARAnchor to the sceneView. 2. You call hitTest(_:, types:) on the sceneView object. The hitTest method has two parameters. It takes a CGPoint in the view’s coordinate system, in this case, the screen’s center, and the type of ARHitTestResult to search for. Here, you use the existingPlaneUsingExtent result type, which searches for points where the ray from the viewCenter intersects with any detected horizontal planes while considering the limited extent of the planes. The result of hitTest(_:, types:) is an array of all hit test results sorted from the nearest to the farthest. You pick the first plane that the ray intersects. You will get raywenderlich.com 33 ARKit by Tutorials Chapter 8: Adding Objects to Your World results for hitTest(_:, types:) any time the screen’s center falls within the rendered horizontal plane. 3. You add an ARAnchor to the ARSession at the point where your object will be placed. The ARAnchor object is initialized with a transformation matrix that defines the anchor’s rotation, translation and scale in world coordinates. The ARSCNView receives a callback in the delegate method renderer(_:didAdd:for:) after the anchor is added. This is where you handle rendering your portal. Adding crosshairs Before you add the portal to the scene, there is one last thing you need to add in the view. In the previous section, you implemented detecting hit testing for sceneView with the center of the device screen. In this section, you’ll work on adding a view to display the screen’s center to help the user position the device. Open Main.storyboard. Navigate to the Object Library by pressing ⇧+⌘+L or by clicking on the Library button in the toolbar. Search for UIView and select the View object. raywenderlich.com 34 ARKit by Tutorials Chapter 8: Adding Objects to Your World Drag and drop the view object onto the PortalViewController. Change the name of the view to Crosshair. Add layout constraints to the view such that its center matches its superview’s center. Add constraints to set the width and height of the view to 10. raywenderlich.com 35 ARKit by Tutorials Chapter 8: Adding Objects to Your World In the Size Inspector tab, your constraints should look like this: Navigate to the Attributes inspector tab and change the background color of the Crosshair view to Light Gray Color. Select the assistant editor and you’ll see PortalViewController.swift on the right. raywenderlich.com 36 ARKit by Tutorials Chapter 8: Adding Objects to Your World Press Ctrl and drag from the Crosshair view in storyboard to the PortalViewController code, just above the declaration for sceneView. Enter crosshair for the name of the IBOutlet and click Connect. Build and run the app. Notice there’s a gray square view at the center of the screen. This is the crosshair view that you just added. raywenderlich.com 37 ARKit by Tutorials Chapter 8: Adding Objects to Your World Now add the following code to the ARSCNViewDelegate extension of the PortalViewController. // 1 func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) { // 2 DispatchQueue.main.async { // 3 if let _ = self.sceneView?.hitTest(self.viewCenter, types: [.existingPlaneUsingExtent]).first { self.crosshair.backgroundColor = UIColor.green } else { // 4 self.crosshair.backgroundColor = UIColor.lightGray } } } Here’s what’s happening with the code you just added: 1. This method is part of the SCNSceneRendererDelegate protocol which is implemented by the ARSCNViewDelegate. It contains callbacks which can be used to perform operations at various times during the rendering. renderer(_: updateAtTime:) is called exactly once per frame and should be used to perform any per-frame logic. 2. You run the code to detect if the screen’s center falls in the existing detected horizontal planes and update the UI accordingly on the main queue. 3. This performs a hit test on the sceneView with the viewCenter to determine if the view center indeed intersects with a horizontal plane. If there’s at least one result detected, the crosshair view’s background color is changed to green. 4. If the hit test does not return any results, the crosshair view’s background color is reset to light gray. Build and run the app. Move the device around so that it detects and renders a horizontal plane, as shown on the left. Now move the device such that the device screen’s center falls within the plane, as shown on the right. raywenderlich.com 38 ARKit by Tutorials Chapter 8: Adding Objects to Your World Notice that the center view’s color changes to green. Adding a state machine Now that you have set up the app for detecting planes and placing an ARAnchor, you can get started with adding the portal. To track the state your app, add the following variables to PortalViewController: var portalNode: SCNNode? = nil var isPortalPlaced = false You store the SCNNode object that represents your portal in portalNode and use isPortalPlaced to keep track of whether the portal is rendered in the scene. Add the following method to PortalViewController: func makePortal() -> SCNNode { // 1 let portal = SCNNode() // 2 let box = SCNBox(width: 1.0, height: 1.0, raywenderlich.com 39 ARKit by Tutorials } Chapter 8: Adding Objects to Your World length: 1.0, chamferRadius: 0) let boxNode = SCNNode(geometry: box) // 3 portal.addChildNode(boxNode) return portal Here you define makePortal(), a method that will configure and render the portal. A few things are happening here: 1. You create an SCNNode object which will represent your portal. 2. This initializes a SCNBox object which is a cube and makes a SCNNode object for the box using the SCNBox geometry. 3. You add the boxNode as a child node to your portal and return the portal node. Here, makePortal() is creating a portal node with a box object inside it as a placeholder. Now replace the renderer(_:, didAdd:, for:) and renderer(_:, didUpdate:, for:) methods for the SCNSceneRendererDelegate with the following: func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) { DispatchQueue.main.async { // 1 if let planeAnchor = anchor as? ARPlaneAnchor, !self.isPortalPlaced { #if DEBUG let debugPlaneNode = createPlaneNode( center: planeAnchor.center, extent: planeAnchor.extent) node.addChildNode(debugPlaneNode) self.debugPlanes.append(debugPlaneNode) #endif self.messageLabel?.alpha = 1.0 self.messageLabel?.text = """ Tap on the detected \ horizontal plane to place the portal """ } else if !self.isPortalPlaced {// 2 // 3 self.portalNode = self.makePortal() if let portal = self.portalNode { // 4 node.addChildNode(portal) self.isPortalPlaced = true // 5 self.removeDebugPlanes() self.sceneView?.debugOptions = [] raywenderlich.com 40 ARKit by Tutorials } } } Chapter 8: Adding Objects to Your World // 6 DispatchQueue.main.async { self.messageLabel?.text = "" self.messageLabel?.alpha = 0 } } func renderer(_ renderer: SCNSceneRenderer, didUpdate node: SCNNode, for anchor: ARAnchor) { DispatchQueue.main.async { // 7 if let planeAnchor = anchor as? ARPlaneAnchor, node.childNodes.count > 0, !self.isPortalPlaced { updatePlaneNode(node.childNodes[0], center: planeAnchor.center, extent: planeAnchor.extent) } } } Here are the changes you made: 1. You’re adding a horizontal plane to the scene to show the detected planes only if the anchor that was added to the scene is an ARPlaneAnchor, and only if isPortalPlaced equals false, which means the portal has not yet been placed. 2. If the anchor that was added was not an ARPlaneAnchor, and the portal node still hasn’t been placed, this must be the anchor you add when the user taps on the screen to place the portal. 3. You create the portal node by calling makePortal(). 4. renderer(_:, didAdd:, for:) is called with the SCNNode object, node, that is added to the scene. You want to place the portal node at the location of the node. So you add the portal node as a child node of node, and you set isPortalPlaced to true to track that the portal node has been added. 5. To clean up the scene, you remove all rendered horizontal planes and reset the debugOptions for sceneView so that the feature points are no longer rendered on screen. 6. You update the messageLabel on the main thread to reset its text and hide it. raywenderlich.com 41 ARKit by Tutorials Chapter 8: Adding Objects to Your World 7. In the renderer(_:, didUpdate:, for:) you update the rendered horizontal plane only if the given anchor is an ARPlaneAnchor, if the node has at least one child node and if the portal hasn’t been placed yet. Finally, replace removeAllNodes() with the following. func removeAllNodes() { // 1 removeDebugPlanes() // 2 self.portalNode?.removeFromParentNode() // 3 self.isPortalPlaced = false } This method is used for cleanup and removing all rendered objects from the scene. Here’s a closer look at what’s happening: 1. You remove all the rendered horizontal planes. 2. You then remove the portalNode from its parent node. 3. Change the isPortalPlaced variable to false to reset the state. Build and run the app; let the app detect a horizontal plane and then tap on the screen when the crosshair view turns green. You will see a rather plain-looking, huge white box. raywenderlich.com 42 ARKit by Tutorials Chapter 8: Adding Objects to Your World This is the placeholder for your portal. In the next chapter, you’ll add some walls and a doorway to the portal. You’ll also add textures to the walls so that they look more realistic. Where to go from here? This has been quite a ride! Here’s a summary of what you learned in this chapter: • You can now detect and handle ARSession interruptions when the app goes to the background. • You understand how hit testing works with an ARSCNView and the detected horizontal planes in the scene. • You can use the results of hit testing to place ARAnchors and SCNNode objects corresponding to them. In the next chapter, you’ll learn how to use SceneKit to build your portal’s walls and learn more about how to use textures and lighting inside your Portal. raywenderlich.com 43 9 Chapter 9: Geometry, Textures & Lighting By Namrata Bandekar You learned how to add 3D objects to your scene with SceneKit. Now it’s time to put that knowledge to use and build a portal. In this chapter, you will learn how to: • Create walls, a ceiling and roof for your portal and adjust their position and rotation. • Make the inside of the portal look more realistic with different textures. • Add lighting to your scene. Getting started Before you begin, here’s an overview of how SceneKit works. Load up the starter project from the starter folder. The SceneKit coordinate system As you saw in the previous chapter, SceneKit can be used to add virtual 3D objects to your view. The SceneKit content view is comprised of a hierarchical tree structure of nodes, also known as the scene graph. A scene consists of a root node, which defines a coordinate space for the world of the scene, and other nodes that populate the world with visible content. Each node or 3D object that you render on screen is an object of type SCNNode. An SCNNode object defines the coordinate space transform (position, orientation and scale) relative to its parent node. It doesn’t have any visible content by itself. The rootNode object in a scene defines the coordinate system of the world rendered by SceneKit. Each child node you add to this root node creates its own coordinate system, which, in turn, is inherited by its own children. raywenderlich.com 44 ARKit by Tutorials Chapter 9: Geometry, Textures & Lighting SceneKit uses a right-handed coordinate system where (by default) the direction of view is along the negative z-axis, as illustrated below. The position of the SCNNode object is defined using an SCNVector3 which locates it within the coordinate system of its parent. The default position is the zero vector, indicating that the node is placed at the origin of the parent node’s coordinate system. In this case, SCNVector3 is a three-component vector where each of the components is a Float value representing the coordinate on each axis. The SCNNode object’s orientation — expressed as pitch, yaw, and roll angles — is defined by its eulerAngles property. This is also represented by an SCNVector3 struct where each vector component is an angle in radians. Textures The SCNNode object by itself doesn’t have any visible content. You add 2D and 3D objects to a scene by attaching SCNGeometry objects to nodes. Geometries have attached SCNMaterial objects that determine their appearance. An SCNMaterial has several visual properties. Each visual property is an instance of the SCNMaterialProperty class that provides a solid color, texture or other 2D content. There are a variety of visual properties for basic shading, physically based shading, and special effects that you can use to make the material look more realistic. raywenderlich.com 45 ARKit by Tutorials Chapter 9: Geometry, Textures & Lighting The SceneKit asset catalog is explicitly designed to help you manage your project’s assets separately from the code. In your starter project, open the Assets.scnassets folder. Notice that you already have images representing different visual properties for the ceiling, floor and walls. With SceneKit, you can also use nodes with attached SCNLight objects to shade the geometries in a scene with light and shadow effects. Building the portal Let’s jump right into creating the floor for the portal. Open SCNNodeHelpers.swift and add the following to the top of the file just below the import SceneKit statement. // 1 let SURFACE_LENGTH: CGFloat = 3.0 let SURFACE_HEIGHT: CGFloat = 0.2 let SURFACE_WIDTH: CGFloat = 3.0 // 2 let SCALEX: Float = 2.0 let SCALEY: Float = 2.0 // 3 let WALL_WIDTH:CGFloat = 0.2 let WALL_HEIGHT:CGFloat = 3.0 let WALL_LENGTH:CGFloat = 3.0 raywenderlich.com 46 ARKit by Tutorials Chapter 9: Geometry, Textures & Lighting You’re doing a few things here: 1. You define constants for the dimensions of the floor and ceiling of your portal. The height of the roof and ceiling corresponds to the thickness. 2. These are constants to scale and repeat the textures over the surfaces. 3. These define the width, height and length of the wall nodes. Next, add the following method to SCNNodeHelpers: func repeatTextures(geometry: SCNGeometry, scaleX: Float, scaleY: Float) { // 1 geometry.firstMaterial?.diffuse.wrapS = SCNWrapMode.repeat geometry.firstMaterial?.selfIllumination.wrapS = SCNWrapMode.repeat geometry.firstMaterial?.normal.wrapS = SCNWrapMode.repeat geometry.firstMaterial?.specular.wrapS = SCNWrapMode.repeat geometry.firstMaterial?.emission.wrapS = SCNWrapMode.repeat geometry.firstMaterial?.roughness.wrapS = SCNWrapMode.repeat // 2 geometry.firstMaterial?.diffuse.wrapT = SCNWrapMode.repeat geometry.firstMaterial?.selfIllumination.wrapT = SCNWrapMode.repeat geometry.firstMaterial?.normal.wrapT = SCNWrapMode.repeat geometry.firstMaterial?.specular.wrapT = SCNWrapMode.repeat geometry.firstMaterial?.emission.wrapT = SCNWrapMode.repeat geometry.firstMaterial?.roughness.wrapT = SCNWrapMode.repeat } // 3 geometry.firstMaterial?.diffuse.contentsTransform = SCNMatrix4MakeScale(scaleX, scaleY, 0) geometry.firstMaterial?.selfIllumination.contentsTransform = SCNMatrix4MakeScale(scaleX, scaleY, 0) geometry.firstMaterial?.normal.contentsTransform = SCNMatrix4MakeScale(scaleX, scaleY, 0) geometry.firstMaterial?.specular.contentsTransform = SCNMatrix4MakeScale(scaleX, scaleY, 0) geometry.firstMaterial?.emission.contentsTransform = SCNMatrix4MakeScale(scaleX, scaleY, 0) geometry.firstMaterial?.roughness.contentsTransform = SCNMatrix4MakeScale(scaleX, scaleY, 0) This defines a method to repeat the texture images over the surface in the X and Y dimensions. raywenderlich.com 47 ARKit by Tutorials Chapter 9: Geometry, Textures & Lighting Here’s the breakdown: 1. The method takes an SCNGeometry object and the X and Y scaling factors as the input. Texture mapping uses the S and T coordinate system which is just another naming convention: S corresponds to X and T corresponds to Y. Here you define the wrapping mode for the S dimension as SCNWrapMode.repeat for all the visual properties of your material. 2. You define the wrapping mode for the T dimension as SCNWrapMode.repeat as well for all visual properties. With the repeat mode, texture sampling uses only the fractional part of texture coordinates. 3. Here, each of the visual properties contentsTransform is set to a scale transform described by anSCNMatrix4 struct. You set the X and Y scaling factors to scaleX and scaleY respectively. You only want to show the floor and ceiling nodes when the user is inside the portal; any other time, you need to hide them. To implement this, add the following method to SCNNodeHelpers: func makeOuterSurfaceNode(width: CGFloat, height: CGFloat, length: CGFloat) -> SCNNode { // 1 let outerSurface = SCNBox(width: SURFACE_WIDTH, height: SURFACE_HEIGHT, length: SURFACE_LENGTH, chamferRadius: 0) // 2 outerSurface.firstMaterial?.diffuse.contents = UIColor.white outerSurface.firstMaterial?.transparency = 0.000001 } // 3 let outerSurfaceNode = SCNNode(geometry: outerSurface) outerSurfaceNode.renderingOrder = 10 return outerSurfaceNode Taking a look at each numbered comment: 1. Create an outerSurface scene box geometry object with the dimensions of the floor and ceiling. 2. Add visible content to the box object’s diffuse property so it’s rendered. You set the transparency to a low value, so the object is hidden from view. raywenderlich.com 48 ARKit by Tutorials Chapter 9: Geometry, Textures & Lighting 3. Create an SCNNode object from the outerSurface geometry. Set renderingOrder for the node to 10. Nodes with a larger rendering order are rendered last. To make the ceiling and floor invisible from outside the portal, you will make the rendering order of the inner ceiling and floor nodes much larger than 10. Now add the following code to SCNNodeHelpers to create the portal floor: func makeFloorNode() -> SCNNode { // 1 let outerFloorNode = makeOuterSurfaceNode( width: SURFACE_WIDTH, height: SURFACE_HEIGHT, length: SURFACE_LENGTH) // 2 outerFloorNode.position = SCNVector3(SURFACE_HEIGHT * 0.5, -SURFACE_HEIGHT, 0) let floorNode = SCNNode() floorNode.addChildNode(outerFloorNode) // 3 let innerFloor = SCNBox(width: SURFACE_WIDTH, height: SURFACE_HEIGHT, length: SURFACE_LENGTH, chamferRadius: 0) // 4 innerFloor.firstMaterial?.lightingModel = .physicallyBased innerFloor.firstMaterial?.diffuse.contents = UIImage(named: "Assets.scnassets/floor/textures/Floor_Diffuse.png") innerFloor.firstMaterial?.normal.contents = UIImage(named: "Assets.scnassets/floor/textures/Floor_Normal.png") innerFloor.firstMaterial?.roughness.contents = UIImage(named: "Assets.scnassets/floor/textures/Floor_Roughness.png") innerFloor.firstMaterial?.specular.contents = UIImage(named: "Assets.scnassets/floor/textures/Floor_Specular.png") innerFloor.firstMaterial?.selfIllumination.contents = UIImage(named: "Assets.scnassets/floor/textures/Floor_Gloss.png") // 5 repeatTextures(geometry: innerFloor, scaleX: SCALEX, scaleY: SCALEY) // 6 let innerFloorNode = SCNNode(geometry: innerFloor) innerFloorNode.renderingOrder = 100 // 7 innerFloorNode.position = SCNVector3(SURFACE_HEIGHT * 0.5, 0, 0) floorNode.addChildNode(innerFloorNode) raywenderlich.com 49 ARKit by Tutorials } Chapter 9: Geometry, Textures & Lighting return floorNode Breaking this down: 1. Create the lower side of the floor node using the floor’s dimensions. 2. Position outerFloorNode such that it’s laid out on the bottom side of the floor node. Add the node to the floorNode which holds both the inner and outer surfaces of the floor. 3. You make the geometry of the floor using the SCNBox object initialized with the constants declared previously for each dimension. 4. The lightingModel of the material for the floor is set to physicallyBased. This type of shading incorporates a realistic abstraction of physical lights and materials. The contents for various visual properties for the material are set using texture images from the scnassets catalog. 5. The texture for the material is repeated over the X and Y dimensions using repeatTextures(), which you defined before. 6. You create a node for the floor using the innerFloor geometry object and set the rendering order to higher than that of the outerFloorNode. This ensures that when the user is outside the portal, the floor node will be invisible. 7. Finally, set the position of innerFloorNode to sit above the outerFloorNode and add it as a child to floorNode. Return the floor node object to the caller. Open PortalViewController.swift and add the following constants: let POSITION_Y: CGFloat = -WALL_HEIGHT*0.5 let POSITION_Z: CGFloat = -SURFACE_LENGTH*0.5 These constants represent the position offsets for nodes in the Y and Z dimensions. Add the floor node to your portal by replacing makePortal(). func makePortal() -> SCNNode { // 1 let portal = SCNNode() // 2 let floorNode = makeFloorNode() floorNode.position = SCNVector3(0, POSITION_Y, POSITION_Z) } // 3 portal.addChildNode(floorNode) return portal raywenderlich.com 50 ARKit by Tutorials Chapter 9: Geometry, Textures & Lighting Fairly straightforward code: 1. You create a SCNNode object to hold the portal. 2. You create the floor node using makeFloorNode() defined in SCNNodeHelpers. You set the position of floorNode using the constant offsets. The center of the SCNGeometry is set to this location in the node’s parent’s coordinate system. 3. Add the floorNode to the portal node and return the portal node. Note that the portal node is added to the node created at the anchor’s position when the user taps the view in renderer(_ :, didAdd:, for:). Build and run the app. You’ll notice the floor node is dark. That’s because you haven’t added a light source yet! Now add the ceiling node. Open SCNNodeHelpers.swift and add the following method: func makeCeilingNode() -> SCNNode { // 1 let outerCeilingNode = makeOuterSurfaceNode( width: SURFACE_WIDTH, height: SURFACE_HEIGHT, raywenderlich.com 51 ARKit by Tutorials Chapter 9: Geometry, Textures & Lighting length: SURFACE_LENGTH) // 2 outerCeilingNode.position = SCNVector3(SURFACE_HEIGHT * 0.5, SURFACE_HEIGHT, 0) let ceilingNode = SCNNode() ceilingNode.addChildNode(outerCeilingNode) // 3 let innerCeiling = SCNBox(width: SURFACE_WIDTH, height: SURFACE_HEIGHT, length: SURFACE_LENGTH, chamferRadius: 0) // 4 innerCeiling.firstMaterial?.lightingModel = .physicallyBased innerCeiling.firstMaterial?.diffuse.contents = UIImage(named: "Assets.scnassets/ceiling/textures/Ceiling_Diffuse.png") innerCeiling.firstMaterial?.emission.contents = UIImage(named: "Assets.scnassets/ceiling/textures/Ceiling_Emis.png") innerCeiling.firstMaterial?.normal.contents = UIImage(named: "Assets.scnassets/ceiling/textures/Ceiling_Normal.png") innerCeiling.firstMaterial?.specular.contents = UIImage(named: "Assets.scnassets/ceiling/textures/Ceiling_Specular.png") innerCeiling.firstMaterial?.selfIllumination.contents = UIImage(named: "Assets.scnassets/ceiling/textures/Ceiling_Gloss.png") // 5 repeatTextures(geometry: innerCeiling, scaleX: SCALEX, scaleY: SCALEY) // 6 let innerCeilingNode = SCNNode(geometry: innerCeiling) innerCeilingNode.renderingOrder = 100 } // 7 innerCeilingNode.position = SCNVector3(SURFACE_HEIGHT * 0.5, 0, 0) ceilingNode.addChildNode(innerCeilingNode) return ceilingNode Here’s what’s happening: 1. Similar to the floor, you create an outerCeilingNode with the dimensions for the ceiling. 2. Set the position of the outer ceiling node so that it goes on top of the ceiling. Create a node to hold the inner and outer sides of the ceiling. Add outerCeilingNode as a child of the ceilingNode. raywenderlich.com 52 ARKit by Tutorials Chapter 9: Geometry, Textures & Lighting 3. Make innerCeiling an SCNBox object with the respective dimensions. 4. Set the lightingModel to physicallyBased. Also, set the contents of the visual properties that are defined by various texture images found in the assets catalog. 5. repeatTextures() wraps the texture images in both the X and Y dimensions to create a repeated pattern for the ceiling. 6. Create innerCeilingNode using the innerCeiling geometry and set its renderingOrder property to a high value so that it gets rendered after the outerCeilingNode. 7. Position innerCeilingNode within its parent node and add it as a child of ceilingNode. Return ceilingNode to the caller. Now to call this from somewhere. Open PortalViewController.swift and add the following block of code to makePortal() just before the return statement: // 1 let ceilingNode = makeCeilingNode() ceilingNode.position = SCNVector3(0, POSITION_Y+WALL_HEIGHT, POSITION_Z) // 2 portal.addChildNode(ceilingNode) 1. Create the ceiling node using makeCeilingNode() which you just defined. Set the position of the center of ceilingNode to the SCNVector3 struct. The Y coordinate of the center is offset by the Y position of the floor added to the height of the wall. You also subtract SURFACE_HEIGHT to account for the thickness of the ceiling. The Z coordinate is set to the POSITION_Z offset similar to the floor. This is how far away from the center of the ceiling is from the camera along the Z axis. 2. Add ceilingNode as a child of the portal. raywenderlich.com 53 ARKit by Tutorials Chapter 9: Geometry, Textures & Lighting Build and run the app. Here’s what you’ll see: Time to add the walls! Open SCNNodeHelpers.swift and add the following method: func makeWallNode(length: CGFloat = WALL_LENGTH, height: CGFloat = WALL_HEIGHT, maskLowerSide:Bool = false) -> SCNNode { // 1 let outerWall = SCNBox(width: WALL_WIDTH, height: height, length: length, chamferRadius: 0) // 2 outerWall.firstMaterial?.diffuse.contents = UIColor.white outerWall.firstMaterial?.transparency = 0.000001 // 3 let outerWallNode = SCNNode(geometry: outerWall) let multiplier: CGFloat = maskLowerSide ? -1 : 1 outerWallNode.position = SCNVector3(WALL_WIDTH*multiplier,0,0) outerWallNode.renderingOrder = 10 // 4 let wallNode = SCNNode() wallNode.addChildNode(outerWallNode) // 5 let innerWall = SCNBox(width: WALL_WIDTH, height: height, length: length, chamferRadius: 0) // 6 innerWall.firstMaterial?.lightingModel = .physicallyBased innerWall.firstMaterial?.diffuse.contents = UIImage(named: raywenderlich.com 54 ARKit by Tutorials Chapter 9: Geometry, Textures & Lighting "Assets.scnassets/wall/textures/Walls_Diffuse.png") innerWall.firstMaterial?.metalness.contents = UIImage(named: "Assets.scnassets/wall/textures/Walls_Metalness.png") innerWall.firstMaterial?.roughness.contents = UIImage(named: "Assets.scnassets/wall/textures/Walls_Roughness.png") innerWall.firstMaterial?.normal.contents = UIImage(named: "Assets.scnassets/wall/textures/Walls_Normal.png") innerWall.firstMaterial?.specular.contents = UIImage(named: "Assets.scnassets/wall/textures/Walls_Spec.png") innerWall.firstMaterial?.selfIllumination.contents = UIImage(named: "Assets.scnassets/wall/textures/Walls_Gloss.png") } // 7 let innerWallNode = SCNNode(geometry: innerWall) wallNode.addChildNode(innerWallNode) return wallNode Going over the code step-by-step: 1. You create an outerWall node that will sit on the outside of the wall to make it appear transparent from the outside. You create an SCNBox object matching the wall’s dimensions. 2. You set the diffuse contents of the material to a monochrome white color and the transparency to a low number. This helps achieve the see-through effect if you look at the wall from outside of the room. 3. You create a node with the outerWall geometry. The multiplier is set based on which side of the wall the outer wall needs to be rendered. If maskLowerSide is set to true, the outer wall is placed below the inner wall in the wall node’s coordinate system; otherwise, it’s placed above. You set the position of the node such that the outer wall is offset by the wall width in the X dimension. Set the rendering order for the outer wall to a low number so that it’s rendered first. This makes the walls invisible from the outside. 4. You also create a node to hold the wall and add the outerWallNode as its child node. 5. You make innerWall an SCNBox object with the respective wall dimensions. 6. You set the lightingModel to physicallyBased. Similar to the ceiling and floor nodes, you set the contents of the visual properties that are defined by various texture images for the walls. raywenderlich.com 55 ARKit by Tutorials Chapter 9: Geometry, Textures & Lighting 7. Finally, you create an innerWallNode object using the innerWall geometry. Add this node to the parent wallNode object. By default, innerWallNode is placed at the origin of wallNode. Return the node to the caller. Now add the far wall for the portal. Open PortalViewController.swift and add the following to the end of makePortal() just before the return statement: // 1 let farWallNode = makeWallNode() // 2 farWallNode.eulerAngles = SCNVector3(0, 90.0.degreesToRadians, 0) // 3 farWallNode.position = SCNVector3(0, POSITION_Y+WALL_HEIGHT*0.5, POSITION_Z-SURFACE_LENGTH*0.5) portal.addChildNode(farWallNode) This is fairly straightforward: 1. Create a node for the far wall. farWallNode needs the mask on the lower side. So the default value of false for maskLowerSide will do. 2. Add eulerAngles to the node. Since the wall is rotated along the Y axis and perpendicular to the camera, it has a rotation of 90 degrees for the second component. The wall does not have a rotation angle for the X and Z axes. 3. Set the position of the center of farWallNode such that its height is offset by POSITION_Y. Its depth is calculated by adding the depth of the center of the ceiling to the distance from the center of the ceiling to its far end. Build and run the app, and you will see the far wall attached to the ceiling on top and attached to the floor on the bottom. raywenderlich.com 56 ARKit by Tutorials Chapter 9: Geometry, Textures & Lighting Next up you will add the right and left walls. In makePortal(), add the following code just before the return portal statement to create the right and left side walls: // 1 let rightSideWallNode = makeWallNode(maskLowerSide: true) // 2 rightSideWallNode.eulerAngles = SCNVector3(0, 180.0.degreesToRadians, 0) // 3 rightSideWallNode.position = SCNVector3(WALL_LENGTH*0.5, POSITION_Y+WALL_HEIGHT*0.5, POSITION_Z) portal.addChildNode(rightSideWallNode) // 4 let leftSideWallNode = makeWallNode(maskLowerSide: true) // 5 leftSideWallNode.position = SCNVector3(-WALL_LENGTH*0.5, POSITION_Y+WALL_HEIGHT*0.5, POSITION_Z) portal.addChildNode(leftSideWallNode) Going through this step-by-step: 1. Create a node for the right wall. You want to put the outer wall on the lower side of the node, so you set maskLowerSide to true. 2. You set the rotation of the wall along the Y axis to 180 degrees. This ensures the wall has its inner side facing the right way. 3. Set the location of the wall so that it’s flush with the right edge of the far wall, ceiling and floor. Add rightSideWallNode as a child node of portal. 4. Similar to the right wall node, create a node to represent the left wall with maskLowerSide set to true. 5. The left wall does not have any rotation applied to it, but you adjust its location so that it’s flush with the left edge of the far wall, floor and ceiling. You add the left wall node as a child node of the portal node. raywenderlich.com 57 ARKit by Tutorials Chapter 9: Geometry, Textures & Lighting Build and run the app, and your portal now has three walls. If you move out of the portal, none of the walls are visible. Adding the doorway There’s one thing missing in your portal: an entrance! Currently, the portal does not have a fourth wall. Instead of adding another wall, you will add just the necessary parts of a wall to leave room for a doorway. Open PortalViewController.swift and add these constants: let DOOR_WIDTH:CGFloat = 1.0 let DOOR_HEIGHT:CGFloat = 2.4 As their names suggest, these define the width and height of the doorway. Add the following to PortalViewController: func addDoorway(node: SCNNode) { // 1 let halfWallLength: CGFloat = WALL_LENGTH * 0.5 let frontHalfWallLength: CGFloat = (WALL_LENGTH - DOOR_WIDTH) * 0.5 raywenderlich.com 58 ARKit by Tutorials Chapter 9: Geometry, Textures & Lighting // 2 let rightDoorSideNode = makeWallNode(length: frontHalfWallLength) rightDoorSideNode.eulerAngles = SCNVector3(0,270.0.degreesToRadians, 0) rightDoorSideNode.position = SCNVector3(halfWallLength - 0.5 * DOOR_WIDTH, POSITION_Y+WALL_HEIGHT*0.5, POSITION_Z+SURFACE_LENGTH*0.5) node.addChildNode(rightDoorSideNode) // 3 let leftDoorSideNode = makeWallNode(length: frontHalfWallLength) leftDoorSideNode.eulerAngles = SCNVector3(0, 270.0.degreesToRadians, 0) leftDoorSideNode.position = SCNVector3(-halfWallLength + 0.5 * frontHalfWallLength, POSITION_Y+WALL_HEIGHT*0.5, POSITION_Z+SURFACE_LENGTH*0.5) node.addChildNode(leftDoorSideNode) } addDoorway(node:) is a method that adds a wall with an entrance to the given node. Here’s what you’re doing: 1. Define constants to store the half wall length and the length of the front wall on each side of the door. 2. Create a node to represent the wall on the right side of the entrance using the constants declared in the previous step. You also adjust the rotation and location of the node so that it’s attached to the front edge of the right wall, ceiling and floor. You then add rightDoorSideNode as a child of the given node. 3. Similar to step 2, you create a node for the left side of the doorway, and set the rotation and location of leftDoorSideNode so that it is flush with the front edge of the left wall, ceiling and floor nodes. Finally, you use addChildNode() to add it as a child node to node. In makePortal(), add the following just before return portal: addDoorway(node: portal) Here, you add the doorway to the portal node. raywenderlich.com 59 ARKit by Tutorials Chapter 9: Geometry, Textures & Lighting Build and run the app. You’ll see the doorway on the portal, but the top of the door is currently touching the ceiling. You’ll need to add another piece of the wall to make the doorway span the pre-defined DOOR_HEIGHT. Add the following at the end of addDoorway(node:): // 1 let aboveDoorNode = makeWallNode(length: DOOR_WIDTH, height: WALL_HEIGHT - DOOR_HEIGHT) // 2 aboveDoorNode.eulerAngles = SCNVector3(0, 270.0.degreesToRadians, 0) // 3 aboveDoorNode.position = SCNVector3(0, POSITION_Y+(WALL_HEIGHT-DOOR_HEIGHT)*0.5+DOOR_HEIGHT, POSITION_Z+SURFACE_LENGTH*0.5) node.addChildNode(aboveDoorNode) 1. Create a wall node with the respective dimensions to fit above the entrance of the portal. 2. Adjust the rotation of aboveDoorNode so that it’s at the front of the portal. The masked side is placed on the outside. raywenderlich.com 60 ARKit by Tutorials Chapter 9: Geometry, Textures & Lighting 3. Set the position of the node so that it’s placed on top of the doorway that you just built. Add it as a child node of node. Build and run. This time you’ll notice the doorway is now complete with a proper wall. Placing lights That portal doesn’t look too inviting; it’s rather dark and gloomy. You can add a light source to brighten it up! Add the following method to PortalViewController: func placeLightSource(rootNode: SCNNode) { // 1 let light = SCNLight() light.intensity = 10 // 2 light.type = .omni // 3 let lightNode = SCNNode() lightNode.light = light // 4 raywenderlich.com 61 ARKit by Tutorials } Chapter 9: Geometry, Textures & Lighting lightNode.position = SCNVector3(0, POSITION_Y+WALL_HEIGHT, POSITION_Z) rootNode.addChildNode(lightNode) Here’s how it works: 1. Create an SCNLight object and set its intensity. Since you’re using the physicallyBased lighting model, this value is the luminous flux of the light source. The default value is 1000 lumens, but you want an intensity which is much lower, giving it a slightly darker look. 2. A light’s type determines the shape and direction of illumination provided by the light, as well as the set of attributes available for modifying the light’s behavior. Here, you set the type of the light to omnidirectional, also known as a point light. An omnidirectional light has constant intensity and a direction. The light’s position relative to other objects in your scene determines its direction. 3. You create a node to hold the light and attach the light object to the node using its light property. 4. Place the light at the center of the ceiling using the Y and Z offsets and then add lightNode as a child of the rootNode. In makePortal(), add the following just before return portal. placeLightSource(rootNode: portal) This places the light source inside the portal. Build and run the app, and you’ll see a brighter, more inviting doorway to your virtual world! raywenderlich.com 62 ARKit by Tutorials Chapter 9: Geometry, Textures & Lighting Where to go from here? The portal is complete! You have learned a lot through creating this sci-fi portal. Let’s take a quick look at all the things you covered in this chapter. • You have a basic understanding of SceneKit’s coordinate system and materials. • You learned how to create SCNNode objects with different geometries and attach textures to them. • You also placed light sources in your scene so that the portal looked more realistic. Going forward, there are many changes you can make to the portal project. You can: • Make a door that opens or shuts when the user taps on the screen. • Explore various geometries to create a room that spans infinitely. • Experiment with different shapes for the doorway. Don’t stop here. Let your sci-fi imagination run wild. raywenderlich.com 63 W Where to Go From Here? We hope you enjoyed this sample of ARKit by Tutorials! If you enjoyed this sample, be sure to check out the full book, which contains the following chapters: • Chapter 1: Hello ARKit!: You’ll start off at the very beginning with a guided tour of what augmented reality is and how ARKit can help. You’ll also learn about the limitations of ARKit, which is equally important. • Chapter 2: Your First ARKit App: Once you’ve covered the basics, you’ll get your hands dirty by creating your own ARKit app. This will be the start of creating a fun augmented reality poker dice table-top game. • Chapter 3: Basic Session Management: Here, you’ll learn about session management, an essential part of every ARKit app. Not only will your app comply with the basic requirements, but it will also provide the users with a seamless AR experience. • Chapter 4: Adding 3D Objects & Textures: Next, you’ll add some basic 3D objects to your project. You’ll also create some materials and learn about the different types of textures used in PBR-based materials. • Chapter 5: Detecting Surfaces: In this chapter, you’ll learn how to detect and manage surfaces — like floors, walls and tables — with ARKit. Once you know how to do that, you’ll know where (and how) to place AR content within the scene. • Chapter 6: Adding Physics: Finally, you’ll add physics to your virtual content. With physics, you can make things bounce off of surfaces or interactive with other virtual contents, just like the real thing. raywenderlich.com 64 ARKit by Tutorials Where to Go From Here? • Chapter 7: Building a Portal: In this chapter, you’ll explore an app to review the basics of ARKit development. You’ll set up an ARSession and add plane detection and other functions so that the app can render horizontal planes using the ARSCNViewDelegate protocol. • Chapter 8: Adding Objects to Your World: Over the course of building your portal, you’ll learn how to handle ARSession interruptions elegantly when your app goes to the background. You’ll then explore how hit-testing with an ARSCNView works as you start adding objects to the detected horizontal plane in the device’s surroundings. For adding virtual objects, you’ll use ARAnchors and SCNNode objects to define their position and geometry. • Chapter 9: Geometry, Textures & Lighting: In the final chapter of this section, you’ll first dive deeper to understand SceneKit’s coordinate system and materials. Next, you’ll use SCNNode objects with different geometries and attach textures to them to create the walls, floor and ceiling of your portal. Finally, you’ll make your portal appear realistic by adding lighting. • Chapter 10: Detecting Placeholders: In the first chapter of this section, you’ll start with detecting a rectangular placeholder. You’ll display some place markers above its edges and then a plane on top of it — although initially it won’t be oriented correctly. • Chapter 11: Beginning User Interaction: In the second chapter, you’ll fix the plane orientation issue so that it’s parallel to and covering the detected placeholder. However, this is only the beginning. You’ll also replace arbitrary rectangle detection with QR code detection, and then you’ll add some interactive content to the rectangular plane; first an image, then a carousel, and finally a video player. • Chapter 12: Advanced User Interaction: In this chapter, you're going to improve user interaction by using storyboards instead of standalone view controllers. You’ll add a full screen mode for the interactive content, and you’ll learn an alternative detection method: image detection. • Chapter 13: Location Tracking & Beacons: The last chapter in this section focuses on using location and proximity features to monitor the user’s location, geofencing to detect when they enter or leave a monitored area and beacons to detect when they’re very close to a predefined location. • Chapter 14: Getting Started with Face-Based AR: Start your journey with FaceBased AR here! In this chapter, you’ll discover the four primary features that ARKit provides for face tracking. You’ll also learn how to add face-base session tracking to your own iOS apps. raywenderlich.com 65 ARKit by Tutorials Where to Go From Here? • Chapter 15: Tracking the User’s Face: Learn how to work with ARFaceAnchor objects, and get a brief overview of how the face position and orientation work within a SceneKit scene. Use different techniques to change the appearance of things. • Chapter 16: Creating Assets for Face-Based AR: Learn how to create 3D content using 3D Text, and discover how you can use occlusion geometry to obscure parts of the user’s face. • Chapter 17: Working with Blend Shapes: Make your own Pig Animoji! Discover how you can track specific user expressions using 53 different blend shapes, like jawOpen, eyeBlinkLeft and eyeBlinkRight. • Chapter 18: Recording Your ARKit Experience with ReplayKit: Extend your app with ReplayKit. Using RPScreenRecorder and RPPreviewViewController, you can record, preview, edit and share your AR experiences. • Chapter 19: Beginning Game Physics: Here, you’ll learn more about SceneKit’s built-in physics simulation capabilities. SceneKit has decent vehicle physics simulation right out-of-the-box, and you’ll be using it to build a fun, little remotecontrolled AR monster truck. • Chapter 20: Advanced Game Physics: By this point, you’ve got the Monster Truck built, but it’s not doing much — at least not yet. In this chapter, you’ll add the finishing touches, like throttle control, braking and steering — you know, everything that you’d need to control a little monster truck. • Chapter 21: World Tracking & Persistent AR Experiences: This chapter starts off with a fun Sketch app. You’ll explore the concept of an ARWorldMap to save custom ARAnchor objects in your session and how to save its space-mapping to local storage on the device. In this process, you’ll learn how to find the best time to capture a world map to enable reliable restoration later on. After that, you’ll add functionality to load the world map from a file and enable the user to assist with relocalizing the data. Finally, you’ll restore custom nodes from saved anchors to recreate the AR content from a previous session. • Chapter 22: Shared AR Experiences: In the second chapter of this section, you’ll dive deep into understanding the Multipeer Connectivity framework, an Apple framework used to connect two or more devices. This chapter starts off by demonstrating how to browse for nearby devices and send a world map from one device to all others in an MCSession. raywenderlich.com 66 ARKit by Tutorials Where to Go From Here? Next, you’ll learn how to handle received world map data from another device, and how to parse and relocalize that data to restore the contents of the shared ARSession. Finally, you’ll tie together learnings from both chapters to save, restore and share the data on a real-time basis, creating a multi-user Sketch app. • Chapter 23: Introducing USDZ & AR Quick Look: In this chapter, you’ll learn about Apple’s new USDZ file format and walk through converting 3D models into this new, universal format. After converting, you’ll explore integrating AR Quick Look into your existing web pages and apps to enable them to display USDZ content in stunning augmented reality. • Chapter 24: Detecting Images & 3D Objects: If you need a word to describe this chapter, here it is: detection. You'll learn how to detect 2D images and track their position when moving. You’ll also take a look at how to detect 3D objects. The full book is now available for purchase: • www.store.raywenderlich.com/products/arkit-by-tutorials. We hope you enjoy the book! — The ARKit by Tutorials team raywenderlich.com 67 ARKit by Tutorials raywenderlich.com Where to Go From Here? 68