Flutter对接OpenGL离屏渲染 (MacOS)

背景

最近在学习图像处理相关的技术,作为一个iOSer,有点无法忍受OpenCV的默认UI,于是打算用Flutter作为UI框架,对接图像处理的相关能力。目前初步完成了App框架和H,S,L三个分量的调整功能。

image.png

主题

本篇博客的主题是如何使用Flutter对接macos的OpenGL离屏渲染,达到native处理图片然后在flutter页面显示的目的,主要包括如下内容

  • macos下如何进行OpenGL离屏渲染
  • OpenGL离屏渲染的结果如何同步给Flutter

macos下如何进行OpenGL离屏渲染

配置OpenGL上下文

在macos上,使用NSOpenGLContext来配置和激活OpenGL

static NSOpenGLPixelFormatAttribute kDefaultAttributes[] = {
    NSOpenGLPFADoubleBuffer, //双缓冲
    NSOpenGLPFADepthSize, 24, //深度缓冲位深
    NSOpenGLPFAStencilSize, 8, //模板缓冲位深
    NSOpenGLPFAMultisample, //多重采样
    NSOpenGLPFASampleBuffers, (NSOpenGLPixelFormatAttribute)1, //多重采样buffer
    NSOpenGLPFASamples, (NSOpenGLPixelFormatAttribute)4, // 多重采样数
    NSOpenGLPFAOpenGLProfile, NSOpenGLProfileVersion3_2Core, // OpenGL3.2
    0};

NSOpenGLPixelFormat *pixelFormat = [[NSOpenGLPixelFormat alloc] initWithAttributes:kDefaultAttributes];
_openglContext = [NSOpenGLContext.alloc initWithFormat:pixelFormat shareContext:nil];

[_openglContext makeCurrentContext];
复制代码

设置FBO

由于要离屏渲染,所以需要自己创建FBO作为RenderTarget,并且使用Texture作为Color Attachment

glGenFramebuffersEXT(1, &framebuffer);
glGenTextures(1, &fboTexture);
int TEXWIDE = textureInfo.width;
int TEXHIGH = textureInfo.height;
GLenum status;
glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, framebuffer);
glBindTexture(GL_TEXTURE_2D, fboTexture);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, TEXWIDE, TEXHIGH, 0,
                GL_BGRA, GL_UNSIGNED_BYTE, NULL);
glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT0_EXT,
                GL_TEXTURE_2D, fboTexture, 0);
复制代码

读取OpenGL渲染结果

主要通过glReadPixels来读取OpenGL的渲染结果,结果存储到 _tempImageCache

glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, framebuffer);

glClearColor(1.0, 0, 0, 1);
glClear(GL_COLOR_BUFFER_BIT);

glViewport(0, 0, textureInfo.width, textureInfo.height);

[_glContext active];
[_glContext setUniform1f:@"hueAdjust" value:self.hueAdjust];
[_glContext setUniform1f:@"saturationAdjust" value:self.saturationAdjust];
[_glContext setUniform1f:@"lightnessAdjust" value:self.lightnessAdjust];
[_imagePlane draw:_glContext];
glFlush();

glReadPixels(0, 0, textureInfo.width, textureInfo.height, GL_BGRA, GL_UNSIGNED_BYTE, _tempImageCache);
复制代码

OpenGL离屏渲染的结果如何同步给Flutter

主要利用Flutter的外接纹理来同步OpenGL离屏渲染的结果

根据图片大小创建CVPixelBuffer

- (CVPixelBufferRef)copyPixelBuffer中判断,如果_pixelBuffer未初始化或者图片大小改变了,重新创建CVPixelBuffer

if (!_pixelBuffer
    || CVPixelBufferGetWidth(_pixelBuffer) != imageWidth
    || CVPixelBufferGetHeight(_pixelBuffer) != imageHeight) {
    if (_pixelBuffer) {
        CVPixelBufferRelease(_pixelBuffer);
    }
    if (_cacheImagePixels) {
        free(_cacheImagePixels);
    }
    _cacheImagePixels = (uint8_t *)malloc(imageWidth * imageHeight * 4);
    memset(_cacheImagePixels, 0xff, imageWidth * imageHeight * 4);
    CVReturn result = CVPixelBufferCreate(kCFAllocatorDefault, imageWidth, imageHeight, kCVPixelFormatType_32BGRA, (__bridge CFDictionaryRef)pixelAttributes, &_pixelBuffer);
    CVPixelBufferRetain(_pixelBuffer);
    if (result != kCVReturnSuccess) return nil;
}
复制代码

填充数据

glReadPixel返回的数据写入CVPixelBuffer,这里有点要注意,CVPixelBuffer需要字节对齐,所以一行的长度会大于等于一行像素的真实字节长度。只能逐行拷贝glReadPixel的结果

CVPixelBufferLockBaseAddress(pixelBuffer, 0);
size_t lines = CVPixelBufferGetHeight(pixelBuffer);
size_t bytesPerRow = CVPixelBufferGetBytesPerRow(pixelBuffer);
int srcBytesPerRow = textureInfo.width * 4;
uint8_t *addr = CVPixelBufferGetBaseAddress(pixelBuffer);
glReadPixels(0, 0, textureInfo.width, textureInfo.height, GL_BGRA, GL_UNSIGNED_BYTE, _tempImageCache);
for (int line = 0; line < lines; ++line) {
    memcpy(addr + bytesPerRow * line, _tempImageCache + srcBytesPerRow * line, srcBytesPerRow);
}
CVPixelBufferUnlockBaseAddress(pixelBuffer, 0);
复制代码

Flutter侧显示

Flutter这边使用Texture组件即可,就是标准的外接纹理使用流程

更进一步

  • 可以让Flutter的CVPixelBuffer作为FBO的ColorAttachment,我目前使用glReadPixel,处理速度勉强够用,暂时没有优化
  • 使用Metal替换OpenGL,进一步提升效率

猜你喜欢

转载自juejin.im/post/7100371624028799007