| 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 ¶llelspeed, 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 |