GPUImage 詳解與框架源碼分析

一丹弱、前言

這篇文章咱們來看一下cats-ossandroid-gpuimage喊积。根據(jù)作者自己的解釋俏让,該項目的創(chuàng)意來自于IOS GPUImage笔咽。而GPU Image 的作用是利用 OpenGL 幫助我們實現(xiàn)圖片初級處理,像高斯模糊蔫巩,亮度谆棱,飽和度,白平衡等一些基礎(chǔ)的濾鏡圆仔。另外垃瞧,GPU Image 幫助我們搭建好了一個框架,使得我們可以忽略使用 Open GL 過程中的各種繁鎖的步驟坪郭,我們只要專注于自己的業(yè)務(wù)个从,通過繼承 GPUImageFilter 或者組合其他的 Filter 就可以實現(xiàn)我們自己需要的功能。例如應(yīng)用于人像美容處理的美顏,磨皮嗦锐,美白等功能嫌松。那么,先來看看效果圖吧奕污。

原圖
Invert濾鏡

當(dāng)然萎羔,受限于作者的水平以及精力,文章不會對算法的細節(jié)進行分析碳默,而主要就是分析框架本身的架構(gòu)以及邏輯贾陷。

二、基本應(yīng)用

這里主要是對官文的一個簡讀嘱根。

1.依賴

當(dāng)前的最新版本是 2.0.3

repositories {
    jcenter()
}

dependencies {
    implementation 'jp.co.cyberagent.android:gpuimage:2.0.3'
}

2.帶預(yù)覽界面

一般可以結(jié)合相機一起使用髓废,以實現(xiàn)實時濾鏡功能

@Override
public void onCreate(final Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity);

    Uri imageUri = ...;
    gpuImage = new GPUImage(this);
    gpuImage.setGLSurfaceView((GLSurfaceView) findViewById(R.id.surfaceView));
    // this loads image on the current thread, should be run in a thread
    gpuImage.setImage(imageUri); 
    gpuImage.setFilter(new GPUImageSepiaFilter());

    // Later when image should be saved saved:
    gpuImage.saveToPictures("GPUImage", "ImageWithFilter.jpg", null);
}

3.使用GPUImageView

GPUImageView 繼承自 FrameLayout,其他就主要就是個幫助類儿子,幫助我們集成使用 GpuImageFilter 和 SurfaceView/TextureView
xml

<jp.co.cyberagent.android.gpuimage.GPUImageView
    android:id="@+id/gpuimageview"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:gpuimage_show_loading="false"
    app:gpuimage_surface_type="texture_view" /> <!-- surface_view or texture_view -->

java code

@Override
public void onCreate(final Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity);

    Uri imageUri = ...;
    gpuImageView = findViewById(R.id.gpuimageview);
    gpuImageView.setImage(imageUri); // this loads image on the current thread, should be run in a thread
    gpuImageView.setFilter(new GPUImageSepiaFilter());

    // Later when image should be saved saved:
    gpuImageView.saveToPictures("GPUImage", "ImageWithFilter.jpg", null);
}

4.不帶預(yù)覽界面

和帶預(yù)覽界面是相對的瓦哎,其專業(yè)的名稱是離屏渲染砸喻,后面在分析代碼的時候會再詳情講解

