AVFoundation + OpenGL ES 实现视频滤镜

最近在学习OpenGL,本篇文章是学习OpenGL一段时间后做的练手项目的总结。先来看看最终的效果:


逐渐马赛克.gif

幻影.gif

练手项目就不使用第三方框架了,就使用 AVFoundation 和 OpenGLES 来实现。AVFoudation用来采集摄像头的每帧数据;OpenGLES用于处理特效,并将图像显示到界面上。

(一)AVFoudation 采集视频数据

(1)几个重要的类

AVCaptureSession:整个视频捕捉功能的管理
AVCaptureDevice:捕捉设备,代表摄像头,麦克风等硬件
AVCaptureDeviceInput:AVCaptureDevice不能直接使用,需要包装成 AVCaptureDeviceInput,才能传入AVCaptureSession中
AVCaptureOutput:结果输出类,设置了什么输出,最后就会把捕捉结果以设置的格式输出
a AVCaptureStillImageOutput 输出静态图片
b AVCaputureMovieFileOutput 输出视频
c AVCaputureAudioDataOutput 输出每帧音频数据
d AVCaputureVideoDataOutput 输出每帧视频数据
例如,我只需要用到每帧视频数据,那么设置 AVCaputureVideoDataOutput 就可以了

AVCaptureConnection:代表输入和输出设备之间的连接,设置一些输入或者输出的属性
AVCaptureVideoPreviewLayer:照片/视频捕捉结果的预览图层

(2)初始化和设置 session

基本思路就是创建session,然后将输入设备添加到session中,再设置捕捉之后需要输出的数据格式,然后开启session,就能捕捉到数据了。这里要注意的是改变session的配置时,都需要在改变前后写上 beginConfiguration 和 commitConfiguration 方法。

- (void)setup {
    //所有video设备
    NSArray *videoDevices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo];
    //前置摄像头
    self.frontCamera = [AVCaptureDeviceInput deviceInputWithDevice:videoDevices.lastObject error:nil];
    self.backCamera = [AVCaptureDeviceInput deviceInputWithDevice:videoDevices.firstObject error:nil];
    //设置当前设备为前置
    self.videoInputDevice = self.backCamera;
    //视频输出
    self.videoDataOutput = [[AVCaptureVideoDataOutput alloc] init];
    [self.videoDataOutput setSampleBufferDelegate:self queue:self.captureQueue];
    // 丢弃延迟的视频帧
    self.videoDataOutput.alwaysDiscardsLateVideoFrames = YES;
    // 指定像素的输出格式
    self.videoDataOutput.videoSettings = @{
        (__bridge NSString *)kCVPixelBufferPixelFormatTypeKey:@(kCVPixelFormatType_420YpCbCr8BiPlanarFullRange)
    };
    //配置
    [self.captureSession beginConfiguration];
    if ([self.captureSession canAddInput:self.videoInputDevice]) {
        [self.captureSession addInput:self.videoInputDevice];
    }
    if([self.captureSession canAddOutput:self.videoDataOutput]){
        [self.captureSession addOutput:self.videoDataOutput];
    }
    // 设置分辨率
    [self setVideoPreset];
    [self.captureSession commitConfiguration];
    
    self.videoConnection = [self.videoDataOutput connectionWithMediaType:AVMediaTypeVideo];
    //设置视频输出方向
    self.videoConnection.videoOrientation = AVCaptureVideoOrientationPortrait;
    // 设置fps
    [self updateFps:30];
}

// 设置分辨率
- (void)setVideoPreset{
    if ([self.captureSession canSetSessionPreset:AVCaptureSessionPreset1920x1080])  {
        self.captureSession.sessionPreset = AVCaptureSessionPreset1920x1080;
        _witdh = 1080; _height = 1920;
    }else if ([self.captureSession canSetSessionPreset:AVCaptureSessionPreset1280x720]) {
        self.captureSession.sessionPreset = AVCaptureSessionPreset1280x720;
        _witdh = 720; _height = 1280;
    }else{
        self.captureSession.sessionPreset = AVCaptureSessionPreset640x480;
        _witdh = 480; _height = 640;
    }
}

// 设置fps
-(void)updateFps:(NSInteger) fps{
    //获取当前capture设备
    NSArray *videoDevices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo];
    
    //遍历所有设备(前后摄像头)
    for (AVCaptureDevice *vDevice in videoDevices) {
        //获取当前支持的最大fps
        float maxRate = [(AVFrameRateRange *)[vDevice.activeFormat.videoSupportedFrameRateRanges objectAtIndex:0] maxFrameRate];
        //如果想要设置的fps小于或等于做大fps,就进行修改
        if (maxRate >= fps) {
            //实际修改fps的代码
            if ([vDevice lockForConfiguration:NULL]) {
                vDevice.activeVideoMinFrameDuration = CMTimeMake(10, (int)(fps * 10));
                vDevice.activeVideoMaxFrameDuration = vDevice.activeVideoMinFrameDuration;
                [vDevice unlockForConfiguration];
            }
        }
    }
}

- (AVCaptureSession *)captureSession{
    if (!_captureSession) {
        _captureSession = [[AVCaptureSession alloc] init];
    }
    return _captureSession;
}

- (dispatch_queue_t)captureQueue{
    if (!_captureQueue) {
        _captureQueue = dispatch_queue_create("TMCapture Queue", NULL);
    }
    return _captureQueue;
}

输出的视频帧以代理方式回调:

-(void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection {
    [self.delegate captureSampleBuffer:sampleBuffer];
}

(二) OpenGLES 处理特效

这一部分内容较多,需要有OpenGL的基础知识,分为如下几个步骤:

a 创建 frameBuffer 和 renderBuffer
b 创建纹理缓冲区,从视频帧数据获取纹理
c 编译链接自定义shader
d 将 attributes,uniforms,texture 传入 shader
e 绘制与显示

首先自定义一个类继承于 CAEAGLLayer(这个类是苹果提供的专门用于显示OpenGL图像数据的layer),提供一个方法接收外部的视频帧数据:

typedef NS_ENUM(NSInteger, LZProgramType) {
    LZProgramTypeVertigo, // 幻影
    LZProgramTypeRag, // 局部模糊
    LZProgramTypeShake, // 抖动
    LZProgramTypeMosaic // 马赛克
};

@interface LZDisplayLayer : CAEAGLLayer

// 使用哪一种特效
@property(nonatomic, assign) LZProgramType useProgram;

- (instancetype)initWithFrame:(CGRect)frame;
- (void)displayWithPixelBuffer:(CVPixelBufferRef)pixelBuffer;

@end

(1)创建 frameBuffer 和 renderBuffer

我们最终绘制完成的每帧数据将保存在这两个Buffer中,renderBuffer会与CAEAGLLayer绑定。

- (void)createBuffers
{
    // 创建帧缓存区
    glGenFramebuffers(1, &_frameBufferHandle);
    glBindFramebuffer(GL_FRAMEBUFFER, _frameBufferHandle);
    
    // 创建color缓存区
    glGenRenderbuffers(1, &_colorBufferHandle);
    glBindRenderbuffer(GL_RENDERBUFFER, _colorBufferHandle);
    
    // 绑定渲染缓存区
    [_context renderbufferStorage:GL_RENDERBUFFER fromDrawable:self];
    
    // 得到渲染缓存区的尺寸
    glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_WIDTH, &_backingWidth);
    glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_HEIGHT, &_backingHeight);
    
    // 绑定renderBuffer到FrameBuffer
    glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, _colorBufferHandle);
  
}

(2)创建纹理缓冲区,从视频帧数据获取纹理

纹理在OpenGL中就代表图像的原始数据(位图),由于视频帧数据是YUV420格式的数据(AVCaptureSession 采集时设置的kCVPixelFormatType_420YpCbCr8BiPlanarFullRange),会有两个平面(Y平面和UV平面),所以对应的纹理也需要创建两个。后面在shader的编写中,会把YUV数据转化为RGB数据。
由于是视频数据渲染比较频繁,所以使用纹理缓冲区,其工作原理就是创建一块专门用于存放纹理的缓冲区,每次创建新的纹理都使用缓冲区的内存,这样不用重新创建,在需要频繁创建纹理时可以提高效率。

