Metal 进阶:离屏渲染

881 阅读4分钟

引言

Hi,大家好,我是一牛。今天我想和大家分享的是Metal的离屏渲染。我们先回顾下离屏渲染的概念。

离屏渲染(Off-screen Rendering) 是指在图形处理过程中,图像数据的渲染并不直接显示在屏幕上,而是先渲染到一个内存缓冲区(如纹理、帧缓冲区等),然后再将该图像数据从内存缓冲区传输到屏幕或其他输出设备上。离屏渲染常用于各种图形操作,如图像处理、阴影生成、后期处理效果等。

我们将创建两个渲染通道,一个通道用来渲染(离屏)生成纹理,另一个通道将新生成的纹理渲染到屏幕上。我们也会用到绘制三角形渲染图片的知识,让我们开启今天的学习吧。

创建纹理

func createOffscreenTetureDescriptor() -> MTLTextureDescriptor {
    let descriptor = MTLTextureDescriptor()
    descriptor.textureType = .type2D
    descriptor.width = 512
    descriptor.height = 512
    descriptor.pixelFormat = .rgba8Unorm
    descriptor.usage = [.renderTarget, .shaderRead]
    return descriptor
}
offscreenTexture = device.makeTexture(descriptor: textureDescriptor)

这里我们需要注意的是usage属性,我们需要渲染数据到纹理中,并且在第二个渲染通道中需要读取这个纹理。为此,我们需要给它设置成 MTLTextureUsage.renderTargetMTLTextureUsage.shaderRead。在这里我们创建了一个长和宽是512的纹理。

创建离屏渲染通道

offscreenRenderPassDescriptor = MTLRenderPassDescriptor()
offscreenRenderPassDescriptor.colorAttachments[0].texture = offscreenTexture
offscreenRenderPassDescriptor.colorAttachments[0].loadAction = .clear
offscreenRenderPassDescriptor.colorAttachments[0].storeAction = .store
offscreenRenderPassDescriptor.colorAttachments[0].clearColor = MTLClearColor(red: 0.5, green: 1.0, blue: 1.0, alpha: 1.0)

这是我们创建的第一个渲染通道,这个通道负责渲染生成纹理。我们将新生成的纹理赋值给colorAttachments[0].texture, 告诉这个渲染通道将数据渲染到这个离屏的纹理上。

加载操作(load action)决定了在渲染通道开始时,纹理的初始内容。这里我们给它设置的是.clear,表明在GPU绘制之前抹除渲染目标的内容。

存储操作(store action)只在渲染通道完成后执行,在这里,我们给它配置成.store,表明我们将使用这个纹理。

如图,这是离屏渲染产生的纹理

Screenshot 2024-12-17 at 14.13.59.png

创建渲染管道

// Offscreen pipeline
let offscreenPipelineDescriptor = MTLRenderPipelineDescriptor()
offscreenPipelineDescriptor.label = "Offscreen Render Pinepline"
offscreenPipelineDescriptor.vertexFunction = library?.makeFunction(name: "offscreenVertexShader")
offscreenPipelineDescriptor.fragmentFunction = library?.makeFunction(name: "offscreenFragmentShader")
offscreenPipelineDescriptor.colorAttachments[0].pixelFormat = offscreenTexture.pixelFormat
do {
    offscreenPipelineState = try device.makeRenderPipelineState(descriptor: offscreenPipelineDescriptor)
} catch {
    fatalError("fail to create offscreen pipelineState")
}

在这里,我们需要注意的是,描述符的像素格式pixelFormat需要和离屏纹理的像素格式一样。

离屏渲染纹理

let commanderEncoder = commanderBuffer?.makeRenderCommandEncoder(descriptor: offscreenRenderPassDescriptor)
commanderEncoder?.setRenderPipelineState(offscreenPipelineState)
let vertices = [
    //左下角
    OffscreenVertex(position: [-1.0, -1.0, 1.0, 1.0], color: [1.0, 0.0, 0.0, 1.0]),
    //正上方
    OffscreenVertex(position: [ 0.0,  1.0, 1.0, 1.0], color: [1.0, 0.5, 0.0, 1.0]),
    //右下角
    OffscreenVertex(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<OffscreenVertex>.stride * vertices.count)
commanderEncoder?.setVertexBuffer(vertexBuffer, offset: 0, index: 0)
commanderEncoder?.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: vertices.count)
commanderEncoder?.endEncoding()

在这里我们绘制了一个简单三角形,感到困惑的话请参阅之前的文章。

绘制纹理到屏幕

这里基本上是复制了渲染图片的过程

// Drawable Render Pass
guard let renderPassDesriptor = view.currentRenderPassDescriptor else {
    return
}
let commanderEncoder = commanderBuffer?.makeRenderCommandEncoder(descriptor: renderPassDesriptor)
commanderEncoder?.setRenderPipelineState(drawablePipelineState)
if let offscreenTexture {
    let vertices = [
        // 左上角
        Vertex(pixelPosition: [-256,  256], textureCoordinate: [0.0, 0.0]),
        // 左下角
        Vertex(pixelPosition: [-256, -256], textureCoordinate: [0.0, 1.0]),
        // 右下角
        Vertex(pixelPosition: [ 256, -256], textureCoordinate: [1.0, 1.0]),
        // 左上角
        Vertex(pixelPosition: [-256,  256], textureCoordinate: [0.0, 0.0]),
        // 右下角
        Vertex(pixelPosition: [ 256, -256], textureCoordinate: [1.0, 1.0]),
        // 右上角
        Vertex(pixelPosition: [ 256,  256], textureCoordinate: [1.0, 0.0]),
    ]
    let vertexBuffer = device.makeBuffer(bytes: vertices, length: MemoryLayout<Vertex>.stride * vertices.count)
    commanderEncoder?.setVertexBuffer(vertexBuffer, offset: 0, index: 0)
    commanderEncoder?.setVertexBytes(&viewPortSize, length: MemoryLayout<vector_int2>.stride, index: 1)
    commanderEncoder?.setFragmentTexture(offscreenTexture, index: 0)
    commanderEncoder?.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: vertices.count)
}
commanderEncoder?.endEncoding()
guard let drawable = view.currentDrawable else {
    return
}
commanderBuffer?.present(drawable)
commanderBuffer?.commit()

在第一个渲染通道结束后,我们将渲染得到的纹理交给第二个渲染通道,最终绘制到屏幕上。

Screenshot 2024-12-17 at 15.15.35.png

结语

离屏渲染作为图形渲染中的一种重要技术,广泛应用于图像处理、后期效果和复杂的渲染管线中。通过将渲染结果输出到内存缓冲区而非直接显示,离屏渲染不仅能有效提升性能,减少屏幕更新时的延迟,还能为后期处理和多重效果合成提供强大支持。在现代图形应用中,掌握和合理利用离屏渲染技术,无疑是提升渲染效率和视觉效果的重要手段。随着图形硬件的不断发展,离屏渲染在未来将继续发挥其关键作用,助力开发者实现更加精细和动态的视觉体验。

项目源码