《OpenGL ES 2.0 for Android》讀書筆記

這是一本關於OpenGL ES 2.0(如下簡稱OpenGL)快速入門的書。本書使用OpenGL2.0完成了一個3D遊戲的製做,遊戲名叫作Air Hockey,從Android開發環境的搭建到最後遊戲的開發完工,做者每一步都講述的很詳實,是一個很好的學習OpenGL的例子。🌰 本文是我在通讀全篇後寫下的總結。java

推薦的一些在線資料

OpenGL的繪圖方式 —— 點、線、三角形

咱們都知道OpenGL是用來2D或3D繪圖的,能夠繪製直線、各種圖形、各種圖像。github

OpenGL其實只能繪製三角形,肯定三個頂點,而後就能夠繪製一個三角形,多個三角形拼在一塊兒就能夠組成各式各樣的圖形,把圖片資源貼到這些各式各樣的圖形上就能夠實現圖像的繪製。編程

因此,想要用OpenGL繪製圖形,只須要肯定兩個問題:頂點、三角形上的顏色。數組

Air Hockey的效果圖

經過本文的講解,最終作出的效果以下。所有使用OpenGL繪製而成,並添加了交互邏輯。這個遊戲貌似國內不多人玩,能夠去應用商店下載一個玩一玩。bash

-w400

OpenGL座標和屏幕座標

OpenGL中的座標涉及到各類轉換操做,也是比較容易混亂的一點,這裏單獨說明。app

咱們平時理解的二維座標

本遊戲主要有一個桌子,兩個冰球,而後還有中間的一條線。 換句話說,就是有一個長方形、兩個圓點、一條直線。 根據上面的三角形繪製理論,一個長方形等於兩個三角形。因此界面的元素實際上是兩個三角形+兩個圓點+一條直線。 定義座標以下: https://cdn.wxdut.com/ ide

-w400

上面的座標表示以下:函數

float[] tableVerticesWithTriangles = {
            
            // Triangle 1
            0f, 0f,
            9f, 14f,
            0f, 14f,
            
            // Triangle 2
            0f, 0f,
            9f, 0f,
            9f, 14f
            
            // Line 1
            0f, 7f,
            9f, 7f,
            
            // Mallets
            4.5f, 2f,
            4.5f, 12f
};
複製代碼

三角形頂點描述方向

細心的會發現,上面描述三角形的三個頂點的時候是順時針方向(counter-clockwise order),也被稱爲風向(winding order)。後面默認都是這個方向。

OpenGL座標

上面咱們定義一套座標,看起來很是合理,可是有個問題:手機屏幕大小不一,縱橫座標的範圍又是多少?咱們上面定義了一個Mallet,座標爲(4.5f, 2f),在不一樣屏幕的手機上顯示效果確定不同,並且這個座標裏的4.5f2f也是隨意寫的,只有相對大小,沒有具體的參照。

事實上,OpenGL的座標範圍都是[-1, +1] https://cdn.wxdut.com/

-w400

也就是說,想經過OpenGL繪製到屏幕上的內容,其座標值必須在[-1, +1]之間,不然就沒法顯示到屏幕上。

因此咱們須要對上面定義的座標進行修改,使其可以顯示到屏幕上。

float[] tableVerticesWithTriangles = {
            // Triangle 1
            -0.5f, -0.5f,
            0.5f, 0.5f,
            -0.5f, 0.5f,
            // Triangle 2
            -0.5f, -0.5f,
            0.5f, -0.5f,
            0.5f, 0.5f,
            // Line 1
            -0.5f, 0f,
            0.5f, 0f,
            // Mallets
            0f, -0.25f,
            0f, 0.25f
};
複製代碼

這樣一來,咱們想繪製的東西就會顯示到屏幕上。

調整屏幕縱橫比

常常上一步的處理,咱們可讓東西繪製到屏幕上,可是依然會有問題。OpenGL認爲全部的屏幕的範圍都是[-1,+1] 最簡單的一個問題是,好比咱們想繪製一個正方形,座標範圍爲[-1,+1],顯示到屏幕上就變成了長方形。被拉長了,這個應該很好理解。

好比在OpenGL中,一個常規的座標範圍是正方形:

