iPhone OpenGL ES 2.0中更快的glReadPixels替代方案

67

有没有比使用glReadPixels更快的访问帧缓冲区的方法?我需要只读访问帧缓冲区中的一个小矩形渲染区域,以便在CPU中进一步处理数据。由于需要反复执行此操作,因此性能很重要。我在网上搜索了一些方法,如使用像素缓冲对象和glMapBuffer,但似乎OpenGL ES 2.0不支持它们。

3个回答

136
自iOS 5.0起,现在有一种更快的方法从OpenGL ES中获取数据。虽然不是很明显,但事实证明,在iOS 5.0中添加的纹理缓存支持不仅适用于将相机帧快速上传到OpenGL ES,而且可以反向使用它来快速访问OpenGL ES纹理中的原始像素。
您可以利用这一点通过使用带有附加纹理的帧缓冲对象(FBO)来获取OpenGL ES渲染的像素,并且该纹理已经从纹理缓存中提供。一旦您将场景渲染到该FBO中,该场景的BGRA像素将包含在您的CVPixelBufferRef中,因此无需使用 glReadPixels() 将其下拉。
在我的基准测试中,这比使用 glReadPixels() 快得多。我发现在我的iPhone 4上, glReadPixels() 是读取720p视频帧以编码到磁盘的瓶颈。它使得编码无法超过8-9 FPS。使用快速纹理缓存读取替换它现在允许我以20 FPS编码720p视频,瓶颈已从像素读取转移到了管道的OpenGL ES处理和实际电影编码部分。在iPhone 4S上,这允许您以30 FPS的完整速度编写1080p视频。
我的实现可以在我的开源GPUImage框架中的GPUImageMovieWriter类中找到,但它受到了Dennis Muhlestein关于该主题的文章和苹果公司的ChromaKey示例应用程序(仅在WWDC 2011上发布)的启发。

我先配置我的AVAssetWriter,添加输入并配置像素缓冲区输入。以下代码用于设置像素缓冲区输入:

NSDictionary *sourcePixelBufferAttributesDictionary = [NSDictionary dictionaryWithObjectsAndKeys: [NSNumber numberWithInt:kCVPixelFormatType_32BGRA], kCVPixelBufferPixelFormatTypeKey,
                                                       [NSNumber numberWithInt:videoSize.width], kCVPixelBufferWidthKey,
                                                       [NSNumber numberWithInt:videoSize.height], kCVPixelBufferHeightKey,
                                                       nil];

assetWriterPixelBufferInput = [AVAssetWriterInputPixelBufferAdaptor assetWriterInputPixelBufferAdaptorWithAssetWriterInput:assetWriterVideoInput sourcePixelBufferAttributes:sourcePixelBufferAttributesDictionary];

一旦我有了这个,我会使用以下代码配置将呈现我的视频帧的FBO:

if ([GPUImageOpenGLESContext supportsFastTextureUpload])
{
    CVReturn err = CVOpenGLESTextureCacheCreate(kCFAllocatorDefault, NULL, (__bridge void *)[[GPUImageOpenGLESContext sharedImageProcessingOpenGLESContext] context], NULL, &coreVideoTextureCache);
    if (err) 
    {
        NSAssert(NO, @"Error at CVOpenGLESTextureCacheCreate %d");
    }

    CVPixelBufferPoolCreatePixelBuffer (NULL, [assetWriterPixelBufferInput pixelBufferPool], &renderTarget);

    CVOpenGLESTextureRef renderTexture;
    CVOpenGLESTextureCacheCreateTextureFromImage (kCFAllocatorDefault, coreVideoTextureCache, renderTarget,
                                                  NULL, // texture attributes
                                                  GL_TEXTURE_2D,
                                                  GL_RGBA, // opengl format
                                                  (int)videoSize.width,
                                                  (int)videoSize.height,
                                                  GL_BGRA, // native iOS format
                                                  GL_UNSIGNED_BYTE,
                                                  0,
                                                  &renderTexture);

    glBindTexture(CVOpenGLESTextureGetTarget(renderTexture), CVOpenGLESTextureGetName(renderTexture));
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);

    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, CVOpenGLESTextureGetName(renderTexture), 0);
}

