From 136f40ff62fb7f0bf4577cbc769f90a741b17c00 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sun, 31 Mar 2024 17:42:48 +0200 Subject: [PATCH] More Bugfixes, more converted FX --- wled00/FX.cpp | 502 ++++++++++++++++-------------------- wled00/FX_fcn.cpp | 2 +- wled00/FXparticleSystem.cpp | 224 ++++++++-------- wled00/FXparticleSystem.h | 27 +- 4 files changed, 343 insertions(+), 412 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index ce4dd38823..f06ead8bde 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -7892,7 +7892,7 @@ uint16_t mode_particlerotatingspray(void) { if (SEGLEN == 1) return mode_static(); - const uint8_t numSprays = 8; // maximum number of sprays + uint8_t numSprays; // maximum number of sprays ParticleSystem *PartSys = NULL; uint32_t i = 0; uint32_t j = 0; @@ -7900,7 +7900,7 @@ uint16_t mode_particlerotatingspray(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, numSprays)) + if (!initParticleSystem(PartSys, 0)) return mode_static(); // allocation failed; //allocation failed Serial.print("PS pointer "); @@ -7913,7 +7913,8 @@ uint16_t mode_particlerotatingspray(void) SEGMENT.aux0 = 0; // starting angle SEGMENT.aux1 = 0x01; // check flags - // TODO: use SEGMENT.step for smooth direction change + // TODO: use SEGMENT.step for smooth direction change + numSprays = min(PartSys->numSources, (uint8_t) 8); for (i = 0; i < numSprays; i++) { PartSys->sources[i].source.sat = 255; // set saturation @@ -7941,7 +7942,9 @@ uint16_t mode_particlerotatingspray(void) } else PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS -/* + + numSprays = min(PartSys->numSources, (uint8_t)8); + /* if (SEGMENT.call < 3) { Serial.print("segment data ptr in candyFX"); @@ -7955,7 +7958,7 @@ uint16_t mode_particlerotatingspray(void) Serial.println("ERROR: paticle system not found, nullpointer"); return mode_static(); // something went wrong, no data! (TODO: ask how to handle this so it always works) } -//!!! + //!!! for (i = 0; i < numSprays; i++) { PartSys->sources[i].source.sat = 255; // set saturation @@ -7976,7 +7979,7 @@ uint16_t mode_particlerotatingspray(void) PartSys->sources[i].source.hue = coloroffset * i; } } -//!!! + //!!! if (SEGMENT.check1 != (SEGMENT.aux1 & 0x01)) //state change { if (SEGMENT.check1) @@ -8002,7 +8005,7 @@ uint16_t mode_particlerotatingspray(void) //set rotation direction and speed int32_t rotationspeed = SEGMENT.speed << 2; bool direction = SEGMENT.check2; - + if (SEGMENT.custom2 > 0) // automatic direction change enabled { uint16_t changeinterval = (265 - SEGMENT.custom2); @@ -8051,20 +8054,20 @@ uint16_t mode_particlerotatingspray(void) PartSys->SprayEmit(PartSys->sources[3]); }*/ // calculate angle offset for an even distribution - uint16_t angleoffset = 0xFFFF / spraycount; + uint16_t angleoffset = 0xFFFF / spraycount; - for (j = 0; j < spraycount; j++) - { - // calculate the x and y speed using aux0 as the 16bit angle. returned value by sin16/cos16 is 16bit, shifting it by 8 bits results in +/-128, divide that by custom1 slider value - PartSys->sources[j].vx = (cos16(SEGMENT.aux0 + angleoffset * j) >> 8) / ((263 - SEGMENT.intensity) >> 3); // update spray angle (rotate all sprays with angle offset) - PartSys->sources[j].vy = (sin16(SEGMENT.aux0 + angleoffset * j) >> 8) / ((263 - SEGMENT.intensity) >> 3); // update spray angle (rotate all sprays with angle offset) - PartSys->sources[j].var = (SEGMENT.custom3 >> 1); // emiting variation = nozzle size (custom 3 goes from 0-32) + for (j = 0; j < spraycount; j++) + { + // calculate the x and y speed using aux0 as the 16bit angle. returned value by sin16/cos16 is 16bit, shifting it by 8 bits results in +/-128, divide that by custom1 slider value + PartSys->sources[j].vx = (cos16(SEGMENT.aux0 + angleoffset * j) >> 8) / ((263 - SEGMENT.intensity) >> 3); // update spray angle (rotate all sprays with angle offset) + PartSys->sources[j].vy = (sin16(SEGMENT.aux0 + angleoffset * j) >> 8) / ((263 - SEGMENT.intensity) >> 3); // update spray angle (rotate all sprays with angle offset) + PartSys->sources[j].var = (SEGMENT.custom3 >> 1); // emiting variation = nozzle size (custom 3 goes from 0-32) } -#ifdef ESP8266 + #ifdef ESP8266 if (SEGMENT.call & 0x01) // every other frame, do not emit to save particles percycle = 0; -#endif + #endif //TODO: limit the emit amount by particle speed. should not emit more than one for every speed of like 20 or so, it looks weird on initialisation also make it depnd on angle speed, emit no more than once every few degrees -> less overlap (need good finetuning) @@ -8122,7 +8125,7 @@ uint16_t mode_particlefireworks(void) { for (i = 0; i < numParticles; i++) { - particles[i].ttl = 0; + PartSys->particles[i].ttl = 0; } for (j = 0; j < numRockets; j++) { @@ -8183,13 +8186,13 @@ uint16_t mode_particlefireworks(void) } for (i = 0; i < numParticles; i++) { - if (particles[i].ttl == 0) + if (PartSys->particles[i].ttl == 0) { // particle is dead if (emitparticles > 0) { if (circularexplosion) // do circle emit { - Emitter_Angle_emit(&rockets[j], &particles[i], angle, currentspeed); + Emitter_Angle_emit(&rockets[j], &PartSys->particles[i], angle, currentspeed); emitparticles--; // set angle for next particle angle += angleincrement; @@ -8209,7 +8212,7 @@ uint16_t mode_particlefireworks(void) } else { - Emitter_Fountain_emit(&rockets[j], &particles[i]); + Emitter_Fountain_emit(&rockets[j], &PartSys->particles[i]); emitparticles--; if ((j % 3) == 0) { @@ -8228,9 +8231,9 @@ uint16_t mode_particlefireworks(void) // update particles for (i = 0; i < numParticles; i++) { - if (particles[i].ttl) + if (PartSys->particles[i].ttl) { - Particle_Gravity_update(&particles[i], SEGMENT.check1, SEGMENT.check2, SEGMENT.check3, SEGMENT.custom2); + Particle_Gravity_update(&PartSys->particles[i], SEGMENT.check1, SEGMENT.check2, SEGMENT.check3, SEGMENT.custom2); } } // update the rockets, set the speed state @@ -8278,8 +8281,9 @@ uint16_t mode_particlefireworks(void) //TODO: after implementing gravity function, add slider custom3 to set gravity force static const char _data_FX_MODE_PARTICLEFIREWORKS[] PROGMEM = "PS Fireworks@Launches,Explosion Size,Fuse,Bounce,,Cylinder,Walls,Ground;;!;012;pal=11,sx=100,ix=50,c1=64,c2=128,c3=10,o1=0,o2=0,o3=0"; */ + /* - * Particle Volcano (gravity spray) + * Particle Volcano * Particles are sprayed from below, spray moves back and forth if option is set * Uses palette for particle color * by DedeHai (Damian Schneider) @@ -8325,7 +8329,7 @@ uint16_t mode_particlevolcano(void) { for (i = 0; i < numParticles; i++) { - particles[i].ttl = 0; + PartSys->particles[i].ttl = 0; } for (i = 0; i < numSprays; i++) { @@ -8376,10 +8380,10 @@ uint16_t mode_particlevolcano(void) j = 0; for (i = 0; i < numParticles; i++) { - if (particles[i].ttl == 0) // find a dead particle + if (PartSys->particles[i].ttl == 0) // find a dead particle { // spray[j].source.hue = random16(); //set random color for each particle (using palette) - Emitter_Fountain_emit(&spray[j], &particles[i]); + Emitter_Fountain_emit(&spray[j], &PartSys->particles[i]); j = (j + 1) % numSprays; if (percycle-- == 0) { @@ -8401,9 +8405,9 @@ uint16_t mode_particlevolcano(void) { //set color according to ttl ('color by age') if (SEGMENT.check1) - particles[i].hue = min((uint16_t)220, particles[i].ttl); + PartSys->particles[i].hue = min((uint16_t)220, PartSys->particles[i].ttl); - Particle_Gravity_update(&particles[i], false, SEGMENT.check2, true, hardness); + Particle_Gravity_update(&PartSys->particles[i], false, SEGMENT.check2, true, hardness); } SEGMENT.fill(BLACK); // clear the matrix @@ -8421,7 +8425,7 @@ static const char _data_FX_MODE_PARTICLEVOLCANO[] PROGMEM = "PS Volcano@Speed,In */ //TODO: -//do not use with to calculate numflames. it changes when mirror/transpose is activated, so will crash if height>width and then transposed +//do not use width to calculate numflames. it changes when mirror/transpose is activated, so will crash if height>width and then transposed //if using width*height and assuming a square, it will look sparse on a wide matrix... //could just allocate way too many and then dynamically adjust at at the expense of ram usage (but flames only use about 16bytes so is ok) //TODO: add 2D perlin noise to add to flame speed for randomness? may look good, may look awful, test it. also may hit FPS hard. @@ -8435,22 +8439,23 @@ uint16_t mode_particlefire(void) uint32_t i; //index variable #ifdef ESP8266 - const uint32_t numFlames = min((uint32_t)12, (cols << 1)); // limit to 18 flames, not enough ram on ESP8266 + uint32_t numFlames = min((uint32_t)12, (cols << 1)); // limit to 18 flames, not enough ram on ESP8266 const uint32_t numNormalFlames = numFlames - (numFlames / 3); // number of normal flames, rest of flames are baseflames uint32_t percycle = numFlames >> 2; // maximum number of particles emitted per cycle #else - const uint32_t numFlames = (cols*2); // number of flames: depends on fire width. for a fire width of 16 pixels, about 25-30 flames give good results, add a few for the base flames + uint32_t numFlames = (cols*2); // number of flames: depends on fire width. for a fire width of 16 pixels, about 25-30 flames give good results, add a few for the base flames const uint32_t numNormalFlames = numFlames;//- (cols / 2); // number of normal flames, rest of flames are baseflames uint32_t percycle = (numFlames) / 2; // maximum number of particles emitted per cycle #endif 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, numFlames)) + if (!initParticleSystem(PartSys, 0)) return mode_static(); // allocation failed; //allocation failed Serial.println("fireinit done"); SEGMENT.aux0 = rand(); // aux0 is wind position (index) in the perlin noise // initialize the flame sprays + numFlames = PartSys->numSources; for (i = 0; i < numFlames; i++) { PartSys->sources[i].source.ttl = 0; @@ -8463,17 +8468,19 @@ uint16_t mode_particlefire(void) } else PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS -/* - if (SEGMENT.call < 3) - { - Serial.print("segment data ptr in fireFX"); - Serial.println((uintptr_t)(SEGMENT.data)); - }*/ - - if (PartSys == NULL) + + numFlames = PartSys->numSources; + /* + if (SEGMENT.call < 3) { - Serial.println("ERROR: paticle system not found, nullpointer"); - return mode_static(); // something went wrong, no data! (TODO: ask how to handle this so it always works) + Serial.print("segment data ptr in fireFX"); + Serial.println((uintptr_t)(SEGMENT.data)); + }*/ + + if (PartSys == NULL) + { + Serial.println("ERROR: paticle system not found, nullpointer"); + return mode_static(); // something went wrong, no data! (TODO: ask how to handle this so it always works) } if(SEGMENT.check2) //wrap X set @@ -8505,7 +8512,7 @@ uint16_t mode_particlefire(void) if (i < numNormalFlames) { - PartSys->sources[i].source.ttl = 1 + random16((SEGMENT.intensity * SEGMENT.intensity) >> 8) / (1 + (SEGMENT.speed >> 5)); //'hotness' of fire, faster flames reduce the effect or flame height will scale too much with speed + PartSys->sources[i].source.ttl = 1 + random16((SEGMENT.custom1 * SEGMENT.custom1) >> 8) / (1 + (SEGMENT.speed >> 5)); //'hotness' of fire, faster flames reduce the effect or flame height will scale too much with speed // PartSys->sources[i].source.ttl = (rand() % ((SEGMENT.intensity * SEGMENT.intensity) >> 9) / (1 + (SEGMENT.speed >> 6))) + 10; //'hotness' of fire, faster flames reduce the effect or flame height will scale too much with speed // PartSys->sources[i].source.ttl = random16(SEGMENT.intensity+10) + 5; 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 @@ -8514,7 +8521,7 @@ uint16_t mode_particlefire(void) PartSys->sources[i].vy = 5 + (SEGMENT.speed >> 2); // emitting speed (upwards) PartSys->sources[i].var = random16(5) + 3; // speed variation around vx,vy (+/- var/2) } - else // base flames: make the base brighter, flames are slower and short lived + else // base flames: make the base brighter, flames are slower and short lived //TODO: not used anymore { // PartSys->sources[i].source.ttl = random16(25) + 15; // lifetime of one flame // PartSys->sources[i].maxLife = 25; // defines flame height together with the vy speed, vy speed*maxlife/PS_P_RADIUS is the average flame height @@ -8549,18 +8556,18 @@ uint16_t mode_particlefire(void) j = (j + 1) % numFlames; } /* //TODO: add wind back in, may need a PS function to add a constant velocity to all particles (or can use force but is slower. or just do it here may be the fastest way) - else if (particles[i].y > PS_P_RADIUS) // particle is alive, apply wind if y > 1 + else if (PartSys->particles[i].y > PS_P_RADIUS) // particle is alive, apply wind if y > 1 { // add wind using perlin noise - particles[i].vx = windspeed; //todo: should this be depending on position? would be slower but may look better (used in old, slow fire) + PartSys->particles[i].vx = windspeed; //todo: should this be depending on position? would be slower but may look better (used in old, slow fire) }*/ SEGMENT.fill(BLACK); // clear the matrix - PartSys->updateFire(SEGMENT.custom1, SEGMENT.check1); // update and render the fire, colormode is determined by custom 3 slider + PartSys->updateFire(SEGMENT.intensity, SEGMENT.check1); // update and render the fire, colormode is determined by custom 3 slider return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEFIRE[] PROGMEM = "PS Fire@Speed,Intensity,Base Flames,Wind,Color Scheme,Palette,Cylinder;;!;035;sx=130,ix=120,c1=110,c2=128,c3=0,o1=0"; +static const char _data_FX_MODE_PARTICLEFIRE[] PROGMEM = "PS Fire@Speed,Intensity,Height,Wind,,Palette,Cylinder;;!;035;sx=130,ix=120,c1=110,c2=128,c3=0,o1=0"; /* PS Ballpit: particles falling down, user can enable these three options: X-wraparound, side bounce, ground bounce @@ -8576,11 +8583,11 @@ uint16_t mode_particlefall(void) return mode_static(); ParticleSystem *PartSys = NULL; - uint32_t i; // index variable + 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)) //init, no sources needed + if (!initParticleSystem(PartSys, 0)) //init return mode_static(); // allocation failed; //allocation failed PartSys->setKillOutOfBounds(true); PartSys->enableGravity(true); @@ -8608,79 +8615,78 @@ uint16_t mode_particlefall(void) PartSys->enableParticleCollisions(false); } + uint32_t i; // index variable + if (SEGMENT.call % (64 - (SEGMENT.intensity >> 2)) == 0 && SEGMENT.intensity > 1) // every nth frame emit particles, stop emitting if set to zero { for (i = 0; i < PartSys->usedParticles; i++) // emit particles { if (PartSys->particles[i].ttl == 0) // find a dead particle { - // emit particle at random position just over the top of the matrix + // emit particle at random position over the top of the matrix (random16 is not random enough) PartSys->particles[i].ttl = 1500 - (SEGMENT.speed << 2) + random16(500); // if speed is higher, make them die sooner - if (random16(5) == 0) // 16% of particles apper anywhere - PartSys->particles[i].x = random16(PartSys->maxX); - else // rest is emitted at center half - PartSys->particles[i].x = random16((PartSys->maxX) >> 1) + (PartSys->maxX) >> 2; - - PartSys->particles[i].y = random16(PartSys->maxY) + PartSys->maxY; // particles appear somewhere above the matrix, maximum is double the height - PartSys->particles[i].vx = (((int16_t)random16(SEGMENT.custom1)) - (SEGMENT.custom1 >> 1)) >> 1; // side speed is +/- a quarter of the custom1 slider - PartSys->particles[i].vy = -(SEGMENT.speed >> 1); // downward speed - PartSys->particles[i].hue = random16(); // set random color - PartSys->particles[i].sat = ((SEGMENT.custom3) << 3) + 7; // set saturation - break; // emit only one particle per round + PartSys->particles[i].x = random(PartSys->maxX >> 1) + PartSys->maxX >> 2; + PartSys->particles[i].y = (PartSys->maxY<<1); // particles appear somewhere above the matrix, maximum is double the height + PartSys->particles[i].vx = (((int16_t)random(SEGMENT.custom1)) - (SEGMENT.custom1 >> 1)+5) >> 1; // side speed is +/- a quarter of the custom1 slider + PartSys->particles[i].vy = map(SEGMENT.speed, 0, 255, -5, -100); // downward speed + PartSys->particles[i].hue = random16(); // set random color + PartSys->particles[i].sat = ((SEGMENT.custom3) << 3) + 7; // set saturation + PartSys->particles[i].collide = true; //enable collision for particle + break; // emit only one particle per round } } } -//!!! - /* - i=0; - // if (SEGMENT.call % 2 == 0) // every nth frame emit particles, stop emitting if set to zero - { - - uint8_t emit = 5; - //i = random() % PartSys->usedParticles; - while (i < PartSys->usedParticles) // emit particles + //!!! + /* + i=0; + // if (SEGMENT.call % 2 == 0) // every nth frame emit particles, stop emitting if set to zero { - if (PartSys->particles[i].ttl == 0) // find a dead particle + + + uint8_t emit = 5; + //i = random() % PartSys->usedParticles; + while (i < PartSys->usedParticles) // emit particles { - Serial.print(" i="); - Serial.println(i); - int32_t x = random16((PartSys->maxX << 1)) - (PartSys->maxX >> 1); - int32_t y = random16((PartSys->maxY << 1)) - (PartSys->maxY >> 1); - - PartSys->particles[i].x = x; - PartSys->particles[i].y = y; - //PartSys->particles[i].x = random16((PartSys->maxX) >> 1); - //PartSys->particles[i].y = random16((PartSys->maxY) >> 1); - PartSys->particles[i].vx = random16(10) - 5; - PartSys->particles[i].vy = random16(10) - 5; - PartSys->particles[i].ttl = 3000; - PartSys->particles[i].hue = random16(); // set random color - PartSys->particles[i].sat = 255; - if(emit-- == 0) - break; + if (PartSys->particles[i].ttl == 0) // find a dead particle + { + Serial.print(" i="); + Serial.println(i); + int32_t x = random16((PartSys->maxX << 1)) - (PartSys->maxX >> 1); + int32_t y = random16((PartSys->maxY << 1)) - (PartSys->maxY >> 1); + + PartSys->particles[i].x = x; + PartSys->particles[i].y = y; + //PartSys->particles[i].x = random16((PartSys->maxX) >> 1); + //PartSys->particles[i].y = random16((PartSys->maxY) >> 1); + PartSys->particles[i].vx = random16(10) - 5; + PartSys->particles[i].vy = random16(10) - 5; + PartSys->particles[i].ttl = 3000; + PartSys->particles[i].hue = random16(); // set random color + PartSys->particles[i].sat = 255; + if(emit-- == 0) + break; + } + i++; } - i++; } - } - */ - //!!! + */ + //!!! - uint32_t frictioncoefficient = 1; - if (SEGMENT.speed < 50) // for low speeds, apply more friction - { - frictioncoefficient = 50 - SEGMENT.speed; - } - if (SEGMENT.call % 3 == 0) - PartSys->applyFriction(frictioncoefficient); + uint32_t frictioncoefficient = 1; + if (SEGMENT.speed < 50) // for low speeds, apply more friction + frictioncoefficient = 50 - SEGMENT.speed; - SEGMENT.fill(BLACK); // clear the matrix - PartSys->update(); // update and render + if (SEGMENT.call % 3 == 0) + PartSys->applyFriction(frictioncoefficient); // add some frictino to help smooth things - return FRAMETIME; + SEGMENT.fill(BLACK); // clear the matrix + PartSys->update(); // update and render + + return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEFALL[] PROGMEM = "PS Ballpit@Speed,Intensity,Randomness,Hardness,Saturation,Cylinder,Walls,Ground;;!;012;pal=11,sx=100,ix=200,c1=31,c2=100,c3=28,o1=0,o2=0,o3=1"; +static const char _data_FX_MODE_PARTICLEFALL[] PROGMEM = "PS Ballpit@Speed,Intensity,Randomness,Hardness,Saturation,Cylinder,Walls,Ground;;!;012;pal=11,sx=100,ix=200,c1=31,c2=100,c3=31,o1=0,o2=0,o3=1"; /* * Particle Waterfall @@ -9015,67 +9021,57 @@ static const char _data_FX_MODE_PARTICLEPERLIN[] PROGMEM = "PS Fuzzy Noise@Speed * Particle smashing down like meteorites and exploding as they hit the ground, has many parameters to play with * by DedeHai (Damian Schneider) */ -/* + uint16_t mode_particleimpact(void) { if (SEGLEN == 1) return mode_static(); - - const uint32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; - const uint32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); - - // particle system box dimensions - const uint32_t Max_x(cols * PS_P_RADIUS - 1); - const uint32_t Max_y(rows * PS_P_RADIUS - 1); - -#ifdef ESP8266 - const uint32_t numParticles = 150; - const uint8_t MaxNumMeteors = 2; -#else - const uint32_t numParticles = 550; - const uint8_t MaxNumMeteors = 8; -#endif - - PSparticle *particles; - PSsource *meteors; - - // allocate memory and divide it into proper pointers, max is 32k for all segments. - uint32_t dataSize = sizeof(PSparticle) * numParticles; - dataSize += sizeof(PSsource) * (MaxNumMeteors); - if (!SEGENV.allocateData(dataSize)) - return mode_static(); // allocation failed; //allocation failed - - meteors = reinterpret_cast(SEGENV.data); - // calculate the end of the spray data and assign it as the data pointer for the particles: - particles = reinterpret_cast(meteors + MaxNumMeteors); // cast the data array into a particle pointer - + ParticleSystem *PartSys = NULL; uint32_t i = 0; - uint32_t j = 0; - uint8_t numMeteors = map(SEGMENT.custom3, 0, 31, 1, MaxNumMeteors); // number of meteors to use for animation + uint8_t MaxNumMeteors; - if (SEGMENT.call == 0) // initialization + if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. { - for (i = 0; i < numParticles; i++) - { - particles[i].ttl = 0; - } + if (!initParticleSystem(PartSys, 0)) // init, no additional data needed + return mode_static(); // allocation failed; //allocation failed + // PartSys->setKillOutOfBounds(true); + PartSys->enableGravity(true); + PartSys->setBounceY(true); //always use ground bounce + // PartSys->setUsedParticles((PartSys->numParticles * 3) / 2); // use 2/3 of available particles + MaxNumMeteors = min(PartSys->numSources, (uint8_t)8); for (i = 0; i < MaxNumMeteors; i++) { - meteors[i].source.y = 10; - meteors[i].source.ttl = random16(20 * i); // set initial delay for meteors - meteors[i].source.vy = 10; // at positive speeds, no particles are emitted and if particle dies, it will be relaunched - meteors[i].source.sat = 255; //full saturation, color chosen by palette + PartSys->sources[i].vx = 0; //emit speed in x + PartSys->sources[i].source.y = 10; + PartSys->sources[i].source.ttl = random16(20 * i); // set initial delay for meteors + PartSys->sources[i].source.vy = 10; // at positive speeds, no particles are emitted and if particle dies, it will be relaunched + PartSys->sources[i].source.sat = 255; // full saturation, color chosen by palette } } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS - // update particles, create particles + MaxNumMeteors = min(PartSys->numSources, (uint8_t)8); + uint8_t numMeteors = map(SEGMENT.custom3, 0, 31, 1, MaxNumMeteors); // number of meteors to use for animation + + if (PartSys == NULL) + { + Serial.println("ERROR: paticle system not found, nullpointer"); + return mode_static(); // something went wrong, no data! (TODO: ask how to handle this so it always works) + } + + PartSys->setWrapX(SEGMENT.check1); + PartSys->setBounceX(SEGMENT.check2); + PartSys->setWallHardness(SEGMENT.custom2); // + PartSys->enableParticleCollisions(SEGMENT.check3, SEGMENT.custom2); // enable collisions and set particle collision hardness + uint32_t emitparticles; // number of particles to emit for each rocket's state - i = 0; - for (j = 0; j < numMeteors; j++) + + for (i = 0; i < numMeteors; i++) { // determine meteor state by its speed: - if (meteors[j].source.vy < 0) // moving down, emit sparks + if ( PartSys->sources[i].source.vy < 0) // moving down, emit sparks { #ifdef ESP8266 emitparticles = 1; @@ -9083,102 +9079,74 @@ uint16_t mode_particleimpact(void) emitparticles = 2; #endif } - else if (meteors[j].source.vy > 0) // moving up means meteor is on 'standby' + else if ( PartSys->sources[i].source.vy > 0) // moving up means meteor is on 'standby' { emitparticles = 0; } else // speed is zero, explode! { - meteors[j].source.vy = 125; // set source speed positive so it goes into timeout and launches again + PartSys->sources[i].source.vy = 125; // set source speed positive so it goes into timeout and launches again #ifdef ESP8266 emitparticles = random16(SEGMENT.intensity >> 2) + 10; // defines the size of the explosion #else emitparticles = random16(SEGMENT.intensity >> 1) + 10; // defines the size of the explosion #endif } - - while(i < numParticles) - { - if (particles[i].ttl == 0) // particle is dead - { - if (emitparticles > 0) - { - Emitter_Fountain_emit(&meteors[j], &particles[i]); - emitparticles--; - } - else - break; // done emitting for this meteor - } - i++; - } - } - - uint8_t hardness = SEGMENT.custom2; // how hard the collisions are, 255 = fully hard, no energy is lost in collision - - if (SEGMENT.check3) // use collisions if option is set - { - detectCollisions(particles, numParticles, hardness); - } - // update particles - for (i = 0; i < numParticles; i++) - { - if (particles[i].ttl) + for (int e = emitparticles; e > 0; e--) { - Particle_Gravity_update(&particles[i], SEGMENT.check1, SEGMENT.check2, true, hardness); + PartSys->sprayEmit(PartSys->sources[i]); } } // update the meteors, set the speed state for (i = 0; i < numMeteors; i++) { - if (meteors[i].source.ttl) + if (PartSys->sources[i].source.ttl) { - Particle_Gravity_update(&meteors[i].source, SEGMENT.check1, SEGMENT.check2, true, 255); // move the meteor, age the meteor (ttl--) - if (meteors[i].source.vy > 0) - meteors[i].source.y = 5; //'hack' to keep the meteors within frame, as ttl will be set to zero by gravity update if too far out of frame + PartSys->applyGravity(&PartSys->sources[i].source); + PartSys->particleMoveUpdate(PartSys->sources[i].source, PartSys->particlesettings); + // 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 + if ((PartSys->sources[i].source.y < PS_P_RADIUS) && ( PartSys->sources[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 - meteors[i].maxLife = 200; - meteors[i].minLife = 50; + PartSys->sources[i].source.vy = 0; // set speed zero so it will explode + PartSys->sources[i].source.vx = 0; + //PartSys->sources[i].source.y = 5; // offset from ground so explosion happens not out of frame (TODO: still needed?) + PartSys->sources[i].source.collide = true; + PartSys->sources[i].maxLife = 200; + PartSys->sources[i].minLife = 50; #ifdef ESP8266 - meteors[i].source.ttl = random16(255 - (SEGMENT.speed>>1)) + 10; // standby time til next launch (in frames at 42fps, max of 265 is about 6 seconds + PartSys->sources[i].source.ttl = random16(255 - (SEGMENT.speed>>1)) + 10; // standby time til next launch (in frames at 42fps, max of 265 is about 6 seconds #else - meteors[i].source.ttl = random16((255 - SEGMENT.speed)) + 10; // standby time til next launch (in frames at 42fps, max of 265 is about 6 seconds + PartSys->sources[i].source.ttl = random16((255 - SEGMENT.speed)) + 10; // standby time til next launch (in frames at 42fps, max of 265 is about 6 seconds #endif - meteors[i].vx = 0; // emitting speed x - meteors[i].vy = (SEGMENT.custom1 >> 2); // emitting speed y - meteors[i].var = (SEGMENT.custom1 >> 1); // speed variation around vx,vy (+/- var/2) + PartSys->sources[i].vy = (SEGMENT.custom1 >> 2); // emitting speed y + PartSys->sources[i].var = (SEGMENT.custom1 >> 1); // speed variation around vx,vy (+/- var/2) } - } - else if (meteors[i].source.vy > 0) // meteor is exploded and time is up (ttl==0 and positive speed), relaunch it + } + else if ( PartSys->sources[i].source.vy > 0) // meteor is exploded and time is up (ttl==0 and positive speed), relaunch it { // reinitialize meteor - meteors[i].source.y = Max_y + (PS_P_RADIUS << 2); // start 4 pixels above the top - meteors[i].source.x = random16(Max_x); - meteors[i].source.vy = -random16(30) - 30; // meteor downward speed - meteors[i].source.vx = random16(30) - 15; - meteors[i].source.hue = random16(); // random color - meteors[i].source.ttl = 1000; // long life, 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].vx = 0; // emitting speed - meteors[i].vy = -9; // emitting speed (down) - meteors[i].var = 5; // speed variation around vx,vy (+/- var/2) + PartSys->sources[i].source.y = PartSys->maxY + (PS_P_RADIUS << 2); // start 4 pixels above the top + PartSys->sources[i].source.x = random16(PartSys->maxX); + PartSys->sources[i].source.vy = -random16(30) - 30; // meteor downward speed + PartSys->sources[i].source.vx = random16(30) - 15; + PartSys->sources[i].source.hue = random16(); // random color + PartSys->sources[i].source.ttl = 1000; // long life, will explode at bottom + PartSys->sources[i].source.collide = false; // trail particles will not collide + PartSys->sources[i].maxLife = 60; // spark particle life + PartSys->sources[i].minLife = 20; + PartSys->sources[i].vy = -9; // emitting speed (down) + PartSys->sources[i].var = 5; // speed variation around vx,vy (+/- var/2) } } - SEGMENT.fill(BLACK); // clear the matrix - // render the particles - ParticleSys_render(particles, numParticles, false, false); + SEGMENT.fill(BLACK); // clear the matrix + PartSys->update(); // update and render return FRAMETIME; } static const char _data_FX_MODE_PARTICLEIMPACT[] PROGMEM = "PS Impact@Launches,Explosion Size,Explosion Force,Bounce,Meteors,Cylinder,Walls,Collisions;;!;012;pal=0,sx=32,ix=85,c1=100,c2=100,c3=8,o1=0,o2=1,o3=1"; -*/ + /* Particle Attractor, a particle attractor sits in the matrix center, a spray bounces around and seeds particles uses inverse square law like in planetary motion @@ -9434,46 +9402,45 @@ uint16_t mode_particlespray(void) } static const char _data_FX_MODE_PARTICLESPRAY[] PROGMEM = "PS Spray@Speed,!,Left/Right,Up/Down,Angle,Gravity,Cylinder/Square,Collisions;;!;012;pal=0,sx=150,ix=90,c3=31,o1=0,o2=0,o3=0"; */ + /* Particle base Graphical Equalizer Uses palette for particle color by DedeHai (Damian Schneider) */ -/* + uint16_t mode_particleGEQ(void) { - if (SEGLEN == 1) return mode_static(); - const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; - //const uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); - -#ifdef ESP8266 - const uint32_t numParticles = 150; // maximum number of particles -#else - const uint32_t numParticles = 500; // maximum number of particles -#endif - - PSparticle *particles; + ParticleSystem *PartSys = NULL; - // allocate memory and divide it into proper pointers, max is 32k for all segments. - uint32_t dataSize = sizeof(PSparticle) * numParticles; - if (!SEGENV.allocateData(dataSize)) - return mode_static(); // allocation failed; //allocation failed + 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, 0)) // init + return mode_static(); // allocation failed; //allocation failed + PartSys->setKillOutOfBounds(true); + PartSys->setUsedParticles((PartSys->numParticles * 3) / 2); // use 2/3 of available particles + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS - // calculate the end of the spray data and assign it as the data pointer for the particles: - particles = reinterpret_cast(SEGENV.data); // cast the data array into a particle pointer - uint32_t i; - if (SEGMENT.call == 0) // initialization + if (PartSys == NULL) { - for (i = 0; i < numParticles; i++) - { - particles[i].ttl = 0; - particles[i].sat = 255; //full color - } + Serial.println("ERROR: paticle system not found, nullpointer"); + return mode_static(); // something went wrong, no data! (TODO: ask how to handle this so it always works) } + uint32_t i; + //set particle system properties + PartSys->setWrapX(SEGMENT.check1); + PartSys->setBounceX(SEGMENT.check2); + PartSys->setBounceY(SEGMENT.check3); + PartSys->enableParticleCollisions(false); + PartSys->setWallHardness(SEGMENT.custom2); + PartSys->enableGravity(true, SEGMENT.custom3<<2); //set gravity strength + um_data_t *um_data; if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { @@ -9489,7 +9456,7 @@ uint16_t mode_particleGEQ(void) //implement it simply first, then add complexity... need to check what looks good i = 0; uint32_t bin; //current bin - uint32_t binwidth = (cols * PS_P_RADIUS - 1)>>4; //emit poisition variation for one bin (+/-) + uint32_t binwidth = (PartSys->maxX + 1)>>4; //emit poisition variation for one bin (+/-) is equal to width/16 (for 16 bins) uint32_t threshold = 300 - SEGMENT.intensity; uint32_t emitparticles = 0; @@ -9512,58 +9479,31 @@ uint16_t mode_particleGEQ(void) } } - while (i < numParticles && emitparticles > 0) // emit particles if there are any left, low frequencies take priority + while (i < PartSys->usedParticles && emitparticles > 0) // emit particles if there are any left, low frequencies take priority { - if (particles[i].ttl == 0) // find a dead particle + if (PartSys->particles[i].ttl == 0) // find a dead particle { //set particle properties - particles[i].ttl = map(SEGMENT.intensity, 0,255, emitspeed>>1, emitspeed + random16(emitspeed)) ; // set particle alive, particle lifespan is in number of frames - particles[i].x = xposition + random16(binwidth) - (binwidth>>1); //position randomly, deviating half a bin width - particles[i].y = 0; //start at the bottom - particles[i].vx = random16(SEGMENT.custom1>>1)-(SEGMENT.custom1>>2) ; //x-speed variation - particles[i].vy = emitspeed; - particles[i].hue = (bin<<4) + random16(17) - 8; // color from palette according to bin - //particles[i].sat = ((SEGMENT.custom3) << 3) + 7; // set saturation + PartSys->particles[i].ttl = map(SEGMENT.intensity, 0,255, emitspeed>>1, emitspeed + random16(emitspeed)) ; // set particle alive, particle lifespan is in number of frames + PartSys->particles[i].x = xposition + random16(binwidth) - (binwidth>>1); //position randomly, deviating half a bin width + PartSys->particles[i].y = 0; //start at the bottom + PartSys->particles[i].vx = random16(SEGMENT.custom1>>1)-(SEGMENT.custom1>>2) ; //x-speed variation + PartSys->particles[i].vy = emitspeed; + PartSys->particles[i].hue = (bin<<4) + random16(17) - 8; // color from palette according to bin + PartSys->particles[i].sat = 255; // set saturation emitparticles--; } i++; } } - uint8_t hardness = SEGMENT.custom2; // how hard the collisions are, 255 = full hard. - // detectCollisions(particles, numParticles, hardness); - - // now move the particles - for (i = 0; i < numParticles; i++) - { - particles[i].vy -= (SEGMENT.custom3>>3); // apply stronger gravity - Particle_Gravity_update(&particles[i], SEGMENT.check1, SEGMENT.check2, SEGMENT.check3, hardness); - } - SEGMENT.fill(BLACK); // clear the matrix - - // render the particles - ParticleSys_render(particles, numParticles, SEGMENT.check1, false); // custom3 slider is saturation - + PartSys->update(); // update and render return FRAMETIME; } static const char _data_FX_MODE_PARTICLEGEQ[] PROGMEM = "PS Equalizer@Speed,Intensity,Diverge,Bounce,Gravity,Cylinder,Walls,Floor;;!;012;pal=0,sx=155,ix=200,c1=0,c2=128,c3=31,o1=0,o2=0,o3=0"; -uint32_t rand16seedESP = 1337; -uint32_t random16_ESP() -{ - rand16seedESP = rand16seedESP * FASTLED_RAND16_2053 + FASTLED_RAND16_13849; - return rand16seedESP; -} -uint32_t random16_ESP(uint32_t limit) -{ - uint32_t r = random16_ESP(); - uint32_t p = limit * r; - r = p >> 16; - return r; -} -*/ /* * Particle rotating GEQ * Particles sprayed from center with a rotating spray @@ -9611,7 +9551,7 @@ uint16_t mode_particlecenterGEQ(void) SEGMENT.aux1 = 0xFF; // user check for (i = 0; i < numParticles; i++) { - particles[i].ttl = 0; + PartSys->particles[i].ttl = 0; } for (i = 0; i < numSprays; i++) { @@ -9653,7 +9593,7 @@ uint16_t mode_particlecenterGEQ(void) while (i < numParticles) { - if (particles[i].ttl == 0) // find a dead particle + if (PartSys->particles[i].ttl == 0) // find a dead particle { uint8_t emitspeed = 5 + (((uint32_t)fftResult[j] * ((uint32_t)SEGMENT.speed+10)) >> 9); // emit speed according to loudness of band uint8_t emitangle = j * 16 + random16(SEGMENT.custom3 >> 1) + angleoffset; @@ -9672,7 +9612,7 @@ uint16_t mode_particlecenterGEQ(void) } } if (emitparticles) - Emitter_Angle_emit(&spray[j], &particles[i], emitangle, emitspeed); + Emitter_Angle_emit(&spray[j], &PartSys->particles[i], emitangle, emitspeed); j = (j + 1) % numSprays; } i++; @@ -9683,7 +9623,7 @@ uint16_t mode_particlecenterGEQ(void) for (i = 0; i < numParticles; i++) { - Particle_Move_update(&particles[i], true); // move the particles, kill out of bounds particles + Particle_Move_update(&PartSys->particles[i], true); // move the particles, kill out of bounds particles } SEGMENT.fill(BLACK); // clear the matrix @@ -9941,6 +9881,8 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_PARTICLEROTATINGSPRAY, &mode_particlerotatingspray, _data_FX_MODE_PARTICLEROTATINGSPRAY); addEffect(FX_MODE_PARTICLEFIRE, &mode_particlefire, _data_FX_MODE_PARTICLEFIRE); addEffect(FX_MODE_PARTICLEFALL, &mode_particlefall, _data_FX_MODE_PARTICLEFALL); + addEffect(FX_MODE_PARTICLEIMPACT, &mode_particleimpact, _data_FX_MODE_PARTICLEIMPACT); + addEffect(FX_MODE_PARTICLESGEQ, &mode_particleGEQ, _data_FX_MODE_PARTICLEGEQ); /* addEffect(FX_MODE_PARTICLEVOLCANO, &mode_particlevolcano, _data_FX_MODE_PARTICLEVOLCANO); @@ -9949,10 +9891,10 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_PARTICLEBOX, &mode_particlebox, _data_FX_MODE_PARTICLEBOX); addEffect(FX_MODE_PARTICLEWATERFALL, &mode_particlewaterfall, _data_FX_MODE_PARTICLEWATERFALL); - addEffect(FX_MODE_PARTICLEIMPACT, &mode_particleimpact, _data_FX_MODE_PARTICLEIMPACT); + 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_PARTICLECENTERGEQ, &mode_particlecenterGEQ, _data_FX_MODE_PARTICLECCIRCULARGEQ); diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index 5568fcff15..d5b04576a2 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -148,7 +148,7 @@ Segment& Segment::operator= (Segment &&orig) noexcept { bool IRAM_ATTR Segment::allocateData(size_t len) { if (len == 0) return false; // nothing to do if (data && _dataLen >= len) { // already allocated enough (reduce fragmentation) - if (call == 0) memset(data, 0, len); // erase buffer if called during effect initialisation + //!!! if (call == 0) memset(data, 0, len); // erase buffer if called during effect initialisation return true; } DEBUG_PRINT(F("Allocating Data")); diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index 2fa2c1e00d..f49a0c198c 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -56,14 +56,14 @@ ParticleSystem::ParticleSystem(uint16_t width, uint16_t height, uint16_t numbero 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 + //particlesettings = {false, false, false, false, false, false, false, false}; // all settings off by default initPSpointers(); // set the particle and sources pointer (call this before accessing sprays or particles) setMatrixSize(width, height); setWallHardness(255); // set default wall hardness to max emitIndex = 0; for (int i = 0; i < numParticles; i++) { - particles[i].ttl = 0; //initialize all particles to dead + //particles[i].ttl = 0; //initialize all particles to dead } Serial.println("PS Constructor done"); } @@ -199,6 +199,7 @@ void ParticleSystem::sprayEmit(PSsource &emitter) particles[emitIndex].ttl = random16(emitter.maxLife - emitter.minLife) + emitter.minLife; particles[emitIndex].hue = emitter.source.hue; particles[emitIndex].sat = emitter.source.sat; + particles[emitIndex].collide = emitter.source.collide; break; } /* @@ -385,7 +386,6 @@ void ParticleSystem::applyAngleForce(PSparticle *part, uint32_t numparticles, ui void ParticleSystem::applyGravity(PSparticle *part, uint32_t numarticles, uint8_t force, uint8_t *counter) { int32_t dv; // velocity increase - if (force > 15) dv = (force >> 4); // apply the 4 MSBs else @@ -411,6 +411,21 @@ void ParticleSystem::applyGravity(PSparticle *part, uint32_t numarticles, uint8_ applyGravity(part, numarticles, gforce, counter); } +//apply gravity to single particle using system settings (use this for sources) +void ParticleSystem::applyGravity(PSparticle *part) +{ + int32_t dv; // velocity increase + if (gforce > 15) + dv = (gforce >> 4); // apply the 4 MSBs + else + dv = 1; + + if (gforcecounter + gforce > 15) //counter is updated in global update when applying gravity + { + part->vy = part->vy - dv > PS_P_MAXSPEED ? PS_P_MAXSPEED : part->vy - dv; // limit the force, this is faster than min or if/else + } +} + // slow down particles 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) { @@ -536,23 +551,10 @@ void ParticleSystem::ParticleSys_render() // TODO: if pointer returns null, use classic render (or do not render this frame) if (useLocalBuffer) { - // allocate memory for the 2D array in one contiguous block - colorbuffer = (CRGB **)malloc((maxXpixel + 1) * sizeof(CRGB *) + (maxXpixel + 1) * (maxYpixel + 1) * sizeof(CRGB)); + // allocate memory for the local renderbuffer + colorbuffer = allocate2Dbuffer(maxXpixel + 1, maxYpixel + 1); if (colorbuffer == NULL) - { - DEBUG_PRINT(F("PS renderbuffer memory alloc failed")); - useLocalBuffer = false; - //return; - } - else{ - // assign pointers of 2D array - CRGB *start = (CRGB *)(colorbuffer + (maxXpixel + 1)); - for (i = 0; i < maxXpixel + 1; i++) - { - colorbuffer[i] = start + i * (maxYpixel + 1); - } - memset(start, 0, (maxXpixel + 1) * (maxYpixel + 1) * sizeof(CRGB)); // set all values to zero - } + useLocalBuffer = false; //render to segment pixels directly if not enough memory } // go over particles and render them to the buffer @@ -805,10 +807,10 @@ void ParticleSystem::fireParticleupdate() particles[i].ttl--; // apply velocity particles[i].x = particles[i].x + (int32_t)particles[i].vx; - particles[i].y = particles[i].y + (int32_t)particles[i].vy + (particles[i].ttl >> 4); // younger particles move faster upward as they are hotter, used for fire + particles[i].y = particles[i].y + (int32_t)particles[i].vy + (particles[i].ttl >> 4); // younger particles move faster upward as they are hotter particles[i].outofbounds = 0; // check if particle is out of bounds, wrap x around to other side if wrapping is enabled - // as fire particles start below the frame, lots of particles are out of bounds in y direction. to improve animation speed, only check x direction if y is not out of bounds + // 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 // y-direction if (particles[i].y < -PS_P_HALFRADIUS) particles[i].outofbounds = 1; @@ -842,41 +844,22 @@ void ParticleSystem::renderParticleFire(uint32_t intensity, bool usepalette) uint32_t flameheat; //depends on particle.ttl uint32_t i; uint32_t debug = 0; - //CRGB colorbuffer[(maxXpixel+1)][(maxYpixel+1)] = {0}; - - // Allocate memory for the array of pointers to rows - CRGB **colorbuffer = (CRGB **)calloc(maxXpixel + 1, sizeof(CRGB *)); + + // allocate memory for the local renderbuffer + CRGB **colorbuffer = allocate2Dbuffer(maxXpixel + 1, maxYpixel + 1); if (colorbuffer == NULL) { - Serial.println("Memory allocation failed111"); - return; + SEGMENT.setPalette(35); // set fire palette + SEGMENT.check1 = true; //enable palette from now on + usepalette = true; //use palette f } + //TODO: move the rendering over to the render-particle function, add a parameter 'rendertype' to select normal rendering or fire rendering, in the future this can also be used to render smaller/larger particle sizes - // Allocate memory for each row - for (i = 0; i <= maxXpixel; i++) - { - colorbuffer[i] = (CRGB *)calloc(maxYpixel + 1, sizeof(CRGB)); - if (colorbuffer[i] == NULL) - { - Serial.println("Memory allocation failed222"); - return; - } - } - - // go over particles and update matrix cells on the way - // note: some pixels (the x+1 ones) can be out of bounds, it is probably faster than to check that for every pixel as this only happens on the right border (and nothing bad happens as this is checked down the road) for (i = 0; i < usedParticles; i++) { - if (particles[i].outofbounds) //lots of fire particles are out of bounds, check first - { - //Serial.print("o"); - continue; - } - if (particles[i].ttl == 0) - { - //Serial.print("d"); + if (particles[i].outofbounds || particles[i].ttl == 0) // lots of fire particles are out of bounds, check first continue; - } + if (usepalette) { // generate RGB values for particle @@ -890,38 +873,29 @@ void ParticleSystem::renderParticleFire(uint32_t intensity, bool usepalette) renderParticle(&particles[i], brightness, intensity, pixco); if (intensity[0] > 0) - colorbuffer[pixco[0][0]][pixco[0][1]] = fast_color_add(colorbuffer[pixco[0][0]][pixco[0][1]], baseRGB, intensity[0]); // bottom left - // SEGMENT.addPixelColorXY(pixco[0][0], maxYpixel - pixco[0][1], baseRGB.scale8((uint8_t)intensity[0])); + colorbuffer[pixco[0][0]][pixco[0][1]] = fast_color_add(colorbuffer[pixco[0][0]][pixco[0][1]], baseRGB, intensity[0]); // bottom left if (intensity[1] > 0) - colorbuffer[pixco[1][0]][pixco[1][1]] = fast_color_add(colorbuffer[pixco[1][0]][pixco[1][1]], baseRGB, intensity[1]); - // SEGMENT.addPixelColorXY(pixco[1][0], maxYpixel - pixco[1][1], baseRGB.scale8((uint8_t)intensity[1])); // bottom right + colorbuffer[pixco[1][0]][pixco[1][1]] = fast_color_add(colorbuffer[pixco[1][0]][pixco[1][1]], baseRGB, intensity[1]); if (intensity[2] > 0) colorbuffer[pixco[2][0]][pixco[2][1]] = fast_color_add(colorbuffer[pixco[2][0]][pixco[2][1]], baseRGB, intensity[2]); - // SEGMENT.addPixelColorXY(pixco[2][0], maxYpixel - pixco[2][1], baseRGB.scale8((uint8_t)intensity[2])); // top right if (intensity[3] > 0) colorbuffer[pixco[3][0]][pixco[3][1]] = fast_color_add(colorbuffer[pixco[3][0]][pixco[3][1]], baseRGB, intensity[3]); - // SEGMENT.addPixelColorXY(pixco[3][0], maxYpixel - pixco[3][1], baseRGB.scale8((uint8_t)intensity[3])); // top left } else{ flameheat = particles[i].ttl; int32_t pixelheat[4] = {0}; // note: passed array needs to be set to 0 or checking in rendering function does not work (if values persist), this is faster then setting it to 0 there renderParticle(&particles[i], flameheat, pixelheat, pixco); //render heat to physical pixels - - // TODO: add one more pixel closer to the particle, so it is 3 pixels wide + if (pixelheat[0] >= 0) - PartMatrix_addHeat(pixelheat[0], &colorbuffer[pixco[0][0]][pixco[0][1]].r, intensity); - //PartMatrix_addHeat(pixco[0][0], pixco[0][1], pixelheat[0], intensity); + PartMatrix_addHeat(pixelheat[0], &colorbuffer[pixco[0][0]][pixco[0][1]].r, intensity); if (pixelheat[1] >= 0) - PartMatrix_addHeat(pixelheat[1], &colorbuffer[pixco[1][0]][pixco[1][1]].r, intensity); - //PartMatrix_addHeat(pixco[1][0], pixco[1][1], pixelheat[1], intensity); + PartMatrix_addHeat(pixelheat[1], &colorbuffer[pixco[1][0]][pixco[1][1]].r, intensity); if (pixelheat[2] >= 0) PartMatrix_addHeat(pixelheat[2], &colorbuffer[pixco[2][0]][pixco[2][1]].r, intensity); - //PartMatrix_addHeat(pixco[2][0], pixco[2][1], pixelheat[2], intensity); if (pixelheat[3] >= 0) PartMatrix_addHeat(pixelheat[3], &colorbuffer[pixco[3][0]][pixco[3][1]].r, intensity); - //PartMatrix_addHeat(pixco[3][0], pixco[3][1], pixelheat[3], intensity); - // TODO: add heat to a third pixel. need to konw dx and dy, the heatvalue is (flameheat - pixelheat) vom pixel das weiter weg ist vom partikelzentrum + // TODO: add heat to a third pixel? need to konw dx and dy, the heatvalue is (flameheat - pixelheat) vom pixel das weiter weg ist vom partikelzentrum // also wenn dx < halfradius dann links, sonst rechts. rechts flameheat-pixelheat vom linken addieren und umgekehrt // das ist relativ effizient um rechnen und sicher schneller als die alte variante. gibt ein FPS drop, das könnte man aber // mit einer schnelleren add funktion im segment locker ausgleichen @@ -939,15 +913,7 @@ void ParticleSystem::renderParticleFire(uint32_t intensity, bool usepalette) SEGMENT.setPixelColorXY(x, maxYpixel - y, colorbuffer[x][y]); } } - - // Free memory for each row - for (int i = 0; i <= maxXpixel; i++) - { - free(colorbuffer[i]); - } - - // Free memory for the array of pointers to rows - free(colorbuffer); + free(colorbuffer); // free buffer memory } // adds 'heat' to red color channel, if it overflows, add it to next color channel @@ -994,9 +960,7 @@ void ParticleSystem::PartMatrix_addHeat(int32_t heat, uint8_t *currentcolor, uin } if (i == 2) // blue channel was reached, limit the color value so it does not go full white - currentcolor[i] = currentcolor[i] > 60 ? 60 : currentcolor[i]; //faster than min() - - //SEGMENT.setPixelColorXY(col, maxYpixel - row, currentcolor); + currentcolor[i] = currentcolor[i] > 50 ? 50 : currentcolor[i]; //faster than min() } // detect collisions in an array of particles and handle them @@ -1016,10 +980,13 @@ void ParticleSystem::handleCollisions() } collisioncounter++; + //startparticle = 0;//!!! test: do all collisions every frame, + //endparticle = usedParticles; + for (i = startparticle; i < endparticle; i++) { - // go though all 'higher number' particles and see if any of those are in close proximity and if they are, make them collide - if (particles[i].ttl > 0 && particles[i].outofbounds == 0) // if particle is alive and does collide and is not out of view + // go though all 'higher number' particles and see if any of those are in close proximity and if they are, make them collide + if (particles[i].ttl > 0 && particles[i].outofbounds == 0 && particles[i].collide) // if particle is alive and does collide and is not out of view { int32_t dx, dy; // distance to other particles for (j = i + 1; j < usedParticles; j++) @@ -1041,7 +1008,7 @@ void ParticleSystem::handleCollisions() // handle a collision if close proximity is detected, i.e. dx and/or dy smaller than 2*PS_P_RADIUS // takes two pointers to the particles to collide and the particle hardness (softer means more energy lost in collision, 255 means full hard) -void ParticleSystem::collideParticles(PSparticle *particle1, PSparticle *particle2) +void ParticleSystem::collideParticles(PSparticle *particle1, PSparticle *particle2) //TODO: dx,dy is calculated just above, can pass it over here to save some CPU time { int32_t dx = particle2->x - particle1->x; @@ -1055,38 +1022,31 @@ void ParticleSystem::collideParticles(PSparticle *particle1, PSparticle *particl 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 TODO: is this really needed? only happens on fast particles, would save some code (but make it a tiny bit less accurate on fast particles but probably not an issue) - - if (relativeVx < 0) - { // if true, particle2 is on the right side - particle1->x--; - particle2->x++; - } - else - { - particle1->x++; - particle2->x--; - } + distanceSquared++; + } + /* + // Adjust positions based on relative velocity direction -> does not really do any good. + uint32_t add = 1; + if (relativeVx < 0) // if true, particle2 is on the right side + add = -1; + particle1->x += add; + particle2->x -= add; + add = 1; if (relativeVy < 0) - { - particle1->y--; - particle2->y++; - } - else - { - particle1->y++; - particle2->y--; - } + add = -1; + particle1->y += add; + particle2->y -= add; + distanceSquared++; - } + }*/ // Calculate dot product of relative velocity and relative distance int32_t dotProduct = (dx * relativeVx + dy * relativeVy); // If particles are moving towards each other if (dotProduct < 0) { - const uint32_t bitshift = 14; // bitshift used to avoid floats + const uint32_t bitshift = 16; // bitshift used to avoid floats (dx/dy are 7bit, relativV are 8bit -> dotproduct is 15bit so 16bit shift is ok) // Calculate new velocities after collision int32_t impulse = (((dotProduct << (bitshift)) / (distanceSquared)) * collisionHardness) >> 8; @@ -1096,18 +1056,18 @@ void ParticleSystem::collideParticles(PSparticle *particle1, PSparticle *particl particle1->vy += yimpulse; particle2->vx -= ximpulse; particle2->vy -= yimpulse; - - //TODO: this is removed for now as it does not seem to do much and does not help with piling. if soft, much energy is lost anyway at a collision, so they are automatically sticky + //also second version using multiplication is slower on ESP8266 than the if's - if (collisionHardness < 220) // if particles are soft, they become 'sticky' i.e. they are slowed down at collisions + if (collisionHardness < 128) // if particles are soft, they become 'sticky' i.e. they are slowed down at collisions even more { + //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; - const uint32_t coeff = collisionHardness + 20; + const uint32_t coeff = 191 + (collisionHardness>>1); particle1->vx = ((int32_t)particle1->vx * coeff) >> 8; particle1->vy = ((int32_t)particle1->vy * coeff) >> 8; @@ -1116,20 +1076,21 @@ void ParticleSystem::collideParticles(PSparticle *particle1, PSparticle *particl } } - // particles have volume, push particles apart if they are too close by moving each particle by a fixed amount away from the other particle + + // particles have volume, push particles apart if they are too close by moving each particle by a fixed amount away from the other particle // if pushing is made dependent on hardness, things start to oscillate much more, better to just add a fixed, small increment (tried lots of configurations, this one works best) // 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) { uint8_t choice = dotProduct & 0x01; // random16(2); // randomly choose one particle to push, avoids oscillations note: dotprouct LSB should be somewhat random, so no need to calculate a random number const int32_t HARDDIAMETER = 2 * PS_P_HARDRADIUS; - const int32_t pushamount = 2; //push a small amount + const int32_t pushamount = 1; // push a small amount int32_t push = pushamount; if (dx < HARDDIAMETER && dx > -HARDDIAMETER) { // distance is too small, push them apart - if (dx <= 0) + if (dx < 0) push = -pushamount; //-(PS_P_HARDRADIUS + dx); // inverted push direction if (choice) // chose one of the particles to push, avoids oscillations @@ -1141,7 +1102,7 @@ void ParticleSystem::collideParticles(PSparticle *particle1, PSparticle *particl push = pushamount; // reset push variable to 1 if (dy < HARDDIAMETER && dy > -HARDDIAMETER) { - if (dy <= 0) + if (dy < 0) push = -pushamount; //-(PS_P_HARDRADIUS + dy); // inverted push direction if (choice) // chose one of the particles to push, avoids oscillations @@ -1192,6 +1153,25 @@ int32_t ParticleSystem::calcForce_dV(int8_t force, uint8_t* counter) return dv; } +// allocate memory for the 2D array in one contiguous block and set values to zero +CRGB **ParticleSystem::allocate2Dbuffer(uint32_t cols, uint32_t rows) +{ + CRGB ** array2D = (CRGB **)malloc(cols * sizeof(CRGB *) + cols * rows * sizeof(CRGB)); + if (array2D == NULL) + DEBUG_PRINT(F("PS buffer alloc failed")); + else + { + // assign pointers of 2D array + CRGB *start = (CRGB *)(array2D + cols); + for (int i = 0; i < cols; i++) + { + array2D[i] = start + i * rows; + } + memset(start, 0, cols * rows * sizeof(CRGB)); // set all values to zero + } + return array2D; +} + // 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 @@ -1234,6 +1214,24 @@ uint32_t calculateNumberOfParticles() return numberofParticles; } +uint32_t calculateNumberOfSources() +{ + uint32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; + uint32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); +#ifdef ESP8266 + int numberofSources = (cols * rows) / 8; + numberofSources = min(numberofSources, 16); +#elseif ARDUINO_ARCH_ESP32S2 + int numberofSources = (cols * rows) / 6; + numberofSources = min(numberofSources, 32); +#else + int numberofSources = (cols * rows) / 4; + numberofSources = min(numberofSources, 64); +#endif + // TODO: may be too many.. + return numberofSources; +} + //allocate memory for particle system class, particles, sprays plus additional memory requested by FX bool allocateParticleSystemMemory(uint16_t numparticles, uint16_t numsources, uint16_t additionalbytes) { @@ -1247,11 +1245,13 @@ bool allocateParticleSystemMemory(uint16_t numparticles, uint16_t numsources, ui return(SEGMENT.allocateData(requiredmemory)); } -bool initParticleSystem(ParticleSystem *&PartSys, uint16_t numsources) +// initialize Particle System, allocate additional bytes if needed (pointer to those bytes can be read from particle system class: PSdataEnd) +bool initParticleSystem(ParticleSystem *&PartSys, uint16_t additionalbytes) { Serial.println("PS init function"); uint32_t numparticles = calculateNumberOfParticles(); - if (!allocateParticleSystemMemory(numparticles, numsources, 0)) + uint32_t numsources = calculateNumberOfSources(); + if (!allocateParticleSystemMemory(numparticles, numsources, additionalbytes)) { DEBUG_PRINT(F("PS init failed: memory depleted")); return false; diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index 6dc5b4f2c3..76d4d201ac 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -48,27 +48,13 @@ typedef struct { //two byte bit field: //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 flag1 : 1; // unused flags... - bool flag2 : 1; - bool flag3 : 1; + bool collide : 1; //if set, particle takes part in collisions + bool flag3 : 1; // unused flags... + bool flag4 : 1; uint16_t ttl; // time to live, 12 bit or 4095 max (which is 50s at 80FPS) } PSparticle; -// struct for a single particle -typedef struct -{ - int16_t x; // x position in particle system - int16_t y; // y position in particle system - int8_t vx; // horizontal velocity - int8_t vy; // vertical velocity - uint8_t hue; // color hue - // two byte bit field: - bool outofbounds : 1; // out of bounds flag, set to true if particle is outside of display area - uint16_t ttl : 7; // time to live, 7 bit or 128 max (need to adjust fire animation to not exceed this value! max is now 137 because of +10 -> done einfach durch zwei geteilt.) -} PSfireparticle; -//todo: wenn man reduzierte partikel verwenet, kann man das nicht mit palette rendern. erst ausprobieren, ob das gut aussieht, dann entscheiden. ist vermutlich keine gute idee... - //struct for a particle source typedef struct { uint16_t minLife; //minimum ttl of emittet particles @@ -112,6 +98,7 @@ class ParticleSystem //particle physics void applyGravity(PSparticle *part, uint32_t numarticles, uint8_t force, uint8_t *counter); void applyGravity(PSparticle *part, uint32_t numarticles, uint8_t *counter); //use global gforce + void applyGravity(PSparticle *part); //use global system settings void applyForce(PSparticle *part, uint32_t numparticles, int8_t xforce, int8_t yforce, uint8_t *counter); void applyAngleForce(PSparticle *part, uint32_t numparticles, uint8_t force, uint8_t angle, uint8_t *counter); void applyFriction(PSparticle *part, uint8_t coefficient); // apply friction to specific particle @@ -139,6 +126,7 @@ class ParticleSystem 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) private: //rendering functions @@ -156,6 +144,7 @@ class ParticleSystem void initPSpointers(); // call this after allocating the memory to initialize the pointers int32_t wraparound(int32_t w, int32_t maxvalue); int32_t calcForce_dV(int8_t force, uint8_t *counter); + CRGB **allocate2Dbuffer(uint32_t cols, uint32_t rows); // note: variables that are accessed often are 32bit for speed uint32_t emitIndex; // index to count through particles to emit so searching for dead pixels is faster @@ -164,12 +153,12 @@ class ParticleSystem uint8_t gforcecounter; //counter for global gravity uint8_t gforce; //gravity strength, default is 8 uint8_t collisioncounter; //counter to handle collisions - PSsettings particlesettings; // settings used when updating particles }; //initialization functions (not part of class) -bool initParticleSystem(ParticleSystem *&PartSys, uint16_t numsources); +bool initParticleSystem(ParticleSystem *&PartSys, uint16_t additionalbytes); uint32_t calculateNumberOfParticles(); +uint32_t calculateNumberOfSources(); bool allocateParticleSystemMemory(uint16_t numparticles, uint16_t numsources, uint16_t additionalbytes); //color add function CRGB fast_color_add(CRGB c1, CRGB c2, uint32_t scale); // fast and accurate color adding with scaling (scales c2 before adding) \ No newline at end of file