前言
Metal入門教程(一)圖片繪制
Metal入門教程(二)三維變換
Metal入門教程(三)攝像頭采集渲染
Metal入門教程(四)灰度計算
Metal入門教程(五)視頻渲染
Metal入門教程(六)邊界檢測
Metal入門教程(七)天空盒全景
前面的教程介紹了Metal的圖片繪制、三維變換、視頻渲染、天空盒,用計算管道實現(xiàn)灰度計算和sobel邊界檢測,這次探究Metal與OpenGL ES的交互。
Metal系列教程的代碼地址;
OpenGL ES系列教程在這里;
你的star和fork是我的源動力,你的意見能讓我走得更遠。
正文
UIImage是我們常用的圖像類,可以轉(zhuǎn)成CVPixelBufferRef,表示存儲在內(nèi)存的圖像數(shù)據(jù);
id<MTLTexture> 是Metal的紋理,表示的是存儲在顯存的圖像數(shù)據(jù);
GLuint 是OpenGL ES的紋理,表示的是存儲在顯存的圖像數(shù)據(jù)。
CVPixelBufferRef <==> Metal紋理在Metal入門教程(五)視頻渲染有詳細的介紹;
CVPixelBufferRef <==> OpenGL ES紋理在OpenGL ES文集也有相關(guān)的介紹;
本文就是基于CVPixelBufferRef,將Metal的紋理轉(zhuǎn)成CVPixelBufferRef,再用CVPixelBufferRef轉(zhuǎn)成OpenGL ES的紋理,實現(xiàn)Metal到OpenGL ES的交互。
效果展示

具體步驟
1、Metal渲染
Metal渲染包括兩步,分別是渲染到紋理和渲染到屏幕。
紋理是由CVMetalTextureGetTexture方法生成,需要事先創(chuàng)建CVOpenGLESTextureCacheRef,再CVMetalTextureCacheCreateTextureFromImage創(chuàng)建具體的CVOpenGLESTextureRef,具體的創(chuàng)建過程如下:
- (void)setupRenderTargetWithSize:(CGSize)size {
CFDictionaryRef empty= CFDictionaryCreate(kCFAllocatorDefault,
NULL,
NULL,
0,
&kCFTypeDictionaryKeyCallBacks,
&kCFTypeDictionaryValueCallBacks);
CFMutableDictionaryRef attrs= CFDictionaryCreateMutable(kCFAllocatorDefault,
1,
&kCFTypeDictionaryKeyCallBacks,
&kCFTypeDictionaryValueCallBacks);
CFDictionarySetValue(attrs,
kCVPixelBufferIOSurfacePropertiesKey,
empty);
CVPixelBufferRef renderTarget;
CVPixelBufferCreate(kCFAllocatorDefault, size.width, size.height,
kCVPixelFormatType_32BGRA,
attrs,
&renderTarget);
size_t width = CVPixelBufferGetWidthOfPlane(renderTarget, 0);
size_t height = CVPixelBufferGetHeightOfPlane(renderTarget, 0);
MTLPixelFormat pixelFormat = MTLPixelFormatBGRA8Unorm;
CVMetalTextureRef texture = NULL;
CVReturn status = CVMetalTextureCacheCreateTextureFromImage(NULL, self.textureCache, renderTarget, NULL, pixelFormat, width, height, 0, &texture);
if(status == kCVReturnSuccess)
{
self.destTexture = CVMetalTextureGetTexture(texture);
self.renderPixelBuffer = renderTarget;
CFRelease(texture);
}
else {
NSAssert(NO, @"CVMetalTextureCacheCreateTextureFromImage fail");
}
}
渲染到紋理用的是computeShader實現(xiàn),設(shè)置對應(yīng)的輸入和輸出紋理即可。
需要注意的是,這次給commandBuffer添加了結(jié)束回調(diào)。在Metal渲染完畢后,會把CVPixelBufferRef里面的數(shù)據(jù)轉(zhuǎn)成UIImage,同時也會傳給OpenGL ES渲染:
// 渲染到紋理
{
// 創(chuàng)建計算指令的編碼器
id<MTLComputeCommandEncoder> renderToTextureEncoder = [commandBuffer computeCommandEncoder];
// 設(shè)置計算管道,以調(diào)用shaders.metal中的內(nèi)核計算函數(shù)
[renderToTextureEncoder setComputePipelineState:self.computePipelineState];
// 輸入紋理
[renderToTextureEncoder setTexture:self.sourceTexture
atIndex:LYFragmentTextureIndexTextureSource];
// 輸出紋理
[renderToTextureEncoder setTexture:self.destTexture
atIndex:LYFragmentTextureIndexTextureDest];
// 計算區(qū)域
[renderToTextureEncoder dispatchThreadgroups:self.groupCount
threadsPerThreadgroup:self.groupSize];
// 調(diào)用endEncoding釋放編碼器,下個encoder才能創(chuàng)建
[renderToTextureEncoder endEncoding];
}
[commandBuffer addCompletedHandler:^(id<MTLCommandBuffer> buffer) {
if (kCVReturnSuccess == CVPixelBufferLockBaseAddress(self.renderPixelBuffer,
kCVPixelBufferLock_ReadOnly)) { // 獲取到buffer
dispatch_async(dispatch_get_main_queue(), ^{
UIImage *image = [self lyGetImageFromPixelBuffer:self.renderPixelBuffer]; // 轉(zhuǎn)成UIImage
if (!self.imageView) {
self.imageView = [[UIImageView alloc] initWithImage:image];
[self.view addSubview:self.imageView];
}
// OpenGL ES渲染
[self.glView displayPixelBuffer:self.renderPixelBuffer];
// 釋放資源
CVPixelBufferUnlockBaseAddress(self.renderPixelBuffer, kCVPixelBufferLock_ReadOnly);
});
}
}];
最后再把上一步的輸出紋理作為輸入,直接渲染到屏幕。
2、UIImageView渲染
UIImageView顯示的數(shù)據(jù)對象是UIImage,需要把CVPixelBufferRef轉(zhuǎn)成UIImage,具體的步驟如下:
/**
* 根據(jù)CVPixelBufferRef返回圖像
*
* @param pixelBufferRef 像素緩存引用
*
* @return UIImage對象
*/
- (UIImage *)lyGetImageFromPixelBuffer:(CVPixelBufferRef)pixelBufferRef {
CVImageBufferRef imageBuffer = pixelBufferRef;
CVPixelBufferLockBaseAddress(imageBuffer, 0);
void *baseAddress = CVPixelBufferGetBaseAddress(imageBuffer);
size_t width = CVPixelBufferGetWidth(imageBuffer);
size_t height = CVPixelBufferGetHeight(imageBuffer);
size_t bufferSize = CVPixelBufferGetDataSize(imageBuffer);
size_t bytesPerRow = CVPixelBufferGetBytesPerRowOfPlane(imageBuffer, 0); //
CGColorSpaceRef rgbColorSpace = CGColorSpaceCreateDeviceRGB();
CGDataProviderRef provider = CGDataProviderCreateWithData(NULL, baseAddress, bufferSize, NULL);
// rgba的時候是kCGImageAlphaNoneSkipFirst | kCGBitmapByteOrderDefault,這樣會導(dǎo)致出現(xiàn)藍色的圖片
CGImageRef cgImage = CGImageCreate(width, height, 8, 32, bytesPerRow, rgbColorSpace, kCGBitmapByteOrder32Little | kCGImageAlphaPremultipliedFirst, provider, NULL, true, kCGRenderingIntentDefault);
UIImage *image = [UIImage imageWithCGImage:cgImage];
CGImageRelease(cgImage);
CGDataProviderRelease(provider);
CGColorSpaceRelease(rgbColorSpace);
CVPixelBufferUnlockBaseAddress(imageBuffer, 0);
return image;
}
CGImageCreate方法的bitmapInfo參數(shù)(第七個)需要特別注意,這里的格式要和CVPixelBufferCreate方法的pixelFormatType(第四個)對應(yīng),否則會出現(xiàn)圖片顏色異常的情況。
3、OpenGL ES渲染
OpenGL ES的渲染關(guān)鍵在于把CVPixelBufferRef轉(zhuǎn)成紋理,我們可以用CVOpenGLESTextureCacheCreateTextureFromImage方法獲取到CVOpenGLESTextureRef,再用CVOpenGLESTextureGetTarget和CVOpenGLESTextureGetName獲取紋理的相應(yīng)信息,之后便是正常的OpenGL ES渲染邏輯。
遇到的問題
1、OpenGL ES渲染的圖片不透明
按照設(shè)計,左上角是UIKit渲染的圖片,右上角是OpenGL ES渲染的圖片,中間是Metal渲染的圖片,并UIKit和OpenGL ES渲染區(qū)域應(yīng)該是透明的。但是實際的效果如下:

OpenGL ES渲染的圖片不透明!
- 首先猜測的點是因為圖片本身不透明,導(dǎo)致底為黑色;這個通過UIImageView渲染的效果排除;
- 其次猜測的是OpenGL ES渲染的時候,把透明的部分設(shè)置成黑色;下面一段是shader 代碼:
varying highp vec2 texCoordVarying;
uniform sampler2D inputTexture;
precision mediump float;
void main()
{
lowp vec3 rgb = texture2D(inputTexture, texCoordVarying).rgb;
gl_FragColor = vec4(rgb, 1);
}
通過上面的代碼可以看出,確實所有像素點的alpha值都是1!
但這個并不是問題所在,因為這里只決定三角形區(qū)域內(nèi)的像素點顏色,黑色部分為背景,并不受shader的影響。
- 通過上面的猜測,發(fā)現(xiàn)問題可能在glClearColor設(shè)置了不透明的顏色。檢查代碼果然有一行
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);,把背景設(shè)置成不透明的黑色!
但在把這段代碼修改成glClearColor(0.0f, 0.0f, 0.0f, 0.0f);,發(fā)現(xiàn)黑色背景依舊沒有消除!這次問題出現(xiàn)在哪里? - 沿著我們分析的路徑,我們已經(jīng)排除掉shader、GL指令的影響,當渲染出來透明的區(qū)域時,理應(yīng)和UIKit的視圖進行混合?;貞浵翺penGL ES教程里的內(nèi)容,蘋果用CAEAGLLayer來承載OpenGL ES的內(nèi)容,最終渲染結(jié)果都是CAEAGLLayer上顏色緩沖區(qū)的數(shù)據(jù)。既然我們判斷渲染結(jié)果沒有問題,那么應(yīng)該從CAEAGLLayer與UILayer的混合開始檢查。通過排查CAEAGLLayer的屬性設(shè)置,終于發(fā)現(xiàn)問題所在是
eaglLayer.opaque = YES;!CAEAGLLayer的不透明屬性設(shè)置為YES,有利于性能的優(yōu)化,但是在demo里是會造成額外的影響!
通過把opaque屬性設(shè)置成NO,問題終于解決。
總結(jié)
本文介紹基于CVPixelBufferRef如何把Metal紋理轉(zhuǎn)成OpenGL ES紋理,而OpenGL ES紋理轉(zhuǎn)成Metal紋理的操作也類似。
在理解轉(zhuǎn)換過程中,有兩個等式非常關(guān)鍵:
CVBufferRef = CVImageBufferRef = CVMetalTextureRef
CVImageBufferRef=CVPixelBufferRef
在理清這些數(shù)據(jù)結(jié)構(gòu)后,能更好理解蘋果如何對CPU和GPU的數(shù)據(jù)進行轉(zhuǎn)換。
預(yù)告,下一篇是對入門教程的總結(jié)。