Android OpenGL二 —— 使用投影和相机变换
OpenGL 的投影和相机视图
书接上文,在Android OpenGL ES一 ——入门中,利用OpenGL绘制了一个三角形。但最终呈现出来的图形和我们预想的正三角形不一致,这是因为你定义的坐标点,并没有被正确的绘制到屏幕上,本文将解决这个问题。
投影视图?相机视图?跟投影仪和相机有关系?这都什么鬼!!!!!
为了不被看官大人喷,先引入一些OpenGL中的一些概念。
OpenGL中的视图
计算机图形的要点是创建三维图像的二维投影,在各种屏幕上的图像必定是二维的。区别在于,OpenGL在决定如何在屏幕上绘制时,采用的是三维坐标的方式考虑。为了将所绘制物体的三维坐标转换为屏幕上的二维坐标。通常需要做如下几步:
- 相机视图和投影变换(此外还有模型):这些变换通过矩阵乘法的方式,实现移动、旋转、缩放等操作。
- 裁剪:三维物体投影到特定的屏幕,肯定有不需要看见的边缘部分,需要把他们裁减掉。
- 视口变换:该操作实现从三维坐标到二维坐标的映射。
这里主要解释相机视图和投影视图。
相机视图和投影视图
看到相机视图,你是不是觉得疑惑。绘图和相机有什么关系。没错,确实没有什么关系。相机只是OpenGL中对于更好解释视图概念的一种形象比喻。
我们使用相机拍照时,通常存在以下操作。
- 在固定的位置摆放相机,这就是相机视图的操作,也称作视图变换。
- 在场景中摆放物体的位置,这是模型变换(仅作了解,本文不会涉及)。
- 调整相机焦距,这是投影变换。
- 其他
上述操作一,相当于设置相机的位置,也可以理解为设置人眼的位置坐标,这决定你站在场景空间中的什么位置观察物体。术语中的解释是,设置视景体的坐标(设置人眼的观察坐标后,相对的,视景体的坐标也被确定)。
操作三,投影变换最终决定三维物体最终投影在屏幕上的形状,当然,这将涉及到立体到平面的坐标变换。投影变换分为透视投影好平面投影。这里这简单介绍透视投影。透视投影的特点是,物体越远,在图像中看上去越小(是不是和真是情况一致?)。
看到这里,你应该对相机视图、投影视图有了一定了解。接下来进入正文。
两个视图概念在Android中的体现
- Projection(投影视图变换) - 该变化根据显示他们的GLSurfaceView的高和宽来调整绘制对象的坐标。没有这些坐标的调整计算,OpenGL ES绘制的对象将被不对等的视图窗口所扭曲。典型的投影变换只需要在OpenGL视图比例创建或改变的渲染器的onSurfaceChanged()方法计算即可。
- Camera View (相机视图变换)- 这个变换根据虚拟相机的位置调整绘制对象的坐标。需要注意的是,OpenGL ES并不是定义一个真是的相机对象,而是通过转换绘制对象的显示来提供虚拟相机的公共方法。当你构建你GLSurfaceView时,相机视图的变化也许只会被计算一次,或者更具用户的活动动态变化。
定义投影
投影坐标变换数据是在GLSurfaceView.Renderer的onSurfaceChanged()方法中进行的。示例代码中获取GLSurfaceView的宽高,并且通过Matrix.frustumM()填充投影的变换矩阵。
// 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) {
GLES20.glViewport(0, 0, width, height);
float ratio = (float) width / height;
// 定义了一个用于计算透视投影矩阵的**平截头体**,和投影矩阵相乘得到一个矩阵
// 这个投影矩阵被用于onDrawFrame()中绘制对象的坐标系
Matrix.frustumM(mProjectionMatrix, 0, -ratio, ratio, -1, 1, 3, 7);
}
这段代码填充一个投影矩阵,你可以在onDrawFrame()中将 mProjectionMatrix 和相机视图变换联合到一起,这将在下面的内容中提及。
在绘制对象时只应用投影变换非常空洞,通常,为了在屏幕上展示任何对象,你也需要同时使用相机视图变换。
定义相机视图
将相机视图变换,作为绘制进程的一部分,加入你的渲染器(renderer)中,即可完成绘制对象的坐标转换操作。在如下代码中,使用Matrix.setLookAtM()计算相机视图变换,然后将之前计算好的投影矩阵与之关联。最后将关联起来的变换举证传递给图形绘制。
@Override
public void onDrawFrame(GL10 unused) {
...
/**
* 设置相机的位置(相当于观察点的坐标)
*void setLookAtM(matrix, offset,//视图矩阵和偏移量
* eysx, eyey, eyez,//定义目标观察点的位置
* centerx, centery, centerz,//指定视线上任意一点
* upx, upy, upz)//表示那个方向朝上
**/
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);
}
应用相机和投影变换
public class Triangle {
private final String vertexShaderCode =
// 这个矩阵的成员变量提供了一个Hook来操作使用顶点渲染器渲染的对象的坐标
"uniform mat4 uMVPMatrix;" +
"attribute vec4 vPosition;" +
"void main() {" +
// 矩阵必须包含gl_Position修饰符
// 注意,为了保证乘积的可靠性,uMVPMatrix元素必须是第一个
" gl_Position = uMVPMatrix * vPosition;" +
"}";
// 用于设置视图变换
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);
}
只要你正确计算并应用了投影和相机视图变换,你的图形对象就能够呈现出正常的比例,比如这样。
初步接触OpenGL,欢迎评论交流。源码地址:https://github.com/MrHeLi/OpenGLDemo
参看资料:
OpenGL练习主页:https://learnopengl-cn.github.io/01%20Getting%20started/01%20OpenGL/
Android官网:https://developer.android.com/training/graphics/opengl/projection.html
OpenGL编程指南(第七版)。