GPUImage中的OpenGL

因为项目的要求所以先前研究了很久的OpenGL,用来完成超大图片的实现,本身用到OpenGL的部分有限,毕竟不涉及3D和光影等部分,但是自己初学OpenGL的时候可是花了不少力气,一整套从基础的三角到视角转换到高级特性全部溜了一遍,不过网上这样的教程实在是太多所以也没有专程写下来的必要=。=(主要是当时学的时候太弱智没有记录下来学习心得,现在再翻手写笔记太麻烦了)。
正好最近想看看滤镜和图片处理功能中OpenGL的使用,权当复习一下了,所以找到久负盛名的开源框架GPUImage,稍微看一下内容,然后整理一下OpenGL比较基础的使用方法把。


GPUImage介绍

GPUImage是iOS上一个基于OpenGL进行图片处理的开源框架,由于自带各种滤镜并且能接受很多种类的输入数据,所以不管是效率还是泛用性都是iOS上进行图片处理工作的优先选择。
以图片处理为例,GPUImage让用户不需要进行非常繁杂的OpenGL上下文和指令输入,更别提还有复杂的shader渲染计算,从图片数据转为OpenGL可以识别的数据,进入渲染管线最终成为从framebuffer取出显示在屏幕上,这些操作全部都交由GPUImage框架来完成。
最有意思的是,调用GPUImage的各种组件的方法是一种链式的添加,比如获取图片,添加滤镜,输出图片所用的组件按照调用顺序一个个“挂”起来。看看例子:

1
2
3
4
5
6
7
8
9
10
11
UIImage *inputImage = [UIImage imageNamed:@"sample.jpg"];
GPUImagePicture *sourcePicture = [[GPUImagePicture alloc] initWithImage:inputImage smoothlyScaleOutput:YES];
// 创建素描滤镜并且添加到图片对象之后
GPUImageSketchFilter *customFilter = [[GPUImageSketchFilter alloc] init];
[sourcePicture addTarget:customFilter];
// 创建输出组件
GPUImageView *imageView = [[GPUImageView alloc] initWithFrame:mainSreenFrame];
[self.view addSubview:imageView];
// 在链的尾端添加输出组件
[customFilter addTarget:imageView];
[sourcePicture processImage];

非常简单明了的调用,获取图片,使用素描滤镜然后在新建的Imageview中显示出来。
GPUImage中包含了非常多的滤镜,超过100种,包括对图片和视频进行处理的各种方案。当然没必要一个个细看清楚,我们可以从调用的角度来看一看进行一次图片处理的时候会调用什么方法。先看看框架中主要的几个部件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#import "GLProgram.h"
// Base classes
#import "GPUImageContext.h"
#import "GPUImageOutput.h"
#import "GPUImageView.h"
#import "GPUImageVideoCamera.h"
#import "GPUImageStillCamera.h"
#import "GPUImageMovie.h"
#import "GPUImagePicture.h"
#import "GPUImageRawDataInput.h"
#import "GPUImageRawDataOutput.h"
#import "GPUImageMovieWriter.h"
#import "GPUImageFilterPipeline.h"
#import "GPUImageTextureOutput.h"
#import "GPUImageFilterGroup.h"
#import "GPUImageTextureInput.h"
#import "GPUImageUIElement.h"
#import "GPUImageBuffer.h"
#import "GPUImageFramebuffer.h"
#import "GPUImageFramebufferCache.h"

其中最为重要的是GLProgram,GPUImageContext,GPUImageFramebuffer。第一个管理了OpenGL的调用,熟悉OpenGL的人都知道,想要使用OpenGL必须将其所有的渲染管线打包成一个program,这里就是封装了我们渲染图片所用的各种部件包括shader。第二个是上下文,也是OpenGL绘制图片时需要获取的信息。第三个FrameBuffer大家也应该不陌生,在管线中处理过的光栅化之后的图形数据最后会在frameBuffer中,等待后续处理然后直接作为显示的像素使用。
其他的后面可能会提到,比如处理输入的GPUImageVideoCamera,GPUImageStillCamera,GPUimagePicture等。
大致的流程是这样的:

  1. 获取数据
    我们知道OpenGL用作处理图片的工作方式就是将图像信息转化为OpenGL的纹理,然后对每个像素点进行计算处理。那么首先要完成的工作就是采集数据了。考虑到GPUImage并不只是处理静态图片,还包括视频,纯纹理,摄像头数据等不同来源不同格式的数据,所以光从数据接口就有很多不同的形式,处理静态图片的GPUImagePicture,处理视频文件的GPUImageMovie,调用摄像头处理照片和视频的GPUImageStillCamera和GPUImageVideoCamera等。

  2. 传递数据
    GPUImage的图像渲染过程是一个链式的渲染过程,一个阶段接在另一个阶段之后,不管中间有多少组件,最终逻辑上都是 输入组件->中间处理->最终输出 这么三个部分。而每个Filter中间,都是依靠outputFrameBuffer和inputFrameBuffer来完成数据传递的。一个Filter通过GPUImageInput协议来完成数据的传入,而GPUImageOutput来完成数据向后传递的实现。

  3. 处理数据
    当一个Filter获取到待处理的数据之后,会按照这个流程来完成渲染:(1)向frameBufferCache申请一个outputFrameBuffer (2)将申请得到的outputFrameBuffer激活并设为渲染对象 (3)glClear清空画布信息 (4)传入顶点并设置输入纹理及其坐标 (5)调用绘制指令。 至于shader是各个filter自己封装好的,并不需要使用者自己调用。比如我们调用GPUImageFilter默认的shader,其实什么渲染效果都没有,就是维持原图状态。

  4. 输出数据
    主要是GPUImageView和GPUImageMovieWriter,其实就是将这些输出模块添加到滤镜链中等着处理完了再显示就行了。

大致就是这样,其实和OpenGL渲染本质的状态机是差不多的,其内核只管从外界拿来数据然后按照固定的流程执行渲染最后再给出最终结果,无非是根据渲染的指令不同而改变一下渲染的具体设置,但是OpenGL本身是不断循环往复这个渲染流水线,可以说是无限循环了。


具体例子分析

输入数据处理

