天天看點

Android視訊編輯器(四)通過OpenGL給視訊增加不同濾鏡效果前言結語

前言

        在上面的幾篇文章中,我們實作了錄制視訊、通過opengl在錄制過程中和給本地視訊添加水印和美顔效果,還沒看過的童鞋,可以看該系列的前面三篇文章。而這篇部落格,我們來實作給視訊添加各種各樣的濾鏡。現如今給視訊加各種不同的濾鏡對各視訊類app來說,已經是标配功能。而添加各類濾鏡可以使我們拍攝的視訊更美觀漂亮。是以這篇部落格,我們就要來實作給視訊添加上除美顔之外的其他濾鏡。      本系列的文章包括如下:

        1、android視訊編輯器之視訊錄制、斷點續錄、對焦等        2、android視訊編輯器之錄制過程中加水印和美白效果        3、android視訊編輯器之本地視訊加美白效果和加視訊水印        4、android視訊編輯器之 通過OpenGL給視訊增加不同的濾鏡效果         5、android視訊編輯器之音頻編解碼、mono轉stereo、音頻混音、音頻音量調節        6、android視訊編輯器之通過OpenGL做不同視訊的拼接        7、android視訊編輯器之音視訊裁剪、增加背景音樂等       ps:    本項目的大部分濾鏡來自于MagicCamera這個開源項目,非常感謝原作者的分享,有興趣的朋友可以去看看。

用新濾鏡替換美顔濾鏡

        在以前的部落格中,我們實作了一個MagicBeautyFilter,來對視訊進行美顔處理,那從原理上來說,隻要我們更換掉這個MagicBeautyFilter,采用其他濾鏡的Filter,即可實作添加完全不同的一種濾鏡。        如此的話,那我們先來簡單來更換一個懷舊風的濾鏡。        首先,建立一個MagicAntiqueFilter類,繼承GPUImageFilter,在MagicAntiqueFilter的構造函數中,傳入懷舊風的fragmentShader。  

public MagicAntiqueFilter(){
        super(NO_FILTER_VERTEX_SHADER, OpenGlUtils.readShaderFromRawResource(R.raw.antique));
    }
           

        在OpenGL的shader檔案中,每個濾鏡都需要兩個shader檔案,其中vertexShader是控制每個像素點的位置的,而fragmentShader就是控制每個像素點的顔色的,而我們這裡傳入的R.raw.antique檔案就是懷舊風的顔色shader檔案。         而fragmentShader最重要的是,在代碼的最後,給gl_FragColor指派,進而決定像素點的顔色,也就是說每個fragmentShader檔案的最後一行代碼,肯定是類似如下的指派代碼:       

gl_FragColor = vec4(textureColor.r, textureColor.g, textureColor.b, 1.0);
           

          至于具體的濾鏡實作邏輯,有興趣的朋友,就可以深入了解下OpenGL的shader檔案編寫規則,這裡就不做深入了,不然每個濾鏡的實作邏輯可能都能寫一篇部落格了。濾鏡的實作原理可以見  濾鏡制作的基本方法(一)          實作了MagicAntiqueFilter類之後,我們簡單的把原來的MagicBeautyFilter替換為新的濾鏡。在CameraDrawer類中進行替換,然後加上一些控制邏輯        

//        mBeautyFilter = new MagicBeautyFilter();
mBeautyFilter = new MagicAntiqueFilter();
           
if (mBeautyFilter != null && isOpen){
            EasyGlUtils.bindFrameTexture(fFrame[0],fTexture[0]);
            GLES20.glViewport(0,0,mPreviewWidth,mPreviewHeight);
            mBeautyFilter.onDrawFrame(mBeFilter.getOutputTexture());
            EasyGlUtils.unBindFrameBuffer();
            mProcessFilter.setTextureId(fTexture[0]);
        }else {
            mProcessFilter.setTextureId(mBeFilter.getOutputTexture());
    }
           
private boolean isOpen = true;
    public void changeFilter(){
        isOpen = !isOpen;
    }
           

       這個changeFiler方法就是暴露給上層的控制api。進而決定是否加該濾鏡。

      然後具體的效果就如下

