本文從下面鏈接翻譯過來:
Android Lesson One: Getting Started
這是在Android上使用OpenGL ES2的第一個教程。 在本課中,我們將逐步介紹代碼,並瞭解如何創建OpenGL ES2上下文並繪製到屏幕上。 我們還將瞭解着色器是什麼以及它們如何工作,以及如何使用矩陣將場景轉換爲您在屏幕上看到的圖像。 最後,您需要在清單文件(AndroidManifest.xml)中添加使用OpenGL ES2的說明,以告知Android應用市場你的應用僅對支持的設備可見。
開發環境搭建
在開始之前,您需要確保在計算機上安裝了以下工具:
1.Java環境
2.Android studio開發工具
3.Android真機設備一部
開始
我們將查看下面的所有代碼並解釋每個部分的作用。 您可以通過創建自己的項目逐段複製代碼,也可以在課程的最後下載完整的項目代碼。 安裝完工具後,在Android Studio中創建一個新的Android項目。 名稱無關緊要,但對於本課,我將應用的入口稱爲LessonOneActivity。
我們來看看代碼:
/** GLSurfaceView的一個引用 */
private GLSurfaceView mGLSurfaceView;
GLSurfaceView是一個特殊的視圖,它爲我們管理OpenGL表面並將其繪製到Android視圖系統中。 它還增加了許多功能,使其更易於使用OpenGL,包括但不限於:
- 它爲OpenGL提供專用的渲染線程,以便不影響到Android主線程。
- 它支持連續或按需渲染。
- 它使用EGL(OpenGL和底層窗口系統之間的接口)來處理屏幕設置。
GLSurfaceView使得從Android設置和使用OpenGL相對輕鬆。
@Override
protected void onCreate(@Nullable Bundle 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);
// 將渲染器設置爲我們的演示渲染器,定義如下
mGLSurfaceView.setRenderer(new LessonOneRenderer());
} else {
//如果您想同時支持ES 1和ES 2,則可以在此處創建與OpenGL ES 1.x兼容的渲染器。
return;
}
setContentView(mGLSurfaceView);
}
LessonOneActivity的onCreate()方法是創建OpenGL上下文以及一切開始重要部分。在onCreate()中,在調用父類之後做的第一件事是創建我們的GLSurfaceView。然後我們需要弄清楚系統是否支持OpenGL ES2。爲此,我們得到一個ActivityManager實例,它允許我們與全局系統狀態進行交互。然後我們可以使用它來獲取設備配置信息,它將告訴我們設備是否支持OpenGL ES2。
一旦我們知道設備支持OpenGL ES2,我們告訴GLSurfaceView我們想要一個OpenGL ES2兼容表面,然後我們傳入一個自定義渲染器。無論何時調整表面或繪製新幀,系統都會調用此渲染器。我們也可以通過傳入不同的渲染器來支持OpenGL ES1.x,但是由於API不同,我們需要編寫不同的代碼。在本課中,我們只關注支持OpenGL ES2。
最後,我們將內容視圖設置爲GLSurfaceView,它告訴Activity的內容應由我們的OpenGL表面填充。要進入OpenGL,就這麼簡單!
@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需要我們在Activity的onResume()和onPaused()時調用它的onResume()和onPause()方法。 我們在此處添加調用以完善我們的Activity。
可視化3D世界
在本節中,我們將開始研究OpenGL ES2的工作原理以及如何開始在屏幕上繪製內容。 在LessonOneActivity中,我們將自定義的GLSurfaceView.Renderer傳遞給GLSurfaceView。 渲染器有三個重要的方法,系統會自動調用這些方法:
@Override
public void onSurfaceCreated(GL10 glUnused, EGLConfig eglConfig) {
}
首次創建Surface時會調用此方法。 如果我們丟失Surface上下文並且稍後由系統重新創建它,也將調用此方法。
@Override
public void onSurfaceChanged(GL10 glUnused, int width, int height) {
}
只要Surface發生變化,就會調用它; 例如,從縱向切換到橫向時。 在創建Surface後也會調用它。
@Override
public void onDrawFrame(GL10 glUnused) {
}
只要是繪製新幀的時候就會調用它。
您可能已經注意到傳入的GL10實例稱爲glUnused。 使用OpenGL ES2繪圖時我們不使用它; 相反,我們使用GLES20類的靜態方法。 GL10參數僅在那裏,因爲相同的接口用於OpenGL ES1.x.
在我們的渲染器可以顯示任何內容之前,我們需要顯示一些內容。 在OpenGL ES2中,我們通過指定數字數組傳遞內容。 這些數字可以表示位置,顏色或我們需要的任何其他內容。 在這個演示中,我們將顯示三個三角形。
// New class members
/** Store our model data in a float buffer. */
private final FloatBuffer mTriangle1Vertices;
private final FloatBuffer mTriangle2Vertices;
private final FloatBuffer mTriangle3Vertices;
/** How many bytes per float. */
private final int mBytesPerFloat = 4;
/**
* Initialize the model data.
*/
public LessonOneRenderer() {
// This triangle is red, green, and blue.
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};
...
// Initialize the buffers.
mTriangle1Vertices = ByteBuffer.allocateDirect(triangle1VerticesData.length * mBytesPerFloat)
.order(ByteOrder.nativeOrder()).asFloatBuffer();
...
mTriangle1Vertices.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 ES2中不起作用。我們不是通過一堆方法調用來定義點,而是定義一個數組。 讓我們再看看我們的數組:
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),後四個數字代表顏色(紅色,綠色,藍色和alpha(透明度))。 您不必太擔心如何定義此數組; 請記住,當我們想要在OpenGL ES2中繪製內容時,我們需要以塊的形式傳遞數據,而不是一次傳遞一個。
理解緩衝區
// Initialize the buffers.
mTriangle1Vertices = ByteBuffer.allocateDirect(triangle1VerticesData.length * mBytesPerFloat)
.order(ByteOrder.nativeOrder()).asFloatBuffer();
...
mTriangle1Vertices.put(triangle1VerticesData).position(0);
我們在Android上使用Java進行編碼,但OpenGL ES2的底層實現實際上是用C語言編寫的。在我們將數據傳遞給OpenGL之前,我們需要將其轉換爲一種它能夠理解的形式。 Java和本機系統可能不會以相同的順序存儲它們的字節,因此我們使用一組特殊的緩衝區類並創建一個足夠大的ByteBuffer來保存我們的數據,並告訴它使用本機字節順序存儲其數據。 然後我們將它轉換爲FloatBuffer,以便我們可以使用它來保存浮點數據。 最後,我們將數組複製到緩衝區中。
這個緩衝區的東西可能看起來很混亂(當我第一次遇到它時候也是這樣認爲!),但請記住,在將數據傳遞給OpenGL之前,我們需要做一個額外的步驟。 我們的緩衝區現在可以用於將數據傳遞到OpenGL。
另外,float buffers are slow on Froyo and moderately faster on Gingerbread,所以你可能不希望經常更換它們。
理解矩陣
// New class definitions
/**
* Store the view matrix. This can be thought of as our camera. This matrix transforms world space to eye space;
* it positions things relative to our eye.
*/
private float[] mViewMatrix = new float[16];
@Override
public void onSurfaceCreated(GL10 glUnused, EGLConfig config) {
// Set the background clear color to gray.
GLES20.glClearColor(0.5f, 0.5f, 0.5f, 0.5f);
// Position the eye behind the origin.
final float eyeX = 0.0f;
final float eyeY = 0.0f;
final float eyeZ = 1.5f;
// We are looking toward the distance
final float lookX = 0.0f;
final float lookY = 0.0f;
final float lookZ = -5.0f;
// Set our up vector. This is where our head would be pointing were we holding the camera.
final float upX = 0.0f;
final float upY = 1.0f;
final float upZ = 0.0f;
// Set the view matrix. This matrix can be said to represent the camera position.
// NOTE: In OpenGL 1, a ModelView matrix is used, which is a combination of a model and
// view matrix. In OpenGL 2, we can keep track of these matrices separately if we choose.
Matrix.setLookAtM(mViewMatrix, 0, eyeX, eyeY, eyeZ, lookX, lookY, lookZ, upX, upY, upZ);
...
}
另一個“有趣”的主題是矩陣! 無論何時進行3D編程,這些都將成爲您最好的朋友,因此您需要很好地瞭解它們。
當我們的Surface被創建時,我們要做的第一件事就是將清除屏幕的顏色設置爲灰色。 alpha部分也已設置爲灰色,但我們在本課程中沒有進行Alpha混合,因此該值未使用。 我們只需要設置一次清除屏幕的顏色顏色,因爲我們以後不會更改它。
我們要做的第二件事是設置我們的視圖矩陣。 我們使用了幾種不同類型的矩陣,它們都做了一些重要的事情:
- 模型矩陣。 該矩陣用於在“世界”中的某處放置模型。例如,如果您有一輛汽車的模型,並且您希望它位於東邊1000米處,您將使用模型矩陣來執行此操作。
- 視圖矩陣。 該矩陣代表相機。如果我們想要查看我們位於東邊1000米處的汽車,我們也必須向東移動1000米(另一種思考方式是我們保持靜止,世界其他地方向西移動1000米)。我們使用視圖矩陣來做到這一點。
- 投影矩陣。 由於我們的屏幕是二維平面的,我們需要進行最後的轉換,將我們的視圖“投影”到我們的屏幕上並獲得漂亮的3D視角。 這就是投影矩陣的用途。
你可以找到對矩陣的一個很好的解釋SongHo’s OpenGL Tutorials。 我建議你多閱讀幾次,直到你理解爲止; 別擔心,我也閱讀了好幾次才理解它!
在OpenGL 1中,模型矩陣與視圖矩陣是結合在一起的。Camera被假設放在了(0,0,0)位置並且面向-Z方向。
我們不需要手工構建這些矩陣。 Android有一個Matrix幫助程序類,可以爲我們做繁重的工作。 在這裏,我爲攝像機創建了一個視圖矩陣,它位於原點後面,朝向遠處。
定義頂點和片元着色器
final String vertexShader =
"uniform mat4 u_MVPMatrix; \n" // A constant representing the combined model/view/projection matrix.
+ "attribute vec4 a_Position; \n" // Per-vertex position information we will pass in.
+ "attribute vec4 a_Color; \n" // Per-vertex color information we will pass in.
+ "varying vec4 v_Color; \n" // This will be passed into the fragment shader.
+ "void main() \n" // The entry point for our vertex shader.
+ "{ \n"
+ " v_Color = a_Color; \n" // Pass the color through to the fragment shader.
// It will be interpolated across the triangle.
+ " gl_Position = u_MVPMatrix \n" // gl_Position is a special variable used to store the final position.
+ " * a_Position; \n" // Multiply the vertex by the matrix to get the final point in
+ "} \n"; // normalized screen coordinates.
在OpenGL ES2中,我們想要在屏幕上顯示的任何內容首先必須通過頂點和片元着色器。好消息是這些着色器並不像它們看起來那麼複雜。頂點着色器對每個頂點執行操作,這些操作的結果用於片元着色器,對每個像素執行額外的計算。
每個着色器基本上由輸入,輸出和程序組成。首先,我們定義一個uniform類型變量,它是一個包含所有變換的組合矩陣。用於將所有頂點投影到屏幕上。然後我們爲位置和顏色定義兩個attribute類型變量。這些屬性將從我們之前定義的緩衝區中讀取,並指定每個頂點的位置和顏色。然後我們定義一個varying類型變量,它在三角形上進行插值計算,並將其傳遞給片元着色器。當它到達片元着色器時,它將爲每個像素保存一個插值。
假設我們定義了一個三角形的三個點分別是紅色、綠色和藍色,我們調整它的大小,使其佔據屏幕上的10個像素。 當片元着色器運行時,它將爲每個像素包含不同的varying類型顏色。 在某一點上, varying 類型顏色可能是紅色,也可能是在紅色和藍色之間,還有可能是更紫色的顏色。
除了設置顏色外,我們還告訴OpenGL頂點的最終位置應該在屏幕上的具體位置。 然後我們定義片元着色器:
final String fragmentShader =
"precision mediump float; \n" // Set the default precision to medium. We don't need as high of a
// precision in the fragment shader.
+ "varying vec4 v_Color; \n" // This is the color from the vertex shader interpolated across the
// triangle per fragment.
+ "void main() \n" // The entry point for our fragment shader.
+ "{ \n"
+ " gl_FragColor = v_Color; \n" // Pass the color directly through the pipeline.
+ "} \n";
這是片元着色器,它實際上會將東西顯示到屏幕上。 在這個着色器中,我們從頂點着色器中獲取varying類型的顏色值,然後直接將其傳遞給OpenGL。 該點已經按像素插值,因爲片元着色器針對將要繪製的每個像素運行。
更多信息請參考OpenGL ES 2 quick reference card。
將着色器加載到OpenGL中
// Load in the vertex shader.
int vertexShaderHandle = GLES20.glCreateShader(GLES20.GL_VERTEX_SHADER);
if (vertexShaderHandle != 0)
{
// Pass in the shader source.
GLES20.glShaderSource(vertexShaderHandle, vertexShader);
// Compile the shader.
GLES20.glCompileShader(vertexShaderHandle);
// Get the compilation status.
final int[] compileStatus = new int[1];
GLES20.glGetShaderiv(vertexShaderHandle, GLES20.GL_COMPILE_STATUS, compileStatus, 0);
// If the compilation failed, delete the shader.
if (compileStatus[0] == 0)
{
GLES20.glDeleteShader(vertexShaderHandle);
vertexShaderHandle = 0;
}
}
if (vertexShaderHandle == 0)
{
throw new RuntimeException("Error creating vertex shader.");
}
首先,我們創建着色器對象。如果成功,我們將獲得對象的引用。然後我們使用這個引用來傳遞着色器源代碼,然後我們編譯它。我們可以從OpenGL獲取狀態,看看它是否成功編譯。如果有錯誤,我們可以使用GLES20.glGetShaderInfoLog(着色器)找出原因。 我們按照相同的步驟加載片元着色器。
將頂點和片段着色器鏈接到一個程序中
// Create a program object and store the handle to it.
int programHandle = GLES20.glCreateProgram();
if (programHandle != 0)
{
// Bind the vertex shader to the program.
GLES20.glAttachShader(programHandle, vertexShaderHandle);
// Bind the fragment shader to the program.
GLES20.glAttachShader(programHandle, fragmentShaderHandle);
// Bind attributes
GLES20.glBindAttribLocation(programHandle, 0, "a_Position");
GLES20.glBindAttribLocation(programHandle, 1, "a_Color");
// Link the two shaders together into a program.
GLES20.glLinkProgram(programHandle);
// Get the link status.
final int[] linkStatus = new int[1];
GLES20.glGetProgramiv(programHandle, GLES20.GL_LINK_STATUS, linkStatus, 0);
// If the link failed, delete the program.
if (linkStatus[0] == 0)
{
GLES20.glDeleteProgram(programHandle);
programHandle = 0;
}
}
if (programHandle == 0)
{
throw new RuntimeException("Error creating program.");
}
在我們使用頂點和片元着色器之前,我們需要將它們綁定到一個程序中。 這是將頂點着色器的輸出與片元着色器的輸入相連接的內容。 這也是讓我們從程序傳遞輸入並使用着色器繪製形狀的原因。
我們創建一個新的程序對象,如果成功,我們就會附加我們的着色器。 我們希望將位置和顏色作爲屬性傳遞,因此我們需要綁定這些屬性。 然後我們將着色器鏈接在一起。
//New class members
/** This will be used to pass in the transformation matrix. */
private int mMVPMatrixHandle;
/** This will be used to pass in model position information. */
private int mPositionHandle;
/** This will be used to pass in model color information. */
private int mColorHandle;
@Override
public void onSurfaceCreated(GL10 glUnused, EGLConfig config)
{
...
// Set program handles. These will later be used to pass in values to the program.
mMVPMatrixHandle = GLES20.glGetUniformLocation(programHandle, "u_MVPMatrix");
mPositionHandle = GLES20.glGetAttribLocation(programHandle, "a_Position");
mColorHandle = GLES20.glGetAttribLocation(programHandle, "a_Color");
// Tell OpenGL to use this program when rendering.
GLES20.glUseProgram(programHandle);
}
在我們成功鏈接着色器程序之後,我們完成了幾項任務,以便我們可以實際使用它。 第一個任務是獲取引用,以便我們可以將數據傳遞到着色器程序中。 然後我們告訴OpenGL在繪圖時使用這個着色器程序。 由於我們在本課中只使用了一個着色器程序,因此我們可以將它放在onSurfaceCreated()而不是onDrawFrame()中。
設置透視投影
// New class members
/** Store the projection matrix. This is used to project the scene onto a 2D viewport. */
private float[] mProjectionMatrix = new float[16];
@Override
public void onSurfaceChanged(GL10 glUnused, int width, int height)
{
// Set the OpenGL viewport to the same size as the surface.
GLES20.glViewport(0, 0, width, height);
// Create a new perspective projection matrix. The height will stay the same
// while the width will vary as per aspect ratio.
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()被調用至少一次,並且每當我們的surface被改變時。 由於我們只需要在我們投影到的屏幕發生變化時重置投影矩陣,onSurfaceChanged()就是理想的選擇。
把東西畫到屏幕上!
// New class members
/**
* Store the model matrix. This matrix is used to move models from object space (where each model can be thought
* of being located at the center of the universe) to world space.
*/
private float[] mModelMatrix = new float[16];
@Override
public void onDrawFrame(GL10 glUnused)
{
GLES20.glClear(GLES20.GL_DEPTH_BUFFER_BIT | GLES20.GL_COLOR_BUFFER_BIT);
// Do a complete rotation every 10 seconds.
long time = SystemClock.uptimeMillis() % 10000L;
float angleInDegrees = (360.0f / 10000.0f) * ((int) time);
// Draw the triangle facing straight on.
Matrix.setIdentityM(mModelMatrix, 0);
Matrix.rotateM(mModelMatrix, 0, angleInDegrees, 0.0f, 0.0f, 1.0f);
drawTriangle(mTriangle1Vertices);
...
}
這是實際顯示在屏幕上的內容。 首先清除了屏幕,因此我們沒有得到任何奇怪的鏡面效果。我們希望三角形在屏幕上能有平滑的動畫,我們使用時間旋轉三角形。 每當您在屏幕上製作動畫時,通常最好使用時間而不是幀速率。
實際繪圖在drawTriangle中完成:
// New class members
/** Allocate storage for the final combined matrix. This will be passed into the shader program. */
private float[] mMVPMatrix = new float[16];
/** How many elements per vertex. */
private final int mStrideBytes = 7 * mBytesPerFloat;
/** Offset of the position data. */
private final int mPositionOffset = 0;
/** Size of the position data in elements. */
private final int mPositionDataSize = 3;
/** Offset of the color data. */
private final int mColorOffset = 3;
/** Size of the color data in elements. */
private final int mColorDataSize = 4;
/**
* Draws a triangle from the given vertex data.
*
* @param aTriangleBuffer The buffer containing the vertex data.
*/
private void drawTriangle(final FloatBuffer aTriangleBuffer)
{
// Pass in the position information
aTriangleBuffer.position(mPositionOffset);
GLES20.glVertexAttribPointer(mPositionHandle, mPositionDataSize, GLES20.GL_FLOAT, false,
mStrideBytes, aTriangleBuffer);
GLES20.glEnableVertexAttribArray(mPositionHandle);
// Pass in the color information
aTriangleBuffer.position(mColorOffset);
GLES20.glVertexAttribPointer(mColorHandle, mColorDataSize, GLES20.GL_FLOAT, false,
mStrideBytes, aTriangleBuffer);
GLES20.glEnableVertexAttribArray(mColorHandle);
// This multiplies the view matrix by the model matrix, and stores the result in the MVP matrix
// (which currently contains model * view).
Matrix.multiplyMM(mMVPMatrix, 0, mViewMatrix, 0, mModelMatrix, 0);
// This multiplies the modelview matrix by the projection matrix, and stores the result in the MVP matrix
// (which now contains 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);
}
你還記得我們最初創建渲染器時定義的那些緩衝區嗎? 我們終於可以使用它們了。 我們需要告訴OpenGL如何使用GLES20.glVertexAttribPointer()來使用這些數據。 讓我們來看看第一個調用。
// Pass in the position information
aTriangleBuffer.position(mPositionOffset);
GLES20.glVertexAttribPointer(mPositionHandle, mPositionDataSize, GLES20.GL_FLOAT, false,
mStrideBytes, aTriangleBuffer);
GLES20.glEnableVertexAttribArray(mPositionHandle);
我們將緩衝區位置設置爲位置偏移量,它位於緩衝區的開頭。然後我們告訴OpenGL使用這些數據並將其提供給頂點着色器並將其應用於我們的position屬性。我們還需要告訴OpenGL每個頂點或步幅之間有多少個元素。
注意:步幅需要以字節爲單位進行定義。雖然我們在頂點之間有7個元素(3個用於位置,4個用於顏色),但實際上我們有28個字節,因爲每個浮點數佔用4個字節。忘記此步驟可能不會導致任何錯誤,但您會想知道爲什麼您在屏幕上看不到任何內容。
最後,我們啓用頂點屬性並轉到下一個屬性進行設置。繼續看代碼,我們構建一個組合矩陣,將點投影到屏幕上。 我們也可以在頂點着色器中執行此操作,但由於它只需要完成一次,所以我們可以只緩存結果。 我們使用GLES20.glUniformMatrix4fv()將最終矩陣傳遞給頂點着色器,GLES20.glDrawArrays()將我們的點轉換爲三角形並將其繪製在屏幕上。
概括
呼! 這是一個很重要的課程,如果你完成了這一課,你會非常開心。 我們學習瞭如何創建OpenGL上下文,傳遞形狀數據,加載頂點和像素着色器,設置轉換矩陣,最後將它們組合在一起。 如果一切順利,您應該會看到類似於下側屏幕截圖的內容。
這一課有很多要消化的內容,你可能需要多次閱讀這些步驟才能理解它。 OpenGL ES2需要更多的設置工作才能開始,但是一旦你完成了這個過程幾次,你就會記住前面的流程。
在Android Market上發佈
在開發應用程序時,我們不希望無法運行這些應用程序的人在市場上看到它們,否則當應用程序在其設備上崩潰時,我們可能會收到大量糟糕的評論和評分。 要防止OpenGL ES2應用程序出現在不支持它的設備上,您可以將其添加到清單中:
<uses-feature
android:glEsVersion="0x00020000"
android:required="true" />
這告訴市場您的應用程序需要OpenGL ES2,它會將您的應用程序隱藏掉如果設備不支持它。
進一步探索
嘗試更改動畫速度,頂點或顏色,看看會發生什麼!
可以從GitHub上的項目站點下載本課程的完整源代碼。