Refactor and further generalization of particle model
- Collapsed the per-particle data into a single struct, and
use that to communicate with drawables, too. Let the drawables
manage allocation of xforms, colors, etc. Helpful for non-atlas
drawables, and just to keep the effect code simpler.
- Having all of the params in a single struct allows us to move
the remaining animated behaviors into affectors (color/frame).
- Added SkColorCurve, which works like SkCurve for SkColor4f.
Use that to create a color affector (rather than simple
start/end colors in the effect params).
- Also put the stable random in SkParticleState. This is going
to be necessary if/when we change affectors to operate on all
particles (rather than one at a time). Still need to move t
value into the particle struct (or eval it from the lifetime
params on demand).
Change-Id: Icf39116acbfd5d6e8eb91e9affbd8898d106211d
Reviewed-on: https://skia-review.googlesource.com/c/193473
Reviewed-by: Brian Osman <brianosman@google.com>
Commit-Queue: Brian Osman <brianosman@google.com>
diff --git a/modules/particles/src/SkParticleEffect.cpp b/modules/particles/src/SkParticleEffect.cpp
index 39b8dd6..4dfb7c2 100644
--- a/modules/particles/src/SkParticleEffect.cpp
+++ b/modules/particles/src/SkParticleEffect.cpp
@@ -11,7 +11,6 @@
#include "SkAnimTimer.h"
#include "SkCanvas.h"
#include "SkColorData.h"
-#include "SkNx.h"
#include "SkPaint.h"
#include "SkParticleAffector.h"
#include "SkParticleDrawable.h"
@@ -24,8 +23,6 @@
v->visit("Duration", fEffectDuration);
v->visit("Rate", fRate);
v->visit("Life", fLifetime);
- v->visit("StartColor", fStartColor);
- v->visit("EndColor", fEndColor);
v->visit("Drawable", fDrawable);
v->visit("Emitter", fEmitter);
@@ -66,9 +63,6 @@
float deltaTime = static_cast<float>(now - fLastTime);
fLastTime = now;
- Sk4f startColor = Sk4f::Load(fParams->fStartColor.vec());
- Sk4f colorScale = Sk4f::Load(fParams->fEndColor.vec()) - startColor;
-
SkParticleUpdateParams updateParams;
updateParams.fDeltaTime = deltaTime;
updateParams.fRandom = &fRandom;
@@ -77,9 +71,8 @@
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];
- fFrames[i] = fFrames[fCount - 1];
- fColors[i] = fColors[fCount - 1];
+ fParticles[i] = fParticles[fCount - 1];
+ fStableRandoms[i] = fStableRandoms[fCount - 1];
--i;
--fCount;
}
@@ -91,24 +84,24 @@
fSpawnRemainder = desired - numToSpawn;
numToSpawn = SkTPin(numToSpawn, 0, fParams->fMaxCount - fCount);
if (fParams->fEmitter) {
- // 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.
+ // 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));
for (int i = 0; i < numToSpawn; ++i) {
+ // Mutate our SkRandom so each particle definitely gets a different stable generator
+ fRandom.nextU();
+
fParticles[fCount].fTimeOfBirth = now;
fParticles[fCount].fTimeOfDeath = now + fParams->fLifetime.eval(updateParams.fParticleT,
fRandom);
- fParticles[fCount].fPV.fPose = fParams->fEmitter->emit(fRandom);
- fParticles[fCount].fPV.fVelocity.fLinear = { 0.0f, 0.0f };
- fParticles[fCount].fPV.fVelocity.fAngular = 0.0f;
+ fParticles[fCount].fPose = fParams->fEmitter->emit(fRandom);
+ fParticles[fCount].fVelocity.fLinear = { 0.0f, 0.0f };
+ fParticles[fCount].fVelocity.fAngular = 0.0f;
+ fParticles[fCount].fColor = { 1.0f, 1.0f, 1.0f, 1.0f };
+ fParticles[fCount].fFrame = 0.0f;
- fParticles[fCount].fStableRandom = fRandom;
- fFrames[fCount] = 0.0f;
+ fParticles[fCount].fStableRandom = fStableRandoms[fCount] = fRandom;
fCount++;
}
@@ -116,47 +109,37 @@
for (int i = fCount - numToSpawn; i < fCount; ++i) {
for (auto affector : fParams->fSpawnAffectors) {
if (affector) {
- affector->apply(updateParams, fParticles[i].fPV);
+ affector->apply(updateParams, fParticles[i]);
}
}
}
}
+ // Restore all stable random generators so update affectors get consistent behavior each frame
+ for (int i = 0; i < fCount; ++i) {
+ fParticles[i].fStableRandom = fStableRandoms[i];
+ }
+
// 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));
-
- SkRandom stableRandom = fParticles[i].fStableRandom;
- updateParams.fStableRandom = &stableRandom;
- updateParams.fParticleT = t;
-
- // Set sprite frame by lifetime (TODO: Remove, add affector)
- fFrames[i] = t;
-
- // Set color by lifetime
- fColors[i] = Sk4f_toL32(swizzle_rb(startColor + (colorScale * t)));
+ updateParams.fParticleT =
+ static_cast<float>((now - fParticles[i].fTimeOfBirth) /
+ (fParticles[i].fTimeOfDeath - fParticles[i].fTimeOfBirth));
for (auto affector : fParams->fUpdateAffectors) {
if (affector) {
- affector->apply(updateParams, fParticles[i].fPV);
+ affector->apply(updateParams, fParticles[i]);
}
}
// Integrate position / orientation
- fParticles[i].fPV.fPose.fPosition += fParticles[i].fPV.fVelocity.fLinear * deltaTime;
+ fParticles[i].fPose.fPosition += fParticles[i].fVelocity.fLinear * deltaTime;
- SkScalar c, s = SkScalarSinCos(fParticles[i].fPV.fVelocity.fAngular * deltaTime, &c);
- SkVector oldHeading = fParticles[i].fPV.fPose.fHeading;
- fParticles[i].fPV.fPose.fHeading = { oldHeading.fX * c - oldHeading.fY * s,
- oldHeading.fX * s + oldHeading.fY * c };
- }
-
- // Re-generate all xforms
- SkPoint ofs = fParams->fDrawable ? fParams->fDrawable->center() : SkPoint{ 0.0f, 0.0f };
- for (int i = 0; i < fCount; ++i) {
- fXforms[i] = fParticles[i].fPV.fPose.asRSXform(ofs);
+ SkScalar c, s = SkScalarSinCos(fParticles[i].fVelocity.fAngular * deltaTime, &c);
+ SkVector oldHeading = fParticles[i].fPose.fHeading;
+ fParticles[i].fPose.fHeading = { oldHeading.fX * c - oldHeading.fY * s,
+ oldHeading.fX * s + oldHeading.fY * c };
}
// Mark effect as dead if we've reached the end (and are not looping)
@@ -169,16 +152,13 @@
if (this->isAlive() && fParams->fDrawable) {
SkPaint paint;
paint.setFilterQuality(SkFilterQuality::kMedium_SkFilterQuality);
- fParams->fDrawable->draw(
- canvas, fXforms.get(), fFrames.get(), fColors.get(), fCount, &paint);
+ fParams->fDrawable->draw(canvas, fParticles.get(), fCount, &paint);
}
}
void SkParticleEffect::setCapacity(int capacity) {
fParticles.realloc(capacity);
- fXforms.realloc(capacity);
- fFrames.realloc(capacity);
- fColors.realloc(capacity);
+ fStableRandoms.realloc(capacity);
fCapacity = capacity;
fCount = SkTMin(fCount, fCapacity);