Metal框架详细解析(三) —— 渲染简单的2D三角形(一)

版本记录

版本号 时间
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库独立于MetalMetalKit,但强烈建议用于开发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数据类型。

verticesviewportSizePointer参数都使用SIMD数据类型,这些类型是C和Metal着色语言代码都能理解的类型。因此,示例可以在共享AAPLShaderTypes.h标头中定义AAPLVertex结构,该结构包含在AAPLRenderer.mAAPLShaders.metal代码中。因此,共享头确保三角形顶点的数据类型在Objective-C声明(triangleVertices)中与在Metal着色语言声明(vertices)中相同。在Metal应用程序中使用SIMD数据类型可确保内存布局在CPU / GPU声明中完全匹配,并有助于将顶点数据从CPU发送到GPU。

注意:对AAPLVertex结构的任何更改都会同等地影响AAPLRenderer.mAAPLShaders.metal代码。

verticesviewportSizePointer参数都使用[[buffer(index)]]属性限定符。 AAPLVertexInputIndexVerticesAAPLVertexInputIndexViewportSize的值是用于在AAPLRenderer.mAAPLShaders.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无法在构建时链接vertexShaderfragmentShader函数;相反,应用程序需要在运行时显式链接这些函数。

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:方法开始后端编译过程,该过程链接vertexShaderfragmentShader函数,从而产生完全编译的管道。

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函数的签名有两个参数,verticesviewportSizePointer,它们使用[[buffer(index)]]属性限定符。 setVertexBytes:length:atIndex:方法中index参数的值映射到[[buffer(index)]]属性限定符中具有相同index值的参数。因此,调用setVertexBytes:length:atIndex:方法为特定的顶点函数参数设置特定的顶点数据。

AAPLVertexInputIndexVerticesAAPLVertexInputIndexViewportSize值在AAPLRenderer.mAAPLShaders.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中渲染基本几何体,感兴趣的给个赞或者关注~~~

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,723评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,080评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,604评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,440评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,431评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,499评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,893评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,541评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,751评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,547评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,619评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,320评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,890评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,896评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,137评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,796评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,335评论 2 342

推荐阅读更多精彩内容