006-图形渲染:渲染命令编码器

图形渲染:渲染命令编码器

这一章节讨论如何创建和使用 MTLRenderCommandEncoder、 MTLParallelRenderCommandEncoder 对象,它们均用于将图形渲染命令编码到命令缓冲中。图 5-1 表示如何用 MTLRenderCommandEncoder 描述一次图形渲染流水线。

006-图形渲染:渲染命令编码器_第1张图片
image

图元-顶点着色器函数-光栅化-片段着色器函数-裁剪-多重采样-模版检测-深度检测-可视化结果-混叠-附件

MTLRenderCommandEncoder 对象代表一个独立的渲染命令编码器,MTLParallelRenderCommandEncoder 对象则允许将一次渲染流程拆分成一些独立的 MTLRenderCommandEncoder 对象,每一个都可能被分配到一个独立线程上。不同的渲染命令编码器里的命令最终被链接到一起,并按照连续、可预测的顺序被执行,详细内容请查阅 多线程下的渲染流程

创建和使用一个渲染命令编码器

创建、初始化和使用一个渲染命令编码器的步骤如下:

  • 创建一个 MTLRenderPassDescriptor 对象来定义一系列的附件,这些附件将作为此次渲染流程中的命令缓冲里的图形命令的渲染目标。通常你可以创建一次 MTLRenderPassDescriptor 对象后,每次渲染一帧的时候复用它。具体查阅 创建一个渲染流程描述符
  • 调用 MTLCommandBuffer 的 renderCommandEncoderWithDescriptor: 方法,传入一个渲染流程描述符,创建一个 MTLRenderCommandEncoder 对象。具体查阅 利用渲染流程描述符创建一个渲染命令编码器
  • 创建一个 MTLRenderPipelineState 对象来为一个或多个绘制调用定义其图形渲染流水线的状态(包括着色器、混叠、多重采样、可视化测试等)。调用 MTLRenderCommandEncoder 的 setRenderPipelineState: 设置渲染流水线状态以绘制图元,细节请查阅 创建一个渲染流水线状态
  • 设置命令编码器所需要使用的纹理、缓冲和采样器,详细过程在 为一个渲染命令编码器确定资源
  • 调用 MTLRenderCommandEncoder 的方法确定额外的固定功能状态,包括深度和模版状态,在 固定功能状态操作 中有介绍
  • 最终,调用 MTLRenderCommandEncoder 的方法绘制图元,在 绘制几何图元 有介绍

创建一个渲染流程描述符

一个渲染流程描述符代表了已编码渲染命令的目标,它是一系列的附件。一个渲染流程描述符的属性可能包含了最多四个颜色像素数据附件,一个深度像素数据附件,一个模版像素数据附件。便捷方法 renderPassDescriptor 将采用颜色、深度和模版附件属性的默认值创建一个 MTLRenderPassDescriptor 对象。visibilityResultBuffer 属性确定了一个缓冲区,设备可以更新它以表示是否有图形采样通过了深度检测和模版检测,[固定功能状态操作](Fixed-Function State Operations) 有介绍。

每一个附件,包括将被写入的纹理,都被一个附件描述符表示。对于附件描述符,必须选择合适的关联纹理的像素格式来存储颜色、深度或模版数据。对于颜色附件描述符 MTLRenderPassColorAttachmentDescriptor,需要使用颜色渲染像素格式。对于一个深度附件描述符 MTLRenderPassDepthAttachmentDescriptor,需要使用深度渲染像素格式,如 MTLPixelFormatDepth32Float。对于一个模版附件描述符 MTLRenderPassStencilAttachmentDescriptor,必须使用一个模版渲染像素格式,如 MTLPixelFormatStencil8。

纹理的每一个像素在设备上实际使用到的内存大小并不总是与它在 Metal 中定义的像素格式大小匹配,因为设备为了对齐或其他目的会添加一些填充位。可以查阅 Metal 特性集合表 了解每一个像素格式实际使用多大存储空间,以及附件的尺寸和个数。

加载和存储操作

一个附件描述符的 loadAction 和 storeAction 属性明确了将在渲染流程的开始或结束时执行操作。(对于 MTLParallelRenderCommandEncoder,加载和存储操作将出现在整体命令的边界处,而不是每一个 MTLRenderCommandEncoder 对象的边界处,具体查阅 多线程下的渲染流程)。

loadAction 可能值有:

  • MTLLoadActionClear,在某一附件描述符的每一个像素内写入相同值,查阅 明确清除加载操作 了解更多内容
  • MTLLoadActionLoad,保留纹理的当前内容
  • MTLLoadActionDontCare,在渲染流程开始时允许附件内的每个像素内存储任何值

如果你的应用在给定帧内会渲染所有的像素,那么使用默认的存储操作 MTLLoadActionClear 即可。MTLLoadActionDontCare 允许 GPU 避免加载纹理当前内容,从而保证最佳性能。否则你可以使用 MTLLoadActionClear 操作来清除附件之前的内容,或者使用 MTLLoadActionLoad 操作来保留它们。MTLLoadActionClear 也能避免加载纹理当前内容,但是用一种固定颜色填充目标区域会耗费一定的性能。

storeAction 可能值有:

  • MTLStoreActionStore,保存渲染流程最终结果到附件内
  • MTLStoreActionMultisampleResolve,将渲染目标额多重采样数据解析为单重采样,将它们存储到由纹理描述符的 resolveTexture 属性确定的纹理中,留下未定义的附件内容,查阅 示例:为多重渲染创建渲染流程描述符 了解更多内容。
  • MTLStoreActionDontCare,当渲染流程结束后,将附件内容保持为未定义状态,由于避免了为保持渲染结果的一些必要操作,因此它可以提高性能

对于颜色附件,MTLStoreActionStore 是默认值,因为应用几乎总是需要在渲染流程结束时,存储附件里最终的颜色值。对于深度和模版附件, MTLStoreActionDontCare 是默认值,因为对于这些附件,一般并不需要在渲染流程结束后存储结果。

明确清除加载操作

如果一个附件描述符的 loadAction 属性被设置为 MTLLoadActionClear,那么一个清除值将会在渲染流程开始时写入到附件描述符的每一个像素内,这个清除值取决于附件描述符的类型:

  • 对于 MTLRenderPassColorAttachmentDescriptor,clearColor 属性包含一个 MTLClearColor 值,它由四个 double 精度的浮点 RGBA 分量组成,被用于清除颜色附件。MTLClearColorMake 函数可以从红、绿、蓝以及一个 alpha 分量参数中创建一个清除颜色,默认色值为 MTLClearColorMake,即不透明纯黑色。
  • 对于 MTLRenderPassDepthAttachmentDescriptor,clearDepth 属性包含一个 double 精度的浮点清除值,范围在 [0.0, 1.0] 之间,默认值为 1.0。
  • 对于 MTLRenderPassStencilAttachmentDescriptor,clearStencil 包含一个 32 位无符号整数,被用于清除模版附件,默认值为 0。

示例:创建一个包含加载和存储操作的渲染流程描述符

