本文参考了王刚的《疯狂Android讲义(第3版)》P554-P559
要求:利用OpenGL ES绘制一个三棱锥,并对每个面进行纹理贴图,每个面使用不同的图片进行渲染。
环境:Android Studio 3.6.1,gradle版本为5.6.4,OpenGL使用1x版本,新建工程后需在res/drawable目录下放4张图片img1、img2、img3、img4(建议图片长宽均是2的n次方,如果不符合则系统会自动调节,我使用的图片长宽是256×256)。
运行结果:

主要步骤:
1.在MainActivity.java文件自定义内部公共类MyRenderer实现接口Renderer(也可新建Java文件来实现)。
2.定义三棱锥的顶点坐标、纹理坐标、几个需要用到的Buffer及其他数据等。
// 定义三棱椎的4个顶点
private float[] taperVertices = new float[]{
0.0f, 0.5f, 0.0f, //0
-0.5f, -0.5f, -0.2f, //1
0.5f, -0.5f, -0.2f, //2
0.0f, -0.2f, 0.2f //3
};
// 定义纹理贴图的坐标数据
private float[] taperTextures = {
0.5000f, 0.0000f, 0.0000f, 1.0000f, 1.0000f, 1.0000f,
0.0000f, 1.0000f, 1.0000f, 1.0000f, 0.5000f, 0.0000f,
0.0000f, 1.0000f, 1.0000f, 1.0000f, 0.5000f, 0.0000f,
1.0000f, 1.0000f, 0.0000f, 1.0000f, 0.5000f, 0.0000f,
};
private Context context;
private FloatBuffer taperVerticesBuffer;
private ByteBuffer[] taperFacts_indices;
private FloatBuffer taperTexturesBuffer;
// 定义本程序所使用的纹理
private int[] textures = new int[4];
// 旋转角度
float rotate = 0;
定义纹理坐标数据要注意:Android使用的OpenGL ES的纹理坐标系跟官方的OpenGL纹理坐标系统不一样。官方的OpenGL ES纹理坐标为左下角是(0, 0)右上角是(1, 1);而Android的纹理坐标左上角为(0, 0)右下角为(1, 1)。
3.实现MyRenderer(Context main)方法
public MyRenderer(Context main) {
this.context = main;
// 将三棱椎的顶点位置数据数组包装成FloatBuffer
taperVerticesBuffer = floatBufferUtil(taperVertices);
// 将三棱椎的4个面的数组indices[1:4]包装成ByteBuffer并加入一个数组中
ByteBuffer indices1 = ByteBuffer.wrap(new byte[]{
0, 1, 2,
0, 0, 0,
0, 0, 0,
0, 0, 0
});
ByteBuffer indices2 = ByteBuffer.wrap(new byte[]{
0, 0, 0,
0, 1, 3,
0, 0, 0,
0, 0, 0
});
ByteBuffer indices3 = ByteBuffer.wrap(new byte[]{
0, 0, 0,
0, 0, 0,
1, 2, 3,
0, 0, 0
});
ByteBuffer indices4 = ByteBuffer.wrap(new byte[]{
0, 0, 0,
0, 0, 0,
0, 0, 0,
0, 2, 3
});
taperFacts_indices = new ByteBuffer[]{indices1, indices2, indices3, indices4};
// 将立方体的纹理贴图的坐标数据包装成FloatBuffer
taperTexturesBuffer = floatBufferUtil(taperTextures);
}
4.纹理映射功能默认是关闭的,所以需要在onSurfaceCreated内启用纹理映射功能。
// 启用2D纹理贴图
gl.glEnable(GL10.GL_TEXTURE_2D);
// 装载纹理
loadTexture(gl);
loadTexture()是自定义的一个装载纹理的方法,实现方法如下:
private void loadTexture(GL10 gl) {
Bitmap[] bitmap = new Bitmap[4];
try {
// 加载位图
bitmap[0] = BitmapFactory.decodeResource(context.getResources(),
R.drawable.img1);
bitmap[1] = BitmapFactory.decodeResource(context.getResources(),
R.drawable.img2);
bitmap[2] = BitmapFactory.decodeResource(context.getResources(),
R.drawable.img3);
bitmap[3] = BitmapFactory.decodeResource(context.getResources(),
R.drawable.img4);
// 创建纹理对象,指定生成N个纹理(第一个参数指定生成几个纹理)
// textures数组将负责存储所有纹理的代号
gl.glGenTextures(4, textures, 0);
for (int i = 0; i < textures.length; i++) {
// 通知OpenGL将texture纹理绑定到GL10.GL_TEXTURE_2D目标中
gl.glBindTexture(GL10.GL_TEXTURE_2D, textures[i]);
// 设置纹理被缩小(距离视点很远时被缩小)时的滤波方式
gl.glTexParameterf(GL10.GL_TEXTURE_2D,
GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_NEAREST);
// 设置纹理被放大(距离视点很近时被方法)时的滤波方式
gl.glTexParameterf(GL10.GL_TEXTURE_2D,
GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_LINEAR);
// 设置在横向、纵向上都是平铺纹理
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);
// 加载位图生成纹理
GLUtils.texImage2D(GL10.GL_TEXTURE_2D, 0, bitmap[i], 0);
}
} finally {
// 生成纹理之后,回收位图
for (int i = 0; i < bitmap.length; i++)
bitmap[i].recycle();
}
}
其中几个函数的含义如下:
void glGenTextures(int n,int[] textures,int offset) //创建纹理对象
- n:需要生成的纹理对象个数;
- textures:保存生成所有纹理对象的数组;
- offset:纹理对象数组的偏移(生成的纹理从数组什么位置开始赋值)。
void glBindTexture (int target, int texture) //绑定纹理,绑定后的纹理才处于活动状态
- target:纹理类型,使用 2D 纹理则参数设为GL_TEXTURE_2D;
- texture:纹理对象的 ID。
void glTexParameterf(int target,int pname,float param) //对绑定的纹理进行基本属性设置
- target:纹理类型,表示我们激活的纹理单元对应了什么样的操作类型,比如GL_TEXTURE_1D、GL_TEXTURE_2D等;
- pname:属性名称;
- param:属性值。
void texImage2D (int target, int level, Bitmap bitmap, int border) //指定纹理
- target:操作的目标类型,本程序均设为 GL_TEXTURE_2D 即可
- level:纹理的级别,表示多级分辨率的纹理图象的级数。若只有一种分辨率,level为0。
- bitmap:使用的图像
- border:边框,一般设为0
Bitmap decodeResource(Resources res, int id) //加载位图
- res:包含图像数据的Resources对象
- id:图像数据的资源id
5.绘制图像及纹理
//绘制图形的方法
@Override
public void onDrawFrame(GL10 gl) {
// 清除屏幕缓存和深度缓存
gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
// 启用顶点坐标数据
gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
// 启用贴图坐标数组数据
gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
// 设置当前矩阵模式为模型视图。
gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
// 把绘图中心移入屏幕2个单位
gl.glTranslatef(0f, 0.0f, -2.0f);
// 旋转图形
gl.glRotatef(rotate, -0.1f, -0.1f, 0.05f); // 旋转总坐标系
// 设置顶点的位置数据
gl.glVertexPointer(3, GL10.GL_FLOAT, 0, taperVerticesBuffer);
// 设置贴图的坐标数据
gl.glTexCoordPointer(2, GL10.GL_FLOAT, 0, taperTexturesBuffer); // ②
//使用for循环给三棱锥每个面都贴上相应的纹理图
for(int i = 0; i < taperFacts_indices.length; i++) {
// 绑定纹理,执行纹理贴图
gl.glBindTexture(GL10.GL_TEXTURE_2D, textures[i]);
// 按taperFacts_indices[i]指定的面绘制三角形
gl.glDrawElements(GL10.GL_TRIANGLES, 3*(i+1), GL10.GL_UNSIGNED_BYTE, taperFacts_indices[i]);
}
// 绘制结束
gl.glFinish();
// 禁用顶点、纹理坐标数组
gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);
gl.glDisableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
//角度+1使绘制的图形旋转显示其他面
rotate += 1;
}
void glTexCoordPointer(int size, int type, int stride, Buffer pointer) //设置顶点数组为纹理坐标缓存
- size:纹理顶点坐标的分量个数,本程序使用纹理坐标的2个分量(s, t)映射顶点坐标的3个分量(x, y, z);
- type:纹理坐标的数据类型,short, int, float, double都可以;
- stride:位图的宽度,可以理解为相邻的两个纹理之间跨多少个字节,一般为0,因为一般不会在纹理中再添加其他的信息;
- pointer:存放纹理坐标的数组。
总结:如果想多面体各个面的纹理不一样,则在画每个面之前要加上void glBindTexture (int target, int texture)函数绑定纹理。