音視頻開發(fā)之旅(九) OpenGL ES 繪制平面圖形

目錄

  1. 寫著色器代碼
  2. 通過GLSurfaceview加載Shader并運行
  3. 遇到的問題
  4. 參考
  5. 收獲

我們前兩篇介紹了OpenGL ES 基本概念GLSL及Shader的渲染流程,這篇我們開始實戰(zhàn),通過GLSurfaceView加載著色器,來繪制三角形、正方形和直線這些平面圖形。在實踐過程中遇到的問題有時候讓人沒有頭緒,檢查了一遍又一遍代碼,發(fā)現(xiàn)流程沒有問題,但屏幕就是一片漆黑。。通過近一個小時的排查,發(fā)現(xiàn)問題出在了這里。。。下面開始我們今天的學習時間之旅,希望對你也有幫助。

GLSL著色器的編寫

如果對OpenGL的基本概念以及渲染流程不清晰的,建議看下前兩篇文章,這些基本概念和流程要了解或者理解,否則后面實踐之旅就是跳坑之旅。

我們先通過下面重要的二張圖,快速回顧下


工欲善其事,必先利其器,如何方便的編寫GLSL代碼吶? AndoridStudio提供了“Support for GLSL”插件。VS Code也有比較強大的插件比如“Shader Toy
”和“Shader languages support for VS Code”。但是都不足之處,就是沒有自動提示和補全的功能。所以編寫GLSL代碼是要細心。

1.1 著色器代碼的編寫

首先我們來編寫下頂點著色器和片元著色器

//vertex_shader.glsl 頂點著色器

attribute vec4 a_Position;
attribute vec4 a_Color;

varying vec4 v_Color;

void main() {
    v_Color = a_Color;
    gl_Position = a_Position;
}

上述代碼簡單語法回顧

attribute是修飾符 只能用于頂點著色器,用于修飾可變的參數(shù)
vec4: 浮點型向量

gl_Position:內(nèi)置變量

varying:也是一個修飾符,用于頂點著色器和片元著色器的值傳遞。
注意:必須要在頂點著色器和片元著色器都定義同名同類型同varying修飾符的變量,才能正常傳遞。
//fragment_shader.glsl 片元著色器

precision mediump float;

varying vec4 v_Color;

void main() {
    gl_FragColor = v_Color;
}

這里對精度precision mediump 做下說明
用于修飾浮點型和整形,有三個等級 highp\mediump、lowp


1.2 著色器代碼的讀取

著色器代碼通常放在assets目錄下或者raw目錄下,當然也見到過直接寫在代碼里。我們?yōu)榱朔奖?,采用比較通用的方式:把glsl代碼文件放在了assets下,再加載前需要先把他們讀到內(nèi)存中,常規(guī)的文件讀寫

public static String loadAsset(Resources res, String path) {
     StringBuilder stringBuilder = new StringBuilder();
     try {
         InputStream is = res.getAssets().open(path);

         byte[] buffer = new byte[1024];
         int count;
         while (-1 != (count = is.read(buffer))) {
             stringBuilder.append(new String(buffer, 0, count));
         }
         String result = stringBuilder.toString().replaceAll("\\r\\n", "\n");
        return result;
     } catch (IOException e) {
         e.printStackTrace();
     }
     return "";
 }

1.3 著色器的創(chuàng)建、設(shè)置源碼、編譯

private static int loadShader(int type, String codeStr) {
        //1. 根據(jù)類型(頂點著色器、片元著色器)創(chuàng)建著色器,拿到著色器句柄
        int shader = GLES20.glCreateShader(type);
        Log.i(TAG, "compileShaderCode: type=" + type + " shaderId=" + shader);

        if (shader > 0) {
            //2. 設(shè)置著色器代碼 ,shader句柄和code進行綁定
            GLES20.glShaderSource(shader, codeStr);
            //3. 編譯著色器,
            GLES20.glCompileShader(shader);

            //4. 查詢編譯狀態(tài)
            int[] status = new int[1];
            GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, status, 0);
            Log.i(TAG, "loadShader: status[0]=" + status[0]);
            //如果失敗,釋放資源
            if (status[0] == 0) {
                GLES20.glDeleteShader(shader);
                return 0;
            }
        }
        return shader;
    }

1.4 程序的創(chuàng)建、attach著色器、鏈接、使用

public static int loadProgram(String verCode, String fragmentCode) {
        //1. 創(chuàng)建Shader程序,獲取到program句柄
        int programId = GLES20.glCreateProgram();
        if(programId == 0){
            Log.e(TAG, "loadProgram: glCreateProgram error" );
            return 0;
        }
        //2. 根據(jù)著色器語言類型和代碼,attach著色器
        GLES20.glAttachShader(programId, loadShader(GLES20.GL_VERTEX_SHADER, verCode));
        GLES20.glAttachShader(programId, loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentCode));
        //3. 鏈接
        GLES20.glLinkProgram(programId);
        //4. 使用
        GLES20.glUseProgram(programId);
        return programId;
    }

