前言
Metal入門教程(一)圖片繪制
Metal入門教程(二)三維變換
Metal入門教程(三)攝像頭采集渲染
Metal入門教程(四)灰度計(jì)算
Metal入門教程(五)視頻渲染
前面的教程既介紹了Metal的圖片繪制、三維變換、視頻渲染,也引入MetalPerformanceShaders處理攝像頭數(shù)據(jù)以及用計(jì)算管道實(shí)現(xiàn)灰度計(jì)算,這次嘗試實(shí)現(xiàn)sobel邊界檢測(cè)。
Metal系列教程的代碼地址;
OpenGL ES系列教程在這里;
你的star和fork是我的源動(dòng)力,你的意見(jiàn)能讓我走得更遠(yuǎn)。
正文
Metal shading language
這次的學(xué)習(xí)重點(diǎn)是Metal的shader語(yǔ)言Metal shading language,主要有兩個(gè)用途圖形渲染和通用計(jì)算。
Metal著色語(yǔ)言支持部分C++特性,比如說(shuō)重載(除了聲明為圖形渲染和通用計(jì)算入口的函數(shù));Metal著色語(yǔ)言不支持遞歸函數(shù)調(diào)用、new和delete操作符、虛函數(shù)、異常處理、函數(shù)指針等特性。同樣,Metal有自己的標(biāo)準(zhǔn)庫(kù),不能用C++ 11的標(biāo)準(zhǔn)庫(kù)。
Metal中常用的數(shù)據(jù)結(jié)構(gòu)有向量、矩陣、原子數(shù)據(jù)類型、緩存、紋理、采樣器、數(shù)組、用戶自定義結(jié)構(gòu)體等,C++的數(shù)據(jù)結(jié)構(gòu)double, long, unsigned long, long long,unsigned long long, long double均不支持。
常用的基礎(chǔ)數(shù)據(jù)類型:
- half 是16bit是浮點(diǎn)數(shù),表達(dá)方式0.5h
- float 是32bit的浮點(diǎn)數(shù),表達(dá)方式0.5f
- size_t 是64bit的無(wú)符號(hào)整數(shù),通常用于sizeof的返回值
- ptrdiff_t 是64bit的有符號(hào)整數(shù),通常用于指針的差值
常用的向量數(shù)據(jù)類型:
- 一維向量:half2、half3、half4、float2、float3、float4等。
- 二維向量:half4x4、half3x3、float4x4、float3x3等;
對(duì)于向量的訪問(wèn),比如說(shuō)vec=float4(1.0f, 1.0f, 1.0f, 1.0f),其訪問(wèn)方式可以是vec[0]、vec[1],也可以是vec.x、vec.y,也可以是vec.r、vec.g。(.xyzw和.rgba,前者對(duì)應(yīng)三維坐標(biāo),后者對(duì)應(yīng)RGB顏色空間)
同時(shí)只取部分、亂序取均可,比如說(shuō)我們常用到的float4 color=texture.bgra;
Metal關(guān)鍵函數(shù)用到的指針參數(shù)要用地址空間修飾符,如下面的buffer參數(shù)
vertex RasterizerData // 返回給片元著色器的結(jié)構(gòu)體
vertexShader(uint vertexID [[ vertex_id ]], // vertex_id是頂點(diǎn)shader每次處理的index,用于定位當(dāng)前的頂點(diǎn)
constant LYVertex *vertexArray [[ buffer(0) ]]) { // buffer表明是緩存數(shù)據(jù),0是索引
Metal內(nèi)存訪問(wèn)模式主要有兩種:Device和Constant。
- Device模式,通用的訪問(wèn)模式,使用限制比較少;
- Constant模式,快速訪問(wèn)只讀模式,參數(shù)對(duì)應(yīng)buffer大小不能改變;
需要注意的是,有些GPU支持**前置深度測(cè)試(early depth testing),其允許在fragment shader之前進(jìn)行深度測(cè)試。如果某個(gè)像素點(diǎn)沒(méi)有被顯示,那么可以放棄渲染,以減少運(yùn)算。
Metal同樣支持前置深度測(cè)試,實(shí)現(xiàn)方式是在fragment關(guān)鍵字前面加上[[early_fragment_tests]],且前置深度測(cè)試要求不能對(duì)像素的深度值進(jìn)行寫(xiě)操作。
demo思路
基于Sobel算子,對(duì)圖像進(jìn)行邊界檢測(cè)。自定義計(jì)算shader,接受圖像的輸入并輸出檢測(cè)后的結(jié)果,效果如下:

Sobel算子的實(shí)現(xiàn)需要訪問(wèn)像素周邊的8個(gè)像素的值,在compute shader中,我們可以通過(guò)修改grid的xy坐標(biāo)進(jìn)行操作。在拿到位置的坐標(biāo)后,通過(guò)sourceTexture.read讀取像素值,分別算出橫向和豎向的差別h和v,統(tǒng)一轉(zhuǎn)亮度值。最后求h和v的向量和,再寫(xiě)回紋理中。
constant int sobelStep = 2;
constant half3 kRec709Luma = half3(0.2126, 0.7152, 0.0722); // 把rgba轉(zhuǎn)成亮度值
kernel void
sobelKernel(texture2d<half, access::read> sourceTexture [[texture(LYFragmentTextureIndexTextureSource)]],
texture2d<half, access::write> destTexture [[texture(LYFragmentTextureIndexTextureDest)]],
uint2 grid [[thread_position_in_grid]])
{
/*
行數(shù) 9個(gè)像素 位置
上 | * * * | | 左 中 右 |
中 | * * * | | 左 中 右 |
下 | * * * | | 左 中 右 |
*/
half4 topLeft = sourceTexture.read(uint2(grid.x - sobelStep, grid.y - sobelStep)); // 左上
half4 top = sourceTexture.read(uint2(grid.x, grid.y - sobelStep)); // 上
half4 topRight = sourceTexture.read(uint2(grid.x + sobelStep, grid.y - sobelStep)); // 右上
half4 centerLeft = sourceTexture.read(uint2(grid.x - sobelStep, grid.y)); // 中左
half4 centerRight = sourceTexture.read(uint2(grid.x + sobelStep, grid.y)); // 中右
half4 bottomLeft = sourceTexture.read(uint2(grid.x - sobelStep, grid.y + sobelStep)); // 下左
half4 bottom = sourceTexture.read(uint2(grid.x, grid.y + sobelStep)); // 下中
half4 bottomRight = sourceTexture.read(uint2(grid.x + sobelStep, grid.y + sobelStep)); // 下右
half4 h = -topLeft - 2.0 * top - topRight + bottomLeft + 2.0 * bottom + bottomRight; // 橫方向差別
half4 v = -bottom - 2.0 * centerLeft - topLeft + bottomRight + 2.0 * centerRight + topRight; // 豎方向差別
half grayH = dot(h.rgb, kRec709Luma); // 轉(zhuǎn)換成亮度
half grayV = dot(v.rgb, kRec709Luma); // 轉(zhuǎn)換成亮度
// sqrt(h^2 + v^2),相當(dāng)于求點(diǎn)到(h, v)的距離,所以可以用length
half color = length(half2(grayH, grayV));
destTexture.write(half4(color, color, color, 1.0), grid); // 寫(xiě)回對(duì)應(yīng)紋理
}
demo中以Camera為圖像輸入源,實(shí)時(shí)對(duì)每一幀的圖像進(jìn)行處理

總結(jié)
Metal shading language的重要性不言而喻,Metal入門教程(四)灰度計(jì)算重在如何搭建計(jì)算shader的通道,而sobel的實(shí)現(xiàn)相對(duì)灰度計(jì)算略為復(fù)雜,更有益于實(shí)踐練習(xí),后續(xù)還會(huì)補(bǔ)上直方圖均衡的demo。