Android OpenGL頂點及繪製基礎知識
上一次我講述了OpenGL的作用了,這次我使用了OpenGL來繪製一張桌子,其實我是將一個冰球桌拆分成幾塊來講述,現在就來繪製冰球桌的一些基本元素。在繪製的同時我順便來介紹下一些基礎知識。
一、OpenGL中頂點的作用
頂點:代表幾何物件的拐角的點,其中最主要的屬性就是其位置,代表其在空間中的位置,另外,OpenGL只能夠繪製點、直線、三角形。
點和直線我們可以理解,使用三角形是因為三角形由於其穩定的結構可以來繪製複雜的物件和紋理場景。
OpenGL在繪製的時候採用逆時針繪製比較好,這種順序叫做捲曲順序,可以優化效能。
1.使資料可以被OpenGL存取
OpenGL作為本地系統庫是直接執行在硬體上的,而我們的程式碼是執行在Dalvik上,導致OpenGL無法去讀取我們的資料,所以有兩種方案解決上述問題:
- 從Java呼叫原生代碼
- 通過使用JNI技術,一般我們呼叫GLES20包裡的技術,就是在後臺使用JNI
- 把記憶體從Java堆複製到本地堆:意思就是改變記憶體分配的方式,Java有個特殊的類集合,把Java資料複製到本地記憶體中。
private static final int BYTES_PER_FLOAT = 4;
private final FloatBuffer vertexData;
public AirHockeyRenderer() {
float[] tableVerticesWithTriangles = {
// Order of coordinates: X, Y, R, G, B
// Triangle Fan
0f, 0f, 1f, 1f, 1f,
-0.5f , -0.5f, 0.7f, 0.7f, 0.7f,
0.5f, -0.5f, 0.7f, 0.7f, 0.7f,
0.5f, 0.5f, 0.7f, 0.7f, 0.7f,
-0.5f, 0.5f, 0.7f, 0.7f, 0.7f,
-0.5f, -0.5f, 0.7f, 0.7f, 0.7f,
// Line 1
-0.5f, 0f, 1f, 0f, 0f,
0.5f, 0f, 1f, 0f, 0f,
// Mallets
0f , -0.25f, 0f, 0f, 1f,
0f, 0.25f, 1f, 0f, 0f
};
vertexData = ByteBuffer
ByteBuffer//分配了一塊本地記憶體,這塊記憶體不會被垃圾回收器管理,這裡需要知道具體分配多少記憶體塊
.allocateDirect(tableVerticesWithTriangles.length * BYTES_PER_FLOAT)
//告訴位元組緩衝區讀取的的內容序列,最重要的是保持同樣的順序
.order(ByteOrder.nativeOrder())
//得到一個可以放映底層位元組的FloatBuffer類例項,可以避免操作單獨位元組的麻煩
.asFloatBuffer();
vertexData.put(tableVerticesWithTriangles);
}
FloatBuffer用來在本地記憶體中儲存資料
vertexData = ByteBuffer
.allocateDirect(tableVerticesWithTriangles.length * BYTES_PER_FLOAT)
.order(ByteOrder.nativeOrder())
.asFloatBuffer();
vertexData.put(tableVerticesWithTriangles);
- allocateDirect:分配了一塊指定大小的本地記憶體,並且這塊本地記憶體不會受到gc管理,需要指定位元組大小
- order:告訴位元組緩衝區按照本地位元組序組織內容。
- asFloatBuffer:表示我們不願單獨操作位元組,而是一整塊的操作,通過pu可以實現將資料從Dalvik的記憶體複製到本地記憶體。
二、著色器
一般我們定義好物體的頂點,被讀取到本地記憶體中,在繪製到螢幕的時候,需要通過管道進行傳輸,這類管道其實也成為著色器。
著色器:會告訴GPU如何處理繪製資料
在OpenGL中,總共有兩種型別的著色器:
(1)頂點著色器生成每個頂點的最終位置,一旦 最終位置確定了,OpenGL就可以把這些點集合組裝成點、直線、以及三角形
(2)片段著色器為組成的點、直線、三角形的每個片段生成最終的顏色,針對每個片段,都會執行一次,類似於計算機螢幕上的一個畫素,一個長方形片段。
一旦最後顏色生成後,OpenGl會把他們寫到幀緩衝區的記憶體塊中,然後Android會把這塊幀緩衝區顯示到螢幕上。
1.建立一個頂點的著色器
attribute vec4 a_Position;
void main(){
gl_Position = a_Position;
}
著色器採用了GLSL定義,這種語法同C語言類似,當我們定義每一個單一的頂點,頂點著色器都會被呼叫一次,它被呼叫的時候,他會在a_Position屬性裡接受當前頂點的位置,這個屬性被定義為vec4屬性。
vec4是包含4個分量的向量,可以認為有x,y,z,w,其中x、y、z表示三維位置,w比較特殊,目前預設為1。
attribute表示頂點的所有屬性集合
main方法主要是著色器的入口,他所做的就是把前面定義過的位置複製到制定的輸出變數gl_Position,OpenGL會把gl_Position中儲存的值作為當前頂點的最終位置,並組裝成點。線和三角形。
2.建立第一個片段著色器
(1)光柵化技術
顯示屏成千上萬色彩原理:主要是每個畫素由3個子元件構成,分別發出紅、藍、綠三種光,利用人眼把光混在一起,從而創造出成千上萬的色彩。
光柵化:把每個點、直線和三角形分解為大量的小片段,然後對映到螢幕上,通常上一個片段對應一個畫素點,但是在高解析度下存在使用較大片段。
(2)編寫程式碼
片段著色器的目的:告訴GPU每個片段最終的顏色是什麼,對於每個片段的著色器都會被呼叫一次。
precision mediump float;
uniform vec4 u_Color;
void main(){
gl_FragColor = u_Color;
}
(3)精度定位符
precision mediump float;
uniform vec4 u_Color;
void main(){
gl_FragColor = u_Color;
}
在這個片段渲染器中,precision mediump float這句程式碼定義了所有的浮點型別資料的預設精度。
另外可以選擇lowp、mediump和highp,他們分別對應低精度、中精度和高精度,一般只有某些硬體支援高精度。
這點和頂點著色器不一樣,頂點著色器由於位置的精確度,一般預設為高精度,所以不需要再去怎麼修改,而片段著色器則採用中等精度,主要是考慮到效能和相容性。
(4)生成片段的顏色
顏色的傳遞使用一個叫uniform,每個頂點都需要設定一個,uniform也是一個四分量向量,主要面對紅、綠、藍、alpha。
main方法是片段中著色器的入口,著色器的顏色一定要給gl_GragColor賦值,OpenGL會使用這個顏色作為當前片段的最終顏色。
前面我們講述完原理,接著我們要開始編寫著色器程式,通過其來驅動OpenGL進行繪製
四、載入著色器
我們需要寫一個可以從資原始檔中讀取那些程式碼的方法
1.從資源中載入文字
/**
* 讀取著色器程式碼
* @param context 上下文
* @param resourceId 資源ID
* @return 程式碼字串
*/
public static String readTextFileFromResource(Context context,int resourceId){
StringBuilder sb = new StringBuilder();
try {
InputStream is = context.getResources().openRawResource(resourceId);
InputStreamReader inputStreamReader = new InputStreamReader(is);
BufferedReader br = new BufferedReader(inputStreamReader);
String nextLine;
while((nextLine = br.readLine())!=null){
sb.append(nextLine);
sb.append('\n');
}
} catch (IOException e) {
e.printStackTrace();
}
return sb.toString();
}
2.讀入著色器程式碼
String vertexShaderSource = TextResourceReader
.readTextFileFromResource(mContext, R.raw.simple_vertex_shader);
String fragmentShaderSource = TextResourceReader
.readTextFileFromResource(mContext,R.raw.simple_fragment_shader);
二、編譯著色器
通過一個輔助類來返回著色其物件
/**
* author: machenshuang
* <p>
* Date: 2017-11-15 15:07
* <p>
* 描述:著色器物件生成輔助類
*/
public class ShaderHelper {
private static final String TAG = "ShaderHelper";
public static int compileVertexShader(String shaderCode) {
return compileShader(GL_VERTEX_SHADER, shaderCode);
}
public static int compileFragmentShader(String shaderCode) {
return compileShader(GL_FRAGMENT_SHADER, shaderCode);
}
public static int compileShader(int type, String shaderCode) {
return 0;
}
}
1.建立一個新的著色器物件
建立一個新的著色器物件,並且檢查是否成功。
public static int compileShader(int type, String shaderCode) {
final int shaderObjectId = glCreateShader(type);
if (shaderObjectId == 0){
if (LoggerConfig.ON){
Log.d(TAG,"Could not create new shader.");
}
return 0;
}
return shaderObjectId;
}
這裡的glCreateShader呼叫建立一個新的著色器物件,並把id存入到shaderObjectId中。
OpenGL建立shader物件並且進行檢查有效性的總結:
- 首先使用一個glCreateShader()一樣來建立一個物件,會返回一個int型值
- 這個整形值就是OpenGL物件的引用,想要使用就必須把整型值傳回給OpenGL
- 返回值返回0,則表示建立失敗。
2.上傳和編譯著色器程式碼
glShaderSource(shaderObjectId,shaderCode);
3.取出編譯狀態
final int[] compileStatus = new int[1];
glGetShaderiv(shaderObjectId,GL_COMPILE_STATUS,compileStatus,0);
為了檢查編譯是否成功還是失敗,首先建立一個大小為1的陣列,接著呼叫glGetShaderiv,告訴OpenGL讀取與shaderObjectId關聯的編譯狀態,並寫入到陣列中去。
4.取出著色器資訊日誌
if (LoggerConfig.ON){
//Log.d(TAG,"Could not create new shader.");
Log.d(TAG,"Results of compiling source:"+"\n"+shaderCode+"\n:"
+ glGetShaderInfoLog(shaderObjectId));
}
return 0;
通過glGetShaderInfoLog可以獲取到一個關於著色器的有用內容
5.驗證編譯狀態並返回著色器物件ID
if (compileStatus[0]==0){
glDeleteShader(shaderObjectId);
if (LoggerConfig.ON){
Log.d(TAG,"Compilation of shader failed.");
}
return 0;
}
6.在Renderer類中編譯著色器
int vertexShader = ShaderHelper.compileVertexShader(vertexShaderSource);
int fragmentShader = ShaderHelper.compileFragmentShader(fragmentShaderSource);
回顧下流程,我們建立了ShaderHelper,並加入一個用來建立、編譯新著色器物件的方法,同時建立LoggerConfig,一個用來在單一程式碼行開啟或者關閉日誌的類,對於ShaderHelper:
- compileShader:這個compileShader(int type, String shaderCode)方法使用了著色器原始碼和型別,typ可以代表頂點著色器或者片段著色器。如果OpenGL編譯成功,就會返回著色器物件的Id,否則就會返回0。
- compileVertexShader():這個方法呼叫compileShader(int type, String shaderCode),使用GL_VERTEX_SHADER作為著色器型別
- compileFragmentShader:這個方法也呼叫了compileShader(int type, String shaderCode),使用了GL_FRAGMENT_SHADER作為著色器型別
三、將著色器連結進入OpenGL的程式
1.理解OpenGL的程式
OpenGL程式就是把一個頂點著色器和片段著色器放在一起組成單個物件,但是頂點著色器和片段著色器不一定是一對一的。
2.新建程式物件並附上著色器
final int programObjectId = glCreateProgram();
if (programObjectId == 0){
if (LoggerConfig.ON == true){
Log.d(TAG,"Could not crate new program");
}
return 0;
}
上述程式碼建立程式物件,接著附上著色器
//把頂點著色器和片段著色器都附加到程式物件上
glAttachShader(programObjectId,vertexShaderId);
glAttachShader(programObjectId,fragmentShaderId);
3.連結程式
glLinkProgram(programObjectId);
final int[] linkStatus = new int[1];
glGetProgramiv(programObjectId,GL_LINK_STATUS,linkStatus,0);
if (LoggerConfig.ON){
Log.d(TAG,"Results of linking program:\n"
+ glGetProgramInfoLog(programObjectId));
}
通過glLinkProgram將這些著色器聯合起來,同時通過glGetProgramiv檢查這個連結是成功還是錯誤。同時藉助glGetProgramInfoLog來檢視錯誤日誌
4.檢測連結狀態並返回程式物件ID
if (linkStatus[0] == 0){
glDeleteProgram(programObjectId);
if (LoggerConfig.ON){
Log.d(TAG,"Linking of program failed");
}
return 0;
}
四、最後拼接
1.驗證OpenGL程式的物件
/**
* 驗證programObjectId是否有效
* @param programObjectId OpenGL物件的id
* @return boolean
*/
public static boolean validateProgram(int programObjectId) {
glValidateProgram(programObjectId);
final int[] validateStatus = new int[1];
glGetProgramiv(programObjectId, GL_VALIDATE_STATUS, validateStatus, 0);
Log.d(TAG, "Results of validating program:" + validateStatus[0]
+ "\nLog:" + glGetProgramInfoLog(programObjectId));
return validateStatus[0] != 0;
}
2.獲得一個uniform的位置
當OpenGL程式把著色器都連結成一個程式的時候,它實際上用一個位置編號把片段著色器中定義的每一個uniform都關聯起來,利用位置編號來給著色器傳送資料。
private static final String U_COLOR = "u_Color";
private int uColorLocation;
//獲取uniform的位置
uColorLocation = glGetUniformLocation(program,U_COLOR);
通過為我們為定義的uniform的名字建立一個常量和一個用來容納它在OpenGL程式物件中的位置變數,uniform的位置變數只有當程式連結成功後才能捕獲到,並且位置是固定的,及時有同名的uniform,也不意味著他們有相同的位置。
我們通過glGetUniformLocation來獲取它的位置。
3.獲取屬性的位置
//獲取attr需要的名字和變數
private static final String A_POSITION = "a_Position";
private int aPostionLocation;
//獲取attr的位置
aPostionLocation = glGetAttribLocation(program, A_POSITION);
4.關聯屬性與頂點資料的陣列
//4.關聯屬性與頂點資料的陣列
vertexData.position(0);
glVertexAttribPointer(aPostionLocation,POSITION_COPOMENT_COUNT,GL_FLOAT,
false,0,vertexData);
我們在前面建立了一個本地緩衝區,稱為vertexData,並且已經對這個緩衝區賦予一些資料了,在我們告訴OpenGL ES從這個緩衝區讀取資料前,要確保它從開頭處讀取資料,我們通過呼叫position(0)來保證,然後呼叫glVertexAttribPointer告訴OpenGL,它可以在緩衝區vertexData中找到a_Position的對應的資料,詳見如下:
假如使用錯誤的引數,會導致嚴重的後果,所以需要傳遞一些正確的引數。
5.使能頂點陣列
//5.使能頂點陣列
glEnableVertexAttribArray(aPostionLocation);
五、在螢幕上繪製
glUniform4f(uColorLocation,1.0f,1.0f,1.0f,1.0f);
glDrawArrays(GL_TRIANGLES,0,6);
首先通過glUniform4f更新u_Color的值,並且為它4個分量設定好值,設定完了之後呼叫glDrawArrays進行繪製,第一個引數告訴OpenGL要繪製三角形,第二個引數告訴繪製的陣列起點,第三個告訴OpenGL一共讀取六個頂點。
六、總結:
這篇文章主要是展示怎樣去定義頂點座標,並將其複製到本地記憶體中,讓OpenGL去讀取它,同時瞭解了著色器的作用,如何用程式碼去實現著色器,包括著色器的建立、編譯以及同OpenGL程式物件連結起來,頂點著色器內部的屬性變數同頂點屬性陣列關聯起來,從而在螢幕上顯示出東西,基本上完成OpenGL的繪製過程,當然還有其他複雜的東西,下期再講。