iOS 利用 Metal 實(shí)現(xiàn) LUT 濾鏡

源碼地址:MetalCode

之前探究過 iOS 上通過 CoreImage踊兜、OpenGLES 等技術(shù)實(shí)現(xiàn) LUT 濾鏡的對(duì)比 -- iOS 針對(duì) LUT 濾鏡的實(shí)現(xiàn)對(duì)比竿滨,但是其實(shí)在圖形處理這塊,Apple 更推崇自家公司的 Metal捏境,這是一個(gè)和 OpenGLES 類似的面向底層的圖形編程接口于游,最早在 2014 年的 WWDC 的時(shí)候發(fā)布,可用于從 CPU 發(fā)送指令到 GPU 驅(qū)動(dòng) GPU 進(jìn)行大量并行矩陣運(yùn)算垫言。

Metal 提供以下特性:

  • 低開銷接口贰剥。Metal 被設(shè)計(jì)用于消滅像狀態(tài)檢查一類的隱性性能瓶頸,你可以控制 GPU 的異步行為筷频,以實(shí)現(xiàn)用于并行創(chuàng)建和提交命令緩沖區(qū)的高效多線程操作
  • 內(nèi)存和資源管理蚌成。Metal 框架提供了表示 GPU 內(nèi)存分配的緩沖區(qū)和紋理對(duì)象前痘,紋理對(duì)象具有確切的像素格式,能被用于紋理圖像或附件
  • 集成對(duì)圖形和計(jì)算操作的支持担忧。Metal 對(duì)圖形操作和計(jì)算操作使用了相同的數(shù)據(jù)結(jié)構(gòu)和資源(如 buffer芹缔、texture、command queue)瓶盛,Metal 的著色器語言同時(shí)支持圖形函數(shù)和計(jì)算函數(shù)乖菱,Metal 框架支持在運(yùn)行時(shí)接口(CPU)、圖形著色器和計(jì)算方法間共享資源
  • 預(yù)編譯著色器蓬网。Metal 的著色器函數(shù)能與代碼一同在編譯器編譯窒所,并在運(yùn)行時(shí)加載,這樣的流程能提供更方便的著色器調(diào)試功能帆锋。

Metal 的對(duì)象關(guān)系如圖所示

image

其中連接 CPU 和 GPU 的就是命令隊(duì)列 Command Queue吵取,其上裝載多個(gè)命令緩沖 Command Buffer,Command Buffer 里能承載 Metal 定義的多種圖形锯厢、計(jì)算命令編碼器皮官,在編碼器中就是開發(fā)者創(chuàng)建的實(shí)際的命令和資源,它們最終被傳送到 GPU 中進(jìn)行計(jì)算和渲染实辑。

接下來就用 Metal 實(shí)現(xiàn)一個(gè)圖片 LUT 濾鏡捺氢。

1. 初始化

Metal 初始化工作主要將一些初始化開銷大、能夠復(fù)用的對(duì)象進(jìn)行預(yù)先生成和持有.

首先初始化 GPU 接口剪撬,可以理解為持有 GPU摄乒,在 Metal 中它被定義為 MTLDevice 類型的對(duì)象

    self.mtlDevice = MTLCreateSystemDefaultDevice(); // 獲取 GPU 接口

其次初始化一個(gè) MTKView,它相當(dāng)于畫布残黑,用于 GPU 渲染內(nèi)容到屏幕上

    [self.view addSubview:self.mtlView];
    [self.mtlView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.left.top.equalTo(self.view);
        make.width.height.equalTo(self.view);
    }];

Metal 的渲染流程與 OpenGLES 很類似馍佑,大致如下圖所示

因此同樣的,需要傳入頂點(diǎn)數(shù)據(jù)梨水、頂點(diǎn)著色器和片段著色器

頂點(diǎn)數(shù)據(jù)定義如下拭荤,每一行的前四個(gè)分量為頂點(diǎn)坐標(biāo),后兩個(gè)分量為紋理坐標(biāo)(歸一化)疫诽。

static const float vertexArrayData[] = {
    -1.0, -1.0, 0.0, 1.0, 0, 1,
    -1.0, 1.0, 0.0, 1.0, 0, 0,
    1.0, -1.0, 0.0, 1.0, 1, 1,
    -1.0, 1.0, 0.0, 1.0, 0, 0,
    1.0, 1.0, 0.0, 1.0, 1, 0,
    1.0, -1.0, 0.0, 1.0, 1, 1
};

