引言
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.renderTarget和 MTLTextureUsage.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,表明我们将使用这个纹理。
如图,这是离屏渲染产生的纹理
创建渲染管道
// 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()
在第一个渲染通道结束后,我们将渲染得到的纹理交给第二个渲染通道,最终绘制到屏幕上。
结语
离屏渲染作为图形渲染中的一种重要技术,广泛应用于图像处理、后期效果和复杂的渲染管线中。通过将渲染结果输出到内存缓冲区而非直接显示,离屏渲染不仅能有效提升性能,减少屏幕更新时的延迟,还能为后期处理和多重效果合成提供强大支持。在现代图形应用中,掌握和合理利用离屏渲染技术,无疑是提升渲染效率和视觉效果的重要手段。随着图形硬件的不断发展,离屏渲染在未来将继续发挥其关键作用,助力开发者实现更加精细和动态的视觉体验。