Metal 框架之自定义设置渲染通道

1,155 阅读7分钟

「这是我参与11月更文挑战的第22天,活动详情查看:2021最后一次更文挑战

概述

渲染通道是一系列渲染命令,用于绘制一组纹理。本示例执行一对渲染通道来渲染视图的内容。对于第一个通道,示例创建了一个自定义渲染,将图像渲染成纹理。这个通道是一个离屏渲染通道,因为样本渲染为普通纹理,而不是由显示子系统创建的纹理。第二个渲染通道使用 MTKView 对象提供的渲染通道描述符,来渲染和显示最终图像。该示例使用离屏渲染通道的纹理作为第二个渲染通道绘制命令的源数据。

离屏渲染通道是更大或更复杂渲染器的基本构成要素。例如,许多光照和阴影算法需要一个离屏渲染通道来渲染阴影信息,并需要第二个通道来计算最终的场景光照。在对不需要在屏幕上显示的数据进行批处理时,离屏渲染通道也很有用。

创建离屏通道纹理

MTKView 对象会自动创建可绘制的纹理以进行渲染。本示例还需要一个离屏渲染通道中渲染的纹理。要创建该纹理,首先创建一个 MTLTextureDescriptor 对象并配置其属性。

MTLTextureDescriptor *texDescriptor = [MTLTextureDescriptor new];

texDescriptor.textureType = MTLTextureType2D;

texDescriptor.width = 512;

texDescriptor.height = 512;

texDescriptor.pixelFormat = MTLPixelFormatRGBA8Unorm;

texDescriptor.usage = MTLTextureUsageRenderTarget |

                      MTLTextureUsageShaderRead;

本示例配置 ‘usage’ 属性以准确说明它打算如何使用新纹理。它需要在离屏通道中将数据渲染到纹理,并在第二个通道中从纹理读取数据。该示例通过设置 renderTarget 和 shaderRead 标志来指定这种用法。

精确设置使用标志可以提高性能,因为 Metal 可以只为指定的用途配置纹理的底层数据。

创建渲染管线

渲染管线指定如何执行绘图命令,包括要执行的顶点和片元函数,以及它作用于的任何渲染目标的像素格式。之后,当示例创建自定义渲染通道时,它必须使用相同的像素格式。

本示例为每个渲染通道创建一个渲染管线,使用以下代码来创建离屏渲染管线:

pipelineStateDescriptor.label = @"Offscreen Render Pipeline";
pipelineStateDescriptor.sampleCount = 1;
pipelineStateDescriptor.vertexFunction =  [defaultLibrary newFunctionWithName:@"simpleVertexShader"];
pipelineStateDescriptor.fragmentFunction =  [defaultLibrary newFunctionWithName:@"simpleFragmentShader"];
pipelineStateDescriptor.colorAttachments[0].pixelFormat = _renderTargetTexture.pixelFormat;
_renderToTextureRenderPipeline = [_device newRenderPipelineStateWithDescriptor:pipelineStateDescriptor error:&error];

为可绘制渲染通道创建管道的代码与《 Metal 框架之渲染管线渲染图元 》中的代码类似。为了保证两种像素格式匹配,示例将描述符的像素格式设置为视图的 colorPixelFormat。类似地,在创建离屏渲染管道时,示例将描述符的像素格式设置为离屏纹理的格式。

配置离屏渲染通道描述符

为了渲染到离屏纹理,示例配置了一个新的渲染通道描述符。创建一个 MTLRenderPassDescriptor 对象并配置其属性。由于本示例渲染到单色纹理,所以将 colorAttachment[0].texture 设置为指向离屏纹理:

_renderToTextureRenderPassDescriptor.colorAttachments[0].texture = _renderTargetTexture;

本示例还必须为此渲染目标配置加载操作和存储操作。

_*renderToTextureRenderPassDescriptor.colorAttachments[0].loadAction = MTLLoadActionClear;

_renderToTextureRenderPassDescriptor.colorAttachments[0].clearColor = MTLClearColorMake(1, 1, 1, 1);

_renderToTextureRenderPassDescriptor.colorAttachments[0].storeAction = MTLStoreActionStore;

在 GPU 执行任何绘图命令之前,加载动作在渲染通道开始时确定纹理的初始内容。类似地,在渲染通道完成后运行存储操作,并确定 GPU 是否将最终图像写回纹理。该示例配置了一个加载操作来擦除渲染目标的内容,以及一个存储操作来将渲染数据存储回纹理。存储操作是必须要执行的,因为第二个渲染通道中的绘图命令将对该数据进行采样。