public void onCreate(final Bundle savedInstanceState) {
    public void onCreate(final Bundle savedInstanceState) {
    Uri imageUri = ...;
    gpuImage = new GPUImage(context);
    gpuImage.setFilter(new GPUImageSobelEdgeDetection());
    gpuImage.setImage(imageUri);
    gpuImage.saveToPictures("GPUImage", "ImageWithFilter.jpg", null);
}

OpenGL 原生的使用方式真是十分的啰嗦柔逼,過程繁多。而 Android 官方也沒有出一個好用的 SDK 用以完善生態(tài)割岛,減少開發(fā)者的工作愉适。

三、源碼分析

1.框架概覽

框架圖

image.png

上面是一個從輸入——處理——輸出的角度所繪制的一個框圖癣漆,雖然 GPUImage 所涉及的知識是 OpenGL 等一些較有難度的圖像知識维咸,但其封裝的框架相對來說是比較簡單的。如上圖所示惠爽,輸入可以是一個 Bitmap 或者 一個 YUV 格式(一般是相機原始數(shù)據(jù)格式)的數(shù)據(jù)癌蓖,然后經(jīng)由 GPUImage 模塊中的 GPUImageRender 進行渲染處理,在渲染之前先由 GPUImageFilter 進行處理婚肆,然后才真正渲染到 GLSurfaceView/GLTextureView 上租副,也就是屏幕上〗闲裕或者也可以通過離屏渲染將結(jié)果渲染到 Buffer 中用僧,最后保存到 Bitmap 中。

框架類圖

GPUImage Main.jpg

GPUImage 可以看作是模塊對外的接口赞咙,它封裝了主要的類 GPUImageRenderer及其渲染的一些屬性责循,而 GPUImageFilter 與 GLSurfaceView 均由外部傳入,并與GPUImageRenderer 建立起聯(lián)系攀操。
GPUImageRenderer 其繼承自 Render 類院仿,主要負責(zé)調(diào)用 GPUImageFilter 進行圖像的處理,再渲染到 GLSurfaceView 中速和。而這里所謂的處理意蛀,也就是通常所說的運用一些圖像處理算法耸别,只不過其不是通過 CPU 進行運算而是通過 GPU 進行運算。
GPUImageFilter 是所有 filter 的基類县钥,其默認實現(xiàn)是不帶任何濾鏡效果秀姐。而其子類可以直接繼承自 GPUImageFilter 從而實現(xiàn)單一的濾鏡效果∪糁或者也可以繼承如 GPUImageFilterGroup 實現(xiàn)多個濾鏡的效果省有。而關(guān)于如何組合,可以繼承類圖中如 GPUImage3x3TextureSamplingFilter 實現(xiàn) 3 張圖片紋理采樣的濾鏡效果谴麦。當(dāng)然也可以自己定義組織規(guī)則蠢沿。

通過上面的框架圖和框架類圖,對 GPUImage 應(yīng)該有一個整體的認知了匾效。接下來我們按照帶預(yù)覽界面這個 demo 的流程先來分析一下更細節(jié)的實現(xiàn)原理舷蟀。

2.帶預(yù)覽界面的渲染實現(xiàn)

初始化——構(gòu)建 GPUImage

GPUImage初始化.jpg
/**
     * Instantiates a new GPUImage object.
     *
     * @param context the context
     */
    public GPUImage(final Context context) {
        if (!supportsOpenGLES2(context)) {
            throw new IllegalStateException("OpenGL ES 2.0 is not supported on this phone.");
        }

        this.context = context;
        filter = new GPUImageFilter();
        renderer = new GPUImageRenderer(filter);
    }

GPUImage 的構(gòu)建非常簡單,就是依次構(gòu)建了 GPUImageFilter 和 GPUImageRender面哼。GPUImageFilter 是所有 filter 的基類野宜,它是不帶任何濾鏡效果的。同時它通過定義多個勾子方法來完成初始化魔策,處理以及銷毀的生命周期匈子。如下圖所示。

image.png

而它的構(gòu)造方法也是很簡單的闯袒,就是接收了頂點著色器腳本片元著色器腳本

public GPUImageFilter() {
        this(NO_FILTER_VERTEX_SHADER, NO_FILTER_FRAGMENT_SHADER);
    }

    public GPUImageFilter(final String vertexShader, final String fragmentShader) {
        runOnDraw = new LinkedList<>();
        this.vertexShader = vertexShader;
        this.fragmentShader = fragmentShader;
    }

關(guān)于著色器腳本虎敦,是一種 glsl 語言,風(fēng)格類似于 c 語言政敢,對此感興趣的可以參考一下相關(guān)的wiki其徙。而這兩個著色器的作用分別是 OpenGL 流水線中用于計算頂點位置和給頂點上色的 2 個工序。對于完全沒有接觸過 OpenGL 的同學(xué)可能覺得這里看不明白喷户,先不用著急唾那,這里先有這個概念就可以了。

接著是創(chuàng)建 GPUImageRenderer摩骨,來看看其構(gòu)造方法通贞。

public GPUImageRenderer(final GPUImageFilter filter) {
        // 接收 filter
        this.filter = filter;
        // 創(chuàng)建 2 個任務(wù)隊列
        runOnDraw = new LinkedList<>();
        runOnDrawEnd = new LinkedList<>();
        // 創(chuàng)建頂點 Buffer 并賦值
        glCubeBuffer = ByteBuffer.allocateDirect(CUBE.length * 4)
                .order(ByteOrder.nativeOrder())
                .asFloatBuffer();
        glCubeBuffer.put(CUBE).position(0);
        // 創(chuàng)建紋理 Buffer
        glTextureBuffer = ByteBuffer.allocateDirect(TEXTURE_NO_ROTATION.length * 4)
                .order(ByteOrder.nativeOrder())
                .asFloatBuffer();
        // 設(shè)置旋轉(zhuǎn)方向
        setRotation(Rotation.NORMAL, false, false);
    }

GPUImageRenderer 的構(gòu)造方法主要是構(gòu)建了自己的運行時環(huán)境。其中最主要的是創(chuàng)建頂點 Buffer恼五,創(chuàng)建紋理 Buffer 以及設(shè)置旋轉(zhuǎn)方向昌罩。這里的 Buffer 分配涉及到的是 Java 的 NI/O,其分配置的內(nèi)存空間是在 native 層灾馒。而這里 * 4 是因為 float 占 4 個字節(jié)茎用。

先來看看 CUBE 的定義

public static final float CUBE[] = {
            -1.0f, -1.0f,//左下角坐標(biāo)
            1.0f, -1.0f,//右下角坐標(biāo)
            -1.0f, 1.0f,//左上角坐標(biāo)
            1.0f, 1.0f,//右上角坐標(biāo)
    };

這不是一堆沒有意義的數(shù)字,這里其實是定義了一個 2 * 4 的頂點數(shù)組,2 代表是 2 維的轨功,即 2 維坐標(biāo)系中的某個點 (x,y)旭斥;而 4 則代表是有 4 個頂點。再來看看這些數(shù)字的值古涧,它們都在 -1 到 1 之間垂券。這個就是與 OpenGL 中的眾多坐標(biāo)系相關(guān)了。OpenGL 的坐標(biāo)系是 3 維的羡滑,它是以原點(0,0,0) 為中心菇爪,并有 3 個不同的方向 (x,y,z) 軸所組成的。這里所定義的頂點中柒昏,沒有 z 坐標(biāo)凳宙,即深度為 0。而之所以是在 -1 到 1 之間职祷,是因為被歸一化了氏涩。OpenGL 在流水線中,在最后做 NDC 運算后有梆,會將所有的坐標(biāo)都映射到 -1 到 1 之間是尖。 如下是一個常見的 3 維坐標(biāo)系。


image.png

而我們的這里定義的數(shù)字可以看成如下坐標(biāo)系淳梦。


image.png

最終我們會拿這 4 個頂點來構(gòu)造出 2 個三角形析砸,從而形成一個面昔字。在這個形成的面上爆袍,會將圖片以紋理的形式貼在這個區(qū)域上。

再來看看紋理坐標(biāo) TEXTURE_NO_ROTATION 以及其他旋轉(zhuǎn)角度的定義

public static final float TEXTURE_NO_ROTATION[] = {
            0.0f, 1.0f,
            1.0f, 1.0f,
            0.0f, 0.0f,
            1.0f, 0.0f,
    };

    public static final float TEXTURE_ROTATED_90[] = {
            1.0f, 1.0f,
            1.0f, 0.0f,
            0.0f, 1.0f,
            0.0f, 0.0f,
    };
    public static final float TEXTURE_ROTATED_180[] = {
            1.0f, 0.0f,
            0.0f, 0.0f,
            1.0f, 1.0f,
            0.0f, 1.0f,
    };
    public static final float TEXTURE_ROTATED_270[] = {
            0.0f, 0.0f,
            0.0f, 1.0f,
            1.0f, 0.0f,
            1.0f, 1.0f,
    };

紋理坐又是另一個坐標(biāo)系作郭,即紋理坐標(biāo)系陨囊。我們熟悉的是 Android 的屏幕坐標(biāo)系原點是在左上角的,而紋理坐標(biāo)系的原點是以紋理的左下角為原點夹攒。并且是在 0 到 1 之間蜘醋。而不管原來的圖片寬高為多少,所有的坐標(biāo)都會被映射成 0 到 1 之間的數(shù)值咏尝。對比一下如下紋理坐標(biāo)系压语。當(dāng)不進行任何旋轉(zhuǎn)時,那么得到的坐標(biāo)就是 TEXTURE_NO_ROTATION编检,而當(dāng)作逆時針旋轉(zhuǎn) 90 度時胎食,得到的就是 TEXTURE_ROTATED_90。另外 2 個同理允懂。


image.png

OpenGL 中的坐標(biāo)系比較多厕怜,短短幾句是講不清楚的。這里只是根據(jù)坐標(biāo)系的規(guī)則簡單的描述了頂點和紋理坐標(biāo)這些數(shù)值的由來。只做適當(dāng)展開粥航,不作詳細深究琅捏。后面有機會會再專門進行 OpenGL 坐標(biāo)系的講解。

接著往下看 setRotation()递雀,其還有另外 2 個參數(shù)代表是否要進行橫向和眾向的翻轉(zhuǎn)柄延,這與相機的角度和成像原理有關(guān)系,這里先不深入缀程±狗伲看看其進一步調(diào)用的 adjustImageScaling()

private void adjustImageScaling() {
        float outputWidth = this.outputWidth;
        float outputHeight = this.outputHeight;
        // 豎屏情況下
        if (rotation == Rotation.ROTATION_270 || rotation == Rotation.ROTATION_90) {
            outputWidth = this.outputHeight;
            outputHeight = this.outputWidth;
        }
        // 這里相當(dāng)于是把圖片根據(jù)視口大小(簡單理解為 GLSurfaceView的大小)進行比例縮放
        float ratio1 = outputWidth / imageWidth;
        float ratio2 = outputHeight / imageHeight;
        float ratioMax = Math.max(ratio1, ratio2);
        int imageWidthNew = Math.round(imageWidth * ratioMax);
        int imageHeightNew = Math.round(imageHeight * ratioMax);

        float ratioWidth = imageWidthNew / outputWidth;
        float ratioHeight = imageHeightNew / outputHeight;
        // 獲取頂點數(shù)據(jù)
        float[] cube = CUBE;
        // 獲取對應(yīng)角度的紋理坐標(biāo),并根據(jù)翻轉(zhuǎn)參數(shù)進行相應(yīng)的翻轉(zhuǎn)
        float[] textureCords = TextureRotationUtil.getRotation(rotation, flipHorizontal, flipVertical);
        // 根據(jù) scaleType 對紋理坐標(biāo)或者頂點坐標(biāo)進行計算
        if (scaleType == GPUImage.ScaleType.CENTER_CROP) {
            float distHorizontal = (1 - 1 / ratioWidth) / 2;
            float distVertical = (1 - 1 / ratioHeight) / 2;
            textureCords = new float[]{
                    addDistance(textureCords[0], distHorizontal), addDistance(textureCords[1], distVertical),
                    addDistance(textureCords[2], distHorizontal), addDistance(textureCords[3], distVertical),
                    addDistance(textureCords[4], distHorizontal), addDistance(textureCords[5], distVertical),
                    addDistance(textureCords[6], distHorizontal), addDistance(textureCords[7], distVertical),
            };
        } else {
            cube = new float[]{
                    CUBE[0] / ratioHeight, CUBE[1] / ratioWidth,
                    CUBE[2] / ratioHeight, CUBE[3] / ratioWidth,
                    CUBE[4] / ratioHeight, CUBE[5] / ratioWidth,
                    CUBE[6] / ratioHeight, CUBE[7] / ratioWidth,
            };
        }
       // 最后把頂點坐標(biāo)和紋理坐標(biāo)送到相應(yīng)的 buffer 中
        glCubeBuffer.clear();
        glCubeBuffer.put(cube).position(0);
        glTextureBuffer.clear();
        glTextureBuffer.put(textureCords).position(0);
    }

假設(shè)這里的 scaleType 是 CENTER_CROP杠输,并假設(shè)圖片的寬高為 80 * 200赎败,而視口的寬高為 100 * 200,那么得到的效果如下圖所示——注意超出橙色線框外的圖像是不可見的蠢甲,這里只是為了展示效果僵刮。


image.png

如果不是 CENTER_CROP,而是 CENTER_INSIDE鹦牛,那么是改變頂點的位置搞糕。效果圖如下。有興趣的同學(xué)也可以自己仔細的推導(dǎo)一下曼追。


image.png

這里最主的是通過 adjustImageScaling() 方法的計算窍仰,最終確定了頂點坐標(biāo)以及紋理坐標(biāo),并送進了相應(yīng)的 Buffer 礼殊,而這 2 個 Buffer 中的數(shù)字最終會被送到 OpenGL 的流水線中進行渲染驹吮。

建立與GLSurfaceView 的關(guān)聯(lián)——GPUImage#setGLSurfaceView()

/**
     * Sets the GLSurfaceView which will display the preview.
     *
     * @param view the GLSurfaceView
     */
    public void setGLSurfaceView(final GLSurfaceView view) {
        surfaceType = SURFACE_TYPE_SURFACE_VIEW;
        glSurfaceView = view;
        glSurfaceView.setEGLContextClientVersion(2);
        glSurfaceView.setEGLConfigChooser(8, 8, 8, 8, 16, 0);
        glSurfaceView.getHolder().setFormat(PixelFormat.RGBA_8888);
        glSurfaceView.setRenderer(renderer);
        glSurfaceView.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
        glSurfaceView.requestRender();
    }

該方法中設(shè)置了OpenGL 的版本,圖片格式晶伦,刷新模式等碟狞,而最主要的是將 GPUImageRender 設(shè)置給了 GLSurfaceView,在 render 的 onDrawFrame() 勾子方法中將數(shù)據(jù)渲染到GLSurfaceView 的 Buffer 中婚陪。

GLSurfaceView 是 Android 自已定義的族沃,除此之外,框架還定義了一個 GLTextureView泌参,其繼承自 TextureView脆淹。它的主要功能就是在模仿 GLSurfaceView,創(chuàng)建一個GLThread然后不斷回調(diào) render 的 onDrawFrame() 沽一,從而達到不斷刷新 View 的目的盖溺。關(guān)于SurfaceView 和 TextureView,這里稍微展開一下锯玛,有興趣的可以了解一下咐柜,不感興趣的也可以跳過:

SurfaceView 是一個有自己獨立Surface的View兼蜈,它的渲染可以放在單獨線程而不是主線程中。作為一個 view 在App 進程中它也是在 view hierachy 中的拙友,但在系統(tǒng)的 WindowManagerService 以及 SurfaceFlinger 中为狸,它是有自己的 WindowState 和 Surface 的,簡單理解就是有自己的畫布——Buffer遗契。因為它是不作變形和動畫的辐棒。
TextureView 跟普通的View一樣,在App進程中和系統(tǒng)的 WindowManagerService 以及 SurfaceFlinger 中都同屬一個 view hierachy牍蜂、widnowstate 和 surface漾根。由 4.0 引入,早期還是靠主線程來渲染鲫竞,在 5.0 之后加入了渲染線程辐怕,才由渲染線程來專門渲染。當(dāng)然从绘,和普通 View 一樣寄疏,它是支持變形和動畫的。另外僵井,還有更重要的一點是陕截,它必須在支持硬件加速的 window 中進行渲染,否則就會是一片空白批什。

最后的 glSurfaceView.requestRender() 會喚醒線程進行后續(xù)的渲染农曲。

設(shè)置/更新圖片源——GPUImage#setImage()/updatePreviewFrame()

設(shè)置圖片源,可以是直接設(shè)置一個圖片驻债,圖片可以是 bitmap乳规,文件或者 URI。而其更常用的一個場景是相機的預(yù)覽幀——YUV原始數(shù)據(jù)却汉。當(dāng)然驯妄,YUV數(shù)據(jù)也要轉(zhuǎn)成通常所使用的 RGB 數(shù)據(jù)才能交給 Render 對其進行渲染荷并。關(guān)于 YUV 請參考YUV 數(shù)據(jù)格式詳解Video Rendering with 8-Bit YUV Formats合砂。也可以看看下圖直觀的感受一下,“Y”表示明亮度(Luminance源织、Luma)翩伪,“U”和“V”則是色度、濃度(Chrominance谈息、Chroma)

image.png

不管是直接設(shè)置圖片缘屹,還是原始YUV數(shù)據(jù),都要將其綁定到 OpenGL 中的紋理 ID 中去侠仇。以 onPreviewFrame 來看一看轻姿。

public void onPreviewFrame(final byte[] data, final int width, final int height) {
        if (glRgbBuffer == null) {
            glRgbBuffer = IntBuffer.allocate(width * height);
        }
        if (runOnDraw.isEmpty()) {
            runOnDraw(new Runnable() {
                @Override
                public void run() {
                    // YUV 轉(zhuǎn) RGB
                    GPUImageNativeLibrary.YUVtoRBGA(data, width, height, glRgbBuffer.array());
                    // 加載紋理
                    glTextureId = OpenGlUtils.loadTexture(glRgbBuffer, width, height, glTextureId);

                    if (imageWidth != width) {
                        imageWidth = width;
                        imageHeight = height;
                        adjustImageScaling();
                    }
                }
            });
        }
    }

GPUImageNativeLibrary.YUVtoRBGA() 就不看了犁珠,來看一看 OpenGlUtils.loadTexture()。

public static int loadTexture(final IntBuffer data, final int width, final int height, final int usedTexId) {
        int textures[] = new int[1];
        if (usedTexId == NO_TEXTURE) {
            // 產(chǎn)生紋理 ID 數(shù)組互亮,這里采樣器只有一個犁享,因此 1 個元素就夠了
            GLES20.glGenTextures(1, textures, 0);
            // 綁定紋理采樣器到紋理 ID
            GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textures[0]);
           // 設(shè)定采樣的方式
            GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D,
                    GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
            GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D,
                    GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);
            GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D,
                    GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);
            GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D,
                    GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);
            // 將圖片 buffer 送進 OpenGL 的紋理采樣器中
            GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_RGBA, width, height,
                    0, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, data);
        } else {
            GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, usedTexId);
            GLES20.glTexSubImage2D(GLES20.GL_TEXTURE_2D, 0, 0, 0, width,
                    height, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, data);
            textures[0] = usedTexId;
        }
        return textures[0];
    }

