十六、 Metal - Metal实现视频处理

音视频开发:OpenGL + OpenGL ES + Metal 系列文章汇总

在音视频开发中很重要的一部分是视频的处理,因此本文针对Metal对实时录像渲染和本地视频文件渲染进行分析,并且分析YUV的实现逻辑。

主要内容:

  1. 视频采集(了解)
  2. 实时录像渲染
  3. YUV的实现逻辑
  4. 本地视频文件的渲染

Metal的渲染流程、片元函数顶点函数的使用,纹理的设置等操作在前三篇博客中已经有专门解读,这里不再说明,只是增加对视频帧的处理的分析

1、实时录像渲染

1.1 简单介绍

案例地址: 视频渲染

主要学习内容:

  1. 采集视频过程
  2. 视频帧转化为纹理的过程
  3. 对纹理的渲染

1.2 视频采集

视频的采集使用到了AVFoundation框架,而这个框架不在本文中着重详解,因此这里仅简单说明,后续会专门写博客解读AVFoundation,可持续关注。

过程:

  1. 创建采集会话captureSession,用来管理采集过程
  2. 添加输入对象
    1. 获取摄像头
    2. 先将摄像头对象转换为Session可使用的AVCaptureDeviceInput对象,也就是输入对象
    3. 将输入对象添加到会话中
  3. 添加输出对象
    1. 创建输出对象
    2. 设置是否丢弃帧,颜色格式,设置代理
    3. 将输出对象添加到会话中
  4. 创建输入输出连接
    1. 创建视频连接对象
    2. 设置视频方向
  5. 开始采集

代码:

- (void)setupCaptureSession {
    
    //1.创建mCaptureSession
    self.mCaptureSession = [[AVCaptureSession alloc] init];
    //设置视频采集的分辨率
    self.mCaptureSession.sessionPreset = AVCaptureSessionPreset1920x1080;
  
    //2.创建串行队列
    self.mProcessQueue = dispatch_queue_create("mProcessQueue", DISPATCH_QUEUE_SERIAL);
   
    //3.获取摄像头设备(前置/后置摄像头设备)
    //因为有多个摄像头,所以需要判断一下爱
    NSArray *devices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo];
    AVCaptureDevice *inputCamera = nil;
    //循环设备数组,找到后置摄像头.设置为当前inputCamera
    for (AVCaptureDevice *device in devices) {
        if ([device position] == AVCaptureDevicePositionBack) {
            inputCamera = device;
        }
    }
    
    //4.将AVCaptureDevice 转换为AVCaptureDeviceInput
    self.mCaptureDeviceInput = [[AVCaptureDeviceInput alloc] initWithDevice:inputCamera error:nil];
    
    //5. 将设备添加到mCaptureSession中
    if ([self.mCaptureSession canAddInput:self.mCaptureDeviceInput]) {
        [self.mCaptureSession addInput:self.mCaptureDeviceInput];
    }
    
    //输出的连接
    //6.创建AVCaptureVideoDataOutput 对象
    self.mCaptureDeviceOutput = [[AVCaptureVideoDataOutput alloc] init];
    
    /*设置视频帧延迟到底时是否丢弃数据.
     YES: 处理现有帧的调度队列在captureOutput:didOutputSampleBuffer:FromConnection:Delegate方法中被阻止时,对象会立即丢弃捕获的帧。
     NO: 在丢弃新帧之前,允许委托有更多的时间处理旧帧,但这样可能会内存增加.
     */
    [self.mCaptureDeviceOutput setAlwaysDiscardsLateVideoFrames:NO];
    
    //这里设置格式为BGRA,而不用YUV的颜色空间,避免使用Shader转换
    //注意:这里必须和后面CVMetalTextureCacheCreateTextureFromImage 保存图像像素存储格式保持一致.否则视频会出现异常现象.
    //每一个像素点使用的颜色保存格式
    [self.mCaptureDeviceOutput setVideoSettings:[NSDictionary dictionaryWithObject:[NSNumber numberWithInt:kCVPixelFormatType_32BGRA] forKey:(id)kCVPixelBufferPixelFormatTypeKey]];
    
    //设置视频捕捉输出的代理方法
    //添加一个代理方法,当采集到视频数据需要输出时就会调用代理方法
    //输出到这个队列中
    [self.mCaptureDeviceOutput setSampleBufferDelegate:self queue:self.mProcessQueue];
    
    //7.添加输出
    if ([self.mCaptureSession canAddOutput:self.mCaptureDeviceOutput]) {
        [self.mCaptureSession addOutput:self.mCaptureDeviceOutput];
    }
    
    //8.输入与输出链接
    //视频连接对象
    AVCaptureConnection *connection = [self.mCaptureDeviceOutput connectionWithMediaType:AVMediaTypeVideo];
    
    //9.设置视频方向
    //注意: 一定要设置视频方向.否则视频会是朝向异常的.
    [connection setVideoOrientation:AVCaptureVideoOrientationPortrait];
    
    //10.开始捕捉
    [self.mCaptureSession startRunning];
    
}

