iOS 基于Metal的图片渲染流程及详细解析

1,239 阅读8分钟

1.Metal是苹果的图形图像渲染框架,也可以实现普通的GPU高并发计算,流程与OpenGL ES非常类似,且其里面也有封装好的滤镜,可以直接使用无需自己写.metal渲染的代码实现,视情况而定

2.主要思路

1.OC代码段编写Metal渲染所需的相关流程代码

2.OC和metal文件之间的桥接文件提供数据类型和端口索引

3.编写metal的 vertex顶点函数 fragment片元函数

4.在MTKView的代理方法里面渲染每一帧的图片纹理数据

3.具体步骤

1. OC代码段编写Metal渲染所需的相关流程代码

1.创建MTKView,设置代理,这是Metal渲染的目标view,这里的代码都是面向协议的编码,MTKView的 id device,device非常重要,device可以理解成GPU,其他的很多代码都是跟device相关,里面的渲染管道,命令队列,命令缓存区,渲染命令编码器,纹理,各种缓存区,MTLLibrary都需要device生成

2.创建纹理

3.创建顶点

4.创建渲染管道,加载顶点函数和片元函数

2.OC和metal文件之间的桥接文件提供数据类型和端口索引

1.具体的数据结构和索引视情况而定,顶点坐标,纹理坐标,图片纹理,顶点索引,纹理索引,其他需要的参数,比如时间,数组,矩阵,需要的都可以

2.视频的纹理是Y纹理 和 UV纹理两个部分,需要增加YUV到RGB的颜色转换矩阵

3.编写metal的 vertex顶点函数 fragment片元函数

1.编写metal函数,语法看起来有些复杂,其实还好,要注意与桥接对象的数据类型的对应关系,主要有数据结构和端口索引值,类型关键字比如 [[position]] [[buffer]] [[stage_in]] 等等,函数修饰符,参数地址空间修饰符等,其余编码的思路与OpenGL ES基本一致,只是语法有些差异

4.在MTKView的代理方法里面渲染每一帧的图片纹理数据
  1. MTKView的两个代理方法

**

// 设置渲染范围
- (void)mtkView:(nonnull MTKView *)view drawableSizeWillChange:(CGSize)size {
    self.viewportSize = (vector_int2){size.width, size.height};
}

// MTKViewDelegate
// 每一帧渲染命令的具体实现
- (void)drawInMTKView:(nonnull MTKView *)view

4.代码实现

1.OC类

1.头文件和变量 属性

**

#import <MetalKit/MetalKit.h>
#import <Metal/Metal.h>
#import <AVFoundation/AVFoundation.h>

// 桥接类
#import "YYImageShaderTypes.h"

@interface MetalImageFilterView ()
<MTKViewDelegate>
{
    CGRect m_frame;
    BOOL isChangeFillMode;
    CGSize imageSize;
}
// 渲染范围
@property (nonatomic, assign) vector_int2 viewportSize;

// MTKView Metal渲染的view
@property (nonatomic, strong) MTKView * mtkView;

// 用来渲染的设备(GPU)
@property (nonatomic, strong) id <MTLDevice> device;

// 渲染管道,管理顶点函数和片元函数
@property (nonatomic, strong) id <MTLRenderPipelineState> renderPipelineState;

// 渲染指令队列
@property (nonatomic, strong) id <MTLCommandQueue> commondQueue;

// 顶点缓存对象
@property (nonatomic, strong) id <MTLBuffer> vertexBuffer;

// 纹理对象
@property (nonatomic, strong) id <MTLTexture> texture;

// 顶点数量
@property (nonatomic, assign) NSUInteger vertexCount;

//

2.初始化调用

**


