1. 程式人生 > >Android OpenGL 顯示基本圖形及相關概念解讀

Android OpenGL 顯示基本圖形及相關概念解讀

在上一篇文章中,我們知道了如何在Android開發一個OpenGL模型顯示。但是並沒有對具體模型資料進行顯示,只是展示一個背景顏色而已,在本章中,我們學習如何將一個模型資料顯示成一個具體的3D圖形。在Android中開發OpenGL程式非常簡單,但是卻有很多OpenGL相關概念是必須要清楚的,瞭解這些相關概念才能寫出正確的程式碼,否則,你寫出來的程式可能會無緣無故崩潰,或者是畫出來的模型顯示不出來等等問題。

本文是建立在上一篇文章之上,只修改GLRender類,其他部分保持不變,如果你沒有看上一篇文章,請先移步【Android OpenGL入門】

1 模型資料

前面我們說過,一個3D模型一般是由很多三角片(或四邊形)組成,因此,首先我們需要有三角形的點資料。既然是3D模型,自然每個點座標是在三維座標系中,因此,每個點需要3個數來表示。

我們定義一個三角形,需要9個數,如果我們有float型別表示一個數,那麼定義一個三角形(三個點)如下:

 private float[] mTriangleArray = {
            0f, 1f, 0f,
            -1f, -1f, 0f,
            1f, -1f, 0f
};

此時,我們就有了一個三角形的3個點資料了。但是,OpenGL並不是對堆裡面的資料進行操作,而是在直接記憶體中(Direct Memory),即操作的資料需要儲存到NIO裡面的Buffer物件中。而我們上面宣告的float[]物件儲存在堆中,因此,需要我們將float[]

物件轉為java.nio.Buffer物件。

我們可以選擇在建構函式裡面,將float[]物件轉為java.nio.Buffer,如下所示:

private FloatBuffer mTriangleBuffer;
public GLRenderer() {
    //先初始化buffer,陣列的長度*4,因為一個float佔4個位元組
    ByteBuffer bb = ByteBuffer.allocateDirect(mTriangleArray.length * 4);
    //以本機位元組順序來修改此緩衝區的位元組順序
    bb.order(ByteOrder.nativeOrder());
    mTriangleBuffer = bb.asFloatBuffer();
    //將給定float[]資料從當前位置開始,依次寫入此緩衝區
mTriangleBuffer.put(mTriangleArray); //設定此緩衝區的位置。如果標記已定義並且大於新的位置,則要丟棄該標記。 mTriangleBuffer.position(0); }

注意,ByteBufferFloatBuffer以及IntBuffer都是繼承自抽象類java.nio.Buffer

另外,OpenGL在底層的實現是C語言,與Java預設的資料儲存位元組順序可能不同,即大端小端問題。因此,為了保險起見,在將資料傳遞給OpenGL之前,我們需要指明使用本機的儲存順序。

此時,我們順利地將float[]轉為了FloatBuffer,後面繪製三角形的時候,直接通過成員變數mTriangleBuffer即可。

2 矩陣變換

在現實世界中,我們要觀察一個物體可以通過如下幾種方式:

  • 從不同位置去觀察。(檢視變換)
  • 移動或旋轉物體,放縮物體(雖然實際生活中不能放縮,但是計算機世界是可以的)。(模型變換)
  • 給物體拍照印成照片。可以做到“近大遠小”、裁剪只看部分等等透視效果。(投影變換)
  • 只拍攝物體的一部分,使得物體在照片中只顯示部分。(視口變換)

上面所述效果,可以在OpenGL中全部實現。有一點需要很清楚,就是OpenGL的變換其實都是通過矩陣相乘來實現的。

2.1 模型變換和檢視變換

高中我們學過相對運動,就是說,改變觀測點的位置與改變物體位置都可以達到等效的運動效果。因此,在OpenGL中,這兩種變換本質上用的是同一個函式。

在進行變換之前,我們需要聲明當前是使用哪種變換。在本節中,宣告使用模型檢視變換,而模型檢視變換在OpenGL中對應標識為:GL10.GL_MODELVIEW。通過glMatrixMode函式來宣告:

gl.glMatrixMode(GL10.GL_MODELVIEW);

接下來你就可以對模型進行:平移、放縮、旋轉等操作啦。但是有一點值得注意的是,在此之前,你可能針對模型做了其他的操作,而我們知道,每次操作相當於一次矩陣相乘。OpenGL中,使用“當前矩陣”表示要執行的變化,為了防止前面執行過變換“保留”在“當前矩陣”,我們需要把“當前矩陣”復位,即變為單位矩陣(對角線上的元素全為1),通過執行如下函式:

gl.glLoadIdentity();

此時,當前變換矩陣為單位矩陣,後面才可以繼續做變換,例如:


//繞(1,0,0)向量旋轉30度
gl.glRotatef(30, 1, 0, 0);

//沿x軸方向移動1個單位
gl.glTranslatef(1, 0, 0);

//x,y,z方向放縮0.1倍
gl.glScalef(0.1f, 0.1f, 0.1f);

上面的效果都是矩陣相乘實現,因此我們需要注意變換次序問題,舉個例子,假設“當前矩陣”為單位矩陣,然後乘以一個表示旋轉的矩陣R,再乘以一個表示移動的矩陣T,最後得到的矩陣,再與每個頂點相乘。假設表示模型所以頂點的矩陣為V,則實際就是((RT)V),由矩陣乘法結合律,((RT)V)=(R(TV)),這導致的就是,先移動再旋轉。即:

實際變換順序與程式碼中的順序是相反的

上面所講的都是改變物體的位置或方向來實現“相對運動”的,如果我們不想改變物體,而是改變觀察點,可以使用如下函式

/**
* gl: GL10型變數
* eyeX,eyeY,eyeZ: 觀測點座標(相機座標)
* centerX,centerY,centerZ:觀察位置的座標
* upX,upY,upZ :相機向上方向在世界座標系中的方向(即保證看到的物體跟期望的不會顛倒)
*/
GLU.gluLookAt(gl,eyeX,eyeY,eyeZ,centerX,centerY,centerZ,upX,upY,upZ);

2.2 投影變換

投影變換就是定義一個可視空間,可視空間之外的物體是看不到的(即不會再螢幕中)。在此之前,我們的三維座標中的三個座標軸取值為[-1,1],從現在開始,座標可以不再是從-11了!

OpenGL支援主要兩種投影變換:

  • 透視投影
  • 正投影

當然了,投影也是通過矩陣來實現的,如果想要設定為投影變換,跟前面類似:

gl.glMatrixMode(GL10.GL_PROJECTION);
gl.glLoadIdentity();

同樣的道理,glLoadIdentity()函式也需要立即呼叫。

通過如下函式可將當前可視空間設定為透視投影空間:

gl.glFrustumf(left,right,bottom,top,near,far);

上面函式對應引數如下圖所示(圖片出自www.opengl.org):

透視投影變換glFrustumf

當然了,也可以通過另一個函式實現相同的效果:

 GLU.gluPerspective(gl,fovy,aspect,near,far);

上面函式對應的引數如下圖所示(圖片出自www.opengl.org):

透視投影變換gluPerspective

而對於正投影來說,相當於觀察點處於無窮遠,當然了,這是一種理想狀態,但是有時使用正投影效率可能會更高。可以通過如下函式設定正投影:

gl.glOrthof(left,right,bottom,top,near,far);

上面函式對應的引數如下圖所示(圖片出自www.opengl.org):
正投影

2.3 視口變換

我們可以選擇將影象繪製到螢幕視窗的那個區域,一般預設是在整個視窗中繪製,但是,如果你不希望在整個視窗中繪製,而是在視窗的某個小區域中繪製,你也可以自己定製:

@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
    gl.glViewport(0, 0, width, height);
}

每次視窗發生變化時,我們可以設定繪製區域,即在onSurfaceChanged函式中呼叫glViewport函式。

3 啟用相關功能及配置

3.1 glClearColor()

