Android OpenGL ES(八)----紋理程式設計框架
1.把紋理載入進OpenGL中
我們的第一個任務就是把一個影象檔案的資料載入到一個OpenGL的紋理中。
作為開始,讓我們重新捨棄第二篇的框架,重新建立一個程式,新建一個util工具包,在該包下建立一個新類TextureHelper,我們將以下面的方法簽名開始:
public static int loadTexture(Context context,int resourceId){}
這個方法會把Android上下文,和資源ID作為輸入引數,並返回載入影象的OpenGL紋理的ID。開始時,我們會使用建立其他OpenGL物件時一樣的模式生成一個新的紋理ID。
final int[] textureObjectIds=new int[1];
GLES20.glGenTextures(1,textureObjectIds,0);
if(textureObjectId[0]==0){
Log.w(TAG,"建立紋理失敗!");
}
通過傳遞1作為第一個引數呼叫glGenTextures(),我們就建立了一個紋理物件。OpenGL會把那個生成的ID儲存在textureObjectIds中。我們也檢查了glGenTextures()呼叫是否成功,如果結果不等於0就繼續,否則記錄那個錯誤並返回0。因為TAG還沒有定義,讓我們在類的頂部為它加入如下定義:
private static final String TAG="TextureHelper";
載入點陣圖資料並與紋理繫結
下一步是使用Android的API讀入影象檔案的資料。OpenGL不能直接讀取PNG或者JPEG檔案的資料,因為這些檔案被編碼為特定的壓縮格式。OpenGL需要非壓縮形式的原始資料,因此,我們需要用Android內建的點陣圖解碼器把影象檔案解壓縮為OpenGL能理解的形式。
讓我們繼續實現loadTexture(),把那個影象解壓縮為一個Android點陣圖:
final BitmapFactory.Options options=new BitmapFactory.Options();
options.inScaled=false;
final Bitmap bitmap=BitmapFactory.decodeResource(context.getResource(),resourceId,options);
if(bitmap==null){
Log.w(TAG,"載入點陣圖失敗");
GLES20.glDeleteTexture(1,textureObjectIds,0);
return 0;
}
首先建立一個新的BitmapFactory.Options的例項,命名為“options”,並且設定inScaled為"false"。這告訴Android我們想要原始的影象資料,而不是這個影象的壓縮版本。
接下來呼叫BitmapFactory.decodeResource()做實際的解碼工作,把我們剛剛定義的Android上下文,資源ID和解碼的options傳遞進去。這個呼叫會把解碼後的影象存入bitmap,如果失敗就會返回空值。我們檢查了那個失敗,如果點陣圖是空值,那個OpenGL紋理物件會被刪除。如果解碼成功,就繼續處理那個紋理。
在可以使用這個新生成的紋理物件做任何其他事之前,我們需要告訴OpenGL後面紋理的呼叫應該應用於這個紋理物件。我們為此使用一個glBindTexture()呼叫:
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D,textureObjectIds[0]);
第一個引數GL_TEXTURE_2D告訴OpenGL這應該被作為一個二位紋理對待,第二個引數告訴OpenGL要繫結到哪個紋理物件的ID。
既然上一篇博文已經瞭解了紋理過濾,我們直接編寫loadTexture()後面的程式碼:
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D,GLES20.GL_TEXTURE_MIN_FILTER,GLES20.GL_LINEAR_MIPMAP_LINEAR);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D,GLES20.GL_TEXTURE_MAG_FILTER,GLES20.GL_LINEAR);
我們用一個glTexParameteri()呼叫設定每個過濾器:GL_TEXTURE_MIN_FILTER是指縮小的情況,而GL_TEXTURE_MAG_FILTER是指放大的情況。對於縮小的情況,我們選擇GL_LINEAR_MIPMAP_LINEAR,它告訴OpenGL使用三線性過濾;我們設定放大過濾器為GL_LINEAR,它告訴OpenGL使用雙線性過濾。
載入紋理到OpenGL並返回其ID
我們現在可以用一個簡單的GLUtil_texImage2D()呼叫載入點陣圖資料到OpenGL裡了:
GLUtil_texImage2D(GLES20.GL_TEXTURE_2D,0,bitmap,0);
這個呼叫告訴OpenGL讀入bitmap定義的點陣圖資料,並把它複製到當前繫結的紋理物件。
既然這些資料已經被載入進OpenGL了,我們就不需要持有Android的點陣圖了。正常情況下,釋放這個點陣圖資料也會花費Dalvik的幾個垃圾回收週期,因此我們應該呼叫bitmap物件的recycle()方法立即釋放這些資料:
bitmap.recycle();
生成MIP貼圖也是一件容易的事情。我們用一個快速的glGenerateMipmap()呼叫告訴OpenGL生成所有必要的級別:
GLES20.glGenerateMipmap(GLES20.GL_TEXTURE_2D);
既然我們完成了紋理物件的載入,一個很好的實踐就是解除與這個紋理的繫結,這樣我們就不會用其他紋理方法呼叫意外地改變這個紋理:
GLES20.gl_BindTexture(GLES20.GL_TEXTURE_2D,0);
傳遞0給glBindTexture()就與當前的紋理接觸綁定了。最後一步是返回紋理物件ID:
return textureObjectIds[0];
我們現在有一個方法了,它可以從資原始檔夾讀入影象檔案,並把圖形資料載入進OpenGL。我們也取回一個紋理ID,它可被用做這個紋理的引用,如果載入失敗,我們會得到0。以上所有方法都是TextureHelper類下loadTexture()方法裡面的程式碼。
2.建立新的著色器集合
在把紋理繪製到螢幕之前,我們不得不建立一套新的著色器,它們可以接收紋理,並把它們應用在要繪製的片段上。這些新的著色器與我們目前為止使用過的著色器相似,只是為了支援紋理做了一些輕微的改動。
建立新的頂點著色器
在專案中res/raw/目錄下新建一個檔案,命名為“texture_vertex_shader.glsl”,並加入如下內容:
uniform mat4 u_Matrix;
attribute vec4 a_Position;
attribute vec2 a_TextureCoordinates;
varying vec2 v_TextureCoordinates;
void main(){
v_TextureCoordinates=a_TextureCoordinates
gl_Position=u_Matrix*a_Position;
}
這個著色器的大多數程式碼看上去應該都比較熟悉:我們已經為矩陣定義了一個uniform,並且也為位置定義了一個屬性。我們使用這些去設定最後的gl_Position。而對於這些新的東西,我們同樣給紋理座標家了一個新的屬性,它叫“a_TextureCoordinates”。因為它有兩個分量:S座標和T座標,所以被定義為vec2。我們把這些座標傳遞給頂點著色器被插值的varying,稱為v_TextureCoordinates。
建立新的片段著色器
在同樣的目錄,建立一個叫做“texture_fragment_shader.glsl”的新檔案,並加入如下程式碼:
precision mediump float;
uniform sampler2D u_TextureUnit;
varying vec2 v_TextureCoordinates;
void main(){
gl_FragColor=texture2D(u_TextureUnit,v_TextureCoordinates);
}
為了把紋理繪製到一個物體上,OpenGL會為每個片段都呼叫片段著色器,並且每個呼叫都接受v_TextureCoordinates的紋理座標。片段著色器也通過uniform------u_TextureUnit接受實際的紋理資料,u_TextureUnit被定義為一個sampler2D, 這個變數型別指的是一個二維紋理資料的陣列。
被插值的紋理座標和紋理資料被傳遞給著色器函式texture2D(),它會讀入紋理中那個特定的座標處的顏色值。接著通過把結果賦值給gl_FragColor設定片段的顏色。
3.為頂點資料建立新的類結構
首先,我們將把頂點資料分離到不同的類中,每個類代表一個物理物件的型別。我們將為桌子建立一個新類,併為木槌建立一個新類。因為紋理上已經有一條直線了,所以我們不需要給那個分割線建立新類。
為了減少重複,我們會建立獨立的類,用於封裝實際的頂點陣列。新的類結構看上去如下圖所示:
我們會建立Mallet類管理木槌的資料,以及Table管理桌子的資料;並且每個類都會有一個VertexArray類的例項,它用來封裝儲存頂點矩陣的FloatBuffer。
我們將從VertexArray類開始。在你的專案中建立一個新的包,命名為data,並在那個包中建立一個新類,命名為VertexArray,程式碼如下:
private final FloatBuffer floatBuffer;
public VertexArray(float[] vertexData){
this.floatBuffer=ByteBuffer.allocateDirect(VertexData.length*BYTES_PER_FLOAT)
.order(ByteOrder.nativeOrder())
.asFloatBuffer()
.put(vertexData);
}
public void setVertexAttribPointer(int dataOffset,int attributeLocation,int compontCount,int stride){
this.floatBuffer.position(dataOffset);
GLES20.glVertexAttribPointer(attributeLocation,compontCount,GLES20.GL_FLOAT,false,stride,this.floatBuffer);
GLES20.glEnableVertexAttribArray(attributeLocation);
this.floatBuffer.position(0);
}
這段程式碼包含一個FloatBuffer,如第二篇博文解釋的,它是用來在原生代碼中儲存頂點矩陣資料的。這個構建器取用一個Java的浮點陣列,並把它寫進這個緩衝區。
我們也建立一個通用的方法把著色器中的屬性與這些資料關聯起來。它遵循我們在第三篇博文中解釋過的同樣的模式。
因為我們最終要在幾個類中都使用BYTES_PER_FLOAT,我們需要給它找個新的地方。要做到這點,我們要在data包中建立一個名為Constans的新類,並加入如下程式碼:
public Class Constants{
public static final int BYTES_PER_FLOAT=4;
}
加入桌子資料
現在我們將定義一個儲存桌子資料的類,這個類會儲存桌子的位置資料;我們還會加入紋理座標,並把這個紋理應用於這個桌子。
新增類常量,建立一個包,名為object;在這個包中,建立名為Table的新類,並在類的內部加入如下程式碼:
private static final int POSITION_COMPONENT_COUNT=2;
private static final int TEXTURE_COORDINATES_COMPONENT_COUNT=2;
private static final int STRIDE=(POSITION_COMPONENT_COUNT+TEXTURE_COORDINATES_COMPONENT_COUNT)*Constans.BYTES_PER_FLOAT;
如你所見,我們定義了位置分量計數,紋理座標分量計數以及跨距。
新增頂點資料,如下程式碼定義頂點資料:
private static final float[] VERTEX_DATA={
//X,Y,S,T
0f,0f,0.5f,0.5f,
-0.5f,-0.8f,0f,0.9f,
0.5f,-0.8f,1f,0.9f,
0.5f,0.8f,1f,0.1f,
-0.5f,0.8f,0f,0.1f,
-0.5f,-0.8f,0f,0.9f
}
這個陣列包含了空氣曲棍球桌子的頂點資料。我們也定義了X和Y的位置,以及S和T紋理座標。你可能注意到了那個T分量正是按那個Y分量相反的方向定義的。之所以會這樣,如我們上篇博文解釋的,影象的朝向是右邊向上的。當我們使用一個對稱的紋理座標時,這一點實際上沒有關係,但是在其他情況下,這就有問題了,因此一定要記住這個原則。
剪裁紋理
我們還使用了0.1f和0.9f作為T座標。為什麼?這個桌子是1個單位寬,1.6個單位高,而紋理影象是512*1024畫素,因此,如果它的寬度對應1個單位,那紋理的高實際就是2個單位。為了避免把紋理壓扁,我們使用乏味0.1到0.9剪裁它的邊緣,而不是用0.0到1.0,並且只畫它的中間部分。
即使不使用剪裁,我們還可以堅持使用從0.0到1.0的紋理座標,把這個紋理預拉伸,這樣被壓扁到空氣曲棍球桌子之後,它看去就是正確的了。採用這種方法,那些無法顯示的紋理部分就不會佔用任何記憶體了。
初始化和繪製資料
現在為 Table類建立一個建構函式。這個建構函式會使用VertexArray把資料複製到本地記憶體中的一個FloatBuffer。
private final VertexArray vertexArray;
public Table(){
this.vertexArray=new VertexArray(VERTEX_DATA);
}
新增一個方法把頂點陣列繫結到一個著色器程式上:
public void bindData(TextureShaderProgram textureProgram){
this.vertexArray.setVertexAttribPointer(
0,
textureProgram.getPositionLocation(),
POSITION_COMPONENT_COUNT,
STRIDE);
this.vertexArray.setVertexAttribPointer(
POSITION_COMPONENT_COUNT,
textureProgram.getTextureLocation(),
TEXTURE_COORDINATES_COMPONENT_COUNT,
STRIDE);
}
這個方法為每個頂點呼叫了setVertexAttribPointer(),並從著色器程式獲取每個屬性的位置。它通過呼叫getPositionLocation()把位置繫結到被引用的著色器屬性上,並通過getTextureLocation()把紋理座標繫結到被引用的著色器屬性上。當我們建立著色器的類時,會定義這些方法。
我們只需加入最後一個方法就可以畫出這張桌子了:
public void draw(){
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_FAN,0,6);
}
加入木槌資料
在同一個包中建立另一個類,命名為“Mallet”。在這個類中加入如下程式碼:
private static final int POSITION_COMPONENT_COUNT=2;
private static final int COLOR_COMPONENT_COUNT=3;
private static final int STRIDE=(POSITION_COMPONENT_COUNT+COLOR_COMPONENT_COUNT)*Constans.BYTES_PER_FLOAT;
private static final float[] VERTEX_DATA={
0f,-0.4f,0f,0f,1f,
0f,0.4f,1f,0f,0f
}
private final VertexArray vertexArray;
public Mallet(){
this.vertexArray=new VertexArray(VERTEX_DATA);
}
public void bindData(ColorShaderProgram colorProgram){
this.vertexArray.setVertexAttribPointer(
0,
colorProgram.getPositionLocation(),
POSITION_COMPONENT_COUNT,
STRIDE);
this.vertexArray.setVertexAttribPointer(
POSITION_COMPONENT_COUNT,
colorProgram.getColorLocation(),
COLOR_COMPONENT_COUNT,
STRIDE);
}
public void draw(){
GLES20.glDrawArrays(GLES20.GL_POINTS,0,2);
}
它遵循與Table類一樣的模式,與之前一樣,我們還是把木槌畫為點。
頂點資料現在被定義好了:我們有一個類表示桌子資料,另一個類表示木槌資料,第三個類使得更容易管理頂點資料本身。下一步是為著色器程式定義類。
4.為著色器程式新增類
我們會為紋理著色器建立一個類,並顏色器程式建立另一個類:我們會用紋理著色器繪製桌子,用顏色著色器繪製木槌。我們也會建立一個基類作為它們的公共函式。我們不用再擔心那條直線,因為它是紋理的一部分。
我們開始給ShaderHelper加入一個輔助函式,開啟博文第三篇的類,在其尾部加入如下方法:
public static int buildProgram(String vertexShaderSource,String fragmentShaderSource){
int program;
int vertexShader=compileVertexShader(vertexShaderSource);
int fragmentShader=compileFragmentShader(fragmentShaderSource);
program=linkProgram(vertexShader,fragmentShader);
validateProgram(program);
return program;
}
這個輔助函式會編譯vertexShaderSource和fragmentShaderSource定義的著色器,並把它們連結在一起成為一個程式。我們會使用這個輔助函式組成我們的基類。
建立一個名為programs的包,並在包中建立一個名為ShaderProgram的新類,加入如下程式碼:
protected static final String U_MATRIX="u_Matrix";
protected static final StringU_TEXTURE_UNIT="u_TextureUnit";
protected static final StringA_POSITION="a_Position";
protected static final StringA_COLOR="a_Color";
protected static final StringA_TEXTURE_COORDINATES="a_TextureCoordinates";
protected final int program;
protected ShaderProgram(Context context,int vertexShaderResourceId,int fragmentShaderReourceId){
this.program=ShaderHelper.buildProgram(
TextResourceReader.readTextFileFromResource(context,vertexShaderResourceId),
TextResourceReader.readTextFileFromResource(context,fragmentShaderReourceId));
}
public void useProgram(){
GLES20.glUseProgram();
}
我們通過定義一些公用的常量作為這個類的開始,在建構函式中,我們呼叫剛剛定義過的輔助函式,其使用是指定的著色器構建了一個OpenGL著色器程式。我們用useProgram()作為結束,其呼叫glUseProgram()告訴OpenGL接下來的渲染要使用這個程式。
加入紋理著色器程式
我們現在將定義一個類來建立和表示紋理著色器程式。
建立一個名為TextureShaderProgram的新類,其繼承自ShaderProgram,並在該類內部加入如下程式碼:
private final int uMatrixLocation;
private final int uTextureUnitLocation;
private final int aPositionLocation;
private final int aTextureCoordinatesLocation;
我們加入了四個整型用來儲存那些uniform和屬性的位置。
下一步是初始化著色器程式,建立用於初始化著色器程式的建構函式,程式碼如下:
public TextureShaderProgram(Context context){
super(context,R.raw.texture_vertex_shader,R.raw.texture_fragment_shader);
this.uMatrixLocation=GLES20.glGetUniformLocation(program,U_MATRIX);
this.uTextureUnitLocation=GLES20.glGetUniformLocation(program,U_TEXTURE_UNIT);
this.aPositionLocation=GLES20.glGetAttribLocation(program,A_POSITION);
this.aTextureCoordinatesLocation=GLES20.glGetAttribLocation(program,A_TEXTURE_COORDINATES);
}
這個建構函式會用我們選擇的資源呼叫其父類的建構函式,其父類會構造著色器程式。我們讀入並儲存那些uniform和屬性的位置。
設定uniform並返回屬性的位置
傳遞矩陣和紋理給它們的uniform。加入如下程式碼:
public void setUniforms(float[] matrix,int textureId){
GLES20.glUniformMatrix4fv(this.uMatrixLocation,1,false,matrix,0);
GLES20.glActiveTexture(GLES20.GL_TEXTURE_2D);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D,textureId);
GLES20.glUniformli(this.uTextureUnitLocation,0);
}
第一步是傳遞矩陣給它的uniform,這足夠簡單明瞭。下一部分就是需要更多的解釋了。當我們在OpenGL裡使用紋理進行繪製時,我們不需要直接給著色器傳遞紋理。相反,我們使用紋理單元儲存那個紋理。之所以這樣做,是因為一個GPU只能同時繪製數量有限的紋理。它使用這些紋理表示當前正在被繪製的活動的紋理。
如果需要切換紋理,我們可以在紋理單元中來回切換紋理,但是,如果我們切換得太頻繁,可能會渲染的速度。也可以同時用幾個紋理單元繪製多個紋理。
通過呼叫glActiveTexture()把活動的紋理單元設定成為紋理單元0,我們以此開始,然後通過呼叫glBindTexture()把這個紋理繫結到這個單元,接著,通過呼叫glUniformli()把被選定的紋理單元傳遞給片段著色器中的u_TextureUnit。
我們幾乎已經完成了這個紋理器類;只需要一種方法來獲取屬性的位置,以便可以把它們繫結到正確的頂點陣列資料矩陣。加入如下程式碼完成這個類:
public int getPositionLocation(){
return this.aPositionLocation;
}
public int getTextureLocation(){
return this.aTextureCoordinatesLocation;
}
加入顏色著色器程式
在同一個包中建立另一個類,命名為ColorShaderProgram。這個類應該也繼承自ShaderProgram,它也遵循與TextureShaderProgram一樣的模式:有一個建構函式,一個設定uniform的方法和獲取屬性位置的方法。在此類內部加入如下程式碼:
private final int uMatrixLocation;
private final int aPositionLocation;
private final int aColorLocation;
public ColorShaderProgram(Context context){
super(context,R.raw.simple_vertex_shader,R.raw.simple_fragment_shader);
this.uMatrixLocation=GLES20.glGetUniformLocation(program,U_MATRIX);
this.aPositionLocation=GLES20.glGetAttribLocation(program,A_POSITION);
this.aColorLocation=GLES20.glGetAttribLocation(program,A_COLOR);
}
public void setUniform(float[] matrix){
GLES20.glUniformMatrix4fv(this.uMatrixLocation,1,false,matrix,0);
}
public int getPositionLocation(){
return this.aPositionLocation;
}
public int getColorLocation(){
return this.aColorLocation;
}
我們會使用這個專案繪製木槌。
通過把這些著色器程式與這些程式要繪製的資料進行解耦,就很容易重用這些程式碼了。比如,我們可以通過這個顏色著色器程式用一種顏色屬性繪製任何物體,而不僅僅是木槌。
5.繪製紋理
既然我們已經把頂點資料和著色器程式分別放於不同的類中了,現在就可以更新渲染類,使用紋理進行繪製了。開啟LYJRenderer,刪掉所有第三篇該類下面的程式碼,只保留onSurfaceChanged(),這是我們唯一不會改變的。加入如下成員變數和建構函式:
private final Context context;
private final float[] projectionMatrix=new float[16];
private final float[] modelMatrix=new float[16];
private Table table;
private Mallet mallet;
private TextureShaderProgram textureProgram;
private ColorShaderProgram colorProgram;
private int texture;
public LYJRenderer(Context context){
this.context=context
}
我們只保留上下文和矩陣的變數,並添加了頂點陣列,著色器程式和紋理的變數。這個建構函式被簡化為只儲存一個Android上下文的引用。
初始化變數
在onSurfaceCreated()加入初始化這些變數:
GLES20.glClearColor(0.0f,0.f,0.0f,0.0f);
this.table=new Table();
this.mallet=new Mallet();
this.textureProgram=new TextureShaderProgram(context);
this.colorProgram=new ColorShaderProgram (context);
this.texture=TextureHelper.loadTexture(Context,R.drawable.air_hockey_surface);
我們把清屏顏色設定為黑色,初始化頂點陣列和著色器程式。並用本篇博文的第一個小標題的函式載入紋理。
使用紋理進行繪製
不再贅述onSurfaceChanged(),因為它保持不變的,加入如下程式碼到onDrawFrame()繪製桌子和木槌:
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
this.textureProgram.useProgram();
this.textureProgram.setUniforms(this.projectionMatrix,this.texture);
this.table.bindData(this.textureProgram);
this.table.draw();
this.colorProgram.useProgram();
this.colorProgram.setUniforms(this.projectionMatrix);
this.mallet.bindData(this.colorProgram);
this.mallet.draw();
我們清空了渲染表面,接下來,我們做的第一件事是繪製桌子。我們首先呼叫this.textureProgram.useProgram();告訴OpenGL使用這個程式,然後通過呼叫this.textureProgram.setUniforms(this.projectionMatrix,this.texture);把那些uniform傳遞進來。下一步是通過呼叫this.table.bindData(this.textureProgram);把頂點陣列資料和著色器程式定起來,最後呼叫this.table.draw();繪製桌子。
我們重複同樣的呼叫順序,用顏色著色器程式繪製了木槌。
程式執行後的效果圖如下圖所示: