一、流程
为了让GPU代表你执行工作,你向它发送命令。命令执行绘图、并行计算或应用程序所需的资源管理工作。Metal应用程序和GPU之间的关系是一种客户机-服务器模式:
你的Metal应用程序就是客户端。
GPU是服务器。
你通过向GPU发送命令来发出请求。
在处理命令后,GPU可以通知你的应用当它准备好做更多的工作。
下图使用Metal时的客户机-服务器使用模式。
要向GPU发送命令,你可以使用命令编码器对象将它们添加到命令缓冲区(command buffer)中。您将命令缓冲区添加到命令队列(command queue),然后在准备好让Metal执行命令缓冲区的命令时提交命令缓冲区。在命令缓冲区中放置命令、进入队列和提交命令缓冲区的顺序非常重要,因为它影响Metal承诺执行命令的可感知顺序。
您在初始化时创建一些metal对象,通常会无限期地保留它们。这些对象是命令队列和管道对象。您只创建一次,因为设置它们的成本很高,但一旦初始化,它们就可以快速重用。
Make a Command Queue
commandQueue = device.makeCommandQueue()
创建一个或多个管道对象
管道对象告诉Metal如何处理命令。管道对象封装了用Metal阴影语言编写的函数。以下是管道如何适合你的金属工作流程:
编写Metal着色器函数来处理数据。
创建一个包含着色器的管道对象。
当您准备使用它时,请启用管道。
绘制,计算,或blit调用。
Metal不会立即执行你的draw、compute或blit调用;相反,您可以使用一个encoder对象来插入将这些调用封装到命令缓冲区中的命令。提交命令缓冲区后,Metal将其发送给GPU,并使用活动管道对象来处理其命令。
向GPU发出命令命令队列和管道设置好后,是时候向GPU发出命令了。以下是你要遵循的过程:1、创建一个命令缓冲区。2、用命令填充缓冲区。3、将命令缓冲区提交给GPU。
如果将动画作为渲染循环的一部分执行,则需要对动画的每一帧执行此操作。您还可以按照这个过程执行一次性图像处理或机器学习任务。
向命令缓冲区添加命令
当您在类似于draw、compute或blit操作的encoder对象上调用特定于任务的函数时,编码器将与这些调用对应的命令放置在命令缓冲区中。编码器对命令进行编码,包括GPU在运行时处理任务所需的所有内容。下图显示了工作流程:
You encode actual commands with concrete subclasses of MTLCommandEncoder, depending on your task:
Use MTLRenderCommandEncoder to issue render commands.
Use MTLComputeCommandEncoder to issue parallel computation commands.
Use MTLBlitCommandEncoder to issue resource management commands.
See Using a Render Pipeline to Render Primitives for a complete rendering example. See Processing a Texture in a Compute Function for a complete parallel processing example.
Commit a Command Buffer
1、视图管理
MTKView:Metal为我们提供的是MTKView,继承自UIView,用于处理metal绘制并显示到屏幕过程中的细节
MTKView*view=[[MTKView alloc]init];
2、MTLDevice(MTLDevice对象表示可以执行命令的GPU。MTLDevice协议具有:创建新命令队列、 从内存中分配缓冲区、 创建纹理、 查询设备功能的方法,一个MTLDevice对象就代表这着一个GPU)
MTLCreateSystemDefaultDevice()来获取代表默认的GPU单个对象
//创建一个默认的deviceview.device=MTLCreateSystemDefaultDevice();
if(!view.device){
NSLog(@"Metal is not supported on this device");
return;
}
3、MTLCommandQueue
在获取了GPU之后,还需要一个渲染队列MTLCommandQueue,这个队列是与GPU交互的第一个对象,队列MTLCommandQueue中存储的是将要进行渲染的命令MTLCommandBuffer每个命令队列的生命周期很长,因此commandQueue可以重复使用,而不是频繁创建和销毁
//通过 MTLDevice 创建 MTLCommandQueue
id<MTLCommandQueue>commandQueue=[view.device newCommandQueue];
4、MTLCommandBuffer
命令缓冲区主要是用于存储编码的命令,其生命周期是直到缓冲区被提交到GPU执行为止,单个的命令缓冲区可以包含不同的编码命令,主要取决于用于构建它的编码器的类型和数量。
//通过 MTLCommandQueue 创建 MTLCommandBuffer
id<MTLCommandBuffer>commandBuffer=[commandQueue commandBuffer];
//给commandBuffer起个名字
commandBuffer.label=@"MyCommand";
MTLCommandBuffer对象的提交,是提交到MTLCommandQueue对象中的。只有在提交后开始执行,通过入队顺序执行。有两种执行方式:
enqueue : 顺序执行
commit : 插队尽快执行,如果前面有commit还是需要排队等着
5、MTLRenderCommandEncoder
命令编码器表示单个渲染过程中相关联的渲染状态和渲染命令,有以下功能:
指定图形资源,例如缓存区和纹理对象,其中包含顶点、片元、纹理图片数据
指定一个MTLRenderPipelineState对象,表示编译的渲染状态,包含顶点着色器和片元着色器的编译&链接情况
指定固定功能,包括视口、三角形填充模式、剪刀矩形、深度、模板测试以及其他值
绘制3D图元
MTLRenderCommandEncoder的创建,需要渲染描述符MTLRenderPassDescriptor
//1.从视图绘制中,获得渲染描述符MTLRenderPassDescriptor*renderPassDescriptor=view.currentRenderPassDescriptor;
//2.判断renderPassDescriptor 渲染描述符是否创建成功,否则则跳过任何渲染.
if(renderPassDescriptor!=nil){
//3.创建MTLRenderCommandEncoder 对象
id<MTLRenderCommandEncoder>renderEncoder=[commandBuffer renderCommandEncoderWithDescriptor:renderPassDescriptor];
//4.给Encoder命名
renderEncoder.label=@"MyRenderEncoder";
//5.一些Metal文件的绘制操作//
下面讲解绘制操作(渲染管道渲染基元)
//6.结束工作
[renderEncoder endEncoding]
;}
6、MTKViewDelegate
//设置MTKView 的代理(由自定义的CustomRender来实现MTKView 的代理方法)
view.delegate=render;
//视图可以根据视图属性上设置帧速率(指定时间来调用drawInMTKView方法--视图需要渲染时调用)也就是每60帧刷新一次屏幕
view.preferredFramesPerSecond=60;
//每当视图需要渲染时调用
-(void)drawInMTKView:(nonnull MTKView*)view;
//当MTKView视图发生大小改变时,或者重新布局时调用
-(void)mtkView:(nonnull MTKView*)view drawableSizeWillChange:(CGSize)size;
二、 渲染管道Metal Render Pipeline
创建一个管线状态描述符对象,它是管线状态的工厂类,用于描述管线的一些属性,顶点函数、片元函数、像素格式、采样计数等
一个渲染管线流程绘图命令和数据写入到一个渲染通道的目标。渲染管道有许多阶段,一些使用着着色器编程,另一些使用固定或可配置的行为编程
CPU部分: 处理顶点数据,传给顶点程序(着色器)
GPU部分:顶点着色器处理CPU传过来的顶点数据,进行一系列坐标转换、裁剪
进行图元装配
光栅化
片元程序(着色器)去处理纹理、透明度、深度等
把最终数据存储到帧缓冲区,并显示到屏幕上
1、是cpu提供给GPU的顶点信息,包括了顶点的位置、颜色(只是顶点的颜色,和纹理的颜色无关)、纹理坐标(用于纹理贴图)等顶点信息。
2、(顶点着色器)
顶点着色器是处理VBO/VAO提供的顶点信息的程序。VBO/VAO提供的每个顶点都执行一遍顶点着色器。Uniforms(一种变量类型)在每个顶点保持一致,Attribute每个顶点都不同(可以理解为输入顶点属性)。执行一次VertexShader输出一个Varying(可变变量)和gl_positon。
顶点着色器的输入包括:
着色器程序:描述顶点上执行操作的顶点着色器程序源代码或者可执行文件
顶点着色器输入(或者属性):用顶点数组提供的每个顶点的数据
统一变量(uniform):顶点/片段 着色器使用的不变数据
采样器(Samplers):代表顶点着色器使用纹理的特殊统一变量类型
重点:VertexShader就是顶点着色器编程可以操作的阶段,用于控制顶点坐标的转换过程,片段着色器控制着每个像素颜色的计算过程
3.Primitive Assembly(图元装配):
顶点着色器下一个阶段是图元装配,图元(prmitive)是三角形、直线或者点精灵等几何对象。这个阶段,把顶点着色器输出的顶点组合成图元。
将顶点数据根据Primitive(原始链接关系)还原出网格结构,网格由顶点和索引组成,在此阶段根据索引将顶点链接在一起,组成点、线、面三种不同的图元,之后就是对超出屏幕的三角形进行剪裁,如果三角形ABC三个顶点其中一个点在屏幕的外面,另外两个点在屏幕里面,其实屏幕上看到的应该是个四边形,然后再将这个四边形切成2个小的三角形
简而言之,将顶点着色器计算之后得到的点根据链接关系组成点、线、面(三角形)
需要注意的是:
OpenGL ES的图元连接方式有9种:点、线、线段、线环、四边形、四边形带、三角形、三角形带、三角形扇
而Metal只有5种:点、线段、线环、三角形、三角形扇
4.rasterization(光栅化):
光栅化是将图元转化为一组二维片段的过程,然后,这些片段由片段着色器处理(片段着色器的输入)。这些二维片段代表着可在屏幕上绘制的像素。用于从分配给每个图元顶点的顶点着色器输出生成每个片段值的机制称作插值(Interpolation)。这句不是人话的话解释了一个问题,就是从cpu提供的分散的顶点信息是如何变成屏幕上密集的像素的,图元装配后顶点可以理解成变为图形,光栅化时可以根据图形的形状,插值出那个图形区域的像素(纹理坐标v_texCoord、颜色等信息)。注意,此时的像素并不是屏幕上的像素,是不带有颜色的。接下来的片段着色器完成上色的工作。
5.FragmentShader(片段着色器):
片段着色器为片段(像素)上的操作实现了通用的可编程方法,光栅化输出的每个片段都执行一遍片段着色器,对光栅化阶段生成每个片段执行这个着色器,生成一个或多个(多重渲染)颜色值作为输出。
6.Per-Fragment Operations(逐片段操作)
(1)pixelOwnershipTest(像素归属测试):
这个用来确定帧缓冲区中位置(x,y)的像素是不是归当前上下文所有。例如,如果一个显示帧缓冲区窗口被另一个窗口所遮蔽,则窗口系统可以确定被遮蔽的像素不属于此opengl的上下文,从而不显示这些像素。
(2)ScissorTest(剪裁测试):
如果该片段位于剪裁区域外,则被抛弃
(3)StencilTest and DepthTest(模板和深度测试):
深度测试比较好理解,若片段着色器返回的深度小于缓冲区中的深度,则舍弃。模板测试没有用过,不清楚具体功能,猜测功能应该和名字一样,模板形状内可通过。
(4)Blending(混合):
将新生成的片段颜色值与保存在帧缓冲区的颜色值组合起来,产生新的RGBA。
(5)dithering(抖动):
在逐片段操作阶段的最后,片段要么被拒绝,要么在帧缓冲区(x,y)的某个位置写入片段的颜色,深度或者模板值。写入片段颜色,深度和模板值取决于弃用的相应写入掩码。写入掩码可以更精确的控制写入相关缓冲区的颜色、深度和模板值。例如:可以设置颜色缓冲区的写入掩码,使得任何红色值都不能被写入颜色缓冲区。