原创:知识探索型文章
创作不易,请珍惜,之后会持续更新,不断完善
个人比较喜欢做笔记和写总结,毕竟好记性不如烂笔头哈哈,这些文章记录了我的IOS成长历程,希望能与大家一起进步
温馨提示:由于不支持目录跳转,大家可通过command + F 输入目录标题后迅速寻找到你所需要的内容
目录
- 一、使用Metal实现视频采集和预览(可省略)
- 1、准备工作
- 2、设置Metal
- 3、设置AVFoundation进行视频采集
- 4、实现视频采集回调方法
- 5、实现视图渲染的方法
- 二、使用Metal实现视频渲染
- 1、颜色编码格式
- 2、AssetReader 工具类
- 3、RendererCamera 视图控制器
- 4、ShaderTypes 桥接文件
- 5、Shaders.metal 顶点片源函数文件
- Demo
- 参考文献
一、使用Metal实现视频采集和预览(可省略)
这里使用MTKView
来进行视频预览,但AVFoundation
自身也提供预览图层AVCaptureVideoPreviewLayer
,所以在实际开发过程中直接使用AVFoundation
提供的预览图层即可,没必要多此一举使用MTKView
来进行视频预览,本Demo
仅是教学目的而用此方式。
1、准备工作
导入框架
#import
#import
#import
#import
// 有一些滤镜处理的Metal实现
#import
代理方法
@interface ViewController ()
@end
私有属性
@property (nonatomic, strong) MTKView *mtkView;// 展示视图
@property (nonatomic, strong) AVCaptureSession *captureSession;// 负责输入和输出设备之间的数据传递
@property (nonatomic, strong) AVCaptureDeviceInput *captureDeviceInput;// 负责从AVCaptureDevice获得输入数据
@property (nonatomic, strong) AVCaptureVideoDataOutput *captureDeviceOutput;// 输出设备
@property (nonatomic, strong) dispatch_queue_t processQueue;// 处理队列
@property (nonatomic, assign) CVMetalTextureCacheRef textureCache;// 纹理缓存区
@property (nonatomic, strong) id commandQueue;// 命令队列
@property (nonatomic, strong) id texture;// 纹理
整体流程
- (void)viewDidLoad
{
[super viewDidLoad];
// 设置Metal
[self setupMetal];
// 设置AVFoundation
[self setupCaptureSession];
}
2、设置Metal
- (void)setupMetal
{
}
❶ 获取MTKView
self.mtkView = [[MTKView alloc] initWithFrame:self.view.bounds];
self.mtkView.device = MTLCreateSystemDefaultDevice();
[self.view insertSubview:self.mtkView atIndex:0];
self.mtkView.delegate = self;
//
self.mtkView.framebufferOnly = NO;
❷ 设置MTKView的drawable纹理是可读写的(默认是只读)
self.mtkView.framebufferOnly = NO;
❸ 创建命令队列
self.commandQueue = [self.mtkView.device newCommandQueue];
❹ 创建Core Video的Metal纹理缓存区
- allocator:内存分配器,使用默认选项
NULL
- cacheAttributes:缓存区,默认为NULL
- metalDevice:
- textureAttributes:创建纹理选项的字典,使用默认选项
NULL
- cacheOut:返回时,包含新创建的纹理缓存
CVMetalTextureCacheCreate(NULL, NULL, self.mtkView.device, NULL, &_textureCache);
3、设置AVFoundation进行视频采集
- (void)setupCaptureSession
{
}
❶ 创建captureSession
// 1.创建captureSession
self.captureSession = [[AVCaptureSession alloc] init];
// 设置视频采集的分辨率
self.captureSession.sessionPreset = AVCaptureSessionPreset1920x1080;
❷ 创建串行队列(视频是有序的)
self.processQueue = dispatch_queue_create("processQueue", DISPATCH_QUEUE_SERIAL);
❸ 获取摄像头设备(前置/后置摄像头设备)
NSArray *devices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo];
AVCaptureDevice *inputCamera = nil;
// 循环设备数组,找到后置摄像头,设置为当前inputCamera
for (AVCaptureDevice *device in devices)
{
if ([device position] == AVCaptureDevicePositionBack)
{
inputCamera = device;
}
}
❹ 将AVCaptureDevice转换为AVCaptureDeviceInput
self.captureDeviceInput = [[AVCaptureDeviceInput alloc] initWithDevice:inputCamera error:nil];
// 将设备添加到captureSession中
if ([self.captureSession canAddInput:self.captureDeviceInput])
{
[self.captureSession addInput:self.captureDeviceInput];
}
❺ 创建AVCaptureVideoDataOutput对象
self.captureDeviceOutput = [[AVCaptureVideoDataOutput alloc] init];
// 设置视频帧延迟时是否丢弃数据
[self.captureDeviceOutput setAlwaysDiscardsLateVideoFrames:NO];
// 这里设置格式为BGRA,而不用YUV的颜色空间,避免使用Shader转换
// 这里必须和后面CVMetalTextureCacheCreateTextureFromImage保存图像像素存储格式保持一致,否则视频会出现异常现象
[self.captureDeviceOutput setVideoSettings:[NSDictionary dictionaryWithObject:[NSNumber numberWithInt:kCVPixelFormatType_32BGRA] forKey:(id)kCVPixelBufferPixelFormatTypeKey]];
// 设置视频捕捉输出的代理方法
[self.captureDeviceOutput setSampleBufferDelegate:self queue:self.processQueue];
// 添加输出
if ([self.captureSession canAddOutput:self.captureDeviceOutput])
{
[self.captureSession addOutput:self.captureDeviceOutput];
}
❻ 将输入与输出连接
AVCaptureConnection *connection = [self.captureDeviceOutput connectionWithMediaType:AVMediaTypeVideo];
// 一定要设置视频方向,否则视频方向是异常的
[connection setVideoOrientation:AVCaptureVideoOrientationPortrait];
❼ 开始捕捉
[self.captureSession startRunning];
4、实现视频采集回调方法
这里只进行视频的预览,不需要进行压缩和编码等操作,所以当拿到原始数据后就可以直接进行渲染。
- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection
{
}
❶ 从sampleBuffer获取视频像素缓存区对象
CVPixelBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
❷ 获取捕捉视频的宽和高
size_t width = CVPixelBufferGetWidth(pixelBuffer);
size_t height = CVPixelBufferGetHeight(pixelBuffer);
❸ 根据视频像素缓存区创建Metal纹理缓存区
- allocator:内存分配器,默认
kCFAllocatorDefault
- textureCache:纹理缓存区对象
- sourceImage:视频图像缓冲区
- textureAttributes:纹理参数字典,默认为
NULL
- pixelFormat:图像缓存区数据的
Metal
像素格式常量。注意如果MTLPixelFormatBGRA8Unorm
和摄像头采集时设置的颜色格式不一致,则会出现图像异常的情况 - width:纹理图像的宽度(像素)
- height:纹理图像的高度(像素)
- planeIndex:如果图像缓冲区是平面的,则为映射纹理数据的平面索引。对于非平面图像缓冲区忽略
- textureOut:返回时,返回创建的
Metal
纹理缓冲区
CVMetalTextureRef tmpTexture = NULL;
CVReturn status = CVMetalTextureCacheCreateTextureFromImage(kCFAllocatorDefault, self.textureCache, pixelBuffer, NULL, MTLPixelFormatBGRA8Unorm, width, height, 0, &tmpTexture);
// 判断纹理缓冲区是否创建成功
if(status == kCVReturnSuccess)
{
...
}
❹ 设置可绘制纹理的当前大小
self.mtkView.drawableSize = CGSizeMake(width, height);
❺ 返回纹理缓冲区的Metal纹理对象
self.texture = CVMetalTextureGetTexture(tmpTexture);
❻ 使用完毕,则释放纹理缓冲区
CFRelease(tmpTexture);
5、实现视图渲染的方法
- (void)drawInMTKView:(MTKView *)view
{
// 判断是否获取了AVFoundation采集的纹理数据
if (self.texture)
{
...
}
}
❶ 创建指令缓冲
id commandBuffer = [self.commandQueue commandBuffer];
❷ 将MTKView作为目标渲染纹理
id drawingTexture = view.currentDrawable.texture;
❸ 创建高斯滤镜,sigma值越高图像越模糊
MPSImageGaussianBlur *filter = [[MPSImageGaussianBlur alloc] initWithDevice:self.mtkView.device sigma:1];
❹ 高斯滤镜以Metal纹理作为输入和输出
// 输入:摄像头采集的图像 self.texture
// 输出:创建的纹理 drawingTexture(其实就是view.currentDrawable.texture)
[filter encodeToCommandBuffer:commandBuffer sourceTexture:self.texture destinationTexture:drawingTexture];
❺ 展示显示的内容并提交命令
[commandBuffer presentDrawable:view.currentDrawable];
[commandBuffer commit];
❻ 清空当前纹理,准备下一次的纹理数据读取
self.texture = NULL;
二、使用Metal实现视频渲染
1、颜色编码格式
RGB 颜色编码
RGB 三个字母分别代表了 红(Red
)、绿(Green
)、蓝(Blue
),这三种颜色称为 三原色,将它们以不同的比例相加,可以产生多种多样的颜色。
在图像显示中,一张 1280 * 720
大小的图片,就代表着它有 1280 * 720
个像素点。其中每一个像素点的颜色显示都采用 RGB 编码方法,将 RGB 分别取不同的值,就会展示不同的颜色。
RGB 图像中,每个像素点都有红、绿、蓝三个原色,其中每种原色都占用 8 bit
,也就是一个字节,那么一个像素点也就占用 24 bit
,也就是三个字节。
一张 1280 * 720
大小的图片,就占用 1280 * 720 * 3 / 1024 / 1024 = 2.63 MB
存储空间。
YUV 颜色编码
YUV 颜色编码采用的是 明亮度 和 色度 来指定像素的颜色。其中,Y 表示明亮度,而 U 和 V 表示色度。而色度又定义了颜色的两个方面:色调和饱和度。
和 RGB 表示图像类似,每个像素点都包含 Y、U、V 分量。但是它的 Y 和 UV 分量是可以分离的,如果没有 UV 分量一样可以显示完整的图像,只不过是黑白的。
对于 YUV 图像来说,并不是每个像素点都需要包含了 Y、U、V 三个分量,根据不同的采样格式,可以每个 Y 分量都对应自己的 UV 分量,也可以几个 Y 分量共用 UV 分量。
RGB 到 YUV 的转换
对于图像显示器来说,它是通过 RGB 模型来显示图像的,而在传输图像数据时又是使用 YUV 模型,这是因为 YUV 模型可以节省带宽。因此就需要采集图像时将 RGB 模型转换到 YUV 模型,显示时再将 YUV 模型转换为 RGB 模型。RGB 到 YUV 的转换,就是将图像所有像素点的 R、G、B 分量转换到 Y、U、V 分量。
YUV 4:4:4 采样
YUV 4:4:4 采样,意味着 Y、U、V 三个分量的采样比例相同,因此在生成的图像里,每个像素的三个分量信息完整,都是 8 bit
,也就是一个字节。
可以看到这种采样方式的图像和 RGB 颜色模型的图像大小是一样,并没有达到节省带宽的目的,当将 RGB 图像转换为 YUV 图像时,也是先转换为 YUV 4:4:4 采样的图像。
假如图像像素为:[Y0 U0 V0]、[Y1 U1 V1]、[Y2 U2 V2]、[Y3 U3 V3]
那么采样的码流为:Y0 U0 V0 Y1 U1 V1 Y2 U2 V2 Y3 U3 V3
最后映射出的像素点依旧为 [Y0 U0 V0]、[Y1 U1 V1]、[Y2 U2 V2]、[Y3 U3 V3]
可以看到这种采样方式的图像和 RGB 颜色模型的图像大小是一样,并没有达到节省带宽的目的,当将 RGB 图像转换为 YUV 图像时,也是先转换为 YUV 4:4:4 采样的图像。
YUV 4:2:2 采样
YUV 4:2:2 采样,意味着 UV 分量是 Y 分量采样的一半,Y 分量和 UV 分量按照 2 : 1 的比例采样。如果水平方向有 10 个像素点,那么采样了 10 个 Y 分量,而只采样了 5 个 UV 分量。
假如图像像素为:[Y0 U0 V0]、[Y1 U1 V1]、[Y2 U2 V2]、[Y3 U3 V3]
那么采样的码流为:Y0 U0 Y1 V1 Y2 U2 Y3 V3
其中,每采样过一个像素点,都会采样其 Y 分量,而 U、V 分量就会间隔一个采集一个
最后映射出的像素点为 [Y0 U0 V1]、[Y1 U0 V1]、[Y2 U2 V3]、[Y3 U2 V3]
采样的码流映射为像素点,还是要满足每个像素点有 Y、U、V 三个分量。但是可以看到,第一和第二像素点公用了 U0、V1 分量,第三和第四个像素点公用了 U2、V3 分量,这样就节省了图像空间。
一张 1280 * 720 大小的图片,在 YUV 4:2:2 采样时的大小为:(1280 * 720 * 8 + 1280 * 720 * 0.5 * 8 * 2)/ 8 / 1024 / 1024 = 1.76 MB
可以看到 YUV 4:2:2 采样的图像比 RGB 模型图像节省了三分之一的存储空间,在传输时占用的带宽也会随之减少。
YUV 4:2:0 采样
YUV 4:2:0 采样,并不是指只采样 U 分量而不采样 V 分量。而是指,在每一行扫描时,只扫描一种色度分量(U 或者 V),和 Y 分量按照 2 : 1 的方式采样。比如,第一行扫描时,YU 按照 2 : 1 的方式采样,那么第二行扫描时,YV 分量按照 2:1 的方式采样。对于每个色度分量来说,它的水平方向和竖直方向的采样和 Y 分量相比都是 2:1 。
假设第一行扫描了 U 分量,第二行扫描了 V 分量,那么需要扫描两行才能够组成完整的 UV 分量。
假设图像像素为:
[Y0 U0 V0]、[Y1 U1 V1]、 [Y2 U2 V2]、 [Y3 U3 V3]
[Y5 U5 V5]、[Y6 U6 V6]、 [Y7 U7 V7] 、[Y8 U8 V8]
那么采样的码流为:Y0 U0 Y1 Y2 U2 Y3 Y5 V5 Y6 Y7 V7 Y8
其中,每采样过一个像素点,都会采样其 Y 分量,而 U、V 分量就会间隔一行按照 2 : 1 进行采样。
最后映射出的像素点为:
[Y0 U0 V5]、[Y1 U0 V5]、[Y2 U2 V7]、[Y3 U2 V7]
[Y5 U0 V5]、[Y6 U0 V5]、[Y7 U2 V7]、[Y8 U2 V7]
从映射出的像素点中可以看到,四个 Y 分量是共用了一套 UV 分量,而且是按照 2*2 的小方格的形式分布的,相比 YUV 4:2:2 采样中两个 Y 分量共用一套 UV 分量,这样更能够节省空间。
一张 1280 * 720 大小的图片,在 YUV 4:2:0 采样时的大小为:(1280 * 720 * 8 + 1280 * 720 * 0.25 * 8 * 2)/ 8 / 1024 / 1024 = 1.32 MB
2、AssetReader 工具类
a、准备工作
导入的框架
#import
提供的方法
// 初始化 AssetReader
- (instancetype)initWithUrl:(NSURL *)url;
// 从mov文件读取CMSampleBufferRef数据
- (CMSampleBufferRef)readBuffer;
成员变量
@implementation AssetReader
{
AVAssetReaderTrackOutput *readerVideoTrackOutput;// 视频输出轨道
AVAssetReader *assetReader;// 可以从原始数据里获取解码后的音视频数据
NSURL *videoUrl;// 视频地址
NSLock *lock;// 锁
}
实现初始化方法
- (instancetype)initWithUrl:(NSURL *)url
{
self = [super init];
if(self != nil)
{
videoUrl = url;
lock = [[NSLock alloc] init];
[self setUpAsset];
}
return self;
}
b、设置Asset
- (void)setUpAsset
{
}
❶ 创建AVURLAsset
AVURLAsset
是AVAsset
的子类,用于从本地或者远程URL
初始化资源。
// 默认为NO,设置为YES则可获取精确的时长
NSDictionary *inputOptions = [NSDictionary dictionaryWithObject:[NSNumber numberWithBool:YES] forKey:AVURLAssetPreferPreciseDurationAndTimingKey];
AVURLAsset *inputAsset = [[AVURLAsset alloc] initWithURL:videoUrl options:inputOptions];
❷ 异步加载资源
对资源所需的键执行标准的异步载入操作,这样就可以访问资源的tracks
视频轨道属性时不会受到阻碍。
__weak typeof(self) weakSelf = self;// 解决循环引用
NSString *tracks = @"tracks";// 键名称
[inputAsset loadValuesAsynchronouslyForKeys:@[tracks] completionHandler: ^{
__strong typeof(self) strongSelf = weakSelf;// 延长self生命周期
...
});
}];
❸ 开辟子线程并发队列异步函数来处理读取的inputAsset
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSError *error = nil;
// 获取视频轨道状态码
AVKeyValueStatus tracksStatus = [inputAsset statusOfValueForKey:@"tracks" error:&error];
// 如果状态不等于成功加载则返回并打印错误信息
if (tracksStatus != AVKeyValueStatusLoaded)
{
NSLog(@"视频轨道状态为失败,错误信息为:%@", error);
return;
}
// 处理读取的inputAsset
[strongSelf processWithAsset:inputAsset];
});
c、处理获取到的asset
- (void)processWithAsset:(AVAsset *)asset
{
[lock lock];// 锁定
...
[lock unlock];// 取消锁
}
❶ 创建AVAssetReader
NSError *error = nil;
assetReader = [AVAssetReader assetReaderWithAsset:asset error:&error];
❷ 设置像素格式为YUV 4:2:0
NSMutableDictionary *outputSettings = [NSMutableDictionary dictionary];
[outputSettings setObject:@(kCVPixelFormatType_420YpCbCr8BiPlanarFullRange) forKey:(id)kCVPixelBufferPixelFormatTypeKey];
❸ 读取资源中的视频信息
readerVideoTrackOutput = [AVAssetReaderTrackOutput assetReaderTrackOutputWithTrack:[[asset tracksWithMediaType:AVMediaTypeVideo] objectAtIndex:0] outputSettings:outputSettings];
❹ 缓存区的数据输出之前是否会被复制
默认为NO,设置为YES则表示输出的是从缓存区复制的数据,所以你可以自由的修改这些数据。
readerVideoTrackOutput.alwaysCopiesSampleData = NO;
❺ 为assetReader填充输出
[assetReader addOutput:readerVideoTrackOutput];
❻ assetReader开始读取,倘若URL错误导致无法读取则进行提示
if ([assetReader startReading] == NO)
{
NSLog(@"URL错误导致无法读取,错误资源为:%@", asset);
}
d、读取Buffer数据
- (CMSampleBufferRef)readBuffer
{
[lock lock];// 锁定
...
[lock unlock];// 取消锁
return sampleBufferRef;// 返回读取到的sampleBufferRef数据
}
❶ 读取缓存区的内容
CMSampleBufferRef sampleBufferRef = nil;
if (readerVideoTrackOutput)// 判断视频输出轨道是否创建成功
{
// 复制下一个缓存区的内容到sampleBufferRef
sampleBufferRef = [readerVideoTrackOutput copyNextSampleBuffer];
}
❷ 内容读取完成后的操作
因为是逐帧读取,所以需要重复清空再读取直到视频内容全部读取完成。
// 判断资源读取器是否存在并且处于已经完成读取的状态
if (assetReader && assetReader.status == AVAssetReaderStatusCompleted)
{
// 清空视频输出轨道
readerVideoTrackOutput = nil;
// 清空资源读取器
assetReader = nil;
// 重新初始化二者
[self setUpAsset];
}
3、RendererCamera 视图控制器
a、准备工作
导入框架
#import
#import
私有属性
@interface RendererCameraViewController ()
@property (nonatomic, strong) MTKView *mtkView;
// 用来读取mov或者mp4文件中的视频数据
@property (nonatomic, strong) AssetReader *reader;
// 由CoreVideo框架提供的高速纹理读取缓存区,用来迅速读取纹理到GPU
@property (nonatomic, assign) CVMetalTextureCacheRef textureCache;
@property (nonatomic, assign) vector_uint2 viewportSize;// 视口大小
@property (nonatomic, strong) id pipelineState;// 渲染管道
@property (nonatomic, strong) id commandQueue;// 命令队列
@property (nonatomic, strong) id texture;// 纹理
@property (nonatomic, strong) id vertices;// 顶点缓存区
@property (nonatomic, strong) id convertMatrix;// YUV->RGB 转换矩阵
@property (nonatomic, assign) NSUInteger numVertices;// 顶点个数
@end
整体流程
- (void)viewDidLoad
{
[super viewDidLoad];
// 1.设置MTKView
[self setupMTKView];
// 2.设置AssetReader
[self setupAsset];
// 3.设置渲染管道
[self setupPipeline];
// 4.设置顶点数据
[self setupVertex];
// 5.设置转换矩阵
[self setupMatrix];
}
b、设置MTKView
- (void)setupMTKView
{
// 初始化mtkView
self.mtkView = [[MTKView alloc] initWithFrame:self.view.bounds];
// 获取默认的device
self.mtkView.device = MTLCreateSystemDefaultDevice();
// 设置代理
self.mtkView.delegate = self;
// 获取视口大小
self.viewportSize = (vector_uint2){self.mtkView.drawableSize.width, self.mtkView.drawableSize.height};
self.view = self.mtkView;
}
c、设置AssetReader
- (void)setupAsset
{
// 视频文件路径
NSURL *url = [[NSBundle mainBundle] URLForResource:@"kun2" withExtension:@"mp4"];
// 初始化AssetReader
self.reader = [[AssetReader alloc] initWithUrl:url];
// 通过CoreVideo提供给CPU/GPU高速缓存通道读取纹理数据
CVMetalTextureCacheCreate(NULL, NULL, self.mtkView.device, NULL, &_textureCache);
}
d、设置渲染管道
- (void)setupPipeline
{
}
❶ 获取metal文件
- newDefaultLibrary:一个
metal
文件时推荐使用 - newLibraryWithFile:从
Library
读取指定metal
文件 - newLibraryWithData:从
Data
中获取metal
文件
id defaultLibrary = [self.mtkView.device newDefaultLibrary];
// 顶点shader,vertexShader是函数名
id vertexFunction = [defaultLibrary newFunctionWithName:@"vertexShader"];
// 片元shader,samplingShader是函数名
id fragmentFunction = [defaultLibrary newFunctionWithName:@"samplingShader"];
❷ 渲染管道描述信息类
MTLRenderPipelineDescriptor *pipelineStateDescriptor = [[MTLRenderPipelineDescriptor alloc] init];
// 设置vertexFunction
pipelineStateDescriptor.vertexFunction = vertexFunction;
// 设置fragmentFunction
pipelineStateDescriptor.fragmentFunction = fragmentFunction;
// 设置颜色格式
pipelineStateDescriptor.colorAttachments[0].pixelFormat = self.mtkView.colorPixelFormat;
❸ 根据渲染管道描述信息初始化渲染管道
self.pipelineState = [self.mtkView.device newRenderPipelineStateWithDescriptor:pipelineStateDescriptor error:NULL];
❹ 初始化渲染指令队列,保证渲染指令有序地提交到GPU
self.commandQueue = [self.mtkView.device newCommandQueue];
e、设置顶点
- (void)setupVertex
{
}
❶ 创建顶点坐标(x,y,z,w) 纹理坐标(x,y)
- 为了让视频全屏铺满,所有顶点大小均设置[-1,1]
- 图像由上三角和下三角组成
static const Vertex 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 } },
};
❷ 创建顶点缓存区
self.vertices = [self.mtkView.device newBufferWithBytes:quadVertices
length:sizeof(quadVertices)
options:MTLResourceStorageModeShared];
❸ 计算顶点个数
self.numVertices = sizeof(quadVertices) / sizeof(Vertex);
f、设置YUV->RGB转换的矩阵
- (void)setupMatrix
{
}
❶ 转化矩阵(共有三种,区别在于清晰度)
// 不要求掌握
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},
};
❷ 设置偏移量
// 不要求掌握
vector_float3 kColorConversion601FullRangeOffset = (vector_float3){ -(16.0/255.0), -0.5, -0.5};
❸ 创建转化矩阵结构体
ConvertMatrix matrix;
matrix.matrix = kColorConversion601FullRangeMatrix;// 设置转化矩阵
matrix.offset = kColorConversion601FullRangeOffset;// 设置offset偏移量
❹ 创建转换矩阵缓存区
self.convertMatrix = [self.mtkView.device newBufferWithBytes:&matrix
length:sizeof(ConvertMatrix)
options:MTLResourceStorageModeShared];
g、设置纹理
- (void)setupTextureWithEncoder:(id)encoder buffer:(CMSampleBufferRef)sampleBuffer
{
}
❶ 从CMSampleBuffer读取CVPixelBuffer
CVPixelBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
❷ 创建临时纹理。由于这个方法会多次调用,所以在每次创建新纹理之前需要清空之前的旧纹理
id textureY = nil;
id textureUV = nil;
❸ 获取Y纹理的宽高
size_t width = CVPixelBufferGetWidthOfPlane(pixelBuffer, 0);
size_t height = CVPixelBufferGetHeightOfPlane(pixelBuffer, 0);
❹ 设置像素格式为普通格式,即一个8位规范化的无符号整数组件
MTLPixelFormat pixelFormat = MTLPixelFormatR8Unorm;
❺ 创建CoreVideo的Metal纹理
CVMetalTextureRef texture = NULL;
❻ 根据视频像素缓存区创建Metal纹理缓存区
CVReturn status = CVMetalTextureCacheCreateTextureFromImage(NULL, self.textureCache, pixelBuffer, NULL, pixelFormat, width, height, 0, &texture);
// 判断Metal纹理缓存区是否创建成功
if(status == kCVReturnSuccess)
{
}
❼ 转成Metal用的纹理
textureY = CVMetalTextureGetTexture(texture);
// 使用完毕释放
CFRelease(texture);
❽ 同理设置纹理UV
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);
}
❾ 向片元函数设置textureY / textureUV纹理
不能在客户端中完成像素点的颜色YUG到RGB的转化,因为视频有无数帧图片,每张图片有无数个像素点,造成计算量非常庞大,CPU不足以支撑起如此庞大的计算量,所以需要将Y / UV纹理传递到片源函数中去。
if(textureY != nil && textureUV != nil)
{
// 向片元函数设置textureY纹理
[encoder setFragmentTexture:textureY atIndex:FragmentTextureIndexTextureY];
// 向片元函数设置textureUV纹理
[encoder setFragmentTexture:textureUV atIndex:FragmentTextureIndexTextureUV];
}
❿ 使用完毕则将sampleBuffer释放
CFRelease(sampleBuffer);
h、绘制视图
- (void)drawInMTKView:(MTKView *)view
{
}
❶ 每次渲染都要单独创建一个命令缓冲区
id commandBuffer = [self.commandQueue commandBuffer];
❷ 获取渲染描述信息
MTLRenderPassDescriptor *renderPassDescriptor = view.currentRenderPassDescriptor;
❸ 从AssetReader中读取图像数据
CMSampleBufferRef sampleBuffer = [self.reader readBuffer];
❹ 判断renderPassDescriptor和sampleBuffer是否已经获取到了
if(renderPassDescriptor && sampleBuffer)
{
}
❺ 设置渲染描述信息中的颜色附着(默认背景色)
renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColorMake(0.0, 0.5, 0.5, 1.0f);
❻ 根据渲染描述信息创建渲染命令编码器
id renderEncoder = [commandBuffer renderCommandEncoderWithDescriptor:renderPassDescriptor];
❼ 设置视口大小(显示区域)
[renderEncoder setViewport:(MTLViewport){0.0, 0.0, self.viewportSize.x, self.viewportSize.y, -1.0, 1.0 }];
❽ 为渲染编码器设置渲染管道
[renderEncoder setRenderPipelineState:self.pipelineState];
❾ 设置顶点缓存区
[renderEncoder setVertexBuffer:self.vertices
offset:0
atIndex:VertexInputIndexVertices];
❿ 设置纹理(将sampleBuffer数据设置到renderEncoder中)
[self setupTextureWithEncoder:renderEncoder buffer:sampleBuffer];
设置片元函数转化矩阵
[renderEncoder setFragmentBuffer:self.convertMatrix
offset:0
atIndex:FragmentInputIndexMatrix];
开始绘制
[renderEncoder drawPrimitives:MTLPrimitiveTypeTriangle
vertexStart:0
vertexCount:self.numVertices];
显示并结束编码
[commandBuffer presentDrawable:view.currentDrawable];
[renderEncoder endEncoding];
提交命令
[commandBuffer commit];
4、ShaderTypes 桥接文件
顶点数据结构
typedef struct
{
vector_float4 position;// 顶点坐标(x,y,z,w)
vector_float2 textureCoordinate;// 纹理坐标(s,t)
} Vertex;
转换矩阵
typedef struct
{
matrix_float3x3 matrix;// 三维矩阵
vector_float3 offset;// 偏移量
} ConvertMatrix;
顶点函数输入索引
typedef enum VertexInputIndex
{
VertexInputIndexVertices = 0,
} VertexInputIndex;
片元函数缓存区索引
typedef enum FragmentBufferIndex
{
FragmentInputIndexMatrix = 0,
} FragmentBufferIndex;
片元函数纹理索引
typedef enum FragmentTextureIndex
{
FragmentTextureIndexTextureY = 0,// Y纹理
FragmentTextureIndexTextureUV = 1,// UV纹理
} FragmentTextureIndex;
5、Shaders.metal 顶点片源函数文件
结构体(用于顶点函数输出/片元函数输入)
typedef struct
{
float4 clipSpacePosition [[position]];// position修饰符表示这个是顶点
float2 textureCoordinate;// 纹理坐标
} RasterizerData;
顶点函数
- RasterizerData:返回数据类型
- vertex_id:是顶点
shader
每次处理的index
,用于定位当前的顶点 - buffer:表明是缓存数据
- VertexInputIndexVertices:是索引0
vertex RasterizerData
vertexShader(uint vertexID [[ vertex_id ]],
constant Vertex *vertexArray [[ buffer(VertexInputIndexVertices) ]])
{
RasterizerData out;
out.clipSpacePosition = vertexArray[vertexID].position;// 顶点坐标
out.textureCoordinate = vertexArray[vertexID].textureCoordinate;// 纹理坐标
return out;
}
片源函数
- float4:返回每个像素点的颜色值
- stage_in:表示这个数据来自光栅化(光栅化是顶点处理之后的步骤,无法修改)
- textureY:表明是纹理数据,
FragmentTextureIndexTextureY
是索引 - textureUV:表明是纹理数据,
FragmentTextureIndexTextureUV
是索引 - buffer:表明是缓存数据,
FragmentInputIndexMatrix
是索引 - 从
textureY
的纹理采集器读取纹理坐标对应的R值(Y) - 从
textureUV
的纹理采集器读取纹理坐标对应的RG值(UV)
fragment float4
samplingShader(RasterizerData input [[stage_in]],
texture2d textureY [[ texture(FragmentTextureIndexTextureY) ]],
texture2d textureUV [[ texture(FragmentTextureIndexTextureUV) ]],
constant ConvertMatrix *convertMatrix [[ buffer(FragmentInputIndexMatrix) ]])
{
// 获取纹理采样器
constexpr sampler textureSampler (mag_filter::linear,
min_filter::linear);
// 读取YUV颜色值
float3 yuv = float3(textureY.sample(textureSampler, input.textureCoordinate).r,
textureUV.sample(textureSampler, input.textureCoordinate).rg);
// 将YUV颜色值转化为RGB颜色值
float3 rgb = convertMatrix->matrix * (yuv + convertMatrix->offset);
// 返回RGBA颜色值
return float4(rgb, 1.0);
}
Demo
Demo在我的Github上,欢迎下载。
Multi-MediaDemo
参考文献
- 一文读懂 YUV 的采样与格式