From 5c1f8eb094de6431b8427f9f90cd04620204fa9e Mon Sep 17 00:00:00 2001 From: Brian Osman Date: Thu, 14 Feb 2019 14:49:55 -0500 Subject: [PATCH] Various particle system improvements Effects now have a duration, and can be played looped or one-shot. Added a second list of affectors that are applied at spawn vs. update. Effects grab and store the SkRandom at construction, so it no longer needs to be passed to update(). Bug: skia: Change-Id: Ib54d60466e162e4d4b70fa64c1215fc01680d47a Reviewed-on: https://skia-review.googlesource.com/c/191722 Commit-Queue: Brian Osman Reviewed-by: Michael Ludwig Reviewed-by: Brian Osman --- modules/particles/include/SkParticleEffect.h | 20 +++- modules/particles/src/SkParticleEffect.cpp | 108 ++++++++++++------- resources/particles/default.json | 2 +- resources/particles/explosion.json | 2 +- resources/particles/penguin_cannon.json | 2 +- resources/particles/snowfall.json | 2 +- resources/particles/swirl.json | 2 +- resources/particles/warp.json | 2 +- tools/viewer/ParticlesSlide.cpp | 13 ++- tools/viewer/ParticlesSlide.h | 2 + 10 files changed, 104 insertions(+), 51 deletions(-) diff --git a/modules/particles/include/SkParticleEffect.h b/modules/particles/include/SkParticleEffect.h index 83d5fb0896..34d3c51b61 100644 --- a/modules/particles/include/SkParticleEffect.h +++ b/modules/particles/include/SkParticleEffect.h @@ -44,6 +44,7 @@ struct InitialVelocityParams { class SkParticleEffectParams : public SkRefCnt { public: int fMaxCount = 128; + float fEffectDuration = 1.0f; float fRate = 8.0f; SkRangedFloat fLifetime = { 1.0f, 1.0f }; SkColor4f fStartColor = { 1.0f, 1.0f, 1.0f, 1.0f }; @@ -64,19 +65,25 @@ public: // Emitter shape & parameters sk_sp fEmitter; - // Update rules - SkTArray> fAffectors; + // Rules that configure particles at spawn time + SkTArray> fSpawnAffectors; + + // Rules that update existing particles over their lifetime + SkTArray> fUpdateAffectors; void visitFields(SkFieldVisitor* v); }; class SkParticleEffect : public SkRefCnt { public: - SkParticleEffect(sk_sp params); + SkParticleEffect(sk_sp params, const SkRandom& random); - void update(SkRandom& random, const SkAnimTimer& timer); + void start(const SkAnimTimer& timer, bool looping = false); + void update(const SkAnimTimer& timer); void draw(SkCanvas* canvas); + bool isAlive() { return fSpawnTime >= 0; } + SkParticleEffectParams* getParams() { return fParams.get(); } private: @@ -106,6 +113,11 @@ private: sk_sp fImage; SkRect fImageRect; + SkRandom fRandom; + + bool fLooping; + double fSpawnTime; + int fCount; double fLastTime; float fSpawnRemainder; diff --git a/modules/particles/src/SkParticleEffect.cpp b/modules/particles/src/SkParticleEffect.cpp index faef1a6b05..b43122850f 100644 --- a/modules/particles/src/SkParticleEffect.cpp +++ b/modules/particles/src/SkParticleEffect.cpp @@ -44,6 +44,7 @@ void InitialVelocityParams::visitFields(SkFieldVisitor* v) { void SkParticleEffectParams::visitFields(SkFieldVisitor* v) { v->visit("MaxCount", fMaxCount); + v->visit("Duration", fEffectDuration); v->visit("Rate", fRate); v->visit("Life", fLifetime); v->visit("StartColor", fStartColor); @@ -58,13 +59,17 @@ void SkParticleEffectParams::visitFields(SkFieldVisitor* v) { v->visit("Emitter", fEmitter); - v->visit("Affectors", fAffectors); + v->visit("Spawn", fSpawnAffectors); + v->visit("Update", fUpdateAffectors); } -SkParticleEffect::SkParticleEffect(sk_sp params) +SkParticleEffect::SkParticleEffect(sk_sp params, const SkRandom& random) : fParams(std::move(params)) + , fRandom(random) + , fLooping(false) + , fSpawnTime(-1.0) , fCount(0) - , fLastTime(-1.0f) + , fLastTime(-1.0) , fSpawnRemainder(0.0f) { this->setCapacity(fParams->fMaxCount); @@ -82,8 +87,15 @@ SkParticleEffect::SkParticleEffect(sk_sp params) fImageRect = SkRect::MakeIWH(w / fParams->fImageCols, h / fParams->fImageRows); } -void SkParticleEffect::update(SkRandom& random, const SkAnimTimer& timer) { - if (!timer.isRunning()) { +void SkParticleEffect::start(const SkAnimTimer& timer, bool looping) { + fCount = 0; + fLastTime = fSpawnTime = timer.secs(); + fSpawnRemainder = 0.0f; + fLooping = looping; +} + +void SkParticleEffect::update(const SkAnimTimer& timer) { + if (!timer.isRunning() || !this->isAlive()) { return; } @@ -93,12 +105,6 @@ void SkParticleEffect::update(SkRandom& random, const SkAnimTimer& timer) { } double now = timer.secs(); - - if (fLastTime < 0) { - // Hack: kick us off with 1/30th of a second on first update - fLastTime = now - (1.0 / 30); - } - float deltaTime = static_cast(now - fLastTime); fLastTime = now; @@ -107,20 +113,56 @@ void SkParticleEffect::update(SkRandom& random, const SkAnimTimer& timer) { SkParticleUpdateParams updateParams; updateParams.fDeltaTime = deltaTime; - updateParams.fRandom = &random; + updateParams.fRandom = &fRandom; - // Age/update old particles + // Remove particles that have reached their end of life for (int i = 0; i < fCount; ++i) { if (now > fParticles[i].fTimeOfDeath) { // NOTE: This is fast, but doesn't preserve drawing order. Could be a problem... - fParticles[i] = fParticles[fCount - 1]; + fParticles[i] = fParticles[fCount - 1]; fSpriteRects[i] = fSpriteRects[fCount - 1]; - fColors[i] = fColors[fCount - 1]; + fColors[i] = fColors[fCount - 1]; --i; --fCount; - continue; + } + } + + // Spawn new particles + float desired = fParams->fRate * deltaTime + fSpawnRemainder; + int numToSpawn = sk_float_round2int(desired); + fSpawnRemainder = desired - numToSpawn; + numToSpawn = SkTPin(numToSpawn, 0, fParams->fMaxCount - fCount); + if (fParams->fEmitter) { + for (int i = 0; i < numToSpawn; ++i) { + fParticles[fCount].fTimeOfBirth = now; + fParticles[fCount].fTimeOfDeath = now + fParams->fLifetime.eval(fRandom); + fParticles[fCount].fPV.fPose = fParams->fEmitter->emit(fRandom); + fParticles[fCount].fPV.fVelocity = fParams->fVelocity.eval(fRandom); + fParticles[fCount].fStableRandom = fRandom; + fSpriteRects[fCount] = this->spriteRect(0); + fCount++; } + // No, this isn't "stable", but spawn affectors are only run once anyway. + // Would it ever make sense to give the same random to all particles spawned on a given + // frame? Having a hard time thinking when that would be useful. + updateParams.fStableRandom = &fRandom; + // ... and this isn't "particle" t, it's effect t. + double t = (now - fSpawnTime) / fParams->fEffectDuration; + updateParams.fParticleT = static_cast(fLooping ? fmod(t, 1.0) : SkTPin(t, 0.0, 1.0)); + + // Apply spawn affectors + for (int i = fCount - numToSpawn; i < fCount; ++i) { + for (auto affector : fParams->fSpawnAffectors) { + if (affector) { + affector->apply(updateParams, fParticles[i].fPV); + } + } + } + } + + // Apply update rules + for (int i = 0; i < fCount; ++i) { // Compute fraction of lifetime that's elapsed float t = static_cast((now - fParticles[i].fTimeOfBirth) / (fParticles[i].fTimeOfDeath - fParticles[i].fTimeOfBirth)); @@ -136,7 +178,7 @@ void SkParticleEffect::update(SkRandom& random, const SkAnimTimer& timer) { // Set color by lifetime fColors[i] = Sk4f_toL32(swizzle_rb(startColor + (colorScale * t))); - for (auto affector : fParams->fAffectors) { + for (auto affector : fParams->fUpdateAffectors) { if (affector) { affector->apply(updateParams, fParticles[i].fPV); } @@ -154,35 +196,25 @@ void SkParticleEffect::update(SkRandom& random, const SkAnimTimer& timer) { oldHeading.fX * s + oldHeading.fY * c }; } - // Spawn new particles - float desired = fParams->fRate * deltaTime + fSpawnRemainder; - int numToSpawn = sk_float_round2int(desired); - fSpawnRemainder = desired - numToSpawn; - numToSpawn = SkTPin(numToSpawn, 0, fParams->fMaxCount - fCount); - if (fParams->fEmitter) { - for (int i = 0; i < numToSpawn; ++i) { - fParticles[fCount].fTimeOfBirth = now; - fParticles[fCount].fTimeOfDeath = now + fParams->fLifetime.eval(random); - fParticles[fCount].fPV.fPose = fParams->fEmitter->emit(random); - fParticles[fCount].fPV.fVelocity = fParams->fVelocity.eval(random); - fParticles[fCount].fStableRandom = random; - fSpriteRects[fCount] = this->spriteRect(0); - fCount++; - } - } - // Re-generate all xforms SkPoint ofs = this->spriteCenter(); for (int i = 0; i < fCount; ++i) { fXforms[i] = fParticles[i].fPV.fPose.asRSXform(ofs); } + + // Mark effect as dead if we've reached the end (and are not looping) + if (!fLooping && (now - fSpawnTime) > fParams->fEffectDuration) { + fSpawnTime = -1.0; + } } void SkParticleEffect::draw(SkCanvas* canvas) { - SkPaint paint; - paint.setFilterQuality(SkFilterQuality::kMedium_SkFilterQuality); - canvas->drawAtlas(fImage, fXforms.get(), fSpriteRects.get(), fColors.get(), fCount, - SkBlendMode::kModulate, nullptr, &paint); + if (this->isAlive()) { + SkPaint paint; + paint.setFilterQuality(SkFilterQuality::kMedium_SkFilterQuality); + canvas->drawAtlas(fImage, fXforms.get(), fSpriteRects.get(), fColors.get(), fCount, + SkBlendMode::kModulate, nullptr, &paint); + } } void SkParticleEffect::setCapacity(int capacity) { diff --git a/resources/particles/default.json b/resources/particles/default.json index 6da123cdb1..0ae3f0ea58 100644 --- a/resources/particles/default.json +++ b/resources/particles/default.json @@ -40,7 +40,7 @@ "Text": "SKIA", "FontSize": 96 }, - "Affectors": [ + "Update": [ { "Type": "SkDirectionalForceAffector", "Force": { "x": 0, "y": 0 } diff --git a/resources/particles/explosion.json b/resources/particles/explosion.json index ae6ddfde77..c2abee3478 100644 --- a/resources/particles/explosion.json +++ b/resources/particles/explosion.json @@ -40,7 +40,7 @@ "Center": { "x": 200, "y": 200 }, "Radius": 60 }, - "Affectors": [ + "Update": [ { "Type": "SkPointForceAffector", "Point": { "x": 200, "y": 200 }, diff --git a/resources/particles/penguin_cannon.json b/resources/particles/penguin_cannon.json index 303c31d485..d69c2aa2e0 100644 --- a/resources/particles/penguin_cannon.json +++ b/resources/particles/penguin_cannon.json @@ -29,7 +29,7 @@ "P1": { "x": 237, "y": 396 }, "P2": { "x": 214, "y": 398 } }, - "Affectors": [ + "Update": [ { "Type": "SkDirectionalForceAffector", "Force": { "x": 0, "y": 50 } diff --git a/resources/particles/snowfall.json b/resources/particles/snowfall.json index ddfe92eb27..865893f679 100644 --- a/resources/particles/snowfall.json +++ b/resources/particles/snowfall.json @@ -40,7 +40,7 @@ "P1": { "x": 61, "y": 34 }, "P2": { "x": 606, "y": 32 } }, - "Affectors": [ + "Update": [ { "Type": "SkJitterAffector", "X": { diff --git a/resources/particles/swirl.json b/resources/particles/swirl.json index 25f8ac9e5a..8f5827279c 100644 --- a/resources/particles/swirl.json +++ b/resources/particles/swirl.json @@ -40,7 +40,7 @@ "P1": { "x": 200, "y": 200 }, "P2": { "x": 250, "y": 200 } }, - "Affectors": [ + "Update": [ { "Type": "SkRangedForceAffector", "Angle": { diff --git a/resources/particles/warp.json b/resources/particles/warp.json index 8d40876b09..4645e0dfe5 100644 --- a/resources/particles/warp.json +++ b/resources/particles/warp.json @@ -40,7 +40,7 @@ "Center": { "x": 380.8, "y": 273.92 }, "Radius": 43 }, - "Affectors": [ + "Update": [ { "Type": "SkPointForceAffector", "Point": { "x": 375, "y": 273 }, diff --git a/tools/viewer/ParticlesSlide.cpp b/tools/viewer/ParticlesSlide.cpp index a948c7e0b7..4d4cc252fe 100644 --- a/tools/viewer/ParticlesSlide.cpp +++ b/tools/viewer/ParticlesSlide.cpp @@ -254,7 +254,8 @@ ParticlesSlide::ParticlesSlide() { } void ParticlesSlide::load(SkScalar winWidth, SkScalar winHeight) { - fEffect.reset(new SkParticleEffect(LoadEffectParams("resources/particles/default.json"))); + fEffect.reset(new SkParticleEffect(LoadEffectParams("resources/particles/default.json"), + fRandom)); } void ParticlesSlide::draw(SkCanvas* canvas) { @@ -262,11 +263,16 @@ void ParticlesSlide::draw(SkCanvas* canvas) { gDragPoints.reset(); if (ImGui::Begin("Particles")) { + static bool looped = true; + ImGui::Checkbox("Looped", &looped); + if (fTimer && ImGui::Button("Play")) { + fEffect->start(*fTimer, looped); + } static char filename[64] = "resources/particles/default.json"; ImGui::InputText("Filename", filename, sizeof(filename)); if (ImGui::Button("Load")) { if (auto newParams = LoadEffectParams(filename)) { - fEffect.reset(new SkParticleEffect(std::move(newParams))); + fEffect.reset(new SkParticleEffect(std::move(newParams), fRandom)); } } ImGui::SameLine(); @@ -309,7 +315,8 @@ void ParticlesSlide::draw(SkCanvas* canvas) { } bool ParticlesSlide::animate(const SkAnimTimer& timer) { - fEffect->update(fRandom, timer); + fTimer = &timer; + fEffect->update(timer); return true; } diff --git a/tools/viewer/ParticlesSlide.h b/tools/viewer/ParticlesSlide.h index 07c9cfaec1..23cfba9c27 100644 --- a/tools/viewer/ParticlesSlide.h +++ b/tools/viewer/ParticlesSlide.h @@ -13,6 +13,7 @@ #include "SkPath.h" #include "SkRandom.h" +class SkAnimTimer; class SkParticleEffect; class ParticlesSlide : public Slide { @@ -31,6 +32,7 @@ public: private: SkRandom fRandom; + const SkAnimTimer* fTimer; sk_sp fEffect; };