- (id)initWithFrame:(CGRect)frame {
    self = [super initWithFrame:frame];
    if (self) {
        m_frame = frame;
        
        // 1.创建 MTKView
        [self createMTKView];
        
        // 2.设置顶点 1.0和1.0表示宽高保持默认的拉伸状态,不去动态调整
        [self setupVertexsWithWidthScaling:1.0f heightScaling:1.0f];
        
        // 3.设置纹理
        [self setupTexture];
        
        // 4.创建渲染管道
        [self createPipeLineState];
    }
    return self;
}

3.创建MTKView

**

// 创建 MTKView
- (void)createMTKView {
    MTKView * mtkView = [[MTKView alloc] initWithFrame:CGRectMake(0, 0, m_frame.size.width, m_frame.size.height)];
    mtkView.delegate = self;
    
    // 创建Device
    mtkView.device = MTLCreateSystemDefaultDevice();
    
    // 设置device
    self.device = mtkView.device;
    self.viewportSize = (vector_int2){mtkView.drawableSize.width, mtkView.drawableSize.height};
    self.mtkView = mtkView;
    [self addSubview:mtkView];
}

4.创建顶点数据结构数组和缓存区

**

// 2.设置顶点
- (void)setupVertexs {
    // 1.顶点纹理数组
    // 顶点x,y,z,w  纹理x,y
    YYVertex vertexArray[] = {
        {{-1.0, -1.0, 0.0, 1.0}, {0.0, 0.0}}, // 左下
        {{1.0, -1.0, 0.0, 1.0}, {1.0, 0.0}}, // 右下
        {{-1.0, 1.0, 0.0, 1.0}, {0.0, 1.0}}, //左上
        {{1.0, 1.0, 0.0, 1.0}, {1.0, 1.0}}, // 右上
    };
    
    // 2.生成顶点缓存
    // MTLResourceStorageModeShared 属性可共享的,表示可以被顶点或者片元函数或者其他函数使用
    self.vertexBuffer = [self.device newBufferWithBytes:vertexArray length:sizeof(vertexArray) options:MTLResourceStorageModeShared];
    
    // 3.获取顶点数量
    self.vertexCount = sizeof(vertexArray) / sizeof(YYVertex);
}

5.创建图片纹理

**

// 3.设置纹理
- (void)setupTexture {
    UIImage * image = [UIImage imageNamed:@"linXinRuMetal1.jpg"];
    
    // 1.创建纹理描述符
    MTLTextureDescriptor * textureDescriptor = [[MTLTextureDescriptor alloc] init];
    // 设置纹理描述符的宽,高,像素存储格式
    textureDescriptor.width = image.size.width;
    textureDescriptor.height = image.size.height;
    imageSize = image.size;
    //MTLPixelFormatRGBA8Unorm 表示每个像素有蓝色,绿色,红色和alpha通道.其中每个通道都是8位无符号归一化的值.(即0映射成0,255映射成1)
    textureDescriptor.pixelFormat = MTLPixelFormatRGBA8Unorm;
    
    // 2.创建纹理对象
    id <MTLTexture> texture = [self.device newTextureWithDescriptor:textureDescriptor];
    self.texture = texture;
    // id <MTLDevice> -> id <MTLTexture>
    
    // 3.将图片数据读取到纹理对象内
    /*
     typedef struct
     {
     MTLOrigin origin; //开始位置x,y,z
     MTLSize   size; //尺寸width,height,depth
     } MTLRegion;
     */
    //MLRegion结构用于标识纹理的特定区域。 demo使用图像数据填充整个纹理;因此,覆盖整个纹理的像素区域等于纹理的尺寸。
    //4. 创建MTLRegion 结构体  [纹理上传的范围]
    MTLRegion region = {{0, 0, 0}, {image.size.width, image.size.height, 1}};
    
    // 图片的二进制数据 UIImage的数据需要转成二进制才能上传,且不用jpg、png的NSData
    Byte * imageBytes = [self loadImage:image];
    
    // 将图片数据读取到纹理对象内
    // region 纹理区域
    // 0 mip贴图层次
    // imageBytes 图片二进制数据
    // image.size.width * 4 每一行字节数
    if (imageBytes) {
        [self.texture replaceRegion:region mipmapLevel:0 withBytes:imageBytes bytesPerRow:image.size.width * 4];
    }
}

