1// Copyright (C) 2021 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
3
4#include <QtQuick3D/private/qquick3dquaternionutils_p.h>
5#include "qquick3dparticlesystem_p.h"
6#include "qquick3dparticleemitter_p.h"
7#include "qquick3dparticletrailemitter_p.h"
8#include "qquick3dparticlemodelparticle_p.h"
9#include "qquick3dparticleaffector_p.h"
10#include <private/qqmldelegatemodel_p.h>
11#include "qquick3dparticlerandomizer_p.h"
12#include "qquick3dparticlespriteparticle_p.h"
13#include "qquick3dparticlelineparticle_p.h"
14#include "qquick3dparticlemodelblendparticle_p.h"
15#include <QtQuick3DUtils/private/qquick3dprofiler_p.h>
16#include <qtquick3d_tracepoints_p.h>
17#include <cmath>
18
19QT_BEGIN_NAMESPACE
20
21/*!
22 \qmltype ParticleSystem3D
23 \inherits Node
24 \inqmlmodule QtQuick3D.Particles3D
25 \brief A system which includes particle, emitter, and affector types.
26 \since 6.2
27
28 This element is the root of the particle system, which handles the system timing and groups all
29 the other related elements like particles, emitters, and affectors together. To group the system
30 elements, they either need to be direct children of the ParticleSystem3D like this:
31
32 \qml,
33 ParticleSystem3D {
34 ParticleEmitter3D {
35 ...
36 }
37 SpriteParticle3D {
38 ...
39 }
40 }
41 \endqml
42
43 Or if the system elements are not direct children, they need to use \c system property to point
44 which ParticleSystem3D they belong to. Like this:
45
46 \qml
47 ParticleSystem3D {
48 id: psystem
49 }
50 ParticleEmitter3D {
51 system: psystem
52 ...
53 }
54 SpriteParticle3D {
55 system: psystem
56 ...
57 }
58 \endqml
59*/
60
61Q_TRACE_POINT(qtquick3d, QSSG_particleUpdate_entry);
62Q_TRACE_POINT(qtquick3d, QSSG_particleUpdate_exit, int particleCount);
63
64QQuick3DParticleSystem::QQuick3DParticleSystem(QQuick3DNode *parent)
65 : QQuick3DNode(parent)
66 , m_running(true)
67 , m_paused(false)
68 , m_initialized(false)
69 , m_componentComplete(false)
70 , m_animation(new QQuick3DParticleSystemAnimation(this))
71 , m_updateAnimation(new QQuick3DParticleSystemUpdate(this))
72 , m_logging(false)
73 , m_loggingData(new QQuick3DParticleSystemLogging(this))
74{
75 connect(sender: m_loggingData, signal: &QQuick3DParticleSystemLogging::loggingIntervalChanged, slot: [this]() {
76 m_loggingTimer.setInterval(m_loggingData->m_loggingInterval);
77 });
78}
79
80QQuick3DParticleSystem::~QQuick3DParticleSystem()
81{
82 m_animation->stop();
83 m_updateAnimation->stop();
84
85 for (const auto &connection : std::as_const(t&: m_connections))
86 QObject::disconnect(connection);
87 // purposeful copy
88 const auto particles = m_particles;
89 const auto emitters = m_emitters;
90 const auto trailEmitters = m_trailEmitters;
91 const auto affectors = m_affectors;
92 for (auto *particle : particles)
93 particle->setSystem(nullptr);
94 for (auto *emitter : emitters)
95 emitter->setSystem(nullptr);
96 for (auto *emitter : trailEmitters)
97 emitter->setSystem(nullptr);
98 for (auto *affector : affectors)
99 affector->setSystem(nullptr);
100}
101
102/*!
103 \qmlproperty bool ParticleSystem3D::running
104
105 This property defines if system is currently running. If running is set to \c false,
106 the particle system will stop the simulation. All particles will be destroyed when
107 the system is set to running again.
108
109 Running should be set to \c false when manually modifying/animating the \l {ParticleSystem3D::time}{time} property.
110
111 The default value is \c true.
112*/
113bool QQuick3DParticleSystem::isRunning() const
114{
115 return m_running;
116}
117
118/*!
119 \qmlproperty bool ParticleSystem3D::paused
120
121 This property defines if system is currently paused. If paused is set to \c true, the
122 particle system will not advance the simulation. When paused is set to \c false again,
123 the simulation will resume from the same point where it was paused.
124
125 The default value is \c false.
126*/
127bool QQuick3DParticleSystem::isPaused() const
128{
129 return m_paused;
130}
131
132/*!
133 \qmlproperty int ParticleSystem3D::startTime
134
135 This property defines time in milliseconds where the system starts. This can be useful
136 to warm up the system so that a set of particles has already been emitted. If for example
137 \l startTime is set to 2000 and system \l time is animating from 0 to 1000, actually
138 animation shows particles from 2000 to 3000ms.
139
140 The default value is \c 0.
141*/
142int QQuick3DParticleSystem::startTime() const
143{
144 return m_startTime;
145}
146
147/*!
148 \qmlproperty int ParticleSystem3D::time
149
150 This property defines time in milliseconds for the system.
151 \note When modifying the time property, \l {ParticleSystem3D::running}{running}
152 should usually be set to \c false.
153
154 Here is an example how to manually animate the system for 3 seconds, in a loop, at half speed:
155
156 \qml
157 ParticleSystem3D {
158 running: false
159 NumberAnimation on time {
160 loops: Animation.Infinite
161 from: 0
162 to: 3000
163 duration: 6000
164 }
165 }
166 \endqml
167*/
168int QQuick3DParticleSystem::time() const
169{
170 return m_time;
171}
172
173/*!
174 \qmlproperty bool ParticleSystem3D::useRandomSeed
175
176 This property defines if particle system seed should be random or user defined.
177 When \c true, a new random value for \l {ParticleSystem3D::seed}{seed} is generated every time particle
178 system is restarted.
179
180 The default value is \c true.
181
182 \note This property should not be modified during the particle animations.
183
184 \sa seed
185*/
186bool QQuick3DParticleSystem::useRandomSeed() const
187{
188 return m_useRandomSeed;
189}
190
191/*!
192 \qmlproperty int ParticleSystem3D::seed
193
194 This property defines the seed value used for particles randomization. With the same seed,
195 particles effect will be identical on every run. This is useful when deterministic behavior
196 is desired over random behavior.
197
198 The default value is \c 0 when \l {ParticleSystem3D::useRandomSeed}{useRandomSeed} is set to
199 \c false, and something in between \c 1..INT32_MAX when \l {ParticleSystem3D::useRandomSeed}{useRandomSeed}
200 is set to \c true.
201
202 \note This property should not be modified during the particle animations.
203
204 \sa useRandomSeed
205*/
206int QQuick3DParticleSystem::seed() const
207{
208 return m_seed;
209}
210
211/*!
212 \qmlproperty bool ParticleSystem3D::logging
213
214 Set this to true to collect \l {ParticleSystem3D::loggingData}{loggingData}.
215
216 \note This property has some performance impact, so it should not be enabled in releases.
217
218 The default value is \c false.
219
220 \sa loggingData
221*/
222bool QQuick3DParticleSystem::logging() const
223{
224 return m_logging;
225}
226
227/*!
228 \qmlproperty ParticleSystem3DLogging ParticleSystem3D::loggingData
229 \readonly
230
231 This property contains logging data which can be useful when developing and optimizing
232 the particle effects.
233
234 \note This property contains correct data only when \l {ParticleSystem3D::logging}{logging} is set
235 to \c true and particle system is running.
236
237 \sa logging
238*/
239QQuick3DParticleSystemLogging *QQuick3DParticleSystem::loggingData() const
240{
241 return m_loggingData;
242}
243
244/*!
245 \qmlmethod ParticleSystem3D::reset()
246
247 This method resets the internal state of the particle system to it's initial state.
248 This can be used when \l running property is \c false to reset the system.
249 The \l running is \c true this method does not need to be called as the system is managing
250 the internal state, but when it is \c false the system needs to be told when the system should
251 be reset.
252*/
253void QQuick3DParticleSystem::reset()
254{
255 for (auto emitter : std::as_const(t&: m_emitters))
256 emitter->reset();
257 for (auto emitter : std::as_const(t&: m_trailEmitters))
258 emitter->reset();
259 for (auto particle : std::as_const(t&: m_particles))
260 particle->reset();
261 m_particleIdIndex = 0;
262}
263
264/*!
265 Returns the current time of the system (m_time + m_startTime).
266 \internal
267*/
268int QQuick3DParticleSystem::currentTime() const
269{
270 return m_currentTime;
271}
272
273void QQuick3DParticleSystem::setRunning(bool running)
274{
275 if (m_running != running) {
276 m_running = running;
277 Q_EMIT runningChanged();
278 setPaused(false);
279
280 if (m_running)
281 reset();
282
283 if (m_componentComplete && !m_running && m_useRandomSeed)
284 doSeedRandomization();
285
286 (m_running && !isEditorModeOn()) ? m_animation->start() : m_animation->stop();
287 }
288}
289
290void QQuick3DParticleSystem::setPaused(bool paused)
291{
292 if (m_paused != paused) {
293 m_paused = paused;
294 if (m_animation->state() != QAbstractAnimation::Stopped)
295 m_paused ? m_animation->pause() : m_animation->resume();
296 Q_EMIT pausedChanged();
297 }
298}
299
300void QQuick3DParticleSystem::setStartTime(int startTime)
301{
302 if (m_startTime == startTime)
303 return;
304
305 m_startTime = startTime;
306 Q_EMIT startTimeChanged();
307}
308
309void QQuick3DParticleSystem::setTime(int time)
310{
311 if (m_time == time)
312 return;
313
314 // Update the time and mark the system dirty
315 m_time = time;
316 m_updateAnimation->setDirty(true);
317
318 Q_EMIT timeChanged();
319}
320
321void QQuick3DParticleSystem::setUseRandomSeed(bool randomize)
322{
323 if (m_useRandomSeed == randomize)
324 return;
325
326 m_useRandomSeed = randomize;
327 // When set to true, random values are recalculated with a random seed
328 // and random values will become independent of particle index when possible.
329 if (m_useRandomSeed)
330 doSeedRandomization();
331 m_rand.setDeterministic(!m_useRandomSeed);
332 Q_EMIT useRandomSeedChanged();
333}
334
335void QQuick3DParticleSystem::setSeed(int seed)
336{
337 if (m_seed == seed)
338 return;
339
340 m_seed = seed;
341 m_rand.init(seed: m_seed);
342 Q_EMIT seedChanged();
343}
344
345void QQuick3DParticleSystem::setLogging(bool logging)
346{
347 if (m_logging == logging)
348 return;
349
350 m_logging = logging;
351
352 resetLoggingVariables();
353 m_loggingData->resetData();
354
355 if (m_logging)
356 m_loggingTimer.start();
357 else
358 m_loggingTimer.stop();
359
360 Q_EMIT loggingChanged();
361}
362
363/*!
364 Set editor time which in editor mode overwrites the time.
365 \internal
366*/
367void QQuick3DParticleSystem::setEditorTime(int time)
368{
369 if (m_editorTime == time)
370 return;
371
372 // Update the time and mark the system dirty
373 m_editorTime = time;
374 m_updateAnimation->setDirty(true);
375}
376
377void QQuick3DParticleSystem::componentComplete()
378{
379 QQuick3DNode::componentComplete();
380 m_componentComplete = true;
381 m_updateAnimation->start();
382
383 connect(sender: &m_loggingTimer, signal: &QTimer::timeout, context: this, slot: &QQuick3DParticleSystem::updateLoggingData);
384 m_loggingTimer.setInterval(m_loggingData->m_loggingInterval);
385
386 if (m_useRandomSeed)
387 doSeedRandomization();
388 else
389 m_rand.init(seed: m_seed);
390
391 m_time = 0;
392 m_currentTime = 0;
393 m_editorTime = 0;
394
395 Q_EMIT timeChanged();
396
397 // Reset restarts the animation (if running)
398 if (m_animation->state() == QAbstractAnimation::Running)
399 m_animation->stop();
400 if (m_running && !isEditorModeOn())
401 m_animation->start();
402 if (m_paused)
403 m_animation->pause();
404
405 m_initialized = true;
406}
407
408void QQuick3DParticleSystem::refresh()
409{
410 // If the system isn't running, force refreshing by calling update
411 // with the current time. QAbstractAnimation::setCurrentTime() implementation
412 // always calls updateCurrentTime() even if the time would remain the same.
413 if (!m_running || m_paused || isEditorModeOn())
414 m_animation->setCurrentTime(isEditorModeOn() ? m_editorTime : m_time);
415}
416
417void QQuick3DParticleSystem::markDirty()
418{
419 // Mark the system dirty so things are updated at the next frame.
420 m_updateAnimation->setDirty(true);
421}
422
423int QQuick3DParticleSystem::particleCount() const
424{
425 int pCount = 0;
426 for (auto particle : std::as_const(t: m_particles))
427 pCount += particle->maxAmount();
428 return pCount;
429}
430
431void QQuick3DParticleSystem::registerParticle(QQuick3DParticle *particle)
432{
433 auto *model = qobject_cast<QQuick3DParticleModelParticle *>(object: particle);
434 if (model) {
435 registerParticleModel(m: model);
436 return;
437 }
438 auto *sprite = qobject_cast<QQuick3DParticleSpriteParticle *>(object: particle);
439 if (sprite) {
440 registerParticleSprite(m: sprite);
441 return;
442 }
443 m_particles << particle;
444}
445
446void QQuick3DParticleSystem::registerParticleModel(QQuick3DParticleModelParticle *m)
447{
448 m_particles << m;
449}
450
451void QQuick3DParticleSystem::registerParticleSprite(QQuick3DParticleSpriteParticle *m)
452{
453 m_particles << m;
454}
455
456void QQuick3DParticleSystem::unRegisterParticle(QQuick3DParticle *particle)
457{
458 auto *model = qobject_cast<QQuick3DParticleModelParticle *>(object: particle);
459 if (model) {
460 m_particles.removeAll(t: particle);
461 return;
462 }
463 auto *sprite = qobject_cast<QQuick3DParticleSpriteParticle *>(object: particle);
464 if (sprite) {
465 m_particles.removeAll(t: particle);
466 return;
467 }
468
469 m_particles.removeAll(t: particle);
470}
471
472void QQuick3DParticleSystem::registerParticleEmitter(QQuick3DParticleEmitter *e)
473{
474 auto te = qobject_cast<QQuick3DParticleTrailEmitter *>(object: e);
475 if (te)
476 m_trailEmitters << te;
477 else
478 m_emitters << e;
479}
480
481void QQuick3DParticleSystem::unRegisterParticleEmitter(QQuick3DParticleEmitter *e)
482{
483 auto te = qobject_cast<QQuick3DParticleTrailEmitter *>(object: e);
484 if (te)
485 m_trailEmitters.removeAll(t: te);
486 else
487 m_emitters.removeAll(t: e);
488}
489
490void QQuick3DParticleSystem::registerParticleAffector(QQuick3DParticleAffector *a)
491{
492 m_affectors << a;
493 m_connections.insert(key: a, value: connect(sender: a, signal: &QQuick3DParticleAffector::update, context: this, slot: &QQuick3DParticleSystem::markDirty));
494}
495
496void QQuick3DParticleSystem::unRegisterParticleAffector(QQuick3DParticleAffector *a)
497{
498 QObject::disconnect(m_connections[a]);
499 m_connections.remove(key: a);
500 m_affectors.removeAll(t: a);
501}
502
503void QQuick3DParticleSystem::updateCurrentTime(int currentTime)
504{
505 if (!m_initialized || isGloballyDisabled() || (isEditorModeOn() && !visible()))
506 return;
507
508 Q_QUICK3D_PROFILE_START(QQuick3DProfiler::Quick3DParticleUpdate);
509
510 Q_TRACE(QSSG_particleUpdate_entry);
511
512 m_currentTime = currentTime;
513 const float timeS = float(m_currentTime / 1000.0f);
514
515 m_particlesMax = 0;
516 m_particlesUsed = 0;
517 m_updates++;
518
519 m_perfTimer.restart();
520
521 // Emit new particles
522 for (auto emitter : std::as_const(t&: m_emitters))
523 emitter->emitParticles();
524
525 // Prepare Affectors
526 for (auto affector : std::as_const(t&: m_affectors)) {
527 if (affector->m_enabled)
528 affector->prepareToAffect();
529 }
530
531 // Animate current particles
532 for (auto particle : std::as_const(t&: m_particles)) {
533
534 // Collect possible trail emits
535 QVector<TrailEmits> trailEmits;
536 for (auto emitter : std::as_const(t&: m_trailEmitters)) {
537 if (emitter->follow() == particle) {
538 int emitAmount = emitter->getEmitAmount();
539 if (emitAmount > 0 || emitter->hasBursts()) {
540 TrailEmits e;
541 e.emitter = emitter;
542 e.amount = emitAmount;
543 trailEmits << e;
544 }
545 }
546 }
547
548 m_particlesMax += particle->maxAmount();
549
550 QQuick3DParticleSpriteParticle *spriteParticle = qobject_cast<QQuick3DParticleSpriteParticle *>(object: particle);
551 if (spriteParticle) {
552 processSpriteParticle(spriteParticle, trailEmits, timeS);
553 continue;
554 }
555 QQuick3DParticleModelParticle *modelParticle = qobject_cast<QQuick3DParticleModelParticle *>(object: particle);
556 if (modelParticle) {
557 processModelParticle(modelParticle, trailEmits, timeS);
558 continue;
559 }
560 QQuick3DParticleModelBlendParticle *mbp = qobject_cast<QQuick3DParticleModelBlendParticle *>(object: particle);
561 if (mbp) {
562 processModelBlendParticle(particle: mbp, trailEmits, timeS);
563 continue;
564 }
565 }
566
567 // Clear bursts from trailemitters
568 for (auto emitter : std::as_const(t&: m_trailEmitters))
569 emitter->clearBursts();
570
571 m_timeAnimation += m_perfTimer.nsecsElapsed();
572 m_updateAnimation->setDirty(false);
573 Q_QUICK3D_PROFILE_END_WITH_ID(QQuick3DProfiler::Quick3DParticleUpdate, m_particlesUsed, Q_QUICK3D_PROFILE_GET_ID(this));
574
575 Q_TRACE(QSSG_particleUpdate_exit, m_particlesUsed);
576
577}
578
579void QQuick3DParticleSystem::processModelParticle(QQuick3DParticleModelParticle *modelParticle, const QVector<TrailEmits> &trailEmits, float timeS)
580{
581 modelParticle->clearInstanceTable();
582
583 const int c = modelParticle->maxAmount();
584
585 for (int i = 0; i < c; i++) {
586 const auto d = &modelParticle->m_particleData.at(i);
587
588 const float particleTimeEnd = d->startTime + d->lifetime;
589
590 if (timeS < d->startTime || timeS > particleTimeEnd) {
591 if (timeS > particleTimeEnd && d->lifetime > 0.0f) {
592 for (auto trailEmit : std::as_const(t: trailEmits))
593 trailEmit.emitter->emitTrailParticles(centerPos: d->startPosition + (d->startVelocity * (particleTimeEnd - d->startTime)), emitAmount: 0, triggerType: QQuick3DParticleDynamicBurst::TriggerEnd);
594 }
595 // Particle not alive currently
596 continue;
597 }
598
599 const float particleTimeS = timeS - d->startTime;
600 QQuick3DParticleDataCurrent currentData;
601 if (timeS >= d->startTime && d->lifetime <= 0.0f) {
602 for (auto trailEmit : std::as_const(t: trailEmits))
603 trailEmit.emitter->emitTrailParticles(centerPos: d->startPosition, emitAmount: 0, triggerType: QQuick3DParticleDynamicBurst::TriggerStart);
604 }
605 // Process features shared for both model & sprite particles
606 processParticleCommon(currentData, d, particleTimeS);
607
608 // Add a base rotation if alignment requested
609 if (modelParticle->m_alignMode != QQuick3DParticle::AlignNone)
610 processParticleAlignment(currentData, particle: modelParticle, d);
611
612 // 0.0 -> 1.0 during the particle lifetime
613 const float timeChange = std::max(a: 0.0f, b: std::min(a: 1.0f, b: particleTimeS / d->lifetime));
614
615 // Scale from initial to endScale
616 currentData.scale = modelParticle->m_initialScale * (d->endSize * timeChange + d->startSize * (1.0f - timeChange));
617
618 // Fade in & out
619 const float particleTimeLeftS = d->lifetime - particleTimeS;
620 processParticleFadeInOut(currentData, particle: modelParticle, particleTimeS, particleTimeLeftS);
621
622 // Affectors
623 for (auto affector : std::as_const(t&: m_affectors)) {
624 // If affector is set to affect only particular particles, check these are included
625 if (affector->m_enabled && (affector->m_particles.isEmpty() || affector->m_particles.contains(t: modelParticle)))
626 affector->affectParticle(sd: *d, d: &currentData, time: particleTimeS);
627 }
628
629 // Emit new particles from trails
630 for (auto trailEmit : std::as_const(t: trailEmits))
631 trailEmit.emitter->emitTrailParticles(centerPos: currentData.position, emitAmount: trailEmit.amount, triggerType: QQuick3DParticleDynamicBurst::TriggerTime);
632
633 const QColor color(currentData.color.r, currentData.color.g, currentData.color.b, currentData.color.a);
634 // Set current particle properties
635 modelParticle->addInstance(position: currentData.position, scale: currentData.scale, eulerRotation: currentData.rotation, color, age: timeChange);
636 }
637 modelParticle->commitInstance();
638}
639
640static QVector3D mix(const QVector3D &a, const QVector3D &b, float f)
641{
642 return (b - a) * f + a;
643}
644
645void QQuick3DParticleSystem::processModelBlendParticle(QQuick3DParticleModelBlendParticle *particle, const QVector<TrailEmits> &trailEmits, float timeS)
646{
647 const int c = particle->maxAmount();
648
649 for (int i = 0; i < c; i++) {
650 const auto d = &particle->m_particleData.at(i);
651
652 const float particleTimeEnd = d->startTime + d->lifetime;
653
654 if (timeS < d->startTime || timeS > particleTimeEnd) {
655 if (timeS > particleTimeEnd && d->lifetime > 0.0f) {
656 for (auto trailEmit : std::as_const(t: trailEmits))
657 trailEmit.emitter->emitTrailParticles(centerPos: d->startPosition + (d->startVelocity * (particleTimeEnd - d->startTime)), emitAmount: 0, triggerType: QQuick3DParticleDynamicBurst::TriggerEnd);
658 }
659 // Particle not alive currently
660 float age = 0.0f;
661 float size = 0.0f;
662 QVector3D pos;
663 QVector3D rot;
664 QVector4D color(float(d->startColor.r)/ 255.0f,
665 float(d->startColor.g)/ 255.0f,
666 float(d->startColor.b)/ 255.0f,
667 float(d->startColor.a)/ 255.0f);
668 if (d->startTime > 0.0f && timeS > particleTimeEnd
669 && (particle->modelBlendMode() == QQuick3DParticleModelBlendParticle::Construct ||
670 particle->modelBlendMode() == QQuick3DParticleModelBlendParticle::Transfer)) {
671 age = 1.0f;
672 size = 1.0f;
673 pos = particle->particleEndPosition(particleIndex: i);
674 rot = particle->particleEndRotation(particleIndex: i);
675 if (particle->fadeOutEffect() == QQuick3DParticle::FadeOpacity)
676 color.setW(0.0f);
677 } else if (particle->modelBlendMode() == QQuick3DParticleModelBlendParticle::Explode ||
678 particle->modelBlendMode() == QQuick3DParticleModelBlendParticle::Transfer) {
679 age = 0.0f;
680 size = 1.0f;
681 pos = particle->particleCenter(particleIndex: i);
682 if (particle->fadeInEffect() == QQuick3DParticle::FadeOpacity)
683 color.setW(0.0f);
684 }
685 particle->setParticleData(particleIndex: i, position: pos, rotation: rot, color, size, age);
686 continue;
687 }
688
689 const float particleTimeS = timeS - d->startTime;
690 QQuick3DParticleDataCurrent currentData;
691 if (timeS >= d->startTime && d->lifetime <= 0.0f) {
692 for (auto trailEmit : std::as_const(t: trailEmits))
693 trailEmit.emitter->emitTrailParticles(centerPos: d->startPosition, emitAmount: 0, triggerType: QQuick3DParticleDynamicBurst::TriggerStart);
694 }
695
696 // Process features shared for both model & sprite particles
697 processParticleCommon(currentData, d, particleTimeS);
698
699 // 0.0 -> 1.0 during the particle lifetime
700 const float timeChange = std::max(a: 0.0f, b: std::min(a: 1.0f, b: particleTimeS / d->lifetime));
701
702 // Scale from initial to endScale
703 const float scale = d->endSize * timeChange + d->startSize * (1.0f - timeChange);
704 currentData.scale = QVector3D(scale, scale, scale);
705
706 // Fade in & out
707 const float particleTimeLeftS = d->lifetime - particleTimeS;
708 processParticleFadeInOut(currentData, particle, particleTimeS, particleTimeLeftS);
709
710 // Affectors
711 for (auto affector : std::as_const(t&: m_affectors)) {
712 // If affector is set to affect only particular particles, check these are included
713 if (affector->m_enabled && (affector->m_particles.isEmpty() || affector->m_particles.contains(t: particle)))
714 affector->affectParticle(sd: *d, d: &currentData, time: particleTimeS);
715 }
716
717 // Emit new particles from trails
718 for (auto trailEmit : std::as_const(t: trailEmits))
719 trailEmit.emitter->emitTrailParticles(centerPos: currentData.position, emitAmount: trailEmit.amount, triggerType: QQuick3DParticleDynamicBurst::TriggerTime);
720
721 // Set current particle properties
722 const QVector4D color(float(currentData.color.r) / 255.0f,
723 float(currentData.color.g) / 255.0f,
724 float(currentData.color.b) / 255.0f,
725 float(currentData.color.a) / 255.0f);
726 float endTimeS = particle->endTime() * 0.001f;
727 if ((particle->modelBlendMode() == QQuick3DParticleModelBlendParticle::Construct ||
728 particle->modelBlendMode() == QQuick3DParticleModelBlendParticle::Transfer)
729 && particleTimeLeftS < endTimeS) {
730 QVector3D endPosition = particle->particleEndPosition(particleIndex: i);
731 QVector3D endRotation = particle->particleEndRotation(particleIndex: i);
732 float factor = 1.0f - particleTimeLeftS / endTimeS;
733 currentData.position = mix(a: currentData.position, b: endPosition, f: factor);
734 currentData.rotation = mix(a: currentData.rotation, b: endRotation, f: factor);
735 }
736 particle->setParticleData(particleIndex: i, position: currentData.position, rotation: currentData.rotation,
737 color, size: currentData.scale.x(), age: timeChange);
738 }
739 particle->commitParticles();
740}
741
742void QQuick3DParticleSystem::processSpriteParticle(QQuick3DParticleSpriteParticle *spriteParticle, const QVector<TrailEmits> &trailEmits, float timeS)
743{
744 const int c = spriteParticle->maxAmount();
745
746 for (int i = 0; i < c; i++) {
747 const auto d = &spriteParticle->m_particleData.at(i);
748
749 const float particleTimeEnd = d->startTime + d->lifetime;
750 auto &particleData = spriteParticle->m_spriteParticleData[i];
751 if (timeS < d->startTime || timeS > particleTimeEnd) {
752 if (timeS > particleTimeEnd && particleData.age > 0.0f) {
753 for (auto trailEmit : std::as_const(t: trailEmits))
754 trailEmit.emitter->emitTrailParticles(centerPos: particleData.position, emitAmount: 0, triggerType: QQuick3DParticleDynamicBurst::TriggerEnd);
755 auto *lineParticle = qobject_cast<QQuick3DParticleLineParticle *>(object: spriteParticle);
756 if (lineParticle)
757 lineParticle->saveLineSegment(particleIndex: i, time: timeS);
758 }
759 // Particle not alive currently
760 spriteParticle->resetParticleData(particleIndex: i);
761 continue;
762 }
763 const float particleTimeS = timeS - d->startTime;
764 QQuick3DParticleDataCurrent currentData;
765 if (timeS >= d->startTime && timeS < particleTimeEnd && particleData.age == 0.0f) {
766 for (auto trailEmit : std::as_const(t: trailEmits))
767 trailEmit.emitter->emitTrailParticles(centerPos: d->startPosition, emitAmount: 0, triggerType: QQuick3DParticleDynamicBurst::TriggerStart);
768 }
769 // Process features shared for both model & sprite particles
770 processParticleCommon(currentData, d, particleTimeS);
771
772 // Add a base rotation if alignment requested
773 if (!spriteParticle->m_billboard && spriteParticle->m_alignMode != QQuick3DParticle::AlignNone)
774 processParticleAlignment(currentData, particle: spriteParticle, d);
775
776 // 0.0 -> 1.0 during the particle lifetime
777 const float timeChange = std::max(a: 0.0f, b: std::min(a: 1.0f, b: particleTimeS / d->lifetime));
778
779 // Scale from initial to endScale
780 const float scale = d->endSize * timeChange + d->startSize * (1.0f - timeChange);
781 currentData.scale = QVector3D(scale, scale, scale);
782
783 // Fade in & out
784 const float particleTimeLeftS = d->lifetime - particleTimeS;
785 processParticleFadeInOut(currentData, particle: spriteParticle, particleTimeS, particleTimeLeftS);
786
787 float animationFrame = 0.0f;
788 if (auto sequence = spriteParticle->m_spriteSequence) {
789 // animationFrame range is [0..1) where 0.0 is the beginning of the first frame
790 // and 0.9999 is the end of the last frame.
791 const bool isSingleFrame = (sequence->animationDirection() == QQuick3DParticleSpriteSequence::SingleFrame);
792 float startFrame = sequence->firstFrame(index: d->index, singleFrame: isSingleFrame);
793 if (sequence->animationDirection() == QQuick3DParticleSpriteSequence::Normal) {
794 animationFrame = fmodf(x: startFrame + particleTimeS / d->animationTime, y: 1.0f);
795 } else if (sequence->animationDirection() == QQuick3DParticleSpriteSequence::Reverse) {
796 animationFrame = fmodf(x: startFrame + 0.9999f - fmodf(x: particleTimeS / d->animationTime, y: 1.0f), y: 1.0f);
797 } else if (sequence->animationDirection() == QQuick3DParticleSpriteSequence::Alternate) {
798 animationFrame = startFrame + particleTimeS / d->animationTime;
799 animationFrame = fabsf(x: fmodf(x: 1.0f + animationFrame, y: 2.0f) - 1.0f);
800 } else if (sequence->animationDirection() == QQuick3DParticleSpriteSequence::AlternateReverse) {
801 animationFrame = fmodf(x: startFrame + 0.9999f, y: 1.0f) - particleTimeS / d->animationTime;
802 animationFrame = fabsf(x: fmodf(x: fabsf(x: 1.0f + animationFrame), y: 2.0f) - 1.0f);
803 } else {
804 // SingleFrame
805 animationFrame = startFrame;
806 }
807 animationFrame = std::clamp(val: animationFrame, lo: 0.0f, hi: 0.9999f);
808 }
809
810 // Affectors
811 for (auto affector : std::as_const(t&: m_affectors)) {
812 // If affector is set to affect only particular particles, check these are included
813 if (affector->m_enabled && (affector->m_particles.isEmpty() || affector->m_particles.contains(t: spriteParticle)))
814 affector->affectParticle(sd: *d, d: &currentData, time: particleTimeS);
815 }
816
817 // Emit new particles from trails
818 for (auto trailEmit : std::as_const(t: trailEmits))
819 trailEmit.emitter->emitTrailParticles(centerPos: currentData.position, emitAmount: trailEmit.amount, triggerType: QQuick3DParticleDynamicBurst::TriggerTime);
820
821
822 // Set current particle properties
823 const QVector4D color(float(currentData.color.r) / 255.0f,
824 float(currentData.color.g) / 255.0f,
825 float(currentData.color.b) / 255.0f,
826 float(currentData.color.a) / 255.0f);
827 const QVector3D offset(spriteParticle->offsetX(), spriteParticle->offsetY(), 0);
828 spriteParticle->setParticleData(particleIndex: i, position: currentData.position + (offset * currentData.scale.x()),
829 rotation: currentData.rotation, color, size: currentData.scale.x(), age: timeChange,
830 animationFrame);
831 }
832 spriteParticle->commitParticles(timeS);
833}
834
835void QQuick3DParticleSystem::processParticleCommon(QQuick3DParticleDataCurrent &currentData, const QQuick3DParticleData *d, float particleTimeS)
836{
837 m_particlesUsed++;
838
839 currentData.position = d->startPosition;
840
841 // Initial color from start color
842 currentData.color = d->startColor;
843
844 // Initial position from start velocity
845 currentData.position += d->startVelocity * particleTimeS;
846
847 // Initial rotation from start velocity
848 constexpr float step = 360.0f / 127.0f;
849 currentData.rotation = QVector3D(
850 d->startRotation.x * step + abs(x: d->startRotationVelocity.x) * d->startRotationVelocity.x * particleTimeS,
851 d->startRotation.y * step + abs(x: d->startRotationVelocity.y) * d->startRotationVelocity.y * particleTimeS,
852 d->startRotation.z * step + abs(x: d->startRotationVelocity.z) * d->startRotationVelocity.z * particleTimeS);
853}
854
855void QQuick3DParticleSystem::processParticleFadeInOut(QQuick3DParticleDataCurrent &currentData, const QQuick3DParticle *particle, float particleTimeS, float particleTimeLeftS)
856{
857 const float fadeInS = particle->m_fadeInDuration / 1000.0f;
858 const float fadeOutS = particle->m_fadeOutDuration / 1000.0f;
859 if (particleTimeS < fadeInS) {
860 // 0.0 -> 1.0 during the particle fadein
861 const float fadeIn = particleTimeS / fadeInS;
862 if (particle->m_fadeInEffect == QQuick3DParticleModelParticle::FadeOpacity)
863 currentData.color.a *= fadeIn;
864 else if (particle->m_fadeInEffect == QQuick3DParticleModelParticle::FadeScale)
865 currentData.scale *= fadeIn;
866 }
867 if (particleTimeLeftS < fadeOutS) {
868 // 1.0 -> 0.0 during the particle fadeout
869 const float fadeOut = particleTimeLeftS / fadeOutS;
870 if (particle->m_fadeOutEffect == QQuick3DParticleModelParticle::FadeOpacity)
871 currentData.color.a *= fadeOut;
872 else if (particle->m_fadeOutEffect == QQuick3DParticleModelParticle::FadeScale)
873 currentData.scale *= fadeOut;
874 }
875}
876
877void QQuick3DParticleSystem::processParticleAlignment(QQuick3DParticleDataCurrent &currentData, const QQuick3DParticle *particle, const QQuick3DParticleData *d)
878{
879 if (particle->m_alignMode == QQuick3DParticle::AlignTowardsTarget) {
880 QQuaternion alignQuat = QQuick3DQuaternionUtils::lookAt(sourcePosition: particle->alignTargetPosition(), targetPosition: currentData.position);
881 currentData.rotation = (alignQuat * QQuaternion::fromEulerAngles(eulerAngles: currentData.rotation)).toEulerAngles();
882 } else if (particle->m_alignMode == QQuick3DParticle::AlignTowardsStartVelocity) {
883 QQuaternion alignQuat = QQuick3DQuaternionUtils::lookAt(sourcePosition: d->startVelocity, targetPosition: QVector3D());
884 currentData.rotation = (alignQuat * QQuaternion::fromEulerAngles(eulerAngles: currentData.rotation)).toEulerAngles();
885 }
886}
887
888bool QQuick3DParticleSystem::isGloballyDisabled()
889{
890 static const bool disabled = qEnvironmentVariableIntValue(varName: "QT_QUICK3D_DISABLE_PARTICLE_SYSTEMS");
891 return disabled;
892}
893
894bool QQuick3DParticleSystem::isEditorModeOn()
895{
896 static const bool editorMode = qEnvironmentVariableIntValue(varName: "QT_QUICK3D_EDITOR_PARTICLE_SYSTEMS");
897 return editorMode;
898}
899
900void QQuick3DParticleSystem::updateLoggingData()
901{
902 if (m_updates == 0)
903 return;
904
905 if (m_loggingData->m_particlesMax != m_particlesMax) {
906 m_loggingData->m_particlesMax = m_particlesMax;
907 Q_EMIT m_loggingData->particlesMaxChanged();
908 }
909 if (m_loggingData->m_particlesUsed != m_particlesUsed) {
910 m_loggingData->m_particlesUsed = m_particlesUsed;
911 Q_EMIT m_loggingData->particlesUsedChanged();
912 }
913 if (m_loggingData->m_updates != m_updates) {
914 m_loggingData->m_updates = m_updates;
915 Q_EMIT m_loggingData->updatesChanged();
916 }
917
918 m_loggingData->updateTimes(time: m_timeAnimation);
919
920 Q_EMIT loggingDataChanged();
921 resetLoggingVariables();
922}
923
924void QQuick3DParticleSystem::resetLoggingVariables()
925{
926 m_particlesMax = 0;
927 m_particlesUsed = 0;
928 m_updates = 0;
929 m_timeAnimation = 0;
930}
931
932QPRand *QQuick3DParticleSystem::rand()
933{
934 return &m_rand;
935}
936
937void QQuick3DParticleSystem::doSeedRandomization()
938{
939 // Random 1..INT32_MAX, making sure seed changes from the initial 0.
940 setSeed(QRandomGenerator::global()->bounded(highest: 1 + (INT32_MAX - 1)));
941}
942
943bool QQuick3DParticleSystem::isShared(const QQuick3DParticle *particle) const
944{
945 int count = 0;
946 for (auto emitter : std::as_const(t: m_emitters)) {
947 count += emitter->particle() == particle;
948 if (count > 1)
949 return true;
950 }
951 for (auto emitter : std::as_const(t: m_trailEmitters)) {
952 count += emitter->particle() == particle;
953 if (count > 1)
954 return true;
955 }
956 return false;
957}
958
959QT_END_NAMESPACE
960

source code of qtquick3d/src/quick3dparticles/qquick3dparticlesystem.cpp