从0打造一个GPUImage(5)
知己知彼
GPUImage2的框架
为啥不说GPUImage1是因为GPUImage1写的太啰嗦了。当然主要是Objective-C这门语言的问题。
相比较而言,GPUImage2的代码几乎是1的一半。而且结构非常清晰。

框架概述
整个GPUImage2大概分为4部分。

Base
Base主要一些基础工具类。
包含。
1. ShaderProgram
用来compiler和attach我们的vertex shader和fragment shader。看过前几篇文章的应该知道这个类的作用。
2. SerialDispatch
一个protocol
public protocol SerialDispatch {
var serialDispatchQueue:DispatchQueue { get }
var dispatchQueueKey:DispatchSpecificKey<Int> { get }
func makeCurrentContext()
}主要是定义了一个串行queue和这个队列的key,还有一个方法。
它的extension封装了一些GCD的方法。比如runOperationSynchronously,其实就是
public func runOperationSynchronously(_ operation:() -> ()) {
// TODO: Verify this works as intended
if (DispatchQueue.getSpecific(key:self.dispatchQueueKey) == 81) {
operation()
} else {
self.serialDispatchQueue.sync {
self.makeCurrentContext()
operation()
}
}
}为了防止deadlock使用了getspecific来防止在同一个队列使用sync。
3.ShaderUniformSettings
作者封装的用来给uniform赋值的类。
看过之前的文章,大家应该知道,shader有3种类型修饰,attribute, varying, uniform.
attribute只能用在vertex shader中,一般用来传递顶点数据。由于GPUImage不做3D,所以attribute修饰的变量在GPUImage的shader几乎只有一种,就是我们绘制正方形的顶点数据。
varying用来在vertex shader和fragment shader之间传递数据。我们现在使用的只有一个,就是用来传递纹理坐标。
所以,在GPUImage中,用来修改图像属性的各种变量有且只有一种选择就是把图像的参数,例如亮度,对比度等设置为uniform类型。
实际上,uniform的官方定义就是uniform变量是外部application程序传递给(vertex和fragment)shader的变量
所以,GPUImage的fragment shader中存在大量的uniform变量,为了方便获取和设置这些uniform变量,GPUImage的作者有理由创造这么一个类来管理。
我随便找几个GPUImage内置的fragment shader,大家可以看一下uniform变量的使用。



4.OpenGLContext
这个class是一个实现了上文提到的SerialDispatch协议的类。
并且由于跨平台的原因,在Mac和iOS目录中各有一个同名的OpenGLContext。
这个类主要包含一个EAGLContext和shader cache。
我刚看到这个类的时候在疑惑为什么不把它做成一个单例。因为理论上我们在整个流程不大可能用到第二个EAGLContext,实际上整个GPUIMage中确实只用到了一个OpenGLContext,在OpenGLContext_Shared中,我们能找到这样一句话.