// 图片加载为二进制数据
- (Byte *)loadImage:(UIImage *)image {
    CGImageRef spriteImage = image.CGImage;
    size_t width = CGImageGetWidth(spriteImage);
    size_t height = CGImageGetHeight(spriteImage);
    
    Byte * spriteData = (Byte *)calloc(width * height * 4, sizeof(Byte));
    
    CGContextRef context = CGBitmapContextCreate(spriteData, width, height, 8, width *4, CGImageGetColorSpace(spriteImage), kCGImageAlphaPremultipliedLast);
    
    // 纹理翻转
    CGContextTranslateCTM(context, 0, height);
    CGContextScaleCTM(context, 1.0, -1.0);
    
    CGContextDrawImage(context, CGRectMake(0, 0, width, height), spriteImage);
    CFRelease(context);
    
    return spriteData;
}

6.创建渲染管道

**

// 4.创建渲染管道
// 根据.metal里的函数名,使用MTLLibrary创建顶点函数和片元函数
// 从这里可以看出来,MTLLibrary里面包含所有.metal的文件,所以,不同的.metal里面的函数名不能相同
// id <MTLDevice> 创建library、MTLRenderPipelineState、MTLCommandQueue
- (void)createPipeLineState {
    
    // 1.从项目中加载.metal文件,创建一个library
    id <MTLLibrary> library = [self.device newDefaultLibrary];
    // id <MTLDevice> -> id <MTLLibrary>
    
    // 2.从库中MTLLibrary,加载顶点函数
    id <MTLFunction> vertexFunction = [library newFunctionWithName:@"vertexImageShader"];
    
    // 3.从库中MTLLibrary,加载顶点函数
    id <MTLFunction> fragmentFunction = [library newFunctionWithName:@"fragmentImageShader"];
    
    // 4.创建管道渲染管道描述符
    MTLRenderPipelineDescriptor * renderPipeDescriptor = [[MTLRenderPipelineDescriptor alloc] init];
    
    // 5.设置管道顶点函数和片元函数
    renderPipeDescriptor.vertexFunction = vertexFunction;
    renderPipeDescriptor.fragmentFunction = fragmentFunction;
    
    // 6.设置管道描述的关联颜色存储方式
    renderPipeDescriptor.colorAttachments[0].pixelFormat = self.mtkView.colorPixelFormat;

    NSError * error = nil;
    // 7.根据渲染管道描述符 创建渲染管道
    id <MTLRenderPipelineState> renderPipelineState = [self.device newRenderPipelineStateWithDescriptor:renderPipeDescriptor error:&error];
    self.renderPipelineState = renderPipelineState;
    // id <MTLDevice> -> id <MTLRenderPipelineState>
    
    // 8. 创建渲染指令队列
    id <MTLCommandQueue> commondQueue = [self.device newCommandQueue];
    self.commondQueue = commondQueue;
    // id <MTLDevice> -> id <MTLCommandQueue>
}

7.MTKView代理方法实现,每一帧渲染流程实现

**

// MTKViewDelegate
- (void)mtkView:(nonnull MTKView *)view drawableSizeWillChange:(CGSize)size {
    self.viewportSize = (vector_int2){size.width, size.height};
}

