上篇我们学习了对平面的变换,这篇我们将要学习对立体图形的变换。立体图形也可以称为3D图形,立体图形是由平面图形组合而来的,我先看下一个立方体的结构
一个立方体有8个顶点,6个面,可以用12个三角形组合而成。我们可以使用36个(或24个)顶点的数据和glDrawArrays的方式来绘制,但是这样照成了浪费,有许多顶点都是可以复用的,所以我们使用8个顶点的数据和glDrawElements的方式来绘制立方体。
首先我们定义顶点的坐标,在平面图形上我们的z坐标都是0,而在立体图形上的z坐标就需要定义为不同的值了。我们把立方体的中心视为(0,0,0)中心点,那么顶点0的坐标我们设置为(-0.5f, 0.5f, 0.5f),如果觉得比较抽象,可以拿一个盒子来参照下,我们得出8个顶点数据如下,
/**
* 立方体的8个顶点
*/
private float[] CubeCoords = 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, // 下右后顶点
};
然后我们设置索引,立方体的上面是由0,1,2,3这四个顶点组合而成的,那么这两个三角形就是:0,1,2和1,2,3,同理我们可以得到其他面的索引如下,
/**
* 索引
*/
private short[] indices = new short[]{
0, 1, 2, 1, 2, 3, // 上面
4, 5, 6, 5, 6, 7,// 下面
0, 1, 4, 1, 4, 5, // 前面
2, 3, 6, 3, 6, 7, // 后面
0, 2, 4, 2, 4, 6, // 左面
1, 3, 5, 3, 5, 7 // 右面
};
然后我们设置下各个顶点的颜色值,
/**
* 颜色
*/
private float[] colors = {
0f, 0f, 0f, 1f,
0f, 0f, 1f, 1f,
0f, 1f, 0f, 1f,
0f, 1f, 1f, 1f,
1f, 0f, 0f, 1f,
1f, 0f, 1f, 1f,
1f, 1f, 0f, 1f,
1f, 1f, 1f, 1f,
1f, 0f, 0f, 1f,
0f, 1f, 0f, 1f,
0f, 0f, 1f, 1f,
1f, 0f, 1f, 1f
};
这时候我们可以绘制出来的就是一个长方体了,需要结合缩放或者透视投影变为立方体。 但是我们只能看到一面,其他的面是无法看到的。最后我们需要结合旋转变换和观察点来观察我们的立方体,使我们能看到另外几个面。
着色器的代码如下
vertexShaderCode =
"uniform mat4 uMVPMatrix;" +
"attribute vec4 aPosition;" +
"attribute vec4 aColor;" +
"varying vec4 ourColor;" +
"void main() {" +
" gl_Position = uMVPMatrix * aPosition;" +
" ourColor = aColor;" + // 将ourColor设置为我们输入的颜色
"}";
fragmentShaderCode =
"precision mediump float;" + //预定义的全局默认精度
"varying vec4 ourColor;" +
"void main() {" +
" gl_FragColor = ourColor;" +
"}"; // 动态改变颜色
在onSurfaceChanged中使用投影矩阵
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
super.onSurfaceChanged(gl, width, height);
Matrix.setIdentityM(projectionMatrix, 0);
float ratio = (float) width / height;
// 设置透视投影矩阵,近点是3,远点是7
Matrix.frustumM(projectionMatrix, 0, -ratio, ratio, -1, 1, 3, 7);
}
最后在onDrawFrame中绘制顶点,设置颜色,并开始不停的循转
@Override
public void onDrawFrame(GL10 gl) {
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT);
int shaderProgram = OpenGLUtil.createProgram(vertexShaderCode, fragmentShaderCode);
GLES20.glUseProgram(shaderProgram);
int positionHandle = GLES20.glGetAttribLocation(shaderProgram, "aPosition");
GLES20.glEnableVertexAttribArray(positionHandle);
FloatBuffer vertexBuffer = OpenGLUtil.createFloatBuffer(CubeCoords);
GLES20.glVertexAttribPointer(positionHandle, 3, GLES20.GL_FLOAT,
false, 3 * 4, vertexBuffer);
int colorHandle = GLES20.glGetAttribLocation(shaderProgram, "aColor");
GLES20.glEnableVertexAttribArray(colorHandle);
FloatBuffer colorBuffer = OpenGLUtil.createFloatBuffer(colors);
GLES20.glVertexAttribPointer(colorHandle, 4, GLES20.GL_FLOAT,
false, 4 * 4, colorBuffer);
// 得到形状的变换矩阵的句柄
int mMVPMatrixHandle = GLES20.glGetUniformLocation(shaderProgram, "uMVPMatrix");
// 创建一个旋转矩阵
Matrix.setRotateM(rotationMatrix, 0, angle, 0, 1, 0);
// 设置观察点,当eyeZ是3时最大,是7时最小,超过这个范围时不可见
Matrix.setLookAtM(viewMatrix, 0, 0, 0, 4f,
0f, 0f, 0f,
0f, 1.0f, 0.0f);
// 计算
Matrix.multiplyMM(tempMatrix, 0, projectionMatrix, 0, viewMatrix, 0);
// 计算
Matrix.multiplyMM(vPMatrix, 0, tempMatrix, 0, rotationMatrix, 0);
// 将视图转换传递给着色器
GLES20.glUniformMatrix4fv(mMVPMatrixHandle, 1, false, vPMatrix, 0);
ShortBuffer indexBuffer = OpenGLUtil.createShortBuffer(indices);
GLES20.glDrawElements(GLES20.GL_TRIANGLES, indices.length,
GLES20.GL_UNSIGNED_SHORT, indexBuffer);
GLES20.glDisableVertexAttribArray(positionHandle);
angle += 2;
}
这时候我们就能看到一个不停旋转的立方体了。
我们用的是GL_TRIANGLES绘制的三角形,但是我们可以使用GL_TRIANGLE_STRIP来绘制面,减少索引的大小。在使用这两种方式时,需要先转为一个平面,然后再确定索引,如向上立方体转为平面图形时如下,
让我们确定开始点和结束点后确定索引的顺序:2, 3, 0, 1, 5, 3, 7, 2, 6, 0, 4, 5, 6, 7
注:立方体展开图有14种之多,可以自己展开后来确定索引。
如果以GL_TRIANGLE_FAN的方式绘制,则需要先以0顶点绘制三个面,再以7顶点绘制三个面。
总的来说以GL_TRIANGLE_STRIP方式绘制比较难理解,但是索引最少;以GL_TRIANGLE_FAN绘制需要的索引次之,也便于理解;以GL_TRIANGLES的方式绘制最好理解,但是索引量最大。