[OpenGL]未來視覺5-抖音濾鏡

Cang_Wang發表於2019-03-14

大家好,我係蒼王。

以下是我這個系列的相關文章,有興趣可以參考一下,可以給個喜歡或者關注我的文章。

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);
}
複製程式碼

5幻覺

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群

相關文章