Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Capture image/screenshot of SCNView with SCNNode hidden: drawViewHierarchyInRect afterScreenUpdates not working

The code below captures a screenshot of SCNView1, but it's not quite working.

SCNView1 contains Node1. The goal is to capture SCNView1 without Node1.

However, setting afterScreenUpdates to true (or false) doesn't help: the screenshot contains Node1 no matter what.

What's the problem?

Node1.hidden = true
let screenshot = turnViewToImage(SCNView1, opaque: false, afterUpdates: true)
// Save screenshot to disk

func turnViewToImage(targetView: UIView, opaque: Bool, afterUpdates: Bool = false) -> UIImage {
    UIGraphicsBeginImageContextWithOptions(targetView.bounds.size, opaque, UIScreen.mainScreen().scale)
    let context = UIGraphicsGetCurrentContext()
    CGContextSetInterpolationQuality(context, CGInterpolationQuality.High)
    targetView.drawViewHierarchyInRect(targetView.bounds, afterScreenUpdates: afterUpdates)
    let image = UIGraphicsGetImageFromCurrentImageContext()
    UIGraphicsEndImageContext()
    return image
}
like image 883
Crashalot Avatar asked Apr 28 '16 00:04

Crashalot


1 Answers

We're dealing with two different systems here that don't necessarily communicate: the SceneKit rendering engine and the UIKit/CoreGraphics drawing system that's trying to capture the screenshot. When you hide a SCNNode, it doesn't instantly invalidate the view for redrawing like hiding a UIView subview would, so setting afterScreenUpdates to true doesn't have an effect at this point in time. After the node is marked as hidden, after we know the SceneKit render loop has completed once, and after the view is ready to be redrawn on screen, we can then capture the screenshot. We can listen to events in the render loop by creating a SCNSceneRendererDelegate for the SCNView (SCNSceneRendererDelegate). We can implement the renderer:didRenderScene:atTime: delegate method.

Assign a delegate to your scene view (scnView.delegate = self). When you want to do your screen capture, hide the node and set a boolean flag that's in the same scope as the delegate to true:

Node1.hidden = true
screenCaptureFlag = true  // screenCaptureFlag is a controller property

Then you'll implement your delegate:

func renderer(renderer: SCNSceneRenderer, didRenderScene scene: SCNScene, atTime time: NSTimeInterval) {
    if screenCaptureFlag {
        screenCaptureFlag = false  // unflag
        let scnView = self.view as! SCNView
        // Dispatch asynchronously to main queue
        // Will run once SCNView is ready to be redrawn on screen
        // Also avoids SceneKit rendering freeze
        dispatch_async(dispatch_get_main_queue()) {
            // Get your screenshot the simple way
            let screenshot = scnView.snapshot()
            // Or use your function
            // let screenshot = self.turnViewToImage(scnView, opaque: false, afterUpdates: true)

            // Then save the screenshot, or do whatever you want
            let urlPath = NSURL(fileURLWithPath: NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true).first!).URLByAppendingPathComponent("Image.png")
            try! UIImagePNGRepresentation(screenshot)!.writeToURL(urlPath, options: .AtomicWrite)
        }
    }
}

This delegate method isn't perfect for us because even though the scene is rendered, it isn't yet ready to redraw the view on screen because this delegate method is giving us a chance to do any custom rendering that we want. Also, if you try to call drawViewHierarchyInRect:afterScreenUpdates: or snapshot here, SceneKit rendering will completely stop (both in simulator and on device for iOS 9.3), which may be a SceneKit bug. We don't have a better option for delegate methods, but my solution is to dispatch an async call on the main queue which will run after the SceneKit render loop is fully completed and the rendered image is ready to be redrawn on screen. If you use drawViewHierarchyInRect:afterScreenUpdates:, make sure afterScreenUpdates is set to true because it appears that at this point the actual drawing still hasn't been performed yet. And as I'm sure you know, make sure the async call operates on the main thread, because UIKit objects should never be touched from a different thread.

By the way, I replicated your problem and found my solution using the default SceneKit project template provided by Xcode 7.3 (the one with the spinning ship). I toggle the ship's hidden property every time the scene view is tapped and then set the flag to capture a screenshot. We can use this template as a basis for further discussion if needed.

like image 78
Christopher Whidden Avatar answered Sep 30 '22 12:09

Christopher Whidden