Apple Metal 2 2.你好,三角形

原文https://developer.apple.com/documentation/metal/hello_triangle

你好,三角形

示例代码

概览

设备和命令例子中,你学习了如何编写一个使用Metal技术的应用以及了解到一些GPU基础的渲染命令.

在这个例子中,你将要学习使用Metal API 渲染基础的几何体. 尤其 , 你将要学习如何使用顶点数据和SIMD 类型, 配置 图像渲染管线,书写GPU 函数和 绘制调用相关的问题.

Metal 图像渲染管线

Metal 图像渲染管线是有多个图像处理单元阶段组成, 一些事可编程管线,还有一些是固定的 ,执行绘制命令.
Metal 定义 输入, 处理 和管线的输出.作为 渲染命令应用到中心数据.

这是一个基础的表格,管线接受 顶点作为输入 和 渲染像素作为输出.
这个例子,关注 三个主要的管线状态: 顶点函数,光栅化状态 和 片段函数. 顶点函数和片段函数 作为可编程状态. 光栅化是固定的状态.

24096b5e-34f4-460d-a72b-ca5fb5ef51e5.png

MTLRenderPipelineState 对象 展示出 一个 图像渲染管线. 管线的多种状态 能够配置通过使用 MTLRenderPipelineDescriptor对象. 这是定义大部分 如何Metal 处理 输入 顶点 到 渲染 输出像素.

顶点数据

一个顶点是一个 简单的点 在空间中 ,2 个多活多条线.

顶点是 展示位 笛卡尔坐标系 的集合 ,定义 指定的几何体, 可选的数据 连接着 坐标系.

案例中的渲染器是一个简单的2D三角形,它由三个顶点构成. 每一个顶点结构体包含 左边位置还有三角边颜色.

d66f00af-2659-4bc7-8eb2-cccbfdf72a6f.png

位置是被要求的顶点属性. 然后颜色是可选的.
在这个例子中, 这个管线 使用 两个 顶点属性 去渲染 颜色化的三角形 在 指定的 可绘画对象范围.

使用SIMD 数据类型

顶点数据 通常由 指定的模型软件从包含3D 模型 数据的文件中载入.
详细的模型 可能包含成千上万个顶点和很多的属性.但是 最终 它们需要被 打包,编码 然后传送到 GPU 中.

示例的三角形 每一个顶点 结构体 定义 2D位置(x,y) 和 RGBA 颜色(red,green,alpha) .

硬编码到结构体的数组. 每一个元素表示为一个单一顶点.
结构体 使用 数组元素的数据类型 定义 每一个顶点的内存分布.

顶点数据 和3D图形数据 是通用的, 通常 定义 向量数据类型 和 简化的通用图像算法 和GPU 处理.
这个例子使用 优化的向量数据类型 , 用SIMD 库提供, 展示三角形的顶点.

SIMD 库 是独立的 ,和Metal,MetalKit 分开.但是 非常推荐使用 开发Metal App , 主要优点是 方便和高性能.

三角形的2D顶点 组件 加入到展示位 vector_float2 SIMD 数据类型,
2 32位 浮点值类型,
同样的 三角形的RGBA 颜色组件 vector_float4 SIMD 数据类型 ,包含4个32位浮点值,
这些属性 组成 AAPLVertex 结构体.

