Android 3D 遊戲開發教程– Part I-VI

開始

Android 3D 遊戲開發教程– Part I-VI

本帖最後由 huzht 於 2010-4-25 07:58 編輯
這幾篇Android 3D遊戲開發的文章原文出自一位德國人Martin 在droidnova.com寫的文章,有lixinso翻譯爲中文。
第一部分首先介紹OpenGL相關的術語,並引導你開始3D開發的第一步。
這個關於3D遊戲的系列的叫做 Vortex .
這個教程主要focus在3D編程上,其他的東西比如菜單和程序生命週期雖然是代碼的一部分,但是在這裏不會被提到。
首先開始介紹OpenGL的術語。
頂點Vertex
頂點是3D空間中的一個點,也是許多對象的基礎元素。在OpenGL中你可以生命少至二維座標(X,Y),多至四維(X,Y,Z,W). w軸是可選的,默認的值是1.0. Z軸也是可選的,默認爲0. 在這個系列中,我們將要用到3個主要的座標X,Y,Z,因爲W一般都是被用來作爲佔位符。vertex的複數是vertices(這對非英語母語的人來說比較重要,因爲這容易產生歧義)。所有的對象都是用vertices作爲它們的點,因爲點就是vertex。
三角形Triangle
三角形需要三個點才能創建。因此在OpenGL中,我們使用3個頂點來創建一個三角形。
多邊形Polygon
多邊形是至少有3個連接着的點組成的一個對象。三角形也是一個多邊形。
圖元Primitives
一個Primitive是一個三維的對象,使用三角形或者多邊形創建。形象的說,一個有50000個頂點的非常精細的模型是一個Primitive,同樣一個只有500個頂點的低模也叫做一個Primitive。
現在我們可以開始變成了。
創建一個工程交Vortex,activity也是這個名字。我們的工程應該大概是這個樣子的: 

package com.droidnova.android.games.vortex;

import android.app.Activity;

import android.os.Bundle;

public class Vortex extends Activity {

    private static final String LOG_TAG = Vortex.class.getSimpleName();

    private VortexView _vortexView;

    @Override

    protected void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        _vortexView = new VortexView(this);

        setContentView(_vortexView);

    }

}

複製代碼

如上圖所示,我們已經添加了View。讓我們看一下VortexView類。 

package com.droidnova.android.games.vortex;

import android.content.Context;

import android.opengl.GLSurfaceView;

public class VortexView extends GLSurfaceView {

    private static final String LOG_TAG = VortexView.class.getSimpleName();

    private VortexRenderer _renderer;

    public VortexView(Context context) {

        super(context);

        _renderer = new VortexRenderer();

        setRenderer(_renderer);

    }

}

複製代碼

如上所示,我們繼承了GLSurfaceView是因爲它會幫助我們畫3D圖像。接下來看VortexRenderer類。一個Renderer包含畫一幀所必需的所有東西。 引用自這兒references 。Renderer負責OpenGL call來render一個幀。
來看一下這個類: 

package com.droidnova.android.games.vortex;

import javax.microedition.khronos.egl.EGLConfig;

import javax.microedition.khronos.opengles.GL10;

import android.opengl.GLSurfaceView;

public class VortexRenderer implements GLSurfaceView.Renderer {

private static final String LOG_TAG = VortexRenderer.class.getSimpleName();

private float _red = 0.9f;

private float _green = 0.2f;

private float _blue = 0.2f;

@Override

public void onSurfaceCreated(GL10 gl, EGLConfig config) {

// Do nothing special.

}

@Override

public void onSurfaceChanged(GL10 gl, int w, int h) {

gl.glViewport(0, 0, w, h);

}

@Override

public void onDrawFrame(GL10 gl) {

// define the color we want to be displayed as the "clipping wall"

gl.glClearColor(_red, _green, _blue, 1.0f);

// clear the color buffer to show the ClearColor we called above...

gl.glClear(GL10.GL_COLOR_BUFFER_BIT);

}

}

複製代碼

好,我們做了什麼?
首先我們實現了GLSurfaceView.Renderer這個接口,主要是實現3個方法:onSurfaceCreated(), onSurfaceChanged() 和 onDrawFrame()。這些方法很容易理解,第一個在surface創建以後調用,第二個是在surface發生改變以後調用,例如從豎屏切換到橫屏的時候,最後一個方法是當任何時候調用一個畫圖方法的時候。
從11行到13行,我們用浮點數來定義RGB顏色系統中的每一個顏色。
在28行,我們通過glClearColor()方法爲底色定義了顏色。底色是在我們能看到的所有東西的後面,所以所有在底色後面的東西都是不可見的。可以想象這種東西爲濃霧,擋住了所有的東西。然後我們將要爲之設置距離來show一下它怎麼用的。那時候你就一定會明白它是怎麼存在的了。
爲了讓顏色變化可見,我們必須調用glClear()以及顏色緩衝的Mask來清空buffer,然後爲我們的底色使用新的底色。
爲了能看到它在起作用,我們這裏爲MotionEvent創建一個response,使用它來改變顏色。首先在VortexRenderer中來創建一個設置顏色的函數。 

public void setColor(float r, float g, float b) {

    _red = r;

    _green = g;

    _blue = b;

}

複製代碼

下面是VortexView類中創建的方法來處理MotionEvent。 

public boolean onTouchEvent(final MotionEvent event) {

queueEvent(new Runnable() {

public void run() {

_renderer.setColor(event.getX() / getWidth(), event.getY() / getHeight(), 1.0f);

}

});

return true;

}

複製代碼

