【问题标题】:Rendering animated CALayer off-screen using CARenderer with MTLTexture使用带有 MTLTexture 的 CARenderer 将动画 CALayer 渲染到屏幕外
【发布时间】:2019-10-02 15:47:39
【问题描述】:

我想将动画NSView(或只是底层的CALayer)渲染成一系列图像,而不在屏幕上ATALL呈现视图。我想出了如何使用CARendererMTLTexture 来做到这一点,但以下方法存在一些问题。

这在操场上运行并将输出存储到您下载的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:…),但怀疑它与具有清晰颜色描述的渲染通道相比效率不高。这是真的?渲染通道可以和CARenderer一起使用吗?

  2. 第一帧(在实际项目中是两到三帧)通常是空的。我怀疑这与CARenderer 渲染或CGImage 使用Core Image 构造期间的一些异步行为有关。如何避免这种情况?纹理上有某种等待直到渲染完成的回调吗?

【问题讨论】:

  • CARenderer 对于它对纹理的作用和纹理的状态非常不透明。对于问题 1,我建议设置一个针对纹理的渲染通道描述符,并使用加载操作将其清除,创建渲染命令编码器,立即结束编码器,并提交命令缓冲区。对于问题 2,尝试创建 blit 命令编码器,使用它来编码纹理的同步资源命令,结束它并提交它。
  • 肯,感谢您的意见!它现在几乎可以工作了。我没有看到有或没有 blit 的任何区别,并且猜测它是不需要的,但第一帧总是空的。如果我设置一个零延迟的计时器并从回调中渲染,那么第一帧就会很好。我猜它与CARenderer有关,你知道如何解决这个问题吗?更新代码:gist.github.com/ianbytchek/7f4168df16b8bc170ef587344b6c1444
  • 另外,我是 Metal 的菜鸟。重新创建命令缓冲区和编码器是使用它们的正确方法吗?是否有更优化的方法,无需在渲染循环中重新创建它们?
  • 不知道为什么第一帧还是空的。希望其他人会有一个想法。就您对 Metal 的使用而言,是的,为每一帧创建命令缓冲区和编码器是正确的。

标签: rendering metal core-image quartz-core off-screen


【解决方案1】:

在与 Apple 开发者技术支持交谈后,似乎:

Core Image 延迟渲染,直到客户端请求访问帧缓冲区,即CVPixelBufferLockBaseAddress

所以,解决方法就是在调用CIContext.render之后做CVPixelBufferLockBaseAddress,如下所示:

for frameNumber in 0 ..< frameCount {
    var pixelBuffer: CVPixelBuffer?
    guard let pixelBufferPool: CVPixelBufferPool = pixelBufferAdaptor.pixelBufferPool else { preconditionFailure() }
    precondition(CVPixelBufferPoolCreatePixelBuffer(nil, pixelBufferPool, &pixelBuffer) == kCVReturnSuccess)

    let ciImage = CIImage(cgImage: frameImage)
    context.render(ciImage, to: pixelBuffer!)

    precondition(CVPixelBufferLockBaseAddress(pixelBuffer!, []) == kCVReturnSuccess)
    defer { precondition(CVPixelBufferUnlockBaseAddress(pixelBuffer!, []) == kCVReturnSuccess) }

    let bytes = UnsafeBufferPointer(start: CVPixelBufferGetBaseAddress(pixelBuffer!)!.assumingMemoryBound(to: UInt8.self), count: CVPixelBufferGetDataSize(pixelBuffer!))
    precondition(bytes.contains(where: { $0 != 0 }))

    while !input.isReadyForMoreMediaData { Thread.sleep(forTimeInterval: 10 / 1000) }
    precondition(pixelBufferAdaptor.append(pixelBuffer!, withPresentationTime: CMTime(seconds: Double(frameNumber) * frameRate, preferredTimescale: 600)))
}

附:这与Making CIContext.render(CIImage, CVPixelBuffer) work with AVAssetWriter 问题的答案相同——您可能需要查看它以更深入地了解在使用 AVFoundation 时此问题可能发生的位置和方式。虽然问题不同,但解决方案完全相同。

【讨论】:

    【解决方案2】:

    我认为您可以使用AVVideoCompositionCoreAnimationTool 来渲染带有动画的视图。

    【讨论】:

    • 当您需要将动画输出为支持 AVFoundation 的视频格式时,这将是一个好主意,否则这是一个非常针对特定目的的解决方案。文档将其描述为 An object used to incorporate Core Animation into a video composition. 我想它仍然在幕后使用 CARenderer
    • 但也适用于 iOS - 不仅适用于 macOS。从生成的视频中,您可以提取任何您想要的帧。
    • 你当然可以,但如果你现在只需要渲染,那就太过分了。此外,如果没有无损输出,图像很难保存,这将是巨大的。使用 ProRes 编码可能是一个不错的选择,但同样,文件大小会很大。刚刚看到CARenderer 仅在 macOS 上可用,但我认为还有其他更适合这项工作的东西。我从彼得罗扎沃茨克看到你? :) 给我留言? t.me/ianbytchek
    猜你喜欢
    • 1970-01-01
    • 2011-04-19
    • 1970-01-01
    • 2014-03-18
    • 2014-08-26
    • 1970-01-01
    • 2015-06-15
    • 2020-08-26
    • 1970-01-01
    相关资源
    最近更新 更多