Revision control
Copy as Markdown
// Copyright (C) 2025 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
// Qt-Security score:significant reason:default
// Based on:
// https://behreajj.medium.com/making-a-capsule-mesh-via-script-in-five-3d-environments-c2214abf02db
#include "capsulegeometry_p.h"
#if QT_CONFIG(concurrent)
#include <QtConcurrentRun>
#endif
#include <QQuaternion>
#include <cmath>
QT_BEGIN_NAMESPACE
/*!
\qmltype CapsuleGeometry
\inqmlmodule QtQuick3D.Helpers
\inherits Geometry
\since 6.10
\brief Provides geometry for a capsule.
A geometry for generating a capsule model. The capsule is centered
at \c{(0, 0, 0)} with the height of the capsule extending in the x
direction and the diameter on the yz plane.
*/
/*! \qmlproperty bool CapsuleGeometry::enableNormals
\default true
Generate mesh face normals.
*/
/*! \qmlproperty bool CapsuleGeometry::enableUV
\default false
Generate mesh uv coordinates.
*/
/*! \qmlproperty int CapsuleGeometry::longitudes
\default 32
Number of longitudes, or meridians, distributed by azimuth.
*/
/*! \qmlproperty int CapsuleGeometry::latitudes
\default 16
Number of latitudes, distributed by inclination. Will always be snapped to an even number.
*/
/*! \qmlproperty int CapsuleGeometry::rings
\default 1
Number of sections in cylinder between hemispheres.
*/
/*! \qmlproperty real CapsuleGeometry::height
\default 100
Height of the middle cylinder on the x axis, excluding the hemispheres.
*/
/*! \qmlproperty real CapsuleGeometry::diameter
\default 100
Diameter on the yz plane.
*/
/*!
\qmlproperty UVProfile CapsuleGeometry::uvProfile
\default CapsuleGeometry.Fixed
Manner in which UV coordinates are distributed along the length of the capsule.
\value CapsuleGeometry.Fixed The upper third of the UV texture is the North
hemisphere, the middle third is the cylinder and the last third is the
South hemisphere.
\value CapsuleGeometry.Aspect UVs match the height to radius ratio.
\value CapsuleGeometry.Uniform Uniform proportion for all UV cells,
according to the ratio of latitudes to rings.
*/
/*!
\qmlproperty bool CapsuleGeometry::asynchronous
\default true
This property holds whether the geometry generation should be asynchronous.
*/
/*!
\qmlproperty bool CapsuleGeometry::status
\readonly
This property holds the status of the geometry generation when asynchronous is true.
\value CapsuleGeometry.Null The geometry generation has not started
\value CapsuleGeometry.Ready The geometry generation is complete.
\value CapsuleGeometry.Loading The geometry generation is in progress.
\value CapsuleGeometry.Error The geometry generation failed.
*/
CapsuleGeometry::CapsuleGeometry(QQuick3DObject *parent) : QQuick3DGeometry(parent)
{
#if QT_CONFIG(concurrent)
connect(&m_geometryDataWatcher, &QFutureWatcher<GeometryData>::finished, this, &CapsuleGeometry::requestFinished);
#endif
scheduleGeometryUpdate();
}
CapsuleGeometry::~CapsuleGeometry() = default;
void CapsuleGeometry::setEnableNormals(bool enable)
{
if (m_enableNormals == enable)
return;
m_enableNormals = enable;
emit enableNormalsChanged();
scheduleGeometryUpdate();
}
void CapsuleGeometry::setEnableUV(bool enable)
{
if (m_enableUV == enable)
return;
m_enableUV = enable;
emit enableUVChanged();
scheduleGeometryUpdate();
}
void CapsuleGeometry::setLongitudes(int longitudes)
{
if (m_longitudes == longitudes)
return;
m_longitudes = longitudes;
emit longitudesChanged();
scheduleGeometryUpdate();
}
void CapsuleGeometry::setLatitudes(int latitudes)
{
if (m_latitudes == latitudes)
return;
m_latitudes = latitudes;
emit latitudesChanged();
scheduleGeometryUpdate();
}
void CapsuleGeometry::setRings(int rings)
{
if (m_rings == rings)
return;
m_rings = rings;
emit ringsChanged();
scheduleGeometryUpdate();
}
void CapsuleGeometry::setHeight(float height)
{
if (m_height == height)
return;
m_height = height;
emit heightChanged();
scheduleGeometryUpdate();
}
void CapsuleGeometry::setDiameter(float diameter)
{
if (m_diameter == diameter)
return;
m_diameter = diameter;
emit diameterChanged();
scheduleGeometryUpdate();
}
CapsuleGeometry::UVProfile CapsuleGeometry::uvProfile() const
{
return m_uvProfile;
}
void CapsuleGeometry::setUVProfile(UVProfile newUVProfile)
{
if (m_uvProfile == newUVProfile)
return;
m_uvProfile = newUVProfile;
emit uvProfileChanged();
scheduleGeometryUpdate();
}
bool CapsuleGeometry::asynchronous() const
{
return m_asynchronous;
}
void CapsuleGeometry::setAsynchronous(bool newAsynchronous)
{
if (m_asynchronous == newAsynchronous)
return;
m_asynchronous = newAsynchronous;
emit asynchronousChanged();
}
CapsuleGeometry::Status CapsuleGeometry::status() const
{
return m_status;
}
void CapsuleGeometry::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
#if QT_CONFIG(concurrent)
if (m_asynchronous) {
m_geometryDataFuture = QtConcurrent::run(generateCapsuleGeometryAsync,
m_enableNormals,
m_enableUV,
m_longitudes,
m_latitudes,
m_rings,
m_height,
m_diameter,
m_uvProfile);
m_geometryDataWatcher.setFuture(m_geometryDataFuture);
m_status = Status::Loading;
Q_EMIT statusChanged();
} else {
#else
{
#endif // QT_CONFIG(concurrent)
updateGeometry(generateCapsuleGeometry(m_enableNormals, m_enableUV, m_longitudes, m_latitudes, m_rings, m_height, m_diameter, m_uvProfile));
}
}
void CapsuleGeometry::requestFinished()
{
#if QT_CONFIG(concurrent)
const auto output = m_geometryDataFuture.takeResult();
updateGeometry(output);
#endif
}
void CapsuleGeometry::scheduleGeometryUpdate()
{
if (!m_geometryUpdateRequested) {
QMetaObject::invokeMethod(this, "doUpdateGeometry", Qt::QueuedConnection);
m_geometryUpdateRequested = true;
}
}
void CapsuleGeometry::updateGeometry(const GeometryData &geometryData)
{
clear();
setStride(geometryData.stride);
setPrimitiveType(QQuick3DGeometry::PrimitiveType::Triangles);
addAttribute(QQuick3DGeometry::Attribute::PositionSemantic, 0, QQuick3DGeometry::Attribute::ComponentType::F32Type);
if (geometryData.enableNormals) {
addAttribute(QQuick3DGeometry::Attribute::NormalSemantic,
geometryData.strideNormal,
QQuick3DGeometry::Attribute::ComponentType::F32Type);
}
if (geometryData.enableUV) {
addAttribute(QQuick3DGeometry::Attribute::TexCoordSemantic, geometryData.strideUV, QQuick3DGeometry::Attribute::ComponentType::F32Type);
}
addAttribute(QQuick3DGeometry::Attribute::IndexSemantic, 0, QQuick3DGeometry::Attribute::ComponentType::U32Type);
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();
emit geometryChanged();
}
CapsuleGeometry::GeometryData CapsuleGeometry::generateCapsuleGeometry(bool enableNormals,
bool enableUV,
int longitudes,
int latitudes,
int rings,
float height,
float diameter,
UVProfile uvProfile)
{
longitudes = qMax(3, longitudes);
latitudes = qMax(2, latitudes + (latitudes % 2)); // make even
rings = qMax(1, rings);
height = qMax(0.00001f, height);
diameter = qMax(0.00001f, diameter);
const UVProfile profile = uvProfile;
const float radius = diameter / 2;
const float depth = height;
bool calcMiddle = rings > 0;
int halfLats = latitudes / 2;
int halfLatsn1 = halfLats - 1;
int halfLatsn2 = halfLats - 2;
int ringsp1 = rings + 1;
int lonsp1 = longitudes + 1;
float halfDepth = depth * 0.5f;
float summit = halfDepth + radius;
// Vertex index offsets.
int vertOffsetNorthHemi = longitudes;
int vertOffsetNorthEquator = vertOffsetNorthHemi + lonsp1 * halfLatsn1;
int vertOffsetCylinder = vertOffsetNorthEquator + lonsp1;
int vertOffsetSouthEquator = calcMiddle ?
vertOffsetCylinder + lonsp1 * rings :
vertOffsetCylinder;
int vertOffsetSouthHemi = vertOffsetSouthEquator + lonsp1;
int vertOffsetSouthPolar = vertOffsetSouthHemi + lonsp1 * halfLatsn2;
int vertOffsetSouthCap = vertOffsetSouthPolar + lonsp1;
// Initialize arrays.
int vertLen = vertOffsetSouthCap + longitudes;
QList<QVector3D> vs = QList<QVector3D>(vertLen);
QList<QVector2D> vts = QList<QVector2D>(vertLen);
QList<QVector3D> vns = QList<QVector3D>(vertLen);
float toTheta = 2.0f * M_PI / longitudes;
float toPhi = M_PI / latitudes;
float toTexHorizontal = 1.0f / longitudes;
float toTexVertical = 1.0f / halfLats;
// Calculate positions for texture coordinates vertical.
float vtAspectRatio = 1.0f;
switch (profile)
{
case UVProfile::Aspect:
vtAspectRatio = radius / (depth + radius + radius);
break;
case UVProfile::Uniform:
vtAspectRatio = (float) halfLats / (ringsp1 + latitudes);
break;
case UVProfile::Fixed:
default:
vtAspectRatio = 1.0f / 3.0f;
break;
}
float vtAspectNorth = 1.0f - vtAspectRatio;
float vtAspectSouth = vtAspectRatio;
QList<QVector2D> thetaCartesian = QList<QVector2D>(longitudes);
QList<QVector2D> rhoThetaCartesian = QList<QVector2D>(longitudes);
QList<float> sTextureCache = QList<float>(lonsp1);
// Polar vertices.
for (int j = 0; j < longitudes; ++j)
{
float jf = j;
float sTexturePolar = 1.0f - ((jf + 0.5f) * toTexHorizontal);
float theta = jf * toTheta;
float cosTheta = std::cos(theta);
float sinTheta = std::sin(theta);
thetaCartesian[j] = QVector2D(cosTheta, sinTheta);
rhoThetaCartesian[j] = QVector2D(
radius * cosTheta,
radius * sinTheta);
// North.
vs[j] = QVector3D(0.0f, summit, 0.0f);
vts[j] = QVector2D(sTexturePolar, 1.0f);
vns[j] = QVector3D(0.0f, 1.0f, 0.0f);
// South.
int idx = vertOffsetSouthCap + j;
vs[idx] = QVector3D(0.0f, -summit, 0.0f);
vts[idx] = QVector2D(sTexturePolar, 0.0f);
vns[idx] = QVector3D(0.0f, -1.0f, 0.0f);
}
// Equatorial vertices.
for (int j = 0; j < lonsp1; ++j)
{
float sTexture = 1.0f - j * toTexHorizontal;
sTextureCache[j] = sTexture;
// Wrap to first element upon reaching last.
int jMod = j % longitudes;
QVector2D tc = thetaCartesian[jMod];
QVector2D rtc = rhoThetaCartesian[jMod];
// North equator.
int idxn = vertOffsetNorthEquator + j;
vs[idxn] = QVector3D(rtc.x(), halfDepth, -rtc.y());
vts[idxn] = QVector2D(sTexture, vtAspectNorth);
vns[idxn] = QVector3D(tc.x(), 0.0f, -tc.y());
// South equator.
int idxs = vertOffsetSouthEquator + j;
vs[idxs] = QVector3D(rtc.x(), -halfDepth, -rtc.y());
vts[idxs] = QVector2D(sTexture, vtAspectSouth);
vns[idxs] = QVector3D(tc.x(), 0.0f, -tc.y());
}
// Hemisphere vertices.
for (int i = 0; i < halfLatsn1; ++i)
{
float ip1f = i + 1.0f;
float phi = ip1f * toPhi;
// For coordinates.
float cosPhiSouth = std::cos(phi);
float sinPhiSouth = std::sin(phi);
// Symmetrical hemispheres mean cosine and sine only needs
// to be calculated once.
float cosPhiNorth = sinPhiSouth;
float sinPhiNorth = -cosPhiSouth;
float rhoCosPhiNorth = radius * cosPhiNorth;
float rhoSinPhiNorth = radius * sinPhiNorth;
float zOffsetNorth = halfDepth - rhoSinPhiNorth;
float rhoCosPhiSouth = radius * cosPhiSouth;
float rhoSinPhiSouth = radius * sinPhiSouth;
float zOffsetSouth = -halfDepth - rhoSinPhiSouth;
// For texture coordinates.
float tTexFac = ip1f * toTexVertical;
float cmplTexFac = 1.0f - tTexFac;
float tTexNorth = cmplTexFac + vtAspectNorth * tTexFac;
float tTexSouth = cmplTexFac * vtAspectSouth;
int iLonsp1 = i * lonsp1;
int vertCurrLatNorth = vertOffsetNorthHemi + iLonsp1;
int vertCurrLatSouth = vertOffsetSouthHemi + iLonsp1;
for (int j = 0; j < lonsp1; ++j)
{
int jMod = j % longitudes;
float sTexture = sTextureCache[j];
QVector2D tc = thetaCartesian[jMod];
// North hemisphere.
int idxn = vertCurrLatNorth + j;
vs[idxn] = QVector3D(
rhoCosPhiNorth * tc.x(),
zOffsetNorth,
-rhoCosPhiNorth * tc.y());
vts[idxn] = QVector2D(sTexture, tTexNorth);
vns[idxn] = QVector3D(
cosPhiNorth * tc.x(),
-sinPhiNorth,
-cosPhiNorth * tc.y());
// South hemisphere.
int idxs = vertCurrLatSouth + j;
vs[idxs] = QVector3D(
rhoCosPhiSouth * tc.x(),
zOffsetSouth,
-rhoCosPhiSouth * tc.y());
vts[idxs] = QVector2D(sTexture, tTexSouth);
vns[idxs] = QVector3D(
cosPhiSouth * tc.x(),
-sinPhiSouth,
-cosPhiSouth * tc.y());
}
}
// Cylinder vertices.
if (calcMiddle)
{
// Exclude both origin and destination edges
// (North and South equators) from the interpolation.
float toFac = 1.0f / ringsp1;
int idxCylLat = vertOffsetCylinder;
for (int h = 1; h < ringsp1; ++h)
{
float fac = h * toFac;
float cmplFac = 1.0f - fac;
float tTexture = cmplFac * vtAspectNorth + fac * vtAspectSouth;
float z = halfDepth - depth * fac;
for (int j = 0; j < lonsp1; ++j)
{
int jMod = j % longitudes;
QVector2D tc = thetaCartesian[jMod];
QVector2D rtc = rhoThetaCartesian[jMod];
float sTexture = sTextureCache[j];
vs[idxCylLat] = QVector3D(rtc.x(), z, -rtc.y());
vts[idxCylLat] = QVector2D(sTexture, tTexture);
vns[idxCylLat] = QVector3D(tc.x(), 0.0f, -tc.y());
++idxCylLat;
}
}
}
// Triangle indices.
// Stride is 3 for polar triangles;
// stride is 6 for two triangles forming a quad.
int lons3 = longitudes * 3;
int lons6 = longitudes * 6;
int hemiLons = halfLatsn1 * lons6;
int triOffsetNorthHemi = lons3;
int triOffsetCylinder = triOffsetNorthHemi + hemiLons;
int triOffsetSouthHemi = triOffsetCylinder + ringsp1 * lons6;
int triOffsetSouthCap = triOffsetSouthHemi + hemiLons;
int fsLen = triOffsetSouthCap + lons3;
QList<int> tris = QList<int>(fsLen);
// Polar caps.
for (int i = 0, k = 0, m = triOffsetSouthCap; i < longitudes; ++i, k += 3, m += 3)
{
// North.
tris[k] = i;
tris[k + 1] = vertOffsetNorthHemi + i;
tris[k + 2] = vertOffsetNorthHemi + i + 1;
// South.
tris[m] = vertOffsetSouthCap + i;
tris[m + 1] = vertOffsetSouthPolar + i + 1;
tris[m + 2] = vertOffsetSouthPolar + i;
}
// Hemispheres.
for (int i = 0, k = triOffsetNorthHemi, m = triOffsetSouthHemi; i < halfLatsn1; ++i)
{
int iLonsp1 = i * lonsp1;
int vertCurrLatNorth = vertOffsetNorthHemi + iLonsp1;
int vertNextLatNorth = vertCurrLatNorth + lonsp1;
int vertCurrLatSouth = vertOffsetSouthEquator + iLonsp1;
int vertNextLatSouth = vertCurrLatSouth + lonsp1;
for (int j = 0; j < longitudes; ++j, k += 6, m += 6)
{
// North.
int north00 = vertCurrLatNorth + j;
int north01 = vertNextLatNorth + j;
int north11 = vertNextLatNorth + j + 1;
int north10 = vertCurrLatNorth + j + 1;
tris[k] = north00;
tris[k + 1] = north11;
tris[k + 2] = north10;
tris[k + 3] = north00;
tris[k + 4] = north01;
tris[k + 5] = north11;
// South.
int south00 = vertCurrLatSouth + j;
int south01 = vertNextLatSouth + j;
int south11 = vertNextLatSouth + j + 1;
int south10 = vertCurrLatSouth + j + 1;
tris[m] = south00;
tris[m + 1] = south11;
tris[m + 2] = south10;
tris[m + 3] = south00;
tris[m + 4] = south01;
tris[m + 5] = south11;
}
}
// Cylinder.
for (int i = 0, k = triOffsetCylinder; i < ringsp1; ++i)
{
int vertCurrLat = vertOffsetNorthEquator + i * lonsp1;
int vertNextLat = vertCurrLat + lonsp1;
for (int j = 0; j < longitudes; ++j, k += 6)
{
int cy00 = vertCurrLat + j;
int cy01 = vertNextLat + j;
int cy11 = vertNextLat + j + 1;
int cy10 = vertCurrLat + j + 1;
tris[k] = cy00;
tris[k + 1] = cy11;
tris[k + 2] = cy10;
tris[k + 3] = cy00;
tris[k + 4] = cy01;
tris[k + 5] = cy11;
}
}
auto vertices = vs;
auto vertexTextures = vts;
auto vertexNormals = vns;
uint32_t stride = 3 * sizeof(float);
uint32_t strideNormal = 0;
uint32_t strideUV = 0;
if (enableNormals) {
strideNormal = stride;
stride += 3 * sizeof(float);
}
if (enableUV) {
strideUV = stride;
stride += 2 * sizeof(float);
}
QByteArray vertexData(vertices.length() * stride, Qt::Initialization::Uninitialized);
QByteArray indexData(tris.length() * sizeof(int), Qt::Initialization::Uninitialized);
const auto getVertexPtr = [&](const int vertexIdx) {
return reinterpret_cast<QVector3D *>(vertexData.data() + stride * vertexIdx);
};
const auto getNormalPtr = [&](const int vertexIdx) {
return reinterpret_cast<QVector3D *>(vertexData.data() + stride * vertexIdx + strideNormal);
};
const auto getTexturePtr = [&](const int vertexIdx) {
return reinterpret_cast<QVector2D *>(vertexData.data() + stride * vertexIdx + strideUV);
};
const QQuaternion rotateZ = QQuaternion::fromEulerAngles(0, 0, 90);
uint32_t *indexPtr = reinterpret_cast<uint32_t *>(indexData.data());
for (qsizetype i = 0; i < vertices.length(); i++) {
*getVertexPtr(i) = rotateZ * vertices[i];
}
for (qsizetype i = 0; i < tris.length() / 3; i++) {
std::array<int, 3> triIndices = { tris[i * 3], tris[i * 3 + 1], tris[i * 3 + 2] };
*indexPtr = triIndices[0];
indexPtr++;
*indexPtr = triIndices[1];
indexPtr++;
*indexPtr = triIndices[2];
indexPtr++;
if (enableNormals) {
*getNormalPtr(triIndices[0]) = rotateZ * vertexNormals[triIndices[0]];
*getNormalPtr(triIndices[1]) = rotateZ * vertexNormals[triIndices[1]];
*getNormalPtr(triIndices[2]) = rotateZ * vertexNormals[triIndices[2]];
}
if (enableUV) {
*getTexturePtr(triIndices[0]) = vertexTextures[triIndices[0]];
*getTexturePtr(triIndices[1]) = vertexTextures[triIndices[1]];
*getTexturePtr(triIndices[2]) = vertexTextures[triIndices[2]];
}
}
GeometryData geometryData;
geometryData.indexData = indexData;
geometryData.vertexData = vertexData;
geometryData.boundsMin = QVector3D(-radius - halfDepth, -radius, -radius);
geometryData.boundsMax = QVector3D(radius + halfDepth, radius, radius);
geometryData.stride = stride;
geometryData.strideNormal = strideNormal;
geometryData.strideUV = strideUV;
geometryData.enableNormals = enableNormals;
geometryData.enableUV = enableUV;
return geometryData;
}
#if QT_CONFIG(concurrent)
void CapsuleGeometry::generateCapsuleGeometryAsync(QPromise<CapsuleGeometry::GeometryData> &promise,
bool enableNormals,
bool enableUV,
int longitudes,
int latitudes,
int rings,
float height,
float diameter,
CapsuleGeometry::UVProfile uvProfile)
{
auto output = generateCapsuleGeometry(enableNormals, enableUV, longitudes, latitudes, rings, height, diameter, uvProfile);
promise.addResult(output);
}
#endif
QT_END_NAMESPACE