typedef struct
{
    //  Positions in pixel space (i.e. a value of 100 indicates 100 pixels from the origin/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 渲染内容.

一个视窗是3D面积 有 x, y 偏移值 还有 宽度和高度, 近和远平面,(通过这些最新的不是需要 因为当前例子仅仅渲染2D内容)

设置管线的自定义视窗 要求 编码MTLViewport 结构体 到 渲染命令编码器 调用 setViewport:方法 ,
如果视窗 不指定,Metal 将设置默认视窗大小和可绘画对象(被用于创建渲染命令编码器)一致.

写顶点函数

主要的顶点函数任务 是处理 输入的顶点数据和映射每一个顶点的位置到视窗(viewport),

子序列状态 这个管线能够 涉及 视窗位置和渲染像素 明确的坐标在可绘画对象.

顶点函数 完成任务, 翻译抽象的顶点坐标系到 标准化设备坐标系 (clip-space坐标系).

clip-space 是一个2D坐标系系统 映射 视窗面积 在 [-1.0 ,1.0]范围 ,沿着x,y 轴 .
视窗 左下角 映射(-1.0,-1.0),右上角映射(1.0,1.0) 以及中心 映射(0.0,0.0)

de456929-7205-4672-b4b9-d9aa01e1da0b.png

顶点函数执行一次 每一个顶点绘制一次,
在这个例子中,每一个帧,三个顶点被绘制组成为一个三角形,因此,顶点函数每一帧执行三次.

顶点函数使用 Metal Shading Language 书写. 这是一门基于C++ 14标准的语言. Metal Shading Language 代码看起来和传统的C/C++ 代码相似.
但是有两个最基本的不同,传统的C/C++代码 是被CPU 执行,而Metal Shading Language 代码是被 GPU 执行.
GPU 提供大量处理带宽和能工作 ,并行 大量的顶点和片段, 然而 ,需要少的内存比 CPU , 不需要处理控制流操作 高效, 高延迟.

顶点函数是又名 顶点着色器 和它的签名.

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

声明顶点函数参数

第一个参数, vertexID 使用
[[vertex_id]] 属性标识符 和 当前的顶点索引 执行.
当绘制调用 使用这个顶点函数, 这个值开始为0 ,增加 每一个祈祷 顶点着色器, 一个参数使用 [[vertex_id]] 属性标识符 使用 索引 数组包含顶点.

第二个参数, 顶点 , 是一个包含每一个顶点被定义为AAPLVertex类型的顶点数组. 顶点数组结构体指针.

第三个和最后一个参数 viewportSizePointer 包含 视窗的大小 和 vector_uint2数据类型.

vertices 和 viewportSizePointer 参数 使用SIMA 数据类型, 这个类型理解为 在C和 Metal Shading Language 代码.

示例能够定义 AAPLVertex 结构体 在 AAPLShaderTypes.h 头文件, 包含 AAPLRenderer.mAAPLShaders.metal 代码.
因此 这共享的头文件 确保 三角形的顶点的数据类型 在 OC 声明中 (triangleVertices) 在Metal Shading Language 声明(vertices) ,使用SIMD 数据类型 在你的Metal app 确保 内存分布正确 跨CPU/GPU 描述 , 加速发送顶点数据 从CPU 发送到GPU.

注意:
AAPLVertex结构体的改变都会影响到AAPLRenderer.m 和 AAPLShaders.metal 代码匹配.

verticesviewportSizePointer 参数 使用 [[buffer(index)]] 属性标识符.
AAPLVertexInputIndex 的值 和 AAPLVertexInputIndexViewportSize 索引 使用 表示 和 设置 顶点函数的输入 在AAPLRenderer.mAAPLShaders.metal代码.

声明顶点函数返回值

RasterizerData结构体定义顶点着色器的返回值.

typedef struct
{
    // The [[position]] attribute qualifier of this member indicates 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 qualifier, the rasterizer will
    //   interpolate its value with values of other vertices making up the triangle and
    //   pass that interpolated value to the fragment shader for each fragment in that triangle
    float4 color;

} RasterizerData;

顶点函数必须返回 clip-space位置值, 为每一个顶点 通过 [[position]]属性标识符.
clipSpacePosition成员使用

当属性被声明的时候, 接下里状态就是光栅化, 使用 clipSpacePosition 值定义 三角形的角 的位置 和 决定 像素的渲染.

处理顶点数据

例子中 顶点函数的函数体 有两个输入顶点.

  1. 执行 坐标系统转化, 写 结果 顶点clip-space 到 out.clipSpacePosition 返回值.
  2. 通过顶点颜色 out.color 返回值.

获取输入顶点, vertexID 参数被使用 索引 在 verteices array.

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

这个例子包含 2D 顶点坐标系 在 position成员 每一个顶点元素 和 转化为 clip-space position 写入到 'out.clipSpacePosition' 返回值.
每一个输入位置 定义相关的 像素个数在 X, Y 放心 从 视窗的中心, 因此, 去转化 像素 坐标到clip-space 坐标, 在顶点函数 分割为 视窗 大小的一般.

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

最后 顶点函数访问 每个顶点元素的颜色成员变量 和 通过 out.color 返回值 没有执行任何修改.

out.color = vertices[vertexID].color;

RasterizerData的内容返回值 是完整的 , 结构体 根据管线到达下一个状态.

光栅化

在顶点函数执行三次以后, 三角形的每一个顶点,下一个状态 管线 光栅化 开始.

光栅化 状态, 管线的光栅化单元 生成片段.
一个片段包含原生的 预像素数据 , 用来生产 可绘制对象渲染的像素.

每一个完整的三角形通过顶点函数生产. 光栅化决定 目标可绘画对象的像素 是 被 三角形覆盖.

每一个像素的中心 在可绘画对象中 在 三角形里面.
接下来的示意图, 仅仅片段 中心像素 在三角形 产生.
这些片段 被展示位灰色正方形.

cbe1a0ab-ec96-4f40-ac17-c318af19920c.png

光栅化 永阳 决定 被发送到接下来的状态的值,在管线中 , 片段着色器 在 管线之前, 顶点函数 输出 数据类型为 RasterizerData结构体的返回值, 包含 clip-space 坐标 和 颜色.
clipSpacePosition成员 要求使用 [[position]] 属性标识符.
指示 这些值能被直接使用 决定 三角形片段 面积.
这个颜色成员 没有 属性标识符, 指示 这些值可能会被中断 在通过三角形的片段时候.

光栅化通道 颜色值 到 片段函数 在 转化 配一个顶点值 到 每一个片段值.
这个会话固定 中断函数 ,计算 单一权重颜色 起源于 三角形的三个顶点颜色值.
中断函数的权重 (坐标系) 是相关距离 每一个顶点位置 和 片段中心.

  • 如何片段准确在三角形中间, 和三角形的每一个顶点等距离. 每一个顶点颜色 权重为1/3. ,接下来的示意图, 这展示出了, 灰度片段(0.33,0.33,0.33) 在三角形的中间.
  • If a fragment is very close to one vertex and very far from the other two, the color of the close vertex is weighted toward 1 and the color of the far ones is weighted toward 0. In the following diagram, this is shown as the reddish fragment (0.5, 0.25, 0.25) near the bottom-right corner of the triangle.

如何片段非常接近一个顶点以及 跟另外两个顶点非常远的话,这个接近顶点的颜色是趋向于1 ,并且与远的顶点颜色趋向于0,. 在接下来的示意图中, 微红片段(0.5,0.25,0.25)接近 , 三角形的有低部角.

  • 如果片段是在三角形的边缘, 在三角形任意两个顶点之间的边缘. 每一条顶点定义的边缘的颜色权重是1/2, 颜色的无边权重是0,
    在接下来的示例图中, 展示了青色片段(0,0.5,0.5)在 三角形的左边缘.
d2937441-8d28-4657-8e67-75b9b28edf86.png

Because rasterization is a fixed pipeline stage, its behavior can’t be modified by custom Metal shading language code. After the rasterizer creates a fragment, along with its associated values, the results are passed along to the next stage in the pipeline.
因为光栅化是固定的管线状态,他的行为是不能被Metal Shading Language 代码修改的, 在光栅化创建片段以后, 复制, 这个结果是 传递到下一个状态.

编写片段着色器

The main task of a fragment function (also known as fragment shader) is to process incoming fragment data and calculate a color value for the drawable’s pixels.

片段函数的主要任务是 处理 传进来的片段数据,然后计算 可绘画对象的像素的颜色值.

The fragment function in this sample is called fragmentShader and this is its signature

片段函数 被叫做 片段着色器, fragment 是函数标识符.

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

The function has a single parameter, in, that uses the same RasterizerData structure returned by the vertex function. The [[stage_in]] attribute qualifier indicates that this parameter comes from the rasterizer. The function returns a four-component floating-point vector, which contains the final RGBA color value to be rendered to the drawable.

这个函数有单一的参数, in 使用相同的RasterizerData 结构体 返回 顶点函数 .
'[[stage_in]]' 属性标识符 指示 参数来自于 光栅化着色器. 这个函数 返回 4个浮点型向量, 包含 RGBA 颜色值, 渲染到 可绘画对象.

This sample demonstrates a very simple fragment function that returns the interpolated color value from the rasterizer, without further processing. Each fragment renders its interpolated color value to its corresponding pixel in the triangle.

该示例 展示 一个简单的片段着色器函数,返回 中断的颜色值从光栅化着色器中, 木有进行处理.
每一个片段渲染 他的中断颜色值 和 三角形对应的像素.

return in.color;

包含函数库和创建管线

当编译这个例子时候, Xcode 编译AAPLShaders.metal文件 和 Objective-C代码.

然后 ,Xcode 不能连接 vertexShaderfragmentShader 函数 在编译时刻,
该APP 需要明确连接这些函数在运行时.

Metal Shading Language 代码 编译在两个时刻:

  1. Front-end compilation happens in Xcode at build time. .metal files are compiled from high-level source code into intermediate representation (IR) files.

  2. 前端编译发生在Xcode 编译时刻, .Metal文件编译在高级源码 到IR 文件.

  3. Back-end compilation happens in a physical device at runtime. IR files are then compiled into low-level machine code.

后端发生在物理设备在运行时,IR 文件编译在底层机器码.

每一个GPU 家族 有不同的指令集. 结果, Metal Shading language 代码 仅仅能完全编译到原生的GPU 代码 在运行时, 物理设备.
前端 编译 通过存储IR 在 default.metal 文件 被打包在案例 .app 包中

default.metallib 文件是Metal Shading language 函数库 展示了 MTLLibrary 对象处理 在运行时 调用newDefaultLibrary 方法

在这个library ,指定函数 展示 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对象 使用 MTLRenderStatePipeline对象 展示 图像渲染管线 调用newRenderPipelineStateWithDescriptor:error: MTLDevice 对象 开始 后端编译处理 , 连接带vertexShader 和 fragmentShader 函数 返回全编译的管线.

MTLRenderStatePipeline对象包含 额外的管线 设置 配置 MTLRenderPipelineDescriptor
,而且 顶点和片段函数, 该例子也通用配置 pixelFormat值 在 第一个入口 colorAttachments数组. 该例子是仅仅渲染但一个目标, 视图的可绘画对象(color Attachments[0])

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];

