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

 

程序运行后的效果图以下图所示:


相关文章
相关标签/搜索