GCC Code Coverage Report


Directory: ./
File: overlays/wled/FXparticleSystem.cpp
Date: 2026-10-29 08:11:58
Exec Total Coverage
Lines: 1000 1098 91.1%
Functions: 80 86 93.0%
Branches: 534 672 79.5%

Line Branch Exec Source
1 /*
2 FXparticleSystem.cpp
3
4 Particle system with functions for particle generation, particle movement and particle rendering to RGB matrix.
5 by DedeHai (Damian Schneider) 2013-2024
6
7 Copyright (c) 2024 Damian Schneider
8 Licensed under the EUPL v. 1.2 or later
9 */
10
11 #ifdef WLED_DISABLE_2D
12 #define WLED_DISABLE_PARTICLESYSTEM2D
13 #endif
14
15 #if !(defined(WLED_DISABLE_PARTICLESYSTEM2D) && defined(WLED_DISABLE_PARTICLESYSTEM1D)) // not both disabled
16 #include "FXparticleSystem.h"
17 // local shared functions (used both in 1D and 2D system)
18 static int32_t calcForce_dv(const int8_t force, uint8_t &counter);
19 static bool checkBoundsAndWrap(int32_t &position, const int32_t max, const int32_t particleradius, const bool wrap); // returns false if out of bounds by more than particleradius
20 static void fast_color_add(CRGB &c1, const CRGB &c2, uint8_t scale = 255); // fast and accurate color adding with scaling (scales c2 before adding)
21 static void fast_color_scale(CRGB &c, const uint8_t scale); // fast scaling function using 32bit variable and pointer. note: keep 'scale' within 0-255
22 //static CRGB *allocateCRGBbuffer(uint32_t length);
23 #endif
24
25 #ifndef WLED_DISABLE_PARTICLESYSTEM2D
26 64 ParticleSystem2D::ParticleSystem2D(uint32_t width, uint32_t height, uint32_t numberofparticles, uint32_t numberofsources, bool isadvanced, bool sizecontrol) {
27 PSPRINTLN("\n ParticleSystem2D constructor");
28 64 numSources = numberofsources; // number of sources allocated in init
29 64 numParticles = numberofparticles; // number of particles allocated in init
30 64 usedParticles = numParticles; // use all particles by default
31 64 advPartProps = nullptr; //make sure we start out with null pointers (just in case memory was not cleared)
32 64 advPartSize = nullptr;
33 64 setMatrixSize(width, height);
34 64 updatePSpointers(isadvanced, sizecontrol); // set the particle and sources pointer (call this before accessing sprays or particles)
35 64 setWallHardness(255); // set default wall hardness to max
36 64 setWallRoughness(0); // smooth walls by default
37 64 setGravity(0); //gravity disabled by default
38 64 setParticleSize(1); // 2x2 rendering size by default
39 64 motionBlur = 0; //no fading by default
40 64 smearBlur = 0; //no smearing by default
41 64 emitIndex = 0;
42 64 collisionStartIdx = 0;
43
44 //initialize some default non-zero values most FX use
45
2/2
✓ Branch 0 taken 22548 times.
✓ Branch 1 taken 64 times.
22612 for (uint32_t i = 0; i < numParticles; i++) {
46 22548 particles[i].sat = 255; // full saturation
47 }
48
2/2
✓ Branch 0 taken 460 times.
✓ Branch 1 taken 64 times.
524 for (uint32_t i = 0; i < numSources; i++) {
49 460 sources[i].source.sat = 255; //set saturation to max by default
50 460 sources[i].source.ttl = 1; //set source alive
51 460 sources[i].sourceFlags.asByte = 0; // all flags disabled
52 }
53
54 64 }
55
56 // update function applies gravity, moves the particles, handles collisions and renders the particles
57 270 void ParticleSystem2D::update(void) {
58 //apply gravity globally if enabled
59
2/2
✓ Branch 0 taken 100 times.
✓ Branch 1 taken 170 times.
270 if (particlesettings.useGravity)
60 100 applyGravity();
61
62 //update size settings before handling collisions
63
2/2
✓ Branch 0 taken 18 times.
✓ Branch 1 taken 252 times.
270 if (advPartSize) {
64
2/2
✓ Branch 0 taken 187 times.
✓ Branch 1 taken 18 times.
205 for (uint32_t i = 0; i < usedParticles; i++) {
65
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 187 times.
187 if (updateSize(&advPartProps[i], &advPartSize[i]) == false) { // if particle shrinks to 0 size
66 ✗ particles[i].ttl = 0; // kill particle
67 }
68 }
69 }
70
71 // handle collisions (can push particles, must be done before updating particles or they can render out of bounds, causing a crash if using local buffer for speed)
72
2/2
✓ Branch 0 taken 86 times.
✓ Branch 1 taken 184 times.
270 if (particlesettings.useCollisions)
73 86 handleCollisions();
74
75 //move all particles
76
2/2
✓ Branch 0 taken 74041 times.
✓ Branch 1 taken 270 times.
74311 for (uint32_t i = 0; i < usedParticles; i++) {
77
2/2
✓ Branch 0 taken 13716 times.
✓ Branch 1 taken 60325 times.
74041 particleMoveUpdate(particles[i], particleFlags[i], nullptr, advPartProps ? &advPartProps[i] : nullptr); // note: splitting this into two loops is slower and uses more flash
78 }
79
80 270 render();
81 270 }
82
83 // update function for fire animation
84 18 void ParticleSystem2D::updateFire(const uint8_t intensity,const bool renderonly) {
85
1/2
✓ Branch 0 taken 18 times.
✗ Branch 1 not taken.
18 if (!renderonly)
86 18 fireParticleupdate();
87
2/2
✓ Branch 0 taken 14 times.
✓ Branch 1 taken 4 times.
18 fireIntesity = intensity > 0 ? intensity : 1; // minimum of 1, zero checking is used in render function
88 18 render();
89 18 }
90
91 // set percentage of used particles as uint8_t i.e 127 means 50% for example
92 80 void ParticleSystem2D::setUsedParticles(uint8_t percentage) {
93 80 usedParticles = (numParticles * ((int)percentage+1)) >> 8; // number of particles to use (percentage is 0-255, 255 = 100%)
94 PSPRINT(" SetUsedpaticles: allocated particles: ");
95 PSPRINT(numParticles);
96 PSPRINT(" ,used particles: ");
97 PSPRINTLN(usedParticles);
98 80 }
99
100 237 void ParticleSystem2D::setWallHardness(uint8_t hardness) {
101 237 wallHardness = hardness;
102 237 }
103
104 80 void ParticleSystem2D::setWallRoughness(uint8_t roughness) {
105 80 wallRoughness = roughness;
106 80 }
107
108 4 void ParticleSystem2D::setCollisionHardness(uint8_t hardness) {
109 4 collisionHardness = (int)hardness + 1;
110 4 }
111
112 352 void ParticleSystem2D::setMatrixSize(uint32_t x, uint32_t y) {
113 352 maxXpixel = x - 1; // last physical pixel that can be drawn to
114 352 maxYpixel = y - 1;
115 352 maxX = x * PS_P_RADIUS - 1; // particle system boundary for movements
116 352 maxY = y * PS_P_RADIUS - 1; // this value is often needed (also by FX) to calculate positions
117 352 }
118
119 144 void ParticleSystem2D::setWrapX(bool enable) {
120 144 particlesettings.wrapX = enable;
121 144 }
122
123 ✗ void ParticleSystem2D::setWrapY(bool enable) {
124 ✗ particlesettings.wrapY = enable;
125 ✗ }
126
127 134 void ParticleSystem2D::setBounceX(bool enable) {
128 134 particlesettings.bounceX = enable;
129 134 }
130
131 96 void ParticleSystem2D::setBounceY(bool enable) {
132 96 particlesettings.bounceY = enable;
133 96 }
134
135 62 void ParticleSystem2D::setKillOutOfBounds(bool enable) {
136 62 particlesettings.killoutofbounds = enable;
137 62 }
138
139 36 void ParticleSystem2D::setColorByAge(bool enable) {
140 36 particlesettings.colorByAge = enable;
141 36 }
142
143 128 void ParticleSystem2D::setMotionBlur(uint8_t bluramount) {
144
2/2
✓ Branch 0 taken 124 times.
✓ Branch 1 taken 4 times.
128 if (particlesize < 2) // only allow motion blurring on default particle sizes or advanced size (cannot combine motion blur with normal blurring used for particlesize, would require another buffer)
145 124 motionBlur = bluramount;
146 128 }
147
148 44 void ParticleSystem2D::setSmearBlur(uint8_t bluramount) {
149 44 smearBlur = bluramount;
150 44 }
151
152
153 // render size using smearing (see blur function)
154 160377 void ParticleSystem2D::setParticleSize(uint8_t size) {
155 160377 particlesize = size;
156 160377 particleHardRadius = PS_P_MINHARDRADIUS; // ~1 pixel
157
2/2
✓ Branch 0 taken 81 times.
✓ Branch 1 taken 160296 times.
160377 if (particlesize > 1) {
158 81 particleHardRadius = max(particleHardRadius, (uint32_t)particlesize); // radius used for wall collisions & particle collisions
159 81 motionBlur = 0; // disable motion blur if particle size is set
160 }
161
2/2
✓ Branch 0 taken 22 times.
✓ Branch 1 taken 160274 times.
160296 else if (particlesize == 0)
162 22 particleHardRadius = particleHardRadius >> 1; // single pixel particles have half the radius (i.e. 1/2 pixel)
163 160377 }
164
165 // enable/disable gravity, optionally, set the force (force=8 is default) can be -127 to +127, 0 is disable
166 // if enabled, gravity is applied to all particles in ParticleSystemUpdate()
167 // force is in 3.4 fixed point notation so force=16 means apply v+1 each frame default of 8 is every other frame (gives good results)
168 134 void ParticleSystem2D::setGravity(int8_t force) {
169
2/2
✓ Branch 0 taken 44 times.
✓ Branch 1 taken 90 times.
134 if (force) {
170 44 gforce = force;
171 44 particlesettings.useGravity = true;
172 } else {
173 90 particlesettings.useGravity = false;
174 }
175 134 }
176
177 162 void ParticleSystem2D::enableParticleCollisions(bool enable, uint8_t hardness) { // enable/disable gravity, optionally, set the force (force=8 is default) can be 1-255, 0 is also disable
178 162 particlesettings.useCollisions = enable;
179 162 collisionHardness = (int)hardness + 1;
180 162 }
181
182 // emit one particle with variation, returns index of emitted particle (or -1 if no particle emitted)
183 543 int32_t ParticleSystem2D::sprayEmit(const PSsource &emitter) {
184 543 bool success = false;
185
2/2
✓ Branch 0 taken 799 times.
✓ Branch 1 taken 11 times.
810 for (uint32_t i = 0; i < usedParticles; i++) {
186 799 emitIndex++;
187
2/2
✓ Branch 0 taken 13 times.
✓ Branch 1 taken 786 times.
799 if (emitIndex >= usedParticles)
188 13 emitIndex = 0;
189
2/2
✓ Branch 0 taken 532 times.
✓ Branch 1 taken 267 times.
799 if (particles[emitIndex].ttl == 0) { // find a dead particle
190 532 success = true;
191 532 particles[emitIndex].vx = emitter.vx + hw_random16(emitter.var << 1) - emitter.var; // random(-var, var)
192 532 particles[emitIndex].vy = emitter.vy + hw_random16(emitter.var << 1) - emitter.var; // random(-var, var)
193 532 particles[emitIndex].x = emitter.source.x;
194 532 particles[emitIndex].y = emitter.source.y;
195 532 particles[emitIndex].hue = emitter.source.hue;
196 532 particles[emitIndex].sat = emitter.source.sat;
197 532 particleFlags[emitIndex].collide = emitter.sourceFlags.collide;
198 532 particles[emitIndex].ttl = hw_random16(emitter.minLife, emitter.maxLife);
199
2/2
✓ Branch 0 taken 24 times.
✓ Branch 1 taken 508 times.
532 if (advPartProps)
200 24 advPartProps[emitIndex].size = emitter.size;
201 532 break;
202 }
203 }
204
2/2
✓ Branch 0 taken 532 times.
✓ Branch 1 taken 11 times.
543 if (success)
205 532 return emitIndex;
206 else
207 11 return -1;
208 }
209
210 // Spray emitter for particles used for flames (particle TTL depends on source TTL)
211 222 void ParticleSystem2D::flameEmit(const PSsource &emitter) {
212 222 int emitIndex = sprayEmit(emitter);
213
2/2
✓ Branch 0 taken 220 times.
✓ Branch 1 taken 2 times.
222 if (emitIndex > 0) particles[emitIndex].ttl += emitter.source.ttl;
214 222 }
215
216 // Emits a particle at given angle and speed, angle is from 0-65535 (=0-360deg), speed is also affected by emitter->var
217 // angle = 0 means in positive x-direction (i.e. to the right)
218 256 int32_t ParticleSystem2D::angleEmit(PSsource &emitter, const uint16_t angle, const int32_t speed) {
219 256 emitter.vx = ((int32_t)cos16_t(angle) * speed) / (int32_t)32600; // cos16_t() and sin16_t() return signed 16bit, division should be 32767 but 32600 gives slightly better rounding
220 256 emitter.vy = ((int32_t)sin16_t(angle) * speed) / (int32_t)32600; // note: cannot use bit shifts as bit shifting is asymmetrical for positive and negative numbers and this needs to be accurate!
221 256 return sprayEmit(emitter);
222 }
223
224 // particle moves, decays and dies, if killoutofbounds is set, out of bounds particles are set to ttl=0
225 // uses passed settings to set bounce or wrap, if useGravity is enabled, it will never bounce at the top and killoutofbounds is not applied over the top
226 74218 void ParticleSystem2D::particleMoveUpdate(PSparticle &part, PSparticleFlags &partFlags, PSsettings2D *options, PSadvancedParticle *advancedproperties) {
227
2/2
✓ Branch 0 taken 74149 times.
✓ Branch 1 taken 69 times.
74218 if (options == nullptr)
228 74149 options = &particlesettings; //use PS system settings by default
229
230
2/2
✓ Branch 0 taken 3584 times.
✓ Branch 1 taken 70634 times.
74218 if (part.ttl > 0) {
231
2/2
✓ Branch 0 taken 3504 times.
✓ Branch 1 taken 80 times.
3584 if (!partFlags.perpetual)
232 3504 part.ttl--; // age
233
2/2
✓ Branch 0 taken 26 times.
✓ Branch 1 taken 3558 times.
3584 if (options->colorByAge)
234 26 part.hue = min(part.ttl, (uint16_t)255); //set color to ttl
235
236 3584 int32_t renderradius = PS_P_HALFRADIUS; // used to check out of bounds
237 3584 int32_t newX = part.x + (int32_t)part.vx;
238 3584 int32_t newY = part.y + (int32_t)part.vy;
239 3584 partFlags.outofbounds = false; // reset out of bounds (in case particle was created outside the matrix and is now moving into view) note: moving this to checks below adds code and is not faster
240
241
2/2
✓ Branch 0 taken 2229 times.
✓ Branch 1 taken 1355 times.
3584 if (advancedproperties) { //using individual particle size?
242
1/2
✓ Branch 0 taken 2229 times.
✗ Branch 1 not taken.
2229 setParticleSize(particlesize); // updates default particleHardRadius
243
2/2
✓ Branch 0 taken 10 times.
✓ Branch 1 taken 2219 times.
2229 if (advancedproperties->size > PS_P_MINHARDRADIUS) {
244 10 particleHardRadius += (advancedproperties->size - PS_P_MINHARDRADIUS); // update radius
245 10 renderradius = particleHardRadius;
246 }
247 }
248 // note: if wall collisions are enabled, bounce them before they reach the edge, it looks much nicer if the particle does not go half out of view
249
2/2
✓ Branch 0 taken 2393 times.
✓ Branch 1 taken 1191 times.
3584 if (options->bounceY) {
250
6/6
✓ Branch 0 taken 2148 times.
✓ Branch 1 taken 245 times.
✓ Branch 2 taken 145 times.
✓ Branch 3 taken 2003 times.
✓ Branch 4 taken 14 times.
✓ Branch 5 taken 131 times.
2393 if ((newY < (int32_t)particleHardRadius) || ((newY > (int32_t)(maxY - particleHardRadius)) && !options->useGravity)) { // reached floor / ceiling
251
1/2
✓ Branch 0 taken 259 times.
✗ Branch 1 not taken.
259 bounce(part.vy, part.vx, newY, maxY);
252 }
253 }
254
255
2/2
✓ Branch 0 taken 180 times.
✓ Branch 1 taken 3404 times.
3584 if (!checkBoundsAndWrap(newY, maxY, renderradius, options->wrapY)) { // check out of bounds note: this must not be skipped. if gravity is enabled, particles will never bounce at the top
256 180 partFlags.outofbounds = true;
257
2/2
✓ Branch 0 taken 163 times.
✓ Branch 1 taken 17 times.
180 if (options->killoutofbounds) {
258
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 161 times.
163 if (newY < 0) // if gravity is enabled, only kill particles below ground
259 2 part.ttl = 0;
260
2/2
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 157 times.
161 else if (!options->useGravity)
261 4 part.ttl = 0;
262 }
263 }
264
265
2/2
✓ Branch 0 taken 3578 times.
✓ Branch 1 taken 6 times.
3584 if (part.ttl) { //check x direction only if still alive
266
2/2
✓ Branch 0 taken 2248 times.
✓ Branch 1 taken 1330 times.
3578 if (options->bounceX) {
267
4/4
✓ Branch 0 taken 2170 times.
✓ Branch 1 taken 78 times.
✓ Branch 2 taken 5 times.
✓ Branch 3 taken 2165 times.
2248 if ((newX < (int32_t)particleHardRadius) || (newX > (int32_t)(maxX - particleHardRadius))) // reached a wall
268
1/2
✓ Branch 0 taken 83 times.
✗ Branch 1 not taken.
83 bounce(part.vx, part.vy, newX, maxX);
269 }
270
2/2
✓ Branch 0 taken 6 times.
✓ Branch 1 taken 1324 times.
1330 else if (!checkBoundsAndWrap(newX, maxX, renderradius, options->wrapX)) { // check out of bounds
271 6 partFlags.outofbounds = true;
272
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 if (options->killoutofbounds)
273 6 part.ttl = 0;
274 }
275 }
276
277 3584 part.x = (int16_t)newX; // set new position
278 3584 part.y = (int16_t)newY; // set new position
279 }
280 74218 }
281
282 // move function for fire particles
283 18 void ParticleSystem2D::fireParticleupdate() {
284
2/2
✓ Branch 0 taken 6776 times.
✓ Branch 1 taken 18 times.
6794 for (uint32_t i = 0; i < usedParticles; i++) {
285
2/2
✓ Branch 0 taken 619 times.
✓ Branch 1 taken 6157 times.
6776 if (particles[i].ttl > 0)
286 {
287 619 particles[i].ttl--; // age
288 619 int32_t newY = particles[i].y + (int32_t)particles[i].vy + (particles[i].ttl >> 2); // younger particles move faster upward as they are hotter
289 619 int32_t newX = particles[i].x + (int32_t)particles[i].vx;
290 619 particleFlags[i].outofbounds = 0; // reset out of bounds flag note: moving this to checks below is not faster but adds code
291 // check if particle is out of bounds, wrap x around to other side if wrapping is enabled
292 // as fire particles start below the frame, lots of particles are out of bounds in y direction. to improve speed, only check x direction if y is not out of bounds
293
2/2
✓ Branch 0 taken 614 times.
✓ Branch 1 taken 5 times.
619 if (newY < -PS_P_HALFRADIUS)
294 614 particleFlags[i].outofbounds = 1;
295
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 5 times.
5 else if (newY > int32_t(maxY + PS_P_HALFRADIUS)) // particle moved out at the top
296 ✗ particles[i].ttl = 0;
297 else // particle is in frame in y direction, also check x direction now Note: using checkBoundsAndWrap() is slower, only saves a few bytes
298 {
299
2/4
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 5 times.
5 if ((newX < 0) || (newX > (int32_t)maxX)) { // handle out of bounds & wrap
300 ✗ if (particlesettings.wrapX) {
301 ✗ newX = newX % (maxX + 1);
302 ✗ if (newX < 0) // handle negative modulo
303 ✗ newX += maxX + 1;
304 }
305 ✗ else if ((newX < -PS_P_HALFRADIUS) || (newX > int32_t(maxX + PS_P_HALFRADIUS))) { //if fully out of view
306 ✗ particles[i].ttl = 0;
307 }
308 }
309 5 particles[i].x = newX;
310 }
311 619 particles[i].y = newY;
312 }
313 }
314 18 }
315
316 // update advanced particle size control, returns false if particle shrinks to 0 size
317 187 bool ParticleSystem2D::updateSize(PSadvancedParticle *advprops, PSsizeControl *advsize) {
318
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 187 times.
187 if (advsize == nullptr) // safety check
319 ✗ return false;
320 // grow/shrink particle
321 187 int32_t newsize = advprops->size;
322 187 uint32_t counter = advsize->sizecounter;
323 187 uint32_t increment = 0;
324 // calculate grow speed using 0-8 for low speeds and 9-15 for higher speeds
325
1/2
✓ Branch 0 taken 187 times.
✗ Branch 1 not taken.
187 if (advsize->grow) increment = advsize->growspeed;
326 ✗ else if (advsize->shrink) increment = advsize->shrinkspeed;
327
2/2
✓ Branch 0 taken 166 times.
✓ Branch 1 taken 21 times.
187 if (increment < 9) { // 8 means +1 every frame
328 166 counter += increment;
329
2/2
✓ Branch 0 taken 94 times.
✓ Branch 1 taken 72 times.
166 if (counter > 7) {
330 94 counter -= 8;
331 94 increment = 1;
332 } else
333 72 increment = 0;
334 166 advsize->sizecounter = counter;
335 } else {
336 21 increment = (increment - 8) << 1; // 9 means +2, 10 means +4 etc. 15 means +14
337 }
338
339
1/2
✓ Branch 0 taken 187 times.
✗ Branch 1 not taken.
187 if (advsize->grow) {
340
1/2
✓ Branch 0 taken 187 times.
✗ Branch 1 not taken.
187 if (newsize < advsize->maxsize) {
341 187 newsize += increment;
342
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 187 times.
187 if (newsize >= advsize->maxsize) {
343 ✗ advsize->grow = false; // stop growing, shrink from now on if enabled
344 ✗ newsize = advsize->maxsize; // limit
345 ✗ if (advsize->pulsate) advsize->shrink = true;
346 }
347 }
348 ✗ } else if (advsize->shrink) {
349 ✗ if (newsize > advsize->minsize) {
350 ✗ newsize -= increment;
351 ✗ if (newsize <= advsize->minsize) {
352 ✗ if (advsize->minsize == 0)
353 ✗ return false; // particle shrunk to zero
354 ✗ advsize->shrink = false; // disable shrinking
355 ✗ newsize = advsize->minsize; // limit
356 ✗ if (advsize->pulsate) advsize->grow = true;
357 }
358 }
359 }
360 187 advprops->size = newsize;
361 // handle wobbling
362
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 187 times.
187 if (advsize->wobble) {
363 ✗ advsize->asymdir += advsize->wobblespeed; // note: if need better wobblespeed control a counter is already in the struct
364 }
365 187 return true;
366 }
367
368 // calculate x and y size for asymmetrical particles (advanced size control)
369 83 void ParticleSystem2D::getParticleXYsize(PSadvancedParticle *advprops, PSsizeControl *advsize, uint32_t &xsize, uint32_t &ysize) {
370
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 83 times.
83 if (advsize == nullptr) // if advsize is valid, also advanced properties pointer is valid (handled by updatePSpointers())
371 ✗ return;
372 83 int32_t size = advprops->size;
373 83 int32_t asymdir = advsize->asymdir;
374 83 int32_t deviation = ((uint32_t)size * (uint32_t)advsize->asymmetry + 255) >> 8; // deviation from symmetrical size
375 // Calculate x and y size based on deviation and direction (0 is symmetrical, 64 is x, 128 is symmetrical, 192 is y)
376
2/2
✓ Branch 0 taken 18 times.
✓ Branch 1 taken 65 times.
83 if (asymdir < 64) {
377 18 deviation = (asymdir * deviation) >> 6;
378
2/2
✓ Branch 0 taken 48 times.
✓ Branch 1 taken 17 times.
65 } else if (asymdir < 192) {
379 48 deviation = ((128 - asymdir) * deviation) >> 6;
380 } else {
381 17 deviation = ((asymdir - 255) * deviation) >> 6;
382 }
383 // Calculate x and y size based on deviation, limit to 255 (rendering function cannot handle larger sizes)
384 83 xsize = min((size - deviation), (int32_t)255);
385 83 ysize = min((size + deviation), (int32_t)255);;
386 }
387
388 // function to bounce a particle from a wall using set parameters (wallHardness and wallRoughness)
389 342 void ParticleSystem2D::bounce(int8_t &incomingspeed, int8_t &parallelspeed, int32_t &position, const uint32_t maxposition) {
390 342 incomingspeed = -incomingspeed;
391 342 incomingspeed = (incomingspeed * wallHardness + 128) >> 8; // reduce speed as energy is lost on non-hard surface
392
2/2
✓ Branch 0 taken 323 times.
✓ Branch 1 taken 19 times.
342 if (position < (int32_t)particleHardRadius)
393 323 position = particleHardRadius; // fast particles will never reach the edge if position is inverted, this looks better
394 else
395 19 position = maxposition - particleHardRadius;
396
2/2
✓ Branch 0 taken 23 times.
✓ Branch 1 taken 319 times.
342 if (wallRoughness) {
397 23 int32_t incomingspeed_abs = abs((int32_t)incomingspeed);
398 23 int32_t totalspeed = incomingspeed_abs + abs((int32_t)parallelspeed);
399 // transfer an amount of incomingspeed speed to parallel speed
400 23 int32_t donatespeed = ((hw_random16(incomingspeed_abs << 1) - incomingspeed_abs) * (int32_t)wallRoughness) / (int32_t)255; // take random portion of + or - perpendicular speed, scaled by roughness
401 23 parallelspeed = limitSpeed((int32_t)parallelspeed + donatespeed);
402 // give the remainder of the speed to perpendicular speed
403 23 donatespeed = int8_t(totalspeed - abs(parallelspeed)); // keep total speed the same
404
2/2
✓ Branch 0 taken 9 times.
✓ Branch 1 taken 14 times.
23 incomingspeed = incomingspeed > 0 ? donatespeed : -donatespeed;
405 }
406 342 }
407
408 // apply a force in x,y direction to individual particle
409 // caller needs to provide a 8bit counter (for each particle) that holds its value between calls
410 // force is in 3.4 fixed point notation so force=16 means apply v+1 each frame default of 8 is every other frame (gives good results)
411 7134 void ParticleSystem2D::applyForce(PSparticle &part, const int8_t xforce, const int8_t yforce, uint8_t &counter) {
412 // for small forces, need to use a delay counter
413 7134 uint8_t xcounter = counter & 0x0F; // lower four bits
414 7134 uint8_t ycounter = counter >> 4; // upper four bits
415
416 // velocity increase
417 7134 int32_t dvx = calcForce_dv(xforce, xcounter);
418 7134 int32_t dvy = calcForce_dv(yforce, ycounter);
419
420 // save counter values back
421 7134 counter = xcounter & 0x0F; // write lower four bits, make sure not to write more than 4 bits
422 7134 counter |= (ycounter << 4) & 0xF0; // write upper four bits
423
424 // apply the force to particle
425 7134 part.vx = limitSpeed((int32_t)part.vx + dvx);
426 7134 part.vy = limitSpeed((int32_t)part.vy + dvy);
427 7134 }
428
429 // apply a force in x,y direction to individual particle using advanced particle properties
430 2581 void ParticleSystem2D::applyForce(const uint32_t particleindex, const int8_t xforce, const int8_t yforce) {
431
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2581 times.
2581 if (advPartProps == nullptr)
432 ✗ return; // no advanced properties available
433 2581 applyForce(particles[particleindex], xforce, yforce, advPartProps[particleindex].forcecounter);
434 }
435
436 // apply a force in x,y direction to all particles
437 // force is in 3.4 fixed point notation (see above)
438 18 void ParticleSystem2D::applyForce(const int8_t xforce, const int8_t yforce) {
439 // for small forces, need to use a delay counter
440 uint8_t tempcounter;
441 // note: this is not the most computationally efficient way to do this, but it saves on duplicate code and is fast enough
442
2/2
✓ Branch 0 taken 4553 times.
✓ Branch 1 taken 18 times.
4571 for (uint32_t i = 0; i < usedParticles; i++) {
443 4553 tempcounter = forcecounter;
444
1/2
✓ Branch 0 taken 4553 times.
✗ Branch 1 not taken.
4553 applyForce(particles[i], xforce, yforce, tempcounter);
445 }
446 18 forcecounter = tempcounter; // save value back
447 18 }
448
449 // apply a force in angular direction to single particle
450 // caller needs to provide a 8bit counter that holds its value between calls (if using single particles, a counter for each particle is needed)
451 // angle is from 0-65535 (=0-360deg) angle = 0 means in positive x-direction (i.e. to the right)
452 // force is in 3.4 fixed point notation so force=16 means apply v+1 each frame (useful force range is +/- 127)
453 ✗ void ParticleSystem2D::applyAngleForce(PSparticle &part, const int8_t force, const uint16_t angle, uint8_t &counter) {
454 ✗ int8_t xforce = ((int32_t)force * cos16_t(angle)) / 32767; // force is +/- 127
455 ✗ int8_t yforce = ((int32_t)force * sin16_t(angle)) / 32767; // note: cannot use bit shifts as bit shifting is asymmetrical for positive and negative numbers and this needs to be accurate!
456 ✗ applyForce(part, xforce, yforce, counter);
457 ✗ }
458
459 ✗ void ParticleSystem2D::applyAngleForce(const uint32_t particleindex, const int8_t force, const uint16_t angle) {
460 ✗ if (advPartProps == nullptr)
461 ✗ return; // no advanced properties available
462 ✗ applyAngleForce(particles[particleindex], force, angle, advPartProps[particleindex].forcecounter);
463 }
464
465 // apply a force in angular direction to all particles
466 // angle is from 0-65535 (=0-360deg) angle = 0 means in positive x-direction (i.e. to the right)
467 ✗ void ParticleSystem2D::applyAngleForce(const int8_t force, const uint16_t angle) {
468 ✗ int8_t xforce = ((int32_t)force * cos16_t(angle)) / 32767; // force is +/- 127
469 ✗ int8_t yforce = ((int32_t)force * sin16_t(angle)) / 32767; // note: cannot use bit shifts as bit shifting is asymmetrical for positive and negative numbers and this needs to be accurate!
470 ✗ applyForce(xforce, yforce);
471 ✗ }
472
473 // apply gravity to all particles using PS global gforce setting
474 // force is in 3.4 fixed point notation, see note above
475 // note: faster than apply force since direction is always down and counter is fixed for all particles
476 100 void ParticleSystem2D::applyGravity() {
477 100 int32_t dv = calcForce_dv(gforce, gforcecounter);
478
2/2
✓ Branch 0 taken 52 times.
✓ Branch 1 taken 48 times.
100 if (dv == 0) return;
479
2/2
✓ Branch 0 taken 15278 times.
✓ Branch 1 taken 48 times.
15326 for (uint32_t i = 0; i < usedParticles; i++) {
480 // Note: not checking if particle is dead is faster as most are usually alive and if few are alive, rendering is fast anyways
481 15278 particles[i].vy = limitSpeed((int32_t)particles[i].vy - dv);
482 }
483 }
484
485 // apply gravity to single particle using system settings (use this for sources)
486 // function does not increment gravity counter, if gravity setting is disabled, this cannot be used
487 126 void ParticleSystem2D::applyGravity(PSparticle &part) {
488 126 uint32_t counterbkp = gforcecounter; // backup PS gravity counter
489 126 int32_t dv = calcForce_dv(gforce, gforcecounter);
490 126 gforcecounter = counterbkp; //save it back
491 126 part.vy = limitSpeed((int32_t)part.vy - dv);
492 126 }
493
494 // slow down particle by friction, the higher the speed, the higher the friction. a high friction coefficient slows them more (255 means instant stop)
495 // note: a coefficient smaller than 0 will speed them up (this is a feature, not a bug), coefficient larger than 255 inverts the speed, so don't do that
496 ✗ void ParticleSystem2D::applyFriction(PSparticle &part, const int32_t coefficient) {
497 // note: not checking if particle is dead can be done by caller (or can be omitted)
498 #if defined(CONFIG_IDF_TARGET_ESP32C3) || defined(ESP8266) // use bitshifts with rounding instead of division (2x faster)
499 int32_t friction = 256 - coefficient;
500 part.vx = ((int32_t)part.vx * friction + (((int32_t)part.vx >> 31) & 0xFF)) >> 8; // note: (v>>31) & 0xFF)) extracts the sign and adds 255 if negative for correct rounding using shifts
501 part.vy = ((int32_t)part.vy * friction + (((int32_t)part.vy >> 31) & 0xFF)) >> 8;
502 #else // division is faster on ESP32, S2 and S3
503 ✗ int32_t friction = 255 - coefficient;
504 ✗ part.vx = ((int32_t)part.vx * friction) / 255;
505 ✗ part.vy = ((int32_t)part.vy * friction) / 255;
506 #endif
507 ✗ }
508
509 // apply friction to all particles
510 // note: not checking if particle is dead is faster as most are usually alive and if few are alive, rendering is fast anyways
511 24 void ParticleSystem2D::applyFriction(const int32_t coefficient) {
512 #if defined(CONFIG_IDF_TARGET_ESP32C3) || defined(ESP8266) // use bitshifts with rounding instead of division (2x faster)
513 int32_t friction = 256 - coefficient;
514 for (uint32_t i = 0; i < usedParticles; i++) {
515 particles[i].vx = ((int32_t)particles[i].vx * friction + (((int32_t)particles[i].vx >> 31) & 0xFF)) >> 8; // note: (v>>31) & 0xFF)) extracts the sign and adds 255 if negative for correct rounding using shifts
516 particles[i].vy = ((int32_t)particles[i].vy * friction + (((int32_t)particles[i].vy >> 31) & 0xFF)) >> 8;
517 }
518 #else // division is faster on ESP32, S2 and S3
519 24 int32_t friction = 255 - coefficient;
520
2/2
✓ Branch 0 taken 4716 times.
✓ Branch 1 taken 24 times.
4740 for (uint32_t i = 0; i < usedParticles; i++) {
521 4716 particles[i].vx = ((int32_t)particles[i].vx * friction) / 255;
522 4716 particles[i].vy = ((int32_t)particles[i].vy * friction) / 255;
523 }
524 #endif
525 24 }
526
527 // attracts a particle to an attractor particle using the inverse square-law
528 2138 void ParticleSystem2D::pointAttractor(const uint32_t particleindex, PSparticle &attractor, const uint8_t strength, const bool swallow) {
529
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2138 times.
2138 if (advPartProps == nullptr)
530 ✗ return; // no advanced properties available
531
532 // Calculate the distance between the particle and the attractor
533 2138 int32_t dx = attractor.x - particles[particleindex].x;
534 2138 int32_t dy = attractor.y - particles[particleindex].y;
535
536 // Calculate the force based on inverse square law
537 2138 int32_t distanceSquared = dx * dx + dy * dy;
538
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2138 times.
2138 if (distanceSquared < 8192) {
539 ✗ if (swallow) { // particle is close, age it fast so it fades out, do not attract further
540 ✗ if (particles[particleindex].ttl > 7)
541 ✗ particles[particleindex].ttl -= 8;
542 else {
543 ✗ particles[particleindex].ttl = 0;
544 ✗ return;
545 }
546 }
547 ✗ distanceSquared = 2 * PS_P_RADIUS * PS_P_RADIUS; // limit the distance to avoid very high forces
548 }
549
550 2138 int32_t force = ((int32_t)strength << 16) / distanceSquared;
551 2138 int8_t xforce = (force * dx) / 1024; // scale to a lower value, found by experimenting
552 2138 int8_t yforce = (force * dy) / 1024; // note: cannot use bit shifts as bit shifting is asymmetrical for positive and negative numbers and this needs to be accurate!
553 2138 applyForce(particleindex, xforce, yforce);
554 }
555
556 // render particles to the LED buffer (uses palette to render the 8bit particle color value)
557 // if wrap is set, particles half out of bounds are rendered to the other side of the matrix
558 // warning: do not render out of bounds particles or system will crash! rendering does not check if particle is out of bounds
559 // firemode is only used for PS Fire FX
560 288 void ParticleSystem2D::render() {
561 CRGB baseRGB;
562 uint32_t brightness; // particle brightness, fades if dying
563 288 TBlendType blend = LINEARBLEND; // default color rendering: wrap palette
564
2/2
✓ Branch 0 taken 18 times.
✓ Branch 1 taken 270 times.
288 if (particlesettings.colorByAge) {
565 18 blend = LINEARBLEND_NOWRAP;
566 }
567
568
2/2
✓ Branch 0 taken 148 times.
✓ Branch 1 taken 140 times.
288 if (motionBlur) { // motion-blurring active
569
2/2
✓ Branch 0 taken 1848 times.
✓ Branch 1 taken 148 times.
1996 for (int32_t y = 0; y <= maxYpixel; y++) {
570 1848 int index = y * (maxXpixel + 1);
571
2/2
✓ Branch 0 taken 56136 times.
✓ Branch 1 taken 1848 times.
57984 for (int32_t x = 0; x <= maxXpixel; x++) {
572 56136 fast_color_scale(framebuffer[index], motionBlur); // note: could skip if only globalsmear is active but usually they are both active and scaling is fast enough
573 56136 index++;
574 }
575 }
576 }
577 else { // no blurring: clear buffer
578 140 memset(framebuffer, 0, (maxXpixel+1) * (maxYpixel+1) * sizeof(CRGB));
579 }
580
581 // go over particles and render them to the buffer
582
2/2
✓ Branch 0 taken 80817 times.
✓ Branch 1 taken 288 times.
81105 for (uint32_t i = 0; i < usedParticles; i++) {
583
4/4
✓ Branch 0 taken 4019 times.
✓ Branch 1 taken 76798 times.
✓ Branch 2 taken 750 times.
✓ Branch 3 taken 3269 times.
80817 if (particles[i].ttl == 0 || particleFlags[i].outofbounds)
584 77548 continue;
585 // generate RGB values for particle
586
2/2
✓ Branch 0 taken 5 times.
✓ Branch 1 taken 3264 times.
3269 if (fireIntesity) { // fire mode
587 5 brightness = (uint32_t)particles[i].ttl * (3 + (fireIntesity >> 5)) + 20;
588 5 brightness = min(brightness, (uint32_t)255);
589
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 baseRGB = ColorFromPaletteWLED(SEGPALETTE, brightness, 255, LINEARBLEND_NOWRAP);
590 }
591 else {
592 3264 brightness = min((particles[i].ttl << 1), (int)255);
593
1/2
✓ Branch 0 taken 3264 times.
✗ Branch 1 not taken.
3264 baseRGB = ColorFromPaletteWLED(SEGPALETTE, particles[i].hue, 255, blend);
594
2/2
✓ Branch 0 taken 92 times.
✓ Branch 1 taken 3172 times.
3264 if (particles[i].sat < 255) {
595 CHSV32 baseHSV;
596
1/2
✓ Branch 0 taken 92 times.
✗ Branch 1 not taken.
92 rgb2hsv((uint32_t((byte(baseRGB.r) << 16) | (byte(baseRGB.g) << 8) | (byte(baseRGB.b)))), baseHSV); // convert to HSV
597 92 baseHSV.s = particles[i].sat; // set the saturation
598 uint32_t tempcolor;
599
1/2
✓ Branch 0 taken 92 times.
✗ Branch 1 not taken.
92 hsv2rgb(baseHSV, tempcolor); // convert back to RGB
600 92 baseRGB = (CRGB)tempcolor;
601 }
602 }
603
1/2
✓ Branch 0 taken 3269 times.
✗ Branch 1 not taken.
3269 renderParticle(i, brightness, baseRGB, particlesettings.wrapX, particlesettings.wrapY);
604 }
605
606 // apply global size rendering
607
2/2
✓ Branch 0 taken 32 times.
✓ Branch 1 taken 256 times.
288 if (particlesize > 1) {
608 32 uint32_t passes = particlesize / 64 + 1; // number of blur passes, four passes max
609 32 uint32_t bluramount = particlesize;
610 32 uint32_t bitshift = 0;
611
2/2
✓ Branch 0 taken 70 times.
✓ Branch 1 taken 32 times.
102 for (uint32_t i = 0; i < passes; i++) {
612
2/2
✓ Branch 0 taken 8 times.
✓ Branch 1 taken 62 times.
70 if (i == 2) // for the last two passes, use higher amount of blur (results in a nicer brightness gradient with soft edges)
613 8 bitshift = 1;
614
1/2
✓ Branch 0 taken 70 times.
✗ Branch 1 not taken.
70 blur2D(framebuffer, maxXpixel + 1, maxYpixel + 1, bluramount << bitshift, bluramount << bitshift);
615 70 bluramount -= 64;
616 }
617 }
618
619 // apply 2D blur to rendered frame
620
2/2
✓ Branch 0 taken 36 times.
✓ Branch 1 taken 252 times.
288 if (smearBlur) {
621
1/2
✓ Branch 0 taken 36 times.
✗ Branch 1 not taken.
36 blur2D(framebuffer, maxXpixel + 1, maxYpixel + 1, smearBlur, smearBlur);
622 }
623
624 // transfer the framebuffer to the segment
625
2/2
✓ Branch 0 taken 3568 times.
✓ Branch 1 taken 288 times.
3856 for (int y = 0; y <= maxYpixel; y++) {
626 3568 int index = y * (maxXpixel + 1); // current row index for 1D buffer
627
2/2
✓ Branch 0 taken 108176 times.
✓ Branch 1 taken 3568 times.
111744 for (int x = 0; x <= maxXpixel; x++) {
628
3/6
✓ Branch 0 taken 108176 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 108176 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 108176 times.
✗ Branch 5 not taken.
108176 SEGMENT.setPixelColorXY(x, y, framebuffer[index++]);
629 }
630 }
631 288 }
632
633 // calculate pixel positions and brightness distribution and render the particle to local buffer or global buffer
634 3269 __attribute__((optimize("O2"))) void ParticleSystem2D::renderParticle(const uint32_t particleindex, const uint8_t brightness, const CRGB& color, const bool wrapX, const bool wrapY) {
635 3269 uint32_t size = particlesize;
636
4/4
✓ Branch 0 taken 2211 times.
✓ Branch 1 taken 1058 times.
✓ Branch 2 taken 131 times.
✓ Branch 3 taken 2080 times.
3269 if (advPartProps && advPartProps[particleindex].size > 0) // use advanced size properties (0 means use global size including single pixel rendering)
637 131 size = advPartProps[particleindex].size;
638
639
2/2
✓ Branch 0 taken 20 times.
✓ Branch 1 taken 3249 times.
3269 if (size == 0) { // single pixel rendering
640 20 uint32_t x = particles[particleindex].x >> PS_P_RADIUS_SHIFT;
641 20 uint32_t y = particles[particleindex].y >> PS_P_RADIUS_SHIFT;
642
3/4
✓ Branch 0 taken 19 times.
✓ Branch 1 taken 1 times.
✓ Branch 2 taken 19 times.
✗ Branch 3 not taken.
20 if (x <= (uint32_t)maxXpixel && y <= (uint32_t)maxYpixel) {
643 19 fast_color_add(framebuffer[x + (maxYpixel - y) * (maxXpixel + 1)], color, brightness);
644 }
645 20 return;
646 }
647 3249 uint8_t pxlbrightness[4]; // brightness values for the four pixels representing a particle
648 3249 struct {
649 int32_t x,y;
650 } pixco[4]; // particle pixel coordinates, the order is bottom left [0], bottom right[1], top right [2], top left [3] (thx @blazoncek for improved readability struct)
651 3249 bool pixelvalid[4] = {true, true, true, true}; // is set to false if pixel is out of bounds
652
653 // add half a radius as the rendering algorithm always starts at the bottom left, this leaves things positive, so shifts can be used, then shift coordinate by a full pixel (x--/y-- below)
654 3249 int32_t xoffset = particles[particleindex].x + PS_P_HALFRADIUS;
655 3249 int32_t yoffset = particles[particleindex].y + PS_P_HALFRADIUS;
656 3249 int32_t dx = xoffset & (PS_P_RADIUS - 1); // relativ particle position in subpixel space
657 3249 int32_t dy = yoffset & (PS_P_RADIUS - 1); // modulo replaced with bitwise AND, as radius is always a power of 2
658 3249 int32_t x = (xoffset >> PS_P_RADIUS_SHIFT); // divide by PS_P_RADIUS which is 64, so can bitshift (compiler can not optimize integer)
659 3249 int32_t y = (yoffset >> PS_P_RADIUS_SHIFT);
660
661 // set the four raw pixel coordinates
662 3249 pixco[1].x = pixco[2].x = x; // bottom right & top right
663 3249 pixco[2].y = pixco[3].y = y; // top right & top left
664 3249 x--; // shift by a full pixel here, this is skipped above to not do -1 and then +1
665 3249 y--;
666 3249 pixco[0].x = pixco[3].x = x; // bottom left & top left
667 3249 pixco[0].y = pixco[1].y = y; // bottom left & bottom right
668
669 // calculate brightness values for all four pixels representing a particle using linear interpolation
670 // could check for out of frame pixels here but calculating them is faster (very few are out)
671 // precalculate values for speed optimization
672 3249 int32_t precal1 = (int32_t)PS_P_RADIUS - dx;
673 3249 int32_t precal2 = ((int32_t)PS_P_RADIUS - dy) * brightness;
674 3249 int32_t precal3 = dy * brightness;
675 3249 pxlbrightness[0] = (precal1 * precal2) >> PS_P_SURFACE; // bottom left value equal to ((PS_P_RADIUS - dx) * (PS_P_RADIUS-dy) * brightness) >> PS_P_SURFACE
676 3249 pxlbrightness[1] = (dx * precal2) >> PS_P_SURFACE; // bottom right value equal to (dx * (PS_P_RADIUS-dy) * brightness) >> PS_P_SURFACE
677 3249 pxlbrightness[2] = (dx * precal3) >> PS_P_SURFACE; // top right value equal to (dx * dy * brightness) >> PS_P_SURFACE
678 3249 pxlbrightness[3] = (precal1 * precal3) >> PS_P_SURFACE; // top left value equal to ((PS_P_RADIUS-dx) * dy * brightness) >> PS_P_SURFACE
679
680
4/4
✓ Branch 0 taken 2201 times.
✓ Branch 1 taken 1048 times.
✓ Branch 2 taken 83 times.
✓ Branch 3 taken 2118 times.
3249 if (advPartProps && advPartProps[particleindex].size > 1) { //render particle to a bigger size
681 83 CRGB renderbuffer[100]; // 10x10 pixel buffer
682 83 memset(renderbuffer, 0, sizeof(renderbuffer)); // clear buffer
683 //particle size to pixels: < 64 is 4x4, < 128 is 6x6, < 192 is 8x8, bigger is 10x10
684 //first, render the pixel to the center of the renderbuffer, then apply 2D blurring
685 83 fast_color_add(renderbuffer[4 + (4 * 10)], color, pxlbrightness[0]); // oCrder is: bottom left, bottom right, top right, top left
686 83 fast_color_add(renderbuffer[5 + (4 * 10)], color, pxlbrightness[1]);
687 83 fast_color_add(renderbuffer[5 + (5 * 10)], color, pxlbrightness[2]);
688 83 fast_color_add(renderbuffer[4 + (5 * 10)], color, pxlbrightness[3]);
689 83 uint32_t rendersize = 2; // initialize render size, minimum is 4x4 pixels, it is incremented int he loop below to start with 4
690 83 uint32_t offset = 4; // offset to zero coordinate to write/read data in renderbuffer (actually needs to be 3, is decremented in the loop below)
691 83 uint32_t maxsize = advPartProps[particleindex].size;
692 83 uint32_t xsize = maxsize;
693 83 uint32_t ysize = maxsize;
694
1/2
✓ Branch 0 taken 83 times.
✗ Branch 1 not taken.
83 if (advPartSize) { // use advanced size control
695
1/2
✓ Branch 0 taken 83 times.
✗ Branch 1 not taken.
83 if (advPartSize[particleindex].asymmetry > 0)
696 83 getParticleXYsize(&advPartProps[particleindex], &advPartSize[particleindex], xsize, ysize);
697 83 maxsize = (xsize > ysize) ? xsize : ysize; // choose the bigger of the two
698 }
699 83 maxsize = maxsize/64 + 1; // number of blur passes depends on maxsize, four passes max
700 83 uint32_t bitshift = 0;
701
2/2
✓ Branch 0 taken 83 times.
✓ Branch 1 taken 83 times.
166 for (uint32_t i = 0; i < maxsize; i++) {
702
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 83 times.
83 if (i == 2) //for the last two passes, use higher amount of blur (results in a nicer brightness gradient with soft edges)
703 ✗ bitshift = 1;
704 83 rendersize += 2;
705 83 offset--;
706 83 blur2D(renderbuffer, rendersize, rendersize, xsize << bitshift, ysize << bitshift, offset, offset, true);
707 83 xsize = xsize > 64 ? xsize - 64 : 0;
708 83 ysize = ysize > 64 ? ysize - 64 : 0;
709 }
710
711 // calculate origin coordinates to render the particle to in the framebuffer
712 83 uint32_t xfb_orig = x - (rendersize>>1) + 1 - offset;
713 83 uint32_t yfb_orig = y - (rendersize>>1) + 1 - offset;
714 83 uint32_t xfb, yfb; // coordinates in frame buffer to write to note: by making this uint, only overflow has to be checked (spits a warning though)
715
716 //note on y-axis flip: WLED has the y-axis defined from top to bottom, so y coordinates must be flipped. doing this in the buffer xfer clashes with 1D/2D combined rendering, which does not invert y
717 // transferring the 1D buffer in inverted fashion will flip the x-axis of overlaid 2D FX, so the y-axis flip is done here so the buffer is flipped in y, giving correct results
718
719 // transfer particle renderbuffer to framebuffer
720
2/2
✓ Branch 0 taken 332 times.
✓ Branch 1 taken 83 times.
415 for (uint32_t xrb = offset; xrb < rendersize + offset; xrb++) {
721 332 xfb = xfb_orig + xrb;
722
2/2
✓ Branch 0 taken 6 times.
✓ Branch 1 taken 326 times.
332 if (xfb > (uint32_t)maxXpixel) {
723
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 6 times.
6 if (wrapX) { // wrap x to the other side if required
724 ✗ if (xfb > (uint32_t)maxXpixel << 1) // xfb is "negative", handle it
725 ✗ xfb = (maxXpixel + 1) + (int32_t)xfb; // this always overflows to within bounds
726 else
727 ✗ xfb = xfb % (maxXpixel + 1); // note: without the above "negative" check, this works only for powers of 2
728 }
729 else
730 6 continue;
731 }
732
733
2/2
✓ Branch 0 taken 1304 times.
✓ Branch 1 taken 326 times.
1630 for (uint32_t yrb = offset; yrb < rendersize + offset; yrb++) {
734 1304 yfb = yfb_orig + yrb;
735
2/2
✓ Branch 0 taken 59 times.
✓ Branch 1 taken 1245 times.
1304 if (yfb > (uint32_t)maxYpixel) {
736
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 59 times.
59 if (wrapY) {// wrap y to the other side if required
737 ✗ if (yfb > (uint32_t)maxYpixel << 1) // yfb is "negative", handle it
738 ✗ yfb = (maxYpixel + 1) + (int32_t)yfb; // this always overflows to within bounds
739 else
740 ✗ yfb = yfb % (maxYpixel + 1); // note: without the above "negative" check, this works only for powers of 2
741 }
742 else
743 59 continue;
744 }
745 1245 fast_color_add(framebuffer[xfb + (maxYpixel - yfb) * (maxXpixel + 1)], renderbuffer[xrb + yrb * 10]);
746 }
747 }
748 83 } else { // standard rendering (2x2 pixels)
749 // check for out of frame pixels and wrap them if required: x,y is bottom left pixel coordinate of the particle
750
2/2
✓ Branch 0 taken 23 times.
✓ Branch 1 taken 3143 times.
3166 if (x < 0) { // left pixels out of frame
751
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 23 times.
23 if (wrapX) { // wrap x to the other side if required
752 ✗ pixco[0].x = pixco[3].x = maxXpixel;
753 } else {
754 23 pixelvalid[0] = pixelvalid[3] = false; // out of bounds
755 }
756 }
757
2/2
✓ Branch 0 taken 9 times.
✓ Branch 1 taken 3134 times.
3143 else if (pixco[1].x > (int32_t)maxXpixel) { // right pixels, only has to be checked if left pixel is in frame
758
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 9 times.
9 if (wrapX) { // wrap y to the other side if required
759 ✗ pixco[1].x = pixco[2].x = 0;
760 } else {
761 9 pixelvalid[1] = pixelvalid[2] = false; // out of bounds
762 }
763 }
764
765
2/2
✓ Branch 0 taken 56 times.
✓ Branch 1 taken 3110 times.
3166 if (y < 0) { // bottom pixels out of frame
766
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 56 times.
56 if (wrapY) { // wrap y to the other side if required
767 ✗ pixco[0].y = pixco[1].y = maxYpixel;
768 } else {
769 56 pixelvalid[0] = pixelvalid[1] = false; // out of bounds
770 }
771 }
772
2/2
✓ Branch 0 taken 71 times.
✓ Branch 1 taken 3039 times.
3110 else if (pixco[2].y > maxYpixel) { // top pixels
773
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 71 times.
71 if (wrapY) { // wrap y to the other side if required
774 ✗ pixco[2].y = pixco[3].y = 0;
775 } else {
776 71 pixelvalid[2] = pixelvalid[3] = false; // out of bounds
777 }
778 }
779
2/2
✓ Branch 0 taken 12664 times.
✓ Branch 1 taken 3166 times.
15830 for (uint32_t i = 0; i < 4; i++) {
780
2/2
✓ Branch 0 taken 12357 times.
✓ Branch 1 taken 307 times.
12664 if (pixelvalid[i])
781 12357 fast_color_add(framebuffer[pixco[i].x + (maxYpixel - pixco[i].y) * (maxXpixel + 1)], color, pxlbrightness[i]); // order is: bottom left, bottom right, top right, top left
782 }
783 }
784 }
785
786 // detect collisions in an array of particles and handle them
787 // uses binning by dividing the frame into slices in x direction which is efficient if using gravity in y direction (but less efficient for FX that use forces in x direction)
788 // for code simplicity, no y slicing is done, making very tall matrix configurations less efficient
789 // note: also tested adding y slicing, it gives diminishing returns, some FX even get slower. FX not using gravity would benefit with a 10% FPS improvement
790 86 void ParticleSystem2D::handleCollisions() {
791 86 uint32_t collDistSq = particleHardRadius << 1; // distance is double the radius note: particleHardRadius is updated when setting global particle size
792 86 collDistSq = collDistSq * collDistSq; // square it for faster comparison (square is one operation)
793 // note: partices are binned in x-axis, assumption is that no more than half of the particles are in the same bin
794 // if they are, collisionStartIdx is increased so each particle collides at least every second frame (which still gives decent collisions)
795 86 constexpr int BIN_WIDTH = 6 * PS_P_RADIUS; // width of a bin in sub-pixels
796 86 int32_t overlap = particleHardRadius << 1; // overlap bins to include edge particles to neighbouring bins
797 86 if (advPartProps) //may be using individual particle size
798 36 overlap += 512; // add 2 * max radius (approximately)
799 86 uint32_t maxBinParticles = max((uint32_t)50, (usedParticles + 1) / 2); // assume no more than half of the particles are in the same bin, do not bin small amounts of particles
800 86 uint32_t numBins = (maxX + (BIN_WIDTH - 1)) / BIN_WIDTH; // number of bins in x direction
801 86 uint16_t binIndices[maxBinParticles]; // creat array on stack for indices, 2kB max for 1024 particles (ESP32_MAXPARTICLES/2)
802 uint32_t binParticleCount; // number of particles in the current bin
803 86 uint16_t nextFrameStartIdx = hw_random16(usedParticles); // index of the first particle in the next frame (set to fixed value if bin overflow)
804 86 uint32_t pidx = collisionStartIdx; //start index in case a bin is full, process remaining particles next frame
805
806 // fill the binIndices array for this bin
807
2/2
✓ Branch 0 taken 416 times.
✓ Branch 1 taken 86 times.
502 for (uint32_t bin = 0; bin < numBins; bin++) {
808 416 binParticleCount = 0; // reset for this bin
809 416 int32_t binStart = bin * BIN_WIDTH - overlap; // note: first bin will extend to negative, but that is ok as out of bounds particles are ignored
810 416 int32_t binEnd = binStart + BIN_WIDTH + overlap; // note: last bin can be out of bounds, see above;
811
812 // fill the binIndices array for this bin
813
2/2
✓ Branch 0 taken 110864 times.
✓ Branch 1 taken 389 times.
111253 for (uint32_t i = 0; i < usedParticles; i++) {
814
2/2
✓ Branch 0 taken 10992 times.
✓ Branch 1 taken 99872 times.
110864 if (particles[pidx].ttl > 0) { // is alive
815
4/4
✓ Branch 0 taken 6076 times.
✓ Branch 1 taken 4916 times.
✓ Branch 2 taken 4270 times.
✓ Branch 3 taken 1806 times.
10992 if (particles[pidx].x >= binStart && particles[pidx].x <= binEnd) { // >= and <= to include particles on the edge of the bin (overlap to ensure boarder particles collide with adjacent bins)
816
4/4
✓ Branch 0 taken 4173 times.
✓ Branch 1 taken 97 times.
✓ Branch 2 taken 4153 times.
✓ Branch 3 taken 20 times.
4270 if(particleFlags[pidx].outofbounds == 0 && particleFlags[pidx].collide) { // particle is in frame and does collide note: checking flags is quite slow and usually these are set, so faster to check here
817
2/2
✓ Branch 0 taken 27 times.
✓ Branch 1 taken 4126 times.
4153 if (binParticleCount >= maxBinParticles) { // bin is full, more particles in this bin so do the rest next frame
818 27 nextFrameStartIdx = pidx; // bin overflow can only happen once as bin size is at least half of the particles (or half +1)
819 27 break;
820 }
821 4126 binIndices[binParticleCount++] = pidx;
822 }
823 }
824 }
825 110837 pidx++;
826
2/2
✓ Branch 0 taken 407 times.
✓ Branch 1 taken 110430 times.
110837 if (pidx >= usedParticles) pidx = 0; // wrap around
827 }
828
829
2/2
✓ Branch 0 taken 4126 times.
✓ Branch 1 taken 416 times.
4542 for (uint32_t i = 0; i < binParticleCount; i++) { // go though all 'higher number' particles in this bin and see if any of those are in close proximity and if they are, make them collide
830 4126 uint32_t idx_i = binIndices[i];
831
2/2
✓ Branch 0 taken 158062 times.
✓ Branch 1 taken 4126 times.
162188 for (uint32_t j = i + 1; j < binParticleCount; j++) { // check against higher number particles
832 158062 uint32_t idx_j = binIndices[j];
833
2/2
✓ Branch 0 taken 158024 times.
✓ Branch 1 taken 38 times.
158062 if (advPartProps) { //may be using individual particle size
834
1/2
✓ Branch 0 taken 158024 times.
✗ Branch 1 not taken.
158024 setParticleSize(particlesize); // updates base particleHardRadius
835 158024 collDistSq = (particleHardRadius << 1) + (((uint32_t)advPartProps[idx_i].size + (uint32_t)advPartProps[idx_j].size) >> 1); // collision distance note: not 100% clear why the >> 1 is needed, but it is.
836 158024 collDistSq = collDistSq * collDistSq; // square it for faster comparison
837 }
838 158062 int32_t dx = (particles[idx_j].x + particles[idx_j].vx) - (particles[idx_i].x + particles[idx_i].vx); // distance with lookahead
839
2/2
✓ Branch 0 taken 59633 times.
✓ Branch 1 taken 98429 times.
158062 if (dx * dx < collDistSq) { // check x direction, if close, check y direction (squaring is faster than abs() or dual compare)
840 59633 int32_t dy = (particles[idx_j].y + particles[idx_j].vy) - (particles[idx_i].y + particles[idx_i].vy); // distance with lookahead
841
2/2
✓ Branch 0 taken 26666 times.
✓ Branch 1 taken 32967 times.
59633 if (dy * dy < collDistSq) // particles are close
842
1/2
✓ Branch 0 taken 26666 times.
✗ Branch 1 not taken.
26666 collideParticles(particles[idx_i], particles[idx_j], dx, dy, collDistSq);
843 }
844 }
845 }
846 }
847 86 collisionStartIdx = nextFrameStartIdx; // set the start index for the next frame
848
2/2
✓ Branch 0 taken 36 times.
✓ Branch 1 taken 50 times.
172 }
849
850 // handle a collision if close proximity is detected, i.e. dx and/or dy smaller than 2*PS_P_RADIUS
851 // takes two pointers to the particles to collide and the particle hardness (softer means more energy lost in collision, 255 means full hard)
852 26666 __attribute__((optimize("O2"))) void ParticleSystem2D::collideParticles(PSparticle &particle1, PSparticle &particle2, int32_t dx, int32_t dy, const uint32_t collDistSq) {
853 26666 int32_t distanceSquared = dx * dx + dy * dy;
854 // Calculate relative velocity note: could zero check but that does not improve overall speed but deminish it as that is rarely the case and pushing is still required
855 26666 int32_t relativeVx = (int32_t)particle2.vx - (int32_t)particle1.vx;
856 26666 int32_t relativeVy = (int32_t)particle2.vy - (int32_t)particle1.vy;
857
858 // if dx and dy are zero (i.e. same position) give them an offset, if speeds are also zero, also offset them (pushes particles apart if they are clumped before enabling collisions)
859
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 26664 times.
26666 if (distanceSquared == 0) {
860 // Adjust positions based on relative velocity direction
861 2 dx = -1;
862
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 if (relativeVx < 0) // if true, particle2 is on the right side
863 dx = 1;
864
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 else if (relativeVx == 0)
865 2 relativeVx = 1;
866
867 2 dy = -1;
868
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 if (relativeVy < 0)
869 dy = 1;
870
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 else if (relativeVy == 0)
871 2 relativeVy = 1;
872
873 distanceSquared = 2; // 1 + 1
874 }
875
876 // Calculate dot product of relative velocity and relative distance
877 26666 int32_t dotProduct = (dx * relativeVx + dy * relativeVy); // is always negative if moving towards each other
878
879
2/2
✓ Branch 0 taken 5914 times.
✓ Branch 1 taken 20752 times.
26666 if (dotProduct < 0) {// particles are moving towards each other
880 // integer math used to avoid floats.
881 // overflow check: dx/dy are 7bit, relativV are 8bit -> dotproduct is 15bit, dotproduct/distsquared ist 8b, multiplied by collisionhardness of 8bit. so a 16bit shift is ok, make it 15 to be sure no overflows happen
882 // note: cannot use right shifts as bit shifting in right direction is asymmetrical for positive and negative numbers and this needs to be accurate! the trick is: only shift positive numers
883 // Calculate new velocities after collision
884 5914 int32_t surfacehardness = 1 + max(collisionHardness, (int32_t)PS_P_MINSURFACEHARDNESS); // if particles are soft, the impulse must stay above a limit or collisions slip through at higher speeds, 170 seems to be a good value
885 5914 int32_t impulse = (((((-dotProduct) << 15) / distanceSquared) * surfacehardness) >> 8); // note: inverting before bitshift corrects for asymmetry in right-shifts (is slightly faster)
886
887 #if defined(CONFIG_IDF_TARGET_ESP32C3) || defined(ESP8266) // use bitshifts with rounding instead of division (2x faster)
888 int32_t ximpulse = (impulse * dx + ((dx >> 31) & 32767)) >> 15; // note: extracting sign bit and adding rounding value to correct for asymmetry in right shifts
889 int32_t yimpulse = (impulse * dy + ((dy >> 31) & 32767)) >> 15;
890 #else
891 5914 int32_t ximpulse = (impulse * dx) / 32767;
892 5914 int32_t yimpulse = (impulse * dy) / 32767;
893 #endif
894 5914 particle1.vx -= ximpulse; // note: impulse is inverted, so subtracting it
895 5914 particle1.vy -= yimpulse;
896 5914 particle2.vx += ximpulse;
897 5914 particle2.vy += yimpulse;
898
899
3/4
✓ Branch 0 taken 33 times.
✓ Branch 1 taken 5881 times.
✓ Branch 2 taken 33 times.
✗ Branch 3 not taken.
5914 if (collisionHardness < PS_P_MINSURFACEHARDNESS && (SEGMENT.call & 0x07) == 0) { // if particles are soft, they become 'sticky' i.e. apply some friction (they do pile more nicely and stop sloshing around)
900 33 const uint32_t coeff = collisionHardness + (255 - PS_P_MINSURFACEHARDNESS);
901 // Note: could call applyFriction, but this is faster and speed is key here
902 #if defined(CONFIG_IDF_TARGET_ESP32C3) || defined(ESP8266) // use bitshifts with rounding instead of division (2x faster)
903 particle1.vx = ((int32_t)particle1.vx * coeff + (((int32_t)particle1.vx >> 31) & 0xFF)) >> 8; // note: (v>>31) & 0xFF)) extracts the sign and adds 255 if negative for correct rounding using shifts
904 particle1.vy = ((int32_t)particle1.vy * coeff + (((int32_t)particle1.vy >> 31) & 0xFF)) >> 8;
905 particle2.vx = ((int32_t)particle2.vx * coeff + (((int32_t)particle2.vx >> 31) & 0xFF)) >> 8;
906 particle2.vy = ((int32_t)particle2.vy * coeff + (((int32_t)particle2.vy >> 31) & 0xFF)) >> 8;
907 #else // division is faster on ESP32, S2 and S3
908 33 particle1.vx = ((int32_t)particle1.vx * coeff) / 255;
909 33 particle1.vy = ((int32_t)particle1.vy * coeff) / 255;
910 33 particle2.vx = ((int32_t)particle2.vx * coeff) / 255;
911 33 particle2.vy = ((int32_t)particle2.vy * coeff) / 255;
912 #endif
913 }
914
915 // particles have volume, push particles apart if they are too close
916 // tried lots of configurations, it works best if not moved but given a little velocity, it tends to oscillate less this way
917 // when hard pushing by offsetting position, they sink into each other under gravity
918 // a problem with giving velocity is, that on harder collisions, this adds up as it is not dampened enough, so add friction in the FX if required
919
2/2
✓ Branch 0 taken 3652 times.
✓ Branch 1 taken 2262 times.
5914 if (distanceSquared < collDistSq && dotProduct > -250) { // too close and also slow, push them apart
920 3652 int32_t notsorandom = dotProduct & 0x01; //dotprouct LSB should be somewhat random, so no need to calculate a random number
921 3652 int32_t pushamount = 1 + ((250 + dotProduct) >> 6); // the closer dotproduct is to zero, the closer the particles are
922 3652 int32_t push = 0;
923
2/2
✓ Branch 0 taken 1856 times.
✓ Branch 1 taken 1796 times.
3652 if (dx < 0) // particle 1 is on the right
924 push = pushamount;
925
2/2
✓ Branch 0 taken 1841 times.
✓ Branch 1 taken 15 times.
1856 else if (dx > 0)
926 1841 push = -pushamount;
927 else { // on the same x coordinate, shift it a little so they do not stack
928
2/2
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 11 times.
15 if (notsorandom)
929 4 particle1.x++; // move it so pile collapses
930 else
931 11 particle1.x--;
932 }
933 3652 particle1.vx += push;
934 3652 push = 0;
935
2/2
✓ Branch 0 taken 1888 times.
✓ Branch 1 taken 1764 times.
3652 if (dy < 0)
936 push = pushamount;
937
2/2
✓ Branch 0 taken 1859 times.
✓ Branch 1 taken 29 times.
1888 else if (dy > 0)
938 1859 push = -pushamount;
939 else { // dy==0
940
2/2
✓ Branch 0 taken 13 times.
✓ Branch 1 taken 16 times.
29 if (notsorandom)
941 13 particle1.y++; // move it so pile collapses
942 else
943 16 particle1.y--;
944 }
945 3652 particle1.vy += push;
946
947 // note: pushing may push particles out of frame, if bounce is active, it will move it back as position will be limited to within frame, if bounce is disabled: bye bye
948
2/2
✓ Branch 0 taken 25 times.
✓ Branch 1 taken 3627 times.
3652 if (collisionHardness < 5) { // if they are very soft, stop slow particles completely to make them stick to each other
949 25 particle1.vx = 0;
950 25 particle1.vy = 0;
951 25 particle2.vx = 0;
952 25 particle2.vy = 0;
953 //push them apart
954 25 particle1.x += push;
955 25 particle1.y += push;
956 }
957 }
958 }
959 26666 }
960
961 // update size and pointers (memory location and size can change dynamically)
962 // note: do not access the PS class in FX befor running this function (or it messes up SEGENV.data)
963 288 void ParticleSystem2D::updateSystem(void) {
964 PSPRINTLN("updateSystem2D");
965 288 setMatrixSize(SEGMENT.vWidth(), SEGMENT.vHeight());
966 288 updatePSpointers(advPartProps != nullptr, advPartSize != nullptr); // update pointers to PS data, also updates availableParticles
967 PSPRINTLN("\n END update System2D, running FX...");
968 288 }
969
970 // set the pointers for the class (this only has to be done once and not on every FX call, only the class pointer needs to be reassigned to SEGENV.data every time)
971 // function returns the pointer to the next byte available for the FX (if it assigned more memory for other stuff using the above allocate function)
972 // FX handles the PSsources, need to tell this function how many there are
973 352 void ParticleSystem2D::updatePSpointers(bool isadvanced, bool sizecontrol) {
974 PSPRINTLN("updatePSpointers");
975 // Note on memory alignment:
976 // a pointer MUST be 4 byte aligned. sizeof() in a struct/class is always aligned to the largest element. if it contains a 32bit, it will be padded to 4 bytes, 16bit is padded to 2byte alignment.
977 // The PS is aligned to 4 bytes, a PSparticle is aligned to 2 and a struct containing only byte sized variables is not aligned at all and may need to be padded when dividing the memoryblock.
978 // by making sure that the number of sources and particles is a multiple of 4, padding can be skipped here as alignent is ensured, independent of struct sizes.
979 352 particles = reinterpret_cast<PSparticle *>(this + 1); // pointer to particles
980 352 particleFlags = reinterpret_cast<PSparticleFlags *>(particles + numParticles); // pointer to particle flags
981 352 sources = reinterpret_cast<PSsource *>(particleFlags + numParticles); // pointer to source(s) at data+sizeof(ParticleSystem2D)
982 352 framebuffer = reinterpret_cast<CRGB *>(sources + numSources); // pointer to framebuffer
983 // align pointer after framebuffer
984 352 uintptr_t p = reinterpret_cast<uintptr_t>(framebuffer + (maxXpixel+1)*(maxYpixel+1));
985 352 p = (p + 3) & ~0x03; // align to 4-byte boundary
986 352 PSdataEnd = reinterpret_cast<uint8_t *>(p); // pointer to first available byte after the PS for FX additional data
987
2/2
✓ Branch 0 taken 110 times.
✓ Branch 1 taken 242 times.
352 if (isadvanced) {
988 110 advPartProps = reinterpret_cast<PSadvancedParticle *>(PSdataEnd);
989 110 PSdataEnd = reinterpret_cast<uint8_t *>(advPartProps + numParticles);
990
2/2
✓ Branch 0 taken 22 times.
✓ Branch 1 taken 88 times.
110 if (sizecontrol) {
991 22 advPartSize = reinterpret_cast<PSsizeControl *>(PSdataEnd);
992 22 PSdataEnd = reinterpret_cast<uint8_t *>(advPartSize + numParticles);
993 }
994 }
995 #ifdef DEBUG_PS
996 Serial.printf_P(PSTR(" particles %p "), particles);
997 Serial.printf_P(PSTR(" sources %p "), sources);
998 Serial.printf_P(PSTR(" adv. props %p "), advPartProps);
999 Serial.printf_P(PSTR(" adv. ctrl %p "), advPartSize);
1000 Serial.printf_P(PSTR("end %p\n"), PSdataEnd);
1001 #endif
1002
1003 352 }
1004
1005 // blur a matrix in x and y direction, blur can be asymmetric in x and y
1006 // for speed, 1D array and 32bit variables are used, make sure to limit them to 8bit (0-255) or result is undefined
1007 // to blur a subset of the buffer, change the xsize/ysize and set xstart/ystart to the desired starting coordinates (default start is 0/0)
1008 // subset blurring only works on 10x10 buffer (single particle rendering), if other sizes are needed, buffer width must be passed as parameter
1009 189 void blur2D(CRGB *colorbuffer, uint32_t xsize, uint32_t ysize, uint32_t xblur, uint32_t yblur, uint32_t xstart, uint32_t ystart, bool isparticle) {
1010 CRGB seeppart, carryover;
1011 189 uint32_t seep = xblur >> 1;
1012 189 uint32_t width = xsize; // width of the buffer, used to calculate the index of the pixel
1013
1014
2/2
✓ Branch 0 taken 83 times.
✓ Branch 1 taken 106 times.
189 if (isparticle) { //first and last row are always black in first pass of particle rendering
1015 83 ystart++;
1016 83 ysize--;
1017 83 width = 10; // buffer size is 10x10
1018 }
1019
1020
2/2
✓ Branch 0 taken 1620 times.
✓ Branch 1 taken 189 times.
1809 for (uint32_t y = ystart; y < ystart + ysize; y++) {
1021 carryover = BLACK;
1022 1620 uint32_t indexXY = xstart + y * width;
1023
2/2
✓ Branch 0 taken 42993 times.
✓ Branch 1 taken 1620 times.
44613 for (uint32_t x = xstart; x < xstart + xsize; x++) {
1024 42993 seeppart = colorbuffer[indexXY]; // create copy of current color
1025 42993 fast_color_scale(seeppart, seep); // scale it and seep to neighbours
1026
2/2
✓ Branch 0 taken 41622 times.
✓ Branch 1 taken 1371 times.
42993 if (x > 0) {
1027 41622 fast_color_add(colorbuffer[indexXY - 1], seeppart);
1028
2/2
✓ Branch 0 taken 1437 times.
✓ Branch 1 taken 40185 times.
41622 if (carryover) // note: check adds overhead but is faster on average
1029 1437 fast_color_add(colorbuffer[indexXY], carryover);
1030 }
1031 42993 carryover = seeppart;
1032 42993 indexXY++; // next pixel in x direction
1033 }
1034 }
1035
1036
2/2
✓ Branch 0 taken 83 times.
✓ Branch 1 taken 106 times.
189 if (isparticle) { // first and last row are now smeared
1037 83 ystart--;
1038 83 ysize++;
1039 }
1040
1041 189 seep = yblur >> 1;
1042
2/2
✓ Branch 0 taken 3099 times.
✓ Branch 1 taken 189 times.
3288 for (uint32_t x = xstart; x < xstart + xsize; x++) {
1043 carryover = BLACK;
1044 3099 uint32_t indexXY = x + ystart * width;
1045
2/2
✓ Branch 0 taken 43325 times.
✓ Branch 1 taken 3099 times.
46424 for (uint32_t y = ystart; y < ystart + ysize; y++) {
1046 43325 seeppart = colorbuffer[indexXY]; // create copy of current color
1047 43325 fast_color_scale(seeppart, seep); // scale it and seep to neighbours
1048
2/2
✓ Branch 0 taken 40558 times.
✓ Branch 1 taken 2767 times.
43325 if (y > 0) {
1049 40558 fast_color_add(colorbuffer[indexXY - width], seeppart);
1050
2/2
✓ Branch 0 taken 1734 times.
✓ Branch 1 taken 38824 times.
40558 if (carryover) // note: check adds overhead but is faster on average
1051 1734 fast_color_add(colorbuffer[indexXY], carryover);
1052 }
1053 43325 carryover = seeppart;
1054 43325 indexXY += width; // next pixel in y direction
1055 }
1056 }
1057 189 }
1058
1059 //non class functions to use for initialization
1060 64 uint32_t calculateNumberOfParticles2D(uint32_t const pixels, const bool isadvanced, const bool sizecontrol) {
1061 64 uint32_t numberofParticles = pixels; // 1 particle per pixel (for example 512 particles on 32x16)
1062 #ifdef ESP8266
1063 uint32_t particlelimit = ESP8266_MAXPARTICLES; // maximum number of paticles allowed (based on one segment of 16x16 and 4k effect ram)
1064 #elif ARDUINO_ARCH_ESP32S2
1065 uint32_t particlelimit = ESP32S2_MAXPARTICLES; // maximum number of paticles allowed (based on one segment of 32x32 and 24k effect ram)
1066 #else
1067 64 uint32_t particlelimit = ESP32_MAXPARTICLES; // maximum number of paticles allowed (based on two segments of 32x32 and 40k effect ram)
1068 #endif
1069 64 numberofParticles = max((uint32_t)4, min(numberofParticles, particlelimit)); // limit to 4 - particlelimit
1070
2/2
✓ Branch 0 taken 20 times.
✓ Branch 1 taken 44 times.
64 if (isadvanced) // advanced property array needs ram, reduce number of particles to use the same amount
1071 20 numberofParticles = (numberofParticles * sizeof(PSparticle)) / (sizeof(PSparticle) + sizeof(PSadvancedParticle));
1072
2/2
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 60 times.
64 if (sizecontrol) // advanced property array needs ram, reduce number of particles
1073 4 numberofParticles /= 8; // if advanced size control is used, much fewer particles are needed note: if changing this number, adjust FX using this accordingly
1074
1075 //make sure it is a multiple of 4 for proper memory alignment (easier than using padding bytes)
1076 64 numberofParticles = (numberofParticles+3) & ~0x03;
1077 64 return numberofParticles;
1078 }
1079
1080 64 uint32_t calculateNumberOfSources2D(uint32_t pixels, uint32_t requestedsources) {
1081 #ifdef ESP8266
1082 int numberofSources = min((pixels) / 8, (uint32_t)requestedsources);
1083 numberofSources = max(1, min(numberofSources, ESP8266_MAXSOURCES)); // limit
1084 #elif ARDUINO_ARCH_ESP32S2
1085 int numberofSources = min((pixels) / 6, (uint32_t)requestedsources);
1086 numberofSources = max(1, min(numberofSources, ESP32S2_MAXSOURCES)); // limit
1087 #else
1088 64 int numberofSources = min((pixels) / 4, (uint32_t)requestedsources);
1089 64 numberofSources = max(1, min(numberofSources, ESP32_MAXSOURCES)); // limit
1090 #endif
1091 // make sure it is a multiple of 4 for proper memory alignment
1092 64 numberofSources = (numberofSources+3) & ~0x03;
1093 64 return numberofSources;
1094 }
1095
1096 //allocate memory for particle system class, particles, sprays plus additional memory requested by FX //TODO: add percentofparticles like in 1D to reduce memory footprint of some FX?
1097 64 bool allocateParticleSystemMemory2D(uint32_t numparticles, uint32_t numsources, bool isadvanced, bool sizecontrol, uint32_t additionalbytes) {
1098 PSPRINTLN("PS 2D alloc");
1099 PSPRINTLN("numparticles:" + String(numparticles) + " numsources:" + String(numsources) + " additionalbytes:" + String(additionalbytes));
1100 64 uint32_t requiredmemory = sizeof(ParticleSystem2D);
1101 // functions above make sure numparticles is a multiple of 4 bytes (to avoid alignment issues)
1102 64 requiredmemory += sizeof(PSparticleFlags) * numparticles;
1103 64 requiredmemory += sizeof(PSparticle) * numparticles;
1104
2/2
✓ Branch 0 taken 20 times.
✓ Branch 1 taken 44 times.
64 if (isadvanced)
1105 20 requiredmemory += sizeof(PSadvancedParticle) * numparticles;
1106
2/2
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 60 times.
64 if (sizecontrol)
1107 4 requiredmemory += sizeof(PSsizeControl) * numparticles;
1108 64 requiredmemory += sizeof(PSsource) * numsources;
1109 64 requiredmemory += sizeof(CRGB) * SEGMENT.virtualLength(); // virtualLength is witdh * height
1110 64 requiredmemory += additionalbytes + 3; // add 3 to ensure there is room for stuffing bytes
1111 //requiredmemory = (requiredmemory + 3) & ~0x03; // align memory block to next 4-byte boundary
1112 PSPRINTLN("mem alloc: " + String(requiredmemory));
1113 64 return(SEGMENT.allocateData(requiredmemory));
1114 }
1115
1116 // initialize Particle System, allocate additional bytes if needed (pointer to those bytes can be read from particle system class: PSdataEnd)
1117 80 bool initParticleSystem2D(ParticleSystem2D *&PartSys, uint32_t requestedsources, uint32_t additionalbytes, bool advanced, bool sizecontrol) {
1118 PSPRINT("PS 2D init ");
1119
2/2
✓ Branch 0 taken 16 times.
✓ Branch 1 taken 64 times.
80 if (!strip().isMatrix) return false; // only for 2D
1120 64 uint32_t cols = SEGMENT.virtualWidth();
1121 64 uint32_t rows = SEGMENT.virtualHeight();
1122 64 uint32_t pixels = cols * rows;
1123
1124 64 uint32_t numparticles = calculateNumberOfParticles2D(pixels, advanced, sizecontrol);
1125 PSPRINT(" segmentsize:" + String(cols) + " x " + String(rows));
1126 PSPRINT(" request numparticles:" + String(numparticles));
1127 64 uint32_t numsources = calculateNumberOfSources2D(pixels, requestedsources);
1128
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 64 times.
64 if (!allocateParticleSystemMemory2D(numparticles, numsources, advanced, sizecontrol, additionalbytes))
1129 {
1130 DEBUG_PRINT(F("PS init failed: memory depleted"));
1131 ✗ return false;
1132 }
1133
1134
2/6
✓ Branch 0 taken 64 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 64 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
64 PartSys = new (SEGENV.data) ParticleSystem2D(cols, rows, numparticles, numsources, advanced, sizecontrol); // particle system constructor
1135
1136 PSPRINTLN("******init done, pointers:");
1137 64 return true;
1138 }
1139
1140 #endif // WLED_DISABLE_PARTICLESYSTEM2D
1141
1142
1143 ////////////////////////
1144 // 1D Particle System //
1145 ////////////////////////
1146 #ifndef WLED_DISABLE_PARTICLESYSTEM1D
1147
1148 78 ParticleSystem1D::ParticleSystem1D(uint32_t length, uint32_t numberofparticles, uint32_t numberofsources, bool isadvanced) {
1149 78 numSources = numberofsources;
1150 78 numParticles = numberofparticles; // number of particles allocated in init
1151 78 usedParticles = numParticles; // use all particles by default
1152 78 advPartProps = nullptr; //make sure we start out with null pointers (just in case memory was not cleared)
1153 //advPartSize = nullptr;
1154 78 setSize(length);
1155 78 updatePSpointers(isadvanced); // set the particle and sources pointer (call this before accessing sprays or particles)
1156 78 setWallHardness(255); // set default wall hardness to max
1157 78 setGravity(0); //gravity disabled by default
1158 78 setParticleSize(0); // 1 pixel size by default
1159 78 motionBlur = 0; //no fading by default
1160 78 smearBlur = 0; //no smearing by default
1161 78 emitIndex = 0;
1162 78 collisionStartIdx = 0;
1163 // initialize some default non-zero values most FX use
1164
2/2
✓ Branch 0 taken 452 times.
✓ Branch 1 taken 78 times.
530 for (uint32_t i = 0; i < numSources; i++) {
1165 452 sources[i].source.ttl = 1; //set source alive
1166 452 sources[i].sourceFlags.asByte = 0; // all flags disabled
1167 }
1168
1169
2/2
✓ Branch 0 taken 45 times.
✓ Branch 1 taken 33 times.
78 if (isadvanced) {
1170
2/2
✓ Branch 0 taken 8640 times.
✓ Branch 1 taken 45 times.
8685 for (uint32_t i = 0; i < numParticles; i++) {
1171 8640 advPartProps[i].sat = 255; // set full saturation (for particles that are transferred from non-advanced system)
1172 }
1173 }
1174 78 }
1175
1176 // update function applies gravity, moves the particles, handles collisions and renders the particles
1177 1605 void ParticleSystem1D::update(void) {
1178 //apply gravity globally if enabled
1179
2/2
✓ Branch 0 taken 88 times.
✓ Branch 1 taken 1517 times.
1605 if (particlesettings.useGravity) //note: in 1D system, applying gravity after collisions also works but may be worse
1180 88 applyGravity();
1181
1182 // handle collisions (can push particles, must be done before updating particles or they can render out of bounds, causing a crash if using local buffer for speed)
1183
2/2
✓ Branch 0 taken 88 times.
✓ Branch 1 taken 1517 times.
1605 if (particlesettings.useCollisions)
1184 88 handleCollisions();
1185
1186 //move all particles
1187
2/2
✓ Branch 0 taken 100508 times.
✓ Branch 1 taken 1605 times.
102113 for (uint32_t i = 0; i < usedParticles; i++) {
1188
2/2
✓ Branch 0 taken 35886 times.
✓ Branch 1 taken 64622 times.
100508 particleMoveUpdate(particles[i], particleFlags[i], nullptr, advPartProps ? &advPartProps[i] : nullptr);
1189 }
1190
1191
2/2
✓ Branch 0 taken 31 times.
✓ Branch 1 taken 1574 times.
1605 if (particlesettings.colorByPosition) {
1192 31 uint32_t scale = (255 << 16) / maxX; // speed improvement: multiplication is faster than division
1193
2/2
✓ Branch 0 taken 5540 times.
✓ Branch 1 taken 31 times.
5571 for (uint32_t i = 0; i < usedParticles; i++) {
1194 5540 particles[i].hue = (scale * particles[i].x) >> 16; // note: x is > 0 if not out of bounds
1195 }
1196 }
1197
1198 1605 render();
1199 1605 }
1200
1201 // set percentage of used particles as uint8_t i.e 127 means 50% for example
1202 1375 void ParticleSystem1D::setUsedParticles(const uint8_t percentage) {
1203 1375 usedParticles = (numParticles * ((int)percentage+1)) >> 8; // number of particles to use (percentage is 0-255, 255 = 100%)
1204 PSPRINT(" SetUsedpaticles: allocated particles: ");
1205 PSPRINT(numParticles);
1206 PSPRINT(" ,used particles: ");
1207 PSPRINTLN(usedParticles);
1208 1375 }
1209
1210 157 void ParticleSystem1D::setWallHardness(const uint8_t hardness) {
1211 157 wallHardness = hardness;
1212 157 }
1213
1214 1683 void ParticleSystem1D::setSize(const uint32_t x) {
1215 1683 maxXpixel = x - 1; // last physical pixel that can be drawn to
1216 1683 maxX = x * PS_P_RADIUS_1D - 1; // particle system boundary for movements
1217 1683 }
1218
1219 23 void ParticleSystem1D::setWrap(const bool enable) {
1220 23 particlesettings.wrap = enable;
1221 23 }
1222
1223 97 void ParticleSystem1D::setBounce(const bool enable) {
1224 97 particlesettings.bounce = enable;
1225 97 }
1226
1227 40 void ParticleSystem1D::setKillOutOfBounds(const bool enable) {
1228 40 particlesettings.killoutofbounds = enable;
1229 40 }
1230
1231 46 void ParticleSystem1D::setColorByAge(const bool enable) {
1232 46 particlesettings.colorByAge = enable;
1233 46 }
1234
1235 6157 void ParticleSystem1D::setColorByPosition(const bool enable) {
1236 6157 particlesettings.colorByPosition = enable;
1237 6157 }
1238
1239 1605 void ParticleSystem1D::setMotionBlur(const uint8_t bluramount) {
1240 1605 motionBlur = bluramount;
1241 1605 }
1242
1243 1352 void ParticleSystem1D::setSmearBlur(const uint8_t bluramount) {
1244 1352 smearBlur = bluramount;
1245 1352 }
1246
1247 // render size, 0 = 1 pixel, 1 = 2 pixel (interpolated), bigger sizes require adanced properties
1248 1427 void ParticleSystem1D::setParticleSize(const uint8_t size) {
1249
2/2
✓ Branch 0 taken 38 times.
✓ Branch 1 taken 1389 times.
1427 particlesize = size > 0 ? 1 : 0; // TODO: add support for global sizes? see note above (motion blur)
1250
2/2
✓ Branch 0 taken 1389 times.
✓ Branch 1 taken 38 times.
1427 particleHardRadius = PS_P_MINHARDRADIUS_1D >> (!particlesize); // 2 pixel sized particles or single pixel sized particles
1251 1427 }
1252
1253 // enable/disable gravity, optionally, set the force (force=8 is default) can be -127 to +127, 0 is disable
1254 // if enabled, gravity is applied to all particles in ParticleSystemUpdate()
1255 // force is in 3.4 fixed point notation so force=16 means apply v+1 each frame default of 8 is every other frame (gives good results)
1256 216 void ParticleSystem1D::setGravity(const int8_t force) {
1257
2/2
✓ Branch 0 taken 88 times.
✓ Branch 1 taken 128 times.
216 if (force) {
1258 88 gforce = force;
1259 88 particlesettings.useGravity = true;
1260 }
1261 else
1262 128 particlesettings.useGravity = false;
1263 216 }
1264
1265 97 void ParticleSystem1D::enableParticleCollisions(const bool enable, const uint8_t hardness) {
1266 97 particlesettings.useCollisions = enable;
1267 97 collisionHardness = hardness;
1268 97 }
1269
1270 // emit one particle with variation, returns index of last emitted particle (or -1 if no particle emitted)
1271 1030 int32_t ParticleSystem1D::sprayEmit(const PSsource1D &emitter) {
1272
2/2
✓ Branch 0 taken 2303 times.
✓ Branch 1 taken 38 times.
2341 for (uint32_t i = 0; i < usedParticles; i++) {
1273 2303 emitIndex++;
1274
2/2
✓ Branch 0 taken 68 times.
✓ Branch 1 taken 2235 times.
2303 if (emitIndex >= usedParticles)
1275 68 emitIndex = 0;
1276
2/2
✓ Branch 0 taken 992 times.
✓ Branch 1 taken 1311 times.
2303 if (particles[emitIndex].ttl == 0) { // find a dead particle
1277 992 particles[emitIndex].vx = emitter.v + hw_random16(emitter.var << 1) - emitter.var; // random(-var,var)
1278 992 particles[emitIndex].x = emitter.source.x;
1279 992 particles[emitIndex].hue = emitter.source.hue;
1280 992 particles[emitIndex].ttl = hw_random16(emitter.minLife, emitter.maxLife);
1281 992 particleFlags[emitIndex].collide = emitter.sourceFlags.collide; // TODO: could just set all flags (asByte) but need to check if that breaks any of the FX
1282 992 particleFlags[emitIndex].reversegrav = emitter.sourceFlags.reversegrav;
1283 992 particleFlags[emitIndex].perpetual = emitter.sourceFlags.perpetual;
1284
2/2
✓ Branch 0 taken 560 times.
✓ Branch 1 taken 432 times.
992 if (advPartProps) {
1285 560 advPartProps[emitIndex].sat = emitter.sat;
1286 560 advPartProps[emitIndex].size = emitter.size;
1287 }
1288 992 return emitIndex;
1289 }
1290 }
1291 38 return -1;
1292 }
1293
1294 // particle moves, decays and dies, if killoutofbounds is set, out of bounds particles are set to ttl=0
1295 // uses passed settings to set bounce or wrap, if useGravity is set, it will never bounce at the top and killoutofbounds is not applied over the top
1296 110039 void ParticleSystem1D::particleMoveUpdate(PSparticle1D &part, PSparticleFlags1D &partFlags, PSsettings1D *options, PSadvancedParticle1D *advancedproperties) {
1297
2/2
✓ Branch 0 taken 109975 times.
✓ Branch 1 taken 64 times.
110039 if (options == nullptr)
1298 109975 options = &particlesettings; // use PS system settings by default
1299
1300
2/2
✓ Branch 0 taken 41307 times.
✓ Branch 1 taken 68732 times.
110039 if (part.ttl > 0) {
1301
2/2
✓ Branch 0 taken 9366 times.
✓ Branch 1 taken 31941 times.
41307 if (!partFlags.perpetual)
1302 9366 part.ttl--; // age
1303
2/2
✓ Branch 0 taken 140 times.
✓ Branch 1 taken 41167 times.
41307 if (options->colorByAge)
1304 140 part.hue = min(part.ttl, (uint16_t)255); // set color to ttl
1305
1306 41307 int32_t renderradius = PS_P_HALFRADIUS_1D; // used to check out of bounds, default for 2 pixel rendering
1307 41307 int32_t newX = part.x + (int32_t)part.vx;
1308 41307 partFlags.outofbounds = false; // reset out of bounds (in case particle was created outside the matrix and is now moving into view)
1309
1310
2/2
✓ Branch 0 taken 8757 times.
✓ Branch 1 taken 32550 times.
41307 if (advancedproperties) { // using individual particle size?
1311
2/2
✓ Branch 0 taken 6042 times.
✓ Branch 1 taken 2715 times.
8757 if (advancedproperties->size > 1)
1312 6042 particleHardRadius = PS_P_MINHARDRADIUS_1D + (advancedproperties->size >> 1);
1313 else // single pixel particles use half the collision distance for walls
1314 2715 particleHardRadius = PS_P_MINHARDRADIUS_1D >> 1;
1315 8757 renderradius = particleHardRadius; // note: for single pixel particles, it should be zero, but it does not matter as out of bounds checking is done in rendering function
1316 }
1317
1318 // if wall collisions are enabled, bounce them before they reach the edge, it looks much nicer if the particle is not half out of view
1319
2/2
✓ Branch 0 taken 6808 times.
✓ Branch 1 taken 34499 times.
41307 if (options->bounce) {
1320
4/4
✓ Branch 0 taken 6793 times.
✓ Branch 1 taken 15 times.
✓ Branch 2 taken 7 times.
✓ Branch 3 taken 6786 times.
6808 if ((newX < (int32_t)particleHardRadius) || ((newX > (int32_t)(maxX - particleHardRadius)))) { // reached a wall
1321 22 bool bouncethis = true;
1322
2/2
✓ Branch 0 taken 11 times.
✓ Branch 1 taken 11 times.
22 if (options->useGravity) {
1323
2/2
✓ Branch 0 taken 7 times.
✓ Branch 1 taken 4 times.
11 if (partFlags.reversegrav) { // skip bouncing at x = 0
1324
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 7 times.
7 if (newX < (int32_t)particleHardRadius)
1325 ✗ bouncethis = false;
1326
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 4 times.
4 } else if (newX > (int32_t)particleHardRadius) { // skip bouncing at x = max
1327 ✗ bouncethis = false;
1328 }
1329 }
1330
1/2
✓ Branch 0 taken 22 times.
✗ Branch 1 not taken.
22 if (bouncethis) {
1331 22 part.vx = -part.vx; // invert speed
1332 22 part.vx = ((int32_t)part.vx * (int32_t)wallHardness) / 255; // reduce speed as energy is lost on non-hard surface
1333
2/2
✓ Branch 0 taken 15 times.
✓ Branch 1 taken 7 times.
22 if (newX < (int32_t)particleHardRadius)
1334 15 newX = particleHardRadius; // fast particles will never reach the edge if position is inverted, this looks better
1335 else
1336 7 newX = maxX - particleHardRadius;
1337 }
1338 }
1339 }
1340
1341
2/2
✓ Branch 0 taken 7978 times.
✓ Branch 1 taken 33329 times.
41307 if (!checkBoundsAndWrap(newX, maxX, renderradius, options->wrap)) { // check out of bounds note: this must not be skipped or it can lead to crashes
1342 7978 partFlags.outofbounds = true;
1343
2/2
✓ Branch 0 taken 11 times.
✓ Branch 1 taken 7967 times.
7978 if (options->killoutofbounds) {
1344 11 bool killthis = true;
1345
2/2
✓ Branch 0 taken 8 times.
✓ Branch 1 taken 3 times.
11 if (options->useGravity) { // if gravity is used, only kill below 'floor level'
1346
2/2
✓ Branch 0 taken 6 times.
✓ Branch 1 taken 2 times.
8 if (partFlags.reversegrav) { // skip at x = 0, do not skip far out of bounds
1347
2/4
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 6 times.
6 if (newX < 0 || newX > maxX << 2)
1348 ✗ killthis = false;
1349 } else { // skip at x = max, do not skip far out of bounds
1350
1/4
✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
2 if (newX > 0 && newX < maxX << 2)
1351 ✗ killthis = false;
1352 }
1353 }
1354
1/2
✓ Branch 0 taken 11 times.
✗ Branch 1 not taken.
11 if (killthis)
1355 11 part.ttl = 0;
1356 }
1357 }
1358
1359
2/2
✓ Branch 0 taken 36574 times.
✓ Branch 1 taken 4733 times.
41307 if (!partFlags.fixed)
1360 36574 part.x = newX; // set new position
1361 else
1362 4733 part.vx = 0; // set speed to zero. note: particle can get speed in collisions, if unfixed, it should not speed away
1363 }
1364 110039 }
1365
1366 // apply a force in x direction to individual particle (or source)
1367 // caller needs to provide a 8bit counter (for each paticle) that holds its value between calls
1368 // force is in 3.4 fixed point notation so force=16 means apply v+1 each frame default of 8 is every other frame
1369 3122 void ParticleSystem1D::applyForce(PSparticle1D &part, const int8_t xforce, uint8_t &counter) {
1370 3122 int32_t dv = calcForce_dv(xforce, counter); // velocity increase
1371 3122 part.vx = limitSpeed((int32_t)part.vx + dv); // apply the force to particle
1372 3122 }
1373
1374 // apply a force to all particles
1375 // force is in 3.4 fixed point notation (see above)
1376 14 void ParticleSystem1D::applyForce(const int8_t xforce) {
1377 14 int32_t dv = calcForce_dv(xforce, forcecounter); // velocity increase
1378
2/2
✓ Branch 0 taken 1603 times.
✓ Branch 1 taken 14 times.
1617 for (uint32_t i = 0; i < usedParticles; i++) {
1379 1603 particles[i].vx = limitSpeed((int32_t)particles[i].vx + dv);
1380 }
1381 14 }
1382
1383 // apply gravity to all particles using PS global gforce setting
1384 // gforce is in 3.4 fixed point notation, see note above
1385 88 void ParticleSystem1D::applyGravity() {
1386 88 int32_t dv_raw = calcForce_dv(gforce, gforcecounter);
1387
2/2
✓ Branch 0 taken 18630 times.
✓ Branch 1 taken 88 times.
18718 for (uint32_t i = 0; i < usedParticles; i++) {
1388 18630 int32_t dv = dv_raw;
1389
2/2
✓ Branch 0 taken 6791 times.
✓ Branch 1 taken 11839 times.
18630 if (particleFlags[i].reversegrav) dv = -dv_raw;
1390 // note: not checking if particle is dead is omitted as most are usually alive and if few are alive, rendering is fast anyways
1391 18630 particles[i].vx = limitSpeed((int32_t)particles[i].vx - dv);
1392 }
1393 88 }
1394
1395 // apply gravity to single particle using system settings (use this for sources)
1396 // function does not increment gravity counter, if gravity setting is disabled, this cannot be used
1397 ✗ void ParticleSystem1D::applyGravity(PSparticle1D &part, PSparticleFlags1D &partFlags) {
1398 ✗ uint32_t counterbkp = gforcecounter;
1399 ✗ int32_t dv = calcForce_dv(gforce, gforcecounter);
1400 ✗ if (partFlags.reversegrav) dv = -dv;
1401 ✗ gforcecounter = counterbkp; //save it back
1402 ✗ part.vx = limitSpeed((int32_t)part.vx - dv);
1403 ✗ }
1404
1405
1406 // slow down particle by friction, the higher the speed, the higher the friction. a high friction coefficient slows them more (255 means instant stop)
1407 // note: a coefficient smaller than 0 will speed them up (this is a feature, not a bug), coefficient larger than 255 inverts the speed, so don't do that
1408 20 void ParticleSystem1D::applyFriction(int32_t coefficient) {
1409 #if defined(CONFIG_IDF_TARGET_ESP32C3) || defined(ESP8266) // use bitshifts with rounding instead of division (2x faster)
1410 int32_t friction = 256 - coefficient;
1411 for (uint32_t i = 0; i < usedParticles; i++) {
1412 if (particles[i].ttl)
1413 particles[i].vx = ((int32_t)particles[i].vx * friction + (((int32_t)particles[i].vx >> 31) & 0xFF)) >> 8; // note: (v>>31) & 0xFF)) extracts the sign and adds 255 if negative for correct rounding using shifts
1414 }
1415 #else // division is faster on ESP32, S2 and S3
1416 20 int32_t friction = 255 - coefficient;
1417
2/2
✓ Branch 0 taken 2837 times.
✓ Branch 1 taken 20 times.
2857 for (uint32_t i = 0; i < usedParticles; i++) {
1418
2/2
✓ Branch 0 taken 816 times.
✓ Branch 1 taken 2021 times.
2837 if (particles[i].ttl)
1419 816 particles[i].vx = ((int32_t)particles[i].vx * friction) / 255;
1420 }
1421 #endif
1422 20 }
1423
1424
1425 // render particles to the LED buffer (uses palette to render the 8bit particle color value)
1426 // if wrap is set, particles half out of bounds are rendered to the other side of the matrix
1427 // warning: do not render out of bounds particles or system will crash! rendering does not check if particle is out of bounds
1428 1605 void ParticleSystem1D::render() {
1429 CRGB baseRGB;
1430 uint32_t brightness; // particle brightness, fades if dying
1431 1605 TBlendType blend = LINEARBLEND; // default color rendering: wrap palette
1432
4/4
✓ Branch 0 taken 1582 times.
✓ Branch 1 taken 23 times.
✓ Branch 2 taken 31 times.
✓ Branch 3 taken 1551 times.
1605 if (particlesettings.colorByAge || particlesettings.colorByPosition) {
1433 54 blend = LINEARBLEND_NOWRAP;
1434 }
1435
1436 #ifdef ESP8266 // no local buffer on ESP8266
1437 if (motionBlur)
1438 SEGMENT.fadeToBlackBy(255 - motionBlur);
1439 else
1440 SEGMENT.fill(BLACK); // clear the buffer before rendering to it
1441 #else
1442
2/2
✓ Branch 0 taken 154 times.
✓ Branch 1 taken 1451 times.
1605 if (motionBlur) { // blurring active
1443
2/2
✓ Branch 0 taken 53258 times.
✓ Branch 1 taken 154 times.
53412 for (int32_t x = 0; x <= maxXpixel; x++) {
1444 53258 fast_color_scale(framebuffer[x], motionBlur);
1445 }
1446 }
1447 else { // no blurring: clear buffer
1448 1451 memset(framebuffer, 0, (maxXpixel+1) * sizeof(CRGB));
1449 }
1450 #endif
1451 // go over particles and render them to the buffer
1452
2/2
✓ Branch 0 taken 100508 times.
✓ Branch 1 taken 1605 times.
102113 for (uint32_t i = 0; i < usedParticles; i++) {
1453
4/4
✓ Branch 0 taken 41145 times.
✓ Branch 1 taken 59363 times.
✓ Branch 2 taken 7967 times.
✓ Branch 3 taken 33178 times.
100508 if ( particles[i].ttl == 0 || particleFlags[i].outofbounds)
1454 67330 continue;
1455
1456 // generate RGB values for particle
1457 33178 brightness = min(particles[i].ttl << 1, (int)255);
1458
1/2
✓ Branch 0 taken 33178 times.
✗ Branch 1 not taken.
33178 baseRGB = ColorFromPaletteWLED(SEGPALETTE, particles[i].hue, 255, blend);
1459
1460
2/2
✓ Branch 0 taken 3932 times.
✓ Branch 1 taken 29246 times.
33178 if (advPartProps) { //saturation is advanced property in 1D system
1461
2/2
✓ Branch 0 taken 394 times.
✓ Branch 1 taken 3538 times.
3932 if (advPartProps[i].sat < 255) {
1462 CHSV32 baseHSV;
1463
1/2
✓ Branch 0 taken 394 times.
✗ Branch 1 not taken.
394 rgb2hsv((uint32_t((byte(baseRGB.r) << 16) | (byte(baseRGB.g) << 8) | (byte(baseRGB.b)))), baseHSV); // convert to HSV
1464 394 baseHSV.s = advPartProps[i].sat; // set the saturation
1465 uint32_t tempcolor;
1466
1/2
✓ Branch 0 taken 394 times.
✗ Branch 1 not taken.
394 hsv2rgb(baseHSV, tempcolor); // convert back to RGB
1467 394 baseRGB = (CRGB)tempcolor;
1468 }
1469 }
1470
1/2
✓ Branch 0 taken 33178 times.
✗ Branch 1 not taken.
33178 renderParticle(i, brightness, baseRGB, particlesettings.wrap);
1471 }
1472 // apply smear-blur to rendered frame
1473
2/2
✓ Branch 0 taken 46 times.
✓ Branch 1 taken 1559 times.
1605 if (smearBlur) {
1474 #ifdef ESP8266
1475 SEGMENT.blur(smearBlur, true); // no local buffer on ESP8266
1476 #else
1477
1/2
✓ Branch 0 taken 46 times.
✗ Branch 1 not taken.
46 blur1D(framebuffer, maxXpixel + 1, smearBlur, 0);
1478 #endif
1479 }
1480
1481 // add background color
1482 1605 uint32_t bg_color = SEGCOLOR(1);
1483
1/2
✓ Branch 0 taken 1605 times.
✗ Branch 1 not taken.
1605 if (bg_color > 0) { //if not black
1484 1605 CRGB bg_color_crgb = bg_color; // convert to CRGB
1485
2/2
✓ Branch 0 taken 150465 times.
✓ Branch 1 taken 1605 times.
152070 for (int32_t i = 0; i <= maxXpixel; i++) {
1486 #ifdef ESP8266 // no local buffer on ESP8266
1487 SEGMENT.addPixelColor(i, bg_color, true);
1488 #else
1489 150465 fast_color_add(framebuffer[i], bg_color_crgb);
1490 #endif
1491 }
1492 }
1493
1494 #ifndef ESP8266
1495 // transfer the frame-buffer to segment
1496
2/2
✓ Branch 0 taken 150465 times.
✓ Branch 1 taken 1605 times.
152070 for (int x = 0; x <= maxXpixel; x++) {
1497
3/6
✓ Branch 0 taken 150465 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 150465 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 150465 times.
✗ Branch 5 not taken.
150465 SEGMENT.setPixelColor(x, framebuffer[x]);
1498 }
1499 #endif
1500 1605 }
1501
1502 // calculate pixel positions and brightness distribution and render the particle to local buffer or global buffer
1503 33178 __attribute__((optimize("O2"))) void ParticleSystem1D::renderParticle(const uint32_t particleindex, const uint8_t brightness, const CRGB &color, const bool wrap) {
1504 33178 uint32_t size = particlesize;
1505
2/2
✓ Branch 0 taken 3932 times.
✓ Branch 1 taken 29246 times.
33178 if (advPartProps) // use advanced size properties (1D system has no large size global rendering TODO: add large global rendering?)
1506 3932 size = advPartProps[particleindex].size;
1507
1508
2/2
✓ Branch 0 taken 27733 times.
✓ Branch 1 taken 5445 times.
33178 if (size == 0) { //single pixel particle, can be out of bounds as oob checking is made for 2-pixel particles (and updating it uses more code)
1509 27733 uint32_t x = particles[particleindex].x >> PS_P_RADIUS_SHIFT_1D;
1510
2/2
✓ Branch 0 taken 27069 times.
✓ Branch 1 taken 664 times.
27733 if (x <= (uint32_t)maxXpixel) { //by making x unsigned there is no need to check < 0 as it will overflow
1511 #ifdef ESP8266 // no local buffer on ESP8266
1512 SEGMENT.addPixelColor(x, color.scale8(brightness), true);
1513 #else
1514 27069 fast_color_add(framebuffer[x], color, brightness);
1515 #endif
1516 }
1517 27733 return;
1518 }
1519 //render larger particles
1520 5445 bool pxlisinframe[2] = {true, true};
1521 5445 int32_t pxlbrightness[2];
1522 5445 int32_t pixco[2]; // physical pixel coordinates of the two pixels representing a particle
1523
1524 // add half a radius as the rendering algorithm always starts at the bottom left, this leaves things positive, so shifts can be used, then shift coordinate by a full pixel (x-- below)
1525 5445 int32_t xoffset = particles[particleindex].x + PS_P_HALFRADIUS_1D;
1526 5445 int32_t dx = xoffset & (PS_P_RADIUS_1D - 1); //relativ particle position in subpixel space, modulo replaced with bitwise AND
1527 5445 int32_t x = xoffset >> PS_P_RADIUS_SHIFT_1D; // divide by PS_P_RADIUS, bitshift of negative number stays negative -> checking below for x < 0 works (but does not when using division)
1528
1529 // set the raw pixel coordinates
1530 5445 pixco[1] = x; // right pixel
1531 5445 x--; // shift by a full pixel here, this is skipped above to not do -1 and then +1
1532 5445 pixco[0] = x; // left pixel
1533
1534 //calculate the brightness values for both pixels using linear interpolation (note: in standard rendering out of frame pixels could be skipped but if checks add more clock cycles over all)
1535 5445 pxlbrightness[0] = (((int32_t)PS_P_RADIUS_1D - dx) * brightness) >> PS_P_SURFACE_1D;
1536 5445 pxlbrightness[1] = (dx * brightness) >> PS_P_SURFACE_1D;
1537
1538 // check if particle has advanced size properties and buffer is available
1539
4/4
✓ Branch 0 taken 3290 times.
✓ Branch 1 taken 2155 times.
✓ Branch 2 taken 2703 times.
✓ Branch 3 taken 587 times.
5445 if (advPartProps && advPartProps[particleindex].size > 1) {
1540 2703 CRGB renderbuffer[10]; // 10 pixel buffer
1541 2703 memset(renderbuffer, 0, sizeof(renderbuffer)); // clear buffer
1542 //render particle to a bigger size
1543 //particle size to pixels: 2 - 63 is 4 pixels, < 128 is 6pixels, < 192 is 8 pixels, bigger is 10 pixels
1544 //first, render the pixel to the center of the renderbuffer, then apply 1D blurring
1545 2703 fast_color_add(renderbuffer[4], color, pxlbrightness[0]);
1546 2703 fast_color_add(renderbuffer[5], color, pxlbrightness[1]);
1547 2703 uint32_t rendersize = 2; // initialize render size, minimum is 4 pixels, it is incremented int he loop below to start with 4
1548 2703 uint32_t offset = 4; // offset to zero coordinate to write/read data in renderbuffer (actually needs to be 3, is decremented in the loop below)
1549 2703 uint32_t blurpasses = size/64 + 1; // number of blur passes depends on size, four passes max
1550 2703 uint32_t bitshift = 0;
1551
2/2
✓ Branch 0 taken 5267 times.
✓ Branch 1 taken 2703 times.
7970 for (uint32_t i = 0; i < blurpasses; i++) {
1552
2/2
✓ Branch 0 taken 956 times.
✓ Branch 1 taken 4311 times.
5267 if (i == 2) //for the last two passes, use higher amount of blur (results in a nicer brightness gradient with soft edges)
1553 956 bitshift = 1;
1554 5267 rendersize += 2;
1555 5267 offset--;
1556 5267 blur1D(renderbuffer, rendersize, size << bitshift, offset);
1557 5267 size = size > 64 ? size - 64 : 0;
1558 }
1559
1560 // calculate origin coordinates to render the particle to in the framebuffer
1561 2703 uint32_t xfb_orig = x - (rendersize>>1) + 1 - offset; //note: using uint is fine
1562 2703 uint32_t xfb; // coordinates in frame buffer to write to note: by making this uint, only overflow has to be checked
1563
1564 // transfer particle renderbuffer to framebuffer
1565
2/2
✓ Branch 0 taken 15940 times.
✓ Branch 1 taken 2703 times.
18643 for (uint32_t xrb = offset; xrb < rendersize+offset; xrb++) {
1566 15940 xfb = xfb_orig + xrb;
1567
2/2
✓ Branch 0 taken 268 times.
✓ Branch 1 taken 15672 times.
15940 if (xfb > (uint32_t)maxXpixel) {
1568
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 268 times.
268 if (wrap) { // wrap x to the other side if required
1569 ✗ if (xfb > (uint32_t)maxXpixel << 1) // xfb is "negative"
1570 ✗ xfb = (maxXpixel + 1) + (int32_t)xfb; // this always overflows to within bounds
1571 else
1572 ✗ xfb = xfb % (maxXpixel + 1); // note: without the above "negative" check, this works only for powers of 2
1573 }
1574 else
1575 268 continue;
1576 }
1577 #ifdef ESP8266 // no local buffer on ESP8266
1578 SEGMENT.addPixelColor(xfb, renderbuffer[xrb], true);
1579 #else
1580 15672 fast_color_add(framebuffer[xfb], renderbuffer[xrb]);
1581 #endif
1582 }
1583 2703 }
1584 else { // standard rendering (2 pixels per particle)
1585 // check if any pixels are out of frame
1586
2/2
✓ Branch 0 taken 61 times.
✓ Branch 1 taken 2681 times.
2742 if (x < 0) { // left pixels out of frame
1587
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 61 times.
61 if (wrap) // wrap x to the other side if required
1588 ✗ pixco[0] = maxXpixel;
1589 else
1590 61 pxlisinframe[0] = false; // pixel is out of matrix boundaries, do not render
1591 }
1592
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2681 times.
2681 else if (pixco[1] > (int32_t)maxXpixel) { // right pixel, only has to be checkt if left pixel did not overflow
1593 ✗ if (wrap) // wrap y to the other side if required
1594 ✗ pixco[1] = 0;
1595 else
1596 ✗ pxlisinframe[1] = false;
1597 }
1598
2/2
✓ Branch 0 taken 5484 times.
✓ Branch 1 taken 2742 times.
8226 for (uint32_t i = 0; i < 2; i++) {
1599
2/2
✓ Branch 0 taken 5423 times.
✓ Branch 1 taken 61 times.
5484 if (pxlisinframe[i]) {
1600 #ifdef ESP8266 // no local buffer on ESP8266
1601 SEGMENT.addPixelColor(pixco[i], color.scale8((uint8_t)pxlbrightness[i]), true);
1602 #else
1603 5423 fast_color_add(framebuffer[pixco[i]], color, pxlbrightness[i]);
1604 #endif
1605 }
1606 }
1607 }
1608
1609 }
1610
1611 // detect collisions in an array of particles and handle them
1612 88 void ParticleSystem1D::handleCollisions() {
1613 88 uint32_t collisiondistance = particleHardRadius << 1;
1614 // note: partices are binned by position, assumption is that no more than half of the particles are in the same bin
1615 // if they are, collisionStartIdx is increased so each particle collides at least every second frame (which still gives decent collisions)
1616 88 constexpr int BIN_WIDTH = 32 * PS_P_RADIUS_1D; // width of each bin, a compromise between speed and accuracy (larger bins are faster but collapse more)
1617 88 int32_t overlap = particleHardRadius << 1; // overlap bins to include edge particles to neighbouring bins
1618 88 if (advPartProps) //may be using individual particle size
1619 46 overlap += 256; // add 2 * max radius (approximately)
1620 88 uint32_t maxBinParticles = max((uint32_t)50, (usedParticles + 1) / 4); // do not bin small amounts, limit max to 1/4 of particles
1621 88 uint32_t numBins = (maxX + (BIN_WIDTH - 1)) / BIN_WIDTH; // calculate number of bins
1622 88 uint16_t binIndices[maxBinParticles]; // array to store indices of particles in a bin
1623 uint32_t binParticleCount; // number of particles in the current bin
1624 88 uint16_t nextFrameStartIdx = hw_random16(usedParticles); // index of the first particle in the next frame (set to fixed value if bin overflow)
1625 88 uint32_t pidx = collisionStartIdx; //start index in case a bin is full, process remaining particles next frame
1626
2/2
✓ Branch 0 taken 888 times.
✓ Branch 1 taken 88 times.
976 for (uint32_t bin = 0; bin < numBins; bin++) {
1627 888 binParticleCount = 0; // reset for this bin
1628 888 int32_t binStart = bin * BIN_WIDTH - overlap; // note: first bin will extend to negative, but that is ok as out of bounds particles are ignored
1629 888 int32_t binEnd = binStart + BIN_WIDTH + overlap; // note: last bin can be out of bounds, see above
1630
1631 // fill the binIndices array for this bin
1632
2/2
✓ Branch 0 taken 186027 times.
✓ Branch 1 taken 888 times.
186915 for (uint32_t i = 0; i < usedParticles; i++) {
1633
2/2
✓ Branch 0 taken 100438 times.
✓ Branch 1 taken 85589 times.
186027 if (particles[pidx].ttl > 0) { // alivee
1634
4/4
✓ Branch 0 taken 49023 times.
✓ Branch 1 taken 51415 times.
✓ Branch 2 taken 7376 times.
✓ Branch 3 taken 41647 times.
100438 if (particles[pidx].x >= binStart && particles[pidx].x <= binEnd) { // >= and <= to include particles on the edge of the bin (overlap to ensure boarder particles collide with adjacent bins)
1635
3/4
✓ Branch 0 taken 7376 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 6991 times.
✓ Branch 3 taken 385 times.
7376 if(particleFlags[pidx].outofbounds == 0 && particleFlags[pidx].collide) { // particle is in frame and does collide note: checking flags is quite slow and usually these are set, so faster to check here
1636
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 6991 times.
6991 if (binParticleCount >= maxBinParticles) { // bin is full, more particles in this bin so do the rest next frame
1637 ✗ nextFrameStartIdx = pidx; // bin overflow can only happen once as bin size is at least half of the particles (or half +1)
1638 ✗ break;
1639 }
1640 6991 binIndices[binParticleCount++] = pidx;
1641 }
1642 }
1643 }
1644 186027 pidx++;
1645
2/2
✓ Branch 0 taken 824 times.
✓ Branch 1 taken 185203 times.
186027 if (pidx >= usedParticles) pidx = 0; // wrap around
1646 }
1647
1648
2/2
✓ Branch 0 taken 6991 times.
✓ Branch 1 taken 888 times.
7879 for (uint32_t i = 0; i < binParticleCount; i++) { // go though all 'higher number' particles and see if any of those are in close proximity and if they are, make them collide
1649 6991 uint32_t idx_i = binIndices[i];
1650
2/2
✓ Branch 0 taken 108258 times.
✓ Branch 1 taken 6991 times.
115249 for (uint32_t j = i + 1; j < binParticleCount; j++) { // check against higher number particles
1651 108258 uint32_t idx_j = binIndices[j];
1652
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 108258 times.
108258 if (advPartProps) { // use advanced size properties
1653 ✗ collisiondistance = (PS_P_MINHARDRADIUS_1D << particlesize) + ((advPartProps[idx_i].size + advPartProps[idx_j].size) >> 1);
1654 }
1655 108258 int32_t dx = (particles[idx_j].x + particles[idx_j].vx) - (particles[idx_i].x + particles[idx_i].vx); // distance between particles with lookahead
1656 108258 uint32_t dx_abs = abs(dx);
1657
2/2
✓ Branch 0 taken 8429 times.
✓ Branch 1 taken 99829 times.
108258 if (dx_abs <= collisiondistance) { // collide if close
1658
1/2
✓ Branch 0 taken 8429 times.
✗ Branch 1 not taken.
8429 collideParticles(particles[idx_i], particleFlags[idx_i], particles[idx_j], particleFlags[idx_j], dx, dx_abs, collisiondistance);
1659 }
1660 }
1661 }
1662 }
1663 88 collisionStartIdx = nextFrameStartIdx; // set the start index for the next frame
1664
2/2
✓ Branch 0 taken 46 times.
✓ Branch 1 taken 42 times.
176 }
1665 // handle a collision if close proximity is detected, i.e. dx and/or dy smaller than 2*PS_P_RADIUS
1666 // takes two pointers to the particles to collide and the particle hardness (softer means more energy lost in collision, 255 means full hard)
1667 8429 __attribute__((optimize("O2"))) void ParticleSystem1D::collideParticles(PSparticle1D &particle1, const PSparticleFlags1D &particle1flags, PSparticle1D &particle2, const PSparticleFlags1D &particle2flags, const int32_t dx, const uint32_t dx_abs, const uint32_t collisiondistance) {
1668 8429 int32_t dv = particle2.vx - particle1.vx;
1669 8429 int32_t dotProduct = (dx * dv); // is always negative if moving towards each other
1670
1671
2/2
✓ Branch 0 taken 3199 times.
✓ Branch 1 taken 5230 times.
8429 if (dotProduct < 0) { // particles are moving towards each other
1672 3199 uint32_t surfacehardness = max(collisionHardness, (int32_t)PS_P_MINSURFACEHARDNESS_1D); // if particles are soft, the impulse must stay above a limit or collisions slip through
1673 // Calculate new velocities after collision note: not using dot product like in 2D as impulse is purely speed depnedent
1674 #if defined(CONFIG_IDF_TARGET_ESP32C3) || defined(ESP8266) // use bitshifts with rounding instead of division (2x faster)
1675 int32_t impulse = ((dv * surfacehardness) + ((dv >> 31) & 0xFF)) >> 8; // note: (v>>31) & 0xFF)) extracts the sign and adds 255 if negative for correct rounding using shifts
1676 #else // division is faster on ESP32, S2 and S3
1677 3199 int32_t impulse = (dv * surfacehardness) / 255;
1678 #endif
1679 3199 particle1.vx += impulse;
1680 3199 particle2.vx -= impulse;
1681
1682 // if one of the particles is fixed, transfer the impulse back so it bounces
1683
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 3199 times.
3199 if (particle1flags.fixed)
1684 ✗ particle2.vx = -particle1.vx;
1685
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 3199 times.
3199 else if (particle2flags.fixed)
1686 ✗ particle1.vx = -particle2.vx;
1687
1688
1/4
✗ Branch 0 not taken.
✓ Branch 1 taken 3199 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
3199 if (collisionHardness < PS_P_MINSURFACEHARDNESS_1D && (SEGMENT.call & 0x07) == 0) { // if particles are soft, they become 'sticky' i.e. apply some friction
1689 ✗ const uint32_t coeff = collisionHardness + (250 - PS_P_MINSURFACEHARDNESS_1D);
1690 #if defined(CONFIG_IDF_TARGET_ESP32C3) || defined(ESP8266) // use bitshifts with rounding instead of division (2x faster)
1691 particle1.vx = ((int32_t)particle1.vx * coeff + (((int32_t)particle1.vx >> 31) & 0xFF)) >> 8; // note: (v>>31) & 0xFF)) extracts the sign and adds 255 if negative for correct rounding using shifts
1692 particle2.vx = ((int32_t)particle2.vx * coeff + (((int32_t)particle2.vx >> 31) & 0xFF)) >> 8;
1693 #else // division is faster on ESP32, S2 and S3
1694 ✗ particle1.vx = ((int32_t)particle1.vx * coeff) / 255;
1695 ✗ particle2.vx = ((int32_t)particle2.vx * coeff) / 255;
1696 #endif
1697 }
1698 }
1699
1700
4/4
✓ Branch 0 taken 2236 times.
✓ Branch 1 taken 6193 times.
✓ Branch 2 taken 1739 times.
✓ Branch 3 taken 497 times.
8429 if (dx_abs < (collisiondistance - 8) && abs(dv) < 5) { // overlapping and moving slowly
1701 // particles have volume, push particles apart if they are too close
1702 // behaviour is different than in 2D, we need pixel accurate stacking here, push the top particle
1703 // note: like in 2D, pushing by a distance makes softer piles collapse, giving particles speed prevents that and looks nicer
1704 1739 int32_t pushamount = 1;
1705
2/2
✓ Branch 0 taken 14 times.
✓ Branch 1 taken 1725 times.
1739 if (dx < 0) // particle2.x < particle1.x
1706 14 pushamount = -pushamount;
1707 1739 particle1.vx -= pushamount;
1708 1739 particle2.vx += pushamount;
1709
1710
2/2
✓ Branch 0 taken 1300 times.
✓ Branch 1 taken 439 times.
1739 if (dx_abs < collisiondistance >> 1) { // too close, force push particles so they dont collapse
1711 1300 pushamount = 1 + ((collisiondistance - dx_abs) >> 3); // note: push amount found by experimentation
1712
1713
2/2
✓ Branch 0 taken 1295 times.
✓ Branch 1 taken 5 times.
1300 if (particle1.x < (maxX >> 1)) { // lower half, push particle with larger x in positive direction
1714
3/4
✓ Branch 0 taken 5 times.
✓ Branch 1 taken 1290 times.
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
1295 if (dx < 0 && !particle1flags.fixed) { // particle2.x < particle1.x -> push particle 1
1715 5 particle1.vx++;// += pushamount;
1716 5 particle1.x += pushamount;
1717 }
1718
1/2
✓ Branch 0 taken 1290 times.
✗ Branch 1 not taken.
1290 else if (!particle2flags.fixed) { // particle1.x < particle2.x -> push particle 2
1719 1290 particle2.vx++;// += pushamount;
1720 1290 particle2.x += pushamount;
1721 }
1722 }
1723 else { // upper half, push particle with smaller x
1724
1/4
✗ Branch 0 not taken.
✓ Branch 1 taken 5 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
5 if (dx < 0 && !particle2flags.fixed) { // particle2.x < particle1.x -> push particle 2
1725 ✗ particle2.vx--;// -= pushamount;
1726 ✗ particle2.x -= pushamount;
1727 }
1728
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 else if (!particle1flags.fixed) { // particle1.x < particle2.x -> push particle 1
1729 5 particle1.vx--;// -= pushamount;
1730 5 particle1.x -= pushamount;
1731 }
1732 }
1733 }
1734 }
1735 8429 }
1736
1737 // update size and pointers (memory location and size can change dynamically)
1738 // note: do not access the PS class in FX befor running this function (or it messes up SEGENV.data)
1739 1605 void ParticleSystem1D::updateSystem(void) {
1740 1605 setSize(SEGMENT.virtualLength()); // update size
1741 1605 updatePSpointers(advPartProps != nullptr);
1742 1605 }
1743
1744 // set the pointers for the class (this only has to be done once and not on every FX call, only the class pointer needs to be reassigned to SEGENV.data every time)
1745 // function returns the pointer to the next byte available for the FX (if it assigned more memory for other stuff using the above allocate function)
1746 // FX handles the PSsources, need to tell this function how many there are
1747 1683 void ParticleSystem1D::updatePSpointers(bool isadvanced) {
1748 // Note on memory alignment:
1749 // a pointer MUST be 4 byte aligned. sizeof() in a struct/class is always aligned to the largest element. if it contains a 32bit, it will be padded to 4 bytes, 16bit is padded to 2byte alignment.
1750 // The PS is aligned to 4 bytes, a PSparticle is aligned to 2 and a struct containing only byte sized variables is not aligned at all and may need to be padded when dividing the memoryblock.
1751 // by making sure that the number of sources and particles is a multiple of 4, padding can be skipped here as alignent is ensured, independent of struct sizes.
1752 1683 particles = reinterpret_cast<PSparticle1D *>(this + 1); // pointer to particles
1753 1683 particleFlags = reinterpret_cast<PSparticleFlags1D *>(particles + numParticles); // pointer to particle flags
1754 1683 sources = reinterpret_cast<PSsource1D *>(particleFlags + numParticles); // pointer to source(s)
1755 #ifdef ESP8266 // no local buffer on ESP8266
1756 PSdataEnd = reinterpret_cast<uint8_t *>(sources + numSources);
1757 #else
1758 1683 framebuffer = reinterpret_cast<CRGB *>(sources + numSources); // pointer to framebuffer
1759 // align pointer after framebuffer to 4bytes
1760 1683 uintptr_t p = reinterpret_cast<uintptr_t>(framebuffer + (maxXpixel+1)); // maxXpixel is SEGMENT.virtualLength() - 1
1761 1683 p = (p + 3) & ~0x03; // align to 4-byte boundary
1762 1683 PSdataEnd = reinterpret_cast<uint8_t *>(p); // pointer to first available byte after the PS for FX additional data
1763 #endif
1764
2/2
✓ Branch 0 taken 252 times.
✓ Branch 1 taken 1431 times.
1683 if (isadvanced) {
1765 252 advPartProps = reinterpret_cast<PSadvancedParticle1D *>(PSdataEnd);
1766 252 PSdataEnd = reinterpret_cast<uint8_t *>(advPartProps + numParticles); // since numParticles is a multiple of 4, this is always aligned to 4 bytes. No need to add padding bytes here
1767 }
1768 #ifdef WLED_DEBUG_PS
1769 PSPRINTLN(" PS Pointers: ");
1770 PSPRINT(" PS : 0x");
1771 Serial.println((uintptr_t)this, HEX);
1772 PSPRINT(" Particleflags : 0x");
1773 Serial.println((uintptr_t)particleFlags, HEX);
1774 PSPRINT(" Particles : 0x");
1775 Serial.println((uintptr_t)particles, HEX);
1776 PSPRINT(" Sources : 0x");
1777 Serial.println((uintptr_t)sources, HEX);
1778 #endif
1779 1683 }
1780
1781 //non class functions to use for initialization, fraction is uint8_t: 255 means 100%
1782 78 uint32_t calculateNumberOfParticles1D(const uint32_t fraction, const bool isadvanced) {
1783 78 uint32_t numberofParticles = SEGMENT.virtualLength(); // one particle per pixel (if possible)
1784 #ifdef ESP8266
1785 uint32_t particlelimit = ESP8266_MAXPARTICLES_1D; // maximum number of paticles allowed
1786 #elif ARDUINO_ARCH_ESP32S2
1787 uint32_t particlelimit = ESP32S2_MAXPARTICLES_1D; // maximum number of paticles allowed
1788 #else
1789 78 uint32_t particlelimit = ESP32_MAXPARTICLES_1D; // maximum number of paticles allowed
1790 #endif
1791 78 numberofParticles = min(numberofParticles, particlelimit); // limit to particlelimit
1792
2/2
✓ Branch 0 taken 45 times.
✓ Branch 1 taken 33 times.
78 if (isadvanced) // advanced property array needs ram, reduce number of particles to use the same amount
1793 45 numberofParticles = (numberofParticles * sizeof(PSparticle1D)) / (sizeof(PSparticle1D) + sizeof(PSadvancedParticle1D));
1794 78 numberofParticles = (numberofParticles * (fraction + 1)) >> 8; // calculate fraction of particles
1795 78 numberofParticles = numberofParticles < 20 ? 20 : numberofParticles; // 20 minimum
1796 //make sure it is a multiple of 4 for proper memory alignment (easier than using padding bytes)
1797 78 numberofParticles = (numberofParticles+3) & ~0x03; // note: with a separate particle buffer, this is probably unnecessary
1798 PSPRINTLN(" calc numparticles:" + String(numberofParticles))
1799 78 return numberofParticles;
1800 }
1801
1802 78 uint32_t calculateNumberOfSources1D(const uint32_t requestedsources) {
1803 #ifdef ESP8266
1804 int numberofSources = max(1, min((int)requestedsources,ESP8266_MAXSOURCES_1D)); // limit
1805 #elif ARDUINO_ARCH_ESP32S2
1806 int numberofSources = max(1, min((int)requestedsources, ESP32S2_MAXSOURCES_1D)); // limit
1807 #else
1808 78 int numberofSources = max(1, min((int)requestedsources, ESP32_MAXSOURCES_1D)); // limit
1809 #endif
1810 // make sure it is a multiple of 4 for proper memory alignment (so minimum is acutally 4)
1811 78 numberofSources = (numberofSources+3) & ~0x03;
1812 78 return numberofSources;
1813 }
1814
1815 //allocate memory for particle system class, particles, sprays plus additional memory requested by FX
1816 78 bool allocateParticleSystemMemory1D(const uint32_t numparticles, const uint32_t numsources, const bool isadvanced, const uint32_t additionalbytes) {
1817 78 uint32_t requiredmemory = sizeof(ParticleSystem1D);
1818 // functions above make sure these are a multiple of 4 bytes (to avoid alignment issues)
1819 78 requiredmemory += sizeof(PSparticleFlags1D) * numparticles;
1820 78 requiredmemory += sizeof(PSparticle1D) * numparticles;
1821 78 requiredmemory += sizeof(PSsource1D) * numsources;
1822 #ifndef ESP8266 // no local buffer on ESP8266
1823 78 requiredmemory += sizeof(CRGB) * SEGMENT.virtualLength();
1824 #endif
1825 78 requiredmemory += additionalbytes + 3; // add 3 to ensure room for stuffing bytes to make it 4 byte aligned
1826
2/2
✓ Branch 0 taken 45 times.
✓ Branch 1 taken 33 times.
78 if (isadvanced)
1827 45 requiredmemory += sizeof(PSadvancedParticle1D) * numparticles;
1828 78 return(SEGMENT.allocateData(requiredmemory));
1829 }
1830
1831 // initialize Particle System, allocate additional bytes if needed (pointer to those bytes can be read from particle system class: PSdataEnd)
1832 // note: percentofparticles is in uint8_t, for example 191 means 75%, (deafaults to 255 or 100% meaning one particle per pixel), can be more than 100% (but not recommended, can cause out of memory)
1833 78 bool initParticleSystem1D(ParticleSystem1D *&PartSys, const uint32_t requestedsources, const uint8_t fractionofparticles, const uint32_t additionalbytes, const bool advanced) {
1834
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 78 times.
78 if (SEGLEN == 1) return false; // single pixel not supported
1835 78 uint32_t numparticles = calculateNumberOfParticles1D(fractionofparticles, advanced);
1836 78 uint32_t numsources = calculateNumberOfSources1D(requestedsources);
1837
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 78 times.
78 if (!allocateParticleSystemMemory1D(numparticles, numsources, advanced, additionalbytes)) {
1838 DEBUG_PRINT(F("PS init failed: memory depleted"));
1839 ✗ return false;
1840 }
1841
5/12
✓ Branch 0 taken 78 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 78 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 78 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 78 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✓ Branch 9 taken 78 times.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
78 PartSys = new (SEGENV.data) ParticleSystem1D(SEGMENT.virtualLength(), numparticles, numsources, advanced); // particle system constructor
1842 78 return true;
1843 }
1844
1845 // blur a 1D buffer, sub-size blurring can be done using start and size
1846 // for speed, 32bit variables are used, make sure to limit them to 8bit (0-255) or result is undefined
1847 // to blur a subset of the buffer, change the size and set start to the desired starting coordinates
1848 5313 void blur1D(CRGB *colorbuffer, uint32_t size, uint32_t blur, uint32_t start)
1849 {
1850 CRGB seeppart, carryover;
1851 5313 uint32_t seep = blur >> 1;
1852 carryover = BLACK;
1853
2/2
✓ Branch 0 taken 45050 times.
✓ Branch 1 taken 5313 times.
50363 for (uint32_t x = start; x < start + size; x++) {
1854 45050 seeppart = colorbuffer[x]; // create copy of current color
1855 45050 fast_color_scale(seeppart, seep); // scale it and seep to neighbours
1856
2/2
✓ Branch 0 taken 44524 times.
✓ Branch 1 taken 526 times.
45050 if (x > 0) {
1857 44524 fast_color_add(colorbuffer[x-1], seeppart);
1858
2/2
✓ Branch 0 taken 19436 times.
✓ Branch 1 taken 25088 times.
44524 if (carryover) // note: check adds overhead but is faster on average
1859 19436 fast_color_add(colorbuffer[x], carryover); // is black on first pass
1860 }
1861 45050 carryover = seeppart;
1862 }
1863 5313 }
1864 #endif // WLED_DISABLE_PARTICLESYSTEM1D
1865
1866 #if !(defined(WLED_DISABLE_PARTICLESYSTEM2D) && defined(WLED_DISABLE_PARTICLESYSTEM1D)) // not both disabled
1867
1868 //////////////////////////////
1869 // Shared Utility Functions //
1870 //////////////////////////////
1871
1872 // calculate the delta speed (dV) value and update the counter for force calculation (is used several times, function saves on codesize)
1873 // force is in 3.4 fixedpoint notation, +/-127
1874 17718 static int32_t calcForce_dv(const int8_t force, uint8_t &counter) {
1875
2/2
✓ Branch 0 taken 5994 times.
✓ Branch 1 taken 11724 times.
17718 if (force == 0)
1876 5994 return 0;
1877 // for small forces, need to use a delay counter
1878 11724 int32_t force_abs = abs(force); // absolute value (faster than lots of if's only 7 instructions)
1879 11724 int32_t dv = 0;
1880 // for small forces, need to use a delay counter, apply force only if it overflows
1881
2/2
✓ Branch 0 taken 7313 times.
✓ Branch 1 taken 4411 times.
11724 if (force_abs < 16) {
1882 7313 counter += force_abs;
1883
2/2
✓ Branch 0 taken 1205 times.
✓ Branch 1 taken 6108 times.
7313 if (counter > 15) {
1884 1205 counter -= 16;
1885
2/2
✓ Branch 0 taken 13 times.
✓ Branch 1 taken 1192 times.
1205 dv = force < 0 ? -1 : 1; // force is either 1 or -1 if it is small (zero force is handled above)
1886 }
1887 }
1888 else
1889 4411 dv = force / 16; // MSBs, note: cannot use bitshift as dv can be negative
1890
1891 11724 return dv;
1892 }
1893
1894 // check if particle is out of bounds and wrap it around if required, returns false if out of bounds
1895 46221 static bool checkBoundsAndWrap(int32_t &position, const int32_t max, const int32_t particleradius, const bool wrap) {
1896
2/2
✓ Branch 0 taken 8949 times.
✓ Branch 1 taken 37272 times.
46221 if ((uint32_t)position > (uint32_t)max) { // check if particle reached an edge, cast to uint32_t to save negative checking (max is always positive)
1897
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 8949 times.
8949 if (wrap) {
1898 ✗ position = position % (max + 1); // note: cannot optimize modulo, particles can be far out of bounds when wrap is enabled
1899 ✗ if (position < 0)
1900 ✗ position += max + 1;
1901 }
1902
4/4
✓ Branch 0 taken 2058 times.
✓ Branch 1 taken 6891 times.
✓ Branch 2 taken 1273 times.
✓ Branch 3 taken 785 times.
8949 else if (((position < -particleradius) || (position > max + particleradius))) // particle is leaving boundaries, out of bounds if it has fully left
1903 8164 return false; // out of bounds
1904 }
1905 38057 return true; // particle is in bounds
1906 }
1907
1908 // fastled color adding is very inaccurate in color preservation (but it is fast)
1909 // a better color add function is implemented in colors.cpp but it uses 32bit RGBW. to use it colors need to be shifted just to then be shifted back by that function, which is slow
1910 // this is a fast version for RGB (no white channel, PS does not handle white) and with native CRGB including scaling of second color
1911 // note: result is stored in c1, not using a return value is faster as the CRGB struct does not need to be copied upon return
1912 // note2: function is mainly used to add scaled colors, so checking if one color is black is slower
1913 // note3: scale is 255 when using blur, checking for that makes blur faster
1914 367299 __attribute__((optimize("O2"))) static void fast_color_add(CRGB &c1, const CRGB &c2, const uint8_t scale) {
1915 367299 uint32_t r, g, b;
1916
2/2
✓ Branch 0 taken 28220 times.
✓ Branch 1 taken 339079 times.
367299 if (scale < 255) {
1917 28220 r = c1.r + ((c2.r * scale) >> 8);
1918 28220 g = c1.g + ((c2.g * scale) >> 8);
1919 28220 b = c1.b + ((c2.b * scale) >> 8);
1920 } else {
1921 339079 r = c1.r + c2.r;
1922 339079 g = c1.g + c2.g;
1923 339079 b = c1.b + c2.b;
1924 }
1925
1926 // note: this chained comparison is the fastest method for max of 3 values (faster than std:max() or using xor)
1927
2/2
✓ Branch 0 taken 132200 times.
✓ Branch 1 taken 235099 times.
367299 uint32_t max = (r > g) ? ((r > b) ? r : b) : ((g > b) ? g : b);
1928
2/2
✓ Branch 0 taken 333437 times.
✓ Branch 1 taken 33862 times.
367299 if (max <= 255) {
1929 333437 c1.r = r; // save result to c1
1930 333437 c1.g = g;
1931 333437 c1.b = b;
1932 } else {
1933 33862 uint32_t newscale = (255U << 16) / max;
1934 33862 c1.r = (r * newscale) >> 16;
1935 33862 c1.g = (g * newscale) >> 16;
1936 33862 c1.b = (b * newscale) >> 16;
1937 }
1938 367299 }
1939
1940 // faster than fastled color scaling as it does in place scaling
1941 240762 __attribute__((optimize("O2"))) static void fast_color_scale(CRGB &c, const uint8_t scale) {
1942 240762 c.r = ((c.r * scale) >> 8);
1943 240762 c.g = ((c.g * scale) >> 8);
1944 240762 c.b = ((c.b * scale) >> 8);
1945 240762 }
1946
1947 #endif // !(defined(WLED_DISABLE_PARTICLESYSTEM2D) && defined(WLED_DISABLE_PARTICLESYSTEM1D))
1948