1.3 视频帧转化为纹理

在捕捉视频时,每一帧都会回调captureOutput这个方法,它是视频采集回调方法。
我们就可以在这个方法里将视频帧转化为纹理。

过程:

  1. 从sampleBuffer 获取视频像素缓存区对象
  2. 获取捕捉视频的宽和高
  3. 将获取到的视频帧转换为纹理数据
  4. 通过纹理数据得到纹理对象

代码:

- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection {
    
    //1.从sampleBuffer 获取视频像素缓存区对象
    CVPixelBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
   
    //2.获取捕捉视频的宽和高
    size_t width = CVPixelBufferGetWidth(pixelBuffer);
    size_t height = CVPixelBufferGetHeight(pixelBuffer);
    
    //将获取到的视频帧转换为纹理
    /*3. 根据视频像素缓存区 创建 Metal 纹理缓存区
     CVReturn CVMetalTextureCacheCreateTextureFromImage(CFAllocatorRef allocator,                         CVMetalTextureCacheRef textureCache,
     CVImageBufferRef sourceImage,
     CFDictionaryRef textureAttributes,
     MTLPixelFormat pixelFormat,
     size_t width,
     size_t height,
     size_t planeIndex,
     CVMetalTextureRef  *textureOut);
     
     功能: 从现有图像缓冲区创建核心视频Metal纹理缓冲区。
     参数1: allocator 内存分配器,默认kCFAllocatorDefault
     参数2: textureCache 纹理缓存区对象
     参数3: sourceImage 视频图像缓冲区
     参数4: textureAttributes 纹理参数字典.默认为NULL
     参数5: pixelFormat 图像缓存区数据的Metal 像素格式常量.注意如果MTLPixelFormatBGRA8Unorm和摄像头采集时设置的颜色格式不一致,则会出现图像异常的情况;
     参数6: width,纹理图像的宽度(像素)
     参数7: height,纹理图像的高度(像素)
     参数8: planeIndex.如果图像缓冲区是平面的,则为映射纹理数据的平面索引。对于非平面图像缓冲区忽略。
     参数9: textureOut,返回时,返回创建的Metal纹理缓冲区。
     
     // Mapping a BGRA buffer:
     CVMetalTextureCacheCreateTextureFromImage(kCFAllocatorDefault, textureCache, pixelBuffer, NULL, MTLPixelFormatBGRA8Unorm, width, height, 0, &outTexture);
     
     // Mapping the luma plane of a 420v buffer:
     CVMetalTextureCacheCreateTextureFromImage(kCFAllocatorDefault, textureCache, pixelBuffer, NULL, MTLPixelFormatR8Unorm, width, height, 0, &outTexture);
     
     // Mapping the chroma plane of a 420v buffer as a source texture:
     CVMetalTextureCacheCreateTextureFromImage(kCFAllocatorDefault, textureCache, pixelBuffer, NULL, MTLPixelFormatRG8Unorm width/2, height/2, 1, &outTexture);
     
     // Mapping a yuvs buffer as a source texture (note: yuvs/f and 2vuy are unpacked and resampled -- not colorspace converted)
     CVMetalTextureCacheCreateTextureFromImage(kCFAllocatorDefault, textureCache, pixelBuffer, NULL, MTLPixelFormatGBGR422, width, height, 1, &outTexture);
     
     */
    CVMetalTextureRef tmpTexture = NULL;
    CVReturn status = CVMetalTextureCacheCreateTextureFromImage(kCFAllocatorDefault, self.textureCache, pixelBuffer, NULL, MTLPixelFormatBGRA8Unorm, width, height, 0, &tmpTexture);
    
    //4.判断tmpTexture 是否创建成功
    if(status == kCVReturnSuccess)
    {
        //5.设置可绘制纹理的当前大小。
        self.mtkView.drawableSize = CGSizeMake(width, height);
        //6.返回纹理缓冲区的Metal纹理对象。
        self.texture = CVMetalTextureGetTexture(tmpTexture);
        //7.使用完毕,则释放tmpTexture
        CFRelease(tmpTexture);
    }
}

重要API

1、CMSampleBufferRef -> CVPixelBufferRef

API:CVPixelBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer)

从sampleBuffer 获取视频像素缓存区对象

2、CVPixelBufferRef -> CVMetalTextureRef

API:CVReturn status = CVMetalTextureCacheCreateTextureFromImage(kCFAllocatorDefault, self.textureCache, pixelBuffer, NULL, MTLPixelFormatBGRA8Unorm, width, height, 0, &tmpTexture);