创建纹理缓冲区:

    /*
     CVOpenGLESTextureCacheCreate
     功能:   创建 CVOpenGLESTextureCacheRef 创建新的纹理缓存
     参数1:  kCFAllocatorDefault默认内存分配器.
     参数2:  NULL
     参数3:  EAGLContext  图形上下文
     参数4:  NULL
     参数5:  新创建的纹理缓存
     @result kCVReturnSuccess
     */
    CVReturn err;
    err = CVOpenGLESTextureCacheCreate(kCFAllocatorDefault, NULL, _context, NULL, &_videoTextureCache);
    if (err != noErr) {
        NSLog(@"Error at CVOpenGLESTextureCacheCreate %d", err);
        return;
    }

创建纹理:

    // 返回像素缓冲区的平面数
    size_t planeCount = CVPixelBufferGetPlaneCount(pixelBuffer);
    /*
     从像素缓存区pixelBuffer创建Y和UV纹理,这些纹理会被绘制在帧缓存区的Y平面上.
     */
    
    // 激活纹理
    glActiveTexture(GL_TEXTURE0);
    
    // 创建亮度纹理-Y纹理
    /*
     CVOpenGLESTextureCacheCreateTextureFromImage
     功能:根据CVImageBuffer创建CVOpenGlESTexture 纹理对象
     参数1: 内存分配器,kCFAllocatorDefault
     参数2: 纹理缓存.纹理缓存将管理纹理的纹理缓存对象
     参数3: sourceImage.
     参数4: 纹理属性.默认给NULL
     参数5: 目标纹理,GL_TEXTURE_2D
     参数6: 指定纹理中颜色组件的数量(GL_RGBA, GL_LUMINANCE, GL_RGBA8_OES, GL_RG, and GL_RED (NOTE: 在 GLES3 使用 GL_R8 替代 GL_RED).)
     参数7: 帧宽度
     参数8: 帧高度
     参数9: 格式指定像素数据的格式
     参数10: 指定像素数据的数据类型,GL_UNSIGNED_BYTE
     参数11: planeIndex
     参数12: 纹理输出新创建的纹理对象将放置在此处。
     */
    CVReturn err;
    err = CVOpenGLESTextureCacheCreateTextureFromImage(kCFAllocatorDefault,
                                                       _videoTextureCache,
                                                       pixelBuffer,
                                                       NULL,
                                                       GL_TEXTURE_2D,
                                                       GL_RED_EXT,
                                                       frameWidth,
                                                       frameHeight,
                                                       GL_RED_EXT,
                                                       GL_UNSIGNED_BYTE,
                                                       0,
                                                       &_lumaTexture);
    if (err) {
        NSLog(@"Error at CVOpenGLESTextureCacheCreateTextureFromImage %d", err);
    }
    
    // 配置亮度纹理属性
    // 绑定纹理.
    glBindTexture(CVOpenGLESTextureGetTarget(_lumaTexture), CVOpenGLESTextureGetName(_lumaTexture));
    // 配置纹理放大/缩小过滤方式以及纹理围绕S/T环绕方式
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(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);

    // 如果颜色通道个数>1,则除了Y还有UV-Plane.
    if(planeCount == 2) {
        // 激活UV-plane纹理
        glActiveTexture(GL_TEXTURE1);
        // 创建UV-plane纹理
        /*
         CVOpenGLESTextureCacheCreateTextureFromImage
         功能:根据CVImageBuffer创建CVOpenGlESTexture 纹理对象
         参数1: 内存分配器,kCFAllocatorDefault
         参数2: 纹理缓存.纹理缓存将管理纹理的纹理缓存对象
         参数3: sourceImage.
         参数4: 纹理属性.默认给NULL
         参数5: 目标纹理,GL_TEXTURE_2D
         参数6: 指定纹理中颜色组件的数量(GL_RGBA, GL_LUMINANCE, GL_RGBA8_OES, GL_RG, and GL_RED (NOTE: 在 GLES3 使用 GL_R8 替代 GL_RED).)
         参数7: 帧宽度
         参数8: 帧高度
         参数9: 格式指定像素数据的格式
         参数10: 指定像素数据的数据类型,GL_UNSIGNED_BYTE
         参数11: planeIndex
         参数12: 纹理输出新创建的纹理对象将放置在此处。
         */
        err = CVOpenGLESTextureCacheCreateTextureFromImage(kCFAllocatorDefault,
                                                           _videoTextureCache,
                                                           pixelBuffer,
                                                           NULL,
                                                           GL_TEXTURE_2D,
                                                           GL_RG_EXT,
                                                           frameWidth / 2,
                                                           frameHeight / 2,
                                                           GL_RG_EXT,
                                                           GL_UNSIGNED_BYTE,
                                                           1,
                                                           &_chromaTexture);
        if (err) {
            NSLog(@"Error at CVOpenGLESTextureCacheCreateTextureFromImage %d", err);
        }
        
        // 绑定纹理
        glBindTexture(CVOpenGLESTextureGetTarget(_chromaTexture), CVOpenGLESTextureGetName(_chromaTexture));
        // 配置纹理放大/缩小过滤方式以及纹理围绕S/T环绕方式
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
        glTexParameteri(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);
    }

