| 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, context: &m_loggingTimer, 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 | |