捕获 image/screenshot 个隐藏 SCNNode 的 SCNView:drawViewHierarchyInRect afterScreenUpdates 不工作

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

下面的代码捕获了 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
}

我们在这里处理两个不一定通信的不同系统:SceneKit 渲染引擎和试图捕获屏幕截图的 UIKit/CoreGraphics 绘图系统.当您隐藏 SCNNode 时,它不会像隐藏 UIView 子视图那样立即使重绘视图无效,因此将 afterScreenUpdates 设置为 true 没有效果在这个时间点。节点被标记为隐藏后,我们知道 SceneKit 渲染循环已经完成一次,并且视图准备好在屏幕上重绘后,我们就可以捕获屏幕截图了。我们可以通过为 SCNView (SCNSceneRendererDelegate) 创建一个 SCNSceneRendererDelegate 来监听渲染循环中的事件。我们可以实现 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 项目模板找到了我的解决方案。每次点击场景视图时,我都会切换船的隐藏 属性,然后设置标志以捕获屏幕截图。如果需要,我们可以使用此模板作为进一步讨论的基础。