GPUImage 詳解與框架源碼分析

一、前言

這篇文章咱們來看一下cats-oss的android-gpuimage。根據作者自己的解釋,該項目的創意來自於IOS GPUImage。而GPU Image 的作用是利用 OpenGL 幫助我們實現圖片初級處理,像高斯模糊,亮度,飽和度,白平衡等一些基礎的濾鏡。另外,GPU Image 幫助我們搭建好了一個框架,使得我們可以忽略使用 Open GL 過程中的各種繁鎖的步驟,我們只要專注於自己的業務,通過繼承 GPUImageFilter 或者組合其他的 Filter 就可以實現我們自己需要的功能。例如應用於人像美容處理的美顏,磨皮,美白等功能。那麼,先來看看效果圖吧。

GPUImage 詳解與框架源碼分析

原圖


GPUImage 詳解與框架源碼分析

Invert濾鏡


當然,受限於作者的水平以及精力,文章不會對算法的細節進行分析,而主要就是分析框架本身的架構以及邏輯。

二、基本應用

這裡主要是對官文的一個簡讀。

1.依賴

當前的最新版本是 2.0.3

<code>repositories {    jcenter()}dependencies {    implementation 'jp.co.cyberagent.android:gpuimage:2.0.3'}/<code>

2.帶預覽界面

一般可以結合相機一起使用,以實現實時濾鏡功能

<code>@Overridepublic 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);}/<code>

3.使用GPUImageView

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

<code><jp.co.cyberagent.android.gpuimage.gpuimageview> /<code>

java code

<code>@Overridepublic 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);}/<code>

4.不帶預覽界面

和帶預覽界面是相對的,其專業的名稱是離屏渲染,後面在分析代碼的時候會再詳情講解

<code>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);}/<code>

OpenGL 原生的使用方式真是十分的囉嗦,過程繁多。而 Android 官方也沒有出一個好用的 SDK 用以完善生態,減少開發者的工作。

三、源碼分析

1.框架概覽

框架圖

GPUImage 詳解與框架源碼分析

image


上面是一個從輸入——處理——輸出的角度所繪製的一個框圖,雖然 GPUImage 所涉及的知識是 OpenGL 等一些較有難度的圖像知識,但其封裝的框架相對來說是比較簡單的。如上圖所示,輸入可以是一個 Bitmap 或者 一個 YUV 格式(一般是相機原始數據格式)的數據,然後經由 GPUImage 模塊中的 GPUImageRender 進行渲染處理,在渲染之前先由 GPUImageFilter 進行處理,然後才真正渲染到 GLSurfaceView/GLTextureView 上,也就是屏幕上。或者也可以通過離屏渲染將結果渲染到 Buffer 中,最後保存到 Bitmap 中。

框架類圖

GPUImage 詳解與框架源碼分析

GPUImage Main.jpg



GPUImage 可以看作是模塊對外的接口,它封裝了主要的類 GPUImageRenderer及其渲染的一些屬性,而 GPUImageFilter 與 GLSurfaceView 均由外部傳入,並與GPUImageRenderer 建立起聯繫。
GPUImageRenderer 其繼承自 Render 類,主要負責調用 GPUImageFilter 進行圖像的處理,再渲染到 GLSurfaceView 中。而這裡所謂的處理,也就是通常所說的運用一些圖像處理算法,只不過其不是通過 CPU 進行運算而是通過 GPU 進行運算。
GPUImageFilter 是所有 filter 的基類,其默認實現是不帶任何濾鏡效果。而其子類可以直接繼承自 GPUImageFilter 從而實現單一的濾鏡效果。或者也可以繼承如 GPUImageFilterGroup 實現多個濾鏡的效果。而關於如何組合,可以繼承類圖中如 GPUImage3x3TextureSamplingFilter 實現 3 張圖片紋理採樣的濾鏡效果。當然也可以自己定義組織規則。


通過上面的框架圖和框架類圖,對 GPUImage 應該有一個整體的認知了。接下來我們按照帶預覽界面這個 demo 的流程先來分析一下更細節的實現原理。

2.帶預覽界面的渲染實現

初始化——構建 GPUImage

GPUImage 詳解與框架源碼分析

