版本记录
版本号 | 时间 |
---|---|
V1.0 | 2018.10.05 星期五 |
前言
很多做视频和图像的,相信对这个框架都不是很陌生,它渲染高级3D图形,并使用GPU执行数据并行计算。接下来的几篇我们就详细的解析这个框架。感兴趣的看下面几篇文章。
1. Metal框架详细解析(一)—— 基本概览
2. Metal框架详细解析(二) —— 器件和命令(一)
Overview
在上一篇中,您学习了如何编写使用Metal的应用程序并向GPU发出基本渲染命令。
在本示例中,您将学习如何在Metal中渲染基本几何体。 特别是,您将学习如何使用顶点数据和SIMD
类型,配置图形渲染管道,编写GPU函数以及发出绘制调用。
The Metal Graphics Rendering Pipeline - Metal图形渲染管道
Metal图形渲染管道由多个图形处理单元(GPU)阶段组成,一些是可编程的,一些是固定的,用于执行绘图命令。 Metal将管道的输入,过程和输出定义为应用于某些数据的一组渲染命令。 在最基本的形式中,管道接收顶点作为输入并将像素渲染为输出。 此示例主要关注管道的三个主要阶段:顶点函数,光栅化阶段和片段函数。 顶点函数和片段函数是可编程阶段。 光栅化阶段是固定的。
MTLRenderPipelineState
对象表示图形渲染管道。 可以使用MTLRenderPipelineDescriptor
对象配置此管道的许多阶段,该对象定义了Metal处理输入顶点到渲染输出像素的大部分方式。
Vertex Data - 顶点数据
顶点只是两个或多个线相交的空间中的一个点。 通常,顶点表示为定义特定几何的笛卡尔坐标的集合,以及与每个坐标相关联的可选数据。
此示例呈现由三个顶点组成的简单2D三角形,每个顶点包含三角形角的位置和颜色。
Position
是必需的顶点属性,而color
是可选的。 对于此示例,管道使用两个顶点属性将彩色三角形渲染到drawable
的特定区域。
Use SIMD Data Types - 使用SIMD数据类型
顶点数据通常从包含从专用建模软件导出的3D模型数据的文件加载。详细模型可能包含数千个具有许多属性的顶点,但最终它们都以某种形式的数组阵列结束,这些阵列经过特殊打包,编码并发送到GPU。
示例的三角形为其三个顶点中的每一个定义了2D位置(x,y)和RGBA颜色(红色,绿色,蓝色,alpha)。这种相对少量的数据被直接硬编码到结构数组中,其中数组的每个元素代表单个顶点。用作数组元素的数据类型的结构定义了每个顶点的内存布局。
顶点数据和一般的3D图形数据通常用矢量数据类型定义,简化了常见的图形算法和GPU处理。此示例使用SIMD库提供的优化矢量数据类型来表示三角形的顶点。 SIMD
库独立于Metal
和MetalKit
,但强烈建议用于开发Metal应用程序,主要是因为它的便利性和性能优势。
三角形的2D位置组件由vector_float2 SIMD
数据类型联合表示,该类型包含两个32位浮点值。类似地,三角形的RGBA颜色分量用vector_float4 SIMD
数据类型联合表示,该数据类型包含四个32位浮点值。然后将这两个属性组合成单个AAPLVertex
结构。
typedef struct
{
// Positions in pixel space
// (e.g. a value of 100 indicates 100 pixels from the center)
vector_float2 position;
// Floating-point RGBA colors
vector_float4 color;
} AAPLVertex;
三角形的三个顶点直接硬编码为AAPLVertex
元素数组,从而定义每个顶点的精确属性值。
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 } },
};
Set a Viewport - 设置视口
视口指定Metal
渲染内容的drawable
区域。 视口是具有x和y偏移,宽度和高度以及近和远平面的3D区域(尽管这里不需要这两个,因为此示例仅渲染2D内容)。
为管道分配自定义视口需要通过调用setViewport:
方法将MTLViewport
结构编码为渲染命令编码器。 如果未指定视口,Metal会设置一个默认视口,其大小与用于创建渲染命令编码器的drawable
相同。
Write a Vertex Function - 写一个顶点函数
顶点函数(也称为顶点着色器vertex shader
)的主要任务是处理传入的顶点数据并将每个顶点映射到视口中的位置。 这样,管道中的后续阶段可以引用此视口位置并将像素渲染到drawable
中的精确位置。 顶点函数通过将任意顶点坐标转换为标准化设备坐标(也称为剪辑空间坐标clip-space coordinates
)来完成此任务。
剪辑空间Clip space
是一个2D坐标系,它将视口区域沿x轴和y轴映射到[-1.0,1.0]范围。 视口的左下角映射到(-1.0,-1.0),右上角映射到(1.0,1.0),中心映射到(0.0,0.0)。
顶点函数对于绘制的每个顶点执行一次。 在此示例中,对于每个帧,绘制三个顶点以构成三角形。 因此,顶点函数每帧执行三次。
顶点函数是用Metal着色语言Metal shading language
编写的,它基于C ++ 14
。Metal着色语言代码可能看起来类似于传统的C / C ++
代码,但两者根本不同。 传统的C / C ++
代码通常在CPU上执行,而Metal着色语言代码专门在GPU上执行。 GPU提供了更大的处理带宽,并且可以在大量顶点和片段上并行工作。 但是,它具有比CPU少的内存,不能有效地处理控制流操作,并且通常具有更高的延迟。
此示例中的顶点函数称为vertexShader
,这是它的声明。
vertex RasterizerData
vertexShader(uint vertexID [[vertex_id]],
constant AAPLVertex *vertices [[buffer(AAPLVertexInputIndexVertices)]],
constant vector_uint2 *viewportSizePointer [[buffer(AAPLVertexInputIndexViewportSize)]])
1. Declare Vertex Function Parameters - 声明顶点函数参数
第一个参数vertexID
使用[[vertex_id]]
属性限定符并保存当前正在执行的顶点的索引。当绘制调用使用此顶点函数时,此值从0开始,并在每次调用vertexShader
函数时递增。使用[[vertex_id]]
属性限定符的参数通常用于索引包含顶点的数组。
第二个参数vertices
是包含顶点的数组,每个顶点定义为AAPLVertex
数据类型。指向此结构的指针定义了这些顶点的数组。
第三个也是最后一个参数viewportSizePointer
包含视口的大小,并具有vector_uint2
数据类型。
vertices
和viewportSizePointer
参数都使用SIMD
数据类型,这些类型是C和Metal着色语言代码都能理解的类型。因此,示例可以在共享AAPLShaderTypes.h
标头中定义AAPLVertex
结构,该结构包含在AAPLRenderer.m
和AAPLShaders.metal
代码中。因此,共享头确保三角形顶点的数据类型在Objective-C声明(triangleVertices)
中与在Metal
着色语言声明(vertices)
中相同。在Metal应用程序中使用SIMD
数据类型可确保内存布局在CPU / GPU声明中完全匹配,并有助于将顶点数据从CPU发送到GPU。
注意:对
AAPLVertex
结构的任何更改都会同等地影响AAPLRenderer.m
和AAPLShaders.metal
代码。
vertices
和viewportSizePointer
参数都使用[[buffer(index)]]
属性限定符。 AAPLVertexInputIndexVertices
和AAPLVertexInputIndexViewportSize
的值是用于在AAPLRenderer.m
和AAPLShaders.metal
代码中标识和设置顶点函数输入的索引。
2. Declare Vertex Function Return Values - 声明顶点函数返回值
typedef struct
{
// The [[position]] attribute of this member indicates that this value is the clip space
// position of the vertex when this structure is returned from the vertex function
float4 clipSpacePosition [[position]];
// Since this member does not have a special attribute, the rasterizer interpolates
// its value with the values of the other triangle vertices and then passes
// the interpolated value to the fragment shader for each fragment in the triangle
float4 color;
} RasterizerData;
顶点函数必须通过[[position]]
属性限定符为clipSpacePosition
成员使用返回每个顶点的剪辑空间位置值。 声明此属性后,管道的下一个阶段(栅格化rasterization
)使用clipSpacePosition
值来标识三角形角的位置,并确定要渲染的像素。
3. Process Vertex Data - 处理顶点数据
示例顶点函数的主体对输入顶点做两件事:
- 1) 执行坐标系转换,将生成的顶点剪辑空间位置写入
out.clipSpacePosition
返回值。 - 2) 将顶点颜色传递给
out.color
返回值。
要获取输入顶点,vertexID
参数用于索引顶点数组。
float2 pixelSpacePosition = vertices[vertexID].position.xy;
此示例从每个vertices
元素的position
成员获取2D顶点坐标,并将其转换为写入out.clipSpacePosition
返回值的剪辑空间位置。 每个顶点输入位置相对于从视口中心开始的x和y方向上的像素数定义。 因此,为了将这些像素空间位置转换为剪辑空间位置,顶点函数除以视口大小的一半。
out.clipSpacePosition.xy = pixelSpacePosition / (viewportSize / 2.0);
最后,顶点函数访问每个vertices
元素的color
成员并将其传递给out.color
返回值,而不执行任何修改。
out.color = vertices[vertexID].color;
RasterizerData
返回值的内容现在已完成,结构将传递到管道中的下一个阶段。
Rasterization - 光栅化
顶点函数执行三次后,对于每个三角形的顶点执行一次,管道中的下一个阶段,即栅格化开始。
光栅化是管道光栅化器单元产生碎片的阶段。 片段包含原始预像素数据,用于生成渲染到drawable的像素。 对于由顶点函数生成的每个完整三角形,光栅化器确定目标可绘制的哪些像素被三角形覆盖。 它通过测试drawable
中每个像素的中心是否在三角形内部来实现。 在下图中,仅生成像素中心位于三角形内部的片段。 这些片段显示为灰色方块。
栅格化还确定发送到管道中下一个阶段的值:片段函数。在管道的早期,顶点函数输出RasterizerData
结构的值,该结构包含剪辑空间位置(clipSpacePosition)
和颜色(color)
。 clipSpacePosition
成员使用所需的[[position]]
属性限定符,指示这些值直接用于确定三角形的片段覆盖区域。color
成员没有属性限定符,表示应该在三角形的片段中插入这些值。
在将每个顶点值转换为每个片段值之后,光栅化器将color
值传递给片段函数。此转换使用固定插值函数,该函数计算从三角形的三个顶点的color
值派生的单个加权颜色。插值函数的权重(也称为重心坐标barycentric coordinates
)是每个顶点位置与片段中心的相对距离。例如:
如果片段正好位于三角形的中间,与每个三角形的三个顶点等距,则每个顶点的颜色加权1/3。 在下图中,这显示为三角形中心的灰色片段
(0.33,0.33,0.33)
。如果一个片段非常靠近一个顶点并且距离另外两个非常远,则将近顶点的颜色加权为1,将远点的颜色加权为0。在下图中,这显示为偏红色 片段
(0.5,0.25,0.25)
靠近三角形的右下角。如果片段位于三角形的边缘,在三个顶点中的两个顶点的中间,则每个边缘定义顶点的颜色加权1/2,非边缘顶点的颜色加权0。在下图中, 这显示为三角形左边缘的青色片段
(0.0,0.5,0.5)
。
由于光栅化是固定的管道阶段,因此无法通过自定义Metal
着色语言代码修改其行为。 在光栅化器创建片段及其关联值之后,结果将传递到管道中的下一个阶段。
Write a Fragment Function - 写一个片段函数
片段函数(也称为片段着色器fragment shader
)的主要任务是处理传入的片段数据并计算可绘制像素的颜色值。
此示例中的片段函数称为fragmentShader
,这是它的签名。
fragment float4 fragmentShader(RasterizerData in [[stage_in]])
该函数有一个参数in
,它使用由顶点函数返回的相同RasterizerData
结构。 [[stage_in]]
属性限定符表示此参数来自光栅化器。 该函数返回一个四分量浮点向量,其中包含要呈现给drawable
的最终RGBA颜色值。
此示例演示了一个非常简单的片段函数,该函数返回光栅化器的插值color
值,无需进一步处理。 每个片段将其插值color
值渲染到三角形中的对应像素。
return in.color;
Obtain Function Libraries and Create a Pipeline - 获取函数库并创建管道
在构建示例时,Xcode会编译AAPLShaders.metal
文件以及Objective-C
代码。但是,Xcode无法在构建时链接vertexShader
和fragmentShader
函数;相反,应用程序需要在运行时显式链接这些函数。
Metal
着色语言代码分两个阶段编译:
1) 前端编译在构建时在Xcode中发生
.metal
文件从高级源代码编译为中间表示(IR)文
件。2) 后端编译在运行时在物理设备中进行。然后将
IR
文件编译为低级机器代码。
每个GPU
系列都有不同的指令集。因此,Metal shading语言代码只能在运行时由物理设备本身完全编译为本机GPU代码。前端编译通过将IR存储在打包在示例的.app包中的default.metallib文件中来减少一些编译开销。
default.metallib
文件是Metal
着色语言函数库,由运行时通过调用newDefaultLibrary
方法检索的MTLLibrary
对象表示。从该库中,可以检索由MTLFunction
对象表示的特定函数。
// Load all the shader files with a .metal file extension in the project
id<MTLLibrary> defaultLibrary = [_device newDefaultLibrary];
// Load the vertex function from the library
id<MTLFunction> vertexFunction = [defaultLibrary newFunctionWithName:@"vertexShader"];
// Load the fragment function from the library
id<MTLFunction> fragmentFunction = [defaultLibrary newFunctionWithName:@"fragmentShader"];
这些MTLFunction
对象用于创建表示图形渲染管道的MTLRenderPipelineState
对象。调用MTLDevice
对象的newRenderPipelineStateWithDescriptor:error:
方法开始后端编译过程,该过程链接vertexShader
和fragmentShader
函数,从而产生完全编译的管道。
MTLRenderPipelineState
对象包含由MTLRenderPipelineDescriptor
对象配置的其他管道设置。除顶点和片段函数外,此示例还配置colorAttachments
数组中第一个条目的pixelFormat
值。此示例仅渲染到单个目标,即视图的drawable(colorAttachments [0])
,其像素格式由视图本身(colorPixelFormat)
配置。视图的像素格式定义了每个像素的内存布局;在创建管道时,Metal必须能够引用此布局,以便它可以正确呈现fragment函数生成的颜色值。
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];
Send Vertex Data to a Vertex Function - 将顶点数据发送到顶点函数
创建管道后,可以将其分配给渲染命令编码器。 此操作将由该特定管道处理所有后续渲染命令。
[renderEncoder setRenderPipelineState:_pipelineState];
此示例使用setVertexBytes:length:atIndex:
方法将顶点数据发送到顶点函数。如前所述,示例的vertexShader
函数的签名有两个参数,vertices
和viewportSizePointer
,它们使用[[buffer(index)]]
属性限定符。 setVertexBytes:length:atIndex:
方法中index
参数的值映射到[[buffer(index)]]
属性限定符中具有相同index
值的参数。因此,调用setVertexBytes:length:atIndex:
方法为特定的顶点函数参数设置特定的顶点数据。
AAPLVertexInputIndexVertices
和AAPLVertexInputIndexViewportSize
值在AAPLRenderer.m
和AAPLShaders.metal文
件之间共享的AAPLShaderTypes.h
标头中定义。该示例将这些值用于setVertexBytes:length:atIndex:
方法的index
参数以及与同一顶点函数对应的[[buffer(index)]]
属性限定符。通过减少由于硬编码整数(可能将错误的数据发送到错误的参数)导致的潜在索引不匹配,可以跨不同文件共享这些值,从而使示例更加健壮。
此示例将以下顶点数据发送到顶点函数:
- 使用
AAPLVertexInputIndexVertices
索引值将triangleVertices
指针发送到vertices
参数 - 使用
AAPLVertexInputIndexViewportSize
索引值将_viewportSize
指针发送到viewportSizePointer
参数
// You send a pointer to the `triangleVertices` array also and indicate its size
// The `AAPLVertexInputIndexVertices` enum value corresponds to the `vertexArray`
// argument in the `vertexShader` function because its buffer attribute also uses
// the `AAPLVertexInputIndexVertices` enum value for its index
[renderEncoder setVertexBytes:triangleVertices
length:sizeof(triangleVertices)
atIndex:AAPLVertexInputIndexVertices];
// You send a pointer to `_viewportSize` and also indicate its size
// The `AAPLVertexInputIndexViewportSize` enum value corresponds to the
// `viewportSizePointer` argument in the `vertexShader` function because its
// buffer attribute also uses the `AAPLVertexInputIndexViewportSize` enum value
// for its index
[renderEncoder setVertexBytes:&_viewportSize
length:sizeof(_viewportSize)
atIndex:AAPLVertexInputIndexViewportSize];
Draw the Triangle - 绘制三角形
设置管道及其关联的顶点数据后,发出绘制调用会执行管道并绘制样本的单个三角形。该示例将单个绘图命令编码到渲染命令编码器(render command encoder)
中。
三角形是Metal
中的几何图元,需要绘制三个顶点。其他基元包括需要两个顶点的线,或者只需要一个顶点的点。 drawPrimitives:vertexStart:vertexCount:
方法允许您准确指定要绘制的基元类型以及要使用的从先前设置的顶点数据派生的顶点数据。为vertexStart
参数设置0表示绘图应以顶点数组中的第一个顶点开始。这意味着顶点函数的vertexID
参数的第一个值(使用[[vertex_id]]
属性限定符)将为0
。设置为vertexCount
参数的3
表示应绘制三个顶点,从而生成一个三角形。 (也就是说,对于vertexID
参数,顶点函数执行三次,值为0,1和2)。
// Draw the 3 vertices of our triangle
[renderEncoder drawPrimitives:MTLPrimitiveTypeTriangle
vertexStart:0
vertexCount:3];
此调用是对单个三角形的渲染命令进行编码所需的最后一次调用。 绘图完成后,渲染循环可以结束编码,提交命令缓冲区,并呈现包含渲染三角形的drawable
。
后记
本篇主要讲述了您学习如何在Metal中渲染基本几何体,感兴趣的给个赞或者关注~~~