题记:前文介绍了着色器基础和实战,也会提到一些效果需要多次渲染即离屏渲染完成,就需要一个滤镜链。本文结合GPUImage介绍滤镜链。 音视频学习Demo有C++版的滤镜链,文章或代码若有错误,也希望大佬不吝赐教。
一、滤镜链基础
在专栏之前的文章已经介绍的差不多了,这一节简单总结一下。
1.1 多纹理输入
片元着色器定义多个纹理
ini
uniform sampler2D inputImageTexture;
uniform sampler2D inputImageTexture2;
void main() {
lowp vec4 textureColor = texture2D(inputImageTexture, textureCoordinate);
lowp vec4 textureColor2 = texture2D(inputImageTexture2, textureCoordinate2);
gl_FragColor = mix(textureColor, textureColor2, mixturePercent);
}
这里需要明白的是,Shader中对应的是纹理单元 ,所以激活哪个纹理单元,就要uniform
的值设置为那个纹理单元对应的值(也可以根据uniform的值设置对应的纹理单元)。
scss
inputImageTextureUniform = [filterProgram uniformIndex:@"inputImageTexture"];
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, texture1);
glUniform1i(inputImageTextureUniform, 1);
inputImageTextureUniform2 = [filterProgram uniformIndex:@"inputImageTexture2"];
glActiveTexture(GL_TEXTURE2);
glBindTexture(GL_TEXTURE_2D, texture2);
glUniform1i(inputImageTextureUniform2, 2);
1.2 动图效果
发布器上的动画效果,如转场动画或者灵魂出窍之类的效果。OpenGL本身并没有动画效果,实现时只需要把time作为uniform
传入即可。
scss
uniform float time;
void main (void) {
// 动画持续0.2s
float duration = 0.2;
float progress = mod(time, duration) / duration;
// xxx 着色器处理
}
1.3 滤镜级联
这里说的级联是指把FBO的输出,作为下一个FBO的输入,其实就是离屏渲染的内容。如在音视频学习Demo中添加如下代码。
ini
pointsFilter->addTarget(grayFilter);
grayFilter->addTarget(maskFilter);
maskLink->input = pointsFilter;
maskLink->output = maskFilter;
[**self**.gpuView setFilterLinks:maskLink];
绘图要经历如下阶段,实际上还有一部上下颠倒的滤镜(图片读取以左上为原点)
主要实现步骤前面文章也讲过,这里重提一下
- FBO绑定纹理
scss
// 创建纹理
GLuint texture;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL);
// 创建 FBO 并附加纹理
GLuint fbo;
glGenFramebuffers(1, &fbo);
glBindFramebuffer(GL_FRAMEBUFFER, fbo); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0);
- 激活FBO
arduino
glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
// 设置视口
glViewport(0, 0, size.width, size.height);
- 渲染FBO,得到纹理
scss
// 渲染FBO
gldrawxxx
// xxx获取绑定的texture,切换下一个FBO,绑定纹理
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, texture);
glUniform1i(nextInput, 1);
当然这个结构可以是链状,也可以是比较复杂的网状图。所以下文会介绍一下滤镜链的设计。
二、GPUImage的滤镜链设计
2.1 对象封装
OpenGL是面向过程的,尤其是状态机的设计,一般需要做个面向对象的封装。GPUImage中的主要对两个对象进行了封装。
2.1.1 GPUImageFramebuffer
GPUImageFramebuffer
是对FBO的封装,也可以单独使用Texture。主要属性如下
ini
typedef struct GPUTextureOptions {
GLenum minFilter;
GLenum magFilter;
GLenum wrapS;
GLenum wrapT;
GLenum internalFormat;
GLenum format;
GLenum type;
} GPUTextureOptions;
@property(readonly) CGSize size;
@property(readonly) GPUTextureOptions textureOptions;
@property(readonly) GLuint texture;
定义绑定方法
scss
- (void)activateFramebuffer;
{
glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
glViewport(0, 0, (int)_size.width, (int)_size.height);
}
以及引用计数相关方法,如滤镜链中,被多个地方用到,这里保证了不用时释放或者加入Cache。
csharp
- (void)lock;
- (void)unlock;
- (void)clearAllLocks;
实现中有两种纹理方式,通用和共享内存
arduino
if ([GPUImageContext supportsFastTextureUpload]) {
// CVOpenGLESTextureCacheCreateTextureFromImage
} else {
// generateTexture
}
2.1.2 GLProgram
GLProgram
是对Shader程序的封装。
erlang
- (id)initWithVertexShaderString:(NSString *)vShaderString
fragmentShaderString:(NSString *)fShaderString;
- (void)addAttribute:(NSString *)attributeName;
- (GLuint)attributeIndex:(NSString *)attributeName;
- (GLuint)uniformIndex:(NSString *)uniformName;
- (BOOL)link;
- (void)use;
- (void)validate;
这里需要注意的是,在GLProgram中调用addAttribute
是主动去设置Shader的属性,之前的例子中都是获取。这里主动设置,并保存在队列中。
scss
- (void)addAttribute:(NSString *)attributeName
{
if (![attributes containsObject:attributeName])
{
[attributes addObject:attributeName];
glBindAttribLocation(program,
(GLuint)[attributes indexOfObject:attributeName],
[attributeName UTF8String]);
}
}
2.2 缓存管理
2.2.1 GPUImageFramebufferCache
GPUImageFramebuffer
的创建入口,内部维持map,key是size。
erlang
- (GPUImageFramebuffer *)fetchFramebufferForSize:(CGSize)framebufferSize textureOptions:(GPUTextureOptions)textureOptions onlyTexture:(BOOL)onlyTexture;
- (GPUImageFramebuffer *)fetchFramebufferForSize:(CGSize)framebufferSize onlyTexture:(BOOL)onlyTexture;
/// 暂时不用时放入Cache
- (void)returnFramebufferToCache:(GPUImageFramebuffer *)framebuffer;
当FrameBuffer的引用为0时,放入Cache中。
ini
- (void)unlock {
if (referenceCountingDisabled) return;
framebufferReferenceCount--;
if (framebufferReferenceCount < 1)
{
[[GPUImageContext sharedFramebufferCache] returnFramebufferToCache:self];
}
}
此处设置需要手动去lock,unlock,但事实上C++和iOS都是自动引用计数了,个人觉得此处有些不妥,只需要加一层box就可以解决问题,参考个人音视频学习Demo。
2.2.2 GLProgram Cache
GLProgram Cache主要通过shaderProgramCache
实现,并提供programForVertexShaderString
方法,shaderProgramCache
的key是vshader+fshader。
objectivec
@interface GPUImageContext()
{
NSMutableDictionary *shaderProgramCache;
}
2.3 滤镜链
了解了基础知识,我们来看一下滤镜链的设计。
2.3.1 滤镜结点
为了滤镜链可以串联,GPUImage中一般定义了如下结点。其实GPUImage名字容易让人误解,GPUImageOutput
和<GPUImageInput>
是结点的一体两面,<GPUImageInput>
定义输入处理,一些头部结点如图片输入,相机输入,不需要GPUImageInput。 笔者觉得完全可以定义在一起更加清晰一些,参考音视频学习Demo。
所以结点的定义
less
@interface GPUImageFilter : GPUImageOutput <GPUImageInput>
端输入结点如图片相机等的定义
less
@interface GPUImagePicture : GPUImageOutput
先想一下我们最后需要的结构,是一种图的结构。
那结点主要有以下能力:
- 可以添加多个target,如
input
- 可以有多个输入,如
filter4
再来考虑是如何工作的:
input
发起开始和结束input
渲染内部的FBO,然后把FBO交给后续target,如filter1
和filter2
。此时的FBO应该被filter1
和filter2
去持有(引用计数+2),直到渲染完毕或其他不需要的情况。filter1
到filter3
同样的情况,filter4
需要等待filter2
和filter3
完成后才能开始filter4
渲染完毕后交给预览和本地写入文件。
2.3.2 GPUImageInput接口
此时来看一个GPUImage中输入端的定义
objectivec
@protocol GPUImageInput <NSObject>
- (void)newFrameReadyAtTime:(CMTime)frameTime atIndex:(NSInteger)textureIndex;
- (void)setInputFramebuffer:(GPUImageFramebuffer *)newInputFramebuffer atIndex:(NSInteger)textureIndex;
- (NSInteger)nextAvailableTextureIndex;
xxx
@end
-
newFrameReadyAtTime
表示自己的第textureIndex
就绪了。如果检查到所有纹理就绪,就可以处理自身了。同样自身处理好了,就通知后续结点ready了。如在
GPUImageTwoInputFilter
中我们会看到(等待两个纹理都完成)ini- (void)newFrameReadyAtTime:(CMTime)frameTime atIndex:(NSInteger)textureIndex; { if (textureIndex == 0)hasReceivedFirstFrame = YES; if (textureIndex == 1) hasReceivedSecondFrame = YES; if ((hasReceivedFirstFrame && hasReceivedSecondFrame) || updatedMovieFrameOppositeStillImage) { CMTime passOnFrameTime = (!CMTIME_IS_INDEFINITE(firstFrameTime)) ? firstFrameTime : secondFrameTime; // [super newFrameReadyAtTime:passOnFrameTime atIndex:0]; // super方法如下 [self renderToTextureWithVertices:imageVertices textureCoordinates:[[self class] textureCoordinatesForRotation:inputRotation]]; [self informTargetsAboutNewFrameAtTime:frameTime]; } }
而在
informTargetsAboutNewFrameAtTime
方法中会设置target的输入(当前FBO,尺寸等)和通知自身的就绪。ini- (void)informTargetsAboutNewFrameAtTime:(CMTime)frameTime { // Get all targets the framebuffer so they can grab a lock on it for (id<GPUImageInput> currentTarget in targets) { xxx NSInteger indexOfObject = [targets indexOfObject:currentTarget]; NSInteger textureIndex = [[targetTextureIndices objectAtIndex:indexOfObject] integerValue]; [self setInputFramebufferForTarget:currentTarget atIndex:textureIndex]; [currentTarget setInputSize:[self outputFrameSize] atIndex:textureIndex]; [currentTarget newFrameReadyAtTime:frameTime atIndex:textureIndex]; } }
结点会存一个
targets
和targetTextureIndices
,targetTextureIndices[i]
表示的是当前的输出是第i
个目标targets[i]
的第几个输入。-
序号的来历就是target的
nextAvailableTextureIndex
方法 -
setInputFramebufferForTarget
其实就是调用tareget的setInputFramebuffer
的方法css- (void)setInputFramebufferForTarget:(id<GPUImageInput>)target atIndex:(NSInteger)inputTextureIndex; { [target setInputFramebuffer:[self framebufferForOutput] atIndex:inputTextureIndex]; }
-
-
endProcessing
就是结束流程,依次调用targets的方法。
2.3.3 GPUImageOutput基类
objectivec
@interface GPUImageOutput : NSObject {
GPUImageFramebuffer *outputFramebuffer;
NSMutableArray *targets, *targetTextureIndices;
CGSize inputTextureSize, cachedMaximumOutputSize, forcedMaximumSize;
BOOL overrideInputSize;
xxx
}
xxx
@property(nonatomic) BOOL enabled;
@property(readwrite, nonatomic) GPUTextureOptions outputTextureOptions;
// 获取图片
- (CGImageRef)newCGImageFromCurrentlyProcessedOutput;
@end
GPUImageOutput
的大部分方法上一节已讲过。其中outputFrame
就是在Cache中复用,在自身的renderxxx
方法用即可。outputFrame
会在设置target的地方lock(引用计数+1)。render完也可以unlock输入的FrameBuffer(引用计数-1)
ini
outputFramebuffer = [[GPUImageContext sharedFramebufferCache] fetchFramebufferForSize:[self sizeOfFBO] textureOptions:self.outputTextureOptions onlyTexture:NO];
newCGImagexxx
的方法就是读取帧,获取位图数据。注意可能需要同步wait
。读出的方法iOS也是有两种(和读入对应,共享内存和readpixels)
scss
// iOS方式,CV共享内存,可以获取到指针位置
rawImagePixels = (GLubyte *)CVPixelBufferGetBaseAddress(renderTarget);
// 通用方式,激活FBO,通过glReadPixels
[self activateFramebuffer];
rawImagePixels = (GLubyte *)malloc(totalBytesForImage);
glReadPixels(0, 0, (int)_size.width, (int)_size.height, GL_RGBA, GL_UNSIGNED_BYTE, rawImagePixels);