美文网首页
十六、 Metal - Metal实现视频处理

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

作者: iOS之文一 | 来源:发表于2021-10-08 02:22 被阅读0次

    音视频开发: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<MTLCommandBuffer> commandBuffer = [self.commandQueue commandBuffer];
            
            //3.将MTKView 作为目标渲染纹理
            id<MTLTexture> 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<MTLTexture> 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<MTLLibrary> defaultLibrary = [self.mtkView.device newDefaultLibrary];
        // 顶点shader,vertexShader是函数名
        id<MTLFunction> vertexFunction = [defaultLibrary newFunctionWithName:@"vertexShader"];
        // 片元shader,samplingShader是函数名
        id<MTLFunction> 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<MTLCommandBuffer> 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<MTLRenderCommandEncoder> 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<MTLRenderCommandEncoder>)encoder buffer:(CMSampleBufferRef)sampleBuffer {
        
        //1.从CMSampleBuffer读取CVPixelBuffer,
        CVPixelBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
        
        id<MTLTexture> textureY = nil;
        id<MTLTexture> 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<float> textureY [[ texture(CCFragmentTextureIndexTextureY) ]],
                   texture2d<float> 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实现视频处理

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