1. 程式人生 > >Android開發筆記(一百五十五)利用GL10描繪點、線、面

Android開發筆記(一百五十五)利用GL10描繪點、線、面

上一篇文章介紹了GL10的常用方法,包括如何設定顏色、如何指定座標系、如何調整鏡頭引數、如何挪動觀測方位等等,不過這些方法只是繪圖前的準備工作,真正描繪點、線、面的製圖工作並未涉及,那麼本文就來談談如何利用GL10進行實際的三維繪圖操作。

首先在三維座標系中,每個點都有x、y、z三個方向上的座標值,這樣需要三個浮點數來表示一個點。然後一個面又至少由三個點組成,例如三個點可以構成一個三角形,而四個點可以構成一個四邊形。於是OpenGL使用浮點陣列表達一塊平面區域的時候,陣列大小=該面的頂點個數*3,也就是說,每三個浮點數用來指定一個頂點的x、y、z三軸座標,所以總共需要三倍於頂點數量的浮點數才能表示這些頂點構成的平面。以下舉個定義四邊形的浮點陣列例子:
    float verticesFront[] = { 1f, 1f, 1f,   1f, 1f, -1f,   -1f, 1f, -1f,  -1f, 1f, 1f };
上述的浮點陣列一共有12個浮點數,其中每三個浮點數代表一個點,因此這個四邊形由下列座標的頂點構成:點1座標(1,1,1)、點2座標(1,1,-1)、點3座標(-1,1,-1)、點4座標(-1,1,1)。
不過這個浮點陣列並不能直接傳給OpenGL處理,因為OpenGL的底層是用C語言實現的,C語言與其它語言(如Java)預設的資料儲存方式在位元組順序上可能不同(如大端小端問題),所以其它語言的資料結構必須轉換成C語言能夠識別的形式,說白了就是翻譯。這裡面C語言能聽懂的資料結構名叫FloatBuffer,於是問題的實質就變成了如何將浮點陣列folat[]轉換為浮點快取FloatBuffer,具體的轉換過程已經有了現成的模板,開發者只管套進去即可,詳細的轉換函式程式碼如下所示:
    public static FloatBuffer getFloatBuffer(float[] array) {
        //初始化位元組緩衝區的大小=陣列長度*陣列元素大小。float型別的元素大小為Float.SIZE,
        //int型別的元素大小為Integer.SIZE,double型別的元素大小為Double.SIZE。
        ByteBuffer byteBuffer = ByteBuffer.allocateDirect(array.length * Float.SIZE);
        //以本機位元組順序來修改位元組緩衝區的位元組順序
        //OpenGL在底層的實現是C語言,與Java預設的資料儲存位元組順序可能不同,即大端小端問題。
        //因此,為了保險起見,在將資料傳遞給OpenGL之前,需要指明使用本機的儲存順序
        byteBuffer.order(ByteOrder.nativeOrder());
        //根據設定好的引數構造浮點緩衝區
        FloatBuffer floatBuffer = byteBuffer.asFloatBuffer();
        //把陣列資料寫入緩衝區
        floatBuffer.put(array);
        //設定浮點緩衝區的初始位置
        floatBuffer.position(0);
        return floatBuffer;
    }

現在有了可供OpenGL識別的FloatBuffer物件,接著描繪三維圖形就有章可循了。繪製圖形之前要先呼叫glEnableClientState方法啟用頂點開關,繪製完成之後要呼叫glDisableClientState方法禁用頂點開關,在這兩個方法之中再進行實際的點、線、面繪製操作。下面是繪製三維圖形的函式呼叫順序示例:
        // 啟用頂點開關
        gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
        // 指定三維物體的頂點座標集合
        // gl.glVertexPointer(***);
        // 在頂點座標集合之間繪製點、線、面
        // gl.glDrawArrays(***);
        // 禁用頂點開關
        gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);
注意到上面程式碼給出了描繪動作的兩個方法glVertexPointer和glDrawArrays,其中前者指定了三維物體的頂點座標集合,後者才在頂點座標集合之間繪製點、線、面。那麼這兩個方法的輸入引數又是怎樣取值的呢?先來看看glVertexPointer方法的函式引數定義,說明如下:
    void glVertexPointer(
        int size, // 指定頂點的座標維度。三維空間有x、y、z三個座標軸,所以三維空間的size為3。同理,二維平面的size為2,相對論時空觀的size為4(三維空間+時間)
        int type, // 指定頂點的資料型別。GL10.GL_FLOAT表示浮點數,GL_SHORT表示短整型,等等。
        int stride, // 指定頂點之間的間隔。通常取值為0,表示這些頂點是連續的。
        java.nio.Buffer pointer // 所有頂點座標的資料集合。這個便是前面轉換而來的FloatBuffer物件了。
    );
通常情況下,OpenGL用於處理三維空間的連續頂點的圖形繪製,故而一般可按以下格式呼叫glVertexPointer方法:
        // 三維空間,頂點的座標值為浮點數,且頂點是連續的集合
        gl.glVertexPointer(3, GL10.GL_FLOAT, 0, buffer);

