本案例的目的在于理解Metal中使用着色器绘制三角形的流程
整体效果图如下
效果图
整体的流程图如下
整体流程
与Metal 入门级01:颜色的渲染加载相比,viewDidLoad函数基本没变化,主要变化的是initWithMetalKitView
函数和drawInMTKView
代理方法
- initWithMetalKitView函数:增加了metal着色器文件初始化的准备工作
-
drawInMTKView
代理方法:增加了metal绘制三角形的流程
准备工作
创建metal文件
-
command + N --> Metal File 创建metal着色器文件
创建metal文件
- 定义顶点着色器输入和片元着色器输入,相当于OpenGL ES中的
varying
修饰的变量,即桥接变量
//顶点着色器输出和片元着色器输入(相当于OpenGL ES中的varying修饰的变量,即桥接)
typedef struct
{
// 处理空间的顶点信息,相当于OpenGL ES中的gl_Position
// float4 修饰符,是一个4维向量
float4 clipSpacePosition [[position]];
// 颜色,相当于OpenGL ES中的gl_FragColor
float4 color;
}RasterizerData;
- 定义顶点着色器函数 和 片元着色器函数
//顶点着色器函数
/*
vertex:修饰符,表示是顶点着色器
RasterizerData:返回值
vertexShader:函数名称,可自定义
vertexID:metal自己反馈的id
vertices:1)告诉存储的位置buffer 2)告诉传递数据的入口是CJLVertexInputIndexVertices
vertices 和 viewportSizePointer 都是通过CJLRenderer 传递进来的
*/
vertex RasterizerData
vertexShader(uint vertexID [[vertex_id]],
constant CJLVertex *vertices [[buffer(CJLVertexInputIndexVertices)]],
constant vector_uint2 *viewportSizePointer [[buffer(CJLVertexInputIndexViewportSize)]])
{
}
/*
fragment:修饰符,表示是片元着色器
float4:返回值,即颜色值RGBA
fragmentShader:函数名称,可自定义
RasterizerData:参数类型(可修改)
in:形参变量(可修改)
[[stage_in]]:属性修饰符,表示单个片元输入(由定点函数输出)(不可修改),相当于OpenGL ES中的varying
*/
fragment float4 fragmentShader(RasterizerData in [[stage_in]])
{
}
创建C 与 OC的桥接文件
该头文件的目的是为了c代码与OC代码可以共享与 shader 和 C 代码 为了确保Metal Shader缓存区索引能够匹配 Metal API Buffer 设置的集合调用
- 定义缓存区索引值,表示向metal着色器传递数据的入口枚举值,相当于OpenGL ES中GLSL语言定义的顶点坐标入口
position
typedef enum CJLVertexInputIndex
{
// 顶点
CJLVertexInputIndexVertices = 0,
// 视图大小
CJLVertexInputIndexViewportSize = 1,
}CJLVertexInputIndex;
- 定义图形数据的结构体,包含顶点和颜色值,类似于OpenGL ES中的顶点数据的结构体
//结构体:顶点/颜色值
typedef struct
{
// 像素空间的位置
// 像素中心点(100,100)
vector_float4 position;
// RGBA颜色
vector_float4 color;
}CJLVertex;
viewDidLoad函数
这个函数的流程如下
viewDidLoad函数流程
与Metal 入门级01:颜色的渲染加载相比,只是多了一个设置视口大小,会触发MTKViewDelegate
协议的drawableSizeWillChange
方法,这里不做过多说明
渲染循环类
渲染循环类CJLRenderer
是服务于MTKView
的,用于管理view的渲染以及view的代理方法的回调
initWithMetalKitView函数
这部分的代码都是绘制前的准备工作,流程图如下
initWithMetalKitView函数流程
主要分为是三步
- 设置device
- metal着色器文件加载
- 设置命令队列
其中第一步和第三步在上个案例均有提及,这里不再说明,主要说说metal着色器文件的加载
metal着色器文件加载
根据上图所示,可以划分为以下几步
- 加载metal文件
从bundle中获取metal后缀的着色器,并获取顶点着色器和片元着色器所对应的函数,其中函数的获取需要通过MTLLibrary
对象根据对应的函数名称获取
//2.在项目中加载所有的(.metal)着色器文件
// 从bundle中获取.metal文件
id<MTLLibrary> defaultLibrary = [_device newDefaultLibrary];
//从库中加载顶点函数
id<MTLFunction> vertexFunction = [defaultLibrary newFunctionWithName:@"vertexShader"];
//从库中加载片元函数
id<MTLFunction> fragmentFunction = [defaultLibrary newFunctionWithName:@"fragmentShader"];
- 配置渲染管道
通过MTLRenderPipelineDescriptor
初始化渲染管道,并设置管管名称、顶点 & 片元着色器对应的函数以及存储颜色数据的组件
//3.配置用于创建管道状态的管道,即渲染管道
MTLRenderPipelineDescriptor *pipelineStateDescriptor = [[MTLRenderPipelineDescriptor alloc] init];
//管道名称
pipelineStateDescriptor.label = @"Simple Pipeline";
//可编程函数,用于处理渲染过程中的各个顶点
pipelineStateDescriptor.vertexFunction = vertexFunction;
//可编程函数,用于处理渲染过程中各个片段/片元
pipelineStateDescriptor.fragmentFunction = fragmentFunction;
//一组存储颜色数据的组件
pipelineStateDescriptor.colorAttachments[0].pixelFormat = mtkView.colorPixelFormat;
- 创建渲染管线状态对象
通过device和渲染管道创建渲染管线状态的对象,并判断是否创建成功
//4.同步创建并返回渲染管线状态对象
_pipelineState = [_device newRenderPipelineStateWithDescriptor:pipelineStateDescriptor error:&error];
//判断是否返回了管线状态对象
if (!_pipelineState) {
//如果我们没有正确设置管道描述符,则管道状态创建可能失败
NSLog(@"Failed to created pipeline state, error %@", error);
return nil;
}
drawInMTKView代理方法
当view和render以及视口初始化后,MTKView对象view中默认的帧速率是60,与屏幕刷新的帧率是一致的,所以会随着屏幕刷新的帧率,不停的调用该绘制的代理方法
默认的帧速率
该方法整体的流程如下
drawInMTKView代理方法流程
相比颜色渲染的案例,多了metal绘制三角形的流程,下面主要说下该流程,三角形的绘制时在创建commandEncoder对象
之后,结束commandEncoder工作
之前完成的,且在绘制之前,需要在创建commandBuffer之前
创建三角形的顶点和颜色数据
// 1. 顶点数据、颜色数据
static const CJLVertex triangleVertices[] =
{
//顶点, RGBA颜色值
{ { 0.5, -0.25, 0.0, 1.0 }, { 1, 0, 0, 1 } },
{ { -0.5, -0.25, 0.0, 1.0 }, { 0, 1, 0, 1 } },
{ { -0.0f, 0.25, 0.0, 1.0 }, { 0, 0, 1, 1 } },
};
三角形的绘制主要分为以下几步:
- 设置可绘制的区域,即设置视口,相当于OpenGL ES中的
glViewPort
-
通过MTLViewport创建视口对象
视口主要用于指定Metal渲染内容的drawable区域。 视口是具有x和y偏移,宽度和高度以及近和远平面的3D区域 -
为管道分配自定义视口需要通过调用
setViewport
方法将MTLViewport结构编码为渲染命令编码器。 如果未指定视口,Metal会设置一个默认视口,其大小与用于创建渲染命令编码器的drawable相同。
-
MTLViewport viewPort = {
0.0, 0.0, _viewportSize.x, _viewportSize.y, -1.0, 1.0
};
//设置视口 相当于OpenGL ES中的glViewPort
[renderEncoder setViewport:viewPort];
//6.设置当前渲染管道状态对象
[renderEncoder setRenderPipelineState:_pipelineState];
- 传递数据
从app的OC代码中传递数据到Metal 顶点着色器/片元着色器 函数,,相当于OpenGL ES中的glVertexAttribPointer,目前需要传递的数据有两种- 顶点+颜色数据
- 视图大小平数据
// 1) 指向要传递给着色器的内存的指针
// 2) 我们想要传递的数据的内存大小
// 3)一个整数索引,它对应于我们的“vertexShader”函数中的缓冲区属性限定符的索引。
// CJLVertexInputIndexVertices 是顶点数据的入口,需要由自己定义,相当于OpenGL ES中glGetAttribLocation
[renderEncoder setVertexBytes:triangleVertices
length:sizeof(triangleVertices)
atIndex:CJLVertexInputIndexVertices];
//viewPortSize 数据
//1) 发送到顶点着色函数中,视图大小
//2) 视图大小内存空间大小
//3) 对应的索引
[renderEncoder setVertexBytes:&_viewportSize
length:sizeof(_viewportSize)
atIndex:CJLVertexInputIndexViewportSize];
- 绘制三角形,相当于OpenGL ES中的
glDrawArrays
metal中的图元有5种,OpenGL ES中则是有9种,metal的图元类型如下表所示
图元类型 | 说明 |
---|---|
MTLPrimitiveTypePoint = 0 | 点 |
MTLPrimitiveTypeLine = 1 | 线 |
MTLPrimitiveTypeLineStrip = 2 | 线环 |
MTLPrimitiveTypeTriangle = 3 | 三角形 |
MTLPrimitiveTypeTriangleStrip = 4 | 三角形扇 |
// @method drawPrimitives:vertexStart:vertexCount:
//@brief 在不使用索引列表的情况下,绘制图元
//@param 绘制图形组装的基元类型
//@param 从哪个位置数据开始绘制,一般为0
//@param 每个图元的顶点个数,绘制的图型顶点数量
[renderEncoder drawPrimitives:MTLPrimitiveTypeTriangle
vertexStart:0
vertexCount:3];
以上就是三角形的绘制步骤
完善metal着色器函数的代码
这部分使用的metal独有的语法,类似于OpenGL ES中的GLSL语言
其中metal中的图形渲染管道的关系如下所示:顶点+颜色数据传入顶点着色器,顶点着色器处理顶点,中间经过metal自行完成的图元装配和光栅化,将处理后的数据传入片元着色器进行处理
metal管道渲染流程
顶点着色器函数
主要有两部分处理
- 顶点:执行坐标系转换,将生成的顶点剪辑空间写入到返回值中.
- 颜色:将顶点颜色值传递给返回值
vertex RasterizerData
vertexShader(uint vertexID [[vertex_id]],
constant CJLVertex *vertices [[buffer(CJLVertexInputIndexVertices)]],
constant vector_uint2 *viewportSizePointer [[buffer(CJLVertexInputIndexViewportSize)]])
{
//1、定义out
RasterizerData out;
// 2、没有旋转等变换,原样输出
//每个顶点着色器的输出位置在剪辑空间中(也称为归一化设备坐标空间,NDC),剪辑空间中的(-1,-1)表示视口的左下角,而(1,1)表示视口的右上角.
out.clipSpacePosition = vertices[vertexID].position;
//把我们输入的颜色直接赋值给输出颜色. 这个值将于构成三角形的顶点的其他颜色值插值,从而为我们片段着色器中的每个片段生成颜色值.
out.color = vertices[vertexID].color;
//完成! 将结构体传递到管道中下一个阶段:
return out;
}
片元着色器函数
原样输出颜色值
//当顶点函数执行3次,三角形的每个顶点执行一次后,则执行管道中的下一个阶段.栅格化/光栅化.
/*
metal自行完成的过程
1)图元装配
2)光栅化
*/
//片元着色器函数:描述片元函数
fragment float4 fragmentShader(RasterizerData in [[stage_in]])
{
//返回输入的片元颜色
return in.color;
}
完整的代码见Github - 17_Metal_三角形_OC
网友评论