我們創建了一個匿名的Runnable對象,這裏的run()方法調用renderer中的setColor方法。這有會根據MotionEvent座標做一些小的計算。
現在我們已經有了一個小小的程序來使用OpenGl來改變我們的背景色了。
在德語中我們叫這種小case爲“Mit Kanonen auf Spatzen schießen”,翻譯過來應該是“你在車輪上打死了一隻蒼蠅”。這說的恰到好處,這只是一個最最最小的例子,要學習OpenGL,你現在要準備更多更多的東西。
這部分最後提一下OpenGL的文檔documentation for OpenGL 。這個東西雖然可用想不高,但是它最少是一個文檔。
Eclipse工程源代碼在這裏下載(原地址): Vortex Part I
這裏是幾個截圖:
wps_clip_image-28293
wps_clip_image-14303
wps_clip_image-18782
------------------------------------------------------------------
這個系列的第二部分是關於如何添加一個三角形並可以旋轉它。
第一件事情是初始化需要顯示的三角形。我們來在VortexRenderer類中添加一個方法initTriangle()。 

// new object variables we need

// a raw buffer to hold indices

private ShortBuffer _indexBuffer;

// a raw buffer to hold the vertices

private FloatBuffer _vertexBuffer;

private short[] _indicesArray = {0, 1, 2};

private int _nrOfVertices = 3;

// code snipped

private void initTriangle() {

    // float has 4 bytes

    ByteBuffer vbb = ByteBuffer.allocateDirect(_nrOfVertices * 3 * 4);

    vbb.order(ByteOrder.nativeOrder());

    _vertexBuffer = vbb.asFloatBuffer();

    // short has 2 bytes

    ByteBuffer ibb = ByteBuffer.allocateDirect(_nrOfVertices * 2);

    ibb.order(ByteOrder.nativeOrder());

    _indexBuffer = ibb.asShortBuffer();

    float[] coords = {

        -0.5f, -0.5f, 0f, // (x1, y1, z1)

        0.5f, -0.5f, 0f, // (x2, y2, z2)

        0f, 0.5f, 0f // (x3, y3, z3)

    };

    _vertexBuffer.put(coords);

    _indexBuffer.put(_indicesArray);

    _vertexBuffer.position(0);

    _indexBuffer.position(0);

}

複製代碼

讓我們從新的對象變量開始. _vertexBuffer爲我們的三角形保存座標._indexBuffer保存索引. _nrOfVertices變量定義需要多少個頂點.對於一個三角形來說,一共需要三個頂點.
這個方法首先爲這裏兩個buffer分配必須的內存(14-22行). 接下來我們定義一些座標(24-28行) 後面的註釋對用途給予了說明.
在30行,我們將coords數組填充給_vertexBuffer . 同樣在31行將indices數組填充給_indexBuffer 。最後將兩個buffer都設置position爲0.
爲了防止每次都對三角形進行初始化,我們僅僅在onDrawFrame()之前的行數調用它一次。一個比較好的選擇就是在onSurfaceCreated()函數中. 

@Override

public void onSurfaceCreated(GL10 gl, EGLConfig config) {

    // preparation

    gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);

    initTriangle();

}

複製代碼

glEnableClientState() 設置OpenGL使用vertex數組來畫。這是很重要的,因爲如果不這麼設置OpenGL不知道如何處理我們的數據。接下來我們就要初始化我們的三角形。
爲什麼我們不需使用不同的buffer? 在新的onDrawFrame()方法中我們必須添加一些新的OpenGL調用。 

@Override

public void onDrawFrame(GL10 gl) {

// define the color we want to be displayed as the "clipping wall"

gl.glClearColor(_red, _green, _blue, 1.0f);

// clear the color buffer to show the ClearColor we called above...

gl.glClear(GL10.GL_COLOR_BUFFER_BIT);

// set the color of our element

gl.glColor4f(0.5f, 0f, 0f, 0.5f);

// define the vertices we want to draw

gl.glVertexPointer(3, GL10.GL_FLOAT, 0, _vertexBuffer);

// finally draw the vertices

gl.glDrawElements(GL10.GL_TRIANGLES, _nrOfVertices, GL10.GL_UNSIGNED_SHORT, _indexBuffer);

}

複製代碼

好,一步一步地看。
glClearColor() 和 glClear() 在教程I部分已經提到過。在第10行使用glColor4f(red, green, blue, alpha)設置三角形爲暗紅色 .
在第13行,我們使用glVertexPointer()初始化Vertex Pointer. 第一個參數是大小,也是頂點的維數。我們使用的是x,y,z三維座標。第二個參數,GL_FLOAT定義buffer中使用的數據類型。第三個變量是0,是因爲我們的座標是在數組中緊湊的排列的,沒有使用offset。最後哦胡第四個參數頂點緩衝。
最後,glDrawElements()將所有這些元素畫出來。第一個參數定義了什麼樣的圖元將被畫出來。第二個參數定義有多少個元素,第三個是indices使用的數據類型。最後一個是繪製頂點使用的索引緩衝。
當最後測試這個應用的使用,你會看到一個在屏幕中間靜止的三角形。當你點擊屏幕的時候,屏幕的背景顏色還是會改變。
現在往裏面添加對三角形的旋轉。下面的代碼是寫在VortexRenderer類中的. 

private float _angle;

public void setAngle(float angle) {

_angle = angle;

}

複製代碼

glRotatef()方法在glColor4f()之前被onDrawFrame()調用. 

@Override

public void onDrawFrame(GL10 gl) {

// set rotation

gl.glRotatef(_angle, 0f, 1f, 0f);

gl.glColor4f(0.5f, 0f, 0f, 0.5f);

// code snipped

}

複製代碼

這時候我們可以繞y軸旋轉。如果需要改變只需要改變glRotate()方法中的0f。這個參數中的值表示一個向量,標誌三角形繞着旋轉的座標軸。
要讓它可用,我們必須在VortexView中的onTouchEvent()中添加一個調用。 

