Metal入门教程(五)视频渲染

前言

Metal入门教程(一)图片绘制 Metal入门教程(二)三维变换 Metal入门教程(三)摄像头采集渲染 Metal入门教程(四)灰度计算

前面的教程介绍了Metal如何显示图片、自定义shader实现三维变换、用MetalPerformanceShaders处理摄像头数据以及用Metal计算管道实现灰度计算,这次用介绍如何用Metal渲染视频。

Metal系列教程的代码地址; OpenGL ES系列教程在这里

你的star和fork是我的源动力,你的意见能让我走得更远

正文

视频渲染其实就是对CMSampleBuffer的绘制,从代码简洁角度出发,demo中引入简单封装的LYAssetReader读取视频文件。Metal渲染回调时读取CMSampleBuffer,然后获取其CVPixelBufferRef,再用CoreVideo提供的方法进行处理,得到Y和UV的纹理。Shader中定义了YUV转RGB的矩阵,用其对两个纹理进行处理,最终得到RGB的颜色值并显示到屏幕上。

效果展示

核心思路

从CPU传数据到GPU,会阻塞等待CPU的数据传送完毕,比如所我们在Metal入门教程(一)图片绘制中的上传图片逻辑:

    Byte *imageBytes = [self loadImage:image];
    if (imageBytes) { // UIImage的数据需要转成二进制才能上传,且不用jpg、png的NSData
        [self.texture replaceRegion:region
                    mipmapLevel:0
                      withBytes:imageBytes
                    bytesPerRow:4 * image.size.width];
        free(imageBytes); // 需要释放资源
        imageBytes = NULL;
    }

replaceRegion如果用在需要频繁上传纹理的视频渲染场景,会有很多等待的时间。 为了提升性能,CoreVideo提供了新的接口,也就是本文介绍的重点。 苹果的文档上没有介绍此方案的实现,通过查阅资料,猜测苹果是通过DMA的方式提供更高效率的访问。 从DMA的资料可以看出,苹果会创建一块与GPU高速交流的内存,再把这块内存和视频渲染用的缓存进行关联。 整体的架构如下:

手绘的大致架构

通过苹果的头文件,我们知道 CVBufferRef = CVImageBufferRef = CVMetalTextureRef CVImageBufferRef CVPixelBufferRef 当CVPixelBufferRef和CVMetalTextureRef绑定之后,通过getText的接口,我们可以拿到Metal用的纹理,所有渲染到该纹理的数据,会通过高速通道返回给CPU。

Similarly, attempts to change texture data from CPU memory with commands like glTexSubImage2D can block until commands that use that texture have finished.They may not block, as some implementations will just allocate some CPU memory and copy the user's pixel data into that. They will do the DMA directly to the texture some time later.

具体步骤

1、设置管道
// 设置渲染管道
-(void)setupPipeline {
    id<MTLLibrary> defaultLibrary = [self.mtkView.device newDefaultLibrary]; // .metal
    id<MTLFunction> vertexFunction = [defaultLibrary newFunctionWithName:@"vertexShader"]; // 顶点shader,vertexShader是函数名
    id<MTLFunction> fragmentFunction = [defaultLibrary newFunctionWithName:@"samplingShader"]; // 片元shader,samplingShader是函数名
    
    MTLRenderPipelineDescriptor *pipelineStateDescriptor = [[MTLRenderPipelineDescriptor alloc] init];
    pipelineStateDescriptor.vertexFunction = vertexFunction;
    pipelineStateDescriptor.fragmentFunction = fragmentFunction;
    pipelineStateDescriptor.colorAttachments[0].pixelFormat = self.mtkView.colorPixelFormat; // 设置颜色格式
    self.pipelineState = [self.mtkView.device newRenderPipelineStateWithDescriptor:pipelineStateDescriptor
                                                                             error:NULL]; // 创建图形渲染管道,耗性能操作不宜频繁调用
    self.commandQueue = [self.mtkView.device newCommandQueue]; // CommandQueue是渲染指令队列,保证渲染指令有序地提交到GPU
}

这一步的设置与之前一致。

2、设置顶点
- (void)setupVertex {
    static const LYVertex 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(LYVertex); // 顶点个数
}

这次为了让视频全屏显示,把顶点的大小都设置到1。

3、设置矩阵
- (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}; // 这个是偏移
    
    LYConvertMatrix matrix;
    // 设置参数
    matrix.matrix = kColorConversion601FullRangeMatrix;
    matrix.offset = kColorConversion601FullRangeOffset;
    
    self.convertMatrix = [self.mtkView.device newBufferWithBytes:&matrix
                                                          length:sizeof(LYConvertMatrix)
                                                         options:MTLResourceStorageModeShared];
}

转换矩阵根据读取视频时设置的参数,分别有三种可能:

  • kColorConversion601Default
  • kColorConversion601FullRangeDefault
  • kColorConversion709Default