Android視訊編輯器(四)通過OpenGL給視訊增加不同濾鏡效果前言結語
Android視訊編輯器(四)通過OpenGL給視訊增加不同濾鏡效果前言結語

       難道這樣就完了?我們又不講解OpenGL濾鏡的具體實作,上面這樣就可以實作給視訊加各種其他的濾鏡,那這篇文章豈不是看起來不就虎頭蛇尾的。。。必不可能讓本篇部落格如此虎頭蛇尾,那下面我們做什麼呢?既然如此的話,我們下面就來實作一個看起來很酷炫的功能,在傳統的app更換濾鏡的時候,都是選擇相應的圖示,然後進行濾鏡的更換,那我們必不可能如此low,是以呢,我們來實作vue的濾鏡更換方式,通過在螢幕上左右滑動,進而實作更換濾鏡。        也就是如下效果

Android視訊編輯器(四)通過OpenGL給視訊增加不同濾鏡效果前言結語

      ps:不要在意右下角的那個奇葩水印,這是視訊本身的原因。圖檔來自于vue這個應用選取本地視訊時候的手機截圖。        左邊就是有濾鏡,而右邊就是沒有濾鏡的效果。這樣可以讓使用者明顯的看出不同的濾鏡有什麼效果,友善進行對比。

實作左右滑動切換濾鏡

      那麼我們來分析一下,如何才能實作這樣的效果呢?

      首先,我們肯定需要1個以上的濾鏡,才能進行切換,是以先将來自于MagicCamera的各種濾鏡放進項目,如下是他們的Filter類和shader檔案。是以我們去MagicCamera中借用了一些濾鏡過來。

Android視訊編輯器(四)通過OpenGL給視訊增加不同濾鏡效果前言結語
Android視訊編輯器(四)通過OpenGL給視訊增加不同濾鏡效果前言結語

        然後,我們這個就比以前的要複雜一些了,我們先需要儲存三個濾鏡,一個是curFilter,一個左邊的濾鏡leftFilter,以及一個右邊的濾鏡rightFilter。然後監聽界面的滑動事件,如果是向左滑,那麼在繪制的時候,左邊部分我們加上curFilter,右邊部分加上rightFilter,同理向右滑,我們右邊部分加上curFilter,而左邊部分加上leftFilter。這裡有一個核心的問題,如何給同一幀添加兩個不同的濾鏡,其實在OpenGL中,繪制的時候,除了可以通過GLES20.glViewport方法設定畫面的大小,還有一個GLES20.glScissor函數,可以對畫面進行裁剪繪制,也就是通過裁剪測試可以是渲染的時候用來限制繪制區域,可以在螢幕(幀緩沖)上指定一個矩形區域,不在此矩形區域内的片元将被丢棄,隻有在矩形區域内的片元才有機會最終進入幀緩沖。          開啟裁剪測試功能

GLES20.glEnable(GLES20.GL_SCISSOR_TEST);
           

          設定要裁剪的區域

GLES20.glScissor(0,0,offset,height);
           

          關閉裁剪測試功能

GLES20.glDisable(GLES20.GL_SCISSOR_TEST);
           

          通過裁剪功能,我們就可以把同一個畫面幀的資料,通過進行裁剪,來加上不同的濾鏡,進行繪制。最終實作我們想要的效果。

          既然分析完畢了,那我們就來一步步實作我們的代碼

功能實作

         首先,建立一個SlideGpufilterGroup類,該類就是我們來實作這項功能的核心類

public class SlideGpufilterGroup {
     }
           

          然後,我們需要一個數組來标記我們所有的濾鏡

private MagicFilterType[] types = new MagicFilterType[]{
            MagicFilterType.NONE,
            MagicFilterType.WARM,
            MagicFilterType.ANTIQUE,
            MagicFilterType.INKWELL,
            MagicFilterType.BRANNAN,
            MagicFilterType.N1977,
            MagicFilterType.FREUD,
            MagicFilterType.HEFE,
            MagicFilterType.HUDSON,
            MagicFilterType.NASHVILLE,
            MagicFilterType.COOL
    };
           

       這裡的MagicFilterType,就是我們定義的用來表示濾鏡的枚舉。       然後定義三個濾鏡,分别表示目前的濾鏡,左邊一個的濾鏡,和右邊一個的濾鏡,對外提供他們的初始化和設定size。