GPUImage初始化.jpg


<code>/**     * 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);    }/<code>

GPUImage 的構建非常簡單,就是依次構建了 GPUImageFilter 和 GPUImageRender。GPUImageFilter 是所有 filter 的基類,它是不帶任何濾鏡效果的。同時它通過定義多個勾子方法來完成初始化,處理以及銷燬的生命週期。如下圖所示。

GPUImage 詳解與框架源碼分析

image.png



而它的構造方法也是很簡單的,就是接收了頂點著色器腳本片元著色器腳本


<code>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;    }/<code>

關於著色器腳本,是一種 glsl 語言,風格類似於 c 語言,對此感興趣的可以參考一下相關的wiki。而這兩個著色器的作用分別是 OpenGL 流水線中用於計算頂點位置和給頂點上色的 2 個工序。對於完全沒有接觸過 OpenGL 的同學可能覺得這裡看不明白,先不用著急,這裡先有這個概念就可以了。

接著是創建 GPUImageRenderer,來看看其構造方法。

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

GPUImageRenderer 的構造方法主要是構建了自己的運行時環境。其中最主要的是創建頂點 Buffer,創建紋理 Buffer 以及設置旋轉方向。這裡的 Buffer 分配涉及到的是 Java 的 NI/O,其分配置的內存空間是在 native 層。而這裡 * 4 是因為 float 佔 4 個字節。

先來看看 CUBE 的定義

<code>public static final float CUBE[] = {            -1.0f, -1.0f,//左下角座標            1.0f, -1.0f,//右下角座標            -1.0f, 1.0f,//左上角座標            1.0f, 1.0f,//右上角座標    };/<code>

這不是一堆沒有意義的數字,這裡其實是定義了一個 2 * 4 的頂點數組,2 代表是 2 維的,即 2 維座標系中的某個點 (x,y);而 4 則代表是有 4 個頂點。再來看看這些數字的值,它們都在 -1 到 1 之間。這個就是與 OpenGL 中的眾多座標系相關了。OpenGL 的座標系是 3 維的,它是以原點(0,0,0) 為中心,並有 3 個不同的方向 (x,y,z) 軸所組成的。這裡所定義的頂點中,沒有 z 座標,即深度為 0。而之所以是在 -1 到 1 之間,是因為被歸一化了。OpenGL 在流水線中,在最後做 NDC 運算後,會將所有的座標都映射到 -1 到 1 之間。 如下是一個常見的 3 維座標系。


GPUImage 詳解與框架源碼分析

image.png


而我們的這裡定義的數字可以看成如下座標系。


GPUImage 詳解與框架源碼分析

image.png


最終我們會拿這 4 個頂點來構造出 2 個三角形,從而形成一個面。在這個形成的面上,會將圖片以紋理的形式貼在這個區域上。

再來看看紋理座標 TEXTURE_NO_ROTATION 以及其他旋轉角度的定義

<code>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,    };/<code>

紋理坐又是另一個座標系,即紋理座標系。我們熟悉的是 Android 的屏幕座標系原點是在左上角的,而紋理座標系的原點是以紋理的左下角為原點。並且是在 0 到 1 之間。而不管原來的圖片寬高為多少,所有的座標都會被映射成 0 到 1 之間的數值。對比一下如下紋理座標系。當不進行任何旋轉時,那麼得到的座標就是 TEXTURE_NO_ROTATION,而當作逆時針旋轉 90 度時,得到的就是 TEXTURE_ROTATED_90。另外 2 個同理。


GPUImage 詳解與框架源碼分析

image.png


OpenGL 中的座標系比較多,短短几句是講不清楚的。這裡只是根據座標系的規則簡單的描述了頂點和紋理座標這些數值的由來。只做適當展開,不作詳細深究。後面有機會會再專門進行 OpenGL 座標系的講解。

接著往下看 setRotation(),其還有另外 2 個參數代表是否要進行橫向和眾向的翻轉,這與相機的角度和成像原理有關係,這裡先不深入。看看其進一步調用的 adjustImageScaling()

<code>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;        }        // 這裡相當於是把圖片根據視口大小(簡單理解為 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;        // 獲取頂點數據        float[] cube = CUBE;        // 獲取對應角度的紋理座標,並根據翻轉參數進行相應的翻轉        float[] textureCords = TextureRotationUtil.getRotation(rotation, flipHorizontal, flipVertical);        // 根據 scaleType 對紋理座標或者頂點座標進行計算        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,            };        }       // 最後把頂點座標和紋理座標送到相應的 buffer 中        glCubeBuffer.clear();        glCubeBuffer.put(cube).position(0);        glTextureBuffer.clear();        glTextureBuffer.put(textureCords).position(0);    }/<code>

假設這裡的 scaleType 是 CENTER_CROP,並假設圖片的寬高為 80 * 200,而視口的寬高為 100 * 200,那麼得到的效果如下圖所示——注意超出橙色線框外的圖像是不可見的,這裡只是為了展示效果。


GPUImage 詳解與框架源碼分析

image.png


如果不是 CENTER_CROP,而是 CENTER_INSIDE,那麼是改變頂點的位置。效果圖如下。有興趣的同學也可以自己仔細的推導一下。


GPUImage 詳解與框架源碼分析

image.png


這裡最主的是通過 adjustImageScaling() 方法的計算,最終確定了頂點座標以及紋理座標,並送進了相應的 Buffer ,而這 2 個 Buffer 中的數字最終會被送到 OpenGL 的流水線中進行渲染。

建立與GLSurfaceView 的關聯——GPUImage#setGLSurfaceView()

<code>/**     * 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();    }/<code>

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

GLSurfaceView 是 Android 自已定義的,除此之外,框架還定義了一個 GLTextureView,其繼承自 TextureView。它的主要功能就是在模仿 GLSurfaceView,創建一個GLThread然後不斷回調 render 的 onDrawFrame() ,從而達到不斷刷新 View 的目的。關於SurfaceView 和 TextureView,這裡稍微展開一下,有興趣的可以瞭解一下,不感興趣的也可以跳過:

SurfaceView 是一個有自己獨立Surface的View,它的渲染可以放在單獨線程而不是主線程中。作為一個 view 在App 進程中它也是在 view hierachy 中的,但在系統的 WindowManagerService 以及 SurfaceFlinger 中,它是有自己的 WindowState 和 Surface 的,簡單理解就是有自己的畫布——Buffer。因為它是不作變形和動畫的。TextureView 跟普通的View一樣,在App進程中和系統的 WindowManagerService 以及 SurfaceFlinger 中都同屬一個 view hierachy、widnowstate 和 surface。由 4.0 引入,早期還是靠主線程來渲染,在 5.0 之後加入了渲染線程,才由渲染線程來專門渲染。當然,和普通 View 一樣,它是支持變形和動畫的。另外,還有更重要的一點是,它必須在支持硬件加速的 window 中進行渲染,否則就會是一片空白。

最後的 glSurfaceView.requestRender() 會喚醒線程進行後續的渲染。

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

設置圖片源,可以是直接設置一個圖片,圖片可以是 bitmap,文件或者 URI。而其更常用的一個場景是相機的預覽幀——YUV原始數據。當然,YUV數據也要轉成通常所使用的 RGB 數據才能交給 Render 對其進行渲染。關於 YUV 請參考YUV 數據格式詳解和Video Rendering with 8-Bit YUV Formats。也可以看看下圖直觀的感受一下,“Y”表示明亮度(Luminance、Luma),“U”和“V”則是色度、濃度(Chrominance、Chroma)

GPUImage 詳解與框架源碼分析

image.png


不管是直接設置圖片,還是原始YUV數據,都要將其綁定到 OpenGL 中的紋理 ID 中去。以 onPreviewFrame 來看一看。

<code>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 轉 RGB                    GPUImageNativeLibrary.YUVtoRBGA(data, width, height, glRgbBuffer.array());                    // 加載紋理                    glTextureId = OpenGlUtils.loadTexture(glRgbBuffer, width, height, glTextureId);                    if (imageWidth != width) {                        imageWidth = width;                        imageHeight = height;                        adjustImageScaling();                    }                }            });        }    }/<code>

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

<code>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) {            // 產生紋理 ID 數組,這裡採樣器只有一個,因此 1 個元素就夠了            GLES20.glGenTextures(1, textures, 0);            // 綁定紋理採樣器到紋理 ID            GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textures[0]);           // 設定採樣的方式            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];    }/<code>

這個方法的其他細節請參考註釋即可。通過這個方法的主要目的就是將圖片送進 OpenGL 的 sample2D 採樣器中,此 Sample2D 採樣器是在 片元 Shader 腳本中定義的。如下定義中的 inputImageTexture。

<code>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" +            "}";/<code>

設置 Filter——GPUImage#setImageFilter()

<code>/**     * 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();    }/<code>

調用了 render 的 setFilter,並再次發起渲染請求。來進一步看看。

<code>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();                }                // 然後調用 fiter.ifNeedInit() 進行初始化                GPUImageRenderer.this.filter.ifNeedInit();                // 設置 OpenGL 上下文所使用的程序 ID                GLES20.glUseProgram(GPUImageRenderer.this.filter.getProgram());                // 更新視口大小                GPUImageRenderer.this.filter.onOutputSizeChanged(outputWidth, outputHeight);            }        });    }/<code>

這裡的主要過程就對應了前面 GPUImageFilter 生命週期的流程圖。其首先判斷是否有舊的 filter,如果有則先銷燬。銷燬很簡單,主要就是通過 GLES20.glDeleteProgram(glProgId) 銷燬 OpenGL 當前運行的程序 ID,然後再通過勾子方法 onDestroy() 通知 GPUImageFilter 的子類釋放其他所用到的資源。這裡重點需要了解一下的是其初始化的過程。

ififNeedInit() 主要就是調用了 onInit()

<code>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;    }/<code>

創建程序ID,獲取 頂點位置屬性 "position",紋理座標屬性"inputTextureCoordinate",統一變量"inputImageTexture"。這裡主要是 loadProgram() 需要說一下,其主要完成的功能便是加載頂點以及片元著色器,然後創建程序,附加著色器,最後鏈接程序。這些過程都是 OpenGL 編程過程中所必須經歷的步驟,這裡只稍做了解即可。為了文章的完整性,這裡也將相關的代碼貼出來。

<code>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;    }/<code>
<code>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;    }/<code>

至此,可以說用來渲染圖片的環境是已經建立好了。如確定頂點座標,縮放方式,建立OpenGL的渲染環境等等。下面就看如何繪製出來了。

渲染——渲染 Filter

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

<code>@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();        }    }/<code>

其實應該能想得到,其最主要的就是通過調用 filter 的 onDraw() 進行渲染。

<code>public void onDraw(final int textureId, final FloatBuffer cubeBuffer,                       final FloatBuffer textureBuffer) {        // 激程序 ID        GLES20.glUseProgram(glProgId);        runPendingOnDrawTasks();        if (!isInitialized) {            return;        }       // 將頂點 buffer 的數據送給屬性 "position",並使能屬性        cubeBuffer.position(0);       // 下面的 2 表示每個點的 size 大小,即這裡的一個座標只需要取 2 個表示 (x,y) 即可。如果為 3 則表示 (x,y,z)        GLES20.glVertexAttribPointer(glAttribPosition, 2, GLES20.GL_FLOAT, false, 0, cubeBuffer);        GLES20.glEnableVertexAttribArray(glAttribPosition);        // 將紋理座標 buffer 的數據送給屬性 "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);    }/<code> 

渲染的過程就是 OpenGL 方法的一些調用,其中的意思也都在代碼裡增加了註釋說明。其子類 Filter 也都採用這個 onDraw() 進行繪製。而決定每個 filter 渲染出什麼樣的濾鏡效果就都在其定義的頂點著色器和片元著色器裡了。

至此,將圖片經 GPUImageFilter 渲染到 GLSurfaceView 上的過程已經分析完了。如前面所說,有了 GPUImage 這個框架,就不需要我們去處理 OpenGL 裡面的各種繁瑣的細節了。一般的,我們只需要寫好我們自己的著色器,剩下的就都可以交給 GPUImage 來完成了。

3.離屏渲染

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


GPUImage 詳解與框架源碼分析

離屏渲染.jpg


初始化離屏渲染的環境

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

<code>/**     * 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;    }/<code>

省略的部分與 GLSurfaceView 相關,主要主是銷燬的相關工作。構造 GPUImageRenderer 前面也分析過了。這裡主要只分析 PixelBuffer 相關的調用。首先看看其構造函數。

<code>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        // 創建 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        };        // 創建上下文        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();    }/<code>

關鍵過程在註釋中都有添加。這裡主要關注的是 eglCreatePbufferSurface() 的調用,其主要作用就是在顯存中開闢一個 buffer,並不關聯任何屏幕上的 window。那與之對應的 GLSurfaceView 是否有在屏幕的 window 上開闢一個 buffer 呢。

<code>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;        }/<code>

如 GLSurfaceView 中創建 EGLSurface 的代碼所示,果然是有的,只不過它調用的是另一個方法 eglCreateWindowSurface()。這裡所傳的參數裡需要注意的是nativeWindow,它其實就是 SurfaceHolder。

到這裡也就創建好了離屏渲染所需要的環境,接著與之前一樣,給 GPUImageRenderer 設置圖片以及 Filter 並作好相關渲染準備。

獲取渲染結果

先調用 getBitmap() ,該方法中會進一步調用 render 的 onDrawFrame,從而使得圖片按照 filter 所希望的效果將圖片渲染到 PixelBuffer 中所創建的 EGLSurface 中。然後調用 convertToBitmap() 方法將EGLSurface 中的 buffer 中的內容轉換成 bitmap。

convertToBitmap() 只是一個簡單的調用,其進一步調用了 native 函數 GPUImageNativeLibrary.adjustBitmap(bitmap) 來真正執行轉換的操作。

<code>JNIEXPORT void JNICALLJava_jp_co_cyberagent_android_gpuimage_GPUImageNativeLibrary_adjustBitmap(JNIEnv *jenv, jclass thiz,                                                                       jobject src) {    unsigned char *srcByteBuffer;    int result = 0;    int i, j;    // 聲明一個 AndroidBitmapInfo 結構    AndroidBitmapInfo srcInfo;    // 從圖片中獲取 info    result = AndroidBitmap_getInfo(jenv, src, &srcInfo);    if (result != ANDROID_BITMAP_RESULT_SUCCESS) {        return;    }    // 將圖片 src 的數據指針賦值給 srcByteBuffer    result = AndroidBitmap_lockPixels(jenv, src, (void **) &srcByteBuffer);    if (result != ANDROID_BITMAP_RESULT_SUCCESS) {        return;    }    int width = srcInfo.width;    int height = srcInfo.height;    // 從當前 EGL 運行環境中讀取圖片數據並保存在 srcByteBuffer 中,也就保存到了位圖裡面了    glReadPixels(0, 0, srcInfo.width, srcInfo.height, GL_RGBA, GL_UNSIGNED_BYTE, srcByteBuffer);    int *pIntBuffer = (int *) srcByteBuffer;   // OpenGL和Android的Bitmap色彩空間不一致,這裡需要做轉換。以中間為基線進行對調。    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);}/<code>

這段代碼可能有些是似曾相識的。當我們在完成截屏功能時,如果碰到有 video 的時候,截出來是黑的。有很多大神提供實現工具,而其內部的原理就是這個,即讀取當前上下文的 buffer 中的圖片數據,然後保存到 bitmap 或者 創建 bitmap。由於在 OpenGL 的 buffer 中其順序是 左上 到 右下,而圖片紋理的順序是 左下 到 右上。因此需要以中間為基準將數據進行對調。

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

四、後記

同樣感謝你能讀到此文章,也希望你能有所收穫。當然,對於 GPUImage 的分析與閱讀需要有一定的 OpenGL 的基礎,不然會覺得裡面的概念繁多而且也比較抽象。另外,文章主要只是分析了 GPUImage 使用 filter 進行界面渲染或者離屏渲染過程的一個解讀。由於我在圖形圖像領域也只是一個稍微入了門的小菜鳥,對於圖像處理算法更是知之甚少,所以對於 Filter 的具體算法實現沒有進行分析。對於文中的分析,如存在錯誤或者有不清楚的地方,也歡迎留言討論,將不勝感激。


分享到:


相關文章: