添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

假设我们自定义一个OpenGL ES程序来处理图片,那么会有以下几个步骤:

1、初始化OpenGL ES环境,编译、链接顶点着色器和片元着色器;

2、缓存顶点、纹理坐标数据,传送图像数据到GPU;

3、绘制图元到特定的帧缓存;

4、在帧缓存取出绘制的图像。

GPUImageFilter 负责的是第一、二、三步。

GPUImageFramebuffer 负责是第四步。

一、GPUImageFilter解析

GPUImageFilter和响应链的其他元素实现了 GPUImageInput 协议,他们都可以提供纹理参与响应链,或者从响应链的前面接收并处理纹理。响应链的下一个对象是target,响应链可能有多个分支(添加多个targets)。

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.

  • 获取纹理坐标
  • 1
    + (const GLfloat *)textureCoordinatesForRotation:(GPUImageRotationMode)rotationMode;
  • 绘制结果输出
    绘制的结果后输入到 outputframebuffer 指定的缓存
  • usingNextFrameForImageCapture 代表着输出的结果会被用于获取图像,所以在绘制之前要加锁

    1
    2
    3
    4
    if (usingNextFrameForImageCapture)
    {
    [outputFramebuffer lock];
    }
  • 绑定纹理
    glBindTexture(GL_TEXTURE_2D, [firstInputFramebuffer texture]);
    绑定输入纹理,OpenGL ES才能确定要处理纹理数据
  • 绑定顶点和纹理坐标并绘制图元
  • 1
    2
    3
    glVertexAttribPointer(filterPositionAttribute, 2, GL_FLOAT, 0, 0, vertices);
    glVertexAttribPointer(filterTextureCoordinateAttribute, 2, GL_FLOAT, 0, 0, textureCoordinates);
    glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);

    GL_TRIANGLE_STRIP 模式用于绘制三角形带。 这里有介绍

  • 纹理解锁
    [firstInputFramebuffer unlock]; 输入纹理使用完毕,解锁。在调用这个解锁之前必须确定之前已经调用加锁,否则会报错。
    GPUImageFramebuffer 使用引用计数来管理缓存,当引用计数小于0的时候会回收缓存。
  • 信号量
    如果设置了 usingNextFrameForImageCapture ,则会通过GCD信号量来通知仍在等待绘制完成的函数。
  • 1
    2
    3
    4
    if (usingNextFrameForImageCapture)
    {
    dispatch_semaphore_signal(imageCaptureSemaphore);
    }
  • 通知targets
    - (void)informTargetsAboutNewFrameAtTime:(CMTime)frameTime;
    当self的帧绘制完成后,通知自己的targets,并将自己的输出设置为targets的输入纹理:
    [self setInputFramebufferForTarget:currentTarget atIndex:textureIndex];
    然后解锁自己使用的输出缓冲区 [[self framebufferForOutput] unlock];
    (在上一个函数已经lock了这个缓冲区,所以这里的unlock不会马上回收内存,等到targets使用完自己的纹理后调用unlock,缓存会被回收)
    在设置完缓冲区后,self会通知所有targets(除了设置忽略的)
    [currentTarget newFrameReadyAtTime:frameTime atIndex:textureIndex];
  • 等待渲染完成
  • 1
    2
    3
    4
    if (dispatch_semaphore_wait(imageCaptureSemaphore, convertedTimeout) != 0)
    {
    return NULL;
    }
  • 一系列setter
    - (void)setInteger:(GLint)newInteger forUniformName:(NSString *)uniformName;
    这些函数是设置GLSL里面的变量
  • 二、GPUImageFramebuffer

    管理纹理缓存格式、帧缓存的buffer。

  • 纹理格式
    默认的纹理格式 defaultTextureOptions
  • 缓存创建
    generateTexture 会创建对应的纹理缓存
    generateFramebuffer 会创建对应的帧缓存
    注意:iOS5.0以上会使用 CVOpenGLESTextureCache
    否则会使用 glTexImage2D() ,这个我们更熟悉的函数来传送CPU图像数据到GPU
  • 指定渲染目标
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, _texture, 0);
    把渲染目标指定为图像
  • 调整视口大小
    先绑定自己的帧缓存,再调整视口大小。
  • 1
    2
    3
    4
    5
    - (void)activateFramebuffer;
    {
    glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
    glViewport(0, 0, (int)_size.width, (int)_size.height);
    }
  • 解锁
    当引用计数小于1的时候,会调用下面的函数把自己放回缓存管理cache。(注意这个和 destroyFramebuffer不一样,一个是回收再利用,一个是销毁)
  • 1
    [[GPUImageContext sharedFramebufferCache] returnFramebufferToCache:self];
  • 从帧缓存中读取图片
    newCGImageFromFramebufferContents 函数获取图像数据。
    CVPixelBufferGetBaseAddress glReadPixels 都可以获得图像数据,根据iOS版本不同调用不同函数。
    最后通过CGImageCreate,创建 CGImageRef,然后返回。
  • CVPixelBuffer
    CV像素缓存是一个主内存的图像缓存,应用在渲染帧、压缩解压视频、使用CoreImage都会用到CV像素缓存。
    在访问CPU的像素数据之前,必须调用CVPixelBufferLockBaseAddress,并在访问后调用CVPixelBufferUnlockBaseAddress。如果lockFLags带有kCVPixelBufferLock_ReadOnly参数,那么unlocking 的时候也需要。
  • A Core Video pixel buffer is an image buffer that holds pixels in main memory. Applications generating frames, compressing or decompressing video, or using Core Image can all make use of Core Video pixel buffers.

  • CVOpenGLESTextureCache
    缓存和管理CVOpenGLESTextureRef纹理,这些纹理缓存提供了一个直接读写多种颜色格式缓存的方式。
  • Core Video OpenGLES texture caches are used to cache and manage CVOpenGLESTextureRef textures. These texture caches provide you with a way to directly read and write buffers with various pixel formats, such as 420v or BGRA, from GLES.

  • CVOpenGLESTexture
    CV纹理是纹理图像缓存,提供OpenGL图像数据
  • Core Video OpenGLES textures are texture-based image buffers used for supplying source image data to OpenGL.

    扩展

    GPUImage的四大输入基础类,都可以作为响应链的起点。这些基础类会把图像作为纹理,传给OpenGL ES处理,然后把纹理传递给响应链的下一个对象。
    GPUImageVideoCamera 摄像头-视频流
    GPUImageStillCamera 摄像头-照相
    GPUImagePicture 图片
    GPUImageMovie 视频
    响应链,先要理解帧缓存的概念,这在
    OpenGL ES教程-帧缓存 有提到过。

    总结

    用一句话来解释GPUImageFilter就是用来接收源图像,通过自定义的顶点、片元着色器来渲染新的图像,并在绘制完成后通知响应链的下一个对象。
    GPUImageFramebuffer就是用来管理纹理缓存的格式与读写帧缓存的buffer。

    上一篇介绍的是 GPUImageFramebuffer GPUImageFilter
    简单回顾一下:

  • GPUImageFilter 就是用来接收源图像,通过自定义的顶点、片元着色器来渲染新的图像,并在绘制完成后通知响应链的下一个对象。
  • GPUImageFramebuffer 就是用来管理纹理缓存的格式与读写帧缓存的buffer。
  • 这一篇介绍的是 GPUImageVideoCamera GPUImageView

    GPUImageVideoCamera

    GPUImageVideoCamera是GPUImageOutput的子类,提供来自摄像头的图像数据作为源数据,一般是响应链的源头。

    1、视频图像采集 :AVCaptureSession

    GPUImage使用 AVFoundation框架 来获取视频。
    AVCaptureSession类从AV输入设备的采集数据到制定的输出。
    为了实现实时的图像捕获,要实现AVCaptureSession类,添加合适的输入(AVCaptureDeviceInput)和输出(比如 AVCaptureMovieFileOutput)
    调用 startRunning 开始输入到输出的数据流,调用 stopRunning 停止数据流。
    需要注意的是startRunning函数会花费一定的时间,所以不能在主线程(UI线程)调用,防止卡顿。
    sessionPreset 属性可以自定义一些设置。
    特殊的选项比如说高帧率,可以通过 AVCaptureDevice来设置。
    AVCaptureSession使用的简单示例:

    1
    2
    3
    4
    _captureSession = [[AVCaptureSession alloc] init];
    [_captureSession beginConfiguration];
    // 中间可以实现关于session属性的设置
    [_captureSession commitConfiguration];
  • AVCaptureVideoDataOutput
    AVCaptureVideoDataOutput AVCaptureOutput 的子类,用来处理从摄像头采集的未压缩或者压缩过的图像帧。
    通过 captureOutput:didOutputSampleBuffer:fromConnection: delegate ,可以访问图像帧。
    通过下面这个方法,可以设置delegate。
  • 1
    2
    3
    - (void)setSampleBufferDelegate:
    (id<AVCaptureVideoDataOutputSampleBufferDelegate>)sampleBufferDelegate
    queue:(dispatch_queue_t)sampleBufferCallbackQueue;

    需要注意的是,当一个新的视频图像帧被采集后,它会被传送到output,调用这里设置的delegate。所有的delegate函数会在这个queue中调用。 如果队列被阻塞 ,新的图像帧到达后会被自动丢弃(默认alwaysDiscardsLateVideoFrames = YES)。这允许app处理当前的图像帧,不需要去管理不断增加的内存, 因为处理速度跟不上采集的速度,等待处理的图像帧会占用内存,并且不断增大
    必须使用 同步队列 处理图像帧,保证帧的序列是顺序的。

  • frameRenderingSemaphore 帧渲染的信号量
    下面有一个这样的调用,用于等待处理完一帧后,再接着处理下一帧。
  • 1
    2
    3
    4
    5
    6
    7
    if (dispatch_semaphore_wait(frameRenderingSemaphore, DISPATCH_TIME_NOW) != 0)
    {
    return;
    }
    runAsynchronouslyOnVideoProcessingQueue(^{
    dispatch_semaphore_signal(frameRenderingSemaphore);
    });
  • rotateCamera
    前后摄像头翻转:更改videoInput的设置。
  • 2、颜色空间:YUV

    YUV是被欧洲电视系统所采用的一种颜色编码方法。
    采用YUV色彩空间的重要性是它的 亮度信号Y 色度信号U、V 是分离的。如果只有Y信号分量而没有U、V分量,那么这样表示的图像就是黑白灰度图像。彩色电视采用YUV空间正是为了用亮度信号Y解决彩色电视机与黑白电视机的兼容问题,使黑白电视机也能接收彩色电视信号。

    YCbCr或Y’CbCr有的时候会被写作:YCBCR或是Y’CBCR,是色彩空间的一种,通常会用于影片中的影像连续处理,或是数字摄影系统中。Y’为颜色的亮度(luma)成分、而CB和CR则为蓝色和红色的浓度偏移量成份。
    YUV主要用于优化彩色视频信号的传输,使其向后相容老式黑白电视。与RGB视频信号传输相比,它最大的优点在于只需占用极少的频宽(RGB要求三个独立的视频信号同时传输)。

    CbCr 则是在世界数字组织视频标准研制过程中作为ITU - R BT.601 建议的一部分,其实是YUV经过缩放和偏移的翻版。其中Y与YUV 中的Y含义一致,Cb,Cr 同样都指色彩,只是在表示方法上不同而已。在YUV 家族中,YCbCr 是在计算机系统中应用最多的成员,其应用领域很广泛,JPEG、MPEG均采用此格式。一般人们所讲的YUV大多是指YCbCr。YCbCr 有许多取样格式,如4∶4∶4,4∶2∶2,4∶1∶1 和4∶2∶0。
    百度百科的介绍
    YUV数据格式-图文详解
    GPUImage中的YUV
    GLProgram *yuvConversionProgram; 将YUV颜色空间转换成RGB颜色空间的GLSL。
    CVPixelBufferGetPlaneCount() 返回缓冲区的平面数。
    通过 CVOpenGLESTextureCacheCreateTextureFromImage() 创建两个纹理luminanceTextureRef(亮度纹理)和chrominanceTextureRef(色度纹理)。
    convertYUVToRGBOutput() 把YUV颜色空间的纹理转换成RGB颜色空间的纹理
    顶点着色器-通用 kGPUImageVertexShaderString
    片元着色器:
    1、 kGPUImageYUVFullRangeConversionForLAFragmentShaderString
    2、 kGPUImageYUVVideoRangeConversionForLAFragmentShaderString
    区别在不同的格式
    video-range (luma=[16,235] chroma=[16,240])
    full-range (luma=[0,255] chroma=[1,255])

    3、纹理绘制

    glActiveTextue 并不是激活纹理单元,而是选择当前活跃的纹理单元。每一个纹理单元都有GL_TEXTURE_1D, 2D, 3D 和 CUBE_MAP。

    1
    2
    3
    glActiveTexture(GL_TEXTURE1);
    glGenTextures(1, &_texture);
    glBindTexture(GL_TEXTURE_2D, _texture);

    SO的详细介绍

    GPUImageView

    GPUImageView是响应链的终点,一般用于显示GPUImage的图像。

    1、填充模式

    GPUImageFillModeType fillMode 图像的填充模式。
    sizeInPixels 像素区域大小。
    recalculateViewGeometry() 重新计算图像顶点位置数据。
    AVMakeRectWithAspectRatioInsideRect() 在保证宽高比不变的前提下,得到一个尽可能大的矩形。
    如果是kGPUImageFillModeStretch
    图像拉伸,直接使宽高等于1.0即可,原图像会直接铺满整个屏幕。

    如果是kGPUImageFillModePreserveAspectRatio
    保持原宽高比,并且图像不超过屏幕。那么以当前屏幕大小为准。
    widthScaling = insetRect.size.width / currentViewSize.width;

    如果是kGPUImageFillModePreserveAspectRatioAndFill
    保持原宽高比,并且图像要铺满整个屏幕。那么图像大小为准。
    widthScaling = currentViewSize.height / insetRect.size.height;

    imageVertices存放着顶点数据,上面的修改都会存放在这个数组。

    2、OpenGL ES绘制

    - (void)newFrameReadyAtTime:(CMTime)frameTime atIndex:(NSInteger)textureIndex; 源图像已经准备好,开始绘制。
    setDisplayFramebuffer() 会绑定GPUImageView的帧缓存,同时调试视口大小为view的大小。
    glActiveTexture 上面已经介绍过,是选择一个纹理单元。先选择纹理单元4,然后把源图像数据绑定到GL_TEXTURE_2D的位置上。最后告诉片元着色器,纹理单元是4。

    1
    2
    3
    glActiveTexture(GL_TEXTURE4);
    glBindTexture(GL_TEXTURE_2D, [inputFramebufferForDisplay texture]);
    glUniform1i(displayInputTextureUniform, 4);

    这两行是分别绑定顶点坐标数据和纹理坐标数据。

    1
    2
    glVertexAttribPointer(displayPositionAttribute, 2, GL_FLOAT, 0, 0, imageVertices);
    glVertexAttribPointer(displayTextureCoordinateAttribute, 2, GL_FLOAT, 0, 0, [GPUImageView textureCoordinatesForRotation:inputRotation]);

    这两行是设定输入的源图像数据缓存,并且对缓存加锁。

    1
    2
    inputFramebufferForDisplay = newInputFramebuffer;
    [inputFramebufferForDisplay lock];

    在准备好着色器、纹理data、顶点位置坐标和纹理坐标后,就可以调用
    glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); 绘制图像。

  • GPUImageFilter 就是用来接收源图像,通过自定义的顶点、片元着色器来渲染新的图像,并在绘制完成后通知响应链的下一个对象。
  • GPUImageFramebuffer 就是用来管理纹理缓存的格式与读写帧缓存的buffer。
  • GPUImageVideoCamera GPUImageOutput 的子类,提供来自摄像头的图像数据作为源数据,一般是响应链的源头。
  • GPUImageView 是响应链的终点,一般用于显示GPUImage的图像。
  • 琨君 基于GPUImage的实时美颜滤镜 对GPUImage实现美颜滤镜的原理和思路做了详细介绍。
    本文以 琨君的代码 为demo,结合前两篇解析,探究美颜过程中的GPUImage实现。

    GPUImage类介绍

    1、GPUImageFilterGroup

    GPUImageFilterGroup 是多个filter的集合, terminalFilter 为最终的filter, initialFilters 为filter数组。 GPUImageFilterGroup 本身不绘制图像,对 GPUImageFilterGroup 添加删除Target操作的操作都会转为 terminalFilter 的操作。

    2、GPUImageTwoInputFilter

    GPUImageTwoInputFilter GPUImageFilter 的子类,对两个输入纹理进行通用的处理,需要继承它并准备自己的片元着色器。
    两个输入纹理默认为 inputImageTexture inputImageTexture2

  • 重写了下面的函数,修改 GPUImageFilter 绘制的逻辑。
  • 1
    2
    - (void)renderToTextureWithVertices:(const GLfloat *)vertices
    textureCoordinates:(const GLfloat *)textureCoordinates;

    下面这部分是核心的绘制逻辑:
    glActiveTexture() 是选择纹理单元, glBindTexture() 是把纹理单元和 firstInputFramebuffer secondInputFramebuffer 管理的纹理内存绑定。 glUniform1i() 告诉GLSL选择的纹理单元是2。
    这部分在上一篇介绍也有提到,再详细阐述: glActiveTexture() 选择的是纹理单元,和 glGenTextures() 返回的数字没有关系,可以在纹理单元2上面绑定纹理12。
    glGenTextures() 返回的纹理可以是 GL_TEXTURE_2D 类型也可以是 GL_TEXTURE_CUBE_MAP 类型,取决于 glBindTexture() 第一次绑定纹理的是 GL_TEXTURE_2D 还是 GL_TEXTURE_CUBE_MAP

    1
    2
    3
    4
    5
    6
    7
    glActiveTexture(GL_TEXTURE2);
    glBindTexture(GL_TEXTURE_2D, [firstInputFramebuffer texture]);
    glUniform1i(filterInputTextureUniform, 2);

    glActiveTexture(GL_TEXTURE3);
    glBindTexture(GL_TEXTURE_2D, [secondInputFramebuffer texture]);
    glUniform1i(filterInputTextureUniform2, 3);
  • nextAvailableTextureIndex 用于获取下一个纹理索引
  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    - (NSInteger)nextAvailableTextureIndex;
    {
    if (hasSetFirstTexture)
    {
    return 1;
    }
    else
    {
    return 0;
    }
    }

    setInputFramebuffer: atIndex: 会根据上面获取的 textureIndex 设置 firstInputFramebuffer secondInputFramebuffer 。如果是 textureIndex = 0 ,设置 hasSetFirstTexture 表示已经设置第一个纹理。

    3、GPUImageThreeInputFilter

    GPUImageThreeInputFilter 的逻辑与 GPUImageTwoInputFilter 类似,增加了 thirdInputFramebuffer 作为第三个纹理 inputImageTexture3 的输入。

    4、GPUImageBeautifyFilter

    GPUImageBeautifyFilter 基于GPUImage的实时美颜滤镜 中的美颜滤镜,包括 GPUImageBilateralFilter GPUImageCannyEdgeDetectionFilter GPUImageCombinationFilter GPUImageHSBFilter

    绘制流程图

  • 1、 GPUImageVideoCamera 捕获摄像头图像
    调用 newFrameReadyAtTime: atIndex: 通知 GPUImageBeautifyFilter
  • 2、 GPUImageBeautifyFilter 调用 newFrameReadyAtTime: atIndex:
    通知 GPUImageBilateralFliter 输入纹理已经准备好;
  • 3、 GPUImageBilateralFliter 绘制图像后在 informTargetsAboutNewFrameAtTime()
    调用 setInputFramebufferForTarget: atIndex:
    把绘制的图像设置为 GPUImageCombinationFilter 输入纹理,
    并通知 GPUImageCombinationFilter 纹理已经绘制完毕;
  • 4、 GPUImageBeautifyFilter 调用 newFrameReadyAtTime: atIndex:
    通知 GPUImageCannyEdgeDetectionFilter 输入纹理已经准备好;
  • 5、同3, GPUImageCannyEdgeDetectionFilter 绘制图像后,
    把图像设置为 GPUImageCombinationFilter 输入纹理;
  • 6、 GPUImageBeautifyFilter 调用 newFrameReadyAtTime: atIndex:
    通知 GPUImageCombinationFilter 输入纹理已经准备好;
  • 7、 GPUImageCombinationFilter 判断是否有三个纹理,三个纹理都已经准备好后
    调用 GPUImageThreeInputFilter 的绘制函数 renderToTextureWithVertices: textureCoordinates:
    图像绘制完后,把图像设置为 GPUImageHSBFilter 的输入纹理,
    通知 GPUImageHSBFilter 纹理已经绘制完毕;
  • 8、 GPUImageHSBFilter 调用 renderToTextureWithVertices: textureCoordinates: 绘制图像,
    完成后把图像设置为 GPUImageView 的输入纹理,并通知 GPUImageView 输入纹理已经绘制完毕;
  • 9、 GPUImageView 把输入纹理绘制到自己的帧缓存,然后通过
    [self.context presentRenderbuffer:GL_RENDERBUFFER]; 显示到 UIView 上。
  • 总结

    GPUImageFilter
    GPUImageFramebuffer
    GPUImageVideoCamera
    GPUImageView
    GPUImageFilterGroup
    GPUImageTwoInputFilter
    GPUImageThreeInputFilter
    这是学习这个demo需要了解的7个类。
    在绘制流程图的过程中,对GPUImage的响应链有了更清晰的认识。

    这次介绍的 GPUImageContext GPUImageFramebufferCache GPUImagePicture

    GPUImageContext

    GPUImageContext是GPUImage对OpenGL ES上下文的封装,添加了GPUImage相关的上下文,比如说Program的使用缓存,处理队列,CV纹理缓存等。

    1、属性介绍

    contextQueue 统一处理队列
    currentShaderProgram 正在使用的program
    context OpenGL ES的上下文
    coreVideoTextureCache CV纹理缓存
    framebufferCache GPUImageBuffer缓存
    shaderProgramCache Program的缓存
    shaderProgramUsageHistory Program的使用历史

    2、方法介绍