Learn OpenGLES:畫三角形

上一貼我們簡單的搭建了一個OpenGL的運行窗口,也簡單的對這個窗口進行繪製。 
這一貼,我們將介紹OpenGL如何將CPU中的內存數據送到GPU的內存中,Shader又是如何找到這些數據,並進行繪製的。
我們將通過繪製三角形這一簡單的例子,爲大家簡單的介紹下OpenGL的管線流程,以及如何渲染顏色,顏色漸變動畫等知識。

#介紹OpenGL的管線
Opengl 中所有的事物,都是由點來表示的,而這些點又是由3D座標表示的。但是最終要在屏幕或窗口中顯示的是2D畫面,這也就是說Opengl一大部分的工作是 如何將3D 座標變換爲2D座標,Opengl中 有一個 圖形管線(graphics pipeline)來專門處理這一過程。 圖形流程可以分爲兩部分:一, 將3d座標轉換爲2d座標;二,將2d座標變換爲實際的彩色像素。

圖形流程一般以 一列3D座標(物體模型的頂點)作爲輸入,然後生成新的頂點,並通過對頂點位置屬性的空間變換,對顏色屬性的燈光變換,再將其轉爲2D座標,最終渲染爲2D彩色圖形。這一過程可分爲多個步驟,而每一步驟都是以上一步的輸出爲輸入。 在Opengl中,每一步都被高度定製(簡單的由一個API表示)。 這所有的步驟之間可以並行執行。多數顯卡都有數千個小的處理單元(processing cores),通過在GPU中運行小的工程從而使得圖形流程可以很快處理數據 。這些小的工程被稱爲着色器(shaders)。

有些着色器是可以供開發者配置,通過編寫這些着色器可以替換opengl裏默認的着色器, 從而可以使得開發者細緻的掌控流程中一些特定的部分。 OpenGL爲我們提供了GLSL語言,該語言除了簡單的基本類型(類C)外,都是一些抽象的函數實現,而這些函數實現的算法都集成到了GPU中,從而大大的節省了CPU的運行時間。


下圖簡單的描述了圖形管線的處理步驟:


如何由頂點再到最後的渲染成像一目瞭然。 這裏面,最重要的兩個就是頂點着色(Vertex Shader) 和 片段着色(Fragment Shader), 由於OpenGL 2.0以後,接口編程的開放,這兩個就需要用戶自己定製,而其他的在一般情況下可保持默認。

# 頂點由CPU到GPU
 在MyGLRenderer裏, 創建一個構造函數,定義三角形的頂點數據:
 
1
2
3
4
5
6
float[]  verticesWithTriangle =
 {
      0f, 0f,
      5f, 5f,
      10f, 0f  
}

 一般在Java中的數據,是由虛擬機爲其分配好內存的,因此這一步還不能讓CPU真正獲取我們所定義的數據,並將數據傳遞給GPU,
好在Java爲我們提供了Buffer這樣的對象, 它可以直接在Native層分配內存,以讓CPU獲取。 在Java中,一個浮點型是4字節,因此,我們可定義BYTES_PER_FLOAT = 4,並有
1
2
vertexData = ByteBuffer.allocateDirect(tableVerticesWithTriangles.length*BYTES_PER_FLOAT)
   .order(ByteOrder.nativeOrder()).asFloatBuffer();
在這裏,我麼將分配好的字節按nativeOrder進行排序,這樣在大小端機器上都能適用。

CPU的數據是要送到GPU供Shader使用的,因此,我們需要在Shader中制定頂點的屬性
首先,在Android工程目錄, res下面創建一個raw文件夾,並創建vertexShader.glsl
1
2
3
4
5
6
7
// vertex shader
attribute vec4 a_Position;
               
void main()
{
    gl_Position = a_Position;
}
上面我們只定義了頂點的位置屬性, 因此,我們只聲明 一個位置屬性。 vec4是一個四維數組,如果我們沒有爲其分配數據,它將自動填充0, 0, 0, 1。 在vertexShader, 最終OpenGL 會將輸入的頂點位置賦值給gl_Position 進行輸出。

接着,我們定義fragmentShader.glsl
1
2
3
4
5
6
7
8
//fragShader
precision mediump float;
uniform vec4 u_Color;
               
void main()
{
    gl_FragColor = u_Color;
}

第一句定義了數據的精度,就像java語言的double, float類型一樣。 highp就像double代表了高精度,因其效率不高,只用於有限的幾個操作。我們只需用mediump即可。 然後,定義了一個 uniform顏色變量, 一般uniform變量在shader中不會有太大變化。 最後,將顏色賦值給gl_FragColor, 完成最後的顏色輸出。
gl_Position 和 gl_FragColor 都是OpenGL的內置變量。

# 鏈接着色器到工程中
  爲了能夠讓Opengl能夠使用這些着色器,還需要對這些着色器進行編譯,以便能夠在運行時使用它們。
 第一件事,就是創建一個 着色器工程(Shader Program):
