您好,欢迎访问代码之道!登录后台查看权限
  • 欢迎大神光临
  • 有朋自远方来 不亦悦乎

Metal中文文档:你好三角形

Objective-C 老刘 2018-09-20 2638 次浏览 0个评论

示范如何去渲染一个2D三角形。

概述

在上一个设备和命令的例子,你学会了如何使用Metal 写一个应用,发送简单的渲染命令到GPU。

在这个例子中,你将学会如何在Metal上渲染一个简单几何图形。重点是,你将学会如何用顶点数据(vertex data) 和SIMD类型去工作。配置图形渲染管线,写GPU函数,并发起绘制调用。

Metal图形渲染管线

Metal执行绘制命令的图形渲染管线由多个GPU阶段组成,有些是可编程的有些是固定的。 Metal将管线的输入、处理过程、输出定义为渲染命令的集合,应用到特定的数据。 大多数的基本形式是:管线接收顶点和输入(输入buffer,输入纹理),然后渲染像素点为输出。 这个例子重点关注管线的三个主要阶段:顶点函数,光栅阶段,片元函数。顶点函数和片元函数是可编程阶段, 光栅阶段是固定的。

Metal管线阶段

Metal图形渲染管线对应类MTLRenderPipelineState,一个对象就代表一个正在渲染的管线。 许多管线阶段都是可以用一个MTLRenderPipelineDescriptor对象配置的,她定义了Metal如何 处理从输入顶点到渲染输出像素的大部分(参数)。

顶点数据

一个顶点就是空间中两条或者更多条直线相交的点(注:比如三角两条边相交处,就是顶点)。 通常,顶点(Vertices)表示笛卡儿坐标(Cartesian coordinate)中的一个集合,用来定义 一个特定的几何体,以及和每个坐标相关联的可选数据(注:比如每个顶点的颜色)。

这个例子渲染了一个由三个顶点构成的简单2D三角形,每个顶点包含一个三角形拐角的位置和颜色。

三角形顶点

位置是顶点必须有的属性,然而颜色是可选的。对于这个例子,管线用到了顶点的两个属性,将彩色的三 角形绘制到drawable的指定区域。

使用SIMD数据类型

从特定的模型软件导出的3D数据文件加载顶点数据是很有用的。一个具体的模型可能包含上千个有许多属性的顶点, 但是最终他们都会被打包、编码为数组的形式,发送到GPU。

这个例子中,对三角形三个顶点中的每个顶点都定义了一个2D位置(x,y)和RGBA颜色(红,绿,蓝, 透明度)属性。

三角形的数据相对比较少,所以直接在代码中填写一个结构体的数组,数组的每个元素代表一个顶点。 结构体用作数组元素类型,定义了每个顶点的内存布局。

一般顶点数据和3D图形数据,使用向量数据类型定义是有用的,简化常用的图形算法和GPU处理。这个例子充分 利用了SIMD库提供的向量类型来表达三角形的顶点。SIMD库是独立于Metal和MetalKit的,但是因为她的便利性和性能好处, 所以极力推荐用于开发Metal应用。

用一个vector_float2SIMD数据类型表示三角形的2D位置坐标,她包含两个32位的浮点数。 类似,用一个vector_float4SIMD数据类型表示三角形的RGBA颜色,她包含4个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 } },
};

视口(ViewPort)设置

视口用于指定Metal渲染内容到drawable的区域。视口是由x、y、width、height、 和远、近平面组成的3D区域(这里不需要最后两个参数,因为这个例子渲染的内容仅仅是2D的)。

为管线分配一个自定义的视口需要调用渲染命令编码器的方法setViewport:,将一个 MTLViewport结构体编码进去。如果没有指定一个视口,Metal将设置一个和创建编码器的drawable 一样尺寸的默认视口。

编写顶点函数