然后加載到頂點(diǎn) buffer 中

    self.vertexBuffer = [self.mtlDevice newBufferWithBytes:vertexArrayData length:sizeof(vertexArrayData) options:0]; // 利用數(shù)組初始化一個(gè)頂點(diǎn)緩存舅世,MTLResourceStorageModeShared 資源存儲(chǔ)在CPU和GPU都可訪問的系統(tǒng)存儲(chǔ)器中

Metal 搜索頂點(diǎn)著色器和片段著色器的范圍是以 Bundle 為維度的,在一個(gè) Bundle 內(nèi)放進(jìn)任意名稱的 Metal 文件奇徒,其中的著色器函數(shù)都可以被 Metal 搜索并加載到內(nèi)存中雏亚。

    id<MTLLibrary> library = [self.mtlDevice newDefaultLibraryWithBundle:[NSBundle bundleWithPath:[[NSBundle mainBundle] pathForResource:@"XXXXXX" ofType:@"bundle"]] error:nil];
    id<MTLFunction> vertextFunc = [library newFunctionWithName:@"vertex_func"];
    id<MTLFunction> fragFunc = [library newFunctionWithName:@"fragment_func"]; //從 bundle 中獲取頂點(diǎn)著色器和片段著色器

接下來要將著色器函數(shù)裝配到渲染管線上,需要用到 MTLRenderPipelineDescriptor 對(duì)象

    MTLRenderPipelineDescriptor *pipelineDescriptor = [MTLRenderPipelineDescriptor new];
    pipelineDescriptor.vertexFunction = vertextFunc;
    pipelineDescriptor.fragmentFunction = fragFunc;
    pipelineDescriptor.colorAttachments[0].pixelFormat = MTLPixelFormatBGRA8Unorm; //此設(shè)置配置像素格式逼龟,以便通過渲染管線的所有內(nèi)容都符合相同的顏色分量順序(在本例中為Blue(藍(lán)色)评凝,Green(綠色)妒牙,Red(紅色)账劲,Alpha(阿爾法))以及尺寸(在這種情況下逮诲,8-bit(8位)顏色值變?yōu)?從0到255)
    self.pipelineState = [self.mtlDevice newRenderPipelineStateWithDescriptor:pipelineDescriptor error:nil]; // 初始化一個(gè)渲染管線狀態(tài)描述位损话,相當(dāng)于 CPU 和 GPU 之間建立的管道

最后是初始化渲染隊(duì)列,以及創(chuàng)建紋理緩存

    self.commandQueue = [self.mtlDevice newCommandQueue]; // 獲取一個(gè)渲染隊(duì)列翎碑,其中裝載需要渲染的指令 MTLCommandBuffer    
    CVMetalTextureCacheCreate(NULL, NULL, self.mtlDevice, NULL, &_textureCache); // 創(chuàng)建紋理緩存

2. 圖片紋理加載

Metal 為加載圖片紋理提供了便捷類 MTKTextureLoader谬返,能夠根據(jù)多種參數(shù)生成 MTLTexture 紋理對(duì)象,但是實(shí)際使用中發(fā)現(xiàn)了兩個(gè)問題:

  • 問題1:BGRA 問題

Metal 的渲染視圖 MTKView 默認(rèn)支持的 pixelFormat 是 MTLPixelFormatBGRA8Unorm日杈,而說明文檔上說 MTKView 還支持以下格式

The color pixel format for the current drawable's texture.
The pixel format for a MetalKit view must be MTLPixelFormatBGRA8Unorm, MTLPixelFormatBGRA8Unorm_sRGB, MTLPixelFormatRGBA16Float, MTLPixelFormatBGRA10_XR, or MTLPixelFormatBGRA10_XR_sRGB.

但是我嘗試設(shè)置為其他值時(shí)都發(fā)生了 crash遣铝,所以整個(gè)渲染流程、命令編碼過程中都需要設(shè)置 pixelFormat 為 BGRA 格式莉擒,這樣遇到的問題就是針對(duì)一些內(nèi)部像素排列順序是 RGBA 格式的圖片酿炸,生成的紋理和最終渲染出來的圖片會(huì)發(fā)藍(lán),為了確保傳入的圖片都是 BGRA 格式的圖片涨冀,我預(yù)先將傳入的圖片按 BGRA 渲染到 CGContext 上填硕,再提取出 UIImage 對(duì)象傳入

