Android OpenGL ES(八)----紋理編程框架

1.把紋理加載進OpenGL中



咱們的第一個任務就是把一個圖像文件的數據加載到一個OpenGL的紋理中。數組


做爲開始,讓咱們從新捨棄第二篇的框架,從新建立一個程序,新建一個util工具包,在該包下建立一個新類TextureHelper,咱們將如下面的方法簽名開始:框架


public static int loadTexture(Context context,int resourceId){}ide


這個方法會把Android上下文,和資源ID做爲輸入參數,並返回加載圖像的OpenGL紋理的ID。開始時,咱們會使用建立其餘OpenGL對象時同樣的模式生成一個新的紋理ID。函數


final int[] textureObjectIds=new int[1];工具

GLES20.glGenTextures(1,textureObjectIds,0);ui

if(textureObjectId[0]==0){this

Log.w(TAG,"建立紋理失敗!");編碼

}spa



經過傳遞1做爲第一個參數調用glGenTextures(),咱們就建立了一個紋理對象。OpenGL會把那個生成的ID存儲在textureObjectIds中。咱們也檢查了glGenTextures()調用是否成功,若是結果不等於0就繼續,不然記錄那個錯誤並返回0。由於TAG尚未定義,讓咱們在類的頂部爲它加入以下定義:.net


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();繪製桌子。


咱們重複一樣的調用順序,用顏色着色器程序繪製了木槌。


源代碼地址:http://download.csdn.net/detail/liyuanjinglyj/8848105

 

程序運行後的效果圖以下圖所示:


相關文章
相關標籤/搜索