Metal入门学习:绘制渲染三角形

news2024/12/27 3:33:48
一、编程指南PDF下载链接(中英文档)
  • 1、Metal编程指南PDF链接
    https://github.com/dennie-lee/ios_tech_record/raw/main/Metal学习PDF/Metal 编程指南.pdf

  • 2、Metal着色语言(Metal Shader Language:简称MSL)编程指南PDF链接
    https://github.com/dennie-lee/ios_tech_record/raw/main/Metal学习PDF/Metal 着色语言指南.pdf

  • 3、补充:官网API文档链接
    https://developer.apple.com/documentation/metal/using_a_render_pipeline_to_render_primitives

二、内容前述

此示例展示如何配置渲染管线并将其用作渲染通道的一部分,以将简单的2D彩色三角形绘制到视图中。该示例为每个顶点提供位置和颜色,渲染管线使用该数据渲染三角形,在为三角形顶点指定的颜色之间插入颜色值。效果如下在这里插入图片描述

三、了解Metal渲染管线

渲染管线处理绘图命令并将数据写入渲染通道的目标。渲染管线有许多阶段,一些使用着色器编程,另一些具有固定或可配置的行为。此示例侧重于管道的三个主要阶段:顶点阶段、光栅化阶段和片段阶段。顶点阶段和片段阶段是可编程的,可以使用Metal着色语言(MSL)编写函数。光栅化阶段具有固定的行为,如下图所示:
在这里插入图片描述
渲染从绘图命令开始,其中包括顶点数和要渲染的图元类型。 例如,这是此示例中的绘图命令:

// Draw the triangle.
[renderEncoder drawPrimitives:MTLPrimitiveTypeTriangle
                  vertexStart:0
                  vertexCount:3];

顶点阶段为每个顶点提供数据。当处理了足够多的顶点后,渲染管线将图元光栅化,确定渲染目标中的哪些像素位于图元的边界内。 片段阶段确定要写入这些像素的渲染目标的值。

四、渲染管道处理数据

顶点函数为单个顶点生成数据,片段函数为单个片段生成数据,可以自定义它们的工作方式。在配置管道阶段时要牢记目标,想象希望管道生成什么以及它如何生成这些结果。决定将哪些数据传递到渲染管道以及将哪些数据传递到管道的后续阶段。通常在三个地方执行此操作:

管道的输入,由应用程序提供并传递到顶点阶段。
顶点阶段的输出,传递到光栅化阶段。
片段阶段的输入,由应用程序提供或由光栅化阶段生成。

在此示例中,管道的输入数据是顶点的位置及其颜色。演示了通常在顶点函数中执行的变换类型,输入坐标在自定义坐标空间中定义,以距视图中心的像素为单位进行测量。这些坐标需要转换为 Metal 的坐标系。

声明一个Vertex 结构,使用SIMD向量类型来保存位置和颜色数据。要共享结构在内存中的布局方式的单一定义,请在公共标头中声明结构并将其导入Metal着色器和应用程序。

//输入的顶点数据信息
typedef struct {
    //向量:顶点位置
    vector_float2 position;
    //向量:颜色值
    vector_float4 color;
} Vertex;

SIMD类型在Metal Shading Language中很常见,使用simd库也是正常。SIMD类型包含特定数据类型的多个通道,因此将位置声明为vector_float2意味着它包含两个32位浮点值(将保存x和y坐标)。颜色使用vector_float4存储,因为它们有四个通道:红色、绿色、蓝色和alpha。

在应用程序中,输入数据使用常量数组指定:

//三个顶点位置和颜色值
static const Vertex vertices[] = {
    //位置       //颜色值
    {{250,-250},{1.0,0.0,0.0,1.0}},
    {{-250,-250},{0.0,1.0,0.0,1.0}},
    {{0,250},   {0.0,0.0,1.0,1.0}},
};

顶点阶段为顶点生成数据,需要提供颜色和变换后的位置。再次使用SIMD类型声明一个包含位置和颜色值的RasterizerData结构。

struct RasterizerData{
    //[[position]]:在顶点着色函数中,表示当前的顶点信息,类型是float4、
    //还可以表示描述了片元的窗口的相对坐标(x,y,z,1/w),
    //即该像素点在屏幕上的位置信息
    //----[[position]]----请在着色函数编程指南文档查看解释
    float4 position [[position]];
    //颜色值
    float4 color;
};

输出位置(在下面详细描述)必须定义为vector_float4。颜色的声明与输入数据结构中的颜色相同。

因为Metal需要知道光栅化数据中的哪个字段提供位置数据,而Metal不会对结构中的字段强制执行任何特定的命名约定,所以使用[[position]]属性限定符注释位置字段以声明该字段保存输出位置。

片段函数只是将光栅化阶段的数据传递给后面的阶段,因此它不需要任何额外的参数。

五、定义顶点着色函数

声明顶点函数,包括它的输入参数和它输出的数据。就像使用kernel关键字声明计算函数一样,使用vertex关键字声明顶点函数。(kernel:可查看这篇文章https://blog.csdn.net/qqwyuli/article/details/130785820)

//[[vertex_id]] :顶点id标识符,并不由开发者传递
//属性修饰符"[[buffer(index)]]" 为着色函数参数设定了缓存的位置
vertex RasterizerData vertexShader(uint vertexID [[vertex_id]],
                                   constant Vertex* vertices [[buffer(VertexInputIndexVertices)]],
                                   constant vector_uint2 *viewPortSizePointer [[buffer(VertexInputIndexViewportSize)]])

第一个参数vertexID使用[[vertex_id]]属性限定符,Metal的一个关键字。执行渲染命令时,GPU会多次调用顶点函数,为每个顶点生成一个唯一值。

第二个参数 vertices 是一个包含顶点数据的数组,使用之前定义的Vertex结构。

要将位置转换为Metal的坐标,该函数需要绘制三角形的视口大小(以像素为单位),因此它存储在 viewportSizePointer 参数中。

第二个和第三个参数具有[[buffer(n)]]属性限定符。默认情况下,Metal会自动为每个参数分配参数表中的槽。当将[[buffer(n)]]限定符添加到缓冲区参数时,明确地告诉Metal要使用哪个插槽。显式声明插槽可以更轻松地修改着色器,而无需更改应用程序代码。在共享头文件中声明两个索引的常量,如下所示:

typedef enum AAPLVertexInputIndex
{
    VertexInputIndexVertices     = 0,
    VertexInputIndexViewportSize = 1,
} AAPLVertexInputIndex;

该函数的输出是一个 RasterizerData 结构。

六、编写顶点函数

顶点函数必须生成输出结构的两个字段。使用vertexID参数索引顶点数组并读取顶点的输入数据。此外,检索视口尺寸

float2 pixelSpaceXY = vertices[vertexID].position.xy;
    vector_float2 viewPortSize = vector_float2(*viewPortSizePointer);

顶点函数必须提供裁剪空间坐标中的位置数据,裁剪空间坐标是使用四维齐次向量(x,y,z,w)指定的3D点。光栅化阶段采用输出位置并将x、y和z坐标除以w,以在标准化设备坐标中生成3D点。规范化设备坐标与视口大小无关。
在这里插入图片描述
规范化设备坐标使用左手坐标系并映射到视口中的位置。基元被裁剪到这个坐标系中的一个盒子,然后光栅化。裁剪框的左下角位于(x,y)坐标(-1.0,-1.0),右上角位于 (1.0,1.0)。正z值指向远离相机的方向(进入屏幕)。z坐标的可见部分介于0.0(近裁剪平面)和1.0(远裁剪平面)之间。

将输入坐标系转换为归一化设备坐标系
在这里插入图片描述
因为这是一个二维应用,不需要齐次坐标,所以先给输出坐标写一个默认值,w值设置为1.0,其他坐标设置为0.0。这意味着坐标已经在规范化的设备坐标空间中,顶点函数应该在该坐标空间中生成(x,y)坐标。将输入位置除以视口大小的一半以生成规范化的设备坐标。由于此计算是使用SIMD类型执行的,因此可以使用一行代码同时划分两个通道。 进行除法,将结果放入输出位置的x和y通道

out.position = vector_float4(0,0,0,1.0);
out.position.xy = pixelSpaceXY / (viewPortSize / 2);

最后将颜色值复制到out.color返回值中

out.color = vertices[vertexID].color;

完整的顶点着色函数

//[[vertex_id]] :顶点id标识符,并不由开发者传递
//属性修饰符"[[buffer(index)]]" 为着色函数参数设定了缓存的位置
vertex RasterizerData vertexShader(uint vertexID [[vertex_id]],
                                   constant Vertex* vertices [[buffer(VertexInputIndexVertices)]],
                                   constant vector_uint2 *viewPortSizePointer [[buffer(VertexInputIndexViewportSize)]]){
    RasterizerData out;
    
    float2 pixelSpaceXY = vertices[vertexID].position.xy;
    vector_float2 viewPortSize = vector_float2(*viewPortSizePointer);
    
    out.position = vector_float4(0,0,0,1.0);
    out.position.xy = pixelSpaceXY / (viewPortSize / 2);
    
    out.color = vertices[vertexID].color;
    
    return out;
};
七、编写片段着色函数(片元着色函数)

片段是对呈现目标的可能更改。光栅化器确定渲染目标的哪些像素被基元覆盖。仅渲染像素中心位于三角形内的片段。
在这里插入图片描述
片段函数处理来自单个位置的光栅化器的传入信息,并计算每个渲染目标的输出值。这些片段值由管道中的后续阶段处理,最终写入渲染目标。
注意:
片段被称为可能更改的原因是片段阶段之后的管道阶段可以配置为拒绝某些片段或更改写入渲染目标的内容。在此示例中,片段阶段计算的所有值都按原样写入渲染目标。

此示例中的片段着色器接收与顶点着色器输出中声明的相同参数。使用fragment关键字声明片段函数。它采用单个参数,与顶点阶段提供的RasterizerData结构相同。添加 [[stage_in]]属性限定符以指示此参数由光栅器生成。
在这里插入图片描述
返回插值颜色作为函数的输出

return in.color;

完整的片元着色函数

fragment float4 fragmentShader(RasterizerData in [[stage_in]]){
    return in.color;
};
八、创建渲染Pipeline State对象

顶点函数和片元函数已经完成,可以创建一个使用它们的渲染管道。首先,获取默认库并为每个函数获取一个MTLFunction对象。

id<MTLLibrary> defaultLibrary = [_device newDefaultLibrary];
id<MTLFunction> vertexFunction = [defaultLibrary newFunctionWithName:@"vertexShader"];
id<MTLFunction> fragmentFunction = [defaultLibrary newFunctionWithName:@"fragmentShader"];

接下来,创建一个 MTLRenderPipelineState 对象。渲染管道有更多阶段需要配置,可以使用 MTLRenderPipelineDescriptor 来配置管道。

MTLRenderPipelineDescriptor *descriptor = [[MTLRenderPipelineDescriptor alloc] init];
descriptor.label = @"Simple pipeline";
descriptor.vertexFunction = vertexFunction;
descriptor.fragmentFunction = fragmentFunction;
descriptor.colorAttachments[0].pixelFormat = view.colorPixelFormat;
    
_pipelineState = [_device newRenderPipelineStateWithDescriptor:descriptor error:&error];

除了指定顶点和片段函数外,还声明了管道将绘制到的所有渲染目标的像素格式。像素格式(MTLPixelFormat)定义了像素数据的内存布局。对于简单格式,此定义包括每个像素的字节数、存储在像素中的数据通道数以及这些通道的位布局。由于此示例只有一个渲染目标并且由视图提供,因此将视图的像素格式复制到渲染管道描述符中。渲染管道状态必须使用与渲染通道指定的像素格式兼容的像素格式。在此示例中,渲染通道和管道状态对象都使用视图的像素格式,因此它们始终相同。

当 Metal 创建渲染管道状态对象时,管道被配置为将片段函数的输出转换为渲染目标的像素格式。如果要针对不同的像素格式,则需要创建不同的管道状态对象。可以在针对不同像素格式的多个管道中重复使用相同的着色器。

九、设置视口

现在有了管道的渲染管道状态对象,将要渲染三角形。可以使用渲染命令编码器来执行此操作。首先,设置视口,以便 Metal 知道要绘制到渲染目标的哪一部分。

[commandEncoder setViewport:(MTLViewport){0.0,0.0,_viewPortSize.x,_viewPortSize.y,0.0,1.0}];
十、设置渲染Pipeline State

为要使用的管道设置渲染管道状态。

[commandEncoder setRenderPipelineState:_pipelineState];
十一、将参数数据发送到顶点函数

通常使用缓冲区(MTLBuffer)将数据传递给着色器。然而,当只需要将少量数据传递给顶点函数时,就像这里的情况一样,将数据直接复制到命令缓冲区中。

该示例将两个参数的数据复制到命令缓冲区中。顶点数据是从示例中定义的数组中复制的。视口数据是从用于设置视口的同一变量复制而来的。

在此示例中,片段函数仅使用它从光栅器接收的数据,因此没有要设置的参数。

//设置顶点着色器参数
[commandEncoder setVertexBytes:vertices length:sizeof(vertices) atIndex:VertexInputIndexVertices];
[commandEncoder setVertexBytes:&_viewPortSize length:sizeof(_viewPortSize) atIndex:VertexInputIndexViewportSize];
十二、对绘图命令进行编码

指定图元的种类、起始索引和顶点数。渲染三角形时,将使用vertexID参数的值 0、1 和2调用顶点函数。

[commandEncoder drawPrimitives:MTLPrimitiveTypeTriangle vertexStart:0 vertexCount:3];

与使用Metal绘制到屏幕一样,结束编码过程并提交命令缓冲区。但是,可以使用同一组步骤对更多渲染命令进行编码。最终图像呈现为好像命令是按照指定的顺序处理的。(为了性能,允许 GPU 并行处理命令甚至部分命令,只要最终结果看起来是按顺序呈现的。)

十三、尝试颜色插值

在此示例中,颜色值被插值到整个三角形中 这通常是想要的效果,但有时希望一个值由一个顶点生成并在整个图元中保持不变。在顶点函数的输出上指定平面属性限定符来执行此操作。现在试试这个,在示例项目中找到RasterizerData的定义,并将[[flat]]限定符添加到其颜色字段。

float4 color [[flat]];

再次运行示例。渲染管线在整个三角形上统一使用第一个顶点(称为激发顶点)的颜色值,并忽略其他两个顶点的颜色。可以混合使用平面着色和插值,只需在顶点函数的输出中添加或省略平面限定符即可。金属着色语言规范定义了其他属性限定符,也可以使用它们来修改光栅化行为。

十四、完整代码

例子:github链接:https://github.com/dennie-lee/MetalDrawTriangleDemo

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/549656.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

chatgpt赋能Python-pythoncumsum

Python中的cumsum-累积求和函数 在数据处理中&#xff0c;经常需要对一个序列的元素进行累加。Python中提供了累积求和函数cumsum()&#xff0c;用于对一个序列的元素进行累加求和操作。 什么是cumsum()函数 cumsum()函数是Python中numpy模块中的一个函数&#xff0c;用于对…

通过小米万兆路由器将小米SoundMove 无缝接入 ChatGPT

通过小米万兆路由器将小米SoundMove 无缝接入 ChatGPT 本教程内容参考 Github 地址(可选)部署查看小米 SoundMove 信息的环境(可选)查看小米 SoundMove 的信息以容器方式部署程序到小米万兆路由器实际效果有待改善点 本教程内容 1 是记录了将小米 SoundMove 接入 ChatGPT 的操…

面向《海贼王》领域数据的知识图谱项目

访问【WRITE-BUG数字空间】_[内附完整源码和文档] 本次任务试图为《海贼王》中出现的各个实体&#xff0c;包括人物、地点、组织等&#xff0c;构建一个知识图谱&#xff0c;帮助我们更好的理解这部作品。 项目内容包括数据采集、知识存储、知识抽取、知识计算、知识应用五大部…

【运动规划算法项目实战】如何使用Pure Pursuit算法进行路径跟踪(附ROS C++代码)

文章目录 前言一、简介二、Pure Pursuit算法优缺点三、 代码实现3.1 算法实现步骤3.2 pure_pursuit.h3.3 pure_pursuit.cpp3.4 cubic_spline_path.py3.5 节点连接关系3.6 RVIZ显示四、总结前言 在自动驾驶和机器人导航领域,路径跟踪是一项关键技术,它使车辆或机器人能够按照…

多线程排序法

多线程排序法 chatGPT给我改的多线程排序法 using System.Collections.Concurrent; using System.Threading; ConcurrentBag<int> sortedList new ConcurrentBag<int>(); void Sort() { int[] arr {2, 6, 12, 8}; List<Thread> threads new List<Threa…

chatgpt赋能Python-pythondone

PythonDone&#xff1a;将Python编程变得更加简单 介绍 Python是一种有着广泛应用的高级编程语言&#xff0c;由于其简洁易学、开发效率高、可移植性好等特点&#xff0c;成为业内最热门的技术之一。但是&#xff0c;对于一些初学者来说&#xff0c;Python的学习过程可能还是…

【论文分享|SIGMOD‘22】WeTune 自动发现和验证重写规则

作者&#xff1a;谢其骏 北京航空航天大学在读硕士&#xff0c; Databend 研发工程师实习生 https://github.com/jun0315 论文原文&#xff1a; Zhaoguo Wang, Zhou Zhou, Yicun Yang, Haoran Ding, Gansen Hu, Ding Ding, Chuzhe Tang, Haibo Chen, Jinyang Li. WeTune: Auto…

【AIGC】11、MDETR | LeCun 团队于 2021 年推出的端到端多模态理解模型

文章目录 一、背景二、方法2.1 DETR2.2 MDETR 三、效果3.1 预训练调整后的检测器3.2 下游任务 论文&#xff1a;MDETR - Modulated Detection for End-to-End Multi-Modal Understanding 代码&#xff1a;https://github.com/ashkamath/mdetr 出处&#xff1a;ICCV 2021 Oral…

chatgpt赋能Python-pythonctrl快捷键

PythonCtrl快捷键使用指南 作为一名有10年Python编程经验的工程师&#xff0c;我深知PythonCtrl快捷键的重要性。PythonCtrl作为一个Python的开源编辑器&#xff0c;在每一个版本中都加入了更多的功能和快捷键&#xff0c;使得Python编程更加高效和易用。在本篇文章中&#xf…

卡方分布分析与应用

卡方检验(chi-square&#xff0c;记为χ2检验)是统计学中常用来计数数据分析的方法&#xff0c;对于总体的分布不作任何假设&#xff0c;因此它属于非参数检验法中的一种。本博文从理论到实际应用去阐述卡方检验&#xff0c;最后用python语言去实现卡方分布的代码。 1. 卡方分…

Spring Security的基本组件

一.简介 Spring Security通过一些列的过滤器完成了用户身份认证及其授权工作&#xff0c;每个过滤器都有不同分工&#xff0c;当然这些过滤器并不是全部都一起工作&#xff0c;而是根据我们需要什么功能&#xff0c;才会选取对应的过滤器加入。 当然这些过滤器并不是直接加入…

Linux 终端特殊符号含义大全

Linux特殊符号使用及含义 总结 Linux 终端中有许多特殊符号&#xff0c;本文对常用的进行了总结&#xff1a; $ 表示变量/普通终端用户&#xff1a;用于引用变量的值/表示终端中的普通用户。# 表示注释/超级用户&#xff1a;用于在脚本中注释代码/表示终端中的超级用户。/ &…

Doxygen 源码分析: QCString类

2023-05-20 23:41:56 ChrisZZ imzhuofoxmailcom Hompage https://github.com/zchrissirhcz 文章目录 1. Doxygen 版本2. QCString 类概览3. QCString 特殊成员函数3.1 default 方式的构造函数3.2 单个参数和两个参数的构造函数 4. inline方式实现的成员函数4.1 operator 函数4.…

chatgpt赋能Python-pythonguanwang

Python官网SEO分析 Python是一种高级编程语言&#xff0c;被广泛应用于Web开发、数据科学、人工智能、机器学习等领域。Python官网&#xff08;https://www.python.org&#xff09;是Python语言的官方网站&#xff0c;为Python用户和开发者提供了最新的Python解释器、文档、库…

中文Python(5)中文Python的while条件循语句

中文Python&#xff08;5&#xff09;中文Python的while条件循语句 Python是一种流行的编程语言&#xff0c;其简单而直观的语法吸引了很多人学习和使用。中文Python则是针对中文用户开发的一种版本。中文Python原先为了给不懂编写程序的人写量化程序&#xff0c;我们开发了中…

代码随想录算法训练营 Day 43 | 1049.最后一块石头的重量 II,494.目标和,474.一和零

1049.最后一块石头的重量 II 讲解链接&#xff1a;代码随想录-1049.最后一块石头的重量 II 确定 dp 数组以及下标的含义&#xff1a;dp[j]表示容量&#xff08;这里说容量更形象&#xff0c;其实就是重量&#xff09;为 j 的背包&#xff0c;最多可以背最大重量为 dp[j]。 石…

XPath语法:在XML文档中定位和选择节点的利器

XPath&#xff08;XML Path Language&#xff09;是一种用于在XML文档中定位和选择节点的语言。它提供了强大的定位和选择能力&#xff0c;使开发人员能够准确、灵活地定位所需的元素。本篇博客将介绍XPath的语法和常用定位方法&#xff0c;帮助你在Web自动化测试等场景中更好地…

Glob 文件匹配

前言 glob本质是Unix shell 风格的路径匹配规则。 该规则后续被其它语言支持。 ?&#xff1a;匹配一个任意字符 *&#xff1a;匹配任意个任意字符 [sequence]&#xff1a;匹配出现在sequence里面的一个字符 [!sequence]&#xff1a;匹配没有出现在sequence里面的一个字符 [a…

【解决】CSS下拉菜单不会显示的问题

导航栏的下拉菜单显示&#xff0c;但按 F5 刷新的一瞬间又能看见 下拉菜单的内容&#xff0c;但就是不会显示出来&#xff0c;一开始以为是 js 代码写错或者 css 的动画函数的影响&#xff0c;后面找到一篇博客&#xff0c;说这是老生常谈的问题&#xff0c;对于小白确实很难找…

移动应用数据安全性:如何防止应用程序被黑客攻击和数据泄露?

第一章&#xff1a;引言 在移动应用成为人们生活中不可或缺的一部分的今天&#xff0c;数据安全性已经成为一个非常重要的问题。随着黑客攻击和数据泄露事件的频繁发生&#xff0c;用户对于移动应用程序的信任度也在逐渐下降。本文将探讨移动应用数据安全性的重要性&#xff0…