// Copyright (C) 2024 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only #include "spheregeometry_p.h" #include #if QT_CONFIG(concurrent) #include #endif QT_BEGIN_NAMESPACE /*! \qmltype SphereGeometry \inqmlmodule QtQuick3D.Helpers \inherits Geometry \since 6.9 \brief Provides geometry for a sphere. SphereGeometry is a geometry type that represents a sphere. The sphere's size is defined by its radius. The topology of the sphere is defined by the number of rings and segments. */ /*! \qmlproperty real SphereGeometry::radius The radius of the sphere. The default value is 100.0. */ /*! \qmlproperty int SphereGeometry::rings The number of rings in the sphere. The default value is 16. */ /*! \qmlproperty int SphereGeometry::segments The number of segments in the sphere. The default value is 32. */ /*! \qmlproperty bool SphereGeometry::asynchronous This property holds whether the geometry generation should be asynchronous. */ /*! \qmlproperty bool SphereGeometry::status \readonly This property holds the status of the geometry generation when asynchronous is true. \value SphereGeometry.Null The geometry generation has not started \value SphereGeometry.Ready The geometry generation is complete. \value SphereGeometry.Loading The geometry generation is in progress. \value SphereGeometry.Error The geometry generation failed. */ SphereGeometry::SphereGeometry(QQuick3DObject *parent) : QQuick3DGeometry(parent) { #if QT_CONFIG(concurrent) connect(&m_geometryDataWatcher, &QFutureWatcher::finished, this, &SphereGeometry::requestFinished); #endif scheduleGeometryUpdate(); } SphereGeometry::~SphereGeometry() { } float SphereGeometry::radius() const { return m_radius; } void SphereGeometry::setRadius(float newRadius) { if (qFuzzyCompare(m_radius, newRadius)) return; m_radius = newRadius; emit radiusChanged(); scheduleGeometryUpdate(); } int SphereGeometry::rings() const { return m_rings; } void SphereGeometry::setRings(int newRings) { if (m_rings == newRings) return; m_rings = newRings; emit ringsChanged(); scheduleGeometryUpdate(); } int SphereGeometry::segments() const { return m_segments; } void SphereGeometry::setSegments(int newSegments) { if (m_segments == newSegments) return; m_segments = newSegments; emit segmentsChanged(); scheduleGeometryUpdate(); } bool SphereGeometry::asynchronous() const { return m_asynchronous; } void SphereGeometry::setAsynchronous(bool newAsynchronous) { if (m_asynchronous == newAsynchronous) return; m_asynchronous = newAsynchronous; emit asynchronousChanged(); } SphereGeometry::Status SphereGeometry::status() const { return m_status; } void SphereGeometry::doUpdateGeometry() { // reset the flag since we are processing the update m_geometryUpdateRequested = false; #if QT_CONFIG(concurrent) if (m_geometryDataFuture.isRunning()) { m_pendingAsyncUpdate = true; return; } #endif // Check validity of the geometry parameters if (m_radius < 0 || m_rings < 1 || m_segments < 3) { clear(); update(); return; } #if QT_CONFIG(concurrent) if (m_asynchronous) { m_geometryDataFuture = QtConcurrent::run(generateSphereGeometryAsync, m_radius, m_rings, m_segments); m_geometryDataWatcher.setFuture(m_geometryDataFuture); m_status = Status::Loading; Q_EMIT statusChanged(); } else { #else { #endif // QT_CONFIG(concurrent) updateGeometry(generateSphereGeometry(m_radius, m_rings, m_segments)); } } void SphereGeometry::requestFinished() { #if QT_CONFIG(concurrent) const auto output = m_geometryDataFuture.takeResult(); updateGeometry(output); #endif } void SphereGeometry::scheduleGeometryUpdate() { if (!m_geometryUpdateRequested) { QMetaObject::invokeMethod(this, "doUpdateGeometry", Qt::QueuedConnection); m_geometryUpdateRequested = true; } } void SphereGeometry::updateGeometry(const GeometryData &geometryData) { setStride(sizeof(float) * 8); // 3 for position, 2 for uv0, 3 for normal setPrimitiveType(QQuick3DGeometry::PrimitiveType::Triangles); addAttribute(QQuick3DGeometry::Attribute::PositionSemantic, 0, QQuick3DGeometry::Attribute::F32Type); addAttribute(QQuick3DGeometry::Attribute::TexCoord0Semantic, 3 * sizeof(float), QQuick3DGeometry::Attribute::F32Type); addAttribute(QQuick3DGeometry::Attribute::NormalSemantic, 5 * sizeof(float), QQuick3DGeometry::Attribute::F32Type); addAttribute(QQuick3DGeometry::Attribute::IndexSemantic, 0, QQuick3DGeometry::Attribute::U16Type); setBounds(geometryData.boundsMin, geometryData.boundsMax); setVertexData(geometryData.vertexData); setIndexData(geometryData.indexData); // If the geometry update was requested while the geometry was being generated asynchronously, // we need to schedule another geometry update now that the geometry is ready. if (m_pendingAsyncUpdate) { m_pendingAsyncUpdate = false; scheduleGeometryUpdate(); } else { m_status = Status::Ready; Q_EMIT statusChanged(); } update(); } SphereGeometry::GeometryData SphereGeometry::generateSphereGeometry(float radius, int rings, int segments) { GeometryData geometryData; // Pre-compute the size of the vertex and index data const int numVertices = (rings + 1) * (segments + 1); const int numIndices = rings * segments * 6; const int vertexStride = sizeof(float) * (3 + 2 + 3); // 3 for position, 2 for uv, 3 for normal const int indexStride = sizeof(uint16_t); // Resize the QByteArrays to fit all the vertex and index data geometryData.vertexData.resize(numVertices * vertexStride); geometryData.indexData.resize(numIndices * indexStride); // Bounds initialization using std::numeric_limits::max() and lowest() QVector3D boundsMin(std::numeric_limits::max(), std::numeric_limits::max(), std::numeric_limits::max()); QVector3D boundsMax(std::numeric_limits::lowest(), std::numeric_limits::lowest(), std::numeric_limits::lowest()); // Temporary pointers for direct writing into the QByteArray float* vertexPtr = reinterpret_cast(geometryData.vertexData.data()); uint16_t* indexPtr = reinterpret_cast(geometryData.indexData.data()); // Loop through rings and segments to generate the vertex data for (int i = 0; i <= rings; ++i) { float phi = M_PI * i / rings; // from 0 to PI float y = radius * std::cos(phi); float ringRadius = radius * std::sin(phi); for (int j = 0; j <= segments; ++j) { float theta = 2 * M_PI * j / segments; // from 0 to 2PI float x = ringRadius * std::cos(theta); float z = ringRadius * std::sin(theta); // Position (vec3) *vertexPtr++ = x; *vertexPtr++ = y; *vertexPtr++ = z; // UV coordinates (vec2) *vertexPtr++ = 1.0f - static_cast(j) / segments; *vertexPtr++ = 1.0f - static_cast(i) / rings; // Normalized normal vector (vec3) QVector3D normal(x, y, z); normal.normalize(); *vertexPtr++ = normal.x(); *vertexPtr++ = normal.y(); *vertexPtr++ = normal.z(); // Update bounds boundsMin.setX(std::min(boundsMin.x(), x)); boundsMin.setY(std::min(boundsMin.y(), y)); boundsMin.setZ(std::min(boundsMin.z(), z)); boundsMax.setX(std::max(boundsMax.x(), x)); boundsMax.setY(std::max(boundsMax.y(), y)); boundsMax.setZ(std::max(boundsMax.z(), z)); } } // Loop through rings and segments to generate the index data for (int i = 0; i < rings; ++i) { for (int j = 0; j < segments; ++j) { uint16_t a = static_cast(i * (segments + 1) + j); uint16_t b = static_cast(a + segments + 1); uint16_t c = static_cast(b + 1); uint16_t d = static_cast(a + 1); // First triangle (a, d, b) *indexPtr++ = a; *indexPtr++ = d; *indexPtr++ = b; // Second triangle (b, d, c) *indexPtr++ = b; *indexPtr++ = d; *indexPtr++ = c; } } geometryData.boundsMin = boundsMin; geometryData.boundsMax = boundsMax; // Return the geometry data return geometryData; } #if QT_CONFIG(concurrent) void SphereGeometry::generateSphereGeometryAsync(QPromise &promise, float radius, int rings, int segments) { auto output = generateSphereGeometry(radius, rings, segments); promise.addResult(output); } #endif QT_END_NAMESPACE