-w400
可是到了一個 720*1280的手機上就變成了下面的樣子:
-w400

爲了解決這個問題,咱們還須要一些額外處理。

咱們把OpenGL的座標稱爲normalized device coordinates,寬和高的範圍都是[-1,+1]

在一個寬320高720的屏幕上,咱們想要顯示一個全屏的長方形,則x軸座標範圍爲[0, 320],x軸座標範圍爲[0, 720],這種座標咱們定義爲virtual coordinate space。然而OpenGL能識別的座標範圍是[-1,+1],因此咱們須要把前者換算成後者,也就是把virtual coordinate space轉換成normalized device coordinates,以便於OpenGL能正常顯示。

  • 這裏有兩個關鍵詞:

    • normalized device coordinates:這個是OpenGL的座標,寬和高的範圍都是[-1,+1]
    • virtual coordinate space:這個是根據屏幕縱橫比調整以後的座標,寬的範圍爲[-1,+1],高的範圍爲[-height/width,+height/width],其中height是屏幕的高,width是屏幕的寬。

正交投影

上面提到須要把方便易懂的virtual coordinate space座標轉換成normalized device coordinates,而後傳給OpenGL繪製。這裏就用到了下面提到的正交變換API。

orthoM(float[] m, int mOffset, float left, float right, float bottom, float top, float near, float far)
複製代碼

參數解釋以下

參數 含義
float[] m 正交變換矩陣
int mOffset 偏移量,默認爲0
float left x軸最小值
float right x軸最大值
float bottom y軸最小值
float top y軸最大值
float near z軸最小值
float far z軸最大值

該函數會生成下面這個變換矩陣:

在編程時,要考慮到這一點,在設置位置時須要進行一下正交變換。

寫個僞代碼方便理解:

normalized_device_coordinates = orthoM(virtual_coordinate_space);
複製代碼

OpenGL管線(Pipeline)

我理解的管線其實就是OpenGL從用戶指定的頂點數據,一直到最終顯示到手機屏幕上,中間所須要經歷的步驟,把這些步驟按照時間前後順序串成一條線,稱爲管線。

爲了理解上面的管線圖,咱們取圖像上的一個像素點的顯示過程來講明。 圖像上的一個像素點要想最終顯示到顯示器上,有兩個關鍵點:

① 像素點顯示的位置 ② 像素點顯示的顏色值

那上面的圖就能夠這麼理解:

上面的兩個步驟分別經過兩種不一樣的Shader處理:

① 使用Vertex Shader肯定每個點的具體顯示的位置 ② 使用Fragment Shader肯定每個點的具體顏色值

Vertex Shader和Fragment Shader的關係能夠用下圖表示。

-w400

Shader

Shader有兩種,分別爲Vertex Shader和Fragment Shader。

Shader有專門的語言,OpenGL Shading Language,簡稱GLSL。語法相似於C語言,通常在/res/raw文件夾下,命名爲xxx.glsl。若是對glsl語言不熟悉的話牆裂建議先看一下OpenGL Shading Language(GLSL)語法一覽

Shader一般用xxx.glsl文件描述,該文件中通常形式以下:

attribute vec4 a_Position;
void main()
{
    gl_Position = a_Position;
}
複製代碼

其中main方法是Shader的入口函數,當Shader被調用時main方法就會被執行。

Shader的初始化

定義Shader

有了上面的glsl的基本語法知識後,咱們開始嘗試用glsl來表達Shader。

// AirHockey1/res/raw/simple_vertex_shader.glsl
attribute vec4 a_Position;
void main()
{
    gl_Position = a_Position;
}
複製代碼

上面的Vertex Shader很簡單,就是聲明瞭一個vec4的變量a_Position,而且在Shader執行時進行賦值操做gl_Position = a_Position;

// AirHockey1/res/raw/simple_fragment_shader.glsl
precision mediump float;
uniform vec4 u_Color;
void main()
{
    gl_FragColor = u_Color;
}
複製代碼

上面的Fragment Shader也很簡單,就是聲明瞭一個vec4的變量u_Color,而且在Shader執行時進行賦值操做gl_FragColor = u_Color;

建立Shader