上面说了这么多,对于GPUImage的流程也有一定的了解,现在我们按照一开始的例子来具体分析每一步的具体代码:
先看第一步输入数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
- (id)initWithImage:(UIImage *)newImageSource smoothlyScaleOutput:(BOOL)smoothlyScaleOutput;
{
return [self initWithCGImage:[newImageSource CGImage] smoothlyScaleOutput:smoothlyScaleOutput];
}
--------> 跳转
- (id)initWithCGImage:(CGImageRef)newImageSource smoothlyScaleOutput:(BOOL)smoothlyScaleOutput;
{
return [self initWithCGImage:newImageSource smoothlyScaleOutput:smoothlyScaleOutput removePremultiplication:NO];
}
-------> 跳转
// 最终的图片数据处理的核心方法,代码量非常大,一点点分析
- (id)initWithCGImage:(CGImageRef)newImageSource smoothlyScaleOutput:(BOOL)smoothlyScaleOutput removePremultiplication:(BOOL)removePremultiplication;
{
if (!(self = [super init]))
{
return nil;
}
hasProcessedImage = NO;
self.shouldSmoothlyScaleOutput = smoothlyScaleOutput;
imageUpdateSemaphore = dispatch_semaphore_create(0);
dispatch_semaphore_signal(imageUpdateSemaphore);
// 获取图片的宽和高,由于是要交给OpenGL,只能调用core graphic能识别的形式
CGFloat widthOfImage = CGImageGetWidth(newImageSource);
CGFloat heightOfImage = CGImageGetHeight(newImageSource);
// 输入数据有误进行判断
NSAssert( widthOfImage > 0 && heightOfImage > 0, @"Passed image must not be empty - it should be at least 1px tall and wide");
pixelSizeOfImage = CGSizeMake(widthOfImage, heightOfImage);
CGSize pixelSizeToUseForTexture = pixelSizeOfImage;
BOOL shouldRedrawUsingCoreGraphics = NO;
// OpenGL中对纹理的大小还有尺寸都有要求,如果超过了最大极限就需要进行修改
CGSize scaledImageSizeToFitOnGPU = [GPUImageContext sizeThatFitsWithinATextureForSize:pixelSizeOfImage];// 这个方法返回pixelSizeOfImage在设备上作为texture能够显示的最大尺寸(可能会被等比压缩)
// 如果调整过尺寸,所有渲染计算全部按调整过的尺寸来,并且需要重新绘制
if (!CGSizeEqualToSize(scaledImageSizeToFitOnGPU, pixelSizeOfImage))
{
pixelSizeOfImage = scaledImageSizeToFitOnGPU;
pixelSizeToUseForTexture = pixelSizeOfImage;
shouldRedrawUsingCoreGraphics = YES;
}
//如果要使用mipmaps压缩图片,就要保证图片的宽高尺寸是2的整数次方(这里的操作就有可能会让图片变形了)
if (self.shouldSmoothlyScaleOutput)
{
CGFloat powerClosestToWidth = ceil(log2(pixelSizeOfImage.width));
CGFloat powerClosestToHeight = ceil(log2(pixelSizeOfImage.height));
pixelSizeToUseForTexture = CGSizeMake(pow(2.0, powerClosestToWidth), pow(2.0, powerClosestToHeight));
shouldRedrawUsingCoreGraphics = YES;
}
GLubyte *imageData = NULL;
CFDataRef dataFromImageDataProvider = NULL;
GLenum format = GL_BGRA;
BOOL isLitteEndian = YES;
BOOL alphaFirst = NO;
BOOL premultiplied = NO;
if (!shouldRedrawUsingCoreGraphics) {
// 由于OpenGL本身并解析数据,只是按照固定的memory layout死板地写入数据,所以如果这里的图片数据不是由GPUImage生成的,就必须先检验一下是否合乎约定的memory layout
// 这里首先判断是不是合法,不然就要重写数据,判断的标准是原图每个最小单元是8个bit,每个像素是32个bit,4个字节
if (CGImageGetBytesPerRow(newImageSource) != CGImageGetWidth(newImageSource) * 4 ||
CGImageGetBitsPerPixel(newImageSource) != 32 ||
CGImageGetBitsPerComponent(newImageSource) != 8)
{
// 如果不合法,就跳到后面去重写图片数据
shouldRedrawUsingCoreGraphics = YES;
} else {
// 检查bitmap的格式,如果包含浮点成分就不能直接在OpenGL中调用
CGBitmapInfo bitmapInfo = CGImageGetBitmapInfo(newImageSource);
if ((bitmapInfo & kCGBitmapFloatComponents) != 0) {
shouldRedrawUsingCoreGraphics = YES;
} else {
CGBitmapInfo byteOrderInfo = bitmapInfo & kCGBitmapByteOrderMask;
if (byteOrderInfo == kCGBitmapByteOrder32Little) {
// 对小字节序的判断,如果alpha不在颜色数据的第一位就重新绘制
CGImageAlphaInfo alphaInfo = bitmapInfo & kCGBitmapAlphaInfoMask;
if (alphaInfo != kCGImageAlphaPremultipliedFirst && alphaInfo != kCGImageAlphaFirst &&
alphaInfo != kCGImageAlphaNoneSkipFirst) {
shouldRedrawUsingCoreGraphics = YES;
}
} else if (byteOrderInfo == kCGBitmapByteOrderDefault || byteOrderInfo == kCGBitmapByteOrder32Big) {
isLitteEndian = NO;
// 大字节序的判断,和上面差不多
CGImageAlphaInfo alphaInfo = bitmapInfo & kCGBitmapAlphaInfoMask;
if (alphaInfo != kCGImageAlphaPremultipliedLast && alphaInfo != kCGImageAlphaLast &&
alphaInfo != kCGImageAlphaNoneSkipLast) {
shouldRedrawUsingCoreGraphics = YES;
} else {
/* Can access directly using GL_RGBA pixel format */
premultiplied = alphaInfo == kCGImageAlphaPremultipliedLast || alphaInfo == kCGImageAlphaPremultipliedLast;
alphaFirst = alphaInfo == kCGImageAlphaFirst || alphaInfo == kCGImageAlphaPremultipliedFirst;
format = GL_RGBA;
}
}
}
}
}
// 上面很多情况都需要重新绘制图片数据,这里就是具体的绘制方法
if (shouldRedrawUsingCoreGraphics)
{
// 大小改变需要重绘或者数据格式不对,imageData就是乘装数据的指针,每个像素4个字节
imageData = (GLubyte *) calloc(1, (int)pixelSizeToUseForTexture.width * (int)pixelSizeToUseForTexture.height * 4);
CGColorSpaceRef genericRGBColorspace = CGColorSpaceCreateDeviceRGB();
// 创建上下文,也就是说明怎么解析数据,这里说明每个元素8个bit,每一行数据是宽度乘以4,也就是每个像素四个字节
CGContextRef imageContext = CGBitmapContextCreate(imageData, (size_t)pixelSizeToUseForTexture.width, (size_t)pixelSizeToUseForTexture.height, 8, (size_t)pixelSizeToUseForTexture.width * 4, genericRGBColorspace, kCGBitmapByteOrder32Little | kCGImageAlphaPremultipliedFirst);
// 正式绘制数据,完成后将中间变量release掉
CGContextDrawImage(imageContext, CGRectMake(0.0, 0.0, pixelSizeToUseForTexture.width, pixelSizeToUseForTexture.height), newImageSource);
CGContextRelease(imageContext);
CGColorSpaceRelease(genericRGBColorspace);
isLitteEndian = YES;
alphaFirst = YES;
premultiplied = YES;
}
else
{
// 不用重新绘制就直接拿数据呗
dataFromImageDataProvider = CGDataProviderCopyData(CGImageGetDataProvider(newImageSource));
imageData = (GLubyte *)CFDataGetBytePtr(dataFromImageDataProvider);
}
// 如果不使用premultiplied alpha就要对premultiplied过的数据进行一下还原
if (removePremultiplication && premultiplied) {
NSUInteger totalNumberOfPixels = round(pixelSizeToUseForTexture.width * pixelSizeToUseForTexture.height);
uint32_t *pixelP = (uint32_t *)imageData;
uint32_t pixel;
CGFloat srcR, srcG, srcB, srcA;
// premultiplied的数据中RGB是已经和alpha通道混合后的结果,如果不使用这个处理,那就要将图片数据还原出来
for (NSUInteger idx=0; idx<totalNumberOfPixels; idx++, pixelP++) {
pixel = isLitteEndian ? CFSwapInt32LittleToHost(*pixelP) : CFSwapInt32BigToHost(*pixelP);
// 提取alpha通道值
if (alphaFirst) {
srcA = (CGFloat)((pixel & 0xff000000) >> 24) / 255.0f;
}
else {
srcA = (CGFloat)(pixel & 0x000000ff) / 255.0f;
pixel >>= 8;
}
// 提取R,G,B通道值
srcR = (CGFloat)((pixel & 0x00ff0000) >> 16) / 255.0f;
srcG = (CGFloat)((pixel & 0x0000ff00) >> 8) / 255.0f;
srcB = (CGFloat)(pixel & 0x000000ff) / 255.0f;
// 还原本来的R,G,B值
srcR /= srcA; srcG /= srcA; srcB /= srcA;
// 重新拼成pixel数据
pixel = (uint32_t)(srcR * 255.0) << 16;
pixel |= (uint32_t)(srcG * 255.0) << 8;
pixel |= (uint32_t)(srcB * 255.0);
// 再将原来的alpha通道值加上去
if (alphaFirst) {
pixel |= (uint32_t)(srcA * 255.0) << 24;
}
else {
pixel <<= 8;
pixel |= (uint32_t)(srcA * 255.0);
}
*pixelP = isLitteEndian ? CFSwapInt32HostToLittle(pixel) : CFSwapInt32HostToBig(pixel);
}
}
// 在GPUImageContext专门创建的一个队列中完成图片处理
runSynchronouslyOnVideoProcessingQueue(^{
// 创建渲染上下文
[GPUImageContext useImageProcessingContext];
// 从bufferCache中创建合适大小的outputFramebuffer
outputFramebuffer = [[GPUImageContext sharedFramebufferCache] fetchFramebufferForSize:pixelSizeToUseForTexture onlyTexture:YES];
[outputFramebuffer disableReferenceCounting];
// OpenGL例行公事,向OpenGL申请GL_TEXTURE_2D接口
glBindTexture(GL_TEXTURE_2D, [outputFramebuffer texture]);
if (self.shouldSmoothlyScaleOutput)
{
// 如果需要就对像素数据进行处理,mipmap能压缩使处理效率更高
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
}
// 将刚刚申请的GL_TEXTURE_2D赋予真实的像素数据
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, (int)pixelSizeToUseForTexture.width, (int)pixelSizeToUseForTexture.height, 0, format, GL_UNSIGNED_BYTE, imageData);
if (self.shouldSmoothlyScaleOutput)
{
glGenerateMipmap(GL_TEXTURE_2D);
}
// 数据填充之后将接口“断开”
glBindTexture(GL_TEXTURE_2D, 0);
});
// 记得释放掉不用的内存,这里ARC是管不了的
if (shouldRedrawUsingCoreGraphics)
{
free(imageData);
}
else
{
if (dataFromImageDataProvider)
{
CFRelease(dataFromImageDataProvider);
}
}
return self;
}

