源碼地址: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)系如圖所示
其中連接 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 圖
最終濾鏡效果