顶点函数(也叫顶点着色器)的主要任务是处理传进来的顶点并将每个顶点映射到视口中对应的位置。 这样,在后续的管线阶段中,就可以参考视口的位置并渲染像素数据到一个精准的drawable位置 中。顶点函数完成从顶点坐标转换到归一化设备坐标的任务,也叫做裁剪坐标。

裁剪空间是一个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提供更多的带宽, 在大量顶点和片元情况下可以并行工作。然而,GPU比CPU内存少,并且不能高效地处理控制流(注:if等流程控制)操作, 并且通常有更高的延迟。

顶点函数在这个例子中命名为vertexShader,并且这就是她的签名。

vertex RasterizerData
vertexShader(uint vertexID [[vertex_id]],
    constant AAPLVertex *vertices [[buffer(AAPLVertexInputIndexVertices)]],
    constant vector_uint2 *viewportSizePointer [[buffer(AAPLVertexInputIndexViewportSize)]])

声明顶点函数参数

Declare Vertex Function Parameters

第一个参数,vertexID,用了属性[[vertex_id]]修饰,她包含当前正在被执行的顶点的索引值(数组下标)。 当使用该函数开始绘制时,这个值从0开始递增,vertexShader函数的每次执行都分配到唯一的值。 一个参数使用属性[[vertex_id]]修饰,就表明该参数用于顶点数组的索引值。

第二个参数,vertices,她包含顶点的数组,每个顶点都定义为AAPLVertex数据类型。 用这个结构体指针定义顶点数组(指向顶点数组)。

第三个参数也就是最后一个参数,viewportSizePointer,包含视口的大小,数据类型为vector_uint2。 The third and final parameter, viewportSizePointer, contains the size of the viewport and has a vector_uint2 data type.

verticesviewportSizePointer两个参数都使用了SIMD数据类型,这些类型可以被C和Metal 着色器语言代码理解。因此,这个例子在共享的头文件AAPLShaderTypes.h中定义了结构体AAPLVertex, 她被AAPLRenderer.m和AAPLShaders.metal包含。共享的头文件,保证三角形的顶点数据在 Objective-C中的声明(triangleVertices)和在Metal着色器语言中的声明(vertices) 是一样的。使用SIMD类型确保内存布局声明在CPU/GPU之间正确匹配,使得从CPU发送顶点数据到GPU 变得容易。

笔记
对结构体`AAPLVertex`的任何修改,将对文件AAPLRenderer.m和AAPLShaders.metal 产生同样的影响。

verticesviewportSizePointer这两个参数都使用了属性[[buffer(index)]]修饰。 AAPLVertexInputIndexVerticesAAPLVertexInputIndexViewportSize的值是一种指示, 用于标识AAPLRenderer.m和AAPLShaders.metal之间的顶点函数输入。

声明顶点函数的返回值

结构体RasterizerData定义了顶点函数的返回值类型。

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;

顶点函数必须为每个顶点返回一个裁剪空间坐标,这里用的是成员clipSpacePosition, 需通过属性[[position]]修饰。当成员被[[position]]修饰,在管线的下个阶段(光栅化), 就会用clipSpacePosition的值去标识三角形的拐角位置,从而确定需要渲染的像素点。

处理顶点数据

例子的顶点函数需要为输入顶点做两件事情:

  • 执行坐标系统变换,将所得的裁剪空间位置写到返回值的out.clipSpacePosition
  • 直接将顶点颜色给返回值的out.color

以参数vertexID作为顶点数组vertices的下标得到一个输入顶点。

float2 pixelSpacePosition = vertices[vertexID].position.xy;

这个例子从每个顶点元素的position成员获得一个2D顶点坐标,并将她转为裁剪空间位置,然后写到 返回值的out.clipSpacePosition。输入顶点x、y都对应像素数,但是原点为视口的中心。因此,从 像素空间位置转为为裁剪空间位置,只要除以视口尺寸的一半就可以了。

out.clipSpacePosition.xy = pixelSpacePosition / (viewportSize / 2.0);