上面看起来一两百行的代码非常麻烦,但是其实逻辑非常清晰:(1)首先判断图片的大小是否合法,如果合法就什么都不做,如果不合法就改变至可接受的范围 (2)判断是不是需要重新计算像素数据,有很多原因需要重新计算数据,比如图片大小需要改变、数据格式不正确等原因 (3)如果需要重新绘制就重新绘制吧,不管怎么样得到新数据还是使用原数据,先看一下有没有premultiplied alpha,对应这个操作需要进行一些还原计算(去除RGB通道中alpha值的影响) (4)最后是OpenGL的例行操作,将像素数据作为texture交给OpenGL管线。
具体的重要步骤已经用注释的形式添加在了代码中,可以细看。

其中有一些需要说明的地方:

  • OpenGL要求纹理的宽和高都是2的整数幂,但是事实上我们经常会遇到纹理不满足这种尺寸的情况,有很多解决办法,比如取一个满足要求的背景,比如256256,然后将我们200200的的图片放到上面,这样读取纹理的时候的尺寸是满足要求的,但是实际使用只需要这有效的一部分就行了;也有atlas texture这种操作,将多个素材拼到一起凑成一个大的纹理,调用的时候只调用其中的具体部分,其实本质和前面那个方法差不多,但是这样能省去很多单个读取纹理的时间,大大提高效率。在GPUImage中没有用到这么麻烦的操作,只是简单地区长和宽最接近的2整数次幂,然后作为纹理的大小就是了,这样图片有可能会比例变化,但是实际上使用纹理的时候也会拉伸,所以不至于视觉上很严重。
  • mipmap:
    mipmap是一种纹理处理技术,用来解决纹理调用中出现的闪烁和性能低效问题。闪烁是指当屏幕上被渲染物体的表面与它相对应的纹理显得非常小的时候会出现的一种现象。我们知道纹理的大小是固定的,但是实际使用起来由于会出现缩放裁剪等情况,我们可能并不需要原纹理那样大小的文件,所以mipmap技术就应运而生,加载纹理的时候不仅仅只加载当前这个纹理信息,而是进而加载一系列固定比例的缩小图,在OpenGL中被称为不同的层级。使用mipmap纹理的时候系统会根据实际需要的纹理大小从不同层级中选出最适合的一个。打个比方我们的原始纹理是256256,经过mipmap处理,每个层级的大小可能就是256256、128128、6464….调用这个纹理的时候可能根据实际大小需要会选择64*64的层级即可。
  • premultiplied alpha:
    我们知道通常使用的RGBA四个通道来表示一个像素,每个通道8位,比如红色60%透明度就是(255,0,0,153)或者说用正规浮点数记录alpha为(255,0,0,0.6),前三个代表三个颜色通道的值,alpha则是透明度,透明度是进行颜色混合的关键因素,假如一个像素的透明度是1.0,那么我们是看不到这个像素后面的颜色数据的,但是如果alpha<1.0,就要和后面的像素进行混合,比例分别是alpha和(1-alpha)。premultiplied alpha会让RGB三个通道提前乘以透明度,比如这里的(255,0,0,0.6)如果经过premultiplied alpha处理后就会是(153,0.,0,0.6)。这样的操作能让纹理进行texture filting(插值时的权重会出现问题),但是也让颜色数据变得更不直观。
    回到代码中,其实做的非常简单,如果使用了premultiplied alpha,那么就分别将R、G、B、A提取出来然后还原本来的R、G、B,最后还原成没有经过premultiplied alpha处理的原像素数据。

