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. 

IMG_5414.PNG

IMG_5415.PNG

IMG_1078.PNG

IMG_5416.PNG

IMG_5417.PNG

IMG_5418.PNG

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