功能: 从现有图像缓冲区创建核心视频Metal纹理缓冲区。
参数1: allocator 内存分配器,默认kCFAllocatorDefault
参数2: textureCache 纹理缓存区对象
参数3: sourceImage 视频图像缓冲区
参数4: textureAttributes 纹理参数字典.默认为NULL
参数5: pixelFormat 图像缓存区数据的Metal 像素格式常量.注意如果MTLPixelFormatBGRA8Unorm和摄像头采集时设置的颜色格式不一致,则会出现图像异常的情况;
参数6: width,纹理图像的宽度(像素)
参数7: height,纹理图像的高度(像素)
参数8: planeIndex.如果图像缓冲区是平面的,则为映射纹理数据的平面索引。对于非平面图像缓冲区忽略。
参数9: textureOut,返回时,返回创建的Metal纹理缓冲区。

3、CVMetalTextureRef -> MTLTexture

API:self.texture = CVMetalTextureGetTexture(tmpTexture);
通过纹理缓冲区得到Metal纹理对象

注意: sampleBuffer表示采集到的原始数据,它是CMSampleBufferRef类型,这个类型就是获取到的帧数据。我们对数据的处理最原始的数据就是它

1.4 对纹理的渲染

渲染本身就是传递纹理数据到metal文件中,在上一篇博客中已经详细解读,但是此处使用到了Metal内置的滤镜,所以有必要再说明一下内置滤镜的使用。

过程:

  1. 创建命令缓存区
  2. 将MTKView的纹理作为目标渲染纹理(即将纹理绘制到当前view的可绘制界面的纹理上)
  3. 设置高斯模糊滤镜
  4. 对纹理进行滤镜设置
  5. 添加“展示显示的内容”的命令
  6. 提交命令

代码:

- (void)drawInMTKView:(MTKView *)view {
  
    //1.判断是否获取了AVFoundation 采集的纹理数据
    if (self.texture) {
        
        //2.创建指令缓冲
        id commandBuffer = [self.commandQueue commandBuffer];
        
        //3.将MTKView 作为目标渲染纹理
        id drawingTexture = view.currentDrawable.texture;
        
        //4.设置滤镜
        /*
         MetalPerformanceShaders是Metal的一个集成库,有一些滤镜处理的Metal实现;
         MPSImageGaussianBlur 高斯模糊处理;
         */
       
        //创建高斯滤镜处理filter
        //注意:sigma值可以修改,sigma值越高图像越模糊;
        MPSImageGaussianBlur *filter = [[MPSImageGaussianBlur alloc] initWithDevice:self.mtkView.device sigma:1];
        
        //5.MPSImageGaussianBlur以一个Metal纹理作为输入,以一个Metal纹理作为输出;
        //输入:摄像头采集的图像 self.texture
        //输出:创建的纹理 drawingTexture(其实就是view.currentDrawable.texture)
        [filter encodeToCommandBuffer:commandBuffer sourceTexture:self.texture destinationTexture:drawingTexture];
        
        //6.展示显示的内容
        [commandBuffer presentDrawable:view.currentDrawable];
        
        //7.提交命令
        [commandBuffer commit];
        
        //8.清空当前纹理,准备下一次的纹理数据读取.
        self.texture = NULL;
    }
}

内置滤镜的使用就这三步,看下各自的API

  1. id drawingTexture = view.currentDrawable.texture;

这是获取到当前视图的可绘制界面的纹理对象。

  1. MPSImageGaussianBlur *filter = [[MPSImageGaussianBlur alloc] initWithDevice:self.mtkView.device sigma:1];
    • 创建一个高斯滤镜处理器filter
    • sigma值可以修改,sigma值越高图像越模糊
  2. [filter encodeToCommandBuffer:commandBuffer sourceTexture:self.texture destinationTexture:drawingTexture];
    • 将滤镜的命令添加到命令缓存区中
    • 滤镜的命令以一个初始纹理作为输入
    • 以一个结果纹理作为输出,这个结果纹理就是上面创建的当前视图的纹理对象。

2、YUV

2.1 YUV的认识

YUV颜色编码格式是作为视频颜色的标准格式。
YUV颜色编码采用明亮度和色度来指定颜色,没有采用三原色。其中Y就表示明亮度,U和V表示色度,其中U是色调,V是饱和度。
Y一定要存在,可以没有UV信息,如果没有UV信息,也可以显示图片,但是图片是黑白的。

RGB颜色编码格式
用RGB表示的图像中,每个像素点都有红、绿、蓝三个原色,每种颜色都占用8 bit,即一个字节,所以一个像素点占用24bit,即3个字节,如下图所示

RGB.png

问:为什么使用YUV,而不使用RGB?
答:通过RGB颜色编码格式的图片,一个像素点会占用3*8 = 24bit,一张1280 *720图片需要1280 *720 * 24 = 2.63MB,而一个视频由每帧画面组成,所以会占用更多的内存,同时如果帧率是60fps,则播放该视频所使用的带宽将会非常惊人。因此不可以使用RGB颜色编码格式。而使用YUV可以通过4:2:0的采样方式,减少一帧画面的存储空间。这样既减少了内存占用,又节省了带宽,所以需要使用YUV。

YUV相较RGB的优势:

  1. 降低占用的存储空间
  2. 显示画面时,节省带宽