后面还有OpenGL中绑定数据的方法,觉得需要结合OpenGL的用法来说,后面再细说。


Filter原型:GPUImageFilter

接下来看filter的原理,由于filter的种类太多,我们就从中挑选一个sketch filter来研究好了,在此之前,先看看所有filter共同的父类:GPUImageFilter。
看看代码中的注释说明:

GPUImage’s base filter class:
Filters and other subsequent elements in the chain conform to the GPUImageInput protocol, which lets them take in the supplied or processed texture from the previous link in the chain and do something with it. Objects one step further down the chain are considered targets, and processing can be branched by adding multiple targets to a single output or filter.
解释一下就是GPUImage最基本的filter类(什么渲染效果都没有),filters或者其他渲染链上的元素遵从GPUImageInput协议,让它们可以从链的前一个组件中获取数据。接在后面的部件称为target,也可以通过一次添加多个target完成分支渲染。

而GPUImageFilter又继承自GPUImageOutput,这个类的职责就是(1)维护一个outputFramebuffer,准备交给渲染链后面的组件 (2)通过GPUImageInput协议来从前面的组件接收数据 (3)记录下后面的组件作为自己的target (4)提供了不同线程上运行渲染片段的接口
基本上就是作为一个组件的基本逻辑,在这个类的基础上我们就可以实现链式渲染。

回到GPUImageFilter,由于是GPUImageOutput的子类,所以完整继承了上面我们说的这些作为渲染组件的特性。看一看其本身的结构特点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
extern NSString *const kGPUImageVertexShaderString;
extern NSString *const kGPUImagePassthroughFragmentShaderString;
struct GPUVector4 {
GLfloat one;
GLfloat two;
GLfloat three;
GLfloat four;
};
typedef struct GPUVector4 GPUVector4;
struct GPUVector3 {
GLfloat one;
GLfloat two;
GLfloat three;
};
typedef struct GPUVector3 GPUVector3;
struct GPUMatrix4x4 {
GPUVector4 one;
GPUVector4 two;
GPUVector4 three;
GPUVector4 four;
};
typedef struct GPUMatrix4x4 GPUMatrix4x4;
struct GPUMatrix3x3 {
GPUVector3 one;
GPUVector3 two;
GPUVector3 three;
};
typedef struct GPUMatrix3x3 GPUMatrix3x3;

上面是使用OpenGL需要的两个字符串和结构体,我们之前说了,OpenGL识别数据是靠用户传入的纯数字节流和解析方法,并没有能够提供关于数据结构上的接口,所以我们为了方便坐标颜色等因素的计算必须自己先定下结构体,比如上面的GPUVector4就是一个4维的向量,其中元素的类型是GLfloat,而GPUMatrix4x4是一个4x4的矩阵。
两个字符串,一个是vertexShader,一个是fragmentShader。前者是OpenGL处理输入顶点的程序,后者是光栅化之后如何处理每个片元的程序,是OpenGL的核心概念,也是我们从顶点数据到最终能够在图像上看到渲染完成后的图片的关键点。这个基础的内容很多,就没必要细说了,简单而言,前者是如何处理顶点的计算法则,后者是处理颜色的法则。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@interface GPUImageFilter : GPUImageOutput <GPUImageInput>
{
// 输入和输出的数据
GPUImageFramebuffer *firstInputFramebuffer;
GPUImageFramebuffer *outputFramebuffer;
// 创建OpenGL程序(program),以及需要使用的参数句柄,还有颜色分量
GLProgram *filterProgram;
GLint filterPositionAttribute, filterTextureCoordinateAttribute;
GLint filterInputTextureUniform;
GLfloat backgroundColorRed, backgroundColorGreen, backgroundColorBlue, backgroundColorAlpha;
BOOL isEndProcessing;
// 尺寸和方向数据
CGSize currentFilterSize;
GPUImageRotationMode inputRotation;
BOOL currentlyReceivingMonochromeInput;
NSMutableDictionary *uniformStateRestorationBlocks;
dispatch_semaphore_t imageCaptureSemaphore;
// 后边接的targets,还有其纹理坐标
NSMutableArray *targets, *targetTextureIndices;
CGSize inputTextureSize, cachedMaximumOutputSize, forcedMaximumSize;
BOOL overrideInputSize;
BOOL allTargetsWantMonochromeData;
BOOL usingNextFrameForImageCapture;
}

