Android OpenGL添加紋理

轉載請註明出處:【huachao1001的專欄:http://blog.csdn.net/huachao1001】

上一篇文章【Android OpenGL添加光照和材料屬性 】我們已經學瞭如何爲3D模型添加光照和材料屬性,使得模型看起來更有立體感。今天我們學習如何爲3D模型貼上紋理,使得模型看起來更真實!目前我在網上沒有找到帶有紋理圖片的STL模型文件,如果隨便貼一張圖片上去的話並不好看,看起來不會很真實。好在手頭上現在有2個帶有紋理的STL格式文件,雖然這兩個模型看起來有點殘缺,但是不影響我們學習如何貼紋理。先看看效果~.

圖片1如下:

原圖1

對應的模型:

綁定紋理

圖片2如下:
原圖2

對應的模型:

綁定紋理

注意,本文的講解是建立在《Android OpenGL顯示任意3D模型文件 》之上,請務必先看這篇文章再往下讀(當然了,如果你已經有一定的Android OpenGL基礎,可以不用看)。好啦,看完效果後,我們開始學習吧!

1 相關基礎

1.1 貼紋理原理簡單概述

其實貼紋理的原理非常簡單,就是給每個三角形貼上圖片即可。那麼如何給三角形貼圖片呢?我們知道,既然是給三角形貼圖片,那肯定就需要一張圖,然後在這張圖片上指定三角形的三個頂點對應這張圖的位置。這樣就可以準確的爲這個三角形貼好圖片了。

值得注意的是,三角形的三個頂點在圖片上的位置取值範圍爲[0,1]。即以相對圖片的寬高比例來計算的。

在加載紋理圖片時,OpenGL爲每張圖片分配好ID,將圖片緩存起來。在貼圖時,通過ID來查找圖片。

1.2 相關API

跟繪製三角形類似,如果需要開啓貼紋理功能需要如下代碼:

gl.glEnable(GL10.GL_TEXTURE_2D);

對應的關閉爲:

gl.glDisable(GL10.GL_TEXTURE_2D);

前面1.1節中,我們提到:在加載紋理圖片時,OpenGL爲每張圖片分配好ID,將圖片緩存起來。在貼圖時,通過ID來查找圖片。因此,在我們開始貼圖之前,需要爲當前模型綁定好紋理圖片的ID:

//根據ID綁定對應的紋理
gl.glBindTexture(GL10.GL_TEXTURE_2D, model.getTextureIds()[0]);

上面代碼中,我們看到,在Model實體類中,通過getTextureIds函數來獲取ID數組,並取出數組的第一個數據。而Model類是我們自己自定義的實體類,顯然不可能在我們的自定義的實體類中“無中生有”出一個ID數組。那麼這個ID數組從哪裏來?

注意,紋理的ID是保存在一個int[]數組中,數組的第一個元素即爲ID。至於爲什麼用數組來保存,我暫時還沒弄清。可能是擴展性更好吧~

關於紋理對應的ID,後面詳細說。我們繼續往下走,在拿到紋理ID的情況下,如何繪製。首先你需要啓用紋理座標數組:

gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY);

然後在繪製三角形之前,將紋理座標數據設定好:

gl.glTexCoordPointer(2, GL10.GL_FLOAT, 0, model.getTextureBuffer());

此時即可繪製紋理。當然了,我們現在只是大致講講,更完整更詳細的內容第二節談~。

1.3 pxy文件格式

由於我採用的模型的紋理座標數據是pxy格式。pxy格式裏面保存的是浮點數的集合,即每4個字節爲一個數據。每2個浮點數表示一個座標點,每三個座標點對應一個三角形在圖片中的紋理區域。

1.4 多個模型數據

爲什麼要提多個模型數據呢?我們知道,三維模型中,自然是包含各個角度的映像圖片。一張圖片往往很難包含整個模型的紋理信息。因此,我們將一個3D模型分割成多個3D模型,每個模型對應一張紋理圖片。理論上說,3張圖片即可包含整個模型紋理信息了,即,將一個模型分割成3個3D模型。當然了,分割的越多,紋理圖片的構造就越簡單(你也可以以一張360°全景紋理圖片,但是構造這樣的圖片成本比較高)。

可能你會說,我們該如何分割模型?每個模型的座標位置信息我們無法給它分割,因爲座標位置數據是一個數組,是按照三角形頂點的順序指定的。

請注意一點,我們只負責顯示模型,我們不管如何分割,分割這塊丟個模型的設計者!因爲設計者可以通過相關的3D設計軟件輕鬆的分割。我們只需關注,如何同時顯示多個3D模型,併爲每個3D模型貼好對應的紋理即可。