demo中均有具体的参数值。 LYConvertMatrix是自定义的矩阵结构体,包括一个矩阵和一个向量,用于YUV到RGB的颜色空间转换。

4、设置纹理
// 设置纹理
- (void)setupTextureWithEncoder:(id<MTLRenderCommandEncoder>)encoder buffer:(CMSampleBufferRef)sampleBuffer {
    CVPixelBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer); // 从CMSampleBuffer读取CVPixelBuffer,
    
    id<MTLTexture> textureY = nil;
    id<MTLTexture> textureUV = nil;
    // textureY 设置
    {
        size_t width = CVPixelBufferGetWidthOfPlane(pixelBuffer, 0);
        size_t height = CVPixelBufferGetHeightOfPlane(pixelBuffer, 0);
        MTLPixelFormat pixelFormat = MTLPixelFormatR8Unorm; // 这里的颜色格式不是RGBA

        CVMetalTextureRef texture = NULL; // CoreVideo的Metal纹理
        CVReturn status = CVMetalTextureCacheCreateTextureFromImage(NULL, self.textureCache, pixelBuffer, NULL, pixelFormat, width, height, 0, &texture);
        if(status == kCVReturnSuccess)
        {
            textureY = CVMetalTextureGetTexture(texture); // 转成Metal用的纹理
            CFRelease(texture);
        }
    }
    
    // textureUV 设置
    {
        size_t width = CVPixelBufferGetWidthOfPlane(pixelBuffer, 1);
        size_t height = CVPixelBufferGetHeightOfPlane(pixelBuffer, 1);
        MTLPixelFormat pixelFormat = MTLPixelFormatRG8Unorm; // 2-8bit的格式
        
        CVMetalTextureRef texture = NULL; // CoreVideo的Metal纹理
        CVReturn status = CVMetalTextureCacheCreateTextureFromImage(NULL, self.textureCache, pixelBuffer, NULL, pixelFormat, width, height, 1, &texture);
        if(status == kCVReturnSuccess)
        {
            textureUV = CVMetalTextureGetTexture(texture); // 转成Metal用的纹理
            CFRelease(texture);
        }
    }
    
    if(textureY != nil && textureUV != nil)
    {
        [encoder setFragmentTexture:textureY
                            atIndex:LYFragmentTextureIndexTextureY]; // 设置纹理
        [encoder setFragmentTexture:textureUV
                            atIndex:LYFragmentTextureIndexTextureUV]; // 设置纹理
    }
    CFRelease(sampleBuffer); // 记得释放
}

设置纹理的时候需要分两步,首先是读取Y纹理,此时用的是MTLPixelFormatR8Unorm格式,并不是RGBA的方式进行读取,参考自YUV格式。 同理,读取UV纹理的时候,用的格式是MTLPixelFormatRG8Unorm,这表示16bit。

5、渲染处理
- (void)drawInMTKView:(MTKView *)view {
    // 每次渲染都要单独创建一个CommandBuffer
    id<MTLCommandBuffer> commandBuffer = [self.commandQueue commandBuffer];
    MTLRenderPassDescriptor *renderPassDescriptor = view.currentRenderPassDescriptor;
    // MTLRenderPassDescriptor描述一系列attachments的值,类似GL的FrameBuffer;同时也用来创建MTLRenderCommandEncoder
    CMSampleBufferRef sampleBuffer = [self.reader readBuffer]; // 从LYAssetReader中读取图像数据
    if(renderPassDescriptor && sampleBuffer)
    {
        renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColorMake(0.0, 0.5, 0.5, 1.0f); // 设置默认颜色
        id<MTLRenderCommandEncoder> renderEncoder = [commandBuffer renderCommandEncoderWithDescriptor:renderPassDescriptor]; //编码绘制指令的Encoder
        [renderEncoder setViewport:(MTLViewport){0.0, 0.0, self.viewportSize.x, self.viewportSize.y, -1.0, 1.0 }]; // 设置显示区域
        [renderEncoder setRenderPipelineState:self.pipelineState]; // 设置渲染管道,以保证顶点和片元两个shader会被调用
        
        [renderEncoder setVertexBuffer:self.vertices
                                offset:0
                               atIndex:LYVertexInputIndexVertices]; // 设置顶点缓存
        
        [self setupTextureWithEncoder:renderEncoder buffer:sampleBuffer];
        [renderEncoder setFragmentBuffer:self.convertMatrix
                                  offset:0
                                 atIndex:LYFragmentInputIndexMatrix];
        
        [renderEncoder drawPrimitives:MTLPrimitiveTypeTriangle
                          vertexStart:0
                          vertexCount:self.numVertices]; // 绘制
        
        [renderEncoder endEncoding]; // 结束
        
        [commandBuffer presentDrawable:view.currentDrawable]; // 显示
    }
    
    [commandBuffer commit]; // 提交;
}

