从0打造一个GPUImage(6)-GPUImage的多滤镜处理逻辑

从0打造一个GPUImage(6)

如何传递texture是理解GPUImage2框架的核心问题

GPUImage管线流程

上一章提出了一个问题。GPUImage如何实现类似于。

let testImage = UIImage(named:"WID-small.jpg")!
let toonFilter = SmoothToonFilter()
let luminanceFilter = Luminance()
let filteredImage = testImage.filterWithPipeline{input, output in
    input --> toonFilter --> luminanceFilter --> output
}

上一篇文章也提到过。目前我们能想到的方法有两个。

方法一, 利用反复生成图片然后传递纹理来处理

简单来说,既然我们向shader传递纹理数据是通过

UIImage -> texture -> framebuffer -> glReadPixels -> 获取图片

这个路径,那么也就是说,我们只需要在每次shader处理完获取图片然后使用另一个shaderProgram,再次进行这个流程就可以了。

所以完整路径变成了。

UIImage -> texture -> framebuffer -> glReadPixels -> 生成图片 -> 更换shader -> 上一个流程获取的UIImage -> texture -> framebuffer -> glReadPixels -> 获取图片

但是,这个方法是行不通的。

原因1.GLFramebuffer在这里attach的是一个renderbuffer,而renderbuffer的尺寸往往和真实图片无关,而是与显示图片的视图尺寸有直接关系。

也就是说,当你使用glReadPixel去读取GLRenderBuffer里的像素数据的时候,返回的图片大小只能是你GLRenderBuffer的大小。(例如移动端可能最多和你的屏幕尺寸相当)。所以当你使用这种方法处理大图的时候,会发现最后生成的图片是一张小图。

原因2: 处理过程涉及到太多的CPU处理环节

我们使用OpenGL ES来处理图片的原因最直接的一个原因是因为,使用GPU处理图片的速度远超CPU。而整个过程除了一些不可避免的预处理,比如CPU向GPU传递纹理数据,uniform类型的值,我们可以使用CPU以外,应该尽量避免使用CPU。
那么每当我们在glReadPixels然后再利用CoreGrpaphics来生成图片的时候,我们已经完全的使用了CPU。这必然会极大的拖慢整个处理流程的速度。

正确的处理方法

我之前提到过,GLFramebuffer本身如果不挂载任何东西的时候是不能工作的。
那么,如果想要使framebuffer正常工作,有一个必要条件。
就是,必须挂载一个renderbuffer或者texture。

上一章我们介绍了如何利用offscreen framebuffer来渲染一张全尺寸的图片。

为了方便大家回忆。我再贴出这部分代码来分析一下。

- (void)createOffscreenBuffer:(CGSize)imageSize {
    glGenFramebuffers(1, &_offscreenFramebuffer);
    glBindFramebuffer(GL_FRAMEBUFFER, _offscreenFramebuffer);
    
    //Create the texture
    GLuint texture;
    glGenTextures(1, &texture);
    glBindTexture(GL_TEXTURE_2D, texture);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA,  imageSize.width, imageSize.height, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL);
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
    
    //Bind the texture to your FBO
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0);
    
    //Test if everything failed
    GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
    if(status != GL_FRAMEBUFFER_COMPLETE) {
        printf("failed to make complete framebuffer object %x", status);
    }
    glBindFramebuffer(GL_FRAMEBUFFER, 0);
    glBindTexture(GL_TEXTURE_2D, 0);
}

也就是说我们生成的framebuffer挂载的是一个GL_TEXTURE_2D,而不是一个GL_RENDERBUFFER

用一张图片来显示两者的区别是。



那么如果我们用这个挂载着GL_TEXTURE_2D的framerBuffer来渲染图片的话。整个GPU渲染流程就变成了最后渲染的结果并不是到了屏幕上,到哪了呢?

注意这句话
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, imageSize.width, imageSize.height, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL);
最后一个参数是NULL,也就是说在这里我们只是声明了一个空的纹理,里面并没有填充任何数据。
这种挂载着纹理的framebuffer最终会把GPU管线处理的结果渲染到这个空的纹理上,鉴于之前这个纹理的id是我们自己声明的,所以我们自然可以使用这个渲染之后的纹理了。

这就很好办了。

整个流程已然打通。脑袋豁然开朗。

如图。



如何用代码实现他们?

所以,经过上面的分析。我们到了需要用代码论证的地方了。
Demo在这里,github.com/zangqilong19

首先根据上图分析的结果是。

如果我们需要调节一个图片的亮度和对比度最后渲染到屏幕上,那么我们总共需要以下东西。

  1. 3个framebuffer,两个framebuffer挂载texture, 最后一个纹理挂载renderbuffer用来渲染到屏幕上。
  2. 需要3个glProgram,第一个shaderProgram装载的是亮度的fragment shader, 第二个shaderProgram装载的是对比度的fragment Shader,第三个shaderProgram最简单,他不需要任何特殊的fragmentshader,只需要把rgb信息从纹理中取出,然后展示就行了。

