Metal应用--绘制图片

用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;
  • 顶点着色器,和Metal应用--绘制大量顶点一样,只不过是将颜色改成了纹理坐标
//顶点着色器
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 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 _device;
    
    //渲染管道:顶点着色器/片元着色器,存储于.metal shader文件中
    id _pipelineState;
    
    //命令队列,从命令缓存区获取
    id _commandQueue;
    
    //纹理对象
    id _texture;
    
    //顶点缓存区
    id _vertexBuffer;
    
    //当前视图大小,这样我们才能在渲染通道中使用此视图
    vector_uint2 _viewportSize;
    
    //顶点个数
    NSInteger _numVertices;
     
    MTKView *_mtkView;
}

我们用id _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 defaultLibrary = [_device newDefaultLibrary];
    //从库中加载顶点函数
    id vertexFunction = [defaultLibrary newFunctionWithName:@"vertexShader"];
    //从库中加载片元函数
    id 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 commandBuffer = [_commandQueue commandBuffer];
    //指定缓存区名称
    commandBuffer.label = @"MyCommand";
    //2.currentRenderPassDescriptor描述符包含currentDrawable's的纹理、视图的深度、模板和sample缓冲区和清晰的值。
    MTLRenderPassDescriptor *renderPassDescriptor = view.currentRenderPassDescriptor;
    if (renderPassDescriptor != nil) {
        //3.创建渲染命令编码器,这样我们才可以渲染到something
        id  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应用--绘制图片)