准备工作
- Metal渲染的构建流程, 请参考:Metal之简单渲染动态切换屏幕颜色
- Metal三角形的渲染显示与渲染构建流程大体一致, 本文主要介绍以下方面的修改和实现:
① metal渲染文件
② 创建C 与 OC 的桥接函数
③ initWithMetalKitView方法中需要加载metal文件
④ drawInMTKView方法中加载三角形数据 - Metal三角形的渲染显示是基于世界坐标系下。
渲染流程
一、metal渲染文件
- command + N --> Metal File 创建metal着色器文件, 新建metal文件:YDWShaders.metal
- 定义顶点着色器输入和片元着色器输入,相当于OpenGL ES中的varying修饰的变量,即桥接变量;
// 顶点着色器输出和片段着色器输入
// 结构体
typedef struct {
// 处理空间的顶点信息
float4 clipSpacePosition [[position]];
// 颜色
float4 color;
} RasterizerData;
- 定义顶点着色器函数和片元着色器函数:
① 处理顶点数据: 执行坐标系转换,将生成的顶点剪辑空间写入到返回值中; 将顶点颜色值传递给返回值;
② 初始化输出剪辑空间位置: 索引到我们的数组位置以获得当前顶点, 我们的位置是在像素维度中指定的, 每个顶点着色器的输出位置在剪辑空间中(也称为归一化设备坐标空间,NDC),剪辑空间中的(-1,-1)表示视口的左下角,而(1,1)表示视口的右上角, 计算和写入 XY值到我们的剪辑空间的位置.为了从像素空间中的位置转换到剪辑空间的位置,我们将像素坐标除以视口的大小的一半;
③ 把输入的颜色直接赋值给输出颜色, 这个值将于构成三角形的顶点的其他颜色值插值, 从而为片段着色器中的每个片段生成颜色值;
//顶点着色器函数
/*
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)]]) {
// 定义out
RasterizerData out;
// 初始化输出剪辑空间位置
out.clipSpacePosition = vertices[vertexID].position;
// 把输入的颜色直接赋值给输出颜色. 这个值将于构成三角形的顶点的其他颜色值插值,从而为片段着色器中的每个片段生成颜色值
out.color = vertices[vertexID].color;
// 完成,将结构体传递到管道中下一个阶段
return out;
}
/*
fragment:修饰符,表示是片元着色器
float4:返回值,即颜色值RGBA
fragmentShader:函数名称,可自定义
RasterizerData:参数类型(可修改)
in:形参变量(可修改)
[[stage_in]]:属性修饰符,表示单个片元输入(由定点函数输出)(不可修改),相当于OpenGL ES中的varying
*/
fragment float4 fragmentShader(RasterizerData in [[stage_in]]) {
// 返回输入的片元颜色
return in.color;
}
二、创建C与OC的桥接文件
该头文件的目的是为了c代码与OC代码可以共享与 shader 和 C 代码 为了确保Metal Shader缓存区索引能够匹配 Metal API Buffer 设置的集合调用
- 定义缓存区索引值,表示向metal着色器传递数据的入口枚举值,相当于OpenGL ES中GLSL语言定义的顶点坐标入口position
// 缓存区索引值 共享与shader和C代码, 为了确保Metal Shader缓存区索引能够匹配 Metal API Buffer 设置的集合调用
typedef enum YDWVertexInputIndex {
// 顶点
YDWVertexInputIndexVertices = 0,
// 视图大小
YDWVertexInputIndexViewportSize = 1,
} YDWVertexInputIndex;
- 定义图形数据的结构体,包含顶点和颜色值,类似于OpenGL ES中的顶点数据的结构体
// 结构体: 顶点/颜色值
typedef struct {
// 像素空间的位置
// 像素中心点(100,100)
vector_float4 position;
// RGBA颜色
vector_float4 color;
} YDWVertex;
三、initWithMetalKitView方法中需要加载metal文件
- 获取GPU设备device
- 加载.metal着色器文件
- 配置用于创建管道状态的管道描述符
- 同步创建并返回渲染管线状态对象
- 创建命令队列
// 初始化MTKView
- (nonnull instancetype)initWithMetalKitView:(nonnull MTKView *)mtkView {
self = [super init];
if(self) {
NSError *error = NULL;
// 获取GPU 设备
_device = mtkView.device;
// 在项目中加载所有的(.metal)着色器文件
// 从bundle中获取.metal文件
id<MTLLibrary> defaultLibrary = [_device newDefaultLibrary];
// 从库中加载顶点函数
id<MTLFunction> vertexFunction = [defaultLibrary newFunctionWithName:@"vertexShader"];
// 从库中加载片元函数
id<MTLFunction> fragmentFunction = [defaultLibrary newFunctionWithName:@"fragmentShader"];
// 配置用于创建管道状态的管道
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];
// 判断是否返回了管线状态对象
if (!_pipelineState) {
// 如果没有正确设置管道描述符,则管道状态创建可能失败
NSLog(@"Failed to created pipeline state, error %@", error);
return nil;
}
// 创建命令队列
_commandQueue = [_device newCommandQueue];
}
return self;
}
四、drawInMTKView方法中加载三角形数据
- 顶点数据/颜色数据
// 顶点数据/颜色数据
static const YDWVertex 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 } },
};
- 为当前渲染的每个渲染传递创建一个新的命令缓冲区
// 为当前渲染的每个渲染传递创建一个新的命令缓冲区
id<MTLCommandBuffer> commandBuffer = [_commandQueue commandBuffer];
// 指定缓存区名称
commandBuffer.label = @"MyCommand";
- 创建命令描述符
MTLRenderPassDescriptor *renderPassDescriptor = view.currentRenderPassDescriptor;
- 创建渲染命令编码器
// 创建渲染命令编码器,这样才可以渲染到something
id<MTLRenderCommandEncoder> renderEncoder =[commandBuffer renderCommandEncoderWithDescriptor:renderPassDescriptor];
// 渲染器名称
renderEncoder.label = @"MyRenderEncoder";
- 设置我们绘制的可绘制区域:
视口指定Metal渲染内容的drawable区域。 视口是具有x和y偏移,宽度和高度以及近和远平面的3D区域。
为管道分配自定义视口需要通过调用setViewport:方法将MTLViewport结构编码为渲染命令编码器。 如果未指定视口,Metal会设置一个默认视口,其大小与用于创建渲染命令编码器的drawable相同。
MTLViewport viewPort = {
0.0,0.0,_viewportSize.x,_viewportSize.y,-1.0,1.0
};
[renderEncoder setViewport:viewPort];
- 设置当前渲染管道状态对象
[renderEncoder setRenderPipelineState:_pipelineState];
- 从应用程序OC代码中发送数据给 Metal 顶点着色器函数
顶点数据+颜色数据: 指向要传递给着色器的内存的指针, 想要传递的数据的内存大小;
一个整数索引,它对应于我们的“vertexShader”函数中的缓冲区属性限定符的索引。
// 指向要传递给着色器的内存的指针
// 想要传递的数据的内存大小
// 一个整数索引,它对应于“vertexShader”函数中的缓冲区属性限定符的索引
[renderEncoder setVertexBytes:triangleVertices
length:sizeof(triangleVertices)
atIndex:YDWVertexInputIndexVertices];
// 发送到顶点着色函数中,视图大小
// 视图大小内存空间大小
// 对应的索引
[renderEncoder setVertexBytes:&_viewportSize
length:sizeof(_viewportSize)
atIndex:YDWVertexInputIndexViewportSize];
- 画出三角形的3个顶点:
// @method drawPrimitives:vertexStart:vertexCount:
// @brief 在不使用索引列表的情况下,绘制图元
// @param 绘制图形组装的基元类型
// @param 从哪个位置数据开始绘制,一般为0
// @param 每个图元的顶点个数,绘制的图型顶点数量
/*
MTLPrimitiveTypePoint = 0, 点
MTLPrimitiveTypeLine = 1, 线段
MTLPrimitiveTypeLineStrip = 2, 线环
MTLPrimitiveTypeTriangle = 3, 三角形
MTLPrimitiveTypeTriangleStrip = 4, 三角型扇
*/
[renderEncoder drawPrimitives:MTLPrimitiveTypeTriangle
vertexStart:0
vertexCount:3];
- 结束编码: 表示已该编码器生成的命令都已完成, 且从NTLCommandBuffer中分离
[renderEncoder endEncoding];
- 一旦框架缓冲区完成,使用当前可绘制的进度表
[commandBuffer presentDrawable:view.currentDrawable];
- 命令缓冲区提交GPU进行显示
[commandBuffer commit];