Skip to content

Instantly share code, notes, and snippets.

@heyflavio
Last active October 15, 2020 10:14
Show Gist options
  • Save heyflavio/af162d740a11e9a3517e4a0ea4adc997 to your computer and use it in GitHub Desktop.
Save heyflavio/af162d740a11e9a3517e4a0ea4adc997 to your computer and use it in GitHub Desktop.
Decora's ARKit basic setup

iOS ARKit - Decora's virtual objects setup

Software requirements

  • iOS 11.0+
  • Xcode 9.0+
  • Swift 4.0+

Before starting

Zip file organization

All zip contents are disposed in its root path. If the zip contains a .DAE file, its contents will be provided as followed:

  • 12345678.DAE
  • 12345678_DIFFUSE.png
  • 12345678_METALLIC.png
  • 12345678_NORMAL.png
  • 12345678_ROUGHNESS.png
  • 12345678_TRANSPARENT.png
  • 12345678_AO.png
  • 12345678_ILLUMINATION.png

Or, if contains a .obj file:

  • 12345678.obj
  • 12345678_DIFFUSE.png
  • 12345678_METALLIC.png
  • 12345678_NORMAL.png
  • 12345678_ROUGHNESS.png
  • 12345678_TRANSPARENT.png
  • 12345678_AO.png
  • 12345678_ILLUMINATION.png
  • 12345678.mtl

Once you unzip the downloaded file and stored its destination URL path, you are ready to setup the virtual object.

Setup

Importing virtual objects

There are two possible approaches to add a virtual object in a node, depending on the model file extension (.DAE or .OBJ). In both, is required to have the file path URL:

import SceneKit
import SceneKit.ModelIO

    var node: SCNNode
    let virtualObjectURL: URL
        
    if isDAEFile {
        let referenceNode = SCNReferenceNode(url: virtualObjectURL)!
        referenceNode.load()
        node = referenceNode
    } else {
        let asset = MDLAsset(url: virtualObjectURL!)
        node = SCNNode(mdlObject: asset.object(at: 0))
    }
    
    node.setUniformScale(0.01)
extension SCNNode {
   
   func setUniformScale(_ scale: Float) {
       self.simdScale = float3(scale, scale, scale)
   }

}

The uniform scale is performed due to adjust the correct size of the object in the scene, once the model file property sizes are computed in centimeters, and ARKit scales every element in meters.

Adjusting virtual objects materials

In order to set the material's textures and the physically based lighting model, execute the function below, passing the node where the object is inserted, and the URL's for the textures local file paths (e.g. .png, .jpeg):

    func setPhysicallyBasedOnMaterials(for node: SCNNode, with textureURLs: [URL]) {
        node.childNodes.forEach { childNode in
            
            guard let geometry = childNode.geometry else {
                setPhysicallyBasedOnMaterials(for: childNode)
                return
            }
            
            geometry.materials.forEach { material in
                
                material.lightingModel = .physicallyBased
                material.isDoubleSided = true
                
                textureURLs.forEach {
                    guard let imageData = try? Data(contentsOf: $0) else { return }
                    
                    if $0.absoluteString.contains("_DIFFUSE") {
                        material.diffuse.contents = UIImage(data: imageData)
                    } else if $0.absoluteString.contains("_ROUGHNESS") {
                        material.roughness.contents = UIImage(data: imageData)
                    } else  if $0.absoluteString.contains("_METALLIC") {
                        material.metalness.contents = UIImage(data: imageData)
                    } else if $0.absoluteString.contains("_NORMAL") {
                        material.normal.contents = UIImage(data: imageData)
                    } else if $0.absoluteString.contains("_AO") {
                        material.ambientOcclusion.contents = UIImage(data: imageData)
                    } else if $0.absoluteString.contains("_ILLUMINATION") {
                        material.selfIllumination.contents = UIImage(data: imageData)
                    } else if $0.absoluteString.contains("_TRANSPARENT") {
                        material.transparent.contents = UIImage(data: imageData)
                        material.transparencyMode = .rgbZero
                    }
                }
            }
        }
    }

Other node related interesting functions

To get the node's bounding box size:

let nodeVectorSize = node.boundingBoxSize()
extension SCNBoundingVolume {
    
