From ac5399aa26f403dbb0c7ab05163c0bd9d0d60f6b Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sun, 5 May 2024 08:18:43 +0200 Subject: [PATCH] Added advanced particle size control and new Blob FX - advanced size control allows for growing, shrinking, wobbling - render function updated to support asymmetric rendering - various code improvements and bugfixes - some FX parameter tuning - bugfix: removed sli() sei() calls in render function that caused random flickering on S3/C3 (may add that back in but only for ESP8266 to reduce fragmentation) - removed some debug / test stuff --- wled00/FX.cpp | 263 +++++++++++++------------- wled00/FX.h | 3 +- wled00/FXparticleSystem.cpp | 361 +++++++++++++++++++++--------------- wled00/FXparticleSystem.h | 56 +++--- 4 files changed, 371 insertions(+), 312 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index c0fc30c747..df953a8750 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -8136,7 +8136,7 @@ uint16_t mode_particlefireworks(void) currentspeed = speed; counter = 0; angleincrement = 2730 + random16(5461); // minimum 15° (=2730), + random(30°) (=5461) - angle = random16(); // random start angle + angle = esp_random(); // random start angle speedvariation = angle & 0x01; // 0 or 1, no need for a new random number // calculate the number of particles to make complete circles percircle = (uint16_t)0xFFFF / angleincrement + 1; @@ -8193,7 +8193,7 @@ uint16_t mode_particlefireworks(void) { if (PartSys->sources[j].source.ttl) { - PartSys->particleMoveUpdate(PartSys->sources[j].source, PartSys->particlesettings); //todo: need different settings for rocket? + PartSys->particleMoveUpdate(PartSys->sources[j].source); //todo: need different settings for rocket? } else if (PartSys->sources[j].source.vy > 0) // rocket has died and is moving up. stop it so it will explode (is handled in the code above) { @@ -8209,9 +8209,9 @@ uint16_t mode_particlefireworks(void) { // reinitialize rocket PartSys->sources[j].source.y = PS_P_RADIUS<<1; // start from bottom - PartSys->sources[j].source.x = (rand() % (PartSys->maxX >> 1)) + (PartSys->maxX >> 2); // centered half + PartSys->sources[j].source.x = random(PartSys->maxX >> 2, PartSys->maxX >> 1); // centered half PartSys->sources[j].source.vy = random16(SEGMENT.custom1 >> 3) + 5; // rocket speed depends also on rocket fuse - PartSys->sources[j].source.vx = random(5) - 2; //i.e. not perfectly straight up + PartSys->sources[j].source.vx = random(-3,3); //i.e. not perfectly straight up PartSys->sources[j].source.sat = 30; // low saturation -> exhaust is off-white PartSys->sources[j].source.ttl = random16(SEGMENT.custom1) + (SEGMENT.custom1 >> 1); // sets explosion height (rockets explode at the top if set too high as paticle update set speed to zero if moving out of matrix) PartSys->sources[j].maxLife = 40; // exhaust particle life @@ -8259,7 +8259,7 @@ uint16_t mode_particlevolcano(void) PartSys->sources[i].maxLife = 300; // lifetime in frames PartSys->sources[i].minLife = 250; PartSys->sources[i].source.collide = true; // seeded particles will collide (if enabled) - PartSys->sources[i].source.perpetural = true; // source never dies + PartSys->sources[i].source.perpetual = true; // source never dies } } else @@ -8299,7 +8299,7 @@ uint16_t mode_particlevolcano(void) PartSys->sources[i].var = SEGMENT.custom3 | 0x01; // emiting variation = nozzle size (custom 3 goes from 0-31), only use odd numbers // spray[j].source.hue = random16(); //set random color for each particle (using palette) -> does not look good PartSys->sprayEmit(PartSys->sources[i]); - PartSys->particleMoveUpdate(PartSys->sources[i].source, PartSys->particlesettings); //move the source (also applies gravity, which is corrected for above, that is a hack but easier than creating more particlesettings) + PartSys->particleMoveUpdate(PartSys->sources[i].source); //move the source (also applies gravity, which is corrected for above, that is a hack but easier than creating more particlesettings) //Serial.println("emit"); } } @@ -8326,9 +8326,9 @@ uint16_t mode_particlefire(void) if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. { - if (!initParticleSystem(PartSys, 25, false, 4)) //maximum number of source (PS will determine the exact number based on segment size) and need 4 additional bytes for time keeping (uint32_t lastcall) + if (!initParticleSystem(PartSys, 25, 4)) //maximum number of source (PS will determine the exact number based on segment size) and need 4 additional bytes for time keeping (uint32_t lastcall) return mode_static(); // allocation failed; //allocation failed - SEGMENT.aux0 = rand(); // aux0 is wind position (index) in the perlin noise + SEGMENT.aux0 = esp_random(); // aux0 is wind position (index) in the perlin noise numFlames = PartSys->numSources; DEBUG_PRINTF_P(PSTR("segment data ptr in fireFX %p\n"), SEGMENT.data); } @@ -8377,13 +8377,13 @@ uint16_t mode_particlefire(void) // initialize new flame: set properties of source if (random16(20) == 0 || SEGMENT.call == 0) // from time to time, change flame position { - PartSys->sources[i].source.x = (PartSys->maxX >> 1) - (spread>>1) + (rand() % spread) ; // distribute randomly on chosen width + PartSys->sources[i].source.x = (PartSys->maxX >> 1) - (spread>>1) + random(spread); // distribute randomly on chosen width } PartSys->sources[i].source.y = -PS_P_RADIUS; // set the source below the frame PartSys->sources[i].source.ttl = 5 + random16((SEGMENT.custom1 * SEGMENT.custom1) >> 7) / (2 + (firespeed >> 4)); //'hotness' of fire, faster flames reduce the effect or flame height will scale too much with speed -> new, this works! PartSys->sources[i].maxLife = random16(7) + 13; // defines flame height together with the vy speed, vy speed*maxlife/PS_P_RADIUS is the average flame height PartSys->sources[i].minLife = 4; - PartSys->sources[i].vx = (int8_t)random(4) - 2; // emitting speed (sideways) + PartSys->sources[i].vx = (int8_t)random(-3, 3); // emitting speed (sideways) PartSys->sources[i].vy = 5 + (firespeed >> 2); // emitting speed (upwards) -> this is good PartSys->sources[i].var = (random16(2 + (firespeed >> 5)) + 3) | 0x01; // speed variation around vx,vy (+/- var/2), only use odd numbers } @@ -8445,7 +8445,7 @@ uint16_t mode_particlepit(void) if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. { - if (!initParticleSystem(PartSys, 1, true)) //init, request one source (actually dont really need one TODO: test if using zero sources also works) + if (!initParticleSystem(PartSys, 1, 0, true)) //init, request one source (actually dont really need one TODO: test if using zero sources also works) return mode_static(); // allocation failed; //allocation failed PartSys->setKillOutOfBounds(true); PartSys->setGravity(); //enable with default gravity @@ -8515,92 +8515,6 @@ uint16_t mode_particlepit(void) PartSys->update(); // update and render -//Experiment: blur to grow the particles, also blur asymmetric, make the particles wobble: - /* - SEGMENT.blur(SEGMENT.custom1, true); - if (SEGMENT.custom1 > 64) - SEGMENT.blur(SEGMENT.custom1 - 64, true); - if (SEGMENT.custom1 > 128) - SEGMENT.blur((SEGMENT.custom1 - 128) << 1, true); - if (SEGMENT.custom1 > 192) - SEGMENT.blur((SEGMENT.custom1 - 192) << 1, true); - */ -/* -//wobbling - static uint8_t testcntr; - static uint8_t wobbleamount = 200; - wobbleamount -= 2; - - testcntr+=15; - -// int32_t ysize = (int16_t)sin8(testcntr); - // int32_t xsize = 255-ysize; - - int32_t ysize = (int32_t)sin8(testcntr)-128; - int32_t xsize = -ysize; //TODO: xsize is not really needed, calculation can be simplified using just ysize - - //ysize = (((int16_t)SEGMENT.custom1 * ysize) >> 8); - //xsize = (((int16_t)SEGMENT.custom1 * xsize) >> 8); - ysize = (int32_t)SEGMENT.custom1 - ((ysize * wobbleamount * SEGMENT.custom1) >> 15); - xsize = (int32_t)SEGMENT.custom1 - ((xsize * wobbleamount * SEGMENT.custom1) >> 15); - - Serial.print(SEGMENT.custom1); - Serial.print(" "); - Serial.print(wobbleamount); - Serial.print(" "); - - Serial.print(xsize); - Serial.print(" "); - Serial.print(ysize); - - - const unsigned cols = PartSys->maxXpixel + 1; - const unsigned rows = PartSys->maxYpixel + 1; - uint8_t xiterations = 1 + (xsize>>8); //allow for wobble size > 255 - uint8_t yiterations = 1 + (ysize>>8); - uint8_t secondpassxsize = xsize - 255; - uint8_t secondpassysize = ysize - 255; - if (xsize > 255) - xsize = 255; //first pass, full sized - if (ysize > 255) - ysize = 255; - - Serial.print(xsize); - Serial.print(" "); - Serial.println(ysize); - for (uint32_t j = 0; j < xiterations; j++) - { - for (uint32_t i = 0; i < cols; i++) - { - SEGMENT.blurCol(i, xsize, true); - if (xsize > 64) - SEGMENT.blurCol(i, xsize - 64, true); - if (xsize > 128) - SEGMENT.blurCol(i, (xsize - 128) << 1, true); - if (xsize > 192) - SEGMENT.blurCol(i, (xsize - 192) << 1, true); - } - //set size for second pass: - xsize = secondpassxsize; - } - for (uint32_t j = 0; j < yiterations; j++) - { - for (unsigned i = 0; i < rows; i++) - { - SEGMENT.blurRow(i, ysize, true); - if (ysize > 64) - SEGMENT.blurRow(i, ysize - 64, true); - if (ysize > 128) - SEGMENT.blurRow(i, (ysize - 128) << 1, true); - if (ysize > 192) - SEGMENT.blurRow(i, (ysize - 192) << 1, true); - } - // set size for second pass: - ysize = secondpassysize; - } -*/ - - /* //rotat image (just a test, non working yet) float angle = PI/3; @@ -8743,10 +8657,28 @@ uint16_t mode_particlebox(void) ParticleSystem *PartSys = NULL; uint32_t i; - if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. + if (SEGMENT.call == 0) // initialization { if (!initParticleSystem(PartSys, 1)) // init - return mode_static(); // allocation failed; //allocation failed + return mode_static(); // allocation failed + PartSys->setBounceX(true); + PartSys->setBounceY(true); + //set max number of particles and save to aux1 for later + #ifdef ESP8266 + SEGMENT.aux1 = min((uint16_t)((PartSys->maxXpixel * PartSys->maxYpixel) >> 1), PartSys->numParticles); + #else + SEGMENT.aux1 = min((uint16_t)((PartSys->maxXpixel * PartSys->maxYpixel)), PartSys->numParticles); //max number of particles + #endif + for (i = 0; i < SEGMENT.aux1; i++) + { + PartSys->particles[i].ttl = 500; //set all particles alive (not all are rendered though) + PartSys->particles[i].perpetual = true; //never die + PartSys->particles[i].hue = i * 3; // color range + PartSys->particles[i].x = map(i, 0, SEGMENT.aux1, 1, PartSys->maxX); // distribute along x according to color + PartSys->particles[i].y = random16(PartSys->maxY >> 2); //bottom quarter + PartSys->particles[i].collide = true; // all particles collide + } + SEGMENT.aux0 = rand(); // position in perlin noise } else PartSys = reinterpret_cast(SEGMENT.data); // if not first call, just set the pointer to the PS @@ -8758,31 +8690,11 @@ uint16_t mode_particlebox(void) } PartSys->updateSystem(); // update system properties (dimensions and data pointers) - PartSys->setBounceX(true); - PartSys->setBounceY(true); + PartSys->setWallHardness(min(SEGMENT.custom2, (uint8_t)200)); // wall hardness is 200 or more PartSys->enableParticleCollisions(true, max(2, (int)SEGMENT.custom2)); // enable collisions and set particle collision hardness - - #ifdef ESP8266 - uint16_t maxnumParticles = min((uint16_t)((PartSys->maxXpixel * PartSys->maxYpixel) >> 1), PartSys->numParticles); - #else - uint16_t maxnumParticles = min((uint16_t)((PartSys->maxXpixel * PartSys->maxYpixel)), PartSys->numParticles); - #endif - PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 10, maxnumParticles)); - - if (SEGMENT.call == 0) // initialization of particles (cannot be done in above loop, only if code lines above here are copied there) - { - SEGMENT.aux0 = rand(); // position in perlin noise - for (i = 0; i < maxnumParticles; i++) - { - PartSys->particles[i].ttl = 500; //set all particles alive (not all are rendered though) - PartSys->particles[i].perpetural = true; //never dies - PartSys->particles[i].hue = i * 5; // color range - PartSys->particles[i].x = map(i, 0, maxnumParticles, 1, PartSys->maxX); // distribute along x according to color - PartSys->particles[i].y = random16(PartSys->maxY); // randomly in y direction - PartSys->particles[i].collide = true; // all particles collide - } - } + PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 10, SEGMENT.aux1)); //aux1 holds max number of particles to use + if (SEGMENT.call % (((255 - SEGMENT.speed) >> 6) + 1) == 0 && SEGMENT.speed > 0) // how often the force is applied depends on speed setting { @@ -8823,7 +8735,7 @@ uint16_t mode_particlebox(void) return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEBOX[] PROGMEM = "PS Box@Speed,Particles,Tilt Strength,Hardness,Friction,Random,Direction,Sloshing;;!;2;pal=43,sx=120,ix=100,c1=100,c2=210,o1=1"; +static const char _data_FX_MODE_PARTICLEBOX[] PROGMEM = "PS Box@Speed,Particles,Tilt Strength,Hardness,Friction,Random,Direction,Sloshing;;!;2;pal=53,sx=120,ix=100,c1=100,c2=210,o1=1"; /* Fuzzy Noise: Perlin noise 'gravity' mapping as in particles on 'noise hills' viewed from above @@ -8838,7 +8750,7 @@ uint16_t mode_particleperlin(void) uint32_t i; if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. { - if (!initParticleSystem(PartSys, 1, true)) // init with 1 source and advanced properties + if (!initParticleSystem(PartSys, 1, 0, true)) // init with 1 source and advanced properties return mode_static(); // allocation failed; //allocation failed PartSys->setKillOutOfBounds(true); //should never happen, but lets make sure there are no stray particles SEGMENT.aux0 = rand(); @@ -8987,7 +8899,7 @@ uint16_t mode_particleimpact(void) if (PartSys->sources[i].source.vy < 0) //move down { PartSys->applyGravity(&PartSys->sources[i].source); - PartSys->particleMoveUpdate(PartSys->sources[i].source, meteorsettings); + PartSys->particleMoveUpdate(PartSys->sources[i].source, &meteorsettings); // if source reaches the bottom, set speed to 0 so it will explode on next function call (handled above) if (PartSys->sources[i].source.y < PS_P_RADIUS<<1) // reached the bottom pixel on its way down @@ -9051,14 +8963,13 @@ uint16_t mode_particleattractor(void) PSparticle *attractor; //particle pointer to the attractor if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. { - if (!initParticleSystem(PartSys, 1, true)) // init using 1 source and advanced particle settings + if (!initParticleSystem(PartSys, 1, 0, true)) // init using 1 source and advanced particle settings return mode_static(); // allocation failed; //allocation failed //DEBUG_PRINTF_P(PSTR("sources in FX %p\n"), &PartSys->sources[0]); PartSys->sources[0].source.hue = random16(); - PartSys->sources[0].source.vx = -7; - PartSys->sources[0].source.collide = true; // seeded particles will collide - //PartSys->sources[0].source.ttl = 100; //TODO: remove, is now done in PS init - PartSys->sources[0].source.perpetural = true; //source does not age + PartSys->sources[0].source.vx = -7; //will collied with wall and get random bounce direction + PartSys->sources[0].source.collide = true; // seeded particles will collide + PartSys->sources[0].source.perpetual = true; //source does not age #ifdef ESP8266 PartSys->sources[0].maxLife = 200; // lifetime in frames (ESP8266 has less particles) PartSys->sources[0].minLife = 30; @@ -9067,6 +8978,7 @@ uint16_t mode_particleattractor(void) PartSys->sources[0].minLife = 50; #endif PartSys->sources[0].var = 7; // emiting variation + PartSys->sources[0].size = 100; //!!! debug remove this again PartSys->setWallHardness(255); //bounce forever PartSys->setWallRoughness(200); //randomize wall bounce } @@ -9099,13 +9011,13 @@ uint16_t mode_particleattractor(void) attractor->vx = PartSys->sources[0].source.vy; // set to spray movemement but reverse x and y attractor->vy = PartSys->sources[0].source.vx; attractor->ttl = 100; - attractor->perpetural = true; + attractor->perpetual = true; } //set attractor properties if (SEGMENT.check2) { if((SEGMENT.call % 3) == 0) // move slowly - PartSys->particleMoveUpdate(*attractor, sourcesettings); // move the attractor + PartSys->particleMoveUpdate(*attractor, &sourcesettings); // move the attractor } else{ @@ -9129,7 +9041,7 @@ uint16_t mode_particleattractor(void) } if (SEGMENT.call % (33 - SEGMENT.custom3) == 0) PartSys->applyFriction(2); - PartSys->particleMoveUpdate(PartSys->sources[0].source, sourcesettings); // move the source + PartSys->particleMoveUpdate(PartSys->sources[0].source, &sourcesettings); // move the source PartSys->update(); // update and render return FRAMETIME; } @@ -9423,7 +9335,7 @@ static const char _data_FX_MODE_PARTICLEGEQ[] PROGMEM = "PS Equalizer@Speed,Inte Particle replacement of Ghost Rider by DedeHai (Damian Schneider), original by stepko adapted by Blaz Kristan (AKA blazoncek) */ #define MAXANGLESTEP 2200 //32767 means 180° -uint16_t mode_particlghostrider(void) +uint16_t mode_particleghostrider(void) { if (SEGLEN == 1) return mode_static(); @@ -9493,7 +9405,7 @@ uint16_t mode_particlghostrider(void) PartSys->sources[0].source.vx = ((int32_t)cos16(SEGMENT.aux0) * speed) / (int32_t)32767; PartSys->sources[0].source.vy = ((int32_t)sin16(SEGMENT.aux0) * speed) / (int32_t)32767; PartSys->sources[0].source.ttl = 500; //source never dies - PartSys->particleMoveUpdate(PartSys->sources[0].source, ghostsettings); + PartSys->particleMoveUpdate(PartSys->sources[0].source, &ghostsettings); //set head (steal one of the particles) PartSys->particles[PartSys->usedParticles-1].x = PartSys->sources[0].source.x; PartSys->particles[PartSys->usedParticles-1].y = PartSys->sources[0].source.y; @@ -9509,6 +9421,84 @@ uint16_t mode_particlghostrider(void) } static const char _data_FX_MODE_PARTICLEGHOSTRIDER[] PROGMEM = "PS Ghost Rider@Speed,Spiral,Blur,Color Cycle,Spread,Color by age,Walls;;!;2;pal=1,sx=70,ix=0,c1=220,c2=30,c3=21,o1=1,o2=0,o3=0"; + +/* +PS Blobs: large particles bouncing around +Uses palette for particle color +by DedeHai (Damian Schneider) +*/ +uint16_t mode_particleblobs(void) +{ + if (SEGLEN == 1) + return mode_static(); + ParticleSystem *PartSys = NULL; + + if (SEGMENT.call == 0) + { + if (!initParticleSystem(PartSys, 1, 0, true, true)) //init, request one source, no additional bytes, advanced size & size control (actually dont really need one TODO: test if using zero sources also works) + return mode_static(); // allocation failed; //allocation failed + //PartSys->setGravity(); //enable with default gravity + PartSys->setBounceX(true); + PartSys->setBounceY(true); + PartSys->setWallHardness(255); + PartSys->setWallRoughness(255); + PartSys->setCollisionHardness(255); + //PartSys->setParticleSize(0); //set global size to zero or motion blur cannot be used (is zero by default) + } + else + PartSys = reinterpret_cast(SEGMENT.data); // if not first call, just set the pointer to the PS + + if (PartSys == NULL) + { + DEBUG_PRINT(F("ERROR: FX PartSys nullpointer")); + return mode_static(); // something went wrong, no data! + } + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setUsedParticles(min(PartSys->numParticles, (uint16_t)map(SEGMENT.intensity,0 ,255, 1, (PartSys->maxXpixel * PartSys->maxYpixel)>>4))); + PartSys->enableParticleCollisions(SEGMENT.check2); + + for (uint32_t i = 0; i < PartSys->usedParticles; i++) // update particles + { + if(SEGMENT.aux0 != SEGMENT.speed || PartSys->particles[i].ttl == 0) //speed changed or dead + { + PartSys->particles[i].vx = (int16_t)random(-(SEGMENT.speed >> 2), SEGMENT.speed >> 2); + PartSys->particles[i].vy = (int16_t)random(-(SEGMENT.speed >> 2), SEGMENT.speed >> 2); + } + if(SEGMENT.aux1 != SEGMENT.custom1 || PartSys->particles[i].ttl == 0) //size changed or dead + PartSys->advPartSize[i].maxsize = 60 + (SEGMENT.custom1 >> 1) + random((SEGMENT.custom1 >> 2)); //set each particle to slightly randomized size + + //PartSys->particles[i].perpetual = SEGMENT.check2; //infinite life if set + if (PartSys->particles[i].ttl == 0) // find dead particle, renitialize + { + PartSys->particles[i].ttl = 300 + random16(((uint16_t)SEGMENT.custom2 << 3) + 100); + PartSys->particles[i].x = random(PartSys->maxX); + PartSys->particles[i].y = random16(PartSys->maxY); + PartSys->particles[i].hue = random16(); // set random color + PartSys->particles[i].collide = true; //enable collision for particle + PartSys->advPartProps[i].size = 0; //start out small + PartSys->advPartSize[i].asymmetry = random16(220); + PartSys->advPartSize[i].asymdir = random16(255); + //set advanced size control properties + PartSys->advPartSize[i].grow = true; + PartSys->advPartSize[i].growspeed = 1 + random16(9); + PartSys->advPartSize[i].shrinkspeed = 1 + random16(9); + PartSys->advPartSize[i].wobblespeed = 1 + random(3); + } + //PartSys->advPartSize[i].asymmetry++; + PartSys->advPartSize[i].pulsate = SEGMENT.check3; + PartSys->advPartSize[i].wobble = SEGMENT.check1; + } + SEGMENT.aux0 = SEGMENT.speed; //write state back + SEGMENT.aux1 = SEGMENT.custom1; + + PartSys->setMotionBlur(((SEGMENT.custom3) << 3) + 7); + PartSys->update(); // update and render + + return FRAMETIME; +} +static const char _data_FX_MODE_PARTICLEBLOBS[] PROGMEM = "PS Blobs@Speed,Blobs,Size,Life,Blur,Wobble,Collide,Pulsate;;!;2;sx=30,ix=64,c1=200,c2=130,c3=0,o1=0,o2=0,o3=1"; + + /* * Particle rotating GEQ * Particles sprayed from center with a rotating spray @@ -9893,7 +9883,8 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_PARTICLEATTRACTOR, &mode_particleattractor, _data_FX_MODE_PARTICLEATTRACTOR); addEffect(FX_MODE_PARTICLESPRAY, &mode_particlespray, _data_FX_MODE_PARTICLESPRAY); addEffect(FX_MODE_PARTICLESGEQ, &mode_particleGEQ, _data_FX_MODE_PARTICLEGEQ); - addEffect(FX_MODE_PARTICLEGHOSTRIDER, &mode_particlghostrider, _data_FX_MODE_PARTICLEGHOSTRIDER); + addEffect(FX_MODE_PARTICLEGHOSTRIDER, &mode_particleghostrider, _data_FX_MODE_PARTICLEGHOSTRIDER); + addEffect(FX_MODE_PARTICLEBLOBS, &mode_particleblobs, _data_FX_MODE_PARTICLEBLOBS); // addEffect(FX_MODE_PARTICLECENTERGEQ, &mode_particlecenterGEQ, _data_FX_MODE_PARTICLECCIRCULARGEQ); diff --git a/wled00/FX.h b/wled00/FX.h index 03c15f6825..3d98cb2c27 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -327,7 +327,8 @@ #define FX_MODE_PARTICLESGEQ 198 #define FX_MODE_PARTICLECENTERGEQ 199 #define FX_MODE_PARTICLEGHOSTRIDER 200 -#define MODE_COUNT 201 +#define FX_MODE_PARTICLEBLOBS 201 +#define MODE_COUNT 202 typedef enum mapping1D2D { M12_Pixels = 0, diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index 3a0eb31252..1e1a41129f 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -43,14 +43,15 @@ #include "FastLED.h" #include "FX.h" -ParticleSystem::ParticleSystem(uint16_t width, uint16_t height, uint16_t numberofparticles, uint16_t numberofsources, bool isadvanced) +ParticleSystem::ParticleSystem(uint16_t width, uint16_t height, uint16_t numberofparticles, uint16_t numberofsources, bool isadvanced, bool sizecontrol) { //Serial.println("PS Constructor"); numSources = numberofsources; numParticles = numberofparticles; // set number of particles in the array usedParticles = numberofparticles; // use all particles by default - //particlesettings = {false, false, false, false, false, false, false, false}; // all settings off by default - updatePSpointers(isadvanced); // set the particle and sources pointer (call this before accessing sprays or particles) + advPartProps = NULL; //make sure we start out with null pointers (just in case memory was not cleared) + advPartSize = NULL; + updatePSpointers(isadvanced, sizecontrol); // set the particle and sources pointer (call this before accessing sprays or particles) setMatrixSize(width, height); setWallHardness(255); // set default wall hardness to max setWallRoughness(0); // smooth walls by default @@ -88,7 +89,7 @@ ParticleSystem::ParticleSystem(uint16_t width, uint16_t height, uint16_t numbero Serial.println(particles[i].y); } }*/ - // Serial.println("PS Constructor done"); + Serial.println("PS Constructor done"); } //update function applies gravity, moves the particles, handles collisions and renders the particles @@ -98,6 +99,15 @@ void ParticleSystem::update(void) //apply gravity globally if enabled if (particlesettings.useGravity) applyGravity(); + + //update size settings before handling collisions + if(advPartSize) + { + for (int i = 0; i < usedParticles; i++) + { + updateSize(&advPartProps[i], &advPartSize[i]); + } + } // 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) if (particlesettings.useCollisions) @@ -108,11 +118,13 @@ void ParticleSystem::update(void) { if(advPartProps) { - advprop = &advPartProps[i]; + advprop = &advPartProps[i]; } - particleMoveUpdate(particles[i], particlesettings, advprop); + particleMoveUpdate(particles[i], &particlesettings, advprop); } - /*!!! remove this + + + /*TODO remove this Serial.print("alive particles: "); uint32_t aliveparticles = 0; for (int i = 0; i < numParticles; i++) @@ -158,7 +170,7 @@ void ParticleSystem::setMatrixSize(uint16_t x, uint16_t y) maxXpixel = x - 1; // last physical pixel that can be drawn to maxYpixel = y - 1; maxX = x * PS_P_RADIUS - 1; // particle system boundary for movements - maxY = y * PS_P_RADIUS - 1; // this value is often needed by FX to calculate positions + maxY = y * PS_P_RADIUS - 1; // this value is often needed (also by FX) to calculate positions } void ParticleSystem::setWrapX(bool enable) @@ -193,7 +205,7 @@ void ParticleSystem::setColorByAge(bool enable) void ParticleSystem::setMotionBlur(uint8_t bluramount) { - if(particlesize == 0) //only allwo motion blurring on default particle size + if(particlesize == 0) //only allwo motion blurring on default particle size or advanced size(cannot combine motion blur with normal blurring used for particlesize, would require another buffer) motionBlur = bluramount; } @@ -236,9 +248,9 @@ void ParticleSystem::sprayEmit(PSsource &emitter) { particles[emitIndex].x = emitter.source.x; // + random16(emitter.var) - (emitter.var >> 1); //randomness uses cpu cycles and is almost invisible, removed for now. particles[emitIndex].y = emitter.source.y; // + random16(emitter.var) - (emitter.var >> 1); - particles[emitIndex].vx = emitter.vx + random(emitter.var) - (emitter.var>>1); + particles[emitIndex].vx = emitter.vx + random(emitter.var) - (emitter.var>>1); //TODO: could use random(min,max) but need to adjust all FX as it would double the var amount particles[emitIndex].vy = emitter.vy + random(emitter.var) - (emitter.var>>1); - particles[emitIndex].ttl = random16(emitter.maxLife - emitter.minLife) + emitter.minLife; + particles[emitIndex].ttl = random(emitter.minLife, emitter.maxLife); particles[emitIndex].hue = emitter.source.hue; particles[emitIndex].sat = emitter.source.sat; particles[emitIndex].collide = emitter.source.collide; @@ -300,13 +312,14 @@ void ParticleSystem::angleEmit(PSsource &emitter, uint16_t angle, int8_t speed) // particle moves, decays and dies, if killoutofbounds is set, out of bounds particles are set to ttl=0 // 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 -void ParticleSystem::particleMoveUpdate(PSparticle &part, PSsettings &options, PSadvancedParticle *advancedproperties) +void ParticleSystem::particleMoveUpdate(PSparticle &part, PSsettings *options, PSadvancedParticle *advancedproperties) { - + if(options == NULL) + options = &particlesettings; //use PS system settings by default if (part.ttl > 0) { - if(!part.perpetural) + if(!part.perpetual) part.ttl--; // age if (particlesettings.colorByAge) part.hue = part.ttl > 255 ? 255 : part.ttl; //set color to ttl @@ -323,7 +336,7 @@ void ParticleSystem::particleMoveUpdate(PSparticle &part, PSsettings &options, P particleHardRadius = max(PS_P_MINHARDRADIUS, (int)particlesize + (advancedproperties->size)); } //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 - if (options.bounceX) + if (options->bounceX) { if ((newX < particleHardRadius) || (newX > maxX - particleHardRadius)) // reached a wall bounce(part.vx, part.vy, newX, maxX); @@ -331,7 +344,7 @@ void ParticleSystem::particleMoveUpdate(PSparticle &part, PSsettings &options, P if ((newX < 0) || (newX > maxX)) // check if particle reached an edge (note: this also checks out of bounds and must not be skipped, even if bounce is enabled) { - if (options.wrapX) + if (options->wrapX) { newX = (uint16_t)newX % (maxX + 1); } @@ -347,13 +360,13 @@ void ParticleSystem::particleMoveUpdate(PSparticle &part, PSsettings &options, P if(isleaving) { part.outofbounds = 1; - if (options.killoutofbounds) + if (options->killoutofbounds) part.ttl = 0; } } } - if (options.bounceY) + if (options->bounceY) { if ((newY < particleHardRadius) || (newY > maxY - particleHardRadius)) // reached floor / ceiling { @@ -363,13 +376,13 @@ void ParticleSystem::particleMoveUpdate(PSparticle &part, PSsettings &options, P { /* //TODO: is this check really needed? is checked below. on quick tests, it crashed (but not in all animations... -> seems ok. leave it for now, need to check this later - if (options.useGravity) // do not bounce on top if using gravity (open container) if this is needed implement it in the FX + if (options->useGravity) // do not bounce on top if using gravity (open container) if this is needed implement it in the FX { if (newY > maxY + PS_P_HALFRADIUS) part.outofbounds = 1; // set out of bounds, kill out of bounds over the top does not apply if gravity is used (user can implement it in FX if needed) } else*/ - if(!options.useGravity) + if(!options->useGravity) { bounce(part.vy, part.vx, newY, maxY); } @@ -379,7 +392,7 @@ void ParticleSystem::particleMoveUpdate(PSparticle &part, PSsettings &options, P if (((newY < 0) || (newY > maxY))) // check if particle reached an edge (makes sure particles are within frame for rendering) { - if (options.wrapY) + if (options->wrapY) { newY = (uint16_t)newY % (maxY + 1); } @@ -394,11 +407,11 @@ void ParticleSystem::particleMoveUpdate(PSparticle &part, PSsettings &options, P if(isleaving) { part.outofbounds = 1; - if (options.killoutofbounds) + if (options->killoutofbounds) { if (newY < 0) // if gravity is enabled, only kill particles below ground part.ttl = 0; - else if (!options.useGravity) + else if (!options->useGravity) part.ttl = 0; } } @@ -409,24 +422,109 @@ void ParticleSystem::particleMoveUpdate(PSparticle &part, PSsettings &options, P } } +// update advanced particle size control TODO: are the counters needed? it may be good enough like this... +void ParticleSystem::updateSize(PSadvancedParticle *advprops, PSsizeControl *advsize) +{ + if(advsize == NULL) //just a safety check + return; + + //grow/shrink particle + int32_t newsize = advprops->size; + uint32_t counter = advsize->sizecounter; + uint32_t increment; + //calculate grow speed using 0-8 for low speeds and 9-15 for higher speeds + if(advsize->grow) increment = advsize->growspeed; + else if(advsize->shrink) increment = advsize->shrinkspeed; + if(increment < 9) //8 means +1 every frame + { + counter += increment; + if(counter > 7) + { + counter -= 8; + increment = 1; + } + else + increment = 0; + advsize->sizecounter = counter; + } + else{ + increment = (increment - 8) << 1; //9 means +2, 10 means +4 etc. 15 means +14 + } + if(advsize->grow) + { + if(newsize < advsize->maxsize) + { + newsize += increment; + if(newsize >= advsize->maxsize) + { + advsize->grow = false; //stop growing, shrink from now on if enabled + newsize = advsize->maxsize; //limit + if(advsize->pulsate) advsize->shrink = true; + } + } + } + else if(advsize->shrink) + { + if(newsize > advsize->minsize) + { + newsize -= increment; + if(newsize <= advsize->minsize) + { + //if(advsize->minsize == 0) part.ttl = 0; //TODO: need to pass particle or return kill instruction + advsize->shrink = false; //disable shrinking + newsize = advsize->minsize; //limit + if(advsize->pulsate) advsize->grow = true; + } + } + } + advprops->size = newsize; + //handle wobbling + if(advsize->wobble) + { + advsize->asymdir += advsize->wobblespeed; //todo: need better wobblespeed control? + } +} + +// calculate x and y size for asymmetrical particles (advanced size control) +void ParticleSystem::getParticleXYsize(PSadvancedParticle *advprops, PSsizeControl *advsize, uint32_t &xsize, uint32_t &ysize) +{ + //advsize->asymdir = 50; //!!! + if(advsize == NULL) //if advanced size is valid, also advced properties pointer is valid (handled by pointer assignment function) + return; + int32_t deviation = ((uint32_t)advprops->size * (uint32_t)advsize->asymmetry) / 255; //deviation from symmetrical size + // Calculate x and y size based on deviation and direction (0 is symmetrical, 64 is x, 128 is symmetrical, 192 is y) + if (advsize->asymdir < 64) { + deviation = ((int32_t)advsize->asymdir * deviation) / 64; + } else if (advsize->asymdir < 192) { + deviation = ((128 - (int32_t)advsize->asymdir) * deviation) / 64; + } else { + deviation = (((int32_t)advsize->asymdir - 255) * deviation) / 64; + } + // Calculate x and y size based on deviation, limit to 255 (rendering function cannot handle lareger sizes) + xsize = ((int32_t)advprops->size - deviation) > 255 ? 255 : advprops->size - deviation; + ysize = ((int32_t)advprops->size + deviation) > 255 ? 255 : advprops->size + deviation; +} + //function to bounce a particle from a wall using set parameters (wallHardness and wallRoughness) void ParticleSystem::bounce(int8_t &incomingspeed, int8_t ¶llelspeed, int32_t &position, uint16_t maxposition) { - incomingspeed = -incomingspeed; // invert speed - incomingspeed = (incomingspeed * wallHardness) / 255; // reduce speed as energy is lost on non-hard surface - if (position < particleHardRadius) - position = particleHardRadius; // fast particles will never reach the edge if position is inverted - else - position = maxposition - particleHardRadius; - if(wallRoughness) - { - //transfer an amount of incomingspeed speed to parallel speed - int32_t donatespeed = abs(incomingspeed); - donatespeed = ((random(donatespeed << 1) - donatespeed) * wallRoughness) / 255; //take random portion of + or - x speed, scaled by roughness - parallelspeed += donatespeed; - donatespeed = abs(donatespeed); - incomingspeed -= incomingspeed > 0 ? donatespeed : -donatespeed; - } + incomingspeed = -incomingspeed; // invert speed + incomingspeed = (incomingspeed * wallHardness) / 255; // reduce speed as energy is lost on non-hard surface + if (position < particleHardRadius) + position = particleHardRadius; // fast particles will never reach the edge if position is inverted + else + position = maxposition - particleHardRadius; + if(wallRoughness) + { + int32_t totalspeed = abs(incomingspeed) + abs(parallelspeed); + //transfer an amount of incomingspeed speed to parallel speed + int32_t donatespeed = abs(incomingspeed); + donatespeed = (random(-donatespeed, donatespeed) * wallRoughness) / 255; //take random portion of + or - perpendicular speed, scaled by roughness + parallelspeed = limitSpeed((int32_t)parallelspeed + donatespeed); + incomingspeed = limitSpeed((int32_t)incomingspeed - donatespeed); + donatespeed = totalspeed - abs(parallelspeed); //keep total speed the same + incomingspeed = incomingspeed > 0 ? donatespeed : -donatespeed; + } } @@ -461,12 +559,13 @@ void ParticleSystem::applyForce(uint16_t particleindex, int8_t xforce, int8_t yf } // apply a force in x,y direction to all particles +// force is in 3.4 fixed point notation (see above) void ParticleSystem::applyForce(int8_t xforce, int8_t yforce) { // for small forces, need to use a delay counter uint8_t tempcounter; - //note: this is not the most compuatationally effeicient way to do this, but it saves on duplacte code and is fast enough + //note: this is not the most compuatationally efficient way to do this, but it saves on duplacte code and is fast enough for (uint i = 0; i < usedParticles; i++) { tempcounter = forcecounter; @@ -533,7 +632,8 @@ void ParticleSystem::applyGravity(PSparticle *part) } // slow down particle by friction, the higher the speed, the higher the friction. a high friction coefficient slows them more (255 means instant stop) -void ParticleSystem::applyFriction(PSparticle *part, uint8_t coefficient) +// 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 +void ParticleSystem::applyFriction(PSparticle *part, int32_t coefficient) { int32_t friction = 255 - coefficient; // note: not checking if particle is dead can be done by caller (or can be omitted) @@ -543,7 +643,7 @@ void ParticleSystem::applyFriction(PSparticle *part, uint8_t coefficient) } // apply friction to all particles -void ParticleSystem::applyFriction(uint8_t coefficient) +void ParticleSystem::applyFriction(int32_t coefficient) { for (uint32_t i = 0; i < usedParticles; i++) { @@ -656,17 +756,18 @@ void ParticleSystem::ParticleSys_render(bool firemode, uint32_t fireintensity) if (useLocalBuffer) { - cli(); //no interrupts so we get one block of memory for both buffers, less fragmentation + /* Serial.print("heap: "); Serial.print(heap_caps_get_free_size(MALLOC_CAP_8BIT)); Serial.print(" block: "); Serial.println(heap_caps_get_largest_free_block(MALLOC_CAP_8BIT));*/ + //cli(); //no interrupts so we get one block of memory for both buffers, less fragmentation -> leads to flickering on S3. so not a good idea. // allocate empty memory for the local renderbuffer framebuffer = allocate2Dbuffer(maxXpixel + 1, maxYpixel + 1); if (framebuffer == NULL) { - sei(); //re enable interrupts + //sei(); //re enable interrupts Serial.println("Frame buffer alloc failed"); useLocalBuffer = false; //render to segment pixels directly if not enough memory } @@ -675,7 +776,7 @@ void ParticleSystem::ParticleSys_render(bool firemode, uint32_t fireintensity) { renderbuffer = allocate2Dbuffer(10, 10); //buffer to render individual particles to if size > 0 note: null checking is done when accessing it } - sei(); //re enable interrupts + //sei(); //re enable interrupts if (motionBlur > 0) // using SEGMENT.fadeToBlackBy is much slower, this approximately doubles the speed of fade calculation { uint32_t yflipped; @@ -736,28 +837,21 @@ void ParticleSystem::ParticleSys_render(bool firemode, uint32_t fireintensity) if(particlesize > 0) { - if (useLocalBuffer) + uint32_t passes = particlesize/64 + 1; //number of blur passes + uint32_t bluramount = particlesize; //number of blur passes + uint32_t bitshift = 0; + + for(int i = 0; i < passes; i++) //run four passes max { - //uint32_t firstblur = particlesize > 64 ? 64 : particlesize; //attempt to add motion blurring, but does not work... - blur2D(framebuffer, maxYpixel + 1, maxYpixel + 1, particlesize, particlesize); - if (particlesize > 64) - blur2D(framebuffer, maxYpixel + 1, maxYpixel + 1, particlesize - 64, particlesize - 64); - if (particlesize > 128) - blur2D(framebuffer, maxYpixel + 1, maxYpixel + 1, (particlesize - 128) << 1, (particlesize - 128) << 1); - if (particlesize > 192) - blur2D(framebuffer, maxYpixel + 1, maxYpixel + 1, (particlesize - 192) << 1, (particlesize - 192) << 1); - } - else - { - SEGMENT.blur(particlesize, true); - if (particlesize > 64) - SEGMENT.blur(particlesize - 64, true); - if (particlesize > 128) - SEGMENT.blur((particlesize - 128) << 1, true); - if (particlesize > 192) - SEGMENT.blur((particlesize - 192) << 1, true); - } - + if(i == 2) //for the last two passes, use higher amount of blur (results in a nicer brightness gradient with soft edges) + bitshift = 1; + + if (useLocalBuffer) + blur2D(framebuffer, maxXpixel + 1, maxYpixel + 1, bluramount << bitshift, bluramount << bitshift); + else + SEGMENT.blur(bluramount << bitshift, true); + bluramount -= 64; + } } if (useLocalBuffer) //transfer local buffer back to segment @@ -774,7 +868,7 @@ void ParticleSystem::ParticleSys_render(bool firemode, uint32_t fireintensity) free(framebuffer); // free buffer memory } if(renderbuffer) - free(renderbuffer); // free buffer memory + free(renderbuffer); // free buffer memory } // calculate pixel positions and brightness distribution and render the particle to local buffer or global buffer @@ -889,35 +983,38 @@ void ParticleSystem::renderParticle(CRGB **framebuffer, uint32_t particleindex, fast_color_add(renderbuffer[5][4], color, pxlbrightness[1]); fast_color_add(renderbuffer[5][5], color, pxlbrightness[2]); fast_color_add(renderbuffer[4][5], color, pxlbrightness[3]); //TODO: make this a loop somehow? needs better coordinate handling... - uint32_t rendersize = 4; - uint32_t offset = 3; //offset to zero coordinate to write/read data in renderbuffer - //TODO: add asymmetrical size support - blur2D(renderbuffer, rendersize, rendersize, advPartProps[particleindex].size, advPartProps[particleindex].size, true, offset, offset, true); //blur to 4x4 - if (advPartProps[particleindex].size > 64) + uint32_t rendersize = 2; //initialize render size, minimum is 4x4 pixels, it is incremented int he loop below to start with 4 + 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) + + uint32_t maxsize = advPartProps[particleindex].size; + uint32_t xsize = maxsize; + uint32_t ysize = maxsize; + if(advPartSize) //use advanced size control { - rendersize += 2; - offset--; - blur2D(renderbuffer, rendersize, rendersize, advPartProps[particleindex].size - 64, advPartProps[particleindex].size - 64, true, offset, offset, true); //blur to 6x6 + if(advPartSize[particleindex].asymmetry > 0) + getParticleXYsize(&advPartProps[particleindex], &advPartSize[particleindex], xsize, ysize); + maxsize = xsize; + if(ysize > maxsize) maxsize = ysize; //maxsize is now the bigger of the two } - if (advPartProps[particleindex].size > 128) + maxsize = maxsize/64 + 1; //number of blur passes depends on maxsize + uint32_t bitshift = 0; + for(int i = 0; i < maxsize; i++) //run four passes max { + if(i == 2) //for the last two passes, use higher amount of blur (results in a nicer brightness gradient with soft edges) + bitshift = 1; rendersize += 2; - offset--; - blur2D(renderbuffer, rendersize, rendersize, (advPartProps[particleindex].size - 128) << 1, (advPartProps[particleindex].size - 128) << 1, true, offset, offset, true); //blur to 8x8 - } - if (advPartProps[particleindex].size > 192) - { - rendersize += 2; - offset--; - blur2D(renderbuffer, rendersize, rendersize, (advPartProps[particleindex].size - 192) << 1, (advPartProps[particleindex].size - 192) << 1, true, offset, offset, true); //blur to 10x10 - } + offset--; + blur2D(renderbuffer, rendersize, rendersize, xsize << bitshift, ysize << bitshift, true, offset, offset, true); //blur to 4x4 + xsize = xsize > 64 ? xsize - 64 : 0; + ysize = ysize > 64 ? ysize - 64 : 0; + } //calculate origin coordinates to render the particle to in the framebuffer uint32_t xfb_orig = x - (rendersize>>1) + 1 - offset; uint32_t yfb_orig = y - (rendersize>>1) + 1 - offset; uint32_t xfb, yfb; //coordinates in frame buffer to write to note: by making this uint, only overflow has to be checked - //transfer renderbuffer to framebuffer + //transfer particle renderbuffer to framebuffer for(uint32_t xrb = offset; xrb < rendersize+offset; xrb++) { xfb = xfb_orig + xrb; @@ -945,7 +1042,7 @@ void ParticleSystem::renderParticle(CRGB **framebuffer, uint32_t particleindex, } } } - else + else //standard rendering { if (framebuffer) { @@ -965,45 +1062,6 @@ void ParticleSystem::renderParticle(CRGB **framebuffer, uint32_t particleindex, } } - //TODO: for advance pixels, render them to larger size in a local buffer. or better make a new function for that? - //easiest would be to create a 2x2 buffer for the original values, but that may not be as fast for smaller pixels... - //just make a new function that these colors are rendered to and then blurr it so it stays fast for normal rendering. - -/* - //uint32_t firstblur = particlesize > 64 ? 64 : particlesize; //attempt to add motion blurring, but does not work... - blur2D(framebuffer, maxYpixel + 1, maxYpixel + 1, particlesize, particlesize); //4x4 - if (particlesize > 64) - blur2D(framebuffer, maxYpixel + 1, maxYpixel + 1, particlesize - 64, particlesize - 64); //6x6 - if (particlesize > 128) - blur2D(framebuffer, maxYpixel + 1, maxYpixel + 1, (particlesize - 128) << 1, (particlesize - 128) << 1); 8x8 - if (particlesize > 192) - blur2D(framebuffer, maxYpixel + 1, maxYpixel + 1, (particlesize - 192) << 1, (particlesize - 192) << 1); 10x10 - */ - - -/* - Serial.print("x:"); - Serial.print(particle->x); - Serial.print(" y:"); - Serial.print(particle->y); - //Serial.print(" xo"); - //Serial.print(xoffset); - //Serial.print(" dx"); - //Serial.print(dx); - //Serial.print(" "); - for(uint8_t t = 0; t<4; t++) - { - Serial.print(" v"); - Serial.print(pxlbrightness[t]); - Serial.print(" x"); - Serial.print(pixco[t][0]); - Serial.print(" y"); - Serial.print(pixco[t][1]); - - Serial.print(" "); - } - Serial.println(" "); -*/ /* // debug: check coordinates if out of buffer boundaries print out some info (rendering out of bounds particle causes crash!) @@ -1401,13 +1459,13 @@ void ParticleSystem::updateSystem(void) uint32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; uint32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); setMatrixSize(cols, rows); - updatePSpointers(advPartProps != NULL); + updatePSpointers(advPartProps != NULL, advPartSize != NULL); } // 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) // 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) // FX handles the PSsources, need to tell this function how many there are -void ParticleSystem::updatePSpointers(bool isadvanced) +void ParticleSystem::updatePSpointers(bool isadvanced, bool sizecontrol) { //DEBUG_PRINT(F("*** PS pointers ***")); //DEBUG_PRINTF_P(PSTR("this PS %p "), this); @@ -1416,26 +1474,32 @@ void ParticleSystem::updatePSpointers(bool isadvanced) //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. //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. particles = reinterpret_cast(this + 1); // pointer to particle array at data+sizeof(ParticleSystem) + sources = reinterpret_cast(particles + numParticles); // pointer to source(s) if(isadvanced) { - advPartProps = reinterpret_cast(particles + numParticles); - sources = reinterpret_cast(advPartProps + numParticles); // pointer to source(s) + advPartProps = reinterpret_cast(sources + numSources); + PSdataEnd = reinterpret_cast(advPartProps + numParticles); + if(sizecontrol) + { + advPartSize = reinterpret_cast(advPartProps + numParticles); + PSdataEnd = reinterpret_cast(advPartSize + numParticles); + } } else { - advPartProps = NULL; - sources = reinterpret_cast(particles + numParticles); // pointer to source(s) - } - PSdataEnd = reinterpret_cast(sources + numSources); // pointer to first available byte after the PS for FX additional data - - //DEBUG_PRINTF_P(PSTR(" particles %p "), particles); - //DEBUG_PRINTF_P(PSTR(" sources %p "), sources); - //DEBUG_PRINTF_P(PSTR(" adv. props %p\n"), advPartProps); - //DEBUG_PRINTF_P(PSTR("end %p\n"), PSdataEnd); + PSdataEnd = reinterpret_cast(sources + numSources); // pointer to first available byte after the PS for FX additional data + } + /* + DEBUG_PRINTF_P(PSTR(" particles %p "), particles); + DEBUG_PRINTF_P(PSTR(" sources %p "), sources); + DEBUG_PRINTF_P(PSTR(" adv. props %p "), advPartProps); + DEBUG_PRINTF_P(PSTR(" adv. ctrl %p "), advPartSize); + DEBUG_PRINTF_P(PSTR("end %p\n"), PSdataEnd); + */ } //non class functions to use for initialization -uint32_t calculateNumberOfParticles(bool isadvanced) +uint32_t calculateNumberOfParticles(bool isadvanced, bool sizecontrol) { uint32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; uint32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); @@ -1452,7 +1516,10 @@ uint32_t calculateNumberOfParticles(bool isadvanced) numberofParticles = max((uint32_t)1, min(numberofParticles, particlelimit)); if (isadvanced) // advanced property array needs ram, reduce number of particles to use the same amount numberofParticles = (numberofParticles * sizeof(PSparticle)) / (sizeof(PSparticle) + sizeof(PSadvancedParticle)); - //make sure it is a multiple of 4 for proper memory alignment + if (sizecontrol) // advanced property array needs ram, reduce number of particles to use the same amount + numberofParticles /= 8; // if size control is used, much fewer particles are needed + + //make sure it is a multiple of 4 for proper memory alignment (easier than using padding bytes) numberofParticles = ((numberofParticles+3) >> 2) << 2; return numberofParticles; } @@ -1477,13 +1544,15 @@ uint32_t calculateNumberOfSources(uint8_t requestedsources) } //allocate memory for particle system class, particles, sprays plus additional memory requested by FX -bool allocateParticleSystemMemory(uint16_t numparticles, uint16_t numsources, bool isadvanced, uint16_t additionalbytes) +bool allocateParticleSystemMemory(uint16_t numparticles, uint16_t numsources, bool isadvanced, bool sizecontrol, uint16_t additionalbytes) { uint32_t requiredmemory = sizeof(ParticleSystem); //functions above make sure these are a multiple of 4 bytes (to avoid alignment issues) requiredmemory += sizeof(PSparticle) * numparticles; if (isadvanced) requiredmemory += sizeof(PSadvancedParticle) * numparticles; + if (sizecontrol) + requiredmemory += sizeof(PSsizeControl) * numparticles; requiredmemory += sizeof(PSsource) * numsources; requiredmemory += additionalbytes; //Serial.print("allocating: "); @@ -1495,14 +1564,14 @@ bool allocateParticleSystemMemory(uint16_t numparticles, uint16_t numsources, bo } // initialize Particle System, allocate additional bytes if needed (pointer to those bytes can be read from particle system class: PSdataEnd) -bool initParticleSystem(ParticleSystem *&PartSys, uint8_t requestedsources, bool isadvanced, uint16_t additionalbytes) +bool initParticleSystem(ParticleSystem *&PartSys, uint8_t requestedsources, uint16_t additionalbytes, bool largesizes, bool sizecontrol) { //Serial.println("PS init function"); - uint32_t numparticles = calculateNumberOfParticles(isadvanced); + uint32_t numparticles = calculateNumberOfParticles(largesizes, sizecontrol); uint32_t numsources = calculateNumberOfSources(requestedsources); //Serial.print("numsources: "); //Serial.println(numsources); - if (!allocateParticleSystemMemory(numparticles, numsources, isadvanced, additionalbytes)) + if (!allocateParticleSystemMemory(numparticles, numsources, largesizes, sizecontrol, additionalbytes)) { DEBUG_PRINT(F("PS init failed: memory depleted")); return false; @@ -1512,7 +1581,7 @@ bool initParticleSystem(ParticleSystem *&PartSys, uint8_t requestedsources, bool uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); //Serial.println("calling constructor"); - PartSys = new (SEGMENT.data) ParticleSystem(cols, rows, numparticles, numsources, isadvanced); // particle system constructor TODO: why does VS studio thinkt this is bad? + PartSys = new (SEGMENT.data) ParticleSystem(cols, rows, numparticles, numsources, largesizes, sizecontrol); // particle system constructor TODO: why does VS studio thinkt this is bad? //Serial.print("PS pointer at "); //Serial.println((uintptr_t)PartSys); return true; @@ -1619,7 +1688,7 @@ void blur2D(CRGB **colorbuffer, uint32_t xsize, uint32_t ysize, uint32_t xblur, seeppart = colorbuffer[x][y]; //create copy of current color fast_color_scale(seeppart, seep); //scale it and seep to neighbours if(!smear) //fade current pixel if smear is disabled - fast_color_scale(colorbuffer[x][y], 255 - xblur); + fast_color_scale(colorbuffer[x][y], 255 - yblur); if(y > 0) { diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index 7413dccedc..ecf9c262a5 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -58,7 +58,7 @@ typedef struct { uint16_t ttl : 12; // time to live, 12 bit or 4095 max (which is 50s at 80FPS) bool outofbounds : 1; //out of bounds flag, set to true if particle is outside of display area bool collide : 1; //if set, particle takes part in collisions - bool perpetural : 1; //if set, particle does not age (TTL is not decremented in move function, it still dies from killoutofbounds) + bool perpetual : 1; //if set, particle does not age (TTL is not decremented in move function, it still dies from killoutofbounds) bool flag4 : 1; // unused flag } PSparticle; @@ -67,29 +67,24 @@ typedef struct { uint8_t size; //particle size, 255 means 10 pixels in diameter uint8_t forcecounter; //counter for applying forces to individual particles - - //bool flag1 : 1; // unused flags... for now. - //bool flag2 : 1; - //bool flag3 : 1; - //bool flag4 : 1; } PSadvancedParticle; -// struct for advanced particle size control (optional) TODO: this is currently just an idea, may not make it into final code if too slow / complex +// struct for advanced particle size control (optional) typedef struct { - uint8_t sizeasymmetry; // asymmetrical size TODO: need something better to define this? - uint8_t targetsize; // target size for growing / shrinking + uint8_t asymmetry; // asymmetrical size (0=symmetrical, 255 fully asymmetric) + uint8_t asymdir; // direction of asymmetry, 64 is x, 192 is y (0 and 128 is symmetrical) + uint8_t maxsize; // target size for growing + uint8_t minsize; // target size for shrinking + uint8_t sizecounter : 4; // counters used for size contol (grow/shrink/wobble) + uint8_t wobblecounter : 4; uint8_t growspeed : 4; - uint8_t shrinkspeed : 4; - uint8_t sizecounter; // counter that can be used for size contol TODO: need more than one? - //ideas: - //wobbleamount, rotation angle for asymmetic particles - //a flag 'usegravity' that can be set to false for selective gravity application - + uint8_t shrinkspeed : 4; + uint8_t wobblespeed : 4; bool grow : 1; // flags bool shrink : 1; - bool wobble : 1; - bool flag4 : 1; + bool pulsate : 1; //grows & shrinks & grows & ... + bool wobble : 1; //alternate x and y size } PSsizeControl; @@ -125,7 +120,7 @@ typedef union class ParticleSystem { public: - ParticleSystem(uint16_t width, uint16_t height, uint16_t numberofparticles, uint16_t numberofsources, bool isadvanced = false); // constructor + ParticleSystem(uint16_t width, uint16_t height, uint16_t numberofparticles, uint16_t numberofsources, bool isadvanced = false, bool sizecontrol = false); // constructor // note: memory is allcated in the FX function, no deconstructor needed void update(void); //update the particles according to set options and render to the matrix void updateFire(uint32_t intensity, bool renderonly = false); // update function for fire, if renderonly is set, particles are not updated (required to fix transitions with frameskips) @@ -137,7 +132,7 @@ class ParticleSystem void angleEmit(PSsource& emitter, uint16_t angle, int8_t speed); // move functions - void particleMoveUpdate(PSparticle &part, PSsettings &options, PSadvancedParticle *advancedproperties = NULL); + void particleMoveUpdate(PSparticle &part, PSsettings *options = NULL, PSadvancedParticle *advancedproperties = NULL); //particle physics void applyGravity(PSparticle *part); // applies gravity to single particle (use this for sources) void applyForce(PSparticle *part, int8_t xforce, int8_t yforce, uint8_t *counter); @@ -146,8 +141,8 @@ class ParticleSystem void applyAngleForce(PSparticle *part, int8_t force, uint16_t angle, uint8_t *counter); void applyAngleForce(uint16_t particleindex, int8_t force, uint16_t angle); // use this for advanced property particles void applyAngleForce(int8_t force, uint16_t angle); //apply angular force to all particles - void applyFriction(PSparticle *part, uint8_t coefficient); // apply friction to specific particle - void applyFriction(uint8_t coefficient); // apply friction to all used particles + void applyFriction(PSparticle *part, int32_t coefficient); // apply friction to specific particle + void applyFriction(int32_t coefficient); // apply friction to all used particles void pointAttractor(uint16_t particleindex, PSparticle *attractor, uint8_t strength, bool swallow); void lineAttractor(uint16_t particleindex, PSparticle *attractorcenter, uint16_t attractorangle, uint8_t strength); @@ -164,7 +159,7 @@ class ParticleSystem void setKillOutOfBounds(bool enable); //if enabled, particles outside of matrix instantly die void setSaturation(uint8_t sat); //set global color saturation void setColorByAge(bool enable); - void setMotionBlur(uint8_t bluramount); + void setMotionBlur(uint8_t bluramount); //note: motion blur can only be used if 'particlesize' is set to zero void setParticleSize(uint8_t size); void setGravity(int8_t force = 8); void enableParticleCollisions(bool enable, uint8_t hardness = 255); @@ -172,13 +167,13 @@ class ParticleSystem PSparticle *particles; // pointer to particle array PSsource *sources; // pointer to sources PSadvancedParticle *advPartProps; // pointer to advanced particle properties (can be NULL) - uint8_t* PSdataEnd; //points to first available byte after the PSmemory, is set in setPointers(). use this to set pointer to FX custom data + PSsizeControl *advPartSize; //pointer to advanced particle size control (can be NULL) + uint8_t* PSdataEnd; //points to first available byte after the PSmemory, is set in setPointers(). use this for FX custom data uint16_t maxX, maxY; //particle system size i.e. width-1 / height-1 in subpixels uint32_t maxXpixel, maxYpixel; // last physical pixel that can be drawn to (FX can read this to read segment size if required), equal to width-1 / height-1 uint8_t numSources; //number of sources uint16_t numParticles; // number of particles available in this system - uint16_t usedParticles; // number of particles used in animation (can be smaller then numParticles) - PSsettings particlesettings; // settings used when updating particles (can also used by FX to move sources), do not edit properties directly, use functions above + uint16_t usedParticles; // number of particles used in animation (can be smaller then numParticles) private: //rendering functions @@ -192,7 +187,9 @@ class ParticleSystem void fireParticleupdate(); //utility functions - void updatePSpointers(bool isadvanced); // update the data pointers to current segment data space + void updatePSpointers(bool isadvanced, bool sizecontrol); // update the data pointers to current segment data space + void updateSize(PSadvancedParticle *advprops, PSsizeControl *advsize); //advanced size control + void getParticleXYsize(PSadvancedParticle *advprops, PSsizeControl *advsize, uint32_t &xsize, uint32_t &ysize); void bounce(int8_t &incomingspeed, int8_t ¶llelspeed, int32_t &position, uint16_t maxposition); //bounce on a wall int16_t wraparound(uint16_t p, uint32_t maxvalue); int32_t calcForce_dv(int8_t force, uint8_t *counter); @@ -200,6 +197,7 @@ class ParticleSystem CRGB **allocate2Dbuffer(uint32_t cols, uint32_t rows); // note: variables that are accessed often are 32bit for speed + PSsettings particlesettings; // settings used when updating particles (can also used by FX to move sources), do not edit properties directly, use functions above uint32_t emitIndex; // index to count through particles to emit so searching for dead pixels is faster int32_t collisionHardness; uint8_t wallHardness; @@ -215,10 +213,10 @@ class ParticleSystem }; //initialization functions (not part of class) -bool initParticleSystem(ParticleSystem *&PartSys, uint8_t requestedsources, bool advanced = false, uint16_t additionalbytes = 0); -uint32_t calculateNumberOfParticles(bool advanced); +bool initParticleSystem(ParticleSystem *&PartSys, uint8_t requestedsources, uint16_t additionalbytes = 0, bool largesizes = false, bool sizecontrol = false); +uint32_t calculateNumberOfParticles(bool advanced, bool sizecontrol); uint32_t calculateNumberOfSources(uint8_t requestedsources); -bool allocateParticleSystemMemory(uint16_t numparticles, uint16_t numsources, bool advanced, uint16_t additionalbytes); +bool allocateParticleSystemMemory(uint16_t numparticles, uint16_t numsources, bool advanced, bool sizecontrol, uint16_t additionalbytes); //color add function void fast_color_add(CRGB &c1, CRGB &c2, uint32_t scale = 255); // fast and accurate color adding with scaling (scales c2 before adding) void fast_color_scale(CRGB &c, uint32_t scale); //fast scaling function using 32bit factor (keep it 0-255) and pointer