Metal 使用加载和存储操作来优化 GPU 管理纹理数据的方式。大纹理会消耗大量内存,处理这些纹理会消耗大量内存带宽。正确设置渲染目标操作可以减少 GPU 用于访问纹理的内存带宽量,从而提高性能和电池寿命。请参阅 《 Metal 框架之设置加载和存储操作 》 来获取更加详细的指导。

渲染通道描述符具有本示例中未使用的其他属性,可进一步修改渲染过程。有关自定义渲染通道描述符的其他方法的信息,请参阅 MTLRenderPassDescriptor。

渲染到离屏纹理

本示例囊括了对两个渲染通道进行编码所需要的所有内容。在查看示例如何编码渲染通道之前,了解 Metal 如何在 GPU 上调度命令非常重要。当 App 将命令缓冲区提交到命令队列时,默认情况下,Metal 按照队列中的顺序执行命令。为了提高性能并更好地利用 GPU,Metal 可以并发运行命令,只要这样做不会产生与顺序执行不一致的结果。为了实现这一点,当一个通道写入资源,并且之后的通道从中读取时(如本示例中所示),Metal 会检测依赖项,并自动延迟后一个通道的执行,直到第一个通道完成。所以与《 Metal 框架之同步 CPU 与 GPU 工作 》 不同,在 《 Metal 框架之同步 CPU 与 GPU 工作 》中 CPU和GPU需要显式同步,本示例不需要做任何特殊的事情。它只是按顺序对两个通道进行编码,Metal 确保它们按该顺序运行。

该示例从离屏渲染通道开始,将两个渲染通道编码到一个命令缓冲区中。它使用之前创建的离屏渲染通道描述符来创建渲染一个命令编码器。 


id<MTLRenderCommandEncoder> renderEncoder =

    [commandBuffer renderCommandEncoderWithDescriptor:_renderToTextureRenderPassDescriptor];

renderEncoder.label = @"Offscreen Render Pass";

[renderEncoder setRenderPipelineState:_renderToTextureRenderPipeline];

渲染通道中的其他所有内容都与《 Metal 框架之渲染管线渲染图元 》类似。它配置管道和任何必要的参数,然后对绘图命令进行编码。命令编码完成后,调用endEncoding()完成编码过程。

[renderEncoder endEncoding];

多个通道必须按顺序编码到命令缓冲区中,因此示例必须在下一个渲染通道开始之前完成第一个渲染通道的编码。

渲染到可绘制纹理

第二个渲染通道需要渲染最终图像。可绘制渲染管道的片段着色器从纹理中采样数据并将该采样作为最终颜色返回:


// Fragment shader that samples a texture and outputs the sampled color.

fragment float4 textureFragmentShader(TexturePipelineRasterizerData in      [[stage_in]],

                                      texture2d<float>              texture [[texture(AAPLTextureInputIndexColor)]])

{

    sampler simpleSampler;

    // Sample data from the texture.

    float4 colorSample = texture.sample(simpleSampler, in.texcoord);

    // Return the color sample as the final color.

    return colorSample;

}

本示例使用视图的渲染通道描述符来创建第二个渲染通道,并编码绘图命令以渲染纹理四边形。将离屏纹理指定为命令的纹理参数。

id<MTLRenderCommandEncoder> renderEncoder =

    [commandBuffer renderCommandEncoderWithDescriptor:drawableRenderPassDescriptor];

renderEncoder.label = @"Drawable Render Pass";

[renderEncoder setRenderPipelineState:_drawableRenderPipeline];

[renderEncoder setVertexBytes:&quadVertices
                       length:sizeof(quadVertices)
                      atIndex:AAPLVertexInputIndexVertices];
[renderEncoder setVertexBytes:&_aspectRatio
                       length:sizeof(_aspectRatio)
                        atIndex:AAPLVertexInputIndexAspectRatio];



// Set the offscreen texture as the source texture.

[renderEncoder setFragmentTexture:_renderTargetTexture atIndex:AAPLTextureInputIndexColor]

当提交命令缓冲区时,Metal 会依次执行两个渲染通道。在这种情况下,Metal 检测到第一个渲染通道写入离屏纹理,第二个通道从纹理读取数据。当 Metal 检测到这种依赖时,它会阻止后续的通道的执行,直到 GPU 执行完第一个通道。

总结

本文介绍了如何通过创建一个自定义渲染通道,来渲染离屏纹理。本示例执行一对渲染通道来渲染视图的内容。详细介绍了创建两个渲染通道的步骤,以及如何设置的渲染顺序。

本文示例代码下载