Cube OpenGL ES 2.0 示例

展示如何手动旋转带有用户输入的纹理化 3D 立方体。

Cube OpenGL ES 2.0 示例展示了如何使用 OpenGL ES 2.0 和 Qt 手动旋转带有用户输入的纹理化 3D 立方体。它展示了如何高效地处理多边形几何形状,以及如何为可编程图形管线编写简单的顶点和片段着色器。此外,它还展示了如何使用四元数表示 3D 对象的方向。

此示例是为 OpenGL ES 2.0 编写的,但它也适用于桌面 OpenGL,因为此示例非常简单,而且桌面 OpenGL API 在大多数情况下是相同的。即使没有 OpenGL 支持,它也可以编译,但此时它只会显示一个标签,说明需要 OpenGL 支持。

Screenshot of the Cube example running on N900

该示例包含两个类

  • MainWidget 扩展 QOpenGLWidget 并包含 OpenGL ES 2.0 的初始化、绘图以及鼠标和定时事件处理
  • GeometryEngine 处理多边形几何形状。将多边形几何形状传输到顶点缓冲对象,并从顶点缓冲对象中绘制几何形状。

我们将从初始化 MainWidget 中的 OpenGL ES 2.0 开始。

初始化 OpenGL ES 2.0

由于 OpenGL ES 2.0 已不再支持固定图形管线,因此必须由我们自己实现。这使得图形管线非常灵活,但在同时它也变得更加困难,因为用户必须实现图形管线才能运行甚至最简单的示例。它也使图形管线更加高效,因为用户可以决定应用程序需要哪种类型的管线。

首先,我们必须实现顶点着色器。它接受顶点数据和模型视图投影矩阵 (MVP) 作为参数。它使用 MVP 矩阵将顶点位置变换到屏幕空间,并将纹理坐标传递到片段着色器。纹理坐标将在多边形面上自动插值。

void main()
{
    // Calculate vertex position in screen space
    gl_Position = mvp_matrix * a_position;

    // Pass texture coordinate to fragment shader
    // Value will be automatically interpolated to fragments inside polygon faces
    v_texcoord = a_texcoord;
}

之后,我们需要实现图形管线的第二部分 - 片段着色器。对于这项练习,我们需要实现一个处理纹理的片段着色器。它接受插值后的纹理坐标作为参数,并从给定的纹理中查找片段颜色。

void main()
{
    // Set fragment color from texture
    gl_FragColor = texture2D(texture, v_texcoord);
}

使用 QOpenGLShaderProgram,我们可以编译、链接并将着色器代码绑定到图形管线。此代码使用 Qt 资源文件来访问着色器源代码。

void MainWidget::initShaders()
{
    // Compile vertex shader
    if (!program.addShaderFromSourceFile(QOpenGLShader::Vertex, ":/vshader.glsl"))
        close();

    // Compile fragment shader
    if (!program.addShaderFromSourceFile(QOpenGLShader::Fragment, ":/fshader.glsl"))
        close();

    // Link shader pipeline
    if (!program.link())
        close();

    // Bind shader pipeline for use
    if (!program.bind())
        close();
}

以下代码启用了深度缓存和背面消隐。

    // Enable depth buffer
    glEnable(GL_DEPTH_TEST);

    // Enable back face culling
    glEnable(GL_CULL_FACE);

从 Qt 资源文件加载纹理

QOpenGLWidget 接口实现了从 QImage 到 OpenGL 纹理内存加载纹理的方法。我们仍然需要使用 OpenGL 提供的函数来指定 OpenGL 纹理单元并配置纹理过滤选项。

void MainWidget::initTextures()
{
    // Load cube.png image
    texture = new QOpenGLTexture(QImage(":/cube.png").mirrored());

    // Set nearest filtering mode for texture minification
    texture->setMinificationFilter(QOpenGLTexture::Nearest);

    // Set bilinear filtering mode for texture magnification
    texture->setMagnificationFilter(QOpenGLTexture::Linear);

    // Wrap texture coordinates by repeating
    // f.ex. texture coordinate (1.1, 1.2) is same as (0.1, 0.2)
    texture->setWrapMode(QOpenGLTexture::Repeat);
}

立方体几何形状

OpenGL中有许多渲染多边形的方法,但最有效的方法是仅使用三角形带原语,并从图形硬件内存中渲染顶点。OpenGL具有创建缓冲对象以访问此内存区域并将顶点数据传输到这些缓冲区的一种机制。在OpenGL术语中,这些称为顶点缓冲对象(VBO)。

Cube faces and vertices

立方体面分解成三角形的这种方式。顶点的顺序是这样排列的,以确保使用三角形带正确地获得顶点顺序。OpenGL根据顶点顺序确定三角形的前后表面。默认情况下,OpenGL使用逆时针顺序作为正面。这些信息用于背面裁剪,这通过不渲染三角形的背面来提高渲染性能。这样,图形管线可以省略面向屏幕的三角形侧面。

使用QOpenGLBuffer可以很容易地创建顶点缓冲对象并将数据传输到它们。MainWidget确保GeometryEngine实例在OpenGL上下文当前时创建和销毁。这样我们可以在构造函数中使用OpenGL资源,并在析构函数中进行适当的清理。

