[OpenGL]未来视觉5-抖音滤镜

作者: CangWang | 来源:发表于2019-03-14 11:51 被阅读144次

    大家好,我系苍王。
    以下是我这个系列的相关文章,有兴趣可以参考一下,可以给个喜欢或者关注我的文章。
    OpenGL和音视频相关的文章,将会在
    [OpenGL]未来视觉-MagicCamera3实用开源库 当中给大家呈现
    里面会记录我编写这个库的一些经历和经验。

    提到抖音特效,相信很多人都会看过这篇文章
    当一个 Android 开发玩抖音玩疯了之后(二)
    里面提供了六种抖音特效的编写和实现,是使用java代码来实现的,其中提供的demo,并没提供可以选择哪种效果,默认录制完小视频后使用了幻觉的效果。
    这边花了一些时间将这六种效果编写为C++的opengles的代码,并能够在预览的界面中可以选择,已经支持15秒短视频录制(硬解录制)。

    抖音效果

    这里整个框架都是使用C++来编写的,所以如果你寻找C++框架和多种滤镜特效,将会非常适合你们,MagicCamera3,欢迎fork和star。

    1.灵魂出窍

    效果说明,上一帧的透明度不断减少,叠加在现在这帧的上面,并有放大扩散效果。


    soulout.gif
    void MagicSoulOutFilter::onDrawArraysPre() {
        //存在两个图层,开启颜色混合
        glEnable(GL_BLEND);
        //透明度混合
        // 表示源颜色乘以自身的alpha 值,目标颜色乘目标颜色值混合,如果不开启,直接被目标的画面覆盖
        glBlendFunc(GL_SRC_ALPHA,GL_DST_ALPHA);
        //最大帧数mMaxFrames为15,灵魂出窍只显示15帧,设定有8帧不显示
        mProgress = (float)mFrames/mMaxFrames;
        //当progress大于1后置0
        if (mProgress > 1.0f){
            mProgress = 0;
        }
    
        mFrames++;
        //skipFrames为8,23帧后置为0
        if (mFrames > mMaxFrames + mSkipFrames){
            mFrames = 0;
        }
        //setIdentityM函数移植于java中Matrix.setIdentity,初始化矩阵全置为0
        setIdentityM(mMvpMatrix,0);
        //第一帧没有放大,直接设置单位矩阵
        glUniformMatrix4fv(mMvpMatrixLocation,1,GL_FALSE,mMvpMatrix);
        //透明度为1
        float backAlpha = 1;
        if (mProgress > 0){ //如果是灵魂出窍效果显示中
            //计算出窍时透明度值
            alpha = 0.2f - mProgress * 0.2f;
            backAlpha = 1 - alpha;
        }
        //设置不显示灵魂出窍效果时,背景不透明度,不然会黑色
        glUniform1f(mAlphaLocation,backAlpha);
       /**显示相机画面**/
    }
    
    void MagicSoulOutFilter::onDrawArraysAfter() {
        if (mProgress>0){ //如果是灵魂出窍效果显示中
            glUniform1f(mAlphaLocation,alpha);
            //设置放大值
            float scale = 1 + mProgress;
            //设置正交矩阵放大
            scaleM(mMvpMatrix,0,scale,scale,scale);
            //设置到shader里面
            glUniformMatrix4fv(mMvpMatrixLocation,1,GL_FALSE,mMvpMatrix);
            //画出灵魂出校效果
            glDrawArrays(GL_TRIANGLE_STRIP,0,4);
        }
        //关闭颜色混合
        glDisable(GL_BLEND);
    }
    

    这个效果要注意的是
    1.要开启颜色混合,不然灵魂出窍的画面会直接覆盖到上面,直接变成了循环放大的效果
    2.摄像头采集的帧数并不是恒定的,会有变化的,要看fps,所以会偶尔感觉效果帧图时快时慢
    3.使用了java的Matrix中的函数实现正交矩阵的变换,有其他人推荐glm的函数,不知道是否更加高效就是了。

    2.抖动

    shade.gif

    抖动效果包含着两种基础效果
    1.放大
    2.色值偏移
    上一个灵魂出窍的效果已经分析过放大效果了,是一样的。
    那这里最主要是色值偏移的效果,下面是色值偏移的计算。

    #version 300 es
    precision mediump float;
     //每个点的xy坐标
     in vec2 textureCoordinate;
     //对应纹理
     uniform sampler2D inputImageTexture;
     uniform float uTextureCoordOffset;
     out vec4 glFragColor;
    
     void main()
     {
        //直接采样蓝色色值
        vec4 blue = texture(inputImageTexture,textureCoordinate);
         //从效果看,绿色和红色色值特别明显,所以需要对其色值偏移。绿色和红色需要分开方向,不然重叠一起会混色。
         //坐标向左上偏移,然后再采样色值
        vec4 green = texture(inputImageTexture, vec2(textureCoordinate.x + uTextureCoordOffset, textureCoordinate.y + uTextureCoordOffset));
         //坐标向右下偏移,然后再采样色值
        vec4 red = texture(inputImageTexture,vec2(textureCoordinate.x - uTextureCoordOffset,textureCoordinate.y - uTextureCoordOffset));
         //RG两个经过偏移后分别采样,B沿用原来的色值,透明度为1,组合最终输出
        glFragColor = vec4(red.r,green.g,blue.b,blue.a);
     }
    

    在画图前调用计算偏移和放大值

    void MagicShakeEffectFilter::onDrawArraysPre() {
        mProgress = (float)mFrames/mMaxFrames;
        if (mProgress>1){
            mProgress = 0;
        }
        mFrames++;
        if (mFrames>mMaxFrames + mSkipFrames){
            mFrames = 0;
        }
        float scale= 1.0f+0.2f*mProgress;
        //清空正交矩阵
        setIdentityM(mMvpMatrix,0);
        //设置正交矩阵放大,在原位置的地方放大长宽
        scaleM(mMvpMatrix,0,scale,scale,1.0f);
        glUniformMatrix4fv(mMvpMatrixLocation,1,GL_FALSE,mMvpMatrix);
        //设置色值偏移量
        float textureCoordOffset = 0.01f *mProgress;
        glUniform1f(mTextureCoordOffsetLocation,textureCoordOffset);
    }
    

    3.毛刺

    glitch.gif

    其效果分为两部分,
    1.某一行像素值偏移一段距离,产生割裂的感觉,而且是随着y轴变化的
    (1)通过输入纹理中y值到生成(-1到1)的值 jitter
    (2)jitter使用step比较输入的y偏移值来判断是否产生偏移
    (3)取输入的x偏移值赋给jitter
    (4)通过计算偏移值再计算RGB的分量
    (5)最后组合输出
    2.色值偏移
    以下是片段着色器代码,估计初学入门者跟我一样,看得估计也不是很懂,这边需要了解glsl的内建函数,还有色值偏移,还有的颜色的敏感性的。

    #version 300 es
    precision highp float;
     
     in vec2 textureCoordinate;
     uniform sampler2D inputImageTexture;
     //这是个二阶向量,x是横向偏移的值,y是阈值
     uniform vec2 uScanLineJitter;
     //颜色偏移的值
     uniform float uColorDrift;
     out vec4 glFragColor;
    
     float nrand(in float x,in float y){
        //fract(x) = x - floor(x);
        //dot是向量点乘,,sin就是正弦函数
        return fract(sin(dot(vec2(x,y) ,vec2(12.9898,78.233))) * 43758.5453);
     }
    
     void main()
     {
        float u = textureCoordinate.x;
        float v = textureCoordinate.y;
        //用y计算0~1的随机值,再取值-1~1的数
        float jitter = nrand(v ,0.0) * 2.0 - 1.0;
        float drift = uColorDrift;
        //计算向左或向右偏移
        //意思是,如果第一个参数大于第二个参数,那么返回0,否则返回1
        float offsetParam = step(uScanLineJitter.y,abs(jitter));
        //如果offset为0就不偏移,如果为1,就偏移jtter*uScanLineJitter.x的位置
        jitter = jitter * offsetParam * uScanLineJitter.x;
        //这里计算最终的像素值,纹理坐标是0到1之间的数,如果小于0,那么图像就捅到屏幕右边去,如果超过1,那么就捅到屏幕左边去,形成颜色偏移
        vec4 color1 = texture(inputImageTexture,fract(vec2(u + jitter ,v)));
        vec4 color2 = texture(inputImageTexture,fract(vec2(u + jitter + v * drift ,v)));
        glFragColor = vec4(color1.r ,color2.g ,color1.b ,1.0);
     }
    

    通过帧数的不同来计算偏移

    void MagicGlitchFilter::onDrawArraysPre() {
        glUniform2f(mScanLineJitterLocation,mJitterSequence[mFrames],mThreshHoldSequence[mFrames]);
        glUniform1f(mColorDriftLocation,mDriftSequence[mFrames]);
        mFrames ++;
        if (mFrames>mMaxFrames){
            mFrames = 0;
        }
    }
    
    void MagicGlitchFilter::onDrawArraysAfter() {
    
    }
    
    
    void MagicGlitchFilter::onInit() {
        GPUImageFilter::onInit();
        mScanLineJitterLocation = glGetUniformLocation(mGLProgId,"uScanLineJitter");
        mColorDriftLocation = glGetUniformLocation(mGLProgId,"uColorDrift");
    }
    
    void MagicGlitchFilter::onInitialized() {
        GPUImageFilter::onInitialized();
        //颜色偏移量
        mDriftSequence = new float[9]{0.0f, 0.03f, 0.032f, 0.035f, 0.03f, 0.032f, 0.031f, 0.029f, 0.025f};
        //偏移的x值
        mJitterSequence = new float[9]{0.0f, 0.03f, 0.01f, 0.02f, 0.05f, 0.055f, 0.03f, 0.02f, 0.025f};
        //偏移的y值
        mThreshHoldSequence = new float[9]{1.0f, 0.965f, 0.9f, 0.9f, 0.9f, 0.6f, 0.8f, 0.5f, 0.5f};
    }
    

    缩放

    scale.gif

    缩放效果是最简单的,通过中间帧的计算出放大和缩小正交矩阵

    void MagicScaleFilter::onDrawArraysPre() {
        if (mFrames <= mMiddleFrames){ //根据中间帧为间隔,放大过程
            mProgress = mFrames * 1.0f /mMiddleFrames;
        } else{  //缩小过程
            mProgress = 2.0f - mFrames * 1.0f /mMiddleFrames;
        }
        setIdentityM(mMvpMatrix, 0);
        float scale = 1.0f+0.3f*mProgress;
        //正交矩阵放大
        scaleM(mMvpMatrix,0,scale,scale,scale);
        glUniformMatrix4fv(mMvpMatrixLocation,1,GL_FALSE,mMvpMatrix);
        mFrames++;
        if (mFrames>mMaxFrames){
            mFrames = 0;
        }
    }
    

    4.反白

    shinewhite.gif

    也就是通过取帧的时间来计算白色比例(rgb 0为黑色,1为白色)

    #version 300 es
    precision mediump float;
     
     in vec2 textureCoordinate;
     uniform sampler2D inputImageTexture;
     //控制曝光程度
     uniform float uAdditionalColor;
     out vec4 glFragColor;
    
     void main()
     {
        vec4 color = texture(inputImageTexture,textureCoordinate);
         //最大值为1,色值全部变白,最小值回回到原本的色值
        glFragColor = vec4(color.r + uAdditionalColor,color.g+uAdditionalColor,color.b+uAdditionalColor,color.a);
     }
    

    输入progress比例值

    void MagicShineWhiteFilter::onDrawArraysPre() {
        if (mFrames<=mMiddleFrames){ //根据中间值来增加色值
            mProgress = mFrames*1.0f /mMiddleFrames;
        } else{ //减少色值
            mProgress = 2.0f-mFrames*1.0f /mMiddleFrames;
        }
        mFrames++;
        if (mFrames > mMaxFrames){
            mFrames = 0;
        }
    
        glUniform1f(mAdditionColorLocation,mProgress);
    }
    

    幻觉

    verigo.gif
    幻觉这个效果需要使用Lut纹理,以及fbo缓存混色叠加
    1.Lut图说白了,就是颜色查找替换,Lut图一般可以使用ps输出,通过设计师给出,可以大大减少编写滤镜的的编写。
    LUT原理说明
    怎么正确制作该死的LUT图
    2.FBO提供了一系列的缓冲区,包括颜色缓冲区、深度缓冲区和模板缓冲区(需要注意的是FBO中并没有提供累积缓冲区)这些逻辑的缓冲区在FBO中被称为 framebuffer-attachable images说明它们是可以绑定到FBO的二维像素数组。

    FBO中有两类绑定的对象:纹理图像(texture images)和渲染图像(renderbuffer images)。如果纹理对象绑定到FBO,那么OpenGL就会执行渲染到纹理(render to texture)的操作,如果渲染对象绑定到FBO,那么OpenGL会执行离屏渲染(offscreen rendering),这里这两种都会使用到。

    初始化顶点着色器和片段着色器,Lut纹理,以及初始化fbo id

    void MagicVerigoFilter::onInitialized() {
        GPUImageFilter::onInitialized();
        //用于上一帧数据,用于下一帧
        mLastFrameProgram = loadProgram(readShaderFromAsset(mAssetManager,"nofilter_v.glsl")->c_str(),readShaderFromAsset(mAssetManager,"common_f.glsl")->c_str());
        //此帧使用的绘制program
        mCurrentFrameProgram = loadProgram(readShaderFromAsset(mAssetManager,"nofilter_v.glsl")->c_str(),readShaderFromAsset(mAssetManager,"verigo_f2.glsl")->c_str());
        //Lut纹理
        mLutTexture = loadTextureFromAssetsRepeat(mAssetManager,"lookup_vertigo.png");
    }
    
    void MagicVerigoFilter::onInputSizeChanged(const int width, const int height) {
        mScreenWidth = width;
        mScreenHeight = height;
        //建立三个fbo纹理
        mRenderBuffer  = new RenderBuffer(GL_TEXTURE8,width,height);
        mRenderBuffer2 = new RenderBuffer(GL_TEXTURE9,width,height);
        mRenderBuffer3 = new RenderBuffer(GL_TEXTURE10,width,height);
    }
    
    

    RenderBuffer使用到的fbo初始化,纹理绑定,和纹理记录

    //fbo初始化
    RenderBuffer::RenderBuffer(GLenum activeTextureUnit, int width, int height) {
        mWidth = width;
        mHeight = height;
        //激活纹理插槽
        glActiveTexture(activeTextureUnit);
    //    mTextureId = get2DTextureRepeatID();
        //纹理id
        mTextureId = get2DTextureID();
    //    unsigned char* texBuffer = (unsigned char*)malloc(sizeof(unsigned char*) * width * height * 4);
    //    glTexImage2D(GL_TEXTURE_2D,0,GL_RGBA,width,height,0,GL_RGBA,GL_UNSIGNED_BYTE,texBuffer);
        glTexImage2D(GL_TEXTURE_2D,0,GL_RGBA,width,height,0,GL_RGBA,GL_UNSIGNED_BYTE, nullptr);
        glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
        glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);
        glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_S,GL_CLAMP_TO_EDGE);
        glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_T,GL_CLAMP_TO_EDGE);
        //生成fb的id
        glGenFramebuffers(1,&mFrameBufferId);
        glBindFramebuffer(GL_FRAMEBUFFER,mFrameBufferId);
        //生成渲染缓冲区id
        glGenRenderbuffers(1,&mRenderBufferId);
        glBindRenderbuffer(GL_RENDERBUFFER,mRenderBufferId);
        //指定存储在 renderbuffer 中图像的宽高以及颜色格式,并按照此规格为之分配存储空间
        glRenderbufferStorage(GL_RENDERBUFFER,GL_DEPTH_COMPONENT16,width,height);
        //复位
        glBindFramebuffer(GL_FRAMEBUFFER,0);
        glBindRenderbuffer(GL_RENDERBUFFER,0);
    }
    //fbo绘制前配置
    void RenderBuffer::bind() {
        //清空视口
        glViewport(0,0,mWidth,mHeight);
        //绑定fb的纹理id
        glBindFramebuffer(GL_FRAMEBUFFER,mFrameBufferId);
        //绑定2D纹理关联到fbo
        glFramebufferTexture2D(GL_FRAMEBUFFER,GL_COLOR_ATTACHMENT0,GL_TEXTURE_2D,mTextureId,0);
        //绑定fbo纹理到渲染缓冲区对象
        glBindRenderbuffer(GL_RENDERBUFFER,mRenderBufferId);
        //将渲染缓冲区作为深度缓冲区附加到fbo
        glFramebufferRenderbuffer(GL_FRAMEBUFFER,GL_DEPTH_ATTACHMENT,GL_RENDERBUFFER,mRenderBufferId);
        //检查fbo的状态
        if(glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE){
            ALOGE("framebuffer error");
        }
    }
    //fbo记录
    void RenderBuffer::unbind() {
        //移除绑定
        glBindFramebuffer(GL_FRAMEBUFFER,0);
        glBindRenderbuffer(GL_RENDERBUFFER,0);
    //    glActiveTexture(GL_TEXTURE0);
    }
    

    1.这里先保存摄像头数据到第一个fbo中。
    2.摄像头数据和Lut纹理混合,以及再使用上一帧(第二个fbo的纹理)的色值组合后,显示到屏幕上。
    3.将第2步显示的画面数据保存到第三个fbo中
    4.将第三个fbo中的纹理再次保存到第二个fbo中,用于下一帧的绘制。(无法减少这一步,不然会灰屏)

    void MagicVerigoFilter::onDrawPrepare() {
        //绑定纹理
        mRenderBuffer->bind();
        glClear(GL_COLOR_BUFFER_BIT);
    }
    
    void MagicVerigoFilter::onDrawArraysAfter() {
        //将摄像头的数据保存到mRenderBuffer的fbo中
        mRenderBuffer->unbind();
    
        //在顶层画帧,真正画绘制的画面
        drawCurrentFrame();
    
        mRenderBuffer3->bind();
        //绘制当前帧到mRenderBuffer3的fbo中
        drawCurrentFrame();
        //将当前帧保存到生成mRenderBuffer3的fbo
        mRenderBuffer3->unbind();
    
        mRenderBuffer2->bind();
        //使用mRenderBuffer的fbo,再绘制mRenderBuffer2的纹理fbo中
        drawToBuffer();
        //生成mRenderBuffer2的fbo,用于下一帧的绘制
        mRenderBuffer2->unbind();
        mFirst = false;
    }
    

    色彩混合还是要看shader。

    #version 300 es
    precision mediump float;
    in mediump vec2 textureCoordinate;
    uniform sampler2D inputImageTexture;     //当前输入纹理
    uniform sampler2D inputTextureLast; //上一次纹理
    uniform sampler2D lookupTable;      // 颜色查找表纹理
    
    out vec4 glFragColor;
    
    //固定的Lut纹理对换计算
    vec4 getLutColor(vec4 textureColor,sampler2D lookupTexture){
        float blueColor = textureColor.b * 63.0;
    
        mediump vec2 quad1;
        quad1.y = floor(floor(blueColor)/8.0);
        quad1.x = floor(blueColor) - quad1.y*8.0;
    
        mediump vec2 quad2;
        quad2.y = floor(ceil(blueColor) /8.0);
        quad2.x = ceil(blueColor) - quad2.y*8.0;
    
        highp vec2 texPos1;
        texPos1.x = (quad1.x * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * textureColor.r);
        texPos1.y = (quad1.y * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * textureColor.g);
        texPos1.y = 1.0-texPos1.y;
    
        highp vec2 texPos2;
        texPos2.x = (quad2.x * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * textureColor.r);
        texPos2.y = (quad2.y * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * textureColor.g);
        texPos2.y = 1.0-texPos2.y;
    
        lowp vec4 newColor1 = texture(lookupTexture,texPos1);
        lowp vec4 newColor2 = texture(lookupTexture,texPos2);
    
        lowp vec4 newColor = mix(newColor1,newColor2,fract(blueColor));
        return newColor;
    }
    
    void main(){
        //上一帧纹理
        vec4 lastFrame = texture(inputTextureLast,textureCoordinate);
        //此帧对应的Lut转换纹理
        vec4 currentFrame = getLutColor(texture(inputImageTexture,textureCoordinate),lookupTable);
        //上一帧和此帧混色处理
        glFragColor = vec4(0.95 * lastFrame.r  +  0.05* currentFrame.r,currentFrame.g * 0.2 + lastFrame.g * 0.8, currentFrame.b,1.0);
    }
    
    image.png

    这里最难理解应该是叠色,移动的时候,很明显看到移出的是蓝色,取出此帧的蓝色部分移出,蓝色的部分就应该全取此帧的蓝色值。

    image.png

    然后移动过后,发现红色值滞留,而其他值已经近乎没有,那么应该取上一帧的绝大部分的红色值(如果全取会有留影,不会消失)
    所以多试几次的经验值就是。
    glFragColor = vec4(0.95 * lastFrame.r + 0.05* currentFrame.r,currentFrame.g * 0.2 + lastFrame.g * 0.8, currentFrame.b,1.0);

    暂时介绍六种滤镜效果,以后会不定时更新效果,有兴趣的同学,可以关注点赞一下。
    新建一个专栏群,希望有兴趣的同学多多讨论。


    客户端音视频Opengles群

    相关文章

      网友评论

        本文标题:[OpenGL]未来视觉5-抖音滤镜

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