Listing 5-1 创建了一个简单的包含颜色和深度附件的渲染流程描述符。首先,创建了两个纹理对象,分别是色彩渲染格式和深度渲染格式。接下来调用 MTLRenderPassDescriptor 的便捷方法 renderPassDescriptor 创建一个默认的渲染流程描述符。然后通过 MTLRenderPassDescriptor 的属性访问颜色和深度附件。将纹理和操作赋值到 colorAttachments[0](代表第一个颜色附件)和深度附件上。

Listing 5-1

MTLTextureDescriptor *colorTexDesc = [MTLTextureDescriptor
           texture2DDescriptorWithPixelFormat:MTLPixelFormatRGBA8Unorm
           width:IMAGE_WIDTH height:IMAGE_HEIGHT mipmapped:NO];
id  colorTex = [device newTextureWithDescriptor:colorTexDesc];
 
MTLTextureDescriptor *depthTexDesc = [MTLTextureDescriptor
           texture2DDescriptorWithPixelFormat:MTLPixelFormatDepth32Float
           width:IMAGE_WIDTH height:IMAGE_HEIGHT mipmapped:NO];
id  depthTex = [device newTextureWithDescriptor:depthTexDesc];
 
MTLRenderPassDescriptor *renderPassDesc = [MTLRenderPassDescriptor renderPassDescriptor];
renderPassDesc.colorAttachments[0].texture = colorTex;
renderPassDesc.colorAttachments[0].loadAction = MTLLoadActionClear;
renderPassDesc.colorAttachments[0].storeAction = MTLStoreActionStore;
renderPassDesc.colorAttachments[0].clearColor = MTLClearColorMake(0.0,1.0,0.0,1.0);
 
renderPassDesc.depthAttachment.texture = depthTex;
renderPassDesc.depthAttachment.loadAction = MTLLoadActionClear;
renderPassDesc.depthAttachment.storeAction = MTLStoreActionStore;
renderPassDesc.depthAttachment.clearDepth = 1.0;

示例:为多重采样渲染创建渲染流程描述符

为了使用 MTLStoreActionMultisampleResolve 操作,你必须设置 texture 属性为一个多重采样类型的纹理,resolveTexture 属性将包含多重采样解析结果(如果设置的纹理不支持多重采样,则多重采样解析结果是不确定的)。resolveLevel,resolveSlice 和 resolveDepthPlane 属性分别用于确定纹理贴图层级、立方体切片、多重采样深度平面,也必须被用于多重采样操作。大部分情况下,resolveLevel、resolveSlice 和 resolveDepthPlane 的默认值就足够了。在 Listing 5-2 中,初始化创建了一个附件,并设置它的 loadAction、storeAction、texture 和 resolveTexture 以支持多重采样解析。

Listing 5-2

MTLTextureDescriptor *colorTexDesc = [MTLTextureDescriptor
           texture2DDescriptorWithPixelFormat:MTLPixelFormatRGBA8Unorm
           width:IMAGE_WIDTH height:IMAGE_HEIGHT mipmapped:NO];
id  colorTex = [device newTextureWithDescriptor:colorTexDesc];
 
MTLTextureDescriptor *msaaTexDesc = [MTLTextureDescriptor
           texture2DDescriptorWithPixelFormat:MTLPixelFormatRGBA8Unorm
           width:IMAGE_WIDTH height:IMAGE_HEIGHT mipmapped:NO];
msaaTexDesc.textureType = MTLTextureType2DMultisample;
msaaTexDesc.sampleCount = sampleCount;  //  must be > 1
id  msaaTex = [device newTextureWithDescriptor:msaaTexDesc];
 
MTLRenderPassDescriptor *renderPassDesc = [MTLRenderPassDescriptor renderPassDescriptor];
renderPassDesc.colorAttachments[0].texture = msaaTex;
renderPassDesc.colorAttachments[0].resolveTexture = colorTex;
renderPassDesc.colorAttachments[0].loadAction = MTLLoadActionClear;
renderPassDesc.colorAttachments[0].storeAction = MTLStoreActionMultisampleResolve;
renderPassDesc.colorAttachments[0].clearColor = MTLClearColorMake(0.0,1.0,0.0,1.0);

使用渲染流程描述符创建一个渲染命令编码器

创建完渲染流程描述符后,你可以通过调用 MTLCommandBuffer 对象的 renderCommandEncoderWithDescriptor 方法来创建一个渲染命令编码器,如 Listing 5-3 代码所示

Listing 5-3

id  renderCE = [commandBuffer renderCommandEncoderWithDescriptor:renderPassDesc];

通过 CoreAnimation 展示渲染内容

CoreAnimation 定义了 CAMetalLayer 类,这个类被设计用于支持 Metal 渲染图层内容的视图的一些特定行为。一个 CAMetalLayer 对象能够呈现内容几何信息(位置和尺寸)、可视化属性(背景颜色、边框和阴影)、以及 Metal 呈现颜色附件内容所需的资源。它同时包含了内容呈现的时间安排,从而确保内容能够尽可能在准备就绪后展示出来,或是在一个特定时间展示出来。有关 CoreAnimation 更多信息请查阅 CoreAnimation 编程指南。

CoreAnimation 还为展示资源的对象定义了 CAMetalDrawable,它继承自 MTLDrawable,提供了遵循 MTLTexture 协议的对象,因此可以被渲染命令用作渲染目标。为了渲染到 CAMetalLayer,你需要为每一次渲染流程获取一个新的 CAMetalDrawable 对象,获取这个 CAMetalDrawable 对象所提供的 MTLTexture 对象,并使用这个 MTLTexture 对象创建颜色附件。与颜色附件不同的是,深度和模版附件的创建是很消耗性能的,因此如果你需要使用深度附件或者模版附件,最好只创建它们一次,然后在每次渲染一帧的时候复用它们。

通常来说你可以使用 layerClass 方法指定 CAMetalLayer 为你的自定义 UIView 子类的底层 layer 类型,如 Listing 5-4 所示。或者你也可以通过 CAMetalLayer 的 init 方法创建并持有一个 CAMetalLayer 层级。

Listing 5-4

+ (id) layerClass {
    return [CAMetalLayer class];
}

为了在 layer 中展示由 Metal 渲染的内容,你必须从 CAMetalLayer 对象中获取一个可展示资源(一个 CAMetalDrawable 对象),然后通过将其绑定到一个 MTLRenderPassDescriptor 对象来将内容渲染到此资源中的纹理。要做到这一点,你需要先设置 CAMetalLayer 提供的描述可展示资源的属性,然后在每次开始渲染新的一帧前调用它的 nextDrawable 方法。如果 CAMetalLayer 的属性没有设置,则调用 nextDrawable 将会失败。下面的 CAMetalLayer 属性描述了绘制对象:

  • device 声明资源从哪个 MTLDevice 对象创建而来
  • pixelFormat 声明纹理的像素格式,可选值为 MTLPixelFormatBGRA8Unorm(默认)和 MTLPixelFormatBGRA8Unorm_sRGB
  • drawableSize 定义了设备像素内纹理的尺寸,为了确保 app 在精确尺寸下渲染内容,不带来额外的采样阶段,你需要在计算 layer 的理想尺寸时考虑目标屏幕的 nativeScale 或 nativeBounds
  • framebufferOnly 属性声明是否纹理只能被用于附件(YES),还是可以被用于纹理采样、像素读写操作(NO)。如果是 YES,则 layer 对象会优化纹理的展示。大部分 app 推荐设置为 YES
  • presentsWithTransaction 属性声明是否对于 layer 呈现的资源的修改会按照标准 CoreAnimation 转换机制进行更新(YES),否则与正常 layer 更新异步执行(NO,默认)

