1.Metal是苹果的图形图像渲染框架,也可以实现普通的GPU高并发计算,流程与OpenGL ES非常类似,且其里面也有封装好的滤镜,可以直接使用无需自己写.metal渲染的代码实现,视情况而定
2.主要思路
Metal视频渲染和图片渲染流程和思路完全一致,只不过图片读取的是图片纹理,视频读取的是Y和UV两个纹理,外加需要传递YUV到RGB的颜色转换矩阵和偏移量
1.OC代码段编写Metal渲染所需的相关流程代码
2.OC和metal文件之间的桥接文件提供数据类型和端口索引
3.编写metal的 vertex顶点函数 fragment片元函数
4.在MTKView的代理方法里面渲染每一帧的图片纹理数据
5.视频纹理的获取和颜色转换矩阵设置
6.视频流捕获AVFoundation,具体在前面的文字里有展开分析
3.具体步骤
1. OC代码段编写Metal渲染所需的相关流程代码
1.创建MTKView,设置代理,这是Metal渲染的目标view,这里的代码都是面向协议的编码,MTKView的 id
2.创建纹理
3.创建顶点
4.创建渲染管道,加载顶点函数和片元函数
2.OC和metal文件之间的桥接文件提供数据类型和端口索引
1.具体的数据结构和索引视情况而定,顶点坐标,纹理坐标,图片纹理,顶点索引,纹理索引,其他需要的参数,比如时间,数组,矩阵,需要的都可以
2.视频的纹理是Y纹理 和 UV纹理两个部分,需要增加YUV到RGB的颜色转换矩阵
3.编写metal的 vertex顶点函数 fragment片元函数
1.编写metal函数,语法看起来有些复杂,其实还好,要注意与桥接对象的数据类型的对应关系,主要有数据结构和端口索引值,类型关键字比如 [[position]] [[buffer]] [[stage_in]] 等等,函数修饰符,参数地址空间修饰符等,其余编码的思路与OpenGL ES基本一致,只是语法有些差异
3.编写metal的 vertex顶点函数 fragment片元函数
1.编写metal函数,语法看起来有些复杂,其实还好,要注意与桥接对象的数据类型的对应关系,主要有数据结构和端口索引值,类型关键字比如 [[position]] [[buffer]] [[stage_in]] 等等,函数修饰符,参数地址空间修饰符等,其余编码的思路与OpenGL ES基本一致,只是语法有些差异
4.在MTKView的代理方法里面渲染每一帧的图片纹理数据
MTKView的两个代理方法
// 设置渲染范围
- (void)mtkView:(nonnull MTKView *)view drawableSizeWillChange:(CGSize)size {
self.viewportSize = (vector_int2){size.width, size.height};
}
// MTKViewDelegate
// 每一帧渲染命令的具体实现
- (void)drawInMTKView:(nonnull MTKView *)view
5.视频纹理的获取和颜色转换矩阵设置
1.视频流数据CMSampleBuffer或者是CVPixelBuffer 解析成Metal的纹理CVMetalTextureRef 类型,分为Y纹理和UV纹理,需要注意的是采集的颜色输出格式和Y纹理和UV纹理的MTLPixelFormat格式,否则颜色会异常,偏红偏绿偏蓝,这部分获取的Y和UV纹理用来设置MTKView渲染代理的纹理数据
2.设置视频颜色格式YUV和渲染纹理的RGB颜色转换矩阵,这也需要传递的参数
4.代码实现
1.OC类->客户端代码,一般针对于Metal函数 或者OpenGL ES的shader文件,我们写的CPU执行的代码相对于以上两种是GPU执行的代码统称为客户端
1.头文件 变量 属性
#import
#import
#import
#import "VideoCaptureView.h"
#import "YYVideoShaderTypes.h"
@interface MetalVideoFilterView ()
{
CGRect m_frame;
BOOL isChangeFillMode;
}
// 渲染范围
@property (nonatomic, assign) vector_int2 viewportSize;
// MTKView Metal渲染的view
@property (nonatomic, strong) MTKView * mtkView;
// 用来渲染的设备(GPU)
@property (nonatomic, strong) id device;
// 渲染管道,管理顶点函数和片元函数
@property (nonatomic, strong) id renderPipelineState;
// 渲染指令队列
@property (nonatomic, strong) id commondQueue;
// 顶点缓存对象
@property (nonatomic, strong) id vertexBuffer;
// 颜色转换矩阵
@property (nonatomic, strong) id convertMatrix;
// 纹理对象
@property (nonatomic, strong) id texture;
// 顶点数量
@property (nonatomic, assign) NSUInteger vertexCount;
// 获取视频数据
@property (nonatomic, strong) VideoCaptureView * m_videoCaptureView;
// 视频数据
@property (atomic, assign) CVPixelBufferRef pixelBuffer;
// 纹理缓存 用于使用CVPixelBuffer格式的数据生成Metal的纹理
@property (nonatomic, assign) CVMetalTextureCacheRef textureCache;
// Y纹理
@property (atomic, strong) id textureY;
// UV纹理
@property (atomic, strong) id textureUV;
2.初始化调用
- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
m_frame = frame;
[self createVideoCaptureView];
// 1.创建 MTKView
[self createMTKView];
// 2.设置顶点
[self setupVertexsWithWidthScaling:1.0f heightScaling:1.0f];
// 3. 设置颜色转换矩阵 YUV->RGB
[self setupMatrix];
// 4.创建渲染管道
[self createPipeLineState];
}
return self;
}
3.创建MTKView
// 创建 MTKView
- (void)createMTKView {
MTKView * mtkView = [[MTKView alloc] initWithFrame:CGRectMake(0, 0, m_frame.size.width, m_frame.size.height)];
mtkView.delegate = self;
// 创建Device
mtkView.device = MTLCreateSystemDefaultDevice();
// 设置device
self.device = mtkView.device;
self.viewportSize = (vector_int2){mtkView.drawableSize.width, mtkView.drawableSize.height};
self.mtkView = mtkView;
[self addSubview:mtkView];
//3._textureCache的创建(通过CoreVideo提供给CPU/GPU高速缓存通道读取纹理数据)
CVMetalTextureCacheCreate(NULL, NULL, self.mtkView.device, NULL, &_textureCache);
}
4.设置顶点数据 顶点缓存
- (void)setupVertexsWithWidthScaling:(CGFloat)widthScaling heightScaling:(CGFloat)heightScaling {
// 1.顶点纹理数组
// 顶点x,y,z,w 纹理x,y
// 因为图片和视频的默认纹理是反的 左上 00 右上10 左下 01 右下11
// // 左下 右下
YYVideoVertex vertexArray[] = {
{{-1.0 * widthScaling, -1.0 * heightScaling, 0.0, 1.0}, {0.0, 1.0}},
{{1.0 * widthScaling, -1.0 * heightScaling, 0.0, 1.0}, {1.0, 1.0}},
{{-1.0 * widthScaling, 1.0 * heightScaling, 0.0, 1.0}, {0.0, 0.0}}, //左上
{{1.0 * widthScaling, 1.0 * heightScaling, 0.0, 1.0}, {1.0, 0.0}}, // 右上
};
// 2.生成顶点缓存
// MTLResourceStorageModeShared 属性可共享的,表示可以被顶点或者片元函数或者其他函数使用
self.vertexBuffer = [self.device newBufferWithBytes:vertexArray length:sizeof(vertexArray) options:MTLResourceStorageModeShared];
// 3.获取顶点数量
self.vertexCount = sizeof(vertexArray) / sizeof(YYVideoVertex);
}
5.设置渲染管道
// 4.创建渲染管道
// 根据.metal里的函数名,使用MTLLibrary创建顶点函数和片元函数
// 从这里可以看出来,MTLLibrary里面包含所有.metal的文件,所以,不同的.metal里面的函数名不能相同
// id 创建library、MTLRenderPipelineState、MTLCommandQueue
- (void)createPipeLineState {
// 1.从项目中加载.metal文件,创建一个library
id library = [self.device newDefaultLibrary];
// id -> id
// 2.从库中MTLLibrary,加载顶点函数
id vertexFunction = [library newFunctionWithName:@"vertexVideoShader"];
// 3.从库中MTLLibrary,加载顶点函数
id fragmentFunction = [library newFunctionWithName:@"fragmentVideoShader"];
// 4.创建管道渲染管道描述符
MTLRenderPipelineDescriptor * renderPipeDescriptor = [[MTLRenderPipelineDescriptor alloc] init];
// 5.设置管道顶点函数和片元函数
renderPipeDescriptor.vertexFunction = vertexFunction;
renderPipeDescriptor.fragmentFunction = fragmentFunction;
// 6.设置管道描述的关联颜色存储方式
renderPipeDescriptor.colorAttachments[0].pixelFormat = self.mtkView.colorPixelFormat;
NSError * error = nil;
// 7.根据渲染管道描述符 创建渲染管道
id renderPipelineState = [self.device newRenderPipelineStateWithDescriptor:renderPipeDescriptor error:&error];
self.renderPipelineState = renderPipelineState;
// id -> id
// 8. 创建渲染指令队列
id commondQueue = [self.device newCommandQueue];
self.commondQueue = commondQueue;
// id -> id
}
6.设置颜色转换矩阵和偏移量的 数据结构
// 设置YUV->RGB转换的矩阵
- (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 = (matrix_float3x3){
(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.创建转化矩阵结构体.
YYVideoYUVToRGBConvertMatrix matrix;
//设置转化矩阵
/*
kColorConversion601DefaultMatrix;
kColorConversion601FullRangeMatrix;
kColorConversion709DefaultMatrix;
*/
matrix.matrix = kColorConversion601FullRangeMatrix;
//设置offset偏移量
matrix.offset = kColorConversion601FullRangeOffset;
//4.创建转换矩阵缓存区.
self.convertMatrix = [self.mtkView.device newBufferWithBytes:&matrix length:sizeof(YYVideoYUVToRGBConvertMatrix) options:MTLResourceStorageModeShared];
}
7.或者视频流转换为Y和UV纹理
// 视频捕捉的View
- (void)createVideoCaptureView {
VideoCaptureView * videoCaptureView = [[VideoCaptureView alloc] initWithFrame:CGRectMake(0, 0, m_frame.size.width, m_frame.size.height)];
videoCaptureView.m_delegate = self;
self.m_videoCaptureView = videoCaptureView;
}
// 捕获视频的代理回调方法
- (void)outputCVPixelBuffer:(CVPixelBufferRef)pixelBuffer {
// 视频对外输出一般要CMSampleBufferRef从中提取出CVPixelBufferRef
// CVPixelBufferRetain 使用这个强引用
if (_pixelBuffer) {
CFRelease(_pixelBuffer);
}
_pixelBuffer = CVPixelBufferRetain(pixelBuffer);
[self setupTextureWithPixelBuffer:_pixelBuffer];
// NSLog(@"outputSampleBuffer");
// AVFoundation采集的CMSampleBufferRef不需要释放,因为只是参数,代理方法内部有释放
// CFRelease(sampleBuffer);
}
// 生成Y纹理和UV纹理,提供给MTKView的代理方法使用
- (void)setupTextureWithPixelBuffer:(CVPixelBufferRef)pixelBuffer {
if (!pixelBuffer) {
return;
}
// Y纹理
{
// 0表示Y纹理
size_t width = CVPixelBufferGetWidthOfPlane(pixelBuffer, 0);
size_t height = CVPixelBufferGetHeightOfPlane(pixelBuffer, 0);
if (isChangeFillMode) {
isChangeFillMode = NO;
[self resetVertexWithWidth:width height:height];
}
// 像素颜色格式 MTLPixelFormatR8Unorm 表示只取R一个颜色分支
MTLPixelFormat pixelFormat = MTLPixelFormatR8Unorm;
CVMetalTextureRef texture = NULL;
CVMetalTextureCacheRef textureCache = NULL;
// 开辟纹理缓存区
CVReturn TextureCacheCreateStatus =CVMetalTextureCacheCreate(NULL, NULL, self.device, NULL, &textureCache);
if(TextureCacheCreateStatus == kCVReturnSuccess) {
// NSLog(@"CVMetalTextureCacheCreate is success");
}
// 根据CVPixelBufferRef数据,使用CVMetalTextureCacheRef,创建CVMetalTextureRef
// 0表示Y纹理
CVReturn status = CVMetalTextureCacheCreateTextureFromImage(kCFAllocatorDefault, self.textureCache, pixelBuffer, NULL, pixelFormat, width, height, 0, &texture);
if(status == kCVReturnSuccess) {
// 根据纹理CVMetalTextureRef 创建id
self.textureY = CVMetalTextureGetTexture(texture);
// 使用完毕释放资源
CFRelease(texture);
// NSLog(@"create Y texture is Success");
} else {
// NSLog(@"create Y texture is failed");
// NSLog(@"status == %d", status);
}
CFRelease(textureCache);
}
// UV纹理
{
// 1表示UV纹理
size_t width = CVPixelBufferGetWidthOfPlane(pixelBuffer, 1);
size_t height = CVPixelBufferGetHeightOfPlane(pixelBuffer, 1);
// 像素颜色格式 MTLPixelFormatRG8Unorm 表示只取RG两个颜色分支
MTLPixelFormat pixelFormat = MTLPixelFormatRG8Unorm;
CVMetalTextureRef texture = NULL;
CVMetalTextureCacheRef textureCache = NULL;
// 开辟纹理缓存区
CVReturn TextureCacheCreateStatus =CVMetalTextureCacheCreate(NULL, NULL, self.device, NULL, &textureCache);
if(TextureCacheCreateStatus == kCVReturnSuccess) {
// NSLog(@"CVMetalTextureCacheCreate is success");
}
// 根据CVPixelBufferRef数据,使用CVMetalTextureCacheRef,创建CVMetalTextureRef
// 1表示UV纹理
CVReturn status = CVMetalTextureCacheCreateTextureFromImage(kCFAllocatorDefault, self.textureCache, pixelBuffer, NULL, pixelFormat, width, height, 1, &texture);
if(status == kCVReturnSuccess) {
// 根据纹理CVMetalTextureRef 创建id
self.textureUV = CVMetalTextureGetTexture(texture);
// 使用完毕释放资源
CFRelease(texture);
// NSLog(@"create UV texture is Success");
} else {
// NSLog(@"create UV texture is failed");
// NSLog(@"status == %d", status);
}
CFRelease(textureCache);
}
}
8.MTKView代理方法
// MTKViewDelegate 设置渲染区域或者是区域变动时执行
- (void)mtkView:(nonnull MTKView *)view drawableSizeWillChange:(CGSize)size {
self.viewportSize = (vector_int2){size.width, size.height};
}
// MTKViewDelegate 每一帧的渲染流程
- (void)drawInMTKView:(nonnull MTKView *)view {
if (self.textureY && self.textureUV) {
// 1.为当前渲染的每个渲染传递创建一个新的命令缓冲区
id commandBuffer = [self.commondQueue commandBuffer];
//指定缓存区名称
commandBuffer.label = @"EachCommand";
// 2.获取渲染命令编码器 MTLRenderCommandEncoder的描述符
// currentRenderPassDescriptor描述符包含currentDrawable's的纹理、视图的深度、模板和sample缓冲区和清晰的值。
// MTLRenderPassDescriptor描述一系列attachments的值,类似GL的FrameBuffer;同时也用来创建MTLRenderCommandEncoder
MTLRenderPassDescriptor * renderPassDescriptor = view.currentRenderPassDescriptor;
if (renderPassDescriptor) {
// 设置默认颜色 背景色
renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColorMake(1.0, 1.0, 1.0, 1.0f);
// 3.根据描述创建x 渲染命令编码器
id renderEncoder = [commandBuffer renderCommandEncoderWithDescriptor:renderPassDescriptor];
// typedef struct {
// double originX, originY, width, height, znear, zfar;
// } MTLViewport;
// 4.设置绘制区域
[renderEncoder setViewport:(MTLViewport){0, 0, self.viewportSize.x, self.viewportSize.y, -1.0, 1.0}];
// 5.设置渲染管道
[renderEncoder setRenderPipelineState:self.renderPipelineState];
// 6.传递顶点缓存
[renderEncoder setVertexBuffer:self.vertexBuffer offset:0 atIndex:YYVideoVertexInputIndexVertexs];
// 7.传递颜色转换矩阵
[renderEncoder setFragmentBuffer:self.convertMatrix offset:0 atIndex:YYVideoConvertMatrixIndexYUVToRGB];
// 设置Y UV纹理
if (self.textureY) {
// NSLog(@"setFragmentTexture textureY");
[renderEncoder setFragmentTexture:self.textureY atIndex:YYVidoTextureIndexYTexture];
self.textureY = nil;
}
if (self.textureUV) {
// NSLog(@"setFragmentTexture textureUV");
[renderEncoder setFragmentTexture:self.textureUV atIndex:YYVidoTextureIndexUVTexture];
self.textureUV = nil;
}
// 8.绘制
[renderEncoder drawPrimitives:MTLPrimitiveTypeTriangleStrip vertexStart:0 vertexCount:self.vertexCount];
// 9.命令结束
[renderEncoder endEncoding];
// 10.显示
[commandBuffer presentDrawable:view.currentDrawable];
}
// 11. 提交
[commandBuffer commit];
}
}
9.视频显示模式设置,同Image显示模式设置
- (void)resetVertexWithWidth:(CGFloat)width height:(CGFloat)height {
dispatch_async(dispatch_get_main_queue(), ^{
CGSize inputImageSize = CGSizeMake(width, height);
CGFloat heightScaling = 1.0, widthScaling = 1.0;
CGSize currentViewSize = self.bounds.size;
CGRect insetRect = AVMakeRectWithAspectRatioInsideRect(inputImageSize, self.bounds);
switch(self.fillMode)
{
case kMetalVideoFilterViewFillModeStretch:
{
widthScaling = 1.0;
heightScaling = 1.0;
};
break;
case kMetalVideoFilterViewFillModePreserveAspectRatio:
{
widthScaling = insetRect.size.width / currentViewSize.width;
heightScaling = insetRect.size.height / currentViewSize.height;
};
break;
case kMetalVideoFilterViewFillModePreserveAspectRatioAndFill:
{
widthScaling = currentViewSize.height / insetRect.size.height;
heightScaling = currentViewSize.width / insetRect.size.width;
};
break;
}
// NSLog(@"widthScaling == %lf", widthScaling);
// NSLog(@"heightScaling == %lf", heightScaling);
[self setupVertexsWithWidthScaling:widthScaling heightScaling:heightScaling];
});
}
- (void)setFillMode:(kMetalVideoFilterViewFillModeType)fillMode {
isChangeFillMode = YES;
_fillMode = fillMode;
}
2.桥接类 .h文件,里面有数据结构和端口索引值
simd这个类提供一些公共的数据类型,客户端和Metal都可以使用的类型
#include
// 顶点坐标和纹理坐标数据结构
typedef struct {
// 顶点坐标 4维向量
vector_float4 position;
// 纹理坐标
vector_float2 textureCoordinate;
} YYVideoVertex;
//颜色转换数据结构 YUV转RGBA 转换矩阵
typedef struct {
//三维矩阵
matrix_float3x3 matrix;
//偏移量
vector_float3 offset;
} YYVideoYUVToRGBConvertMatrix;
// 顶点index
typedef enum {
YYVideoVertexInputIndexVertexs = 0,
} YYVideoVertexInputIndex;
// 纹理 index
typedef enum {
// Y纹理索引 index
YYVidoTextureIndexYTexture = 0,
// UV纹理索引 index
YYVidoTextureIndexUVTexture = 1,
} YYVideoTextureIndex;
// 颜色转换结构体 index
typedef enum {
YYVideoConvertMatrixIndexYUVToRGB = 0,
} YYVideoConvertMatrixIndex;
#endif /* YYVideoShaderTypes_h */
3.Metal代码实现 顶点片元函数实现
1.需要注意,函数修饰符,变量修饰符,地址空间修饰符,桥接或者输出的数据结构类型,索引值,内部的逻辑编码与OpenGL ES非常类似,逻辑一致
#include
#import "YYVideoShaderTypes.h"
using namespace metal;
// 定义了一个类型为RasterizerData的结构体,里面有一个float4向量和float2向量,其中float4被[[position]]修饰,其表示的变量为顶点
typedef struct {
// float4 4维向量 clipSpacePosition参数名
// position 修饰符的表示顶点 语法是[[position]],这是苹果内置的语法和position关键字不能改变
float4 clipSpacePosition [[position]];
// float2 2维向量 表示纹理
float2 textureCoordinate;
} RasterizerData;
// 顶点函数通过一个自定义的结构体,返回对应的数据,顶点函数的输入参数也可以是自定义结构体
// 顶点函数
// vertex 函数修饰符表示顶点函数,
// RasterizerData返回值类型,
// vertexImageShader函数名
// vertex_id 顶点id修饰符,苹果内置不可变,[[vertex_id]]
// buffer 缓存数据修饰符,苹果内置不可变,YYImageVertexInputIndexVertexs是索引
// [[buffer(YYImageVertexInputIndexVertexs)]]
// constant 变量类型修饰符,表示存储在device区域
vertex RasterizerData vertexVideoShader(uint vertexID [[vertex_id]], constant YYVideoVertex * vertexArray [[buffer(YYVideoVertexInputIndexVertexs)]]) {
RasterizerData outData;
// 获取YYVertex里面的顶点坐标和纹理坐标
outData.clipSpacePosition = vertexArray[vertexID].position;
outData.textureCoordinate = vertexArray[vertexID].textureCoordinate;
return outData;
}
// 片元函数
// fragment 函数修饰符表示片元函数 float4 返回值类型->颜色RGBA fragmentImageShader 函数名
// RasterizerData 参数类型 input 变量名
// [[stage_in] stage_in表示这个数据来自光栅化。(光栅化是顶点处理之后的步骤,业务层无法修改)
// texture2d 类型表示纹理 baseTexture 变量名
// [[ texture(index)]] 纹理修饰符
// 可以加索引 [[ texture(0)]]纹理0, [[ texture(1)]]纹理1
// YYImageTextureIndexBaseTexture表示纹理索引
fragment float4 fragmentVideoShader(RasterizerData input [[stage_in]], texture2d textureY [[texture (YYVidoTextureIndexYTexture)]], texture2d textureUV [[texture(YYVidoTextureIndexUVTexture)]], constant YYVideoYUVToRGBConvertMatrix * convertMatrix [[buffer(YYVideoConvertMatrixIndexYUVToRGB)]]) {
// constexpr 修饰符
// sampler 采样器
// textureSampler 采样器变量名
// mag_filter:: linear, min_filter:: linear 设置放大缩小过滤方式
constexpr sampler textureSampler(mag_filter:: linear, min_filter:: linear);
// 得到纹理对应位置的颜色
// float YColor = textureY.sample(textureSampler, input.textureCoordinate).r;
// float2 UVColor = textureUV.sample(textureSampler, input.textureCoordinate).rg;
// float3 color = float3(YColor, UVColor);
// float3 outputColor = matrix->matrix * (color + matrix->offset);
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);
// 返回颜色值
return float4(rgb, 1.0);
}