1/*
2* Copyright 2019 Google LLC
3*
4* Use of this source code is governed by a BSD-style license that can be
5* found in the LICENSE file.
6*/
7
8#include "modules/particles/include/SkParticleEffect.h"
9
10#include "include/core/SkPaint.h"
11#include "modules/particles/include/SkParticleBinding.h"
12#include "modules/particles/include/SkParticleDrawable.h"
13#include "modules/particles/include/SkReflected.h"
14#include "modules/skresources/include/SkResources.h"
15#include "src/sksl/SkSLByteCode.h"
16#include "src/sksl/SkSLCompiler.h"
17
18static inline float bits_to_float(uint32_t u) {
19 float f;
20 memcpy(&f, &u, sizeof(uint32_t));
21 return f;
22}
23
24static inline uint32_t float_to_bits(float f) {
25 uint32_t u;
26 memcpy(&u, &f, sizeof(uint32_t));
27 return u;
28}
29
30static const char* kCommonHeader =
31R"(
32struct Effect {
33 float age;
34 float lifetime;
35 int loop;
36 float rate;
37 int burst;
38
39 float2 pos;
40 float2 dir;
41 float scale;
42 float2 vel;
43 float spin;
44 float4 color;
45 float frame;
46 uint flags;
47 uint seed;
48};
49
50uniform float dt;
51
52// We use the LCG from Numerical Recipes. It's not great, but has nice properties for our situation:
53// It's fast (just integer mul + add), and only uses vectorized instructions. The modulus is 2^32,
54// so we can just rely on uint overflow. All output bits are used, again that simplifies the usage
55// here (although we shift down so that our float divisor retains precision).
56float rand(inout uint seed) {
57 seed = seed * 1664525 + 1013904223;
58 return float(seed >> 4) / 0xFFFFFFF;
59}
60)";
61
62static const char* kParticleHeader =
63R"(
64struct Particle {
65 float age;
66 float lifetime;
67 float2 pos;
68 float2 dir;
69 float scale;
70 float2 vel;
71 float spin;
72 float4 color;
73 float frame;
74 uint flags;
75 uint seed;
76};
77
78uniform Effect effect;
79)";
80
81static const char* kDefaultEffectCode =
82R"(void effectSpawn(inout Effect effect) {
83}
84
85void effectUpdate(inout Effect effect) {
86}
87)";
88
89static const char* kDefaultParticleCode =
90R"(void spawn(inout Particle p) {
91}
92
93void update(inout Particle p) {
94}
95)";
96
97SkParticleEffectParams::SkParticleEffectParams()
98 : fMaxCount(128)
99 , fDrawable(nullptr)
100 , fEffectCode(kDefaultEffectCode)
101 , fParticleCode(kDefaultParticleCode) {}
102
103void SkParticleEffectParams::visitFields(SkFieldVisitor* v) {
104 v->visit("MaxCount", fMaxCount);
105 v->visit("Drawable", fDrawable);
106 v->visit("EffectCode", fEffectCode);
107 v->visit("Code", fParticleCode);
108 v->visit("Bindings", fBindings);
109}
110
111void SkParticleEffectParams::prepare(const skresources::ResourceProvider* resourceProvider) {
112 for (auto& binding : fBindings) {
113 if (binding) {
114 binding->prepare(resourceProvider);
115 }
116 }
117 if (fDrawable) {
118 fDrawable->prepare(resourceProvider);
119 }
120
121 auto buildProgram = [this](const SkSL::String& code, Program* p) {
122 SkSL::Compiler compiler;
123 SkSL::Program::Settings settings;
124 settings.fRemoveDeadFunctions = false;
125
126 SkTArray<std::unique_ptr<SkParticleExternalValue>> externalValues;
127
128 for (const auto& binding : fBindings) {
129 if (binding) {
130 auto value = binding->toValue(compiler);
131 compiler.registerExternalValue(value.get());
132 externalValues.push_back(std::move(value));
133 }
134 }
135
136 auto program = compiler.convertProgram(SkSL::Program::kGeneric_Kind, code, settings);
137 if (!program) {
138 SkDebugf("%s\n", compiler.errorText().c_str());
139 return;
140 }
141
142 auto byteCode = compiler.toByteCode(*program);
143 if (!byteCode) {
144 SkDebugf("%s\n", compiler.errorText().c_str());
145 return;
146 }
147
148 p->fByteCode = std::move(byteCode);
149 p->fExternalValues.swap(externalValues);
150 };
151
152 SkSL::String effectCode(kCommonHeader);
153 effectCode.append(fEffectCode.c_str());
154
155 SkSL::String particleCode(kCommonHeader);
156 particleCode.append(kParticleHeader);
157 particleCode.append(fParticleCode.c_str());
158
159 buildProgram(effectCode, &fEffectProgram);
160 buildProgram(particleCode, &fParticleProgram);
161}
162
163SkParticleEffect::SkParticleEffect(sk_sp<SkParticleEffectParams> params)
164 : fParams(std::move(params))
165 , fLooping(false)
166 , fCount(0)
167 , fLastTime(-1.0)
168 , fSpawnRemainder(0.0f) {
169 fState.fAge = -1.0f;
170 this->setCapacity(fParams->fMaxCount);
171}
172
173void SkParticleEffect::start(double now, bool looping, SkPoint position, SkVector heading,
174 float scale, SkVector velocity, float spin, SkColor4f color,
175 float frame, uint32_t flags, uint32_t seed) {
176 fCount = 0;
177 fLastTime = now;
178 fSpawnRemainder = 0.0f;
179 fLooping = looping;
180
181 fState.fAge = 0.0f;
182
183 // A default lifetime makes sense - many effects are simple loops that don't really care.
184 // Every effect should define its own rate of emission, or only use bursts, so leave that as
185 // zero initially.
186 fState.fLifetime = 1.0f;
187 fState.fLoopCount = 0;
188 fState.fRate = 0.0f;
189 fState.fBurst = 0;
190
191 fState.fPosition = position;
192 fState.fHeading = heading;
193 fState.fScale = scale;
194 fState.fVelocity = velocity;
195 fState.fSpin = spin;
196 fState.fColor = color;
197 fState.fFrame = frame;
198 fState.fFlags = flags;
199 fState.fRandom = seed;
200
201 // Defer running effectSpawn until the first update (to reuse the code when looping)
202}
203
204// Numerical Recipes
205static uint32_t nr_rand(uint32_t x) {
206 return x * 1664525 + 1013904223;
207}
208
209// Spawns new effects that were requested by any *effect* script (copies default values from
210// the current effect state).
211void SkParticleEffect::processEffectSpawnRequests(double now) {
212 for (const auto& spawnReq : fSpawnRequests) {
213 sk_sp<SkParticleEffect> newEffect(new SkParticleEffect(std::move(spawnReq.fParams)));
214 fState.fRandom = nr_rand(fState.fRandom);
215
216 newEffect->start(now, spawnReq.fLoop, fState.fPosition, fState.fHeading, fState.fScale,
217 fState.fVelocity, fState.fSpin, fState.fColor, fState.fFrame,
218 fState.fFlags, fState.fRandom);
219 fSubEffects.push_back(std::move(newEffect));
220 }
221 fSpawnRequests.reset();
222}
223
224void SkParticleEffect::runEffectScript(double now, const char* entry) {
225 if (const auto& byteCode = fParams->fEffectProgram.fByteCode) {
226 if (auto fun = byteCode->getFunction(entry)) {
227 for (const auto& value : fParams->fEffectProgram.fExternalValues) {
228 value->setEffect(this);
229 }
230 SkAssertResult(byteCode->run(fun, &fState.fAge, sizeof(EffectState) / sizeof(float),
231 nullptr, 0,
232 fEffectUniforms.data(), fEffectUniforms.count()));
233 this->processEffectSpawnRequests(now);
234 }
235 }
236}
237
238void SkParticleEffect::processParticleSpawnRequests(double now, int start) {
239 const auto& data = fParticles.fData;
240 for (const auto& spawnReq : fSpawnRequests) {
241 int idx = start + spawnReq.fIndex;
242 sk_sp<SkParticleEffect> newEffect(new SkParticleEffect(std::move(spawnReq.fParams)));
243 newEffect->start(now, spawnReq.fLoop,
244 { data[SkParticles::kPositionX ][idx],
245 data[SkParticles::kPositionY ][idx] },
246 { data[SkParticles::kHeadingX ][idx],
247 data[SkParticles::kHeadingY ][idx] },
248 data[SkParticles::kScale ][idx],
249 { data[SkParticles::kVelocityX ][idx],
250 data[SkParticles::kVelocityY ][idx] },
251 data[SkParticles::kVelocityAngular][idx],
252 { data[SkParticles::kColorR ][idx],
253 data[SkParticles::kColorG ][idx],
254 data[SkParticles::kColorB ][idx],
255 data[SkParticles::kColorA ][idx] },
256 data[SkParticles::kSpriteFrame ][idx],
257 float_to_bits(data[SkParticles::kFlags ][idx]),
258 float_to_bits(data[SkParticles::kRandom ][idx]));
259 fSubEffects.push_back(std::move(newEffect));
260 }
261 fSpawnRequests.reset();
262}
263
264void SkParticleEffect::runParticleScript(double now, const char* entry, int start, int count) {
265 if (const auto& byteCode = fParams->fParticleProgram.fByteCode) {
266 if (auto fun = byteCode->getFunction(entry)) {
267 float* args[SkParticles::kNumChannels];
268 for (int i = 0; i < SkParticles::kNumChannels; ++i) {
269 args[i] = fParticles.fData[i].get() + start;
270 }
271 for (const auto& value : fParams->fParticleProgram.fExternalValues) {
272 value->setEffect(this);
273 }
274 memcpy(&fParticleUniforms[1], &fState.fAge, sizeof(EffectState));
275 SkAssertResult(byteCode->runStriped(fun, count, args, SkParticles::kNumChannels,
276 nullptr, 0,
277 fParticleUniforms.data(),
278 fParticleUniforms.count()));
279 this->processParticleSpawnRequests(now, start);
280 }
281 }
282}
283
284void SkParticleEffect::advanceTime(double now) {
285 // TODO: Sub-frame spawning. Tricky with script driven position. Supply variable effect.age?
286 // Could be done if effect.age were an external value that offset by particle lane, perhaps.
287 float deltaTime = static_cast<float>(now - fLastTime);
288 if (deltaTime <= 0.0f) {
289 return;
290 }
291 fLastTime = now;
292
293 // Handle user edits to fMaxCount
294 if (fParams->fMaxCount != fCapacity) {
295 this->setCapacity(fParams->fMaxCount);
296 }
297
298 // Ensure our storage block for uniforms are large enough
299 auto resizeWithZero = [](SkTArray<float, true>* uniforms, const SkSL::ByteCode* byteCode) {
300 if (byteCode) {
301 int newCount = byteCode->getUniformSlotCount();
302 if (newCount > uniforms->count()) {
303 uniforms->push_back_n(newCount - uniforms->count(), 0.0f);
304 } else {
305 uniforms->resize(newCount);
306 }
307 }
308 };
309 resizeWithZero(&fEffectUniforms, this->effectCode());
310 resizeWithZero(&fParticleUniforms, this->particleCode());
311
312 // Copy known values into the uniform blocks
313 SkASSERT(!this->effectCode() || this->effectCode()->getUniformLocation("dt") == 0);
314 SkASSERT(!this->particleCode() || this->particleCode()->getUniformLocation("dt") == 0);
315 SkASSERT(!this->particleCode() || this->particleCode()->getUniformLocation("effect.age") == 1);
316 fEffectUniforms[0] = deltaTime;
317 fParticleUniforms[0] = deltaTime;
318
319 // Is this the first update after calling start()?
320 // Run 'effectSpawn' to set initial emitter properties.
321 if (fState.fAge == 0.0f && fState.fLoopCount == 0) {
322 this->runEffectScript(now, "effectSpawn");
323 }
324
325 fState.fAge += deltaTime / fState.fLifetime;
326 if (fState.fAge > 1) {
327 // We always run effectDeath when age crosses 1, whether we're looping or actually dying
328 this->runEffectScript(now, "effectDeath");
329
330 if (fLooping) {
331 // If we looped, then run effectSpawn again (with the updated loop count)
332 fState.fLoopCount += sk_float_floor2int(fState.fAge);
333 fState.fAge = fmodf(fState.fAge, 1.0f);
334 this->runEffectScript(now, "effectSpawn");
335 } else {
336 // Effect is dead if we've reached the end (and are not looping)
337 return;
338 }
339 }
340
341 // Advance age for existing particles, shuffle all dying particles to the end of the arrays
342 int numDyingParticles = 0;
343 for (int i = 0; i < fCount; ++i) {
344 fParticles.fData[SkParticles::kAge][i] +=
345 fParticles.fData[SkParticles::kLifetime][i] * deltaTime;
346 if (fParticles.fData[SkParticles::kAge][i] > 1.0f) {
347 // NOTE: This is fast, but doesn't preserve drawing order. Could be a problem...
348 for (int j = 0; j < SkParticles::kNumChannels; ++j) {
349 std::swap(fParticles.fData[j][i], fParticles.fData[j][fCount - 1]);
350 }
351 std::swap(fStableRandoms[i], fStableRandoms[fCount - 1]);
352 --i;
353 --fCount;
354 ++numDyingParticles;
355 }
356 }
357
358 // Run the death script for all particles that just died
359 this->runParticleScript(now, "death", fCount, numDyingParticles);
360
361 // Run 'effectUpdate' to adjust emitter properties
362 this->runEffectScript(now, "effectUpdate");
363
364 // Do integration of effect position and orientation
365 {
366 fState.fPosition += fState.fVelocity * deltaTime;
367 float s = sk_float_sin(fState.fSpin * deltaTime),
368 c = sk_float_cos(fState.fSpin * deltaTime);
369 // Using setNormalize to prevent scale drift
370 fState.fHeading.setNormalize(fState.fHeading.fX * c - fState.fHeading.fY * s,
371 fState.fHeading.fX * s + fState.fHeading.fY * c);
372 }
373
374 // Spawn new particles
375 float desired = fState.fRate * deltaTime + fSpawnRemainder + fState.fBurst;
376 fState.fBurst = 0;
377 int numToSpawn = sk_float_round2int(desired);
378 fSpawnRemainder = desired - numToSpawn;
379 numToSpawn = SkTPin(numToSpawn, 0, fParams->fMaxCount - fCount);
380 if (numToSpawn) {
381 const int spawnBase = fCount;
382
383 for (int i = 0; i < numToSpawn; ++i) {
384 // Mutate our random seed so each particle definitely gets a different generator
385 fState.fRandom = nr_rand(fState.fRandom);
386 fParticles.fData[SkParticles::kAge ][fCount] = 0.0f;
387 fParticles.fData[SkParticles::kLifetime ][fCount] = 0.0f;
388 fParticles.fData[SkParticles::kPositionX ][fCount] = fState.fPosition.fX;
389 fParticles.fData[SkParticles::kPositionY ][fCount] = fState.fPosition.fY;
390 fParticles.fData[SkParticles::kHeadingX ][fCount] = fState.fHeading.fX;
391 fParticles.fData[SkParticles::kHeadingY ][fCount] = fState.fHeading.fY;
392 fParticles.fData[SkParticles::kScale ][fCount] = fState.fScale;
393 fParticles.fData[SkParticles::kVelocityX ][fCount] = fState.fVelocity.fX;
394 fParticles.fData[SkParticles::kVelocityY ][fCount] = fState.fVelocity.fY;
395 fParticles.fData[SkParticles::kVelocityAngular][fCount] = fState.fSpin;
396 fParticles.fData[SkParticles::kColorR ][fCount] = fState.fColor.fR;
397 fParticles.fData[SkParticles::kColorG ][fCount] = fState.fColor.fG;
398 fParticles.fData[SkParticles::kColorB ][fCount] = fState.fColor.fB;
399 fParticles.fData[SkParticles::kColorA ][fCount] = fState.fColor.fA;
400 fParticles.fData[SkParticles::kSpriteFrame ][fCount] = fState.fFrame;
401 fParticles.fData[SkParticles::kFlags ][fCount] = bits_to_float(fState.fFlags);
402 fParticles.fData[SkParticles::kRandom ][fCount] = bits_to_float(fState.fRandom);
403 fCount++;
404 }
405
406 // Run the spawn script
407 this->runParticleScript(now, "spawn", spawnBase, numToSpawn);
408
409 // Now stash copies of the random seeds and compute inverse particle lifetimes
410 // (so that subsequent updates are faster)
411 for (int i = spawnBase; i < fCount; ++i) {
412 fParticles.fData[SkParticles::kLifetime][i] =
413 sk_ieee_float_divide(1.0f, fParticles.fData[SkParticles::kLifetime][i]);
414 fStableRandoms[i] = fParticles.fData[SkParticles::kRandom][i];
415 }
416 }
417
418 // Restore all stable random seeds so update scripts get consistent behavior each frame
419 for (int i = 0; i < fCount; ++i) {
420 fParticles.fData[SkParticles::kRandom][i] = fStableRandoms[i];
421 }
422
423 // Run the update script
424 this->runParticleScript(now, "update", 0, fCount);
425
426 // Do fixed-function update work (integration of position and orientation)
427 for (int i = 0; i < fCount; ++i) {
428 fParticles.fData[SkParticles::kPositionX][i] +=
429 fParticles.fData[SkParticles::kVelocityX][i] * deltaTime;
430 fParticles.fData[SkParticles::kPositionY][i] +=
431 fParticles.fData[SkParticles::kVelocityY][i] * deltaTime;
432
433 float spin = fParticles.fData[SkParticles::kVelocityAngular][i];
434 float s = sk_float_sin(spin * deltaTime),
435 c = sk_float_cos(spin * deltaTime);
436 float oldHeadingX = fParticles.fData[SkParticles::kHeadingX][i],
437 oldHeadingY = fParticles.fData[SkParticles::kHeadingY][i];
438 fParticles.fData[SkParticles::kHeadingX][i] = oldHeadingX * c - oldHeadingY * s;
439 fParticles.fData[SkParticles::kHeadingY][i] = oldHeadingX * s + oldHeadingY * c;
440 }
441}
442
443void SkParticleEffect::update(double now) {
444 if (this->isAlive(false)) {
445 this->advanceTime(now);
446 }
447
448 // Now update all of our sub-effects, removing any that have died
449 for (int i = 0; i < fSubEffects.count(); ++i) {
450 fSubEffects[i]->update(now);
451 if (!fSubEffects[i]->isAlive()) {
452 fSubEffects[i] = fSubEffects.back();
453 fSubEffects.pop_back();
454 --i;
455 }
456 }
457}
458
459void SkParticleEffect::draw(SkCanvas* canvas) {
460 if (this->isAlive(false) && fParams->fDrawable) {
461 SkPaint paint;
462 paint.setFilterQuality(SkFilterQuality::kMedium_SkFilterQuality);
463 fParams->fDrawable->draw(canvas, fParticles, fCount, paint);
464 }
465
466 for (const auto& subEffect : fSubEffects) {
467 subEffect->draw(canvas);
468 }
469}
470
471void SkParticleEffect::setCapacity(int capacity) {
472 for (int i = 0; i < SkParticles::kNumChannels; ++i) {
473 fParticles.fData[i].realloc(capacity);
474 }
475 fStableRandoms.realloc(capacity);
476
477 fCapacity = capacity;
478 fCount = std::min(fCount, fCapacity);
479}
480
481void SkParticleEffect::RegisterParticleTypes() {
482 REGISTER_REFLECTED(SkReflected);
483 SkParticleBinding::RegisterBindingTypes();
484 SkParticleDrawable::RegisterDrawableTypes();
485}
486