Сделайте снимок текущего экрана с помощью Metal в быстром темпе

Я пытался:

let scale = UIScreen.mainScreen().scale        
UIGraphicsBeginImageContextWithOptions(metalLayer.bounds.size, false, scale)
            
// metalLayer.renderInContext(UIGraphicsGetCurrentContext()!)

// self.view.layer ...

metalLayer.presentationLayer()!.renderInContext(UIGraphicsGetCurrentContext()!)
            
let image = UIGraphicsGetImageFromCurrentImageContext()           
UIGraphicsEndImageContext()
UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil)

Но результат - пустой скриншот. Любая помощь была бы хороша!

Имейте в виду, что я хочу сделать снимок CAMetalLayer.


person Jonas    schedule 21.11.2015    source источник


Ответы (4)


Чтобы сделать снимок экрана, вам нужно получить MTLTexture буфера кадра.

1. Если вы используете MTKView:

let texture = view.currentDrawable!.texture

2. Если вы не используете MTKView

Вот что я бы сделал - у меня было бы свойство, содержащее последний вывод, представленный на экране:

let lastDrawableDisplayed: CAMetalDrawable?

И затем, когда вы представляете возможность рисования на экране, я бы обновил его:

let commandBuffer = commandQueue.commandBuffer()
commandBuffer.addCompletedHandler { buffer in
  self.lastDrawableDisplayed = drawable
}

Теперь, когда вам нужно сделать снимок экрана, вы можете получить такую ​​текстуру:

let texture = lastDrawableDisplayed.texture

Хорошо, теперь, когда у вас есть MTLTexture, вы можете преобразовать его в CGImage, а затем в UIImage или NSImage.

Вот код для игровой площадки OS X (MetalKit.MTLTextureLoader недоступен для игровых площадок iOS), в котором я конвертирую MTLTexture в CGImage

Я сделал для этого небольшое расширение над MTLTexture.

import Metal
import MetalKit
import Cocoa

let device = MTLCreateSystemDefaultDevice()!
let textureLoader = MTKTextureLoader(device: device)

let path = "path/to/your/image.jpg"
let data = NSData(contentsOfFile: path)!

let texture = try! textureLoader.newTextureWithData(data, options: nil)

extension MTLTexture {
  
  func bytes() -> UnsafeMutablePointer<Void> {
    let width = self.width
    let height = self.height
    let rowBytes = self.width * 4
    let p = malloc(width * height * 4)
    
    self.getBytes(p, bytesPerRow: rowBytes, fromRegion: MTLRegionMake2D(0, 0, width, height), mipmapLevel: 0)
    
    return p
  }
  
  func toImage() -> CGImage? {
    let p = bytes()
    
    let pColorSpace = CGColorSpaceCreateDeviceRGB()
    
    let rawBitmapInfo = CGImageAlphaInfo.NoneSkipFirst.rawValue | CGBitmapInfo.ByteOrder32Little.rawValue
    let bitmapInfo:CGBitmapInfo = CGBitmapInfo(rawValue: rawBitmapInfo)
    
    let selftureSize = self.width * self.height * 4
    let rowBytes = self.width * 4
    let provider = CGDataProviderCreateWithData(nil, p, selftureSize, nil)
    let cgImageRef = CGImageCreate(self.width, self.height, 8, 32, rowBytes, pColorSpace, bitmapInfo, provider, nil, true, CGColorRenderingIntent.RenderingIntentDefault)!
    
    return cgImageRef
  }
}

if let imageRef = texture.toImage() {
  let image = NSImage(CGImage: imageRef, size: NSSize(width: texture.width, height: texture.height))
}
person haawa    schedule 24.11.2015
comment
по крайней мере один } отсутствует. - person Klaas; 30.09.2018
comment
Не захватывает прозрачный фон. - person Anessence; 25.10.2019

Для Swift 4.0 просто конвертирую код, предоставленный haawa

let lastDrawableDisplayed = metalView?.currentDrawable?.texture

if let imageRef = lastDrawableDisplayed?.toImage() {
    let uiImage:UIImage = UIImage.init(cgImage: imageRef)
}

extension MTLTexture {

    func bytes() -> UnsafeMutableRawPointer {
        let width = self.width
        let height   = self.height
        let rowBytes = self.width * 4
        let p = malloc(width * height * 4)

        self.getBytes(p!, bytesPerRow: rowBytes, from: MTLRegionMake2D(0, 0, width, height), mipmapLevel: 0)

        return p!
    }

    func toImage() -> CGImage? {
        let p = bytes()

        let pColorSpace = CGColorSpaceCreateDeviceRGB()

        let rawBitmapInfo = CGImageAlphaInfo.noneSkipFirst.rawValue | CGBitmapInfo.byteOrder32Little.rawValue
        let bitmapInfo:CGBitmapInfo = CGBitmapInfo(rawValue: rawBitmapInfo)

        let selftureSize = self.width * self.height * 4
        let rowBytes = self.width * 4
        let releaseMaskImagePixelData: CGDataProviderReleaseDataCallback = { (info: UnsafeMutableRawPointer?, data: UnsafeRawPointer, size: Int) -> () in
            return
        }
        let provider = CGDataProvider(dataInfo: nil, data: p, size: selftureSize, releaseData: releaseMaskImagePixelData)
        let cgImageRef = CGImage(width: self.width, height: self.height, bitsPerComponent: 8, bitsPerPixel: 32, bytesPerRow: rowBytes, space: pColorSpace, bitmapInfo: bitmapInfo, provider: provider!, decode: nil, shouldInterpolate: true, intent: CGColorRenderingIntent.defaultIntent)!

        return cgImageRef
    }
}
person Ankit Kedia    schedule 31.05.2018
comment
Большое спасибо, потому что в моем случае это единственный способ получить изображение github.com/BradLarson/ # GPUImage3 - person WINSergey; 10.10.2018
comment
У меня не работает: _validateGetBytes: 51: неудавшееся утверждение `текстура не должна быть текстурой framebufferOnly. ' - person Alex Stone; 29.01.2021

Мне не удалось получить принятый ответ для работы в Swift 4 / Metal 2 с XCode 9.1 на iPhone 6s. Поэтому я использовал немного другой подход, предполагая, что lastDrawableDisplayed сохраняется, как описано в принятом ответе. Быстрая и грязная обработка без каких-либо исключений:

let context = CIContext()
let texture = self.lastDrawableDisplayed!.texture
let cImg = CIImage(mtlTexture: texture, options: nil)!
let cgImg = context.createCGImage(cImg, from: cImg.extent)!
let uiImg = UIImage(cgImage: cgImg)

Это основано на документации по используемому инициализатору CIImage:

init(mtlTexture:options:) Инициализирует объект изображения данными, предоставленными текстурой Metal.

и Обработка CIImage, в котором описывается, как создать CGImage с использованием CIContext:

CIContext() Создание [s] объекта CIContext (с параметрами по умолчанию) [...] context.createCGImage Визуализация [s] выходного изображения в изображение Core Graphics, которое вы можете отобразить или сохранить в файл.

Надеюсь, что это поможет всем, кто использует Swift 4.

Изменить: Кроме того, в моем проекте есть несколько наложений CAMetalLayer, и я хочу объединить их в один UIImage. Поэтому необходимо иметь ссылки на последний CAMetalDrawable объект каждого слоя. Перед добавлением нового слоя (и, следовательно, использованием в качестве поставщика nextDrawable()) я просто добавляю lastDrawableDisplayed в массив [CAMetalDrawable]. При «экспорте» слоев я просто записываю все UIImages впоследствии в контекст растровой графики и получаю окончательное изображение с UIGraphicsGetImageFromCurrentImageContext().

Изменить: если у вас возникли проблемы с ориентацией, попробуйте следующее:

let uiImg = UIImage(cgImage: cgImg, scale: 1.0, orientation: UIImageOrientation.downMirrored)
person 1awuesterose    schedule 04.12.2017
comment
Подход с CIImage работает у меня, когда мне нужно сделать снимок с прозрачным фоном! Спасибо! - person Anessence; 25.10.2019
comment
это блестящий ответ! - person ZAY; 24.02.2020
comment
У меня тоже не работает: -[MTLDebugComputeCommandEncoder setTexture:atIndex:]:373: failed assertion frameBufferOnly texture not supported for compute. Требуется (с падением производительности): metalView.framebufferOnly = false - person Alex Stone; 29.01.2021

быстрый 4.2

extension MTLTexture {
    
    func bytes() -> UnsafeMutableRawPointer {
        let width = self.width
        let height   = self.height
        let rowBytes = self.width * 4
        let p = malloc(width * height * 4)
        
        self.getBytes(p!, bytesPerRow: rowBytes, from: MTLRegionMake2D(0, 0, width, height), mipmapLevel: 0)
        
        return p!
    }
    
    func toImage() -> CGImage? {
        let p = bytes()
        
        let pColorSpace = CGColorSpaceCreateDeviceRGB()
        
        let rawBitmapInfo = CGImageAlphaInfo.noneSkipFirst.rawValue | CGBitmapInfo.byteOrder32Little.rawValue
        let bitmapInfo:CGBitmapInfo = CGBitmapInfo(rawValue: rawBitmapInfo)
        
        let selftureSize = self.width * self.height * 4
        let rowBytes = self.width * 4
        let releaseMaskImagePixelData: CGDataProviderReleaseDataCallback = { (info: UnsafeMutableRawPointer?, data: UnsafeRawPointer, size: Int) -> () in
            return
        }
        let provider = CGDataProvider(dataInfo: nil, data: p, size: selftureSize, releaseData: releaseMaskImagePixelData)
        let cgImageRef = CGImage(width: self.width, height: self.height, bitsPerComponent: 8, bitsPerPixel: 32, bytesPerRow: rowBytes, space: pColorSpace, bitmapInfo: bitmapInfo, provider: provider!, decode: nil, shouldInterpolate: true, intent: CGColorRenderingIntent.defaultIntent)!
        
        return cgImageRef
    }
}
person masaldana2    schedule 05.03.2019
comment
Дубликат ответа @ Ankit - person Warren Stringer; 23.09.2019