// Copyright (C) 2021 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only #include "qquick3dparticleattractor_p.h" #include "qquick3dparticlerandomizer_p.h" #include "qquick3dparticleutils_p.h" QT_BEGIN_NAMESPACE /*! \qmltype Attractor3D \inherits Affector3D \inqmlmodule QtQuick3D.Particles3D \brief Attracts particles towards a position or a shape. \since 6.2 This element attracts particles towards a position inside the 3D view. To model the gravity of a massive object whose center of gravity is far away, use \l Gravity3D. The attraction position is defined either with the \l {Node::position}{position} and \l positionVariation or with \l shape. If both are defined, \l shape is used. */ // Minimum duration in seconds const float MIN_DURATION = 0.001f; QQuick3DParticleAttractor::QQuick3DParticleAttractor(QQuick3DNode *parent) : QQuick3DParticleAffector(parent) , m_duration(-1) , m_durationVariation(0) { } /*! \qmlproperty vector3d Attractor3D::positionVariation This property defines the variation on attract position. It can be used to not attract into a single point, but randomly towards a wider area. Here is an example how to attract particles into some random point inside (50, 50, 50) cube at position (100, 0, 0) within 2 to 4 seconds: \qml Attractor3D { position: Qt.vector3d(100, 0, 0) positionVariation: Qt.vector3d(50, 50, 50) duration: 3000 durationVariation: 1000 } \endqml The default value is \c (0, 0, 0) (no variation). \sa {Node::position}, shape */ QVector3D QQuick3DParticleAttractor::positionVariation() const { return m_positionVariation; } void QQuick3DParticleAttractor::setPositionVariation(const QVector3D &positionVariation) { if (m_positionVariation == positionVariation) return; m_positionVariation = positionVariation; Q_EMIT positionVariationChanged(); Q_EMIT update(); } /*! \qmlproperty ParticleAbstractShape3D Attractor3D::shape This property defines a \l ParticleAbstractShape3D for particles attraction. Each particle will be attracted into a random position inside this shape. This is an alternative for defining \l {Node::position}{position} and \l positionVariation. Here is an example how to attract particles into some random point inside sphere by the end of the particles \l {ParticleEmitter3D::}{lifeSpan}: \qml Attractor3D { position: Qt.vector3d(100, 0, 0) shape: ParticleShape3D { type: ParticleShape3D.Sphere fill: true } } \endqml \sa {Node::position}, positionVariation */ QQuick3DParticleAbstractShape *QQuick3DParticleAttractor::shape() const { return m_shape; } void QQuick3DParticleAttractor::setShape(QQuick3DParticleAbstractShape *shape) { if (m_shape == shape) return; m_shape = shape; m_shapeDirty = true; Q_EMIT shapeChanged(); Q_EMIT update(); } /*! \qmlproperty int Attractor3D::duration This property defines the duration in milliseconds how long it takes for particles to reach the attaction position. When the value is -1, particle lifeSpan is used as the duration. The default value is \c -1. */ int QQuick3DParticleAttractor::duration() const { return m_duration; } void QQuick3DParticleAttractor::setDuration(int duration) { if (m_duration == duration) return; m_duration = duration; Q_EMIT durationChanged(); Q_EMIT update(); } /*! \qmlproperty int Attractor3D::durationVariation This property defines the duration variation in milliseconds. The actual duration to reach attractor is between \c duration - \c durationVariation and \c duration + \c durationVariation. The default value is \c 0 (no variation). */ int QQuick3DParticleAttractor::durationVariation() const { return m_durationVariation; } void QQuick3DParticleAttractor::setDurationVariation(int durationVariation) { if (m_durationVariation == durationVariation) return; m_durationVariation = durationVariation; Q_EMIT durationVariationChanged(); Q_EMIT update(); } /*! \qmlproperty bool Attractor3D::hideAtEnd This property defines if the particle should disappear when it reaches the attractor. The default value is \c false. */ bool QQuick3DParticleAttractor::hideAtEnd() const { return m_hideAtEnd; } /*! \qmlproperty bool Attractor3D::useCachedPositions This property defines if the attractor caches possible positions within its shape. Cached positions give less random results but are better for performance. The default value is \c true. */ bool QQuick3DParticleAttractor::useCachedPositions() const { return m_useCachedPositions; } /*! \qmlproperty int Attractor3D::positionsAmount This property defines the amount of possible positions stored within the attractor shape. By default the amount equals the particle count, but a lower amount can be used for a smaller cache. Higher amount can be used for additional randomization. */ int QQuick3DParticleAttractor::positionsAmount() const { return m_positionsAmount; } void QQuick3DParticleAttractor::setHideAtEnd(bool hideAtEnd) { if (m_hideAtEnd == hideAtEnd) return; m_hideAtEnd = hideAtEnd; Q_EMIT hideAtEndChanged(); Q_EMIT update(); } void QQuick3DParticleAttractor::setUseCachedPositions(bool useCachedPositions) { if (m_useCachedPositions == useCachedPositions) return; m_useCachedPositions = useCachedPositions; Q_EMIT useCachedPositionsChanged(); m_shapeDirty = true; } void QQuick3DParticleAttractor::setPositionsAmount(int positionsAmount) { if (m_positionsAmount == positionsAmount) return; m_positionsAmount = positionsAmount; Q_EMIT positionsAmountChanged(); m_shapeDirty = true; } void QQuick3DParticleAttractor::updateShapePositions() { m_shapePositionList.clear(); if (!system() || !m_shape) return; m_shape->m_system = system(); if (m_useCachedPositions) { // Get count of particles positions needed int pCount = 0; if (m_positionsAmount > 0) { pCount = m_positionsAmount; } else { if (!m_particles.isEmpty()) { for (auto p : m_particles) { auto pp = qobject_cast(p); pCount += pp->maxAmount(); } } else { pCount = system()->particleCount(); } } m_shapePositionList.reserve(pCount); for (int i = 0; i < pCount; i++) m_shapePositionList << m_shape->getPosition(i); } else { m_shapePositionList.clear(); m_shapePositionList.squeeze(); } m_shapeDirty = false; } void QQuick3DParticleAttractor::prepareToAffect() { if (m_shapeDirty) updateShapePositions(); m_centerPos = position(); m_particleTransform = calculateParticleTransform(parentNode(), m_systemSharedParent); } void QQuick3DParticleAttractor::affectParticle(const QQuick3DParticleData &sd, QQuick3DParticleDataCurrent *d, float time) { if (!system()) return; auto rand = system()->rand(); float duration = m_duration < 0 ? sd.lifetime : (m_duration / 1000.0f); float durationVariation = m_durationVariation == 0 ? 0.0f : (m_durationVariation / 1000.0f) - 2.0f * rand->get(sd.index, QPRand::AttractorDurationV) * (m_durationVariation / 1000.0f); duration = std::max(duration + durationVariation, MIN_DURATION); float pEnd = std::min(1.0f, std::max(0.0f, time / duration)); // TODO: Should we support easing? //pEnd = easeInOutQuad(pEnd); if (m_hideAtEnd && pEnd >= 1.0f) { d->color.a = 0; return; } float pStart = 1.0f - pEnd; QVector3D pos = m_centerPos; if (m_shape) { if (m_useCachedPositions) pos += m_shapePositionList[sd.index % m_shapePositionList.size()]; else pos += m_shape->getPosition(sd.index); } if (!m_positionVariation.isNull()) { pos.setX(pos.x() + m_positionVariation.x() - 2.0f * rand->get(sd.index, QPRand::AttractorPosVX) * m_positionVariation.x()); pos.setY(pos.y() + m_positionVariation.y() - 2.0f * rand->get(sd.index, QPRand::AttractorPosVY) * m_positionVariation.y()); pos.setZ(pos.z() + m_positionVariation.z() - 2.0f * rand->get(sd.index, QPRand::AttractorPosVZ) * m_positionVariation.z()); } d->position = (pStart * d->position) + (pEnd * m_particleTransform.map(pos)); } QT_END_NAMESPACE