2.2 YUV的采样方式

对于YUV来说,每个像素点的UV分量可以根据不同的采样方式省略掉,但是个像素点的Y一定要存在,因为它表示明亮度。而UV是用来表示色度的,简单来说就是颜色值,而在一个画面中相邻的像素点的色值肉眼无法清晰判断,所以可以让某些像素点的UV分量省略掉,而去借用相邻像素点的UV分量。

那么让哪些像素点的UV分量省略,再去借用哪些相邻像素点的UV分量呢?由此引出了不同的采样方式。
有4:4:4,4:2:2,4:2:0,三总,我们用的是4:2:0,但是为了更好的理解这种采样方式的实现,前两种也进行分析

2.2.1 YUV采样格式 - YUV4:4:4

YUV4:4:4 采样格式,表示其中的Y、U、V三个分量的采样比例是相同的,也就是三个分量全部采样。
很明显,这种采样方式,一个像素点也会有三个字节,所以与RGB颜色编码格式相比,并没有减少存储空间,也没有节省带宽。

示意图:


YUV4:4:4

总结:YUV4:4:4采样格式,YUV三个分量全部采样,没有减少存储空间,没有节省带宽。

2.2.2 YUV采样格式 - YUV4:2:2

YUV4:2:2采样格式表示Y分量的采样量是UV分量的2倍,即Y分量与UV分量是按照2:1的比例采样。每个像素点的Y分量都要采样,但是UV分量需要间隔一个像素点进行采样。也就是一个像素点会采样Y分量,但是UV分量只采其中的一个。

示意图:


YUV4:2:2

总结:YUV4:2:2采样格式,左右两个像素点共用一套UV分量,每个像素点都会采样Y分量,但是一个像素点只采样UV分量中的一个,且是间隔采样。

2.2.2 YUV采样格式 - YUV4:2:0

YUV4:2:0采样格式表示4个像素点中,这个格式的写法表示有4个Y分量,2个U(V)分量,而V(U)分量不采样。

问题:
其中一个U或V分量不采样,那么怎么获取呢?

解答:
这个分量可以通过下一行来获取。

因此YUV4:2:0采样,并不是指只采样U分量⽽不采样V分量。⽽是指,在每⼀⾏扫描时,只扫描⼀种⾊度分量(U或者V),和Y分量按照2:1的⽅式采样。⽐如,第⼀⾏扫描时,YU按照2:1的⽅式采样,那么第⼆⾏扫描时,YV分量按照2:1的⽅式采样。对于每个⾊度分量来说,它的⽔平⽅向和竖直⽅向的采样和Y分量相⽐都是2:1。假设第⼀⾏扫描了U分量,第⼆⾏扫描了V分量,那么需要扫描两行才能组成完整的UV分量。

示意图:

YUV4:2:0

总结:YUV4:2:0采样格式,上下左右4个像素点共用一套UV分量。每个像素点的Y分量都会采样,但是U/V分量均会到相邻的上下像素点进行借用。

2.3 RGB-YUV颜色编码转换

对于图像显示器来说,它是通过RGB模型来显示图像的,⽽在传输图像数据时⼜是使⽤YUV模型,这是因为YUV模型可以节省带宽。因此就需要采集图像时将RGB模型转换到YUV模型,显示时再将YUV模型转换为RGB模型

RGB 到 YUV的转换,其实就是将图像所有像素点的R、G、B分量 转换到 Y、U、V分量,其对应的转换公式如下

转换公式了解即可,不用自己设计。

//YUV和RGB的转换:
Y = 0.299 R + 0.587 G + 0.114 B
U = -0.1687 R - 0.3313 G + 0.5 B + 128
V = 0.5 R - 0.4187 G - 0.0813 B + 128

R = Y + 1.402 (V-128)
G= Y - 0.34414 (U-128) - 0.71414 (V-128)
B= Y + 1.772 (U-128)

3、本地视频文件的渲染

3.1 简单介绍

案例地址: 本地视频文件渲染

效果:

实现思路:

  1. 自定义一个CCAssetReader工具类,用来读取mov/mp4视频文件,基本功能使用AVFoundation实现的
  2. 将读取到的视频帧转换为纹理数据
  3. 传递纹理对象到片元函数,
  4. 在片元函数中将颜色编码格式由YUV转换为RGB,显示到屏幕上。

过程:

  1. 初始化
  2. 图形绘制
  3. 视频帧转换纹理
  4. 片元函数中实现YUV转化RGB

重点学习内容:

  1. YUV转化RGB值
  2. 视频帧转化纹理

其他所有的内容前文都已经熟悉了,这里新增的只有YUV转化RGB的过程,以及CCAssetReader的简单使用。
CCAssetReader涉及AVFoundation的使用,以后会详细讲解,这里直接使用,并不会解读代码。

3.2 准备工作

  • 创建OC与Metal文件共用的.h文件(该文件用作数据传递)
  • 创建CCAssetReader工具类
