android 遊戲導引(4. 簡單紋理貼圖)
這一節主要講述 opengl 的貼圖技術,涉及了簡單的紋理知識。臨近放年假,忙啊。
源碼下載: 點我吧
Table of Contents
- 1 紋理 Texture
- 1.1 紋理座標 和 紋理映射
- 1.2 opengl 中啟用紋理映射功能
- 1.3 建立紋理
- 1.4 指定紋理
- 1.5 刪除紋理
- 1.6 綁定紋理
- 1.7 設定過濾器
- 1.8 紋理映射
- 2 常見的幾個問題
- 3 代碼實現
- 4 貼圖一個機器人
1 紋理 Texture
紋理定義了物體表面的結構,如花紋,圖案,皺紋等等。有了紋理,模型世界才會更加豐富多彩。如一個球形模型,我們給其映射足球的紋理,這就是一個足球,給其映射地球紋理,就是一個地球。另外,如果給一個四邊形映射一個牆的紋理,這邊是牆,否則,我們需要一塊磚一塊磚的構建在本節中,我們所指的是狹義的紋理: 映像紋理(對應的有函數紋理—用數學函數來定義的紋理)。
紋理實際上是一個二維數組,其元素是一些顏色值,每一元素稱之為紋理像素 (texel)。 紋理對象是一個內部資料類型,儲存著紋理資料。你不能直接存取紋理對象,但是可以通過一個整數的 ID 來作為其控制代碼跟蹤之。通過此控制代碼,你可以作為當前使用的紋理(稱之為紋理綁定),也可以從記憶體中刪除這個紋理對象,還可以為一的紋理賦值(將一些紋理資料載入到關聯的紋理中,稱之為指定紋理)。
通常一個紋理映射的步驟是:
- 建立紋理對象。就是獲得一個新的紋理控制代碼 ID.
- 指定紋理。就是將資料賦值給 ID 的紋理對象,在這一步,映像資料正式載入到了 ID 的紋理對象中。
- 設定過濾器。定義了opengl現實映像的效果,如紋理放大時的馬賽克消除。
- 綁定紋理對象。就是將 ID 的紋理作為下面操作的紋理。
- 紋理映射。將已綁定紋理的資料繪製到螢幕上去,在這一步,就能看到貼圖的效果了。
1.1 紋理座標 和 紋理映射
一個紋理對象有其自己的一套座標系,左下角是 (0,0) 右上方是 (1,1):
我們要將一個映像的一部分繪製到螢幕上,稱之為紋理映射, 就是將映像根據上述座標系計算出要繪製的部分的各個點的紋理座標,然後一一對應到螢幕上的座標中去(中的座標系是左上方為原點的):
1.2 opengl 中啟用紋理映射功能
在預設設定中,紋理映射是關閉的,啟用的參數是 GLTEXTURE2D, 還有其他的參數: GL_TEXTURE_1D, GL_TEXTURE_3D, GL_TEXTURE_CUBE_MAP。我們只用到2D紋理,其他不再贅述。
gl.glEnable(GL_TEXTURE_2D)
1.3 建立紋理
建立紋理,用函數 glGenTextures() 完成,函數返回新建立的紋理的 ID。此函數可以建立 n 個紋理,並將紋理ID 放在 textures 中:
void glGenTextures (int n, IntBuffer textures) |
範例:
IntBuffer intBuffer = IntBuffer.allocate(1);gl.glGenTextures(1, intBuffer);int textureId = intBuffer.get(); // 紋理 ID
1.4 指定紋理
OpenGL 提供了三個函數來指定紋理: glTexImage1D(), glTexImage2D(), glTexImage3D(). 這三個版本用於相應維數的紋理,我們用到的是 2D 版本: glTexImage2D().
void glTexImage2D (int target, int level, int internalformat, int width, int height, int border, int format, int type, Buffer pixels) |
參數過多,可以使用 GLUtils 中的 texImage2D() 函數,好處是直接將 Bitmap 資料作為參數:
void texImage2D (int target, int level, Bitmap bitmap, int border) |
參數:
-
target
-
操作的目標類型,設為 GL_TEXTURE_2D 即可
-
level
-
紋理的層級,本節不涉及,設為 0 即可
-
bitmap
-
映像
-
border
-
邊框,一般設為0
GLUtils.texImage2D (GL10.GL_TEXTURE_2D, 0, mBitmap, 0);
1.5 刪除紋理 刪除紋理, 第三個參數指明了第二個參數 textures 數組中紋理ID 的步長,一般是緊湊順序存放,設為0即可。
void glDeleteTextures (int n, int[] textures, int offset) |
1.6 綁定紋理
綁定後,此紋理處於活動狀態。在第一次綁定一個紋理對象時, 會將一系列初始值來適應你的應用。綁定比較簡單,用函數 glBindTexture():
void glBindTexture (int target, int texture) |
第一個參數是紋理類型,我們使用 2D 紋理,參數設為 GL_TEXTURE_2D, 第二個參數是紋理對象的 ID。
1.7 設定過濾器
有兩個版本:float版和int版本。
void glTexParameterf (int target, int pname, float param) |
|
void glTexParameterx (int target, int pname, int param) |
一般我們設定兩個, 一個放大器的: GL_TEXTURE_MAG_FILTER, 一個縮小器的: GL_TEXTURE_MIN_FILTER.
下面的兩行告訴OpenGL在顯示映像時,當它比放大得原始的紋理大 ( GL_TEXTURE_MAG_FILTER )或縮小得比原始得紋理小( GL_TEXTURE_MIN_FILTER )時OpenGL採用的濾波方式。通常這兩種情況下我都採用 GL_LINEAR 。這使得紋理從很遠處到離螢幕很近時都平滑顯示。使用 GL_LINEAR 需要CPU和顯卡做更多的運算。如果您的機器很慢,您也許應該採用 GL_NEAREST 。過濾的紋理在放大的時候,看起來斑駁的很(馬賽克)。您也可以結合這兩種濾波方式。在近處時使用 GL_LINEAR ,遠處時 GL_NEAREST 。
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR); // 線形濾波glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR); // 線形濾波
1.8 紋理映射
用函數 glTexCoordPointer 指定紋理座標數組,
void glTexCoordPointer (int size, int type, int stride, Buffer pointer) |
預設這個功能是關閉的,所以需要開啟:
gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY);// ... // 關閉gl.glDisableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
2 常見的幾個問題 2.1 貼圖呈現白色
可能的原因:
- 未啟用 GL_TEXTURE_2D 選項。請使用 glEnable() 和 glDisable() 函數進行開啟和關閉。
- 紋理對象無資料。 使用 GLUtils.texImage2D() 來指定,指定前需 glBindTexture() 啟用當前紋理。
2.2 映像扭曲
可能的原因:
- 紋理座標和頂點座標組應關係是否正確,調整之
- 映像的大小不是 2 的次冪, 解決: 內部重建一張 2 的次冪的image,調整uv座標
3 代碼實現
先定義一個紋理對象,其基本介面有:
@note: 為了處理 2 的次冪,內部對原始映像不是2的次冪的重建立立了一個映像。詳見代碼吧。
public class Texture2D { private int mWidth; private int mHeight; private int mPow2Width; private int mPow2Height; private float maxU = 1.0f; private float maxV = 1.0f; private Bitmap mBitmap = null; private int textureId = 0; // 刪除紋理資料 public void delete(GL10 gl) { if (textureId != 0){ gl.glDeleteTextures(1, new int[]{textureId}, 0); textureId = 0; } // bitmap if (mBitmap != null) { if (mBitmap.isRecycled()) mBitmap.recycle(); mBitmap = null; } } public static int pow2(int size) { int small = (int)(Math.log((double)size)/Math.log(2.0f)) ; if ( (1 << small) >= size) return 1 << small; else return 1 << (small + 1); } // 構建,延遲到第一次綁定時 public Texture2D(Bitmap bmp) { // mBitmap = bmp; mWidth = bmp.getWidth(); mHeight = bmp.getHeight(); mPow2Height = pow2(mHeight); mPow2Width =pow2(mWidth); maxU = mWidth/(float)mPow2Width; maxV = mHeight/(float)mPow2Height; Bitmap bitmap = Bitmap.createBitmap(mPow2Width, mPow2Height, bmp.hasAlpha() ? Bitmap.Config.ARGB_8888 : Bitmap.Config.RGB_565); Canvas canvas = new Canvas(bitmap); canvas.drawBitmap(bmp, 0, 0, null); mBitmap = bitmap; } // 第一次會載入紋理資料 public void bind(GL10 gl) { if (textureId ==0) { int[] textures = new int[1]; gl.glGenTextures(1, textures, 0); textureId = textures[0]; gl.glBindTexture(GL10.GL_TEXTURE_2D, textureId); gl.glTexParameterx(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_LINEAR); gl.glTexParameterx(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_LINEAR); GLUtils.texImage2D(GL10.GL_TEXTURE_2D, 0, mBitmap, 0); mBitmap.recycle(); mBitmap = null; } gl.glBindTexture(GL10.GL_TEXTURE_2D, textureId); } // 繪製到螢幕上 public void draw(GL10 gl, float x, float y) { gl.glEnable(GL10.GL_TEXTURE_2D); gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY); gl.glEnableClientState(GL10.GL_VERTEX_ARRAY); // 綁定 this.bind(gl); // 映射 FloatBuffer verticleBuffer = FloatBuffer.wrap(new float[]{ x,y, x+mWidth, 0, x, y+mHeight, x+mWidth, y+mHeight, }); FloatBuffer coordBuffer = FloatBuffer.wrap(new float[]{ 0,0, maxU,0, 0,maxV, maxU,maxV, }); gl.glTexCoordPointer(2, GL10.GL_FLOAT, 0, coordBuffer); gl.glVertexPointer(2, GL10.GL_FLOAT, 0, verticleBuffer); gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP,0,4); gl.glDisableClientState(GL10.GL_VERTEX_ARRAY); gl.glDisableClientState(GL10.GL_TEXTURE_COORD_ARRAY); gl.glDisable(GL10.GL_TEXTURE_2D); } public void draw(GL10 gl, float x, float y, float width, float height) { gl.glEnable(GL10.GL_TEXTURE_2D); gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY); gl.glEnableClientState(GL10.GL_VERTEX_ARRAY); // 綁定 bind(gl); // 映射 // 映射 FloatBuffer verticleBuffer = FloatBuffer.wrap(new float[]{ x,y, x+width, 0, x, y+height, x+width, y+height, }); FloatBuffer coordBuffer = FloatBuffer.wrap(new float[]{ 0,0, maxU,0, 0,maxV, maxU,maxV, }); gl.glTexCoordPointer(2, GL10.GL_FLOAT, 0, coordBuffer); gl.glVertexPointer(2, GL10.GL_FLOAT, 0, verticleBuffer); gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP,0,4); gl.glDisableClientState(GL10.GL_VERTEX_ARRAY); gl.glDisableClientState(GL10.GL_TEXTURE_COORD_ARRAY); gl.glDisable(GL10.GL_TEXTURE_2D); gl.glDisableClientState(GL10.GL_VERTEX_ARRAY); gl.glDisableClientState(GL10.GL_TEXTURE_COORD_ARRAY); gl.glDisable(GL10.GL_TEXTURE_2D); } }
4 貼圖一個機器人
代碼很簡單了,在情境 scene 的 draw() 中繪製一個 texture2D, 具體下載代碼看看吧:
public class AndroidScene extends GlObject{ Texture2D texture; public AndroidScene() { super(); // 使用 assets 檔案夾下的 androida.jpg Bitmap androidBitmap = GameSystem.getInstance().getBitmapFromAssets("androida.jpg"); texture = new Texture2D(androidBitmap); } public void draw(GL10 gl) { texture.draw(gl, 0, 0); }}
這一節有點枯燥,學習愉快。