从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在这里,https://github.com/zangqilong198812/OpenGLESTutorial
首先根据上图分析的结果是。
如果我们需要调节一个图片的亮度和对比度最后渲染到屏幕上,那么我们总共需要以下东西。
- 3个framebuffer,两个framebuffer挂载texture, 最后一个纹理挂载renderbuffer用来渲染到屏幕上。
- 需要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];
}
- 毫无疑问,我们要使用brightnessFrameBuffer,自然要绑定他。
- 清屏,设置viewport
- 使用亮度shader
- 传递原始纹理
- 渲染
搞定
渲染后的纹理呢?
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
那么结果就是这样了。