這個方法的其他細節(jié)請參考注釋即可。通過這個方法的主要目的就是將圖片送進 OpenGL 的 sample2D 采樣器中豹休,此 Sample2D 采樣器是在 片元 Shader 腳本中定義的炊昆。如下定義中的 inputImageTexture。

public static final String NO_FILTER_FRAGMENT_SHADER = "" +
            "varying highp vec2 textureCoordinate;\n" +
            " \n" +
            "uniform sampler2D inputImageTexture;\n" +
            " \n" +
            "void main()\n" +
            "{\n" +
            "     gl_FragColor = texture2D(inputImageTexture, textureCoordinate);\n" +
            "}";

設(shè)置 Filter——GPUImage#setImageFilter()

/**
     * Sets the filter which should be applied to the image which was (or will
     * be) set by setImage(...).
     *
     * @param filter the new filter
     */
    public void setFilter(final GPUImageFilter filter) {
        this.filter = filter;
        renderer.setFilter(this.filter);
        requestRender();
    }

調(diào)用了 render 的 setFilter威根,并再次發(fā)起渲染請求凤巨。來進一步看看。

public void setFilter(final GPUImageFilter filter) {
        runOnDraw(new Runnable() {

            @Override
            public void run() {
                final GPUImageFilter oldFilter = GPUImageRenderer.this.filter;
                GPUImageRenderer.this.filter = filter;
               // 如果存在有舊的 filter洛搀,則先銷毀
                if (oldFilter != null) {
                    oldFilter.destroy();
                }
                // 然后調(diào)用 fiter.ifNeedInit() 進行初始化
                GPUImageRenderer.this.filter.ifNeedInit();
                // 設(shè)置 OpenGL 上下文所使用的程序 ID
                GLES20.glUseProgram(GPUImageRenderer.this.filter.getProgram());
                // 更新視口大小
                GPUImageRenderer.this.filter.onOutputSizeChanged(outputWidth, outputHeight);
            }
        });
    }

