Захват изображения/скриншот SCNView со скрытым SCNNode: drawViewHierarchyInRect afterScreenUpdates не работает

В приведенном ниже коде сделан снимок экрана SCNView1, но он не совсем работает.

SCNView1 содержит Node1. Цель состоит в том, чтобы захватить SCNView1 без Node1.

Однако установка 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
}

person Crashalot    schedule 28.04.2016    source источник


Ответы (1)


Здесь мы имеем дело с двумя разными системами, которые не обязательно взаимодействуют друг с другом: механизм рендеринга SceneKit и система рисования UIKit/CoreGraphics, которая пытается сделать снимок экрана. Когда вы скрываете SCNNode, это не делает представление недействительным мгновенно для перерисовки, как скрытие подвида UIView, поэтому установка afterScreenUpdates на true не имеет эффекта в данный момент времени. После того, как узел помечен как скрытый, после того, как мы знаем, что цикл рендеринга SceneKit завершился один раз, и после того, как представление готово к перерисовке на экране, мы можем сделать снимок экрана. Мы можем прослушивать события в цикле рендеринга, создав SCNSceneRendererDelegate для SCNView (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 объекты никогда не должны касаться другого потока.

Кстати, я воспроизвел вашу проблему и нашел свое решение, используя шаблон проекта SceneKit по умолчанию, предоставленный Xcode 7.3 (тот, что с вращающимся кораблем). Я переключаю скрытое свойство корабля каждый раз, когда нажимаю на вид сцены, а затем устанавливаю флаг, чтобы сделать снимок экрана. При необходимости мы можем использовать этот шаблон в качестве основы для дальнейшего обсуждения.

person Christopher Whidden    schedule 01.05.2016
comment
хорошо, большое спасибо. другой вариант, который будет изучен, — введение некоторой задержки (например, 100 мс) перед захватом снимка экрана. неэлегантно, но может работать, поскольку пользовательский поток уже содержит задержку через сетевой вызов. - person Crashalot; 02.05.2016
comment
Кстати, вы сталкивались с проблемой, подобной этой раньше? stackoverflow.com/questions/36728704 / - person Crashalot; 02.05.2016
comment
надеялся, что кто-то другой опубликует решение, для которого не требуется переменная флага, но, по крайней мере, это работает. Спасибо за вашу помощь! - person Crashalot; 16.05.2016
comment
Крис, похоже, у меня похожая проблема. У меня есть обновление CoreMotion, которое запускается 30 раз в секунду, вызывая рабочий метод. Внутри этого метода я устанавливаю ориентацию камеры сцены, и это дает вам хороший панорамный вид. Однако, когда я пытаюсь сделать снимок в том же обновлении движения, snapshot() выдает SIG или EXE_BAD, хотя ТОЧНО тот же код работает как действие на кнопке. Звучит так, как будто это одно и то же? - person Maury Markowitz; 31.01.2017