以上是GPUImageFilter的实例变量,有一部分由于继承自GPUImageOutput所以也放到一起来看了。
可以看到有两个GPUImageFramebuffer类型的变量,firstInputFramebuffer和outputFramebuffer,从字面上可以理解这就是输入和输出的framebuffer数据。后面是OpenGL需要使用的各种句柄,如attribute,uniform这些参数是除顶点数据、纹理等数据之外从外界向shader输入出具体参数的方法。然后还有尺寸、方向的数据,以及作为chain中的组件的targets数组等需要的部件。

现在先看看整个GPUImageFilter中最重要的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
- (id)initWithVertexShaderFromString:(NSString *)vertexShaderString fragmentShaderFromString:(NSString *)fragmentShaderString;
{
if (!(self = [super init]))
{
return nil;
}
// 初始化参数,存放处理uniform的dictionary初始容量为10,并且创建一个信号量imageCaptureSemaphore
uniformStateRestorationBlocks = [NSMutableDictionary dictionaryWithCapacity:10];
_preventRendering = NO;
currentlyReceivingMonochromeInput = NO;
inputRotation = kGPUImageNoRotation;
backgroundColorRed = 0.0;
backgroundColorGreen = 0.0;
backgroundColorBlue = 0.0;
backgroundColorAlpha = 0.0;
imageCaptureSemaphore = dispatch_semaphore_create(0);
dispatch_semaphore_signal(imageCaptureSemaphore);
// 专用线程中处理
runSynchronouslyOnVideoProcessingQueue(^{
// 获取渲染需要的上下文context
[GPUImageContext useImageProcessingContext];
// 创建program,并且将编译过的vertex shader和fragment shader程序添加上去
filterProgram = [[GPUImageContext sharedImageProcessingContext] programForVertexShaderString:vertexShaderString fragmentShaderString:fragmentShaderString];
if (!filterProgram.initialized)
{
// 如果OpenGL program初始化失败,就调用默认初始化方法,为program添加"position"&"inputTextureCoordinate"参数
[self initializeAttributes];
// 如果没有完成连接,program创建失败,报错
if (![filterProgram link])
{
NSString *progLog = [filterProgram programLog];
NSLog(@"Program link log: %@", progLog);
NSString *fragLog = [filterProgram fragmentShaderLog];
NSLog(@"Fragment shader compile log: %@", fragLog);
NSString *vertLog = [filterProgram vertexShaderLog];
NSLog(@"Vertex shader compile log: %@", vertLog);
filterProgram = nil;
NSAssert(NO, @"Filter shader link failed");
}
}
// 关联program中的参数到这里的实例变量上
filterPositionAttribute = [filterProgram attributeIndex:@"position"];
filterTextureCoordinateAttribute = [filterProgram attributeIndex:@"inputTextureCoordinate"];
filterInputTextureUniform = [filterProgram uniformIndex:@"inputImageTexture"]; // This does assume a name of "inputImageTexture" for the fragment shader
[GPUImageContext setActiveShaderProgram:filterProgram];
// 允许使用变量,这是OpenGL固定操作
glEnableVertexAttribArray(filterPositionAttribute);
glEnableVertexAttribArray(filterTextureCoordinateAttribute);
});
return self;
}

这是最基本的初始化方法,其他的初始化API也都是调用这个方法的基础上进行变化,其中GPUImageFilter本身的两个shader非常简单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
NSString *const kGPUImageVertexShaderString = SHADER_STRING
(
attribute vec4 position;
attribute vec4 inputTextureCoordinate;
varying vec2 textureCoordinate;
void main()
{
gl_Position = position;
textureCoordinate = inputTextureCoordinate.xy;
}
);
NSString *const kGPUImagePassthroughFragmentShaderString = SHADER_STRING
(
varying highp vec2 textureCoordinate;
uniform sampler2D inputImageTexture;
void main()
{
gl_FragColor = texture2D(inputImageTexture, textureCoordinate);
}
);

vertex shader需要三个参数,position也就是顶点的位置,由于一般这里都是处理纹理,顶点基本就等于“相框”的顶点,没什么复杂操作;然后是纹理的位置,然后将纹理的坐标传给fragment shader。在vertex shader程序的main函数里,也只是原封不动地将坐标交给fragment shader,啥也没干。
而fragment shader,接受textureCoordinate作为参数,并且还有一个inputImageTexture作为uniform变量,也就是实际的纹理数据,然后再main函数里将纹理“贴”到固定的矩形里就行了,也基本上是什么都没做。
后面的很多各种Filter,基本上都是用的这么一个vertex shader,因为只要有个“相框”就行了,不需要复杂的位置视角变换;而不同的是各自如何处理颜色的fragment shader会不一样。
这样我们就完成了一个最基本的GPUImageFilter的初始化。

但是我们实际使用的时候并不会直接调用GPUImageFilter,毕竟它只是提供了Filter的原型,自己并没有实际的图形操作。例子中我们调用的是GPUImageSketchFilter,那么就一点点来看GPUImage是怎么将原型filter扩展成多种多样的特效filter的过程的。


特定filter的实现

下图是GPUImageSketchFilter的继承关系:
sketch继承
我们已经讲过了GPUImageOutput和GPUImageFilter,那么一步步看看每一个类都添加了什么功能吧:

  • GPUImageTwoPassFilter:
    我们先看看其实例变量:
    1
    2
    3
    4
    5
    6
    7
    GPUImageFramebuffer *secondOutputFramebuffer;
    GLProgram *secondFilterProgram;
    GLint secondFilterPositionAttribute, secondFilterTextureCoordinateAttribute;
    GLint secondFilterInputTextureUniform, secondFilterInputTextureUniform2;
    NSMutableDictionary *secondProgramUniformStateRestorationBlocks;

