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 | |
19 | QT_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 | |
61 | Q_TRACE_POINT(qtquick3d, QSSG_particleUpdate_entry); |
62 | Q_TRACE_POINT(qtquick3d, QSSG_particleUpdate_exit, int particleCount); |
63 | |
64 | QQuick3DParticleSystem::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 | |
80 | QQuick3DParticleSystem::~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 | */ |
113 | bool 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 | */ |
127 | bool 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 | */ |
142 | int 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 | */ |
168 | int 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 | */ |
186 | bool 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 | */ |
206 | int 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 | */ |
222 | bool 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 | */ |
239 | QQuick3DParticleSystemLogging *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 | */ |
253 | void 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 | */ |
268 | int QQuick3DParticleSystem::currentTime() const |
269 | { |
270 | return m_currentTime; |
271 | } |
272 | |
273 | void 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 | |
290 | void 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 | |
300 | void QQuick3DParticleSystem::setStartTime(int startTime) |
301 | { |
302 | if (m_startTime == startTime) |
303 | return; |
304 | |
305 | m_startTime = startTime; |
306 | Q_EMIT startTimeChanged(); |
307 | } |
308 | |
309 | void 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 | |
321 | void 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 | |
335 | void 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 | |
345 | void 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 | */ |
367 | void 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 | |
377 | void 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 | |
408 | void 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 | |
417 | void QQuick3DParticleSystem::markDirty() |
418 | { |
419 | // Mark the system dirty so things are updated at the next frame. |
420 | m_updateAnimation->setDirty(true); |
421 | } |
422 | |
423 | int 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 | |
431 | void 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 | |
446 | void QQuick3DParticleSystem::registerParticleModel(QQuick3DParticleModelParticle *m) |
447 | { |
448 | m_particles << m; |
449 | } |
450 | |
451 | void QQuick3DParticleSystem::registerParticleSprite(QQuick3DParticleSpriteParticle *m) |
452 | { |
453 | m_particles << m; |
454 | } |
455 | |
456 | void 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 | |
472 | void 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 | |
481 | void 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 | |
490 | void 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 | |
496 | void 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 | |
503 | void 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 | |
579 | void 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: ¤tData, 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 | |
640 | static QVector3D mix(const QVector3D &a, const QVector3D &b, float f) |
641 | { |
642 | return (b - a) * f + a; |
643 | } |
644 | |
645 | void 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: ¤tData, 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 | |
742 | void 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: ¤tData, 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 | |
835 | void QQuick3DParticleSystem::processParticleCommon(QQuick3DParticleDataCurrent ¤tData, 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 | |
855 | void QQuick3DParticleSystem::processParticleFadeInOut(QQuick3DParticleDataCurrent ¤tData, 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 | |
877 | void QQuick3DParticleSystem::processParticleAlignment(QQuick3DParticleDataCurrent ¤tData, 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 | |
888 | bool QQuick3DParticleSystem::isGloballyDisabled() |
889 | { |
890 | static const bool disabled = qEnvironmentVariableIntValue(varName: "QT_QUICK3D_DISABLE_PARTICLE_SYSTEMS" ); |
891 | return disabled; |
892 | } |
893 | |
894 | bool QQuick3DParticleSystem::isEditorModeOn() |
895 | { |
896 | static const bool editorMode = qEnvironmentVariableIntValue(varName: "QT_QUICK3D_EDITOR_PARTICLE_SYSTEMS" ); |
897 | return editorMode; |
898 | } |
899 | |
900 | void 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 | |
924 | void QQuick3DParticleSystem::resetLoggingVariables() |
925 | { |
926 | m_particlesMax = 0; |
927 | m_particlesUsed = 0; |
928 | m_updates = 0; |
929 | m_timeAnimation = 0; |
930 | } |
931 | |
932 | QPRand *QQuick3DParticleSystem::rand() |
933 | { |
934 | return &m_rand; |
935 | } |
936 | |
937 | void 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 | |
943 | bool 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 | |
959 | QT_END_NAMESPACE |
960 | |