Android OpenGL顯示任意3D模型檔案
*本篇文章已授權微信公眾號 guolin_blog (郭霖)獨家釋出
前面兩篇文章我們介紹了OpenGL相關的基本知識,現在我們已經會繪製基本的圖案了,但是還遠遠不能滿足我們的需求。我們要做的是顯示任意的模型,這也是本文所要做的事情。在閱讀本文之前,請先確保你已經看過我前面兩篇文章:
雖然標題是說顯示任意3D檔案,但是本文主要是以STL格式檔案為例。其他的格式本質上都是一樣的,只是解析部分的程式碼不同而已。接下來我們開始學習~
1 STL檔案
它是標準的3D檔案格式,一般3D印表機都是支援列印STL檔案,關於STL檔案的格式、以及相關介紹請參考百度百科:【stl格式】。當然了,我在程式碼的註釋中也會進行相關解釋。
1.1 解析準備
首先,在解析STL檔案格式之前,我們需要進行構思。我們無非就是把STL檔案中的三角形的頂點資訊提取出來。因此我們的主要目標就是把所有點資訊讀取出來。
但是,3D模型的座標位置很隨機,大小也隨機。而不同的模型所處的位置不同,為了能夠讓模型處於手機顯示中心,我們必須對模型進行移動、放縮處理。使得任意大小、任意位置的模型都能在我們的GLSurfaceView中以“相同”的大小顯示。
因此,我們不僅僅要讀取頂點資訊,而且還要獲取模型的邊界資訊。我們想象成一個立方體,這個立方體剛好包裹住模型。即我們要讀取x、y、z三個方向上的最大值最小值。
1.2 開始解析
首先,我們定義一個Model類,用於表示一個模型物件:
package com.hc.opengl;
import java.nio.FloatBuffer;
/**
* Package com.hc.opengl
* Created by HuaChao on 2016/7/28.
*/
public classModel {
//三角面個數
private int facetCount;
//頂點座標陣列
private float[] verts;
//每個頂點對應的法向量陣列
private float[] vnorms;
//每個三角面的屬性資訊
private short[] remarks;
//頂點陣列轉換而來的Buffer
private FloatBuffer vertBuffer;
//每個頂點對應的法向量轉換而來的Buffer
private FloatBuffer vnormBuffer;
//以下分別儲存所有點在x,y,z方向上的最大值、最小值
float maxX;
float minX;
float maxY;
float minY;
float maxZ;
float minZ;
//返回模型的中心點
//注意,下載的原始碼中,此函式修改修正如下
public Point getCentrePoint() {
float cx = minX + (maxX - minX) / 2;
float cy = minY + (maxY - minY) / 2;
float cz = minZ + (maxZ - minZ) / 2;
return new Point(cx, cy, cz);
}
//包裹模型的最大半徑
public float getR() {
float dx = (maxX - minX);
float dy = (maxY - minY);
float dz = (maxZ - minZ);
float max = dx;
if (dy > max)
max = dy;
if (dz > max)
max = dz;
return max;
}
//設定頂點陣列的同時,設定對應的Buffer
public void setVerts(float[] verts) {
this.verts = verts;
vertBuffer = Util.floatToBuffer(verts);
}
//設定頂點陣列法向量的同時,設定對應的Buffer
public void setVnorms(float[] vnorms) {
this.vnorms = vnorms;
vnormBuffer = Util.floatToBuffer(vnorms);
}
//···
//其他屬性對應的setter、getter函式
//···
}
- 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
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
接下來就是將stl檔案轉換成Model物件,我們定義一個STLReader類:
package com.hc.opengl;
import android.content.Context;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
/**
* Package com.hc.opengl
* Created by HuaChao on 2016/7/28.
*/
public classSTLReader {
private StlLoadListener stlLoadListener;
public Model parserBinStlInSDCard(String path)
throws IOException {
File file = new File(path);
FileInputStream fis = new FileInputStream(file);
return parserBinStl(fis);
}
public Model parserBinStlInAssets(Context context, String fileName)
throws IOException {
InputStream is = context.getAssets().open(fileName);
return parserBinStl(is);
}
//解析二進位制的Stl檔案
public Model parserBinStl(InputStream in) throws IOException {
if (stlLoadListener != null)
stlLoadListener.onstart();
Model model = new Model();
//前面80位元組是檔案頭,用於存貯檔名;
in.skip(80);
//緊接著用 4 個位元組的整數來描述模型的三角面片個數
byte[] bytes = new byte[4];
in.read(bytes);// 讀取三角面片個數
int facetCount = Util.byte4ToInt(bytes, 0);
model.setFacetCount(facetCount);
if (facetCount == 0) {
in.close();
return model;
}
// 每個三角面片佔用固定的50個位元組
byte[] facetBytes = new byte[50 * facetCount];
// 將所有的三角面片讀取到位元組陣列
in.read(facetBytes);
//資料讀取完畢後,可以把輸入流關閉
in.close();
parseModel(model, facetBytes);
if (stlLoadListener != null)
stlLoadListener.onFinished();
return model;
}
/**
* 解析模型資料,包括頂點資料、法向量資料、所佔空間範圍等
*/
private void parseModel(Model model, byte[] facetBytes) {
int facetCount = model.getFacetCount();
/**
* 每個三角面片佔用固定的50個位元組,50位元組當中:
* 三角片的法向量:(1個向量相當於一個點)*(3維/點)*(4位元組浮點數/維)=12位元組
* 三角片的三個點座標:(3個點)*(3維/點)*(4位元組浮點數/維)=36位元組
* 最後2個位元組用來描述三角面片的屬性資訊
* **/
// 儲存所有頂點座標資訊,一個三角形3個頂點,一個頂點3個座標軸
float[] verts = new float[facetCount * 3 * 3];
// 儲存所有三角面對應的法向量位置,
// 一個三角面對應一個法向量,一個法向量有3個點
// 而繪製模型時,是針對需要每個頂點對應的法向量,因此儲存長度需要*3
// 又同一個三角面的三個頂點的法向量是相同的,
// 因此後面寫入法向量資料的時候,只需連續寫入3個相同的法向量即可
float[] vnorms = new float[facetCount * 3 * 3];
//儲存所有三角面的屬性資訊
short[] remarks = new short[facetCount];
int stlOffset = 0;
try {
for (int i = 0; i < facetCount; i++) {
if (stlLoadListener != null) {
stlLoadListener.onLoading(i, facetCount);
}
for (int j = 0; j < 4; j++) {
float x = Util.byte4ToFloat(facetBytes, stlOffset);
float y = Util.byte4ToFloat(facetBytes, stlOffset + 4);
float z = Util.byte4ToFloat(facetBytes, stlOffset + 8);
stlOffset += 12;
if (j == 0) {//法向量
vnorms[i * 9] = x;
vnorms[i * 9 + 1] = y;
vnorms[i * 9 + 2] = z;
vnorms[i * 9 + 3] = x;
vnorms[i * 9 + 4] = y;
vnorms[i * 9 + 5] = z;
vnorms[i * 9 + 6] = x;
vnorms[i * 9 + 7] = y;
vnorms[i * 9 + 8] = z;
} else {//三個頂點
verts[i * 9 + (j - 1) * 3] = x;
verts[i * 9 + (j - 1) * 3 + 1] = y;
verts[i * 9 + (j - 1) * 3 + 2] = z;
//記錄模型中三個座標軸方向的最大最小值
if (i == 0 && j == 1) {
model.minX = model.maxX = x;
model.minY = model.maxY = y;
model.minZ = model.maxZ = z;
} else {
model.minX = Math.min(model.minX, x);
model.minY = Math.min(model.minY, y);
model.minZ = Math.min(model.minZ, z);
model.maxX = Math.max(model.maxX, x);
model.maxY = Math.max(model.maxY, y);
model.maxZ = Math.max(model.maxZ, z);
}
}
}
short r = Util.byte2ToShort(facetBytes, stlOffset);
stlOffset = stlOffset + 2;
remarks[i] = r;
}
} catch (Exception e) {
if (stlLoadListener != null) {
stlLoadListener.onFailure(e);
} else {
e.printStackTrace();
}
}
//將讀取的資料設定到Model物件中
model.setVerts(verts);
model.setVnorms(vnorms);
model.setRemarks(remarks);
}
public static interfaceStlLoadListener {
void onstart();
void onLoading(int cur, int total);
void onFinished();
void onFailure(Exception e);
}
}
- 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
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
- 121
- 122
- 123
- 124
- 125
- 126
- 127
- 128
- 129
- 130
- 131
- 132
- 133
- 134
- 135
- 136
- 137
- 138
- 139
- 140
- 141
- 142
- 143
- 144
- 145
- 146
- 147
- 148
- 149
- 150
- 151
- 152
- 153
- 154
- 155
- 156
- 157
注意到,我們需要頻繁的將byte陣列轉為short、float型別,我們直接把這些函式裝到一個工具類Util中:
package com.hc.opengl;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
/**
* Package com.hc.opengl
* Created by HuaChao on 2016/7/28.
*/
public classUtil {
public static FloatBuffer floatToBuffer(float[] a) {
//先初始化buffer,陣列的長度*4,因為一個float佔4個位元組
ByteBuffer bb = ByteBuffer.allocateDirect(a.length * 4);
//陣列排序用nativeOrder
bb.order(ByteOrder.nativeOrder());
FloatBuffer buffer = bb.asFloatBuffer();
buffer.put(a);
buffer.position(0);
return buffer;
}
public static int byte4ToInt(byte[] bytes, int offset) {
int b3 = bytes[offset + 3] & 0xFF;
int b2 = bytes[offset + 2] & 0xFF;
int b1 = bytes[offset + 1] & 0xFF;
int b0 = bytes[offset + 0] & 0xFF;
return (b3 << 24) | (b2 << 16) | (b1 << 8) | b0;
}
public static short byte2ToShort(byte[] bytes, int offset) {
int b1 = bytes[offset + 1] & 0xFF;
int b0 = bytes[offset + 0] & 0xFF;
return (short) ((b1 << 8) | b0);
}
public static float byte4ToFloat(byte[] bytes, int offset) {
return Float.intBitsToFloat(byte4ToInt(bytes, offset));
}
}
- 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
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
為了更好的表示三維座標系下的一個點,我們定義Point類:
/**
* Package com.hc.opengl
* Created by HuaChao on 2016/7/28.
*/
public classPoint {
public float x;
public float y;
public float z;
public Point(float x, float y, float z) {
this.x = x;
this.y = y;
this.z = z;
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
2 編寫Render
上一節我們只是拿資料而已,還沒開始繪製,真正的大招現在才開始。因為我們目標是顯示任意模型,因此,必須把模型移動到我們的“視野”中,才能看得到(當然了,如果圖形本身就是在我們的視野中,那就不一定需要這樣的操作了)。廢話不多說,直接看原始碼:
/**
* Package com.hc.opengl
* Created by HuaChao on 2016/7/28.
*/
public classGLRendererimplementsGLSurfaceView.Renderer {
private Model model;
private Point mCenterPoint;
private Point eye = new Point(0, 0, -3);
private Point up = new Point(0, 1, 0);
private Point center = new Point(0, 0, 0);
private float mScalef = 1;
private float mDegree = 0;
public GLRenderer(Context context) {
try {
model = new STLReader().parserBinStlInAssets(context, "huba.stl");
} catch (IOException e) {
e.printStackTrace();
}
}
public void rotate(float degree) {
mDegree = degree;
}
@Override
public void onDrawFrame(GL10 gl) {
// 清除螢幕和深度快取
gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
gl.glLoadIdentity();// 重置當前的模型觀察矩陣
//眼睛對著原點看
GLU.gluLookAt(gl, eye.x, eye.y, eye.z, center.x,
center.y, center.z, up.x, up.y, up.z);
//為了能有立體感覺,通過改變mDegree值,讓模型不斷旋轉
gl.glRotatef(mDegree, 0, 1, 0);
//將模型放縮到View剛好裝下
gl.glScalef(mScalef, mScalef, mScalef);
//把模型移動到原點
gl.glTranslatef(-mCenterPoint.x, -mCenterPoint.y,
-mCenterPoint.z);
//===================begin==============================//
//允許給每個頂點設定法向量
gl.glEnableClientState(GL10.GL_NORMAL_ARRAY);
// 允許設定頂點
gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
// 允許設定顏色
//設定法向量資料來源
gl.glNormalPointer(GL10.GL_FLOAT, 0, model.getVnormBuffer());
// 設定三角形頂點資料來源
gl.glVertexPointer(3, GL10.GL_FLOAT, 0, model.getVertBuffer());
// 繪製三角形
gl.glDrawArrays(GL10.GL_TRIANGLES, 0, model.getFacetCount() * 3);
// 取消頂點設定
gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);
//取消法向量設定
gl.glDisableClientState(GL10.GL_NORMAL_ARRAY);
//=====================end============================//
}
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
// 設定OpenGL場景的大小,(0,0)表示視窗內部視口的左下角,(width, height)指定了視口的大小
gl.glViewport(0, 0, width, height);
gl.glMatrixMode(GL10.GL_PROJECTION); // 設定投影矩陣
gl.glLoadIdentity(); // 設定矩陣為單位矩陣,相當於重置矩陣
GLU.gluPerspective(gl, 45.0f, ((float) width) / height, 1f, 100f);// 設定透視範圍
//以下兩句宣告,以後所有的變換都是針對模型(即我們繪製的圖形)
gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
}
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
gl.glEnable(GL10.GL_DEPTH_TEST); // 啟用深度快取
gl.glClearDepthf(1.0f); // 設定深度快取值
gl.glDepthFunc(GL10.GL_LEQUAL); // 設定深度快取比較函式
gl.glShadeModel(GL10.GL_SMOOTH);// 設定陰影模式GL_SMOOTH
float r = model.getR();
//r是半徑,不是直徑,因此用0.5/r可以算出放縮比例
mScalef = 0.5f / r;
mCenterPoint = model.getCentrePoint();
}
}
- 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
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
在MainActivity中不斷呼叫旋轉函式:
package com.hc.opengl;
public classMainActivityextendsAppCompatActivity {
private boolean supportsEs2;
private GLSurfaceView glView;
private float rotateDegreen = 0;
private GLRenderer glRenderer;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
checkSupported();
if (supportsEs2) {
glView = new GLSurfaceView(this);
glRenderer = new GLRenderer(this);
glView.setRenderer(glRenderer);
setContentView(glView);
} else {
setContentView(R.layout.activity_main);
Toast.makeText(this, "當前裝置不支援OpenGL ES 2.0!", Toast.LENGTH_SHORT).show();
}
}
public void rotate(float degree) {
glRenderer.rotate(degree);
glView.invalidate();
}
private Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
rotate(rotateDegreen);
}
};
@Override
protected void onResume() {
super.onResume();
if (glView != null) {
glView.onResume();
//不斷改變rotateDegreen值,實現旋轉
new Thread() {
@Override
public void run() {
while (true) {
try {
sleep(100);
rotateDegreen += 5;
handler.sendEmptyMessage(0x001);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}.start();
}
}
private void checkSupported() {
ActivityManager activityManager = (ActivityManager) getSystemService(ACTIVITY_SERVICE);
ConfigurationInfo configurationInfo = activityManager.getDeviceConfigurationInfo();
supportsEs2 = configurationInfo.reqGlEsVersion >= 0x2000;
boolean isEmulator = 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"));
supportsEs2 = supportsEs2 || isEmulator;
}
@Override
protected void onPause() {
super.onPause();
if (glView != null) {
glView.onPause();
}
}
}
- 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
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
3 最後一步
一切看起來都已經完成了,但似乎少了點什麼。啊哈~,少了STL檔案,其實網上有很多STL模型檔案免費下載,大家可以隨便搜尋。我下載了一個胡巴的模型:
下載完成後,執行如下:
看到結果是不是覺得很失望?貌似看不到輪廓,其實,主要是跟燈光有關,我們程式中沒有設定燈光。我們知道,我們在真實世界中看到物體主要是物體表面發生漫反射。我們所看到的物體跟光源的位置、物體的材質等等有關。另外,也可以通過貼紋理來做到。但是到目前為止,我們還沒有這些知識,程式碼裡面也沒有涉及到這些,因此我們這能看到當前這個樣子。後面我們會繼續深入學習相關知識,歡迎關注~。
好啦,最後獻上原始碼吧~,注意,下載的原始碼中Model
類的getCentrePoint
函式需要修改,請以本文中的Model
類為主。