很明显这个类就像它的名字一样,在原有的一套inputFramebuffer+outputFramebuffer的基础上添加了一个新的secondOutputFramebuffer。也就是说一组input数据输入之后,有两套完全不同的pipeline来处理这个数据,最后从两个framebuffer分别输出,名副其实的“two pass”。
知道了这个前提,那么初始化方法其实我们很好猜到到底干了些啥:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
GPUImageTwoPassFilter.m
- (id)initWithFirstStageVertexShaderFromString:(NSString *)firstStageVertexShaderString firstStageFragmentShaderFromString:(NSString *)firstStageFragmentShaderString secondStageVertexShaderFromString:(NSString *)secondStageVertexShaderString secondStageFragmentShaderFromString:(NSString *)secondStageFragmentShaderString;
{
// 这里调用了GPUImageFilter中的初始化方法
if (!(self = [super initWithVertexShaderFromString:firstStageVertexShaderString fragmentShaderFromString:firstStageFragmentShaderString]))
{
return nil;
}
// 其实是和构建第一套pipeline一样的方法
secondProgramUniformStateRestorationBlocks = [NSMutableDictionary dictionaryWithCapacity:10];
runSynchronouslyOnVideoProcessingQueue(^{
[GPUImageContext useImageProcessingContext];
secondFilterProgram = [[GPUImageContext sharedImageProcessingContext] programForVertexShaderString:secondStageVertexShaderString fragmentShaderString:secondStageFragmentShaderString];
if (!secondFilterProgram.initialized)
{
[self initializeSecondaryAttributes];
if (![secondFilterProgram link])
{
NSString *progLog = [secondFilterProgram programLog];
NSLog(@"Program link log: %@", progLog);
NSString *fragLog = [secondFilterProgram fragmentShaderLog];
NSLog(@"Fragment shader compile log: %@", fragLog);
NSString *vertLog = [secondFilterProgram vertexShaderLog];
NSLog(@"Vertex shader compile log: %@", vertLog);
secondFilterProgram = nil;
NSAssert(NO, @"Filter shader link failed");
}
}
secondFilterPositionAttribute = [secondFilterProgram attributeIndex:@"position"];
secondFilterTextureCoordinateAttribute = [secondFilterProgram attributeIndex:@"inputTextureCoordinate"];
secondFilterInputTextureUniform = [secondFilterProgram uniformIndex:@"inputImageTexture"]; // This does assume a name of "inputImageTexture" for the fragment shader
secondFilterInputTextureUniform2 = [secondFilterProgram uniformIndex:@"inputImageTexture2"]; // This does assume a name of "inputImageTexture2" for second input texture in the fragment shader
[GPUImageContext setActiveShaderProgram:secondFilterProgram];
glEnableVertexAttribArray(secondFilterPositionAttribute);
glEnableVertexAttribArray(secondFilterTextureCoordinateAttribute);
});
return self;
}

不用看这么多内容,其实很简单,第一步调用父类的初始化方法产生一套pipeline,对应的framebuffer,第二步按照同样的流程再生成一个pipeline(全部都是second前缀),需要说明的是第二套pipeline的vertex shader & fragment shader是需要调用初始化方法的时候当做参数传入的。

  • GPUImageSobelEdgeDetectionFilter:
    这里说明一下SobelEdgeDetection是一种边缘检测算法,那么很清楚这个类就是加入了这一边缘检测处理的子类。至于说为什么这里会用到这个算法,因为我们调用的滤镜就是将图像素描化的效果,所以要进行边缘检测并且改变图片的灰度表达。具体的计算方法不细讲了,这里看看初始化的方法和fragment shader。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    - (id)initWithFragmentShaderFromString:(NSString *)fragmentShaderString;
    {
    // Do a luminance pass first to reduce the calculations performed at each fragment in the edge detection phase
    // 这里直接调用了父类的初始化方法,第二个fragment shader做为参数交给初始化方法
    if (!(self = [super initWithFirstStageVertexShaderFromString:kGPUImageVertexShaderString firstStageFragmentShaderFromString:kGPUImageLuminanceFragmentShaderString secondStageVertexShaderFromString:kGPUImageNearbyTexelSamplingVertexShaderString secondStageFragmentShaderFromString:fragmentShaderString]))
    {
    return nil;
    }
    hasOverriddenImageSizeFactor = NO;
    texelWidthUniform = [secondFilterProgram uniformIndex:@"texelWidth"];
    texelHeightUniform = [secondFilterProgram uniformIndex:@"texelHeight"];
    edgeStrengthUniform = [secondFilterProgram uniformIndex:@"edgeStrength"];
    self.edgeStrength = 1.0;
    return self;
    }

没什么好说的,看看fragmentshader:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
NSString *const kGPUImageSobelEdgeDetectionFragmentShaderString = SHADER_STRING
(
precision mediump float;
varying vec2 textureCoordinate;
varying vec2 leftTextureCoordinate;
varying vec2 rightTextureCoordinate;
varying vec2 topTextureCoordinate;
varying vec2 topLeftTextureCoordinate;
varying vec2 topRightTextureCoordinate;
varying vec2 bottomTextureCoordinate;
varying vec2 bottomLeftTextureCoordinate;
varying vec2 bottomRightTextureCoordinate;
uniform sampler2D inputImageTexture;
uniform float edgeStrength;
void main()
{
float bottomLeftIntensity = texture2D(inputImageTexture, bottomLeftTextureCoordinate).r;
float topRightIntensity = texture2D(inputImageTexture, topRightTextureCoordinate).r;
float topLeftIntensity = texture2D(inputImageTexture, topLeftTextureCoordinate).r;
float bottomRightIntensity = texture2D(inputImageTexture, bottomRightTextureCoordinate).r;
float leftIntensity = texture2D(inputImageTexture, leftTextureCoordinate).r;
float rightIntensity = texture2D(inputImageTexture, rightTextureCoordinate).r;
float bottomIntensity = texture2D(inputImageTexture, bottomTextureCoordinate).r;
float topIntensity = texture2D(inputImageTexture, topTextureCoordinate).r;
float h = -topLeftIntensity - 2.0 * topIntensity - topRightIntensity + bottomLeftIntensity + 2.0 * bottomIntensity + bottomRightIntensity;
float v = -bottomLeftIntensity - 2.0 * leftIntensity - topLeftIntensity + bottomRightIntensity + 2.0 * rightIntensity + topRightIntensity;
float mag = length(vec2(h, v)) * edgeStrength;
gl_FragColor = vec4(vec3(mag), 1.0);
}
);