final int shaderObjectId = glCreateShader(type);
    if (shaderObjectId == 0) {
        if (LoggerConfig.ON) {
        Log.w(TAG, "Could not create new shader.");
    }
    return 0;
}
複製代碼

調用APIglCreateShader建立一個空的Shader,其中參數type有兩種,分別爲GL_VERTEX_SHADERGL_FRAGMENT_SHADER,分別表示Vertex Shader和Fragment Shader。

該函數返回一個int,是Shader的惟一標識ID,咱們後面能夠用它來找到這個Shader,相似於Java中的指針同樣,指向了建立的對象。

該函數默認返回一個大於0的數值,若是返回0,則表示建立失敗。

填充Shader

上一步建立了一個空的Shader,id爲shaderObjectId,下面給Shader填充具體的邏輯。

glShaderSource(shaderObjectId, shaderCode);
複製代碼

調用APIglShaderSource爲Shader填充具體的邏輯,其中,參數shaderObjectId是Shader的惟一標識ID,在調用glCreateShader建立Shader的時候獲得的;參數shaderCode是指上面用glsl寫的Shader代碼。

編譯Shader

上面咱們建立並填充了Shader,下面對Shader進行編譯。

glCompileShader(shaderObjectId);
複製代碼

調用APIglCompileShader對Shader進行編譯,其中,參數shaderObjectId是Shader的惟一標識ID,在調用glCreateShader建立Shader的時候獲得的。

不過這一步不必定會成功,好比你的shaderCode寫得有問題,咱們須要確保這一步成功才能繼續下面的工做。

OpenGL不會自動throw Exception,不過咱們可使用API獲取執行狀態。

final int[] compileStatus = new int[1];
glGetShaderiv(shaderObjectId, GL_COMPILE_STATUS, compileStatus, 0);

if (LoggerConfig.ON) {
    // Print the shader info log to the Android log output.
    Log.v(TAG, "Results of compiling source:" + "\n" + shaderCode + "\n:"
        + glGetShaderInfoLog(shaderObjectId));
}

if (compileStatus[0] == 0) {
    // If it failed, delete the shader object.
    glDeleteShader(shaderObjectId);
    if (LoggerConfig.ON) {
        Log.w(TAG, "Compilation of shader failed.");
    }
    return 0;
}
複製代碼

連接Shader

咱們知道Vertex Shader和Fragment Shader分別是OpenGL管線中的重要的兩步,Vertex Shader肯定位置,Fragment Shader肯定該位置的顏色。它們之間是一一對應,不可或缺的,咱們須要將它們連接起來。

在OpenGL中,Vertex Shader和Fragment Shader連接到一塊兒,成爲一個Program。

建立Program
final int programObjectId = glCreateProgram();
    if (programObjectId == 0) {
        if (LoggerConfig.ON) {
        Log.w(TAG, "Could not create new program");
    }
    return 0;
}
複製代碼

調用APIglCreateProgram建立一個空的Program。

該函數返回一個int,是Shader的惟一標識ID,咱們後面能夠用它來找到這個Shader,相似於Java中的指針同樣,指向了建立的對象。

該函數默認返回一個大於0的數值,若是返回0,則表示建立失敗。

綁定Shader

上面建立了Program,下面給這個Program綁定Vertex Shader和Fragment Shader。

glAttachShader(programObjectId, vertexShaderId);
glAttachShader(programObjectId, fragmentShaderId);
複製代碼

調用APIglAttachShader爲Program綁定Shader。參數也很好理解了,programObjectId是Program的惟一ID標識,vertexShaderId和fragmentShaderId是指Shader的惟一ID標識。

連接Shader

給Program填充了Shader以後就能夠進行連接了。

glLinkProgram(programObjectId);
複製代碼

一樣的,連接Shader也不必定會成功,咱們須要驗證下。

final int[] linkStatus = new int[1];
glGetProgramiv(programObjectId, GL_LINK_STATUS, linkStatus, 0);

if (LoggerConfig.ON) {
    // Print the program info log to the Android log output.
    Log.v(TAG, "Results of linking program:\n"
        + glGetProgramInfoLog(programObjectId));
}

if (linkStatus[0] == 0) {
    // If it failed, delete the program object.
    glDeleteProgram(programObjectId);
    if (LoggerConfig.ON) {
        Log.w(TAG, "Linking of program failed.");
    }
    return 0;
}
複製代碼
驗證Program

