Qt Quick 3D - 自定义几何示例

演示通过 C++ 和 QML 提供自定义顶点数据。

此示例利用QQuick3DGeometrythe geometry property of Model 渲染网格,网格的顶点、法线和纹理坐标由 C++ 和 QML 指定,而非预制资产。

此外,还演示了GridGeometryGridGeometry 是一个内置的QQuick3DGeometry 实现,它提供了一个具有适合显示网格的线条基元的网格。

本示例的重点是提供自定义几何体的代码,因此让我们先看看ExampleTriangleGeometry C++ 头文件:

class ExampleTriangleGeometry : public QQuick3DGeometry
{
    Q_OBJECT
    QML_NAMED_ELEMENT(ExampleTriangleGeometry)
    Q_PROPERTY(bool normals READ normals WRITE setNormals NOTIFY normalsChanged)
    Q_PROPERTY(float normalXY READ normalXY WRITE setNormalXY NOTIFY normalXYChanged)
    Q_PROPERTY(bool uv READ uv WRITE setUV NOTIFY uvChanged)
    Q_PROPERTY(float uvAdjust READ uvAdjust WRITE setUVAdjust NOTIFY uvAdjustChanged)

public:
    ExampleTriangleGeometry();

    bool normals() const { return m_hasNormals; }
    void setNormals(bool enable);

    float normalXY() const { return m_normalXY; }
    void setNormalXY(float xy);

    bool uv() const { return m_hasUV; }
    void setUV(bool enable);

    float uvAdjust() const { return m_uvAdjust; }
    void setUVAdjust(float f);

signals:
    void normalsChanged();
    void normalXYChanged();
    void uvChanged();
    void uvAdjustChanged();

private:
    void updateData();

    bool m_hasNormals = false;
    float m_normalXY = 0.0f;
    bool m_hasUV = false;
    float m_uvAdjust = 0.0f;
};

最重要的一点是,我们的ExampleTriangleGeometry 类继承自QQuick3DGeometry ,我们调用了QML_NAMED_ELEMENT(ExampleTriangleGeometry) 宏,使我们的类可以在 QML 中访问。通过Q_PROPERTY 宏还定义了一些属性,这些属性会自动暴露在我们的 QML 对象中。现在,让我们看看 QML 模型:

Model {
    id: triangleModel
    visible: false
    scale: Qt.vector3d(100, 100, 100)
    geometry: ExampleTriangleGeometry {
        normals: cbNorm.checked
        normalXY: sliderNorm.value
        uv: cbUV.checked
        uvAdjust: sliderUV.value
    }
    materials: [
        PrincipledMaterial {
            Texture {
                id: baseColorMap
                source: "qt_logo_rect.png"
            }
            cullMode: PrincipledMaterial.NoCulling
            baseColorMap: cbTexture.checked ? baseColorMap : null
            specularAmount: 0.5
        }
    ]
}

请注意,我们指定了geometry 属性来使用我们的ExampleTriangleGeometry 类,并指定了相关属性。这就是 QML 使用自定义几何体所需的全部内容。

现在,让我们看看 C++ 代码的另一个重要部分,即updateData() 方法。每当创建ExampleTriangleGeometry 类或更新其任何 QML 属性时,该方法都会为我们的自定义几何体创建和上传数据。

void ExampleTriangleGeometry::updateData()
{
    clear();

    int stride = 3 * sizeof(float);
    if (m_hasNormals)
        stride += 3 * sizeof(float);
    if (m_hasUV)
        stride += 2 * sizeof(float);

    QByteArray vertexData(3 * stride, Qt::Initialization::Uninitialized);
    float *p = reinterpret_cast<float *>(vertexData.data());

    // a triangle, front face = counter-clockwise
    *p++ = -1.0f; *p++ = -1.0f; *p++ = 0.0f;
    if (m_hasNormals) {
        *p++ = m_normalXY; *p++ = m_normalXY; *p++ = 1.0f;
    }
    if (m_hasUV) {
        *p++ = 0.0f + m_uvAdjust; *p++ = 0.0f + m_uvAdjust;
    }
    *p++ = 1.0f; *p++ = -1.0f; *p++ = 0.0f;
    if (m_hasNormals) {
        *p++ = m_normalXY; *p++ = m_normalXY; *p++ = 1.0f;
    }
    if (m_hasUV) {
        *p++ = 1.0f - m_uvAdjust; *p++ = 0.0f + m_uvAdjust;
    }
    *p++ = 0.0f; *p++ = 1.0f; *p++ = 0.0f;
    if (m_hasNormals) {
        *p++ = m_normalXY; *p++ = m_normalXY; *p++ = 1.0f;
    }
    if (m_hasUV) {
        *p++ = 1.0f - m_uvAdjust; *p++ = 1.0f - m_uvAdjust;
    }

    setVertexData(vertexData);
    setStride(stride);
    setBounds(QVector3D(-1.0f, -1.0f, 0.0f), QVector3D(+1.0f, +1.0f, 0.0f));

    setPrimitiveType(QQuick3DGeometry::PrimitiveType::Triangles);

    addAttribute(QQuick3DGeometry::Attribute::PositionSemantic,
                 0,
                 QQuick3DGeometry::Attribute::F32Type);

    if (m_hasNormals) {
        addAttribute(QQuick3DGeometry::Attribute::NormalSemantic,
                     3 * sizeof(float),
                     QQuick3DGeometry::Attribute::F32Type);
    }

    if (m_hasUV) {
        addAttribute(QQuick3DGeometry::Attribute::TexCoordSemantic,
                     m_hasNormals ? 6 * sizeof(float) : 3 * sizeof(float),
                     QQuick3DGeometry::Attribute::F32Type);
    }
}

该方法首先调用clear() 清除所有先前上传的数据。然后,它会计算顶点的跨距,同时考虑到法线和 uv 坐标的存在。然后创建一个字节数组来保存顶点缓冲区,并在缓冲区中填充一个三角形的顶点,该三角形的角分别为 (-1, -1, 0)、(1, -1, 0) 和 (0, 1, 0)。

然后上传顶点数据,并通过调用setVertexData()setStride() 设置跨距。几何体的边界通过调用setBounds 进行设置。虽然在本示例中没有使用,但设置边界是阴影工作所必需的。然后,通过调用setPrimitiveType() 设置基元类型。最后,我们通过为每个属性调用addAttribute() 来指定位置、法线和 UV 坐标等属性在内存中如何排列在之前上传的缓冲区中。

示例项目 @ code.qt.io

© 2025 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.