public boolean onTouchEvent(final MotionEvent event) {

    queueEvent(new Runnable() {

        public void run() {

            _renderer.setColor(event.getX() / getWidth(), event.getY() / getHeight(), 1.0f);

            _renderer.setAngle(event.getX() / 10);

        }

    });

    return true;

}

複製代碼

上面代碼中除以10是爲了減小角度變換的速度。
現在編譯運行這個程序。如果你在屏幕的最左邊點擊,你會看到三角形輕微旋轉。如果你將手指移到右邊,旋轉的速度就會變得很快。
Eclipse工程的源代碼在這裏下載(原鏈接): Vortex Part II
wps_clip_image-17772
wps_clip_image-5137
wps_clip_image-8801
--------------------------------------------------------------------------
在這個系列的第三部分給你show一下如何停止三角形的轉動,並告訴你原來的旋轉其實只是在三角形上進行的旋轉,而不是在攝像機“camera”上進行的旋轉。
我們希望能對旋轉進行更多的控制。爲此,在每次調用onDrawFrame()方法的時候都會重置這個矩陣。這會重設三角形的角度以便其總是可以旋轉到給定的角度。 

@Override

public void onDrawFrame(GL10 gl) {

    // define the color we want to be displayed as the "clipping wall"

    gl.glClearColor(_red, _green, _blue, 1.0f);

    // reset the matrix - good to fix the rotation to a static angle

    gl.glLoadIdentity();

    // clear the color buffer and the depth buffer to show the ClearColor

    // we called above...

    gl.glClear(GL10.GL_COLOR_BUFFER_BIT);

    // code snipped

}

複製代碼

在VortexView類中,你應該刪除“除以10”以便其可以旋轉範圍更大一些。 

_renderer.setAngle(event.getX());

複製代碼

如果嘗試了這些,你將會看到旋轉只會根據觸摸的到的位置來旋轉。如果沒有觸摸屏幕,旋轉不會發生改變。
下一件事情:我們旋轉的是三角形本身,還是旋轉的view/camera?
爲了驗證它,最簡單的辦法是創建第二個不旋轉的三角形進行對照。
最快也是最笨的辦法是copy&paste initTriangle()方法爲一個新的方法initStaticTriangle(),copy&paste其中的兩個buffer,copy&paste並修改onDrawFrame()方法中的最後四行。
不要忘記了改變第二個三角形的顏色以及改變第二個三角形的座標,這樣方便我們能看到兩個三角形。我將每個地方的0.5f都改成了0.4f.
這裏是整個的類: 

package com.droidnova.android.games.vortex;

import java.nio.ByteBuffer;

import java.nio.ByteOrder;

import java.nio.FloatBuffer;

import java.nio.ShortBuffer;

import javax.microedition.khronos.egl.EGLConfig;

import javax.microedition.khronos.opengles.GL10;

import android.opengl.GLSurfaceView;

public class VortexRenderer implements GLSurfaceView.Renderer {

    private static final String LOG_TAG = VortexRenderer.class.getSimpleName();

    private float _red = 0f;

    private float _green = 0f;

    private float _blue = 0f;

    // a raw buffer to hold indices allowing a reuse of points.

    private ShortBuffer _indexBuffer;

    private ShortBuffer _indexBufferStatic;

    // a raw buffer to hold the vertices

    private FloatBuffer _vertexBuffer;

    private FloatBuffer _vertexBufferStatic;

    private short[] _indicesArray = {0, 1, 2};

    private int _nrOfVertices = 3;

    private float _angle;

    @Override

    public void onSurfaceCreated(GL10 gl, EGLConfig config) {

        // preparation

        gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);

        initTriangle();

        initStaticTriangle();

    }

    @Override

    public void onSurfaceChanged(GL10 gl, int w, int h) {

        gl.glViewport(0, 0, w, h);

    }

    public void setAngle(float angle) {

        _angle = angle;

    }

    @Override

    public void onDrawFrame(GL10 gl) {

        // define the color we want to be displayed as the "clipping wall"

        gl.glClearColor(_red, _green, _blue, 1.0f);

        // reset the matrix - good to fix the rotation to a static angle

        gl.glLoadIdentity();

        // clear the color buffer to show the ClearColor we called above...

        gl.glClear(GL10.GL_COLOR_BUFFER_BIT);

        // draw the static triangle

        gl.glColor4f(0f, 0.5f, 0f, 0.5f);

        gl.glVertexPointer(3, GL10.GL_FLOAT, 0, _vertexBufferStatic);

        gl.glDrawElements(GL10.GL_TRIANGLES, _nrOfVertices, GL10.GL_UNSIGNED_SHORT, _indexBufferStatic);

        // set rotation for the non-static triangle

        gl.glRotatef(_angle, 0f, 1f, 0f);

        gl.glColor4f(0.5f, 0f, 0f, 0.5f);

        gl.glVertexPointer(3, GL10.GL_FLOAT, 0, _vertexBuffer);

        gl.glDrawElements(GL10.GL_TRIANGLES, _nrOfVertices, GL10.GL_UNSIGNED_SHORT, _indexBuffer);

    }

    private void initTriangle() {

        // float has 4 bytes

        ByteBuffer vbb = ByteBuffer.allocateDirect(_nrOfVertices * 3 * 4);

        vbb.order(ByteOrder.nativeOrder());

        _vertexBuffer = vbb.asFloatBuffer();

        // short has 4 bytes

        ByteBuffer ibb = ByteBuffer.allocateDirect(_nrOfVertices * 2);

        ibb.order(ByteOrder.nativeOrder());

        _indexBuffer = ibb.asShortBuffer();

        float[] coords = {

            -0.5f, -0.5f, 0f, // (x1, y1, z1)

            0.5f, -0.5f, 0f, // (x2, y2, z2)

            0f, 0.5f, 0f // (x3, y3, z3)

        };

        _vertexBuffer.put(coords);

        _indexBuffer.put(_indicesArray);

        _vertexBuffer.position(0);

        _indexBuffer.position(0);

    }

    private void initStaticTriangle() {

        // float has 4 bytes

        ByteBuffer vbb = ByteBuffer.allocateDirect(_nrOfVertices * 3 * 4);

        vbb.order(ByteOrder.nativeOrder());

        _vertexBufferStatic = vbb.asFloatBuffer();

        // short has 4 bytes

        ByteBuffer ibb = ByteBuffer.allocateDirect(_nrOfVertices * 2);

        ibb.order(ByteOrder.nativeOrder());

        _indexBufferStatic = ibb.asShortBuffer();

        float[] coords = {

            -0.4f, -0.4f, 0f, // (x1, y1, z1)

            0.4f, -0.4f, 0f, // (x2, y2, z2)

            0f, 0.4f, 0f // (x3, y3, z3)

        };

        _vertexBufferStatic.put(coords);

        _indexBufferStatic.put(_indicesArray);

        _vertexBufferStatic.position(0);

        _indexBufferStatic.position(0);

    }

    public void setColor(float r, float g, float b) {

        _red = r;

        _green = g;

        _blue = b;

    }

}

