blob: d8bb7b632a8be43d900d849efc80e2650b9e99e3 [file] [log] [blame]
/*
* Copyright 2019 Google LLC
*
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*/
#include "modules/particles/include/SkParticleEffect.h"
#include "include/core/SkPaint.h"
#include "include/private/SkTPin.h"
#include "modules/particles/include/SkParticleBinding.h"
#include "modules/particles/include/SkParticleDrawable.h"
#include "modules/particles/include/SkReflected.h"
#include "modules/skresources/include/SkResources.h"
#include "src/core/SkPaintPriv.h"
#include "src/sksl/SkSLByteCode.h"
#include "src/sksl/SkSLCompiler.h"
#include "src/sksl/SkSLUtil.h"
static const char* kCommonHeader =
R"(
struct Effect {
float age;
float lifetime;
int loop;
float rate;
int burst;
float2 pos;
float2 dir;
float scale;
float2 vel;
float spin;
float4 color;
float frame;
float seed;
};
uniform float dt;
// We use a not-very-random pure-float PRNG. It does have nice properties for our situation:
// It's fast-ish. Importantly, it only uses types and operations that exist in public SkSL's
// minimum spec (no bitwise operations on integers).
float rand(inout float seed) {
seed = sin(31*seed) + sin(19*seed + 1);
return fract(abs(10*seed));
}
)";
static const char* kParticleHeader =
R"(
struct Particle {
float age;
float lifetime;
float2 pos;
float2 dir;
float scale;
float2 vel;
float spin;
float4 color;
float frame;
float seed;
};
uniform Effect effect;
)";
static const char* kDefaultEffectCode =
R"(void effectSpawn(inout Effect effect) {
}
void effectUpdate(inout Effect effect) {
}
)";
static const char* kDefaultParticleCode =
R"(void spawn(inout Particle p) {
}
void update(inout Particle p) {
}
)";
SkParticleEffectParams::SkParticleEffectParams()
: fMaxCount(128)
, fDrawable(nullptr)
, fEffectCode(kDefaultEffectCode)
, fParticleCode(kDefaultParticleCode) {}
void SkParticleEffectParams::visitFields(SkFieldVisitor* v) {
v->visit("MaxCount", fMaxCount);
v->visit("Drawable", fDrawable);
v->visit("EffectCode", fEffectCode);
v->visit("Code", fParticleCode);
v->visit("Bindings", fBindings);
}
void SkParticleEffectParams::prepare(const skresources::ResourceProvider* resourceProvider) {
for (auto& binding : fBindings) {
if (binding) {
binding->prepare(resourceProvider);
}
}
if (fDrawable) {
fDrawable->prepare(resourceProvider);
}
auto buildProgram = [this](const SkSL::String& code, Program* p) {
SkSL::ShaderCapsPointer caps = SkSL::ShaderCapsFactory::Standalone();
SkSL::Compiler compiler(caps.get());
SkSL::Program::Settings settings;
settings.fRemoveDeadFunctions = false;
std::vector<std::unique_ptr<SkSL::ExternalFunction>> externalValues;
externalValues.reserve(fBindings.size());
for (const auto& binding : fBindings) {
if (binding) {
externalValues.push_back(binding->toValue(compiler));
}
}
auto program = compiler.convertProgram(SkSL::Program::kGeneric_Kind, code, settings,
&externalValues);
if (!program) {
SkDebugf("%s\n", compiler.errorText().c_str());
return;
}
auto byteCode = compiler.toByteCode(*program);
if (!byteCode) {
SkDebugf("%s\n", compiler.errorText().c_str());
return;
}
p->fByteCode = std::move(byteCode);
p->fExternalValues.swap(externalValues);
};
SkSL::String effectCode(kCommonHeader);
effectCode.append(fEffectCode.c_str());
SkSL::String particleCode(kCommonHeader);
particleCode.append(kParticleHeader);
particleCode.append(fParticleCode.c_str());
buildProgram(effectCode, &fEffectProgram);
buildProgram(particleCode, &fParticleProgram);
}
SkParticleEffect::SkParticleEffect(sk_sp<SkParticleEffectParams> params)
: fParams(std::move(params))
, fLooping(false)
, fCount(0)
, fLastTime(-1.0)
, fSpawnRemainder(0.0f) {
fState.fAge = -1.0f;
this->setCapacity(fParams->fMaxCount);
}
void SkParticleEffect::start(double now, bool looping, SkPoint position, SkVector heading,
float scale, SkVector velocity, float spin, SkColor4f color,
float frame, float seed) {
fCount = 0;
fLastTime = now;
fSpawnRemainder = 0.0f;
fLooping = looping;
fState.fAge = 0.0f;
// A default lifetime makes sense - many effects are simple loops that don't really care.
// Every effect should define its own rate of emission, or only use bursts, so leave that as
// zero initially.
fState.fLifetime = 1.0f;
fState.fLoopCount = 0;
fState.fRate = 0.0f;
fState.fBurst = 0;
fState.fPosition = position;
fState.fHeading = heading;
fState.fScale = scale;
fState.fVelocity = velocity;
fState.fSpin = spin;
fState.fColor = color;
fState.fFrame = frame;
fState.fRandom = seed;
// Defer running effectSpawn until the first update (to reuse the code when looping)
}
// Just the update step from our "rand" function
static float advance_seed(float x) {
return sinf(31*x) + sinf(19*x + 1);
}
void SkParticleEffect::runEffectScript(const char* entry) {
if (const auto& byteCode = fParams->fEffectProgram.fByteCode) {
if (auto fun = byteCode->getFunction(entry)) {
SkAssertResult(byteCode->run(fun, &fState.fAge, sizeof(EffectState) / sizeof(float),
nullptr, 0,
fEffectUniforms.data(), fEffectUniforms.count()));
}
}
}
void SkParticleEffect::runParticleScript(const char* entry, int start, int count) {
if (const auto& byteCode = fParams->fParticleProgram.fByteCode) {
if (auto fun = byteCode->getFunction(entry)) {
float* args[SkParticles::kNumChannels];
for (int i = 0; i < SkParticles::kNumChannels; ++i) {
args[i] = fParticles.fData[i].get() + start;
}
memcpy(&fParticleUniforms[1], &fState.fAge, sizeof(EffectState));
SkAssertResult(byteCode->runStriped(fun, count, args, SkParticles::kNumChannels,
nullptr, 0,
fParticleUniforms.data(),
fParticleUniforms.count()));
}
}
}
void SkParticleEffect::advanceTime(double now) {
// TODO: Sub-frame spawning. Tricky with script driven position. Supply variable effect.age?
// Could be done if effect.age were an external value that offset by particle lane, perhaps.
float deltaTime = static_cast<float>(now - fLastTime);
if (deltaTime <= 0.0f) {
return;
}
fLastTime = now;
// Handle user edits to fMaxCount
if (fParams->fMaxCount != fCapacity) {
this->setCapacity(fParams->fMaxCount);
}
// Ensure our storage block for uniforms are large enough
auto resizeWithZero = [](SkTArray<float, true>* uniforms, const SkSL::ByteCode* byteCode) {
if (byteCode) {
int newCount = byteCode->getUniformSlotCount();
if (newCount > uniforms->count()) {
uniforms->push_back_n(newCount - uniforms->count(), 0.0f);
} else {
uniforms->resize(newCount);
}
}
};
resizeWithZero(&fEffectUniforms, this->effectCode());
resizeWithZero(&fParticleUniforms, this->particleCode());
// Copy known values into the uniform blocks
SkASSERT(!this->effectCode() || this->effectCode()->getUniformLocation("dt") == 0);
SkASSERT(!this->particleCode() || this->particleCode()->getUniformLocation("dt") == 0);
SkASSERT(!this->particleCode() || this->particleCode()->getUniformLocation("effect.age") == 1);
fEffectUniforms[0] = deltaTime;
fParticleUniforms[0] = deltaTime;
// Is this the first update after calling start()?
// Run 'effectSpawn' to set initial emitter properties.
if (fState.fAge == 0.0f && fState.fLoopCount == 0) {
this->runEffectScript("effectSpawn");
}
fState.fAge += deltaTime / fState.fLifetime;
if (fState.fAge > 1) {
// We always run effectDeath when age crosses 1, whether we're looping or actually dying
this->runEffectScript("effectDeath");
if (fLooping) {
// If we looped, then run effectSpawn again (with the updated loop count)
fState.fLoopCount += sk_float_floor2int(fState.fAge);
fState.fAge = fmodf(fState.fAge, 1.0f);
this->runEffectScript("effectSpawn");
} else {
// Effect is dead if we've reached the end (and are not looping)
return;
}
}
// Advance age for existing particles, shuffle all dying particles to the end of the arrays
int numDyingParticles = 0;
for (int i = 0; i < fCount; ++i) {
fParticles.fData[SkParticles::kAge][i] +=
fParticles.fData[SkParticles::kLifetime][i] * deltaTime;
if (fParticles.fData[SkParticles::kAge][i] > 1.0f) {
// NOTE: This is fast, but doesn't preserve drawing order. Could be a problem...
for (int j = 0; j < SkParticles::kNumChannels; ++j) {
std::swap(fParticles.fData[j][i], fParticles.fData[j][fCount - 1]);
}
std::swap(fStableRandoms[i], fStableRandoms[fCount - 1]);
--i;
--fCount;
++numDyingParticles;
}
}
// Run the death script for all particles that just died
this->runParticleScript("death", fCount, numDyingParticles);
// Run 'effectUpdate' to adjust emitter properties
this->runEffectScript("effectUpdate");
// Do integration of effect position and orientation
{
fState.fPosition += fState.fVelocity * deltaTime;
float s = sk_float_sin(fState.fSpin * deltaTime),
c = sk_float_cos(fState.fSpin * deltaTime);
// Using setNormalize to prevent scale drift
fState.fHeading.setNormalize(fState.fHeading.fX * c - fState.fHeading.fY * s,
fState.fHeading.fX * s + fState.fHeading.fY * c);
}
// Spawn new particles
float desired = fState.fRate * deltaTime + fSpawnRemainder + fState.fBurst;
fState.fBurst = 0;
int numToSpawn = sk_float_round2int(desired);
fSpawnRemainder = desired - numToSpawn;
numToSpawn = SkTPin(numToSpawn, 0, fParams->fMaxCount - fCount);
if (numToSpawn) {
const int spawnBase = fCount;
for (int i = 0; i < numToSpawn; ++i) {
// Mutate our random seed so each particle definitely gets a different generator
fState.fRandom = advance_seed(fState.fRandom);
fParticles.fData[SkParticles::kAge ][fCount] = 0.0f;
fParticles.fData[SkParticles::kLifetime ][fCount] = 0.0f;
fParticles.fData[SkParticles::kPositionX ][fCount] = fState.fPosition.fX;
fParticles.fData[SkParticles::kPositionY ][fCount] = fState.fPosition.fY;
fParticles.fData[SkParticles::kHeadingX ][fCount] = fState.fHeading.fX;
fParticles.fData[SkParticles::kHeadingY ][fCount] = fState.fHeading.fY;
fParticles.fData[SkParticles::kScale ][fCount] = fState.fScale;
fParticles.fData[SkParticles::kVelocityX ][fCount] = fState.fVelocity.fX;
fParticles.fData[SkParticles::kVelocityY ][fCount] = fState.fVelocity.fY;
fParticles.fData[SkParticles::kVelocityAngular][fCount] = fState.fSpin;
fParticles.fData[SkParticles::kColorR ][fCount] = fState.fColor.fR;
fParticles.fData[SkParticles::kColorG ][fCount] = fState.fColor.fG;
fParticles.fData[SkParticles::kColorB ][fCount] = fState.fColor.fB;
fParticles.fData[SkParticles::kColorA ][fCount] = fState.fColor.fA;
fParticles.fData[SkParticles::kSpriteFrame ][fCount] = fState.fFrame;
fParticles.fData[SkParticles::kRandom ][fCount] = fState.fRandom;
fCount++;
}
// Run the spawn script
this->runParticleScript("spawn", spawnBase, numToSpawn);
// Now stash copies of the random seeds and compute inverse particle lifetimes
// (so that subsequent updates are faster)
for (int i = spawnBase; i < fCount; ++i) {
fParticles.fData[SkParticles::kLifetime][i] =
sk_ieee_float_divide(1.0f, fParticles.fData[SkParticles::kLifetime][i]);
fStableRandoms[i] = fParticles.fData[SkParticles::kRandom][i];
}
}
// Restore all stable random seeds so update scripts get consistent behavior each frame
for (int i = 0; i < fCount; ++i) {
fParticles.fData[SkParticles::kRandom][i] = fStableRandoms[i];
}
// Run the update script
this->runParticleScript("update", 0, fCount);
// Do fixed-function update work (integration of position and orientation)
for (int i = 0; i < fCount; ++i) {
fParticles.fData[SkParticles::kPositionX][i] +=
fParticles.fData[SkParticles::kVelocityX][i] * deltaTime;
fParticles.fData[SkParticles::kPositionY][i] +=
fParticles.fData[SkParticles::kVelocityY][i] * deltaTime;
float spin = fParticles.fData[SkParticles::kVelocityAngular][i];
float s = sk_float_sin(spin * deltaTime),
c = sk_float_cos(spin * deltaTime);
float oldHeadingX = fParticles.fData[SkParticles::kHeadingX][i],
oldHeadingY = fParticles.fData[SkParticles::kHeadingY][i];
fParticles.fData[SkParticles::kHeadingX][i] = oldHeadingX * c - oldHeadingY * s;
fParticles.fData[SkParticles::kHeadingY][i] = oldHeadingX * s + oldHeadingY * c;
}
}
void SkParticleEffect::update(double now) {
if (this->isAlive()) {
this->advanceTime(now);
}
}
void SkParticleEffect::draw(SkCanvas* canvas) {
if (this->isAlive() && fParams->fDrawable) {
SkPaint paint;
SkPaintPriv::SetFQ(&paint, SkFilterQuality::kMedium_SkFilterQuality);
fParams->fDrawable->draw(canvas, fParticles, fCount, paint);
}
}
void SkParticleEffect::setCapacity(int capacity) {
for (int i = 0; i < SkParticles::kNumChannels; ++i) {
fParticles.fData[i].realloc(capacity);
}
fStableRandoms.realloc(capacity);
fCapacity = capacity;
fCount = std::min(fCount, fCapacity);
}
void SkParticleEffect::RegisterParticleTypes() {
REGISTER_REFLECTED(SkReflected);
SkParticleBinding::RegisterBindingTypes();
SkParticleDrawable::RegisterDrawableTypes();
}