Metal 进阶:读取可绘制对象的像素

引言

Hi,大家好,我是一牛。同学们已经了解过,如何绘制三角形到Metal 的可绘制对象中。Metal 的可绘制对象其实就是一幅当前帧的纹理。同学们可能会猜想,既然我们能绘制三角形,那我们是否可以读取这幅纹理的像素,或者将该纹理保存为图片? 答案是可以的。今天我将带着大家一起学习下如何读取可绘制对象(Drawable)的像素。

关键配置

ini 复制代码
metalView.framebufferOnly = false

首先我们需要将这个属性设置成false 。这个属性的默认值是true ,表示可绘制对象的纹理只能用来渲染,而我们需要读取该纹理,所以我们需要将这个属性设置成false。值得注意的是,这样做可能会影响性能,我们在做性能优化时,需要考虑到这点。

ini 复制代码
metalView.colorPixelFormat = MTLPixelFormat.rgba8Unorm

为了方便演示,我们将可绘制对象的纹理像素格式设置成rgba8Unorm ,这代表像素有四个通道,每个通道是8个比特,并且按照 RGBA 的顺序排列。

创建渲染管线描述符

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命令编码器,用于将图像数据从一个缓冲区拷贝到另一个缓冲区。

在这里我们将输出的像素缓冲设置成统一内存(GPUCPU 都可以访问),这样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 命令编码器,我们将帧缓冲的纹理拷贝到统一内存中,实现了读取屏幕像素的目的。

本实例已开源

相关推荐
开心就好20259 小时前
iOS App 安全加固流程记录,代码、资源与安装包保护
后端·ios
开心就好20259 小时前
iOS App 性能测试工具怎么选?使用克魔助手(Keymob)结合 Instruments 完成
后端·ios
zhongjiahao1 天前
面试常问的 RunLoop,到底在Loop什么?
ios
wvy2 天前
iOS 26手势返回到根页面时TabBar的动效问题
ios
RickeyBoy3 天前
iOS 图片取色完全指南:从像素格式到工程实践
ios
aiopencode3 天前
使用 Ipa Guard 命令行版本将 IPA 混淆接入自动化流程
后端·ios
二流小码农3 天前
鸿蒙开发:路由组件升级,支持页面一键创建
android·ios·harmonyos
iceiceiceice4 天前
iOS PDF阅读器段评实现:如何从 PDFSelection 精准还原一个自然段
前端·人工智能·ios
TT_Close4 天前
【Flutter×鸿蒙】FVM 不认鸿蒙 SDK?4步手动塞进去
flutter·swift·harmonyos
张江4 天前
Swift Concurrency学习
swift