- (unsigned char *)bitmapFromImage:(UIImage *)targetImage
{
    CGImageRef imageRef = targetImage.CGImage;
    
    NSUInteger iWidth = CGImageGetWidth(imageRef);
    NSUInteger iHeight = CGImageGetHeight(imageRef);
    NSUInteger iBytesPerPixel = 4;
    NSUInteger iBytesPerRow = iBytesPerPixel * iWidth;
    NSUInteger iBitsPerComponent = 8;
    unsigned char *imageBytes = malloc(iWidth * iHeight * iBytesPerPixel);
    
    CGColorSpaceRef colorspace = CGColorSpaceCreateDeviceRGB();
    
    CGContextRef context = CGBitmapContextCreate(imageBytes,
                                                 iWidth,
                                                 iHeight,
                                                 iBitsPerComponent,
                                                 iBytesPerRow,
                                                 colorspace,
                                                 kCGBitmapByteOrder32Little | kCGImageAlphaPremultipliedFirst); // 轉(zhuǎn) BGRA 格式
    
    CGRect rect = CGRectMake(0, 0, iWidth, iHeight);
    CGContextDrawImage(context, rect, imageRef);
    CGColorSpaceRelease(colorspace);
    CGContextRelease(context);
    return imageBytes;
}

- (NSData *)imageDataFromBitmap:(unsigned char *)imageBytes imageSize:(CGSize)imageSize
{
    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
    CGContextRef context = CGBitmapContextCreate(imageBytes,
                                                 imageSize.width,
                                                 imageSize.height,
                                                 8,
                                                 imageSize.width * 4,
                                                 colorSpace,
                                                 kCGBitmapByteOrder32Little | kCGImageAlphaPremultipliedFirst);
    CGImageRef imageRef = CGBitmapContextCreateImage(context);
    CGContextRelease(context);
    CGColorSpaceRelease(colorSpace);
    
    UIImage *result = [UIImage imageWithCGImage:imageRef];
    NSData *imageData = UIImagePNGRepresentation(result);
    CGImageRelease(imageRef);
    return imageData;
}
  • 問題2:sRGB 問題

這個(gè)問題的表現(xiàn)現(xiàn)象是最終渲染出來的圖片偏暗,即使是原圖也會(huì)偏暗鹿鳖,StackOverflow 上有很多對(duì)這個(gè) strange color 問題的回答扁眯,均提到將 MTKTextureLoader 的 MTKTextureLoaderOptionSRGB 選項(xiàng)設(shè)置為 NO,它默認(rèn)為 YES翅帜。

我的理解是姻檀,sRGB 實(shí)際上是一種對(duì)顏色的編碼,其效果是增加暗色域的編碼精度涝滴,降低亮色域的編碼精度绣版。那么針對(duì) sRGB 編碼的圖片就需要進(jìn)行一次 gamma 校正,以確保進(jìn)行諸如 LUT 對(duì)照計(jì)算時(shí)能夠嚴(yán)格按照線性 RGB 計(jì)算狭莱。但是實(shí)際上我傳入的圖片都是以 RGB 格式排列僵娃,因此不需要進(jìn)行 gamma 校正,如果不設(shè)置關(guān)閉 sRGB 的校正腋妙,就會(huì)對(duì)線性 RGB 格式的數(shù)據(jù)進(jìn)行校正,導(dǎo)致最終圖片偏暗讯榕。這個(gè)問題在我做 CoreImage 的濾鏡調(diào)研時(shí)也出現(xiàn)過骤素,下面是效果圖

  • 最終代碼

生成 LUT 紋理代碼

    unsigned char *imageBytes = [self bitmapFromImage:lutImage];
    NSData *imageData = [self imageDataFromBitmap:imageBytes imageSize:CGSizeMake(CGImageGetWidth(lutImage.CGImage), CGImageGetHeight(lutImage.CGImage))];
    free(imageBytes);
    self.lutTexture = [loader newTextureWithData:imageData options:@{MTKTextureLoaderOptionSRGB:@(NO)} error:&err]; // 生成 LUT 濾鏡紋理

