記錄一下 OpenGL ES Android 開發的入門教程。邏輯性可能不那麼強,想到哪寫到哪。也可能本身的一些理解有誤。java
參考資料:android
LearnOpenGL CN
Android官方文檔
《OpenGL ES應用開發實踐指南Android卷》
《OpenGL ES 3.0 編程指南第2版》git
目前android 4.3或以上支持opengles 3.0,但目前不少運行android 4.3系統的硬件能支持opengles 3.0的也是很是少的。不過,opengles 3.0是向後兼容的,當程序發現硬件不支持opengles 3.0時則會自動調用opengles 2.0的API。Andorid 中使用 OpenGLES 有兩種方式,一種是基於Android框架API, 另外一種是基於 Native Development Kit(NDK)使用 OpenGL。本文介紹Android框架接口。github
本文寫一個最基本的三角形繪製,來講明一下 OpenGL ES 的基本流程,以及注意點。編程
<!-- Tell the system this app requires OpenGL ES 3.0. --> <uses-feature android:glEsVersion="0x00030000" android:required="true" />
若是程序中使用了紋理壓縮的話,還需進行以下聲明,以防止不支持這些壓縮格式的設備嘗試運行程序。小程序
<supports-gl-texture android:name="GL_OES_compressed_ETC1_RGB8_texture" /> <supports-gl-texture android:name="GL_OES_compressed_paletted_texture" />
MainActivity.java 代碼:windows
package com.sharpcj.openglesdemo; import android.app.ActivityManager; import android.content.Context; import android.content.pm.ConfigurationInfo; import android.opengl.GLSurfaceView; import android.os.Build; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.util.Log; public class MainActivity extends AppCompatActivity { private static final String TAG = MainActivity.class.getSimpleName(); private GLSurfaceView mGlSurfaceView; private boolean mRendererSet; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // setContentView(R.layout.activity_main); if (!checkGlEsSupport(this)) { Log.d(TAG, "Device is not support OpenGL ES 2"); return; } mGlSurfaceView = new GLSurfaceView(this); mGlSurfaceView.setEGLContextClientVersion(2); mGlSurfaceView.setEGLConfigChooser(8, 8, 8, 8, 16, 0); mGlSurfaceView.setRenderer(new MyRenderer(this)); setContentView(mGlSurfaceView); mRendererSet = true; } @Override protected void onPause() { super.onPause(); if (mRendererSet) { mGlSurfaceView.onPause(); } } @Override protected void onResume() { super.onResume(); if (mRendererSet) { mGlSurfaceView.onResume(); } } /** * 檢查設備是否支持 OpenGLEs 2.0 * * @param context 上下文環境 * @return 返回設備是否支持 OpenGLEs 2.0 */ public boolean checkGlEsSupport(Context context) { final ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); final ConfigurationInfo configurationInfo = activityManager.getDeviceConfigurationInfo(); final boolean supportGlEs2 = configurationInfo.reqGlEsVersion >= 0x20000 || (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1 && (Build.FINGERPRINT.startsWith("generic") || Build.FINGERPRINT.startsWith("unknown") || Build.MODEL.contains("google_sdk") || Build.MODEL.contains("Emulator") || Build.MODEL.contains("Andorid SDK built for x86"))); return supportGlEs2; } }
關鍵步驟:數組
setContentView()
方法,傳入 GLSurfaceView 對象。建立一個類,實現 GLSurfaceView.Renderer
接口,並實現其中的關鍵方法app
package com.sharpcj.openglesdemo; import android.content.Context; import android.opengl.GLSurfaceView; import javax.microedition.khronos.egl.EGLConfig; import javax.microedition.khronos.opengles.GL10; import static android.opengl.GLES30.*; public class MyRenderer implements GLSurfaceView.Renderer { private Context mContext; private MyTriangle mTriangle; public MyRenderer(Context mContext) { this.mContext = mContext; } @Override public void onSurfaceCreated(GL10 gl, EGLConfig config) { glClearColor(1.0f, 1.0f, 1.0f, 1.0f); mTriangle = new MyTriangle(mContext); } @Override public void onSurfaceChanged(GL10 gl, int width, int height) { glViewport(0, 0, width, height); } @Override public void onDrawFrame(GL10 gl) { glClear(GL_COLOR_BUFFER_BIT); mTriangle.draw(); } }
三個關鍵方法:框架
glViewport(0, 0, width, height); 用於設置視口。
glCrearColor(1.0f, 1.0f, 1.0f, 1.0f) 方法用指定顏色(這裏是白色)清空屏幕。
在 onDrawFrame 中調用 glClearColor(GL_COLOR_BUFFER_BIT) ,擦除屏幕現有的繪製,並用以前的顏色清空屏幕。 該方法中必定要繪製一些東西,即使只是清空屏幕,由於該方法調用後會交換緩衝區,並顯示在屏幕上,不然可能會出現閃爍。該例子中將具體的繪製封裝在了 Triangle 類中的draw
方法中了。
注意:在 windows 版的 OpenGL 中,須要手動調用glfwSwapBuffers(window)
來交換緩衝區。
建立 MyTriangle.java
類:
package com.sharpcj.openglesdemo; import android.content.Context; import com.sharpcj.openglesdemo.util.ShaderHelper; import com.sharpcj.openglesdemo.util.TextResourceReader; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.FloatBuffer; import static android.opengl.GLES30.*; public class MyTriangle { private final FloatBuffer mVertexBuffer; static final int COORDS_PER_VERTEX = 3; // number of coordinates per vertex in this array static final int COLOR_PER_VERTEX = 3; // number of coordinates per vertex in this array static float triangleCoords[] = { // in counterclockwise order: 0.0f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f, // top -0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, // bottom left 0.5f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0f // bottom right }; private Context mContext; private int mProgram; public MyTriangle(Context context) { mContext = context; // initialize vertex byte buffer for shape coordinates mVertexBuffer = ByteBuffer.allocateDirect(triangleCoords.length * 4).order(ByteOrder.nativeOrder()).asFloatBuffer(); mVertexBuffer.put(triangleCoords); // add the coordinates to the FloatBuffer mVertexBuffer.position(0); // set the buffer to read the first coordinate String vertexShaderCode = TextResourceReader.readTextFileFromResource(mContext, R.raw.simple_vertex_glsl); String fragmentShaderCode = TextResourceReader.readTextFileFromResource(mContext, R.raw.simple_fragment_glsl); int vertexShader = ShaderHelper.compileVertexShader(vertexShaderCode); int fragmentShader = ShaderHelper.compileFragmentShader(fragmentShaderCode); mProgram = ShaderHelper.linkProgram(vertexShader, fragmentShader); } public void draw() { if (!ShaderHelper.validateProgram(mProgram)) { glDeleteProgram(mProgram); return; } glUseProgram(mProgram); // Add program to OpenGL ES environment // int aPos = glGetAttribLocation(mProgram, "aPos"); // get handle to vertex shader's vPosition member mVertexBuffer.position(0); glVertexAttribPointer(0, COORDS_PER_VERTEX, GL_FLOAT, false, (COORDS_PER_VERTEX + COLOR_PER_VERTEX) * 4, mVertexBuffer); // Prepare the triangle coordinate data glEnableVertexAttribArray(0); // Enable a handle to the triangle vertices // int aColor = glGetAttribLocation(mProgram, "aColor"); mVertexBuffer.position(3); glVertexAttribPointer(1, COORDS_PER_VERTEX, GL_FLOAT, false, (COORDS_PER_VERTEX + COLOR_PER_VERTEX) * 4, mVertexBuffer); // Prepare the triangle coordinate data glEnableVertexAttribArray(1); // Draw the triangle glDrawArrays(GL_TRIANGLES, 0, 3); } }
在該類中,咱們使用了,兩個工具類:
TextResourceReader.java
, 用於讀取文件的類容,返回一個字符串,準確說,它與 OpenGL 自己沒有關係。
package com.sharpcj.openglesdemo.util; import android.content.Context; import android.content.res.Resources; import android.util.Log; import java.io.BufferedReader; import java.io.Closeable; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; public class TextResourceReader { private static String TAG = "TextResourceReader"; public static String readTextFileFromResource(Context context, int resourceId) { StringBuilder body = new StringBuilder(); InputStream inputStream = null; InputStreamReader inputStreamReader = null; BufferedReader bufferedReader = null; try { inputStream = context.getResources().openRawResource(resourceId); inputStreamReader = new InputStreamReader(inputStream); 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); } finally { closeStream(inputStream); closeStream(inputStreamReader); closeStream(bufferedReader); } return body.toString(); } private static void closeStream(Closeable c) { if (c != null) { try { c.close(); } catch (IOException e) { Log.e(TAG, e.getMessage()); } } } }
ShaderHelper.java
着色器的工具類,這個跟 OpenGL 就有很是大的關係了。
package com.sharpcj.openglesdemo.util; import android.util.Log; import static android.opengl.GLES30.*; 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); } private static int compileShader(int type, String shaderCode) { final int shaderObjectId = glCreateShader(type); if (shaderObjectId == 0) { Log.w(TAG, "could not create new shader."); return 0; } glShaderSource(shaderObjectId, shaderCode); glCompileShader(shaderObjectId); final int[] compileStatus = new int[1]; glGetShaderiv(shaderObjectId, GL_COMPILE_STATUS, compileStatus, 0); /*Log.d(TAG, "Results of compiling source: " + "\n" + shaderCode + "\n: " + glGetShaderInfoLog(shaderObjectId));*/ if (compileStatus[0] == 0) { glDeleteShader(shaderObjectId); Log.w(TAG, "Compilation of shader failed."); return 0; } return shaderObjectId; } public static int linkProgram(int vertexShaderId, int fragmentShaderId) { final int programObjectId = glCreateProgram(); if (programObjectId == 0) { Log.w(TAG, "could not create new program"); return 0; } glAttachShader(programObjectId, vertexShaderId); glAttachShader(programObjectId, fragmentShaderId); glLinkProgram(programObjectId); final int[] linkStatus = new int[1]; glGetProgramiv(programObjectId, GL_LINK_STATUS, linkStatus, 0); /*Log.d(TAG, "Results of linking program: \n" + glGetProgramInfoLog(programObjectId));*/ if (linkStatus[0] == 0) { glDeleteProgram(programObjectId); Log.w(TAG, "Linking of program failed"); return 0; } return programObjectId; } public static boolean validateProgram(int programId) { glValidateProgram(programId); final int[] validateStatus = new int[1]; glGetProgramiv(programId, GL_VALIDATE_STATUS, validateStatus, 0); /*Log.d(TAG, "Results of validating program: " + validateStatus[0] + "\n Log: " + glGetProgramInfoLog(programId));*/ return validateStatus[0] != 0; } }
着色器是 OpenGL 裏面很是重要的概念,這裏我先把代碼貼上來,而後來說流程。
在 res/raw 文件夾下,咱們建立了兩個着色器文件。
頂點着色器,simple_vertex_shader.glsl
#version 330 layout (location = 0) in vec3 aPos; // 位置變量的屬性位置值爲 0 layout (location = 1) in vec3 aColor; // 顏色變量的屬性位置值爲 1 out vec3 vColor; // 向片斷着色器輸出一個顏色 void main() { gl_Position = vec4(aPos.xyz, 1.0); vColor = aColor; // 將ourColor設置爲咱們從頂點數據那裏獲得的輸入顏色 }
片斷着色器, simple_fragment_shader.glsl
#version 330 precision mediump float; in vec3 vColor; out vec4 FragColor; void main() { FragColor = vec4(vColor, 1.0); }
所有的代碼就只這樣了,具體繪製過程下面來講。運行程序,咱們看到效果以下:
一張圖說明 OpenGL 渲染過程:
咱們看 MyTriangle.java
這個類。
要繪製三角形,咱們確定要定義三角形的頂點座標和顏色。(廢話,否則GPU怎麼知道用什麼顏色繪製在哪裏)。
首先咱們定義了一個 float 型數組:
static float triangleCoords[] = { // in counterclockwise order: 0.0f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f, // top -0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, // bottom left 0.5f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0f // bottom right };
注意:這個數組中,定義了 top, bottom left, bottom right 三個點。每一個點包含六個數據,前三個數表示頂點座標,後三個點表示顏色的 RGB 值。
可能注意到了,由於咱們這裏繪製最簡單的平面二維圖像,Z 軸座標都爲 0 ,屏幕中的 X, Y 座標點都是在(-1,1)的範圍。咱們沒有對視口作任何變換,設置的默認視口,此時的座標系統是以屏幕正中心爲座標原點。 屏幕最左爲 X 軸 -1 , 屏幕最右爲 X 軸 +1。同理,屏幕最下方爲 Y 軸 -1, 屏幕最上方爲 Y 軸 +1。OpenGL 座標系統使用的是右手座標系,Z 軸正方向爲垂直屏幕向外。
mVertexBuffer = ByteBuffer.allocateDirect(triangleCoords.length * 4).order(ByteOrder.nativeOrder()).asFloatBuffer(); mVertexBuffer.put(triangleCoords);
這一行代碼,做用是將數據從 java 堆複製到本地堆。咱們知道,在 java 虛擬機內存模型中,數組存在 java 堆中,受 JVM 垃圾回收機制影響,可能會被回收掉。因此咱們要將數據複製到本地堆。
首先調用 ByteBuffer.allocateDirect()
分配一塊本地內存,一個 float 類型的數字佔 4 個字節,因此分配的內存大小爲 triangleCoords.length * 4 。
調用 order()
指定字節緩衝區中的排列順序, 傳入 ByteOrder.nativeOrder() 保證做爲一個平臺,使用相同的排序順序。
調用 asFloatBuffer()
能夠獲得一個反映底層字節的 FloatBuffer 類的實例。
最後調用 put(triangleCoords)
把數據從 Android 虛擬機堆內存中複製到本地內存。
接下來,經過 TextResourceReader 工具類,讀取頂點着色器和片斷着色器文件的的內容。
String vertexShaderCode = TextResourceReader.readTextFileFromResource(mContext, R.raw.simple_vertex_shader); String fragmentShaderCode = TextResourceReader.readTextFileFromResource(mContext, R.raw.simple_fragment_shader);
而後經過 ShaderHelper 工具類編譯着色器。而後連接到程序。
int vertexShader = ShaderHelper.compileVertexShader(vertexShaderCode); int fragmentShader = ShaderHelper.compileFragmentShader(fragmentShaderCode); mProgram = ShaderHelper.linkProgram(vertexShader, fragmentShader); ShaderHelper.validateProgram(mProgram);
着色器是一個運行在 GPU 上的小程序。着色器的文件其實定義了變量,而且包含 main 函數。關於着色器的詳細教程,請查閱:(LearnOpenGL CN 中的着色器教程)[https://learnopengl-cn.github.io/01%20Getting%20started/05%20Shaders/]
我這裏記錄一下,着色器的編譯過程:
int shaderObjectId = glCreateShader(type);`
建立一個着色器,並返回着色器的句柄(相似java中的引用),若是返回了 0 ,說明建立失敗。GLES 中定義了常量,GL_VERTEX_SHADER
和 GL_FRAGMENT_SHADER
做爲參數,分別建立頂點着色器和片斷着色器。
編譯着色器,
glShaderSource(shaderObjectId, shaderCode); glCompileShader(shaderObjectId);
下面的代碼,用於獲取編譯着色器的狀態結果。
final int[] compileStatus = new int[1]; glGetShaderiv(shaderObjectId, GL_COMPILE_STATUS, compileStatus, 0); Log.d(TAG, "Results of compiling source: " + "\n" + shaderCode + "\n: " + glGetShaderInfoLog(shaderObjectId)); if (compileStatus[0] == 0) { glDeleteShader(shaderObjectId); Log.w(TAG, "Compilation of shader failed."); return 0; }
親測上面的程序在我手上真機能夠正常運行,在 genymotion 模擬器中運行報了以下錯誤:
JNI DETECTED ERROR IN APPLICATION: input is not valid Modified UTF-8: illegal start byte 0xfe
網上搜索了一下,這個異常是因爲Java虛擬機內部的dalvik/vm/CheckJni.c中的checkUtfString函數拋出的,而且JVM的這個接口明確是不支持四個字節的UTF8字符。所以須要在調用函數以前,對接口傳入的字符串進行過濾,過濾函數,能夠上網搜到,這不是本文重點,因此我把這個 log 註釋掉了
Log.d(TAG, "Results of compiling source: " + "\n" + shaderCode + "\n: " + glGetShaderInfoLog(shaderObjectId));
編譯完着色器以後,須要將着色器鏈接到程序才能使用。
int programObjectId = glCreateProgram();
建立一個 program 對象,並返回句柄,若是返回了 0 ,說明建立失敗。
glAttachShader(programObjectId, vertexShaderId); glAttachShader(programObjectId, fragmentShaderId); glLinkProgram(programObjectId);
將頂點着色器個片斷着色器連接到 program 對象。下面的代碼用於獲取連接的狀態結果:
final int[] linkStatus = new int[1]; glGetProgramiv(programObjectId, GL_LINK_STATUS, linkStatus, 0); /*Log.d(TAG, "Results of linking program: \n" + glGetProgramInfoLog(programObjectId));*/ if (linkStatus[0] == 0) { glDeleteProgram(programObjectId); Log.w(TAG, "Linking of program failed"); return 0; }
在使用 program 對象以前,咱們還作了有效性判斷:
glValidateProgram(programId); final int[] validateStatus = new int[1]; glGetProgramiv(programId, GL_VALIDATE_STATUS, validateStatus, 0); /*Log.d(TAG, "Results of validating program: " + validateStatus[0] + "\n Log: " + glGetProgramInfoLog(programId));*/
若是 validateStatus[0] == 0 , 則無效。
首先調用glUseProgram(mProgram)
將 program 對象添加到 OpenGL ES 的繪製環境。
看以下代碼:
mVertexData.position(0); // 移動指針到 0,表示從開頭開始讀取 // 告訴 OpenGL, 能夠在緩衝區中找到 a_Position 對應的數據 int aPos = glGetAttribLocation(mProgram, "aPos"); glVertexAttribPointer(aPos, COORDS_PER_VERTEX, GL_FLOAT, false, (COORDS_PER_VERTEX + COLOR_PER_VERTEX) * 4, mVertexBuffer); // Prepare the triangle coordinate data glEnableVertexAttribArray(aPos); int aColor = glGetUniformLocation(mProgram, "aColor"); glVertexAttribPointer(1, COORDS_PER_VERTEX, GL_FLOAT, false, (COORDS_PER_VERTEX + COLOR_PER_VERTEX) * 4, mVertexBuffer); // Prepare the triangle coordinate data glEnableVertexAttribArray(aColor);
在 OpenGL ES 2.0 中,咱們經過如上代碼,使用數據。調用 glGetAttribLocation()
方法,找到頂點和顏色對應的數據位置,第一個參數是 program 對象,第二個參數是着色器中的入參參數名。
而後調用 glVertexAttribPointer()
方法
參數以下(圖片截取自《OpenGL ES應用開發實踐指南Android卷》):
最後調用glEnableVertexAttribArray(aPos);
使 OpenGL 能使用這個數據。
可是你發現,咱們上面給的代碼中並無調用 glGetAttribLocation()
方法尋找位置,這是由於,我使用的 OpenGLES 3.0 ,在 OpenGL ES 3.0 中,着色器代碼中,新增了 layout(location = 0)
相似的語法支持。
#version 330 layout (location = 0) in vec3 aPos; // 位置變量的屬性位置值爲 0 layout (location = 1) in vec3 aColor; // 顏色變量的屬性位置值爲 1 out vec3 vColor; // 向片斷着色器輸出一個顏色 void main() { gl_Position = vec4(aPos.xyz, 1.0); vColor = aColor; // 將ourColor設置爲咱們從頂點數據那裏獲得的輸入顏色 }
這裏已經指明瞭屬性在頂點數組中對應的位置,因此在代碼中,能夠直接使用 0 和 1 來表示位置。
最後調用 glDrawArrays(GL_TRIANGLES, 0, 3)
繪製出一個三角形。
glDrawArrays() 方法第一個參數指定繪製的類型, OpenGLES 中定義了一些常量,一般有 GL_TRIANGLES , GL_POINTS, GL_LINES, GL_TRIANGLE_STRIP, GL_TRIANGLE_FAN 等等類型,具體每種類型表明的意思能夠查閱API 文檔。
VAO : 頂點數組對象
VBO :頂點緩衝對象
經過使用 VAO 和 VBO ,能夠創建 VAO 與 VBO 的索引對應關係,一次寫入數據以後,每次使用只須要調用 glBindVertexArray
方法便可,避免重複進行數據的複製, 大大提升繪製效率。
int[] VBO = new int[2]; int[] VAO = new int[2]; glGenVertexArrays(2, VAO, 0); glGenBuffers(2, VBO, 0); glBindVertexArray(VAO[0]); glBindBuffer(GL_ARRAY_BUFFER, VBO[0]); glBufferData(GL_ARRAY_BUFFER, triangleCoords.length * 4, mVertexBuffer, GL_STATIC_DRAW); glVertexAttribPointer(0, COORDS_PER_VERTEX, GL_FLOAT, false, (COORDS_PER_VERTEX + COLOR_PER_VERTEX) * 4, 0); glEnableVertexAttribArray(0); glVertexAttribPointer(1, COORDS_PER_VERTEX, GL_FLOAT, false, (COORDS_PER_VERTEX + COLOR_PER_VERTEX) * 4, COORDS_PER_VERTEX * 4); glEnableVertexAttribArray(1); glBindVertexArray(VAO[0]); glBindVertexArray(VAO[1]); glBindBuffer(GL_ARRAY_BUFFER, VBO[1]); glBufferData(GL_ARRAY_BUFFER, triangleCoords.length * 4, mVertexBuffer2, GL_STATIC_DRAW); glVertexAttribPointer(0, COORDS_PER_VERTEX, GL_FLOAT, false, (COORDS_PER_VERTEX + COLOR_PER_VERTEX) * 4, 0); glEnableVertexAttribArray(0); glVertexAttribPointer(1, COORDS_PER_VERTEX, GL_FLOAT, false, (COORDS_PER_VERTEX + COLOR_PER_VERTEX) * 4, COORDS_PER_VERTEX * 4); glEnableVertexAttribArray(1); glBindVertexArray(VAO[1]); glBindVertexArray(VAO[0]); glDrawArrays(GL_TRIANGLES, 0, 3); glBindVertexArray(VAO[1]); glDrawArrays(GL_TRIANGLES, 0, 3);