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

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

作者: 你可记得叫安可 | 来源:发表于2020-02-03 20:33 被阅读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) 来剔除正面。不过并不建议这样做,因为大家都是约定俗成逆时针为正面,剔除反面。如果你特别指定规则,会给其他人造成困扰。


    拓展

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

    • 开启面剔除的情况下,我们将永远也看不到这个面,即使开启了深度测试也将看不到。
    • 关闭面剔除深度测试的情况下,我们将在某些角度看不到多个面(就是本文中第二个动图),这是因为后渲染的面会覆盖了先渲染的面。
    • 关闭面剔除,开启深度测试,我们将看到正常的效果,但是物体上的每个点都进行了深度测试,这也是一笔性能开销。

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

    相关文章

      网友评论

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

          本文链接:https://www.haomeiwen.com/subject/cplcxhtx.html