OpenGL ES MVP/变换投影矩阵(8)
简述
通过前面几节的学习,目前我们已经可以渲染自己想要的图像,也可以通过纹理加载图片进行渲染。接下来我们来学习一下MVP,这里的MVP不是Android应用开发里的框架MVP,而是Model,View,Projection。
首先我们先来解释一下这个是做什么的,这三者都是用来计算最终图像呈现的,举个例子,我们如果要开发一个游戏,游戏里的一个物体放在那里,物体放置的角度不同,我们看到的效果可能不一样,我们从不同的位置角度看物体,看到的也可能不一样,MVP就是在计算物体在不同的放置情况,不同的观察角度最终的成像,接下来我们一个一个介绍。
变换矩阵
在开始介绍MVP三个概念之前,我们先来介绍一下变换矩阵。
变换矩阵是MVP的基础原理,我们这里只介绍一下概念,不会深入它的原理,因为它的原理是一些数学运算,需要一些线性代数的基础。
我们对图像的变换计算,都是通过乘以一个矩阵来实现的,我们只需要知道我们将坐标乘以一个矩阵,将它映射到另一个坐标,这样就可以完成图形的变换,比如平移,缩放,翻转等等,所以我们各种变换都是通过乘以一个变换矩阵实现的,每一个模型都有一个对应的矩阵。
MVP
坐标变换:
最终坐标 = Mat(projection) * Mat(View) * Mat(model) * 原坐标
投影(Projection)
我们的图像可能是3D的,但是最终需要显示在我们的屏幕上,我们的屏幕是2D的,只能显示一个平面,所以我们需要通过计算将3D图像最终显示在2D到图像上,这便是一种投影。
投影矩阵一般有两种,一种是透视投影矩阵,另一种是正交投影矩阵。
透视投影矩阵是可以越远越小,类似现在的3D游戏,而正交投影有点类似2.5D游戏,大小不会受到远近的影响。
视口(View)
视口所处理的问题就是相机位置,比如相机向左移动,其实看到的图像就向右移动了,View的变换矩阵就是用于描述这个逻辑。
模型(Model)
这个变换矩阵描述的是物体自己的变化,比如物体自己放大,翻转,平移等等。
渲染正方体
我们以渲染一个旋转的正方体为例来介绍这几个模型。
顶点数据
我们需要渲染一个正方体,有8个顶点,正方体由6个面组成,每个面是2个三角形,就是12个三角形,我们索引缓冲区就是12个三角形,36个点。
private float[] vertexArray = new float[] {
-0.5f, -0.5f, -0.5f,
0.5f, -0.5f, -0.5f,
-0.5f, 0.5f, -0.5f,
0.5f, 0.5f, -0.5f,
-0.5f, -0.5f, 0.5f,
0.5f, -0.5f, 0.5f,
-0.5f, 0.5f, 0.5f,
0.5f, 0.5f, 0.5f,
};
private short[] indexArray = new short[] {
0,1,2,
1,2,3,
0,1,4,
1,4,5,
2,3,7,
2,6,7,
4,5,6,
5,6,7,
0,2,4,
2,6,4,
1,3,5,
3,5,7
};
着色器
顶点着色器有一个统一变量mvpMatrix,这个矩阵由model矩阵,view矩阵,projection矩阵相乘得到,我们这里只是演示就将乘法放在Cpu上处理,其实这种乘法操作放在GPU上效率会更高。
片段着色器中我们使用一个统一变量表示颜色,这里我们想要把每一个面渲染成不同颜色,因为如果是同颜色又没有边界,就看不出立体的效果。
这里我们会在每渲染两个三角形后修改统一变量来修改颜色,后面渲染的时候会看到,这个方法其实效率不是最高的,因为会多次调用DrawCall,多次DrawCall必然会增加开销,不过我们这里主要是为了演示变换矩阵的用法,所以就怎么方便怎么来了。
private final String vertexShaderCode =
"attribute vec4 vPosition;" +
"uniform mat4 mvpMatrix;" +
"void main() {" +
" gl_Position = mvpMatrix * vPosition;" +
"}";
private final String fragmentShaderCode =
"precision mediump float;" +
"uniform vec4 outputColor;" +
"void main() {" +
" gl_FragColor = outputColor;" +
"}";
顶点缓冲区/索引缓冲区数据填充
填充流程和之前基本一样,新增了一个GLES30.glEnable(GL_DEPTH_TEST)调用,这个表示需要OpenGL ES根据Z轴来判断深度,处理覆盖关系。
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
// 清除颜色
GLES30.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
GLES30.glEnable(GL_DEPTH_TEST);
// 创建顶点缓冲区
int[] idBuffer = new int[2];
GLES30.glGenBuffers(2, idBuffer, 0);
vertexBufferId = idBuffer[0];
elementBufferId = idBuffer[1];
// 顶点缓冲区数据填充
FloatBuffer vertexBuffer = ByteBuffer.allocateDirect(vertexArray.length * 4).order(ByteOrder.nativeOrder()).asFloatBuffer();
vertexBuffer.put(vertexArray);
vertexBuffer.position(0);
GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER, vertexBufferId);
GLES30.glBufferData(
GLES30.GL_ARRAY_BUFFER,
vertexArray.length * 4,
vertexBuffer,
GLES30.GL_STATIC_DRAW
);
GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER, 0);
ShortBuffer indexBuffer = ByteBuffer.allocateDirect(indexArray.length * 2).order(ByteOrder.nativeOrder()).asShortBuffer();
indexBuffer.put(indexArray);
indexBuffer.position(0);
GLES30.glBindBuffer(GLES30.GL_ELEMENT_ARRAY_BUFFER, elementBufferId);
GLES30.glBufferData(
GLES30.GL_ELEMENT_ARRAY_BUFFER,
indexArray.length * 2,
indexBuffer,
GLES30.GL_STATIC_DRAW
);
GLES30.glBindBuffer(GLES30.GL_ELEMENT_ARRAY_BUFFER, 0);
// shader
shaderProgramId = initShaderProgram(vertexShaderCode, fragmentShaderCode);
}
初始化projection和view矩阵
setIdentityM初始化矩阵,会将矩阵所有值初始化为0,斜角线值初始化为1。
使用setLookAtM获取视口矩阵,使用perspectiveM获取透视投影矩阵,参数下面介绍了。
将两个矩阵相乘存放在vpMatrix,这里之所以没有把model矩阵也放上,是因为我们需要通过model矩阵变化实现正方体旋转,所以需要将model矩阵放在渲染的时候。
public void onSurfaceChanged(GL10 gl, int width, int height) {
GLES30.glViewport(0, 0, width, height);
// setLookAtM用于获取view矩阵,参数为相机位置x,y,z。模型中心位置坐标x,y,z。向上向量x,y,z
float[] viewMatrix = new float[16];
Matrix.setIdentityM(viewMatrix, 0);
Matrix.setLookAtM(viewMatrix, 0, 3,3,5,0,0f,0f,0f,1f, 0f);
float[] projectionMatrix = new float[16];
Matrix.setIdentityM(projectionMatrix, 0);
// 获取透视投影矩阵,参数有:视角范围45度,宽高比,透视最近距离和最远距离。
Matrix.perspectiveM(projectionMatrix,0,45f, ((float) getWidth()) / getHeight(),0.3f,50);
Matrix.multiplyMM(vpMatrix, 0, projectionMatrix, 0, viewMatrix, 0);
}
渲染
我们这里用radiu表示渲染角度,每次渲染加2度,如果是60HZ则一秒钟会旋转120度。
通过model矩阵旋转radius度,每次渲染两个三角形换一个颜色,以实现不同面不同颜色。
乘出来最终的矩阵就是mvp矩阵,通过统一变量传递mvp矩阵。
public void onDrawFrame(GL10 gl) {
// 清除屏幕
GLES30.glClear(GL_DEPTH_BUFFER_BIT);
GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT);
// 使能着色器程序
GLES30.glUseProgram(shaderProgramId);
GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER, vertexBufferId);
int positionLocation = GLES30.glGetAttribLocation(shaderProgramId, "vPosition");
GLES30.glEnableVertexAttribArray(positionLocation);
GLES30.glVertexAttribPointer(positionLocation, 3, GLES30.GL_FLOAT, false, 3 * 4, 0);
radiu += 2;
if (radiu >= 360) {
radiu = radiu % 360;
}
Matrix.setIdentityM(modelMatrix, 0);
// model矩阵旋转。后三个参数x,y,z为变量轴
Matrix.rotateM(modelMatrix, 0, radiu, 1, 1, 0);
Matrix.multiplyMM(mvpMatrix, 0, vpMatrix, 0, modelMatrix, 0);
int matrixLocation = GLES30.glGetUniformLocation(shaderProgramId, "mvpMatrix");
FloatBuffer mvpMatrixBuffer = ByteBuffer.allocateDirect(vertexArray.length * 4).order(ByteOrder.nativeOrder()).asFloatBuffer();
mvpMatrixBuffer.put(mvpMatrix);
mvpMatrixBuffer.position(0);
// 将mvp矩阵通过统一变量传给着色器
GLES30.glUniformMatrix4fv(matrixLocation, 1, false, mvpMatrixBuffer);
int outputColorLocation = GLES30.glGetUniformLocation(shaderProgramId, "outputColor");
GLES30.glBindBuffer(GLES30.GL_ELEMENT_ARRAY_BUFFER, elementBufferId);
// 调用DrawCall绘制三角形
// 每次渲染6个顶点,两个三角形,然后就换颜色。
for (int i = 0; i < 6; i++) {
GLES30.glUniform4f(outputColorLocation, colorList[i][0], colorList[i][1], colorList[i][2], colorList[i][3]);
GLES30.glDrawElements(GLES30.GL_TRIANGLES, 6, GLES30.GL_UNSIGNED_SHORT, 6 * i * 2);
}
// 清除配置
GLES30.glDisableVertexAttribArray(positionLocation);
GLES30.glBindBuffer(GLES30.GL_ELEMENT_ARRAY_BUFFER, 0);
GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER, 0);
GLES30.glUseProgram(0);
}
效果
小结
我们这节通过实现一个旋转的正方体来简单的介绍了一下MVP的矩阵变换模式。这也是我们OpenGL ES最后一节了,其实OpenGL ES还有很多高级的技术,但是几乎都是跟图像数学相关的,OpenGL ES自身的核心接口能力以及介绍的差不多了。
也许在遥远的未来,我们还会写一章Vulkan的教程,在这里埋个坑。