前言
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下的同步与异步操作