美文网首页OpenGL & Metal
Metal应用--绘制图片

Metal应用--绘制图片

作者: 黑眼豆豆_ | 来源:发表于2020-09-03 16:51 被阅读0次

    用Metal绘制图片跟用Metal应用--绘制大量顶点整体流程类似,但是由于是纹理,在部分地方有些区别。

    LeoShaderType.h

    • 添加纹理索引,因为可能会有多个纹理,所以添加纹理索引表示区分
    //纹理索引
    typedef enum LeoTextureIndex
    {
        LeoTextureIndexBaseColor = 0
    }LeoTextureIndex;
    
    • 定义枚举表示顶点视图大小
    typedef enum {
        //顶点
        LeoVertexInputIndexVertices     = 0,
        //视图大小
        LeoVertexInputIndexViewportSize = 1,
    } LeoVertexInputIndex;
    
    • 定义结构体,用来封装有OC传到Metal的顶点坐标纹理坐标
    //结构体: 顶点/纹理
    typedef struct {
        // 像素空间的位置
        // 像素中心点(100,100)
        //float float
        vector_float2 position;
        // 2D 纹理
        vector_float2 textureCoordinate;
    } LeoVertex;
    

    LeoShaders.metal

    • 定义结构体,包含顶点坐标纹理坐标,用来封装从顶点着色器给片元着色器传的值
    // 顶点着色器输出和片段着色器输入
    //结构体
    typedef struct
    {
        //处理空间的顶点信息
        float4 clipSpacePosition [[position]];    
        //纹理
        float2 textureCoordinate;
    } RasterizerData;
    
    //顶点着色器
    vertex RasterizerData vertexShader(uint vertexID[[vertex_id]],constant LeoVertex *vertices [[buffer(LeoVertexInputIndexVertices)]],constant vector_uint2 *viewportSizePointer [[buffer(LeoVertexInputIndexViewportSize)]]){
     
        /*
         处理顶点数据:
         1) 执行坐标系转换,将生成的顶点剪辑空间写入到返回值中.
         2) 将顶点颜色值传递给返回值
         */
        //定义out
        RasterizerData out;
        //初始化输出剪辑空间位置
        out.clipSpacePosition = vector_float4(0.0,0.0,0.0,1.0);
        // 索引到我们的数组位置以获得当前顶点
        // 我们的位置是在像素维度中指定的.
        float2 pixelSpacePosition = vertices[vertexID].position.xy;
        //将vierportSizePointer 从verctor_uint2 转换为vector_float2 类型
        vector_float2 viewportSize = vector_float2(*viewportSizePointer);
        //每个顶点着色器的输出位置在剪辑空间中(也称为归一化设备坐标空间,NDC),剪辑空间中的(-1,-1)表示视口的左下角,而(1,1)表示视口的右上角.
        //计算和写入 XY值到我们的剪辑空间的位置.为了从像素空间中的位置转换到剪辑空间的位置,我们将像素坐标除以视口的大小的一半.
        out.clipSpacePosition.xy = pixelSpacePosition / (viewportSize / 2.0);
        //把我们输入的颜色直接赋值给输出颜色. 这个值将于构成三角形的顶点的其他颜色值插值,从而为我们片段着色器中的每个片段生成颜色值.
        out.textureCoordinate  = vertices[vertexID].textureCoordinate;
        //完成! 将结构体传递到管道中下一个阶段:
        return out;
    }
    
    • 片元着色器

      设置采样器,设置过滤方式

        //设置采样器
        constexpr sampler textureSample(mag_filter::linear,min_filter::linear);
    

      获取纹素

        //获取纹素
        const float4 colorSampler = colorTexture.sample(textureSample, in.textureCoordinate);
    

    完整代码如下:

    //当顶点函数执行3次,三角形的每个顶点执行一次后,则执行管道中的下一个阶段.栅格化/光栅化.
    
    // 片元函数
    //[[stage_in]],片元着色函数使用的单个片元输入数据是由顶点着色函数输出.然后经过光栅化生成的.单个片元输入函数数据可以使用"[[stage_in]]"属性修饰符.
    //一个顶点着色函数可以读取单个顶点的输入数据,这些输入数据存储于参数传递的缓存中,使用顶点和实例ID在这些缓存中寻址.读取到单个顶点的数据.另外,单个顶点输入数据也可以通过使用"[[stage_in]]"属性修饰符的产生传递给顶点着色函数.
    //被stage_in 修饰的结构体的成员不能是如下这些.Packed vectors 紧密填充类型向量,matrices 矩阵,structs 结构体,references or pointers to type 某类型的引用或指针. arrays,vectors,matrices 标量,向量,矩阵数组.
    
    fragment float4 fragmentShader(RasterizerData in [[stage_in]],texture2d<float> colorTexture [[texture(LeoTextureIndexBaseColor)]]){
        //设置采样器
        constexpr sampler textureSample(mag_filter::linear,min_filter::linear);
        //获取纹素
        const float4 colorSampler = colorTexture.sample(textureSample, in.textureCoordinate);
        //返回颜色
        return colorSampler;
    }
    

    LeoRenderer.m

    • 定义一些变量
    @implementation LeoRenderer{
        //渲染的设备(GPU)
        id<MTLDevice> _device;
        
        //渲染管道:顶点着色器/片元着色器,存储于.metal shader文件中
        id<MTLRenderPipelineState> _pipelineState;
        
        //命令队列,从命令缓存区获取
        id<MTLCommandQueue> _commandQueue;
        
        //纹理对象
        id<MTLTexture> _texture;
        
        //顶点缓存区
        id<MTLBuffer> _vertexBuffer;
        
        //当前视图大小,这样我们才能在渲染通道中使用此视图
        vector_uint2 _viewportSize;
        
        //顶点个数
        NSInteger _numVertices;
         
        MTKView *_mtkView;
    }
    

    我们用id<MTLBuffer> _vertexBuffer存放纹理

    • 设置顶点相关操作
    -(void)setupVertex{
        //1.根据顶点/纹理坐标建立一个MTLBuffer
        static const LeoVertex quadVertices[] = {
            //像素坐标,纹理坐标
            { {  250,  -400 },  { 1.f, 0.f } },
            { { -250,  -400 },  { 0.f, 0.f } },
            { { -250,   400 },  { 0.f, 1.f } },
            
            { {  250,  -400 },  { 1.f, 0.f } },
            { { -250,   400 },  { 0.f, 1.f } },
            { {  250,   400 },  { 1.f, 1.f } },
            
        };
        
        //2.创建我们的顶点缓冲区,并用我们的Qualsits数组初始化它
        _vertexBuffer = [_device newBufferWithBytes:quadVertices length:sizeof(quadVertices) options:(MTLResourceStorageModeShared)];
        //3.通过将字节长度除以每个顶点的大小来计算顶点的数目
        _numVertices = sizeof(quadVertices)/sizeof(LeoVertex);
    }
    
    • 设置渲染管道相关操作
    -(void)setupPipeLine{
        //1.创建我们的渲染通道
        //从项目中加载.metal文件,创建一个library
        id<MTLLibrary> defaultLibrary = [_device newDefaultLibrary];
        //从库中加载顶点函数
        id<MTLFunction> vertexFunction = [defaultLibrary newFunctionWithName:@"vertexShader"];
        //从库中加载片元函数
        id<MTLFunction> fragmentFunction = [defaultLibrary newFunctionWithName:@"fragmentShader"];
        //2.配置用于创建管道状态的管道
        MTLRenderPipelineDescriptor *pipelineDescriptor = [[MTLRenderPipelineDescriptor alloc] init];
        //管道名称
        pipelineDescriptor.label = @"Texturing Pipeline";
        //可编程函数,用于处理渲染过程中的各个顶点
        pipelineDescriptor.vertexFunction = vertexFunction;
        //可编程函数,用于处理渲染过程总的各个片段/片元
        pipelineDescriptor.fragmentFunction = fragmentFunction;
        //设置管道中存储颜色数据的组件格式
        pipelineDescriptor.colorAttachments[0].pixelFormat = _mtkView.colorPixelFormat;
        
        //3.同步创建并返回渲染管线对象
        NSError *error = NULL;
        _pipelineState = [_device newRenderPipelineStateWithDescriptor:pipelineDescriptor error:&error];
        //判断是否创建成功
        if (!_pipelineState)
        {
            NSLog(@"Failed to created pipeline state, error %@", error);
        }
        //4.使用_device创建commandQueue
        _commandQueue = [_device newCommandQueue];
    }
    
    • 设置纹理
       设置纹理描述符
        //获取图片
        UIImage *image = [UIImage imageNamed:@"jay.jpg"];
        //2.纹理描述符
        MTLTextureDescriptor *textureDescriptor = [[MTLTextureDescriptor alloc] init];
        //表示每个像素有蓝色,绿色,红色和alpha通道.其中每个通道都是8位无符号归一化的值.(即0映射成0,255映射成1);
        textureDescriptor.pixelFormat = MTLPixelFormatRGBA8Unorm;
        //设置纹理的像素尺寸
        textureDescriptor.width = image.size.width;
        textureDescriptor.height = image.size.height;
    

      使用描述符从设备中创建纹理

        //3.使用描述符从设备中创建纹理
        _texture = [_device newTextureWithDescriptor:textureDescriptor];
    

      设置纹理范围

        MTLRegion region = {{ 0, 0, 0 }, {image.size.width, image.size.height, 1}};
    

      从UIImage获取纹理(Byte *)

    //从UIImage 中读取Byte 数据返回
    - (Byte *)loadImage:(UIImage *)image {
        // 1.获取图片的CGImageRef
        CGImageRef spriteImage = image.CGImage;
        
        // 2.读取图片的大小
        size_t width = CGImageGetWidth(spriteImage);
        size_t height = CGImageGetHeight(spriteImage);
       
        //3.计算图片大小.rgba共4个byte
        Byte * spriteData = (Byte *) calloc(width * height * 4, sizeof(Byte));
        
        //4.创建画布
        CGContextRef spriteContext = CGBitmapContextCreate(spriteData, width, height, 8, width*4, CGImageGetColorSpace(spriteImage), kCGImageAlphaPremultipliedLast);
        
        //5.在CGContextRef上绘图
        CGContextDrawImage(spriteContext, CGRectMake(0, 0, width, height), spriteImage);
        
        //6.图片翻转过来
        CGRect rect = CGRectMake(0, 0, width, height);
        CGContextTranslateCTM(spriteContext, rect.origin.x, rect.origin.y);
        CGContextTranslateCTM(spriteContext, 0, rect.size.height);
        CGContextScaleCTM(spriteContext, 1.0, -1.0);
        CGContextTranslateCTM(spriteContext, -rect.origin.x, -rect.origin.y);
        CGContextDrawImage(spriteContext, rect, spriteImage);
        
        //7.释放spriteContext
        CGContextRelease(spriteContext);
        
        return spriteData;
    }
    

      调用

        //获取纹理
        Byte *imageBytes = [self loadImage:image];
    

      将图片复制到纹理0中(即用纹理替换region表示的区域)

    //判断imageBytes是否存在
     if (imageBytes) {
         //将图片复制到纹理0中(即用纹理替换region表示的区域)
         [_texture replaceRegion:region mipmapLevel:0 withBytes:imageBytes bytesPerRow:4 * image.size.width];
         free(imageBytes);
         imageBytes = nil;
      }
    
    • 用代理方法drawableSizeWillChange设置视图大小
    //每当视图改变方向或调整大小时调用
    -(void)mtkView:(MTKView *)view drawableSizeWillChange:(CGSize)size{
        // 保存可绘制的大小,因为当我们绘制时,我们将把这些值传递给顶点着色器
        _viewportSize.x = size.width;
        _viewportSize.y = size.height;
    }
    
    • 具体绘制操作,执行代理方法drawInMTKView

      将纹理对象传入到Fragment方法中

     //设置纹理对象 
    [renderCommandEncoder setFragmentTexture:_texture atIndex:LeoTextureIndexBaseColor];
    

    完整代码如下:

    -(void)drawInMTKView:(MTKView *)view{
        //1.为当前渲染的每个渲染传递创建一个新的命令缓冲区
        id<MTLCommandBuffer> commandBuffer = [_commandQueue commandBuffer];
        //指定缓存区名称
        commandBuffer.label = @"MyCommand";
        //2.currentRenderPassDescriptor描述符包含currentDrawable's的纹理、视图的深度、模板和sample缓冲区和清晰的值。
        MTLRenderPassDescriptor *renderPassDescriptor = view.currentRenderPassDescriptor;
        if (renderPassDescriptor != nil) {
            //3.创建渲染命令编码器,这样我们才可以渲染到something
            id <MTLRenderCommandEncoder> renderCommandEncoder = [commandBuffer renderCommandEncoderWithDescriptor:renderPassDescriptor];
            //渲染器名称
            renderCommandEncoder.label = @"MyRenderEncoder";
            //4.设置我们绘制的可绘制区域
            /*
             typedef struct {
             double originX, originY, width, height, znear, zfar;
             } MTLViewport;
             */
            [renderCommandEncoder setViewport:(MTLViewport){0.0, 0.0, _viewportSize.x, _viewportSize.y, -1.0, 1.0 }];
            
            //5.设置渲染管道
            [renderCommandEncoder setRenderPipelineState:_pipelineState];
            
            //6.加载数据
            //将数据加载到MTLBuffer --> 顶点函数
            [renderCommandEncoder setVertexBuffer:_vertexBuffer offset:0 atIndex:LeoVertexInputIndexVertices];
            //将数据加载到MTLBuffer --> 顶点函数
            [renderCommandEncoder setVertexBytes:&_viewportSize length:sizeof(_viewportSize) atIndex:LeoVertexInputIndexViewportSize];
         
            //7.设置纹理对象
            [renderCommandEncoder setFragmentTexture:_texture atIndex:LeoTextureIndexBaseColor];
            
            //8.绘制
            // @method drawPrimitives:vertexStart:vertexCount:
            //@brief 在不使用索引列表的情况下,绘制图元
            //@param 绘制图形组装的基元类型
            //@param 从哪个位置数据开始绘制,一般为0
            //@param 每个图元的顶点个数,绘制的图型顶点数量
            /*
             MTLPrimitiveTypePoint = 0, 点
             MTLPrimitiveTypeLine = 1, 线段
             MTLPrimitiveTypeLineStrip = 2, 线环
             MTLPrimitiveTypeTriangle = 3,  三角形
             MTLPrimitiveTypeTriangleStrip = 4, 三角型扇
             */
            [renderCommandEncoder drawPrimitives:(MTLPrimitiveTypeTriangle) vertexStart:0 vertexCount:_numVertices];
            
            //9.表示已该编码器生成的命令都已完成,并且从NTLCommandBuffer中分离
            [renderCommandEncoder endEncoding];
            
            //10.一旦框架缓冲区完成,使用当前可绘制的进度表
            [commandBuffer presentDrawable:view.currentDrawable];
        }
        
        //11.最后,在这里完成渲染并将命令缓冲区推送到GPU
        [commandBuffer commit];
    }
    

    ViewController

    最后在ViewController进行调用

    //
    //  ViewController.m
    //  OpenGL_ES_Test
    //
    //  Created by leosun on 2020/7/27.
    //  Copyright © 2020 leosun. All rights reserved.
    //
    
    #import "ViewController.h"
    #import "LeoRenderer.h"
    
    @interface ViewController (){
        MTKView *_view;
        LeoRenderer *_render;
    }
    
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
    
        //1. 获取_view
        _view = (MTKView *)self.view;
        
        //2.为_view 设置MTLDevice(必须)
        //一个MTLDevice 对象就代表这着一个GPU,通常我们可以调用方法MTLCreateSystemDefaultDevice()来获取代表默认的GPU单个对象.
        _view.device = MTLCreateSystemDefaultDevice();
        
        //3.判断是否设置成功
        if (!_view.device) {
            NSLog(@"Metal is not supported on this device");
            return;
        }
        
        //4. 创建LeoRenderer
        //分开你的渲染循环:
        //在我们开发Metal 程序时,将渲染循环分为自己创建的类,是非常有用的一种方式,使用单独的类,我们可以更好管理初始化Metal,以及Metal视图委托.
        _render = [[LeoRenderer alloc] initWithMetalKitView:_view];
    
        //5.判断_render 是否创建成功
        if (!_render) {
            NSLog(@"Renderer failed initialization");
            return;
        }
        
        //用视图大小初始化渲染器
        [_render mtkView:_view drawableSizeWillChange:_view.drawableSize];
    
        //6.设置MTKView 的代理(由CCRender来实现MTKView 的代理方法)
        _view.delegate = _render;
    }
    @end
    

    相关文章

      网友评论

        本文标题:Metal应用--绘制图片

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