private GPUImageFilter curFilter;
    private GPUImageFilter leftFilter;
    private GPUImageFilter rightFilter;

    public void init() {
        curFilter.init();
        leftFilter.init();
        rightFilter.init();
    }
    private void onFilterSizeChanged(int width, int height) {
        curFilter.onInputSizeChanged(width, height);
        leftFilter.onInputSizeChanged(width, height);
        rightFilter.onInputSizeChanged(width, height);
        curFilter.onDisplaySizeChanged(width, height);
        leftFilter.onDisplaySizeChanged(width, height);
        rightFilter.onDisplaySizeChanged(width, height);
    }
           

       然後定義繪制的畫面的寬高,由接口從外部進行設定

private int width, height;
     public void onSizeChanged(int width, int height) {
        this.width = width;
        this.height = height;
        GLES20.glGenFramebuffers(1, fFrame, 0);
        EasyGlUtils.genTexturesWithParameter(1, fTexture, 0, GLES20.GL_RGBA, width, height);
        onFilterSizeChanged(width, height);
     }
           

       然後再定義一個幀緩沖區和一個紋理,用于繪制圖像

private int[] fFrame = new int[1];
    private int[] fTexture = new int[1];
           

      并且初始化一個Scroller,來完成滑動事件的響應,Scroller的使用可以見這篇文章 Android Scroller完全解析。

private Scroller scroller;
           
scroller = new Scroller(MyApplication.getContext());
           

       初始化一個目前的filter的index,也就是标記位,辨別目前是哪一個filter。

private int curIndex = 0;
           

       然後,在構造函數裡面進行一些初始化

public SlideGpufilterGroup() {
        initFilter();
        scroller = new Scroller(MyApplication.getContext());
    }
           

        initFilter方法其實就是目前三種filter的初始化   

public void initFilter() {
        curFilter = getFilter(getCurIndex());
        leftFilter = getFilter(getLeftIndex());
        rightFilter = getFilter(getRightIndex());
    }
           

       getFilter其實就是根據filter的index進行filter對象的初始化

public GPUImageFilter getFilter(int index) {
        GPUImageFilter filter = MagicFilterFactory.initFilters(types[index]);
        if (filter == null) {
            filter = new GPUImageFilter();
        }
        return filter;
    }
           

        而getCurIndex、getLeftIndex、getRightIndex這三個方法,就是分别擷取到目前filter的index和左邊、右邊的filter的index。

private int getCurIndex() {
        return curIndex;
    }
    private int getLeftIndex() {
        int leftIndex = curIndex - 1;
        if (leftIndex < 0) {
            leftIndex = types.length - 1;
        }
        return leftIndex;
    }
    private int getRightIndex() {
        int rightIndex = curIndex + 1;
        if (rightIndex >= types.length) {
            rightIndex = 0;
        }
        return rightIndex;
    }
           

        目前的filter和左邊、右邊的filter我們都有了,現在還有兩個點,一個就是怎麼樣具體去繪制目前資料幀,另一個就是怎麼進行filter的切換。

        首先,我們來看看詳細的繪制規則,首先對外提供一個onDrawFrame方法       

public void onDrawFrame(int textureId) {
        EasyGlUtils.bindFrameTexture(fFrame[0], fTexture[0]);
        if (direction == 0 && offset == 0) {
            curFilter.onDrawFrame(textureId);
        } else if (direction == 1) {
            onDrawSlideLeft(textureId);
        } else if (direction == -1) {
            onDrawSlideRight(textureId);
        }
        EasyGlUtils.unBindFrameBuffer();
    }
           

        該方法,接受一個紋理id,就是我們目前要繪制的紋理。然後将幀緩沖和紋理進行綁定, 下面就是一個if else的判斷,這裡direction其實就是我們自己定義的,用來表示目前是什麼動作的一個int。       

int direction;//0為靜止,-1為向左滑,1為向右滑
           

        用這樣一個int,就可以區分目前的滑動是什麼樣的狀态,如果為0表示沒有滑動,那麼我們隻用繪制curFilter就行了。而上面我們已經進行了curFilter的初始化,它也繼承自GPUImageFilter。我們隻用調用onDrawFragme把目前的紋理id傳入進去就行了。

        而這個offset,其實就是我們記錄的目前的滑動量,如果offset其實=0的,那表示沒有偏移量,那肯定也隻用繪制目前這個filter就行了。

        如果,目前的狀态是向右滑動,也就是direction = 1,我們就調用onDrawSlideLeft函數。

private void onDrawSlideLeft(int textureId) {
        if (locked && scroller.computeScrollOffset()) {
            offset = scroller.getCurrX();
            drawSlideLeft(textureId);
        } else {
            drawSlideLeft(textureId);
            if (locked) {
                if (needSwitch) {
                    reCreateRightFilter();
                    if (mListener != null) {
                        mListener.onFilterChange(types[curIndex]);
                    }
                }
                offset = 0;
                direction = 0;
                locked = false;
            }
        }
    }
           

         我們先來看看scroller的computeScrollOffset方法 官方注釋是     

/**
     * Call this when you want to know the new location.  If it returns true,
     * the animation is not yet finished.
     */ 
    public boolean computeScrollOffset() {
           

         其實就是如果還在滑動的話,就傳回true。         而這個locked,其實就是我們定義的一個用來控制流程的boolean值,指派情況,後面的代碼會詳細講解。         我們先來繼續看這個if邏輯,如果是true的話,就拿到目前的offset偏移量,然後調用drawSlideLeft方法。

private void drawSlideLeft(int textureId) {
        GLES20.glViewport(0, 0, width, height);
        GLES20.glEnable(GLES20.GL_SCISSOR_TEST);
        GLES20.glScissor(0, 0, offset, height);
        leftFilter.onDrawFrame(textureId);
        GLES20.glDisable(GLES20.GL_SCISSOR_TEST);
        GLES20.glViewport(0, 0, width, height);
        GLES20.glEnable(GLES20.GL_SCISSOR_TEST);
        GLES20.glScissor(offset, 0, width - offset, height);
        curFilter.onDrawFrame(textureId);
        GLES20.glDisable(GLES20.GL_SCISSOR_TEST);
    }
           

        這個方法,就是繪制左滑情況的具體繪制,首先設定畫面大小,然後開啟了裁剪功能,然後用leftFilter繪制左側的畫面,用curFilter繪制右側的畫面,兩者合起來就是一個完整的畫面。offset就是用來确定左邊和右邊分别應該繪制多寬的值。同樣我們也會有一個相同原理,類似代碼的drawSlideRight方法     

private void drawSlideRight(int textureId) {
        GLES20.glViewport(0, 0, width, height);
        GLES20.glEnable(GLES20.GL_SCISSOR_TEST);
        GLES20.glScissor(0, 0, width - offset, height);
        curFilter.onDrawFrame(textureId);
        GLES20.glDisable(GLES20.GL_SCISSOR_TEST);
        GLES20.glViewport(0, 0, width, height);
        GLES20.glEnable(GLES20.GL_SCISSOR_TEST);
        GLES20.glScissor(width - offset, 0, offset, height);
        rightFilter.onDrawFrame(textureId);
        GLES20.glDisable(GLES20.GL_SCISSOR_TEST);
 }
           

        然後繼續看if判斷的false情況,如果是false的話,說明滑動動畫還沒停下來,如果locked是true的話,就判斷是否需要切換濾鏡,needSwitch在什麼情況下為true呢,其實他的判斷是通過判斷滑動事件,如果目前的offset超過了螢幕寬度的1/3,具體的地方我們接下來會說到。就是在滑動動畫停止的時候,如果需要切換filter,我們就調用了reCreateRightFilter()方法,并且傳回了一個監聽回調。而這個reCreateRightFilter方法具體代碼如下。   

private void reCreateRightFilter() {
        decreaseCurIndex();
        rightFilter.destroy();
        rightFilter = curFilter;
        curFilter = leftFilter;
        leftFilter = getFilter(getLeftIndex());
        leftFilter.init();
        leftFilter.onDisplaySizeChanged(width, height);
        leftFilter.onInputSizeChanged(width, height);
        needSwitch = false;
    }
           

      這些代碼裡面的decreaseCurIndex方法,其實就是移動了curIndex,這個值,因為要向左一個切換濾鏡,那麼curIndex需要-1.   

private void decreaseCurIndex() {
        curIndex--;
        if (curIndex < 0) {
            curIndex = types.length - 1;
        }
    }
           

      然後就銷毀rightFilter。如果把目前的curFilter指派給rightFilter,目前的curFilter設定為之前的leftFilter,然後再通過getFilter向左一個擷取到新的leftFilter。如此就完成了一次濾鏡的切換。那麼同樣的如果需要往右切換filter的話,那肯定有一個類似的方法。    

private void reCreateLeftFilter() {
        increaseCurIndex();
        leftFilter.destroy();
        leftFilter = curFilter;
        curFilter = rightFilter;
        rightFilter = getFilter(getRightIndex());
        rightFilter.init();
        rightFilter.onDisplaySizeChanged(width, height);
        rightFilter.onInputSizeChanged(width, height);
        needSwitch = false;
    }
           

        這就是核心的onDrawFrame函數,然後在解綁掉幀緩沖和紋理。

EasyGlUtils.unBindFrameBuffer();
           

        這個類大部分的代碼就是這樣,最後剩下最重要的函數就是對外提供的onTouchEvent函數

public void onTouchEvent(MotionEvent event) {
        if (locked) {
            return;
        }
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                downX = (int) event.getX();
                break;
            case MotionEvent.ACTION_MOVE:
                if (downX == -1) {
                    return;
                }
                int curX = (int) event.getX();
                if (curX > downX) {
                    direction = 1;
                } else {
                    direction = -1;
                }
                offset = Math.abs(curX - downX);
                break;
            case MotionEvent.ACTION_UP:
                if (downX == -1) {
                    return;
                }
                if (offset == 0) {
                    return;
                }
                locked = true;
                downX = -1;
                if (offset > Constants.screenWidth / 3) {
                    scroller.startScroll(offset, 0, Constants.screenWidth - offset, 0, 100 * (1 - offset / Constants.screenWidth));
                    needSwitch = true;
                } else {
                    scroller.startScroll(offset, 0, -offset, 0, 100 * (offset / Constants.screenWidth));
                    needSwitch = false;
                }
                break;
        }
    }
           

        這個函數,基本上就是在down事件的時候,擷取到X值,然後在move事件的時候,就根據目前的X和down時候的X值做對比的時候,判斷是像左滑還是右滑。然後在up事件的時候,将偏移量offset和螢幕寬度進行比較,判斷是否需要切換濾鏡。        所有的邏輯,基本上就是這樣,讓我們來看他一下最後的實作效果

Android視訊編輯器(四)通過OpenGL給視訊增加不同濾鏡效果前言結語

結語

      到這裡的話呢,本篇文章就已經要結束了,我們這篇部落客要就是實作了給視訊更換不同的濾鏡效果,并且實作了一種很友善的切換濾鏡的方式。至于本地視訊添加切換不同濾鏡效果,原理是相同的,這裡就不多講了,相關代碼我都已經上傳到了我的github上面。請大家多多star。Thank you。       然後,按照預定的計劃,我們下篇文章就會說到android平台音頻的硬解碼,單聲道(mono)轉立體聲(stero),兩個音頻的混音,音頻原始音量調節這四個方面的知識。基本上也就涵蓋了android平台的視訊編輯器關于音頻的大部分操作,當然有朋友可能會說到變聲,這個的話,如果後面有時間,我再寫篇部落格專門介紹和實作以下android平台的音頻的變聲。      因為個人水準有限,難免有錯誤和不足之處,還望大家能包涵和提醒。謝謝啦!!!

其他      項目的github位址,麻煩順手給個star,謝謝啦~        VideoEditor-For-Android

繼續閱讀