複製代碼

如果作了以上這些,你會看到只有一個三角形可以旋轉。如果你想兩個都可以旋轉,只需要在“draw the static triangle”這個註釋的旁邊也給它加上一行代碼就可以了。
編譯並運行這個程序,你可以看到綠色的三角形在旋轉,同時紅色的三角形還是呆在原來的地方。
這也充分驗證了我們的答案,我們旋轉的只是三角形而不是整個場景。
Eclipse工程源代碼在這裏下載: Vortex Part III
wps_clip_image-170
wps_clip_image-8788
----------------------------------------------------------------
這個系列的第四部分講如何給三角形添加顏色。
在上一部分我們創建了第二個靜態的三角形來驗證我們旋轉的是三角形而不是整個場景。這裏我們將這個靜態的三角形刪除掉。刪除掉initStaticTriangle()函數,刪除兩個buffer,_indexBufferStatic和_vertexBufferStatic。同時也要刪除原來初始靜止三角形時用到的onDrawFrame()中的最後四行。
新的onDrawFrame()方法如下: 

@Override

public void onDrawFrame(GL10 gl) {

    // define the color we want to be displayed as the "clipping wall"

    gl.glClearColor(_red, _green, _blue, 1.0f);

    // reset the matrix - good to fix the rotation to a static angle

    gl.glLoadIdentity();

    // clear the color buffer to show the ClearColor we called above...

    gl.glClear(GL10.GL_COLOR_BUFFER_BIT);

    // set rotation for the non-static triangle

    gl.glRotatef(_angle, 0f, 1f, 0f);

    gl.glColor4f(0.5f, 0f, 0f, 0.5f);

    gl.glVertexPointer(3, GL10.GL_FLOAT, 0, _vertexBuffer);

    gl.glDrawElements(GL10.GL_TRIANGLES, _nrOfVertices, GL10.GL_UNSIGNED_SHORT, _indexBuffer);

}

複製代碼

現在我們爲保存顏色信息創建一個新的buffer。這個_colorBuffer是一個對象變量,但是我們需要在initTriangle()方法中定義顏色並填充這個buffer. 

// code snipped

// a raw buffer to hold the colors

private FloatBuffer _colorBuffer;

// code snipped

private void initTriangle() {

    // float has 4 bytes

    ByteBuffer vbb = ByteBuffer.allocateDirect(_nrOfVertices * 3 * 4);

    vbb.order(ByteOrder.nativeOrder());

    _vertexBuffer = vbb.asFloatBuffer();

    // short has 4 bytes

    ByteBuffer ibb = ByteBuffer.allocateDirect(_nrOfVertices * 2);

    ibb.order(ByteOrder.nativeOrder());

    _indexBuffer = ibb.asShortBuffer();

    // float has 4 bytes, 4 colors (RGBA) * number of vertices * 4 bytes

    ByteBuffer cbb = ByteBuffer.allocateDirect(4 * _nrOfVertices * 4);

    cbb.order(ByteOrder.nativeOrder());

    _colorBuffer = cbb.asFloatBuffer();

    float[] coords = {

        -0.5f, -0.5f, 0f, // (x1, y1, z1)

        0.5f, -0.5f, 0f, // (x2, y2, z2)

        0.5f, 0.5f, 0f // (x3, y3, z3)

    };

    float[] colors = {

        1f, 0f, 0f, 1f, // point 1

        0f, 1f, 0f, 1f, // point 2

        0f, 0f, 1f, 1f, // point 3

    };

    _vertexBuffer.put(coords);

    _indexBuffer.put(_indicesArray);

    _colorBuffer.put(colors);

    _vertexBuffer.position(0);

    _indexBuffer.position(0);

    _colorBuffer.position(0);

}

複製代碼

我們創建了一個FloatBuffer類型的對象變量_colorBuffer(第四行)。在initTriangle()方法中我們爲新的顏色buffer分配了足夠多的內存(19-21行)。接下來我們創建了一個float數組(23-27行),每個頂點有4個值。 這個結構是RGBA(Red,Green,Blue,alpha)的,所以第一個頂點是紅顏色,第二個顏色是綠色,第三個顏色是藍色。最後兩部和_vertexBuffer相同。我們將顏色數組放到buffer裏面,將buffer的position設置爲0.
當這些準備工作都做完了以後,我們開始告訴OpenGL ES使用我們的顏色數組。這通過glEnableClientState(),以及glColorPointer()來完成,和vertexBuffer類似。 

