音视频学习笔记十五——渲染与滤镜之GPUImage滤镜链

题记:前文介绍了着色器基础和实战,也会提到一些效果需要多次渲染即离屏渲染完成,就需要一个滤镜链。本文结合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,如filter1filter2。此时的FBO应该被filter1filter2去持有(引用计数+2),直到渲染完毕或其他不需要的情况。
  • filter1filter3同样的情况,filter4需要等待filter2filter3完成后才能开始
  • 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];
      }
    }

    结点会存一个targetstargetTextureIndicestargetTextureIndices[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);
相关推荐
是阿鸽呀4 天前
【音视频开发】8. 使用 FFmpeg 解码 AAC 音频流
音视频开发
AJi4 天前
Android音视频框架探索(一):多媒体系统服务MediaServer
android·ffmpeg·音视频开发
音视频牛哥9 天前
RTSP协议规范与SmartMediaKit播放器技术解析
音视频开发·视频编码·直播
音视频牛哥10 天前
基于SmartMediaKit的无纸化同屏会议与智慧教室技术方案
音视频开发·视频编码·直播
路漫漫心远12 天前
音视频学习笔记十三——渲染与滤镜之着色器基础
音视频开发
是阿鸽呀12 天前
【音视频开发】7. 使用 FFmpeg7 提取 MP4 中的 H264 视频并封装成 Annex-B 流
音视频开发
程序员_Rya13 天前
RTC、直播、点播技术对比|腾讯云/即构/声网如何 选型 2025 版
音视频开发·直播·技术选型·音视频sdk·音视频对比
AJi14 天前
FFmpeg学习(五):音视频数据转换
ffmpeg·音视频开发·视频编码
音视频牛哥15 天前
Android平台GB28181执法记录仪技术方案与实现
音视频开发·视频编码·直播