天天看點

Android OpenGL 使用紋理(五) 1.把紋理加載進OpenGL中 2.建立新的着色器集合 3.為頂點資料建立新的類結構 4.為着色器程式添加類 5.繪制紋理

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.為頂點資料建立新的類結構

首先,我們将把頂點資料分離到不同的類中,每個類代表一個實體對象的類型。我們将為桌子建立一個新類,并為木槌建立一個新類。因為紋理上已經有一條直線了,是以我們不需要給那個分割線建立新類。

為了減少重複,我們會建立獨立的類,用于封裝實際的頂點數組。新的類結構看上去如下圖所示:

Android OpenGL 使用紋理(五) 1.把紋理加載進OpenGL中 2.建立新的着色器集合 3.為頂點資料建立新的類結構 4.為着色器程式添加類 5.繪制紋理

我們會建立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.為着色器程式添加類

我們會為紋理着色器建立一個類,并顔色器程式建立另一個類:我們會用紋理着色器繪制桌子,用顔色着色器繪制木槌。我們也會建立一個基類作為它們的公共函數。我們不用再擔心那條直線,因為它是紋理的一部分。

Android OpenGL 使用紋理(五) 1.把紋理加載進OpenGL中 2.建立新的着色器集合 3.為頂點資料建立新的類結構 4.為着色器程式添加類 5.繪制紋理

我們開始給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

程式運作後的效果圖如下圖所示:

Android OpenGL 使用紋理(五) 1.把紋理加載進OpenGL中 2.建立新的着色器集合 3.為頂點資料建立新的類結構 4.為着色器程式添加類 5.繪制紋理
Android OpenGL 使用紋理(五) 1.把紋理加載進OpenGL中 2.建立新的着色器集合 3.為頂點資料建立新的類結構 4.為着色器程式添加類 5.繪制紋理