OpenGL(6) —— 绘制一个立方体,面剔除和深度测试
代码实践
定义物体的颜色和局部空间中的坐标
首先我们定义六个面的顶点坐标,每个面由两个三角形组成。
// 假设摄像机是由 Z 轴上方往下看
final float cubePosition[] = {
// Front face(由两个三角形的坐标定义)
-1.0f, 1.0f, 1.0f,
-1.0f, -1.0f, 1.0f,
1.0f, 1.0f, 1.0f,
-1.0f, -1.0f, 1.0f,
1.0f, -1.0f, 1.0f,
1.0f, 1.0f, 1.0f,
// Right face
1.0f, 1.0f, 1.0f,
1.0f, -1.0f, 1.0f,
1.0f, 1.0f, -1.0f,
1.0f, -1.0f, 1.0f,
1.0f, -1.0f, -1.0f,
1.0f, 1.0f, -1.0f,
// Back face
1.0f, 1.0f, -1.0f,
1.0f, -1.0f, -1.0f,
-1.0f, 1.0f, -1.0f,
1.0f, -1.0f, -1.0f,
-1.0f, -1.0f, -1.0f,
-1.0f, 1.0f, -1.0f,
// Left face
-1.0f, 1.0f, -1.0f,
-1.0f, -1.0f, -1.0f,
-1.0f, 1.0f, 1.0f,
-1.0f, -1.0f, -1.0f,
-1.0f, -1.0f, 1.0f,
-1.0f, 1.0f, 1.0f,
// Top face
-1.0f, 1.0f, -1.0f,
-1.0f, 1.0f, 1.0f,
1.0f, 1.0f, -1.0f,
-1.0f, 1.0f, 1.0f,
1.0f, 1.0f, 1.0f,
1.0f, 1.0f, -1.0f,
// Bottom face
1.0f, -1.0f, -1.0f,
1.0f, -1.0f, 1.0f,
-1.0f, -1.0f, -1.0f,
1.0f, -1.0f, 1.0f,
-1.0f, -1.0f, 1.0f,
-1.0f, -1.0f, -1.0f,
};
然后我们再给每个顶点定义颜色:
final float[] cubeColor = {
// Front face (red)
0.0f, 1.0f, 0.0f, 1.0f, // 这个顶点的颜色与该面其他顶点颜色不一样
1.0f, 0.0f, 0.0f, 1.0f,
1.0f, 0.0f, 0.0f, 1.0f,
1.0f, 0.0f, 0.0f, 1.0f,
1.0f, 0.0f, 0.0f, 1.0f,
1.0f, 0.0f, 0.0f, 1.0f,
// Right face (green)
0.0f, 1.0f, 0.0f, 1.0f,
0.0f, 1.0f, 0.0f, 1.0f,
0.0f, 1.0f, 0.0f, 1.0f,
0.0f, 1.0f, 0.0f, 1.0f,
0.0f, 1.0f, 0.0f, 1.0f,
0.0f, 1.0f, 0.0f, 1.0f,
// Back face (blue)
0.0f, 0.0f, 1.0f, 1.0f,
0.0f, 0.0f, 1.0f, 1.0f,
0.0f, 0.0f, 1.0f, 1.0f,
0.0f, 0.0f, 1.0f, 1.0f,
0.0f, 0.0f, 1.0f, 1.0f,
0.0f, 0.0f, 1.0f, 1.0f,
// Left face (yellow)
1.0f, 1.0f, 0.0f, 1.0f,
1.0f, 1.0f, 0.0f, 1.0f,
1.0f, 1.0f, 0.0f, 1.0f,
1.0f, 1.0f, 0.0f, 1.0f,
1.0f, 1.0f, 0.0f, 1.0f,
1.0f, 1.0f, 0.0f, 1.0f,
// Top face (cyan)
0.0f, 1.0f, 1.0f, 1.0f,
0.0f, 1.0f, 1.0f, 1.0f,
0.0f, 1.0f, 1.0f, 1.0f,
0.0f, 1.0f, 1.0f, 1.0f,
0.0f, 1.0f, 1.0f, 1.0f,
0.0f, 1.0f, 1.0f, 1.0f,
// Bottom face (magenta)
1.0f, 0.0f, 1.0f, 1.0f,
1.0f, 0.0f, 1.0f, 1.0f,
1.0f, 0.0f, 1.0f, 1.0f,
1.0f, 0.0f, 1.0f, 1.0f,
1.0f, 0.0f, 1.0f, 1.0f,
1.0f, 0.0f, 1.0f, 1.0f
};
我们让 front face 其中有一个顶点颜色与该面上其他的顶点不一样,这样在光栅化后,顶点之间的片段颜色就会有个动态过度的效果(可以在动图里看到)。
定义模型矩阵
旋转效果是模型矩阵的职责,而且每一帧旋转的角度都不一样,因此只能写在 onDrawFrame()
中:
Matrix.setIdentityM(mModelMatrix, 0);
Matrix.translateM(mModelMatrix, 0, 0.0f, 0.0f, -5.0f);
Matrix.rotateM(mModelMatrix, 0, angleInDegrees, 1.0f, 1.0f, 0.0f);
上面的效果为,先将物体向 z
轴负方向移动 5 个单位,然后以 (1.0f, 1.0f, 0.0f)
为轴旋转 angleInDegrees
度。其中 angleInDegrees
在另一个线程中随时间流逝不断增加。
定义观察矩阵
在 onSurfaceCreated()
中定义观察空间中的观察矩阵
// Position the eye behind the origin
final float eyeX = 0.0f;
final float eyeY = 0.0f;
final float eyeZ = -0.5f;
// We are looking toward the distance
float lookX = 0.0f;
float lookY = 0.0f;
float lookZ = -5.0f;
// Set our up vector. This is where our head would be pointing were we holding the camera.
float upX = 0.0f;
float upY = 1.0f;
float upZ = 0.0f;
Matrix.setLookAtM(mViewMatrix, 0, eyeX, eyeY, eyeZ, lookX, lookY, lookZ, upX, upY, upZ);
由上可以看到,我们定义摄像机位置在 (0.0f, 0.0f, -0.5f)
处,观察的中心点坐标为 (0.0f, 0.0f, -5.0f)
(与模型矩阵中物体平移距离相同),右向量为 (0.0f, 1.0f, 0.0f)
。
定义投影矩阵
因为投影矩阵中会设置近平面的宽高比,因此需要在 onSurfaceChanged()
中定义投影矩阵:
final float ratio = (float) width / height;
final float left = -ratio;
final float right = ratio;
final float bottom = -1.0f;
final float top = 1.0f;
final float near = 1.0f;
final float far = 10.0f;
Matrix.frustumM(mProjectionMatrix, 0, left, right, bottom, top, near, far);
Matrix.frustumM
都是固定写法,其中摄像机距离近平面为 1,远平面为 10。
着色器代码
String vertexShader =
"uniform mat4 u_MVPMatrix; \n" // A constant representing the combined model/view/projection matrix.
+ "attribute vec4 a_Position; \n" // Per-vertex position information we will pass in.
+ "attribute vec4 a_Color; \n" // Per-vertex color information we will pass in.
+ "varying vec4 v_Color; \n" // This will be passed into the fragment shader.
+ "void main() \n" // The entry point for our vertex shader.
+ "{ \n" //
+ " v_Color = a_Color; \n" // Pass the color through to the fragment shader.
+ " gl_Position = u_MVPMatrix \n" // gl_Position is a special variable used to store the final position.
+ " * a_Position; \n" // Multiply the vertex by the matrix to get the final point in
+ "} \n"; // normalized screen coordinates.
String fragmentShader =
"precision mediump float; \n" // Set the default precision to medium. We don't need as high of a
// precision in the fragment shader.
+ "varying vec4 v_Color; \n" // This is the color from the vertex shader interpolated across the
// triangle per fragment.
+ "void main() \n" // The entry point for our fragment shader.
+ "{ \n" //
+ " gl_FragColor = v_Color; \n" // Pass the color directly through the pipeline.
+ "} \n";
代码中片段的颜色通过 varying vec4 v_Color
在顶点着色器和片段着色器之间共享。顶点着色器接受一个模型矩阵、观察矩阵、投影矩阵按顺序变换后的一个结果矩阵 uniform mat4 u_MVPMatrix
。
绘制立方体
将顶点坐标上传到 GPU 并使能,将顶点的颜色上传到 GPU 并使能,将上述的三个矩阵相乘后的结果矩阵传给顶点着色器中的 u_MVPMatrix
。最后调用 glDrawArrays()
将立方体绘制到屏幕上。
cubePositionsBuffer.position(0);
glVertexAttribPointer(mPositionHandle, POSITION_DATA_SIZE, GL_FLOAT,
false, 0, cubePositionsBuffer);
glEnableVertexAttribArray(mPositionHandle);
cubeColorsBuffer.position(0);
glVertexAttribPointer(mColorHandle, COLOR_DATA_SIZE, GL_FLOAT,
false, 0, cubeColorsBuffer);
glEnableVertexAttribArray(mColorHandle);
Matrix.multiplyMM(mMVPMatrix, 0, mViewMatrix, 0, mModelMatrix, 0);
Matrix.multiplyMM(mMVPMatrix, 0, mProjectionMatrix, 0, mMVPMatrix, 0);
glUniformMatrix4fv(mMVPMatrixHandle, 1, false, mMVPMatrix, 0);
glDrawArrays(GL_TRIANGLES, 0, 36);
深度测试
完成上面的代码后,我们可以得到下图的效果。
可以看到有些面明显是没有被显示,而是被其他面遮挡了。这主要是因为,即使这个面在前面(即离摄像机更近),但是它的绘制顺序却后于另一些面,而被另一些面所遮挡了。要解决这个问题,我们可以开启深度测试。
深度测试为屏幕上每一个像素点保存一个深度值,深度值在
(0, 1)
之间,值越小表示离观察者越近。片段着色器中的每一个片段的像素,都需要先做深度测试,如果该像素比深度缓冲中对应的深度值小,那么就更新该深度缓冲区,否则就抛弃该像素。最后绘制的结果就是,屏幕上该像素对应的点显示的就是该点对应物体上最前面的那个点。
代码实践
我们可以通过 glEnable(GL_DEPTH_TEST)
来开启深度测试,同时在每一次调用 onDrawFrame()
通过调用 glClear(GL_DEPTH_BUFFER_BIT)
来清除上一帧的深度缓存。
面剔除
对于这种立方体,位于物体背面的面,我们有时是知道它一定会前面的面所盖住的(例如本文中的立方体)。那么我们就可以告诉 OpenGL 当这些面处于物体背面时,就不在处理这些面。这就是面剔除。
OpenGL 定义了物体面的正面和反面。OpenGL 检查所有正面朝向观察者的面,渲染这些正面,丢弃所有的反面,这将为 OpenGL 节省一半的性能。
定义正面、反面
OpenGL 通过连接顺序(Winding Order)来定义正、反面。默认逆时针为正面,顺时针为反面。
所以当我们定义物体面的顶点时,我们一定要想象它是面朝观察者以逆时针方向定义顶点。这样当这个面旋转到不需要观察者看到的背面时,观察者看到的就是正面,因此 OpenGL 不会渲染它。如下图,左边的面就被剔除了:
面剔除
代码实践
我们通过 glEnable(GL_CULL_FACE)
来开启面剔除。对于本文中的立方体,我们开启面剔除并关掉深度测试一样可以达到正确的显示效果。并且少了深度测试,从而更加节省了性能和开销。因此在本文例子中,使用面剔除更加合适。
上文所提到的,连接顺序来定义正反面,只是 OpenGL 的默认规则是逆时针为正面,你也可以通过
glFrontFace(GL_CW)
改变这个规则为顺时针为正面。并且也可以通过glCullFace(GL_FRONT)
来剔除正面。不过并不建议这样做,因为大家都是约定俗成逆时针为正面,剔除反面。如果你特别指定规则,会给其他人造成困扰。
拓展
如果我们将深度测试和面剔除组合使用会发生什么呢?下面以本文中的立方体,假设有一个面是按照顺时针来定义的,:
- 开启面剔除的情况下,我们将永远也看不到这个面,即使开启了深度测试也将看不到。
- 关闭面剔除和深度测试的情况下,我们将在某些角度看不到多个面(就是本文中第二个动图),这是因为后渲染的面会覆盖了先渲染的面。
- 关闭面剔除,开启深度测试,我们将看到正常的效果,但是物体上的每个点都进行了深度测试,这也是一笔性能开销。
因此,我们应该总是按照逆时针方向定义面的顶点,并在某些情况下(比如本文的立方体)开启面剔除来提高性能。如果面剔除不能处理某些情况,比如某个面的反面也需要观察者看到,那么我们就应该关闭面剔除,打开深度测试,以保证物体被正确显示。