Metal系列教程的代码地址; OpenGL ES系列教程在这里;
你的star和fork是我的源动力,你的意见能让我走得更远。
UIImage是我们常用的图像类,可以转成CVPixelBufferRef,表示存储在内存的图像数据; id<MTLTexture> 是Metal的纹理,表示的是存储在显存的图像数据; GLuint 是OpenGL ES的纹理,表示的是存储在显存的图像数据。
CVPixelBufferRef <==> Metal纹理在Metal入门教程(五)视频渲染有详细的介绍; CVPixelBufferRef <==> OpenGL ES纹理在OpenGL ES文集也有相关的介绍; 本文就是基于CVPixelBufferRef,将Metal的纹理转成CVPixelBufferRef,再用CVPixelBufferRef转成OpenGL ES的纹理,实现Metal到OpenGL ES的交互。
三种渲染效果展示
Metal渲染包括两步,分别是渲染到纹理和渲染到屏幕。
纹理是由CVMetalTextureGetTexture
方法生成,需要事先创建CVOpenGLESTextureCacheRef,再CVMetalTextureCacheCreateTextureFromImage
创建具体的CVOpenGLESTextureRef,具体的创建过程如下:
- (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实现,设置对应的输入和输出纹理即可。 需要注意的是,这次给commandBuffer添加了结束回调。在Metal渲染完毕后,会把CVPixelBufferRef里面的数据转成UIImage,同时也会传给OpenGL ES渲染:
// 渲染到纹理
{
// 创建计算指令的编码器
id<MTLComputeCommandEncoder> renderToTextureEncoder = [commandBuffer computeCommandEncoder];
// 设置计算管道,以调用shaders.metal中的内核计算函数
[renderToTextureEncoder setComputePipelineState:self.computePipelineState];
// 输入纹理
[renderToTextureEncoder setTexture:self.sourceTexture
atIndex:LYFragmentTextureIndexTextureSource];
// 输出纹理
[renderToTextureEncoder setTexture:self.destTexture
atIndex:LYFragmentTextureIndexTextureDest];
// 计算区域
[renderToTextureEncoder dispatchThreadgroups:self.groupCount
threadsPerThreadgroup:self.groupSize];
// 调用endEncoding释放编码器,下个encoder才能创建
[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]; // 转成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);
});
}
}];
最后再把上一步的输出纹理作为输入,直接渲染到屏幕。
UIImageView显示的数据对象是UIImage,需要把CVPixelBufferRef转成UIImage,具体的步骤如下:
/**
* 根据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,这样会导致出现蓝色的图片
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参数(第七个)需要特别注意,这里的格式要和CVPixelBufferCreate
方法的pixelFormatType(第四个)对应,否则会出现图片颜色异常的情况。
OpenGL ES的渲染关键在于把CVPixelBufferRef转成纹理,我们可以用CVOpenGLESTextureCacheCreateTextureFromImage
方法获取到CVOpenGLESTextureRef,再用CVOpenGLESTextureGetTarget
和CVOpenGLESTextureGetName
获取纹理的相应信息,之后便是正常的OpenGL ES渲染逻辑。
按照设计,左上角是UIKit渲染的图片,右上角是OpenGL ES渲染的图片,中间是Metal渲染的图片,并UIKit和OpenGL ES渲染区域应该是透明的。但是实际的效果如下:
Metal与OpenGL
OpenGL ES渲染的图片不透明!
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! 但这个并不是问题所在,因为这里只决定三角形区域内的像素点颜色,黑色部分为背景,并不受shader的影响。
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
,把背景设置成不透明的黑色!
但在把这段代码修改成glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
,发现黑色背景依旧没有消除!这次问题出现在哪里?eaglLayer.opaque = YES;
!CAEAGLLayer的不透明属性设置为YES,有利于性能的优化,但是在demo里是会造成额外的影响!
通过把opaque属性设置成NO,问题终于解决。本文介绍基于CVPixelBufferRef如何把Metal纹理转成OpenGL ES纹理,而OpenGL ES纹理转成Metal纹理的操作也类似。
在理解转换过程中,有两个等式非常关键:
CVBufferRef = CVImageBufferRef = CVMetalTextureRef
CVImageBufferRef=CVPixelBufferRef
在理清这些数据结构后,能更好理解苹果如何对CPU和GPU的数据进行转换。
预告,下一篇是对入门教程的总结。