前言
文章翻译是的apple的Metal开发文档demo:Using a Render Pipeline to Render Primitives
),项目下载链接
)
概览
在《使用Metal填充一个view》中,您学习了如何设置MTKView对象,以及如何使用渲染过程更改视图的内容,该示例只是将视图的内容擦除为背景色,这个例子将为你演示怎样为渲染通道配置一个渲染管线来绘制一个彩色2D的三角形到view上,这个例子将为每个顶点配置一个位置和颜色,渲染管道使用该数据渲染三角形,并且根据三角形的顶点颜色采用插值的方式来绘制。
注意
这个xcode的项目包含了运行在macOS,ios和tvOS上的工程,Matal不支持在ios和tvOS的模拟器上运行,所以ios和tvOS的工程需要真机测试,默认选择的工程是macOS平台的工程。
理解Metal的渲染管道
渲染管道执行一系列渲染指令和将数据传递到渲染过程的目标位置,渲染管道有许多阶段,一些使用着色器编程,另一些使用固定或可配置的行为,这个例子主要关注三个主要的阶段:顶点阶段,光栅化阶段和片元阶段,顶点阶段和片元阶段是可以编程的,所以你可以使用 Metal Shading Language (MSL)语言来为他们写一些函数,光栅化阶段只要固定的行为。
Ps:
从这个描叙看所谓的渲染管道类似与opengGL ES中的program,而Metal Shading Language (MSL)类似与Opengl shader language,这样有了对比,可能更有助于我们理解Matel中的这些概念。
图1 Matal图像渲染管道的主要阶段
渲染从一个包含了顶点数量和渲染的原始的图像类型的指令开始,举例来说,下面这个就是例子中的一个渲染指令。
// Draw the triangle. 画一个三角形
[renderEncoder drawPrimitives:MTLPrimitiveTypeTriangle
vertexStart:0
vertexCount:3];
顶点阶段为每个顶点提供数据,当处理了足够多的顶点时,渲染管道光栅化基本体,确定渲染目标中哪些像素位于基本体的边界内。片段阶段确定要写入这些像素的渲染目标的颜色值。
在本示例的其余部分中,您将看到如何编写顶点和片元函数,如何创建渲染管道状态对象,以及如何对使用此管道的draw命令进行编码。
你的自定义渲染管道如何处理数据
顶点函数为单个顶点生成数据,片元函数为单个片元生成数据,但是你决定它们怎么生成数据,您在配置管道的各个阶段时要考虑目标,这意味着您知道希望管道生成什么以及它如何生成这些结果。
决定将哪些数据传递到渲染管道中,以及将哪些数据传递到管道的后期阶段。通常有三个地方:
1.管道的输入,由应用程序提供并传递到vertex阶段
2.顶点阶段的输出,传递到光栅化阶段
3.片段阶段的输入,由应用程序提供或由光栅化阶段生成。
在这个例子中,输入的管道的数据是顶点的位置和颜色,为了演示通常在顶点函数中执行的转换类型,输入坐标在自定义坐标空间中定义,以距离视图中心的像素为单位进行测量。这些坐标需要转换成Metal的坐标系
声明一个AAPLVertex的结构体,使用SIMD向量类型保存位置和颜色数据
要共享一个在内存中定义的结构体,请在公共头文件中声明该结构体,并将其导入到Metal和应用程序中。
typedef struct
{
vector_float2 position;
vector_float4 color;
} AAPLVertex; // 定义的公共结构体
SIMD的类型在Metal Shading language中很常见,你在你的程序中也应该使用 simd library,SIMD类型包含特定数据类型的多个通道,所以把位置声明为一个包含两个32位float值的vector_float2类型(保存x和y的坐标值)颜色值保存在一个vector_float4类型中,它有红,绿,蓝和透明度4个通道。
在程序中,使用常量数组指定输入数据:
static const AAPLVertex triangleVertices[] =
{
// 2D positions, RGBA colors
{ { 250, -250 }, { 1, 0, 0, 1 } },
{ { -250, -250 }, { 0, 1, 0, 1 } },
{ { 0, 250 }, { 0, 0, 1, 1 } },
};
顶点阶段为顶点生成数据,因此它需要提供颜色和变换的位置。声明包含位置和颜色值的RasterizerData结构体,使用SIMD的类型
typedef struct
{
//此成员的 [[positing]] 属性表示此值是此结构从顶点函数返回的顶点信息。
float4 position [[position]];
//由于此成员没有特殊属性,因此光栅化器使用其他三角形顶点的值插值其值,然后将插值值传递给每个三角形中的片元
float4 color;
} RasterizerData;
输出位置(在下面详细描述)必须定义为vector_float4,颜色声明为输入数据结构中的color属性
您需要告诉Metal光栅化数据中的哪个字段提供了位置数据,因为Metal没有对结构中的字段强制任何特定的命名约定,请使用[[position]]属性限定符注释位置字段,以声明此字段保留输出位置。
fragment函数只是将光栅化阶段的数据传递给后期阶段,因此不需要任何额外的参数。
定义一个顶点函数
声明顶点函数,包括它的输入参数和它输出的数据。与使用kernel关键字声明计算函数类似,您也可以使用vertex关键字声明顶点函数
vertex RasterizerData
vertexShader(uint vertexID [[vertex_id]],
constant AAPLVertex *vertices [[buffer(AAPLVertexInputIndexVertices)]],
constant vector_uint2 *viewportSizePointer [[buffer(AAPLVertexInputIndexViewportSize)]])
第一个参数vertexID使用了[[vertex_id]]属性限定符,这是另一个Metal关键字。执行render命令时,GPU多次调用顶点函数,为每个顶点生成一个唯一的值。
第二个参数vertices是一个包含顶点数据的数组,使用前面定义的AAPLVertex结构。
若要将位置转换为Metal的坐标,该函数需要绘制三角形的视区的大小(以像素为单位),因此它存储在viewportSizePointer参数中
第二个和第三个参数具有[[buffer(n)]]属性限定符。默认情况下,Metal会自动为每个参数在参数表中分配插槽。将[[buffer(n)]]限定符添加到缓冲区参数时,将明确告诉Metal要使用哪个插槽。显式声明槽可以更容易地修改着色器,而无需更改应用程序代码。声明共享头文件中两个指示符的常量。
函数返回一个RasterizerData的结构体
实现顶点函数
顶点函数必须生成输出结构的两个字段。使用vertexID参数索引到顶点数组并读取顶点的输入数据。另外,检索视口尺寸。
float2 pixelSpacePosition = vertices[vertexID].position.xy;
// Get the viewport size and cast to float.
vector_float2 viewportSize = vector_float2(*viewportSizePointer);
顶点函数必须在剪辑空间坐标中提供位置数据,剪辑空间坐标是使用四维均匀向量(x,y,z,w)指定的三维点。光栅化阶段获取输出位置,并将x、y和z坐标除以w,以生成标准化设备坐标中的3D点。标准化设备坐标与视区大小无关
图2 归一化设备坐标系
标准化设备坐标使用左手坐标系并映射到视口中的位置。基本体被剪裁到这个坐标系中的一个框中,然后光栅化。剪辑框的左下角位于(-1.0,-1.0)的(x,y)坐标,右上角位于(1.0,1.0)。正z值指向远离相机(进入屏幕)的方向。z坐标的可见部分介于0.0(近剪裁平面)和1.0(远剪裁平面)之间。
将输入坐标系转换为标准化设备坐标系
因为这是一个二维应用程序,不需要同质坐标,所以首先向输出坐标写入一个默认值,w值设置为1.0,其他坐标设置为0.0。这意味着坐标已经在标准化设备坐标空间中,顶点函数应该在该坐标空间中生成(x,y)坐标。将输入位置除以视窗大小的一半以生成标准化设备坐标。由于此计算是使用SIMD类型执行的,因此可以使用一行代码同时分割两个通道。执行除法并将结果放入输出位置的x和y通道。
out.position = vector_float4(0.0, 0.0, 0.0, 1.0);
out.position.xy = pixelSpacePosition / (viewportSize / 2.0);
最后,将颜色值复制到out.color返回值中
实现一个片元函数
片元是对渲染目标的可能更改。光栅化器确定基本体覆盖渲染目标的哪些像素。仅渲染像素中心位于三角形内部的片元。
图3光栅化阶段产生的碎片
片段函数处理来自光栅化器的单个位置的传入信息,并计算每个渲染目标的输出值。这些片段值由管道中的后期处理,最终写入呈现目标
注意
片段之所以称为可能的更改,是因为可以将片段阶段之后的管道阶段配置为拒绝某些片段或更改写入呈现目标的内容。在此示例中,fragment stage计算的所有值都按原样写入到呈现目标。
此示例中的片段着色器接收顶点着色器输出中声明的相同参数。使用fragment关键字声明fragment函数。它只需要一个参数,即vertex stage提供的光栅化数据结构。添加[[stage_in]]属性限定符以指示此参数是由光栅化器生成的。
fragment float4 fragmentShader(RasterizerData in [[stage_in]])
如果片段函数写入多个渲染目标,则它必须为每个渲染目标声明一个包含字段的结构。由于此示例只有一个渲染目标,因此可以直接指定浮点向量作为函数的输出。此输出是要写入渲染目标的颜色。
光栅化阶段计算每个片段参数的值,并用它们调用片段函数。光栅化阶段将其颜色参数计算为三角形顶点处颜色的混合。片段离顶点越近,顶点对最终颜色的贡献就越大。
图4插值片段颜色
函数返回插值后的颜色值
return in.color;
创建渲染管道状态对象
现在这些函数已经完成,可以创建使用它们的渲染管道。首先,获取默认库并为每个函数获取MTLFunction对象。
id defaultLibrary = [_device newDefaultLibrary];
id vertexFunction = [defaultLibrary newFunctionWithName:@"vertexShader"];
id fragmentFunction = [defaultLibrary newFunctionWithName:@"fragmentShader"];
接下来,创建MTLRenderPipelineState对象。渲染管道有更多要配置的阶段,因此可以使用MTLRenderPipelineDescriptor来配置管道。
MTLRenderPipelineDescriptor *pipelineStateDescriptor = [[MTLRenderPipelineDescriptor alloc] init];
pipelineStateDescriptor.label = @"Simple Pipeline";
pipelineStateDescriptor.vertexFunction = vertexFunction;
pipelineStateDescriptor.fragmentFunction = fragmentFunction;
pipelineStateDescriptor.colorAttachments[0].pixelFormat = mtkView.colorPixelFormat;
_pipelineState = [_device newRenderPipelineStateWithDescriptor:pipelineStateDescriptor
error:&error];
除了指定顶点和片段函数外,还需要声明管道将绘制到的所有渲染目标的像素格式。像素格式(MTLPixelFormat)定义像素数据的内存布局。对于简单格式,此定义包括每个像素的字节数、存储在像素中的数据通道数以及这些通道的位布局。由于此示例只有一个呈现目标,并且由视图提供,请将视图的像素格式复制到呈现管道描述符中。渲染管道状态必须使用与渲染过程指定的像素格式兼容的像素格式。在此示例中,渲染过程和管道状态对象都使用视图的像素格式,因此它们始终相同
当Metal创建渲染管道状态对象时,管道被配置为将片段函数的输出转换为渲染目标的像素格式。如果要以不同的像素格式为目标,则需要创建不同的管道状态对象。可以在多个针对不同像素格式的管道中重用相同的着色器
设置视口
现在已经有了管道的渲染管道状态对象,您将渲染三角形。使用渲染命令编码器执行此操作。首先,设置视口,以便Metal知道要绘制到渲染目标的哪个部分。
// Set the region of the drawable to draw into.
[renderEncoder setViewport:(MTLViewport){0.0, 0.0, _viewportSize.x, _viewportSize.y, 0.0, 1.0 }];
Set the Render Pipeline State
为要使用的管道设置渲染管道状态。
[renderEncoder setRenderPipelineState:_pipelineState];
将参数数据发送到顶点函数
通常,使用缓冲区(MTLBuffer)将数据传递给着色器。但是,当您只需要向vertex函数传递少量数据时(如这里的情况),请将数据直接复制到命令缓冲区中。
示例将两个参数的数据复制到命令缓冲区中。顶点数据从示例中定义的数组中复制。从用于设置视口的同一变量复制视口数据。
在此示例中,fragment函数仅使用从光栅化器接收的数据,因此没有要设置的参数。
[renderEncoder setVertexBytes:triangleVertices
length:sizeof(triangleVertices)
atIndex:AAPLVertexInputIndexVertices];
[renderEncoder setVertexBytes:&_viewportSize
length:sizeof(_viewportSize)
atIndex:AAPLVertexInputIndexViewportSize];
对绘图命令进行编码
指定基本体的类型、起始索引和顶点数。当三角形被渲染时,顶点函数被调用,顶点参数的值为0、1和2。
// Draw the triangle.
[renderEncoder drawPrimitives:MTLPrimitiveTypeTriangle
vertexStart:0
vertexCount:3];
与使用Metal绘制屏幕一样,结束编码过程并提交命令缓冲区。但是,可以使用相同的步骤集对更多渲染命令进行编码。最终的图像将呈现为命令是按照指定的顺序处理的。(为了提高性能,GPU可以并行处理命令,甚至部分命令,只要最终结果看起来是按顺序呈现的)
颜色插值实验
在这个示例中,颜色值是通过三角形插值的。这通常是您想要的,但有时您希望一个值由一个顶点生成,并在整个基本体中保持不变。为此,请在顶点函数的输出上指定平面属性限定符。现在试试这个。在示例项目中查找光栅化数据的定义,并将[[flat]]限定符添加到其颜色字段中。
float4 color [[flat]];
再次运行样本。渲染管道使用来自第一个顶点(称为激发顶点)的颜色值均匀地穿过三角形,并且它忽略来自其他两个顶点的颜色。只需在顶点函数的输出中添加或省略平面限定符,就可以混合使用平面着色和插值。Metal Shading Language specification规范定义了其他属性限定符,您也可以使用这些限定符来修改光栅化行为。