如果 nextDrawable 方法成功,则会返回一个包含下列可读属性的 CAMetalDrawable 对象:

  • texture 属性包含纹理对象,你可以在创建渲染流水线(MTLRenderPipelineColorAttachmentDescriptor 对象)时将其作为附件使用
  • layer 属性指向负责展示绘制内容的 CAMetalLayer 对象

绘制资源数目紧缺,因此一个较长的帧渲染时间会暂时耗尽这些绘制资源,导致 nextDrawable 的调用阻塞它的 CPU 线程,直到这个调用完成。为了避免昂贵的 CPU 调用,应当在调用 nextDrawable 前执行完所有不需要绘制资源的帧操作。

你必须调用绘制资源的 present 方法将绘制资源提交到 Core Animation,才能在渲染完成后展示出绘制资源内的内容。为了同步绘制资源的展示与命令缓冲渲染的完成,你可以调用 MTLCommandBuffer 对象的便捷方法 presentDrawable:presentDrawable:atTime:,这些方法会使用已调度回调(查阅 为命令缓冲执行注册回调 block)来调用绘制资源的 present 方法,这基本能覆盖所有场景。presentDrawable:atTime: 还可以控制绘制资源何时被展现。

创建一个渲染流水线状态

为了使用 MTLRenderCommandEncoder 对象来编码渲染命令,你必须首先确定一个 MTLRenderPipelineState 对象来定义任何绘制调用中的图形状态。一个渲染流水线状态对象是一个常驻对象,它可以不依赖于渲染命令编码器而创建,提前被缓存,并在多个渲染命令编码器之间复用。当描述同一组图形状态时,复用一个提前创建的渲染流水线状态对象能够避免重新评估和翻译确定状态到 GPU 命令的昂贵操作。

一个渲染流水线状态对象是一个不可变对象,为了创建一个渲染流水线状态,你需要首先创建并配置一个可变的 MTLRenderPipelineDescriptor 对象,它描述了一个渲染流水线状态的属性。然后利用这个 MTLRenderPipelineDescriptor 对象就可以创建一个 MTLRenderPipelineState 对象。

创建并配置一个渲染流水线

为了创建一个渲染流水线状态对象,首先要创建一个 MTLRenderPipelineDescriptor 对象,这个对象的属性描述了你希望在渲染流程中使用的图形渲染流水线状态,具体由下图描述。新创建的 MTLRenderPipelineDescriptor 对象的 colorAttachments 属性包含一个 MTLRenderPipelineColorAttachmentDescriptor 对象的数组,每一个描述符代表了一个指定了混合操作及相关因素的颜色附件状态,具体在 在渲染流水线附件描述符中配置混合 有介绍。附件描述符同样确定了附件的像素格式,它必须符合渲染流水线描述符对应下标下的纹理的像素格式,否则就会报错。

006-图形渲染:渲染命令编码器_第2张图片
image

为了配置颜色附件,需要设置 MTLRenderPipelineDescriptor 对象的下面这些属性:

  • 设置 depthAttachmentPixelFormat 以符合 MTLRenderPipelineDescriptor 的 depthAttachment 里的纹理像素格式
  • 设置 stencilAttachmentPixelFormat 属性以符合 MTLRenderPipelineDescriptor 的 stencilAttachment 里的纹理像素格式
  • 为了确定渲染流水线状态的顶点和片段着色器,需要分别设置 vertexFunction 或 fragmentFunction 属性。设置 fragmentFunction 为 nil 会关闭将像素光栅化为指定颜色附件的功能,这一般用于仅有深度渲染的过程,或是用于从顶点着色器将数据输出到缓冲区对象
  • 如果顶点着色器有一个依赖特定顶点的输入属性的参数,需要设置 vertexDescriptor 属性来描述这个参数内的顶点数据格式,具体在 数据组织中的顶点描述符 介绍
  • rasterizationEnabled 属性的默认值 YES 能够应用于大多数渲染场景中,可以在仅使用顶点阶段的流水线中(例如收集在顶点着色器中变换的数据)将这个属性设置为 NO
  • 如果附件支持多重采样(即附件是一个 MTLTextureType2DMultisample 类型的纹理),则可为每个像素创建多重采样。为了确定片段如何组合以提供像素覆盖,可以使用下面的 MTLRenderPipelineDescriptor 属性
    • sampleCount 确定每个像素的采样次数。MTLRenderCommandEncoder 创建后,所有附件里的每个纹理对象的 sampleCount 必须与这个 sampleCount 属性一致。不支持多重采样的附件的 sampleCount 为默认值 1
    • alphaToCoverageEnabled 设置为 YES 时,colorAttachments[0] 的 alpha 通道片段输出会被读取并用于确定覆盖掩码
    • alphaToOneEnabled 设置为 YES 时,colorAttachments[0] 的 alpha 通道片段值将强制设置为 1.0,这是最大可表示的值。(其他附件不受影响)

利用描述符创建一个渲染流水线状态

创建完一个渲染流水线描述符并确定了它的属性后,可以使用描述符来创建一个 MTLRenderPipelineState 对象。由于创建一个渲染流水线状态对象需要昂贵的图形状态评估和可能的图形着色器汇编,你可以使用阻塞或者异步的方式,在适合于你的应用设计的情况下安置这样的工作。

  • 同步创建渲染流水线状态对象需要调用 MTLDevice 的 newRenderPipelineStateWithDescriptor:error: 或者 newRenderPipelineStateWithDescriptor:options:reflection:error:,这些方法会阻塞当前线程,直到 Metal 评估完描述符的图形状态信息,编译着色器代码,创建一个流水线状态对象
  • 异步创建渲染流水线状态对象需要调用 MTLDevice 的 newRenderPipelineStateWithDescriptor:completionHandler:newRenderPipelineStateWithDescriptor:options:completionHandler: 方法,这些方法会立即返回,Metal 异步地评估描述符图形状态信息,并编码着色器代码,创建完流水线状态对象后,调用你的完成回调返回新创建的 MTLRenderPipelineState 对象

创建了 MTLRenderPipelineState 对象以后,你也可以选择创建流水线着色器函数及其参数的反射数据。newRenderPipelineStateWithDescriptor:options:reflection:error:newRenderPipelineStateWithDescriptor:options:completionHandler: 方法能提供这样的数据。如果你不使用这些数据,应当尽量避免持有它。更多信息请查阅 运行时确定函数细节

创建完 MTLRenderPipelineState 后可以通过 MTLRenderCommandEncoder 的 setRenderPipelineState: 方法将渲染流水线状态与命令编码器绑定,并用于渲染。

Listing 5-5 展示了如何创建一个名为 pipeline 的渲染流水线状态

MTLRenderPipelineDescriptor *renderPipelineDesc =
                             [[MTLRenderPipelineDescriptor alloc] init];