生成原圖紋理代碼

    unsigned char *imageBytes = [self bitmapFromImage:image];
    NSData *imageData = [self imageDataFromBitmap:imageBytes imageSize:CGSizeMake(CGImageGetWidth(image.CGImage), CGImageGetHeight(image.CGImage))];
    free(imageBytes);    
    MTKTextureLoader *loader = [[MTKTextureLoader alloc] initWithDevice:self.mtlDevice];
    NSError* err;
    self.originalTexture = [loader newTextureWithData:imageData options:@{MTKTextureLoaderOptionSRGB:@(NO)} error:&err];

3. 著色器代碼

Metal 著色器代碼與 OpenGLES 的著色器類似,因?yàn)樗鼈兊脑矶际且粯拥?/p>

#include <metal_stdlib>
using namespace metal;

struct Vertex {
    packed_float4 position;
    packed_float2 texCoords;
};

struct ColoredVertex
{
    float4 position [[position]];
    float2 texCoords;
};

vertex ColoredVertex vertex_func(constant Vertex *vertices [[buffer(0)]], uint vid [[vertex_id]]) {
    Vertex inVertex = vertices[vid];
    ColoredVertex outVertex;
    outVertex.position = inVertex.position;
    outVertex.texCoords = inVertex.texCoords;
    
    return outVertex;
}

fragment half4 fragment_func(ColoredVertex vert [[stage_in]], texture2d<half> originalTexture [[texture(0)]], texture2d<half> lutTexture [[texture(1)]]) {
    // stage_in 修飾光柵化頂點(diǎn)
    float width = originalTexture.get_width();
    float height = originalTexture.get_height();
    uint2 gridPos = uint2(vert.texCoords.x * width ,vert.texCoords.y * height);
    
    half4 color = originalTexture.read(gridPos);
    
    float blueColor = color.b * 63.0;
    
    int2 quad1;
    quad1.y = floor(floor(blueColor) / 8.0);
    quad1.x = floor(blueColor) - (quad1.y * 8.0);
    
    int2 quad2;
    quad2.y = floor(ceil(blueColor) / 8.0);
    quad2.x = ceil(blueColor) - (quad2.y * 8.0);
    
    half2 texPos1;
    texPos1.x = (quad1.x * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * color.r);
    texPos1.y = (quad1.y * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * color.g);
    
    half2 texPos2;
    texPos2.x = (quad2.x * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * color.r);
    texPos2.y = (quad2.y * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * color.g);
    
    half4 newColor1 = lutTexture.read(uint2(texPos1.x * 512,texPos1.y * 512));
    half4 newColor2 = lutTexture.read(uint2(texPos2.x * 512,texPos2.y * 512));
    
    half4 newColor = mix(newColor1, newColor2, half(fract(blueColor)));
    half4 finalColor = mix(color, half4(newColor.rgb, 1.0), half(1.0));
    
    half4 realColor = half4(finalColor);
    return realColor;
}

此處不再贅述愚屁。

4. 渲染到屏幕

渲染過程首先要獲取到下一個(gè)內(nèi)容區(qū)緩存济竹,即“畫布”

    id<CAMetalDrawable> drawable = [(CAMetalLayer*)[self.mtlView layer] nextDrawable]; // 獲取下一個(gè)可用的內(nèi)容區(qū)緩存,用于繪制內(nèi)容
    if (!drawable) {
        return;
    }

然后生成 MTLRenderPassDescriptor霎槐,相當(dāng)于對(duì)此次渲染流程的描述符

    MTLRenderPassDescriptor *renderPassDescriptor = [self.mtlView currentRenderPassDescriptor]; // 獲取當(dāng)前的渲染描述符
    if (!renderPassDescriptor) {
        return;
    }
    renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColorMake(0.5, 0.5, 0.5, 1.0); // 設(shè)置顏色附件的清除顏色
    renderPassDescriptor.colorAttachments[0].loadAction = MTLLoadActionClear; // 用于避免渲染新的幀時(shí)附帶上舊的內(nèi)容

