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:
parent
f6d28e2a46
commit
5c1f8eb094
@ -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;
|
||||
|
@ -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) {
|
||||
|
@ -40,7 +40,7 @@
|
||||
"Text": "SKIA",
|
||||
"FontSize": 96
|
||||
},
|
||||
"Affectors": [
|
||||
"Update": [
|
||||
{
|
||||
"Type": "SkDirectionalForceAffector",
|
||||
"Force": { "x": 0, "y": 0 }
|
||||
|
@ -40,7 +40,7 @@
|
||||
"Center": { "x": 200, "y": 200 },
|
||||
"Radius": 60
|
||||
},
|
||||
"Affectors": [
|
||||
"Update": [
|
||||
{
|
||||
"Type": "SkPointForceAffector",
|
||||
"Point": { "x": 200, "y": 200 },
|
||||
|
@ -29,7 +29,7 @@
|
||||
"P1": { "x": 237, "y": 396 },
|
||||
"P2": { "x": 214, "y": 398 }
|
||||
},
|
||||
"Affectors": [
|
||||
"Update": [
|
||||
{
|
||||
"Type": "SkDirectionalForceAffector",
|
||||
"Force": { "x": 0, "y": 50 }
|
||||
|
@ -40,7 +40,7 @@
|
||||
"P1": { "x": 61, "y": 34 },
|
||||
"P2": { "x": 606, "y": 32 }
|
||||
},
|
||||
"Affectors": [
|
||||
"Update": [
|
||||
{
|
||||
"Type": "SkJitterAffector",
|
||||
"X": {
|
||||
|
@ -40,7 +40,7 @@
|
||||
"P1": { "x": 200, "y": 200 },
|
||||
"P2": { "x": 250, "y": 200 }
|
||||
},
|
||||
"Affectors": [
|
||||
"Update": [
|
||||
{
|
||||
"Type": "SkRangedForceAffector",
|
||||
"Angle": {
|
||||
|
@ -40,7 +40,7 @@
|
||||
"Center": { "x": 380.8, "y": 273.92 },
|
||||
"Radius": 43
|
||||
},
|
||||
"Affectors": [
|
||||
"Update": [
|
||||
{
|
||||
"Type": "SkPointForceAffector",
|
||||
"Point": { "x": 375, "y": 273 },
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
};
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user