@Override

public void onSurfaceCreated(GL10 gl, EGLConfig config) {

    // preparation

    gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);

    gl.glEnableClientState(GL10.GL_COLOR_ARRAY);

    initTriangle();

}

// code snipped

@Override

public void onDrawFrame(GL10 gl) {

    // code snipped

    // gl.glColor4f(0.5f, 0f, 0f, 0.5f);

    gl.glVertexPointer(3, GL10.GL_FLOAT, 0, _vertexBuffer);

    gl.glColorPointer(4, GL10.GL_FLOAT, 0, _colorBuffer);

    gl.glDrawElements(GL10.GL_TRIANGLES, _nrOfVertices, GL10.GL_UNSIGNED_SHORT, _indexBuffer);

}

複製代碼

第五行我們enable了color mode。在17行我們設置了顏色 pointer。參數4表示RGBA(RGBA剛好是四個值),其餘的幾個參數大家都比較熟悉了。
也許你也看到了,我們註釋掉了15行,因爲我們使用的是color mode,所以不再需要glColor4f。它會覆蓋,所以我們可以注註釋掉或者刪除掉他。
Eclipse工程源代碼參考: Vortex Part IV
wps_clip_image-16178
wps_clip_image-31928
---------------------------------------------------------------------------
系列的第五部分講如果創建你的第一個完整的3D對象。這個case中是一個4面的金字塔。
爲了讓我們接下來的開發更容易,這裏需要做一些準備。
我們必須將計算buffer以及創建數組時的大變得更加動態。 

private int _nrOfVertices = 0;

private void initTriangle() {

    float[] coords = {

            // coodinates

    };

    _nrOfVertices = coords.length;

    float[] colors = {

            // colors

    };

    short[] indices = new short[] {

            // indices

    };

    // float has 4 bytes, coordinate * 4 bytes

    ByteBuffer vbb = ByteBuffer.allocateDirect(coords.length * 4);

    vbb.order(ByteOrder.nativeOrder());

    _vertexBuffer = vbb.asFloatBuffer();

    // short has 2 bytes, indices * 2 bytes

    ByteBuffer ibb = ByteBuffer.allocateDirect(indices.length * 2);

    ibb.order(ByteOrder.nativeOrder());

    _indexBuffer = ibb.asShortBuffer();

    // float has 4 bytes, colors (RGBA) * 4 bytes

    ByteBuffer cbb = ByteBuffer.allocateDirect(colors.length * 4);

    cbb.order(ByteOrder.nativeOrder());

    _colorBuffer = cbb.asFloatBuffer();

    _vertexBuffer.put(coords);

    _indexBuffer.put(indices);

    _colorBuffer.put(colors);

    _vertexBuffer.position(0);

    _indexBuffer.position(0);

    _colorBuffer.position(0);

}

複製代碼

爲了更加的動態,我們必須改變一些變量以便我們接下來的工作。讓我們來細看一下:
在第一行你可以看到,我們初始化_nrOfVertices爲0,因爲我們可以在第七行那裏通過座標的大小來確定它。
我們同時也將_indecesArray改爲局部變量indices,並在13行進行了初始化。
這個buffer創建過程被放在了座標、顏色以及頂點數組的下邊,因爲buffer大小取決於數組。所以請看17-18行,22-23行,27-28行。在註釋裏面我解釋了計算方法。
主要的好處是,我們可以創建更多的vertices,而不用手動重新計算有多少個vertices,以及數組和buffer的大小。
下一步:你需要明白OpenGL如何繪製並決定我們看到的東西。
相對於OpenGL來說OpenGL ES一個很大的缺點就是除了三角形以外沒有別的圖元類型。我們沒有其它多邊形,所以我們想要創建的所有的對象都必須由三角形構成。我引用一個blog的帖子來說明這個問題: IPhone developer ,同時也推薦他的這些文章 OpenGL ES series.
這裏有更多的關於三角形的東西你需要知道。在OpenGL中,有一個概念叫做彎曲(winding),意思是vertices繪製時的順序。與現實世界中的對象不同,OpenGL中的多邊形一般沒有兩個面。他們只有一個面,一般是正面,一個三角形只有當其正面面對觀察者的時候纔可以被看到。可以配置OpenGL將一個多邊形作爲兩面的,但是默認情況下三角形只有一個可見的面。知道了那邊是多邊形的正面以後,OpenGL就可以少做一半的計算量。如果設置兩面都可視,則需要多的計算。
雖然有時候一個多邊形會獨立地顯示,但是你或許不是非常需要它的背面顯示,經常一個三角形是一個更大的對象的一部分,多邊形的一面將在這個物體的內部,所以永遠也不會被看到。這個沒有被顯示的一面被稱作背面,OpenGl通過繪製的順序來確定那個面是正面哪個是背面。頂點按照逆時針繪製的是正面(默認是這樣,但是可以被改變)。因爲OpenGL能很容易地確定哪些三角形對用戶是可視的,它就可以通過使用Backface Culling來避免爲那些不顯示在前面的多邊形做無用功。我們將在下一篇文章裏討論視角的問題,但是你現在可以想象它爲一個虛擬攝像機,或者通過一個虛擬的窗口來觀察OpenGL的世界。
wps_clip_image-31415
在上面的示意圖中,左邊青綠色的的三角形是背面,將不會被繪製,因爲它相對於觀察者來說是順時針的。而在右邊的這個三角形是正面,將會被繪製,因爲繪製頂點的順序相對於觀察者來說是逆時針的。
因爲我們想做的是創建一個漂亮的金字塔,我們首先disable這個glClearColor()。我們可以刪除掉變量_red, _green, _blue,還有方法setColor(). 我們也想改變視角,所以我們將旋轉分的x和y軸上。 