2 代碼編寫

2.1 解析pxy文件

前面我們大致介紹了pxy文件格式,我們知道,pxy保存的就是當前stl文件中三角形頂點在紋理圖片上對應的座標。每個頂點佔2個浮點數(對應x、y)。那麼我們的解析就非常簡單了,在STLReader類中,添加如下函數:

private void parseTexture(Model model, byte[] textureBytes) {
    int facetCount = model.getFacetCount();
    // 三角面個數有三個頂點,一個頂點對應紋理二維座標
    float[] textures = new float[facetCount * 3 * 2];
    int textureOffset = 0;
    for (int i = 0; i < facetCount * 3; i++) {
        //第i個頂點對應的紋理座標
        //tx和ty的取值範圍爲[0,1],表示的座標位置是在紋理圖片上的對應比例
        float tx = Util.byte4ToFloat(textureBytes, textureOffset);
        float ty = Util.byte4ToFloat(textureBytes, textureOffset + 4);

        textures[i * 2] = tx;
        //我們的pxy文件原點是在左下角,因此需要用1減去y座標值
        textures[i * 2 + 1] = 1 - ty;

        textureOffset += 8;
    }
    model.setTextures(textures);
}

同時,我們需要在Model類中添加紋理相關數據屬性,並且添加對應的settergetter函數。代碼我就不貼出來了,後面我會上傳源碼。

現在我們編寫好了解析pxy紋理座標數據,接下來就是把解析stl文件和pxy文件整合在一起的函數,在STLReader中添加:

public Model parseStlWithTexture(InputStream stlInput, InputStream textureInput) throws IOException {
    Model model = parseBinStl(stlInput);
    int facetCount = model.getFacetCount();
    // 三角面片有3個頂點,一個頂點有2個座標軸數據,每個座標軸數據是float類型(4字節)
    byte[] textureBytes = new byte[facetCount * 3 * 2 * 4];
    textureInput.read(textureBytes);// 將所有紋理座標讀出來
    parseTexture(model, textureBytes);
    return model;
}

此時,我們的STLReader類就可以通過parseStlWithTexture函數完美的將stl和pxy數據封裝到Model對象中了。

2.2 加載紋理圖片

前面我們提到了,OpenGL爲每張紋理圖片生成一個ID。接下來我們看看如何將一個紋理圖片加載到OpenGL通道中,並且分配一個ID。首先,我們需要讀取紋理圖片,生成Bitmap對象。然後調用glGenTextures函數,生成ID,並將此ID保存到Model對象中。此時,我們已經拿到了ID,但是這個ID並沒有綁定Bitmap對象。在將IDBitmap綁定之前,需要調用glBindTexture函數,將生成的ID綁定到紋理通道,並且通過glTexParameterf設定當前綁定紋理的相關屬性。 最後通過GLUtils.texImage2D函數將Bitmap對象與當前紋理通道綁定,而當前紋理通道已經綁定好了ID,從而達到了ID與紋理的間接綁定。以後使用紋理時,就可以直接通過ID來訪問,無需直接訪問Bitmap對象了。說了這麼多,有點抽象,直接通過代碼來的實在:

private void loadTexture(GL10 gl, Model model, boolean isAssets) {
    Log.d("GLRenderer", "綁定紋理:" + model.getPictureName());
    Bitmap bitmap = null;
    try {
        // 打開圖片資源
        if (isAssets) {//如果是從assets中讀取
            bitmap = BitmapFactory.decodeStream(context.getAssets().open(model.getPictureName()));
        } else {//否則就是從SD卡里面讀取
            bitmap = BitmapFactory.decodeFile(model.getPictureName());
        }
        // 生成一個紋理對象,並將其ID保存到成員變量 texture 中
        int[] textures = new int[1];
        gl.glGenTextures(1, textures, 0);
        model.setTextureIds(textures);

        // 將生成的空紋理綁定到當前2D紋理通道
        gl.glBindTexture(GL10.GL_TEXTURE_2D, textures[0]);

        // 設置2D紋理通道當前綁定的紋理的屬性
        gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER,
                GL10.GL_NEAREST);
        gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MAG_FILTER,
                GL10.GL_LINEAR);

        // 將bitmap應用到2D紋理通道當前綁定的紋理中
        GLUtils.texImage2D(GL10.GL_TEXTURE_2D, 0, bitmap, 0);
        gl.glBindTexture(GL10.GL_TEXTURE_2D, 0);
    } catch (IOException e) {
        e.printStackTrace();
    } finally {

        if (bitmap != null)
            bitmap.recycle();

    }
}

2.3 讀取多個模型數據

我們前面說過,將一個模型分割成多個模型,因此,我們需要讀取多個模型數據,並保存起來。我們在此之前已經學會了讀取一個模型數據,那麼讀取多個模型數據通過for循環即可。爲了讀取上的方便,我們爲將每個模型數據按序命名。另外,我們知道,目前爲止,我們的一個模型對應三種格式文件:

  • pxy:三角形對應的紋理座標
  • stl:三角網數據
  • jpg:紋理圖片

爲了讀取上的方便,我們將同一個模型的這三個文件設爲相同的名稱,如:1.pxy1.stl1.jpg。各個模型之間按序命名,格式如下圖:

命名格式

此時,我們就可以很輕鬆的讀取啦~。在GLRenderer中:

private List<Model> models = new ArrayList<>();

public GLRenderer(Context context) {
    this.context = context;
    try {
        STLReader reader = new STLReader();
        for (int i = 1; i <= 6; i++) {
            Model model = reader.parserStlWithTextureInAssets(context, "chuwang/" + i);

            models.add(model);
        }

    } catch (IOException e) {
        e.printStackTrace();
    }
}

此時我們就完成了將所有的模型數據保存在List<Model>類型的models對象中。

2.4 開始繪製

前面所做的鋪墊已經完成,接下來就是最後的繪製了!相比上一篇的代碼,我們只需修改onSurfaceCreatedonDrawFrame函數。其實大部分代碼都是相同的,只是我們通過for循環的方式,將所有的模型繪製出來而已。

但是有個區別需要注意,就是我們需要獲取所有模型在xyz座標中的最大值最小值,以及所有模型加在一起後的中心點位置。前面我們只有一個模型,很快就算好了。多個模型我們也是很簡單,只需根據每個模型的在xyz座標中的最大值最小值計算即可,詳情請看我的附件源碼。

我們看看onSurfaceCreated函數,onSurfaceCreated函數需要負責計算所有模型的中心點、所有模型在xyz座標中的最大值最小值,以及加載所有模型對應的紋理圖片:

@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
    gl.glEnable(GL10.GL_DEPTH_TEST); // 啓用深度緩存
    gl.glClearColor(0f, 0f, 0f, 0f);// 設置深度緩存值
    gl.glDepthFunc(GL10.GL_LEQUAL); // 設置深度緩存比較函數
    gl.glShadeModel(GL10.GL_SMOOTH);// 設置陰影模式GL_SMOOTH


    //初始化相關數據
    initConfigData(gl);

}

private void initConfigData(GL10 gl) {
    float r = Util.getR(models);
    mScalef = 0.5f / r;
    mCenterPoint = Util.getCenter(models);

    //爲每個模型綁定紋理
    for (Model model : models) {
        loadTexture(gl, model, true);
    }

}

再看看onDrawFrame函數,onDrawFrame函數需要通過for循環的方式,繪製出每個模型:

@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==============================//
    for (Model model : models) {
        //開啓貼紋理功能
        gl.glEnable(GL10.GL_TEXTURE_2D);
        //根據ID綁定對應的紋理
        gl.glBindTexture(GL10.GL_TEXTURE_2D, model.getTextureIds()[0]);
        //啓用相關功能
        gl.glEnableClientState(GL10.GL_NORMAL_ARRAY);
        gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
        gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY);

        //開始繪製
        gl.glNormalPointer(GL10.GL_FLOAT, 0, model.getVnormBuffer());
        gl.glVertexPointer(3, GL10.GL_FLOAT, 0, model.getVertBuffer());
        gl.glTexCoordPointer(2, GL10.GL_FLOAT, 0, model.getTextureBuffer());

        gl.glDrawArrays(GL10.GL_TRIANGLES, 0, model.getFacetCount() * 3);

        //關閉當前模型貼紋理,即將紋理id設置爲0
        gl.glBindTexture(GL10.GL_TEXTURE_2D, 0);

        //關閉對應的功能
        gl.glDisableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
        gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);
        gl.glDisableClientState(GL10.GL_NORMAL_ARRAY);
        gl.glDisable(GL10.GL_TEXTURE_2D);
    }


    //=====================end============================//

}

從代碼上也可以看出,相比前幾篇文章,代碼的修改並不大。細心的童鞋會發現,我這裏並沒有開啓光照、材料屬性。主要是我們已經貼好紋理了,並且默認上模型會有光照效果。

最後看看效果吧,其實效果已經在最開始已經看過了,我們再看看

綁定紋理

最後看看源碼吧,由於AndroidStudio項目過大,我只上傳app/src/main裏面內容:http://download.csdn.net/download/huachao1001/9599565

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