// MTKViewDelegate
- (void)drawInMTKView:(nonnull MTKView *)view {
    // 1.为当前渲染的每个渲染传递创建一个新的命令缓冲区
    id <MTLCommandBuffer> commandBuffer = [self.commondQueue commandBuffer];
    
    //指定缓存区名称
    commandBuffer.label = @"EachCommand";
    
    // 2.获取渲染命令编码器 MTLRenderCommandEncoder的描述符
    // currentRenderPassDescriptor描述符包含currentDrawable's的纹理、视图的深度、模板和sample缓冲区和清晰的值。
    // MTLRenderPassDescriptor描述一系列attachments的值,类似GL的FrameBuffer;同时也用来创建MTLRenderCommandEncoder
    MTLRenderPassDescriptor * renderPassDescriptor = view.currentRenderPassDescriptor;
    if (renderPassDescriptor) {
        // 设置默认颜色 背景色
        renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColorMake(1.0, 1.0, 1.0, 1.0f);
        
        // 3.根据描述创建x 渲染命令编码器
        id <MTLRenderCommandEncoder> renderEncoder = [commandBuffer renderCommandEncoderWithDescriptor:renderPassDescriptor];
        
//        typedef struct {
//            double originX, originY, width, height, znear, zfar;
//        } MTLViewport;
        // 4.设置绘制区域
        [renderEncoder setViewport:(MTLViewport){0, 0, self.viewportSize.x, self.viewportSize.y, -1.0, 1.0}];
        
        // 5.设置渲染管道
        [renderEncoder setRenderPipelineState:self.renderPipelineState];
        
        // 6.传递顶点缓存
        [renderEncoder setVertexBuffer:self.vertexBuffer offset:0 atIndex:YYImageVertexInputIndexVertexs];
        
        // 7.传递纹理缓存
        [renderEncoder setFragmentTexture:self.texture atIndex:YYImageTextureIndexBaseTexture];
        
        // 8.绘制
        [renderEncoder drawPrimitives:MTLPrimitiveTypeTriangleStrip vertexStart:0 vertexCount:self.vertexCount];
        
        // 9.命令结束
        [renderEncoder endEncoding];
        
        // 10.显示
        [commandBuffer presentDrawable:view.currentDrawable];
    }
    // 11. 提交
    [commandBuffer commit];
}

8.图片显示模式调整
主要有3种模式,

1.默认大小设置多大就多大

2.图片自适应屏幕不产生拉伸变形

3.全屏且看起来宽高比例正常,其实是略微拉伸了宽度或者高度,只是改变不大,因为如果图片或视频大小不是按照屏幕比例来的就会有形变

**

- (void)setFillMode:(kMetalImageFilterViewFillModeType)fillMode {
    isChangeFillMode = YES;
    _fillMode = fillMode;
    [self resetVertexWithWidth:imageSize.width height:imageSize.height];
    isChangeFillMode = NO;
}

- (void)resetVertexWithWidth:(CGFloat)width height:(CGFloat)height  {
    
    dispatch_async(dispatch_get_main_queue(), ^{
        CGSize inputImageSize = CGSizeMake(width, height);
        CGFloat heightScaling = 1.0, widthScaling = 1.0;
        CGSize currentViewSize = self.bounds.size;
        
        CGRect insetRect = AVMakeRectWithAspectRatioInsideRect(inputImageSize, self.bounds);
        switch(self.fillMode)
        {
            case kMetalImageFilterViewFillModeStretch:
                
            {
                widthScaling = 1.0;
                heightScaling = 1.0;
            };
                break;
                
            case kMetalImageFilterViewFillModePreserveAspectRatio:
            {
                widthScaling = insetRect.size.width / currentViewSize.width;
                heightScaling = insetRect.size.height / currentViewSize.height;
            };
                break;
                
            case kMetalImageFilterViewFillModePreserveAspectRatioAndFill:
            {
                widthScaling = currentViewSize.height / insetRect.size.height;
                heightScaling = currentViewSize.width / insetRect.size.width;
            };
                break;
        }
        
        //        NSLog(@"widthScaling == %lf", widthScaling);
        //        NSLog(@"heightScaling == %lf", heightScaling);
        [self setupVertexsWithWidthScaling:widthScaling heightScaling:heightScaling];
    });
}
2.桥接类

**

/*
 介绍:
 头文件包含了 Metal shaders 与C/OBJC 源之间共享的类型和枚举常数
 */
#ifndef YYImageShaderTypes_h
#define YYImageShaderTypes_h