GeometryEngine::GeometryEngine()
    : indexBuf(QOpenGLBuffer::IndexBuffer)
{
    initializeOpenGLFunctions();

    // Generate 2 VBOs
    arrayBuf.create();
    indexBuf.create();

    // Initializes cube geometry and transfers it to VBOs
    initCubeGeometry();
}

GeometryEngine::~GeometryEngine()
{
    arrayBuf.destroy();
    indexBuf.destroy();
}
    // Transfer vertex data to VBO 0
    arrayBuf.bind();
    arrayBuf.allocate(vertices, 24 * sizeof(VertexData));

    // Transfer index data to VBO 1
    indexBuf.bind();
    indexBuf.allocate(indices, 34 * sizeof(GLushort));

从VBO中绘制原语并通知可编程图形管线如何定位顶点数据需要几个步骤。首先我们需要绑定要使用的VBO。之后,我们绑定着色程序属性名称并配置它在绑定VBO中具有的数据类型。最后,我们将使用来自其他VBO的索引绘制三角形带原语。

void GeometryEngine::drawCubeGeometry(QOpenGLShaderProgram *program)
{
    // Tell OpenGL which VBOs to use
    arrayBuf.bind();
    indexBuf.bind();

    // Offset for position
    quintptr offset = 0;

    // Tell OpenGL programmable pipeline how to locate vertex position data
    int vertexLocation = program->attributeLocation("a_position");
    program->enableAttributeArray(vertexLocation);
    program->setAttributeBuffer(vertexLocation, GL_FLOAT, offset, 3, sizeof(VertexData));

    // Offset for texture coordinate
    offset += sizeof(QVector3D);

    // Tell OpenGL programmable pipeline how to locate vertex texture coordinate data
    int texcoordLocation = program->attributeLocation("a_texcoord");
    program->enableAttributeArray(texcoordLocation);
    program->setAttributeBuffer(texcoordLocation, GL_FLOAT, offset, 2, sizeof(VertexData));

    // Draw cube geometry using indices from VBO 1
    glDrawElements(GL_TRIANGLE_STRIP, 34, GL_UNSIGNED_SHORT, nullptr);
}

透视投影

使用QMatrix4x4辅助方法,计算透视投影矩阵非常简单。此矩阵用于将顶点投影到屏幕空间。

void MainWidget::resizeGL(int w, int h)
{
    // Calculate aspect ratio
    qreal aspect = qreal(w) / qreal(h ? h : 1);

    // Set near plane to 3.0, far plane to 7.0, field of view 45 degrees
    const qreal zNear = 3.0, zFar = 7.0, fov = 45.0;

    // Reset projection
    projection.setToIdentity();

    // Set perspective projection
    projection.perspective(fov, aspect, zNear, zFar);
}

3D对象的朝向

四元数是表示3D对象朝向的一种便利方法。四元数涉及相当复杂的数学,但幸运的是,QQuaternion已实现了四元数背后的所有必要数学。这使我们能够将立方体的朝向存储在四元数中,并且旋转立方体围绕给定轴非常简单。

以下代码根据鼠标事件计算旋转轴和角速度。

void MainWidget::mousePressEvent(QMouseEvent *e)
{
    // Save mouse press position
    mousePressPosition = QVector2D(e->position());
}

void MainWidget::mouseReleaseEvent(QMouseEvent *e)
{
    // Mouse release position - mouse press position
    QVector2D diff = QVector2D(e->position()) - mousePressPosition;

    // Rotation axis is perpendicular to the mouse position difference
    // vector
    QVector3D n = QVector3D(diff.y(), diff.x(), 0.0).normalized();

    // Accelerate angular speed relative to the length of the mouse sweep
    qreal acc = diff.length() / 100.0;

    // Calculate new rotation axis as weighted sum
    rotationAxis = (rotationAxis * angularSpeed + n * acc).normalized();

    // Increase angular speed
    angularSpeed += acc;
}

QBasicTimer用于动画场景和更新立方体的朝向。通过相乘四元数可以简单地将旋转串联。

void MainWidget::timerEvent(QTimerEvent *)
{
    // Decrease angular speed (friction)
    angularSpeed *= 0.99;

    // Stop rotation when speed goes below threshold
    if (angularSpeed < 0.01) {
        angularSpeed = 0.0;
    } else {
        // Update rotation
        rotation = QQuaternion::fromAxisAndAngle(rotationAxis, angularSpeed) * rotation;

        // Request an update
        update();
    }
}

通过四元数和沿Z轴移动世界来计算模型视图矩阵。将此矩阵与投影矩阵相乘以获取用于着色程序的前鹏视图投影矩阵(MVP)。

    // Calculate model view transformation
    QMatrix4x4 matrix;
    matrix.translate(0.0, 0.0, -5.0);
    matrix.rotate(rotation);

    // Set modelview-projection matrix
    program.setUniformValue("mvp_matrix", projection * matrix);

示例项目 @ code.qt.io

© 2024 The Qt Company Ltd. 本文档的贡献是各自所有者的版权。

此文档提供的文档根据自由软件开发基金会发布的

GNU自由文档许可证版本1.3的条款提供许可。Qt和相应的标志是芬兰及/或其他国家的The Qt Company Ltd.的商标。所有其他商标均为其各自所有者的财产。