OpenGL ES 运用投影与相机视角
文章中所有的代码示例都已放在 Github 上,可以去项目 OpenGL-ES-Learning 中查看 。
在前面的文章中介绍了如何绘制形状,相信你对于使用 OpenGL ES 进行绘制的流程有了大致的了解。其中包括一些基本概念:
- GLSurfaceView 作为绘制视图内容的容器载体;
- GLSurfaceView. Renderer 作为控制绘制内容和过程的渲染器
- OpenGL 坐标系的概念以及借助 ByteBuffer 定义形状的坐标数据
- 绘制形状的三要素:顶点着色器、片元着色器、程式
如果你对这些概念还有些不熟悉,可以回头再看一下前面的文章,把这些简单的概念过一遍可以强化理解。
在 OpenGL ES 环境中,利用投影和相机视角可以让显示的绘图对象更加酷似于我们用肉眼看到的真实物体。该物理视角的模拟是对绘制对象坐标的进行数学变换实现的:
-
投影(Projection):这个是基于调整绘图对象在 GLSurfaceView 中的宽和高的坐标来转换的。如果没有该计算,那么用 OpenGL ES 绘制的对象会由于其长宽比例和 View 窗口比例的不一致而发生形变。一个投影变换一般仅当 OpenGL View 的比例在刚被建立或发生变化(在 onSurfaceChanged() 中回调)时才进行计算。
-
相机视角(Camera View):这个变换会基于一个虚拟相机位置改变来进行。注意到 OpenGL ES 并没有定义一个没有定义一个真实的 camera 对象,而是提供了一些辅助方法,通过对绘图对象的变换来模拟相机视角。一个相机视角变换可能仅在GLSurfaceView 刚建立时计算一次,也可能根据用户的行为或者 app 的功能进行动态调整。
关于更多 OpenGL ES 投影和坐标映射的知识,可以阅读 Mapping Coordinates for Drawn Objects。
本篇文章主要阐述如何创建一个投影和一个相机视角,并应用到 GLSurfaceView 中的绘制图像上。
定义一个投影
投影变换的数据是在 GLSurfaceView.Renderer 类的 onSurfaceChanged() 方法中计算出来的。
下面的代码先获取 GLSurfaceView 的高和宽,然后利用它并使用 Matrix.frustumM() 方法来填充一个投影变换矩阵(Projection Transformation Matrix):
public class MyGLRenderer3 implements GLSurfaceView.Renderer {
...
// mMVPMatrix is an abbreviation for "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 gl, int width, int height) {
GLES20.glViewport(0, 0, width, height);
// 获取 GLSurfaceView 的宽和高的比例
float ratio = (float) width / height;
// this projection matrix is applied to object coordinates
// in the onDrawFrame() method
// 填充了一个投影矩阵:mProjectionMatrix
Matrix.frustumM(mProjectionMatrix, 0, -ratio, ratio, -1, 1, 3, 7);
}
}
Note: 在绘图对象上只应用一个投影变换时会导致显示 empty display 。所以我们在进行 projection transformation(投影变换)时通常还要进行一个相机视角转化,使得显示对象能全部出现在屏幕上。
定义一个相机视角
在渲染器中添加一个相机视角变换作为图形绘制过程的一部分,以此完成我们的绘图对象所需变换的所有步骤。下面的代码在 onDrawFrame 回调返回中使用 Matrix.setLookAtM() 方法来计算相机视角变换,然后与之前计算的投影矩阵结合起来,结合后的变换矩阵传递给绘制图像:
@Override
public void onDrawFrame(GL10 gl) {
// Set the camera position (View matrix)
Matrix.setLookAtM(mViewMatrix, 0, 0, 0, -3, 0f, 0f, 0f, 0f, 1.0f, 0.0f);
// Calculate the projection and view transformation
Matrix.multiplyMM(mMVPMatrix, 0, mProjectionMatrix, 0, mViewMatrix, 0);
// Draw shape
mTriangle.draw(mMVPMatrix);
}
因此整个渲染器包含了投影变换和相机视变换的结合,该类的全部代码如下所示:
public class MyGLRenderer3 implements GLSurfaceView.Renderer {
private Triangle mTriangle;
// mMVPMatrix is an abbreviation for "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 onSurfaceCreated(GL10 gl, EGLConfig config) {
// initialize a triangle
mTriangle = new Triangle();
}
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
GLES20.glViewport(0, 0, width, height);
float ratio = (float) width / height;
// this projection matrix is applied to object coordinates
// in the onDrawFrame() method
Matrix.frustumM(mProjectionMatrix, 0, -ratio, ratio, -1, 1, 3, 7);
}
@Override
public void onDrawFrame(GL10 gl) {
// Set the camera position (View matrix)
// 改变摄像头在 z 轴上的位置(镜头拉伸),让视图调整到合适的大小。
Matrix.setLookAtM(mViewMatrix, 0, 0, 0, -3, 0f, 0f, 0f, 0f, 1.0f, 0.0f);
// Calculate the projection and view transformation
Matrix.multiplyMM(mMVPMatrix, 0, mProjectionMatrix, 0, mViewMatrix, 0);
// Draw shape
mTriangle.draw(mMVPMatrix);
}
}
应用投影和相机变换
为了使用在之前章节中结合了的相机视角变换和投影变换,我们首先为之前在 Triangle 类中定义的顶点着色器添加一个 Matrix 变量:
class Triangle {
/**
* 顶点着色器代码
*/
private final String vertexShaderCode =
// This matrix member variable provides a hook to manipulate
// the coordinates of the objects that use this vertex shader
"uniform mat4 uMVPMatrix;" + // 添加一个 Matrix 变量
"attribute vec4 vPosition;" + // 应用程序传入顶点着色器的顶点位置
"void main() {" +
// the matrix must be included as a modifier of gl_Position
// Note that the uMVPMatrix factor *must be first* in order
// for the matrix multiplication product to be correct.
" gl_Position = uMVPMatrix * vPosition;" + // 设置此次绘制此顶点位置,进行矩阵变换
"}";
// Use to access and set the view transformation
private int mMVPMatrixHandle;
...
}
再修改原有图形对象(Triangle)的 draw() 方法,使得它接收组合后的变换矩阵,并将它应用到图形上:
public void draw(float[] mvpMatrix) { // pass in the calculated transformation matrix
...
// get handle to shape's transformation matrix
mMVPMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uMVPMatrix");
// Pass the projection and view transformation to the shader
GLES20.glUniformMatrix4fv(mMVPMatrixHandle, 1, false, mvpMatrix, 0);
// Draw the triangle
GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, vertexCount);
// Disable vertex array
GLES20.glDisableVertexAttribArray(mPositionHandle);
}
下面给出图形对象的完整代码:
class Triangle {
// 绘制形状的顶点数量
private static final int COORDS_PER_VERTEX = 3;
/**
* 顶点着色器代码
*/
private final String vertexShaderCode =
// This matrix member variable provides a hook to manipulate
// the coordinates of the objects that use this vertex shader
"uniform mat4 uMVPMatrix;" +
"attribute vec4 vPosition;" + // 应用程序传入顶点着色器的顶点位置
"void main() {" +
// the matrix must be included as a modifier of gl_Position
// Note that the uMVPMatrix factor *must be first* in order
// for the matrix multiplication product to be correct.
" gl_Position = uMVPMatrix * vPosition;" + // 设置此次绘制此顶点位置
"}";
/**
* 片元着色器代码
*/
private final String fragmentShaderCode =
"precision mediump float;" + // 设置工作精度
"uniform vec4 vColor;" + // 应用程序传入着色器的颜色变量
"void main() {" +
" gl_FragColor = vColor;" + // 颜色值传给gl_FragColor内建变量,完成片元的着色
"}";
/**
* 定义三角形顶点的坐标数据的浮点型缓冲区
*/
private FloatBuffer vertexBuffer;
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 };
private final int mProgram;
private int mPositionHandle;
private int mColorHandle;
// Use to access and set the view transformation
private int mMVPMatrixHandle;
private final int vertexCount = triangleCoords.length / COORDS_PER_VERTEX;
private final int vertexStride = COORDS_PER_VERTEX * 4; // 4 bytes per vertex
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);
// 加载编译顶点渲染器
int vertexShader = MyGLRenderer2.loadShader(GLES20.GL_VERTEX_SHADER,
vertexShaderCode);
// 加载编译片元渲染器
int fragmentShader = MyGLRenderer2.loadShader(GLES20.GL_FRAGMENT_SHADER,
fragmentShaderCode);
// create empty OpenGL ES Program
mProgram = GLES20.glCreateProgram();
// add the vertex shader to program
GLES20.glAttachShader(mProgram, vertexShader);
// add the fragment shader to program
GLES20.glAttachShader(mProgram, fragmentShader);
// creates OpenGL ES program executables
GLES20.glLinkProgram(mProgram);
}
public void draw(float[] mvpMatrix) { // pass in the calculated transformation matrix
// Add program to OpenGL ES environment
GLES20.glUseProgram(mProgram);
// get handle to vertex shader's vPosition member
mPositionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition");
// Enable a handle to the triangle vertices
GLES20.glEnableVertexAttribArray(mPositionHandle);
// Prepare the triangle coordinate data
GLES20.glVertexAttribPointer(mPositionHandle, COORDS_PER_VERTEX,
GLES20.GL_FLOAT, false,
vertexStride, vertexBuffer);
// get handle to fragment shader's vColor member
mColorHandle = GLES20.glGetUniformLocation(mProgram, "vColor");
// Set color for drawing the triangle
GLES20.glUniform4fv(mColorHandle, 1, color, 0);
// Draw the triangle
GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, vertexCount);
// Disable vertex array
GLES20.glDisableVertexAttribArray(mPositionHandle);
// get handle to shape's transformation matrix
mMVPMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uMVPMatrix");
// Pass the projection and view transformation to the shader
GLES20.glUniformMatrix4fv(mMVPMatrixHandle, 1, false, mvpMatrix, 0);
// Draw the triangle
GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, vertexCount);
// Disable vertex array
GLES20.glDisableVertexAttribArray(mPositionHandle);
}
}
一旦我们正确地计算并应用了投影变换和相机视角变换,我们的图形就会以正确的比例绘制出来,它看上去会像是这样:
效果图文章中所有的代码示例都已放在 Github 上,可以去项目 OpenGL-ES-Learning 中查看 。
现在,应用已经可以按正确的比例显示图形了,下面就来学习为图形添加一些动作效果。