Last Chance: RW Accelerator Bootcamps

Bootcamps start August 15th — Don't miss out!
Save 50% and go from novice coder to job-ready mobile developer in just 12 weeks.

Home iOS & Swift Tutorials

Building a Portal App in ARKit: Materials and Lighting

Learn how to add materials and lighting effects to your AR portal app with the final tutorial in this series taken from our book, ARKIt by Tutorials!

3.5/5 2 Ratings


  • Swift 4, iOS 11, Xcode 9

This is an excerpt taken from Chapter 9, “Materials and Lighting”, of our book ARKit by Tutorials. This book show you how to build five immersive, great-looking AR apps in ARKit, Apple’s augmented reality framework. Enjoy!

In the first and second parts of this three-part tutorial series on ARKit, you learned how to add 3D objects to your scene with SceneKit. Now it’s time to put that knowledge to use and build the full portal. In this tutorial, 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

Download the materials for this tutorial using the link at the top, then load up the starter project from the starter folder. Before you begin, you’ll need to know a little bit about how SceneKit works.

The SceneKit Coordinate System

As you saw in the previous part in this tutorial series, 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.

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.


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 which can be used to make the material look more realistic.

The SceneKit asset catalog is designed specifically 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 in to 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

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.

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 is rendered. You set the transparency to a very low value so the object is hidden from view.
  1. 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()

  // 3
  let innerFloor = SCNBox(width: SURFACE_WIDTH,
                          height: SURFACE_HEIGHT,
                          length: SURFACE_LENGTH,
                          chamferRadius: 0)
  // 4
  innerFloor.firstMaterial?.lightingModel = .physicallyBased
  innerFloor.firstMaterial?.diffuse.contents =
  innerFloor.firstMaterial?.normal.contents =
  innerFloor.firstMaterial?.roughness.contents =
  innerFloor.firstMaterial?.specular.contents =
  innerFloor.firstMaterial?.selfIllumination.contents =
  // 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)
  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:


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
  return portal

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,
                          length: SURFACE_LENGTH)
  // 2                                            
  outerCeilingNode.position = SCNVector3(SURFACE_HEIGHT * 0.5,
                                         SURFACE_HEIGHT, 0)
  let ceilingNode = SCNNode()

  // 3
  let innerCeiling = SCNBox(width: SURFACE_WIDTH,
                            height: SURFACE_HEIGHT,
                            length: SURFACE_LENGTH,
                            chamferRadius: 0)
  // 4                            
  innerCeiling.firstMaterial?.lightingModel = .physicallyBased
  innerCeiling.firstMaterial?.diffuse.contents =
  innerCeiling.firstMaterial?.emission.contents =
  innerCeiling.firstMaterial?.normal.contents =
  innerCeiling.firstMaterial?.specular.contents =
  innerCeiling.firstMaterial?.selfIllumination.contents =
  // 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)
  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.
  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,
// 2
  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 the center of the ceiling is from the camera along the Z axis.

  2. Add ceilingNode as a child of the portal.

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()

  // 5
  let innerWall = SCNBox(width: WALL_WIDTH,
                         height: height,
                         length: length,
                         chamferRadius: 0)
  // 6                       
  innerWall.firstMaterial?.lightingModel = .physicallyBased
  innerWall.firstMaterial?.diffuse.contents =
  innerWall.firstMaterial?.metalness.contents =
  innerWall.firstMaterial?.roughness.contents =
  innerWall.firstMaterial?.normal.contents =
  innerWall.firstMaterial?.specular.contents =
  innerWall.firstMaterial?.selfIllumination.contents =

  // 7
  let innerWallNode = SCNNode(geometry: innerWall)
  return wallNode

Going over the code step-by-step:

  1. You create an outerWall node which 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 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.
  1. 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,

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.

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,

// 4
let leftSideWallNode = makeWallNode(maskLowerSide: true)

// 5
leftSideWallNode.position = SCNVector3(-WALL_LENGTH*0.5,

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.

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

  // 2
  let rightDoorSideNode = makeWallNode(length: frontHalfWallLength)
  rightDoorSideNode.eulerAngles = SCNVector3(0,270.0.degreesToRadians, 0)
  rightDoorSideNode.position = SCNVector3(halfWallLength - 0.5 * DOOR_WIDTH,

  // 3
  let leftDoorSideNode = makeWallNode(length: frontHalfWallLength)
  leftDoorSideNode.eulerAngles = SCNVector3(0, 270.0.degreesToRadians, 0)
  leftDoorSideNode.position = SCNVector3(-halfWallLength + 0.5 * frontHalfWallLength,

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 makePortalNode(), add the following just before return portal:

addDoorway(node: portal)

Here you add the doorway to the portal node.

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 =
  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.
  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. In fact, 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
  lightNode.position = SCNVector3(0,

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!

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 tutorial series.

  • 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.

But don’t stop here. Let your sci-fi imagination run wild!

If you enjoyed what you learned in this tutorial, why not check out our complete book, ARKit by Tutorials, available on our online store?

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.

If you’ve worked with any of Apple’s other frameworks, you’re probably expecting that it will take a long time to get things working. But with ARKit, it only takes a few lines of code — ARKit does most of the the heavy lifting for you, so you can focus on what’s important: creating an immersive and engaging AR experience.

In 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.

And, as part of AR/VR Week, it’s currently on sale. But don’t wait too long, as this deal is only good until Friday, March 29th!

If you have any questions or comments on this tutorial, feel free to join the discussion below!




More like this