轉載請註明出處:【huachao1001的專欄:http://blog.csdn.net/huachao1001】
上一篇文章【Android OpenGL添加光照和材料屬性 】我們已經學瞭如何爲3D模型添加光照和材料屬性,使得模型看起來更有立體感。今天我們學習如何爲3D模型貼上紋理,使得模型看起來更真實!目前我在網上沒有找到帶有紋理圖片的STL模型文件,如果隨便貼一張圖片上去的話並不好看,看起來不會很真實。好在手頭上現在有2個帶有紋理的STL格式文件,雖然這兩個模型看起來有點殘缺,但是不影響我們學習如何貼紋理。先看看效果~.
圖片1如下:
對應的模型:
圖片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
類中添加紋理相關數據屬性,並且添加對應的setter
、getter
函數。代碼我就不貼出來了,後面我會上傳源碼。
現在我們編寫好了解析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
對象。在將ID
和Bitmap
綁定之前,需要調用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.pxy
、1.stl
、1.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 開始繪製
前面所做的鋪墊已經完成,接下來就是最後的繪製了!相比上一篇的代碼,我們只需修改onSurfaceCreated
和onDrawFrame
函數。其實大部分代碼都是相同的,只是我們通過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