在每次的渲染回调中,都要从LYAssetReader读取一帧视频数据,然后进行处理。

6、Shader逻辑
typedef struct
{
    float4 clipSpacePosition [[position]]; // position的修饰符表示这个是顶点
    
    float2 textureCoordinate; // 纹理坐标,会做插值处理
    
} RasterizerData;

vertex RasterizerData // 返回给片元着色器的结构体
vertexShader(uint vertexID [[ vertex_id ]], // vertex_id是顶点shader每次处理的index,用于定位当前的顶点
             constant LYVertex *vertexArray [[ buffer(LYVertexInputIndexVertices) ]]) { // buffer表明是缓存数据,0是索引
    RasterizerData out;
    out.clipSpacePosition = vertexArray[vertexID].position;
    out.textureCoordinate = vertexArray[vertexID].textureCoordinate;
    return out;
}

fragment float4
samplingShader(RasterizerData input [[stage_in]], // stage_in表示这个数据来自光栅化。(光栅化是顶点处理之后的步骤,业务层无法修改)
               texture2d<float> textureY [[ texture(LYFragmentTextureIndexTextureY) ]], // texture表明是纹理数据,LYFragmentTextureIndexTextureY是索引
               texture2d<float> textureUV [[ texture(LYFragmentTextureIndexTextureUV) ]], // texture表明是纹理数据,LYFragmentTextureIndexTextureUV是索引
               constant LYConvertMatrix *convertMatrix [[ buffer(LYFragmentInputIndexMatrix) ]]) //buffer表明是缓存数据,LYFragmentInputIndexMatrix是索引
{
    constexpr sampler textureSampler (mag_filter::linear,
                                      min_filter::linear); // sampler是采样器
    
    float3 yuv = float3(textureY.sample(textureSampler, input.textureCoordinate).r,
                          textureUV.sample(textureSampler, input.textureCoordinate).rg);
    
    float3 rgb = convertMatrix->matrix * (yuv + convertMatrix->offset);
        
    return float4(rgb, 1.0);
}

RasterizerData是自定义的结构体,用于vertex Shader返回数据给fragment shader; Metal种的内存访问主要有两种方式:Device模式和Constant模式。Device模式是比较通用的访问模式,使用限制比较少,而Constant模式是为了多次读取而设计的快速访问只读模式,通过Constant内存模式访问的参数的数据的字节数量是固定的,所以LYConvertMatrix参数用的是constant模式。

总结

Metal是今年学习的一个重点,如何使用API是其次,重点是学习苹果如何设计Metal这个语言。

Demo的地址在Github 引用:OpenGL下的同步与异步操作

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏阿炬.NET

FineUIMvc表格数据库分页,使用CYQ.Data组件

1264
来自专栏kalifaの日々

动态规划真的可以为所欲为的(Leetcode 62/63)

看起来不错的运行效率 62题: 动态规划递推公式: 站在当前方块上可选择的路径数量 = 我正下方那个方块可选择的路径数量 + 我右侧那个方块可选择的路径数量; ...

3606
来自专栏人工智能

如何在Weka中加载CSV机器学习数据

原文地址:https://machinelearningmastery.com/load-csv-machine-learning-data-weka/

54810
来自专栏HT

HTML5五种客户端离线存储方案

最近折腾HTML5游戏需要离线存储功能,便把目前可用的几种HTML5存储方式研究了下,基于HT for Web写了个综合的实例,分别利用了Cookie、WebS...

2366
来自专栏Android点滴积累

Android高效内存之让你的图片省内存

Android高效内存之让你的图片省内存        在做内存优化的时候,我们发现除了解决内存泄露问题,剩下的就只有想办法减少真实的内存占用。而在App中,大...

21611
来自专栏hightopo

原 HTML5五种客户端离线存储方案

1334
来自专栏生信宝典

利用ComplexHeatmap绘制热图(一)

作者:严涛 浙江大学作物遗传育种在读研究生(生物信息学方向)伪码农,R语言爱好者,爱开源。

4352
来自专栏CDA数据分析师

Python 爬取北京二手房数据,分析北漂族买得起房吗? | 附完整源码

本文主要分为两部分:Python爬取赶集网北京二手房数据,R对爬取的二手房房价做线性回归分析,适合刚刚接触Python&R的同学们学习参考。

1172
来自专栏非典型技术宅

Quartz2D进行渲染1. 渲染模式2. even-odd rule:奇偶填充规则3. nonzero winding number rule:非零绕数规则4. 其他会用到的渲染模式5. 混合模式

1203
来自专栏逍遥剑客的游戏开发

挑战蓝龙Kalecgos

1072

扫码关注云+社区