渲染一个简单的二维三角形
概观
在使用Metal绘制到屏幕时,您学习了如何设置MTKView
对象并使用渲染过程更改视图的内容。该示例只是将视图的内容删除为背景颜色。此示例显示如何配置渲染管道并将其用作渲染过程的一部分,以将简单的2D彩色三角形绘制到视图中。该示例为每个顶点提供位置和颜色,渲染管道使用该数据渲染三角形,在为三角形顶点指定的颜色之间插入颜色。
注意
Xcode项目包含在macOS,iOS和tvOS上运行示例的方案。iOS或者tvOS模拟器不支持Metal,因此iOS和tvOS方案需要物理设备来运行示例。默认方案是macOS。
了解Metal Render Pipeline
一个渲染管线流程绘图命令和数据写入到一个渲染通道的目标。渲染管道有许多阶段,一些使用着着色器编程,另一些使用固定或可配置的行为编程。此示例主要关注管道的三个阶段:顶点阶段、光栅化阶段和片段阶段。顶点阶段和片段阶段是可编程的,因此您可以使用MSL为他们编写函数。光栅化阶段具有固定的行为。
图1 Metal图形化渲染管道的主要阶段
渲染从绘图命令开始,该命令包括顶点的计数和要渲染的基元类型。例如,以下是此示例中的绘图命令:
renderEncoder?.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 3)
顶点阶段为每个顶点提供数据。当处理了足够的顶点时,渲染管道栅格化基元,确定渲染目标中的哪些像素基于基元的边界内。片段阶段确定要写入这些元素的渲染目标的值。
在本示例的其余部分,您将看到如何编写顶点和片段函数,如何创建渲染管道状态对象,以及最后如何编码使用此管道的绘制命令。
决定自定义渲染管道如何处理数据
顶点函数为单个顶点生成数据,片段函数生成单个片段的数据,但您可以决定它们的工作方式。您可以考虑目标来配置管道的各个阶段,这意味着您知道管道要生成什么以及如何生成这些结果。
确定要传递到渲染管道的数据以及将哪些数据传递到管道的后续阶段。通常有三个地方可以执行此操作:
- 管道的输入,由您的应用程序提供并传递到顶点。
- 顶点阶段的输出,传递光栅化阶段。
- 片段阶段的输入,由您的应用提供或光栅化阶段生成。
在此示例中,管道的输入数据是顶点的位置及颜色。为了演示通常在顶点函数中执行的变换类型,输入坐标在自定义坐标空间中定义,以视图中心的像素为单位进行测量。
声明一个AAPLVertex
结构,使用SIMD矢量类型来保存位置和颜色数据。要共享结构在内存中的布局方式的单个定义,请在通用头文件中声明结构,并将其导入着色器(Metal shader)
和app中。
typedef struct
{
vector_float2 position;
vector_float4 color;
} AAPLVertex;
SIMD类型在MSL中很常见,您也应该在应用程序中使用simd
库。SIMD类型包含特定数据类型的多个通道,因此将位置声明为vector_float2
包含两个32位浮点数(他将保存x和y坐标)。颜色需要使用vector_float4
,因为它们有4个通道R、G、B、A。
在应用程序中,使用常亮数组输入指定数据:
let triangleVertices: [AAPLVertex] =
[AAPLVertex(position: [250, -250], color: [1, 0, 0, 1]),
AAPLVertex(position: [-250, -250], color: [0, 1, 0, 1]),
AAPLVertex(position: [0, 250], color: [0, 0, 1, 1])]
顶点阶段为顶点生成数据,因此需要提供颜色和变换位置。使用SIMD类型声明RasterizerData
包含位置和颜色值的结构。
typedef struct
{
float4 position [[position]];
float4 color;
} RasterizerData;
输出位置(下面详细描述)必须定义为vector_float4
,颜色声明为输入数据结构的颜色。
您需要告诉Metal光栅化数据中哪个字段提供位置数据,因为Metal不对结构中的字段强制执行任何特定的命名约定。position
使用[[position]]
属性限定符注释该字段以声明此字段包含输出位置。
片段函数只是将光栅化阶段的数据传递给后期阶段,因=因此它不需要任何其他参数。
声明顶点函数
声明顶点函数,包括其输入参数及输出数据。就像使用kernel
关键字声明计算函数一样,您使用vertex
关键字声明顶点函数。
vertex RasterizerData
vertexShader(uint vertexID [[vertex_id]],
constant AAPLVertex *vertices [[buffer(AAPLVertexInputIndexVertices)]],
constant vector_uint2 *viewportSizePointer [[buffer(AAPLVertexInputIndexViewportSize)]])
第一个参数是vertexID
,使用[[vertex_id]]
属性限定符,他还是一个Metal关键字。执行渲染命令时,GPU会多次调用顶点函数,为每一个顶点生成唯一值。
第二个参数vertices
是一个包含订单数据的数组,使用AAPLVertex
先前定义的结构。
要将位置转换为Metal的坐标,该函数需要绘制三角形的视口大小(以像素为单位),因此将其存储在viewportSizePointer
参数中。
第二个和第三个参数具有[buffer(n)]
属性限定符,默认情况下,Metal会自动为参数表分配每个参数的插槽。将[[buffer(n)]]
限定符添加到缓冲区参数时,可以明确告知Metal使用哪个插槽。明确声明插槽可以更轻松地修改着色器,而无需更改应用程序代码。在共享头文件中声明两个指标的常量。
函数的输出是一个RasterizerData
结构。
写入顶点函数
您的顶点函数必须生成输出结构的两个字段。使用vertexID
参数索引到数组并读取顶点的输入数据。另外,检索视口尺寸。
float2 pixelSpacePosition = vertices[vertexID].position.xy;
vector_float2 viewportSize = vector_float2(*viewportSizePointer);
顶点函数必须在clip-space
坐标空间中提供位置数据,这是使用四维同质矢量(x, y, z, w)指定的3D点。光栅化阶段采用输出位置并将x,y和z坐标除以w,以在标准化设备坐标中生成3D点。标准化设备坐标与视口大小无关。
标准化设备坐标使用左手坐标系并映射到视口中的位置。基元被剪切到此坐标系中的框,然后进行栅格化。剪切框的左下角位于(x,y)
坐标的(-1,-1.0)
,右上角位于(1.0, 1.0)
。正z
值指向远离相机(进入屏幕)。坐标的可见部分位于(近剪辑平面)和(远剪辑平面)之间。
图2 标准化设备坐标系
[图片上传失败...(image-f39984-1565142312808)]
您的顶点函数需要将输入坐标系转化为此输出坐标系。
因为这是一个2D应用程序而不需要同质坐标,所以首先默认值写入输出坐标,其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
中。
out.color = vertices[vertexID].color;
写一个片段函数
一个片段是一个可能改变的渲染目标。光栅化器确定渲染目标的哪些像素被基元覆盖。仅渲染像素中心在三角形内部的片段。
图3 光栅化阶段生成的碎片
片段函数处理来自光栅化器的输入信息,用于单个位置,并计算每个渲染目标的输出值。这些片段由管道中的后续阶段处理,最终写入渲染目标。
注意
片段被称为可能更改的原因是因为片段阶段之后的管道阶段可以配置为拒绝某些片段或更改写入渲染目标的内容。在此示例中,片段阶段计算的所有值都按原样写入渲染目标。
此示例中的片段着色器接收与顶点着色器输出中声明的相同参数。使用fragment
关键字声明片段函数。他需要一个参数,与顶点阶段提供的结构RasterizerData
相同。添加[[stage_in]]
限定符以指示此参数是由光栅化器生成。
fragment float4 fragmentShader(RasterizerData in [[stage_in]])
如果您的片段函数写入多个渲染目标,则它必须为每个渲染目标声明一个包含字段的结构。由于此示例仅具有单个渲染目标,因此您可以直接将浮点矢量指定为函数的输出。此输出的是要渲染目标的颜色。
光栅化阶段计算每个片段参数的值,并使他们调用片段函数。光栅化阶段将其颜色参数计算为三角形顶点处颜色的混合。片段离顶点越近,顶点对最终颜色的贡献越大。
图4 插值片段颜色
将插值颜色作为函数的输出返回
return in.color;
创建渲染管道状态对象
现在函数已经完成,您可以创建使用他们的渲染管道。首先,获取默认库并获取每个函数的MTLFunction对象。
let defaultLibrary = device?.makeDefaultLibrary()
let vertexFunction = defaultLibrary?.makeFunction(name: "vertexShader")
let framentFunction = defaultLibrary?.makeFunction(name: "fragmentShader")
接下里,创建一个MTLRenderPipelineState对象,渲染管道有更多要配置的阶段,因此您使用MTLRenderPipelineDescriptor来配置管道。
let pipelineStateDescriptor = MTLRenderPipelineDescriptor()
pipelineStateDescriptor.label = "Simple Pipeline"
pipelineStateDescriptor.vertexFunction = vertexFunction
pipelineStateDescriptor.fragmentFunction = framentFunction
pipelineStateDescriptor.colorAttachments[0].pixelFormat = mtkView.colorPixelFormat
do {
try pipelineState = device?.makeRenderPipelineState(descriptor: pipelineStateDescriptor)
} catch {
assert(false, "Failed to created pipline state: \(error.localizedDescription)")
}
除了指定顶点和片段函数之外,还要声明管道将绘制的所有渲染目标的像素格式。像素格式(MTLPixelFormat)定义像素数据的存储器布局。对于简单格式,此定义包括每个像素的字节数,存储在像素种的数据通道以及这些通道的位布局。此示例只有一个渲染目标并且由视图提供,因此将视图的像素格式复制到渲染管道描述符中。渲染管道状态必须使用与渲染过程指定的像素格式,因此他们始终相同。
当Metal创建渲染管道状态对象,管道配置为将片段函数的输出转换为渲染目标的像素格式。如果要定位不同的像素格式,则需要创建不同的管道状态对象。您可以在针对不同像素格式的多个管道中重复使用相同的着色器。
设置视口
现在您已拥有管道的渲染管道状态对象,您将渲染三角形。您可以使用渲染命令编码器执行此操作。首先,设置视口,以便Metal知道要绘制渲染目标的哪个部分。
renderEncoder?.setViewport(MTLViewport(originX: 0.0,
originY: 0.0,
width: Double(viewportSize.x),
height: Double(viewportSize.y),
znear: 0.0,
zfar: 1.0))
设置渲染管道状态
设置要使用的管道的渲染管道状态。
renderEncoder?.setRenderPipelineState(pipelineState)
将参数数据发送到顶点函数
通常,您使用buffers
(MTLBuffer)将数据传递给着色器。但是,当您需要将少量数据传递给顶点函数时(如此处所示),将数据直接复制到命令缓冲区中。
该示例将两个参数的数据复制到命令缓冲区中。顶点数据从样本中定义的数组中复制。视口数据是从用于设置视口的相同变量中复制。
在此示例中,片段函数仅使用光栅化器接受的数据,因此没有要设置的参数。
renderEncoder?.setVertexBytes(triangleVertices,
length: MemoryLayout<AAPLVertex>.size * triangleVertices.count,
index: Int(AAPLVertexInputIndexVertices.rawValue))
renderEncoder?.setVertexBytes(&viewportSize,
length: MemoryLayout<vector_uint2>.size,
index: Int(AAPLVertexInputIndexViewportSize.rawValue))
编码绘图命令
指定基元的类型,起始索引和顶点数。渲染三角形时,将vertexID
参数值为0、1和2来调用顶点函数。
使用颜色插值进行实验
在此示例中,颜色值在三角形中进行插值。这通常是你想要的,但有时你想要一个顶点生成一个值,并在整个基元上保持不变。在顶点函数的输出上指定flat
属性限定符以执行此操作。现在试试吧。在示例项目中找到RasterizerData
的定义,并将[[flat]]
限定符添加到其颜色字段中。
float4 color [[flat]];
再次运行该示例。 渲染管道在三角形上均匀地使用第一个顶点(称为激发顶点)的颜色值,并忽略其他两个顶点的颜色。 您可以使用平面着色和插值的混合,只需在顶点函数的输出上添加或省略平坦限定符即可。 “金属着色语言”规范定义了您还可以用来修改光栅化行为的其他属性限定符。
网友评论