從顯示一張圖片開始學習OpenGL ES

前言

網上不少介紹OpenGL ES的文章,但因爲OpenGL ES內容太多,因此這些文章不免過於臃腫雜亂,很難抓住重點,對於初學者來講最後仍是雲裏霧裏。不少人(包括筆者本人)開始深刻了解OpenGL ES是由於其涉及到實時濾鏡的應用,一般都會參考開源框架GPUImage的實現。若是沒有掌握基本的OpenGL Es的開發知識,很難弄懂其中代碼原因。java

目前很流行的短視頻特效處理也有涉及到OpenGL的應用,因而已經踩坑無數的筆者下決心讓後來者少走彎路,以最實用的場景——顯示一張圖片開始學習OpenGL ES.android

本文章適合初學Android OpenGL ES 2.0+,以及想要了解OpenGL實時濾鏡實現原理的同窗。git

準備

在開始實現以前,先要講一些基本的知識,也是OpenGL ES 2D\3D繪圖的一些基本理論,這裏咱們只講繪製一張圖片後面須要用到的知識點github

座標系

OpenGL擁有獨立的座標系,沒有任何變換前的初始座標系爲三維座標系,x y z 取值範圍都是 [-1, 1]:編程

因爲咱們繪製的是2D圖片,所以能夠簡化爲二維座標系(只包含xy軸),座標系的原點在窗口中央,x 軸向右,y 軸向上:數組

這時就有疑問了,咱們的屏幕或顯示窗口長寬的比例不是1:1(即不是正方形),怎麼跟OpenGL的初始世界座標系對應呢?若是咱們沒有指定投影比例,那麼世界座標系則會填充整個顯示窗口,這樣就會致使拉伸變形,好比把上面的三角形投射在窗口時的顯示以下:框架

若是要指定投影比例就得應用到投影和矩陣變換,這裏咱們仍使用初始的世界座標系,好比爲了上面的三角形顯示正常,根據拉伸比例改變繪製的頂點座標便可。ide

頂點座標

在OpenGL ES中,支持三種類型的繪製:點、直線以及三角形;由這三種圖形組成其餘全部圖形,好比咱們看到的圓滑的球體也是由一個個三角形組成的,三角形越多看上去越圓滑:函數

在繪製圖形時咱們須要提供相應的頂點位置,而後指定繪製方式去繪製這些頂點,以此呈現出咱們想要的圖形。工具

後面咱們顯示一張圖片的時候也須要繪製由兩個三角形組成的矩形,經過GL_TRIANGLE_STRIP 繪製方式(即每相鄰三個頂點組成一個三角形,爲一系列相接三角形構成)繪製:

紋理貼圖(紋理映射)

咱們須要顯示的是一張圖片,而上面一直說繪製圖形。這就比如咱們往牆上貼牆紙,首先得搭建好房子,而後決定牆紙的每一個地方貼在牆上的哪一個位置,這個過程在OpenGL的繪製過程當中叫作紋理貼圖,也叫紋理映射。

紋理貼圖時涉及到UV座標,全部的圖像文件都是二維的一個平面,經過這個平面的UV座標咱們能夠定位圖象上的任意一個象素,在android的uv座標的原點在左上角:

咱們根據頂點的渲染順序,定義每一個頂點uv座標,以下圖是咱們定義的四個頂點,繪製成一個矩形:

那麼根據頂點的渲染順序,定義每一個頂點uv座標:

指定好特定頂點對應的紋理座標後,頂點與頂點間的其他部分會進行圖像光滑插值處理,最後整張紋理就顯示出來啦。

光柵化

光柵化就是把頂點數據轉換爲片元的過程。片元中的每個元素對應於幀緩衝區中的一個像素。

把虛擬世界中的三維幾何信息投影到二維屏幕上,因爲目前的顯示設備屏幕都是離散化的(有一個個的像素組成),所以須要把投影結果離散化,將其分解爲一個個離散化的小單元,這些小單元稱之爲片元(片斷,Fragment).

着色器

OpenGL ES2.0使用可編程渲染管線,既然是可編程,那就須要咱們本身寫着色器代碼(GLSL),OpenGL中有頂點着色器(Vertex Shader)和片元着色器(Fragment Shader)。

頂點着色器主要用來處理圖形中每一個頂點的最終位置。頂點數據由咱們傳進着色器,因爲繪製圖片不須要變換頂點,因此頂點着色器裏面咱們不須要特殊處理每一個頂點。而片元着色器主要處理每一個片元的最終顏色,這裏咱們只要根據傳進來的貼圖數據,進行紋理採樣便可。

開始動手實現!

在Android系統中使用OpenGL須要涉及到兩個最基本的的類,GLSurfaceView和GLSurfaceView.Renderer。

  • GLSurfaceView繼承了SurfaceView類,它是專門用來顯示OpenGL渲染的圖形。能夠這麼理解,GLSurfaceView就是前面咱們說的用來顯示OpenGL圖形的窗口。
  • GLSurfaceView.Renderer是GLSurfaceview的渲染器,經過GLSurfaceView.setRender()設置。
interface GLSurfaceView.Renderer {
	//在Surface建立的時候回調,能夠在這裏進行一些初始化操做
	public void onSurfaceCreated(GL10 gl, EGLConfig config);
	//在Surface尺寸改變的的時候回調,能夠在這裏設置窗口的大小
	public void onSurfaceChanged(GL10 gl, int width, int height);
	//繪製每一幀的時候回調
	public void onDrawFrame(GL10 gl);
}
複製代碼

這裏須要特別說明,Render渲染器的回調是在一個單獨的線程上執行的,所以咱們進行OpenGL的相關操做也須要切換到該GL環境下的線程上,能夠經過GLSurfaceView.queueEvent(Runnable)把操做放入GL環境的隊列中,也能夠本身控制隊列,等待Render回調時再執行隊列的操做。

代碼以下:

public class GLShowImageActivity extends Activity {
    // 繪製圖片的原理:定義一組矩形區域的頂點,而後根據紋理座標把圖片做爲紋理貼在該矩形區域內。

    // 原始的矩形區域的頂點座標,由於後面使用了頂點法繪製頂點,因此不用定義繪製頂點的索引。不管窗口的大小爲多少,在OpenGL二維座標系中都是爲下面表示的矩形區域
    static final float CUBE[] = { // 窗口中心爲OpenGL二維座標系的原點(0,0)
            -1.0f, -1.0f, // v1
            1.0f, -1.0f,  // v2
            -1.0f, 1.0f,  // v3
            1.0f, 1.0f,   // v4
    };
    // 紋理也有座標系,稱UV座標,或者ST座標。UV座標定義爲左上角(0,0),右下角(1,1),一張圖片不管大小爲多少,在UV座標系中都是圖片左上角爲(0,0),右下角(1,1)
    // 紋理座標,每一個座標的紋理採樣對應上面頂點座標。
    public static final float TEXTURE_NO_ROTATION[] = {
            0.0f, 1.0f, // v1
            1.0f, 1.0f, // v2
            0.0f, 0.0f, // v3
            1.0f, 0.0f, // v4
    };

    private GLSurfaceView mGLSurfaceView;
    private int mGLTextureId = OpenGlUtils.NO_TEXTURE; // 紋理id
    private GLImageHandler mGLImageHandler = new GLImageHandler();

