翻譯文html
原文標題:OpenGL Android Lesson One: Getting Started 原文連接:www.learnopengles.com/android-les…java
這是在Android中使用OpenGL ES2的第一個教程。這一課中,咱們將一步一步跟隨代碼,學習如何建立一個OpenGL ES 2並繪製到屏幕上。 咱們還將瞭解什麼是着色器,它們如何工做,以及怎樣使用矩陣將場景轉換爲您在屏幕上看到的圖像。最後,您須要在清單文件中添加您正在使用OpenGL ES 2的說明,以告知Android應用市場支持的設備可見。android
咱們將過一道下面全部的代碼而且解釋每一部分的做用。您能夠跟着拷貝每一處的代碼片斷來建立您本身的項目,您也能夠在文章末尾下載這個已完成的項目。 在開發工具(如:Android Studio)中建立您的Android項目,名字不重要,這裏因爲這個課程我將MainActivity
改名爲LessonOneActivity
。git
咱們來看這段代碼:github
/** 保留對GLSurfaceView的引用*/
private GLSurfaceView mGLSurfaceView;
複製代碼
這個GLSurfaceView是一個特別的View,它爲咱們管理OpenGL界面而且將它繪製在Android View系統。它還添加了許多功能,使其更易於使用OpenGL,包括下面等等:編程
GLSurfaceView
使得在Android中設置和使用OpenGL相對輕鬆數組
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mGLSurfaceView = new GLSurfaceView(this);
//檢測系統是否支持OpenGL ES 2.0
final ActivityManager activityManager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
final ConfigurationInfo configurationInfo = activityManager.getDeviceConfigurationInfo();
final boolean supportsEs2 = configurationInfo.reqGlEsVersion >= 0x20000;
if (supportsEs2) {
// 請求一個OpenGL ES 2.0兼容的上下文
mGLSurfaceView.setEGLContextClientVersion(2);
// 設置咱們的Demo渲染器,定義在後面講
mGLSurfaceView.setRenderer(new LessonOneRenderer());
} else {
// 若是您想同時支持ES 1.0和2.0的話,這裏您能夠建立兼容OpenGL ES 1.0的渲染器
return;
}
setContentView(mGLSurfaceView);
}
複製代碼
在onCreate()
方法中是咱們建立OpenGL上下文以及一切開始發生的重要部分。 在咱們的onCreate()
方法中,在調用super.onCreate()
後咱們首先建立了GLSurfaceView
實例。 而後咱們須要弄清楚系統是否支持OpenGL ES 2.爲此,咱們得到一個ActivityManager
實例,它容許咱們與全局系統狀態進行交互。 而後咱們使用它獲取設備配置信息,它將告訴咱們設備是否支持OpenGL ES 2。 咱們也能夠經過傳入不一樣的渲染器來支持OpenGL ES 1.x,儘管由於API不一樣,咱們須要編寫不一樣的代碼。對於本課咱們僅僅關注支持OpenGL ES 2。緩存
一旦咱們知道設備是否支持OpenGL ES 2,咱們告訴GLSurfaceView
兼容OpenGL ES 2,而後傳入咱們的自定義渲染器。不管什麼時候調整界面或繪製新幀,系統都會調用此渲染器。app
最後,咱們調用setContentView()
設置GLSurfaceView爲顯示內容,它告訴Android這個活動內容因該被咱們的OpenGL界面填充。要入門OpenGL,就是這麼簡單。less
@Override
protected void onResume() {
super.onResume();
//Activity 必須在onResume中調用GLSurfaceView的onResume方法
mGLSurfaceView.onResume();
}
@Override
protected void onPause() {
super.onPause();
//Activity 必須在onPause中調用GLSurfaceView的onPause方法
mGLSurfaceView.onPause();
}
複製代碼
GLSurfaceView
要求咱們在ActivityonResume()
和onPause()
的父方法被調用後分別調用它的onResume()
和onPause()
方法。咱們在此添加調用以完善咱們的Activity。
在這部分,咱們來看怎樣讓OpenGL ES 2工做,以及咱們如何在屏幕上繪製東西。 在Activity中咱們傳入自定義的GLSurfaceView.Renderer到GLSurfaceView
,它將在這裏定義。 這個渲染器有三個重要的方法,每當系統事件發生時,它們將會自動被調用:
public void onSurfaceCreated(GL10 gl, EGLConfig config)
當界面第一次被建立時調用,若是咱們失去界面上下文而且以後由系統重建,也會被調用。
public void onSurfaceChanged(GL10 gl, int width, int height)
每當界面改變時被調用;例如,從縱屏切換到橫屏,在建立界面後也會被調用。
public void onDrawFrame(GL10 gl)
每當繪製新幀時被調用。
您可能注意到GL10
的實例被傳入名字是gl
。當使用OpengGL ES 2繪製時,咱們不能使用它; 咱們使用GLES20
類的靜態方法來代替。這個GL10
參數僅僅是在這裏,由於相同的接口被使用在OpenGL ES 1.x。
在咱們的渲染器能夠顯示任何內容以前,咱們須要有些東西去顯示。在OpenGL ES 2,咱們經過制定數字數組傳遞內容。這些數字能夠表示位置、顏色或任何咱們須要的。在這個Demo中,咱們將顯示三個三角形。
// 新類成員
private final FloatBuffer mTriangle1Verticels;
private final FloatBuffer mTriangle2Verticels;
private final FloatBuffer mTriangle3Verticels;
/** 每一個Float多少字節*/
private final int mBytePerFloat = 4;
/** * 初始Model數據 */
public LessonOneRenderer() {
// 這個三角形是紅色,藍色和綠色組成
final float[] triangle1VerticesData = {
// X, Y, Z,
// R, G, B, A
-0.5F, -0.25F, 0.0F,
1.0F, 0.0F, 0.0F, 1.0F,
0.5F, -0.25F, 0.0F,
0.0F, 0.0F, 1.0F, 1.0F,
0.0F, 0.559016994F, 0.0F,
0.0F, 1.0F, 0.0F, 1.0F
};
...
// 初始化緩衝區
mTriangle1Verticels = ByteBuffer.allocateDirect(triangle1VerticesData.length * mBytePerFloat).order(ByteOrder.nativeOrder()).asFloatBuffer();
...
mTriangle1Verticels.put(triangle1VerticesData).position(0);
...
}
複製代碼
那麼,這些是什麼意思?若是您曾經使用過OpenGL 1, 您可能會習慣這樣作:
glBegin(GL_TRIANGLES);
glVertex3f(-0.5f, -0.25f, 0.0f);
glColor3f(1.0f, 0.0f, 0.0f);
...
glEnd();
複製代碼
這種方法在OpenGL ES 2中不起做用。咱們不是經過一堆方法調用來定義點,而是定義一個數組。讓咱們再來看看咱們這個數組:
final float[] triangle1VerticesData = {
// X, Y, Z,
// R, G, B, A
-0.5f, -0.25f, 0.0f,
1.0f, 0.0f, 0.0f, 1.0f,
...
};
複製代碼
上面展現的表明三角形的一個點。咱們已設置好前三個數字表明位置(X,Y,Z),隨後的四個數字表明顏色(紅,綠,藍,透明度)。 您沒必要太擔憂如何定義這個數組;只要記住當咱們想繪製東西在OpenGL ES 2時,咱們須要以塊的形式傳遞數據,而不是一次傳遞一個。
// 初始化緩衝區
mTriangle1Verticels = ByteBuffer.allocateDirect(triangle1VerticesData.length * mBytePerFloat).order(ByteOrder.nativeOrder()).asFloatBuffer();
...
複製代碼
咱們在Android上使用Java進行編碼,但OpengGL ES 2底層實現其實使用C語言編寫的。 在咱們將數據傳遞給OpenGL以前,咱們須要將其轉換成它能理解的形式。 Java和native系統可能不會以相同的順序存儲它們的字節,所以咱們使用一個特殊的緩衝類並建立一個足夠大的ByteBuffer
來保存咱們的數據,並告訴它使用native字節順序存儲數據。 而後咱們將它轉換成FloatBuffer
,以便咱們可使用它來保存浮點數據。 最後,咱們將數組複製到緩衝區。
這個緩衝區的東西看起來可能很混亂,單請記住,在將數據傳遞給OpenGL以前,咱們須要作一個額外的步驟。咱們如今的緩衝區已準備好能夠用於將數據傳入OpenGL。
另外,float緩衝區在Froyo上很慢,在Gingerbread上緩慢,所以您可能不但願常常更換它們。
// new class 定義
/** * 存儲view矩陣。能夠認爲這是一個相機,咱們經過相機將世界空間轉換爲眼睛空間 * 它定位相對於咱們眼睛的東西 */
private float[] mViewMatrix = new float[16];
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
// 設置背景清理顏色爲灰色
GLES20.glClearColor(0.5F, 0.5F, 0.5F, 0.5F);
// 將眼睛放到原點以後
final float eyeX = 0.0F;
final float eyeY = 0.0F;
final float eyeZ = 1.5F;
// 咱們的眼睛望向哪
final float lookX = 0.0F;
final float lookY = 0.0F;
final float lookZ = -5.0F;
// 設置咱們的向量,這是咱們拿着相機時頭指向的方向
final float upX = 0.0F;
final float upY = 1.0F;
final float upZ = 0.0F;
// 具體場景:把手機正放桌面上,而後咱們去看屏幕裏面的東西
// 設置view矩陣,能夠說這個矩陣表明相機的位置
// 注意:在OpenGL 1中使用ModelView matrix,這是一個model和view矩陣的組合。
//在OpenGL2中,咱們選擇分別跟蹤這些矩陣
Matrix.setLookAtM(mViewMatrix, 0, eyeX, eyeY, eyeZ, lookX, lookY, lookZ, upX, upY, upZ);
...
}
複製代碼
另外一個有趣的話題是矩陣!不管您什麼時候進行3D編程,這些都將成爲您最好的朋友。所以,您須要很好的瞭解他們。
當咱們的界面被建立,咱們第一件事情是設置清理顏色爲灰色。alpha部分也設置爲灰色,但在咱們本課程中沒有進行alpha混合,所以該值未使用。咱們只須要設置一次清理顏色,以後咱們不會更改它。
咱們第二件事情是設置view矩陣。咱們使用了幾個不一樣種類的矩陣,它們都作了些重要的事情:
能夠在SongHo的OpenGL教程中找到很好的解釋。我建議您閱讀幾回直到您把握好這個想法爲止;別擔憂,我也閱讀了它好幾回!
在OpenGL 1中,模型和視圖矩陣被組合而且假設了攝像機處於(0,0,0)座標並面向Z軸方向。
咱們不須要手動構建這些矩陣,Android有一個Matrix幫助類,它能爲咱們作繁重的工做。這裏,我爲攝像機建立了一個視圖矩陣,它位於原點後,朝向遠處。
final String vertexShader =
"uniform mat4 u_MVPMatrix; \n" + // 一個表示組合model、view、projection矩陣的常量
"attribute vec4 a_Position; \n" + // 咱們將要傳入的每一個頂點的位置信息
"attribute vec4 a_Color; \n" + // 咱們將要傳入的每一個頂點的顏色信息
"varying vec4 v_Color; \n" + // 他將被傳入片斷着色器
"void main() \n" + // 頂點着色器入口
"{ \n" +
" v_Color = a_Color; \n" + // 將顏色傳遞給片斷着色器
// 它將在三角形內插值
" gl_Position = u_MVPMatrix \n" + // gl_Position是一個特殊的變量用來存儲最終的位置
" * a_Position \n" + // 將頂點乘以矩陣獲得標準化屏幕座標的最終點
"} \n";
複製代碼
在OpenGL ES 2中任何咱們想展現在屏幕中的東西都必須先通過頂點和片斷着色器,還好這些着色器並不像他們看起來的那麼複雜。頂點着色器在每一個頂點執行操做,並把這些操做的結果使用在片斷着色器作額外的每像素計算。
每一個着色器基本由輸入(input)、輸出(output)和一個程序(program)組成。 首先咱們定義一個統一(uniform),它是一個包含全部變換的組合矩陣。它是全部頂點的常量,用於將它們投影到屏幕上。 而後咱們定義了位置和顏色屬性(attribute),這些屬性將從咱們以前定義的緩存區中讀入,並指定每一個頂點的位置和顏色。 接着咱們定義了一個變量(varying),它負責在三角形中插值並傳遞到片斷着色器。當它運行到片斷着色器,它將爲每一個像素持有一個插值。
假設咱們定義了一個三角形每一個點都是紅色、綠色和藍色,咱們調整它的大小讓它佔用10像素屏幕。當片斷着色器運行時,它將爲每像素包含一個不一樣的變量(varying)顏色。在某一點上,變量(varying)將是紅色,可是在紅色和藍色之間它多是更紫的顏色。
除了設置顏色,咱們還告訴OpenGL頂點在屏幕上的最終位置。而後咱們定義片斷着色器:
final String fragmentShader =
"precision mediump float; \n" + // 咱們將默認精度設置爲中等,咱們不須要片斷着色器中的高精度
"varying vec4 v_Color; \n" + // 這是從三角形每一個片斷內插的頂點着色器的顏色
"void main() \n" + // 片斷着色器入口
"{ \n" +
" gl_FragColor = v_Color; \n" + // 直接將顏色傳遞
"} \n";
複製代碼
這是個片斷着色器,它會將東西放到屏幕上。在這個着色器中,咱們獲得的變量(varying)顏色來自頂點着色器,而後將它直接傳遞給OpenGL。該點已按像素插值,由於片斷着色器將針對每一個將要繪製的像素點運行。
更多信息:OpenGL ES 2 API快速參考卡
// 加載頂點着色器
int vertexShaderHandle = GLES20.glCreateShader(GLES20.GL_VERTEX_SHADER);
if (vertexShaderHandle != 0) {
// 傳入頂點着色器源代碼
GLES20.glShaderSource(vertexShaderHandle, vertexShader);
// 編譯頂點着色器
GLES20.glCompileShader(vertexShaderHandle);
// 獲取編譯狀態
final int[] compileStatus = new int[1];
GLES20.glGetShaderiv(vertexShaderHandle, GLES20.GL_COMPILE_STATUS, compileStatus, 0);
// 若是編譯失敗則刪除着色器
if (compileStatus[0] == 0) {
GLES20.glDeleteShader(vertexShaderHandle);
vertexShaderHandle = 0;
}
}
if (vertexShaderHandle == 0) {
throw new RuntimeException("Error creating vertex shader.");
}
複製代碼
首先,咱們建立一個着色器對象。若是成功,咱們將獲得這個對象的引用。 而後,咱們使用這個引用傳入着色器源碼而後編譯它。 咱們能夠從OpenGL獲取編譯是否成功的狀態,若是失敗咱們可使用GLES20.glGetShaderInfoLog(shader)
找到緣由。咱們按照相同的步驟加載片斷着色器。
// 建立一個程序對象並將引用放進去
int programHandle = GLES20.glCreateProgram();
if (programHandle != 0) {
// 綁定頂點着色器到程序對象中
GLES20.glAttachShader(programHandle, vertexShaderHandle);
// 綁定片斷着色器到程序對象中
GLES20.glAttachShader(programHandle, fragmentShaderHandle);
// 綁定屬性
GLES20.glBindAttribLocation(programHandle, 0, "a_Position");
GLES20.glBindAttribLocation(programHandle, 1, "a_Color");
// 將兩個着色器鏈接到程序
GLES20.glLinkProgram(programHandle);
// 獲取鏈接狀態
final int[] linkStatus = new int[1];
GLES20.glGetProgramiv(programHandle, GLES20.GL_LINK_STATUS, linkStatus, 0);
// 若是鏈接失敗,刪除這程序
if (linkStatus[0] == 0) {
GLES20.glDeleteProgram(programHandle);
programHandle = 0;
}
}
if (programHandle == 0) {
throw new RuntimeException("Error creating program.");
}
複製代碼
在咱們使用頂點和片斷着色器以前,咱們須要將它們綁定到一個程序中,它鏈接了頂點着色器的輸出和片斷着色器的輸入。這也是讓咱們從程序傳遞輸入並使用着色器繪製形狀的緣由。
咱們建立一個程序對象,若是成功綁定着色器。咱們想要將位置和顏色做爲屬性傳遞進去,所以咱們須要綁定這些屬性。而後咱們將着色器鏈接到一塊兒。
// 新類成員
/** 這將用於傳遞變換矩陣*/
private int mMVPMatrixHandle;
/** 用於傳遞model位置信息*/
private int mPositionHandle;
/** 用於傳遞模型顏色信息*/
private int mColorHandle;
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
...
// 設置程序引用,這將在以後傳遞值到程序時使用
mMVPMatrixHandle = GLES20.glGetUniformLocation(programHandle, "u_MVPMatrix");
mPositionHandle = GLES20.glGetAttribLocation(programHandle, "a_Position");
mColorHandle = GLES20.glGetAttribLocation(programHandle, "a_Color");
// 告訴OpenGL渲染的時候使用這個程序
GLES20.glUseProgram(programHandle);
}
複製代碼
在咱們成功鏈接程序後,咱們還要完成幾個任務,以便咱們能實際使用它。 第一個任務是獲取引用,由於咱們要傳遞數據到程序中。 而後咱們要告訴OpenGL在繪製時使用咱們這個程序。 因爲本課咱們僅使用了一個程序,咱們能夠將它放到onSurfaceCreated()
方法中而不是onDrawFrame()
// 新類成員
// 存放投影矩陣,用於將場景投影到2D視角
private float[] mProjectionMatrix = new float[16];
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
// 設置OpenGL界面和當前視圖相同的尺寸
GLES20.glViewport(0, 0, width, height);
// 建立一個新的透視投影矩陣,高度保持不變,而高度根據縱橫比而變換
final float ratio = (float) width / height;
final float left = -ratio;
final float right = ratio;
final float bottom = -1.0F;
final float top = 1.0F;
final float near = 1.0F;
final float far = 10.0F;
Matrix.frustumM(mProjectionMatrix, 0, left, right, bottom, top, near, far);
}
複製代碼
onSurfaceChanged()
方法至少被調用一次,每當界面改變也會被調用。由於咱們須要每當界面改變的時候重置投影矩陣,那麼onSurfaceChanged()
方法中是個理想的地方。
// 新類成員
// 存放模型矩陣,該矩陣用於將模型從對象空間(能夠認爲每一個模型開始都位於宇宙的中心)移動到世界空間
private float[] mModelMatrix = new float[16];
@Override
public void onDrawFrame(GL10 gl) {
GLES20.glClear(GLES20.GL_DEPTH_BUFFER_BIT | GLES20.GL_COLOR_BUFFER_BIT);
// 每10s完成一次旋轉
long time = SystemClock.uptimeMillis() % 10000L;
float angleDegrees = (360.0F / 10000.0F) * ((int)time);
// 畫三角形
Matrix.setIdentityM(mModelMatrix, 0);
Matrix.rotateM(mModelMatrix, 0, angleDegrees, 0.0F, 0.0F, 1.0F);
drawTriangle(mTriangle1Verticels);
...
}
複製代碼
這是實際顯示在屏幕上的內容。咱們清理屏幕,所以不會獲得任何奇怪的鏡像效應影響,咱們但願咱們的三角形在屏幕上能有平滑的動畫,一般使用時間而不是幀率更好。
實際繪製在
drawTriangle()
方法中完成
// 新的類成員
/** 爲最終的組合矩陣分配存儲空間,這將用來傳入着色器程序*/
private float[] mMVPMatrix = new float[16];
/** 每一個頂點有多少字節組成,每次須要邁過這麼一大步(每一個頂點有7個元素,3個表示位置,4個表示顏色,7 * 4 = 28個字節)*/
private final int mStrideBytes = 7 * mBytePerFloat;
/** 位置數據偏移量*/
private final int mPositionOffset = 0;
/** 一個元素的位置數據大小*/
private final int mPositionDataSize = 3;
/** 顏色數據偏移量*/
private final int mColorOffset = 3;
/** 一個元素的顏色數據大小*/
private final int mColorDataSize = 4;
/** * 從給定的頂點數據中繪製一個三角形 * @param aTriangleBuffer 包含頂點數據的緩衝區 */
private void drawTriangle(FloatBuffer aTriangleBuffer) {
aTriangleBuffer.position(mPositionOffset);
GLES20.glVertexAttribPointer(
mPositionHandle, mPositionDataSize, GLES20.GL_FLOAT, false,
mStrideBytes, aTriangleBuffer);
GLES20.glEnableVertexAttribArray(mPositionHandle);
// 傳入顏色信息
aTriangleBuffer.position(mColorOffset);
GLES20.glVertexAttribPointer(mColorHandle, mColorDataSize, GLES20.GL_FLOAT, false,
mStrideBytes, aTriangleBuffer);
GLES20.glEnableVertexAttribArray(mColorHandle);
// 將視圖矩陣乘以模型矩陣,並將結果存放到MVP Matrix(model * view)
Matrix.multiplyMM(mMVPMatrix, 0, mViewMatrix, 0, mModelMatrix, 0);
// 將上面計算好的視圖模型矩陣乘以投影矩陣,並將結果存放到MVP Matrix(model * view * projection)
Matrix.multiplyMM(mMVPMatrix, 0, mProjectionMatrix, 0, mMVPMatrix, 0);
GLES20.glUniformMatrix4fv(mMVPMatrixHandle, 1, false, mMVPMatrix, 0);
GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, 3);
}
複製代碼
您還記得咱們最初建立渲染器時定義的那些緩衝區嗎?咱們終於可使用它們了。 咱們須要使用GLES20.glVertexAttribPointer()
來告訴OpenGL怎樣使用這些數據。
咱們來看第一個使用
aTriangleBuffer.position(mPositionOffset);
GLES20.glVertexAttribPointer(
mPositionHandle, mPositionDataSize, GLES20.GL_FLOAT, false,
mStrideBytes, aTriangleBuffer);
GLES20.glEnableVertexAttribArray(mPositionHandle);
複製代碼
咱們設置緩衝區的位置偏移,它位於緩衝區的開頭。而後咱們告訴OpenGL使用這些數據並將其提供給頂點着色器並將其應用到位置屬性(a_Position)。咱們也須要告訴OpenGL每一個頂點或邁幅之間有多少個元素。
注意:邁幅(Stride)須要定義爲字節(byte),儘管每一個頂點之間咱們有7個元素(3個是位置,4個是顏色),但咱們事實上有28個字節,由於每一個浮點數(float)就是4個字節(byte)。忘記此步驟您可能沒有任何錯誤,可是你會想知道爲何您的屏幕上看不到任何內容。
最終,咱們使用了頂點屬性,往下咱們使用了下一個屬性。再日後點咱們構建一個組合矩陣,將點投影到屏幕上。咱們也能夠在頂點着色器中執行此操做,可是因爲它只須要執行一次咱們也能夠只緩存結果。 咱們使用GLES20.glUniformMatrix4fv()
方法將最終的矩陣傳入頂點着色器。 GLES20.glDrawArrays()
將咱們的點轉換爲三角形並將其繪製在屏幕上。
呼呼!這是重要的一課,若是您完成了本課,感謝您! 咱們學習了怎樣建立OpenGL上下文,傳入形狀數據,加載頂點和片斷着色器,設置咱們的轉換矩陣,最終放在一塊兒。 若是一切順利,您因該看到了相似下面的截屏。
這一課有不少須要消化的內容,您可能須要屢次閱讀這些步驟才能理解它。 OpenGL ES 2須要更多的設置才能開始,可是一旦您完成了這個過程幾回,您就會記住這個流程。
當開發的應用咱們不想在沒法運行這些應用程序的人在市場上看到它們,不然當應用程序在其設備上崩潰時,咱們可能會收到大量糟糕的評論和評分。 要防止OpenGL ES 2 應用程序出如今不支持它的設備上,你能夠在清單文件中添加:
<uses-feature android:glEsVersion="0x00020000" android:required="true" />
複製代碼
這告訴市場您的app須要有OpenGL ES 2支持,不支持的設備將會隱藏您的app。
嘗試更改動畫速度,頂點或顏色,看看會發生什麼! 能夠在Github下載本課程源代碼:下載項目
本課的編譯版本也能夠再Android市場下:google play 下載apk
「我」也編譯了個apk,方便你們下載:github download