renderPipelineDesc.vertexFunction = vertFunc;
renderPipelineDesc.fragmentFunction = fragFunc;
renderPipelineDesc.colorAttachments[0].pixelFormat = MTLPixelFormatRGBA8Unorm;
 
// Create MTLRenderPipelineState from MTLRenderPipelineDescriptor
NSError *errors = nil;
id  pipeline = [device
         newRenderPipelineStateWithDescriptor:renderPipelineDesc error:&errors];
assert(pipeline && !errors);
 
// Set the pipeline state for MTLRenderCommandEncoder
[renderCE setRenderPipelineState:pipeline];

变量 vertFunc 和 fragFunc 是 renderPipelineDesc 渲染流水线状态描述符的着色器函数属性,调用 MTLDevice 的 newRenderPipelineStateWithDescriptor:error: 方法会同步地使用流水线状态描述符来创建渲染流水线状态对象,调用 MTLRenderCommandEncoder 的 setRenderPipelineState: 方法确定将使用这个 MTLRenderPipelineState 对象来关联渲染命令编码器。

注意:由于 MTLRenderPipelineState 对象的创建非常耗费性能,你应当尽可能在使用相同的图形状态时复用它。

在渲染流水线附件描述符中配置混合

混合是一个高度配置化的操作,能够将片段着色器的输出(source)与附件里的像素值(destination)进行组合展示。混合操作决定如何根据混合参数,将 source 值与 destination 值进行组合。

为一个颜色附件配置混合操作时,需要设置下列 MTLRenderPipelineColorAttachmentDescriptor 属性

  • 设置 blendingEnabled 为 YES 以启动混合操作,默认情况下混合操作是关闭的
  • writeMask 决定哪些颜色通道可以混合。默认值 MTLColorWriteMaskAll 允许所有颜色通道进行混合
  • rgbBlendOperation 和 alphaBlendOperation 分别用 MTLBlendOperation 类型的值声明 rgb 和 alpha 片段数据的混合操作。两个属性默认值都是 MTLBlendOperationAdd
  • sourceRGBBlendFactor、sourceAlphaBlendFactor、destinationRGBBlendFactor、和 destinationAlphaBlendFactor 分别确定 source 和 destination 的混合参数

理解混合参数和操作

有四个标识常量混合颜色值的参数:MTLBlendFactorBlendColor、MTLBlendFactorOneMinusBlendColor、MTLBlendFactorBlendAlpha、和MTLBlendFactorOneMinusBlendAlpha。调用 MTLRenderCommandEncoder 的 setBlendColorRed:green:blue:alpha: 方法以确定这些混合参数所使用的色值和 alpha 值,具体介绍在 固定函数状态操作 中。

一些混合操作会将 source 值乘以一个源 MTLBlendFactor 值(简称 SBF),将 destination 值乘以一个目标 MTLBlendFactor(简称 DBF),然后通过 MTLBlendOperation 确定的算术运算计算出结果(如果混合操作是 MTLBlendOperationMin 或 MTLBlendOperationMax,SBF 和 DBF 将被忽略)。例如,如果将 rgbBlendOperation 和 alphaBlendOperation 均设置为 MTLBlendOperationAdd,那么 RGB 和 Alpha 值的混合操作将被定义如下

  • RGB = (Source.rgb * sourceRGBBlendFactor) + (Dest.rgb * destinationRGBBlendFactor)
  • Alpha = (Source.a * sourceAlphaBlendFactor) + (Dest.a * destinationAlphaBlendFactor)

默认的混合模式下,source 值将直接覆写 destination 值。它等同于将 sourceRGBBlendFactor 和 sourceAlphaBlendFactor 设置为 MTLBlendFactorOne,然后将 destinationRGBBlendFactor 和 destinationAlphaBlendFactor 设置为 MTLBlendFactorZero,具体公式如下

  • RGB = (Source.rgb * 1.0) + (Dest.rgb * 0.0)
  • A = (Source.a * 1.0) + (Dest.a * 0.0)

更普遍的情况下,source 的 alpha 值将用来确定目标色值需要保留多少,具体公式如下

  • RGB = (Source.rgb * 1.0) + (Dest.rgb * (1 - Source.a))
  • A = (Source.a * 1.0) + (Dest.a * (1 - Source.a))

使用一个自定义的混合配置

listing 5-6 展示了自定义混合配置的代码,使用了 MTLBlendOperationAdd 混合操作,源混合参数为 MTLBlendFactorOne,目标混合参数为 MTLBlendFactorOneMinusSourceAlpha,colorAttachments[0] 是一个 MTLRenderPipelineColorAttachmentDescriptor 对象,它具有能够配置混合操作的属性。

// Listing 5-6  Specifying a Custom Blending Configuration
MTLRenderPipelineDescriptor *renderPipelineDesc = 
                             [[MTLRenderPipelineDescriptor alloc] init];
renderPipelineDesc.colorAttachments[0].blendingEnabled = YES; 
renderPipelineDesc.colorAttachments[0].rgbBlendOperation = MTLBlendOperationAdd;
renderPipelineDesc.colorAttachments[0].alphaBlendOperation = MTLBlendOperationAdd;
renderPipelineDesc.colorAttachments[0].sourceRGBBlendFactor = MTLBlendFactorOne;
renderPipelineDesc.colorAttachments[0].sourceAlphaBlendFactor = MTLBlendFactorOne;
renderPipelineDesc.colorAttachments[0].destinationRGBBlendFactor = 
       MTLBlendFactorOneMinusSourceAlpha;
renderPipelineDesc.colorAttachments[0].destinationAlphaBlendFactor = 
       MTLBlendFactorOneMinusSourceAlpha;

NSError *errors = nil;
id  pipeline = [device 
         newRenderPipelineStateWithDescriptor:renderPipelineDesc error:&errors];

为渲染命令编码器确定资源

本节讨论的 MTLRenderCommandEncoder 方法都是用于确定在顶点着色器函数和片段着色器函数中使用的资源的,其中顶点着色器函数和片段着色器函数由 MTLRenderPipelineState 对象的 vertexFunction 和 fragmentFunction 属性确定。这些方法向渲染命令着色器对应的变量表下标(atIndex)分配着色器资源(缓冲、纹理和采样器),如下图所示

006-图形渲染:渲染命令编码器_第3张图片
image

下列 setVertex 前缀方法能够分配一个或多个资源到对应的顶点着色器函数的参数上

  • setVertexBuffer:offset:atIndex:
  • setVertexBuffers:offsets:withRange:
  • setVertexTexture:atIndex:
  • setVertexTextures:withRange:
  • setVertexSamplerState:atIndex:
  • setVertexSamplerState:lodMinClamp:lodMaxClamp:atIndex:
  • setVertexSamplerStates:withRange:
  • setVertexSamplerStates:lodMinClamps:lodMaxClamps:withRange:

下列 setFragment 前缀方法能够分配一个或多个资源到对应的片段着色器函数的参数上

  • setFragmentBuffer:offset:atIndex:
  • setFragmentBuffers:offsets:withRange:
  • setFragmentTexture:atIndex:
  • setFragmentTextures:withRange:
  • setFragmentSamplerState:atIndex:
  • setFragmentSamplerState:lodMinClamp:lodMaxClamp:atIndex:
  • setFragmentSamplerStates:withRange:
  • setFragmentSamplerStates:lodMinClamps:lodMaxClamps:withRange:

缓冲参数表最多可以有 31 个实体,纹理参数表最多可以有 31 个实体,采样器状态参数表最多可以有 16 个实体。

Metal 着色器语言代码(MSLC)中用于声明资源位置的属性限定符必须与 Metal framework 的函数确定的参数表下标对应。在 Listing 5-7 中,两个下标分别是 0 和 1 的缓冲(posBuf 和 texCoordBuf)分别分配到了顶点着色器

// Listing 5-7  Metal Framework: Specifying Resources for a Vertex Function

[renderEnc setVertexBuffer:posBuf offset:0 atIndex:0];
[renderEnc setVertexBuffer:texCoordBuf offset:0 atIndex:1];

在 Listing 5-8 中,函数签名必须有相应的参数带上对应的属性限定符 buffer(0) 和 buffer(1)

// Listing 5-8  Metal Shading Language: Vertex Function Arguments Match the Framework Argument Table Indices

vertex VertexOutput metal_vert(float4 *posData [[ buffer(0) ]],
                               float2 *texCoordData [[ buffer(1) ]])

类似的,在 Listing 5-9 中,将一个缓冲、一个纹理以及一个采样器(分别对应 fragmentColorBuf、shadeTex、和 sampler 变量)定义到了片段着色器上,且下标均为 0。

// Listing 5-9  Metal Framework: Specifying Resources for a Fragment Function

[renderEnc setFragmentBuffer:fragmentColorBuf offset:0 atIndex:0];
[renderEnc setFragmentTexture:shadeTex atIndex:0];
[renderEnc setFragmentSamplerState:sampler atIndex:0];

在 Listing 5-10 中,函数签名里也有对应参数 buffer(0)、texture(0) 和 sampler(0) 分别用属性限定符进行修饰

// Listing 5-10  Metal Shading Language: Fragment Function Arguments Match the Framework Argument Table Indices

fragment float4 metal_frag(VertexOutput in [[stage_in]],
                           float4 *fragColorData [[ buffer(0) ]],
                           texture2d shadeTexValues [[ texture(0) ]],
                           sampler samplerValues [[ sampler(0) ]] )

数据结构的顶点描述符

译者:

这一节的方法主要用于交叉存储顶点数据到一个缓冲区的场景,通常不同的顶点数据都是被存储到不同的 buffer 中,有特定的结构。

The same shader can be used with different vertex descriptors and, thus, different distribution of vertex attributes across buffers. That provides flexibility for different scenarios, some where the vertex attributes are separated out into different buffers (similar to the first example I gave) and others where they're interleaved in the same buffer (similar to the second example).

在 Metal 框架中,每个管线状态可以有一个 MTLVertexDescriptor 对象,用于描述输入到顶点着色器功能的数据的结构,并在着色语言和框架代码之间共享资源位置信息。

在 MSLC 中,每顶点输入(例如整数、浮点数类型的标量或矢量)都可以被组织成一个结构体,它可以通过一个用 [[stage_in]] 属性限定符声明的参数传递。Listing 5-11 示例展示了 VertexInput 结构体如何在示例顶点函数 vertexMath 中充当参数。每顶点输入结构的每个字段都有 [[attribute(index)]] 限定符,它指定了参数在顶点属性参数表中的索引。

// Listing 5-11  Metal Shading Language: Vertex Function Inputs with Attribute Indices

struct VertexInput {
    float2    position [[ attribute(0) ]];
    float4    color    [[ attribute(1) ]];
    float2    uv1      [[ attribute(2) ]];
    float2    uv2      [[ attribute(3) ]];
};

struct VertexOutput {
    float4 pos [[ position ]];
    float4 color;
};

vertex VertexOutput vertexMath(VertexInput in [[ stage_in ]])
{
  VertexOutput out;
  out.pos = float4(in.position.x, in.position.y, 0.0, 1.0);

  float sum1 = in.uv1.x + in.uv2.x;
  float sum2 = in.uv1.y + in.uv2.y;
  out.color = in.color + float4(sum1, sum2, 0.0f, 0.0f);
  return out;
}

要使用 [[stage_in]] 限定符引用着色器函数的输入,需要创建一个 MTLVertexDescriptor 描述对象,然后将其设置为 MTLRenderPipelineState 的 vertexDescriptor 属性。MTLVertexDescriptor 有两个属性:attributes 和 layouts。

MTLVertexDescriptor 的 attributes 属性是一个 MTLVertexAttributeDescriptorArray 对象,它定义了如何在映射到顶点函数参数的缓冲区 buffer 中组织每个顶点属性。attributes 属性可以支持访问在同一缓冲区 buffer 中交错的多个属性(例如顶点坐标,曲面法线和纹理坐标)。着色语言代码中成员变量的顺序不必与框架代码的缓冲区中的顺序一致。MTLVertexAttributeDescriptorArray 数组中每一个顶点属性描述符都有以下属性,它们提供了用于定位和加载参数数据的顶点着色器函数信息:

  • bufferIndex,缓冲参数表的下标,指定访问哪个 MTLBuffer。
  • format,指定如何在框架中解析数据。如果数据类型不是精确类型匹配,可能被转换或扩展。例如,如果着色语言类型为 half4,而框架格式为 MTLVertexFormatFloat2,则当数据被用于顶点着色器的参数时,它可能从 float 转换成 half,并从 2 个分量转换成 4 个分量(其中后两个分量分别为 0.0 和 1.0)
  • offset,指定数据的位置相对于顶点开头的偏移

图 5-4 说明了 Metal 框架代码中的 MTLVertexAttributeDescriptorArray 对象,它实现了一个交错缓冲区,该缓冲区对应于 Listing 5-11 中顶点函数 vertexMath 的输入。

006-图形渲染:渲染命令编码器_第4张图片
image

Listing 5-12 展示了对应于图 5-4 中展示的交错缓冲区的 Metal 框架代码

// Listing 5-12  Metal Framework: Using a Vertex Descriptor to Access Interleaved Data

id  vertexFunc = [library newFunctionWithName:@"vertexMath"];            
MTLRenderPipelineDescriptor* pipelineDesc =      
                             [[MTLRenderPipelineDescriptor alloc] init];
MTLVertexDescriptor* vertexDesc = [[MTLVertexDescriptor alloc] init];

vertexDesc.attributes[0].format = MTLVertexFormatFloat2;
vertexDesc.attributes[0].bufferIndex = 0;
vertexDesc.attributes[0].offset = 0;
vertexDesc.attributes[1].format = MTLVertexFormatFloat4;
vertexDesc.attributes[1].bufferIndex = 0;
vertexDesc.attributes[1].offset = 2 * sizeof(float);  // 8 bytes
vertexDesc.attributes[2].format = MTLVertexFormatFloat2;
vertexDesc.attributes[2].bufferIndex = 0;
vertexDesc.attributes[2].offset = 8 * sizeof(float);  // 32 bytes
vertexDesc.attributes[3].format = MTLVertexFormatFloat2;
vertexDesc.attributes[3].bufferIndex = 0;
vertexDesc.attributes[3].offset = 6 * sizeof(float);  // 24 bytes
vertexDesc.layouts[0].stride = 10 * sizeof(float);    // 40 bytes
vertexDesc.layouts[0].stepFunction = MTLVertexStepFunctionPerVertex;