// 这个simd.h文件里有一些桥接的数据类型
#include <simd/simd.h>

// 存储数据的自定义结构,用于桥接OC和Metal代码
// YYVertex结构体类型

typedef struct {
    // 顶点坐标 4维向量
    vector_float4 position;
    
    // 纹理坐标
    vector_float2 textureCoordinate;
    
} YYVertex;


// 自定义枚举,用于桥接OC和Metal代码
// 顶点的桥接枚举值 YYImageVertexInputIndexVertexs
typedef enum {
    
    YYImageVertexInputIndexVertexs = 0,
    
} YYImageVertexInputIndex;


// 纹理的桥接枚举值 YYImageTextureIndexBaseTexture
typedef enum {
    
    YYImageTextureIndexBaseTexture = 0,
    
} YYImageTextureIndex;


#endif /* YYImageShaderTypes_h */
3.Metal 顶点片元代码实现

1.需要注意,函数修饰符,变量修饰符,地址空间修饰符,桥接或者输出的数据结构类型,索引值,内部的逻辑编码与OpenGL ES非常类似,逻辑一致

**

#include <metal_stdlib>
#import "YYImageShaderTypes.h"
using namespace metal;

// 定义了一个类型为RasterizerData的结构体,里面有一个float4向量和float2向量,其中float4被[[position]]修饰,其表示的变量为顶点

typedef struct {
    // float4 4维向量 clipSpacePosition参数名
    // position 修饰符的表示顶点 语法是[[position]],这是苹果内置的语法和position关键字不能改变
    float4 clipSpacePosition [[position]];
    
    // float2 2维向量  表示纹理
    float2 textureCoordinate;
    
} RasterizerData;

// 顶点函数通过一个自定义的结构体,返回对应的数据,顶点函数的输入参数也可以是自定义结构体

// 顶点函数
// vertex 函数修饰符表示顶点函数,
// RasterizerData返回值类型,
// vertexImageShader函数名
// vertex_id 顶点id修饰符,苹果内置不可变,[[vertex_id]]
// buffer 缓存数据修饰符,苹果内置不可变,YYImageVertexInputIndexVertexs是索引
// [[buffer(YYImageVertexInputIndexVertexs)]]
// constant 变量类型修饰符,表示存储在device区域

vertex RasterizerData vertexImageShader(uint vertexID [[vertex_id]], constant YYVertex * vertexArray [[buffer(YYImageVertexInputIndexVertexs)]]) {
    
    RasterizerData outData;
    
    // 获取YYVertex里面的顶点坐标和纹理坐标
    outData.clipSpacePosition = vertexArray[vertexID].position;
    outData.textureCoordinate = vertexArray[vertexID].textureCoordinate;
    
    return outData;
}

// 片元函数
// fragment 函数修饰符表示片元函数 float4 返回值类型->颜色RGBA fragmentImageShader 函数名
// RasterizerData 参数类型 input 变量名
// [[stage_in] stage_in表示这个数据来自光栅化。(光栅化是顶点处理之后的步骤,业务层无法修改)
// texture2d 类型表示纹理 baseTexture 变量名
// [[ texture(index)]] 纹理修饰符
// 可以加索引 [[ texture(0)]]纹理0, [[ texture(1)]]纹理1
// YYImageTextureIndexBaseTexture表示纹理索引

fragment float4 fragmentImageShader(RasterizerData input [[stage_in]], texture2d<half> baseTexture [[ texture (YYImageTextureIndexBaseTexture) ]]) {
    
    // constexpr 修饰符
    // sampler 采样器
    // textureSampler 采样器变量名
    // mag_filter:: linear, min_filter:: linear 设置放大缩小过滤方式
    constexpr sampler textureSampler(mag_filter:: linear, min_filter:: linear);
    
    // 得到纹理对应位置的颜色
    half4 color = baseTexture.sample(textureSampler, input.textureCoordinate);
    
    // 返回颜色值
    return float4(color);
}