因爲配置不一樣,有可能設置的Program不兼容,這裏驗證下。

public static boolean validateProgram(int programObjectId) {

    glValidateProgram(programObjectId);
    
    final int[] validateStatus = new int[1];
    glGetProgramiv(programObjectId, GL_VALIDATE_STATUS, validateStatus, 0);
    
    Log.v(TAG, "Results of validating program: " + validateStatus[0]
        + "\nLog:" + glGetProgramInfoLog(programObjectId));
    
    return validateStatus[0] != 0;
}
複製代碼

Shader的賦值

給Vertex Shader賦值

上面咱們進行了Shader的初始化,並連接成了Program。下面咱們經過對兩個Shader進行賦值來實現繪製效果。

private static final String A_POSITION = "a_Position";
private int aPositionLocation;

// 得到Vertex Shader的位置參數的地址,以便於後續賦值
aPositionLocation = glGetAttribLocation(program, A_POSITION);

// 給Vertex Shader賦值
vertexData.position(0);
glVertexAttribPointer(aPositionLocation, POSITION_COMPONENT_COUNT, GL_FLOAT,
false, 0, vertexData);

// 通知OpenGL使用頂點數據進行繪製
glEnableVertexAttribArray(aPositionLocation);
複製代碼

這裏有個重要的API——glVertexAttribPointer,用來給Vertex Shader賦值。它的參數比較多,說明以下:

函數定義:glVertexAttribPointer(int index, int size, int type, boolean normalized, int stride, Buffer ptr)

參數說明:

參數 類型 做用
index int Vertex Shader的a_Position的位置,也就是上面取到的aPositionLocation
size int 這個是指Position的維度,好比二維座標就填2,三維座標就填3,以此類推
type int 這裏是指Position的數據類型,float就填GL_FLOAT,int就填GL_INT,以此類推
normalized boolean 這裏只有Position的數據類型爲int的時候纔會用到,其餘場景爲false
stride int 跨度,指的是相鄰兩個Position數據之間的間隔,默認填0
ptr Buffer Position的數據buffer

給Fragment Shader賦值

private static final String U_COLOR = "u_Color";
private int uColorLocation;

uColorLocation = glGetUniformLocation(program, U_COLOR);
複製代碼

顯示到屏幕上

咱們先來回顧一下Vertex數組:

float[] tableVerticesWithTriangles = {
// Triangle 1
0f, 0f,
9f, 14f,
0f, 14f,
// Triangle 2
0f, 0f,
9f, 0f,
9f, 14f,
// Line 1
0f, 7f,
9f, 7f,
// Mallets
4.5f, 2f,
4.5f, 12f
};
複製代碼
// 指定顏色,顏色寫入的位置爲uColorLocation,顏色值的rgba分別爲1.0f, 1.0f, 1.0f, 1.0f。
glUniform4f(uColorLocation, 1.0f, 1.0f, 1.0f, 1.0f);

// 繪製兩個三角形,從數組的下標0開始,繪製六個頂點。
glDrawArrays(GL_TRIANGLES, 0, 6);

// 繪製兩條線,從數組的下標6開始,繪製兩個頂點。
glUniform4f(uColorLocation, 1.0f, 0.0f, 0.0f, 1.0f);
glDrawArrays(GL_LINES, 6, 2);

// Draw the first mallet blue.
glUniform4f(uColorLocation, 0.0f, 0.0f, 1.0f, 1.0f);
glDrawArrays(GL_POINTS, 8, 1);
// Draw the second mallet red.
glUniform4f(uColorLocation, 1.0f, 0.0f, 0.0f, 1.0f);
glDrawArrays(GL_POINTS, 9, 1);
複製代碼

Shader使用小結

本章節剛開始講到了Shader的分類、glsl語言。

後面詳述了Shader的建立、填充、編譯、連接,還講到了Shader的賦值與最終顯示。

Texture

咱們上面使用簡單的Vertex Shader和Fragment Shader實現了基本圖形和顏色的繪製,可是這遠遠不夠。 若是想繪製一些圖片呢,就須要用到新的東西——Texture。