    private FloatBuffer mGLCubeBuffer;
    private FloatBuffer mGLTextureBuffer;
    private int mOutputWidth, mOutputHeight; // 窗口大小
    private int mImageWidth, mImageHeight; // bitmap圖片實際大小

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_01);
        mGLSurfaceView = findViewById(R.id.gl_surfaceview);
        mGLSurfaceView.setEGLContextClientVersion(2); // 建立OpenGL ES 2.0 的上下文環境

        mGLSurfaceView.setRenderer(new MyRender());
        mGLSurfaceView.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY); // 手動刷新
    }

    private class MyRender implements GLSurfaceView.Renderer {

        @Override
        public void onSurfaceCreated(GL10 gl, EGLConfig config) {
            GLES20.glClearColor(0, 0, 0, 1);
            GLES20.glDisable(GLES20.GL_DEPTH_TEST); // 當咱們須要繪製透明圖片時,就須要關閉它
            mGLImageHandler.init();

            // 須要顯示的圖片
            Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.thelittleprince);
            mImageWidth = bitmap.getWidth();
            mImageHeight = bitmap.getHeight();
            // 把圖片數據加載進GPU,生成對應的紋理id
            mGLTextureId = OpenGlUtils.loadTexture(bitmap, mGLTextureId, true); // 加載紋理

            // 頂點數組緩衝器
            mGLCubeBuffer = ByteBuffer.allocateDirect(CUBE.length * 4)
                    .order(ByteOrder.nativeOrder())
                    .asFloatBuffer();
            mGLCubeBuffer.put(CUBE).position(0);

            // 紋理數組緩衝器
            mGLTextureBuffer = ByteBuffer.allocateDirect(TEXTURE_NO_ROTATION.length * 4)
                    .order(ByteOrder.nativeOrder())
                    .asFloatBuffer();
            mGLTextureBuffer.put(TEXTURE_NO_ROTATION).position(0);
        }

        @Override
        public void onSurfaceChanged(GL10 gl, int width, int height) {
            mOutputWidth = width;
            mOutputHeight = height;
            GLES20.glViewport(0, 0, width, height); // 設置窗口大小
            adjustImageScaling(); // 調整圖片顯示大小。若是不調用該方法,則會致使圖片整個拉伸到填充窗口顯示區域
        }

        @Override
        public void onDrawFrame(GL10 gl) { // 繪製
            GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT);
            // 根據紋理id,頂點和紋理座標數據繪製圖片
            mGLImageHandler.onDraw(mGLTextureId, mGLCubeBuffer, mGLTextureBuffer);
        }

        // 調整圖片顯示大小爲居中顯示
        private void adjustImageScaling() {
            float outputWidth = mOutputWidth;
            float outputHeight = mOutputHeight;

            float ratio1 = outputWidth / mImageWidth;
            float ratio2 = outputHeight / mImageHeight;
            float ratioMax = Math.min(ratio1, ratio2);
            // 居中後圖片顯示的大小
            int imageWidthNew = Math.round(mImageWidth * ratioMax);
            int imageHeightNew = Math.round(mImageHeight * ratioMax);

            // 圖片被拉伸的比例
            float ratioWidth = outputWidth / imageWidthNew;
            float ratioHeight = outputHeight / imageHeightNew;
            // 根據拉伸比例還原頂點
            float[] cube = new float[]{
                        CUBE[0] / ratioWidth, CUBE[1] / ratioHeight,
                        CUBE[2] / ratioWidth, CUBE[3] / ratioHeight,
                        CUBE[4] / ratioWidth, CUBE[5] / ratioHeight,
                        CUBE[6] / ratioWidth, CUBE[7] / ratioHeight,
                };

            mGLCubeBuffer.clear();
            mGLCubeBuffer.put(cube).position(0);
        }
    }
}
複製代碼

對於着色器的語法和相關使用,這裏我不去贅述,我給的建議是,先了解頂點着色器和片元着色器的主要做用,而後在把這篇教程理解一遍後,對着色器感興趣的話再去查找相關的資料。這裏咱們只是顯示一張圖片,使用的着色器代碼很簡單,都加了註釋,不影響你們理解哈。

/** * 負責顯示一張圖片 */
public class GLImageHandler {
    // 數據中有多少個頂點,管線就調用多少次頂點着色器
    public static final String NO_FILTER_VERTEX_SHADER = "" +
            "attribute vec4 position;\n" + // 頂點着色器的頂點座標,由外部程序傳入
            "attribute vec4 inputTextureCoordinate;\n" + // 傳入的紋理座標
            " \n" +
            "varying vec2 textureCoordinate;\n" +
            " \n" +
            "void main()\n" +
            "{\n" +
            " gl_Position = position;\n" +
            " textureCoordinate = inputTextureCoordinate.xy;\n" + // 最終頂點位置
            "}";

    // 光柵化後產生了多少個片斷,就會插值計算出多少個varying變量,同時渲染管線就會調用多少次片斷着色器
    public static final String NO_FILTER_FRAGMENT_SHADER = "" +
            "varying highp vec2 textureCoordinate;\n" + // 最終頂點位置,上面頂點着色器的varying變量會傳遞到這裏
            " \n" +
            "uniform sampler2D inputImageTexture;\n" + // 外部傳入的圖片紋理 即表明整張圖片的數據
            " \n" +
            "void main()\n" +
            "{\n" +
            " gl_FragColor = texture2D(inputImageTexture, textureCoordinate);\n" +  // 調用函數 進行紋理貼圖
            "}";