1.5 狀態(tài)查詢

  1. 著色器Shader和Program創(chuàng)建后會拿到對應(yīng)的句柄,通過檢查是否大于0驗證是否可用

  2. GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, status, 0);著色器編譯后檢查編譯的狀態(tài)是否大于0判斷可用性。

  3. glGetProgramiv(programObjectId, GL_VALIDATE_STATUS,
    validateStatus, 0);//程序鏈接后,檢查程序的可用性

1.6 輸入與輸出

輸入:給著色器賦值
輸出:在屏幕上顯示

前面5個步驟把準備工作都做好了,那邊現(xiàn)在面臨兩個問題.

1. 我們看到頂點著色器中有兩個attribute修飾的變量,如何給它們賦值?
2. 如何把著色器再屏幕上繪制出來?

這個環(huán)節(jié)我們就來解決這兩個問題

首先定義好頂點坐標和顏色的位數(shù)和著色器的數(shù)據(jù)

    //每個頂點坐標的個數(shù)
    private final static int COORDS_PER_VERTEX = 2;

    //每個頂點顏色的個數(shù)
    private final static int COLOR_PER_VERTEX = 3;
    
    // 浮點類型占用的字節(jié)數(shù)
    private final static int BYTES_PER_FLOAT = 4;
    
    //下面兩個字符串常量就是GLSL頂點著色器的輸入

    private static final String A_POSITION = "a_Position";
    private static final String A_COLOR = "a_Color";

    //STRIDE是一個頂點的字節(jié)偏移(頂點坐標xy+顏色rgb)
    private final int STRIDE = (COORDS_PER_VERTEX+ COLOR_PER_VERTEX )* BYTES_PER_FLOAT;

    private FloatBuffer mVertexData;

    public MyRender2() {
        //頂點數(shù)組
        float[] TRIANGLE_COORDS = {
                0.5f, 0.5f,1f, 0.5f,0.5f,
                -0.5f, -0.5f,0.5f, 1f,0.5f,
                0.5f, -0.5f,0.5f, 0.5f,1f

        };

       //通過nio ByteBuffer把設(shè)置的頂點數(shù)據(jù)加載到內(nèi)存
        mVertexData = ByteBuffer
                .allocateDirect(TRIANGLE_COORDS.length * BYTES_PER_FLOAT) //需要多少字節(jié)內(nèi)存
                .order(ByteOrder.nativeOrder())//大小端排序
                .asFloatBuffer()
                .put(TRIANGLE_COORDS);//設(shè)置數(shù)據(jù)
    }

然后給頂點著色器的變量賦值

 String vertexCode = ShaderHelper.loadAsset(MyApplication.getContext().getResources(), "vertex_shader.glsl");
        String fragmentCode = ShaderHelper.loadAsset(MyApplication.getContext().getResources(), "fragment_shader.glsl");
        //創(chuàng)建著色器程序
        programId = ShaderHelper.loadProgram(vertexCode, fragmentCode);


        int aPosition = GLES20.glGetAttribLocation(programId, A_POSITION);
        Log.i(TAG, "drawFrame: aposition="+aPosition);
        mVertexData.position(0);
        GLES20.glVertexAttribPointer(aPosition,
                COORDS_PER_VERTEX,//用幾個偏移描述一個頂點
                GLES20.GL_FLOAT,//頂點數(shù)據(jù)類型
                false,
                STRIDE,//一個頂點需要多少個字節(jié)偏移
                mVertexData//分配的buffer
        );

        //開啟頂點著色器的attribute
        GLES20.glEnableVertexAttribArray(aPosition);

        int aColor = GLES20.glGetAttribLocation(programId, A_COLOR);
        mVertexData.position(COORDS_PER_VERTEX);
        GLES20.glVertexAttribPointer(aColor,COLOR_PER_VERTEX,GL_FLOAT,false,STRIDE,mVertexData);
        GLES20.glEnableVertexAttribArray(aColor);

關(guān)鍵API說明

  1. 獲取著色器attribute一個屬性:int aPosition = GLES20.glGetAttribLocation(programId, A_POSITION);_

  2. 數(shù)據(jù)的偏移:mVertexData.position(0); 因為有坐標和顏色兩個變量的值,數(shù)據(jù)又是根據(jù)頂點一一設(shè)定的。

  3. 給attribute賦值:GLES20. glVertexAttribPointer(
    int indx,//attribute的句柄
    int size,//在數(shù)組中占用的位數(shù)
    int type,//數(shù)據(jù)的類型
    boolean normalized,
    int stride,//步幅 單位字節(jié)數(shù)
    java.nio.Buffer ptr // 元數(shù)據(jù)
    )

  4. 使能對應(yīng)的attribute屬性:GLES20.glEnableVertexAttribArray(aPosition);

通過上面幾個環(huán)節(jié)我們可以看到,我們可以看到,是如何給著色器語言中的變量賦值的。下面我們看來下如何渲染。

我們采用GlSurfaceView,通過Render來實現(xiàn),具體如下:

    //1. 設(shè)置OpenGL ES的版本
    glSView.setEGLContextClientVersion(3);

     //2. 給glSurfaceView設(shè)置render
    glSView.setRenderer(new MyRender2());

  
