首先聲明下,本文為筆者學習《OpenGL ES應用開發實踐指南(Android卷)》的筆記,涉及的代碼均出自原書,如有需要,請到原書指定源碼地址下載。
在Android、iOS等移動平台上,開發者可以使用跨平台應用編程接口創建二維或者三維圖形,或進行圖像處理和計算機視覺應用,結合兩者將能構建豐富有趣的交互體驗。前者稱為OpenGL,后者稱為OpenCV,不過本文主要介紹前者,OpenCV在后續文章中涉及。OpenGL應用於桌面系統的歷史已經很長了,但考慮到移動平台的特點(計算能力、性能等),將OpenGL應用與移動端使用的是一個特殊的嵌入式版本:OpenGL ES(OpenGL for Embedded System)。OpenGL ES有三個版本:版本1.0提供了一個不太靈活的、固定功能的管道;2.0版本推出了可編程的管道,提供我們所需的所有功能;3.0版本在2.0的基礎上增加了一些新的特性,目前還沒有廣泛使用。《OpenGL ES應用開發實踐指南(Android卷)》基於2.0版本進行說明,本文主要內容如下:
- OpenGL ES的基本用法,即HelloWorld實現
- 二維或者三維圖形繪制流程
- 着色器(頂點和片段)編譯
一、OpenGL ES的基本用法
OpenGL ES的HelloWorld實現非常簡單,只需兩步:(1)在xml布局文件或者代碼中引入GLSurfaceView;(2)為GLSurfaceView設置Renderer。代碼如下:
glSurfaceView = new GLSurfaceView(this); // Check if the system supports OpenGL ES 2.0. ActivityManager activityManager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE); ConfigurationInfo configurationInfo = activityManager .getDeviceConfigurationInfo(); // Even though the latest emulator supports OpenGL ES 2.0, // it has a bug where it doesn't set the reqGlEsVersion so // the above check doesn't work. The below will detect if the // app is running on an emulator, and assume that it supports // OpenGL ES 2.0. final boolean supportsEs2 = 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("Android SDK built for x86"))); if (supportsEs2) { // Request an OpenGL ES 2.0 compatible context. glSurfaceView.setEGLContextClientVersion(2); // Assign our renderer. glSurfaceView.setRenderer(new AirHockeyRenderer(this)); rendererSet = true; } else { /* * This is where you could create an OpenGL ES 1.x compatible * renderer if you wanted to support both ES 1 and ES 2. Since * we're not doing anything, the app will crash if the device * doesn't support OpenGL ES 2.0. If we publish on the market, we * should also add the following to AndroidManifest.xml: * * <uses-feature android:glEsVersion="0x00020000" * android:required="true" /> * * This hides our app from those devices which don't support OpenGL * ES 2.0. */ Toast.makeText(this, "This device does not support OpenGL ES 2.0.", Toast.LENGTH_LONG).show(); return; } setContentView(glSurfaceView);
首先通過GLSurfaceView的構造函數創建GLSurfaceView對象glSurfaceView,然后檢查系統是否支持OpenGL ES 2.0版,支持就為glSurfaceView設置Renderer,否則彈出提示並返回,最后設置界面展示。
需要說明的是,使用GLSurfaceView還需要處理Activity的生命周期,否則,用戶切換到另一個應用,應用就會奔潰。
@Override protected void onPause() { super.onPause(); if (rendererSet) { glSurfaceView.onPause(); } } @Override protected void onResume() { super.onResume(); if (rendererSet) { glSurfaceView.onResume(); } }
除了GLSurfaceView,另一個重點就是Renderer。Renderer是一個接口,定義了上如下方法:
public interface Renderer { void onSurfaceCreated(GL10 gl, EGLConfig config); void onSurfaceChanged(GL10 gl, int width, int height); void onDrawFrame(GL10 gl); }
onSurfaceCreated(GL10 gl, EGLConfig config)方法在Surface被創建的時候調用。但在實踐中,設備被喚醒或者用戶從其他activity切換回來時,這個方法也可能被調用。
onSurfaceChanged(GL10 gl, int width, int height)方法在Surface尺寸變化時調用,在橫豎屏切換時,Surface尺寸都會變化。
onDrawFrame(GL10 gl)方法在繪制一幀時調用。在這個方法中,一定要繪制一些東西,即使只是清空屏幕,因為在這個方法返回后,渲染緩沖區會被交換並顯示到屏幕上,如果什么都沒畫,可能看到糟糕的閃爍效果。
HelloWorld應用中使用了AirHockeyRenderer,該類實現了Renderer接口。
@Override public void onSurfaceCreated(GL10 glUnused, EGLConfig config) { glClearColor(0.0f, 0.0f, 0.0f, 0.0f); }
@Override public void onSurfaceChanged(GL10 glUnused, int width, int height) { // Set the OpenGL viewport to fill the entire surface. glViewport(0, 0, width, height); }
@Override public void onDrawFrame(GL10 glUnused) { // Clear the rendering surface. glClear(GL_COLOR_BUFFER_BIT); }
二、繪制流程
首先,在OpenGL ES中,只支持三種類型的繪制:點、直線以及三角形。所以需要在繪制圖像之前,需要把一個圖像分解為這三種圖像的組合。
其次,OpenGL作為本地庫直接運行在硬件上,沒有虛擬機,也沒有垃圾回收或者內存壓縮。在Java層定義圖像的數據需要能被OpenGL存取,因此,需要把內存從Java堆復制到本地堆。使用的方法是通過ByteBuffer:
private final FloatBuffer vertexData; vertexData = ByteBuffer .allocateDirect(tableVerticesWithTriangles.length * BYTES_PER_FLOAT) .order(ByteOrder.nativeOrder()) .asFloatBuffer(); vertexData.put(tableVerticesWithTriangles);
在上述代碼中,首先使用ByteBuffer.allocateDirect()分配一塊本地內存,這塊本地內存不會被垃圾回收器管理。這個方法需要確定分配的內存的大小(單位為字節);因為頂點存儲在浮點數組中,並且每個浮點數有4個字節,所以這塊內存的大小為tableVerticesWithTriangles.length * BYTES_PER_FLOAT。order(ByteOrder.nativeOrder())的意思是使緩沖區按照本地字節序組織內容,字節序請移步這里。asFloatBuffer()方法得到一個可以反映底層字節的FloatBuffer類實例,因為我們直接操作的是浮點數,而不是單獨的字節。
最后,通過在OpenGL管道傳遞數據,着色器告訴GPU如何繪制數據。着色器分為頂點着色器和片段着色器。
綜上,整體流程為:讀取頂點數據——執行頂點着色器——組裝圖元——光柵化圖元——執行片段着色器——寫入幀緩沖區——顯示到屏幕上。
再介紹下着色器,頂點着色器生成每個頂點的最終位置,針對每個頂點,執行一次;片段着色器為組成點、直線和三角形的每個片段生成最終的顏色,針對每個片段,執行一次。
頂點着色器:
attribute vec4 a_Position; void main() { gl_Position = a_Position; gl_PointSize = 10.0; }
片段着色器:
precision mediump float; uniform vec4 u_Color; void main() { gl_FragColor = u_Color; }
着色器使用GLSL定義,GLSL是OpenGL的着色語言,語法結構與C語言相似。頂點着色器中,gl_Position接收當前頂點的位置,gl_PointSize設置頂點的大小;片段着色器中,gl_FragColor接收片段的顏色。precision mediump float為精度限定符,可以選擇lowp、mediump和highp,分別對應低精度、中等精度和高精度。
三、着色器編譯
首先,從資源中加載着色器文本。即將本地資源文件讀流,然后將流轉換為String:
public static String readTextFileFromResource(Context context, int resourceId) { StringBuilder body = new StringBuilder(); try { InputStream inputStream = context.getResources().openRawResource(resourceId); InputStreamReader inputStreamReader = new InputStreamReader(inputStream); BufferedReader 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); } return body.toString(); }
這段代碼很簡單,不做具體介紹了。
其次,編譯着色器。
/** * Loads and compiles a vertex shader, returning the OpenGL object ID. */ public static int compileVertexShader(String shaderCode) { return compileShader(GL_VERTEX_SHADER, shaderCode); } /** * Loads and compiles a fragment shader, returning the OpenGL object ID. */ public static int compileFragmentShader(String shaderCode) { return compileShader(GL_FRAGMENT_SHADER, shaderCode); } /** * Compiles a shader, returning the OpenGL object ID. */ private static int compileShader(int type, String shaderCode) { // Create a new shader object. final int shaderObjectId = glCreateShader(type); if (shaderObjectId == 0) { if (LoggerConfig.ON) { Log.w(TAG, "Could not create new shader."); } return 0; } // Pass in the shader source. glShaderSource(shaderObjectId, shaderCode); // Compile the shader. glCompileShader(shaderObjectId); // Get the compilation status. 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)); } // Verify the compile status. 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; } // Return the shader object ID. return shaderObjectId; }
compileVertexShader和compileFragmentShader都是通過compileShader來實現的,編譯頂點着色器和片段着色器通過type進行區分:GL_VERTEX_SHADER和GL_FRAGMENT_SHADER。
compileShader方法包含的步驟是:新建着色器對象(glCreateShader)——上傳着色器源代碼(glShaderSource)——取出編譯狀態(glCompileShader)——取出着色器信息日志(glGetShaderiv)——驗證編譯狀態並返回着色器對象ID(compileStatus[0] == 0。
再次,把着色器一起鏈接進OpenGL的程序。
/** * Links a vertex shader and a fragment shader together into an OpenGL * program. Returns the OpenGL program object ID, or 0 if linking failed. */ public static int linkProgram(int vertexShaderId, int fragmentShaderId) { // Create a new program object. final int programObjectId = glCreateProgram(); if (programObjectId == 0) { if (LoggerConfig.ON) { Log.w(TAG, "Could not create new program"); } return 0; } // Attach the vertex shader to the program. glAttachShader(programObjectId, vertexShaderId); // Attach the fragment shader to the program. glAttachShader(programObjectId, fragmentShaderId); // Link the two shaders together into a program. glLinkProgram(programObjectId); // Get the link status. 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)); } // Verify the link status. 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; } // Return the program object ID. return programObjectId; }
一個OpenGL的程序就是把一個頂點着色器和一個片段着色器鏈接在一起變成單個對象。鏈接與編譯在流程上大致相同,主要流程為:新建程序並附上着色器對象——鏈接程序——驗證鏈接狀態並返回程序對象ID——給渲染類加入代碼。
再次是驗證OpenGL程序的對象:
/** * Validates an OpenGL program. Should only be called when developing the * application. */ 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; }
調用glValidateProgram來驗證這個程序,然后用GL_VALIDATE_STATUS參數調用glGetProgramiv()方法檢測結果。需要說明的是,只有在開發和調試應用的時候才去驗證程序。
在onSurfaceCreate()結尾處使用程序。glUseProgram告訴OpenGL在繪制任何東西到屏幕上的時候使用這里定義的程序。驗證程序對象之后需要獲取uniform的位置和屬性的位置,最后關聯屬性與頂點數據的數組,使能頂點數組。上述流程走完之后,就可以正常繪制任何圖像了,繪制圖像的代碼在onDrawFrame()處:
// Clear the rendering surface. glClear(GL_COLOR_BUFFER_BIT); // Draw the table. glUniform4f(uColorLocation, 1.0f, 1.0f, 1.0f, 1.0f); glDrawArrays(GL_TRIANGLES, 0, 6); // Draw the center dividing line. 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);
glUniform4f更新着色器代碼中的u_Color值,glDrawArrays代表繪制,GL_TRIANGLES表示要畫三角形,0,6表示讀入從開頭的6個頂點。
總結:
(1)定義頂點屬性數組,並將數組復制到本地內存;
(2)創建頂點着色器和片段着色器,着色器只是可以運行在GPU上的一個特殊類型的程序。
(3)如何創建和編譯着色器;
(4)鏈接頂點着色器和片段着色器形成OpenGL程序對象;
(5)關聯頂點着色器內部的屬性變量與頂點屬性數組。