這里的主要過程就對應(yīng)了前面 GPUImageFilter 生命周期的流程圖敢茁。其首先判斷是否有舊的 filter,如果有則先銷毀留美。銷毀很簡單卷要,主要就是通過 GLES20.glDeleteProgram(glProgId) 銷毀 OpenGL 當(dāng)前運行的程序 ID,然后再通過勾子方法 onDestroy() 通知 GPUImageFilter 的子類釋放其他所用到的資源独榴。這里重點需要了解一下的是其初始化的過程僧叉。

ififNeedInit() 主要就是調(diào)用了 onInit()

public void onInit() {
        glProgId = OpenGlUtils.loadProgram(vertexShader, fragmentShader);
        glAttribPosition = GLES20.glGetAttribLocation(glProgId, "position");
        glUniformTexture = GLES20.glGetUniformLocation(glProgId, "inputImageTexture");
        glAttribTextureCoordinate = GLES20.glGetAttribLocation(glProgId, "inputTextureCoordinate");
        isInitialized = true;
    }

創(chuàng)建程序ID,獲取 頂點位置屬性 "position",紋理坐標(biāo)屬性"inputTextureCoordinate"棺榔,統(tǒng)一變量"inputImageTexture"瓶堕。這里主要是 loadProgram() 需要說一下,其主要完成的功能便是加載頂點以及片元著色器症歇,然后創(chuàng)建程序郎笆,附加著色器,最后鏈接程序忘晤。這些過程都是 OpenGL 編程過程中所必須經(jīng)歷的步驟宛蚓,這里只稍做了解即可。為了文章的完整性设塔,這里也將相關(guān)的代碼貼出來凄吏。

