最近遇到將 YUV 格式的視頻轉換成 RGB 格式的問題啸如,解決方法也比較多奇颠,比如 openCV 或 OpenGL 等蒜撮,聽聞 Metal 上 CPU 和 GPU 之間可以共享內存數(shù)據,性能甩 OpenGL 幾條街览爵,遂決定用 Metal 來折騰一下置鼻。雖然 Metal 在語法上和 OpenGL ES 有較大的差異,但是 Metal 也是基于可編程渲染管線設計的一套圖形編程接口蜓竹,openGL 上的許多概念箕母,如頂點和片元著色器储藐、幀緩沖、紋理采樣等嘶是,在 Metal 上同樣適用邑茄。
大致流程是:先通過 AVCaptureVideoDataOutput 回調函數(shù)捕獲視頻幀,然后將視頻幀分別拆解成 luma 紋理和 chroma 紋理俊啼,再提交到 Metal 著色器做色彩空間轉換:
- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection;
自定義圖層
使用 OpenGL 的時候肺缕,我們可能會設置自定義圖層 CAEAGLLayer 來展示渲染結果,而 CAMetalLayer 則是 Metal 專門用來渲染的圖層授帕,它也是 CALayer 的子類同木,可以展示 Metal 幀緩沖區(qū)的內容:
+ (Class)layerClass
{
return [CAMetalLayer class];
}
創(chuàng)建命令隊列
首先通過調用 MTLCreateSystemDefaultDevice( ) 函數(shù)來獲取一個系統(tǒng)能夠使用的 MTLDevice 對象,MTLDevice 代表一個執(zhí)行渲染命令的 GPU 設備跛十,然后通過 MTLDevice 對象創(chuàng)建一個命令隊列 MTLCommandQueue:
id <MTLDevice> device = MTLCreateSystemDefaultDevice();
id <MTLCommandQueue> commandQueue = [device newCommandQueue];
MTLDevice 和 MTLCommandQueue 實際上是定義了相關接口的協(xié)議彤路,Metal 中許多的接口定義采用了這種設計方式。使用 Metal 執(zhí)行渲染命令的時候芥映,一般要先將命令經過渲染命令編碼器(MTLRenderCommandEncoder)編碼后洲尊,添加到一個命令緩沖(MTLCommandBuffer)對象,一個命令緩沖可以包含多個被編碼過的命令奈偏,然后命令緩沖對象會被提交到命令隊列(MTLCommandQueue)坞嘀,最后由命令隊列按順序提交給 GPU 處理。
創(chuàng)建渲染管道
1惊来、創(chuàng)建著色器程序
創(chuàng)建一個擴展名為 .metal 的文件丽涩,編寫實現(xiàn)顏色空間轉換的 Shader 代碼( .metal 文件自帶 Metal Shader 語法高亮和語法檢查):
typedef struct {
packed_float3 position;
packed_float2 textureCoordinate;
} AAPLVertex;
typedef struct {
float4 clipSpacePosition [[position]];
float2 textureCoordinate;
} RasterizerData;
vertex RasterizerData
vertexShader(constant AAPLVertex *vertexArray [[ buffer(0) ]],
uint vertexID [[ vertex_id ]])
{
RasterizerData out;
out.clipSpacePosition = float4(vertexArray[vertexID].position,1);
out.textureCoordinate = vertexArray[vertexID].textureCoordinate;
return out;
}
fragment half4
samplingShader(RasterizerData in [[ stage_in ]],
texture2d<float> lumaTexture [[ texture(0) ]],
texture2d<float> chromaTexture [[ texture(1) ]],
sampler textureSampler [[ sampler(0) ]],
constant float3x3 *yuvToRGBMatrix [[ buffer(0) ]])
{
float3 yuv;
yuv.x = lumaTexture.sample(textureSampler, in.textureCoordinate).r - float(0.062745);
yuv.yz = chromaTexture.sample(textureSampler, in.textureCoordinate).rg - float2(0.5);
return half4(half3((*yuvToRGBMatrix) * yuv), yuv.x);
}
Metal Shader 語法確實比較怪異,尤其是變量屬性 [[ attribute(x) ]] 讓筆者懵了好久裁蚁。
Shader 代碼中定義了兩個結構體:
- AAPLVertex 結構體定義了傳入頂點著色器的頂點數(shù)據類型矢渊;
- RasterizerData 結構體定義了從頂點著色器傳入片段著色器的頂點數(shù)據類型;
帶 vertex 標志的函數(shù) vertexShader 是頂點著色器函數(shù)枉证,它接收一個頂點數(shù)組指針 vertexArray 和一個索引 vertexID 作為參數(shù):
constant AAPLVertex *vertexArray [[ buffer(0) ]],
uint vertexID [[ vertex_id ]]
vertexArray 參數(shù)后面緊跟著的屬性 [[ buffer(0) ]] 標明從索引為0的緩沖區(qū)中讀取頂點數(shù)組的值(后面我們會將頂點數(shù)組加載到索引為0的緩沖區(qū)中)矮男,與 [[ buffer(index) ]] 類似的變量屬性還有 [[ texture(index) ]] 和 [[ sampler(index) ]],分別表示讀取索引為 index 的紋理和采樣器室谚,index 對應著我們在渲染命令編碼器中設置紋理毡鉴、緩沖區(qū)或采樣器時指定的索引值。
vertexID 參數(shù)后面緊跟著的屬性 [[ vertex_id ]] 標明當前處理的頂點的索引舞萄,頂點著色器函數(shù)會對頂點數(shù)組 vertexArray 中的每個頂點執(zhí)行一次眨补。這里頂點著色器函數(shù) vertexShader 不對頂點數(shù)據做額外處理,將頂點坐標及其對應的紋理坐標直接輸出倒脓。
帶 fragment 標志的函數(shù) samplingShader 是片元著色器函數(shù),它接收五個參數(shù):
RasterizerData in [[ stage_in ]],
texture2d<float> lumaTexture [[ texture(0) ]],
texture2d<float> chromaTexture [[ texture(1) ]],
sampler textureSampler [[ sampler(0) ]],
constant float3x3 *yuvToRGBMatrix [[ buffer(0) ]]
其中帶 [[ stage_in ]] 標記的 in 參數(shù)是從頂點著色器傳入片段著色器的頂點數(shù)據(包括頂點坐標和紋理坐標)含思;其它參數(shù)包括視頻幀的Y紋理 lumaTexture 和 UV 紋理 chromaTexture崎弃、紋理采樣器 textureSampler 以及 YUV-RGB 的轉換矩陣 yuvToRGBMatrix甘晤,這幾個參數(shù)都是通過渲染命令編碼器設置的。獲取到這些參數(shù)后饲做,就是根據 YUV 到 RGB 的轉換規(guī)則线婚,做一下顏色空間轉換了:
// YUV-RGB 轉換公式
B = 1.164(Y - 0.0627) + 2.018(U - 0.500)
G = 1.164(Y - 0.0627) - 0.813(V - 0.500) - 0.391(U - 0.500)
R = 1.164(Y - 0.0627) + 1.596(V - 0.500)
2、加載著色器程序
創(chuàng)建一個 MTLLibrary 對象來加載頂點著色器和片元著色器程序
id <MTLLibrary> defaultLibrary = [device newDefaultLibrary];
id <MTLFunction> vertexProgram = [defaultLibrary newFunctionWithName:@"vertexShader"];
id <MTLFunction> fragmentProgram = [defaultLibrary newFunctionWithName:@"samplingShader"];
3盆均、創(chuàng)建渲染管道
首先創(chuàng)建一個 MTLRenderPipelineDescriptor 對象塞弊,渲染管道描述符用來指定圖形函數(shù)(包括頂點著色器函數(shù)和片元著色器函數(shù))和多重采樣等渲染配置
MTLRenderPipelineDescriptor *pipelineStateDescriptor = [MTLRenderPipelineDescriptor new];
pipelineStateDescriptor.label = @"Simple Pipeline";
pipelineStateDescriptor.vertexFunction = vertexProgram;
pipelineStateDescriptor.fragmentFunction = fragmentProgram;
pipelineStateDescriptor.colorAttachments[0].pixelFormat = MTLPixelFormatBGRA8Unorm;
設置完頂點著色器、片元著色器函數(shù)和幀緩沖區(qū)的像素格式后泪姨,通過調用同步方法 newRenderPipelineStateWithDescriptor 來編譯頂點和片元著色器程序游沿,
同時生成一個渲染管道狀態(tài)( MTLRenderPipelineState )對象,這一步會比較耗時肮砾,因此 Metal 官方文檔建議應該盡早創(chuàng)建渲染管道狀態(tài)對象并于后期復用該對象:
id <MTLRenderPipelineState> pipelineState = [device newRenderPipelineStateWithDescriptor:pipelineStateDescriptor error:&error];
到這一步诀黍,渲染管道已經創(chuàng)建完成。前面提到 Metal 渲染指令需要經過命令編碼器編碼后才能提交 GPU 處理仗处,著色器程序等渲染管道配置需要通過 MTLRenderPipelineState 對象傳遞給命令編碼器
數(shù)據準備
根據前面 Shader 代碼眯勾,需要向 GPU 傳遞的數(shù)據包括:頂點數(shù)據、視頻幀紋理婆誓、紋理采樣器和 YUV-RGB 轉換矩陣
1吃环、頂點數(shù)據
static const float quad[] =
{
-0.5, 0.5, 0, 1, 1,
0.5, -0.5, 0, 0, 0,
0.5, 0.5, 0, 0, 1,
-0.5, 0.5, 0, 1, 1,
0.5, -0.5, 0, 0, 0,
-0.5, -0.5, 0, 1, 0,
};
每一行的前三個數(shù)字代表了每一個頂點的(x,y洋幻,z)坐標模叙,后兩個數(shù)字代表每個頂點的紋理坐標。為了使用 GPU 繪制頂點數(shù)據鞋屈,需要將它放入緩沖區(qū)(MTLBuffer)中范咨,緩沖區(qū)是被 CPU 和 GPU 共享的內存塊。
id <MTLBuffer> vertexBuffer = [device newBufferWithBytes:quad length:sizeof(quad) options:0];
vertexBuffer.label = @"Vertices";
2厂庇、視頻幀紋理
這里處理的視頻幀(pixelBuffer)是 NV12 格式渠啊,雙平面,存儲順序是先存儲 Y权旷,再 UV 交替存儲替蛉。可以先通過 Core Video 接口從視頻幀中拆解出 Y 平面和 UV 平面數(shù)據拄氯,再將兩個平面數(shù)據分別解析成 Y 紋理和 UV 紋理:
CVMetalTextureCacheRef textureCache;
CVMetalTextureRef yTexture ;
float yWidth = CVPixelBufferGetWidthOfPlane(pixelBuffer, 0);
float yHeight = CVPixelBufferGetHeightOfPlane(pixelBuffer, 0);
CVMetalTextureCacheCreateTextureFromImage(kCFAllocatorDefault, textureCache, pixelBuffer, nil, MTLPixelFormatR8Unorm, yWidth, yHeight, 0, &yTexture);
CVMetalTextureRef uvTexture;
float uvWidth = CVPixelBufferGetWidthOfPlane(pixelBuffer, 1);
float uvHeight = CVPixelBufferGetHeightOfPlane(pixelBuffer, 1);
CVMetalTextureCacheCreateTextureFromImage(kCFAllocatorDefault, textureCache, pixelBuffer, nil, MTLPixelFormatRG8Unorm, uvWidth, uvHeight, 1, &uvTexture);
id<MTLTexture> lumaTexture = CVMetalTextureGetTexture(yTexture);
id<MTLTexture> chromaTexture = CVMetalTextureGetTexture(uvTexture);
3躲查、采樣器
采樣的結果是產生紋素,紋素通常都包含一種顏色译柏,對視頻幀做格式轉換的時候镣煮,需要用采樣器對 Y 紋理和 UV 紋理進行采樣,提取紋素的 Y鄙麦、U典唇、V 分量镊折,再應用顏色空間轉換公式,轉換成 R介衔、G恨胚、B 分量。
首先創(chuàng)建一個采樣器描述符對象炎咖,設置紋理被縮小時使用最近點采樣赃泡,設置紋理被放大時使用線性紋理過濾:
MTLSamplerDescriptor *samplerDescriptor = [MTLSamplerDescriptor new];
samplerDescriptor.minFilter = MTLSamplerMinMagFilterNearest;
samplerDescriptor.magFilter = MTLSamplerMinMagFilterLinear;
采樣器描述符對象描述了如何創(chuàng)建采樣器,接下來我們需要根據采樣器描述符對象創(chuàng)建一個采樣器狀態(tài)對象:
id<MTLSamplerState> samplerState = [device newSamplerStateWithDescriptor:samplerDescriptor];
4乘盼、YUV-RGB 轉換矩陣
根據 YUV-RGB 顏色轉換規(guī)則升熊,構造一個 3x3 的轉換矩陣,并把矩陣放到緩沖區(qū)里:
simd::float3 firstColumn = simd::float3{1.164, 1.164, 1.164};
simd::float3 secondColumn = simd::float3{0, 0.392, 2.017};
simd::float3 thirdColumn = simd::float3{1.596, 0.813, 0};
simd::float3x3 yuvToRGB2 = simd::float3x3{firstColumn, secondColumn, thirdColumn};
id<MTLBuffer> matrixBuffer = [device newBufferWithBytes: &yuvToRGB2 length: sizeof(yuvToRGB2) options:0];
執(zhí)行渲染命令
1蹦肴、創(chuàng)建渲染路徑描述符
從 metalLayer 上獲取一個可繪制的資源對象(CAMetalDrawable)僚碎,它包含一個紋理(MTLTexture)對象,這個紋理對象代表一個可用作圖形呈現(xiàn)命令目標的緩沖區(qū)(一個可被附加到幀緩沖上的紋理):
id<CAMetalDrawable> drawable = [metalLayer nextDrawable];
創(chuàng)建一個渲染路徑描述符(MTLRenderPassDescriptor)對象阴幌,它包含了一些用于呈現(xiàn)渲染結果的附件(包括顏色附件勺阐、深度附件等),通俗地講矛双,通過 MTLRenderPassDescriptor 對象可以給幀緩沖附加顏色附件渊抽、深度附件和模板附件。將 drawable 對象的紋理賦給顏色附件的紋理屬性后议忽,相當于把一個紋理附加到幀緩沖上懒闷,所有渲染命令會寫入到 drawable 對象的紋理上,渲染結果將展示到該 drawable 對象所對應的一個CAMetalLayer 對象上栈幸。同時愤估,我們設置每次執(zhí)行渲染命令前先清除幀緩沖區(qū)顏色,執(zhí)行渲染命令后速址,將結果存儲到幀緩沖區(qū)中:
MTLRenderPassDescriptor *renderPassDescriptor = [MTLRenderPassDescriptor renderPassDescriptor];
MTLRenderPassColorAttachmentDescriptor *colorAttachment = renderPassDescriptor.colorAttachments[0];
colorAttachment.texture = drawable.texture;
colorAttachment.loadAction = MTLLoadActionClear;
colorAttachment.clearColor = MTLClearColorMake(1, 1, 1, 1);
colorAttachment.storeAction = MTLStoreActionStore;
2玩焰、創(chuàng)建命令緩沖
Metal 渲染指令需要經過命令編碼器編碼后添加到一個命令緩沖對象,最后由命令緩沖對象提交到命令隊列執(zhí)行芍锚。前面已經創(chuàng)建好了命令隊列昔园,這里通過命令隊列獲取一個命令緩沖( MTLCommandBuffer )對象:
id <MTLCommandBuffer> commandBuffer = [commandQueue commandBuffer];
3、創(chuàng)建命令編碼器
創(chuàng)建一個命令編碼器( MTLRenderCommandEncoder )并炮,開始編寫繪制指令默刚,編碼器會將我們的繪制指令轉換為 GPU 能理解的語言對象:
id <MTLRenderCommandEncoder> renderEncoder = [commandBuffer renderCommandEncoderWithDescriptor:renderPassDescriptor];
前面我們創(chuàng)建了一個渲染管道狀態(tài)(MTLRenderPipelineState)對象,它包含了預編譯的頂點著色器函數(shù)和片元著色器函數(shù)等管道配置逃魄,這里將該對象賦給命令編碼器荤西,命令編碼器會將著色器程序提交到 GPU 去執(zhí)行:
[renderEncoder setRenderPipelineState:pipelineState];
前面已經準備好了著色器程序運行所需要的數(shù)據,包括頂點數(shù)據、視頻幀紋理皂冰、采樣器等店展,這些數(shù)據將通過命令編碼器傳遞到 GPU 處理(個人感覺 Metal 上的參數(shù)傳遞操作確實要比 OpenGL 來得簡單一些)
// 設置頂點數(shù)據
[renderEncoder setVertexBuffer:vertexBuffer offset:0 atIndex:0];
// 設置轉換矩陣
[renderEncoder setFragmentBuffer:matrixBuffer offset:0 atIndex:0];
// 設置紋理數(shù)據
[renderEncoder setFragmentTexture:videoTexture[0] atIndex:0];
[renderEncoder setFragmentTexture:videoTexture[1] atIndex:1];
// 設置采樣器
[renderEncoder setFragmentSamplerState:samplerState atIndex:0];
一切準備就緒后养篓,通知命令編碼器執(zhí)行圖形繪制秃流,視頻展示區(qū)域是一個矩形,因此需要依據6個頂點坐標來繪制兩個三個角形:
[renderEncoder drawPrimitives:MTLPrimitiveTypeTriangle vertexStart:0 vertexCount:6 instanceCount:2];
結束本次命令編碼過程:
[renderEncoder endEncoding];
通知渲染緩沖柳弄,一旦繪圖指令執(zhí)行完畢舶胀,將渲染結果展示到屏幕上:
[commandBuffer presentDrawable:drawable];
最后,提交渲染緩沖給 GPU 處理:
[commandBuffer commit];
筆者對 Metal 還處于初學狀態(tài)碧注,如有理解錯誤或表述不當嚣伐,歡迎 Metal 大神幫忙指正!