接下來從命令隊(duì)列中取出一個(gè)可用的命令 buffer送浊,裝載進(jìn)去一個(gè)命令編碼器,命令編碼器里包含著色器所需的頂點(diǎn)丘跌、紋理等

    id<MTLCommandBuffer> commandBuffer = [self.commandQueue commandBuffer]; // 獲取一個(gè)可用的命令 buffer
    id<MTLRenderCommandEncoder> commandEncoder = [commandBuffer renderCommandEncoderWithDescriptor:renderPassDescriptor]; // 通過渲染描述符構(gòu)建 encoder
    [commandEncoder setCullMode:MTLCullModeBack]; // 設(shè)置剔除背面
    [commandEncoder setFrontFacingWinding:MTLWindingClockwise]; // 設(shè)定按順時(shí)針順序繪制頂點(diǎn)的圖元是朝前的
    [commandEncoder setViewport:(MTLViewport){0.0, 0.0, self.mtlView.drawableSize.width, self.mtlView.drawableSize.height, -1.0, 1.0 }]; // 設(shè)置可視區(qū)域
    [commandEncoder setRenderPipelineState:self.pipelineState];// 設(shè)置渲染管線狀態(tài)位
    [commandEncoder setVertexBuffer:self.vertexBuffer offset:0 atIndex:0]; // 設(shè)置頂點(diǎn)buffer
    [commandEncoder setFragmentTexture:self.originalTexture atIndex:0]; // 設(shè)置紋理 0袭景,即原圖
    [commandEncoder setFragmentTexture:self.lutTexture atIndex:1]; // 設(shè)置紋理 1唁桩,即 LUT 圖
    [commandEncoder drawPrimitives:MTLPrimitiveTypeTriangle vertexStart:0 vertexCount:6]; // 繪制三角形圖元

最終提交到隊(duì)列中

    [commandEncoder endEncoding];
    [commandBuffer presentDrawable:drawable];
    [commandBuffer commit];

仍然選取如下原圖做測(cè)試

選擇如下 LUT 圖

lookup.png

最終濾鏡效果

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市耸棒,隨后出現(xiàn)的幾起案子荒澡,更是在濱河造成了極大的恐慌,老刑警劉巖与殃,帶你破解...
    沈念sama閱讀 211,123評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件单山,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡幅疼,警方通過查閱死者的電腦和手機(jī)米奸,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,031評(píng)論 2 384
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來爽篷,“玉大人悴晰,你說我怎么就攤上這事±浅溃” “怎么了膨疏?”我有些...
    開封第一講書人閱讀 156,723評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)钻弄。 經(jīng)常有香客問我佃却,道長(zhǎng),這世上最難降的妖魔是什么窘俺? 我笑而不...
    開封第一講書人閱讀 56,357評(píng)論 1 283
  • 正文 為了忘掉前任饲帅,我火速辦了婚禮,結(jié)果婚禮上瘤泪,老公的妹妹穿的比我還像新娘灶泵。我一直安慰自己,他們只是感情好对途,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,412評(píng)論 5 384
  • 文/花漫 我一把揭開白布赦邻。 她就那樣靜靜地躺著,像睡著了一般实檀。 火紅的嫁衣襯著肌膚如雪惶洲。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,760評(píng)論 1 289
  • 那天膳犹,我揣著相機(jī)與錄音恬吕,去河邊找鬼。 笑死须床,一個(gè)胖子當(dāng)著我的面吹牛铐料,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 38,904評(píng)論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼钠惩,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼柒凉!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起妻柒,我...
    開封第一講書人閱讀 37,672評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤扛拨,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后举塔,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體绑警,經(jīng)...
    沈念sama閱讀 44,118評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,456評(píng)論 2 325
  • 正文 我和宋清朗相戀三年央渣,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了计盒。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,599評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡芽丹,死狀恐怖北启,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情拔第,我是刑警寧澤咕村,帶...
    沈念sama閱讀 34,264評(píng)論 4 328
  • 正文 年R本政府宣布,位于F島的核電站蚊俺,受9級(jí)特大地震影響懈涛,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜泳猬,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,857評(píng)論 3 312
  • 文/蒙蒙 一批钠、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧得封,春花似錦埋心、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,731評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至疫粥,卻和暖如春洋腮,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背手形。 一陣腳步聲響...
    開封第一講書人閱讀 31,956評(píng)論 1 264
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留悯恍,地道東北人库糠。 一個(gè)月前我還...
    沈念sama閱讀 46,286評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親瞬欧。 傳聞我的和親對(duì)象是個(gè)殘疾皇子贷屎,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,465評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容