    private final LinkedList<Runnable> mRunOnDraw;
    private final String mVertexShader;
    private final String mFragmentShader;
    protected int mGLProgId;
    protected int mGLAttribPosition;
    protected int mGLUniformTexture;
    protected int mGLAttribTextureCoordinate;

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

    public GLImageHandler(final String vertexShader, final String fragmentShader) {
        mRunOnDraw = new LinkedList<Runnable>();
        mVertexShader = vertexShader;
        mFragmentShader = fragmentShader;
    }

    public final void init() {
        mGLProgId = OpenGlUtils.loadProgram(mVertexShader, mFragmentShader); // 編譯連接着色器,建立着色器程序
        mGLAttribPosition = GLES20.glGetAttribLocation(mGLProgId, "position"); // 頂點着色器的頂點座標
        mGLUniformTexture = GLES20.glGetUniformLocation(mGLProgId, "inputImageTexture"); // 傳入的圖片紋理
        mGLAttribTextureCoordinate = GLES20.glGetAttribLocation(mGLProgId, "inputTextureCoordinate"); // 頂點着色器的紋理座標
    }

    public void onDraw(final int textureId, final FloatBuffer cubeBuffer, final FloatBuffer textureBuffer) {
        GLES20.glUseProgram(mGLProgId);
        // 頂點着色器的頂點座標
        cubeBuffer.position(0);
        GLES20.glVertexAttribPointer(mGLAttribPosition, 2, GLES20.GL_FLOAT, false, 0, cubeBuffer);
        GLES20.glEnableVertexAttribArray(mGLAttribPosition);
        // 頂點着色器的紋理座標
        textureBuffer.position(0);
        GLES20.glVertexAttribPointer(mGLAttribTextureCoordinate, 2, GLES20.GL_FLOAT, false, 0, textureBuffer);
        GLES20.glEnableVertexAttribArray(mGLAttribTextureCoordinate);
        // 傳入的圖片紋理
        if (textureId != OpenGlUtils.NO_TEXTURE) {
            GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
            GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId);
            GLES20.glUniform1i(mGLUniformTexture, 0);
        }

        // 繪製頂點 ,方式有頂點法和索引法
        GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); // 頂點法,按照傳入渲染管線的頂點順序及採用的繪製方式將頂點組成圖元進行繪製

        GLES20.glDisableVertexAttribArray(mGLAttribPosition);
        GLES20.glDisableVertexAttribArray(mGLAttribTextureCoordinate);
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0);
    }
}
複製代碼

上面的代碼中使用的OpenGlUtils類是封裝的一個工具類,主要負責加載紋理id,以及加載着色器代碼,這裏不詳細貼出代碼細節(都是一些模板代碼),感興趣的同窗待會能夠在該文章對應的項目代碼查看哦。

最終咱們的圖片就顯示出來啦。

注意事項

  • 須要在GLSurfaceView中設置OpenGL的版本:
setEGLContextClientVersion(2); // 2.0
複製代碼

不然會報相似錯誤glDrawArrays is called with VERTEX_ARRAY client state disabled!

  • 操做跟GPU相關的接口時須要在GLSurfaceView渲染的線程裏不然會報call to OpenGL ES API with no current context。好比獲取紋理id不能在界面初始化時,須要在onSurfaceCreated以後

完整代碼地址

github.com/1993hzw/Ope…

後話

OpenGL ES的初步介紹就到此爲止了,雖然一直想盡可能通俗簡單地講解,但整個寫下來發現仍是要涉及到不少東西,所以有不足的地方還望各位讀者指正!其實上面講的就是GPUImage這個開源庫的核心原理,同時目前流行的短視頻特效也是有很多涉及到OpenGL處理的,但願此文對你們學習OpenGL有些許幫助吧。

最後,謝謝你們的的支持!!!後面會根據這篇文章的反響,考慮是否須要繼續寫下一篇關於濾鏡的實現(其實主要是經過編寫着色器實現)。

相關文章
相關標籤/搜索