1 | // Copyright (C) 2016 The Qt Company Ltd. |
2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only |
3 | |
4 | #include "qquickparticlesystem_p.h" |
5 | #include <QtQuick/qsgnode.h> |
6 | #include "qquickparticleemitter_p.h" |
7 | #include "qquickparticleaffector_p.h" |
8 | #include "qquickparticlepainter_p.h" |
9 | #include <private/qquickspriteengine_p.h> |
10 | #include <private/qquicksprite_p.h> |
11 | #include "qquickv4particledata_p.h" |
12 | #include "qquickparticlegroup_p.h" |
13 | |
14 | #include "qquicktrailemitter_p.h"//###For auto-follow on states, perhaps should be in emitter? |
15 | #include <private/qqmlengine_p.h> |
16 | #include <private/qqmlglobal_p.h> |
17 | #include <cmath> |
18 | #include <QDebug> |
19 | |
20 | QT_BEGIN_NAMESPACE |
21 | //###Switch to define later, for now user-friendly (no compilation) debugging is worth it |
22 | DEFINE_BOOL_CONFIG_OPTION(qmlParticlesDebug, QML_PARTICLES_DEBUG) |
23 | |
24 | |
25 | /* \internal ParticleSystem internals documentation |
26 | |
27 | Affectors, Painters, Emitters and Groups all register themselves on construction as a callback |
28 | from their setSystem (or componentComplete if they have a system from a parent). |
29 | |
30 | Particle data is stored by group, They have a group index (used by the particle system almost |
31 | everywhere) and a global index (used by the Stochastic state engine powering stochastic group |
32 | transitions). Each group has a recycling list/heap that stores the particle data. |
33 | |
34 | The recycling list/heap is a heap of particle data sorted by when they're expected to die. If |
35 | they die prematurely then they are marked as reusable (and will probably still be alive when |
36 | they exit the heap). If they have their life extended, then they aren't dead when expected. |
37 | If this happens, they go back in the heap with the new estimate. If they have died on schedule, |
38 | then the indexes are marked as reusable. If no indexes are reusable when new particles are |
39 | requested, then the list is extended. This relatively complex datastructure is because memory |
40 | allocation and deallocation on this scale proved to be a significant performance cost. In order |
41 | to reuse the indexes validly (even when particles can have their life extended or cut short |
42 | dynamically, or particle counts grow) this seemed to be the most efficient option for keeping |
43 | track of which indices could be reused. |
44 | |
45 | When a new particle is emitted, the emitter gets a new datum from the group (through the |
46 | system), and sets properties on it. Then it's passed back to the group briefly so that it can |
47 | now guess when the particle will die. Then the painters get a change to initialize properties |
48 | as well, since particle data includes shared data from painters as well as logical particle |
49 | data. |
50 | |
51 | Every animation advance, the simulation advances by running all emitters for the elapsed |
52 | duration, then running all affectors, then telling all particle painters to update changed |
53 | particles. The ParticlePainter superclass stores these changes, and they are implemented |
54 | when the painter is called to paint in the render thread. |
55 | |
56 | Particle group changes move the particle from one group to another by killing the old particle |
57 | and then creating a new one with the same data in the new group. |
58 | |
59 | Note that currently groups only grow. Given that data is stored in vectors, it is non-trivial |
60 | to pluck out the unused indexes when the count goes down. Given the dynamic nature of the |
61 | system, it is difficult to tell if those unused data instances will be used again. Still, |
62 | some form of garbage collection is on the long term plan. |
63 | */ |
64 | |
65 | /*! |
66 | \qmltype ParticleSystem |
67 | //! \instantiates QQuickParticleSystem |
68 | \inqmlmodule QtQuick.Particles |
69 | \brief A system which includes particle painter, emitter, and affector types. |
70 | \ingroup qtquick-particles |
71 | |
72 | */ |
73 | |
74 | /*! |
75 | \qmlproperty bool QtQuick.Particles::ParticleSystem::running |
76 | |
77 | If running is set to false, the particle system will stop the simulation. All particles |
78 | will be destroyed when the system is set to running again. |
79 | |
80 | It can also be controlled with the start() and stop() methods. |
81 | */ |
82 | |
83 | |
84 | /*! |
85 | \qmlproperty bool QtQuick.Particles::ParticleSystem::paused |
86 | |
87 | If paused is set to true, the particle system will not advance the simulation. When |
88 | paused is set to false again, the simulation will resume from the same point it was |
89 | paused. |
90 | |
91 | The simulation will automatically pause if it detects that there are no live particles |
92 | left, and unpause when new live particles are added. |
93 | |
94 | It can also be controlled with the pause() and resume() methods. |
95 | */ |
96 | |
97 | /*! |
98 | \qmlproperty bool QtQuick.Particles::ParticleSystem::empty |
99 | |
100 | empty is set to true when there are no live particles left in the system. |
101 | |
102 | You can use this to pause the system, keeping it from spending any time updating, |
103 | but you will need to resume it in order for additional particles to be generated |
104 | by the system. |
105 | |
106 | To kill all the particles in the system, use an Age affector. |
107 | */ |
108 | |
109 | /*! |
110 | \qmlproperty list<Sprite> QtQuick.Particles::ParticleSystem::particleStates |
111 | |
112 | You can define a sub-set of particle groups in this property in order to provide them |
113 | with stochastic state transitions. |
114 | |
115 | Each QtQuick::Sprite in this list is interpreted as corresponding to the particle group |
116 | with the same name. Any transitions defined in these sprites will take effect on the particle |
117 | groups as well. Additionally TrailEmitters, Affectors and ParticlePainters defined |
118 | inside one of these sprites are automatically associated with the corresponding particle group. |
119 | */ |
120 | |
121 | /*! |
122 | \qmlmethod QtQuick.Particles::ParticleSystem::pause() |
123 | |
124 | Pauses the simulation if it is running. |
125 | |
126 | \sa resume, paused |
127 | */ |
128 | |
129 | /*! |
130 | \qmlmethod QtQuick.Particles::ParticleSystem::resume() |
131 | |
132 | Resumes the simulation if it is paused. |
133 | |
134 | \sa pause, paused |
135 | */ |
136 | |
137 | /*! |
138 | \qmlmethod QtQuick.Particles::ParticleSystem::start() |
139 | |
140 | Starts the simulation if it has not already running. |
141 | |
142 | \sa stop, restart, running |
143 | */ |
144 | |
145 | /*! |
146 | \qmlmethod QtQuick.Particles::ParticleSystem::stop() |
147 | |
148 | Stops the simulation if it is running. |
149 | |
150 | \sa start, restart, running |
151 | */ |
152 | |
153 | /*! |
154 | \qmlmethod QtQuick.Particles::ParticleSystem::restart() |
155 | |
156 | Stops the simulation if it is running, and then starts it. |
157 | |
158 | \sa start, stop, running |
159 | */ |
160 | /*! |
161 | \qmlmethod QtQuick.Particles::ParticleSystem::reset() |
162 | |
163 | Discards all currently existing particles. |
164 | |
165 | */ |
166 | |
167 | static inline int roundedTime(qreal a) |
168 | {// in ms |
169 | return (int)qRound(d: a*1000.0); |
170 | } |
171 | |
172 | QQuickParticleDataHeap::QQuickParticleDataHeap() |
173 | : m_data(0) |
174 | { |
175 | m_data.reserve(size: 1000); |
176 | clear(); |
177 | } |
178 | |
179 | void QQuickParticleDataHeap::grow() //###Consider automatic growth vs resize() calls from GroupData |
180 | { |
181 | m_data.resize(size: qsizetype(1) << ++m_size); |
182 | } |
183 | |
184 | void QQuickParticleDataHeap::insert(QQuickParticleData* data) |
185 | { |
186 | insertTimed(data, time: roundedTime(a: data->t + data->lifeSpan)); |
187 | } |
188 | |
189 | void QQuickParticleDataHeap::insertTimed(QQuickParticleData* data, int time) |
190 | { |
191 | //TODO: Optimize 0 lifespan (or already dead) case |
192 | if (m_lookups.contains(key: time)) { |
193 | m_data[m_lookups[time]].data << data; |
194 | return; |
195 | } |
196 | if (m_end == (1 << m_size)) |
197 | grow(); |
198 | m_data[m_end].time = time; |
199 | m_data[m_end].data.clear(); |
200 | m_data[m_end].data.insert(value: data); |
201 | m_lookups.insert(key: time, value: m_end); |
202 | bubbleUp(m_end++); |
203 | } |
204 | |
205 | int QQuickParticleDataHeap::top() |
206 | { |
207 | if (m_end == 0) |
208 | return 1 << 30; |
209 | return m_data[0].time; |
210 | } |
211 | |
212 | QSet<QQuickParticleData*> QQuickParticleDataHeap::pop() |
213 | { |
214 | if (!m_end) |
215 | return QSet<QQuickParticleData*> (); |
216 | QSet<QQuickParticleData*> ret = m_data[0].data; |
217 | m_lookups.remove(key: m_data[0].time); |
218 | if (m_end == 1) { |
219 | --m_end; |
220 | } else { |
221 | m_data[0] = m_data[--m_end]; |
222 | bubbleDown(0); |
223 | } |
224 | return ret; |
225 | } |
226 | |
227 | void QQuickParticleDataHeap::clear() |
228 | { |
229 | m_size = 0; |
230 | m_end = 0; |
231 | //m_size is in powers of two. So to start at 0 we have one allocated |
232 | m_data.resize(size: 1); |
233 | m_lookups.clear(); |
234 | } |
235 | |
236 | bool QQuickParticleDataHeap::contains(QQuickParticleData* d) |
237 | { |
238 | for (int i=0; i<m_end; i++) |
239 | if (m_data[i].data.contains(value: d)) |
240 | return true; |
241 | return false; |
242 | } |
243 | |
244 | void QQuickParticleDataHeap::swap(int a, int b) |
245 | { |
246 | m_tmp = m_data[a]; |
247 | m_data[a] = m_data[b]; |
248 | m_data[b] = m_tmp; |
249 | m_lookups[m_data[a].time] = a; |
250 | m_lookups[m_data[b].time] = b; |
251 | } |
252 | |
253 | void QQuickParticleDataHeap::bubbleUp(int idx)//tends to be called once |
254 | { |
255 | if (!idx) |
256 | return; |
257 | int parent = (idx-1)/2; |
258 | if (m_data[idx].time < m_data[parent].time) { |
259 | swap(a: idx, b: parent); |
260 | bubbleUp(idx: parent); |
261 | } |
262 | } |
263 | |
264 | void QQuickParticleDataHeap::bubbleDown(int idx)//tends to be called log n times |
265 | { |
266 | int left = idx*2 + 1; |
267 | if (left >= m_end) |
268 | return; |
269 | int lesser = left; |
270 | int right = idx*2 + 2; |
271 | if (right < m_end) { |
272 | if (m_data[left].time > m_data[right].time) |
273 | lesser = right; |
274 | } |
275 | if (m_data[idx].time > m_data[lesser].time) { |
276 | swap(a: idx, b: lesser); |
277 | bubbleDown(idx: lesser); |
278 | } |
279 | } |
280 | |
281 | QQuickParticleGroupData::QQuickParticleGroupData(const QString &name, QQuickParticleSystem* sys) |
282 | : index(sys->registerParticleGroupData(name, pgd: this)) |
283 | , m_size(0) |
284 | , m_system(sys) |
285 | { |
286 | initList(); |
287 | } |
288 | |
289 | QQuickParticleGroupData::~QQuickParticleGroupData() |
290 | { |
291 | for (QQuickParticleData *d : std::as_const(t&: data)) |
292 | delete d; |
293 | } |
294 | |
295 | QString QQuickParticleGroupData::name() const//### Worth caching as well? |
296 | { |
297 | return m_system->groupIds.key(value: index); |
298 | } |
299 | |
300 | void QQuickParticleGroupData::setSize(int newSize) |
301 | { |
302 | if (newSize == m_size) |
303 | return; |
304 | Q_ASSERT(newSize > m_size);//XXX allow shrinking |
305 | data.resize(size: newSize); |
306 | freeList.resize(newSize); |
307 | for (int i=m_size; i<newSize; i++) { |
308 | data[i] = new QQuickParticleData; |
309 | data[i]->groupId = index; |
310 | data[i]->index = i; |
311 | } |
312 | int delta = newSize - m_size; |
313 | m_size = newSize; |
314 | for (QQuickParticlePainter *p : std::as_const(t&: painters)) |
315 | p->setCount(p->count() + delta); |
316 | } |
317 | |
318 | void QQuickParticleGroupData::initList() |
319 | { |
320 | dataHeap.clear(); |
321 | } |
322 | |
323 | void QQuickParticleGroupData::kill(QQuickParticleData* d) |
324 | { |
325 | Q_ASSERT(d->groupId == index); |
326 | d->lifeSpan = 0;//Kill off |
327 | for (QQuickParticlePainter *p : std::as_const(t&: painters)) |
328 | p->reload(d); |
329 | freeList.free(index: d->index); |
330 | } |
331 | |
332 | QQuickParticleData* QQuickParticleGroupData::newDatum(bool respectsLimits) |
333 | { |
334 | //recycle();//Extra recycler round to be sure? |
335 | |
336 | while (freeList.hasUnusedEntries()) { |
337 | int idx = freeList.alloc(); |
338 | if (data[idx]->stillAlive(system: m_system)) {// ### This means resurrection of 'dead' particles. Is that allowed? |
339 | prepareRecycler(d: data[idx]); |
340 | continue; |
341 | } |
342 | return data[idx]; |
343 | } |
344 | if (respectsLimits) |
345 | return nullptr; |
346 | |
347 | int oldSize = m_size; |
348 | setSize(oldSize + 10);//###+1,10%,+10? Choose something non-arbitrarily |
349 | int idx = freeList.alloc(); |
350 | Q_ASSERT(idx == oldSize); |
351 | return data[idx]; |
352 | } |
353 | |
354 | bool QQuickParticleGroupData::recycle() |
355 | { |
356 | m_latestAliveParticles.clear(); |
357 | |
358 | while (dataHeap.top() <= m_system->timeInt) { |
359 | for (QQuickParticleData *datum : dataHeap.pop()) { |
360 | if (!datum->stillAlive(system: m_system)) { |
361 | freeList.free(index: datum->index); |
362 | } else { |
363 | m_latestAliveParticles.push_back(t: datum); |
364 | } |
365 | } |
366 | } |
367 | |
368 | for (auto particle : m_latestAliveParticles) |
369 | prepareRecycler(d: particle); //ttl has been altered mid-way, put it back |
370 | |
371 | //TODO: If the data is clear, gc (consider shrinking stack size)? |
372 | return freeList.count() == 0; |
373 | } |
374 | |
375 | void QQuickParticleGroupData::prepareRecycler(QQuickParticleData* d) |
376 | { |
377 | if (d->lifeSpan*1000 < m_system->maxLife) { |
378 | dataHeap.insert(data: d); |
379 | } else { |
380 | int extend = 2 * m_system->maxLife / 3; |
381 | while ((roundedTime(a: d->t) + extend) <= m_system->timeInt) |
382 | d->extendLife(time: m_system->maxLife / 3000.0, particleSystem: m_system); |
383 | dataHeap.insertTimed(data: d, time: roundedTime(a: d->t) + extend); |
384 | } |
385 | } |
386 | |
387 | QQuickParticleData::QQuickParticleData() |
388 | : index(0) |
389 | , systemIndex(-1) |
390 | , groupId(0) |
391 | , colorOwner(nullptr) |
392 | , rotationOwner(nullptr) |
393 | , deformationOwner(nullptr) |
394 | , animationOwner(nullptr) |
395 | , v4Datum(nullptr) |
396 | { |
397 | x = 0; |
398 | y = 0; |
399 | t = -1; |
400 | lifeSpan = 0; |
401 | size = 0; |
402 | endSize = 0; |
403 | vx = 0; |
404 | vy = 0; |
405 | ax = 0; |
406 | ay = 0; |
407 | xx = 1; |
408 | xy = 0; |
409 | yx = 0; |
410 | yy = 1; |
411 | rotation = 0; |
412 | rotationVelocity = 0; |
413 | autoRotate = 0; |
414 | animIdx = 0; |
415 | frameDuration = 1; |
416 | frameAt = -1; |
417 | frameCount = 1; |
418 | animT = -1; |
419 | animX = 0; |
420 | animY = 0; |
421 | animWidth = 1; |
422 | animHeight = 1; |
423 | color.r = 255; |
424 | color.g = 255; |
425 | color.b = 255; |
426 | color.a = 255; |
427 | delegate = nullptr; |
428 | } |
429 | |
430 | QQuickParticleData::~QQuickParticleData() |
431 | { |
432 | delete v4Datum; |
433 | } |
434 | |
435 | QQuickParticleData::QQuickParticleData(const QQuickParticleData &other) |
436 | { |
437 | *this = other; |
438 | } |
439 | |
440 | QQuickParticleData &QQuickParticleData::operator=(const QQuickParticleData &other) |
441 | { |
442 | clone(other); |
443 | |
444 | groupId = other.groupId; |
445 | index = other.index; |
446 | systemIndex = other.systemIndex; |
447 | // Lazily initialized |
448 | v4Datum = nullptr; |
449 | |
450 | return *this; |
451 | } |
452 | |
453 | void QQuickParticleData::clone(const QQuickParticleData& other) |
454 | { |
455 | x = other.x; |
456 | y = other.y; |
457 | t = other.t; |
458 | lifeSpan = other.lifeSpan; |
459 | size = other.size; |
460 | endSize = other.endSize; |
461 | vx = other.vx; |
462 | vy = other.vy; |
463 | ax = other.ax; |
464 | ay = other.ay; |
465 | xx = other.xx; |
466 | xy = other.xy; |
467 | yx = other.yx; |
468 | yy = other.yy; |
469 | rotation = other.rotation; |
470 | rotationVelocity = other.rotationVelocity; |
471 | autoRotate = other.autoRotate; |
472 | animIdx = other.animIdx; |
473 | frameDuration = other.frameDuration; |
474 | frameCount = other.frameCount; |
475 | animT = other.animT; |
476 | animX = other.animX; |
477 | animY = other.animY; |
478 | animWidth = other.animWidth; |
479 | animHeight = other.animHeight; |
480 | color = other.color; |
481 | delegate = other.delegate; |
482 | |
483 | colorOwner = other.colorOwner; |
484 | rotationOwner = other.rotationOwner; |
485 | deformationOwner = other.deformationOwner; |
486 | animationOwner = other.animationOwner; |
487 | } |
488 | |
489 | QV4::ReturnedValue QQuickParticleData::v4Value(QQuickParticleSystem* particleSystem) |
490 | { |
491 | if (!v4Datum) |
492 | v4Datum = new QQuickV4ParticleData(qmlEngine(particleSystem)->handle(), this, particleSystem); |
493 | return v4Datum->v4Value(); |
494 | } |
495 | |
496 | void QQuickParticleData::debugDump(QQuickParticleSystem* particleSystem) const |
497 | { |
498 | qDebug() << "Particle" << systemIndex << groupId << "/" << index << stillAlive(system: particleSystem) |
499 | << "Pos: " << x << "," << y |
500 | << "Vel: " << vx << "," << vy |
501 | << "Acc: " << ax << "," << ay |
502 | << "Size: " << size << "," << endSize |
503 | << "Time: " << t << "," <<lifeSpan << ";" << (particleSystem->timeInt / 1000.0) ; |
504 | } |
505 | |
506 | void QQuickParticleData::extendLife(float time, QQuickParticleSystem* particleSystem) |
507 | { |
508 | qreal newX = curX(particleSystem); |
509 | qreal newY = curY(particleSystem); |
510 | qreal newVX = curVX(particleSystem); |
511 | qreal newVY = curVY(particleSystem); |
512 | |
513 | t += time; |
514 | animT += time; |
515 | |
516 | qreal elapsed = (particleSystem->timeInt / 1000.0) - t; |
517 | qreal evy = newVY - elapsed*ay; |
518 | qreal ey = newY - elapsed*evy - 0.5 * elapsed*elapsed*ay; |
519 | qreal evx = newVX - elapsed*ax; |
520 | qreal ex = newX - elapsed*evx - 0.5 * elapsed*elapsed*ax; |
521 | |
522 | x = ex; |
523 | vx = evx; |
524 | y = ey; |
525 | vy = evy; |
526 | } |
527 | |
528 | QQuickParticleSystem::QQuickParticleSystem(QQuickItem *parent) : |
529 | QQuickItem(parent), |
530 | stateEngine(nullptr), |
531 | nextFreeGroupId(0), |
532 | m_animation(nullptr), |
533 | m_running(true), |
534 | initialized(0), |
535 | particleCount(0), |
536 | m_nextIndex(0), |
537 | m_componentComplete(false), |
538 | m_paused(false), |
539 | m_empty(true) |
540 | { |
541 | m_debugMode = qmlParticlesDebug(); |
542 | } |
543 | |
544 | QQuickParticleSystem::~QQuickParticleSystem() |
545 | { |
546 | for (QQuickParticleGroupData *gd : std::as_const(t&: groupData)) |
547 | delete gd; |
548 | } |
549 | |
550 | void QQuickParticleSystem::initGroups() |
551 | { |
552 | m_reusableIndexes.clear(); |
553 | m_nextIndex = 0; |
554 | |
555 | qDeleteAll(c: groupData); |
556 | groupData.clear(); |
557 | groupIds.clear(); |
558 | nextFreeGroupId = 0; |
559 | |
560 | for (auto e : std::as_const(t&: m_emitters)) { |
561 | e->reclaculateGroupId(); |
562 | } |
563 | for (QQuickParticlePainter *p : std::as_const(t&: m_painters)) { |
564 | p->recalculateGroupIds(); |
565 | } |
566 | |
567 | QQuickParticleGroupData *pd = new QQuickParticleGroupData(QString(), this); // Default group |
568 | Q_ASSERT(pd->index == 0); |
569 | Q_UNUSED(pd); |
570 | } |
571 | |
572 | void QQuickParticleSystem::registerParticlePainter(QQuickParticlePainter* p) |
573 | { |
574 | if (m_debugMode) |
575 | qDebug() << "Registering Painter" << p << "to" << this; |
576 | //TODO: a way to Unregister emitters, painters and affectors |
577 | m_painters << QPointer<QQuickParticlePainter>(p);//###Set or uniqueness checking? |
578 | |
579 | connect(sender: p, signal: &QQuickParticlePainter::groupsChanged, context: this, slot: [this, p] { this->loadPainter(p); }, type: Qt::QueuedConnection); |
580 | loadPainter(p); |
581 | } |
582 | |
583 | void QQuickParticleSystem::registerParticleEmitter(QQuickParticleEmitter* e) |
584 | { |
585 | if (m_debugMode) |
586 | qDebug() << "Registering Emitter" << e << "to" << this; |
587 | m_emitters << QPointer<QQuickParticleEmitter>(e);//###How to get them out? |
588 | } |
589 | |
590 | void QQuickParticleSystem::finishRegisteringParticleEmitter(QQuickParticleEmitter* e) |
591 | { |
592 | connect(sender: e, SIGNAL(particleCountChanged()), |
593 | receiver: this, SLOT(emittersChanged())); |
594 | connect(sender: e, SIGNAL(groupChanged(QString)), |
595 | receiver: this, SLOT(emittersChanged())); |
596 | if (m_componentComplete) |
597 | emittersChanged(); |
598 | e->reset();//Start, so that starttime factors appropriately |
599 | } |
600 | |
601 | void QQuickParticleSystem::registerParticleAffector(QQuickParticleAffector* a) |
602 | { |
603 | if (m_debugMode) |
604 | qDebug() << "Registering Affector" << a << "to" << this; |
605 | if (!m_affectors.contains(t: a)) |
606 | m_affectors << QPointer<QQuickParticleAffector>(a); |
607 | } |
608 | |
609 | void QQuickParticleSystem::registerParticleGroup(QQuickParticleGroup* g) |
610 | { |
611 | if (m_debugMode) |
612 | qDebug() << "Registering Group" << g << "to" << this; |
613 | m_groups << QPointer<QQuickParticleGroup>(g); |
614 | createEngine(); |
615 | } |
616 | |
617 | void QQuickParticleSystem::setRunning(bool arg) |
618 | { |
619 | if (m_running != arg) { |
620 | m_running = arg; |
621 | emit runningChanged(arg); |
622 | setPaused(false); |
623 | if (m_animation)//Not created until componentCompleted |
624 | m_running ? m_animation->start() : m_animation->stop(); |
625 | reset(); |
626 | } |
627 | } |
628 | |
629 | void QQuickParticleSystem::setPaused(bool arg) { |
630 | if (m_paused != arg) { |
631 | m_paused = arg; |
632 | if (m_animation && m_animation->state() != QAbstractAnimation::Stopped) |
633 | m_paused ? m_animation->pause() : m_animation->resume(); |
634 | if (!m_paused) { |
635 | for (QQuickParticlePainter *p : std::as_const(t&: m_painters)) { |
636 | if (p) { |
637 | p->update(); |
638 | } |
639 | } |
640 | } |
641 | emit pausedChanged(arg); |
642 | } |
643 | } |
644 | |
645 | void QQuickParticleSystem::statePropertyRedirect(QQmlListProperty<QObject> *prop, QObject *value) |
646 | { |
647 | //Hooks up automatic state-associated stuff |
648 | QQuickParticleSystem* sys = qobject_cast<QQuickParticleSystem*>(object: prop->object->parent()); |
649 | QQuickParticleGroup* group = qobject_cast<QQuickParticleGroup*>(object: prop->object); |
650 | if (!group || !sys || !value) |
651 | return; |
652 | stateRedirect(group, sys, value); |
653 | } |
654 | |
655 | void QQuickParticleSystem::stateRedirect(QQuickParticleGroup* group, QQuickParticleSystem* sys, QObject *value) |
656 | { |
657 | QStringList list; |
658 | list << group->name(); |
659 | QQuickParticleAffector* a = qobject_cast<QQuickParticleAffector*>(object: value); |
660 | if (a) { |
661 | a->setParentItem(sys); |
662 | a->setGroups(list); |
663 | a->setSystem(sys); |
664 | return; |
665 | } |
666 | QQuickTrailEmitter* fe = qobject_cast<QQuickTrailEmitter*>(object: value); |
667 | if (fe) { |
668 | fe->setParentItem(sys); |
669 | fe->setFollow(group->name()); |
670 | fe->setSystem(sys); |
671 | return; |
672 | } |
673 | QQuickParticleEmitter* e = qobject_cast<QQuickParticleEmitter*>(object: value); |
674 | if (e) { |
675 | e->setParentItem(sys); |
676 | e->setGroup(group->name()); |
677 | e->setSystem(sys); |
678 | return; |
679 | } |
680 | QQuickParticlePainter* p = qobject_cast<QQuickParticlePainter*>(object: value); |
681 | if (p) { |
682 | p->setParentItem(sys); |
683 | p->setGroups(list); |
684 | p->setSystem(sys); |
685 | return; |
686 | } |
687 | qWarning() << value << " was placed inside a particle system state but cannot be taken into the particle system. It will be lost." ; |
688 | } |
689 | |
690 | |
691 | int QQuickParticleSystem::registerParticleGroupData(const QString &name, QQuickParticleGroupData *pgd) |
692 | { |
693 | Q_ASSERT(!groupIds.contains(name)); |
694 | int id; |
695 | if (nextFreeGroupId >= groupData.size()) { |
696 | groupData.push_back(t: pgd); |
697 | nextFreeGroupId = groupData.size(); |
698 | id = nextFreeGroupId - 1; |
699 | } else { |
700 | id = nextFreeGroupId; |
701 | groupData[id] = pgd; |
702 | searchNextFreeGroupId(); |
703 | } |
704 | groupIds.insert(key: name, value: id); |
705 | return id; |
706 | } |
707 | |
708 | void QQuickParticleSystem::searchNextFreeGroupId() |
709 | { |
710 | ++nextFreeGroupId; |
711 | for (int ei = groupData.size(); nextFreeGroupId != ei; ++nextFreeGroupId) { |
712 | if (groupData[nextFreeGroupId] == nullptr) { |
713 | return; |
714 | } |
715 | } |
716 | } |
717 | |
718 | void QQuickParticleSystem::componentComplete() |
719 | |
720 | { |
721 | QQuickItem::componentComplete(); |
722 | m_componentComplete = true; |
723 | m_animation = new QQuickParticleSystemAnimation(this); |
724 | reset();//restarts animation as well |
725 | } |
726 | |
727 | void QQuickParticleSystem::reset() |
728 | { |
729 | if (!m_componentComplete) |
730 | return; |
731 | |
732 | timeInt = 0; |
733 | //Clear guarded pointers which have been deleted |
734 | m_emitters.removeAll(t: nullptr); |
735 | m_painters.removeAll(t: nullptr); |
736 | m_affectors.removeAll(t: nullptr); |
737 | |
738 | bySysIdx.resize(size: 0); |
739 | initGroups();//Also clears all logical particles |
740 | |
741 | if (!m_running) |
742 | return; |
743 | |
744 | for (QQuickParticleEmitter *e : std::as_const(t&: m_emitters)) |
745 | e->reset(); |
746 | |
747 | emittersChanged(); |
748 | |
749 | for (QQuickParticlePainter *p : std::as_const(t&: m_painters)) { |
750 | loadPainter(p); |
751 | p->reset(); |
752 | } |
753 | |
754 | //### Do affectors need reset too? |
755 | if (m_animation) {//Animation is explicitly disabled in benchmarks |
756 | //reset restarts animation (if running) |
757 | if ((m_animation->state() == QAbstractAnimation::Running)) |
758 | m_animation->stop(); |
759 | m_animation->start(); |
760 | if (m_paused) |
761 | m_animation->pause(); |
762 | } |
763 | |
764 | initialized = true; |
765 | } |
766 | |
767 | |
768 | void QQuickParticleSystem::loadPainter(QQuickParticlePainter *painter) |
769 | { |
770 | if (!m_componentComplete || !painter) |
771 | return; |
772 | |
773 | for (QQuickParticleGroupData *sg : groupData) { |
774 | sg->painters.removeOne(element: painter); |
775 | } |
776 | |
777 | int particleCount = 0; |
778 | if (painter->groups().isEmpty()) {//Uses default particle |
779 | static QStringList def = QStringList() << QString(); |
780 | painter->setGroups(def); |
781 | particleCount += groupData[0]->size(); |
782 | groupData[0]->painters << painter; |
783 | } else { |
784 | for (auto groupId : painter->groupIds()) { |
785 | QQuickParticleGroupData *gd = groupData[groupId]; |
786 | particleCount += gd->size(); |
787 | gd->painters << painter; |
788 | } |
789 | } |
790 | painter->setCount(particleCount); |
791 | painter->update();//Initial update here |
792 | return; |
793 | } |
794 | |
795 | void QQuickParticleSystem::emittersChanged() |
796 | { |
797 | if (!m_componentComplete) |
798 | return; |
799 | |
800 | QVector<int> previousSizes; |
801 | QVector<int> newSizes; |
802 | previousSizes.reserve(size: groupData.size()); |
803 | newSizes.reserve(size: groupData.size()); |
804 | for (int i = 0, ei = groupData.size(); i != ei; ++i) { |
805 | previousSizes << groupData[i]->size(); |
806 | newSizes << 0; |
807 | } |
808 | |
809 | // Populate groups and set sizes. |
810 | for (int i = 0; i < m_emitters.size(); ) { |
811 | QQuickParticleEmitter *e = m_emitters.at(i); |
812 | if (!e) { |
813 | m_emitters.removeAt(i); |
814 | continue; |
815 | } |
816 | |
817 | int groupId = e->groupId(); |
818 | if (groupId == QQuickParticleGroupData::InvalidID) { |
819 | groupId = (new QQuickParticleGroupData(e->group(), this))->index; |
820 | previousSizes << 0; |
821 | newSizes << 0; |
822 | } |
823 | newSizes[groupId] += e->particleCount(); |
824 | //###: Cull emptied groups? |
825 | |
826 | ++i; |
827 | } |
828 | |
829 | //TODO: Garbage collection? |
830 | particleCount = 0; |
831 | for (int i = 0, ei = groupData.size(); i != ei; ++i) { |
832 | groupData[i]->setSize(qMax(a: newSizes[i], b: previousSizes[i])); |
833 | particleCount += groupData[i]->size(); |
834 | } |
835 | |
836 | if (m_debugMode) |
837 | qDebug() << "Particle system emitters changed. New particle count: " << particleCount << "in" << groupData.size() << "groups." ; |
838 | |
839 | if (particleCount > bySysIdx.size())//New datum requests haven't updated it |
840 | bySysIdx.resize(size: particleCount); |
841 | |
842 | for (QQuickParticleAffector *a : std::as_const(t&: m_affectors)) {//Groups may have changed |
843 | if (a) { |
844 | a->m_updateIntSet = true; |
845 | } |
846 | } |
847 | |
848 | for (QQuickParticlePainter *p : std::as_const(t&: m_painters)) |
849 | loadPainter(painter: p); |
850 | |
851 | if (!m_groups.isEmpty()) |
852 | createEngine(); |
853 | |
854 | } |
855 | |
856 | void QQuickParticleSystem::createEngine() |
857 | { |
858 | if (!m_componentComplete) |
859 | return; |
860 | if (stateEngine && m_debugMode) |
861 | qDebug() << "Resetting Existing Sprite Engine..." ; |
862 | //### Solve the losses if size/states go down |
863 | for (QQuickParticleGroup *group : std::as_const(t&: m_groups)) { |
864 | bool exists = false; |
865 | for (auto it = groupIds.keyBegin(), end = groupIds.keyEnd(); it != end; ++it) { |
866 | if (group->name() == *it) { |
867 | exists = true; |
868 | break; |
869 | } |
870 | } |
871 | if (!exists) { |
872 | new QQuickParticleGroupData(group->name(), this); |
873 | } |
874 | } |
875 | |
876 | if (m_groups.size()) { |
877 | //Reorder groups List so as to have the same order as groupData |
878 | // TODO: can't we just merge the two lists? |
879 | QList<QQuickParticleGroup*> newList; |
880 | for (int i = 0, ei = groupData.size(); i != ei; ++i) { |
881 | bool exists = false; |
882 | QString name = groupData[i]->name(); |
883 | for (QQuickParticleGroup *existing : std::as_const(t&: m_groups)) { |
884 | if (existing->name() == name) { |
885 | newList << existing; |
886 | exists = true; |
887 | } |
888 | } |
889 | if (!exists) { |
890 | newList << new QQuickParticleGroup(this); |
891 | newList.back()->setName(name); |
892 | } |
893 | } |
894 | m_groups = newList; |
895 | QList<QQuickStochasticState*> states; |
896 | states.reserve(size: m_groups.size()); |
897 | for (QQuickParticleGroup *g : std::as_const(t&: m_groups)) |
898 | states << (QQuickStochasticState*)g; |
899 | |
900 | if (!stateEngine) |
901 | stateEngine = new QQuickStochasticEngine(this); |
902 | stateEngine->setCount(particleCount); |
903 | stateEngine->m_states = states; |
904 | |
905 | connect(sender: stateEngine, SIGNAL(stateChanged(int)), |
906 | receiver: this, SLOT(particleStateChange(int))); |
907 | |
908 | } else { |
909 | if (stateEngine) |
910 | delete stateEngine; |
911 | stateEngine = nullptr; |
912 | } |
913 | |
914 | } |
915 | |
916 | void QQuickParticleSystem::particleStateChange(int idx) |
917 | { |
918 | moveGroups(d: bySysIdx[idx], newGIdx: stateEngine->curState(index: idx)); |
919 | } |
920 | |
921 | void QQuickParticleSystem::moveGroups(QQuickParticleData *d, int newGIdx) |
922 | { |
923 | if (!d || newGIdx == d->groupId) |
924 | return; |
925 | |
926 | QQuickParticleData* pd = newDatum(groupId: newGIdx, respectLimits: false, sysIdx: d->systemIndex); |
927 | if (!pd) |
928 | return; |
929 | |
930 | pd->clone(other: *d); |
931 | finishNewDatum(pd); |
932 | |
933 | d->systemIndex = -1; |
934 | groupData[d->groupId]->kill(d); |
935 | } |
936 | |
937 | int QQuickParticleSystem::nextSystemIndex() |
938 | { |
939 | if (!m_reusableIndexes.isEmpty()) { |
940 | int ret = *(m_reusableIndexes.begin()); |
941 | m_reusableIndexes.remove(value: ret); |
942 | return ret; |
943 | } |
944 | if (m_nextIndex >= bySysIdx.size()) { |
945 | bySysIdx.resize(size: bySysIdx.size() < 10 ? 10 : bySysIdx.size()*1.1);//###+1,10%,+10? Choose something non-arbitrarily |
946 | if (stateEngine) |
947 | stateEngine->setCount(bySysIdx.size()); |
948 | |
949 | } |
950 | return m_nextIndex++; |
951 | } |
952 | |
953 | QQuickParticleData* QQuickParticleSystem::newDatum(int groupId, bool respectLimits, int sysIndex) |
954 | { |
955 | Q_ASSERT(groupId < groupData.size());//XXX shouldn't really be an assert |
956 | |
957 | QQuickParticleData* ret = groupData[groupId]->newDatum(respectsLimits: respectLimits); |
958 | if (!ret) { |
959 | return nullptr; |
960 | } |
961 | if (sysIndex == -1) { |
962 | if (ret->systemIndex == -1) |
963 | ret->systemIndex = nextSystemIndex(); |
964 | } else { |
965 | if (ret->systemIndex != -1) { |
966 | if (stateEngine) |
967 | stateEngine->stop(index: ret->systemIndex); |
968 | m_reusableIndexes << ret->systemIndex; |
969 | bySysIdx[ret->systemIndex] = 0; |
970 | } |
971 | ret->systemIndex = sysIndex; |
972 | } |
973 | bySysIdx[ret->systemIndex] = ret; |
974 | |
975 | if (stateEngine) |
976 | stateEngine->start(index: ret->systemIndex, state: ret->groupId); |
977 | |
978 | m_empty = false; |
979 | return ret; |
980 | } |
981 | |
982 | void QQuickParticleSystem::emitParticle(QQuickParticleData* pd, QQuickParticleEmitter* particleEmitter) |
983 | {// called from prepareNextFrame()->emitWindow - enforce? |
984 | //Account for relative emitter position |
985 | bool okay = false; |
986 | QTransform t = particleEmitter->itemTransform(this, &okay); |
987 | if (okay) { |
988 | qreal tx,ty; |
989 | t.map(x: pd->x, y: pd->y, tx: &tx, ty: &ty); |
990 | pd->x = tx; |
991 | pd->y = ty; |
992 | } |
993 | |
994 | finishNewDatum(pd); |
995 | } |
996 | |
997 | void QQuickParticleSystem::finishNewDatum(QQuickParticleData *pd) |
998 | { |
999 | Q_ASSERT(pd); |
1000 | groupData[pd->groupId]->prepareRecycler(d: pd); |
1001 | |
1002 | for (QQuickParticleAffector *a : std::as_const(t&: m_affectors)) |
1003 | if (a && a->m_needsReset) |
1004 | a->reset(pd); |
1005 | for (QQuickParticlePainter *p : std::as_const(t&: groupData[pd->groupId]->painters)) |
1006 | if (p) |
1007 | p->load(pd); |
1008 | } |
1009 | |
1010 | void QQuickParticleSystem::updateCurrentTime( int currentTime ) |
1011 | { |
1012 | if (!initialized) |
1013 | return;//error in initialization |
1014 | |
1015 | //### Elapsed time never shrinks - may cause problems if left emitting for weeks at a time. |
1016 | qreal dt = timeInt / 1000.; |
1017 | timeInt = currentTime; |
1018 | qreal time = timeInt / 1000.; |
1019 | dt = time - dt; |
1020 | needsReset.clear(); |
1021 | |
1022 | m_emitters.removeAll(t: nullptr); |
1023 | m_painters.removeAll(t: nullptr); |
1024 | m_affectors.removeAll(t: nullptr); |
1025 | |
1026 | bool oldClear = m_empty; |
1027 | m_empty = true; |
1028 | for (QQuickParticleGroupData *gd : std::as_const(t&: groupData))//Recycle all groups and see if they're out of live particles |
1029 | m_empty = gd->recycle() && m_empty; |
1030 | |
1031 | if (stateEngine) |
1032 | stateEngine->updateSprites(time: timeInt); |
1033 | |
1034 | for (QQuickParticleEmitter *emitter : std::as_const(t&: m_emitters)) |
1035 | emitter->emitWindow(timeStamp: timeInt); |
1036 | for (QQuickParticleAffector *a : std::as_const(t&: m_affectors)) |
1037 | a->affectSystem(dt); |
1038 | for (QQuickParticleData *d : needsReset) |
1039 | for (QQuickParticlePainter *p : std::as_const(t&: groupData[d->groupId]->painters)) |
1040 | p->reload(d); |
1041 | |
1042 | if (oldClear != m_empty) |
1043 | emptyChanged(arg: m_empty); |
1044 | } |
1045 | |
1046 | int QQuickParticleSystem::systemSync(QQuickParticlePainter* p) |
1047 | { |
1048 | if (!m_running) |
1049 | return 0; |
1050 | if (!initialized) |
1051 | return 0;//error in initialization |
1052 | p->performPendingCommits(); |
1053 | return timeInt; |
1054 | } |
1055 | |
1056 | |
1057 | QT_END_NAMESPACE |
1058 | |
1059 | #include "moc_qquickparticlesystem_p.cpp" |
1060 | |