1. 程式人生 > >Android OpenGL顯示任意3D模型檔案

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類為主。