上面提到,Fragment Shader有衆多的fragment,一個fragment相似於一個像素。Texture包含了不少的texel,這texel也能夠理解成一個像素點。

使用Texture能夠把各種圖片加載到OpenGL中進而進行顯示,從而實現炫酷的遊戲場景。

-w400
舉個例子,上圖中,遊戲的背景是一張圖片,而不是簡單的純色背景。

  • 注意

在OpenGL ES 2.0中,Texture不必定要是正方形,可是S和T的值必須是2的n次方。

Texture座標和圖片座標

Texture自己也是有座標的,對於二維Texture來講,兩個維度分別被稱爲S和T,再也不是x和y軸。 而且,S和T軸的範圍都是[0,1]

-w400

除了Texture有座標外,圖片自己也有座標,座標以下。其中,左上角爲原點,y軸向下,x軸向左。

-w400

  • 關於座標這一點必定要搞清楚,否則後面各類變換會懵逼的。總結下其實也沒幾個座標。
  1. OpenGL有座標範圍,座標值範圍是[-1,1],中心爲原點。
  2. Texture有座標,座標值範圍是[0,1],左下角爲原點。
  3. 圖片有座標,左上角爲原點,x軸向左,y軸向下。

把圖片加載到Texture中

使用Texture,第一步固然是建立並加載圖片進來。

建立Texture

建立一個空的Texture的方式和上面建立Shader差很少,也是直接調用API,而後底層建立一個Texture並返回Texture的惟一ID標識,咱們後面能夠根據這個ID來得到這個Texture。

一樣的,若是ID爲0,則建立失敗,正常狀況ID是大於0的。

final int[] textureObjectIds = new int[1];
glGenTextures(1, textureObjectIds, 0);
if (textureObjectIds[0] == 0) {
    if (LoggerConfig.ON) {
        Log.w(TAG, "Could not generate a new OpenGL texture object.");
    }
    return 0;
}
複製代碼

這裏有一個新的API:glGenTextures,參數說明以下:

參數 含義
int n 給新建立的Texture返回n個惟一ID標識,這裏只須要填1就好了
int[] textures OpenGL會將Texture的ID存放到這個數組裏,固然,textures.length >= n + offset
int offset 參數textures的offset,textures.length >= n + offset,你懂的

加載Bitmap並綁定到Texture

這裏很顯然,應該是調用API把Bitmap和Texture綁定起來進行顯示,邏輯很簡單。

OpenGL在同一時間只能綁定一個Texture,因此這裏先把texture綁定到OpenGL,而後再將bitmap傳給OpenGL,就能夠實現綁定操做。

glBindTexture(GL_TEXTURE_2D, textureObjectIds[0]);
texImage2D(GL_TEXTURE_2D, 0, bitmap, 0);
// OpenGL會copy一份bitmap,因此這裏直接回收掉就行
bitmap.recycle();
glGenerateMipmap(GL_TEXTURE_2D);
複製代碼

使用Texture

上面咱們已經把bitmap和texture綁定了,下面使用這個texture進行繪製。

// 使用該Texture
glActiveTexture(GL_TEXTURE0);
glUniform1i(uTextureUnitLocation, 0);
複製代碼

建立Texture相關的Shader

建立Vertex Shader

uniform mat4 u_Matrix;
attribute vec4 a_Position;
attribute vec2 a_TextureCoordinates;
varying vec2 v_TextureCoordinates;
void main()
{
    v_TextureCoordinates = a_TextureCoordinates;
    gl_Position = u_Matrix * a_Position;
}
複製代碼

建立Fragment Shader

precision mediump float;
uniform sampler2D u_TextureUnit;
varying vec2 v_TextureCoordinates;
void main()
{
    gl_FragColor = texture2D(u_TextureUnit, v_TextureCoordinates);
}
複製代碼

u_TextureUnit是Texture的數據,v_TextureCoordinates是Texture的某個位置,texture2D返回這個Texture在v_TextureCoordinates這個位置的顏色,並賦值給gl_FragColor,進而繪製到屏幕。

OpenGL繪圖實例

順手寫了兩個demo,有須要的能夠參考下。OpenGL-ES-2.0-for-Android

主要看一下下面兩個功能:

參考文檔

www.learnopengles.com/understandi…

相關文章
相關標籤/搜索