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 命令编码器,我们将帧缓冲的纹理拷贝到统一内存中,实现了读取屏幕像素的目的。

本实例已开源

相关推荐
鸿蒙布道师1 小时前
鸿蒙NEXT开发数值工具类(TS)
android·ios·华为·harmonyos·arkts·鸿蒙系统·huawei
程序员老刘5 小时前
谨慎升级macOS 15.4,规避 ITMS-90048 错误
flutter·macos·ios
90后的晨仔7 小时前
iOS 蓝牙开发基础知识梳理
ios
90后的晨仔7 小时前
什么是WebSocket ?ios 中如何使用?
ios
鸿蒙布道师10 小时前
鸿蒙NEXT开发日期工具类(ArkTs)
android·ios·华为·harmonyos·arkts·鸿蒙系统·huawei
无知的前端11 小时前
iOS开发,runtime实现切片编程原理以及实战用例
ios·面试·性能优化
木西12 小时前
React Native项目初始化及相关通用工具集成
android·react native·ios
胡八一1 天前
Window调试 ios 的 Safari 浏览器
前端·ios·safari
karshey1 天前
【IOS webview】源代码映射错误,页面卡住不动
ios