設定清屏顏色,每次清屏時,使用該顏色填充整個螢幕。使用例子:

 gl.glClearColor(1.0f, 1.0f, 1.0f, 0f);

裡面引數分別代表RGBA,取值範圍為[0,1]而不是[0,255]

3.2 glDepthFunc()

OpenGL中物體模型的每個畫素都有一個深度快取的值(在01之間,可以看成是距離),可以通過glClearDepthf函式設定預設的“當前畫素”z值。在繪製時,通過將待繪製的模型畫素點的深度值與“當前畫素”z值進行比較,將符合條件的畫素繪製出來,不符合條件的不繪製。具體的“指定條件”可取以下值:

  • GL10.GL_NEVER:永不繪製
  • GL10.GL_LESS:只繪製模型中畫素點的z<當前畫素z值的部分
  • GL10.GL_EQUAL:只繪製模型中畫素點的z=當前畫素z值的部分
  • GL10.GL_LEQUAL:只繪製模型中畫素點的z<=當前畫素z值的部分
  • GL10.GL_GREATER :只繪製模型中畫素點的z>當前畫素z值的部分
  • GL10.GL_NOTEQUAL:只繪製模型中畫素點的z!=當前畫素z值的部分
  • GL10.GL_GEQUAL:只繪製模型中畫素點的z>=當前畫素z值的部分
  • GL10.GL_ALWAYS:總是繪製

通過目標畫素與當前畫素在z方向上值大小的比較是否滿足引數指定的條件,來決定在深度(z方向)上是否繪製該目標畫素。

注意, 該函式只有啟用“深度測試”時才有效,通過glEnable(GL_DEPTH_TEST)開啟深度測試以及glDisable(GL_DEPTH_TEST)關閉深度測試。

例子:

 gl.glDepthFunc(GL10.GL_LEQUAL);

3.3 glClearDepthf()

給深度快取設定預設值。

快取中的每個畫素的深度值預設都是這個, 假設在 gl.glDepthFunc(GL10.GL_LEQUAL);前提下:

  • 如果指定“當前畫素值”為1時,我們知道,一個模型深度值取值和範圍為[0,1]。這個時候你往裡面畫一個物體, 由於物體的每個畫素的深度值都小於等於1, 所以整個物體都被顯示了出來。
  • 如果指定“當前畫素值”為0, 物體的每個畫素的深度值都大於等於0, 所以整個物體都不可見。
    如果指定“當前畫素值”為0.5, 那麼物體就只有深度小於等於0.5的那部分才是可見的

使用例子:

gl.glClearDepthf(1.0f);

3.3 glEnable(),glDisable()

glEnable()啟用相關功能,glDisable()關閉相關功能。

比如:

//啟用深度測試
gl.glEnable(GL10.GL_DEPTH_TEST);
//關閉深度測試
gl.glDisable(GL10.GL_DEPTH_TEST)
//開啟燈照效果
gl.glEnable(GL10.GL_LIGHTING);
// 啟用光源0
gl.glEnable(GL10.GL_LIGHT0);
// 啟用顏色追蹤
gl.glEnable(GL10.GL_COLOR_MATERIAL);

3.5 glHint()

如果OpenGL在某些地方不能有效執行是,給他指定其他操作。

函式原型為:

void glHint(GLenum target,GLenum mod)

其中,target:指定所控制行為的符號常量,可以是以下值(引自【OpenGL函式思考-glHint 】):

  • GL_FOG_HINT:指定霧化計算的精度。如果OpenGL實現不能有效的支援每個畫素的霧化計算,則GL_DONT_CAREGL_FASTEST霧化效果中每個定點的計算。
  • GL_LINE_SMOOTH_HINT:指定反走樣線段的取樣質量。如果應用較大的濾波函式,GL_NICEST在光柵化期間可以生成更多的畫素段。
  • GL_PERSPECTIVE_CORRECTION_HINT:指定顏色和紋理座標的差值質量。如果OpenGL不能有效的支援透視修正引數差值,那麼GL_DONT_CAREGL_FASTEST可以執行顏色、紋理座標的簡單線性差值計算。
  • GL_POINT_SMOOTH_HINT:指定反走樣點的取樣質量,如果應用較大的濾波函式,GL_NICEST在光柵化期間可以生成更多的畫素段。
  • GL_POLYGON_SMOOTH_HINT:指定反走樣多邊形的取樣質量,如果應用較大的濾波函式,GL_NICEST在光柵化期間可以生成更多的畫素段。

