Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

UIViewController with ARSCNView takes significant time before presenting on screen

I have a UIViewController which uses ARSCNView and adding few elements to it via Scenekit as shown in below example. Everything is working fine except when I call present to show this view controller, it takes a significant time or delay before presenting on the screen.

@IBOutlet var sceneView: ARSCNView!

override func viewDidLoad() {
    super.viewDidLoad()

    sceneView.showsStatistics =  DebugSettings.isDebugActive

    for (index, coach) in coachPositions.enumerated() {
        let coachGeometry = SCNBox(width: 0.1, height: 0.1, length: 0.1, chamferRadius: 0.005)
        let coachNode = TrainEngineNode(position:  SCNVector3Make(0, Float(index) * 0.1, -0.5), geometry: coachGeometry)
        sceneView.scene.rootNode.addChildNode(coachNode)
    }

    self.sceneView.autoenablesDefaultLighting = true
}

override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)

    // Create a session configuration
    let configuration = ARWorldTrackingConfiguration()

    // Run the view's session
    sceneView.session.run(configuration)
}
like image 544
kidsid49 Avatar asked Nov 24 '17 13:11

kidsid49


2 Answers

The delay, as everyone has pointed out, is the fact that you're blocking the main thread when you're setting up your scene. All UI updates happen on the main thread and the display refreshes 60 times per second, except the latest iPad Pros which can do 120Hz. This means that any unit of synchronous work you do has to complete in less than 1/60 = 0.016667 seconds.

Your initializer TrainEngineNode(position:geometry:) is likely doing work such as loading resources from a DAE or OBJ file which is probably where all that time is going. I say that because if you change this line:

let coachNode = TrainEngineNode(position:  SCNVector3Make(0, Float(index) * 0.1, -0.5), geometry: coachGeometry)

to this, which I need to do because TrainEngineNode is not provided in your question:

let coachNode = SCNNode(geometry: coachGeometry)

then there is no noticeable delay on an iPhone 7.

SceneKit and ARKit do not require you to make changes on any particular thread, so any time you have this situation you can simply offload the work onto a background queue, or preferably a serial queue that you manage. There are some caveats around doing work at an appropriate time, such as adding your objects to the scene after the ARSession is fully running. For some excellent ideas for patterns you might follow for creating an ARKit application, I recommend this sample provided by Apple.

A simplified example follows:

class SimpleARViewController: UIViewController {

    @IBOutlet weak var sceneView: ARSCNView!
    weak var activityIndicator: UIActivityIndicatorView?

    var coachPositions = [1, 2, 3, 4, 5]

    let updateQueue = DispatchQueue(label: "com.example.apple-samplecode.arkitexample.serialSceneKitQueue")

    override func viewDidLoad() {
        super.viewDidLoad()

        sceneView.alpha = 0
        view.backgroundColor = .black
        sceneView.isPlaying = false
        sceneView.session.delegate = self

        let activityIndicator = UIActivityIndicatorView(activityIndicatorStyle: .whiteLarge)
        activityIndicator.startAnimating()
        activityIndicator.translatesAutoresizingMaskIntoConstraints = false

        view.addSubview(activityIndicator)
        NSLayoutConstraint.activate([view.centerXAnchor.constraint(equalTo: activityIndicator.centerXAnchor, constant: 0),
                                     view.centerYAnchor.constraint(equalTo: activityIndicator.centerYAnchor, constant: 0)])
        self.activityIndicator = activityIndicator

        sceneView.autoenablesDefaultLighting = true
    }

    private func loadScene() {

        SCNTransaction.begin()
        SCNTransaction.disableActions = true

        for (index, _) in self.coachPositions.enumerated() {
            let coachGeometry = SCNBox(width: 0.1, height: 0.1, length: 0.1, chamferRadius: 0.005)

            // Simulates loading time of `TrainEngineNode(position:geometry:)`
            usleep(500000)

            let coachNode = SCNNode(geometry: coachGeometry)
            coachNode.worldPosition = SCNVector3Make(0, Float(index) * 0.1, -0.5)

            self.sceneView.scene.rootNode.addChildNode(coachNode)
        }

        SCNTransaction.commit()
        self.isLoading = false
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)

        let configuration = ARWorldTrackingConfiguration()
        sceneView.session.run(configuration)
    }

    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)

        sceneView.session.pause()
    }

    var isLoading = true
    var hasLoaded = false
}

extension SimpleARViewController: ARSessionDelegate {

    func session(_ session: ARSession, didUpdate frame: ARFrame) {

        // Waiting until after the session starts prevents objects from jumping around
        if hasLoaded == false {
            hasLoaded = true

            updateQueue.async { [weak self] in
                self?.loadScene()
            }
        } else if isLoading == false {
            guard let activityIndicator = self.activityIndicator else { return }

            DispatchQueue.main.async {
                UIView.animate(withDuration: 0.35, animations: { [weak self] in
                    self?.sceneView.alpha = 1
                    activityIndicator.alpha = 0
                }, completion: { _ in
                    activityIndicator.removeFromSuperview()
                })
            }
        }
    }
}

This code results in the following interaction:

enter image description here

As you can see in the gif, there is no delay in view controller presentation because all of the loading "work" is now done on our private serial queue. Even this contrived example can be further improved by decoupling the creation of the TrainEngineNode's from the time which they are added to the scene. For the sake of brevity, I kept the logic as similar to what you already have while presenting a relatively robust initialization. For a more robust/abstracted implementation of 3D object loading, I recommend looking at VirtualObjectLoader and how it is used in the aforementioned sample project.

like image 169
allenh Avatar answered Sep 30 '22 18:09

allenh


if your loop take a long time, you can try this:

override func viewDidLoad() {
    super.viewDidLoad()

    // start your loading animator

    DispatchQueue.main.async {
        self.sceneView.showsStatistics =  DebugSettings.isDebugActive
        for (index, coach) in self.coachPositions.enumerated() {
            let coachGeometry = SCNBox(width: 0.1, height: 0.1, length: 0.1, chamferRadius: 0.005)
            let coachNode = TrainEngineNode(position:  SCNVector3Make(0, Float(index) * 0.1, -0.5), geometry: coachGeometry)
            self.sceneView.scene.rootNode.addChildNode(coachNode)
        }
        self.sceneView.autoenablesDefaultLighting = true

        // stop loading animator

    }
}

If don't work, you can delay time like this:

override func viewDidLoad() {
    super.viewDidLoad()

    // start your loading animator

    let delayTimeInterval: Double = 0.1 // 0.1 second
    let delayTime = DispatchTime(uptimeNanoseconds: DispatchTime.now().uptimeNanoseconds + UInt64(delayTimeInterval * Double(NSEC_PER_SEC)))
    DispatchQueue.main.asyncAfter(deadline: delayTime) {                            
        self.sceneView.showsStatistics =  DebugSettings.isDebugActive
        for (index, coach) in self.coachPositions.enumerated() {
            let coachGeometry = SCNBox(width: 0.1, height: 0.1, length: 0.1, chamferRadius: 0.005)
            let coachNode = TrainEngineNode(position:  SCNVector3Make(0, Float(index) * 0.1, -0.5), geometry: coachGeometry)
            self.sceneView.scene.rootNode.addChildNode(coachNode)
        }
        self.sceneView.autoenablesDefaultLighting = true

        // stop loading animator
    }
}
like image 40
Lionking Avatar answered Sep 30 '22 17:09

Lionking