sobel边缘检测是一个根据目标与其周围像素的计算方法,利用查分算子进行纵向和横向的卷积计算灰度值,最后得出目标像素和周围点的差别,如果梯度大于某个阈值就认为是边缘点。这个算法精确度不是很高,但是在不要求精确度的情况下确实简单实用。
细节计算就不说明了。

  • GPUImageSketchFilter:
    这是最终封装好给外接调用的filter,点进去看发现其实初始化也只是封装好了父类的初始化方法而已,使用了自己的fragment shader进行封装。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    NSString *const kGPUImageSketchFragmentShaderString = SHADER_STRING
    (
    precision mediump float;
    varying vec2 textureCoordinate;
    varying vec2 leftTextureCoordinate;
    varying vec2 rightTextureCoordinate;
    varying vec2 topTextureCoordinate;
    varying vec2 topLeftTextureCoordinate;
    varying vec2 topRightTextureCoordinate;
    varying vec2 bottomTextureCoordinate;
    varying vec2 bottomLeftTextureCoordinate;
    varying vec2 bottomRightTextureCoordinate;
    uniform float edgeStrength;
    uniform sampler2D inputImageTexture;
    void main()
    {
    float bottomLeftIntensity = texture2D(inputImageTexture, bottomLeftTextureCoordinate).r;
    float topRightIntensity = texture2D(inputImageTexture, topRightTextureCoordinate).r;
    float topLeftIntensity = texture2D(inputImageTexture, topLeftTextureCoordinate).r;
    float bottomRightIntensity = texture2D(inputImageTexture, bottomRightTextureCoordinate).r;
    float leftIntensity = texture2D(inputImageTexture, leftTextureCoordinate).r;
    float rightIntensity = texture2D(inputImageTexture, rightTextureCoordinate).r;
    float bottomIntensity = texture2D(inputImageTexture, bottomTextureCoordinate).r;
    float topIntensity = texture2D(inputImageTexture, topTextureCoordinate).r;
    float h = -topLeftIntensity - 2.0 * topIntensity - topRightIntensity + bottomLeftIntensity + 2.0 * bottomIntensity + bottomRightIntensity;
    float v = -bottomLeftIntensity - 2.0 * leftIntensity - topLeftIntensity + bottomRightIntensity + 2.0 * rightIntensity + topRightIntensity;
    // 也就是这里有差别
    float mag = 1.0 - (length(vec2(h, v)) * edgeStrength);
    gl_FragColor = vec4(vec3(mag), 1.0);
    }
    );
    - (id)init;
    {
    if (!(self = [self initWithFragmentShaderFromString:kGPUImageSketchFragmentShaderString]))
    {
    return nil;
    }
    return self;
    }

仔细观察的话就能发现,其实这个shader计算每个像素颜色的方法和我们在其父类SobelEdgeDetectiveFilter中看到的是基本相同的,只有最后决定颜色的时候是反过来用1减去本来计算出来的各通路的值的。

至此,我们例子中的初始化滤镜组件的部分就完成了。简单来说就是在最原始的GPUImageFilter的基础上再加一组pipeline,然后按照特定的fragment shader来输出数据
不过这个例子只是GPUImage使用方式中的一种,只针对静态图片的滤镜是这样,别的情况下比如是对视频进行逐帧操作或者是拍下照片等,重点可能就不在fragment shader上了。限于篇幅没法一次说明那么多内容。


显示容器GPUImageView

GPUImageView结构上很简单,就是遵循了GPUImageInput协议的UIImageView。严格来说其在图片处理方面和前面介绍的filter组件是一样的,接受input数据之后创建自己的一套渲染pipeline,包括vertex shader & fragment shader,而这里使用的图片处理仅仅只负责显示出来的图片的角度,而不管光影颜色等复杂因素。初始化方法就不看了,和前面提到的pipeline的初始化是差不多的。这个GPUImageView要说和前面的组件有什么不一样的话可能就只有它将framebuffer直接显示在自己的视图上了吧。


最终启动渲染链

上面我们将整个渲染链中的组件,从数据读入、数据处理到图像展示的三个部分全部初始化完成并依次“组装”(addtarget)起来了。最后就调用链首的proccess方法。
这里我们调用的是GPUImagePicture的proccess方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
- (void)processImage;
{
[self processImageWithCompletionHandler:nil];
}
- (BOOL)processImageWithCompletionHandler:(void (^)(void))completion;
{
hasProcessedImage = YES;
// 减了1的信号量还不为0就报错
if (dispatch_semaphore_wait(imageUpdateSemaphore, DISPATCH_TIME_NOW) != 0)
{
return NO;
}
// 给每个target依次输入数据
runAsynchronouslyOnVideoProcessingQueue(^{
for (id<GPUImageInput> currentTarget in targets)
{
NSInteger indexOfObject = [targets indexOfObject:currentTarget];
NSInteger textureIndexOfTarget = [[targetTextureIndices objectAtIndex:indexOfObject] integerValue];
[currentTarget setCurrentlyReceivingMonochromeInput:NO];
[currentTarget setInputSize:pixelSizeOfImage atIndex:textureIndexOfTarget];
[currentTarget setInputFramebuffer:outputFramebuffer atIndex:textureIndexOfTarget];
[currentTarget newFrameReadyAtTime:kCMTimeIndefinite atIndex:textureIndexOfTarget];
}
dispatch_semaphore_signal(imageUpdateSemaphore);
if (completion != nil) {
completion();
}
});
return YES;
}