mod:指定所採取行為的符號常量,可以是以下值:

  • GL_FASTEST:選擇速度最快選項。
  • GL_NICEST:選擇最高質量選項。
  • GL_DONT_CARE:對選項不做考慮。

例子:

gl.glHint(GL10.GL_PERSPECTIVE_CORRECTION_HINT, GL10.GL_NICEST);

3.6 glEnableClientState()

當我們需要啟用頂點陣列(儲存每個頂點的座標資料)、頂點顏色陣列(儲存每個頂點的顏色)等等,就要通過glEnableClientState()函式來開啟:

//以下兩步為繪製顏色與頂點前必做操作
// 允許設定頂點
//GL10.GL_VERTEX_ARRAY頂點陣列
gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
// 允許設定顏色
//GL10.GL_COLOR_ARRAY顏色陣列
gl.glEnableClientState(GL10.GL_COLOR_ARRAY);

3.7 glShadeModel()

設定著色器模式,有如下兩個選擇:

  • GL10.GL_FLAT
  • GL10.GL_SMOOTH(預設)

如果為每個頂點指定了頂點的顏色,此時:

  • GL_SMOOTH:根據頂點的不同顏色,最終以漸變的形式填充圖形。
  • GL_FLAT:假設有n個三角片,則取最後n個頂點的顏色填充著n個三角片。

使用例子:

gl.glShadeModel(GL10.GL_SMOOTH);

4 開始繪製

前面講了很多概念,但是其實都是非常值得學習的。有了這些基礎,我們才能理解如何寫OpenGL,從上一篇文章中我們知道,開發OpenGL大部分工作都是在Renderer類上面,我直接粘Renderder程式碼:

/**
 * Package com.hc.opengl
 * Created by HuaChao on 2016/7/28.
 */
public class GLRenderer implements GLSurfaceView.Renderer {
    private float[] mTriangleArray = {
            0f, 1f, 0f,
            -1f, -1f, 0f,
            1f, -1f, 0f
    };
    //三角形各頂點顏色(三個頂點)
    private float[] mColor = new float[]{
            1, 1, 0, 1,
            0, 1, 1, 1,
            1, 0, 1, 1
    };
    private FloatBuffer mTriangleBuffer;
    private FloatBuffer mColorBuffer;


    public GLRenderer() {
        //點相關
        //先初始化buffer,陣列的長度*4,因為一個float佔4個位元組
        ByteBuffer bb = ByteBuffer.allocateDirect(mTriangleArray.length * 4);
        //以本機位元組順序來修改此緩衝區的位元組順序
        bb.order(ByteOrder.nativeOrder());
        mTriangleBuffer = bb.asFloatBuffer();
        //將給定float[]資料從當前位置開始,依次寫入此緩衝區
        mTriangleBuffer.put(mTriangleArray);
        //設定此緩衝區的位置。如果標記已定義並且大於新的位置,則要丟棄該標記。
        mTriangleBuffer.position(0);


        //顏色相關
        ByteBuffer bb2 = ByteBuffer.allocateDirect(mColor.length * 4);
        bb2.order(ByteOrder.nativeOrder());
        mColorBuffer = bb2.asFloatBuffer();
        mColorBuffer.put(mColor);
        mColorBuffer.position(0);
    }

