渲染一个简单的二维三角形。
本文内容来自苹果
本文主要讲述使用Metal绘制到屏幕时,将视图的内容删除为背景颜色。显示如何配置渲染管道并将其用作渲染过程的一部分,以简单的2D彩色三角形绘制到视图中。为每个顶点提供位置和颜色,渲染管道使用该数据渲染三角形,在为三角形顶点指定的颜色之间插入颜色值。
注意
iOS或tvOS模拟器不支持Metal,因此iOS和tvOS方案需要物理设备来运行示例
了解Metal Render Pipeline
一个渲染管线流程绘图命令和数据写入到一个渲染通道的目标。渲染管道有许多阶段,一些使用着色器编程,另一些使用固定或可配置的行为编程。此示例主要关注管道的三个主要阶段:顶点阶段,光栅化阶段和片段阶段。顶点阶段和片段阶段是可编程的,因此您可以使用金属着色语言(MSL)为它们编写函数。光栅化阶段具有固定的行为。
图1 Metal图形渲染管道的主要阶段
渲染从绘图命令开始,该命令包括顶点计数和要渲染的基元类型。例如,以下是此示例中的绘图命令:
// Draw the triangle.
[renderEncoder drawPrimitives:MTLPrimitiveTypeTriangle
vertexStart:0
vertexCount:3];
顶点阶段为每个顶点提供数据。当处理了足够的顶点时,渲染管道栅格化基元,确定渲染目标中的哪些像素位于基元的边界内。片段阶段确定要写入这些像素的渲染目标的值。
在本示例的其余部分中,您将看到如何编写顶点和片段函数,如何创建渲染管道状态对象,以及最后如何编码使用此管道的绘制命令。
自定义渲染管道如何处理数据
顶点函数为单个顶点生成数据
,片段函数生成单个片段的数据
,但您可以决定它们的工作方式。您可以考虑目标来配置管道的各个阶段,这意味着您知道管道要生成什么以及如何生成这些结果。
确定要传递到渲染管道的数据以及将哪些数据传递到管道的后续阶段。通常有三个地方可以执行此操作:
- 管道的输入,由您的应用程序提供并传递到顶点阶段。
- 顶点阶段的输出,传递给光栅化阶段。
- 片段阶段的输入,由您的应用提供或由光栅化阶段生成。
管道的输入数据是顶点的位置及其颜色。通常在顶点函数中执行的变换类型,输入坐标在自定义坐标空间中定义,以视图中心的像素为单位进行测量。这些坐标需要转换为Metal的坐标系。
声明一个PLVertex结构,使用SIMD矢量类型来保存位置和颜色数据。要共享结构在内存中的布局方式的单个定义,请在通用标头中声明结构,并将其导入Metal shader和app中。
typedef struct
{
vector_float2 position;
vector_float4 color;
} AAPLVertex;
SIMD类型在Metal Shading Language中很常见,您也应该使用simd库在应用程序中使用它们。SIMD类型包含特定数据类型的多个通道
,因此将位置声明为包含两个32位浮点值(它将保存x和y坐标)。颜色使用a存储,因此它们有四个通道 - 红色,绿色,蓝色和alpha。vector_float2vector_float4
在应用程序中,使用常量数组指定输入数据:
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 } },
};
顶点阶段为顶点生成数据,因此需要提供颜色和变换位置。使用SIMD类型声明包含位置和颜色值的结构。RasterizerData
typedef struct
{
//此成员的[[位置]]属性表示此值
//是此结构为
//从Vertex函数返回。
float4 position [[position]];
//由于此成员没有特殊属性,因此光栅化器
//用其他三角形顶点的值插入其值
//然后将插值值传递给每个
//三角形中的片段。
float4 color;
} RasterizerData;
输出位置(下面详细描述)必须定义为a 。颜色声明为输入数据结构中的颜色。vector_float4
您需要告诉Metal光栅化数据中的哪个字段提供位置数据,因为Metal不对结构中的字段强制执行任何特定的命名约定。position使用[[position]]属性限定符注释该字段以声明此字段包含输出位置。
片段函数只是将光栅化阶段的数据传递给后期阶段,因此它不需要任何其他参数。
声明顶点函数
vertex RasterizerData
vertexShader(uint vertexID [[vertex_id]],
constant AAPLVertex *vertices [[buffer(AAPLVertexInputIndexVertices)]],
constant vector_uint2 *viewportSizePointer [[buffer(AAPLVertexInputIndexViewportSize)]])
第一个参数是使用属性限定符,它是另一个Metal关键字。执行渲染命令时,GPU会多次调用顶点函数,为每个顶点生成唯一值。vertexID[[vertex_id]]
第二个参数vertices是一个包含顶点数据的数组,使用AAPLVertex先前定义的结构。
要将位置转换为Metal的坐标,该函数需要绘制三角形的视口大小(以像素为单位),因此将其存储在参数中。viewportSizePointer
第二个和第三个参数具有[[buffer(n)]]属性限定符。默认情况下,Metal会自动为参数表分配每个参数的插槽。将[[buffer(n)]]限定符添加到缓冲区参数时,可以明确告知Metal使用哪个插槽。明确声明插槽可以更轻松地修改着色器,而无需更改应用程序代码。在共享头文件中声明两个指标的常量。
函数的输出是一个结构。RasterizerData
写入顶点函数
您的顶点函数必须生成输出结构的两个字段。使用参数索引到数组并读取顶点的输入数据。另外,检索视口尺寸。vertexIDvertices
float2 pixelSpacePosition = vertices[vertexID].position.xy;
// 获取视区大小并将其转换为float
vector_float2 viewportSize = vector_float2(*viewportSizePointer);
顶点函数必须在剪辑空间坐标中提供位置数据,这是使用四维同质矢量(x,y,z,w)指定的3D点。光栅化阶段获取输出位置并将,,和坐标分开x,以在标准化设备坐标中生成3D点。标准化设备坐标与视口大小无关。yzw
图2标准化设备坐标系
标准化设备坐标使用左手坐标系并映射到视口中的位置。基元被剪切到此坐标系中的框,然后进行栅格化。剪切框的左下角位于(x,y)坐标处,右上角位于。正-Z值指向远离相机(进入屏幕)。坐标的可见部分位于(近剪裁平面)和(远剪裁平面)之间。
将输入坐标系转换为标准化设备坐标系。
因为这是一个2D应用程序而且不需要同质坐标,所以首先将默认值写入输出坐标,其w值设置为,其他坐标设置为。这意味着坐标已经在标准化设备坐标空间中,并且顶点函数应该在该坐标空间中生成(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
out.color = vertices[vertexID].color;
写一个片段函数
一个片段是一个可能改变的渲染目标。光栅化器确定渲染目标的哪些像素被基元覆盖。仅渲染像素中心在三角形内部的片段。
图3光栅化阶段生成的碎片
片段函数处理来自光栅化器的输入信息,用于单个位置,并计算每个渲染目标的输出值。这些片段值由管道中的后续阶段处理,最终写入渲染目标。
注意
片段被称为可能的更改的原因是因为片段阶段之后的管道阶段可以配置为拒绝某些片段或更改写入渲染目标的内容。在此示例中,片段阶段计算的所有值都按原样写入渲染目标。
此示例中的片段着色器接收与顶点着色器输出中声明的相同参数。使用fragment关键字声明片段函数。它需要一个参数,与顶点阶段提供的结构相同。添加属性限定符以指示此参数是由光栅化器生成的。RasterizerData[[stage_in]]
fragment float4 fragmentShader(RasterizerData in [[stage_in]])
光栅化阶段计算每个片段参数的值,并使用它们调用片段函数。光栅化阶段将其颜色参数计算为三角形顶点处颜色的混合。片段离顶点越近,顶点对最终颜色的贡献越大。
图4插值片段颜色
将插值颜色作为函数的输出返回。
return in.color;
创建渲染管道状态对象
现在函数已完成,您可以创建使用它们的渲染管道。首先,获取默认库并获取MTLFunction
每个函数的对象。
id<MTLLibrary> defaultLibrary = [_device newDefaultLibrary];
id<MTLFunction> vertexFunction = [defaultLibrary newFunctionWithName:@"vertexShader"];
id<MTLFunction> fragmentFunction = [defaultLibrary newFunctionWithName:@"fragmentShader"];
接下来,创建一个对象。渲染管道有更多要配置的阶段,因此您使用a 来配置管道。
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 }];
设置渲染管道状态
设置要使用的管道的渲染管道状态。
[renderEncoder setRenderPipelineState:_pipelineState];
将参数数据发送到顶点函数
通常,您使用buffers(MTLBuffer
)将数据传递给着色器。但是,当您需要将少量数据传递给顶点函数时(如此处所示),将数据直接复制到命令缓冲区中。
该示例将两个参数的数据复制到命令缓冲区中。顶点数据从样本中定义的数组中复制。视口数据是从用于设置视口的相同变量中复制的。
在此示例中,片段函数仅使用从光栅化器接收的数据,因此没有要设置的参数。
[renderEncoder setVertexBytes:triangleVertices
length:sizeof(triangleVertices)
atIndex:AAPLVertexInputIndexVertices];
[renderEncoder setVertexBytes:&_viewportSize
length:sizeof(_viewportSize)
atIndex:AAPLVertexInputIndexViewportSize];
编码绘图命令
指定基元的类型,起始索引和顶点数。渲染三角形时,将为参数调用顶点函数,值为0,1和2 。vertexID
// Draw the triangle.
[renderEncoder drawPrimitives:MTLPrimitiveTypeTriangle
vertexStart:0
vertexCount:3];
与使用Metal绘制屏幕一样,您可以结束编码过程并提交命令缓冲区。但是,您可以使用同一组步骤编码更多渲染命令。渲染最终图像,就好像命令按指定顺序处理一样。(为了提高性能,允许GPU并行处理命令甚至部分命令,只要最终结果看起来按顺序呈现。)
使用颜色插值进行实验
在此示例中,颜色值在三角形中进行插值。这通常是你想要的,但有时候你想要一个顶点生成一个值,并在整个基元上保持不变。flat
在顶点函数的输出上指定属性限定符以执行此操作。现在试试吧。在示例项目中找到定义并将限定符添加到其字段中。RasterizerData[[flat]]color``float4 color [[flat]];
再次运行该示例。渲染管道在三角形上均匀地使用第一个顶点(称为激发顶点)的颜色值,并忽略其他两个顶点的颜色。您可以使用平面着色和插值的混合,只需flat
在顶点函数的输出上添加或省略限定符即可。“ 金属着色语言”规范定义了您还可以用来修改光栅化行为的其他属性限定符。