OpenGL(6) —— 绘制一个立方体,面剔除和深度测试

2020-02-03  本文已影响0人  你可记得叫安可
旋转的彩色立方体

代码实践

定义物体的颜色和局部空间中的坐标

首先我们定义六个面的顶点坐标,每个面由两个三角形组成。

// 假设摄像机是由 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) 来剔除正面。不过并不建议这样做,因为大家都是约定俗成逆时针为正面,剔除反面。如果你特别指定规则,会给其他人造成困扰。


拓展

如果我们将深度测试面剔除组合使用会发生什么呢?下面以本文中的立方体,假设有一个面是按照顺时针来定义的,:

因此,我们应该总是按照逆时针方向定义面的顶点,并在某些情况下(比如本文的立方体)开启面剔除来提高性能。如果面剔除不能处理某些情况,比如某个面的反面也需要观察者看到,那么我们就应该关闭面剔除,打开深度测试,以保证物体被正确显示。

上一篇下一篇

猜你喜欢

热点阅读