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 <brianosman@google.com>
Reviewed-by: Michael Ludwig <michaelludwig@google.com>
Reviewed-by: Brian Osman <brianosman@google.com>
This commit is contained in:
Brian Osman 2019-02-14 14:49:55 -05:00 committed by Skia Commit-Bot
parent f6d28e2a46
commit 5c1f8eb094
10 changed files with 104 additions and 51 deletions

View File

@ -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<SkParticleEmitter> fEmitter;
// Update rules
SkTArray<sk_sp<SkParticleAffector>> fAffectors;
// Rules that configure particles at spawn time
SkTArray<sk_sp<SkParticleAffector>> fSpawnAffectors;
// Rules that update existing particles over their lifetime
SkTArray<sk_sp<SkParticleAffector>> fUpdateAffectors;
void visitFields(SkFieldVisitor* v);
};
class SkParticleEffect : public SkRefCnt {
public:
SkParticleEffect(sk_sp<SkParticleEffectParams> params);
SkParticleEffect(sk_sp<SkParticleEffectParams> 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<SkImage> fImage;
SkRect fImageRect;
SkRandom fRandom;
bool fLooping;
double fSpawnTime;
int fCount;
double fLastTime;
float fSpawnRemainder;

View File

@ -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<SkParticleEffectParams> params)
SkParticleEffect::SkParticleEffect(sk_sp<SkParticleEffectParams> 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<SkParticleEffectParams> 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<float>(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<float>(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<float>((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) {

View File

@ -40,7 +40,7 @@
"Text": "SKIA",
"FontSize": 96
},
"Affectors": [
"Update": [
{
"Type": "SkDirectionalForceAffector",
"Force": { "x": 0, "y": 0 }

View File

@ -40,7 +40,7 @@
"Center": { "x": 200, "y": 200 },
"Radius": 60
},
"Affectors": [
"Update": [
{
"Type": "SkPointForceAffector",
"Point": { "x": 200, "y": 200 },

View File

@ -29,7 +29,7 @@
"P1": { "x": 237, "y": 396 },
"P2": { "x": 214, "y": 398 }
},
"Affectors": [
"Update": [
{
"Type": "SkDirectionalForceAffector",
"Force": { "x": 0, "y": 50 }

View File

@ -40,7 +40,7 @@
"P1": { "x": 61, "y": 34 },
"P2": { "x": 606, "y": 32 }
},
"Affectors": [
"Update": [
{
"Type": "SkJitterAffector",
"X": {

View File

@ -40,7 +40,7 @@
"P1": { "x": 200, "y": 200 },
"P2": { "x": 250, "y": 200 }
},
"Affectors": [
"Update": [
{
"Type": "SkRangedForceAffector",
"Angle": {

View File

@ -40,7 +40,7 @@
"Center": { "x": 380.8, "y": 273.92 },
"Radius": 43
},
"Affectors": [
"Update": [
{
"Type": "SkPointForceAffector",
"Point": { "x": 375, "y": 273 },

View File

@ -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;
}

View File

@ -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<SkParticleEffect> fEffect;
};