再來看看glDrawArrays方法的函式引數定義,說明如下:
    void glDrawArrays(
        int mode, // 指定頂點之間的繪製模式。是隻描繪點,還是描繪頂點之間的線段,還是描繪頂點構成的平面。
        int first, // 從第first個頂點開始繪製。若無意外都是取值0,表示從陣列下標的第0個開始繪製。
        int count // 本次繪製操作的頂點數量。也就是說,從第first個點描繪到第(first+count)個頂點。
    );
這裡補充介紹一下glDrawArrays方法的繪製模式取值,常見的幾種繪製模式取值說明如下:
GL10.GL_POINTS : 只描繪各個獨立的點
GL10.GL_LINE_STRIP : 前後兩個頂點用線段連線,但不閉合(最後一個點與第一個點不連線)
GL10.GL_LINE_LOOP : 前後兩個頂點用線段連線,並且閉合(最後一個點與第一個點有線段連線)
GL10.GL_TRIANGLES : 每隔三個頂點繪製一個三角形的平面
按照本文的演示要求,只需繪製一個立方體的線段框架,因此可按以下格式呼叫glDrawArrays方法:
        // 每個面畫閉合的四邊形線段,從第0個點開始繪製,繪製四邊形的所有頂點(pointCount=4)
        gl.glDrawArrays(GL10.GL_LINE_LOOP, 0, pointCount);

好不容易囉嗦了這麼多,繪製一個簡單的立方體已經八九不離十了,還是先來瞅瞅OpenGL繪製的立方體效果長啥樣:

見過了立方體的效果圖,再來看看完整立方體的圖形繪製程式碼片段:
    // 宣告立方體六個面的頂點集合的初始浮點陣列定義
    private ArrayList<FloatBuffer> mVertices = new ArrayList<FloatBuffer>();
    float[] verticesFront = { 1f, 1f, 1f,   1f, 1f, -1f,   -1f, 1f, -1f,  -1f, 1f, 1f };
    float[] verticesBack = { 1f, -1f, 1f,  1f, -1f, -1f,  -1f, -1f, -1f, -1f, -1f, 1f };
    float[] verticesTop = { 1f, 1f, 1f,   1f, -1f, 1f,   -1f, -1f, 1f,  -1f, 1f, 1f };
    float[] verticesBottom = { 1f, 1f, -1f,  1f, -1f, -1f,  -1f, -1f, -1f, -1f, 1f, -1f };
    float[] verticesLeft = { -1f, 1f, 1f,  -1f, 1f, -1f,  -1f, -1f, -1f,  -1f, -1f, 1f };
    float[] verticesRight = { 1f, 1f, 1f,   1f, 1f, -1f,   1f, -1f, -1f,  1f, -1f, 1f };
    int pointCount = verticesFront.length/3;
    
    // 把頂點集合的資料結構由float[]轉換為FloatBuffer
    private void initVertexs() {
        mVertices.add(FileUtil.getFloatBuffer(verticesFront));
        mVertices.add(FileUtil.getFloatBuffer(verticesBack));
        mVertices.add(FileUtil.getFloatBuffer(verticesTop));
        mVertices.add(FileUtil.getFloatBuffer(verticesBottom));
        mVertices.add(FileUtil.getFloatBuffer(verticesLeft));
        mVertices.add(FileUtil.getFloatBuffer(verticesRight));
    }

    // 根據頂點資料集合,繪製立方體的線段框架
    private void drawCube(GL10 gl) {
        // 啟用頂點開關
        gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
        // 立方體由六個正方形平面組成
        for (FloatBuffer buffer : mVertices) {
            // 將頂點座標傳給 OpenGL 管道
            //size: 每個頂點有幾個數值描述。必須是2,3 ,4 之一。
            //type: 陣列中每個頂點的座標型別。取值:GL_BYTE, GL_SHORT, GL_FIXED, GL_FLOAT。
            //stride:陣列中每個頂點間的間隔,步長(位元組位移)。取值若為0,表示陣列是連續的
            //pointer:即儲存頂點的Buffer
            gl.glVertexPointer(3, GL10.GL_FLOAT, 0, buffer);
            // 用畫線的方式將點連線並畫出來
            //GL_POINTS ————繪製獨立的點
            //GL_LINE_STRIP————繪製連續的線段,不封閉
            //GL_LINE_LOOP————繪製連續的線段,封閉
            //GL_LINES————頂點兩兩連線,為多條線段構成
            //GL_TRIANGLES————每隔三個頂點構成一個三角形
            //GL_TRIANGLE_STRIP————每相鄰三個頂點組成一個三角形
            //GL_TRIANGLE_FAN————以一個點為三角形公共頂點,組成一系列相鄰的三角形
            gl.glDrawArrays(GL10.GL_LINE_LOOP, 0, pointCount);
        }
        // 禁用頂點開關
        gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);
    }

立方體算是最簡單的三維物體了,倘若是一個球體,也能按照上述的程式碼邏輯繪製球形框架,當然這個近似球體需要由許多個小三角形構成。下面是利用OpenGL繪製的球體效果圖:



點此檢視Android開發筆記的完整目錄


__________________________________________________________________________
本文現已同步釋出到微信公眾號“老歐說安卓”,開啟微信掃一掃下面的二維碼,或者直接搜尋公眾號“老歐說安卓”新增關注,更快更方便地閱讀技術乾貨。