首先,我們需要將Shader的代碼,讀到一個字符串中。在我們package下,創建一個TexResourceRender.java
裏面寫上如下函數:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public static String readTextFromResource(Context context, int resourceId)
    {
        StringBuilder body = new StringBuilder();
               
        try{
            InputStream inputStream = context.getResources().openRawResource(resourceId);
               
            InputStreamReader inputStreamReader = new InputStreamReader(inputStream);
            BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
               
            String nextLine;
               
            while((nextLine = bufferedReader.readLine())!=null)
            {
                body.append(nextLine);
                body.append("\n");
            }
        }catch(IOException e)
        {
            throw new RuntimeException("Could not open resource: "+ resourceId, e);
        }catch(Resources.NotFoundException nfe)
        {
            throw new RuntimeException("Resource not found: "+resourceId, nfe);
        }
        return body.toString();
               
    }
以上代碼表示: 我們從res/raw讀取了glsl的內容到字符串中,爲了覆蓋所有的執行可能,我們用try catch包含了文件讀取異常和讀取不到的情況。
爲了,能夠獲得程序在最後運行的信息,我們需要用Android的Log日誌,將信息打印出來,但又不希望只打印我們程序中信息,因此定義一個Logger.java
1
2
3
public Logger{
     public static boolean ON = true;
}
這樣就可以用ON來判斷,是否要打印我們的日誌

接着,創建一個ShaderUtils.java,用於完成shader工程的編譯,鏈接:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static int compileVertexShader(String shaderCode)
    {
        return compileShader(GL_VERTEX_SHADER,shaderCode); 
    }
               
public static int compileFragShader(String shaderCode)
 {
     return compileShader(GL_FRAGMENT_SHADER, shaderCode);
}
               
               
public static int compileShader(int type, String shaderCode)
{
}

這裏,我們將重點介紹compileShader, OpenGL的Shader編譯流程大體如下:1 創建一個Shader 2 將Shader源碼賦值給Shader 3 編譯 Shader 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
final int shaderObjectId = glCreateShader(type); // 1
             
        if(shaderObjectId == 0)
        {
            if(LoggerConfig.ON)
            {
                Log.w(TAG, "Could not create new shader.");
            }
             
            return 0;
        }
             
        glShaderSource(shaderObjectId, shaderCode); // 2
        glCompileShader(shaderObjectId);   // 3
             
        final int[] compileStatus = new int[1]; 
        glGetShaderiv(shaderObjectId, GL_COMPILE_STATUS, compileStatus, 0);
             
        if(LoggerConfig.ON)
        {
            Log.v(TAG, "Results of compiling source: "+"\n" + shaderCode +"\n:"
            + glGetShaderInfoLog(shaderObjectId));
        }
             
        if(compileStatus[0] == 0)
        {
            glDeleteShader(shaderObjectId);
             
            if(LoggerConfig.ON)
            {
                Log.w(TAG, "Compliation of shader failed");
            }
             
            return 0;
        }
             
        return shaderObjectId;
爲了檢測編譯的狀態,一般在Java平臺上,都是創建一個數組,然後獲得的編譯信息 都是存在該數組的第一個元素中。 如果,編譯的狀態有錯誤,刪除創建的Shader標識, 如果沒錯,就返回該標識。

在MyGLRenderer.java的 onSurfaceChanged 下的glClear後讀取 glsl的shader文件源碼, 之後編譯它們
1
2
3
4
5
String vertexShaderSource = TextResourceRender.readTextFromResource(mContext, R.raw.simple_vertex_shader);
 String fragShaderSource = TextResourceRender.readTextFromResource(mContext, R.raw.simple_frag_shader);
             
int vertexShader = ShaderUtils.compileVertexShader(vertexShaderSource);
int fragShader = ShaderUtils.compileFragShader(fragShaderSource);
Shader編譯以後,需要鏈接到工程中 ,鏈接工程的步驟大體如下: 1 創建一個program 2 將Shader依附在工程上 3 鏈接工程
 它的步驟與 Shader的編譯相似,在ShaderUtils.java添加如下代碼:
  
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public static int LinkProgram(int vertexShaderId, int fragShaderId)
  {
      final int programObjectId = glCreateProgram();   //1
      if(programObjectId ==0)
      {
          if(LoggerConfig.ON)
          {
              Log.w("TAG""Could not create new program");
          }
             
          return 0;
      }
             
      glAttachShader(programObjectId, vertexShaderId);  //2
      glAttachShader(programObjectId, fragShaderId);
             
      glLinkProgram(programObjectId);   //3
             
      //check any error
      final int[] linkStatus = new int[1];
      glGetProgramiv(programObjectId, GL_LINK_STATUS, linkStatus, 0);
             
      if(LoggerConfig.ON)
      {
          Log.v(TAG, "Results of linking program:\n" + glGetProgramInfoLog(programObjectId));
      }
             
      if(linkStatus[0] == 0)
      {
          glDeleteProgram(programObjectId);
          if(LoggerConfig.ON)
          {
              Log.w(TAG, "Linking of program valid");
          }
          return 0;
      }
             
      return programObjectId;
             
  }

