使用隐藏的SCNNode捕获SCNView的图像/屏幕截图:在屏幕更新后无效的drawViewHierarchyInRect

时间:2016-04-28 00:15:12

标签: ios swift core-graphics scenekit capture

下面的代码捕获了SCNView1的屏幕截图,但它并没有完全正常工作。

SCNView1包含Node1。目标是在没有Node1的情况下捕获SCNView1。

但是,将afterScreenUpdates设置为true(或false)无效:屏幕截图包含Node1,无论如何。

有什么问题?

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
}

1 个答案:

答案 0 :(得分:11)

我们在这里处理两个不一定通信的不同系统:SceneKit渲染引擎和试图捕获屏幕截图的UIKit / CoreGraphics绘图系统。当您隐藏SCNNode时,它不会立即使重绘的视图无效,就像隐藏UIView子视图一样,因此将afterScreenUpdates设置为true没有效果在这个时间点。在节点被标记为隐藏之后,在我们知道SceneKit渲染循环已完成一次之后,并且在视图准备好在屏幕上重绘之后,我们就可以捕获屏幕截图。我们可以通过为SCNSceneRendererDelegateSCNSceneRendererDelegate)创建SCNView来监听渲染循环中的事件。我们可以实现renderer:didRenderScene:atTime:委托方法。

将代理人分配到场景视图(scnView.delegate = self)。 当您想要进行屏幕捕获时,隐藏节点并设置一个布尔标志,该标志与委托true的范围相同:

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

然后你将实现你的代表:

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)
        }
    }
}

这种委托方法对我们来说并不完美,因为即使渲染了场景,它还没有准备好在屏幕上重绘视图,因为这种委托方法让我们有机会进行任何我们想要的自定义渲染。此外,如果您尝试在此处调用drawViewHierarchyInRect:afterScreenUpdates:snapshot,则SceneKit渲染将完全停止(在模拟器和iOS 9.3设备上),这可能是SceneKit错误。对于委托方法,我们没有更好的选择,但我的解决方案是在主队列上调度异步调用,该主队列将在SceneKit渲染循环完全完成后运行,并且渲染的图像已准备好在屏幕上重绘。如果您使用drawViewHierarchyInRect:afterScreenUpdates:,请确保afterScreenUpdates设置为true,因为此时似乎尚未执行实际绘图。并且我确定你知道,确保异步调用在主线程上运行,因为永远不应该从另一个线程触及UIKit个对象。

顺便说一句,我复制了你的问题,并使用Xcode 7.3(带有旋转船的那个)提供的默认SceneKit项目模板找到了我的解决方案。每次点击场景视图时,我都会切换船的隐藏属性,然后设置标记以捕获屏幕截图。如果需要,我们可以使用此模板作为进一步讨论的基础。