最后,顶点函数访问每个顶点的成员color,不经任何转换,直接传递给返回值的out.color

out.color = vertices[vertexID].color;

现在,完成了返回值RasterizerData的内容,这个结构体将传递到下个管线阶段。

光栅化(Rasterization)

在顶点函数为了三角形的每个顶点执行了3次之后,管线的下个阶段,光栅化开始了。

光栅化是管线光栅单元产生片元(fragment)的阶段。片元包括未加工的预像素数据,产生用于渲染 到drawable的像素数据。顶点函数每次完成三角形生成后,由光栅器确定三角形覆盖的是drawable的那些像素。 她也要测试drawable的每个像素的中心是否在三角形内。下图展示了那些片元的像素中心是在生成三角形之内的, 这些像素统一用灰色表示。

像素中心在三角内的片元.png

光栅化也决定下一个管线阶段(片元函数)的相关值。在上一个管线阶段,顶点函数输出了一个 RasterizerData的值,她包含裁剪坐标clipSpacePosition和颜色colorclipSpacePosition 成员用了属性[[position]]修饰,表明将直接用于裁决三角形片元覆盖区域。颜色成员没有用任何属性修饰, 表明这些值应该在三角形片元间内插。

光栅器从每个顶点转换得到每个片元之后,将颜色传递到片元函数。这个转换使用了固定的插值函数, 由三角形的三个顶点的颜色计算得到单一加权颜色。内插函数权重(也称为重心坐标,barycentric coordinates) 和每个顶点到该片元中心的距离有关。比如:

  • 如果一个片元刚好在三角形的中间,和三角形的三个顶点都是等距的,那么每个顶点的颜色权重是 1/3,下图展示的灰色片元(0.33, 0.33, 0.33)就是在三角形的中心。
  • 如果一个片元离其中一个顶点超近而离另外两个贼远,那么近的顶点将趋向于1,远的那两个将趋向 于0。在下图中,淡红的片元(0.5, 0.25, 0.25)就在三角形的右下角。
  • 如果一个片元在三角形的边缘,在三个顶点中的两个之间,那么这两个边缘顶点的权重分别是1/2, 非边缘顶点的权重为0。在下图中,蓝绿色的片元(0.5, 0.25, 0.25)就在三角形的左边缘。

片元权重示例

因为光栅化是固定的管线状态,意味着她的行为不能通过Metal着色器语言代码修改。光栅器创建片元后, 和她相关的结果数据就会被传递到下一个管线阶段。

编写片元函数

片元函数(也叫做片元着色器)的主要任务是处理传进来的片元数据并计算drawable的像素颜色。

这个例子的中的片元函数叫做fragmentShader并且这就是她的签名。

fragment float4 fragmentShader(RasterizerData in [[stage_in]])

这个函数有一个参数,in,和顶点函数返回值使用一样的结构体RasterizerData。 使用属性[[stage_in]]修饰表明这个参数从光栅器而来。这个函数的返回值类型是一个由4 个浮点数组成的向量,她包含最终被渲染到drawable的颜色(RGBA)。

这个例子展示的是一个非常简单的片元函数:返回从光栅器得到的插值色,没有特殊处理。 每个片元将她的插值色渲染到和她相关的三角形中的像素点。

return in.color;

获取函数库和管线

当构建这个例子时,Xcode将AAPLShaders.metal文件和Objective-C代码一起编译。但是Xcode不会 在编译时链接vertexShader和fragmentShader函数;相反,应用需要在运行时连接这些函数。

Metal着色器代码编译需要两个阶段:

  • 前端编译(注:编译前端主要包括词法分析、语法分析、语义分析、中间代码生成这几个部分)发生在Xcode在编译期, 将.metal文件从高级源码编译为中间(IR)文件。
  • 后端编译(注:编译后端包含代码优化和目标代码生成部分)发生在物理设备的运行期,IR文件被编译到低级机器码。

