Android framework提供了很多的標準工具來創(chuàng)建有吸引力的,功能強大的圖形用戶界面.但是如果你想要擁有更多的控制權來控制你的app在屏幕上畫的東西或是畫三維圖像,你就需要使用一個不同的工具,這就是OpenGL ES. Android framework中的OpenGL ES API帶有一堆用于顯示高端動畫的工具,只要你能想象的到的效果,它就有工具來讓你實現(xiàn),并且使用這些工具還可以利用大多數(shù)Android設備的GPU來加速.
下面的代碼使用的是OpenGL ES 2.0的API,這個版本的API也是推薦使用的,更多的信息可參考OpenGL Developer Guide.
注意: 不要將OpenGL ES 1.x API 和 2.0 API的方法弄混,這兩個API是不能相互替換的.
1. 建立一個OpenGL ES環(huán)境
要在你的應用中使用OpenGL ES來畫圖,你要先有一個view container.創(chuàng)建view container的最直接的一個方法是實現(xiàn)GLSurfaceView和GLSurfaceView.Renderer這兩個類.其中:
- GLSurfaceView是一個view container,用來存放繪制的圖像.
- GLSurfaceView.Renderer用來控制view中繪制的內(nèi)容.
注: GLSurfaceView只是一個存放OpenGL ES繪制的圖像的view container,適用于全屏或近乎全屏的view. 對于只占據(jù)整個布局一部分的view來說,可以使用TextureView.你還可以直接自定義一個view繼承SurfaceView,這樣你就能控制更多同時也需要寫更多的代碼.
1.1 在Manifest中聲明OpenGL ES的使用
OpenGL ES 2.0 API的聲明如下,注意每個版本android:glEsVersion
的值都不同:
<uses-feature android:glEsVersion="0x00020000" android:required="true" />
如果你的應用使用了Texture compression,你也要在manife中通過<supports-gl-texture>聲明你的應用所支持的格式,然后用戶在Google Play上下載安裝時就會過濾不支持的應用.
<supports-gl-texture android:name="GL_OES_compressed_ETC1_RGB8_texture" />
<supports-gl-texture android:name="GL_OES_compressed_paletted_texture" />
1.2 給OpenGL ES繪制的圖像創(chuàng)建Activity
使用OpenGL ES的應用的activity和其他的應用一樣,主要的不同在于content view中放的是什么.其他app放的是Button,TextView等等,而用OpenGL ES的應用的Activity不僅可以放其他標準的View,還可以放GLSurfaceView.如下示例:
public class OpenGLES20Activity extends Activity {
private GLSurfaceView mGLView;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Create a GLSurfaceView instance and set it
// as the ContentView for this Activity.
mGLView = new MyGLSurfaceView(this);
setContentView(mGLView);
}
}
注意: OpenGL ES 2.0需要Android 2.2(API 8)及以上版本才支持.
1.3 創(chuàng)建一個GLSurfaceView對象
GLSurfaceView是一個特殊的View,可以用來繪制OpenGL ES圖像. 實際圖像的繪制是由GLSurfaceView中設置的GLSurfaceView.Renderer來控制的,因此GLSurfaceView中要做的事情不多,所以代碼會很少,但是不要去直接去用匿名類來實現(xiàn)它,因為在后續(xù)的事件處理上還有需要.如下示例:
class MyGLSurfaceView extends GLSurfaceView {
private final MyGLRenderer mRenderer;
public MyGLSurfaceView(Context context){
super(context);
// Create an OpenGL ES 2.0 context
setEGLContextClientVersion(2);
mRenderer = new MyGLRenderer();
// Set the Renderer for drawing on the GLSurfaceView
setRenderer(mRenderer);
}
}
有一個可選的設置是setRenderMode(),將模式設置為RENDERMODE_WHEN_DIRTY,這樣可以減少渲染次數(shù),也就可以減少電量的使用以及更少的使用系統(tǒng)的GPU和CPU資源.
// Render the view only when there is a change in the drawing data
setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
1.4 創(chuàng)建一個Renderer類
Renderer這個類中有個三個方法會被系統(tǒng)回調(diào):
- onSurfaceCreated() - 設置view的OpenGL ES環(huán)境時調(diào)用.
- onDrawFrame() - view的每次重繪都會調(diào)用.
- onSurfaceChanged() - 當view的geometry改變的時候調(diào)用,例如屏幕翻轉(zhuǎn).
如下繪制黑色背景示例:
public class MyGLRenderer implements GLSurfaceView.Renderer {
public void onSurfaceCreated(GL10 unused, EGLConfig config) {
// Set the background frame color
GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
}
public void onDrawFrame(GL10 unused) {
// Redraw background color
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
}
public void onSurfaceChanged(GL10 unused, int width, int height) {
GLES20.glViewport(0, 0, width, height);
}
}
上述代碼就是一個完整OpenGL ES的使用,運行起來會是一個黑色的背景.
2. 定義Shape
定義多邊形是實現(xiàn)各種復雜的圖形的基礎,下面將介紹相關基礎知識.
2.1 定義三角形
OpenGL ES 允許你使用三維空間坐標來定義繪制的對象,在你繪制三角形之前,你需要先定義坐標. 最典型的做法是定義一個float類型的數(shù)組來存放三角形各頂點坐標,為了效率最大化,你可以使用ByteBuffer,這會被傳到OpenGL ES的pipeline來處理.如下示例:
public class Triangle {
private FloatBuffer vertexBuffer;
// number of coordinates per vertex in this array
static final int COORDS_PER_VERTEX = 3;
static float triangleCoords[] = { // in counterclockwise order:
0.0f, 0.622008459f, 0.0f, // top
-0.5f, -0.311004243f, 0.0f, // bottom left
0.5f, -0.311004243f, 0.0f // bottom right
};
// Set color with red, green, blue and alpha (opacity) values
float color[] = { 0.63671875f, 0.76953125f, 0.22265625f, 1.0f };
public Triangle() {
// initialize vertex byte buffer for shape coordinates
ByteBuffer bb = ByteBuffer.allocateDirect(
// (number of coordinate values * 4 bytes per float)
triangleCoords.length * 4);
// use the device hardware's native byte order
bb.order(ByteOrder.nativeOrder());
// create a floating point buffer from the ByteBuffer
vertexBuffer = bb.asFloatBuffer();
// add the coordinates to the FloatBuffer
vertexBuffer.put(triangleCoords);
// set the buffer to read the first coordinate
vertexBuffer.position(0);
}
}
默認情況下,OpenGL ES設定GLSurfaceView的frame的中心為坐標的原點[0,0,0](X,Y,Z),frame的右上角的坐標為[1,1,0],左下角的坐標為[-1,-1,0],如下圖(來自官網(wǎng)).
注意:上述圖形坐標是按逆時針來排序的,
2.2 定義正方形
正方形的定義方式有很多種,有一種常用的方法就是畫兩個三角形,如下圖(來自官網(wǎng)):
為了避免兩次定義被兩個三角形公用的兩個點的坐標,使用一個drawing list來告訴OpenGL ES繪制順序,代碼如下:
public class Square {
private FloatBuffer vertexBuffer;
private ShortBuffer drawListBuffer;
// number of coordinates per vertex in this array
static final int COORDS_PER_VERTEX = 3;
static float squareCoords[] = {
-0.5f, 0.5f, 0.0f, // top left
-0.5f, -0.5f, 0.0f, // bottom left
0.5f, -0.5f, 0.0f, // bottom right
0.5f, 0.5f, 0.0f }; // top right
private short drawOrder[] = { 0, 1, 2, 0, 2, 3 }; // order to draw vertices
public Square() {
// initialize vertex byte buffer for shape coordinates
ByteBuffer bb = ByteBuffer.allocateDirect(
// (# of coordinate values * 4 bytes per float)
squareCoords.length * 4);
bb.order(ByteOrder.nativeOrder());
vertexBuffer = bb.asFloatBuffer();
vertexBuffer.put(squareCoords);
vertexBuffer.position(0);
// initialize byte buffer for the draw list
ByteBuffer dlb = ByteBuffer.allocateDirect(
// (# of coordinate values * 2 bytes per short)
drawOrder.length * 2);
dlb.order(ByteOrder.nativeOrder());
drawListBuffer = dlb.asShortBuffer();
drawListBuffer.put(drawOrder);
drawListBuffer.position(0);
}
}
3. 繪制Shape
在前面定義完形狀之后,現(xiàn)在開始將它們畫出來.
3.1 初始化shape
在繪制之前,需要先將各shape初始化并加載,如果這些shape的坐標不會在執(zhí)行的時候變化,那么可以在onSurfaceCreated()中進行初始化和加載工作,這樣會更省內(nèi)存和提高處理效率.
public class MyGLRenderer implements GLSurfaceView.Renderer {
...
private Triangle mTriangle;
private Square mSquare;
public void onSurfaceCreated(GL10 unused, EGLConfig config) {
...
// initialize a triangle
mTriangle = new Triangle();
// initialize a square
mSquare = new Square();
}
...
}
3.2 繪制一個Shape
使用OpenGL ES 2.0來繪制圖像需要你提供一些細節(jié)給它,如下:
- Vertex Shader - 渲染shape頂點的OpenGl ES代碼.
- Fragment Shader - 渲染shape的face的顏色和texture的OpenGl ES代碼.
- Program - 包含繪制一個或多個shape的shader的OpenGL ES對象.
以上三個,你至少需要一個vertex shader來繪制shape和一個fragment shader來繪制顏色和texture,這些shader必須要被編譯然后再添加到一個OpenGL ES program中,然后這個progrem會被用來繪制shape.如下示例:
public class Triangle {
private final String vertexShaderCode =
"attribute vec4 vPosition;" +
"void main() {" +
" gl_Position = vPosition;" +
"}";
private final String fragmentShaderCode =
"precision mediump float;" +
"uniform vec4 vColor;" +
"void main() {" +
" gl_FragColor = vColor;" +
"}";
...
}
上面的Shader包含有OpenGl Shading Language(GLSL)代碼,這些代碼必須在使用之前編譯,如下面的編譯方法:
public static int loadShader(int type, String shaderCode){
// create a vertex shader type (GLES20.GL_VERTEX_SHADER)
// or a fragment shader type (GLES20.GL_FRAGMENT_SHADER)
int shader = GLES20.glCreateShader(type);
// add the source code to the shader and compile it
GLES20.glShaderSource(shader, shaderCode);
GLES20.glCompileShader(shader);
return shader;
}
注: 編譯OpenGL shader和把它們綁定到program上是非常消耗CPU的,所以你應該要盡量避免多次執(zhí)行這些工作.
public class Triangle() {
...
private final int mProgram;
public Triangle() {
...
int vertexShader = MyGLRenderer.loadShader(GLES20.GL_VERTEX_SHADER,
vertexShaderCode);
int fragmentShader = MyGLRenderer.loadShader(GLES20.GL_FRAGMENT_SHADER,
fragmentShaderCode);
// create empty OpenGL ES Program
mProgram = GLES20.glCreateProgram();
// add the vertex shader to program
GLES20.glAttachShader(mProgram, vertexShader);
// add the fragment shader to program
GLES20.glAttachShader(mProgram, fragmentShader);
// creates OpenGL ES program executables
GLES20.glLinkProgram(mProgram);
}
}
現(xiàn)在就可以來繪制shape了.使用OpenGL ES需要一些參數(shù)來告訴redering pipeline你要繪制什么并且要怎么繪制,由于每個shape的drawing option都不一樣,因此將每個shape的繪制邏輯放到自己的類里面是一個比較好的方法.如下代碼,將繪制邏輯方法draw()方法中:
private int mPositionHandle;
private int mColorHandle;
private final int vertexCount = triangleCoords.length / COORDS_PER_VERTEX;
private final int vertexStride = COORDS_PER_VERTEX * 4; // 4 bytes per vertex
public void draw() {
// Add program to OpenGL ES environment
GLES20.glUseProgram(mProgram);
// get handle to vertex shader's vPosition member
mPositionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition");
// Enable a handle to the triangle vertices
GLES20.glEnableVertexAttribArray(mPositionHandle);
// Prepare the triangle coordinate data
GLES20.glVertexAttribPointer(mPositionHandle, COORDS_PER_VERTEX,
GLES20.GL_FLOAT, false,
vertexStride, vertexBuffer);
// get handle to fragment shader's vColor member
mColorHandle = GLES20.glGetUniformLocation(mProgram, "vColor");
// Set color for drawing the triangle
GLES20.glUniform4fv(mColorHandle, 1, color, 0);
// Draw the triangle
GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, vertexCount);
// Disable vertex array
GLES20.glDisableVertexAttribArray(mPositionHandle);
}
現(xiàn)在就可以調(diào)用draw()方法來繪制了,在onDrawFrame()中調(diào)用:
public void onDrawFrame(GL10 unused) {
...
mTriangle.draw();
}
效果如下:
上面的示例有些不足的地方:
- 效果不夠叼,嚇不到別人.
- 上面三角形有點扭曲,屏幕旋轉(zhuǎn)后會變形,因為上面的三角形的頂點不是按GLSurfaceView的frame的比例來定點的,這個可以在下一個知識點中通過projection和camera view來解決.
- 這個是靜態(tài)的,不夠叼.
4. 應用Projection和Camera Views
在OpenGL ES環(huán)境中,projection和camera views允許你將圖像以現(xiàn)實中人眼所看到的物理物體的形式顯示出來,這種物理視角的模擬是通過對繪制對象的坐標進行相應的數(shù)學轉(zhuǎn)換來實現(xiàn):
- Projection - 這個是根據(jù)繪制對象在GLSurfaceView中的寬和高的坐標來轉(zhuǎn)換的.如果沒有這個計算,圖像會變形.這個轉(zhuǎn)換計算通常只是在OpenGL View的比例剛被建立或改變(在onSurfaceChanged()中回調(diào)).
- Camera View - 這個是根據(jù)繪制對象的虛擬camera position來轉(zhuǎn)換的.有一點很重要的是OpenGL ES沒有定義一個真實的camera對象,而是提供工具方法通過轉(zhuǎn)換繪制對象來模擬一個camera. 一個camera view的轉(zhuǎn)換可能只會在GLSurfaceView剛建立的時候計算一次,也有可能會根據(jù)用戶的行為或app的行為而動態(tài)改變.
4.1 定義一個Projection
projection轉(zhuǎn)換的數(shù)據(jù)是在GLSurfaceView.Renderer
的onSurfaceChanged()
方法中計算出來的,如下:
// mMVPMatrix is an abbreviation for "Model View Projection Matrix"
private final float[] mMVPMatrix = new float[16];
private final float[] mProjectionMatrix = new float[16];
private final float[] mViewMatrix = new float[16];
@Override
public void onSurfaceChanged(GL10 unused, int width, int height) {
GLES20.glViewport(0, 0, width, height);
float ratio = (float) width / height;
// this projection matrix is applied to object coordinates
// in the onDrawFrame() method
Matrix.frustumM(mProjectionMatrix, 0, -ratio, ratio, -1, 1, 3, 7);
}
注:剛應用projection轉(zhuǎn)換到繪制對象上時會導致empty display,通常你在projection轉(zhuǎn)換時也要應用camera view轉(zhuǎn)換來讓屏幕有東西顯示.
4.2 定義一個Camera View
通過Matrix.setLookAtM()方法來計算camera view轉(zhuǎn)換,然后與之前計算的projection的矩陣進行combine,如下:
@Override
public void onDrawFrame(GL10 unused) {
...
// Set the camera position (View matrix)
Matrix.setLookAtM(mViewMatrix, 0, 0, 0, -3, 0f, 0f, 0f, 0f, 1.0f, 0.0f);
// Calculate the projection and view transformation
Matrix.multiplyMM(mMVPMatrix, 0, mProjectionMatrix, 0, mViewMatrix, 0);
// Draw shape
mTriangle.draw(mMVPMatrix);
}
4.3 應用Projection和Camera轉(zhuǎn)換
為了讓combine后的projection和camera view的轉(zhuǎn)換矩陣在預覽的時候顯示,需要先在vertex shader中添加一個matrix變量:
public class Triangle {
private final String vertexShaderCode =
// This matrix member variable provides a hook to manipulate
// the coordinates of the objects that use this vertex shader
"uniform mat4 uMVPMatrix;" +
"attribute vec4 vPosition;" +
"void main() {" +
// the matrix must be included as a modifier of gl_Position
// Note that the uMVPMatrix factor *must be first* in order
// for the matrix multiplication product to be correct.
" gl_Position = uMVPMatrix * vPosition;" +
"}";
// Use to access and set the view transformation
private int mMVPMatrixHandle;
...
}
然后修改繪制對象的draw()方法:
public void draw(float[] mvpMatrix) { // pass in the calculated transformation matrix
...
// get handle to shape's transformation matrix
mMVPMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uMVPMatrix");
// Pass the projection and view transformation to the shader
GLES20.glUniformMatrix4fv(mMVPMatrixHandle, 1, false, mvpMatrix, 0);
// Draw the triangle
GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, vertexCount);
// Disable vertex array
GLES20.glDisableVertexAttribArray(mPositionHandle);
}
跑起來,效果如下:
5. 添加動作
前面主要一個基本的OpenGL的繪制圖像功能,實際上你還可以將其與Canvas和Drawable配合一起使用.OpenGL ES還提供了其他的功能,你可以使用它們來在三維空間中移動和轉(zhuǎn)換繪制對象.
5.1 旋轉(zhuǎn)shape
要旋轉(zhuǎn)一個圖像,只需要創(chuàng)建一個變換矩陣(a rotation matrix)然后將其與projection和camera view的轉(zhuǎn)換矩陣combine:
private float[] mRotationMatrix = new float[16];
public void onDrawFrame(GL10 gl) {
float[] scratch = new float[16];
...
// Create a rotation transformation for the triangle
long time = SystemClock.uptimeMillis() % 4000L;
float angle = 0.090f * ((int) time);
Matrix.setRotateM(mRotationMatrix, 0, angle, 0, 0, -1.0f);
// Combine the rotation matrix with the projection and camera view
// Note that the mMVPMatrix factor *must be first* in order
// for the matrix multiplication product to be correct.
Matrix.multiplyMM(scratch, 0, mMVPMatrix, 0, mRotationMatrix, 0);
// Draw triangle
mTriangle.draw(scratch);
}
效果如下:
注: 若圖像沒有旋轉(zhuǎn),你可能是設將GLSurfaceview的模式設置為GLSurfaceView.RENDERMODE_WHEN_DIRTY,將其注釋掉即可.
5.2 允許持續(xù)渲染
如前面所說,若要運行繪制對象持續(xù)渲染,如下將渲染模式設置為RENDERMODE_CONTINUOUSLY(也是默認的),或者直接將之前設置為其他的方法注釋掉:
public MyGLSurfaceView(Context context) {
...
// Render the view only when there is a change in the drawing data.
// To allow the triangle to rotate automatically, this line is commented out:
//setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
}
6. 響應用戶操作
6.1 設置Touch監(jiān)聽
為了響應用戶的touch事件,你必須要在你的GLSurfaceView實現(xiàn)類中實現(xiàn)onTouchEvent()方法,如下:
private final float TOUCH_SCALE_FACTOR = 180.0f / 320;
private float mPreviousX;
private float mPreviousY;
@Override
public boolean onTouchEvent(MotionEvent e) {
// MotionEvent reports input details from the touch screen
// and other input controls. In this case, you are only
// interested in events where the touch position changed.
float x = e.getX();
float y = e.getY();
switch (e.getAction()) {
case MotionEvent.ACTION_MOVE:
float dx = x - mPreviousX;
float dy = y - mPreviousY;
// reverse direction of rotation above the mid-line
if (y > getHeight() / 2) {
dx = dx * -1 ;
}
// reverse direction of rotation to left of the mid-line
if (x < getWidth() / 2) {
dy = dy * -1 ;
}
mRenderer.setAngle(
mRenderer.getAngle() +
((dx + dy) * TOUCH_SCALE_FACTOR));
requestRender();
}
mPreviousX = x;
mPreviousY = y;
return true;
}
要注意的是在計算完角度后,要調(diào)用 requestRender ()來告訴渲染器去渲染,這是本例中最有效的方法,因為這可以減少渲染器的無用渲染次數(shù),當然該這也要將渲染模式設置為GLSurfaceView.RENDERMODE_WHEN_DIRTY才有效:
public MyGLSurfaceView(Context context) {
...
// Render the view only when there is a change in the drawing data
setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
}
6.2 暴露旋轉(zhuǎn)角度
由于渲染器是在其他的線程上運行的,你需要將角度變量聲明為volatile
:
public class MyGLRenderer implements GLSurfaceView.Renderer {
...
public volatile float mAngle;
public float getAngle() {
return mAngle;
}
public void setAngle(float angle) {
mAngle = angle;
}
}
6.3 應用旋轉(zhuǎn)
將之前生成角度的方法注釋掉,取而代之的是touch事件產(chǎn)生的角度:
public void onDrawFrame(GL10 gl) {
...
float[] scratch = new float[16];
// Create a rotation for the triangle
// long time = SystemClock.uptimeMillis() % 4000L;
// float angle = 0.090f * ((int) time);
Matrix.setRotateM(mRotationMatrix, 0, mAngle, 0, 0, -1.0f);
// Combine the rotation matrix with the projection and camera view
// Note that the mMVPMatrix factor *must be first* in order
// for the matrix multiplication product to be correct.
Matrix.multiplyMM(scratch, 0, mMVPMatrix, 0, mRotationMatrix, 0);
// Draw triangle
mTriangle.draw(scratch);
}
最終效果如下:
總結(jié)
本篇只是通過一個例子簡單介紹了Android中OpenGL ES的非常基本的使用, 只是熟悉一下流程,來發(fā)現(xiàn)該功能的強大,背后的原理還是需要去其他的地方學習.