然後,在MyGLRenderer.java的 onSurfaceCreated 下創建program 工程
1
int program = ShaderUtills.LinkProgram(vertexShader, fragShader);
在使用創建的program之前,我們很想知道,創建的工程是否符合opengl當前上下文的狀態, 知道爲什麼它有時候會運行失效,根據OpenGLES2.0的官方文檔, 我們需在ShaderUtils.java下創建如下方法:
1
2
3
4
5
6
7
8
9
public static boolean validateProgram(int programObjected)
    {
        glValidateProgram(programObjected);
        final int[] validateStatus = new int[1];
        glGetProgramiv(programObjected, GL_VALIDATE_STATUS, validateStatus, 0 );
        Log.v(TAG, "Results of validating program: "+ validateStatus[0] +"\nLog: "+glGetProgramInfoLog(programObjected));   // 打印日誌信息
             
        return validateStatus[0] !=0;
    }


接着,我們就可以使用Shader工程了, 
1
2
3
4
5
6
if(LoggerConfig.ON)
 {
     ShaderUtils.validateProgram(program);
 }
           
glUseProgram(program);



有了program標識後,我們可以使用它將數據送到Shader裏了,首先在MyGLRenderer.java的Filed域中定義
1
2
3
4
5
private static final String U_COLOR = “u_Color”;  // 標識fragment shader裏 uniform變量
private static final String A_POSITION = "a_Position";
               
private int uColorLocation;  // 存放shader裏的變量位置
private int aPositionLocation;

然後,在onSurfaceCreated裏
1
2
3
4
5
6
uColorLocation = glGetUniformLocation(program, U_COLOR);
aPositionLocation = glGetAttribLocation(program, A_POSITION);
               
vertexData.position(0);
glVertexAttribPointer(aPositionLocation, 2, GL_FLOAT, false0, vertexData);
glEnableVertexAttribArray(aPositionLocation);
首先找到 shader裏,變量的位置, 然後將vertexData的數據用glVertexAttribPointer 傳遞給Shader, 該函數的6個參數分別代表爲:
1 shader中頂點變量的位置 
2 頂點的座標屬性分量個數
3 指明瞭頂點的數據類型 
4 頂點是否歸一化(是否是整數)
5 代表,每個頂點的總數據大小(包含了所有屬性的內存), 由於這裏,我們只定義了頂點屬性一種,因此可以賦值爲0,(後面我們會介紹,頂點有多個屬性的情況,再來着重介紹該函數)
6 數據的首地址

最後,用glEnableVertexAttribArray 激活這一頂點屬性即可。
到這裏,我們將重要的頂點數據由CPU送到GPU,可供VertexShader適用。 關於uniform變量,它相當於圖像管線中的全局變量,即vertex shader和fragment shader能對其共享,它傳入的值一般不會被管線流程改變。

在onDrawFrame方法的glClear(GL_CLOR_BUFFER_BIT)後面加上:
1
glUniform4f(uColorLocation, 0,  1.0f, 0f, 0f); // 第二個參數表示offset, 後面是rgb顏色分量,這裏爲紅色
最後,glDrawArrays(GL_TRIANGLE, 0, 3); //
GL_TRIANGLE 是以三角形的形式去着色,同理還有點(GL_POINT), 線(GL_LINE)的方式。


看一下效果:




哎呦, 哪裏不對勁, 爲什麼是在右上角,沒顯示全? 這事因爲座標系沒有對,在opengl裏,座標系是圖像的中心點,即我們定義的(0, 0)點。 而(10, 0)點跑到屏幕外面去了的原因是, opengl的座標範圍一般都在(-1, 1)之間,也就是設備歸一化。 所以,我們如果想讓自己的三角形顯示在中間需要對之前定義的頂點做以下處理:
1
2
3
4
5
6
float[]  verticesWithTriangle =
 {
      -1f, -1f,
      0f, 1f,
      1f, 1f 
}
這樣就可以顯示在中間了。


接下來,通過系統時間來改變每次傳入的顏色, 可以實現以下動畫效果:
1
glUniform4f(aColorPosition, 0f, (float)(Math.abs(Math.sin(System.currentTimeMiilus()/1000)));
由於顏色的最小值爲0, 爲了不讓動畫有較長的黑屏現象,可對sin的值去絕對值,這樣由黃到黑的不斷交替變換的三角形就出來了

GIF圖,製作時取得幀較少。  大家湊合着看吧。    


記住,雖然OpenGL到目前可以爲用戶高度定製,但是那也只是限於在Shader中,而Shader之外只能調用GL的API, 所以OpenGL的調用順序一定要搞清楚。 而更需記住的是,OpenGL是一個狀態機,對於其操作,只需使用簡單的int類型,記住其返回的標識以代表我們獲取並執行了GL某個狀態。
附: 本教程可能講解的不夠深入, 但是我會繼續努力的,爭取爲大家講清楚每一個概念(概念可以慢慢講清,API沒法講清, 只有大家多實踐了)。 歡迎大家提問,交流
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章