在上一篇教程中,我们主要讨论了网格,以及如何在网格中添加颜色。实际上向网格添加颜色的更普遍的做法是使用纹理。向网格添加纹理是与添加颜色有些不同的,接下来,我将展示这些操作,并解释一些基本知识。
载入Bitmap
第一步,载入用于生成纹理的Bitmap。你可以通过下载,生成或者从resources中加载一张图片。我这里是用的最简单的方式——从resources中直接加载一张图片。
Bitmap bitmap = BitmapFactory.decodeResource(contect.getResources(),R.drawable.icon);
关于纹理需要注意的一点是,一些硬件会要求纹理的高宽必须为2的N次幂(1,2,4,8,16,32,64。。。。),如果你在这些设备上使用一个比如30px*30px大小的纹理,那么你只能得到一个白色方块(除非你更改了默认颜色)。
生成纹理
在我们加载Bitmap完成之后,我们就需要告诉opengl去生成一个纹理了。
首先,我们要先让opengl生成一些纹理的id,我们将会用它持有我们后续产生的纹理。在这个例子中,我们只有一个纹理。
// 创建一个数组,容量为我们需要的纹理数量,int[] textures = new int[1];// 让opengl去产生纹理idgl.glGenTextures(1, textures, 0);
使用相同的参数,你可以删除纹理:
// 删除纹理gl.glDeleteTextures(1, textures, 0)
在纹理生成之后,就像其他的元素一样,我们只需要告诉opengl要处理什么即可。我们使用glBindTexture命令来绑定纹理:
gl.glBindTexture(GL10.GL_TEXTURE_2D, textures[0]);
这样,我们后续调用的关于纹理的所有的命令都将会应用给这个id的纹理。
glTexParameter
我们需要给纹理设置一些参数,第一个参数就是需要告诉opengl是否要缩放纹理以匹配绘制的场景。如果纹理偏小,那么通过缩放的函数可以达到放大的目的:
//如果纹理偏小,则放大gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_LINEAR);
反之,下面是如何通过缩放函数去缩小纹理的操作:
// scale linearly when image smalled than texturegl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_LINEAR);
你需要传递一个参数给这些函数——这里我只展示了两个,其余的你可以自己尝试。
如果你想要一个清晰而干净的渲染结果,那么你应该使用这个参数:GL10.GL_NEAREST.
如果你想要一个模糊的渲染结果,那么你应该使用这个参数:GL10.GL_LINEAR.
UV Mapping
我们还需要告诉opengl如何把这张图片映射到网格上,需要两步,首先我们要建立UV坐标。UV映射是把二维图片的像素映射到三维顶点的过程。UV坐标左上角为(0,0),右下角为(1,1),如下左图。而下右图所示的是我们如何建立平面。
为了正确的映射纹理,我们需要把纹理的左下角部分texture(0,1)映射到我们平面的左下角的顶点vertice(0),把纹理的右下角(1,1)映射到我们平面顶点的右面(1).......后面的你知道怎么回事了。
我们把映射放进一个float数组中:
float textureCoordinates[] = {0.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f };
如果我们使用0.5代替上面数组中的1.0:
float textureCoordinates[] = {0.0f, 0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 0.5f, 0.0f };
平面上将会只有被映射纹理的左上角,如下图:
回到glParameterf,如果我们使用另一种方式,同时把上面的1.0替换成2.0,如下:
float textureCoordinates[] = {0.0f, 2.0f, 2.0f, 2.0f, 0.0f, 0.0f, 2.0f, 0.0f };
实际上我们是在告诉opengl去使用纹理中“不存在”的部分,所以我们需要告诉opengl如何去处理这个“不存在”的部分。
GL_REPEAT意味着opengl将会按照类似1.0的那个设置来展示并重复使用纹理。
GL_CLAMP_TO_EDGE意味着opengl将会绘制纹理一次,然后只重复纹理的最后一条像素线。
由于我们是在处理2D的纹理,所以我们需要告诉opengl如何去做。
下面的四个图示是GL_REPEAT和GL_CLAMP_EDGE组合结果示意图:
这是我们如何去使用glParameterf方法:
gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_S, GL10.GL_REPEAT);gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_T, GL10.GL_REPEAT);
最后我们要做的是把Bitmap帮到到我们前面产生的纹理id上。
GLUtils.texImage2D(GL10.GL_TEXTURE_2D, 0, bitmap, 0);
使用纹理
为了能够使用纹理,我们要做的仅仅是像其他元素一样,用UV坐标构造一个ByteBuffer。
FloatBuffer byteBuf = ByteBuffer.allocateDirect(texture.length * 4);byteBuf.order(ByteOrder.nativeOrder());textureBuffer = byteBuf.asFloatBuffer();textureBuffer.put(textureCoordinates);textureBuffer.position(0);
渲染
// 告诉opengl开启纹理模式gl.glEnable(GL10.GL_TEXTURE_2D);// 告诉opengl,纹理已经被分配到内存中gl.glBindTexture(GL10.GL_TEXTURE_2D, textures[0]);// 告诉opengl开启使用UV坐标映射gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY);// 告诉openglUV坐标的buffergl.glTexCoordPointer(2, GL10.GL_FLOAT, 0, textureBuffer);// 这里省略网格渲染// 禁用UV映射gl.glDisableClientState(GL10.GL_TEXTURE_COORD_ARRAY);//禁用纹理模式gl.glDisable(GL10.GL_TEXTURE_2D);
组装
我使用的是上一篇教程的修改后的代码。最大的不同应该是我重命名了一些变量和方法名,增加了更多的注释,并且现在所有的代码基于Apache协议授权。为了让代码更易懂,我删除了原来的Plane类,改为使用SimplePlane。
更新mesh类
首先我们需要更新mesh类(se.jayway.opengl.tutorial.mesh.Mesh)。我们需要增加加载和渲染纹理的方法。我们需要能够设置和保存UV坐标。
// Our UV texture buffer.private FloatBuffer mTextureBuffer;/** * Set the texture coordinates. * * @param textureCoords */protected void setTextureCoordinates(float[] textureCoords) { // float is 4 bytes, therefore we multiply the number if // vertices with 4. ByteBuffer byteBuf = ByteBuffer.allocateDirect( textureCoords.length * 4); byteBuf.order(ByteOrder.nativeOrder()); mTextureBuffer = byteBuf.asFloatBuffer(); mTextureBuffer.put(textureCoords); mTextureBuffer.position(0);}
我们还需要设置Bitmap和生成纹理的方法:
// Our texture id.private int mTextureId = -1;// The bitmap we want to load as a texture.private Bitmap mBitmap;/** * Set the bitmap to load into a texture. * * @param bitmap */public void loadBitmap(Bitmap bitmap) { this.mBitmap = bitmap; mShouldLoadTexture = true;}/** * Loads the texture. * * @param gl */private void loadGLTexture(GL10 gl) { // Generate one texture pointer... int[] textures = new int[1]; gl.glGenTextures(1, textures, 0); mTextureId = textures[0]; // ...and bind it to our array gl.glBindTexture(GL10.GL_TEXTURE_2D, mTextureId); // Create Nearest Filtered Texture gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_LINEAR); gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_LINEAR); // Different possible texture parameters, e.g. GL10.GL_CLAMP_TO_EDGE gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_S, GL10.GL_CLAMP_TO_EDGE); gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_T, GL10.GL_REPEAT); // Use the Android GLUtils to specify a two-dimensional texture image // from our bitmap GLUtils.texImage2D(GL10.GL_TEXTURE_2D, 0, mBitmap, 0);}
最终,我们加入对纹理加载的调用以及告知opengl用这个纹理去渲染。因篇幅有限,我精简了一部分代码,你可以从附件里找到完整版。
// Indicates if we need to load the texture.private boolean mShouldLoadTexture = false;/** * Render the mesh. * * @param gl * the OpenGL context to render to. */public void draw(GL10 gl) { ... // Smooth color if (mColorBuffer != null) { // Enable the color array buffer to be used during rendering. gl.glEnableClientState(GL10.GL_COLOR_ARRAY); gl.glColorPointer(4, GL10.GL_FLOAT, 0, mColorBuffer); } if (mShouldLoadTexture) { loadGLTexture(gl); mShouldLoadTexture = false; } if (mTextureId != -1 && mTextureBuffer != null) { gl.glEnable(GL10.GL_TEXTURE_2D); // Enable the texture state gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY); // Point to our buffers gl.glTexCoordPointer(2, GL10.GL_FLOAT, 0, mTextureBuffer); gl.glBindTexture(GL10.GL_TEXTURE_2D, mTextureId); } gl.glTranslatef(x, y, z); ... // Point out the where the color buffer is. gl.glDrawElements(GL10.GL_TRIANGLES, mNumOfIndices, GL10.GL_UNSIGNED_SHORT, mIndicesBuffer); ... if (mTextureId != -1 && mTextureBuffer != null) { gl.glDisableClientState(GL10.GL_TEXTURE_COORD_ARRAY); } ...}
创建SimplePlane类
我们还需要创建SimplePlane.java 。这段代码很简单,而且如果你看过我之前的教程,那么你能明白是什么意思。新增的变量是textureCoordinates。
package se.jayway.opengl.tutorial.mesh;/** * SimplePlane is a setup class for Mesh that creates a plane mesh. * * @author Per-Erik Bergman (per-erik.bergman@jayway.com) * */public class SimplePlane extends Mesh { /** * Create a plane with a default with and height of 1 unit. */ public SimplePlane() { this(1, 1); } /** * Create a plane. * * @param width * the width of the plane. * @param height * the height of the plane. */ public SimplePlane(float width, float height) { // Mapping coordinates for the vertices float textureCoordinates[] = { 0.0f, 2.0f, // 2.0f, 2.0f, // 0.0f, 0.0f, // 2.0f, 0.0f, // }; short[] indices = new short[] { 0, 1, 2, 1, 3, 2 }; float[] vertices = new float[] { -0.5f, -0.5f, 0.0f, 0.5f, -0.5f, 0.0f, -0.5f, 0.5f, 0.0f, 0.5f, 0.5f, 0.0f }; setIndices(indices); setVertices(vertices); setTextureCoordinates(textureCoordinates); }}
引用
你可以下载这篇教程的源码:
你也可以使用版本工具检出:
上一篇教程: