Рендеринг анимированного CALayer вне экрана с помощью CARenderer с MTLTexture - PullRequest
0 голосов
/ 15 мая 2019

Я хочу визуализировать анимированные NSView (или только базовые CALayer) в серию изображений без представления на экране ВСЕ * . Я понял, как это сделать с CARenderer и MTLTexture, но есть некоторые проблемы с приведенным ниже подходом.

Он запускается на детской площадке и сохраняет выходные данные в папку Off-screen Render в ваших загрузках:

import AppKit
import Metal
import QuartzCore
import PlaygroundSupport

let view = NSView(frame: CGRect(x: 0, y: 0, width: 600, height: 400))
let circle = NSView(frame: CGRect(x: 0, y: 0, width: 50, height: 50))

circle.wantsLayer = true
circle.layer?.backgroundColor = NSColor.red.cgColor
circle.layer?.cornerRadius = 25
view.wantsLayer = true
view.addSubview(circle)

let textureDescriptor = MTLTextureDescriptor.texture2DDescriptor(pixelFormat: .rgba8Unorm, width: 600, height: 400, mipmapped: false)
textureDescriptor.usage = [MTLTextureUsage.shaderRead, .shaderWrite, .renderTarget]

let device = MTLCreateSystemDefaultDevice()!
let texture: MTLTexture = device.makeTexture(descriptor: textureDescriptor)!
let context = CIContext(mtlDevice: device)
let renderer = CARenderer(mtlTexture: texture)

renderer.layer = view.layer
renderer.bounds = view.frame

let outputURL: URL = try! FileManager.default.url(for: .downloadsDirectory, in: .userDomainMask, appropriateFor: nil, create: false).appendingPathComponent("Off-screen Render")
try? FileManager.default.removeItem(at: outputURL)
try! FileManager.default.createDirectory(at: outputURL, withIntermediateDirectories: true, attributes: nil)

var frameNumber: Int = 0

func render() {
    Swift.print("Rendering frame #\(frameNumber)…")

    renderer.beginFrame(atTime: CACurrentMediaTime(), timeStamp: nil)
    renderer.addUpdate(renderer.bounds)
    renderer.render()
    renderer.endFrame()

    let ciImage: CIImage = CIImage(mtlTexture: texture)!
    let cgImage: CGImage = context.createCGImage(ciImage, from: ciImage.extent)!
    let url: URL = outputURL.appendingPathComponent("frame-\(frameNumber).png")
    let destination: CGImageDestination = CGImageDestinationCreateWithURL(url as CFURL, kUTTypePNG, 1, nil)!
    CGImageDestinationAddImage(destination, cgImage, nil)
    guard CGImageDestinationFinalize(destination) else { fatalError() }

    frameNumber += 1
}

var timer: Timer?

NSAnimationContext.runAnimationGroup({ context in
    context.duration = 0.25
    view.animator().frame.origin = CGPoint(x: 550, y: 350)
}, completionHandler: {
    timer?.invalidate()
    render()
    Swift.print("Finished off-screen rendering of \(frameNumber) frames in \(outputURL.path)…")
})

// Make the first render immediately after the animation start and after it completes. For the purpose
// of this demo timer is used instead of display link.

render()
timer = Timer.scheduledTimer(withTimeInterval: 1 / 30, repeats: true, block: { _ in render() })

Проблемы с приведенным выше кодом показаны в приложении ниже:

  1. Текстура не очищается, и каждый следующий кадр рисуется поверх предыдущего рендера. Я знаю, что могу использовать replace(region:…), но подозреваю, что это не эффективно по сравнению с проходом рендеринга с четким описанием цвета. Это правда? Можно ли использовать pass pass с CARenderer?

  2. Первый кадр (в реальном проекте это два-три кадра) часто выходит пустым. Я подозреваю, что это связано с некоторым асинхронным поведением при CARenderer рендеринге или при CGImage построении с использованием Core Image. Как этого можно избежать? Есть ли какой-нибудь обратный вызов с ожиданием, пока не закончится рендеринг текстуры?

enter image description here

...