    @Override
    public void onDrawFrame(GL10 gl) {


        // 清除螢幕和深度快取
        gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
        // 重置當前的模型觀察矩陣
        gl.glLoadIdentity();

        // 允許設定頂點
        //GL10.GL_VERTEX_ARRAY頂點陣列
        gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
        // 允許設定顏色
        //GL10.GL_COLOR_ARRAY顏色陣列
        gl.glEnableClientState(GL10.GL_COLOR_ARRAY);

        //將三角形在z軸上移動
        gl.glTranslatef(0f, 0.0f, -2.0f);

        // 設定三角形
        gl.glVertexPointer(3, GL10.GL_FLOAT, 0, mTriangleBuffer);
        // 設定三角形顏色
        gl.glColorPointer(4, GL10.GL_FLOAT, 0, mColorBuffer);
        // 繪製三角形
        gl.glDrawArrays(GL10.GL_TRIANGLES, 0, 3);


        // 取消顏色設定
        gl.glDisableClientState(GL10.GL_COLOR_ARRAY);
        // 取消頂點設定
        gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);

        //繪製結束
        gl.glFinish();

    }

    @Override
    public void onSurfaceChanged(GL10 gl, int width, int height) {
        float ratio = (float) width / height;
        // 設定OpenGL場景的大小,(0,0)表示視窗內部視口的左下角,(w,h)指定了視口的大小
        gl.glViewport(0, 0, width, height);
        // 設定投影矩陣
        gl.glMatrixMode(GL10.GL_PROJECTION);
        // 重置投影矩陣
        gl.glLoadIdentity();
        // 設定視口的大小
        gl.glFrustumf(-ratio, ratio, -1, 1, 1, 10);
        //以下兩句宣告,以後所有的變換都是針對模型(即我們繪製的圖形)
        gl.glMatrixMode(GL10.GL_MODELVIEW);
        gl.glLoadIdentity();

    }

    @Override
    public void onSurfaceCreated(GL10 gl, EGLConfig config) {
        // 設定白色為清屏
        gl.glClearColor(1, 1, 1, 1);

    }
}

效果如下:

顯示效果

5 幾個重要的函式

5.1 glVertexPointer()

其實就是設定一個指標,這個指標指向頂點陣列,後面繪製三角形(或矩形)根據這裡指定的頂點陣列來讀取資料。
函式原型如下:

void glVertexPointer(int size,int type,int stride,Buffer pointer)

其中:

  • size: 每個頂點有幾個數值描述。必須是2,3 ,4 之一。
  • type: 陣列中每個頂點的座標型別。取值:GL_BYTE,GL_SHORT, GL_FIXED, GL_FLOAT
  • stride:陣列中每個頂點間的間隔,步長(位元組位移)。取值若為0,表示陣列是連續的
  • pointer:即儲存頂點的Buffer

5.2 glColorPointer()

跟上面類似,只是設定指向顏色陣列的指標。
函式原型:

void glColorPointer(
        int size,
        int type,
        int stride,
        java.nio.Buffer pointer
    );
  • size: 每種顏色元件的數量。 值必須為 3 或 4。
  • type: 顏色陣列中的每個顏色分量的資料型別。 使用下列常量指定可接受的資料型別:GL_BYTE GL_UNSIGNED_BYTEGL_SHORT GL_UNSIGNED_SHORTGL_INT GL_UNSIGNED_INTGL_FLOAT,或 GL_DOUBLE
  • stride:連續顏色之間的位元組偏移量。 當偏移量為0時,表示資料是連續的。
  • pointer:即顏色的Buffer

5.3 glDrawArrays()

繪製數組裡面所有點構成的各個三角片。

函式原型:

void glDrawArrays(
    int mode,
    int first,
    int count
);

其中:

  • mode:有三種取值
    • GL_TRIANGLES:每三個頂之間繪製三角形,之間不連線
    • GL_TRIANGLE_FAN:以V0 V1 V2,V0 V2 V3,V0 V3 V4,……的形式繪製三角形
    • GL_TRIANGLE_STRIP:順序在每三個頂點之間均繪製三角形。這個方法可以保證從相同的方向上所有三角形均被繪製。以V0 V1 V2 ,V1 V2 V3,V2 V3 V4,……的形式繪製三角形
  • first:從陣列快取中的哪一位開始繪製,一般都定義為0
  • count:頂點的數量

相關資料: