OpenGL.ES在Android上的简单实践:11-全景(正方体-索引-深度测试)
0、全景图要怎么看?
What is 全景?可能很多人单看这名字不太清楚。但看到下面的图的时候就噢的一声~瞬间廓然开朗
大家在欣赏大视野的同时,有些人可能也觉得和普通的照片没啥区别嘛╮(╯▽╰)╭就比普通的照片大一点嘛。是这样吗?再看看下面的连接:http://720yun.com/t/242jO7smen4?pano_id=3330421 (这是我比较喜欢的陕西榆林黄土流沙地貌的全景图 ( ̄▽ ̄)/) 怎么样,打开新世界了是吧?所以这就是全世界了吗?错了,有空的同学可以找一个叫Insta360的app,它是国外社交软件Instagram的衍生工具Insta360 ONE摄像头的附属应用,里面才是全世界啊,兄die!
本篇开始,我们将基于OpenGL,自己做一个Insta360 Player,实现其中的水晶球-鱼眼-小行星等效果,增加对OpenGL的认识,包括不仅限以下内容:索引,VBO,FBO,三大矩阵的深入使用,各种功能标志的理解和使用,特效动画,最后还会延伸一些模拟视频渲染的知识。
1、认识索引
在开始之前,我们有这么一个简单的需求:用我们之前学过的知识,画出一个彩色的正方体。我们如下图一一分析。
一个正方体有八个面,每个面四个点,And笛卡尔坐标系把空间分成了8个区间,所以这些点都分别坐落在坐标系的其中一个区间;我们各取一个单位1长度,这样就按如图显示呈现出8个坐标点。
下一步我们开始拼凑正方体Cube的每个平面的三角形,数据有点多,注意是否多了少了个负号,颜色值有没写错(# ̄~ ̄#)
public class Cube {
private static final float[] CUBE_DATA = {
//x, y, z R, G, B
1f, 1f, 1f, 1, 0, 1, //近平面第一个三角形
-1f, 1f, 1f, 1, 0, 0,
-1f, -1f, 1f, 0, 0, 1,
1f, 1f, 1f, 1, 0, 1, //近平面第二个三角形
-1f, -1f, 1f, 0, 0, 1,
1f, -1f, 1f, 0, 1, 0,
1f, 1f, -1f, 0, 0, 1, //远平面第一个三角形
-1f, 1f, -1f, 0, 1, 0,
-1f, -1f, -1f, 1, 0, 1,
1f, 1f, -1f, 0, 0, 1, //远平面第二个三角形
-1f, -1f, -1f, 1, 0, 1,
1f, -1f, -1f, 1, 0, 0,
-1f, 1f, -1f, 0, 1, 0, //左平面第一个三角形
-1f, 1f, 1f, 1, 0, 0,
-1f, -1f, 1f, 0, 0, 1,
-1f, 1f, -1f, 0, 1, 0, //左平面第二个三角形
-1f, -1f, 1f, 0, 0, 1,
-1f, -1f, -1f, 1, 0, 1,
1f, 1f, -1f, 0, 0, 1, //右平面第一个三角形
1f, 1f, 1f, 1, 0, 1,
1f, -1f, 1f, 0, 1, 0,
1f, 1f, -1f, 0, 0, 1, //右平面第二个三角形
1f, -1f, 1f, 0, 1, 0,
1f, -1f, -1f, 1, 0, 0,
1f, 1f, -1f, 0, 0, 1, //上平面第一个三角形
-1f, 1f, -1f, 0, 1, 0,
-1f, 1f, 1f, 1, 0, 0,
1f, 1f, -1f, 0, 0, 1, //上平面第二个三角形
-1f, 1f, 1f, 1, 0, 0,
1f, 1f, 1f, 1, 0, 1,
1f, -1f, -1f, 1, 0, 0, //下平面第一个三角形
-1f, -1f, -1f, 1, 0, 1,
-1f, -1f, 1f, 0, 0, 1,
1f, -1f, -1f, 1, 0, 0, //下平面第二个三角形
-1f, -1f, 1f, 0, 0, 1,
1f, -1f, 1f, 0, 1, 0,
};
}
顶点创建完后,我们准备对应的着色器。因为我们的顶点数据包括位置和颜色,所以我们顶点着色器如下:
uniform mat4 u_Matrix;
attribute vec4 a_Position;
attribute vec4 a_Color;
varying vec4 v_Color;
void main()
{
v_Color = a_Color;
gl_Position = u_Matrix * a_Position;
gl_PointSize = 10.0;
}
u_Mateix存放三大矩阵的乘积结果,通过varying把颜色值传递到片段着色器,通过OpenGL内置的颜色混合效果显示出来
precision mediump float;
varying vec4 v_Color;
void main()
{
gl_FragColor = v_Color;
}
我们建立对应的着色器程序CubeShaderProgram:
public class CubeShaderProgram extends ShaderProgram {
protected static final String U_MATRIX = "u_Matrix";
public final int uMatrixLocation;
protected static final String A_POSITION = "a_Position";
public final int aPositionLocation;
protected static final String A_COLOR = "a_Color";
public final int aColorLocation;
public CubeShaderProgram(Context context) {
super(context, R.raw.cube_one_vs, R.raw.cube_one_fs);
uMatrixLocation = GLES20.glGetUniformLocation(programId, U_MATRIX);
aColorLocation = GLES20.glGetAttribLocation(programId, A_COLOR);
aPositionLocation = GLES20.glGetAttribLocation(programId, A_POSITION);
}
public void setUniforms(float[] matrix){
GLES20.glUniformMatrix4fv(uMatrixLocation, 1, false, matrix, 0);
}
}
再下一步,我们加入三大矩阵,为Cube绑定着色器程序,准备好一切之后,我们就可以在测试Activity的页面上替换Renderer画出来了。最终的代码如下:
public class Cube {
private static final int POSITION_COMPONENT_COUNT = 3;
private static final int COLOR_COMPONENT_COUNT = 3;
private static final int STRIDE = (POSITION_COMPONENT_COUNT+COLOR_COMPONENT_COUNT)* Constants.BYTES_PER_FLOAT;
private final VertexArray vertexArray;
public float[] modelMatrix = new float[16];
public Cube() {
vertexArray = new VertexArray(CUBE_DATA);
Matrix.setIdentityM(modelMatrix,0);
}
public void bindData(CubeShaderProgram shaderProgram){
vertexArray.setVertexAttributePointer(
shaderProgram.aPositionLocation,
POSITION_COMPONENT_COUNT,
STRIDE,
0);
vertexArray.setVertexAttributePointer(
shaderProgram.aColorLocation,
COLOR_COMPONENT_COUNT,
STRIDE,
POSITION_COMPONENT_COUNT
);
}
public void draw() {
GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, 6*2*3);
}
private static final float[] CUBE_DATA = { ... ... };
}
public class CubeRenderer implements GLSurfaceView.Renderer {
private final Context context;
private final float[] modelViewProjectionMatrix = new float[16];
private final float[] viewProjectionMatrix = new float[16];
private final float[] projectionMatrix = new float[16];
private final float[] viewMatrix = new float[16];
Cube cube;
CubeShaderProgram cubeShaderProgram;
public CubeRenderer(Context context) {
this.context = context;
Matrix.setIdentityM(projectionMatrix,0);
Matrix.setIdentityM(viewMatrix,0);
Matrix.setIdentityM(viewProjectionMatrix,0);
Matrix.setIdentityM(modelViewProjectionMatrix,0);
}
@Override
public void onSurfaceCreated(GL10 gl10, EGLConfig eglConfig) {
GLES20.glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
cube = new Cube();
cubeShaderProgram = new CubeShaderProgram(context);
}
@Override
public void onSurfaceChanged(GL10 gl10, int width, int height) {
GLES20.glViewport(0,0,width,height);
MatrixHelper.perspectiveM(projectionMatrix, 45, (float)width/(float)height, 1f, 100f);
Matrix.setLookAtM(viewMatrix, 0,
4f, 4f, 4f,
0f, 0f, 0f,
0f, 1f, 0f);
Matrix.multiplyMM(viewProjectionMatrix,0, projectionMatrix,0, viewMatrix,0);
}
@Override
public void onDrawFrame(GL10 gl10) {
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
Matrix.multiplyMM(modelViewProjectionMatrix,0, viewProjectionMatrix,0, cube.modelMatrix,0);
cubeShaderProgram.userProgram();
cubeShaderProgram.setUniforms(modelViewProjectionMatrix);
cube.bindData(cubeShaderProgram);
cube.draw();
}
}
到此,我们复习了整个OpenGL的渲染流程 和 其中的关键知识点,如果以上代码还有疑问的同学,请重新阅读系列文章1~10或者评论私信留言。
那么,问题来了,正方体点的数据那么复杂,而且数据量占位庞大,不符合我们实际的使用逻辑啊。我们正常的使用逻辑是怎样的呢?我们正式引入索引这个概念。
一个立方体只有8个相互独立的顶点,每个顶点都会在不同的平面重复的使用,就像以上给出的顶点数据,共6个平面,每个平面2个三角形,每个三角形3个顶点数据,每组顶点数据是3个位置分量+3个颜色分量,共6*2*3*(3+3)=216个浮点数,其中有很大一部分数据是重复的。通过使用索引数组,就不用重复的顶点数据了,我们只需要重复使用那些索引值,这使得我们减少了数据的整体大小。下面我们开始改造Cube,学习怎样使用索引。
首先我们修改顶点数组,只保存每个独立的顶点数据,
private static final float[] CUBE_DATA = {
//x, y, z R, G, B
-1f, 1f, 1f, 1f, 0f, 0f, // 0 left top near
1f, 1f, 1f, 1f, 0f, 1f, // 1 right top near
-1f, -1f, 1f, 0f, 0f, 1f, // 2 left bottom near
1f, -1f, 1f, 0f, 1f, 0f, // 3 right bottom near
-1f, 1f, -1f, 0f, 1f, 0f, // 4 left top far
1f, 1f, -1f, 0f, 0f, 1f, // 5 right top far
-1f, -1f, -1f, 1f, 0f, 1f, // 6 left bottom far
1f, -1f, -1f, 1f, 0f, 0f, // 7 right bottom far
};
接下来,我们开始创建正方体的索引数组:
ByteBuffer indexArray = ByteBuffer.allocateDirect(6 * 2 * 3)
.put(new byte[]{
//front
1, 0, 2,
1, 2, 3,
//back
5, 4, 6,
5, 6, 7,
//left
4, 0, 2,
4, 2, 6,
//right
5, 1, 3,
5, 3, 7,
//top
5, 4, 0,
5, 0, 1,
//bottom
7, 6, 2,
7, 2, 3
});
这个索引数组试用偏移值指向每个顶点。比如,0指向数组中第一个顶点,且1指向第二个顶点。通过这个索引数组,我们把所有顶点分别绑定成三角形组,每个有立方体上每个面的两个三角形。通过一个索引数组,我们可以用位置指向每个顶点,而不用一遍又一遍地重复同一个顶点数据。
最后,我们还要修正draw方法,注意参数的说明:
public void draw() {
// GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, 6*2*3);
GLES20.glDrawElements(GLES20.GL_TRIANGLES, 6*2*3, GLES20.GL_UNSIGNED_BYTE, indexArray);
}
void glDrawElements( GLenum mode, GLsizei count, GLenum type, const GLvoid *indices);
其中:
mode指定绘制图元的类型,我们学过的类型有这些:GL_POINTS, GL_LINE_STRIP, GL_LINES, GL_TRIANGLE_STRIP, GL_TRIANGLE_FAN, GL_TRIANGLES。
count为绘制图元的数量乘上一个图元的顶点数:这里数量=6个面*2个三角形*3个顶点。
type为索引值的类型,只能是下列值之一:GL_UNSIGNED_BYTE, GL_UNSIGNED_SHORT, or GL_UNSIGNED_INT。
我们这里顶点数量不多,用BYTE就满足了,所以我就直接使用ByteBuffer存储索引值。在一些大项目工程中,一个模型就上达几十万个顶点数据,这时候就要注意使用SHORT或者更大的INT。
indices:指向索引存贮位置的指针。
好了,修改完毕了,就这么简单,so easy嘛o(* ̄︶ ̄*)o 此时我们运行项目,是不是和修改之前的一样?效果如下图:
2、深度测试
观察仔细的同学可能已经发现了,观察矩阵定义在(4f,4f,4f)的位置上,就是近平面的右上角往下观察正方体,但是这个颜色的混合过滤貌似有点毛病啊,这好像就是底部平面的颜色都浮现出来了。这是为啥啊?
其实这和我们画三角形的顺序有关,我们看看我们的三角形索引数组,我们画三角形的顺序如下:近平面->远平面->左平面->右平面->顶部平面->底部平面,此时先画的三角形着色,会被后画的所覆盖,导致颜色线性混合出毛病了。那么这个问题怎么解?难不成我们还要自己排列这个画图的顺序咯,太反人类了吧?
此时我们要引入深度测试。简单的说,所谓深度,就是在openGL坐标系中,像素点Z坐标距离摄像机(观察矩阵)的距离。摄像机可能放在坐标系的任何位置,那么,就不能简单的说Z数值越大或越小,就是越靠近摄像机。OpenGL中的深度测试是采用深度缓存器算法,消除场景中的不可见面。在默认情况下,深度缓存中深度值的范围在0.0到1.0之间。
深度缓冲区原理就是把一个距离观察平面(近裁剪面)的深度值(或距离)与窗口中的每个像素相关联。首先,使用glClear(GL_DEPTH_BUFFER_BIT),把所有像素的深度值设置为最大值(一般是远裁剪面)。然后,在场景中以任意次序绘制所有物体。硬件或者软件所执行的图形计算把每一个绘制表面转换为窗口上一些像素的集合,此时并不考虑是否被其他物体遮挡。然后,OpenGL会计算这些表面和观察平面的距离。如果启用了深度缓冲区,在绘制每个像素之前,OpenGL会把它的深度值和已经存储在这个像素的深度值进行比较。新像素深度值<原先像素深度值,则新像素值会取代原先的;反之,新像素值被遮挡,他颜色值和深度将被丢弃。
为了启动深度缓冲区,必须先启动它,即glEnable(GL_DEPTH_TEST)。每次绘制场景之前,需要先清除深度缓冲区,即glClear(GL_DEPTH_BUFFER_BIT),然后以任意次序绘制场景中的物体。
让我们更新CubeRenderer的代码,开启深度测试:
@Override
public void onSurfaceChanged(GL10 gl10, int width, int height) {
GLES20.glViewport(0,0,width,height);
GLES20.glEnable(GLES20.GL_DEPTH_TEST);
MatrixHelper.perspectiveM(projectionMatrix, 45, (float)width/(float)height, 1f, 100f);
Matrix.setLookAtM(viewMatrix, 0,
4f, 4f, 4f,
0f, 0f, 0f,
0f, 1f, 0f);
Matrix.multiplyMM(viewProjectionMatrix,0, projectionMatrix,0, viewMatrix,0);
}
@Override
public void onDrawFrame(GL10 gl10) {
GLES20.glClear( GLES20.GL_DEPTH_BUFFER_BIT | GLES20.GL_COLOR_BUFFER_BIT);
... ...
}
运行项目看看效果是否如下:
小结:这章内容不多,主要是复习了渲染流程,和其流程环节的知识点,毕竟这个流程是模板代码。然后我们新增加了索引和深度测试,都不难比较好理解。
项目源码:https://github.com/MrZhaozhirong/BlogApp 参考子目录cube/CubeActivity