本篇文章属于 使用 OpenGL ES 进行图形绘制 这个系列的第二篇文章,主要内容是介绍在如何在 Android 应用中利用定义 OpenGL 中图形的形状。文章中所有的代码示例都已放在 Github 上,可以去项目 OpenGL-ES-Learning 中查看 。
如同学习绘制自定义 View 一样,定义图形的形状是实现各种复杂的图形的基础,下面将介绍 OpenGL ES 相对于 Android 设备屏幕的坐标系、定义形状和形状绘制等基础知识。
定义一个三角形
OpenGL ES 允许我们使用三维空间的坐标来定义绘画对象。所以在我们能画三角形之前,必须先定义它的坐标。在 OpenGL 中,典型的办法是为坐标定义一个 Float 类型的顶点数组。为了效率最大化,我们可以将坐标写入一个 ByteBuffer,它将会传入 OpenGl ES 的 pipeline 来处理。
ByteBuffer 俗称缓冲器,在 NIO 中,数据的读写操作始终是与缓冲区相关联的。读取时信道 (SocketChannel) 将数据读入缓冲区,写入时首先要将发送的数据按顺序填入缓冲区。缓冲区是定长的,基本上它只是一个列表,它的所有元素都是基本数据类型。ByteBuffer 是最常用的缓冲区,它提供了读写其他数据类型的方法,且信道的读写方法只接收 ByteBuffer。关于 ByteBuffer 的更多内容,推荐一篇文章 Android中直播视频技术探究之—基础核心类ByteBuffer解析
public class Triangle {
/**
* 定义三角形顶点的坐标数据的浮点型缓冲区
*/
private FloatBuffer vertexBuffer;
// 坐标数组中的顶点坐标个数
static final int COORDINATES_PRE_VERTEX = 3;
static float triangleCoords[] = { // 以逆时针顺序;
0.0f, 0.622008459f, 0.0f, // top
-0.5f, -0.311004243f, 0.0f, // bottom left
0.5f, -0.311004243f, 0.0f // bottom right
};
// Set color with red, green, blue and alpha (opacity) values
float color[] = { 0.63671875f, 0.76953125f, 0.22265625f, 1.0f };
public Triangle(){
// 初始化形状中顶点坐标数据的字节缓冲区
// 通过 allocateDirect 方法获取到 DirectByteBuffer 实例
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(
// 顶点坐标个数 * 坐标数据类型 float 一个是 4 bytes
triangleCoords.length * 4
);
// 设置缓冲区使用设备硬件的原本字节顺序进行读取;
byteBuffer.order(ByteOrder.nativeOrder());
// 因为 ByteBuffer 是将数据移进移出通道的唯一方式使用,这里使用 “as” 方法从 ByteBuffer 中获得一个基本类型缓冲区(浮点缓冲区)
vertexBuffer = byteBuffer.asFloatBuffer();
// 把顶点坐标信息数组存储到 FloatBuffer
vertexBuffer.put(triangleCoords);
// 设置从缓冲区的第一个位置开始读取顶点坐标信息
vertexBuffer.position(0);
}
}
跟 View 的坐标系原点位于屏幕左上角不同,默认情况下 OpenGL ES 会假定一个坐标系,在这个坐标系中,[0, 0, 0](分别对应X轴坐标, Y轴坐标, Z轴坐标)对应的是GLSurfaceView 的中心。如 [1, 1, 0] 对应的是右上角,[-1, -1, 0] 对应的则是左下角。
在 OpenGL 里,我们要渲染的一切物体都要映射到 X 轴和 Y 轴上 [-1,1] 的范围内,对于Z轴也一样。这个范围内的坐标被称为归一化设备坐标,其独立于屏幕实际尺寸或形状。也就是说在 Android 设备上显示图形时屏幕的尺寸和形状虽然会有所不同,但是 OpenGL 假设了一个平方均匀的坐标系,默认情况下将这些坐标按比例绘制非正方形屏幕上,就好像它是完全正方形一样。
如上图所示的坐标系,左图是默认的 OpenGL 坐标系,右图是实际展示 Android 设备屏幕时的坐标系,会看到三角形会有一个明显的拉伸。默认情况下,对于 OpenGL 而言不管硬件设备屏幕是不是正方形,都把它当作一个正方形来处理,三维坐标都限定在 [-1, 1]内。 所以 Open GL 的坐标体系独立于实际的屏幕尺寸。
要处理画面被拉伸的问题,可以考虑调整坐标空间,把屏幕的形状考虑在内,可行的一个方法是把较小的范围固定在 [-1,1] 内,而按屏幕尺寸的比例调整较大的范围。这里推荐一篇文章:Android OpenGL ES 调整屏幕的宽高比,文章中提到了如何处理 Open GL 在实际展示时宽高比控制。而这篇文章:OpenGL ES 透视投影则是对归一化设备坐标到视口(视口:OpenGL 渲染操作最终显示窗口)的窗口坐标转化说明。这些内容建议暂时放一下,遇到相关问题时在深入了解。
注意到上面这个形状的坐标是以逆时针顺序定义的。绘制的顺序非常关键,因为它定义了哪一面是形状的正面(希望绘制的一面),以及背面(使用 OpenGL ES 的 Cull Face 功能可以让背面不要绘制)。更多关于该方面的信息,可以阅读 OpenGL ES 开发手册。
定义一个矩形
在 OpenGL 中定义三角形非常简单,那定义一个矩形呢?有很多方法可以用来定义矩形,不过在 OpenGL ES 中最典型的办法是使用两个三角形拼接在一起:
同样的我们通过按逆时针顺序为三角形顶点定义坐标来表示这个图形,并将值放入一个 ByteBuffer 中。为了避免由两个三角形重合的那条边的顶点被重复定义,可以使用一个绘制列表(drawing list)来告诉 OpenGL ES 绘制顺序。下面是代码样例:
public class Square {
/**
* 顶点坐标数据缓冲区(float 类型)
*/
private FloatBuffer vertexBuffer;
/**
* 绘制顺序数据缓冲区(short类型)
*/
private ShortBuffer drawListBuffer;
/**
* 顶点坐标数据的数组
*/
static float squareCoords[] = {
-0.5f, 0.5f, 0.0f, // top left
-0.5f, -0.5f, 0.0f, // bottom left
0.5f, -0.5f, 0.0f, // bottom right
0.5f, 0.5f, 0.0f }; // top right
/**
* 绘制顶点顺序
*/
private short drawOrder[] = { 0, 1, 2, 0, 2, 3 }; // order to draw vertices
public Square() {
// initialize vertex byte buffer for shape coordinates
ByteBuffer bb = ByteBuffer.allocateDirect(
// (# of coordinate values * 4 bytes per float)
squareCoords.length * 4);
bb.order(ByteOrder.nativeOrder());
vertexBuffer = bb.asFloatBuffer();
vertexBuffer.put(squareCoords);
vertexBuffer.position(0);
// initialize byte buffer for the draw list
ByteBuffer dlb = ByteBuffer.allocateDirect(
// (# of coordinate values * 2 bytes per short)
drawOrder.length * 2);
dlb.order(ByteOrder.nativeOrder());
drawListBuffer = dlb.asShortBuffer();
drawListBuffer.put(drawOrder);
drawListBuffer.position(0);
}
}
该样例可以看作是一个如何使用 OpenGL 创建复杂图形的启发,通常来说我们需要使用三角形的集合来绘制对象。
文章中所有的代码示例都已放在 Github 上,可以去项目 OpenGL-ES-Learning 中查看 。
上述内容主要是对如何定义简单形状进行一个说明,了解 OpenGL 坐标体系规则,下面将了解如何在屏幕上画这些形状。