在demo里,我只展示了如何用处理亮度的framebuffer处理纹理然后把纹理传递给展示的framebuffer。如何串联对比度的shader大同小异,这个你们可以自己尝试写一下。

首先看代码。

- (void)viewDidLoad {
    [super viewDidLoad];
     [self setupOpenGLContext];
    processImage = [UIImage imageNamed:@"wuyanzu.jpg"];
    texName = [self getTextureFromImage:processImage];
    
    
    [self setupCAEAGLLayer:self.view.bounds];
    [self clearRenderBuffers];
    [self setupRenderBuffers];
    [self createBrightnessFrameBuffer:processImage];
    [self setupRenderScreenViewPort];
    [self setupRenderShader];
    [self setupBrightnessShader];;
    [self renderToScreen];
}

在这里,需要注意的是[self createBrightnessFrameBuffer:processImage.size];
这就是我们处理亮度的framebuffer。

texName 代表原始图片的纹理ID。
[self setupBrightnessShader];我们创建了专门处理亮度的shader。

然后我们的UI是这样的。(刚进控制器是黑的,只要拖动一下slider就能显示图片了。)


UISlider主要控制我们的图片亮度。

那么我们看看slider的事件里如何使用brightnessBuffer处理纹理并且将纹理传递给renderbuffer。

- (IBAction)valueChanged:(UISlider *)sender {
    // 让OpenGL绑定亮度的framebuffer
    glBindFramebuffer(GL_FRAMEBUFFER, _brightnessFramebuffer);
    glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT);
    glViewport(0, 0, (GLsizei)processImage.size.width, (GLsizei)processImage.size.height);
    
    // 使用亮度shader
    [brightnessShader prepareToDraw];
    // 传递调节亮度的值区间 (-1 - 1)
    glUniform1f(_brightness, sender.value);
    // 传递原始纹理数据
    glActiveTexture(GL_TEXTURE5);
    glBindTexture(GL_TEXTURE_2D, texName);
    glUniform1i(_textureSlot, 5);
    
    // 开始绘制
    [self drawRawImage];
    
    // 绘制纹理完毕,开始绘制到屏幕上
    
    [self renderToScreen];
    
}
  1. 毫无疑问,我们要使用brightnessFrameBuffer,自然要绑定他。
  2. 清屏,设置viewport
  3. 使用亮度shader
  4. 传递原始纹理
  5. 渲染

搞定
渲染后的纹理呢?

 glGenTextures(1, &brightnessTexture);
    glBindTexture(GL_TEXTURE_2D, brightnessTexture);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA,  image.size.width, image.size.height, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL);
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
    
    //Bind the texture to your FBO
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, brightnessTexture, 0);

自然是这个叫做brightnessTexture的纹理了。

然后看看如何渲染到屏幕的。

- (void)renderToScreen {
    glBindFramebuffer(GL_FRAMEBUFFER, _frameBuffer);
    [self setupRenderScreenViewPort];
    [renderShader prepareToDraw];

    UIImage *image = processImage;
    CGRect realRect = AVMakeRectWithAspectRatioInsideRect(image.size, self.view.bounds);
    CGFloat widthRatio = realRect.size.width/self.view.bounds.size.width;
    CGFloat heightRatio = realRect.size.height/self.view.bounds.size.height;
    
    const GLfloat vertices[] = {
        -widthRatio, -heightRatio, 0,   //左下
        widthRatio,  -heightRatio, 0,   //右下
        -widthRatio, heightRatio,  0,   //左上
        widthRatio,  heightRatio,  0 }; //右上
    
//    const GLfloat originVertices[] = {
//        -1, -1, 0,   //左下
//        1,  -1, 0,   //右下
//        -1, 1,  0,   //左上
//        1,  1,  0 }; //右上
    
    glEnableVertexAttribArray(_positionSlot2);
    glVertexAttribPointer(_positionSlot2, 3, GL_FLOAT, GL_FALSE, 0, vertices);
    
    // normal
    static const GLfloat coords[] = {
        0, 0,
        1, 0,
        0, 1,
        1, 1
    };
    glEnableVertexAttribArray(_textureCoordSlot2);
    glVertexAttribPointer(_textureCoordSlot2, 2, GL_FLOAT, GL_FALSE, 0, coords);
    
    
    glActiveTexture(GL_TEXTURE5);
    glBindTexture(GL_TEXTURE_2D, brightnessTexture);
    glUniform1i(_textureSlot2, 5);

  
    
    glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
    [_eaglContext presentRenderbuffer:GL_RENDERBUFFER];
}

没有任何不同。
只是传递的纹理变成了。brightnessTexture

那么结果就是这样了。

编辑于 2018-01-12 09:05