(3)编译链接自定义shader

特效的实现需要我们自定义片元着色器,使用OpenGL和苹果封装的着色器无法实现,所以需要自己编译链接编写的shader;分为2步:

a 编译shader
b 将shader和program链接

编译 shader:

- (GLuint)compileShaderWithName:(NSString *)name type:(GLenum)shaderType {
    
    //1.获取shader 路径
    NSString *shaderPath = [[NSBundle mainBundle] pathForResource:name ofType:shaderType == GL_VERTEX_SHADER ? @"vsh" : @"fsh"];
    NSError *error;
    NSString *shaderString = [NSString stringWithContentsOfFile:shaderPath encoding:NSUTF8StringEncoding error:&error];
    if (!shaderString) {
        NSAssert(NO, @"读取shader失败");
        exit(1);
    }
    
    //2. 创建shader->根据shaderType
    GLuint shader = glCreateShader(shaderType);
    
    //3.获取shader source
    const char *shaderStringUTF8 = [shaderString UTF8String];
    int shaderStringLength = (int)[shaderString length];
    glShaderSource(shader, 1, &shaderStringUTF8, &shaderStringLength);
    
    //4.编译shader
    glCompileShader(shader);
    
    //5.查看编译是否成功
    GLint compileSuccess;
    glGetShaderiv(shader, GL_COMPILE_STATUS, &compileSuccess);
    if (compileSuccess == GL_FALSE) {
        GLchar messages[256];
        glGetShaderInfoLog(shader, sizeof(messages), 0, &messages[0]);
        NSString *messageString = [NSString stringWithUTF8String:messages];
        NSAssert(NO, @"shader编译失败:%@", messageString);
        exit(1);
    }
    //6.返回shader
    return shader;
}

将shader和program链接:

- (GLuint)programWithShaderName:(NSString *)shaderName {
    //1. 编译顶点着色器/片元着色器
    GLuint vertexShader = [self compileShaderWithName:@"Vertex" type:GL_VERTEX_SHADER];
    GLuint fragmentShader = [self compileShaderWithName:shaderName type:GL_FRAGMENT_SHADER];
    
    //2. 将顶点/片元附着到program
    GLuint program = glCreateProgram();
    glAttachShader(program, vertexShader);
    glAttachShader(program, fragmentShader);
    
    //3.linkProgram
    glLinkProgram(program);
    
    //4.检查是否link成功
    GLint linkSuccess;
    glGetProgramiv(program, GL_LINK_STATUS, &linkSuccess);
    if (linkSuccess == GL_FALSE) {
        GLchar messages[256];
        glGetProgramInfoLog(program, sizeof(messages), 0, &messages[0]);
        NSString *messageString = [NSString stringWithUTF8String:messages];
        NSAssert(NO, @"program链接失败:%@", messageString);
        exit(1);
    }
    //5.返回program
    return program;
}

