专栏首页落影的专栏Metal入门教程(五)视频渲染
原创

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 条评论
登录 后参与评论

相关文章

  • Metal入门教程(二)三维变换

    上一篇的教程介绍了如何绘制一张图片,这次的目标是把图片显示到3D物体上,并进行三维变换。

    落影
  • Metal入门教程总结

    本文介绍Metal和Metal Shader Language,以及Metal和OpenGL ES的差异性,也是实现入门教程的心得总结。

    落影
  • Metal入门教程(六)边界检测

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

    落影
  • Pandas的对齐运算

    王小婷
  • 架构细节 | 看看 Medium 的开发团队用了哪些技术?

    image.png 说到底,Medium是个社交网络,人们可以在这里分享有意思的故事和想法。据统计,目前累积的用户阅读时间已经超过14亿分钟,合两千六百年。 ...

    春哥大魔王
  • 灵活运用CSS开发技巧

    何为技巧,意指表现在文学、工艺、体育等方面的巧妙技能。代码作为一门现代高级工艺,推动着人类科学技术的发展,同时犹如文字一样承托着人类文化的进步。

    Nealyang
  • 89 次荣登活跃榜,最高排名第 9 ,从零学算法第二周周报发布

    当搜索一个键时,哈希表使用相同的哈希函数来查找对应的桶,并只在特定的桶中进行搜索。

    double
  • 微信搜索新发现:iPhone 内存不足看这里!

    还一直坚持着小内存的小伙伴,你的 iPhone 是不是每天都在提醒你内存不足,但你又无能为力呢?真是苦了你们这些小仙男、小仙女了... ? 那哎妹今天不搞事,只...

    企鹅号小编
  • ADO对SQL Server 2008数据库的基础操作

    最近在学习ADO与数据库的相关知识,现在我将自己学到的东西整理写出来,也算是对学习的一种复习。

    Masimaro
  • 性能测试中会遇到的瓶颈

    性能测试这种测试方式在发生过程中,其中一个过渡性的工作,就是对执行过程中的问题,进行定位,对功能的定位,对负载的定位,最重要的,当然就是问题中说的“瓶颈”,接触...

    小老鼠

扫码关注云+社区

领取腾讯云代金券