Metal学习笔记(五)-- 使用Metal实现摄像预览渲染
通常我们可以使用AVCaptureVideoPreviewLayer
实现摄像头捕捉图像的渲染展示,但学习完Metal后,本文尝试使用Metal实现摄像头预览的渲染。
基本思路
首先需要用AVFoundation
采集摄像头数据得到CMSampleBufferRef
,然后通过CoreVideo
的方法将CMSampleBufferRef转化为Metal
的MTLTexture
对象,最后用MetalPerformanceShaders
的高斯模糊滤镜对图像进行处理,最后渲染到屏幕上。
实现步骤
1.AVCaptureSession配置
- (void)setupCaptureSession {
_captureSession = [[AVCaptureSession alloc] init];
_captureSession.sessionPreset = AVCaptureSessionPreset1920x1080;
self.processQueue = dispatch_queue_create("progress queue", DISPATCH_QUEUE_SERIAL);
AVCaptureDevice *inputDevice = [AVCaptureDevice defaultDeviceWithDeviceType:AVCaptureDeviceTypeBuiltInTelephotoCamera mediaType:AVMediaTypeVideo position:AVCaptureDevicePositionBack];
if (!inputDevice) {
NSLog(@"没有摄像头");
return;
}
_deviceInput = [[AVCaptureDeviceInput alloc] initWithDevice:inputDevice error:nil];
if ([self.captureSession canAddInput:_deviceInput]) {
[self.captureSession addInput:_deviceInput];
}
_videoDataOutput = [[AVCaptureVideoDataOutput alloc] init];
/*设置视频帧延迟到底时是否丢弃数据.
YES: 处理现有帧的调度队列在captureOutput:didOutputSampleBuffer:FromConnection:Delegate方法中被阻止时,对象会立即丢弃捕获的帧。
NO: 在丢弃新帧之前,允许委托有更多的时间处理旧帧,但这样可能会内存增加.
*/
_videoDataOutput.alwaysDiscardsLateVideoFrames = NO;
_videoDataOutput.videoSettings = @{(id)kCVPixelBufferPixelFormatTypeKey: @(kCVPixelFormatType_32BGRA)};
[_videoDataOutput setSampleBufferDelegate:self queue:self.processQueue];
if ([self.captureSession canAddOutput:_videoDataOutput]) {
[self.captureSession addOutput:_videoDataOutput];
}
AVCaptureConnection *connect = [self.videoDataOutput connectionWithMediaType:AVMediaTypeVideo];
[connect setVideoOrientation:AVCaptureVideoOrientationPortrait];
}
这里只是常规的捕捉数据代码,需要注意的是AVCaptureSession
添加AVCaptureDeviceInput
需要先判断能否添加,避免摄像头被其他进程占用导致添加失败。另外AVCaptureVideoDataOutput
的输出颜色格式参数需要与后面的纹理像素格式一致,否则会出现色差。
2.Metal相关设置
- (void)setupMetal:(CGRect)frame {
_mtkView = [[MTKView alloc] initWithFrame:frame device:MTLCreateSystemDefaultDevice()];
if (!_mtkView.device) {
NSLog(@"not metal deivce support");
return;
}
_commandQueue = [_mtkView.device newCommandQueue];
_mtkView.framebufferOnly = NO;
_mtkView.delegate = self;
CVMetalTextureCacheCreate(NULL, NULL, _mtkView.device, NULL, &_textureCacehe);
}
这些只是常规的设置,另外需要将MTKView
的drawable设置为可读可写(默认是只读),另外需要创建一个纹理CVMetalTextureCacheRef
(来自Core Video框架)
3.处理采集回调
摄像头捕捉数据后回调用代理方法captureOutput:didOutputSampleBuffer:fromConnection:
,可以在这里进行数据转化。
- (void)captureOutput:(AVCaptureOutput *)output didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection {
CVPixelBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
size_t width = CVPixelBufferGetWidth(pixelBuffer);
size_t height = CVPixelBufferGetHeight(pixelBuffer);
CVMetalTextureRef tmpTexture = NULL;
/*3. 根据视频像素缓存区 创建 Metal 纹理缓存区
功能: 从现有图像缓冲区创建核心视频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(kCFAllocatorDefault, _textureCacehe, pixelBuffer, NULL, MTLPixelFormatBGRA8Unorm, width, height, 0, &tmpTexture);
if (status == kCVReturnSuccess) {
self.mtkView.drawableSize = CGSizeMake(width, height);
self.texture = CVMetalTextureGetTexture(tmpTexture);
CFRelease(tmpTexture);
}
}
这里首先需要调用CMSampleBufferGetImageBuffer
方法将CMSampleBufferRef
转化为CVPixelBufferRef
,然后调用CVMetalTextureCacheCreateTextureFromImage
将CVPixelBufferRef
转化为CVMetalTextureRef
并保存到纹理缓冲区,再通过CVMetalTextureGetTexture
获的MTLTexture
对象,这些方法都是由Core Video
提供。
4.渲染处理
- (void)drawInMTKView:(MTKView *)view {
if (self.texture) {
id<MTLCommandBuffer> commandBuffer = [self.commandQueue commandBuffer];
id<MTLTexture> drawingTexture = view.currentDrawable.texture;
// 高斯滤镜
MPSImageGaussianBlur *filter = [[MPSImageGaussianBlur alloc] initWithDevice:self.mtkView.device sigma:0];
// 把摄像头返回图像数据的原始数据
[filter encodeToCommandBuffer:commandBuffer sourceTexture:self.texture destinationTexture:drawingTexture];
[commandBuffer presentDrawable:self.mtkView.currentDrawable];
[commandBuffer commit];
self.texture = NULL;
}
}
也可以使用常规的渲染管道一步步渲染,不过为了方便,这里使用了苹果提供的滤镜。
5.开始捕捉
- (void)startRunning {
[self.captureSession startRunning];
}