pipelineDesc.vertexDescriptor = vertexDesc;
pipelineDesc.vertexFunction = vertFunc;

在 MTLVertexDescriptor 对象的 attributes 属性中,每一个 MTLVertexAttributeDescriptor 对象都与着色器函数的 VertexInput 中一个结构体成员变量相对应。attributes[1].bufferIndex = 0 明确了参数表中 0 下标下的缓冲区的用途。(本例中所有 MTLVertexAttributeDescriptor 都有相同的 bufferIndex,意味着每一个顶点属性都引用了参数表中下标为 0 的缓冲区)。offset 值确定了顶点内数据的位置,因此 attributes[1].offset = 2 * sizeof(float) 指定了从缓冲区的起始位置开始,偏移 8 个字节的位置。format 值与片段函数中响应数据类型相匹配,因此 attributes[1].format = MTLVertexFormatFloat4 表明需要使用四个浮点数组合的格式。

MTLVertexDescriptor 对象的 layouts 属性是一个 MTLVertexBufferLayoutDescriptorArray 对象,layouts 中每一个 MTLVertexBufferLayoutDescriptor 对象明确了当 Metal 绘制图元时,如何从参数表中对应的 MTLBuffer 中获取顶点和属性数据。(更多有关绘制图元的信息请查阅绘制几何图元)。MTLVertexBufferLayoutDescriptor 的 stepFunction 属性确定了访问属性数据的方式是每个顶点访问一次,或者一些顶点需要访问数据,或者仅访问一次。如果 stepFunction 被设置为一些顶点需要访问数据,则 MTLVertexBufferLayoutDescriptor 的 stepRate 属性确定了哪些顶点需要访问数据,stride 属性确定了两个顶点间的字节距离。

图 5-5 展示了对应于 listing 5-12 的 MTLVertexBufferLayoutDescriptor 对象,layouts [0] 指定了如何从缓冲区参数表中的相应索引 0 获取顶点数据。stride 确定两个顶点间距离为 40 字节。layouts[0].stepFunction 的值 MTLVertexStepFunctionPerVertex 确定了绘制时每个顶点都需要获取属性数据。如果 stepFunction 的值是 MTLVertexStepFunctionPerInstance,则 stepRate 属性确定了属性数据获取的频度。例如,如 stepRate 为 1,则每个顶点都会访问数据,如果 stepRate 为 2,则每两个顶点访问一次数据。

006-图形渲染:渲染命令编码器_第5张图片
image

执行固定功能渲染命令编码器操作