这将从与我的资产写入器输入相关联的池中提取像素缓冲区,创建并关联一个纹理,并将该纹理用作我的FBO的目标。

渲染帧后,我锁定像素缓冲区的基地址:

CVPixelBufferLockBaseAddress(pixel_buffer, 0);

然后将其直接输入到我的资源编写器中进行编码:

CMTime currentTime = CMTimeMakeWithSeconds([[NSDate date] timeIntervalSinceDate:startTime],120);

if(![assetWriterPixelBufferInput appendPixelBuffer:pixel_buffer withPresentationTime:currentTime]) 
{
    NSLog(@"Problem appending pixel buffer at time: %lld", currentTime.value);
} 
else 
{
//        NSLog(@"Recorded pixel buffer at time: %lld", currentTime.value);
}
CVPixelBufferUnlockBaseAddress(pixel_buffer, 0);

if (![GPUImageOpenGLESContext supportsFastTextureUpload])
{
    CVPixelBufferRelease(pixel_buffer);
}
请注意,此处没有手动阅读任何内容。另外,纹理本来就是以BGRA格式本地存在的,这也是AVAssetWriters在编码视频时优化使用的格式,因此这里不需要进行任何颜色调整。原始的BGRA像素只需输入编码器即可生成电影。
除了在AVAssetWriter中使用外,我还在
此答案中编写了一些代码以提取原始像素。与使用glReadPixels()相比,在实践中它也经历了显着的加速,尽管比我使用AVAssetWriter的像素缓冲池看到的要小得多。
很遗憾,这些信息都没有被记录在任何地方,因为它可以大幅提高视频捕捉性能。

1
请注意,如果您不是使用AVAssetWriterInputPixelBufferAdaptor池创建像素缓冲区,则需要将CVPixelBufferRef配置为IOSurface,以使此技术起作用。请参阅Dennis Muhlestein的文章,了解如何完成此操作的示例。 - jgh
@BradLarson 在调用CVPixelBufferLockBaseAddress之前,您是否需要调用glFinishglFlush以确保GPU没有在帧缓冲区中仍在处理命令? - Mark Ingram
1
@MarkIngram - 是的,在上述代码中,在-appendPixelBuffer:withPresentationTime:之前必须使用glFinish()(在iOS中,glFlush()在许多情况下都不会阻塞) 。否则,在录制视频时,您将看到屏幕撕裂。虽然我在上面的代码中没有它,但它作为渲染例程的一部分在此之前被调用,我在使用上述框架时也是如此。 - Brad Larson
@BradLarson 在绘制三角形时,同时从纹理中读取内容是否可行?它能够同时进行绑定和读取操作吗? - Mark Ingram
@BradLarson:使用CVOpenGLESTextureCacheCreateTextureFromImage创建一个没有alpha通道的3通道RGB/BGR纹理是否可行?我似乎找不到正确的标志(https://dev59.com/RYXca4cB1Zd3GeqPJICl)。 - Adi Shavit
显示剩余18条评论

0
关于atisman提到的黑屏问题,我也遇到了。确保你的纹理和其他设置都没问题。我试图捕捉AIR的OpenGL层,最终成功了,但问题是我在应用程序清单中不小心没有将“depthAndStencil”设置为true,我的FBO纹理高度只有一半(屏幕被分成两半并镜像,我猜是因为wrap texture param的问题)。我的视频是黑色的。
这真的很令人沮丧,因为根据Brad发布的内容,一旦我在纹理中有一些数据,它应该可以正常工作。不幸的是,情况并非如此,要使其正常工作,一切都必须是“正确的”-在纹理中有数据并不能保证在视频中看到相同的数据。一旦我添加了depthAndStencil,我的纹理就自动修复为全高度,并且我开始直接从AIR的OpenGL层获取视频录制,无需使用glReadPixels等任何东西:)
所以,是的,Brad描述的确实可以在不需要在每个帧上重新创建缓冲区的情况下工作,你只需要确保你的设置是正确的。如果你得到的是黑屏,请尝试调整视频/纹理大小或其他设置(FBO的设置?)。

