From dc5c58e98a0d8ec5dd753b8d8c3c0f9f444d90db Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sat, 10 Feb 2024 18:25:37 +0100 Subject: [PATCH] Fixed some bugs in particle system, runs much smoother now also tweaked some of the FX --- wled00/FX.cpp | 93 +++++++++++++++++++++++-------------- wled00/FXparticleSystem.cpp | 69 +++++++++++++-------------- wled00/FXparticleSystem.h | 29 ++++++++---- 3 files changed, 113 insertions(+), 78 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index b2d6e0d22d..36eecb3095 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -8004,8 +8004,8 @@ uint16_t mode_particlefireworks(void) const uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); // particle system box dimensions - const uint16_t PS_MAX_X(cols * PS_P_RADIUS - 1); - const uint16_t PS_MAX_Y(rows * PS_P_RADIUS - 1); + const uint16_t PS_MAX_X=(cols * PS_P_RADIUS - 1); + const uint16_t PS_MAX_Y=(rows * PS_P_RADIUS - 1); #ifdef ESP8266 const uint32_t numParticles = 250; @@ -8170,7 +8170,7 @@ uint16_t mode_particlefireworks(void) static const char _data_FX_MODE_PARTICLEFIREWORKS[] PROGMEM = "Particle Fireworks@Launches,Explosion Size,Height,Bounce,Rockets,Wrap X,Bounce X,Bounce Y;;!;012;pal=11,sx=100,ix=50,c1=64,c2=128,c3=10,o1=0,o2=0,o3=1"; /* - * Particle gravity spray + * Particle Volcano (gravity spray) * Particles are sprayed from below, spray moves back and forth * Uses palette for particle color * by DedeHai (Damian Schneider) @@ -8184,6 +8184,8 @@ uint16_t mode_particlespray(void) const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; const uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); + //particle system x dimension + const uint16_t PS_MAX_X = (cols * PS_P_RADIUS - 1); const uint32_t numParticles = 450; const uint8_t numSprays = 1; @@ -8218,11 +8220,12 @@ uint16_t mode_particlespray(void) { spray[i].source.hue = random8(); spray[i].source.sat = 255; // set full saturation - spray[i].source.x = 4 * PS_P_RADIUS * (i + 1); + spray[i].source.x = (cols * PS_P_RADIUS)/(numSprays+1) * (i + 1); spray[i].source.y = 5; // just above the lower edge, if zero, particles already 'bounce' at start and loose speed. - spray[i].source.vx = 10; + spray[i].source.vx = 0; spray[i].maxLife = 300; // lifetime in frames - spray[i].minLife = 30; + spray[i].minLife = 20; + spray[i].source.collide = true; //seeded particles will collide spray[i].vx = 0; // emitting speed spray[i].vy = 20; // emitting speed // spray.var = 10 + (random8() % 4); @@ -8236,33 +8239,35 @@ uint16_t mode_particlespray(void) for (i = 0; i < numSprays; i++) { spray[i].source.hue++; // = random8(); //change hue of spray source - } - - for (i = 0; i < numSprays; i++) - { // percycle = 1+(SEGMENT.intensity>>4); //how many particles are sprayed per cycle and how fast ist the color changing - if (spray[i].source.vx > 0) // moving to the right currently - { - spray[i].source.vx = SEGMENT.speed >> 4; // spray speed + if(SEGMENT.check2) //bounce + { + if (spray[i].source.vx > 0) // moving to the right currently + { + spray[i].source.vx = SEGMENT.speed >> 4; // spray speed + } + else + { + spray[i].source.vx = -(SEGMENT.speed >> 4); // spray speed (is currently moving negative so keep it negative) + } } - else - { - spray[i].source.vx = -(SEGMENT.speed >> 4); // spray speed (is currently moving negative so keep it negative) + else{ //wrap on the right side + spray[i].source.vx = SEGMENT.speed >> 4; // spray speed + if (spray[i].source.x >= PS_MAX_X-32) spray[i].source.x = 1; //wrap if close to border (need to wrap before the bounce updated detects a border collision or it will just be stuck) } - spray[i].vy = SEGMENT.custom1 >> 3; // emitting speed, upward - spray[i].vx = ((int16_t)SEGMENT.custom2 - (int16_t)127) / 8; // emitting speed, left/right (=angle) - spray[i].var = SEGMENT.custom3 >> 1; // emiting variation = nozzle size (custom 3 goes from 0-32) - spray[i].source.y = spray[i].var + 1; // need to 'lift up' the source as 'var' also changes particle spawn position randomly + spray[i].vy = SEGMENT.custom1 >> 2; // emitting speed, upward + spray[i].vx = 0; + spray[i].var = 0;//!!!SEGMENT.custom3; // emiting variation = nozzle size (custom 3 goes from 0-32) + //spray[i].source.y = spray[i].var + 1; // need to 'lift up' the source as 'var' also changes particle spawn position randomly spray[i].source.ttl = 255; // source never dies, replenish its lifespan } i = 0; j = 0; - while (i < numParticles) + for (i = 0; i < numParticles; i++) { if (particles[i].ttl == 0) // find a dead particle - { - // ColorFromPalette(SEGPALETTE, random8() , 255, LINEARBLEND); + { // spray[j].source.hue = random8(); //set random color for each particle (using palette) Emitter_Fountain_emit(&spray[j], &particles[i]); j = (j + 1) % numSprays; @@ -8271,9 +8276,11 @@ uint16_t mode_particlespray(void) break; // quit loop if all particles of this round emitted } } - i++; } } + uint8_t hardness = SEGMENT.custom2; + if (SEGMENT.check3)//collisions enabled + detectCollisions(particles, numParticles, hardness); for (i = 0; i < numSprays; i++) { @@ -8283,19 +8290,23 @@ uint16_t mode_particlespray(void) for (i = 0; i < numParticles; i++) { // Particle_Move_update(&particles[i]); //move the particles - Particle_Gravity_update(&particles[i], SEGMENT.check1, SEGMENT.check2, SEGMENT.check3, (uint8_t)200); + //set color according to ttl ('color by age') + if (SEGMENT.check1) + particles[i].hue = min((uint16_t)220, particles[i].ttl); + + Particle_Gravity_update(&particles[i], false, SEGMENT.check2, true, hardness); } SEGMENT.fill(BLACK); // clear the matrix // render the particles - ParticleSys_render(particles, numParticles, SEGMENT.check1, false); + ParticleSys_render(particles, numParticles, false, false); // CRGB c = PURPLE; // SEGMENT.setPixelColorXY(0, 0, c); return FRAMETIME; } -static const char _data_FX_MODE_PARTICLESPRAY[] PROGMEM = "Particle Spray@Moving Speed,Intensity,Particle Speed,Spray Angle,Nozzle Size,Wrap X,Bounce X,Bounce Y;;!;012;pal=11,sx=100,ix=200,c1=190,c2=128,c3=8,o1=0,o2=0,o3=0"; +static const char _data_FX_MODE_PARTICLESPRAY[] PROGMEM = "Particle Volcano@Moving Speed,Intensity,Particle Speed,Bouncyness,Nozzle Size,Color by Age,Bounce X,Collisions;;!;012;pal=35,sx=0,ix=160,c1=100,c2=160,c3=10,o1=1,o2=1,o3=1"; // good default values for sliders: 100,200,190, 45 /*syntax for json configuration string: @@ -8536,6 +8547,7 @@ uint16_t mode_particlefall(void) particles[i].vy = -(SEGMENT.speed >> 1); particles[i].hue = random8(); // set random color particles[i].sat = ((SEGMENT.custom3) << 3) + 7; // set saturation + particles[i].collide = true; // particle will collide break; // quit loop if all particles of this round emitted } i++; @@ -8615,6 +8627,7 @@ uint16_t mode_particlepile(void) spray[i].source.x = 2 * PS_P_RADIUS * (i + 1); spray[i].source.y = 14 * PS_P_RADIUS; // source y position, fixed at 14pixel height spray[i].source.vx = 10; + spray[i].source.collide = true; //seeded particles will collide spray[i].maxLife = 600; // lifetime in frames spray[i].minLife = 200; spray[i].vx = 0; // emitting speed @@ -8734,6 +8747,7 @@ uint16_t mode_particlebox(void) particles[i].sat = 255; // set full saturation (lets palette choose the color) particles[i].x = map(i, 0, 255, 1, cols * PS_P_RADIUS); // distribute along x according to color particles[i].y = random16((rows >> 2) * PS_P_RADIUS); // in the bottom quarder + particles[i].collide = true; //all particles collide } } @@ -8784,7 +8798,7 @@ uint16_t mode_particlebox(void) // scale gravity force xgravity = ((int16_t)xgravity * scale) >> 8; ygravity = ((int16_t)ygravity * scale) >> 8; - // todo: check with console output to see if scaling is applied correctly. + } // scale the gravity force down @@ -8800,6 +8814,11 @@ uint16_t mode_particlebox(void) particles[i].vx += xgravity; particles[i].vy += ygravity; particles[i].ttl = 500; // particles never die + + // apply a little gravity downwards to bias things in rocking mode + if (SEGMENT.check1 && SEGMENT.call % 2 == 0) + particles[i].vy--; + } } } @@ -8816,8 +8835,7 @@ uint16_t mode_particlebox(void) if (SEGMENT.call % 8 == 0) { applyFriction(&particles[i], 1); - } - // Particle_Gravity_update(&particles[i], SEGMENT.check1, SEGMENT.check2, SEGMENT.check3, (uint8_t)200); + } Particle_Bounce_update(&particles[i], hardness); } @@ -9037,18 +9055,19 @@ uint16_t mode_particleimpact(void) Serial.print(" "); if (meteors[i].source.ttl) { - Particle_Move_update(&meteors[i].source); // move the meteor, age the meteor (ttl--) + Particle_Gravity_update(&meteors[i].source, SEGMENT.check1, SEGMENT.check2, true, 255); // move the meteor, age the meteor (ttl--) // if source reaches the bottom, set speed to 0 so it will explode on next function call (handled above) if ((meteors[i].source.y < PS_P_RADIUS) && (meteors[i].source.vy < 0)) // reached the bottom pixel on its way down { meteors[i].source.vy = 0; // set speed zero so it will explode meteors[i].source.vx = 0; - meteors[i].source.y = 5; // offset from ground so explosion happens not out of frame (if moving fast, this can happen) + meteors[i].source.y = 5; // offset from ground so explosion happens not out of frame + meteors[i].source.collide = true; // explosion particles will collide if checked meteors[i].maxLife = 200; meteors[i].minLife = 50; meteors[i].source.ttl = random8((255 - SEGMENT.speed)) + 10; // standby time til next launch (in frames at 42fps, max of 265 is about 6 seconds meteors[i].vx = 0; // emitting speed x - meteors[i].vy = 8; // emitting speed y + meteors[i].vy = (SEGMENT.custom1 >> 2); // emitting speed y meteors[i].var = (SEGMENT.custom1 >> 1); // speed variation around vx,vy (+/- var/2) } } @@ -9057,14 +9076,15 @@ uint16_t mode_particleimpact(void) // reinitialize rocket meteors[i].source.y = PS_MAX_Y + PS_P_RADIUS<<2; // start 4 pixels above the top meteors[i].source.x = random16(PS_MAX_X); - meteors[i].source.vy = -30 - random(30) - 10; // TODO: need to make this user selectable? + meteors[i].source.vy = -random(30) - 30; //meteor downward speed meteors[i].source.vx = random8(30) - 15; meteors[i].source.hue = random8(); // random color meteors[i].source.ttl = 1000; // long live, will explode at bottom + meteors[i].source.collide = false; // trail particles will not collide meteors[i].maxLife = 60; // spark particle life - meteors[i].minLife = 20; + meteors[i].minLife = 20; meteors[i].vx = 0; // emitting speed - meteors[i].vy = -10; // emitting speed + meteors[i].vy = -9; // emitting speed (down) meteors[i].var = 5; // speed variation around vx,vy (+/- var/2) } } @@ -9136,6 +9156,7 @@ uint16_t mode_particleattractor(void) spray->source.vx = random8(5) + 6; spray->source.vy = random8(4) + 3; spray->source.ttl = 100; + spray->source.collide = true; //seeded particles will collide (if checked) spray->maxLife = 300; // seeded particle lifetime in frames spray->minLife = 30; spray->vx = 0; // emitting speed diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index fed38d1fdd..cc9716ffa8 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -46,7 +46,7 @@ void Emitter_Flame_emit(PSpointsource *emitter, PSparticle *part) part->vy = emitter->vy + random8(emitter->var) - (emitter->var >> 1); part->ttl = (uint8_t)((rand() % (emitter->maxLife - emitter->minLife)) + emitter->minLife + emitter->source.ttl); // flame intensity dies down with emitter TTL part->hue = emitter->source.hue; - part->sat = emitter->source.sat; + //part->sat = emitter->source.sat; //flame does not use saturation } // fountain style emitter @@ -56,9 +56,10 @@ void Emitter_Fountain_emit(PSpointsource *emitter, PSparticle *part) part->y = emitter->source.y; // + random8(emitter->var) - (emitter->var >> 1); part->vx = emitter->vx + random8(emitter->var) - (emitter->var >> 1); part->vy = emitter->vy + random8(emitter->var) - (emitter->var >> 1); - part->ttl = (rand() % (emitter->maxLife - emitter->minLife)) + emitter->minLife; + part->ttl = random16(emitter->maxLife - emitter->minLife) + emitter->minLife; part->hue = emitter->source.hue; part->sat = emitter->source.sat; + part->collide = emitter->source.collide; } // Emits a particle at given angle and speed, angle is from 0-255 (=0-360deg), speed is also affected by emitter->var @@ -732,7 +733,7 @@ void detectCollisions(PSparticle* particles, uint32_t numparticles, uint8_t hard { // go though all 'higher number' particles and see if any of those are in close proximity // if they are, make them collide - if (particles[i].ttl > 0) // if particle is alive + if (particles[i].ttl > 0 && particles[i].collide) // if particle is alive and does collide { int32_t dx, dy; // distance to other particles for (j = i + 1; j < numparticles; j++) @@ -769,7 +770,8 @@ void handleCollision(PSparticle *particle1, PSparticle *particle2, const uint8_t if (distanceSquared == 0) // add distance in case particles exactly meet at center, prevents dotProduct=0 (this can only happen if they move towards each other) { // Adjust positions based on relative velocity direction - if (relativeVx < 0) { //if true, particle2 is on the right side + + if (relativeVx <= 0) { //if true, particle2 is on the right side particle1->x--; particle2->x++; } else{ @@ -777,7 +779,7 @@ void handleCollision(PSparticle *particle1, PSparticle *particle2, const uint8_t particle2->x--; } - if (relativeVy < 0) { + if (relativeVy <= 0) { particle1->y--; particle2->y++; } else{ @@ -796,23 +798,22 @@ void handleCollision(PSparticle *particle1, PSparticle *particle2, const uint8_t // Calculate new velocities after collision int32_t impulse = (((dotProduct << (bitshift)) / (distanceSquared)) * hardness) >> 8; - - particle1->vx += (impulse * dx) >> bitshift; - particle1->vy += (impulse * dy) >> bitshift; - particle2->vx -= (impulse * dx) >> bitshift; - particle2->vy -= (impulse * dy) >> bitshift; - + int32_t ximpulse = (impulse * dx) >> bitshift; + int32_t yimpulse = (impulse * dy) >> bitshift; + particle1->vx += ximpulse; + particle1->vy += yimpulse; + particle2->vx -= ximpulse; + particle2->vy -= yimpulse; + if (hardness < 50) // if particles are soft, they become 'sticky' i.e. no slow movements { - if (particle1->vx < 2 && particle1->vx > -2) - particle1->vx = 0; - if (particle1->vy < 2 && particle1->vy > -2) - particle1->vy = 0; - if (particle2->vx < 2 && particle1->vx > -2) - particle1->vx = 0; - if (particle2->vy < 2 && particle1->vy > -2) - particle1->vy = 0; + particle1->vx = (particle1->vx < 2 && particle1->vx > -2) ? 0 : particle1->vx; + particle1->vy = (particle1->vy < 2 && particle1->vy > -2) ? 0 : particle1->vy; + + particle2->vx = (particle2->vx < 2 && particle2->vx > -2) ? 0 : particle2->vx; + particle2->vy = (particle2->vy < 2 && particle2->vy > -2) ? 0 : particle2->vy; } + } // particles have volume, push particles apart if they are too close by moving each particle by a fixed amount away from the other particle @@ -820,31 +821,31 @@ void handleCollision(PSparticle *particle1, PSparticle *particle2, const uint8_t // one problem remaining is, particles get squished if (external) force applied is higher than the pushback but this may also be desirable if particles are soft. also some oscillations cannot be avoided without addigng a counter if (distanceSquared < 2 * PS_P_HARDRADIUS * PS_P_HARDRADIUS) { - int32_t push; - //uint8_t rndchoice = random8(2); - const uint32_t HARDDIAMETER = PS_P_HARDRADIUS <<1; + uint8_t rndchoice = random8(2); + const uint32_t HARDDIAMETER = 2*PS_P_HARDRADIUS; + if (dx < HARDDIAMETER && dx > -HARDDIAMETER) { // distance is too small, push them apart - push = 1; + int32_t push = 3; if (dx < 0) // dx is negative push =-push; // negative push direction - - //if (rndchoice) // randomly chose one of the particles to push, avoids oscillations - // particle1->x -= push; - //else - particle2->x += push; //only push one particle to avoid oscillations + + if (rndchoice) // randomly chose one of the particles to push, avoids oscillations + particle1->x -= push; + else + particle2->x += push; // only push one particle to avoid oscillations } + if (dy < HARDDIAMETER && dy > -HARDDIAMETER) { // distance is too small (or negative) - push = 1; + int32_t push = 3; if (dy < 0) // dy is negative push = -push;//negative push direction - - //if (rndchoice) // randomly chose one of the particles to push, avoids oscillations - // particle1->y -= push; - //else - particle2->y += push; // only push one particle to avoid oscillations + if (rndchoice) // randomly chose one of the particles to push, avoids oscillations + particle1->y -= push; + else + particle2->y += push; // only push one particle to avoid oscillations } //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 } diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index 78acf52c51..ef0d5aa715 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -31,23 +31,36 @@ //particle dimensions (subpixel division) #define PS_P_RADIUS 64 //subpixel size, each pixel is divided by this for particle movement -#define PS_P_HARDRADIUS 100 //hard surface radius of a particle, used for collision detection proximity,also this is forbidden to be entered by another particle (for stacking) +#define PS_P_HARDRADIUS 100 //hard surface radius of a particle, used for collision detection proximity #define PS_P_SURFACE 12 //shift: 2^PS_P_SURFACE = (PS_P_RADIUS)^2 -//todo: can add bitfields to add in more stuff -//but when using bit fields, computation time increases as instructions are needed to mask the fields, only do it with variables that do not get updated too often +//todo: can add bitfields to add in more stuff, but accessing bitfields is slower than direct memory access! +//flags as bitfields is still very fast to access. + +union Flags { + struct { + + }; + uint8_t flagsByte; +}; + //struct for a single particle typedef struct { int16_t x; //x position in particle system - int16_t y; //y position in particle system - uint16_t ttl; //time to live - uint8_t outofbounds; //set to 1 if outside of matrix TODO: could make this a union and add more flags instead of wasting a whole byte on this - uint8_t hue; //color hue - uint8_t sat; //color saturation + int16_t y; //y position in particle system int8_t vx; //horizontal velocity int8_t vy; //vertical velocity + uint16_t ttl; // time to live + uint8_t hue; // color hue + uint8_t sat; // color saturation + //add a one byte bit field: + bool outofbounds : 1; //out of bounds flag, set to true if particle is outside of display area + bool collide : 1; //if flag is set, particle will take part in collisions + bool flag2 : 1; // unused flags... could use one for collisions to make those selective. + bool flag3 : 1; + uint8_t counter : 4; //a 4 bit counter for particle control } PSparticle; //struct for a particle source