1、概述
在上一篇文章OpenGL ES 显示图形(上)中已经实现了显示三角形,但是遇到了一些问题,也就是说这个三角形和实际需求的不一样,存在变形拉伸。关于这个问题出现的原因是因为在Android设备上显示图形它们的屏幕尺寸和形状可能不同。OpenGL采用方形,均匀的坐标系统,默认情况下,可以将这些坐标快速绘制到典型的非方形屏幕上,就好像它是完全正方形一样。
上图左侧显示了OpenGL框架的统一坐标系统,以及这些坐标如何实际映射到右侧横向的典型设备屏幕。要解决此问题,可以应用OpenGL投影模式和摄像机视图来转换坐标,以便图形对象在任何显示上都具有正确的比例。为了应用投影和摄像机视图,需要创建投影矩阵和摄像机视图矩阵,并将它们应用于OpenGL渲染管道。投影矩阵重新计算图形的坐标,以便它们正确映射到Android设备屏幕。摄像机视图矩阵创建一个变换,从特定的眼睛位置渲染对象。这边就涉及到一个视见体的概念,其实OpenGL图形所处与于一个三维的世界,可以想象一下在屏幕上显示的其实是一个图像的投影,而屏幕可以理解为一块投影幕布,这样就可以通过改变相机的坐标和旋转移动投影来达到显示在投影上的物体的形状的拉伸与缩放。
如上图所示前面的那个点就代表一个假想的摄像机点,或者可以理解为假想人眼所处位置,中间的那个屏幕是要展示的渲染的对象。最后一块屏幕是手机屏幕。就可以通过移动假象摄像机点和手机屏幕来得到不同缩放和拉伸程度的图像。当然,这些都是假象的,这边的摄像机点不是指真的手机摄像头,也不会真的需要通过移动手机屏幕来实现效果。这边都是通过对图像进行相关的矩阵变换来实现实际的想要的效果。移动假想的摄像机就相当于对原图像其做一个摄像机矩阵变换,旋转移动手机屏幕相当于对原图做一个投影矩阵变换。下面还是继续以之前一篇文章的Demo为例子,讨论下用OpenGL ES 对这个问题的解决方法,以及图像的动画和交互。
2、投影和相机矩阵变换
如前面所说在OpenGL ES环境中,投影和摄像机矩阵允许假想以更接近用眼睛看物理对象的方式显示绘制对象。这种物理观察模拟是通过绘制对象坐标的数学变换完成的:
① 投影变换:此变换根据绘制对象的显示位置的宽度和高度调整绘制对象的坐标GLSurfaceView。如果没有这个变换,OpenGL ES绘制的对象会因视图窗口的不等比例而拉伸。通常只需要在onSurfaceChanged()渲染器的方法中建立或更改OpenGL视图的比例计算投影变换。
② 摄像机变换:此变换根据假想摄像机位置调整绘制对象的坐标。注意OpenGL ES不定义实际的相机对象,而是提供通过转换绘制对象的显示来模拟相机的效果。建立摄像机转换时,可能只计算一次 GLSurfaceView,或者也可能会根据用户操作或应用程序的功能动态更改。
2.1 定义投影变换
投影变换的数据在GLSurfaceView.Renderer类中onSurfaceChanged() 进行计算。以下示例代码通过获取GLSurfaceView的高度和宽度,通过使用其宽高来计算填充投影变换矩阵Matrix。
// mMVPMatrix是“模型视图投影矩阵”(Model View Projection Matrix)的缩写
private final float[] mMVPMatrix = new float[16];
private final float[] mProjectionMatrix = new float[16];
private final float[] mViewMatrix = new float[16];
@Override
public void onSurfaceChanged(GL10 unused, int width, int height) {
Log.d(TAG,"onSurfaceChanged:width = "+width+",height = "+height);
GLES20.glViewport(0, 0, width, height);
float ratio = (float) width / height;
// 此投影矩阵应用于onDrawFrame()方法中的对象坐标
Matrix.frustumM(mProjectionMatrix, 0, -ratio, ratio, -1, 1, 3, 7);
}
投影矩阵的核心方法是frustumM(float[] m, int offset, float left, float right, float bottom, float top, float near, float far)
第一个参数是输出的投影矩阵,第二个参数是从输出的数组的什么位置开始写入,第三到第六之后分别是投影对应于OpenGL ES默认坐标的对应上限的映射关系顺序分别为左右下上。最后两个参数有点抽象,用于显示图像的前面和背面,near和far需要结合假想摄相机即观察者眼睛的位置来设置,例如setLookAtM()(下面会讨论这个函数)中设置cx = 0, cy = 0, cz = 10,near设置的范围需要是小于10才可以看得到绘制的图像,如果大于10,图像就会处于了观察这眼睛的后面,这样绘制的图像就会消失在镜头前,far参数影响的是立体图形的背面,far一定比near大,一般会设置得比较大,如果设置的比较小,一旦3D图形尺寸很大,这时候由于far太小,这个投影矩阵就没法容纳图形全部的背面,这样3D图形的背面会有部分无法显示。
2.2 定义摄像机变换
通过在渲染器中添加摄像机视图变换作为绘图过程的一部分,完成变换绘制对象的过程。在以下示例代码中,使用Matrix.setLookAtM() 方法计算摄像机视图变换,然后将其与先前计算的投影矩阵组合。然后将组合的变换矩阵传递到绘制的形状。
// MyGLRenderer.class
@Override
public void onDrawFrame(GL10 unused) {
...
// 设置相机的位置 (View matrix)
Matrix.setLookAtM(mViewMatrix, 0, 0, 0, -3, 0f, 0f, 0f, 0f, 1.0f, 0.0f);
// 计算投影和视图转换
Matrix.multiplyMM(mMVPMatrix, 0, mProjectionMatrix, 0, mViewMatrix, 0);
// 绘制三角形
mTriangle.draw(mMVPMatrix);
}
相机位置设置核心函数是setLookAtM(float[] rm, int rmOffset, float eyeX, float eyeY, float eyeZ, float centerX, float centerY, float centerZ, float upX, float upY, float upZ) 这个函数的参数非常多,这也侧面印证了OpenGL ES 相当灵活而复杂。
第一个参数还是输出的矩阵,第二个还是输出的矩阵从那个下标开始写入。之后几个参数如下:eyeX、eyeY、eyeZ 假想相机或者说观察者眼睛位置,centerX、centerY、centerZ 目标视图中心位置,upX、upY、upZ相机角度。也就是说随着upX、upY、upZ的变化可以理解为相机在全方位的旋转。
结下来的multiplyMM(float[] result, int resultOffset, float[] lhs, int lhsOffset, float[] rhs, int rhsOffset); 这个方法是两个矩阵相乘,如果学过线性代数肯定知道矩阵相乘的话AB 和 BA 得到的是不同的矩阵,所以这里面的顺序不能对换。
2.3 应用投影和相机变换
为了使用上面的投影和摄像机视图变换矩阵,首先将矩阵变量添加到先前在Triangle类中定义的顶点着色器中:
public class Triangle {
private final StringvertexShaderCode =
//此矩阵成员变量提供了一个钩子来操纵使用此顶点着色器的对象的坐标
"uniform mat4 uMVPMatrix;" +
"attribute vec4 vPosition;" +
"void main() {" +
// uMVPMatrix因子必须在*前面才能使矩阵乘法乘积正确。
" gl_Position = uMVPMatrix * vPosition;" +
"}";
// 用于访问和设置模型视图投影矩阵(mMVPMatrix)
private int mMVPMatrixHandle;
...
}
接下来,修改图形对象的draw()方法以接受组合变换矩阵并将其应用于形状:
public void draw(float[] mvpMatrix) { // 将投影矩阵传入
...
// 获取形状变换矩阵的具柄
mMVPMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uMVPMatrix");
// 将模型视图投影矩阵传递给着色器
GLES20.glUniformMatrix4fv(mMVPMatrixHandle, 1, false, mvpMatrix, 0);
// 绘制三角形
GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, vertexCount);
// 禁用顶点数组
GLES20.glDisableVertexAttribArray(mPositionHandle);
}
一旦正确计算并应用了投影和摄像机视图转换,图形对象将以正确的比例绘制,并且应如下所示:
3、添加动画
在屏幕上绘制的对象是OpenGL的一个非常基本的功能。OpenGL ES还提供了三维或以其他独特方式移动和转换绘制对象的附加功能,以创建非常酷炫的用户体验。其实所有这些动画都和上面所述的图像修正的核心原理是一样的,都是通过对图像进行各种矩阵变换来实现。
使用OpenGL ES 2.0旋转绘图对象,需要在渲染器中,创建另一个变换矩阵(旋转矩阵),然后将其与投影和摄像机视图变换矩阵组合:
// MyGLRenderer.class
private float[] mRotationMatrix = new float[16];
Override
public void onDrawFrame(GL10 gl) {
float[] scratch = new float[16];
...
// 创建旋转矩阵
long time = SystemClock.uptimeMillis() % 4000L;
float angle = 0.090f * ((int) time);
Matrix.setRotateM(mRotationMatrix, 0, angle, 0, 0, -1.0f);
//将旋转矩阵与投影和摄像机视图结合时,mMVPMatrix因子必须在*前面才能使矩阵乘法乘积正确。
Matrix.multiplyMM(scratch, 0, mMVPMatrix, 0, mRotationMatrix, 0);
// 绘制三角形
mTriangle.draw(scratch);
}
如果在进行这些更改后三角形不旋转,请确保已注释掉 GLSurfaceView.RENDERMODE_WHEN_DIRTY 设置。否则OpenGL只旋转一个增量的形状然后等待调用requestRender()来进行图像重绘。
public MyGLSurfaceView(Context context) extends GLSurfaceView {
...
//setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
}
这边要说明下,除非在确定没有任何用户交互的情况对图像进行动画,否则平时建议打开这个标志。下个小节会取消注释此代码。
4、触摸事件交互
对象能运动之后,肯定会进一步思考如何使其实现和用户的交互。 OpenGL ES图形交互和普通的视图于用户比较类似。这里举一个通过扩展GLSurfaceView以覆盖 onTouchEvent()侦听触摸事件来进行交互的例子。
4.1 设置触摸监听器
为了使的OpenGL ES应用程序响应触摸事件,必须在GLSurfaceView类中实onTouchEvent()方法 。下面的示例实现通过监听 MotionEvent.ACTION_MOVE事件并将其转换为形状的旋转角度的方法来实现触摸交互。
// MyGLSurfaceView.class
private final float TOUCH_SCALE_FACTOR = 180.0f / 320;
private float mPreviousX;
private float mPreviousY;
@Override
public boolean onTouchEvent(MotionEvent e) {
// MotionEvent报告触摸屏和其他输入控件的输入详细信息。
// 在这种情况下,这里只对触摸位置发生变化的事件感兴趣。
float x = e.getX();
float y = e.getY();
switch (e.getAction()) {
case MotionEvent.ACTION_MOVE:
float dx = x - mPreviousX;
float dy = y - mPreviousY;
// reverse direction of rotation above the mid-line
if (y > getHeight() / 2) {
dx = dx * -1 ;
}
// reverse direction of rotation to left of the mid-line
if (x < getWidth() / 2) {
dy = dy * -1 ;
}
mRenderer.setAngle(
mRenderer.getAngle() +
((dx + dy) * TOUCH_SCALE_FACTOR));
requestRender();
}
mPreviousX = x;
mPreviousY = y;
return true;
}
这边要注意,在计算旋转角度后,需要调用 requestRender()告诉渲染器是时候进行重新渲染帧。在这个例子中,这样做是最有效的,因为除非旋转发生变化,否则不需要重绘帧。要实现这样的效率,需要把上一节的setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);注释取消掉。
4.2 暴露旋转角度属性
上面的示例代码已经通过触摸事件获取到来要旋转的角度,现在要将这个旋转角度传给渲染器进行渲染,所以要求通过添加公共成员来公开渲染器旋转角度。由于渲染器代码在与应用程序的主用户界面线程在不同的线程上运行,因此必须将此公共变量声明为volatile。以下是声明变量并公开getter和setter对的代码:
public class MyGLRenderer implements GLSurfaceView.Renderer {
...
public volatile float mAngle;
public float getAngle() {
return mAngle;
}
public void setAngle(float angle) {
mAngle = angle;
}
}
4.3、应用旋转交互
要应用触摸输入生成的旋转,请注释掉前面的角度和添加的代码mAngle,其中包含触摸输入生成的角度。
public void onDrawFrame(GL10 gl) {
...
float[] scratch = new float[16];
// 给三角形创建一个旋转变换
// long time = SystemClock.uptimeMillis() % 4000L;
// float angle = 0.090f * ((int) time);
// Matrix.setRotateM(mRotationMatrix, 0, angle, 0, 0, -1.0f);
Matrix.setRotateM(mRotationMatrix, 0, mAngle, 0, 0, -1.0f);
//将旋转矩阵与投影和摄像机视图结合时,mMVPMatrix因子必须在*前面才能使矩阵乘法乘积正确。
Matrix.multiplyMM(scratch, 0, mMVPMatrix, 0, mRotationMatrix, 0);
// 绘制三角形
mTriangle.draw(scratch);
}
这样就可以通过拖动屏幕实现三角形的动画。