美文网首页音视频
AVFoundation + OpenGL ES 实现视频滤镜

AVFoundation + OpenGL ES 实现视频滤镜

作者: 主动打电话 | 来源:发表于2020-12-09 22:38 被阅读0次

    最近在学习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 实现视频滤镜

        本文链接:https://www.haomeiwen.com/subject/ljbkgktx.html