在整个GPUImage流程中,我们也只用到这么一个。
但是我在Mac文件夹中的OpenGLContext类中我发现了一行注释。// TODO: Figure out way to allow for multiple contexts for different GPUs
说明在针对Mac平台上有可能存在的双显卡(一般是一个主板集成显卡和一个AMD显卡),就可能需要设置两个context来分别对应GPU了。
5.OpenGLRendering
用来负责render 的类。其中最主要的一个方法就是public func renderQuadWithShader(_ shader:ShaderProgram, uniformSettings:ShaderUniformSettings? = nil, vertices:[GLfloat], inputTextures:[InputTextureProperties])
之前的章节我介绍过,绘制图片的方法是把图片绘制到一个正方形上。
回顾下之前的方法。
- (void)drawTrangle {
[self activeTexture];
UIImage *image = [UIImage imageNamed:@"wuyanzu.jpg"];
const GLfloat vertices[] = {
-1, -1, 0, //左下
1, -1, 0, //右下
-1, 1, 0, //左上
1, 1, 0 }; //右上
glEnableVertexAttribArray(_positionSlot);
glVertexAttribPointer(_positionSlot, 3, GL_FLOAT, GL_FALSE, 0, vertices);
// normal
static const GLfloat coords[] = {
0, 0,
1, 0,
0, 1,
1, 1
};
glEnableVertexAttribArray(_textureCoordSlot);
glVertexAttribPointer(_textureCoordSlot, 2, GL_FLOAT, GL_FALSE, 0, coords);
static const GLfloat colors[] = {
1, 0, 0, 1,
1, 0, 0, 1,
1, 0, 0, 1,
1, 0, 0, 1
};
glEnableVertexAttribArray(_colorSlot);
glVertexAttribPointer(_colorSlot, 4, GL_FLOAT, GL_FALSE, 0, colors);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
[_eaglContext presentRenderbuffer:GL_RENDERBUFFER];再看看GPUImage的这个方法。基本上是一个意思。
public func renderQuadWithShader(_ shader:ShaderProgram, uniformSettings:ShaderUniformSettings? = nil, vertices:[GLfloat], inputTextures:[InputTextureProperties]) {
sharedImageProcessingContext.makeCurrentContext()
shader.use()
uniformSettings?.restoreShaderSettings(shader)
guard let positionAttribute = shader.attributeIndex("position") else { fatalError("A position attribute was missing from the shader program during rendering.") }
glVertexAttribPointer(positionAttribute, 2, GLenum(GL_FLOAT), 0, 0, vertices)
for (index, inputTexture) in inputTextures.enumerated() {
if let textureCoordinateAttribute = shader.attributeIndex("inputTextureCoordinate".withNonZeroSuffix(index)) {
glVertexAttribPointer(textureCoordinateAttribute, 2, GLenum(GL_FLOAT), 0, 0, inputTexture.textureCoordinates)
} else if (index == 0) {
fatalError("The required attribute named inputTextureCoordinate was missing from the shader program during rendering.")
}
glActiveTexture(textureUnitForIndex(index))
glBindTexture(GLenum(GL_TEXTURE_2D), inputTexture.texture)
shader.setValue(GLint(index), forUniform:"inputImageTexture".withNonZeroSuffix(index))
}
glDrawArrays(GLenum(GL_TRIANGLE_STRIP), 0, 4)
for (index, _) in inputTextures.enumerated() {
glActiveTexture(textureUnitForIndex(index))
glBindTexture(GLenum(GL_TEXTURE_2D), 0)
}
}在这里详细介绍一下这个函数的意思。
sharedImageProcessingContext.makeCurrentContext()设置EAGLContext的currentContext是我声明的一个context。shader.use()指定使用的shader programuniformSettings?.restoreShaderSettings(shader)这个uniformSetting我们前面也说过,就是储存你对fragment shader里一些属性的值。在这里的时候,我们调用这个方法就是将你对shader的改动真正的应用到shader里。其实你点开这个方法就能见到。guard let positionAttribute = shader.attributeIndex("position") else { fatalError("A position attribute was missing from the shader program during rendering.") }
glVertexAttribPointer(positionAttribute, 2, GLenum(GL_FLOAT), 0, 0, vertices)
这两句就不用多说了。给veretx shader的顶点属性赋值,检测如果vertex shader里没有position这个attribute的话直接报错。- 接下来是一个for循环。
for (index, inputTexture) in inputTextures.enumerated() {
if let textureCoordinateAttribute = shader.attributeIndex("inputTextureCoordinate".withNonZeroSuffix(index)) {
glVertexAttribPointer(textureCoordinateAttribute, 2, GLenum(GL_FLOAT), 0, 0, inputTexture.textureCoordinates)
} else if (index == 0) {
fatalError("The required attribute named inputTextureCoordinate was missing from the shader program during rendering.")
}
glActiveTexture(textureUnitForIndex(index))
glBindTexture(GLenum(GL_TEXTURE_2D), inputTexture.texture)
shader.setValue(GLint(index), forUniform:"inputImageTexture".withNonZeroSuffix(index))
}概括一下就是,设置fragment里的texture和textureCoordinate。
不理解为啥是个for循环的话。因为GPUImage最多支持5个texture。
看Operations目录的这几个文件,研究一下你就懂了。


剩下的就是一些util方法了。不细细介绍了。
6.Pipeline
此类功能非常多,阅读起来也比较困难,但是,这个类是整个GPUImage的核心,可以这么说,如果看懂了ImageConsumer和ImageSource这两个protocol,理解整个GPUImage对你来说再也没有任何困难。
阅读之前,我们需要提两个问题
问题1, 如何动态修改texture并且显示在framebuffer上?
在之前的篇章,我们虽然使用了shader,也知道怎么更改shader的属性,并且成功的将图片显示在了屏幕上。
但是我们没有触及一个核心,如何动态修改shader并实时渲染。
今天我们来解决这个问题。
首先打开之前的demo。
在storyboard里为vc添加一个UISlider。

设置slider的范围为-1 - 1
并且,拷贝GPUImage的其中一个滤镜Brightness_GL.fsh到我们的项目里。
这个shader的内容如下。
precision mediump float;
uniform sampler2D u_Texture;
varying vec2 v_TexCoordOut;
uniform float brightness;
void main()
{
vec4 textureColor = texture2D(u_Texture, v_TexCoordOut);
gl_FragColor = vec4((textureColor.rgb + vec3(brightness)), textureColor.w);
}简单来说,就通过更改brightness这个参数来更改整张图片的亮度。
然后给我们的UISlider增加一个action
- (IBAction)valueChanged:(UISlider *)sender {
glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
[shaderCompiler prepareToDraw];
glUniform1f(_brightness, sender.value);
[self activeTexture];
[self drawTrangle];
}解释一下。
首先,我们执行了清屏。
然后调用[shaderCompiler prepareToDraw]; 其实就是使用我们当前的shader program.
prepareToDraw的方法体是。
- (void)prepareToDraw {
glUseProgram(_programHandle);
}然后,我们需要更改shader中的uniform类型的变量brightness的值。
brightness的地址,我们已经通过_brightness = [shaderCompiler uniformIndex:@"brightness"];获取到并且赋给了_brightness这个变量。
所以直接调用glUniform1f(_brightness, sender.value);来更改他。
然后传递我们的纹理。最后重绘即可。
效果如下。

问题2: 如何把我们的纹理导出成为一个图片文件?
核心是使用一个叫做glReadPixels方法。
方法如下。
- (void)getImageFromBuffe:(int)width withHeight:(int)height {
GLint x = 0, y = 0;
NSInteger dataLength = width * height * 4;
GLubyte *data = (GLubyte*)malloc(dataLength * sizeof(GLubyte));
glPixelStorei(GL_PACK_ALIGNMENT, 4);
glReadPixels(x, y, width, height, GL_RGBA, GL_UNSIGNED_BYTE, data);
CGDataProviderRef ref = CGDataProviderCreateWithData(NULL, data, dataLength, NULL);
CGColorSpaceRef colorspace = CGColorSpaceCreateDeviceRGB();
CGImageRef iref = CGImageCreate(width, height, 8, 32, width * 4, colorspace, kCGBitmapByteOrder32Big | kCGImageAlphaPremultipliedLast,
ref, NULL, true, kCGRenderingIntentDefault);
UIGraphicsBeginImageContext(CGSizeMake(width, height));
CGContextRef cgcontext = UIGraphicsGetCurrentContext();
CGContextSetBlendMode(cgcontext, kCGBlendModeCopy);
CGContextDrawImage(cgcontext, CGRectMake(0.0, 0.0, width, height), iref);
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
free(data);
CFRelease(ref);
CFRelease(colorspace);
CGImageRelease(iref);
}解释一下就是,glReadPiexel会读取framebuffer上的pixel信息,然后储存在data里。
我们使用CoreGraphics从data生成UIImage即可。
我们打个断点测试一下图片。

其实这个图片是有问题的。
1. 图片上下有白边。
2. 图片大小不对。通过po image.size得到的图片大小是(width = 414, height = 736).其实是我们的屏幕大小。
为什么会发生这样的原因呢?
这里,需要再说一下framebuffer的结构了。

framebuffer本身是没有任何作用的,他必须挂载一些附件才能工作。比如我们之前挂在了一个GLRenderBuffer,所以framebuffer就可以在屏幕上显示我们渲染的结果。
这时候我们读取framebuffer的像素实际上是读取renderbuffer里的东西。那么renderbuffer的大小和屏幕大小相同,所以我们获取的图片大小和内容就是当前屏幕的内容。
那么怎么获取原始图片分辨率的经过处理的图片呢?
这里就需要使用离屏渲染的概念了。
我们只需要再创建一个offscreen framebuffer,然后让他装载一个texture。最后,我们使用这个离屏的framebuffer来处理我们的图片。
过程如下。
- 创建离屏buffer。
- (void)createOffscreenBuffer:(UIImage *)image {
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, 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, 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);
}在这里为什么glTexImage2D传递的纹理数据是NULL呢?
因为framebuffer正常工作的前提是至少需要一个renderbuffer或者texture,为了能够让我们的离屏buffer正常工作,我们需要传入一个空的texture。
然后,在获取图片的时候。
- (IBAction)getImage:(id)sender {
glBindFramebuffer(GL_FRAMEBUFFER, _offscreenFramebuffer);
glViewport(0, 0, processImage.size.width, processImage.size.height);
glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
[shaderCompiler prepareToDraw];
glUniform1f(_brightness, _brightSlider.value);
[self activeTexture];
[self drawRaw];
[self getImageFromBuffe:width withHeight:height];
}我们需要把当前framebuffer转换为我们的离屏buffer。
设置viewport。传递数据,最后绘制。
然后就可以得到一个原始分辨率的处理过的图片了。
Pipeline
好了现在说会GPUImage的pipeline。
首先,为什么GPUImage会设置pipeline?
因为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
}就是GPUImage允许串联多个滤镜直接生成一个最终结果的图片。
所以pipeline的核心就是,如何串联多个filter?
那么解决方案有两个。
大家可以思考一下,有哪两种解决方案?
我们下期详细介绍GPUImage的管线(pipeline)方案。