Metal(2)- 颜色渲染和绘制三角形案例解析

2020-09-16  本文已影响0人  恍然如梦_b700

颜色渲染案例

本案例的目的在于了解metal相关API的使用,及简单渲染的流程

整体的效果图如下

image

案例的整体流程如下

image

主要分为两部分

viewDidLoad函数

该函数中主要是加载view以及view传递给render渲染循环类,主要流程如下

image

分为以下几步

获取view
获取MTKView的对象view的方式有两种,类似于GLKitGLKView

_view = (MTKView*)self.view;

设置device
主要是获取GPU的使用权限,一个MTLDevice对象代表着一个GPU,一般使用默认方法MTLCreateSystemDefaultDevice来获取默认的单个GPU对象,并且在创建完成后,需要判断是否获取GPU的使用权限,如果不成功,则中断渲染流程

_view.device = MTLCreateSystemDefaultDevice();
if (!_view.device) {
    NSLog(@"Metal is not supported on this device");
    return;
}

创建render
在metal框架中,苹果建议在开发mental程序时,最好是将渲染循环独立成一个类,目的是为了更高的管理metal以及metal视图委托。创建完成后,同样需要判断是否创建成功,如果不成功,则中断渲染流程

_render = [[CJLRenderer alloc] initWithMetalKitView:_view];

//5.判断_render 是否创建成功
if (!_render) {
    NSLog(@"Renderer failed initialization");
    return;
}

设置view的delegate
将view的渲染处理加油render对象处理

_view.delegate = _render;

设置帧速率
在view中可以通过设置帧速率,不同的触发试图渲染,然后回调MTKViewDelegate中的drawInMTKView方法

_view.preferredFramesPerSecond = 60;

渲染循环类

管理metal的初始化以及metal中的视图委托,主要有以下四个函数

由于本案例不涉及view大小的改变,所以着重讲initWithMetalKitViewdrawInMTKView方法

initWithMetalKitView函数

渲染循环类对外的初始化方法,主要是通过传入的view,获取metal设备以及创建命令队列,流程如下

image
_device = mtkView.device;

_commandQueue = [_device newCommandQueue];

drawInMTKView代理方法

通过view设置的帧速率,每当到指定时间时,就会触发view的渲染,继而回调drawInMTKView代理方法进行绘制渲染。主要流程如下

image
Color color = [self makeFancyColor];

view.clearColor = MTLClearColorMake(color.red, color.green, color.blue, color.alpha);

id<MTLCommandBuffer> commandBuffer = [_commandQueue commandBuffer];
    //设置渲染缓存区的命名
    commandBuffer.label = @"MyCommand";

MTLRenderPassDescriptor *renderPassDescriptor = view.currentRenderPassDescriptor;

id<MTLRenderCommandEncoder> renderEncoder = [commandBuffer renderCommandEncoderWithDescriptor:renderPassDescriptor];
        //命令编辑器命名
        renderEncoder.label = @"MyRenderEncoder";

 [renderEncoder endEncoding];

[commandBuffer presentDrawable:view.currentDrawable];

[commandBuffer commit];

以上几个步骤,在metal渲染中几乎都要使用到,需要牢记!!

绘制三角形

本案例的目的在于理解Metal中使用着色器绘制三角形的流程

整体效果图如下

image

整体的流程图如下

image

颜色的渲染加载相比,viewDidLoad函数基本没变化,主要变化的是initWithMetalKitView函数和drawInMTKView代理方法

准备工作

创建metal文件

//顶点着色器输出和片元着色器输入(相当于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 设置的集合调用

typedef enum CJLVertexInputIndex
{
//    顶点
    CJLVertexInputIndexVertices = 0,

//    视图大小
    CJLVertexInputIndexViewportSize = 1,

}CJLVertexInputIndex;

//结构体:顶点/颜色值
typedef struct
{
//    像素空间的位置
//    像素中心点(100,100)
    vector_float4 position;

//    RGBA颜色
    vector_float4 color;
}CJLVertex;

viewDidLoad函数

这个函数的流程如下

image

Metal 入门级01:颜色的渲染加载相比,只是多了一个设置视口大小,会触发MTKViewDelegate协议的drawableSizeWillChange方法,这里不做过多说明

渲染循环类

渲染循环类CJLRenderer是服务于MTKView的,用于管理view的渲染以及view的代理方法的回调

initWithMetalKitView函数

这部分的代码都是绘制前的准备工作,流程图如下

image

主要分为是三步

其中第一步和第三步在上个案例均有提及,这里不再说明,主要说说metal着色器文件的加载

metal着色器文件加载

根据上图所示,可以划分为以下几步

//2.在项目中加载所有的(.metal)着色器文件
// 从bundle中获取.metal文件
id<MTLLibrary> defaultLibrary = [_device newDefaultLibrary];
//从库中加载顶点函数
id<MTLFunction> vertexFunction = [defaultLibrary newFunctionWithName:@"vertexShader"];
//从库中加载片元函数
id<MTLFunction> fragmentFunction = [defaultLibrary newFunctionWithName:@"fragmentShader"];

//3.配置用于创建管道状态的管道,即渲染管道
MTLRenderPipelineDescriptor *pipelineStateDescriptor = [[MTLRenderPipelineDescriptor alloc] init];
//管道名称
pipelineStateDescriptor.label = @"Simple Pipeline";
//可编程函数,用于处理渲染过程中的各个顶点
pipelineStateDescriptor.vertexFunction = vertexFunction;
//可编程函数,用于处理渲染过程中各个片段/片元
pipelineStateDescriptor.fragmentFunction = fragmentFunction;
//一组存储颜色数据的组件
pipelineStateDescriptor.colorAttachments[0].pixelFormat = mtkView.colorPixelFormat;

//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,与屏幕刷新的帧率是一致的,所以会随着屏幕刷新的帧率,不停的调用该绘制的代理方法

image

该方法整体的流程如下

image

相比颜色渲染的案例,多了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 } },
};

三角形的绘制主要分为以下几步:

MTLViewport viewPort = {
    0.0, 0.0, _viewportSize.x, _viewportSize.y, -1.0, 1.0
};
//设置视口 相当于OpenGL ES中的glViewPort
[renderEncoder setViewport:viewPort];

//6.设置当前渲染管道状态对象
[renderEncoder setRenderPipelineState:_pipelineState];

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

图元类型 说明
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自行完成的图元装配和光栅化,将处理后的数据传入片元着色器进行处理

image

顶点着色器函数
主要有两部分处理

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

上一篇下一篇

猜你喜欢

热点阅读