发送顶点数据到顶点函数

在管线创建之后, 它能够赋值 一个 渲染命令编码器, 这个操作 所有子序列渲染命令 将要处理 通过指定的管线.

[renderEncoder setRenderPipelineState:_pipelineState];

This sample uses the setVertexBytes:length:atIndex: method to send vertex data to a vertex function. As mentioned earlier, the signature of the sample’s vertexShader function has two parameters, vertices and viewportSizePointer, that use the [[buffer(index)]] attribute qualifier. The value of the index parameter in the setVertexBytes:length:atIndex: method maps to the parameter with the same index value in the [[buffer(index)]] attribute qualifier. Thus, calling the setVertexBytes:length:atIndex: method sets specific vertex data for a specific vertex function parameter.

The AAPLVertexInputIndexVertices and AAPLVertexInputIndexViewportSize values are defined in the AAPLShaderTypes.h header shared between the AAPLRenderer.m and AAPLShaders.metal files. The sample uses these values for the index parameter of both the setVertexBytes:length:atIndex: method and the [[buffer(index)]] attribute qualifier corresponding to the same vertex function. Sharing these values across different files makes the sample more robust by reducing potential index mismatches due to hard-coded integers (which could send the wrong data to the wrong parameter).

This sample sends the following vertex data to a vertex function:

  • The triangleVertices pointer is sent to the vertices parameter, using the AAPLVertexInputIndexVertices index value

  • The _viewportSize pointer is sent to viewportSizePointer parameter, using the AAPLVertexInputIndexViewportSize index value

