音视频开发:OpenGL + OpenGL ES + Metal 系列文章汇总
在音视频开发中很重要的一部分是视频的处理,因此本文针对Metal对实时录像渲染和本地视频文件渲染进行分析,并且分析YUV的实现逻辑。
主要内容:
- 视频采集(了解)
- 实时录像渲染
- YUV的实现逻辑
- 本地视频文件的渲染
Metal的渲染流程、片元函数顶点函数的使用,纹理的设置等操作在前三篇博客中已经有专门解读,这里不再说明,只是增加对视频帧的处理的分析
1、实时录像渲染
1.1 简单介绍
案例地址: 视频渲染
主要学习内容:
- 采集视频过程
- 视频帧转化为纹理的过程
- 对纹理的渲染
1.2 视频采集
视频的采集使用到了AVFoundation框架,而这个框架不在本文中着重详解,因此这里仅简单说明,后续会专门写博客解读AVFoundation,可持续关注。
过程:
- 创建采集会话captureSession,用来管理采集过程
- 添加输入对象
- 获取摄像头
- 先将摄像头对象转换为Session可使用的AVCaptureDeviceInput对象,也就是输入对象
- 将输入对象添加到会话中
- 添加输出对象
- 创建输出对象
- 设置是否丢弃帧,颜色格式,设置代理
- 将输出对象添加到会话中
- 创建输入输出连接
- 创建视频连接对象
- 设置视频方向
- 开始采集
代码:
- (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这个方法,它是视频采集回调方法。
我们就可以在这个方法里将视频帧转化为纹理。
过程:
- 从sampleBuffer 获取视频像素缓存区对象
- 获取捕捉视频的宽和高
- 将获取到的视频帧转换为纹理数据
- 通过纹理数据得到纹理对象
代码:
- (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内置的滤镜,所以有必要再说明一下内置滤镜的使用。
过程:
- 创建命令缓存区
- 将MTKView的纹理作为目标渲染纹理(即将纹理绘制到当前view的可绘制界面的纹理上)
- 设置高斯模糊滤镜
- 对纹理进行滤镜设置
- 添加“展示显示的内容”的命令
- 提交命令
代码:
- (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
- id
drawingTexture = view.currentDrawable.texture;
这是获取到当前视图的可绘制界面的纹理对象。
- MPSImageGaussianBlur *filter = [[MPSImageGaussianBlur alloc] initWithDevice:self.mtkView.device sigma:1];
- 创建一个高斯滤镜处理器filter
- sigma值可以修改,sigma值越高图像越模糊
- [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个字节,如下图所示
问:为什么使用YUV,而不使用RGB?
答:通过RGB颜色编码格式的图片,一个像素点会占用3*8 = 24bit,一张1280 *720图片需要1280 *720 * 24 = 2.63MB,而一个视频由每帧画面组成,所以会占用更多的内存,同时如果帧率是60fps,则播放该视频所使用的带宽将会非常惊人。因此不可以使用RGB颜色编码格式。而使用YUV可以通过4:2:0的采样方式,减少一帧画面的存储空间。这样既减少了内存占用,又节省了带宽,所以需要使用YUV。
YUV相较RGB的优势:
- 降低占用的存储空间
- 显示画面时,节省带宽
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采样格式,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采样格式,左右两个像素点共用一套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采样格式,上下左右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 简单介绍
案例地址: 本地视频文件渲染
效果:
实现思路:
- 自定义一个CCAssetReader工具类,用来读取mov/mp4视频文件,基本功能使用AVFoundation实现的
- 将读取到的视频帧转换为纹理数据
- 传递纹理对象到片元函数,
- 在片元函数中将颜色编码格式由YUV转换为RGB,显示到屏幕上。
过程:
- 初始化
- 图形绘制
- 视频帧转换纹理
- 片元函数中实现YUV转化RGB
重点学习内容:
- YUV转化RGB值
- 视频帧转化纹理
其他所有的内容前文都已经熟悉了,这里新增的只有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 初始化
渲染初始化前文已经写过多编了,这里仅做粗略解读
过程:
- MTKView初始化
- CCAssetReader设置
- 渲染管道设置
- 顶点数据设置
- 转换矩阵设置
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};
}
注意:
- 必须要给view设置device,device是所有命令的开始
- 代理的设置可以使用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];
}
注意:
- 这个完全是固定流程,记住API就可以了
- 命令队列在哪里创建都可以,只要在创建命令缓存区之前创建好就可以。
3.2.4 顶点数据设置
过程:
- 创建顶点坐标、纹理坐标
- 添加到顶点缓存区中
- 计算顶点个数
代码:
- (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];
}
注意:
- 转化矩阵有多种,我们选一种就可以,具体的计算也不用关注
- 存储到显存中更方便着色器的取用
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 视频帧转化纹理
过程:
- 从CMSampleBuffer读取CVPixelBuffer,
- 根据视频像素缓存区 创建 Metal 纹理缓存区
- 纹理缓存区转换为纹理对象
- 向片元函数设置纹理
代码:
- (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);
}
注意:
- 最原始的数据帧是CMSampleBufferRef
- CVPixelBufferRef是视频像素缓存区,存储有这一个帧的所有像素
- CVMetalTextureCacheRef是用来创建和管理纹理的纹理缓存对象
- CVPixelBufferRef和CVMetalTextureCacheRef一起创建出存储有纹理数据的纹理缓存区
- 得到的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的向量,这里也要注意是从右向左乘