引言
Hi,大家好,我是一牛。同学们已经了解过,如何绘制三角形到Metal 的可绘制对象中。Metal 的可绘制对象其实就是一幅当前帧的纹理。同学们可能会猜想,既然我们能绘制三角形,那我们是否可以读取这幅纹理的像素,或者将该纹理保存为图片? 答案是可以的。今天我将带着大家一起学习下如何读取可绘制对象(Drawable)的像素。
关键配置
ini
metalView.framebufferOnly = false
首先我们需要将这个属性设置成false 。这个属性的默认值是true ,表示可绘制对象的纹理只能用来渲染,而我们需要读取该纹理,所以我们需要将这个属性设置成false。值得注意的是,这样做可能会影响性能,我们在做性能优化时,需要考虑到这点。
ini
metalView.colorPixelFormat = MTLPixelFormat.rgba8Unorm
为了方便演示,我们将可绘制对象的纹理像素格式设置成rgba8Unorm ,这代表像素有四个通道,每个通道是8个比特,并且按照 R 、G 、B 、A 的顺序排列。
创建渲染管线描述符
ini
let pipelineDescriptor = MTLRenderPipelineDescriptor()
pipelineDescriptor.vertexFunction = vertextFunc
pipelineDescriptor.fragmentFunction = fragmentFunc
pipelineDescriptor.colorAttachments[0].pixelFormat = mtkView.colorPixelFormat;
在这里我们需要将颜色附件的像素格式设置成和可绘制对象的纹理像素格式一致。在 Metal 中颜色附件用于渲染管线中的渲染目标。颜色附件通常与帧缓冲区相关联,最终渲染的结果会被写入到颜色附件中,所以二者的像素格式必须一致。
渲染渐变的长方形
php
func drawQuad(in view: MTKView, with commandBuffer: MTLCommandBuffer?) {
guard let renderPassDesriptor = view.currentRenderPassDescriptor else {
return
}
let commanderEncoder = commandBuffer?.makeRenderCommandEncoder(descriptor: renderPassDesriptor)
commanderEncoder?.setRenderPipelineState(pipelineState)
let vertices = [
//top-left
Vertext(position: [-1.0, 1.0, 1.0, 1.0], color: [1.0, 0.0, 1.0, 1.0]),
//bottom-left
Vertext(position: [-1.0, -1.0, 1.0, 1.0], color: [1.0, 0.0, 0.0, 1.0]),
//bottom-right
Vertext(position: [ 1.0, -1.0, 1.0, 1.0], color: [1.0, 1.0, 0.0, 1.0]),
//top-left
Vertext(position: [-1.0, 1.0, 1.0, 1.0], color: [1.0, 0.0, 1.0, 1.0]),
//bottom-right
Vertext(position: [ 1.0, -1.0, 1.0, 1.0], color: [1.0, 1.0, 0.0, 1.0]),
//top-right
Vertext(position: [ 1.0, 1.0, 1.0, 1.0], color: [0.0, 1.0, 0.0, 1.0]),
]
let vertexBuffer = device.makeBuffer(bytes: vertices, length: MemoryLayout<Vertext>.stride * vertices.count)
commanderEncoder?.setVertexBuffer(vertexBuffer, offset: 0, index: 0)
commanderEncoder?.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: vertices.count)
commanderEncoder?.endEncoding()
}
在这里,我们绘制了两个三角形,并且给三角形的顶点设置了颜色,并且我们在片元着色器中直接返回了三角形顶点的颜色,Metal 会使用插值算法,将三角形内部做像素填充,实现渐变效果。
读取像素值
less
func readPixels(commandBuffer: MTLCommandBuffer?, from texture: MTLTexture, at region: CGRect) -> MTLBuffer? {
let sourceOrigin = MTLOrigin(x: Int(region.origin.x), y: Int(region.origin.y), z: 0)
let sourceSize = MTLSize(width: Int(region.size.width), height: Int(region.size.height), depth: 1)
let bytesPerPixel = 4
let bytesPerRow = sourceSize.width * bytesPerPixel
let bytesPerImage = sourceSize.height * bytesPerRow
guard let readBuffer = texture.device.makeBuffer(length: bytesPerImage, options: .storageModeShared) else {
return nil
}
let blitEncoder = commandBuffer?.makeBlitCommandEncoder()
blitEncoder?.copy(from: texture, sourceSlice: 0, sourceLevel: 0, sourceOrigin: sourceOrigin, sourceSize: sourceSize, to: readBuffer, destinationOffset: 0, destinationBytesPerRow: bytesPerRow, destinationBytesPerImage: bytesPerImage)
blitEncoder?.endEncoding()
commandBuffer?.commit()
commandBuffer?.waitUntilCompleted()
return readBuffer
}
为了读取纹理的像素值,我们需要使用Blit
命令编码器,用于将图像数据从一个缓冲区拷贝到另一个缓冲区。
在这里我们将输出的像素缓冲设置成统一内存(GPU 和CPU 都可以访问),这样Blit
命令编码器可以将GPU中的图像数据拷贝到该输出缓冲中。
swift
// 设置输出缓存为统一内存
guard let readBuffer = texture.device.makeBuffer(length: bytesPerImage, options: .storageModeShared) else {
return nil
}
之前我们将Metal 的可绘制对象的纹理像素格式设置成rgba8Unorm,所以在这里我们需要设置输出缓冲匹配该像素格式。
csharp
// 每个像素4个字节
let bytesPerPixel = 4
// 每一行的字节数
let bytesPerRow = sourceSize.width * bytesPerPixel
// 图片的字节数
let bytesPerImage = sourceSize.height * bytesPerRow
提交命令缓冲时,我们还需要在GPU执行命令缓冲区前,阻塞当前线程,我们才能读取正确的像素。
scss
commandBuffer?.waitUntilCompleted()
点击屏幕获取像素
由于纹理左上角坐标是 (0, 0) , 而当前视图左下角的坐标是 (0,0) , 我们需要将点击点转换到纹理坐标
ini
let bottomUpPixelPosition = view.convertToBacking(event.locationInWindow)
let bottomDownPixelPosition = CGPoint(x: bottomUpPixelPosition.x, y: view.frame.size.height - bottomUpPixelPosition.y)
点击时我们需要先编码渲染命令,然后编码读取纹理命令,这两个操作需要在一帧内提交给GPU 。由于此时手动渲染了一帧图片,因此在MTKViewDelegate中不需要重复渲染。
php
let commandBuffer = commandQueue.makeCommandBuffer()
// 渲染纹理
drawQuad(in: theView, with: commandBuffer)
isDrawForReadThisFrame = false
guard let readTexture = theView.currentDrawable?.texture else {
return nil
}
// We only want to get the pixel of the click point.
let region = CGRect(x: pixelPosition.x, y: pixelPosition.y, width: 1, height: 1)
// 读取纹理
guard let pixelBuffer = readPixels(commandBuffer: commandBuffer, from: readTexture, at: region) else {
return nil
}
将输出缓存转像素
less
let pixelPointer = pixelBuffer.contents().bindMemory(to: UInt8.self, capacity: 4)
let r = pixelPointer[0]
let g = pixelPointer[1]
let b = pixelPointer[2]
let a = pixelPointer[3]
return Pixel(r: r, g: g, b: b, a: a)
结语
通过Blit 命令编码器,我们将帧缓冲的纹理拷贝到统一内存中,实现了读取屏幕像素的目的。