public static int loadProgram(final String strVSource, final String strFSource) {
        int iVShader;
        int iFShader;
        int iProgId;
        int[] link = new int[1];
        iVShader = loadShader(strVSource, GLES20.GL_VERTEX_SHADER);
        if (iVShader == 0) {
            Log.d("Load Program", "Vertex Shader Failed");
            return 0;
        }
        iFShader = loadShader(strFSource, GLES20.GL_FRAGMENT_SHADER);
        if (iFShader == 0) {
            Log.d("Load Program", "Fragment Shader Failed");
            return 0;
        }

        iProgId = GLES20.glCreateProgram();

        GLES20.glAttachShader(iProgId, iVShader);
        GLES20.glAttachShader(iProgId, iFShader);

        GLES20.glLinkProgram(iProgId);

        GLES20.glGetProgramiv(iProgId, GLES20.GL_LINK_STATUS, link, 0);
        if (link[0] <= 0) {
            Log.d("Load Program", "Linking Failed");
            return 0;
        }
        GLES20.glDeleteShader(iVShader);
        GLES20.glDeleteShader(iFShader);
        return iProgId;
    }
public static int loadShader(final String strSource, final int iType) {
        int[] compiled = new int[1];
        int iShader = GLES20.glCreateShader(iType);
        GLES20.glShaderSource(iShader, strSource);
        GLES20.glCompileShader(iShader);
        GLES20.glGetShaderiv(iShader, GLES20.GL_COMPILE_STATUS, compiled, 0);
        if (compiled[0] == 0) {
            Log.d("Load Shader Failed", "Compilation\n" + GLES20.glGetShaderInfoLog(iShader));
            return 0;
        }
        return iShader;
    }

