Android移動開發-使用OpenGL來繪製3D紋理圖的實現
OpenGL(全寫Open Graphics Library)是指定義了一個跨程式語言、跨平臺的程式設計介面規格的專業的圖形程式介面。它用於三維影象(二維的亦可),是一個功能強大,呼叫方便的底層圖形庫。
OpenGL是行業領域中最為廣泛接納的 2D/3D 圖形 API,其自誕生至今已催生了各種計算機平臺及裝置上的數千優秀應用程式。OpenGL是獨立於視窗作業系統或其它作業系統的,亦是網路透明的。在包含CAD、內容創作、能源、娛樂、遊戲開發、製造業、製藥業及虛擬現實等行業領域中,OpenGL幫助程式設計師實現在 PC、工作站、超級計算機等硬體裝置上的高效能、極具衝擊力的高視覺表現力圖形處理軟體的開發。
雖然OpenGL本身是一種高效、簡潔的開發圖形庫的介面,它定義了一個跨程式語言、跨平臺的程式設計介面規範,主要用於三維圖形程式設計。但在手機等手持終端上執行OpenGL有些不太適合,所以Android系統內建的是 OpenGL ES 的支援。在3D繪製中進行的紋理貼圖也很簡單,與設定頂點顏色的步驟相似,只要三步,步驟如下:
Step1:設定啟用貼圖座標陣列;
Step2:設定貼圖座標的陣列資訊;
Step3:呼叫GL10的glBindTexture(int target, int texture)方法執行貼圖。下面的程式Dome示範瞭如何為一個立方體進行貼圖,而且這個程式還提供了手勢檢測器,允許使用者通過手勢來改變該立方體的角度。
- MainActivity.java邏輯程式碼如下:
package com.fukaimei.texture3d;
import android.app.Activity;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.opengl.GLSurfaceView;
import android.opengl.GLSurfaceView.Renderer;
import android.opengl.GLUtils;
import android.os.Bundle;
import android.view.GestureDetector;
import android.view.GestureDetector.OnGestureListener;
import android.view.MotionEvent;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
public class MainActivity extends Activity implements OnGestureListener {
// 定義旋轉角度
private float anglex = 0f;
private float angley = 0f;
static final float ROTATE_FACTOR = 60;
// 定義手勢檢測器例項
GestureDetector detector;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 建立一個GLSurfaceView,用於顯示OpenGL繪製的圖形
GLSurfaceView glView = new GLSurfaceView(this);
// 建立GLSurfaceView的內容繪製器
MyRenderer myRender = new MyRenderer(this);
// 為GLSurfaceView設定繪製器
glView.setRenderer(myRender);
setContentView(glView);
// 建立手勢檢測器
detector = new GestureDetector(this, this);
}
@Override
public boolean onTouchEvent(MotionEvent me) {
// 將該Activity上的觸碰事件交給GestureDetector處理
return detector.onTouchEvent(me);
}
@Override
public boolean onFling(MotionEvent event1, MotionEvent event2,
float velocityX, float velocityY) {
velocityX = velocityX > 2000 ? 2000 : velocityX;
velocityX = velocityX < -2000 ? -2000 : velocityX;
velocityY = velocityY > 2000 ? 2000 : velocityY;
velocityY = velocityY < -2000 ? -2000 : velocityY;
// 根據橫向上的速度計算沿Y軸旋轉的角度
angley += velocityX * ROTATE_FACTOR / 4000;
// 根據縱向上的速度計算沿X軸旋轉的角度
anglex += velocityY * ROTATE_FACTOR / 4000;
return true;
}
@Override
public boolean onDown(MotionEvent arg0) {
return false;
}
@Override
public void onLongPress(MotionEvent event) {
}
@Override
public boolean onScroll(MotionEvent event1, MotionEvent event2,
float distanceX, float distanceY) {
return false;
}
@Override
public void onShowPress(MotionEvent event) {
}
@Override
public boolean onSingleTapUp(MotionEvent event) {
return false;
}
public class MyRenderer implements Renderer {
// 立方體的頂點座標(一共是36個頂點,組成12個三角形)
private float[] cubeVertices = {-0.6f, -0.6f, -0.6f, -0.6f, 0.6f,
-0.6f, 0.6f, 0.6f, -0.6f, 0.6f, 0.6f, -0.6f, 0.6f, -0.6f, -0.6f,
-0.6f, -0.6f, -0.6f, -0.6f, -0.6f, 0.6f, 0.6f, -0.6f, 0.6f, 0.6f,
0.6f, 0.6f, 0.6f, 0.6f, 0.6f, -0.6f, 0.6f, 0.6f, -0.6f, -0.6f,
0.6f, -0.6f, -0.6f, -0.6f, 0.6f, -0.6f, -0.6f, 0.6f, -0.6f, 0.6f,
0.6f, -0.6f, 0.6f, -0.6f, -0.6f, 0.6f, -0.6f, -0.6f, -0.6f, 0.6f,
-0.6f, -0.6f, 0.6f, 0.6f, -0.6f, 0.6f, 0.6f, 0.6f, 0.6f, 0.6f,
0.6f, 0.6f, -0.6f, 0.6f, 0.6f, -0.6f, -0.6f, 0.6f, 0.6f, -0.6f,
-0.6f, 0.6f, -0.6f, -0.6f, 0.6f, 0.6f, -0.6f, 0.6f, 0.6f, 0.6f,
0.6f, 0.6f, 0.6f, 0.6f, -0.6f, -0.6f, 0.6f, -0.6f, -0.6f, -0.6f,
-0.6f, -0.6f, -0.6f, 0.6f, -0.6f, -0.6f, 0.6f, -0.6f, 0.6f, 0.6f,
-0.6f, 0.6f, -0.6f,};
// 定義立方體所需要的6個面(一共是12個三角形所需的頂點)
private byte[] cubeFacets = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12,
13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29,
30, 31, 32, 33, 34, 35,};
// 定義紋理貼圖的座標資料
private float[] cubeTextures = {1.0000f, 1.0000f, 1.0000f, 0.0000f,
0.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 1.0000f, 1.0000f,
1.0000f, 0.0000f, 1.0000f, 1.0000f, 1.0000f, 1.0000f, 0.0000f,
1.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 1.0000f, 0.0000f,
1.0000f, 1.0000f, 1.0000f, 1.0000f, 0.0000f, 1.0000f, 0.0000f,
0.0000f, 0.0000f, 0.0000f, 1.0000f, 0.0000f, 1.0000f, 1.0000f,
1.0000f, 1.0000f, 0.0000f, 1.0000f, 0.0000f, 0.0000f, 0.0000f,
0.0000f, 1.0000f, 0.0000f, 1.0000f, 1.0000f, 1.0000f, 1.0000f,
0.0000f, 1.0000f, 0.0000f, 0.0000f, 0.0000f, 0.0000f, 1.0000f,
0.0000f, 1.0000f, 1.0000f, 1.0000f, 1.0000f, 0.0000f, 1.0000f,
0.0000f, 0.0000f, 0.0000f, 0.0000f, 1.0000f};
private Context context;
private FloatBuffer cubeVerticesBuffer;
private ByteBuffer cubeFacetsBuffer;
private FloatBuffer cubeTexturesBuffer;
// 定義本程式所使用的紋理
private int texture;
public MyRenderer(Context main) {
this.context = main;
// 將立方體的頂點位置資料陣列包裝成FloatBuffer;
cubeVerticesBuffer = floatBufferUtil(cubeVertices);
// 將立方體的6個面(12個三角形)的陣列包裝成ByteBuffer
cubeFacetsBuffer = ByteBuffer.wrap(cubeFacets);
// 將立方體的紋理貼圖的座標資料包裝成FloatBuffer
cubeTexturesBuffer = floatBufferUtil(cubeTextures);
}
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
// 關閉抗抖動
gl.glDisable(GL10.GL_DITHER);
// 設定系統對透視進行修正
gl.glHint(GL10.GL_PERSPECTIVE_CORRECTION_HINT, GL10.GL_FASTEST);
gl.glClearColor(0, 0, 0, 0);
// 設定陰影平滑模式
gl.glShadeModel(GL10.GL_SMOOTH);
// 啟用深度測試
gl.glEnable(GL10.GL_DEPTH_TEST);
// 設定深度測試的型別
gl.glDepthFunc(GL10.GL_LEQUAL);
// 啟用2D紋理貼圖
gl.glEnable(GL10.GL_TEXTURE_2D);
// 裝載紋理
loadTexture(gl);
}
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
// 設定3D視窗的大小及位置
gl.glViewport(0, 0, width, height);
// 將當前矩陣模式設為投影矩陣
gl.glMatrixMode(GL10.GL_PROJECTION);
// 初始化單位矩陣
gl.glLoadIdentity();
// 計算透視視窗的寬度、高度比
float ratio = (float) width / height;
// 呼叫此方法設定透視視窗的空間大小。
gl.glFrustumf(-ratio, ratio, -1, 1, 1, 10);
}
public void onDrawFrame(GL10 gl) {
// 清除螢幕快取和深度快取
gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
// 啟用頂點座標資料
gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
// 啟用貼圖座標陣列資料
gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY); // ①
// 設定當前矩陣模式為模型檢視。
gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
// 把繪圖中心移入螢幕2個單位
gl.glTranslatef(0f, 0.0f, -2.0f);
// 旋轉圖形
gl.glRotatef(angley, 0, 1, 0);
gl.glRotatef(anglex, 1, 0, 0);
// 設定頂點的位置資料
gl.glVertexPointer(3, GL10.GL_FLOAT, 0, cubeVerticesBuffer);
// 設定貼圖的座標資料
gl.glTexCoordPointer(2, GL10.GL_FLOAT, 0, cubeTexturesBuffer); // ②
// 執行紋理貼圖
gl.glBindTexture(GL10.GL_TEXTURE_2D, texture); // ③
// 按cubeFacetsBuffer指定的面繪製三角形
gl.glDrawElements(GL10.GL_TRIANGLES, cubeFacetsBuffer.remaining(),
GL10.GL_UNSIGNED_BYTE, cubeFacetsBuffer);
// 繪製結束
gl.glFinish();
// 禁用頂點、紋理座標陣列
gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);
gl.glDisableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
// 遞增角度值以便每次以不同角度繪製
}
private void loadTexture(GL10 gl) {
Bitmap bitmap = null;
try {
// 載入點陣圖
bitmap = BitmapFactory.decodeResource(context.getResources(),
R.drawable.sand);
int[] textures = new int[1];
// 指定生成N個紋理(第一個引數指定生成一個紋理)
// textures陣列將負責儲存所有紋理的代號
gl.glGenTextures(1, textures, 0);
// 獲取textures紋理陣列中的第一個紋理
texture = textures[0];
// 通知OpenGL將texture紋理繫結到GL10.GL_TEXTURE_2D目標中
gl.glBindTexture(GL10.GL_TEXTURE_2D, texture);
// 設定紋理被縮小(距離視點很遠時被縮小)時的濾波方式
gl.glTexParameterf(GL10.GL_TEXTURE_2D,
GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_NEAREST);
// 設定紋理被放大(距離視點很近時被方法)時的濾波方式
gl.glTexParameterf(GL10.GL_TEXTURE_2D,
GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_LINEAR);
// 設定在橫向、縱向上都是平鋪紋理
gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_S,
GL10.GL_REPEAT);
gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_T,
GL10.GL_REPEAT);
// 載入點陣圖生成紋理
GLUtils.texImage2D(GL10.GL_TEXTURE_2D, 0, bitmap, 0);
} finally {
// 生成紋理之後,回收點陣圖
if (bitmap != null)
bitmap.recycle();
}
}
}
// 定義一個工具方法,將float[]陣列轉換為OpenGL ES所需的FloatBuffer
private FloatBuffer floatBufferUtil(float[] arr) {
FloatBuffer mBuffer;
// 初始化ByteBuffer,長度為arr陣列的長度*4,因為一個int佔4個位元組
ByteBuffer qbb = ByteBuffer.allocateDirect(arr.length * 4);
// 陣列排列用nativeOrder
qbb.order(ByteOrder.nativeOrder());
mBuffer = qbb.asFloatBuffer();
mBuffer.put(arr);
mBuffer.position(0);
return mBuffer;
}
}
上面的Demo程式中標①、②、③序號的程式碼就完成紋理貼圖的三個步驟;3D max 就不會像“複用”立方體的頂點——它是直接計算該立方體需要12個三角面,每個三角面需要的3個頂點,這樣一共是36個頂點——其實有大量頂點的位置是相同,但3D max 不管這些。它認為這個立方體需要36個頂點,但個頂點的位置需要X、Y、Z三個座標值,因此匯出這個立方體的頂點座標資訊的陣列就是一個長度為108的陣列。
- Demo程式執行效果介面截圖如下(使用者可以通過手勢來旋轉該立方體):