Image Anchor App
For the Image Anchor exercise, I chose to use Starbucks logo as the target to trigger AR contents. I was inspired by how I used to always draw over Starbucks logo on their paper cups as a young kid. So this time I decided to add an overlay on the Starbucks logo through augmented reality. I initially added a skeleton drinking coffee gif and a 3D text mesh that would show up once the image anchor were detected. Later I was playing with the mermaid figure itself. I created my own gif with the mermaid dancing by distorting it every frame and adding the moving mouth overlay in Procreate. I was also playing with the iPhone built-in sensors in this assignment: once the ambient light is below 300, a warning stating "Too Dark" will show up; if the microphone detected loud sound, the mermaid will start spinning.






https://youtu.be/i2x3s2_SZ_w
//
// ContentView.swift
// ImageAnchor
//
// Created by Nien Lam on 9/21/21.
// Copyright © 2021 Line Break, LLC. All rights reserved.
//
import SwiftUI
import ARKit
import RealityKit
import Combine
import CoreMotion
import AVFoundation
import CoreAudio
// MARK: - View model for handling communication between the UI and ARView.
class ViewModel: ObservableObject {
let uiSignal = PassthroughSubject<UISignal, Never>()
// Variable for tracking ambient light intensity.
@Published var ambientIntensity: Double = 0
enum UISignal {
}
}
// MARK: - UI Layer.
struct ContentView : View {
@StateObject var viewModel: ViewModel
var body: some View {
ZStack {
// AR View.
ARViewContainer(viewModel: viewModel)
//Text("\\(viewModel.ambientIntensity)")
if viewModel.ambientIntensity < 300 {
Text("Too Dark")
.font(.system(size: 72, weight: .heavy, design: .serif))
.italic()
.bold()
.foregroundColor(.red)
.padding()
.border(Color.yellow)
.background(Color.indigo)
}
}
.edgesIgnoringSafeArea(.all)
.statusBar(hidden: true)
}
}
// MARK: - AR View.
struct ARViewContainer: UIViewRepresentable {
let viewModel: ViewModel
func makeUIView(context: Context) -> ARView {
SimpleARView(frame: .zero, viewModel: viewModel)
}
func updateUIView(_ uiView: ARView, context: Context) {}
}
class SimpleARView: ARView, ARSessionDelegate {
var viewModel: ViewModel
var arView: ARView { return self }
var subscriptions = Set<AnyCancellable>()
// Dictionary for tracking image anchors.
var imageAnchorToEntity: [ARImageAnchor: AnchorEntity] = [:]
// Materials array for animation.
var materialsArray = [RealityKit.Material]()
// Index for animation.
var materialIdx = 0
// Variable adjust animated texture timing.
var lastUpdateTime = Date()
// Using plane entity for animation.
var planeEntity: ModelEntity?
// Example box entity.
var boxEntity: ModelEntity?
var textEntity: ModelEntity?
// Motion manager for tracking movement.
let motionManager = CMMotionManager()
// Recorder for microphone usage.
var recorder: AVAudioRecorder!
init(frame: CGRect, viewModel: ViewModel) {
self.viewModel = viewModel
super.init(frame: frame)
}
required init?(coder decoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
required init(frame frameRect: CGRect) {
fatalError("init(frame:) has not been implemented")
}
override func didMoveToSuperview() {
super.didMoveToSuperview()
UIApplication.shared.isIdleTimerDisabled = true
setupMotionManager()
setupMicrophoneSensor()
setupScene()
setupMaterials()
}
func setupMotionManager() {
motionManager.startAccelerometerUpdates()
motionManager.startGyroUpdates()
motionManager.startMagnetometerUpdates()
motionManager.startDeviceMotionUpdates()
}
func setupMicrophoneSensor() {
let documents = URL(fileURLWithPath: NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.documentDirectory, FileManager.SearchPathDomainMask.userDomainMask, true)[0])
let url = documents.appendingPathComponent("record.caf")
let recordSettings: [String: Any] = [
AVFormatIDKey: kAudioFormatAppleIMA4,
AVSampleRateKey: 44100.0,
AVNumberOfChannelsKey: 2,
AVEncoderBitRateKey: 12800,
AVLinearPCMBitDepthKey: 16,
AVEncoderAudioQualityKey: AVAudioQuality.max.rawValue
]
let audioSession = AVAudioSession.sharedInstance()
do {
try audioSession.setCategory(AVAudioSession.Category.playAndRecord)
try audioSession.setActive(true)
try recorder = AVAudioRecorder(url:url, settings: recordSettings)
} catch {
return
}
recorder.prepareToRecord()
recorder.isMeteringEnabled = true
recorder.record()
}
func setupScene() {
// Setup world tracking and plane detection.
let configuration = ARImageTrackingConfiguration()
arView.renderOptions = [.disableDepthOfField, .disableMotionBlur]
// TODO: Update target image and physical width in meters. //////////////////////////////////////
let targetImage = "starbucks.png"
let physicalWidth = 0.1524
if let refImage = UIImage(named: targetImage)?.cgImage {
let arReferenceImage = ARReferenceImage(refImage, orientation: .up, physicalWidth: physicalWidth)
var set = Set<ARReferenceImage>()
set.insert(arReferenceImage)
configuration.trackingImages = set
} else {
print("❗️ Error loading target image")
}
arView.session.run(configuration)
// Called every frame.
scene.subscribe(to: SceneEvents.Update.self) { event in
// Call renderLoop method on every frame.
self.renderLoop()
}.store(in: &subscriptions)
// Process UI signals.
viewModel.uiSignal.sink { [weak self] in
self?.processUISignal($0)
}.store(in: &subscriptions)
// Set session delegate.
arView.session.delegate = self
}
// Hide/Show active tetromino.
func processUISignal(_ signal: ViewModel.UISignal) {
}
func session(_ session: ARSession, didAdd anchors: [ARAnchor]) {
anchors.compactMap { $0 as? ARImageAnchor }.forEach {
// Create anchor from image.
let anchorEntity = AnchorEntity(anchor: $0)
// Track image anchors added to scene.
imageAnchorToEntity[$0] = anchorEntity
// Add anchor to scene.
arView.scene.addAnchor(anchorEntity)
// Call setup method for entities.
// IMPORTANT: Play USDZ animations after entity is added to the scene.
setupEntities(anchorEntity: anchorEntity)
}
}
func session(_ session: ARSession, didUpdate frame: ARFrame) {
if let intensity = frame.lightEstimate?.ambientIntensity {
viewModel.ambientIntensity = intensity
}
}
// TODO: Do any material setup work. //////////////////////////////////////
func setupMaterials() {
// Create array of materials holding horse textures.
for idx in 1...74 {
var unlitMaterial = UnlitMaterial()
let imageNamed = "IMG_\\(idx).PNG"
unlitMaterial.color.texture = UnlitMaterial.Texture.init(try! .load(named: imageNamed))
unlitMaterial.color.tint = UIColor.white.withAlphaComponent(0.999999)
materialsArray.append(unlitMaterial)
}
}
// TODO: Setup entities. //////////////////////////////////////
// IMPORTANT: Attach to anchor entity. Called when image target is found.
func setupEntities(anchorEntity: AnchorEntity) {
// Checker material.
var checkerMaterial = PhysicallyBasedMaterial()
let texture = PhysicallyBasedMaterial.Texture.init(try! .load(named: "velvet.jpeg"))
checkerMaterial.baseColor.tint = .green
checkerMaterial.baseColor.texture = texture
checkerMaterial.roughness = PhysicallyBasedMaterial.Roughness(floatLiteral: 0.1)
checkerMaterial.metallic = PhysicallyBasedMaterial.Metallic(floatLiteral: 1)
checkerMaterial.emissiveColor = PhysicallyBasedMaterial.EmissiveColor(color: .blue, texture: texture)
checkerMaterial.emissiveIntensity = 1
// Setup example box entity.
let textMesh = MeshResource.generateText("Drink Coffee", extrusionDepth: 0.02, font: .systemFont(ofSize: 0.05) , containerFrame: CGRect.zero, alignment: .right)
//let boxMesh = MeshResource.generateBox(size: [0.1, 0.1, 0.1], cornerRadius: 0.0)
boxEntity = ModelEntity(mesh: textMesh, materials: [checkerMaterial])
// Position and add box entity to anchor.
boxEntity?.position.x = -0.165
boxEntity?.position.y = -0.02
boxEntity?.position.z = 0.12
anchorEntity.addChild(boxEntity!)
// Setup, position and add plane entity to anchor.
planeEntity = try! Entity.loadModel(named: "plane.usda")
planeEntity?.scale = [0.43, 0.43, 0.43]
planeEntity?.position.x = 0
planeEntity?.position.y = 0
planeEntity?.position.z = 0
planeEntity?.orientation *= simd_quatf(angle: -1.4 , axis: [1,0,0])
anchorEntity.addChild(planeEntity!)
}
// TODO: Animate entities. //////////////////////////////////////
func renderLoop() {
// Time interval from last animated material update.
let currentTime = Date()
let timeInterval = currentTime.timeIntervalSince(lastUpdateTime)
// Animate material every 1 / 15 of second.
if timeInterval > 1 / 15 {
// Cycle material index.
materialIdx = (materialIdx < materialsArray.count - 1) ? materialIdx + 1 : 0
// Get and set material from array.
let material = materialsArray[materialIdx]
planeEntity?.model?.materials = [material]
// Remember last update time.
lastUpdateTime = currentTime
}
// Spin boxEntity.
boxEntity?.orientation *= simd_quatf(angle: 0.1, axis: [1, 0, 0])
////////////////////////////////////////////////////////////////////////////
// Sensor: Ambient light intensity
print("ambientIntensity: ", viewModel.ambientIntensity)
//
// var textMaterial = PhysicallyBasedMaterial()
// let texture = PhysicallyBasedMaterial.Texture.init(try! .load(named: "red.jpeg"))
// textMaterial.baseColor.tint = .red
// textMaterial.baseColor.texture = texture
// textMaterial.roughness = PhysicallyBasedMaterial.Roughness(floatLiteral: 0.1)
// textMaterial.metallic = PhysicallyBasedMaterial.Metallic(floatLiteral: 1)
// let tooDark = MeshResource.generateText("TOO DARK", extrusionDepth: 0.02, font: .systemFont(ofSize: 0.05) , containerFrame: CGRect.zero, alignment: .right)
// textEntity = ModelEntity(mesh: tooDark, materials: [textMaterial])
//
// if viewModel.ambientIntensity < 300 {
// Text("hello")
// }
////////////////////////////////////////////////////////////////////////////ƒ
// Sensor: Accelerometer data
// if let accelerometerData = motionManager.accelerometerData {
// print("accelerometerData x: ", accelerometerData.acceleration.x)
// print("accelerometerData y: ", accelerometerData.acceleration.y)
// print("accelerometerData z: ", accelerometerData.acceleration.z)
// }
// Other motion sensor data.
/*
if let gyroData = motionManager.gyroData {
print(gyroData.rotationRate.x)
}
if let magnetometerData = motionManager.magnetometerData {
print(magnetometerData)
}
if let deviceMotion = motionManager.deviceMotion {
print(deviceMotion.userAcceleration)
}
*/
////////////////////////////////////////////////////////////////////////////
//Sensor: Decibel power
recorder.updateMeters()
let decibelPower = recorder.averagePower(forChannel: 0)
print("decibelPower: ", decibelPower)
if decibelPower > -20 {
planeEntity?.orientation *= simd_quatf(angle: 0.1, axis: [0, 1, 0])
}
}
}
Tetrominoes Controller Exercise
//
// ContentView.swift
// TetrominoesController
//
// Created by Nien Lam on 9/21/21.
// Copyright © 2021 Line Break, LLC. All rights reserved.
//
import SwiftUI
import ARKit
import RealityKit
import Combine
// MARK: - View model for handling communication between the UI and ARView.
class ViewModel: ObservableObject {
let uiSignal = PassthroughSubject<UISignal, Never>()
@Published var positionLocked = false
enum UISignal {
case straightSelected
case squareSelected
case tSelected
case lSelected
case skewSelected
case moveLeft
case moveRight
case rotateCCW
case rotateCW
case lockPosition
}
}
// MARK: - UI Layer.
struct ContentView : View {
@StateObject var viewModel: ViewModel
var body: some View {
ZStack {
// AR View.
ARViewContainer(viewModel: viewModel)
// Left / Right controls.
HStack {
HStack {
Button {
viewModel.uiSignal.send(.moveLeft)
} label: {
buttonIcon("arrow.left", color: .blue)
}
}
HStack {
Button {
viewModel.uiSignal.send(.moveRight)
} label: {
buttonIcon("arrow.right", color: .blue)
}
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)
.padding(.horizontal, 30)
// Rotation controls.
HStack {
HStack {
Button {
viewModel.uiSignal.send(.rotateCCW)
} label: {
buttonIcon("rotate.left", color: .red)
}
}
HStack {
Button {
viewModel.uiSignal.send(.rotateCW)
} label: {
buttonIcon("rotate.right", color: .red)
}
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .trailing)
.padding(.horizontal, 30)
// Lock release button.
Button {
viewModel.uiSignal.send(.lockPosition)
} label: {
Label("Lock Position", systemImage: "target")
.font(.system(.title))
.foregroundColor(.white)
.labelStyle(IconOnlyLabelStyle())
.frame(width: 44, height: 44)
.opacity(viewModel.positionLocked ? 0.25 : 1.0)
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottomLeading)
.padding(.bottom, 30)
// Bottom buttons.
HStack {
Button {
viewModel.uiSignal.send(.straightSelected)
} label: {
tetrominoIcon("straight", color: Color(red: 0, green: 1, blue: 1))
}
Button {
viewModel.uiSignal.send(.squareSelected)
} label: {
tetrominoIcon("square", color: .yellow)
}
Button {
viewModel.uiSignal.send(.tSelected)
} label: {
tetrominoIcon("t", color: .purple)
}
Button {
viewModel.uiSignal.send(.lSelected)
} label: {
tetrominoIcon("l", color: .orange)
}
Button {
viewModel.uiSignal.send(.skewSelected)
} label: {
tetrominoIcon("skew", color: .green)
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottom)
.padding(.bottom, 30)
}
.edgesIgnoringSafeArea(.all)
.statusBar(hidden: true)
}
// Helper methods for rendering icons.
func tetrominoIcon(_ image: String, color: Color) -> some View {
Image(image)
.resizable()
.padding(3)
.frame(width: 44, height: 44)
.background(color)
.cornerRadius(5)
}
func buttonIcon(_ systemName: String, color: Color) -> some View {
Image(systemName: systemName)
.resizable()
.padding(10)
.frame(width: 44, height: 44)
.foregroundColor(.white)
.background(color)
.cornerRadius(5)
}
}
// MARK: - AR View.
struct ARViewContainer: UIViewRepresentable {
let viewModel: ViewModel
func makeUIView(context: Context) -> ARView {
SimpleARView(frame: .zero, viewModel: viewModel)
}
func updateUIView(_ uiView: ARView, context: Context) {}
}
class SimpleARView: ARView {
var viewModel: ViewModel
var arView: ARView { return self }
var originAnchor: AnchorEntity!
var subscriptions = Set<AnyCancellable>()
// Empty entity for cursor.
var cursor: Entity!
// Scene lights.
var directionalLight: DirectionalLight!
// Reference to entity pieces.
// This needs to be set in the setup.
var straightPiece: Entity!
var squarePiece: Entity!
var tPiece: Entity!
var lPiece: Entity!
var skewPiece: Entity!
// The selected tetromino.
var activeTetromino: Entity?
init(frame: CGRect, viewModel: ViewModel) {
self.viewModel = viewModel
super.init(frame: frame)
}
required init?(coder decoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
required init(frame frameRect: CGRect) {
fatalError("init(frame:) has not been implemented")
}
override func didMoveToSuperview() {
super.didMoveToSuperview()
UIApplication.shared.isIdleTimerDisabled = true
setupScene()
setupEntities()
disablePieces()
}
func setupScene() {
// Setup world tracking and plane detection.
let configuration = ARWorldTrackingConfiguration()
configuration.planeDetection = [.horizontal, .vertical]
configuration.environmentTexturing = .automatic
arView.renderOptions = [.disableDepthOfField, .disableMotionBlur]
arView.session.run(configuration)
// Called every frame.
scene.subscribe(to: SceneEvents.Update.self) { event in
if !self.viewModel.positionLocked {
self.updateCursor()
}
}.store(in: &subscriptions)
// Process UI signals.
viewModel.uiSignal.sink { [weak self] in
self?.processUISignal($0)
}.store(in: &subscriptions)
}
// Hide/Show active tetromino & process controls.
func processUISignal(_ signal: ViewModel.UISignal) {
switch signal {
case .straightSelected:
disablePieces()
clearActiveTetrominoTransform()
straightPiece.isEnabled = true
activeTetromino = straightPiece
case .squareSelected:
disablePieces()
clearActiveTetrominoTransform()
squarePiece.isEnabled = true
activeTetromino = squarePiece
case .tSelected:
disablePieces()
clearActiveTetrominoTransform()
tPiece.isEnabled = true
activeTetromino = tPiece
case .lSelected:
disablePieces()
clearActiveTetrominoTransform()
lPiece.isEnabled = true
activeTetromino = lPiece
case .skewSelected:
disablePieces()
clearActiveTetrominoTransform()
skewPiece.isEnabled = true
activeTetromino = skewPiece
case .lockPosition:
disablePieces()
viewModel.positionLocked.toggle()
case .moveLeft:
moveLeftPressed()
case .moveRight:
moveRightPressed()
case .rotateCCW:
rotateCCWPressed()
case .rotateCW:
rotateCWPressed()
}
}
func disablePieces() {
straightPiece.isEnabled = false
squarePiece.isEnabled = false
tPiece.isEnabled = false
lPiece.isEnabled = false
skewPiece.isEnabled = false
}
func clearActiveTetrominoTransform() {
activeTetromino?.transform = Transform.identity
}
// Move cursor to plane detected.
func updateCursor() {
// Raycast to get cursor position.
let results = raycast(from: center,
allowing: .existingPlaneGeometry,
alignment: .any)
// Move cursor to position if hitting plane.
if let result = results.first {
cursor.isEnabled = true
cursor.move(to: result.worldTransform, relativeTo: originAnchor)
} else {
cursor.isEnabled = false
}
}
func setupEntities() {
// Create an anchor at scene origin.
originAnchor = AnchorEntity(world: .zero)
arView.scene.addAnchor(originAnchor)
// Create and add empty cursor entity to origin anchor.
cursor = Entity()
originAnchor.addChild(cursor)
// Add directional light.
directionalLight = DirectionalLight()
directionalLight.light.intensity = 1000
directionalLight.look(at: [0,0,0], from: [1, 1.1, 1.3], relativeTo: originAnchor)
directionalLight.shadow = DirectionalLightComponent.Shadow(maximumDistance: 0.5, depthBias: 2)
originAnchor.addChild(directionalLight)
// Add checkerboard plane.
var checkerBoardMaterial = PhysicallyBasedMaterial()
checkerBoardMaterial.baseColor.texture = .init(try! .load(named: "checker-board.png"))
let checkerBoardPlane = ModelEntity(mesh: .generatePlane(width: 0.5, depth: 0.5), materials: [checkerBoardMaterial])
cursor.addChild(checkerBoardPlane)
// Create an relative origin entity above the checkerboard.
let relativeOrigin = Entity()
relativeOrigin.position.x = 0.05 / 2
relativeOrigin.position.z = 0.05 / 2
relativeOrigin.position.y = 0.05 * 2.5
cursor.addChild(relativeOrigin)
// TODO: Refactor code using TetrominoEntity Classes. ////////////////////////////////////////////
straightPiece = TetrominoEntity(color: .cyan, name: "straight")
relativeOrigin.addChild(straightPiece)
squarePiece = TetrominoEntity(color: .yellow, name: "square")
relativeOrigin.addChild(squarePiece)
tPiece = TetrominoEntity(color: .purple, name: "T")
relativeOrigin.addChild(tPiece)
lPiece = TetrominoEntity(color: .orange, name: "L")
relativeOrigin.addChild(lPiece)
skewPiece = TetrominoEntity(color: .green, name: "skew")
relativeOrigin.addChild(skewPiece)
//////////////////////////////////////////////////////////////////////////////////////////////////
}
// TODO: Implement controls to move and rotate tetromino.
//
// IMPORTANT: Use optional activeTetromino variable for movement and rotation.
//
// e.g. activeTetromino?.position.x
func moveLeftPressed() {
print("🔺 Did press move left")
let boxSize: Float = 0.05
let bound: Float = -0.20
//let newPos: Float = activeTetromino?.position.x ?? 0()!
if activeTetromino?.position.x ?? 0 > bound {
activeTetromino?.position.x -= boxSize
}
}
func moveRightPressed() {
print("🔺 Did press move right")
let boxSize: Float = 0.05
let bound: Float = 0.20
if activeTetromino?.position.x ?? 0 < bound {
activeTetromino?.position.x += boxSize
}
}
func rotateCCWPressed() {
print("🔺 Did press rotate CCW")
let orientation = simd_quatf(angle: Float.pi / 2, axis: [0, 0, 1])
let transform = Transform(rotation: orientation)
activeTetromino?.transform.matrix *= transform.matrix
}
func rotateCWPressed() {
print("🔺 Did press rotate CW")
let orientation = simd_quatf(angle: -Float.pi / 2, axis: [0, 0, 1])
let transform = Transform(rotation: orientation)
activeTetromino?.transform.matrix *= transform.matrix
}
}
// TODO: Design a subclass of Entity for creating a tetromino entity.
class TetrominoEntity: Entity {
// Define inputs to class.
init(color: UIColor, name: String) {
super.init()
let boxSize: Float = 0.05
let cornerRadius: Float = 0.002
let boxMesh = MeshResource.generateBox(size: boxSize, cornerRadius: cornerRadius)
let material = SimpleMaterial(color: color, isMetallic: true)
// Create and position ModelEntity boxes.
if name == "straight" {
let box = ModelEntity(mesh: boxMesh, materials: [material])
self.addChild(box)
let box2 = ModelEntity(mesh: boxMesh, materials: [material])
box2.position.y = boxSize
self.addChild(box2)
let box3 = ModelEntity(mesh: boxMesh, materials: [material])
box3.position.y = boxSize * 2
self.addChild(box3)
let box4 = ModelEntity(mesh: boxMesh, materials: [material])
box4.position.y = boxSize * 3
self.addChild(box4)
} else if name == "square"{
let box = ModelEntity(mesh: boxMesh, materials: [material])
self.addChild(box)
let box2 = ModelEntity(mesh: boxMesh, materials: [material])
box2.position.y = boxSize
self.addChild(box2)
let box3 = ModelEntity(mesh: boxMesh, materials: [material])
box3.position.x = boxSize
self.addChild(box3)
let box4 = ModelEntity(mesh: boxMesh, materials: [material])
box4.position.x = boxSize
box4.position.y = boxSize
self.addChild(box4)
} else if name == "T"{
let box = ModelEntity(mesh: boxMesh, materials: [material])
self.addChild(box)
let box2 = ModelEntity(mesh: boxMesh, materials: [material])
box2.position.y = boxSize
self.addChild(box2)
let box3 = ModelEntity(mesh: boxMesh, materials: [material])
box3.position.x = boxSize
self.addChild(box3)
let box4 = ModelEntity(mesh: boxMesh, materials: [material])
box4.position.x = -boxSize
self.addChild(box4)
} else if name == "L"{
let box = ModelEntity(mesh: boxMesh, materials: [material])
self.addChild(box)
let box2 = ModelEntity(mesh: boxMesh, materials: [material])
box2.position.y = boxSize
self.addChild(box2)
let box3 = ModelEntity(mesh: boxMesh, materials: [material])
box3.position.y = boxSize * 2
self.addChild(box3)
let box4 = ModelEntity(mesh: boxMesh, materials: [material])
box4.position.x = boxSize
self.addChild(box4)
} else if name == "skew" {
let box = ModelEntity(mesh: boxMesh, materials: [material])
self.addChild(box)
let box2 = ModelEntity(mesh: boxMesh, materials: [material])
box2.position.y = boxSize
self.addChild(box2)
let box3 = ModelEntity(mesh: boxMesh, materials: [material])
box3.position.x = boxSize
box3.position.y = boxSize
self.addChild(box3)
let box4 = ModelEntity(mesh: boxMesh, materials: [material])
box4.position.x = -boxSize
self.addChild(box4)
}
// Add modelEntity to 'self' which is an entity.
/*
self.addChild(modelEntity)
*/
}
required init() {
fatalError("init() has not been implemented")
}
}
Class Notes
Camera Projections of 3D
Projection - Orthographic
- shows the 3d object "flat" from front, side, top
- shows true size of the object without perspective distortion
- (normally used in architetural diagrams )