至此,可以說用來渲染圖片的環(huán)境是已經(jīng)建立好了闰蛔。如確定頂點坐標(biāo)痕钢,縮放方式,建立OpenGL的渲染環(huán)境等等序六。下面就看如何繪制出來了任连。

渲染——渲染 Filter

前面在介紹 Render 的時候有講過,GLSurfaceView 就是通過 GLThread 不斷回調(diào) render 的勾子方法 onDrawFrame() 來達到刷新 view 的目的例诀。那么我們來看看 GPUImageRenderer 的 onDrawFrame()随抠。

@Override
    public void onDrawFrame(final GL10 gl) {
        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT);
        runAll(runOnDraw);
        filter.onDraw(glTextureId, glCubeBuffer, glTextureBuffer);
        runAll(runOnDrawEnd);
        if (surfaceTexture != null) {
            surfaceTexture.updateTexImage();
        }
    }

其實應(yīng)該能想得到裁着,其最主要的就是通過調(diào)用 filter 的 onDraw() 進行渲染。

public void onDraw(final int textureId, final FloatBuffer cubeBuffer,
                       final FloatBuffer textureBuffer) {
        // 激程序 ID
        GLES20.glUseProgram(glProgId);
        runPendingOnDrawTasks();
        if (!isInitialized) {
            return;
        }
       // 將頂點 buffer 的數(shù)據(jù)送給屬性 "position"拱她,并使能屬性
        cubeBuffer.position(0);
       // 下面的 2 表示每個點的 size 大小跨算,即這里的一個坐標(biāo)只需要取 2 個表示 (x,y) 即可。如果為 3 則表示 (x,y,z)
        GLES20.glVertexAttribPointer(glAttribPosition, 2, GLES20.GL_FLOAT, false, 0, cubeBuffer);
        GLES20.glEnableVertexAttribArray(glAttribPosition);
        // 將紋理坐標(biāo) buffer 的數(shù)據(jù)送給屬性 "inputTextureCoordinate"椭懊,并使能屬性
        textureBuffer.position(0);
        GLES20.glVertexAttribPointer(glAttribTextureCoordinate, 2, GLES20.GL_FLOAT, false, 0,
                textureBuffer);
        GLES20.glEnableVertexAttribArray(glAttribTextureCoordinate);
        if (textureId != OpenGlUtils.NO_TEXTURE) {
            // 激活诸蚕,綁定紋理,并指定采樣器 "inputImageTexture" 為 0 號紋理
            GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
            GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId);
            GLES20.glUniform1i(glUniformTexture, 0);
        }
        onDrawArraysPre();
        // 繪制 3 角形
        GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
        GLES20.glDisableVertexAttribArray(glAttribPosition);
        GLES20.glDisableVertexAttribArray(glAttribTextureCoordinate);
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0);
    }

渲染的過程就是 OpenGL 方法的一些調(diào)用氧猬,其中的意思也都在代碼里增加了注釋說明背犯。其子類 Filter 也都采用這個 onDraw() 進行繪制。而決定每個 filter 渲染出什么樣的濾鏡效果就都在其定義的頂點著色器和片元著色器里了盅抚。

至此漠魏,將圖片經(jīng) GPUImageFilter 渲染到 GLSurfaceView 上的過程已經(jīng)分析完了。如前面所說妄均,有了 GPUImage 這個框架柱锹,就不需要我們?nèi)ヌ幚?OpenGL 里面的各種繁瑣的細節(jié)了。一般的丰包,我們只需要寫好我們自己的著色器禁熏,剩下的就都可以交給 GPUImage 來完成了。

3.離屏渲染

所謂離屏渲染邑彪,就是將Render渲染出來的圖片不送進 GLSurfaceView瞧毙,而保存在特定的 Buffer 中。下面看看它的時序圖寄症。


離屏渲染.jpg

初始化離屏渲染的環(huán)境

其中的 1 - 4 步比較簡單宙彪,就不展開了。從 getBitmapWithFilterApplied () 開始有巧。