// Here we're sending a pointer to our 'triangleVertices' array (and indicating its size).
//   The AAPLVertexInputIndexVertices enum value corresponds to the 'vertexArray' argument
//   in our 'vertexShader' function because its buffer attribute qualifier also uses
//   AAPLVertexInputIndexVertices for its index
[renderEncoder setVertexBytes:triangleVertices
                       length:sizeof(triangleVertices)
                      atIndex:AAPLVertexInputIndexVertices];

// Here we're sending a pointer to '_viewportSize' and also indicate its size so the whole
//   think is passed into the shader.  The AAPLVertexInputIndexViewportSize enum value
///  corresponds to the 'viewportSizePointer' argument in our 'vertexShader' function
//   because its buffer attribute qualifier also uses AAPLVertexInputIndexViewportSize
//   for its index
[renderEncoder setVertexBytes:&_viewportSize
                       length:sizeof(_viewportSize)
                      atIndex:AAPLVertexInputIndexViewportSize];

绘制三角形

After setting a pipeline and its associated vertex data, issuing a draw call executes the pipeline and draws the sample’s single triangle. The sample encodes a single drawing command into the render command encoder.

Triangles are geometric primitives in Metal that require three vertices to be drawn. Other primitives include lines that require two vertices, or points that require just one vertex. The drawPrimitives:vertexStart:vertexCount: method lets you specify exactly what type of primitive to draw and which vertices, derived from the previously set vertex data, to use. Setting 0 for the vertexStart parameter indicates that drawing should begin with the first vertex in the array of vertices. This means that the first value of the vertex function’s vertexID parameter, which uses the [[vertex_id]] attribute qualifier, will be 0. Setting 3 for the vertexCount parameter indicates that three vertices should be drawn, producing a single triangle. (That is, the vertex function is executed three times with values of 0, 1, and 2 for the vertexID parameter).

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

该调用是最后一次调用需要编码渲染命令为单个三角形.
当绘制完成以后, 这个渲染循环能够结束编码.提交到Command Buffer , 展示可绘画对象包含渲染的三角形.

接下来

在这个例子中,你将要学习如何使用Metal API 来渲染基本的几何体.

Basic Buffers 示例中, 你将要学习如何使用顶点缓冲区来改善你的渲染效率.

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

推荐阅读更多精彩内容