public class MyRender2 implements GLSurfaceView.Renderer {

@Override
    public void onSurfaceCreated(GL10 gl, EGLConfig config) {
      //著色器的加載、賦值
    }

    @Override
    public void onSurfaceChanged(GL10 gl, int width, int height) {
        GLES20.glViewport(0, 0, width, height);
    }

    @Override
    public void onDrawFrame(GL10 gl) {
        //清屏
        GLES20.glClear(GL_COLOR_BUFFER_BIT);
        
        //繪制
        GLES20.glDrawArrays(GLES20.GL_TRIANGLE_FAN,0,3);
    }
}

是的,在onDrawFrame中進行不斷的 glDrawArrays來繪制刷新   
 public static native void glDrawArrays(
        int mode, //點、線、三角形
        int first,//頂點的第一個數(shù)據(jù)的index
        int count//頂點的總數(shù)
    );

二、實踐:用GLSurfaceView加載GLSL繪制屏幕圖形

2.1 三角形

上面的代碼中定義的就是三角形,對應(yīng)頂點數(shù)據(jù)如下

float[] TRIANGLE_COORDS = {0.5f, 0.5f,1f, 0.5f,0.5f,
                -0.5f, -0.5f,0.5f, 1f,0.5f,
                0.5f, -0.5f,0.5f, 0.5f,1f

        };

兩個坐標位x&y,和三個顏色位rgb

效果如下


2.2 正方形

只需要修改 頂點數(shù)組和glDrawArrays的count參數(shù)即可

float[] TRIANGLE_COORDS = {
               0.5f, 0.5f,1f, 0.5f,0.5f,
               -0.5f, -0.5f,0.5f, 1f,0.5f,
               0.5f, -0.5f,0.5f, 0.5f,1f,
               0.5f, 0.5f,1f, 0.5f,0.5f,
               -0.5f, 0.5f,0.5f, 0.5f,1f,
               -0.5f, -0.5f,0.5f, 1f,0.5f,
       };


  public void onDrawFrame(GL10 gl) {
       GLES20.glClear(GL_COLOR_BUFFER_BIT);
       GLES20.glDrawArrays(GLES20.GL_TRIANGLES,0,6);
   }

2.3 直線

     float[] TRIANGLE_COORDS = {
            -0.5f, 0f, 1f, 0f, 0f,
             0.5f, 0f, 0f, 0f, 1f,
        };

    public void onDrawFrame(GL10 gl) {
        GLES20.glClear(GL_COLOR_BUFFER_BIT);
        GLES20.glDrawArrays(GLES20.GL_LINES,0,2);
    }

通過log分析,我們發(fā)現(xiàn)GLSurfaceView.Renderer是運行中一個叫GLThread的線程中,它的作用和意義是什么,下一篇我們通過對GLSurfaceView的源碼分析來理解EGL和GLThread,了解完整的流程,然后不使用Render,采用自建管理EGL和創(chuàng)建GLThread,通過TextureView實現(xiàn)圖形的繪制。做到知其然,也知其所以然。

三、遇到的問題

1. A/libc: Fatal signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x0 in tid 9940 (GLThread 6703)_

發(fā)現(xiàn)是GLES20.glCreateProgram()時出現(xiàn)的上述崩潰
原因是因為沒有g(shù)lSView.setEGLContextClientVersion(3);
2. GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, status, 0)
Shader編譯后獲取狀態(tài)為0(失敗了)

原因是因為glsl語言注釋不對用成了 # 而不是 //
3.  無法正常的看到期望的圖片
這個就是開頭說的說的那個折騰了一個小時的問題,一個“逗號”引發(fā)的問題

具體如下:

float[] TRIANGLE_COORDS = {0.5f, 0.5f,1f, 0.5f,0.5f ///注意這里沒有加逗號,就是這個導(dǎo)致的,glsl認為到這里就結(jié)束了。。但是代碼上寫的是3個頂點,找不到就無法正常繪制。多么痛的領(lǐng)悟。
                -0.5f, -0.5f,0.5f, 1f,0.5f,
                0.5f, -0.5f,0.5f, 0.5f,1f

        };

四、參考

《OpenGL ES應(yīng)用開發(fā)實踐指南》
《音視頻開發(fā)進階指南》
[搭建OpenGL ES環(huán)境的兩種方式]
[Android OpenGL ES(一)-開始描繪一個平面三角形]
[Android OpenGL ES(三)-平面圖形]
[EGL 環(huán)境搭建流程]

五、收獲

  1. 通過代碼實踐加深了對GLSL語法,OpenGL基本概念和繪制流程的熟悉。
  2. glsl程序編寫androidstudio等IDE插件的了解
  3. 理解實現(xiàn)了如何給著色器輸入數(shù)據(jù),又如何在屏幕上繪制。
  4. 繪制三角形、正方形、直線等平面圖形
  5. 遇到的問題分析解決,彌補認知不足。

下一篇我們來解析GLSurfaceView源碼&自己實現(xiàn)EGL管理與GLThread。歡迎關(guān)注公眾號“音視頻開發(fā)之旅”,一起學習成長。

歡迎交流

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容