I am working with iOS 11, XCode 9 and Metal 2. I have a MTLTexture
with pixel format bgra8Unorm
. I cannot change this pixel format, because according to pixelFormat documentation:
The pixel format for a Metal layer must be bgra8Unorm, bgra8Unorm_srgb, rgba16Float, BGRA10_XR, or bgra10_XR_sRGB.
The other pixel formats are not suitable for my application.
Now I want to create an UIImage
from the texture. I am able to do so by extracting the pixel bytes from the texture (doc):
getBytes(_:bytesPerRow:bytesPerImage:from:mipmapLevel:slice:)
I am processing these bytes to get an UIImage
:
func getUIImageForRGBAData(data: Data) -> UIImage? {
let d = (data as NSData)
let width = GlobalConfiguration.textureWidth
let height = GlobalConfiguration.textureHeight
let rowBytes = width * 4
let size = rowBytes * height
let pointer = malloc(size)
memcpy(pointer, d.bytes, d.length)
let colorSpace = CGColorSpaceCreateDeviceRGB()
let context = CGContext(data: pointer, width: width, height: height, bitsPerComponent: 8, bytesPerRow: rowBytes, space: colorSpace, bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue)!
let imgRef = context.makeImage()
let image = UIImage(cgImage: imgRef!)
return image
}
However, CGContext assumes that the pixels are in the rgba8 format. For example, red texture pixels are blue in the final UIImage. Is there a way to change the pixelFormat in this process to get the proper colors?
This function will swizzle the bytes of a .bgra8Unorm
texture into RGBA order and create a UIImage
from the data:
func makeImage(from texture: MTLTexture) -> UIImage? {
let width = texture.width
let height = texture.height
let bytesPerRow = width * 4
let data = UnsafeMutableRawPointer.allocate(bytes: bytesPerRow * height, alignedTo: 4)
defer {
data.deallocate(bytes: bytesPerRow * height, alignedTo: 4)
}
let region = MTLRegionMake2D(0, 0, width, height)
texture.getBytes(data, bytesPerRow: bytesPerRow, from: region, mipmapLevel: 0)
var buffer = vImage_Buffer(data: data, height: UInt(height), width: UInt(width), rowBytes: bytesPerRow)
let map: [UInt8] = [2, 1, 0, 3]
vImagePermuteChannels_ARGB8888(&buffer, &buffer, map, 0)
guard let colorSpace = CGColorSpace(name: CGColorSpace.genericRGBLinear) else { return nil }
guard let context = CGContext(data: data, width: width, height: height, bitsPerComponent: 8, bytesPerRow: bytesPerRow,
space: colorSpace, bitmapInfo: CGImageAlphaInfo.noneSkipLast.rawValue) else { return nil }
guard let cgImage = context.makeImage() else { return nil }
return UIImage(cgImage: cgImage)
}
Caveat: This function is very expensive. Creating an image from a Metal texture every frame is almost never what you want to do.