setInputFramebuffer: atIndex:这个方法是GPUImageInput协议中需要实现的方法,由于我们的例子中使用的是GPUImageTwoInputFilter这个类,那么就看它里面对protocal的实现方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
// 按照协议GPUImageInput要求传入数据
- (void)setInputFramebuffer:(GPUImageFramebuffer *)newInputFramebuffer atIndex:(NSInteger)textureIndex;
{
if (textureIndex == 0)
{
firstInputFramebuffer = newInputFramebuffer;
hasSetFirstTexture = YES;
[firstInputFramebuffer lock];
}
else
{
secondInputFramebuffer = newInputFramebuffer;
[secondInputFramebuffer lock];
}
}
// 启动图片处理
- (void)newFrameReadyAtTime:(CMTime)frameTime atIndex:(NSInteger)textureIndex;
{
// You can set up infinite update loops, so this helps to short circuit them
if (hasReceivedFirstFrame && hasReceivedSecondFrame)
{
return;
}
BOOL updatedMovieFrameOppositeStillImage = NO;
if (textureIndex == 0)
{
hasReceivedFirstFrame = YES;
firstFrameTime = frameTime;
if (secondFrameCheckDisabled)
{
hasReceivedSecondFrame = YES;
}
if (!CMTIME_IS_INDEFINITE(frameTime))
{
if CMTIME_IS_INDEFINITE(secondFrameTime)
{
updatedMovieFrameOppositeStillImage = YES;
}
}
}
else
{
hasReceivedSecondFrame = YES;
secondFrameTime = frameTime;
if (firstFrameCheckDisabled)
{
hasReceivedFirstFrame = YES;
}
if (!CMTIME_IS_INDEFINITE(frameTime))
{
if CMTIME_IS_INDEFINITE(firstFrameTime)
{
updatedMovieFrameOppositeStillImage = YES;
}
}
}
// || (hasReceivedFirstFrame && secondFrameCheckDisabled) || (hasReceivedSecondFrame && firstFrameCheckDisabled)
if ((hasReceivedFirstFrame && hasReceivedSecondFrame) || updatedMovieFrameOppositeStillImage)
{
CMTime passOnFrameTime = (!CMTIME_IS_INDEFINITE(firstFrameTime)) ? firstFrameTime : secondFrameTime;
[super newFrameReadyAtTime:passOnFrameTime atIndex:0]; // Bugfix when trying to record: always use time from first input (unless indefinite, in which case use the second input)
hasReceivedFirstFrame = NO;
hasReceivedSecondFrame = NO;
}
}

前面都是对收到的数据进行判断,可以看到最终调用的还是父类的方法,那我们回头去看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
GPUImageFilter.m
- (void)newFrameReadyAtTime:(CMTime)frameTime atIndex:(NSInteger)textureIndex;
{
// 四个角没什么疑问吧。。
static const GLfloat imageVertices[] = {
-1.0f, -1.0f,
1.0f, -1.0f,
-1.0f, 1.0f,
1.0f, 1.0f,
};
[self renderToTextureWithVertices:imageVertices textureCoordinates:[[self class] textureCoordinatesForRotation:inputRotation]];
[self informTargetsAboutNewFrameAtTime:frameTime];
}
======>>>
- (void)renderToTextureWithVertices:(const GLfloat *)vertices textureCoordinates:(const GLfloat *)textureCoordinates;
{
if (self.preventRendering)
{
[firstInputFramebuffer unlock];
return;
}
[GPUImageContext setActiveShaderProgram:filterProgram];
outputFramebuffer = [[GPUImageContext sharedFramebufferCache] fetchFramebufferForSize:[self sizeOfFBO] textureOptions:self.outputTextureOptions onlyTexture:NO];
[outputFramebuffer activateFramebuffer];
if (usingNextFrameForImageCapture)
{
[outputFramebuffer lock];
}
[self setUniformsForProgramAtIndex:0];
glClearColor(backgroundColorRed, backgroundColorGreen, backgroundColorBlue, backgroundColorAlpha);
glClear(GL_COLOR_BUFFER_BIT);
glActiveTexture(GL_TEXTURE2);
glBindTexture(GL_TEXTURE_2D, [firstInputFramebuffer texture]);
glUniform1i(filterInputTextureUniform, 2);
glVertexAttribPointer(filterPositionAttribute, 2, GL_FLOAT, 0, 0, vertices);
glVertexAttribPointer(filterTextureCoordinateAttribute, 2, GL_FLOAT, 0, 0, textureCoordinates);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
[firstInputFramebuffer unlock];
if (usingNextFrameForImageCapture)
{
dispatch_semaphore_signal(imageCaptureSemaphore);
}
}

好了,可以看到这里最终是调用了OpenGL的渲染管线,将最终渲染的结果放到framebuffer中,等待交给后面的组件使用。大部分都是固定管线操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
- (void)informTargetsAboutNewFrameAtTime:(CMTime)frameTime;
{
if (self.frameProcessingCompletionBlock != NULL)
{
self.frameProcessingCompletionBlock(self, frameTime);
}
// Get all targets the framebuffer so they can grab a lock on it
for (id<GPUImageInput> currentTarget in targets)
{
if (currentTarget != self.targetToIgnoreForUpdates)
{
NSInteger indexOfObject = [targets indexOfObject:currentTarget];
NSInteger textureIndex = [[targetTextureIndices objectAtIndex:indexOfObject] integerValue];
[self setInputFramebufferForTarget:currentTarget atIndex:textureIndex];
[currentTarget setInputSize:[self outputFrameSize] atIndex:textureIndex];
}
}
// 解除资源锁定
[[self framebufferForOutput] unlock];
if (usingNextFrameForImageCapture)
{
// usingNextFrameForImageCapture = NO;
}
else
{
[self removeOutputFramebuffer];
}
// 触发后面target的处理
for (id<GPUImageInput> currentTarget in targets)
{
if (currentTarget != self.targetToIgnoreForUpdates)
{
NSInteger indexOfObject = [targets indexOfObject:currentTarget];
NSInteger textureIndex = [[targetTextureIndices objectAtIndex:indexOfObject] integerValue];
[currentTarget newFrameReadyAtTime:frameTime atIndex:textureIndex];
}
}
}

完成渲染之后再调用后面的target,将数据传递下去。直到最终显示在GPUImageView上。就是这样一个逻辑。

总结

GPUImage作为一个用途广泛的图形视频处理框架,内容实在是太过丰富,对应不同的数据源还有不同的渲染、处理方法,搭配起来有非常复杂的变化。我们这里只是拿一个例子来分析了GPUImage对一个静态图片的渲染方法,其中涉及了部分OpenGL的绘制指令,包括program、vertex shader、fragment shdaer等内容,这些是OpenGL的基础内容,只有自己动手才能明白每一步有什么意义。在这个框架中我们能看到很清晰的“渲染链”的逻辑,完美地应对了复杂渲染的搭配。并且从最基本的GPUImageFilter不断拓展为各种效果的设计方法也值得学习。可以说这个框架的设计思路和结构,非常值得学习,里面调用OpenGL的指令也适合让初学者了解OpenGL渲染中每一步的意义。

当前网速较慢或者你使用的浏览器不支持博客特定功能,请尝试刷新或换用Chrome、Firefox等现代浏览器