0
我也遇到了一个情况,即在渲染过程中没有出现错误,但是在录制的视频文件中只出现黑屏。我花了3天时间让一切正常工作。以下是我遗漏的两个必要要点:
1. 在创建CVPixelBuffer时(无论是从池中还是直接创建),需要将kCVPixelBufferIOSurfacePropertiesKey设置为CFDictionary(可以为空)。我错误地将这个键设置为布尔值,直到我意识到错误。
...
let k = bounds.height / bounds.width
let vw = 720.0 // 1080.0
let vh = vw * k
let pixelBufferWidth = Int(vw) - Int(vw) % 16
let pixelBufferHeight = Int(vh) - Int(vh) % 16
...

var pixelBuffer:CVPixelBuffer? = nil
let ioSurfaceProperties = [:] as [String : Any] as CFDictionary
let pixelBufferAttributes = [
    String(kCVPixelBufferIOSurfacePropertiesKey) : ioSurfaceProperties // <-- IMPORTANT
] as [String : Any] as CFDictionary

let cvReturn1 = CVPixelBufferCreate(kCFAllocatorDefault,
                                    pixelBufferWidth,
                                    pixelBufferHeight,
                                    kCVPixelFormatType_32BGRA,
                                    pixelBufferAttributes, 
                                    &pixelBuffer) 
guard cvReturn1 == 0,
      let sourceImage = pixelBuffer
else{
    fatalError("createCVTextureCache fail CVPixelBufferCreate(nil, \(pixelBufferWidth), \(pixelBufferHeight), \(kCVPixelFormatType_32BGRA), \(pixelBufferAttributes)) return non zero (\(cvReturnToString(cvReturn1)))")
}
...

CVOpenGLESTextureCacheCreateTextureFromImage函数初始化的CVOpenGLESTexture值必须保存到录制结束。如果不这样做,而只使用OpenGL所需的textureName和textureTarget值,那么在配置FrameBufer后,将会得到INCOMPLETE_ATTACHMENT(检查帧缓冲状态时)和INVALID_FRAMEBUFFER_OPERATION(检查glerrors时),并且文件将只会是黑色的。
....
var cvGlTexture:CVOpenGLESTexture? = nil
let cvReturn2 = CVOpenGLESTextureCacheCreateTextureFromImage(kCFAllocatorDefault,
                                                             textureCache, 
                                                             sourceImage,
                                                             nil, //textureAttributes: CFDictionary
                                                             GLenum(GL_TEXTURE_2D),                                                        
                                                             GL_RGBA,
                                                             GLsizei(w), //width: GLsizei
                                                             GLsizei(h), //height: GLsizei
                                                             GLenum(GL_BGRA), //format: GLenum
                                                             GLenum(GL_UNSIGNED_BYTE), //type: GLenum
                                                             0, //planeIndex: Int
                                                             &cvGlTexture)//textureOut: unsafeMutablePointer<CVOpenGLESTexture?>
guard cvReturn2 == 0,
      let cvTexture = cvGlTexture
else{
    fatalError("createCVTextureCache fail CVOpenGLESTextureCacheCreateTextureFromImage(...) return non zero (\(cvReturnToString(cvReturn2)))")
}
GLView.cvTextureTarget = CVOpenGLESTextureGetTarget(cvTexture)
GLView.cvTextureName = CVOpenGLESTextureGetName(cvTexture)
GLView.cvTexture = cvTexture // <--- IMPORTANT (RETAIN) 
....

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接