- iOS 11.0+
- Xcode 9.0+
- Swift 4.0+
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.
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.
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
}
}
}
}
}
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)
}
}
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 }
}
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 []
}
}
}