/**
     * Gets the given bitmap with current filter applied as a Bitmap.
     *
     * @param bitmap  the bitmap on which the current filter should be applied
     * @param recycle recycle the bitmap or not.
     * @return the bitmap with filter applied
     */
    public Bitmap getBitmapWithFilterApplied(final Bitmap bitmap, boolean recycle) {
        ......
        GPUImageRenderer renderer = new GPUImageRenderer(filter);
        renderer.setRotation(Rotation.NORMAL,
                this.renderer.isFlippedHorizontally(), this.renderer.isFlippedVertically());
        renderer.setScaleType(scaleType);
        PixelBuffer buffer = new PixelBuffer(bitmap.getWidth(), bitmap.getHeight());
        buffer.setRenderer(renderer);
        renderer.setImageBitmap(bitmap, recycle);
        Bitmap result = buffer.getBitmap();
        filter.destroy();
        renderer.deleteImage();
        buffer.destroy();

        this.renderer.setFilter(filter);
        if (currentBitmap != null) {
            this.renderer.setImageBitmap(currentBitmap, false);
        }
        requestRender();

        return result;
    }

省略的部分與 GLSurfaceView 相關(guān)释漆,主要主是銷毀的相關(guān)工作。構(gòu)造 GPUImageRenderer 前面也分析過了篮迎。這里主要只分析 PixelBuffer 相關(guān)的調(diào)用男图。首先看看其構(gòu)造函數(shù)。

public PixelBuffer(final int width, final int height) {
        this.width = width;
        this.height = height;

        int[] version = new int[2];
        int[] attribList = new int[]{
                EGL_WIDTH, this.width,
                EGL_HEIGHT, this.height,
                EGL_NONE
        };

        // No error checking performed, minimum required code to elucidate logic
        // 創(chuàng)建 egl  
        egl10 = (EGL10) EGLContext.getEGL();
        // 獲取 default_display  
        eglDisplay = egl10.eglGetDisplay(EGL_DEFAULT_DISPLAY);
        egl10.eglInitialize(eglDisplay, version);
        eglConfig = chooseConfig(); // Choosing a config is a little more
  
        int EGL_CONTEXT_CLIENT_VERSION = 0x3098;
        int[] attrib_list = {
                EGL_CONTEXT_CLIENT_VERSION, 2,
                EGL10.EGL_NONE
        };
        // 創(chuàng)建上下文
        eglContext = egl10.eglCreateContext(eglDisplay, eglConfig, EGL_NO_CONTEXT, attrib_list);
        // 在顯存中開辟一個 Buffer柑潦,渲染后的圖片將存放在這里
        eglSurface = egl10.eglCreatePbufferSurface(eglDisplay, eglConfig, attribList);
        egl10.eglMakeCurrent(eglDisplay, eglSurface, eglSurface, eglContext);

        gl10 = (GL10) eglContext.getGL();

        // Record thread owner of OpenGL context
        mThreadOwner = Thread.currentThread().getName();
    }

關(guān)鍵過程在注釋中都有添加享言。這里主要關(guān)注的是 eglCreatePbufferSurface() 的調(diào)用,其主要作用就是在顯存中開辟一個 buffer渗鬼,并不關(guān)聯(lián)任何屏幕上的 window。那與之對應(yīng)的 GLSurfaceView 是否有在屏幕的 window 上開辟一個 buffer 呢荧琼。

public EGLSurface createWindowSurface(EGL10 egl, EGLDisplay display,
                EGLConfig config, Object nativeWindow) {
            EGLSurface result = null;
            try {
                result = egl.eglCreateWindowSurface(display, config, nativeWindow, null);
            } catch (IllegalArgumentException e) {
                ......
            }
            return result;
        }

如 GLSurfaceView 中創(chuàng)建 EGLSurface 的代碼所示譬胎,果然是有的差牛,只不過它調(diào)用的是另一個方法 eglCreateWindowSurface()。這里所傳的參數(shù)里需要注意的是nativeWindow堰乔,它其實就是 SurfaceHolder偏化。

到這里也就創(chuàng)建好了離屏渲染所需要的環(huán)境,接著與之前一樣镐侯,給 GPUImageRenderer 設(shè)置圖片以及 Filter 并作好相關(guān)渲染準(zhǔn)備侦讨。

獲取渲染結(jié)果

先調(diào)用 getBitmap() ,該方法中會進一步調(diào)用 render 的 onDrawFrame苟翻,從而使得圖片按照 filter 所希望的效果將圖片渲染到 PixelBuffer 中所創(chuàng)建的 EGLSurface 中韵卤。然后調(diào)用 convertToBitmap() 方法將EGLSurface 中的 buffer 中的內(nèi)容轉(zhuǎn)換成 bitmap。

convertToBitmap() 只是一個簡單的調(diào)用崇猫,其進一步調(diào)用了 native 函數(shù) GPUImageNativeLibrary.adjustBitmap(bitmap) 來真正執(zhí)行轉(zhuǎn)換的操作沈条。