    func boundingBoxSize() -> SCNVector3 {
        return SCNVector3(self.boundingBox.max.x - self.boundingBox.min.x,
                          self.boundingBox.max.y - self.boundingBox.min.y,
                          self.boundingBox.max.z - self.boundingBox.min.z)
    }
}

To create a selection base:

let nodeVectorSize = node.boundingBoxSize()
let baseNode = getBaseNode(with: nodeVectorSize)
    func getBaseNode(with vector: SCNVector3, scale: Float = 1.0) -> SCNNode {
        let thickness = thicknessValue(for: scale)
        let baseWidth = baseWidthValue(for: vector, scale: scale, borderVisible: true)
        let baseHeight = baseHeightValue(for: vector, scale: scale, borderVisible: true) 
        let baseMinorWidth = baseWidth - thickness
        let baseMinorHeight = baseHeight - thickness
        
        let bezierPath = UIBezierPath()
        bezierPath.move(to: CGPoint(x: baseMinorWidth, y: thickness))
        bezierPath.addLine(to: CGPoint(x: thickness, y: thickness))
        bezierPath.addLine(to: CGPoint(x: thickness, y: baseMinorHeight))
        bezierPath.addLine(to: CGPoint(x: baseMinorWidth, y: baseMinorHeight))
        bezierPath.addLine(to: CGPoint(x: baseMinorWidth, y: thickness))
        bezierPath.close()
        bezierPath.move(to: CGPoint(x: baseWidth, y: 0))
        bezierPath.addLine(to: CGPoint(x: baseWidth, y: baseHeight))
        bezierPath.addLine(to: CGPoint(x: 0, y: baseHeight))
        bezierPath.addLine(to: CGPoint(x: 0, y: 0))
        bezierPath.addLine(to: CGPoint(x: baseWidth, y: 0))
        bezierPath.addLine(to: CGPoint(x: baseWidth, y: 0))
        bezierPath.close()
        
        let shape = SCNShape(path: bezierPath, extrusionDepth: CGFloat(0.25 * scale))
        let baseNode = SCNNode(geometry: shape)
        baseNode.simdPosition = float3(Float(-(baseWidth/2)),
                                       0.0,
                                       Float(-(baseHeight/2)))
        baseNode.eulerAngles = SCNVector3(90.0.degreesToRadians, 0.0, 0.0)
        baseNode.castsShadow = false
        
        let baseMaterial = baseNode.geometry!.firstMaterial!
        baseMaterial.diffuse.contents = UIColor.red.withAlphaComponent(0.7)
        baseMaterial.reflective.contents = UIColor.red
        baseMaterial.multiply.contents = UIColor.black
        baseMaterial.lightingModel = .physicallyBased
        baseMaterial.isDoubleSided = true
        
        return baseNode
    }

    func thicknessValue(for scale: Float) -> CGFloat {
        return CGFloat(6.0 * scale)
    }
    
    func baseWidthValue(for vector: SCNVector3, scale: Float, borderVisible: Bool) -> CGFloat {
        return CGFloat(vector.x * scale) + (borderVisible ? thicknessValue(for: scale) : 0.0)
    }
    
    func baseHeightValue(for vector: SCNVector3, scale: Float, borderVisible: Bool) -> CGFloat {
        return CGFloat(vector.z * scale) + (borderVisible ? thicknessValue(for: scale) : 0.0)
    }
    extension FloatingPoint {
        var degreesToRadians: Self { return self * .pi / 180 }
        var radiansToDegrees: Self { return self * 180 / .pi }
    }

Helper functions

To get the file URL's with an specific file extension in a given URL:

    let filesURLs = FileManager().files(atPath: "any/path/to/files", withExtension: "anyExtension")
extension FileManager {

    func files(atPath path: URL, withExtension fileExtension: String? = nil) -> [URL] {
        do {
            let directoryContents = try FileManager.default.contentsOfDirectory(at: path,
                                                                                includingPropertiesForKeys: nil,
                                                                                options: [])
            if fileExtension != nil {
                return directoryContents.filter{ $0.pathExtension == fileExtension }
            }
            
            return directoryContents
            
        } catch {
            return []
        }
    }

}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment