上一篇文章介绍了OpenGL绘制三维图形的流程,其实没有传说中的那么玄乎,只要放平常心把它当作一个普通控件就好了,接下来继续介绍OpenGL具体的绘图操作,这项工作得靠三维图形的画笔GL10来完成了。
GL10作为三维空间的画笔,它所描绘的三维物体却要显示在二维平面上,显而易见这不是一个简单的伙计。为了理顺物体从三维空间到二维平面的变换关系,有必要搞清楚OpenGL关于三维空间的几个基本概念。下面就概括介绍一下GL10编码的三类常见方法:
一、颜色的取值范围
Android中的三原色,不管是红色还是绿色还是蓝色,取值范围都是0到255,对应的十六进制数值则为00到FF,颜色数值越小表示亮度越弱,数值越大表示亮度越强。但在OpenGL之中,颜色的取值范围却是0.0到1.0,其中0.0对应Android标准的0,1.0对应Android标准的255,同理,OpenGL值为0.5的颜色对应Android标准的128。
GL10与颜色有关的方法主要有两个,说明如下:
glClearColor : 设置背景颜色。以下代码表示给三维空间设置白色背景:
// 设置白色背景。四个参数依次为透明度alpha、红色red、绿色green、蓝色blue
gl.glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
glColor4f : 设置画笔颜色。以下代码表示把画笔颜色设置为橙色:
// 设置画笔颜色为橙色
gl.glColor4f(0.0f, 1.0f, 1.0f, 0.0f);
二、三维坐标系
三维空间用来表达立体形状,需要三个方向的坐标,分别为水平方向的x轴和y轴,以及垂直方向的z轴。如下图的三维坐标系所示,三维空间有个M点,该点在x轴上的投影为P点,在y轴上的投影为Q点,在z轴上的投影为R点,因此M点的坐标位置就是(P, Q, R)。
既然三维空间中的每个点都存在x、y、z三个方向的坐标值,那么与物体位置有关的方法均需提供x、y、z三方向的数值。比如物体的旋转方法glRotatef、平移方法glTranslatef、缩放方法glScalef,要分别指定物体在三个坐标轴上的旋转方向、平移距离、缩放倍率。具体的方法调用例子如下所示:
// 沿着y轴的负方向旋转90度
gl.glRotatef(90, 0, -1, 0);
// 沿x轴方向移动1个单位
gl.glTranslatef(1, 0, 0);
// x,y,z三方向各缩放0.1倍
gl.glScalef(0.1f, 0.1f, 0.1f);
三、坐标矩阵变换
有了三维坐标系,还要把三维物体投影到二维平面上,才能在手机屏幕中绘制三维图形。这个投影操作主要有三个步骤,下面分别展开叙述:
1、设置绘图区域
前面说过OpenGL使用GLSurfaceView这个控件作为绘图场所,于是允许绘制的区域范围自然落在GLSurfaceView内部。设置绘图区域的方法是glViewport,它指定了该区域左上角的平面坐标,以及区域的宽度和高度。当然一般OpenGL的绘图范围与GLSurfaceView的大小重合,所以倘若GLSurfaceView控件的宽度为width,高度为height,则设置绘图区域的方法调用示例如下:
// 设置输出屏幕大小
gl.glViewport(0, 0, width, height);
2、调整镜头参数
框住了绘图区域,还要把三维物体在二维平面上的投影一点一点描绘进去才行,这中间的坐标变换计算由OpenGL内部自行完成,开发者无需关注具体的运算逻辑。好比日常生活中的拍照,用户只管拿起手机咔嚓一下,根本不用关心摄像头怎么生成照片。用户所关心的照片效果,不外乎景物是大还是小,是远还是近;用专业一点的术语来讲,景物的大小由镜头的焦距决定,景物的远近由镜头的视距决定。
对于镜头的焦距而言,拍摄同样尺寸的照片,广角镜头看到的景物比标准镜头看到的景物更多,这意味着单个景物在广角镜头中会比较小,从而照片面积不增大、容纳的景物却变多了。
对于镜头的视距而言,它表示镜头的视力好坏,即最近能看到多近的景物,最远能看到多远的景物。在日常生活当中,每个人的睫毛离自己的眼睛太近了,这么近的东西能看得清楚吗?所以必须规定一下,最近只能看清楚比如离眼睛十厘米的物体。很遥远的景物自然也是看不清楚的,所以也要规定一下,比如最远只能看到一公里之内的人影。这个能看清景物的最近距离和最远距离,就构成了镜头的视距。
所以,镜头的焦距是横向的,它反映了画面的广度;而镜头的视距是纵向的,它反映了画面的深度。在OpenGL中,这些镜头参数的调节依赖于GL10的gluPerspective方法,具体的参数调整代码举例如下:
// 设置投影矩阵,对应gluPerspective(调整相机参数)、glFrustumf(调整透视投影)、glOrthof(调整正投影)
gl.glMatrixMode(GL10.GL_PROJECTION);
// 重置投影矩阵,即去掉所有的参数调整操作
gl.glLoadIdentity();
// 设置透视图视窗大小。第二个参数是焦距的角度,第四个参数是能看清的最近距离,第五个参数是能看清的最远距离
GLU.gluPerspective(gl, 40, (float) width / height, 0.1f, 20.0f);
3、挪动观测方位
调整好了镜头的拍照参数,要不要再摆个POSE,来个花式摄影?比如用户跃上好几级台阶,居高临下拍摄;也可俯下身子,从下向上拍摄;还能把手机横过来拍或者倒过来拍。要是怕摄影家累坏了,不妨叫摆拍的模特自己挪动身影,或者走进或者走远,往左靠一点或者往右靠一点,还可以躺下来甚至倒立过来。
因此,不管是挪动相机的位置,还是挪动物体的位置,都会让照片里的景物发生变化。挪动相机的位置,依靠的是GL10的gluLookAt方法;至于挪动物体的位置,依靠的则是旋转方法glRotatef、平移方法glTranslatef,以及缩放方法glScalef了。下面是OpenGL挪动相机位置的方法调用代码:
// 选择模型观察矩阵,对应gluLookAt(人动)、glTranslatef/glScalef/glRotatef(物动)
gl.glMatrixMode(GL10.GL_MODELVIEW);
// 重置模型矩阵,即去掉所有的位置挪动操作
gl.glLoadIdentity();
// 设置镜头的方位。第二到第四个参数为相机的位置坐标,第五到第七个参数为相机画面中心点的坐标,第八到第十个参数为朝上的坐标方向,比如第八个参数为1表示x轴朝上,第九个参数为1表示y轴朝上,第十个参数为1表示z轴朝上
GLU.gluLookAt(gl, 10.0f, 8.0f, 6.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f);
注意到前面调整相机参数和挪动相机位置这两个动作,都事先调用了glMatrixMode与glLoadIdentity方法,这是什么缘故呢?其实这两个方法结合起来只不过是状态重置操作,好比把手机恢复出厂设置,接下来重新进行状态设置。glMatrixMode方法的参数指定了重置操作的类型,像GL10.GL_PROJECTION类型涵盖了所有的镜头参数调整方法,包括gluPerspective(调整相机参数)、glFrustumf(调整透视投影)、glOrthof(调整正投影)三种方法,每次重置GL10.GL_PROJECTION类型,意味着之前的这三种参数设置统统失效。而GL10.GL_MODELVIEW类型涵盖的是位置变换的相关方法,包括挪动相机的gluLookAt方法,以及挪动物体的glTranslatef/glScalef/glRotatef方法,每次重置GL10.GL_MODELVIEW类型,意味着之前的位置挪动统统失效。
现在了解了以上的三维绘图的常见方法,接下来再看OpenGL的应用代码就会比较轻松了。先来看看一个最简单的三维立方体是如何实现的,下面是OpenGL绘制立方体的代码例子片段:
public class GlCubeActivity extends Activity {
private final static String TAG = "GlCubeActivity";
private GLSurfaceView glsv_content;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_gl_cube);
initVertexs(); // 初始化立方体的顶点集合,具体说明详见下一篇文章
glsv_content = (GLSurfaceView) findViewById(R.id.glsv_content);
// 注册渲染器
glsv_content.setRenderer(new GLRender());
}
private class GLRender implements GLSurfaceView.Renderer {
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
// 背景:白色
gl.glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
// 启动阴影平滑
gl.glShadeModel(GL10.GL_SMOOTH);
}
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
// 设置输出屏幕大小
gl.glViewport(0, 0, width, height);
// 设置投影矩阵,对应gluPerspective(调整相机)、glFrustumf(调整透视投影)、glOrthof(调整正投影)
gl.glMatrixMode(GL10.GL_PROJECTION);
// 重置投影矩阵,即去掉所有的平移、缩放、旋转操作
gl.glLoadIdentity();
// 设置透视图视窗大小
GLU.gluPerspective(gl, 40, (float) width / height, 0.1f, 20.0f);
// 选择模型观察矩阵,对应gluLookAt(人动)、glTranslatef/glScalef/glRotatef(物动)
gl.glMatrixMode(GL10.GL_MODELVIEW);
// 重置模型矩阵
gl.glLoadIdentity();
}
@Override
public void onDrawFrame(GL10 gl) {
// 清除屏幕和深度缓存
gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
// 重置当前的模型观察矩阵
gl.glLoadIdentity();
// 设置画笔颜色
gl.glColor4f(0.0f, 0.0f, 1.0f, 1.0f);
// 设置镜头的方位
GLU.gluLookAt(gl, 10.0f, 8.0f, 6.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f);
// 旋转图形
//gl.glRotatef(angle, 0, 0, -1);
//gl.glRotatef(angle, 0, -1, 0);
// 沿x轴方向移动1个单位
//gl.glTranslatef(1, 0, 0);
// x,y,z方向缩放0.1倍
//gl.glScalef(0.1f, 0.1f, 0.1f);
// 绘制一个立方体,具体说明详见下一篇文章
drawCube(gl);
}
}
}