(4)将 attributes,uniforms,texture 传入 shader

attributes:顶点数据和纹理数据,确定图像的位置和尺寸,可传入顶点着色器,再籍由顶点着色器传入片元着色器
uniforms:应用传给shader的常量,可传入顶点着色器和片元着色器
texture:纹理id,代表图像数据,可传入顶点着色器和片元着色器,本项目中顶点着色器不会用到纹理,因此只传入片元着色器

顶点数据和纹理数据的计算:

// 根据视频的方向和纵横比设置四边形顶点
    CGRect viewBounds = self.bounds;
    CGSize contentSize = CGSizeMake(frameWidth, frameHeight);
    
    /*
     AVMakeRectWithAspectRatioInsideRect
     功能: 返回一个按比例缩放的CGRect,该CGRect保持由边界CGRect内的CGSize指定的纵横比
     参数1:希望保持的宽高比或纵横比
     参数2:填充的rect
     */
    CGRect vertexSamplingRect = AVMakeRectWithAspectRatioInsideRect(contentSize, viewBounds);
    
    // 计算四边形坐标以将帧绘制到其中
    CGSize normalizedSamplingSize = CGSizeMake(0.0, 0.0);
    CGSize cropScaleAmount = CGSizeMake(vertexSamplingRect.size.width/viewBounds.size.width,vertexSamplingRect.size.height/viewBounds.size.height);
    if (cropScaleAmount.width > cropScaleAmount.height) {
        normalizedSamplingSize.width = 1.0;
        normalizedSamplingSize.height = cropScaleAmount.height/cropScaleAmount.width;
    }
    else {
        normalizedSamplingSize.width = cropScaleAmount.width/cropScaleAmount.height;
        normalizedSamplingSize.height = 1.0;;
    }
    
    /*
     四顶点数据定义了绘制像素缓冲区的二维平面区域。
     使用(-1,-1)和(1,1)分别作为左下角和右上角坐标形成的顶点数据覆盖整个屏幕。
     */
    GLfloat quadVertexData [] = {
        -1 * normalizedSamplingSize.width, -1 * normalizedSamplingSize.height,
        normalizedSamplingSize.width, -1 * normalizedSamplingSize.height,
        -1 * normalizedSamplingSize.width, normalizedSamplingSize.height,
        normalizedSamplingSize.width, normalizedSamplingSize.height,
    };

/*
     纹理顶点的设置使我们垂直翻转纹理。这使得我们的左上角原点缓冲区匹配OpenGL的左下角纹理坐标系
     */
    CGRect textureSamplingRect = CGRectMake(0, 0, 1, 1);
    GLfloat quadTextureData[] =  {
        CGRectGetMinX(textureSamplingRect), CGRectGetMaxY(textureSamplingRect),
        CGRectGetMaxX(textureSamplingRect), CGRectGetMaxY(textureSamplingRect),
        CGRectGetMinX(textureSamplingRect), CGRectGetMinY(textureSamplingRect),
        CGRectGetMaxX(textureSamplingRect), CGRectGetMinY(textureSamplingRect)
    };

将 attributes,uniforms,texture 传入 shader:

    // 坐标数据
    int position = glGetAttribLocation(self.usingProgram, "position");
    glVertexAttribPointer(position, 2, GL_FLOAT, 0, 0, quadVertexData);
    glEnableVertexAttribArray(position);

    // 更新纹理坐标属性值
    int texCoord = glGetAttribLocation(self.usingProgram, "texCoord");
    glVertexAttribPointer(texCoord, 2, GL_FLOAT, 0, 0, quadTextureData);
    glEnableVertexAttribArray(texCoord);

    // 使用shaderProgram
    glUseProgram(self.program[self.useProgram]);
    self.usingProgram = self.program[0];

    // 获取uniform的位置
    // Y亮度纹理
    uniforms[UNIFORM_Y] = glGetUniformLocation(self.usingProgram, "SamplerY");
    // UV色量纹理
    uniforms[UNIFORM_UV] = glGetUniformLocation(self.usingProgram, "SamplerUV");
    // YUV->RGB
    uniforms[UNIFORM_COLOR_CONVERSION_MATRIX] = glGetUniformLocation(self.usingProgram, "colorConversionMatrix");
    // 时间差
    uniforms[UNIFORM_TIME] = glGetUniformLocation(self.usingProgram, "Time");
    
    glUniform1i(uniforms[UNIFORM_Y], 0);
    glUniform1i(uniforms[UNIFORM_UV], 1);
    glUniformMatrix3fv(uniforms[UNIFORM_COLOR_CONVERSION_MATRIX], 1, GL_FALSE, _preferredConversion);
    
    //传递Uniform属性到shader
    //UNIFORM_COLOR_CONVERSION_MATRIX YUV->RGB颜色矩阵
    glUniformMatrix3fv(uniforms[UNIFORM_COLOR_CONVERSION_MATRIX], 1, GL_FALSE, _preferredConversion);

    // 传入当前时间与绘制开始时间的时间差
    NSTimeInterval time = [[NSDate date] timeIntervalSinceDate:self.startDate];
    glUniform1f(uniforms[UNIFORM_TIME], time);

(5)绘制与显示

    // 绑定帧缓存区
    glBindFramebuffer(GL_FRAMEBUFFER, _frameBufferHandle);
    // 设置视口.
    glViewport(0, 0, _backingWidth, _backingHeight);
    // 绘制图形
    glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
    // 绑定渲染缓存区
    glBindRenderbuffer(GL_RENDERBUFFER, _colorBufferHandle);
    // 显示到屏幕
    [_context presentRenderbuffer:GL_RENDERBUFFER];

(三)shader 特效

特效是自定义片元着色器编写的,马赛克特效:


逐渐马赛克.gif
precision mediump float;
varying highp vec2 texCoordVarying;
uniform sampler2D SamplerY;
uniform sampler2D SamplerUV;
uniform mat3 colorConversionMatrix;

uniform float Time;

const vec2 TexSize = vec2(375.0, 667.0);
const vec2 mosaicSize = vec2(20.0, 20.0);
const float PI = 3.1415926;

vec4 getRgba(vec2 texCoordVarying) {
    mediump vec3 yuv;
    lowp vec3 rgb;
    yuv.x = (texture2D(SamplerY, texCoordVarying).r - (16.0/255.0));
    yuv.yz = (texture2D(SamplerUV, texCoordVarying).rg - vec2(0.5, 0.5));
    rgb = colorConversionMatrix * yuv;
    return vec4(rgb, 1);
}


void main () {
    float duration = 3.0;
    float maxScale = 1.0;
    float time = mod(Time, duration);
    float progress = sin(time * (PI / duration));
    float scale = maxScale * progress;
    vec2 finSize = mosaicSize * scale;
    
    vec2 intXY = vec2(texCoordVarying.x*TexSize.x, texCoordVarying.y*TexSize.y);
    vec2 XYMosaic = vec2(floor(intXY.x/finSize.x)*finSize.x, floor(intXY.y/finSize.y)*finSize.y);
    vec2 UVMosaic = vec2(XYMosaic.x/TexSize.x, XYMosaic.y/TexSize.y);
    
    gl_FragColor = getRgba(UVMosaic);
}

自己搞的,实际应该不会有这种特效吧哈哈哈。原理就是把整个纹理当成是一张375x667的图片,把图片一块块小区域,切分区域的大小随时间变化(正弦函数取上半部分)。根据当前像素点的坐标数据可以确定其在哪一块区域,然后像素点的颜色值就取其所在区域左上角第一个像素点的颜色。

幻影.gif

局部模糊.gif

抖动.gif

幻影,局部模糊,抖动特效是参考雷曼同学的文章。

至此,从采集视频到添加滤镜整个过程就完成了。完整项目的github地址:
https://github.com/linzhesheng/AVFoundationAndOpenGLES。

你可能感兴趣的:(AVFoundation + OpenGL ES 实现视频滤镜)