diff --git a/wled00/FX.cpp b/wled00/FX.cpp index e846ae13e3..5a61eb7b2e 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -7890,58 +7890,63 @@ static const char _data_FX_MODE_2DWAVINGCELL[] PROGMEM = "Waving Cell@!,,Amplitu uint16_t mode_particlerotatingspray(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(); - -#ifdef ESP8266 - const uint32_t numParticles = 170; // maximum number of particles -#else - const uint32_t numParticles = 700; // maximum number of particles -#endif - const uint8_t numSprays = 8; // maximum number of sprays + ParticleSystem *PartSys = NULL; - PSparticle *particles; - PSpointsource *spray; - - // allocate memory and divide it into proper pointers, max is 32kB for all segments, 100 particles use 1200bytes - uint32_t dataSize = sizeof(PSparticle) * numParticles; - dataSize += sizeof(PSpointsource) * (numSprays); - if (!SEGENV.allocateData(dataSize)) - return mode_static(); // allocation failed; //allocation failed - - spray = reinterpret_cast(SEGENV.data); - // calculate the end of the spray data and assign it as the data pointer for the particles: - particles = reinterpret_cast(spray + numSprays); // cast the data array into a particle pointer + 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)) + return mode_static(); // allocation failed; //allocation failed + + // Serial.print("PS pointer "); + // Serial.println((uintptr_t)PartSys); + // Serial.print("set pointer to data "); + // PartSys = reinterpret_cast(SEGENV.data); // set the pointer to the PS (todo: is done in init function but wiped when leaving it) + // Serial.println((uintptr_t)PartSys); + // Serial.print("SEGdata "); + // Serial.println((uintptr_t)(SEGENV.data)); + PartSys->setKillOutOfBounds(true); + Serial.println("INIT done"); + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + 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) + } uint32_t i = 0; uint32_t j = 0; + uint8_t spraycount = 1 + (SEGMENT.custom1 >> 5); // number of sprays to display, 1-8 if (SEGMENT.call == 0) // initialization { SEGMENT.aux0 = 0; // starting angle SEGMENT.aux1 = 0x01; // check flags - for (i = 0; i < numParticles; i++) - { - particles[i].ttl = 0; - } +//TODO: use SEGMENT.step for smooth direction change for (i = 0; i < numSprays; i++) - { - spray[i].source.sat = 255; // set saturation - spray[i].source.x = (cols * PS_P_RADIUS) / 2; // center - spray[i].source.y = (rows * PS_P_RADIUS) / 2; // center - spray[i].source.vx = 0; - spray[i].source.vy = 0; - spray[i].maxLife = 400; - spray[i].minLife = 200; - spray[i].vx = 0; // emitting speed - spray[i].vy = 0; // emitting speed - spray[i].var = 0; // emitting variation + { + PartSys->sources[i].source.sat = 255; // set saturation + PartSys->sources[i].source.x = (PartSys->maxX - PS_P_HALFRADIUS + 1) >> 1; // center + PartSys->sources[i].source.y = (PartSys->maxY - PS_P_HALFRADIUS + 1) >> 1; // center + PartSys->sources[i].source.vx = 0; + PartSys->sources[i].source.vy = 0; + PartSys->sources[i].maxLife = 900; + PartSys->sources[i].minLife = 800;//!!! + PartSys->sources[i].vx = 0; // emitting speed + PartSys->sources[i].vy = 0; // emitting speed + PartSys->sources[i].var = 0; // emitting variation + if (SEGMENT.check1) // random color is checked + PartSys->sources[i].source.hue = random16(); + else + { + uint8_t coloroffset = 0xFF / spraycount; + PartSys->sources[i].source.hue = coloroffset * i; + } } } @@ -7956,38 +7961,16 @@ uint16_t mode_particlerotatingspray(void) { if (SEGMENT.check1) // random color is checked { - spray[i].source.hue = random16(); + PartSys->sources[i].source.hue = random16(); } else { uint8_t coloroffset = 0xFF / spraycount; - spray[i].source.hue = coloroffset * i; + PartSys->sources[i].source.hue = coloroffset * i; } } } - uint8_t percycle = spraycount; // maximum number of particles emitted per cycle - - #ifdef ESP8266 - if (SEGMENT.call & 0x01) //every other frame, do not emit to save particles - percycle = 0; -#endif - i = 0; - j = random16(spraycount); // start with random spray so all get a chance to emit a particle if maximum number of particles alive is reached. - while (i < numParticles) - { - if (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]); - j = (j + 1) % spraycount; - if (percycle-- == 0) - { - break; // quit loop if all particles of this round emitted - } - } - i++; - } //set rotation direction and speed int32_t rotationspeed = SEGMENT.speed << 2; @@ -8018,27 +8001,62 @@ uint16_t mode_particlerotatingspray(void) else SEGMENT.aux0 -= rotationspeed << 2; - // calculate angle offset for an even distribution - uint16_t angleoffset = 0xFFFF / spraycount; - - for (i = 0; i < spraycount; i++) +/* +//DEBUG: emit single particles in x,y + if (SEGMENT.call % 3000 == 0) { - // 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 - spray[i].vx = (cos16(SEGMENT.aux0 + angleoffset * i) >> 8) / ((263 - SEGMENT.intensity) >> 3); // update spray angle (rotate all sprays with angle offset) - spray[i].vy = (sin16(SEGMENT.aux0 + angleoffset * i) >> 8) / ((263 - SEGMENT.intensity) >> 3); // update spray angle (rotate all sprays with angle offset) - spray[i].var = (SEGMENT.custom3 >> 1); // emiting variation = nozzle size (custom 3 goes from 0-32) + PartSys->sources[0].vx = -1; // emitting speed + PartSys->sources[0].vy = 0; // emitting speed + PartSys->sources[0].var = 0; // emitting variation + PartSys->SprayEmit(PartSys->sources[0]); + + PartSys->sources[1].vx = 1; // emitting speed + PartSys->sources[1].vy = 0; // emitting speed + PartSys->sources[1].var =0; // emitting variation + PartSys->SprayEmit(PartSys->sources[1]); + PartSys->sources[2].vx = 0; // emitting speed + PartSys->sources[2].vy = 1; // emitting speed + PartSys->sources[2].var = 0; // emitting variation + PartSys->SprayEmit(PartSys->sources[2]); + PartSys->sources[3].vx = 0; // emitting speed + PartSys->sources[3].vy = -1; // emitting speed + PartSys->sources[3].var = 0; // emitting variation + PartSys->SprayEmit(PartSys->sources[3]); + }*/ + // calculate angle offset for an even distribution + 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 (i = 0; i < numParticles; i++) +#ifdef ESP8266 + if (SEGMENT.call & 0x01) // every other frame, do not emit to save particles + percycle = 0; +#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) + + j = random16(spraycount); // start with random spray so all get a chance to emit a particle if maximum number of particles alive is reached. + + for (i = 0; i < spraycount; i++) // emit one particle per spray (if available) { - Particle_Move_update(&particles[i], true); // move the particles, kill out of bounds particles + PartSys->SprayEmit(PartSys->sources[j]); + j = (j + 1) % spraycount; + // if (++j > spraycount) // faster than modulo, avoid modulo it in a loop !!! todo: add this back? + // j = 0; } SEGMENT.fill(BLACK); // clear the matrix - // render the particles - ParticleSys_render(particles, numParticles, false, false); + + PartSys->update(); //update all particles and render to frame + return FRAMETIME; -} + } static const char _data_FX_MODE_PARTICLEROTATINGSPRAY[] PROGMEM = "PS Candy@Rotation Speed,Particle Speed,Arms,Flip Speed,Nozzle,Random Color, Direction, Random Flip;;!;012;pal=56,sx=18,ix=190,c1=200,c2=0,c3=0,o1=0,o2=0,o3=0"; /* @@ -8047,6 +8065,7 @@ static const char _data_FX_MODE_PARTICLEROTATINGSPRAY[] PROGMEM = "PS Candy@Rota * Uses ranbow palette as default * by DedeHai (Damian Schneider) */ +/* uint16_t mode_particlefireworks(void) { if (SEGLEN == 1) @@ -8063,17 +8082,17 @@ uint16_t mode_particlefireworks(void) #endif const uint8_t numRockets = 4; PSparticle *particles; - PSpointsource *rockets; + PSsource *rockets; // allocate memory and divide it into proper pointers, max is 32k for all segments. uint32_t dataSize = sizeof(PSparticle) * numParticles; - dataSize += sizeof(PSpointsource) * (numRockets); + dataSize += sizeof(PSsource) * (numRockets); if (!SEGENV.allocateData(dataSize)) return mode_static(); // allocation failed; //allocation failed - rockets = reinterpret_cast(SEGENV.data); + rockets = reinterpret_cast(SEGENV.data); // calculate the end of the spray data and assign it as the data pointer for the particles: particles = reinterpret_cast(rockets + numRockets); // cast the data array into a particle pointer uint32_t i = 0; - uint32_t j = 0; + uint32_t j = 0; if (SEGMENT.call == 0) // initialization { for (i = 0; i < numParticles; i++) @@ -8096,7 +8115,7 @@ uint16_t mode_particlefireworks(void) uint32_t counter; uint32_t angleincrement; uint32_t speedvariation; - + bool circularexplosion = false; for (j = 0; j < numRockets; j++) { @@ -8116,8 +8135,8 @@ uint16_t mode_particlefireworks(void) #else emitparticles = random16(SEGMENT.intensity >> 2) + (SEGMENT.intensity >> 2) + 5; // defines the size of the explosion #endif - rockets[j].source.vy = -1; // set speed negative so it will emit no more particles after this explosion until relaunch - if(random16(4) == 0) //!!! make it 5 + rockets[j].source.vy = -1; // set speed negative so it will emit no more particles after this explosion until relaunch + if(random16(4) == 0) //!!! make it 5 { circularexplosion = true; speed = 2 + random16(3); @@ -8146,7 +8165,7 @@ uint16_t mode_particlefireworks(void) if (circularexplosion) // do circle emit { Emitter_Angle_emit(&rockets[j], &particles[i], angle, currentspeed); - emitparticles--; + emitparticles--; // set angle for next particle angle += angleincrement; counter++; @@ -8180,7 +8199,7 @@ uint16_t mode_particlefireworks(void) } circularexplosion = false; //reset for next rocket } - + // update particles for (i = 0; i < numParticles; i++) { @@ -8203,7 +8222,7 @@ uint16_t mode_particlefireworks(void) rockets[j].source.sat = random16(100) + 155; rockets[j].maxLife = 200; rockets[j].minLife = 50; - rockets[j].source.ttl = random16((1024 - ((uint32_t)SEGMENT.speed<<2))) + 50; // standby time til next launch + rockets[j].source.ttl = random16((1024 - ((uint32_t)SEGMENT.speed<<2))) + 50; // standby time til next launch rockets[j].vx = 0; // emitting speed rockets[j].vy = 3; // emitting speed rockets[j].var = (SEGMENT.intensity >> 3) + 10; // speed variation around vx,vy (+/- var/2) @@ -8230,9 +8249,10 @@ uint16_t mode_particlefireworks(void) ParticleSys_render(particles, numParticles, false, false); return FRAMETIME; } + //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) * Particles are sprayed from below, spray moves back and forth if option is set @@ -8240,9 +8260,9 @@ static const char _data_FX_MODE_PARTICLEFIREWORKS[] PROGMEM = "PS Fireworks@Laun * by DedeHai (Damian Schneider) */ +/* uint16_t mode_particlevolcano(void) { - if (SEGLEN == 1) return mode_static(); @@ -8261,15 +8281,15 @@ uint16_t mode_particlevolcano(void) uint8_t percycle = numSprays; // maximum number of particles emitted per cycle PSparticle *particles; - PSpointsource *spray; + PSsource *spray; // allocate memory and divide it into proper pointers, max is 32k for all segments. uint32_t dataSize = sizeof(PSparticle) * numParticles; - dataSize += sizeof(PSpointsource) * (numSprays); + dataSize += sizeof(PSsource) * (numSprays); if (!SEGENV.allocateData(dataSize)) return mode_static(); // allocation failed; //allocation failed - spray = reinterpret_cast(SEGENV.data); + spray = reinterpret_cast(SEGENV.data); // calculate the end of the spray data and assign it as the data pointer for the particles: particles = reinterpret_cast(spray + numSprays); // cast the data array into a particle pointer @@ -8368,70 +8388,15 @@ uint16_t mode_particlevolcano(void) return FRAMETIME; } static const char _data_FX_MODE_PARTICLEVOLCANO[] PROGMEM = "PS Volcano@Speed,Intensity,Move,Bounce,Size,Color by Age,Walls,Collisions;;!;012;pal=35,sx=100,ix=160,c1=0,c2=160,c3=10,o1=1,o2=0,o3=0"; - -//for debugging speed tests, this speed test function can be used, compiler will not optimize it -void __attribute__((optimize("O0"))) SpeedTestfunction(void) -{ - // unmodifiable compiler code - Serial.print("Speedtest: "); - int32_t i; - volatile int32_t randomnumber; - uint32_t start = micros(); - uint32_t time; - volatile int32_t windspeed; - for (i = 0; i < 100000; i++) - { - //windspeed=(inoise16(SEGMENT.aux0, start >> 2) - 127) / ((271 - SEGMENT.custom2) >> 4); - randomnumber = random8(); - } - time = micros() - start; - Serial.print(time); - Serial.print(" "); - start = micros(); - for (i = 0; i < 100000; i++) - { - //windspeed = (inoise8(SEGMENT.aux0, start >> 2) - 127) / ((271 - SEGMENT.custom2) >> 4); - randomnumber = random8(15); - } - time = micros() - start; - Serial.print(time); - Serial.print(" "); - - start = micros(); - for (i = 0; i < 100000; i++) - { - // windspeed=(inoise16(SEGMENT.aux0, start >> 2) - 127) / ((271 - SEGMENT.custom2) >> 4); - randomnumber = random16(); - } - time = micros() - start; - Serial.print(time); - Serial.print(" "); - start = micros(); - for (i = 0; i < 100000; i++) - { - // windspeed = (inoise8(SEGMENT.aux0, start >> 2) - 127) / ((271 - SEGMENT.custom2) >> 4); - randomnumber = random16(15); - } - - time = micros() - start; - Serial.print(time); - Serial.print(" "); - - Serial.println(" ***"); -} - +*/ /* * Particle Fire * realistic fire effect using particles. heat based and using perlin-noise for wind * by DedeHai (Damian Schneider) */ - +/* uint16_t mode_particlefire(void) { - - // speed tests : - // SpeedTestfunction(); - if (SEGLEN == 1) return mode_static(); @@ -8454,18 +8419,18 @@ uint16_t mode_particlefire(void) PSparticle *particles; - PSpointsource *flames; + PSsource *flames; // allocate memory and divide it into proper pointers uint32_t dataSize = sizeof(PSparticle) * numParticles; - dataSize += sizeof(PSpointsource) * (numFlames); + dataSize += sizeof(PSsource) * (numFlames); if (!SEGENV.allocateData(dataSize)) { return mode_static(); // allocation failed; //allocation failed } - flames = reinterpret_cast(SEGENV.data); + flames = reinterpret_cast(SEGENV.data); // calculate the end of the spray data and assign it as the data pointer for the particles: particles = reinterpret_cast(flames + numFlames); // cast the data array into a particle pointer @@ -8516,7 +8481,7 @@ uint16_t mode_particlefire(void) if (i < numNormalFlames) { - flames[i].source.ttl = random16((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 + flames[i].source.ttl = random16((SEGMENT.intensity * SEGMENT.intensity) >> 9) / (2 + (SEGMENT.speed >> 6)) + 10; //'hotness' of fire, faster flames reduce the effect or flame height will scale too much with speed flames[i].maxLife = random16(7) + 13; // defines flame height together with the vy speed, vy speed*maxlife/PS_P_RADIUS is the average flame height flames[i].minLife = 4; flames[i].vx = (int8_t)random16(4) - 2; // emitting speed (sideways) @@ -8563,7 +8528,7 @@ uint16_t mode_particlefire(void) else if (particles[i].y > PS_P_RADIUS) // particle is alive, apply wind if y > 1 { // add wind using perlin noise - particles[i].vx = windspeed; + particles[i].vx = windspeed; //todo: should this be depending on position? would be slower but may look better (used in old, slow fire) } } @@ -8577,7 +8542,7 @@ uint16_t mode_particlefire(void) return FRAMETIME; } static const char _data_FX_MODE_PARTICLEFIRE[] PROGMEM = "PS Fire@Speed,Intensity,Base Flames,Wind,Color Scheme, Cylinder;;!;012;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 sliders control falling speed, intensity (number of particles spawned), inter-particle collision hardness (0 means no particle collisions) and render saturation @@ -8585,10 +8550,9 @@ this is quite versatile, can be made to look like rain or snow or confetti etc. Uses palette for particle color by DedeHai (Damian Schneider) */ - +/* uint16_t mode_particlefall(void) { - if (SEGLEN == 1) return mode_static(); @@ -8641,7 +8605,7 @@ uint16_t mode_particlefall(void) particles[i].vy = -(SEGMENT.speed >> 1); particles[i].hue = random16(); // set random color particles[i].sat = ((SEGMENT.custom3) << 3) + 7; // set saturation - particles[i].collide = true; // particle will collide + break; //emit only one particle per round } i++; @@ -8674,16 +8638,15 @@ uint16_t mode_particlefall(void) 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"; - +*/ /* * Particle Waterfall * Uses palette for particle color, spray source at top emitting particles, many config options * by DedeHai (Damian Schneider) */ - +/* uint16_t mode_particlewaterfall(void) { - if (SEGLEN == 1) return mode_static(); @@ -8701,15 +8664,15 @@ uint16_t mode_particlewaterfall(void) uint8_t percycle = numSprays; // maximum number of particles emitted per cycle PSparticle *particles; - PSpointsource *spray; + PSsource *spray; // allocate memory and divide it into proper pointers, max is 32k for all segments. uint32_t dataSize = sizeof(PSparticle) * numParticles; - dataSize += sizeof(PSpointsource) * (numSprays); + dataSize += sizeof(PSsource) * (numSprays); if (!SEGENV.allocateData(dataSize)) return mode_static(); // allocation failed; //allocation failed - spray = reinterpret_cast(SEGENV.data); + spray = reinterpret_cast(SEGENV.data); // calculate the end of the spray data and assign it as the data pointer for the particles: particles = reinterpret_cast(spray + numSprays); // cast the data array into a particle pointer @@ -8808,13 +8771,13 @@ uint16_t mode_particlewaterfall(void) return FRAMETIME; } static const char _data_FX_MODE_PARTICLEWATERFALL[] PROGMEM = "PS Waterfall@Speed,Intensity,Variation,Collisions,Position,Cylinder,Walls,Ground;;!;012;pal=9,sx=15,ix=200,c1=15,c2=128,c3=17,o1=0,o2=0,o3=1"; - +*/ /* Particle Box, applies gravity to particles in either a random direction or random but only downwards (sloshing) Uses palette for particle color by DedeHai (Damian Schneider) */ - +/* uint16_t mode_particlebox(void) { if (SEGLEN == 1) @@ -8911,13 +8874,13 @@ uint16_t mode_particlebox(void) return FRAMETIME; } static const char _data_FX_MODE_PARTICLEBOX[] PROGMEM = "PS Box@Speed,Particles,Tilt strength,Hardness,,Sloshing;;!;012;pal=1,sx=120,ix=100,c1=190,c2=210,o1=0"; - +*/ /* Fuzzy Noise: Perlin noise 'gravity' mapping as in particles on 'noise hills' viewed from above calculates slope gradient at the particle positions and applies 'downhill' force, restulting in a fuzzy perlin noise display by DedeHai (Damian Schneider) */ - +/* uint16_t mode_particleperlin(void) { @@ -9004,12 +8967,12 @@ uint16_t mode_particleperlin(void) return FRAMETIME; } static const char _data_FX_MODE_PARTICLEPERLIN[] PROGMEM = "PS Fuzzy Noise@Speed,Particles,,Friction,Scale;;!;012;pal=54,sx=70;ix=200,c1=120,c2=120,c3=4,o1=0"; - +*/ /* * 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) @@ -9031,15 +8994,15 @@ uint16_t mode_particleimpact(void) #endif PSparticle *particles; - PSpointsource *meteors; + PSsource *meteors; // allocate memory and divide it into proper pointers, max is 32k for all segments. uint32_t dataSize = sizeof(PSparticle) * numParticles; - dataSize += sizeof(PSpointsource) * (MaxNumMeteors); + dataSize += sizeof(PSsource) * (MaxNumMeteors); if (!SEGENV.allocateData(dataSize)) return mode_static(); // allocation failed; //allocation failed - meteors = reinterpret_cast(SEGENV.data); + 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 @@ -9135,8 +9098,7 @@ uint16_t mode_particleimpact(void) { 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].source.collide = true; // explosion particles will collide if checked + meteors[i].source.y = 5; // offset from ground so explosion happens not out of frame meteors[i].maxLife = 200; meteors[i].minLife = 50; #ifdef ESP8266 @@ -9173,14 +9135,14 @@ uint16_t mode_particleimpact(void) 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 Uses palette for particle color by DedeHai (Damian Schneider) */ - +/* uint16_t mode_particleattractor(void) { if (SEGLEN == 1) return mode_static(); @@ -9200,13 +9162,13 @@ uint16_t mode_particleattractor(void) PSparticle *particles; PSparticle *attractor; - PSpointsource *spray; + PSsource *spray; uint8_t *counters; //counters for the applied force // allocate memory and divide it into proper pointers, max is 32k for all segments. uint32_t dataSize = sizeof(PSparticle) * (numParticles+1); //space for particles and the attractor dataSize += sizeof(uint8_t) * numParticles; //space for counters - dataSize += sizeof(PSpointsource); //space for spray + dataSize += sizeof(PSsource); //space for spray if (!SEGENV.allocateData(dataSize)) @@ -9214,7 +9176,7 @@ uint16_t mode_particleattractor(void) // divide and cast the data array into correct pointers particles = reinterpret_cast(SEGENV.data); attractor = reinterpret_cast(particles + numParticles + 1); - spray = reinterpret_cast(attractor + 1); + spray = reinterpret_cast(attractor + 1); counters = reinterpret_cast(spray + 1); uint32_t i; @@ -9307,13 +9269,13 @@ uint16_t mode_particleattractor(void) return FRAMETIME; } static const char _data_FX_MODE_PARTICLEATTRACTOR[] PROGMEM = "PS Attractor@Mass,Particles,Emit Speed,Collisions,Friction,Bounce,Trails,Swallow;;!;012;pal=9,sx=100,ix=82,c1=190,c2=0,o1=0,o2=0,o3=0"; - +*/ /* Particle Spray, just a simple spray animation with many parameters Uses palette for particle color by DedeHai (Damian Schneider) */ - +/* uint16_t mode_particlespray(void) { @@ -9336,15 +9298,15 @@ uint16_t mode_particlespray(void) uint8_t percycle = numSprays; // maximum number of particles emitted per cycle PSparticle *particles; - PSpointsource *spray; + PSsource *spray; // allocate memory and divide it into proper pointers, max is 32k for all segments. uint32_t dataSize = sizeof(PSparticle) * numParticles; - dataSize += sizeof(PSpointsource) * (numSprays); + dataSize += sizeof(PSsource) * (numSprays); if (!SEGENV.allocateData(dataSize)) return mode_static(); // allocation failed; //allocation failed - spray = reinterpret_cast(SEGENV.data); + spray = reinterpret_cast(SEGENV.data); // calculate the end of the spray data and assign it as the data pointer for the particles: particles = reinterpret_cast(spray + numSprays); // cast the data array into a particle pointer @@ -9428,13 +9390,13 @@ uint16_t mode_particlespray(void) return FRAMETIME; } 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) { @@ -9558,7 +9520,7 @@ uint32_t random16_ESP(uint32_t limit) r = p >> 16; return r; } - +*/ /* * Particle rotating GEQ * Particles sprayed from center with a rotating spray @@ -9584,15 +9546,15 @@ uint16_t mode_particlecenterGEQ(void) const uint8_t numSprays = 16; // maximum number of sprays PSparticle *particles; - PSpointsource *spray; + PSsource *spray; // allocate memory and divide it into proper pointers, max is 32kB for all segments, 100 particles use 1200bytes uint32_t dataSize = sizeof(PSparticle) * numParticles; - dataSize += sizeof(PSpointsource) * (numSprays); + dataSize += sizeof(PSsource) * (numSprays); if (!SEGENV.allocateData(dataSize)) return mode_static(); // allocation failed; //allocation failed - spray = reinterpret_cast(SEGENV.data); + spray = reinterpret_cast(SEGENV.data); // calculate the end of the spray data and assign it as the data pointer for the particles: particles = reinterpret_cast(spray + numSprays); // cast the data array into a particle pointer @@ -9932,11 +9894,11 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_2DAKEMI, &mode_2DAkemi, _data_FX_MODE_2DAKEMI); // audio - + addEffect(FX_MODE_PARTICLEROTATINGSPRAY, &mode_particlerotatingspray, _data_FX_MODE_PARTICLEROTATINGSPRAY); + /* addEffect(FX_MODE_PARTICLEVOLCANO, &mode_particlevolcano, _data_FX_MODE_PARTICLEVOLCANO); addEffect(FX_MODE_PARTICLEFIRE, &mode_particlefire, _data_FX_MODE_PARTICLEFIRE); addEffect(FX_MODE_PARTICLEFIREWORKS, &mode_particlefireworks, _data_FX_MODE_PARTICLEFIREWORKS); - addEffect(FX_MODE_PARTICLEROTATINGSPRAY, &mode_particlerotatingspray, _data_FX_MODE_PARTICLEROTATINGSPRAY); addEffect(FX_MODE_PARTICLEPERLIN, &mode_particleperlin, _data_FX_MODE_PARTICLEPERLIN); addEffect(FX_MODE_PARTICLEFALL, &mode_particlefall, _data_FX_MODE_PARTICLEFALL); addEffect(FX_MODE_PARTICLEBOX, &mode_particlebox, _data_FX_MODE_PARTICLEBOX); @@ -9945,6 +9907,7 @@ 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_PARTICLECENTERGEQ, &mode_particlecenterGEQ, _data_FX_MODE_PARTICLECCIRCULARGEQ); #endif // WLED_DISABLE_2D diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index 3566755f0f..5568fcff15 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -151,7 +151,8 @@ bool IRAM_ATTR Segment::allocateData(size_t len) { if (call == 0) memset(data, 0, len); // erase buffer if called during effect initialisation return true; } - //DEBUG_PRINTF_P(PSTR("-- Allocating data (%d): %p\n", len, this); + DEBUG_PRINT(F("Allocating Data")); + // DEBUG_PRINTF_P(PSTR("-- Allocating data (%d): %p\n", len, this); deallocateData(); // if the old buffer was smaller release it first if (Segment::getUsedSegmentData() + len > MAX_SEGMENT_DATA) { // not enough memory diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index eb1fa972e6..3a307f4707 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -3,7 +3,6 @@ Particle system with functions for particle generation, particle movement and particle rendering to RGB matrix. by DedeHai (Damian Schneider) 2013-2024 - Rendering is based on algorithm by giladaya, https://github.com/giladaya/arduino-particle-sys LICENSE The MIT License (MIT) @@ -32,45 +31,383 @@ this should be used to optimize speed but not if memory is affected much */ +/* + TODO: + -init funktion für sprays: alles auf null setzen, dann muss man im FX nur noch setzten was man braucht + -pass all pointers by reference to make it consistene throughout the code (or not?) + -add local buffer for faster rendering (-> it is allowed to do so) -> run a test, it crashes. need to find out why exatly + -add possiblity to emit more than one particle, just pass a source and the amount to emit or even add several sources and the amount, function decides if it should do it fair or not + -add an x/y struct, do particle rendering using that, much easier to read + -extend rendering to more than 2x2, 3x2 (fire) should be easy, 3x3 maybe also doable without using much math (need to see if it looks good) + -das system udpate kann fire nicht handlen, es braucht auch noch ein fire update. die funktion kann einen parameter nehmen mit 'use palette' + //todo: eine funktion für init fire? dann wäre der FX etwas aufgeräumter... + -need a random emit? one that does not need an emitter but just takes some properties, so FX can implement their own emitters? + -line emit wäre noch was, der die PS source anders interpretiert + +*/ +// sources need to be updatable by the FX, so functions are needed to apply it to a single particle that are public #include "FXparticleSystem.h" #include "wled.h" #include "FastLED.h" #include "FX.h" -// Fountain style emitter for particles used for flames (particle TTL depends on source TTL) -void Emitter_Flame_emit(PSpointsource *emitter, PSparticle *part) +ParticleSystem::ParticleSystem(uint16_t width, uint16_t height, uint16_t numberofparticles, uint16_t numberofsources) +{ + Serial.print("initializing PS... "); + + 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 + setPSpointers(numberofsources); // 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 + } + Serial.println("PS Constructor done"); +} + +//update function applies gravity, moves the particles, handles collisions and renders the particles +void ParticleSystem::update(void) +{ + uint32_t i; + //apply gravity globally if enabled + if (particlesettings.useGravity) + applyGravity(particles, usedParticles, gforce, &gforcecounter); + + //move all particles + for (i = 0; i < usedParticles; i++) + { + ParticleMoveUpdate(particles[i], particlesettings); + } + + //handle collisions after moving the particles + if (particlesettings.useCollisions) + handleCollisions(); + + //render the particles + ParticleSys_render(); +} + +//update function for fire animation +void ParticleSystem::updateFire(uint8_t colormode) +{ + + // update all fire particles + FireParticle_update(); + + // render the particles + renderParticleFire(colormode); +} + +void ParticleSystem::setUsedParticles(uint32_t num) +{ + usedParticles = min(num, numParticles); //limit to max particles +} + +void ParticleSystem::setWallHardness(uint8_t hardness) +{ + wallHardness = hardness + 1; // at a value of 256, no energy is lost in collisions +} + +void ParticleSystem::setCollisionHardness(uint8_t hardness) +{ + collisionHardness = hardness + 1; // at a value of 256, no energy is lost in collisions +} + +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 + PS_P_HALFRADIUS - 1; // particle system boundaries, allow them to exist one pixel out of boundaries for smooth leaving/entering when kill out of bounds is set + maxY = y * PS_P_RADIUS + PS_P_HALFRADIUS - 1; // it is faster to add this here then on every signle out of bounds check, is deducted when wrapping / bouncing +} + +void ParticleSystem::setWrapX(bool enable) +{ + particlesettings.wrapX = enable; +} + +void ParticleSystem::setWrapY(bool enable) +{ + particlesettings.wrapY = enable; +} + +void ParticleSystem::setBounceX(bool enable) +{ + particlesettings.bounceX = enable; +} + +void ParticleSystem::setBounceY(bool enable) +{ + particlesettings.bounceY = enable; +} + +void ParticleSystem::setKillOutOfBounds(bool enable) +{ + particlesettings.killoutofbounds = enable; +} + +// enable/disable gravity, optionally, set the force (force=8 is default) can be 1-255, 0 is also disable +// if enabled, gravity is applied to all particles in ParticleSystemUpdate() +void ParticleSystem::enableGravity(bool enable, uint8_t force) +{ + particlesettings.useGravity = enable; + if (force > 0) + gforce = force; + else + particlesettings.useGravity = false; + +} + +void ParticleSystem::enableParticleCollisions(bool enable, uint8_t hardness) // enable/disable gravity, optionally, set the force (force=8 is default) can be 1-255, 0 is also disable +{ + particlesettings.useCollisions = enable; + collisionHardness = hardness + 1; +} + +int16_t ParticleSystem::getMaxParticles(void) { - part->x = emitter->source.x + random16(PS_P_RADIUS) - PS_P_HALFRADIUS; // jitter the flame by one pixel to make the flames wider and softer - part->y = emitter->source.y; - part->vx = emitter->vx + random16(emitter->var) - (emitter->var >> 1); - part->vy = emitter->vy + random16(emitter->var) - (emitter->var >> 1); - part->ttl = (uint8_t)((rand() % (emitter->maxLife - emitter->minLife)) + emitter->minLife + emitter->source.ttl); // flame intensity dies down with emitter TTL - // part->hue = emitter->source.hue; //fire uses ttl and not hue for heat - // part->sat = emitter->source.sat; //flame does not use saturation + return numParticles; +} + +// Spray emitter for particles used for flames (particle TTL depends on source TTL) +void ParticleSystem::FlameEmit(PSsource &emitter) +{ + for (uint32_t i = 0; i < usedParticles; i++) + { + emitIndex++; + if (emitIndex >= usedParticles) + emitIndex = 0; + if (particles[emitIndex].ttl == 0) // find a dead particle + { + particles[emitIndex].x = emitter.source.x + random16(PS_P_RADIUS) - PS_P_HALFRADIUS; // jitter the flame by one pixel to make the flames wider and softer + particles[emitIndex].y = emitter.source.y; + particles[emitIndex].vx = emitter.vx + random16(emitter.var) - (emitter.var >> 1); + particles[emitIndex].vy = emitter.vy + random16(emitter.var) - (emitter.var >> 1); + particles[emitIndex].ttl = random16(emitter.maxLife - emitter.minLife) + emitter.minLife + emitter.source.ttl; // flame intensity dies down with emitter TTL + // fire uses ttl and not hue for heat, so no need to set the hue + break; //done + } + } } -// fountain style emitter -void Emitter_Fountain_emit(PSpointsource *emitter, PSparticle *part) +// emit one particle with variation +void ParticleSystem::SprayEmit(PSsource &emitter) { - part->x = emitter->source.x; // + random16(emitter->var) - (emitter->var >> 1); //randomness uses cpu cycles and is almost invisible, removed for now. - part->y = emitter->source.y; // + random16(emitter->var) - (emitter->var >> 1); - part->vx = emitter->vx + random16(emitter->var) - (emitter->var >> 1); - part->vy = emitter->vy + random16(emitter->var) - (emitter->var >> 1); - part->ttl = random16(emitter->maxLife - emitter->minLife) + emitter->minLife; - part->hue = emitter->source.hue; - part->sat = emitter->source.sat; - part->collide = emitter->source.collide; + for (uint32_t i = 0; i < usedParticles; i++) + { + emitIndex++; + if (emitIndex >= usedParticles) + emitIndex = 0; + if (particles[emitIndex].ttl == 0) // find a dead particle + { + 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 + random16(emitter.var) - (emitter.var>>1); + particles[emitIndex].vy = emitter.vy + random16(emitter.var) - (emitter.var>>1); + particles[emitIndex].ttl = random16(emitter.maxLife - emitter.minLife) + emitter.minLife; + particles[emitIndex].hue = emitter.source.hue; + particles[emitIndex].sat = emitter.source.sat; + break; + } + /* + if (emitIndex < 2) + { + Serial.print(" "); + Serial.print(particles[emitIndex].ttl); + Serial.print(" "); + Serial.print(particles[emitIndex].x); + Serial.print(" "); + Serial.print(particles[emitIndex].y); + }*/ + } + //Serial.println("**"); } +//todo: idee: man könnte einen emitter machen, wo die anzahl emittierten partikel von seinem alter abhängt. benötigt aber einen counter +//idee2: source einen counter hinzufügen, dann setting für emitstärke, dann müsste man das nicht immer in den FX animationen handeln + // Emits a particle at given angle and speed, angle is from 0-255 (=0-360deg), speed is also affected by emitter->var -void Emitter_Angle_emit(PSpointsource *emitter, PSparticle *part, uint8_t angle, uint8_t speed) +// angle = 0 means in x-direction +void ParticleSystem::AngleEmit(PSsource &emitter, uint8_t angle, uint32_t speed) +{ + //todo: go to 16 bits, rotating particles could use this, others maybe as well + emitter.vx = (((int32_t)cos8(angle) - 127) * speed) >> 7; // cos is signed 8bit, so 1 is 127, -1 is -127, shift by 7 + emitter.vy = (((int32_t)sin8(angle) - 127) * speed) >> 7; + SprayEmit(emitter); +} + +// 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 +void ParticleSystem::ParticleMoveUpdate(PSparticle &part, PSsettings &options) +{ + if (part.ttl > 0) + { + // age + part.ttl--; + // apply velocity + int32_t newX, newY; //use temporary 32bit vaiable to make function a tad faster (maybe) + newX = part.x + (int16_t)part.vx; + newY = part.y + (int16_t)part.vy; + part.outofbounds = 0; // reset out of bounds (in case particle was created outside the matrix and is now moving into view) + + if (((newX < -PS_P_HALFRADIUS) || (newX > maxX))) // check if particle is out of bounds + { + if (options.killoutofbounds) + part.ttl = 0; + else if (options.bounceX) // particle was in view and now moved out -> bounce it + { + newX = -newX; // invert speed + newX = ((newX) * wallHardness) >> 8; // reduce speed as energy is lost on non-hard surface + if (newX < 0) + newX = -newX; + else + newX = maxX - PS_P_RADIUS - newX; + } + else if (options.wrapX) + { + newX = wraparound(newX, maxX - PS_P_RADIUS); + } + else + part.outofbounds = 1; + } + + if (((newY < -PS_P_HALFRADIUS) || (newY > maxY))) // check if particle is out of bounds + { + if (options.killoutofbounds) + part.ttl = 0; + else if (options.bounceY) // particle was in view and now moved out -> bounce it + { + part.vy = -part.vy; // invert speed + part.vy = (part.vy * wallHardness) >> 8; // reduce speed as energy is lost on non-hard surface + if (newY < 0) + newY = -newY; + else if (options.useGravity == false) //if gravity disabled also bounce at the top + newY = maxY - PS_P_RADIUS - newY; + } + else if (options.wrapY) + { + newY = wraparound(newY, maxY - PS_P_RADIUS); + } + else + part.outofbounds = 1; + } + + part.x = newX; // set new position + part.y = newY; // set new position + } +} + +// apply a force in x,y direction to particles +// caller needs to provide a 8bit counter that holds its value between calls for each group (numparticles can be 1 for single particle) +// force is in 3.4 fixed point notation so force=16 means apply v+1 each frame default of 8 is every other frame (gives good results) +void ParticleSystem::applyForce(PSparticle *part, uint32_t numparticles, int8_t xforce, int8_t yforce, uint8_t *counter) +{ + // for small forces, need to use a delay counter + uint8_t xcounter = (*counter) & 0x0F; // lower four bits + uint8_t ycounter = (*counter) >> 4; // upper four bits + + // velocity increase + int32_t dvx = calcForce_dV(xforce, &xcounter); + int32_t dvy = calcForce_dV(yforce, &ycounter); + + // save counter values back + *counter |= xcounter & 0x0F; // write lower four bits, make sure not to write more than 4 bits + *counter |= (ycounter << 4) & 0xF0; // write upper four bits + + // apply the force to particle: + int32_t i = 0; + if (dvx != 0) + { + if (numparticles == 1) // for single particle, skip the for loop to make it faster + { + particles[0].vx = particles[0].vx + dvx > PS_P_MAXSPEED ? PS_P_MAXSPEED : particles[0].vx + dvx; // limit the force, this is faster than min or if/else + } + else + { + for (i = 0; i < numparticles; i++) + { + // note: not checking if particle is dead is faster as most are usually alive and if few are alive, rendering is faster so no speed penalty + particles[i].vx = particles[i].vx + dvx > PS_P_MAXSPEED ? PS_P_MAXSPEED : particles[i].vx + dvx; + } + } + } + if (dvy != 0) + { + if (numparticles == 1) // for single particle, skip the for loop to make it faster + particles[0].vy = particles[0].vy + dvy > PS_P_MAXSPEED ? PS_P_MAXSPEED : particles[0].vy + dvy; + else + { + for (i = 0; i < numparticles; i++) + { + particles[i].vy = particles[i].vy + dvy > PS_P_MAXSPEED ? PS_P_MAXSPEED : particles[i].vy + dvy; + } + } + } +} + +// apply a force in angular direction to of particles +// caller needs to provide a 8bit counter that holds its value between calls for each group (numparticles can be 1 for single particle) +void ParticleSystem::applyAngleForce(PSparticle *part, uint32_t numparticles, uint8_t force, uint8_t angle, uint8_t *counter) +{ + int8_t xforce = ((int32_t)force * (cos8(angle) - 128)) >> 8; // force is +/- 127 + int8_t yforce = ((int32_t)force * (sin8(angle) - 128)) >> 8; + // noste: sin16 is 10% faster than sin8() on ESP32 but on ESP8266 it is 9% slower, and dont need that 16bit of resolution + // force is in 3.4 fixed point notation so force=16 means apply v+1 each frame (useful force range is +/- 127) + applyForce(part, numparticles, xforce, yforce, counter); +} + +// apply gravity to a group of particles +// faster than apply force since direction is always down and counter is fixed for all particles +// caller needs to provide a 8bit counter that holds its value between calls +// force is in 4.4 fixed point notation so force=16 means apply v+1 each frame default of 8 is every other frame (gives good results), force above 127 are VERY strong +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 + dv = 1; + + *counter += force; + + if (*counter > 15) + { + *counter -= 16; + // apply force to all used particles + for (uint32_t i = 0; i < numarticles; i++) + { + // note: not checking if particle is dead is faster as most are usually alive and if few are alive, rendering is fast anyways + particles[i].vy = particles[i].vy - dv > PS_P_MAXSPEED ? PS_P_MAXSPEED : particles[i].vy - dv; // limit the force, this is faster than min or if/else + } + } +} + +//apply gravity using PS global gforce +void ParticleSystem::applyGravity(PSparticle *part, uint32_t numarticles, uint8_t *counter) { - emitter->vx = (((int16_t)cos8(angle) - 127) * speed) >> 7; // cos is signed 8bit, so 1 is 127, -1 is -127, shift by 7 - emitter->vy = (((int16_t)sin8(angle) - 127) * speed) >> 7; - Emitter_Fountain_emit(emitter, part); + applyGravity(part, numarticles, gforce, counter); } + +// 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, uint32_t numparticles, uint8_t coefficient) +{ + int32_t friction = 256 - coefficient; + for (uint32_t i = 0; i < numparticles; i++) + { + // note: not checking if particle is dead is faster as most are usually alive and if few are alive, rendering is faster + part[i].vx = ((int16_t)part[i].vx * friction) >> 8; + part[i].vy = ((int16_t)part[i].vy * friction) >> 8; + } +} + +// TODO: attract needs to use the above force functions // attracts a particle to an attractor particle using the inverse square-law -void Particle_attractor(PSparticle *particle, PSparticle *attractor, uint8_t *counter, uint8_t strength, bool swallow) +void ParticleSystem::attract(PSparticle *particle, PSparticle *attractor, uint8_t *counter, uint8_t strength, bool swallow) { // Calculate the distance between the particle and the attractor int32_t dx = attractor->x - particle->x; @@ -112,17 +449,13 @@ void Particle_attractor(PSparticle *particle, PSparticle *attractor, uint8_t *co xcounter += xforce_abs; if (xcounter > 15) { - xcounter -= 15; + xcounter -= 16; *counter |= xcounter & 0x0F; // write lower four bits, make sure not to write more than 4 bits // apply force in x direction if (dx < 0) - { particle->vx -= 1; - } else - { particle->vx += 1; - } } else // save counter value *counter |= xcounter & 0x0F; // write lower four bits, make sure not to write more than 4 bits @@ -138,17 +471,13 @@ void Particle_attractor(PSparticle *particle, PSparticle *attractor, uint8_t *co if (ycounter > 15) { - ycounter -= 15; + ycounter -= 16; *counter |= (ycounter << 4) & 0xF0; // write upper four bits if (dy < 0) - { particle->vy -= 1; - } else - { particle->vy += 1; - } } else // save counter value *counter |= (ycounter << 4) & 0xF0; // write upper four bits @@ -160,366 +489,259 @@ void Particle_attractor(PSparticle *particle, PSparticle *attractor, uint8_t *co // TODO: need to limit the max speed? } -// particle moves, decays and dies, if killoutofbounds is set, out of bounds particles are set to ttl=0 -void Particle_Move_update(PSparticle *part, bool killoutofbounds, bool wrapX, bool wrapY) -{ - // Matrix dimension - const int32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; - const int32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); - - // particle box dimensions - const int32_t PS_MAX_X = cols * PS_P_RADIUS - 1; - const int32_t PS_MAX_Y = rows * PS_P_RADIUS - 1; - - if (part->ttl > 0) - { - // age - part->ttl--; - - // apply velocity - int32_t newX, newY; - newX = part->x + (int16_t)part->vx; - newY = part->y + (int16_t)part->vy; - - part->outofbounds = 0; // reset out of bounds (in case particle was created outside the matrix and is now moving into view) - // x direction, handle wraparound - if (wrapX) - { - newX = newX % (PS_MAX_X + 1); - if (newX < 0) - newX = PS_MAX_X - newX; - } - else if ((part->x <= 0) || (part->x >= PS_MAX_X)) // check if particle is out of bounds - { - if (killoutofbounds) - part->ttl = 0; - else - part->outofbounds = 1; - } - part->x = newX; // set new position - - if (wrapY) - { - newY = newY % (PS_MAX_Y + 1); - if (newY < 0) - newY = PS_MAX_Y - newY; - } - else if ((part->y <= 0) || (part->y >= PS_MAX_Y)) // check if particle is out of bounds - { - if (killoutofbounds) - part->ttl = 0; - else - part->outofbounds = 1; - } - part->y = newY; // set new position - } -} -// bounces a particle on the matrix edges, if surface 'hardness' is <255 some energy will be lost in collision (127 means 50% lost) -void Particle_Bounce_update(PSparticle *part, const uint8_t hardness) +// render particles to the LED buffer (uses palette to render the 8bit particle color value) +// if wrap is set, particles half out of bounds are rendered to the other side of the matrix +void ParticleSystem::ParticleSys_render() { - // Matrix dimension - const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; - const uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); - - // particle box dimensions - const uint16_t PS_MAX_X(cols * PS_P_RADIUS - 1); - const uint16_t PS_MAX_Y(rows * PS_P_RADIUS - 1); - - if (part->ttl > 0) - { - // age - part->ttl--; - - part->outofbounds = 0; // reset out of bounds (particles are never out of bounds) - - // apply velocity - int16_t newX, newY; - - // apply velocity - newX = part->x + (int16_t)part->vx; - newY = part->y + (int16_t)part->vy; +#ifdef ESP8266 + const bool fastcoloradd = true; // on ESP8266, we need every bit of performance we can get +#else + const bool fastcoloradd = false; // on ESP32, there is very little benefit from using fast add +#endif - if ((newX <= 0) || (newX >= PS_MAX_X)) - { // reached an edge - part->vx = -part->vx; // invert speed - part->vx = (((int16_t)part->vx) * ((int16_t)hardness + 1)) >> 8; // reduce speed as energy is lost on non-hard surface - } - if ((newY <= 0) || (newY >= PS_MAX_Y)) - { // reached an edge - part->vy = -part->vy; // invert speed - part->vy = (((int16_t)part->vy) * ((int16_t)hardness + 1)) >> 8; // reduce speed as energy is lost on non-hard surface - } + int32_t pixelCoordinates[4][2]; //physical coordinates of the four positions, x,y pairs + //int32_t intensity[4]; + CRGB baseRGB; + uint32_t i; + uint32_t brightness; // particle brightness, fades if dying + //CRGB colorbuffer[maxXpixel/4][maxYpixel/4] = {0}; //put buffer on stack, will this work? or better allocate it? -> crashes hard even with quarter the size - newX = max(newX, (int16_t)0); // limit to positive - newY = max(newY, (int16_t)0); - part->x = min(newX, (int16_t)PS_MAX_X); // limit to matrix boundaries - part->y = min(newY, (int16_t)PS_MAX_Y); + // to create a 2d array on heap: +/* +TODO: using a local buffer crashed immediately, find out why. + // Allocate memory for the array of pointers to rows + CRGB **colorbuffer = (CRGB **)calloc(maxXpixel+1, sizeof(CRGB *)); + if (colorbuffer == NULL) + { + Serial.println("Memory allocation failed111"); + return; } -} - -// particle moves, gravity force is applied and ages, if wrapX is set, pixels leaving in x direction reappear on other side, hardness is surface hardness for bouncing (127 means 50% speed lost each bounce) -void Particle_Gravity_update(PSparticle *part, bool wrapX, bool bounceX, bool bounceY, const uint8_t hardness) -{ - // Matrix dimension - const int32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; - const int32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); - // particle box dimensions - const int32_t PS_MAX_X = cols * PS_P_RADIUS - 1; - const int32_t PS_MAX_Y = rows * PS_P_RADIUS - 1; - - if (part->ttl > 0) + // Allocate memory for each row + for (i = 0; i < maxXpixel; i++) { - // age - part->ttl--; - - // check if particle is out of bounds or died - if ((part->y < -PS_P_RADIUS) || (part->y >= PS_MAX_Y << 2)) - { // if it moves more than 1 pixel below y=0, it will not come back. also remove particles that too far above - part->ttl = 0; - return; // particle died, we are done - } - if (wrapX == false) - { - if ((part->x < -PS_MAX_X) || (part->x >= PS_MAX_X << 2)) - { // left and right: keep it alive as long as its not too far out (if adding more effects like wind, it may come back) - part->ttl = 0; - return; // particle died, we are done - } - } - - // apply acceleration (gravity) every other frame, doing it every frame is too strong - if (SEGMENT.call % 2 == 0) + colorbuffer[i] = (CRGB *)calloc(maxYpixel + 1, sizeof(CRGB)); + if (colorbuffer[i] == NULL) { - if (part->vy > -MAXGRAVITYSPEED) - part->vy = part->vy - 1; + Serial.println("Memory allocation failed222"); + return; } + }*/ - // apply velocity - int32_t newX, newY; - - newX = part->x + (int16_t)part->vx; - newY = part->y + (int16_t)part->vy; +//TODO: in der renderfunktion gibts noch ein bug, am linken rand verschwindet die rechte hälfte der partikel sehr abrupt, das passiert auch wenn man TTX und outofbounds pixel mitrendert (continue unten auskommentiert) +//es hat also nichts mit dem TTL oder dem outofbounds zu tun sondern muss etwas anderes sein... +//rechts und oben gibts ein schönes fade-out der pixel, links und unten verschwinden sie plötzlich muss in der pixel renderfunktion sein. - part->outofbounds = 0; - // check if particle is outside of displayable matrix - // x direction, handle wraparound (will overrule bounce x) and bounceX - if (wrapX) + // go over particles and update matrix cells on the way + for (i = 0; i < usedParticles; i++) + { + /* + if (particles[i].ttl == 0 || particles[i].outofbounds) + { + continue; + }*/ + if (particles[i].ttl == 0) { - newX = newX % (PS_MAX_X + 1); - if (newX < 0) - newX = PS_MAX_X - newX; + continue; } - else + // generate RGB values for particle + brightness = particles[i].ttl > 255 ? 255 : particles[i].ttl; //faster then using min() + baseRGB = ColorFromPalette(SEGPALETTE, particles[i].hue, 255, LINEARBLEND); + if (particles[i].sat < 255) { - if (newX < 0 || newX > PS_MAX_X) - { // reached an edge - if (bounceX) - { - part->vx = -part->vx; // invert speed - part->vx = (((int16_t)part->vx) * (int16_t)hardness) >> 8; // reduce speed as energy is lost on non-hard surface - newX = max(newX, (int32_t)0); // limit to positive - newX = min(newX, (int32_t)PS_MAX_X); // limit to matrix boundaries - } - else // not bouncing and out of matrix - part->outofbounds = 1; - } + CHSV baseHSV = rgb2hsv_approximate(baseRGB); //convert to hsv + baseHSV.s = particles[i].sat; //desaturate + baseRGB = (CRGB)baseHSV; //convert back to RGB } - - part->x = newX; // set new position - - // y direction, handle bounceY (bounces at ground only) - if (newY < 0) - { // || newY > PS_MAX_Y) { //reached an edge - if (bounceY) + int32_t intensity[4] = {0}; //note: intensity 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 + + // calculate brightness values for all four pixels representing a particle using linear interpolation and calculate the coordinates of the phyiscal pixels to add the color to + renderParticle(&particles[i], brightness, intensity, pixelCoordinates); + + if (intensity[0] > 0) + SEGMENT.addPixelColorXY(pixelCoordinates[0][0], maxYpixel - pixelCoordinates[0][1], baseRGB.scale8((uint8_t)intensity[0]), fastcoloradd); // bottom left + if (intensity[1] > 0) + SEGMENT.addPixelColorXY(pixelCoordinates[1][0], maxYpixel - pixelCoordinates[1][1], baseRGB.scale8((uint8_t)intensity[1]), fastcoloradd); // bottom right + if (intensity[2] > 0) + SEGMENT.addPixelColorXY(pixelCoordinates[2][0], maxYpixel - pixelCoordinates[2][1], baseRGB.scale8((uint8_t)intensity[2]), fastcoloradd); // top right + if (intensity[3] > 0) + SEGMENT.addPixelColorXY(pixelCoordinates[3][0], maxYpixel - pixelCoordinates[3][1], baseRGB.scale8((uint8_t)intensity[3]), fastcoloradd); // top left + + //test to render larger pixels with minimal effort (not working yet, need to calculate coordinate from actual dx position but brightness seems right) + // SEGMENT.addPixelColorXY(pixelCoordinates[1][0] + 1, maxYpixel - pixelCoordinates[1][1], baseRGB.scale8((uint8_t)((brightness>>1) - intensity[0])), fastcoloradd); + // SEGMENT.addPixelColorXY(pixelCoordinates[2][0] + 1, maxYpixel - pixelCoordinates[2][1], baseRGB.scale8((uint8_t)((brightness>>1) -intensity[3])), fastcoloradd); + // colorbuffer[pixelCoordinates[0][0]][maxYpixel - pixelCoordinates[0][1]] += baseRGB.scale8((uint8_t)intensity[0]); + // colorbuffer[pixelCoordinates[1][0]][maxYpixel - pixelCoordinates[1][1]] += baseRGB.scale8((uint8_t)intensity[0]); + // colorbuffer[pixelCoordinates[2][0]][maxYpixel - pixelCoordinates[2][1]] += baseRGB.scale8((uint8_t)intensity[0]); + // colorbuffer[pixelCoordinates[3][0]][maxYpixel - pixelCoordinates[3][1]] += baseRGB.scale8((uint8_t)intensity[0]); + } + /* + int x,y; + for (x = 0; x <= maxXpixel; x++) + { + for (y = 0; x <= maxYpixel; y++) + { + if(colorbuffer[x][y]>0) { - part->vy = -part->vy; // invert speed - part->vy = (((int16_t)part->vy) * (int16_t)hardness) >> 8; // reduce speed as energy is lost on non-hard surface - newY = max(newY, (int32_t)0); // limit to positive (helps with piling as that can push particles out of frame) - // newY = min(newY, (int16_t)PS_MAX_Y); //limit to matrix boundaries + SEGMENT.setPixelColorXY(x,y,colorbuffer[x][y]); } - else // not bouncing and out of matrix - part->outofbounds = 1; } - - part->y = newY; // set new position } -} - -// render particles to the LED buffer (uses palette to render the 8bit particle color value) -// if wrap is set, particles half out of bounds are rendered to the other side of the matrix -void ParticleSys_render(PSparticle *particles, uint32_t numParticles, bool wrapX, bool wrapY) -{ -#ifdef ESP8266 - bool fastcoloradd = true; // on ESP8266, we need every bit of performance we can get -#else - bool fastcoloradd = false; // on ESP32, there is little benefit from using fast add -#endif - - const int32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; - const int32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); - int32_t x, y; - uint32_t dx, dy; - uint32_t intensity; - CRGB baseRGB; - uint32_t i; - uint32_t brightess; // particle brightness, fades if dying + // Free memory for each row + for (int i = 0; i <= maxXpixel; i++) + { + free(colorbuffer[i]); + } - uint32_t precal1, precal2, precal3; // precalculate values to improve speed + // Free memory for the array of pointers to rows + free(colorbuffer);*/ +} +// calculate pixel positions and brightness distribution for rendering function +// pixelpositions are the physical positions in the matrix that the particle renders to (4x2 array for the four positions) - // go over particles and update matrix cells on the way - for (i = 0; i < numParticles; i++) +void ParticleSystem::renderParticle(PSparticle* particle, uint32_t brightess, int32_t *pixelvalues, int32_t (*pixelpositions)[2]) +{ + // subtract half a radius as the rendering algorithm always starts at the bottom left, this makes calculations more efficient + int32_t xoffset = particle->x - PS_P_HALFRADIUS; + int32_t yoffset = particle->y - PS_P_HALFRADIUS; + int32_t dx = xoffset % PS_P_RADIUS; //relativ particle position in subpixel space + int32_t dy = yoffset % PS_P_RADIUS; + int32_t x = xoffset >> PS_P_RADIUS_SHIFT; // divide by PS_P_RADIUS which is 64, so can bitshift (compiler may not optimize automatically) + int32_t y = yoffset >> PS_P_RADIUS_SHIFT; + + // set the four raw pixel coordinates, the order is bottom left [0], bottom right[1], top right [2], top left [3] + pixelpositions[0][0] = pixelpositions[3][0] = x; // bottom left & top left + pixelpositions[0][1] = pixelpositions[1][1] = y; // bottom left & bottom right + pixelpositions[1][0] = pixelpositions[2][0] = x + 1; // bottom right & top right + pixelpositions[2][1] = pixelpositions[3][1] = y + 1; // top right & top left + + // now check if any are out of frame. set values to -1 if they are so they can be easily checked after (no value calculation, no setting of pixelcolor if value < 0) + + if (x < 0) // left pixels out of frame { - if (particles[i].ttl == 0 || particles[i].outofbounds) + dx = PS_P_RADIUS + dx; // if x<0, xoffset becomes negative (and so does dx), must adjust dx as modulo will flip its value (really old bug now finally fixed) + //note: due to inverted shift math, a particel at position -32 (xoffset = -64, dx = 64) is rendered at the wrong pixel position (it should be out of frame) + //checking this above makes this algorithm slower (in frame pixels do not have to be checked), so just correct for it here: + if (dx == PS_P_RADIUS) { - continue; + pixelvalues[1] = pixelvalues[2] = -1; // pixel is actually out of matrix boundaries, do not render } - // generate RGB values for particle - brightess = min(particles[i].ttl, (uint16_t)255); + if (particlesettings.wrapX) // wrap x to the other side if required + pixelpositions[0][0] = pixelpositions[3][0] = maxXpixel; + else + pixelvalues[0] = pixelvalues[3] = -1; // pixel is out of matrix boundaries, do not render + } + else if (pixelpositions[1][0] > maxXpixel) // right pixels, only has to be checkt if left pixels did not overflow + { + if (particlesettings.wrapX) // wrap y to the other side if required + pixelpositions[1][0] = pixelpositions[2][0] = 0; + else + pixelvalues[1] = pixelvalues[2] = -1; + } - if (particles[i].sat < 255) + if (y < 0) // bottom pixels out of frame + { + dy = PS_P_RADIUS + dy; //see note above + if (dy == PS_P_RADIUS) { - CHSV baseHSV = rgb2hsv_approximate(ColorFromPalette(SEGPALETTE, particles[i].hue, 255, LINEARBLEND)); - baseHSV.s = particles[i].sat; - baseRGB = (CRGB)baseHSV; + pixelvalues[2] = pixelvalues[3] = -1; // pixel is actually out of matrix boundaries, do not render } + if (particlesettings.wrapY) // wrap y to the other side if required + pixelpositions[0][1] = pixelpositions[1][1] = maxYpixel; else - baseRGB = ColorFromPalette(SEGPALETTE, particles[i].hue, 255, LINEARBLEND); - - // subtract half a radius as the rendering algorithm always starts at the bottom left, this makes calculations more efficient - int32_t xoffset = particles[i].x - PS_P_HALFRADIUS; - int32_t yoffset = particles[i].y - PS_P_HALFRADIUS; - dx = xoffset % (uint32_t)PS_P_RADIUS; - dy = yoffset % (uint32_t)PS_P_RADIUS; - x = (xoffset) >> PS_P_RADIUS_SHIFT; // divide by PS_P_RADIUS which is 64, so can bitshift (compiler may not optimize automatically) - y = (yoffset) >> PS_P_RADIUS_SHIFT; - - // calculate brightness values for all six pixels representing a particle using linear interpolation - // precalculate values for speed optimization - precal1 = PS_P_RADIUS - dx; - precal2 = (PS_P_RADIUS - dy) * brightess; // multiply by ttl, adds more heat for younger particles - precal3 = dy * brightess; - - if (wrapX) - { // wrap it to the other side if required - if (x < 0) - { // left half of particle render is out of frame, wrap it - x = cols - 1; - } - } - if (wrapY) - { // wrap it to the other side if required - if (y < 0) - { // left half of particle render is out of frame, wrap it - y = rows - 1; - } - } + pixelvalues[0] = pixelvalues[1] = -1; + } + else if (pixelpositions[2][1] > maxYpixel) // top pixels + { + if (particlesettings.wrapY) // wrap y to the other side if required + pixelpositions[2][1] = pixelpositions[3][1] = 0; + else + pixelvalues[2] = pixelvalues[3] = -1; + } - // calculate brightness values for all four pixels representing a particle using linear interpolation, - // add color to the LEDs. - // intensity is a scaling value from 0-255 (0-100%) - // bottom left - // calculate the intensity with linear interpolation, divide by surface area (shift by PS_P_SURFACE) to distribute the energy - intensity = (precal1 * precal2) >> PS_P_SURFACE; // equal to (PS_P_RADIUS - dx * (PS_P_RADIUS-dy) * brightess) >> PS_P_SURFACE - // scale the particle base color by the intensity and add it to the pixel - SEGMENT.addPixelColorXY(x, rows - y - 1, baseRGB.scale8(intensity), fastcoloradd); - - // bottom right; - x++; - if (wrapX) - { // wrap it to the other side if required - if (x >= cols) - x = x % cols; // in case the right half of particle render is out of frame, wrap it (note: on microcontrollers with hardware division, the if statement is not really needed) - } - if (x < cols && y < rows) - { - intensity = (dx * precal2) >> PS_P_SURFACE; // equal to (dx * (PS_P_RADIUS-dy) * brightess) >> PS_P_SURFACE - SEGMENT.addPixelColorXY(x, rows - y - 1, baseRGB.scale8(intensity), fastcoloradd); - } - // top right - y++; - if (wrapY) - { // wrap it to the other side if required - if (y >= rows) - y = y % rows; // in case the right half of particle render is out of frame, wrap it (note: on microcontrollers with hardware division, the if statement is not really needed) - } - if (x < cols && y < rows) - { - intensity = (dx * precal3) >> PS_P_SURFACE; // equal to (dx * dy * brightess) >> PS_P_SURFACE - SEGMENT.addPixelColorXY(x, rows - y - 1, baseRGB.scale8(intensity), fastcoloradd); - } - // top left - x--; - if (wrapX) - { // wrap it to the other side if required - if (x < 0) - { // left half of particle render is out of frame, wrap it - x = cols - 1; - } - } - if (x < cols && y < rows) - { - intensity = (precal1 * precal3) >> PS_P_SURFACE; // equal to ((PS_P_RADIUS-dx) * dy * brightess) >> PS_P_SURFACE - SEGMENT.addPixelColorXY(x, rows - y - 1, baseRGB.scale8(intensity), fastcoloradd); - } + // calculate brightness values for all four pixels representing a particle using linear interpolation + // precalculate values for speed optimization + int32_t precal1 = (int32_t)PS_P_RADIUS - dx; + int32_t precal2 = ((int32_t)PS_P_RADIUS - dy) * brightess; + int32_t precal3 = dy * brightess; + + //calculate the values for pixels that are in frame + if (pixelvalues[0] >= 0) + pixelvalues[0] = (precal1 * precal2) >> PS_P_SURFACE; // bottom left value equal to ((PS_P_RADIUS - dx) * (PS_P_RADIUS-dy) * brightess) >> PS_P_SURFACE + if (pixelvalues[1] >= 0) + pixelvalues[1] = (dx * precal2) >> PS_P_SURFACE; // bottom right value equal to (dx * (PS_P_RADIUS-dy) * brightess) >> PS_P_SURFACE + if (pixelvalues[2] >= 0) + pixelvalues[2] = (dx * precal3) >> PS_P_SURFACE; // top right value equal to (dx * dy * brightess) >> PS_P_SURFACE + if (pixelvalues[3] >= 0) + pixelvalues[3] = (precal1 * precal3) >> PS_P_SURFACE; // top left value equal to ((PS_P_RADIUS-dx) * dy * brightess) >> PS_P_SURFACE +/* + Serial.print(particle->x); + Serial.print(" "); + Serial.print(xoffset); + Serial.print(" dx"); + Serial.print(dx); + Serial.print(" "); + for(uint8_t t = 0; t<4; t++) + { + Serial.print("x"); + Serial.print(pixelpositions[t][0]); + Serial.print(" y"); + Serial.print(pixelpositions[t][1]); + Serial.print(" v"); + Serial.print(pixelvalues[t]); + Serial.print(" "); } + Serial.println(" "); + */ } -// update & move particle, wraps around left/right if wrapX is true, wrap around up/down if wrapY is true +// update & move particle, wraps around left/right if settings.wrapX is true, wrap around up/down if settings.wrapY is true // particles move upwards faster if ttl is high (i.e. they are hotter) -void FireParticle_update(PSparticle *part, uint32_t numparticles, bool wrapX) +void ParticleSystem::FireParticle_update() { - // Matrix dimension - const int32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; - const int32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); - - // particle box dimensions - const int32_t PS_MAX_X = (cols * (uint32_t)PS_P_RADIUS - 1); - const int32_t PS_MAX_Y = (rows * (uint32_t)PS_P_RADIUS - 1); + //TODO: cleanup this function? uint32_t i = 0; - for (i = 0; i < numparticles; i++) + for (i = 0; i < usedParticles; i++) { - if (part[i].ttl > 0) + if (particles[i].ttl > 0) { // age - part[i].ttl--; + particles[i].ttl--; // apply velocity - part[i].x = part[i].x + (int32_t)part[i].vx; - part[i].y = part[i].y + (int32_t)part[i].vy + (part[i].ttl >> 4); // younger particles move faster upward as they are hotter, used for fire + 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 - part[i].outofbounds = 0; + 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 // y-direction - if (part[i].y < 0) + if (particles[i].y < -PS_P_HALFRADIUS) { - part[i].outofbounds = 1; + particles[i].outofbounds = 1; } - else if (part[i].y > PS_MAX_Y) // particle moved out on the top + else if (particles[i].y > maxY) // particle moved out on the top { - part[i].ttl = 0; + particles[i].ttl = 0; } else // particle is in frame in y direction, also check x direction now { - if ((part[i].x < 0) || (part[i].x > PS_MAX_X)) + if ((particles[i].x < -PS_P_HALFRADIUS) || (particles[i].x > maxX)) { - if (wrapX) + if (particlesettings.wrapX) { - part[i].x = part[i].x % (PS_MAX_X + 1); - if (part[i].x < 0) - part[i].x = PS_MAX_X - part[i].x; + particles[i].x = wraparound(particles[i].x, maxX); } else { - part[i].ttl = 0; + particles[i].ttl = 0; } } } @@ -529,205 +751,141 @@ void FireParticle_update(PSparticle *part, uint32_t numparticles, bool wrapX) // render fire particles to the LED buffer using heat to color // each particle adds heat according to its 'age' (ttl) which is then rendered to a fire color in the 'add heat' function -void ParticleSys_renderParticleFire(PSparticle *particles, uint32_t numParticles, bool wrapX) +// note: colormode 0-5 are native, heat based color modes, set colormode to 255 to use palette +void ParticleSystem::renderParticleFire(uint8_t colormode) { - - const int32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; - const int32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); - - int32_t x, y; - uint32_t dx, dy; - uint32_t pixelheat; - uint32_t precal1, precal2, precal3; // precalculated values to improve speed +//TODO: if colormode = 255 call normal rendering function + int32_t pixelCoordinates[4][2]; // physical coordinates of the four positions, x,y pairs + int32_t pixelheat[4]; + uint32_t flameheat; //depends on particle.ttl uint32_t i; + // 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 < numParticles; i++) + for (i = 0; i < usedParticles; i++) { - - if (particles[i].outofbounds) - { + if (particles[i].outofbounds) //lots of fire particles are out of bounds, check first continue; - } if (particles[i].ttl == 0) - { continue; - } - // subtract half a radius as the rendering algorithm always starts at the bottom left, this makes calculations more efficient - int32_t xoffset = particles[i].x - PS_P_HALFRADIUS; - int32_t yoffset = particles[i].y - PS_P_HALFRADIUS; - dx = xoffset % (uint32_t)PS_P_RADIUS; - dy = yoffset % (uint32_t)PS_P_RADIUS; - x = (xoffset) >> PS_P_RADIUS_SHIFT; // divide by PS_P_RADIUS which is 64, so can bitshift (compiler may not optimize automatically) - y = (yoffset) >> PS_P_RADIUS_SHIFT; + flameheat = particles[i].ttl; + renderParticle(&particles[i], flameheat, pixelheat, pixelCoordinates); - if (wrapX) - { - if (x < 0) - { // left half of particle render is out of frame, wrap it - x = cols - 1; - } - } - // calculate brightness values for all six pixels representing a particle using linear interpolation - // precalculate values for speed optimization - precal1 = PS_P_RADIUS - dx; - precal2 = (PS_P_RADIUS - dy) * particles[i].ttl; //multiply by ttl, adds more heat for younger particles - precal3 = dy * particles[i].ttl; + //TODO: add one more pixel closer to the particle, so it is 3 pixels wide - // bottom left - if (x < cols && x >= 0 && y < rows && y >= 0) - { - pixelheat = (precal1 * precal2) >> PS_P_SURFACE; - PartMatrix_addHeat(x, y, pixelheat, rows); - // PartMatrix_addHeat(x + 1, y, tempVal, rows); // shift particle by 1 pixel to the right and add heat again (makes flame wider without using more particles) - } - // bottom right; - x++; - if (wrapX) - { // wrap it to the other side if required - if (x >= cols) // if statement is faster on ESP8266 TODO: add a define - x = x % cols; // in case the right half of particle render is out of frame, wrap it (note: on microcontrollers with hardware division, the if statement is not really needed) - } - if (x < cols && y < rows && y >= 0) - { - pixelheat = (dx * precal2) >> PS_P_SURFACE; - PartMatrix_addHeat(x, y, pixelheat, rows); - // PartMatrix_addHeat(x + 1, y, tempVal, rows); // shift particle by 1 pixel to the right and add heat again (makes flame wider without using more particles) - } - // top right - y++; - if (x < cols && y < rows) - { - pixelheat = (dx * precal3) >> PS_P_SURFACE; // - PartMatrix_addHeat(x, y, pixelheat, rows); - // PartMatrix_addHeat(x + 1, y, tempVal, rows); // shift particle by 1 pixel to the right and add heat again (makes flame wider without using more particles) - } - // top left - x--; - if (wrapX) - { // wrap it to the other side if required - if (x < 0) // left half of particle render is out of frame, wrap it - x = cols - 1; - } - if (x < cols && x >= 0 && y < rows) - { - pixelheat = (precal1 * precal3) >> PS_P_SURFACE; - PartMatrix_addHeat(x, y, pixelheat, rows); - // PartMatrix_addHeat(x + 1, y, tempVal, rows); // shift particle by 1 pixel to the right and add heat again (makes flame wider without using more particles) - } + if (pixelheat[0] >= 0) + PartMatrix_addHeat(pixelCoordinates[0][0], pixelCoordinates[0][1], pixelheat[0], colormode); + if (pixelheat[1] >= 0) + PartMatrix_addHeat(pixelCoordinates[1][0], pixelCoordinates[1][1], pixelheat[0], colormode); + if (pixelheat[2] >= 0) + PartMatrix_addHeat(pixelCoordinates[2][0], pixelCoordinates[2][1], pixelheat[0], colormode); + if (pixelheat[3] >= 0) + PartMatrix_addHeat(pixelCoordinates[3][0], pixelCoordinates[3][1], pixelheat[0], colormode); + + // 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 } } // adds 'heat' to red color channel, if it overflows, add it to next color channel -void PartMatrix_addHeat(uint8_t col, uint8_t row, uint32_t heat, uint32_t rows) +// colormode is 0-5 where 0 is normal fire and all others are color variations +void ParticleSystem::PartMatrix_addHeat(uint8_t col, uint8_t row, uint32_t heat, uint8_t colormode) { - // const uint32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); - - CRGB currentcolor = SEGMENT.getPixelColorXY(col, rows - row - 1); // read current matrix color (flip y axis) - uint32_t newcolorvalue; - uint32_t colormode = map(SEGMENT.custom3, 0, 31, 0, 5); // get color mode from slider (3bit value) + CRGB currentcolor = SEGMENT.getPixelColorXY(col, maxYpixel - row); // read current matrix color (flip y axis) + uint32_t newcolorvalue, i; // define how the particle TTL value (which is the heat given to the function) maps to heat, if lower, fire is more red, if higher, fire is brighter as bright flames travel higher and decay faster // need to scale ttl value of particle to a good heat value that decays fast enough #ifdef ESP8266 - heat = heat << 4; //ESP8266 has no hardware multiplication, just use shift (also less particles, need more heat) + heat = heat << 4; //ESP8266 has slow hardware multiplication, just use shift (also less particles, need more heat) #else - heat = heat * 10; + heat = heat * 10; //TODO: need to play with this some more to see if it makes fire better or worse #endif - // i=0 is normal red fire, i=1 is green fire, i=2 is blue fire - uint32_t i = (colormode & 0x07) >> 1; - i = i % 3; - uint32_t increment = (colormode & 0x01) + 1; // 0 (or 3) means only one single color for the flame, 1 is normal, 2 is alternate color modes - if (currentcolor[i] < 255) - { - newcolorvalue = (uint16_t)currentcolor[i] + heat; // add heat, check if it overflows - newcolorvalue = min(newcolorvalue, (uint32_t)255); // limit to 8bit value again - // check if there is heat left over - if (newcolorvalue == 255) - { // there cannot be a leftover if it is not full - heat = heat - (255 - currentcolor[i]); // heat added is difference from current value to full value, subtract it from the inital heat value so heat is the remaining heat not added yet - // this cannot produce an underflow since we never add more than the initial heat value - } - else - { - heat = 0; // no heat left - } - currentcolor[i] = (uint8_t)newcolorvalue; - } - if (heat > 0) // there is still heat left to be added + uint32_t coloridx = (colormode & 0x07) >> 1; // set startindex for colormode 0 is normal red fire, 1 is green fire, 2 is blue fire + if (coloridx > 2) + coloridx -= 3; // faster than i = i % 3 + uint32_t increment = (colormode & 0x01) + 1; // 0 (or 3) means only one single color for the flame, 1 is normal, 2 is alternate color modes + //go over the three colors and fill them with heat, if one overflows, add heat to the next + for (i = 0; i < 3; ++i) { - i += increment; - i = i % 3; - - if (currentcolor[i] < 255) + if (currentcolor[coloridx] < 255) //current color is not yet full { - newcolorvalue = (uint32_t)currentcolor[i] + heat; // add heat, check if it overflows - newcolorvalue = min(newcolorvalue, (uint32_t)255); // limit to 8bit value again - // check if there is heat left over - if (newcolorvalue == 255) // there cannot be a leftover if red is not full - { - heat = heat - (255 - currentcolor[i]); // heat added is difference from current red value to full red value, subtract it from the inital heat value so heat is the remaining heat not added yet - // this cannot produce an underflow since we never add more than the initial heat value - } - else - { - heat = 0; // no heat left - } - currentcolor[i] = (uint8_t)newcolorvalue; + if (heat > 255) + { + heat -= 255 - currentcolor[coloridx]; + currentcolor[coloridx] = 255; + } + else{ + int32_t leftover = heat - currentcolor[coloridx]; + if(leftover <= 0) + { + currentcolor[coloridx] += heat; + break; + } + else{ + currentcolor[coloridx] = 255; + if(heat > leftover) + { + heat -= leftover; + } + else + break; + } + } } + coloridx += increment; + if (coloridx > 2) + coloridx -= 3; // faster than i = i % 3 and is allowed since increment is never more than 2 } - if (heat > 0) // there is still heat left to be added + + if (i == 2) // last color was reached limit the color value (in normal mode, this is blue) so it does not go full white { - i += increment; - i = i % 3; - if (currentcolor[i] < 255) - { - newcolorvalue = currentcolor[i] + heat; // add heat, check if it overflows - newcolorvalue = min(newcolorvalue, (uint32_t)50); // limit so it does not go full white - currentcolor[i] = (uint8_t)newcolorvalue; - } + currentcolor[coloridx] = currentcolor[coloridx] > 60 ? 60 : currentcolor[coloridx]; //faster than min() } - SEGMENT.setPixelColorXY(col, rows - row - 1, currentcolor); + SEGMENT.setPixelColorXY(col, maxYpixel - row, currentcolor); } // detect collisions in an array of particles and handle them -void detectCollisions(PSparticle *particles, uint32_t numparticles, uint8_t hardness) +void ParticleSystem::handleCollisions() { // detect and handle collisions uint32_t i, j; uint32_t startparticle = 0; - uint32_t endparticle = numparticles >> 1; // do half the particles, significantly speeds things up + uint32_t endparticle = usedParticles >> 1; // do half the particles, significantly speeds things up - if (SEGMENT.call % 2 == 0) - { // every second frame, do other half of particles (helps to speed things up as not all collisions are handled each frame, less accurate but good enough) + // every second frame, do other half of particles (helps to speed things up as not all collisions are handled each frame, less accurate but good enough) + // if m ore accurate collisions are needed, just call it twice in a row + if (collisioncounter & 0x01) + { startparticle = endparticle; - endparticle = numparticles; + endparticle = usedParticles; } + collisioncounter++; 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].collide && particles[i].outofbounds == 0) // if particle is alive and does collide and is not out of view + if (particles[i].ttl > 0 && particles[i].outofbounds == 0) // 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 < numparticles; j++) + for (j = i + 1; j < usedParticles; j++) { // check against higher number particles if (particles[j].ttl > 0) // if target particle is alive { dx = particles[i].x - particles[j].x; - if ((dx < (PS_P_HARDRADIUS)) && (dx > (-PS_P_HARDRADIUS))) // check x direction, if close, check y direction + if (dx < PS_P_HARDRADIUS && dx > -PS_P_HARDRADIUS) // check x direction, if close, check y direction { dy = particles[i].y - particles[j].y; - if ((dx < (PS_P_HARDRADIUS)) && (dx > (-PS_P_HARDRADIUS)) && (dy < (PS_P_HARDRADIUS)) && (dy > (-PS_P_HARDRADIUS))) - { // particles are close - handleCollision(&particles[i], &particles[j], hardness); - } + if (dy < PS_P_HARDRADIUS && dy > -PS_P_HARDRADIUS) // particles are close + collideParticles(&particles[i], &particles[j]); } } } @@ -737,7 +895,7 @@ void detectCollisions(PSparticle *particles, uint32_t numparticles, uint8_t hard // 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 handleCollision(PSparticle *particle1, PSparticle *particle2, const uint32_t hardness) +void ParticleSystem::collideParticles(PSparticle *particle1, PSparticle *particle2) { int32_t dx = particle2->x - particle1->x; @@ -785,7 +943,7 @@ void handleCollision(PSparticle *particle1, PSparticle *particle2, const uint32_ const uint32_t bitshift = 14; // bitshift used to avoid floats // Calculate new velocities after collision - int32_t impulse = (((dotProduct << (bitshift)) / (distanceSquared)) * (hardness+1)) >> 8; + int32_t impulse = (((dotProduct << (bitshift)) / (distanceSquared)) * collisionHardness) >> 8; int32_t ximpulse = (impulse * dx) >> bitshift; int32_t yimpulse = (impulse * dy) >> bitshift; particle1->vx += ximpulse; @@ -850,14 +1008,104 @@ void handleCollision(PSparticle *particle1, PSparticle *particle2, const uint32_ } } -// slow down particle by friction, the higher the speed, the higher the friction coefficient must be <255 or friction is flipped -void applyFriction(PSparticle *particle, int32_t coefficient) +//fast calculation of particle wraparound (modulo version takes 37 instructions, this only takes 28, other variants are slower on ESP8266) +//function assumes that out of bounds is checked before calling it +int32_t ParticleSystem::wraparound(int32_t p, int32_t maxvalue) { - //note: to increase calculation efficiency, coefficient is not checked if it is within necessary limits of 0-255! if coefficient is made < 1 particles speed up! - coefficient = (int32_t)255 - coefficient; - if (particle->ttl) + if (p < 0) + { + p += maxvalue + 1; + } + else //if (p > maxvalue) + { + p -= maxvalue + 1; + } + return p; +} + +//calculate the dV value and update the counter for force calculation (is used several times, function saves on codesize) +//force is in 3.4 fixedpoint notation, +/-127 +int32_t ParticleSystem::calcForce_dV(int8_t force, uint8_t* counter) +{ + // for small forces, need to use a delay counter + int32_t force_abs = abs(force); // absolute value (faster than lots of if's only 7 instructions) + int32_t dv; + // for small forces, need to use a delay counter, apply force only if it overflows + if (force_abs < 16) + { + *counter += force_abs; + if (*counter > 15) + { + *counter -= 16; + dv = (force < 0) ? -1 : ((force > 0) ? 1 : 0); // force is either, 1, 0 or -1 if it is small + } + } + else { - particle->vx = ((int16_t)particle->vx * coefficient) >> 8; - particle->vy = ((int16_t)particle->vy * coefficient) >> 8; + dv = force >> 4; // MSBs } + return dv; } + +// 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::setPSpointers(uint16_t numsources) +{ + particles = reinterpret_cast(SEGMENT.data + sizeof(ParticleSystem)); // pointer to particle array + sources = reinterpret_cast(particles + numParticles); // pointer to source(s) + PSdataEnd = reinterpret_cast(sources + numsources); // pointer to first available byte after the PS +} + +//non class functions to use for initialization + +uint32_t calculateNumberOfParticles() +{ + uint32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; + uint32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); +#ifdef ESP8266 + uint32_t numberofParticles = cols * rows ; // 1 particle per pixel +#elseif ARDUINO_ARCH_ESP32S2 + uint32_t numberofParticles = (cols * rows * 3) / 2; // 1.5 particles per pixel (for example 768 particles on 32x16) +#else + uint32_t numberofParticles = (cols * rows * 7) / 4; // 1.75 particles per pixel +#endif + + Serial.print("segsize "); + Serial.print(cols); + Serial.print(" "); + Serial.println(rows); + // TODO: ist das genug für fire auf 32x16? evtl auf 2 gehen? oder das dynamisch machen, als parameter? + return numberofParticles; +} + +//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) +{ + uint32_t requiredmemory = sizeof(ParticleSystem); + requiredmemory += sizeof(PSparticle) * numparticles; + requiredmemory += sizeof(PSsource) * numsources; + requiredmemory += additionalbytes; + return(SEGMENT.allocateData(requiredmemory)); +} + +bool initParticleSystem(ParticleSystem *&PartSys, uint16_t numsources) +{ + Serial.println("PS init function"); + uint32_t numparticles = calculateNumberOfParticles(); + if (!allocateParticleSystemMemory(numparticles, numsources, 0)) + { + DEBUG_PRINT(F("PS init failed: memory depleted")); + return false; + } + Serial.println("memory allocated"); + 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); // particle system constructor + Serial.print("PS pointer at "); + Serial.println((uintptr_t)PartSys); + return true; +} + + diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index f4c2163757..f86b13a074 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -3,7 +3,6 @@ Particle system with functions for particle generation, particle movement and particle rendering to RGB matrix. by DedeHai (Damian Schneider) 2013-2024 - Rendering is based on algorithm by giladaya, https://github.com/giladaya/arduino-particle-sys LICENSE The MIT License (MIT) @@ -35,6 +34,7 @@ #define PS_P_RADIUS_SHIFT 6 // shift for RADIUS #define PS_P_SURFACE 12 // shift: 2^PS_P_SURFACE = (PS_P_RADIUS)^2 #define PS_P_HARDRADIUS 80 //hard surface radius of a particle, used for collision detection proximity +#define PS_P_MAXSPEED 255 //maximum speed a particle can have //struct for a single particle typedef struct { @@ -42,41 +42,135 @@ typedef struct { int16_t y; //y position in particle system int8_t vx; //horizontal velocity int8_t vy; //vertical velocity - uint16_t ttl; // time to live uint8_t hue; // color hue uint8_t sat; // color saturation - //add a one byte bit field: + //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 collide : 1; //if flag is set, particle will take part in collisions - bool flag2 : 1; // unused flags... could use one for collisions to make those selective. + bool flag1 : 1; // unused flags... + bool flag2 : 1; bool flag3 : 1; - uint8_t counter : 4; //a 4 bit counter for particle control + + 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 uint16_t maxLife; //maximum ttl of emitted particles - PSparticle source; //use a particle as the emitter source (speed, position, color) - uint8_t var; //variation of emitted speed - int8_t vx; //emitting speed - int8_t vy; //emitting speed -} PSpointsource; - -#define GRAVITYCOUNTER 2 //the higher the value the lower the gravity (speed is increased every n'th particle update call), values of 1 to 4 give good results -#define MAXGRAVITYSPEED 40 //particle terminal velocity - -void Emitter_Flame_emit(PSpointsource *emitter, PSparticle *part); -void Emitter_Fountain_emit(PSpointsource *emitter, PSparticle *part); -void Emitter_Angle_emit(PSpointsource *emitter, PSparticle *part, uint8_t angle, uint8_t speed); -void Particle_attractor(PSparticle *particle, PSparticle *attractor, uint8_t *counter, uint8_t strength, bool swallow); -void Particle_Move_update(PSparticle *part, bool killoutofbounds = false, bool wrapX = false, bool wrapY = false); -void Particle_Bounce_update(PSparticle *part, const uint8_t hardness); -void Particle_Gravity_update(PSparticle *part, bool wrapX, bool bounceX, bool bounceY, const uint8_t hardness); -void ParticleSys_render(PSparticle *particles, uint32_t numParticles, bool wrapX, bool wrapY); -void FireParticle_update(PSparticle *part, uint32_t numparticles, bool wrapX = false); -void ParticleSys_renderParticleFire(PSparticle *particles, uint32_t numParticles, bool wrapX); -void PartMatrix_addHeat(uint8_t col, uint8_t row, uint32_t heat, uint32_t rows); -void detectCollisions(PSparticle *particles, uint32_t numparticles, uint8_t hardness); -void handleCollision(PSparticle *particle1, PSparticle *particle2, const uint32_t hardness); -void applyFriction(PSparticle *particle, int32_t coefficient); + PSparticle source; //use a particle as the emitter source (speed, position, color) + uint8_t var; //variation of emitted speed + int8_t vx; //emitting speed + int8_t vy; //emitting speed +} PSsource; + +// struct for PS settings +typedef struct +{ + // add a one byte bit field: + bool killoutofbounds : 1; // if set, out of bound particles are killed immediately + bool wrapX : 1; + bool wrapY : 1; + bool bounceX : 1; + bool bounceY : 1; + bool useGravity : 1; //set to 1 if gravity is used, disables bounceY at the top + bool useCollisions : 1; + bool flag8 : 1; // unused flag +} PSsettings; + +class ParticleSystem +{ +public: + ParticleSystem(uint16_t width, uint16_t height, uint16_t numberofparticles, uint16_t numberofsources); // 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(uint8_t colormode); // update function for fire + + // particle emitters + void FlameEmit(PSsource &emitter); + void SprayEmit(PSsource &emitter); + void AngleEmit(PSsource& emitter, uint8_t angle, uint32_t speed); + + //move functions + void ParticleMoveUpdate(PSparticle &part, PSsettings &options); + void FireParticle_update(); + + //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 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, uint32_t numparticles, uint8_t coefficient); // apply friction + void attract(PSparticle *particle, PSparticle *attractor, uint8_t *counter, uint8_t strength, bool swallow); + + //set options + void setUsedParticles(uint32_t num); + void setCollisionHardness(uint8_t hardness); //hardness for particle collisions (255 means full hard) + void setWallHardness(uint8_t hardness); //hardness for bouncing on the wall if bounceXY is set + void setMatrixSize(uint16_t x, uint16_t y); + void setWrapX(bool enable); + void setWrapY(bool enable); + void setBounceX(bool enable); + void setBounceY(bool enable); + void setKillOutOfBounds(bool enable); //if enabled, particles outside of matrix instantly die + void enableGravity(bool enable, uint8_t force = 8); + void enableParticleCollisions(bool enable, uint8_t hardness = 255); + // get options + int16_t getMaxParticles(void); // read size of particle array + + void setPSpointers(uint16_t numsources); //call this after allocating the memory to initialize the PS pointers + + PSparticle *particles; // pointer to particle array + PSsource *sources; + uint8_t* PSdataEnd; //points to first available byte after the PSmemory, is set in setPointers(). use this to set pointer to FX custom data + uint32_t maxXpixel, maxYpixel; // last physical pixel that can be drawn to + uint32_t maxX, maxY; //particle system size (subpixelsize) + uint32_t numParticles; // number of particles available in this system + uint32_t usedParticles; // number of particles used in animation (can be smaller then numParticles) + +private: + //rendering functions + void ParticleSys_render(); + void renderParticleFire(uint8_t colormode); + void PartMatrix_addHeat(uint8_t col, uint8_t row, uint32_t heat, uint8_t colormode); + void renderParticle(PSparticle *particle, uint32_t brightess, int32_t *pixelvalues, int32_t (*pixelpositions)[2]); + + //paricle physics applied by system if flags are set + void handleCollisions(); + void collideParticles(PSparticle *particle1, PSparticle *particle2); + + //utility functions + int32_t wraparound(int32_t w, int32_t maxvalue); + int32_t calcForce_dV(int8_t force, uint8_t *counter); + + // note: variables that are accessed often are 32bit for speed + + //uint8_t numSources; //number of sources note: currently not needed, is handled by FX, only needed to init the pointers correctly + + uint32_t emitIndex; //index to count through particles to emit so searching for dead pixels is faster + int32_t collisionHardness; + int32_t wallHardness; + 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); +uint32_t calculateNumberOfParticles(); +bool allocateParticleSystemMemory(uint16_t numparticles, uint16_t numsources, uint16_t additionalbytes);