3.2.1 共用文件

顶点数据,包含顶点坐标和纹理坐标

//顶点数据结构
typedef struct
{
    //顶点坐标(x,y,z,w)
    vector_float4 position;
    //纹理坐标(s,t)
    vector_float2 textureCoordinate;
} CCVertex;

转换矩阵,用作YUV转换RGB,包括颜色转换矩阵和偏移量

//转换矩阵
typedef struct {
    //三维矩阵
    matrix_float3x3 matrix;
    //偏移量
    vector_float3 offset;
} CCConvertMatrix;

输入索引,用作CPU传入到GPU的数据的索引

//顶点函数输入索引
typedef enum CCVertexInputIndex
{
    CCVertexInputIndexVertices     = 0,
} CCVertexInputIndex;

//片元函数缓存区索引
typedef enum CCFragmentBufferIndex
{
    CCFragmentInputIndexMatrix     = 0,
} CCFragmentBufferIndex;

//片元函数纹理索引
typedef enum CCFragmentTextureIndex
{
    //Y纹理
    CCFragmentTextureIndexTextureY     = 0,
    //UV纹理
    CCFragmentTextureIndexTextureUV     = 1,
} CCFragmentTextureIndex;
3.2.2 CCAssetReader工具类

AVAssetReader是AVFoundation中的一个读取器对象,主要有以下两种功能:

  • 直接从存储中读取原始未解码的媒体样本,获取解码为可渲染形式的样本:从mp4文件中拿到h264,并对其进行解码拿到可渲染的样本
  • 混合资产的多个音轨,并使用和组合多个视频音轨

它的功能来自于AVFoundation,因此不再详细解读,仅了解其在项目中的作用即可

作用:
从mov/mp4视频文件读取到CMSampleBufferRef视频帧数据。

3.2 初始化

渲染初始化前文已经写过多编了,这里仅做粗略解读

过程:

  1. MTKView初始化
  2. CCAssetReader设置
  3. 渲染管道设置
  4. 顶点数据设置
  5. 转换矩阵设置
3.2.1 MTKView初始化

MTKView就是Mteal中用来渲染画面的画板

//获取到mtkView,并赋值devide
-(void)setupMTKView{
//1.初始化mtkView
self.mtkView = [[MTKView alloc] initWithFrame:self.view.bounds];
// 获取默认的device
self.mtkView.device = MTLCreateSystemDefaultDevice();
//设置self.view = self.mtkView;
self.view = self.mtkView;
//设置代理
self.mtkView.delegate = self;
//获取视口size
self.viewportSize = (vector_uint2){self.mtkView.drawableSize.width, self.mtkView.drawableSize.height};
}

注意:

  1. 必须要给view设置device,device是所有命令的开始
  2. 代理的设置可以使用MTKView的两个代理方法drawInMTKView和- (void)mtkView:(MTKView *)view drawableSizeWillChange:(CGSize)size。
3.2.2 CCAssetReader设置

过程:
1、获取视频文件路径
2、获取CCAssetReader
3、创建纹理缓存区(因为一个视频有很多视频帧,所以需要创建一个缓存区专门用来存放纹理)

代码:

-(void)setupCCAsset{
    
    //注意CCAssetReader 支持MOV/MP4文件都可以
    //1.视频文件路径
    //NSURL *url = [[NSBundle mainBundle] URLForResource:@"kun" withExtension:@"mov"];
    NSURL *url = [[NSBundle mainBundle] URLForResource:@"kun2" withExtension:@"mp4"];
    
    //2.初始化CCAssetReader
    self.reader = [[CCAssetReader alloc] initWithUrl:url];
    
    //3._textureCache的创建(通过CoreVideo提供给CPU/GPU高速缓存通道读取纹理数据)
    CVMetalTextureCacheCreate(NULL, NULL, self.mtkView.device, NULL, &_textureCache);
    
}

注意:_textureCache是创建在显存中的纹理缓存区,可以进行高速缓存通道读取

3.2.3 渲染管道设置

过程:
1、获取Metal文件的片元函数和顶点函数
2、创建渲染管道描述类,添加着色器
3、通过渲染管道描述类创建渲染管道
4、创建命令队列

代码:

-(void)setupPipeline {
    
    //1 获取.metal
    /*
     newDefaultLibrary: 默认一个metal 文件时,推荐使用
     newLibraryWithFile:error: 从Library 指定读取metal 文件
     newLibraryWithData:error: 从Data 中获取metal 文件
     */
    id defaultLibrary = [self.mtkView.device newDefaultLibrary];
    // 顶点shader,vertexShader是函数名
    id vertexFunction = [defaultLibrary newFunctionWithName:@"vertexShader"];
    // 片元shader,samplingShader是函数名
    id fragmentFunction = [defaultLibrary newFunctionWithName:@"samplingShader"];
    
    //2.渲染管道描述信息类
    MTLRenderPipelineDescriptor *pipelineStateDescriptor = [[MTLRenderPipelineDescriptor alloc] init];
    //设置vertexFunction
    pipelineStateDescriptor.vertexFunction = vertexFunction;
    //设置fragmentFunction
    pipelineStateDescriptor.fragmentFunction = fragmentFunction;
    // 设置颜色格式
    pipelineStateDescriptor.colorAttachments[0].pixelFormat = self.mtkView.colorPixelFormat;
    
    //3.初始化渲染管道根据渲染管道描述信息
    // 创建图形渲染管道,耗性能操作不宜频繁调用
    self.pipelineState = [self.mtkView.device newRenderPipelineStateWithDescriptor:pipelineStateDescriptor
                                                                             error:NULL];
    
    //4.CommandQueue是渲染指令队列,保证渲染指令有序地提交到GPU
    self.commandQueue = [self.mtkView.device newCommandQueue];
}

注意:

  1. 这个完全是固定流程,记住API就可以了
  2. 命令队列在哪里创建都可以,只要在创建命令缓存区之前创建好就可以。
3.2.4 顶点数据设置

过程:

  1. 创建顶点坐标、纹理坐标
  2. 添加到顶点缓存区中
  3. 计算顶点个数

代码:

- (void)setupVertex {
    
    //1.顶点坐标(x,y,z,w);纹理坐标(x,y)
    //注意: 为了让视频全屏铺满,所以顶点大小均设置[-1,1]
    static const CCVertex quadVertices[] =
    {   // 顶点坐标,分别是x、y、z、w;    纹理坐标,x、y;
        { {  1.0, -1.0, 0.0, 1.0 },  { 1.f, 1.f } },
        { { -1.0, -1.0, 0.0, 1.0 },  { 0.f, 1.f } },
        { { -1.0,  1.0, 0.0, 1.0 },  { 0.f, 0.f } },
        
        { {  1.0, -1.0, 0.0, 1.0 },  { 1.f, 1.f } },
        { { -1.0,  1.0, 0.0, 1.0 },  { 0.f, 0.f } },
        { {  1.0,  1.0, 0.0, 1.0 },  { 1.f, 0.f } },
    };
    
    //2.创建顶点缓存区
    self.vertices = [self.mtkView.device newBufferWithBytes:quadVertices
                                                     length:sizeof(quadVertices)
                                                    options:MTLResourceStorageModeShared];
    //3.计算顶点个数
    self.numVertices = sizeof(quadVertices) / sizeof(CCVertex);
}
3.2.5 转换矩阵设置

过程:
1、创建转换矩阵和偏移量
2、转化矩阵和偏移量存储到显存中,以供片元着色器取用

代码:

- (void)setupMatrix {
    
    //1.转化矩阵
    // BT.601, which is the standard for SDTV.
    matrix_float3x3 kColorConversion601DefaultMatrix = (matrix_float3x3){
        (simd_float3){1.164,  1.164, 1.164},
        (simd_float3){0.0, -0.392, 2.017},
        (simd_float3){1.596, -0.813,   0.0},
    };
    
    // BT.601 full range
    matrix_float3x3 kColorConversion601FullRangeMatrix = (matrix_float3x3){
        (simd_float3){1.0,    1.0,    1.0},
        (simd_float3){0.0,    -0.343, 1.765},
        (simd_float3){1.4,    -0.711, 0.0},
    };
   
    // BT.709, which is the standard for HDTV.
    matrix_float3x3 kColorConversion709DefaultMatrix[] = {
        (simd_float3){1.164,  1.164, 1.164},
        (simd_float3){0.0, -0.213, 2.112},
        (simd_float3){1.793, -0.533,   0.0},
    };
    
    //2.偏移量
    vector_float3 kColorConversion601FullRangeOffset = (vector_float3){ -(16.0/255.0), -0.5, -0.5};
    
    //3.创建转化矩阵结构体.
    CCConvertMatrix matrix;
    //设置转化矩阵
    /*
     kColorConversion601DefaultMatrix;
     kColorConversion601FullRangeMatrix;
     kColorConversion709DefaultMatrix;
     */
    matrix.matrix = kColorConversion601FullRangeMatrix;
    //设置offset偏移量
    matrix.offset = kColorConversion601FullRangeOffset;
    
    //4.创建转换矩阵缓存区.
    self.convertMatrix = [self.mtkView.device newBufferWithBytes:&matrix
                                                        length:sizeof(CCConvertMatrix)
                                                options:MTLResourceStorageModeShared];
}

注意:

  1. 转化矩阵有多种,我们选一种就可以,具体的计算也不用关注
  2. 存储到显存中更方便着色器的取用

3.3 渲染图形

正常的渲染流程在十三、Metal - 初探中已经有详细解读,这里不再说明。

只增加了两个数据的传递,纹理数据和转换矩阵

代码:

- (void)drawInMTKView:(MTKView *)view {
  
    //1.每次渲染都要单独创建一个CommandBuffer
    id commandBuffer = [self.commandQueue commandBuffer];
    //获取渲染描述信息
    MTLRenderPassDescriptor *renderPassDescriptor = view.currentRenderPassDescriptor;
   
    //2. 从CCAssetReader中读取图像数据
    CMSampleBufferRef sampleBuffer = [self.reader readBuffer];
    
    //3.判断renderPassDescriptor 和 sampleBuffer 是否已经获取到了?
    if(renderPassDescriptor && sampleBuffer)
    {
        //4.设置renderPassDescriptor中颜色附着(默认背景色)
        renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColorMake(0.0, 0.5, 0.5, 1.0f);
        
        //5.根据渲染描述信息创建渲染命令编码器
        id renderEncoder = [commandBuffer renderCommandEncoderWithDescriptor:renderPassDescriptor];
        
        //6.设置视口大小(显示区域)
        [renderEncoder setViewport:(MTLViewport){0.0, 0.0, self.viewportSize.x, self.viewportSize.y, -1.0, 1.0 }];
        
        //7.为渲染编码器设置渲染管道
        [renderEncoder setRenderPipelineState:self.pipelineState];
        
        //8.设置顶点缓存区
        [renderEncoder setVertexBuffer:self.vertices
                                offset:0
                               atIndex:CCVertexInputIndexVertices];
        
        //9.设置纹理(将sampleBuffer数据 设置到renderEncoder 中)
        [self setupTextureWithEncoder:renderEncoder buffer:sampleBuffer];
        
        //10.设置片元函数转化矩阵
        [renderEncoder setFragmentBuffer:self.convertMatrix
                                  offset:0
                                 atIndex:CCFragmentInputIndexMatrix];
        
        //11.开始绘制
        [renderEncoder drawPrimitives:MTLPrimitiveTypeTriangle
                          vertexStart:0
                          vertexCount:self.numVertices];
        
        //12.结束编码
        [renderEncoder endEncoding];
        
        //13.显示
        [commandBuffer presentDrawable:view.currentDrawable];
    }
    
    //14.提交命令
    [commandBuffer commit];
    
}

3.4 视频帧转化纹理

过程:

  1. 从CMSampleBuffer读取CVPixelBuffer,
  2. 根据视频像素缓存区 创建 Metal 纹理缓存区
  3. 纹理缓存区转换为纹理对象
  4. 向片元函数设置纹理

代码:

- (void)setupTextureWithEncoder:(id)encoder buffer:(CMSampleBufferRef)sampleBuffer {
    
    //1.从CMSampleBuffer读取CVPixelBuffer,
    CVPixelBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
    
    id textureY = nil;
    id textureUV = nil;
   
    //textureY 设置
    {
        //2.获取纹理的宽高
        size_t width = CVPixelBufferGetWidthOfPlane(pixelBuffer, 0);
        size_t height = CVPixelBufferGetHeightOfPlane(pixelBuffer, 0);
        
        //3.像素格式:普通格式,包含一个8位规范化的无符号整数组件。
        MTLPixelFormat pixelFormat = MTLPixelFormatR8Unorm;
        
        //4.创建CoreVideo的Metal纹理
        CVMetalTextureRef texture = NULL;
        
        /*5. 根据视频像素缓存区 创建 Metal 纹理缓存区
         CVReturn CVMetalTextureCacheCreateTextureFromImage(CFAllocatorRef allocator,
         CVMetalTextureCacheRef textureCache,
         CVImageBufferRef sourceImage,
         CFDictionaryRef textureAttributes,
         MTLPixelFormat pixelFormat,
         size_t width,
         size_t height,
         size_t planeIndex,
         CVMetalTextureRef  *textureOut);
         
         功能: 从现有图像缓冲区创建核心视频Metal纹理缓冲区。
         参数1: allocator 内存分配器,默认kCFAllocatorDefault
         参数2: textureCache 纹理缓存区对象
         参数3: sourceImage 视频图像缓冲区
         参数4: textureAttributes 纹理参数字典.默认为NULL
         参数5: pixelFormat 图像缓存区数据的Metal 像素格式常量.注意如果MTLPixelFormatBGRA8Unorm和摄像头采集时设置的颜色格式不一致,则会出现图像异常的情况;
         参数6: width,纹理图像的宽度(像素)
         参数7: height,纹理图像的高度(像素)
         参数8: planeIndex.如果图像缓冲区是平面的,则为映射纹理数据的平面索引。对于非平面图像缓冲区忽略。
         参数9: textureOut,返回时,返回创建的Metal纹理缓冲区。
         */
        CVReturn status = CVMetalTextureCacheCreateTextureFromImage(NULL, self.textureCache, pixelBuffer, NULL, pixelFormat, width, height, 0, &texture);
        
        //6.判断textureCache 是否创建成功
        if(status == kCVReturnSuccess)
        {
            //7.转成Metal用的纹理
            textureY = CVMetalTextureGetTexture(texture);
           
            //8.使用完毕释放
            CFRelease(texture);
        }
    }
    
    //9.textureUV 设置(同理,参考于textureY 设置)
    {
        size_t width = CVPixelBufferGetWidthOfPlane(pixelBuffer, 1);
        size_t height = CVPixelBufferGetHeightOfPlane(pixelBuffer, 1);
        MTLPixelFormat pixelFormat = MTLPixelFormatRG8Unorm;
        CVMetalTextureRef texture = NULL;
        CVReturn status = CVMetalTextureCacheCreateTextureFromImage(NULL, self.textureCache, pixelBuffer, NULL, pixelFormat, width, height, 1, &texture);
        if(status == kCVReturnSuccess)
        {
            textureUV = CVMetalTextureGetTexture(texture);
            CFRelease(texture);
        }
    }
    
    //10.判断textureY 和 textureUV 是否读取成功
    if(textureY != nil && textureUV != nil)
    {
        //11.向片元函数设置textureY 纹理
        [encoder setFragmentTexture:textureY atIndex:CCFragmentTextureIndexTextureY];
        //12.向片元函数设置textureUV 纹理
        [encoder setFragmentTexture:textureUV atIndex:CCFragmentTextureIndexTextureUV];
    }
    
    //13.使用完毕,则将sampleBuffer 及时释放
    CFRelease(sampleBuffer); 
}