public class VortexRenderer implements GLSurfaceView.Renderer {

private static final String LOG_TAG = VortexRenderer.class.getSimpleName();

// a raw buffer to hold indices allowing a reuse of points.

private ShortBuffer _indexBuffer;

// a raw buffer to hold the vertices

private FloatBuffer _vertexBuffer;

// a raw buffer to hold the colors

private FloatBuffer _colorBuffer;

private int _nrOfVertices = 0;

private float _xAngle;

private float _yAngle;

@Override

public void onSurfaceCreated(GL10 gl, EGLConfig config) {

// code snipped

}

@Override

public void onSurfaceChanged(GL10 gl, int w, int h) {

gl.glViewport(0, 0, w, h);

}

public void setXAngle(float angle) {

_xAngle = angle;

}

public float getXAngle() {

return _xAngle;

}

public void setYAngle(float angle) {

_yAngle = angle;

}

public float getYAngle() {

return _yAngle;

}

// code snipped

複製代碼

爲了確保你那裏有了和這邊相同的對象變量,我也將這些都貼到這個類的上邊了。你可以看到我們現在有兩個float變量,_xAngle和_yAngle(15-16行)還有他們的setter和getter方法(28-42行)
現在讓我們實現根據觸摸屏來計算角度的邏輯,爲了做這個,需要先稍稍改變一下VortexView類。 

// code snipped

private float _x = 0;

private float _y = 0;

// code snipped

public boolean onTouchEvent(final MotionEvent event) {

    if (event.getAction() == MotionEvent.ACTION_DOWN) {

        _x = event.getX();

        _y = event.getY();

    }

    if (event.getAction() == MotionEvent.ACTION_MOVE) {

        final float xdiff = (_x - event.getX());

        final float ydiff = (_y - event.getY());

        queueEvent(new Runnable() {

            public void run() {

                _renderer.setXAngle(_renderer.getXAngle() + ydiff);

                _renderer.setYAngle(_renderer.getYAngle() + xdiff);

            }

        });

        _x = event.getX();

        _y = event.getY();

    }

    return true;

}

複製代碼

在第3和4行我們有兩個變量給我們的x和y值使用。當移動時,我們在ACTION_DOWN事件中設置他們的值,我們根據MotionEvent來計算當前值和舊的值的差。計算他們的差並加到已經應用的角度上。不要被ydiff 添加到x-angle上或者xdiff添加到y-angle上而迷惑。你可以想象,如果你想x軸的值不變,而進行旋轉,只有在y軸上旋轉。對於y軸也一樣。
如果我們向左或者向上移動手指,xdiff/ydiff的值就會變成負的,旋轉就會向後旋轉。所以在這兩個軸上旋轉都比較容易。
現在到了非常有意思的部分:金字塔。
像上面我們所引用的一樣,winding需要一些設置。有的也許是默認的設置,但是我們還是定義一下,爲了安全起見。 

@Override

public void onSurfaceCreated(GL10 gl, EGLConfig config) {

    // preparation

    // enable the differentiation of which side may be visible 

    gl.glEnable(GL10.GL_CULL_FACE);

    // which is the front? the one which is drawn counter clockwise

    gl.glFrontFace(GL10.GL_CCW);

    // which one should NOT be drawn

    gl.glCullFace(GL10.GL_BACK);

    gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);

    gl.glEnableClientState(GL10.GL_COLOR_ARRAY);

    initTriangle();

}

複製代碼

在第5行,我們enable了culling面,以保證只有一面。在第7行我們定義了那種順序是前面。GL_CCW表示逆時針。在第9行,我們最終定義了那個面作爲culling 面。 沃恩設置其爲GL_BACK以保證只顯示正面。這或許有點迷糊偶,你可以看看如果用GL_FRONT_AND_BACK會發生什麼……你將什麼也看不到。

@Override

public void onDrawFrame(GL10 gl) {

// define the color we want to be displayed as the "clipping wall"

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

// reset the matrix - good to fix the rotation to a static angle

gl.glLoadIdentity();

// clear the color buffer to show the ClearColor we called above...

gl.glClear(GL10.GL_COLOR_BUFFER_BIT);

// set rotation

gl.glRotatef(_xAngle, 1f, 0f, 0f);

gl.glRotatef(_yAngle, 0f, 1f, 0f);

//gl.glColor4f(0.5f, 0f, 0f, 0.5f);

gl.glVertexPointer(3, GL10.GL_FLOAT, 0, _vertexBuffer);

gl.glColorPointer(4, GL10.GL_FLOAT, 0, _colorBuffer);

gl.glDrawElements(GL10.GL_TRIANGLES, _nrOfVertices, GL10.GL_UNSIGNED_SHORT, _indexBuffer);

}

複製代碼

在第四行你可以看到背景色是黑色,因爲我們已經刪除了以前設置的動態顏色。在13和14行你可以看到在每個軸的旋轉角度。其它的都和以前講過的一樣。
最後一件事情你需要做的是改變initTriangle()函數中的顏色數組,座標和索引。我們的金子談應該顯示成這樣子的。
wps_clip_image-17464

private void initTriangle() {

    float[] coords = {

            -0.5f, -0.5f, 0.5f, // 0

            0.5f, -0.5f, 0.5f, // 1

            0f, -0.5f, -0.5f, // 2

            0f, 0.5f, 0f, // 3

    };

    _nrOfVertices = coords.length;

    float[] colors = {

            1f, 0f, 0f, 1f, // point 0 red

            0f, 1f, 0f, 1f, // point 1 green

            0f, 0f, 1f, 1f, // point 2 blue

            1f, 1f, 1f, 1f, // point 3 white

    };

    short[] indices = new short[] {

            0, 1, 3, // rwg

            0, 2, 1, // rbg

            0, 3, 2, // rbw

            1, 2, 3, // bwg

    };

    //code snipped

}

複製代碼

正如你在圖上看到的一樣,我們的金字塔有4個角。每個角都有自己的座標,所以我們有4個頂點需要定義。如2-7行。
每一個頂點有自己的顏色,在10-15行定義。
在中間部分給出了定義三角形所需要的索引。時刻記着winding,三角形0,1,3和0,3,1是不一樣的。
每一個索引點指向coords數組中定義的頂點。
編譯運行程序,看一下如果你改變了indices的順序會發生什麼,或者看一下如果你將GL_CCW改成GL_CW以後將會看到什麼。
Eclipse工程的源代碼下載: Vortex Part V
wps_clip_image-10151
wps_clip_image-24121
wps_clip_image-30166
------------------------------------------------------------------
這個系列的第六部分主要是關於如何創建正確的視角,因爲如果沒有正確的視角,3D就沒有任何意義。
在開始之前我們需要先討論一下OpenGL提供的這兩種view:正交和投影。
正交Orthographic (無消失點投影)
正交視圖無法看到一個物體是遠離自己還是正在我們面前。爲什麼?因爲它不會根據距離收縮。所以如果你如果你畫一個固定大小的物體在視點前面,同時畫一個同樣大小的物體在第一個物體的遠後方,你無法說那個物體是第一個。因爲兩個都是一樣的大小,根距離無關。他們不會隨着距離而收縮。
透視Perspective (有消失點投影)
透視視圖和我們從眼睛看到的視圖是一樣的。例如,一個高個子的人站在你面前,你看上去是很高的。如果這個人站在100米以外,他甚至還沒有你的拇指大。他看上去會隨着距離而縮小,但是我們實際上都知道,它依然是個高個子。這種效果叫做透視。上面例子中提到的兩個物體,第二個物體將會顯示地更小,所以我們可以區分哪個是離我們近的物體,那個是離我們遠的物體。
因爲我的例子或許讓你會迷惑。我再次推薦這個blog帖子:iPhone development: OpenGL ES From the Ground Up, Part 3: Viewports in Perspective,這裏面使用或者軌道作爲例子。
我們想創建的第一個view是喲娜orthographic。這個創建過程只需要一次,就是在每次surface被created的時候。所以我們需要改一下代碼。一些在onDrawFrame()中的方法將要被轉移到onSurfaceCreated()方法中。這樣他們只會當程序啓動或者旋轉的時候被執行。 

@Override

public void onSurfaceCreated(GL10 gl, EGLConfig config) {

    // preparation

    // define the color we want to be displayed as the "clipping wall"

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

    // enable the differentiation of which side may be visible 

    gl.glEnable(GL10.GL_CULL_FACE);

    // which is the front? the one which is drawn counter clockwise

    gl.glFrontFace(GL10.GL_CCW);

    // which one should NOT be drawn

    gl.glCullFace(GL10.GL_BACK);

    gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);

    gl.glEnableClientState(GL10.GL_COLOR_ARRAY);

    initTriangle();

}

// code snipped

@Override

public void onDrawFrame(GL10 gl) {

    gl.glLoadIdentity();

    // clear the color buffer and the depth buffer to show the ClearColor

    // we called above...

    gl.glClear(GL10.GL_COLOR_BUFFER_BIT);

    // set rotation

    gl.glRotatef(_xAngle, 1f, 0f, 0f);

    gl.glRotatef(_yAngle, 0f, 1f, 0f);

    //gl.glColor4f(0.5f, 0f, 0f, 0.5f);

    gl.glVertexPointer(3, GL10.GL_FLOAT, 0, _vertexBuffer);

    gl.glColorPointer(4, GL10.GL_FLOAT, 0, _colorBuffer);

    gl.glDrawElements(GL10.GL_TRIANGLES, _nrOfVertices, GL10.GL_UNSIGNED_SHORT, _indexBuffer);

}

複製代碼

你可以看到我們沒有將glClear()和glLoadIdentity()方法從onDrawFrame()移動到onSurfaceCreate中。原因很簡單:他們會在每一幀都被繪製。
因爲我們需要使用屏幕大小來計算屏幕的比例,所以我們引入了兩個對象變量:_width和_height。我們需要在onSurfaceChanged()方法中設置,在每次旋轉改變的時候調用。 

private float _width = 320f;

private float _height = 480f;

@Override

public void onSurfaceChanged(GL10 gl, int w, int h) {

_width = w;

_height = h;

gl.glViewport(0, 0, w, h);

}

複製代碼

現在我們已經有了啓動一個視點所需要的所有東西。我們需要改變一下onSurfaceChanged()方法。 

@Override

public void onSurfaceCreated(GL10 gl, EGLConfig config) {

gl.glMatrixMode(GL10.GL_PROJECTION);

float ratio = _width / _height;

// orthographic:

gl.glOrthof(-1, 1, -1 / ratio, 1 / ratio, 0.01f, 100.0f);

gl.glViewport(0, 0, (int) _width, (int) _height);

gl.glMatrixMode(GL10.GL_MODELVIEW);

gl.glEnable(GL10.GL_DEPTH_TEST);

// define the color we want to be displayed as the "clipping wall"

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

// enable the differentiation of which side may be visible 

gl.glEnable(GL10.GL_CULL_FACE);

// which is the front? the one which is drawn counter clockwise

gl.glFrontFace(GL10.GL_CCW);

// which one should NOT be drawn

gl.glCullFace(GL10.GL_BACK);

gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);

gl.glEnableClientState(GL10.GL_COLOR_ARRAY);

initTriangle();

}

複製代碼

Wow,現在有了好多的新代碼,別怕,我們一步一步來。
在第三行,我們可以看到glMatrixMode(),使用GL10.GL_RPOJECTION作爲參數,在第8行我們可以再次看到這個方法,但是是使用GL10.GL_MODELVIEW作爲變量。這樣使用的原因是3-8行中間的代碼。在這幾行中,4-7行設置了我們的視點,所以我們設置了我們的投影。在9-17行,我們設置了我們的模型環境。在這中上下文中,兩種調用使用不同的參數應該是可理解的。Tips:經常可以試着去刪除掉一些代碼行來看一下結果。這樣比較容易明白哪些代碼行是起什麼作用的。
第四行計算下一樣需要的屏幕比率。在這行中(6行)我們設置我們的視點來做orthographic view。這些參數是爲邊界設定,順序是這樣的:left, right, bottom, top, zNear, zFar。
在第7行我們設置了視點。我們知道這個方法的用法因爲我們已經在onSurfaceChanged()中使用過了。
在第8行我們切換了MatrixMode到GL10.GL_MODELVIEW,設置OpenGL接受關於改變model繪製方式的調用。
第9行我們調用了glEnable()並使用參數GL10.GL_DEPTH_TEST.這使OpenGL ES檢查對象的z-order。如果我們沒有enable它,我們將看到最後被繪製的對象一直顯示在最前面。這意味着,及時即使這個物體本來應該被更近更大的物體遮蓋,我們依然可以看到它。
其它代碼行我們已經在前面的幾篇中已經介紹過了。
透視視圖與之相同,只是不同的地方是不是使用glOrthof()而是使用glFrustumf(). glFrustumf()函數的參數和glOrthof()的參數略有不同。因爲我們沒有縮小物體,但是我們定義的錐體將被漏斗狀切開。看一下這個圖片來了解一下glOrthof()和glFrustumf()的區別。
正交Orthographic:
透視Perspective:
回到我們的代碼: 

@Override

public void onSurfaceCreated(GL10 gl, EGLConfig config) {

    Log.i(LOG_TAG, "onSurfaceCreated()");

    gl.glMatrixMode(GL10.GL_PROJECTION);

    float size = .01f * (float) Math.tan(Math.toRadians(45.0) / 2); 

    float ratio = _width / _height;

    // perspective:

    gl.glFrustumf(-size, size, -size / ratio, size / ratio, 0.01f, 100.0f);

    // orthographic:

    //gl.glOrthof(-1, 1, -1 / ratio, 1 / ratio, 0.01f, 100.0f);

    gl.glViewport(0, 0, (int) _width, (int) _height);

    gl.glMatrixMode(GL10.GL_MODELVIEW);

    gl.glEnable(GL10.GL_DEPTH_TEST);

    // define the color we want to be displayed as the "clipping wall"

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

    // enable the differentiation of which side may be visible 

    gl.glEnable(GL10.GL_CULL_FACE);

    // which is the front? the one which is drawn counter clockwise

    gl.glFrontFace(GL10.GL_CCW);

    // which one should NOT be drawn

    gl.glCullFace(GL10.GL_BACK);

    gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);

    gl.glEnableClientState(GL10.GL_COLOR_ARRAY);

    initTriangle();

}

複製代碼

Information: 我們要記住計算變量大小(5行),我們將會看到當我們討論矩陣的時候它爲什麼可以用。
在第8行我們使用glFrustumf()替代glOrthof().這是我們在orthographic view 和perspective view之前切換時需要做的所有的變化。
但是,hey,還有最後一項我們就能看到結果了。OK,讓我們改一下onDrawFrame()方法吧。 

@Override

public void onDrawFrame(GL10 gl) {

// clear the color buffer and the depth buffer

gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);

gl.glVertexPointer(3, GL10.GL_FLOAT, 0, _vertexBuffer);

gl.glColorPointer(4, GL10.GL_FLOAT, 0, _colorBuffer);

for (int i = 1; i <= 10; i++) {

gl.glLoadIdentity();

gl.glTranslatef(0.0f, -1f, -1.0f + -1.5f * i);

// set rotation

gl.glRotatef(_xAngle, 1f, 0f, 0f);

gl.glRotatef(_yAngle, 0f, 1f, 0f);

gl.glDrawElements(GL10.GL_TRIANGLES, _nrOfVertices, GL10.GL_UNSIGNED_SHORT, _indexBuffer);

}

}

複製代碼

OK,我們改變了什麼?
第三行,我們修改了參數確保depth buffer也會被清除。
在第9行,我們開始一個循環來創建10個物體。
在第10行,我們看到glLoadIdentity(). 現在它在這兒重設矩陣。 這是必須做的因爲我們要使用glRotatef()和glTranslatef()來修改我們的物體。但是爲了確保我們只修改了當前循環到的物體,我們調用glLoadIndentify()。所以我們重設每一個對之前物提的glTranslatef()和glRotatef()調用。
在11行我們看到新的glTranslatef()方法,將我們的物體移動到另外一個位置。在這個例子中,我們不修改x軸上的位置。但是我們修改y軸上的-1.0f,這表示它接近我們的屏幕低端。最後的計算你可以看到,只是修改z軸的位置,表示物體的神對。第一個物體在-2.5f,第二個在-4.0f等等。所以我們將10個物體擺放在了屏幕的中央。
如果你使用glFrustumf()調用,你看到的是這種結果:
wps_clip_image-9012
如果你切換glFrustumf()和glOrthof() ,你將會看到這種結果:
wps_clip_image-3052
Hey等一下,爲什麼只看到一個物體?因爲在正交情況下我們沒有任何的投影。所以每一個獨體的物體都有相同的大小所以沒有縮放,所以沒有任何消失的點,所以實際上所有的物體都在第一個物體的後面。
源代碼: Vortex Part VI

結束


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章