JNIEXPORT void JNICALL
Java_jp_co_cyberagent_android_gpuimage_GPUImageNativeLibrary_adjustBitmap(JNIEnv *jenv, jclass thiz,
                                                                       jobject src) {
    unsigned char *srcByteBuffer;
    int result = 0;
    int i, j;
    // 聲明一個 AndroidBitmapInfo 結(jié)構(gòu)
    AndroidBitmapInfo srcInfo;
    // 從圖片中獲取 info
    result = AndroidBitmap_getInfo(jenv, src, &srcInfo);
    if (result != ANDROID_BITMAP_RESULT_SUCCESS) {
        return;
    }
    // 將圖片 src 的數(shù)據(jù)指針賦值給 srcByteBuffer
    result = AndroidBitmap_lockPixels(jenv, src, (void **) &srcByteBuffer);
    if (result != ANDROID_BITMAP_RESULT_SUCCESS) {
        return;
    }

    int width = srcInfo.width;
    int height = srcInfo.height;
    // 從當(dāng)前 EGL 運行環(huán)境中讀取圖片數(shù)據(jù)并保存在 srcByteBuffer 中,也就保存到了位圖里面了
    glReadPixels(0, 0, srcInfo.width, srcInfo.height, GL_RGBA, GL_UNSIGNED_BYTE, srcByteBuffer);

    int *pIntBuffer = (int *) srcByteBuffer;
   // OpenGL和Android的Bitmap色彩空間不一致诅炉,這里需要做轉(zhuǎn)換蜡歹。以中間為基線進行對調(diào)。
    for (i = 0; i < height / 2; i++) {
        for (j = 0; j < width; j++) {
            int temp = pIntBuffer[(height - i - 1) * width + j];
            pIntBuffer[(height - i - 1) * width + j] = pIntBuffer[i * width + j];
            pIntBuffer[i * width + j] = temp;
        }
    }
    AndroidBitmap_unlockPixels(jenv, src);
}

這段代碼可能有些是似曾相識的涕烧。當(dāng)我們在完成截屏功能時月而,如果碰到有 video 的時候,截出來是黑的议纯。有很多大神提供實現(xiàn)工具景鼠,而其內(nèi)部的原理就是這個,即讀取當(dāng)前上下文的 buffer 中的圖片數(shù)據(jù)痹扇,然后保存到 bitmap 或者 創(chuàng)建 bitmap铛漓。由于在 OpenGL 的 buffer 中其順序是 左上 到 右下,而圖片紋理的順序是 左下 到 右上鲫构。因此需要以中間為基準(zhǔn)將數(shù)據(jù)進行對調(diào)浓恶。

以上,便是離屏渲染的大致分析结笨。

四贩虾、后記

同樣感謝你能讀到此文章,也希望你能有所收獲憔四。當(dāng)然背亥,對于 GPUImage 的分析與閱讀需要有一定的 OpenGL 的基礎(chǔ),不然會覺得里面的概念繁多而且也比較抽象赫模。另外树肃,文章主要只是分析了 GPUImage 使用 filter 進行界面渲染或者離屏渲染過程的一個解讀。由于我在圖形圖像領(lǐng)域也只是一個稍微入了門的小菜鳥瀑罗,對于圖像處理算法更是知之甚少胸嘴,所以對于 Filter 的具體算法實現(xiàn)沒有進行分析雏掠。對于文中的分析,如存在錯誤或者有不清楚的地方劣像,也歡迎留言討論乡话,將不勝感激。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末耳奕,一起剝皮案震驚了整個濱河市绑青,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌屋群,老刑警劉巖闸婴,帶你破解...
    沈念sama閱讀 210,914評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異谓晌,居然都是意外死亡掠拳,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,935評論 2 383
  • 文/潘曉璐 我一進店門纸肉,熙熙樓的掌柜王于貴愁眉苦臉地迎上來溺欧,“玉大人,你說我怎么就攤上這事柏肪〗愕螅” “怎么了?”我有些...
    開封第一講書人閱讀 156,531評論 0 345
  • 文/不壞的土叔 我叫張陵烦味,是天一觀的道長聂使。 經(jīng)常有香客問我,道長谬俄,這世上最難降的妖魔是什么柏靶? 我笑而不...
    開封第一講書人閱讀 56,309評論 1 282
  • 正文 為了忘掉前任,我火速辦了婚禮溃论,結(jié)果婚禮上屎蜓,老公的妹妹穿的比我還像新娘。我一直安慰自己钥勋,他們只是感情好炬转,可當(dāng)我...
    茶點故事閱讀 65,381評論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著算灸,像睡著了一般扼劈。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上菲驴,一...
    開封第一講書人閱讀 49,730評論 1 289
  • 那天荐吵,我揣著相機與錄音,去河邊找鬼。 笑死捍靠,一個胖子當(dāng)著我的面吹牛沐旨,可吹牛的內(nèi)容都是我干的森逮。 我是一名探鬼主播榨婆,決...
    沈念sama閱讀 38,882評論 3 404
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼褒侧!你這毒婦竟也來了良风?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,643評論 0 266
  • 序言:老撾萬榮一對情侶失蹤闷供,失蹤者是張志新(化名)和其女友劉穎烟央,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體歪脏,經(jīng)...
    沈念sama閱讀 44,095評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡疑俭,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,448評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了婿失。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片钞艇。...
    茶點故事閱讀 38,566評論 1 339
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖豪硅,靈堂內(nèi)的尸體忽然破棺而出哩照,到底是詐尸還是另有隱情,我是刑警寧澤懒浮,帶...
    沈念sama閱讀 34,253評論 4 328
  • 正文 年R本政府宣布飘弧,位于F島的核電站,受9級特大地震影響砚著,放射性物質(zhì)發(fā)生泄漏次伶。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,829評論 3 312
  • 文/蒙蒙 一稽穆、第九天 我趴在偏房一處隱蔽的房頂上張望冠王。 院中可真熱鬧,春花似錦秧骑、人聲如沸版确。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,715評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽绒疗。三九已至,卻和暖如春骂澄,著一層夾襖步出監(jiān)牢的瞬間吓蘑,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,945評論 1 264
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留磨镶,地道東北人溃蔫。 一個月前我還...
    沈念sama閱讀 46,248評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像琳猫,于是被迫代替她去往敵國和親伟叛。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,440評論 2 348

推薦閱讀更多精彩內(nèi)容