注意:

  1. 最原始的数据帧是CMSampleBufferRef
  2. CVPixelBufferRef是视频像素缓存区,存储有这一个帧的所有像素
  3. CVMetalTextureCacheRef是用来创建和管理纹理的纹理缓存对象
  4. CVPixelBufferRef和CVMetalTextureCacheRef一起创建出存储有纹理数据的纹理缓存区
  5. 得到的CVMetalTextureRef纹理缓存区需要转换为Metal中的纹理对象,就可以传递给片元函数使用了。

3.5 着色器的实现

顶点着色器:

//RasterizerData 返回数据类型->片元函数
// vertex_id是顶点shader每次处理的index,用于定位当前的顶点
// buffer表明是缓存数据,0是索引
vertex RasterizerData
vertexShader(uint vertexID [[ vertex_id ]],
             constant CCVertex *vertexArray [[ buffer(CCVertexInputIndexVertices) ]])
{
    RasterizerData out;
    //顶点坐标
    out.clipSpacePosition = vertexArray[vertexID].position;
    //纹理坐标
    out.textureCoordinate = vertexArray[vertexID].textureCoordinate;
    return out;
}

片元着色器

// stage_in表示这个数据来自光栅化。(光栅化是顶点处理之后的步骤,业务层无法修改)
// texture表明是纹理数据,CCFragmentTextureIndexTextureY是索引
// texture表明是纹理数据,CCFragmentTextureIndexTextureUV是索引
// buffer表明是缓存数据, CCFragmentInputIndexMatrix是索引
fragment float4
samplingShader(RasterizerData input [[stage_in]],
               texture2d textureY [[ texture(CCFragmentTextureIndexTextureY) ]],
               texture2d textureUV [[ texture(CCFragmentTextureIndexTextureUV) ]],
               constant CCConvertMatrix *convertMatrix [[ buffer(CCFragmentInputIndexMatrix) ]])
{
    //1.获取纹理采样器
    constexpr sampler textureSampler (mag_filter::linear,
                                      min_filter::linear);
    /*
     2. 读取YUV 颜色值
        textureY.sample(textureSampler, input.textureCoordinate).r
        从textureY中的纹理采集器中读取,纹理坐标对应上的R值.(Y)
        textureUV.sample(textureSampler, input.textureCoordinate).rg
        从textureUV中的纹理采集器中读取,纹理坐标对应上的RG值.(UV)
     */
     //r 表示 第一个分量,相当于 index 0
    //rg 表示 数组中前面两个值,相当于 index 的0 和 1,用xy也可以
    float3 yuv = float3(textureY.sample(textureSampler, input.textureCoordinate).r,
                        textureUV.sample(textureSampler, input.textureCoordinate).rg);
    
    //3.将YUV 转化为 RGB值.convertMatrix->matrix * (YUV + convertMatrix->offset)
    float3 rgb = convertMatrix->matrix * (yuv + convertMatrix->offset);
    
    //4.返回颜色值(RGBA)
    return float4(rgb, 1.0);
}

重要API:
获取YUV:
float3 yuv = float3(textureY.sample(textureSampler, input.textureCoordinate).r,
textureUV.sample(textureSampler, input.textureCoordinate).rg);

从获取的纹理数据textureY通过采样器对对应纹理坐标进行采样获取到对应的Y值,同样获取到UV值,再组合到一起,成为YUV。

YUV转化为RGB
float3 rgb = convertMatrix->matrix * (yuv + convertMatrix->offset);

矩阵相乘YUV的向量,这里也要注意是从右向左乘

你可能感兴趣的:(十六、 Metal - Metal实现视频处理)