Metal入门教程(一)图片绘制

作者: 落影loyinglin | 来源:发表于2018-06-24 10:45 被阅读303次

    前言

    这里是一篇Metal新手教程,先定个小目标:把绘制一张图片到屏幕上。
    Metal系列教程的代码地址
    OpenGL ES系列教程在这里

    你的star和fork是我的源动力,你的意见能让我走得更远

    正文

    核心思路

    通过MetalKit,尽量简单地实现把一张图片绘制到屏幕,核心的内容包括:设置渲染管道设置顶点和纹理缓存简单的shader理解

    效果展示

    具体步骤

    1、新建MTKView
        // 初始化 MTKView
        self.mtkView = [[MTKView alloc] initWithFrame:self.view.bounds];
        self.mtkView.device = MTLCreateSystemDefaultDevice(); // 获取默认的device
        self.view = self.mtkView;
        self.mtkView.delegate = self;
        self.viewportSize = (vector_uint2){self.mtkView.drawableSize.width, self.mtkView.drawableSize.height};
        
    

    MTKView是MetalKit提供的一个View,用来显示Metal的绘制;
    MTLDevice代表GPU设备,提供创建缓存、纹理等的接口;

    2、设置渲染管道
    // 设置渲染管道
    -(void)setupPipeline {
        id<MTLLibrary> defaultLibrary = [self.mtkView.device newDefaultLibrary]; // .metal
        id<MTLFunction> vertexFunction = [defaultLibrary newFunctionWithName:@"vertexShader"]; // 顶点shader,vertexShader是函数名
        id<MTLFunction> fragmentFunction = [defaultLibrary newFunctionWithName:@"samplingShader"]; // 片元shader,samplingShader是函数名
        
        MTLRenderPipelineDescriptor *pipelineStateDescriptor = [[MTLRenderPipelineDescriptor alloc] init];
        pipelineStateDescriptor.vertexFunction = vertexFunction;
        pipelineStateDescriptor.fragmentFunction = fragmentFunction;
        pipelineStateDescriptor.colorAttachments[0].pixelFormat = self.mtkView.colorPixelFormat;
        self.pipelineState = [self.mtkView.device newRenderPipelineStateWithDescriptor:pipelineStateDescriptor
                                                                             error:NULL]; // 创建图形渲染管道,耗性能操作不宜频繁调用
        self.commandQueue = [self.mtkView.device newCommandQueue]; // CommandQueue是渲染指令队列,保证渲染指令有序地提交到GPU
    }
    

    MTLRenderPipelineDescriptor是渲染管道的描述符,可以设置顶点处理函数、片元处理函数、输出颜色格式等;
    [device newCommandQueue]创建的是指令队列,用来存放渲染的指令;

    3、设置顶点数据
    - (void)setupVertex {
        static const LYVertex quadVertices[] =
        {   // 顶点坐标,分别是x、y、z、w;    纹理坐标,x、y;
            { {  0.5, -0.5, 0.0, 1.0 },  { 1.f, 1.f } },
            { { -0.5, -0.5, 0.0, 1.0 },  { 0.f, 1.f } },
            { { -0.5,  0.5, 0.0, 1.0 },  { 0.f, 0.f } },
            
            { {  0.5, -0.5, 0.0, 1.0 },  { 1.f, 1.f } },
            { { -0.5,  0.5, 0.0, 1.0 },  { 0.f, 0.f } },
            { {  0.5,  0.5, 0.0, 1.0 },  { 1.f, 0.f } },
        };
        self.vertices = [self.mtkView.device newBufferWithBytes:quadVertices
                                                     length:sizeof(quadVertices)
                                                    options:MTLResourceStorageModeShared]; // 创建顶点缓存
        self.numVertices = sizeof(quadVertices) / sizeof(LYVertex); // 顶点个数
    }
    

    顶点数据里包括顶点坐标,metal的世界坐标系与OpenGL ES一致,范围是[-1, 1],故而点(0, 0)是在屏幕的正中间
    顶点数据里还包括纹理坐标,纹理坐标系的取值范围是[0, 1],原点是在左下角;
    [device newBufferWithBytes:quadVertices..]创建的是顶点缓存,类似OpenGL ES的glGenBuffer创建的缓存。

    4、设置纹理数据
    - (void)setupTexture {
        UIImage *image = [UIImage imageNamed:@"abc"];
        // 纹理描述符
        MTLTextureDescriptor *textureDescriptor = [[MTLTextureDescriptor alloc] init];
        textureDescriptor.pixelFormat = MTLPixelFormatRGBA8Unorm;
        textureDescriptor.width = image.size.width;
        textureDescriptor.height = image.size.height;
        self.texture = [self.mtkView.device newTextureWithDescriptor:textureDescriptor]; // 创建纹理
        
        MTLRegion region = {{ 0, 0, 0 }, {image.size.width, image.size.height, 1}}; // 纹理上传的范围
        Byte *imageBytes = [self loadImage:image];
        if (imageBytes) { // UIImage的数据需要转成二进制才能上传,且不用jpg、png的NSData
            [self.texture replaceRegion:region
                        mipmapLevel:0
                          withBytes:imageBytes
                        bytesPerRow:4 * image.size.width];
            free(imageBytes); // 需要释放资源
            imageBytes = NULL;
        }
    }
    

    MTLTextureDescriptor是纹理数据的描述符,可以设置像素颜色格式、图像宽高等,用于创建纹理;
    纹理创建完毕后,需要用-replaceRegion: mipmapLevel:withBytes:bytesPerRow:接口上传纹理数据;
    MTLRegion类似UIKit的frame,用于表明纹理数据的存放区域;

    5、具体渲染过程
    - (void)drawInMTKView:(MTKView *)view {
        // 每次渲染都要单独创建一个CommandBuffer
        id<MTLCommandBuffer> commandBuffer = [self.commandQueue commandBuffer];
        MTLRenderPassDescriptor *renderPassDescriptor = view.currentRenderPassDescriptor;
        // MTLRenderPassDescriptor描述一系列attachments的值,类似GL的FrameBuffer;同时也用来创建MTLRenderCommandEncoder
        if(renderPassDescriptor != nil)
        {
            renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColorMake(0.0, 0.5, 0.5, 1.0f); // 设置默认颜色
            id<MTLRenderCommandEncoder> renderEncoder = [commandBuffer renderCommandEncoderWithDescriptor:renderPassDescriptor]; //编码绘制指令的Encoder
            [renderEncoder setViewport:(MTLViewport){0.0, 0.0, self.viewportSize.x, self.viewportSize.y, -1.0, 1.0 }]; // 设置显示区域
            [renderEncoder setRenderPipelineState:self.pipelineState]; // 设置渲染管道,以保证顶点和片元两个shader会被调用
            
            [renderEncoder setVertexBuffer:self.vertices
                                    offset:0
                                   atIndex:0]; // 设置顶点缓存
    
            [renderEncoder setFragmentTexture:self.texture
                                      atIndex:0]; // 设置纹理
            
            [renderEncoder drawPrimitives:MTLPrimitiveTypeTriangle
                              vertexStart:0
                              vertexCount:self.numVertices]; // 绘制
            
            [renderEncoder endEncoding]; // 结束
            
            [commandBuffer presentDrawable:view.currentDrawable]; // 显示
        }
        
        [commandBuffer commit]; // 提交;
    }
    
    
    

    drawInMTKView:方法是MetalKit每帧的渲染回调,可以在内部做渲染的处理;
    绘制的第一步是从commandQueue里面创建commandBuffer,commandQueue是整个app绘制的队列,而commandBuffer存放每次渲染的指令,commandQueue内部存在着多个commandBuffer。
    整个绘制的过程与OpenGL ES一致,先设置窗口大小,然后设置顶点数据和纹理,最后绘制两个三角形。
    CommandQueue、CommandBuffer和CommandEncoder的关系如下:

    CommandQueue、CommandBuffer和CommandEncoder的关系
    6、Shader处理
    
    typedef struct
    {
        float4 clipSpacePosition [[position]]; // position的修饰符表示这个是顶点
        
        float2 textureCoordinate; // 纹理坐标,会做插值处理
        
    } RasterizerData;
    
    vertex RasterizerData // 返回给片元着色器的结构体
    vertexShader(uint vertexID [[ vertex_id ]], // vertex_id是顶点shader每次处理的index,用于定位当前的顶点
                 constant LYVertex *vertexArray [[ buffer(0) ]]) { // buffer表明是缓存数据,0是索引
        RasterizerData out;
        out.clipSpacePosition = vertexArray[vertexID].position;
        out.textureCoordinate = vertexArray[vertexID].textureCoordinate;
        return out;
    }
    
    fragment float4
    samplingShader(RasterizerData input [[stage_in]], // stage_in表示这个数据来自光栅化。(光栅化是顶点处理之后的步骤,业务层无法修改)
                   texture2d<half> colorTexture [[ texture(0) ]]) // texture表明是纹理数据,0是索引
    {
        constexpr sampler textureSampler (mag_filter::linear,
                                          min_filter::linear); // sampler是采样器
        
        half4 colorSample = colorTexture.sample(textureSampler, input.textureCoordinate); // 得到纹理对应位置的颜色
        
        return float4(colorSample);
    }
    

    Shader如上。与OpenGL ES的shader相比,最明显是输入的参数可以用结构体,返回的参数也可以用结构体;
    LYVertex是shader和Objective-C公用的结构体,RasterizerData是顶点Shader返回再传给片元Shader的结构体;
    Shader的语法与C++类似,参数名前面的是类型,后面的[[ ]]是描述符。

    总结

    Metal和OpenGL一样,需要有一定的图形学基础,才能理解具体的含义。
    本文为了降低上手的门槛,简化掉一些逻辑,增加很多注释,同时保留最核心的几个步骤以便理解。

    这里可以下载demo代码。

    相关文章

      网友评论

      • Tangdixi:试试用 MTKTextureLoader, 可以直接读 CGImage,省去了中间的 loadImage :sunglasses:
        落影loyinglin:是的,这个后面的demo有用到,这里尽可能贴近gl,减少不必要的上手要求
      • 搜捕儿:大佬觉得metal和opengl相比怎么样?
        搜捕儿:@Tangdixi 是啊,跨平台还是有吸引力的
        Tangdixi:Metal 感觉更加合理的分配了 CPU 跟 GPU 工作,针对了下 OpenGL 的一些痛点,但 OpenGL 跨平台 ....
        落影loyinglin:@搜捕儿 metal对iOS更有好
      • 咬尾巴的妖精猫:DEMO下载下来运行发现不行,屏幕一片黑。模拟器ios11.3,iPhone X
        咬尾巴的妖精猫:@落影loyinglin :joy: 我猜也是。
        落影loyinglin:@咬尾巴的妖精猫 模拟器不支持,用真机

      本文标题:Metal入门教程(一)图片绘制

      本文链接:https://www.haomeiwen.com/subject/ctzryftx.html