使用这些 MTLRenderCommandEncoder 方法来设置固定功能图形状态值。

  • setViewport: 确定屏幕坐标中用于投影虚拟 3D 世界的区域。viewport 值是 3D 的,因此包含深度值,具体查阅 使用 viewPort 和像素坐标系统
  • setTriangleFillMode: 确定绘制的三角形图元是线框模式((MTLTriangleFillModeLines)还是填充模式(MTLTriangleFillModeFill)。默认值为 MTLTriangleFillModeFill
  • setCullMode:setFrontFacingWinding: 一般一起使用,用于确定剔除模式。你可以使用剔除模式隐藏一些几何模型的表面,例如用三角形图元填充的定向半球体(如果一个表面的图元始终以顺时针或逆时针顺序绘制,则此表面是可定向的)。
    • setFrontFacingWinding: 用于确定面向前方的图元,其顶点绘制顺序是顺时针(MTLWindingClockwise)还是逆时针(MTLWindingCounterClockwise)。默认值是 MTLWindingClockwise
    • setCullMode: 用于确定是否启动剔除模式(MTLCullModeNone 代表不启动),以及哪种类型的图元需要剔除(MTLCullModeFront 或 MTLCullModeBack)

使用这些 MTLRenderCommandEncoder 的方法来编码固定功能状态更改命令

  • setScissorRect: 方法确定一个 2D 裁剪区域,在这个裁剪区域外的片段将会被舍弃
  • setDepthStencilState: 设置深度和模版测试状态,具体在 深度和模版状态 中介绍
  • setStencilReferenceValue: 确定模版参考值
  • setDepthBias:slopeScale:clamp: 指定用于将阴影贴图与片段着色器输出的深度值进行比较的调整
  • setVisibilityResultMode:offset: 确定是否监控有通过深度和模板测试的采样。如果将 mode 设置为 MTLVisibilityResultModeBoolean,则一旦有采样通过了深度和模版测试,就会将一个非 0 值写入到 MTLRenderPassDescriptor 的 visibilityResultBuffer属性确定的一个缓冲区内,具体在 创建一个渲染流程描述符 中介绍

你可以使用此模式来执行遮挡测试,如果你绘制了一个边界框,并且没有采样可以通过,那么可知该边界框内的任何对象都会被遮挡,因此不需要渲染。

  • setBlendColorRed:green:blue:alpha: 确定混合色值和 alpha 值的常量,具体在 利用描述符创建一个渲染流水线状态 中介绍。

使用 ViewPort 和像素坐标系统

Metal 定义的标准化设备坐标(NDC)是一个 2x2x1 的立方体,其中心点坐标在 (0, 0, 0.5)。NDC 的左边线和下边线的坐标分别是 x=-1 和 y=-1,右边线和上边线的坐标分别是 x=1 和 y=1。

viewPort 确定了从 NDC 到窗口坐标系的转换,具体来说是通过 MTLRenderCommandEncoder 的 setViewport: 方法设置。窗口坐标系的原点在屏幕左上角。

在 Metal 中,像素中心会偏移(0.5, 0.5)。例如,原点像素的中心位于(0.5, 0.5),右边相邻像素的中心是(1.5, 0.5),对于纹理也同样适用。

执行深度和模版操作

你可以按照下列步骤确定一些深度、模版操作等片段操作:

  • 自定义 MTLDepthStencilDescriptor 对象,其中包含深度与模版状态的设置,自定义的 MTLDepthStencilDescriptor 对象需要创建两个 MTLStencilDescriptor 对象,分别适用于前向图元和后向图元
  • 通过调用 MTLDevice 的 newDepthStencilStateWithDescriptor: 方法,利用一个深度/模版状态描述符来创建一个 MTLDepthStencilState 对象
  • 调用 MTLRenderCommandEncoder 的 setDepthStencilState: 方法,利用前面创建的 MTLDepthStencilState 对象,设置深度及模版状态
  • 如果开始使用模版测试,就调用 setStencilReferenceValue: 来确定模版引用值

如果启动了深度检测,则渲染流水线状态必须包含一个用于写入深度值的深度附件。为了执行模版检测,渲染流水线状态必须包含一个模版附件。查看 创建和配置渲染流水线描述符 如何配置附件。

如果你需要定期改变深度/模板状态,那么你可能希望复用状态描述符对象,仅仅修改所需的状态来创建更多状态对象。

注意:要从着色器函数中的深度格式纹理进行采样,请在着色器中实施采样操作,不要使用 MTLSamplerState

使用下列 MTLDepthStencilDescriptor 的属性来设置深度和模版状态:

  • 设置 depthWriteEnabled 为 YES 来启动向深度附件写入深度值的功能
  • depthCompareFunction 确定深度测试如何执行,如果一个片段的深度值没有通过深度检测,则这个片段就会被舍弃。例如,常用的 MTLCompareFunctionLess 会使深度比前一个写入的像素深度远的片段值无法通过深度测试,即认为这个片段被前一个片段遮挡了
  • frontFaceStencil 和 backFaceStencil 属性分别是前向图元和后向图元的 MTLStencilDescriptor 对象。你可以将同一个 MTLStencilDescriptor 设置到 frontFaceStencil 和 backFaceStencil 属性上,从而在前向图元和后向图元上使用同一个模版状态。通过设置属性为默认值 nil 可以禁止对应图元的模版检测

Metal 会根据是否为有效的模板操作配置了模板描述符而决定是否启动模版检测,因此不必要显式地禁用模版状态。

Listing 5-13 展示了如何利用一个 MTLDepthStencilDescriptor 对象来创建一个 MTLDepthStencilState 对象,然后将这个 MTLDepthStencilState 对象用到一个渲染命令编码器上。示例中通过深度/模板状态描述符的 frontFaceStencil 属性设置了前向图元的模板状态,禁用了后向图元的模版状态

Listing 5-13  Creating and Using a Depth/Stencil Descriptor

MTLDepthStencilDescriptor *dsDesc = [[MTLDepthStencilDescriptor alloc] init];
if (dsDesc == nil)
     exit(1);   //  if the descriptor could not be allocated
dsDesc.depthCompareFunction = MTLCompareFunctionLess;
dsDesc.depthWriteEnabled = YES;
 
dsDesc.frontFaceStencil.stencilCompareFunction = MTLCompareFunctionEqual;
dsDesc.frontFaceStencil.stencilFailureOperation = MTLStencilOperationKeep;
dsDesc.frontFaceStencil.depthFailureOperation = MTLStencilOperationIncrementClamp;
dsDesc.frontFaceStencil.depthStencilPassOperation =
                          MTLStencilOperationIncrementClamp;
dsDesc.frontFaceStencil.readMask = 0x1;
dsDesc.frontFaceStencil.writeMask = 0x1;
dsDesc.backFaceStencil = nil;
id  dsState = [device
                          newDepthStencilStateWithDescriptor:dsDesc];
 
[renderEnc setDepthStencilState:dsState];
[renderEnc setStencilReferenceValue:0xFF];

MTLStencilDescriptor 的下列属性定义了模版测试

  • readMask 是一个位掩码,GPU使用模板参考值和存储的模板值计算此掩码的按位与,模板测试是在得到的掩码参考值和掩码存储值之间的比较
  • writeMask是一个位掩码,用于限制模板操作将哪些模板值写入模板附件
  • stencilCompareFunction 用于确定模版测试如何在片段上实施

绘制几何图元

确定了流水线状态和固定功能状态后,你可以调用下面的 MTLRenderCommandEncoder 方法来绘制几何图元。这些绘制方法会利用资源(例如包含了顶点坐标、纹理坐标、表面法线和其他数据的缓冲)来执行流水线,并配合片段着色器函数和其他你之前在 MTLRenderCommandEncoder 中设置的状态。

  • drawPrimitives:vertexStart:vertexCount:instanceCount: 使用连续的顶点数组元素渲染一定数量(instanceCount)的图元实例,起始顶点的下标在数组的 vertexStart处,结束顶点在数组的 vertexStart + vertexCount - 1 处
  • drawPrimitives:vertexStart:vertexCount: 与上一个方法类似,但是 instanceCount 为 1
  • drawIndexedPrimitives:indexCount:indexType:indexBuffer:indexBufferOffset:instanceCount: 会渲染一定数目(instanceCount)的图元实例,顶点列表来自于 MTLBuffer 类型的对象 indexBuffer 中。indexCount 确定了顶点的数目。indexBufferOffset 标明从 indexBuffer 的起始位置偏移多少字节作为顶点数据起始位置。indexBufferOffset 必须为单个顶点元素尺寸的整数倍,单个顶点元素的尺寸由 indexType 决定
  • drawIndexedPrimitives:indexCount:indexType:indexBuffer:indexBufferOffset: 与上一个方法类型,但是 instanceCount 为 1

对于上述每一个图元绘制方法来说,第一个 MTLPrimitiveType 参数决定了图元的类型。其他输入参数决定了哪些顶点被用来绘制这些图元。对于所有方法而言,instanceStart 参数标明了绘制的起始实例,而 instanceCount 标明了绘制实例的个数。

正如前面所讨论的,setTriangleFillMode: 方法决定了三角图元按照填充模式或线框模式渲染,setCullMode:setFrontFacingWinding: 决定了 GPU 是否在渲染时舍弃一些三角图元。更多信息请查阅 固定功能状态操作

绘制一个点图元时,着色器语言代码必须将顶点函数附上 [[point_size]] 修饰符,否则结果不可预测。

查阅 Metal 着色器语言指南 了解更多有关 Metal 着色器语言属性与修饰符的内容。

结束一次渲染流程

调用渲染命令编码器的 endCoding 可以结束一次渲染流程。在结束了之前的命令编码器之后,你可以创建一个新的任何类型的命令编码器,将更多命令编码到命令缓冲中。

代码示例:绘制一个三角形

下列步骤解释了 listing 5-14 所展示的渲染一个三角形的基本步骤

  • 创建一个 MTLCommandQueue 并使用它来创建一个 MTBCommandBuffer
  • 创建一个 MTLRenderPassDescriptor,指定一组附件作为命令缓冲区中编码渲染命令的目标

示例仅使用了第一个颜色附件(此处假设变量 currentTexture 包含颜色附件所用到的 MTLTexture)。然后用这个 MTLRenderPassDescriptor 创建了一个 MTLRenderCommandEncoder 对象

  • 创建两个 MTLBuffer 对象,posBuf 和 ColBuf,调用newBufferWithBytes:length:options: 方法拷贝顶点坐标 posData 和 顶点颜色数据 colData 到对应的 buffer 中
  • 调用两次 MTLRenderCommandEncoder 的 setVertexBuffer:offset:atIndex: 方法指定坐标和色值

setVertexBuffer:offset:atIndex: 的 atIndex 参数对应于顶点函数的 buffer 属性

  • 创建一个 MTLRenderPipelineDescriptor 对象,设置顶点函数和片段函数:
    • 使用 progSrc 中的源码创建一个 MTLLibrary,假设 progSrc 包含 Metal 着色器代码的源码字符串
    • 然后调用 MTLLibrary 的 newFunctionWithName: 方法创建一个 MTLFunction vertFun 对象以表示 hello_vertex 函数,创建一个 MTLFunction fragFunc 对象以表示 hello_fragment 函数
    • 最后,将这些 MTLFunction 对象设置为 MTLRenderPipelineDescriptor 的 vertexFunction 和 fragmentFunction 属性
  • 调用 MTLDevice 的 newRenderPipelineStateWithDescriptor:error: 方法,从 MTLRenderPipelineDescriptor 创建一个 MTLRenderPipeLineState 对象,然后调用 MTLRenderCommandEncoder 的 setRenderPipelineState:,使用创建的流水线状态来渲染
  • 调用 MTLRenderCommandEncoder 的 drawPrimitives:vertexStart:vertexCount: 方法,渲染一个填充模式的三角形(类型为 MTLPrimitiveTypeTriangle)
  • 调用 endEncoding 方法结束此次渲染流程的编码过程,然后调用 MTLCommandBuffer 的 commit 方法在设备上执行命令
// Listing 5-14  Metal Code for Drawing a Triangle

id  device = MTLCreateSystemDefaultDevice();
 
id  commandQueue = [device newCommandQueue];
id  commandBuffer = [commandQueue commandBuffer];
 
MTLRenderPassDescriptor *renderPassDesc
                               = [MTLRenderPassDescriptor renderPassDescriptor];
renderPassDesc.colorAttachments[0].texture = currentTexture;
renderPassDesc.colorAttachments[0].loadAction = MTLLoadActionClear;
renderPassDesc.colorAttachments[0].clearColor = MTLClearColorMake(0.0,1.0,1.0,1.0);
id  renderEncoder =
           [commandBuffer renderCommandEncoderWithDescriptor:renderPassDesc];
 
static const float posData[] = {
        0.0f, 0.33f, 0.0f, 1.f,
        -0.33f, -0.33f, 0.0f, 1.f,
        0.33f, -0.33f, 0.0f, 1.f,
};
static const float colData[] = {
        1.f, 0.f, 0.f, 1.f,
        0.f, 1.f, 0.f, 1.f,
        0.f, 0.f, 1.f, 1.f,
};
id  posBuf = [device newBufferWithBytes:posData
        length:sizeof(posData) options:nil];
id  colBuf = [device newBufferWithBytes:colorData
        length:sizeof(colData) options:nil];
[renderEncoder setVertexBuffer:posBuf offset:0 atIndex:0];
[renderEncoder setVertexBuffer:colBuf offset:0 atIndex:1];
 
NSError *errors;
id  library = [device newLibraryWithSource:progSrc options:nil
                           error:&errors];
id  vertFunc = [library newFunctionWithName:@"hello_vertex"];
id  fragFunc = [library newFunctionWithName:@"hello_fragment"];
MTLRenderPipelineDescriptor *renderPipelineDesc
                                   = [[MTLRenderPipelineDescriptor alloc] init];
renderPipelineDesc.vertexFunction = vertFunc;
renderPipelineDesc.fragmentFunction = fragFunc;
renderPipelineDesc.colorAttachments[0].pixelFormat = currentTexture.pixelFormat;
id  pipeline = [device
             newRenderPipelineStateWithDescriptor:renderPipelineDesc error:&errors];
[renderEncoder setRenderPipelineState:pipeline];
[renderEncoder drawPrimitives:MTLPrimitiveTypeTriangle
               vertexStart:0 vertexCount:3];
[renderEncoder endEncoding];
[commandBuffer commit];

在 Listing 5-14 中,一个 MTLFunction 对象代表了 hello_vertex 着色器函数,MTLRenderCommandEncoder 的 setVertexBuffer:offset:atIndex: 方法用于指定顶点资源(示例中的两个缓冲对象),顶点资源作为参数传递给 hello_vertex 函数,atIndex 参数对应于顶点着色器函数的 buffer(atIndex) 修饰符。

Listing 5-15  Corresponding Shader Function Declaration

vertex VertexOutput hello_vertex(
                    const global float4 *pos_data [[ buffer(0) ]],
                    const global float4 *color_data [[ buffer(1) ]])
{
    ...
}

多线程编码单一渲染流程

某些情况下,一次渲染流程可能因为单 CPU 工作负载过多而影响 app 的性能。然而如果想把工作任务分解成多个 CPU 线程编码的渲染流程以规避性能瓶颈,却可能仍然会损耗性能。因为每一个渲染流程都需要它自己的中间附件执行存储和加载动作来保存渲染目标内容。

MTLParallelRenderCommandEncoder 对象则可以管理多个子 MTLRenderCommandEncoder 对象,这些 MTLRenderCommandEncoder 对象共享同一份命令缓冲和渲染流程描述符。并行渲染命令编码器确保附件的存储和加载操作只会出现在整个渲染流程的开始和结尾,不会在每个子渲染命令编码器的命令集合的开始和结尾处频繁执行。因此,你可以以安全高效的方式,并行地将每一个 MTLRenderCommandEncoder 对象声明到单独一个线程中去。

调用 MTLCommandBuffer 对象的 parallelRenderCommandEncoderWithDescriptor: 方法创建一个并行渲染命令编码器。在希望执行命令编码的线程上,调用 MTLParallelRenderCommandEncoder 对象的 renderCommandEncoder 方法创建一个子渲染命令编码器。从同一个并行渲染命令编码器创建的所有子渲染命令编码器会将命令编码到同一个命令缓冲中。编码命令的顺序与编码器的创建顺序一致。一个指定的渲染命令编码器可以调用 endEncoding 方法结束编码。所有子渲染命令编码器结束编码后,可以调用 MTLParallelRenderCommandEncoder 的 endEncoding 方法结束渲染流程。

Listing 5-16 展示了 MTLParallelRenderCommandEncoder 创建三个 MTLRenderCommandEncoder 对象 rCE1、rCE2、和 rCE3 的过程:

// Listing 5-16  A Parallel Rendering Encoder with Three Render Command Encoders

MTLRenderPassDescriptor *renderPassDesc 
                     = [MTLRenderPassDescriptor renderPassDescriptor];
renderPassDesc.colorAttachments[0].texture = currentTexture;
renderPassDesc.colorAttachments[0].loadAction = MTLLoadActionClear;
renderPassDesc.colorAttachments[0].clearColor = MTLClearColorMake(0.0,0.0,0.0,1.0);

id  parallelRCE = [commandBuffer 
                     parallelRenderCommandEncoderWithDescriptor:renderPassDesc];
id  rCE1 = [parallelRCE renderCommandEncoder];
id  rCE2 = [parallelRCE renderCommandEncoder];
id  rCE3 = [parallelRCE renderCommandEncoder];

//  not shown: rCE1, rCE2, and rCE3 call methods to encode graphics commands
//
//  rCE1 commands are processed first, because it was created first
//  even though rCE2 and rCE3 end earlier than rCE1
[rCE2 endEncoding];
[rCE3 endEncoding];
[rCE1 endEncoding];

//  all MTLRenderCommandEncoders must end before MTLParallelRenderCommandEncoder
[parallelRCE endEncoding];

命令编码器调用 endEncoding 的顺序并不会决定命令被编码和添加到 MTLCommandBuffer 的顺序。 MTLParallelRenderCommandEncoder 始终按照子命令编码器创建的顺序添加命令到 MTLCommandBuffer,具体如下图所示

006-图形渲染:渲染命令编码器_第6张图片
image

你可能感兴趣的:(006-图形渲染:渲染命令编码器)