每个GPU家族都有不同的指令集,结果就是,Metal着色器语言代码只能通过物理设备自身在运行时将其完全编译为GPU机器码。 前端编译通过存储IR到文件default.metallib,以减少编译的开销,示例代码将该文件存储到.app包中。

文件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对象。调用 设备对象的newRenderPipelineStateWithDescriptor:error:函数开始后端编译处理,链接vertexShaderfragmentShader函数,得到一个完全编译的管线。

一个MTLRenderPipelineState对象包含通过MTLRenderPipelineDescriptor对象配置的 那些管线选项,包括顶点和片元函数,这个例子也对colorAttachments数组的第一个元素 进行了pixelFormat配置。这个例子仅仅渲染一个三角形,view的drawable(colorAttachments[0]) 像素格式是通过view自身的colorPixelFormat配置的。一个view的像素 格式定义了每个像素点的内存布局;Metal在创建管线的时候必须要得知该内存布局,这样她才可以 通过片元函数正确产生渲染颜色。

[renderEncoder setRenderPipelineState:_pipelineState];

这个例子使用了方法setVertexBytes:length:atIndex:,发送数据到顶点函数。 前面提到例子的vertexShader的函数签名有两个参数,verticesviewportSizePointer 使用了属性[[buffer(index)]]修饰。函数setVertexBytes:length:atIndex:的索引 对应属性[[buffer(index)]]修饰的索引,她们的值一样。这样,就可以调用函数setVertexBytes:length:atIndex: 为特定的顶点函数参数设置特定的顶点数据。

AAPLVertexInputIndexVerticesAAPLVertexInputIndexViewportSize定义在头文件 AAPLShaderTypes.h中,该头文件在AAPLRenderer.m和AAPLShaders.metal之间共享。这两个值作为 函数setVertexBytes:length:atIndex:的索引参数,同样作为顶点函数修饰属性[[buffer(index)]] 的索引。在不同的文件间共享这些值,通过减少直接写整数字面量可能导致的索引匹配错误(可能发送数据到错误的参数), 使得代码更加强壮。

接着,例子发送顶点数据到顶点函数:

  • triangleVertices指针发送到参数vertices,用AAPLVertexInputIndexVertices为索引
  • &_viewportSize指针发送到参数viewportSizePointer,用AAPLVertexInputIndexViewportSize为索引
// 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];

绘制三角形

在配置管线并关联顶点数据后,发起绘制调用,执行管线并绘制例子的单个三角形。 这个例子编码一个绘制命令到渲染命令编码器。

在Metal中,三角形(Triangle)是一个几何图元,绘制她,需要三个顶点。其他的图元,线(Line),需要两个顶点, 点(Point)则仅仅要一个顶点。函数drawPrimitives:vertexStart:vertexCount:让你可以精确地使用指定 的绘制类型和顶点绘制,顶点来源于之前的设置。参数vertexStart设置为0,表示应该从顶点数组的第一个顶点开始 绘制。这意味着,使用了属性[[vertex_id]]修饰顶点函数参数vertexID的起始值是什么?是0。设置参数vertexCount 为3,表示生成三角形一共需绘制三个顶点(也就是顶点函数被执行三次,参数vertexID分别为0,1,2)。

// Draw the 3 vertices of our triangle
[renderEncoder drawPrimitives:MTLPrimitiveTypeTriangle
                  vertexStart:0
                  vertexCount:3];

为了这个三角形,该调用是最后一次调用去编码渲染命令了。随着该调用完成,渲染循环就可以结束编码并提交命令缓冲, 将已经渲染好的三角形的drawable显示出来。

下一步

在这个例子中,你学会如何在Metal渲染一个简单的几何图形。在基础缓冲 这个例子中,你将学会如何用一个顶点缓冲提高你的渲染效率。


英文原文Hello Triangle

已有 2638 位网友参与,快来吐槽:

发表评论