From 538ffdbf46abf42e75c261d1a021d0ecf1426543 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sat, 3 Feb 2024 10:45:34 +0100 Subject: [PATCH 001/219] Adding Particle System functions Basic Particle system defining particle structs, some emitters and particle to LED rendering --- wled00/FXparticleSystem.cpp | 506 ++++++++++++++++++++++++++++++++++++ wled00/FXparticleSystem.h | 95 +++++++ 2 files changed, 601 insertions(+) create mode 100644 wled00/FXparticleSystem.cpp create mode 100644 wled00/FXparticleSystem.h diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp new file mode 100644 index 0000000000..1ee97c56e1 --- /dev/null +++ b/wled00/FXparticleSystem.cpp @@ -0,0 +1,506 @@ +/* + FXparticleSystem.cpp + + 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) + Copyright (c) 2024 Damian Schneider + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + +*/ + +#include "FXparticleSystem.h" +#include "wled.h" +#include "FastLED.h" +#include "FX.h" + +//Fountain style emitter for simple particles used for flames (particle TTL depends on source TTL) +void Emitter_Flame_emit(PSpointsource *emitter, PSsimpleparticle *part) { + part->x = emitter->source.x + (rand() % emitter->var) - ((emitter->var) >> 1); + part->y = emitter->source.y + (rand() % emitter->var) - ((emitter->var) >> 1); + part->vx = emitter->vx + (rand() % emitter->var) - ((emitter->var) >> 1); + part->vy = emitter->vy + (rand() % emitter->var) - ((emitter->var) >> 1); + part->ttl = (rand() % (emitter->maxLife - emitter->minLife)) + emitter->minLife + emitter->source.ttl; //flame intensity dies down with emitter TTL + part->hue = emitter->source.hue; +} + +//fountain style emitter +void Emitter_Fountain_emit(PSpointsource *emitter, PSparticle *part) { + part->x = emitter->source.x + (rand() % emitter->var) + - ((emitter->var) >> 1); + part->y = emitter->source.y + (rand() % emitter->var) + - ((emitter->var) >> 1); + part->vx = emitter->vx + (rand() % emitter->var) - ((emitter->var) >> 1); + part->vy = emitter->vy + (rand() % emitter->var) - ((emitter->var) >> 1); + part->ttl = (rand() % (emitter->maxLife - emitter->minLife)) + emitter->minLife; + part->hue = emitter->source.hue; +//part->isAlive = 1; +} + +void Particle_Move_update(PSparticle *part) //particle moves, decays and dies (age or out of matrix) +{ + //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) { + //age + part->ttl--; + + int16_t newX, newY; + //check if particle is out of bounds or dead + if ((part->y == 0) || (part->y >= PS_MAX_Y)) { + part->ttl = 0; + } + if ((part->x == 0) || (part->x >= PS_MAX_X)) { + part->ttl = 0; + } + if (part->vx == 0 && part->vy == 0) { + part->ttl = 0; + } + //apply velocity + + newX = part->x + (int16_t) part->vx; + newY = part->y + (int16_t) part->vy; + newX = max((int16_t)newX, (int16_t)0); //limit to positive + newY = max((int16_t)newY, (int16_t)0); + part->x = min((int16_t)newX, (int16_t)PS_MAX_X); //limit to matrix boundaries + part->y = min((int16_t)newY, (int16_t)PS_MAX_Y); + } +} + + +void Particle_Bounce_update(PSparticle *part) //bounces a particle on the matrix edges +{ + //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) { + //age + part->ttl--; + + //apply acceleration + // vx = min(vx + ax, PS_MAX_X); + // vy = min(vy + ay, PS_MAX_Y); + + //apply velocity + int16_t newX, newY; + + if ((part->vx == 0 && part->vy == 0)) { //stopped moving, make it die + part->ttl = 0; + } else { + if ((part->y == 0) || (part->y >= PS_MAX_Y)) { //reached an edge + part->vy = -part->vy; + } + if ((part->x == 0) || (part->x >= PS_MAX_X)) { //reached an edge + part->vx = -part->vx; + } + + newX = part->x + (int16_t) part->vx; + newY = part->y + (int16_t) part->vy; + 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); + } + } +} + + + +void Particle_Gravity_update(PSparticle *part, bool wrapX) //particle moves, decays and dies (age or out of matrix), if wrapX is set, pixels leaving in x direction reappear on other side +{ + + //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); + + int16_t newX, newY; + + if (part->ttl > 0) { + //age + part->ttl--; + //check if particle is out of bounds or died + if ((part->y < -PS_P_RADIUS) || (part->y >= PS_MAX_Y << 1)) { //if it moves more than 1 pixel below y=0, it will not come back + part->ttl = 0; + return; //particle died, we are done + } + if(wrapX==false){ + if ((part->x < -PS_MAX_X) || (part->x >= PS_MAX_X << 1)) { //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 + } + } + if (part->vx == 0 && part->vy == 0) { + part->ttl = 0; + return; //particle died, we are done + } + + //apply acceleration (gravity) + if (part->gravitycounterx == 0) //to reduce gravity below 1 + { + part->vy = part->vy - 1; + if (part->vy < -MAXGRAVITYSPEED) + part->vy = part->vy + 1; //revert speed to previous + part->gravitycounterx = GRAVITYCOUNTER; + } else { + part->gravitycounterx--; + } + //apply velocity + newX = part->x + (int16_t) part->vx; + newY = part->y + (int16_t) part->vy; + + //check if particle is outside of displayable matrix + + //x direction, handles wraparound + if(wrapX) + { + newX = newX % (PS_MAX_X+1); + if (newX < 0) + newX = PS_MAX_X-newX; + } + else { + if (newX < 0 || newX > PS_MAX_X) + part->outofbounds = 1; + else + part->outofbounds = 0; + } + + + //y direction + if(newY < 0 || newY > PS_MAX_Y) + part->outofbounds = 1; + else + part->outofbounds = 0; + + + //newX = max(newX, (int16_t)-PS_MAX_X); //limit to double matrix size + //newY = max(newY, (int16_t)-PS_MAX_Y); + //part->x = min(newX, (int16_t)(PS_MAX_X<<1)); //limit to double the space boundaries + //part->y = min(newY, (int16_t)(PS_MAX_Y<<1)); + part->x = newX; + part->y = newY; + } +} + +//render particles to the LED buffer (uses palette to render the 8bit particle color value) +void ParticleSys_render(PSparticle *particles, uint16_t numParticles) { + + 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); + + uint8_t x, y, dx, dy; + uint32_t intensity; //todo: can this be an uint8_t or will it mess things up? + CRGB baseRGB; + uint16_t i; + uint8_t brightess; //particle brightness, fades if dying + + +//go over particles and update matrix cells on the way + for (i = 0; i < numParticles; i++) { + if (particles[i].ttl == 0 || particles[i].outofbounds) { + continue; + } + //generate RGB values for particle + brightess = min(particles[i].ttl, (uint16_t)255); + //baseRGB = CHSV(particles[i].hue, 255, 255); + baseRGB = ColorFromPalette(SEGPALETTE, particles[i].hue , 255, LINEARBLEND); + + dx = (uint8_t) ((uint16_t) particles[i].x % (uint16_t) PS_P_RADIUS); + dy = (uint8_t) ((uint16_t) particles[i].y % (uint16_t) PS_P_RADIUS); + + x = (uint8_t) ((uint16_t) particles[i].x / (uint16_t) PS_P_RADIUS); + y = (uint8_t) ((uint16_t) particles[i].y / (uint16_t) PS_P_RADIUS); + + //for vx=1, vy=1: starts out with all four pixels at the same color (32/32) + //moves to upper right pixel (64/64) + //then moves one physical pixel up and right(+1/+1), starts out now with + //lower left pixel fully bright (0/0) and moves to all four pixel at same + //color (32/32) + + if (dx < (PS_P_RADIUS >> 1)) //jump to next physical pixel if half of virtual pixel size is reached + { + x--; //shift x to next pixel left, will overflow to 255 if 0 + dx = dx + (PS_P_RADIUS >> 1); + } else //if jump has ocurred, fade pixel out + { + dx = dx - (PS_P_RADIUS >> 1); //adjust dx so pixel fades + } + + if (dy < (PS_P_RADIUS >> 1)) //jump to next physical pixel if half of virtual pixel size is reached + { + y--; //shift y to next pixel down, will overflow to 255 if 0 + dy = dy + (PS_P_RADIUS >> 1); + } else { + //adjust dy so pixel fades + dy = dy - (PS_P_RADIUS >> 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 + if (x < cols && y < rows) { + //calculate the intensity with linear interpolation + intensity = ((uint32_t) ((PS_P_RADIUS) - dx) * ((PS_P_RADIUS) - dy) * (uint32_t) brightess) >> PS_P_SURFACE; //divide by PS_P_SURFACE to distribute the energy + //scale the particle base color by the intensity and add it to the pixel + SEGMENT.addPixelColorXY(x, rows-y-1, baseRGB.scale8(intensity)); + } + //bottom right; + x++; + if (x < cols && y < rows) { + intensity = ((uint32_t) dx * ((PS_P_RADIUS) - dy)* (uint32_t) brightess) >> PS_P_SURFACE; //divide by PS_P_SURFACE to distribute the energy + SEGMENT.addPixelColorXY(x, rows-y-1, baseRGB.scale8(intensity)); + } + //top right + y++; + if (x < cols && y < rows) { + intensity = ((uint32_t) dx * dy * (uint32_t) brightess) >> PS_P_SURFACE; //divide by PS_P_SURFACE to distribute the energy + SEGMENT.addPixelColorXY(x, rows-y-1, baseRGB.scale8(intensity)); + } + //top left + x--; + if (x < cols && y < rows) { + intensity = ((uint32_t) ((PS_P_RADIUS) - dx) * dy* (uint32_t) brightess) >> PS_P_SURFACE;//divide by PS_P_SURFACE to distribute the energy + SEGMENT.addPixelColorXY(x, rows-y-1, baseRGB.scale8(intensity)); + } + } +} + + +//update & move particle using simple particles, wraps around left/right if wrapX is true, wrap around up/down if wrapY is true +//todo: need to add parameter for fire? there is fire stuff in here, see comments below +void SimpleParticle_update(PSsimpleparticle *part, bool wrapX, bool wrapY) { + //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--; + + //apply velocity + part->x = part->x + (int16_t) part->vx; + part->y = part->y + (int16_t) part->vy + (part->ttl >> 4); //younger particles move faster upward as they are hotter, used for fire //TODO: need to make this optional? + + //check if particle is out of bounds, wrap around to other side if wrapping is enabled + //x-direction + if ((part->x < 0) || (part->x > PS_MAX_X)) { + if(wrapX) + { + part->x = part->x % (PS_MAX_X+1); + if (part->x < 0) + part->x = PS_MAX_X-part->x; + } + else { + part->ttl = 0; //todo: for round flame display, particles need to go modulo + } + } + + //y-direction + if ((part->y < -(PS_P_RADIUS<<4)) || (part->y > PS_MAX_Y)) { //position up to 8 pixels the matrix is allowed, used in fire for wider flames + if(wrapY) + { + part->y = part->y % (PS_MAX_Y+1); + if (part->y < 0) + part->y = PS_MAX_Y-part->y; + } + else { + part->ttl = 0; //todo: for round flame display, particles need to go modulo + } + } + } +} + +//render simple 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(PSsimpleparticle *particles, uint16_t numParticles) { + + 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); + + uint8_t x, y, dx, dy; + uint32_t tempVal; + uint16_t i; + + //go over particles and update matrix cells on the way + for (i = 0; i < numParticles; i++) { + if (particles[i].ttl == 0) { + continue; + } + + //simple particles do not have 'out of bound' parameter, need to check if particle is within matrix boundaries + dx = (uint8_t) ((uint16_t) particles[i].x % (uint16_t) PS_P_RADIUS); + dy = (uint8_t) ((uint16_t) particles[i].y % (uint16_t) PS_P_RADIUS); + + x = (uint8_t) ((uint16_t) particles[i].x / (uint16_t) PS_P_RADIUS); //compiler should optimize to bit shift + y = (uint8_t) ((uint16_t) particles[i].y / (uint16_t) PS_P_RADIUS); + + //for x=1, y=1: starts out with all four pixels at the same color (32/32) + //moves to upper right pixel (64/64) + //then moves one physical pixel up and right(+1/+1), starts out now with + //lower left pixel fully bright (0/0) and moves to all four pixel at same + //color (32/32) + + if (dx < (PS_P_RADIUS >> 1)) //jump to next physical pixel if half of virtual pixel size is reached + { + x--; //shift left, will overflow to 255 if 0 + dx = dx + (PS_P_RADIUS >> 1); + } else //if jump has ocurred, fade pixel out + { + //adjust dx so pixel fades out + dx = dx - (PS_P_RADIUS >> 1); + } + + if (dy < (PS_P_RADIUS >> 1)) //jump to next physical pixel if half of virtual pixel size is reached + { + y--; //shift row, will overflow to 255 if 0 + dy = dy + (PS_P_RADIUS >> 1); + } else { + //adjust dy so pixel fades out + dy = dy - (PS_P_RADIUS >> 1); + } + + //calculate brightness values for all four pixels representing a particle using linear interpolation + //bottom left + if (x < cols && y < rows) { + tempVal = (((uint32_t) ((PS_P_RADIUS) - dx) * ((PS_P_RADIUS) - dy)* (uint32_t) particles[i].ttl) >> PS_P_SURFACE); + PartMatrix_addHeat(x, y, tempVal); + PartMatrix_addHeat(x + 1, y, tempVal); //shift particle by 1 pixel to the right and add heat again (makes flame wider without using more particles) + } + //bottom right; + x++; + if (x < cols && y > PS_P_SURFACE); + PartMatrix_addHeat(x, y, tempVal); + PartMatrix_addHeat(x + 1, y, tempVal); //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) { + tempVal = (((uint32_t) dx * dy * (uint32_t) particles[i].ttl)>> PS_P_SURFACE); // + PartMatrix_addHeat(x, y, tempVal); + PartMatrix_addHeat(x + 1, y, tempVal); //shift particle by 1 pixel to the right and add heat again (makes flame wider without using more particles) + } + //top left + x--; + if (x < cols && y < rows) { + tempVal = (((uint32_t) ((PS_P_RADIUS) - dx) * dy* (uint32_t) particles[i].ttl) >> PS_P_SURFACE); + PartMatrix_addHeat(x, y, tempVal); + PartMatrix_addHeat(x + 1, y, tempVal); //shift particle by 1 pixel to the right and add heat again (makes flame wider without using more particles) + } + } +} + + +//adds 'heat' to red color channel, if it overflows, add it to green, if that overflows add it to blue +void PartMatrix_addHeat(uint8_t col, uint8_t row, uint16_t heat) { + + const uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); + + CRGB currentcolor = SEGMENT.getPixelColorXY(col, rows-row-1); //read current matrix color (flip y axis) + uint16_t newcolorvalue; + uint8_t colormode = SEGMENT.custom2>>5; //get color mode from slider (3bit value) + + //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 + + heat = heat << 3; //need to take a larger value to scale ttl value of particle to a good heat value that decays fast enough + +//i=0 is normal red fire, i=1 is green fire, i=2 is blue fire + uint8_t i = (colormode & 0x07) >> 1; + i = i % 3; + int8_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, is 16bit value + newcolorvalue = min(newcolorvalue, (uint16_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 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 > 0) //there is still heat left to be added + { + i += increment; + i = i % 3; + + if (currentcolor[i] < 255) { + newcolorvalue = (uint16_t)currentcolor[i] + heat; //add heat, check if it overflows + newcolorvalue = min(newcolorvalue,(uint16_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 > 0) //there is still heat left to be added + { + i += increment; + i = i % 3; + if (currentcolor[i] < 255) { + newcolorvalue = currentcolor[i] + heat; //add heat, check if it overflows + newcolorvalue = min(newcolorvalue, (uint16_t)50); //limit so it does not go full white + currentcolor[i] = (uint8_t)newcolorvalue; + } + + } + + + SEGMENT.setPixelColorXY(col, rows-row-1, currentcolor); + + +} \ No newline at end of file diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h new file mode 100644 index 0000000000..ffab7a8811 --- /dev/null +++ b/wled00/FXparticleSystem.h @@ -0,0 +1,95 @@ +/* + FXparticleSystem.cpp + + 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) + Copyright (c) 2024 Damian Schneider + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + +*/ + + +#include + +//particle dimensions (subpixel division) +#define PS_P_RADIUS 64 //subpixel size, each pixel is divided by this for particle movement +<<<<<<< Updated upstream +======= +#define PS_P_HARDRADIUS 100 //hard surface radius of a particle, in collisions, this is forbidden to be entered by another particle (for stacking) +>>>>>>> Stashed changes +#define PS_P_SURFACE 12 //shift: 2^PS_P_SURFACE = (PS_P_RADIUS)^2 + + +//struct for a single particle with gravity (12 bytes) +typedef struct { + int16_t x; //x position in particle system + int16_t y; //y position in particle system + uint16_t ttl; //time to live + uint8_t outofbounds; //set to 1 if outside of matrix + uint8_t hue; //hue + //uint8_t isAlive; //is alive? -> obsolete, ttl > 0 means alive. + uint8_t gravitycounterx; //apply gravity only if counter is 0; + uint8_t gravitycountery; //apply gravity only if counter is 0; + int8_t vx; //horizontal velocity + int8_t vy; //vertical velocity +} PSparticle; + + +//struct for a simple single particle with less memory, only short lived particles are possible (ttl is uint8_t), saves one byte (used for fire), 8bytes +typedef struct { + int16_t x; //x position in particle system + int16_t y; //y position in particle system + uint8_t ttl; //time to live + uint8_t hue; //hue + int8_t vx; //horizontal velocity + int8_t vy; //vertical velocity +} PSsimpleparticle; + +//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) + int8_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 2 to 4 give good results +#define MAXGRAVITYSPEED 40 //particle terminal velocity + +/* +//todo: make these local variables +uint8_t vortexspeed; //speed around vortex +uint8_t vortexdirection; //1 or 0 +int8_t vortexpull; //if positive, vortex pushes, if negative it pulls +*/ + +void Emitter_Flame_emit(PSpointsource *emitter, PSsimpleparticle *part); +void Emitter_Fountain_emit(PSpointsource *emitter, PSparticle *part); +void Particle_Move_update(PSparticle *part); +void Particle_Bounce_update(PSparticle *part); +void Particle_Gravity_update(PSparticle *part, bool wrapX); +void ParticleSys_render(PSparticle *particles, uint16_t numParticles); +void SimpleParticle_update(PSsimpleparticle *part, bool wrapX, bool WrapY); +void ParticleSys_renderParticleFire(PSsimpleparticle *particles, uint16_t numParticles); +void PartMatrix_addHeat(uint8_t col, uint8_t row, uint16_t heat); + From 45f26169043209d7f9cf7dfdc6bb142fa7ba21bd Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sun, 4 Feb 2024 10:02:46 +0100 Subject: [PATCH 002/219] added particle FX animations not yet final version but working --- wled00/FX.cpp | 869 ++++++++++++++++++++++++++++++++++++++++++++++++++ wled00/FX.h | 11 +- 2 files changed, 878 insertions(+), 2 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index a6303810f7..32f7a94448 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -27,6 +27,7 @@ #include "wled.h" #include "FX.h" #include "fcn_declare.h" +#include "FXparticleSystem.h" #define IBN 5100 @@ -7874,6 +7875,871 @@ uint16_t mode_2Dwavingcell() { static const char _data_FX_MODE_2DWAVINGCELL[] PROGMEM = "Waving Cell@!,,Amplitude 1,Amplitude 2,Amplitude 3;;!;2"; + +/* + * Particle gravity spray + * Particles are sprayed from below, spray moves back and forth + * Uses palette for particle color + * by DedeHai (Damian Schneider) + */ + + +uint16_t mode_particlespray(void) +{ + + if (SEGLEN == 1) return mode_static(); + +<<<<<<< Updated upstream + const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; + const uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); + + const uint16_t numParticles = 250; +======= + const uint16_t numParticles = 450; +>>>>>>> Stashed changes + const uint8_t numSprays = 1; + uint8_t percycle = numSprays; //maximum number of particles emitted per cycle + + //test, use static particles + + static PSparticle* particles; + static PSpointsource* spray; + + uint8_t i =0; + uint8_t j =0; + + + if (SEGMENT.call == 0) //initialization + { + //allocate memory and divide it into proper pointers + uint16_t dataSize = sizeof(PSparticle) * numParticles; + dataSize += sizeof(PSpointsource) * (numSprays+1); //+1 to avoid crashes due to memory alignment, makes sure there is a little more memory allocated than needed + if (!SEGENV.allocateData(dataSize)) return mode_static(); //allocation failed; //allocation failed + + // Serial.print(F("particle datasize = ")); + // Serial.println(dataSize); + 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 + + for(i=0; i>4); //how many particles are sprayed per cycle and how fast ist the color changing + if(spray[i].source.vx > 0) //moving to the right currently + { + spray[i].source.vx = SEGMENT.speed>>4; //spray speed + } + else + { + spray[i].source.vx = -(SEGMENT.speed>>4); //spray speed (is currently moving negative so keep it negative) + } + spray[i].vy = SEGMENT.custom1>>3; //emitting speed, upward + spray[i].vx = ((int16_t)SEGMENT.custom2-(int16_t)127)/8; //emitting speed, left/right (=angle) + spray[i].var = SEGMENT.custom3+1; //emiting variation = nozzle size (custom 3 goes from 0-32) + spray[i].source.ttl = 255; //source never dies, replenish its lifespan + } + + i = 0; + j = 0; + while(i>1; //maximum number of particles emitted per cycle + PSparticle* particles; + PSpointsource* flames; + + //allocate memory and divide it into proper pointers + uint32_t dataSize = sizeof(PSparticle) * numParticles; + dataSize += sizeof(PSpointsource) * (numFlames); + + //DEBUG_PRINTLN(F("**********************")); + //DEBUG_PRINT(F("particle datasize = ")); + //DEBUG_PRINTLN(dataSize); + + if (!SEGENV.allocateData(dataSize)){ + return mode_static(); //allocation failed; //allocation failed + } + + 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 + +>>>>>>> Stashed changes + uint16_t i; +/* +#ifdef FIRELAMP + uint16_t numParticles = 300; //number of particles to use + uint8_t perCycle = 15; +#else +uint16_t numParticles = 300; //number of particles to use +uint8_t perCycle = 12; //NUMBEROFFLAMES; //max number of emitted particles per cycle, use to fine tune the appearance, more means brighter flames but they can oscillate +#endif +*/ + + static PSsimpleparticle* simpleparticles; + static PSpointsource* flames; + + if (SEGMENT.call == 0) //initialization + { + DEBUG_PRINTLN(F("Initializing Particle Fire")); + //allocate memory and divide it into proper pointers + uint16_t dataSize = sizeof(PSsimpleparticle) * numParticles; + dataSize += sizeof(PSpointsource) * (numFlames+1); //+1 to avoid crashes due to memory alignment, makes sure there is a little more memory allocated than needed (TODO: need to find out why it really crashes) + + DEBUG_PRINTLN(F("**********************")); + DEBUG_PRINT(F("particle datasize = ")); + DEBUG_PRINTLN(dataSize); + + if (!SEGENV.allocateData(dataSize)){ + return mode_static(); //allocation failed; //allocation failed + } + + flames = reinterpret_cast(SEGENV.data); + //calculate the end of the spray data and assign it as the data pointer for the particles: + simpleparticles = reinterpret_cast(flames+numFlames); //cast the data array into a particle pointer + + //make sure all particles start out dead + for (i = 0; i < numParticles; i++) { + simpleparticles[i].ttl = 0; + } + + //initialize the flame sprays +#ifdef FIRELAMP + for (i = 0; i < numFlames; i++) { + flames[i].source.ttl = 0; + flames[i].source.x = PS_MAX_Y / 2 + (rand() % (PS_P_RADIUS * 6)) - PS_P_RADIUS * 3; //position araound the center + } +#else + for (i = 0; i < numFlames; i++) { + flames[i].source.ttl = 0; + flames[i].source.x = PS_MAX_X / 2 + (rand() % (PS_P_RADIUS * 8)) - PS_P_RADIUS * 4; //position araound the center TODO: make this dynamic depending on matrix size + //other parameters are set when creating the flame (see blow) + } +#endif + } + + //update the flame sprays: + for (i = 0; i < numFlames; i++) { + if (flames[i].source.ttl > 0) { + flames[i].source.ttl--; + flames[i].source.x += flames[i].source.vx; //move the source (if it has x-speed) + } + else //flame source is dead + { + +#ifdef FIRELAMP +//make some of the flames small and slow to add a bright base + if(i>2)/(1+(SEGMENT.speed>>6)) + 10; //'hotness' of fire, faster flames reduce the effect or flame height will scale too much with speed + flames[i].maxLife = random8(7) + 13; //defines flame height together with the vy speed, vy speed*maxlife/PS_P_RADIUS is the average flame height + flames[i].minLife = 2; + flames[i].vx = (int8_t)random8(4) - 2; //emitting speed (sideways) + flames[i].vy = 5+(SEGMENT.speed>>2); //emitting speed (upwards) + flames[i].var = random8(5) + 3; //speed variation around vx,vy (+/- var/2) +#endif + } + } + + static uint16_t windposition = 0; //position in the perlin noise matrix for wind generation + //if (rand() % 5 == 0) //change wind speed sometimes + { + windposition += 4; + } + + //update particles, create particles + uint8_t j = random8(numFlames); //start with a random flame (so each flame gets the chance to emit a particle if perCycle is smaller than number of flames) + for (i = 0; i < numParticles; i++) { + if (simpleparticles[i].ttl == 0 && percycle > 0) { + Emitter_Flame_emit(&flames[j], &simpleparticles[i]); + j++; + percycle--; + if (j >= numFlames) { + j = 0; + } + } + else if (simpleparticles[i].ttl) { //if particle is alive, update it + //add wind, using perlin noise + int8_t windspeed = (int8_t) (inoise8(windposition, simpleparticles[i].y >> 2) - 127) / ((271-SEGMENT.custom1)>>4); + simpleparticles[i].vx = windspeed; + SimpleParticle_update(&simpleparticles[i],false,false); //update particle, no wrapping + } + } + + SEGMENT.fill(BLACK); //clear the matrix + + //render the particles + ParticleSys_renderParticleFire(simpleparticles, numParticles); //draw matrix + + return FRAMETIME; + +} +static const char _data_FX_MODE_PARTICLEFIRE[] PROGMEM = "Particle Fire@Speed,Intensity,Wind Speed, Color Mode, check1, check2, check3;!,!;012;sx=100,ix=120,c1=128,c2=0,c3=8,o1=1,o2=0,o3=1"; +//static const char _data_FX_MODE_PARTICLEFIRE[] PROGMEM = "Particle Fire@Speed,Intensity,Wind Speed, asdf,Color Mode,check1, check2, check3;!,!;2;sx=88,ix=70,c1=190,c2=128,c3=8,o1=1,o2=0,o3=1"; + + + +<<<<<<< Updated upstream +======= +/* +particles falling down, user can enable these three options: X-wraparound, side bounce, ground bounce +sliders control falling speed, intensity (number of particles spawned), WIND OR SPEED RANDOMNESS?, inter-particle collision hardness (0 means no particle collisions) and render saturation +this is quite versatile, can be made to look like rain or snow or confetti, flying sparks etc. +Uses palette for particle color +by DedeHai (Damian Schneider) +*/ + +uint16_t mode_particlefall(void){ + + if (SEGLEN == 1) return mode_static(); + + const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; + const uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); + + const uint16_t numParticles = 500; + + PSparticle* particles; + + //allocate memory and divide it into proper pointers, max is 32k for all segments. + uint32_t dataSize = sizeof(PSparticle) * numParticles; + if (!SEGENV.allocateData(dataSize)) return mode_static(); //allocation failed; //allocation failed + + //calculate the end of the spray data and assign it as the data pointer for the particles: + particles = reinterpret_cast(SEGENV.data); //cast the data array into a particle pointer + + + uint16_t i =0; + uint16_t j =0; + + if (SEGMENT.call == 0) //initialization + { + for(i=0; i>2)) == 0 && SEGMENT.intensity>1) //every nth frame emit particles, stop emitting if zero + { + while(i>1)*PS_P_RADIUS+(cols>>1)*PS_P_RADIUS); //todo: could make this dynamic and random but needs a user variable + + particles[i].y=random16(rows*PS_P_RADIUS)+rows*PS_P_RADIUS; //particles appear somewhere above the matrix, maximum is double the height + particles[i].vx=(((int16_t)random8(SEGMENT.custom1))-(SEGMENT.custom1>>1))>>1; //side speed is +/- a quarter of the custom1 slider + particles[i].vy=-(SEGMENT.speed>>1); + particles[i].hue=random8(); //set random color + break; //quit loop if all particles of this round emitted + } + i++; + } + } + + uint8_t hardness = SEGMENT.custom2; //how hard the particle collisions are, if set to zero, no particle collision is calculated + + if(hardness > 0) { + //detect and handle collisions + int16_t startparticle = 0; + int16_t endparticle = numParticles/2; //do half the particles + + 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 which is overkill) + startparticle = endparticle; + endparticle = numParticles; + } + for(i=startparticle; i0) //if particle is alive + { + int32_t dx, dy; //distance to other particles + for (j = i + 1; j < numParticles; j++) { //check against higher number particles + if(particles[j].ttl>0){ //if target particle is alive + dx = particles[i].x - particles[j].x; + 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); + } + } + } + } + } + } + + //now move the particles + for(i=0; i(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 + + + uint16_t i =0; + uint16_t j =0; + + if (SEGMENT.call == 0) //initialization + { + for(i=0; i0) //every nth frame, cycle color and emit particles, do not emit if intensity is zero + { + + + for(i=0; i>4); //how many particles are sprayed per cycle and how fast ist the color changing + if(spray[i].source.vx > 0) //moving to the right currently + { + spray[i].source.vx = SEGMENT.speed>>4; //spray speed + } + else + { + spray[i].source.vx = -(SEGMENT.speed>>4); //spray speed (is currently moving negative so keep it negative) + } + spray[i].vy = -SEGMENT.custom1>>3; //emitting speed, down + spray[i].vx = ((int16_t)SEGMENT.custom2-(int16_t)127)/8; //emitting speed, left/right (=angle) + spray[i].var = 2; //emiting variation + //spray[i].source.y = spray[i].var+1; //need to 'lift up' the source as 'var' also changes particle spawn position randomly + spray[i].source.ttl = 255; //source never dies, replenish its lifespan + } + + i = 0; + j = 0; + while(i0) //if particle is alive + { + int32_t dx, dy; //distance to other particles + for (j = i + 1; j < numParticles; j++) { //check against higher number particles + if(particles[j].ttl>0){ //if target particle is alive + dx = particles[i].x - particles[j].x; + 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); + } + } + } + } + } + + //now move the particles + for(i=0; i(SEGENV.data); //cast the data array into a particle pointer + + + uint16_t i=0; + uint16_t j=0; + + if (SEGMENT.call == 0) //initialization + { + SEGMENT.aux0 = rand(); //position (either in noise or in sine function) + for(i=0; i>2)*PS_P_RADIUS); //in the bottom quarder + } + } + + + uint16_t displayparticles = SEGMENT.intensity; + + i = 0; + j = 0; + + + if(SEGMENT.call%(((255-SEGMENT.speed)>>6)+1)==0 && SEGMENT.speed > 0) //how often the force is applied depends on speed setting + { + + int8_t xgravity; + int8_t ygravity; + uint8_t scale; + + if(SEGMENT.check1){ //if random is set, use perlin noise for force vector generation + SEGMENT.aux0+=(SEGMENT.speed>>6)+1; //update position in noise + uint8_t angle = (((uint16_t)inoise8(SEGMENT.aux0))<<1)-64; //noise avg. value is 127 scale to 255 (to increase the noise range) and subtract 64 (=90°) to make the average direction downwards (270°), overflow means modulo, so is ok + //calculate x and y vectors from angle: + xgravity = ((int16_t)cos8(angle))-128; //gravity direction +/- 127 + ygravity = ((int16_t)sin8(angle))-128; + //scale the vectors using another inoise value: + scale = inoise8(SEGMENT.aux0+4096); //use a offset in the noise for scale value + //scale the force, if scale is small, make it zero (scale is applied below) + if(scale > (SEGMENT.custom1>>1)+64) scale = (SEGMENT.custom1>>1)+64; //max is 192 + if(scale < 64) scale = 0; + } + else{ //use sinusoidal motion + //angle needs to move from -270° to +90° (from top leftside to top rightside but limited by one of the sliders by the user (custom1=Amount), -270 (and +90) is ~64 in 8bit angle representation (365=255) + //the anglar force changes in a sinusoidal motion, like a rocking boat + //the angle is first calculated using a sine, then shifted so it goes from -127 to +127, then scaled, then shifted to 0 is actually -64 (=-90°=down) + + SEGMENT.aux0++; //move forward in the sinusoidal function + int16_t angle = (int16_t)sin8(SEGMENT.aux0)-128; //shift result (0-255 representing -1 to +1) so it goes from -128 to +127 + scale = 130-(abs(angle)); //force gets weaker at exteme positions + if(scale > 50) scale = 80; //force is strong everywhere but the top + angle = (angle*(int16_t)SEGMENT.custom1)>>8; //scale angle range according to slider + angle -= 63; //make 'down' (or -90°) the zero position + //now calculate the force vectors + xgravity = ((int16_t)cos8((uint8_t)angle))-128; //gravity direction +/- 127 + ygravity = ((int16_t)sin8((uint8_t)angle))-128; + } + + //scale gravity force + xgravity = ((int16_t)xgravity*scale)>>8; + ygravity = ((int16_t)ygravity*scale)>>8; + + //scale the gravity force down even more + xgravity = xgravity >>4; + ygravity = ygravity >>4; + + for(i=0; i0){ + particles[i].vx+=xgravity; + particles[i].vy+=ygravity; + particles[i].ttl=500; //particles never die + } + } + } + + //detect and handle collisions + int16_t startparticle = 0; + int16_t endparticle = displayparticles>>1; //do half the particles + + 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 which is overkill) + startparticle = endparticle; + endparticle = displayparticles; + } + + uint8_t hardness = SEGMENT.custom2; //how hard the collisions are, 255 = full hard. + //hardness = 255; + for(i=startparticle; i0) //if particle is alive + { + int32_t dx, dy; //distance to other particles + for (j = i + 1; j < displayparticles; j++) { //check against higher number particles + if(particles[j].ttl>0){ //if target particle is alive + dx = particles[i].x - particles[j].x; + 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); + } + } + } + } + } + + //now move the particles + for(i=0; i(SEGENV.data); // cast the data array into a particle pointer + + uint16_t i = 0; + uint16_t j = 0; + + if (SEGMENT.call == 0) // initialization + { + SEGMENT.aux0 = rand(); + for (i = 0; i < numParticles; i++) + { + particles[i].ttl = random16(500) + 200; + particles[i].x = random16(cols * PS_P_RADIUS); + particles[i].y = random16(rows * PS_P_RADIUS); + } + } + + uint16_t displayparticles = SEGMENT.intensity; + + // apply 'gravity' from a 2D perlin noise map + SEGMENT.aux0 += SEGMENT.speed >> 4; // noise z-position; + + // update position in noise + for (i = 0; i < displayparticles; i++) + { + + if (particles[i].ttl == 0) // revive dead particles (do not keep them alive forever, they can clump up, need to reseed) + { + particles[i].ttl = random16(500) + 200; + particles[i].x = random16(cols * PS_P_RADIUS); + particles[i].y = random16(rows * PS_P_RADIUS); + } + + int16_t baseheight = inoise8((particles[i].x >> 1), (particles[i].y >> 1), SEGMENT.aux0); // noise value at particle position + particles[i].hue = baseheight; // color particles to perlin noise value + if (SEGMENT.call % 6 == 0) // do not apply the force every frame, is too chaotic + { + int16_t xslope = baseheight - (int16_t)inoise8((particles[i].x >> 1) + 10, (particles[i].y >> 1), SEGMENT.aux0); + int16_t yslope = baseheight - (int16_t)inoise8((particles[i].x >> 1), (particles[i].y >> 1) + 10, SEGMENT.aux0); + // Serial.println(xslope); + particles[i].vx += xslope >> 1; // slope is about 0-8 + particles[i].vy += yslope >> 1; + } + } + + // move particles + for (i = 0; i < displayparticles; i++) + { + + // apply a bit of friction so particles are less jittery + if (SEGMENT.call % 16 == 0) // need to apply friction very rarely or particles will clump + applyFriction(&particles[i], 1); + + Particle_Bounce_update(&particles[i], (uint8_t)200); + } + + SEGMENT.fill(BLACK); // clear the matrix + + // render the particles + ParticleSys_render(particles, displayparticles, 255, false, false); + + return FRAMETIME; +} +static const char _data_FX_MODE_PARTICLEPERLIN[] PROGMEM = "Particle Perlin-Noise@,Particles,;;!;012;pal=1,ix=100"; + #endif // WLED_DISABLE_2D @@ -8114,6 +8980,9 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_2DWAVINGCELL, &mode_2Dwavingcell, _data_FX_MODE_2DWAVINGCELL); addEffect(FX_MODE_2DAKEMI, &mode_2DAkemi, _data_FX_MODE_2DAKEMI); // audio + + addEffect(FX_MODE_PARTICLESPRAY, &mode_particlespray, _data_FX_MODE_PARTICLESPRAY); + addEffect(FX_MODE_PARTICLEFIRE, &mode_particlefire, _data_FX_MODE_PARTICLEFIRE); #endif // WLED_DISABLE_2D } diff --git a/wled00/FX.h b/wled00/FX.h index abf33f9976..37eb2ba246 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -318,8 +318,15 @@ #define FX_MODE_WAVESINS 184 #define FX_MODE_ROCKTAVES 185 #define FX_MODE_2DAKEMI 186 - -#define MODE_COUNT 187 +#define FX_MODE_PARTICLESPRAY 187 +#define FX_MODE_PARTICLEFIRE 188 +#define FX_MODE_PARTICLEFIREWORKS 189 +#define FX_MODE_PARTICLEROTATINGSPRAY 190 +#define FX_MODE_PARTICLEPERLIN 191 +#define FX_MODE_PARTICLEFALL 192 +#define FX_MODE_PARTICLEBOX 193 +#define FX_MODE_PARTICLEPILE 194 +#define MODE_COUNT 195 typedef enum mapping1D2D { M12_Pixels = 0, From 2db3123d9c6e8f85c19cb6736c3a5fb2d4364988 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sun, 4 Feb 2024 10:04:04 +0100 Subject: [PATCH 003/219] removed comments --- wled00/FXparticleSystem.cpp | 8 -------- wled00/FXparticleSystem.h | 10 ---------- 2 files changed, 18 deletions(-) diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index 1ee97c56e1..a29df3b8eb 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -105,9 +105,6 @@ void Particle_Bounce_update(PSparticle *part) //bounces a particle on the matrix //age part->ttl--; - //apply acceleration - // vx = min(vx + ax, PS_MAX_X); - // vy = min(vy + ay, PS_MAX_Y); //apply velocity int16_t newX, newY; @@ -203,11 +200,6 @@ void Particle_Gravity_update(PSparticle *part, bool wrapX) //particle moves, dec else part->outofbounds = 0; - - //newX = max(newX, (int16_t)-PS_MAX_X); //limit to double matrix size - //newY = max(newY, (int16_t)-PS_MAX_Y); - //part->x = min(newX, (int16_t)(PS_MAX_X<<1)); //limit to double the space boundaries - //part->y = min(newY, (int16_t)(PS_MAX_Y<<1)); part->x = newX; part->y = newY; } diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index ffab7a8811..70d06360a3 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -31,10 +31,7 @@ //particle dimensions (subpixel division) #define PS_P_RADIUS 64 //subpixel size, each pixel is divided by this for particle movement -<<<<<<< Updated upstream -======= #define PS_P_HARDRADIUS 100 //hard surface radius of a particle, in collisions, this is forbidden to be entered by another particle (for stacking) ->>>>>>> Stashed changes #define PS_P_SURFACE 12 //shift: 2^PS_P_SURFACE = (PS_P_RADIUS)^2 @@ -76,13 +73,6 @@ typedef struct { #define GRAVITYCOUNTER 2 //the higher the value the lower the gravity (speed is increased every n'th particle update call), values of 2 to 4 give good results #define MAXGRAVITYSPEED 40 //particle terminal velocity -/* -//todo: make these local variables -uint8_t vortexspeed; //speed around vortex -uint8_t vortexdirection; //1 or 0 -int8_t vortexpull; //if positive, vortex pushes, if negative it pulls -*/ - void Emitter_Flame_emit(PSpointsource *emitter, PSsimpleparticle *part); void Emitter_Fountain_emit(PSpointsource *emitter, PSparticle *part); void Particle_Move_update(PSparticle *part); From c320b0ad80edb72ab09adcc22569f398f9015d86 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sun, 4 Feb 2024 10:13:54 +0100 Subject: [PATCH 004/219] cleanup --- wled00/FX.cpp | 64 +++++++++++++++++++++++---------------------------- 1 file changed, 29 insertions(+), 35 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 32f7a94448..78dfe98234 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -7889,17 +7889,15 @@ uint16_t mode_particlespray(void) if (SEGLEN == 1) return mode_static(); -<<<<<<< Updated upstream const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; const uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); const uint16_t numParticles = 250; -======= - const uint16_t numParticles = 450; ->>>>>>> Stashed changes const uint8_t numSprays = 1; uint8_t percycle = numSprays; //maximum number of particles emitted per cycle +//todo: for ESP8266 only about 250 particles are possible, for ESP32 450 or even more + //test, use static particles static PSparticle* particles; @@ -8045,13 +8043,9 @@ uint16_t mode_particlefire(void) const uint16_t PS_MAX_X (cols*PS_P_RADIUS-1); //const uint16_t PS_MAX_Y (rows*PS_P_RADIUS-1); -<<<<<<< Updated upstream - const uint16_t numParticles = 550; - const uint8_t numFlames = (cols-2)<<1; //number of flames: depends on fire width. for a fire width of 16 pixels total, about 25-30 flames give good results - uint8_t percycle = numFlames/2; //maximum number of particles emitted per cycle -======= //test for ESP8266 as it will not run with that many flames (not enough ram) const uint16_t numFlames = cols; //number of flames: depends on fire width. for a fire width of 16 pixels, about 25-30 flames give good results, add a few for the base flames + //for ESP32: (cols-2)<<1 TODO: need to fix this by using ifdef const uint16_t numParticles = numFlames*20; // const uint16_t numFlames = (cols<<1); //number of flames: depends on fire width. for a fire width of 16 pixels, about 25-30 flames give good results, add a few for the base flames @@ -8076,7 +8070,6 @@ uint16_t mode_particlefire(void) //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 ->>>>>>> Stashed changes uint16_t i; /* #ifdef FIRELAMP @@ -8230,46 +8223,47 @@ static const char _data_FX_MODE_PARTICLEFIRE[] PROGMEM = "Particle Fire@Speed,In <<<<<<< Updated upstream -======= -/* -particles falling down, user can enable these three options: X-wraparound, side bounce, ground bounce -sliders control falling speed, intensity (number of particles spawned), WIND OR SPEED RANDOMNESS?, inter-particle collision hardness (0 means no particle collisions) and render saturation -this is quite versatile, can be made to look like rain or snow or confetti, flying sparks etc. -Uses palette for particle color -by DedeHai (Damian Schneider) -*/ +== == == = + /* + particles falling down, user can enable these three options: X-wraparound, side bounce, ground bounce + sliders control falling speed, intensity (number of particles spawned), WIND OR SPEED RANDOMNESS?, inter-particle collision hardness (0 means no particle collisions) and render saturation + this is quite versatile, can be made to look like rain or snow or confetti, flying sparks etc. + Uses palette for particle color + by DedeHai (Damian Schneider) + */ + + uint16_t mode_particlefall(void) +{ -uint16_t mode_particlefall(void){ - - if (SEGLEN == 1) return mode_static(); + if (SEGLEN == 1) + return mode_static(); const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; const uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); const uint16_t numParticles = 500; - - PSparticle* particles; - //allocate memory and divide it into proper pointers, max is 32k for all segments. - uint32_t dataSize = sizeof(PSparticle) * numParticles; - if (!SEGENV.allocateData(dataSize)) return mode_static(); //allocation failed; //allocation failed + PSparticle *particles; - //calculate the end of the spray data and assign it as the data pointer for the particles: - particles = reinterpret_cast(SEGENV.data); //cast the data array into a particle pointer + // allocate memory and divide it into proper pointers, max is 32k for all segments. + uint32_t dataSize = sizeof(PSparticle) * numParticles; + if (!SEGENV.allocateData(dataSize)) + return mode_static(); // allocation failed; //allocation failed + // calculate the end of the spray data and assign it as the data pointer for the particles: + particles = reinterpret_cast(SEGENV.data); // cast the data array into a particle pointer - uint16_t i =0; - uint16_t j =0; - - if (SEGMENT.call == 0) //initialization + uint16_t i = 0; + uint16_t j = 0; + + if (SEGMENT.call == 0) // initialization { - for(i=0; i>2)) == 0 && SEGMENT.intensity>1) //every nth frame emit particles, stop emitting if zero { while(i Date: Sun, 4 Feb 2024 10:15:58 +0100 Subject: [PATCH 005/219] added collision handling back in update from another commit that got lost --- wled00/FXparticleSystem.cpp | 318 +++++++++++++++++++++++++----------- wled00/FXparticleSystem.h | 42 ++--- 2 files changed, 243 insertions(+), 117 deletions(-) diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index a29df3b8eb..ee62e846b4 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -32,28 +32,27 @@ #include "FX.h" //Fountain style emitter for simple particles used for flames (particle TTL depends on source TTL) -void Emitter_Flame_emit(PSpointsource *emitter, PSsimpleparticle *part) { - part->x = emitter->source.x + (rand() % emitter->var) - ((emitter->var) >> 1); - part->y = emitter->source.y + (rand() % emitter->var) - ((emitter->var) >> 1); - part->vx = emitter->vx + (rand() % emitter->var) - ((emitter->var) >> 1); - part->vy = emitter->vy + (rand() % emitter->var) - ((emitter->var) >> 1); - part->ttl = (rand() % (emitter->maxLife - emitter->minLife)) + emitter->minLife + emitter->source.ttl; //flame intensity dies down with emitter TTL +void Emitter_Flame_emit(PSpointsource *emitter, PSparticle *part) { + part->x = emitter->source.x + random8(emitter->var) - (emitter->var >> 1); + part->y = emitter->source.y + random8(emitter->var) - (emitter->var >> 1); + part->vx = emitter->vx + random8(emitter->var) - (emitter->var >> 1); + part->vy = emitter->vy + random8(emitter->var) - (emitter->var >> 1); + part->ttl = (uint8_t)((rand() % (emitter->maxLife - emitter->minLife)) + emitter->minLife + emitter->source.ttl); //flame intensity dies down with emitter TTL part->hue = emitter->source.hue; } //fountain style emitter void Emitter_Fountain_emit(PSpointsource *emitter, PSparticle *part) { - part->x = emitter->source.x + (rand() % emitter->var) - - ((emitter->var) >> 1); - part->y = emitter->source.y + (rand() % emitter->var) - - ((emitter->var) >> 1); - part->vx = emitter->vx + (rand() % emitter->var) - ((emitter->var) >> 1); - part->vy = emitter->vy + (rand() % emitter->var) - ((emitter->var) >> 1); + part->x = emitter->source.x + random8(emitter->var) - (emitter->var >> 1); + part->y = emitter->source.y + random8(emitter->var) - (emitter->var >> 1); + part->vx = emitter->vx + random8(emitter->var) - (emitter->var >> 1); + part->vy = emitter->vy + random8(emitter->var) - (emitter->var >> 1); part->ttl = (rand() % (emitter->maxLife - emitter->minLife)) + emitter->minLife; part->hue = emitter->source.hue; -//part->isAlive = 1; } +//TODO: could solve all update functions in a single function with parameters and handle gravity acceleration in a separte function (uses more cpu time but that is not a huge issue) + void Particle_Move_update(PSparticle *part) //particle moves, decays and dies (age or out of matrix) { //Matrix dimension @@ -64,34 +63,29 @@ void Particle_Move_update(PSparticle *part) //particle moves, decays and dies (a 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) { + if (part->ttl>0) { //age part->ttl--; - int16_t newX, newY; - //check if particle is out of bounds or dead - if ((part->y == 0) || (part->y >= PS_MAX_Y)) { + //apply velocity + part->x += (int16_t) part->vx; + part->y += (int16_t) part->vy; + + //check if particle is out of bounds + if ((part->y <= 0) || (part->y >= PS_MAX_Y)) { part->ttl = 0; } - if ((part->x == 0) || (part->x >= PS_MAX_X)) { + if ((part->x <= 0) || (part->x >= PS_MAX_X)) { part->ttl = 0; } if (part->vx == 0 && part->vy == 0) { part->ttl = 0; } - //apply velocity - - newX = part->x + (int16_t) part->vx; - newY = part->y + (int16_t) part->vy; - newX = max((int16_t)newX, (int16_t)0); //limit to positive - newY = max((int16_t)newY, (int16_t)0); - part->x = min((int16_t)newX, (int16_t)PS_MAX_X); //limit to matrix boundaries - part->y = min((int16_t)newY, (int16_t)PS_MAX_Y); } } -void Particle_Bounce_update(PSparticle *part) //bounces a particle on the matrix edges +void Particle_Bounce_update(PSparticle *part, const uint8_t hardness) //bounces a particle on the matrix edges, if surface 'hardness' is <255 some energy will be lost in collision (127 means 50% lost) { //Matrix dimension const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; @@ -101,37 +95,37 @@ void Particle_Bounce_update(PSparticle *part) //bounces a particle on the matrix 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) { + if (part->ttl>0) { //age part->ttl--; - //apply velocity int16_t newX, newY; - if ((part->vx == 0 && part->vy == 0)) { //stopped moving, make it die - part->ttl = 0; - } else { - if ((part->y == 0) || (part->y >= PS_MAX_Y)) { //reached an edge - part->vy = -part->vy; - } - if ((part->x == 0) || (part->x >= PS_MAX_X)) { //reached an edge - part->vx = -part->vx; - } + //apply velocity + newX = part->x + (int16_t) part->vx; + newY = part->y + (int16_t) part->vy; + + 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)>>8; //reduce speed as energy is lost on non-hard surface + } - newX = part->x + (int16_t) part->vx; - newY = part->y + (int16_t) part->vy; - 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); + 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)>>8; //reduce speed as energy is lost on non-hard surface } + + 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); } } -void Particle_Gravity_update(PSparticle *part, bool wrapX) //particle moves, decays and dies (age or out of matrix), if wrapX is set, pixels leaving in x direction reappear on other side +void Particle_Gravity_update(PSparticle *part, bool wrapX, bool bounceX, bool bounceY, const uint8_t hardness) //particle moves, decays and dies (age or out of matrix), 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) { //Matrix dimension @@ -142,13 +136,12 @@ void Particle_Gravity_update(PSparticle *part, bool wrapX) //particle moves, dec const uint16_t PS_MAX_X (cols*PS_P_RADIUS-1); const uint16_t PS_MAX_Y (rows*PS_P_RADIUS-1); - int16_t newX, newY; - if (part->ttl > 0) { //age part->ttl--; + //check if particle is out of bounds or died - if ((part->y < -PS_P_RADIUS) || (part->y >= PS_MAX_Y << 1)) { //if it moves more than 1 pixel below y=0, it will not come back + if ((part->y < -PS_P_RADIUS) || (part->y >= PS_MAX_Y << 1)) { //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 } @@ -158,55 +151,66 @@ void Particle_Gravity_update(PSparticle *part, bool wrapX) //particle moves, dec return; //particle died, we are done } } - if (part->vx == 0 && part->vy == 0) { - part->ttl = 0; - return; //particle died, we are done - } - //apply acceleration (gravity) - if (part->gravitycounterx == 0) //to reduce gravity below 1 - { - part->vy = part->vy - 1; - if (part->vy < -MAXGRAVITYSPEED) - part->vy = part->vy + 1; //revert speed to previous - part->gravitycounterx = GRAVITYCOUNTER; - } else { - part->gravitycounterx--; - } + //apply acceleration (gravity) every other frame, doing it every frame is too strong + if(SEGMENT.call % 2 == 0) + { + if (part->vy > -MAXGRAVITYSPEED) + part->vy = part->vy - 1; + } + //apply velocity + int16_t newX, newY; + newX = part->x + (int16_t) part->vx; newY = part->y + (int16_t) part->vy; + part->outofbounds = 0; //check if particle is outside of displayable matrix - //x direction, handles wraparound + //x direction, handle wraparound (will overrule bounce x) and bounceX if(wrapX) { newX = newX % (PS_MAX_X+1); if (newX < 0) - newX = PS_MAX_X-newX; + newX = PS_MAX_X-newX; } - else { - if (newX < 0 || newX > PS_MAX_X) - part->outofbounds = 1; - else - part->outofbounds = 0; + else { + 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, (int16_t)0); //limit to positive + newX = min(newX, (int16_t)PS_MAX_X); //limit to matrix boundaries + } + else //not bouncing and out of matrix + part->outofbounds = 1; + } } + 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){ + 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 + part->y += (int16_t) part->vy;//move particle back to within boundaries so it does not disappear for one frame + newY = max(newY, (int16_t)0); //limit to positive + //newY = min(newY, (int16_t)PS_MAX_Y); //limit to matrix boundaries + } + else //not bouncing and out of matrix + part->outofbounds = 1; + } - //y direction - if(newY < 0 || newY > PS_MAX_Y) - part->outofbounds = 1; - else - part->outofbounds = 0; - - part->x = newX; - part->y = newY; + part->y = newY; //set new position } } //render particles to the LED buffer (uses palette to render the 8bit particle color value) -void ParticleSys_render(PSparticle *particles, uint16_t numParticles) { +//if wrap is set, particles half out of bounds are rendered to the other side of the matrix +//saturation is color saturation, if not set to 255, hsv instead of palette is used (palette does not support saturation) +void ParticleSys_render(PSparticle *particles, uint16_t numParticles, uint8_t saturation, bool wrapX, bool wrapY) { const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; const uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); @@ -215,7 +219,8 @@ void ParticleSys_render(PSparticle *particles, uint16_t numParticles) { const uint16_t PS_MAX_X (cols*PS_P_RADIUS-1); const uint16_t PS_MAX_Y (rows*PS_P_RADIUS-1); - uint8_t x, y, dx, dy; + int16_t x, y; + uint8_t dx, dy; uint32_t intensity; //todo: can this be an uint8_t or will it mess things up? CRGB baseRGB; uint16_t i; @@ -229,8 +234,14 @@ void ParticleSys_render(PSparticle *particles, uint16_t numParticles) { } //generate RGB values for particle brightess = min(particles[i].ttl, (uint16_t)255); - //baseRGB = CHSV(particles[i].hue, 255, 255); - baseRGB = ColorFromPalette(SEGPALETTE, particles[i].hue , 255, LINEARBLEND); + + if(saturation<255){ + CHSV baseHSV = rgb2hsv_approximate(ColorFromPalette(SEGPALETTE, particles[i].hue , 255, LINEARBLEND)); + baseHSV.s = saturation; + baseRGB = (CRGB)baseHSV; + } + else + baseRGB = ColorFromPalette(SEGPALETTE, particles[i].hue , 255, LINEARBLEND); dx = (uint8_t) ((uint16_t) particles[i].x % (uint16_t) PS_P_RADIUS); dy = (uint8_t) ((uint16_t) particles[i].y % (uint16_t) PS_P_RADIUS); @@ -262,6 +273,17 @@ void ParticleSys_render(PSparticle *particles, uint16_t numParticles) { dy = dy - (PS_P_RADIUS >> 1); } + 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; + } + } + //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%) @@ -275,18 +297,31 @@ void ParticleSys_render(PSparticle *particles, uint16_t numParticles) { } //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 = ((uint32_t) dx * ((PS_P_RADIUS) - dy)* (uint32_t) brightess) >> PS_P_SURFACE; //divide by PS_P_SURFACE to distribute the energy SEGMENT.addPixelColorXY(x, rows-y-1, baseRGB.scale8(intensity)); } //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 = ((uint32_t) dx * dy * (uint32_t) brightess) >> PS_P_SURFACE; //divide by PS_P_SURFACE to distribute the energy SEGMENT.addPixelColorXY(x, rows-y-1, baseRGB.scale8(intensity)); } //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 = ((uint32_t) ((PS_P_RADIUS) - dx) * dy* (uint32_t) brightess) >> PS_P_SURFACE;//divide by PS_P_SURFACE to distribute the energy SEGMENT.addPixelColorXY(x, rows-y-1, baseRGB.scale8(intensity)); @@ -296,8 +331,7 @@ void ParticleSys_render(PSparticle *particles, uint16_t numParticles) { //update & move particle using simple particles, wraps around left/right if wrapX is true, wrap around up/down if wrapY is true -//todo: need to add parameter for fire? there is fire stuff in here, see comments below -void SimpleParticle_update(PSsimpleparticle *part, bool wrapX, bool wrapY) { +void FireParticle_update(PSparticle *part, bool wrapX=false, bool wrapY=false) { //Matrix dimension const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; const uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); @@ -346,7 +380,7 @@ void SimpleParticle_update(PSsimpleparticle *part, bool wrapX, bool wrapY) { //render simple 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(PSsimpleparticle *particles, uint16_t numParticles) { +void ParticleSys_renderParticleFire(PSparticle *particles, uint16_t numParticles, bool wrapX) { const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; const uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); @@ -355,7 +389,8 @@ void ParticleSys_renderParticleFire(PSsimpleparticle *particles, uint16_t numPar const uint16_t PS_MAX_X (cols*PS_P_RADIUS-1); const uint16_t PS_MAX_Y (rows*PS_P_RADIUS-1); - uint8_t x, y, dx, dy; + int16_t x, y; + uint8_t dx, dy; uint32_t tempVal; uint16_t i; @@ -380,23 +415,29 @@ void ParticleSys_renderParticleFire(PSsimpleparticle *particles, uint16_t numPar if (dx < (PS_P_RADIUS >> 1)) //jump to next physical pixel if half of virtual pixel size is reached { - x--; //shift left, will overflow to 255 if 0 - dx = dx + (PS_P_RADIUS >> 1); - } else //if jump has ocurred, fade pixel out + x--; //shift left + dx = dx + (PS_P_RADIUS >> 1); //add half a radius + } else //if jump has ocurred, fade pixel { - //adjust dx so pixel fades out + //adjust dx so pixel fades dx = dx - (PS_P_RADIUS >> 1); } if (dy < (PS_P_RADIUS >> 1)) //jump to next physical pixel if half of virtual pixel size is reached { - y--; //shift row, will overflow to 255 if 0 + y--; //shift row dy = dy + (PS_P_RADIUS >> 1); } else { - //adjust dy so pixel fades out + //adjust dy so pixel fades dy = dy - (PS_P_RADIUS >> 1); } + 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; + } + } + //calculate brightness values for all four pixels representing a particle using linear interpolation //bottom left if (x < cols && y < rows) { @@ -406,6 +447,10 @@ void ParticleSys_renderParticleFire(PSsimpleparticle *particles, uint16_t numPar } //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 > PS_P_SURFACE); PartMatrix_addHeat(x, y, tempVal); @@ -420,6 +465,11 @@ void ParticleSys_renderParticleFire(PSsimpleparticle *particles, uint16_t numPar } //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) { tempVal = (((uint32_t) ((PS_P_RADIUS) - dx) * dy* (uint32_t) particles[i].ttl) >> PS_P_SURFACE); PartMatrix_addHeat(x, y, tempVal); @@ -436,7 +486,7 @@ void PartMatrix_addHeat(uint8_t col, uint8_t row, uint16_t heat) { CRGB currentcolor = SEGMENT.getPixelColorXY(col, rows-row-1); //read current matrix color (flip y axis) uint16_t newcolorvalue; - uint8_t colormode = SEGMENT.custom2>>5; //get color mode from slider (3bit value) + uint8_t colormode = map(SEGMENT.custom3, 0, 31, 0, 5); //get color mode from slider (3bit value) //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 @@ -495,4 +545,86 @@ void PartMatrix_addHeat(uint8_t col, uint8_t row, uint16_t heat) { SEGMENT.setPixelColorXY(col, rows-row-1, currentcolor); -} \ No newline at end of file +} + +//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) +void handleCollision(PSparticle *particle1, PSparticle *particle2, const uint8_t hardness) { + + int16_t dx = particle2->x - particle1->x; + int16_t dy = particle2->y - particle1->y; + int32_t distanceSquared = dx * dx + dy * dy + 1; //+1 so it is never zero to avoid division by zero below + if(distanceSquared == 0) //add 'noise' in case particles exactly meet at center, prevents dotProduct=0 (this can only happen if they move towards each other) + { + dx++; // + distanceSquared++; + } + + // Calculate relative velocity + int16_t relativeVx = (int16_t)particle2->vx - (int16_t)particle1->vx; + int16_t relativeVy = (int16_t)particle2->vy - (int16_t)particle1->vy; + + // Calculate dot product of relative velocity and relative distance + int32_t dotProduct = (dx * relativeVx + dy * relativeVy); + + // If particles are moving towards each other + if (dotProduct < 0) { + const uint8_t bitshift = 14; //bitshift used to avoid floats + + // Calculate new velocities after collision + int32_t impulse = (((dotProduct<<(bitshift)) / (distanceSquared))*hardness)>>8; + + particle1->vx += (impulse * dx)>>bitshift; + particle1->vy += (impulse * dy)>>bitshift; + particle2->vx -= (impulse * dx)>>bitshift; + particle2->vy -= (impulse * dy)>>bitshift; + + if(hardness<150) //if particles are soft, they become 'sticky' i.e. no slow movements + { + if(particle1->vx<2&&particle1->vx>-2) + particle1->vx=0; + if(particle1->vy<2&&particle1->vy>-2) + particle1->vy=0; + if(particle2->vx<2&&particle1->vx>-2) + particle1->vx=0; + if(particle2->vy<2&&particle1->vy>-2) + particle1->vy=0; + } + + //particles have volume, push particles apart if they are too close by moving each particle by a fixed amount away from the other particle + //move each particle by half of the amount they are overlapping, assumes square particles + + if(dx < 2*PS_P_HARDRADIUS && dx > -2*PS_P_HARDRADIUS){ //distance is too small + int8_t push=1; + if(dx<0) //dx is negative + { + push=-push; //invert push direction + } + particle1->x -= push; + particle2->x += push; + } + if(dy < 2*PS_P_HARDRADIUS && dy > -2*PS_P_HARDRADIUS){ //distance is too small (or negative) + int8_t push=1; + if(dy<0) //dy is negative + { + push=-push; //invert push direction + } + + particle1->y -= push; + particle2->y += push; + } + } + + //particles are close, apply friction -> makes them slow down in mid air, is not a good idea to apply here + //const uint8_t frictioncoefficient=4; + //applyFriction(particle1, frictioncoefficient); + //applyFriction(particle2, frictioncoefficient); + +} + +//slow down particle by friction, the higher the speed, the higher the friction +void applyFriction(PSparticle *particle, uint8_t coefficient) { + particle->vx = ((int16_t)particle->vx * (255 - coefficient)) >> 8; + particle->vy = ((int16_t)particle->vy * (255 - coefficient)) >> 8; +} + \ No newline at end of file diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index 70d06360a3..542cd5204d 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -31,7 +31,7 @@ //particle dimensions (subpixel division) #define PS_P_RADIUS 64 //subpixel size, each pixel is divided by this for particle movement -#define PS_P_HARDRADIUS 100 //hard surface radius of a particle, in collisions, this is forbidden to be entered by another particle (for stacking) +#define PS_P_HARDRADIUS 92 //hard surface radius of a particle, in collisions, this is forbidden to be entered by another particle (for stacking) #define PS_P_SURFACE 12 //shift: 2^PS_P_SURFACE = (PS_P_RADIUS)^2 @@ -42,44 +42,38 @@ typedef struct { uint16_t ttl; //time to live uint8_t outofbounds; //set to 1 if outside of matrix uint8_t hue; //hue - //uint8_t isAlive; //is alive? -> obsolete, ttl > 0 means alive. - uint8_t gravitycounterx; //apply gravity only if counter is 0; - uint8_t gravitycountery; //apply gravity only if counter is 0; int8_t vx; //horizontal velocity int8_t vy; //vertical velocity } PSparticle; - -//struct for a simple single particle with less memory, only short lived particles are possible (ttl is uint8_t), saves one byte (used for fire), 8bytes -typedef struct { - int16_t x; //x position in particle system - int16_t y; //y position in particle system - uint8_t ttl; //time to live - uint8_t hue; //hue - int8_t vx; //horizontal velocity - int8_t vy; //vertical velocity -} PSsimpleparticle; - //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) - int8_t var; //variation of emitted speed + 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 2 to 4 give good results +#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, PSsimpleparticle *part); +/* +//todo: make these local variables +uint8_t vortexspeed; //speed around vortex +uint8_t vortexdirection; //1 or 0 +int8_t vortexpull; //if positive, vortex pushes, if negative it pulls +*/ + +void Emitter_Flame_emit(PSpointsource *emitter, PSparticle *part); void Emitter_Fountain_emit(PSpointsource *emitter, PSparticle *part); void Particle_Move_update(PSparticle *part); -void Particle_Bounce_update(PSparticle *part); -void Particle_Gravity_update(PSparticle *part, bool wrapX); -void ParticleSys_render(PSparticle *particles, uint16_t numParticles); -void SimpleParticle_update(PSsimpleparticle *part, bool wrapX, bool WrapY); -void ParticleSys_renderParticleFire(PSsimpleparticle *particles, uint16_t numParticles); +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, uint16_t numParticles, uint8_t saturation, bool wrapX, bool wrapY); +void FireParticle_update(PSparticle *part, bool wrapX, bool WrapY); +void ParticleSys_renderParticleFire(PSparticle *particles, uint16_t numParticles, bool wrapX); void PartMatrix_addHeat(uint8_t col, uint8_t row, uint16_t heat); - +void handleCollision(PSparticle *particle1, PSparticle *particle2, const uint8_t hardness); +void applyFriction(PSparticle *particle, uint8_t coefficient); From c42e759c5d8ccc7a1b793b5d9f6de9a793d1ffdd Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sun, 4 Feb 2024 10:17:08 +0100 Subject: [PATCH 006/219] reformat --- wled00/FXparticleSystem.cpp | 846 +++++++++++++++++++----------------- 1 file changed, 452 insertions(+), 394 deletions(-) diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index ee62e846b4..2d17dc5251 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -7,7 +7,7 @@ LICENSE The MIT License (MIT) - Copyright (c) 2024 Damian Schneider + Copyright (c) 2024 Damian Schneider Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights @@ -31,600 +31,658 @@ #include "FastLED.h" #include "FX.h" -//Fountain style emitter for simple particles used for flames (particle TTL depends on source TTL) -void Emitter_Flame_emit(PSpointsource *emitter, PSparticle *part) { - part->x = emitter->source.x + random8(emitter->var) - (emitter->var >> 1); - part->y = emitter->source.y + random8(emitter->var) - (emitter->var >> 1); - part->vx = emitter->vx + random8(emitter->var) - (emitter->var >> 1); - part->vy = emitter->vy + random8(emitter->var) - (emitter->var >> 1); - part->ttl = (uint8_t)((rand() % (emitter->maxLife - emitter->minLife)) + emitter->minLife + emitter->source.ttl); //flame intensity dies down with emitter TTL +// Fountain style emitter for simple particles used for flames (particle TTL depends on source TTL) +void Emitter_Flame_emit(PSpointsource *emitter, PSparticle *part) +{ + part->x = emitter->source.x + random8(emitter->var) - (emitter->var >> 1); + part->y = emitter->source.y + random8(emitter->var) - (emitter->var >> 1); + part->vx = emitter->vx + random8(emitter->var) - (emitter->var >> 1); + part->vy = emitter->vy + random8(emitter->var) - (emitter->var >> 1); + part->ttl = (uint8_t)((rand() % (emitter->maxLife - emitter->minLife)) + emitter->minLife + emitter->source.ttl); // flame intensity dies down with emitter TTL part->hue = emitter->source.hue; } -//fountain style emitter -void Emitter_Fountain_emit(PSpointsource *emitter, PSparticle *part) { - part->x = emitter->source.x + random8(emitter->var) - (emitter->var >> 1); - part->y = emitter->source.y + random8(emitter->var) - (emitter->var >> 1); - part->vx = emitter->vx + random8(emitter->var) - (emitter->var >> 1); - part->vy = emitter->vy + random8(emitter->var) - (emitter->var >> 1); +// fountain style emitter +void Emitter_Fountain_emit(PSpointsource *emitter, PSparticle *part) +{ + part->x = emitter->source.x + random8(emitter->var) - (emitter->var >> 1); + part->y = emitter->source.y + random8(emitter->var) - (emitter->var >> 1); + part->vx = emitter->vx + random8(emitter->var) - (emitter->var >> 1); + part->vy = emitter->vy + random8(emitter->var) - (emitter->var >> 1); part->ttl = (rand() % (emitter->maxLife - emitter->minLife)) + emitter->minLife; part->hue = emitter->source.hue; } -//TODO: could solve all update functions in a single function with parameters and handle gravity acceleration in a separte function (uses more cpu time but that is not a huge issue) +// TODO: could solve all update functions in a single function with parameters and handle gravity acceleration in a separte function (uses more cpu time but that is not a huge issue) -void Particle_Move_update(PSparticle *part) //particle moves, decays and dies (age or out of matrix) +void Particle_Move_update(PSparticle *part) // particle moves, decays and dies (age or out of matrix) { - //Matrix dimension - const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; - const uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); + // 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); + // 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 + if (part->ttl > 0) + { + // age part->ttl--; - //apply velocity - part->x += (int16_t) part->vx; - part->y += (int16_t) part->vy; + // apply velocity + part->x += (int16_t)part->vx; + part->y += (int16_t)part->vy; - //check if particle is out of bounds - if ((part->y <= 0) || (part->y >= PS_MAX_Y)) { + // check if particle is out of bounds + if ((part->y <= 0) || (part->y >= PS_MAX_Y)) + { part->ttl = 0; } - if ((part->x <= 0) || (part->x >= PS_MAX_X)) { + if ((part->x <= 0) || (part->x >= PS_MAX_X)) + { part->ttl = 0; } - if (part->vx == 0 && part->vy == 0) { + if (part->vx == 0 && part->vy == 0) + { part->ttl = 0; } } } - -void Particle_Bounce_update(PSparticle *part, const uint8_t hardness) //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) // bounces a particle on the matrix edges, if surface 'hardness' is <255 some energy will be lost in collision (127 means 50% lost) { - //Matrix dimension - const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; - const uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); + // 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); + // 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 + if (part->ttl > 0) + { + // age part->ttl--; - //apply velocity + // apply velocity int16_t newX, newY; - - //apply velocity - newX = part->x + (int16_t) part->vx; - newY = part->y + (int16_t) part->vy; - - 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)>>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)>>8; //reduce speed as energy is lost on non-hard surface - } - - newX = max(newX, (int16_t)0); //limit to positive + + // apply velocity + newX = part->x + (int16_t)part->vx; + newY = part->y + (int16_t)part->vy; + + 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) >> 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) >> 8; // reduce speed as energy is lost on non-hard surface + } + + 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->x = min(newX, (int16_t)PS_MAX_X); // limit to matrix boundaries part->y = min(newY, (int16_t)PS_MAX_Y); } } - - -void Particle_Gravity_update(PSparticle *part, bool wrapX, bool bounceX, bool bounceY, const uint8_t hardness) //particle moves, decays and dies (age or out of matrix), 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) // particle moves, decays and dies (age or out of matrix), 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) { - //Matrix dimension - const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; - const uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); + // 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); + // 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 + if (part->ttl > 0) + { + // age part->ttl--; - //check if particle is out of bounds or died - if ((part->y < -PS_P_RADIUS) || (part->y >= PS_MAX_Y << 1)) { //if it moves more than 1 pixel below y=0, it will not come back. also remove particles that too far above + // check if particle is out of bounds or died + if ((part->y < -PS_P_RADIUS) || (part->y >= PS_MAX_Y << 1)) + { // 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 + return; // particle died, we are done } - if(wrapX==false){ - if ((part->x < -PS_MAX_X) || (part->x >= PS_MAX_X << 1)) { //left and right: keep it alive as long as its not too far out (if adding more effects like wind, it may come back) + if (wrapX == false) + { + if ((part->x < -PS_MAX_X) || (part->x >= PS_MAX_X << 1)) + { // 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 + return; // particle died, we are done } } - //apply acceleration (gravity) every other frame, doing it every frame is too strong - if(SEGMENT.call % 2 == 0) + // apply acceleration (gravity) every other frame, doing it every frame is too strong + if (SEGMENT.call % 2 == 0) { if (part->vy > -MAXGRAVITYSPEED) part->vy = part->vy - 1; - } + } - //apply velocity + // apply velocity int16_t newX, newY; - - newX = part->x + (int16_t) part->vx; - newY = part->y + (int16_t) part->vy; - part->outofbounds = 0; - //check if particle is outside of displayable matrix + newX = part->x + (int16_t)part->vx; + newY = part->y + (int16_t)part->vy; - //x direction, handle wraparound (will overrule bounce x) and bounceX - if(wrapX) + part->outofbounds = 0; + // check if particle is outside of displayable matrix + + // x direction, handle wraparound (will overrule bounce x) and bounceX + if (wrapX) + { + newX = newX % (PS_MAX_X + 1); + if (newX < 0) + newX = PS_MAX_X - newX; + } + else { - newX = newX % (PS_MAX_X+1); - if (newX < 0) - newX = PS_MAX_X-newX; - } - else { - 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, (int16_t)0); //limit to positive - newX = min(newX, (int16_t)PS_MAX_X); //limit to matrix boundaries + 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, (int16_t)0); // limit to positive + newX = min(newX, (int16_t)PS_MAX_X); // limit to matrix boundaries } - else //not bouncing and out of matrix - part->outofbounds = 1; + else // not bouncing and out of matrix + part->outofbounds = 1; } } - - 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){ - 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 - part->y += (int16_t) part->vy;//move particle back to within boundaries so it does not disappear for one frame - newY = max(newY, (int16_t)0); //limit to positive - //newY = min(newY, (int16_t)PS_MAX_Y); //limit to matrix boundaries + + 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) + { + 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 + part->y += (int16_t)part->vy; // move particle back to within boundaries so it does not disappear for one frame + newY = max(newY, (int16_t)0); // limit to positive + // newY = min(newY, (int16_t)PS_MAX_Y); //limit to matrix boundaries } - else //not bouncing and out of matrix + else // not bouncing and out of matrix part->outofbounds = 1; - } + } - part->y = newY; //set new position + 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 -//saturation is color saturation, if not set to 255, hsv instead of palette is used (palette does not support saturation) -void ParticleSys_render(PSparticle *particles, uint16_t numParticles, uint8_t saturation, bool wrapX, bool wrapY) { - - 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); - - int16_t x, y; +// 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 +// saturation is color saturation, if not set to 255, hsv instead of palette is used (palette does not support saturation) +void ParticleSys_render(PSparticle *particles, uint16_t numParticles, uint8_t saturation, bool wrapX, bool wrapY) +{ + + 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); + + int16_t x, y; uint8_t dx, dy; - uint32_t intensity; //todo: can this be an uint8_t or will it mess things up? - CRGB baseRGB; + uint32_t intensity; // todo: can this be an uint8_t or will it mess things up? + CRGB baseRGB; uint16_t i; - uint8_t brightess; //particle brightness, fades if dying - + uint8_t brightess; // particle brightness, fades if dying -//go over particles and update matrix cells on the way - for (i = 0; i < numParticles; i++) { - if (particles[i].ttl == 0 || particles[i].outofbounds) { + // go over particles and update matrix cells on the way + for (i = 0; i < numParticles; i++) + { + if (particles[i].ttl == 0 || particles[i].outofbounds) + { continue; } - //generate RGB values for particle + // generate RGB values for particle brightess = min(particles[i].ttl, (uint16_t)255); - - if(saturation<255){ - CHSV baseHSV = rgb2hsv_approximate(ColorFromPalette(SEGPALETTE, particles[i].hue , 255, LINEARBLEND)); + + if (saturation < 255) + { + CHSV baseHSV = rgb2hsv_approximate(ColorFromPalette(SEGPALETTE, particles[i].hue, 255, LINEARBLEND)); baseHSV.s = saturation; - baseRGB = (CRGB)baseHSV; + baseRGB = (CRGB)baseHSV; } else - baseRGB = ColorFromPalette(SEGPALETTE, particles[i].hue , 255, LINEARBLEND); + baseRGB = ColorFromPalette(SEGPALETTE, particles[i].hue, 255, LINEARBLEND); - dx = (uint8_t) ((uint16_t) particles[i].x % (uint16_t) PS_P_RADIUS); - dy = (uint8_t) ((uint16_t) particles[i].y % (uint16_t) PS_P_RADIUS); + dx = (uint8_t)((uint16_t)particles[i].x % (uint16_t)PS_P_RADIUS); + dy = (uint8_t)((uint16_t)particles[i].y % (uint16_t)PS_P_RADIUS); - x = (uint8_t) ((uint16_t) particles[i].x / (uint16_t) PS_P_RADIUS); - y = (uint8_t) ((uint16_t) particles[i].y / (uint16_t) PS_P_RADIUS); + x = (uint8_t)((uint16_t)particles[i].x / (uint16_t)PS_P_RADIUS); + y = (uint8_t)((uint16_t)particles[i].y / (uint16_t)PS_P_RADIUS); - //for vx=1, vy=1: starts out with all four pixels at the same color (32/32) - //moves to upper right pixel (64/64) - //then moves one physical pixel up and right(+1/+1), starts out now with - //lower left pixel fully bright (0/0) and moves to all four pixel at same - //color (32/32) + // for vx=1, vy=1: starts out with all four pixels at the same color (32/32) + // moves to upper right pixel (64/64) + // then moves one physical pixel up and right(+1/+1), starts out now with + // lower left pixel fully bright (0/0) and moves to all four pixel at same + // color (32/32) - if (dx < (PS_P_RADIUS >> 1)) //jump to next physical pixel if half of virtual pixel size is reached - { - x--; //shift x to next pixel left, will overflow to 255 if 0 + if (dx < (PS_P_RADIUS >> 1)) // jump to next physical pixel if half of virtual pixel size is reached + { + x--; // shift x to next pixel left, will overflow to 255 if 0 dx = dx + (PS_P_RADIUS >> 1); - } else //if jump has ocurred, fade pixel out - { - dx = dx - (PS_P_RADIUS >> 1); //adjust dx so pixel fades + } + else // if jump has ocurred, fade pixel out + { + dx = dx - (PS_P_RADIUS >> 1); // adjust dx so pixel fades } - if (dy < (PS_P_RADIUS >> 1)) //jump to next physical pixel if half of virtual pixel size is reached - { - y--; //shift y to next pixel down, will overflow to 255 if 0 + if (dy < (PS_P_RADIUS >> 1)) // jump to next physical pixel if half of virtual pixel size is reached + { + y--; // shift y to next pixel down, will overflow to 255 if 0 dy = dy + (PS_P_RADIUS >> 1); - } else { - //adjust dy so pixel fades + } + else + { + // adjust dy so pixel fades dy = dy - (PS_P_RADIUS >> 1); } - 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 (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; + 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; } } - //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%) + // 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 - if (x < cols && y < rows) { - //calculate the intensity with linear interpolation - intensity = ((uint32_t) ((PS_P_RADIUS) - dx) * ((PS_P_RADIUS) - dy) * (uint32_t) brightess) >> PS_P_SURFACE; //divide by PS_P_SURFACE to distribute the energy - //scale the particle base color by the intensity and add it to the pixel - SEGMENT.addPixelColorXY(x, rows-y-1, baseRGB.scale8(intensity)); + // bottom left + if (x < cols && y < rows) + { + // calculate the intensity with linear interpolation + intensity = ((uint32_t)((PS_P_RADIUS)-dx) * ((PS_P_RADIUS)-dy) * (uint32_t)brightess) >> PS_P_SURFACE; // divide by PS_P_SURFACE to distribute the energy + // scale the particle base color by the intensity and add it to the pixel + SEGMENT.addPixelColorXY(x, rows - y - 1, baseRGB.scale8(intensity)); } - //bottom right; + // 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 (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 = ((uint32_t) dx * ((PS_P_RADIUS) - dy)* (uint32_t) brightess) >> PS_P_SURFACE; //divide by PS_P_SURFACE to distribute the energy - SEGMENT.addPixelColorXY(x, rows-y-1, baseRGB.scale8(intensity)); + if (x < cols && y < rows) + { + intensity = ((uint32_t)dx * ((PS_P_RADIUS)-dy) * (uint32_t)brightess) >> PS_P_SURFACE; // divide by PS_P_SURFACE to distribute the energy + SEGMENT.addPixelColorXY(x, rows - y - 1, baseRGB.scale8(intensity)); } - //top right + // 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 (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 = ((uint32_t) dx * dy * (uint32_t) brightess) >> PS_P_SURFACE; //divide by PS_P_SURFACE to distribute the energy - SEGMENT.addPixelColorXY(x, rows-y-1, baseRGB.scale8(intensity)); + if (x < cols && y < rows) + { + intensity = ((uint32_t)dx * dy * (uint32_t)brightess) >> PS_P_SURFACE; // divide by PS_P_SURFACE to distribute the energy + SEGMENT.addPixelColorXY(x, rows - y - 1, baseRGB.scale8(intensity)); } - //top left + // 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 (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 = ((uint32_t) ((PS_P_RADIUS) - dx) * dy* (uint32_t) brightess) >> PS_P_SURFACE;//divide by PS_P_SURFACE to distribute the energy - SEGMENT.addPixelColorXY(x, rows-y-1, baseRGB.scale8(intensity)); + if (x < cols && y < rows) + { + intensity = ((uint32_t)((PS_P_RADIUS)-dx) * dy * (uint32_t)brightess) >> PS_P_SURFACE; // divide by PS_P_SURFACE to distribute the energy + SEGMENT.addPixelColorXY(x, rows - y - 1, baseRGB.scale8(intensity)); } } } +// update & move particle using simple particles, wraps around left/right if wrapX is true, wrap around up/down if wrapY is true +void FireParticle_update(PSparticle *part, bool wrapX = false, bool wrapY = false) +{ + // Matrix dimension + const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; + const uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); -//update & move particle using simple particles, wraps around left/right if wrapX is true, wrap around up/down if wrapY is true -void FireParticle_update(PSparticle *part, bool wrapX=false, bool wrapY=false) { - //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); - //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--; + // apply velocity + part->x = part->x + (int16_t)part->vx; + part->y = part->y + (int16_t)part->vy + (part->ttl >> 4); // younger particles move faster upward as they are hotter, used for fire //TODO: need to make this optional? - if (part->ttl>0) { - //age - part->ttl--; - - //apply velocity - part->x = part->x + (int16_t) part->vx; - part->y = part->y + (int16_t) part->vy + (part->ttl >> 4); //younger particles move faster upward as they are hotter, used for fire //TODO: need to make this optional? - - //check if particle is out of bounds, wrap around to other side if wrapping is enabled - //x-direction - if ((part->x < 0) || (part->x > PS_MAX_X)) { - if(wrapX) + // check if particle is out of bounds, wrap around to other side if wrapping is enabled + // x-direction + if ((part->x < 0) || (part->x > PS_MAX_X)) + { + if (wrapX) { - part->x = part->x % (PS_MAX_X+1); - if (part->x < 0) - part->x = PS_MAX_X-part->x; + part->x = part->x % (PS_MAX_X + 1); + if (part->x < 0) + part->x = PS_MAX_X - part->x; } - else { - part->ttl = 0; //todo: for round flame display, particles need to go modulo + else + { + part->ttl = 0; // todo: for round flame display, particles need to go modulo } } - - //y-direction - if ((part->y < -(PS_P_RADIUS<<4)) || (part->y > PS_MAX_Y)) { //position up to 8 pixels the matrix is allowed, used in fire for wider flames - if(wrapY) + + // y-direction + if ((part->y < -(PS_P_RADIUS << 4)) || (part->y > PS_MAX_Y)) + { // position up to 8 pixels the matrix is allowed, used in fire for wider flames + if (wrapY) { - part->y = part->y % (PS_MAX_Y+1); - if (part->y < 0) - part->y = PS_MAX_Y-part->y; + part->y = part->y % (PS_MAX_Y + 1); + if (part->y < 0) + part->y = PS_MAX_Y - part->y; } - else { - part->ttl = 0; //todo: for round flame display, particles need to go modulo + else + { + part->ttl = 0; // todo: for round flame display, particles need to go modulo } } } } -//render simple 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, uint16_t numParticles, bool wrapX) { - - 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); - +// render simple 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, uint16_t numParticles, bool wrapX) +{ + + 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); + int16_t x, y; uint8_t dx, dy; uint32_t tempVal; uint16_t i; - //go over particles and update matrix cells on the way - for (i = 0; i < numParticles; i++) { - if (particles[i].ttl == 0) { + // go over particles and update matrix cells on the way + for (i = 0; i < numParticles; i++) + { + if (particles[i].ttl == 0) + { continue; } - //simple particles do not have 'out of bound' parameter, need to check if particle is within matrix boundaries - dx = (uint8_t) ((uint16_t) particles[i].x % (uint16_t) PS_P_RADIUS); - dy = (uint8_t) ((uint16_t) particles[i].y % (uint16_t) PS_P_RADIUS); + // simple particles do not have 'out of bound' parameter, need to check if particle is within matrix boundaries + dx = (uint8_t)((uint16_t)particles[i].x % (uint16_t)PS_P_RADIUS); + dy = (uint8_t)((uint16_t)particles[i].y % (uint16_t)PS_P_RADIUS); - x = (uint8_t) ((uint16_t) particles[i].x / (uint16_t) PS_P_RADIUS); //compiler should optimize to bit shift - y = (uint8_t) ((uint16_t) particles[i].y / (uint16_t) PS_P_RADIUS); + x = (uint8_t)((uint16_t)particles[i].x / (uint16_t)PS_P_RADIUS); // compiler should optimize to bit shift + y = (uint8_t)((uint16_t)particles[i].y / (uint16_t)PS_P_RADIUS); - //for x=1, y=1: starts out with all four pixels at the same color (32/32) - //moves to upper right pixel (64/64) - //then moves one physical pixel up and right(+1/+1), starts out now with - //lower left pixel fully bright (0/0) and moves to all four pixel at same - //color (32/32) + // for x=1, y=1: starts out with all four pixels at the same color (32/32) + // moves to upper right pixel (64/64) + // then moves one physical pixel up and right(+1/+1), starts out now with + // lower left pixel fully bright (0/0) and moves to all four pixel at same + // color (32/32) - if (dx < (PS_P_RADIUS >> 1)) //jump to next physical pixel if half of virtual pixel size is reached - { - x--; //shift left - dx = dx + (PS_P_RADIUS >> 1); //add half a radius - } else //if jump has ocurred, fade pixel + if (dx < (PS_P_RADIUS >> 1)) // jump to next physical pixel if half of virtual pixel size is reached { - //adjust dx so pixel fades + x--; // shift left + dx = dx + (PS_P_RADIUS >> 1); // add half a radius + } + else // if jump has ocurred, fade pixel + { + // adjust dx so pixel fades dx = dx - (PS_P_RADIUS >> 1); } - if (dy < (PS_P_RADIUS >> 1)) //jump to next physical pixel if half of virtual pixel size is reached - { - y--; //shift row + if (dy < (PS_P_RADIUS >> 1)) // jump to next physical pixel if half of virtual pixel size is reached + { + y--; // shift row dy = dy + (PS_P_RADIUS >> 1); - } else { - //adjust dy so pixel fades + } + else + { + // adjust dy so pixel fades dy = dy - (PS_P_RADIUS >> 1); } - 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 (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; } } - //calculate brightness values for all four pixels representing a particle using linear interpolation - //bottom left - if (x < cols && y < rows) { - tempVal = (((uint32_t) ((PS_P_RADIUS) - dx) * ((PS_P_RADIUS) - dy)* (uint32_t) particles[i].ttl) >> PS_P_SURFACE); - PartMatrix_addHeat(x, y, tempVal); - PartMatrix_addHeat(x + 1, y, tempVal); //shift particle by 1 pixel to the right and add heat again (makes flame wider without using more particles) + // calculate brightness values for all four pixels representing a particle using linear interpolation + // bottom left + if (x < cols && y < rows) + { + tempVal = (((uint32_t)((PS_P_RADIUS)-dx) * ((PS_P_RADIUS)-dy) * (uint32_t)particles[i].ttl) >> PS_P_SURFACE); + PartMatrix_addHeat(x, y, tempVal); + PartMatrix_addHeat(x + 1, y, tempVal); // shift particle by 1 pixel to the right and add heat again (makes flame wider without using more particles) } - //bottom right; + // 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 (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 > PS_P_SURFACE); - PartMatrix_addHeat(x, y, tempVal); - PartMatrix_addHeat(x + 1, y, tempVal); //shift particle by 1 pixel to the right and add heat again (makes flame wider without using more particles) + if (x < cols && y < rows) + { + tempVal = (((uint32_t)dx * ((PS_P_RADIUS)-dy) * (uint32_t)particles[i].ttl) >> PS_P_SURFACE); + PartMatrix_addHeat(x, y, tempVal); + PartMatrix_addHeat(x + 1, y, tempVal); // shift particle by 1 pixel to the right and add heat again (makes flame wider without using more particles) } - //top right + // top right y++; - if (x < cols && y < rows) { - tempVal = (((uint32_t) dx * dy * (uint32_t) particles[i].ttl)>> PS_P_SURFACE); // - PartMatrix_addHeat(x, y, tempVal); - PartMatrix_addHeat(x + 1, y, tempVal); //shift particle by 1 pixel to the right and add heat again (makes flame wider without using more particles) + if (x < cols && y < rows) + { + tempVal = (((uint32_t)dx * dy * (uint32_t)particles[i].ttl) >> PS_P_SURFACE); // + PartMatrix_addHeat(x, y, tempVal); + PartMatrix_addHeat(x + 1, y, tempVal); // shift particle by 1 pixel to the right and add heat again (makes flame wider without using more particles) } - //top left + // 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 (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) { - tempVal = (((uint32_t) ((PS_P_RADIUS) - dx) * dy* (uint32_t) particles[i].ttl) >> PS_P_SURFACE); - PartMatrix_addHeat(x, y, tempVal); - PartMatrix_addHeat(x + 1, y, tempVal); //shift particle by 1 pixel to the right and add heat again (makes flame wider without using more particles) + if (x < cols && y < rows) + { + tempVal = (((uint32_t)((PS_P_RADIUS)-dx) * dy * (uint32_t)particles[i].ttl) >> PS_P_SURFACE); + PartMatrix_addHeat(x, y, tempVal); + PartMatrix_addHeat(x + 1, y, tempVal); // shift particle by 1 pixel to the right and add heat again (makes flame wider without using more particles) } } } +// adds 'heat' to red color channel, if it overflows, add it to green, if that overflows add it to blue +void PartMatrix_addHeat(uint8_t col, uint8_t row, uint16_t heat) +{ -//adds 'heat' to red color channel, if it overflows, add it to green, if that overflows add it to blue -void PartMatrix_addHeat(uint8_t col, uint8_t row, uint16_t heat) { - - const uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); + const uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); - CRGB currentcolor = SEGMENT.getPixelColorXY(col, rows-row-1); //read current matrix color (flip y axis) + CRGB currentcolor = SEGMENT.getPixelColorXY(col, rows - row - 1); // read current matrix color (flip y axis) uint16_t newcolorvalue; - uint8_t colormode = map(SEGMENT.custom3, 0, 31, 0, 5); //get color mode from slider (3bit value) + uint8_t colormode = map(SEGMENT.custom3, 0, 31, 0, 5); // get color mode from slider (3bit value) - //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 + // 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 - heat = heat << 3; //need to take a larger value to scale ttl value of particle to a good heat value that decays fast enough + heat = heat << 3; // need to take a larger value to scale ttl value of particle to a good heat value that decays fast enough -//i=0 is normal red fire, i=1 is green fire, i=2 is blue fire + // i=0 is normal red fire, i=1 is green fire, i=2 is blue fire uint8_t i = (colormode & 0x07) >> 1; i = i % 3; - int8_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, is 16bit value - newcolorvalue = min(newcolorvalue, (uint16_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 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 + int8_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, is 16bit value + newcolorvalue = min(newcolorvalue, (uint16_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 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 > 0) //there is still heat left to be added + if (heat > 0) // there is still heat left to be added { i += increment; i = i % 3; - if (currentcolor[i] < 255) { - newcolorvalue = (uint16_t)currentcolor[i] + heat; //add heat, check if it overflows - newcolorvalue = min(newcolorvalue,(uint16_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 + if (currentcolor[i] < 255) + { + newcolorvalue = (uint16_t)currentcolor[i] + heat; // add heat, check if it overflows + newcolorvalue = min(newcolorvalue, (uint16_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 > 0) //there is still heat left to be added + if (heat > 0) // there is still heat left to be added { i += increment; i = i % 3; - if (currentcolor[i] < 255) { - newcolorvalue = currentcolor[i] + heat; //add heat, check if it overflows - newcolorvalue = min(newcolorvalue, (uint16_t)50); //limit so it does not go full white + if (currentcolor[i] < 255) + { + newcolorvalue = currentcolor[i] + heat; // add heat, check if it overflows + newcolorvalue = min(newcolorvalue, (uint16_t)50); // limit so it does not go full white currentcolor[i] = (uint8_t)newcolorvalue; } - } - - SEGMENT.setPixelColorXY(col, rows-row-1, currentcolor); - - + SEGMENT.setPixelColorXY(col, rows - row - 1, currentcolor); } -//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) -void handleCollision(PSparticle *particle1, PSparticle *particle2, const uint8_t hardness) { - - int16_t dx = particle2->x - particle1->x; - int16_t dy = particle2->y - particle1->y; - int32_t distanceSquared = dx * dx + dy * dy + 1; //+1 so it is never zero to avoid division by zero below - if(distanceSquared == 0) //add 'noise' in case particles exactly meet at center, prevents dotProduct=0 (this can only happen if they move towards each other) - { - dx++; // - distanceSquared++; - } - +// 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) +void handleCollision(PSparticle *particle1, PSparticle *particle2, const uint8_t hardness) +{ + + int16_t dx = particle2->x - particle1->x; + int16_t dy = particle2->y - particle1->y; + int32_t distanceSquared = dx * dx + dy * dy + 1; //+1 so it is never zero to avoid division by zero below + if (distanceSquared == 0) // add 'noise' in case particles exactly meet at center, prevents dotProduct=0 (this can only happen if they move towards each other) + { + dx++; // + distanceSquared++; + } + // Calculate relative velocity int16_t relativeVx = (int16_t)particle2->vx - (int16_t)particle1->vx; int16_t relativeVy = (int16_t)particle2->vy - (int16_t)particle1->vy; // Calculate dot product of relative velocity and relative distance int32_t dotProduct = (dx * relativeVx + dy * relativeVy); - + // If particles are moving towards each other - if (dotProduct < 0) { - const uint8_t bitshift = 14; //bitshift used to avoid floats + if (dotProduct < 0) + { + const uint8_t bitshift = 14; // bitshift used to avoid floats // Calculate new velocities after collision - int32_t impulse = (((dotProduct<<(bitshift)) / (distanceSquared))*hardness)>>8; + int32_t impulse = (((dotProduct << (bitshift)) / (distanceSquared)) * hardness) >> 8; - particle1->vx += (impulse * dx)>>bitshift; - particle1->vy += (impulse * dy)>>bitshift; - particle2->vx -= (impulse * dx)>>bitshift; - particle2->vy -= (impulse * dy)>>bitshift; + particle1->vx += (impulse * dx) >> bitshift; + particle1->vy += (impulse * dy) >> bitshift; + particle2->vx -= (impulse * dx) >> bitshift; + particle2->vy -= (impulse * dy) >> bitshift; - if(hardness<150) //if particles are soft, they become 'sticky' i.e. no slow movements + if (hardness < 150) // if particles are soft, they become 'sticky' i.e. no slow movements { - if(particle1->vx<2&&particle1->vx>-2) - particle1->vx=0; - if(particle1->vy<2&&particle1->vy>-2) - particle1->vy=0; - if(particle2->vx<2&&particle1->vx>-2) - particle1->vx=0; - if(particle2->vy<2&&particle1->vy>-2) - particle1->vy=0; - } - - //particles have volume, push particles apart if they are too close by moving each particle by a fixed amount away from the other particle - //move each particle by half of the amount they are overlapping, assumes square particles - - if(dx < 2*PS_P_HARDRADIUS && dx > -2*PS_P_HARDRADIUS){ //distance is too small - int8_t push=1; - if(dx<0) //dx is negative + if (particle1->vx < 2 && particle1->vx > -2) + particle1->vx = 0; + if (particle1->vy < 2 && particle1->vy > -2) + particle1->vy = 0; + if (particle2->vx < 2 && particle1->vx > -2) + particle1->vx = 0; + if (particle2->vy < 2 && particle1->vy > -2) + particle1->vy = 0; + } + + // particles have volume, push particles apart if they are too close by moving each particle by a fixed amount away from the other particle + // move each particle by half of the amount they are overlapping, assumes square particles + + if (dx < 2 * PS_P_HARDRADIUS && dx > -2 * PS_P_HARDRADIUS) + { // distance is too small + int8_t push = 1; + if (dx < 0) // dx is negative { - push=-push; //invert push direction + push = -push; // invert push direction } - particle1->x -= push; - particle2->x += push; + particle1->x -= push; + particle2->x += push; } - if(dy < 2*PS_P_HARDRADIUS && dy > -2*PS_P_HARDRADIUS){ //distance is too small (or negative) - int8_t push=1; - if(dy<0) //dy is negative + if (dy < 2 * PS_P_HARDRADIUS && dy > -2 * PS_P_HARDRADIUS) + { // distance is too small (or negative) + int8_t push = 1; + if (dy < 0) // dy is negative { - push=-push; //invert push direction + push = -push; // invert push direction } - - particle1->y -= push; - particle2->y += push; - } + + particle1->y -= push; + particle2->y += push; + } } - - //particles are close, apply friction -> makes them slow down in mid air, is not a good idea to apply here - //const uint8_t frictioncoefficient=4; - //applyFriction(particle1, frictioncoefficient); - //applyFriction(particle2, frictioncoefficient); + // particles are close, apply friction -> makes them slow down in mid air, is not a good idea to apply here + // const uint8_t frictioncoefficient=4; + // applyFriction(particle1, frictioncoefficient); + // applyFriction(particle2, frictioncoefficient); } -//slow down particle by friction, the higher the speed, the higher the friction -void applyFriction(PSparticle *particle, uint8_t coefficient) { - particle->vx = ((int16_t)particle->vx * (255 - coefficient)) >> 8; - particle->vy = ((int16_t)particle->vy * (255 - coefficient)) >> 8; +// slow down particle by friction, the higher the speed, the higher the friction +void applyFriction(PSparticle *particle, uint8_t coefficient) +{ + particle->vx = ((int16_t)particle->vx * (255 - coefficient)) >> 8; + particle->vy = ((int16_t)particle->vy * (255 - coefficient)) >> 8; } - \ No newline at end of file From 6165083f160095e73410bcc3aedce7513cb3b094 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sun, 4 Feb 2024 10:20:42 +0100 Subject: [PATCH 007/219] added latest version of functions this somehow also got lost from an earlier commit --- wled00/FX.cpp | 1244 +++++++++++++++++++++++++++++-------------------- 1 file changed, 742 insertions(+), 502 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 78dfe98234..82e711f24f 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -7874,7 +7874,265 @@ uint16_t mode_2Dwavingcell() { } static const char _data_FX_MODE_2DWAVINGCELL[] PROGMEM = "Waving Cell@!,,Amplitude 1,Amplitude 2,Amplitude 3;;!;2"; +/* + * Particle rotating spray + * Particles sprayed from center with a rotating spray + * Uses palette for particle color + * by DedeHai (Damian Schneider) + */ + +uint16_t mode_particlerotatingspray(void) +{ + + if (SEGLEN == 1) + return mode_static(); + + const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; + const uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); + + const uint16_t numParticles = 400; + const uint8_t numSprays = 8; // maximum number of sprays + + PSparticle *particles; + PSpointsource *spray; + + // allocate memory and divide it into proper pointers, max is 32kB for all segments. + 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 + + uint16_t i = 0; + uint16_t j = 0; + uint8_t spraycount = 1 + (SEGMENT.custom2 >> 5); // number of sprays to display, 1-8 + + if (SEGMENT.call == 0) // initialization + { + SEGMENT.aux0 = 0; // starting angle + for (i = 0; i < numParticles; i++) + { + particles[i].ttl = 0; + } + for (i = 0; i < numSprays; i++) + { + spray[i].source.hue = random8(); + spray[i].source.x = (cols * PS_P_RADIUS) / 2; // center + spray[i].source.y = (cols * 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 + } + } + + // change source emitting color from time to time + if (SEGMENT.call % ((263 - SEGMENT.intensity) >> 3) == 0) // every nth frame, cycle color + { + for (i = 0; i < spraycount; i++) + { + spray[i].source.hue++; // = random8(); //change hue of spray source + } + } + + uint8_t percycle = spraycount; // maximum number of particles emitted per cycle + i = 0; + j = random(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 = random8(); //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++; + } + + // calculate angle offset for an even distribution + uint16_t angleoffset = 0xFFFF / spraycount; + + for (i = 0; i < spraycount; i++) + { + SEGMENT.aux0 += SEGMENT.speed << 2; + + // 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) / ((257 - SEGMENT.custom1) >> 1); // update spray angle (rotate all sprays with angle offset) + spray[i].vy = (sin16(SEGMENT.aux0 + angleoffset * i) >> 8) / ((257 - SEGMENT.custom1) >> 1); // 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) + } + + for (i = 0; i < numParticles; i++) + { + Particle_Move_update(&particles[i]); // move the particles + } + + SEGMENT.fill(BLACK); // clear the matrix + + // render the particles + ParticleSys_render(particles, numParticles, 255, false, false); + + return FRAMETIME; +} +static const char _data_FX_MODE_PARTICLEROTATINGSPRAY[] PROGMEM = "Rotating Particle Spray@Rotation Speed,Color Change,Particle Speed,Spray Count,Nozzle Size;;!;012;pal=6,sx=39,ix=178,c1=225,c2=128,c3=0"; + +/* + * Particle Fireworks + * Rockets shoot up and explode in a random color + * Use ranbow palette as default + * by DedeHai (Damian Schneider) + */ + +uint16_t mode_particlefireworks(void) +{ + + if (SEGLEN == 1) + return mode_static(); + + const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; + const uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); + + // particle system box dimensions + const uint16_t PS_MAX_X(cols * PS_P_RADIUS - 1); + const uint16_t PS_MAX_Y(rows * PS_P_RADIUS - 1); + + const uint16_t numParticles = 450; + const uint8_t numRockets = 3; + + PSparticle *particles; + PSpointsource *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); + if (!SEGENV.allocateData(dataSize)) + return mode_static(); // allocation failed; //allocation failed + + // DEBUG_PRINT(F("particle datasize = ")); + // DEBUG_PRINTLN(dataSize); + + 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 + + uint16_t i = 0; + uint16_t j = 0; + if (SEGMENT.call == 0) // initialization + { + for (i = 0; i < numParticles; i++) + { + particles[i].ttl = 0; + } + for (i = 0; i < numRockets; i++) + { + rockets[i].source.ttl = random8(20 * i); // first rocket starts immediately, others follow soon + rockets[i].source.vy = -1; // at negative speed, no particles are emitted and if rocket dies, it will be relaunched + } + } + + // update particles, create particles + + // check each rocket's state and emit particles according to its state: moving up = emit exhaust, at top = explode; falling down = standby time + uint16_t emitparticles; // number of particles to emit for each rocket's state + i = 0; + for (j = 0; j < numRockets; j++) + { + // determine rocket state by its speed: + if (rockets[j].source.vy > 0) + { // moving up, emit exhaust + emitparticles = 1; + } + else if (rockets[j].source.vy < 0) + { // falling down + emitparticles = 0; + } + else + { // speed is zero, explode! + emitparticles = random8(80) + 10; // defines the size of the explosion + rockets[j].source.vy = -1; // set speed negative so it will emit no more particles after this explosion until relaunch + } + + for (i; i < numParticles; i++) + { + if (particles[i].ttl == 0) + { // particle is dead + if (emitparticles > 0) + { + Emitter_Fountain_emit(&rockets[j], &particles[i]); + emitparticles--; + } + else + break; // done emitting for this rocket + } + } + } + + // update particles + for (i = 0; i < numParticles; i++) + { + if (particles[i].ttl) + { + Particle_Gravity_update(&particles[i], SEGMENT.check1, SEGMENT.check2, SEGMENT.check3, (uint8_t)255); + } + } + + // update the rockets, set the speed state + + // todo: man kann für das timing auch die rakete selbst benutzen: dazu einfach vy verwenden: ist es >0, steigt die rakete auf. ist ttl=0 wird vy = -1 gesetzt und ttl als standbyzeit gesetzt. + // ist vy<0 so wird emit funkion nicht aufgerufen. + // ist vy>0 so wird genau ein partikel emittiert. ist vy>0 und + for (i = 0; i < numRockets; i++) + { + if (rockets[i].source.ttl) + { + Particle_Move_update(&rockets[i].source); // move the rocket, age the rocket (ttl--) + } + else if (rockets[i].source.vy > 0) + { // rocket has died and is moving up. stop it so it will explode (is handled in the code above) + rockets[i].source.vy = 0; // set speed to zero so code above will recognize this as an exploding rocket + rockets[i].source.hue = random8(); // random color + rockets[i].maxLife = 250; + rockets[i].minLife = 100; + rockets[i].source.ttl = random8(120) + 50; // standby time til next launch (in frames, so about 2-5 seconds at 30fps) + rockets[i].vx = 0; // emitting speed + rockets[i].vy = 0; // emitting speed + rockets[i].var = 30; // speed variation around vx,vy (+/- var/2) + } + else if (rockets[i].source.vy < 0) // rocket is exploded and time is up (ttl=0 and negative speed), relaunch it + { + // reinitialize rocket + rockets[i].source.y = 1; // start from bottom + rockets[i].source.x = (rand() % (PS_MAX_X >> 1)) + (PS_MAX_Y >> 2); // centered half + rockets[i].source.vy = random8(10) + 5; + rockets[i].source.vx = random8(5) - 2; + rockets[i].source.hue = 30; // rocket exhaust = orange (if using rainbow palette) + rockets[i].source.ttl = random8(80) + 40; + rockets[i].maxLife = 40; + rockets[i].minLife = 10; + rockets[i].vx = 0; // emitting speed + rockets[i].vy = 0; // emitting speed + rockets[i].var = 6; // speed variation around vx,vy (+/- var/2) + } + } + SEGMENT.fill(BLACK); // clear the matrix + + // render the particles + ParticleSys_render(particles, numParticles, 255, false, false); + + return FRAMETIME; +} +static const char _data_FX_MODE_PARTICLEFIREWORKS[] PROGMEM = "Particle Fireworks@Moving Speed,Intensity,Particle Speed,Spray Angle,Nozzle Size,Wrap X,Bounce X,Bounce Y;;!;012;pal=11,sx=100,ix=200,c1=190,c2=128,c3=8,o1=0,o2=0,o3=0"; /* * Particle gravity spray @@ -7883,134 +8141,127 @@ static const char _data_FX_MODE_2DWAVINGCELL[] PROGMEM = "Waving Cell@!,,Amplitu * by DedeHai (Damian Schneider) */ - uint16_t mode_particlespray(void) { - - if (SEGLEN == 1) return mode_static(); + + if (SEGLEN == 1) + return mode_static(); const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; const uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); - const uint16_t numParticles = 250; + const uint16_t numParticles = 450; const uint8_t numSprays = 1; - uint8_t percycle = numSprays; //maximum number of particles emitted per cycle + uint8_t percycle = numSprays; // maximum number of particles emitted per cycle -//todo: for ESP8266 only about 250 particles are possible, for ESP32 450 or even more + PSparticle *particles; + PSpointsource *spray; - //test, use static particles + // allocate memory and divide it into proper pointers, max is 32k for all segments. + uint32_t dataSize = sizeof(PSparticle) * numParticles; + dataSize += sizeof(PSpointsource) * (numSprays + 1); //+1 to avoid crashes due to memory alignment, makes sure there is a little more memory allocated than needed + if (!SEGENV.allocateData(dataSize)) + return mode_static(); // allocation failed; //allocation failed - static PSparticle* particles; - static PSpointsource* spray; + // DEBUG_PRINT(F("particle datasize = ")); + // DEBUG_PRINTLN(dataSize); - uint8_t i =0; - uint8_t j =0; - + 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 + uint16_t i = 0; + uint16_t j = 0; + + if (SEGMENT.call == 0) // initialization { - //allocate memory and divide it into proper pointers - uint16_t dataSize = sizeof(PSparticle) * numParticles; - dataSize += sizeof(PSpointsource) * (numSprays+1); //+1 to avoid crashes due to memory alignment, makes sure there is a little more memory allocated than needed - if (!SEGENV.allocateData(dataSize)) return mode_static(); //allocation failed; //allocation failed - - // Serial.print(F("particle datasize = ")); - // Serial.println(dataSize); - 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 - - for(i=0; i>4); //how many particles are sprayed per cycle and how fast ist the color changing - if(spray[i].source.vx > 0) //moving to the right currently - { - spray[i].source.vx = SEGMENT.speed>>4; //spray speed - } - else - { - spray[i].source.vx = -(SEGMENT.speed>>4); //spray speed (is currently moving negative so keep it negative) - } - spray[i].vy = SEGMENT.custom1>>3; //emitting speed, upward - spray[i].vx = ((int16_t)SEGMENT.custom2-(int16_t)127)/8; //emitting speed, left/right (=angle) - spray[i].var = SEGMENT.custom3+1; //emiting variation = nozzle size (custom 3 goes from 0-32) - spray[i].source.ttl = 255; //source never dies, replenish its lifespan + for (i = 0; i < numSprays; i++) + { + // percycle = 1+(SEGMENT.intensity>>4); //how many particles are sprayed per cycle and how fast ist the color changing + if (spray[i].source.vx > 0) // moving to the right currently + { + spray[i].source.vx = SEGMENT.speed >> 4; // spray speed } + else + { + spray[i].source.vx = -(SEGMENT.speed >> 4); // spray speed (is currently moving negative so keep it negative) + } + spray[i].vy = SEGMENT.custom1 >> 3; // emitting speed, upward + spray[i].vx = ((int16_t)SEGMENT.custom2 - (int16_t)127) / 8; // emitting speed, left/right (=angle) + spray[i].var = SEGMENT.custom3 >> 1; // emiting variation = nozzle size (custom 3 goes from 0-32) + spray[i].source.y = spray[i].var + 1; // need to 'lift up' the source as 'var' also changes particle spawn position randomly + spray[i].source.ttl = 255; // source never dies, replenish its lifespan + } i = 0; j = 0; - while(i>1; //maximum number of particles emitted per cycle - PSparticle* particles; - PSpointsource* flames; + const uint16_t numFlames = (cols << 1); // number of flames: depends on fire width. for a fire width of 16 pixels, about 25-30 flames give good results, add a few for the base flames + const uint16_t numParticles = numFlames * 25; + uint8_t percycle = numFlames >> 1; // maximum number of particles emitted per cycle + PSparticle *particles; + PSpointsource *flames; - //allocate memory and divide it into proper pointers + // allocate memory and divide it into proper pointers uint32_t dataSize = sizeof(PSparticle) * numParticles; - dataSize += sizeof(PSpointsource) * (numFlames); - - //DEBUG_PRINTLN(F("**********************")); - //DEBUG_PRINT(F("particle datasize = ")); - //DEBUG_PRINTLN(dataSize); + dataSize += sizeof(PSpointsource) * (numFlames); - if (!SEGENV.allocateData(dataSize)){ - return mode_static(); //allocation failed; //allocation failed - } - - 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 + // DEBUG_PRINTLN(F("**********************")); + // DEBUG_PRINT(F("particle datasize = ")); + // DEBUG_PRINTLN(dataSize); - uint16_t i; -/* -#ifdef FIRELAMP - uint16_t numParticles = 300; //number of particles to use - uint8_t perCycle = 15; -#else -uint16_t numParticles = 300; //number of particles to use -uint8_t perCycle = 12; //NUMBEROFFLAMES; //max number of emitted particles per cycle, use to fine tune the appearance, more means brighter flames but they can oscillate -#endif -*/ + if (!SEGENV.allocateData(dataSize)) + { + return mode_static(); // allocation failed; //allocation failed + } - static PSsimpleparticle* simpleparticles; - static PSpointsource* flames; + 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 - if (SEGMENT.call == 0) //initialization + uint16_t i; + + if (SEGMENT.call == 0) // initialization { DEBUG_PRINTLN(F("Initializing Particle Fire")); - //allocate memory and divide it into proper pointers - uint16_t dataSize = sizeof(PSsimpleparticle) * numParticles; - dataSize += sizeof(PSpointsource) * (numFlames+1); //+1 to avoid crashes due to memory alignment, makes sure there is a little more memory allocated than needed (TODO: need to find out why it really crashes) - - DEBUG_PRINTLN(F("**********************")); - DEBUG_PRINT(F("particle datasize = ")); - DEBUG_PRINTLN(dataSize); - - if (!SEGENV.allocateData(dataSize)){ - return mode_static(); //allocation failed; //allocation failed - } - - flames = reinterpret_cast(SEGENV.data); - //calculate the end of the spray data and assign it as the data pointer for the particles: - simpleparticles = reinterpret_cast(flames+numFlames); //cast the data array into a particle pointer - - //make sure all particles start out dead - for (i = 0; i < numParticles; i++) { - simpleparticles[i].ttl = 0; - } - - //initialize the flame sprays -#ifdef FIRELAMP - for (i = 0; i < numFlames; i++) { - flames[i].source.ttl = 0; - flames[i].source.x = PS_MAX_Y / 2 + (rand() % (PS_P_RADIUS * 6)) - PS_P_RADIUS * 3; //position araound the center - } -#else - for (i = 0; i < numFlames; i++) { - flames[i].source.ttl = 0; - flames[i].source.x = PS_MAX_X / 2 + (rand() % (PS_P_RADIUS * 8)) - PS_P_RADIUS * 4; //position araound the center TODO: make this dynamic depending on matrix size - //other parameters are set when creating the flame (see blow) - } -#endif - } - - //update the flame sprays: - for (i = 0; i < numFlames; i++) { - if (flames[i].source.ttl > 0) { - flames[i].source.ttl--; - flames[i].source.x += flames[i].source.vx; //move the source (if it has x-speed) - } - else //flame source is dead - { - -#ifdef FIRELAMP -//make some of the flames small and slow to add a bright base - if(i>2)/(1+(SEGMENT.speed>>6)) + 10; //'hotness' of fire, faster flames reduce the effect or flame height will scale too much with speed - flames[i].maxLife = random8(7) + 13; //defines flame height together with the vy speed, vy speed*maxlife/PS_P_RADIUS is the average flame height - flames[i].minLife = 2; - flames[i].vx = (int8_t)random8(4) - 2; //emitting speed (sideways) - flames[i].vy = 5+(SEGMENT.speed>>2); //emitting speed (upwards) - flames[i].var = random8(5) + 3; //speed variation around vx,vy (+/- var/2) -#endif - } - } + SEGMENT.aux0 = rand(); // aux0 is wind position (index) in the perlin noise + // make sure all particles start out dead + for (i = 0; i < numParticles; i++) + { + particles[i].ttl = 0; + } + + // initialize the flame sprays + for (i = 0; i < numFlames; i++) + { + flames[i].source.ttl = 0; + flames[i].source.x = PS_P_RADIUS * 3 + random16(PS_MAX_X - (PS_P_RADIUS * 6)); // distribute randomly but not close to the corners + // other parameters are set when creating the flame (see blow) + } + } - static uint16_t windposition = 0; //position in the perlin noise matrix for wind generation - //if (rand() % 5 == 0) //change wind speed sometimes + // update the flame sprays: + for (i = 0; i < numFlames; i++) + { + if (flames[i].source.ttl > 0) { - windposition += 4; + flames[i].source.ttl--; + flames[i].source.x += flames[i].source.vx; // move the flame source (if it has x-speed) + } + else // flame source is dead + { + // initialize new flame: set properties of source + // from time to time, chang the flame position + // make some of the flames small and slow to add a bright base + if (i < (numFlames - (cols >> 1))) + { // all but the last few are normal flames + if (random8(40) == 0) + { + if (SEGMENT.check1) + { // wrap around in X direction, distribute randomly + flames[i].source.x = random16(PS_MAX_X); + } + else + { // no wrapping + flames[i].source.x = PS_P_RADIUS * 3 + random16(PS_MAX_X - (PS_P_RADIUS * 6)); // distribute randomly but not close to the corners + } + } + flames[i].source.y = -1 * PS_P_RADIUS; // set the source below the frame so particles alredy spread a little when the appear + flames[i].source.vx = 0; // (rand() % 3) - 1; + flames[i].source.vy = 0; + // flames[i].source.hue = random8(15) + 18; //flame color, orange to yellow + flames[i].source.ttl = random8(SEGMENT.intensity >> 2) / (1 + (SEGMENT.speed >> 6)) + 10; //'hotness' of fire, faster flames reduce the effect or flame height will scale too much with speed + flames[i].maxLife = random8(7) + 13; // defines flame height together with the vy speed, vy speed*maxlife/PS_P_RADIUS is the average flame height + flames[i].minLife = 2; + flames[i].vx = (int8_t)random8(4) - 2; // emitting speed (sideways) + flames[i].vy = 5 + (SEGMENT.speed >> 2); // emitting speed (upwards) + flames[i].var = random8(5) + 3; // speed variation around vx,vy (+/- var/2) + } + else + { // base flames to make the base brighter, flames are slower and short lived + flames[i].source.y = -1 * PS_P_RADIUS; // set the source below the frame + flames[i].source.vx = 0; + flames[i].source.vy = 0; // emitter moving speed; + // flames[i].source.hue = 0;//(rand() % 15) + 18; //flame color (not used) + flames[i].source.ttl = random8(25) + 15; // lifetime of one flame + flames[i].maxLife = 25; // defines flame height together with the vy speed, vy speed*maxlife/PS_P_RADIUS is the average flame height + flames[i].minLife = 12; + flames[i].vx = 0; // emitting speed, sideways + flames[i].vy = 1 + (SEGMENT.custom1 >> 4); // slow emitting speed (upwards) + flames[i].var = 2; // speed variation around vx,vy (+/- var/2) + } } + } - //update particles, create particles - uint8_t j = random8(numFlames); //start with a random flame (so each flame gets the chance to emit a particle if perCycle is smaller than number of flames) - for (i = 0; i < numParticles; i++) { - if (simpleparticles[i].ttl == 0 && percycle > 0) { - Emitter_Flame_emit(&flames[j], &simpleparticles[i]); - j++; - percycle--; - if (j >= numFlames) { - j = 0; - } - } - else if (simpleparticles[i].ttl) { //if particle is alive, update it - //add wind, using perlin noise - int8_t windspeed = (int8_t) (inoise8(windposition, simpleparticles[i].y >> 2) - 127) / ((271-SEGMENT.custom1)>>4); - simpleparticles[i].vx = windspeed; - SimpleParticle_update(&simpleparticles[i],false,false); //update particle, no wrapping - } - } + SEGMENT.aux0++; // position in the perlin noise matrix for wind generation - SEGMENT.fill(BLACK); //clear the matrix + // update particles, create particles + uint8_t j = random16(numFlames); // start with a random flame (so each flame gets the chance to emit a particle if perCycle is smaller than number of flames) + for (i = 0; i < numParticles; i++) + { + if (particles[i].ttl == 0 && percycle > 0) + { + Emitter_Flame_emit(&flames[j], &particles[i]); + j++; + if (j >= numFlames) + { // or simpler: j=j%numFlames; + j = 0; + } + percycle--; + } + else if (particles[i].ttl) + { // if particle is alive, update it + // add wind, using perlin noise + int8_t windspeed = (int8_t)(inoise8(SEGMENT.aux0, particles[i].y >> 2) - 127) / ((271 - SEGMENT.custom2) >> 4); + particles[i].vx = windspeed; + FireParticle_update(&particles[i], SEGMENT.check1, false); // update particle, use X-wrapping if check 1 is set by user + } + } - //render the particles - ParticleSys_renderParticleFire(simpleparticles, numParticles); //draw matrix + SEGMENT.fill(BLACK); // clear the matrix - return FRAMETIME; + // render the particles + ParticleSys_renderParticleFire(particles, numParticles, SEGMENT.check1); // draw matrix + return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEFIRE[] PROGMEM = "Particle Fire@Speed,Intensity,Wind Speed, Color Mode, check1, check2, check3;!,!;012;sx=100,ix=120,c1=128,c2=0,c3=8,o1=1,o2=0,o3=1"; -//static const char _data_FX_MODE_PARTICLEFIRE[] PROGMEM = "Particle Fire@Speed,Intensity,Wind Speed, asdf,Color Mode,check1, check2, check3;!,!;2;sx=88,ix=70,c1=190,c2=128,c3=8,o1=1,o2=0,o3=1"; - +static const char _data_FX_MODE_PARTICLEFIRE[] PROGMEM = "Particle Fire@Speed,Intensity,Base Flames,Wind Speed, Color Mode, WrapX;;!;012;sx=100,ix=120,c1=16,c2=128,c3=0,o1=0"; +/*syntax for json configuration string: +@A,B,C,D,E,F,G,H;I,J,K;L;M;N mark commas and semicolons +A - speed +B - intensity +C, D, E, - custom1 to custom3 +F,G,H - check1 to check3 +I,J,K - slot1 to slot3 +L - palette +M - mode (012) +N - parameter defaults (sliders: sx=100 ist speed, ix=24 is intensity, c1 ... c3 =20 is custom 1...3) +a '!' uses default values for that section +*/ -<<<<<<< Updated upstream -== == == = - /* - particles falling down, user can enable these three options: X-wraparound, side bounce, ground bounce - sliders control falling speed, intensity (number of particles spawned), WIND OR SPEED RANDOMNESS?, inter-particle collision hardness (0 means no particle collisions) and render saturation - this is quite versatile, can be made to look like rain or snow or confetti, flying sparks etc. - Uses palette for particle color - by DedeHai (Damian Schneider) - */ +/* +particles falling down, user can enable these three options: X-wraparound, side bounce, ground bounce +sliders control falling speed, intensity (number of particles spawned), WIND OR SPEED RANDOMNESS?, inter-particle collision hardness (0 means no particle collisions) and render saturation +this is quite versatile, can be made to look like rain or snow or confetti, flying sparks etc. +Uses palette for particle color +by DedeHai (Damian Schneider) +*/ - uint16_t mode_particlefall(void) +uint16_t mode_particlefall(void) { if (SEGLEN == 1) @@ -8260,85 +8474,89 @@ static const char _data_FX_MODE_PARTICLEFIRE[] PROGMEM = "Particle Fire@Speed,In { for (i = 0; i < numParticles; i++) { - particles[i].ttl=0; + particles[i].ttl = 0; } } - if(SEGMENT.call % (64-(SEGMENT.intensity>>2)) == 0 && SEGMENT.intensity>1) //every nth frame emit particles, stop emitting if zero + if (SEGMENT.call % (64 - (SEGMENT.intensity >> 2)) == 0 && SEGMENT.intensity > 1) // every nth frame emit particles, stop emitting if zero { - while(i>1)*PS_P_RADIUS+(cols>>1)*PS_P_RADIUS); //todo: could make this dynamic and random but needs a user variable - - particles[i].y=random16(rows*PS_P_RADIUS)+rows*PS_P_RADIUS; //particles appear somewhere above the matrix, maximum is double the height - particles[i].vx=(((int16_t)random8(SEGMENT.custom1))-(SEGMENT.custom1>>1))>>1; //side speed is +/- a quarter of the custom1 slider - particles[i].vy=-(SEGMENT.speed>>1); - particles[i].hue=random8(); //set random color - break; //quit loop if all particles of this round emitted - } + if (particles[i].ttl == 0) // find a dead particle + { + // emit particle at random position just over the top of the matrix + particles[i].ttl = 3000 - (SEGMENT.speed << 3) + random16(500); // if speed is higher, make them die sooner + + if (random8(5) == 0) // 16% of particles apper anywhere + particles[i].x = random16(cols * PS_P_RADIUS - 1); + else // rest is emitted at center half + particles[i].x = random16((cols >> 1) * PS_P_RADIUS + (cols >> 1) * PS_P_RADIUS); // todo: could make this dynamic and random but needs a user variable + + particles[i].y = random16(rows * PS_P_RADIUS) + rows * PS_P_RADIUS; // particles appear somewhere above the matrix, maximum is double the height + particles[i].vx = (((int16_t)random8(SEGMENT.custom1)) - (SEGMENT.custom1 >> 1)) >> 1; // side speed is +/- a quarter of the custom1 slider + particles[i].vy = -(SEGMENT.speed >> 1); + particles[i].hue = random8(); // set random color + break; // quit loop if all particles of this round emitted + } i++; } } - - uint8_t hardness = SEGMENT.custom2; //how hard the particle collisions are, if set to zero, no particle collision is calculated - if(hardness > 0) { - //detect and handle collisions + uint8_t hardness = SEGMENT.custom2; // how hard the particle collisions are, if set to zero, no particle collision is calculated + + if (hardness > 0) + { + // detect and handle collisions int16_t startparticle = 0; - int16_t endparticle = numParticles/2; //do half the particles - - 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 which is overkill) + int16_t endparticle = numParticles / 2; // do half the particles + + 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 which is overkill) startparticle = endparticle; endparticle = numParticles; } - for(i=startparticle; i0) //if particle is alive + // go though all 'higher number' particles and see if any of those are in close proximity + // if they are, make them collide + if (particles[i].ttl > 0) // if particle is alive { - int32_t dx, dy; //distance to other particles - for (j = i + 1; j < numParticles; j++) { //check against higher number particles - if(particles[j].ttl>0){ //if target particle is alive - dx = particles[i].x - particles[j].x; - 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); - } + int32_t dx, dy; // distance to other particles + for (j = i + 1; j < numParticles; j++) + { // check against higher number particles + if (particles[j].ttl > 0) + { // if target particle is alive + dx = particles[i].x - particles[j].x; + 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); } + } } - } + } } } - - //now move the particles - for(i=0; i(SEGENV.data); //cast the data array into a particle pointer + if (!SEGENV.allocateData(dataSize)) + return mode_static(); // allocation failed; //allocation failed + particles = reinterpret_cast(SEGENV.data); // cast the data array into a particle pointer + uint16_t i = 0; + uint16_t j = 0; - uint16_t i=0; - uint16_t j=0; - - if (SEGMENT.call == 0) //initialization + if (SEGMENT.call == 0) // initialization { - SEGMENT.aux0 = rand(); //position (either in noise or in sine function) - for(i=0; i>2)*PS_P_RADIUS); //in the bottom quarder + particles[i].ttl = 500; // all particles are alive (but not all are calculated/rendered) + particles[i].hue = i * 3; // full color range (goes over palette colors three times so it is also colorful when using fewer particles) + particles[i].x = map(i, 0, 255, 1, cols * PS_P_RADIUS); // distribute along x according to color + particles[i].y = random16((rows >> 2) * PS_P_RADIUS); // in the bottom quarder } } - uint16_t displayparticles = SEGMENT.intensity; i = 0; j = 0; - - - if(SEGMENT.call%(((255-SEGMENT.speed)>>6)+1)==0 && SEGMENT.speed > 0) //how often the force is applied depends on speed setting + + if (SEGMENT.call % (((255 - SEGMENT.speed) >> 6) + 1) == 0 && SEGMENT.speed > 0) // how often the force is applied depends on speed setting { - + int8_t xgravity; int8_t ygravity; uint8_t scale; - if(SEGMENT.check1){ //if random is set, use perlin noise for force vector generation - SEGMENT.aux0+=(SEGMENT.speed>>6)+1; //update position in noise - uint8_t angle = (((uint16_t)inoise8(SEGMENT.aux0))<<1)-64; //noise avg. value is 127 scale to 255 (to increase the noise range) and subtract 64 (=90°) to make the average direction downwards (270°), overflow means modulo, so is ok - //calculate x and y vectors from angle: - xgravity = ((int16_t)cos8(angle))-128; //gravity direction +/- 127 - ygravity = ((int16_t)sin8(angle))-128; - //scale the vectors using another inoise value: - scale = inoise8(SEGMENT.aux0+4096); //use a offset in the noise for scale value - //scale the force, if scale is small, make it zero (scale is applied below) - if(scale > (SEGMENT.custom1>>1)+64) scale = (SEGMENT.custom1>>1)+64; //max is 192 - if(scale < 64) scale = 0; - } - else{ //use sinusoidal motion - //angle needs to move from -270° to +90° (from top leftside to top rightside but limited by one of the sliders by the user (custom1=Amount), -270 (and +90) is ~64 in 8bit angle representation (365=255) - //the anglar force changes in a sinusoidal motion, like a rocking boat - //the angle is first calculated using a sine, then shifted so it goes from -127 to +127, then scaled, then shifted to 0 is actually -64 (=-90°=down) - - SEGMENT.aux0++; //move forward in the sinusoidal function - int16_t angle = (int16_t)sin8(SEGMENT.aux0)-128; //shift result (0-255 representing -1 to +1) so it goes from -128 to +127 - scale = 130-(abs(angle)); //force gets weaker at exteme positions - if(scale > 50) scale = 80; //force is strong everywhere but the top - angle = (angle*(int16_t)SEGMENT.custom1)>>8; //scale angle range according to slider - angle -= 63; //make 'down' (or -90°) the zero position - //now calculate the force vectors - xgravity = ((int16_t)cos8((uint8_t)angle))-128; //gravity direction +/- 127 - ygravity = ((int16_t)sin8((uint8_t)angle))-128; - } - - //scale gravity force - xgravity = ((int16_t)xgravity*scale)>>8; - ygravity = ((int16_t)ygravity*scale)>>8; - - //scale the gravity force down even more - xgravity = xgravity >>4; - ygravity = ygravity >>4; - - for(i=0; i0){ - particles[i].vx+=xgravity; - particles[i].vy+=ygravity; - particles[i].ttl=500; //particles never die - } + if (SEGMENT.check1) + { // if random is set, use perlin noise for force vector generation + SEGMENT.aux0 += (SEGMENT.speed >> 6) + 1; // update position in noise + uint8_t angle = (((uint16_t)inoise8(SEGMENT.aux0)) << 1) - 64; // noise avg. value is 127 scale to 255 (to increase the noise range) and subtract 64 (=90°) to make the average direction downwards (270°), overflow means modulo, so is ok + // calculate x and y vectors from angle: + xgravity = ((int16_t)cos8(angle)) - 128; // gravity direction +/- 127 + ygravity = ((int16_t)sin8(angle)) - 128; + // scale the vectors using another inoise value: + scale = inoise8(SEGMENT.aux0 + 4096); // use a offset in the noise for scale value + // scale the force, if scale is small, make it zero (scale is applied below) + if (scale > (SEGMENT.custom1 >> 1) + 64) + scale = (SEGMENT.custom1 >> 1) + 64; // max is 192 + if (scale < 64) + scale = 0; + } + else + { // use sinusoidal motion + // angle needs to move from -270° to +90° (from top leftside to top rightside but limited by one of the sliders by the user (custom1=Amount), -270 (and +90) is ~64 in 8bit angle representation (365=255) + // the anglar force changes in a sinusoidal motion, like a rocking boat + // the angle is first calculated using a sine, then shifted so it goes from -127 to +127, then scaled, then shifted to 0 is actually -64 (=-90°=down) + + SEGMENT.aux0++; // move forward in the sinusoidal function + int16_t angle = (int16_t)sin8(SEGMENT.aux0) - 128; // shift result (0-255 representing -1 to +1) so it goes from -128 to +127 + scale = 130 - (abs(angle)); // force gets weaker at exteme positions + if (scale > 50) + scale = 80; // force is strong everywhere but the top + angle = (angle * (int16_t)SEGMENT.custom1) >> 8; // scale angle range according to slider + angle -= 63; // make 'down' (or -90°) the zero position + // now calculate the force vectors + xgravity = ((int16_t)cos8((uint8_t)angle)) - 128; // gravity direction +/- 127 + ygravity = ((int16_t)sin8((uint8_t)angle)) - 128; + } + + // scale gravity force + xgravity = ((int16_t)xgravity * scale) >> 8; + ygravity = ((int16_t)ygravity * scale) >> 8; + + // scale the gravity force down even more + xgravity = xgravity >> 4; + ygravity = ygravity >> 4; + + for (i = 0; i < numParticles; i++) + { + if (particles[i].ttl > 0) + { + particles[i].vx += xgravity; + particles[i].vy += ygravity; + particles[i].ttl = 500; // particles never die } + } } - //detect and handle collisions + // detect and handle collisions int16_t startparticle = 0; - int16_t endparticle = displayparticles>>1; //do half the particles - - 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 which is overkill) + int16_t endparticle = displayparticles >> 1; // do half the particles + + 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 which is overkill) startparticle = endparticle; endparticle = displayparticles; } - uint8_t hardness = SEGMENT.custom2; //how hard the collisions are, 255 = full hard. - //hardness = 255; - for(i=startparticle; i0) //if particle is alive + // go though all 'higher number' particles and see if any of those are in close proximity + // if they are, make them collide + if (particles[i].ttl > 0) // if particle is alive { - int32_t dx, dy; //distance to other particles - for (j = i + 1; j < displayparticles; j++) { //check against higher number particles - if(particles[j].ttl>0){ //if target particle is alive - dx = particles[i].x - particles[j].x; - 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); - } + int32_t dx, dy; // distance to other particles + for (j = i + 1; j < displayparticles; j++) + { // check against higher number particles + if (particles[j].ttl > 0) + { // if target particle is alive + dx = particles[i].x - particles[j].x; + 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); } + } } - } + } } - - //now move the particles - for(i=0; i Date: Sun, 4 Feb 2024 10:39:01 +0100 Subject: [PATCH 008/219] Update platformio.ini --- platformio.ini | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/platformio.ini b/platformio.ini index 4fc858e2a9..13c4feee42 100644 --- a/platformio.ini +++ b/platformio.ini @@ -10,7 +10,8 @@ # ------------------------------------------------------------------------------ # CI/release binaries -default_envs = nodemcuv2, esp8266_2m, esp01_1m_full, esp32dev, esp32_eth, esp32dev_audioreactive, lolin_s2_mini, esp32c3dev, esp32s3dev_8MB, esp32s3dev_8MB_PSRAM_opi, esp32_wrover +;default_envs = nodemcuv2, esp8266_2m, esp01_1m_full, esp32dev, esp32_eth, esp32dev_audioreactive, lolin_s2_mini, esp32c3dev, esp32s3dev_8MB, esp32s3dev_8MB_PSRAM_opi, esp32_wrover +default_envs = esp32c3dev src_dir = ./wled00 data_dir = ./wled00/data From a7ef020cc96898d5e858895e39662e63f9c8b948 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sun, 4 Feb 2024 12:40:37 +0100 Subject: [PATCH 009/219] updated particle box and firework effects particle box now is more random in random mode (still a work in progress) firework is now more configurable by sliders --- wled00/FX.cpp | 65 ++++++++++++++++++++++++++++++--------------------- 1 file changed, 38 insertions(+), 27 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 82e711f24f..f452eeb033 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -8006,15 +8006,21 @@ uint16_t mode_particlefireworks(void) const uint16_t PS_MAX_X(cols * PS_P_RADIUS - 1); const uint16_t PS_MAX_Y(rows * PS_P_RADIUS - 1); - const uint16_t numParticles = 450; - const uint8_t numRockets = 3; +#ifdef ESP8266 + const uint16_t numParticles = 250; + const uint8_t MaxNumRockets = 4; +#else + const uint16_t numParticles = 650; + const uint8_t MaxNumRockets = 8; +#endif + PSparticle *particles; PSpointsource *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(PSpointsource) * (MaxNumRockets); if (!SEGENV.allocateData(dataSize)) return mode_static(); // allocation failed; //allocation failed @@ -8023,10 +8029,11 @@ uint16_t mode_particlefireworks(void) 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 + particles = reinterpret_cast(rockets + MaxNumRockets); // cast the data array into a particle pointer uint16_t i = 0; uint16_t j = 0; + uint8_t numRockets = (SEGMENT.custom3+1) >> 2; //1 to 8 if (SEGMENT.call == 0) // initialization { @@ -8059,7 +8066,7 @@ uint16_t mode_particlefireworks(void) } else { // speed is zero, explode! - emitparticles = random8(80) + 10; // defines the size of the explosion + emitparticles = random8(SEGMENT.intensity>>1) + 10; // defines the size of the explosion rockets[j].source.vy = -1; // set speed negative so it will emit no more particles after this explosion until relaunch } @@ -8083,7 +8090,7 @@ uint16_t mode_particlefireworks(void) { if (particles[i].ttl) { - Particle_Gravity_update(&particles[i], SEGMENT.check1, SEGMENT.check2, SEGMENT.check3, (uint8_t)255); + Particle_Gravity_update(&particles[i], SEGMENT.check1, SEGMENT.check2, SEGMENT.check3, SEGMENT.custom2); } } @@ -8102,9 +8109,9 @@ uint16_t mode_particlefireworks(void) { // rocket has died and is moving up. stop it so it will explode (is handled in the code above) rockets[i].source.vy = 0; // set speed to zero so code above will recognize this as an exploding rocket rockets[i].source.hue = random8(); // random color - rockets[i].maxLife = 250; - rockets[i].minLife = 100; - rockets[i].source.ttl = random8(120) + 50; // standby time til next launch (in frames, so about 2-5 seconds at 30fps) + rockets[i].maxLife = 200; + rockets[i].minLife = 50; + rockets[i].source.ttl = random8((255 - SEGMENT.speed))+10; // standby time til next launch (in frames at 42fps, max of 265 is about 6 seconds rockets[i].vx = 0; // emitting speed rockets[i].vy = 0; // emitting speed rockets[i].var = 30; // speed variation around vx,vy (+/- var/2) @@ -8114,11 +8121,11 @@ uint16_t mode_particlefireworks(void) // reinitialize rocket rockets[i].source.y = 1; // start from bottom rockets[i].source.x = (rand() % (PS_MAX_X >> 1)) + (PS_MAX_Y >> 2); // centered half - rockets[i].source.vy = random8(10) + 5; + rockets[i].source.vy = random8(SEGMENT.custom1>>3) + 5; //rocket speed depends also on rocket height rockets[i].source.vx = random8(5) - 2; rockets[i].source.hue = 30; // rocket exhaust = orange (if using rainbow palette) - rockets[i].source.ttl = random8(80) + 40; - rockets[i].maxLife = 40; + rockets[i].source.ttl = random8(SEGMENT.custom1) + (SEGMENT.custom1>>1); // sets explosion height (rockets explode at the top if set too high as paticle update set speed to zero if moving out of matrix) + rockets[i].maxLife = 40; //exhaust particle life rockets[i].minLife = 10; rockets[i].vx = 0; // emitting speed rockets[i].vy = 0; // emitting speed @@ -8132,7 +8139,7 @@ uint16_t mode_particlefireworks(void) return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEFIREWORKS[] PROGMEM = "Particle Fireworks@Moving Speed,Intensity,Particle Speed,Spray Angle,Nozzle Size,Wrap X,Bounce X,Bounce Y;;!;012;pal=11,sx=100,ix=200,c1=190,c2=128,c3=8,o1=0,o2=0,o3=0"; +static const char _data_FX_MODE_PARTICLEFIREWORKS[] PROGMEM = "Particle Fireworks@Launches,Explosion Size,Height,Bounce Strength,Rockts,Wrap X,Bounce X,Bounce Y;;!;012;pal=11,sx=100,ix=50,c1=64,c2=128,c3=10,o1=0,o2=0,o3=1"; /* * Particle gravity spray @@ -8771,17 +8778,17 @@ uint16_t mode_particlebox(void) if (SEGMENT.check1) { // if random is set, use perlin noise for force vector generation SEGMENT.aux0 += (SEGMENT.speed >> 6) + 1; // update position in noise - uint8_t angle = (((uint16_t)inoise8(SEGMENT.aux0)) << 1) - 64; // noise avg. value is 127 scale to 255 (to increase the noise range) and subtract 64 (=90°) to make the average direction downwards (270°), overflow means modulo, so is ok + //uint8_t angle = (((uint16_t)inoise8(SEGMENT.aux0)) << 1) - 64; // noise avg. value is 127 scale to 256 (to increase the range) and subtract 64 (=90°) to make the average direction downwards (270°), overflow means modulo, so is ok // calculate x and y vectors from angle: - xgravity = ((int16_t)cos8(angle)) - 128; // gravity direction +/- 127 - ygravity = ((int16_t)sin8(angle)) - 128; + //xgravity = ((int16_t)cos8(angle)) - 128; // gravity direction +/- 127 + //ygravity = ((int16_t)sin8(angle)) - 128; // scale the vectors using another inoise value: - scale = inoise8(SEGMENT.aux0 + 4096); // use a offset in the noise for scale value - // scale the force, if scale is small, make it zero (scale is applied below) - if (scale > (SEGMENT.custom1 >> 1) + 64) - scale = (SEGMENT.custom1 >> 1) + 64; // max is 192 - if (scale < 64) - scale = 0; + //scale = inoise8(SEGMENT.aux0 + 4096); // use a offset in the noise for scale value + + //scale = ((uint16_t)scale * SEGMENT.custom1)>>8; //apply rescaling with user input + //not using angles but x and y tilt from inoise: + xgravity = ((int16_t)inoise8(SEGMENT.aux0)-127); + ygravity = ((int16_t)inoise8(SEGMENT.aux0+10000) - 127); } else { // use sinusoidal motion @@ -8799,15 +8806,19 @@ uint16_t mode_particlebox(void) // now calculate the force vectors xgravity = ((int16_t)cos8((uint8_t)angle)) - 128; // gravity direction +/- 127 ygravity = ((int16_t)sin8((uint8_t)angle)) - 128; - } - // scale gravity force - xgravity = ((int16_t)xgravity * scale) >> 8; - ygravity = ((int16_t)ygravity * scale) >> 8; + // scale gravity force + xgravity = ((int16_t)xgravity * scale) >> 8; + ygravity = ((int16_t)ygravity * scale) >> 8; + //todo: check with console output to see if scaling is applied correctly. + } - // scale the gravity force down even more + + // scale the gravity force down xgravity = xgravity >> 4; ygravity = ygravity >> 4; + //Serial.print(xgravity); + //Serial.println(" "); for (i = 0; i < numParticles; i++) { From 820d8dd3dca5f1acb6111bb38c8092640256bff2 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Tue, 6 Feb 2024 12:44:48 +0100 Subject: [PATCH 010/219] added preliminary functions and FX --- wled00/FX.cpp | 168 ++++++++++++++++++++++++++++++++++-- wled00/FXparticleSystem.cpp | 110 ++++++++++++++++++----- wled00/FXparticleSystem.h | 5 +- 3 files changed, 253 insertions(+), 30 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index f452eeb033..5fd0666235 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -8103,7 +8103,7 @@ uint16_t mode_particlefireworks(void) { if (rockets[i].source.ttl) { - Particle_Move_update(&rockets[i].source); // move the rocket, age the rocket (ttl--) + Particle_Move_update(&rockets[i].source); // move the rocket, age the rocket (ttl--) } else if (rockets[i].source.vy > 0) { // rocket has died and is moving up. stop it so it will explode (is handled in the code above) @@ -8114,7 +8114,7 @@ uint16_t mode_particlefireworks(void) rockets[i].source.ttl = random8((255 - SEGMENT.speed))+10; // standby time til next launch (in frames at 42fps, max of 265 is about 6 seconds rockets[i].vx = 0; // emitting speed rockets[i].vy = 0; // emitting speed - rockets[i].var = 30; // speed variation around vx,vy (+/- var/2) + rockets[i].var = (SEGMENT.intensity >> 2) + 10; // speed variation around vx,vy (+/- var/2) } else if (rockets[i].source.vy < 0) // rocket is exploded and time is up (ttl=0 and negative speed), relaunch it { @@ -8125,7 +8125,7 @@ uint16_t mode_particlefireworks(void) rockets[i].source.vx = random8(5) - 2; rockets[i].source.hue = 30; // rocket exhaust = orange (if using rainbow palette) rockets[i].source.ttl = random8(SEGMENT.custom1) + (SEGMENT.custom1>>1); // sets explosion height (rockets explode at the top if set too high as paticle update set speed to zero if moving out of matrix) - rockets[i].maxLife = 40; //exhaust particle life + rockets[i].maxLife = 30; //exhaust particle life rockets[i].minLife = 10; rockets[i].vx = 0; // emitting speed rockets[i].vy = 0; // emitting speed @@ -8139,7 +8139,7 @@ uint16_t mode_particlefireworks(void) return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEFIREWORKS[] PROGMEM = "Particle Fireworks@Launches,Explosion Size,Height,Bounce Strength,Rockts,Wrap X,Bounce X,Bounce Y;;!;012;pal=11,sx=100,ix=50,c1=64,c2=128,c3=10,o1=0,o2=0,o3=1"; +static const char _data_FX_MODE_PARTICLEFIREWORKS[] PROGMEM = "Particle Fireworks@Launches,Explosion Size,Height,Bounce,Rockets,Wrap X,Bounce X,Bounce Y;;!;012;pal=11,sx=100,ix=50,c1=64,c2=128,c3=10,o1=0,o2=0,o3=1"; /* * Particle gravity spray @@ -8563,7 +8563,7 @@ uint16_t mode_particlefall(void) return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEFALL[] PROGMEM = "Falling Particles@Speed,Intensity,Randomness,Collision hardness,Saturation,Wrap X,Side bounce,Ground bounce;;!;012;pal=11,sx=100,ix=200,c1=31,c2=0,c3=10,o1=0,o2=0,o3=1"; +static const char _data_FX_MODE_PARTICLEFALL[] PROGMEM = "Falling Particles@Speed,Intensity,Randomness,Collision hardness,Saturation,Wrap X,Side bounce,Ground bounce;;!;012;pal=11,sx=100,ix=200,c1=31,c2=0,c3=20,o1=0,o2=0,o3=1"; /* * Particle pile up test @@ -8573,7 +8573,7 @@ static const char _data_FX_MODE_PARTICLEFALL[] PROGMEM = "Falling Particles@Spee uint16_t mode_particlepile(void) { - +/* if (SEGLEN == 1) return mode_static(); @@ -8720,9 +8720,161 @@ uint16_t mode_particlepile(void) // render the particles ParticleSys_render(particles, numParticles, 255, SEGMENT.check1, false); + return FRAMETIME;*/ + +/* + * Particle smashing down like meteorites, maybe call it particle impact or something like that + * by DedeHai (Damian Schneider) + */ + + + if (SEGLEN == 1) + return mode_static(); + + const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; + const uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); + + // particle system box dimensions + const uint16_t PS_MAX_X(cols * PS_P_RADIUS - 1); + const uint16_t PS_MAX_Y(rows * PS_P_RADIUS - 1); + +#ifdef ESP8266 + const uint16_t numParticles = 250; + const uint8_t MaxNumRockets = 4; +#else + const uint16_t numParticles = 650; + const uint8_t MaxNumRockets = 8; +#endif + + PSparticle *particles; + PSpointsource *rockets; + + // allocate memory and divide it into proper pointers, max is 32k for all segments. + uint32_t dataSize = sizeof(PSparticle) * numParticles; + dataSize += sizeof(PSpointsource) * (MaxNumRockets); + if (!SEGENV.allocateData(dataSize)) + return mode_static(); // allocation failed; //allocation failed + + // DEBUG_PRINT(F("particle datasize = ")); + // DEBUG_PRINTLN(dataSize); + + 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 + MaxNumRockets); // cast the data array into a particle pointer + + uint16_t i = 0; + uint16_t j = 0; + uint8_t numRockets = map(SEGMENT.custom3, 0, 31, 1, MaxNumRockets); //number of rockets to use for animation + + if (SEGMENT.call == 0) // initialization + { + for (i = 0; i < numParticles; i++) + { + particles[i].ttl = 0; + } + for (i = 0; i < numRockets; i++) + { + rockets[i].source.ttl = random8(20 * i); // set initial delay for rockets + rockets[i].source.vy = 10; // at positive speeds, no particles are emitted and if particle dies, it will be relaunched + } + } + + // update particles, create particles + + // check each rocket's state and emit particles according to its state: moving up = emit exhaust, at top = explode; falling down = standby time + uint16_t emitparticles; // number of particles to emit for each rocket's state + i = 0; + for (j = 0; j < numRockets; j++) + { + // determine rocket state by its speed: + if (rockets[j].source.vy < 0) // moving down, emit sparks + { + emitparticles = 2; + } + else if (rockets[j].source.vy > 0) // moving up means standby + { + emitparticles = 0; + } + else // speed is zero, explode! + { + rockets[j].source.vy = 2; // set source speed positive so it goes into timeout and launches again + emitparticles = random8(SEGMENT.intensity >> 1) + 10; // defines the size of the explosion + } + + for (i; i < numParticles; i++) + { + if (particles[i].ttl == 0) + { // particle is dead + if (emitparticles > 0) + { + Emitter_Fountain_emit(&rockets[j], &particles[i]); + emitparticles--; + } + else + break; // done emitting for this rocket + } + } + } + + // update particles + for (i = 0; i < numParticles; i++) + { + if (particles[i].ttl) + { + Particle_Gravity_update(&particles[i], SEGMENT.check1, SEGMENT.check2, SEGMENT.check3, SEGMENT.custom2); + } + } + + // update the meteors, set the speed state + for (i = 0; i < numRockets; i++) + { + Serial.print(rockets[i].source.vy); + Serial.print(" "); + if (rockets[i].source.ttl) + { + Particle_Move_update(&rockets[i].source); // move the rocket, age the rocket (ttl--) + // if source reaches the bottom, set speed to 0 so it will explode on next function call (handled above) + if ((rockets[i].source.y < PS_P_RADIUS) && (rockets[i].source.vy < 0)) // reached the bottom pixel on its way down + { + rockets[i].source.vy = 0; // set speed zero so it will explode + rockets[i].source.vx = 0; + rockets[i].source.y = 5; // offset from ground so explosion happens not out of frame (if moving fast, this can happen) + rockets[i].source.hue = random8(); // random color of explosion + rockets[i].maxLife = 200; + rockets[i].minLife = 50; + rockets[i].source.ttl = random8((255 - SEGMENT.speed)) + 10; // standby time til next launch (in frames at 42fps, max of 265 is about 6 seconds + rockets[i].vx = 0; // emitting speed x + rockets[i].vy = 8; // emitting speed y + rockets[i].var = (SEGMENT.custom1 >> 1); // speed variation around vx,vy (+/- var/2) + } + } + else if (rockets[i].source.vy > 0) // rocket is exploded and time is up (ttl==0 and positive speed), relaunch it + { + // reinitialize rocket + rockets[i].source.y = PS_MAX_Y-2; // start from top + rockets[i].source.x = random16(PS_MAX_X); + rockets[i].source.vy = -30 -random(30)-10; //TODO: need to make this user selectable? + rockets[i].source.vx = random8(30) - 15; + rockets[i].source.hue = 220; // rocket exhaust = orange (if using fire palette) + rockets[i].source.ttl = 1000; //long live, will explode at bottom + rockets[i].maxLife = 60; // spark particle life + rockets[i].minLife = 20; + rockets[i].vx = 0; // emitting speed + rockets[i].vy = -10; // emitting speed + rockets[i].var = 5; // speed variation around vx,vy (+/- var/2) + } + } + SEGMENT.fill(BLACK); // clear the matrix + // render the particles + ParticleSys_render(particles, numParticles, 255, false, false); + return FRAMETIME; + } -static const char _data_FX_MODE_PARTICLEPILE[] PROGMEM = "Particle Pile Test@Moving Speed,Intensity,Particle Speed,Spray Angle,Bouncyness,Wrap X,Bounce X,Ground bounce;;!;012;pal=11,sx=100,ix=200,c1=190,c2=128,c3=28,o1=0,o2=0,o3=1"; + +static const char _data_FX_MODE_PARTICLEPILE[] PROGMEM = "Particle Pile Test@Launches,Explosion Size,Explosion Force,Bounce,Rockets,Wrap X,Bounce X,Bounce Y;;!;012;pal=11,sx=100,ix=50,c1=64,c2=128,c3=10,o1=0,o2=0,o3=1"; + +//static const char _data_FX_MODE_PARTICLEPILE[] PROGMEM = "Particle Pile Test@Moving Speed,Intensity,Particle Speed,Spray Angle,Bouncyness,Wrap X,Bounce X,Ground bounce;;!;012;pal=11,sx=100,ix=200,c1=190,c2=128,c3=28,o1=0,o2=0,o3=1"; /* Particle Box, applies gravity to particles in either a random direction or in a rocking motion @@ -8875,7 +9027,7 @@ uint16_t mode_particlebox(void) applyFriction(&particles[i], 1); } // Particle_Gravity_update(&particles[i], SEGMENT.check1, SEGMENT.check2, SEGMENT.check3, (uint8_t)200); - Particle_Bounce_update(&particles[i], (uint8_t)200); + Particle_Bounce_update(&particles[i], hardness); } SEGMENT.fill(BLACK); // clear the matrix diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index 2d17dc5251..86c4be4644 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -53,9 +53,81 @@ void Emitter_Fountain_emit(PSpointsource *emitter, PSparticle *part) part->hue = emitter->source.hue; } -// TODO: could solve all update functions in a single function with parameters and handle gravity acceleration in a separte function (uses more cpu time but that is not a huge issue) +//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) //todo: add a parameter 'swallow' so the attractor can 'suck up' particles that are very close +{ + // Calculate the distance between the particle and the attractor + int16_t dx = attractor->x - particle->x; + int16_t dy = attractor->y - particle->y; + + // Calculate the force based on inverse square law + int32_t distanceSquared = dx * dx + dy * dy; + //check if distance is small enough to even cause a force (for that strength<<10 must be bigger than the distance squared + int32_t shiftedstrength = (int32_t)strength << 16; + if (shiftedstrength > distanceSquared) //if too far away, no force is applied (force < 1) + { + int32_t force = shiftedstrength / distanceSquared; + int32_t xforce = (force * dx)>>12; //scale so that at force starts to increase at about 10 pixels away + int32_t yforce = (force * dy)>>12; + + uint8_t xcounter = *counter & 0xF; //lower four bits + uint8_t ycounter = *counter>>4; // upper four bits + *counter = 0; //reset counter, is set back to correct values below + + //man muss die jetzt noch schlau shiften, dazu ein excel machen + //for small forces, need to use a delay timer (counter) + if(xforce < 16) + { + xcounter += force; + if (xcounter > 16) + { + xcounter -= 16; + *counter |= xcounter; // write lower four bits + // apply force in x direction + if (dx < 0) + { + particle->vx -= 1; + } + else + { + particle->vx += 1; + } + } + + } + else{ + particle->vx += xforce/16; + } + + if(yforce < 16) + { + ycounter += yforce; + if(ycounter > 16) + { + ycounter -= 16; + *counter |= (ycounter<<4); //write upper four bits + + if (dy < 0) + { + particle->vy -= 1; + } + else + { + particle->vy += 1; + } + } -void Particle_Move_update(PSparticle *part) // particle moves, decays and dies (age or out of matrix) + } + else{ + particle->vy += yforce / 16; + } + + } +} + +// TODO: could solve all update functions in a single function with parameters and handle gravity acceleration in a separte function (uses more cpu time but that is not a huge issue) or maybe not, like this, different preferences can be set + +void Particle_Move_update(PSparticle *part) // particle moves, decays and dies { // Matrix dimension const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; @@ -67,26 +139,24 @@ void Particle_Move_update(PSparticle *part) // particle moves, decays and dies ( if (part->ttl > 0) { - // age - part->ttl--; + // age + part->ttl--; + + // apply velocity + part->x += (int16_t)part->vx; + part->y += (int16_t)part->vy; - // apply velocity - part->x += (int16_t)part->vx; - 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) - // check if particle is out of bounds - if ((part->y <= 0) || (part->y >= PS_MAX_Y)) - { - part->ttl = 0; - } - if ((part->x <= 0) || (part->x >= PS_MAX_X)) - { - part->ttl = 0; - } - if (part->vx == 0 && part->vy == 0) - { - part->ttl = 0; - } + // check if particle is out of bounds + if ((part->y <= 0) || (part->y >= PS_MAX_Y)) + { + part->outofbounds = 1; + } + if ((part->x <= 0) || (part->x >= PS_MAX_X)) + { + part->outofbounds = 1; + } } } diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index 542cd5204d..aec519cd9f 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -35,13 +35,14 @@ #define PS_P_SURFACE 12 //shift: 2^PS_P_SURFACE = (PS_P_RADIUS)^2 -//struct for a single particle with gravity (12 bytes) +//struct for a single particle typedef struct { int16_t x; //x position in particle system int16_t y; //y position in particle system uint16_t ttl; //time to live uint8_t outofbounds; //set to 1 if outside of matrix - uint8_t hue; //hue + uint8_t hue; //color hue + uint8_t sat; //color saturation int8_t vx; //horizontal velocity int8_t vy; //vertical velocity } PSparticle; From cc98036e3f096d1da0c59e5cce3d1ff96c990fe9 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Wed, 7 Feb 2024 19:48:54 +0100 Subject: [PATCH 011/219] added particle attractor, added two new FX but still buggy particle attractor animation does not work, somthing wrong with pointer allocation, it worked with static variables --- wled00/FX.cpp | 505 +++++++++++++++++++++++------------- wled00/FX.h | 6 +- wled00/FXparticleSystem.cpp | 131 ++++++---- wled00/FXparticleSystem.h | 3 +- 4 files changed, 413 insertions(+), 232 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 5fd0666235..74a8e6f102 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -8166,7 +8166,7 @@ uint16_t mode_particlespray(void) // allocate memory and divide it into proper pointers, max is 32k for all segments. uint32_t dataSize = sizeof(PSparticle) * numParticles; - dataSize += sizeof(PSpointsource) * (numSprays + 1); //+1 to avoid crashes due to memory alignment, makes sure there is a little more memory allocated than needed + dataSize += sizeof(PSpointsource) * (numSprays); if (!SEGENV.allocateData(dataSize)) return mode_static(); // allocation failed; //allocation failed @@ -8573,7 +8573,7 @@ static const char _data_FX_MODE_PARTICLEFALL[] PROGMEM = "Falling Particles@Spee uint16_t mode_particlepile(void) { -/* + if (SEGLEN == 1) return mode_static(); @@ -8720,161 +8720,9 @@ uint16_t mode_particlepile(void) // render the particles ParticleSys_render(particles, numParticles, 255, SEGMENT.check1, false); - return FRAMETIME;*/ - -/* - * Particle smashing down like meteorites, maybe call it particle impact or something like that - * by DedeHai (Damian Schneider) - */ - - - if (SEGLEN == 1) - return mode_static(); - - const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; - const uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); - - // particle system box dimensions - const uint16_t PS_MAX_X(cols * PS_P_RADIUS - 1); - const uint16_t PS_MAX_Y(rows * PS_P_RADIUS - 1); - -#ifdef ESP8266 - const uint16_t numParticles = 250; - const uint8_t MaxNumRockets = 4; -#else - const uint16_t numParticles = 650; - const uint8_t MaxNumRockets = 8; -#endif - - PSparticle *particles; - PSpointsource *rockets; - - // allocate memory and divide it into proper pointers, max is 32k for all segments. - uint32_t dataSize = sizeof(PSparticle) * numParticles; - dataSize += sizeof(PSpointsource) * (MaxNumRockets); - if (!SEGENV.allocateData(dataSize)) - return mode_static(); // allocation failed; //allocation failed - - // DEBUG_PRINT(F("particle datasize = ")); - // DEBUG_PRINTLN(dataSize); - - 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 + MaxNumRockets); // cast the data array into a particle pointer - - uint16_t i = 0; - uint16_t j = 0; - uint8_t numRockets = map(SEGMENT.custom3, 0, 31, 1, MaxNumRockets); //number of rockets to use for animation - - if (SEGMENT.call == 0) // initialization - { - for (i = 0; i < numParticles; i++) - { - particles[i].ttl = 0; - } - for (i = 0; i < numRockets; i++) - { - rockets[i].source.ttl = random8(20 * i); // set initial delay for rockets - rockets[i].source.vy = 10; // at positive speeds, no particles are emitted and if particle dies, it will be relaunched - } - } - - // update particles, create particles - - // check each rocket's state and emit particles according to its state: moving up = emit exhaust, at top = explode; falling down = standby time - uint16_t emitparticles; // number of particles to emit for each rocket's state - i = 0; - for (j = 0; j < numRockets; j++) - { - // determine rocket state by its speed: - if (rockets[j].source.vy < 0) // moving down, emit sparks - { - emitparticles = 2; - } - else if (rockets[j].source.vy > 0) // moving up means standby - { - emitparticles = 0; - } - else // speed is zero, explode! - { - rockets[j].source.vy = 2; // set source speed positive so it goes into timeout and launches again - emitparticles = random8(SEGMENT.intensity >> 1) + 10; // defines the size of the explosion - } - - for (i; i < numParticles; i++) - { - if (particles[i].ttl == 0) - { // particle is dead - if (emitparticles > 0) - { - Emitter_Fountain_emit(&rockets[j], &particles[i]); - emitparticles--; - } - else - break; // done emitting for this rocket - } - } - } - - // update particles - for (i = 0; i < numParticles; i++) - { - if (particles[i].ttl) - { - Particle_Gravity_update(&particles[i], SEGMENT.check1, SEGMENT.check2, SEGMENT.check3, SEGMENT.custom2); - } - } - - // update the meteors, set the speed state - for (i = 0; i < numRockets; i++) - { - Serial.print(rockets[i].source.vy); - Serial.print(" "); - if (rockets[i].source.ttl) - { - Particle_Move_update(&rockets[i].source); // move the rocket, age the rocket (ttl--) - // if source reaches the bottom, set speed to 0 so it will explode on next function call (handled above) - if ((rockets[i].source.y < PS_P_RADIUS) && (rockets[i].source.vy < 0)) // reached the bottom pixel on its way down - { - rockets[i].source.vy = 0; // set speed zero so it will explode - rockets[i].source.vx = 0; - rockets[i].source.y = 5; // offset from ground so explosion happens not out of frame (if moving fast, this can happen) - rockets[i].source.hue = random8(); // random color of explosion - rockets[i].maxLife = 200; - rockets[i].minLife = 50; - rockets[i].source.ttl = random8((255 - SEGMENT.speed)) + 10; // standby time til next launch (in frames at 42fps, max of 265 is about 6 seconds - rockets[i].vx = 0; // emitting speed x - rockets[i].vy = 8; // emitting speed y - rockets[i].var = (SEGMENT.custom1 >> 1); // speed variation around vx,vy (+/- var/2) - } - } - else if (rockets[i].source.vy > 0) // rocket is exploded and time is up (ttl==0 and positive speed), relaunch it - { - // reinitialize rocket - rockets[i].source.y = PS_MAX_Y-2; // start from top - rockets[i].source.x = random16(PS_MAX_X); - rockets[i].source.vy = -30 -random(30)-10; //TODO: need to make this user selectable? - rockets[i].source.vx = random8(30) - 15; - rockets[i].source.hue = 220; // rocket exhaust = orange (if using fire palette) - rockets[i].source.ttl = 1000; //long live, will explode at bottom - rockets[i].maxLife = 60; // spark particle life - rockets[i].minLife = 20; - rockets[i].vx = 0; // emitting speed - rockets[i].vy = -10; // emitting speed - rockets[i].var = 5; // speed variation around vx,vy (+/- var/2) - } - } - SEGMENT.fill(BLACK); // clear the matrix - // render the particles - ParticleSys_render(particles, numParticles, 255, false, false); - return FRAMETIME; - } - -static const char _data_FX_MODE_PARTICLEPILE[] PROGMEM = "Particle Pile Test@Launches,Explosion Size,Explosion Force,Bounce,Rockets,Wrap X,Bounce X,Bounce Y;;!;012;pal=11,sx=100,ix=50,c1=64,c2=128,c3=10,o1=0,o2=0,o3=1"; - -//static const char _data_FX_MODE_PARTICLEPILE[] PROGMEM = "Particle Pile Test@Moving Speed,Intensity,Particle Speed,Spray Angle,Bouncyness,Wrap X,Bounce X,Ground bounce;;!;012;pal=11,sx=100,ix=200,c1=190,c2=128,c3=28,o1=0,o2=0,o3=1"; +static const char _data_FX_MODE_PARTICLEPILE[] PROGMEM = "Particle Pile Test@Moving Speed,Intensity,Particle Speed,Spray Angle,Bouncyness,Wrap X,Bounce X,Ground bounce;;!;012;pal=11,sx=100,ix=200,c1=190,c2=128,c3=28,o1=0,o2=0,o3=1"; /* Particle Box, applies gravity to particles in either a random direction or in a rocking motion @@ -8927,20 +8775,20 @@ uint16_t mode_particlebox(void) int8_t ygravity; uint8_t scale; - if (SEGMENT.check1) - { // if random is set, use perlin noise for force vector generation - SEGMENT.aux0 += (SEGMENT.speed >> 6) + 1; // update position in noise - //uint8_t angle = (((uint16_t)inoise8(SEGMENT.aux0)) << 1) - 64; // noise avg. value is 127 scale to 256 (to increase the range) and subtract 64 (=90°) to make the average direction downwards (270°), overflow means modulo, so is ok - // calculate x and y vectors from angle: - //xgravity = ((int16_t)cos8(angle)) - 128; // gravity direction +/- 127 - //ygravity = ((int16_t)sin8(angle)) - 128; - // scale the vectors using another inoise value: - //scale = inoise8(SEGMENT.aux0 + 4096); // use a offset in the noise for scale value - - //scale = ((uint16_t)scale * SEGMENT.custom1)>>8; //apply rescaling with user input - //not using angles but x and y tilt from inoise: - xgravity = ((int16_t)inoise8(SEGMENT.aux0)-127); - ygravity = ((int16_t)inoise8(SEGMENT.aux0+10000) - 127); + if (SEGMENT.check1 == 0) // if rocking boat is not set, use perlin noise for force vector generation + { + SEGMENT.aux0 += (SEGMENT.speed >> 6) + 1; // update position in noise + // uint8_t angle = (((uint16_t)inoise8(SEGMENT.aux0)) << 1) - 64; // noise avg. value is 127 scale to 256 (to increase the range) and subtract 64 (=90°) to make the average direction downwards (270°), overflow means modulo, so is ok + // calculate x and y vectors from angle: + // xgravity = ((int16_t)cos8(angle)) - 128; // gravity direction +/- 127 + // ygravity = ((int16_t)sin8(angle)) - 128; + // scale the vectors using another inoise value: + // scale = inoise8(SEGMENT.aux0 + 4096); // use a offset in the noise for scale value + + // scale = ((uint16_t)scale * SEGMENT.custom1)>>8; //apply rescaling with user input + // not using angles but x and y tilt from inoise: + xgravity = ((int16_t)inoise8(SEGMENT.aux0) - 127); + ygravity = ((int16_t)inoise8(SEGMENT.aux0 + 10000) - 127); } else { // use sinusoidal motion @@ -8962,15 +8810,14 @@ uint16_t mode_particlebox(void) // scale gravity force xgravity = ((int16_t)xgravity * scale) >> 8; ygravity = ((int16_t)ygravity * scale) >> 8; - //todo: check with console output to see if scaling is applied correctly. + // todo: check with console output to see if scaling is applied correctly. } - - // scale the gravity force down + // scale the gravity force down xgravity = xgravity >> 4; ygravity = ygravity >> 4; - //Serial.print(xgravity); - //Serial.println(" "); + // Serial.print(xgravity); + // Serial.println(" "); for (i = 0; i < numParticles; i++) { @@ -9037,7 +8884,7 @@ uint16_t mode_particlebox(void) return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEBOX[] PROGMEM = "Particle Box@Speed,Particles,Tilt strength,Bouncyness,,Random;;!;012;pal=9,sx=100,ix=82,c1=190,c2=210,o1=1"; +static const char _data_FX_MODE_PARTICLEBOX[] PROGMEM = "Particle Box@Speed,Particles,Tilt strength,Bouncyness,,Rocking Boat;;!;012;pal=9,sx=100,ix=82,c1=190,c2=210,o1=0"; /* perlin noise 'gravity' mapping as in particles on noise hills viewed from above @@ -9128,6 +8975,307 @@ uint16_t mode_particleperlin(void) } static const char _data_FX_MODE_PARTICLEPERLIN[] PROGMEM = "Particle Perlin-Noise@,Particles,;;!;012;pal=1,ix=100"; +/* +* Particle smashing down like meteorites and exploding as they hit the ground, like inverted fireworks +* has many parameters to play with +* by DedeHai (Damian Schneider) +*/ + +// TODO: clean up the comments +//TODO: maybe add collisions? + +uint16_t mode_particleimpact(void) +{ + if (SEGLEN == 1) + return mode_static(); + + const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; + const uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); + + // particle system box dimensions + const uint16_t PS_MAX_X(cols * PS_P_RADIUS - 1); + const uint16_t PS_MAX_Y(rows * PS_P_RADIUS - 1); + +#ifdef ESP8266 + const uint16_t numParticles = 250; + const uint8_t MaxNumRockets = 4; +#else + const uint16_t numParticles = 550; + const uint8_t MaxNumMeteors = 8; +#endif + + PSparticle *particles; + PSpointsource *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); + if (!SEGENV.allocateData(dataSize)) + return mode_static(); // allocation failed; //allocation failed + + // DEBUG_PRINT(F("particle datasize = ")); + // DEBUG_PRINTLN(dataSize); + + 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 + + uint16_t i = 0; + uint16_t j = 0; + uint8_t numMeteors = map(SEGMENT.custom3, 0, 31, 1, MaxNumMeteors); // number of meteors to use for animation + + if (SEGMENT.call == 0) // initialization + { + for (i = 0; i < numParticles; i++) + { + particles[i].ttl = 0; + } + for (i = 0; i < numMeteors; i++) + { + meteors[i].source.ttl = random8(20 * i); // set initial delay for meteors + meteors[i].source.vy = 10; // at positive speeds, no particles are emitted and if particle dies, it will be relaunched + } + } + + // update particles, create particles + + // check each rocket's state and emit particles according to its state: moving up = emit exhaust, at top = explode; falling down = standby time + uint16_t emitparticles; // number of particles to emit for each rocket's state + i = 0; + for (j = 0; j < numMeteors; j++) + { + // determine rocket state by its speed: + if (meteors[j].source.vy < 0) // moving down, emit sparks + { + emitparticles = 2; + } + else if (meteors[j].source.vy > 0) // moving up means standby + { + emitparticles = 0; + } + else // speed is zero, explode! + { + meteors[j].source.vy = 2; // set source speed positive so it goes into timeout and launches again + emitparticles = random8(SEGMENT.intensity >> 1) + 10; // defines the size of the explosion + } + + for (i; i < numParticles; i++) + { + if (particles[i].ttl == 0) + { // particle is dead + if (emitparticles > 0) + { + Emitter_Fountain_emit(&meteors[j], &particles[i]); + emitparticles--; + } + else + break; // done emitting for this meteor + } + } + } + + // update particles + for (i = 0; i < numParticles; i++) + { + if (particles[i].ttl) + { + Particle_Gravity_update(&particles[i], SEGMENT.check1, SEGMENT.check2, SEGMENT.check3, SEGMENT.custom2); + } + } + + // update the meteors, set the speed state + for (i = 0; i < numMeteors; i++) + { + Serial.print(meteors[i].source.vy); + Serial.print(" "); + if (meteors[i].source.ttl) + { + Particle_Move_update(&meteors[i].source); // move the meteor, age the meteor (ttl--) + // if source reaches the bottom, set speed to 0 so it will explode on next function call (handled above) + if ((meteors[i].source.y < PS_P_RADIUS) && (meteors[i].source.vy < 0)) // reached the bottom pixel on its way down + { + meteors[i].source.vy = 0; // set speed zero so it will explode + meteors[i].source.vx = 0; + meteors[i].source.y = 5; // offset from ground so explosion happens not out of frame (if moving fast, this can happen) + meteors[i].source.hue = random8(); // random color of explosion + meteors[i].maxLife = 200; + meteors[i].minLife = 50; + meteors[i].source.ttl = random8((255 - SEGMENT.speed)) + 10; // standby time til next launch (in frames at 42fps, max of 265 is about 6 seconds + meteors[i].vx = 0; // emitting speed x + meteors[i].vy = 8; // emitting speed y + meteors[i].var = (SEGMENT.custom1 >> 1); // speed variation around vx,vy (+/- var/2) + } + } + else if (meteors[i].source.vy > 0) // rocket is exploded and time is up (ttl==0 and positive speed), relaunch it + { + // reinitialize rocket + meteors[i].source.y = PS_MAX_Y - 2; // start from top + meteors[i].source.x = random16(PS_MAX_X); + meteors[i].source.vy = -30 - random(30) - 10; // TODO: need to make this user selectable? + meteors[i].source.vx = random8(30) - 15; + meteors[i].source.hue = 220; // rocket exhaust = orange (if using fire palette) + meteors[i].source.ttl = 1000; // long live, will explode at bottom + meteors[i].maxLife = 60; // spark particle life + meteors[i].minLife = 20; + meteors[i].vx = 0; // emitting speed + meteors[i].vy = -10; // emitting speed + meteors[i].var = 5; // speed variation around vx,vy (+/- var/2) + } + } + SEGMENT.fill(BLACK); // clear the matrix + // render the particles + ParticleSys_render(particles, numParticles, 255, false, false); + + return FRAMETIME; +} +static const char _data_FX_MODE_PARTICLEIMPACT[] PROGMEM = "Particle Impact@Launches,Explosion Size,Explosion Force,Bounce,Meteors,Wrap X,Bounce X,Bounce Y;;!;012;pal=8,sx=100,ix=50,c1=64,c2=128,c3=10,o1=0,o2=0,o3=1"; + +/* +Particle Attractor, currently just a demo function for the particle attractor +Attractor sits in the matrix center, a spray bounces around and seeds particles. +Uses palette for particle color +by DedeHai (Damian Schneider) +*/ + +uint16_t mode_particleattractor(void) +{ + if (SEGLEN == 1) return mode_static(); + + const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; + const uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); + // particle system box dimensions + const uint16_t PS_MAX_X(cols * PS_P_RADIUS - 1); + const uint16_t PS_MAX_Y(rows * PS_P_RADIUS - 1); + + const uint16_t numParticles = 255; // maximum number of particles + + PSparticle *particles; + PSparticle *attractor; + PSpointsource *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); + dataSize += sizeof(uint8_t) *numParticles; + dataSize += sizeof(PSpointsource); + Serial.println("*************"); + Serial.println(dataSize); + if (!SEGENV.allocateData(dataSize)) + return mode_static(); // allocation failed; //allocation failed + // divide and cast the data array into correct pointers + particles = reinterpret_cast(SEGENV.data); + attractor = reinterpret_cast(particles + 1); + spray = reinterpret_cast(attractor + 1); + counters = reinterpret_cast(spray + 1); + Serial.println((uintptr_t)particles); + Serial.println((uintptr_t)attractor); + Serial.println((uintptr_t)spray); + Serial.println((uintptr_t)counters); + + uint16_t i = 0; + uint16_t j = 0; + + if (SEGMENT.call == 0) // initialization + { + attractor->vx = 0; + attractor->vy = 0; + attractor->x = PS_MAX_X >> 1; // center + attractor->y = PS_MAX_Y >> 1; + + for (i = 0; i < numParticles; i++) + { + particles[i].ttl = 0; + } + + spray->source.hue = random8(); + spray->source.x = 0; + spray->source.y = 0; // just above the lower edge, if zero, particles already 'bounce' at start and loose speed. + spray->source.vx = random8(5) + 6; + spray->source.vy = random8(4) + 3; + spray->source.ttl = 100; + spray->maxLife = 300; // seeded particle lifetime in frames + spray->minLife = 30; + spray->vx = 0; // emitting speed + spray->vy = 0; // emitting speed + spray->var = 6; //emitting speed variation + } + + uint16_t displayparticles = SEGMENT.intensity; + uint8_t hardness = SEGMENT.custom2; // how hard the collisions are, 255 = full hard. + i = 0; + j = 0; + + if (hardness > 1) // enable collisions + { + // detect and handle collisions + int16_t startparticle = 0; + int16_t endparticle = displayparticles >> 1; // do half the particles + + 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 which is overkill) + startparticle = endparticle; + endparticle = displayparticles; + } + + for (i = startparticle; i < endparticle; i++) + { + // go though all 'higher number' particles and see if any of those are in close proximity + // if they are, make them collide + if (particles[i].ttl > 0) // if particle is alive + { + int32_t dx, dy; // distance to other particles + for (j = i + 1; j < displayparticles; j++) + { // check against higher number particles + if (particles[j].ttl > 0) + { // if target particle is alive + dx = particles[i].x - particles[j].x; + 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 (SEGMENT.call % 5 == 0) + { + spray->source.hue++; + spray->source.ttl = 100; //spray never dies + } + + uint8_t emit = 1; //number of particles emitted per frame + Particle_Bounce_update(&spray->source, 255); //bounce the spray around + + // now move the particles + for (i = 0; i < displayparticles; i++) + { + + if (particles[i].ttl == 0 && emit--) // find a dead particle + { + Emitter_Fountain_emit(spray, &particles[i]); + } + + Particle_attractor(&particles[i], attractor, &counters[i], SEGMENT.speed, SEGMENT.check3); + + // Particle_Bounce_update(&particles[i], hardness); + Particle_Move_update(&particles[i]); + } + + SEGMENT.fill(BLACK); // clear the matrix + //ParticleSys_render(&attract, 1, 30, false, false); // render attractor + // render the particles + ParticleSys_render(particles, displayparticles, 255, false, false); + + return FRAMETIME; +} +static const char _data_FX_MODE_PARTICLEATTRACTOR[] PROGMEM = "Particle Attractor@Center Mass,Particles,,Collision Strength,,,,Swallow;;!;012;pal=9,sx=100,ix=82,c1=190,c2=210,o1=0,o2=0,o3=0"; + #endif // WLED_DISABLE_2D @@ -9377,9 +9525,12 @@ void WS2812FX::setupEffectData() { 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); - + +//experimental addEffect(FX_MODE_PARTICLEPILE, &mode_particlepile, _data_FX_MODE_PARTICLEPILE); //particle sandbox used for testing new stuff - + addEffect(FX_MODE_PARTICLEIMPACT, &mode_particleimpact, _data_FX_MODE_PARTICLEIMPACT); + addEffect(FX_MODE_PARTICLEATTRACTOR, &mode_particleattractor, _data_FX_MODE_PARTICLEATTRACTOR); + #endif // WLED_DISABLE_2D } diff --git a/wled00/FX.h b/wled00/FX.h index 37eb2ba246..7621fe023e 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -325,8 +325,10 @@ #define FX_MODE_PARTICLEPERLIN 191 #define FX_MODE_PARTICLEFALL 192 #define FX_MODE_PARTICLEBOX 193 -#define FX_MODE_PARTICLEPILE 194 -#define MODE_COUNT 195 +#define FX_MODE_PARTICLEATTRACTOR 194 +#define FX_MODE_PARTICLEIMPACT 195 +#define FX_MODE_PARTICLEPILE 196 +#define MODE_COUNT 197 typedef enum mapping1D2D { M12_Pixels = 0, diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index 86c4be4644..7d5563c487 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -53,76 +53,103 @@ void Emitter_Fountain_emit(PSpointsource *emitter, PSparticle *part) part->hue = emitter->source.hue; } -//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) //todo: add a parameter 'swallow' so the attractor can 'suck up' particles that are very close +// 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) // todo: add a parameter 'swallow' so the attractor can 'suck up' particles that are very close, also could use hue of attractor particle for strength { // Calculate the distance between the particle and the attractor int16_t dx = attractor->x - particle->x; int16_t dy = attractor->y - particle->y; // Calculate the force based on inverse square law - int32_t distanceSquared = dx * dx + dy * dy; - //check if distance is small enough to even cause a force (for that strength<<10 must be bigger than the distance squared + int32_t distanceSquared = dx * dx + dy * dy + 1; + if (distanceSquared < 4096) + { + if (swallow) // particle is close, kill it + { + particle->ttl = 0; + return; + } + distanceSquared = 4096; // limit the distance to 64 (=size of a particle) to avoid very high forces.TODO: could make this depending on the #define for particle size + } + // check if distance is small enough to even cause a force (for that strength<<10 must be bigger than the distance squared) int32_t shiftedstrength = (int32_t)strength << 16; - if (shiftedstrength > distanceSquared) //if too far away, no force is applied (force < 1) + int32_t force; + int32_t xforce; + int32_t yforce; + int32_t xforce_abs; // absolute value + int32_t yforce_abs; + + if (shiftedstrength < distanceSquared) // if far away, set the force to 1 so it still attracts and does not leave particles just sitting outside its influence radius { - int32_t force = shiftedstrength / distanceSquared; - int32_t xforce = (force * dx)>>12; //scale so that at force starts to increase at about 10 pixels away - int32_t yforce = (force * dy)>>12; + // force calculation above is zero + //give some force in both directions (x and y) to avoid further calculations as this is just to get things moving a little + xforce_abs = 1; + yforce_abs = 1; + } + else + { + force = shiftedstrength / distanceSquared; + xforce = (force * dx) >> 10; // scale to a lower value, found by experimenting + yforce = (force * dy) >> 10; + xforce_abs = abs(xforce); // absolute value + yforce_abs = abs(yforce); + } + uint8_t xcounter = (*counter) & 0x0F; // lower four bits + uint8_t ycounter = (*counter) >> 4; // upper four bits - uint8_t xcounter = *counter & 0xF; //lower four bits - uint8_t ycounter = *counter>>4; // upper four bits - *counter = 0; //reset counter, is set back to correct values below + *counter = 0; // reset counter, is set back to correct values below - //man muss die jetzt noch schlau shiften, dazu ein excel machen - //for small forces, need to use a delay timer (counter) - if(xforce < 16) + // for small forces, need to use a delay timer (counter) + if (xforce_abs < 16) + { + xcounter += xforce_abs; + if (xcounter > 15) { - xcounter += force; - if (xcounter > 16) + xcounter -= 15; + *counter |= xcounter & 0x0F; // write lower four bits, make sure not to write more than 4 bits + // apply force in x direction + if (dx < 0) { - xcounter -= 16; - *counter |= xcounter; // write lower four bits - // apply force in x direction - if (dx < 0) - { - particle->vx -= 1; - } - else - { - particle->vx += 1; - } + particle->vx -= 1; + } + else + { + particle->vx += 1; } - - } - else{ - particle->vx += xforce/16; } + } + else + { + particle->vx += xforce >> 4; // divide by 16 + } - if(yforce < 16) + if (yforce_abs < 16) + { + ycounter += yforce_abs; + + if (ycounter > 15) { - ycounter += yforce; - if(ycounter > 16) - { - ycounter -= 16; - *counter |= (ycounter<<4); //write upper four bits - if (dy < 0) - { - particle->vy -= 1; - } - else - { - particle->vy += 1; - } - } + ycounter -= 15; + *counter |= (ycounter << 4) & 0xF0; // write upper four bits + if (dy < 0) + { + Serial.println("A"); + particle->vy -= 1; + } + else + { + Serial.println("B"); + particle->vy += 1; + } } - else{ - particle->vy += yforce / 16; - } - } + else + { + particle->vy += yforce >> 4; // divide by 16 + } + // TODO: need to limit the max speed? } // TODO: could solve all update functions in a single function with parameters and handle gravity acceleration in a separte function (uses more cpu time but that is not a huge issue) or maybe not, like this, different preferences can be set @@ -185,13 +212,13 @@ void Particle_Bounce_update(PSparticle *part, const uint8_t hardness) // bounces 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) >> 8; // reduce speed as energy is lost on non-hard surface + 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) >> 8; // reduce speed as energy is lost on non-hard surface + part->vy = (((int16_t)part->vy) * ((int16_t)hardness+1)) >> 8; // reduce speed as energy is lost on non-hard surface } newX = max(newX, (int16_t)0); // limit to positive diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index aec519cd9f..82f0359244 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -40,7 +40,7 @@ typedef struct { int16_t x; //x position in particle system int16_t y; //y position in particle system uint16_t ttl; //time to live - uint8_t outofbounds; //set to 1 if outside of matrix + uint8_t outofbounds; //set to 1 if outside of matrix TODO: could make this a union and add more flags instead of wasting a whole byte on this uint8_t hue; //color hue uint8_t sat; //color saturation int8_t vx; //horizontal velocity @@ -69,6 +69,7 @@ int8_t vortexpull; //if positive, vortex pushes, if negative it pulls void Emitter_Flame_emit(PSpointsource *emitter, PSparticle *part); void Emitter_Fountain_emit(PSpointsource *emitter, PSparticle *part); +void Particle_attractor(PSparticle *particle, PSparticle *attractor, uint8_t *counter, uint8_t strength, bool swallow); void Particle_Move_update(PSparticle *part); 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); From 7bcfcb445af6b617229c94d7fd058521ea472837 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Thu, 8 Feb 2024 18:33:00 +0100 Subject: [PATCH 012/219] bugfixes, attracot now works still unknown, why more than 256 particles are needed in memory allocation to not make it crash, but it works for now --- wled00/FX.cpp | 17 ++++++----------- wled00/FXparticleSystem.cpp | 2 -- 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 74a8e6f102..63c46dece9 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -9147,7 +9147,7 @@ uint16_t mode_particleattractor(void) const uint16_t PS_MAX_X(cols * PS_P_RADIUS - 1); const uint16_t PS_MAX_Y(rows * PS_P_RADIUS - 1); - const uint16_t numParticles = 255; // maximum number of particles + const uint16_t numParticles = 265; // maximum number of particles PSparticle *particles; PSparticle *attractor; @@ -9159,19 +9159,14 @@ uint16_t mode_particleattractor(void) uint32_t dataSize = sizeof(PSparticle) * (numParticles + 1); dataSize += sizeof(uint8_t) *numParticles; dataSize += sizeof(PSpointsource); - Serial.println("*************"); - Serial.println(dataSize); + if (!SEGENV.allocateData(dataSize)) return mode_static(); // allocation failed; //allocation failed // divide and cast the data array into correct pointers - particles = reinterpret_cast(SEGENV.data); - attractor = reinterpret_cast(particles + 1); + particles = reinterpret_cast(SEGENV.data); + attractor = reinterpret_cast(particles + numParticles + 1); spray = reinterpret_cast(attractor + 1); counters = reinterpret_cast(spray + 1); - Serial.println((uintptr_t)particles); - Serial.println((uintptr_t)attractor); - Serial.println((uintptr_t)spray); - Serial.println((uintptr_t)counters); uint16_t i = 0; uint16_t j = 0; @@ -9201,7 +9196,7 @@ uint16_t mode_particleattractor(void) spray->var = 6; //emitting speed variation } - uint16_t displayparticles = SEGMENT.intensity; + uint16_t displayparticles = SEGMENT.intensity-1; //cannot go to 255 particles, it will crash if set above 250, why is unclear... maybe array is too small? at 260 it will still crash, if numparticles are set to 265 it does not crash... uint8_t hardness = SEGMENT.custom2; // how hard the collisions are, 255 = full hard. i = 0; j = 0; @@ -9222,7 +9217,7 @@ uint16_t mode_particleattractor(void) { // go though all 'higher number' particles and see if any of those are in close proximity // if they are, make them collide - if (particles[i].ttl > 0) // if particle is alive + if (particles[i].ttl > 0 && particles[i].outofbounds==0) // if particle is alive and on screen { int32_t dx, dy; // distance to other particles for (j = i + 1; j < displayparticles; j++) diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index 7d5563c487..f09c418296 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -135,12 +135,10 @@ void Particle_attractor(PSparticle *particle, PSparticle *attractor, uint8_t *co if (dy < 0) { - Serial.println("A"); particle->vy -= 1; } else { - Serial.println("B"); particle->vy += 1; } } From a147a4bd97483aaf08b2197f2b740b686a6f8506 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Thu, 8 Feb 2024 20:40:21 +0100 Subject: [PATCH 013/219] added angle emitter, added fireworks effect using it --- wled00/FX.cpp | 29 ++++++++++++++++++++++++++--- wled00/FXparticleSystem.cpp | 17 +++++++++++++++-- wled00/FXparticleSystem.h | 3 ++- 3 files changed, 43 insertions(+), 6 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index a4e39d75cc..9308e8dc07 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -8033,7 +8033,7 @@ uint16_t mode_particlefireworks(void) uint16_t i = 0; uint16_t j = 0; - uint8_t numRockets = (SEGMENT.custom3+1) >> 2; //1 to 8 + uint8_t numRockets = 1+ ((SEGMENT.custom3) >> 2); //1 to 8 if (SEGMENT.call == 0) // initialization { @@ -8068,13 +8068,36 @@ uint16_t mode_particlefireworks(void) { // speed is zero, explode! emitparticles = random8(SEGMENT.intensity>>1) + 10; // defines the size of the explosion rockets[j].source.vy = -1; // set speed negative so it will emit no more particles after this explosion until relaunch + if (j == 0) // first rocket, do an angle emit + { + emitparticles>>1; //emit less particles for circle-explosion + rockets[j].maxLife = 150; + rockets[j].minLife = 120; + rockets[j].var = 0; // speed variation around vx,vy (+/- var/2) + } } + uint8_t speed = 5; + uint8_t angle = 0; + for (i; i < numParticles; i++) { if (particles[i].ttl == 0) { // particle is dead - if (emitparticles > 0) + + if (j == 0 && emitparticles > 2) // first rocket, do angle emit + { + Emitter_Angle_emit(&rockets[j], &particles[i],angle,speed); + emitparticles--; + //set angle for next particle + angle += 21; //about 30° + if(angle > 250) //full circle completed, increase speed and reset angle + { + angle = 0; + speed += 8; + } + } + else if (emitparticles > 0) { Emitter_Fountain_emit(&rockets[j], &particles[i]); emitparticles--; @@ -8114,7 +8137,7 @@ uint16_t mode_particlefireworks(void) rockets[i].source.ttl = random8((255 - SEGMENT.speed))+10; // standby time til next launch (in frames at 42fps, max of 265 is about 6 seconds rockets[i].vx = 0; // emitting speed rockets[i].vy = 0; // emitting speed - rockets[i].var = (SEGMENT.intensity >> 2) + 10; // speed variation around vx,vy (+/- var/2) + rockets[i].var = (SEGMENT.intensity >> 3) + 10; // speed variation around vx,vy (+/- var/2) } else if (rockets[i].source.vy < 0) // rocket is exploded and time is up (ttl=0 and negative speed), relaunch it { diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index f09c418296..29c8fe668d 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -45,14 +45,27 @@ void Emitter_Flame_emit(PSpointsource *emitter, PSparticle *part) // fountain style emitter void Emitter_Fountain_emit(PSpointsource *emitter, PSparticle *part) { - part->x = emitter->source.x + random8(emitter->var) - (emitter->var >> 1); - part->y = emitter->source.y + random8(emitter->var) - (emitter->var >> 1); + part->x = emitter->source.x; // + random8(emitter->var) - (emitter->var >> 1); //randomness uses cpu cycles and is almost invisible, removed for now. + part->y = emitter->source.y; // + random8(emitter->var) - (emitter->var >> 1); part->vx = emitter->vx + random8(emitter->var) - (emitter->var >> 1); part->vy = emitter->vy + random8(emitter->var) - (emitter->var >> 1); part->ttl = (rand() % (emitter->maxLife - emitter->minLife)) + emitter->minLife; part->hue = emitter->source.hue; } +// 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) +{ + 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; + Serial.print(angle); + Serial.print(" "); + Serial.print(emitter->vx); + Serial.print(" "); + Serial.print(emitter->vy); + Serial.print(" "); + Emitter_Fountain_emit(emitter, part); +} // 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) // todo: add a parameter 'swallow' so the attractor can 'suck up' particles that are very close, also could use hue of attractor particle for strength { diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index 82f0359244..0bcacd79ce 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -69,7 +69,8 @@ int8_t vortexpull; //if positive, vortex pushes, if negative it pulls void Emitter_Flame_emit(PSpointsource *emitter, PSparticle *part); void Emitter_Fountain_emit(PSpointsource *emitter, PSparticle *part); -void Particle_attractor(PSparticle *particle, PSparticle *attractor, uint8_t *counter, uint8_t strength, bool swallow); +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); 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); From 18c79cedda9a3eaa20b01fd209b2dd3680203c14 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Thu, 8 Feb 2024 21:19:30 +0100 Subject: [PATCH 014/219] Added saturation to be set for each particle individually at the expense of more ram usage, animations now have more options for color control (already used in fireworks now) --- wled00/FX.cpp | 47 +++++++++++++++++++++++-------------- wled00/FXparticleSystem.cpp | 13 ++++++---- wled00/FXparticleSystem.h | 2 +- 3 files changed, 39 insertions(+), 23 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 9308e8dc07..db781b8e5a 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -7915,11 +7915,12 @@ uint16_t mode_particlerotatingspray(void) SEGMENT.aux0 = 0; // starting angle for (i = 0; i < numParticles; i++) { - particles[i].ttl = 0; + particles[i].ttl = 0; } for (i = 0; i < numSprays; i++) { spray[i].source.hue = random8(); + spray[i].source.sat = 255; // set saturation spray[i].source.x = (cols * PS_P_RADIUS) / 2; // center spray[i].source.y = (cols * PS_P_RADIUS) / 2; // center spray[i].source.vx = 0; @@ -7980,7 +7981,7 @@ uint16_t mode_particlerotatingspray(void) SEGMENT.fill(BLACK); // clear the matrix // render the particles - ParticleSys_render(particles, numParticles, 255, false, false); + ParticleSys_render(particles, numParticles, false, false); return FRAMETIME; } @@ -8070,7 +8071,7 @@ uint16_t mode_particlefireworks(void) rockets[j].source.vy = -1; // set speed negative so it will emit no more particles after this explosion until relaunch if (j == 0) // first rocket, do an angle emit { - emitparticles>>1; //emit less particles for circle-explosion + emitparticles>>2; //emit less particles for circle-explosion rockets[j].maxLife = 150; rockets[j].minLife = 120; rockets[j].var = 0; // speed variation around vx,vy (+/- var/2) @@ -8095,6 +8096,8 @@ uint16_t mode_particlefireworks(void) { angle = 0; speed += 8; + rockets[j].source.hue = random8(); //new color for next row + rockets[j].source.sat = random8(); } } else if (emitparticles > 0) @@ -8132,6 +8135,7 @@ uint16_t mode_particlefireworks(void) { // rocket has died and is moving up. stop it so it will explode (is handled in the code above) rockets[i].source.vy = 0; // set speed to zero so code above will recognize this as an exploding rocket rockets[i].source.hue = random8(); // random color + rockets[i].source.sat = random8(100)+155; rockets[i].maxLife = 200; rockets[i].minLife = 50; rockets[i].source.ttl = random8((255 - SEGMENT.speed))+10; // standby time til next launch (in frames at 42fps, max of 265 is about 6 seconds @@ -8147,6 +8151,7 @@ uint16_t mode_particlefireworks(void) rockets[i].source.vy = random8(SEGMENT.custom1>>3) + 5; //rocket speed depends also on rocket height rockets[i].source.vx = random8(5) - 2; rockets[i].source.hue = 30; // rocket exhaust = orange (if using rainbow palette) + rockets[i].source.sat = 250; rockets[i].source.ttl = random8(SEGMENT.custom1) + (SEGMENT.custom1>>1); // sets explosion height (rockets explode at the top if set too high as paticle update set speed to zero if moving out of matrix) rockets[i].maxLife = 30; //exhaust particle life rockets[i].minLife = 10; @@ -8158,7 +8163,7 @@ uint16_t mode_particlefireworks(void) SEGMENT.fill(BLACK); // clear the matrix // render the particles - ParticleSys_render(particles, numParticles, 255, false, false); + ParticleSys_render(particles, numParticles, false, false); return FRAMETIME; } @@ -8207,11 +8212,12 @@ uint16_t mode_particlespray(void) { for (i = 0; i < numParticles; i++) { - particles[i].ttl = 0; + particles[i].ttl = 0; } for (i = 0; i < numSprays; i++) { spray[i].source.hue = random8(); + spray[i].source.sat = 255; // set full saturation spray[i].source.x = 4 * PS_P_RADIUS * (i + 1); spray[i].source.y = 5; // just above the lower edge, if zero, particles already 'bounce' at start and loose speed. spray[i].source.vx = 10; @@ -8283,7 +8289,7 @@ uint16_t mode_particlespray(void) SEGMENT.fill(BLACK); // clear the matrix // render the particles - ParticleSys_render(particles, numParticles, 255, SEGMENT.check1, false); + ParticleSys_render(particles, numParticles, SEGMENT.check1, false); // CRGB c = PURPLE; // SEGMENT.setPixelColorXY(0, 0, c); @@ -8507,7 +8513,7 @@ uint16_t mode_particlefall(void) particles[i].ttl = 0; } } - + particles[i].sat = ((SEGMENT.custom3) << 3) + 7; // set saturation if (SEGMENT.call % (64 - (SEGMENT.intensity >> 2)) == 0 && SEGMENT.intensity > 1) // every nth frame emit particles, stop emitting if zero { while (i < numParticles) // emit particles @@ -8526,6 +8532,7 @@ uint16_t mode_particlefall(void) particles[i].vx = (((int16_t)random8(SEGMENT.custom1)) - (SEGMENT.custom1 >> 1)) >> 1; // side speed is +/- a quarter of the custom1 slider particles[i].vy = -(SEGMENT.speed >> 1); particles[i].hue = random8(); // set random color + particles[i].sat = ((SEGMENT.custom3) << 3) + 7; // set saturation break; // quit loop if all particles of this round emitted } i++; @@ -8581,8 +8588,10 @@ uint16_t mode_particlefall(void) SEGMENT.fill(BLACK); // clear the matrix + + // render the particles - ParticleSys_render(particles, numParticles, ((SEGMENT.custom3) << 3) + 7, SEGMENT.check1, false); // custom3 slider is saturation, from 7 to 255, 7 is close enough to white (for snow for example) + ParticleSys_render(particles, numParticles, SEGMENT.check1, false); // custom3 slider is saturation, from 7 to 255, 7 is close enough to white (for snow for example) return FRAMETIME; } @@ -8627,11 +8636,12 @@ uint16_t mode_particlepile(void) { for (i = 0; i < numParticles; i++) { - particles[i].ttl = 0; + particles[i].ttl = 0; } for (i = 0; i < numSprays; i++) { spray[i].source.hue = random8(); + spray[i].source.sat = 255; // set full saturation spray[i].source.x = 2 * PS_P_RADIUS * (i + 1); spray[i].source.y = 14 * PS_P_RADIUS; // source y position, fixed at 14pixel height spray[i].source.vx = 10; @@ -8741,7 +8751,7 @@ uint16_t mode_particlepile(void) SEGMENT.fill(BLACK); // clear the matrix // render the particles - ParticleSys_render(particles, numParticles, 255, SEGMENT.check1, false); + ParticleSys_render(particles, numParticles, SEGMENT.check1, false); return FRAMETIME; } @@ -8779,8 +8789,9 @@ uint16_t mode_particlebox(void) SEGMENT.aux0 = rand(); // position (either in noise or in sine function) for (i = 0; i < numParticles; i++) { - particles[i].ttl = 500; // all particles are alive (but not all are calculated/rendered) + particles[i].ttl = 500; // all particles are alive (but not all are calculated/rendered) particles[i].hue = i * 3; // full color range (goes over palette colors three times so it is also colorful when using fewer particles) + particles[i].sat = 255; // set full saturation (lets palette choose the color) particles[i].x = map(i, 0, 255, 1, cols * PS_P_RADIUS); // distribute along x according to color particles[i].y = random16((rows >> 2) * PS_P_RADIUS); // in the bottom quarder } @@ -8903,7 +8914,7 @@ uint16_t mode_particlebox(void) SEGMENT.fill(BLACK); // clear the matrix // render the particles - ParticleSys_render(particles, displayparticles, 255, false, false); + ParticleSys_render(particles, displayparticles, false, false); return FRAMETIME; } @@ -8947,6 +8958,7 @@ uint16_t mode_particleperlin(void) particles[i].ttl = random16(500) + 200; particles[i].x = random16(cols * PS_P_RADIUS); particles[i].y = random16(rows * PS_P_RADIUS); + particles[i].sat = 255; //full saturation, color set by palette } } @@ -8992,7 +9004,7 @@ uint16_t mode_particleperlin(void) SEGMENT.fill(BLACK); // clear the matrix // render the particles - ParticleSys_render(particles, displayparticles, 255, false, false); + ParticleSys_render(particles, displayparticles, false, false); return FRAMETIME; } @@ -9036,9 +9048,6 @@ uint16_t mode_particleimpact(void) if (!SEGENV.allocateData(dataSize)) return mode_static(); // allocation failed; //allocation failed - // DEBUG_PRINT(F("particle datasize = ")); - // DEBUG_PRINTLN(dataSize); - 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 @@ -9057,6 +9066,7 @@ uint16_t mode_particleimpact(void) { meteors[i].source.ttl = random8(20 * i); // set initial delay for meteors meteors[i].source.vy = 10; // at positive speeds, no particles are emitted and if particle dies, it will be relaunched + meteors[i].source.sat = 255; //full saturation, color chosen by palette } } @@ -9147,7 +9157,7 @@ uint16_t mode_particleimpact(void) } SEGMENT.fill(BLACK); // clear the matrix // render the particles - ParticleSys_render(particles, numParticles, 255, false, false); + ParticleSys_render(particles, numParticles, false, false); return FRAMETIME; } @@ -9207,6 +9217,7 @@ uint16_t mode_particleattractor(void) } spray->source.hue = random8(); + spray->source.sat = 255; //full saturation, color by palette spray->source.x = 0; spray->source.y = 0; // just above the lower edge, if zero, particles already 'bounce' at start and loose speed. spray->source.vx = random8(5) + 6; @@ -9288,7 +9299,7 @@ uint16_t mode_particleattractor(void) SEGMENT.fill(BLACK); // clear the matrix //ParticleSys_render(&attract, 1, 30, false, false); // render attractor // render the particles - ParticleSys_render(particles, displayparticles, 255, false, false); + ParticleSys_render(particles, displayparticles, false, false); return FRAMETIME; } diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index 29c8fe668d..a58ef50870 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -31,7 +31,7 @@ #include "FastLED.h" #include "FX.h" -// Fountain style emitter for simple particles used for flames (particle TTL depends on source TTL) +// Fountain style emitter for particles used for flames (particle TTL depends on source TTL) void Emitter_Flame_emit(PSpointsource *emitter, PSparticle *part) { part->x = emitter->source.x + random8(emitter->var) - (emitter->var >> 1); @@ -40,6 +40,7 @@ void Emitter_Flame_emit(PSpointsource *emitter, PSparticle *part) part->vy = emitter->vy + random8(emitter->var) - (emitter->var >> 1); part->ttl = (uint8_t)((rand() % (emitter->maxLife - emitter->minLife)) + emitter->minLife + emitter->source.ttl); // flame intensity dies down with emitter TTL part->hue = emitter->source.hue; + part->sat = emitter->source.sat; } // fountain style emitter @@ -51,6 +52,7 @@ void Emitter_Fountain_emit(PSpointsource *emitter, PSparticle *part) part->vy = emitter->vy + random8(emitter->var) - (emitter->var >> 1); part->ttl = (rand() % (emitter->maxLife - emitter->minLife)) + emitter->minLife; part->hue = emitter->source.hue; + part->sat = emitter->source.sat; } // Emits a particle at given angle and speed, angle is from 0-255 (=0-360deg), speed is also affected by emitter->var @@ -333,7 +335,7 @@ void Particle_Gravity_update(PSparticle *part, bool wrapX, bool bounceX, bool bo // 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 // saturation is color saturation, if not set to 255, hsv instead of palette is used (palette does not support saturation) -void ParticleSys_render(PSparticle *particles, uint16_t numParticles, uint8_t saturation, bool wrapX, bool wrapY) +void ParticleSys_render(PSparticle *particles, uint16_t numParticles, bool wrapX, bool wrapY) { const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; @@ -360,10 +362,10 @@ void ParticleSys_render(PSparticle *particles, uint16_t numParticles, uint8_t sa // generate RGB values for particle brightess = min(particles[i].ttl, (uint16_t)255); - if (saturation < 255) + if (particles[i].sat < 255) { CHSV baseHSV = rgb2hsv_approximate(ColorFromPalette(SEGPALETTE, particles[i].hue, 255, LINEARBLEND)); - baseHSV.s = saturation; + baseHSV.s = particles[i].sat; baseRGB = (CRGB)baseHSV; } else @@ -791,6 +793,9 @@ void handleCollision(PSparticle *particle1, PSparticle *particle2, const uint8_t // slow down particle by friction, the higher the speed, the higher the friction void applyFriction(PSparticle *particle, uint8_t coefficient) { + if(particle->ttl) + { particle->vx = ((int16_t)particle->vx * (255 - coefficient)) >> 8; particle->vy = ((int16_t)particle->vy * (255 - coefficient)) >> 8; + } } diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index 0bcacd79ce..695157f7a8 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -74,7 +74,7 @@ void Particle_attractor(PSparticle *particle, PSparticle *attractor, uint8_t *co void Particle_Move_update(PSparticle *part); 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, uint16_t numParticles, uint8_t saturation, bool wrapX, bool wrapY); +void ParticleSys_render(PSparticle *particles, uint16_t numParticles, bool wrapX, bool wrapY); void FireParticle_update(PSparticle *part, bool wrapX, bool WrapY); void ParticleSys_renderParticleFire(PSparticle *particles, uint16_t numParticles, bool wrapX); void PartMatrix_addHeat(uint8_t col, uint8_t row, uint16_t heat); From 7d6965d14c3fcfcc6737ae98cf87bed1c1cfe195 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Thu, 8 Feb 2024 21:50:51 +0100 Subject: [PATCH 015/219] bugfixes in impact animation --- wled00/FX.cpp | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index db781b8e5a..1f4f4b8868 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -9062,7 +9062,7 @@ uint16_t mode_particleimpact(void) { particles[i].ttl = 0; } - for (i = 0; i < numMeteors; i++) + for (i = 0; i < MaxNumMeteors; i++) { meteors[i].source.ttl = random8(20 * i); // set initial delay for meteors meteors[i].source.vy = 10; // at positive speeds, no particles are emitted and if particle dies, it will be relaunched @@ -9129,8 +9129,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 (if moving fast, this can happen) - meteors[i].source.hue = random8(); // random color of explosion + meteors[i].source.y = 5; // offset from ground so explosion happens not out of frame (if moving fast, this can happen) meteors[i].maxLife = 200; meteors[i].minLife = 50; meteors[i].source.ttl = random8((255 - SEGMENT.speed)) + 10; // standby time til next launch (in frames at 42fps, max of 265 is about 6 seconds @@ -9142,11 +9141,11 @@ uint16_t mode_particleimpact(void) else if (meteors[i].source.vy > 0) // rocket is exploded and time is up (ttl==0 and positive speed), relaunch it { // reinitialize rocket - meteors[i].source.y = PS_MAX_Y - 2; // start from top + meteors[i].source.y = PS_MAX_Y + PS_P_RADIUS<<2; // start 4 pixels above the top meteors[i].source.x = random16(PS_MAX_X); meteors[i].source.vy = -30 - random(30) - 10; // TODO: need to make this user selectable? meteors[i].source.vx = random8(30) - 15; - meteors[i].source.hue = 220; // rocket exhaust = orange (if using fire palette) + meteors[i].source.hue = random8(); // random color meteors[i].source.ttl = 1000; // long live, will explode at bottom meteors[i].maxLife = 60; // spark particle life meteors[i].minLife = 20; From d00126bce066521cfeb62be7ef3868db520d4250 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Thu, 8 Feb 2024 22:34:36 +0100 Subject: [PATCH 016/219] added option to use fast color add, may improve performance a little also fixed a bug in fire animation --- wled00/FX.cpp | 27 +++++++++++++++------------ wled00/FXparticleSystem.cpp | 16 ++++++++-------- wled00/FXparticleSystem.h | 2 +- 3 files changed, 24 insertions(+), 21 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 1f4f4b8868..78a790f0ad 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -8387,19 +8387,22 @@ uint16_t mode_particlefire(void) // initialize new flame: set properties of source // from time to time, chang the flame position // make some of the flames small and slow to add a bright base + + if (random8(40) == 0) //from time to time, change flame position (about once per second) + { + if (SEGMENT.check1) + { // wrap around in X direction, distribute randomly + flames[i].source.x = random16(PS_MAX_X); + } + else + { // no wrapping + flames[i].source.x = PS_P_RADIUS * 3 + random16(PS_MAX_X - (PS_P_RADIUS * 6)); // distribute randomly but not close to the corners + } + } + if (i < (numFlames - (cols >> 1))) { // all but the last few are normal flames - if (random8(40) == 0) - { - if (SEGMENT.check1) - { // wrap around in X direction, distribute randomly - flames[i].source.x = random16(PS_MAX_X); - } - else - { // no wrapping - flames[i].source.x = PS_P_RADIUS * 3 + random16(PS_MAX_X - (PS_P_RADIUS * 6)); // distribute randomly but not close to the corners - } - } + flames[i].source.y = -1 * PS_P_RADIUS; // set the source below the frame so particles alredy spread a little when the appear flames[i].source.vx = 0; // (rand() % 3) - 1; flames[i].source.vy = 0; @@ -8459,7 +8462,7 @@ uint16_t mode_particlefire(void) return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEFIRE[] PROGMEM = "Particle Fire@Speed,Intensity,Base Flames,Wind Speed, Color Mode, WrapX;;!;012;sx=100,ix=120,c1=16,c2=128,c3=0,o1=0"; +static const char _data_FX_MODE_PARTICLEFIRE[] PROGMEM = "Particle Fire@Speed,Intensity,Base Flames,Wind Speed, Color Scheme, WrapX;;!;012;sx=100,ix=120,c1=16,c2=128,c3=0,o1=0"; /*syntax for json configuration string: @A,B,C,D,E,F,G,H;I,J,K;L;M;N mark commas and semicolons A - speed diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index a58ef50870..914476a8e6 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -335,7 +335,7 @@ void Particle_Gravity_update(PSparticle *part, bool wrapX, bool bounceX, bool bo // 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 // saturation is color saturation, if not set to 255, hsv instead of palette is used (palette does not support saturation) -void ParticleSys_render(PSparticle *particles, uint16_t numParticles, bool wrapX, bool wrapY) +void ParticleSys_render(PSparticle *particles, uint16_t numParticles, bool wrapX, bool wrapY, bool fastcoloradd) { const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; @@ -351,6 +351,7 @@ void ParticleSys_render(PSparticle *particles, uint16_t numParticles, bool wrapX CRGB baseRGB; uint16_t i; uint8_t brightess; // particle brightness, fades if dying + // go over particles and update matrix cells on the way for (i = 0; i < numParticles; i++) @@ -388,7 +389,7 @@ void ParticleSys_render(PSparticle *particles, uint16_t numParticles, bool wrapX x--; // shift x to next pixel left, will overflow to 255 if 0 dx = dx + (PS_P_RADIUS >> 1); } - else // if jump has ocurred, fade pixel out + else // if jump has ocurred { dx = dx - (PS_P_RADIUS >> 1); // adjust dx so pixel fades } @@ -400,7 +401,6 @@ void ParticleSys_render(PSparticle *particles, uint16_t numParticles, bool wrapX } else { - // adjust dy so pixel fades dy = dy - (PS_P_RADIUS >> 1); } @@ -429,7 +429,7 @@ void ParticleSys_render(PSparticle *particles, uint16_t numParticles, bool wrapX // calculate the intensity with linear interpolation intensity = ((uint32_t)((PS_P_RADIUS)-dx) * ((PS_P_RADIUS)-dy) * (uint32_t)brightess) >> PS_P_SURFACE; // divide by PS_P_SURFACE to distribute the energy // scale the particle base color by the intensity and add it to the pixel - SEGMENT.addPixelColorXY(x, rows - y - 1, baseRGB.scale8(intensity)); + SEGMENT.addPixelColorXY(x, rows - y - 1, baseRGB.scale8(intensity), fastcoloradd); } // bottom right; x++; @@ -441,7 +441,7 @@ void ParticleSys_render(PSparticle *particles, uint16_t numParticles, bool wrapX if (x < cols && y < rows) { intensity = ((uint32_t)dx * ((PS_P_RADIUS)-dy) * (uint32_t)brightess) >> PS_P_SURFACE; // divide by PS_P_SURFACE to distribute the energy - SEGMENT.addPixelColorXY(x, rows - y - 1, baseRGB.scale8(intensity)); + SEGMENT.addPixelColorXY(x, rows - y - 1, baseRGB.scale8(intensity), fastcoloradd); } // top right y++; @@ -453,7 +453,7 @@ void ParticleSys_render(PSparticle *particles, uint16_t numParticles, bool wrapX if (x < cols && y < rows) { intensity = ((uint32_t)dx * dy * (uint32_t)brightess) >> PS_P_SURFACE; // divide by PS_P_SURFACE to distribute the energy - SEGMENT.addPixelColorXY(x, rows - y - 1, baseRGB.scale8(intensity)); + SEGMENT.addPixelColorXY(x, rows - y - 1, baseRGB.scale8(intensity), fastcoloradd); } // top left x--; @@ -467,7 +467,7 @@ void ParticleSys_render(PSparticle *particles, uint16_t numParticles, bool wrapX if (x < cols && y < rows) { intensity = ((uint32_t)((PS_P_RADIUS)-dx) * dy * (uint32_t)brightess) >> PS_P_SURFACE; // divide by PS_P_SURFACE to distribute the energy - SEGMENT.addPixelColorXY(x, rows - y - 1, baseRGB.scale8(intensity)); + SEGMENT.addPixelColorXY(x, rows - y - 1, baseRGB.scale8(intensity), fastcoloradd); } } } @@ -665,7 +665,7 @@ void PartMatrix_addHeat(uint8_t col, uint8_t row, uint16_t heat) // 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 red value to full red value, subtract it from the inital heat value so heat is the remaining heat not added yet + 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 diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index 695157f7a8..283824744b 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -74,7 +74,7 @@ void Particle_attractor(PSparticle *particle, PSparticle *attractor, uint8_t *co void Particle_Move_update(PSparticle *part); 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, uint16_t numParticles, bool wrapX, bool wrapY); +void ParticleSys_render(PSparticle *particles, uint16_t numParticles, bool wrapX, bool wrapY, bool fastcoloradd = false); void FireParticle_update(PSparticle *part, bool wrapX, bool WrapY); void ParticleSys_renderParticleFire(PSparticle *particles, uint16_t numParticles, bool wrapX); void PartMatrix_addHeat(uint8_t col, uint8_t row, uint16_t heat); From e945faf86e578a26c14130eec94dc43e170eb8ac Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Fri, 9 Feb 2024 20:30:26 +0100 Subject: [PATCH 017/219] collision detection is now a function plus some improvements & fixes --- wled00/FX.cpp | 175 +++++++----------------------------- wled00/FXparticleSystem.cpp | 39 ++++++++ wled00/FXparticleSystem.h | 3 +- 3 files changed, 74 insertions(+), 143 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 78a790f0ad..a6d965c3d5 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -8542,41 +8542,8 @@ uint16_t mode_particlefall(void) } } - uint8_t hardness = SEGMENT.custom2; // how hard the particle collisions are, if set to zero, no particle collision is calculated - - if (hardness > 0) - { - // detect and handle collisions - int16_t startparticle = 0; - int16_t endparticle = numParticles / 2; // do half the particles - - 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 which is overkill) - startparticle = endparticle; - endparticle = numParticles; - } - for (i = startparticle; i < endparticle; i++) - { - // go though all 'higher number' particles and see if any of those are in close proximity - // if they are, make them collide - if (particles[i].ttl > 0) // if particle is alive - { - int32_t dx, dy; // distance to other particles - for (j = i + 1; j < numParticles; j++) - { // check against higher number particles - if (particles[j].ttl > 0) - { // if target particle is alive - dx = particles[i].x - particles[j].x; - 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); - } - } - } - } - } - } + uint8_t hardness = SEGMENT.custom2; // how hard the collisions are, 255 = full hard. + detectCollisions(particles, numParticles, hardness); // now move the particles for (i = 0; i < numParticles; i++) @@ -8707,38 +8674,8 @@ uint16_t mode_particlepile(void) // detect and handle collisions - int16_t startparticle = 0; - int16_t endparticle = numParticles / 2; // do half the particles - - 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 which is overkill) - startparticle = endparticle; - endparticle = numParticles; - } - uint8_t hardness = ((SEGMENT.custom3) << 3) + 6; // how hard the collisions are, 255 = full hard. - // hardness = 255; - for (i = startparticle; i < endparticle; i++) - { - // go though all 'higher number' particles and see if any of those are in close proximity - // if they are, make them collide - if (particles[i].ttl > 0) // if particle is alive - { - int32_t dx, dy; // distance to other particles - for (j = i + 1; j < numParticles; j++) - { // check against higher number particles - if (particles[j].ttl > 0) - { // if target particle is alive - dx = particles[i].x - particles[j].x; - 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); - } - } - } - } - } + detectCollisions(particles, numParticles, hardness); // now move the particles for (i = 0; i < numParticles; i++) @@ -8867,39 +8804,9 @@ uint16_t mode_particlebox(void) } } - // detect and handle collisions - int16_t startparticle = 0; - int16_t endparticle = displayparticles >> 1; // do half the particles - - 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 which is overkill) - startparticle = endparticle; - endparticle = displayparticles; - } - uint8_t hardness = SEGMENT.custom2; // how hard the collisions are, 255 = full hard. - // hardness = 255; - for (i = startparticle; i < endparticle; i++) - { - // go though all 'higher number' particles and see if any of those are in close proximity - // if they are, make them collide - if (particles[i].ttl > 0) // if particle is alive - { - int32_t dx, dy; // distance to other particles - for (j = i + 1; j < displayparticles; j++) - { // check against higher number particles - if (particles[j].ttl > 0) - { // if target particle is alive - dx = particles[i].x - particles[j].x; - 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); - } - } - } - } - } + detectCollisions(particles, displayparticles, hardness); + // now move the particles for (i = 0; i < displayparticles; i++) @@ -8917,11 +8824,11 @@ uint16_t mode_particlebox(void) SEGMENT.fill(BLACK); // clear the matrix // render the particles - ParticleSys_render(particles, displayparticles, false, false); + ParticleSys_render(particles, displayparticles, false, false, SEGMENT.check2); return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEBOX[] PROGMEM = "Particle Box@Speed,Particles,Tilt strength,Bouncyness,,Rocking Boat;;!;012;pal=9,sx=100,ix=82,c1=190,c2=210,o1=0"; +static const char _data_FX_MODE_PARTICLEBOX[] PROGMEM = "Particle Box@Speed,Particles,Tilt strength,Bouncyness,,Rocking Boat,Fastcolors;;!;012;pal=9,sx=100,ix=82,c1=190,c2=210,o1=0"; /* perlin noise 'gravity' mapping as in particles on noise hills viewed from above @@ -9019,8 +8926,6 @@ static const char _data_FX_MODE_PARTICLEPERLIN[] PROGMEM = "Particle Perlin-Nois * by DedeHai (Damian Schneider) */ -// TODO: clean up the comments -//TODO: maybe add collisions? uint16_t mode_particleimpact(void) { @@ -9110,17 +9015,23 @@ uint16_t mode_particleimpact(void) } } - // update particles + //add collision if option is set + if(SEGMENT.check3) + { + uint8_t hardness = SEGMENT.custom2; // how hard the collisions are, 255 = full hard. + detectCollisions(particles, numParticles, hardness); + } + // update particles for (i = 0; i < numParticles; i++) { if (particles[i].ttl) { - Particle_Gravity_update(&particles[i], SEGMENT.check1, SEGMENT.check2, SEGMENT.check3, SEGMENT.custom2); + Particle_Gravity_update(&particles[i], SEGMENT.check1, SEGMENT.check2, true, SEGMENT.custom2); } } - - // update the meteors, set the speed state - for (i = 0; i < numMeteors; i++) + + // update the meteors, set the speed state + for (i = 0; i < numMeteors; i++) { Serial.print(meteors[i].source.vy); Serial.print(" "); @@ -9163,7 +9074,7 @@ uint16_t mode_particleimpact(void) return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEIMPACT[] PROGMEM = "Particle Impact@Launches,Explosion Size,Explosion Force,Bounce,Meteors,Wrap X,Bounce X,Bounce Y;;!;012;pal=8,sx=100,ix=50,c1=64,c2=128,c3=10,o1=0,o2=0,o3=1"; +static const char _data_FX_MODE_PARTICLEIMPACT[] PROGMEM = "Particle Impact@Launches,Explosion Size,Explosion Force,Bounce,Meteors,Wrap X,Bounce X,Collisions;;!;012;pal=8,sx=100,ix=50,c1=64,c2=128,c3=10,o1=0,o2=0,o3=1"; /* Particle Attractor, currently just a demo function for the particle attractor @@ -9232,44 +9143,14 @@ uint16_t mode_particleattractor(void) spray->var = 6; //emitting speed variation } - uint16_t displayparticles = SEGMENT.intensity-1; //cannot go to 255 particles, it will crash if set above 250, why is unclear... maybe array is too small? at 260 it will still crash, if numparticles are set to 265 it does not crash... + uint16_t displayparticles = SEGMENT.intensity; //cannot go to 255 particles, it will crash if set above 250, why is unclear... maybe array is too small? at 260 it will still crash, if numparticles are set to 265 it does not crash... uint8_t hardness = SEGMENT.custom2; // how hard the collisions are, 255 = full hard. i = 0; j = 0; if (hardness > 1) // enable collisions { - // detect and handle collisions - int16_t startparticle = 0; - int16_t endparticle = displayparticles >> 1; // do half the particles - - 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 which is overkill) - startparticle = endparticle; - endparticle = displayparticles; - } - - for (i = startparticle; i < endparticle; i++) - { - // go though all 'higher number' particles and see if any of those are in close proximity - // if they are, make them collide - if (particles[i].ttl > 0 && particles[i].outofbounds==0) // if particle is alive and on screen - { - int32_t dx, dy; // distance to other particles - for (j = i + 1; j < displayparticles; j++) - { // check against higher number particles - if (particles[j].ttl > 0) - { // if target particle is alive - dx = particles[i].x - particles[j].x; - 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); - } - } - } - } - } + detectCollisions(particles, displayparticles, hardness); } @@ -9289,7 +9170,17 @@ uint16_t mode_particleattractor(void) if (particles[i].ttl == 0 && emit--) // find a dead particle { - Emitter_Fountain_emit(spray, &particles[i]); + Emitter_Fountain_emit(spray, &particles[i]); //emit one if available + } + + // every now and then, apply 'air friction' to smooth things out, slows down all particles a little + if (SEGMENT.custom3 > 0) + { + if (SEGMENT.call % (32 - SEGMENT.custom3) == 0) + { + if (SEGMENT.check2) + applyFriction(&particles[i], 1); + } } Particle_attractor(&particles[i], attractor, &counters[i], SEGMENT.speed, SEGMENT.check3); @@ -9305,7 +9196,7 @@ uint16_t mode_particleattractor(void) return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEATTRACTOR[] PROGMEM = "Particle Attractor@Center Mass,Particles,,Collision Strength,,,,Swallow;;!;012;pal=9,sx=100,ix=82,c1=190,c2=210,o1=0,o2=0,o3=0"; +static const char _data_FX_MODE_PARTICLEATTRACTOR[] PROGMEM = "Particle Attractor@Center Mass,Particles,,Collision Strength,Friction,,,Swallow;;!;012;pal=9,sx=100,ix=82,c1=190,c2=210,o1=0,o2=0,o3=0"; #endif // WLED_DISABLE_2D diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index 914476a8e6..661bf2ecbe 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -712,6 +712,45 @@ void PartMatrix_addHeat(uint8_t col, uint8_t row, uint16_t heat) SEGMENT.setPixelColorXY(col, rows - row - 1, currentcolor); } +/*detect collisions in an array of particles and handle them*/ +void detectCollisions(PSparticle* particles, uint16_t numparticles, uint8_t hardness) +{ + // detect and handle collisions + uint16_t i,j; + int16_t startparticle = 0; + int16_t endparticle = numparticles >> 1; // do half the particles + + 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 which is overkill) + startparticle = endparticle; + endparticle = numparticles; + } + + for (i = startparticle; i < endparticle; i++) + { + // go though all 'higher number' particles and see if any of those are in close proximity + // if they are, make them collide + if (particles[i].ttl > 0) // if particle is alive + { + int32_t dx, dy; // distance to other particles + for (j = i + 1; j < numparticles; 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 + { + 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); + } + } + } + } + } + } +} // 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) void handleCollision(PSparticle *particle1, PSparticle *particle2, const uint8_t hardness) diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index 283824744b..3de4cb69f9 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -31,7 +31,7 @@ //particle dimensions (subpixel division) #define PS_P_RADIUS 64 //subpixel size, each pixel is divided by this for particle movement -#define PS_P_HARDRADIUS 92 //hard surface radius of a particle, in collisions, this is forbidden to be entered by another particle (for stacking) +#define PS_P_HARDRADIUS 100 //hard surface radius of a particle, used for collision detection proximity,also this is forbidden to be entered by another particle (for stacking) #define PS_P_SURFACE 12 //shift: 2^PS_P_SURFACE = (PS_P_RADIUS)^2 @@ -78,5 +78,6 @@ void ParticleSys_render(PSparticle *particles, uint16_t numParticles, bool wrapX void FireParticle_update(PSparticle *part, bool wrapX, bool WrapY); void ParticleSys_renderParticleFire(PSparticle *particles, uint16_t numParticles, bool wrapX); void PartMatrix_addHeat(uint8_t col, uint8_t row, uint16_t heat); +void detectCollisions(PSparticle *particles, uint16_t numparticles, uint8_t hardness); void handleCollision(PSparticle *particle1, PSparticle *particle2, const uint8_t hardness); void applyFriction(PSparticle *particle, uint8_t coefficient); From f1ffbe0cf77866df42482806f85305a15d4ccefd Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sat, 10 Feb 2024 07:35:45 +0100 Subject: [PATCH 018/219] improved collision efficiency improved efficiency for stackup (pushback), added code to correctly determine direction if particles meed (probably overkill but now its there) --- wled00/FXparticleSystem.cpp | 74 ++++++++++++++++++++++--------------- 1 file changed, 44 insertions(+), 30 deletions(-) diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index 661bf2ecbe..dded3cc7b2 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -758,17 +758,32 @@ void handleCollision(PSparticle *particle1, PSparticle *particle2, const uint8_t int16_t dx = particle2->x - particle1->x; int16_t dy = particle2->y - particle1->y; - int32_t distanceSquared = dx * dx + dy * dy + 1; //+1 so it is never zero to avoid division by zero below - if (distanceSquared == 0) // add 'noise' in case particles exactly meet at center, prevents dotProduct=0 (this can only happen if they move towards each other) - { - dx++; // - distanceSquared++; - } + int32_t distanceSquared = dx * dx + dy * dy; // Calculate relative velocity int16_t relativeVx = (int16_t)particle2->vx - (int16_t)particle1->vx; int16_t relativeVy = (int16_t)particle2->vy - (int16_t)particle1->vy; + if (distanceSquared == 0) // add distance in case particles exactly meet at center, prevents dotProduct=0 (this can only happen if they move towards each other) + { + // Adjust positions based on relative velocity direction + if (relativeVx < 0) { //if true, particle2 is on the right side + particle1->x--; + particle2->x++; + } else{ + particle1->x++; + particle2->x--; + } + + if (relativeVy < 0) { + particle1->y--; + particle2->y++; + } else{ + particle1->y++; + particle2->y--; + } + distanceSquared++; + } // Calculate dot product of relative velocity and relative distance int32_t dotProduct = (dx * relativeVx + dy * relativeVy); @@ -798,35 +813,34 @@ void handleCollision(PSparticle *particle1, PSparticle *particle2, const uint8_t } // particles have volume, push particles apart if they are too close by moving each particle by a fixed amount away from the other particle - // move each particle by half of the amount they are overlapping, assumes square particles - - if (dx < 2 * PS_P_HARDRADIUS && dx > -2 * PS_P_HARDRADIUS) - { // distance is too small - int8_t push = 1; - if (dx < 0) // dx is negative - { - push = -push; // invert push direction + + int8_t push; + + if (distanceSquared < (2 * PS_P_HARDRADIUS) * (2 * PS_P_HARDRADIUS)) + { + if (dx < 2 * PS_P_HARDRADIUS && dx > -2 * PS_P_HARDRADIUS) + { // distance is too small + push = 1; + if (dx < 0) // dx is negative + { + push = -push; // invert push direction + } + particle1->x -= push; + particle2->x += push; } - particle1->x -= push; - particle2->x += push; - } - if (dy < 2 * PS_P_HARDRADIUS && dy > -2 * PS_P_HARDRADIUS) - { // distance is too small (or negative) - int8_t push = 1; - if (dy < 0) // dy is negative - { - push = -push; // invert push direction + if (dy < 2 * PS_P_HARDRADIUS && dy > -2 * PS_P_HARDRADIUS) + { // distance is too small (or negative) + push = 1; + if (dy < 0) // dy is negative + { + push = -push; // invert push direction + } + particle1->y -= push; + particle2->y += push; } - - particle1->y -= push; - particle2->y += push; } } - // particles are close, apply friction -> makes them slow down in mid air, is not a good idea to apply here - // const uint8_t frictioncoefficient=4; - // applyFriction(particle1, frictioncoefficient); - // applyFriction(particle2, frictioncoefficient); } // slow down particle by friction, the higher the speed, the higher the friction From 8fe044ee69161b4854cb71a69129bc07155ada3e Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sat, 10 Feb 2024 08:14:34 +0100 Subject: [PATCH 019/219] added fix for piling oscillations untested, need to verify it works --- wled00/FXparticleSystem.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index dded3cc7b2..46e85877ea 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -820,7 +820,7 @@ void handleCollision(PSparticle *particle1, PSparticle *particle2, const uint8_t { if (dx < 2 * PS_P_HARDRADIUS && dx > -2 * PS_P_HARDRADIUS) { // distance is too small - push = 1; + push = 1+random8(3); //make push distance a little random to avoid oscillations if (dx < 0) // dx is negative { push = -push; // invert push direction @@ -830,7 +830,7 @@ void handleCollision(PSparticle *particle1, PSparticle *particle2, const uint8_t } if (dy < 2 * PS_P_HARDRADIUS && dy > -2 * PS_P_HARDRADIUS) { // distance is too small (or negative) - push = 1; + push = 1+random8(3); if (dy < 0) // dy is negative { push = -push; // invert push direction From da94d31990a4a7284ed2c7c99fc7eb98023c27b6 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sat, 10 Feb 2024 11:32:07 +0100 Subject: [PATCH 020/219] Improved collision handling (faster, less oscillations), changed variables to 32bit for faster calculation 32bit variables are faster on ESP32, so use them whenever a variable is used a lot, it saves one instruction per access. --- wled00/FX.cpp | 74 +++++++++++------------ wled00/FXparticleSystem.cpp | 113 ++++++++++++++++++------------------ wled00/FXparticleSystem.h | 9 ++- 3 files changed, 101 insertions(+), 95 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index a6d965c3d5..0be46bcc3d 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -7890,7 +7890,7 @@ uint16_t mode_particlerotatingspray(void) const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; const uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); - const uint16_t numParticles = 400; + const uint32_t numParticles = 400; const uint8_t numSprays = 8; // maximum number of sprays PSparticle *particles; @@ -7906,8 +7906,8 @@ uint16_t mode_particlerotatingspray(void) // 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 - uint16_t i = 0; - uint16_t j = 0; + uint32_t i = 0; + uint32_t j = 0; uint8_t spraycount = 1 + (SEGMENT.custom2 >> 5); // number of sprays to display, 1-8 if (SEGMENT.call == 0) // initialization @@ -8008,10 +8008,10 @@ uint16_t mode_particlefireworks(void) const uint16_t PS_MAX_Y(rows * PS_P_RADIUS - 1); #ifdef ESP8266 - const uint16_t numParticles = 250; + const uint32_t numParticles = 250; const uint8_t MaxNumRockets = 4; #else - const uint16_t numParticles = 650; + const uint32_t numParticles = 650; const uint8_t MaxNumRockets = 8; #endif @@ -8032,8 +8032,8 @@ uint16_t mode_particlefireworks(void) // calculate the end of the spray data and assign it as the data pointer for the particles: particles = reinterpret_cast(rockets + MaxNumRockets); // cast the data array into a particle pointer - uint16_t i = 0; - uint16_t j = 0; + uint32_t i = 0; + uint32_t j = 0; uint8_t numRockets = 1+ ((SEGMENT.custom3) >> 2); //1 to 8 if (SEGMENT.call == 0) // initialization @@ -8185,7 +8185,7 @@ uint16_t mode_particlespray(void) const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; const uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); - const uint16_t numParticles = 450; + const uint32_t numParticles = 450; const uint8_t numSprays = 1; uint8_t percycle = numSprays; // maximum number of particles emitted per cycle @@ -8205,8 +8205,8 @@ uint16_t mode_particlespray(void) // 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 - uint16_t i = 0; - uint16_t j = 0; + uint32_t i = 0; + uint32_t j = 0; if (SEGMENT.call == 0) // initialization { @@ -8330,8 +8330,8 @@ uint16_t mode_particlefire(void) const uint16_t PS_MAX_X(cols * PS_P_RADIUS - 1); const uint16_t PS_MAX_Y(rows * PS_P_RADIUS - 1); - const uint16_t numFlames = (cols << 1); // number of flames: depends on fire width. for a fire width of 16 pixels, about 25-30 flames give good results, add a few for the base flames - const uint16_t numParticles = numFlames * 25; + const uint32_t numFlames = (cols << 1); // number of flames: depends on fire width. for a fire width of 16 pixels, about 25-30 flames give good results, add a few for the base flames + const uint32_t numParticles = numFlames * 25; uint8_t percycle = numFlames >> 1; // maximum number of particles emitted per cycle PSparticle *particles; PSpointsource *flames; @@ -8353,7 +8353,7 @@ uint16_t mode_particlefire(void) // 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 - uint16_t i; + uint32_t i; if (SEGMENT.call == 0) // initialization { @@ -8494,7 +8494,7 @@ uint16_t mode_particlefall(void) const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; const uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); - const uint16_t numParticles = 500; + const uint32_t numParticles = 500; PSparticle *particles; @@ -8506,8 +8506,8 @@ uint16_t mode_particlefall(void) // calculate the end of the spray data and assign it as the data pointer for the particles: particles = reinterpret_cast(SEGENV.data); // cast the data array into a particle pointer - uint16_t i = 0; - uint16_t j = 0; + uint32_t i = 0; + uint32_t j = 0; if (SEGMENT.call == 0) // initialization { @@ -8582,7 +8582,7 @@ uint16_t mode_particlepile(void) const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; const uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); - const uint16_t numParticles = 400; + const uint32_t numParticles = 400; const uint8_t numSprays = 1; uint8_t percycle = numSprays; // maximum number of particles emitted per cycle @@ -8599,8 +8599,8 @@ uint16_t mode_particlepile(void) // 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 - uint16_t i = 0; - uint16_t j = 0; + uint32_t i = 0; + uint32_t j = 0; if (SEGMENT.call == 0) // initialization { @@ -8711,7 +8711,7 @@ uint16_t mode_particlebox(void) const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; const uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); - const uint16_t numParticles = 255; // maximum number of particles + const uint32_t numParticles = 255; // maximum number of particles PSparticle *particles; @@ -8721,8 +8721,8 @@ uint16_t mode_particlebox(void) return mode_static(); // allocation failed; //allocation failed particles = reinterpret_cast(SEGENV.data); // cast the data array into a particle pointer - uint16_t i = 0; - uint16_t j = 0; + uint32_t i = 0; + uint32_t j = 0; if (SEGMENT.call == 0) // initialization { @@ -8828,7 +8828,7 @@ uint16_t mode_particlebox(void) return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEBOX[] PROGMEM = "Particle Box@Speed,Particles,Tilt strength,Bouncyness,,Rocking Boat,Fastcolors;;!;012;pal=9,sx=100,ix=82,c1=190,c2=210,o1=0"; +static const char _data_FX_MODE_PARTICLEBOX[] PROGMEM = "Particle Box@Speed,Particles,Tilt strength,Bouncyness,,Rocking Boat,Fastcolors;;!;012;pal=1,sx=100,ix=82,c1=190,c2=210,o1=0"; /* perlin noise 'gravity' mapping as in particles on noise hills viewed from above @@ -8847,7 +8847,7 @@ uint16_t mode_particleperlin(void) const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; const uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); - const uint16_t numParticles = 255; + const uint32_t numParticles = 255; PSparticle *particles; @@ -8857,8 +8857,8 @@ uint16_t mode_particleperlin(void) return mode_static(); // allocation failed; //allocation failed particles = reinterpret_cast(SEGENV.data); // cast the data array into a particle pointer - uint16_t i = 0; - uint16_t j = 0; + uint32_t i = 0; + uint32_t j = 0; if (SEGMENT.call == 0) // initialization { @@ -8872,7 +8872,7 @@ uint16_t mode_particleperlin(void) } } - uint16_t displayparticles = SEGMENT.intensity; + uint32_t displayparticles = SEGMENT.intensity; // apply 'gravity' from a 2D perlin noise map SEGMENT.aux0 += SEGMENT.speed >> 4; // noise z-position; @@ -8940,10 +8940,10 @@ uint16_t mode_particleimpact(void) const uint16_t PS_MAX_Y(rows * PS_P_RADIUS - 1); #ifdef ESP8266 - const uint16_t numParticles = 250; - const uint8_t MaxNumRockets = 4; + const uint32_t numParticles = 250; + const uint8_t MaxNumMeteors = 4; #else - const uint16_t numParticles = 550; + const uint32_t numParticles = 550; const uint8_t MaxNumMeteors = 8; #endif @@ -8960,8 +8960,8 @@ uint16_t mode_particleimpact(void) // 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 - uint16_t i = 0; - uint16_t j = 0; + uint32_t i = 0; + uint32_t j = 0; uint8_t numMeteors = map(SEGMENT.custom3, 0, 31, 1, MaxNumMeteors); // number of meteors to use for animation if (SEGMENT.call == 0) // initialization @@ -8981,7 +8981,7 @@ uint16_t mode_particleimpact(void) // update particles, create particles // check each rocket's state and emit particles according to its state: moving up = emit exhaust, at top = explode; falling down = standby time - uint16_t emitparticles; // number of particles to emit for each rocket's state + uint32_t emitparticles; // number of particles to emit for each rocket's state i = 0; for (j = 0; j < numMeteors; j++) { @@ -9031,7 +9031,7 @@ uint16_t mode_particleimpact(void) } // update the meteors, set the speed state - for (i = 0; i < numMeteors; i++) + for (i = 0; i < numMeteors; i++) { Serial.print(meteors[i].source.vy); Serial.print(" "); @@ -9093,7 +9093,7 @@ uint16_t mode_particleattractor(void) const uint16_t PS_MAX_X(cols * PS_P_RADIUS - 1); const uint16_t PS_MAX_Y(rows * PS_P_RADIUS - 1); - const uint16_t numParticles = 265; // maximum number of particles + const uint32_t numParticles = 265; // maximum number of particles PSparticle *particles; PSparticle *attractor; @@ -9114,8 +9114,8 @@ uint16_t mode_particleattractor(void) spray = reinterpret_cast(attractor + 1); counters = reinterpret_cast(spray + 1); - uint16_t i = 0; - uint16_t j = 0; + uint32_t i = 0; + uint32_t j = 0; if (SEGMENT.call == 0) // initialization { diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index 46e85877ea..5112fa908e 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -26,6 +26,12 @@ */ +/* +Note: on ESP32 using 32bit integer is faster than 16bit or 8bit, each operation takes on less instruction, can be testen on https://godbolt.org/ + it does not matter if using int, unsigned int, uint32_t or int32_t, the compiler will make int into 32bit + this should be used to optimize speed but not if memory is affected much +*/ + #include "FXparticleSystem.h" #include "wled.h" #include "FastLED.h" @@ -60,20 +66,14 @@ void Emitter_Angle_emit(PSpointsource *emitter, PSparticle *part, uint8_t angle, { 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; - Serial.print(angle); - Serial.print(" "); - Serial.print(emitter->vx); - Serial.print(" "); - Serial.print(emitter->vy); - Serial.print(" "); Emitter_Fountain_emit(emitter, part); } // 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) // todo: add a parameter 'swallow' so the attractor can 'suck up' particles that are very close, also could use hue of attractor particle for strength { // Calculate the distance between the particle and the attractor - int16_t dx = attractor->x - particle->x; - int16_t dy = attractor->y - particle->y; + int dx = attractor->x - particle->x; + int dy = attractor->y - particle->y; // Calculate the force based on inverse square law int32_t distanceSquared = dx * dx + dy * dy + 1; @@ -84,7 +84,7 @@ void Particle_attractor(PSparticle *particle, PSparticle *attractor, uint8_t *co particle->ttl = 0; return; } - distanceSquared = 4096; // limit the distance to 64 (=size of a particle) to avoid very high forces.TODO: could make this depending on the #define for particle size + distanceSquared = PS_P_RADIUS * PS_P_RADIUS + PS_P_RADIUS * PS_P_RADIUS; // limit the distance of particle size to avoid very high forces } // check if distance is small enough to even cause a force (for that strength<<10 must be bigger than the distance squared) int32_t shiftedstrength = (int32_t)strength << 16; @@ -319,9 +319,8 @@ void Particle_Gravity_update(PSparticle *part, bool wrapX, bool bounceX, bool bo if (bounceY) { 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 - part->y += (int16_t)part->vy; // move particle back to within boundaries so it does not disappear for one frame - newY = max(newY, (int16_t)0); // limit to positive + part->vy = (((int16_t)part->vy) * (int16_t)hardness) >> 8; // reduce speed as energy is lost on non-hard surface + newY = max(newY, (int16_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 } else // not bouncing and out of matrix @@ -335,7 +334,7 @@ void Particle_Gravity_update(PSparticle *part, bool wrapX, bool bounceX, bool bo // 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 // saturation is color saturation, if not set to 255, hsv instead of palette is used (palette does not support saturation) -void ParticleSys_render(PSparticle *particles, uint16_t numParticles, bool wrapX, bool wrapY, bool fastcoloradd) +void ParticleSys_render(PSparticle *particles, uint32_t numParticles, bool wrapX, bool wrapY, bool fastcoloradd) { const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; @@ -349,7 +348,7 @@ void ParticleSys_render(PSparticle *particles, uint16_t numParticles, bool wrapX uint8_t dx, dy; uint32_t intensity; // todo: can this be an uint8_t or will it mess things up? CRGB baseRGB; - uint16_t i; + uint32_t i; uint8_t brightess; // particle brightness, fades if dying @@ -527,20 +526,17 @@ void FireParticle_update(PSparticle *part, bool wrapX = false, bool wrapY = fals // render simple 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, uint16_t numParticles, bool wrapX) +void ParticleSys_renderParticleFire(PSparticle *particles, uint32_t numParticles, bool wrapX) { 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); - int16_t x, y; + int32_t x, y; uint8_t dx, dy; uint32_t tempVal; - uint16_t i; + uint32_t i; // go over particles and update matrix cells on the way for (i = 0; i < numParticles; i++) @@ -713,12 +709,12 @@ void PartMatrix_addHeat(uint8_t col, uint8_t row, uint16_t heat) } /*detect collisions in an array of particles and handle them*/ -void detectCollisions(PSparticle* particles, uint16_t numparticles, uint8_t hardness) +void detectCollisions(PSparticle* particles, uint32_t numparticles, uint8_t hardness) { // detect and handle collisions - uint16_t i,j; - int16_t startparticle = 0; - int16_t endparticle = numparticles >> 1; // do half the particles + uint32_t i,j; + int32_t startparticle = 0; + int32_t endparticle = numparticles >> 1; // do half the particles 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 which is overkill) @@ -756,13 +752,13 @@ void detectCollisions(PSparticle* particles, uint16_t numparticles, uint8_t hard void handleCollision(PSparticle *particle1, PSparticle *particle2, const uint8_t hardness) { - int16_t dx = particle2->x - particle1->x; - int16_t dy = particle2->y - particle1->y; + int32_t dx = particle2->x - particle1->x; + int32_t dy = particle2->y - particle1->y; int32_t distanceSquared = dx * dx + dy * dy; // Calculate relative velocity - int16_t relativeVx = (int16_t)particle2->vx - (int16_t)particle1->vx; - int16_t relativeVy = (int16_t)particle2->vy - (int16_t)particle1->vy; + int32_t relativeVx = (int16_t)particle2->vx - (int16_t)particle1->vx; + int32_t relativeVy = (int16_t)particle2->vy - (int16_t)particle1->vy; if (distanceSquared == 0) // add distance in case particles exactly meet at center, prevents dotProduct=0 (this can only happen if they move towards each other) { @@ -800,7 +796,7 @@ void handleCollision(PSparticle *particle1, PSparticle *particle2, const uint8_t particle2->vx -= (impulse * dx) >> bitshift; particle2->vy -= (impulse * dy) >> bitshift; - if (hardness < 150) // if particles are soft, they become 'sticky' i.e. no slow movements + if (hardness < 50) // if particles are soft, they become 'sticky' i.e. no slow movements { if (particle1->vx < 2 && particle1->vx > -2) particle1->vx = 0; @@ -811,35 +807,42 @@ void handleCollision(PSparticle *particle1, PSparticle *particle2, const uint8_t if (particle2->vy < 2 && particle1->vy > -2) particle1->vy = 0; } + } - // particles have volume, push particles apart if they are too close by moving each particle by a fixed amount away from the other particle - - int8_t push; - - if (distanceSquared < (2 * PS_P_HARDRADIUS) * (2 * PS_P_HARDRADIUS)) - { - if (dx < 2 * PS_P_HARDRADIUS && dx > -2 * PS_P_HARDRADIUS) - { // distance is too small - push = 1+random8(3); //make push distance a little random to avoid oscillations - if (dx < 0) // dx is negative - { - push = -push; // invert push direction - } - particle1->x -= push; - particle2->x += push; - } - if (dy < 2 * PS_P_HARDRADIUS && dy > -2 * PS_P_HARDRADIUS) - { // distance is too small (or negative) - push = 1+random8(3); - if (dy < 0) // dy is negative - { - push = -push; // invert push direction - } - particle1->y -= push; - particle2->y += push; - } + // particles have volume, push particles apart if they are too close by moving each particle by a fixed amount away from the other particle + // if pushing is made dependent on hardness, things start to oscillate much more, better to just add a fixed, small increment (tried lots of configurations, this one works best) + // one problem remaining is, particles get squished if (external) force applied is higher than the pushback but this may also be desirable if particles are soft. also some oscillations cannot be avoided without addigng a counter + if (distanceSquared < 2 * PS_P_HARDRADIUS * PS_P_HARDRADIUS) + { + int32_t push; + //uint8_t rndchoice = random8(2); + const uint32_t HARDDIAMETER = PS_P_HARDRADIUS <<1; + if (dx < HARDDIAMETER && dx > -HARDDIAMETER) + { // distance is too small, push them apart + push = 1; + if (dx < 0) // dx is negative + push =-push; // negative push direction + + //if (rndchoice) // randomly chose one of the particles to push, avoids oscillations + // particle1->x -= push; + //else + particle2->x += push; //only push one particle to avoid oscillations } + if (dy < HARDDIAMETER && dy > -HARDDIAMETER) + { // distance is too small (or negative) + push = 1; + if (dy < 0) // dy is negative + push = -push;//negative push direction + + + //if (rndchoice) // randomly chose one of the particles to push, avoids oscillations + // particle1->y -= push; + //else + particle2->y += push; // only push one particle to avoid oscillations + } + //note: pushing may push particles out of frame, if bounce is active, it will move it back as position will be limited to within frame } + } diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index 3de4cb69f9..80be538a70 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -35,6 +35,9 @@ #define PS_P_SURFACE 12 //shift: 2^PS_P_SURFACE = (PS_P_RADIUS)^2 +//todo: can add bitfields to add in more stuff +//but when using bit fields, computation time increases as instructions are needed to mask the fields, only do it with variables that do not get updated too often + //struct for a single particle typedef struct { int16_t x; //x position in particle system @@ -74,10 +77,10 @@ void Particle_attractor(PSparticle *particle, PSparticle *attractor, uint8_t *co void Particle_Move_update(PSparticle *part); 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, uint16_t numParticles, bool wrapX, bool wrapY, bool fastcoloradd = false); +void ParticleSys_render(PSparticle *particles, uint32_t numParticles, bool wrapX, bool wrapY, bool fastcoloradd = false); void FireParticle_update(PSparticle *part, bool wrapX, bool WrapY); -void ParticleSys_renderParticleFire(PSparticle *particles, uint16_t numParticles, bool wrapX); +void ParticleSys_renderParticleFire(PSparticle *particles, uint32_t numParticles, bool wrapX); void PartMatrix_addHeat(uint8_t col, uint8_t row, uint16_t heat); -void detectCollisions(PSparticle *particles, uint16_t numparticles, uint8_t hardness); +void detectCollisions(PSparticle *particles, uint32_t numparticles, uint8_t hardness); void handleCollision(PSparticle *particle1, PSparticle *particle2, const uint8_t hardness); void applyFriction(PSparticle *particle, uint8_t coefficient); From 7c49f886419c0232118c6abc4dcdc70309f94bf9 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sat, 10 Feb 2024 13:32:23 +0100 Subject: [PATCH 021/219] removed option for fastcolor add it made very little difference in performance, but for ESP8266 it may matter so it is set permanently there. graphically the difference is also very small (sometimes a particle gets brighter or less saturated) --- wled00/FX.cpp | 4 ++-- wled00/FXparticleSystem.cpp | 10 ++++++++-- wled00/FXparticleSystem.h | 2 +- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 0be46bcc3d..b2d6e0d22d 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -8824,11 +8824,11 @@ uint16_t mode_particlebox(void) SEGMENT.fill(BLACK); // clear the matrix // render the particles - ParticleSys_render(particles, displayparticles, false, false, SEGMENT.check2); + ParticleSys_render(particles, displayparticles, false, false); return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEBOX[] PROGMEM = "Particle Box@Speed,Particles,Tilt strength,Bouncyness,,Rocking Boat,Fastcolors;;!;012;pal=1,sx=100,ix=82,c1=190,c2=210,o1=0"; +static const char _data_FX_MODE_PARTICLEBOX[] PROGMEM = "Particle Box@Speed,Particles,Tilt strength,Bouncyness,,Rocking Boat,;;!;012;pal=1,sx=100,ix=82,c1=190,c2=210,o1=0"; /* perlin noise 'gravity' mapping as in particles on noise hills viewed from above diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index 5112fa908e..fed38d1fdd 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -334,9 +334,15 @@ void Particle_Gravity_update(PSparticle *part, bool wrapX, bool bounceX, bool bo // 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 // saturation is color saturation, if not set to 255, hsv instead of palette is used (palette does not support saturation) -void ParticleSys_render(PSparticle *particles, uint32_t numParticles, bool wrapX, bool wrapY, bool fastcoloradd) +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 uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; const uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index 80be538a70..78acf52c51 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -77,7 +77,7 @@ void Particle_attractor(PSparticle *particle, PSparticle *attractor, uint8_t *co void Particle_Move_update(PSparticle *part); 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, bool fastcoloradd = false); +void ParticleSys_render(PSparticle *particles, uint32_t numParticles, bool wrapX, bool wrapY); void FireParticle_update(PSparticle *part, bool wrapX, bool WrapY); void ParticleSys_renderParticleFire(PSparticle *particles, uint32_t numParticles, bool wrapX); void PartMatrix_addHeat(uint8_t col, uint8_t row, uint16_t heat); From dc5c58e98a0d8ec5dd753b8d8c3c0f9f444d90db Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sat, 10 Feb 2024 18:25:37 +0100 Subject: [PATCH 022/219] Fixed some bugs in particle system, runs much smoother now also tweaked some of the FX --- wled00/FX.cpp | 93 +++++++++++++++++++++++-------------- wled00/FXparticleSystem.cpp | 69 +++++++++++++-------------- wled00/FXparticleSystem.h | 29 ++++++++---- 3 files changed, 113 insertions(+), 78 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index b2d6e0d22d..36eecb3095 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -8004,8 +8004,8 @@ uint16_t mode_particlefireworks(void) const uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); // particle system box dimensions - const uint16_t PS_MAX_X(cols * PS_P_RADIUS - 1); - const uint16_t PS_MAX_Y(rows * PS_P_RADIUS - 1); + const uint16_t PS_MAX_X=(cols * PS_P_RADIUS - 1); + const uint16_t PS_MAX_Y=(rows * PS_P_RADIUS - 1); #ifdef ESP8266 const uint32_t numParticles = 250; @@ -8170,7 +8170,7 @@ uint16_t mode_particlefireworks(void) static const char _data_FX_MODE_PARTICLEFIREWORKS[] PROGMEM = "Particle Fireworks@Launches,Explosion Size,Height,Bounce,Rockets,Wrap X,Bounce X,Bounce Y;;!;012;pal=11,sx=100,ix=50,c1=64,c2=128,c3=10,o1=0,o2=0,o3=1"; /* - * Particle gravity spray + * Particle Volcano (gravity spray) * Particles are sprayed from below, spray moves back and forth * Uses palette for particle color * by DedeHai (Damian Schneider) @@ -8184,6 +8184,8 @@ uint16_t mode_particlespray(void) const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; const uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); + //particle system x dimension + const uint16_t PS_MAX_X = (cols * PS_P_RADIUS - 1); const uint32_t numParticles = 450; const uint8_t numSprays = 1; @@ -8218,11 +8220,12 @@ uint16_t mode_particlespray(void) { spray[i].source.hue = random8(); spray[i].source.sat = 255; // set full saturation - spray[i].source.x = 4 * PS_P_RADIUS * (i + 1); + spray[i].source.x = (cols * PS_P_RADIUS)/(numSprays+1) * (i + 1); spray[i].source.y = 5; // just above the lower edge, if zero, particles already 'bounce' at start and loose speed. - spray[i].source.vx = 10; + spray[i].source.vx = 0; spray[i].maxLife = 300; // lifetime in frames - spray[i].minLife = 30; + spray[i].minLife = 20; + spray[i].source.collide = true; //seeded particles will collide spray[i].vx = 0; // emitting speed spray[i].vy = 20; // emitting speed // spray.var = 10 + (random8() % 4); @@ -8236,33 +8239,35 @@ uint16_t mode_particlespray(void) for (i = 0; i < numSprays; i++) { spray[i].source.hue++; // = random8(); //change hue of spray source - } - - for (i = 0; i < numSprays; i++) - { // percycle = 1+(SEGMENT.intensity>>4); //how many particles are sprayed per cycle and how fast ist the color changing - if (spray[i].source.vx > 0) // moving to the right currently - { - spray[i].source.vx = SEGMENT.speed >> 4; // spray speed + if(SEGMENT.check2) //bounce + { + if (spray[i].source.vx > 0) // moving to the right currently + { + spray[i].source.vx = SEGMENT.speed >> 4; // spray speed + } + else + { + spray[i].source.vx = -(SEGMENT.speed >> 4); // spray speed (is currently moving negative so keep it negative) + } } - else - { - spray[i].source.vx = -(SEGMENT.speed >> 4); // spray speed (is currently moving negative so keep it negative) + else{ //wrap on the right side + spray[i].source.vx = SEGMENT.speed >> 4; // spray speed + if (spray[i].source.x >= PS_MAX_X-32) spray[i].source.x = 1; //wrap if close to border (need to wrap before the bounce updated detects a border collision or it will just be stuck) } - spray[i].vy = SEGMENT.custom1 >> 3; // emitting speed, upward - spray[i].vx = ((int16_t)SEGMENT.custom2 - (int16_t)127) / 8; // emitting speed, left/right (=angle) - spray[i].var = SEGMENT.custom3 >> 1; // emiting variation = nozzle size (custom 3 goes from 0-32) - spray[i].source.y = spray[i].var + 1; // need to 'lift up' the source as 'var' also changes particle spawn position randomly + spray[i].vy = SEGMENT.custom1 >> 2; // emitting speed, upward + spray[i].vx = 0; + spray[i].var = 0;//!!!SEGMENT.custom3; // emiting variation = nozzle size (custom 3 goes from 0-32) + //spray[i].source.y = spray[i].var + 1; // need to 'lift up' the source as 'var' also changes particle spawn position randomly spray[i].source.ttl = 255; // source never dies, replenish its lifespan } i = 0; j = 0; - while (i < numParticles) + for (i = 0; i < numParticles; i++) { if (particles[i].ttl == 0) // find a dead particle - { - // ColorFromPalette(SEGPALETTE, random8() , 255, LINEARBLEND); + { // spray[j].source.hue = random8(); //set random color for each particle (using palette) Emitter_Fountain_emit(&spray[j], &particles[i]); j = (j + 1) % numSprays; @@ -8271,9 +8276,11 @@ uint16_t mode_particlespray(void) break; // quit loop if all particles of this round emitted } } - i++; } } + uint8_t hardness = SEGMENT.custom2; + if (SEGMENT.check3)//collisions enabled + detectCollisions(particles, numParticles, hardness); for (i = 0; i < numSprays; i++) { @@ -8283,19 +8290,23 @@ uint16_t mode_particlespray(void) for (i = 0; i < numParticles; i++) { // Particle_Move_update(&particles[i]); //move the particles - Particle_Gravity_update(&particles[i], SEGMENT.check1, SEGMENT.check2, SEGMENT.check3, (uint8_t)200); + //set color according to ttl ('color by age') + if (SEGMENT.check1) + particles[i].hue = min((uint16_t)220, particles[i].ttl); + + Particle_Gravity_update(&particles[i], false, SEGMENT.check2, true, hardness); } SEGMENT.fill(BLACK); // clear the matrix // render the particles - ParticleSys_render(particles, numParticles, SEGMENT.check1, false); + ParticleSys_render(particles, numParticles, false, false); // CRGB c = PURPLE; // SEGMENT.setPixelColorXY(0, 0, c); return FRAMETIME; } -static const char _data_FX_MODE_PARTICLESPRAY[] PROGMEM = "Particle Spray@Moving Speed,Intensity,Particle Speed,Spray Angle,Nozzle Size,Wrap X,Bounce X,Bounce Y;;!;012;pal=11,sx=100,ix=200,c1=190,c2=128,c3=8,o1=0,o2=0,o3=0"; +static const char _data_FX_MODE_PARTICLESPRAY[] PROGMEM = "Particle Volcano@Moving Speed,Intensity,Particle Speed,Bouncyness,Nozzle Size,Color by Age,Bounce X,Collisions;;!;012;pal=35,sx=0,ix=160,c1=100,c2=160,c3=10,o1=1,o2=1,o3=1"; // good default values for sliders: 100,200,190, 45 /*syntax for json configuration string: @@ -8536,6 +8547,7 @@ uint16_t mode_particlefall(void) particles[i].vy = -(SEGMENT.speed >> 1); particles[i].hue = random8(); // set random color particles[i].sat = ((SEGMENT.custom3) << 3) + 7; // set saturation + particles[i].collide = true; // particle will collide break; // quit loop if all particles of this round emitted } i++; @@ -8615,6 +8627,7 @@ uint16_t mode_particlepile(void) spray[i].source.x = 2 * PS_P_RADIUS * (i + 1); spray[i].source.y = 14 * PS_P_RADIUS; // source y position, fixed at 14pixel height spray[i].source.vx = 10; + spray[i].source.collide = true; //seeded particles will collide spray[i].maxLife = 600; // lifetime in frames spray[i].minLife = 200; spray[i].vx = 0; // emitting speed @@ -8734,6 +8747,7 @@ uint16_t mode_particlebox(void) particles[i].sat = 255; // set full saturation (lets palette choose the color) particles[i].x = map(i, 0, 255, 1, cols * PS_P_RADIUS); // distribute along x according to color particles[i].y = random16((rows >> 2) * PS_P_RADIUS); // in the bottom quarder + particles[i].collide = true; //all particles collide } } @@ -8784,7 +8798,7 @@ uint16_t mode_particlebox(void) // scale gravity force xgravity = ((int16_t)xgravity * scale) >> 8; ygravity = ((int16_t)ygravity * scale) >> 8; - // todo: check with console output to see if scaling is applied correctly. + } // scale the gravity force down @@ -8800,6 +8814,11 @@ uint16_t mode_particlebox(void) particles[i].vx += xgravity; particles[i].vy += ygravity; particles[i].ttl = 500; // particles never die + + // apply a little gravity downwards to bias things in rocking mode + if (SEGMENT.check1 && SEGMENT.call % 2 == 0) + particles[i].vy--; + } } } @@ -8816,8 +8835,7 @@ uint16_t mode_particlebox(void) if (SEGMENT.call % 8 == 0) { applyFriction(&particles[i], 1); - } - // Particle_Gravity_update(&particles[i], SEGMENT.check1, SEGMENT.check2, SEGMENT.check3, (uint8_t)200); + } Particle_Bounce_update(&particles[i], hardness); } @@ -9037,18 +9055,19 @@ uint16_t mode_particleimpact(void) Serial.print(" "); if (meteors[i].source.ttl) { - Particle_Move_update(&meteors[i].source); // move the meteor, age the meteor (ttl--) + Particle_Gravity_update(&meteors[i].source, SEGMENT.check1, SEGMENT.check2, true, 255); // move the meteor, age the meteor (ttl--) // if source reaches the bottom, set speed to 0 so it will explode on next function call (handled above) if ((meteors[i].source.y < PS_P_RADIUS) && (meteors[i].source.vy < 0)) // reached the bottom pixel on its way down { meteors[i].source.vy = 0; // set speed zero so it will explode meteors[i].source.vx = 0; - meteors[i].source.y = 5; // offset from ground so explosion happens not out of frame (if moving fast, this can happen) + meteors[i].source.y = 5; // offset from ground so explosion happens not out of frame + meteors[i].source.collide = true; // explosion particles will collide if checked meteors[i].maxLife = 200; meteors[i].minLife = 50; meteors[i].source.ttl = random8((255 - SEGMENT.speed)) + 10; // standby time til next launch (in frames at 42fps, max of 265 is about 6 seconds meteors[i].vx = 0; // emitting speed x - meteors[i].vy = 8; // emitting speed y + meteors[i].vy = (SEGMENT.custom1 >> 2); // emitting speed y meteors[i].var = (SEGMENT.custom1 >> 1); // speed variation around vx,vy (+/- var/2) } } @@ -9057,14 +9076,15 @@ uint16_t mode_particleimpact(void) // reinitialize rocket meteors[i].source.y = PS_MAX_Y + PS_P_RADIUS<<2; // start 4 pixels above the top meteors[i].source.x = random16(PS_MAX_X); - meteors[i].source.vy = -30 - random(30) - 10; // TODO: need to make this user selectable? + meteors[i].source.vy = -random(30) - 30; //meteor downward speed meteors[i].source.vx = random8(30) - 15; meteors[i].source.hue = random8(); // random color meteors[i].source.ttl = 1000; // long live, will explode at bottom + meteors[i].source.collide = false; // trail particles will not collide meteors[i].maxLife = 60; // spark particle life - meteors[i].minLife = 20; + meteors[i].minLife = 20; meteors[i].vx = 0; // emitting speed - meteors[i].vy = -10; // emitting speed + meteors[i].vy = -9; // emitting speed (down) meteors[i].var = 5; // speed variation around vx,vy (+/- var/2) } } @@ -9136,6 +9156,7 @@ uint16_t mode_particleattractor(void) spray->source.vx = random8(5) + 6; spray->source.vy = random8(4) + 3; spray->source.ttl = 100; + spray->source.collide = true; //seeded particles will collide (if checked) spray->maxLife = 300; // seeded particle lifetime in frames spray->minLife = 30; spray->vx = 0; // emitting speed diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index fed38d1fdd..cc9716ffa8 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -46,7 +46,7 @@ void Emitter_Flame_emit(PSpointsource *emitter, PSparticle *part) part->vy = emitter->vy + random8(emitter->var) - (emitter->var >> 1); part->ttl = (uint8_t)((rand() % (emitter->maxLife - emitter->minLife)) + emitter->minLife + emitter->source.ttl); // flame intensity dies down with emitter TTL part->hue = emitter->source.hue; - part->sat = emitter->source.sat; + //part->sat = emitter->source.sat; //flame does not use saturation } // fountain style emitter @@ -56,9 +56,10 @@ void Emitter_Fountain_emit(PSpointsource *emitter, PSparticle *part) part->y = emitter->source.y; // + random8(emitter->var) - (emitter->var >> 1); part->vx = emitter->vx + random8(emitter->var) - (emitter->var >> 1); part->vy = emitter->vy + random8(emitter->var) - (emitter->var >> 1); - part->ttl = (rand() % (emitter->maxLife - emitter->minLife)) + emitter->minLife; + part->ttl = random16(emitter->maxLife - emitter->minLife) + emitter->minLife; part->hue = emitter->source.hue; part->sat = emitter->source.sat; + part->collide = emitter->source.collide; } // Emits a particle at given angle and speed, angle is from 0-255 (=0-360deg), speed is also affected by emitter->var @@ -732,7 +733,7 @@ void detectCollisions(PSparticle* particles, uint32_t numparticles, uint8_t hard { // go though all 'higher number' particles and see if any of those are in close proximity // if they are, make them collide - if (particles[i].ttl > 0) // if particle is alive + if (particles[i].ttl > 0 && particles[i].collide) // if particle is alive and does collide { int32_t dx, dy; // distance to other particles for (j = i + 1; j < numparticles; j++) @@ -769,7 +770,8 @@ void handleCollision(PSparticle *particle1, PSparticle *particle2, const uint8_t if (distanceSquared == 0) // add distance in case particles exactly meet at center, prevents dotProduct=0 (this can only happen if they move towards each other) { // Adjust positions based on relative velocity direction - if (relativeVx < 0) { //if true, particle2 is on the right side + + if (relativeVx <= 0) { //if true, particle2 is on the right side particle1->x--; particle2->x++; } else{ @@ -777,7 +779,7 @@ void handleCollision(PSparticle *particle1, PSparticle *particle2, const uint8_t particle2->x--; } - if (relativeVy < 0) { + if (relativeVy <= 0) { particle1->y--; particle2->y++; } else{ @@ -796,23 +798,22 @@ void handleCollision(PSparticle *particle1, PSparticle *particle2, const uint8_t // Calculate new velocities after collision int32_t impulse = (((dotProduct << (bitshift)) / (distanceSquared)) * hardness) >> 8; - - particle1->vx += (impulse * dx) >> bitshift; - particle1->vy += (impulse * dy) >> bitshift; - particle2->vx -= (impulse * dx) >> bitshift; - particle2->vy -= (impulse * dy) >> bitshift; - + int32_t ximpulse = (impulse * dx) >> bitshift; + int32_t yimpulse = (impulse * dy) >> bitshift; + particle1->vx += ximpulse; + particle1->vy += yimpulse; + particle2->vx -= ximpulse; + particle2->vy -= yimpulse; + if (hardness < 50) // if particles are soft, they become 'sticky' i.e. no slow movements { - if (particle1->vx < 2 && particle1->vx > -2) - particle1->vx = 0; - if (particle1->vy < 2 && particle1->vy > -2) - particle1->vy = 0; - if (particle2->vx < 2 && particle1->vx > -2) - particle1->vx = 0; - if (particle2->vy < 2 && particle1->vy > -2) - particle1->vy = 0; + particle1->vx = (particle1->vx < 2 && particle1->vx > -2) ? 0 : particle1->vx; + particle1->vy = (particle1->vy < 2 && particle1->vy > -2) ? 0 : particle1->vy; + + particle2->vx = (particle2->vx < 2 && particle2->vx > -2) ? 0 : particle2->vx; + particle2->vy = (particle2->vy < 2 && particle2->vy > -2) ? 0 : particle2->vy; } + } // particles have volume, push particles apart if they are too close by moving each particle by a fixed amount away from the other particle @@ -820,31 +821,31 @@ void handleCollision(PSparticle *particle1, PSparticle *particle2, const uint8_t // one problem remaining is, particles get squished if (external) force applied is higher than the pushback but this may also be desirable if particles are soft. also some oscillations cannot be avoided without addigng a counter if (distanceSquared < 2 * PS_P_HARDRADIUS * PS_P_HARDRADIUS) { - int32_t push; - //uint8_t rndchoice = random8(2); - const uint32_t HARDDIAMETER = PS_P_HARDRADIUS <<1; + uint8_t rndchoice = random8(2); + const uint32_t HARDDIAMETER = 2*PS_P_HARDRADIUS; + if (dx < HARDDIAMETER && dx > -HARDDIAMETER) { // distance is too small, push them apart - push = 1; + int32_t push = 3; if (dx < 0) // dx is negative push =-push; // negative push direction - - //if (rndchoice) // randomly chose one of the particles to push, avoids oscillations - // particle1->x -= push; - //else - particle2->x += push; //only push one particle to avoid oscillations + + if (rndchoice) // randomly chose one of the particles to push, avoids oscillations + particle1->x -= push; + else + particle2->x += push; // only push one particle to avoid oscillations } + if (dy < HARDDIAMETER && dy > -HARDDIAMETER) { // distance is too small (or negative) - push = 1; + int32_t push = 3; if (dy < 0) // dy is negative push = -push;//negative push direction - - //if (rndchoice) // randomly chose one of the particles to push, avoids oscillations - // particle1->y -= push; - //else - particle2->y += push; // only push one particle to avoid oscillations + if (rndchoice) // randomly chose one of the particles to push, avoids oscillations + particle1->y -= push; + else + particle2->y += push; // only push one particle to avoid oscillations } //note: pushing may push particles out of frame, if bounce is active, it will move it back as position will be limited to within frame } diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index 78acf52c51..ef0d5aa715 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -31,23 +31,36 @@ //particle dimensions (subpixel division) #define PS_P_RADIUS 64 //subpixel size, each pixel is divided by this for particle movement -#define PS_P_HARDRADIUS 100 //hard surface radius of a particle, used for collision detection proximity,also this is forbidden to be entered by another particle (for stacking) +#define PS_P_HARDRADIUS 100 //hard surface radius of a particle, used for collision detection proximity #define PS_P_SURFACE 12 //shift: 2^PS_P_SURFACE = (PS_P_RADIUS)^2 -//todo: can add bitfields to add in more stuff -//but when using bit fields, computation time increases as instructions are needed to mask the fields, only do it with variables that do not get updated too often +//todo: can add bitfields to add in more stuff, but accessing bitfields is slower than direct memory access! +//flags as bitfields is still very fast to access. + +union Flags { + struct { + + }; + uint8_t flagsByte; +}; + //struct for a single particle typedef struct { int16_t x; //x position in particle system - int16_t y; //y position in particle system - uint16_t ttl; //time to live - uint8_t outofbounds; //set to 1 if outside of matrix TODO: could make this a union and add more flags instead of wasting a whole byte on this - uint8_t hue; //color hue - uint8_t sat; //color saturation + int16_t y; //y position in particle system int8_t vx; //horizontal velocity int8_t vy; //vertical velocity + uint16_t ttl; // time to live + uint8_t hue; // color hue + uint8_t sat; // color saturation + //add a one byte bit field: + bool outofbounds : 1; //out of bounds flag, set to true if particle is outside of display area + bool collide : 1; //if flag is set, particle will take part in collisions + bool flag2 : 1; // unused flags... could use one for collisions to make those selective. + bool flag3 : 1; + uint8_t counter : 4; //a 4 bit counter for particle control } PSparticle; //struct for a particle source From 46aef896b252568c3bd2ea2a40a4d6e89fdbef51 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Tue, 13 Feb 2024 06:47:35 +0100 Subject: [PATCH 023/219] Bugfix in particle push, now piling is working again particle pile-up did not work correctly, now fixed --- wled00/FXparticleSystem.cpp | 53 +++++++++++++++++++++---------------- wled00/FXparticleSystem.h | 2 +- 2 files changed, 31 insertions(+), 24 deletions(-) diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index cc9716ffa8..be6dcae2bd 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -259,14 +259,14 @@ void Particle_Gravity_update(PSparticle *part, bool wrapX, bool bounceX, bool bo part->ttl--; // check if particle is out of bounds or died - if ((part->y < -PS_P_RADIUS) || (part->y >= PS_MAX_Y << 1)) + 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 << 1)) + 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 @@ -733,7 +733,7 @@ void detectCollisions(PSparticle* particles, uint32_t numparticles, uint8_t hard { // go though all 'higher number' particles and see if any of those are in close proximity // if they are, make them collide - if (particles[i].ttl > 0 && particles[i].collide) // if particle is alive and does 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 { int32_t dx, dy; // distance to other particles for (j = i + 1; j < numparticles; j++) @@ -771,7 +771,7 @@ void handleCollision(PSparticle *particle1, PSparticle *particle2, const uint8_t { // Adjust positions based on relative velocity direction - if (relativeVx <= 0) { //if true, particle2 is on the right side + if (relativeVx < 0) { //if true, particle2 is on the right side particle1->x--; particle2->x++; } else{ @@ -779,7 +779,7 @@ void handleCollision(PSparticle *particle1, PSparticle *particle2, const uint8_t particle2->x--; } - if (relativeVy <= 0) { + if (relativeVy < 0) { particle1->y--; particle2->y++; } else{ @@ -805,7 +805,7 @@ void handleCollision(PSparticle *particle1, PSparticle *particle2, const uint8_t particle2->vx -= ximpulse; particle2->vy -= yimpulse; - if (hardness < 50) // if particles are soft, they become 'sticky' i.e. no slow movements + if (hardness < 50) // if particles are soft, they become 'sticky' i.e. slow movements are stopped { particle1->vx = (particle1->vx < 2 && particle1->vx > -2) ? 0 : particle1->vx; particle1->vy = (particle1->vy < 2 && particle1->vy > -2) ? 0 : particle1->vy; @@ -814,38 +814,45 @@ void handleCollision(PSparticle *particle1, PSparticle *particle2, const uint8_t particle2->vy = (particle2->vy < 2 && particle2->vy > -2) ? 0 : particle2->vy; } - } - + } // particles have volume, push particles apart if they are too close by moving each particle by a fixed amount away from the other particle // if pushing is made dependent on hardness, things start to oscillate much more, better to just add a fixed, small increment (tried lots of configurations, this one works best) // one problem remaining is, particles get squished if (external) force applied is higher than the pushback but this may also be desirable if particles are soft. also some oscillations cannot be avoided without addigng a counter - if (distanceSquared < 2 * PS_P_HARDRADIUS * PS_P_HARDRADIUS) + if (distanceSquared < (int32_t)2 * PS_P_HARDRADIUS * PS_P_HARDRADIUS) { - uint8_t rndchoice = random8(2); - const uint32_t HARDDIAMETER = 2*PS_P_HARDRADIUS; + uint8_t choice = random8(2);//randomly choose one particle to push, avoids oscillations + const int32_t HARDDIAMETER = (int32_t)2*PS_P_HARDRADIUS; + if (dx < HARDDIAMETER && dx > -HARDDIAMETER) { // distance is too small, push them apart - int32_t push = 3; - if (dx < 0) // dx is negative - push =-push; // negative push direction - if (rndchoice) // randomly chose one of the particles to push, avoids oscillations + int32_t push; + if (dx <= 0) + push = -1;//-(PS_P_HARDRADIUS + dx); // inverted push direction + else + push = 1;//PS_P_HARDRADIUS - dx; + + if (choice) // chose one of the particles to push, avoids oscillations particle1->x -= push; else - particle2->x += push; // only push one particle to avoid oscillations + particle2->x += push; } - + //Serial.print(" dy"); + //Serial.println(dy); if (dy < HARDDIAMETER && dy > -HARDDIAMETER) - { // distance is too small (or negative) - int32_t push = 3; - if (dy < 0) // dy is negative - push = -push;//negative push direction + { + + int32_t push; + if (dy <= 0) + push = -1; //-(PS_P_HARDRADIUS + dy); // inverted push direction + else + push = 1; // PS_P_HARDRADIUS - dy; - if (rndchoice) // randomly chose one of the particles to push, avoids oscillations + if (choice) // chose one of the particles to push, avoids oscillations particle1->y -= push; else - particle2->y += push; // only push one particle to avoid oscillations + particle2->y += push; } //note: pushing may push particles out of frame, if bounce is active, it will move it back as position will be limited to within frame } diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index ef0d5aa715..e785c5f3f8 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -31,7 +31,7 @@ //particle dimensions (subpixel division) #define PS_P_RADIUS 64 //subpixel size, each pixel is divided by this for particle movement -#define PS_P_HARDRADIUS 100 //hard surface radius of a particle, used for collision detection proximity +#define PS_P_HARDRADIUS 80 //hard surface radius of a particle, used for collision detection proximity #define PS_P_SURFACE 12 //shift: 2^PS_P_SURFACE = (PS_P_RADIUS)^2 From b96ad99e0a99c393e78b672659577a58b9cbe1d7 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Tue, 13 Feb 2024 17:00:42 +0100 Subject: [PATCH 024/219] changed particle pile demo into waterfall plus some tweaks --- wled00/FX.cpp | 129 ++++++++++++++++++++++++-------------------------- wled00/FX.h | 2 +- 2 files changed, 63 insertions(+), 68 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 36eecb3095..b81a43a949 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -8257,7 +8257,7 @@ uint16_t mode_particlespray(void) } spray[i].vy = SEGMENT.custom1 >> 2; // emitting speed, upward spray[i].vx = 0; - spray[i].var = 0;//!!!SEGMENT.custom3; // emiting variation = nozzle size (custom 3 goes from 0-32) + spray[i].var = SEGMENT.custom3; // emiting variation = nozzle size (custom 3 goes from 0-32) //spray[i].source.y = spray[i].var + 1; // need to 'lift up' the source as 'var' also changes particle spawn position randomly spray[i].source.ttl = 255; // source never dies, replenish its lifespan } @@ -8565,7 +8565,7 @@ uint16_t mode_particlefall(void) { applyFriction(&particles[i], 50 - SEGMENT.speed); } - Particle_Gravity_update(&particles[i], SEGMENT.check1, SEGMENT.check2, SEGMENT.check3, 220); // surface hardness is fixed to 220 + Particle_Gravity_update(&particles[i], SEGMENT.check1, SEGMENT.check2, SEGMENT.check3, 150); // surface hardness is fixed to 150 } SEGMENT.fill(BLACK); // clear the matrix @@ -8585,7 +8585,7 @@ static const char _data_FX_MODE_PARTICLEFALL[] PROGMEM = "Falling Particles@Spee * by DedeHai (Damian Schneider) */ -uint16_t mode_particlepile(void) +uint16_t mode_particlewaterfall(void) { if (SEGLEN == 1) @@ -8594,8 +8594,8 @@ uint16_t mode_particlepile(void) const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; const uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); - const uint32_t numParticles = 400; - const uint8_t numSprays = 1; + const uint32_t numParticles = 500; + const uint8_t numSprays = 2; uint8_t percycle = numSprays; // maximum number of particles emitted per cycle PSparticle *particles; @@ -8624,41 +8624,32 @@ uint16_t mode_particlepile(void) { spray[i].source.hue = random8(); spray[i].source.sat = 255; // set full saturation - spray[i].source.x = 2 * PS_P_RADIUS * (i + 1); - spray[i].source.y = 14 * PS_P_RADIUS; // source y position, fixed at 14pixel height - spray[i].source.vx = 10; + spray[i].source.x = (cols * PS_P_RADIUS) / 2 - PS_P_RADIUS + 2*PS_P_RADIUS * (i); + spray[i].source.y = (rows+4) * (PS_P_RADIUS*(i+1)); // source y position, few pixels above the top to increase spreading before entering the matrix + spray[i].source.vx = 0; spray[i].source.collide = true; //seeded particles will collide spray[i].maxLife = 600; // lifetime in frames spray[i].minLife = 200; spray[i].vx = 0; // emitting speed + spray[i].var = 7; // emiting variation } // SEGMENT.palette = 35; //fire palette } - // change source emitting color from time to time + // change source emitting color for (i = 0; i < numSprays; i++) { - spray[i].source.hue++; // = random8(); //change hue of spray source + spray[i].source.hue++; //change hue of spray source } - if (SEGMENT.call % (11 - (SEGMENT.intensity / 25)) == 0 && SEGMENT.intensity > 0) // every nth frame, cycle color and emit particles, do not emit if intensity is zero + if (SEGMENT.call % (9 - (SEGMENT.intensity>>5)) == 0 && SEGMENT.intensity > 0) // every nth frame, cycle color and emit particles, do not emit if intensity is zero { for (i = 0; i < numSprays; i++) - { - // percycle = 1+(SEGMENT.intensity>>4); //how many particles are sprayed per cycle and how fast ist the color changing - if (spray[i].source.vx > 0) // moving to the right currently - { - spray[i].source.vx = SEGMENT.speed >> 4; // spray speed - } - else - { - spray[i].source.vx = -(SEGMENT.speed >> 4); // spray speed (is currently moving negative so keep it negative) - } - spray[i].vy = -SEGMENT.custom1 >> 3; // emitting speed, down - spray[i].vx = ((int16_t)SEGMENT.custom2 - (int16_t)127) / 8; // emitting speed, left/right (=angle) - spray[i].var = 2; // emiting variation - // spray[i].source.y = spray[i].var+1; //need to 'lift up' the source as 'var' also changes particle spawn position randomly + { + spray[i].vy = -SEGMENT.speed >> 3; // emitting speed, down + spray[i].source.x = map(SEGMENT.custom3, 0, 31, 0, (cols - 2) * PS_P_RADIUS) + i * PS_P_RADIUS * 2; // emitter position spray[i].source.ttl = 255; // source never dies, replenish its lifespan + spray[i].var = SEGMENT.custom1 >> 3; // emiting variation 0-32 } i = 0; @@ -8667,8 +8658,6 @@ uint16_t mode_particlepile(void) { if (particles[i].ttl == 0) // find a dead particle { - // ColorFromPalette(SEGPALETTE, random8() , 255, LINEARBLEND); - // spray[j].source.hue = random8(); //set random color for each particle (using palette) Emitter_Fountain_emit(&spray[j], &particles[i]); j = (j + 1) % numSprays; if (percycle-- == 0) @@ -8680,16 +8669,18 @@ uint16_t mode_particlepile(void) } } - for (i = 0; i < numSprays; i++) // update the sprays - { - Particle_Bounce_update(&spray[i].source, (uint8_t)255); // move the source - } // detect and handle collisions + uint8_t hardness = SEGMENT.custom2; // how hard the collisions are, 255 = full hard. - uint8_t hardness = ((SEGMENT.custom3) << 3) + 6; // how hard the collisions are, 255 = full hard. - detectCollisions(particles, numParticles, hardness); - + if (SEGMENT.custom2 > 0) //switch off collisions if hardnes is set to zero + { + detectCollisions(particles, numParticles, hardness); + } + else{ + hardness = 150; //set hardness (for ground bounce) to fixed value if not using collisions + } + // now move the particles for (i = 0; i < numParticles; i++) { @@ -8698,7 +8689,7 @@ uint16_t mode_particlepile(void) { applyFriction(&particles[i], 1); } - Particle_Gravity_update(&particles[i], SEGMENT.check1, SEGMENT.check2, SEGMENT.check3, (uint8_t)200); + Particle_Gravity_update(&particles[i], SEGMENT.check1, SEGMENT.check2, SEGMENT.check3, hardness); } SEGMENT.fill(BLACK); // clear the matrix @@ -8708,7 +8699,7 @@ uint16_t mode_particlepile(void) return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEPILE[] PROGMEM = "Particle Pile Test@Moving Speed,Intensity,Particle Speed,Spray Angle,Bouncyness,Wrap X,Bounce X,Ground bounce;;!;012;pal=11,sx=100,ix=200,c1=190,c2=128,c3=28,o1=0,o2=0,o3=1"; +static const char _data_FX_MODE_PARTICLEWATERFALL[] PROGMEM = "Particle Waterfall@Particle Speed,Intensity,Speed Variation,Collision Hardness,Position,Wrap X,Bounce X,Ground bounce;;!;012;pal=9,sx=150,ix=240,c1=0,c2=128,c3=17,o1=0,o2=0,o3=1"; /* Particle Box, applies gravity to particles in either a random direction or in a rocking motion @@ -8778,7 +8769,7 @@ uint16_t mode_particlebox(void) xgravity = ((int16_t)inoise8(SEGMENT.aux0) - 127); ygravity = ((int16_t)inoise8(SEGMENT.aux0 + 10000) - 127); } - else + else { // use sinusoidal motion // angle needs to move from -270° to +90° (from top leftside to top rightside but limited by one of the sliders by the user (custom1=Amount), -270 (and +90) is ~64 in 8bit angle representation (365=255) // the anglar force changes in a sinusoidal motion, like a rocking boat @@ -8788,8 +8779,8 @@ uint16_t mode_particlebox(void) int16_t angle = (int16_t)sin8(SEGMENT.aux0) - 128; // shift result (0-255 representing -1 to +1) so it goes from -128 to +127 scale = 130 - (abs(angle)); // force gets weaker at exteme positions if (scale > 50) - scale = 80; // force is strong everywhere but the top - angle = (angle * (int16_t)SEGMENT.custom1) >> 8; // scale angle range according to slider + scale = 50; // force is limited at lower angles + angle = (angle * (int16_t)SEGMENT.custom1) >> 8; // scale angle range according to slider (tilt strength) angle -= 63; // make 'down' (or -90°) the zero position // now calculate the force vectors xgravity = ((int16_t)cos8((uint8_t)angle)) - 128; // gravity direction +/- 127 @@ -8802,8 +8793,8 @@ uint16_t mode_particlebox(void) } // scale the gravity force down - xgravity = xgravity >> 4; - ygravity = ygravity >> 4; + xgravity = xgravity >> 5; + ygravity = ygravity >> 5; // Serial.print(xgravity); // Serial.println(" "); @@ -8846,7 +8837,7 @@ uint16_t mode_particlebox(void) return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEBOX[] PROGMEM = "Particle Box@Speed,Particles,Tilt strength,Bouncyness,,Rocking Boat,;;!;012;pal=1,sx=100,ix=82,c1=190,c2=210,o1=0"; +static const char _data_FX_MODE_PARTICLEBOX[] PROGMEM = "Particle Box@Speed,Particles,Tilt strength,Bouncyness,,Rocking Boat,;;!;012;pal=1,sx=120,ix=100,c1=190,c2=210,o1=0"; /* perlin noise 'gravity' mapping as in particles on noise hills viewed from above @@ -8990,6 +8981,7 @@ uint16_t mode_particleimpact(void) } for (i = 0; i < MaxNumMeteors; i++) { + meteors[i].source.y = 10; meteors[i].source.ttl = random8(20 * i); // set initial delay for meteors meteors[i].source.vy = 10; // at positive speeds, no particles are emitted and if particle dies, it will be relaunched meteors[i].source.sat = 255; //full saturation, color chosen by palette @@ -9014,7 +9006,7 @@ uint16_t mode_particleimpact(void) } else // speed is zero, explode! { - meteors[j].source.vy = 2; // set source speed positive so it goes into timeout and launches again + meteors[j].source.vy = 125; // set source speed positive so it goes into timeout and launches again emitparticles = random8(SEGMENT.intensity >> 1) + 10; // defines the size of the explosion } @@ -9033,10 +9025,10 @@ uint16_t mode_particleimpact(void) } } - //add collision if option is set + uint8_t hardness = SEGMENT.custom2; // how hard the collisions are, 255 = full hard. + // add collision if option is set if(SEGMENT.check3) - { - uint8_t hardness = SEGMENT.custom2; // how hard the collisions are, 255 = full hard. + { detectCollisions(particles, numParticles, hardness); } // update particles @@ -9044,42 +9036,42 @@ uint16_t mode_particleimpact(void) { if (particles[i].ttl) { - Particle_Gravity_update(&particles[i], SEGMENT.check1, SEGMENT.check2, true, SEGMENT.custom2); + Particle_Gravity_update(&particles[i], SEGMENT.check1, SEGMENT.check2, true, hardness); } } // update the meteors, set the speed state for (i = 0; i < numMeteors; i++) { - Serial.print(meteors[i].source.vy); - Serial.print(" "); if (meteors[i].source.ttl) { Particle_Gravity_update(&meteors[i].source, SEGMENT.check1, SEGMENT.check2, true, 255); // move the meteor, age the meteor (ttl--) - // if source reaches the bottom, set speed to 0 so it will explode on next function call (handled above) - if ((meteors[i].source.y < PS_P_RADIUS) && (meteors[i].source.vy < 0)) // reached the bottom pixel on its way down - { - meteors[i].source.vy = 0; // set speed zero so it will explode - meteors[i].source.vx = 0; - meteors[i].source.y = 5; // offset from ground so explosion happens not out of frame - meteors[i].source.collide = true; // explosion particles will collide if checked - meteors[i].maxLife = 200; - meteors[i].minLife = 50; - meteors[i].source.ttl = random8((255 - SEGMENT.speed)) + 10; // standby time til next launch (in frames at 42fps, max of 265 is about 6 seconds - meteors[i].vx = 0; // emitting speed x - meteors[i].vy = (SEGMENT.custom1 >> 2); // emitting speed y - meteors[i].var = (SEGMENT.custom1 >> 1); // speed variation around vx,vy (+/- var/2) - } + if (meteors[i].source.vy > 0) + meteors[i].source.y=5; //hack to keep the meteors within frame, as ttl will be set to zero by gravity update if too far out of frame + // if source reaches the bottom, set speed to 0 so it will explode on next function call (handled above) + if ((meteors[i].source.y < PS_P_RADIUS) && (meteors[i].source.vy < 0)) // reached the bottom pixel on its way down + { + meteors[i].source.vy = 0; // set speed zero so it will explode + meteors[i].source.vx = 0; + meteors[i].source.y = 5; // offset from ground so explosion happens not out of frame + meteors[i].source.collide = true; // explosion particles will collide if checked + meteors[i].maxLife = 200; + meteors[i].minLife = 50; + meteors[i].source.ttl = random8((255 - SEGMENT.speed)) + 10; // standby time til next launch (in frames at 42fps, max of 265 is about 6 seconds + meteors[i].vx = 0; // emitting speed x + meteors[i].vy = (SEGMENT.custom1 >> 2); // emitting speed y + meteors[i].var = (SEGMENT.custom1 >> 1); // speed variation around vx,vy (+/- var/2) + } } else if (meteors[i].source.vy > 0) // rocket is exploded and time is up (ttl==0 and positive speed), relaunch it { // reinitialize rocket - meteors[i].source.y = PS_MAX_Y + PS_P_RADIUS<<2; // start 4 pixels above the top + meteors[i].source.y = PS_MAX_Y + (PS_P_RADIUS<<2); // start 4 pixels above the top meteors[i].source.x = random16(PS_MAX_X); meteors[i].source.vy = -random(30) - 30; //meteor downward speed meteors[i].source.vx = random8(30) - 15; meteors[i].source.hue = random8(); // random color - meteors[i].source.ttl = 1000; // long live, will explode at bottom + meteors[i].source.ttl = 1000; // long life, will explode at bottom meteors[i].source.collide = false; // trail particles will not collide meteors[i].maxLife = 60; // spark particle life meteors[i].minLife = 20; @@ -9094,7 +9086,7 @@ uint16_t mode_particleimpact(void) return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEIMPACT[] PROGMEM = "Particle Impact@Launches,Explosion Size,Explosion Force,Bounce,Meteors,Wrap X,Bounce X,Collisions;;!;012;pal=8,sx=100,ix=50,c1=64,c2=128,c3=10,o1=0,o2=0,o3=1"; +static const char _data_FX_MODE_PARTICLEIMPACT[] PROGMEM = "Particle Impact@Launches,Explosion Size,Explosion Force,Bounce,Meteors,Wrap X,Bounce X,Collisions;;!;012;pal=35,sx=32,ix=85,c1=100,c2=100,c3=8,o1=0,o2=1,o3=1"; /* Particle Attractor, currently just a demo function for the particle attractor @@ -9219,6 +9211,9 @@ uint16_t mode_particleattractor(void) } static const char _data_FX_MODE_PARTICLEATTRACTOR[] PROGMEM = "Particle Attractor@Center Mass,Particles,,Collision Strength,Friction,,,Swallow;;!;012;pal=9,sx=100,ix=82,c1=190,c2=210,o1=0,o2=0,o3=0"; + +//TODO: animation idea: just one spray, sliders set x position, y position, speed, intensity and spray angle. ticks set wrap, bounce, gravity? evtl noch life time koppeln mit speed? + #endif // WLED_DISABLE_2D @@ -9470,7 +9465,7 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_PARTICLEBOX, &mode_particlebox, _data_FX_MODE_PARTICLEBOX); //experimental - addEffect(FX_MODE_PARTICLEPILE, &mode_particlepile, _data_FX_MODE_PARTICLEPILE); //particle sandbox used for testing new stuff + addEffect(FX_MODE_PARTICLEWATERFALL, &mode_particlewaterfall, _data_FX_MODE_PARTICLEWATERFALL); addEffect(FX_MODE_PARTICLEIMPACT, &mode_particleimpact, _data_FX_MODE_PARTICLEIMPACT); addEffect(FX_MODE_PARTICLEATTRACTOR, &mode_particleattractor, _data_FX_MODE_PARTICLEATTRACTOR); diff --git a/wled00/FX.h b/wled00/FX.h index d0367fca24..4b4698e3b9 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -327,7 +327,7 @@ #define FX_MODE_PARTICLEBOX 193 #define FX_MODE_PARTICLEATTRACTOR 194 #define FX_MODE_PARTICLEIMPACT 195 -#define FX_MODE_PARTICLEPILE 196 +#define FX_MODE_PARTICLEWATERFALL 196 #define MODE_COUNT 197 typedef enum mapping1D2D { From 32979e59010831b5011f398dff10777c04c82f2c Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sat, 17 Feb 2024 12:50:20 +0100 Subject: [PATCH 025/219] lots of bugfixes --- platformio.ini | 30 +++--- wled00/FX.cpp | 189 ++++++++++++++++++++++++++---------- wled00/FXparticleSystem.cpp | 26 ++--- 3 files changed, 171 insertions(+), 74 deletions(-) diff --git a/platformio.ini b/platformio.ini index 13c4feee42..6c1952134b 100644 --- a/platformio.ini +++ b/platformio.ini @@ -11,13 +11,12 @@ # CI/release binaries ;default_envs = nodemcuv2, esp8266_2m, esp01_1m_full, esp32dev, esp32_eth, esp32dev_audioreactive, lolin_s2_mini, esp32c3dev, esp32s3dev_8MB, esp32s3dev_8MB_PSRAM_opi, esp32_wrover -default_envs = esp32c3dev - +;default_envs = esp32c3dev +;default_envs = esp32dev_audioreactive src_dir = ./wled00 data_dir = ./wled00/data build_cache_dir = ~/.buildcache -extra_configs = - platformio_override.ini +extra_configs = platformio_override.ini [common] # ------------------------------------------------------------------------------ @@ -173,7 +172,7 @@ lib_deps = # SHT85 ;robtillaart/SHT85@~0.3.3 # Audioreactive usermod - ;https://github.com/kosme/arduinoFFT#develop @ ^1.9.2 + https://github.com/kosme/arduinoFFT#develop @ ^1.9.2 extra_scripts = ${scripts_defaults.extra_scripts} @@ -394,7 +393,7 @@ build_flags = ${common.build_flags} ${esp32c3.build_flags} -D WLED_RELEASE_NAME= -DLOLIN_WIFI_FIX ; seems to work much better with this -DARDUINO_USB_CDC_ON_BOOT=1 ;; for virtual CDC USB ;-DARDUINO_USB_CDC_ON_BOOT=0 ;; for serial-to-USB chip -upload_speed = 460800 +upload_speed = 460800 ; 115200 230400 460800 build_unflags = ${common.build_unflags} lib_deps = ${esp32c3.lib_deps} @@ -403,19 +402,22 @@ lib_deps = ${esp32c3.lib_deps} board = esp32-s3-devkitc-1 platform = ${esp32s3.platform} platform_packages = ${esp32s3.platform_packages} -upload_speed = 921600 ; or 460800 +upload_speed = 115200 ; 230400 ;460800 build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=ESP32-S3_8MB -D CONFIG_LITTLEFS_FOR_IDF_3_2 -D WLED_WATCHDOG_TIMEOUT=0 -D ARDUINO_USB_CDC_ON_BOOT=0 ;; -D ARDUINO_USB_MODE=1 ;; for boards with serial-to-USB chip ;-D ARDUINO_USB_CDC_ON_BOOT=1 ;; -D ARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB") - ;-D WLED_DEBUG -lib_deps = ${esp32s3.lib_deps} -board_build.partitions = tools/WLED_ESP32_8MB.csv -board_build.f_flash = 80000000L -board_build.flash_mode = qio -; board_build.flash_mode = dio ;; try this if you have problems at startup -monitor_filters = esp32_exception_decoder + -D WLED_DEBUG + -D USERMOD_AUDIOREACTIVE -D UM_AUDIOREACTIVE_USE_NEW_FFT # additional build flags for audioreactive +lib_deps = ${esp32s3.lib_deps} + https://github.com/kosme/arduinoFFT#develop @ 1.9.2+sha.419d7b0 ;; used for USERMOD_AUDIOREACTIVE - using "known working" hash +;board_build.partitions = tools/WLED_ESP32_8MB.csv +board_build.partitions = tools/WLED_ESP32_4MB_1MB_FS.csv +;board_build.f_flash = 80000000L +;board_build.flash_mode = qio +board_build.flash_mode = dio ;; try this if you have problems at startup +;monitor_filters = esp32_exception_decoder [env:esp32s3dev_8MB_PSRAM_opi] ;; ESP32-S3 development board, with 8MB FLASH and >= 8MB PSRAM (memory_type: qio_opi) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index b81a43a949..8e07477cce 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -6271,7 +6271,7 @@ static const char _data_FX_MODE_2DPLASMAROTOZOOM[] PROGMEM = "Rotozoomer@!,Scale float *fftBin = nullptr; um_data_t *um_data; if (usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { - volumeSmth = *(float*) um_data->u_data[0]; + volumeSmth = *(float*) um_data->u_data[0]; volumeRaw = *(float*) um_data->u_data[1]; fftResult = (uint8_t*) um_data->u_data[2]; samplePeak = *(uint8_t*) um_data->u_data[3]; @@ -8050,6 +8050,8 @@ uint16_t mode_particlefireworks(void) } // update particles, create particles + uint8_t circularexplosion = random8(numRockets + 2); //choose a rocket by random (but not every round one will be picked) + uint8_t spiralexplosion = random8(numRockets + 2); // check each rocket's state and emit particles according to its state: moving up = emit exhaust, at top = explode; falling down = standby time uint16_t emitparticles; // number of particles to emit for each rocket's state @@ -8069,36 +8071,54 @@ uint16_t mode_particlefireworks(void) { // speed is zero, explode! emitparticles = random8(SEGMENT.intensity>>1) + 10; // defines the size of the explosion rockets[j].source.vy = -1; // set speed negative so it will emit no more particles after this explosion until relaunch - if (j == 0) // first rocket, do an angle emit + if (j == circularexplosion || j == spiralexplosion) // chosen rocket, do an angle emit (creating a circle) { - emitparticles>>2; //emit less particles for circle-explosion + emitparticles>>3; //emit less particles for circle-explosions rockets[j].maxLife = 150; rockets[j].minLife = 120; - rockets[j].var = 0; // speed variation around vx,vy (+/- var/2) + rockets[j].var = 0; // speed variation around vx,vy (+/- var/2) } } - uint8_t speed = 5; + uint8_t speed = 3; uint8_t angle = 0; + if (j == spiralexplosion) + angle = random(8); for (i; i < numParticles; i++) { if (particles[i].ttl == 0) { // particle is dead - if (j == 0 && emitparticles > 2) // first rocket, do angle emit + if (j == circularexplosion && emitparticles > 2) //do circle emit { Emitter_Angle_emit(&rockets[j], &particles[i],angle,speed); - emitparticles--; - //set angle for next particle - angle += 21; //about 30° - if(angle > 250) //full circle completed, increase speed and reset angle + + if (angle > 242) // full circle completed, increase speed and reset angle { - angle = 0; - speed += 8; - rockets[j].source.hue = random8(); //new color for next row - rockets[j].source.sat = random8(); + angle += 10; + speed += 6; + rockets[j].source.hue = random8(); // new color for next row + rockets[j].source.sat = random8(); + if(emitparticles > 12) + emitparticles-=12; //emitted about 12 particles for one circle, ensures no incomplete circles are done + else + emitparticles = 0; } + + //set angle for next particle + angle += 21; //about 30° + } + else if (j == spiralexplosion && emitparticles > 2) // do spiral emit + { + Emitter_Angle_emit(&rockets[j], &particles[i], angle, speed); + emitparticles--; + emitparticles--;//only emit half as many particles as in circle explosion, it gets too huge otherwise + angle += 15; + speed++; + rockets[j].source.hue++; + rockets[j].source.sat = random8(155)+100; + } else if (emitparticles > 0) { @@ -8138,7 +8158,7 @@ uint16_t mode_particlefireworks(void) rockets[i].source.sat = random8(100)+155; rockets[i].maxLife = 200; rockets[i].minLife = 50; - rockets[i].source.ttl = random8((255 - SEGMENT.speed))+10; // standby time til next launch (in frames at 42fps, max of 265 is about 6 seconds + rockets[i].source.ttl = random8((255 - SEGMENT.speed))+50; // standby time til next launch (in frames at 42fps, max of 300 is about 7 seconds rockets[i].vx = 0; // emitting speed rockets[i].vy = 0; // emitting speed rockets[i].var = (SEGMENT.intensity >> 3) + 10; // speed variation around vx,vy (+/- var/2) @@ -8167,7 +8187,7 @@ uint16_t mode_particlefireworks(void) return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEFIREWORKS[] PROGMEM = "Particle Fireworks@Launches,Explosion Size,Height,Bounce,Rockets,Wrap X,Bounce X,Bounce Y;;!;012;pal=11,sx=100,ix=50,c1=64,c2=128,c3=10,o1=0,o2=0,o3=1"; +static const char _data_FX_MODE_PARTICLEFIREWORKS[] PROGMEM = "Particle Fireworks@Launches,Explosion Size,Height,Bounce,Rockets,Wrap X,Bounce X,Bounce Y;;!;012;pal=11,sx=100,ix=50,c1=64,c2=128,c3=10,o1=0,o2=0,o3=0"; /* * Particle Volcano (gravity spray) @@ -8517,8 +8537,8 @@ uint16_t mode_particlefall(void) // calculate the end of the spray data and assign it as the data pointer for the particles: particles = reinterpret_cast(SEGENV.data); // cast the data array into a particle pointer + uint32_t i = 0; - uint32_t j = 0; if (SEGMENT.call == 0) // initialization { @@ -8527,7 +8547,7 @@ uint16_t mode_particlefall(void) particles[i].ttl = 0; } } - particles[i].sat = ((SEGMENT.custom3) << 3) + 7; // set saturation + if (SEGMENT.call % (64 - (SEGMENT.intensity >> 2)) == 0 && SEGMENT.intensity > 1) // every nth frame emit particles, stop emitting if zero { while (i < numParticles) // emit particles @@ -8535,7 +8555,7 @@ uint16_t mode_particlefall(void) if (particles[i].ttl == 0) // find a dead particle { // emit particle at random position just over the top of the matrix - particles[i].ttl = 3000 - (SEGMENT.speed << 3) + random16(500); // if speed is higher, make them die sooner + particles[i].ttl = 1500 - (SEGMENT.speed << 2) + random16(500); // if speed is higher, make them die sooner if (random8(5) == 0) // 16% of particles apper anywhere particles[i].x = random16(cols * PS_P_RADIUS - 1); @@ -8565,7 +8585,7 @@ uint16_t mode_particlefall(void) { applyFriction(&particles[i], 50 - SEGMENT.speed); } - Particle_Gravity_update(&particles[i], SEGMENT.check1, SEGMENT.check2, SEGMENT.check3, 150); // surface hardness is fixed to 150 + Particle_Gravity_update(&particles[i], SEGMENT.check1, SEGMENT.check2, SEGMENT.check3, min(hardness,(uint8_t)150)); // surface hardness max is 150 } SEGMENT.fill(BLACK); // clear the matrix @@ -8580,17 +8600,69 @@ uint16_t mode_particlefall(void) static const char _data_FX_MODE_PARTICLEFALL[] PROGMEM = "Falling Particles@Speed,Intensity,Randomness,Collision hardness,Saturation,Wrap X,Side bounce,Ground bounce;;!;012;pal=11,sx=100,ix=200,c1=31,c2=0,c3=20,o1=0,o2=0,o3=1"; /* - * Particle pile up test - * Uses palette for particle color + * Particle Waterfall + * Uses palette for particle color, spray source at top emitting particles, many config options * by DedeHai (Damian Schneider) */ +/*Audio Reactive test: + + volumeSmth = *(float*) um_data->u_data[0]; + volumeRaw = *(float*) um_data->u_data[1]; + fftResult = (uint8_t*) um_data->u_data[2]; + samplePeak = *(uint8_t*) um_data->u_data[3]; + FFT_MajorPeak = *(float*) um_data->u_data[4]; + my_magnitude = *(float*) um_data->u_data[5]; + maxVol = (uint8_t*) um_data->u_data[6]; // requires UI element (SEGMENT.customX?), changes source element + binNum = (uint8_t*) um_data->u_data[7]; // requires UI element (SEGMENT.customX?), changes source element + fftBin = (float*) um_data->u_data[8]; //not sure what this pointer does... not used in any examples + */ + uint16_t mode_particlewaterfall(void) { if (SEGLEN == 1) return mode_static(); + +/* + um_data_t *um_data; + if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) + { + um_data = NULL; //no audio + } + //TODO: will crash if no umdata! + uint8_t volumeSmth = (uint8_t)(*(float *)um_data->u_data[0]); //volume, mapped 0-255 + float volumeRaw = *(float *)um_data->u_data[1]; //always zero? + uint8_t* fftResult = (uint8_t *)um_data->u_data[2]; //16 bins with FFT data + uint8_t samplePeak = *(uint8_t *)um_data->u_data[3]; //0 or 1 if a sample peak is detected (not sure what the thresholds are) + float FFT_MajorPeak = *(float *)um_data->u_data[4]; //frequency in Hz of major peak + float my_magnitude = *(float *)um_data->u_data[5]; //current volume, should fit a uint16_t (goes up to 20'000 or even higher?) but unclear what exactly the value is + //maxVol = (uint8_t *)um_data->u_data[6]; // requires UI element (SEGMENT.customX?), changes source element, see Ripple Peak for an example + //binNum = (uint8_t *)um_data->u_data[7]; // requires UI element (SEGMENT.customX?), changes source element + //float* fftBin = (float *)um_data->u_data[8]; //points to what exactly? + */ +/* + //print values as a test: + Serial.println("***"); + Serial.print(volumeSmth); + Serial.print(", "); + Serial.print(volumeRaw); + Serial.print(", "); + Serial.print(samplePeak); + Serial.print(", "); + Serial.print(FFT_MajorPeak); + Serial.print(", "); + Serial.println(my_magnitude); + + for(int i = 0; i<16;i++) + { + Serial.print(fftResult[i]); + Serial.print(" "); + } + + Serial.println("***"); +*/ const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; const uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); @@ -8641,33 +8713,40 @@ uint16_t mode_particlewaterfall(void) { spray[i].source.hue++; //change hue of spray source } - if (SEGMENT.call % (9 - (SEGMENT.intensity>>5)) == 0 && SEGMENT.intensity > 0) // every nth frame, cycle color and emit particles, do not emit if intensity is zero + + uint8_t intensity = SEGMENT.intensity; +/* + if (um_data != NULL) //audio reactive data available { + intensity = map(volumeSmth,0,255,20,255); + }*/ + if (SEGMENT.call % (9 - (intensity >> 5)) == 0 && intensity > 0) // every nth frame, cycle color and emit particles, do not emit if intensity is zero + { - for (i = 0; i < numSprays; i++) - { - spray[i].vy = -SEGMENT.speed >> 3; // emitting speed, down - spray[i].source.x = map(SEGMENT.custom3, 0, 31, 0, (cols - 2) * PS_P_RADIUS) + i * PS_P_RADIUS * 2; // emitter position - spray[i].source.ttl = 255; // source never dies, replenish its lifespan - spray[i].var = SEGMENT.custom1 >> 3; // emiting variation 0-32 - } + for (i = 0; i < numSprays; i++) + { + spray[i].vy = -SEGMENT.speed >> 3; // emitting speed, down + spray[i].source.x = map(SEGMENT.custom3, 0, 31, 0, (cols - 2) * PS_P_RADIUS) + i * PS_P_RADIUS * 2; // emitter position + spray[i].source.ttl = 255; // source never dies, replenish its lifespan + spray[i].var = SEGMENT.custom1 >> 3; // emiting variation 0-32 + } - i = 0; - j = 0; - while (i < numParticles) - { - if (particles[i].ttl == 0) // find a dead particle + i = 0; + j = 0; + while (i < numParticles) { - Emitter_Fountain_emit(&spray[j], &particles[i]); - j = (j + 1) % numSprays; - if (percycle-- == 0) + if (particles[i].ttl == 0) // find a dead particle { - break; // quit loop if all particles of this round emitted + Emitter_Fountain_emit(&spray[j], &particles[i]); + j = (j + 1) % numSprays; + if (percycle-- == 0) + { + break; // quit loop if all particles of this round emitted + } } + i++; } - i++; } - } // detect and handle collisions @@ -8725,6 +8804,8 @@ uint16_t mode_particlebox(void) return mode_static(); // allocation failed; //allocation failed particles = reinterpret_cast(SEGENV.data); // cast the data array into a particle pointer + + uint32_t i = 0; uint32_t j = 0; @@ -8837,7 +8918,7 @@ uint16_t mode_particlebox(void) return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEBOX[] PROGMEM = "Particle Box@Speed,Particles,Tilt strength,Bouncyness,,Rocking Boat,;;!;012;pal=1,sx=120,ix=100,c1=190,c2=210,o1=0"; +static const char _data_FX_MODE_PARTICLEBOX[] PROGMEM = "Particle Box@Speed,Particles,Tilt strength,Hardness,,Rocking Boat,;;!;012;pal=1,sx=120,ix=100,c1=190,c2=210,o1=0"; /* perlin noise 'gravity' mapping as in particles on noise hills viewed from above @@ -8856,7 +8937,12 @@ uint16_t mode_particleperlin(void) const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; const uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); - const uint32_t numParticles = 255; + +#ifdef ESP8266 + const uint32_t numParticles = 200; +#else + const uint32_t numParticles = 350; +#endif PSparticle *particles; @@ -8881,7 +8967,7 @@ uint16_t mode_particleperlin(void) } } - uint32_t displayparticles = SEGMENT.intensity; + uint32_t displayparticles = map(SEGMENT.intensity,0,255,10,numParticles); // apply 'gravity' from a 2D perlin noise map SEGMENT.aux0 += SEGMENT.speed >> 4; // noise z-position; @@ -8928,7 +9014,7 @@ uint16_t mode_particleperlin(void) return FRAMETIME; } static const char _data_FX_MODE_PARTICLEPERLIN[] PROGMEM = "Particle Perlin-Noise@,Particles,;;!;012;pal=1,ix=100"; - +//TODO: add zoom and maybe some other functions /* * Particle smashing down like meteorites and exploding as they hit the ground, like inverted fireworks * has many parameters to play with @@ -9105,7 +9191,12 @@ uint16_t mode_particleattractor(void) const uint16_t PS_MAX_X(cols * PS_P_RADIUS - 1); const uint16_t PS_MAX_Y(rows * PS_P_RADIUS - 1); - const uint32_t numParticles = 265; // maximum number of particles +#ifdef ESP8266 + const uint32_t numParticles = 150; // maximum number of particles +#else + const uint32_t numParticles = 300; // maximum number of particles +#endif + PSparticle *particles; PSparticle *attractor; @@ -9126,8 +9217,8 @@ uint16_t mode_particleattractor(void) spray = reinterpret_cast(attractor + 1); counters = reinterpret_cast(spray + 1); - uint32_t i = 0; - uint32_t j = 0; + uint32_t i; + uint32_t j; if (SEGMENT.call == 0) // initialization { @@ -9156,7 +9247,7 @@ uint16_t mode_particleattractor(void) spray->var = 6; //emitting speed variation } - uint16_t displayparticles = SEGMENT.intensity; //cannot go to 255 particles, it will crash if set above 250, why is unclear... maybe array is too small? at 260 it will still crash, if numparticles are set to 265 it does not crash... + uint32_t displayparticles = map(SEGMENT.intensity, 0, 255, 1, numParticles); uint8_t hardness = SEGMENT.custom2; // how hard the collisions are, 255 = full hard. i = 0; j = 0; diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index be6dcae2bd..b62ccb3780 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -85,31 +85,31 @@ void Particle_attractor(PSparticle *particle, PSparticle *attractor, uint8_t *co particle->ttl = 0; return; } - distanceSquared = PS_P_RADIUS * PS_P_RADIUS + PS_P_RADIUS * PS_P_RADIUS; // limit the distance of particle size to avoid very high forces + distanceSquared = 4 * PS_P_RADIUS * PS_P_RADIUS; // limit the distance of particle size to avoid very high forces } - // check if distance is small enough to even cause a force (for that strength<<10 must be bigger than the distance squared) + int32_t shiftedstrength = (int32_t)strength << 16; int32_t force; int32_t xforce; int32_t yforce; int32_t xforce_abs; // absolute value int32_t yforce_abs; - - if (shiftedstrength < distanceSquared) // if far away, set the force to 1 so it still attracts and does not leave particles just sitting outside its influence radius - { + // check if distance is small enough to even cause a force (for that strength<<10 must be bigger than the distance squared) + //if ((shiftedstrength) < distanceSquared) // if far away, set the force to 2 so it still attracts and does not leave particles just sitting outside its influence radius + //{ // force calculation above is zero //give some force in both directions (x and y) to avoid further calculations as this is just to get things moving a little - xforce_abs = 1; - yforce_abs = 1; - } - else - { + //xforce_abs = 1; + //yforce_abs = 1; + //} + //else + //{ force = shiftedstrength / distanceSquared; xforce = (force * dx) >> 10; // scale to a lower value, found by experimenting yforce = (force * dy) >> 10; xforce_abs = abs(xforce); // absolute value yforce_abs = abs(yforce); - } + //} uint8_t xcounter = (*counter) & 0x0F; // lower four bits uint8_t ycounter = (*counter) >> 4; // upper four bits @@ -133,6 +133,8 @@ void Particle_attractor(PSparticle *particle, PSparticle *attractor, uint8_t *co particle->vx += 1; } } + else //save counter value + *counter |= xcounter & 0x0F; // write lower four bits, make sure not to write more than 4 bits } else { @@ -158,6 +160,8 @@ void Particle_attractor(PSparticle *particle, PSparticle *attractor, uint8_t *co particle->vy += 1; } } + else // save counter value + *counter |= (ycounter << 4) & 0xF0; // write upper four bits } else { From 74ed705b9cb465ba573b6bce35ecaabfd64eeb0d Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sat, 17 Feb 2024 14:19:56 +0100 Subject: [PATCH 026/219] updated particle attractor animation parameters and added more user options to perlin noise --- wled00/FX.cpp | 69 +++++++++++++++++++++++-------------- wled00/FXparticleSystem.cpp | 23 ++++--------- 2 files changed, 50 insertions(+), 42 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 8e07477cce..05d03ac603 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -8939,7 +8939,7 @@ uint16_t mode_particleperlin(void) #ifdef ESP8266 - const uint32_t numParticles = 200; + const uint32_t numParticles = 150; #else const uint32_t numParticles = 350; #endif @@ -8970,7 +8970,8 @@ uint16_t mode_particleperlin(void) uint32_t displayparticles = map(SEGMENT.intensity,0,255,10,numParticles); // apply 'gravity' from a 2D perlin noise map - SEGMENT.aux0 += SEGMENT.speed >> 4; // noise z-position; + + SEGMENT.aux0 += 1+(SEGMENT.speed >> 5); // noise z-position // update position in noise for (i = 0; i < displayparticles; i++) @@ -8982,28 +8983,34 @@ uint16_t mode_particleperlin(void) particles[i].x = random16(cols * PS_P_RADIUS); particles[i].y = random16(rows * PS_P_RADIUS); } + int32_t xnoise = particles[i].x / (1 + (SEGMENT.custom3>>1)); //position in perlin noise, scaled by slider + int32_t ynoise = particles[i].y / (1 + (SEGMENT.custom3>>1)); - int16_t baseheight = inoise8((particles[i].x >> 1), (particles[i].y >> 1), SEGMENT.aux0); // noise value at particle position + int16_t baseheight = inoise8(xnoise, ynoise, SEGMENT.aux0); // noise value at particle position particles[i].hue = baseheight; // color particles to perlin noise value if (SEGMENT.call % 6 == 0) // do not apply the force every frame, is too chaotic { - int16_t xslope = baseheight - (int16_t)inoise8((particles[i].x >> 1) + 10, (particles[i].y >> 1), SEGMENT.aux0); - int16_t yslope = baseheight - (int16_t)inoise8((particles[i].x >> 1), (particles[i].y >> 1) + 10, SEGMENT.aux0); - // Serial.println(xslope); + int16_t xslope = baseheight - (int16_t)inoise8(xnoise + 10, ynoise, SEGMENT.aux0); + int16_t yslope = baseheight - (int16_t)inoise8(xnoise, ynoise + 10, SEGMENT.aux0); + particles[i].vx += xslope >> 1; // slope is about 0-8 particles[i].vy += yslope >> 1; } } + uint8_t hardness = SEGMENT.custom1; // how hard the collisions are, 255 = full hard. + if(SEGMENT.check1) //collisions enabled + { + detectCollisions(particles, displayparticles, hardness); + } // move particles for (i = 0; i < displayparticles; i++) { - // apply a bit of friction so particles are less jittery - if (SEGMENT.call % 16 == 0) // need to apply friction very rarely or particles will clump + if (SEGMENT.call % (16-(SEGMENT.custom2>>4)) == 0) // need to apply friction very rarely or particles will clump applyFriction(&particles[i], 1); - Particle_Bounce_update(&particles[i], (uint8_t)200); + Particle_Bounce_update(&particles[i], hardness); } SEGMENT.fill(BLACK); // clear the matrix @@ -9013,15 +9020,15 @@ uint16_t mode_particleperlin(void) return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEPERLIN[] PROGMEM = "Particle Perlin-Noise@,Particles,;;!;012;pal=1,ix=100"; -//TODO: add zoom and maybe some other functions +static const char _data_FX_MODE_PARTICLEPERLIN[] PROGMEM = "Particle Perlin-Noise@Speed,Particles,Collision Hardness,Friction,Scale,Collisions;;!;012;pal=54,sx=70;ix=200,c1=190,c2=120,c3=4,o1=0"; + + /* * Particle smashing down like meteorites and exploding as they hit the ground, like inverted fireworks * has many parameters to play with * by DedeHai (Damian Schneider) */ - uint16_t mode_particleimpact(void) { if (SEGLEN == 1) @@ -9235,9 +9242,9 @@ uint16_t mode_particleattractor(void) spray->source.hue = random8(); spray->source.sat = 255; //full saturation, color by palette spray->source.x = 0; - spray->source.y = 0; // just above the lower edge, if zero, particles already 'bounce' at start and loose speed. - spray->source.vx = random8(5) + 6; - spray->source.vy = random8(4) + 3; + spray->source.y = 0; + spray->source.vx = random8(5) + 2; + spray->source.vy = random8(4) + 1; spray->source.ttl = 100; spray->source.collide = true; //seeded particles will collide (if checked) spray->maxLife = 300; // seeded particle lifetime in frames @@ -9257,8 +9264,6 @@ uint16_t mode_particleattractor(void) detectCollisions(particles, displayparticles, hardness); } - - if (SEGMENT.call % 5 == 0) { spray->source.hue++; @@ -9268,39 +9273,51 @@ uint16_t mode_particleattractor(void) uint8_t emit = 1; //number of particles emitted per frame Particle_Bounce_update(&spray->source, 255); //bounce the spray around + SEGMENT.aux0++; //emitting angle + // now move the particles for (i = 0; i < displayparticles; i++) { if (particles[i].ttl == 0 && emit--) // find a dead particle { - Emitter_Fountain_emit(spray, &particles[i]); //emit one if available + //Emitter_Fountain_emit(spray, &particles[i]); //emit one if available + if(SEGMENT.call % 2 == 0) //alternate direction of emit + Emitter_Angle_emit(spray, &particles[i], SEGMENT.aux0, SEGMENT.custom1 >> 4); + else + Emitter_Angle_emit(spray, &particles[i], SEGMENT.aux0+128, SEGMENT.custom1 >> 4); //emit at 180° as well } // every now and then, apply 'air friction' to smooth things out, slows down all particles a little if (SEGMENT.custom3 > 0) { if (SEGMENT.call % (32 - SEGMENT.custom3) == 0) - { - if (SEGMENT.check2) - applyFriction(&particles[i], 1); + { + applyFriction(&particles[i], 4); } } Particle_attractor(&particles[i], attractor, &counters[i], SEGMENT.speed, SEGMENT.check3); - - // Particle_Bounce_update(&particles[i], hardness); - Particle_Move_update(&particles[i]); + if(SEGMENT.check1) + Particle_Bounce_update(&particles[i], hardness); + else + Particle_Move_update(&particles[i]); } - SEGMENT.fill(BLACK); // clear the matrix + if (SEGMENT.check2) + SEGMENT.fadeToBlackBy(20); // fade the matrix + else + SEGMENT.fill(BLACK); // clear the matrix + + + //ParticleSys_render(&attract, 1, 30, false, false); // render attractor // render the particles ParticleSys_render(particles, displayparticles, false, false); return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEATTRACTOR[] PROGMEM = "Particle Attractor@Center Mass,Particles,,Collision Strength,Friction,,,Swallow;;!;012;pal=9,sx=100,ix=82,c1=190,c2=210,o1=0,o2=0,o3=0"; +static const char _data_FX_MODE_PARTICLEATTRACTOR[] PROGMEM = "Particle Attractor@Center Mass,Particles,Emit Speed,Collision Strength,Friction,Bounce,Trails,Swallow;;!;012;pal=9,sx=100,ix=82,c1=190,c2=210,o1=0,o2=0,o3=0"; //TODO: animation idea: just one spray, sliders set x position, y position, speed, intensity and spray angle. ticks set wrap, bounce, gravity? evtl noch life time koppeln mit speed? diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index b62ccb3780..4201e59b60 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -94,22 +94,13 @@ void Particle_attractor(PSparticle *particle, PSparticle *attractor, uint8_t *co int32_t yforce; int32_t xforce_abs; // absolute value int32_t yforce_abs; - // check if distance is small enough to even cause a force (for that strength<<10 must be bigger than the distance squared) - //if ((shiftedstrength) < distanceSquared) // if far away, set the force to 2 so it still attracts and does not leave particles just sitting outside its influence radius - //{ - // force calculation above is zero - //give some force in both directions (x and y) to avoid further calculations as this is just to get things moving a little - //xforce_abs = 1; - //yforce_abs = 1; - //} - //else - //{ - force = shiftedstrength / distanceSquared; - xforce = (force * dx) >> 10; // scale to a lower value, found by experimenting - yforce = (force * dy) >> 10; - xforce_abs = abs(xforce); // absolute value - yforce_abs = abs(yforce); - //} + + force = shiftedstrength / distanceSquared; + xforce = (force * dx) >> 10; // scale to a lower value, found by experimenting + yforce = (force * dy) >> 10; + xforce_abs = abs(xforce); // absolute value + yforce_abs = abs(yforce); + uint8_t xcounter = (*counter) & 0x0F; // lower four bits uint8_t ycounter = (*counter) >> 4; // upper four bits From 11a84c1b2a142d3debaaeb35335079454830a2ec Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sat, 17 Feb 2024 14:23:05 +0100 Subject: [PATCH 027/219] removed TODOs --- wled00/FX.cpp | 6 ------ 1 file changed, 6 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 05d03ac603..2c725ab718 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -8141,10 +8141,6 @@ uint16_t mode_particlefireworks(void) } // update the rockets, set the speed state - - // todo: man kann für das timing auch die rakete selbst benutzen: dazu einfach vy verwenden: ist es >0, steigt die rakete auf. ist ttl=0 wird vy = -1 gesetzt und ttl als standbyzeit gesetzt. - // ist vy<0 so wird emit funkion nicht aufgerufen. - // ist vy>0 so wird genau ein partikel emittiert. ist vy>0 und for (i = 0; i < numRockets; i++) { if (rockets[i].source.ttl) @@ -8924,8 +8920,6 @@ static const char _data_FX_MODE_PARTICLEBOX[] PROGMEM = "Particle Box@Speed,Part perlin noise 'gravity' mapping as in particles on noise hills viewed from above calculates slope gradient at the particle positions restults in a fuzzy perlin noise disply - -//TODO: add more parameters, like size and moving speed thorugh the noise */ uint16_t mode_particleperlin(void) From 241b08082e98942fae2290658ff02eb3c4247828 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sat, 17 Feb 2024 16:50:16 +0100 Subject: [PATCH 028/219] added particle WrapUpdate function, added spray FX, some bugfixes --- wled00/FX.cpp | 136 ++++++++++++++++++++++++++++++++++-- wled00/FX.h | 5 +- wled00/FXparticleSystem.cpp | 49 +++++++++++++ wled00/FXparticleSystem.h | 1 + 4 files changed, 182 insertions(+), 9 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 2c725ab718..303cfb451d 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -7889,8 +7889,13 @@ uint16_t mode_particlerotatingspray(void) const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; const uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); + +#ifdef ESP8266 + const uint32_t numParticles = 150; // maximum number of particles +#else + const uint32_t numParticles = 500; // maximum number of particles +#endif - const uint32_t numParticles = 400; const uint8_t numSprays = 8; // maximum number of sprays PSparticle *particles; @@ -7922,7 +7927,7 @@ uint16_t mode_particlerotatingspray(void) spray[i].source.hue = random8(); spray[i].source.sat = 255; // set saturation spray[i].source.x = (cols * PS_P_RADIUS) / 2; // center - spray[i].source.y = (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; @@ -8192,7 +8197,7 @@ static const char _data_FX_MODE_PARTICLEFIREWORKS[] PROGMEM = "Particle Firework * by DedeHai (Damian Schneider) */ -uint16_t mode_particlespray(void) +uint16_t mode_particlevolcano(void) { if (SEGLEN == 1) @@ -8322,7 +8327,7 @@ uint16_t mode_particlespray(void) return FRAMETIME; } -static const char _data_FX_MODE_PARTICLESPRAY[] PROGMEM = "Particle Volcano@Moving Speed,Intensity,Particle Speed,Bouncyness,Nozzle Size,Color by Age,Bounce X,Collisions;;!;012;pal=35,sx=0,ix=160,c1=100,c2=160,c3=10,o1=1,o2=1,o3=1"; +static const char _data_FX_MODE_PARTICLEVOLCANO[] PROGMEM = "Particle Volcano@Moving Speed,Intensity,Particle Speed,Bouncyness,Nozzle Size,Color by Age,Bounce X,Collisions;;!;012;pal=35,sx=0,ix=160,c1=100,c2=160,c3=10,o1=1,o2=1,o3=1"; // good default values for sliders: 100,200,190, 45 /*syntax for json configuration string: @@ -9314,7 +9319,123 @@ uint16_t mode_particleattractor(void) static const char _data_FX_MODE_PARTICLEATTRACTOR[] PROGMEM = "Particle Attractor@Center Mass,Particles,Emit Speed,Collision Strength,Friction,Bounce,Trails,Swallow;;!;012;pal=9,sx=100,ix=82,c1=190,c2=210,o1=0,o2=0,o3=0"; -//TODO: animation idea: just one spray, sliders set x position, y position, speed, intensity and spray angle. ticks set wrap, bounce, gravity? evtl noch life time koppeln mit speed? + + +uint16_t mode_particlespray(void) +{ + + if (SEGLEN == 1) + return mode_static(); + + const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; + const uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); + // particle system x dimension + const uint16_t PS_MAX_X = (cols * PS_P_RADIUS - 1); + const uint16_t PS_MAX_Y = (rows * PS_P_RADIUS - 1); + + const uint32_t numParticles = 450; + const uint8_t numSprays = 1; + uint8_t percycle = numSprays; // maximum number of particles emitted per cycle + + PSparticle *particles; + PSpointsource *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); + if (!SEGENV.allocateData(dataSize)) + return mode_static(); // allocation failed; //allocation failed + + // DEBUG_PRINT(F("particle datasize = ")); + // DEBUG_PRINTLN(dataSize); + + 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 + + uint32_t i = 0; + uint32_t j = 0; + + if (SEGMENT.call == 0) // initialization + { + for (i = 0; i < numParticles; i++) + { + particles[i].ttl = 0; + } + for (i = 0; i < numSprays; i++) + { + spray[i].source.hue = random8(); + spray[i].source.sat = 255; // set full saturation + spray[i].source.x = (cols * PS_P_RADIUS) / (numSprays + 1) * (i + 1); + spray[i].source.y = 5; // just above the lower edge, if zero, particles already 'bounce' at start and loose speed. + spray[i].source.vx = 0; + spray[i].maxLife = 300; // lifetime in frames + spray[i].minLife = 20; + spray[i].source.collide = true; // seeded particles will collide + spray[i].vx = 0; // emitting speed + spray[i].vy = 0; // emitting speed + spray[i].var = 10; + } + // SEGMENT.palette = 35; //fire palette + } + + // change source emitting color from time to time + if (SEGMENT.call % (11 - (SEGMENT.intensity / 25)) == 0) // every nth frame, cycle color and emit particles + { + for (i = 0; i < numSprays; i++) + { + spray[i].source.hue++; // = random8(); //change hue of spray source + //spray[i].var = SEGMENT.custom3; // emiting variation = nozzle size (custom 3 goes from 0-32) + spray[i].source.x = map(SEGMENT.custom1,0,255,0,PS_MAX_X); + spray[i].source.y = map(SEGMENT.custom2,0,255,0,PS_MAX_Y); + } + + i = 0; + j = 0; + for (i = 0; i < numParticles; i++) + { + if (particles[i].ttl == 0) // find a dead particle + { + // spray[j].source.hue = random8(); //set random color for each particle (using palette) + Emitter_Angle_emit(&spray[j], &particles[i], SEGMENT.custom3<<3, SEGMENT.speed>>2); + j = (j + 1) % numSprays; + if (percycle-- == 0) + { + break; // quit loop if all particles of this round emitted + } + } + } + } + + uint8_t hardness = 200; + + if (SEGMENT.check3) // collisions enabled + detectCollisions(particles, numParticles, hardness); + + + for (i = 0; i < numParticles; i++) + { + //particles[i].hue = min((uint16_t)220, particles[i].ttl); + if (SEGMENT.check1) //use gravity + Particle_Gravity_update(&particles[i], SEGMENT.check2, SEGMENT.check2 == 0, true, hardness); + else //bounce particles + { + if(SEGMENT.check2) //wrap x + Particle_Wrap_update(&particles[i], true, false); + else //bounce + Particle_Bounce_update(&particles[i], hardness); + } + } + + SEGMENT.fill(BLACK); // clear the matrix + + // render the particles + ParticleSys_render(particles, numParticles, SEGMENT.check2, false); + + return FRAMETIME; +} +static const char _data_FX_MODE_PARTICLESPRAY[] PROGMEM = "Particle Spray@Particle Speed,Intensity,X Position,Y Position,Angle,Gravity,WrapX/Bounce,Collisions;;!;012;pal=0,sx=180,ix=200,c1=220,c2=30,c3=12,o1=1,o2=0,o3=1"; +// TODO: animation idea: just one spray, sliders set x position, y position, speed, intensity and spray angle. ticks set wrap/bounce, gravity? evtl noch life time koppeln mit speed? #endif // WLED_DISABLE_2D @@ -9558,7 +9679,7 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_2DAKEMI, &mode_2DAkemi, _data_FX_MODE_2DAKEMI); // audio - addEffect(FX_MODE_PARTICLESPRAY, &mode_particlespray, _data_FX_MODE_PARTICLESPRAY); + 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); @@ -9569,7 +9690,8 @@ void WS2812FX::setupEffectData() { //experimental addEffect(FX_MODE_PARTICLEWATERFALL, &mode_particlewaterfall, _data_FX_MODE_PARTICLEWATERFALL); addEffect(FX_MODE_PARTICLEIMPACT, &mode_particleimpact, _data_FX_MODE_PARTICLEIMPACT); - addEffect(FX_MODE_PARTICLEATTRACTOR, &mode_particleattractor, _data_FX_MODE_PARTICLEATTRACTOR); + addEffect(FX_MODE_PARTICLEATTRACTOR, &mode_particleattractor, _data_FX_MODE_PARTICLEATTRACTOR); + addEffect(FX_MODE_PARTICLESPRAY, &mode_particlespray, _data_FX_MODE_PARTICLESPRAY); #endif // WLED_DISABLE_2D diff --git a/wled00/FX.h b/wled00/FX.h index 4b4698e3b9..d35c29ea17 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -318,7 +318,7 @@ #define FX_MODE_WAVESINS 184 #define FX_MODE_ROCKTAVES 185 #define FX_MODE_2DAKEMI 186 -#define FX_MODE_PARTICLESPRAY 187 +#define FX_MODE_PARTICLEVOLCANO 187 #define FX_MODE_PARTICLEFIRE 188 #define FX_MODE_PARTICLEFIREWORKS 189 #define FX_MODE_PARTICLEROTATINGSPRAY 190 @@ -328,7 +328,8 @@ #define FX_MODE_PARTICLEATTRACTOR 194 #define FX_MODE_PARTICLEIMPACT 195 #define FX_MODE_PARTICLEWATERFALL 196 -#define MODE_COUNT 197 +#define FX_MODE_PARTICLESPRAY 197 +#define MODE_COUNT 198 typedef enum mapping1D2D { M12_Pixels = 0, diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index 4201e59b60..fdd488e98a 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -237,6 +237,55 @@ void Particle_Bounce_update(PSparticle *part, const uint8_t hardness) // bounces } } +void Particle_Wrap_update(PSparticle *part, bool wrapX, bool wrapY) // particle moves, decays and dies (age or out of matrix), if wrap is set, pixels leaving the matrix reappear on other side +{ + // 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--; + + // 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 + { + 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 + { + part->outofbounds = 1; + } + part->y = newY; // set new position + } + +} + void Particle_Gravity_update(PSparticle *part, bool wrapX, bool bounceX, bool bounceY, const uint8_t hardness) // particle moves, decays and dies (age or out of matrix), 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) { diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index e785c5f3f8..74a12c2580 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -89,6 +89,7 @@ void Emitter_Angle_emit(PSpointsource *emitter, PSparticle *part, uint8_t angle, void Particle_attractor(PSparticle *particle, PSparticle *attractor, uint8_t *counter, uint8_t strength, bool swallow); void Particle_Move_update(PSparticle *part); void Particle_Bounce_update(PSparticle *part, const uint8_t hardness); +void Particle_Wrap_update(PSparticle *part, bool wrapX, bool wrapY); 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, bool wrapX, bool WrapY); From 09041551862361cd8c7444ca25403f5f96a9077b Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sun, 18 Feb 2024 15:52:36 +0100 Subject: [PATCH 029/219] fixed touch buttons for ESP32 S2 and S3 touch is implemented differently on S2 and S3, these changes make touch buttons work on S2 and S3 --- wled00/button.cpp | 27 +++++++++++++++++++++++---- wled00/cfg.cpp | 13 +++++++++++-- wled00/fcn_declare.h | 1 + 3 files changed, 35 insertions(+), 6 deletions(-) diff --git a/wled00/button.cpp b/wled00/button.cpp index 29cb0abebf..bf093a2071 100644 --- a/wled00/button.cpp +++ b/wled00/button.cpp @@ -99,11 +99,21 @@ bool isButtonPressed(uint8_t i) case BTN_TYPE_TOUCH: case BTN_TYPE_TOUCH_SWITCH: #if defined(ARDUINO_ARCH_ESP32) && !defined(CONFIG_IDF_TARGET_ESP32C3) - if (digitalPinToTouchChannel(btnPin[i]) >= 0 && touchRead(pin) <= touchThreshold) return true; + #ifdef SOC_TOUCH_VERSION_2 //ESP32 S2 and S3 provide a function to check touch state (state is updated in interrupt) + if (touchInterruptGetLastStatus(pin)) + { + return true; + } + #else + if (digitalPinToTouchChannel(btnPin[i]) >= 0 && touchRead(pin) <= touchThreshold) + { + return true; + } + #endif #endif - break; - } - return false; + break; + } + return false; } void handleSwitch(uint8_t b) @@ -406,3 +416,12 @@ void handleIO() offMode = true; } } + +void IRAM_ATTR touchButtonISR() +{ + +#if defined SOC_TOUCH_VERSION_1 //ESP32 original + touchInterruptSetThresholdDirection(flase); //todo: need to flip direction, for that proably need to read current state or something. +#endif + // For S2 and S3: nothing to do, ISR is just used to update registers of HAL driver +} \ No newline at end of file diff --git a/wled00/cfg.cpp b/wled00/cfg.cpp index 771ada25b3..82fb911a4c 100755 --- a/wled00/cfg.cpp +++ b/wled00/cfg.cpp @@ -228,6 +228,7 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { // read multiple button configuration JsonObject btn_obj = hw["btn"]; + CJSON(touchThreshold, btn_obj[F("tt")]); bool pull = btn_obj[F("pull")] | (!disablePullUp); // if true, pullup is enabled disablePullUp = !pull; JsonArray hw_btn_ins = btn_obj["ins"]; @@ -252,8 +253,16 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { btnPin[s] = -1; pinManager.deallocatePin(pin,PinOwner::Button); } + //if touch pin, enable the touch interrupt on ESP32 S2 & S3 + #ifdef SOC_TOUCH_VERSION_2 // ESP32 S2 and S3 have a fucntion to check touch state but need to attach an interrupt to do so + else if ((buttonType[s] == BTN_TYPE_TOUCH || buttonType[s] == BTN_TYPE_TOUCH_SWITCH)) + { + touchAttachInterrupt(btnPin[s], touchButtonISR, touchThreshold<<2); //threshold on Touch V2 is much higher (TODO: may need shift by 3 if very noisy) + } + #endif + else - #endif + #endif { if (disablePullUp) { pinMode(btnPin[s], INPUT); @@ -299,7 +308,7 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { } } } - CJSON(touchThreshold,btn_obj[F("tt")]); + CJSON(buttonPublishMqtt,btn_obj["mqtt"]); int hw_ir_pin = hw["ir"]["pin"] | -2; // 4 diff --git a/wled00/fcn_declare.h b/wled00/fcn_declare.h index 47a8173cce..c902dad64f 100644 --- a/wled00/fcn_declare.h +++ b/wled00/fcn_declare.h @@ -20,6 +20,7 @@ void doublePressAction(uint8_t b=0); bool isButtonPressed(uint8_t b=0); void handleButton(); void handleIO(); +void IRAM_ATTR touchButtonISR(); //cfg.cpp bool deserializeConfig(JsonObject doc, bool fromFS = false); From d21ad8e7d1651a96f879d43ef1146a72a6ed8271 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Wed, 21 Feb 2024 18:38:34 +0100 Subject: [PATCH 030/219] some tuning for touch buttons on S2/S3 now better fits the default threshold value of 32 --- wled00/button.cpp | 17 ++++++++--------- wled00/cfg.cpp | 2 +- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/wled00/button.cpp b/wled00/button.cpp index bf093a2071..05c6ad4bbc 100644 --- a/wled00/button.cpp +++ b/wled00/button.cpp @@ -100,10 +100,8 @@ bool isButtonPressed(uint8_t i) case BTN_TYPE_TOUCH_SWITCH: #if defined(ARDUINO_ARCH_ESP32) && !defined(CONFIG_IDF_TARGET_ESP32C3) #ifdef SOC_TOUCH_VERSION_2 //ESP32 S2 and S3 provide a function to check touch state (state is updated in interrupt) - if (touchInterruptGetLastStatus(pin)) - { - return true; - } + if (touchInterruptGetLastStatus(pin)) + return true; #else if (digitalPinToTouchChannel(btnPin[i]) >= 0 && touchRead(pin) <= touchThreshold) { @@ -419,9 +417,10 @@ void handleIO() void IRAM_ATTR touchButtonISR() { - -#if defined SOC_TOUCH_VERSION_1 //ESP32 original - touchInterruptSetThresholdDirection(flase); //todo: need to flip direction, for that proably need to read current state or something. -#endif - // For S2 and S3: nothing to do, ISR is just used to update registers of HAL driver + // used for ESP32 S2 and S3: nothing to do, ISR is just used to update registers of HAL driver + // asm volatile("nop" ::); //prevent compiler to remove this function (TODO: is this really needed? probably not) + + // #if defined SOC_TOUCH_VERSION_1 //ESP32 original -> unused + // touchInterruptSetThresholdDirection(false); //todo: need to flip direction, for that proably need to read current state or something. + // #endif } \ No newline at end of file diff --git a/wled00/cfg.cpp b/wled00/cfg.cpp index 82fb911a4c..416497f2f1 100755 --- a/wled00/cfg.cpp +++ b/wled00/cfg.cpp @@ -257,7 +257,7 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { #ifdef SOC_TOUCH_VERSION_2 // ESP32 S2 and S3 have a fucntion to check touch state but need to attach an interrupt to do so else if ((buttonType[s] == BTN_TYPE_TOUCH || buttonType[s] == BTN_TYPE_TOUCH_SWITCH)) { - touchAttachInterrupt(btnPin[s], touchButtonISR, touchThreshold<<2); //threshold on Touch V2 is much higher (TODO: may need shift by 3 if very noisy) + touchAttachInterrupt(btnPin[s], touchButtonISR, touchThreshold<<4); //threshold on Touch V2 is much higher (1500 is a value given by Espressif example) } #endif From 1a7ef9b0ca7063abd42846b3346d54e5947302d9 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Wed, 21 Feb 2024 20:26:17 +0100 Subject: [PATCH 031/219] updated rotating particle spray with more user options --- wled00/FX.cpp | 42 +++++++++++++++++++++++++++---------- wled00/FXparticleSystem.cpp | 18 ++++++++++++---- wled00/FXparticleSystem.h | 4 ++-- 3 files changed, 47 insertions(+), 17 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 303cfb451d..be71c1091a 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -7893,7 +7893,7 @@ uint16_t mode_particlerotatingspray(void) #ifdef ESP8266 const uint32_t numParticles = 150; // maximum number of particles #else - const uint32_t numParticles = 500; // maximum number of particles + const uint32_t numParticles = 700; // maximum number of particles #endif const uint8_t numSprays = 8; // maximum number of sprays @@ -7901,7 +7901,7 @@ uint16_t mode_particlerotatingspray(void) PSparticle *particles; PSpointsource *spray; - // allocate memory and divide it into proper pointers, max is 32kB for all segments. + // 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)) @@ -7918,13 +7918,14 @@ uint16_t mode_particlerotatingspray(void) if (SEGMENT.call == 0) // initialization { SEGMENT.aux0 = 0; // starting angle + SEGMENT.aux1 = 0xFF; // user check for (i = 0; i < numParticles; i++) { particles[i].ttl = 0; } for (i = 0; i < numSprays; i++) { - spray[i].source.hue = random8(); + spray[i].source.hue = random8(); //TODO: how to keep track of options? can use SEGMENT.aux1: change hue to random or rainbow depending on check but need to find out when it changed. 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 @@ -7939,12 +7940,28 @@ uint16_t mode_particlerotatingspray(void) } // change source emitting color from time to time - if (SEGMENT.call % ((263 - SEGMENT.intensity) >> 3) == 0) // every nth frame, cycle color + if (SEGMENT.call % ((263 - SEGMENT.intensity) >> 3) == 0) // every nth frame, cycle color and update hue if necessary { for (i = 0; i < spraycount; i++) - { + { spray[i].source.hue++; // = random8(); //change hue of spray source } + if (SEGMENT.check1 != SEGMENT.aux1) + { + SEGMENT.aux1 = SEGMENT.check1; + for (i = 0; i < spraycount; i++) + { + if (SEGMENT.check1) // random color is checked + { + spray[i].source.hue = random8(); + } + else + { + uint8_t coloroffset = 0xFF / spraycount; + spray[i].source.hue = coloroffset * i; + } + } + } } uint8_t percycle = spraycount; // maximum number of particles emitted per cycle @@ -7970,17 +7987,20 @@ uint16_t mode_particlerotatingspray(void) for (i = 0; i < spraycount; i++) { - SEGMENT.aux0 += SEGMENT.speed << 2; - + if (SEGMENT.check2) + SEGMENT.aux0 += SEGMENT.speed << 2; + else + SEGMENT.aux0 -= SEGMENT.speed << 2; + // 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) / ((257 - SEGMENT.custom1) >> 1); // update spray angle (rotate all sprays with angle offset) - spray[i].vy = (sin16(SEGMENT.aux0 + angleoffset * i) >> 8) / ((257 - SEGMENT.custom1) >> 1); // update spray angle (rotate all sprays with angle offset) + spray[i].vx = (cos16(SEGMENT.aux0 + angleoffset * i) >> 8) / ((263 - SEGMENT.custom1) >> 3); // update spray angle (rotate all sprays with angle offset) + spray[i].vy = (sin16(SEGMENT.aux0 + angleoffset * i) >> 8) / ((263 - SEGMENT.custom1) >> 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) } for (i = 0; i < numParticles; i++) { - Particle_Move_update(&particles[i]); // move the particles + Particle_Move_update(&particles[i], true); // move the particles, kill out of bounds particles } SEGMENT.fill(BLACK); // clear the matrix @@ -7990,7 +8010,7 @@ uint16_t mode_particlerotatingspray(void) return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEROTATINGSPRAY[] PROGMEM = "Rotating Particle Spray@Rotation Speed,Color Change,Particle Speed,Spray Count,Nozzle Size;;!;012;pal=6,sx=39,ix=178,c1=225,c2=128,c3=0"; +static const char _data_FX_MODE_PARTICLEROTATINGSPRAY[] PROGMEM = "Rotating Particle Spray@Rotation Speed,Color Change,Particle Speed,Spray Count,Nozzle Size,Random Color, Direction;;!;012;pal=6,sx=39,ix=178,c1=225,c2=128,c3=0"; /* * Particle Fireworks diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index fdd488e98a..4355d5343b 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -163,7 +163,7 @@ void Particle_attractor(PSparticle *particle, PSparticle *attractor, uint8_t *co // TODO: could solve all update functions in a single function with parameters and handle gravity acceleration in a separte function (uses more cpu time but that is not a huge issue) or maybe not, like this, different preferences can be set -void Particle_Move_update(PSparticle *part) // particle moves, decays and dies +void Particle_Move_update(PSparticle *part, bool killoutofbounds) // particle moves, decays and dies, if killoutofbounds is set, out of bounds particles are set to ttl=0 { // Matrix dimension const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; @@ -187,13 +187,20 @@ void Particle_Move_update(PSparticle *part) // particle moves, decays and dies // check if particle is out of bounds if ((part->y <= 0) || (part->y >= PS_MAX_Y)) { - part->outofbounds = 1; + if (killoutofbounds) + part->ttl = 0; + else + part->outofbounds = 1; } if ((part->x <= 0) || (part->x >= PS_MAX_X)) { - part->outofbounds = 1; + if(killoutofbounds) + part->ttl = 0; + else + part->outofbounds = 1; } } + } void Particle_Bounce_update(PSparticle *part, const uint8_t hardness) // bounces a particle on the matrix edges, if surface 'hardness' is <255 some energy will be lost in collision (127 means 50% lost) @@ -211,6 +218,8 @@ void Particle_Bounce_update(PSparticle *part, const uint8_t hardness) // bounces // age part->ttl--; + part->outofbounds = 0; // reset out of bounds (particles are never out of bounds) + // apply velocity int16_t newX, newY; @@ -235,6 +244,7 @@ void Particle_Bounce_update(PSparticle *part, const uint8_t hardness) // bounces part->x = min(newX, (int16_t)PS_MAX_X); // limit to matrix boundaries part->y = min(newY, (int16_t)PS_MAX_Y); } + } void Particle_Wrap_update(PSparticle *part, bool wrapX, bool wrapY) // particle moves, decays and dies (age or out of matrix), if wrap is set, pixels leaving the matrix reappear on other side @@ -523,7 +533,7 @@ void ParticleSys_render(PSparticle *particles, uint32_t numParticles, bool wrapX } // update & move particle using simple particles, wraps around left/right if wrapX is true, wrap around up/down if wrapY is true -void FireParticle_update(PSparticle *part, bool wrapX = false, bool wrapY = false) +void FireParticle_update(PSparticle *part, bool wrapX, bool wrapY) { // Matrix dimension const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index 74a12c2580..e626aa03d7 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -87,12 +87,12 @@ 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); +void Particle_Move_update(PSparticle *part, bool killoutofbounds = false); void Particle_Bounce_update(PSparticle *part, const uint8_t hardness); void Particle_Wrap_update(PSparticle *part, bool wrapX, bool wrapY); 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, bool wrapX, bool WrapY); +void FireParticle_update(PSparticle *part, bool wrapX = false, bool WrapY = false); void ParticleSys_renderParticleFire(PSparticle *particles, uint32_t numParticles, bool wrapX); void PartMatrix_addHeat(uint8_t col, uint8_t row, uint16_t heat); void detectCollisions(PSparticle *particles, uint32_t numparticles, uint8_t hardness); From 6740cb69bc61caabdad40451b47744738aced706 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Thu, 22 Feb 2024 19:16:42 +0100 Subject: [PATCH 032/219] chaned rotating spray default parameters --- wled00/FX.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index be71c1091a..0316a3c8fc 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -8010,7 +8010,7 @@ uint16_t mode_particlerotatingspray(void) return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEROTATINGSPRAY[] PROGMEM = "Rotating Particle Spray@Rotation Speed,Color Change,Particle Speed,Spray Count,Nozzle Size,Random Color, Direction;;!;012;pal=6,sx=39,ix=178,c1=225,c2=128,c3=0"; +static const char _data_FX_MODE_PARTICLEROTATINGSPRAY[] PROGMEM = "Rotating Particle Spray@Rotation Speed,Color Change,Particle Speed,Spray Count,Nozzle Size,Random Color, Direction;;!;012;pal=56,sx=18,ix=222,c1=190,c2=200,c3=0,o1=0,o2=0"; /* * Particle Fireworks From d01a1511cb4f9d2400541e2e8e0d0a8f20cfbe8c Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Mon, 26 Feb 2024 18:20:07 +0100 Subject: [PATCH 033/219] add todo --- wled00/FX.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 0316a3c8fc..4f3d620272 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -8884,6 +8884,7 @@ uint16_t mode_particlebox(void) scale = 50; // force is limited at lower angles angle = (angle * (int16_t)SEGMENT.custom1) >> 8; // scale angle range according to slider (tilt strength) angle -= 63; // make 'down' (or -90°) the zero position + //TODO: need to debug this, angle is not symmetrical around 'down' // now calculate the force vectors xgravity = ((int16_t)cos8((uint8_t)angle)) - 128; // gravity direction +/- 127 ygravity = ((int16_t)sin8((uint8_t)angle)) - 128; From 62a975d687841189e4ef24b5b154f3a3a7fc6779 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Fri, 8 Mar 2024 18:14:52 +0100 Subject: [PATCH 034/219] Revert "some tuning for touch buttons on S2/S3" This reverts commit d21ad8e7d1651a96f879d43ef1146a72a6ed8271. --- wled00/button.cpp | 17 +++++++++-------- wled00/cfg.cpp | 2 +- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/wled00/button.cpp b/wled00/button.cpp index 05c6ad4bbc..bf093a2071 100644 --- a/wled00/button.cpp +++ b/wled00/button.cpp @@ -100,8 +100,10 @@ bool isButtonPressed(uint8_t i) case BTN_TYPE_TOUCH_SWITCH: #if defined(ARDUINO_ARCH_ESP32) && !defined(CONFIG_IDF_TARGET_ESP32C3) #ifdef SOC_TOUCH_VERSION_2 //ESP32 S2 and S3 provide a function to check touch state (state is updated in interrupt) - if (touchInterruptGetLastStatus(pin)) - return true; + if (touchInterruptGetLastStatus(pin)) + { + return true; + } #else if (digitalPinToTouchChannel(btnPin[i]) >= 0 && touchRead(pin) <= touchThreshold) { @@ -417,10 +419,9 @@ void handleIO() void IRAM_ATTR touchButtonISR() { - // used for ESP32 S2 and S3: nothing to do, ISR is just used to update registers of HAL driver - // asm volatile("nop" ::); //prevent compiler to remove this function (TODO: is this really needed? probably not) - - // #if defined SOC_TOUCH_VERSION_1 //ESP32 original -> unused - // touchInterruptSetThresholdDirection(false); //todo: need to flip direction, for that proably need to read current state or something. - // #endif + +#if defined SOC_TOUCH_VERSION_1 //ESP32 original + touchInterruptSetThresholdDirection(flase); //todo: need to flip direction, for that proably need to read current state or something. +#endif + // For S2 and S3: nothing to do, ISR is just used to update registers of HAL driver } \ No newline at end of file diff --git a/wled00/cfg.cpp b/wled00/cfg.cpp index 416497f2f1..82fb911a4c 100755 --- a/wled00/cfg.cpp +++ b/wled00/cfg.cpp @@ -257,7 +257,7 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { #ifdef SOC_TOUCH_VERSION_2 // ESP32 S2 and S3 have a fucntion to check touch state but need to attach an interrupt to do so else if ((buttonType[s] == BTN_TYPE_TOUCH || buttonType[s] == BTN_TYPE_TOUCH_SWITCH)) { - touchAttachInterrupt(btnPin[s], touchButtonISR, touchThreshold<<4); //threshold on Touch V2 is much higher (1500 is a value given by Espressif example) + touchAttachInterrupt(btnPin[s], touchButtonISR, touchThreshold<<2); //threshold on Touch V2 is much higher (TODO: may need shift by 3 if very noisy) } #endif From 66ac5ac81b6ee25a39482bbe31e9199e8e51ec5a Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Fri, 8 Mar 2024 18:14:59 +0100 Subject: [PATCH 035/219] Revert "fixed touch buttons for ESP32 S2 and S3" This reverts commit 09041551862361cd8c7444ca25403f5f96a9077b. --- wled00/button.cpp | 27 ++++----------------------- wled00/cfg.cpp | 13 ++----------- wled00/fcn_declare.h | 1 - 3 files changed, 6 insertions(+), 35 deletions(-) diff --git a/wled00/button.cpp b/wled00/button.cpp index bf093a2071..29cb0abebf 100644 --- a/wled00/button.cpp +++ b/wled00/button.cpp @@ -99,21 +99,11 @@ bool isButtonPressed(uint8_t i) case BTN_TYPE_TOUCH: case BTN_TYPE_TOUCH_SWITCH: #if defined(ARDUINO_ARCH_ESP32) && !defined(CONFIG_IDF_TARGET_ESP32C3) - #ifdef SOC_TOUCH_VERSION_2 //ESP32 S2 and S3 provide a function to check touch state (state is updated in interrupt) - if (touchInterruptGetLastStatus(pin)) - { - return true; - } - #else - if (digitalPinToTouchChannel(btnPin[i]) >= 0 && touchRead(pin) <= touchThreshold) - { - return true; - } - #endif + if (digitalPinToTouchChannel(btnPin[i]) >= 0 && touchRead(pin) <= touchThreshold) return true; #endif - break; - } - return false; + break; + } + return false; } void handleSwitch(uint8_t b) @@ -416,12 +406,3 @@ void handleIO() offMode = true; } } - -void IRAM_ATTR touchButtonISR() -{ - -#if defined SOC_TOUCH_VERSION_1 //ESP32 original - touchInterruptSetThresholdDirection(flase); //todo: need to flip direction, for that proably need to read current state or something. -#endif - // For S2 and S3: nothing to do, ISR is just used to update registers of HAL driver -} \ No newline at end of file diff --git a/wled00/cfg.cpp b/wled00/cfg.cpp index 82fb911a4c..771ada25b3 100755 --- a/wled00/cfg.cpp +++ b/wled00/cfg.cpp @@ -228,7 +228,6 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { // read multiple button configuration JsonObject btn_obj = hw["btn"]; - CJSON(touchThreshold, btn_obj[F("tt")]); bool pull = btn_obj[F("pull")] | (!disablePullUp); // if true, pullup is enabled disablePullUp = !pull; JsonArray hw_btn_ins = btn_obj["ins"]; @@ -253,16 +252,8 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { btnPin[s] = -1; pinManager.deallocatePin(pin,PinOwner::Button); } - //if touch pin, enable the touch interrupt on ESP32 S2 & S3 - #ifdef SOC_TOUCH_VERSION_2 // ESP32 S2 and S3 have a fucntion to check touch state but need to attach an interrupt to do so - else if ((buttonType[s] == BTN_TYPE_TOUCH || buttonType[s] == BTN_TYPE_TOUCH_SWITCH)) - { - touchAttachInterrupt(btnPin[s], touchButtonISR, touchThreshold<<2); //threshold on Touch V2 is much higher (TODO: may need shift by 3 if very noisy) - } - #endif - else - #endif + #endif { if (disablePullUp) { pinMode(btnPin[s], INPUT); @@ -308,7 +299,7 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { } } } - + CJSON(touchThreshold,btn_obj[F("tt")]); CJSON(buttonPublishMqtt,btn_obj["mqtt"]); int hw_ir_pin = hw["ir"]["pin"] | -2; // 4 diff --git a/wled00/fcn_declare.h b/wled00/fcn_declare.h index c902dad64f..47a8173cce 100644 --- a/wled00/fcn_declare.h +++ b/wled00/fcn_declare.h @@ -20,7 +20,6 @@ void doublePressAction(uint8_t b=0); bool isButtonPressed(uint8_t b=0); void handleButton(); void handleIO(); -void IRAM_ATTR touchButtonISR(); //cfg.cpp bool deserializeConfig(JsonObject doc, bool fromFS = false); From b99a62feff45e4769ba579ceb0f7474b299dfeb3 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Fri, 8 Mar 2024 19:23:52 +0100 Subject: [PATCH 036/219] removed comments, added comments --- wled00/FXparticleSystem.cpp | 54 +++++++++++++++++-------------------- 1 file changed, 25 insertions(+), 29 deletions(-) diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index 4355d5343b..cd92d46ac3 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -70,7 +70,7 @@ void Emitter_Angle_emit(PSpointsource *emitter, PSparticle *part, uint8_t angle, Emitter_Fountain_emit(emitter, part); } // 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) // todo: add a parameter 'swallow' so the attractor can 'suck up' particles that are very close, also could use hue of attractor particle for strength +void Particle_attractor(PSparticle *particle, PSparticle *attractor, uint8_t *counter, uint8_t strength, bool swallow) { // Calculate the distance between the particle and the attractor int dx = attractor->x - particle->x; @@ -138,7 +138,6 @@ void Particle_attractor(PSparticle *particle, PSparticle *attractor, uint8_t *co if (ycounter > 15) { - ycounter -= 15; *counter |= (ycounter << 4) & 0xF0; // write upper four bits @@ -161,9 +160,8 @@ void Particle_attractor(PSparticle *particle, PSparticle *attractor, uint8_t *co // TODO: need to limit the max speed? } -// TODO: could solve all update functions in a single function with parameters and handle gravity acceleration in a separte function (uses more cpu time but that is not a huge issue) or maybe not, like this, different preferences can be set - -void Particle_Move_update(PSparticle *part, bool killoutofbounds) // particle moves, decays and dies, if killoutofbounds is set, out of bounds particles are set to ttl=0 +// 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) { // Matrix dimension const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; @@ -203,7 +201,8 @@ void Particle_Move_update(PSparticle *part, bool killoutofbounds) // particle mo } -void Particle_Bounce_update(PSparticle *part, const uint8_t hardness) // bounces a particle on the matrix edges, if surface 'hardness' is <255 some energy will be lost in collision (127 means 50% lost) +// 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) { // Matrix dimension const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; @@ -247,7 +246,9 @@ void Particle_Bounce_update(PSparticle *part, const uint8_t hardness) // bounces } -void Particle_Wrap_update(PSparticle *part, bool wrapX, bool wrapY) // particle moves, decays and dies (age or out of matrix), if wrap is set, pixels leaving the matrix reappear on other side +// particle moves, decays and dies (age or out of matrix), if wrap is set, pixels leaving the matrix reappear on other side +//TODO: this is just move update with wrap, could make one function out of it +void Particle_Wrap_update(PSparticle *part, bool wrapX, bool wrapY) { // Matrix dimension const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; @@ -296,9 +297,9 @@ void Particle_Wrap_update(PSparticle *part, bool wrapX, bool wrapY) // particle } -void Particle_Gravity_update(PSparticle *part, bool wrapX, bool bounceX, bool bounceY, const uint8_t hardness) // particle moves, decays and dies (age or out of matrix), 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) +// 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 uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; const uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); @@ -388,7 +389,6 @@ void Particle_Gravity_update(PSparticle *part, bool wrapX, bool bounceX, bool bo // 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 -// saturation is color saturation, if not set to 255, hsv instead of palette is used (palette does not support saturation) void ParticleSys_render(PSparticle *particles, uint32_t numParticles, bool wrapX, bool wrapY) { #ifdef ESP8266 @@ -532,7 +532,8 @@ void ParticleSys_render(PSparticle *particles, uint32_t numParticles, bool wrapX } } -// update & move particle using simple particles, wraps around left/right if wrapX is true, wrap around up/down if wrapY is true +// update & move particle, wraps around left/right if wrapX is true, wrap around up/down if wrapY is true +// particles move upwards faster if ttl is high (i.e. they are hotter) void FireParticle_update(PSparticle *part, bool wrapX, bool wrapY) { // Matrix dimension @@ -585,7 +586,7 @@ void FireParticle_update(PSparticle *part, bool wrapX, bool wrapY) } } -// render simple particles to the LED buffer using heat to color +// 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) { @@ -607,6 +608,8 @@ void ParticleSys_renderParticleFire(PSparticle *particles, uint32_t numParticles continue; } + //TODO: no more using simple particles, can use the out of bounds here + // simple particles do not have 'out of bound' parameter, need to check if particle is within matrix boundaries dx = (uint8_t)((uint16_t)particles[i].x % (uint16_t)PS_P_RADIUS); dy = (uint8_t)((uint16_t)particles[i].y % (uint16_t)PS_P_RADIUS); @@ -614,12 +617,6 @@ void ParticleSys_renderParticleFire(PSparticle *particles, uint32_t numParticles x = (uint8_t)((uint16_t)particles[i].x / (uint16_t)PS_P_RADIUS); // compiler should optimize to bit shift y = (uint8_t)((uint16_t)particles[i].y / (uint16_t)PS_P_RADIUS); - // for x=1, y=1: starts out with all four pixels at the same color (32/32) - // moves to upper right pixel (64/64) - // then moves one physical pixel up and right(+1/+1), starts out now with - // lower left pixel fully bright (0/0) and moves to all four pixel at same - // color (32/32) - if (dx < (PS_P_RADIUS >> 1)) // jump to next physical pixel if half of virtual pixel size is reached { x--; // shift left @@ -650,7 +647,7 @@ void ParticleSys_renderParticleFire(PSparticle *particles, uint32_t numParticles } } - // calculate brightness values for all four pixels representing a particle using linear interpolation + // calculate brightness values for all six pixels representing a particle using linear interpolation // bottom left if (x < cols && y < rows) { @@ -697,7 +694,7 @@ void ParticleSys_renderParticleFire(PSparticle *particles, uint32_t numParticles } } -// adds 'heat' to red color channel, if it overflows, add it to green, if that overflows add it to blue +// adds 'heat' to red color channel, if it overflows, add it to next color channel void PartMatrix_addHeat(uint8_t col, uint8_t row, uint16_t heat) { @@ -769,24 +766,23 @@ void PartMatrix_addHeat(uint8_t col, uint8_t row, uint16_t heat) SEGMENT.setPixelColorXY(col, rows - row - 1, currentcolor); } -/*detect collisions in an array of particles and handle them*/ +// detect collisions in an array of particles and handle them void detectCollisions(PSparticle* particles, uint32_t numparticles, uint8_t hardness) { // detect and handle collisions uint32_t i,j; int32_t startparticle = 0; - int32_t endparticle = numparticles >> 1; // do half the particles + int32_t endparticle = numparticles >> 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 which is overkill) + { // 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) startparticle = endparticle; endparticle = numparticles; } for (i = startparticle; i < endparticle; i++) { - // go though all 'higher number' particles and see if any of those are in close proximity - // if they are, make them collide + // 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 { int32_t dx, dy; // distance to other particles @@ -808,6 +804,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) void handleCollision(PSparticle *particle1, PSparticle *particle2, const uint8_t hardness) @@ -866,9 +863,9 @@ void handleCollision(PSparticle *particle1, PSparticle *particle2, const uint8_t particle2->vx = (particle2->vx < 2 && particle2->vx > -2) ? 0 : particle2->vx; particle2->vy = (particle2->vy < 2 && particle2->vy > -2) ? 0 : particle2->vy; - } - + } } + // particles have volume, push particles apart if they are too close by moving each particle by a fixed amount away from the other particle // if pushing is made dependent on hardness, things start to oscillate much more, better to just add a fixed, small increment (tried lots of configurations, this one works best) // one problem remaining is, particles get squished if (external) force applied is higher than the pushback but this may also be desirable if particles are soft. also some oscillations cannot be avoided without addigng a counter @@ -892,8 +889,7 @@ void handleCollision(PSparticle *particle1, PSparticle *particle2, const uint8_t else particle2->x += push; } - //Serial.print(" dy"); - //Serial.println(dy); + if (dy < HARDDIAMETER && dy > -HARDDIAMETER) { From 9c6d6f19bea04d274b9bc1a7ecf39f2401cac1cf Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sun, 10 Mar 2024 22:35:13 +0100 Subject: [PATCH 037/219] cleanup -removed wrap_update function, now integrated into move_update -added more 'out of bounds' checking in fire functions, may speed it up a little --- wled00/FX.cpp | 6 +- wled00/FXparticleSystem.cpp | 125 +++++++++++++----------------------- wled00/FXparticleSystem.h | 5 +- 3 files changed, 49 insertions(+), 87 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 4f3d620272..c0ae71247b 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -8503,7 +8503,7 @@ uint16_t mode_particlefire(void) // add wind, using perlin noise int8_t windspeed = (int8_t)(inoise8(SEGMENT.aux0, particles[i].y >> 2) - 127) / ((271 - SEGMENT.custom2) >> 4); particles[i].vx = windspeed; - FireParticle_update(&particles[i], SEGMENT.check1, false); // update particle, use X-wrapping if check 1 is set by user + FireParticle_update(&particles[i], SEGMENT.check1); // update particle, use X-wrapping if check 1 is set by user } } @@ -9441,8 +9441,8 @@ uint16_t mode_particlespray(void) Particle_Gravity_update(&particles[i], SEGMENT.check2, SEGMENT.check2 == 0, true, hardness); else //bounce particles { - if(SEGMENT.check2) //wrap x - Particle_Wrap_update(&particles[i], true, false); + if(SEGMENT.check2) //wrap x + Particle_Move_update(&particles[i], true, true, false); else //bounce Particle_Bounce_update(&particles[i], hardness); } diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index cd92d46ac3..60976b620d 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -161,7 +161,7 @@ void Particle_attractor(PSparticle *particle, PSparticle *attractor, uint8_t *co } // 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) +void Particle_Move_update(PSparticle *part, bool killoutofbounds, bool wrapX, bool wrapY) { // Matrix dimension const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; @@ -182,21 +182,42 @@ void Particle_Move_update(PSparticle *part, bool killoutofbounds) part->outofbounds = 0; // reset out of bounds (in case particle was created outside the matrix and is now moving into view) - // check if particle is out of bounds - if ((part->y <= 0) || (part->y >= PS_MAX_Y)) + // 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; } - if ((part->x <= 0) || (part->x >= PS_MAX_X)) + 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) + if (killoutofbounds) part->ttl = 0; else part->outofbounds = 1; } + part->y = newY; // set new position } } @@ -246,57 +267,6 @@ void Particle_Bounce_update(PSparticle *part, const uint8_t hardness) } -// particle moves, decays and dies (age or out of matrix), if wrap is set, pixels leaving the matrix reappear on other side -//TODO: this is just move update with wrap, could make one function out of it -void Particle_Wrap_update(PSparticle *part, bool wrapX, bool wrapY) -{ - // 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--; - - // 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 - { - 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 - { - part->outofbounds = 1; - } - part->y = newY; // set new position - } - -} - // 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) { @@ -534,7 +504,7 @@ void ParticleSys_render(PSparticle *particles, uint32_t numParticles, bool wrapX // update & move particle, wraps around left/right if wrapX is true, wrap around up/down if wrapY is true // particles move upwards faster if ttl is high (i.e. they are hotter) -void FireParticle_update(PSparticle *part, bool wrapX, bool wrapY) +void FireParticle_update(PSparticle *part, bool wrapX) { // Matrix dimension const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; @@ -551,8 +521,9 @@ void FireParticle_update(PSparticle *part, bool wrapX, bool wrapY) // apply velocity part->x = part->x + (int16_t)part->vx; - part->y = part->y + (int16_t)part->vy + (part->ttl >> 4); // younger particles move faster upward as they are hotter, used for fire //TODO: need to make this optional? + part->y = part->y + (int16_t)part->vy + (part->ttl >> 4); // younger particles move faster upward as they are hotter, used for fire + part->outofbounds = 0; // check if particle is out of bounds, wrap around to other side if wrapping is enabled // x-direction if ((part->x < 0) || (part->x > PS_MAX_X)) @@ -565,23 +536,18 @@ void FireParticle_update(PSparticle *part, bool wrapX, bool wrapY) } else { - part->ttl = 0; // todo: for round flame display, particles need to go modulo + part->ttl = 0; } } // y-direction if ((part->y < -(PS_P_RADIUS << 4)) || (part->y > PS_MAX_Y)) - { // position up to 8 pixels the matrix is allowed, used in fire for wider flames - if (wrapY) - { - part->y = part->y % (PS_MAX_Y + 1); - if (part->y < 0) - part->y = PS_MAX_Y - part->y; - } - else - { - part->ttl = 0; // todo: for round flame display, particles need to go modulo - } + { // position up to 8 pixels below the matrix is allowed, used for wider flames at the bottom + part->ttl = 0; + } + else if (part->y < 0) + { + part->outofbounds = 1; } } } @@ -594,23 +560,20 @@ void ParticleSys_renderParticleFire(PSparticle *particles, uint32_t numParticles const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; const uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); - int32_t x, y; uint8_t dx, dy; uint32_t tempVal; 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++) { - if (particles[i].ttl == 0) + if (particles[i].ttl == 0 || particles[i].outofbounds) { continue; } - //TODO: no more using simple particles, can use the out of bounds here - - // simple particles do not have 'out of bound' parameter, need to check if particle is within matrix boundaries dx = (uint8_t)((uint16_t)particles[i].x % (uint16_t)PS_P_RADIUS); dy = (uint8_t)((uint16_t)particles[i].y % (uint16_t)PS_P_RADIUS); @@ -639,8 +602,8 @@ void ParticleSys_renderParticleFire(PSparticle *particles, uint32_t numParticles dy = dy - (PS_P_RADIUS >> 1); } - if (wrapX) - { // wrap it to the other side if required + if (wrapX) + { if (x < 0) { // left half of particle render is out of frame, wrap it x = cols - 1; @@ -649,7 +612,7 @@ void ParticleSys_renderParticleFire(PSparticle *particles, uint32_t numParticles // calculate brightness values for all six pixels representing a particle using linear interpolation // bottom left - if (x < cols && y < rows) + if (x < cols && x >=0 && y < rows && y >=0) { tempVal = (((uint32_t)((PS_P_RADIUS)-dx) * ((PS_P_RADIUS)-dy) * (uint32_t)particles[i].ttl) >> PS_P_SURFACE); PartMatrix_addHeat(x, y, tempVal); @@ -662,7 +625,7 @@ void ParticleSys_renderParticleFire(PSparticle *particles, uint32_t numParticles 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) + if (x < cols && y < rows && y >= 0) { tempVal = (((uint32_t)dx * ((PS_P_RADIUS)-dy) * (uint32_t)particles[i].ttl) >> PS_P_SURFACE); PartMatrix_addHeat(x, y, tempVal); @@ -673,7 +636,7 @@ void ParticleSys_renderParticleFire(PSparticle *particles, uint32_t numParticles if (x < cols && y < rows) { tempVal = (((uint32_t)dx * dy * (uint32_t)particles[i].ttl) >> PS_P_SURFACE); // - PartMatrix_addHeat(x, y, tempVal); + PartMatrix_addHeat(x, y, tempVal); PartMatrix_addHeat(x + 1, y, tempVal); // shift particle by 1 pixel to the right and add heat again (makes flame wider without using more particles) } // top left @@ -685,7 +648,7 @@ void ParticleSys_renderParticleFire(PSparticle *particles, uint32_t numParticles x = cols - 1; } } - if (x < cols && y < rows) + if (x < cols && x >= 0 && y < rows) { tempVal = (((uint32_t)((PS_P_RADIUS)-dx) * dy * (uint32_t)particles[i].ttl) >> PS_P_SURFACE); PartMatrix_addHeat(x, y, tempVal); diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index e626aa03d7..824acc7b37 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -87,12 +87,11 @@ 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); +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_Wrap_update(PSparticle *part, bool wrapX, bool wrapY); 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, bool wrapX = false, bool WrapY = false); +void FireParticle_update(PSparticle *part, bool wrapX = false); void ParticleSys_renderParticleFire(PSparticle *particles, uint32_t numParticles, bool wrapX); void PartMatrix_addHeat(uint8_t col, uint8_t row, uint16_t heat); void detectCollisions(PSparticle *particles, uint32_t numparticles, uint8_t hardness); From 4930cda17d8518b2c3ac0669888820d4a04f4a8a Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Tue, 12 Mar 2024 20:17:02 +0100 Subject: [PATCH 038/219] cleanup session -removed particle box 'rocking boat' (buggy) and replaced with random sloshing. -removed comments -changed some variables into 32bit for speed boost on ESP32 -added link to KB at the top of FX.cpp --- wled00/FX.cpp | 121 +++++++----------------------------- wled00/FXparticleSystem.cpp | 25 ++++---- 2 files changed, 34 insertions(+), 112 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index c0ae71247b..5686152b70 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -24,6 +24,8 @@ Modified heavily for WLED */ +// information for custom FX metadata strings: https://kno.wled.ge/interfaces/json-api/#effect-metadata + #include "wled.h" #include "FX.h" #include "fcn_declare.h" @@ -8515,19 +8517,7 @@ uint16_t mode_particlefire(void) return FRAMETIME; } static const char _data_FX_MODE_PARTICLEFIRE[] PROGMEM = "Particle Fire@Speed,Intensity,Base Flames,Wind Speed, Color Scheme, WrapX;;!;012;sx=100,ix=120,c1=16,c2=128,c3=0,o1=0"; -/*syntax for json configuration string: -@A,B,C,D,E,F,G,H;I,J,K;L;M;N mark commas and semicolons -A - speed -B - intensity -C, D, E, - custom1 to custom3 -F,G,H - check1 to check3 -I,J,K - slot1 to slot3 -L - palette -M - mode (012) -N - parameter defaults (sliders: sx=100 ist speed, ix=24 is intensity, c1 ... c3 =20 is custom 1...3) -a '!' uses default values for that section -*/ /* particles falling down, user can enable these three options: X-wraparound, side bounce, ground bounce @@ -8580,8 +8570,8 @@ uint16_t mode_particlefall(void) if (random8(5) == 0) // 16% of particles apper anywhere particles[i].x = random16(cols * PS_P_RADIUS - 1); - else // rest is emitted at center half - particles[i].x = random16((cols >> 1) * PS_P_RADIUS + (cols >> 1) * PS_P_RADIUS); // todo: could make this dynamic and random but needs a user variable + else // rest is emitted at center half + particles[i].x = random16((cols >> 1) * PS_P_RADIUS + (cols >> 1) * PS_P_RADIUS); particles[i].y = random16(rows * PS_P_RADIUS) + rows * PS_P_RADIUS; // particles appear somewhere above the matrix, maximum is double the height particles[i].vx = (((int16_t)random8(SEGMENT.custom1)) - (SEGMENT.custom1 >> 1)) >> 1; // side speed is +/- a quarter of the custom1 slider @@ -8645,45 +8635,6 @@ uint16_t mode_particlewaterfall(void) if (SEGLEN == 1) return mode_static(); - -/* - um_data_t *um_data; - if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) - { - um_data = NULL; //no audio - } - //TODO: will crash if no umdata! - uint8_t volumeSmth = (uint8_t)(*(float *)um_data->u_data[0]); //volume, mapped 0-255 - float volumeRaw = *(float *)um_data->u_data[1]; //always zero? - uint8_t* fftResult = (uint8_t *)um_data->u_data[2]; //16 bins with FFT data - uint8_t samplePeak = *(uint8_t *)um_data->u_data[3]; //0 or 1 if a sample peak is detected (not sure what the thresholds are) - float FFT_MajorPeak = *(float *)um_data->u_data[4]; //frequency in Hz of major peak - float my_magnitude = *(float *)um_data->u_data[5]; //current volume, should fit a uint16_t (goes up to 20'000 or even higher?) but unclear what exactly the value is - //maxVol = (uint8_t *)um_data->u_data[6]; // requires UI element (SEGMENT.customX?), changes source element, see Ripple Peak for an example - //binNum = (uint8_t *)um_data->u_data[7]; // requires UI element (SEGMENT.customX?), changes source element - //float* fftBin = (float *)um_data->u_data[8]; //points to what exactly? - */ -/* - //print values as a test: - Serial.println("***"); - Serial.print(volumeSmth); - Serial.print(", "); - Serial.print(volumeRaw); - Serial.print(", "); - Serial.print(samplePeak); - Serial.print(", "); - Serial.print(FFT_MajorPeak); - Serial.print(", "); - Serial.println(my_magnitude); - - for(int i = 0; i<16;i++) - { - Serial.print(fftResult[i]); - Serial.print(" "); - } - - Serial.println("***"); -*/ const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; const uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); @@ -8852,54 +8803,28 @@ uint16_t mode_particlebox(void) if (SEGMENT.call % (((255 - SEGMENT.speed) >> 6) + 1) == 0 && SEGMENT.speed > 0) // how often the force is applied depends on speed setting { - int8_t xgravity; - int8_t ygravity; + int32_t xgravity; + int32_t ygravity; uint8_t scale; - if (SEGMENT.check1 == 0) // if rocking boat is not set, use perlin noise for force vector generation + SEGMENT.aux0 += (SEGMENT.speed >> 6) + 1; // update position in noise + + xgravity = ((int16_t)inoise8(SEGMENT.aux0) - 127); + ygravity = ((int16_t)inoise8(SEGMENT.aux0 + 10000) - 127); + if (SEGMENT.check1) //sloshing, y force is alwys downwards { - SEGMENT.aux0 += (SEGMENT.speed >> 6) + 1; // update position in noise - // uint8_t angle = (((uint16_t)inoise8(SEGMENT.aux0)) << 1) - 64; // noise avg. value is 127 scale to 256 (to increase the range) and subtract 64 (=90°) to make the average direction downwards (270°), overflow means modulo, so is ok - // calculate x and y vectors from angle: - // xgravity = ((int16_t)cos8(angle)) - 128; // gravity direction +/- 127 - // ygravity = ((int16_t)sin8(angle)) - 128; - // scale the vectors using another inoise value: - // scale = inoise8(SEGMENT.aux0 + 4096); // use a offset in the noise for scale value - - // scale = ((uint16_t)scale * SEGMENT.custom1)>>8; //apply rescaling with user input - // not using angles but x and y tilt from inoise: - xgravity = ((int16_t)inoise8(SEGMENT.aux0) - 127); - ygravity = ((int16_t)inoise8(SEGMENT.aux0 + 10000) - 127); - } - else - { // use sinusoidal motion - // angle needs to move from -270° to +90° (from top leftside to top rightside but limited by one of the sliders by the user (custom1=Amount), -270 (and +90) is ~64 in 8bit angle representation (365=255) - // the anglar force changes in a sinusoidal motion, like a rocking boat - // the angle is first calculated using a sine, then shifted so it goes from -127 to +127, then scaled, then shifted to 0 is actually -64 (=-90°=down) - - SEGMENT.aux0++; // move forward in the sinusoidal function - int16_t angle = (int16_t)sin8(SEGMENT.aux0) - 128; // shift result (0-255 representing -1 to +1) so it goes from -128 to +127 - scale = 130 - (abs(angle)); // force gets weaker at exteme positions - if (scale > 50) - scale = 50; // force is limited at lower angles - angle = (angle * (int16_t)SEGMENT.custom1) >> 8; // scale angle range according to slider (tilt strength) - angle -= 63; // make 'down' (or -90°) the zero position - //TODO: need to debug this, angle is not symmetrical around 'down' - // now calculate the force vectors - xgravity = ((int16_t)cos8((uint8_t)angle)) - 128; // gravity direction +/- 127 - ygravity = ((int16_t)sin8((uint8_t)angle)) - 128; - - // scale gravity force - xgravity = ((int16_t)xgravity * scale) >> 8; - ygravity = ((int16_t)ygravity * scale) >> 8; - + if(ygravity > 0) + ygravity = -ygravity; } + + // scale the gravity force down - xgravity = xgravity >> 5; - ygravity = ygravity >> 5; - // Serial.print(xgravity); - // Serial.println(" "); + xgravity /= 16; + ygravity /= 16; + Serial.print(xgravity); + Serial.print(" "); + Serial.println(ygravity); for (i = 0; i < numParticles; i++) { @@ -8911,8 +8836,7 @@ uint16_t mode_particlebox(void) // apply a little gravity downwards to bias things in rocking mode if (SEGMENT.check1 && SEGMENT.call % 2 == 0) - particles[i].vy--; - + particles[i].vy--; } } } @@ -8940,7 +8864,7 @@ uint16_t mode_particlebox(void) return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEBOX[] PROGMEM = "Particle Box@Speed,Particles,Tilt strength,Hardness,,Rocking Boat,;;!;012;pal=1,sx=120,ix=100,c1=190,c2=210,o1=0"; +static const char _data_FX_MODE_PARTICLEBOX[] PROGMEM = "Particle Box@Speed,Particles,Tilt strength,Hardness,,Sloshing;;!;012;pal=1,sx=120,ix=100,c1=190,c2=210,o1=0"; /* perlin noise 'gravity' mapping as in particles on noise hills viewed from above @@ -9456,7 +9380,6 @@ uint16_t mode_particlespray(void) return FRAMETIME; } static const char _data_FX_MODE_PARTICLESPRAY[] PROGMEM = "Particle Spray@Particle Speed,Intensity,X Position,Y Position,Angle,Gravity,WrapX/Bounce,Collisions;;!;012;pal=0,sx=180,ix=200,c1=220,c2=30,c3=12,o1=1,o2=0,o3=1"; -// TODO: animation idea: just one spray, sliders set x position, y position, speed, intensity and spray angle. ticks set wrap/bounce, gravity? evtl noch life time koppeln mit speed? #endif // WLED_DISABLE_2D diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index 60976b620d..ab3a911af1 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -27,9 +27,9 @@ */ /* -Note: on ESP32 using 32bit integer is faster than 16bit or 8bit, each operation takes on less instruction, can be testen on https://godbolt.org/ - it does not matter if using int, unsigned int, uint32_t or int32_t, the compiler will make int into 32bit - this should be used to optimize speed but not if memory is affected much + Note on ESP32: using 32bit integer is faster than 16bit or 8bit, each operation takes on less instruction, can be testen on https://godbolt.org/ + it does not matter if using int, unsigned int, uint32_t or int32_t, the compiler will make int into 32bit + this should be used to optimize speed but not if memory is affected much */ #include "FXparticleSystem.h" @@ -366,8 +366,7 @@ void ParticleSys_render(PSparticle *particles, uint32_t numParticles, bool wrapX #else bool fastcoloradd = false; // on ESP32, there is little benefit from using fast add #endif - - + const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; const uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); @@ -377,7 +376,7 @@ void ParticleSys_render(PSparticle *particles, uint32_t numParticles, bool wrapX int16_t x, y; uint8_t dx, dy; - uint32_t intensity; // todo: can this be an uint8_t or will it mess things up? + uint32_t intensity; CRGB baseRGB; uint32_t i; uint8_t brightess; // particle brightness, fades if dying @@ -661,10 +660,10 @@ void ParticleSys_renderParticleFire(PSparticle *particles, uint32_t numParticles void PartMatrix_addHeat(uint8_t col, uint8_t row, uint16_t heat) { - const uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); + 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) - uint16_t newcolorvalue; + uint32_t newcolorvalue; uint8_t colormode = map(SEGMENT.custom3, 0, 31, 0, 5); // get color mode from slider (3bit value) // 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 @@ -677,8 +676,8 @@ void PartMatrix_addHeat(uint8_t col, uint8_t row, uint16_t heat) int8_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, is 16bit value - newcolorvalue = min(newcolorvalue, (uint16_t)255); // limit to 8bit value again + 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 @@ -700,7 +699,7 @@ void PartMatrix_addHeat(uint8_t col, uint8_t row, uint16_t heat) if (currentcolor[i] < 255) { newcolorvalue = (uint16_t)currentcolor[i] + heat; // add heat, check if it overflows - newcolorvalue = min(newcolorvalue, (uint16_t)255); // limit to 8bit value again + 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 { @@ -721,7 +720,7 @@ void PartMatrix_addHeat(uint8_t col, uint8_t row, uint16_t heat) if (currentcolor[i] < 255) { newcolorvalue = currentcolor[i] + heat; // add heat, check if it overflows - newcolorvalue = min(newcolorvalue, (uint16_t)50); // limit so it does not go full white + newcolorvalue = min(newcolorvalue, (uint32_t)50); // limit so it does not go full white currentcolor[i] = (uint8_t)newcolorvalue; } } @@ -867,7 +866,7 @@ void handleCollision(PSparticle *particle1, PSparticle *particle2, const uint8_t else particle2->y += push; } - //note: pushing may push particles out of frame, if bounce is active, it will move it back as position will be limited to within frame + //note: pushing may push particles out of frame, if bounce is active, it will move it back as position will be limited to within frame, if bounce is disabled: bye bye } From ac0921818542e74f5ded49f56d31df9299aaf834 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Tue, 12 Mar 2024 20:45:52 +0100 Subject: [PATCH 039/219] cleanup removed / added comments --- wled00/FX.cpp | 160 ++++++++++++++------------------------------------ 1 file changed, 45 insertions(+), 115 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 5686152b70..53d953ffa4 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -8016,8 +8016,8 @@ static const char _data_FX_MODE_PARTICLEROTATINGSPRAY[] PROGMEM = "Rotating Part /* * Particle Fireworks - * Rockets shoot up and explode in a random color - * Use ranbow palette as default + * Rockets shoot up and explode in a random color, sometimes in a defined pattern + * Uses ranbow palette as default * by DedeHai (Damian Schneider) */ @@ -8214,7 +8214,7 @@ static const char _data_FX_MODE_PARTICLEFIREWORKS[] PROGMEM = "Particle Firework /* * Particle Volcano (gravity spray) - * Particles are sprayed from below, spray moves back and forth + * Particles are sprayed from below, spray moves back and forth if option is set * Uses palette for particle color * by DedeHai (Damian Schneider) */ @@ -8243,9 +8243,6 @@ uint16_t mode_particlevolcano(void) if (!SEGENV.allocateData(dataSize)) return mode_static(); // allocation failed; //allocation failed - // DEBUG_PRINT(F("particle datasize = ")); - // DEBUG_PRINTLN(dataSize); - 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 @@ -8273,7 +8270,6 @@ uint16_t mode_particlevolcano(void) spray[i].vy = 20; // emitting speed // spray.var = 10 + (random8() % 4); } - // SEGMENT.palette = 35; //fire palette } // change source emitting color from time to time @@ -8301,7 +8297,6 @@ uint16_t mode_particlevolcano(void) spray[i].vy = SEGMENT.custom1 >> 2; // emitting speed, upward spray[i].vx = 0; spray[i].var = SEGMENT.custom3; // emiting variation = nozzle size (custom 3 goes from 0-32) - //spray[i].source.y = spray[i].var + 1; // need to 'lift up' the source as 'var' also changes particle spawn position randomly spray[i].source.ttl = 255; // source never dies, replenish its lifespan } @@ -8332,7 +8327,6 @@ uint16_t mode_particlevolcano(void) for (i = 0; i < numParticles; i++) { - // Particle_Move_update(&particles[i]); //move the particles //set color according to ttl ('color by age') if (SEGMENT.check1) particles[i].hue = min((uint16_t)220, particles[i].ttl); @@ -8344,31 +8338,13 @@ uint16_t mode_particlevolcano(void) // render the particles ParticleSys_render(particles, numParticles, false, false); - // CRGB c = PURPLE; - // SEGMENT.setPixelColorXY(0, 0, c); - return FRAMETIME; } static const char _data_FX_MODE_PARTICLEVOLCANO[] PROGMEM = "Particle Volcano@Moving Speed,Intensity,Particle Speed,Bouncyness,Nozzle Size,Color by Age,Bounce X,Collisions;;!;012;pal=35,sx=0,ix=160,c1=100,c2=160,c3=10,o1=1,o2=1,o3=1"; -// good default values for sliders: 100,200,190, 45 -/*syntax for json configuration string: -@A,B,C,D,E,F,G,H;I,J,K;L;M;N mark commas and semicolons -A - speed -B - intensity -C, D, E, - custom1 to custom3 -F,G,H - check1 to check3 -I,J,K - slot1 to slot3 -L - palette -M - mode (012) -N - parameter defaults (sliders: sx=100 ist speed, ix=24 is intensity, c1 ... c3 =20 is custom 1...3) - -a '!' uses default values for that section -*/ - /* * Particle Fire - * realistic fire effect using particles, heat and perlinnoise wind + * realistic fire effect using particles. heat based and using perlin-noise for wind * by DedeHai (Damian Schneider) */ @@ -8394,10 +8370,6 @@ uint16_t mode_particlefire(void) uint32_t dataSize = sizeof(PSparticle) * numParticles; dataSize += sizeof(PSpointsource) * (numFlames); - // DEBUG_PRINTLN(F("**********************")); - // DEBUG_PRINT(F("particle datasize = ")); - // DEBUG_PRINTLN(dataSize); - if (!SEGENV.allocateData(dataSize)) { return mode_static(); // allocation failed; //allocation failed @@ -8411,7 +8383,6 @@ uint16_t mode_particlefire(void) if (SEGMENT.call == 0) // initialization { - DEBUG_PRINTLN(F("Initializing Particle Fire")); SEGMENT.aux0 = rand(); // aux0 is wind position (index) in the perlin noise // make sure all particles start out dead for (i = 0; i < numParticles; i++) @@ -8424,7 +8395,7 @@ uint16_t mode_particlefire(void) { flames[i].source.ttl = 0; flames[i].source.x = PS_P_RADIUS * 3 + random16(PS_MAX_X - (PS_P_RADIUS * 6)); // distribute randomly but not close to the corners - // other parameters are set when creating the flame (see blow) + //note: other parameters are set when creating the flame (see blow) } } @@ -8442,25 +8413,23 @@ uint16_t mode_particlefire(void) // from time to time, chang the flame position // make some of the flames small and slow to add a bright base - if (random8(40) == 0) //from time to time, change flame position (about once per second) + if (random8(40) == 0) //from time to time, change flame position (about once per second at 40 fps) { if (SEGMENT.check1) { // wrap around in X direction, distribute randomly flames[i].source.x = random16(PS_MAX_X); } - else - { // no wrapping + else // no X-wrapping + { flames[i].source.x = PS_P_RADIUS * 3 + random16(PS_MAX_X - (PS_P_RADIUS * 6)); // distribute randomly but not close to the corners } } if (i < (numFlames - (cols >> 1))) { // all but the last few are normal flames - flames[i].source.y = -1 * PS_P_RADIUS; // set the source below the frame so particles alredy spread a little when the appear flames[i].source.vx = 0; // (rand() % 3) - 1; flames[i].source.vy = 0; - // flames[i].source.hue = random8(15) + 18; //flame color, orange to yellow flames[i].source.ttl = random8(SEGMENT.intensity >> 2) / (1 + (SEGMENT.speed >> 6)) + 10; //'hotness' of fire, faster flames reduce the effect or flame height will scale too much with speed flames[i].maxLife = random8(7) + 13; // defines flame height together with the vy speed, vy speed*maxlife/PS_P_RADIUS is the average flame height flames[i].minLife = 2; @@ -8472,8 +8441,7 @@ uint16_t mode_particlefire(void) { // base flames to make the base brighter, flames are slower and short lived flames[i].source.y = -1 * PS_P_RADIUS; // set the source below the frame flames[i].source.vx = 0; - flames[i].source.vy = 0; // emitter moving speed; - // flames[i].source.hue = 0;//(rand() % 15) + 18; //flame color (not used) + flames[i].source.vy = 0; // emitter moving speed; flames[i].source.ttl = random8(25) + 15; // lifetime of one flame flames[i].maxLife = 25; // defines flame height together with the vy speed, vy speed*maxlife/PS_P_RADIUS is the average flame height flames[i].minLife = 12; @@ -8501,8 +8469,8 @@ uint16_t mode_particlefire(void) percycle--; } else if (particles[i].ttl) - { // if particle is alive, update it - // add wind, using perlin noise + { + // add wind using perlin noise int8_t windspeed = (int8_t)(inoise8(SEGMENT.aux0, particles[i].y >> 2) - 127) / ((271 - SEGMENT.custom2) >> 4); particles[i].vx = windspeed; FireParticle_update(&particles[i], SEGMENT.check1); // update particle, use X-wrapping if check 1 is set by user @@ -8521,7 +8489,7 @@ static const char _data_FX_MODE_PARTICLEFIRE[] PROGMEM = "Particle Fire@Speed,In /* particles falling down, user can enable these three options: X-wraparound, side bounce, ground bounce -sliders control falling speed, intensity (number of particles spawned), WIND OR SPEED RANDOMNESS?, inter-particle collision hardness (0 means no particle collisions) and render saturation +sliders control falling speed, intensity (number of particles spawned), inter-particle collision hardness (0 means no particle collisions) and render saturation this is quite versatile, can be made to look like rain or snow or confetti, flying sparks etc. Uses palette for particle color by DedeHai (Damian Schneider) @@ -8601,8 +8569,6 @@ uint16_t mode_particlefall(void) SEGMENT.fill(BLACK); // clear the matrix - - // render the particles ParticleSys_render(particles, numParticles, SEGMENT.check1, false); // custom3 slider is saturation, from 7 to 255, 7 is close enough to white (for snow for example) @@ -8616,19 +8582,6 @@ static const char _data_FX_MODE_PARTICLEFALL[] PROGMEM = "Falling Particles@Spee * by DedeHai (Damian Schneider) */ -/*Audio Reactive test: - - volumeSmth = *(float*) um_data->u_data[0]; - volumeRaw = *(float*) um_data->u_data[1]; - fftResult = (uint8_t*) um_data->u_data[2]; - samplePeak = *(uint8_t*) um_data->u_data[3]; - FFT_MajorPeak = *(float*) um_data->u_data[4]; - my_magnitude = *(float*) um_data->u_data[5]; - maxVol = (uint8_t*) um_data->u_data[6]; // requires UI element (SEGMENT.customX?), changes source element - binNum = (uint8_t*) um_data->u_data[7]; // requires UI element (SEGMENT.customX?), changes source element - fftBin = (float*) um_data->u_data[8]; //not sure what this pointer does... not used in any examples - */ - uint16_t mode_particlewaterfall(void) { @@ -8677,7 +8630,6 @@ uint16_t mode_particlewaterfall(void) spray[i].vx = 0; // emitting speed spray[i].var = 7; // emiting variation } - // SEGMENT.palette = 35; //fire palette } // change source emitting color @@ -8687,11 +8639,7 @@ uint16_t mode_particlewaterfall(void) } uint8_t intensity = SEGMENT.intensity; -/* - if (um_data != NULL) //audio reactive data available - { - intensity = map(volumeSmth,0,255,20,255); - }*/ + if (SEGMENT.call % (9 - (intensity >> 5)) == 0 && intensity > 0) // every nth frame, cycle color and emit particles, do not emit if intensity is zero { @@ -8720,7 +8668,6 @@ uint16_t mode_particlewaterfall(void) } } - // detect and handle collisions uint8_t hardness = SEGMENT.custom2; // how hard the collisions are, 255 = full hard. @@ -8753,7 +8700,7 @@ uint16_t mode_particlewaterfall(void) static const char _data_FX_MODE_PARTICLEWATERFALL[] PROGMEM = "Particle Waterfall@Particle Speed,Intensity,Speed Variation,Collision Hardness,Position,Wrap X,Bounce X,Ground bounce;;!;012;pal=9,sx=150,ix=240,c1=0,c2=128,c3=17,o1=0,o2=0,o3=1"; /* -Particle Box, applies gravity to particles in either a random direction or in a rocking motion +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) */ @@ -8776,8 +8723,6 @@ uint16_t mode_particlebox(void) return mode_static(); // allocation failed; //allocation failed particles = reinterpret_cast(SEGENV.data); // cast the data array into a particle pointer - - uint32_t i = 0; uint32_t j = 0; @@ -8816,8 +8761,6 @@ uint16_t mode_particlebox(void) if(ygravity > 0) ygravity = -ygravity; } - - // scale the gravity force down xgravity /= 16; @@ -8832,11 +8775,7 @@ uint16_t mode_particlebox(void) { particles[i].vx += xgravity; particles[i].vy += ygravity; - particles[i].ttl = 500; // particles never die - - // apply a little gravity downwards to bias things in rocking mode - if (SEGMENT.check1 && SEGMENT.call % 2 == 0) - particles[i].vy--; + particles[i].ttl = 500; // particles never die } } } @@ -8844,7 +8783,6 @@ uint16_t mode_particlebox(void) uint8_t hardness = SEGMENT.custom2; // how hard the collisions are, 255 = full hard. detectCollisions(particles, displayparticles, hardness); - // now move the particles for (i = 0; i < displayparticles; i++) { @@ -8869,7 +8807,7 @@ static const char _data_FX_MODE_PARTICLEBOX[] PROGMEM = "Particle Box@Speed,Part /* perlin noise 'gravity' mapping as in particles on noise hills viewed from above calculates slope gradient at the particle positions -restults in a fuzzy perlin noise disply +restults in a fuzzy perlin noise display */ uint16_t mode_particleperlin(void) @@ -8914,7 +8852,6 @@ uint16_t mode_particleperlin(void) uint32_t displayparticles = map(SEGMENT.intensity,0,255,10,numParticles); // apply 'gravity' from a 2D perlin noise map - SEGMENT.aux0 += 1+(SEGMENT.speed >> 5); // noise z-position // update position in noise @@ -8930,9 +8867,9 @@ uint16_t mode_particleperlin(void) int32_t xnoise = particles[i].x / (1 + (SEGMENT.custom3>>1)); //position in perlin noise, scaled by slider int32_t ynoise = particles[i].y / (1 + (SEGMENT.custom3>>1)); - int16_t baseheight = inoise8(xnoise, ynoise, SEGMENT.aux0); // noise value at particle position - particles[i].hue = baseheight; // color particles to perlin noise value - if (SEGMENT.call % 6 == 0) // do not apply the force every frame, is too chaotic + int16_t baseheight = inoise8(xnoise, ynoise, SEGMENT.aux0); // noise value at particle position + particles[i].hue = baseheight; // color particles to perlin noise value + if (SEGMENT.call % 6 == 0) // do not apply the force every frame, is too chaotic { int16_t xslope = baseheight - (int16_t)inoise8(xnoise + 10, ynoise, SEGMENT.aux0); int16_t yslope = baseheight - (int16_t)inoise8(xnoise, ynoise + 10, SEGMENT.aux0); @@ -8968,8 +8905,7 @@ static const char _data_FX_MODE_PARTICLEPERLIN[] PROGMEM = "Particle Perlin-Nois /* -* Particle smashing down like meteorites and exploding as they hit the ground, like inverted fireworks -* has many parameters to play with +* Particle smashing down like meteorites and exploding as they hit the ground, has many parameters to play with * by DedeHai (Damian Schneider) */ @@ -9027,30 +8963,29 @@ uint16_t mode_particleimpact(void) // update particles, create particles - // check each rocket's state and emit particles according to its state: moving up = emit exhaust, at top = explode; falling down = standby time uint32_t emitparticles; // number of particles to emit for each rocket's state i = 0; for (j = 0; j < numMeteors; j++) { - // determine rocket state by its speed: + // determine meteor state by its speed: if (meteors[j].source.vy < 0) // moving down, emit sparks { emitparticles = 2; } - else if (meteors[j].source.vy > 0) // moving up means standby + else if (meteors[j].source.vy > 0) // moving up means meteor is on 'standby' { emitparticles = 0; } else // speed is zero, explode! { - meteors[j].source.vy = 125; // set source speed positive so it goes into timeout and launches again + meteors[j].source.vy = 125; // set source speed positive so it goes into timeout and launches again emitparticles = random8(SEGMENT.intensity >> 1) + 10; // defines the size of the explosion } for (i; i < numParticles; i++) { - if (particles[i].ttl == 0) - { // particle is dead + if (particles[i].ttl == 0) // particle is dead + { if (emitparticles > 0) { Emitter_Fountain_emit(&meteors[j], &particles[i]); @@ -9062,10 +8997,10 @@ uint16_t mode_particleimpact(void) } } - uint8_t hardness = SEGMENT.custom2; // how hard the collisions are, 255 = full hard. - // add collision if option is set - if(SEGMENT.check3) - { + uint8_t hardness = SEGMENT.custom2; // how hard the collisions are, 255 = fully hard, no energy is lost in collision + + if (SEGMENT.check3) // use collisions if option is set + { detectCollisions(particles, numParticles, hardness); } // update particles @@ -9077,21 +9012,21 @@ uint16_t mode_particleimpact(void) } } - // update the meteors, set the speed state + // update the meteors, set the speed state for (i = 0; i < numMeteors; i++) { if (meteors[i].source.ttl) { Particle_Gravity_update(&meteors[i].source, SEGMENT.check1, SEGMENT.check2, true, 255); // move the meteor, age the meteor (ttl--) if (meteors[i].source.vy > 0) - meteors[i].source.y=5; //hack to keep the meteors within frame, as ttl will be set to zero by gravity update if too far out of frame + meteors[i].source.y=5; //'hack' to keep the meteors within frame, as ttl will be set to zero by gravity update if too far out of frame // if source reaches the bottom, set speed to 0 so it will explode on next function call (handled above) if ((meteors[i].source.y < PS_P_RADIUS) && (meteors[i].source.vy < 0)) // reached the bottom pixel on its way down { - meteors[i].source.vy = 0; // set speed zero so it will explode + meteors[i].source.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].source.collide = true; // explosion particles will collide if checked meteors[i].maxLife = 200; meteors[i].minLife = 50; meteors[i].source.ttl = random8((255 - SEGMENT.speed)) + 10; // standby time til next launch (in frames at 42fps, max of 265 is about 6 seconds @@ -9100,9 +9035,9 @@ uint16_t mode_particleimpact(void) meteors[i].var = (SEGMENT.custom1 >> 1); // speed variation around vx,vy (+/- var/2) } } - else if (meteors[i].source.vy > 0) // rocket is exploded and time is up (ttl==0 and positive speed), relaunch it + else if (meteors[i].source.vy > 0) // meteor is exploded and time is up (ttl==0 and positive speed), relaunch it { - // reinitialize rocket + // reinitialize meteor meteors[i].source.y = PS_MAX_Y + (PS_P_RADIUS<<2); // start 4 pixels above the top meteors[i].source.x = random16(PS_MAX_X); meteors[i].source.vy = -random(30) - 30; //meteor downward speed @@ -9126,8 +9061,8 @@ uint16_t mode_particleimpact(void) static const char _data_FX_MODE_PARTICLEIMPACT[] PROGMEM = "Particle Impact@Launches,Explosion Size,Explosion Force,Bounce,Meteors,Wrap X,Bounce X,Collisions;;!;012;pal=35,sx=32,ix=85,c1=100,c2=100,c3=8,o1=0,o2=1,o3=1"; /* -Particle Attractor, currently just a demo function for the particle attractor -Attractor sits in the matrix center, a spray bounces around and seeds particles. +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) */ @@ -9154,14 +9089,13 @@ uint16_t mode_particleattractor(void) PSpointsource *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); dataSize += sizeof(uint8_t) *numParticles; dataSize += sizeof(PSpointsource); if (!SEGENV.allocateData(dataSize)) - return mode_static(); // allocation failed; //allocation failed + return mode_static(); // allocation failed // divide and cast the data array into correct pointers particles = reinterpret_cast(SEGENV.data); attractor = reinterpret_cast(particles + numParticles + 1); @@ -9224,8 +9158,7 @@ uint16_t mode_particleattractor(void) { if (particles[i].ttl == 0 && emit--) // find a dead particle - { - //Emitter_Fountain_emit(spray, &particles[i]); //emit one if available + { if(SEGMENT.call % 2 == 0) //alternate direction of emit Emitter_Angle_emit(spray, &particles[i], SEGMENT.aux0, SEGMENT.custom1 >> 4); else @@ -9253,8 +9186,6 @@ uint16_t mode_particleattractor(void) else SEGMENT.fill(BLACK); // clear the matrix - - //ParticleSys_render(&attract, 1, 30, false, false); // render attractor // render the particles ParticleSys_render(particles, displayparticles, false, false); @@ -9263,8 +9194,11 @@ uint16_t mode_particleattractor(void) } static const char _data_FX_MODE_PARTICLEATTRACTOR[] PROGMEM = "Particle Attractor@Center Mass,Particles,Emit Speed,Collision Strength,Friction,Bounce,Trails,Swallow;;!;012;pal=9,sx=100,ix=82,c1=190,c2=210,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) { @@ -9291,9 +9225,6 @@ uint16_t mode_particlespray(void) if (!SEGENV.allocateData(dataSize)) return mode_static(); // allocation failed; //allocation failed - // DEBUG_PRINT(F("particle datasize = ")); - // DEBUG_PRINTLN(dataSize); - 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 @@ -9321,7 +9252,6 @@ uint16_t mode_particlespray(void) spray[i].vy = 0; // emitting speed spray[i].var = 10; } - // SEGMENT.palette = 35; //fire palette } // change source emitting color from time to time From 87adbedc71388d0dc035cee4f59a2a7f863ee25e Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Fri, 15 Mar 2024 20:52:47 +0100 Subject: [PATCH 040/219] added particle GEQ effect (untested) --- wled00/FX.cpp | 119 ++++++++++++++++++++++++++++++++++++++++++++++++-- wled00/FX.h | 3 +- 2 files changed, 117 insertions(+), 5 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 3654d06703..383ed1b5bd 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -8551,8 +8551,7 @@ uint16_t mode_particlefall(void) particles[i].vy = -(SEGMENT.speed >> 1); particles[i].hue = random8(); // set random color particles[i].sat = ((SEGMENT.custom3) << 3) + 7; // set saturation - particles[i].collide = true; // particle will collide - break; // quit loop if all particles of this round emitted + particles[i].collide = true; // particle will collide } i++; } @@ -9316,6 +9315,119 @@ uint16_t mode_particlespray(void) } static const char _data_FX_MODE_PARTICLESPRAY[] PROGMEM = "Particle Spray@Particle Speed,Intensity,X Position,Y Position,Angle,Gravity,WrapX/Bounce,Collisions;;!;012;pal=0,sx=180,ix=200,c1=220,c2=30,c3=12,o1=1,o2=0,o3=1"; +/* +Particle base Graphical Equalizer +Uses palette for particle color +by DedeHai (Damian Schneider) +*/ + +uint16_t mode_particleGEQ(void) +{ + + if (SEGLEN == 1) + return mode_static(); + + const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; + const uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); + +#ifdef ESP8266 + const uint32_t numParticles = 150; // maximum number of particles +#else + const uint32_t numParticles = 500; // maximum number of particles +#endif + + PSparticle *particles; + + // allocate memory and divide it into proper pointers, max is 32k for all segments. + uint32_t dataSize = sizeof(PSparticle) * numParticles; + if (!SEGENV.allocateData(dataSize)) + return mode_static(); // allocation failed; //allocation failed + + // calculate the end of the spray data and assign it as the data pointer for the particles: + particles = reinterpret_cast(SEGENV.data); // cast the data array into a particle pointer + + if (SEGMENT.call == 0) // initialization + { + for (i = 0; i < numParticles; i++) + { + particles[i].ttl = 0; + particles[i].sat = 255; //full color + } + } + + um_data_t *um_data; + if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) + { + // add support for no audio + um_data = simulateSound(SEGMENT.soundSim); + } + + uint8_t *fftResult = (uint8_t *)um_data->u_data[2]; // 16 bins with FFT data, log mapped already, each band contains frequency amplitude 0-255 + + //map the bands into 16 positions on x axis, emit some particles according to frequency loudness + //Idea: emit 20 particles at full loudness, can use a shift for that, for example shift by 4 or 5 + //in order to also emit particles for not so loud bands, get a bunch of particles based on frame counter and current loudness? + //implement it simply first, then add complexity... need to check what looks good + uint32_t i = 0; + uint32_t bin; //current bin + uint8_t binwidth = (cols * PS_P_RADIUS - 1)>>4; //emit poisition variation for one bin (+/-) + + for (bin = 0; bin < 16; bin++) + { + uint32_t xposition = map(fftResult[band], 0, 255, cols * PS_P_RADIUS - 1); //emit position according to frequency band + uint8_t emitspeed = fftResult[band] >> 2; // emit speed according to loudness of band TODO: SEGMENT.speed? + uint8_t emitparticles = 0; + //wie sollen die emitted werden? man könnte anzahl berechnen, dann einen emit loop machen + if (fftResult[band] > 10) + { + emitparticles = fftResult[band]/10; + } + else if(fftResult[band] > 0)// band has low volue + { + uint32_t restvolume = 12 - fftResult[band]; + if (random8() % restvolume == 0) + { + emitparticles = 1; + } + } + while (i < numParticles && emitparticles) // emit particles if there are any left, low frequencies take priority + { + if (particles[i].ttl == 0) // find a dead particle + { + //set particle properties + particles[i].ttl = emitspeed; // set particle alive, particle lifespan is in number of frames + particles[i].x = xposition + random8(binwidth) - binwidth>>1; //position randomly, deviating half a bin width + particles[i].y = 0; //start at the bottom + particles[i].vx = rand(9)-4; //x-speed variation + particles[i].vy = emitspeed; + particles[i].hue = bin<<4 + random8(binwidth) - binwidth>>1; // color from palette according to bin + //particles[i].sat = ((SEGMENT.custom3) << 3) + 7; // set saturation + } + emitparticles--; + i++; + } + } + + + uint8_t hardness = SEGMENT.custom2; // how hard the collisions are, 255 = full hard. + // detectCollisions(particles, numParticles, hardness); + + // now move the particles + for (i = 0; i < numParticles; i++) + { + //Particle_Gravity_update(&particles[i], SEGMENT.check1, SEGMENT.check2, SEGMENT.check3, min(hardness, (uint8_t)150)); // surface hardness max is 150 + Particle_Gravity_update(&particles[i], SEGMENT.check1, SEGMENT.check2, SEGMENT.check3, min(hardness, (uint8_t)150)); // surface hardness max is 150 + } + + SEGMENT.fill(BLACK); // clear the matrix + + // render the particles + ParticleSys_render(particles, numParticles, SEGMENT.check1, false); // custom3 slider is saturation, from 7 to 255, 7 is close enough to white (for snow for example) + + return FRAMETIME; +} +static const char _data_FX_MODE_PARTICLEGEQ[] PROGMEM = "Particle GEQ@Speed,Intensity,Randomness,Collision hardness,Saturation,Wrap X,Side bounce,Ground bounce;;!;012;pal=11,sx=100,ix=200,c1=31,c2=0,c3=20,o1=0,o2=0,o3=1"; + #endif // WLED_DISABLE_2D @@ -9565,12 +9677,11 @@ void WS2812FX::setupEffectData() { 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); - -//experimental addEffect(FX_MODE_PARTICLEWATERFALL, &mode_particlewaterfall, _data_FX_MODE_PARTICLEWATERFALL); addEffect(FX_MODE_PARTICLEIMPACT, &mode_particleimpact, _data_FX_MODE_PARTICLEIMPACT); addEffect(FX_MODE_PARTICLEATTRACTOR, &mode_particleattractor, _data_FX_MODE_PARTICLEATTRACTOR); addEffect(FX_MODE_PARTICLESPRAY, &mode_particlespray, _data_FX_MODE_PARTICLESPRAY); + addEffect(FX_MODE_PARTICLESGEQ, &mode_particleGEQ, _data_FX_MODE_PARTICLEGEQ); #endif // WLED_DISABLE_2D diff --git a/wled00/FX.h b/wled00/FX.h index d35c29ea17..ae17b4a7ba 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -329,7 +329,8 @@ #define FX_MODE_PARTICLEIMPACT 195 #define FX_MODE_PARTICLEWATERFALL 196 #define FX_MODE_PARTICLESPRAY 197 -#define MODE_COUNT 198 +#define FX_MODE_PARTICLESGEQ 198 +#define MODE_COUNT 199 typedef enum mapping1D2D { M12_Pixels = 0, From ecc64ae03dcf7f619218569145b169eea37d00a8 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sat, 16 Mar 2024 11:43:22 +0100 Subject: [PATCH 041/219] Particle GEQ fixes, it now actually works --- wled00/FX.cpp | 74 ++++++++++++++++++++++++++++----------------------- 1 file changed, 40 insertions(+), 34 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 383ed1b5bd..95ed7e83be 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -9328,7 +9328,7 @@ uint16_t mode_particleGEQ(void) return mode_static(); const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; - const uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); + //const uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); #ifdef ESP8266 const uint32_t numParticles = 150; // maximum number of particles @@ -9345,7 +9345,7 @@ uint16_t mode_particleGEQ(void) // calculate the end of the spray data and assign it as the data pointer for the particles: particles = reinterpret_cast(SEGENV.data); // cast the data array into a particle pointer - + uint32_t i; if (SEGMENT.call == 0) // initialization { for (i = 0; i < numParticles; i++) @@ -9368,65 +9368,71 @@ uint16_t mode_particleGEQ(void) //Idea: emit 20 particles at full loudness, can use a shift for that, for example shift by 4 or 5 //in order to also emit particles for not so loud bands, get a bunch of particles based on frame counter and current loudness? //implement it simply first, then add complexity... need to check what looks good - uint32_t i = 0; + i = 0; uint32_t bin; //current bin - uint8_t binwidth = (cols * PS_P_RADIUS - 1)>>4; //emit poisition variation for one bin (+/-) + uint32_t binwidth = (cols * PS_P_RADIUS - 1)>>4; //emit poisition variation for one bin (+/-) + uint8_t emitparticles = 0; for (bin = 0; bin < 16; bin++) { - uint32_t xposition = map(fftResult[band], 0, 255, cols * PS_P_RADIUS - 1); //emit position according to frequency band - uint8_t emitspeed = fftResult[band] >> 2; // emit speed according to loudness of band TODO: SEGMENT.speed? - uint8_t emitparticles = 0; - //wie sollen die emitted werden? man könnte anzahl berechnen, dann einen emit loop machen - if (fftResult[band] > 10) + + uint32_t xposition = binwidth*bin + (binwidth>>1); // emit position according to frequency band + uint8_t emitspeed = fftResult[bin] / map(SEGMENT.speed,0,255,10,1); // emit speed according to loudness of band + emitparticles = 0; + + uint8_t threshold = map(SEGMENT.intensity, 0, 255, 250, 2); + + if (fftResult[bin] > threshold) { - emitparticles = fftResult[band]/10; + emitparticles = 1;// + (fftResult[bin]>>6); } - else if(fftResult[band] > 0)// band has low volue + else if(fftResult[bin] > 0)// band has low volue { - uint32_t restvolume = 12 - fftResult[band]; + uint32_t restvolume = ((threshold - fftResult[bin])>>2) + 2; if (random8() % restvolume == 0) { emitparticles = 1; } } - while (i < numParticles && emitparticles) // emit particles if there are any left, low frequencies take priority + + while (i < numParticles && emitparticles > 0) // emit particles if there are any left, low frequencies take priority { if (particles[i].ttl == 0) // find a dead particle { //set particle properties - particles[i].ttl = emitspeed; // set particle alive, particle lifespan is in number of frames - particles[i].x = xposition + random8(binwidth) - binwidth>>1; //position randomly, deviating half a bin width + particles[i].ttl = map(SEGMENT.intensity, 0,255, emitspeed>>1, emitspeed + random8(emitspeed)) ; // set particle alive, particle lifespan is in number of frames + particles[i].x = xposition + random8(binwidth) - (binwidth>>1); //position randomly, deviating half a bin width particles[i].y = 0; //start at the bottom - particles[i].vx = rand(9)-4; //x-speed variation + particles[i].vx = random8(SEGMENT.custom1>>1)-(SEGMENT.custom1>>2) ; //x-speed variation particles[i].vy = emitspeed; - particles[i].hue = bin<<4 + random8(binwidth) - binwidth>>1; // color from palette according to bin - //particles[i].sat = ((SEGMENT.custom3) << 3) + 7; // set saturation + particles[i].hue = (bin<<4) + random8(17) - 8; // color from palette according to bin + //particles[i].sat = ((SEGMENT.custom3) << 3) + 7; // set saturation + emitparticles--; } - emitparticles--; - i++; + i++; } } + // Serial.println(" "); - uint8_t hardness = SEGMENT.custom2; // how hard the collisions are, 255 = full hard. - // detectCollisions(particles, numParticles, hardness); + uint8_t hardness = SEGMENT.custom2; // how hard the collisions are, 255 = full hard. + // detectCollisions(particles, numParticles, hardness); - // now move the particles - for (i = 0; i < numParticles; i++) - { - //Particle_Gravity_update(&particles[i], SEGMENT.check1, SEGMENT.check2, SEGMENT.check3, min(hardness, (uint8_t)150)); // surface hardness max is 150 - Particle_Gravity_update(&particles[i], SEGMENT.check1, SEGMENT.check2, SEGMENT.check3, min(hardness, (uint8_t)150)); // surface hardness max is 150 - } + // now move the particles + for (i = 0; i < numParticles; i++) + { + particles[i].vy -= (SEGMENT.custom3>>3); // apply stronger gravity + Particle_Gravity_update(&particles[i], SEGMENT.check1, SEGMENT.check2, SEGMENT.check3, hardness); + } - SEGMENT.fill(BLACK); // clear the matrix + SEGMENT.fill(BLACK); // clear the matrix - // render the particles - ParticleSys_render(particles, numParticles, SEGMENT.check1, false); // custom3 slider is saturation, from 7 to 255, 7 is close enough to white (for snow for example) + // render the particles + ParticleSys_render(particles, numParticles, SEGMENT.check1, false); // custom3 slider is saturation - return FRAMETIME; -} -static const char _data_FX_MODE_PARTICLEGEQ[] PROGMEM = "Particle GEQ@Speed,Intensity,Randomness,Collision hardness,Saturation,Wrap X,Side bounce,Ground bounce;;!;012;pal=11,sx=100,ix=200,c1=31,c2=0,c3=20,o1=0,o2=0,o3=1"; + return FRAMETIME; + } +static const char _data_FX_MODE_PARTICLEGEQ[] PROGMEM = "Particle GEQ@Speed,Intensity,Randomness,Collision hardness,Gravity,Wrap X,Side bounce,Ground bounce;;!;012;pal=54,sx=100,ix=200,c1=0,c2=0,c3=0,o1=0,o2=0,o3=0"; #endif // WLED_DISABLE_2D From 79917762291bcde032a01d0fea3c738e76acd483 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sun, 17 Mar 2024 15:41:46 +0100 Subject: [PATCH 042/219] GEQ FX parameter tuning --- wled00/FX.cpp | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 95ed7e83be..f881fcf60f 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -9371,17 +9371,15 @@ uint16_t mode_particleGEQ(void) i = 0; uint32_t bin; //current bin uint32_t binwidth = (cols * PS_P_RADIUS - 1)>>4; //emit poisition variation for one bin (+/-) - uint8_t emitparticles = 0; + uint32_t threshold = 300 - SEGMENT.intensity; + uint32_t emitparticles = 0; for (bin = 0; bin < 16; bin++) { - uint32_t xposition = binwidth*bin + (binwidth>>1); // emit position according to frequency band - uint8_t emitspeed = fftResult[bin] / map(SEGMENT.speed,0,255,10,1); // emit speed according to loudness of band + uint8_t emitspeed = 5+((uint32_t)fftResult[bin]*(uint32_t)SEGMENT.speed)>>9; // emit speed according to loudness of band emitparticles = 0; - uint8_t threshold = map(SEGMENT.intensity, 0, 255, 250, 2); - if (fftResult[bin] > threshold) { emitparticles = 1;// + (fftResult[bin]>>6); From a56d888f8d7c3e7253fb1427dff9ad12039410f0 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Wed, 20 Mar 2024 19:39:26 +0100 Subject: [PATCH 043/219] added rotating GEQ, work in progress -animation works but sliders are too sensitive. need to adjust the ranges --- wled00/FX.cpp | 141 +++++++++++++++++++++++++++++++++++++++++++++++++- wled00/FX.h | 3 +- 2 files changed, 142 insertions(+), 2 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index f881fcf60f..a3ecd2c229 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -9377,7 +9377,7 @@ uint16_t mode_particleGEQ(void) for (bin = 0; bin < 16; bin++) { uint32_t xposition = binwidth*bin + (binwidth>>1); // emit position according to frequency band - uint8_t emitspeed = 5+((uint32_t)fftResult[bin]*(uint32_t)SEGMENT.speed)>>9; // emit speed according to loudness of band + uint8_t emitspeed = 5 + (((uint32_t)fftResult[bin]*(uint32_t)SEGMENT.speed)>>9); // emit speed according to loudness of band emitparticles = 0; if (fftResult[bin] > threshold) @@ -9432,6 +9432,144 @@ uint16_t mode_particleGEQ(void) } static const char _data_FX_MODE_PARTICLEGEQ[] PROGMEM = "Particle GEQ@Speed,Intensity,Randomness,Collision hardness,Gravity,Wrap X,Side bounce,Ground bounce;;!;012;pal=54,sx=100,ix=200,c1=0,c2=0,c3=0,o1=0,o2=0,o3=0"; +/* + * Particle rotating spray + * Particles sprayed from center with a rotating spray + * Uses palette for particle color + * by DedeHai (Damian Schneider) + */ + +uint16_t mode_particlecenterGEQ(void) +{ + + if (SEGLEN == 1) + return mode_static(); + + const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; + const uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); + +#ifdef ESP8266 + const uint32_t numParticles = 50; // maximum number of particles +#else + const uint32_t numParticles = 500; // maximum number of particles +#endif + + const uint8_t numSprays = 16; // maximum number of sprays + + 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 + + uint32_t i = 0; + uint32_t j = 0; + uint8_t spraycount = 1 + (SEGMENT.custom2 >> 5); // number of sprays to display, 1-8 + + if (SEGMENT.call == 0) // initialization + { + SEGMENT.aux0 = 0; // starting angle + SEGMENT.aux1 = 0xFF; // user check + for (i = 0; i < numParticles; i++) + { + particles[i].ttl = 0; + } + for (i = 0; i < numSprays; i++) + { + spray[i].source.hue = i*16; //even color distribution + 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 + } + } + + um_data_t *um_data; + if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) + { + // add support for no audio + um_data = simulateSound(SEGMENT.soundSim); + } + + uint8_t *fftResult = (uint8_t *)um_data->u_data[2]; // 16 bins with FFT data, log mapped already, each band contains frequency amplitude 0-255 + + i = 0; + + uint32_t threshold = 300 - SEGMENT.intensity; + + + uint8_t percycle = numSprays; // maximum number of particles emitted per cycle + i = 0; + j = random(numSprays); // start with random spray so all get a chance to emit a particle if maximum number of particles alive is reached. + if (SEGMENT.check2) + SEGMENT.aux0 += SEGMENT.custom1; + else + SEGMENT.aux0 -= SEGMENT.custom1; + + uint32_t angleoffset = SEGMENT.aux0 >> 4; + + while (i < numParticles) + { + if (particles[i].ttl == 0) // find a dead particle + { + uint8_t emitspeed = 5 + (((uint32_t)fftResult[j] * ((uint32_t)SEGMENT.speed+10)) >> 9); // emit speed according to loudness of band + uint8_t emitangle = j * 16 + random(SEGMENT.custom3 >> 1) + angleoffset; + + uint32_t emitparticles = 0; + if (fftResult[j] > threshold) + { + emitparticles = 1; // + (fftResult[bin]>>6); + } + else if (fftResult[j] > 0) // band has low volue + { + uint32_t restvolume = ((threshold - fftResult[j]) >> 2) + 2; + if (random8() % restvolume == 0) + { + emitparticles = 1; + } + } + if (emitparticles) + Emitter_Angle_emit(&spray[j], &particles[i], emitangle, emitspeed); + j = (j + 1) % numSprays; + } + i++; + //todo: could add a break if all 16 sprays have been checked, would speed it up + } + +/* + if (SEGMENT.check2) + SEGMENT.aux0 += SEGMENT.custom1 << 2; + else + SEGMENT.aux0 -= SEGMENT.custom1 << 2; +*/ //TODO: add rotation + + for (i = 0; i < numParticles; i++) + { + Particle_Move_update(&particles[i], true); // move the particles, kill out of bounds particles + } + + SEGMENT.fill(BLACK); // clear the matrix + + // render the particles + ParticleSys_render(particles, numParticles, false, false); + + return FRAMETIME; + } +static const char _data_FX_MODE_PARTICLECCIRCULARGEQ[] PROGMEM = "PS Center GEQ@Speed,Color Change,Particle Speed,Spray Count,Nozzle Size,Random Color, Direction;;!;012;pal=56,sx=0,ix=222,c1=190,c2=200,c3=0,o1=0,o2=0"; + #endif // WLED_DISABLE_2D @@ -9686,6 +9824,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.h b/wled00/FX.h index ae17b4a7ba..546d1350cb 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -330,7 +330,8 @@ #define FX_MODE_PARTICLEWATERFALL 196 #define FX_MODE_PARTICLESPRAY 197 #define FX_MODE_PARTICLESGEQ 198 -#define MODE_COUNT 199 +#define FX_MODE_PARTICLECENTERGEQ 199 +#define MODE_COUNT 200 typedef enum mapping1D2D { M12_Pixels = 0, From ff9d2ebd44544e0883f4c765cc10955bf2b30db5 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sun, 17 Mar 2024 21:59:42 +0100 Subject: [PATCH 044/219] FX update - changed firework exhaust to low saturation - updated rotating particle spray animation --- wled00/FX.cpp | 87 +++++++++++++++++++++++++++++++-------------------- 1 file changed, 53 insertions(+), 34 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index a3ecd2c229..d3343eb3b3 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -7920,19 +7920,18 @@ uint16_t mode_particlerotatingspray(void) uint32_t i = 0; uint32_t j = 0; - uint8_t spraycount = 1 + (SEGMENT.custom2 >> 5); // number of sprays to display, 1-8 + 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 = 0xFF; // user check + SEGMENT.aux1 = 0x01; // check flags for (i = 0; i < numParticles; i++) { particles[i].ttl = 0; } for (i = 0; i < numSprays; i++) - { - spray[i].source.hue = random8(); //TODO: how to keep track of options? can use SEGMENT.aux1: change hue to random or rainbow depending on check but need to find out when it changed. + { 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 @@ -7945,28 +7944,24 @@ uint16_t mode_particlerotatingspray(void) spray[i].var = 0; // emitting variation } } - - // change source emitting color from time to time - if (SEGMENT.call % ((263 - SEGMENT.intensity) >> 3) == 0) // every nth frame, cycle color and update hue if necessary + + if (SEGMENT.check1 != (SEGMENT.aux1 & 0x01)) //state change { + if (SEGMENT.check1) + SEGMENT.aux1 |= 0x01; //set the flag + else + SEGMENT.aux1 &= ~0x01; // clear the flag + for (i = 0; i < spraycount; i++) - { - spray[i].source.hue++; // = random8(); //change hue of spray source - } - if (SEGMENT.check1 != SEGMENT.aux1) { - SEGMENT.aux1 = SEGMENT.check1; - for (i = 0; i < spraycount; i++) + if (SEGMENT.check1) // random color is checked { - if (SEGMENT.check1) // random color is checked - { - spray[i].source.hue = random8(); - } - else - { - uint8_t coloroffset = 0xFF / spraycount; - spray[i].source.hue = coloroffset * i; - } + spray[i].source.hue = random8(); + } + else + { + uint8_t coloroffset = 0xFF / spraycount; + spray[i].source.hue = coloroffset * i; } } } @@ -7989,20 +7984,44 @@ uint16_t mode_particlerotatingspray(void) i++; } + //set rotation direction and speed + int32_t rotationspeed = SEGMENT.speed << 2; + bool direction = SEGMENT.check2; + + if (SEGMENT.custom2 > 0) // automatic direction change enabled + { + uint16_t changeinterval = (265 - SEGMENT.custom2); + direction = SEGMENT.aux1 & 0x02; //set direction according to flag + + if (SEGMENT.check3) // random interval + { + changeinterval = 20 + changeinterval + random16(changeinterval); + } + + if (SEGMENT.call % changeinterval == 0) //flip direction on next frame + { + SEGMENT.aux1 |= 0x04; // set the update flag (for random interval update) + if (direction) + SEGMENT.aux1 &= ~0x02; // clear the direction flag + else + SEGMENT.aux1 |= 0x02; // set the direction flag + } + } + + if (direction) + SEGMENT.aux0 += rotationspeed << 2; + else + SEGMENT.aux0 -= rotationspeed << 2; + // calculate angle offset for an even distribution uint16_t angleoffset = 0xFFFF / spraycount; for (i = 0; i < spraycount; i++) { - if (SEGMENT.check2) - SEGMENT.aux0 += SEGMENT.speed << 2; - else - SEGMENT.aux0 -= SEGMENT.speed << 2; - // 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.custom1) >> 3); // update spray angle (rotate all sprays with angle offset) - spray[i].vy = (sin16(SEGMENT.aux0 + angleoffset * i) >> 8) / ((263 - SEGMENT.custom1) >> 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) + 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) } for (i = 0; i < numParticles; i++) @@ -8017,7 +8036,7 @@ uint16_t mode_particlerotatingspray(void) return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEROTATINGSPRAY[] PROGMEM = "Rotating Particle Spray@Rotation Speed,Color Change,Particle Speed,Spray Count,Nozzle Size,Random Color, Direction;;!;012;pal=56,sx=18,ix=222,c1=190,c2=200,c3=0,o1=0,o2=0"; +static const char _data_FX_MODE_PARTICLEROTATINGSPRAY[] PROGMEM = "Rotating Particle Spray@Rotation Speed,Particle Speed,Spray Count,Flip Speed, Nozzle Size,Random Color, Direction, Random Flip;;!;012;pal=56,sx=18,ix=190,c1=200,c2=0,c3=0,o1=0,o2=0,o3=0"; /* * Particle Fireworks @@ -8199,9 +8218,9 @@ uint16_t mode_particlefireworks(void) rockets[i].source.vy = random8(SEGMENT.custom1>>3) + 5; //rocket speed depends also on rocket height rockets[i].source.vx = random8(5) - 2; rockets[i].source.hue = 30; // rocket exhaust = orange (if using rainbow palette) - rockets[i].source.sat = 250; - rockets[i].source.ttl = random8(SEGMENT.custom1) + (SEGMENT.custom1>>1); // sets explosion height (rockets explode at the top if set too high as paticle update set speed to zero if moving out of matrix) - rockets[i].maxLife = 30; //exhaust particle life + rockets[i].source.sat = 30; // low saturation -> exhaust is off-white + rockets[i].source.ttl = random8(SEGMENT.custom1) + (SEGMENT.custom1 >> 1); // sets explosion height (rockets explode at the top if set too high as paticle update set speed to zero if moving out of matrix) + rockets[i].maxLife = 30; // exhaust particle life rockets[i].minLife = 10; rockets[i].vx = 0; // emitting speed rockets[i].vy = 0; // emitting speed From 7026159da3e87c73952d129a4c923c7f109f862d Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Tue, 19 Mar 2024 20:17:13 +0100 Subject: [PATCH 045/219] Cleanup & Bugfixes plus major improvements for ESP8266 -added particle reductions for ESP8266 for all FX -Removed collisions from Particle Perlin Noise FX, slows things down and does not contribute to a better effect experience -lots of optimizations for ESP8266, all FX now work (at least on 160MHz but still slow) -Some bugfixes -removed unused variables to make compiler happy --- wled00/FX.cpp | 269 +++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 213 insertions(+), 56 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index d3343eb3b3..917c4aa738 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -7894,9 +7894,15 @@ uint16_t mode_particlerotatingspray(void) if (SEGLEN == 1) return mode_static(); +<<<<<<< HEAD const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; const uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); +======= + const uint32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; + const uint32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); + +>>>>>>> 6d70b6ab (Cleanup & Bugfixes plus major improvements for ESP8266) #ifdef ESP8266 const uint32_t numParticles = 150; // maximum number of particles #else @@ -8051,16 +8057,25 @@ uint16_t mode_particlefireworks(void) if (SEGLEN == 1) return mode_static(); - const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; - const uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); + const uint32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; + const uint32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); // particle system box dimensions +<<<<<<< HEAD const uint16_t PS_MAX_X=(cols * PS_P_RADIUS - 1); const uint16_t PS_MAX_Y=(rows * PS_P_RADIUS - 1); #ifdef ESP8266 const uint32_t numParticles = 250; const uint8_t MaxNumRockets = 4; +======= + const uint32_t PS_MAX_X = (cols * PS_P_RADIUS - 1); + const uint32_t PS_MAX_Y = (rows * PS_P_RADIUS - 1); + +#ifdef ESP8266 + const uint32_t numParticles = 100; + const uint8_t MaxNumRockets = 2; +>>>>>>> 6d70b6ab (Cleanup & Bugfixes plus major improvements for ESP8266) #else const uint32_t numParticles = 650; const uint8_t MaxNumRockets = 8; @@ -8085,7 +8100,11 @@ uint16_t mode_particlefireworks(void) uint32_t i = 0; uint32_t j = 0; +<<<<<<< HEAD uint8_t numRockets = 1+ ((SEGMENT.custom3) >> 2); //1 to 8 +======= + uint8_t numRockets = min(uint8_t(1 + ((SEGMENT.custom3) >> 2)), MaxNumRockets); // 1 to 8 +>>>>>>> 6d70b6ab (Cleanup & Bugfixes plus major improvements for ESP8266) if (SEGMENT.call == 0) // initialization { @@ -8119,12 +8138,26 @@ uint16_t mode_particlefireworks(void) emitparticles = 0; } else +<<<<<<< HEAD { // speed is zero, explode! emitparticles = random8(SEGMENT.intensity>>1) + 10; // defines the size of the explosion rockets[j].source.vy = -1; // set speed negative so it will emit no more particles after this explosion until relaunch if (j == circularexplosion || j == spiralexplosion) // chosen rocket, do an angle emit (creating a circle) { emitparticles>>3; //emit less particles for circle-explosions +======= + { // speed is zero, explode! + + #ifdef ESP8266 + emitparticles = random8(SEGMENT.intensity >> 2) + 10; // defines the size of the explosion + #else + emitparticles = random8(SEGMENT.intensity >> 1) + 10; // 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 (j == circularexplosion || j == spiralexplosion) // chosen rocket, do an angle emit (creating a circle) + { + emitparticles = emitparticles >> 3; // emit less particles for circle-explosions +>>>>>>> 6d70b6ab (Cleanup & Bugfixes plus major improvements for ESP8266) rockets[j].maxLife = 150; rockets[j].minLife = 120; rockets[j].var = 0; // speed variation around vx,vy (+/- var/2) @@ -8136,7 +8169,7 @@ uint16_t mode_particlefireworks(void) if (j == spiralexplosion) angle = random(8); - for (i; i < numParticles; i++) + while(i < numParticles) { if (particles[i].ttl == 0) { // particle is dead @@ -8163,9 +8196,14 @@ uint16_t mode_particlefireworks(void) else if (j == spiralexplosion && emitparticles > 2) // do spiral emit { Emitter_Angle_emit(&rockets[j], &particles[i], angle, speed); +<<<<<<< HEAD emitparticles--; emitparticles--;//only emit half as many particles as in circle explosion, it gets too huge otherwise angle += 15; +======= + emitparticles-=2; // only emit half as many particles as in circle explosion, it gets too huge otherwise + angle += 15; +>>>>>>> 6d70b6ab (Cleanup & Bugfixes plus major improvements for ESP8266) speed++; rockets[j].source.hue++; rockets[j].source.sat = random8(155)+100; @@ -8179,6 +8217,7 @@ uint16_t mode_particlefireworks(void) else break; // done emitting for this rocket } + i++; } } @@ -8249,12 +8288,24 @@ uint16_t mode_particlevolcano(void) if (SEGLEN == 1) return mode_static(); +<<<<<<< HEAD const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; const uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); //particle system x dimension const uint16_t PS_MAX_X = (cols * PS_P_RADIUS - 1); +======= + const uint32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; + + // particle system x dimension + const uint32_t PS_MAX_X = (cols * PS_P_RADIUS - 1); +>>>>>>> 6d70b6ab (Cleanup & Bugfixes plus major improvements for ESP8266) - const uint32_t numParticles = 450; +#ifdef ESP8266 + const uint32_t numParticles = 100; // maximum number of particles +#else + const uint32_t numParticles = 450; // maximum number of particles +#endif + const uint8_t numSprays = 1; uint8_t percycle = numSprays; // maximum number of particles emitted per cycle @@ -8316,7 +8367,12 @@ uint16_t mode_particlevolcano(void) } else{ //wrap on the right side spray[i].source.vx = SEGMENT.speed >> 4; // spray speed +<<<<<<< HEAD if (spray[i].source.x >= PS_MAX_X-32) spray[i].source.x = 1; //wrap if close to border (need to wrap before the bounce updated detects a border collision or it will just be stuck) +======= + if (spray[i].source.x >= PS_MAX_X - 32) //compiler warning can be ignored, source.x is always > 0 + spray[i].source.x = 1; // wrap if close to border (need to wrap before the bounce updated detects a border collision or it will just be stuck) +>>>>>>> 6d70b6ab (Cleanup & Bugfixes plus major improvements for ESP8266) } spray[i].vy = SEGMENT.custom1 >> 2; // emitting speed, upward spray[i].vx = 0; @@ -8364,7 +8420,7 @@ uint16_t mode_particlevolcano(void) ParticleSys_render(particles, numParticles, false, false); return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEVOLCANO[] PROGMEM = "Particle Volcano@Moving Speed,Intensity,Particle Speed,Bouncyness,Nozzle Size,Color by Age,Bounce X,Collisions;;!;012;pal=35,sx=0,ix=160,c1=100,c2=160,c3=10,o1=1,o2=1,o3=1"; +static const char _data_FX_MODE_PARTICLEVOLCANO[] PROGMEM = "Particle Volcano@Move,Intensity,Speed,Bounce,Size,Color by Age,Bounce X,Collisions;;!;012;pal=35,sx=0,ix=160,c1=100,c2=160,c3=10,o1=1,o2=0,o3=0"; /* * Particle Fire @@ -8377,15 +8433,21 @@ uint16_t mode_particlefire(void) if (SEGLEN == 1) return mode_static(); - const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; - const uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); + const uint32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; // particle system box dimensions - const uint16_t PS_MAX_X(cols * PS_P_RADIUS - 1); - const uint16_t PS_MAX_Y(rows * PS_P_RADIUS - 1); - + const uint32_t PS_MAX_X = (cols * PS_P_RADIUS - 1); + + #ifdef ESP8266 + const uint32_t numFlames = min((uint32_t)10, cols); // limit to 10 flames, not enough ram on ESP8266 + const uint32_t numParticles = numFlames * 16; + const uint32_t numNormalFlames = numFlames - (cols >> 2); // number of normal flames, rest of flames are baseflames + #else const uint32_t numFlames = (cols << 1); // number of flames: depends on fire width. for a fire width of 16 pixels, about 25-30 flames give good results, add a few for the base flames const uint32_t numParticles = numFlames * 25; + const uint32_t numNormalFlames = numFlames - (cols >> 1); // number of normal flames, rest of flames are baseflames + #endif + uint8_t percycle = numFlames >> 1; // maximum number of particles emitted per cycle PSparticle *particles; PSpointsource *flames; @@ -8419,7 +8481,13 @@ uint16_t mode_particlefire(void) { flames[i].source.ttl = 0; flames[i].source.x = PS_P_RADIUS * 3 + random16(PS_MAX_X - (PS_P_RADIUS * 6)); // distribute randomly but not close to the corners +<<<<<<< HEAD //note: other parameters are set when creating the flame (see blow) +======= + flames[i].source.vx = 0; // emitter moving speed; + flames[i].source.vy = 0; + // note: other parameters are set when creating the flame (see blow) +>>>>>>> 6d70b6ab (Cleanup & Bugfixes plus major improvements for ESP8266) } } @@ -8449,23 +8517,34 @@ uint16_t mode_particlefire(void) } } +<<<<<<< HEAD if (i < (numFlames - (cols >> 1))) { // all but the last few are normal flames flames[i].source.y = -1 * PS_P_RADIUS; // set the source below the frame so particles alredy spread a little when the appear flames[i].source.vx = 0; // (rand() % 3) - 1; flames[i].source.vy = 0; +======= + flames[i].source.y = -1 * PS_P_RADIUS; // set the source below the frame + + if (i < numNormalFlames) + { // all but the last few are normal flames +>>>>>>> 6d70b6ab (Cleanup & Bugfixes plus major improvements for ESP8266) flames[i].source.ttl = random8(SEGMENT.intensity >> 2) / (1 + (SEGMENT.speed >> 6)) + 10; //'hotness' of fire, faster flames reduce the effect or flame height will scale too much with speed flames[i].maxLife = random8(7) + 13; // defines flame height together with the vy speed, vy speed*maxlife/PS_P_RADIUS is the average flame height - flames[i].minLife = 2; + flames[i].minLife = 3; flames[i].vx = (int8_t)random8(4) - 2; // emitting speed (sideways) flames[i].vy = 5 + (SEGMENT.speed >> 2); // emitting speed (upwards) flames[i].var = random8(5) + 3; // speed variation around vx,vy (+/- var/2) } else +<<<<<<< HEAD { // base flames to make the base brighter, flames are slower and short lived flames[i].source.y = -1 * PS_P_RADIUS; // set the source below the frame flames[i].source.vx = 0; flames[i].source.vy = 0; // emitter moving speed; +======= + { // base flames to make the base brighter, flames are slower and short lived +>>>>>>> 6d70b6ab (Cleanup & Bugfixes plus major improvements for ESP8266) flames[i].source.ttl = random8(25) + 15; // lifetime of one flame flames[i].maxLife = 25; // defines flame height together with the vy speed, vy speed*maxlife/PS_P_RADIUS is the average flame height flames[i].minLife = 12; @@ -8508,7 +8587,7 @@ uint16_t mode_particlefire(void) return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEFIRE[] PROGMEM = "Particle Fire@Speed,Intensity,Base Flames,Wind Speed, Color Scheme, WrapX;;!;012;sx=100,ix=120,c1=16,c2=128,c3=0,o1=0"; +static const char _data_FX_MODE_PARTICLEFIRE[] PROGMEM = "Particle Fire@Speed,Intensity,Base Flames,Wind Speed, Color Scheme, WrapX;;!;012;sx=100,ix=120,c1=30,c2=128,c3=0,o1=0"; /* @@ -8525,10 +8604,14 @@ uint16_t mode_particlefall(void) if (SEGLEN == 1) return mode_static(); - const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; - const uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); + const uint32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; + const uint32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); - const uint32_t numParticles = 500; +#ifdef ESP8266 + const uint32_t numParticles = 100; // maximum number of particles +#else + const uint32_t numParticles = 500; // maximum number of particles +#endif PSparticle *particles; @@ -8597,7 +8680,7 @@ uint16_t mode_particlefall(void) return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEFALL[] PROGMEM = "Falling Particles@Speed,Intensity,Randomness,Collision hardness,Saturation,Wrap X,Side bounce,Ground bounce;;!;012;pal=11,sx=100,ix=200,c1=31,c2=0,c3=20,o1=0,o2=0,o3=1"; +static const char _data_FX_MODE_PARTICLEFALL[] PROGMEM = "Falling Particles@Speed,Intensity,Randomness,Collision hardness,Saturation,Wrap X,Side bounce,Ground bounce;;!;012;pal=11,sx=100,ix=200,c1=31,c2=100,c3=20,o1=0,o2=0,o3=1"; /* * Particle Waterfall @@ -8611,11 +8694,17 @@ uint16_t mode_particlewaterfall(void) if (SEGLEN == 1) return mode_static(); - const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; - const uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); + const uint32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; + const uint32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); - const uint32_t numParticles = 500; +#ifdef ESP8266 + const uint32_t numParticles = 100; // maximum number of particles + const uint8_t numSprays = 1; +#else + const uint32_t numParticles = 500; // maximum number of particles const uint8_t numSprays = 2; +#endif + uint8_t percycle = numSprays; // maximum number of particles emitted per cycle PSparticle *particles; @@ -8647,11 +8736,25 @@ uint16_t mode_particlewaterfall(void) spray[i].source.x = (cols * PS_P_RADIUS) / 2 - PS_P_RADIUS + 2*PS_P_RADIUS * (i); spray[i].source.y = (rows+4) * (PS_P_RADIUS*(i+1)); // source y position, few pixels above the top to increase spreading before entering the matrix spray[i].source.vx = 0; +<<<<<<< HEAD spray[i].source.collide = true; //seeded particles will collide spray[i].maxLife = 600; // lifetime in frames spray[i].minLife = 200; spray[i].vx = 0; // emitting speed spray[i].var = 7; // emiting variation +======= + spray[i].source.collide = true; // seeded particles will collide + #ifdef ESP8266 + spray[i].maxLife = 100; // lifetime in frames + spray[i].minLife = 50; + #else + spray[i].maxLife = 400; // lifetime in frames + spray[i].minLife = 150; + #endif + + spray[i].vx = 0; // emitting speed + spray[i].var = 7; // emiting variation +>>>>>>> 6d70b6ab (Cleanup & Bugfixes plus major improvements for ESP8266) } } @@ -8733,10 +8836,14 @@ uint16_t mode_particlebox(void) if (SEGLEN == 1) return mode_static(); - const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; - const uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); + 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 = 80; // maximum number of particles +#else const uint32_t numParticles = 255; // maximum number of particles +#endif PSparticle *particles; @@ -8747,7 +8854,6 @@ uint16_t mode_particlebox(void) particles = reinterpret_cast(SEGENV.data); // cast the data array into a particle pointer uint32_t i = 0; - uint32_t j = 0; if (SEGMENT.call == 0) // initialization { @@ -8763,17 +8869,15 @@ uint16_t mode_particlebox(void) } } - uint16_t displayparticles = SEGMENT.intensity; + uint16_t displayparticles = map(SEGMENT.intensity, 0, 255, 10, numParticles); i = 0; - j = 0; if (SEGMENT.call % (((255 - SEGMENT.speed) >> 6) + 1) == 0 && SEGMENT.speed > 0) // how often the force is applied depends on speed setting { int32_t xgravity; int32_t ygravity; - uint8_t scale; SEGMENT.aux0 += (SEGMENT.speed >> 6) + 1; // update position in noise @@ -8788,9 +8892,6 @@ uint16_t mode_particlebox(void) // scale the gravity force down xgravity /= 16; ygravity /= 16; - Serial.print(xgravity); - Serial.print(" "); - Serial.println(ygravity); for (i = 0; i < numParticles; i++) { @@ -8828,9 +8929,10 @@ uint16_t mode_particlebox(void) static const char _data_FX_MODE_PARTICLEBOX[] PROGMEM = "Particle Box@Speed,Particles,Tilt strength,Hardness,,Sloshing;;!;012;pal=1,sx=120,ix=100,c1=190,c2=210,o1=0"; /* -perlin noise 'gravity' mapping as in particles on noise hills viewed from above -calculates slope gradient at the particle positions +Perlin Noise 'gravity' mapping as in particles on 'noise hills' viewed from above +calculates slope gradient at the particle positions and applies 'downhill' force restults in a fuzzy perlin noise display +by DedeHai (Damian Schneider) */ uint16_t mode_particleperlin(void) @@ -8839,12 +8941,12 @@ uint16_t mode_particleperlin(void) if (SEGLEN == 1) return mode_static(); - const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; - const uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); + 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 = 150; + const uint32_t numParticles = 80; #else const uint32_t numParticles = 350; #endif @@ -8858,7 +8960,6 @@ uint16_t mode_particleperlin(void) particles = reinterpret_cast(SEGENV.data); // cast the data array into a particle pointer uint32_t i = 0; - uint32_t j = 0; if (SEGMENT.call == 0) // initialization { @@ -8901,11 +9002,14 @@ uint16_t mode_particleperlin(void) particles[i].vy += yslope >> 1; } } +<<<<<<< HEAD uint8_t hardness = SEGMENT.custom1; // how hard the collisions are, 255 = full hard. if(SEGMENT.check1) //collisions enabled { detectCollisions(particles, displayparticles, hardness); } +======= +>>>>>>> 6d70b6ab (Cleanup & Bugfixes plus major improvements for ESP8266) // move particles for (i = 0; i < displayparticles; i++) @@ -8914,7 +9018,7 @@ uint16_t mode_particleperlin(void) if (SEGMENT.call % (16-(SEGMENT.custom2>>4)) == 0) // need to apply friction very rarely or particles will clump applyFriction(&particles[i], 1); - Particle_Bounce_update(&particles[i], hardness); + Particle_Bounce_update(&particles[i], 255); } SEGMENT.fill(BLACK); // clear the matrix @@ -8924,7 +9028,7 @@ uint16_t mode_particleperlin(void) return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEPERLIN[] PROGMEM = "Particle Perlin-Noise@Speed,Particles,Collision Hardness,Friction,Scale,Collisions;;!;012;pal=54,sx=70;ix=200,c1=190,c2=120,c3=4,o1=0"; +static const char _data_FX_MODE_PARTICLEPERLIN[] PROGMEM = "Particle Perlin-Noise@Speed,Particles,,Friction,Scale;;!;012;pal=54,sx=70;ix=200,c1=120,c2=120,c3=4,o1=0"; /* @@ -8937,16 +9041,16 @@ uint16_t mode_particleimpact(void) if (SEGLEN == 1) return mode_static(); - const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; - const uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); + const uint32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; + const uint32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); // particle system box dimensions - const uint16_t PS_MAX_X(cols * PS_P_RADIUS - 1); - const uint16_t PS_MAX_Y(rows * PS_P_RADIUS - 1); + const uint32_t PS_MAX_X(cols * PS_P_RADIUS - 1); + const uint32_t PS_MAX_Y(rows * PS_P_RADIUS - 1); #ifdef ESP8266 - const uint32_t numParticles = 250; - const uint8_t MaxNumMeteors = 4; + const uint32_t numParticles = 150; + const uint8_t MaxNumMeteors = 2; #else const uint32_t numParticles = 550; const uint8_t MaxNumMeteors = 8; @@ -8993,7 +9097,11 @@ uint16_t mode_particleimpact(void) // determine meteor state by its speed: if (meteors[j].source.vy < 0) // moving down, emit sparks { + #ifdef ESP8266 + emitparticles = 1; + #else emitparticles = 2; + #endif } else if (meteors[j].source.vy > 0) // moving up means meteor is on 'standby' { @@ -9002,10 +9110,14 @@ uint16_t mode_particleimpact(void) else // speed is zero, explode! { meteors[j].source.vy = 125; // set source speed positive so it goes into timeout and launches again + #ifdef ESP8266 + emitparticles = random8(SEGMENT.intensity >> 2) + 10; // defines the size of the explosion + #else emitparticles = random8(SEGMENT.intensity >> 1) + 10; // defines the size of the explosion + #endif } - for (i; i < numParticles; i++) + while(i < numParticles) { if (particles[i].ttl == 0) // particle is dead { @@ -9017,6 +9129,7 @@ uint16_t mode_particleimpact(void) else break; // done emitting for this meteor } + i++; } } @@ -9042,6 +9155,7 @@ uint16_t mode_particleimpact(void) { Particle_Gravity_update(&meteors[i].source, SEGMENT.check1, SEGMENT.check2, true, 255); // move the meteor, age the meteor (ttl--) if (meteors[i].source.vy > 0) +<<<<<<< HEAD meteors[i].source.y=5; //'hack' to keep the meteors within frame, as ttl will be set to zero by gravity update if too far out of frame // if source reaches the bottom, set speed to 0 so it will explode on next function call (handled above) if ((meteors[i].source.y < PS_P_RADIUS) && (meteors[i].source.vy < 0)) // reached the bottom pixel on its way down @@ -9057,6 +9171,27 @@ uint16_t mode_particleimpact(void) meteors[i].vy = (SEGMENT.custom1 >> 2); // emitting speed y meteors[i].var = (SEGMENT.custom1 >> 1); // speed variation around vx,vy (+/- var/2) } +======= + meteors[i].source.y = 5; //'hack' to keep the meteors within frame, as ttl will be set to zero by gravity update if too far out of frame + // if source reaches the bottom, set speed to 0 so it will explode on next function call (handled above) + if ((meteors[i].source.y < PS_P_RADIUS) && (meteors[i].source.vy < 0)) // reached the bottom pixel on its way down + { + meteors[i].source.vy = 0; // set speed zero so it will explode + meteors[i].source.vx = 0; + meteors[i].source.y = 5; // offset from ground so explosion happens not out of frame + meteors[i].source.collide = true; // explosion particles will collide if checked + meteors[i].maxLife = 200; + meteors[i].minLife = 50; + #ifdef ESP8266 + meteors[i].source.ttl = random8(255 - (SEGMENT.speed>>1)) + 10; // standby time til next launch (in frames at 42fps, max of 265 is about 6 seconds + #else + meteors[i].source.ttl = random8((255 - SEGMENT.speed)) + 10; // standby time til next launch (in frames at 42fps, max of 265 is about 6 seconds + #endif + meteors[i].vx = 0; // emitting speed x + meteors[i].vy = (SEGMENT.custom1 >> 2); // emitting speed y + meteors[i].var = (SEGMENT.custom1 >> 1); // speed variation around vx,vy (+/- var/2) + } +>>>>>>> 6d70b6ab (Cleanup & Bugfixes plus major improvements for ESP8266) } else if (meteors[i].source.vy > 0) // meteor is exploded and time is up (ttl==0 and positive speed), relaunch it { @@ -9094,14 +9229,14 @@ uint16_t mode_particleattractor(void) { if (SEGLEN == 1) return mode_static(); - const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; - const uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); + const uint32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; + const uint32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); // particle system box dimensions - const uint16_t PS_MAX_X(cols * PS_P_RADIUS - 1); - const uint16_t PS_MAX_Y(rows * PS_P_RADIUS - 1); + const uint32_t PS_MAX_X(cols * PS_P_RADIUS - 1); + const uint32_t PS_MAX_Y(rows * PS_P_RADIUS - 1); #ifdef ESP8266 - const uint32_t numParticles = 150; // maximum number of particles + const uint32_t numParticles = 90; // maximum number of particles #else const uint32_t numParticles = 300; // maximum number of particles #endif @@ -9113,9 +9248,15 @@ uint16_t mode_particleattractor(void) uint8_t *counters; //counters for the applied force // allocate memory and divide it into proper pointers, max is 32k for all segments. +<<<<<<< HEAD uint32_t dataSize = sizeof(PSparticle) * (numParticles + 1); dataSize += sizeof(uint8_t) *numParticles; dataSize += sizeof(PSpointsource); +======= + 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 +>>>>>>> 6d70b6ab (Cleanup & Bugfixes plus major improvements for ESP8266) if (!SEGENV.allocateData(dataSize)) return mode_static(); // allocation failed @@ -9126,7 +9267,6 @@ uint16_t mode_particleattractor(void) counters = reinterpret_cast(spray + 1); uint32_t i; - uint32_t j; if (SEGMENT.call == 0) // initialization { @@ -9155,11 +9295,15 @@ uint16_t mode_particleattractor(void) spray->var = 6; //emitting speed variation } - uint32_t displayparticles = map(SEGMENT.intensity, 0, 255, 1, numParticles); + uint32_t displayparticles = map(SEGMENT.intensity, 0, 255, 1, numParticles) - 1; //TODO: the -1 is a botch fix, it crashes for some reason if going to max number of particles, is this a rounding error? uint8_t hardness = SEGMENT.custom2; // how hard the collisions are, 255 = full hard. i = 0; +<<<<<<< HEAD j = 0; +======= + +>>>>>>> 6d70b6ab (Cleanup & Bugfixes plus major improvements for ESP8266) if (hardness > 1) // enable collisions { detectCollisions(particles, displayparticles, hardness); @@ -9209,13 +9353,17 @@ uint16_t mode_particleattractor(void) else SEGMENT.fill(BLACK); // clear the matrix +<<<<<<< HEAD //ParticleSys_render(&attract, 1, 30, false, false); // render attractor +======= + // ParticleSys_render(&attract, 1, 30, false, false); // render attractor +>>>>>>> 6d70b6ab (Cleanup & Bugfixes plus major improvements for ESP8266) // render the particles ParticleSys_render(particles, displayparticles, false, false); return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEATTRACTOR[] PROGMEM = "Particle Attractor@Center Mass,Particles,Emit Speed,Collision Strength,Friction,Bounce,Trails,Swallow;;!;012;pal=9,sx=100,ix=82,c1=190,c2=210,o1=0,o2=0,o3=0"; +static const char _data_FX_MODE_PARTICLEATTRACTOR[] PROGMEM = "Particle Attractor@Center 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 @@ -9229,13 +9377,18 @@ uint16_t mode_particlespray(void) if (SEGLEN == 1) return mode_static(); - const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; - const uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); + const uint32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; + const uint32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); // particle system x dimension - const uint16_t PS_MAX_X = (cols * PS_P_RADIUS - 1); - const uint16_t PS_MAX_Y = (rows * PS_P_RADIUS - 1); + const uint32_t PS_MAX_X = (cols * PS_P_RADIUS - 1); + const uint32_t PS_MAX_Y = (rows * PS_P_RADIUS - 1); +#ifdef ESP8266 + const uint32_t numParticles = 80; +#else const uint32_t numParticles = 450; +#endif + const uint8_t numSprays = 1; uint8_t percycle = numSprays; // maximum number of particles emitted per cycle @@ -9295,7 +9448,11 @@ uint16_t mode_particlespray(void) if (particles[i].ttl == 0) // find a dead particle { // spray[j].source.hue = random8(); //set random color for each particle (using palette) +<<<<<<< HEAD Emitter_Angle_emit(&spray[j], &particles[i], SEGMENT.custom3<<3, SEGMENT.speed>>2); +======= + Emitter_Angle_emit(&spray[j], &particles[i], 255-(SEGMENT.custom3 << 3), SEGMENT.speed >> 2); +>>>>>>> 6d70b6ab (Cleanup & Bugfixes plus major improvements for ESP8266) j = (j + 1) % numSprays; if (percycle-- == 0) { @@ -9332,7 +9489,7 @@ uint16_t mode_particlespray(void) return FRAMETIME; } -static const char _data_FX_MODE_PARTICLESPRAY[] PROGMEM = "Particle Spray@Particle Speed,Intensity,X Position,Y Position,Angle,Gravity,WrapX/Bounce,Collisions;;!;012;pal=0,sx=180,ix=200,c1=220,c2=30,c3=12,o1=1,o2=0,o3=1"; +static const char _data_FX_MODE_PARTICLESPRAY[] PROGMEM = "Particle Spray@Particle Speed,Intensity,X Position,Y Position,Angle,Gravity,WrapX/Bounce,Collisions;;!;012;pal=0,sx=100,ix=160,c1=100,c2=50,c3=20,o1=0,o2=1,o3=0"; /* Particle base Graphical Equalizer From 913e91025a7d8dcd05496bef0ea34398ffd146f6 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Thu, 21 Mar 2024 15:55:40 +0100 Subject: [PATCH 046/219] Particle FX Rename, default parameter tuning, bugfix -Now shorter names, 'PS' in front to filter the list -Tuned default parameters to make them look better by default -Bugfix in particle system (removed duplicate application of velocity) -reduced PS fire RAM usage (less particles, less base flames, no noticeable difference) -some variable renaming --- wled00/FX.cpp | 225 +++++++++--------------------------- wled00/FXparticleSystem.cpp | 6 - 2 files changed, 53 insertions(+), 178 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 917c4aa738..eadbf19de7 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -7882,7 +7882,7 @@ uint16_t mode_2Dwavingcell() { static const char _data_FX_MODE_2DWAVINGCELL[] PROGMEM = "Waving Cell@!,,Amplitude 1,Amplitude 2,Amplitude 3;;!;2"; /* - * Particle rotating spray + * Particle System Candy (aka rotating sprays) * Particles sprayed from center with a rotating spray * Uses palette for particle color * by DedeHai (Damian Schneider) @@ -7894,15 +7894,9 @@ uint16_t mode_particlerotatingspray(void) if (SEGLEN == 1) return mode_static(); -<<<<<<< HEAD - const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; - const uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); - -======= const uint32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; const uint32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); ->>>>>>> 6d70b6ab (Cleanup & Bugfixes plus major improvements for ESP8266) #ifdef ESP8266 const uint32_t numParticles = 150; // maximum number of particles #else @@ -8042,7 +8036,7 @@ uint16_t mode_particlerotatingspray(void) return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEROTATINGSPRAY[] PROGMEM = "Rotating Particle Spray@Rotation Speed,Particle Speed,Spray Count,Flip Speed, Nozzle Size,Random Color, Direction, Random Flip;;!;012;pal=56,sx=18,ix=190,c1=200,c2=0,c3=0,o1=0,o2=0,o3=0"; +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"; /* * Particle Fireworks @@ -8061,16 +8055,8 @@ uint16_t mode_particlefireworks(void) const uint32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); // particle system box dimensions -<<<<<<< HEAD - const uint16_t PS_MAX_X=(cols * PS_P_RADIUS - 1); - const uint16_t PS_MAX_Y=(rows * PS_P_RADIUS - 1); - -#ifdef ESP8266 - const uint32_t numParticles = 250; - const uint8_t MaxNumRockets = 4; -======= - const uint32_t PS_MAX_X = (cols * PS_P_RADIUS - 1); - const uint32_t PS_MAX_Y = (rows * PS_P_RADIUS - 1); + const uint32_t Max_x = (cols * PS_P_RADIUS - 1); + const uint32_t Max_y = (rows * PS_P_RADIUS - 1); #ifdef ESP8266 const uint32_t numParticles = 100; @@ -8081,7 +8067,6 @@ uint16_t mode_particlefireworks(void) const uint8_t MaxNumRockets = 8; #endif - PSparticle *particles; PSpointsource *rockets; @@ -8091,20 +8076,13 @@ uint16_t mode_particlefireworks(void) if (!SEGENV.allocateData(dataSize)) return mode_static(); // allocation failed; //allocation failed - // DEBUG_PRINT(F("particle datasize = ")); - // DEBUG_PRINTLN(dataSize); - 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 + MaxNumRockets); // cast the data array into a particle pointer uint32_t i = 0; uint32_t j = 0; -<<<<<<< HEAD - uint8_t numRockets = 1+ ((SEGMENT.custom3) >> 2); //1 to 8 -======= uint8_t numRockets = min(uint8_t(1 + ((SEGMENT.custom3) >> 2)), MaxNumRockets); // 1 to 8 ->>>>>>> 6d70b6ab (Cleanup & Bugfixes plus major improvements for ESP8266) if (SEGMENT.call == 0) // initialization { @@ -8138,14 +8116,6 @@ uint16_t mode_particlefireworks(void) emitparticles = 0; } else -<<<<<<< HEAD - { // speed is zero, explode! - emitparticles = random8(SEGMENT.intensity>>1) + 10; // defines the size of the explosion - rockets[j].source.vy = -1; // set speed negative so it will emit no more particles after this explosion until relaunch - if (j == circularexplosion || j == spiralexplosion) // chosen rocket, do an angle emit (creating a circle) - { - emitparticles>>3; //emit less particles for circle-explosions -======= { // speed is zero, explode! #ifdef ESP8266 @@ -8157,7 +8127,6 @@ uint16_t mode_particlefireworks(void) if (j == circularexplosion || j == spiralexplosion) // chosen rocket, do an angle emit (creating a circle) { emitparticles = emitparticles >> 3; // emit less particles for circle-explosions ->>>>>>> 6d70b6ab (Cleanup & Bugfixes plus major improvements for ESP8266) rockets[j].maxLife = 150; rockets[j].minLife = 120; rockets[j].var = 0; // speed variation around vx,vy (+/- var/2) @@ -8196,14 +8165,9 @@ uint16_t mode_particlefireworks(void) else if (j == spiralexplosion && emitparticles > 2) // do spiral emit { Emitter_Angle_emit(&rockets[j], &particles[i], angle, speed); -<<<<<<< HEAD - emitparticles--; - emitparticles--;//only emit half as many particles as in circle explosion, it gets too huge otherwise - angle += 15; -======= emitparticles-=2; // only emit half as many particles as in circle explosion, it gets too huge otherwise angle += 15; ->>>>>>> 6d70b6ab (Cleanup & Bugfixes plus major improvements for ESP8266) + speed++; rockets[j].source.hue++; rockets[j].source.sat = random8(155)+100; @@ -8253,8 +8217,8 @@ uint16_t mode_particlefireworks(void) { // reinitialize rocket rockets[i].source.y = 1; // start from bottom - rockets[i].source.x = (rand() % (PS_MAX_X >> 1)) + (PS_MAX_Y >> 2); // centered half - rockets[i].source.vy = random8(SEGMENT.custom1>>3) + 5; //rocket speed depends also on rocket height + rockets[i].source.x = (rand() % (Max_x >> 1)) + (Max_y >> 2); // centered half + rockets[i].source.vy = random8(SEGMENT.custom1 >> 3) + 5; // rocket speed depends also on rocket height rockets[i].source.vx = random8(5) - 2; rockets[i].source.hue = 30; // rocket exhaust = orange (if using rainbow palette) rockets[i].source.sat = 30; // low saturation -> exhaust is off-white @@ -8273,7 +8237,7 @@ uint16_t mode_particlefireworks(void) return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEFIREWORKS[] PROGMEM = "Particle Fireworks@Launches,Explosion Size,Height,Bounce,Rockets,Wrap X,Bounce X,Bounce Y;;!;012;pal=11,sx=100,ix=50,c1=64,c2=128,c3=10,o1=0,o2=0,o3=0"; +static const char _data_FX_MODE_PARTICLEFIREWORKS[] PROGMEM = "PS Fireworks@Launches,Explosion Size,Height,Bounce,Rockets,Wrap X,Bounce X,Bounce Y;;!;012;pal=11,sx=100,ix=50,c1=64,c2=128,c3=10,o1=0,o2=0,o3=0"; /* * Particle Volcano (gravity spray) @@ -8288,17 +8252,10 @@ uint16_t mode_particlevolcano(void) if (SEGLEN == 1) return mode_static(); -<<<<<<< HEAD - const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; - const uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); - //particle system x dimension - const uint16_t PS_MAX_X = (cols * PS_P_RADIUS - 1); -======= const uint32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; // particle system x dimension - const uint32_t PS_MAX_X = (cols * PS_P_RADIUS - 1); ->>>>>>> 6d70b6ab (Cleanup & Bugfixes plus major improvements for ESP8266) + const uint32_t Max_x = (cols * PS_P_RADIUS - 1); #ifdef ESP8266 const uint32_t numParticles = 100; // maximum number of particles @@ -8367,10 +8324,7 @@ uint16_t mode_particlevolcano(void) } else{ //wrap on the right side spray[i].source.vx = SEGMENT.speed >> 4; // spray speed -<<<<<<< HEAD - if (spray[i].source.x >= PS_MAX_X-32) spray[i].source.x = 1; //wrap if close to border (need to wrap before the bounce updated detects a border collision or it will just be stuck) -======= - if (spray[i].source.x >= PS_MAX_X - 32) //compiler warning can be ignored, source.x is always > 0 + if (spray[i].source.x >= Max_x - 32) //compiler warning can be ignored, source.x is always > 0 spray[i].source.x = 1; // wrap if close to border (need to wrap before the bounce updated detects a border collision or it will just be stuck) >>>>>>> 6d70b6ab (Cleanup & Bugfixes plus major improvements for ESP8266) } @@ -8420,7 +8374,7 @@ uint16_t mode_particlevolcano(void) ParticleSys_render(particles, numParticles, false, false); return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEVOLCANO[] PROGMEM = "Particle Volcano@Move,Intensity,Speed,Bounce,Size,Color by Age,Bounce X,Collisions;;!;012;pal=35,sx=0,ix=160,c1=100,c2=160,c3=10,o1=1,o2=0,o3=0"; +static const char _data_FX_MODE_PARTICLEVOLCANO[] PROGMEM = "PS Volcano@Move,Intensity,Speed,Bounce,Size,Color by Age,Bounce X,Collisions;;!;012;pal=35,sx=0,ix=160,c1=100,c2=160,c3=10,o1=1,o2=0,o3=0"; /* * Particle Fire @@ -8436,7 +8390,7 @@ uint16_t mode_particlefire(void) const uint32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; // particle system box dimensions - const uint32_t PS_MAX_X = (cols * PS_P_RADIUS - 1); + const uint32_t Max_x = (cols * PS_P_RADIUS - 1); #ifdef ESP8266 const uint32_t numFlames = min((uint32_t)10, cols); // limit to 10 flames, not enough ram on ESP8266 @@ -8444,8 +8398,8 @@ uint16_t mode_particlefire(void) const uint32_t numNormalFlames = numFlames - (cols >> 2); // number of normal flames, rest of flames are baseflames #else const uint32_t numFlames = (cols << 1); // number of flames: depends on fire width. for a fire width of 16 pixels, about 25-30 flames give good results, add a few for the base flames - const uint32_t numParticles = numFlames * 25; - const uint32_t numNormalFlames = numFlames - (cols >> 1); // number of normal flames, rest of flames are baseflames + const uint32_t numParticles = numFlames * 20; + const uint32_t numNormalFlames = numFlames - (cols/3); // number of normal flames, rest of flames are baseflames #endif uint8_t percycle = numFlames >> 1; // maximum number of particles emitted per cycle @@ -8480,10 +8434,7 @@ uint16_t mode_particlefire(void) for (i = 0; i < numFlames; i++) { flames[i].source.ttl = 0; - flames[i].source.x = PS_P_RADIUS * 3 + random16(PS_MAX_X - (PS_P_RADIUS * 6)); // distribute randomly but not close to the corners -<<<<<<< HEAD - //note: other parameters are set when creating the flame (see blow) -======= + flames[i].source.x = PS_P_RADIUS * 3 + random16(Max_x - (PS_P_RADIUS * 6)); // distribute randomly but not close to the corners flames[i].source.vx = 0; // emitter moving speed; flames[i].source.vy = 0; // note: other parameters are set when creating the flame (see blow) @@ -8509,22 +8460,15 @@ uint16_t mode_particlefire(void) { if (SEGMENT.check1) { // wrap around in X direction, distribute randomly - flames[i].source.x = random16(PS_MAX_X); + flames[i].source.x = random16(Max_x); } else // no X-wrapping - { - flames[i].source.x = PS_P_RADIUS * 3 + random16(PS_MAX_X - (PS_P_RADIUS * 6)); // distribute randomly but not close to the corners + { + flames[i].source.x = PS_P_RADIUS * 3 + random16(Max_x - (PS_P_RADIUS * 6)); // distribute randomly but not close to the corners } } -<<<<<<< HEAD - if (i < (numFlames - (cols >> 1))) - { // all but the last few are normal flames - flames[i].source.y = -1 * PS_P_RADIUS; // set the source below the frame so particles alredy spread a little when the appear - flames[i].source.vx = 0; // (rand() % 3) - 1; - flames[i].source.vy = 0; -======= - flames[i].source.y = -1 * PS_P_RADIUS; // set the source below the frame + flames[i].source.y = -PS_P_RADIUS; // set the source below the frame if (i < numNormalFlames) { // all but the last few are normal flames @@ -8537,14 +8481,7 @@ uint16_t mode_particlefire(void) flames[i].var = random8(5) + 3; // speed variation around vx,vy (+/- var/2) } else -<<<<<<< HEAD - { // base flames to make the base brighter, flames are slower and short lived - flames[i].source.y = -1 * PS_P_RADIUS; // set the source below the frame - flames[i].source.vx = 0; - flames[i].source.vy = 0; // emitter moving speed; -======= { // base flames to make the base brighter, flames are slower and short lived ->>>>>>> 6d70b6ab (Cleanup & Bugfixes plus major improvements for ESP8266) flames[i].source.ttl = random8(25) + 15; // lifetime of one flame flames[i].maxLife = 25; // defines flame height together with the vy speed, vy speed*maxlife/PS_P_RADIUS is the average flame height flames[i].minLife = 12; @@ -8587,13 +8524,12 @@ uint16_t mode_particlefire(void) return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEFIRE[] PROGMEM = "Particle Fire@Speed,Intensity,Base Flames,Wind Speed, Color Scheme, WrapX;;!;012;sx=100,ix=120,c1=30,c2=128,c3=0,o1=0"; - +static const char _data_FX_MODE_PARTICLEFIRE[] PROGMEM = "PS Fire@Speed,Intensity,Base Flames,Wind,Color Scheme, WrapX;;!;012;sx=100,ix=120,c1=30,c2=128,c3=0,o1=0"; /* -particles falling down, user can enable these three options: X-wraparound, side bounce, ground bounce +PS Hail: 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 -this is quite versatile, can be made to look like rain or snow or confetti, flying sparks etc. +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) */ @@ -8680,7 +8616,7 @@ uint16_t mode_particlefall(void) return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEFALL[] PROGMEM = "Falling Particles@Speed,Intensity,Randomness,Collision hardness,Saturation,Wrap X,Side bounce,Ground bounce;;!;012;pal=11,sx=100,ix=200,c1=31,c2=100,c3=20,o1=0,o2=0,o3=1"; +static const char _data_FX_MODE_PARTICLEFALL[] PROGMEM = "PS Hail@Speed,Intensity,Randomness,Hardness,Saturation,Wrap X,Side bounce,Ground bounce;;!;012;pal=11,sx=100,ix=200,c1=31,c2=100,c3=28,o1=0,o2=0,o3=1"; /* * Particle Waterfall @@ -8736,13 +8672,6 @@ uint16_t mode_particlewaterfall(void) spray[i].source.x = (cols * PS_P_RADIUS) / 2 - PS_P_RADIUS + 2*PS_P_RADIUS * (i); spray[i].source.y = (rows+4) * (PS_P_RADIUS*(i+1)); // source y position, few pixels above the top to increase spreading before entering the matrix spray[i].source.vx = 0; -<<<<<<< HEAD - spray[i].source.collide = true; //seeded particles will collide - spray[i].maxLife = 600; // lifetime in frames - spray[i].minLife = 200; - spray[i].vx = 0; // emitting speed - spray[i].var = 7; // emiting variation -======= spray[i].source.collide = true; // seeded particles will collide #ifdef ESP8266 spray[i].maxLife = 100; // lifetime in frames @@ -8751,10 +8680,8 @@ uint16_t mode_particlewaterfall(void) spray[i].maxLife = 400; // lifetime in frames spray[i].minLife = 150; #endif - spray[i].vx = 0; // emitting speed spray[i].var = 7; // emiting variation ->>>>>>> 6d70b6ab (Cleanup & Bugfixes plus major improvements for ESP8266) } } @@ -8823,7 +8750,7 @@ uint16_t mode_particlewaterfall(void) return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEWATERFALL[] PROGMEM = "Particle Waterfall@Particle Speed,Intensity,Speed Variation,Collision Hardness,Position,Wrap X,Bounce X,Ground bounce;;!;012;pal=9,sx=150,ix=240,c1=0,c2=128,c3=17,o1=0,o2=0,o3=1"; +static const char _data_FX_MODE_PARTICLEWATERFALL[] PROGMEM = "PS Waterfall@Speed,Intensity,Variation,Collisions,Position,Wrap X,Bounce X,Ground bounce;;!;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) @@ -8926,12 +8853,11 @@ uint16_t mode_particlebox(void) return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEBOX[] PROGMEM = "Particle Box@Speed,Particles,Tilt strength,Hardness,,Sloshing;;!;012;pal=1,sx=120,ix=100,c1=190,c2=210,o1=0"; +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"; /* -Perlin Noise 'gravity' mapping as in particles on 'noise hills' viewed from above -calculates slope gradient at the particle positions and applies 'downhill' force -restults in a fuzzy perlin noise display +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) */ @@ -9002,14 +8928,6 @@ uint16_t mode_particleperlin(void) particles[i].vy += yslope >> 1; } } -<<<<<<< HEAD - uint8_t hardness = SEGMENT.custom1; // how hard the collisions are, 255 = full hard. - if(SEGMENT.check1) //collisions enabled - { - detectCollisions(particles, displayparticles, hardness); - } -======= ->>>>>>> 6d70b6ab (Cleanup & Bugfixes plus major improvements for ESP8266) // move particles for (i = 0; i < displayparticles; i++) @@ -9028,8 +8946,7 @@ uint16_t mode_particleperlin(void) return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEPERLIN[] PROGMEM = "Particle Perlin-Noise@Speed,Particles,,Friction,Scale;;!;012;pal=54,sx=70;ix=200,c1=120,c2=120,c3=4,o1=0"; - +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 @@ -9045,8 +8962,8 @@ uint16_t mode_particleimpact(void) const uint32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); // particle system box dimensions - const uint32_t PS_MAX_X(cols * PS_P_RADIUS - 1); - const uint32_t PS_MAX_Y(rows * PS_P_RADIUS - 1); + const uint32_t Max_x(cols * PS_P_RADIUS - 1); + const uint32_t Max_y(rows * PS_P_RADIUS - 1); #ifdef ESP8266 const uint32_t numParticles = 150; @@ -9155,23 +9072,6 @@ uint16_t mode_particleimpact(void) { Particle_Gravity_update(&meteors[i].source, SEGMENT.check1, SEGMENT.check2, true, 255); // move the meteor, age the meteor (ttl--) if (meteors[i].source.vy > 0) -<<<<<<< HEAD - meteors[i].source.y=5; //'hack' to keep the meteors within frame, as ttl will be set to zero by gravity update if too far out of frame - // if source reaches the bottom, set speed to 0 so it will explode on next function call (handled above) - if ((meteors[i].source.y < PS_P_RADIUS) && (meteors[i].source.vy < 0)) // reached the bottom pixel on its way down - { - meteors[i].source.vy = 0; // set speed zero so it will explode - meteors[i].source.vx = 0; - meteors[i].source.y = 5; // offset from ground so explosion happens not out of frame - meteors[i].source.collide = true; // explosion particles will collide if checked - meteors[i].maxLife = 200; - meteors[i].minLife = 50; - meteors[i].source.ttl = random8((255 - SEGMENT.speed)) + 10; // standby time til next launch (in frames at 42fps, max of 265 is about 6 seconds - meteors[i].vx = 0; // emitting speed x - meteors[i].vy = (SEGMENT.custom1 >> 2); // emitting speed y - meteors[i].var = (SEGMENT.custom1 >> 1); // speed variation around vx,vy (+/- var/2) - } -======= meteors[i].source.y = 5; //'hack' to keep the meteors within frame, as ttl will be set to zero by gravity update if too far out of frame // if source reaches the bottom, set speed to 0 so it will explode on next function call (handled above) if ((meteors[i].source.y < PS_P_RADIUS) && (meteors[i].source.vy < 0)) // reached the bottom pixel on its way down @@ -9191,14 +9091,13 @@ uint16_t mode_particleimpact(void) meteors[i].vy = (SEGMENT.custom1 >> 2); // emitting speed y meteors[i].var = (SEGMENT.custom1 >> 1); // speed variation around vx,vy (+/- var/2) } ->>>>>>> 6d70b6ab (Cleanup & Bugfixes plus major improvements for ESP8266) - } + } else if (meteors[i].source.vy > 0) // meteor is exploded and time is up (ttl==0 and positive speed), relaunch it { // reinitialize meteor - meteors[i].source.y = PS_MAX_Y + (PS_P_RADIUS<<2); // start 4 pixels above the top - meteors[i].source.x = random16(PS_MAX_X); - meteors[i].source.vy = -random(30) - 30; //meteor downward speed + meteors[i].source.y = Max_y + (PS_P_RADIUS << 2); // start 4 pixels above the top + meteors[i].source.x = random16(Max_x); + meteors[i].source.vy = -random(30) - 30; // meteor downward speed meteors[i].source.vx = random8(30) - 15; meteors[i].source.hue = random8(); // random color meteors[i].source.ttl = 1000; // long life, will explode at bottom @@ -9216,7 +9115,7 @@ uint16_t mode_particleimpact(void) return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEIMPACT[] PROGMEM = "Particle Impact@Launches,Explosion Size,Explosion Force,Bounce,Meteors,Wrap X,Bounce X,Collisions;;!;012;pal=35,sx=32,ix=85,c1=100,c2=100,c3=8,o1=0,o2=1,o3=1"; +static const char _data_FX_MODE_PARTICLEIMPACT[] PROGMEM = "PS Impact@Launches,Explosion Size,Explosion Force,Bounce,Meteors,Wrap X,Bounce X,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 @@ -9232,8 +9131,8 @@ uint16_t mode_particleattractor(void) const uint32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; const uint32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); // particle system box dimensions - const uint32_t PS_MAX_X(cols * PS_P_RADIUS - 1); - const uint32_t PS_MAX_Y(rows * PS_P_RADIUS - 1); + const uint32_t Max_x(cols * PS_P_RADIUS - 1); + const uint32_t Max_y(rows * PS_P_RADIUS - 1); #ifdef ESP8266 const uint32_t numParticles = 90; // maximum number of particles @@ -9248,15 +9147,10 @@ uint16_t mode_particleattractor(void) uint8_t *counters; //counters for the applied force // allocate memory and divide it into proper pointers, max is 32k for all segments. -<<<<<<< HEAD - uint32_t dataSize = sizeof(PSparticle) * (numParticles + 1); - dataSize += sizeof(uint8_t) *numParticles; - dataSize += sizeof(PSpointsource); -======= 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 ->>>>>>> 6d70b6ab (Cleanup & Bugfixes plus major improvements for ESP8266) + if (!SEGENV.allocateData(dataSize)) return mode_static(); // allocation failed @@ -9272,8 +9166,8 @@ uint16_t mode_particleattractor(void) { attractor->vx = 0; attractor->vy = 0; - attractor->x = PS_MAX_X >> 1; // center - attractor->y = PS_MAX_Y >> 1; + attractor->x = Max_x >> 1; // center + attractor->y = Max_y >> 1; for (i = 0; i < numParticles; i++) { @@ -9298,12 +9192,7 @@ uint16_t mode_particleattractor(void) uint32_t displayparticles = map(SEGMENT.intensity, 0, 255, 1, numParticles) - 1; //TODO: the -1 is a botch fix, it crashes for some reason if going to max number of particles, is this a rounding error? uint8_t hardness = SEGMENT.custom2; // how hard the collisions are, 255 = full hard. i = 0; -<<<<<<< HEAD - j = 0; - -======= ->>>>>>> 6d70b6ab (Cleanup & Bugfixes plus major improvements for ESP8266) if (hardness > 1) // enable collisions { detectCollisions(particles, displayparticles, hardness); @@ -9353,17 +9242,14 @@ uint16_t mode_particleattractor(void) else SEGMENT.fill(BLACK); // clear the matrix -<<<<<<< HEAD - //ParticleSys_render(&attract, 1, 30, false, false); // render attractor -======= // ParticleSys_render(&attract, 1, 30, false, false); // render attractor ->>>>>>> 6d70b6ab (Cleanup & Bugfixes plus major improvements for ESP8266) + // render the particles ParticleSys_render(particles, displayparticles, false, false); return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEATTRACTOR[] PROGMEM = "Particle Attractor@Center 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"; +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 @@ -9380,8 +9266,8 @@ uint16_t mode_particlespray(void) const uint32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; const uint32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); // particle system x dimension - const uint32_t PS_MAX_X = (cols * PS_P_RADIUS - 1); - const uint32_t PS_MAX_Y = (rows * PS_P_RADIUS - 1); + const uint32_t Max_x = (cols * PS_P_RADIUS - 1); + const uint32_t Max_y = (rows * PS_P_RADIUS - 1); #ifdef ESP8266 const uint32_t numParticles = 80; @@ -9435,10 +9321,10 @@ uint16_t mode_particlespray(void) { for (i = 0; i < numSprays; i++) { - spray[i].source.hue++; // = random8(); //change hue of spray source - //spray[i].var = SEGMENT.custom3; // emiting variation = nozzle size (custom 3 goes from 0-32) - spray[i].source.x = map(SEGMENT.custom1,0,255,0,PS_MAX_X); - spray[i].source.y = map(SEGMENT.custom2,0,255,0,PS_MAX_Y); + spray[i].source.hue++; // = random8(); //change hue of spray source + // spray[i].var = SEGMENT.custom3; // emiting variation = nozzle size (custom 3 goes from 0-32) + spray[i].source.x = map(SEGMENT.custom1, 0, 255, 0, Max_x); + spray[i].source.y = map(SEGMENT.custom2, 0, 255, 0, Max_y); } i = 0; @@ -9448,11 +9334,7 @@ uint16_t mode_particlespray(void) if (particles[i].ttl == 0) // find a dead particle { // spray[j].source.hue = random8(); //set random color for each particle (using palette) -<<<<<<< HEAD - Emitter_Angle_emit(&spray[j], &particles[i], SEGMENT.custom3<<3, SEGMENT.speed>>2); -======= Emitter_Angle_emit(&spray[j], &particles[i], 255-(SEGMENT.custom3 << 3), SEGMENT.speed >> 2); ->>>>>>> 6d70b6ab (Cleanup & Bugfixes plus major improvements for ESP8266) j = (j + 1) % numSprays; if (percycle-- == 0) { @@ -9462,8 +9344,7 @@ uint16_t mode_particlespray(void) } } - uint8_t hardness = 200; - + const uint8_t hardness = 200; if (SEGMENT.check3) // collisions enabled detectCollisions(particles, numParticles, hardness); @@ -9489,7 +9370,7 @@ uint16_t mode_particlespray(void) return FRAMETIME; } -static const char _data_FX_MODE_PARTICLESPRAY[] PROGMEM = "Particle Spray@Particle Speed,Intensity,X Position,Y Position,Angle,Gravity,WrapX/Bounce,Collisions;;!;012;pal=0,sx=100,ix=160,c1=100,c2=50,c3=20,o1=0,o2=1,o3=0"; +static const char _data_FX_MODE_PARTICLESPRAY[] PROGMEM = "PS Spray@Speed,!,X Position,Y Position,Angle,Gravity,WrapX/Bounce,Collisions;;!;012;pal=0,sx=150,ix=90,c3=31,o1=0,o2=0,o3=0"; /* Particle base Graphical Equalizer @@ -9604,9 +9485,9 @@ uint16_t mode_particleGEQ(void) // render the particles ParticleSys_render(particles, numParticles, SEGMENT.check1, false); // custom3 slider is saturation - return FRAMETIME; - } -static const char _data_FX_MODE_PARTICLEGEQ[] PROGMEM = "Particle GEQ@Speed,Intensity,Randomness,Collision hardness,Gravity,Wrap X,Side bounce,Ground bounce;;!;012;pal=54,sx=100,ix=200,c1=0,c2=0,c3=0,o1=0,o2=0,o3=0"; + return FRAMETIME; +} +static const char _data_FX_MODE_PARTICLEGEQ[] PROGMEM = "PS Equalizer@Speed,Intensity,Diverge,Bounce,Gravity,WrapX,BounceX,Floor;;!;012;pal=0,sx=155,ix=200,c1=0,c2=128,c3=31,o1=0,o2=0,o3=0"; /* * Particle rotating spray diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index ab3a911af1..1350eeb88b 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -176,12 +176,6 @@ void Particle_Move_update(PSparticle *part, bool killoutofbounds, bool wrapX, bo // age part->ttl--; - // apply velocity - part->x += (int16_t)part->vx; - 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) - // apply velocity int32_t newX, newY; newX = part->x + (int16_t)part->vx; From 306a850a11c3e3272d2455e3ba2c166dc77e889a Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Thu, 21 Mar 2024 22:42:45 +0100 Subject: [PATCH 047/219] slight speed improvements in fire, like 1-2FPS --- wled00/FXparticleSystem.cpp | 81 ++++++++++++++++++++----------------- wled00/FXparticleSystem.h | 8 ++-- 2 files changed, 50 insertions(+), 39 deletions(-) diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index 1350eeb88b..3a8b864449 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -500,12 +500,14 @@ void ParticleSys_render(PSparticle *particles, uint32_t numParticles, bool wrapX void FireParticle_update(PSparticle *part, bool wrapX) { // Matrix dimension - const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; - const uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); + const int32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; + const int32_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); + 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); + + if (part->ttl > 0) { @@ -513,8 +515,8 @@ void FireParticle_update(PSparticle *part, bool wrapX) part->ttl--; // apply velocity - part->x = part->x + (int16_t)part->vx; - part->y = part->y + (int16_t)part->vy + (part->ttl >> 4); // younger particles move faster upward as they are hotter, used for fire + part->x = part->x + (int32_t)part->vx; + part->y = part->y + (int32_t)part->vy + (part->ttl >> 4); // younger particles move faster upward as they are hotter, used for fire part->outofbounds = 0; // check if particle is out of bounds, wrap around to other side if wrapping is enabled @@ -550,11 +552,11 @@ void FireParticle_update(PSparticle *part, bool wrapX) void ParticleSys_renderParticleFire(PSparticle *particles, uint32_t numParticles, bool wrapX) { - const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; - const uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); + const uint32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; + const uint32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); int32_t x, y; - uint8_t dx, dy; + uint32_t dx, dy; uint32_t tempVal; uint32_t i; @@ -562,37 +564,45 @@ void ParticleSys_renderParticleFire(PSparticle *particles, uint32_t numParticles // 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++) { - if (particles[i].ttl == 0 || particles[i].outofbounds) + + if (particles[i].outofbounds) + { + continue; + } + + if (particles[i].ttl == 0) { continue; } + + dx = (uint8_t)((uint16_t)particles[i].x % (uint16_t)PS_P_RADIUS); dy = (uint8_t)((uint16_t)particles[i].y % (uint16_t)PS_P_RADIUS); - x = (uint8_t)((uint16_t)particles[i].x / (uint16_t)PS_P_RADIUS); // compiler should optimize to bit shift - y = (uint8_t)((uint16_t)particles[i].y / (uint16_t)PS_P_RADIUS); + x = (uint8_t)((uint16_t)particles[i].x >> PS_P_RADIUS_SHIFT); // divide by PS_P_RADIUS which is 64, so can bitshift (compiler may not optimize automatically) + y = (uint8_t)((uint16_t)particles[i].y >> PS_P_RADIUS_SHIFT); if (dx < (PS_P_RADIUS >> 1)) // jump to next physical pixel if half of virtual pixel size is reached { x--; // shift left - dx = dx + (PS_P_RADIUS >> 1); // add half a radius + dx += PS_P_RADIUS >> 1; // add half a radius } else // if jump has ocurred, fade pixel { // adjust dx so pixel fades - dx = dx - (PS_P_RADIUS >> 1); + dx -= PS_P_RADIUS >> 1; } if (dy < (PS_P_RADIUS >> 1)) // jump to next physical pixel if half of virtual pixel size is reached { y--; // shift row - dy = dy + (PS_P_RADIUS >> 1); + dy += PS_P_RADIUS >> 1; } else { // adjust dy so pixel fades - dy = dy - (PS_P_RADIUS >> 1); + dy -= PS_P_RADIUS >> 1; } if (wrapX) @@ -605,56 +615,55 @@ void ParticleSys_renderParticleFire(PSparticle *particles, uint32_t numParticles // calculate brightness values for all six pixels representing a particle using linear interpolation // bottom left - if (x < cols && x >=0 && y < rows && y >=0) + //if (x < cols && x >=0 && y < rows && y >=0) { tempVal = (((uint32_t)((PS_P_RADIUS)-dx) * ((PS_P_RADIUS)-dy) * (uint32_t)particles[i].ttl) >> PS_P_SURFACE); - PartMatrix_addHeat(x, y, tempVal); - PartMatrix_addHeat(x + 1, y, tempVal); // shift particle by 1 pixel to the right and add heat again (makes flame wider without using more particles) + PartMatrix_addHeat(x, y, tempVal, 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 (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 && y >= 0) + //if (x < cols && y < rows && y >= 0) { tempVal = (((uint32_t)dx * ((PS_P_RADIUS)-dy) * (uint32_t)particles[i].ttl) >> PS_P_SURFACE); - PartMatrix_addHeat(x, y, tempVal); - PartMatrix_addHeat(x + 1, y, tempVal); // shift particle by 1 pixel to the right and add heat again (makes flame wider without using more particles) + PartMatrix_addHeat(x, y, tempVal, 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) + //if (x < cols && y < rows) { tempVal = (((uint32_t)dx * dy * (uint32_t)particles[i].ttl) >> PS_P_SURFACE); // - PartMatrix_addHeat(x, y, tempVal); - PartMatrix_addHeat(x + 1, y, tempVal); // shift particle by 1 pixel to the right and add heat again (makes flame wider without using more particles) + PartMatrix_addHeat(x, y, tempVal, 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 + if (x < 0) // left half of particle render is out of frame, wrap it x = cols - 1; - } + } - if (x < cols && x >= 0 && y < rows) + //if (x < cols && x >= 0 && y < rows) { tempVal = (((uint32_t)((PS_P_RADIUS)-dx) * dy * (uint32_t)particles[i].ttl) >> PS_P_SURFACE); - PartMatrix_addHeat(x, y, tempVal); - PartMatrix_addHeat(x + 1, y, tempVal); // shift particle by 1 pixel to the right and add heat again (makes flame wider without using more particles) + PartMatrix_addHeat(x, y, tempVal, 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) } } } // adds 'heat' to red color channel, if it overflows, add it to next color channel -void PartMatrix_addHeat(uint8_t col, uint8_t row, uint16_t heat) +void PartMatrix_addHeat(uint8_t col, uint8_t row, uint32_t heat, uint32_t rows) { - const uint32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); + //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; @@ -665,9 +674,9 @@ void PartMatrix_addHeat(uint8_t col, uint8_t row, uint16_t heat) heat = heat << 3; // need to take a larger value to scale ttl value of particle to a good heat value that decays fast enough // i=0 is normal red fire, i=1 is green fire, i=2 is blue fire - uint8_t i = (colormode & 0x07) >> 1; + uint32_t i = (colormode & 0x07) >> 1; i = i % 3; - int8_t increment = (colormode & 0x01) + 1; // 0 (or 3) means only one single color for the flame, 1 is normal, 2 is alternate color modes + 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 diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index 824acc7b37..6b01ee9c73 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -30,9 +30,11 @@ #include //particle dimensions (subpixel division) -#define PS_P_RADIUS 64 //subpixel size, each pixel is divided by this for particle movement +#define PS_P_RADIUS 64 //subpixel size, each pixel is divided by this for particle movement, if this value is changed, also change the shift defines (next two lines) +#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_SURFACE 12 //shift: 2^PS_P_SURFACE = (PS_P_RADIUS)^2 + //todo: can add bitfields to add in more stuff, but accessing bitfields is slower than direct memory access! @@ -93,7 +95,7 @@ void Particle_Gravity_update(PSparticle *part, bool wrapX, bool bounceX, bool bo void ParticleSys_render(PSparticle *particles, uint32_t numParticles, bool wrapX, bool wrapY); void FireParticle_update(PSparticle *part, bool wrapX = false); void ParticleSys_renderParticleFire(PSparticle *particles, uint32_t numParticles, bool wrapX); -void PartMatrix_addHeat(uint8_t col, uint8_t row, uint16_t heat); +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 uint8_t hardness); void applyFriction(PSparticle *particle, uint8_t coefficient); From 35c21572fdf3b1f2e2aa2fc699e22e64f418f6d9 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sat, 23 Mar 2024 20:11:23 +0100 Subject: [PATCH 048/219] Big update: lots of little fixes and big speed boost on fire animation -fixed fire burning more on the left side -fixed crash in particle attractor -added many improvements for ESP8266 -improved particle rendering efficiency -efficiency improvements in general -changed the way fire is rendered, now more than 2x faster -re-tuned fire to new rendering, also seperately tuned it for ESP8266 -changed all random8() to random16() as it runs faster on ESPs -some reformating -some renaming of effect stuff -fine tuning on falling particle effect -improvements to collision handling (faster and better) -added a (temporary) function for speed tests, will be removed again --- wled00/FX.cpp | 368 ++++++++++++++---------- wled00/FXparticleSystem.cpp | 547 +++++++++++++++++------------------- wled00/FXparticleSystem.h | 15 +- 3 files changed, 485 insertions(+), 445 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index eadbf19de7..11f8ab8e5b 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -7898,7 +7898,7 @@ uint16_t mode_particlerotatingspray(void) const uint32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); #ifdef ESP8266 - const uint32_t numParticles = 150; // maximum number of particles + const uint32_t numParticles = 170; // maximum number of particles #else const uint32_t numParticles = 700; // maximum number of particles #endif @@ -7956,7 +7956,7 @@ uint16_t mode_particlerotatingspray(void) { if (SEGMENT.check1) // random color is checked { - spray[i].source.hue = random8(); + spray[i].source.hue = random16(); } else { @@ -7967,13 +7967,18 @@ uint16_t mode_particlerotatingspray(void) } uint8_t percycle = spraycount; // maximum number of particles emitted per cycle - i = 0; - j = random(spraycount); // start with random spray so all get a chance to emit a particle if maximum number of particles alive is reached. + + #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 = random8(); //set random color for each particle (using palette) + // 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) @@ -8030,10 +8035,8 @@ uint16_t mode_particlerotatingspray(void) } SEGMENT.fill(BLACK); // clear the matrix - // render the particles ParticleSys_render(particles, numParticles, false, false); - 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"; @@ -8059,9 +8062,8 @@ uint16_t mode_particlefireworks(void) const uint32_t Max_y = (rows * PS_P_RADIUS - 1); #ifdef ESP8266 - const uint32_t numParticles = 100; + const uint32_t numParticles = 120; const uint8_t MaxNumRockets = 2; ->>>>>>> 6d70b6ab (Cleanup & Bugfixes plus major improvements for ESP8266) #else const uint32_t numParticles = 650; const uint8_t MaxNumRockets = 8; @@ -8092,14 +8094,14 @@ uint16_t mode_particlefireworks(void) } for (i = 0; i < numRockets; i++) { - rockets[i].source.ttl = random8(20 * i); // first rocket starts immediately, others follow soon + rockets[i].source.ttl = random16(20 * i); // first rocket starts immediately, others follow soon rockets[i].source.vy = -1; // at negative speed, no particles are emitted and if rocket dies, it will be relaunched } } // update particles, create particles - uint8_t circularexplosion = random8(numRockets + 2); //choose a rocket by random (but not every round one will be picked) - uint8_t spiralexplosion = random8(numRockets + 2); + uint8_t circularexplosion = random16(numRockets + 2); //choose a rocket by random (but not every round one will be picked) + uint8_t spiralexplosion = random16(numRockets + 2); // check each rocket's state and emit particles according to its state: moving up = emit exhaust, at top = explode; falling down = standby time uint16_t emitparticles; // number of particles to emit for each rocket's state @@ -8115,13 +8117,13 @@ uint16_t mode_particlefireworks(void) { // falling down emitparticles = 0; } - else - { // speed is zero, explode! + else // speed is zero, explode! + { #ifdef ESP8266 - emitparticles = random8(SEGMENT.intensity >> 2) + 10; // defines the size of the explosion + emitparticles = random16(SEGMENT.intensity >> 2) + 10; // defines the size of the explosion #else - emitparticles = random8(SEGMENT.intensity >> 1) + 10; // defines the size of the explosion + emitparticles = random16(SEGMENT.intensity >> 1) + 10; // 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 (j == circularexplosion || j == spiralexplosion) // chosen rocket, do an angle emit (creating a circle) @@ -8136,7 +8138,7 @@ uint16_t mode_particlefireworks(void) uint8_t speed = 3; uint8_t angle = 0; if (j == spiralexplosion) - angle = random(8); + angle = random16(8); while(i < numParticles) { @@ -8151,8 +8153,8 @@ uint16_t mode_particlefireworks(void) { angle += 10; speed += 6; - rockets[j].source.hue = random8(); // new color for next row - rockets[j].source.sat = random8(); + rockets[j].source.hue = random16(); // new color for next row + rockets[j].source.sat = random16(); if(emitparticles > 12) emitparticles-=12; //emitted about 12 particles for one circle, ensures no incomplete circles are done else @@ -8170,7 +8172,7 @@ uint16_t mode_particlefireworks(void) speed++; rockets[j].source.hue++; - rockets[j].source.sat = random8(155)+100; + rockets[j].source.sat = random16(155)+100; } else if (emitparticles > 0) @@ -8204,11 +8206,11 @@ uint16_t mode_particlefireworks(void) else if (rockets[i].source.vy > 0) { // rocket has died and is moving up. stop it so it will explode (is handled in the code above) rockets[i].source.vy = 0; // set speed to zero so code above will recognize this as an exploding rocket - rockets[i].source.hue = random8(); // random color - rockets[i].source.sat = random8(100)+155; + rockets[i].source.hue = random16(); // random color + rockets[i].source.sat = random16(100)+155; rockets[i].maxLife = 200; rockets[i].minLife = 50; - rockets[i].source.ttl = random8((255 - SEGMENT.speed))+50; // standby time til next launch (in frames at 42fps, max of 300 is about 7 seconds + rockets[i].source.ttl = random16((255 - SEGMENT.speed))+50; // standby time til next launch (in frames at 42fps, max of 300 is about 7 seconds rockets[i].vx = 0; // emitting speed rockets[i].vy = 0; // emitting speed rockets[i].var = (SEGMENT.intensity >> 3) + 10; // speed variation around vx,vy (+/- var/2) @@ -8218,11 +8220,11 @@ uint16_t mode_particlefireworks(void) // reinitialize rocket rockets[i].source.y = 1; // start from bottom rockets[i].source.x = (rand() % (Max_x >> 1)) + (Max_y >> 2); // centered half - rockets[i].source.vy = random8(SEGMENT.custom1 >> 3) + 5; // rocket speed depends also on rocket height - rockets[i].source.vx = random8(5) - 2; + rockets[i].source.vy = random16(SEGMENT.custom1 >> 3) + 5; // rocket speed depends also on rocket height + rockets[i].source.vx = random16(5) - 2; rockets[i].source.hue = 30; // rocket exhaust = orange (if using rainbow palette) rockets[i].source.sat = 30; // low saturation -> exhaust is off-white - rockets[i].source.ttl = random8(SEGMENT.custom1) + (SEGMENT.custom1 >> 1); // sets explosion height (rockets explode at the top if set too high as paticle update set speed to zero if moving out of matrix) + rockets[i].source.ttl = random16(SEGMENT.custom1) + (SEGMENT.custom1 >> 1); // sets explosion height (rockets explode at the top if set too high as paticle update set speed to zero if moving out of matrix) rockets[i].maxLife = 30; // exhaust particle life rockets[i].minLife = 10; rockets[i].vx = 0; // emitting speed @@ -8237,7 +8239,7 @@ uint16_t mode_particlefireworks(void) return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEFIREWORKS[] PROGMEM = "PS Fireworks@Launches,Explosion Size,Height,Bounce,Rockets,Wrap X,Bounce X,Bounce Y;;!;012;pal=11,sx=100,ix=50,c1=64,c2=128,c3=10,o1=0,o2=0,o3=0"; +static const char _data_FX_MODE_PARTICLEFIREWORKS[] PROGMEM = "PS Fireworks@Launches,Explosion Size,Height,Bounce,Rockets,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) @@ -8290,7 +8292,7 @@ uint16_t mode_particlevolcano(void) } for (i = 0; i < numSprays; i++) { - spray[i].source.hue = random8(); + spray[i].source.hue = random16(); spray[i].source.sat = 255; // set full saturation spray[i].source.x = (cols * PS_P_RADIUS)/(numSprays+1) * (i + 1); spray[i].source.y = 5; // just above the lower edge, if zero, particles already 'bounce' at start and loose speed. @@ -8300,7 +8302,7 @@ uint16_t mode_particlevolcano(void) spray[i].source.collide = true; //seeded particles will collide spray[i].vx = 0; // emitting speed spray[i].vy = 20; // emitting speed - // spray.var = 10 + (random8() % 4); + // spray.var = 10 + (random16() % 4); } } @@ -8309,7 +8311,7 @@ uint16_t mode_particlevolcano(void) { for (i = 0; i < numSprays; i++) { - spray[i].source.hue++; // = random8(); //change hue of spray source + spray[i].source.hue++; // = random16(); //change hue of spray source // percycle = 1+(SEGMENT.intensity>>4); //how many particles are sprayed per cycle and how fast ist the color changing if(SEGMENT.check2) //bounce { @@ -8326,7 +8328,6 @@ uint16_t mode_particlevolcano(void) spray[i].source.vx = SEGMENT.speed >> 4; // spray speed if (spray[i].source.x >= Max_x - 32) //compiler warning can be ignored, source.x is always > 0 spray[i].source.x = 1; // wrap if close to border (need to wrap before the bounce updated detects a border collision or it will just be stuck) ->>>>>>> 6d70b6ab (Cleanup & Bugfixes plus major improvements for ESP8266) } spray[i].vy = SEGMENT.custom1 >> 2; // emitting speed, upward spray[i].vx = 0; @@ -8340,7 +8341,7 @@ uint16_t mode_particlevolcano(void) { if (particles[i].ttl == 0) // find a dead particle { - // spray[j].source.hue = random8(); //set random color for each particle (using palette) + // spray[j].source.hue = random16(); //set random color for each particle (using palette) Emitter_Fountain_emit(&spray[j], &particles[i]); j = (j + 1) % numSprays; if (percycle-- == 0) @@ -8374,7 +8375,58 @@ uint16_t mode_particlevolcano(void) ParticleSys_render(particles, numParticles, false, false); return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEVOLCANO[] PROGMEM = "PS Volcano@Move,Intensity,Speed,Bounce,Size,Color by Age,Bounce X,Collisions;;!;012;pal=35,sx=0,ix=160,c1=100,c2=160,c3=10,o1=1,o2=0,o3=0"; +static const char _data_FX_MODE_PARTICLEVOLCANO[] PROGMEM = "PS Volcano@Move,Intensity,Speed,Bounce,Size,Color by Age,Walls,Collisions;;!;012;pal=35,sx=0,ix=160,c1=100,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 @@ -8384,6 +8436,10 @@ static const char _data_FX_MODE_PARTICLEVOLCANO[] PROGMEM = "PS Volcano@Move,Int uint16_t mode_particlefire(void) { + + // speed tests : + // SpeedTestfunction(); + if (SEGLEN == 1) return mode_static(); @@ -8391,18 +8447,20 @@ uint16_t mode_particlefire(void) // particle system box dimensions const uint32_t Max_x = (cols * PS_P_RADIUS - 1); - + #ifdef ESP8266 - const uint32_t numFlames = min((uint32_t)10, cols); // limit to 10 flames, not enough ram on ESP8266 - const uint32_t numParticles = numFlames * 16; - const uint32_t numNormalFlames = numFlames - (cols >> 2); // number of normal flames, rest of flames are baseflames + const uint32_t numFlames = min((uint32_t)12, (cols<<1)); // limit to 18 flames, not enough ram on ESP8266 + const uint32_t numParticles = numFlames * 15; //limit number of particles to about 180 or ram will be depleted + const uint32_t numNormalFlames = numFlames - (numFlames / 3); // number of normal flames, rest of flames are baseflames + uint32_t percycle = numFlames >> 2 ;// maximum number of particles emitted per cycle #else const uint32_t numFlames = (cols << 1); // number of flames: depends on fire width. for a fire width of 16 pixels, about 25-30 flames give good results, add a few for the base flames - const uint32_t numParticles = numFlames * 20; - const uint32_t numNormalFlames = numFlames - (cols/3); // number of normal flames, rest of flames are baseflames - #endif + const uint32_t numParticles = numFlames * 18; + const uint32_t numNormalFlames = numFlames - (cols / 3); // number of normal flames, rest of flames are baseflames + uint32_t percycle = numFlames / 3; // maximum number of particles emitted per cycle +#endif + - uint8_t percycle = numFlames >> 1; // maximum number of particles emitted per cycle PSparticle *particles; PSpointsource *flames; @@ -8434,11 +8492,9 @@ uint16_t mode_particlefire(void) for (i = 0; i < numFlames; i++) { flames[i].source.ttl = 0; - flames[i].source.x = PS_P_RADIUS * 3 + random16(Max_x - (PS_P_RADIUS * 6)); // distribute randomly but not close to the corners flames[i].source.vx = 0; // emitter moving speed; - flames[i].source.vy = 0; + flames[i].source.vy = 0; // note: other parameters are set when creating the flame (see blow) ->>>>>>> 6d70b6ab (Cleanup & Bugfixes plus major improvements for ESP8266) } } @@ -8447,76 +8503,80 @@ uint16_t mode_particlefire(void) { if (flames[i].source.ttl > 0) { - flames[i].source.ttl--; - flames[i].source.x += flames[i].source.vx; // move the flame source (if it has x-speed) + flames[i].source.ttl--; } else // flame source is dead { - // initialize new flame: set properties of source - // from time to time, chang the flame position - // make some of the flames small and slow to add a bright base - - if (random8(40) == 0) //from time to time, change flame position (about once per second at 40 fps) + // initialize new flame: set properties of source + if (random16(20) == 0 || SEGMENT.call == 0) // from time to time, change flame position { if (SEGMENT.check1) { // wrap around in X direction, distribute randomly - flames[i].source.x = random16(Max_x); + flames[i].source.x = rand() % Max_x; } else // no X-wrapping - { - flames[i].source.x = PS_P_RADIUS * 3 + random16(Max_x - (PS_P_RADIUS * 6)); // distribute randomly but not close to the corners + { + flames[i].source.x = (rand() % (Max_x - (PS_P_RADIUS * ((cols>>3)+1)))) + PS_P_RADIUS * ((cols>>4)+1); // distribute randomly but not close to the corners (cannot use random16() it is not random enough, tends to burn on left side) } } flames[i].source.y = -PS_P_RADIUS; // set the source below the frame - if (i < numNormalFlames) - { // all but the last few are normal flames ->>>>>>> 6d70b6ab (Cleanup & Bugfixes plus major improvements for ESP8266) - flames[i].source.ttl = random8(SEGMENT.intensity >> 2) / (1 + (SEGMENT.speed >> 6)) + 10; //'hotness' of fire, faster flames reduce the effect or flame height will scale too much with speed - flames[i].maxLife = random8(7) + 13; // defines flame height together with the vy speed, vy speed*maxlife/PS_P_RADIUS is the average flame height - flames[i].minLife = 3; - flames[i].vx = (int8_t)random8(4) - 2; // emitting speed (sideways) + 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].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) flames[i].vy = 5 + (SEGMENT.speed >> 2); // emitting speed (upwards) - flames[i].var = random8(5) + 3; // speed variation around vx,vy (+/- var/2) + flames[i].var = random16(5) + 3;; // speed variation around vx,vy (+/- var/2) } else - { // base flames to make the base brighter, flames are slower and short lived - flames[i].source.ttl = random8(25) + 15; // lifetime of one flame + { // base flames to make the base brighter, flames are slower and short lived + flames[i].source.ttl = random16(25) + 15; // lifetime of one flame flames[i].maxLife = 25; // defines flame height together with the vy speed, vy speed*maxlife/PS_P_RADIUS is the average flame height flames[i].minLife = 12; flames[i].vx = 0; // emitting speed, sideways - flames[i].vy = 1 + (SEGMENT.custom1 >> 4); // slow emitting speed (upwards) - flames[i].var = 2; // speed variation around vx,vy (+/- var/2) + flames[i].vy = (SEGMENT.custom1 >> 4); // slow emitting speed (upwards) + flames[i].var = 5; // speed variation around vx,vy (+/- var/2) } } } - SEGMENT.aux0++; // position in the perlin noise matrix for wind generation + if (SEGMENT.call & 0x01) //update noise position every second frames + { + SEGMENT.aux0++; // position in the perlin noise matrix for wind generation + if (SEGMENT.call & 0x02) //every tird frame + SEGMENT.aux1++; // move in noise y direction so noise does not repeat as often + } + int8_t windspeed = (int8_t)(inoise8(SEGMENT.aux0,SEGMENT.aux1) - 127) / ((271 - SEGMENT.custom2) >> 4); // update particles, create particles uint8_t j = random16(numFlames); // start with a random flame (so each flame gets the chance to emit a particle if perCycle is smaller than number of flames) for (i = 0; i < numParticles; i++) { - if (particles[i].ttl == 0 && percycle > 0) + if (particles[i].ttl == 0) { - Emitter_Flame_emit(&flames[j], &particles[i]); - j++; - if (j >= numFlames) - { // or simpler: j=j%numFlames; - j = 0; + if(percycle > 0) + { + Emitter_Flame_emit(&flames[j], &particles[i]); + j++; + if (j >= numFlames) + { // or simpler: j=j%numFlames; but that is slow on ESP8266 + j = 0; + } + percycle--; } - percycle--; } - else if (particles[i].ttl) - { - // add wind using perlin noise - int8_t windspeed = (int8_t)(inoise8(SEGMENT.aux0, particles[i].y >> 2) - 127) / ((271 - SEGMENT.custom2) >> 4); + 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; - FireParticle_update(&particles[i], SEGMENT.check1); // update particle, use X-wrapping if check 1 is set by user } } + FireParticle_update(particles, numParticles, SEGMENT.check1); // update particle, use X-wrapping if check 1 is set by user + SEGMENT.fill(BLACK); // clear the matrix // render the particles @@ -8524,10 +8584,10 @@ uint16_t mode_particlefire(void) return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEFIRE[] PROGMEM = "PS Fire@Speed,Intensity,Base Flames,Wind,Color Scheme, WrapX;;!;012;sx=100,ix=120,c1=30,c2=128,c3=0,o1=0"; +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 Hail: particles falling down, user can enable these three options: X-wraparound, side bounce, ground bounce +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 this is quite versatile, can be made to look like rain or snow or confetti etc. Uses palette for particle color @@ -8579,17 +8639,18 @@ uint16_t mode_particlefall(void) // emit particle at random position just over the top of the matrix particles[i].ttl = 1500 - (SEGMENT.speed << 2) + random16(500); // if speed is higher, make them die sooner - if (random8(5) == 0) // 16% of particles apper anywhere + if (random16(5) == 0) // 16% of particles apper anywhere particles[i].x = random16(cols * PS_P_RADIUS - 1); else // rest is emitted at center half particles[i].x = random16((cols >> 1) * PS_P_RADIUS + (cols >> 1) * PS_P_RADIUS); particles[i].y = random16(rows * PS_P_RADIUS) + rows * PS_P_RADIUS; // particles appear somewhere above the matrix, maximum is double the height - particles[i].vx = (((int16_t)random8(SEGMENT.custom1)) - (SEGMENT.custom1 >> 1)) >> 1; // side speed is +/- a quarter of the custom1 slider + particles[i].vx = (((int16_t)random16(SEGMENT.custom1)) - (SEGMENT.custom1 >> 1)) >> 1; // side speed is +/- a quarter of the custom1 slider particles[i].vy = -(SEGMENT.speed >> 1); - particles[i].hue = random8(); // set random color + particles[i].hue = random16(); // set random color particles[i].sat = ((SEGMENT.custom3) << 3) + 7; // set saturation - particles[i].collide = true; // particle will collide + particles[i].collide = true; // particle will collide + break; //emit only one particle per round } i++; } @@ -8599,14 +8660,18 @@ uint16_t mode_particlefall(void) detectCollisions(particles, numParticles, hardness); // now move the particles + + uint32_t frictioncoefficient = 1; + if (SEGMENT.speed < 50) + { + frictioncoefficient = 50 - SEGMENT.speed; + } + for (i = 0; i < numParticles; i++) { - // apply 'air friction' to smooth things out, slows down all particles depending on their speed, only done on low speeds - if (SEGMENT.speed < 50) - { - applyFriction(&particles[i], 50 - SEGMENT.speed); - } - Particle_Gravity_update(&particles[i], SEGMENT.check1, SEGMENT.check2, SEGMENT.check3, min(hardness,(uint8_t)150)); // surface hardness max is 150 + // apply 'air friction' to smooth things out, slows down all particles depending on their speed + applyFriction(&particles[i], frictioncoefficient); + Particle_Gravity_update(&particles[i], SEGMENT.check1, SEGMENT.check2, SEGMENT.check3, min(hardness,(uint8_t)200)); // surface hardness max is 200 } SEGMENT.fill(BLACK); // clear the matrix @@ -8616,7 +8681,7 @@ uint16_t mode_particlefall(void) return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEFALL[] PROGMEM = "PS Hail@Speed,Intensity,Randomness,Hardness,Saturation,Wrap X,Side bounce,Ground bounce;;!;012;pal=11,sx=100,ix=200,c1=31,c2=100,c3=28,o1=0,o2=0,o3=1"; +static const char _data_FX_MODE_PARTICLEFALL[] PROGMEM = "PS Ballpit@Speed,Intensity,Randomness,Hardness,Saturation,Cylinder,Walls,Ground;;!;012;pal=11,sx=100,ix=200,c1=31,c2=100,c3=28,o1=0,o2=0,o3=1"; /* * Particle Waterfall @@ -8667,7 +8732,7 @@ uint16_t mode_particlewaterfall(void) } for (i = 0; i < numSprays; i++) { - spray[i].source.hue = random8(); + spray[i].source.hue = random16(); spray[i].source.sat = 255; // set full saturation spray[i].source.x = (cols * PS_P_RADIUS) / 2 - PS_P_RADIUS + 2*PS_P_RADIUS * (i); spray[i].source.y = (rows+4) * (PS_P_RADIUS*(i+1)); // source y position, few pixels above the top to increase spreading before entering the matrix @@ -8750,7 +8815,7 @@ uint16_t mode_particlewaterfall(void) return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEWATERFALL[] PROGMEM = "PS Waterfall@Speed,Intensity,Variation,Collisions,Position,Wrap X,Bounce X,Ground bounce;;!;012;pal=9,sx=15,ix=200,c1=15,c2=128,c3=17,o1=0,o2=0,o3=1"; +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) @@ -8808,7 +8873,7 @@ uint16_t mode_particlebox(void) SEGMENT.aux0 += (SEGMENT.speed >> 6) + 1; // update position in noise - xgravity = ((int16_t)inoise8(SEGMENT.aux0) - 127); + xgravity = ((int16_t)inoise8(SEGMENT.aux0) - 127); //TODO: inoise 16 would be faster ygravity = ((int16_t)inoise8(SEGMENT.aux0 + 10000) - 127); if (SEGMENT.check1) //sloshing, y force is alwys downwards { @@ -8999,7 +9064,7 @@ uint16_t mode_particleimpact(void) for (i = 0; i < MaxNumMeteors; i++) { meteors[i].source.y = 10; - meteors[i].source.ttl = random8(20 * i); // set initial delay for meteors + meteors[i].source.ttl = random16(20 * i); // set initial delay for meteors meteors[i].source.vy = 10; // at positive speeds, no particles are emitted and if particle dies, it will be relaunched meteors[i].source.sat = 255; //full saturation, color chosen by palette } @@ -9028,9 +9093,9 @@ uint16_t mode_particleimpact(void) { meteors[j].source.vy = 125; // set source speed positive so it goes into timeout and launches again #ifdef ESP8266 - emitparticles = random8(SEGMENT.intensity >> 2) + 10; // defines the size of the explosion + emitparticles = random16(SEGMENT.intensity >> 2) + 10; // defines the size of the explosion #else - emitparticles = random8(SEGMENT.intensity >> 1) + 10; // defines the size of the explosion + emitparticles = random16(SEGMENT.intensity >> 1) + 10; // defines the size of the explosion #endif } @@ -9083,9 +9148,9 @@ uint16_t mode_particleimpact(void) meteors[i].maxLife = 200; meteors[i].minLife = 50; #ifdef ESP8266 - meteors[i].source.ttl = random8(255 - (SEGMENT.speed>>1)) + 10; // standby time til next launch (in frames at 42fps, max of 265 is about 6 seconds + meteors[i].source.ttl = random16(255 - (SEGMENT.speed>>1)) + 10; // standby time til next launch (in frames at 42fps, max of 265 is about 6 seconds #else - meteors[i].source.ttl = random8((255 - SEGMENT.speed)) + 10; // standby time til next launch (in frames at 42fps, max of 265 is about 6 seconds + meteors[i].source.ttl = random16((255 - SEGMENT.speed)) + 10; // standby time til next launch (in frames at 42fps, max of 265 is about 6 seconds #endif meteors[i].vx = 0; // emitting speed x meteors[i].vy = (SEGMENT.custom1 >> 2); // emitting speed y @@ -9097,9 +9162,9 @@ uint16_t mode_particleimpact(void) // reinitialize meteor meteors[i].source.y = Max_y + (PS_P_RADIUS << 2); // start 4 pixels above the top meteors[i].source.x = random16(Max_x); - meteors[i].source.vy = -random(30) - 30; // meteor downward speed - meteors[i].source.vx = random8(30) - 15; - meteors[i].source.hue = random8(); // random color + meteors[i].source.vy = -random16(30) - 30; // meteor downward speed + meteors[i].source.vx = random16(30) - 15; + meteors[i].source.hue = random16(); // random color meteors[i].source.ttl = 1000; // long life, will explode at bottom meteors[i].source.collide = false; // trail particles will not collide meteors[i].maxLife = 60; // spark particle life @@ -9115,7 +9180,7 @@ uint16_t mode_particleimpact(void) return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEIMPACT[] PROGMEM = "PS Impact@Launches,Explosion Size,Explosion Force,Bounce,Meteors,Wrap X,Bounce X,Collisions;;!;012;pal=0,sx=32,ix=85,c1=100,c2=100,c3=8,o1=0,o2=1,o3=1"; +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 @@ -9174,12 +9239,12 @@ uint16_t mode_particleattractor(void) particles[i].ttl = 0; } - spray->source.hue = random8(); + spray->source.hue = random16(); spray->source.sat = 255; //full saturation, color by palette spray->source.x = 0; spray->source.y = 0; - spray->source.vx = random8(5) + 2; - spray->source.vy = random8(4) + 1; + spray->source.vx = random16(5) + 2; + spray->source.vy = random16(4) + 1; spray->source.ttl = 100; spray->source.collide = true; //seeded particles will collide (if checked) spray->maxLife = 300; // seeded particle lifetime in frames @@ -9189,7 +9254,7 @@ uint16_t mode_particleattractor(void) spray->var = 6; //emitting speed variation } - uint32_t displayparticles = map(SEGMENT.intensity, 0, 255, 1, numParticles) - 1; //TODO: the -1 is a botch fix, it crashes for some reason if going to max number of particles, is this a rounding error? + uint32_t displayparticles = map(SEGMENT.intensity, 0, 255, 1, numParticles) - 2; //TODO: the -2 is a botch fix, it crashes for some reason if going to max number of particles, is this a rounding error? uint8_t hardness = SEGMENT.custom2; // how hard the collisions are, 255 = full hard. i = 0; @@ -9302,7 +9367,7 @@ uint16_t mode_particlespray(void) } for (i = 0; i < numSprays; i++) { - spray[i].source.hue = random8(); + spray[i].source.hue = random16(); spray[i].source.sat = 255; // set full saturation spray[i].source.x = (cols * PS_P_RADIUS) / (numSprays + 1) * (i + 1); spray[i].source.y = 5; // just above the lower edge, if zero, particles already 'bounce' at start and loose speed. @@ -9321,7 +9386,7 @@ uint16_t mode_particlespray(void) { for (i = 0; i < numSprays; i++) { - spray[i].source.hue++; // = random8(); //change hue of spray source + spray[i].source.hue++; // = random16(); //change hue of spray source // spray[i].var = SEGMENT.custom3; // emiting variation = nozzle size (custom 3 goes from 0-32) spray[i].source.x = map(SEGMENT.custom1, 0, 255, 0, Max_x); spray[i].source.y = map(SEGMENT.custom2, 0, 255, 0, Max_y); @@ -9333,7 +9398,7 @@ uint16_t mode_particlespray(void) { if (particles[i].ttl == 0) // find a dead particle { - // spray[j].source.hue = random8(); //set random color for each particle (using palette) + // spray[j].source.hue = random16(); //set random color for each particle (using palette) Emitter_Angle_emit(&spray[j], &particles[i], 255-(SEGMENT.custom3 << 3), SEGMENT.speed >> 2); j = (j + 1) % numSprays; if (percycle-- == 0) @@ -9370,7 +9435,7 @@ uint16_t mode_particlespray(void) return FRAMETIME; } -static const char _data_FX_MODE_PARTICLESPRAY[] PROGMEM = "PS Spray@Speed,!,X Position,Y Position,Angle,Gravity,WrapX/Bounce,Collisions;;!;012;pal=0,sx=150,ix=90,c3=31,o1=0,o2=0,o3=0"; +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 @@ -9444,7 +9509,7 @@ uint16_t mode_particleGEQ(void) else if(fftResult[bin] > 0)// band has low volue { uint32_t restvolume = ((threshold - fftResult[bin])>>2) + 2; - if (random8() % restvolume == 0) + if (random16() % restvolume == 0) { emitparticles = 1; } @@ -9455,12 +9520,12 @@ uint16_t mode_particleGEQ(void) if (particles[i].ttl == 0) // find a dead particle { //set particle properties - particles[i].ttl = map(SEGMENT.intensity, 0,255, emitspeed>>1, emitspeed + random8(emitspeed)) ; // set particle alive, particle lifespan is in number of frames - particles[i].x = xposition + random8(binwidth) - (binwidth>>1); //position randomly, deviating half a bin width + particles[i].ttl = map(SEGMENT.intensity, 0,255, emitspeed>>1, emitspeed + random16(emitspeed)) ; // set particle alive, particle lifespan is in number of frames + particles[i].x = xposition + random16(binwidth) - (binwidth>>1); //position randomly, deviating half a bin width particles[i].y = 0; //start at the bottom - particles[i].vx = random8(SEGMENT.custom1>>1)-(SEGMENT.custom1>>2) ; //x-speed variation + particles[i].vx = random16(SEGMENT.custom1>>1)-(SEGMENT.custom1>>2) ; //x-speed variation particles[i].vy = emitspeed; - particles[i].hue = (bin<<4) + random8(17) - 8; // color from palette according to bin + particles[i].hue = (bin<<4) + random16(17) - 8; // color from palette according to bin //particles[i].sat = ((SEGMENT.custom3) << 3) + 7; // set saturation emitparticles--; } @@ -9468,29 +9533,42 @@ uint16_t mode_particleGEQ(void) } } - // Serial.println(" "); - - uint8_t hardness = SEGMENT.custom2; // how hard the collisions are, 255 = full hard. - // detectCollisions(particles, numParticles, hardness); + uint8_t hardness = SEGMENT.custom2; // how hard the collisions are, 255 = full hard. + // detectCollisions(particles, numParticles, hardness); - // now move the particles - for (i = 0; i < numParticles; i++) - { - particles[i].vy -= (SEGMENT.custom3>>3); // apply stronger gravity - Particle_Gravity_update(&particles[i], SEGMENT.check1, SEGMENT.check2, SEGMENT.check3, hardness); - } + // now move the particles + for (i = 0; i < numParticles; i++) + { + particles[i].vy -= (SEGMENT.custom3>>3); // apply stronger gravity + Particle_Gravity_update(&particles[i], SEGMENT.check1, SEGMENT.check2, SEGMENT.check3, hardness); + } - SEGMENT.fill(BLACK); // clear the matrix + SEGMENT.fill(BLACK); // clear the matrix - // render the particles - ParticleSys_render(particles, numParticles, SEGMENT.check1, false); // custom3 slider is saturation + // render the particles + ParticleSys_render(particles, numParticles, SEGMENT.check1, false); // custom3 slider is saturation return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEGEQ[] PROGMEM = "PS Equalizer@Speed,Intensity,Diverge,Bounce,Gravity,WrapX,BounceX,Floor;;!;012;pal=0,sx=155,ix=200,c1=0,c2=128,c3=31,o1=0,o2=0,o3=0"; +static const char _data_FX_MODE_PARTICLEGEQ[] PROGMEM = "PS Equalizer@Speed,Intensity,Diverge,Bounce,Gravity,Cylinder,Walls,Floor;;!;012;pal=0,sx=155,ix=200,c1=0,c2=128,c3=31,o1=0,o2=0,o3=0"; + + +uint32_t rand16seedESP = 1337; +uint32_t random16_ESP() +{ + rand16seedESP = rand16seedESP * FASTLED_RAND16_2053 + FASTLED_RAND16_13849; + return rand16seedESP; +} +uint32_t random16_ESP(uint32_t limit) +{ + uint32_t r = random16_ESP(); + uint32_t p = limit * r; + r = p >> 16; + return r; +} /* - * Particle rotating spray + * Particle rotating GEQ * Particles sprayed from center with a rotating spray * Uses palette for particle color * by DedeHai (Damian Schneider) @@ -9528,7 +9606,7 @@ uint16_t mode_particlecenterGEQ(void) uint32_t i = 0; uint32_t j = 0; - uint8_t spraycount = 1 + (SEGMENT.custom2 >> 5); // number of sprays to display, 1-8 + //uint8_t spraycount = 1 + (SEGMENT.custom2 >> 5); // number of sprays to display, 1-8 if (SEGMENT.call == 0) // initialization { @@ -9567,10 +9645,8 @@ uint16_t mode_particlecenterGEQ(void) uint32_t threshold = 300 - SEGMENT.intensity; - - uint8_t percycle = numSprays; // maximum number of particles emitted per cycle i = 0; - j = random(numSprays); // start with random spray so all get a chance to emit a particle if maximum number of particles alive is reached. + j = random16(numSprays); // start with random spray so all get a chance to emit a particle if maximum number of particles alive is reached. if (SEGMENT.check2) SEGMENT.aux0 += SEGMENT.custom1; else @@ -9578,12 +9654,12 @@ uint16_t mode_particlecenterGEQ(void) uint32_t angleoffset = SEGMENT.aux0 >> 4; - while (i < numParticles) + while (i < numParticles) { if (particles[i].ttl == 0) // find a dead particle { uint8_t emitspeed = 5 + (((uint32_t)fftResult[j] * ((uint32_t)SEGMENT.speed+10)) >> 9); // emit speed according to loudness of band - uint8_t emitangle = j * 16 + random(SEGMENT.custom3 >> 1) + angleoffset; + uint8_t emitangle = j * 16 + random16(SEGMENT.custom3 >> 1) + angleoffset; uint32_t emitparticles = 0; if (fftResult[j] > threshold) @@ -9593,7 +9669,7 @@ uint16_t mode_particlecenterGEQ(void) else if (fftResult[j] > 0) // band has low volue { uint32_t restvolume = ((threshold - fftResult[j]) >> 2) + 2; - if (random8() % restvolume == 0) + if (random16() % restvolume == 0) { emitparticles = 1; } @@ -9606,12 +9682,7 @@ uint16_t mode_particlecenterGEQ(void) //todo: could add a break if all 16 sprays have been checked, would speed it up } -/* - if (SEGMENT.check2) - SEGMENT.aux0 += SEGMENT.custom1 << 2; - else - SEGMENT.aux0 -= SEGMENT.custom1 << 2; -*/ //TODO: add rotation + for (i = 0; i < numParticles; i++) { @@ -9623,9 +9694,10 @@ uint16_t mode_particlecenterGEQ(void) // render the particles ParticleSys_render(particles, numParticles, false, false); - return FRAMETIME; - } -static const char _data_FX_MODE_PARTICLECCIRCULARGEQ[] PROGMEM = "PS Center GEQ@Speed,Color Change,Particle Speed,Spray Count,Nozzle Size,Random Color, Direction;;!;012;pal=56,sx=0,ix=222,c1=190,c2=200,c3=0,o1=0,o2=0"; + + return FRAMETIME; +} + static const char _data_FX_MODE_PARTICLECCIRCULARGEQ[] PROGMEM = "PS Center GEQ@Speed,Color Change,Particle Speed,Spray Count,Nozzle Size,Random Color, Direction;;!;012;pal=56,sx=0,ix=222,c1=190,c2=200,c3=0,o1=0,o2=0"; #endif // WLED_DISABLE_2D diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index 3a8b864449..eb1fa972e6 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -40,22 +40,22 @@ // Fountain style emitter for particles used for flames (particle TTL depends on source TTL) void Emitter_Flame_emit(PSpointsource *emitter, PSparticle *part) { - part->x = emitter->source.x + random8(emitter->var) - (emitter->var >> 1); - part->y = emitter->source.y + random8(emitter->var) - (emitter->var >> 1); - part->vx = emitter->vx + random8(emitter->var) - (emitter->var >> 1); - part->vy = emitter->vy + random8(emitter->var) - (emitter->var >> 1); + part->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; - //part->sat = emitter->source.sat; //flame does not use saturation + // part->hue = emitter->source.hue; //fire uses ttl and not hue for heat + // part->sat = emitter->source.sat; //flame does not use saturation } // fountain style emitter void Emitter_Fountain_emit(PSpointsource *emitter, PSparticle *part) { - part->x = emitter->source.x; // + random8(emitter->var) - (emitter->var >> 1); //randomness uses cpu cycles and is almost invisible, removed for now. - part->y = emitter->source.y; // + random8(emitter->var) - (emitter->var >> 1); - part->vx = emitter->vx + random8(emitter->var) - (emitter->var >> 1); - part->vy = emitter->vy + random8(emitter->var) - (emitter->var >> 1); + part->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; @@ -65,16 +65,16 @@ void Emitter_Fountain_emit(PSpointsource *emitter, PSparticle *part) // 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) { - 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->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); } // 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) { // Calculate the distance between the particle and the attractor - int dx = attractor->x - particle->x; - int dy = attractor->y - particle->y; + int32_t dx = attractor->x - particle->x; + int32_t dy = attractor->y - particle->y; // Calculate the force based on inverse square law int32_t distanceSquared = dx * dx + dy * dy + 1; @@ -87,7 +87,7 @@ void Particle_attractor(PSparticle *particle, PSparticle *attractor, uint8_t *co } distanceSquared = 4 * PS_P_RADIUS * PS_P_RADIUS; // limit the distance of particle size to avoid very high forces } - + int32_t shiftedstrength = (int32_t)strength << 16; int32_t force; int32_t xforce; @@ -124,7 +124,7 @@ void Particle_attractor(PSparticle *particle, PSparticle *attractor, uint8_t *co particle->vx += 1; } } - else //save counter value + else // save counter value *counter |= xcounter & 0x0F; // write lower four bits, make sure not to write more than 4 bits } else @@ -150,7 +150,7 @@ void Particle_attractor(PSparticle *particle, PSparticle *attractor, uint8_t *co particle->vy += 1; } } - else // save counter value + else // save counter value *counter |= (ycounter << 4) & 0xF0; // write upper four bits } else @@ -164,60 +164,59 @@ void Particle_attractor(PSparticle *particle, PSparticle *attractor, uint8_t *co void Particle_Move_update(PSparticle *part, bool killoutofbounds, bool wrapX, bool wrapY) { // Matrix dimension - const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; - const uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); + const int32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; + const int32_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); + 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; + // age + part->ttl--; - 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 + // apply velocity + int32_t newX, newY; + newX = part->x + (int16_t)part->vx; + newY = part->y + (int16_t)part->vy; - 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 - } + 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) +void Particle_Bounce_update(PSparticle *part, const uint8_t hardness) { // Matrix dimension const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; @@ -242,15 +241,15 @@ void Particle_Bounce_update(PSparticle *part, const uint8_t hardness) newY = part->y + (int16_t)part->vy; 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 + { // 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 + { // 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 } newX = max(newX, (int16_t)0); // limit to positive @@ -258,19 +257,18 @@ void Particle_Bounce_update(PSparticle *part, const uint8_t hardness) part->x = min(newX, (int16_t)PS_MAX_X); // limit to matrix boundaries part->y = min(newY, (int16_t)PS_MAX_Y); } - } // 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) +void Particle_Gravity_update(PSparticle *part, bool wrapX, bool bounceX, bool bounceY, const uint8_t hardness) { // Matrix dimension - const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; - const uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); + const int32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; + const int32_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); + 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) { @@ -300,7 +298,7 @@ void Particle_Gravity_update(PSparticle *part, bool wrapX, bool bounceX, bool bo } // apply velocity - int16_t newX, newY; + int32_t newX, newY; newX = part->x + (int16_t)part->vx; newY = part->y + (int16_t)part->vy; @@ -323,8 +321,8 @@ void Particle_Gravity_update(PSparticle *part, bool wrapX, bool bounceX, bool bo { 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, (int16_t)0); // limit to positive - newX = min(newX, (int16_t)PS_MAX_X); // limit to matrix boundaries + 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; @@ -339,9 +337,9 @@ void Particle_Gravity_update(PSparticle *part, bool wrapX, bool bounceX, bool bo if (bounceY) { 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, (int16_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 + 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 } else // not bouncing and out of matrix part->outofbounds = 1; @@ -360,21 +358,19 @@ void ParticleSys_render(PSparticle *particles, uint32_t numParticles, bool wrapX #else bool fastcoloradd = false; // on ESP32, there is little benefit from using fast add #endif - - 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); + const int32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; + const int32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); - int16_t x, y; - uint8_t dx, dy; + int32_t x, y; + uint32_t dx, dy; uint32_t intensity; CRGB baseRGB; uint32_t i; - uint8_t brightess; // particle brightness, fades if dying - + uint32_t brightess; // particle brightness, fades if dying + + uint32_t precal1, precal2, precal3; // precalculate values to improve speed + // go over particles and update matrix cells on the way for (i = 0; i < numParticles; i++) @@ -395,37 +391,19 @@ void ParticleSys_render(PSparticle *particles, uint32_t numParticles, bool wrapX else baseRGB = ColorFromPalette(SEGPALETTE, particles[i].hue, 255, LINEARBLEND); - dx = (uint8_t)((uint16_t)particles[i].x % (uint16_t)PS_P_RADIUS); - dy = (uint8_t)((uint16_t)particles[i].y % (uint16_t)PS_P_RADIUS); - - x = (uint8_t)((uint16_t)particles[i].x / (uint16_t)PS_P_RADIUS); - y = (uint8_t)((uint16_t)particles[i].y / (uint16_t)PS_P_RADIUS); + // 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; - // for vx=1, vy=1: starts out with all four pixels at the same color (32/32) - // moves to upper right pixel (64/64) - // then moves one physical pixel up and right(+1/+1), starts out now with - // lower left pixel fully bright (0/0) and moves to all four pixel at same - // color (32/32) - - if (dx < (PS_P_RADIUS >> 1)) // jump to next physical pixel if half of virtual pixel size is reached - { - x--; // shift x to next pixel left, will overflow to 255 if 0 - dx = dx + (PS_P_RADIUS >> 1); - } - else // if jump has ocurred - { - dx = dx - (PS_P_RADIUS >> 1); // adjust dx so pixel fades - } - - if (dy < (PS_P_RADIUS >> 1)) // jump to next physical pixel if half of virtual pixel size is reached - { - y--; // shift y to next pixel down, will overflow to 255 if 0 - dy = dy + (PS_P_RADIUS >> 1); - } - else - { - dy = dy - (PS_P_RADIUS >> 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) * brightess; // multiply by ttl, adds more heat for younger particles + precal3 = dy * brightess; if (wrapX) { // wrap it to the other side if required @@ -447,13 +425,11 @@ void ParticleSys_render(PSparticle *particles, uint32_t numParticles, bool wrapX // intensity is a scaling value from 0-255 (0-100%) // bottom left - if (x < cols && y < rows) - { - // calculate the intensity with linear interpolation - intensity = ((uint32_t)((PS_P_RADIUS)-dx) * ((PS_P_RADIUS)-dy) * (uint32_t)brightess) >> PS_P_SURFACE; // divide by PS_P_SURFACE to distribute the energy - // scale the particle base color by the intensity and add it to the pixel - SEGMENT.addPixelColorXY(x, rows - y - 1, baseRGB.scale8(intensity), fastcoloradd); - } + // 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) @@ -463,7 +439,7 @@ void ParticleSys_render(PSparticle *particles, uint32_t numParticles, bool wrapX } if (x < cols && y < rows) { - intensity = ((uint32_t)dx * ((PS_P_RADIUS)-dy) * (uint32_t)brightess) >> PS_P_SURFACE; // divide by PS_P_SURFACE to distribute the energy + 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 @@ -475,7 +451,7 @@ void ParticleSys_render(PSparticle *particles, uint32_t numParticles, bool wrapX } if (x < cols && y < rows) { - intensity = ((uint32_t)dx * dy * (uint32_t)brightess) >> PS_P_SURFACE; // divide by PS_P_SURFACE to distribute the energy + 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 @@ -489,7 +465,7 @@ void ParticleSys_render(PSparticle *particles, uint32_t numParticles, bool wrapX } if (x < cols && y < rows) { - intensity = ((uint32_t)((PS_P_RADIUS)-dx) * dy * (uint32_t)brightess) >> PS_P_SURFACE; // divide by PS_P_SURFACE to distribute the energy + 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); } } @@ -497,52 +473,56 @@ void ParticleSys_render(PSparticle *particles, uint32_t numParticles, bool wrapX // update & move particle, wraps around left/right if wrapX is true, wrap around up/down if wrapY is true // particles move upwards faster if ttl is high (i.e. they are hotter) -void FireParticle_update(PSparticle *part, bool wrapX) +void FireParticle_update(PSparticle *part, uint32_t numparticles, bool wrapX) { // 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); - + 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); + uint32_t i = 0; - - if (part->ttl > 0) + for (i = 0; i < numparticles; i++) { - // age - part->ttl--; - - // apply velocity - part->x = part->x + (int32_t)part->vx; - part->y = part->y + (int32_t)part->vy + (part->ttl >> 4); // younger particles move faster upward as they are hotter, used for fire - - part->outofbounds = 0; - // check if particle is out of bounds, wrap around to other side if wrapping is enabled - // x-direction - if ((part->x < 0) || (part->x > PS_MAX_X)) + if (part[i].ttl > 0) { - if (wrapX) + // age + part[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 + + part[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) { - part->x = part->x % (PS_MAX_X + 1); - if (part->x < 0) - part->x = PS_MAX_X - part->x; + part[i].outofbounds = 1; } - else + else if (part[i].y > PS_MAX_Y) // particle moved out on the top { - part->ttl = 0; + part[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 (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; + } + else + { + part[i].ttl = 0; + } + } } - } - - // y-direction - if ((part->y < -(PS_P_RADIUS << 4)) || (part->y > PS_MAX_Y)) - { // position up to 8 pixels below the matrix is allowed, used for wider flames at the bottom - part->ttl = 0; - } - else if (part->y < 0) - { - part->outofbounds = 1; } } } @@ -552,20 +532,20 @@ void FireParticle_update(PSparticle *part, bool wrapX) void ParticleSys_renderParticleFire(PSparticle *particles, uint32_t numParticles, bool wrapX) { - const uint32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; - const uint32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); + 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 tempVal; + uint32_t pixelheat; + uint32_t precal1, precal2, precal3; // precalculated values to improve speed 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++) { - if (particles[i].outofbounds) + if (particles[i].outofbounds) { continue; } @@ -575,38 +555,16 @@ void ParticleSys_renderParticleFire(PSparticle *particles, uint32_t numParticles continue; } - - - dx = (uint8_t)((uint16_t)particles[i].x % (uint16_t)PS_P_RADIUS); - dy = (uint8_t)((uint16_t)particles[i].y % (uint16_t)PS_P_RADIUS); - - x = (uint8_t)((uint16_t)particles[i].x >> PS_P_RADIUS_SHIFT); // divide by PS_P_RADIUS which is 64, so can bitshift (compiler may not optimize automatically) - y = (uint8_t)((uint16_t)particles[i].y >> PS_P_RADIUS_SHIFT); - - if (dx < (PS_P_RADIUS >> 1)) // jump to next physical pixel if half of virtual pixel size is reached - { - x--; // shift left - dx += PS_P_RADIUS >> 1; // add half a radius - } - else // if jump has ocurred, fade pixel - { - // adjust dx so pixel fades - dx -= PS_P_RADIUS >> 1; - } + // 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; - if (dy < (PS_P_RADIUS >> 1)) // jump to next physical pixel if half of virtual pixel size is reached - { - y--; // shift row - dy += PS_P_RADIUS >> 1; - } - else + if (wrapX) { - // adjust dy so pixel fades - dy -= PS_P_RADIUS >> 1; - } - - if (wrapX) - { if (x < 0) { // left half of particle render is out of frame, wrap it x = cols - 1; @@ -614,47 +572,51 @@ void ParticleSys_renderParticleFire(PSparticle *particles, uint32_t numParticles } // 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; + // bottom left - //if (x < cols && x >=0 && y < rows && y >=0) + if (x < cols && x >= 0 && y < rows && y >= 0) { - tempVal = (((uint32_t)((PS_P_RADIUS)-dx) * ((PS_P_RADIUS)-dy) * (uint32_t)particles[i].ttl) >> PS_P_SURFACE); - PartMatrix_addHeat(x, y, tempVal, 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) + 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) + { // 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) + if (x < cols && y < rows && y >= 0) { - tempVal = (((uint32_t)dx * ((PS_P_RADIUS)-dy) * (uint32_t)particles[i].ttl) >> PS_P_SURFACE); - PartMatrix_addHeat(x, y, tempVal, 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) + 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) + if (x < cols && y < rows) { - tempVal = (((uint32_t)dx * dy * (uint32_t)particles[i].ttl) >> PS_P_SURFACE); // - PartMatrix_addHeat(x, y, tempVal, 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) + 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 + { // 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) + if (x < cols && x >= 0 && y < rows) { - tempVal = (((uint32_t)((PS_P_RADIUS)-dx) * dy * (uint32_t)particles[i].ttl) >> PS_P_SURFACE); - PartMatrix_addHeat(x, y, tempVal, 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) + 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) } } } @@ -663,16 +625,19 @@ void ParticleSys_renderParticleFire(PSparticle *particles, uint32_t numParticles void PartMatrix_addHeat(uint8_t col, uint8_t row, uint32_t heat, uint32_t rows) { - //const uint32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); + // 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; - uint8_t colormode = map(SEGMENT.custom3, 0, 31, 0, 5); // get color mode from slider (3bit value) + uint32_t colormode = map(SEGMENT.custom3, 0, 31, 0, 5); // get color mode from slider (3bit value) // 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 - - heat = heat << 3; // need to take a larger value to scale ttl value of particle to a good heat value that decays fast enough - + // 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) + #else + heat = heat * 10; + #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; @@ -685,7 +650,7 @@ void PartMatrix_addHeat(uint8_t col, uint8_t row, uint32_t heat, uint32_t rows) 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 + // this cannot produce an underflow since we never add more than the initial heat value } else { @@ -701,13 +666,13 @@ void PartMatrix_addHeat(uint8_t col, uint8_t row, uint32_t heat, uint32_t rows) if (currentcolor[i] < 255) { - newcolorvalue = (uint16_t)currentcolor[i] + heat; // add heat, check if it overflows + 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 + // this cannot produce an underflow since we never add more than the initial heat value } else { @@ -732,23 +697,23 @@ void PartMatrix_addHeat(uint8_t col, uint8_t row, uint32_t heat, uint32_t rows) } // detect collisions in an array of particles and handle them -void detectCollisions(PSparticle* particles, uint32_t numparticles, uint8_t hardness) +void detectCollisions(PSparticle *particles, uint32_t numparticles, uint8_t hardness) { // detect and handle collisions - uint32_t i,j; - int32_t startparticle = 0; - int32_t endparticle = numparticles >> 1; // do half the particles, significantly speeds things up + uint32_t i, j; + uint32_t startparticle = 0; + uint32_t endparticle = numparticles >> 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) startparticle = endparticle; endparticle = numparticles; } - + 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 + // 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 { int32_t dx, dy; // distance to other particles for (j = i + 1; j < numparticles; j++) @@ -756,7 +721,7 @@ void detectCollisions(PSparticle* particles, uint32_t numparticles, uint8_t hard 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))) @@ -771,37 +736,44 @@ 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) -void handleCollision(PSparticle *particle1, PSparticle *particle2, const uint8_t hardness) +// 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) { int32_t dx = particle2->x - particle1->x; int32_t dy = particle2->y - particle1->y; int32_t distanceSquared = dx * dx + dy * dy; - // Calculate relative velocity + // Calculate relative velocity (if it is zero, could exit but extra check does not overall speed but deminish it) int32_t relativeVx = (int16_t)particle2->vx - (int16_t)particle1->vx; int32_t relativeVy = (int16_t)particle2->vy - (int16_t)particle1->vy; + if (distanceSquared == 0) // add distance in case particles exactly meet at center, prevents dotProduct=0 (this can only happen if they move towards each other) { - // Adjust positions based on relative velocity direction - - if (relativeVx < 0) { //if true, particle2 is on the right side - particle1->x--; - particle2->x++; - } else{ - particle1->x++; - particle2->x--; - } - - if (relativeVy < 0) { - particle1->y--; - particle2->y++; - } else{ - particle1->y++; - particle2->y--; - } + // Adjust positions based on relative velocity direction TODO: is this really needed? only happens on fast particles, would save some code (but make it a tiny bit less accurate on fast particles but probably not an issue) + + if (relativeVx < 0) + { // if true, particle2 is on the right side + particle1->x--; + particle2->x++; + } + else + { + particle1->x++; + particle2->x--; + } + + if (relativeVy < 0) + { + particle1->y--; + particle2->y++; + } + else + { + particle1->y++; + particle2->y--; + } distanceSquared++; } // Calculate dot product of relative velocity and relative distance @@ -810,77 +782,82 @@ void handleCollision(PSparticle *particle1, PSparticle *particle2, const uint8_t // If particles are moving towards each other if (dotProduct < 0) { - const uint8_t bitshift = 14; // bitshift used to avoid floats + const uint32_t bitshift = 14; // bitshift used to avoid floats // Calculate new velocities after collision - int32_t impulse = (((dotProduct << (bitshift)) / (distanceSquared)) * hardness) >> 8; + int32_t impulse = (((dotProduct << (bitshift)) / (distanceSquared)) * (hardness+1)) >> 8; int32_t ximpulse = (impulse * dx) >> bitshift; int32_t yimpulse = (impulse * dy) >> bitshift; particle1->vx += ximpulse; particle1->vy += yimpulse; particle2->vx -= ximpulse; particle2->vy -= yimpulse; - - if (hardness < 50) // if particles are soft, they become 'sticky' i.e. slow movements are stopped + /* + //TODO: this is removed for now as it does not seem to do much and does not help with piling. if soft, much energy is lost anyway at a collision, so they are automatically sticky + //also second version using multiplication is slower on ESP8266 than the if's + if (hardness < 50) // if particles are soft, they become 'sticky' i.e. they are slowed down at collisions { - particle1->vx = (particle1->vx < 2 && particle1->vx > -2) ? 0 : particle1->vx; - particle1->vy = (particle1->vy < 2 && particle1->vy > -2) ? 0 : particle1->vy; + + //particle1->vx = (particle1->vx < 2 && particle1->vx > -2) ? 0 : particle1->vx; + //particle1->vy = (particle1->vy < 2 && particle1->vy > -2) ? 0 : particle1->vy; + + //particle2->vx = (particle2->vx < 2 && particle2->vx > -2) ? 0 : particle2->vx; + //particle2->vy = (particle2->vy < 2 && particle2->vy > -2) ? 0 : particle2->vy; - particle2->vx = (particle2->vx < 2 && particle2->vx > -2) ? 0 : particle2->vx; - particle2->vy = (particle2->vy < 2 && particle2->vy > -2) ? 0 : particle2->vy; - } - } + const uint32_t coeff = 100; + particle1->vx = ((int32_t)particle1->vx * coeff) >> 8; + particle1->vy = ((int32_t)particle1->vy * coeff) >> 8; + + particle2->vx = ((int32_t)particle2->vx * coeff) >> 8; + particle2->vy = ((int32_t)particle2->vy * coeff) >> 8; + }*/ + } // particles have volume, push particles apart if they are too close by moving each particle by a fixed amount away from the other particle // if pushing is made dependent on hardness, things start to oscillate much more, better to just add a fixed, small increment (tried lots of configurations, this one works best) // one problem remaining is, particles get squished if (external) force applied is higher than the pushback but this may also be desirable if particles are soft. also some oscillations cannot be avoided without addigng a counter - if (distanceSquared < (int32_t)2 * PS_P_HARDRADIUS * PS_P_HARDRADIUS) + if (distanceSquared < 2 * PS_P_HARDRADIUS * PS_P_HARDRADIUS) { - uint8_t choice = random8(2);//randomly choose one particle to push, avoids oscillations - const int32_t HARDDIAMETER = (int32_t)2*PS_P_HARDRADIUS; - + uint8_t choice = dotProduct & 0x01; // random16(2); // randomly choose one particle to push, avoids oscillations note: dotprouct LSB should be somewhat random, so no need to calculate a random number + const int32_t HARDDIAMETER = 2 * PS_P_HARDRADIUS; + const int32_t pushamount = 2; //push a small amount + int32_t push = pushamount; if (dx < HARDDIAMETER && dx > -HARDDIAMETER) { // distance is too small, push them apart - int32_t push; if (dx <= 0) - push = -1;//-(PS_P_HARDRADIUS + dx); // inverted push direction - else - push = 1;//PS_P_HARDRADIUS - dx; + push = -pushamount; //-(PS_P_HARDRADIUS + dx); // inverted push direction if (choice) // chose one of the particles to push, avoids oscillations particle1->x -= push; else - particle2->x += push; + particle2->x += push; } + push = pushamount; // reset push variable to 1 if (dy < HARDDIAMETER && dy > -HARDDIAMETER) { - - int32_t push; if (dy <= 0) - push = -1; //-(PS_P_HARDRADIUS + dy); // inverted push direction - else - push = 1; // PS_P_HARDRADIUS - dy; + push = -pushamount; //-(PS_P_HARDRADIUS + dy); // inverted push direction if (choice) // chose one of the particles to push, avoids oscillations particle1->y -= push; else - particle2->y += push; + particle2->y += push; } - //note: pushing may push particles out of frame, if bounce is active, it will move it back as position will be limited to within frame, if bounce is disabled: bye bye + // note: pushing may push particles out of frame, if bounce is active, it will move it back as position will be limited to within frame, if bounce is disabled: bye bye } - - } -// slow down particle by friction, the higher the speed, the higher the friction -void applyFriction(PSparticle *particle, uint8_t coefficient) +// 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) { - if(particle->ttl) + //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) { - particle->vx = ((int16_t)particle->vx * (255 - coefficient)) >> 8; - particle->vy = ((int16_t)particle->vy * (255 - coefficient)) >> 8; + particle->vx = ((int16_t)particle->vx * coefficient) >> 8; + particle->vy = ((int16_t)particle->vy * coefficient) >> 8; } } diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index 6b01ee9c73..8b275a07e9 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -39,15 +39,6 @@ //todo: can add bitfields to add in more stuff, but accessing bitfields is slower than direct memory access! //flags as bitfields is still very fast to access. - -union Flags { - struct { - - }; - uint8_t flagsByte; -}; - - //struct for a single particle typedef struct { int16_t x; //x position in particle system @@ -93,9 +84,9 @@ void Particle_Move_update(PSparticle *part, bool killoutofbounds = false, bool w 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, bool wrapX = false); +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 uint8_t hardness); -void applyFriction(PSparticle *particle, uint8_t coefficient); +void handleCollision(PSparticle *particle1, PSparticle *particle2, const uint32_t hardness); +void applyFriction(PSparticle *particle, int32_t coefficient); From 500b84724a470f0140fbd74597eb1a09b958f7d0 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sat, 23 Mar 2024 20:11:48 +0100 Subject: [PATCH 049/219] bugfix --- wled00/FXparticleSystem.h | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index 8b275a07e9..91156eb30d 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -31,14 +31,11 @@ //particle dimensions (subpixel division) #define PS_P_RADIUS 64 //subpixel size, each pixel is divided by this for particle movement, if this value is changed, also change the shift defines (next two lines) +#define PS_P_HALFRADIUS 32 #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 - - -//todo: can add bitfields to add in more stuff, but accessing bitfields is slower than direct memory access! -//flags as bitfields is still very fast to access. //struct for a single particle typedef struct { int16_t x; //x position in particle system From c6d5d3efa254387702e589df4f413987f106da0f Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sun, 24 Mar 2024 12:02:55 +0100 Subject: [PATCH 050/219] updated PS Fireworks with many changes and fine-tuning of parameters -removed spiral explosions -added more versatility to circular explosions -removed user selectable amount of rockets -tuned explosion size of circular explosions to match random explosions (more or less, may need improvement) -changed order of sliders in volcano animation --- wled00/FX.cpp | 232 ++++++++++++++++++-------------------- wled00/FXparticleSystem.h | 7 -- 2 files changed, 112 insertions(+), 127 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 11f8ab8e5b..e846ae13e3 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -8047,65 +8047,57 @@ 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) return mode_static(); - const uint32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; const uint32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); - // particle system box dimensions const uint32_t Max_x = (cols * PS_P_RADIUS - 1); const uint32_t Max_y = (rows * PS_P_RADIUS - 1); - #ifdef ESP8266 const uint32_t numParticles = 120; - const uint8_t MaxNumRockets = 2; #else - const uint32_t numParticles = 650; - const uint8_t MaxNumRockets = 8; + const uint32_t numParticles = 400; #endif - + const uint8_t numRockets = 4; PSparticle *particles; PSpointsource *rockets; - // allocate memory and divide it into proper pointers, max is 32k for all segments. uint32_t dataSize = sizeof(PSparticle) * numParticles; - dataSize += sizeof(PSpointsource) * (MaxNumRockets); + dataSize += sizeof(PSpointsource) * (numRockets); if (!SEGENV.allocateData(dataSize)) return mode_static(); // allocation failed; //allocation failed - 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 + MaxNumRockets); // cast the data array into a particle pointer - + particles = reinterpret_cast(rockets + numRockets); // cast the data array into a particle pointer uint32_t i = 0; - uint32_t j = 0; - uint8_t numRockets = min(uint8_t(1 + ((SEGMENT.custom3) >> 2)), MaxNumRockets); // 1 to 8 - + uint32_t j = 0; if (SEGMENT.call == 0) // initialization { for (i = 0; i < numParticles; i++) { particles[i].ttl = 0; } - for (i = 0; i < numRockets; i++) + for (j = 0; j < numRockets; j++) { - rockets[i].source.ttl = random16(20 * i); // first rocket starts immediately, others follow soon - rockets[i].source.vy = -1; // at negative speed, no particles are emitted and if rocket dies, it will be relaunched + rockets[j].source.ttl = random16(20 * j); // first rocket starts immediately, others follow soon + rockets[j].source.vy = -1; // at negative speed, no particles are emitted and if rocket dies, it will be relaunched } } - - // update particles, create particles - uint8_t circularexplosion = random16(numRockets + 2); //choose a rocket by random (but not every round one will be picked) - uint8_t spiralexplosion = random16(numRockets + 2); - // check each rocket's state and emit particles according to its state: moving up = emit exhaust, at top = explode; falling down = standby time - uint16_t emitparticles; // number of particles to emit for each rocket's state + int32_t emitparticles; // number of particles to emit for each rocket's state i = 0; + // variables for circle explosions + uint32_t speed; + uint32_t currentspeed; + uint8_t angle; + uint32_t counter; + uint32_t angleincrement; + uint32_t speedvariation; + + bool circularexplosion = false; for (j = 0; j < numRockets; j++) { // determine rocket state by its speed: @@ -8118,75 +8110,77 @@ uint16_t mode_particlefireworks(void) emitparticles = 0; } else // speed is zero, explode! - { - - #ifdef ESP8266 - emitparticles = random16(SEGMENT.intensity >> 2) + 10; // defines the size of the explosion - #else - emitparticles = random16(SEGMENT.intensity >> 1) + 10; // defines the size of the explosion - #endif - rockets[j].source.vy = -1; // set speed negative so it will emit no more particles after this explosion until relaunch - if (j == circularexplosion || j == spiralexplosion) // chosen rocket, do an angle emit (creating a circle) + { +#ifdef ESP8266 + emitparticles = random16(SEGMENT.intensity >> 3) + (SEGMENT.intensity >> 3) + 5; // defines the size of the explosion +#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 { - emitparticles = emitparticles >> 3; // emit less particles for circle-explosions - rockets[j].maxLife = 150; - rockets[j].minLife = 120; - rockets[j].var = 0; // speed variation around vx,vy (+/- var/2) + circularexplosion = true; + speed = 2 + random16(3); + currentspeed = speed; + counter = 0; + angleincrement = random16(20) + 10; + speedvariation = random16(3); + angle = random16(); // random start angle + // calculate the number of particles to make complete circles + int percircle = 256 / angleincrement + 2; + #ifdef ESP8266 //TODO: this line is untested on ESP8266 + int circles = (SEGMENT.intensity >> 7) + 1; // max(4, (int)min((int32_t)1, (emitparticles>>2) / percircle)); + #else + int circles = (SEGMENT.intensity >> 6) + 1;// max(4, (int)min((int32_t)1, (emitparticles>>2) / percircle)); + #endif + emitparticles = percircle * circles; + rockets[j].var = 0; //no variation for a nice circle } } - - uint8_t speed = 3; - uint8_t angle = 0; - if (j == spiralexplosion) - angle = random16(8); - - while(i < numParticles) + for (i = 0; i < numParticles; i++) { if (particles[i].ttl == 0) { // particle is dead - - if (j == circularexplosion && emitparticles > 2) //do circle emit + if (emitparticles > 0) { - Emitter_Angle_emit(&rockets[j], &particles[i],angle,speed); - - if (angle > 242) // full circle completed, increase speed and reset angle + if (circularexplosion) // do circle emit { - angle += 10; - speed += 6; - rockets[j].source.hue = random16(); // new color for next row - rockets[j].source.sat = random16(); - if(emitparticles > 12) - emitparticles-=12; //emitted about 12 particles for one circle, ensures no incomplete circles are done + Emitter_Angle_emit(&rockets[j], &particles[i], angle, currentspeed); + emitparticles--; + // set angle for next particle + angle += angleincrement; + counter++; + if (counter & 0x01) // make every second particle a lower speed + currentspeed = speed - speedvariation; else - emitparticles = 0; + currentspeed = speed; + if (counter > 256 / angleincrement + 2) // full circle completed, increase speed + { + counter = 0; + speed += 5; //increase speed to form a second circle + speedvariation = speedvariation<<1; //double speed variation + rockets[j].source.hue = random16(); // new color for next circle + rockets[j].source.sat = min((uint16_t)40,random16()); + } + } + else + { + Emitter_Fountain_emit(&rockets[j], &particles[i]); + emitparticles--; + if ((j % 3) == 0) + { + rockets[j].source.hue = random16(); // random color for each particle + rockets[j].source.sat = min((uint16_t)40, random16()); + } } - - //set angle for next particle - angle += 21; //about 30° - } - else if (j == spiralexplosion && emitparticles > 2) // do spiral emit - { - Emitter_Angle_emit(&rockets[j], &particles[i], angle, speed); - emitparticles-=2; // only emit half as many particles as in circle explosion, it gets too huge otherwise - angle += 15; - - speed++; - rockets[j].source.hue++; - rockets[j].source.sat = random16(155)+100; - - } - else if (emitparticles > 0) - { - Emitter_Fountain_emit(&rockets[j], &particles[i]); - emitparticles--; } else break; // done emitting for this rocket } - i++; } + circularexplosion = false; //reset for next rocket } - + // update particles for (i = 0; i < numParticles; i++) { @@ -8195,51 +8189,49 @@ uint16_t mode_particlefireworks(void) Particle_Gravity_update(&particles[i], SEGMENT.check1, SEGMENT.check2, SEGMENT.check3, SEGMENT.custom2); } } - // update the rockets, set the speed state - for (i = 0; i < numRockets; i++) + for (j = 0; j < numRockets; j++) { - if (rockets[i].source.ttl) + if (rockets[j].source.ttl) + { + Particle_Move_update(&rockets[j].source); // move the rocket, age the rocket (ttl--) + } + else if (rockets[j].source.vy > 0) // rocket has died and is moving up. stop it so it will explode (is handled in the code above) { - Particle_Move_update(&rockets[i].source); // move the rocket, age the rocket (ttl--) - } - else if (rockets[i].source.vy > 0) - { // rocket has died and is moving up. stop it so it will explode (is handled in the code above) - rockets[i].source.vy = 0; // set speed to zero so code above will recognize this as an exploding rocket - rockets[i].source.hue = random16(); // random color - rockets[i].source.sat = random16(100)+155; - rockets[i].maxLife = 200; - rockets[i].minLife = 50; - rockets[i].source.ttl = random16((255 - SEGMENT.speed))+50; // standby time til next launch (in frames at 42fps, max of 300 is about 7 seconds - rockets[i].vx = 0; // emitting speed - rockets[i].vy = 0; // emitting speed - rockets[i].var = (SEGMENT.intensity >> 3) + 10; // speed variation around vx,vy (+/- var/2) - } - else if (rockets[i].source.vy < 0) // rocket is exploded and time is up (ttl=0 and negative speed), relaunch it + rockets[j].source.vy = 0; // set speed to zero so code above will recognize this as an exploding rocket + rockets[j].source.hue = random16(); // random color + 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].vx = 0; // emitting speed + rockets[j].vy = 3; // emitting speed + rockets[j].var = (SEGMENT.intensity >> 3) + 10; // speed variation around vx,vy (+/- var/2) + } + else if (rockets[j].source.vy < 0) // rocket is exploded and time is up (ttl=0 and negative speed), relaunch it { // reinitialize rocket - rockets[i].source.y = 1; // start from bottom - rockets[i].source.x = (rand() % (Max_x >> 1)) + (Max_y >> 2); // centered half - rockets[i].source.vy = random16(SEGMENT.custom1 >> 3) + 5; // rocket speed depends also on rocket height - rockets[i].source.vx = random16(5) - 2; - rockets[i].source.hue = 30; // rocket exhaust = orange (if using rainbow palette) - rockets[i].source.sat = 30; // low saturation -> exhaust is off-white - rockets[i].source.ttl = random16(SEGMENT.custom1) + (SEGMENT.custom1 >> 1); // sets explosion height (rockets explode at the top if set too high as paticle update set speed to zero if moving out of matrix) - rockets[i].maxLife = 30; // exhaust particle life - rockets[i].minLife = 10; - rockets[i].vx = 0; // emitting speed - rockets[i].vy = 0; // emitting speed - rockets[i].var = 6; // speed variation around vx,vy (+/- var/2) + rockets[j].source.y = 1; // start from bottom + rockets[j].source.x = (rand() % (Max_x >> 1)) + (Max_y >> 2); // centered half + rockets[j].source.vy = random16(SEGMENT.custom1 >> 3) + 5; // rocket speed depends also on rocket height + rockets[j].source.vx = random16(5) - 2; + rockets[j].source.hue = 30; // rocket exhaust = orange (if using rainbow palette) + rockets[j].source.sat = 30; // low saturation -> exhaust is off-white + rockets[j].source.ttl = random16(SEGMENT.custom1) + (SEGMENT.custom1 >> 1); // sets explosion height (rockets explode at the top if set too high as paticle update set speed to zero if moving out of matrix) + rockets[j].maxLife = 30; // exhaust particle life + rockets[j].minLife = 10; + rockets[j].vx = 0; // emitting speed + rockets[j].vy = 0; // emitting speed + rockets[j].var = 6; // speed variation around vx,vy (+/- var/2) } } SEGMENT.fill(BLACK); // clear the matrix - // render the particles ParticleSys_render(particles, numParticles, false, false); - return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEFIREWORKS[] PROGMEM = "PS Fireworks@Launches,Explosion Size,Height,Bounce,Rockets,Cylinder,Walls,Ground;;!;012;pal=11,sx=100,ix=50,c1=64,c2=128,c3=10,o1=0,o2=0,o3=0"; +//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) @@ -8317,19 +8309,19 @@ uint16_t mode_particlevolcano(void) { if (spray[i].source.vx > 0) // moving to the right currently { - spray[i].source.vx = SEGMENT.speed >> 4; // spray speed + spray[i].source.vx = SEGMENT.custom1 >> 4; // spray movingspeed } else { - spray[i].source.vx = -(SEGMENT.speed >> 4); // spray speed (is currently moving negative so keep it negative) + spray[i].source.vx = -(SEGMENT.custom1 >> 4); // spray speed (is currently moving negative so keep it negative) } } else{ //wrap on the right side - spray[i].source.vx = SEGMENT.speed >> 4; // spray speed + spray[i].source.vx = SEGMENT.custom1 >> 4; // spray speed if (spray[i].source.x >= Max_x - 32) //compiler warning can be ignored, source.x is always > 0 spray[i].source.x = 1; // wrap if close to border (need to wrap before the bounce updated detects a border collision or it will just be stuck) } - spray[i].vy = SEGMENT.custom1 >> 2; // emitting speed, upward + spray[i].vy = SEGMENT.speed >> 2; // emitting speed spray[i].vx = 0; spray[i].var = SEGMENT.custom3; // emiting variation = nozzle size (custom 3 goes from 0-32) spray[i].source.ttl = 255; // source never dies, replenish its lifespan @@ -8375,7 +8367,7 @@ uint16_t mode_particlevolcano(void) ParticleSys_render(particles, numParticles, false, false); return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEVOLCANO[] PROGMEM = "PS Volcano@Move,Intensity,Speed,Bounce,Size,Color by Age,Walls,Collisions;;!;012;pal=35,sx=0,ix=160,c1=100,c2=160,c3=10,o1=1,o2=0,o3=0"; +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) @@ -9573,7 +9565,7 @@ uint32_t random16_ESP(uint32_t limit) * Uses palette for particle color * by DedeHai (Damian Schneider) */ - +/* uint16_t mode_particlecenterGEQ(void) { @@ -9698,7 +9690,7 @@ uint16_t mode_particlecenterGEQ(void) return FRAMETIME; } static const char _data_FX_MODE_PARTICLECCIRCULARGEQ[] PROGMEM = "PS Center GEQ@Speed,Color Change,Particle Speed,Spray Count,Nozzle Size,Random Color, Direction;;!;012;pal=56,sx=0,ix=222,c1=190,c2=200,c3=0,o1=0,o2=0"; - +*/ #endif // WLED_DISABLE_2D @@ -9953,7 +9945,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); + // addEffect(FX_MODE_PARTICLECENTERGEQ, &mode_particlecenterGEQ, _data_FX_MODE_PARTICLECCIRCULARGEQ); #endif // WLED_DISABLE_2D diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index 91156eb30d..f4c2163757 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -66,13 +66,6 @@ typedef struct { #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 -/* -//todo: make these local variables -uint8_t vortexspeed; //speed around vortex -uint8_t vortexdirection; //1 or 0 -int8_t vortexpull; //if positive, vortex pushes, if negative it pulls -*/ - 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); From 03967a95eaebec20bf1b30bcfdb8346e138ba954 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Fri, 29 Mar 2024 20:25:13 +0100 Subject: [PATCH 051/219] put particle system in a class. work in progress. another huge update to the particle system. went through the whole code, rewrote many of the functions. many improvements over the previous code. fixed many bugs (and even an ancient one in rendering function). spent many hours optimizing the code for speed and usability. still a work in progress, debugging is ongoing. more updates to come. --- wled00/FX.cpp | 345 +++++----- wled00/FX_fcn.cpp | 3 +- wled00/FXparticleSystem.cpp | 1222 +++++++++++++++++++++-------------- wled00/FXparticleSystem.h | 152 ++++- 4 files changed, 1014 insertions(+), 708 deletions(-) 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); From 5f824c34b3cfdee2703783c06604f041125386d8 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sun, 31 Mar 2024 09:50:46 +0200 Subject: [PATCH 052/219] many (many!) bugfixes, added fire FX back in (and improved it a lot) added local renderbuffer for huge speed boost -lots of bugfixes in Particle system -added local rendering buffer (renders to buffer in heap) -added fast and accurate color-add function -bugfixes in render function -added improved 'sticky' particles in collision (unfinished business) -added ballpit animation back -lots of tweaks to fire FX and fire rendering functions, looks even better now (still unfinished) -added palette render option to fire still many debug print outputs around, needs cleanup at one point --- wled00/FX.cpp | 514 +++++++++++++++------------ wled00/FXparticleSystem.cpp | 687 +++++++++++++++++++++++------------- wled00/FXparticleSystem.h | 51 ++- 3 files changed, 742 insertions(+), 510 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 5a61eb7b2e..ce4dd38823 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -7894,53 +7894,39 @@ uint16_t mode_particlerotatingspray(void) return mode_static(); const uint8_t numSprays = 8; // maximum number of sprays ParticleSystem *PartSys = NULL; + 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 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("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.aux0 = 0; // starting angle SEGMENT.aux1 = 0x01; // check flags -//TODO: use SEGMENT.step for smooth direction change + // TODO: use SEGMENT.step for smooth direction change for (i = 0; i < numSprays; i++) - { - 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.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].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 { @@ -7948,8 +7934,49 @@ uint16_t mode_particlerotatingspray(void) PartSys->sources[i].source.hue = coloroffset * i; } } + PartSys->setKillOutOfBounds(true); + Serial.print("segment data ptr in FX"); + Serial.println((uintptr_t)(SEGMENT.data)); + Serial.println("INIT done"); } - + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS +/* + if (SEGMENT.call < 3) + { + Serial.print("segment data ptr in candyFX"); + Serial.println((uintptr_t)(SEGMENT.data)); + }*/ + //Serial.print("rotPSP "); + //Serial.println((uintptr_t)PartSys); + + 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) + } +//!!! + for (i = 0; i < numSprays; i++) + { + 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; + } + } +//!!! if (SEGMENT.check1 != (SEGMENT.aux1 & 0x01)) //state change { if (SEGMENT.check1) @@ -8045,10 +8072,8 @@ uint16_t mode_particlerotatingspray(void) for (i = 0; i < spraycount; i++) // emit one particle per spray (if available) { - PartSys->SprayEmit(PartSys->sources[j]); + 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 @@ -8394,113 +8419,120 @@ static const char _data_FX_MODE_PARTICLEVOLCANO[] PROGMEM = "PS Volcano@Speed,In * realistic fire effect using particles. heat based and using perlin-noise for wind * by DedeHai (Damian Schneider) */ -/* + +//TODO: +//do not use with to calculate numflames. it changes when mirror/transpose is activated, so will crash if height>width and then transposed +//if using width*height and assuming a square, it will look sparse on a wide matrix... +//could just allocate way too many and then dynamically adjust at at the expense of ram usage (but flames only use about 16bytes so is ok) +//TODO: add 2D perlin noise to add to flame speed for randomness? may look good, may look awful, test it. also may hit FPS hard. uint16_t mode_particlefire(void) { if (SEGLEN == 1) - return mode_static(); - - const uint32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; - - // particle system box dimensions - const uint32_t Max_x = (cols * PS_P_RADIUS - 1); + return mode_static(); + ParticleSystem *PartSys = NULL; + const uint32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; + const uint32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); + uint32_t i; //index variable - #ifdef ESP8266 - const uint32_t numFlames = min((uint32_t)12, (cols<<1)); // limit to 18 flames, not enough ram on ESP8266 - const uint32_t numParticles = numFlames * 15; //limit number of particles to about 180 or ram will be depleted +#ifdef ESP8266 + const uint32_t numFlames = min((uint32_t)12, (cols << 1)); // limit to 18 flames, not enough ram on ESP8266 const uint32_t numNormalFlames = numFlames - (numFlames / 3); // number of normal flames, rest of flames are baseflames - uint32_t percycle = numFlames >> 2 ;// maximum number of particles emitted per cycle - #else - const uint32_t numFlames = (cols << 1); // number of flames: depends on fire width. for a fire width of 16 pixels, about 25-30 flames give good results, add a few for the base flames - const uint32_t numParticles = numFlames * 18; - const uint32_t numNormalFlames = numFlames - (cols / 3); // number of normal flames, rest of flames are baseflames - uint32_t percycle = numFlames / 3; // maximum number of particles emitted per cycle + uint32_t percycle = numFlames >> 2; // maximum number of particles emitted per cycle +#else + const uint32_t numFlames = (cols*2); // number of flames: depends on fire width. for a fire width of 16 pixels, about 25-30 flames give good results, add a few for the base flames + const uint32_t numNormalFlames = numFlames;//- (cols / 2); // number of normal flames, rest of flames are baseflames + uint32_t percycle = (numFlames) / 2; // maximum number of particles emitted per cycle #endif - - PSparticle *particles; - PSsource *flames; - - // allocate memory and divide it into proper pointers - uint32_t dataSize = sizeof(PSparticle) * numParticles; - dataSize += sizeof(PSsource) * (numFlames); - - if (!SEGENV.allocateData(dataSize)) - { - return mode_static(); // allocation failed; //allocation failed - } - - 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 - - uint32_t i; - - if (SEGMENT.call == 0) // initialization + if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. { + if (!initParticleSystem(PartSys, numFlames)) + return mode_static(); // allocation failed; //allocation failed + Serial.println("fireinit done"); SEGMENT.aux0 = rand(); // aux0 is wind position (index) in the perlin noise - // make sure all particles start out dead - for (i = 0; i < numParticles; i++) - { - particles[i].ttl = 0; - } - // initialize the flame sprays for (i = 0; i < numFlames; i++) { - flames[i].source.ttl = 0; - flames[i].source.vx = 0; // emitter moving speed; - flames[i].source.vy = 0; + PartSys->sources[i].source.ttl = 0; + PartSys->sources[i].source.vx = 0; // emitter moving speed; + PartSys->sources[i].source.vy = 0; // note: other parameters are set when creating the flame (see blow) } + Serial.print("segment data ptr in fireFX"); + Serial.println((uintptr_t)(SEGMENT.data)); } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS +/* + if (SEGMENT.call < 3) + { + Serial.print("segment data ptr in fireFX"); + Serial.println((uintptr_t)(SEGMENT.data)); + }*/ + + if (PartSys == NULL) + { + Serial.println("ERROR: paticle system not found, nullpointer"); + return mode_static(); // something went wrong, no data! (TODO: ask how to handle this so it always works) + } + + if(SEGMENT.check2) //wrap X set + PartSys->setWrapX(true); // update the flame sprays: for (i = 0; i < numFlames; i++) { - if (flames[i].source.ttl > 0) + if ( PartSys->sources[i].source.ttl > 0) { - flames[i].source.ttl--; + PartSys->sources[i].source.ttl--; } else // flame source is dead { // initialize new flame: set properties of source if (random16(20) == 0 || SEGMENT.call == 0) // from time to time, change flame position { - if (SEGMENT.check1) - { // wrap around in X direction, distribute randomly - flames[i].source.x = rand() % Max_x; + if (SEGMENT.check2) // wrap around in X direction, distribute randomly + { + PartSys->sources[i].source.x = rand() % (PartSys->maxX - PS_P_HALFRADIUS + 1); // note: cannot use rand16() here, it is not random enough: tends to burn on left side } else // no X-wrapping - { - flames[i].source.x = (rand() % (Max_x - (PS_P_RADIUS * ((cols>>3)+1)))) + PS_P_RADIUS * ((cols>>4)+1); // distribute randomly but not close to the corners (cannot use random16() it is not random enough, tends to burn on left side) + { + PartSys->sources[i].source.x = (rand() % ((PartSys->maxX - PS_P_HALFRADIUS + 1) - (PS_P_RADIUS * ((cols >> 3) + 1)))) + PS_P_RADIUS * ((cols >> 4) + 1); // distribute randomly but not close to the corners } } - flames[i].source.y = -PS_P_RADIUS; // set the source below the frame + PartSys->sources[i].source.y = -PS_P_RADIUS; // set the source below the frame if (i < numNormalFlames) - { - 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) - flames[i].vy = 5 + (SEGMENT.speed >> 2); // emitting speed (upwards) - flames[i].var = random16(5) + 3;; // speed variation around vx,vy (+/- var/2) + { + PartSys->sources[i].source.ttl = 1 + random16((SEGMENT.intensity * SEGMENT.intensity) >> 8) / (1 + (SEGMENT.speed >> 5)); //'hotness' of fire, faster flames reduce the effect or flame height will scale too much with speed + // PartSys->sources[i].source.ttl = (rand() % ((SEGMENT.intensity * SEGMENT.intensity) >> 9) / (1 + (SEGMENT.speed >> 6))) + 10; //'hotness' of fire, faster flames reduce the effect or flame height will scale too much with speed + // PartSys->sources[i].source.ttl = random16(SEGMENT.intensity+10) + 5; + PartSys->sources[i].maxLife = random16(7) + 13; // defines flame height together with the vy speed, vy speed*maxlife/PS_P_RADIUS is the average flame height + PartSys->sources[i].minLife = 4; + PartSys->sources[i].vx = (int8_t)random16(4) - 2; // emitting speed (sideways) + PartSys->sources[i].vy = 5 + (SEGMENT.speed >> 2); // emitting speed (upwards) + PartSys->sources[i].var = random16(5) + 3; // speed variation around vx,vy (+/- var/2) } - else - { // base flames to make the base brighter, flames are slower and short lived - flames[i].source.ttl = random16(25) + 15; // lifetime of one flame - flames[i].maxLife = 25; // defines flame height together with the vy speed, vy speed*maxlife/PS_P_RADIUS is the average flame height - flames[i].minLife = 12; - flames[i].vx = 0; // emitting speed, sideways - flames[i].vy = (SEGMENT.custom1 >> 4); // slow emitting speed (upwards) - flames[i].var = 5; // speed variation around vx,vy (+/- var/2) + else // base flames: make the base brighter, flames are slower and short lived + { + // PartSys->sources[i].source.ttl = random16(25) + 15; // lifetime of one flame + // PartSys->sources[i].maxLife = 25; // defines flame height together with the vy speed, vy speed*maxlife/PS_P_RADIUS is the average flame height + // PartSys->sources[i].minLife = 12; + // PartSys->sources[i].vx = 0; // emitting speed, sideways + // PartSys->sources[i].vy = (SEGMENT.custom1 >> 4); // slow emitting speed (upwards) + //PartSys->sources[i].var = 5; // speed variation around vx,vy (+/- var/2) + PartSys->sources[i].source.ttl = random16(25) + 15; // lifetime of one flame + PartSys->sources[i].maxLife = 0; // defines flame height together with the vy speed, vy speed*maxlife/PS_P_RADIUS is the average flame height + PartSys->sources[i].minLife = 0; + PartSys->sources[i].vx = 0; // emitting speed, sideways + PartSys->sources[i].vy = (SEGMENT.custom1 >> 4); // slow emitting speed (upwards) + PartSys->sources[i].var = 0; // speed variation around vx,vy (+/- var/2) } } } - - if (SEGMENT.call & 0x01) //update noise position every second frames + // percycle = map(SEGMENT.intensity,0,255, 2, (numFlames*3) / 2); + if (SEGMENT.call & 0x01) // update noise position every second frames { SEGMENT.aux0++; // position in the perlin noise matrix for wind generation if (SEGMENT.call & 0x02) //every tird frame @@ -8510,39 +8542,26 @@ uint16_t mode_particlefire(void) // update particles, create particles uint8_t j = random16(numFlames); // start with a random flame (so each flame gets the chance to emit a particle if perCycle is smaller than number of flames) - for (i = 0; i < numParticles; i++) + + for(i=0; i < percycle; i++) { - if (particles[i].ttl == 0) - { - if(percycle > 0) - { - Emitter_Flame_emit(&flames[j], &particles[i]); - j++; - if (j >= numFlames) - { // or simpler: j=j%numFlames; but that is slow on ESP8266 - j = 0; - } - percycle--; - } - } + PartSys->flameEmit(PartSys->sources[j]); + j = (j + 1) % numFlames; + } + /* //TODO: add wind back in, may need a PS function to add a constant velocity to all particles (or can use force but is slower. or just do it here may be the fastest way) else if (particles[i].y > PS_P_RADIUS) // particle is alive, apply wind if y > 1 { // add wind using perlin noise particles[i].vx = windspeed; //todo: should this be depending on position? would be slower but may look better (used in old, slow fire) - } - } - - FireParticle_update(particles, numParticles, SEGMENT.check1); // update particle, use X-wrapping if check 1 is set by user - + }*/ + SEGMENT.fill(BLACK); // clear the matrix - - // render the particles - ParticleSys_renderParticleFire(particles, numParticles, SEGMENT.check1); // draw matrix + PartSys->updateFire(SEGMENT.custom1, SEGMENT.check1); // update and render the fire, colormode is determined by custom 3 slider return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEFIRE[] PROGMEM = "PS Fire@Speed,Intensity,Base Flames,Wind,Color Scheme, Cylinder;;!;012;sx=130,ix=120,c1=110,c2=128,c3=0,o1=0"; -*/ +static const char _data_FX_MODE_PARTICLEFIRE[] PROGMEM = "PS Fire@Speed,Intensity,Base Flames,Wind,Color Scheme,Palette,Cylinder;;!;035;sx=130,ix=120,c1=110,c2=128,c3=0,o1=0"; + /* 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 @@ -8550,95 +8569,119 @@ 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(); + ParticleSystem *PartSys = NULL; - 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 = 100; // maximum number of particles -#else - const uint32_t numParticles = 500; // maximum number of particles -#endif - - PSparticle *particles; - - // allocate memory and divide it into proper pointers, max is 32k for all segments. - uint32_t dataSize = sizeof(PSparticle) * numParticles; - if (!SEGENV.allocateData(dataSize)) - return mode_static(); // allocation failed; //allocation failed - - // calculate the end of the spray data and assign it as the data pointer for the particles: - particles = reinterpret_cast(SEGENV.data); // cast the data array into a particle pointer - + uint32_t i; // index variable - uint32_t i = 0; + if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. + { + if (!initParticleSystem(PartSys, 1)) //init, no sources needed + return mode_static(); // allocation failed; //allocation failed + PartSys->setKillOutOfBounds(true); + PartSys->enableGravity(true); + PartSys->setUsedParticles((PartSys->numParticles*3)/2); //use 2/3 of available particles + Serial.println("ballpit done"); + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS - if (SEGMENT.call == 0) // initialization + if (PartSys == NULL) { - for (i = 0; i < numParticles; i++) - { - particles[i].ttl = 0; - } + Serial.println("ERROR: paticle system not found, nullpointer"); + return mode_static(); // something went wrong, no data! (TODO: ask how to handle this so it always works) } - - if (SEGMENT.call % (64 - (SEGMENT.intensity >> 2)) == 0 && SEGMENT.intensity > 1) // every nth frame emit particles, stop emitting if zero + + PartSys->setWrapX(SEGMENT.check1); + PartSys->setBounceX(SEGMENT.check2); + PartSys->setBounceY(SEGMENT.check3); + PartSys->setWallHardness(min(SEGMENT.custom2, (uint8_t)200)); // wall hardness is 200 or more + if (SEGMENT.custom2>0) { - while (i < numParticles) // emit particles + PartSys->enableParticleCollisions(true, SEGMENT.custom2); // enable collisions and set particle collision hardness + } + else{ + PartSys->enableParticleCollisions(false); + } + + if (SEGMENT.call % (64 - (SEGMENT.intensity >> 2)) == 0 && SEGMENT.intensity > 1) // every nth frame emit particles, stop emitting if set to zero { - if (particles[i].ttl == 0) // find a dead particle + for (i = 0; i < PartSys->usedParticles; i++) // emit particles { - // emit particle at random position just over the top of the matrix - particles[i].ttl = 1500 - (SEGMENT.speed << 2) + random16(500); // if speed is higher, make them die sooner - - if (random16(5) == 0) // 16% of particles apper anywhere - particles[i].x = random16(cols * PS_P_RADIUS - 1); - else // rest is emitted at center half - particles[i].x = random16((cols >> 1) * PS_P_RADIUS + (cols >> 1) * PS_P_RADIUS); - - particles[i].y = random16(rows * PS_P_RADIUS) + rows * PS_P_RADIUS; // particles appear somewhere above the matrix, maximum is double the height - particles[i].vx = (((int16_t)random16(SEGMENT.custom1)) - (SEGMENT.custom1 >> 1)) >> 1; // side speed is +/- a quarter of the custom1 slider - particles[i].vy = -(SEGMENT.speed >> 1); - particles[i].hue = random16(); // set random color - particles[i].sat = ((SEGMENT.custom3) << 3) + 7; // set saturation - - break; //emit only one particle per round + if (PartSys->particles[i].ttl == 0) // find a dead particle + { + // emit particle at random position just over the top of the matrix + PartSys->particles[i].ttl = 1500 - (SEGMENT.speed << 2) + random16(500); // if speed is higher, make them die sooner + if (random16(5) == 0) // 16% of particles apper anywhere + PartSys->particles[i].x = random16(PartSys->maxX); + else // rest is emitted at center half + PartSys->particles[i].x = random16((PartSys->maxX) >> 1) + (PartSys->maxX) >> 2; + + PartSys->particles[i].y = random16(PartSys->maxY) + PartSys->maxY; // particles appear somewhere above the matrix, maximum is double the height + PartSys->particles[i].vx = (((int16_t)random16(SEGMENT.custom1)) - (SEGMENT.custom1 >> 1)) >> 1; // side speed is +/- a quarter of the custom1 slider + PartSys->particles[i].vy = -(SEGMENT.speed >> 1); // downward speed + PartSys->particles[i].hue = random16(); // set random color + PartSys->particles[i].sat = ((SEGMENT.custom3) << 3) + 7; // set saturation + break; // emit only one particle per round + } } - i++; } - } - uint8_t hardness = SEGMENT.custom2; // how hard the collisions are, 255 = full hard. - detectCollisions(particles, numParticles, hardness); +//!!! + /* + i=0; + // if (SEGMENT.call % 2 == 0) // every nth frame emit particles, stop emitting if set to zero + { + + + uint8_t emit = 5; + //i = random() % PartSys->usedParticles; + while (i < PartSys->usedParticles) // emit particles + { + if (PartSys->particles[i].ttl == 0) // find a dead particle + { + Serial.print(" i="); + Serial.println(i); + int32_t x = random16((PartSys->maxX << 1)) - (PartSys->maxX >> 1); + int32_t y = random16((PartSys->maxY << 1)) - (PartSys->maxY >> 1); + + PartSys->particles[i].x = x; + PartSys->particles[i].y = y; + //PartSys->particles[i].x = random16((PartSys->maxX) >> 1); + //PartSys->particles[i].y = random16((PartSys->maxY) >> 1); + PartSys->particles[i].vx = random16(10) - 5; + PartSys->particles[i].vy = random16(10) - 5; + PartSys->particles[i].ttl = 3000; + PartSys->particles[i].hue = random16(); // set random color + PartSys->particles[i].sat = 255; + if(emit-- == 0) + break; + } + i++; + } + } + */ + //!!! - // now move the particles - uint32_t frictioncoefficient = 1; - if (SEGMENT.speed < 50) + if (SEGMENT.speed < 50) // for low speeds, apply more friction { frictioncoefficient = 50 - SEGMENT.speed; } - - for (i = 0; i < numParticles; i++) - { - // apply 'air friction' to smooth things out, slows down all particles depending on their speed - applyFriction(&particles[i], frictioncoefficient); - Particle_Gravity_update(&particles[i], SEGMENT.check1, SEGMENT.check2, SEGMENT.check3, min(hardness,(uint8_t)200)); // surface hardness max is 200 - } + if (SEGMENT.call % 3 == 0) + PartSys->applyFriction(frictioncoefficient); SEGMENT.fill(BLACK); // clear the matrix - - // render the particles - ParticleSys_render(particles, numParticles, SEGMENT.check1, false); // custom3 slider is saturation, from 7 to 255, 7 is close enough to white (for snow for example) + PartSys->update(); // update and render return FRAMETIME; -} + } static const char _data_FX_MODE_PARTICLEFALL[] PROGMEM = "PS Ballpit@Speed,Intensity,Randomness,Hardness,Saturation,Cylinder,Walls,Ground;;!;012;pal=11,sx=100,ix=200,c1=31,c2=100,c3=28,o1=0,o2=0,o3=1"; -*/ + /* * Particle Waterfall * Uses palette for particle color, spray source at top emitting particles, many config options @@ -8660,7 +8703,7 @@ uint16_t mode_particlewaterfall(void) const uint32_t numParticles = 500; // maximum number of particles const uint8_t numSprays = 2; #endif - + uint8_t percycle = numSprays; // maximum number of particles emitted per cycle PSparticle *particles; @@ -8683,7 +8726,7 @@ uint16_t mode_particlewaterfall(void) { for (i = 0; i < numParticles; i++) { - particles[i].ttl = 0; + particles[i].ttl = 0; } for (i = 0; i < numSprays; i++) { @@ -8705,7 +8748,7 @@ uint16_t mode_particlewaterfall(void) } } - // change source emitting color + // change source emitting color for (i = 0; i < numSprays; i++) { spray[i].source.hue++; //change hue of spray source @@ -8751,7 +8794,7 @@ uint16_t mode_particlewaterfall(void) else{ hardness = 150; //set hardness (for ground bounce) to fixed value if not using collisions } - + // now move the particles for (i = 0; i < numParticles; i++) { @@ -8807,7 +8850,7 @@ uint16_t mode_particlebox(void) SEGMENT.aux0 = rand(); // position (either in noise or in sine function) for (i = 0; i < numParticles; i++) { - particles[i].ttl = 500; // all particles are alive (but not all are calculated/rendered) + particles[i].ttl = 500; // all particles are alive (but not all are calculated/rendered) particles[i].hue = i * 3; // full color range (goes over palette colors three times so it is also colorful when using fewer particles) particles[i].sat = 255; // set full saturation (lets palette choose the color) particles[i].x = map(i, 0, 255, 1, cols * PS_P_RADIUS); // distribute along x according to color @@ -8827,7 +8870,7 @@ uint16_t mode_particlebox(void) int32_t ygravity; SEGMENT.aux0 += (SEGMENT.speed >> 6) + 1; // update position in noise - + xgravity = ((int16_t)inoise8(SEGMENT.aux0) - 127); //TODO: inoise 16 would be faster ygravity = ((int16_t)inoise8(SEGMENT.aux0 + 10000) - 127); if (SEGMENT.check1) //sloshing, y force is alwys downwards @@ -8846,7 +8889,7 @@ uint16_t mode_particlebox(void) { particles[i].vx += xgravity; particles[i].vy += ygravity; - particles[i].ttl = 500; // particles never die + particles[i].ttl = 500; // particles never die } } } @@ -8862,7 +8905,7 @@ uint16_t mode_particlebox(void) if (SEGMENT.call % 8 == 0) { applyFriction(&particles[i], 1); - } + } Particle_Bounce_update(&particles[i], hardness); } @@ -8966,14 +9009,14 @@ 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"; +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) -*/ + * 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) +uint16_t mode_particleimpact(void) { if (SEGLEN == 1) return mode_static(); @@ -9021,7 +9064,7 @@ uint16_t mode_particleimpact(void) meteors[i].source.y = 10; meteors[i].source.ttl = random16(20 * i); // set initial delay for meteors meteors[i].source.vy = 10; // at positive speeds, no particles are emitted and if particle dies, it will be relaunched - meteors[i].source.sat = 255; //full saturation, color chosen by palette + meteors[i].source.sat = 255; //full saturation, color chosen by palette } } @@ -9073,7 +9116,7 @@ uint16_t mode_particleimpact(void) uint8_t hardness = SEGMENT.custom2; // how hard the collisions are, 255 = fully hard, no energy is lost in collision if (SEGMENT.check3) // use collisions if option is set - { + { detectCollisions(particles, numParticles, hardness); } // update particles @@ -9084,7 +9127,7 @@ uint16_t mode_particleimpact(void) Particle_Gravity_update(&particles[i], SEGMENT.check1, SEGMENT.check2, true, hardness); } } - + // update the meteors, set the speed state for (i = 0; i < numMeteors; i++) { @@ -9098,14 +9141,14 @@ 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.y = 5; // offset from ground so explosion happens not out of frame meteors[i].maxLife = 200; meteors[i].minLife = 50; #ifdef ESP8266 meteors[i].source.ttl = random16(255 - (SEGMENT.speed>>1)) + 10; // standby time til next launch (in frames at 42fps, max of 265 is about 6 seconds #else meteors[i].source.ttl = random16((255 - SEGMENT.speed)) + 10; // standby time til next launch (in frames at 42fps, max of 265 is about 6 seconds - #endif + #endif meteors[i].vx = 0; // emitting speed x meteors[i].vy = (SEGMENT.custom1 >> 2); // emitting speed y meteors[i].var = (SEGMENT.custom1 >> 1); // speed variation around vx,vy (+/- var/2) @@ -9118,11 +9161,11 @@ uint16_t mode_particleimpact(void) meteors[i].source.x = random16(Max_x); meteors[i].source.vy = -random16(30) - 30; // meteor downward speed meteors[i].source.vx = random16(30) - 15; - meteors[i].source.hue = random16(); // random color + meteors[i].source.hue = random16(); // random color meteors[i].source.ttl = 1000; // long life, will explode at bottom meteors[i].source.collide = false; // trail particles will not collide meteors[i].maxLife = 60; // spark particle life - meteors[i].minLife = 20; + meteors[i].minLife = 20; meteors[i].vx = 0; // emitting speed meteors[i].vy = -9; // emitting speed (down) meteors[i].var = 5; // speed variation around vx,vy (+/- var/2) @@ -9156,9 +9199,9 @@ uint16_t mode_particleattractor(void) #ifdef ESP8266 const uint32_t numParticles = 90; // maximum number of particles #else - const uint32_t numParticles = 300; // maximum number of particles + const uint32_t numParticles = 300; // maximum number of particles #endif - + PSparticle *particles; PSparticle *attractor; @@ -9196,7 +9239,7 @@ uint16_t mode_particleattractor(void) spray->source.hue = random16(); spray->source.sat = 255; //full saturation, color by palette spray->source.x = 0; - spray->source.y = 0; + spray->source.y = 0; spray->source.vx = random16(5) + 2; spray->source.vy = random16(4) + 1; spray->source.ttl = 100; @@ -9205,7 +9248,7 @@ uint16_t mode_particleattractor(void) spray->minLife = 30; spray->vx = 0; // emitting speed spray->vy = 0; // emitting speed - spray->var = 6; //emitting speed variation + spray->var = 6; //emitting speed variation } uint32_t displayparticles = map(SEGMENT.intensity, 0, 255, 1, numParticles) - 2; //TODO: the -2 is a botch fix, it crashes for some reason if going to max number of particles, is this a rounding error? @@ -9222,8 +9265,8 @@ uint16_t mode_particleattractor(void) spray->source.hue++; spray->source.ttl = 100; //spray never dies } - - uint8_t emit = 1; //number of particles emitted per frame + + uint8_t emit = 1; //number of particles emitted per frame Particle_Bounce_update(&spray->source, 255); //bounce the spray around SEGMENT.aux0++; //emitting angle @@ -9233,7 +9276,7 @@ uint16_t mode_particleattractor(void) { if (particles[i].ttl == 0 && emit--) // find a dead particle - { + { if(SEGMENT.call % 2 == 0) //alternate direction of emit Emitter_Angle_emit(spray, &particles[i], SEGMENT.aux0, SEGMENT.custom1 >> 4); else @@ -9244,7 +9287,7 @@ uint16_t mode_particleattractor(void) if (SEGMENT.custom3 > 0) { if (SEGMENT.call % (32 - SEGMENT.custom3) == 0) - { + { applyFriction(&particles[i], 4); } } @@ -9293,7 +9336,7 @@ uint16_t mode_particlespray(void) #else const uint32_t numParticles = 450; #endif - + const uint8_t numSprays = 1; uint8_t percycle = numSprays; // maximum number of particles emitted per cycle @@ -9367,9 +9410,9 @@ uint16_t mode_particlespray(void) if (SEGMENT.check3) // collisions enabled detectCollisions(particles, numParticles, hardness); - + for (i = 0; i < numParticles; i++) - { + { //particles[i].hue = min((uint16_t)220, particles[i].ttl); if (SEGMENT.check1) //use gravity Particle_Gravity_update(&particles[i], SEGMENT.check2, SEGMENT.check2 == 0, true, hardness); @@ -9439,10 +9482,10 @@ uint16_t mode_particleGEQ(void) } uint8_t *fftResult = (uint8_t *)um_data->u_data[2]; // 16 bins with FFT data, log mapped already, each band contains frequency amplitude 0-255 - + //map the bands into 16 positions on x axis, emit some particles according to frequency loudness //Idea: emit 20 particles at full loudness, can use a shift for that, for example shift by 4 or 5 - //in order to also emit particles for not so loud bands, get a bunch of particles based on frame counter and current loudness? + //in order to also emit particles for not so loud bands, get a bunch of particles based on frame counter and current loudness? //implement it simply first, then add complexity... need to check what looks good i = 0; uint32_t bin; //current bin @@ -9472,7 +9515,7 @@ uint16_t mode_particleGEQ(void) while (i < numParticles && emitparticles > 0) // emit particles if there are any left, low frequencies take priority { if (particles[i].ttl == 0) // find a dead particle - { + { //set particle properties particles[i].ttl = map(SEGMENT.intensity, 0,255, emitspeed>>1, emitspeed + random16(emitspeed)) ; // set particle alive, particle lifespan is in number of frames particles[i].x = xposition + random16(binwidth) - (binwidth>>1); //position randomly, deviating half a bin width @@ -9480,10 +9523,10 @@ uint16_t mode_particleGEQ(void) particles[i].vx = random16(SEGMENT.custom1>>1)-(SEGMENT.custom1>>2) ; //x-speed variation particles[i].vy = emitspeed; particles[i].hue = (bin<<4) + random16(17) - 8; // color from palette according to bin - //particles[i].sat = ((SEGMENT.custom3) << 3) + 7; // set saturation + //particles[i].sat = ((SEGMENT.custom3) << 3) + 7; // set saturation emitparticles--; } - i++; + i++; } } @@ -9492,9 +9535,9 @@ uint16_t mode_particleGEQ(void) // now move the particles for (i = 0; i < numParticles; i++) - { + { particles[i].vy -= (SEGMENT.custom3>>3); // apply stronger gravity - Particle_Gravity_update(&particles[i], SEGMENT.check1, SEGMENT.check2, SEGMENT.check3, hardness); + Particle_Gravity_update(&particles[i], SEGMENT.check1, SEGMENT.check2, SEGMENT.check3, hardness); } SEGMENT.fill(BLACK); // clear the matrix @@ -9596,7 +9639,7 @@ uint16_t mode_particlecenterGEQ(void) uint8_t *fftResult = (uint8_t *)um_data->u_data[2]; // 16 bins with FFT data, log mapped already, each band contains frequency amplitude 0-255 i = 0; - + uint32_t threshold = 300 - SEGMENT.intensity; i = 0; @@ -9650,7 +9693,7 @@ uint16_t mode_particlecenterGEQ(void) return FRAMETIME; -} +} static const char _data_FX_MODE_PARTICLECCIRCULARGEQ[] PROGMEM = "PS Center GEQ@Speed,Color Change,Particle Speed,Spray Count,Nozzle Size,Random Color, Direction;;!;012;pal=56,sx=0,ix=222,c1=190,c2=200,c3=0,o1=0,o2=0"; */ #endif // WLED_DISABLE_2D @@ -9894,13 +9937,16 @@ 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_PARTICLEFIRE, &mode_particlefire, _data_FX_MODE_PARTICLEFIRE); + addEffect(FX_MODE_PARTICLEFALL, &mode_particlefall, _data_FX_MODE_PARTICLEFALL); /* 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_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); addEffect(FX_MODE_PARTICLEWATERFALL, &mode_particlewaterfall, _data_FX_MODE_PARTICLEWATERFALL); addEffect(FX_MODE_PARTICLEIMPACT, &mode_particleimpact, _data_FX_MODE_PARTICLEIMPACT); diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index 3a307f4707..2fa2c1e00d 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -33,14 +33,13 @@ /* TODO: + -add SEGMENT.fill(BLACK); // clear the matrix into rendering function? -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 @@ -54,11 +53,11 @@ ParticleSystem::ParticleSystem(uint16_t width, uint16_t height, uint16_t numberofparticles, uint16_t numberofsources) { Serial.print("initializing PS... "); - + numSources = numberofsources; numParticles = numberofparticles; // set number of particles in the array usedParticles = numberofparticles; // use all particles by default particlesettings = {false, false, false, false, false, false, false, false}; // all settings off by default - setPSpointers(numberofsources); // set the particle and sources pointer (call this before accessing sprays or particles) + initPSpointers(); // set the particle and sources pointer (call this before accessing sprays or particles) setMatrixSize(width, height); setWallHardness(255); // set default wall hardness to max emitIndex = 0; @@ -72,37 +71,31 @@ ParticleSystem::ParticleSystem(uint16_t width, uint16_t height, uint16_t numbero //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 + // handle collisions (can push particles, must be done before updating particles or they can render out of bounds, causing a crash if using local buffer for speed) if (particlesettings.useCollisions) handleCollisions(); - //render the particles + //move all particles + for (int i = 0; i < usedParticles; i++) + { + particleMoveUpdate(particles[i], particlesettings); + } + ParticleSys_render(); } //update function for fire animation -void ParticleSystem::updateFire(uint8_t colormode) +void ParticleSystem::updateFire(uint32_t intensity, bool usepalette) { - - // update all fire particles - FireParticle_update(); - - // render the particles - renderParticleFire(colormode); + fireParticleupdate(); + renderParticleFire(intensity, usepalette); } -void ParticleSystem::setUsedParticles(uint32_t num) +void ParticleSystem::setUsedParticles(uint16_t num) { usedParticles = min(num, numParticles); //limit to max particles } @@ -121,8 +114,8 @@ 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 + maxX = x * PS_P_RADIUS - 1; // particle system boundary for movements + maxY = y * PS_P_RADIUS - 1; // this value is often needed by FX to calculate positions } void ParticleSystem::setWrapX(bool enable) @@ -158,8 +151,7 @@ void ParticleSystem::enableGravity(bool enable, uint8_t force) if (force > 0) gforce = force; else - particlesettings.useGravity = false; - + 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 @@ -168,13 +160,9 @@ void ParticleSystem::enableParticleCollisions(bool enable, uint8_t hardness) // collisionHardness = hardness + 1; } -int16_t ParticleSystem::getMaxParticles(void) -{ - return numParticles; -} // Spray emitter for particles used for flames (particle TTL depends on source TTL) -void ParticleSystem::FlameEmit(PSsource &emitter) +void ParticleSystem::flameEmit(PSsource &emitter) { for (uint32_t i = 0; i < usedParticles; i++) { @@ -195,7 +183,7 @@ void ParticleSystem::FlameEmit(PSsource &emitter) } // emit one particle with variation -void ParticleSystem::SprayEmit(PSsource &emitter) +void ParticleSystem::sprayEmit(PSsource &emitter) { for (uint32_t i = 0; i < usedParticles; i++) { @@ -232,72 +220,102 @@ void ParticleSystem::SprayEmit(PSsource &emitter) // Emits a particle at given angle and speed, angle is from 0-255 (=0-360deg), speed is also affected by emitter->var // angle = 0 means in x-direction -void ParticleSystem::AngleEmit(PSsource &emitter, uint8_t angle, uint32_t speed) +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 + //todo: go to 16 bits, rotating particles could use this, others maybe as well. from rotating spray FX, angleoffset is the angle in 16bit + //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) 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); + 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) +// uses passed settings to set bounce or wrap, if useGravity is set, it will never bounce at the top and killoutofbounds is not applied over the top +void ParticleSystem::particleMoveUpdate(PSparticle &part, PSsettings &options) { 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 + //Serial.print("x:"); + //Serial.print(part.x); + //Serial.print("y:"); + //Serial.print(part.y); + int32_t newX = part.x + (int16_t)part.vx; + int32_t newY = part.y + (int16_t)part.vy; + //Serial.print(" "); + //Serial.print(newY); + part.outofbounds = 0; // reset out of bounds (in case particle was created outside the matrix and is now moving into view) + + if (((newX < 0) || (newX > maxX))) // check if particle reached an edge { - 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 (options.bounceX) // particle was in view and now moved out -> bounce it + { + part.vx = -part.vx; // invert speed + part.vx = (part.vx * wallHardness) >> 8; // reduce speed as energy is lost on non-hard surface if (newX < 0) - newX = -newX; + newX = 0;//-newX; set to boarder (less acurate but at high speeds they will bounce mid frame if just flipped) else - newX = maxX - PS_P_RADIUS - newX; + newX = maxX; // maxX - (newX - (int32_t)maxX); } else if (options.wrapX) { - newX = wraparound(newX, maxX - PS_P_RADIUS); + newX = wraparound(newX, maxX); } - else + else if (((newX <= -PS_P_HALFRADIUS) || (newX > maxX + PS_P_HALFRADIUS))) // particle is leaving, set out of bounds if it has fully left + { part.outofbounds = 1; + if (options.killoutofbounds) + part.ttl = 0; + } } - - if (((newY < -PS_P_HALFRADIUS) || (newY > maxY))) // check if particle is out of bounds + if (((newY < 0) || (newY > maxY))) // check if particle reached an edge { - if (options.killoutofbounds) - part.ttl = 0; - else if (options.bounceY) // particle was in view and now moved out -> bounce it + 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; + if (newY > maxY) + { + if (options.useGravity) // do not bounce on top if using gravity (open container) + { + if(newY > maxY + PS_P_HALFRADIUS) + part.outofbounds = 1; // set out of bounds, kill out of bounds over the top does not apply if gravity is used (user can implement it in FX if needed) + } + else + { + part.vy = -part.vy; // invert speed + part.vy = (part.vy * wallHardness) >> 8; // reduce speed as energy is lost on non-hard surface + newY = maxY; //(int32_t)maxY - (newY - (int32_t)maxY); + } + } + else //bounce at bottom + { + part.vy = -part.vy; // invert speed + part.vy = (part.vy * wallHardness) >> 8; // reduce speed as energy is lost on non-hard surface + newY = 0;// -newY; + } } else if (options.wrapY) { - newY = wraparound(newY, maxY - PS_P_RADIUS); + newY = wraparound(newY, maxY); } - else + else if (((newY <= -PS_P_HALFRADIUS) || (newY > maxY + PS_P_HALFRADIUS))) // particle is leaving, set out of bounds if it has fully left + { part.outofbounds = 1; + if (options.killoutofbounds) + { + if (newY < 0) // if gravity is enabled, only kill particles below ground + part.ttl = 0; + else if (!options.useGravity) + part.ttl = 0; + } + } + } - part.x = newX; // set new position - part.y = newY; // set new position + part.x = (int16_t)newX; // set new position + part.y = (int16_t)newY; // set new position } } @@ -394,14 +412,23 @@ void ParticleSystem::applyGravity(PSparticle *part, uint32_t numarticles, uint8_ } // 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) +void ParticleSystem::applyFriction(PSparticle *part, uint8_t coefficient) +{ + int32_t friction = 256 - coefficient; + + part->vx = ((int16_t)part->vx * friction) >> 8; + part->vy = ((int16_t)part->vy * friction) >> 8; +} + +void ParticleSystem::applyFriction(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 + for (uint32_t i = 0; i < usedParticles; 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; + particles[i].vx = ((int16_t)particles[i].vx * friction) >> 8; + particles[i].vy = ((int16_t)particles[i].vy * friction) >> 8; } } @@ -489,62 +516,56 @@ void ParticleSystem::attract(PSparticle *particle, PSparticle *attractor, uint8_ // TODO: need to limit the max speed? } - // 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 +// warning: do not render out of bounds particles or system will crash! rendering does not check if particle is out of bounds void ParticleSystem::ParticleSys_render() { -#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 - - int32_t pixelCoordinates[4][2]; //physical coordinates of the four positions, x,y pairs + int32_t pixco[4][2]; //physical pixel coordinates of the four positions, x,y pairs //int32_t intensity[4]; CRGB baseRGB; + bool useLocalBuffer = true; + CRGB **colorbuffer; 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 - - // 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; - } - - // Allocate memory for each row - for (i = 0; i < maxXpixel; i++) + //CRGB colorbuffer[maxXpixel+1][maxYpixel+1] = {0}; //put buffer on stack (not a good idea, can cause crashes on large segments if other function run the stack into the heap) + //calloc(maxXpixel + 1, sizeof(CRGB *)) + // to create a 2d array on heap: + // TODO: put this in a function? fire render also uses this + // TODO: if pointer returns null, use classic render (or do not render this frame) + if (useLocalBuffer) { - colorbuffer[i] = (CRGB *)calloc(maxYpixel + 1, sizeof(CRGB)); - if (colorbuffer[i] == NULL) + // allocate memory for the 2D array in one contiguous block + colorbuffer = (CRGB **)malloc((maxXpixel + 1) * sizeof(CRGB *) + (maxXpixel + 1) * (maxYpixel + 1) * sizeof(CRGB)); + if (colorbuffer == NULL) { - Serial.println("Memory allocation failed222"); - return; + DEBUG_PRINT(F("PS renderbuffer memory alloc failed")); + useLocalBuffer = false; + //return; } - }*/ - -//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. - + else{ + // assign pointers of 2D array + CRGB *start = (CRGB *)(colorbuffer + (maxXpixel + 1)); + for (i = 0; i < maxXpixel + 1; i++) + { + colorbuffer[i] = start + i * (maxYpixel + 1); + } + memset(start, 0, (maxXpixel + 1) * (maxYpixel + 1) * sizeof(CRGB)); // set all values to zero + } + } - // go over particles and update matrix cells on the way + // go over particles and render them to the buffer for (i = 0; i < usedParticles; i++) - { - /* - if (particles[i].ttl == 0 || particles[i].outofbounds) + { + if (particles[i].ttl == 0) { + //Serial.print("d"); continue; - }*/ - if (particles[i].ttl == 0) + } + if(particles[i].outofbounds) { + //Serial.print("o"); continue; } // generate RGB values for particle @@ -559,51 +580,79 @@ TODO: using a local buffer crashed immediately, find out why. 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++) + renderParticle(&particles[i], brightness, intensity, pixco); + + //debug: check coordinates if out of buffer boundaries print out some info + for(uint32_t d; d<4; d++) { - if(colorbuffer[x][y]>0) + if (pixco[d][0] < 0 || pixco[d][0] > maxXpixel) + { + intensity[d] = -1; //do not render + Serial.print("uncought out of bounds: x="); + Serial.print(pixco[d][0]); + Serial.print("particle x="); + Serial.print(particles[i].x); + Serial.print(" y="); + Serial.println(particles[i].y); + useLocalBuffer = false; + free(colorbuffer); // free buffer memory + } + if (pixco[d][1] < 0 || pixco[d][1] > maxYpixel) { - SEGMENT.setPixelColorXY(x,y,colorbuffer[x][y]); + intensity[d] = -1; // do not render + Serial.print("uncought out of bounds: y="); + Serial.print(pixco[d][1]); + Serial.print("particle x="); + Serial.print(particles[i].x); + Serial.print(" y="); + Serial.println(particles[i].y); + useLocalBuffer = false; + free(colorbuffer); // free buffer memory } } + if (useLocalBuffer) + { + if (intensity[0] > 0) + colorbuffer[pixco[0][0]][pixco[0][1]] = fast_color_add(colorbuffer[pixco[0][0]][pixco[0][1]], baseRGB, intensity[0]); // bottom left + if (intensity[1] > 0) + colorbuffer[pixco[1][0]][pixco[1][1]] = fast_color_add(colorbuffer[pixco[1][0]][pixco[1][1]], baseRGB, intensity[1]); // bottom right + if (intensity[2] > 0) + colorbuffer[pixco[2][0]][pixco[2][1]] = fast_color_add(colorbuffer[pixco[2][0]][pixco[2][1]], baseRGB, intensity[2]); // top right + if (intensity[3] > 0) + colorbuffer[pixco[3][0]][pixco[3][1]] = fast_color_add(colorbuffer[pixco[3][0]][pixco[3][1]], baseRGB, intensity[3]); // top left + } + else + { + if (intensity[0] > 0) + SEGMENT.addPixelColorXY(pixco[0][0], maxYpixel - pixco[0][1], baseRGB.scale8((uint8_t)intensity[0])); // bottom left + if (intensity[1] > 0) + SEGMENT.addPixelColorXY(pixco[1][0], maxYpixel - pixco[1][1], baseRGB.scale8((uint8_t)intensity[1])); // bottom right + if (intensity[2] > 0) + SEGMENT.addPixelColorXY(pixco[2][0], maxYpixel - pixco[2][1], baseRGB.scale8((uint8_t)intensity[2])); // top right + if (intensity[3] > 0) + SEGMENT.addPixelColorXY(pixco[3][0], maxYpixel - pixco[3][1], baseRGB.scale8((uint8_t)intensity[3])); // 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), could probably be extended to 3x3 + // SEGMENT.addPixelColorXY(pixco[1][0] + 1, maxYpixel - pixco[1][1], baseRGB.scale8((uint8_t)((brightness>>1) - intensity[0])), fastcoloradd); + // SEGMENT.addPixelColorXY(pixco[2][0] + 1, maxYpixel - pixco[2][1], baseRGB.scale8((uint8_t)((brightness>>1) -intensity[3])), fastcoloradd); + } } - - // Free memory for each row - for (int i = 0; i <= maxXpixel; i++) + if (useLocalBuffer) { - free(colorbuffer[i]); + uint32_t yflipped; + for (int y = 0; y <= maxYpixel; y++) + { + yflipped = maxYpixel - y; + for (int x = 0; x <= maxXpixel; x++) + { + SEGMENT.setPixelColorXY(x, yflipped, colorbuffer[x][y]); + } + } + free(colorbuffer); // free buffer memory } - - // 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) - 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 @@ -664,7 +713,6 @@ void ParticleSystem::renderParticle(PSparticle* particle, uint32_t brightess, in pixelvalues[2] = pixelvalues[3] = -1; } - // 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; @@ -681,31 +729,72 @@ void ParticleSystem::renderParticle(PSparticle* particle, uint32_t brightess, in 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("x:"); Serial.print(particle->x); - Serial.print(" "); - Serial.print(xoffset); - Serial.print(" dx"); - Serial.print(dx); - Serial.print(" "); + Serial.print(" y:"); + Serial.print(particle->y); + //Serial.print(" xo"); + //Serial.print(xoffset); + //Serial.print(" dx"); + //Serial.print(dx); + //Serial.print(" "); for(uint8_t t = 0; t<4; t++) { - Serial.print("x"); + Serial.print(" v"); + Serial.print(pixelvalues[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(" "); - */ +*/ + // debug: check coordinates if out of buffer boundaries print out some info + for (uint32_t d; d < 4; d++) + { + if (pixelpositions[d][0] < 0 || pixelpositions[d][0] > maxXpixel) + { + //Serial.print("<"); + if (pixelvalues[d] >= 0) + { + Serial.print("uncought out of bounds: x:"); + Serial.print(pixelpositions[d][0]); + Serial.print(" y:"); + Serial.print(pixelpositions[d][1]); + Serial.print("particle x="); + Serial.print(particle->x); + Serial.print(" y="); + Serial.println(particle->y); + pixelvalues[d] = -1; // do not render + } + } + if (pixelpositions[d][1] < 0 || pixelpositions[d][1] > maxYpixel) + { + //Serial.print("^"); + if (pixelvalues[d] >= 0) + { + Serial.print("uncought out of bounds: x:"); + Serial.print(pixelpositions[d][0]); + Serial.print(" y:"); + Serial.print(pixelpositions[d][1]); + Serial.print("particle x="); + Serial.print(particle->x); + Serial.print(" y="); + Serial.println(particle->y); + pixelvalues[d] = -1; // do not render + } + } + } } // 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 ParticleSystem::FireParticle_update() +void ParticleSystem::fireParticleupdate() { - //TODO: cleanup this function? + //TODO: cleanup this function? check if normal move is much slower, change move function to check y first and check again + //todo: kill out of bounds funktioniert nicht? uint32_t i = 0; for (i = 0; i < usedParticles; i++) @@ -714,32 +803,26 @@ void ParticleSystem::FireParticle_update() { // age particles[i].ttl--; - // apply velocity particles[i].x = particles[i].x + (int32_t)particles[i].vx; particles[i].y = particles[i].y + (int32_t)particles[i].vy + (particles[i].ttl >> 4); // younger particles move faster upward as they are hotter, used for fire - particles[i].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 (particles[i].y < -PS_P_HALFRADIUS) - { + if (particles[i].y < -PS_P_HALFRADIUS) particles[i].outofbounds = 1; - } - else if (particles[i].y > maxY) // particle moved out on the top - { + else if (particles[i].y > maxY + PS_P_HALFRADIUS) // particle moved out at the top particles[i].ttl = 0; - } else // particle is in frame in y direction, also check x direction now { - if ((particles[i].x < -PS_P_HALFRADIUS) || (particles[i].x > maxX)) + if ((particles[i].x < 0) || (particles[i].x > maxX)) { if (particlesettings.wrapX) { particles[i].x = wraparound(particles[i].x, maxX); } - else + else if ((particles[i].x < -PS_P_HALFRADIUS) || (particles[i].x > maxX + PS_P_HALFRADIUS)) //if fully out of view { particles[i].ttl = 0; } @@ -751,106 +834,169 @@ void ParticleSystem::FireParticle_update() // 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 -// note: colormode 0-5 are native, heat based color modes, set colormode to 255 to use palette -void ParticleSystem::renderParticleFire(uint8_t colormode) +// without using a plette native, heat based color mode applied +// intensity from 0-255 is mapped such that higher values result in more intense flames +void ParticleSystem::renderParticleFire(uint32_t intensity, bool usepalette) { -//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]; + int32_t pixco[4][2]; // physical coordinates of the four positions, x,y pairs uint32_t flameheat; //depends on particle.ttl uint32_t i; - + uint32_t debug = 0; + //CRGB colorbuffer[(maxXpixel+1)][(maxYpixel+1)] = {0}; + + // Allocate memory for the array of pointers to rows + CRGB **colorbuffer = (CRGB **)calloc(maxXpixel + 1, sizeof(CRGB *)); + if (colorbuffer == NULL) + { + Serial.println("Memory allocation failed111"); + return; + } + + // Allocate memory for each row + for (i = 0; i <= maxXpixel; i++) + { + colorbuffer[i] = (CRGB *)calloc(maxYpixel + 1, sizeof(CRGB)); + if (colorbuffer[i] == NULL) + { + Serial.println("Memory allocation failed222"); + return; + } + } + // go over particles and update matrix cells on the way // note: some pixels (the x+1 ones) can be out of bounds, it is probably faster than to check that for every pixel as this only happens on the right border (and nothing bad happens as this is checked down the road) for (i = 0; i < usedParticles; i++) { if (particles[i].outofbounds) //lots of fire particles are out of bounds, check first + { + //Serial.print("o"); continue; - + } if (particles[i].ttl == 0) + { + //Serial.print("d"); continue; + } + if (usepalette) + { + // generate RGB values for particle + uint32_t brightness = (uint32_t)particles[i].ttl * (1 + (intensity >> 4)) + (intensity >> 2); + brightness > 255 ? 255 : brightness; // faster then using min() + CRGB baseRGB = ColorFromPalette(SEGPALETTE, brightness, 255, LINEARBLEND); + + 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, pixco); + + if (intensity[0] > 0) + colorbuffer[pixco[0][0]][pixco[0][1]] = fast_color_add(colorbuffer[pixco[0][0]][pixco[0][1]], baseRGB, intensity[0]); // bottom left + // SEGMENT.addPixelColorXY(pixco[0][0], maxYpixel - pixco[0][1], baseRGB.scale8((uint8_t)intensity[0])); + if (intensity[1] > 0) + colorbuffer[pixco[1][0]][pixco[1][1]] = fast_color_add(colorbuffer[pixco[1][0]][pixco[1][1]], baseRGB, intensity[1]); + // SEGMENT.addPixelColorXY(pixco[1][0], maxYpixel - pixco[1][1], baseRGB.scale8((uint8_t)intensity[1])); // bottom right + if (intensity[2] > 0) + colorbuffer[pixco[2][0]][pixco[2][1]] = fast_color_add(colorbuffer[pixco[2][0]][pixco[2][1]], baseRGB, intensity[2]); + // SEGMENT.addPixelColorXY(pixco[2][0], maxYpixel - pixco[2][1], baseRGB.scale8((uint8_t)intensity[2])); // top right + if (intensity[3] > 0) + colorbuffer[pixco[3][0]][pixco[3][1]] = fast_color_add(colorbuffer[pixco[3][0]][pixco[3][1]], baseRGB, intensity[3]); + // SEGMENT.addPixelColorXY(pixco[3][0], maxYpixel - pixco[3][1], baseRGB.scale8((uint8_t)intensity[3])); // top left + } + else{ + flameheat = particles[i].ttl; + int32_t pixelheat[4] = {0}; // note: passed array needs to be set to 0 or checking in rendering function does not work (if values persist), this is faster then setting it to 0 there + renderParticle(&particles[i], flameheat, pixelheat, pixco); //render heat to physical pixels + + // TODO: add one more pixel closer to the particle, so it is 3 pixels wide + if (pixelheat[0] >= 0) + PartMatrix_addHeat(pixelheat[0], &colorbuffer[pixco[0][0]][pixco[0][1]].r, intensity); + //PartMatrix_addHeat(pixco[0][0], pixco[0][1], pixelheat[0], intensity); + if (pixelheat[1] >= 0) + PartMatrix_addHeat(pixelheat[1], &colorbuffer[pixco[1][0]][pixco[1][1]].r, intensity); + //PartMatrix_addHeat(pixco[1][0], pixco[1][1], pixelheat[1], intensity); + if (pixelheat[2] >= 0) + PartMatrix_addHeat(pixelheat[2], &colorbuffer[pixco[2][0]][pixco[2][1]].r, intensity); + //PartMatrix_addHeat(pixco[2][0], pixco[2][1], pixelheat[2], intensity); + if (pixelheat[3] >= 0) + PartMatrix_addHeat(pixelheat[3], &colorbuffer[pixco[3][0]][pixco[3][1]].r, intensity); + //PartMatrix_addHeat(pixco[3][0], pixco[3][1], pixelheat[3], intensity); + + // TODO: add heat to a third pixel. need to konw dx and dy, the heatvalue is (flameheat - pixelheat) vom pixel das weiter weg ist vom partikelzentrum + // 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 + //debug++; //!!! + } + } +// Serial.println(" "); +// Serial.print("rp:"); +// Serial.println(debug); + + for (int x = 0; x <= maxXpixel;x++) + { + for (int y = 0; y <= maxYpixel; y++) + { + SEGMENT.setPixelColorXY(x, maxYpixel - y, colorbuffer[x][y]); + } + } - flameheat = particles[i].ttl; - renderParticle(&particles[i], flameheat, pixelheat, pixelCoordinates); - - - //TODO: add one more pixel closer to the particle, so it is 3 pixels wide - - 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 + // Free memory for each row + for (int i = 0; i <= maxXpixel; i++) + { + free(colorbuffer[i]); } + + // Free memory for the array of pointers to rows + free(colorbuffer); } // adds 'heat' to red color channel, if it overflows, add it to next color channel -// 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) +void ParticleSystem::PartMatrix_addHeat(int32_t heat, uint8_t *currentcolor, uint32_t intensity) { - 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 slow hardware multiplication, just use shift (also less particles, need more heat) + heat = heat * (1 + (intensity >> 4)) + (intensity >> 3); // ESP8266 TODO: does this still need different value like in the old version? currently set to same #else - heat = heat * 10; //TODO: need to play with this some more to see if it makes fire better or worse + heat = heat * (1 + (intensity >> 4)) + (intensity >> 3); // todo: make this a variable to pass #endif - 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) + uint32_t i; + //go over the three color channels and fill them with heat, if one overflows, add heat to the next, start with red + for (i = 0; i < 3; i++) { - if (currentcolor[coloridx] < 255) //current color is not yet full + if (currentcolor[i] < 255) //current color is not yet full { - if (heat > 255) + if (heat > 255) + { + heat -= 255 - currentcolor[i]; + currentcolor[i] = 255; + } + else{ + int32_t leftover = heat - (255 - currentcolor[i]); + if(leftover <= 0) //all heat is being used up for this color { - heat -= 255 - currentcolor[coloridx]; - currentcolor[coloridx] = 255; + currentcolor[i] += heat; + break; } else{ - int32_t leftover = heat - currentcolor[coloridx]; - if(leftover <= 0) + currentcolor[i] = 255; + if(heat > leftover) { - currentcolor[coloridx] += heat; - break; - } - else{ - currentcolor[coloridx] = 255; - if(heat > leftover) - { - heat -= leftover; - } - else - break; + heat -= leftover; } - } - } - coloridx += increment; - if (coloridx > 2) - coloridx -= 3; // faster than i = i % 3 and is allowed since increment is never more than 2 - } - - if (i == 2) // last color was reached limit the color value (in normal mode, this is blue) so it does not go full white - { - currentcolor[coloridx] = currentcolor[coloridx] > 60 ? 60 : currentcolor[coloridx]; //faster than min() + else + break; + } + } + } } - SEGMENT.setPixelColorXY(col, maxYpixel - row, currentcolor); + if (i == 2) // blue channel was reached, limit the color value so it does not go full white + currentcolor[i] = currentcolor[i] > 60 ? 60 : currentcolor[i]; //faster than min() + + //SEGMENT.setPixelColorXY(col, maxYpixel - row, currentcolor); } // detect collisions in an array of particles and handle them @@ -950,25 +1096,24 @@ void ParticleSystem::collideParticles(PSparticle *particle1, PSparticle *particl particle1->vy += yimpulse; particle2->vx -= ximpulse; particle2->vy -= yimpulse; - /* + //TODO: this is removed for now as it does not seem to do much and does not help with piling. if soft, much energy is lost anyway at a collision, so they are automatically sticky //also second version using multiplication is slower on ESP8266 than the if's - if (hardness < 50) // if particles are soft, they become 'sticky' i.e. they are slowed down at collisions - { - + if (collisionHardness < 220) // if particles are soft, they become 'sticky' i.e. they are slowed down at collisions + { //particle1->vx = (particle1->vx < 2 && particle1->vx > -2) ? 0 : particle1->vx; //particle1->vy = (particle1->vy < 2 && particle1->vy > -2) ? 0 : particle1->vy; //particle2->vx = (particle2->vx < 2 && particle2->vx > -2) ? 0 : particle2->vx; //particle2->vy = (particle2->vy < 2 && particle2->vy > -2) ? 0 : particle2->vy; - const uint32_t coeff = 100; + const uint32_t coeff = collisionHardness + 20; particle1->vx = ((int32_t)particle1->vx * coeff) >> 8; particle1->vy = ((int32_t)particle1->vy * coeff) >> 8; particle2->vx = ((int32_t)particle2->vx * coeff) >> 8; particle2->vy = ((int32_t)particle2->vy * coeff) >> 8; - }*/ + } } // particles have volume, push particles apart if they are too close by moving each particle by a fixed amount away from the other particle @@ -1050,11 +1195,19 @@ int32_t ParticleSystem::calcForce_dV(int8_t force, uint8_t* counter) // 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) +void ParticleSystem::initPSpointers() { - particles = reinterpret_cast(SEGMENT.data + sizeof(ParticleSystem)); // pointer to particle array + Serial.print("this "); + Serial.println((uintptr_t)this); + particles = reinterpret_cast(this + 1); // pointer to particle array sizeof(ParticleSystem) sources = reinterpret_cast(particles + numParticles); // pointer to source(s) - PSdataEnd = reinterpret_cast(sources + numsources); // pointer to first available byte after the PS + PSdataEnd = reinterpret_cast(sources + numSources); // pointer to first available byte after the PS + Serial.print("particles "); + Serial.println((uintptr_t)particles); + Serial.print("sources "); + Serial.println((uintptr_t)sources); + Serial.print("end "); + Serial.println((uintptr_t)PSdataEnd); } //non class functions to use for initialization @@ -1064,17 +1217,19 @@ 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 + uint32_t numberofParticles = (cols * rows * 3)>>2 ; // 0.75 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) + uint32_t numberofParticles = (cols * rows); // 1 particle per pixel #else - uint32_t numberofParticles = (cols * rows * 7) / 4; // 1.75 particles per pixel + uint32_t numberofParticles = (cols * rows * 3) / 2; // 1.5 particles per pixel (for example 768 particles on 32x16) #endif Serial.print("segsize "); Serial.print(cols); + Serial.print(" "); Serial.print(" "); - Serial.println(rows); + Serial.print("particles: "); + Serial.println(numberofParticles); // TODO: ist das genug für fire auf 32x16? evtl auf 2 gehen? oder das dynamisch machen, als parameter? return numberofParticles; } @@ -1086,6 +1241,9 @@ bool allocateParticleSystemMemory(uint16_t numparticles, uint16_t numsources, ui requiredmemory += sizeof(PSparticle) * numparticles; requiredmemory += sizeof(PSsource) * numsources; requiredmemory += additionalbytes; + Serial.print("allocatin: "); + Serial.print(requiredmemory); + Serial.print("Bytes"); return(SEGMENT.allocateData(requiredmemory)); } @@ -1098,14 +1256,43 @@ bool initParticleSystem(ParticleSystem *&PartSys, uint16_t numsources) DEBUG_PRINT(F("PS init failed: memory depleted")); return false; } - Serial.println("memory allocated"); + Serial.print("segment data ptr"); + Serial.println((uintptr_t)(SEGMENT.data)); 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 + PartSys = new (SEGMENT.data) ParticleSystem(cols, rows, numparticles, numsources); // particle system constructor TODO: why does VS studio thinkt this is bad? Serial.print("PS pointer at "); Serial.println((uintptr_t)PartSys); return true; } - +// fastled color adding is very inaccurate in color preservation +// a better color add function is implemented in colors.cpp but it uses 32bit RGBW. so colors need to be shifted and then shifted back by that function, which is slow +// this is a fast version for RGB (no white channel, PS does not handle white) and with native CRGB including scaling of second color (fastled scale8 can be made faster using native 32bit on ESP) +CRGB fast_color_add(CRGB c1, CRGB c2, uint32_t scale) +{ + CRGB result; + scale++; //add one to scale so 255 will not scale when shifting + uint32_t r = c1.r + ((c2.r * (scale)) >> 8); + uint32_t g = c1.g + ((c2.g * (scale)) >> 8); + uint32_t b = c1.b + ((c2.b * (scale)) >> 8); + uint32_t max = r; + if (g > max) + max = g; + if (b > max) + max = b; + if (max < 256) + { + result.r = r; + result.g = g; + result.b = b; + } + else + { + result.r = (r * 255) / max; + result.g = (g * 255) / max; + result.b = (b * 255) / max; + } + return result; +} diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index f86b13a074..6dc5b4f2c3 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -27,6 +27,7 @@ #include +#include "FastLED.h" //particle dimensions (subpixel division) #define PS_P_RADIUS 64 //subpixel size, each pixel is divided by this for particle movement, if this value is changed, also change the shift defines (next two lines) @@ -98,27 +99,27 @@ class ParticleSystem 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 + void updateFire(uint32_t intensity, bool usepalette); // update function for fire // particle emitters - void FlameEmit(PSsource &emitter); - void SprayEmit(PSsource &emitter); - void AngleEmit(PSsource& emitter, uint8_t angle, uint32_t speed); + 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(); + void particleMoveUpdate(PSparticle &part, PSsettings &options); //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 applyFriction(PSparticle *part, uint8_t coefficient); // apply friction to specific particle + void applyFriction(uint8_t coefficient); // apply friction to all used particles void attract(PSparticle *particle, PSparticle *attractor, uint8_t *counter, uint8_t strength, bool swallow); //set options - void setUsedParticles(uint32_t num); + void setUsedParticles(uint16_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); @@ -129,39 +130,35 @@ class ParticleSystem 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) + PSsource *sources; // pointer to 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 + uint16_t maxX, maxY; //particle system size i.e. width-1 / height-1 in subpixels + uint32_t maxXpixel, maxYpixel; // last physical pixel that can be drawn to (FX can read this to read segment size if required) + uint8_t numSources; //number of sources + uint16_t numParticles; // number of particles available in this system + uint16_t usedParticles; // number of particles used in animation (can be smaller then numParticles) 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 ParticleSys_render(); + void renderParticleFire(uint32_t intensity, bool usepalette); + void PartMatrix_addHeat(int32_t heat, uint8_t *currentcolor, uint32_t intensity); 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); + void fireParticleupdate(); //utility functions + void initPSpointers(); // call this after allocating the memory to initialize the pointers int32_t wraparound(int32_t w, int32_t maxvalue); int32_t calcForce_dV(int8_t force, uint8_t *counter); // 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 + 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 @@ -174,3 +171,5 @@ class ParticleSystem bool initParticleSystem(ParticleSystem *&PartSys, uint16_t numsources); uint32_t calculateNumberOfParticles(); bool allocateParticleSystemMemory(uint16_t numparticles, uint16_t numsources, uint16_t additionalbytes); +//color add function +CRGB fast_color_add(CRGB c1, CRGB c2, uint32_t scale); // fast and accurate color adding with scaling (scales c2 before adding) \ No newline at end of file From 136f40ff62fb7f0bf4577cbc769f90a741b17c00 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sun, 31 Mar 2024 17:42:48 +0200 Subject: [PATCH 053/219] More Bugfixes, more converted FX --- wled00/FX.cpp | 502 ++++++++++++++++-------------------- wled00/FX_fcn.cpp | 2 +- wled00/FXparticleSystem.cpp | 224 ++++++++-------- wled00/FXparticleSystem.h | 27 +- 4 files changed, 343 insertions(+), 412 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index ce4dd38823..f06ead8bde 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -7892,7 +7892,7 @@ uint16_t mode_particlerotatingspray(void) { if (SEGLEN == 1) return mode_static(); - const uint8_t numSprays = 8; // maximum number of sprays + uint8_t numSprays; // maximum number of sprays ParticleSystem *PartSys = NULL; uint32_t i = 0; uint32_t j = 0; @@ -7900,7 +7900,7 @@ uint16_t mode_particlerotatingspray(void) if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. { - if (!initParticleSystem(PartSys, numSprays)) + if (!initParticleSystem(PartSys, 0)) return mode_static(); // allocation failed; //allocation failed Serial.print("PS pointer "); @@ -7913,7 +7913,8 @@ uint16_t mode_particlerotatingspray(void) SEGMENT.aux0 = 0; // starting angle SEGMENT.aux1 = 0x01; // check flags - // TODO: use SEGMENT.step for smooth direction change + // TODO: use SEGMENT.step for smooth direction change + numSprays = min(PartSys->numSources, (uint8_t) 8); for (i = 0; i < numSprays; i++) { PartSys->sources[i].source.sat = 255; // set saturation @@ -7941,7 +7942,9 @@ uint16_t mode_particlerotatingspray(void) } else PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS -/* + + numSprays = min(PartSys->numSources, (uint8_t)8); + /* if (SEGMENT.call < 3) { Serial.print("segment data ptr in candyFX"); @@ -7955,7 +7958,7 @@ uint16_t mode_particlerotatingspray(void) Serial.println("ERROR: paticle system not found, nullpointer"); return mode_static(); // something went wrong, no data! (TODO: ask how to handle this so it always works) } -//!!! + //!!! for (i = 0; i < numSprays; i++) { PartSys->sources[i].source.sat = 255; // set saturation @@ -7976,7 +7979,7 @@ uint16_t mode_particlerotatingspray(void) PartSys->sources[i].source.hue = coloroffset * i; } } -//!!! + //!!! if (SEGMENT.check1 != (SEGMENT.aux1 & 0x01)) //state change { if (SEGMENT.check1) @@ -8002,7 +8005,7 @@ uint16_t mode_particlerotatingspray(void) //set rotation direction and speed int32_t rotationspeed = SEGMENT.speed << 2; bool direction = SEGMENT.check2; - + if (SEGMENT.custom2 > 0) // automatic direction change enabled { uint16_t changeinterval = (265 - SEGMENT.custom2); @@ -8051,20 +8054,20 @@ uint16_t mode_particlerotatingspray(void) PartSys->SprayEmit(PartSys->sources[3]); }*/ // calculate angle offset for an even distribution - uint16_t angleoffset = 0xFFFF / spraycount; + uint16_t angleoffset = 0xFFFF / spraycount; - for (j = 0; j < spraycount; j++) - { - // calculate the x and y speed using aux0 as the 16bit angle. returned value by sin16/cos16 is 16bit, shifting it by 8 bits results in +/-128, divide that by custom1 slider value - PartSys->sources[j].vx = (cos16(SEGMENT.aux0 + angleoffset * j) >> 8) / ((263 - SEGMENT.intensity) >> 3); // update spray angle (rotate all sprays with angle offset) - PartSys->sources[j].vy = (sin16(SEGMENT.aux0 + angleoffset * j) >> 8) / ((263 - SEGMENT.intensity) >> 3); // update spray angle (rotate all sprays with angle offset) - PartSys->sources[j].var = (SEGMENT.custom3 >> 1); // emiting variation = nozzle size (custom 3 goes from 0-32) + for (j = 0; j < spraycount; j++) + { + // calculate the x and y speed using aux0 as the 16bit angle. returned value by sin16/cos16 is 16bit, shifting it by 8 bits results in +/-128, divide that by custom1 slider value + PartSys->sources[j].vx = (cos16(SEGMENT.aux0 + angleoffset * j) >> 8) / ((263 - SEGMENT.intensity) >> 3); // update spray angle (rotate all sprays with angle offset) + PartSys->sources[j].vy = (sin16(SEGMENT.aux0 + angleoffset * j) >> 8) / ((263 - SEGMENT.intensity) >> 3); // update spray angle (rotate all sprays with angle offset) + PartSys->sources[j].var = (SEGMENT.custom3 >> 1); // emiting variation = nozzle size (custom 3 goes from 0-32) } -#ifdef ESP8266 + #ifdef ESP8266 if (SEGMENT.call & 0x01) // every other frame, do not emit to save particles percycle = 0; -#endif + #endif //TODO: limit the emit amount by particle speed. should not emit more than one for every speed of like 20 or so, it looks weird on initialisation also make it depnd on angle speed, emit no more than once every few degrees -> less overlap (need good finetuning) @@ -8122,7 +8125,7 @@ uint16_t mode_particlefireworks(void) { for (i = 0; i < numParticles; i++) { - particles[i].ttl = 0; + PartSys->particles[i].ttl = 0; } for (j = 0; j < numRockets; j++) { @@ -8183,13 +8186,13 @@ uint16_t mode_particlefireworks(void) } for (i = 0; i < numParticles; i++) { - if (particles[i].ttl == 0) + if (PartSys->particles[i].ttl == 0) { // particle is dead if (emitparticles > 0) { if (circularexplosion) // do circle emit { - Emitter_Angle_emit(&rockets[j], &particles[i], angle, currentspeed); + Emitter_Angle_emit(&rockets[j], &PartSys->particles[i], angle, currentspeed); emitparticles--; // set angle for next particle angle += angleincrement; @@ -8209,7 +8212,7 @@ uint16_t mode_particlefireworks(void) } else { - Emitter_Fountain_emit(&rockets[j], &particles[i]); + Emitter_Fountain_emit(&rockets[j], &PartSys->particles[i]); emitparticles--; if ((j % 3) == 0) { @@ -8228,9 +8231,9 @@ uint16_t mode_particlefireworks(void) // update particles for (i = 0; i < numParticles; i++) { - if (particles[i].ttl) + if (PartSys->particles[i].ttl) { - Particle_Gravity_update(&particles[i], SEGMENT.check1, SEGMENT.check2, SEGMENT.check3, SEGMENT.custom2); + Particle_Gravity_update(&PartSys->particles[i], SEGMENT.check1, SEGMENT.check2, SEGMENT.check3, SEGMENT.custom2); } } // update the rockets, set the speed state @@ -8278,8 +8281,9 @@ uint16_t mode_particlefireworks(void) //TODO: after implementing gravity function, add slider custom3 to set gravity force static const char _data_FX_MODE_PARTICLEFIREWORKS[] PROGMEM = "PS Fireworks@Launches,Explosion Size,Fuse,Bounce,,Cylinder,Walls,Ground;;!;012;pal=11,sx=100,ix=50,c1=64,c2=128,c3=10,o1=0,o2=0,o3=0"; */ + /* - * Particle Volcano (gravity spray) + * Particle Volcano * Particles are sprayed from below, spray moves back and forth if option is set * Uses palette for particle color * by DedeHai (Damian Schneider) @@ -8325,7 +8329,7 @@ uint16_t mode_particlevolcano(void) { for (i = 0; i < numParticles; i++) { - particles[i].ttl = 0; + PartSys->particles[i].ttl = 0; } for (i = 0; i < numSprays; i++) { @@ -8376,10 +8380,10 @@ uint16_t mode_particlevolcano(void) j = 0; for (i = 0; i < numParticles; i++) { - if (particles[i].ttl == 0) // find a dead particle + if (PartSys->particles[i].ttl == 0) // find a dead particle { // spray[j].source.hue = random16(); //set random color for each particle (using palette) - Emitter_Fountain_emit(&spray[j], &particles[i]); + Emitter_Fountain_emit(&spray[j], &PartSys->particles[i]); j = (j + 1) % numSprays; if (percycle-- == 0) { @@ -8401,9 +8405,9 @@ uint16_t mode_particlevolcano(void) { //set color according to ttl ('color by age') if (SEGMENT.check1) - particles[i].hue = min((uint16_t)220, particles[i].ttl); + PartSys->particles[i].hue = min((uint16_t)220, PartSys->particles[i].ttl); - Particle_Gravity_update(&particles[i], false, SEGMENT.check2, true, hardness); + Particle_Gravity_update(&PartSys->particles[i], false, SEGMENT.check2, true, hardness); } SEGMENT.fill(BLACK); // clear the matrix @@ -8421,7 +8425,7 @@ static const char _data_FX_MODE_PARTICLEVOLCANO[] PROGMEM = "PS Volcano@Speed,In */ //TODO: -//do not use with to calculate numflames. it changes when mirror/transpose is activated, so will crash if height>width and then transposed +//do not use width to calculate numflames. it changes when mirror/transpose is activated, so will crash if height>width and then transposed //if using width*height and assuming a square, it will look sparse on a wide matrix... //could just allocate way too many and then dynamically adjust at at the expense of ram usage (but flames only use about 16bytes so is ok) //TODO: add 2D perlin noise to add to flame speed for randomness? may look good, may look awful, test it. also may hit FPS hard. @@ -8435,22 +8439,23 @@ uint16_t mode_particlefire(void) uint32_t i; //index variable #ifdef ESP8266 - const uint32_t numFlames = min((uint32_t)12, (cols << 1)); // limit to 18 flames, not enough ram on ESP8266 + uint32_t numFlames = min((uint32_t)12, (cols << 1)); // limit to 18 flames, not enough ram on ESP8266 const uint32_t numNormalFlames = numFlames - (numFlames / 3); // number of normal flames, rest of flames are baseflames uint32_t percycle = numFlames >> 2; // maximum number of particles emitted per cycle #else - const uint32_t numFlames = (cols*2); // number of flames: depends on fire width. for a fire width of 16 pixels, about 25-30 flames give good results, add a few for the base flames + uint32_t numFlames = (cols*2); // number of flames: depends on fire width. for a fire width of 16 pixels, about 25-30 flames give good results, add a few for the base flames const uint32_t numNormalFlames = numFlames;//- (cols / 2); // number of normal flames, rest of flames are baseflames uint32_t percycle = (numFlames) / 2; // maximum number of particles emitted per cycle #endif if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. { - if (!initParticleSystem(PartSys, numFlames)) + if (!initParticleSystem(PartSys, 0)) return mode_static(); // allocation failed; //allocation failed Serial.println("fireinit done"); SEGMENT.aux0 = rand(); // aux0 is wind position (index) in the perlin noise // initialize the flame sprays + numFlames = PartSys->numSources; for (i = 0; i < numFlames; i++) { PartSys->sources[i].source.ttl = 0; @@ -8463,17 +8468,19 @@ uint16_t mode_particlefire(void) } else PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS -/* - if (SEGMENT.call < 3) - { - Serial.print("segment data ptr in fireFX"); - Serial.println((uintptr_t)(SEGMENT.data)); - }*/ - - if (PartSys == NULL) + + numFlames = PartSys->numSources; + /* + if (SEGMENT.call < 3) { - Serial.println("ERROR: paticle system not found, nullpointer"); - return mode_static(); // something went wrong, no data! (TODO: ask how to handle this so it always works) + Serial.print("segment data ptr in fireFX"); + Serial.println((uintptr_t)(SEGMENT.data)); + }*/ + + if (PartSys == NULL) + { + Serial.println("ERROR: paticle system not found, nullpointer"); + return mode_static(); // something went wrong, no data! (TODO: ask how to handle this so it always works) } if(SEGMENT.check2) //wrap X set @@ -8505,7 +8512,7 @@ uint16_t mode_particlefire(void) if (i < numNormalFlames) { - PartSys->sources[i].source.ttl = 1 + random16((SEGMENT.intensity * SEGMENT.intensity) >> 8) / (1 + (SEGMENT.speed >> 5)); //'hotness' of fire, faster flames reduce the effect or flame height will scale too much with speed + PartSys->sources[i].source.ttl = 1 + random16((SEGMENT.custom1 * SEGMENT.custom1) >> 8) / (1 + (SEGMENT.speed >> 5)); //'hotness' of fire, faster flames reduce the effect or flame height will scale too much with speed // PartSys->sources[i].source.ttl = (rand() % ((SEGMENT.intensity * SEGMENT.intensity) >> 9) / (1 + (SEGMENT.speed >> 6))) + 10; //'hotness' of fire, faster flames reduce the effect or flame height will scale too much with speed // PartSys->sources[i].source.ttl = random16(SEGMENT.intensity+10) + 5; PartSys->sources[i].maxLife = random16(7) + 13; // defines flame height together with the vy speed, vy speed*maxlife/PS_P_RADIUS is the average flame height @@ -8514,7 +8521,7 @@ uint16_t mode_particlefire(void) PartSys->sources[i].vy = 5 + (SEGMENT.speed >> 2); // emitting speed (upwards) PartSys->sources[i].var = random16(5) + 3; // speed variation around vx,vy (+/- var/2) } - else // base flames: make the base brighter, flames are slower and short lived + else // base flames: make the base brighter, flames are slower and short lived //TODO: not used anymore { // PartSys->sources[i].source.ttl = random16(25) + 15; // lifetime of one flame // PartSys->sources[i].maxLife = 25; // defines flame height together with the vy speed, vy speed*maxlife/PS_P_RADIUS is the average flame height @@ -8549,18 +8556,18 @@ uint16_t mode_particlefire(void) j = (j + 1) % numFlames; } /* //TODO: add wind back in, may need a PS function to add a constant velocity to all particles (or can use force but is slower. or just do it here may be the fastest way) - else if (particles[i].y > PS_P_RADIUS) // particle is alive, apply wind if y > 1 + else if (PartSys->particles[i].y > PS_P_RADIUS) // particle is alive, apply wind if y > 1 { // add wind using perlin noise - particles[i].vx = windspeed; //todo: should this be depending on position? would be slower but may look better (used in old, slow fire) + PartSys->particles[i].vx = windspeed; //todo: should this be depending on position? would be slower but may look better (used in old, slow fire) }*/ SEGMENT.fill(BLACK); // clear the matrix - PartSys->updateFire(SEGMENT.custom1, SEGMENT.check1); // update and render the fire, colormode is determined by custom 3 slider + PartSys->updateFire(SEGMENT.intensity, SEGMENT.check1); // update and render the fire, colormode is determined by custom 3 slider return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEFIRE[] PROGMEM = "PS Fire@Speed,Intensity,Base Flames,Wind,Color Scheme,Palette,Cylinder;;!;035;sx=130,ix=120,c1=110,c2=128,c3=0,o1=0"; +static const char _data_FX_MODE_PARTICLEFIRE[] PROGMEM = "PS Fire@Speed,Intensity,Height,Wind,,Palette,Cylinder;;!;035;sx=130,ix=120,c1=110,c2=128,c3=0,o1=0"; /* PS Ballpit: particles falling down, user can enable these three options: X-wraparound, side bounce, ground bounce @@ -8576,11 +8583,11 @@ uint16_t mode_particlefall(void) return mode_static(); ParticleSystem *PartSys = NULL; - uint32_t i; // index variable + if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. { - if (!initParticleSystem(PartSys, 1)) //init, no sources needed + if (!initParticleSystem(PartSys, 0)) //init return mode_static(); // allocation failed; //allocation failed PartSys->setKillOutOfBounds(true); PartSys->enableGravity(true); @@ -8608,79 +8615,78 @@ uint16_t mode_particlefall(void) PartSys->enableParticleCollisions(false); } + uint32_t i; // index variable + if (SEGMENT.call % (64 - (SEGMENT.intensity >> 2)) == 0 && SEGMENT.intensity > 1) // every nth frame emit particles, stop emitting if set to zero { for (i = 0; i < PartSys->usedParticles; i++) // emit particles { if (PartSys->particles[i].ttl == 0) // find a dead particle { - // emit particle at random position just over the top of the matrix + // emit particle at random position over the top of the matrix (random16 is not random enough) PartSys->particles[i].ttl = 1500 - (SEGMENT.speed << 2) + random16(500); // if speed is higher, make them die sooner - if (random16(5) == 0) // 16% of particles apper anywhere - PartSys->particles[i].x = random16(PartSys->maxX); - else // rest is emitted at center half - PartSys->particles[i].x = random16((PartSys->maxX) >> 1) + (PartSys->maxX) >> 2; - - PartSys->particles[i].y = random16(PartSys->maxY) + PartSys->maxY; // particles appear somewhere above the matrix, maximum is double the height - PartSys->particles[i].vx = (((int16_t)random16(SEGMENT.custom1)) - (SEGMENT.custom1 >> 1)) >> 1; // side speed is +/- a quarter of the custom1 slider - PartSys->particles[i].vy = -(SEGMENT.speed >> 1); // downward speed - PartSys->particles[i].hue = random16(); // set random color - PartSys->particles[i].sat = ((SEGMENT.custom3) << 3) + 7; // set saturation - break; // emit only one particle per round + PartSys->particles[i].x = random(PartSys->maxX >> 1) + PartSys->maxX >> 2; + PartSys->particles[i].y = (PartSys->maxY<<1); // particles appear somewhere above the matrix, maximum is double the height + PartSys->particles[i].vx = (((int16_t)random(SEGMENT.custom1)) - (SEGMENT.custom1 >> 1)+5) >> 1; // side speed is +/- a quarter of the custom1 slider + PartSys->particles[i].vy = map(SEGMENT.speed, 0, 255, -5, -100); // downward speed + PartSys->particles[i].hue = random16(); // set random color + PartSys->particles[i].sat = ((SEGMENT.custom3) << 3) + 7; // set saturation + PartSys->particles[i].collide = true; //enable collision for particle + break; // emit only one particle per round } } } -//!!! - /* - i=0; - // if (SEGMENT.call % 2 == 0) // every nth frame emit particles, stop emitting if set to zero - { - - uint8_t emit = 5; - //i = random() % PartSys->usedParticles; - while (i < PartSys->usedParticles) // emit particles + //!!! + /* + i=0; + // if (SEGMENT.call % 2 == 0) // every nth frame emit particles, stop emitting if set to zero { - if (PartSys->particles[i].ttl == 0) // find a dead particle + + + uint8_t emit = 5; + //i = random() % PartSys->usedParticles; + while (i < PartSys->usedParticles) // emit particles { - Serial.print(" i="); - Serial.println(i); - int32_t x = random16((PartSys->maxX << 1)) - (PartSys->maxX >> 1); - int32_t y = random16((PartSys->maxY << 1)) - (PartSys->maxY >> 1); - - PartSys->particles[i].x = x; - PartSys->particles[i].y = y; - //PartSys->particles[i].x = random16((PartSys->maxX) >> 1); - //PartSys->particles[i].y = random16((PartSys->maxY) >> 1); - PartSys->particles[i].vx = random16(10) - 5; - PartSys->particles[i].vy = random16(10) - 5; - PartSys->particles[i].ttl = 3000; - PartSys->particles[i].hue = random16(); // set random color - PartSys->particles[i].sat = 255; - if(emit-- == 0) - break; + if (PartSys->particles[i].ttl == 0) // find a dead particle + { + Serial.print(" i="); + Serial.println(i); + int32_t x = random16((PartSys->maxX << 1)) - (PartSys->maxX >> 1); + int32_t y = random16((PartSys->maxY << 1)) - (PartSys->maxY >> 1); + + PartSys->particles[i].x = x; + PartSys->particles[i].y = y; + //PartSys->particles[i].x = random16((PartSys->maxX) >> 1); + //PartSys->particles[i].y = random16((PartSys->maxY) >> 1); + PartSys->particles[i].vx = random16(10) - 5; + PartSys->particles[i].vy = random16(10) - 5; + PartSys->particles[i].ttl = 3000; + PartSys->particles[i].hue = random16(); // set random color + PartSys->particles[i].sat = 255; + if(emit-- == 0) + break; + } + i++; } - i++; } - } - */ - //!!! + */ + //!!! - uint32_t frictioncoefficient = 1; - if (SEGMENT.speed < 50) // for low speeds, apply more friction - { - frictioncoefficient = 50 - SEGMENT.speed; - } - if (SEGMENT.call % 3 == 0) - PartSys->applyFriction(frictioncoefficient); + uint32_t frictioncoefficient = 1; + if (SEGMENT.speed < 50) // for low speeds, apply more friction + frictioncoefficient = 50 - SEGMENT.speed; - SEGMENT.fill(BLACK); // clear the matrix - PartSys->update(); // update and render + if (SEGMENT.call % 3 == 0) + PartSys->applyFriction(frictioncoefficient); // add some frictino to help smooth things - return FRAMETIME; + SEGMENT.fill(BLACK); // clear the matrix + PartSys->update(); // update and render + + return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEFALL[] PROGMEM = "PS Ballpit@Speed,Intensity,Randomness,Hardness,Saturation,Cylinder,Walls,Ground;;!;012;pal=11,sx=100,ix=200,c1=31,c2=100,c3=28,o1=0,o2=0,o3=1"; +static const char _data_FX_MODE_PARTICLEFALL[] PROGMEM = "PS Ballpit@Speed,Intensity,Randomness,Hardness,Saturation,Cylinder,Walls,Ground;;!;012;pal=11,sx=100,ix=200,c1=31,c2=100,c3=31,o1=0,o2=0,o3=1"; /* * Particle Waterfall @@ -9015,67 +9021,57 @@ static const char _data_FX_MODE_PARTICLEPERLIN[] PROGMEM = "PS Fuzzy Noise@Speed * Particle smashing down like meteorites and exploding as they hit the ground, has many parameters to play with * by DedeHai (Damian Schneider) */ -/* + uint16_t mode_particleimpact(void) { if (SEGLEN == 1) return mode_static(); - - const uint32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; - const uint32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); - - // particle system box dimensions - const uint32_t Max_x(cols * PS_P_RADIUS - 1); - const uint32_t Max_y(rows * PS_P_RADIUS - 1); - -#ifdef ESP8266 - const uint32_t numParticles = 150; - const uint8_t MaxNumMeteors = 2; -#else - const uint32_t numParticles = 550; - const uint8_t MaxNumMeteors = 8; -#endif - - PSparticle *particles; - PSsource *meteors; - - // allocate memory and divide it into proper pointers, max is 32k for all segments. - uint32_t dataSize = sizeof(PSparticle) * numParticles; - dataSize += sizeof(PSsource) * (MaxNumMeteors); - if (!SEGENV.allocateData(dataSize)) - return mode_static(); // allocation failed; //allocation failed - - meteors = reinterpret_cast(SEGENV.data); - // calculate the end of the spray data and assign it as the data pointer for the particles: - particles = reinterpret_cast(meteors + MaxNumMeteors); // cast the data array into a particle pointer - + ParticleSystem *PartSys = NULL; uint32_t i = 0; - uint32_t j = 0; - uint8_t numMeteors = map(SEGMENT.custom3, 0, 31, 1, MaxNumMeteors); // number of meteors to use for animation + uint8_t MaxNumMeteors; - if (SEGMENT.call == 0) // initialization + if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. { - for (i = 0; i < numParticles; i++) - { - particles[i].ttl = 0; - } + if (!initParticleSystem(PartSys, 0)) // init, no additional data needed + return mode_static(); // allocation failed; //allocation failed + // PartSys->setKillOutOfBounds(true); + PartSys->enableGravity(true); + PartSys->setBounceY(true); //always use ground bounce + // PartSys->setUsedParticles((PartSys->numParticles * 3) / 2); // use 2/3 of available particles + MaxNumMeteors = min(PartSys->numSources, (uint8_t)8); for (i = 0; i < MaxNumMeteors; i++) { - meteors[i].source.y = 10; - meteors[i].source.ttl = random16(20 * i); // set initial delay for meteors - meteors[i].source.vy = 10; // at positive speeds, no particles are emitted and if particle dies, it will be relaunched - meteors[i].source.sat = 255; //full saturation, color chosen by palette + PartSys->sources[i].vx = 0; //emit speed in x + PartSys->sources[i].source.y = 10; + PartSys->sources[i].source.ttl = random16(20 * i); // set initial delay for meteors + PartSys->sources[i].source.vy = 10; // at positive speeds, no particles are emitted and if particle dies, it will be relaunched + PartSys->sources[i].source.sat = 255; // full saturation, color chosen by palette } } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS - // update particles, create particles + MaxNumMeteors = min(PartSys->numSources, (uint8_t)8); + uint8_t numMeteors = map(SEGMENT.custom3, 0, 31, 1, MaxNumMeteors); // number of meteors to use for animation + + if (PartSys == NULL) + { + Serial.println("ERROR: paticle system not found, nullpointer"); + return mode_static(); // something went wrong, no data! (TODO: ask how to handle this so it always works) + } + + PartSys->setWrapX(SEGMENT.check1); + PartSys->setBounceX(SEGMENT.check2); + PartSys->setWallHardness(SEGMENT.custom2); // + PartSys->enableParticleCollisions(SEGMENT.check3, SEGMENT.custom2); // enable collisions and set particle collision hardness + uint32_t emitparticles; // number of particles to emit for each rocket's state - i = 0; - for (j = 0; j < numMeteors; j++) + + for (i = 0; i < numMeteors; i++) { // determine meteor state by its speed: - if (meteors[j].source.vy < 0) // moving down, emit sparks + if ( PartSys->sources[i].source.vy < 0) // moving down, emit sparks { #ifdef ESP8266 emitparticles = 1; @@ -9083,102 +9079,74 @@ uint16_t mode_particleimpact(void) emitparticles = 2; #endif } - else if (meteors[j].source.vy > 0) // moving up means meteor is on 'standby' + else if ( PartSys->sources[i].source.vy > 0) // moving up means meteor is on 'standby' { emitparticles = 0; } else // speed is zero, explode! { - meteors[j].source.vy = 125; // set source speed positive so it goes into timeout and launches again + PartSys->sources[i].source.vy = 125; // set source speed positive so it goes into timeout and launches again #ifdef ESP8266 emitparticles = random16(SEGMENT.intensity >> 2) + 10; // defines the size of the explosion #else emitparticles = random16(SEGMENT.intensity >> 1) + 10; // defines the size of the explosion #endif } - - while(i < numParticles) - { - if (particles[i].ttl == 0) // particle is dead - { - if (emitparticles > 0) - { - Emitter_Fountain_emit(&meteors[j], &particles[i]); - emitparticles--; - } - else - break; // done emitting for this meteor - } - i++; - } - } - - uint8_t hardness = SEGMENT.custom2; // how hard the collisions are, 255 = fully hard, no energy is lost in collision - - if (SEGMENT.check3) // use collisions if option is set - { - detectCollisions(particles, numParticles, hardness); - } - // update particles - for (i = 0; i < numParticles; i++) - { - if (particles[i].ttl) + for (int e = emitparticles; e > 0; e--) { - Particle_Gravity_update(&particles[i], SEGMENT.check1, SEGMENT.check2, true, hardness); + PartSys->sprayEmit(PartSys->sources[i]); } } // update the meteors, set the speed state for (i = 0; i < numMeteors; i++) { - if (meteors[i].source.ttl) + if (PartSys->sources[i].source.ttl) { - Particle_Gravity_update(&meteors[i].source, SEGMENT.check1, SEGMENT.check2, true, 255); // move the meteor, age the meteor (ttl--) - if (meteors[i].source.vy > 0) - meteors[i].source.y = 5; //'hack' to keep the meteors within frame, as ttl will be set to zero by gravity update if too far out of frame + PartSys->applyGravity(&PartSys->sources[i].source); + PartSys->particleMoveUpdate(PartSys->sources[i].source, PartSys->particlesettings); + // if source reaches the bottom, set speed to 0 so it will explode on next function call (handled above) - if ((meteors[i].source.y < PS_P_RADIUS) && (meteors[i].source.vy < 0)) // reached the bottom pixel on its way down + if ((PartSys->sources[i].source.y < PS_P_RADIUS) && ( PartSys->sources[i].source.vy < 0)) // reached the bottom pixel on its way down { - meteors[i].source.vy = 0; // set speed zero so it will explode - meteors[i].source.vx = 0; - meteors[i].source.y = 5; // offset from ground so explosion happens not out of frame - meteors[i].maxLife = 200; - meteors[i].minLife = 50; + PartSys->sources[i].source.vy = 0; // set speed zero so it will explode + PartSys->sources[i].source.vx = 0; + //PartSys->sources[i].source.y = 5; // offset from ground so explosion happens not out of frame (TODO: still needed?) + PartSys->sources[i].source.collide = true; + PartSys->sources[i].maxLife = 200; + PartSys->sources[i].minLife = 50; #ifdef ESP8266 - meteors[i].source.ttl = random16(255 - (SEGMENT.speed>>1)) + 10; // standby time til next launch (in frames at 42fps, max of 265 is about 6 seconds + PartSys->sources[i].source.ttl = random16(255 - (SEGMENT.speed>>1)) + 10; // standby time til next launch (in frames at 42fps, max of 265 is about 6 seconds #else - meteors[i].source.ttl = random16((255 - SEGMENT.speed)) + 10; // standby time til next launch (in frames at 42fps, max of 265 is about 6 seconds + PartSys->sources[i].source.ttl = random16((255 - SEGMENT.speed)) + 10; // standby time til next launch (in frames at 42fps, max of 265 is about 6 seconds #endif - meteors[i].vx = 0; // emitting speed x - meteors[i].vy = (SEGMENT.custom1 >> 2); // emitting speed y - meteors[i].var = (SEGMENT.custom1 >> 1); // speed variation around vx,vy (+/- var/2) + PartSys->sources[i].vy = (SEGMENT.custom1 >> 2); // emitting speed y + PartSys->sources[i].var = (SEGMENT.custom1 >> 1); // speed variation around vx,vy (+/- var/2) } - } - else if (meteors[i].source.vy > 0) // meteor is exploded and time is up (ttl==0 and positive speed), relaunch it + } + else if ( PartSys->sources[i].source.vy > 0) // meteor is exploded and time is up (ttl==0 and positive speed), relaunch it { // reinitialize meteor - meteors[i].source.y = Max_y + (PS_P_RADIUS << 2); // start 4 pixels above the top - meteors[i].source.x = random16(Max_x); - meteors[i].source.vy = -random16(30) - 30; // meteor downward speed - meteors[i].source.vx = random16(30) - 15; - meteors[i].source.hue = random16(); // random color - meteors[i].source.ttl = 1000; // long life, will explode at bottom - meteors[i].source.collide = false; // trail particles will not collide - meteors[i].maxLife = 60; // spark particle life - meteors[i].minLife = 20; - meteors[i].vx = 0; // emitting speed - meteors[i].vy = -9; // emitting speed (down) - meteors[i].var = 5; // speed variation around vx,vy (+/- var/2) + PartSys->sources[i].source.y = PartSys->maxY + (PS_P_RADIUS << 2); // start 4 pixels above the top + PartSys->sources[i].source.x = random16(PartSys->maxX); + PartSys->sources[i].source.vy = -random16(30) - 30; // meteor downward speed + PartSys->sources[i].source.vx = random16(30) - 15; + PartSys->sources[i].source.hue = random16(); // random color + PartSys->sources[i].source.ttl = 1000; // long life, will explode at bottom + PartSys->sources[i].source.collide = false; // trail particles will not collide + PartSys->sources[i].maxLife = 60; // spark particle life + PartSys->sources[i].minLife = 20; + PartSys->sources[i].vy = -9; // emitting speed (down) + PartSys->sources[i].var = 5; // speed variation around vx,vy (+/- var/2) } } - SEGMENT.fill(BLACK); // clear the matrix - // render the particles - ParticleSys_render(particles, numParticles, false, false); + SEGMENT.fill(BLACK); // clear the matrix + PartSys->update(); // update and render return FRAMETIME; } static const char _data_FX_MODE_PARTICLEIMPACT[] PROGMEM = "PS Impact@Launches,Explosion Size,Explosion Force,Bounce,Meteors,Cylinder,Walls,Collisions;;!;012;pal=0,sx=32,ix=85,c1=100,c2=100,c3=8,o1=0,o2=1,o3=1"; -*/ + /* Particle Attractor, a particle attractor sits in the matrix center, a spray bounces around and seeds particles uses inverse square law like in planetary motion @@ -9434,46 +9402,45 @@ uint16_t mode_particlespray(void) } static const char _data_FX_MODE_PARTICLESPRAY[] PROGMEM = "PS Spray@Speed,!,Left/Right,Up/Down,Angle,Gravity,Cylinder/Square,Collisions;;!;012;pal=0,sx=150,ix=90,c3=31,o1=0,o2=0,o3=0"; */ + /* Particle base Graphical Equalizer Uses palette for particle color by DedeHai (Damian Schneider) */ -/* + uint16_t mode_particleGEQ(void) { - if (SEGLEN == 1) return mode_static(); - const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; - //const uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); - -#ifdef ESP8266 - const uint32_t numParticles = 150; // maximum number of particles -#else - const uint32_t numParticles = 500; // maximum number of particles -#endif - - PSparticle *particles; + ParticleSystem *PartSys = NULL; - // allocate memory and divide it into proper pointers, max is 32k for all segments. - uint32_t dataSize = sizeof(PSparticle) * numParticles; - if (!SEGENV.allocateData(dataSize)) - return mode_static(); // allocation failed; //allocation failed + if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. + { + if (!initParticleSystem(PartSys, 0)) // init + return mode_static(); // allocation failed; //allocation failed + PartSys->setKillOutOfBounds(true); + PartSys->setUsedParticles((PartSys->numParticles * 3) / 2); // use 2/3 of available particles + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS - // calculate the end of the spray data and assign it as the data pointer for the particles: - particles = reinterpret_cast(SEGENV.data); // cast the data array into a particle pointer - uint32_t i; - if (SEGMENT.call == 0) // initialization + if (PartSys == NULL) { - for (i = 0; i < numParticles; i++) - { - particles[i].ttl = 0; - particles[i].sat = 255; //full color - } + Serial.println("ERROR: paticle system not found, nullpointer"); + return mode_static(); // something went wrong, no data! (TODO: ask how to handle this so it always works) } + uint32_t i; + //set particle system properties + PartSys->setWrapX(SEGMENT.check1); + PartSys->setBounceX(SEGMENT.check2); + PartSys->setBounceY(SEGMENT.check3); + PartSys->enableParticleCollisions(false); + PartSys->setWallHardness(SEGMENT.custom2); + PartSys->enableGravity(true, SEGMENT.custom3<<2); //set gravity strength + um_data_t *um_data; if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { @@ -9489,7 +9456,7 @@ uint16_t mode_particleGEQ(void) //implement it simply first, then add complexity... need to check what looks good i = 0; uint32_t bin; //current bin - uint32_t binwidth = (cols * PS_P_RADIUS - 1)>>4; //emit poisition variation for one bin (+/-) + uint32_t binwidth = (PartSys->maxX + 1)>>4; //emit poisition variation for one bin (+/-) is equal to width/16 (for 16 bins) uint32_t threshold = 300 - SEGMENT.intensity; uint32_t emitparticles = 0; @@ -9512,58 +9479,31 @@ uint16_t mode_particleGEQ(void) } } - while (i < numParticles && emitparticles > 0) // emit particles if there are any left, low frequencies take priority + while (i < PartSys->usedParticles && emitparticles > 0) // emit particles if there are any left, low frequencies take priority { - if (particles[i].ttl == 0) // find a dead particle + if (PartSys->particles[i].ttl == 0) // find a dead particle { //set particle properties - particles[i].ttl = map(SEGMENT.intensity, 0,255, emitspeed>>1, emitspeed + random16(emitspeed)) ; // set particle alive, particle lifespan is in number of frames - particles[i].x = xposition + random16(binwidth) - (binwidth>>1); //position randomly, deviating half a bin width - particles[i].y = 0; //start at the bottom - particles[i].vx = random16(SEGMENT.custom1>>1)-(SEGMENT.custom1>>2) ; //x-speed variation - particles[i].vy = emitspeed; - particles[i].hue = (bin<<4) + random16(17) - 8; // color from palette according to bin - //particles[i].sat = ((SEGMENT.custom3) << 3) + 7; // set saturation + PartSys->particles[i].ttl = map(SEGMENT.intensity, 0,255, emitspeed>>1, emitspeed + random16(emitspeed)) ; // set particle alive, particle lifespan is in number of frames + PartSys->particles[i].x = xposition + random16(binwidth) - (binwidth>>1); //position randomly, deviating half a bin width + PartSys->particles[i].y = 0; //start at the bottom + PartSys->particles[i].vx = random16(SEGMENT.custom1>>1)-(SEGMENT.custom1>>2) ; //x-speed variation + PartSys->particles[i].vy = emitspeed; + PartSys->particles[i].hue = (bin<<4) + random16(17) - 8; // color from palette according to bin + PartSys->particles[i].sat = 255; // set saturation emitparticles--; } i++; } } - uint8_t hardness = SEGMENT.custom2; // how hard the collisions are, 255 = full hard. - // detectCollisions(particles, numParticles, hardness); - - // now move the particles - for (i = 0; i < numParticles; i++) - { - particles[i].vy -= (SEGMENT.custom3>>3); // apply stronger gravity - Particle_Gravity_update(&particles[i], SEGMENT.check1, SEGMENT.check2, SEGMENT.check3, hardness); - } - SEGMENT.fill(BLACK); // clear the matrix - - // render the particles - ParticleSys_render(particles, numParticles, SEGMENT.check1, false); // custom3 slider is saturation - + PartSys->update(); // update and render return FRAMETIME; } static const char _data_FX_MODE_PARTICLEGEQ[] PROGMEM = "PS Equalizer@Speed,Intensity,Diverge,Bounce,Gravity,Cylinder,Walls,Floor;;!;012;pal=0,sx=155,ix=200,c1=0,c2=128,c3=31,o1=0,o2=0,o3=0"; -uint32_t rand16seedESP = 1337; -uint32_t random16_ESP() -{ - rand16seedESP = rand16seedESP * FASTLED_RAND16_2053 + FASTLED_RAND16_13849; - return rand16seedESP; -} -uint32_t random16_ESP(uint32_t limit) -{ - uint32_t r = random16_ESP(); - uint32_t p = limit * r; - r = p >> 16; - return r; -} -*/ /* * Particle rotating GEQ * Particles sprayed from center with a rotating spray @@ -9611,7 +9551,7 @@ uint16_t mode_particlecenterGEQ(void) SEGMENT.aux1 = 0xFF; // user check for (i = 0; i < numParticles; i++) { - particles[i].ttl = 0; + PartSys->particles[i].ttl = 0; } for (i = 0; i < numSprays; i++) { @@ -9653,7 +9593,7 @@ uint16_t mode_particlecenterGEQ(void) while (i < numParticles) { - if (particles[i].ttl == 0) // find a dead particle + if (PartSys->particles[i].ttl == 0) // find a dead particle { uint8_t emitspeed = 5 + (((uint32_t)fftResult[j] * ((uint32_t)SEGMENT.speed+10)) >> 9); // emit speed according to loudness of band uint8_t emitangle = j * 16 + random16(SEGMENT.custom3 >> 1) + angleoffset; @@ -9672,7 +9612,7 @@ uint16_t mode_particlecenterGEQ(void) } } if (emitparticles) - Emitter_Angle_emit(&spray[j], &particles[i], emitangle, emitspeed); + Emitter_Angle_emit(&spray[j], &PartSys->particles[i], emitangle, emitspeed); j = (j + 1) % numSprays; } i++; @@ -9683,7 +9623,7 @@ uint16_t mode_particlecenterGEQ(void) for (i = 0; i < numParticles; i++) { - Particle_Move_update(&particles[i], true); // move the particles, kill out of bounds particles + Particle_Move_update(&PartSys->particles[i], true); // move the particles, kill out of bounds particles } SEGMENT.fill(BLACK); // clear the matrix @@ -9941,6 +9881,8 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_PARTICLEROTATINGSPRAY, &mode_particlerotatingspray, _data_FX_MODE_PARTICLEROTATINGSPRAY); addEffect(FX_MODE_PARTICLEFIRE, &mode_particlefire, _data_FX_MODE_PARTICLEFIRE); addEffect(FX_MODE_PARTICLEFALL, &mode_particlefall, _data_FX_MODE_PARTICLEFALL); + addEffect(FX_MODE_PARTICLEIMPACT, &mode_particleimpact, _data_FX_MODE_PARTICLEIMPACT); + addEffect(FX_MODE_PARTICLESGEQ, &mode_particleGEQ, _data_FX_MODE_PARTICLEGEQ); /* addEffect(FX_MODE_PARTICLEVOLCANO, &mode_particlevolcano, _data_FX_MODE_PARTICLEVOLCANO); @@ -9949,10 +9891,10 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_PARTICLEBOX, &mode_particlebox, _data_FX_MODE_PARTICLEBOX); addEffect(FX_MODE_PARTICLEWATERFALL, &mode_particlewaterfall, _data_FX_MODE_PARTICLEWATERFALL); - addEffect(FX_MODE_PARTICLEIMPACT, &mode_particleimpact, _data_FX_MODE_PARTICLEIMPACT); + addEffect(FX_MODE_PARTICLEATTRACTOR, &mode_particleattractor, _data_FX_MODE_PARTICLEATTRACTOR); addEffect(FX_MODE_PARTICLESPRAY, &mode_particlespray, _data_FX_MODE_PARTICLESPRAY); - addEffect(FX_MODE_PARTICLESGEQ, &mode_particleGEQ, _data_FX_MODE_PARTICLEGEQ); + */ // addEffect(FX_MODE_PARTICLECENTERGEQ, &mode_particlecenterGEQ, _data_FX_MODE_PARTICLECCIRCULARGEQ); diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index 5568fcff15..d5b04576a2 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -148,7 +148,7 @@ Segment& Segment::operator= (Segment &&orig) noexcept { bool IRAM_ATTR Segment::allocateData(size_t len) { if (len == 0) return false; // nothing to do if (data && _dataLen >= len) { // already allocated enough (reduce fragmentation) - if (call == 0) memset(data, 0, len); // erase buffer if called during effect initialisation + //!!! if (call == 0) memset(data, 0, len); // erase buffer if called during effect initialisation return true; } DEBUG_PRINT(F("Allocating Data")); diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index 2fa2c1e00d..f49a0c198c 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -56,14 +56,14 @@ ParticleSystem::ParticleSystem(uint16_t width, uint16_t height, uint16_t numbero numSources = numberofsources; numParticles = numberofparticles; // set number of particles in the array usedParticles = numberofparticles; // use all particles by default - particlesettings = {false, false, false, false, false, false, false, false}; // all settings off by default + //particlesettings = {false, false, false, false, false, false, false, false}; // all settings off by default initPSpointers(); // set the particle and sources pointer (call this before accessing sprays or particles) setMatrixSize(width, height); setWallHardness(255); // set default wall hardness to max emitIndex = 0; for (int i = 0; i < numParticles; i++) { - particles[i].ttl = 0; //initialize all particles to dead + //particles[i].ttl = 0; //initialize all particles to dead } Serial.println("PS Constructor done"); } @@ -199,6 +199,7 @@ void ParticleSystem::sprayEmit(PSsource &emitter) particles[emitIndex].ttl = random16(emitter.maxLife - emitter.minLife) + emitter.minLife; particles[emitIndex].hue = emitter.source.hue; particles[emitIndex].sat = emitter.source.sat; + particles[emitIndex].collide = emitter.source.collide; break; } /* @@ -385,7 +386,6 @@ void ParticleSystem::applyAngleForce(PSparticle *part, uint32_t numparticles, ui void ParticleSystem::applyGravity(PSparticle *part, uint32_t numarticles, uint8_t force, uint8_t *counter) { int32_t dv; // velocity increase - if (force > 15) dv = (force >> 4); // apply the 4 MSBs else @@ -411,6 +411,21 @@ void ParticleSystem::applyGravity(PSparticle *part, uint32_t numarticles, uint8_ applyGravity(part, numarticles, gforce, counter); } +//apply gravity to single particle using system settings (use this for sources) +void ParticleSystem::applyGravity(PSparticle *part) +{ + int32_t dv; // velocity increase + if (gforce > 15) + dv = (gforce >> 4); // apply the 4 MSBs + else + dv = 1; + + if (gforcecounter + gforce > 15) //counter is updated in global update when applying gravity + { + part->vy = part->vy - dv > PS_P_MAXSPEED ? PS_P_MAXSPEED : part->vy - dv; // limit the force, this is faster than min or if/else + } +} + // slow down particles by friction, the higher the speed, the higher the friction. a high friction coefficient slows them more (255 means instant stop) void ParticleSystem::applyFriction(PSparticle *part, uint8_t coefficient) { @@ -536,23 +551,10 @@ void ParticleSystem::ParticleSys_render() // TODO: if pointer returns null, use classic render (or do not render this frame) if (useLocalBuffer) { - // allocate memory for the 2D array in one contiguous block - colorbuffer = (CRGB **)malloc((maxXpixel + 1) * sizeof(CRGB *) + (maxXpixel + 1) * (maxYpixel + 1) * sizeof(CRGB)); + // allocate memory for the local renderbuffer + colorbuffer = allocate2Dbuffer(maxXpixel + 1, maxYpixel + 1); if (colorbuffer == NULL) - { - DEBUG_PRINT(F("PS renderbuffer memory alloc failed")); - useLocalBuffer = false; - //return; - } - else{ - // assign pointers of 2D array - CRGB *start = (CRGB *)(colorbuffer + (maxXpixel + 1)); - for (i = 0; i < maxXpixel + 1; i++) - { - colorbuffer[i] = start + i * (maxYpixel + 1); - } - memset(start, 0, (maxXpixel + 1) * (maxYpixel + 1) * sizeof(CRGB)); // set all values to zero - } + useLocalBuffer = false; //render to segment pixels directly if not enough memory } // go over particles and render them to the buffer @@ -805,10 +807,10 @@ void ParticleSystem::fireParticleupdate() particles[i].ttl--; // apply velocity particles[i].x = particles[i].x + (int32_t)particles[i].vx; - particles[i].y = particles[i].y + (int32_t)particles[i].vy + (particles[i].ttl >> 4); // younger particles move faster upward as they are hotter, used for fire + particles[i].y = particles[i].y + (int32_t)particles[i].vy + (particles[i].ttl >> 4); // younger particles move faster upward as they are hotter particles[i].outofbounds = 0; // check if particle is out of bounds, wrap x around to other side if wrapping is enabled - // as fire particles start below the frame, lots of particles are out of bounds in y direction. to improve animation speed, only check x direction if y is not out of bounds + // as fire particles start below the frame, lots of particles are out of bounds in y direction. to improve speed, only check x direction if y is not out of bounds // y-direction if (particles[i].y < -PS_P_HALFRADIUS) particles[i].outofbounds = 1; @@ -842,41 +844,22 @@ void ParticleSystem::renderParticleFire(uint32_t intensity, bool usepalette) uint32_t flameheat; //depends on particle.ttl uint32_t i; uint32_t debug = 0; - //CRGB colorbuffer[(maxXpixel+1)][(maxYpixel+1)] = {0}; - - // Allocate memory for the array of pointers to rows - CRGB **colorbuffer = (CRGB **)calloc(maxXpixel + 1, sizeof(CRGB *)); + + // allocate memory for the local renderbuffer + CRGB **colorbuffer = allocate2Dbuffer(maxXpixel + 1, maxYpixel + 1); if (colorbuffer == NULL) { - Serial.println("Memory allocation failed111"); - return; + SEGMENT.setPalette(35); // set fire palette + SEGMENT.check1 = true; //enable palette from now on + usepalette = true; //use palette f } + //TODO: move the rendering over to the render-particle function, add a parameter 'rendertype' to select normal rendering or fire rendering, in the future this can also be used to render smaller/larger particle sizes - // Allocate memory for each row - for (i = 0; i <= maxXpixel; i++) - { - colorbuffer[i] = (CRGB *)calloc(maxYpixel + 1, sizeof(CRGB)); - if (colorbuffer[i] == NULL) - { - Serial.println("Memory allocation failed222"); - return; - } - } - - // go over particles and update matrix cells on the way - // note: some pixels (the x+1 ones) can be out of bounds, it is probably faster than to check that for every pixel as this only happens on the right border (and nothing bad happens as this is checked down the road) for (i = 0; i < usedParticles; i++) { - if (particles[i].outofbounds) //lots of fire particles are out of bounds, check first - { - //Serial.print("o"); - continue; - } - if (particles[i].ttl == 0) - { - //Serial.print("d"); + if (particles[i].outofbounds || particles[i].ttl == 0) // lots of fire particles are out of bounds, check first continue; - } + if (usepalette) { // generate RGB values for particle @@ -890,38 +873,29 @@ void ParticleSystem::renderParticleFire(uint32_t intensity, bool usepalette) renderParticle(&particles[i], brightness, intensity, pixco); if (intensity[0] > 0) - colorbuffer[pixco[0][0]][pixco[0][1]] = fast_color_add(colorbuffer[pixco[0][0]][pixco[0][1]], baseRGB, intensity[0]); // bottom left - // SEGMENT.addPixelColorXY(pixco[0][0], maxYpixel - pixco[0][1], baseRGB.scale8((uint8_t)intensity[0])); + colorbuffer[pixco[0][0]][pixco[0][1]] = fast_color_add(colorbuffer[pixco[0][0]][pixco[0][1]], baseRGB, intensity[0]); // bottom left if (intensity[1] > 0) - colorbuffer[pixco[1][0]][pixco[1][1]] = fast_color_add(colorbuffer[pixco[1][0]][pixco[1][1]], baseRGB, intensity[1]); - // SEGMENT.addPixelColorXY(pixco[1][0], maxYpixel - pixco[1][1], baseRGB.scale8((uint8_t)intensity[1])); // bottom right + colorbuffer[pixco[1][0]][pixco[1][1]] = fast_color_add(colorbuffer[pixco[1][0]][pixco[1][1]], baseRGB, intensity[1]); if (intensity[2] > 0) colorbuffer[pixco[2][0]][pixco[2][1]] = fast_color_add(colorbuffer[pixco[2][0]][pixco[2][1]], baseRGB, intensity[2]); - // SEGMENT.addPixelColorXY(pixco[2][0], maxYpixel - pixco[2][1], baseRGB.scale8((uint8_t)intensity[2])); // top right if (intensity[3] > 0) colorbuffer[pixco[3][0]][pixco[3][1]] = fast_color_add(colorbuffer[pixco[3][0]][pixco[3][1]], baseRGB, intensity[3]); - // SEGMENT.addPixelColorXY(pixco[3][0], maxYpixel - pixco[3][1], baseRGB.scale8((uint8_t)intensity[3])); // top left } else{ flameheat = particles[i].ttl; int32_t pixelheat[4] = {0}; // note: passed array needs to be set to 0 or checking in rendering function does not work (if values persist), this is faster then setting it to 0 there renderParticle(&particles[i], flameheat, pixelheat, pixco); //render heat to physical pixels - - // TODO: add one more pixel closer to the particle, so it is 3 pixels wide + if (pixelheat[0] >= 0) - PartMatrix_addHeat(pixelheat[0], &colorbuffer[pixco[0][0]][pixco[0][1]].r, intensity); - //PartMatrix_addHeat(pixco[0][0], pixco[0][1], pixelheat[0], intensity); + PartMatrix_addHeat(pixelheat[0], &colorbuffer[pixco[0][0]][pixco[0][1]].r, intensity); if (pixelheat[1] >= 0) - PartMatrix_addHeat(pixelheat[1], &colorbuffer[pixco[1][0]][pixco[1][1]].r, intensity); - //PartMatrix_addHeat(pixco[1][0], pixco[1][1], pixelheat[1], intensity); + PartMatrix_addHeat(pixelheat[1], &colorbuffer[pixco[1][0]][pixco[1][1]].r, intensity); if (pixelheat[2] >= 0) PartMatrix_addHeat(pixelheat[2], &colorbuffer[pixco[2][0]][pixco[2][1]].r, intensity); - //PartMatrix_addHeat(pixco[2][0], pixco[2][1], pixelheat[2], intensity); if (pixelheat[3] >= 0) PartMatrix_addHeat(pixelheat[3], &colorbuffer[pixco[3][0]][pixco[3][1]].r, intensity); - //PartMatrix_addHeat(pixco[3][0], pixco[3][1], pixelheat[3], intensity); - // TODO: add heat to a third pixel. need to konw dx and dy, the heatvalue is (flameheat - pixelheat) vom pixel das weiter weg ist vom partikelzentrum + // TODO: add heat to a third pixel? need to konw dx and dy, the heatvalue is (flameheat - pixelheat) vom pixel das weiter weg ist vom partikelzentrum // also wenn dx < halfradius dann links, sonst rechts. rechts flameheat-pixelheat vom linken addieren und umgekehrt // das ist relativ effizient um rechnen und sicher schneller als die alte variante. gibt ein FPS drop, das könnte man aber // mit einer schnelleren add funktion im segment locker ausgleichen @@ -939,15 +913,7 @@ void ParticleSystem::renderParticleFire(uint32_t intensity, bool usepalette) SEGMENT.setPixelColorXY(x, maxYpixel - y, colorbuffer[x][y]); } } - - // Free memory for each row - for (int i = 0; i <= maxXpixel; i++) - { - free(colorbuffer[i]); - } - - // Free memory for the array of pointers to rows - free(colorbuffer); + free(colorbuffer); // free buffer memory } // adds 'heat' to red color channel, if it overflows, add it to next color channel @@ -994,9 +960,7 @@ void ParticleSystem::PartMatrix_addHeat(int32_t heat, uint8_t *currentcolor, uin } if (i == 2) // blue channel was reached, limit the color value so it does not go full white - currentcolor[i] = currentcolor[i] > 60 ? 60 : currentcolor[i]; //faster than min() - - //SEGMENT.setPixelColorXY(col, maxYpixel - row, currentcolor); + currentcolor[i] = currentcolor[i] > 50 ? 50 : currentcolor[i]; //faster than min() } // detect collisions in an array of particles and handle them @@ -1016,10 +980,13 @@ void ParticleSystem::handleCollisions() } collisioncounter++; + //startparticle = 0;//!!! test: do all collisions every frame, + //endparticle = usedParticles; + for (i = startparticle; i < endparticle; i++) { - // go though all 'higher number' particles and see if any of those are in close proximity and if they are, make them collide - if (particles[i].ttl > 0 && particles[i].outofbounds == 0) // if particle is alive and does collide and is not out of view + // go though all 'higher number' particles and see if any of those are in close proximity and if they are, make them collide + if (particles[i].ttl > 0 && particles[i].outofbounds == 0 && particles[i].collide) // if particle is alive and does collide and is not out of view { int32_t dx, dy; // distance to other particles for (j = i + 1; j < usedParticles; j++) @@ -1041,7 +1008,7 @@ void ParticleSystem::handleCollisions() // handle a collision if close proximity is detected, i.e. dx and/or dy smaller than 2*PS_P_RADIUS // takes two pointers to the particles to collide and the particle hardness (softer means more energy lost in collision, 255 means full hard) -void ParticleSystem::collideParticles(PSparticle *particle1, PSparticle *particle2) +void ParticleSystem::collideParticles(PSparticle *particle1, PSparticle *particle2) //TODO: dx,dy is calculated just above, can pass it over here to save some CPU time { int32_t dx = particle2->x - particle1->x; @@ -1055,38 +1022,31 @@ void ParticleSystem::collideParticles(PSparticle *particle1, PSparticle *particl if (distanceSquared == 0) // add distance in case particles exactly meet at center, prevents dotProduct=0 (this can only happen if they move towards each other) { - // Adjust positions based on relative velocity direction TODO: is this really needed? only happens on fast particles, would save some code (but make it a tiny bit less accurate on fast particles but probably not an issue) - - if (relativeVx < 0) - { // if true, particle2 is on the right side - particle1->x--; - particle2->x++; - } - else - { - particle1->x++; - particle2->x--; - } + distanceSquared++; + } + /* + // Adjust positions based on relative velocity direction -> does not really do any good. + uint32_t add = 1; + if (relativeVx < 0) // if true, particle2 is on the right side + add = -1; + particle1->x += add; + particle2->x -= add; + add = 1; if (relativeVy < 0) - { - particle1->y--; - particle2->y++; - } - else - { - particle1->y++; - particle2->y--; - } + add = -1; + particle1->y += add; + particle2->y -= add; + distanceSquared++; - } + }*/ // Calculate dot product of relative velocity and relative distance int32_t dotProduct = (dx * relativeVx + dy * relativeVy); // If particles are moving towards each other if (dotProduct < 0) { - const uint32_t bitshift = 14; // bitshift used to avoid floats + const uint32_t bitshift = 16; // bitshift used to avoid floats (dx/dy are 7bit, relativV are 8bit -> dotproduct is 15bit so 16bit shift is ok) // Calculate new velocities after collision int32_t impulse = (((dotProduct << (bitshift)) / (distanceSquared)) * collisionHardness) >> 8; @@ -1096,18 +1056,18 @@ void ParticleSystem::collideParticles(PSparticle *particle1, PSparticle *particl particle1->vy += yimpulse; particle2->vx -= ximpulse; particle2->vy -= yimpulse; - - //TODO: this is removed for now as it does not seem to do much and does not help with piling. if soft, much energy is lost anyway at a collision, so they are automatically sticky + //also second version using multiplication is slower on ESP8266 than the if's - if (collisionHardness < 220) // if particles are soft, they become 'sticky' i.e. they are slowed down at collisions + if (collisionHardness < 128) // if particles are soft, they become 'sticky' i.e. they are slowed down at collisions even more { + //particle1->vx = (particle1->vx < 2 && particle1->vx > -2) ? 0 : particle1->vx; //particle1->vy = (particle1->vy < 2 && particle1->vy > -2) ? 0 : particle1->vy; //particle2->vx = (particle2->vx < 2 && particle2->vx > -2) ? 0 : particle2->vx; //particle2->vy = (particle2->vy < 2 && particle2->vy > -2) ? 0 : particle2->vy; - const uint32_t coeff = collisionHardness + 20; + const uint32_t coeff = 191 + (collisionHardness>>1); particle1->vx = ((int32_t)particle1->vx * coeff) >> 8; particle1->vy = ((int32_t)particle1->vy * coeff) >> 8; @@ -1116,20 +1076,21 @@ void ParticleSystem::collideParticles(PSparticle *particle1, PSparticle *particl } } - // particles have volume, push particles apart if they are too close by moving each particle by a fixed amount away from the other particle + + // particles have volume, push particles apart if they are too close by moving each particle by a fixed amount away from the other particle // if pushing is made dependent on hardness, things start to oscillate much more, better to just add a fixed, small increment (tried lots of configurations, this one works best) // one problem remaining is, particles get squished if (external) force applied is higher than the pushback but this may also be desirable if particles are soft. also some oscillations cannot be avoided without addigng a counter if (distanceSquared < 2 * PS_P_HARDRADIUS * PS_P_HARDRADIUS) { uint8_t choice = dotProduct & 0x01; // random16(2); // randomly choose one particle to push, avoids oscillations note: dotprouct LSB should be somewhat random, so no need to calculate a random number const int32_t HARDDIAMETER = 2 * PS_P_HARDRADIUS; - const int32_t pushamount = 2; //push a small amount + const int32_t pushamount = 1; // push a small amount int32_t push = pushamount; if (dx < HARDDIAMETER && dx > -HARDDIAMETER) { // distance is too small, push them apart - if (dx <= 0) + if (dx < 0) push = -pushamount; //-(PS_P_HARDRADIUS + dx); // inverted push direction if (choice) // chose one of the particles to push, avoids oscillations @@ -1141,7 +1102,7 @@ void ParticleSystem::collideParticles(PSparticle *particle1, PSparticle *particl push = pushamount; // reset push variable to 1 if (dy < HARDDIAMETER && dy > -HARDDIAMETER) { - if (dy <= 0) + if (dy < 0) push = -pushamount; //-(PS_P_HARDRADIUS + dy); // inverted push direction if (choice) // chose one of the particles to push, avoids oscillations @@ -1192,6 +1153,25 @@ int32_t ParticleSystem::calcForce_dV(int8_t force, uint8_t* counter) return dv; } +// allocate memory for the 2D array in one contiguous block and set values to zero +CRGB **ParticleSystem::allocate2Dbuffer(uint32_t cols, uint32_t rows) +{ + CRGB ** array2D = (CRGB **)malloc(cols * sizeof(CRGB *) + cols * rows * sizeof(CRGB)); + if (array2D == NULL) + DEBUG_PRINT(F("PS buffer alloc failed")); + else + { + // assign pointers of 2D array + CRGB *start = (CRGB *)(array2D + cols); + for (int i = 0; i < cols; i++) + { + array2D[i] = start + i * rows; + } + memset(start, 0, cols * rows * sizeof(CRGB)); // set all values to zero + } + return array2D; +} + // set the pointers for the class (this only has to be done once and not on every FX call, only the class pointer needs to be reassigned to SEGENV.data every time) // function returns the pointer to the next byte available for the FX (if it assigned more memory for other stuff using the above allocate function) // FX handles the PSsources, need to tell this function how many there are @@ -1234,6 +1214,24 @@ uint32_t calculateNumberOfParticles() return numberofParticles; } +uint32_t calculateNumberOfSources() +{ + uint32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; + uint32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); +#ifdef ESP8266 + int numberofSources = (cols * rows) / 8; + numberofSources = min(numberofSources, 16); +#elseif ARDUINO_ARCH_ESP32S2 + int numberofSources = (cols * rows) / 6; + numberofSources = min(numberofSources, 32); +#else + int numberofSources = (cols * rows) / 4; + numberofSources = min(numberofSources, 64); +#endif + // TODO: may be too many.. + return numberofSources; +} + //allocate memory for particle system class, particles, sprays plus additional memory requested by FX bool allocateParticleSystemMemory(uint16_t numparticles, uint16_t numsources, uint16_t additionalbytes) { @@ -1247,11 +1245,13 @@ bool allocateParticleSystemMemory(uint16_t numparticles, uint16_t numsources, ui return(SEGMENT.allocateData(requiredmemory)); } -bool initParticleSystem(ParticleSystem *&PartSys, uint16_t numsources) +// initialize Particle System, allocate additional bytes if needed (pointer to those bytes can be read from particle system class: PSdataEnd) +bool initParticleSystem(ParticleSystem *&PartSys, uint16_t additionalbytes) { Serial.println("PS init function"); uint32_t numparticles = calculateNumberOfParticles(); - if (!allocateParticleSystemMemory(numparticles, numsources, 0)) + uint32_t numsources = calculateNumberOfSources(); + if (!allocateParticleSystemMemory(numparticles, numsources, additionalbytes)) { DEBUG_PRINT(F("PS init failed: memory depleted")); return false; diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index 6dc5b4f2c3..76d4d201ac 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -48,27 +48,13 @@ typedef struct { //two byte bit field: //uint16_t ttl : 12; // time to live, 12 bit or 4095 max (which is 50s at 80FPS) bool outofbounds : 1; //out of bounds flag, set to true if particle is outside of display area - bool flag1 : 1; // unused flags... - bool flag2 : 1; - bool flag3 : 1; + bool collide : 1; //if set, particle takes part in collisions + bool flag3 : 1; // unused flags... + bool flag4 : 1; uint16_t ttl; // time to live, 12 bit or 4095 max (which is 50s at 80FPS) } PSparticle; -// struct for a single particle -typedef struct -{ - int16_t x; // x position in particle system - int16_t y; // y position in particle system - int8_t vx; // horizontal velocity - int8_t vy; // vertical velocity - uint8_t hue; // color hue - // two byte bit field: - bool outofbounds : 1; // out of bounds flag, set to true if particle is outside of display area - uint16_t ttl : 7; // time to live, 7 bit or 128 max (need to adjust fire animation to not exceed this value! max is now 137 because of +10 -> done einfach durch zwei geteilt.) -} PSfireparticle; -//todo: wenn man reduzierte partikel verwenet, kann man das nicht mit palette rendern. erst ausprobieren, ob das gut aussieht, dann entscheiden. ist vermutlich keine gute idee... - //struct for a particle source typedef struct { uint16_t minLife; //minimum ttl of emittet particles @@ -112,6 +98,7 @@ class ParticleSystem //particle physics void applyGravity(PSparticle *part, uint32_t numarticles, uint8_t force, uint8_t *counter); void applyGravity(PSparticle *part, uint32_t numarticles, uint8_t *counter); //use global gforce + void applyGravity(PSparticle *part); //use global system settings void applyForce(PSparticle *part, uint32_t numparticles, int8_t xforce, int8_t yforce, uint8_t *counter); void applyAngleForce(PSparticle *part, uint32_t numparticles, uint8_t force, uint8_t angle, uint8_t *counter); void applyFriction(PSparticle *part, uint8_t coefficient); // apply friction to specific particle @@ -139,6 +126,7 @@ class ParticleSystem uint8_t numSources; //number of sources uint16_t numParticles; // number of particles available in this system uint16_t usedParticles; // number of particles used in animation (can be smaller then numParticles) + PSsettings particlesettings; // settings used when updating particles (can also used by FX to move sources) private: //rendering functions @@ -156,6 +144,7 @@ class ParticleSystem void initPSpointers(); // call this after allocating the memory to initialize the pointers int32_t wraparound(int32_t w, int32_t maxvalue); int32_t calcForce_dV(int8_t force, uint8_t *counter); + CRGB **allocate2Dbuffer(uint32_t cols, uint32_t rows); // note: variables that are accessed often are 32bit for speed uint32_t emitIndex; // index to count through particles to emit so searching for dead pixels is faster @@ -164,12 +153,12 @@ class ParticleSystem uint8_t gforcecounter; //counter for global gravity uint8_t gforce; //gravity strength, default is 8 uint8_t collisioncounter; //counter to handle collisions - PSsettings particlesettings; // settings used when updating particles }; //initialization functions (not part of class) -bool initParticleSystem(ParticleSystem *&PartSys, uint16_t numsources); +bool initParticleSystem(ParticleSystem *&PartSys, uint16_t additionalbytes); uint32_t calculateNumberOfParticles(); +uint32_t calculateNumberOfSources(); bool allocateParticleSystemMemory(uint16_t numparticles, uint16_t numsources, uint16_t additionalbytes); //color add function CRGB fast_color_add(CRGB c1, CRGB c2, uint32_t scale); // fast and accurate color adding with scaling (scales c2 before adding) \ No newline at end of file From 59f2b9ae98b77b345984c097e79f352ded1abcff Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Mon, 1 Apr 2024 19:04:28 +0200 Subject: [PATCH 054/219] more bugfixes, some animation tuning, added volcano back in - fixed animation transitions with proper pointer setting in each FX call - added color by age setting to particle system - maybe fixed collision having a tendency to go to the left - fixed bugs in fireworks FX - added fire spread width as a slider - changed max number of particles and sprays based on some ram calculations - some other random fixes --- wled00/FX.cpp | 634 +++++++++++++++--------------------- wled00/FXparticleSystem.cpp | 186 ++++++----- wled00/FXparticleSystem.h | 15 +- 3 files changed, 366 insertions(+), 469 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index f06ead8bde..065ff95c00 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -7902,9 +7902,7 @@ uint16_t mode_particlerotatingspray(void) { if (!initParticleSystem(PartSys, 0)) return mode_static(); // allocation failed; //allocation failed - - Serial.print("PS pointer "); - Serial.println((uintptr_t)PartSys); + DEBUG_PRINTF_P(PSTR("PS pointer %p\n"), 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); @@ -7936,13 +7934,16 @@ uint16_t mode_particlerotatingspray(void) } } PartSys->setKillOutOfBounds(true); - Serial.print("segment data ptr in FX"); - Serial.println((uintptr_t)(SEGMENT.data)); + + DEBUG_PRINTF_P(PSTR("segment data ptr in candy FX %p\n"), SEGMENT.data); Serial.println("INIT done"); } else PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + //DEBUG_PRINTF_P(PSTR("segment data ptr in candy FX %p\n"), SEGMENT.data); + PartSys->updatePSpointers(); // update data pointers (needed in case our memory was moved) + numSprays = min(PartSys->numSources, (uint8_t)8); /* if (SEGMENT.call < 3) @@ -8064,18 +8065,18 @@ uint16_t mode_particlerotatingspray(void) PartSys->sources[j].var = (SEGMENT.custom3 >> 1); // emiting variation = nozzle size (custom 3 goes from 0-32) } - #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) { - PartSys->sprayEmit(PartSys->sources[j]); + #ifdef ESP8266 + if (SEGMENT.call & 0x01) // every other frame, do not emit to save particles + #endif + PartSys->sprayEmit(PartSys->sources[j]); j = (j + 1) % spraycount; } @@ -8084,7 +8085,7 @@ uint16_t mode_particlerotatingspray(void) 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"; /* @@ -8093,194 +8094,171 @@ 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) return mode_static(); - const uint32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; - const uint32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); - // particle system box dimensions - const uint32_t Max_x = (cols * PS_P_RADIUS - 1); - const uint32_t Max_y = (rows * PS_P_RADIUS - 1); -#ifdef ESP8266 - const uint32_t numParticles = 120; -#else - const uint32_t numParticles = 400; -#endif - const uint8_t numRockets = 4; - PSparticle *particles; - PSsource *rockets; - // allocate memory and divide it into proper pointers, max is 32k for all segments. - uint32_t dataSize = sizeof(PSparticle) * numParticles; - dataSize += sizeof(PSsource) * (numRockets); - if (!SEGENV.allocateData(dataSize)) - return mode_static(); // allocation failed; //allocation failed - 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 + ParticleSystem *PartSys = NULL; + uint8_t numRockets; uint32_t i = 0; uint32_t j = 0; - if (SEGMENT.call == 0) // initialization + + if (SEGMENT.call == 0) // initialization { - for (i = 0; i < numParticles; i++) - { - PartSys->particles[i].ttl = 0; - } + if (!initParticleSystem(PartSys, 0)) // init, no additional data needed + return mode_static(); // allocation failed; //allocation failed + PartSys->setKillOutOfBounds(true); //out of bounds particles dont return (except on top, taken care of by gravity setting) + numRockets = min(PartSys->numSources, (uint8_t)4); for (j = 0; j < numRockets; j++) { - rockets[j].source.ttl = random16(20 * j); // first rocket starts immediately, others follow soon - rockets[j].source.vy = -1; // at negative speed, no particles are emitted and if rocket dies, it will be relaunched + PartSys->sources[j].source.ttl = 500 * j; // first rocket starts immediately, others follow soon + PartSys->sources[j].source.vy = -1; // at negative speed, no particles are emitted and if rocket dies, it will be relaunched } } + 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! + } + PartSys->updatePSpointers(); //update data pointers (needed in case our memory was moved) + numRockets = min(PartSys->numSources, (uint8_t)4); + + PartSys->setWrapX(SEGMENT.check1); + PartSys->setBounceY(SEGMENT.check2); + PartSys->setWallHardness(SEGMENT.custom2); + PartSys->enableGravity(true, map(SEGMENT.custom3,0,31,0,10)); // todo: make it a slider to adjust + // check each rocket's state and emit particles according to its state: moving up = emit exhaust, at top = explode; falling down = standby time int32_t emitparticles; // number of particles to emit for each rocket's state - i = 0; - // variables for circle explosions - uint32_t speed; - uint32_t currentspeed; - uint8_t angle; - uint32_t counter; - uint32_t angleincrement; - uint32_t speedvariation; + // variables for circle explosions + uint8_t speed; + uint8_t currentspeed; + uint16_t angle; + uint8_t counter; + uint16_t angleincrement; + uint8_t percircle; + uint8_t speedvariation; bool circularexplosion = false; for (j = 0; j < numRockets; j++) { // determine rocket state by its speed: - if (rockets[j].source.vy > 0) + if (PartSys->sources[j].source.vy > 0) { // moving up, emit exhaust emitparticles = 1; } - else if (rockets[j].source.vy < 0) + else if (PartSys->sources[j].source.vy < 0) { // falling down emitparticles = 0; } else // speed is zero, explode! { -#ifdef ESP8266 + #ifdef ESP8266 emitparticles = random16(SEGMENT.intensity >> 3) + (SEGMENT.intensity >> 3) + 5; // defines the size of the explosion -#else + #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 + #endif + PartSys->sources[j].source.vy = -1; // set speed negative so it will emit no more particles after this explosion until relaunch + if(random16(4) == 0) { circularexplosion = true; speed = 2 + random16(3); currentspeed = speed; counter = 0; - angleincrement = random16(20) + 10; - speedvariation = random16(3); + angleincrement = 2730 + random16(5461); // minimum 15° (=2730), + random(30°) (=5461) angle = random16(); // random start angle + speedvariation = angle & 0x01; // 0 or 1, no need for a new random number // calculate the number of particles to make complete circles - int percircle = 256 / angleincrement + 2; + percircle = (uint16_t)0xFFFF / angleincrement + 1; #ifdef ESP8266 //TODO: this line is untested on ESP8266 - int circles = (SEGMENT.intensity >> 7) + 1; // max(4, (int)min((int32_t)1, (emitparticles>>2) / percircle)); + int circles = (SEGMENT.intensity >> 7) + 1; #else - int circles = (SEGMENT.intensity >> 6) + 1;// max(4, (int)min((int32_t)1, (emitparticles>>2) / percircle)); + int circles = (SEGMENT.intensity >> 6) + 1; #endif emitparticles = percircle * circles; - rockets[j].var = 0; //no variation for a nice circle + PartSys->sources[j].var = 0; //no variation for nicer circles } } - for (i = 0; i < numParticles; i++) + for (i = 0; i < emitparticles; i++) { - if (PartSys->particles[i].ttl == 0) - { // particle is dead - if (emitparticles > 0) + if (circularexplosion) // do circle emit + { + if (counter & 0x01) // make every second particle a lower speed + currentspeed = speed - speedvariation; + else + currentspeed = speed; + PartSys->angleEmit(PartSys->sources[j], angle, currentspeed); + counter++; + if (counter > percircle) // full circle completed, increase speed { - if (circularexplosion) // do circle emit - { - Emitter_Angle_emit(&rockets[j], &PartSys->particles[i], angle, currentspeed); - emitparticles--; - // set angle for next particle - angle += angleincrement; - counter++; - if (counter & 0x01) // make every second particle a lower speed - currentspeed = speed - speedvariation; - else - currentspeed = speed; - if (counter > 256 / angleincrement + 2) // full circle completed, increase speed - { - counter = 0; - speed += 5; //increase speed to form a second circle - speedvariation = speedvariation<<1; //double speed variation - rockets[j].source.hue = random16(); // new color for next circle - rockets[j].source.sat = min((uint16_t)40,random16()); - } - } - else - { - Emitter_Fountain_emit(&rockets[j], &PartSys->particles[i]); - emitparticles--; - if ((j % 3) == 0) - { - rockets[j].source.hue = random16(); // random color for each particle - rockets[j].source.sat = min((uint16_t)40, random16()); - } - } + counter = 0; + speed += 5; //increase speed to form a second circle + speedvariation = speedvariation ? speedvariation + random16(4) : 0; // double speed variation + PartSys->sources[j].source.hue = random16(); // new color for next circle + PartSys->sources[j].source.sat = min((uint16_t)150,random16()); + } + angle += angleincrement; // set angle for next particle + } + else + { + PartSys->sprayEmit(PartSys->sources[j]); + if ((j % 3) == 0) + { + PartSys->sources[j].source.hue = random16(); // random color for each particle (this is also true for exhaust, but that is white anyways) + // PartSys->sources[j].source.sat = min((uint16_t)150, random16()); //dont change saturation, this can also be exhaust! } - else - break; // done emitting for this rocket } } + if(i == 0) //no particles emitted, this rocket is falling + PartSys->sources[j].source.y = 1000; // set position up high so gravity wont pull it to the ground and bounce it (vy MUST stay negative until relaunch) circularexplosion = false; //reset for next rocket } - // update particles - for (i = 0; i < numParticles; i++) - { - if (PartSys->particles[i].ttl) - { - Particle_Gravity_update(&PartSys->particles[i], SEGMENT.check1, SEGMENT.check2, SEGMENT.check3, SEGMENT.custom2); - } - } // update the rockets, set the speed state for (j = 0; j < numRockets; j++) { - if (rockets[j].source.ttl) + if (PartSys->sources[j].source.ttl) { - Particle_Move_update(&rockets[j].source); // move the rocket, age the rocket (ttl--) + PartSys->particleMoveUpdate(PartSys->sources[j].source, PartSys->particlesettings); //todo: need different settings for rocket? } - else if (rockets[j].source.vy > 0) // rocket has died and is moving up. stop it so it will explode (is handled in the code above) + else if (PartSys->sources[j].source.vy > 0) // rocket has died and is moving up. stop it so it will explode (is handled in the code above) { - rockets[j].source.vy = 0; // set speed to zero so code above will recognize this as an exploding rocket - rockets[j].source.hue = random16(); // random color - 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].vx = 0; // emitting speed - rockets[j].vy = 3; // emitting speed - rockets[j].var = (SEGMENT.intensity >> 3) + 10; // speed variation around vx,vy (+/- var/2) - } - else if (rockets[j].source.vy < 0) // rocket is exploded and time is up (ttl=0 and negative speed), relaunch it + PartSys->sources[j].source.vy = 0; // set speed to zero so code above will recognize this as an exploding rocket + PartSys->sources[j].source.hue = random16(); // random color + PartSys->sources[j].source.sat = random16(55) + 200; + PartSys->sources[j].maxLife = 200; + PartSys->sources[j].minLife = 100; + PartSys->sources[j].source.ttl = random16((1024 - ((uint32_t)SEGMENT.speed<<2))) + 100; // standby time til next launch + PartSys->sources[j].var = (SEGMENT.intensity >> 3) + 10; // speed variation around vx,vy (+/- var/2) + } + else if ( PartSys->sources[j].source.vy < 0) // rocket is exploded and time is up (ttl=0 and negative speed), relaunch it { // reinitialize rocket - rockets[j].source.y = 1; // start from bottom - rockets[j].source.x = (rand() % (Max_x >> 1)) + (Max_y >> 2); // centered half - rockets[j].source.vy = random16(SEGMENT.custom1 >> 3) + 5; // rocket speed depends also on rocket height - rockets[j].source.vx = random16(5) - 2; - rockets[j].source.hue = 30; // rocket exhaust = orange (if using rainbow palette) - rockets[j].source.sat = 30; // low saturation -> exhaust is off-white - rockets[j].source.ttl = random16(SEGMENT.custom1) + (SEGMENT.custom1 >> 1); // sets explosion height (rockets explode at the top if set too high as paticle update set speed to zero if moving out of matrix) - rockets[j].maxLife = 30; // exhaust particle life - rockets[j].minLife = 10; - rockets[j].vx = 0; // emitting speed - rockets[j].vy = 0; // emitting speed - rockets[j].var = 6; // speed variation around vx,vy (+/- var/2) + PartSys->sources[j].source.y = 1; // start from bottom + PartSys->sources[j].source.x = (rand() % (PartSys->maxX >> 1)) + (PartSys->maxX >> 2); // centered half + PartSys->sources[j].source.vy = random16(SEGMENT.custom1 >> 3) + 5; // rocket speed depends also on rocket fuse + PartSys->sources[j].source.vx = random16(5) - 2; //i.e. not perfectly straight up + PartSys->sources[j].source.sat = 30; // low saturation -> exhaust is off-white + PartSys->sources[j].source.ttl = random16(SEGMENT.custom1) + (SEGMENT.custom1 >> 1); // sets explosion height (rockets explode at the top if set too high as paticle update set speed to zero if moving out of matrix) + PartSys->sources[j].maxLife = 30; // exhaust particle life + PartSys->sources[j].minLife = 10; + PartSys->sources[j].vx = 0; // emitting speed + PartSys->sources[j].vy = 0; // emitting speed + PartSys->sources[j].var = 6; // speed variation around vx,vy (+/- var/2) } } + SEGMENT.fill(BLACK); // clear the matrix - // render the particles - ParticleSys_render(particles, numParticles, false, false); + PartSys->update(); // update and render 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"; -*/ +static const char _data_FX_MODE_PARTICLEFIREWORKS[] PROGMEM = "PS Fireworks@Launches,Explosion Size,Fuse,Bounce,Gravity,Cylinder,Ground,;;!;012;pal=11,sx=100,ix=50,c1=84,c2=128,c3=12,o1=0,o2=0,o3=0"; /* * Particle Volcano @@ -8289,164 +8267,103 @@ 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(); - - const uint32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; - - // particle system x dimension - const uint32_t Max_x = (cols * PS_P_RADIUS - 1); - -#ifdef ESP8266 - const uint32_t numParticles = 100; // maximum number of particles -#else - const uint32_t numParticles = 450; // maximum number of particles -#endif - - const uint8_t numSprays = 1; - uint8_t percycle = numSprays; // maximum number of particles emitted per cycle - - PSparticle *particles; - PSsource *spray; - - // allocate memory and divide it into proper pointers, max is 32k for all segments. - uint32_t dataSize = sizeof(PSparticle) * numParticles; - dataSize += sizeof(PSsource) * (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 - + ParticleSystem *PartSys = NULL; + uint8_t numSprays; uint32_t i = 0; uint32_t j = 0; - if (SEGMENT.call == 0) // initialization + if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. { - for (i = 0; i < numParticles; i++) - { - PartSys->particles[i].ttl = 0; - } + if (!initParticleSystem(PartSys, 0)) // init, no additional data needed + return mode_static(); // allocation failed; //allocation failed + PartSys->setBounceY(true); + PartSys->enableGravity(true); //enable with default gforce + PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting) + numSprays = min(PartSys->numSources, (uint8_t)1); //number of sprays for (i = 0; i < numSprays; i++) { - spray[i].source.hue = random16(); - spray[i].source.sat = 255; // set full saturation - spray[i].source.x = (cols * PS_P_RADIUS)/(numSprays+1) * (i + 1); - spray[i].source.y = 5; // just above the lower edge, if zero, particles already 'bounce' at start and loose speed. - spray[i].source.vx = 0; - spray[i].maxLife = 300; // lifetime in frames - spray[i].minLife = 20; - spray[i].source.collide = true; //seeded particles will collide - spray[i].vx = 0; // emitting speed - spray[i].vy = 20; // emitting speed - // spray.var = 10 + (random16() % 4); + PartSys->sources[i].source.hue = random16(); + PartSys->sources[i].source.sat = 255; // set full saturation + PartSys->sources[i].source.x = PartSys->maxX / (numSprays + 1) * (i + 1); //distribute evenly + PartSys->sources[i].maxLife = 300; // lifetime in frames + PartSys->sources[i].minLife = 250; + PartSys->sources[i].source.collide = true; // seeded particles will collide (if enabled) + PartSys->sources[i].vx = 0; // emitting speed } } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS - // change source emitting color from time to time - if (SEGMENT.call % (11 - (SEGMENT.intensity / 25)) == 0) // every nth frame, cycle color and emit particles + if (PartSys == NULL) { - for (i = 0; i < numSprays; i++) - { - spray[i].source.hue++; // = random16(); //change hue of spray source - // percycle = 1+(SEGMENT.intensity>>4); //how many particles are sprayed per cycle and how fast ist the color changing - if(SEGMENT.check2) //bounce - { - if (spray[i].source.vx > 0) // moving to the right currently - { - spray[i].source.vx = SEGMENT.custom1 >> 4; // spray movingspeed - } - else - { - spray[i].source.vx = -(SEGMENT.custom1 >> 4); // spray speed (is currently moving negative so keep it negative) - } - } - else{ //wrap on the right side - spray[i].source.vx = SEGMENT.custom1 >> 4; // spray speed - if (spray[i].source.x >= Max_x - 32) //compiler warning can be ignored, source.x is always > 0 - spray[i].source.x = 1; // wrap if close to border (need to wrap before the bounce updated detects a border collision or it will just be stuck) - } - spray[i].vy = SEGMENT.speed >> 2; // emitting speed - spray[i].vx = 0; - spray[i].var = SEGMENT.custom3; // emiting variation = nozzle size (custom 3 goes from 0-32) - spray[i].source.ttl = 255; // source never dies, replenish its lifespan - } - - i = 0; - j = 0; - for (i = 0; i < numParticles; i++) - { - if (PartSys->particles[i].ttl == 0) // find a dead particle - { - // spray[j].source.hue = random16(); //set random color for each particle (using palette) - Emitter_Fountain_emit(&spray[j], &PartSys->particles[i]); - j = (j + 1) % numSprays; - if (percycle-- == 0) - { - break; // quit loop if all particles of this round emitted - } - } - } + 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) } - uint8_t hardness = SEGMENT.custom2; - if (SEGMENT.check3)//collisions enabled - detectCollisions(particles, numParticles, hardness); - for (i = 0; i < numSprays; i++) - { - Particle_Bounce_update(&spray[i].source, (uint8_t)255); // move the source - } + numSprays = min(PartSys->numSources, (uint8_t)1); // number of sprays - for (i = 0; i < numParticles; i++) - { - //set color according to ttl ('color by age') - if (SEGMENT.check1) - PartSys->particles[i].hue = min((uint16_t)220, PartSys->particles[i].ttl); + // Particle System settings + PartSys->updatePSpointers(); // update data pointers (needed in case our memory was moved) + PartSys->setColorByAge(SEGMENT.check1); + PartSys->setBounceX(SEGMENT.check2); + PartSys->setWallHardness(SEGMENT.custom2); - Particle_Gravity_update(&PartSys->particles[i], false, SEGMENT.check2, true, hardness); + if (SEGMENT.check3) // collisions enabled + PartSys->enableParticleCollisions(true, SEGMENT.custom2); // enable collisions and set particle collision hardness + else + PartSys->enableParticleCollisions(false); + + // change source emitting color from time to time, emit one particle per spray + if (SEGMENT.call % (11 - (SEGMENT.intensity / 25)) == 0) // every nth frame, cycle color and emit particles (and update the sources) + { + for (i = 0; i < numSprays; i++) + { + PartSys->sources[i].source.y = 5; // reset to just above the lower edge, if zero, particles already 'bounce' at start and loose speed. + PartSys->sources[i].source.vy = 0; //reset speed (so no extra particlesettin is required to keep the source 'afloat') + PartSys->sources[i].source.hue++; // = random16(); //change hue of spray source + // percycle = 1+(SEGMENT.intensity>>4); //how many particles are sprayed per cycle and how fast ist the color changing + PartSys->sources[i].source.vx > 0 ? SEGMENT.custom1 >> 4 : -(SEGMENT.custom1 >> 4); // set moving speed but keep the direction + PartSys->sources[i].vy = SEGMENT.speed >> 2; // emitting speed + PartSys->sources[i].vx = 0; + PartSys->sources[i].var = SEGMENT.custom3; // emiting variation = nozzle size (custom 3 goes from 0-31) + PartSys->sources[i].source.ttl = 255; // source never dies, replenish its lifespan + // spray[j].source.hue = random16(); //set random color for each particle (using palette) -> does not look good + PartSys->sprayEmit(PartSys->sources[i]); + PartSys->particleMoveUpdate(PartSys->sources[i].source, PartSys->particlesettings); //move the source (also applies gravity, which is corrected for above, that is a hack but easier than creating more particlesettings) + //Serial.println("emit"); + } } SEGMENT.fill(BLACK); // clear the matrix - - // render the particles - ParticleSys_render(particles, numParticles, false, false); + PartSys->update(); // update and render 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"; -*/ -/* - * Particle Fire - * realistic fire effect using particles. heat based and using perlin-noise for wind - * by DedeHai (Damian Schneider) - */ +static const char _data_FX_MODE_PARTICLEVOLCANO[] PROGMEM = "PS Volcano@Speed,Intensity,Move,Bounce,Spread,Color by Age,Walls,Collisions;;!;012;pal=35,sx=100,ix=160,c1=0,c2=160,c3=10,o1=1,o2=0,o3=0"; -//TODO: -//do not use width to calculate numflames. it changes when mirror/transpose is activated, so will crash if height>width and then transposed -//if using width*height and assuming a square, it will look sparse on a wide matrix... -//could just allocate way too many and then dynamically adjust at at the expense of ram usage (but flames only use about 16bytes so is ok) -//TODO: add 2D perlin noise to add to flame speed for randomness? may look good, may look awful, test it. also may hit FPS hard. + /* + * Particle Fire + * realistic fire effect using particles. heat based and using perlin-noise for wind + * by DedeHai (Damian Schneider) + */ + + // TODO: + // do not use width to calculate numflames. it changes when mirror/transpose is activated, so will crash if height>width and then transposed + // if using width*height and assuming a square, it will look sparse on a wide matrix... + // could just allocate way too many and then dynamically adjust at at the expense of ram usage (but flames only use about 16bytes so is ok) + // TODO: add 2D perlin noise to add to flame speed for randomness? may look good, may look awful, test it. also may hit FPS hard. uint16_t mode_particlefire(void) { if (SEGLEN == 1) - return mode_static(); - ParticleSystem *PartSys = NULL; - const uint32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; - const uint32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); - uint32_t i; //index variable + return mode_static(); -#ifdef ESP8266 - uint32_t numFlames = min((uint32_t)12, (cols << 1)); // limit to 18 flames, not enough ram on ESP8266 - const uint32_t numNormalFlames = numFlames - (numFlames / 3); // number of normal flames, rest of flames are baseflames - uint32_t percycle = numFlames >> 2; // maximum number of particles emitted per cycle -#else - uint32_t numFlames = (cols*2); // number of flames: depends on fire width. for a fire width of 16 pixels, about 25-30 flames give good results, add a few for the base flames - const uint32_t numNormalFlames = numFlames;//- (cols / 2); // number of normal flames, rest of flames are baseflames - uint32_t percycle = (numFlames) / 2; // maximum number of particles emitted per cycle -#endif + ParticleSystem *PartSys = NULL; + uint32_t i; // index variable + uint32_t numFlames; // number of flames: depends on fire width. for a fire width of 16 pixels, about 25-30 flames give good results if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. { @@ -8458,18 +8375,29 @@ uint16_t mode_particlefire(void) numFlames = PartSys->numSources; for (i = 0; i < numFlames; i++) { - PartSys->sources[i].source.ttl = 0; + PartSys->sources[i].source.ttl = 1000; PartSys->sources[i].source.vx = 0; // emitter moving speed; PartSys->sources[i].source.vy = 0; + PartSys->sources[i].source.sat = 255; //!!!debug, test if transitions are better (non white) // note: other parameters are set when creating the flame (see blow) } - Serial.print("segment data ptr in fireFX"); - Serial.println((uintptr_t)(SEGMENT.data)); + + DEBUG_PRINTF_P(PSTR("segment data ptr in fireFX %p\n"), SEGMENT.data); } else PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS - - numFlames = PartSys->numSources; + 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) + } + + DEBUG_PRINTF_P(PSTR("segment data ptr in Fire FX %p\n"), SEGMENT.data); + PartSys->updatePSpointers(); // update data pointers (needed in case our memory was moved) + + uint32_t spread = (PartSys->maxX >> 5) * (SEGMENT.custom3 + 1); //fire around segment center (in subpixel points) + numFlames = min((uint)PartSys->numSources, 1+((spread/PS_P_RADIUS)<<1)); // number of flames used depends on spread with, good value is (fire width in pixel) * 2 + uint32_t percycle = numFlames / 2; // maximum number of particles emitted per cycle /* if (SEGMENT.call < 3) { @@ -8477,68 +8405,37 @@ uint16_t mode_particlefire(void) Serial.println((uintptr_t)(SEGMENT.data)); }*/ - if (PartSys == NULL) - { - Serial.println("ERROR: paticle system not found, nullpointer"); - return mode_static(); // something went wrong, no data! (TODO: ask how to handle this so it always works) - } - - if(SEGMENT.check2) //wrap X set - PartSys->setWrapX(true); + PartSys->setWrapX(SEGMENT.check2); // update the flame sprays: for (i = 0; i < numFlames; i++) { - if ( PartSys->sources[i].source.ttl > 0) + if (PartSys->sources[i].source.ttl > 0) { - PartSys->sources[i].source.ttl--; + PartSys->sources[i].source.ttl--; } else // flame source is dead { // initialize new flame: set properties of source if (random16(20) == 0 || SEGMENT.call == 0) // from time to time, change flame position - { - if (SEGMENT.check2) // wrap around in X direction, distribute randomly - { - PartSys->sources[i].source.x = rand() % (PartSys->maxX - PS_P_HALFRADIUS + 1); // note: cannot use rand16() here, it is not random enough: tends to burn on left side - } - else // no X-wrapping - { - PartSys->sources[i].source.x = (rand() % ((PartSys->maxX - PS_P_HALFRADIUS + 1) - (PS_P_RADIUS * ((cols >> 3) + 1)))) + PS_P_RADIUS * ((cols >> 4) + 1); // distribute randomly but not close to the corners - } + { + //!!!remove if done PartSys->sources[i].source.x = ((rand() % (PartSys->maxX>>1)) + (PartSys->maxX>>2)); // distribute randomly on center half corners + PartSys->sources[i].source.x = (PartSys->maxX >> 1) - (spread>>1) + (rand() % spread) ; // distribute randomly on chosen width } - PartSys->sources[i].source.y = -PS_P_RADIUS; // set the source below the frame - - if (i < numNormalFlames) - { + PartSys->sources[i].source.y = -PS_P_RADIUS; // set the source below the frame PartSys->sources[i].source.ttl = 1 + random16((SEGMENT.custom1 * SEGMENT.custom1) >> 8) / (1 + (SEGMENT.speed >> 5)); //'hotness' of fire, faster flames reduce the effect or flame height will scale too much with speed - // PartSys->sources[i].source.ttl = (rand() % ((SEGMENT.intensity * SEGMENT.intensity) >> 9) / (1 + (SEGMENT.speed >> 6))) + 10; //'hotness' of fire, faster flames reduce the effect or flame height will scale too much with speed - // PartSys->sources[i].source.ttl = random16(SEGMENT.intensity+10) + 5; + //!!! remove when done PartSys->sources[i].source.ttl = (rand() % ((SEGMENT.intensity * SEGMENT.intensity) >> 9) / (1 + (SEGMENT.speed >> 6))) + 10; //'hotness' of fire, faster flames reduce the effect or flame height will scale too much with speed + //!!! remove when done PartSys->sources[i].source.ttl = random16(SEGMENT.intensity+10) + 5; PartSys->sources[i].maxLife = random16(7) + 13; // defines flame height together with the vy speed, vy speed*maxlife/PS_P_RADIUS is the average flame height PartSys->sources[i].minLife = 4; PartSys->sources[i].vx = (int8_t)random16(4) - 2; // emitting speed (sideways) PartSys->sources[i].vy = 5 + (SEGMENT.speed >> 2); // emitting speed (upwards) PartSys->sources[i].var = random16(5) + 3; // speed variation around vx,vy (+/- var/2) - } - else // base flames: make the base brighter, flames are slower and short lived //TODO: not used anymore - { - // PartSys->sources[i].source.ttl = random16(25) + 15; // lifetime of one flame - // PartSys->sources[i].maxLife = 25; // defines flame height together with the vy speed, vy speed*maxlife/PS_P_RADIUS is the average flame height - // PartSys->sources[i].minLife = 12; - // PartSys->sources[i].vx = 0; // emitting speed, sideways - // PartSys->sources[i].vy = (SEGMENT.custom1 >> 4); // slow emitting speed (upwards) - //PartSys->sources[i].var = 5; // speed variation around vx,vy (+/- var/2) - PartSys->sources[i].source.ttl = random16(25) + 15; // lifetime of one flame - PartSys->sources[i].maxLife = 0; // defines flame height together with the vy speed, vy speed*maxlife/PS_P_RADIUS is the average flame height - PartSys->sources[i].minLife = 0; - PartSys->sources[i].vx = 0; // emitting speed, sideways - PartSys->sources[i].vy = (SEGMENT.custom1 >> 4); // slow emitting speed (upwards) - PartSys->sources[i].var = 0; // speed variation around vx,vy (+/- var/2) - } } + } - // percycle = map(SEGMENT.intensity,0,255, 2, (numFlames*3) / 2); + // percycle = map(SEGMENT.intensity,0,255, 2, (numFlames*3) / 2); //TODO: will this give better flames or worse? if (SEGMENT.call & 0x01) // update noise position every second frames { SEGMENT.aux0++; // position in the perlin noise matrix for wind generation @@ -8567,7 +8464,7 @@ uint16_t mode_particlefire(void) return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEFIRE[] PROGMEM = "PS Fire@Speed,Intensity,Height,Wind,,Palette,Cylinder;;!;035;sx=130,ix=120,c1=110,c2=128,c3=0,o1=0"; +static const char _data_FX_MODE_PARTICLEFIRE[] PROGMEM = "PS Fire@Speed,Intensity,Height,Wind,Spread,Palette,Cylinder;;!;035;sx=130,ix=120,c1=110,c2=128,c3=22,o1=0"; /* PS Ballpit: particles falling down, user can enable these three options: X-wraparound, side bounce, ground bounce @@ -8583,8 +8480,6 @@ uint16_t mode_particlefall(void) return mode_static(); ParticleSystem *PartSys = NULL; - - if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. { if (!initParticleSystem(PartSys, 0)) //init @@ -8602,7 +8497,7 @@ uint16_t mode_particlefall(void) Serial.println("ERROR: paticle system not found, nullpointer"); return mode_static(); // something went wrong, no data! (TODO: ask how to handle this so it always works) } - + PartSys->updatePSpointers(); // update data pointers (needed in case our memory was moved) PartSys->setWrapX(SEGMENT.check1); PartSys->setBounceX(SEGMENT.check2); PartSys->setBounceY(SEGMENT.check3); @@ -8736,28 +8631,28 @@ uint16_t mode_particlewaterfall(void) } for (i = 0; i < numSprays; i++) { - spray[i].source.hue = random16(); - spray[i].source.sat = 255; // set full saturation - spray[i].source.x = (cols * PS_P_RADIUS) / 2 - PS_P_RADIUS + 2*PS_P_RADIUS * (i); - spray[i].source.y = (rows+4) * (PS_P_RADIUS*(i+1)); // source y position, few pixels above the top to increase spreading before entering the matrix - spray[i].source.vx = 0; - spray[i].source.collide = true; // seeded particles will collide + PartSys->sources[i].source.hue = random16(); + PartSys->sources[i].source.sat = 255; // set full saturation + PartSys->sources[i].source.x = (cols * PS_P_RADIUS) / 2 - PS_P_RADIUS + 2*PS_P_RADIUS * (i); + PartSys->sources[i].source.y = (rows+4) * (PS_P_RADIUS*(i+1)); // source y position, few pixels above the top to increase spreading before entering the matrix + PartSys->sources[i].source.vx = 0; + PartSys->sources[i].source.collide = true; // seeded particles will collide #ifdef ESP8266 - spray[i].maxLife = 100; // lifetime in frames - spray[i].minLife = 50; + PartSys->sources[i].maxLife = 100; // lifetime in frames + PartSys->sources[i].minLife = 50; #else - spray[i].maxLife = 400; // lifetime in frames - spray[i].minLife = 150; + PartSys->sources[i].maxLife = 400; // lifetime in frames + PartSys->sources[i].minLife = 150; #endif - spray[i].vx = 0; // emitting speed - spray[i].var = 7; // emiting variation + PartSys->sources[i].vx = 0; // emitting speed + PartSys->sources[i].var = 7; // emiting variation } } // change source emitting color for (i = 0; i < numSprays; i++) { - spray[i].source.hue++; //change hue of spray source + PartSys->sources[i].source.hue++; //change hue of spray source } uint8_t intensity = SEGMENT.intensity; @@ -8767,10 +8662,10 @@ uint16_t mode_particlewaterfall(void) for (i = 0; i < numSprays; i++) { - spray[i].vy = -SEGMENT.speed >> 3; // emitting speed, down - spray[i].source.x = map(SEGMENT.custom3, 0, 31, 0, (cols - 2) * PS_P_RADIUS) + i * PS_P_RADIUS * 2; // emitter position - spray[i].source.ttl = 255; // source never dies, replenish its lifespan - spray[i].var = SEGMENT.custom1 >> 3; // emiting variation 0-32 + PartSys->sources[i].vy = -SEGMENT.speed >> 3; // emitting speed, down + PartSys->sources[i].source.x = map(SEGMENT.custom3, 0, 31, 0, (cols - 2) * PS_P_RADIUS) + i * PS_P_RADIUS * 2; // emitter position + PartSys->sources[i].source.ttl = 255; // source never dies, replenish its lifespan + PartSys->sources[i].var = SEGMENT.custom1 >> 3; // emiting variation 0-32 } i = 0; @@ -9051,14 +8946,13 @@ uint16_t mode_particleimpact(void) else PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS - MaxNumMeteors = min(PartSys->numSources, (uint8_t)8); - uint8_t numMeteors = map(SEGMENT.custom3, 0, 31, 1, MaxNumMeteors); // number of meteors to use for animation - if (PartSys == NULL) { Serial.println("ERROR: paticle system not found, nullpointer"); return mode_static(); // something went wrong, no data! (TODO: ask how to handle this so it always works) } + MaxNumMeteors = min(PartSys->numSources, (uint8_t)8); + uint8_t numMeteors = map(SEGMENT.custom3, 0, 31, 1, MaxNumMeteors); // number of meteors to use for animation PartSys->setWrapX(SEGMENT.check1); PartSys->setBounceX(SEGMENT.check2); @@ -9111,7 +9005,7 @@ uint16_t mode_particleimpact(void) { PartSys->sources[i].source.vy = 0; // set speed zero so it will explode PartSys->sources[i].source.vx = 0; - //PartSys->sources[i].source.y = 5; // offset from ground so explosion happens not out of frame (TODO: still needed?) + //PartSys->sources[i].source.y = 5; // offset from ground so explosion happens not out of frame (TODO: still needed? the class takes care of that) PartSys->sources[i].source.collide = true; PartSys->sources[i].maxLife = 200; PartSys->sources[i].minLife = 50; @@ -9132,7 +9026,7 @@ uint16_t mode_particleimpact(void) PartSys->sources[i].source.vy = -random16(30) - 30; // meteor downward speed PartSys->sources[i].source.vx = random16(30) - 15; PartSys->sources[i].source.hue = random16(); // random color - PartSys->sources[i].source.ttl = 1000; // long life, will explode at bottom + PartSys->sources[i].source.ttl = 255; // long life, will explode at bottom PartSys->sources[i].source.collide = false; // trail particles will not collide PartSys->sources[i].maxLife = 60; // spark particle life PartSys->sources[i].minLife = 20; @@ -9332,17 +9226,17 @@ uint16_t mode_particlespray(void) } for (i = 0; i < numSprays; i++) { - spray[i].source.hue = random16(); - spray[i].source.sat = 255; // set full saturation - spray[i].source.x = (cols * PS_P_RADIUS) / (numSprays + 1) * (i + 1); - spray[i].source.y = 5; // just above the lower edge, if zero, particles already 'bounce' at start and loose speed. - spray[i].source.vx = 0; - spray[i].maxLife = 300; // lifetime in frames - spray[i].minLife = 20; - spray[i].source.collide = true; // seeded particles will collide - spray[i].vx = 0; // emitting speed - spray[i].vy = 0; // emitting speed - spray[i].var = 10; + PartSys->sources[i].source.hue = random16(); + PartSys->sources[i].source.sat = 255; // set full saturation + PartSys->sources[i].source.x = (cols * PS_P_RADIUS) / (numSprays + 1) * (i + 1); + PartSys->sources[i].source.y = 5; // just above the lower edge, if zero, particles already 'bounce' at start and loose speed. + PartSys->sources[i].source.vx = 0; + PartSys->sources[i].maxLife = 300; // lifetime in frames + PartSys->sources[i].minLife = 20; + PartSys->sources[i].source.collide = true; // seeded particles will collide + PartSys->sources[i].vx = 0; // emitting speed + PartSys->sources[i].vy = 0; // emitting speed + PartSys->sources[i].var = 10; } } @@ -9351,10 +9245,10 @@ uint16_t mode_particlespray(void) { for (i = 0; i < numSprays; i++) { - spray[i].source.hue++; // = random16(); //change hue of spray source - // spray[i].var = SEGMENT.custom3; // emiting variation = nozzle size (custom 3 goes from 0-32) - spray[i].source.x = map(SEGMENT.custom1, 0, 255, 0, Max_x); - spray[i].source.y = map(SEGMENT.custom2, 0, 255, 0, Max_y); + PartSys->sources[i].source.hue++; // = random16(); //change hue of spray source + // PartSys->sources[i].var = SEGMENT.custom3; // emiting variation = nozzle size (custom 3 goes from 0-32) + PartSys->sources[i].source.x = map(SEGMENT.custom1, 0, 255, 0, Max_x); + PartSys->sources[i].source.y = map(SEGMENT.custom2, 0, 255, 0, Max_y); } i = 0; @@ -9439,7 +9333,7 @@ uint16_t mode_particleGEQ(void) PartSys->setBounceY(SEGMENT.check3); PartSys->enableParticleCollisions(false); PartSys->setWallHardness(SEGMENT.custom2); - PartSys->enableGravity(true, SEGMENT.custom3<<2); //set gravity strength + PartSys->enableGravity(true, SEGMENT.custom3<<1); //set gravity strength um_data_t *um_data; if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) @@ -9555,17 +9449,17 @@ uint16_t mode_particlecenterGEQ(void) } for (i = 0; i < numSprays; i++) { - spray[i].source.hue = i*16; //even color distribution - 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.hue = i*16; //even color distribution + PartSys->sources[i].source.sat = 255; // set saturation + PartSys->sources[i].source.x = (cols * PS_P_RADIUS) / 2; // center + PartSys->sources[i].source.y = (rows * PS_P_RADIUS) / 2; // center + PartSys->sources[i].source.vx = 0; + PartSys->sources[i].source.vy = 0; + PartSys->sources[i].maxLife = 400; + PartSys->sources[i].minLife = 200; + PartSys->sources[i].vx = 0; // emitting speed + PartSys->sources[i].vy = 0; // emitting speed + PartSys->sources[i].var = 0; // emitting variation } } @@ -9879,22 +9773,18 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_PARTICLEROTATINGSPRAY, &mode_particlerotatingspray, _data_FX_MODE_PARTICLEROTATINGSPRAY); + addEffect(FX_MODE_PARTICLEFIREWORKS, &mode_particlefireworks, _data_FX_MODE_PARTICLEFIREWORKS); addEffect(FX_MODE_PARTICLEFIRE, &mode_particlefire, _data_FX_MODE_PARTICLEFIRE); addEffect(FX_MODE_PARTICLEFALL, &mode_particlefall, _data_FX_MODE_PARTICLEFALL); addEffect(FX_MODE_PARTICLEIMPACT, &mode_particleimpact, _data_FX_MODE_PARTICLEIMPACT); addEffect(FX_MODE_PARTICLESGEQ, &mode_particleGEQ, _data_FX_MODE_PARTICLEGEQ); - /* addEffect(FX_MODE_PARTICLEVOLCANO, &mode_particlevolcano, _data_FX_MODE_PARTICLEVOLCANO); - - addEffect(FX_MODE_PARTICLEFIREWORKS, &mode_particlefireworks, _data_FX_MODE_PARTICLEFIREWORKS); + /* addEffect(FX_MODE_PARTICLEPERLIN, &mode_particleperlin, _data_FX_MODE_PARTICLEPERLIN); - addEffect(FX_MODE_PARTICLEBOX, &mode_particlebox, _data_FX_MODE_PARTICLEBOX); addEffect(FX_MODE_PARTICLEWATERFALL, &mode_particlewaterfall, _data_FX_MODE_PARTICLEWATERFALL); - addEffect(FX_MODE_PARTICLEATTRACTOR, &mode_particleattractor, _data_FX_MODE_PARTICLEATTRACTOR); addEffect(FX_MODE_PARTICLESPRAY, &mode_particlespray, _data_FX_MODE_PARTICLESPRAY); - */ // addEffect(FX_MODE_PARTICLECENTERGEQ, &mode_particlecenterGEQ, _data_FX_MODE_PARTICLECCIRCULARGEQ); diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index f49a0c198c..5aff415d4b 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -33,6 +33,7 @@ /* TODO: + -add function to 'update sources' so FX does not have to take care of that. FX can still implement its own version if so desired. config should be optional, if not set, use default config. -add SEGMENT.fill(BLACK); // clear the matrix into rendering function? -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?) @@ -52,19 +53,28 @@ ParticleSystem::ParticleSystem(uint16_t width, uint16_t height, uint16_t numberofparticles, uint16_t numberofsources) { - Serial.print("initializing PS... "); + Serial.println("PS Constructor"); numSources = numberofsources; numParticles = numberofparticles; // set number of particles in the array usedParticles = numberofparticles; // use all particles by default //particlesettings = {false, false, false, false, false, false, false, false}; // all settings off by default - initPSpointers(); // set the particle and sources pointer (call this before accessing sprays or particles) + updatePSpointers(); // 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; + /* + Serial.println("alive particles: "); for (int i = 0; i < numParticles; i++) { //particles[i].ttl = 0; //initialize all particles to dead - } + //if (particles[i].ttl) + { + Serial.print("x:"); + Serial.print(particles[i].x); + Serial.print(" y:"); + Serial.println(particles[i].y); + } + }*/ Serial.println("PS Constructor done"); } @@ -143,6 +153,11 @@ void ParticleSystem::setKillOutOfBounds(bool enable) particlesettings.killoutofbounds = enable; } +void ParticleSystem::setColorByAge(bool enable) +{ + particlesettings.colorByAge = 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) @@ -160,28 +175,6 @@ void ParticleSystem::enableParticleCollisions(bool enable, uint8_t hardness) // collisionHardness = hardness + 1; } - -// 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 - } - } -} - // emit one particle with variation void ParticleSystem::sprayEmit(PSsource &emitter) { @@ -194,8 +187,8 @@ void ParticleSystem::sprayEmit(PSsource &emitter) { particles[emitIndex].x = emitter.source.x; // + random16(emitter.var) - (emitter.var >> 1); //randomness uses cpu cycles and is almost invisible, removed for now. particles[emitIndex].y = emitter.source.y; // + random16(emitter.var) - (emitter.var >> 1); - particles[emitIndex].vx = emitter.vx + random16(emitter.var) - (emitter.var>>1); - particles[emitIndex].vy = emitter.vy + random16(emitter.var) - (emitter.var>>1); + particles[emitIndex].vx = emitter.vx + random(emitter.var) - (emitter.var>>1); + particles[emitIndex].vy = emitter.vy + random(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; @@ -216,18 +209,36 @@ void ParticleSystem::sprayEmit(PSsource &emitter) //Serial.println("**"); } +// 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<<1) - PS_P_RADIUS; // jitter the flame by one pixel to make the flames wider at the base + particles[emitIndex].y = emitter.source.y; + particles[emitIndex].vx = emitter.vx + random16(emitter.var) - (emitter.var >> 1); //random16 is good enough for fire and much faster + 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 + } + } +} + //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 -// angle = 0 means in x-direction -void ParticleSystem::angleEmit(PSsource &emitter, uint8_t angle, uint32_t speed) +// Emits a particle at given angle and speed, angle is from 0-65535 (=0-360deg), speed is also affected by emitter->var +// angle = 0 means in positive x-direction (i.e. to the right) +void ParticleSystem::angleEmit(PSsource &emitter, uint16_t angle, uint32_t speed) { - //todo: go to 16 bits, rotating particles could use this, others maybe as well. from rotating spray FX, angleoffset is the angle in 16bit - //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) - 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; + emitter.vx = ((int32_t)cos16(angle) * speed) >> 15; // cos16() and sin16() return signed 16bit + emitter.vy = ((int32_t)sin16(angle) * speed) >> 15; sprayEmit(emitter); } @@ -239,12 +250,14 @@ void ParticleSystem::particleMoveUpdate(PSparticle &part, PSsettings &options) { // age part.ttl--; - // apply velocity - //Serial.print("x:"); - //Serial.print(part.x); - //Serial.print("y:"); - //Serial.print(part.y); - int32_t newX = part.x + (int16_t)part.vx; + if (particlesettings.colorByAge) + part.hue = part.ttl > 255 ? 255 : part.ttl; //set color to ttl + // apply velocity + // Serial.print("x:"); + // Serial.print(part.x); + // Serial.print("y:"); + // Serial.print(part.y); + int32_t newX = part.x + (int16_t)part.vx; int32_t newY = part.y + (int16_t)part.vy; //Serial.print(" "); //Serial.print(newY); @@ -368,14 +381,14 @@ void ParticleSystem::applyForce(PSparticle *part, uint32_t numparticles, int8_t } } -// apply a force in angular direction to of particles +// apply a force in angular direction to group of particles //TODO: actually test if this works as expected, this is untested code // 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) +void ParticleSystem::applyAngleForce(PSparticle *part, uint32_t numparticles, uint8_t force, uint16_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) + int8_t xforce = ((int32_t)force * cos8(angle)) >> 15; // force is +/- 127 + int8_t yforce = ((int32_t)force * sin8(angle)) >> 15; + // noste: sin16 is 10% faster than sin8() on ESP32 but on ESP8266 it is 9% slower + // 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); } @@ -1010,16 +1023,13 @@ void ParticleSystem::handleCollisions() // takes two pointers to the particles to collide and the particle hardness (softer means more energy lost in collision, 255 means full hard) void ParticleSystem::collideParticles(PSparticle *particle1, PSparticle *particle2) //TODO: dx,dy is calculated just above, can pass it over here to save some CPU time { - int32_t dx = particle2->x - particle1->x; - int32_t dy = particle2->y - particle1->y; + int32_t dy = particle2->y - particle1->y; int32_t distanceSquared = dx * dx + dy * dy; - // Calculate relative velocity (if it is zero, could exit but extra check does not overall speed but deminish it) int32_t relativeVx = (int16_t)particle2->vx - (int16_t)particle1->vx; int32_t relativeVy = (int16_t)particle2->vy - (int16_t)particle1->vy; - if (distanceSquared == 0) // add distance in case particles exactly meet at center, prevents dotProduct=0 (this can only happen if they move towards each other) { distanceSquared++; @@ -1042,16 +1052,16 @@ void ParticleSystem::collideParticles(PSparticle *particle1, PSparticle *particl }*/ // Calculate dot product of relative velocity and relative distance int32_t dotProduct = (dx * relativeVx + dy * relativeVy); - + uint32_t notsorandom = dotProduct & 0x01; // random16(2); //dotprouct LSB should be somewhat random, so no need to calculate a random number // If particles are moving towards each other if (dotProduct < 0) { - const uint32_t bitshift = 16; // bitshift used to avoid floats (dx/dy are 7bit, relativV are 8bit -> dotproduct is 15bit so 16bit shift is ok) + const uint32_t bitshift = 15; // bitshift used to avoid floats (dx/dy are 7bit, relativV are 8bit -> dotproduct is 15bit so up to 16bit shift is ok) // Calculate new velocities after collision int32_t impulse = (((dotProduct << (bitshift)) / (distanceSquared)) * collisionHardness) >> 8; - int32_t ximpulse = (impulse * dx) >> bitshift; - int32_t yimpulse = (impulse * dy) >> bitshift; + int32_t ximpulse = 1 + (impulse * dx) >> bitshift; + int32_t yimpulse = 1 + (impulse * dy) >> bitshift; particle1->vx += ximpulse; particle1->vy += yimpulse; particle2->vx -= ximpulse; @@ -1067,11 +1077,11 @@ void ParticleSystem::collideParticles(PSparticle *particle1, PSparticle *particl //particle2->vx = (particle2->vx < 2 && particle2->vx > -2) ? 0 : particle2->vx; //particle2->vy = (particle2->vy < 2 && particle2->vy > -2) ? 0 : particle2->vy; - const uint32_t coeff = 191 + (collisionHardness>>1); - particle1->vx = ((int32_t)particle1->vx * coeff) >> 8; + const uint32_t coeff = 247 + (collisionHardness>>4); + particle1->vx = ((((int32_t)particle1->vx + notsorandom) * coeff) >> 8); // add 1 sometimes to balance favour to left side TODO: did this really fix it? particle1->vy = ((int32_t)particle1->vy * coeff) >> 8; - particle2->vx = ((int32_t)particle2->vx * coeff) >> 8; + particle2->vx = (((int32_t)particle2->vx * coeff) >> 8); particle2->vy = ((int32_t)particle2->vy * coeff) >> 8; } } @@ -1082,7 +1092,7 @@ void ParticleSystem::collideParticles(PSparticle *particle1, PSparticle *particl // one problem remaining is, particles get squished if (external) force applied is higher than the pushback but this may also be desirable if particles are soft. also some oscillations cannot be avoided without addigng a counter if (distanceSquared < 2 * PS_P_HARDRADIUS * PS_P_HARDRADIUS) { - uint8_t choice = dotProduct & 0x01; // random16(2); // randomly choose one particle to push, avoids oscillations note: dotprouct LSB should be somewhat random, so no need to calculate a random number + const int32_t HARDDIAMETER = 2 * PS_P_HARDRADIUS; const int32_t pushamount = 1; // push a small amount int32_t push = pushamount; @@ -1093,7 +1103,7 @@ void ParticleSystem::collideParticles(PSparticle *particle1, PSparticle *particl if (dx < 0) push = -pushamount; //-(PS_P_HARDRADIUS + dx); // inverted push direction - if (choice) // chose one of the particles to push, avoids oscillations + if (notsorandom) // chose one of the particles to push, avoids oscillations particle1->x -= push; else particle2->x += push; @@ -1105,7 +1115,7 @@ void ParticleSystem::collideParticles(PSparticle *particle1, PSparticle *particl if (dy < 0) push = -pushamount; //-(PS_P_HARDRADIUS + dy); // inverted push direction - if (choice) // chose one of the particles to push, avoids oscillations + if (notsorandom) // chose one of the particles to push, avoids oscillations particle1->y -= push; else particle2->y += push; @@ -1175,19 +1185,18 @@ CRGB **ParticleSystem::allocate2Dbuffer(uint32_t cols, uint32_t rows) // 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::initPSpointers() +void ParticleSystem::updatePSpointers() { - Serial.print("this "); - Serial.println((uintptr_t)this); - particles = reinterpret_cast(this + 1); // pointer to particle array sizeof(ParticleSystem) + DEBUG_PRINT(F("*** PS pointers ***")); + DEBUG_PRINTF_P(PSTR("this PS %p\n"), this); + + particles = reinterpret_cast(this + 1); // pointer to particle array at data+sizeof(ParticleSystem) sources = reinterpret_cast(particles + numParticles); // pointer to source(s) PSdataEnd = reinterpret_cast(sources + numSources); // pointer to first available byte after the PS - Serial.print("particles "); - Serial.println((uintptr_t)particles); - Serial.print("sources "); - Serial.println((uintptr_t)sources); - Serial.print("end "); - Serial.println((uintptr_t)PSdataEnd); + + DEBUG_PRINTF_P(PSTR("particles %p\n"), particles); + DEBUG_PRINTF_P(PSTR("sources %p\n"), sources); + DEBUG_PRINTF_P(PSTR("end %p\n"), PSdataEnd); } //non class functions to use for initialization @@ -1197,20 +1206,16 @@ 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 * 3)>>2 ; // 0.75 particle per pixel + uint numberofParticles = (cols * rows * 3)>>2 ; // 0.75 particle per pixel + uint particlelimit = 148; //maximum number of paticles allowed (based on one segment of 16x16 and 4k effect ram) #elseif ARDUINO_ARCH_ESP32S2 - uint32_t numberofParticles = (cols * rows); // 1 particle per pixel + uint numberofParticles = (cols * rows); // 1 particle per pixe + uint particlelimit = 768; // maximum number of paticles allowed (based on one segment of 32x32 and 24k effect ram) #else - uint32_t numberofParticles = (cols * rows * 3) / 2; // 1.5 particles per pixel (for example 768 particles on 32x16) + uint numberofParticles = (cols * rows * 3) / 2; // 1.5 particles per pixel (for example 768 particles on 32x16) + uint particlelimit = 1280; // maximum number of paticles allowed (based on two segments of 32x32 and 40k effect ram) #endif - - Serial.print("segsize "); - Serial.print(cols); - Serial.print(" "); - Serial.print(" "); - Serial.print("particles: "); - Serial.println(numberofParticles); - // TODO: ist das genug für fire auf 32x16? evtl auf 2 gehen? oder das dynamisch machen, als parameter? + numberofParticles = max((uint)1, min(numberofParticles, particlelimit)); return numberofParticles; } @@ -1220,15 +1225,14 @@ uint32_t calculateNumberOfSources() uint32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); #ifdef ESP8266 int numberofSources = (cols * rows) / 8; - numberofSources = min(numberofSources, 16); + numberofSources = max(1 , min(numberofSources, 16)); //limit to 1 - 16 #elseif ARDUINO_ARCH_ESP32S2 int numberofSources = (cols * rows) / 6; - numberofSources = min(numberofSources, 32); + numberofSources = max(1, min(numberofSources, 48)); // limit to 1 - 48 #else int numberofSources = (cols * rows) / 4; - numberofSources = min(numberofSources, 64); -#endif - // TODO: may be too many.. + numberofSources = max(1 , min(numberofSources, 72)); //limit to 1 - 72 +#endif return numberofSources; } @@ -1239,9 +1243,11 @@ bool allocateParticleSystemMemory(uint16_t numparticles, uint16_t numsources, ui requiredmemory += sizeof(PSparticle) * numparticles; requiredmemory += sizeof(PSsource) * numsources; requiredmemory += additionalbytes; - Serial.print("allocatin: "); + Serial.print("allocating: "); Serial.print(requiredmemory); - Serial.print("Bytes"); + Serial.println("Bytes"); + Serial.print("allocating for segment at"); + Serial.println((uintptr_t)SEGMENT.data); return(SEGMENT.allocateData(requiredmemory)); } @@ -1256,7 +1262,7 @@ bool initParticleSystem(ParticleSystem *&PartSys, uint16_t additionalbytes) DEBUG_PRINT(F("PS init failed: memory depleted")); return false; } - Serial.print("segment data ptr"); + Serial.print("segment.data ptr"); Serial.println((uintptr_t)(SEGMENT.data)); uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index 76d4d201ac..4982224352 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -46,13 +46,13 @@ typedef struct { uint8_t hue; // color hue uint8_t sat; // color saturation //two byte bit field: - //uint16_t ttl : 12; // time to live, 12 bit or 4095 max (which is 50s at 80FPS) + uint16_t ttl : 12; // time to live, 12 bit or 4095 max (which is 50s at 80FPS) bool outofbounds : 1; //out of bounds flag, set to true if particle is outside of display area bool collide : 1; //if set, particle takes part in collisions bool flag3 : 1; // unused flags... bool flag4 : 1; - uint16_t ttl; // time to live, 12 bit or 4095 max (which is 50s at 80FPS) + //uint16_t ttl; // time to live, 12 bit or 4095 max (which is 50s at 80FPS) } PSparticle; //struct for a particle source @@ -76,7 +76,7 @@ typedef struct 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 + bool colorByAge : 1; // if set, particle hue is set by ttl value } PSsettings; class ParticleSystem @@ -86,11 +86,12 @@ class ParticleSystem // note: memory is allcated in the FX function, no deconstructor needed void update(void); //update the particles according to set options and render to the matrix void updateFire(uint32_t intensity, bool usepalette); // update function for fire + void updatePSpointers(); // update the data pointers to current segment data space // particle emitters void flameEmit(PSsource &emitter); void sprayEmit(PSsource &emitter); - void angleEmit(PSsource& emitter, uint8_t angle, uint32_t speed); + void angleEmit(PSsource& emitter, uint16_t angle, uint32_t speed); //move functions void particleMoveUpdate(PSparticle &part, PSsettings &options); @@ -100,7 +101,7 @@ class ParticleSystem void applyGravity(PSparticle *part, uint32_t numarticles, uint8_t *counter); //use global gforce void applyGravity(PSparticle *part); //use global system settings void applyForce(PSparticle *part, uint32_t numparticles, int8_t xforce, int8_t yforce, uint8_t *counter); - void applyAngleForce(PSparticle *part, uint32_t numparticles, uint8_t force, uint8_t angle, uint8_t *counter); + void applyAngleForce(PSparticle *part, uint32_t numparticles, uint8_t force, uint16_t angle, uint8_t *counter); void applyFriction(PSparticle *part, uint8_t coefficient); // apply friction to specific particle void applyFriction(uint8_t coefficient); // apply friction to all used particles void attract(PSparticle *particle, PSparticle *attractor, uint8_t *counter, uint8_t strength, bool swallow); @@ -115,6 +116,7 @@ class ParticleSystem void setBounceX(bool enable); void setBounceY(bool enable); void setKillOutOfBounds(bool enable); //if enabled, particles outside of matrix instantly die + void setColorByAge(bool enable); void enableGravity(bool enable, uint8_t force = 8); void enableParticleCollisions(bool enable, uint8_t hardness = 255); @@ -126,7 +128,7 @@ class ParticleSystem uint8_t numSources; //number of sources uint16_t numParticles; // number of particles available in this system uint16_t usedParticles; // number of particles used in animation (can be smaller then numParticles) - PSsettings particlesettings; // settings used when updating particles (can also used by FX to move sources) + PSsettings particlesettings; // settings used when updating particles (can also used by FX to move sources), do not edit properties directly, use functions above private: //rendering functions @@ -141,7 +143,6 @@ class ParticleSystem void fireParticleupdate(); //utility functions - void initPSpointers(); // call this after allocating the memory to initialize the pointers int32_t wraparound(int32_t w, int32_t maxvalue); int32_t calcForce_dV(int8_t force, uint8_t *counter); CRGB **allocate2Dbuffer(uint32_t cols, uint32_t rows); From 32343eaa757de7656cf0814e9a0f16eabb3feb7d Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Tue, 2 Apr 2024 20:26:24 +0200 Subject: [PATCH 055/219] updated fire, added some functions to PS, ported attractor FX - added turbulance to fire (after hours of fine-tuning) it now looks even more awesome - added attractor animation back in and improved it with more functionality - fixed the attractor function in the PS - renamed FX: 'candy' is now called 'vortex' - added new force function to apply a force immediately (and handle the timing in the FX) - added update function to PS for size update to handle dynamic segment size change - made waterfall width dynamic on segment width - removed some debug stuff - added #defines for maximum number of particles/sprays - updated fire parameter to make it look better on ESP8266 - some little bugfixes --- wled00/FX.cpp | 871 ++++++++++++++---------------------- wled00/FX_fcn.cpp | 2 +- wled00/FXparticleSystem.cpp | 152 ++++--- wled00/FXparticleSystem.h | 27 +- 4 files changed, 441 insertions(+), 611 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 065ff95c00..c4ede81734 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -7882,7 +7882,7 @@ uint16_t mode_2Dwavingcell() { static const char _data_FX_MODE_2DWAVINGCELL[] PROGMEM = "Waving Cell@!,,Amplitude 1,Amplitude 2,Amplitude 3;;!;2"; /* - * Particle System Candy (aka rotating sprays) + * Particle System Vortex (aka rotating sprays) * Particles sprayed from center with a rotating spray * Uses palette for particle color * by DedeHai (Damian Schneider) @@ -7934,53 +7934,19 @@ uint16_t mode_particlerotatingspray(void) } } PartSys->setKillOutOfBounds(true); - - DEBUG_PRINTF_P(PSTR("segment data ptr in candy FX %p\n"), SEGMENT.data); - Serial.println("INIT done"); } else PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + if (PartSys == NULL) + { + DEBUG_PRINT(F("ERROR: FX PartSys nullpointer")); + return mode_static(); // something went wrong, no data! + } //DEBUG_PRINTF_P(PSTR("segment data ptr in candy FX %p\n"), SEGMENT.data); - PartSys->updatePSpointers(); // update data pointers (needed in case our memory was moved) - + PartSys->updateSystem(); // update system properties (dimensions and data pointers) numSprays = min(PartSys->numSources, (uint8_t)8); - /* - if (SEGMENT.call < 3) - { - Serial.print("segment data ptr in candyFX"); - Serial.println((uintptr_t)(SEGMENT.data)); - }*/ - //Serial.print("rotPSP "); - //Serial.println((uintptr_t)PartSys); - 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) - } - //!!! - for (i = 0; i < numSprays; i++) - { - 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; - } - } - //!!! if (SEGMENT.check1 != (SEGMENT.aux1 & 0x01)) //state change { if (SEGMENT.check1) @@ -8002,7 +7968,6 @@ uint16_t mode_particlerotatingspray(void) } } - //set rotation direction and speed int32_t rotationspeed = SEGMENT.speed << 2; bool direction = SEGMENT.check2; @@ -8032,29 +7997,7 @@ uint16_t mode_particlerotatingspray(void) else SEGMENT.aux0 -= rotationspeed << 2; -/* -//DEBUG: emit single particles in x,y - if (SEGMENT.call % 3000 == 0) - { - 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 + // calculate angle offset for an even distribution uint16_t angleoffset = 0xFFFF / spraycount; for (j = 0; j < spraycount; j++) @@ -8065,12 +8008,9 @@ uint16_t mode_particlerotatingspray(void) PartSys->sources[j].var = (SEGMENT.custom3 >> 1); // emiting variation = nozzle size (custom 3 goes from 0-32) } - //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) { #ifdef ESP8266 @@ -8080,13 +8020,10 @@ uint16_t mode_particlerotatingspray(void) j = (j + 1) % spraycount; } - SEGMENT.fill(BLACK); // clear the matrix - 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"; +static const char _data_FX_MODE_PARTICLEROTATINGSPRAY[] PROGMEM = "PS Vortex@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"; /* * Particle Fireworks @@ -8121,10 +8058,10 @@ uint16_t mode_particlefireworks(void) if (PartSys == NULL) { - Serial.println("ERROR: paticle system not found, nullpointer"); + DEBUG_PRINT(F("ERROR: FX PartSys nullpointer")); return mode_static(); // something went wrong, no data! } - PartSys->updatePSpointers(); //update data pointers (needed in case our memory was moved) + PartSys->updateSystem(); // update system properties (dimensions and data pointers) numRockets = min(PartSys->numSources, (uint8_t)4); PartSys->setWrapX(SEGMENT.check1); @@ -8133,7 +8070,7 @@ uint16_t mode_particlefireworks(void) PartSys->enableGravity(true, map(SEGMENT.custom3,0,31,0,10)); // todo: make it a slider to adjust // check each rocket's state and emit particles according to its state: moving up = emit exhaust, at top = explode; falling down = standby time - int32_t emitparticles; // number of particles to emit for each rocket's state + uint32_t emitparticles; // number of particles to emit for each rocket's state // variables for circle explosions uint8_t speed; @@ -8174,11 +8111,7 @@ uint16_t mode_particlefireworks(void) speedvariation = angle & 0x01; // 0 or 1, no need for a new random number // calculate the number of particles to make complete circles percircle = (uint16_t)0xFFFF / angleincrement + 1; - #ifdef ESP8266 //TODO: this line is untested on ESP8266 - int circles = (SEGMENT.intensity >> 7) + 1; - #else int circles = (SEGMENT.intensity >> 6) + 1; - #endif emitparticles = percircle * circles; PartSys->sources[j].var = 0; //no variation for nicer circles } @@ -8191,7 +8124,7 @@ uint16_t mode_particlefireworks(void) currentspeed = speed - speedvariation; else currentspeed = speed; - PartSys->angleEmit(PartSys->sources[j], angle, currentspeed); + PartSys->angleEmit(PartSys->sources[j], angle, currentspeed); //note: compiler warnings can be ignored, variables are set just above counter++; if (counter > percircle) // full circle completed, increase speed { @@ -8252,12 +8185,9 @@ uint16_t mode_particlefireworks(void) } } - SEGMENT.fill(BLACK); // clear the matrix PartSys->update(); // update and render 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,Gravity,Cylinder,Ground,;;!;012;pal=11,sx=100,ix=50,c1=84,c2=128,c3=12,o1=0,o2=0,o3=0"; /* @@ -8266,8 +8196,6 @@ static const char _data_FX_MODE_PARTICLEFIREWORKS[] PROGMEM = "PS Fireworks@Laun * Uses palette for particle color * by DedeHai (Damian Schneider) */ - - uint16_t mode_particlevolcano(void) { if (SEGLEN == 1) @@ -8275,7 +8203,6 @@ uint16_t mode_particlevolcano(void) ParticleSystem *PartSys = NULL; uint8_t numSprays; uint32_t i = 0; - uint32_t j = 0; if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. { @@ -8301,14 +8228,14 @@ uint16_t mode_particlevolcano(void) 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) + DEBUG_PRINT(F("ERROR: FX PartSys nullpointer")); + return mode_static(); // something went wrong, no data! } numSprays = min(PartSys->numSources, (uint8_t)1); // number of sprays // Particle System settings - PartSys->updatePSpointers(); // update data pointers (needed in case our memory was moved) + PartSys->updateSystem(); // update system properties (dimensions and data pointers) PartSys->setColorByAge(SEGMENT.check1); PartSys->setBounceX(SEGMENT.check2); PartSys->setWallHardness(SEGMENT.custom2); @@ -8339,23 +8266,16 @@ uint16_t mode_particlevolcano(void) } } - SEGMENT.fill(BLACK); // clear the matrix PartSys->update(); // update and render return FRAMETIME; } static const char _data_FX_MODE_PARTICLEVOLCANO[] PROGMEM = "PS Volcano@Speed,Intensity,Move,Bounce,Spread,Color by Age,Walls,Collisions;;!;012;pal=35,sx=100,ix=160,c1=0,c2=160,c3=10,o1=1,o2=0,o3=0"; /* - * Particle Fire - * realistic fire effect using particles. heat based and using perlin-noise for wind - * by DedeHai (Damian Schneider) - */ - - // TODO: - // do not use width to calculate numflames. it changes when mirror/transpose is activated, so will crash if height>width and then transposed - // if using width*height and assuming a square, it will look sparse on a wide matrix... - // could just allocate way too many and then dynamically adjust at at the expense of ram usage (but flames only use about 16bytes so is ok) - // TODO: add 2D perlin noise to add to flame speed for randomness? may look good, may look awful, test it. also may hit FPS hard. + * Particle Fire + * realistic fire effect using particles. heat based and using perlin-noise for wind + * by DedeHai (Damian Schneider) + */ uint16_t mode_particlefire(void) { if (SEGLEN == 1) @@ -8372,10 +8292,10 @@ uint16_t mode_particlefire(void) Serial.println("fireinit done"); SEGMENT.aux0 = rand(); // aux0 is wind position (index) in the perlin noise // initialize the flame sprays - numFlames = PartSys->numSources; + numFlames = PartSys->numSources; for (i = 0; i < numFlames; i++) { - PartSys->sources[i].source.ttl = 1000; + PartSys->sources[i].source.ttl = 0; PartSys->sources[i].source.vx = 0; // emitter moving speed; PartSys->sources[i].source.vy = 0; PartSys->sources[i].source.sat = 255; //!!!debug, test if transitions are better (non white) @@ -8386,25 +8306,19 @@ uint16_t mode_particlefire(void) } 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) + DEBUG_PRINT(F("ERROR: FX PartSys nullpointer")); + return mode_static(); // something went wrong, no data! } - DEBUG_PRINTF_P(PSTR("segment data ptr in Fire FX %p\n"), SEGMENT.data); - PartSys->updatePSpointers(); // update data pointers (needed in case our memory was moved) + // DEBUG_PRINTF_P(PSTR("segment data ptr in Fire FX %p\n"), SEGMENT.data); + PartSys->updateSystem(); // update system properties (dimensions and data pointers) uint32_t spread = (PartSys->maxX >> 5) * (SEGMENT.custom3 + 1); //fire around segment center (in subpixel points) - numFlames = min((uint)PartSys->numSources, 1+((spread/PS_P_RADIUS)<<1)); // number of flames used depends on spread with, good value is (fire width in pixel) * 2 - uint32_t percycle = numFlames / 2; // maximum number of particles emitted per cycle - /* - if (SEGMENT.call < 3) - { - Serial.print("segment data ptr in fireFX"); - Serial.println((uintptr_t)(SEGMENT.data)); - }*/ - + numFlames = min((uint)PartSys->numSources, (1 + ((spread / PS_P_RADIUS) << 1))); // number of flames used depends on spread with, good value is (fire width in pixel) * 2 + uint32_t percycle = numFlames*2/3;// / 2; // maximum number of particles emitted per cycle (TODO: for ESP826 maybe use flames/2) PartSys->setWrapX(SEGMENT.check2); // update the flame sprays: @@ -8418,15 +8332,13 @@ uint16_t mode_particlefire(void) { // initialize new flame: set properties of source if (random16(20) == 0 || SEGMENT.call == 0) // from time to time, change flame position - { - //!!!remove if done PartSys->sources[i].source.x = ((rand() % (PartSys->maxX>>1)) + (PartSys->maxX>>2)); // distribute randomly on center half corners + { PartSys->sources[i].source.x = (PartSys->maxX >> 1) - (spread>>1) + (rand() % spread) ; // distribute randomly on chosen width } PartSys->sources[i].source.y = -PS_P_RADIUS; // set the source below the frame - PartSys->sources[i].source.ttl = 1 + random16((SEGMENT.custom1 * SEGMENT.custom1) >> 8) / (1 + (SEGMENT.speed >> 5)); //'hotness' of fire, faster flames reduce the effect or flame height will scale too much with speed - //!!! remove when done PartSys->sources[i].source.ttl = (rand() % ((SEGMENT.intensity * SEGMENT.intensity) >> 9) / (1 + (SEGMENT.speed >> 6))) + 10; //'hotness' of fire, faster flames reduce the effect or flame height will scale too much with speed - //!!! remove when done PartSys->sources[i].source.ttl = random16(SEGMENT.intensity+10) + 5; + //PartSys->sources[i].source.ttl = 10 + random16((SEGMENT.custom1 * SEGMENT.custom1) >> 8) / (1 + (SEGMENT.speed >> 5)); //'hotness' of fire, faster flames reduce the effect or flame height will scale too much with speed + PartSys->sources[i].source.ttl = 5 + random16((SEGMENT.custom1 * SEGMENT.custom1) >> 7) / (2 + (SEGMENT.speed >> 4)); //'hotness' of fire, faster flames reduce the effect or flame height will scale too much with speed PartSys->sources[i].maxLife = random16(7) + 13; // defines flame height together with the vy speed, vy speed*maxlife/PS_P_RADIUS is the average flame height PartSys->sources[i].minLife = 4; PartSys->sources[i].vx = (int8_t)random16(4) - 2; // emitting speed (sideways) @@ -8436,35 +8348,59 @@ uint16_t mode_particlefire(void) } // percycle = map(SEGMENT.intensity,0,255, 2, (numFlames*3) / 2); //TODO: will this give better flames or worse? - if (SEGMENT.call & 0x01) // update noise position every second frames + if (SEGMENT.call & 0x01) // update noise position every second frames, also add wind { SEGMENT.aux0++; // position in the perlin noise matrix for wind generation if (SEGMENT.call & 0x02) //every tird frame SEGMENT.aux1++; // move in noise y direction so noise does not repeat as often + + // add wind force to all particles + int8_t windspeed = ((int16_t)(inoise8(SEGMENT.aux0, SEGMENT.aux1) - 127) * SEGMENT.custom2) >> 12; + PartSys->applyForce(PartSys->particles, PartSys->usedParticles, windspeed, 0); } - int8_t windspeed = (int8_t)(inoise8(SEGMENT.aux0,SEGMENT.aux1) - 127) / ((271 - SEGMENT.custom2) >> 4); + SEGMENT.step++; - // update particles, create particles - uint8_t j = random16(numFlames); // start with a random flame (so each flame gets the chance to emit a particle if perCycle is smaller than number of flames) +//this is a work in progress, maybe will be added... hard to find settings that look good TODO: looks ok on speed 130, need to tune it for other speeds + if (SEGMENT.check3) + { + if (SEGMENT.call % map(SEGMENT.speed,0,255,4,15)==0) // update noise position every xth frames, also add wind -> has do be according to speed. 135-> every third frame + { + for (i = 0; i < PartSys->usedParticles; i++) + { + //if (PartSys->particles[i].y > (PS_P_RADIUS << 1) && PartSys->particles[i].y < PartSys->maxY - (PS_P_RADIUS * 4)) // do not apply turbulance everywhere + if (PartSys->particles[i].y < PartSys->maxY/4)// - (PS_P_RADIUS * 4)) // do not apply turbulance everywhere -> bottom quarter seems a good balance + { + //int32_t curl = ((int16_t)inoise8(PartSys->particles[i].x , PartSys->particles[i].y >> 1, SEGMENT.step<<2 ) - 127); + //int32_t curl = ((int16_t)inoise8(PartSys->particles[i].x, PartSys->particles[i].y , SEGMENT.step << 4) - 127); + int32_t curl = ((int16_t)inoise8(PartSys->particles[i].x, PartSys->particles[i].y , SEGMENT.step << 4) - 127); //-> this is good! + + //int32_t curl = ((int16_t)inoise8(PartSys->particles[i].x>>1, SEGMENT.step<<5) - 127); + // curl = ((curl * PartSys->particles[i].y) / PartSys->maxY); //'curl' stronger at the top + //int modulation = inoise8(SEGMENT.step<<3, SEGMENT.aux1) ; + //PartSys->particles[i].vx += curl>>2; + //PartSys->particles[i].vy += curl>>3; + //PartSys->particles[i].vx += (curl * ((SEGMENT.custom2 * modulation)>>7)) >> 9; + //PartSys->particles[i].vy += ((curl ) * ((SEGMENT.custom2 * modulation)>>7))>>10; + //PartSys->particles[i].vx += (curl * curl * (SEGMENT.speed+10)) >> 14; //this may be a bad idea -> yes, squre is always positive... and a bit strong + PartSys->particles[i].vx += (curl * (SEGMENT.speed + 10)) >> 9; //-> this is not too bad! + // PartSys->particles[i].vy += (curl * SEGMENT.custom2 ) >> 13; + } + } + } + } + uint8_t j = random16(numFlames); // start with a random flame (so each flame gets the chance to emit a particle if available particles is smaller than number of flames) for(i=0; i < percycle; i++) { PartSys->flameEmit(PartSys->sources[j]); j = (j + 1) % numFlames; } - /* //TODO: add wind back in, may need a PS function to add a constant velocity to all particles (or can use force but is slower. or just do it here may be the fastest way) - else if (PartSys->particles[i].y > PS_P_RADIUS) // particle is alive, apply wind if y > 1 - { - // add wind using perlin noise - PartSys->particles[i].vx = windspeed; //todo: should this be depending on position? would be slower but may look better (used in old, slow fire) - }*/ - - SEGMENT.fill(BLACK); // clear the matrix - PartSys->updateFire(SEGMENT.intensity, SEGMENT.check1); // update and render the fire, colormode is determined by custom 3 slider + + PartSys->updateFire(SEGMENT.intensity, true); //SEGMENT.check1); // update and render the fire, colormode is determined by custom 3 slider return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEFIRE[] PROGMEM = "PS Fire@Speed,Intensity,Height,Wind,Spread,Palette,Cylinder;;!;035;sx=130,ix=120,c1=110,c2=128,c3=22,o1=0"; +static const char _data_FX_MODE_PARTICLEFIRE[] PROGMEM = "PS Fire@Speed,Intensity,Height,Wind,Spread,Palette,Cylinder,Turbulence;;!;035;sx=130,ix=120,c1=110,c2=128,c3=22,o1=0"; /* PS Ballpit: particles falling down, user can enable these three options: X-wraparound, side bounce, ground bounce @@ -8473,7 +8409,6 @@ 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) @@ -8494,10 +8429,10 @@ uint16_t mode_particlefall(void) 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) + DEBUG_PRINT(F("ERROR: FX PartSys nullpointer")); + return mode_static(); // something went wrong, no data! } - PartSys->updatePSpointers(); // update data pointers (needed in case our memory was moved) + PartSys->updateSystem(); // update system properties (dimensions and data pointers) PartSys->setWrapX(SEGMENT.check1); PartSys->setBounceX(SEGMENT.check2); PartSys->setBounceY(SEGMENT.check3); @@ -8520,7 +8455,7 @@ uint16_t mode_particlefall(void) { // emit particle at random position over the top of the matrix (random16 is not random enough) PartSys->particles[i].ttl = 1500 - (SEGMENT.speed << 2) + random16(500); // if speed is higher, make them die sooner - PartSys->particles[i].x = random(PartSys->maxX >> 1) + PartSys->maxX >> 2; + PartSys->particles[i].x = random(PartSys->maxX >> 1) + (PartSys->maxX >> 2); PartSys->particles[i].y = (PartSys->maxY<<1); // particles appear somewhere above the matrix, maximum is double the height PartSys->particles[i].vx = (((int16_t)random(SEGMENT.custom1)) - (SEGMENT.custom1 >> 1)+5) >> 1; // side speed is +/- a quarter of the custom1 slider PartSys->particles[i].vy = map(SEGMENT.speed, 0, 255, -5, -100); // downward speed @@ -8532,43 +8467,6 @@ uint16_t mode_particlefall(void) } } - - //!!! - /* - i=0; - // if (SEGMENT.call % 2 == 0) // every nth frame emit particles, stop emitting if set to zero - { - - - uint8_t emit = 5; - //i = random() % PartSys->usedParticles; - while (i < PartSys->usedParticles) // emit particles - { - if (PartSys->particles[i].ttl == 0) // find a dead particle - { - Serial.print(" i="); - Serial.println(i); - int32_t x = random16((PartSys->maxX << 1)) - (PartSys->maxX >> 1); - int32_t y = random16((PartSys->maxY << 1)) - (PartSys->maxY >> 1); - - PartSys->particles[i].x = x; - PartSys->particles[i].y = y; - //PartSys->particles[i].x = random16((PartSys->maxX) >> 1); - //PartSys->particles[i].y = random16((PartSys->maxY) >> 1); - PartSys->particles[i].vx = random16(10) - 5; - PartSys->particles[i].vy = random16(10) - 5; - PartSys->particles[i].ttl = 3000; - PartSys->particles[i].hue = random16(); // set random color - PartSys->particles[i].sat = 255; - if(emit-- == 0) - break; - } - i++; - } - } - */ - //!!! - uint32_t frictioncoefficient = 1; if (SEGMENT.speed < 50) // for low speeds, apply more friction frictioncoefficient = 50 - SEGMENT.speed; @@ -8576,7 +8474,6 @@ uint16_t mode_particlefall(void) if (SEGMENT.call % 3 == 0) PartSys->applyFriction(frictioncoefficient); // add some frictino to help smooth things - SEGMENT.fill(BLACK); // clear the matrix PartSys->update(); // update and render return FRAMETIME; @@ -8588,282 +8485,227 @@ static const char _data_FX_MODE_PARTICLEFALL[] PROGMEM = "PS Ballpit@Speed,Inten * 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(); + ParticleSystem *PartSys = NULL; + uint8_t numSprays; + uint32_t i = 0; - const uint32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; - const uint32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); - + if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. + { + if (!initParticleSystem(PartSys, 0)) // init, no additional data needed + return mode_static(); // allocation failed; //allocation failed + PartSys->enableGravity(true); // enable with default gforce + PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting) + numSprays = min((uint32_t)PartSys->numSources, min(PartSys->maxXpixel/5, (uint32_t)2)); // number of sprays + for (i = 0; i < numSprays; i++) + { + PartSys->sources[i].source.hue = random16(); + PartSys->sources[i].source.sat = 255; // set full saturation + PartSys->sources[i].source.vx = 0; + PartSys->sources[i].source.collide = true; // seeded particles will collide #ifdef ESP8266 - const uint32_t numParticles = 100; // maximum number of particles - const uint8_t numSprays = 1; + PartSys->sources[i].maxLife = 250; // lifetime in frames (ESP8266 has less particles, make them short lived to keep the water flowing) + PartSys->sources[i].minLife = 100; #else - const uint32_t numParticles = 500; // maximum number of particles - const uint8_t numSprays = 2; + PartSys->sources[i].maxLife = 400; // lifetime in frames + PartSys->sources[i].minLife = 150; #endif + PartSys->sources[i].vx = 0; // emitting speed + PartSys->sources[i].var = 7; // emiting variation + } + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS - uint8_t percycle = numSprays; // maximum number of particles emitted per cycle - - PSparticle *particles; - PSsource *spray; - - // allocate memory and divide it into proper pointers, max is 32k for all segments. - uint32_t dataSize = sizeof(PSparticle) * numParticles; - dataSize += sizeof(PSsource) * (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 - - uint32_t i = 0; - uint32_t j = 0; + 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) + } + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setWrapX(SEGMENT.check1); // cylinder + PartSys->setBounceX(SEGMENT.check2); // walls + PartSys->setBounceY(SEGMENT.check3); // ground + PartSys->setWallHardness(SEGMENT.custom2); + numSprays = min((uint32_t)PartSys->numSources, min(PartSys->maxXpixel / 5, (uint32_t)2)); // number of sprays depends on segment width - if (SEGMENT.call == 0) // initialization + if (SEGMENT.custom2 > 0) // collisions enabled + PartSys->enableParticleCollisions(true, SEGMENT.custom2); // enable collisions and set particle collision hardness + else { - for (i = 0; i < numParticles; i++) - { - particles[i].ttl = 0; - } - for (i = 0; i < numSprays; i++) - { - PartSys->sources[i].source.hue = random16(); - PartSys->sources[i].source.sat = 255; // set full saturation - PartSys->sources[i].source.x = (cols * PS_P_RADIUS) / 2 - PS_P_RADIUS + 2*PS_P_RADIUS * (i); - PartSys->sources[i].source.y = (rows+4) * (PS_P_RADIUS*(i+1)); // source y position, few pixels above the top to increase spreading before entering the matrix - PartSys->sources[i].source.vx = 0; - PartSys->sources[i].source.collide = true; // seeded particles will collide - #ifdef ESP8266 - PartSys->sources[i].maxLife = 100; // lifetime in frames - PartSys->sources[i].minLife = 50; - #else - PartSys->sources[i].maxLife = 400; // lifetime in frames - PartSys->sources[i].minLife = 150; - #endif - PartSys->sources[i].vx = 0; // emitting speed - PartSys->sources[i].var = 7; // emiting variation - } + PartSys->enableParticleCollisions(false); + PartSys->setWallHardness(120); // set hardness (for ground bounce) to fixed value if not using collisions } - // change source emitting color for (i = 0; i < numSprays; i++) { PartSys->sources[i].source.hue++; //change hue of spray source } - uint8_t intensity = SEGMENT.intensity; - - if (SEGMENT.call % (9 - (intensity >> 5)) == 0 && intensity > 0) // every nth frame, cycle color and emit particles, do not emit if intensity is zero + if (SEGMENT.call % (9 - (SEGMENT.intensity >> 5)) == 0 && SEGMENT.intensity > 0) // every nth frame, cycle color and emit particles, do not emit if intensity is zero + { + for (i = 0; i < numSprays; i++) { - - for (i = 0; i < numSprays; i++) - { - PartSys->sources[i].vy = -SEGMENT.speed >> 3; // emitting speed, down - PartSys->sources[i].source.x = map(SEGMENT.custom3, 0, 31, 0, (cols - 2) * PS_P_RADIUS) + i * PS_P_RADIUS * 2; // emitter position - PartSys->sources[i].source.ttl = 255; // source never dies, replenish its lifespan - PartSys->sources[i].var = SEGMENT.custom1 >> 3; // emiting variation 0-32 - } - - i = 0; - j = 0; - while (i < numParticles) - { - if (particles[i].ttl == 0) // find a dead particle - { - Emitter_Fountain_emit(&spray[j], &particles[i]); - j = (j + 1) % numSprays; - if (percycle-- == 0) - { - break; // quit loop if all particles of this round emitted - } - } - i++; - } + PartSys->sources[i].vy = -SEGMENT.speed >> 3; // emitting speed, down + PartSys->sources[i].source.x = map(SEGMENT.custom3, 0, 31, 0, (PartSys->maxXpixel - numSprays * 2) * PS_P_RADIUS) + i * PS_P_RADIUS * 2; // emitter position + PartSys->sources[i].source.y = PartSys->maxY + (PS_P_RADIUS * ((i<<2) + 4)); // source y position, few pixels above the top to increase spreading before entering the matrix + //PartSys->sources[i].source.ttl = 255; // source never dies, replenish its lifespan TODO: source is not moved? + PartSys->sources[i].var = SEGMENT.custom1 >> 3; // emiting variation 0-32 } - // detect and handle collisions - uint8_t hardness = SEGMENT.custom2; // how hard the collisions are, 255 = full hard. - - if (SEGMENT.custom2 > 0) //switch off collisions if hardnes is set to zero - { - detectCollisions(particles, numParticles, hardness); - } - else{ - hardness = 150; //set hardness (for ground bounce) to fixed value if not using collisions - } - - // now move the particles - for (i = 0; i < numParticles; i++) - { - // every now and then, apply 'air friction' to smooth things out, slows down all particles a little - if (SEGMENT.call % 8 == 0) + for (i = 0; i < numSprays; i++) { - applyFriction(&particles[i], 1); + PartSys->sprayEmit(PartSys->sources[i]); } - Particle_Gravity_update(&particles[i], SEGMENT.check1, SEGMENT.check2, SEGMENT.check3, hardness); } - SEGMENT.fill(BLACK); // clear the matrix - - // render the particles - ParticleSys_render(particles, numParticles, SEGMENT.check1, false); + if (SEGMENT.call % 8 == 0) + PartSys->applyFriction(1); + PartSys->update(); // update and render 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) return mode_static(); + ParticleSystem *PartSys = NULL; + uint32_t i; - const uint32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; - const uint32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); + if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. + { + if (!initParticleSystem(PartSys, 0)) // init + return mode_static(); // allocation failed; //allocation failed + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS -#ifdef ESP8266 - const uint32_t numParticles = 80; // maximum number of particles -#else - const uint32_t numParticles = 255; // maximum number of particles -#endif + if (PartSys == NULL) + { + Serial.println("ERROR: paticle system not found, nullpointer"); + return mode_static(); // something went wrong, no data! + } - PSparticle *particles; + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setBounceX(true); + PartSys->setBounceY(true); + PartSys->setWallHardness(min(SEGMENT.custom2, (uint8_t)200)); // wall hardness is 200 or more + if (SEGMENT.custom2 > 0) + PartSys->enableParticleCollisions(true, SEGMENT.custom2); // enable collisions and set particle collision hardness + else + PartSys->enableParticleCollisions(false); - // allocate memory and divide it into proper pointers, max is 32k for all segments. - uint32_t dataSize = sizeof(PSparticle) * numParticles; - if (!SEGENV.allocateData(dataSize)) - return mode_static(); // allocation failed; //allocation failed - particles = reinterpret_cast(SEGENV.data); // cast the data array into a particle pointer - uint32_t i = 0; + #ifdef ESP8266 + uint16_t maxnumParticles = min((uint16_t)((PartSys->maxXpixel * PartSys->maxYpixel) >> 1), PartSys->numParticles); + #else + uint16_t maxnumParticles = min((uint16_t)((PartSys->maxXpixel * PartSys->maxYpixel)), PartSys->numParticles); + #endif + PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 10, maxnumParticles)); - if (SEGMENT.call == 0) // initialization + if (SEGMENT.call == 0) // initialization of particles { - SEGMENT.aux0 = rand(); // position (either in noise or in sine function) - for (i = 0; i < numParticles; i++) + SEGMENT.aux0 = rand(); // position in perlin noise + for (i = 0; i < maxnumParticles; i++) { - particles[i].ttl = 500; // all particles are alive (but not all are calculated/rendered) - particles[i].hue = i * 3; // full color range (goes over palette colors three times so it is also colorful when using fewer particles) - particles[i].sat = 255; // set full saturation (lets palette choose the color) - particles[i].x = map(i, 0, 255, 1, cols * PS_P_RADIUS); // distribute along x according to color - particles[i].y = random16((rows >> 2) * PS_P_RADIUS); // in the bottom quarder - particles[i].collide = true; //all particles collide + PartSys->particles[i].ttl = 500; //set all particles alive (not all are rendered though) + PartSys->particles[i].hue = i * 4; // full color range + PartSys->particles[i].sat = 255; // set full saturation (lets palette choose the color) + PartSys->particles[i].x = map(i, 0, maxnumParticles, 1, PartSys->maxX); // distribute along x according to color + PartSys->particles[i].y = random16(PartSys->maxY); // randomly in y direction + PartSys->particles[i].collide = true; // all particles collide } } - uint16_t displayparticles = map(SEGMENT.intensity, 0, 255, 10, numParticles); - - i = 0; - if (SEGMENT.call % (((255 - SEGMENT.speed) >> 6) + 1) == 0 && SEGMENT.speed > 0) // how often the force is applied depends on speed setting { - int32_t xgravity; int32_t ygravity; SEGMENT.aux0 += (SEGMENT.speed >> 6) + 1; // update position in noise - xgravity = ((int16_t)inoise8(SEGMENT.aux0) - 127); //TODO: inoise 16 would be faster + xgravity = ((int16_t)inoise8(SEGMENT.aux0) - 127); //TODO: inoise 16 would be faster? ygravity = ((int16_t)inoise8(SEGMENT.aux0 + 10000) - 127); if (SEGMENT.check1) //sloshing, y force is alwys downwards { if(ygravity > 0) ygravity = -ygravity; } - // scale the gravity force down - xgravity /= 16; - ygravity /= 16; + xgravity /= 2 + ((256 - SEGMENT.custom1) >> 3); //(divide by 1-32) + ygravity /= 2 + ((256 - SEGMENT.custom1) >> 3); - for (i = 0; i < numParticles; i++) + PartSys->applyForce(PartSys->particles, PartSys->usedParticles, xgravity, ygravity); + + //reset particle TTL so they never die + for (i = 0; i < PartSys->usedParticles; i++) { - if (particles[i].ttl > 0) + if (PartSys->particles[i].ttl > 0) { - particles[i].vx += xgravity; - particles[i].vy += ygravity; - particles[i].ttl = 500; // particles never die + PartSys->particles[i].ttl = 500; // particles never die } } } - uint8_t hardness = SEGMENT.custom2; // how hard the collisions are, 255 = full hard. - detectCollisions(particles, displayparticles, hardness); - - // now move the particles - for (i = 0; i < displayparticles; i++) - { - particles[i].ttl = 500; // particles never die - // every now and then, apply 'air friction' to smooth things out, slows down all particles a little - if (SEGMENT.call % 8 == 0) - { - applyFriction(&particles[i], 1); - } - Particle_Bounce_update(&particles[i], hardness); - } - - SEGMENT.fill(BLACK); // clear the matrix + if (SEGMENT.call % 8 == 0) + PartSys->applyFriction(1); - // render the particles - ParticleSys_render(particles, displayparticles, false, false); + PartSys->update(); // update and render 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"; -*/ +static const char _data_FX_MODE_PARTICLEBOX[] PROGMEM = "PS Box@Speed,Particles,Tilt Strength,Hardness,,Sloshing;;!;012;pal=1,sx=120,ix=100,c1=100,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) { - 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 = 80; -#else - const uint32_t numParticles = 350; -#endif - - PSparticle *particles; - - // allocate memory and divide it into proper pointers, max is 32k for all segments. - uint32_t dataSize = sizeof(PSparticle) * numParticles; - if (!SEGENV.allocateData(dataSize)) - return mode_static(); // allocation failed; //allocation failed - particles = reinterpret_cast(SEGENV.data); // cast the data array into a particle pointer - - uint32_t i = 0; - - if (SEGMENT.call == 0) // initialization + ParticleSystem *PartSys = NULL; + uint32_t i; + if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. { + if (!initParticleSystem(PartSys, 0)) // init + return mode_static(); // allocation failed; //allocation failed + PartSys->setKillOutOfBounds(true); //should never happen, but lets make sure there are no stray particles SEGMENT.aux0 = rand(); - for (i = 0; i < numParticles; i++) + for (i = 0; i < PartSys->numParticles; i++) { - particles[i].ttl = random16(500) + 200; - particles[i].x = random16(cols * PS_P_RADIUS); - particles[i].y = random16(rows * PS_P_RADIUS); - particles[i].sat = 255; //full saturation, color set by palette + PartSys->particles[i].sat = 255; // full saturation, color set by palette } } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS - uint32_t displayparticles = map(SEGMENT.intensity,0,255,10,numParticles); + if (PartSys == NULL) + { + DEBUG_PRINT(F("ERROR: FX PartSys nullpointer")); + return mode_static(); // something went wrong, no data! + } + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setWrapX(SEGMENT.check1); + PartSys->setBounceX(!SEGMENT.check1); + PartSys->setBounceY(true); + PartSys->setWallHardness(255); + uint32_t displayparticles = map(SEGMENT.intensity, 0, 255, 10, PartSys->numParticles); + PartSys->setUsedParticles(displayparticles); // apply 'gravity' from a 2D perlin noise map SEGMENT.aux0 += 1+(SEGMENT.speed >> 5); // noise z-position @@ -8871,47 +8713,33 @@ uint16_t mode_particleperlin(void) // update position in noise for (i = 0; i < displayparticles; i++) { - - if (particles[i].ttl == 0) // revive dead particles (do not keep them alive forever, they can clump up, need to reseed) + if (PartSys->particles[i].ttl == 0) // revive dead particles (do not keep them alive forever, they can clump up, need to reseed) { - particles[i].ttl = random16(500) + 200; - particles[i].x = random16(cols * PS_P_RADIUS); - particles[i].y = random16(rows * PS_P_RADIUS); - } - int32_t xnoise = particles[i].x / (1 + (SEGMENT.custom3>>1)); //position in perlin noise, scaled by slider - int32_t ynoise = particles[i].y / (1 + (SEGMENT.custom3>>1)); - - int16_t baseheight = inoise8(xnoise, ynoise, SEGMENT.aux0); // noise value at particle position - particles[i].hue = baseheight; // color particles to perlin noise value - if (SEGMENT.call % 6 == 0) // do not apply the force every frame, is too chaotic + PartSys->particles[i].ttl = random16(500) + 200; + PartSys->particles[i].x = random16(PartSys->maxX); + PartSys->particles[i].y = random16(PartSys->maxY); + } + uint16_t xnoise = (PartSys->particles[i].x / (16 - (SEGMENT.custom3>>1))); // position in perlin noise, scaled by slider + uint16_t ynoise = (PartSys->particles[i].y / (16 - (SEGMENT.custom3>>1))); + int16_t baseheight = inoise8(xnoise, ynoise, SEGMENT.aux0); // noise value at particle position + PartSys->particles[i].hue = baseheight; // color particles to perlin noise value + if (SEGMENT.call % 6 == 0) // do not apply the force every frame, is too chaotic { - int16_t xslope = baseheight - (int16_t)inoise8(xnoise + 10, ynoise, SEGMENT.aux0); - int16_t yslope = baseheight - (int16_t)inoise8(xnoise, ynoise + 10, SEGMENT.aux0); - - particles[i].vx += xslope >> 1; // slope is about 0-8 - particles[i].vy += yslope >> 1; + Serial.print(i); Serial.print(" "); + int8_t xslope = (baseheight - (int16_t)inoise8(xnoise + 10, ynoise, SEGMENT.aux0)); + int8_t yslope = (baseheight - (int16_t)inoise8(xnoise, ynoise + 10, SEGMENT.aux0)); + PartSys->applyForce(&(PartSys->particles[i]), 1, xslope, yslope); } } - // move particles - for (i = 0; i < displayparticles; i++) - { - // apply a bit of friction so particles are less jittery - if (SEGMENT.call % (16-(SEGMENT.custom2>>4)) == 0) // need to apply friction very rarely or particles will clump - applyFriction(&particles[i], 1); - - Particle_Bounce_update(&particles[i], 255); - } - - SEGMENT.fill(BLACK); // clear the matrix - - // render the particles - ParticleSys_render(particles, displayparticles, false, false); + if (SEGMENT.call % (16 - (SEGMENT.custom2 >> 4)) == 0) + PartSys->applyFriction(2); + PartSys->update(); // update and render 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"; -*/ +static const char _data_FX_MODE_PARTICLEPERLIN[] PROGMEM = "PS Fuzzy Noise@Speed,Particles,,Friction,Scale,Cylinder;;!;012;pal=54,sx=70;ix=200,c1=120,c2=120,c3=20,o1=0"; + /* * Particle smashing down like meteorites and exploding as they hit the ground, has many parameters to play with * by DedeHai (Damian Schneider) @@ -8967,11 +8795,11 @@ uint16_t mode_particleimpact(void) // determine meteor state by its speed: if ( PartSys->sources[i].source.vy < 0) // moving down, emit sparks { - #ifdef ESP8266 + #ifdef ESP8266 emitparticles = 1; - #else + #else emitparticles = 2; - #endif + #endif } else if ( PartSys->sources[i].source.vy > 0) // moving up means meteor is on 'standby' { @@ -8980,11 +8808,11 @@ uint16_t mode_particleimpact(void) else // speed is zero, explode! { PartSys->sources[i].source.vy = 125; // set source speed positive so it goes into timeout and launches again - #ifdef ESP8266 - emitparticles = random16(SEGMENT.intensity >> 2) + 10; // defines the size of the explosion - #else + #ifdef ESP8266 + emitparticles = random16(SEGMENT.intensity >> 3) + 5; // defines the size of the explosion + #else emitparticles = random16(SEGMENT.intensity >> 1) + 10; // defines the size of the explosion - #endif + #endif } for (int e = emitparticles; e > 0; e--) { @@ -9003,19 +8831,21 @@ uint16_t mode_particleimpact(void) // if source reaches the bottom, set speed to 0 so it will explode on next function call (handled above) if ((PartSys->sources[i].source.y < PS_P_RADIUS) && ( PartSys->sources[i].source.vy < 0)) // reached the bottom pixel on its way down { - PartSys->sources[i].source.vy = 0; // set speed zero so it will explode - PartSys->sources[i].source.vx = 0; - //PartSys->sources[i].source.y = 5; // offset from ground so explosion happens not out of frame (TODO: still needed? the class takes care of that) - PartSys->sources[i].source.collide = true; - PartSys->sources[i].maxLife = 200; - PartSys->sources[i].minLife = 50; + PartSys->sources[i].source.vy = 0; // set speed zero so it will explode + PartSys->sources[i].source.vx = 0; + //PartSys->sources[i].source.y = 5; // offset from ground so explosion happens not out of frame (TODO: still needed? the class takes care of that) + PartSys->sources[i].source.collide = true; #ifdef ESP8266 - PartSys->sources[i].source.ttl = random16(255 - (SEGMENT.speed>>1)) + 10; // standby time til next launch (in frames at 42fps, max of 265 is about 6 seconds + PartSys->sources[i].maxLife = 130; + PartSys->sources[i].minLife = 20; + PartSys->sources[i].source.ttl = random16(255 - (SEGMENT.speed>>1)) + 10; // standby time til next launch (in frames at 42fps, max of 265 is about 6 seconds #else - PartSys->sources[i].source.ttl = random16((255 - SEGMENT.speed)) + 10; // standby time til next launch (in frames at 42fps, max of 265 is about 6 seconds + PartSys->sources[i].maxLife = 200; + PartSys->sources[i].minLife = 50; + PartSys->sources[i].source.ttl = random16((255 - SEGMENT.speed)) + 10; // standby time til next launch (in frames at 42fps, max of 265 is about 6 seconds #endif - PartSys->sources[i].vy = (SEGMENT.custom1 >> 2); // emitting speed y - PartSys->sources[i].var = (SEGMENT.custom1 >> 1); // speed variation around vx,vy (+/- var/2) + PartSys->sources[i].vy = (SEGMENT.custom1 >> 2); // emitting speed y + PartSys->sources[i].var = (SEGMENT.custom1 >> 1); // speed variation around vx,vy (+/- var/2) } } else if ( PartSys->sources[i].source.vy > 0) // meteor is exploded and time is up (ttl==0 and positive speed), relaunch it @@ -9035,11 +8865,10 @@ uint16_t mode_particleimpact(void) } } - SEGMENT.fill(BLACK); // clear the matrix PartSys->update(); // update and render return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEIMPACT[] PROGMEM = "PS Impact@Launches,Explosion Size,Explosion Force,Bounce,Meteors,Cylinder,Walls,Collisions;;!;012;pal=0,sx=32,ix=85,c1=100,c2=100,c3=8,o1=0,o2=1,o3=1"; +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=0,o3=1"; /* Particle Attractor, a particle attractor sits in the matrix center, a spray bounces around and seeds particles @@ -9047,134 +8876,108 @@ 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(); - - const uint32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; - const uint32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); - // particle system box dimensions - const uint32_t Max_x(cols * PS_P_RADIUS - 1); - const uint32_t Max_y(rows * PS_P_RADIUS - 1); - -#ifdef ESP8266 - const uint32_t numParticles = 90; // maximum number of particles -#else - const uint32_t numParticles = 300; // maximum number of particles -#endif - - - PSparticle *particles; - PSparticle *attractor; - 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(PSsource); //space for spray - - - if (!SEGENV.allocateData(dataSize)) - return mode_static(); // allocation failed - // 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); - counters = reinterpret_cast(spray + 1); - - uint32_t i; + if (SEGLEN == 1) + return mode_static(); + ParticleSystem *PartSys = NULL; + uint32_t i = 0; + PSparticle *attractor; //particle pointer to the attractor + uint8_t *counters; // counters for the applied force + PSsettings sourcesettings = {0, 0, 1, 1, 0, 0, 0, 0}; // PS settings for bounceY, bounceY used for source movement (it always bounces whereas particles do not) - if (SEGMENT.call == 0) // initialization + if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. { - attractor->vx = 0; - attractor->vy = 0; - attractor->x = Max_x >> 1; // center - attractor->y = Max_y >> 1; - - for (i = 0; i < numParticles; i++) - { - particles[i].ttl = 0; - } - - spray->source.hue = random16(); - spray->source.sat = 255; //full saturation, color by palette - spray->source.x = 0; - spray->source.y = 0; - spray->source.vx = random16(5) + 2; - spray->source.vy = random16(4) + 1; - spray->source.ttl = 100; - spray->source.collide = true; //seeded particles will collide (if checked) - spray->maxLife = 300; // seeded particle lifetime in frames - spray->minLife = 30; - spray->vx = 0; // emitting speed - spray->vy = 0; // emitting speed - spray->var = 6; //emitting speed variation + uint32_t numParticles = (calculateNumberOfParticles() * 2) / 3; // use 75% of available particles to keep FPS high (also we need an attractor particle) + if (!initParticleSystem(PartSys, numParticles+10)) // init, need one extra byte per used particle for force counters //!!! looking for a reason it crashes... + return mode_static(); // allocation failed; //allocation failed + + PartSys->sources[0].source.hue = random16(); + PartSys->sources[0].source.sat = 255; // set full saturation + PartSys->sources[0].source.x = PS_P_RADIUS; //start out in bottom left corner + PartSys->sources[0].source.y = PS_P_RADIUS<<1; + PartSys->sources[0].source.vx = random16(5) + 3; + PartSys->sources[0].source.vy = PartSys->sources[0].source.vx - 2; //move slower in y + PartSys->sources[0].source.collide = true; // seeded particles will collide + PartSys->sources[0].source.ttl = 100; //is replenished below, it never dies + #ifdef ESP8266 + PartSys->sources[0].maxLife = 200; // lifetime in frames (ESP8266 has less particles) + PartSys->sources[0].minLife = 30; + #else + PartSys->sources[0].maxLife = 350; // lifetime in frames + PartSys->sources[0].minLife = 50; + #endif + PartSys->sources[0].vx = 0; // emitting speed + PartSys->sources[0].vy = 0; // emitting speed + PartSys->sources[0].var = 6; // emiting variation } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS - uint32_t displayparticles = map(SEGMENT.intensity, 0, 255, 1, numParticles) - 2; //TODO: the -2 is a botch fix, it crashes for some reason if going to max number of particles, is this a rounding error? - uint8_t hardness = SEGMENT.custom2; // how hard the collisions are, 255 = full hard. - i = 0; - - if (hardness > 1) // enable collisions + if (PartSys == NULL) { - detectCollisions(particles, displayparticles, hardness); + 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) } + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setWallHardness(230); //walls are always same hardness + PartSys->setColorByAge(SEGMENT.check1); - if (SEGMENT.call % 5 == 0) + if (SEGMENT.custom2 > 0) // collisions enabled + PartSys->enableParticleCollisions(true, SEGMENT.custom2); // enable collisions and set particle collision hardness + else + PartSys->enableParticleCollisions(false); + + uint16_t lastusedparticle = (PartSys->numParticles * 2) / 3; + uint32_t displayparticles = map(SEGMENT.intensity, 0, 255, 10, lastusedparticle); + PartSys->setUsedParticles(displayparticles); + + // set pointers + attractor = reinterpret_cast(&PartSys->particles[lastusedparticle + 1]); + counters = reinterpret_cast(PartSys->PSdataEnd); + //set attractor properties + if(SEGMENT.check2) //move attractor { - spray->source.hue++; - spray->source.ttl = 100; //spray never dies + attractor->vx = PartSys->sources[0].source.vy; // set to spray movemement but reverse x and y + attractor->vy = PartSys->sources[0].source.vx; + PartSys->particleMoveUpdate(*attractor, sourcesettings); // move the attractor } - - uint8_t emit = 1; //number of particles emitted per frame - Particle_Bounce_update(&spray->source, 255); //bounce the spray around - - SEGMENT.aux0++; //emitting angle - - // now move the particles - for (i = 0; i < displayparticles; i++) + else{ + attractor->vx = 0; // not moving + attractor->vy = 0; + attractor->x = PartSys->maxX >> 1; // center + attractor->y = PartSys->maxY >> 1; + } + + if (SEGMENT.call % 5 == 0) { - - if (particles[i].ttl == 0 && emit--) // find a dead particle - { - if(SEGMENT.call % 2 == 0) //alternate direction of emit - Emitter_Angle_emit(spray, &particles[i], SEGMENT.aux0, SEGMENT.custom1 >> 4); - else - Emitter_Angle_emit(spray, &particles[i], SEGMENT.aux0+128, SEGMENT.custom1 >> 4); //emit at 180° as well - } - - // every now and then, apply 'air friction' to smooth things out, slows down all particles a little - if (SEGMENT.custom3 > 0) - { - if (SEGMENT.call % (32 - SEGMENT.custom3) == 0) - { - applyFriction(&particles[i], 4); - } - } - - Particle_attractor(&particles[i], attractor, &counters[i], SEGMENT.speed, SEGMENT.check3); - if(SEGMENT.check1) - Particle_Bounce_update(&particles[i], hardness); - else - Particle_Move_update(&particles[i]); + PartSys->sources[0].source.hue++; + PartSys->sources[0].source.ttl = 100; //spray never dies } - if (SEGMENT.check2) - SEGMENT.fadeToBlackBy(20); // fade the matrix + SEGMENT.aux0 += 256; //emitting angle, one full turn in 255 frames (0xFFFF is 360°) + if (SEGMENT.call % 2 == 0) // alternate direction of emit + PartSys->angleEmit(PartSys->sources[0], SEGMENT.aux0, SEGMENT.custom1 >> 4); else - SEGMENT.fill(BLACK); // clear the matrix - - // ParticleSys_render(&attract, 1, 30, false, false); // render attractor + PartSys->angleEmit(PartSys->sources[0], SEGMENT.aux0 + 0x7FFF, SEGMENT.custom1 >> 4); // emit at 180° as well - // render the particles - ParticleSys_render(particles, displayparticles, false, false); + // apply force + for(i = 0; i < displayparticles; i++) + { + PartSys->attract(&PartSys->particles[i], attractor, counters, SEGMENT.speed, SEGMENT.check3); + } + if (SEGMENT.call % (33 - SEGMENT.custom3) == 0) + PartSys->applyFriction(2); + PartSys->particleMoveUpdate(PartSys->sources[0].source, sourcesettings); // move the source + + PartSys->update(); // update and render 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"; -*/ +static const char _data_FX_MODE_PARTICLEATTRACTOR[] PROGMEM = "PS Attractor@Mass,Particles,Emit Speed,Collisions,Friction,Color by Age,Move,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 @@ -9287,7 +9090,6 @@ uint16_t mode_particlespray(void) } } - SEGMENT.fill(BLACK); // clear the matrix // render the particles ParticleSys_render(particles, numParticles, SEGMENT.check2, false); @@ -9391,7 +9193,6 @@ uint16_t mode_particleGEQ(void) } } - SEGMENT.fill(BLACK); // clear the matrix PartSys->update(); // update and render return FRAMETIME; } @@ -9520,7 +9321,6 @@ uint16_t mode_particlecenterGEQ(void) Particle_Move_update(&PartSys->particles[i], true); // move the particles, kill out of bounds particles } - SEGMENT.fill(BLACK); // clear the matrix // render the particles ParticleSys_render(particles, numParticles, false, false); @@ -9774,16 +9574,21 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_PARTICLEROTATINGSPRAY, &mode_particlerotatingspray, _data_FX_MODE_PARTICLEROTATINGSPRAY); addEffect(FX_MODE_PARTICLEFIREWORKS, &mode_particlefireworks, _data_FX_MODE_PARTICLEFIREWORKS); + addEffect(FX_MODE_PARTICLEVOLCANO, &mode_particlevolcano, _data_FX_MODE_PARTICLEVOLCANO); addEffect(FX_MODE_PARTICLEFIRE, &mode_particlefire, _data_FX_MODE_PARTICLEFIRE); addEffect(FX_MODE_PARTICLEFALL, &mode_particlefall, _data_FX_MODE_PARTICLEFALL); + addEffect(FX_MODE_PARTICLEWATERFALL, &mode_particlewaterfall, _data_FX_MODE_PARTICLEWATERFALL); + addEffect(FX_MODE_PARTICLEBOX, &mode_particlebox, _data_FX_MODE_PARTICLEBOX); + addEffect(FX_MODE_PARTICLEPERLIN, &mode_particleperlin, _data_FX_MODE_PARTICLEPERLIN); addEffect(FX_MODE_PARTICLEIMPACT, &mode_particleimpact, _data_FX_MODE_PARTICLEIMPACT); + addEffect(FX_MODE_PARTICLEATTRACTOR, &mode_particleattractor, _data_FX_MODE_PARTICLEATTRACTOR); addEffect(FX_MODE_PARTICLESGEQ, &mode_particleGEQ, _data_FX_MODE_PARTICLEGEQ); - addEffect(FX_MODE_PARTICLEVOLCANO, &mode_particlevolcano, _data_FX_MODE_PARTICLEVOLCANO); + + + + /* - addEffect(FX_MODE_PARTICLEPERLIN, &mode_particleperlin, _data_FX_MODE_PARTICLEPERLIN); - addEffect(FX_MODE_PARTICLEBOX, &mode_particlebox, _data_FX_MODE_PARTICLEBOX); - addEffect(FX_MODE_PARTICLEWATERFALL, &mode_particlewaterfall, _data_FX_MODE_PARTICLEWATERFALL); - addEffect(FX_MODE_PARTICLEATTRACTOR, &mode_particleattractor, _data_FX_MODE_PARTICLEATTRACTOR); + addEffect(FX_MODE_PARTICLESPRAY, &mode_particlespray, _data_FX_MODE_PARTICLESPRAY); */ // addEffect(FX_MODE_PARTICLECENTERGEQ, &mode_particlecenterGEQ, _data_FX_MODE_PARTICLECCIRCULARGEQ); diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index d5b04576a2..1d6e74483c 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -148,7 +148,7 @@ Segment& Segment::operator= (Segment &&orig) noexcept { bool IRAM_ATTR Segment::allocateData(size_t len) { if (len == 0) return false; // nothing to do if (data && _dataLen >= len) { // already allocated enough (reduce fragmentation) - //!!! if (call == 0) memset(data, 0, len); // erase buffer if called during effect initialisation + if (call == 0) memset(data, 0, len); // erase buffer if called during effect initialisation return true; } DEBUG_PRINT(F("Allocating Data")); diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index 5aff415d4b..bf5eee0121 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -33,17 +33,11 @@ /* TODO: - -add function to 'update sources' so FX does not have to take care of that. FX can still implement its own version if so desired. config should be optional, if not set, use default config. - -add SEGMENT.fill(BLACK); // clear the matrix into rendering function? - -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 function to 'update sources' so FX does not have to take care of that. FX can still implement its own version if so desired. config should be optional, if not set, use default config. -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) - -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" @@ -356,38 +350,51 @@ void ParticleSystem::applyForce(PSparticle *part, uint32_t numparticles, int8_t { 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 + part[0].vx = part[0].vx + dvx > PS_P_MAXSPEED ? PS_P_MAXSPEED : part[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; + part[i].vx = part[i].vx + dvx > PS_P_MAXSPEED ? PS_P_MAXSPEED : part[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; + part[0].vy = part[0].vy + dvy > PS_P_MAXSPEED ? PS_P_MAXSPEED : part[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; + part[i].vy = part[i].vy + dvy > PS_P_MAXSPEED ? PS_P_MAXSPEED : part[i].vy + dvy; } } } } + +// apply a force in x,y direction to particles directly (no counter required) +void ParticleSystem::applyForce(PSparticle *part, uint32_t numparticles, int8_t xforce, int8_t yforce) +{ + //note: could make this faster for single particles by adding an if statement, but it is fast enough as is + for (uint 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 + part[i].vx = part[i].vx + xforce > PS_P_MAXSPEED ? PS_P_MAXSPEED : part[i].vx + xforce; + part[i].vy = part[i].vy + yforce > PS_P_MAXSPEED ? PS_P_MAXSPEED : part[i].vy + yforce; + } +} + // apply a force in angular direction to group of particles //TODO: actually test if this works as expected, this is untested code // 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, uint16_t angle, uint8_t *counter) { int8_t xforce = ((int32_t)force * cos8(angle)) >> 15; // force is +/- 127 int8_t yforce = ((int32_t)force * sin8(angle)) >> 15; - // noste: sin16 is 10% faster than sin8() on ESP32 but on ESP8266 it is 9% slower + // note: sin16 is 10% faster than sin8() on ESP32 but on ESP8266 it is 9% slower // 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); } @@ -448,6 +455,7 @@ void ParticleSystem::applyFriction(PSparticle *part, uint8_t coefficient) part->vy = ((int16_t)part->vy * friction) >> 8; } +// apply friction to all particles void ParticleSystem::applyFriction(uint8_t coefficient) { int32_t friction = 256 - coefficient; @@ -462,36 +470,37 @@ void ParticleSystem::applyFriction(uint8_t coefficient) // TODO: attract needs to use the above force functions // attracts a particle to an attractor particle using the inverse square-law -void ParticleSystem::attract(PSparticle *particle, PSparticle *attractor, uint8_t *counter, uint8_t strength, bool swallow) +void ParticleSystem::attract(PSparticle *part, 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; - int32_t dy = attractor->y - particle->y; + int32_t dx = attractor->x - part->x; + int32_t dy = attractor->y - part->y; // Calculate the force based on inverse square law int32_t distanceSquared = dx * dx + dy * dy + 1; - if (distanceSquared < 4096) + if (distanceSquared < 8192) { - if (swallow) // particle is close, kill it - { - particle->ttl = 0; - return; + if (swallow) // particle is close, age it fast so it fades out, do not attract further + { + if (part->ttl > 7) + part->ttl -= 8; + else + { + part->ttl = 0; + return; + } } - distanceSquared = 4 * PS_P_RADIUS * PS_P_RADIUS; // limit the distance of particle size to avoid very high forces + distanceSquared = 4 * PS_P_RADIUS * PS_P_RADIUS; // limit the distance to avoid very high forces } - int32_t shiftedstrength = (int32_t)strength << 16; - int32_t force; - int32_t xforce; - int32_t yforce; - int32_t xforce_abs; // absolute value - int32_t yforce_abs; + int32_t force = ((int32_t)strength << 16) / distanceSquared; + int8_t xforce = (force * dx) >> 10; // scale to a lower value, found by experimenting + int8_t yforce = (force * dy) >> 10; - force = shiftedstrength / distanceSquared; - xforce = (force * dx) >> 10; // scale to a lower value, found by experimenting - yforce = (force * dy) >> 10; - xforce_abs = abs(xforce); // absolute value - yforce_abs = abs(yforce); + applyForce(part, 1, xforce, yforce, counter); + /* + int32_t xforce_abs = abs(xforce); // absolute value + int32_t yforce_abs = abs(yforce); uint8_t xcounter = (*counter) & 0x0F; // lower four bits uint8_t ycounter = (*counter) >> 4; // upper four bits @@ -541,7 +550,7 @@ void ParticleSystem::attract(PSparticle *particle, PSparticle *attractor, uint8_ { particle->vy += yforce >> 4; // divide by 16 } - // TODO: need to limit the max speed? + */ } // render particles to the LED buffer (uses palette to render the 8bit particle color value) @@ -572,17 +581,10 @@ void ParticleSystem::ParticleSys_render() // go over particles and render them to the buffer for (i = 0; i < usedParticles; i++) - { - if (particles[i].ttl == 0) - { - //Serial.print("d"); - continue; - } - if(particles[i].outofbounds) - { - //Serial.print("o"); + { + if (particles[i].ttl == 0 || particles[i].outofbounds) continue; - } + // 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); @@ -596,7 +598,7 @@ void ParticleSystem::ParticleSys_render() // 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, pixco); - + /* //debug: check coordinates if out of buffer boundaries print out some info for(uint32_t d; d<4; d++) { @@ -624,7 +626,7 @@ void ParticleSystem::ParticleSys_render() useLocalBuffer = false; free(colorbuffer); // free buffer memory } - } + }*/ if (useLocalBuffer) { if (intensity[0] > 0) @@ -638,6 +640,7 @@ void ParticleSystem::ParticleSys_render() } else { + SEGMENT.fill(BLACK); // clear the matrix if (intensity[0] > 0) SEGMENT.addPixelColorXY(pixco[0][0], maxYpixel - pixco[0][1], baseRGB.scale8((uint8_t)intensity[0])); // bottom left if (intensity[1] > 0) @@ -766,8 +769,9 @@ void ParticleSystem::renderParticle(PSparticle* particle, uint32_t brightess, in } Serial.println(" "); */ +/* // debug: check coordinates if out of buffer boundaries print out some info - for (uint32_t d; d < 4; d++) + for (uint32_t d = 0; d < 4; d++) { if (pixelpositions[d][0] < 0 || pixelpositions[d][0] > maxXpixel) { @@ -802,6 +806,7 @@ void ParticleSystem::renderParticle(PSparticle* particle, uint32_t brightess, in } } } +*/ } // update & move particle, wraps around left/right if settings.wrapX is true, wrap around up/down if settings.wrapY is true @@ -820,7 +825,7 @@ void ParticleSystem::fireParticleupdate() particles[i].ttl--; // apply velocity particles[i].x = particles[i].x + (int32_t)particles[i].vx; - particles[i].y = particles[i].y + (int32_t)particles[i].vy + (particles[i].ttl >> 4); // younger particles move faster upward as they are hotter + particles[i].y = particles[i].y + (int32_t)particles[i].vy + (particles[i].ttl >> 2); // younger particles move faster upward as they are hotter particles[i].outofbounds = 0; // check if particle is out of bounds, wrap x around to other side if wrapping is enabled // as fire particles start below the frame, lots of particles are out of bounds in y direction. to improve speed, only check x direction if y is not out of bounds @@ -936,9 +941,11 @@ void ParticleSystem::PartMatrix_addHeat(int32_t heat, uint8_t *currentcolor, uin // 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 * (1 + (intensity >> 4)) + (intensity >> 3); // ESP8266 TODO: does this still need different value like in the old version? currently set to same + heat = heat * (1 + (intensity >> 3)) + (intensity >> 3); //ESP8266 has to make due with less flames, so add more heat #else - heat = heat * (1 + (intensity >> 4)) + (intensity >> 3); // todo: make this a variable to pass + //heat = (heat * (1 + (intensity)) >> 4) + (intensity >> 3); //this makes it more pulsating + //heat = heat * (1 + (intensity >> 4)) + (intensity >> 3); //this is good, but pulsating at low speeds + heat = heat * heat / (1 + (255-intensity)) + (intensity >> 3); #endif uint32_t i; @@ -1056,12 +1063,12 @@ void ParticleSystem::collideParticles(PSparticle *particle1, PSparticle *particl // If particles are moving towards each other if (dotProduct < 0) { - const uint32_t bitshift = 15; // bitshift used to avoid floats (dx/dy are 7bit, relativV are 8bit -> dotproduct is 15bit so up to 16bit shift is ok) + const uint32_t bitshift = 16; // bitshift used to avoid floats (dx/dy are 7bit, relativV are 8bit -> dotproduct is 15bit so up to 16bit shift is ok) // Calculate new velocities after collision int32_t impulse = (((dotProduct << (bitshift)) / (distanceSquared)) * collisionHardness) >> 8; - int32_t ximpulse = 1 + (impulse * dx) >> bitshift; - int32_t yimpulse = 1 + (impulse * dy) >> bitshift; + int32_t ximpulse = ((impulse + 1) * dx) >> bitshift; // todo: is the +1 a good idea? + int32_t yimpulse = ((impulse + 1) * dy) >> bitshift; particle1->vx += ximpulse; particle1->vy += yimpulse; particle2->vx -= ximpulse; @@ -1139,13 +1146,13 @@ int32_t ParticleSystem::wraparound(int32_t p, int32_t maxvalue) return p; } -//calculate the dV value and update the counter for force calculation (is used several times, function saves on codesize) +//calculate the delta speed (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; + int32_t dv = 0; // for small forces, need to use a delay counter, apply force only if it overflows if (force_abs < 16) { @@ -1173,7 +1180,7 @@ CRGB **ParticleSystem::allocate2Dbuffer(uint32_t cols, uint32_t rows) { // assign pointers of 2D array CRGB *start = (CRGB *)(array2D + cols); - for (int i = 0; i < cols; i++) + for (uint i = 0; i < cols; i++) { array2D[i] = start + i * rows; } @@ -1182,6 +1189,16 @@ CRGB **ParticleSystem::allocate2Dbuffer(uint32_t cols, uint32_t rows) return array2D; } +//update size and pointers (memory location and size can change dynamically) +void ParticleSystem::updateSystem(void) +{ + // update matrix size + uint32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; + uint32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); + setMatrixSize(cols, rows); + updatePSpointers(); +} + // 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 @@ -1200,20 +1217,19 @@ void ParticleSystem::updatePSpointers() } //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 - uint numberofParticles = (cols * rows * 3)>>2 ; // 0.75 particle per pixel - uint particlelimit = 148; //maximum number of paticles allowed (based on one segment of 16x16 and 4k effect ram) -#elseif ARDUINO_ARCH_ESP32S2 + uint numberofParticles = (cols * rows * 3) / 4; // 0.75 particle per pixel + uint particlelimit = ESP8266_MAXPARTICLES; // maximum number of paticles allowed (based on one segment of 16x16 and 4k effect ram) +#elif ARDUINO_ARCH_ESP32S2 uint numberofParticles = (cols * rows); // 1 particle per pixe - uint particlelimit = 768; // maximum number of paticles allowed (based on one segment of 32x32 and 24k effect ram) + uint particlelimit = ESP32S2_MAXPARTICLES; // maximum number of paticles allowed (based on one segment of 32x32 and 24k effect ram) #else - uint numberofParticles = (cols * rows * 3) / 2; // 1.5 particles per pixel (for example 768 particles on 32x16) - uint particlelimit = 1280; // maximum number of paticles allowed (based on two segments of 32x32 and 40k effect ram) + uint numberofParticles = (cols * rows); // 1 particle per pixel (for example 768 particles on 32x16) + uint particlelimit = ESP32_MAXPARTICLES; // maximum number of paticles allowed (based on two segments of 32x32 and 40k effect ram) #endif numberofParticles = max((uint)1, min(numberofParticles, particlelimit)); return numberofParticles; @@ -1225,13 +1241,13 @@ uint32_t calculateNumberOfSources() uint32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); #ifdef ESP8266 int numberofSources = (cols * rows) / 8; - numberofSources = max(1 , min(numberofSources, 16)); //limit to 1 - 16 -#elseif ARDUINO_ARCH_ESP32S2 + numberofSources = max(1, min(numberofSources, ESP8266_MAXSOURCES)); // limit to 1 - 16 +#elif ARDUINO_ARCH_ESP32S2 int numberofSources = (cols * rows) / 6; - numberofSources = max(1, min(numberofSources, 48)); // limit to 1 - 48 + numberofSources = max(1, min(numberofSources, ESP32S2_MAXSOURCES)); // limit to 1 - 48 #else int numberofSources = (cols * rows) / 4; - numberofSources = max(1 , min(numberofSources, 72)); //limit to 1 - 72 + numberofSources = max(1, min(numberofSources, ESP32_MAXSOURCES)); // limit to 1 - 72 #endif return numberofSources; } @@ -1274,7 +1290,7 @@ bool initParticleSystem(ParticleSystem *&PartSys, uint16_t additionalbytes) } // fastled color adding is very inaccurate in color preservation -// a better color add function is implemented in colors.cpp but it uses 32bit RGBW. so colors need to be shifted and then shifted back by that function, which is slow +// a better color add function is implemented in colors.cpp but it uses 32bit RGBW. to use it colors need to be shifted just to then be shifted back by that function, which is slow // this is a fast version for RGB (no white channel, PS does not handle white) and with native CRGB including scaling of second color (fastled scale8 can be made faster using native 32bit on ESP) CRGB fast_color_add(CRGB c1, CRGB c2, uint32_t scale) { @@ -1284,7 +1300,7 @@ CRGB fast_color_add(CRGB c1, CRGB c2, uint32_t scale) uint32_t g = c1.g + ((c2.g * (scale)) >> 8); uint32_t b = c1.b + ((c2.b * (scale)) >> 8); uint32_t max = r; - if (g > max) + if (g > max) //note: using ? operator would be slower by 2 cpu cycles max = g; if (b > max) max = b; diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index 4982224352..7d9ed1ca45 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -29,13 +29,21 @@ #include #include "FastLED.h" +//memory allocation +#define ESP8266_MAXPARTICLES 148 // enough for one 16x16 segment with transitions +#define ESP8266_MAXSOURCES 16 +#define ESP32S2_MAXPARTICLES 768 // enough for four 16x16 segments +#define ESP32S2_MAXSOURCES 48 +#define ESP32_MAXPARTICLES 1024 // enough for four 16x16 segments TODO: not enough for one 64x64 panel... +#define ESP32_MAXSOURCES 64 + //particle dimensions (subpixel division) #define PS_P_RADIUS 64 //subpixel size, each pixel is divided by this for particle movement, if this value is changed, also change the shift defines (next two lines) #define PS_P_HALFRADIUS 32 #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 +#define PS_P_MAXSPEED 200 //maximum speed a particle can have //struct for a single particle typedef struct { @@ -51,8 +59,6 @@ typedef struct { bool collide : 1; //if set, particle takes part in collisions bool flag3 : 1; // unused flags... bool flag4 : 1; - - //uint16_t ttl; // time to live, 12 bit or 4095 max (which is 50s at 80FPS) } PSparticle; //struct for a particle source @@ -68,15 +74,15 @@ typedef struct { // struct for PS settings typedef struct { - // add a one byte bit field: - bool killoutofbounds : 1; // if set, out of bound particles are killed immediately + // add a one byte bit field: bool wrapX : 1; bool wrapY : 1; bool bounceX : 1; bool bounceY : 1; + bool killoutofbounds : 1; // if set, out of bound particles are killed immediately bool useGravity : 1; //set to 1 if gravity is used, disables bounceY at the top bool useCollisions : 1; - bool colorByAge : 1; // if set, particle hue is set by ttl value + bool colorByAge : 1; // if set, particle hue is set by ttl value in render function } PSsettings; class ParticleSystem @@ -86,14 +92,15 @@ class ParticleSystem // note: memory is allcated in the FX function, no deconstructor needed void update(void); //update the particles according to set options and render to the matrix void updateFire(uint32_t intensity, bool usepalette); // update function for fire - void updatePSpointers(); // update the data pointers to current segment data space + // particle emitters void flameEmit(PSsource &emitter); void sprayEmit(PSsource &emitter); void angleEmit(PSsource& emitter, uint16_t angle, uint32_t speed); - - //move functions + void updateSystem(void); // call at the beginning of every FX, updates pointers and dimensions + + // move functions void particleMoveUpdate(PSparticle &part, PSsettings &options); //particle physics @@ -101,6 +108,7 @@ class ParticleSystem void applyGravity(PSparticle *part, uint32_t numarticles, uint8_t *counter); //use global gforce void applyGravity(PSparticle *part); //use global system settings void applyForce(PSparticle *part, uint32_t numparticles, int8_t xforce, int8_t yforce, uint8_t *counter); + void applyForce(PSparticle *part, uint32_t numparticles, int8_t xforce, int8_t yforce); void applyAngleForce(PSparticle *part, uint32_t numparticles, uint8_t force, uint16_t angle, uint8_t *counter); void applyFriction(PSparticle *part, uint8_t coefficient); // apply friction to specific particle void applyFriction(uint8_t coefficient); // apply friction to all used particles @@ -143,6 +151,7 @@ class ParticleSystem void fireParticleupdate(); //utility functions + void updatePSpointers(); // update the data pointers to current segment data space int32_t wraparound(int32_t w, int32_t maxvalue); int32_t calcForce_dV(int8_t force, uint8_t *counter); CRGB **allocate2Dbuffer(uint32_t cols, uint32_t rows); From 0a251aefbe7f50bf071a1ee8e11b1640c163d8d9 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Thu, 4 Apr 2024 17:51:32 +0200 Subject: [PATCH 056/219] another huge update, many improvements, fine-tune collision, fire and some other FX MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit -removed classic fire render as palette now looks much better -tweaked fire parameters to more awesome -added (optional) turbulence to fire using perlin-noise -ported spray FX to use PS-class -now definitely fixed asymmetrical collision issue: do not use bitshifts on negative numbers! -changed piling behaviour of particles, full rework. looks way more natural now and works much better -changed bouncing behavour: they now bounce at full diameter, making them stay fully in frame when laying on the floor -replaced all relevant bitshifts with divisions for higher accuracy throughout the PS -added new modes to particle box FX -changed a lot of FX parameters (finetuning) -changed all config strings to proper settings (matrix only) -fixed newly introduced bugs -added speedup/slowdown to vortex FX (aka candy, aka rotating sprays) -some renaming -fixed bugs… lots of bugs -merged rendering functions, removed obsolete stuff --- wled00/FX.cpp | 412 +++++++++++------------ wled00/FX.h | 4 +- wled00/FXparticleSystem.cpp | 648 +++++++++++++++++------------------- wled00/FXparticleSystem.h | 10 +- 4 files changed, 511 insertions(+), 563 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index c4ede81734..9fb83d736b 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -7882,13 +7882,13 @@ uint16_t mode_2Dwavingcell() { static const char _data_FX_MODE_2DWAVINGCELL[] PROGMEM = "Waving Cell@!,,Amplitude 1,Amplitude 2,Amplitude 3;;!;2"; /* - * Particle System Vortex (aka rotating sprays) + * Particle System Vortex * Particles sprayed from center with a rotating spray * Uses palette for particle color * by DedeHai (Damian Schneider) */ -uint16_t mode_particlerotatingspray(void) +uint16_t mode_particlevortex(void) { if (SEGLEN == 1) return mode_static(); @@ -7900,16 +7900,10 @@ uint16_t mode_particlerotatingspray(void) if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. { - if (!initParticleSystem(PartSys, 0)) + if (!initParticleSystem(PartSys)) return mode_static(); // allocation failed; //allocation failed - DEBUG_PRINTF_P(PSTR("PS pointer %p\n"), 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)); - SEGMENT.aux0 = 0; // starting angle + //SEGMENT.aux0 = 0; // starting angle SEGMENT.aux1 = 0x01; // check flags // TODO: use SEGMENT.step for smooth direction change numSprays = min(PartSys->numSources, (uint8_t) 8); @@ -7968,13 +7962,13 @@ uint16_t mode_particlerotatingspray(void) } } - //set rotation direction and speed - int32_t rotationspeed = SEGMENT.speed << 2; - bool direction = SEGMENT.check2; - + // set rotation direction and speed TODO: use SEGMENT.step to increment until speed is reached, increment speed depends on rotation speed as well fast rotations speed up faster too + // can use direction flag to determine current direction + bool direction = SEGMENT.check2; //no automatic direction change, set it to flag + if (SEGMENT.custom2 > 0) // automatic direction change enabled { - uint16_t changeinterval = (265 - SEGMENT.custom2); + uint16_t changeinterval = (270 - SEGMENT.custom2); direction = SEGMENT.aux1 & 0x02; //set direction according to flag if (SEGMENT.check3) // random interval @@ -7992,10 +7986,20 @@ uint16_t mode_particlerotatingspray(void) } } + int32_t currentspeed = (int32_t)SEGMENT.step; //make a signed integer out of step + int32_t speedincrement = 20 + (SEGMENT.speed >> 5) + (SEGMENT.custom2 >> 2); if (direction) - SEGMENT.aux0 += rotationspeed << 2; + { + if(currentspeed < (SEGMENT.speed << 2)) //speed is not on target speed yet + currentspeed += speedincrement; + } else - SEGMENT.aux0 -= rotationspeed << 2; + { + if (currentspeed > -(SEGMENT.speed << 2)) // speed is not on target speed yet + currentspeed -= speedincrement; + } + SEGMENT.aux0 += currentspeed; + SEGMENT.step = (uint32_t)currentspeed; //save it back // calculate angle offset for an even distribution uint16_t angleoffset = 0xFFFF / spraycount; @@ -8005,7 +8009,7 @@ uint16_t mode_particlerotatingspray(void) // 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) + PartSys->sources[j].var = (SEGMENT.custom3 >> 1); // emiting variation = nozzle size (custom 3 goes from 0-32) } //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) @@ -8023,7 +8027,7 @@ uint16_t mode_particlerotatingspray(void) PartSys->update(); //update all particles and render to frame return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEROTATINGSPRAY[] PROGMEM = "PS Vortex@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"; +static const char _data_FX_MODE_PARTICLEVORTEX[] PROGMEM = "PS Vortex@Rotation Speed,Particle Speed,Arms,Auto Flip,Nozzle,Random Color, Direction, Random Flip;;!;2;pal=56,sx=200,ix=190,c1=200,c2=0,c3=0,o1=0,o2=0,o3=0"; /* * Particle Fireworks @@ -8043,9 +8047,10 @@ uint16_t mode_particlefireworks(void) if (SEGMENT.call == 0) // initialization { - if (!initParticleSystem(PartSys, 0)) // init, no additional data needed + if (!initParticleSystem(PartSys)) // init, no additional data needed return mode_static(); // allocation failed; //allocation failed - PartSys->setKillOutOfBounds(true); //out of bounds particles dont return (except on top, taken care of by gravity setting) + PartSys->setKillOutOfBounds(true); //out of bounds particles dont return (except on top, taken care of by gravity setting) + PartSys->setWallHardness(100); //ground bounce is fixed numRockets = min(PartSys->numSources, (uint8_t)4); for (j = 0; j < numRockets; j++) { @@ -8066,7 +8071,7 @@ uint16_t mode_particlefireworks(void) PartSys->setWrapX(SEGMENT.check1); PartSys->setBounceY(SEGMENT.check2); - PartSys->setWallHardness(SEGMENT.custom2); + //PartSys->setWallHardness(SEGMENT.custom2); //not used anymore, can be removed PartSys->enableGravity(true, map(SEGMENT.custom3,0,31,0,10)); // todo: make it a slider to adjust // check each rocket's state and emit particles according to its state: moving up = emit exhaust, at top = explode; falling down = standby time @@ -8165,13 +8170,13 @@ uint16_t mode_particlefireworks(void) PartSys->sources[j].source.sat = random16(55) + 200; PartSys->sources[j].maxLife = 200; PartSys->sources[j].minLife = 100; - PartSys->sources[j].source.ttl = random16((1024 - ((uint32_t)SEGMENT.speed<<2))) + 100; // standby time til next launch - PartSys->sources[j].var = (SEGMENT.intensity >> 3) + 10; // speed variation around vx,vy (+/- var/2) + PartSys->sources[j].source.ttl = random16((1200 - ((uint32_t)SEGMENT.speed << 2))) + 550 - (SEGMENT.speed << 1); // standby time til next launch + PartSys->sources[j].var = ((SEGMENT.intensity >> 3) + 10) | 0x01; // speed variation around vx,vy (+/- var/2), only use odd nubmers } else if ( PartSys->sources[j].source.vy < 0) // rocket is exploded and time is up (ttl=0 and negative speed), relaunch it { // reinitialize rocket - PartSys->sources[j].source.y = 1; // start from bottom + PartSys->sources[j].source.y = PS_P_RADIUS; // start from bottom PartSys->sources[j].source.x = (rand() % (PartSys->maxX >> 1)) + (PartSys->maxX >> 2); // centered half PartSys->sources[j].source.vy = random16(SEGMENT.custom1 >> 3) + 5; // rocket speed depends also on rocket fuse PartSys->sources[j].source.vx = random16(5) - 2; //i.e. not perfectly straight up @@ -8181,14 +8186,14 @@ uint16_t mode_particlefireworks(void) PartSys->sources[j].minLife = 10; PartSys->sources[j].vx = 0; // emitting speed PartSys->sources[j].vy = 0; // emitting speed - PartSys->sources[j].var = 6; // speed variation around vx,vy (+/- var/2) + PartSys->sources[j].var = 5; // speed variation around vx,vy (+/- var/2) } } PartSys->update(); // update and render return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEFIREWORKS[] PROGMEM = "PS Fireworks@Launches,Explosion Size,Fuse,Bounce,Gravity,Cylinder,Ground,;;!;012;pal=11,sx=100,ix=50,c1=84,c2=128,c3=12,o1=0,o2=0,o3=0"; +static const char _data_FX_MODE_PARTICLEFIREWORKS[] PROGMEM = "PS Fireworks@Launches,Explosion Size,Fuse,Gravity,Cylinder,Ground,;;!;2;pal=11,sx=100,ix=50,c1=84,c2=128,c3=12,o1=0,o2=0,o3=0"; /* * Particle Volcano @@ -8201,12 +8206,12 @@ uint16_t mode_particlevolcano(void) if (SEGLEN == 1) return mode_static(); ParticleSystem *PartSys = NULL; - uint8_t numSprays; + uint8_t numSprays; //note: so far only one tested but more is possible uint32_t i = 0; if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. { - if (!initParticleSystem(PartSys, 0)) // init, no additional data needed + if (!initParticleSystem(PartSys)) // init, no additional data needed return mode_static(); // allocation failed; //allocation failed PartSys->setBounceY(true); PartSys->enableGravity(true); //enable with default gforce @@ -8250,14 +8255,14 @@ uint16_t mode_particlevolcano(void) { for (i = 0; i < numSprays; i++) { - PartSys->sources[i].source.y = 5; // reset to just above the lower edge, if zero, particles already 'bounce' at start and loose speed. + PartSys->sources[i].source.y = PS_P_RADIUS + 5; // reset to just above the lower edge that is allowed for bouncing particles, if zero, particles already 'bounce' at start and loose speed. PartSys->sources[i].source.vy = 0; //reset speed (so no extra particlesettin is required to keep the source 'afloat') PartSys->sources[i].source.hue++; // = random16(); //change hue of spray source // percycle = 1+(SEGMENT.intensity>>4); //how many particles are sprayed per cycle and how fast ist the color changing PartSys->sources[i].source.vx > 0 ? SEGMENT.custom1 >> 4 : -(SEGMENT.custom1 >> 4); // set moving speed but keep the direction PartSys->sources[i].vy = SEGMENT.speed >> 2; // emitting speed PartSys->sources[i].vx = 0; - PartSys->sources[i].var = SEGMENT.custom3; // emiting variation = nozzle size (custom 3 goes from 0-31) + PartSys->sources[i].var = SEGMENT.custom3 | 0x01; // emiting variation = nozzle size (custom 3 goes from 0-31), only use odd numbers PartSys->sources[i].source.ttl = 255; // source never dies, replenish its lifespan // spray[j].source.hue = random16(); //set random color for each particle (using palette) -> does not look good PartSys->sprayEmit(PartSys->sources[i]); @@ -8269,7 +8274,7 @@ uint16_t mode_particlevolcano(void) PartSys->update(); // update and render return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEVOLCANO[] PROGMEM = "PS Volcano@Speed,Intensity,Move,Bounce,Spread,Color by Age,Walls,Collisions;;!;012;pal=35,sx=100,ix=160,c1=0,c2=160,c3=10,o1=1,o2=0,o3=0"; +static const char _data_FX_MODE_PARTICLEVOLCANO[] PROGMEM = "PS Volcano@Speed,Intensity,Move,Bounce,Spread,Color by Age,Walls,Collisions;;!;2;pal=35,sx=100,ix=190,c1=0,c2=160,c3=6,o1=1,o2=0,o3=0"; /* * Particle Fire @@ -8287,7 +8292,7 @@ uint16_t mode_particlefire(void) if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. { - if (!initParticleSystem(PartSys, 0)) + if (!initParticleSystem(PartSys)) return mode_static(); // allocation failed; //allocation failed Serial.println("fireinit done"); SEGMENT.aux0 = rand(); // aux0 is wind position (index) in the perlin noise @@ -8297,8 +8302,7 @@ uint16_t mode_particlefire(void) { PartSys->sources[i].source.ttl = 0; PartSys->sources[i].source.vx = 0; // emitter moving speed; - PartSys->sources[i].source.vy = 0; - PartSys->sources[i].source.sat = 255; //!!!debug, test if transitions are better (non white) + PartSys->sources[i].source.vy = 0; // note: other parameters are set when creating the flame (see blow) } @@ -8317,8 +8321,9 @@ uint16_t mode_particlefire(void) PartSys->updateSystem(); // update system properties (dimensions and data pointers) uint32_t spread = (PartSys->maxX >> 5) * (SEGMENT.custom3 + 1); //fire around segment center (in subpixel points) - numFlames = min((uint)PartSys->numSources, (1 + ((spread / PS_P_RADIUS) << 1))); // number of flames used depends on spread with, good value is (fire width in pixel) * 2 + numFlames = min((uint32_t)PartSys->numSources, (1 + ((spread / PS_P_RADIUS) << 1))); // number of flames used depends on spread with, good value is (fire width in pixel) * 2 uint32_t percycle = numFlames*2/3;// / 2; // maximum number of particles emitted per cycle (TODO: for ESP826 maybe use flames/2) + // percycle = map(SEGMENT.intensity,0,255, 2, (numFlames*3) / 2); //TODO: will this give better flames or worse? PartSys->setWrapX(SEGMENT.check2); // update the flame sprays: @@ -8337,17 +8342,19 @@ uint16_t mode_particlefire(void) } PartSys->sources[i].source.y = -PS_P_RADIUS; // set the source below the frame - //PartSys->sources[i].source.ttl = 10 + random16((SEGMENT.custom1 * SEGMENT.custom1) >> 8) / (1 + (SEGMENT.speed >> 5)); //'hotness' of fire, faster flames reduce the effect or flame height will scale too much with speed - PartSys->sources[i].source.ttl = 5 + random16((SEGMENT.custom1 * SEGMENT.custom1) >> 7) / (2 + (SEGMENT.speed >> 4)); //'hotness' of fire, faster flames reduce the effect or flame height will scale too much with speed + //PartSys->sources[i].source.ttl = 10 + random16((SEGMENT.custom1 * SEGMENT.custom1) >> 8) / (1 + (SEGMENT.speed >> 5)); //old not really good, too intense + PartSys->sources[i].source.ttl = 5 + random16((SEGMENT.custom1 * SEGMENT.custom1) >> 7) / (2 + (SEGMENT.speed >> 4)); //'hotness' of fire, faster flames reduce the effect or flame height will scale too much with speed -> new, this works! + //PartSys->sources[i].source.ttl = 5 + random16(SEGMENT.custom1) / (1 + (SEGMENT.speed >> 5)); // this is experimental, fine tuning all parameters PartSys->sources[i].maxLife = random16(7) + 13; // defines flame height together with the vy speed, vy speed*maxlife/PS_P_RADIUS is the average flame height PartSys->sources[i].minLife = 4; PartSys->sources[i].vx = (int8_t)random16(4) - 2; // emitting speed (sideways) - PartSys->sources[i].vy = 5 + (SEGMENT.speed >> 2); // emitting speed (upwards) - PartSys->sources[i].var = random16(5) + 3; // speed variation around vx,vy (+/- var/2) + PartSys->sources[i].vy = 5 + (SEGMENT.speed >> 2); // emitting speed (upwards) -> this is good + //PartSys->sources[i].var = (random16(5) + 3) | 0x01; // speed variation around vx,vy (+/- var/2), only use odd numbers + PartSys->sources[i].var = (random16(2 + (SEGMENT.speed >> 5)) + 3) | 0x01; // speed variation around vx,vy (+/- var/2), only use odd numbers } } - // percycle = map(SEGMENT.intensity,0,255, 2, (numFlames*3) / 2); //TODO: will this give better flames or worse? + if (SEGMENT.call & 0x01) // update noise position every second frames, also add wind { SEGMENT.aux0++; // position in the perlin noise matrix for wind generation @@ -8360,7 +8367,7 @@ uint16_t mode_particlefire(void) } SEGMENT.step++; -//this is a work in progress, maybe will be added... hard to find settings that look good TODO: looks ok on speed 130, need to tune it for other speeds +//this is a work in progress... hard to find settings that look good TODO: looks ok on speed 130, need to tune it for other speeds if (SEGMENT.check3) { if (SEGMENT.call % map(SEGMENT.speed,0,255,4,15)==0) // update noise position every xth frames, also add wind -> has do be according to speed. 135-> every third frame @@ -8395,12 +8402,35 @@ uint16_t mode_particlefire(void) PartSys->flameEmit(PartSys->sources[j]); j = (j + 1) % numFlames; } +/* + j=5; + //a test: emitting one base particle per frame + if (SEGMENT.check1) + { + for (i = 0; i < PartSys->usedParticles; i++) // emit particles //todo: if this works, make it the last spray + { + if (PartSys->particles[i].ttl == 0) // find a dead particle + { + // emit particle at random position over the top of the matrix (random16 is not random enough) + PartSys->particles[i].vy = 1 ;//+ (SEGMENT.speed >> 3); + PartSys->particles[i].ttl = 10;//(PS_P_RADIUS<<2) / PartSys->particles[i].vy; + PartSys->particles[i].x = (PartSys->maxX >> 1) - (spread>>1) + (rand() % spread) ; + PartSys->particles[i].y = 0; + PartSys->particles[i].vx = 0;//(((int16_t)random(SEGMENT.custom1)) - (SEGMENT.custom1 >> 1) + 5) >> 1; // side speed is +/- a quarter of the custom1 slider + Serial.print("*"); + j--; + } + if(j==0) break; + } + Serial.println("B"); + }*/ + - PartSys->updateFire(SEGMENT.intensity, true); //SEGMENT.check1); // update and render the fire, colormode is determined by custom 3 slider + PartSys->updateFire(SEGMENT.intensity); // update and render the fire return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEFIRE[] PROGMEM = "PS Fire@Speed,Intensity,Height,Wind,Spread,Palette,Cylinder,Turbulence;;!;035;sx=130,ix=120,c1=110,c2=128,c3=22,o1=0"; +static const char _data_FX_MODE_PARTICLEFIRE[] PROGMEM = "PS Fire@Speed,Intensity,Base Heat,Wind,Spread,,Cylinder,Turbulence;;!;2;pal=35,sx=130,ix=120,c1=110,c2=128,c3=22,o1=0"; /* PS Ballpit: particles falling down, user can enable these three options: X-wraparound, side bounce, ground bounce @@ -8409,7 +8439,7 @@ 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) +uint16_t mode_particlepit(void) { if (SEGLEN == 1) return mode_static(); @@ -8417,12 +8447,11 @@ uint16_t mode_particlefall(void) if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. { - if (!initParticleSystem(PartSys, 0)) //init + if (!initParticleSystem(PartSys)) //init return mode_static(); // allocation failed; //allocation failed PartSys->setKillOutOfBounds(true); PartSys->enableGravity(true); PartSys->setUsedParticles((PartSys->numParticles*3)/2); //use 2/3 of available particles - Serial.println("ballpit done"); } else PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS @@ -8467,18 +8496,18 @@ uint16_t mode_particlefall(void) } } - uint32_t frictioncoefficient = 1; - if (SEGMENT.speed < 50) // for low speeds, apply more friction - frictioncoefficient = 50 - SEGMENT.speed; + uint32_t frictioncoefficient = 1; + if (SEGMENT.speed < 50) // for low speeds, apply more friction + frictioncoefficient = 50 - SEGMENT.speed; - if (SEGMENT.call % 3 == 0) - PartSys->applyFriction(frictioncoefficient); // add some frictino to help smooth things + if (SEGMENT.call % (3+(SEGMENT.custom2>>2)) == 0) + PartSys->applyFriction(frictioncoefficient); PartSys->update(); // update and render return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEFALL[] PROGMEM = "PS Ballpit@Speed,Intensity,Randomness,Hardness,Saturation,Cylinder,Walls,Ground;;!;012;pal=11,sx=100,ix=200,c1=31,c2=100,c3=31,o1=0,o2=0,o3=1"; +static const char _data_FX_MODE_PARTICLEPIT[] PROGMEM = "PS Ballpit@Speed,Intensity,Randomness,Hardness,Saturation,Cylinder,Walls,Ground;;!;2;pal=11,sx=100,ix=200,c1=120,c2=100,c3=31,o1=0,o2=0,o3=1"; /* * Particle Waterfall @@ -8495,7 +8524,7 @@ uint16_t mode_particlewaterfall(void) if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. { - if (!initParticleSystem(PartSys, 0)) // init, no additional data needed + if (!initParticleSystem(PartSys)) // init, no additional data needed return mode_static(); // allocation failed; //allocation failed PartSys->enableGravity(true); // enable with default gforce PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting) @@ -8554,7 +8583,7 @@ uint16_t mode_particlewaterfall(void) PartSys->sources[i].source.x = map(SEGMENT.custom3, 0, 31, 0, (PartSys->maxXpixel - numSprays * 2) * PS_P_RADIUS) + i * PS_P_RADIUS * 2; // emitter position PartSys->sources[i].source.y = PartSys->maxY + (PS_P_RADIUS * ((i<<2) + 4)); // source y position, few pixels above the top to increase spreading before entering the matrix //PartSys->sources[i].source.ttl = 255; // source never dies, replenish its lifespan TODO: source is not moved? - PartSys->sources[i].var = SEGMENT.custom1 >> 3; // emiting variation 0-32 + PartSys->sources[i].var = (SEGMENT.custom1 >> 3) | 0x01; // emiting variation 0-32, only use odd numbers } for (i = 0; i < numSprays; i++) @@ -8563,13 +8592,13 @@ uint16_t mode_particlewaterfall(void) } } - if (SEGMENT.call % 8 == 0) - PartSys->applyFriction(1); + if (SEGMENT.call % 20 == 0) + PartSys->applyFriction(1); // add just a tiny amount of friction to help smooth things PartSys->update(); // update and render 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"; +static const char _data_FX_MODE_PARTICLEWATERFALL[] PROGMEM = "PS Waterfall@Speed,Intensity,Variation,Collisions,Position,Cylinder,Walls,Ground;;!;2;pal=9,sx=15,ix=200,c1=60,c2=160,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) @@ -8586,7 +8615,7 @@ uint16_t mode_particlebox(void) if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. { - if (!initParticleSystem(PartSys, 0)) // init + if (!initParticleSystem(PartSys)) // init return mode_static(); // allocation failed; //allocation failed } else @@ -8615,13 +8644,13 @@ uint16_t mode_particlebox(void) #endif PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 10, maxnumParticles)); - if (SEGMENT.call == 0) // initialization of particles + if (SEGMENT.call == 0) // initialization of particles (cannot be done in above loop, only if code lines above here are copied there) { SEGMENT.aux0 = rand(); // position in perlin noise for (i = 0; i < maxnumParticles; i++) { PartSys->particles[i].ttl = 500; //set all particles alive (not all are rendered though) - PartSys->particles[i].hue = i * 4; // full color range + PartSys->particles[i].hue = i * 5; // color range PartSys->particles[i].sat = 255; // set full saturation (lets palette choose the color) PartSys->particles[i].x = map(i, 0, maxnumParticles, 1, PartSys->maxX); // distribute along x according to color PartSys->particles[i].y = random16(PartSys->maxY); // randomly in y direction @@ -8633,23 +8662,35 @@ uint16_t mode_particlebox(void) { int32_t xgravity; int32_t ygravity; + int32_t increment = (SEGMENT.speed >> 6) + 1; + if(SEGMENT.check2) //direction + SEGMENT.aux0 += increment; // update counter + else + SEGMENT.aux0 -= increment; - SEGMENT.aux0 += (SEGMENT.speed >> 6) + 1; // update position in noise - - xgravity = ((int16_t)inoise8(SEGMENT.aux0) - 127); //TODO: inoise 16 would be faster? - ygravity = ((int16_t)inoise8(SEGMENT.aux0 + 10000) - 127); - if (SEGMENT.check1) //sloshing, y force is alwys downwards + if(SEGMENT.check1) //random, use perlin noise + { + xgravity = ((int16_t)inoise8(SEGMENT.aux0) - 127); // TODO: inoise 16 would be faster? + ygravity = ((int16_t)inoise8(SEGMENT.aux0 + 10000) - 127); + // scale the gravity force down + xgravity /= 2 + ((256 - SEGMENT.custom1) >> 3); //(divide by 1-32) + ygravity /= 2 + ((256 - SEGMENT.custom1) >> 3); + } + else //go in a circle + { + // PartSys->applyAngleForce(PartSys->particles, PartSys->usedParticles, SEGMENT.custom1>>2, SEGMENT.aux0<<8);//not used, calculate here directly as perlin noise force is in x and y, not angle + xgravity = ((int32_t)(SEGMENT.custom1 >> 3) * cos16(SEGMENT.aux0 << 8)) / 0xFFFF; + ygravity = ((int32_t)(SEGMENT.custom1 >> 3) * sin16(SEGMENT.aux0 << 8)) / 0xFFFF; + } + if (SEGMENT.check3) //sloshing, y force is alwys downwards { if(ygravity > 0) ygravity = -ygravity; } - // scale the gravity force down - xgravity /= 2 + ((256 - SEGMENT.custom1) >> 3); //(divide by 1-32) - ygravity /= 2 + ((256 - SEGMENT.custom1) >> 3); PartSys->applyForce(PartSys->particles, PartSys->usedParticles, xgravity, ygravity); - - //reset particle TTL so they never die + + // reset particle TTL so they never die for (i = 0; i < PartSys->usedParticles; i++) { if (PartSys->particles[i].ttl > 0) @@ -8659,21 +8700,20 @@ uint16_t mode_particlebox(void) } } - if (SEGMENT.call % 8 == 0) - PartSys->applyFriction(1); + if (SEGMENT.call % (32-SEGMENT.custom3) == 0) + PartSys->applyFriction(2); PartSys->update(); // update and render 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=100,c2=210,o1=0"; +static const char _data_FX_MODE_PARTICLEBOX[] PROGMEM = "PS Box@Speed,Particles,Tilt Strength,Hardness,Friction,Random,Direction,Sloshing;;!;2;pal=43,sx=120,ix=100,c1=100,c2=210,o1=1"; /* 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) { if (SEGLEN == 1) @@ -8682,13 +8722,14 @@ uint16_t mode_particleperlin(void) uint32_t i; if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. { - if (!initParticleSystem(PartSys, 0)) // init + if (!initParticleSystem(PartSys)) // init return mode_static(); // allocation failed; //allocation failed PartSys->setKillOutOfBounds(true); //should never happen, but lets make sure there are no stray particles SEGMENT.aux0 = rand(); for (i = 0; i < PartSys->numParticles; i++) { PartSys->particles[i].sat = 255; // full saturation, color set by palette + PartSys->particles[i].collide = true; // all particles colllide } } else @@ -8703,8 +8744,9 @@ uint16_t mode_particleperlin(void) PartSys->setWrapX(SEGMENT.check1); PartSys->setBounceX(!SEGMENT.check1); PartSys->setBounceY(true); - PartSys->setWallHardness(255); - uint32_t displayparticles = map(SEGMENT.intensity, 0, 255, 10, PartSys->numParticles); + PartSys->setWallHardness(SEGMENT.custom1); //wall hardness + PartSys->enableParticleCollisions(SEGMENT.check3, SEGMENT.custom1); // enable collisions and set particle collision hardness + uint32_t displayparticles = map(SEGMENT.intensity, 0, 255, 10, PartSys->numParticles>>1); PartSys->setUsedParticles(displayparticles); // apply 'gravity' from a 2D perlin noise map @@ -8719,13 +8761,13 @@ uint16_t mode_particleperlin(void) PartSys->particles[i].x = random16(PartSys->maxX); PartSys->particles[i].y = random16(PartSys->maxY); } - uint16_t xnoise = (PartSys->particles[i].x / (16 - (SEGMENT.custom3>>1))); // position in perlin noise, scaled by slider - uint16_t ynoise = (PartSys->particles[i].y / (16 - (SEGMENT.custom3>>1))); + uint32_t scale = 16 - ((31 - SEGMENT.custom3) >> 1); + uint16_t xnoise = PartSys->particles[i].x / scale; // position in perlin noise, scaled by slider + uint16_t ynoise = PartSys->particles[i].y / scale; int16_t baseheight = inoise8(xnoise, ynoise, SEGMENT.aux0); // noise value at particle position PartSys->particles[i].hue = baseheight; // color particles to perlin noise value - if (SEGMENT.call % 6 == 0) // do not apply the force every frame, is too chaotic + if (SEGMENT.call % 10 == 0) // do not apply the force every frame, is too chaotic { - Serial.print(i); Serial.print(" "); int8_t xslope = (baseheight - (int16_t)inoise8(xnoise + 10, ynoise, SEGMENT.aux0)); int8_t yslope = (baseheight - (int16_t)inoise8(xnoise, ynoise + 10, SEGMENT.aux0)); PartSys->applyForce(&(PartSys->particles[i]), 1, xslope, yslope); @@ -8738,10 +8780,10 @@ uint16_t mode_particleperlin(void) PartSys->update(); // update and render return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEPERLIN[] PROGMEM = "PS Fuzzy Noise@Speed,Particles,,Friction,Scale,Cylinder;;!;012;pal=54,sx=70;ix=200,c1=120,c2=120,c3=20,o1=0"; +static const char _data_FX_MODE_PARTICLEPERLIN[] PROGMEM = "PS Fuzzy Noise@Speed,Particles,Bounce,Friction,Scale,Cylinder,,Collisions;;!;2;pal=54,sx=75,ix=200,c1=255,c2=180,c3=20,o1=0"; /* - * Particle smashing down like meteorites and exploding as they hit the ground, has many parameters to play with + * Particle smashing down like meteors and exploding as they hit the ground, has many parameters to play with * by DedeHai (Damian Schneider) */ @@ -8752,12 +8794,13 @@ uint16_t mode_particleimpact(void) ParticleSystem *PartSys = NULL; uint32_t i = 0; uint8_t MaxNumMeteors; + PSsettings meteorsettings = {0, 0, 0, 1, 0, 1, 0, 0}; // PS settings for meteors: bounceY and gravity enabled (todo: if ESP8266 is ok with out of bounds particles, this can be removed, it just takes care of the kill out of bounds setting) if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. { - if (!initParticleSystem(PartSys, 0)) // init, no additional data needed - return mode_static(); // allocation failed; //allocation failed - // PartSys->setKillOutOfBounds(true); + if (!initParticleSystem(PartSys)) // init, no additional data needed + return mode_static(); // allocation failed; //allocation failed + PartSys->setKillOutOfBounds(false); //explosions out of frame ar allowed, set to true to save particles (TODO: better enable it in ESP8266?) PartSys->enableGravity(true); PartSys->setBounceY(true); //always use ground bounce // PartSys->setUsedParticles((PartSys->numParticles * 3) / 2); // use 2/3 of available particles @@ -8767,8 +8810,8 @@ uint16_t mode_particleimpact(void) PartSys->sources[i].vx = 0; //emit speed in x PartSys->sources[i].source.y = 10; PartSys->sources[i].source.ttl = random16(20 * i); // set initial delay for meteors - PartSys->sources[i].source.vy = 10; // at positive speeds, no particles are emitted and if particle dies, it will be relaunched - PartSys->sources[i].source.sat = 255; // full saturation, color chosen by palette + PartSys->sources[i].source.vy = 10; // at positive speeds, no particles are emitted and if particle dies, it will be relaunched + PartSys->sources[i].source.sat = 255; // full saturation, color chosen by palette } } else @@ -8779,14 +8822,15 @@ uint16_t mode_particleimpact(void) Serial.println("ERROR: paticle system not found, nullpointer"); return mode_static(); // something went wrong, no data! (TODO: ask how to handle this so it always works) } - MaxNumMeteors = min(PartSys->numSources, (uint8_t)8); - uint8_t numMeteors = map(SEGMENT.custom3, 0, 31, 1, MaxNumMeteors); // number of meteors to use for animation + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) PartSys->setWrapX(SEGMENT.check1); PartSys->setBounceX(SEGMENT.check2); - PartSys->setWallHardness(SEGMENT.custom2); // + PartSys->setWallHardness(SEGMENT.custom2); PartSys->enableParticleCollisions(SEGMENT.check3, SEGMENT.custom2); // enable collisions and set particle collision hardness - + MaxNumMeteors = min(PartSys->numSources, (uint8_t)8); + uint8_t numMeteors = map(SEGMENT.custom3, 0, 31, 1, MaxNumMeteors); // number of meteors to use for animation uint32_t emitparticles; // number of particles to emit for each rocket's state @@ -8825,11 +8869,11 @@ uint16_t mode_particleimpact(void) { if (PartSys->sources[i].source.ttl) { - PartSys->applyGravity(&PartSys->sources[i].source); - PartSys->particleMoveUpdate(PartSys->sources[i].source, PartSys->particlesettings); + PartSys->applyGravity(&PartSys->sources[i].source); + PartSys->particleMoveUpdate(PartSys->sources[i].source, meteorsettings); // if source reaches the bottom, set speed to 0 so it will explode on next function call (handled above) - if ((PartSys->sources[i].source.y < PS_P_RADIUS) && ( PartSys->sources[i].source.vy < 0)) // reached the bottom pixel on its way down + if ((PartSys->sources[i].source.y < PS_P_RADIUS<<1) && ( PartSys->sources[i].source.vy < 0)) // reached the bottom pixel on its way down { PartSys->sources[i].source.vy = 0; // set speed zero so it will explode PartSys->sources[i].source.vx = 0; @@ -8845,7 +8889,7 @@ uint16_t mode_particleimpact(void) PartSys->sources[i].source.ttl = random16((255 - SEGMENT.speed)) + 10; // standby time til next launch (in frames at 42fps, max of 265 is about 6 seconds #endif PartSys->sources[i].vy = (SEGMENT.custom1 >> 2); // emitting speed y - PartSys->sources[i].var = (SEGMENT.custom1 >> 1); // speed variation around vx,vy (+/- var/2) + PartSys->sources[i].var = (SEGMENT.custom1 >> 1) | 0x01; // speed variation around vx,vy (+/- var/2), only use odd numbers } } else if ( PartSys->sources[i].source.vy > 0) // meteor is exploded and time is up (ttl==0 and positive speed), relaunch it @@ -8868,7 +8912,7 @@ uint16_t mode_particleimpact(void) PartSys->update(); // update and render return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEIMPACT[] PROGMEM = "PS Impact@Launches,Explosion Size,Explosion Force,Bounce,Meteors,Cylinder,Walls,Collisions;;!;012;pal=0,sx=32,ix=85,c1=100,c2=100,c3=8,o1=0,o2=0,o3=1"; +static const char _data_FX_MODE_PARTICLEIMPACT[] PROGMEM = "PS Impact@Launches,Explosion Size,Explosion Force,Bounce,Meteors,Cylinder,Walls,Collisions;;!;2;pal=0,sx=32,ix=85,c1=120,c2=125,c3=8,o1=0,o2=0,o3=1"; /* Particle Attractor, a particle attractor sits in the matrix center, a spray bounces around and seeds particles @@ -8890,7 +8934,7 @@ uint16_t mode_particleattractor(void) if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. { uint32_t numParticles = (calculateNumberOfParticles() * 2) / 3; // use 75% of available particles to keep FPS high (also we need an attractor particle) - if (!initParticleSystem(PartSys, numParticles+10)) // init, need one extra byte per used particle for force counters //!!! looking for a reason it crashes... + if (!initParticleSystem(PartSys, numParticles)) // init, need one extra byte per used particle for force counters return mode_static(); // allocation failed; //allocation failed PartSys->sources[0].source.hue = random16(); @@ -8910,7 +8954,7 @@ uint16_t mode_particleattractor(void) #endif PartSys->sources[0].vx = 0; // emitting speed PartSys->sources[0].vy = 0; // emitting speed - PartSys->sources[0].var = 6; // emiting variation + PartSys->sources[0].var = 7; // emiting variation } else PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS @@ -8976,128 +9020,90 @@ uint16_t mode_particleattractor(void) PartSys->update(); // update and render return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEATTRACTOR[] PROGMEM = "PS Attractor@Mass,Particles,Emit Speed,Collisions,Friction,Color by Age,Move,Swallow;;!;012;pal=9,sx=100,ix=82,c1=190,c2=0,o1=0,o2=0,o3=0"; +static const char _data_FX_MODE_PARTICLEATTRACTOR[] PROGMEM = "PS Attractor@Mass,Particles,Emit Speed,Collisions,Friction,Color by Age,Move,Swallow;;!;2;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 +Particle Spray, just a particle spray with many parameters Uses palette for particle color by DedeHai (Damian Schneider) */ -/* + uint16_t mode_particlespray(void) { - if (SEGLEN == 1) return mode_static(); + ParticleSystem *PartSys = NULL; + uint8_t numSprays; + uint32_t i; + const uint8_t hardness = 200; //collision hardness is fixed - const uint32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; - const uint32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); - // particle system x dimension - const uint32_t Max_x = (cols * PS_P_RADIUS - 1); - const uint32_t Max_y = (rows * PS_P_RADIUS - 1); - -#ifdef ESP8266 - const uint32_t numParticles = 80; -#else - const uint32_t numParticles = 450; -#endif - - const uint8_t numSprays = 1; - uint8_t percycle = numSprays; // maximum number of particles emitted per cycle - - PSparticle *particles; - PSsource *spray; - - // allocate memory and divide it into proper pointers, max is 32k for all segments. - uint32_t dataSize = sizeof(PSparticle) * numParticles; - dataSize += sizeof(PSsource) * (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 - - uint32_t i = 0; - uint32_t j = 0; - - if (SEGMENT.call == 0) // initialization + if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. { - for (i = 0; i < numParticles; i++) - { - particles[i].ttl = 0; - } + if (!initParticleSystem(PartSys)) // init, no additional data needed + return mode_static(); // allocation failed; //allocation failed + PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting) + PartSys->setBounceY(true); + numSprays = min(PartSys->numSources, (uint8_t)1); // number of sprays for (i = 0; i < numSprays; i++) { - PartSys->sources[i].source.hue = random16(); - PartSys->sources[i].source.sat = 255; // set full saturation - PartSys->sources[i].source.x = (cols * PS_P_RADIUS) / (numSprays + 1) * (i + 1); - PartSys->sources[i].source.y = 5; // just above the lower edge, if zero, particles already 'bounce' at start and loose speed. - PartSys->sources[i].source.vx = 0; - PartSys->sources[i].maxLife = 300; // lifetime in frames - PartSys->sources[i].minLife = 20; - PartSys->sources[i].source.collide = true; // seeded particles will collide - PartSys->sources[i].vx = 0; // emitting speed - PartSys->sources[i].vy = 0; // emitting speed - PartSys->sources[i].var = 10; + PartSys->sources[i].source.hue = random16(); + PartSys->sources[i].source.sat = 255; // set full saturation + PartSys->sources[i].source.x = PartSys->maxX / (numSprays + 1) * (i + 1); // distribute evenly + PartSys->sources[i].maxLife = 300; // lifetime in frames + PartSys->sources[i].minLife = 100; + PartSys->sources[i].source.collide = true; // seeded particles will collide (if enabled) + PartSys->sources[i].var = 7; } } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + + if (PartSys == NULL) + { + DEBUG_PRINT(F("ERROR: FX PartSys nullpointer")); + return mode_static(); // something went wrong, no data! + } + + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setBounceX(!SEGMENT.check2); + PartSys->setWrapX(SEGMENT.check2); + PartSys->setWallHardness(hardness); + PartSys->enableGravity(SEGMENT.check1); + numSprays = min(PartSys->numSources, (uint8_t)1); // number of sprays + + if (SEGMENT.check3) // collisions enabled + PartSys->enableParticleCollisions(true, hardness); // enable collisions and set particle collision hardness + else + PartSys->enableParticleCollisions(false); + + uint8_t percycle = numSprays; // maximum number of particles emitted per cycle - // change source emitting color from time to time + // change source properties if (SEGMENT.call % (11 - (SEGMENT.intensity / 25)) == 0) // every nth frame, cycle color and emit particles { for (i = 0; i < numSprays; i++) { PartSys->sources[i].source.hue++; // = random16(); //change hue of spray source // PartSys->sources[i].var = SEGMENT.custom3; // emiting variation = nozzle size (custom 3 goes from 0-32) - PartSys->sources[i].source.x = map(SEGMENT.custom1, 0, 255, 0, Max_x); - PartSys->sources[i].source.y = map(SEGMENT.custom2, 0, 255, 0, Max_y); + PartSys->sources[i].source.x = map(SEGMENT.custom1, 0, 255, 0, PartSys->maxX); + PartSys->sources[i].source.y = map(SEGMENT.custom2, 0, 255, 0, PartSys->maxY); } - i = 0; - j = 0; - for (i = 0; i < numParticles; i++) - { - if (particles[i].ttl == 0) // find a dead particle - { - // spray[j].source.hue = random16(); //set random color for each particle (using palette) - Emitter_Angle_emit(&spray[j], &particles[i], 255-(SEGMENT.custom3 << 3), SEGMENT.speed >> 2); - j = (j + 1) % numSprays; - if (percycle-- == 0) - { - break; // quit loop if all particles of this round emitted - } - } - } - } - - const uint8_t hardness = 200; - if (SEGMENT.check3) // collisions enabled - detectCollisions(particles, numParticles, hardness); - - - for (i = 0; i < numParticles; i++) - { - //particles[i].hue = min((uint16_t)220, particles[i].ttl); - if (SEGMENT.check1) //use gravity - Particle_Gravity_update(&particles[i], SEGMENT.check2, SEGMENT.check2 == 0, true, hardness); - else //bounce particles + uint8_t j = 0; + for (i = 0; i < percycle; i++) { - if(SEGMENT.check2) //wrap x - Particle_Move_update(&particles[i], true, true, false); - else //bounce - Particle_Bounce_update(&particles[i], hardness); + // spray[j].source.hue = random16(); //set random color for each particle (using palette) + PartSys->angleEmit(PartSys->sources[j], (256 - (((int32_t)SEGMENT.custom3 + 1) << 3)) << 8, SEGMENT.speed >> 2); + j = (j + 1) % numSprays; } } - - // render the particles - ParticleSys_render(particles, numParticles, SEGMENT.check2, false); - + PartSys->update(); // update and render 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"; -*/ +static const char _data_FX_MODE_PARTICLESPRAY[] PROGMEM = "PS Spray@Speed,!,Left/Right,Up/Down,Angle,Gravity,Cylinder/Square,Collisions;;!;2;pal=0,sx=150,ix=150,c1=220,c2=30,c3=21,o1=0,o2=0,o3=0"; + /* Particle base Graphical Equalizer @@ -9114,7 +9120,7 @@ uint16_t mode_particleGEQ(void) if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. { - if (!initParticleSystem(PartSys, 0)) // init + if (!initParticleSystem(PartSys)) // init return mode_static(); // allocation failed; //allocation failed PartSys->setKillOutOfBounds(true); PartSys->setUsedParticles((PartSys->numParticles * 3) / 2); // use 2/3 of available particles @@ -9129,7 +9135,8 @@ uint16_t mode_particleGEQ(void) } uint32_t i; - //set particle system properties + // set particle system properties + PartSys->updateSystem(); // update system properties (dimensions and data pointers) PartSys->setWrapX(SEGMENT.check1); PartSys->setBounceX(SEGMENT.check2); PartSys->setBounceY(SEGMENT.check3); @@ -9196,7 +9203,7 @@ uint16_t mode_particleGEQ(void) PartSys->update(); // update and render return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEGEQ[] PROGMEM = "PS Equalizer@Speed,Intensity,Diverge,Bounce,Gravity,Cylinder,Walls,Floor;;!;012;pal=0,sx=155,ix=200,c1=0,c2=128,c3=31,o1=0,o2=0,o3=0"; +static const char _data_FX_MODE_PARTICLEGEQ[] PROGMEM = "PS Equalizer@Speed,Intensity,Diverge,Bounce,Gravity,Cylinder,Walls,Floor;;!;2f;pal=0,sx=155,ix=200,c1=0,c2=128,c3=31,o1=0,o2=0,o3=0"; /* @@ -9571,26 +9578,19 @@ 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_PARTICLEVORTEX, &mode_particlevortex, _data_FX_MODE_PARTICLEVORTEX); addEffect(FX_MODE_PARTICLEFIREWORKS, &mode_particlefireworks, _data_FX_MODE_PARTICLEFIREWORKS); addEffect(FX_MODE_PARTICLEVOLCANO, &mode_particlevolcano, _data_FX_MODE_PARTICLEVOLCANO); addEffect(FX_MODE_PARTICLEFIRE, &mode_particlefire, _data_FX_MODE_PARTICLEFIRE); - addEffect(FX_MODE_PARTICLEFALL, &mode_particlefall, _data_FX_MODE_PARTICLEFALL); + addEffect(FX_MODE_PARTICLEPIT, &mode_particlepit, _data_FX_MODE_PARTICLEPIT); addEffect(FX_MODE_PARTICLEWATERFALL, &mode_particlewaterfall, _data_FX_MODE_PARTICLEWATERFALL); addEffect(FX_MODE_PARTICLEBOX, &mode_particlebox, _data_FX_MODE_PARTICLEBOX); addEffect(FX_MODE_PARTICLEPERLIN, &mode_particleperlin, _data_FX_MODE_PARTICLEPERLIN); addEffect(FX_MODE_PARTICLEIMPACT, &mode_particleimpact, _data_FX_MODE_PARTICLEIMPACT); addEffect(FX_MODE_PARTICLEATTRACTOR, &mode_particleattractor, _data_FX_MODE_PARTICLEATTRACTOR); - addEffect(FX_MODE_PARTICLESGEQ, &mode_particleGEQ, _data_FX_MODE_PARTICLEGEQ); - - - - - /* - 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.h b/wled00/FX.h index 151145281c..bfdee58173 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -321,9 +321,9 @@ #define FX_MODE_PARTICLEVOLCANO 187 #define FX_MODE_PARTICLEFIRE 188 #define FX_MODE_PARTICLEFIREWORKS 189 -#define FX_MODE_PARTICLEROTATINGSPRAY 190 +#define FX_MODE_PARTICLEVORTEX 190 #define FX_MODE_PARTICLEPERLIN 191 -#define FX_MODE_PARTICLEFALL 192 +#define FX_MODE_PARTICLEPIT 192 #define FX_MODE_PARTICLEBOX 193 #define FX_MODE_PARTICLEATTRACTOR 194 #define FX_MODE_PARTICLEIMPACT 195 diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index bf5eee0121..1cd99e41c0 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -93,10 +93,10 @@ void ParticleSystem::update(void) } //update function for fire animation -void ParticleSystem::updateFire(uint32_t intensity, bool usepalette) +void ParticleSystem::updateFire(uint32_t intensity) { fireParticleupdate(); - renderParticleFire(intensity, usepalette); + ParticleSys_render(true, intensity); } void ParticleSystem::setUsedParticles(uint16_t num) @@ -106,12 +106,12 @@ void ParticleSystem::setUsedParticles(uint16_t num) void ParticleSystem::setWallHardness(uint8_t hardness) { - wallHardness = hardness + 1; // at a value of 256, no energy is lost in collisions + wallHardness = hardness; } void ParticleSystem::setCollisionHardness(uint8_t hardness) -{ - collisionHardness = hardness + 1; // at a value of 256, no energy is lost in collisions +{ + collisionHardness = hardness; } void ParticleSystem::setMatrixSize(uint16_t x, uint16_t y) @@ -231,8 +231,8 @@ void ParticleSystem::flameEmit(PSsource &emitter) // angle = 0 means in positive x-direction (i.e. to the right) void ParticleSystem::angleEmit(PSsource &emitter, uint16_t angle, uint32_t speed) { - emitter.vx = ((int32_t)cos16(angle) * speed) >> 15; // cos16() and sin16() return signed 16bit - emitter.vy = ((int32_t)sin16(angle) * speed) >> 15; + emitter.vx = ((int32_t)cos16(angle) * speed) / 32767; // cos16() and sin16() return signed 16bit + emitter.vy = ((int32_t)sin16(angle) * speed) / 32767; // note: cannot use bit shifts as bit shifting is asymmetrical for positive and negative numbers and this needs to be accurate! sprayEmit(emitter); } @@ -246,29 +246,28 @@ void ParticleSystem::particleMoveUpdate(PSparticle &part, PSsettings &options) part.ttl--; if (particlesettings.colorByAge) part.hue = part.ttl > 255 ? 255 : part.ttl; //set color to ttl - // apply velocity - // Serial.print("x:"); - // Serial.print(part.x); - // Serial.print("y:"); - // Serial.print(part.y); - int32_t newX = part.x + (int16_t)part.vx; + + int32_t newX = part.x + (int16_t)part.vx; int32_t newY = part.y + (int16_t)part.vy; - //Serial.print(" "); - //Serial.print(newY); part.outofbounds = 0; // reset out of bounds (in case particle was created outside the matrix and is now moving into view) - if (((newX < 0) || (newX > maxX))) // check if particle reached an edge + //if wall collisions are enabled, bounce them before they reach the edge, it looks much nicer if the particle is not half out of vew + if (options.bounceX) { - if (options.bounceX) // particle was in view and now moved out -> bounce it + if ((newX < PS_P_RADIUS) || (newX > maxX - PS_P_RADIUS)) // reached a wall { - part.vx = -part.vx; // invert speed - part.vx = (part.vx * wallHardness) >> 8; // reduce speed as energy is lost on non-hard surface - if (newX < 0) - newX = 0;//-newX; set to boarder (less acurate but at high speeds they will bounce mid frame if just flipped) + part.vx = -part.vx; // invert speed + part.vx = (part.vx * wallHardness) / 255; // reduce speed as energy is lost on non-hard surface + if (newX < PS_P_RADIUS) + newX = PS_P_RADIUS; // fast particles will never reach the edge if position is inverted else - newX = maxX; // maxX - (newX - (int32_t)maxX); + newX = maxX - PS_P_RADIUS; } - else if (options.wrapX) + } + + if ((newX < 0) || (newX > maxX)) // check if particle reached an edge + { + if (options.wrapX) { newX = wraparound(newX, maxX); } @@ -279,32 +278,37 @@ void ParticleSystem::particleMoveUpdate(PSparticle &part, PSsettings &options) part.ttl = 0; } } - if (((newY < 0) || (newY > maxY))) // check if particle reached an edge + + if (options.bounceY) { - if (options.bounceY) // particle was in view and now moved out -> bounce it + if ((newY < PS_P_RADIUS) || (newY > maxY - PS_P_RADIUS)) // reached floor / ceiling { - if (newY > maxY) + if (newY < PS_P_RADIUS) // bounce at bottom { - if (options.useGravity) // do not bounce on top if using gravity (open container) + part.vy = -part.vy; // invert speed + part.vy = (part.vy * wallHardness) / 255; // reduce speed as energy is lost on non-hard surface + newY = PS_P_RADIUS; + } + else + { + if (options.useGravity) // do not bounce on top if using gravity (open container) if this is needed implement it in the FX { - if(newY > maxY + PS_P_HALFRADIUS) + if (newY > maxY + PS_P_HALFRADIUS) part.outofbounds = 1; // set out of bounds, kill out of bounds over the top does not apply if gravity is used (user can implement it in FX if needed) } else { part.vy = -part.vy; // invert speed - part.vy = (part.vy * wallHardness) >> 8; // reduce speed as energy is lost on non-hard surface - newY = maxY; //(int32_t)maxY - (newY - (int32_t)maxY); + part.vy = (part.vy * wallHardness) / 255; // reduce speed as energy is lost on non-hard surface + newY = maxY - PS_P_RADIUS; } - } - else //bounce at bottom - { - part.vy = -part.vy; // invert speed - part.vy = (part.vy * wallHardness) >> 8; // reduce speed as energy is lost on non-hard surface - newY = 0;// -newY; - } + } } - else if (options.wrapY) + } + + if (((newY < 0) || (newY > maxY))) // check if particle reached an edge + { + if (options.wrapY) { newY = wraparound(newY, maxY); } @@ -375,8 +379,7 @@ void ParticleSystem::applyForce(PSparticle *part, uint32_t numparticles, int8_t } } - -// apply a force in x,y direction to particles directly (no counter required) +// apply a force in x,y direction to particles directly (no counter required but no 'sub 1' force supported) void ParticleSystem::applyForce(PSparticle *part, uint32_t numparticles, int8_t xforce, int8_t yforce) { //note: could make this faster for single particles by adding an if statement, but it is fast enough as is @@ -390,15 +393,25 @@ void ParticleSystem::applyForce(PSparticle *part, uint32_t numparticles, int8_t // apply a force in angular direction to group of particles //TODO: actually test if this works as expected, this is untested code // caller needs to provide a 8bit counter that holds its value between calls for each group (numparticles can be 1 for single particle) +// angle is from 0-65535 (=0-360deg) angle = 0 means in positive x-direction (i.e. to the right) void ParticleSystem::applyAngleForce(PSparticle *part, uint32_t numparticles, uint8_t force, uint16_t angle, uint8_t *counter) { - int8_t xforce = ((int32_t)force * cos8(angle)) >> 15; // force is +/- 127 - int8_t yforce = ((int32_t)force * sin8(angle)) >> 15; + int8_t xforce = ((int32_t)force * cos16(angle)) / 32767; // force is +/- 127 + int8_t yforce = ((int32_t)force * sin16(angle)) / 32767; // note: cannot use bit shifts as bit shifting is asymmetrical for positive and negative numbers and this needs to be accurate! // note: sin16 is 10% faster than sin8() on ESP32 but on ESP8266 it is 9% slower // 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 a force in angular direction to particles directly (no counter required but no 'sub 1' force supported) +// angle is from 0-65535 (=0-360deg) angle = 0 means in positive x-direction (i.e. to the right) +void ParticleSystem::applyAngleForce(PSparticle *part, uint32_t numparticles, uint8_t force, uint16_t angle) +{ + int8_t xforce = ((int32_t)force * cos16(angle)) / 32767; // force is +/- 127 + int8_t yforce = ((int32_t)force * sin16(angle)) / 32767; // note: cannot use bit shifts as bit shifting is asymmetrical for positive and negative numbers and this needs to be accurate! + applyForce(part, numparticles, xforce, yforce); +} + // 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 @@ -449,26 +462,26 @@ void ParticleSystem::applyGravity(PSparticle *part) // slow down particles by friction, the higher the speed, the higher the friction. a high friction coefficient slows them more (255 means instant stop) void ParticleSystem::applyFriction(PSparticle *part, uint8_t coefficient) { - int32_t friction = 256 - coefficient; - - part->vx = ((int16_t)part->vx * friction) >> 8; - part->vy = ((int16_t)part->vy * friction) >> 8; + int32_t friction = 255 - coefficient; + // note: not checking if particle is dead is faster as most are usually alive and if few are alive, rendering is faster + // note2: cannot use right shifts as bit shifting in right direction is asymmetrical for positive and negative numbers and this needs to be accurate or things start to go to the left side. + part->vx = ((int16_t)part->vx * friction) / 255; + part->vy = ((int16_t)part->vy * friction) / 255; } // apply friction to all particles void ParticleSystem::applyFriction(uint8_t coefficient) { - int32_t friction = 256 - coefficient; - // note: not checking if particle is dead is faster as most are usually alive and if few are alive, rendering is faster + int32_t friction = 255 - coefficient; for (uint32_t i = 0; i < usedParticles; i++) { // note: not checking if particle is dead is faster as most are usually alive and if few are alive, rendering is faster - particles[i].vx = ((int16_t)particles[i].vx * friction) >> 8; - particles[i].vy = ((int16_t)particles[i].vy * friction) >> 8; + // note2: cannot use right shifts as bit shifting in right direction is asymmetrical for positive and negative numbers and this needs to be accurate or things start to go to the left side. + particles[i].vx = ((int16_t)particles[i].vx * friction) / 255; + particles[i].vy = ((int16_t)particles[i].vy * friction) / 255; } } -// TODO: attract needs to use the above force functions // attracts a particle to an attractor particle using the inverse square-law void ParticleSystem::attract(PSparticle *part, PSparticle *attractor, uint8_t *counter, uint8_t strength, bool swallow) { @@ -494,83 +507,25 @@ void ParticleSystem::attract(PSparticle *part, PSparticle *attractor, uint8_t *c } int32_t force = ((int32_t)strength << 16) / distanceSquared; - int8_t xforce = (force * dx) >> 10; // scale to a lower value, found by experimenting - int8_t yforce = (force * dy) >> 10; + int8_t xforce = (force * dx) / 1024; // scale to a lower value, found by experimenting + int8_t yforce = (force * dy) / 1024; // note: cannot use bit shifts as bit shifting is asymmetrical for positive and negative numbers and this needs to be accurate! applyForce(part, 1, xforce, yforce, counter); - /* - int32_t xforce_abs = abs(xforce); // absolute value - int32_t yforce_abs = abs(yforce); - - uint8_t xcounter = (*counter) & 0x0F; // lower four bits - uint8_t ycounter = (*counter) >> 4; // upper four bits - - *counter = 0; // reset counter, is set back to correct values below - - // for small forces, need to use a delay timer (counter) - if (xforce_abs < 16) - { - xcounter += xforce_abs; - if (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 - } - else - { - particle->vx += xforce >> 4; // divide by 16 - } - - if (yforce_abs < 16) - { - ycounter += yforce_abs; - - if (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 - } - else - { - particle->vy += yforce >> 4; // divide by 16 - } - */ } // 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 // warning: do not render out of bounds particles or system will crash! rendering does not check if particle is out of bounds -void ParticleSystem::ParticleSys_render() +// fireintensity and firemode are optional arguments (fireintensity is only used in firemode) +void ParticleSystem::ParticleSys_render(bool firemode, uint32_t fireintensity) { - - int32_t pixco[4][2]; //physical pixel coordinates of the four positions, x,y pairs - //int32_t intensity[4]; + int32_t pixco[4][2]; //physical pixel coordinates of the four pixels a particle is rendered to. x,y pairs CRGB baseRGB; bool useLocalBuffer = true; CRGB **colorbuffer; uint32_t i; uint32_t brightness; // particle brightness, fades if dying //CRGB colorbuffer[maxXpixel+1][maxYpixel+1] = {0}; //put buffer on stack (not a good idea, can cause crashes on large segments if other function run the stack into the heap) - //calloc(maxXpixel + 1, sizeof(CRGB *)) - // to create a 2d array on heap: - // TODO: put this in a function? fire render also uses this - // TODO: if pointer returns null, use classic render (or do not render this frame) if (useLocalBuffer) { // allocate memory for the local renderbuffer @@ -582,29 +537,42 @@ void ParticleSystem::ParticleSys_render() // go over particles and render them to the buffer for (i = 0; i < usedParticles; i++) { - if (particles[i].ttl == 0 || particles[i].outofbounds) + if (particles[i].outofbounds || particles[i].ttl == 0) continue; // 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(firemode) { - CHSV baseHSV = rgb2hsv_approximate(baseRGB); //convert to hsv - baseHSV.s = particles[i].sat; //desaturate - baseRGB = (CRGB)baseHSV; //convert back to RGB + //brightness = (uint32_t)particles[i].ttl * (1 + (fireintensity >> 4)) + (fireintensity >> 2); //this is good + //brightness = (uint32_t)particles[i].ttl * (fireintensity >> 3) + (fireintensity >> 1); // this is experimental, also works, flamecolor is more even, does not look as good (but less puffy at lower speeds) + //brightness = (((uint32_t)particles[i].ttl * (maxY + PS_P_RADIUS - particles[i].y)) >> 7) + (uint32_t)particles[i].ttl * (fireintensity >> 4) + (fireintensity >> 1); // this is experimental //multiplikation mit weniger als >>4 macht noch mehr puffs bei low speed + //brightness = (((uint32_t)particles[i].ttl * (maxY + PS_P_RADIUS - particles[i].y)) >> 7) + particles[i].ttl + (fireintensity>>1); // this is experimental + //brightness = (((uint32_t)particles[i].ttl * (maxY + PS_P_RADIUS - particles[i].y)) >> 7) + ((particles[i].ttl * fireintensity) >> 5); // this is experimental TODO: test this -> testing... ok but not the best, bit sparky + brightness = (((uint32_t)particles[i].ttl * (maxY + PS_P_RADIUS - particles[i].y)) >> 7) + (fireintensity >> 1); // this is experimental TODO: test this -> testing... does not look too bad! + brightness > 255 ? 255 : brightness; // faster then using min() + baseRGB = ColorFromPalette(SEGPALETTE, brightness, 255, LINEARBLEND); + } + else{ + 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) + { + CHSV baseHSV = rgb2hsv_approximate(baseRGB); //convert to hsv + baseHSV.s = particles[i].sat; //desaturate + baseRGB = (CRGB)baseHSV; //convert back to RGB + } } - 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 + int32_t pxlbrightness[4] = {0}; //note: pxlbrightness 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, pixco); + renderParticle(&particles[i], brightness, pxlbrightness, pixco); /* //debug: check coordinates if out of buffer boundaries print out some info for(uint32_t d; d<4; d++) { if (pixco[d][0] < 0 || pixco[d][0] > maxXpixel) { - intensity[d] = -1; //do not render + pxlbrightness[d] = -1; //do not render Serial.print("uncought out of bounds: x="); Serial.print(pixco[d][0]); Serial.print("particle x="); @@ -616,7 +584,7 @@ void ParticleSystem::ParticleSys_render() } if (pixco[d][1] < 0 || pixco[d][1] > maxYpixel) { - intensity[d] = -1; // do not render + pxlbrightness[d] = -1; // do not render Serial.print("uncought out of bounds: y="); Serial.print(pixco[d][1]); Serial.print("particle x="); @@ -629,29 +597,29 @@ void ParticleSystem::ParticleSys_render() }*/ if (useLocalBuffer) { - if (intensity[0] > 0) - colorbuffer[pixco[0][0]][pixco[0][1]] = fast_color_add(colorbuffer[pixco[0][0]][pixco[0][1]], baseRGB, intensity[0]); // bottom left - if (intensity[1] > 0) - colorbuffer[pixco[1][0]][pixco[1][1]] = fast_color_add(colorbuffer[pixco[1][0]][pixco[1][1]], baseRGB, intensity[1]); // bottom right - if (intensity[2] > 0) - colorbuffer[pixco[2][0]][pixco[2][1]] = fast_color_add(colorbuffer[pixco[2][0]][pixco[2][1]], baseRGB, intensity[2]); // top right - if (intensity[3] > 0) - colorbuffer[pixco[3][0]][pixco[3][1]] = fast_color_add(colorbuffer[pixco[3][0]][pixco[3][1]], baseRGB, intensity[3]); // top left + if (pxlbrightness[0] > 0) + colorbuffer[pixco[0][0]][pixco[0][1]] = fast_color_add(colorbuffer[pixco[0][0]][pixco[0][1]], baseRGB, pxlbrightness[0]); // bottom left + if (pxlbrightness[1] > 0) + colorbuffer[pixco[1][0]][pixco[1][1]] = fast_color_add(colorbuffer[pixco[1][0]][pixco[1][1]], baseRGB, pxlbrightness[1]); // bottom right + if (pxlbrightness[2] > 0) + colorbuffer[pixco[2][0]][pixco[2][1]] = fast_color_add(colorbuffer[pixco[2][0]][pixco[2][1]], baseRGB, pxlbrightness[2]); // top right + if (pxlbrightness[3] > 0) + colorbuffer[pixco[3][0]][pixco[3][1]] = fast_color_add(colorbuffer[pixco[3][0]][pixco[3][1]], baseRGB, pxlbrightness[3]); // top left } else { SEGMENT.fill(BLACK); // clear the matrix - if (intensity[0] > 0) - SEGMENT.addPixelColorXY(pixco[0][0], maxYpixel - pixco[0][1], baseRGB.scale8((uint8_t)intensity[0])); // bottom left - if (intensity[1] > 0) - SEGMENT.addPixelColorXY(pixco[1][0], maxYpixel - pixco[1][1], baseRGB.scale8((uint8_t)intensity[1])); // bottom right - if (intensity[2] > 0) - SEGMENT.addPixelColorXY(pixco[2][0], maxYpixel - pixco[2][1], baseRGB.scale8((uint8_t)intensity[2])); // top right - if (intensity[3] > 0) - SEGMENT.addPixelColorXY(pixco[3][0], maxYpixel - pixco[3][1], baseRGB.scale8((uint8_t)intensity[3])); // top left + if (pxlbrightness[0] > 0) + SEGMENT.addPixelColorXY(pixco[0][0], maxYpixel - pixco[0][1], baseRGB.scale8((uint8_t)pxlbrightness[0])); // bottom left + if (pxlbrightness[1] > 0) + SEGMENT.addPixelColorXY(pixco[1][0], maxYpixel - pixco[1][1], baseRGB.scale8((uint8_t)pxlbrightness[1])); // bottom right + if (pxlbrightness[2] > 0) + SEGMENT.addPixelColorXY(pixco[2][0], maxYpixel - pixco[2][1], baseRGB.scale8((uint8_t)pxlbrightness[2])); // top right + if (pxlbrightness[3] > 0) + SEGMENT.addPixelColorXY(pixco[3][0], maxYpixel - pixco[3][1], baseRGB.scale8((uint8_t)pxlbrightness[3])); // 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), could probably be extended to 3x3 - // SEGMENT.addPixelColorXY(pixco[1][0] + 1, maxYpixel - pixco[1][1], baseRGB.scale8((uint8_t)((brightness>>1) - intensity[0])), fastcoloradd); - // SEGMENT.addPixelColorXY(pixco[2][0] + 1, maxYpixel - pixco[2][1], baseRGB.scale8((uint8_t)((brightness>>1) -intensity[3])), fastcoloradd); + // SEGMENT.addPixelColorXY(pixco[1][0] + 1, maxYpixel - pixco[1][1], baseRGB.scale8((uint8_t)((brightness>>1) - pxlbrightness[0])), fastcoloradd); + // SEGMENT.addPixelColorXY(pixco[2][0] + 1, maxYpixel - pixco[2][1], baseRGB.scale8((uint8_t)((brightness>>1) -pxlbrightness[3])), fastcoloradd); } } if (useLocalBuffer) @@ -825,7 +793,8 @@ void ParticleSystem::fireParticleupdate() particles[i].ttl--; // apply velocity particles[i].x = particles[i].x + (int32_t)particles[i].vx; - particles[i].y = particles[i].y + (int32_t)particles[i].vy + (particles[i].ttl >> 2); // younger particles move faster upward as they are hotter + particles[i].y = particles[i].y + (int32_t)particles[i].vy + (particles[i].ttl >> 2); // younger particles move faster upward as they are hotter + //particles[i].y = particles[i].y + (int32_t)particles[i].vy;// + (particles[i].ttl >> 3); // younger particles move faster upward as they are hotter //!! shift ttl by 2 is the original value, this is experimental 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 speed, only check x direction if y is not out of bounds @@ -852,136 +821,6 @@ void ParticleSystem::fireParticleupdate() } } -// 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 -// without using a plette native, heat based color mode applied -// intensity from 0-255 is mapped such that higher values result in more intense flames -void ParticleSystem::renderParticleFire(uint32_t intensity, bool usepalette) -{ - int32_t pixco[4][2]; // physical coordinates of the four positions, x,y pairs - uint32_t flameheat; //depends on particle.ttl - uint32_t i; - uint32_t debug = 0; - - // allocate memory for the local renderbuffer - CRGB **colorbuffer = allocate2Dbuffer(maxXpixel + 1, maxYpixel + 1); - if (colorbuffer == NULL) - { - SEGMENT.setPalette(35); // set fire palette - SEGMENT.check1 = true; //enable palette from now on - usepalette = true; //use palette f - } - //TODO: move the rendering over to the render-particle function, add a parameter 'rendertype' to select normal rendering or fire rendering, in the future this can also be used to render smaller/larger particle sizes - - for (i = 0; i < usedParticles; i++) - { - if (particles[i].outofbounds || particles[i].ttl == 0) // lots of fire particles are out of bounds, check first - continue; - - if (usepalette) - { - // generate RGB values for particle - uint32_t brightness = (uint32_t)particles[i].ttl * (1 + (intensity >> 4)) + (intensity >> 2); - brightness > 255 ? 255 : brightness; // faster then using min() - CRGB baseRGB = ColorFromPalette(SEGPALETTE, brightness, 255, LINEARBLEND); - - 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, pixco); - - if (intensity[0] > 0) - colorbuffer[pixco[0][0]][pixco[0][1]] = fast_color_add(colorbuffer[pixco[0][0]][pixco[0][1]], baseRGB, intensity[0]); // bottom left - if (intensity[1] > 0) - colorbuffer[pixco[1][0]][pixco[1][1]] = fast_color_add(colorbuffer[pixco[1][0]][pixco[1][1]], baseRGB, intensity[1]); - if (intensity[2] > 0) - colorbuffer[pixco[2][0]][pixco[2][1]] = fast_color_add(colorbuffer[pixco[2][0]][pixco[2][1]], baseRGB, intensity[2]); - if (intensity[3] > 0) - colorbuffer[pixco[3][0]][pixco[3][1]] = fast_color_add(colorbuffer[pixco[3][0]][pixco[3][1]], baseRGB, intensity[3]); - } - else{ - flameheat = particles[i].ttl; - int32_t pixelheat[4] = {0}; // note: passed array needs to be set to 0 or checking in rendering function does not work (if values persist), this is faster then setting it to 0 there - renderParticle(&particles[i], flameheat, pixelheat, pixco); //render heat to physical pixels - - if (pixelheat[0] >= 0) - PartMatrix_addHeat(pixelheat[0], &colorbuffer[pixco[0][0]][pixco[0][1]].r, intensity); - if (pixelheat[1] >= 0) - PartMatrix_addHeat(pixelheat[1], &colorbuffer[pixco[1][0]][pixco[1][1]].r, intensity); - if (pixelheat[2] >= 0) - PartMatrix_addHeat(pixelheat[2], &colorbuffer[pixco[2][0]][pixco[2][1]].r, intensity); - if (pixelheat[3] >= 0) - PartMatrix_addHeat(pixelheat[3], &colorbuffer[pixco[3][0]][pixco[3][1]].r, intensity); - - // TODO: add heat to a third pixel? need to konw dx and dy, the heatvalue is (flameheat - pixelheat) vom pixel das weiter weg ist vom partikelzentrum - // 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 - //debug++; //!!! - } - } -// Serial.println(" "); -// Serial.print("rp:"); -// Serial.println(debug); - - for (int x = 0; x <= maxXpixel;x++) - { - for (int y = 0; y <= maxYpixel; y++) - { - SEGMENT.setPixelColorXY(x, maxYpixel - y, colorbuffer[x][y]); - } - } - free(colorbuffer); // free buffer memory -} - -// adds 'heat' to red color channel, if it overflows, add it to next color channel -void ParticleSystem::PartMatrix_addHeat(int32_t heat, uint8_t *currentcolor, uint32_t intensity) -{ - - // 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 * (1 + (intensity >> 3)) + (intensity >> 3); //ESP8266 has to make due with less flames, so add more heat - #else - //heat = (heat * (1 + (intensity)) >> 4) + (intensity >> 3); //this makes it more pulsating - //heat = heat * (1 + (intensity >> 4)) + (intensity >> 3); //this is good, but pulsating at low speeds - heat = heat * heat / (1 + (255-intensity)) + (intensity >> 3); - #endif - - uint32_t i; - //go over the three color channels and fill them with heat, if one overflows, add heat to the next, start with red - for (i = 0; i < 3; i++) - { - if (currentcolor[i] < 255) //current color is not yet full - { - if (heat > 255) - { - heat -= 255 - currentcolor[i]; - currentcolor[i] = 255; - } - else{ - int32_t leftover = heat - (255 - currentcolor[i]); - if(leftover <= 0) //all heat is being used up for this color - { - currentcolor[i] += heat; - break; - } - else{ - currentcolor[i] = 255; - if(heat > leftover) - { - heat -= leftover; - } - else - break; - } - } - } - } - - if (i == 2) // blue channel was reached, limit the color value so it does not go full white - currentcolor[i] = currentcolor[i] > 50 ? 50 : currentcolor[i]; //faster than min() -} // detect collisions in an array of particles and handle them void ParticleSystem::handleCollisions() @@ -997,10 +836,10 @@ void ParticleSystem::handleCollisions() { startparticle = endparticle; endparticle = usedParticles; - } + } collisioncounter++; - //startparticle = 0;//!!! test: do all collisions every frame, + //startparticle = 0;//!!! test: do all collisions every frame, FPS goes from about 52 to //endparticle = usedParticles; for (i = startparticle; i < endparticle; i++) @@ -1026,9 +865,11 @@ void ParticleSystem::handleCollisions() } } + + // handle a collision if close proximity is detected, i.e. dx and/or dy smaller than 2*PS_P_RADIUS // takes two pointers to the particles to collide and the particle hardness (softer means more energy lost in collision, 255 means full hard) -void ParticleSystem::collideParticles(PSparticle *particle1, PSparticle *particle2) //TODO: dx,dy is calculated just above, can pass it over here to save some CPU time +void ParticleSystem::collideParticles(PSparticle *particle1, PSparticle *particle2) //TODO: dx,dy is calculated just above, can pass it over here to save a few CPU cycles { int32_t dx = particle2->x - particle1->x; int32_t dy = particle2->y - particle1->y; @@ -1037,98 +878,202 @@ void ParticleSystem::collideParticles(PSparticle *particle1, PSparticle *particl int32_t relativeVx = (int16_t)particle2->vx - (int16_t)particle1->vx; int32_t relativeVy = (int16_t)particle2->vy - (int16_t)particle1->vy; - if (distanceSquared == 0) // add distance in case particles exactly meet at center, prevents dotProduct=0 (this can only happen if they move towards each other) + // if dx and dy are zero (i.e. they meet at the center) give them an offset, if speeds are also zero, also offset them (pushes them apart if they are clumped before enabling collisions) + if (distanceSquared == 0) { - distanceSquared++; - } - /* - // Adjust positions based on relative velocity direction -> does not really do any good. - uint32_t add = 1; + // Adjust positions based on relative velocity direction + dx = -1; if (relativeVx < 0) // if true, particle2 is on the right side - add = -1; - particle1->x += add; - particle2->x -= add; - add = 1; - + dx = 1; + else if(relativeVx == 0) //if true + { + relativeVx = 1; + } + + dy = -1; if (relativeVy < 0) - add = -1; - particle1->y += add; - particle2->y -= add; + dy = 1; + else if (relativeVy == 0) + { + relativeVy = 1; + } + + distanceSquared = 2; //1 + 1 + } - distanceSquared++; - }*/ // Calculate dot product of relative velocity and relative distance - int32_t dotProduct = (dx * relativeVx + dy * relativeVy); - uint32_t notsorandom = dotProduct & 0x01; // random16(2); //dotprouct LSB should be somewhat random, so no need to calculate a random number - // If particles are moving towards each other - if (dotProduct < 0) - { - const uint32_t bitshift = 16; // bitshift used to avoid floats (dx/dy are 7bit, relativV are 8bit -> dotproduct is 15bit so up to 16bit shift is ok) + + int32_t dotProduct = (dx * relativeVx + dy * relativeVy); //is always negative if moving towards each other + int32_t notsorandom = dotProduct & 0x01; // random16(2); //dotprouct LSB should be somewhat random, so no need to calculate a random number + if (dotProduct < 0) // particles are moving towards each other + { + // integer math used to avoid floats. + // overflow check: dx/dy are 7bit, relativV are 8bit -> dotproduct is 15bit, dotproduct/distsquared ist 8b, multiplied by collisionhardness of 8bit. so a 16bit shift is ok, make it 15 to be sure no overflows happen + // note: cannot use right shifts as bit shifting in right direction is asymmetrical for positive and negative numbers and this needs to be accurate! the trick is: only shift positive numers // Calculate new velocities after collision - int32_t impulse = (((dotProduct << (bitshift)) / (distanceSquared)) * collisionHardness) >> 8; - int32_t ximpulse = ((impulse + 1) * dx) >> bitshift; // todo: is the +1 a good idea? - int32_t yimpulse = ((impulse + 1) * dy) >> bitshift; + uint32_t surfacehardness = collisionHardness < PS_P_MINSURFACEHARDNESS ? PS_P_MINSURFACEHARDNESS : collisionHardness; // if particles are soft, the impulse must stay above a limit or collisions slip through at higher speeds, 170 seems to be a good value + int32_t impulse = -(((((-dotProduct) << 15) / distanceSquared) * surfacehardness) >> 8); // note: inverting before bitshift corrects for asymmetry in right-shifts (and is slightly faster) + int32_t ximpulse = ((impulse) * dx) / 32767; //cannot use bit shifts here, it can be negative, use division by 2^bitshift + int32_t yimpulse = ((impulse) * dy) / 32767; particle1->vx += ximpulse; particle1->vy += yimpulse; particle2->vx -= ximpulse; particle2->vy -= yimpulse; - - //also second version using multiplication is slower on ESP8266 than the if's - if (collisionHardness < 128) // if particles are soft, they become 'sticky' i.e. they are slowed down at collisions even more - { - - //particle1->vx = (particle1->vx < 2 && particle1->vx > -2) ? 0 : particle1->vx; - //particle1->vy = (particle1->vy < 2 && particle1->vy > -2) ? 0 : particle1->vy; - //particle2->vx = (particle2->vx < 2 && particle2->vx > -2) ? 0 : particle2->vx; - //particle2->vy = (particle2->vy < 2 && particle2->vy > -2) ? 0 : particle2->vy; + if (collisionHardness < surfacehardness) // if particles are soft, they become 'sticky' i.e. apply some friction + { + const uint32_t coeff = collisionHardness + (255 - PS_P_MINSURFACEHARDNESS); + particle1->vx = ((int32_t)particle1->vx * coeff) / 255; + particle1->vy = ((int32_t)particle1->vy * coeff) / 255; + + particle2->vx = ((int32_t)particle2->vx * coeff) / 255; + particle2->vy = ((int32_t)particle2->vy * coeff) / 255; - const uint32_t coeff = 247 + (collisionHardness>>4); - particle1->vx = ((((int32_t)particle1->vx + notsorandom) * coeff) >> 8); // add 1 sometimes to balance favour to left side TODO: did this really fix it? - particle1->vy = ((int32_t)particle1->vy * coeff) >> 8; + if (collisionHardness < 10) // if they are very soft, stop slow particles completely to make them stick to each other + { + particle1->vx = (particle1->vx < 3 && particle1->vx > -3) ? 0 : particle1->vx; + particle1->vy = (particle1->vy < 3 && particle1->vy > -3) ? 0 : particle1->vy; - particle2->vx = (((int32_t)particle2->vx * coeff) >> 8); - particle2->vy = ((int32_t)particle2->vy * coeff) >> 8; + particle2->vx = (particle2->vx < 3 && particle2->vx > -3) ? 0 : particle2->vx; + particle2->vy = (particle2->vy < 3 && particle2->vy > -3) ? 0 : particle2->vy; + } } - } - - // particles have volume, push particles apart if they are too close by moving each particle by a fixed amount away from the other particle - // if pushing is made dependent on hardness, things start to oscillate much more, better to just add a fixed, small increment (tried lots of configurations, this one works best) - // one problem remaining is, particles get squished if (external) force applied is higher than the pushback but this may also be desirable if particles are soft. also some oscillations cannot be avoided without addigng a counter - if (distanceSquared < 2 * PS_P_HARDRADIUS * PS_P_HARDRADIUS) - { + // this part is for particle piling: slow them down if they are close (they become sticky) and push them so they counteract gravity + // particles have volume, push particles apart if they are too close + // tried lots of configurations, it works best if not moved but given a little velocity, it tends to oscillate less this way + // a problem with giving velocity is, that on harder collisions, this adds up as it is not dampened enough, so add friction in the FX if required + if (dotProduct > -250) //this means particles are slow (or really really close) so push them apart. + { + /** + //only apply friction if particles are slow or else fast moving particles (as in explosions) get slowed a lot + relativeVy *= relativeVy; //square the speed, apply friction if speed is below 10 + if (relativeVy < 100) //particles are slow in y direction -> this works but most animations look much nicer without this friction. add friction in FX if required. + { + //now check x as well (no need to check if y speed is high, this saves some computation time) + relativeVx *= relativeVx; // square the speed, apply friction if speed is below 10 + if (relativeVx < 100) // particles are slow in x direction + { + particle1->vx = ((int32_t)particle1->vx * 254) / 256; + particle2->vx = ((int32_t)particle2->vx * 254) / 256; + + particle1->vy = ((int32_t)particle1->vy * 254) / 256; + particle2->vy = ((int32_t)particle2->vy * 254) / 256; + + } + }*/ + - const int32_t HARDDIAMETER = 2 * PS_P_HARDRADIUS; - const int32_t pushamount = 1; // push a small amount - int32_t push = pushamount; - if (dx < HARDDIAMETER && dx > -HARDDIAMETER) - { // distance is too small, push them apart + // const int32_t HARDDIAMETER = 2 * PS_P_HARDRADIUS; // push beyond the hard radius, helps with keeping stuff fluffed up -> not really + // int32_t push = (2 * PS_P_HARDRADIUS * PS_P_HARDRADIUS - distanceSquared) >> 6; // push a small amount, if pushing too much, it becomse chaotic as waves of pushing run through piles + int32_t pushamount = 1 + ((250 + dotProduct) >> 6); // the closer dotproduct is to zero, the closer the particles are + int32_t push; + + // if (dx < HARDDIAMETER && dx > -HARDDIAMETER) //this is always true as it is checked before ntering this function! + { // distance is too small, push them apart + push = 0; + if (dx < 0) // particle 1 is on the right + push = pushamount; //(HARDDIAMETER + dx) / 4; + else if (dx > 0) + push = -pushamount; //-(HARDDIAMETER - dx) / 4; + else // on the same x coordinate, shift it a little so they do not stack + { - if (dx < 0) - push = -pushamount; //-(PS_P_HARDRADIUS + dx); // inverted push direction + if (notsorandom) + particle1->x++; // move it so pile collapses + else + particle1->x--; + } - if (notsorandom) // chose one of the particles to push, avoids oscillations - particle1->x -= push; - else - particle2->x += push; - } + particle1->vx += push; + } - push = pushamount; // reset push variable to 1 - if (dy < HARDDIAMETER && dy > -HARDDIAMETER) - { - if (dy < 0) - push = -pushamount; //-(PS_P_HARDRADIUS + dy); // inverted push direction + // if (dy < HARDDIAMETER && dy > -HARDDIAMETER) //dito + { + push = 0; + if (dy < 0) + push = pushamount; //(HARDDIAMETER + dy) / 4; + else if (dy > 0) + push = -pushamount; //-(HARDDIAMETER - dy) / 4; + else // dy==0 + { + if (notsorandom) + particle1->y++; // move it so pile collapses + else + particle1->y--; + } - if (notsorandom) // chose one of the particles to push, avoids oscillations - particle1->y -= push; - else - particle2->y += push; + particle1->vy += push; + } + /* + if (dx < HARDDIAMETER && dx > -HARDDIAMETER) + { // distance is too small, push them apart + push = 0; + if (dx < 0) // particle 1 is on the right + push = 2; //(HARDDIAMETER + dx) / 4; + else if (dx > 0) + push = -2; //-(HARDDIAMETER - dx) / 4; + else //on the same x coordinate, shift it a little so they do not stack + particle1->x += 2; + if (notsorandom) // chose one of the particles to push, avoids oscillations + { + if (!particle1->flag3) + { + particle1->vx += push; + particle1->flag3 = 1; // particle was pushed, is reset on next push request + } + else + particle1->flag3 = 0; //reset + } + else + { + if (!particle2->flag3) + { + particle2->vx -= push; + particle2->flag3 = 1; // particle was pushed, is reset on next push request + } + else + particle2->flag3 = 0; // reset + } + } + + if (dy < HARDDIAMETER && dy > -HARDDIAMETER) + { + push = 0; + if (dy < 0) + push = 2; //(HARDDIAMETER + dy) / 4; + else if (dy > 0) + push = -2; //-(HARDDIAMETER - dy) / 4; + + if (!notsorandom) // chose one of the particles to push, avoids oscillations + { + if (!particle1->flag3) + { + particle1->vy += push; + particle1->flag3 = 1; // particle was pushed, is reset on next push request + } + else + particle1->flag3 = 0; // reset + } + else + { + if (!particle2->flag3) + { + particle2->vy -= push; + particle2->flag3 = 1; // particle was pushed, is reset on next push request + } + else + particle2->flag3 = 0; // reset + } + }*/ + + // note: pushing may push particles out of frame, if bounce is active, it will move it back as position will be limited to within frame, if bounce is disabled: bye bye } - // note: pushing may push particles out of frame, if bounce is active, it will move it back as position will be limited to within frame, if bounce is disabled: bye bye } + + } //fast calculation of particle wraparound (modulo version takes 37 instructions, this only takes 28, other variants are slower on ESP8266) @@ -1173,7 +1118,9 @@ int32_t ParticleSystem::calcForce_dV(int8_t force, uint8_t* counter) // allocate memory for the 2D array in one contiguous block and set values to zero CRGB **ParticleSystem::allocate2Dbuffer(uint32_t cols, uint32_t rows) { + cli();//!!! test to see if anything messes with the allocation (flicker issues) CRGB ** array2D = (CRGB **)malloc(cols * sizeof(CRGB *) + cols * rows * sizeof(CRGB)); + sei(); if (array2D == NULL) DEBUG_PRINT(F("PS buffer alloc failed")); else @@ -1190,6 +1137,7 @@ CRGB **ParticleSystem::allocate2Dbuffer(uint32_t cols, uint32_t rows) } //update size and pointers (memory location and size can change dynamically) +//note: do not access the PS class in FX befor running this function (or it messes up SEGMENT.data) void ParticleSystem::updateSystem(void) { // update matrix size @@ -1204,16 +1152,16 @@ void ParticleSystem::updateSystem(void) // FX handles the PSsources, need to tell this function how many there are void ParticleSystem::updatePSpointers() { - DEBUG_PRINT(F("*** PS pointers ***")); - DEBUG_PRINTF_P(PSTR("this PS %p\n"), this); + //DEBUG_PRINT(F("*** PS pointers ***")); + //DEBUG_PRINTF_P(PSTR("this PS %p\n"), this); particles = reinterpret_cast(this + 1); // pointer to particle array at data+sizeof(ParticleSystem) sources = reinterpret_cast(particles + numParticles); // pointer to source(s) PSdataEnd = reinterpret_cast(sources + numSources); // pointer to first available byte after the PS - DEBUG_PRINTF_P(PSTR("particles %p\n"), particles); - DEBUG_PRINTF_P(PSTR("sources %p\n"), sources); - DEBUG_PRINTF_P(PSTR("end %p\n"), PSdataEnd); + //DEBUG_PRINTF_P(PSTR("particles %p\n"), particles); + //DEBUG_PRINTF_P(PSTR("sources %p\n"), sources); + //DEBUG_PRINTF_P(PSTR("end %p\n"), PSdataEnd); } //non class functions to use for initialization diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index 7d9ed1ca45..af8c37d1bc 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -43,6 +43,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_MINSURFACEHARDNESS 128 //minimum hardness used in collision impulse calculation, below this hardness, particles become sticky #define PS_P_MAXSPEED 200 //maximum speed a particle can have //struct for a single particle @@ -91,7 +92,7 @@ class ParticleSystem 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(uint32_t intensity, bool usepalette); // update function for fire + void updateFire(uint32_t intensity); // update function for fire // particle emitters @@ -110,6 +111,7 @@ class ParticleSystem void applyForce(PSparticle *part, uint32_t numparticles, int8_t xforce, int8_t yforce, uint8_t *counter); void applyForce(PSparticle *part, uint32_t numparticles, int8_t xforce, int8_t yforce); void applyAngleForce(PSparticle *part, uint32_t numparticles, uint8_t force, uint16_t angle, uint8_t *counter); + void applyAngleForce(PSparticle *part, uint32_t numparticles, uint8_t force, uint16_t angle); void applyFriction(PSparticle *part, uint8_t coefficient); // apply friction to specific particle void applyFriction(uint8_t coefficient); // apply friction to all used particles void attract(PSparticle *particle, PSparticle *attractor, uint8_t *counter, uint8_t strength, bool swallow); @@ -140,9 +142,7 @@ class ParticleSystem private: //rendering functions - void ParticleSys_render(); - void renderParticleFire(uint32_t intensity, bool usepalette); - void PartMatrix_addHeat(int32_t heat, uint8_t *currentcolor, uint32_t intensity); + void ParticleSys_render(bool firemode = false, uint32_t fireintensity = 128); void renderParticle(PSparticle *particle, uint32_t brightess, int32_t *pixelvalues, int32_t (*pixelpositions)[2]); //paricle physics applied by system if flags are set @@ -166,7 +166,7 @@ class ParticleSystem }; //initialization functions (not part of class) -bool initParticleSystem(ParticleSystem *&PartSys, uint16_t additionalbytes); +bool initParticleSystem(ParticleSystem *&PartSys, uint16_t additionalbytes = 0); uint32_t calculateNumberOfParticles(); uint32_t calculateNumberOfSources(); bool allocateParticleSystemMemory(uint16_t numparticles, uint16_t numsources, uint16_t additionalbytes); From 5e2dca35772dd8376b2d015ff5973897366ab1a3 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sat, 6 Apr 2024 07:15:51 +0200 Subject: [PATCH 057/219] Fixed Speed limit limiting speed was incorrect, leading to overflows. fixed this. also fixed bugs in GEQ, removed some debug stuff, added FPS limit to fire (just en experiment) --- wled00/FX.cpp | 40 +++++++------ wled00/FXparticleSystem.cpp | 112 ++++++++++++++++-------------------- wled00/FXparticleSystem.h | 9 +-- 3 files changed, 79 insertions(+), 82 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 9fb83d736b..6cc90a5d18 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -7910,8 +7910,8 @@ uint16_t mode_particlevortex(void) for (i = 0; i < numSprays; i++) { 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.x = (PartSys->maxX + 1) >> 1; // center + PartSys->sources[i].source.y = (PartSys->maxY + 1) >> 1; // center PartSys->sources[i].source.vx = 0; PartSys->sources[i].source.vy = 0; PartSys->sources[i].maxLife = 900; @@ -8294,7 +8294,7 @@ uint16_t mode_particlefire(void) { if (!initParticleSystem(PartSys)) return mode_static(); // allocation failed; //allocation failed - Serial.println("fireinit done"); + // Serial.println("fireinit done"); SEGMENT.aux0 = rand(); // aux0 is wind position (index) in the perlin noise // initialize the flame sprays numFlames = PartSys->numSources; @@ -8379,7 +8379,7 @@ uint16_t mode_particlefire(void) { //int32_t curl = ((int16_t)inoise8(PartSys->particles[i].x , PartSys->particles[i].y >> 1, SEGMENT.step<<2 ) - 127); //int32_t curl = ((int16_t)inoise8(PartSys->particles[i].x, PartSys->particles[i].y , SEGMENT.step << 4) - 127); - int32_t curl = ((int16_t)inoise8(PartSys->particles[i].x, PartSys->particles[i].y , SEGMENT.step << 4) - 127); //-> this is good! + int32_t curl = ((int32_t)inoise8(PartSys->particles[i].x, PartSys->particles[i].y , SEGMENT.step << 4) - 127); //-> this is good! //int32_t curl = ((int16_t)inoise8(PartSys->particles[i].x>>1, SEGMENT.step<<5) - 127); // curl = ((curl * PartSys->particles[i].y) / PartSys->maxY); //'curl' stronger at the top @@ -8424,13 +8424,21 @@ uint16_t mode_particlefire(void) } Serial.println("B"); }*/ - + if(SEGMENT.check1) //low fps is enabled + { + static uint32_t lastcall; //!!! put this in heap + while(millis()-lastcall < 25) + { + yield(); + } + lastcall = millis(); + } PartSys->updateFire(SEGMENT.intensity); // update and render the fire return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEFIRE[] PROGMEM = "PS Fire@Speed,Intensity,Base Heat,Wind,Spread,,Cylinder,Turbulence;;!;2;pal=35,sx=130,ix=120,c1=110,c2=128,c3=22,o1=0"; +static const char _data_FX_MODE_PARTICLEFIRE[] PROGMEM = "PS Fire@Speed,Intensity,Base Heat,Wind,Spread,FPS Limit,Cylinder,Turbulence;;!;2;pal=35,sx=130,ix=120,c1=110,c2=128,c3=22,o1=0"; /* PS Ballpit: particles falling down, user can enable these three options: X-wraparound, side bounce, ground bounce @@ -8551,7 +8559,7 @@ uint16_t mode_particlewaterfall(void) if (PartSys == NULL) { - Serial.println("ERROR: paticle system not found, nullpointer"); + DEBUG_PRINT(F("ERROR: FX PartSys nullpointer")); return mode_static(); // something went wrong, no data! (TODO: ask how to handle this so it always works) } // Particle System settings @@ -8623,7 +8631,7 @@ uint16_t mode_particlebox(void) if (PartSys == NULL) { - Serial.println("ERROR: paticle system not found, nullpointer"); + DEBUG_PRINT(F("ERROR: FX PartSys nullpointer")); return mode_static(); // something went wrong, no data! } @@ -8819,7 +8827,7 @@ uint16_t mode_particleimpact(void) if (PartSys == NULL) { - Serial.println("ERROR: paticle system not found, nullpointer"); + DEBUG_PRINT(F("ERROR: FX PartSys nullpointer")); return mode_static(); // something went wrong, no data! (TODO: ask how to handle this so it always works) } @@ -8961,7 +8969,7 @@ uint16_t mode_particleattractor(void) if (PartSys == NULL) { - Serial.println("ERROR: paticle system not found, nullpointer"); + DEBUG_PRINT(F("ERROR: FX PartSys nullpointer")); return mode_static(); // something went wrong, no data! (TODO: ask how to handle this so it always works) } // Particle System settings @@ -9130,7 +9138,7 @@ uint16_t mode_particleGEQ(void) if (PartSys == NULL) { - Serial.println("ERROR: paticle system not found, nullpointer"); + DEBUG_PRINT(F("ERROR: FX PartSys nullpointer")); return mode_static(); // something went wrong, no data! (TODO: ask how to handle this so it always works) } @@ -9142,7 +9150,7 @@ uint16_t mode_particleGEQ(void) PartSys->setBounceY(SEGMENT.check3); PartSys->enableParticleCollisions(false); PartSys->setWallHardness(SEGMENT.custom2); - PartSys->enableGravity(true, SEGMENT.custom3<<1); //set gravity strength + PartSys->enableGravity(true, SEGMENT.custom3<<2); //set gravity strength um_data_t *um_data; if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) @@ -9166,7 +9174,7 @@ uint16_t mode_particleGEQ(void) for (bin = 0; bin < 16; bin++) { uint32_t xposition = binwidth*bin + (binwidth>>1); // emit position according to frequency band - uint8_t emitspeed = 5 + (((uint32_t)fftResult[bin]*(uint32_t)SEGMENT.speed)>>9); // emit speed according to loudness of band + uint8_t emitspeed = ((uint32_t)fftResult[bin] * (uint32_t)SEGMENT.speed) >> 9; // emit speed according to loudness of band (127 max!) emitparticles = 0; if (fftResult[bin] > threshold) @@ -9187,10 +9195,10 @@ uint16_t mode_particleGEQ(void) if (PartSys->particles[i].ttl == 0) // find a dead particle { //set particle properties - PartSys->particles[i].ttl = map(SEGMENT.intensity, 0,255, emitspeed>>1, emitspeed + random16(emitspeed)) ; // set particle alive, particle lifespan is in number of frames + PartSys->particles[i].ttl = 20 + map(SEGMENT.intensity, 0,255, emitspeed>>1, emitspeed + random16(emitspeed)) ; // set particle alive, particle lifespan is in number of frames PartSys->particles[i].x = xposition + random16(binwidth) - (binwidth>>1); //position randomly, deviating half a bin width - PartSys->particles[i].y = 0; //start at the bottom - PartSys->particles[i].vx = random16(SEGMENT.custom1>>1)-(SEGMENT.custom1>>2) ; //x-speed variation + PartSys->particles[i].y = PS_P_RADIUS; //tart at the bottom (PS_P_RADIUS is minimum position a particle is fully in frame) + PartSys->particles[i].vx = random16(SEGMENT.custom1>>1)-(SEGMENT.custom1>>2) ; //x-speed variation: +/- custom1/4 PartSys->particles[i].vy = emitspeed; PartSys->particles[i].hue = (bin<<4) + random16(17) - 8; // color from palette according to bin PartSys->particles[i].sat = 255; // set saturation diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index 1cd99e41c0..4fee58577c 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -47,7 +47,7 @@ ParticleSystem::ParticleSystem(uint16_t width, uint16_t height, uint16_t numberofparticles, uint16_t numberofsources) { - Serial.println("PS Constructor"); + //Serial.println("PS Constructor"); numSources = numberofsources; numParticles = numberofparticles; // set number of particles in the array usedParticles = numberofparticles; // use all particles by default @@ -69,7 +69,7 @@ ParticleSystem::ParticleSystem(uint16_t width, uint16_t height, uint16_t numbero Serial.println(particles[i].y); } }*/ - Serial.println("PS Constructor done"); + //Serial.println("PS Constructor done"); } //update function applies gravity, moves the particles, handles collisions and renders the particles @@ -285,9 +285,12 @@ void ParticleSystem::particleMoveUpdate(PSparticle &part, PSsettings &options) { if (newY < PS_P_RADIUS) // bounce at bottom { - part.vy = -part.vy; // invert speed - part.vy = (part.vy * wallHardness) / 255; // reduce speed as energy is lost on non-hard surface - newY = PS_P_RADIUS; + if(part.vy < -10) + { + part.vy = -part.vy; // invert speed + part.vy = ((int32_t)part.vy * wallHardness) / 255; // reduce speed as energy is lost on non-hard surface + newY = PS_P_RADIUS; + } } else { @@ -299,7 +302,7 @@ void ParticleSystem::particleMoveUpdate(PSparticle &part, PSsettings &options) else { part.vy = -part.vy; // invert speed - part.vy = (part.vy * wallHardness) / 255; // reduce speed as energy is lost on non-hard surface + part.vy = ((int32_t)part.vy * wallHardness) / 255; // reduce speed as energy is lost on non-hard surface newY = maxY - PS_P_RADIUS; } } @@ -341,8 +344,8 @@ void ParticleSystem::applyForce(PSparticle *part, uint32_t numparticles, int8_t uint8_t ycounter = (*counter) >> 4; // upper four bits // velocity increase - int32_t dvx = calcForce_dV(xforce, &xcounter); - int32_t dvy = calcForce_dV(yforce, &ycounter); + 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 @@ -352,30 +355,18 @@ void ParticleSystem::applyForce(PSparticle *part, uint32_t numparticles, int8_t int32_t i = 0; if (dvx != 0) { - if (numparticles == 1) // for single particle, skip the for loop to make it faster + for (i = 0; i < numparticles; i++) { - part[0].vx = part[0].vx + dvx > PS_P_MAXSPEED ? PS_P_MAXSPEED : part[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 - part[i].vx = part[i].vx + dvx > PS_P_MAXSPEED ? PS_P_MAXSPEED : part[i].vx + dvx; - } + // 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 + part[i].vx = limitSpeed((int32_t)particles[i].vx + dvx); } } if (dvy != 0) { - if (numparticles == 1) // for single particle, skip the for loop to make it faster - part[0].vy = part[0].vy + dvy > PS_P_MAXSPEED ? PS_P_MAXSPEED : part[0].vy + dvy; - else + for (i = 0; i < numparticles; i++) { - for (i = 0; i < numparticles; i++) - { - part[i].vy = part[i].vy + dvy > PS_P_MAXSPEED ? PS_P_MAXSPEED : part[i].vy + dvy; - } - } + part[i].vy = limitSpeed((int32_t)particles[i].vy + dvy); + } } } @@ -386,8 +377,8 @@ void ParticleSystem::applyForce(PSparticle *part, uint32_t numparticles, int8_t for (uint 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 - part[i].vx = part[i].vx + xforce > PS_P_MAXSPEED ? PS_P_MAXSPEED : part[i].vx + xforce; - part[i].vy = part[i].vy + yforce > PS_P_MAXSPEED ? PS_P_MAXSPEED : part[i].vy + yforce; + part[i].vx = limitSpeed((int32_t)part[i].vx + (int32_t)xforce); + part[i].vy = limitSpeed((int32_t)part[i].vy + (int32_t)yforce); } } @@ -415,27 +406,17 @@ void ParticleSystem::applyAngleForce(PSparticle *part, uint32_t numparticles, ui // 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) +// 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) +// positive force means down +void ParticleSystem::applyGravity(PSparticle *part, uint32_t numarticles, int8_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 - } + int32_t dv = calcForce_dv(force, counter); + 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 + part[i].vy = limitSpeed((int32_t)particles[i].vy - dv); } + } //apply gravity using PS global gforce @@ -445,6 +426,7 @@ void ParticleSystem::applyGravity(PSparticle *part, uint32_t numarticles, uint8_ } //apply gravity to single particle using system settings (use this for sources) +//function does not increment gravity counter, if gravity setting is disabled, this cannot be used void ParticleSystem::applyGravity(PSparticle *part) { int32_t dv; // velocity increase @@ -454,8 +436,8 @@ void ParticleSystem::applyGravity(PSparticle *part) dv = 1; if (gforcecounter + gforce > 15) //counter is updated in global update when applying gravity - { - part->vy = part->vy - dv > PS_P_MAXSPEED ? PS_P_MAXSPEED : part->vy - dv; // limit the force, this is faster than min or if/else + { + part->vy = limitSpeed((int32_t)part->vy - dv); } } @@ -1093,11 +1075,11 @@ int32_t ParticleSystem::wraparound(int32_t p, int32_t maxvalue) //calculate the delta speed (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) +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 = 0; + int32_t dv; // for small forces, need to use a delay counter, apply force only if it overflows if (force_abs < 16) { @@ -1115,6 +1097,12 @@ int32_t ParticleSystem::calcForce_dV(int8_t force, uint8_t* counter) return dv; } +//limit speed to prevent overflows +int32_t ParticleSystem::limitSpeed(int32_t speed) +{ + return speed > PS_P_MAXSPEED ? PS_P_MAXSPEED : (speed < -PS_P_MAXSPEED ? -PS_P_MAXSPEED : speed); +} + // allocate memory for the 2D array in one contiguous block and set values to zero CRGB **ParticleSystem::allocate2Dbuffer(uint32_t cols, uint32_t rows) { @@ -1207,18 +1195,18 @@ bool allocateParticleSystemMemory(uint16_t numparticles, uint16_t numsources, ui requiredmemory += sizeof(PSparticle) * numparticles; requiredmemory += sizeof(PSsource) * numsources; requiredmemory += additionalbytes; - Serial.print("allocating: "); - Serial.print(requiredmemory); - Serial.println("Bytes"); - Serial.print("allocating for segment at"); - Serial.println((uintptr_t)SEGMENT.data); + //Serial.print("allocating: "); + //Serial.print(requiredmemory); + //Serial.println("Bytes"); + //Serial.print("allocating for segment at"); + //Serial.println((uintptr_t)SEGMENT.data); return(SEGMENT.allocateData(requiredmemory)); } // initialize Particle System, allocate additional bytes if needed (pointer to those bytes can be read from particle system class: PSdataEnd) bool initParticleSystem(ParticleSystem *&PartSys, uint16_t additionalbytes) { - Serial.println("PS init function"); + //Serial.println("PS init function"); uint32_t numparticles = calculateNumberOfParticles(); uint32_t numsources = calculateNumberOfSources(); if (!allocateParticleSystemMemory(numparticles, numsources, additionalbytes)) @@ -1226,14 +1214,14 @@ bool initParticleSystem(ParticleSystem *&PartSys, uint16_t additionalbytes) DEBUG_PRINT(F("PS init failed: memory depleted")); return false; } - Serial.print("segment.data ptr"); - Serial.println((uintptr_t)(SEGMENT.data)); + //Serial.print("segment.data ptr"); + //Serial.println((uintptr_t)(SEGMENT.data)); uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); - Serial.println("calling constructor"); + //Serial.println("calling constructor"); PartSys = new (SEGMENT.data) ParticleSystem(cols, rows, numparticles, numsources); // particle system constructor TODO: why does VS studio thinkt this is bad? - Serial.print("PS pointer at "); - Serial.println((uintptr_t)PartSys); + //Serial.print("PS pointer at "); + //Serial.println((uintptr_t)PartSys); return true; } diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index af8c37d1bc..2b9a91dec2 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -44,7 +44,7 @@ #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_MINSURFACEHARDNESS 128 //minimum hardness used in collision impulse calculation, below this hardness, particles become sticky -#define PS_P_MAXSPEED 200 //maximum speed a particle can have +#define PS_P_MAXSPEED 120 //maximum speed a particle can have (vx/vy is int8) //struct for a single particle typedef struct { @@ -105,7 +105,7 @@ class ParticleSystem void particleMoveUpdate(PSparticle &part, PSsettings &options); //particle physics - void applyGravity(PSparticle *part, uint32_t numarticles, uint8_t force, uint8_t *counter); + void applyGravity(PSparticle *part, uint32_t numarticles, int8_t force, uint8_t *counter); void applyGravity(PSparticle *part, uint32_t numarticles, uint8_t *counter); //use global gforce void applyGravity(PSparticle *part); //use global system settings void applyForce(PSparticle *part, uint32_t numparticles, int8_t xforce, int8_t yforce, uint8_t *counter); @@ -153,7 +153,8 @@ class ParticleSystem //utility functions void updatePSpointers(); // update the data pointers to current segment data space int32_t wraparound(int32_t w, int32_t maxvalue); - int32_t calcForce_dV(int8_t force, uint8_t *counter); + int32_t calcForce_dv(int8_t force, uint8_t *counter); + int32_t limitSpeed(int32_t speed); CRGB **allocate2Dbuffer(uint32_t cols, uint32_t rows); // note: variables that are accessed often are 32bit for speed @@ -161,7 +162,7 @@ class ParticleSystem int32_t collisionHardness; int32_t wallHardness; uint8_t gforcecounter; //counter for global gravity - uint8_t gforce; //gravity strength, default is 8 + int8_t gforce; //gravity strength, default is 8 (negative is allowed) uint8_t collisioncounter; //counter to handle collisions }; From 50489f7364f4f312a940c368dce5fda6316366a0 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Tue, 16 Apr 2024 19:29:05 +0200 Subject: [PATCH 058/219] work in progress, added motion blur and line attracto (non working) --- wled00/FX.cpp | 249 ++++++++++++++++++++++++++++-------- wled00/FXparticleSystem.cpp | 149 ++++++++++++++++++--- wled00/FXparticleSystem.h | 11 +- 3 files changed, 333 insertions(+), 76 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 6cc90a5d18..16bb461d72 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -5993,6 +5993,7 @@ static const char _data_FX_MODE_2DGHOSTRIDER[] PROGMEM = "Ghost Rider@Fade rate, // 2D Floating Blobs // //////////////////////////// //// Floating Blobs by stepko (c)2021 [https://editor.soulmatelights.com/gallery/573-blobs], adapted by Blaz Kristan (AKA blazoncek) +/* #define MAX_BLOBS 8 uint16_t mode_2Dfloatingblobs(void) { if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up @@ -6087,7 +6088,7 @@ uint16_t mode_2Dfloatingblobs(void) { } #undef MAX_BLOBS static const char _data_FX_MODE_2DBLOBS[] PROGMEM = "Blobs@!,# blobs,Blur,Trail;!;!;2;c1=8"; - +*/ //////////////////////////// // 2D Scrolling text // @@ -7898,14 +7899,14 @@ uint16_t mode_particlevortex(void) uint32_t j = 0; uint8_t spraycount = 1 + (SEGMENT.custom1 >> 5); // number of sprays to display, 1-8 - if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. + if (SEGMENT.call == 0) // initialization { if (!initParticleSystem(PartSys)) return mode_static(); // allocation failed; //allocation failed //SEGMENT.aux0 = 0; // starting angle SEGMENT.aux1 = 0x01; // check flags - // TODO: use SEGMENT.step for smooth direction change + numSprays = min(PartSys->numSources, (uint8_t) 8); for (i = 0; i < numSprays; i++) { @@ -7937,7 +7938,7 @@ uint16_t mode_particlevortex(void) DEBUG_PRINT(F("ERROR: FX PartSys nullpointer")); return mode_static(); // something went wrong, no data! } - //DEBUG_PRINTF_P(PSTR("segment data ptr in candy FX %p\n"), SEGMENT.data); + PartSys->updateSystem(); // update system properties (dimensions and data pointers) numSprays = min(PartSys->numSources, (uint8_t)8); @@ -7962,12 +7963,15 @@ uint16_t mode_particlevortex(void) } } - // set rotation direction and speed TODO: use SEGMENT.step to increment until speed is reached, increment speed depends on rotation speed as well fast rotations speed up faster too + //TODO: speed increment is still wrong, it only works in autochange, manual change to a lower speed does not work. need to make it better. + // set rotation direction and speed // can use direction flag to determine current direction bool direction = SEGMENT.check2; //no automatic direction change, set it to flag - + int32_t currentspeed = (int32_t)SEGMENT.step; // make a signed integer out of step + if (SEGMENT.custom2 > 0) // automatic direction change enabled { + // speedincrement = 1 + (SEGMENT.speed >> 5) + (SEGMENT.custom2 >> 2); uint16_t changeinterval = (270 - SEGMENT.custom2); direction = SEGMENT.aux1 & 0x02; //set direction according to flag @@ -7986,45 +7990,58 @@ uint16_t mode_particlevortex(void) } } - int32_t currentspeed = (int32_t)SEGMENT.step; //make a signed integer out of step - int32_t speedincrement = 20 + (SEGMENT.speed >> 5) + (SEGMENT.custom2 >> 2); - if (direction) - { - if(currentspeed < (SEGMENT.speed << 2)) //speed is not on target speed yet - currentspeed += speedincrement; - } - else + int32_t targetspeed = (direction ? 1 : -1) * (SEGMENT.speed << 2); + int32_t speeddiff = targetspeed - currentspeed; + int32_t speedincrement = speeddiff / 50; + + if (speedincrement == 0) //if speeddiff is not zero, make the increment at least 1 so it reaches target speed { - if (currentspeed > -(SEGMENT.speed << 2)) // speed is not on target speed yet - currentspeed -= speedincrement; + if(speeddiff < 0) + speedincrement = -1; + else if (speeddiff > 0) + speedincrement = 1; } + + currentspeed += speedincrement; SEGMENT.aux0 += currentspeed; SEGMENT.step = (uint32_t)currentspeed; //save it back // calculate angle offset for an even distribution uint16_t angleoffset = 0xFFFF / spraycount; - for (j = 0; j < spraycount; j++) - { + //int32_t particlespeeddiv = ((263 - SEGMENT.intensity) >> 3); + //int32_t particlespeed = 127/particlespeeddiv; //just for testing, need to replace this with angle emit and come up with a new speed calculation + //particle speed goes from 7 to 128 (sin cos return 15bit value but with sign) + + // for (j = 0; j < spraycount; j++) //TODO: use angle emit + // { + // 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) - } + // PartSys->sources[j].vx = (cos16(SEGMENT.aux0 + angleoffset * j) >> 8) / particlespeeddiv; // update spray angle (rotate all sprays with angle offset) + // PartSys->sources[j].vy = (sin16(SEGMENT.aux0 + angleoffset * j) >> 8) / particlespeeddiv; // 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) + // } -//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) +//test to check if less particles are ok at lower speeds. + uint32_t skip = PS_P_HALFRADIUS/SEGMENT.intensity + 1; + if (SEGMENT.call % skip == 0) { - #ifdef ESP8266 - if (SEGMENT.call & 0x01) // every other frame, do not emit to save particles - #endif - PartSys->sprayEmit(PartSys->sources[j]); - j = (j + 1) % spraycount; + 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) + { + PartSys->sources[j].var = (SEGMENT.custom3 >> 1); //update speed variation + #ifdef ESP8266 + if (SEGMENT.call & 0x01) // every other frame, do not emit to save particles + #endif + PartSys->angleEmit(PartSys->sources[j], SEGMENT.aux0 + angleoffset * j, SEGMENT.intensity >> 2); + //PartSys->sprayEmit(PartSys->sources[j]); + j = (j + 1) % spraycount; + } } - PartSys->update(); //update all particles and render to frame + + SEGMENT.blur(50); //TODO: put this in particle system for faster rendering return FRAMETIME; } static const char _data_FX_MODE_PARTICLEVORTEX[] PROGMEM = "PS Vortex@Rotation Speed,Particle Speed,Arms,Auto Flip,Nozzle,Random Color, Direction, Random Flip;;!;2;pal=56,sx=200,ix=190,c1=200,c2=0,c3=0,o1=0,o2=0,o3=0"; @@ -8292,7 +8309,7 @@ uint16_t mode_particlefire(void) if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. { - if (!initParticleSystem(PartSys)) + if (!initParticleSystem(PartSys, 4)) //need 4 additional bytes for time keeping (uint32_t lastcall) return mode_static(); // allocation failed; //allocation failed // Serial.println("fireinit done"); SEGMENT.aux0 = rand(); // aux0 is wind position (index) in the perlin noise @@ -8320,6 +8337,17 @@ uint16_t mode_particlefire(void) // DEBUG_PRINTF_P(PSTR("segment data ptr in Fire FX %p\n"), SEGMENT.data); PartSys->updateSystem(); // update system properties (dimensions and data pointers) + if (SEGMENT.check1) //slow i.e. low fps is enabled TODO: test if this works, adjust if needed + { + uint32_t *lastcall = reinterpret_cast(PartSys->PSdataEnd); + uint32_t period = millis() - *lastcall; + *lastcall = millis(); + if (period < 30) // limit to approximately 30FPS + { + return FRAMETIME; //do not update this frame + } + } + uint32_t spread = (PartSys->maxX >> 5) * (SEGMENT.custom3 + 1); //fire around segment center (in subpixel points) numFlames = min((uint32_t)PartSys->numSources, (1 + ((spread / PS_P_RADIUS) << 1))); // number of flames used depends on spread with, good value is (fire width in pixel) * 2 uint32_t percycle = numFlames*2/3;// / 2; // maximum number of particles emitted per cycle (TODO: for ESP826 maybe use flames/2) @@ -8424,21 +8452,13 @@ uint16_t mode_particlefire(void) } Serial.println("B"); }*/ - if(SEGMENT.check1) //low fps is enabled - { - static uint32_t lastcall; //!!! put this in heap - while(millis()-lastcall < 25) - { - yield(); - } - lastcall = millis(); - } + PartSys->updateFire(SEGMENT.intensity); // update and render the fire return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEFIRE[] PROGMEM = "PS Fire@Speed,Intensity,Base Heat,Wind,Spread,FPS Limit,Cylinder,Turbulence;;!;2;pal=35,sx=130,ix=120,c1=110,c2=128,c3=22,o1=0"; +static const char _data_FX_MODE_PARTICLEFIRE[] PROGMEM = "PS Fire@Speed,Intensity,Base Heat,Wind,Spread,Slow,Cylinder,Turbulence;;!;2;pal=35,sx=130,ix=120,c1=110,c2=128,c3=22,o1=0"; /* PS Ballpit: particles falling down, user can enable these three options: X-wraparound, side bounce, ground bounce @@ -8473,7 +8493,7 @@ uint16_t mode_particlepit(void) PartSys->setWrapX(SEGMENT.check1); PartSys->setBounceX(SEGMENT.check2); PartSys->setBounceY(SEGMENT.check3); - PartSys->setWallHardness(min(SEGMENT.custom2, (uint8_t)200)); // wall hardness is 200 or more + PartSys->setWallHardness(min(SEGMENT.custom2, (uint8_t)200)); //limit to 200 min if (SEGMENT.custom2>0) { PartSys->enableParticleCollisions(true, SEGMENT.custom2); // enable collisions and set particle collision hardness @@ -8494,7 +8514,7 @@ uint16_t mode_particlepit(void) PartSys->particles[i].ttl = 1500 - (SEGMENT.speed << 2) + random16(500); // if speed is higher, make them die sooner PartSys->particles[i].x = random(PartSys->maxX >> 1) + (PartSys->maxX >> 2); PartSys->particles[i].y = (PartSys->maxY<<1); // particles appear somewhere above the matrix, maximum is double the height - PartSys->particles[i].vx = (((int16_t)random(SEGMENT.custom1)) - (SEGMENT.custom1 >> 1)+5) >> 1; // side speed is +/- a quarter of the custom1 slider + PartSys->particles[i].vx = (int16_t)random(100) - 50; // side speed is +/- PartSys->particles[i].vy = map(SEGMENT.speed, 0, 255, -5, -100); // downward speed PartSys->particles[i].hue = random16(); // set random color PartSys->particles[i].sat = ((SEGMENT.custom3) << 3) + 7; // set saturation @@ -8512,10 +8532,16 @@ uint16_t mode_particlepit(void) PartSys->applyFriction(frictioncoefficient); PartSys->update(); // update and render - + SEGMENT.blur(SEGMENT.custom1, true); + //SEGMENT.blur(250); + ///SEGMENT.blur(64); + //SEGMENT.blur(64); + //SEGMENT.blur(64); + //SEGMENT.blur(64); + // SEGMENT.blur(64); //!!! return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEPIT[] PROGMEM = "PS Ballpit@Speed,Intensity,Randomness,Hardness,Saturation,Cylinder,Walls,Ground;;!;2;pal=11,sx=100,ix=200,c1=120,c2=100,c3=31,o1=0,o2=0,o3=1"; +static const char _data_FX_MODE_PARTICLEPIT[] PROGMEM = "PS Ballpit@Speed,Intensity,Size,Hardness,Saturation,Cylinder,Walls,Ground;;!;2;pal=11,sx=100,ix=200,c1=120,c2=100,c3=31,o1=0,o2=0,o3=1"; /* * Particle Waterfall @@ -8533,9 +8559,10 @@ uint16_t mode_particlewaterfall(void) if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. { if (!initParticleSystem(PartSys)) // init, no additional data needed - return mode_static(); // allocation failed; //allocation failed - PartSys->enableGravity(true); // enable with default gforce - PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting) + return mode_static(); // allocation failed; //allocation failed + PartSys->enableGravity(true); // enable with default gforce + PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting) + PartSys->setMotionBlur(190); //anable motion blur numSprays = min((uint32_t)PartSys->numSources, min(PartSys->maxXpixel/5, (uint32_t)2)); // number of sprays for (i = 0; i < numSprays; i++) { @@ -8928,7 +8955,7 @@ 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) @@ -9018,7 +9045,7 @@ uint16_t mode_particleattractor(void) // apply force for(i = 0; i < displayparticles; i++) { - PartSys->attract(&PartSys->particles[i], attractor, counters, SEGMENT.speed, SEGMENT.check3); + PartSys->pointAttractor(&PartSys->particles[i], attractor, &counters[i], SEGMENT.speed, SEGMENT.check3); //TODO: there was a bug here, counters was always the same pointer, check if this works. } if (SEGMENT.call % (33 - SEGMENT.custom3) == 0) PartSys->applyFriction(2); @@ -9029,6 +9056,117 @@ uint16_t mode_particleattractor(void) return FRAMETIME; } static const char _data_FX_MODE_PARTICLEATTRACTOR[] PROGMEM = "PS Attractor@Mass,Particles,Emit Speed,Collisions,Friction,Color by Age,Move,Swallow;;!;2;pal=9,sx=100,ix=82,c1=190,c2=0,o1=0,o2=0,o3=0"; +*/ +/* +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(); + ParticleSystem *PartSys = NULL; + uint32_t i = 0; + PSparticle *attractor; // particle pointer to the attractor + uint8_t *counters; // counters for the applied force + PSsettings sourcesettings = {0, 0, 1, 1, 0, 0, 0, 0}; // PS settings for bounceY, bounceY used for source movement (it always bounces whereas particles do not) + + if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. + { + uint32_t numParticles = (calculateNumberOfParticles() * 2) / 3; // use 75% of available particles to keep FPS high (also we need an attractor particle) + if (!initParticleSystem(PartSys, numParticles)) // init, need one extra byte per used particle for force counters + return mode_static(); // allocation failed; //allocation failed + + PartSys->sources[0].source.hue = random16(); + PartSys->sources[0].source.sat = 255; // set full saturation + PartSys->sources[0].source.x = PS_P_RADIUS; // start out in bottom left corner + PartSys->sources[0].source.y = PS_P_RADIUS << 1; + PartSys->sources[0].source.vx = random16(5) + 3; + PartSys->sources[0].source.vy = PartSys->sources[0].source.vx - 2; // move slower in y + PartSys->sources[0].source.collide = true; // seeded particles will collide + PartSys->sources[0].source.ttl = 100; // is replenished below, it never dies +#ifdef ESP8266 + PartSys->sources[0].maxLife = 200; // lifetime in frames (ESP8266 has less particles) + PartSys->sources[0].minLife = 30; +#else + PartSys->sources[0].maxLife = 350; // lifetime in frames + PartSys->sources[0].minLife = 50; +#endif + PartSys->sources[0].vx = 0; // emitting speed + PartSys->sources[0].vy = 0; // emitting speed + PartSys->sources[0].var = 7; // emiting variation + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + + if (PartSys == NULL) + { + DEBUG_PRINT(F("ERROR: FX PartSys nullpointer")); + return mode_static(); // something went wrong, no data! (TODO: ask how to handle this so it always works) + } + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setWallHardness(230); // walls are always same hardness + PartSys->setColorByAge(SEGMENT.check1); + + if (SEGMENT.custom2 > 0) // collisions enabled + PartSys->enableParticleCollisions(true, SEGMENT.custom2); // enable collisions and set particle collision hardness + else + PartSys->enableParticleCollisions(false); + + uint16_t lastusedparticle = (PartSys->numParticles * 2) / 3; + uint32_t displayparticles = map(SEGMENT.intensity, 0, 255, 10, lastusedparticle); + PartSys->setUsedParticles(displayparticles); + + // set pointers + attractor = reinterpret_cast(&PartSys->particles[lastusedparticle + 1]); + counters = reinterpret_cast(PartSys->PSdataEnd); + // set attractor properties + if (SEGMENT.check2) // move attractor + { + attractor->vx = PartSys->sources[0].source.vy; // set to spray movemement but reverse x and y + attractor->vy = PartSys->sources[0].source.vx; + PartSys->particleMoveUpdate(*attractor, sourcesettings); // move the attractor + } + else + { + attractor->vx = 0; // not moving + attractor->vy = 0; + attractor->x = PartSys->maxX >> 1; // center + attractor->y = PartSys->maxY >> 1; + } + + if (SEGMENT.call % 5 == 0) + { + PartSys->sources[0].source.hue++; + PartSys->sources[0].source.ttl = 100; // spray never dies + } + + SEGMENT.aux0 += 256; // emitting angle, one full turn in 255 frames (0xFFFF is 360°) + if (SEGMENT.call % 2 == 0) // alternate direction of emit + PartSys->angleEmit(PartSys->sources[0], SEGMENT.aux0, SEGMENT.custom1 >> 4); + else + PartSys->angleEmit(PartSys->sources[0], SEGMENT.aux0 + 0x7FFF, SEGMENT.custom1 >> 4); // emit at 180° as well + + SEGMENT.aux1 = 0;//++; //line attractor angle + // apply force + if(SEGMENT.call % 2 == 0) + for (i = 0; i < displayparticles; i++) + { + PartSys->lineAttractor(&PartSys->particles[i], attractor, SEGMENT.aux1, &counters[i], SEGMENT.speed); + } + if (SEGMENT.call % (33 - SEGMENT.custom3) == 0) + PartSys->applyFriction(2); + + PartSys->particleMoveUpdate(PartSys->sources[0].source, sourcesettings); // move the source + + PartSys->update(); // update and render + return FRAMETIME; +} +static const char _data_FX_MODE_PARTICLEATTRACTOR[] PROGMEM = "PS Attractor@Mass,Particles,Emit Speed,Collisions,Friction,Color by Age,Move,Swallow;;!;2;pal=9,sx=100,ix=82,c1=190,c2=0,o1=0,o2=0,o3=0"; /* Particle Spray, just a particle spray with many parameters @@ -9045,12 +9183,13 @@ uint16_t mode_particlespray(void) uint32_t i; const uint8_t hardness = 200; //collision hardness is fixed - if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. + if (SEGMENT.call == 0) // initialization { if (!initParticleSystem(PartSys)) // init, no additional data needed return mode_static(); // allocation failed; //allocation failed PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting) PartSys->setBounceY(true); + PartSys->setMotionBlur(230); // anable motion blur numSprays = min(PartSys->numSources, (uint8_t)1); // number of sprays for (i = 0; i < numSprays; i++) { @@ -9126,7 +9265,7 @@ uint16_t mode_particleGEQ(void) ParticleSystem *PartSys = NULL; - if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. + if (SEGMENT.call == 0) // initialization { if (!initParticleSystem(PartSys)) // init return mode_static(); // allocation failed; //allocation failed @@ -9139,7 +9278,7 @@ uint16_t mode_particleGEQ(void) if (PartSys == NULL) { DEBUG_PRINT(F("ERROR: FX PartSys nullpointer")); - return mode_static(); // something went wrong, no data! (TODO: ask how to handle this so it always works) + return mode_static(); // something went wrong, no data! } uint32_t i; @@ -9542,7 +9681,7 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_2DSPACESHIPS, &mode_2Dspaceships, _data_FX_MODE_2DSPACESHIPS); addEffect(FX_MODE_2DCRAZYBEES, &mode_2Dcrazybees, _data_FX_MODE_2DCRAZYBEES); addEffect(FX_MODE_2DGHOSTRIDER, &mode_2Dghostrider, _data_FX_MODE_2DGHOSTRIDER); - addEffect(FX_MODE_2DBLOBS, &mode_2Dfloatingblobs, _data_FX_MODE_2DBLOBS); + //addEffect(FX_MODE_2DBLOBS, &mode_2Dfloatingblobs, _data_FX_MODE_2DBLOBS); addEffect(FX_MODE_2DSCROLLTEXT, &mode_2Dscrollingtext, _data_FX_MODE_2DSCROLLTEXT); addEffect(FX_MODE_2DDRIFTROSE, &mode_2Ddriftrose, _data_FX_MODE_2DDRIFTROSE); addEffect(FX_MODE_2DDISTORTIONWAVES, &mode_2Ddistortionwaves, _data_FX_MODE_2DDISTORTIONWAVES); diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index 4fee58577c..53faa3c8ac 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -33,10 +33,11 @@ /* TODO: - -add function to 'update sources' so FX does not have to take care of that. FX can still implement its own version if so desired. config should be optional, if not set, use default config. + -add function to 'update sources' so FX does not have to take care of that. FX can still implement its own version if so desired. config should be optional, if not set, use default config. -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) + */ // sources need to be updatable by the FX, so functions are needed to apply it to a single particle that are public @@ -55,6 +56,8 @@ ParticleSystem::ParticleSystem(uint16_t width, uint16_t height, uint16_t numbero updatePSpointers(); // set the particle and sources pointer (call this before accessing sprays or particles) setMatrixSize(width, height); setWallHardness(255); // set default wall hardness to max + particlesize = 0; //minimum size + motionBlur = 0; //no fading by default emitIndex = 0; /* Serial.println("alive particles: "); @@ -152,6 +155,16 @@ void ParticleSystem::setColorByAge(bool enable) particlesettings.colorByAge = enable; } +void ParticleSystem::setMotionBlur(uint8_t bluramount) +{ + motionBlur = bluramount; +} + +// render size using smearing +void ParticleSystem::setParticleSize(uint8_t size) +{ + particlesize = size; +} // 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) @@ -229,11 +242,15 @@ void ParticleSystem::flameEmit(PSsource &emitter) // Emits a particle at given angle and speed, angle is from 0-65535 (=0-360deg), speed is also affected by emitter->var // angle = 0 means in positive x-direction (i.e. to the right) -void ParticleSystem::angleEmit(PSsource &emitter, uint16_t angle, uint32_t speed) +void ParticleSystem::angleEmit(PSsource &emitter, uint16_t angle, int8_t speed) { - emitter.vx = ((int32_t)cos16(angle) * speed) / 32767; // cos16() and sin16() return signed 16bit - emitter.vy = ((int32_t)sin16(angle) * speed) / 32767; // note: cannot use bit shifts as bit shifting is asymmetrical for positive and negative numbers and this needs to be accurate! + emitter.vx = ((int32_t)cos16(angle) * (int32_t)speed) / (int32_t)32600; // cos16() and sin16() return signed 16bit, division should be 32767 but 32600 gives slightly better rounding + emitter.vy = ((int32_t)sin16(angle) * (int32_t)speed) / (int32_t)32600; // note: cannot use bit shifts as bit shifting is asymmetrical for positive and negative numbers and this needs to be accurate! sprayEmit(emitter); + Serial.print(" x: "); + Serial.print(emitter.vx); + Serial.print(" y: "); + Serial.println(emitter.vy); } // particle moves, decays and dies, if killoutofbounds is set, out of bounds particles are set to ttl=0 @@ -285,12 +302,9 @@ void ParticleSystem::particleMoveUpdate(PSparticle &part, PSsettings &options) { if (newY < PS_P_RADIUS) // bounce at bottom { - if(part.vy < -10) - { part.vy = -part.vy; // invert speed part.vy = ((int32_t)part.vy * wallHardness) / 255; // reduce speed as energy is lost on non-hard surface newY = PS_P_RADIUS; - } } else { @@ -465,14 +479,14 @@ void ParticleSystem::applyFriction(uint8_t coefficient) } // attracts a particle to an attractor particle using the inverse square-law -void ParticleSystem::attract(PSparticle *part, PSparticle *attractor, uint8_t *counter, uint8_t strength, bool swallow) +void ParticleSystem::pointAttractor(PSparticle *part, PSparticle *attractor, uint8_t *counter, uint8_t strength, bool swallow) { // Calculate the distance between the particle and the attractor int32_t dx = attractor->x - part->x; int32_t dy = attractor->y - part->y; // Calculate the force based on inverse square law - int32_t distanceSquared = dx * dx + dy * dy + 1; + int32_t distanceSquared = dx * dx + dy * dy; if (distanceSquared < 8192) { if (swallow) // particle is close, age it fast so it fades out, do not attract further @@ -485,7 +499,7 @@ void ParticleSystem::attract(PSparticle *part, PSparticle *attractor, uint8_t *c return; } } - distanceSquared = 4 * PS_P_RADIUS * PS_P_RADIUS; // limit the distance to avoid very high forces + distanceSquared = 2 * PS_P_RADIUS * PS_P_RADIUS; // limit the distance to avoid very high forces } int32_t force = ((int32_t)strength << 16) / distanceSquared; @@ -495,6 +509,56 @@ void ParticleSystem::attract(PSparticle *part, PSparticle *attractor, uint8_t *c applyForce(part, 1, xforce, yforce, counter); } +void ParticleSystem::lineAttractor(PSparticle *part, PSparticle *attractorcenter, uint16_t attractorangle, uint8_t *counter, uint8_t strength) +{ + // Calculate the distance between the particle and the attractor + + //calculate a second point on the line + int32_t x1 = attractorcenter->x + (cos16(attractorangle) >> 5); + int32_t y1 = attractorcenter->y + (sin16(attractorangle) >> 5); + //calculate squared distance from particle to the line: + int32_t dx = (x1 - attractorcenter->x) >> 4; + int32_t dy = (y1 - attractorcenter->y) >> 4; + int32_t d = ((dx * (part->y - attractorcenter->y)) - (dy * (part->x - attractorcenter->x))) >> 8; + int32_t distanceSquared = (d * d) / (dx * dx + dy * dy); + + + // Calculate the force based on inverse square law + if (distanceSquared < 2) + { + distanceSquared = 1; + // distanceSquared = 4 * PS_P_RADIUS * PS_P_RADIUS; // limit the distance to avoid very high forces + } + + int32_t force = (((int32_t)strength << 16) / distanceSquared)>>10; + //apply force in a 90° angle to the line + int8_t xforce = (d > 0 ? 1 : -1) * (force * dy) / 100; // scale to a lower value, found by experimenting + int8_t yforce = (d > 0 ? -1 : 1) * (force * dx) / 100; // note: cannot use bit shifts as bit shifting is asymmetrical for positive and negative numbers and this needs to be accurate! +/* + Serial.print(" partx: "); + Serial.print(part->x); + Serial.print(" party "); + Serial.print(part->y); + Serial.print(" x1 "); + Serial.print(x1); + Serial.print(" y1 "); + Serial.print(y1); + Serial.print(" dx "); + Serial.print(dx); + Serial.print(" dy "); + Serial.print(dy); + Serial.print(" d: "); + Serial.print(d); + Serial.print(" dsq: "); + Serial.print(distanceSquared); + Serial.print(" force: "); + Serial.print(force); + Serial.print(" fx: "); + Serial.print(xforce); + Serial.print(" fy: "); + Serial.println(yforce);*/ + applyForce(part, 1, xforce, yforce, counter); +} // 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 // warning: do not render out of bounds particles or system will crash! rendering does not check if particle is out of bounds @@ -503,19 +567,42 @@ void ParticleSystem::ParticleSys_render(bool firemode, uint32_t fireintensity) { int32_t pixco[4][2]; //physical pixel coordinates of the four pixels a particle is rendered to. x,y pairs CRGB baseRGB; - bool useLocalBuffer = true; + bool useLocalBuffer = true; //use local rendering buffer, gives huge speed boost (at least 30% more FPS) CRGB **colorbuffer; uint32_t i; - uint32_t brightness; // particle brightness, fades if dying - //CRGB colorbuffer[maxXpixel+1][maxYpixel+1] = {0}; //put buffer on stack (not a good idea, can cause crashes on large segments if other function run the stack into the heap) + uint32_t brightness; // particle brightness, fades if dying + // CRGB colorbuffer[maxXpixel+1][maxYpixel+1] = {0}; //put buffer on stack (not a good idea, can cause crashes on large segments if other function run the stack into the heap) if (useLocalBuffer) { // allocate memory for the local renderbuffer colorbuffer = allocate2Dbuffer(maxXpixel + 1, maxYpixel + 1); if (colorbuffer == NULL) - useLocalBuffer = false; //render to segment pixels directly if not enough memory - } + useLocalBuffer = false; //render to segment pixels directly if not enough memory + if (motionBlur > 0) // using SEGMENT.fadeToBlackBy is much slower, this approximately doubles the speed of fade calculation + { + uint32_t residual = motionBlur; //32bit for faster calculation + uint32_t yflipped; + for (int y = 0; y <= maxYpixel; y++) + { + yflipped = maxYpixel - y; + for (int x = 0; x <= maxXpixel; x++) + { + colorbuffer[x][y] = SEGMENT.getPixelColorXY(x, yflipped); + colorbuffer[x][y].r = (colorbuffer[x][y].r * residual) >> 8; + colorbuffer[x][y].g = (colorbuffer[x][y].g * residual) >> 8; + colorbuffer[x][y].b = (colorbuffer[x][y].b * residual) >> 8; + } + } + } + } + else + { + if (motionBlur > 0) + SEGMENT.fadeToBlackBy(256 - motionBlur); + else + SEGMENT.fill(BLACK); //clear the buffer before rendering to it + } // go over particles and render them to the buffer for (i = 0; i < usedParticles; i++) { @@ -589,8 +676,7 @@ void ParticleSystem::ParticleSys_render(bool firemode, uint32_t fireintensity) colorbuffer[pixco[3][0]][pixco[3][1]] = fast_color_add(colorbuffer[pixco[3][0]][pixco[3][1]], baseRGB, pxlbrightness[3]); // top left } else - { - SEGMENT.fill(BLACK); // clear the matrix + { if (pxlbrightness[0] > 0) SEGMENT.addPixelColorXY(pixco[0][0], maxYpixel - pixco[0][1], baseRGB.scale8((uint8_t)pxlbrightness[0])); // bottom left if (pxlbrightness[1] > 0) @@ -599,12 +685,39 @@ void ParticleSystem::ParticleSys_render(bool firemode, uint32_t fireintensity) SEGMENT.addPixelColorXY(pixco[2][0], maxYpixel - pixco[2][1], baseRGB.scale8((uint8_t)pxlbrightness[2])); // top right if (pxlbrightness[3] > 0) SEGMENT.addPixelColorXY(pixco[3][0], maxYpixel - pixco[3][1], baseRGB.scale8((uint8_t)pxlbrightness[3])); // top left + /* + uint32_t color = RGBW32(baseRGB.r, baseRGB.g, baseRGB.b, 0); + if (pxlbrightness[0] > 0) + SEGMENT.addPixelColorXY(pixco[0][0], maxYpixel - pixco[0][1], color_scale(color, pxlbrightness[0])); // bottom left + if (pxlbrightness[1] > 0) + SEGMENT.addPixelColorXY(pixco[1][0], maxYpixel - pixco[1][1], color_scale(color, pxlbrightness[1])); // bottom right + if (pxlbrightness[2] > 0) + SEGMENT.addPixelColorXY(pixco[2][0], maxYpixel - pixco[2][1], color_scale(color, pxlbrightness[2])); // top right + if (pxlbrightness[3] > 0) + SEGMENT.addPixelColorXY(pixco[3][0], maxYpixel - pixco[3][1], color_scale(color, pxlbrightness[3])); // 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), could probably be extended to 3x3 // SEGMENT.addPixelColorXY(pixco[1][0] + 1, maxYpixel - pixco[1][1], baseRGB.scale8((uint8_t)((brightness>>1) - pxlbrightness[0])), fastcoloradd); // SEGMENT.addPixelColorXY(pixco[2][0] + 1, maxYpixel - pixco[2][1], baseRGB.scale8((uint8_t)((brightness>>1) -pxlbrightness[3])), fastcoloradd); } } - if (useLocalBuffer) + + if(particlesize > 0) + { + if (useLocalBuffer) + { + //TODO: come up with a good and short 2D smearing function derived from blur + //put it in a function taking width, height and buffer pointer, so it can be used to blur individual particles for different sizes (will be slow) -> or maybe not, there is little use + // and would need individual sizes for each particle, + } + else + SEGMENT.blur(particlesize, true); //todo: come up with good algorithm for size + } + + if (useLocalBuffer) //transfer local buffer back to segment { uint32_t yflipped; for (int y = 0; y <= maxYpixel; y++) diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index 2b9a91dec2..be903269be 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -98,7 +98,7 @@ class ParticleSystem // particle emitters void flameEmit(PSsource &emitter); void sprayEmit(PSsource &emitter); - void angleEmit(PSsource& emitter, uint16_t angle, uint32_t speed); + void angleEmit(PSsource& emitter, uint16_t angle, int8_t speed); void updateSystem(void); // call at the beginning of every FX, updates pointers and dimensions // move functions @@ -114,8 +114,9 @@ class ParticleSystem void applyAngleForce(PSparticle *part, uint32_t numparticles, uint8_t force, uint16_t angle); void applyFriction(PSparticle *part, uint8_t coefficient); // apply friction to specific particle void applyFriction(uint8_t coefficient); // apply friction to all used particles - void attract(PSparticle *particle, PSparticle *attractor, uint8_t *counter, uint8_t strength, bool swallow); - + void pointAttractor(PSparticle *particle, PSparticle *attractor, uint8_t *counter, uint8_t strength, bool swallow); + void lineAttractor(PSparticle *particle, PSparticle *attractorcenter, uint16_t attractorangle, uint8_t *counter, uint8_t strength); + //set options void setUsedParticles(uint16_t num); void setCollisionHardness(uint8_t hardness); //hardness for particle collisions (255 means full hard) @@ -127,6 +128,8 @@ class ParticleSystem void setBounceY(bool enable); void setKillOutOfBounds(bool enable); //if enabled, particles outside of matrix instantly die void setColorByAge(bool enable); + void setMotionBlur(uint8_t bluramount); + void setParticleSize(uint8_t size); void enableGravity(bool enable, uint8_t force = 8); void enableParticleCollisions(bool enable, uint8_t hardness = 255); @@ -164,6 +167,8 @@ class ParticleSystem uint8_t gforcecounter; //counter for global gravity int8_t gforce; //gravity strength, default is 8 (negative is allowed) uint8_t collisioncounter; //counter to handle collisions + uint8_t particlesize; + uint8_t motionBlur; }; //initialization functions (not part of class) From 856527b447d19ae4596b0b40ba92d98c87d92cc5 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sat, 20 Apr 2024 12:10:24 +0200 Subject: [PATCH 059/219] work in progress, added test function to increase particle size, also added wobbling test --- wled00/FX.cpp | 217 ++++++++++++++++++++++-------------- wled00/FXparticleSystem.cpp | 59 +++++----- wled00/FXparticleSystem.h | 11 +- 3 files changed, 170 insertions(+), 117 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 78a2836ed5..f99e3b9bac 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -7897,45 +7897,35 @@ static const char _data_FX_MODE_2DWAVINGCELL[] PROGMEM = "Waving Cell@!,,Amplitu * Uses palette for particle color * by DedeHai (Damian Schneider) */ - +#define NUMBEROFSOURCES 8 uint16_t mode_particlevortex(void) { if (SEGLEN == 1) return mode_static(); - uint8_t numSprays; // maximum number of sprays ParticleSystem *PartSys = NULL; 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 { - if (!initParticleSystem(PartSys)) + if (!initParticleSystem(PartSys, NUMBEROFSOURCES)) return mode_static(); // allocation failed; //allocation failed //SEGMENT.aux0 = 0; // starting angle SEGMENT.aux1 = 0x01; // check flags - - numSprays = min(PartSys->numSources, (uint8_t) 8); + + uint8_t numSprays = min(PartSys->numSources, (uint8_t)NUMBEROFSOURCES); for (i = 0; i < numSprays; i++) { - PartSys->sources[i].source.sat = 255; // set saturation + PartSys->sources[i].source.sat = 255; // set saturation PartSys->sources[i].source.x = (PartSys->maxX + 1) >> 1; // center PartSys->sources[i].source.y = (PartSys->maxY + 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; - } + 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 } PartSys->setKillOutOfBounds(true); } @@ -7949,9 +7939,9 @@ uint16_t mode_particlevortex(void) } PartSys->updateSystem(); // update system properties (dimensions and data pointers) - numSprays = min(PartSys->numSources, (uint8_t)8); + uint8_t spraycount = min(PartSys->numSources, (uint8_t)(1 + (SEGMENT.custom1 >> 5))); // number of sprays to display, 1-8 - if (SEGMENT.check1 != (SEGMENT.aux1 & 0x01)) //state change + if (SEGMENT.check1 != (SEGMENT.aux1 & 0x01) || SEGMENT.call == 0) //state change { if (SEGMENT.check1) SEGMENT.aux1 |= 0x01; //set the flag @@ -8040,7 +8030,7 @@ uint16_t mode_particlevortex(void) for (i = 0; i < spraycount; i++) // emit one particle per spray (if available) { PartSys->sources[j].var = (SEGMENT.custom3 >> 1); //update speed variation - #ifdef ESP8266 +#ifdef ESP8266 if (SEGMENT.call & 0x01) // every other frame, do not emit to save particles #endif PartSys->angleEmit(PartSys->sources[j], SEGMENT.aux0 + angleoffset * j, SEGMENT.intensity >> 2); @@ -8053,6 +8043,7 @@ uint16_t mode_particlevortex(void) SEGMENT.blur(50); //TODO: put this in particle system for faster rendering return FRAMETIME; } +#undef NUMBEROFSOURCES static const char _data_FX_MODE_PARTICLEVORTEX[] PROGMEM = "PS Vortex@Rotation Speed,Particle Speed,Arms,Auto Flip,Nozzle,Random Color, Direction, Random Flip;;!;2;pal=56,sx=200,ix=190,c1=200,c2=0,c3=0,o1=0,o2=0,o3=0"; /* @@ -8061,7 +8052,7 @@ static const char _data_FX_MODE_PARTICLEVORTEX[] PROGMEM = "PS Vortex@Rotation S * Uses ranbow palette as default * by DedeHai (Damian Schneider) */ - +#define NUMBEROFSOURCES 4 uint16_t mode_particlefireworks(void) { if (SEGLEN == 1) @@ -8073,11 +8064,11 @@ uint16_t mode_particlefireworks(void) if (SEGMENT.call == 0) // initialization { - if (!initParticleSystem(PartSys)) // init, no additional data needed + if (!initParticleSystem(PartSys, NUMBEROFSOURCES)) // init, no additional data needed return mode_static(); // allocation failed; //allocation failed PartSys->setKillOutOfBounds(true); //out of bounds particles dont return (except on top, taken care of by gravity setting) PartSys->setWallHardness(100); //ground bounce is fixed - numRockets = min(PartSys->numSources, (uint8_t)4); + numRockets = min(PartSys->numSources, (uint8_t)NUMBEROFSOURCES); for (j = 0; j < numRockets; j++) { PartSys->sources[j].source.ttl = 500 * j; // first rocket starts immediately, others follow soon @@ -8125,11 +8116,11 @@ uint16_t mode_particlefireworks(void) } else // speed is zero, explode! { - #ifdef ESP8266 +#ifdef ESP8266 emitparticles = random16(SEGMENT.intensity >> 3) + (SEGMENT.intensity >> 3) + 5; // defines the size of the explosion - #else +#else emitparticles = random16(SEGMENT.intensity >> 2) + (SEGMENT.intensity >> 2) + 5; // defines the size of the explosion - #endif +#endif PartSys->sources[j].source.vy = -1; // set speed negative so it will emit no more particles after this explosion until relaunch if(random16(4) == 0) { @@ -8219,6 +8210,7 @@ uint16_t mode_particlefireworks(void) PartSys->update(); // update and render return FRAMETIME; } +#undef NUMBEROFSOURCES static const char _data_FX_MODE_PARTICLEFIREWORKS[] PROGMEM = "PS Fireworks@Launches,Explosion Size,Fuse,Gravity,Cylinder,Ground,;;!;2;pal=11,sx=100,ix=50,c1=84,c2=128,c3=12,o1=0,o2=0,o3=0"; /* @@ -8227,6 +8219,7 @@ static const char _data_FX_MODE_PARTICLEFIREWORKS[] PROGMEM = "PS Fireworks@Laun * Uses palette for particle color * by DedeHai (Damian Schneider) */ +#define NUMBEROFSOURCES 1 uint16_t mode_particlevolcano(void) { if (SEGLEN == 1) @@ -8237,12 +8230,12 @@ uint16_t mode_particlevolcano(void) if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. { - if (!initParticleSystem(PartSys)) // init, no additional data needed + if (!initParticleSystem(PartSys, NUMBEROFSOURCES)) // init, no additional data needed return mode_static(); // allocation failed; //allocation failed PartSys->setBounceY(true); PartSys->enableGravity(true); //enable with default gforce PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting) - numSprays = min(PartSys->numSources, (uint8_t)1); //number of sprays + numSprays = min(PartSys->numSources, (uint8_t)NUMBEROFSOURCES); // number of sprays for (i = 0; i < numSprays; i++) { PartSys->sources[i].source.hue = random16(); @@ -8263,7 +8256,7 @@ uint16_t mode_particlevolcano(void) return mode_static(); // something went wrong, no data! } - numSprays = min(PartSys->numSources, (uint8_t)1); // number of sprays + numSprays = min(PartSys->numSources, (uint8_t)NUMBEROFSOURCES); // number of sprays // Particle System settings PartSys->updateSystem(); // update system properties (dimensions and data pointers) @@ -8300,6 +8293,7 @@ uint16_t mode_particlevolcano(void) PartSys->update(); // update and render return FRAMETIME; } +#undef NUMBEROFSOURCES static const char _data_FX_MODE_PARTICLEVOLCANO[] PROGMEM = "PS Volcano@Speed,Intensity,Move,Bounce,Spread,Color by Age,Walls,Collisions;;!;2;pal=35,sx=100,ix=190,c1=0,c2=160,c3=6,o1=1,o2=0,o3=0"; /* @@ -8318,7 +8312,7 @@ uint16_t mode_particlefire(void) if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. { - if (!initParticleSystem(PartSys, 4)) //need 4 additional bytes for time keeping (uint32_t lastcall) + if (!initParticleSystem(PartSys, 255, 4)) //maximum number of source (PS will determine the exact number based on segment size) and need 4 additional bytes for time keeping (uint32_t lastcall) return mode_static(); // allocation failed; //allocation failed // Serial.println("fireinit done"); SEGMENT.aux0 = rand(); // aux0 is wind position (index) in the perlin noise @@ -8349,12 +8343,12 @@ uint16_t mode_particlefire(void) if (SEGMENT.check1) //slow i.e. low fps is enabled TODO: test if this works, adjust if needed { uint32_t *lastcall = reinterpret_cast(PartSys->PSdataEnd); - uint32_t period = millis() - *lastcall; - *lastcall = millis(); + uint32_t period = strip.now - *lastcall; if (period < 30) // limit to approximately 30FPS { return FRAMETIME; //do not update this frame } + *lastcall = strip.now; } uint32_t spread = (PartSys->maxX >> 5) * (SEGMENT.custom3 + 1); //fire around segment center (in subpixel points) @@ -8484,7 +8478,7 @@ uint16_t mode_particlepit(void) if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. { - if (!initParticleSystem(PartSys)) //init + if (!initParticleSystem(PartSys, 1)) //init, request one source (actually dont really need one TODO: test if using zero sources also works) return mode_static(); // allocation failed; //allocation failed PartSys->setKillOutOfBounds(true); PartSys->enableGravity(true); @@ -8533,21 +8527,87 @@ uint16_t mode_particlepit(void) } } - uint32_t frictioncoefficient = 1; - if (SEGMENT.speed < 50) // for low speeds, apply more friction - frictioncoefficient = 50 - SEGMENT.speed; + + uint32_t frictioncoefficient = 1; + if (SEGMENT.speed < 50) // for low speeds, apply more friction + frictioncoefficient = 50 - SEGMENT.speed; + + if (SEGMENT.call % (3 + (SEGMENT.custom2 >> 2)) == 0) + PartSys->applyFriction(frictioncoefficient); + + // blur function that works: (for testing only) + static uint8_t testcntr; + static uint8_t wobbleamount = 255; + wobbleamount -= 3; + + testcntr+=15; + +// int32_t ysize = (int16_t)sin8(testcntr); + // int32_t xsize = 255-ysize; + + int32_t ysize = (int32_t)sin8(testcntr)-128; + int32_t xsize = -ysize; - if (SEGMENT.call % (3+(SEGMENT.custom2>>2)) == 0) - PartSys->applyFriction(frictioncoefficient); + //ysize = (((int16_t)SEGMENT.custom1 * ysize) >> 8); + //xsize = (((int16_t)SEGMENT.custom1 * xsize) >> 8); + ysize = (int32_t)SEGMENT.custom1 - ((ysize * wobbleamount * SEGMENT.custom1) >> 15); + xsize = (int32_t)SEGMENT.custom1 - ((xsize * wobbleamount * SEGMENT.custom1) >> 15); + + Serial.print(SEGMENT.custom1); + Serial.print(" "); + Serial.print(wobbleamount); + Serial.print(" "); + + Serial.print(xsize); + Serial.print(" "); + Serial.println(ysize); + + // PartSys->setParticleSize(SEGMENT.custom1); + PartSys->update(); // update and render +/* + const unsigned cols = PartSys->maxXpixel + 1; + const unsigned rows = PartSys->maxYpixel + 1; + for (unsigned i = 0; i < cols; i++) + { + SEGMENT.blurCol(i, xsize, true); + if (xsize > 64) + SEGMENT.blurCol(i, xsize - 64, true); + if (xsize > 128) + SEGMENT.blurCol(i, (xsize - 128) << 1, true); + if (xsize > 192) + SEGMENT.blurCol(i, (xsize - 192) << 1, true); + } + for (unsigned i = 0; i < rows; i++){ + SEGMENT.blurRow(i, ysize, true); + if (ysize > 64) + SEGMENT.blurRow(i, ysize - 64, true); + if (ysize > 128) + SEGMENT.blurRow(i, (ysize - 128) << 1, true); + if (ysize > 192) + SEGMENT.blurRow(i, (ysize - 192) << 1, true); + } + + for (unsigned i = 0; i < cols; i++) + { + SEGMENT.blurCol(i, xsize, true); + if (xsize > 64) + SEGMENT.blurCol(i, xsize - 64, true); + if (xsize > 128) + SEGMENT.blurCol(i, (xsize - 128) << 1, true); + if (xsize > 192) + SEGMENT.blurCol(i, (xsize - 192) << 1, true); + } + for (unsigned i = 0; i < rows; i++) + { + SEGMENT.blurRow(i, ysize, true); + if (ysize > 64) + SEGMENT.blurRow(i, ysize - 64, true); + if (ysize > 128) + SEGMENT.blurRow(i, (ysize - 128) << 1, true); + if (ysize > 192) + SEGMENT.blurRow(i, (ysize - 192) << 1, true); + }*/ - PartSys->update(); // update and render - SEGMENT.blur(SEGMENT.custom1, true); - //SEGMENT.blur(250); - ///SEGMENT.blur(64); - //SEGMENT.blur(64); - //SEGMENT.blur(64); - //SEGMENT.blur(64); - // SEGMENT.blur(64); //!!! return FRAMETIME; } static const char _data_FX_MODE_PARTICLEPIT[] PROGMEM = "PS Ballpit@Speed,Intensity,Size,Hardness,Saturation,Cylinder,Walls,Ground;;!;2;pal=11,sx=100,ix=200,c1=120,c2=100,c3=31,o1=0,o2=0,o3=1"; @@ -8567,7 +8627,7 @@ uint16_t mode_particlewaterfall(void) if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. { - if (!initParticleSystem(PartSys)) // init, no additional data needed + if (!initParticleSystem(PartSys, 12)) // init, request 12 sources, no additional data needed return mode_static(); // allocation failed; //allocation failed PartSys->enableGravity(true); // enable with default gforce PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting) @@ -8659,7 +8719,7 @@ uint16_t mode_particlebox(void) if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. { - if (!initParticleSystem(PartSys)) // init + if (!initParticleSystem(PartSys, 1)) // init return mode_static(); // allocation failed; //allocation failed } else @@ -8766,7 +8826,7 @@ uint16_t mode_particleperlin(void) uint32_t i; if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. { - if (!initParticleSystem(PartSys)) // init + if (!initParticleSystem(PartSys, 1)) // init return mode_static(); // allocation failed; //allocation failed PartSys->setKillOutOfBounds(true); //should never happen, but lets make sure there are no stray particles SEGMENT.aux0 = rand(); @@ -8830,7 +8890,7 @@ static const char _data_FX_MODE_PARTICLEPERLIN[] PROGMEM = "PS Fuzzy Noise@Speed * Particle smashing down like meteors and exploding as they hit the ground, has many parameters to play with * by DedeHai (Damian Schneider) */ - +#define NUMBEROFSOURCES 8 uint16_t mode_particleimpact(void) { if (SEGLEN == 1) @@ -8842,13 +8902,13 @@ uint16_t mode_particleimpact(void) if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. { - if (!initParticleSystem(PartSys)) // init, no additional data needed + if (!initParticleSystem(PartSys, NUMBEROFSOURCES)) // init, no additional data needed return mode_static(); // allocation failed; //allocation failed PartSys->setKillOutOfBounds(false); //explosions out of frame ar allowed, set to true to save particles (TODO: better enable it in ESP8266?) PartSys->enableGravity(true); PartSys->setBounceY(true); //always use ground bounce // PartSys->setUsedParticles((PartSys->numParticles * 3) / 2); // use 2/3 of available particles - MaxNumMeteors = min(PartSys->numSources, (uint8_t)8); + MaxNumMeteors = min(PartSys->numSources, (uint8_t)NUMBEROFSOURCES); for (i = 0; i < MaxNumMeteors; i++) { PartSys->sources[i].vx = 0; //emit speed in x @@ -8873,7 +8933,7 @@ uint16_t mode_particleimpact(void) PartSys->setBounceX(SEGMENT.check2); PartSys->setWallHardness(SEGMENT.custom2); PartSys->enableParticleCollisions(SEGMENT.check3, SEGMENT.custom2); // enable collisions and set particle collision hardness - MaxNumMeteors = min(PartSys->numSources, (uint8_t)8); + MaxNumMeteors = min(PartSys->numSources, (uint8_t)NUMBEROFSOURCES); uint8_t numMeteors = map(SEGMENT.custom3, 0, 31, 1, MaxNumMeteors); // number of meteors to use for animation uint32_t emitparticles; // number of particles to emit for each rocket's state @@ -8956,6 +9016,7 @@ uint16_t mode_particleimpact(void) PartSys->update(); // update and render return FRAMETIME; } +#undef NUMBEROFSOURCES static const char _data_FX_MODE_PARTICLEIMPACT[] PROGMEM = "PS Impact@Launches,Explosion Size,Explosion Force,Bounce,Meteors,Cylinder,Walls,Collisions;;!;2;pal=0,sx=32,ix=85,c1=120,c2=125,c3=8,o1=0,o2=0,o3=1"; /* @@ -9086,8 +9147,8 @@ uint16_t mode_particleattractor(void) if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. { uint32_t numParticles = (calculateNumberOfParticles() * 2) / 3; // use 75% of available particles to keep FPS high (also we need an attractor particle) - if (!initParticleSystem(PartSys, numParticles)) // init, need one extra byte per used particle for force counters - return mode_static(); // allocation failed; //allocation failed + if (!initParticleSystem(PartSys, 1, numParticles)) // init, need one source and one extra byte per used particle for force counters + return mode_static(); // allocation failed; //allocation failed PartSys->sources[0].source.hue = random16(); PartSys->sources[0].source.sat = 255; // set full saturation @@ -9194,22 +9255,18 @@ uint16_t mode_particlespray(void) if (SEGMENT.call == 0) // initialization { - if (!initParticleSystem(PartSys)) // init, no additional data needed + if (!initParticleSystem(PartSys, 1)) // init, no additional data needed return mode_static(); // allocation failed; //allocation failed PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting) PartSys->setBounceY(true); PartSys->setMotionBlur(230); // anable motion blur - numSprays = min(PartSys->numSources, (uint8_t)1); // number of sprays - for (i = 0; i < numSprays; i++) - { - PartSys->sources[i].source.hue = random16(); - PartSys->sources[i].source.sat = 255; // set full saturation - PartSys->sources[i].source.x = PartSys->maxX / (numSprays + 1) * (i + 1); // distribute evenly - PartSys->sources[i].maxLife = 300; // lifetime in frames - PartSys->sources[i].minLife = 100; - PartSys->sources[i].source.collide = true; // seeded particles will collide (if enabled) - PartSys->sources[i].var = 7; - } + PartSys->sources[0].source.hue = random16(); + PartSys->sources[0].source.sat = 255; // set full saturation + PartSys->sources[0].maxLife = 300; // lifetime in frames + PartSys->sources[0].minLife = 100; + PartSys->sources[0].source.collide = true; // seeded particles will collide (if enabled) + PartSys->sources[0].var = 7; + } else PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS @@ -9233,26 +9290,16 @@ uint16_t mode_particlespray(void) else PartSys->enableParticleCollisions(false); - uint8_t percycle = numSprays; // maximum number of particles emitted per cycle - // change source properties if (SEGMENT.call % (11 - (SEGMENT.intensity / 25)) == 0) // every nth frame, cycle color and emit particles - { - for (i = 0; i < numSprays; i++) - { - PartSys->sources[i].source.hue++; // = random16(); //change hue of spray source + { + PartSys->sources[0].source.hue++; // = random16(); //change hue of spray source // PartSys->sources[i].var = SEGMENT.custom3; // emiting variation = nozzle size (custom 3 goes from 0-32) - PartSys->sources[i].source.x = map(SEGMENT.custom1, 0, 255, 0, PartSys->maxX); - PartSys->sources[i].source.y = map(SEGMENT.custom2, 0, 255, 0, PartSys->maxY); - } + PartSys->sources[0].source.x = map(SEGMENT.custom1, 0, 255, 0, PartSys->maxX); + PartSys->sources[0].source.y = map(SEGMENT.custom2, 0, 255, 0, PartSys->maxY); - uint8_t j = 0; - for (i = 0; i < percycle; i++) - { // spray[j].source.hue = random16(); //set random color for each particle (using palette) - PartSys->angleEmit(PartSys->sources[j], (256 - (((int32_t)SEGMENT.custom3 + 1) << 3)) << 8, SEGMENT.speed >> 2); - j = (j + 1) % numSprays; - } + PartSys->angleEmit(PartSys->sources[0], (256 - (((int32_t)SEGMENT.custom3 + 1) << 3)) << 8, SEGMENT.speed >> 2); } PartSys->update(); // update and render @@ -9276,7 +9323,7 @@ uint16_t mode_particleGEQ(void) if (SEGMENT.call == 0) // initialization { - if (!initParticleSystem(PartSys)) // init + if (!initParticleSystem(PartSys, 1)) // init return mode_static(); // allocation failed; //allocation failed PartSys->setKillOutOfBounds(true); PartSys->setUsedParticles((PartSys->numParticles * 3) / 2); // use 2/3 of available particles diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index 53faa3c8ac..34bcc08f29 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -56,7 +56,7 @@ ParticleSystem::ParticleSystem(uint16_t width, uint16_t height, uint16_t numbero updatePSpointers(); // set the particle and sources pointer (call this before accessing sprays or particles) setMatrixSize(width, height); setWallHardness(255); // set default wall hardness to max - particlesize = 0; //minimum size + setParticleSize(0); // minimum size motionBlur = 0; //no fading by default emitIndex = 0; /* @@ -164,6 +164,7 @@ void ParticleSystem::setMotionBlur(uint8_t bluramount) void ParticleSystem::setParticleSize(uint8_t size) { particlesize = size; + particleHardRadius = max(PS_P_MINHARDRADIUS, (int)particlesize); } // 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() @@ -271,14 +272,14 @@ void ParticleSystem::particleMoveUpdate(PSparticle &part, PSsettings &options) //if wall collisions are enabled, bounce them before they reach the edge, it looks much nicer if the particle is not half out of vew if (options.bounceX) { - if ((newX < PS_P_RADIUS) || (newX > maxX - PS_P_RADIUS)) // reached a wall + if ((newX < particleHardRadius) || (newX > maxX - particleHardRadius)) // reached a wall { part.vx = -part.vx; // invert speed part.vx = (part.vx * wallHardness) / 255; // reduce speed as energy is lost on non-hard surface - if (newX < PS_P_RADIUS) - newX = PS_P_RADIUS; // fast particles will never reach the edge if position is inverted + if (newX < particleHardRadius) + newX = particleHardRadius; // fast particles will never reach the edge if position is inverted else - newX = maxX - PS_P_RADIUS; + newX = maxX - particleHardRadius; } } @@ -298,13 +299,13 @@ void ParticleSystem::particleMoveUpdate(PSparticle &part, PSsettings &options) if (options.bounceY) { - if ((newY < PS_P_RADIUS) || (newY > maxY - PS_P_RADIUS)) // reached floor / ceiling + if ((newY < particleHardRadius) || (newY > maxY - particleHardRadius)) // reached floor / ceiling { - if (newY < PS_P_RADIUS) // bounce at bottom + if (newY < particleHardRadius) // bounce at bottom { part.vy = -part.vy; // invert speed part.vy = ((int32_t)part.vy * wallHardness) / 255; // reduce speed as energy is lost on non-hard surface - newY = PS_P_RADIUS; + newY = particleHardRadius; } else { @@ -317,7 +318,7 @@ void ParticleSystem::particleMoveUpdate(PSparticle &part, PSsettings &options) { part.vy = -part.vy; // invert speed part.vy = ((int32_t)part.vy * wallHardness) / 255; // reduce speed as energy is lost on non-hard surface - newY = maxY - PS_P_RADIUS; + newY = maxY - particleHardRadius; } } } @@ -730,6 +731,15 @@ void ParticleSystem::ParticleSys_render(bool firemode, uint32_t fireintensity) } free(colorbuffer); // free buffer memory } + + // blur function that works: (for testing only) + SEGMENT.blur(particlesize, true); + if (particlesize > 64) + SEGMENT.blur((particlesize - 64) << 1, true); + if (particlesize > 128) + SEGMENT.blur((particlesize - 128) << 1, true); + if (particlesize > 192) + SEGMENT.blur((particlesize - 192) << 1, true); } // calculate pixel positions and brightness distribution for rendering function @@ -924,7 +934,6 @@ void ParticleSystem::handleCollisions() uint32_t i, j; uint32_t startparticle = 0; uint32_t endparticle = usedParticles >> 1; // do half the particles, significantly speeds things up - // 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) @@ -948,10 +957,10 @@ void ParticleSystem::handleCollisions() 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 < particleHardRadius && dx > -particleHardRadius) // check x direction, if close, check y direction { dy = particles[i].y - particles[j].y; - if (dy < PS_P_HARDRADIUS && dy > -PS_P_HARDRADIUS) // particles are close + if (dy < particleHardRadius && dy > -particleHardRadius) // particles are close collideParticles(&particles[i], &particles[j]); } } @@ -980,18 +989,14 @@ void ParticleSystem::collideParticles(PSparticle *particle1, PSparticle *particl dx = -1; if (relativeVx < 0) // if true, particle2 is on the right side dx = 1; - else if(relativeVx == 0) //if true - { + else if(relativeVx == 0) relativeVx = 1; - } - + dy = -1; if (relativeVy < 0) dy = 1; else if (relativeVy == 0) - { relativeVy = 1; - } distanceSquared = 2; //1 + 1 } @@ -1061,8 +1066,8 @@ void ParticleSystem::collideParticles(PSparticle *particle1, PSparticle *particl - // const int32_t HARDDIAMETER = 2 * PS_P_HARDRADIUS; // push beyond the hard radius, helps with keeping stuff fluffed up -> not really - // int32_t push = (2 * PS_P_HARDRADIUS * PS_P_HARDRADIUS - distanceSquared) >> 6; // push a small amount, if pushing too much, it becomse chaotic as waves of pushing run through piles + // const int32_t HARDDIAMETER = 2 * particleHardRadius; // push beyond the hard radius, helps with keeping stuff fluffed up -> not really + // int32_t push = (2 * particleHardRadius * particleHardRadius - distanceSquared) >> 6; // push a small amount, if pushing too much, it becomse chaotic as waves of pushing run through piles int32_t pushamount = 1 + ((250 + dotProduct) >> 6); // the closer dotproduct is to zero, the closer the particles are int32_t push; @@ -1284,19 +1289,19 @@ uint32_t calculateNumberOfParticles() return numberofParticles; } -uint32_t calculateNumberOfSources() +uint32_t calculateNumberOfSources(uint8_t requestedsources) { uint32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; uint32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); #ifdef ESP8266 - int numberofSources = (cols * rows) / 8; + int numberofSources = min((cols * rows) / 8, (uint32_t)requestedsources); numberofSources = max(1, min(numberofSources, ESP8266_MAXSOURCES)); // limit to 1 - 16 #elif ARDUINO_ARCH_ESP32S2 - int numberofSources = (cols * rows) / 6; + int numberofSources = min((cols * rows) / 6, (uint32_t)requestedsources); numberofSources = max(1, min(numberofSources, ESP32S2_MAXSOURCES)); // limit to 1 - 48 #else - int numberofSources = (cols * rows) / 4; - numberofSources = max(1, min(numberofSources, ESP32_MAXSOURCES)); // limit to 1 - 72 + int numberofSources = min((cols * rows) / 4, (uint32_t)requestedsources); + numberofSources = max(1, min(numberofSources, ESP32_MAXSOURCES)); // limit to 1 - 64 #endif return numberofSources; } @@ -1317,11 +1322,11 @@ bool allocateParticleSystemMemory(uint16_t numparticles, uint16_t numsources, ui } // initialize Particle System, allocate additional bytes if needed (pointer to those bytes can be read from particle system class: PSdataEnd) -bool initParticleSystem(ParticleSystem *&PartSys, uint16_t additionalbytes) +bool initParticleSystem(ParticleSystem *&PartSys, uint8_t requestedsources, uint16_t additionalbytes) { //Serial.println("PS init function"); uint32_t numparticles = calculateNumberOfParticles(); - uint32_t numsources = calculateNumberOfSources(); + uint32_t numsources = calculateNumberOfSources(requestedsources); if (!allocateParticleSystemMemory(numparticles, numsources, additionalbytes)) { DEBUG_PRINT(F("PS init failed: memory depleted")); diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index be903269be..a771b84989 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -42,11 +42,11 @@ #define PS_P_HALFRADIUS 32 #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_MINHARDRADIUS 80 // minimum hard surface radius #define PS_P_MINSURFACEHARDNESS 128 //minimum hardness used in collision impulse calculation, below this hardness, particles become sticky #define PS_P_MAXSPEED 120 //maximum speed a particle can have (vx/vy is int8) -//struct for a single particle +//struct for a single particle (10 bytes) typedef struct { int16_t x; //x position in particle system int16_t y; //y position in particle system @@ -62,7 +62,7 @@ typedef struct { bool flag4 : 1; } PSparticle; -//struct for a particle source +//struct for a particle source (17 bytes) typedef struct { uint16_t minLife; //minimum ttl of emittet particles uint16_t maxLife; //maximum ttl of emitted particles @@ -137,7 +137,7 @@ class ParticleSystem PSsource *sources; // pointer to 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 uint16_t maxX, maxY; //particle system size i.e. width-1 / height-1 in subpixels - uint32_t maxXpixel, maxYpixel; // last physical pixel that can be drawn to (FX can read this to read segment size if required) + uint32_t maxXpixel, maxYpixel; // last physical pixel that can be drawn to (FX can read this to read segment size if required), equal to width-1 / height-1 uint8_t numSources; //number of sources uint16_t numParticles; // number of particles available in this system uint16_t usedParticles; // number of particles used in animation (can be smaller then numParticles) @@ -168,11 +168,12 @@ class ParticleSystem int8_t gforce; //gravity strength, default is 8 (negative is allowed) uint8_t collisioncounter; //counter to handle collisions uint8_t particlesize; + int32_t particleHardRadius; // hard surface radius of a particle, used for collision detection uint8_t motionBlur; }; //initialization functions (not part of class) -bool initParticleSystem(ParticleSystem *&PartSys, uint16_t additionalbytes = 0); +bool initParticleSystem(ParticleSystem *&PartSys, uint8_t requestedsources, uint16_t additionalbytes = 0); uint32_t calculateNumberOfParticles(); uint32_t calculateNumberOfSources(); bool allocateParticleSystemMemory(uint16_t numparticles, uint16_t numsources, uint16_t additionalbytes); From e01781407734db9c12332b8ee4c37c3977660298 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sat, 20 Apr 2024 15:34:16 +0200 Subject: [PATCH 060/219] added more tests, non compiling at the moment --- wled00/FX.cpp | 150 ++++++++++++++++++++++-------------- wled00/FXparticleSystem.cpp | 8 +- wled00/FXparticleSystem.h | 22 +++++- 3 files changed, 120 insertions(+), 60 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index f99e3b9bac..b35694b9c0 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -8339,12 +8339,12 @@ uint16_t mode_particlefire(void) // DEBUG_PRINTF_P(PSTR("segment data ptr in Fire FX %p\n"), SEGMENT.data); PartSys->updateSystem(); // update system properties (dimensions and data pointers) - - if (SEGMENT.check1) //slow i.e. low fps is enabled TODO: test if this works, adjust if needed + uint32_t firespeed = max((uint8_t)100, SEGMENT.speed); //limit speed to 100 minimum, reduce frame rate to make it slower (slower speeds than 100 do not look nice) + if (SEGMENT.speed < 100) //slow, limit FPS { uint32_t *lastcall = reinterpret_cast(PartSys->PSdataEnd); uint32_t period = strip.now - *lastcall; - if (period < 30) // limit to approximately 30FPS + if (period < map(SEGMENT.speed, 0, 99, 100, 20)) // limit to 50FPS - 10FPS { return FRAMETIME; //do not update this frame } @@ -8374,14 +8374,14 @@ uint16_t mode_particlefire(void) PartSys->sources[i].source.y = -PS_P_RADIUS; // set the source below the frame //PartSys->sources[i].source.ttl = 10 + random16((SEGMENT.custom1 * SEGMENT.custom1) >> 8) / (1 + (SEGMENT.speed >> 5)); //old not really good, too intense - PartSys->sources[i].source.ttl = 5 + random16((SEGMENT.custom1 * SEGMENT.custom1) >> 7) / (2 + (SEGMENT.speed >> 4)); //'hotness' of fire, faster flames reduce the effect or flame height will scale too much with speed -> new, this works! - //PartSys->sources[i].source.ttl = 5 + random16(SEGMENT.custom1) / (1 + (SEGMENT.speed >> 5)); // this is experimental, fine tuning all parameters + PartSys->sources[i].source.ttl = 5 + random16((SEGMENT.custom1 * SEGMENT.custom1) >> 7) / (2 + (firespeed >> 4)); //'hotness' of fire, faster flames reduce the effect or flame height will scale too much with speed -> new, this works! + //PartSys->sources[i].source.ttl = 5 + random16(SEGMENT.custom1) / (1 + (firespeed >> 5)); // this is experimental, fine tuning all parameters PartSys->sources[i].maxLife = random16(7) + 13; // defines flame height together with the vy speed, vy speed*maxlife/PS_P_RADIUS is the average flame height PartSys->sources[i].minLife = 4; PartSys->sources[i].vx = (int8_t)random16(4) - 2; // emitting speed (sideways) - PartSys->sources[i].vy = 5 + (SEGMENT.speed >> 2); // emitting speed (upwards) -> this is good + PartSys->sources[i].vy = 5 + (firespeed >> 2); // emitting speed (upwards) -> this is good //PartSys->sources[i].var = (random16(5) + 3) | 0x01; // speed variation around vx,vy (+/- var/2), only use odd numbers - PartSys->sources[i].var = (random16(2 + (SEGMENT.speed >> 5)) + 3) | 0x01; // speed variation around vx,vy (+/- var/2), only use odd numbers + PartSys->sources[i].var = (random16(2 + (firespeed >> 5)) + 3) | 0x01; // speed variation around vx,vy (+/- var/2), only use odd numbers } } @@ -8401,7 +8401,7 @@ uint16_t mode_particlefire(void) //this is a work in progress... hard to find settings that look good TODO: looks ok on speed 130, need to tune it for other speeds if (SEGMENT.check3) { - if (SEGMENT.call % map(SEGMENT.speed,0,255,4,15)==0) // update noise position every xth frames, also add wind -> has do be according to speed. 135-> every third frame + if (SEGMENT.call % map(firespeed,0,255,4,15)==0) // update noise position every xth frames, also add wind -> has do be according to speed. 135-> every third frame { for (i = 0; i < PartSys->usedParticles; i++) { @@ -8419,8 +8419,8 @@ uint16_t mode_particlefire(void) //PartSys->particles[i].vy += curl>>3; //PartSys->particles[i].vx += (curl * ((SEGMENT.custom2 * modulation)>>7)) >> 9; //PartSys->particles[i].vy += ((curl ) * ((SEGMENT.custom2 * modulation)>>7))>>10; - //PartSys->particles[i].vx += (curl * curl * (SEGMENT.speed+10)) >> 14; //this may be a bad idea -> yes, squre is always positive... and a bit strong - PartSys->particles[i].vx += (curl * (SEGMENT.speed + 10)) >> 9; //-> this is not too bad! + //PartSys->particles[i].vx += (curl * curl * (firespeed+10)) >> 14; //this may be a bad idea -> yes, squre is always positive... and a bit strong + PartSys->particles[i].vx += (curl * (firespeed + 10)) >> 9; //-> this is not too bad! // PartSys->particles[i].vy += (curl * SEGMENT.custom2 ) >> 13; } } @@ -8443,7 +8443,7 @@ uint16_t mode_particlefire(void) if (PartSys->particles[i].ttl == 0) // find a dead particle { // emit particle at random position over the top of the matrix (random16 is not random enough) - PartSys->particles[i].vy = 1 ;//+ (SEGMENT.speed >> 3); + PartSys->particles[i].vy = 1 ;//+ (firespeed >> 3); PartSys->particles[i].ttl = 10;//(PS_P_RADIUS<<2) / PartSys->particles[i].vy; PartSys->particles[i].x = (PartSys->maxX >> 1) - (spread>>1) + (rand() % spread) ; PartSys->particles[i].y = 0; @@ -8537,8 +8537,8 @@ uint16_t mode_particlepit(void) // blur function that works: (for testing only) static uint8_t testcntr; - static uint8_t wobbleamount = 255; - wobbleamount -= 3; + static uint8_t wobbleamount = 200; + wobbleamount -= 2; testcntr+=15; @@ -8546,7 +8546,7 @@ uint16_t mode_particlepit(void) // int32_t xsize = 255-ysize; int32_t ysize = (int32_t)sin8(testcntr)-128; - int32_t xsize = -ysize; + int32_t xsize = -ysize; //TODO: xsize is not really needed, calculation can be simplified using just ysize //ysize = (((int16_t)SEGMENT.custom1 * ysize) >> 8); //xsize = (((int16_t)SEGMENT.custom1 * xsize) >> 8); @@ -8560,56 +8560,94 @@ uint16_t mode_particlepit(void) Serial.print(xsize); Serial.print(" "); - Serial.println(ysize); + Serial.print(ysize); - // PartSys->setParticleSize(SEGMENT.custom1); + //PartSys->setParticleSize(SEGMENT.custom1); PartSys->update(); // update and render -/* + const unsigned cols = PartSys->maxXpixel + 1; const unsigned rows = PartSys->maxYpixel + 1; - for (unsigned i = 0; i < cols; i++) + uint8_t xiterations = 1 + (xsize>>8); + uint8_t yiterations = 1 + (ysize>>8); + uint8_t secondpassxsize = xsize - 255; + uint8_t secondpassysize = ysize - 255; + if (xsize > 255) + xsize = 255; //first pass, full sized + if (ysize > 255) + ysize = 255; + + Serial.print(xsize); + Serial.print(" "); + Serial.println(ysize); + for (uint32_t j = 0; j < xiterations; j++) { - SEGMENT.blurCol(i, xsize, true); - if (xsize > 64) - SEGMENT.blurCol(i, xsize - 64, true); - if (xsize > 128) - SEGMENT.blurCol(i, (xsize - 128) << 1, true); - if (xsize > 192) - SEGMENT.blurCol(i, (xsize - 192) << 1, true); - } - for (unsigned i = 0; i < rows; i++){ - SEGMENT.blurRow(i, ysize, true); - if (ysize > 64) - SEGMENT.blurRow(i, ysize - 64, true); - if (ysize > 128) - SEGMENT.blurRow(i, (ysize - 128) << 1, true); - if (ysize > 192) - SEGMENT.blurRow(i, (ysize - 192) << 1, true); - } - - for (unsigned i = 0; i < cols; i++) + for (uint32_t i = 0; i < cols; i++) + { + SEGMENT.blurCol(i, xsize, true); + if (xsize > 64) + SEGMENT.blurCol(i, xsize - 64, true); + if (xsize > 128) + SEGMENT.blurCol(i, (xsize - 128) << 1, true); + if (xsize > 192) + SEGMENT.blurCol(i, (xsize - 192) << 1, true); + } + //set size for second pass: + xsize = secondpassxsize; + } + for (uint32_t j = 0; j < yiterations; j++) { - SEGMENT.blurCol(i, xsize, true); - if (xsize > 64) - SEGMENT.blurCol(i, xsize - 64, true); - if (xsize > 128) - SEGMENT.blurCol(i, (xsize - 128) << 1, true); - if (xsize > 192) - SEGMENT.blurCol(i, (xsize - 192) << 1, true); - } - for (unsigned i = 0; i < rows; i++) + for (unsigned i = 0; i < rows; i++) + { + SEGMENT.blurRow(i, ysize, true); + if (ysize > 64) + SEGMENT.blurRow(i, ysize - 64, true); + if (ysize > 128) + SEGMENT.blurRow(i, (ysize - 128) << 1, true); + if (ysize > 192) + SEGMENT.blurRow(i, (ysize - 192) << 1, true); + } + // set size for second pass: + ysize = secondpassysize; + } +/* +//rotat image (just a test, non working yet) + float angle = PI/3; + // Calculate sine and cosine of the angle + float cosTheta = cos(angle); + float sinTheta = sin(angle); + + // Center of rotation + int centerX = cols / 2; + int centerY = rows / 2; + + // Iterate over each pixel in the output image + for (int y = 0; y < rows; y++) { - SEGMENT.blurRow(i, ysize, true); - if (ysize > 64) - SEGMENT.blurRow(i, ysize - 64, true); - if (ysize > 128) - SEGMENT.blurRow(i, (ysize - 128) << 1, true); - if (ysize > 192) - SEGMENT.blurRow(i, (ysize - 192) << 1, true); - }*/ + for (int x = 0; x < cols; x++) + { + int relX = x - centerX; + int relY = y - centerY; - return FRAMETIME; - } + // Apply rotation using axis symmetry + int origX = round(relX * cosTheta - relY * sinTheta) + centerX; + int origY = round(relX * sinTheta + relY * cosTheta) + centerY; + + // Check if original coordinates are within bounds + if (origX >= 0 && origX < rows && origY >= 0 && origY < cols) + { + // Copy pixel value from original image to rotated image + SEGMENT.setPixelColorXY(x, y, SEGMENT.getPixelColorXY(origX, origY)); + } + + // Copy pixel values from original image to rotated image + rotatedImage[origY][origX] = image[y][x]; + rotatedImage[origY][cols - 1 - origX] = image[y][cols - 1 - x]; + rotatedImage[rows - 1 - origY][origX] = image[rows - 1 - y][x]; + rotatedImage[rows - 1 - origY][cols - 1 - origX] = image[rows - 1 - y][cols - 1 - x]; + } + }*/ + return FRAMETIME; + } static const char _data_FX_MODE_PARTICLEPIT[] PROGMEM = "PS Ballpit@Speed,Intensity,Size,Hardness,Saturation,Cylinder,Walls,Ground;;!;2;pal=11,sx=100,ix=200,c1=120,c2=100,c3=31,o1=0,o2=0,o3=1"; /* diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index 34bcc08f29..bf75e956da 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -56,6 +56,7 @@ ParticleSystem::ParticleSystem(uint16_t width, uint16_t height, uint16_t numbero updatePSpointers(); // set the particle and sources pointer (call this before accessing sprays or particles) setMatrixSize(width, height); setWallHardness(255); // set default wall hardness to max + setSaturation(255); //full saturation by default setParticleSize(0); // minimum size motionBlur = 0; //no fading by default emitIndex = 0; @@ -150,6 +151,11 @@ void ParticleSystem::setKillOutOfBounds(bool enable) particlesettings.killoutofbounds = enable; } +void ParticleSystem::setSaturation(uint8_t sat) +{ + saturation = sat; +} + void ParticleSystem::setColorByAge(bool enable) { particlesettings.colorByAge = enable; @@ -735,7 +741,7 @@ void ParticleSystem::ParticleSys_render(bool firemode, uint32_t fireintensity) // blur function that works: (for testing only) SEGMENT.blur(particlesize, true); if (particlesize > 64) - SEGMENT.blur((particlesize - 64) << 1, true); + SEGMENT.blur(particlesize - 64, true); if (particlesize > 128) SEGMENT.blur((particlesize - 128) << 1, true); if (particlesize > 192) diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index a771b84989..1db5172b57 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -53,7 +53,6 @@ typedef struct { int8_t vx; //horizontal velocity int8_t vy; //vertical velocity uint8_t hue; // color hue - uint8_t sat; // color saturation //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 @@ -62,6 +61,20 @@ typedef struct { bool flag4 : 1; } PSparticle; +// struct for additional particle settings (optional) +typedef struct +{ + uint8_t sat; //particle color saturation + uint8_t size; //particle size, 255 means 10 pixels in diameter + uint8_t sizeasymmetry; //asymmetrical size TODO: need something better to define this? + uint8_t forcecounter; //counter for applying forces to individual particles + + bool flag1 : 1; // unused flags... for now. + bool flag2 : 1; + bool flag3 : 1; + bool flag4 : 1; +} PSadvancedparticle; + //struct for a particle source (17 bytes) typedef struct { uint16_t minLife; //minimum ttl of emittet particles @@ -70,7 +83,7 @@ typedef struct { uint8_t var; //variation of emitted speed int8_t vx; //emitting speed int8_t vy; //emitting speed -} PSsource; +} PSsource; //TODO: sources also need parameter for advanced particles, so size, saturation, ... // struct for PS settings typedef struct @@ -127,6 +140,7 @@ class ParticleSystem void setBounceX(bool enable); void setBounceY(bool enable); void setKillOutOfBounds(bool enable); //if enabled, particles outside of matrix instantly die + void setSaturation(uint8_t sat); //set global color saturation void setColorByAge(bool enable); void setMotionBlur(uint8_t bluramount); void setParticleSize(uint8_t size); @@ -166,7 +180,9 @@ class ParticleSystem int32_t wallHardness; uint8_t gforcecounter; //counter for global gravity int8_t gforce; //gravity strength, default is 8 (negative is allowed) - uint8_t collisioncounter; //counter to handle collisions + uint8_t collisioncounter; //counter to handle collisions TODO: could use the SEGMENT.call? + //global particle properties for basic particles + uint8_t saturation; uint8_t particlesize; int32_t particleHardRadius; // hard surface radius of a particle, used for collision detection uint8_t motionBlur; From 7b68946c023f64dc1dd0da996dfa573b2e303864 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Mon, 22 Apr 2024 18:52:13 +0200 Subject: [PATCH 061/219] in the middle of fixing FX to use new PS functions --- wled00/FX.cpp | 167 +++++++++++------------ wled00/FXparticleSystem.cpp | 261 +++++++++++++++++++++--------------- wled00/FXparticleSystem.h | 98 ++++++++------ 3 files changed, 296 insertions(+), 230 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index b35694b9c0..ebab12c20b 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -7915,8 +7915,7 @@ uint16_t mode_particlevortex(void) uint8_t numSprays = min(PartSys->numSources, (uint8_t)NUMBEROFSOURCES); for (i = 0; i < numSprays; i++) - { - PartSys->sources[i].source.sat = 255; // set saturation + { PartSys->sources[i].source.x = (PartSys->maxX + 1) >> 1; // center PartSys->sources[i].source.y = (PartSys->maxY + 1) >> 1; // center PartSys->sources[i].source.vx = 0; @@ -8089,7 +8088,7 @@ uint16_t mode_particlefireworks(void) PartSys->setWrapX(SEGMENT.check1); PartSys->setBounceY(SEGMENT.check2); //PartSys->setWallHardness(SEGMENT.custom2); //not used anymore, can be removed - PartSys->enableGravity(true, map(SEGMENT.custom3,0,31,0,10)); // todo: make it a slider to adjust + PartSys->setGravity(map(SEGMENT.custom3,0,31,0,10)); // todo: make it a slider to adjust // check each rocket's state and emit particles according to its state: moving up = emit exhaust, at top = explode; falling down = standby time uint32_t emitparticles; // number of particles to emit for each rocket's state @@ -8154,7 +8153,7 @@ uint16_t mode_particlefireworks(void) speed += 5; //increase speed to form a second circle speedvariation = speedvariation ? speedvariation + random16(4) : 0; // double speed variation PartSys->sources[j].source.hue = random16(); // new color for next circle - PartSys->sources[j].source.sat = min((uint16_t)150,random16()); + PartSys->sources[j].sat = min((uint16_t)150,random16()); } angle += angleincrement; // set angle for next particle } @@ -8184,7 +8183,7 @@ uint16_t mode_particlefireworks(void) { PartSys->sources[j].source.vy = 0; // set speed to zero so code above will recognize this as an exploding rocket PartSys->sources[j].source.hue = random16(); // random color - PartSys->sources[j].source.sat = random16(55) + 200; + PartSys->sources[j].sat = random16(55) + 200; PartSys->sources[j].maxLife = 200; PartSys->sources[j].minLife = 100; PartSys->sources[j].source.ttl = random16((1200 - ((uint32_t)SEGMENT.speed << 2))) + 550 - (SEGMENT.speed << 1); // standby time til next launch @@ -8197,7 +8196,7 @@ uint16_t mode_particlefireworks(void) PartSys->sources[j].source.x = (rand() % (PartSys->maxX >> 1)) + (PartSys->maxX >> 2); // centered half PartSys->sources[j].source.vy = random16(SEGMENT.custom1 >> 3) + 5; // rocket speed depends also on rocket fuse PartSys->sources[j].source.vx = random16(5) - 2; //i.e. not perfectly straight up - PartSys->sources[j].source.sat = 30; // low saturation -> exhaust is off-white + PartSys->sources[j].sat = 30; // low saturation -> exhaust is off-white PartSys->sources[j].source.ttl = random16(SEGMENT.custom1) + (SEGMENT.custom1 >> 1); // sets explosion height (rockets explode at the top if set too high as paticle update set speed to zero if moving out of matrix) PartSys->sources[j].maxLife = 30; // exhaust particle life PartSys->sources[j].minLife = 10; @@ -8232,14 +8231,13 @@ uint16_t mode_particlevolcano(void) { if (!initParticleSystem(PartSys, NUMBEROFSOURCES)) // init, no additional data needed return mode_static(); // allocation failed; //allocation failed - PartSys->setBounceY(true); - PartSys->enableGravity(true); //enable with default gforce + PartSys->setBounceY(true); + PartSys->setGravity(); // enable with default gforce PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting) numSprays = min(PartSys->numSources, (uint8_t)NUMBEROFSOURCES); // number of sprays for (i = 0; i < numSprays; i++) { - PartSys->sources[i].source.hue = random16(); - PartSys->sources[i].source.sat = 255; // set full saturation + PartSys->sources[i].source.hue = random16(); PartSys->sources[i].source.x = PartSys->maxX / (numSprays + 1) * (i + 1); //distribute evenly PartSys->sources[i].maxLife = 300; // lifetime in frames PartSys->sources[i].minLife = 250; @@ -8312,7 +8310,7 @@ uint16_t mode_particlefire(void) if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. { - if (!initParticleSystem(PartSys, 255, 4)) //maximum number of source (PS will determine the exact number based on segment size) and need 4 additional bytes for time keeping (uint32_t lastcall) + if (!initParticleSystem(PartSys, 25, false, 4)) //maximum number of source (PS will determine the exact number based on segment size) and need 4 additional bytes for time keeping (uint32_t lastcall) return mode_static(); // allocation failed; //allocation failed // Serial.println("fireinit done"); SEGMENT.aux0 = rand(); // aux0 is wind position (index) in the perlin noise @@ -8337,15 +8335,18 @@ uint16_t mode_particlefire(void) return mode_static(); // something went wrong, no data! } - // DEBUG_PRINTF_P(PSTR("segment data ptr in Fire FX %p\n"), SEGMENT.data); PartSys->updateSystem(); // update system properties (dimensions and data pointers) - uint32_t firespeed = max((uint8_t)100, SEGMENT.speed); //limit speed to 100 minimum, reduce frame rate to make it slower (slower speeds than 100 do not look nice) + PartSys->setWrapX(SEGMENT.check2); + PartSys->setMotionBlur(SEGMENT.check1 * 120); // anable/disable motion blur + + uint32_t firespeed = max((uint8_t)100, SEGMENT.speed); //limit speed to 100 minimum, reduce frame rate to make it slower (slower speeds than 100 do not look nice) if (SEGMENT.speed < 100) //slow, limit FPS { uint32_t *lastcall = reinterpret_cast(PartSys->PSdataEnd); uint32_t period = strip.now - *lastcall; - if (period < map(SEGMENT.speed, 0, 99, 100, 20)) // limit to 50FPS - 10FPS + if (period < map(SEGMENT.speed, 0, 99, 100, 12)) // limit to 80FPS - 10FPS { + SEGMENT.call--; //skipping a frame, decrement the counter (on call0, this is never executed as lastcall is 0, so its fine to not check if >0) return FRAMETIME; //do not update this frame } *lastcall = strip.now; @@ -8355,7 +8356,6 @@ uint16_t mode_particlefire(void) numFlames = min((uint32_t)PartSys->numSources, (1 + ((spread / PS_P_RADIUS) << 1))); // number of flames used depends on spread with, good value is (fire width in pixel) * 2 uint32_t percycle = numFlames*2/3;// / 2; // maximum number of particles emitted per cycle (TODO: for ESP826 maybe use flames/2) // percycle = map(SEGMENT.intensity,0,255, 2, (numFlames*3) / 2); //TODO: will this give better flames or worse? - PartSys->setWrapX(SEGMENT.check2); // update the flame sprays: for (i = 0; i < numFlames; i++) @@ -8394,7 +8394,7 @@ uint16_t mode_particlefire(void) // add wind force to all particles int8_t windspeed = ((int16_t)(inoise8(SEGMENT.aux0, SEGMENT.aux1) - 127) * SEGMENT.custom2) >> 12; - PartSys->applyForce(PartSys->particles, PartSys->usedParticles, windspeed, 0); + PartSys->applyForce(windspeed, 0); } SEGMENT.step++; @@ -8461,7 +8461,7 @@ uint16_t mode_particlefire(void) return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEFIRE[] PROGMEM = "PS Fire@Speed,Intensity,Base Heat,Wind,Spread,Slow,Cylinder,Turbulence;;!;2;pal=35,sx=130,ix=120,c1=110,c2=128,c3=22,o1=0"; +static const char _data_FX_MODE_PARTICLEFIRE[] PROGMEM = "PS Fire@Speed,Intensity,Base Heat,Wind,Spread,Blur,Cylinder,Turbulence;;!;2;pal=35,sx=130,ix=120,c1=110,c2=128,c3=22,o1=0"; /* PS Ballpit: particles falling down, user can enable these three options: X-wraparound, side bounce, ground bounce @@ -8478,10 +8478,10 @@ uint16_t mode_particlepit(void) if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. { - if (!initParticleSystem(PartSys, 1)) //init, request one source (actually dont really need one TODO: test if using zero sources also works) + if (!initParticleSystem(PartSys, 1, true)) //init, request one source (actually dont really need one TODO: test if using zero sources also works) return mode_static(); // allocation failed; //allocation failed PartSys->setKillOutOfBounds(true); - PartSys->enableGravity(true); + PartSys->setGravity(); //enable with default gravity PartSys->setUsedParticles((PartSys->numParticles*3)/2); //use 2/3 of available particles } else @@ -8505,42 +8505,43 @@ uint16_t mode_particlepit(void) PartSys->enableParticleCollisions(false); } - uint32_t i; // index variable + PartSys->setSaturation(((SEGMENT.custom3) << 3) + 7); // set global rendering saturation so individual saturation rendering is enabled - if (SEGMENT.call % (64 - (SEGMENT.intensity >> 2)) == 0 && SEGMENT.intensity > 1) // every nth frame emit particles, stop emitting if set to zero + uint32_t i; // index variable + if (SEGMENT.call % (64 - (SEGMENT.intensity >> 2)) == 0 && SEGMENT.intensity > 1) // every nth frame emit particles, stop emitting if set to zero + { + for (i = 0; i < PartSys->usedParticles; i++) // emit particles { - for (i = 0; i < PartSys->usedParticles; i++) // emit particles + if (PartSys->particles[i].ttl == 0) // find a dead particle { - if (PartSys->particles[i].ttl == 0) // find a dead particle - { - // emit particle at random position over the top of the matrix (random16 is not random enough) - PartSys->particles[i].ttl = 1500 - (SEGMENT.speed << 2) + random16(500); // if speed is higher, make them die sooner - PartSys->particles[i].x = random(PartSys->maxX >> 1) + (PartSys->maxX >> 2); - PartSys->particles[i].y = (PartSys->maxY<<1); // particles appear somewhere above the matrix, maximum is double the height - PartSys->particles[i].vx = (int16_t)random(100) - 50; // side speed is +/- - PartSys->particles[i].vy = map(SEGMENT.speed, 0, 255, -5, -100); // downward speed - PartSys->particles[i].hue = random16(); // set random color - PartSys->particles[i].sat = ((SEGMENT.custom3) << 3) + 7; // set saturation - PartSys->particles[i].collide = true; //enable collision for particle - break; // emit only one particle per round - } + // emit particle at random position over the top of the matrix (random16 is not random enough) + PartSys->particles[i].ttl = 1500 - (SEGMENT.speed << 2) + random16(500); // if speed is higher, make them die sooner + PartSys->particles[i].x = random(PartSys->maxX >> 1) + (PartSys->maxX >> 2); + PartSys->particles[i].y = (PartSys->maxY<<1); // particles appear somewhere above the matrix, maximum is double the height + PartSys->particles[i].vx = (int16_t)random(100) - 50; // side speed is +/- + PartSys->particles[i].vy = map(SEGMENT.speed, 0, 255, -5, -100); // downward speed + PartSys->particles[i].hue = random16(); // set random color + PartSys->particles[i].collide = true; //enable collision for particle + PartSys->advPartProps[i].sat = ((SEGMENT.custom3) << 3) + 7; + break; // emit only one particle per round } } + } - uint32_t frictioncoefficient = 1; - if (SEGMENT.speed < 50) // for low speeds, apply more friction - frictioncoefficient = 50 - SEGMENT.speed; + uint32_t frictioncoefficient = 1; + if (SEGMENT.speed < 50) // for low speeds, apply more friction + frictioncoefficient = 50 - SEGMENT.speed; - if (SEGMENT.call % (3 + (SEGMENT.custom2 >> 2)) == 0) - PartSys->applyFriction(frictioncoefficient); + if (SEGMENT.call % (3 + (SEGMENT.custom2 >> 2)) == 0) + PartSys->applyFriction(frictioncoefficient); - // blur function that works: (for testing only) - static uint8_t testcntr; - static uint8_t wobbleamount = 200; - wobbleamount -= 2; + // blur function that works: (for testing only) + static uint8_t testcntr; + static uint8_t wobbleamount = 200; + wobbleamount -= 2; - testcntr+=15; + testcntr+=15; // int32_t ysize = (int16_t)sin8(testcntr); // int32_t xsize = 255-ysize; @@ -8667,14 +8668,13 @@ uint16_t mode_particlewaterfall(void) { if (!initParticleSystem(PartSys, 12)) // init, request 12 sources, no additional data needed return mode_static(); // allocation failed; //allocation failed - PartSys->enableGravity(true); // enable with default gforce + PartSys->setGravity(); // enable with default gforce PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting) PartSys->setMotionBlur(190); //anable motion blur numSprays = min((uint32_t)PartSys->numSources, min(PartSys->maxXpixel/5, (uint32_t)2)); // number of sprays for (i = 0; i < numSprays; i++) { PartSys->sources[i].source.hue = random16(); - PartSys->sources[i].source.sat = 255; // set full saturation PartSys->sources[i].source.vx = 0; PartSys->sources[i].source.collide = true; // seeded particles will collide #ifdef ESP8266 @@ -8793,7 +8793,6 @@ uint16_t mode_particlebox(void) { PartSys->particles[i].ttl = 500; //set all particles alive (not all are rendered though) PartSys->particles[i].hue = i * 5; // color range - PartSys->particles[i].sat = 255; // set full saturation (lets palette choose the color) PartSys->particles[i].x = map(i, 0, maxnumParticles, 1, PartSys->maxX); // distribute along x according to color PartSys->particles[i].y = random16(PartSys->maxY); // randomly in y direction PartSys->particles[i].collide = true; // all particles collide @@ -8830,7 +8829,7 @@ uint16_t mode_particlebox(void) ygravity = -ygravity; } - PartSys->applyForce(PartSys->particles, PartSys->usedParticles, xgravity, ygravity); + PartSys->applyForce(xgravity, ygravity); // reset particle TTL so they never die for (i = 0; i < PartSys->usedParticles; i++) @@ -8864,13 +8863,12 @@ uint16_t mode_particleperlin(void) uint32_t i; if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. { - if (!initParticleSystem(PartSys, 1)) // init + if (!initParticleSystem(PartSys, 1, true)) // init with 1 source and advanced properties return mode_static(); // allocation failed; //allocation failed PartSys->setKillOutOfBounds(true); //should never happen, but lets make sure there are no stray particles SEGMENT.aux0 = rand(); for (i = 0; i < PartSys->numParticles; i++) { - PartSys->particles[i].sat = 255; // full saturation, color set by palette PartSys->particles[i].collide = true; // all particles colllide } } @@ -8912,7 +8910,7 @@ uint16_t mode_particleperlin(void) { int8_t xslope = (baseheight - (int16_t)inoise8(xnoise + 10, ynoise, SEGMENT.aux0)); int8_t yslope = (baseheight - (int16_t)inoise8(xnoise, ynoise + 10, SEGMENT.aux0)); - PartSys->applyForce(&(PartSys->particles[i]), 1, xslope, yslope); + PartSys->applyForce(i, xslope, yslope); } } @@ -8937,13 +8935,16 @@ uint16_t mode_particleimpact(void) uint32_t i = 0; uint8_t MaxNumMeteors; PSsettings meteorsettings = {0, 0, 0, 1, 0, 1, 0, 0}; // PS settings for meteors: bounceY and gravity enabled (todo: if ESP8266 is ok with out of bounds particles, this can be removed, it just takes care of the kill out of bounds setting) + //PSsettings meteorsettings; + //uint8_t *settingsPtr = reinterpret_cast(&meteorsettings); // access settings as one byte (wmore efficient in code and speed) + //*settingsPtr = 0b00101000; // PS settings for meteors: bounceY and gravity enabled 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, NUMBEROFSOURCES)) // init, no additional data needed return mode_static(); // allocation failed; //allocation failed PartSys->setKillOutOfBounds(false); //explosions out of frame ar allowed, set to true to save particles (TODO: better enable it in ESP8266?) - PartSys->enableGravity(true); + PartSys->setGravity(); //enable default gravity PartSys->setBounceY(true); //always use ground bounce // PartSys->setUsedParticles((PartSys->numParticles * 3) / 2); // use 2/3 of available particles MaxNumMeteors = min(PartSys->numSources, (uint8_t)NUMBEROFSOURCES); @@ -8953,7 +8954,6 @@ uint16_t mode_particleimpact(void) PartSys->sources[i].source.y = 10; PartSys->sources[i].source.ttl = random16(20 * i); // set initial delay for meteors PartSys->sources[i].source.vy = 10; // at positive speeds, no particles are emitted and if particle dies, it will be relaunched - PartSys->sources[i].source.sat = 255; // full saturation, color chosen by palette } } else @@ -9063,25 +9063,27 @@ 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(); ParticleSystem *PartSys = NULL; uint32_t i = 0; - PSparticle *attractor; //particle pointer to the attractor - uint8_t *counters; // counters for the applied force PSsettings sourcesettings = {0, 0, 1, 1, 0, 0, 0, 0}; // PS settings for bounceY, bounceY used for source movement (it always bounces whereas particles do not) - + PSparticle *attractor; //particle pointer to the attractor + +/* + PSsettings sourcesettings; + uint8_t *settingsPtr = reinterpret_cast(&sourcesettings); // access settings as one byte (wmore efficient in code and speed) + *settingsPtr = 0b00001100; // PS settings for bounceY, bounceY used for source movement (it always bounces whereas particles do not) +*/ if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. - { - uint32_t numParticles = (calculateNumberOfParticles() * 2) / 3; // use 75% of available particles to keep FPS high (also we need an attractor particle) - if (!initParticleSystem(PartSys, numParticles)) // init, need one extra byte per used particle for force counters + { + if (!initParticleSystem(PartSys, 1, true)) // init using 1 source and advanced particle settings return mode_static(); // allocation failed; //allocation failed - PartSys->sources[0].source.hue = random16(); - PartSys->sources[0].source.sat = 255; // set full saturation + PartSys->sources[0].source.hue = random16(); PartSys->sources[0].source.x = PS_P_RADIUS; //start out in bottom left corner PartSys->sources[0].source.y = PS_P_RADIUS<<1; PartSys->sources[0].source.vx = random16(5) + 3; @@ -9097,7 +9099,8 @@ uint16_t mode_particleattractor(void) #endif PartSys->sources[0].vx = 0; // emitting speed PartSys->sources[0].vy = 0; // emitting speed - PartSys->sources[0].var = 7; // emiting variation + PartSys->sources[0].var = 7; // emiting variation + PartSys->setWallHardness(230); // walls are always same hardness } else PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS @@ -9109,7 +9112,7 @@ uint16_t mode_particleattractor(void) } // Particle System settings PartSys->updateSystem(); // update system properties (dimensions and data pointers) - PartSys->setWallHardness(230); //walls are always same hardness + PartSys->setColorByAge(SEGMENT.check1); if (SEGMENT.custom2 > 0) // collisions enabled @@ -9123,7 +9126,6 @@ uint16_t mode_particleattractor(void) // set pointers attractor = reinterpret_cast(&PartSys->particles[lastusedparticle + 1]); - counters = reinterpret_cast(PartSys->PSdataEnd); //set attractor properties if(SEGMENT.check2) //move attractor { @@ -9153,7 +9155,7 @@ uint16_t mode_particleattractor(void) // apply force for(i = 0; i < displayparticles; i++) { - PartSys->pointAttractor(&PartSys->particles[i], attractor, &counters[i], SEGMENT.speed, SEGMENT.check3); //TODO: there was a bug here, counters was always the same pointer, check if this works. + PartSys->pointAttractor(i, attractor, SEGMENT.speed, SEGMENT.check3); //TODO: there was a bug here, counters was always the same pointer, check if this works. } if (SEGMENT.call % (33 - SEGMENT.custom3) == 0) PartSys->applyFriction(2); @@ -9164,14 +9166,15 @@ uint16_t mode_particleattractor(void) return FRAMETIME; } static const char _data_FX_MODE_PARTICLEATTRACTOR[] PROGMEM = "PS Attractor@Mass,Particles,Emit Speed,Collisions,Friction,Color by Age,Move,Swallow;;!;2;pal=9,sx=100,ix=82,c1=190,c2=0,o1=0,o2=0,o3=0"; -*/ + + /* 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) @@ -9181,15 +9184,16 @@ uint16_t mode_particleattractor(void) PSparticle *attractor; // particle pointer to the attractor uint8_t *counters; // counters for the applied force PSsettings sourcesettings = {0, 0, 1, 1, 0, 0, 0, 0}; // PS settings for bounceY, bounceY used for source movement (it always bounces whereas particles do not) + PSsettings sourcesettings; + uint8_t *settingsPtr = reinterpret_cast(&sourcesettings); // access settings as one byte (wmore efficient in code and speed) + *settingsPtr = 0b00001100; // PS settings for bounceY, bounceY used for source movement (it always bounces whereas particles do not) if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. { - uint32_t numParticles = (calculateNumberOfParticles() * 2) / 3; // use 75% of available particles to keep FPS high (also we need an attractor particle) - if (!initParticleSystem(PartSys, 1, numParticles)) // init, need one source and one extra byte per used particle for force counters + if (!initParticleSystem(PartSys, 1, true)) // init, need one source. use advanced particles (with individual forces) return mode_static(); // allocation failed; //allocation failed PartSys->sources[0].source.hue = random16(); - PartSys->sources[0].source.sat = 255; // set full saturation PartSys->sources[0].source.x = PS_P_RADIUS; // start out in bottom left corner PartSys->sources[0].source.y = PS_P_RADIUS << 1; PartSys->sources[0].source.vx = random16(5) + 3; @@ -9213,7 +9217,7 @@ uint16_t mode_particleattractor(void) if (PartSys == NULL) { DEBUG_PRINT(F("ERROR: FX PartSys nullpointer")); - return mode_static(); // something went wrong, no data! (TODO: ask how to handle this so it always works) + return mode_static(); // something went wrong, no data! } // Particle System settings PartSys->updateSystem(); // update system properties (dimensions and data pointers) @@ -9225,13 +9229,12 @@ uint16_t mode_particleattractor(void) else PartSys->enableParticleCollisions(false); - uint16_t lastusedparticle = (PartSys->numParticles * 2) / 3; + uint16_t lastusedparticle = (PartSys->numParticles * 2) / 3; //only use 2/3 of the available particles to keep things fast uint32_t displayparticles = map(SEGMENT.intensity, 0, 255, 10, lastusedparticle); PartSys->setUsedParticles(displayparticles); // set pointers attractor = reinterpret_cast(&PartSys->particles[lastusedparticle + 1]); - counters = reinterpret_cast(PartSys->PSdataEnd); // set attractor properties if (SEGMENT.check2) // move attractor { @@ -9264,7 +9267,7 @@ uint16_t mode_particleattractor(void) if(SEGMENT.call % 2 == 0) for (i = 0; i < displayparticles; i++) { - PartSys->lineAttractor(&PartSys->particles[i], attractor, SEGMENT.aux1, &counters[i], SEGMENT.speed); + //PartSys->lineAttractor(&PartSys->particles[i], attractor, SEGMENT.aux1, &counters[i], SEGMENT.speed); //TODO: upate this to advanced particles!!! } if (SEGMENT.call % (33 - SEGMENT.custom3) == 0) PartSys->applyFriction(2); @@ -9275,7 +9278,7 @@ uint16_t mode_particleattractor(void) return FRAMETIME; } static const char _data_FX_MODE_PARTICLEATTRACTOR[] PROGMEM = "PS Attractor@Mass,Particles,Emit Speed,Collisions,Friction,Color by Age,Move,Swallow;;!;2;pal=9,sx=100,ix=82,c1=190,c2=0,o1=0,o2=0,o3=0"; - +*/ /* Particle Spray, just a particle spray with many parameters Uses palette for particle color @@ -9297,9 +9300,8 @@ uint16_t mode_particlespray(void) return mode_static(); // allocation failed; //allocation failed PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting) PartSys->setBounceY(true); - PartSys->setMotionBlur(230); // anable motion blur + PartSys->setMotionBlur(200); // anable motion blur PartSys->sources[0].source.hue = random16(); - PartSys->sources[0].source.sat = 255; // set full saturation PartSys->sources[0].maxLife = 300; // lifetime in frames PartSys->sources[0].minLife = 100; PartSys->sources[0].source.collide = true; // seeded particles will collide (if enabled) @@ -9320,7 +9322,7 @@ uint16_t mode_particlespray(void) PartSys->setBounceX(!SEGMENT.check2); PartSys->setWrapX(SEGMENT.check2); PartSys->setWallHardness(hardness); - PartSys->enableGravity(SEGMENT.check1); + if(SEGMENT.check1) PartSys->setGravity(); numSprays = min(PartSys->numSources, (uint8_t)1); // number of sprays if (SEGMENT.check3) // collisions enabled @@ -9383,7 +9385,7 @@ uint16_t mode_particleGEQ(void) PartSys->setBounceY(SEGMENT.check3); PartSys->enableParticleCollisions(false); PartSys->setWallHardness(SEGMENT.custom2); - PartSys->enableGravity(true, SEGMENT.custom3<<2); //set gravity strength + PartSys->setGravity(SEGMENT.custom3 << 2); // set gravity strength um_data_t *um_data; if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) @@ -9433,8 +9435,7 @@ uint16_t mode_particleGEQ(void) PartSys->particles[i].y = PS_P_RADIUS; //tart at the bottom (PS_P_RADIUS is minimum position a particle is fully in frame) PartSys->particles[i].vx = random16(SEGMENT.custom1>>1)-(SEGMENT.custom1>>2) ; //x-speed variation: +/- custom1/4 PartSys->particles[i].vy = emitspeed; - PartSys->particles[i].hue = (bin<<4) + random16(17) - 8; // color from palette according to bin - PartSys->particles[i].sat = 255; // set saturation + PartSys->particles[i].hue = (bin<<4) + random16(17) - 8; // color from palette according to bin emitparticles--; } i++; diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index bf75e956da..940bccf9fc 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -46,22 +46,29 @@ #include "FastLED.h" #include "FX.h" -ParticleSystem::ParticleSystem(uint16_t width, uint16_t height, uint16_t numberofparticles, uint16_t numberofsources) +ParticleSystem::ParticleSystem(uint16_t width, uint16_t height, uint16_t numberofparticles, uint16_t numberofsources, bool isadvanced) { //Serial.println("PS Constructor"); numSources = numberofsources; numParticles = numberofparticles; // set number of particles in the array usedParticles = numberofparticles; // use all particles by default //particlesettings = {false, false, false, false, false, false, false, false}; // all settings off by default - updatePSpointers(); // set the particle and sources pointer (call this before accessing sprays or particles) + updatePSpointers(isadvanced); // set the particle and sources pointer (call this before accessing sprays or particles) setMatrixSize(width, height); setWallHardness(255); // set default wall hardness to max + setGravity(0); //gravity disabled by default setSaturation(255); //full saturation by default - setParticleSize(0); // minimum size + setParticleSize(0); // minimum size by default motionBlur = 0; //no fading by default emitIndex = 0; /* Serial.println("alive particles: "); + uint32_t aliveparticles = 0; + for (int i = 0; i < numParticles; i++) + { + aliveparticles++; + } + Serial.println(aliveparticles); for (int i = 0; i < numParticles; i++) { //particles[i].ttl = 0; //initialize all particles to dead @@ -73,7 +80,7 @@ ParticleSystem::ParticleSystem(uint16_t width, uint16_t height, uint16_t numbero Serial.println(particles[i].y); } }*/ - //Serial.println("PS Constructor done"); + // Serial.println("PS Constructor done"); } //update function applies gravity, moves the particles, handles collisions and renders the particles @@ -81,8 +88,8 @@ void ParticleSystem::update(void) { //apply gravity globally if enabled if (particlesettings.useGravity) - applyGravity(particles, usedParticles, gforce, &gforcecounter); - + applyGravity(); + // handle collisions (can push particles, must be done before updating particles or they can render out of bounds, causing a crash if using local buffer for speed) if (particlesettings.useCollisions) handleCollisions(); @@ -91,8 +98,17 @@ void ParticleSystem::update(void) for (int i = 0; i < usedParticles; i++) { particleMoveUpdate(particles[i], particlesettings); - } + } + //!!! remove this + //Serial.print("alive particles: "); + uint32_t aliveparticles = 0; + for (int i = 0; i < numParticles; i++) + { + if(particles[i].ttl) + aliveparticles++; + } + //Serial.println(aliveparticles); ParticleSys_render(); } @@ -172,13 +188,16 @@ void ParticleSystem::setParticleSize(uint8_t size) particlesize = size; particleHardRadius = max(PS_P_MINHARDRADIUS, (int)particlesize); } -// enable/disable gravity, optionally, set the force (force=8 is default) can be 1-255, 0 is also disable +// enable/disable gravity, optionally, set the force (force=8 is default) can be 1-255, 0 is 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) +// 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::setGravity(int8_t force) +{ + if (force) + { gforce = force; + particlesettings.useGravity = true; + } else particlesettings.useGravity = false; } @@ -204,9 +223,13 @@ void ParticleSystem::sprayEmit(PSsource &emitter) particles[emitIndex].vx = emitter.vx + random(emitter.var) - (emitter.var>>1); particles[emitIndex].vy = emitter.vy + random(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; + particles[emitIndex].hue = emitter.source.hue; particles[emitIndex].collide = emitter.source.collide; + if (advPartProps) + { + advPartProps[emitIndex].sat = emitter.sat; + advPartProps[emitIndex].size = emitter.size; + } break; } /* @@ -254,10 +277,11 @@ void ParticleSystem::angleEmit(PSsource &emitter, uint16_t angle, int8_t speed) emitter.vx = ((int32_t)cos16(angle) * (int32_t)speed) / (int32_t)32600; // cos16() and sin16() return signed 16bit, division should be 32767 but 32600 gives slightly better rounding emitter.vy = ((int32_t)sin16(angle) * (int32_t)speed) / (int32_t)32600; // note: cannot use bit shifts as bit shifting is asymmetrical for positive and negative numbers and this needs to be accurate! sprayEmit(emitter); + /* Serial.print(" x: "); Serial.print(emitter.vx); Serial.print(" y: "); - Serial.println(emitter.vy); + Serial.println(emitter.vy);*/ } // particle moves, decays and dies, if killoutofbounds is set, out of bounds particles are set to ttl=0 @@ -355,10 +379,10 @@ void ParticleSystem::particleMoveUpdate(PSparticle &part, PSsettings &options) } } -// 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) +// apply a force in x,y direction to individual particle +// caller needs to provide a 8bit counter (for each paticle) that holds its value between calls // 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) +void ParticleSystem::applyForce(PSparticle *part, 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 @@ -369,81 +393,76 @@ void ParticleSystem::applyForce(PSparticle *part, uint32_t numparticles, int8_t 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 = 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) - { - 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 - part[i].vx = limitSpeed((int32_t)particles[i].vx + dvx); - } - } - if (dvy != 0) - { - for (i = 0; i < numparticles; i++) - { - part[i].vy = limitSpeed((int32_t)particles[i].vy + dvy); - } - } + // apply the force to particle: + part->vx = limitSpeed((int32_t)part->vx + dvx); + part->vy = limitSpeed((int32_t)part->vy + dvy); } -// apply a force in x,y direction to particles directly (no counter required but no 'sub 1' force supported) -void ParticleSystem::applyForce(PSparticle *part, uint32_t numparticles, int8_t xforce, int8_t yforce) +// apply a force in x,y direction to individual particle using advanced particle properties +void ParticleSystem::applyForce(uint16_t particleindex, int8_t xforce, int8_t yforce) { - //note: could make this faster for single particles by adding an if statement, but it is fast enough as is - for (uint i = 0; i < numparticles; i++) + if (advPartProps == NULL) + return; // no advanced properties available + applyForce(&particles[particleindex], xforce, yforce, &advPartProps[particleindex].forcecounter); +} + +// apply a force in x,y direction to all particles +void ParticleSystem::applyForce(int8_t xforce, int8_t yforce) +{ + // for small forces, need to use a delay counter + uint8_t tempcounter; + + //note: this is not the most compuatationally effeicient way to do this, but it saves on duplacte code and is fast enough + for (uint i = 0; i < usedParticles; 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 - part[i].vx = limitSpeed((int32_t)part[i].vx + (int32_t)xforce); - part[i].vy = limitSpeed((int32_t)part[i].vy + (int32_t)yforce); + tempcounter = forcecounter; + applyForce(&particles[i], xforce, yforce, &tempcounter); } + forcecounter = tempcounter; //save value back } -// apply a force in angular direction to group of particles //TODO: actually test if this works as expected, this is untested code -// caller needs to provide a 8bit counter that holds its value between calls for each group (numparticles can be 1 for single particle) +// apply a force in angular direction to single particle +// caller needs to provide a 8bit counter that holds its value between calls (if using single particles, a counter for each particle is needed) // angle is from 0-65535 (=0-360deg) angle = 0 means in positive x-direction (i.e. to the right) -void ParticleSystem::applyAngleForce(PSparticle *part, uint32_t numparticles, uint8_t force, uint16_t angle, uint8_t *counter) +// force is in 3.4 fixed point notation so force=16 means apply v+1 each frame (useful force range is +/- 127) +void ParticleSystem::applyAngleForce(PSparticle *part, int8_t force, uint16_t angle, uint8_t *counter) { int8_t xforce = ((int32_t)force * cos16(angle)) / 32767; // force is +/- 127 int8_t yforce = ((int32_t)force * sin16(angle)) / 32767; // note: cannot use bit shifts as bit shifting is asymmetrical for positive and negative numbers and this needs to be accurate! // note: sin16 is 10% faster than sin8() on ESP32 but on ESP8266 it is 9% slower - // 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); + applyForce(part, xforce, yforce, counter); } -// apply a force in angular direction to particles directly (no counter required but no 'sub 1' force supported) +void ParticleSystem::applyAngleForce(uint16_t particleindex, int8_t force, uint16_t angle) +{ + if (advPartProps == NULL) + return; // no advanced properties available + applyAngleForce(&particles[particleindex], force, angle, &advPartProps[particleindex].forcecounter); +} +// apply a force in angular direction to all particles // angle is from 0-65535 (=0-360deg) angle = 0 means in positive x-direction (i.e. to the right) -void ParticleSystem::applyAngleForce(PSparticle *part, uint32_t numparticles, uint8_t force, uint16_t angle) +void ParticleSystem::applyAngleForce(int8_t force, uint16_t angle) { int8_t xforce = ((int32_t)force * cos16(angle)) / 32767; // force is +/- 127 int8_t yforce = ((int32_t)force * sin16(angle)) / 32767; // note: cannot use bit shifts as bit shifting is asymmetrical for positive and negative numbers and this needs to be accurate! - applyForce(part, numparticles, xforce, yforce); + applyForce(xforce, yforce); } -// 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 3.4 fixed point notation so force=16 means apply v+1 each frame default of 8 is every other frame (gives good results) -// positive force means down -void ParticleSystem::applyGravity(PSparticle *part, uint32_t numarticles, int8_t force, uint8_t *counter) -{ - int32_t dv = calcForce_dv(force, counter); - 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 - part[i].vy = limitSpeed((int32_t)particles[i].vy - dv); - } - -} -//apply gravity using PS global gforce -void ParticleSystem::applyGravity(PSparticle *part, uint32_t numarticles, uint8_t *counter) +// apply gravity to all particles using PS global gforce setting +// note: faster than apply force since direction is always down and counter is fixed for all particles +void ParticleSystem::applyGravity() { - applyGravity(part, numarticles, gforce, counter); + int32_t dv = calcForce_dv(gforce, &gforcecounter); + for (uint32_t i = 0; i < usedParticles; 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 = limitSpeed((int32_t)particles[i].vy - dv); + } } //apply gravity to single particle using system settings (use this for sources) @@ -462,11 +481,11 @@ void ParticleSystem::applyGravity(PSparticle *part) } } -// slow down particles by friction, the higher the speed, the higher the friction. a high friction coefficient slows them more (255 means instant stop) +// slow down particle by friction, the higher the speed, the higher the friction. a high friction coefficient slows them more (255 means instant stop) void ParticleSystem::applyFriction(PSparticle *part, uint8_t coefficient) { int32_t friction = 255 - coefficient; - // note: not checking if particle is dead is faster as most are usually alive and if few are alive, rendering is faster + // note: not checking if particle is dead can be done by caller (or can be omitted) // note2: cannot use right shifts as bit shifting in right direction is asymmetrical for positive and negative numbers and this needs to be accurate or things start to go to the left side. part->vx = ((int16_t)part->vx * friction) / 255; part->vy = ((int16_t)part->vy * friction) / 255; @@ -475,22 +494,22 @@ void ParticleSystem::applyFriction(PSparticle *part, uint8_t coefficient) // apply friction to all particles void ParticleSystem::applyFriction(uint8_t coefficient) { - int32_t friction = 255 - coefficient; for (uint32_t i = 0; i < usedParticles; i++) { - // note: not checking if particle is dead is faster as most are usually alive and if few are alive, rendering is faster - // note2: cannot use right shifts as bit shifting in right direction is asymmetrical for positive and negative numbers and this needs to be accurate or things start to go to the left side. - particles[i].vx = ((int16_t)particles[i].vx * friction) / 255; - particles[i].vy = ((int16_t)particles[i].vy * friction) / 255; + if(particles[i].ttl) + applyFriction(&particles[i], coefficient); } } // attracts a particle to an attractor particle using the inverse square-law -void ParticleSystem::pointAttractor(PSparticle *part, PSparticle *attractor, uint8_t *counter, uint8_t strength, bool swallow) +void ParticleSystem::pointAttractor(uint16_t particleindex, PSparticle *attractor, uint8_t strength, bool swallow) // TODO: need to check if this is ok with new advancedprops !!! { + if (advPartProps == NULL) + return; // no advanced properties available + // Calculate the distance between the particle and the attractor - int32_t dx = attractor->x - part->x; - int32_t dy = attractor->y - part->y; + int32_t dx = attractor->x - particles[particleindex].x; + int32_t dy = attractor->y - particles[particleindex].y; // Calculate the force based on inverse square law int32_t distanceSquared = dx * dx + dy * dy; @@ -498,11 +517,11 @@ void ParticleSystem::pointAttractor(PSparticle *part, PSparticle *attractor, uin { if (swallow) // particle is close, age it fast so it fades out, do not attract further { - if (part->ttl > 7) - part->ttl -= 8; + if (particles[particleindex].ttl > 7) + particles[particleindex].ttl -= 8; else { - part->ttl = 0; + particles[particleindex].ttl = 0; return; } } @@ -513,12 +532,14 @@ void ParticleSystem::pointAttractor(PSparticle *part, PSparticle *attractor, uin int8_t xforce = (force * dx) / 1024; // scale to a lower value, found by experimenting int8_t yforce = (force * dy) / 1024; // note: cannot use bit shifts as bit shifting is asymmetrical for positive and negative numbers and this needs to be accurate! - applyForce(part, 1, xforce, yforce, counter); + applyForce(particleindex, xforce, yforce); } -void ParticleSystem::lineAttractor(PSparticle *part, PSparticle *attractorcenter, uint16_t attractorangle, uint8_t *counter, uint8_t strength) +void ParticleSystem::lineAttractor(uint16_t particleindex, PSparticle *attractorcenter, uint16_t attractorangle, uint8_t strength) { // Calculate the distance between the particle and the attractor + if(advPartProps == NULL) + return; //no advanced properties available //calculate a second point on the line int32_t x1 = attractorcenter->x + (cos16(attractorangle) >> 5); @@ -526,7 +547,7 @@ void ParticleSystem::lineAttractor(PSparticle *part, PSparticle *attractorcenter //calculate squared distance from particle to the line: int32_t dx = (x1 - attractorcenter->x) >> 4; int32_t dy = (y1 - attractorcenter->y) >> 4; - int32_t d = ((dx * (part->y - attractorcenter->y)) - (dy * (part->x - attractorcenter->x))) >> 8; + int32_t d = ((dx * (particles[particleindex].y - attractorcenter->y)) - (dy * (particles[particleindex].x - attractorcenter->x))) >> 8; int32_t distanceSquared = (d * d) / (dx * dx + dy * dy); @@ -543,9 +564,9 @@ void ParticleSystem::lineAttractor(PSparticle *part, PSparticle *attractorcenter int8_t yforce = (d > 0 ? -1 : 1) * (force * dx) / 100; // note: cannot use bit shifts as bit shifting is asymmetrical for positive and negative numbers and this needs to be accurate! /* Serial.print(" partx: "); - Serial.print(part->x); + Serial.print(particles[particleindex].x); Serial.print(" party "); - Serial.print(part->y); + Serial.print(particles[particleindex].y); Serial.print(" x1 "); Serial.print(x1); Serial.print(" y1 "); @@ -564,7 +585,8 @@ void ParticleSystem::lineAttractor(PSparticle *part, PSparticle *attractorcenter Serial.print(xforce); Serial.print(" fy: "); Serial.println(yforce);*/ - applyForce(part, 1, xforce, yforce, counter); + + applyForce(particleindex, xforce, yforce); } // 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 @@ -631,10 +653,13 @@ void ParticleSystem::ParticleSys_render(bool firemode, uint32_t fireintensity) else{ 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 (saturation < 255) { CHSV baseHSV = rgb2hsv_approximate(baseRGB); //convert to hsv - baseHSV.s = particles[i].sat; //desaturate + if(advPartProps) + baseHSV.s = advPartProps->sat; + else + baseHSV.s = saturation; baseRGB = (CRGB)baseHSV; //convert back to RGB } } @@ -1256,20 +1281,32 @@ void ParticleSystem::updateSystem(void) uint32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; uint32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); setMatrixSize(cols, rows); - updatePSpointers(); + bool isadvanced = false; + if (advPartProps) //if pointer was previously set i.e. non NULL (pinter is only NULL if never set, even if segment is copied) + isadvanced = true; + updatePSpointers(isadvanced); } // set the pointers for the class (this only has to be done once and not on every FX call, only the class pointer needs to be reassigned to SEGENV.data every time) // function returns the pointer to the next byte available for the FX (if it assigned more memory for other stuff using the above allocate function) // FX handles the PSsources, need to tell this function how many there are -void ParticleSystem::updatePSpointers() +void ParticleSystem::updatePSpointers(bool isadvanced) { //DEBUG_PRINT(F("*** PS pointers ***")); - //DEBUG_PRINTF_P(PSTR("this PS %p\n"), this); - - particles = reinterpret_cast(this + 1); // pointer to particle array at data+sizeof(ParticleSystem) - sources = reinterpret_cast(particles + numParticles); // pointer to source(s) - PSdataEnd = reinterpret_cast(sources + numSources); // pointer to first available byte after the PS + //DEBUG_PRINTF_P(PSTR("this PS %p\n"), this); + byte *nextaddress = NULL; + particles = reinterpret_cast(this + 1); // pointer to particle array at data+sizeof(ParticleSystem) + if(isadvanced) + { + advPartProps = reinterpret_cast(particles + numParticles); + sources = reinterpret_cast(advPartProps + numParticles); // pointer to source(s) + } + else + { + advPartProps = NULL; + sources = reinterpret_cast(particles + numParticles); // pointer to source(s) + } + PSdataEnd = reinterpret_cast(sources + numSources); // pointer to first available byte after the PS for FX additional data //DEBUG_PRINTF_P(PSTR("particles %p\n"), particles); //DEBUG_PRINTF_P(PSTR("sources %p\n"), sources); @@ -1277,21 +1314,23 @@ void ParticleSystem::updatePSpointers() } //non class functions to use for initialization -uint32_t calculateNumberOfParticles() +uint32_t calculateNumberOfParticles(bool isadvanced) { uint32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; uint32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); #ifdef ESP8266 - uint numberofParticles = (cols * rows * 3) / 4; // 0.75 particle per pixel - uint particlelimit = ESP8266_MAXPARTICLES; // maximum number of paticles allowed (based on one segment of 16x16 and 4k effect ram) + uint32_t numberofParticles = (cols * rows * 3) / 4; // 0.75 particle per pixel + uint32_t particlelimit = ESP8266_MAXPARTICLES; // maximum number of paticles allowed (based on one segment of 16x16 and 4k effect ram) #elif ARDUINO_ARCH_ESP32S2 - uint numberofParticles = (cols * rows); // 1 particle per pixe - uint particlelimit = ESP32S2_MAXPARTICLES; // maximum number of paticles allowed (based on one segment of 32x32 and 24k effect ram) + uint32_t numberofParticles = (cols * rows); // 1 particle per pixel + uint32_t particlelimit = ESP32S2_MAXPARTICLES; // maximum number of paticles allowed (based on one segment of 32x32 and 24k effect ram) #else - uint numberofParticles = (cols * rows); // 1 particle per pixel (for example 768 particles on 32x16) - uint particlelimit = ESP32_MAXPARTICLES; // maximum number of paticles allowed (based on two segments of 32x32 and 40k effect ram) + uint32_t numberofParticles = (cols * rows); // 1 particle per pixel (for example 512 particles on 32x16) + uint32_t particlelimit = ESP32_MAXPARTICLES; // maximum number of paticles allowed (based on two segments of 32x32 and 40k effect ram) #endif - numberofParticles = max((uint)1, min(numberofParticles, particlelimit)); + numberofParticles = max((uint32_t)1, min(numberofParticles, particlelimit)); + if (isadvanced) // advanced property array needs ram, reduce number of particles to use the same amount + numberofParticles = (numberofParticles * sizeof(PSparticle)) / (sizeof(PSparticle) + sizeof(PSadvancedParticle)); return numberofParticles; } @@ -1313,10 +1352,12 @@ uint32_t calculateNumberOfSources(uint8_t requestedsources) } //allocate memory for particle system class, particles, sprays plus additional memory requested by FX -bool allocateParticleSystemMemory(uint16_t numparticles, uint16_t numsources, uint16_t additionalbytes) +bool allocateParticleSystemMemory(uint16_t numparticles, uint16_t numsources, bool isadvanced, uint16_t additionalbytes) { uint32_t requiredmemory = sizeof(ParticleSystem); requiredmemory += sizeof(PSparticle) * numparticles; + if (isadvanced) + requiredmemory += sizeof(PSadvancedParticle) * numparticles; requiredmemory += sizeof(PSsource) * numsources; requiredmemory += additionalbytes; //Serial.print("allocating: "); @@ -1328,12 +1369,14 @@ bool allocateParticleSystemMemory(uint16_t numparticles, uint16_t numsources, ui } // initialize Particle System, allocate additional bytes if needed (pointer to those bytes can be read from particle system class: PSdataEnd) -bool initParticleSystem(ParticleSystem *&PartSys, uint8_t requestedsources, uint16_t additionalbytes) +bool initParticleSystem(ParticleSystem *&PartSys, uint8_t requestedsources, bool isadvanced, uint16_t additionalbytes) { //Serial.println("PS init function"); - uint32_t numparticles = calculateNumberOfParticles(); + uint32_t numparticles = calculateNumberOfParticles(isadvanced); uint32_t numsources = calculateNumberOfSources(requestedsources); - if (!allocateParticleSystemMemory(numparticles, numsources, additionalbytes)) + //Serial.print("numsources: "); + //Serial.println(numsources); + if (!allocateParticleSystemMemory(numparticles, numsources, isadvanced, additionalbytes)) { DEBUG_PRINT(F("PS init failed: memory depleted")); return false; @@ -1343,7 +1386,7 @@ bool initParticleSystem(ParticleSystem *&PartSys, uint8_t requestedsources, uint 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 TODO: why does VS studio thinkt this is bad? + PartSys = new (SEGMENT.data) ParticleSystem(cols, rows, numparticles, numsources, isadvanced); // particle system constructor TODO: why does VS studio thinkt this is bad? //Serial.print("PS pointer at "); //Serial.println((uintptr_t)PartSys); return true; diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index 1db5172b57..2f16045565 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -30,9 +30,9 @@ #include "FastLED.h" //memory allocation -#define ESP8266_MAXPARTICLES 148 // enough for one 16x16 segment with transitions +#define ESP8266_MAXPARTICLES 160 // enough for one 16x16 segment with transitions #define ESP8266_MAXSOURCES 16 -#define ESP32S2_MAXPARTICLES 768 // enough for four 16x16 segments +#define ESP32S2_MAXPARTICLES 840 // enough for four 16x16 segments #define ESP32S2_MAXSOURCES 48 #define ESP32_MAXPARTICLES 1024 // enough for four 16x16 segments TODO: not enough for one 64x64 panel... #define ESP32_MAXSOURCES 64 @@ -46,7 +46,7 @@ #define PS_P_MINSURFACEHARDNESS 128 //minimum hardness used in collision impulse calculation, below this hardness, particles become sticky #define PS_P_MAXSPEED 120 //maximum speed a particle can have (vx/vy is int8) -//struct for a single particle (10 bytes) +//struct for a single particle (9 bytes) typedef struct { int16_t x; //x position in particle system int16_t y; //y position in particle system @@ -66,24 +66,44 @@ typedef struct { uint8_t sat; //particle color saturation uint8_t size; //particle size, 255 means 10 pixels in diameter - uint8_t sizeasymmetry; //asymmetrical size TODO: need something better to define this? uint8_t forcecounter; //counter for applying forces to individual particles - bool flag1 : 1; // unused flags... for now. - bool flag2 : 1; - bool flag3 : 1; + //bool flag1 : 1; // unused flags... for now. + //bool flag2 : 1; + //bool flag3 : 1; + //bool flag4 : 1; +} PSadvancedParticle; + +// struct for advanced particle size control (optional) TODO: this is currently just an idea, may not make it into final code if too slow / complex +typedef struct +{ + uint8_t sizeasymmetry; // asymmetrical size TODO: need something better to define this? + uint8_t targetsize; // target size for growing / shrinking + uint8_t growspeed : 4; + uint8_t shrinkspeed : 4; + uint8_t sizecounter; // counter that can be used for size contol TODO: need more than one? + //ideas: + //wobbleamount, rotation angle for asymmetic particles + //a flag 'usegravity' that can be set to false for selective gravity application + + bool grow : 1; // flags + bool shrink : 1; + bool wobble : 1; bool flag4 : 1; -} PSadvancedparticle; +} PSsizeControl; + //struct for a particle source (17 bytes) 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 -} PSsource; //TODO: sources also need parameter for advanced particles, so size, saturation, ... + 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 (use odd numbers for good symmetry) + int8_t vx; // emitting speed + int8_t vy; + uint8_t sat; // particle saturation (advanced property) + uint8_t size; // particle size (advanced property) +} PSsource; // struct for PS settings typedef struct @@ -102,33 +122,32 @@ typedef struct class ParticleSystem { public: - ParticleSystem(uint16_t width, uint16_t height, uint16_t numberofparticles, uint16_t numberofsources); // constructor + ParticleSystem(uint16_t width, uint16_t height, uint16_t numberofparticles, uint16_t numberofsources, bool isadvanced = false); // constructor // note: memory is allcated in the FX function, no deconstructor needed void update(void); //update the particles according to set options and render to the matrix void updateFire(uint32_t intensity); // update function for fire - + void updateSystem(void); // call at the beginning of every FX, updates pointers and dimensions // particle emitters void flameEmit(PSsource &emitter); void sprayEmit(PSsource &emitter); - void angleEmit(PSsource& emitter, uint16_t angle, int8_t speed); - void updateSystem(void); // call at the beginning of every FX, updates pointers and dimensions + void angleEmit(PSsource& emitter, uint16_t angle, int8_t speed); // move functions void particleMoveUpdate(PSparticle &part, PSsettings &options); - //particle physics - void applyGravity(PSparticle *part, uint32_t numarticles, int8_t force, uint8_t *counter); - void applyGravity(PSparticle *part, uint32_t numarticles, uint8_t *counter); //use global gforce - void applyGravity(PSparticle *part); //use global system settings - void applyForce(PSparticle *part, uint32_t numparticles, int8_t xforce, int8_t yforce, uint8_t *counter); - void applyForce(PSparticle *part, uint32_t numparticles, int8_t xforce, int8_t yforce); - void applyAngleForce(PSparticle *part, uint32_t numparticles, uint8_t force, uint16_t angle, uint8_t *counter); - void applyAngleForce(PSparticle *part, uint32_t numparticles, uint8_t force, uint16_t angle); + //particle physics + void applyGravity(PSparticle *part); // applies gravity to single particle (use this for sources) + void applyForce(PSparticle *part, int8_t xforce, int8_t yforce, uint8_t *counter); + void applyForce(uint16_t particleindex, int8_t xforce, int8_t yforce); //use this for advanced property particles + void applyForce(int8_t xforce, int8_t yforce); //apply a force to all particles + void applyAngleForce(PSparticle *part, int8_t force, uint16_t angle, uint8_t *counter); + void applyAngleForce(uint16_t particleindex, int8_t force, uint16_t angle); // use this for advanced property particles + void applyAngleForce(int8_t force, uint16_t angle); //apply angular force to all particles void applyFriction(PSparticle *part, uint8_t coefficient); // apply friction to specific particle void applyFriction(uint8_t coefficient); // apply friction to all used particles - void pointAttractor(PSparticle *particle, PSparticle *attractor, uint8_t *counter, uint8_t strength, bool swallow); - void lineAttractor(PSparticle *particle, PSparticle *attractorcenter, uint16_t attractorangle, uint8_t *counter, uint8_t strength); + void pointAttractor(uint16_t particleindex, PSparticle *attractor, uint8_t strength, bool swallow); + void lineAttractor(uint16_t particleindex, PSparticle *attractorcenter, uint16_t attractorangle, uint8_t strength); //set options void setUsedParticles(uint16_t num); @@ -144,11 +163,12 @@ class ParticleSystem void setColorByAge(bool enable); void setMotionBlur(uint8_t bluramount); void setParticleSize(uint8_t size); - void enableGravity(bool enable, uint8_t force = 8); + void setGravity(int8_t force = 8); void enableParticleCollisions(bool enable, uint8_t hardness = 255); PSparticle *particles; // pointer to particle array PSsource *sources; // pointer to sources + PSadvancedParticle *advPartProps; // pointer to advanced particle properties (can be NULL) uint8_t* PSdataEnd; //points to first available byte after the PSmemory, is set in setPointers(). use this to set pointer to FX custom data uint16_t maxX, maxY; //particle system size i.e. width-1 / height-1 in subpixels uint32_t maxXpixel, maxYpixel; // last physical pixel that can be drawn to (FX can read this to read segment size if required), equal to width-1 / height-1 @@ -163,12 +183,13 @@ class ParticleSystem void renderParticle(PSparticle *particle, uint32_t brightess, int32_t *pixelvalues, int32_t (*pixelpositions)[2]); //paricle physics applied by system if flags are set + void applyGravity(); // applies gravity to all particles void handleCollisions(); void collideParticles(PSparticle *particle1, PSparticle *particle2); void fireParticleupdate(); //utility functions - void updatePSpointers(); // update the data pointers to current segment data space + void updatePSpointers(bool isadvanced); // update the data pointers to current segment data space int32_t wraparound(int32_t w, int32_t maxvalue); int32_t calcForce_dv(int8_t force, uint8_t *counter); int32_t limitSpeed(int32_t speed); @@ -179,19 +200,20 @@ class ParticleSystem int32_t collisionHardness; int32_t wallHardness; uint8_t gforcecounter; //counter for global gravity - int8_t gforce; //gravity strength, default is 8 (negative is allowed) + int8_t gforce; //gravity strength, default is 8 (negative is allowed, positive is downwards) uint8_t collisioncounter; //counter to handle collisions TODO: could use the SEGMENT.call? + uint8_t forcecounter; //counter for globally applied forces //global particle properties for basic particles - uint8_t saturation; - uint8_t particlesize; + uint8_t saturation; //note: on advanced particles, set this to 255 to disable saturation rendering, any other value uses particle sat value + uint8_t particlesize; //global particle size, 0 = 2 pixels, 255 = 10 pixels int32_t particleHardRadius; // hard surface radius of a particle, used for collision detection uint8_t motionBlur; }; //initialization functions (not part of class) -bool initParticleSystem(ParticleSystem *&PartSys, uint8_t requestedsources, uint16_t additionalbytes = 0); -uint32_t calculateNumberOfParticles(); -uint32_t calculateNumberOfSources(); -bool allocateParticleSystemMemory(uint16_t numparticles, uint16_t numsources, uint16_t additionalbytes); +bool initParticleSystem(ParticleSystem *&PartSys, uint8_t requestedsources, bool advanced = false, uint16_t additionalbytes = 0); +uint32_t calculateNumberOfParticles(bool advanced); +uint32_t calculateNumberOfSources(uint8_t requestedsources); +bool allocateParticleSystemMemory(uint16_t numparticles, uint16_t numsources, bool advanced, uint16_t additionalbytes); //color add function CRGB fast_color_add(CRGB c1, CRGB c2, uint32_t scale); // fast and accurate color adding with scaling (scales c2 before adding) \ No newline at end of file From 2e7fbc0310321f223a808a02a2c739adcc658d43 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Wed, 24 Apr 2024 06:39:30 +0200 Subject: [PATCH 062/219] debugging going on --- wled00/FX.cpp | 103 ++++++++++++++++++++---------------- wled00/FXparticleSystem.cpp | 6 +-- wled00/FXparticleSystem.h | 5 +- 3 files changed, 65 insertions(+), 49 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index ebab12c20b..a809d68085 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -8888,7 +8888,7 @@ uint16_t mode_particleperlin(void) PartSys->enableParticleCollisions(SEGMENT.check3, SEGMENT.custom1); // enable collisions and set particle collision hardness uint32_t displayparticles = map(SEGMENT.intensity, 0, 255, 10, PartSys->numParticles>>1); PartSys->setUsedParticles(displayparticles); - + PartSys->setMotionBlur(200); // anable motion blur // apply 'gravity' from a 2D perlin noise map SEGMENT.aux0 += 1+(SEGMENT.speed >> 5); // noise z-position @@ -8906,7 +8906,7 @@ uint16_t mode_particleperlin(void) uint16_t ynoise = PartSys->particles[i].y / scale; int16_t baseheight = inoise8(xnoise, ynoise, SEGMENT.aux0); // noise value at particle position PartSys->particles[i].hue = baseheight; // color particles to perlin noise value - if (SEGMENT.call % 10 == 0) // do not apply the force every frame, is too chaotic + if (SEGMENT.call % 3 == 0) // do not apply the force every frame, is too chaotic { int8_t xslope = (baseheight - (int16_t)inoise8(xnoise + 10, ynoise, SEGMENT.aux0)); int8_t yslope = (baseheight - (int16_t)inoise8(xnoise, ynoise + 10, SEGMENT.aux0)); @@ -8920,7 +8920,7 @@ uint16_t mode_particleperlin(void) PartSys->update(); // update and render return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEPERLIN[] PROGMEM = "PS Fuzzy Noise@Speed,Particles,Bounce,Friction,Scale,Cylinder,,Collisions;;!;2;pal=54,sx=75,ix=200,c1=255,c2=180,c3=20,o1=0"; +static const char _data_FX_MODE_PARTICLEPERLIN[] PROGMEM = "PS Fuzzy Noise@Speed,Particles,Bounce,Friction,Scale,Cylinder,,Collisions;;!;2;pal=54,sx=75,ix=200,c1=130,c2=30,c3=5,o1=0,o3=1"; /* * Particle smashing down like meteors and exploding as they hit the ground, has many parameters to play with @@ -8934,8 +8934,8 @@ uint16_t mode_particleimpact(void) ParticleSystem *PartSys = NULL; uint32_t i = 0; uint8_t MaxNumMeteors; - PSsettings meteorsettings = {0, 0, 0, 1, 0, 1, 0, 0}; // PS settings for meteors: bounceY and gravity enabled (todo: if ESP8266 is ok with out of bounds particles, this can be removed, it just takes care of the kill out of bounds setting) - //PSsettings meteorsettings; + PSsettings meteorsettings;// = {0, 0, 0, 1, 0, 1, 0, 0}; // PS settings for meteors: bounceY and gravity enabled + meteorsettings.asByte = 0b00101000; //uint8_t *settingsPtr = reinterpret_cast(&meteorsettings); // access settings as one byte (wmore efficient in code and speed) //*settingsPtr = 0b00101000; // PS settings for meteors: bounceY and gravity enabled @@ -8951,7 +8951,7 @@ uint16_t mode_particleimpact(void) for (i = 0; i < MaxNumMeteors; i++) { PartSys->sources[i].vx = 0; //emit speed in x - PartSys->sources[i].source.y = 10; + PartSys->sources[i].source.y = 500; PartSys->sources[i].source.ttl = random16(20 * i); // set initial delay for meteors PartSys->sources[i].source.vy = 10; // at positive speeds, no particles are emitted and if particle dies, it will be relaunched } @@ -8993,12 +8993,12 @@ uint16_t mode_particleimpact(void) } else // speed is zero, explode! { - PartSys->sources[i].source.vy = 125; // set source speed positive so it goes into timeout and launches again + PartSys->sources[i].source.vy = 10; // set source speed positive so it goes into timeout and launches again #ifdef ESP8266 emitparticles = random16(SEGMENT.intensity >> 3) + 5; // defines the size of the explosion #else - emitparticles = random16(SEGMENT.intensity >> 1) + 10; // defines the size of the explosion - #endif + emitparticles = map(SEGMENT.intensity, 0, 255, 10, random16(PartSys->numParticles>>2)); // defines the size of the explosion !!!TODO: check if this works, drop esp8266 def if it does +#endif } for (int e = emitparticles; e > 0; e--) { @@ -9011,29 +9011,33 @@ uint16_t mode_particleimpact(void) { if (PartSys->sources[i].source.ttl) { - PartSys->applyGravity(&PartSys->sources[i].source); - PartSys->particleMoveUpdate(PartSys->sources[i].source, meteorsettings); - - // if source reaches the bottom, set speed to 0 so it will explode on next function call (handled above) - if ((PartSys->sources[i].source.y < PS_P_RADIUS<<1) && ( PartSys->sources[i].source.vy < 0)) // reached the bottom pixel on its way down + PartSys->sources[i].source.ttl--; //note: this saves an if statement, but moving down particles age twice + if (PartSys->sources[i].source.vy < 0) //move down { - PartSys->sources[i].source.vy = 0; // set speed zero so it will explode - PartSys->sources[i].source.vx = 0; - //PartSys->sources[i].source.y = 5; // offset from ground so explosion happens not out of frame (TODO: still needed? the class takes care of that) - PartSys->sources[i].source.collide = true; - #ifdef ESP8266 - PartSys->sources[i].maxLife = 130; - PartSys->sources[i].minLife = 20; - PartSys->sources[i].source.ttl = random16(255 - (SEGMENT.speed>>1)) + 10; // standby time til next launch (in frames at 42fps, max of 265 is about 6 seconds - #else - PartSys->sources[i].maxLife = 200; - PartSys->sources[i].minLife = 50; - PartSys->sources[i].source.ttl = random16((255 - SEGMENT.speed)) + 10; // standby time til next launch (in frames at 42fps, max of 265 is about 6 seconds - #endif - PartSys->sources[i].vy = (SEGMENT.custom1 >> 2); // emitting speed y - PartSys->sources[i].var = (SEGMENT.custom1 >> 1) | 0x01; // speed variation around vx,vy (+/- var/2), only use odd numbers - } - } + PartSys->applyGravity(&PartSys->sources[i].source); + PartSys->particleMoveUpdate(PartSys->sources[i].source, meteorsettings); + + // if source reaches the bottom, set speed to 0 so it will explode on next function call (handled above) + if (PartSys->sources[i].source.y < PS_P_RADIUS<<1) // reached the bottom pixel on its way down + { + PartSys->sources[i].source.vy = 0; // set speed zero so it will explode + PartSys->sources[i].source.vx = 0; + //PartSys->sources[i].source.y = 5; // offset from ground so explosion happens not out of frame (!!!TODO: still needed? the class takes care of that) + PartSys->sources[i].source.collide = true; + #ifdef ESP8266 + PartSys->sources[i].maxLife = 130; + PartSys->sources[i].minLife = 20; + PartSys->sources[i].source.ttl = random16(255 - (SEGMENT.speed>>1)) + 10; // standby time til next launch (in frames at 42fps, max of 265 is about 6 seconds + #else + PartSys->sources[i].maxLife = 160; + PartSys->sources[i].minLife = 50; + PartSys->sources[i].source.ttl = random16((255 - SEGMENT.speed)) + 10; // standby time til next launch (in frames at 42fps, max of 265 is about 6 seconds + #endif + PartSys->sources[i].vy = (SEGMENT.custom1 >> 2); // emitting speed y + PartSys->sources[i].var = (SEGMENT.custom1 >> 1) | 0x01; // speed variation around vx,vy (+/- var/2), only use odd numbers + } + } + } else if ( PartSys->sources[i].source.vy > 0) // meteor is exploded and time is up (ttl==0 and positive speed), relaunch it { // reinitialize meteor @@ -9042,9 +9046,9 @@ uint16_t mode_particleimpact(void) PartSys->sources[i].source.vy = -random16(30) - 30; // meteor downward speed PartSys->sources[i].source.vx = random16(30) - 15; PartSys->sources[i].source.hue = random16(); // random color - PartSys->sources[i].source.ttl = 255; // long life, will explode at bottom + PartSys->sources[i].source.ttl = 2000; // long life, will explode at bottom PartSys->sources[i].source.collide = false; // trail particles will not collide - PartSys->sources[i].maxLife = 60; // spark particle life + PartSys->sources[i].maxLife = 60; // spark particle life PartSys->sources[i].minLife = 20; PartSys->sources[i].vy = -9; // emitting speed (down) PartSys->sources[i].var = 5; // speed variation around vx,vy (+/- var/2) @@ -9070,7 +9074,8 @@ uint16_t mode_particleattractor(void) return mode_static(); ParticleSystem *PartSys = NULL; uint32_t i = 0; - PSsettings sourcesettings = {0, 0, 1, 1, 0, 0, 0, 0}; // PS settings for bounceY, bounceY used for source movement (it always bounces whereas particles do not) + PSsettings sourcesettings;// = {0, 0, 1, 1, 0, 0, 0, 0}; // PS settings for bounceY, bounceY used for source movement (it always bounces whereas particles do not) + sourcesettings.asByte = 0b00001100; PSparticle *attractor; //particle pointer to the attractor /* @@ -9100,7 +9105,7 @@ uint16_t mode_particleattractor(void) PartSys->sources[0].vx = 0; // emitting speed PartSys->sources[0].vy = 0; // emitting speed PartSys->sources[0].var = 7; // emiting variation - PartSys->setWallHardness(230); // walls are always same hardness + PartSys->setWallHardness(255); //bounce forever } else PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS @@ -9125,17 +9130,21 @@ uint16_t mode_particleattractor(void) PartSys->setUsedParticles(displayparticles); // set pointers - attractor = reinterpret_cast(&PartSys->particles[lastusedparticle + 1]); - //set attractor properties - if(SEGMENT.check2) //move attractor + //attractor = reinterpret_cast(&PartSys->particles[lastusedparticle + 1]); + attractor = &PartSys->particles[lastusedparticle + 1]; + if(SEGMENT.call == 0) { attractor->vx = PartSys->sources[0].source.vy; // set to spray movemement but reverse x and y attractor->vy = PartSys->sources[0].source.vx; + } + //set attractor properties + if (SEGMENT.check2 && (SEGMENT.call % 2) == 0) // move attractor + { + + attractor->ttl = 255; //must be alive to move PartSys->particleMoveUpdate(*attractor, sourcesettings); // move the attractor } else{ - attractor->vx = 0; // not moving - attractor->vy = 0; attractor->x = PartSys->maxX >> 1; // center attractor->y = PartSys->maxY >> 1; } @@ -9161,8 +9170,7 @@ uint16_t mode_particleattractor(void) PartSys->applyFriction(2); PartSys->particleMoveUpdate(PartSys->sources[0].source, sourcesettings); // move the source - - PartSys->update(); // update and render + PartSys->update(); // update and render return FRAMETIME; } static const char _data_FX_MODE_PARTICLEATTRACTOR[] PROGMEM = "PS Attractor@Mass,Particles,Emit Speed,Collisions,Friction,Color by Age,Move,Swallow;;!;2;pal=9,sx=100,ix=82,c1=190,c2=0,o1=0,o2=0,o3=0"; @@ -9244,8 +9252,6 @@ uint16_t mode_particleattractor(void) } else { - attractor->vx = 0; // not moving - attractor->vy = 0; attractor->x = PartSys->maxX >> 1; // center attractor->y = PartSys->maxY >> 1; } @@ -9273,7 +9279,14 @@ uint16_t mode_particleattractor(void) PartSys->applyFriction(2); PartSys->particleMoveUpdate(PartSys->sources[0].source, sourcesettings); // move the source - + Serial.print("vx:"); + Serial.print(attractor->vx); + Serial.print("vy:"); + Serial.print(attractor->vy); + Serial.print("x:"); + Serial.print(attractor->x); + Serial.print("y:"); + Serial.println(attractor->y); PartSys->update(); // update and render return FRAMETIME; } diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index 940bccf9fc..087990a863 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -873,7 +873,7 @@ void ParticleSystem::renderParticle(PSparticle* particle, uint32_t brightess, in } Serial.println(" "); */ -/* + // debug: check coordinates if out of buffer boundaries print out some info for (uint32_t d = 0; d < 4; d++) { @@ -898,7 +898,7 @@ void ParticleSystem::renderParticle(PSparticle* particle, uint32_t brightess, in //Serial.print("^"); if (pixelvalues[d] >= 0) { - Serial.print("uncought out of bounds: x:"); + Serial.print("uncought out of bounds: y:"); Serial.print(pixelpositions[d][0]); Serial.print(" y:"); Serial.print(pixelpositions[d][1]); @@ -910,7 +910,7 @@ void ParticleSystem::renderParticle(PSparticle* particle, uint32_t brightess, in } } } -*/ + } // update & move particle, wraps around left/right if settings.wrapX is true, wrap around up/down if settings.wrapY is true diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index 2f16045565..72b2acc40e 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -106,8 +106,9 @@ typedef struct { } PSsource; // struct for PS settings -typedef struct +typedef union { + struct{ // add a one byte bit field: bool wrapX : 1; bool wrapY : 1; @@ -117,6 +118,8 @@ typedef struct bool useGravity : 1; //set to 1 if gravity is used, disables bounceY at the top bool useCollisions : 1; bool colorByAge : 1; // if set, particle hue is set by ttl value in render function + }; + byte asByte; } PSsettings; class ParticleSystem From e43f3bd5c612379c7004db005cd90e688f2036de Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Wed, 24 Apr 2024 17:33:02 +0200 Subject: [PATCH 063/219] bugfix in wrap function and firwork FX --- wled00/FX.cpp | 2 +- wled00/FXparticleSystem.cpp | 12 +++++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index a809d68085..f29b8ce825 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -8063,7 +8063,7 @@ uint16_t mode_particlefireworks(void) if (SEGMENT.call == 0) // initialization { - if (!initParticleSystem(PartSys, NUMBEROFSOURCES)) // init, no additional data needed + if (!initParticleSystem(PartSys, NUMBEROFSOURCES, true)) // init with advanced particle properties return mode_static(); // allocation failed; //allocation failed PartSys->setKillOutOfBounds(true); //out of bounds particles dont return (except on top, taken care of by gravity setting) PartSys->setWallHardness(100); //ground bounce is fixed diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index 087990a863..0277df3fa5 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -1207,18 +1207,20 @@ void ParticleSystem::collideParticles(PSparticle *particle1, PSparticle *particl } -//fast calculation of particle wraparound (modulo version takes 37 instructions, this only takes 28, other variants are slower on ESP8266) +//calculation of particle wraparound //function assumes that out of bounds is checked before calling it int32_t ParticleSystem::wraparound(int32_t p, int32_t maxvalue) { + /* + //variant without modulo (but is unsafe, far out particles will not get wrapped!) TODO: !!! remove this variant if (p < 0) - { p += maxvalue + 1; - } else //if (p > maxvalue) - { p -= maxvalue + 1; - } + return p;*/ + p = p % (maxvalue + 1); + if (p < 0) + p = maxvalue - p; return p; } From 3386a844eb156a1f82c2d25e67360b74241afbe2 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Fri, 26 Apr 2024 08:55:17 +0200 Subject: [PATCH 064/219] added local render blurring, boosting FPS, work in progress - changed fast-add function to use pointers - added fast-scaling function - added simple (but fast) 2D blurring function test shows that local blurring of full frame is almost double the speed (40FPS now is 80FPS). lots of comments still there --- wled00/FX.cpp | 140 +++++++++++++------------------ wled00/FXparticleSystem.cpp | 159 ++++++++++++++++++++++++++---------- wled00/FXparticleSystem.h | 4 +- 3 files changed, 178 insertions(+), 125 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index f29b8ce825..99a16518a9 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -8067,6 +8067,7 @@ uint16_t mode_particlefireworks(void) return mode_static(); // allocation failed; //allocation failed PartSys->setKillOutOfBounds(true); //out of bounds particles dont return (except on top, taken care of by gravity setting) PartSys->setWallHardness(100); //ground bounce is fixed + PartSys->setSaturation(0); //need to set saturation lower than 255 so rendering will use individual particle saturation numRockets = min(PartSys->numSources, (uint8_t)NUMBEROFSOURCES); for (j = 0; j < numRockets; j++) { @@ -8083,7 +8084,7 @@ uint16_t mode_particlefireworks(void) return mode_static(); // something went wrong, no data! } PartSys->updateSystem(); // update system properties (dimensions and data pointers) - numRockets = min(PartSys->numSources, (uint8_t)4); + numRockets = min(PartSys->numSources, (uint8_t)NUMBEROFSOURCES); PartSys->setWrapX(SEGMENT.check1); PartSys->setBounceY(SEGMENT.check2); @@ -8115,11 +8116,11 @@ uint16_t mode_particlefireworks(void) } else // speed is zero, explode! { -#ifdef ESP8266 + #ifdef ESP8266 emitparticles = random16(SEGMENT.intensity >> 3) + (SEGMENT.intensity >> 3) + 5; // defines the size of the explosion -#else + #else emitparticles = random16(SEGMENT.intensity >> 2) + (SEGMENT.intensity >> 2) + 5; // defines the size of the explosion -#endif + #endif PartSys->sources[j].source.vy = -1; // set speed negative so it will emit no more particles after this explosion until relaunch if(random16(4) == 0) { @@ -8192,13 +8193,13 @@ uint16_t mode_particlefireworks(void) else if ( PartSys->sources[j].source.vy < 0) // rocket is exploded and time is up (ttl=0 and negative speed), relaunch it { // reinitialize rocket - PartSys->sources[j].source.y = PS_P_RADIUS; // start from bottom + PartSys->sources[j].source.y = PS_P_RADIUS<<1; // start from bottom PartSys->sources[j].source.x = (rand() % (PartSys->maxX >> 1)) + (PartSys->maxX >> 2); // centered half PartSys->sources[j].source.vy = random16(SEGMENT.custom1 >> 3) + 5; // rocket speed depends also on rocket fuse PartSys->sources[j].source.vx = random16(5) - 2; //i.e. not perfectly straight up PartSys->sources[j].sat = 30; // low saturation -> exhaust is off-white PartSys->sources[j].source.ttl = random16(SEGMENT.custom1) + (SEGMENT.custom1 >> 1); // sets explosion height (rockets explode at the top if set too high as paticle update set speed to zero if moving out of matrix) - PartSys->sources[j].maxLife = 30; // exhaust particle life + PartSys->sources[j].maxLife = 40; // exhaust particle life PartSys->sources[j].minLife = 10; PartSys->sources[j].vx = 0; // emitting speed PartSys->sources[j].vy = 0; // emitting speed @@ -8210,7 +8211,7 @@ uint16_t mode_particlefireworks(void) return FRAMETIME; } #undef NUMBEROFSOURCES -static const char _data_FX_MODE_PARTICLEFIREWORKS[] PROGMEM = "PS Fireworks@Launches,Explosion Size,Fuse,Gravity,Cylinder,Ground,;;!;2;pal=11,sx=100,ix=50,c1=84,c2=128,c3=12,o1=0,o2=0,o3=0"; +static const char _data_FX_MODE_PARTICLEFIREWORKS[] PROGMEM = "PS Fireworks@Launches,Explosion Size,Fuse,,Gravity,Cylinder,Ground,;;!;2;pal=11,sx=100,ix=50,c1=84,c2=128,c3=12,o1=0,o2=0,o3=0"; /* * Particle Volcano @@ -8294,11 +8295,11 @@ uint16_t mode_particlevolcano(void) #undef NUMBEROFSOURCES static const char _data_FX_MODE_PARTICLEVOLCANO[] PROGMEM = "PS Volcano@Speed,Intensity,Move,Bounce,Spread,Color by Age,Walls,Collisions;;!;2;pal=35,sx=100,ix=190,c1=0,c2=160,c3=6,o1=1,o2=0,o3=0"; - /* - * Particle Fire - * realistic fire effect using particles. heat based and using perlin-noise for wind - * by DedeHai (Damian Schneider) - */ +/* +* Particle Fire +* realistic fire effect using particles. heat based and using perlin-noise for wind +* by DedeHai (Damian Schneider) +*/ uint16_t mode_particlefire(void) { if (SEGLEN == 1) @@ -8312,7 +8313,6 @@ uint16_t mode_particlefire(void) { if (!initParticleSystem(PartSys, 25, false, 4)) //maximum number of source (PS will determine the exact number based on segment size) and need 4 additional bytes for time keeping (uint32_t lastcall) return mode_static(); // allocation failed; //allocation failed - // Serial.println("fireinit done"); SEGMENT.aux0 = rand(); // aux0 is wind position (index) in the perlin noise // initialize the flame sprays numFlames = PartSys->numSources; @@ -8344,7 +8344,7 @@ uint16_t mode_particlefire(void) { uint32_t *lastcall = reinterpret_cast(PartSys->PSdataEnd); uint32_t period = strip.now - *lastcall; - if (period < map(SEGMENT.speed, 0, 99, 100, 12)) // limit to 80FPS - 10FPS + if (period < map(SEGMENT.speed, 0, 99, 50, 10)) // limit to 90FPS - 20FPS { SEGMENT.call--; //skipping a frame, decrement the counter (on call0, this is never executed as lastcall is 0, so its fine to not check if >0) return FRAMETIME; //do not update this frame @@ -8355,7 +8355,7 @@ uint16_t mode_particlefire(void) uint32_t spread = (PartSys->maxX >> 5) * (SEGMENT.custom3 + 1); //fire around segment center (in subpixel points) numFlames = min((uint32_t)PartSys->numSources, (1 + ((spread / PS_P_RADIUS) << 1))); // number of flames used depends on spread with, good value is (fire width in pixel) * 2 uint32_t percycle = numFlames*2/3;// / 2; // maximum number of particles emitted per cycle (TODO: for ESP826 maybe use flames/2) - // percycle = map(SEGMENT.intensity,0,255, 2, (numFlames*3) / 2); //TODO: will this give better flames or worse? + // percycle = map(SEGMENT.intensity,0,255, 2, (numFlames*3) / 2); //TODO: does this give better flames or worse? // update the flame sprays: for (i = 0; i < numFlames; i++) @@ -8371,16 +8371,12 @@ uint16_t mode_particlefire(void) { PartSys->sources[i].source.x = (PartSys->maxX >> 1) - (spread>>1) + (rand() % spread) ; // distribute randomly on chosen width } - PartSys->sources[i].source.y = -PS_P_RADIUS; // set the source below the frame - //PartSys->sources[i].source.ttl = 10 + random16((SEGMENT.custom1 * SEGMENT.custom1) >> 8) / (1 + (SEGMENT.speed >> 5)); //old not really good, too intense - PartSys->sources[i].source.ttl = 5 + random16((SEGMENT.custom1 * SEGMENT.custom1) >> 7) / (2 + (firespeed >> 4)); //'hotness' of fire, faster flames reduce the effect or flame height will scale too much with speed -> new, this works! - //PartSys->sources[i].source.ttl = 5 + random16(SEGMENT.custom1) / (1 + (firespeed >> 5)); // this is experimental, fine tuning all parameters + PartSys->sources[i].source.ttl = 5 + random16((SEGMENT.custom1 * SEGMENT.custom1) >> 7) / (2 + (firespeed >> 4)); //'hotness' of fire, faster flames reduce the effect or flame height will scale too much with speed -> new, this works! PartSys->sources[i].maxLife = random16(7) + 13; // defines flame height together with the vy speed, vy speed*maxlife/PS_P_RADIUS is the average flame height PartSys->sources[i].minLife = 4; PartSys->sources[i].vx = (int8_t)random16(4) - 2; // emitting speed (sideways) - PartSys->sources[i].vy = 5 + (firespeed >> 2); // emitting speed (upwards) -> this is good - //PartSys->sources[i].var = (random16(5) + 3) | 0x01; // speed variation around vx,vy (+/- var/2), only use odd numbers + PartSys->sources[i].vy = 5 + (firespeed >> 2); // emitting speed (upwards) -> this is good PartSys->sources[i].var = (random16(2 + (firespeed >> 5)) + 3) | 0x01; // speed variation around vx,vy (+/- var/2), only use odd numbers } @@ -8389,43 +8385,29 @@ uint16_t mode_particlefire(void) if (SEGMENT.call & 0x01) // update noise position every second frames, also add wind { SEGMENT.aux0++; // position in the perlin noise matrix for wind generation - if (SEGMENT.call & 0x02) //every tird frame + if (SEGMENT.call & 0x02) //every third frame SEGMENT.aux1++; // move in noise y direction so noise does not repeat as often - // add wind force to all particles - int8_t windspeed = ((int16_t)(inoise8(SEGMENT.aux0, SEGMENT.aux1) - 127) * SEGMENT.custom2) >> 12; + int8_t windspeed = ((int16_t)(inoise8(SEGMENT.aux0, SEGMENT.aux1) - 127) * SEGMENT.custom2) >> 7; PartSys->applyForce(windspeed, 0); } SEGMENT.step++; -//this is a work in progress... hard to find settings that look good TODO: looks ok on speed 130, need to tune it for other speeds - if (SEGMENT.check3) - { - if (SEGMENT.call % map(firespeed,0,255,4,15)==0) // update noise position every xth frames, also add wind -> has do be according to speed. 135-> every third frame + if (SEGMENT.check3) //add turbulance (parameters and algorithm found by experimentation) { - for (i = 0; i < PartSys->usedParticles; i++) + if (SEGMENT.call % map(firespeed,0,255,4,15)==0) { - //if (PartSys->particles[i].y > (PS_P_RADIUS << 1) && PartSys->particles[i].y < PartSys->maxY - (PS_P_RADIUS * 4)) // do not apply turbulance everywhere - if (PartSys->particles[i].y < PartSys->maxY/4)// - (PS_P_RADIUS * 4)) // do not apply turbulance everywhere -> bottom quarter seems a good balance + for (i = 0; i < PartSys->usedParticles; i++) { - //int32_t curl = ((int16_t)inoise8(PartSys->particles[i].x , PartSys->particles[i].y >> 1, SEGMENT.step<<2 ) - 127); - //int32_t curl = ((int16_t)inoise8(PartSys->particles[i].x, PartSys->particles[i].y , SEGMENT.step << 4) - 127); - int32_t curl = ((int32_t)inoise8(PartSys->particles[i].x, PartSys->particles[i].y , SEGMENT.step << 4) - 127); //-> this is good! - - //int32_t curl = ((int16_t)inoise8(PartSys->particles[i].x>>1, SEGMENT.step<<5) - 127); - // curl = ((curl * PartSys->particles[i].y) / PartSys->maxY); //'curl' stronger at the top - //int modulation = inoise8(SEGMENT.step<<3, SEGMENT.aux1) ; - //PartSys->particles[i].vx += curl>>2; - //PartSys->particles[i].vy += curl>>3; - //PartSys->particles[i].vx += (curl * ((SEGMENT.custom2 * modulation)>>7)) >> 9; - //PartSys->particles[i].vy += ((curl ) * ((SEGMENT.custom2 * modulation)>>7))>>10; - //PartSys->particles[i].vx += (curl * curl * (firespeed+10)) >> 14; //this may be a bad idea -> yes, squre is always positive... and a bit strong - PartSys->particles[i].vx += (curl * (firespeed + 10)) >> 9; //-> this is not too bad! - // PartSys->particles[i].vy += (curl * SEGMENT.custom2 ) >> 13; + if (PartSys->particles[i].y < PartSys->maxY/4) // do not apply turbulance everywhere -> bottom quarter seems a good balance + { + int32_t curl = ((int32_t)inoise8(PartSys->particles[i].x, PartSys->particles[i].y , SEGMENT.step << 4) - 127); + PartSys->particles[i].vx += (curl * (firespeed + 10)) >> 9; + + } } } } - } uint8_t j = random16(numFlames); // start with a random flame (so each flame gets the chance to emit a particle if available particles is smaller than number of flames) for(i=0; i < percycle; i++) @@ -8433,35 +8415,12 @@ uint16_t mode_particlefire(void) PartSys->flameEmit(PartSys->sources[j]); j = (j + 1) % numFlames; } -/* - j=5; - //a test: emitting one base particle per frame - if (SEGMENT.check1) - { - for (i = 0; i < PartSys->usedParticles; i++) // emit particles //todo: if this works, make it the last spray - { - if (PartSys->particles[i].ttl == 0) // find a dead particle - { - // emit particle at random position over the top of the matrix (random16 is not random enough) - PartSys->particles[i].vy = 1 ;//+ (firespeed >> 3); - PartSys->particles[i].ttl = 10;//(PS_P_RADIUS<<2) / PartSys->particles[i].vy; - PartSys->particles[i].x = (PartSys->maxX >> 1) - (spread>>1) + (rand() % spread) ; - PartSys->particles[i].y = 0; - PartSys->particles[i].vx = 0;//(((int16_t)random(SEGMENT.custom1)) - (SEGMENT.custom1 >> 1) + 5) >> 1; // side speed is +/- a quarter of the custom1 slider - Serial.print("*"); - j--; - } - if(j==0) break; - } - Serial.println("B"); - }*/ - PartSys->updateFire(SEGMENT.intensity); // update and render the fire return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEFIRE[] PROGMEM = "PS Fire@Speed,Intensity,Base Heat,Wind,Spread,Blur,Cylinder,Turbulence;;!;2;pal=35,sx=130,ix=120,c1=110,c2=128,c3=22,o1=0"; +static const char _data_FX_MODE_PARTICLEFIRE[] PROGMEM = "PS Fire@Speed,Intensity,Base Heat,Wind,Spread,Smooth,Cylinder,Turbulence;;!;2;pal=35,sx=110,c1=110,c2=50,o1=1"; /* PS Ballpit: particles falling down, user can enable these three options: X-wraparound, side bounce, ground bounce @@ -8536,7 +8495,23 @@ uint16_t mode_particlepit(void) if (SEGMENT.call % (3 + (SEGMENT.custom2 >> 2)) == 0) PartSys->applyFriction(frictioncoefficient); - // blur function that works: (for testing only) + //PartSys->setMotionBlur(8); //TODO: what happens with motion blur and frame blur? -> works, but needs to be kept at low value, below 128? it depends on particle size, need to adjust when particle size is set (or disable) but gives interesting effect... + //maybe better disable it. blur of 1 at size 255 does not motion blur at all, blur of 5 also not really, blur of 10 fills the frame -> TODO: disable it!!! + PartSys->setParticleSize(SEGMENT.custom1); + PartSys->update(); // update and render + +//Experiment: blur to grow the particles, also blur asymmetric, make the particles wobble: + /* + SEGMENT.blur(SEGMENT.custom1, true); + if (SEGMENT.custom1 > 64) + SEGMENT.blur(SEGMENT.custom1 - 64, true); + if (SEGMENT.custom1 > 128) + SEGMENT.blur((SEGMENT.custom1 - 128) << 1, true); + if (SEGMENT.custom1 > 192) + SEGMENT.blur((SEGMENT.custom1 - 192) << 1, true); + */ +/* +//wobbling static uint8_t testcntr; static uint8_t wobbleamount = 200; wobbleamount -= 2; @@ -8563,12 +8538,10 @@ uint16_t mode_particlepit(void) Serial.print(" "); Serial.print(ysize); - //PartSys->setParticleSize(SEGMENT.custom1); - PartSys->update(); // update and render const unsigned cols = PartSys->maxXpixel + 1; const unsigned rows = PartSys->maxYpixel + 1; - uint8_t xiterations = 1 + (xsize>>8); + uint8_t xiterations = 1 + (xsize>>8); //allow for wobble size > 255 uint8_t yiterations = 1 + (ysize>>8); uint8_t secondpassxsize = xsize - 255; uint8_t secondpassysize = ysize - 255; @@ -8610,6 +8583,9 @@ uint16_t mode_particlepit(void) // set size for second pass: ysize = secondpassysize; } +*/ + + /* //rotat image (just a test, non working yet) float angle = PI/3; @@ -8647,8 +8623,8 @@ uint16_t mode_particlepit(void) rotatedImage[rows - 1 - origY][cols - 1 - origX] = image[rows - 1 - y][cols - 1 - x]; } }*/ - return FRAMETIME; - } + return FRAMETIME; +} static const char _data_FX_MODE_PARTICLEPIT[] PROGMEM = "PS Ballpit@Speed,Intensity,Size,Hardness,Saturation,Cylinder,Walls,Ground;;!;2;pal=11,sx=100,ix=200,c1=120,c2=100,c3=31,o1=0,o2=0,o3=1"; /* @@ -9121,7 +9097,7 @@ uint16_t mode_particleattractor(void) PartSys->setColorByAge(SEGMENT.check1); if (SEGMENT.custom2 > 0) // collisions enabled - PartSys->enableParticleCollisions(true, SEGMENT.custom2); // enable collisions and set particle collision hardness + PartSys->enableParticleCollisions(true, map(SEGMENT.custom2, 1, 255, 120, 255)); // enable collisions and set particle collision hardness else PartSys->enableParticleCollisions(false); @@ -9138,14 +9114,16 @@ uint16_t mode_particleattractor(void) attractor->vy = PartSys->sources[0].source.vx; } //set attractor properties - if (SEGMENT.check2 && (SEGMENT.call % 2) == 0) // move attractor + if (SEGMENT.check2) { - - attractor->ttl = 255; //must be alive to move + if((SEGMENT.call % 3) == 0) // move slowly + { + attractor->ttl = 100; //must be alive to move PartSys->particleMoveUpdate(*attractor, sourcesettings); // move the attractor + } } else{ - attractor->x = PartSys->maxX >> 1; // center + attractor->x = PartSys->maxX >> 1; // set to center attractor->y = PartSys->maxY >> 1; } diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index 0277df3fa5..4a486dc68d 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -603,14 +603,13 @@ void ParticleSystem::ParticleSys_render(bool firemode, uint32_t fireintensity) // CRGB colorbuffer[maxXpixel+1][maxYpixel+1] = {0}; //put buffer on stack (not a good idea, can cause crashes on large segments if other function run the stack into the heap) if (useLocalBuffer) { - // allocate memory for the local renderbuffer + // allocate empty memory for the local renderbuffer colorbuffer = allocate2Dbuffer(maxXpixel + 1, maxYpixel + 1); if (colorbuffer == NULL) useLocalBuffer = false; //render to segment pixels directly if not enough memory if (motionBlur > 0) // using SEGMENT.fadeToBlackBy is much slower, this approximately doubles the speed of fade calculation { - uint32_t residual = motionBlur; //32bit for faster calculation uint32_t yflipped; for (int y = 0; y <= maxYpixel; y++) { @@ -618,9 +617,7 @@ void ParticleSystem::ParticleSys_render(bool firemode, uint32_t fireintensity) for (int x = 0; x <= maxXpixel; x++) { colorbuffer[x][y] = SEGMENT.getPixelColorXY(x, yflipped); - colorbuffer[x][y].r = (colorbuffer[x][y].r * residual) >> 8; - colorbuffer[x][y].g = (colorbuffer[x][y].g * residual) >> 8; - colorbuffer[x][y].b = (colorbuffer[x][y].b * residual) >> 8; + fast_color_scale(colorbuffer[x][y], motionBlur); } } } @@ -657,7 +654,7 @@ void ParticleSystem::ParticleSys_render(bool firemode, uint32_t fireintensity) { CHSV baseHSV = rgb2hsv_approximate(baseRGB); //convert to hsv if(advPartProps) - baseHSV.s = advPartProps->sat; + baseHSV.s = advPartProps[i].sat; else baseHSV.s = saturation; baseRGB = (CRGB)baseHSV; //convert back to RGB @@ -699,13 +696,13 @@ void ParticleSystem::ParticleSys_render(bool firemode, uint32_t fireintensity) if (useLocalBuffer) { if (pxlbrightness[0] > 0) - colorbuffer[pixco[0][0]][pixco[0][1]] = fast_color_add(colorbuffer[pixco[0][0]][pixco[0][1]], baseRGB, pxlbrightness[0]); // bottom left + fast_color_add(colorbuffer[pixco[0][0]][pixco[0][1]], baseRGB, pxlbrightness[0]); // bottom left if (pxlbrightness[1] > 0) - colorbuffer[pixco[1][0]][pixco[1][1]] = fast_color_add(colorbuffer[pixco[1][0]][pixco[1][1]], baseRGB, pxlbrightness[1]); // bottom right + fast_color_add(colorbuffer[pixco[1][0]][pixco[1][1]], baseRGB, pxlbrightness[1]); // bottom right if (pxlbrightness[2] > 0) - colorbuffer[pixco[2][0]][pixco[2][1]] = fast_color_add(colorbuffer[pixco[2][0]][pixco[2][1]], baseRGB, pxlbrightness[2]); // top right + fast_color_add(colorbuffer[pixco[2][0]][pixco[2][1]], baseRGB, pxlbrightness[2]); // top right if (pxlbrightness[3] > 0) - colorbuffer[pixco[3][0]][pixco[3][1]] = fast_color_add(colorbuffer[pixco[3][0]][pixco[3][1]], baseRGB, pxlbrightness[3]); // top left + fast_color_add(colorbuffer[pixco[3][0]][pixco[3][1]], baseRGB, pxlbrightness[3]); // top left } else { @@ -741,12 +738,27 @@ void ParticleSystem::ParticleSys_render(bool firemode, uint32_t fireintensity) { if (useLocalBuffer) { - //TODO: come up with a good and short 2D smearing function derived from blur - //put it in a function taking width, height and buffer pointer, so it can be used to blur individual particles for different sizes (will be slow) -> or maybe not, there is little use - // and would need individual sizes for each particle, + blur2D(colorbuffer, maxYpixel + 1, maxYpixel + 1, particlesize, particlesize); + if (particlesize > 64) + blur2D(colorbuffer, maxYpixel + 1, maxYpixel + 1, particlesize - 64, particlesize - 64); + if (particlesize > 128) + blur2D(colorbuffer, maxYpixel + 1, maxYpixel + 1, (particlesize - 128) << 1, (particlesize - 128) << 1); + if (particlesize > 192) + blur2D(colorbuffer, maxYpixel + 1, maxYpixel + 1, (particlesize - 192) << 1, (particlesize - 192) << 1); + + //Serial.println("blr"); } else - SEGMENT.blur(particlesize, true); //todo: come up with good algorithm for size + { + SEGMENT.blur(particlesize, true); + if (particlesize > 64) + SEGMENT.blur(particlesize - 64, true); + if (particlesize > 128) + SEGMENT.blur((particlesize - 128) << 1, true); + if (particlesize > 192) + SEGMENT.blur((particlesize - 192) << 1, true); + } + } if (useLocalBuffer) //transfer local buffer back to segment @@ -763,14 +775,7 @@ void ParticleSystem::ParticleSys_render(bool firemode, uint32_t fireintensity) free(colorbuffer); // free buffer memory } - // blur function that works: (for testing only) - SEGMENT.blur(particlesize, true); - if (particlesize > 64) - SEGMENT.blur(particlesize - 64, true); - if (particlesize > 128) - SEGMENT.blur((particlesize - 128) << 1, true); - if (particlesize > 192) - SEGMENT.blur((particlesize - 192) << 1, true); + } // calculate pixel positions and brightness distribution for rendering function @@ -1257,9 +1262,9 @@ int32_t ParticleSystem::limitSpeed(int32_t speed) // allocate memory for the 2D array in one contiguous block and set values to zero CRGB **ParticleSystem::allocate2Dbuffer(uint32_t cols, uint32_t rows) { - cli();//!!! test to see if anything messes with the allocation (flicker issues) + //cli();//!!! test to see if anything messes with the allocation (flicker issues) CRGB ** array2D = (CRGB **)malloc(cols * sizeof(CRGB *) + cols * rows * sizeof(CRGB)); - sei(); + //sei(); if (array2D == NULL) DEBUG_PRINT(F("PS buffer alloc failed")); else @@ -1282,11 +1287,8 @@ void ParticleSystem::updateSystem(void) // update matrix size uint32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; uint32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); - setMatrixSize(cols, rows); - bool isadvanced = false; - if (advPartProps) //if pointer was previously set i.e. non NULL (pinter is only NULL if never set, even if segment is copied) - isadvanced = true; - updatePSpointers(isadvanced); + setMatrixSize(cols, rows); + updatePSpointers(advPartProps != NULL); } // set the pointers for the class (this only has to be done once and not on every FX call, only the class pointer needs to be reassigned to SEGENV.data every time) @@ -1397,29 +1399,100 @@ bool initParticleSystem(ParticleSystem *&PartSys, uint8_t requestedsources, bool // fastled color adding is very inaccurate in color preservation // a better color add function is implemented in colors.cpp but it uses 32bit RGBW. to use it colors need to be shifted just to then be shifted back by that function, which is slow // this is a fast version for RGB (no white channel, PS does not handle white) and with native CRGB including scaling of second color (fastled scale8 can be made faster using native 32bit on ESP) -CRGB fast_color_add(CRGB c1, CRGB c2, uint32_t scale) +// note: result is stored in c1, so c1 will contain the result. not using a return value is much faster as the struct does not need to be copied upon return +void fast_color_add(CRGB &c1, CRGB &c2, uint32_t scale) { - CRGB result; - scale++; //add one to scale so 255 will not scale when shifting - uint32_t r = c1.r + ((c2.r * (scale)) >> 8); - uint32_t g = c1.g + ((c2.g * (scale)) >> 8); - uint32_t b = c1.b + ((c2.b * (scale)) >> 8); + uint32_t r, g, b; + if(scale < 255) + { + r = c1.r + ((c2.r * scale) >> 8); + g = c1.g + ((c2.g * scale) >> 8); + b = c1.b + ((c2.b * scale) >> 8); + } + else{ + r = c1.r + c2.r; + g = c1.g + c2.g; + b = c1.b + c2.b; + } uint32_t max = r; - if (g > max) //note: using ? operator would be slower by 2 cpu cycles + if (g > max) //note: using ? operator would be slower by 2 instructions max = g; if (b > max) max = b; if (max < 256) { - result.r = r; - result.g = g; - result.b = b; + c1.r = r; //save result to c1 + c1.g = g; + c1.b = b; } else { - result.r = (r * 255) / max; - result.g = (g * 255) / max; - result.b = (b * 255) / max; + c1.r = (r * 255) / max; + c1.g = (g * 255) / max; + c1.b = (b * 255) / max; } - return result; } + +//faster than fastled color scaling as it uses a 32bit scale factor and pointer +void fast_color_scale(CRGB &c, uint32_t scale) +{ + c.r = ((c.r * scale) >> 8); + c.g = ((c.g * scale) >> 8); + c.b = ((c.b * scale) >> 8); +} + + + +//blur a matrix in x and y direction, blur can be asymmetric in x and y +//for speed, 32bit variables are used, make sure to limit them to 8bit (0-255) +//note: classic WLED blurrint is not (yet) supportet, this is a smearing function that does not fade original image, just blurrs it +void blur2D(CRGB **colorbuffer, uint32_t xsize, uint32_t ysize, uint32_t xblur, uint32_t yblur, bool particleblur) +{ + //uint32_t keep = smear ? 255 : 255 - xblur; + CRGB seeppart, carryover; + uint32_t seep = xblur >> 1; + uint32_t startrow = 0; + uint32_t endrow = ysize; + if(particleblur) //blurring a single particle, can skip first and last row in x blurring as it is black + { + startrow++; + endrow--; + } + + for(uint32_t y = startrow; y < endrow; y++) + { + //fast_color_scale(colorbuffer[0][y], keep); //first pixel is just faded, carryover is black (this only needs to be done if smear is false) + carryover = BLACK; + for(uint32_t x = 0; x < xsize; x++) + { + seeppart = colorbuffer[x][y]; //create copy of current color + fast_color_scale(seeppart, seep); //scale it and seep to neighbours + if(x > 0) + { + fast_color_add(colorbuffer[x-1][y], seeppart); + fast_color_add(colorbuffer[x][y], carryover); //todo: could check if carryover is > 0, takes 7 instructions, add takes ~35, with lots of same color pixels (like background), it would be faster + } + carryover = seeppart; + } + fast_color_add(colorbuffer[xsize-1][y], carryover); // set last pixel + } + + //uint32_t keep = smear ? 255 : 255 - xblur; + seep = yblur >> 1; + for(uint32_t x = 0; x < xsize; x++) + { + carryover = BLACK; + for(uint32_t y = 0; y < ysize; y++) + { + seeppart = colorbuffer[x][y]; //create copy of current color + fast_color_scale(seeppart, seep); //scale it and seep to neighbours + if(y > 0) + { + fast_color_add(colorbuffer[x][y-1], seeppart); + fast_color_add(colorbuffer[x][y], carryover); //todo: could check if carryover is > 0, takes 7 instructions, add takes ~35, with lots of same color pixels (like background), it would be faster + } + carryover = seeppart; + } + fast_color_add(colorbuffer[x][ysize-1], carryover); // set last pixel + } +} \ No newline at end of file diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index 72b2acc40e..2c90b9e53d 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -219,4 +219,6 @@ uint32_t calculateNumberOfParticles(bool advanced); uint32_t calculateNumberOfSources(uint8_t requestedsources); bool allocateParticleSystemMemory(uint16_t numparticles, uint16_t numsources, bool advanced, uint16_t additionalbytes); //color add function -CRGB fast_color_add(CRGB c1, CRGB c2, uint32_t scale); // fast and accurate color adding with scaling (scales c2 before adding) \ No newline at end of file +void fast_color_add(CRGB &c1, CRGB &c2, uint32_t scale = 255); // fast and accurate color adding with scaling (scales c2 before adding) +void fast_color_scale(CRGB &c, uint32_t scale); //fast scaling function using 32bit factor (keep it 0-255) and pointer +void blur2D(CRGB **colorbuffer, uint32_t xsize, uint32_t ysize, uint32_t xblur, uint32_t yblur, bool particleblur = false); \ No newline at end of file From 54e94ddb91554302d7b5717a01ac99f3b7089970 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sat, 27 Apr 2024 13:18:30 +0200 Subject: [PATCH 065/219] Bugfixes, improvements and added wall roughness setting - fixed bug in PS fuzzy noise which made it asymmetric for some reason, seems to work better now - added particle size option to attractor but had to remove speed setting (now fixed emit speed) - some parameter tuning of FX - improvements to code size in render function - added smear option to blurring (not tested much, may be buggy without smear) - speed improvement to caldForce_dv, added zero checking. --- wled00/FX.cpp | 94 +++++++++++++------------ wled00/FXparticleSystem.cpp | 136 +++++++++++++++++++----------------- wled00/FXparticleSystem.h | 11 +-- 3 files changed, 127 insertions(+), 114 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 99a16518a9..8e4ec1c941 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -7929,7 +7929,7 @@ uint16_t mode_particlevortex(void) PartSys->setKillOutOfBounds(true); } else - PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + PartSys = reinterpret_cast(SEGMENT.data); // if not first call, just set the pointer to the PS if (PartSys == NULL) { @@ -8025,7 +8025,7 @@ uint16_t mode_particlevortex(void) uint32_t skip = PS_P_HALFRADIUS/SEGMENT.intensity + 1; if (SEGMENT.call % skip == 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. + j = random(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) { PartSys->sources[j].var = (SEGMENT.custom3 >> 1); //update speed variation @@ -8076,7 +8076,7 @@ uint16_t mode_particlefireworks(void) } } else - PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + PartSys = reinterpret_cast(SEGMENT.data); // if not first call, just set the pointer to the PS if (PartSys == NULL) { @@ -8196,7 +8196,7 @@ uint16_t mode_particlefireworks(void) PartSys->sources[j].source.y = PS_P_RADIUS<<1; // start from bottom PartSys->sources[j].source.x = (rand() % (PartSys->maxX >> 1)) + (PartSys->maxX >> 2); // centered half PartSys->sources[j].source.vy = random16(SEGMENT.custom1 >> 3) + 5; // rocket speed depends also on rocket fuse - PartSys->sources[j].source.vx = random16(5) - 2; //i.e. not perfectly straight up + PartSys->sources[j].source.vx = random(5) - 2; //i.e. not perfectly straight up PartSys->sources[j].sat = 30; // low saturation -> exhaust is off-white PartSys->sources[j].source.ttl = random16(SEGMENT.custom1) + (SEGMENT.custom1 >> 1); // sets explosion height (rockets explode at the top if set too high as paticle update set speed to zero if moving out of matrix) PartSys->sources[j].maxLife = 40; // exhaust particle life @@ -8231,10 +8231,11 @@ uint16_t mode_particlevolcano(void) if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. { if (!initParticleSystem(PartSys, NUMBEROFSOURCES)) // init, no additional data needed - return mode_static(); // allocation failed; //allocation failed + return mode_static(); // allocation failed PartSys->setBounceY(true); PartSys->setGravity(); // enable with default gforce - PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting) + PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting) + PartSys->setMotionBlur(190); //anable motion blur numSprays = min(PartSys->numSources, (uint8_t)NUMBEROFSOURCES); // number of sprays for (i = 0; i < numSprays; i++) { @@ -8247,7 +8248,7 @@ uint16_t mode_particlevolcano(void) } } else - PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + PartSys = reinterpret_cast(SEGMENT.data); // if not first call, just set the pointer to the PS if (PartSys == NULL) { @@ -8327,7 +8328,7 @@ uint16_t mode_particlefire(void) DEBUG_PRINTF_P(PSTR("segment data ptr in fireFX %p\n"), SEGMENT.data); } else - PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + PartSys = reinterpret_cast(SEGMENT.data); // if not first call, just set the pointer to the PS if (PartSys == NULL) { @@ -8375,7 +8376,7 @@ uint16_t mode_particlefire(void) PartSys->sources[i].source.ttl = 5 + random16((SEGMENT.custom1 * SEGMENT.custom1) >> 7) / (2 + (firespeed >> 4)); //'hotness' of fire, faster flames reduce the effect or flame height will scale too much with speed -> new, this works! PartSys->sources[i].maxLife = random16(7) + 13; // defines flame height together with the vy speed, vy speed*maxlife/PS_P_RADIUS is the average flame height PartSys->sources[i].minLife = 4; - PartSys->sources[i].vx = (int8_t)random16(4) - 2; // emitting speed (sideways) + PartSys->sources[i].vx = (int8_t)random(4) - 2; // emitting speed (sideways) PartSys->sources[i].vy = 5 + (firespeed >> 2); // emitting speed (upwards) -> this is good PartSys->sources[i].var = (random16(2 + (firespeed >> 5)) + 3) | 0x01; // speed variation around vx,vy (+/- var/2), only use odd numbers } @@ -8409,11 +8410,11 @@ uint16_t mode_particlefire(void) } } - uint8_t j = random16(numFlames); // start with a random flame (so each flame gets the chance to emit a particle if available particles is smaller than number of flames) + uint8_t j = random16(); // start with a random flame (so each flame gets the chance to emit a particle if available particles is smaller than number of flames) for(i=0; i < percycle; i++) { - PartSys->flameEmit(PartSys->sources[j]); j = (j + 1) % numFlames; + PartSys->flameEmit(PartSys->sources[j]); } PartSys->updateFire(SEGMENT.intensity); // update and render the fire @@ -8444,7 +8445,7 @@ uint16_t mode_particlepit(void) PartSys->setUsedParticles((PartSys->numParticles*3)/2); //use 2/3 of available particles } else - PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + PartSys = reinterpret_cast(SEGMENT.data); // if not first call, just set the pointer to the PS if (PartSys == NULL) { @@ -8466,8 +8467,8 @@ uint16_t mode_particlepit(void) PartSys->setSaturation(((SEGMENT.custom3) << 3) + 7); // set global rendering saturation so individual saturation rendering is enabled - uint32_t i; // index variable - if (SEGMENT.call % (64 - (SEGMENT.intensity >> 2)) == 0 && SEGMENT.intensity > 1) // every nth frame emit particles, stop emitting if set to zero + uint32_t i; + if (SEGMENT.call % (128 - (SEGMENT.intensity >> 1)) == 0 && SEGMENT.intensity > 0) // every nth frame emit particles, stop emitting if set to zero { for (i = 0; i < PartSys->usedParticles; i++) // emit particles { @@ -8477,7 +8478,7 @@ uint16_t mode_particlepit(void) PartSys->particles[i].ttl = 1500 - (SEGMENT.speed << 2) + random16(500); // if speed is higher, make them die sooner PartSys->particles[i].x = random(PartSys->maxX >> 1) + (PartSys->maxX >> 2); PartSys->particles[i].y = (PartSys->maxY<<1); // particles appear somewhere above the matrix, maximum is double the height - PartSys->particles[i].vx = (int16_t)random(100) - 50; // side speed is +/- + PartSys->particles[i].vx = (int16_t)random(SEGMENT.speed >> 1) - (SEGMENT.speed >> 2); // side speed is +/- PartSys->particles[i].vy = map(SEGMENT.speed, 0, 255, -5, -100); // downward speed PartSys->particles[i].hue = random16(); // set random color PartSys->particles[i].collide = true; //enable collision for particle @@ -8492,11 +8493,11 @@ uint16_t mode_particlepit(void) if (SEGMENT.speed < 50) // for low speeds, apply more friction frictioncoefficient = 50 - SEGMENT.speed; - if (SEGMENT.call % (3 + (SEGMENT.custom2 >> 2)) == 0) + //if (SEGMENT.call % (3 + (SEGMENT.custom2 >> 2)) == 0) + if (SEGMENT.call % (3 + (SEGMENT.speed >> 2)) == 0) PartSys->applyFriction(frictioncoefficient); - //PartSys->setMotionBlur(8); //TODO: what happens with motion blur and frame blur? -> works, but needs to be kept at low value, below 128? it depends on particle size, need to adjust when particle size is set (or disable) but gives interesting effect... - //maybe better disable it. blur of 1 at size 255 does not motion blur at all, blur of 5 also not really, blur of 10 fills the frame -> TODO: disable it!!! + PartSys->setParticleSize(SEGMENT.custom1); PartSys->update(); // update and render @@ -8665,7 +8666,7 @@ uint16_t mode_particlewaterfall(void) } } else - PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + PartSys = reinterpret_cast(SEGMENT.data); // if not first call, just set the pointer to the PS if (PartSys == NULL) { @@ -8737,7 +8738,7 @@ uint16_t mode_particlebox(void) return mode_static(); // allocation failed; //allocation failed } else - PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + PartSys = reinterpret_cast(SEGMENT.data); // if not first call, just set the pointer to the PS if (PartSys == NULL) { @@ -8849,7 +8850,7 @@ uint16_t mode_particleperlin(void) } } else - PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + PartSys = reinterpret_cast(SEGMENT.data); // if not first call, just set the pointer to the PS if (PartSys == NULL) { @@ -8864,28 +8865,27 @@ uint16_t mode_particleperlin(void) PartSys->enableParticleCollisions(SEGMENT.check3, SEGMENT.custom1); // enable collisions and set particle collision hardness uint32_t displayparticles = map(SEGMENT.intensity, 0, 255, 10, PartSys->numParticles>>1); PartSys->setUsedParticles(displayparticles); - PartSys->setMotionBlur(200); // anable motion blur + PartSys->setMotionBlur(230); // anable motion blur // apply 'gravity' from a 2D perlin noise map SEGMENT.aux0 += 1+(SEGMENT.speed >> 5); // noise z-position - // update position in noise for (i = 0; i < displayparticles; i++) { if (PartSys->particles[i].ttl == 0) // revive dead particles (do not keep them alive forever, they can clump up, need to reseed) { PartSys->particles[i].ttl = random16(500) + 200; - PartSys->particles[i].x = random16(PartSys->maxX); - PartSys->particles[i].y = random16(PartSys->maxY); + PartSys->particles[i].x = random(PartSys->maxX); + PartSys->particles[i].y = random(PartSys->maxY); } uint32_t scale = 16 - ((31 - SEGMENT.custom3) >> 1); uint16_t xnoise = PartSys->particles[i].x / scale; // position in perlin noise, scaled by slider uint16_t ynoise = PartSys->particles[i].y / scale; int16_t baseheight = inoise8(xnoise, ynoise, SEGMENT.aux0); // noise value at particle position PartSys->particles[i].hue = baseheight; // color particles to perlin noise value - if (SEGMENT.call % 3 == 0) // do not apply the force every frame, is too chaotic + if (SEGMENT.call % 8 == 0) // do not apply the force every frame, is too chaotic { - int8_t xslope = (baseheight - (int16_t)inoise8(xnoise + 10, ynoise, SEGMENT.aux0)); - int8_t yslope = (baseheight - (int16_t)inoise8(xnoise, ynoise + 10, SEGMENT.aux0)); + int8_t xslope = (baseheight + (int16_t)inoise8(xnoise - 10, ynoise, SEGMENT.aux0)); + int8_t yslope = (baseheight + (int16_t)inoise8(xnoise, ynoise - 10, SEGMENT.aux0)); PartSys->applyForce(i, xslope, yslope); } } @@ -8896,7 +8896,7 @@ uint16_t mode_particleperlin(void) PartSys->update(); // update and render return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEPERLIN[] PROGMEM = "PS Fuzzy Noise@Speed,Particles,Bounce,Friction,Scale,Cylinder,,Collisions;;!;2;pal=54,sx=75,ix=200,c1=130,c2=30,c3=5,o1=0,o3=1"; +static const char _data_FX_MODE_PARTICLEPERLIN[] PROGMEM = "PS Fuzzy Noise@Speed,Particles,Bounce,Friction,Scale,Cylinder,,Collisions;;!;2;pal=64,sx=50,ix=200,c1=130,c2=30,c3=5,o1=0,o3=1"; /* * Particle smashing down like meteors and exploding as they hit the ground, has many parameters to play with @@ -8921,8 +8921,7 @@ uint16_t mode_particleimpact(void) return mode_static(); // allocation failed; //allocation failed PartSys->setKillOutOfBounds(false); //explosions out of frame ar allowed, set to true to save particles (TODO: better enable it in ESP8266?) PartSys->setGravity(); //enable default gravity - PartSys->setBounceY(true); //always use ground bounce - // PartSys->setUsedParticles((PartSys->numParticles * 3) / 2); // use 2/3 of available particles + PartSys->setBounceY(true); //always use ground bounce MaxNumMeteors = min(PartSys->numSources, (uint8_t)NUMBEROFSOURCES); for (i = 0; i < MaxNumMeteors; i++) { @@ -8933,7 +8932,7 @@ uint16_t mode_particleimpact(void) } } else - PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + PartSys = reinterpret_cast(SEGMENT.data); // if not first call, just set the pointer to the PS if (PartSys == NULL) { @@ -9018,9 +9017,9 @@ uint16_t mode_particleimpact(void) { // reinitialize meteor PartSys->sources[i].source.y = PartSys->maxY + (PS_P_RADIUS << 2); // start 4 pixels above the top - PartSys->sources[i].source.x = random16(PartSys->maxX); + PartSys->sources[i].source.x = random(PartSys->maxX); PartSys->sources[i].source.vy = -random16(30) - 30; // meteor downward speed - PartSys->sources[i].source.vx = random16(30) - 15; + PartSys->sources[i].source.vx = random(30) - 15; PartSys->sources[i].source.hue = random16(); // random color PartSys->sources[i].source.ttl = 2000; // long life, will explode at bottom PartSys->sources[i].source.collide = false; // trail particles will not collide @@ -9084,7 +9083,7 @@ uint16_t mode_particleattractor(void) PartSys->setWallHardness(255); //bounce forever } else - PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + PartSys = reinterpret_cast(SEGMENT.data); // if not first call, just set the pointer to the PS if (PartSys == NULL) { @@ -9095,13 +9094,14 @@ uint16_t mode_particleattractor(void) PartSys->updateSystem(); // update system properties (dimensions and data pointers) PartSys->setColorByAge(SEGMENT.check1); + PartSys->setParticleSize(SEGMENT.custom1 >> 1); if (SEGMENT.custom2 > 0) // collisions enabled PartSys->enableParticleCollisions(true, map(SEGMENT.custom2, 1, 255, 120, 255)); // enable collisions and set particle collision hardness else PartSys->enableParticleCollisions(false); - uint16_t lastusedparticle = (PartSys->numParticles * 2) / 3; + uint16_t lastusedparticle = (PartSys->numParticles * 3) >> 2; //use 3/4 of particles uint32_t displayparticles = map(SEGMENT.intensity, 0, 255, 10, lastusedparticle); PartSys->setUsedParticles(displayparticles); @@ -9135,9 +9135,11 @@ uint16_t mode_particleattractor(void) SEGMENT.aux0 += 256; //emitting angle, one full turn in 255 frames (0xFFFF is 360°) if (SEGMENT.call % 2 == 0) // alternate direction of emit - PartSys->angleEmit(PartSys->sources[0], SEGMENT.aux0, SEGMENT.custom1 >> 4); + PartSys->angleEmit(PartSys->sources[0], SEGMENT.aux0, 12); + //PartSys->angleEmit(PartSys->sources[0], SEGMENT.aux0, SEGMENT.custom1 >> 4); else - PartSys->angleEmit(PartSys->sources[0], SEGMENT.aux0 + 0x7FFF, SEGMENT.custom1 >> 4); // emit at 180° as well + PartSys->angleEmit(PartSys->sources[0], SEGMENT.aux0 + 0x7FFF, 12); // emit at 180° as well + //PartSys->angleEmit(PartSys->sources[0], SEGMENT.aux0 + 0x7FFF, SEGMENT.custom1 >> 4); // emit at 180° as well // apply force for(i = 0; i < displayparticles; i++) @@ -9151,7 +9153,7 @@ uint16_t mode_particleattractor(void) PartSys->update(); // update and render return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEATTRACTOR[] PROGMEM = "PS Attractor@Mass,Particles,Emit Speed,Collisions,Friction,Color by Age,Move,Swallow;;!;2;pal=9,sx=100,ix=82,c1=190,c2=0,o1=0,o2=0,o3=0"; +static const char _data_FX_MODE_PARTICLEATTRACTOR[] PROGMEM = "PS Attractor@Mass,Particles,Particle Size,Collisions,Friction,Color by Age,Move,Swallow;;!;2;pal=9,sx=100,ix=82,c1=0,c2=0,o1=0,o2=0,o3=0"; /* @@ -9198,7 +9200,7 @@ uint16_t mode_particleattractor(void) PartSys->sources[0].var = 7; // emiting variation } else - PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + PartSys = reinterpret_cast(SEGMENT.data); // if not first call, just set the pointer to the PS if (PartSys == NULL) { @@ -9300,7 +9302,7 @@ uint16_t mode_particlespray(void) } else - PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + PartSys = reinterpret_cast(SEGMENT.data); // if not first call, just set the pointer to the PS if (PartSys == NULL) { @@ -9360,7 +9362,7 @@ uint16_t mode_particleGEQ(void) PartSys->setUsedParticles((PartSys->numParticles * 3) / 2); // use 2/3 of available particles } else - PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + PartSys = reinterpret_cast(SEGMENT.data); // if not first call, just set the pointer to the PS if (PartSys == NULL) { @@ -9420,11 +9422,11 @@ uint16_t mode_particleGEQ(void) { if (PartSys->particles[i].ttl == 0) // find a dead particle { - //set particle properties + //set particle properties TODO: could also use the spray... PartSys->particles[i].ttl = 20 + map(SEGMENT.intensity, 0,255, emitspeed>>1, emitspeed + random16(emitspeed)) ; // set particle alive, particle lifespan is in number of frames PartSys->particles[i].x = xposition + random16(binwidth) - (binwidth>>1); //position randomly, deviating half a bin width PartSys->particles[i].y = PS_P_RADIUS; //tart at the bottom (PS_P_RADIUS is minimum position a particle is fully in frame) - PartSys->particles[i].vx = random16(SEGMENT.custom1>>1)-(SEGMENT.custom1>>2) ; //x-speed variation: +/- custom1/4 + PartSys->particles[i].vx = random16(SEGMENT.custom1>>1)-(SEGMENT.custom1>>2) ; //x-speed variation: +/- custom1/4 TODO: ok to use random16 here? PartSys->particles[i].vy = emitspeed; PartSys->particles[i].hue = (bin<<4) + random16(17) - 8; // color from palette according to bin emitparticles--; @@ -9469,10 +9471,10 @@ uint16_t mode_particlecenterGEQ(void) // 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(PSsource) * (numSprays); - if (!SEGENV.allocateData(dataSize)) + if (!SEGMENT.allocateData(dataSize)) return mode_static(); // allocation failed; //allocation failed - spray = reinterpret_cast(SEGENV.data); + spray = reinterpret_cast(SEGMENT.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 diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index 4a486dc68d..effda26010 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -56,6 +56,7 @@ ParticleSystem::ParticleSystem(uint16_t width, uint16_t height, uint16_t numbero updatePSpointers(isadvanced); // set the particle and sources pointer (call this before accessing sprays or particles) setMatrixSize(width, height); setWallHardness(255); // set default wall hardness to max + setWallRoughness(200); // smooth walls by default !!! testing this setGravity(0); //gravity disabled by default setSaturation(255); //full saturation by default setParticleSize(0); // minimum size by default @@ -129,6 +130,11 @@ void ParticleSystem::setWallHardness(uint8_t hardness) wallHardness = hardness; } +void ParticleSystem::setWallRoughness(uint8_t roughness) +{ + wallRoughness = roughness; +} + void ParticleSystem::setCollisionHardness(uint8_t hardness) { collisionHardness = hardness; @@ -179,14 +185,16 @@ void ParticleSystem::setColorByAge(bool enable) void ParticleSystem::setMotionBlur(uint8_t bluramount) { - motionBlur = bluramount; + if(particlesize == 0) //only allwo motion blurring on default particle size + motionBlur = bluramount; } // render size using smearing void ParticleSystem::setParticleSize(uint8_t size) { particlesize = size; - particleHardRadius = max(PS_P_MINHARDRADIUS, (int)particlesize); + particleHardRadius = max(PS_P_MINHARDRADIUS, (int)particlesize); + motionBlur = 0; //disable motion blur if particle size is set } // enable/disable gravity, optionally, set the force (force=8 is default) can be 1-255, 0 is disable // if enabled, gravity is applied to all particles in ParticleSystemUpdate() @@ -302,15 +310,8 @@ void ParticleSystem::particleMoveUpdate(PSparticle &part, PSsettings &options) //if wall collisions are enabled, bounce them before they reach the edge, it looks much nicer if the particle is not half out of vew if (options.bounceX) { - if ((newX < particleHardRadius) || (newX > maxX - particleHardRadius)) // reached a wall - { - part.vx = -part.vx; // invert speed - part.vx = (part.vx * wallHardness) / 255; // reduce speed as energy is lost on non-hard surface - if (newX < particleHardRadius) - newX = particleHardRadius; // fast particles will never reach the edge if position is inverted - else - newX = maxX - particleHardRadius; - } + if ((newX < particleHardRadius) || (newX > maxX - particleHardRadius)) // reached a wall + bounce(part.vx, part.vy, newX, maxX); } if ((newX < 0) || (newX > maxX)) // check if particle reached an edge @@ -332,11 +333,7 @@ void ParticleSystem::particleMoveUpdate(PSparticle &part, PSsettings &options) if ((newY < particleHardRadius) || (newY > maxY - particleHardRadius)) // reached floor / ceiling { if (newY < particleHardRadius) // bounce at bottom - { - part.vy = -part.vy; // invert speed - part.vy = ((int32_t)part.vy * wallHardness) / 255; // reduce speed as energy is lost on non-hard surface - newY = particleHardRadius; - } + bounce(part.vy, part.vx, newY, maxY); else { if (options.useGravity) // do not bounce on top if using gravity (open container) if this is needed implement it in the FX @@ -346,9 +343,7 @@ void ParticleSystem::particleMoveUpdate(PSparticle &part, PSsettings &options) } else { - part.vy = -part.vy; // invert speed - part.vy = ((int32_t)part.vy * wallHardness) / 255; // reduce speed as energy is lost on non-hard surface - newY = maxY - particleHardRadius; + bounce(part.vy, part.vx, newY, maxY); } } } @@ -379,6 +374,27 @@ void ParticleSystem::particleMoveUpdate(PSparticle &part, PSsettings &options) } } +//function to bounce a particle from a wall using set parameters (wallHardness and wallRoughness) +void ParticleSystem::bounce(int8_t &incomingspeed, int8_t ¶llelspeed, int32_t &position, uint16_t maxposition) +{ + incomingspeed = -incomingspeed; // invert speed + incomingspeed = (incomingspeed * wallHardness) / 255; // reduce speed as energy is lost on non-hard surface + if (position < particleHardRadius) + position = particleHardRadius; // fast particles will never reach the edge if position is inverted + else + position = maxposition - particleHardRadius; + if(wallRoughness) + { + //transfer an amount of incomingspeed speed to parallel speed + int32_t donatespeed = abs(incomingspeed); + donatespeed = ((random(donatespeed << 1) - donatespeed) * wallRoughness) / 255; //take random portion of + or - x speed, scaled by roughness + parallelspeed += donatespeed; + donatespeed = abs(donatespeed); + incomingspeed -= incomingspeed > 0 ? donatespeed : -donatespeed; + } + +} + // apply a force in x,y direction to individual particle // caller needs to provide a 8bit counter (for each paticle) that holds its value between calls // 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) @@ -616,7 +632,7 @@ void ParticleSystem::ParticleSys_render(bool firemode, uint32_t fireintensity) yflipped = maxYpixel - y; for (int x = 0; x <= maxXpixel; x++) { - colorbuffer[x][y] = SEGMENT.getPixelColorXY(x, yflipped); + colorbuffer[x][y] = SEGMENT.getPixelColorXY(x, yflipped); //copy to local buffer fast_color_scale(colorbuffer[x][y], motionBlur); } } @@ -695,42 +711,19 @@ void ParticleSystem::ParticleSys_render(bool firemode, uint32_t fireintensity) }*/ if (useLocalBuffer) { - if (pxlbrightness[0] > 0) - fast_color_add(colorbuffer[pixco[0][0]][pixco[0][1]], baseRGB, pxlbrightness[0]); // bottom left - if (pxlbrightness[1] > 0) - fast_color_add(colorbuffer[pixco[1][0]][pixco[1][1]], baseRGB, pxlbrightness[1]); // bottom right - if (pxlbrightness[2] > 0) - fast_color_add(colorbuffer[pixco[2][0]][pixco[2][1]], baseRGB, pxlbrightness[2]); // top right - if (pxlbrightness[3] > 0) - fast_color_add(colorbuffer[pixco[3][0]][pixco[3][1]], baseRGB, pxlbrightness[3]); // top left + for(uint32_t i = 0; i < 4; i ++) + { + if (pxlbrightness[i] > 0) + fast_color_add(colorbuffer[pixco[i][0]][pixco[i][1]], baseRGB, pxlbrightness[i]); // order is: bottom left, bottom right, top right, top left + } } else - { - if (pxlbrightness[0] > 0) - SEGMENT.addPixelColorXY(pixco[0][0], maxYpixel - pixco[0][1], baseRGB.scale8((uint8_t)pxlbrightness[0])); // bottom left - if (pxlbrightness[1] > 0) - SEGMENT.addPixelColorXY(pixco[1][0], maxYpixel - pixco[1][1], baseRGB.scale8((uint8_t)pxlbrightness[1])); // bottom right - if (pxlbrightness[2] > 0) - SEGMENT.addPixelColorXY(pixco[2][0], maxYpixel - pixco[2][1], baseRGB.scale8((uint8_t)pxlbrightness[2])); // top right - if (pxlbrightness[3] > 0) - SEGMENT.addPixelColorXY(pixco[3][0], maxYpixel - pixco[3][1], baseRGB.scale8((uint8_t)pxlbrightness[3])); // top left - /* - uint32_t color = RGBW32(baseRGB.r, baseRGB.g, baseRGB.b, 0); - if (pxlbrightness[0] > 0) - SEGMENT.addPixelColorXY(pixco[0][0], maxYpixel - pixco[0][1], color_scale(color, pxlbrightness[0])); // bottom left - if (pxlbrightness[1] > 0) - SEGMENT.addPixelColorXY(pixco[1][0], maxYpixel - pixco[1][1], color_scale(color, pxlbrightness[1])); // bottom right - if (pxlbrightness[2] > 0) - SEGMENT.addPixelColorXY(pixco[2][0], maxYpixel - pixco[2][1], color_scale(color, pxlbrightness[2])); // top right - if (pxlbrightness[3] > 0) - SEGMENT.addPixelColorXY(pixco[3][0], maxYpixel - pixco[3][1], color_scale(color, pxlbrightness[3])); // 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), could probably be extended to 3x3 - // SEGMENT.addPixelColorXY(pixco[1][0] + 1, maxYpixel - pixco[1][1], baseRGB.scale8((uint8_t)((brightness>>1) - pxlbrightness[0])), fastcoloradd); - // SEGMENT.addPixelColorXY(pixco[2][0] + 1, maxYpixel - pixco[2][1], baseRGB.scale8((uint8_t)((brightness>>1) -pxlbrightness[3])), fastcoloradd); + { + for(uint32_t i = 0; i < 4; i ++) + { + if (pxlbrightness[i] > 0) + SEGMENT.addPixelColorXY(pixco[i][0], maxYpixel - pixco[i][1], baseRGB.scale8((uint8_t)pxlbrightness[i])); + } } } @@ -738,6 +731,7 @@ void ParticleSystem::ParticleSys_render(bool firemode, uint32_t fireintensity) { if (useLocalBuffer) { + //uint32_t firstblur = particlesize > 64 ? 64 : particlesize; //attempt to add blurring, but does not work... blur2D(colorbuffer, maxYpixel + 1, maxYpixel + 1, particlesize, particlesize); if (particlesize > 64) blur2D(colorbuffer, maxYpixel + 1, maxYpixel + 1, particlesize - 64, particlesize - 64); @@ -745,8 +739,6 @@ void ParticleSystem::ParticleSys_render(bool firemode, uint32_t fireintensity) blur2D(colorbuffer, maxYpixel + 1, maxYpixel + 1, (particlesize - 128) << 1, (particlesize - 128) << 1); if (particlesize > 192) blur2D(colorbuffer, maxYpixel + 1, maxYpixel + 1, (particlesize - 192) << 1, (particlesize - 192) << 1); - - //Serial.println("blr"); } else { @@ -855,6 +847,11 @@ void ParticleSystem::renderParticle(PSparticle* particle, uint32_t brightess, in 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 + + //TODO: for advance pixels, render them to larger size in a local buffer. or better make a new function for that? + //easiest would be to create a 2x2 buffer for the original values, but that may not be as fast for smaller pixels... + //just make a new function that these colors are rendered to and then blurr it so it stays fast for normal rendering. + /* Serial.print("x:"); Serial.print(particle->x); @@ -922,8 +919,7 @@ void ParticleSystem::renderParticle(PSparticle* particle, uint32_t brightess, in // particles move upwards faster if ttl is high (i.e. they are hotter) void ParticleSystem::fireParticleupdate() { - //TODO: cleanup this function? check if normal move is much slower, change move function to check y first and check again - //todo: kill out of bounds funktioniert nicht? + //TODO: cleanup this function? check if normal move is much slower, change move function to check y first then this function just needs to add ttl to y befor calling normal move function uint32_t i = 0; for (i = 0; i < usedParticles; i++) @@ -935,7 +931,7 @@ void ParticleSystem::fireParticleupdate() // apply velocity particles[i].x = particles[i].x + (int32_t)particles[i].vx; particles[i].y = particles[i].y + (int32_t)particles[i].vy + (particles[i].ttl >> 2); // younger particles move faster upward as they are hotter - //particles[i].y = particles[i].y + (int32_t)particles[i].vy;// + (particles[i].ttl >> 3); // younger particles move faster upward as they are hotter //!! shift ttl by 2 is the original value, this is experimental + //particles[i].y = particles[i].y + (int32_t)particles[i].vy;// + (particles[i].ttl >> 3); // younger particles move faster upward as they are hotter //this is experimental, different shifting 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 speed, only check x direction if y is not out of bounds @@ -1233,6 +1229,8 @@ int32_t ParticleSystem::wraparound(int32_t p, int32_t maxvalue) //force is in 3.4 fixedpoint notation, +/-127 int32_t ParticleSystem::calcForce_dv(int8_t force, uint8_t* counter) { + if(force == 0) + return 0; // 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; @@ -1243,7 +1241,7 @@ int32_t ParticleSystem::calcForce_dv(int8_t force, uint8_t* counter) if (*counter > 15) { *counter -= 16; - dv = (force < 0) ? -1 : ((force > 0) ? 1 : 0); // force is either, 1, 0 or -1 if it is small + dv = force < 0 ? -1 : 1; // force is either, 1 or -1 if it is small (zero force is handled above) } } else @@ -1396,6 +1394,11 @@ bool initParticleSystem(ParticleSystem *&PartSys, uint8_t requestedsources, bool return true; } +/////////////////////// +// Utility Functions // +/////////////////////// + + // fastled color adding is very inaccurate in color preservation // a better color add function is implemented in colors.cpp but it uses 32bit RGBW. to use it colors need to be shifted just to then be shifted back by that function, which is slow // this is a fast version for RGB (no white channel, PS does not handle white) and with native CRGB including scaling of second color (fastled scale8 can be made faster using native 32bit on ESP) @@ -1444,9 +1447,9 @@ void fast_color_scale(CRGB &c, uint32_t scale) //blur a matrix in x and y direction, blur can be asymmetric in x and y -//for speed, 32bit variables are used, make sure to limit them to 8bit (0-255) +//for speed, 32bit variables are used, make sure to limit them to 8bit (0-255) or result is undefined //note: classic WLED blurrint is not (yet) supportet, this is a smearing function that does not fade original image, just blurrs it -void blur2D(CRGB **colorbuffer, uint32_t xsize, uint32_t ysize, uint32_t xblur, uint32_t yblur, bool particleblur) +void blur2D(CRGB **colorbuffer, uint32_t xsize, uint32_t ysize, uint32_t xblur, uint32_t yblur, bool smear, bool particleblur) { //uint32_t keep = smear ? 255 : 255 - xblur; CRGB seeppart, carryover; @@ -1467,17 +1470,19 @@ void blur2D(CRGB **colorbuffer, uint32_t xsize, uint32_t ysize, uint32_t xblur, { seeppart = colorbuffer[x][y]; //create copy of current color fast_color_scale(seeppart, seep); //scale it and seep to neighbours + if(!smear) //fade current pixel if smear is disabled + fast_color_scale(colorbuffer[x][y], 255 - xblur); + if(x > 0) { fast_color_add(colorbuffer[x-1][y], seeppart); fast_color_add(colorbuffer[x][y], carryover); //todo: could check if carryover is > 0, takes 7 instructions, add takes ~35, with lots of same color pixels (like background), it would be faster - } + } carryover = seeppart; } fast_color_add(colorbuffer[xsize-1][y], carryover); // set last pixel } - //uint32_t keep = smear ? 255 : 255 - xblur; seep = yblur >> 1; for(uint32_t x = 0; x < xsize; x++) { @@ -1486,6 +1491,9 @@ void blur2D(CRGB **colorbuffer, uint32_t xsize, uint32_t ysize, uint32_t xblur, { seeppart = colorbuffer[x][y]; //create copy of current color fast_color_scale(seeppart, seep); //scale it and seep to neighbours + if(!smear) //fade current pixel if smear is disabled + fast_color_scale(colorbuffer[x][y], 255 - xblur); + if(y > 0) { fast_color_add(colorbuffer[x][y-1], seeppart); diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index 2c90b9e53d..55696e052e 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -156,6 +156,7 @@ class ParticleSystem void setUsedParticles(uint16_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 setWallRoughness(uint8_t roughness); //wall roughness randomizes wall collisions void setMatrixSize(uint16_t x, uint16_t y); void setWrapX(bool enable); void setWrapY(bool enable); @@ -193,7 +194,8 @@ class ParticleSystem //utility functions void updatePSpointers(bool isadvanced); // update the data pointers to current segment data space - int32_t wraparound(int32_t w, int32_t maxvalue); + void bounce(int8_t &incomingspeed, int8_t ¶llelspeed, int32_t &position, uint16_t maxposition); //bounce on a wall + int32_t wraparound(int32_t p, int32_t maxvalue); int32_t calcForce_dv(int8_t force, uint8_t *counter); int32_t limitSpeed(int32_t speed); CRGB **allocate2Dbuffer(uint32_t cols, uint32_t rows); @@ -201,7 +203,8 @@ class ParticleSystem // note: variables that are accessed often are 32bit for speed uint32_t emitIndex; // index to count through particles to emit so searching for dead pixels is faster int32_t collisionHardness; - int32_t wallHardness; + uint8_t wallHardness; + uint8_t wallRoughness; uint8_t gforcecounter; //counter for global gravity int8_t gforce; //gravity strength, default is 8 (negative is allowed, positive is downwards) uint8_t collisioncounter; //counter to handle collisions TODO: could use the SEGMENT.call? @@ -210,7 +213,7 @@ class ParticleSystem uint8_t saturation; //note: on advanced particles, set this to 255 to disable saturation rendering, any other value uses particle sat value uint8_t particlesize; //global particle size, 0 = 2 pixels, 255 = 10 pixels int32_t particleHardRadius; // hard surface radius of a particle, used for collision detection - uint8_t motionBlur; + uint8_t motionBlur; //enable motion blur, values > 100 gives smoother animations. Note: motion blurring does not work if particlesize is > 0 }; //initialization functions (not part of class) @@ -221,4 +224,4 @@ bool allocateParticleSystemMemory(uint16_t numparticles, uint16_t numsources, bo //color add function void fast_color_add(CRGB &c1, CRGB &c2, uint32_t scale = 255); // fast and accurate color adding with scaling (scales c2 before adding) void fast_color_scale(CRGB &c, uint32_t scale); //fast scaling function using 32bit factor (keep it 0-255) and pointer -void blur2D(CRGB **colorbuffer, uint32_t xsize, uint32_t ysize, uint32_t xblur, uint32_t yblur, bool particleblur = false); \ No newline at end of file +void blur2D(CRGB **colorbuffer, uint32_t xsize, uint32_t ysize, uint32_t xblur, uint32_t yblur, bool smear = true, bool particleblur = false); \ No newline at end of file From 7abc440f90a18538ac277e062c86c7dca4d13615 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sat, 27 Apr 2024 13:40:38 +0200 Subject: [PATCH 066/219] removed zero inits - removed zero initialisations in FX, segment.data is set to zero by alloc function --- wled00/FX.cpp | 31 +++++-------------------------- wled00/FXparticleSystem.cpp | 2 +- 2 files changed, 6 insertions(+), 27 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 8e4ec1c941..cba3ffce14 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -7918,13 +7918,8 @@ uint16_t mode_particlevortex(void) { PartSys->sources[i].source.x = (PartSys->maxX + 1) >> 1; // center PartSys->sources[i].source.y = (PartSys->maxY + 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 } PartSys->setKillOutOfBounds(true); } @@ -8243,8 +8238,7 @@ uint16_t mode_particlevolcano(void) PartSys->sources[i].source.x = PartSys->maxX / (numSprays + 1) * (i + 1); //distribute evenly PartSys->sources[i].maxLife = 300; // lifetime in frames PartSys->sources[i].minLife = 250; - PartSys->sources[i].source.collide = true; // seeded particles will collide (if enabled) - PartSys->sources[i].vx = 0; // emitting speed + PartSys->sources[i].source.collide = true; // seeded particles will collide (if enabled) } } else @@ -8317,14 +8311,6 @@ uint16_t mode_particlefire(void) SEGMENT.aux0 = rand(); // aux0 is wind position (index) in the perlin noise // initialize the flame sprays numFlames = PartSys->numSources; - for (i = 0; i < numFlames; i++) - { - PartSys->sources[i].source.ttl = 0; - PartSys->sources[i].source.vx = 0; // emitter moving speed; - PartSys->sources[i].source.vy = 0; - // note: other parameters are set when creating the flame (see blow) - } - DEBUG_PRINTF_P(PSTR("segment data ptr in fireFX %p\n"), SEGMENT.data); } else @@ -8652,7 +8638,6 @@ uint16_t mode_particlewaterfall(void) for (i = 0; i < numSprays; i++) { PartSys->sources[i].source.hue = random16(); - PartSys->sources[i].source.vx = 0; PartSys->sources[i].source.collide = true; // seeded particles will collide #ifdef ESP8266 PartSys->sources[i].maxLife = 250; // lifetime in frames (ESP8266 has less particles, make them short lived to keep the water flowing) @@ -8661,7 +8646,6 @@ uint16_t mode_particlewaterfall(void) PartSys->sources[i].maxLife = 400; // lifetime in frames PartSys->sources[i].minLife = 150; #endif - PartSys->sources[i].vx = 0; // emitting speed PartSys->sources[i].var = 7; // emiting variation } } @@ -8924,8 +8908,7 @@ uint16_t mode_particleimpact(void) PartSys->setBounceY(true); //always use ground bounce MaxNumMeteors = min(PartSys->numSources, (uint8_t)NUMBEROFSOURCES); for (i = 0; i < MaxNumMeteors; i++) - { - PartSys->sources[i].vx = 0; //emit speed in x + { PartSys->sources[i].source.y = 500; PartSys->sources[i].source.ttl = random16(20 * i); // set initial delay for meteors PartSys->sources[i].source.vy = 10; // at positive speeds, no particles are emitted and if particle dies, it will be relaunched @@ -9063,11 +9046,8 @@ uint16_t mode_particleattractor(void) if (!initParticleSystem(PartSys, 1, true)) // init using 1 source and advanced particle settings return mode_static(); // allocation failed; //allocation failed - PartSys->sources[0].source.hue = random16(); - PartSys->sources[0].source.x = PS_P_RADIUS; //start out in bottom left corner - PartSys->sources[0].source.y = PS_P_RADIUS<<1; - PartSys->sources[0].source.vx = random16(5) + 3; - PartSys->sources[0].source.vy = PartSys->sources[0].source.vx - 2; //move slower in y + PartSys->sources[0].source.hue = random16(); + PartSys->sources[0].source.vx = -7; PartSys->sources[0].source.collide = true; // seeded particles will collide PartSys->sources[0].source.ttl = 100; //is replenished below, it never dies #ifdef ESP8266 @@ -9077,10 +9057,9 @@ uint16_t mode_particleattractor(void) PartSys->sources[0].maxLife = 350; // lifetime in frames PartSys->sources[0].minLife = 50; #endif - PartSys->sources[0].vx = 0; // emitting speed - PartSys->sources[0].vy = 0; // emitting speed PartSys->sources[0].var = 7; // emiting variation PartSys->setWallHardness(255); //bounce forever + PartSys->setWallRoughness(200); //randomize wall bounce } else PartSys = reinterpret_cast(SEGMENT.data); // if not first call, just set the pointer to the PS diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index effda26010..ab56ac8af3 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -56,7 +56,7 @@ ParticleSystem::ParticleSystem(uint16_t width, uint16_t height, uint16_t numbero updatePSpointers(isadvanced); // set the particle and sources pointer (call this before accessing sprays or particles) setMatrixSize(width, height); setWallHardness(255); // set default wall hardness to max - setWallRoughness(200); // smooth walls by default !!! testing this + setWallRoughness(0); // smooth walls by default !!! testing this setGravity(0); //gravity disabled by default setSaturation(255); //full saturation by default setParticleSize(0); // minimum size by default From 53607d6cdb0953af3c330b3d4da74d33eeb949a2 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sun, 28 Apr 2024 11:25:57 +0200 Subject: [PATCH 067/219] added individual size particle rendering plus some fixes - advanced particles can now be rendered to individual sizes. It is computationally intensive but it works well for up to 15 very large particles and more smaller ones - added collision handling for individual sizes (walls and collisions) - fixed bugs in particlebox - fixed fire not transitioning properly (flickering) when frame skip is active - removed 'wraparound' function as it can easily be done by casting to an unsigned and then modulo - fixed ballpit particles wandering left and right very fast if wrapX is set --- wled00/FX.cpp | 35 ++- wled00/FXparticleSystem.cpp | 449 ++++++++++++++++++++++-------------- wled00/FXparticleSystem.h | 15 +- 3 files changed, 305 insertions(+), 194 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index cba3ffce14..3ed6461760 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -8334,6 +8334,8 @@ uint16_t mode_particlefire(void) if (period < map(SEGMENT.speed, 0, 99, 50, 10)) // limit to 90FPS - 20FPS { SEGMENT.call--; //skipping a frame, decrement the counter (on call0, this is never executed as lastcall is 0, so its fine to not check if >0) + //still need to render the frame or flickering will occur in transitions + PartSys->updateFire(SEGMENT.intensity, true); // render the fire without updating it return FRAMETIME; //do not update this frame } *lastcall = strip.now; @@ -8442,7 +8444,7 @@ uint16_t mode_particlepit(void) PartSys->setWrapX(SEGMENT.check1); PartSys->setBounceX(SEGMENT.check2); PartSys->setBounceY(SEGMENT.check3); - PartSys->setWallHardness(min(SEGMENT.custom2, (uint8_t)200)); //limit to 200 min + PartSys->setWallHardness(min(SEGMENT.custom2, (uint8_t)150)); //limit to 100 min (if collisions are disabled, still want bouncy) if (SEGMENT.custom2>0) { PartSys->enableParticleCollisions(true, SEGMENT.custom2); // enable collisions and set particle collision hardness @@ -8469,22 +8471,31 @@ uint16_t mode_particlepit(void) PartSys->particles[i].hue = random16(); // set random color PartSys->particles[i].collide = true; //enable collision for particle PartSys->advPartProps[i].sat = ((SEGMENT.custom3) << 3) + 7; + //set particle size + if(SEGMENT.custom1 == 255) + { + PartSys->setParticleSize(0); //set global size to zero + PartSys->advPartProps[i].size = random(SEGMENT.custom1); //set each particle to random size + } + else + { + PartSys->setParticleSize(SEGMENT.custom1); //set global size + PartSys->advPartProps[i].size = 0; //use global size + } break; // emit only one particle per round } } } - - uint32_t frictioncoefficient = 1; + uint32_t frictioncoefficient = 1 + SEGMENT.check1; //need more friction if wrapX is set, see below note if (SEGMENT.speed < 50) // for low speeds, apply more friction frictioncoefficient = 50 - SEGMENT.speed; //if (SEGMENT.call % (3 + (SEGMENT.custom2 >> 2)) == 0) - if (SEGMENT.call % (3 + (SEGMENT.speed >> 2)) == 0) + //if (SEGMENT.call % (3 + (SEGMENT.speed >> 2)) == 0) + if (SEGMENT.call % 6 == 0)// (3 + max(3, (SEGMENT.speed >> 2))) == 0) //note: if friction is too low, hard particles uncontrollably 'wander' left and right if wrapX is enabled PartSys->applyFriction(frictioncoefficient); - - PartSys->setParticleSize(SEGMENT.custom1); PartSys->update(); // update and render //Experiment: blur to grow the particles, also blur asymmetric, make the particles wobble: @@ -8612,7 +8623,7 @@ uint16_t mode_particlepit(void) }*/ return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEPIT[] PROGMEM = "PS Ballpit@Speed,Intensity,Size,Hardness,Saturation,Cylinder,Walls,Ground;;!;2;pal=11,sx=100,ix=200,c1=120,c2=100,c3=31,o1=0,o2=0,o3=1"; +static const char _data_FX_MODE_PARTICLEPIT[] PROGMEM = "PS Ballpit@Speed,Intensity,Size,Hardness,Saturation,Cylinder,Walls,Ground;;!;2;pal=11,sx=100,ix=200,c1=120,c2=130,c3=31,o1=0,o2=0,o3=1"; /* * Particle Waterfall @@ -9017,7 +9028,7 @@ uint16_t mode_particleimpact(void) return FRAMETIME; } #undef NUMBEROFSOURCES -static const char _data_FX_MODE_PARTICLEIMPACT[] PROGMEM = "PS Impact@Launches,Explosion Size,Explosion Force,Bounce,Meteors,Cylinder,Walls,Collisions;;!;2;pal=0,sx=32,ix=85,c1=120,c2=125,c3=8,o1=0,o2=0,o3=1"; +static const char _data_FX_MODE_PARTICLEIMPACT[] PROGMEM = "PS Impact@Launches,Explosion Size,Explosion Force,Bounce,Meteors,Cylinder,Walls,Collisions;;!;2;pal=0,sx=32,ix=85,c1=70,c2=130,c3=8,o1=0,o2=0,o3=1"; /* Particle Attractor, a particle attractor sits in the matrix center, a spray bounces around and seeds particles @@ -9059,7 +9070,7 @@ uint16_t mode_particleattractor(void) #endif PartSys->sources[0].var = 7; // emiting variation PartSys->setWallHardness(255); //bounce forever - PartSys->setWallRoughness(200); //randomize wall bounce + PartSys->setWallRoughness(200); //randomize wall bounce } else PartSys = reinterpret_cast(SEGMENT.data); // if not first call, just set the pointer to the PS @@ -9073,7 +9084,7 @@ uint16_t mode_particleattractor(void) PartSys->updateSystem(); // update system properties (dimensions and data pointers) PartSys->setColorByAge(SEGMENT.check1); - PartSys->setParticleSize(SEGMENT.custom1 >> 1); + PartSys->setParticleSize(SEGMENT.custom1 >> 1); //set size globally if (SEGMENT.custom2 > 0) // collisions enabled PartSys->enableParticleCollisions(true, map(SEGMENT.custom2, 1, 255, 120, 255)); // enable collisions and set particle collision hardness @@ -9294,7 +9305,7 @@ uint16_t mode_particlespray(void) PartSys->setBounceX(!SEGMENT.check2); PartSys->setWrapX(SEGMENT.check2); PartSys->setWallHardness(hardness); - if(SEGMENT.check1) PartSys->setGravity(); + PartSys->setGravity(8 * SEGMENT.check1); //enable gravity if checked (8 is default strength) numSprays = min(PartSys->numSources, (uint8_t)1); // number of sprays if (SEGMENT.check3) // collisions enabled @@ -9417,7 +9428,7 @@ uint16_t mode_particleGEQ(void) PartSys->update(); // update and render return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEGEQ[] PROGMEM = "PS Equalizer@Speed,Intensity,Diverge,Bounce,Gravity,Cylinder,Walls,Floor;;!;2f;pal=0,sx=155,ix=200,c1=0,c2=128,c3=31,o1=0,o2=0,o3=0"; +static const char _data_FX_MODE_PARTICLEGEQ[] PROGMEM = "PS Equalizer@Speed,Intensity,Diverge,Bounce,Gravity,Cylinder,Walls,Floor;;!;2f;pal=0,sx=155,ix=200,c1=0,c2=128,o1=0,o2=0,o3=0"; /* diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index ab56ac8af3..de083c464b 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -87,10 +87,11 @@ ParticleSystem::ParticleSystem(uint16_t width, uint16_t height, uint16_t numbero //update function applies gravity, moves the particles, handles collisions and renders the particles void ParticleSystem::update(void) { + PSadvancedParticle *advprop = NULL; //apply gravity globally if enabled if (particlesettings.useGravity) applyGravity(); - + // handle collisions (can push particles, must be done before updating particles or they can render out of bounds, causing a crash if using local buffer for speed) if (particlesettings.useCollisions) handleCollisions(); @@ -98,7 +99,11 @@ void ParticleSystem::update(void) //move all particles for (int i = 0; i < usedParticles; i++) { - particleMoveUpdate(particles[i], particlesettings); + if(advPartProps) + { + advprop = &advPartProps[i]; + } + particleMoveUpdate(particles[i], particlesettings, advprop); } //!!! remove this //Serial.print("alive particles: "); @@ -114,9 +119,10 @@ void ParticleSystem::update(void) } //update function for fire animation -void ParticleSystem::updateFire(uint32_t intensity) +void ParticleSystem::updateFire(uint32_t intensity, bool renderonly) { - fireParticleupdate(); + if(!renderonly) + fireParticleupdate(); ParticleSys_render(true, intensity); } @@ -189,11 +195,11 @@ void ParticleSystem::setMotionBlur(uint8_t bluramount) motionBlur = bluramount; } -// render size using smearing +// render size using smearing (see blur function) void ParticleSystem::setParticleSize(uint8_t size) { particlesize = size; - particleHardRadius = max(PS_P_MINHARDRADIUS, (int)particlesize); + particleHardRadius = PS_P_MINHARDRADIUS + particlesize; //note: this sets size if not using advanced props motionBlur = 0; //disable motion blur if particle size is set } // enable/disable gravity, optionally, set the force (force=8 is default) can be 1-255, 0 is disable @@ -294,8 +300,9 @@ void ParticleSystem::angleEmit(PSsource &emitter, uint16_t angle, int8_t speed) // particle moves, decays and dies, if killoutofbounds is set, out of bounds particles are set to ttl=0 // uses passed settings to set bounce or wrap, if useGravity is set, it will never bounce at the top and killoutofbounds is not applied over the top -void ParticleSystem::particleMoveUpdate(PSparticle &part, PSsettings &options) +void ParticleSystem::particleMoveUpdate(PSparticle &part, PSsettings &options, PSadvancedParticle *advancedproperties) { + if (part.ttl > 0) { // age @@ -303,28 +310,45 @@ void ParticleSystem::particleMoveUpdate(PSparticle &part, PSsettings &options) if (particlesettings.colorByAge) part.hue = part.ttl > 255 ? 255 : part.ttl; //set color to ttl + bool usesize = false; //particle uses individual size rendering int32_t newX = part.x + (int16_t)part.vx; int32_t 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 wall collisions are enabled, bounce them before they reach the edge, it looks much nicer if the particle is not half out of vew + if(advancedproperties) //may be using individual particle size + { + if(advancedproperties->size > 0) + usesize = true; //note: variable eases out of frame checking below + particleHardRadius = max(PS_P_MINHARDRADIUS, (int)particlesize + advancedproperties->size); + } + //if wall collisions are enabled, bounce them before they reach the edge, it looks much nicer if the particle is not half out of view if (options.bounceX) { if ((newX < particleHardRadius) || (newX > maxX - particleHardRadius)) // reached a wall bounce(part.vx, part.vy, newX, maxX); } - if ((newX < 0) || (newX > maxX)) // check if particle reached an edge + if ((newX < 0) || (newX > maxX)) // check if particle reached an edge (note: this also checks out of bounds and must not be skipped, even if bounce is enabled) { if (options.wrapX) { - newX = wraparound(newX, maxX); + newX = (uint16_t)newX % (maxX + 1); } else if (((newX <= -PS_P_HALFRADIUS) || (newX > maxX + PS_P_HALFRADIUS))) // particle is leaving, set out of bounds if it has fully left { - part.outofbounds = 1; - if (options.killoutofbounds) - part.ttl = 0; + bool isleaving = true; + if(usesize) //using individual particle size + { + if (((newX > -particleHardRadius) || (newX < maxX + particleHardRadius))) // large particle is not yet leaving the view - note: this is not pixel perfect but good enough + isleaving = false; + } + + if(isleaving) + { + part.outofbounds = 1; + if (options.killoutofbounds) + part.ttl = 0; + } } } @@ -336,12 +360,15 @@ void ParticleSystem::particleMoveUpdate(PSparticle &part, PSsettings &options) bounce(part.vy, part.vx, newY, maxY); else { + /* + //TODO: is this check really needed? is checked below. on quick tests, it crashed (but not in all animations... -> seems ok. leave it for now, need to check this later if (options.useGravity) // do not bounce on top if using gravity (open container) if this is needed implement it in the FX - { + { if (newY > maxY + PS_P_HALFRADIUS) part.outofbounds = 1; // set out of bounds, kill out of bounds over the top does not apply if gravity is used (user can implement it in FX if needed) } - else + else*/ + if(!options.useGravity) { bounce(part.vy, part.vx, newY, maxY); } @@ -349,26 +376,33 @@ void ParticleSystem::particleMoveUpdate(PSparticle &part, PSsettings &options) } } - if (((newY < 0) || (newY > maxY))) // check if particle reached an edge + if (((newY < 0) || (newY > maxY))) // check if particle reached an edge (makes sure particles are within frame for rendering) { - if (options.wrapY) + if (options.wrapY) { - newY = wraparound(newY, maxY); + newY = (uint16_t)newY % (maxY + 1); } else if (((newY <= -PS_P_HALFRADIUS) || (newY > maxY + PS_P_HALFRADIUS))) // particle is leaving, set out of bounds if it has fully left - { - part.outofbounds = 1; - if (options.killoutofbounds) + { + bool isleaving = true; + if(usesize) //using individual particle size { - if (newY < 0) // if gravity is enabled, only kill particles below ground - part.ttl = 0; - else if (!options.useGravity) - part.ttl = 0; + if (((newY > -particleHardRadius) && (newY < maxY + particleHardRadius))) //still withing rendering reach + isleaving = false; + } + if(isleaving) + { + part.outofbounds = 1; + if (options.killoutofbounds) + { + if (newY < 0) // if gravity is enabled, only kill particles below ground + part.ttl = 0; + else if (!options.useGravity) + part.ttl = 0; + } } } - } - part.x = (int16_t)newX; // set new position part.y = (int16_t)newY; // set new position } @@ -551,6 +585,7 @@ void ParticleSystem::pointAttractor(uint16_t particleindex, PSparticle *attracto applyForce(particleindex, xforce, yforce); } +//attract to a line (TODO: this is not yet working) void ParticleSystem::lineAttractor(uint16_t particleindex, PSparticle *attractorcenter, uint16_t attractorangle, uint8_t strength) { // Calculate the distance between the particle and the attractor @@ -610,18 +645,18 @@ void ParticleSystem::lineAttractor(uint16_t particleindex, PSparticle *attractor // fireintensity and firemode are optional arguments (fireintensity is only used in firemode) void ParticleSystem::ParticleSys_render(bool firemode, uint32_t fireintensity) { - int32_t pixco[4][2]; //physical pixel coordinates of the four pixels a particle is rendered to. x,y pairs CRGB baseRGB; bool useLocalBuffer = true; //use local rendering buffer, gives huge speed boost (at least 30% more FPS) - CRGB **colorbuffer; + CRGB **framebuffer = NULL; //local frame buffer + CRGB **renderbuffer = NULL; //local particle render buffer for advanced particles uint32_t i; uint32_t brightness; // particle brightness, fades if dying - // CRGB colorbuffer[maxXpixel+1][maxYpixel+1] = {0}; //put buffer on stack (not a good idea, can cause crashes on large segments if other function run the stack into the heap) + if (useLocalBuffer) { // allocate empty memory for the local renderbuffer - colorbuffer = allocate2Dbuffer(maxXpixel + 1, maxYpixel + 1); - if (colorbuffer == NULL) + framebuffer = allocate2Dbuffer(maxXpixel + 1, maxYpixel + 1); + if (framebuffer == NULL) useLocalBuffer = false; //render to segment pixels directly if not enough memory if (motionBlur > 0) // using SEGMENT.fadeToBlackBy is much slower, this approximately doubles the speed of fade calculation @@ -632,11 +667,15 @@ void ParticleSystem::ParticleSys_render(bool firemode, uint32_t fireintensity) yflipped = maxYpixel - y; for (int x = 0; x <= maxXpixel; x++) { - colorbuffer[x][y] = SEGMENT.getPixelColorXY(x, yflipped); //copy to local buffer - fast_color_scale(colorbuffer[x][y], motionBlur); + framebuffer[x][y] = SEGMENT.getPixelColorXY(x, yflipped); //copy to local buffer + fast_color_scale(framebuffer[x][y], motionBlur); } } } + if(advPartProps) + { + renderbuffer = allocate2Dbuffer(10, 10); //buffer to render individual particles to if size > 0 note: null checking is done when accessing it + } } else { @@ -645,12 +684,14 @@ void ParticleSystem::ParticleSys_render(bool firemode, uint32_t fireintensity) else SEGMENT.fill(BLACK); //clear the buffer before rendering to it } + uint32_t debug = 0; // go over particles and render them to the buffer for (i = 0; i < usedParticles; i++) { if (particles[i].outofbounds || particles[i].ttl == 0) continue; + debug++; // generate RGB values for particle if(firemode) { @@ -676,69 +717,28 @@ void ParticleSystem::ParticleSys_render(bool firemode, uint32_t fireintensity) baseRGB = (CRGB)baseHSV; //convert back to RGB } } - int32_t pxlbrightness[4] = {0}; //note: pxlbrightness 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, pxlbrightness, pixco); - /* - //debug: check coordinates if out of buffer boundaries print out some info - for(uint32_t d; d<4; d++) + if(renderbuffer) //set buffer to zero if it exists { - if (pixco[d][0] < 0 || pixco[d][0] > maxXpixel) - { - pxlbrightness[d] = -1; //do not render - Serial.print("uncought out of bounds: x="); - Serial.print(pixco[d][0]); - Serial.print("particle x="); - Serial.print(particles[i].x); - Serial.print(" y="); - Serial.println(particles[i].y); - useLocalBuffer = false; - free(colorbuffer); // free buffer memory - } - if (pixco[d][1] < 0 || pixco[d][1] > maxYpixel) - { - pxlbrightness[d] = -1; // do not render - Serial.print("uncought out of bounds: y="); - Serial.print(pixco[d][1]); - Serial.print("particle x="); - Serial.print(particles[i].x); - Serial.print(" y="); - Serial.println(particles[i].y); - useLocalBuffer = false; - free(colorbuffer); // free buffer memory - } - }*/ - if (useLocalBuffer) - { - for(uint32_t i = 0; i < 4; i ++) - { - if (pxlbrightness[i] > 0) - fast_color_add(colorbuffer[pixco[i][0]][pixco[i][1]], baseRGB, pxlbrightness[i]); // order is: bottom left, bottom right, top right, top left - } - } - else - { - for(uint32_t i = 0; i < 4; i ++) - { - if (pxlbrightness[i] > 0) - SEGMENT.addPixelColorXY(pixco[i][0], maxYpixel - pixco[i][1], baseRGB.scale8((uint8_t)pxlbrightness[i])); - } + memset(renderbuffer[0], 0, 100 * sizeof(CRGB)); // renderbuffer is 10x10 pixels. note: passing the buffer and setting it zero here is faster than creating a new buffer for every particle } + + renderParticle(framebuffer, i, brightness, baseRGB, renderbuffer); + } if(particlesize > 0) { if (useLocalBuffer) { - //uint32_t firstblur = particlesize > 64 ? 64 : particlesize; //attempt to add blurring, but does not work... - blur2D(colorbuffer, maxYpixel + 1, maxYpixel + 1, particlesize, particlesize); + //uint32_t firstblur = particlesize > 64 ? 64 : particlesize; //attempt to add motion blurring, but does not work... + blur2D(framebuffer, maxYpixel + 1, maxYpixel + 1, particlesize, particlesize); if (particlesize > 64) - blur2D(colorbuffer, maxYpixel + 1, maxYpixel + 1, particlesize - 64, particlesize - 64); + blur2D(framebuffer, maxYpixel + 1, maxYpixel + 1, particlesize - 64, particlesize - 64); if (particlesize > 128) - blur2D(colorbuffer, maxYpixel + 1, maxYpixel + 1, (particlesize - 128) << 1, (particlesize - 128) << 1); + blur2D(framebuffer, maxYpixel + 1, maxYpixel + 1, (particlesize - 128) << 1, (particlesize - 128) << 1); if (particlesize > 192) - blur2D(colorbuffer, maxYpixel + 1, maxYpixel + 1, (particlesize - 192) << 1, (particlesize - 192) << 1); + blur2D(framebuffer, maxYpixel + 1, maxYpixel + 1, (particlesize - 192) << 1, (particlesize - 192) << 1); } else { @@ -761,35 +761,46 @@ void ParticleSystem::ParticleSys_render(bool firemode, uint32_t fireintensity) yflipped = maxYpixel - y; for (int x = 0; x <= maxXpixel; x++) { - SEGMENT.setPixelColorXY(x, yflipped, colorbuffer[x][y]); + SEGMENT.setPixelColorXY(x, yflipped, framebuffer[x][y]); } } - free(colorbuffer); // free buffer memory + free(framebuffer); // free buffer memory } - - + if(renderbuffer) + free(renderbuffer); // free buffer memory + Serial.println(debug); } -// 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) -void ParticleSystem::renderParticle(PSparticle* particle, uint32_t brightess, int32_t *pixelvalues, int32_t (*pixelpositions)[2]) +// calculate pixel positions and brightness distribution and render the particle to local buffer or global buffer +void ParticleSystem::renderParticle(CRGB **framebuffer, uint32_t particleindex, uint32_t brightess, CRGB color, CRGB **renderbuffer) { + int32_t pxlbrightness[4] = {0}; //note: pxlbrightness needs to be set to 0 or checking does not work + int32_t pixco[4][2]; //physical pixel coordinates of the four pixels a particle is rendered to. x,y pairs + bool advancedrender = false; //rendering for advanced particles + // 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 xoffset = particles[particleindex].x - PS_P_HALFRADIUS; + int32_t yoffset = particles[particleindex].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; + //check if particle has advanced size properties and buffer is available + if(advPartProps && renderbuffer) + { + if(advPartProps[particleindex].size > 0) + advancedrender = true; + } + // 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 + pixco[0][0] = pixco[3][0] = x; // bottom left & top left + pixco[0][1] = pixco[1][1] = y; // bottom left & bottom right + pixco[1][0] = pixco[2][0] = x + 1; // bottom right & top right + pixco[2][1] = pixco[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 { 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) @@ -797,19 +808,19 @@ void ParticleSystem::renderParticle(PSparticle* particle, uint32_t brightess, in //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) { - pixelvalues[1] = pixelvalues[2] = -1; // pixel is actually out of matrix boundaries, do not render + pxlbrightness[1] = pxlbrightness[2] = -1; // pixel is actually out of matrix boundaries, do not render } if (particlesettings.wrapX) // wrap x to the other side if required - pixelpositions[0][0] = pixelpositions[3][0] = maxXpixel; + pixco[0][0] = pixco[3][0] = maxXpixel; else - pixelvalues[0] = pixelvalues[3] = -1; // pixel is out of matrix boundaries, do not render + pxlbrightness[0] = pxlbrightness[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 + else if (pixco[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; + pixco[1][0] = pixco[2][0] = 0; else - pixelvalues[1] = pixelvalues[2] = -1; + pxlbrightness[1] = pxlbrightness[2] = -1; } if (y < 0) // bottom pixels out of frame @@ -817,19 +828,25 @@ void ParticleSystem::renderParticle(PSparticle* particle, uint32_t brightess, in dy = PS_P_RADIUS + dy; //see note above if (dy == PS_P_RADIUS) { - pixelvalues[2] = pixelvalues[3] = -1; // pixel is actually out of matrix boundaries, do not render + pxlbrightness[2] = pxlbrightness[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; + pixco[0][1] = pixco[1][1] = maxYpixel; else - pixelvalues[0] = pixelvalues[1] = -1; + pxlbrightness[0] = pxlbrightness[1] = -1; } - else if (pixelpositions[2][1] > maxYpixel) // top pixels + else if (pixco[2][1] > maxYpixel) // top pixels { if (particlesettings.wrapY) // wrap y to the other side if required - pixelpositions[2][1] = pixelpositions[3][1] = 0; + pixco[2][1] = pixco[3][1] = 0; else - pixelvalues[2] = pixelvalues[3] = -1; + pxlbrightness[2] = pxlbrightness[3] = -1; + } + + if(advancedrender) //always render full particles in advanced rendering, undo out of frame marking (faster than checking each time in code above) + { + for(uint32_t i = 0; i < 4; i++) + pxlbrightness[i] = 0; } // calculate brightness values for all four pixels representing a particle using linear interpolation @@ -839,19 +856,112 @@ void ParticleSystem::renderParticle(PSparticle* particle, uint32_t brightess, in 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 + if (pxlbrightness[0] >= 0) + pxlbrightness[0] = (precal1 * precal2) >> PS_P_SURFACE; // bottom left value equal to ((PS_P_RADIUS - dx) * (PS_P_RADIUS-dy) * brightess) >> PS_P_SURFACE + if (pxlbrightness[1] >= 0) + pxlbrightness[1] = (dx * precal2) >> PS_P_SURFACE; // bottom right value equal to (dx * (PS_P_RADIUS-dy) * brightess) >> PS_P_SURFACE + if (pxlbrightness[2] >= 0) + pxlbrightness[2] = (dx * precal3) >> PS_P_SURFACE; // top right value equal to (dx * dy * brightess) >> PS_P_SURFACE + if (pxlbrightness[3] >= 0) + pxlbrightness[3] = (precal1 * precal3) >> PS_P_SURFACE; // top left value equal to ((PS_P_RADIUS-dx) * dy * brightess) >> PS_P_SURFACE + + + + if(advancedrender) + { + //render particle to a bigger size + //particle size to pixels: < 64 is 4x4, < 128 is 6x6, < 192 is 8x8, bigger is 10x10 + + //first, render the pixel to the renderbuffer, then apply 2D blurring + fast_color_add(renderbuffer[4][4], color, pxlbrightness[0]); // order is: bottom left, bottom right, top right, top left + fast_color_add(renderbuffer[5][4], color, pxlbrightness[1]); + fast_color_add(renderbuffer[5][5], color, pxlbrightness[2]); + fast_color_add(renderbuffer[4][5], color, pxlbrightness[3]); //TODO: make this a loop somehow? needs better coordinate handling... + uint32_t rendersize = 4; + uint32_t offset = 3; //offset to zero coordinate to write/read data in renderbuffer + blur2D(renderbuffer, rendersize, rendersize, advPartProps[particleindex].size, advPartProps[particleindex].size, true, offset, offset, true); //blur to 4x4 + if (advPartProps[particleindex].size > 64) + { + rendersize += 2; + offset--; + blur2D(renderbuffer, rendersize, rendersize, advPartProps[particleindex].size - 64, advPartProps[particleindex].size - 64, true, offset, offset, true); //blur to 6x6 + } + if (advPartProps[particleindex].size > 128) + { + rendersize += 2; + offset--; + blur2D(renderbuffer, rendersize, rendersize, (advPartProps[particleindex].size - 128) << 1, (advPartProps[particleindex].size - 128) << 1, true, offset, offset, true); //blur to 8x8 + } + if (advPartProps[particleindex].size > 192) + { + rendersize += 2; + offset--; + blur2D(renderbuffer, rendersize, rendersize, (advPartProps[particleindex].size - 192) << 1, (advPartProps[particleindex].size - 192) << 1, true, offset, offset, true); //blur to 10x10 + } + + //calculate origin coordinates to render the particle to in the framebuffer + uint32_t xfb_orig = x - (rendersize>>1) + 1 - offset; + uint32_t yfb_orig = y - (rendersize>>1) + 1 - offset; + uint32_t xfb, yfb; //coordinates in frame buffer to write to note: by making this uint, only overflow has to be checked + + //transfer renderbuffer to framebuffer + for(uint32_t xrb = offset; xrb < rendersize+offset; xrb++) + { + xfb = xfb_orig + xrb; + if(xfb > maxXpixel) + if (particlesettings.wrapX) // wrap x to the other side if required + xfb = xfb % (maxXpixel + 1); + else + continue; + for(uint32_t yrb = offset; yrb < rendersize+offset; yrb++) + { + yfb = yfb_orig + yrb; + if(yfb > maxYpixel) + if (particlesettings.wrapY) // wrap y to the other side if required + yfb = yfb % (maxYpixel + 1); + else + continue; + //if(xfb < maxXpixel +1 && yfb < maxYpixel +1) + fast_color_add(framebuffer[xfb][yfb], renderbuffer[xrb][yrb]); //TODO: this is just a test, need to render to correct coordinates with out of frame checking + } + } + } + else + { + if (framebuffer) + { + for(uint32_t i = 0; i < 4; i++) + { + if (pxlbrightness[i] > 0) + fast_color_add(framebuffer[pixco[i][0]][pixco[i][1]], color, pxlbrightness[i]); // order is: bottom left, bottom right, top right, top left + } + } + else + { + for(uint32_t i = 0; i < 4; i++) + { + if (pxlbrightness[i] > 0) + SEGMENT.addPixelColorXY(pixco[i][0], maxYpixel - pixco[i][1], color.scale8((uint8_t)pxlbrightness[i])); + } + } + } //TODO: for advance pixels, render them to larger size in a local buffer. or better make a new function for that? //easiest would be to create a 2x2 buffer for the original values, but that may not be as fast for smaller pixels... //just make a new function that these colors are rendered to and then blurr it so it stays fast for normal rendering. +/* + //uint32_t firstblur = particlesize > 64 ? 64 : particlesize; //attempt to add motion blurring, but does not work... + blur2D(framebuffer, maxYpixel + 1, maxYpixel + 1, particlesize, particlesize); //4x4 + if (particlesize > 64) + blur2D(framebuffer, maxYpixel + 1, maxYpixel + 1, particlesize - 64, particlesize - 64); //6x6 + if (particlesize > 128) + blur2D(framebuffer, maxYpixel + 1, maxYpixel + 1, (particlesize - 128) << 1, (particlesize - 128) << 1); 8x8 + if (particlesize > 192) + blur2D(framebuffer, maxYpixel + 1, maxYpixel + 1, (particlesize - 192) << 1, (particlesize - 192) << 1); 10x10 + */ + + /* Serial.print("x:"); Serial.print(particle->x); @@ -865,53 +975,55 @@ void ParticleSystem::renderParticle(PSparticle* particle, uint32_t brightess, in for(uint8_t t = 0; t<4; t++) { Serial.print(" v"); - Serial.print(pixelvalues[t]); + Serial.print(pxlbrightness[t]); Serial.print(" x"); - Serial.print(pixelpositions[t][0]); + Serial.print(pixco[t][0]); Serial.print(" y"); - Serial.print(pixelpositions[t][1]); + Serial.print(pixco[t][1]); Serial.print(" "); } Serial.println(" "); */ - // debug: check coordinates if out of buffer boundaries print out some info +/* + // debug: check coordinates if out of buffer boundaries print out some info (rendering out of bounds particle causes crash!) for (uint32_t d = 0; d < 4; d++) { - if (pixelpositions[d][0] < 0 || pixelpositions[d][0] > maxXpixel) + if (pixco[d][0] < 0 || pixco[d][0] > maxXpixel) { //Serial.print("<"); - if (pixelvalues[d] >= 0) + if (pxlbrightness[d] >= 0) { Serial.print("uncought out of bounds: x:"); - Serial.print(pixelpositions[d][0]); + Serial.print(pixco[d][0]); Serial.print(" y:"); - Serial.print(pixelpositions[d][1]); + Serial.print(pixco[d][1]); Serial.print("particle x="); - Serial.print(particle->x); + Serial.print(particles[particleindex].x); Serial.print(" y="); - Serial.println(particle->y); - pixelvalues[d] = -1; // do not render + Serial.println(particles[particleindex].y); + pxlbrightness[d] = -1; // do not render } } - if (pixelpositions[d][1] < 0 || pixelpositions[d][1] > maxYpixel) + if (pixco[d][1] < 0 || pixco[d][1] > maxYpixel) { //Serial.print("^"); - if (pixelvalues[d] >= 0) + if (pxlbrightness[d] >= 0) { Serial.print("uncought out of bounds: y:"); - Serial.print(pixelpositions[d][0]); + Serial.print(pixco[d][0]); Serial.print(" y:"); - Serial.print(pixelpositions[d][1]); + Serial.print(pixco[d][1]); Serial.print("particle x="); - Serial.print(particle->x); + Serial.print(particles[particleindex].x); Serial.print(" y="); - Serial.println(particle->y); - pixelvalues[d] = -1; // do not render + Serial.println(particles[particleindex].y); + pxlbrightness[d] = -1; // do not render } } } +*/ } @@ -946,7 +1058,7 @@ void ParticleSystem::fireParticleupdate() { if (particlesettings.wrapX) { - particles[i].x = wraparound(particles[i].x, maxX); + particles[i].x = (uint16_t)particles[i].x % (maxX + 1); } else if ((particles[i].x < -PS_P_HALFRADIUS) || (particles[i].x > maxX + PS_P_HALFRADIUS)) //if fully out of view { @@ -958,7 +1070,6 @@ void ParticleSystem::fireParticleupdate() } } - // detect collisions in an array of particles and handle them void ParticleSystem::handleCollisions() { @@ -989,6 +1100,10 @@ void ParticleSystem::handleCollisions() if (particles[j].ttl > 0) // if target particle is alive { dx = particles[i].x - particles[j].x; + if(advPartProps) //may be using individual particle size + { + particleHardRadius = PS_P_MINHARDRADIUS + particlesize + (((uint32_t)advPartProps[i].size + (uint32_t)advPartProps[j].size)>>1); //collision distance + } if (dx < particleHardRadius && dx > -particleHardRadius) // check x direction, if close, check y direction { dy = particles[i].y - particles[j].y; @@ -1208,23 +1323,6 @@ void ParticleSystem::collideParticles(PSparticle *particle1, PSparticle *particl } -//calculation of particle wraparound -//function assumes that out of bounds is checked before calling it -int32_t ParticleSystem::wraparound(int32_t p, int32_t maxvalue) -{ - /* - //variant without modulo (but is unsafe, far out particles will not get wrapped!) TODO: !!! remove this variant - if (p < 0) - p += maxvalue + 1; - else //if (p > maxvalue) - p -= maxvalue + 1; - return p;*/ - p = p % (maxvalue + 1); - if (p < 0) - p = maxvalue - p; - return p; -} - //calculate the delta speed (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) @@ -1446,27 +1544,24 @@ void fast_color_scale(CRGB &c, uint32_t scale) -//blur a matrix in x and y direction, blur can be asymmetric in x and y -//for speed, 32bit variables are used, make sure to limit them to 8bit (0-255) or result is undefined -//note: classic WLED blurrint is not (yet) supportet, this is a smearing function that does not fade original image, just blurrs it -void blur2D(CRGB **colorbuffer, uint32_t xsize, uint32_t ysize, uint32_t xblur, uint32_t yblur, bool smear, bool particleblur) +// blur a matrix in x and y direction, blur can be asymmetric in x and y +// for speed, 32bit variables are used, make sure to limit them to 8bit (0-255) or result is undefined +// to blur a subset of the buffer, change the xsize/ysize and set xstart/ystart to the desired starting coordinates (default start is 0/0) +void blur2D(CRGB **colorbuffer, uint32_t xsize, uint32_t ysize, uint32_t xblur, uint32_t yblur, bool smear, uint32_t xstart, uint32_t ystart, bool isparticle) { - //uint32_t keep = smear ? 255 : 255 - xblur; + + //TODO: for particle rendering, first row and last row can be skipped in x blurring as it is all black, this would increase rendering speed CRGB seeppart, carryover; - uint32_t seep = xblur >> 1; - uint32_t startrow = 0; - uint32_t endrow = ysize; - if(particleblur) //blurring a single particle, can skip first and last row in x blurring as it is black + uint32_t seep = xblur >> 1; + if(isparticle) //first and last row are always black in particle rendering { - startrow++; - endrow--; + ystart++; + ysize--; } - - for(uint32_t y = startrow; y < endrow; y++) - { - //fast_color_scale(colorbuffer[0][y], keep); //first pixel is just faded, carryover is black (this only needs to be done if smear is false) + for(uint32_t y = ystart; y < ystart + ysize; y++) + { carryover = BLACK; - for(uint32_t x = 0; x < xsize; x++) + for(uint32_t x = xstart; x < xstart + xsize; x++) { seeppart = colorbuffer[x][y]; //create copy of current color fast_color_scale(seeppart, seep); //scale it and seep to neighbours @@ -1483,11 +1578,17 @@ void blur2D(CRGB **colorbuffer, uint32_t xsize, uint32_t ysize, uint32_t xblur, fast_color_add(colorbuffer[xsize-1][y], carryover); // set last pixel } + if(isparticle) //now also do first and last row + { + ystart--; + ysize++; + } + seep = yblur >> 1; - for(uint32_t x = 0; x < xsize; x++) + for(uint32_t x = xstart; x < xstart + xsize; x++) { carryover = BLACK; - for(uint32_t y = 0; y < ysize; y++) + for(uint32_t y = ystart; y < ystart + ysize; y++) { seeppart = colorbuffer[x][y]; //create copy of current color fast_color_scale(seeppart, seep); //scale it and seep to neighbours diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index 55696e052e..fba1f203bf 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -42,7 +42,7 @@ #define PS_P_HALFRADIUS 32 #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_MINHARDRADIUS 80 // minimum hard surface radius +#define PS_P_MINHARDRADIUS 70 // minimum hard surface radius #define PS_P_MINSURFACEHARDNESS 128 //minimum hardness used in collision impulse calculation, below this hardness, particles become sticky #define PS_P_MAXSPEED 120 //maximum speed a particle can have (vx/vy is int8) @@ -128,7 +128,7 @@ class ParticleSystem ParticleSystem(uint16_t width, uint16_t height, uint16_t numberofparticles, uint16_t numberofsources, bool isadvanced = false); // constructor // note: memory is allcated in the FX function, no deconstructor needed void update(void); //update the particles according to set options and render to the matrix - void updateFire(uint32_t intensity); // update function for fire + void updateFire(uint32_t intensity, bool renderonly = false); // update function for fire, if renderonly is set, particles are not updated (required to fix transitions with frameskips) void updateSystem(void); // call at the beginning of every FX, updates pointers and dimensions // particle emitters @@ -137,8 +137,7 @@ class ParticleSystem void angleEmit(PSsource& emitter, uint16_t angle, int8_t speed); // move functions - void particleMoveUpdate(PSparticle &part, PSsettings &options); - + void particleMoveUpdate(PSparticle &part, PSsettings &options, PSadvancedParticle *advancedproperties = NULL); //particle physics void applyGravity(PSparticle *part); // applies gravity to single particle (use this for sources) void applyForce(PSparticle *part, int8_t xforce, int8_t yforce, uint8_t *counter); @@ -184,7 +183,7 @@ class ParticleSystem private: //rendering functions void ParticleSys_render(bool firemode = false, uint32_t fireintensity = 128); - void renderParticle(PSparticle *particle, uint32_t brightess, int32_t *pixelvalues, int32_t (*pixelpositions)[2]); + void renderParticle(CRGB **framebuffer, uint32_t particleindex, uint32_t brightess, CRGB color, CRGB **renderbuffer); //paricle physics applied by system if flags are set void applyGravity(); // applies gravity to all particles @@ -195,7 +194,7 @@ class ParticleSystem //utility functions void updatePSpointers(bool isadvanced); // update the data pointers to current segment data space void bounce(int8_t &incomingspeed, int8_t ¶llelspeed, int32_t &position, uint16_t maxposition); //bounce on a wall - int32_t wraparound(int32_t p, int32_t maxvalue); + int16_t wraparound(uint16_t p, uint32_t maxvalue); int32_t calcForce_dv(int8_t force, uint8_t *counter); int32_t limitSpeed(int32_t speed); CRGB **allocate2Dbuffer(uint32_t cols, uint32_t rows); @@ -211,7 +210,7 @@ class ParticleSystem uint8_t forcecounter; //counter for globally applied forces //global particle properties for basic particles uint8_t saturation; //note: on advanced particles, set this to 255 to disable saturation rendering, any other value uses particle sat value - uint8_t particlesize; //global particle size, 0 = 2 pixels, 255 = 10 pixels + uint8_t particlesize; //global particle size, 0 = 2 pixels, 255 = 10 pixels (note: this is also added to individual sized particles) int32_t particleHardRadius; // hard surface radius of a particle, used for collision detection uint8_t motionBlur; //enable motion blur, values > 100 gives smoother animations. Note: motion blurring does not work if particlesize is > 0 }; @@ -224,4 +223,4 @@ bool allocateParticleSystemMemory(uint16_t numparticles, uint16_t numsources, bo //color add function void fast_color_add(CRGB &c1, CRGB &c2, uint32_t scale = 255); // fast and accurate color adding with scaling (scales c2 before adding) void fast_color_scale(CRGB &c, uint32_t scale); //fast scaling function using 32bit factor (keep it 0-255) and pointer -void blur2D(CRGB **colorbuffer, uint32_t xsize, uint32_t ysize, uint32_t xblur, uint32_t yblur, bool smear = true, bool particleblur = false); \ No newline at end of file +void blur2D(CRGB **colorbuffer, uint32_t xsize, uint32_t ysize, uint32_t xblur, uint32_t yblur, bool smear = true, uint32_t xstart = 0, uint32_t ystart = 0, bool isparticle = false); \ No newline at end of file From 2e0cb3a49fb9dbfa3619c6bbcf31c01a8cd8585d Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sun, 28 Apr 2024 19:29:57 +0200 Subject: [PATCH 068/219] Fixed some nasty memory bugs, fixed some FX parameters - fixed memory alignment bug in PS pointer assignment by making sure only multiples of 4 are allowed for particles and sources - added saturation back to particle struct, as it was aligned to 10 bytes anyway. - fixed a bug where a null pointer could be accessed - fixed rendering out of frame particles if buffer allocation failed - improvements on ESP8266 --- wled00/FX.cpp | 79 ++++++++++++++++----------------- wled00/FXparticleSystem.cpp | 88 +++++++++++++++++++++---------------- wled00/FXparticleSystem.h | 12 ++--- 3 files changed, 94 insertions(+), 85 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 3ed6461760..57700cac58 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -7912,7 +7912,11 @@ uint16_t mode_particlevortex(void) //SEGMENT.aux0 = 0; // starting angle SEGMENT.aux1 = 0x01; // check flags - + #ifdef ESP8266 + PartSys->setMotionBlur(150); + #else + PartSys->setMotionBlur(100); + #endif uint8_t numSprays = min(PartSys->numSources, (uint8_t)NUMBEROFSOURCES); for (i = 0; i < numSprays; i++) { @@ -7934,7 +7938,15 @@ uint16_t mode_particlevortex(void) PartSys->updateSystem(); // update system properties (dimensions and data pointers) uint8_t spraycount = min(PartSys->numSources, (uint8_t)(1 + (SEGMENT.custom1 >> 5))); // number of sprays to display, 1-8 - + #ifdef ESP8266 + for (i = 1; i < 4; i++) //need static particles in the center to reduce blinking (would be black every other frame without this hack), just set them there fixed + { + PartSys->particles[PartSys->numParticles - i].x = (PartSys->maxX + 1) >> 1; // center + PartSys->particles[PartSys->numParticles - i].y = (PartSys->maxY + 1) >> 1; // center + PartSys->particles[PartSys->numParticles - i].sat = 230; + PartSys->particles[PartSys->numParticles - i].ttl = 100; //set alive + } + #endif if (SEGMENT.check1 != (SEGMENT.aux1 & 0x01) || SEGMENT.call == 0) //state change { if (SEGMENT.check1) @@ -8017,17 +8029,17 @@ uint16_t mode_particlevortex(void) //test to check if less particles are ok at lower speeds. - uint32_t skip = PS_P_HALFRADIUS/SEGMENT.intensity + 1; + uint32_t skip = PS_P_HALFRADIUS/(SEGMENT.intensity + 1) + 1; if (SEGMENT.call % skip == 0) { j = random(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) { PartSys->sources[j].var = (SEGMENT.custom3 >> 1); //update speed variation -#ifdef ESP8266 + #ifdef ESP8266 if (SEGMENT.call & 0x01) // every other frame, do not emit to save particles #endif - PartSys->angleEmit(PartSys->sources[j], SEGMENT.aux0 + angleoffset * j, SEGMENT.intensity >> 2); + PartSys->angleEmit(PartSys->sources[j], SEGMENT.aux0 + angleoffset * j, (SEGMENT.intensity >> 2)+1); //PartSys->sprayEmit(PartSys->sources[j]); j = (j + 1) % spraycount; } @@ -8061,8 +8073,7 @@ uint16_t mode_particlefireworks(void) if (!initParticleSystem(PartSys, NUMBEROFSOURCES, true)) // init with advanced particle properties return mode_static(); // allocation failed; //allocation failed PartSys->setKillOutOfBounds(true); //out of bounds particles dont return (except on top, taken care of by gravity setting) - PartSys->setWallHardness(100); //ground bounce is fixed - PartSys->setSaturation(0); //need to set saturation lower than 255 so rendering will use individual particle saturation + PartSys->setWallHardness(100); //ground bounce is fixed numRockets = min(PartSys->numSources, (uint8_t)NUMBEROFSOURCES); for (j = 0; j < numRockets; j++) { @@ -8149,7 +8160,7 @@ uint16_t mode_particlefireworks(void) speed += 5; //increase speed to form a second circle speedvariation = speedvariation ? speedvariation + random16(4) : 0; // double speed variation PartSys->sources[j].source.hue = random16(); // new color for next circle - PartSys->sources[j].sat = min((uint16_t)150,random16()); + PartSys->sources[j].source.sat = min((uint16_t)150,random16()); } angle += angleincrement; // set angle for next particle } @@ -8179,7 +8190,7 @@ uint16_t mode_particlefireworks(void) { PartSys->sources[j].source.vy = 0; // set speed to zero so code above will recognize this as an exploding rocket PartSys->sources[j].source.hue = random16(); // random color - PartSys->sources[j].sat = random16(55) + 200; + PartSys->sources[j].source.sat = random16(55) + 200; PartSys->sources[j].maxLife = 200; PartSys->sources[j].minLife = 100; PartSys->sources[j].source.ttl = random16((1200 - ((uint32_t)SEGMENT.speed << 2))) + 550 - (SEGMENT.speed << 1); // standby time til next launch @@ -8192,7 +8203,7 @@ uint16_t mode_particlefireworks(void) PartSys->sources[j].source.x = (rand() % (PartSys->maxX >> 1)) + (PartSys->maxX >> 2); // centered half PartSys->sources[j].source.vy = random16(SEGMENT.custom1 >> 3) + 5; // rocket speed depends also on rocket fuse PartSys->sources[j].source.vx = random(5) - 2; //i.e. not perfectly straight up - PartSys->sources[j].sat = 30; // low saturation -> exhaust is off-white + PartSys->sources[j].source.sat = 30; // low saturation -> exhaust is off-white PartSys->sources[j].source.ttl = random16(SEGMENT.custom1) + (SEGMENT.custom1 >> 1); // sets explosion height (rockets explode at the top if set too high as paticle update set speed to zero if moving out of matrix) PartSys->sources[j].maxLife = 40; // exhaust particle life PartSys->sources[j].minLife = 10; @@ -8331,7 +8342,7 @@ uint16_t mode_particlefire(void) { uint32_t *lastcall = reinterpret_cast(PartSys->PSdataEnd); uint32_t period = strip.now - *lastcall; - if (period < map(SEGMENT.speed, 0, 99, 50, 10)) // limit to 90FPS - 20FPS + if (period < (uint32_t)map(SEGMENT.speed, 0, 99, 50, 10)) // limit to 90FPS - 20FPS { SEGMENT.call--; //skipping a frame, decrement the counter (on call0, this is never executed as lastcall is 0, so its fine to not check if >0) //still need to render the frame or flickering will occur in transitions @@ -8453,8 +8464,6 @@ uint16_t mode_particlepit(void) PartSys->enableParticleCollisions(false); } - PartSys->setSaturation(((SEGMENT.custom3) << 3) + 7); // set global rendering saturation so individual saturation rendering is enabled - uint32_t i; if (SEGMENT.call % (128 - (SEGMENT.intensity >> 1)) == 0 && SEGMENT.intensity > 0) // every nth frame emit particles, stop emitting if set to zero { @@ -8470,7 +8479,7 @@ uint16_t mode_particlepit(void) PartSys->particles[i].vy = map(SEGMENT.speed, 0, 255, -5, -100); // downward speed PartSys->particles[i].hue = random16(); // set random color PartSys->particles[i].collide = true; //enable collision for particle - PartSys->advPartProps[i].sat = ((SEGMENT.custom3) << 3) + 7; + PartSys->particles[i].sat = ((SEGMENT.custom3) << 3) + 7; //set particle size if(SEGMENT.custom1 == 255) { @@ -8745,11 +8754,7 @@ uint16_t mode_particlebox(void) PartSys->setBounceX(true); PartSys->setBounceY(true); PartSys->setWallHardness(min(SEGMENT.custom2, (uint8_t)200)); // wall hardness is 200 or more - if (SEGMENT.custom2 > 0) - PartSys->enableParticleCollisions(true, SEGMENT.custom2); // enable collisions and set particle collision hardness - else - PartSys->enableParticleCollisions(false); - + PartSys->enableParticleCollisions(true, max(2, (int)SEGMENT.custom2)); // enable collisions and set particle collision hardness #ifdef ESP8266 uint16_t maxnumParticles = min((uint16_t)((PartSys->maxXpixel * PartSys->maxYpixel) >> 1), PartSys->numParticles); @@ -8783,17 +8788,20 @@ uint16_t mode_particlebox(void) if(SEGMENT.check1) //random, use perlin noise { - xgravity = ((int16_t)inoise8(SEGMENT.aux0) - 127); // TODO: inoise 16 would be faster? + xgravity = ((int16_t)inoise8(SEGMENT.aux0) - 127); ygravity = ((int16_t)inoise8(SEGMENT.aux0 + 10000) - 127); - // scale the gravity force down - xgravity /= 2 + ((256 - SEGMENT.custom1) >> 3); //(divide by 1-32) - ygravity /= 2 + ((256 - SEGMENT.custom1) >> 3); + // scale the gravity force + //xgravity /= 1 + ((256 - SEGMENT.custom1) >> 5); + //ygravity /= 1 + ((256 - SEGMENT.custom1) >> 5); + // scale the gravity force + xgravity = (xgravity * SEGMENT.custom1) / 50; + ygravity = (ygravity * SEGMENT.custom1) / 50; } else //go in a circle { // PartSys->applyAngleForce(PartSys->particles, PartSys->usedParticles, SEGMENT.custom1>>2, SEGMENT.aux0<<8);//not used, calculate here directly as perlin noise force is in x and y, not angle - xgravity = ((int32_t)(SEGMENT.custom1 >> 3) * cos16(SEGMENT.aux0 << 8)) / 0xFFFF; - ygravity = ((int32_t)(SEGMENT.custom1 >> 3) * sin16(SEGMENT.aux0 << 8)) / 0xFFFF; + xgravity = ((int32_t)(SEGMENT.custom1) * cos16(SEGMENT.aux0 << 8)) / 0xFFFF; + ygravity = ((int32_t)(SEGMENT.custom1) * sin16(SEGMENT.aux0 << 8)) / 0xFFFF; } if (SEGMENT.check3) //sloshing, y force is alwys downwards { @@ -9046,17 +9054,11 @@ uint16_t mode_particleattractor(void) PSsettings sourcesettings;// = {0, 0, 1, 1, 0, 0, 0, 0}; // PS settings for bounceY, bounceY used for source movement (it always bounces whereas particles do not) sourcesettings.asByte = 0b00001100; PSparticle *attractor; //particle pointer to the attractor - -/* - PSsettings sourcesettings; - uint8_t *settingsPtr = reinterpret_cast(&sourcesettings); // access settings as one byte (wmore efficient in code and speed) - *settingsPtr = 0b00001100; // PS settings for bounceY, bounceY used for source movement (it always bounces whereas particles do not) -*/ if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. { if (!initParticleSystem(PartSys, 1, true)) // init using 1 source and advanced particle settings return mode_static(); // allocation failed; //allocation failed - + //DEBUG_PRINTF_P(PSTR("sources in FX %p\n"), &PartSys->sources[0]); PartSys->sources[0].source.hue = random16(); PartSys->sources[0].source.vx = -7; PartSys->sources[0].source.collide = true; // seeded particles will collide @@ -9090,11 +9092,9 @@ uint16_t mode_particleattractor(void) PartSys->enableParticleCollisions(true, map(SEGMENT.custom2, 1, 255, 120, 255)); // enable collisions and set particle collision hardness else PartSys->enableParticleCollisions(false); - uint16_t lastusedparticle = (PartSys->numParticles * 3) >> 2; //use 3/4 of particles uint32_t displayparticles = map(SEGMENT.intensity, 0, 255, 10, lastusedparticle); PartSys->setUsedParticles(displayparticles); - // set pointers //attractor = reinterpret_cast(&PartSys->particles[lastusedparticle + 1]); attractor = &PartSys->particles[lastusedparticle + 1]; @@ -9116,21 +9116,18 @@ uint16_t mode_particleattractor(void) attractor->x = PartSys->maxX >> 1; // set to center attractor->y = PartSys->maxY >> 1; } - if (SEGMENT.call % 5 == 0) { PartSys->sources[0].source.hue++; PartSys->sources[0].source.ttl = 100; //spray never dies } - SEGMENT.aux0 += 256; //emitting angle, one full turn in 255 frames (0xFFFF is 360°) if (SEGMENT.call % 2 == 0) // alternate direction of emit PartSys->angleEmit(PartSys->sources[0], SEGMENT.aux0, 12); //PartSys->angleEmit(PartSys->sources[0], SEGMENT.aux0, SEGMENT.custom1 >> 4); else PartSys->angleEmit(PartSys->sources[0], SEGMENT.aux0 + 0x7FFF, 12); // emit at 180° as well - //PartSys->angleEmit(PartSys->sources[0], SEGMENT.aux0 + 0x7FFF, SEGMENT.custom1 >> 4); // emit at 180° as well - + //PartSys->angleEmit(PartSys->sources[0], SEGMENT.aux0 + 0x7FFF, SEGMENT.custom1 >> 4); // apply force for(i = 0; i < displayparticles; i++) { @@ -9138,7 +9135,6 @@ uint16_t mode_particleattractor(void) } if (SEGMENT.call % (33 - SEGMENT.custom3) == 0) PartSys->applyFriction(2); - PartSys->particleMoveUpdate(PartSys->sources[0].source, sourcesettings); // move the source PartSys->update(); // update and render return FRAMETIME; @@ -9273,8 +9269,7 @@ uint16_t mode_particlespray(void) if (SEGLEN == 1) return mode_static(); ParticleSystem *PartSys = NULL; - uint8_t numSprays; - uint32_t i; + //uint8_t numSprays; const uint8_t hardness = 200; //collision hardness is fixed if (SEGMENT.call == 0) // initialization @@ -9306,7 +9301,7 @@ uint16_t mode_particlespray(void) PartSys->setWrapX(SEGMENT.check2); PartSys->setWallHardness(hardness); PartSys->setGravity(8 * SEGMENT.check1); //enable gravity if checked (8 is default strength) - numSprays = min(PartSys->numSources, (uint8_t)1); // number of sprays + //numSprays = min(PartSys->numSources, (uint8_t)1); // number of sprays if (SEGMENT.check3) // collisions enabled PartSys->enableParticleCollisions(true, hardness); // enable collisions and set particle collision hardness diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index de083c464b..12fbf14944 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -58,10 +58,15 @@ ParticleSystem::ParticleSystem(uint16_t width, uint16_t height, uint16_t numbero setWallHardness(255); // set default wall hardness to max setWallRoughness(0); // smooth walls by default !!! testing this setGravity(0); //gravity disabled by default - setSaturation(255); //full saturation by default setParticleSize(0); // minimum size by default motionBlur = 0; //no fading by default emitIndex = 0; + + //initialize some default non-zero values most FX use + for (int i = 0; i < numSources; i++) + { + sources[i].source.sat = 255; //set saturation to max by default + } /* Serial.println("alive particles: "); uint32_t aliveparticles = 0; @@ -179,11 +184,6 @@ void ParticleSystem::setKillOutOfBounds(bool enable) particlesettings.killoutofbounds = enable; } -void ParticleSystem::setSaturation(uint8_t sat) -{ - saturation = sat; -} - void ParticleSystem::setColorByAge(bool enable) { particlesettings.colorByAge = enable; @@ -238,12 +238,10 @@ void ParticleSystem::sprayEmit(PSsource &emitter) particles[emitIndex].vy = emitter.vy + random(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; particles[emitIndex].collide = emitter.source.collide; - if (advPartProps) - { - advPartProps[emitIndex].sat = emitter.sat; - advPartProps[emitIndex].size = emitter.size; - } + if (advPartProps) + advPartProps[emitIndex].size = emitter.size; break; } /* @@ -657,27 +655,32 @@ void ParticleSystem::ParticleSys_render(bool firemode, uint32_t fireintensity) // allocate empty memory for the local renderbuffer framebuffer = allocate2Dbuffer(maxXpixel + 1, maxYpixel + 1); if (framebuffer == NULL) - useLocalBuffer = false; //render to segment pixels directly if not enough memory - - if (motionBlur > 0) // using SEGMENT.fadeToBlackBy is much slower, this approximately doubles the speed of fade calculation { - uint32_t yflipped; - for (int y = 0; y <= maxYpixel; y++) + Serial.println("Frame buffer alloc failed"); + useLocalBuffer = false; //render to segment pixels directly if not enough memory + } + else{ + if (motionBlur > 0) // using SEGMENT.fadeToBlackBy is much slower, this approximately doubles the speed of fade calculation { - yflipped = maxYpixel - y; - for (int x = 0; x <= maxXpixel; x++) + uint32_t yflipped; + for (uint32_t y = 0; y <= maxYpixel; y++) { - framebuffer[x][y] = SEGMENT.getPixelColorXY(x, yflipped); //copy to local buffer - fast_color_scale(framebuffer[x][y], motionBlur); + yflipped = maxYpixel - y; + for (uint32_t x = 0; x <= maxXpixel; x++) + { + framebuffer[x][y] = SEGMENT.getPixelColorXY(x, yflipped); //copy to local buffer + fast_color_scale(framebuffer[x][y], motionBlur); + } } } - } - if(advPartProps) - { - renderbuffer = allocate2Dbuffer(10, 10); //buffer to render individual particles to if size > 0 note: null checking is done when accessing it + if(advPartProps) + { + renderbuffer = allocate2Dbuffer(10, 10); //buffer to render individual particles to if size > 0 note: null checking is done when accessing it + } } } - else + + if(!useLocalBuffer) //disabled or allocation above failed { if (motionBlur > 0) SEGMENT.fadeToBlackBy(256 - motionBlur); @@ -701,19 +704,16 @@ void ParticleSystem::ParticleSys_render(bool firemode, uint32_t fireintensity) //brightness = (((uint32_t)particles[i].ttl * (maxY + PS_P_RADIUS - particles[i].y)) >> 7) + particles[i].ttl + (fireintensity>>1); // this is experimental //brightness = (((uint32_t)particles[i].ttl * (maxY + PS_P_RADIUS - particles[i].y)) >> 7) + ((particles[i].ttl * fireintensity) >> 5); // this is experimental TODO: test this -> testing... ok but not the best, bit sparky brightness = (((uint32_t)particles[i].ttl * (maxY + PS_P_RADIUS - particles[i].y)) >> 7) + (fireintensity >> 1); // this is experimental TODO: test this -> testing... does not look too bad! - brightness > 255 ? 255 : brightness; // faster then using min() + brightness = brightness > 255 ? 255 : brightness; // faster then using min() baseRGB = ColorFromPalette(SEGPALETTE, brightness, 255, LINEARBLEND); } else{ brightness = particles[i].ttl > 255 ? 255 : particles[i].ttl; //faster then using min() baseRGB = ColorFromPalette(SEGPALETTE, particles[i].hue, 255, LINEARBLEND); - if (saturation < 255) + if (particles[i].sat < 255) { CHSV baseHSV = rgb2hsv_approximate(baseRGB); //convert to hsv - if(advPartProps) - baseHSV.s = advPartProps[i].sat; - else - baseHSV.s = saturation; + baseHSV.s = particles[i].sat; baseRGB = (CRGB)baseHSV; //convert back to RGB } } @@ -787,10 +787,15 @@ void ParticleSystem::renderParticle(CRGB **framebuffer, uint32_t particleindex, int32_t y = yoffset >> PS_P_RADIUS_SHIFT; //check if particle has advanced size properties and buffer is available - if(advPartProps && renderbuffer) + if(advPartProps) { if(advPartProps[particleindex].size > 0) - advancedrender = true; + { + if(renderbuffer) + advancedrender = true; + else + return; //cannot render without buffer, advanced size particles are allowed out of frame + } } // set the four raw pixel coordinates, the order is bottom left [0], bottom right[1], top right [2], top left [3] @@ -1394,22 +1399,26 @@ void ParticleSystem::updatePSpointers(bool isadvanced) { //DEBUG_PRINT(F("*** PS pointers ***")); //DEBUG_PRINTF_P(PSTR("this PS %p\n"), this); - byte *nextaddress = NULL; + //Note on memory alignment: + //a pointer MUST be 4 byte aligned. sizeof() in a struct/class is always aligned to the largest element. if it contains a 32bit, it will be padded to 4 bytes, 16bit is padded to 2byte alignment. + //The PS is aligned to 4 bytes, a PSparticle is aligned to 2 and a struct containing only byte sized variables is not aligned at all and may need to be padded when dividing the memoryblock. + //by making sure that the number of sources and particles is a multiple of 4, padding can be skipped here as alignent is ensured, independent of struct sizes. particles = reinterpret_cast(this + 1); // pointer to particle array at data+sizeof(ParticleSystem) if(isadvanced) { - advPartProps = reinterpret_cast(particles + numParticles); - sources = reinterpret_cast(advPartProps + numParticles); // pointer to source(s) + sources = reinterpret_cast(particles + numParticles); // pointer to source(s) + advPartProps = reinterpret_cast(sources + numParticles); } else { - advPartProps = NULL; sources = reinterpret_cast(particles + numParticles); // pointer to source(s) + advPartProps = NULL; } PSdataEnd = reinterpret_cast(sources + numSources); // pointer to first available byte after the PS for FX additional data //DEBUG_PRINTF_P(PSTR("particles %p\n"), particles); //DEBUG_PRINTF_P(PSTR("sources %p\n"), sources); + //DEBUG_PRINTF_P(PSTR("adv. props %p\n"), advPartProps); //DEBUG_PRINTF_P(PSTR("end %p\n"), PSdataEnd); } @@ -1431,6 +1440,8 @@ uint32_t calculateNumberOfParticles(bool isadvanced) numberofParticles = max((uint32_t)1, min(numberofParticles, particlelimit)); if (isadvanced) // advanced property array needs ram, reduce number of particles to use the same amount numberofParticles = (numberofParticles * sizeof(PSparticle)) / (sizeof(PSparticle) + sizeof(PSadvancedParticle)); + //make sure it is a multiple of 4 for proper memory alignment + numberofParticles = ((numberofParticles+3) >> 2) << 2; return numberofParticles; } @@ -1448,6 +1459,8 @@ uint32_t calculateNumberOfSources(uint8_t requestedsources) int numberofSources = min((cols * rows) / 4, (uint32_t)requestedsources); numberofSources = max(1, min(numberofSources, ESP32_MAXSOURCES)); // limit to 1 - 64 #endif + //make sure it is a multiple of 4 for proper memory alignment + numberofSources = ((numberofSources+3) >> 2) << 2; return numberofSources; } @@ -1455,6 +1468,7 @@ uint32_t calculateNumberOfSources(uint8_t requestedsources) bool allocateParticleSystemMemory(uint16_t numparticles, uint16_t numsources, bool isadvanced, uint16_t additionalbytes) { uint32_t requiredmemory = sizeof(ParticleSystem); + //functions above make sure these are a multiple of 4 bytes (to avoid alignment issues) requiredmemory += sizeof(PSparticle) * numparticles; if (isadvanced) requiredmemory += sizeof(PSadvancedParticle) * numparticles; diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index fba1f203bf..328f1129d7 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -30,7 +30,7 @@ #include "FastLED.h" //memory allocation -#define ESP8266_MAXPARTICLES 160 // enough for one 16x16 segment with transitions +#define ESP8266_MAXPARTICLES 200// // enough for one 16x16 segment with transitions #define ESP8266_MAXSOURCES 16 #define ESP32S2_MAXPARTICLES 840 // enough for four 16x16 segments #define ESP32S2_MAXSOURCES 48 @@ -46,25 +46,26 @@ #define PS_P_MINSURFACEHARDNESS 128 //minimum hardness used in collision impulse calculation, below this hardness, particles become sticky #define PS_P_MAXSPEED 120 //maximum speed a particle can have (vx/vy is int8) -//struct for a single particle (9 bytes) +//struct for a single particle (10 bytes) 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 + uint8_t sat; //particle color saturation //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 set, particle takes part in collisions bool flag3 : 1; // unused flags... - bool flag4 : 1; + bool flag4 : 1; } PSparticle; // struct for additional particle settings (optional) typedef struct { - uint8_t sat; //particle color saturation + uint8_t size; //particle size, 255 means 10 pixels in diameter uint8_t forcecounter; //counter for applying forces to individual particles @@ -101,7 +102,6 @@ typedef struct { uint8_t var; // variation of emitted speed (use odd numbers for good symmetry) int8_t vx; // emitting speed int8_t vy; - uint8_t sat; // particle saturation (advanced property) uint8_t size; // particle size (advanced property) } PSsource; @@ -122,6 +122,7 @@ typedef union byte asByte; } PSsettings; +//class uses approximately 60 bytes class ParticleSystem { public: @@ -209,7 +210,6 @@ class ParticleSystem uint8_t collisioncounter; //counter to handle collisions TODO: could use the SEGMENT.call? uint8_t forcecounter; //counter for globally applied forces //global particle properties for basic particles - uint8_t saturation; //note: on advanced particles, set this to 255 to disable saturation rendering, any other value uses particle sat value uint8_t particlesize; //global particle size, 0 = 2 pixels, 255 = 10 pixels (note: this is also added to individual sized particles) int32_t particleHardRadius; // hard surface radius of a particle, used for collision detection uint8_t motionBlur; //enable motion blur, values > 100 gives smoother animations. Note: motion blurring does not work if particlesize is > 0 From 33ede416e5e430e70e49cc037f338e8238111492 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Mon, 29 Apr 2024 19:46:19 +0200 Subject: [PATCH 069/219] Replaced Ghost Rider FX with PS version - new FX is kept close to original animation but added more user settings --- wled00/FX.cpp | 89 ++++++++++++++++++++++++++++++++++++++- wled00/FX.h | 3 +- wled00/FXparticleSystem.h | 2 +- 3 files changed, 90 insertions(+), 4 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 57700cac58..8c5ee32644 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -5912,6 +5912,7 @@ static const char _data_FX_MODE_2DCRAZYBEES[] PROGMEM = "Crazy Bees@!,Blur;;;2"; // 2D Ghost Rider // ///////////////////////// //// Ghost Rider by stepko (c)2021 [https://editor.soulmatelights.com/gallery/716-ghost-rider], adapted by Blaz Kristan (AKA blazoncek) +/* #define LIGHTERS_AM 64 // max lighters (adequate for 32x32 matrix) uint16_t mode_2Dghostrider(void) { if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up @@ -5996,7 +5997,7 @@ uint16_t mode_2Dghostrider(void) { return FRAMETIME; } static const char _data_FX_MODE_2DGHOSTRIDER[] PROGMEM = "Ghost Rider@Fade rate,Blur;;!;2"; - +*/ //////////////////////////// // 2D Floating Blobs // @@ -9425,6 +9426,89 @@ uint16_t mode_particleGEQ(void) } static const char _data_FX_MODE_PARTICLEGEQ[] PROGMEM = "PS Equalizer@Speed,Intensity,Diverge,Bounce,Gravity,Cylinder,Walls,Floor;;!;2f;pal=0,sx=155,ix=200,c1=0,c2=128,o1=0,o2=0,o3=0"; +/* +Particle replacement of Ghost Rider by DedeHai (Damian Schneider), original by stepko adapted by Blaz Kristan (AKA blazoncek) +*/ +#define MAXANGLESTEP 2000 //32767 means 180° +uint16_t mode_particlghostrider(void) +{ + if (SEGLEN == 1) + return mode_static(); + ParticleSystem *PartSys = NULL; + PSsettings ghostsettings; + ghostsettings.asByte = 0b0000011; //enable wrapX and wrapY + + if (SEGMENT.call == 0) // initialization + { + if (!initParticleSystem(PartSys, 1)) // init, no additional data needed + return mode_static(); // allocation failed; //allocation failed + PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting) + PartSys->sources[0].maxLife = 260; // lifetime in frames + PartSys->sources[0].minLife = 250; + PartSys->sources[0].source.x = random16(PartSys->maxX); + PartSys->sources[0].source.y = random16(PartSys->maxY); + SEGMENT.step = random(MAXANGLESTEP) - (MAXANGLESTEP>>1); //angle increment + } + else + PartSys = reinterpret_cast(SEGMENT.data); // if not first call, just set the pointer to the PS + + if (PartSys == NULL) + { + DEBUG_PRINT(F("ERROR: FX PartSys nullpointer")); + return mode_static(); // something went wrong, no data! + } + + if(SEGMENT.intensity > 0) //spiraling + { + if(SEGMENT.aux1) + { + SEGMENT.step += SEGMENT.intensity>>3; + if((int32_t)SEGMENT.step > MAXANGLESTEP) + SEGMENT.aux1 = 0; + } + else + { + SEGMENT.step -= SEGMENT.intensity>>3; + if((int32_t)SEGMENT.step < -MAXANGLESTEP) + SEGMENT.aux1 = 1; + } + } + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setMotionBlur(SEGMENT.custom1); + //PartSys->setColorByAge(SEGMENT.check1); + PartSys->sources[0].var = (1 + (SEGMENT.custom3>>1)) | 0x01; //odd numbers only + + //color by age (PS always starts with hue = 255 so cannot use that) + if(SEGMENT.check1) + { + for(int i = 0; i < PartSys->usedParticles; i++) + { + PartSys->particles[i].hue = PartSys->sources[0].source.hue + (PartSys->particles[i].ttl<<2); + } + } + SEGMENT.aux0 += (int32_t)SEGMENT.step; //step is angle increment + + uint16_t emitangle = SEGMENT.aux0 + 32767; //+180° + int32_t speed = map(SEGMENT.speed, 0, 255, 12, 64); + PartSys->sources[0].source.vx = ((int32_t)cos16(SEGMENT.aux0) * speed) / (int32_t)32767; + PartSys->sources[0].source.vy = ((int32_t)sin16(SEGMENT.aux0) * speed) / (int32_t)32767; + PartSys->sources[0].source.ttl = 500; //source never dies + PartSys->particleMoveUpdate(PartSys->sources[0].source, ghostsettings); + //set head (steal one of the particles) + PartSys->particles[PartSys->usedParticles-1].x = PartSys->sources[0].source.x; + PartSys->particles[PartSys->usedParticles-1].y = PartSys->sources[0].source.y; + PartSys->particles[PartSys->usedParticles-1].ttl = PartSys->sources[0].source.ttl; + PartSys->particles[PartSys->usedParticles-1].sat = 0; + //emit two particles + PartSys->angleEmit(PartSys->sources[0], emitangle, speed); + PartSys->angleEmit(PartSys->sources[0], emitangle, speed); + PartSys->sources[0].source.hue += SEGMENT.custom2 >> 3; + + PartSys->update(); // update and render + return FRAMETIME; +} +static const char _data_FX_MODE_PARTICLEGHOSTRIDER[] PROGMEM = "PS Ghost Rider@Speed,Spiral,Blur,Color Cycle,Spread,Color by age;;!;2;pal=1,sx=70,ix=0,c1=220,c2=30,c3=21,o1=1,o2=0,o3=0"; /* * Particle rotating GEQ @@ -9753,7 +9837,7 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_2DPLASMAROTOZOOM, &mode_2Dplasmarotozoom, _data_FX_MODE_2DPLASMAROTOZOOM); addEffect(FX_MODE_2DSPACESHIPS, &mode_2Dspaceships, _data_FX_MODE_2DSPACESHIPS); addEffect(FX_MODE_2DCRAZYBEES, &mode_2Dcrazybees, _data_FX_MODE_2DCRAZYBEES); - addEffect(FX_MODE_2DGHOSTRIDER, &mode_2Dghostrider, _data_FX_MODE_2DGHOSTRIDER); + //addEffect(FX_MODE_2DGHOSTRIDER, &mode_2Dghostrider, _data_FX_MODE_2DGHOSTRIDER); //addEffect(FX_MODE_2DBLOBS, &mode_2Dfloatingblobs, _data_FX_MODE_2DBLOBS); addEffect(FX_MODE_2DSCROLLTEXT, &mode_2Dscrollingtext, _data_FX_MODE_2DSCROLLTEXT); addEffect(FX_MODE_2DDRIFTROSE, &mode_2Ddriftrose, _data_FX_MODE_2DDRIFTROSE); @@ -9810,6 +9894,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_PARTICLEGHOSTRIDER, &mode_particlghostrider, _data_FX_MODE_PARTICLEGHOSTRIDER); // addEffect(FX_MODE_PARTICLECENTERGEQ, &mode_particlecenterGEQ, _data_FX_MODE_PARTICLECCIRCULARGEQ); diff --git a/wled00/FX.h b/wled00/FX.h index dac7fe6058..03c15f6825 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -326,7 +326,8 @@ #define FX_MODE_PARTICLESPRAY 197 #define FX_MODE_PARTICLESGEQ 198 #define FX_MODE_PARTICLECENTERGEQ 199 -#define MODE_COUNT 200 +#define FX_MODE_PARTICLEGHOSTRIDER 200 +#define MODE_COUNT 201 typedef enum mapping1D2D { M12_Pixels = 0, diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index 328f1129d7..d976fa8b46 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -119,7 +119,7 @@ typedef union bool useCollisions : 1; bool colorByAge : 1; // if set, particle hue is set by ttl value in render function }; - byte asByte; + byte asByte; //order is: LSB is first entry in the list above } PSsettings; //class uses approximately 60 bytes From a6bf5b603d9f7e5f4184f01345047da95a0acd1f Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Tue, 30 Apr 2024 07:41:04 +0200 Subject: [PATCH 070/219] added walls to ghostride, fixed some bugs --- wled00/FX.cpp | 16 +++++++++++----- wled00/FXparticleSystem.cpp | 29 +++++++++++++---------------- wled00/FXparticleSystem.h | 2 +- 3 files changed, 25 insertions(+), 22 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 8c5ee32644..70e4747167 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -8770,7 +8770,7 @@ uint16_t mode_particlebox(void) for (i = 0; i < maxnumParticles; i++) { PartSys->particles[i].ttl = 500; //set all particles alive (not all are rendered though) - PartSys->particles[i].hue = i * 5; // color range + PartSys->particles[i].hue = i * 5; // color range PartSys->particles[i].x = map(i, 0, maxnumParticles, 1, PartSys->maxX); // distribute along x according to color PartSys->particles[i].y = random16(PartSys->maxY); // randomly in y direction PartSys->particles[i].collide = true; // all particles collide @@ -8975,7 +8975,7 @@ uint16_t mode_particleimpact(void) #ifdef ESP8266 emitparticles = random16(SEGMENT.intensity >> 3) + 5; // defines the size of the explosion #else - emitparticles = map(SEGMENT.intensity, 0, 255, 10, random16(PartSys->numParticles>>2)); // defines the size of the explosion !!!TODO: check if this works, drop esp8266 def if it does + emitparticles = map(SEGMENT.intensity, 0, 255, 10, random16(PartSys->numParticles>>2)); // defines the size of the explosion !!!TODO: check if this works on ESP8266, drop esp8266 def if it does #endif } for (int e = emitparticles; e > 0; e--) @@ -9429,7 +9429,7 @@ static const char _data_FX_MODE_PARTICLEGEQ[] PROGMEM = "PS Equalizer@Speed,Inte /* Particle replacement of Ghost Rider by DedeHai (Damian Schneider), original by stepko adapted by Blaz Kristan (AKA blazoncek) */ -#define MAXANGLESTEP 2000 //32767 means 180° +#define MAXANGLESTEP 2200 //32767 means 180° uint16_t mode_particlghostrider(void) { if (SEGLEN == 1) @@ -9487,10 +9487,16 @@ uint16_t mode_particlghostrider(void) PartSys->particles[i].hue = PartSys->sources[0].source.hue + (PartSys->particles[i].ttl<<2); } } - SEGMENT.aux0 += (int32_t)SEGMENT.step; //step is angle increment + //enable/disable walls + ghostsettings.bounceX = SEGMENT.check2; + ghostsettings.bounceY = SEGMENT.check2; + + SEGMENT.aux0 += (int32_t)SEGMENT.step; //step is angle increment uint16_t emitangle = SEGMENT.aux0 + 32767; //+180° int32_t speed = map(SEGMENT.speed, 0, 255, 12, 64); + int8_t newvx = ((int32_t)cos16(SEGMENT.aux0) * speed) / (int32_t)32767; + int8_t newvy = ((int32_t)sin16(SEGMENT.aux0) * speed) / (int32_t)32767; PartSys->sources[0].source.vx = ((int32_t)cos16(SEGMENT.aux0) * speed) / (int32_t)32767; PartSys->sources[0].source.vy = ((int32_t)sin16(SEGMENT.aux0) * speed) / (int32_t)32767; PartSys->sources[0].source.ttl = 500; //source never dies @@ -9508,7 +9514,7 @@ uint16_t mode_particlghostrider(void) PartSys->update(); // update and render return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEGHOSTRIDER[] PROGMEM = "PS Ghost Rider@Speed,Spiral,Blur,Color Cycle,Spread,Color by age;;!;2;pal=1,sx=70,ix=0,c1=220,c2=30,c3=21,o1=1,o2=0,o3=0"; +static const char _data_FX_MODE_PARTICLEGHOSTRIDER[] PROGMEM = "PS Ghost Rider@Speed,Spiral,Blur,Color Cycle,Spread,Color by age,Walls;;!;2;pal=1,sx=70,ix=0,c1=220,c2=30,c3=21,o1=1,o2=0,o3=0"; /* * Particle rotating GEQ diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index 12fbf14944..64fee78ccc 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -36,9 +36,6 @@ -add function to 'update sources' so FX does not have to take care of that. FX can still implement its own version if so desired. config should be optional, if not set, use default config. -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) - - */ // 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" @@ -56,7 +53,7 @@ ParticleSystem::ParticleSystem(uint16_t width, uint16_t height, uint16_t numbero updatePSpointers(isadvanced); // set the particle and sources pointer (call this before accessing sprays or particles) setMatrixSize(width, height); setWallHardness(255); // set default wall hardness to max - setWallRoughness(0); // smooth walls by default !!! testing this + setWallRoughness(0); // smooth walls by default setGravity(0); //gravity disabled by default setParticleSize(0); // minimum size by default motionBlur = 0; //no fading by default @@ -67,6 +64,10 @@ ParticleSystem::ParticleSystem(uint16_t width, uint16_t height, uint16_t numbero { sources[i].source.sat = 255; //set saturation to max by default } + for (int i = 0; i < numParticles; i++) + { + particles[i].sat = 255; // full saturation + } /* Serial.println("alive particles: "); uint32_t aliveparticles = 0; @@ -110,16 +111,16 @@ void ParticleSystem::update(void) } particleMoveUpdate(particles[i], particlesettings, advprop); } - //!!! remove this - //Serial.print("alive particles: "); + /*!!! remove this + Serial.print("alive particles: "); uint32_t aliveparticles = 0; for (int i = 0; i < numParticles; i++) { if(particles[i].ttl) aliveparticles++; } - - //Serial.println(aliveparticles); + Serial.println(aliveparticles); + */ ParticleSys_render(); } @@ -550,7 +551,7 @@ void ParticleSystem::applyFriction(uint8_t coefficient) } // attracts a particle to an attractor particle using the inverse square-law -void ParticleSystem::pointAttractor(uint16_t particleindex, PSparticle *attractor, uint8_t strength, bool swallow) // TODO: need to check if this is ok with new advancedprops !!! +void ParticleSystem::pointAttractor(uint16_t particleindex, PSparticle *attractor, uint8_t strength, bool swallow) { if (advPartProps == NULL) return; // no advanced properties available @@ -682,19 +683,18 @@ void ParticleSystem::ParticleSys_render(bool firemode, uint32_t fireintensity) if(!useLocalBuffer) //disabled or allocation above failed { + Serial.println("NOT using local buffer!"); if (motionBlur > 0) - SEGMENT.fadeToBlackBy(256 - motionBlur); + SEGMENT.fadeToBlackBy(255 - motionBlur); else SEGMENT.fill(BLACK); //clear the buffer before rendering to it } - uint32_t debug = 0; // go over particles and render them to the buffer for (i = 0; i < usedParticles; i++) { if (particles[i].outofbounds || particles[i].ttl == 0) continue; - debug++; // generate RGB values for particle if(firemode) { @@ -768,7 +768,6 @@ void ParticleSystem::ParticleSys_render(bool firemode, uint32_t fireintensity) } if(renderbuffer) free(renderbuffer); // free buffer memory - Serial.println(debug); } // calculate pixel positions and brightness distribution and render the particle to local buffer or global buffer @@ -1091,7 +1090,7 @@ void ParticleSystem::handleCollisions() } collisioncounter++; - //startparticle = 0;//!!! test: do all collisions every frame, FPS goes from about 52 to + //startparticle = 0;//!!!TODO test: do all collisions every frame //endparticle = usedParticles; for (i = startparticle; i < endparticle; i++) @@ -1363,9 +1362,7 @@ int32_t ParticleSystem::limitSpeed(int32_t speed) // allocate memory for the 2D array in one contiguous block and set values to zero CRGB **ParticleSystem::allocate2Dbuffer(uint32_t cols, uint32_t rows) { - //cli();//!!! test to see if anything messes with the allocation (flicker issues) CRGB ** array2D = (CRGB **)malloc(cols * sizeof(CRGB *) + cols * rows * sizeof(CRGB)); - //sei(); if (array2D == NULL) DEBUG_PRINT(F("PS buffer alloc failed")); else diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index d976fa8b46..dacf6ab5b1 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -30,7 +30,7 @@ #include "FastLED.h" //memory allocation -#define ESP8266_MAXPARTICLES 200// // enough for one 16x16 segment with transitions +#define ESP8266_MAXPARTICLES 180// // enough for one 16x16 segment with transitions #define ESP8266_MAXSOURCES 16 #define ESP32S2_MAXPARTICLES 840 // enough for four 16x16 segments #define ESP32S2_MAXSOURCES 48 From 06ae14c0d6766239b7776d6f8cadd37c5865eaf7 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Wed, 1 May 2024 07:07:48 +0200 Subject: [PATCH 071/219] added 'perpetual' flag to particles --- wled00/FX.cpp | 51 +++++++++++++------------------------ wled00/FXparticleSystem.cpp | 6 +++-- wled00/FXparticleSystem.h | 5 ++-- 3 files changed, 24 insertions(+), 38 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 70e4747167..50c4b122cd 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -7945,7 +7945,7 @@ uint16_t mode_particlevortex(void) PartSys->particles[PartSys->numParticles - i].x = (PartSys->maxX + 1) >> 1; // center PartSys->particles[PartSys->numParticles - i].y = (PartSys->maxY + 1) >> 1; // center PartSys->particles[PartSys->numParticles - i].sat = 230; - PartSys->particles[PartSys->numParticles - i].ttl = 100; //set alive + PartSys->particles[PartSys->numParticles - i].ttl = 255; //set alive } #endif if (SEGMENT.check1 != (SEGMENT.aux1 & 0x01) || SEGMENT.call == 0) //state change @@ -8250,7 +8250,8 @@ uint16_t mode_particlevolcano(void) PartSys->sources[i].source.x = PartSys->maxX / (numSprays + 1) * (i + 1); //distribute evenly PartSys->sources[i].maxLife = 300; // lifetime in frames PartSys->sources[i].minLife = 250; - PartSys->sources[i].source.collide = true; // seeded particles will collide (if enabled) + PartSys->sources[i].source.collide = true; // seeded particles will collide (if enabled) + PartSys->sources[i].source.perpetural = true; // source never dies } } else @@ -8287,8 +8288,7 @@ uint16_t mode_particlevolcano(void) PartSys->sources[i].source.vx > 0 ? SEGMENT.custom1 >> 4 : -(SEGMENT.custom1 >> 4); // set moving speed but keep the direction PartSys->sources[i].vy = SEGMENT.speed >> 2; // emitting speed PartSys->sources[i].vx = 0; - PartSys->sources[i].var = SEGMENT.custom3 | 0x01; // emiting variation = nozzle size (custom 3 goes from 0-31), only use odd numbers - PartSys->sources[i].source.ttl = 255; // source never dies, replenish its lifespan + PartSys->sources[i].var = SEGMENT.custom3 | 0x01; // emiting variation = nozzle size (custom 3 goes from 0-31), only use odd numbers // spray[j].source.hue = random16(); //set random color for each particle (using palette) -> does not look good PartSys->sprayEmit(PartSys->sources[i]); PartSys->particleMoveUpdate(PartSys->sources[i].source, PartSys->particlesettings); //move the source (also applies gravity, which is corrected for above, that is a hack but easier than creating more particlesettings) @@ -8321,7 +8321,6 @@ uint16_t mode_particlefire(void) if (!initParticleSystem(PartSys, 25, false, 4)) //maximum number of source (PS will determine the exact number based on segment size) and need 4 additional bytes for time keeping (uint32_t lastcall) return mode_static(); // allocation failed; //allocation failed SEGMENT.aux0 = rand(); // aux0 is wind position (index) in the perlin noise - // initialize the flame sprays numFlames = PartSys->numSources; DEBUG_PRINTF_P(PSTR("segment data ptr in fireFX %p\n"), SEGMENT.data); } @@ -8706,7 +8705,6 @@ uint16_t mode_particlewaterfall(void) PartSys->sources[i].vy = -SEGMENT.speed >> 3; // emitting speed, down PartSys->sources[i].source.x = map(SEGMENT.custom3, 0, 31, 0, (PartSys->maxXpixel - numSprays * 2) * PS_P_RADIUS) + i * PS_P_RADIUS * 2; // emitter position PartSys->sources[i].source.y = PartSys->maxY + (PS_P_RADIUS * ((i<<2) + 4)); // source y position, few pixels above the top to increase spreading before entering the matrix - //PartSys->sources[i].source.ttl = 255; // source never dies, replenish its lifespan TODO: source is not moved? PartSys->sources[i].var = (SEGMENT.custom1 >> 3) | 0x01; // emiting variation 0-32, only use odd numbers } @@ -8770,6 +8768,7 @@ uint16_t mode_particlebox(void) for (i = 0; i < maxnumParticles; i++) { PartSys->particles[i].ttl = 500; //set all particles alive (not all are rendered though) + PartSys->particles[i].perpetural = true; //never dies PartSys->particles[i].hue = i * 5; // color range PartSys->particles[i].x = map(i, 0, maxnumParticles, 1, PartSys->maxX); // distribute along x according to color PartSys->particles[i].y = random16(PartSys->maxY); // randomly in y direction @@ -8791,16 +8790,12 @@ uint16_t mode_particlebox(void) { xgravity = ((int16_t)inoise8(SEGMENT.aux0) - 127); ygravity = ((int16_t)inoise8(SEGMENT.aux0 + 10000) - 127); - // scale the gravity force - //xgravity /= 1 + ((256 - SEGMENT.custom1) >> 5); - //ygravity /= 1 + ((256 - SEGMENT.custom1) >> 5); // scale the gravity force xgravity = (xgravity * SEGMENT.custom1) / 50; ygravity = (ygravity * SEGMENT.custom1) / 50; } else //go in a circle - { - // PartSys->applyAngleForce(PartSys->particles, PartSys->usedParticles, SEGMENT.custom1>>2, SEGMENT.aux0<<8);//not used, calculate here directly as perlin noise force is in x and y, not angle + { xgravity = ((int32_t)(SEGMENT.custom1) * cos16(SEGMENT.aux0 << 8)) / 0xFFFF; ygravity = ((int32_t)(SEGMENT.custom1) * sin16(SEGMENT.aux0 << 8)) / 0xFFFF; } @@ -8811,15 +8806,6 @@ uint16_t mode_particlebox(void) } PartSys->applyForce(xgravity, ygravity); - - // reset particle TTL so they never die - for (i = 0; i < PartSys->usedParticles; i++) - { - if (PartSys->particles[i].ttl > 0) - { - PartSys->particles[i].ttl = 500; // particles never die - } - } } if (SEGMENT.call % (32-SEGMENT.custom3) == 0) @@ -9016,7 +9002,7 @@ uint16_t mode_particleimpact(void) } } } - else if ( PartSys->sources[i].source.vy > 0) // meteor is exploded and time is up (ttl==0 and positive speed), relaunch it + else if (PartSys->sources[i].source.vy > 0) // meteor is exploded and time is up (ttl==0 and positive speed), relaunch it { // reinitialize meteor PartSys->sources[i].source.y = PartSys->maxY + (PS_P_RADIUS << 2); // start 4 pixels above the top @@ -9024,7 +9010,7 @@ uint16_t mode_particleimpact(void) PartSys->sources[i].source.vy = -random16(30) - 30; // meteor downward speed PartSys->sources[i].source.vx = random(30) - 15; PartSys->sources[i].source.hue = random16(); // random color - PartSys->sources[i].source.ttl = 2000; // long life, will explode at bottom + PartSys->sources[i].source.ttl = 500; // long life, will explode at bottom PartSys->sources[i].source.collide = false; // trail particles will not collide PartSys->sources[i].maxLife = 60; // spark particle life PartSys->sources[i].minLife = 20; @@ -9063,7 +9049,8 @@ uint16_t mode_particleattractor(void) PartSys->sources[0].source.hue = random16(); PartSys->sources[0].source.vx = -7; PartSys->sources[0].source.collide = true; // seeded particles will collide - PartSys->sources[0].source.ttl = 100; //is replenished below, it never dies + //PartSys->sources[0].source.ttl = 100; //TODO: remove, is now done in PS init + PartSys->sources[0].source.perpetural = true; //source does not age #ifdef ESP8266 PartSys->sources[0].maxLife = 200; // lifetime in frames (ESP8266 has less particles) PartSys->sources[0].minLife = 30; @@ -9103,25 +9090,23 @@ uint16_t mode_particleattractor(void) { attractor->vx = PartSys->sources[0].source.vy; // set to spray movemement but reverse x and y attractor->vy = PartSys->sources[0].source.vx; + attractor->ttl = 100; + attractor->perpetural = true; } //set attractor properties if (SEGMENT.check2) { if((SEGMENT.call % 3) == 0) // move slowly - { - attractor->ttl = 100; //must be alive to move - PartSys->particleMoveUpdate(*attractor, sourcesettings); // move the attractor - } + PartSys->particleMoveUpdate(*attractor, sourcesettings); // move the attractor + } else{ attractor->x = PartSys->maxX >> 1; // set to center attractor->y = PartSys->maxY >> 1; } - if (SEGMENT.call % 5 == 0) - { + if (SEGMENT.call % 5 == 0) PartSys->sources[0].source.hue++; - PartSys->sources[0].source.ttl = 100; //spray never dies - } + SEGMENT.aux0 += 256; //emitting angle, one full turn in 255 frames (0xFFFF is 360°) if (SEGMENT.call % 2 == 0) // alternate direction of emit PartSys->angleEmit(PartSys->sources[0], SEGMENT.aux0, 12); @@ -9479,7 +9464,7 @@ uint16_t mode_particlghostrider(void) //PartSys->setColorByAge(SEGMENT.check1); PartSys->sources[0].var = (1 + (SEGMENT.custom3>>1)) | 0x01; //odd numbers only - //color by age (PS always starts with hue = 255 so cannot use that) + //color by age (PS color by age always starts with hue = 255 so cannot use that) if(SEGMENT.check1) { for(int i = 0; i < PartSys->usedParticles; i++) @@ -9504,7 +9489,7 @@ uint16_t mode_particlghostrider(void) //set head (steal one of the particles) PartSys->particles[PartSys->usedParticles-1].x = PartSys->sources[0].source.x; PartSys->particles[PartSys->usedParticles-1].y = PartSys->sources[0].source.y; - PartSys->particles[PartSys->usedParticles-1].ttl = PartSys->sources[0].source.ttl; + PartSys->particles[PartSys->usedParticles-1].ttl = 255; PartSys->particles[PartSys->usedParticles-1].sat = 0; //emit two particles PartSys->angleEmit(PartSys->sources[0], emitangle, speed); diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index 64fee78ccc..57f5094ea5 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -63,6 +63,7 @@ ParticleSystem::ParticleSystem(uint16_t width, uint16_t height, uint16_t numbero for (int i = 0; i < numSources; i++) { sources[i].source.sat = 255; //set saturation to max by default + sources[i].source.ttl = 1; //set source alive } for (int i = 0; i < numParticles; i++) { @@ -304,8 +305,9 @@ void ParticleSystem::particleMoveUpdate(PSparticle &part, PSsettings &options, P if (part.ttl > 0) { - // age - part.ttl--; + + if(!part.perpetural) + part.ttl--; // age if (particlesettings.colorByAge) part.hue = part.ttl > 255 ? 255 : part.ttl; //set color to ttl diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index dacf6ab5b1..7413dccedc 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -58,14 +58,13 @@ typedef struct { uint16_t ttl : 12; // time to live, 12 bit or 4095 max (which is 50s at 80FPS) bool outofbounds : 1; //out of bounds flag, set to true if particle is outside of display area bool collide : 1; //if set, particle takes part in collisions - bool flag3 : 1; // unused flags... - bool flag4 : 1; + bool perpetural : 1; //if set, particle does not age (TTL is not decremented in move function, it still dies from killoutofbounds) + bool flag4 : 1; // unused flag } PSparticle; // struct for additional particle settings (optional) typedef struct { - uint8_t size; //particle size, 255 means 10 pixels in diameter uint8_t forcecounter; //counter for applying forces to individual particles From 3527144b94b61ba46134e635dd05294c14980b87 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Wed, 1 May 2024 12:08:03 +0200 Subject: [PATCH 072/219] Fixed another memory / pointer bug, but there is still one left... -also some minor fixes --- wled00/FX.cpp | 22 ++++++++----- wled00/FXparticleSystem.cpp | 61 ++++++++++++++++++++++++------------- 2 files changed, 55 insertions(+), 28 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 50c4b122cd..c0fc30c747 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -8078,9 +8078,9 @@ uint16_t mode_particlefireworks(void) numRockets = min(PartSys->numSources, (uint8_t)NUMBEROFSOURCES); for (j = 0; j < numRockets; j++) { - PartSys->sources[j].source.ttl = 500 * j; // first rocket starts immediately, others follow soon - PartSys->sources[j].source.vy = -1; // at negative speed, no particles are emitted and if rocket dies, it will be relaunched - } + PartSys->sources[j].source.ttl = 500 * j; // first rocket starts immediately, others follow soon + PartSys->sources[j].source.vy = -1; // at negative speed, no particles are emitted and if rocket dies, it will be relaunched + } } else PartSys = reinterpret_cast(SEGMENT.data); // if not first call, just set the pointer to the PS @@ -8167,6 +8167,14 @@ uint16_t mode_particlefireworks(void) } else { + /* + if( PartSys->sources[j].source.vy < 0) //explosion is ongoing + { + if(i < (emitparticles>>2)) //set 1/4 of particles to larger size + PartSys->sources[j].size = 50+random16(140); + else + PartSys->sources[j].size = 0; + }*/ PartSys->sprayEmit(PartSys->sources[j]); if ((j % 3) == 0) { @@ -8353,7 +8361,7 @@ uint16_t mode_particlefire(void) } uint32_t spread = (PartSys->maxX >> 5) * (SEGMENT.custom3 + 1); //fire around segment center (in subpixel points) - numFlames = min((uint32_t)PartSys->numSources, (1 + ((spread / PS_P_RADIUS) << 1))); // number of flames used depends on spread with, good value is (fire width in pixel) * 2 + numFlames = min((uint32_t)PartSys->numSources, (2 + ((spread / PS_P_RADIUS) << 1))); // number of flames used depends on spread with, good value is (fire width in pixel) * 2 uint32_t percycle = numFlames*2/3;// / 2; // maximum number of particles emitted per cycle (TODO: for ESP826 maybe use flames/2) // percycle = map(SEGMENT.intensity,0,255, 2, (numFlames*3) / 2); //TODO: does this give better flames or worse? @@ -8420,7 +8428,7 @@ uint16_t mode_particlefire(void) return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEFIRE[] PROGMEM = "PS Fire@Speed,Intensity,Base Heat,Wind,Spread,Smooth,Cylinder,Turbulence;;!;2;pal=35,sx=110,c1=110,c2=50,o1=1"; +static const char _data_FX_MODE_PARTICLEFIRE[] PROGMEM = "PS Fire@Speed,Intensity,Base Heat,Wind,Spread,Smooth,Cylinder,Turbulence;;!;2;pal=35,sx=110,c1=110,c2=50,c3=31,o1=1"; /* PS Ballpit: particles falling down, user can enable these three options: X-wraparound, side bounce, ground bounce @@ -8791,8 +8799,8 @@ uint16_t mode_particlebox(void) xgravity = ((int16_t)inoise8(SEGMENT.aux0) - 127); ygravity = ((int16_t)inoise8(SEGMENT.aux0 + 10000) - 127); // scale the gravity force - xgravity = (xgravity * SEGMENT.custom1) / 50; - ygravity = (ygravity * SEGMENT.custom1) / 50; + xgravity = (xgravity * SEGMENT.custom1) / 128; + ygravity = (ygravity * SEGMENT.custom1) / 128; } else //go in a circle { diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index 57f5094ea5..05037efb9b 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -320,7 +320,7 @@ void ParticleSystem::particleMoveUpdate(PSparticle &part, PSsettings &options, P { if(advancedproperties->size > 0) usesize = true; //note: variable eases out of frame checking below - particleHardRadius = max(PS_P_MINHARDRADIUS, (int)particlesize + advancedproperties->size); + particleHardRadius = max(PS_P_MINHARDRADIUS, (int)particlesize + (advancedproperties->size)); } //if wall collisions are enabled, bounce them before they reach the edge, it looks much nicer if the particle is not half out of view if (options.bounceX) @@ -340,7 +340,7 @@ void ParticleSystem::particleMoveUpdate(PSparticle &part, PSsettings &options, P bool isleaving = true; if(usesize) //using individual particle size { - if (((newX > -particleHardRadius) || (newX < maxX + particleHardRadius))) // large particle is not yet leaving the view - note: this is not pixel perfect but good enough + if (((newX > -particleHardRadius) && (newX < maxX + particleHardRadius))) // large particle is not yet leaving the view - note: this is not pixel perfect but good enough isleaving = false; } @@ -719,14 +719,8 @@ void ParticleSystem::ParticleSys_render(bool firemode, uint32_t fireintensity) baseRGB = (CRGB)baseHSV; //convert back to RGB } } - - if(renderbuffer) //set buffer to zero if it exists - { - memset(renderbuffer[0], 0, 100 * sizeof(CRGB)); // renderbuffer is 10x10 pixels. note: passing the buffer and setting it zero here is faster than creating a new buffer for every particle - } - - renderParticle(framebuffer, i, brightness, baseRGB, renderbuffer); + renderParticle(framebuffer, i, brightness, baseRGB, renderbuffer); } if(particlesize > 0) @@ -793,7 +787,27 @@ void ParticleSystem::renderParticle(CRGB **framebuffer, uint32_t particleindex, if(advPartProps[particleindex].size > 0) { if(renderbuffer) - advancedrender = true; + { + advancedrender = true; + //memset(renderbuffer[0], 0, 100 * sizeof(CRGB)); //clear the buffer, renderbuffer is 10x10 pixels + //memset seems to do something bad... trying to set it manually -> was probably a bug in the access below... TODO: remove this + + for(int i = 0; i<10;i++) //this works fine but is slower + { + for(int j = 0; j<10;j++) + { + renderbuffer[i][j] = BLACK; //note: this is way slower than memset (but safer) + } + } + + //faster: the memory block is 300bytes, or 75*32bit + /* + uint32_t* datablock = reinterpret_cast(renderbuffer[0]); + for(int i = 0; i<75;i++) + { + datablock[i] = 0; //note: this is almost as fast as memset but also not safe(?) + }*/ + } else return; //cannot render without buffer, advanced size particles are allowed out of frame } @@ -871,8 +885,6 @@ void ParticleSystem::renderParticle(CRGB **framebuffer, uint32_t particleindex, if (pxlbrightness[3] >= 0) pxlbrightness[3] = (precal1 * precal3) >> PS_P_SURFACE; // top left value equal to ((PS_P_RADIUS-dx) * dy * brightess) >> PS_P_SURFACE - - if(advancedrender) { //render particle to a bigger size @@ -885,6 +897,7 @@ void ParticleSystem::renderParticle(CRGB **framebuffer, uint32_t particleindex, fast_color_add(renderbuffer[4][5], color, pxlbrightness[3]); //TODO: make this a loop somehow? needs better coordinate handling... uint32_t rendersize = 4; uint32_t offset = 3; //offset to zero coordinate to write/read data in renderbuffer + //TODO: add asymmetrical size support blur2D(renderbuffer, rendersize, rendersize, advPartProps[particleindex].size, advPartProps[particleindex].size, true, offset, offset, true); //blur to 4x4 if (advPartProps[particleindex].size > 64) { @@ -915,20 +928,26 @@ void ParticleSystem::renderParticle(CRGB **framebuffer, uint32_t particleindex, { xfb = xfb_orig + xrb; if(xfb > maxXpixel) + { if (particlesettings.wrapX) // wrap x to the other side if required xfb = xfb % (maxXpixel + 1); else continue; + } + for(uint32_t yrb = offset; yrb < rendersize+offset; yrb++) { yfb = yfb_orig + yrb; if(yfb > maxYpixel) - if (particlesettings.wrapY) // wrap y to the other side if required - yfb = yfb % (maxYpixel + 1); - else - continue; + { + if (particlesettings.wrapY) // wrap y to the other side if required + yfb = yfb % (maxYpixel + 1); + else + continue; + } + //if(xfb < maxXpixel +1 && yfb < maxYpixel +1) - fast_color_add(framebuffer[xfb][yfb], renderbuffer[xrb][yrb]); //TODO: this is just a test, need to render to correct coordinates with out of frame checking + fast_color_add(framebuffer[xfb][yfb], renderbuffer[xrb][yrb]); } } } @@ -1364,7 +1383,7 @@ int32_t ParticleSystem::limitSpeed(int32_t speed) // allocate memory for the 2D array in one contiguous block and set values to zero CRGB **ParticleSystem::allocate2Dbuffer(uint32_t cols, uint32_t rows) { - CRGB ** array2D = (CRGB **)malloc(cols * sizeof(CRGB *) + cols * rows * sizeof(CRGB)); + CRGB ** array2D = (CRGB **)calloc(cols, sizeof(CRGB *) + rows * sizeof(CRGB)); if (array2D == NULL) DEBUG_PRINT(F("PS buffer alloc failed")); else @@ -1375,7 +1394,7 @@ CRGB **ParticleSystem::allocate2Dbuffer(uint32_t cols, uint32_t rows) { array2D[i] = start + i * rows; } - memset(start, 0, cols * rows * sizeof(CRGB)); // set all values to zero + //memset(start, 0, cols * rows * sizeof(CRGB)); // set all values to zero (TODO: remove, not needed if calloc is used) } return array2D; } @@ -1405,8 +1424,8 @@ void ParticleSystem::updatePSpointers(bool isadvanced) particles = reinterpret_cast(this + 1); // pointer to particle array at data+sizeof(ParticleSystem) if(isadvanced) { - sources = reinterpret_cast(particles + numParticles); // pointer to source(s) - advPartProps = reinterpret_cast(sources + numParticles); + sources = reinterpret_cast(particles + numParticles); // pointer to source(s) + advPartProps = reinterpret_cast(sources + numParticles); } else { From 77167a28546443c1e8be84f0713f40b227a64285 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Wed, 1 May 2024 13:53:59 +0200 Subject: [PATCH 073/219] Found and fixed crashes, it was wrongly assigned pointers --- wled00/FXparticleSystem.cpp | 54 +++++++++++++++++-------------------- 1 file changed, 24 insertions(+), 30 deletions(-) diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index 05037efb9b..3a0eb31252 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -646,6 +646,7 @@ void ParticleSystem::lineAttractor(uint16_t particleindex, PSparticle *attractor // fireintensity and firemode are optional arguments (fireintensity is only used in firemode) void ParticleSystem::ParticleSys_render(bool firemode, uint32_t fireintensity) { + CRGB baseRGB; bool useLocalBuffer = true; //use local rendering buffer, gives huge speed boost (at least 30% more FPS) CRGB **framebuffer = NULL; //local frame buffer @@ -655,14 +656,26 @@ void ParticleSystem::ParticleSys_render(bool firemode, uint32_t fireintensity) if (useLocalBuffer) { + cli(); //no interrupts so we get one block of memory for both buffers, less fragmentation + /* + Serial.print("heap: "); + Serial.print(heap_caps_get_free_size(MALLOC_CAP_8BIT)); + Serial.print(" block: "); + Serial.println(heap_caps_get_largest_free_block(MALLOC_CAP_8BIT));*/ // allocate empty memory for the local renderbuffer framebuffer = allocate2Dbuffer(maxXpixel + 1, maxYpixel + 1); if (framebuffer == NULL) { + sei(); //re enable interrupts Serial.println("Frame buffer alloc failed"); useLocalBuffer = false; //render to segment pixels directly if not enough memory } else{ + if(advPartProps) + { + renderbuffer = allocate2Dbuffer(10, 10); //buffer to render individual particles to if size > 0 note: null checking is done when accessing it + } + sei(); //re enable interrupts if (motionBlur > 0) // using SEGMENT.fadeToBlackBy is much slower, this approximately doubles the speed of fade calculation { uint32_t yflipped; @@ -676,11 +689,9 @@ void ParticleSystem::ParticleSys_render(bool firemode, uint32_t fireintensity) } } } - if(advPartProps) - { - renderbuffer = allocate2Dbuffer(10, 10); //buffer to render individual particles to if size > 0 note: null checking is done when accessing it - } + } + } if(!useLocalBuffer) //disabled or allocation above failed @@ -789,24 +800,7 @@ void ParticleSystem::renderParticle(CRGB **framebuffer, uint32_t particleindex, if(renderbuffer) { advancedrender = true; - //memset(renderbuffer[0], 0, 100 * sizeof(CRGB)); //clear the buffer, renderbuffer is 10x10 pixels - //memset seems to do something bad... trying to set it manually -> was probably a bug in the access below... TODO: remove this - - for(int i = 0; i<10;i++) //this works fine but is slower - { - for(int j = 0; j<10;j++) - { - renderbuffer[i][j] = BLACK; //note: this is way slower than memset (but safer) - } - } - - //faster: the memory block is 300bytes, or 75*32bit - /* - uint32_t* datablock = reinterpret_cast(renderbuffer[0]); - for(int i = 0; i<75;i++) - { - datablock[i] = 0; //note: this is almost as fast as memset but also not safe(?) - }*/ + memset(renderbuffer[0], 0, 100 * sizeof(CRGB)); //clear the buffer, renderbuffer is 10x10 pixels } else return; //cannot render without buffer, advanced size particles are allowed out of frame @@ -1416,7 +1410,7 @@ void ParticleSystem::updateSystem(void) void ParticleSystem::updatePSpointers(bool isadvanced) { //DEBUG_PRINT(F("*** PS pointers ***")); - //DEBUG_PRINTF_P(PSTR("this PS %p\n"), this); + //DEBUG_PRINTF_P(PSTR("this PS %p "), this); //Note on memory alignment: //a pointer MUST be 4 byte aligned. sizeof() in a struct/class is always aligned to the largest element. if it contains a 32bit, it will be padded to 4 bytes, 16bit is padded to 2byte alignment. //The PS is aligned to 4 bytes, a PSparticle is aligned to 2 and a struct containing only byte sized variables is not aligned at all and may need to be padded when dividing the memoryblock. @@ -1424,19 +1418,19 @@ void ParticleSystem::updatePSpointers(bool isadvanced) particles = reinterpret_cast(this + 1); // pointer to particle array at data+sizeof(ParticleSystem) if(isadvanced) { - sources = reinterpret_cast(particles + numParticles); // pointer to source(s) - advPartProps = reinterpret_cast(sources + numParticles); + advPartProps = reinterpret_cast(particles + numParticles); + sources = reinterpret_cast(advPartProps + numParticles); // pointer to source(s) } else { - sources = reinterpret_cast(particles + numParticles); // pointer to source(s) - advPartProps = NULL; + advPartProps = NULL; + sources = reinterpret_cast(particles + numParticles); // pointer to source(s) } PSdataEnd = reinterpret_cast(sources + numSources); // pointer to first available byte after the PS for FX additional data - //DEBUG_PRINTF_P(PSTR("particles %p\n"), particles); - //DEBUG_PRINTF_P(PSTR("sources %p\n"), sources); - //DEBUG_PRINTF_P(PSTR("adv. props %p\n"), advPartProps); + //DEBUG_PRINTF_P(PSTR(" particles %p "), particles); + //DEBUG_PRINTF_P(PSTR(" sources %p "), sources); + //DEBUG_PRINTF_P(PSTR(" adv. props %p\n"), advPartProps); //DEBUG_PRINTF_P(PSTR("end %p\n"), PSdataEnd); } From ac5399aa26f403dbb0c7ab05163c0bd9d0d60f6b Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sun, 5 May 2024 08:18:43 +0200 Subject: [PATCH 074/219] Added advanced particle size control and new Blob FX - advanced size control allows for growing, shrinking, wobbling - render function updated to support asymmetric rendering - various code improvements and bugfixes - some FX parameter tuning - bugfix: removed sli() sei() calls in render function that caused random flickering on S3/C3 (may add that back in but only for ESP8266 to reduce fragmentation) - removed some debug / test stuff --- wled00/FX.cpp | 263 +++++++++++++------------- wled00/FX.h | 3 +- wled00/FXparticleSystem.cpp | 361 +++++++++++++++++++++--------------- wled00/FXparticleSystem.h | 56 +++--- 4 files changed, 371 insertions(+), 312 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index c0fc30c747..df953a8750 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -8136,7 +8136,7 @@ uint16_t mode_particlefireworks(void) currentspeed = speed; counter = 0; angleincrement = 2730 + random16(5461); // minimum 15° (=2730), + random(30°) (=5461) - angle = random16(); // random start angle + angle = esp_random(); // random start angle speedvariation = angle & 0x01; // 0 or 1, no need for a new random number // calculate the number of particles to make complete circles percircle = (uint16_t)0xFFFF / angleincrement + 1; @@ -8193,7 +8193,7 @@ uint16_t mode_particlefireworks(void) { if (PartSys->sources[j].source.ttl) { - PartSys->particleMoveUpdate(PartSys->sources[j].source, PartSys->particlesettings); //todo: need different settings for rocket? + PartSys->particleMoveUpdate(PartSys->sources[j].source); //todo: need different settings for rocket? } else if (PartSys->sources[j].source.vy > 0) // rocket has died and is moving up. stop it so it will explode (is handled in the code above) { @@ -8209,9 +8209,9 @@ uint16_t mode_particlefireworks(void) { // reinitialize rocket PartSys->sources[j].source.y = PS_P_RADIUS<<1; // start from bottom - PartSys->sources[j].source.x = (rand() % (PartSys->maxX >> 1)) + (PartSys->maxX >> 2); // centered half + PartSys->sources[j].source.x = random(PartSys->maxX >> 2, PartSys->maxX >> 1); // centered half PartSys->sources[j].source.vy = random16(SEGMENT.custom1 >> 3) + 5; // rocket speed depends also on rocket fuse - PartSys->sources[j].source.vx = random(5) - 2; //i.e. not perfectly straight up + PartSys->sources[j].source.vx = random(-3,3); //i.e. not perfectly straight up PartSys->sources[j].source.sat = 30; // low saturation -> exhaust is off-white PartSys->sources[j].source.ttl = random16(SEGMENT.custom1) + (SEGMENT.custom1 >> 1); // sets explosion height (rockets explode at the top if set too high as paticle update set speed to zero if moving out of matrix) PartSys->sources[j].maxLife = 40; // exhaust particle life @@ -8259,7 +8259,7 @@ uint16_t mode_particlevolcano(void) PartSys->sources[i].maxLife = 300; // lifetime in frames PartSys->sources[i].minLife = 250; PartSys->sources[i].source.collide = true; // seeded particles will collide (if enabled) - PartSys->sources[i].source.perpetural = true; // source never dies + PartSys->sources[i].source.perpetual = true; // source never dies } } else @@ -8299,7 +8299,7 @@ uint16_t mode_particlevolcano(void) PartSys->sources[i].var = SEGMENT.custom3 | 0x01; // emiting variation = nozzle size (custom 3 goes from 0-31), only use odd numbers // spray[j].source.hue = random16(); //set random color for each particle (using palette) -> does not look good PartSys->sprayEmit(PartSys->sources[i]); - PartSys->particleMoveUpdate(PartSys->sources[i].source, PartSys->particlesettings); //move the source (also applies gravity, which is corrected for above, that is a hack but easier than creating more particlesettings) + PartSys->particleMoveUpdate(PartSys->sources[i].source); //move the source (also applies gravity, which is corrected for above, that is a hack but easier than creating more particlesettings) //Serial.println("emit"); } } @@ -8326,9 +8326,9 @@ uint16_t mode_particlefire(void) if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. { - if (!initParticleSystem(PartSys, 25, false, 4)) //maximum number of source (PS will determine the exact number based on segment size) and need 4 additional bytes for time keeping (uint32_t lastcall) + if (!initParticleSystem(PartSys, 25, 4)) //maximum number of source (PS will determine the exact number based on segment size) and need 4 additional bytes for time keeping (uint32_t lastcall) return mode_static(); // allocation failed; //allocation failed - SEGMENT.aux0 = rand(); // aux0 is wind position (index) in the perlin noise + SEGMENT.aux0 = esp_random(); // aux0 is wind position (index) in the perlin noise numFlames = PartSys->numSources; DEBUG_PRINTF_P(PSTR("segment data ptr in fireFX %p\n"), SEGMENT.data); } @@ -8377,13 +8377,13 @@ uint16_t mode_particlefire(void) // initialize new flame: set properties of source if (random16(20) == 0 || SEGMENT.call == 0) // from time to time, change flame position { - PartSys->sources[i].source.x = (PartSys->maxX >> 1) - (spread>>1) + (rand() % spread) ; // distribute randomly on chosen width + PartSys->sources[i].source.x = (PartSys->maxX >> 1) - (spread>>1) + random(spread); // distribute randomly on chosen width } PartSys->sources[i].source.y = -PS_P_RADIUS; // set the source below the frame PartSys->sources[i].source.ttl = 5 + random16((SEGMENT.custom1 * SEGMENT.custom1) >> 7) / (2 + (firespeed >> 4)); //'hotness' of fire, faster flames reduce the effect or flame height will scale too much with speed -> new, this works! PartSys->sources[i].maxLife = random16(7) + 13; // defines flame height together with the vy speed, vy speed*maxlife/PS_P_RADIUS is the average flame height PartSys->sources[i].minLife = 4; - PartSys->sources[i].vx = (int8_t)random(4) - 2; // emitting speed (sideways) + PartSys->sources[i].vx = (int8_t)random(-3, 3); // emitting speed (sideways) PartSys->sources[i].vy = 5 + (firespeed >> 2); // emitting speed (upwards) -> this is good PartSys->sources[i].var = (random16(2 + (firespeed >> 5)) + 3) | 0x01; // speed variation around vx,vy (+/- var/2), only use odd numbers } @@ -8445,7 +8445,7 @@ uint16_t mode_particlepit(void) if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. { - if (!initParticleSystem(PartSys, 1, true)) //init, request one source (actually dont really need one TODO: test if using zero sources also works) + if (!initParticleSystem(PartSys, 1, 0, true)) //init, request one source (actually dont really need one TODO: test if using zero sources also works) return mode_static(); // allocation failed; //allocation failed PartSys->setKillOutOfBounds(true); PartSys->setGravity(); //enable with default gravity @@ -8515,92 +8515,6 @@ uint16_t mode_particlepit(void) PartSys->update(); // update and render -//Experiment: blur to grow the particles, also blur asymmetric, make the particles wobble: - /* - SEGMENT.blur(SEGMENT.custom1, true); - if (SEGMENT.custom1 > 64) - SEGMENT.blur(SEGMENT.custom1 - 64, true); - if (SEGMENT.custom1 > 128) - SEGMENT.blur((SEGMENT.custom1 - 128) << 1, true); - if (SEGMENT.custom1 > 192) - SEGMENT.blur((SEGMENT.custom1 - 192) << 1, true); - */ -/* -//wobbling - static uint8_t testcntr; - static uint8_t wobbleamount = 200; - wobbleamount -= 2; - - testcntr+=15; - -// int32_t ysize = (int16_t)sin8(testcntr); - // int32_t xsize = 255-ysize; - - int32_t ysize = (int32_t)sin8(testcntr)-128; - int32_t xsize = -ysize; //TODO: xsize is not really needed, calculation can be simplified using just ysize - - //ysize = (((int16_t)SEGMENT.custom1 * ysize) >> 8); - //xsize = (((int16_t)SEGMENT.custom1 * xsize) >> 8); - ysize = (int32_t)SEGMENT.custom1 - ((ysize * wobbleamount * SEGMENT.custom1) >> 15); - xsize = (int32_t)SEGMENT.custom1 - ((xsize * wobbleamount * SEGMENT.custom1) >> 15); - - Serial.print(SEGMENT.custom1); - Serial.print(" "); - Serial.print(wobbleamount); - Serial.print(" "); - - Serial.print(xsize); - Serial.print(" "); - Serial.print(ysize); - - - const unsigned cols = PartSys->maxXpixel + 1; - const unsigned rows = PartSys->maxYpixel + 1; - uint8_t xiterations = 1 + (xsize>>8); //allow for wobble size > 255 - uint8_t yiterations = 1 + (ysize>>8); - uint8_t secondpassxsize = xsize - 255; - uint8_t secondpassysize = ysize - 255; - if (xsize > 255) - xsize = 255; //first pass, full sized - if (ysize > 255) - ysize = 255; - - Serial.print(xsize); - Serial.print(" "); - Serial.println(ysize); - for (uint32_t j = 0; j < xiterations; j++) - { - for (uint32_t i = 0; i < cols; i++) - { - SEGMENT.blurCol(i, xsize, true); - if (xsize > 64) - SEGMENT.blurCol(i, xsize - 64, true); - if (xsize > 128) - SEGMENT.blurCol(i, (xsize - 128) << 1, true); - if (xsize > 192) - SEGMENT.blurCol(i, (xsize - 192) << 1, true); - } - //set size for second pass: - xsize = secondpassxsize; - } - for (uint32_t j = 0; j < yiterations; j++) - { - for (unsigned i = 0; i < rows; i++) - { - SEGMENT.blurRow(i, ysize, true); - if (ysize > 64) - SEGMENT.blurRow(i, ysize - 64, true); - if (ysize > 128) - SEGMENT.blurRow(i, (ysize - 128) << 1, true); - if (ysize > 192) - SEGMENT.blurRow(i, (ysize - 192) << 1, true); - } - // set size for second pass: - ysize = secondpassysize; - } -*/ - - /* //rotat image (just a test, non working yet) float angle = PI/3; @@ -8743,10 +8657,28 @@ uint16_t mode_particlebox(void) ParticleSystem *PartSys = NULL; uint32_t i; - if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. + if (SEGMENT.call == 0) // initialization { if (!initParticleSystem(PartSys, 1)) // init - return mode_static(); // allocation failed; //allocation failed + return mode_static(); // allocation failed + PartSys->setBounceX(true); + PartSys->setBounceY(true); + //set max number of particles and save to aux1 for later + #ifdef ESP8266 + SEGMENT.aux1 = min((uint16_t)((PartSys->maxXpixel * PartSys->maxYpixel) >> 1), PartSys->numParticles); + #else + SEGMENT.aux1 = min((uint16_t)((PartSys->maxXpixel * PartSys->maxYpixel)), PartSys->numParticles); //max number of particles + #endif + for (i = 0; i < SEGMENT.aux1; i++) + { + PartSys->particles[i].ttl = 500; //set all particles alive (not all are rendered though) + PartSys->particles[i].perpetual = true; //never die + PartSys->particles[i].hue = i * 3; // color range + PartSys->particles[i].x = map(i, 0, SEGMENT.aux1, 1, PartSys->maxX); // distribute along x according to color + PartSys->particles[i].y = random16(PartSys->maxY >> 2); //bottom quarter + PartSys->particles[i].collide = true; // all particles collide + } + SEGMENT.aux0 = rand(); // position in perlin noise } else PartSys = reinterpret_cast(SEGMENT.data); // if not first call, just set the pointer to the PS @@ -8758,31 +8690,11 @@ uint16_t mode_particlebox(void) } PartSys->updateSystem(); // update system properties (dimensions and data pointers) - PartSys->setBounceX(true); - PartSys->setBounceY(true); + PartSys->setWallHardness(min(SEGMENT.custom2, (uint8_t)200)); // wall hardness is 200 or more PartSys->enableParticleCollisions(true, max(2, (int)SEGMENT.custom2)); // enable collisions and set particle collision hardness - - #ifdef ESP8266 - uint16_t maxnumParticles = min((uint16_t)((PartSys->maxXpixel * PartSys->maxYpixel) >> 1), PartSys->numParticles); - #else - uint16_t maxnumParticles = min((uint16_t)((PartSys->maxXpixel * PartSys->maxYpixel)), PartSys->numParticles); - #endif - PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 10, maxnumParticles)); - - if (SEGMENT.call == 0) // initialization of particles (cannot be done in above loop, only if code lines above here are copied there) - { - SEGMENT.aux0 = rand(); // position in perlin noise - for (i = 0; i < maxnumParticles; i++) - { - PartSys->particles[i].ttl = 500; //set all particles alive (not all are rendered though) - PartSys->particles[i].perpetural = true; //never dies - PartSys->particles[i].hue = i * 5; // color range - PartSys->particles[i].x = map(i, 0, maxnumParticles, 1, PartSys->maxX); // distribute along x according to color - PartSys->particles[i].y = random16(PartSys->maxY); // randomly in y direction - PartSys->particles[i].collide = true; // all particles collide - } - } + PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 10, SEGMENT.aux1)); //aux1 holds max number of particles to use + if (SEGMENT.call % (((255 - SEGMENT.speed) >> 6) + 1) == 0 && SEGMENT.speed > 0) // how often the force is applied depends on speed setting { @@ -8823,7 +8735,7 @@ uint16_t mode_particlebox(void) return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEBOX[] PROGMEM = "PS Box@Speed,Particles,Tilt Strength,Hardness,Friction,Random,Direction,Sloshing;;!;2;pal=43,sx=120,ix=100,c1=100,c2=210,o1=1"; +static const char _data_FX_MODE_PARTICLEBOX[] PROGMEM = "PS Box@Speed,Particles,Tilt Strength,Hardness,Friction,Random,Direction,Sloshing;;!;2;pal=53,sx=120,ix=100,c1=100,c2=210,o1=1"; /* Fuzzy Noise: Perlin noise 'gravity' mapping as in particles on 'noise hills' viewed from above @@ -8838,7 +8750,7 @@ uint16_t mode_particleperlin(void) uint32_t i; if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. { - if (!initParticleSystem(PartSys, 1, true)) // init with 1 source and advanced properties + if (!initParticleSystem(PartSys, 1, 0, true)) // init with 1 source and advanced properties return mode_static(); // allocation failed; //allocation failed PartSys->setKillOutOfBounds(true); //should never happen, but lets make sure there are no stray particles SEGMENT.aux0 = rand(); @@ -8987,7 +8899,7 @@ uint16_t mode_particleimpact(void) if (PartSys->sources[i].source.vy < 0) //move down { PartSys->applyGravity(&PartSys->sources[i].source); - PartSys->particleMoveUpdate(PartSys->sources[i].source, meteorsettings); + PartSys->particleMoveUpdate(PartSys->sources[i].source, &meteorsettings); // if source reaches the bottom, set speed to 0 so it will explode on next function call (handled above) if (PartSys->sources[i].source.y < PS_P_RADIUS<<1) // reached the bottom pixel on its way down @@ -9051,14 +8963,13 @@ uint16_t mode_particleattractor(void) PSparticle *attractor; //particle pointer to the attractor if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. { - if (!initParticleSystem(PartSys, 1, true)) // init using 1 source and advanced particle settings + if (!initParticleSystem(PartSys, 1, 0, true)) // init using 1 source and advanced particle settings return mode_static(); // allocation failed; //allocation failed //DEBUG_PRINTF_P(PSTR("sources in FX %p\n"), &PartSys->sources[0]); PartSys->sources[0].source.hue = random16(); - PartSys->sources[0].source.vx = -7; - PartSys->sources[0].source.collide = true; // seeded particles will collide - //PartSys->sources[0].source.ttl = 100; //TODO: remove, is now done in PS init - PartSys->sources[0].source.perpetural = true; //source does not age + PartSys->sources[0].source.vx = -7; //will collied with wall and get random bounce direction + PartSys->sources[0].source.collide = true; // seeded particles will collide + PartSys->sources[0].source.perpetual = true; //source does not age #ifdef ESP8266 PartSys->sources[0].maxLife = 200; // lifetime in frames (ESP8266 has less particles) PartSys->sources[0].minLife = 30; @@ -9067,6 +8978,7 @@ uint16_t mode_particleattractor(void) PartSys->sources[0].minLife = 50; #endif PartSys->sources[0].var = 7; // emiting variation + PartSys->sources[0].size = 100; //!!! debug remove this again PartSys->setWallHardness(255); //bounce forever PartSys->setWallRoughness(200); //randomize wall bounce } @@ -9099,13 +9011,13 @@ uint16_t mode_particleattractor(void) attractor->vx = PartSys->sources[0].source.vy; // set to spray movemement but reverse x and y attractor->vy = PartSys->sources[0].source.vx; attractor->ttl = 100; - attractor->perpetural = true; + attractor->perpetual = true; } //set attractor properties if (SEGMENT.check2) { if((SEGMENT.call % 3) == 0) // move slowly - PartSys->particleMoveUpdate(*attractor, sourcesettings); // move the attractor + PartSys->particleMoveUpdate(*attractor, &sourcesettings); // move the attractor } else{ @@ -9129,7 +9041,7 @@ uint16_t mode_particleattractor(void) } if (SEGMENT.call % (33 - SEGMENT.custom3) == 0) PartSys->applyFriction(2); - PartSys->particleMoveUpdate(PartSys->sources[0].source, sourcesettings); // move the source + PartSys->particleMoveUpdate(PartSys->sources[0].source, &sourcesettings); // move the source PartSys->update(); // update and render return FRAMETIME; } @@ -9423,7 +9335,7 @@ static const char _data_FX_MODE_PARTICLEGEQ[] PROGMEM = "PS Equalizer@Speed,Inte Particle replacement of Ghost Rider by DedeHai (Damian Schneider), original by stepko adapted by Blaz Kristan (AKA blazoncek) */ #define MAXANGLESTEP 2200 //32767 means 180° -uint16_t mode_particlghostrider(void) +uint16_t mode_particleghostrider(void) { if (SEGLEN == 1) return mode_static(); @@ -9493,7 +9405,7 @@ uint16_t mode_particlghostrider(void) PartSys->sources[0].source.vx = ((int32_t)cos16(SEGMENT.aux0) * speed) / (int32_t)32767; PartSys->sources[0].source.vy = ((int32_t)sin16(SEGMENT.aux0) * speed) / (int32_t)32767; PartSys->sources[0].source.ttl = 500; //source never dies - PartSys->particleMoveUpdate(PartSys->sources[0].source, ghostsettings); + PartSys->particleMoveUpdate(PartSys->sources[0].source, &ghostsettings); //set head (steal one of the particles) PartSys->particles[PartSys->usedParticles-1].x = PartSys->sources[0].source.x; PartSys->particles[PartSys->usedParticles-1].y = PartSys->sources[0].source.y; @@ -9509,6 +9421,84 @@ uint16_t mode_particlghostrider(void) } static const char _data_FX_MODE_PARTICLEGHOSTRIDER[] PROGMEM = "PS Ghost Rider@Speed,Spiral,Blur,Color Cycle,Spread,Color by age,Walls;;!;2;pal=1,sx=70,ix=0,c1=220,c2=30,c3=21,o1=1,o2=0,o3=0"; + +/* +PS Blobs: large particles bouncing around +Uses palette for particle color +by DedeHai (Damian Schneider) +*/ +uint16_t mode_particleblobs(void) +{ + if (SEGLEN == 1) + return mode_static(); + ParticleSystem *PartSys = NULL; + + if (SEGMENT.call == 0) + { + if (!initParticleSystem(PartSys, 1, 0, true, true)) //init, request one source, no additional bytes, advanced size & size control (actually dont really need one TODO: test if using zero sources also works) + return mode_static(); // allocation failed; //allocation failed + //PartSys->setGravity(); //enable with default gravity + PartSys->setBounceX(true); + PartSys->setBounceY(true); + PartSys->setWallHardness(255); + PartSys->setWallRoughness(255); + PartSys->setCollisionHardness(255); + //PartSys->setParticleSize(0); //set global size to zero or motion blur cannot be used (is zero by default) + } + else + PartSys = reinterpret_cast(SEGMENT.data); // if not first call, just set the pointer to the PS + + if (PartSys == NULL) + { + DEBUG_PRINT(F("ERROR: FX PartSys nullpointer")); + return mode_static(); // something went wrong, no data! + } + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setUsedParticles(min(PartSys->numParticles, (uint16_t)map(SEGMENT.intensity,0 ,255, 1, (PartSys->maxXpixel * PartSys->maxYpixel)>>4))); + PartSys->enableParticleCollisions(SEGMENT.check2); + + for (uint32_t i = 0; i < PartSys->usedParticles; i++) // update particles + { + if(SEGMENT.aux0 != SEGMENT.speed || PartSys->particles[i].ttl == 0) //speed changed or dead + { + PartSys->particles[i].vx = (int16_t)random(-(SEGMENT.speed >> 2), SEGMENT.speed >> 2); + PartSys->particles[i].vy = (int16_t)random(-(SEGMENT.speed >> 2), SEGMENT.speed >> 2); + } + if(SEGMENT.aux1 != SEGMENT.custom1 || PartSys->particles[i].ttl == 0) //size changed or dead + PartSys->advPartSize[i].maxsize = 60 + (SEGMENT.custom1 >> 1) + random((SEGMENT.custom1 >> 2)); //set each particle to slightly randomized size + + //PartSys->particles[i].perpetual = SEGMENT.check2; //infinite life if set + if (PartSys->particles[i].ttl == 0) // find dead particle, renitialize + { + PartSys->particles[i].ttl = 300 + random16(((uint16_t)SEGMENT.custom2 << 3) + 100); + PartSys->particles[i].x = random(PartSys->maxX); + PartSys->particles[i].y = random16(PartSys->maxY); + PartSys->particles[i].hue = random16(); // set random color + PartSys->particles[i].collide = true; //enable collision for particle + PartSys->advPartProps[i].size = 0; //start out small + PartSys->advPartSize[i].asymmetry = random16(220); + PartSys->advPartSize[i].asymdir = random16(255); + //set advanced size control properties + PartSys->advPartSize[i].grow = true; + PartSys->advPartSize[i].growspeed = 1 + random16(9); + PartSys->advPartSize[i].shrinkspeed = 1 + random16(9); + PartSys->advPartSize[i].wobblespeed = 1 + random(3); + } + //PartSys->advPartSize[i].asymmetry++; + PartSys->advPartSize[i].pulsate = SEGMENT.check3; + PartSys->advPartSize[i].wobble = SEGMENT.check1; + } + SEGMENT.aux0 = SEGMENT.speed; //write state back + SEGMENT.aux1 = SEGMENT.custom1; + + PartSys->setMotionBlur(((SEGMENT.custom3) << 3) + 7); + PartSys->update(); // update and render + + return FRAMETIME; +} +static const char _data_FX_MODE_PARTICLEBLOBS[] PROGMEM = "PS Blobs@Speed,Blobs,Size,Life,Blur,Wobble,Collide,Pulsate;;!;2;sx=30,ix=64,c1=200,c2=130,c3=0,o1=0,o2=0,o3=1"; + + /* * Particle rotating GEQ * Particles sprayed from center with a rotating spray @@ -9893,7 +9883,8 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_PARTICLEATTRACTOR, &mode_particleattractor, _data_FX_MODE_PARTICLEATTRACTOR); addEffect(FX_MODE_PARTICLESPRAY, &mode_particlespray, _data_FX_MODE_PARTICLESPRAY); addEffect(FX_MODE_PARTICLESGEQ, &mode_particleGEQ, _data_FX_MODE_PARTICLEGEQ); - addEffect(FX_MODE_PARTICLEGHOSTRIDER, &mode_particlghostrider, _data_FX_MODE_PARTICLEGHOSTRIDER); + addEffect(FX_MODE_PARTICLEGHOSTRIDER, &mode_particleghostrider, _data_FX_MODE_PARTICLEGHOSTRIDER); + addEffect(FX_MODE_PARTICLEBLOBS, &mode_particleblobs, _data_FX_MODE_PARTICLEBLOBS); // addEffect(FX_MODE_PARTICLECENTERGEQ, &mode_particlecenterGEQ, _data_FX_MODE_PARTICLECCIRCULARGEQ); diff --git a/wled00/FX.h b/wled00/FX.h index 03c15f6825..3d98cb2c27 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -327,7 +327,8 @@ #define FX_MODE_PARTICLESGEQ 198 #define FX_MODE_PARTICLECENTERGEQ 199 #define FX_MODE_PARTICLEGHOSTRIDER 200 -#define MODE_COUNT 201 +#define FX_MODE_PARTICLEBLOBS 201 +#define MODE_COUNT 202 typedef enum mapping1D2D { M12_Pixels = 0, diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index 3a0eb31252..1e1a41129f 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -43,14 +43,15 @@ #include "FastLED.h" #include "FX.h" -ParticleSystem::ParticleSystem(uint16_t width, uint16_t height, uint16_t numberofparticles, uint16_t numberofsources, bool isadvanced) +ParticleSystem::ParticleSystem(uint16_t width, uint16_t height, uint16_t numberofparticles, uint16_t numberofsources, bool isadvanced, bool sizecontrol) { //Serial.println("PS Constructor"); numSources = numberofsources; numParticles = numberofparticles; // set number of particles in the array usedParticles = numberofparticles; // use all particles by default - //particlesettings = {false, false, false, false, false, false, false, false}; // all settings off by default - updatePSpointers(isadvanced); // set the particle and sources pointer (call this before accessing sprays or particles) + advPartProps = NULL; //make sure we start out with null pointers (just in case memory was not cleared) + advPartSize = NULL; + updatePSpointers(isadvanced, sizecontrol); // set the particle and sources pointer (call this before accessing sprays or particles) setMatrixSize(width, height); setWallHardness(255); // set default wall hardness to max setWallRoughness(0); // smooth walls by default @@ -88,7 +89,7 @@ ParticleSystem::ParticleSystem(uint16_t width, uint16_t height, uint16_t numbero Serial.println(particles[i].y); } }*/ - // Serial.println("PS Constructor done"); + Serial.println("PS Constructor done"); } //update function applies gravity, moves the particles, handles collisions and renders the particles @@ -98,6 +99,15 @@ void ParticleSystem::update(void) //apply gravity globally if enabled if (particlesettings.useGravity) applyGravity(); + + //update size settings before handling collisions + if(advPartSize) + { + for (int i = 0; i < usedParticles; i++) + { + updateSize(&advPartProps[i], &advPartSize[i]); + } + } // handle collisions (can push particles, must be done before updating particles or they can render out of bounds, causing a crash if using local buffer for speed) if (particlesettings.useCollisions) @@ -108,11 +118,13 @@ void ParticleSystem::update(void) { if(advPartProps) { - advprop = &advPartProps[i]; + advprop = &advPartProps[i]; } - particleMoveUpdate(particles[i], particlesettings, advprop); + particleMoveUpdate(particles[i], &particlesettings, advprop); } - /*!!! remove this + + + /*TODO remove this Serial.print("alive particles: "); uint32_t aliveparticles = 0; for (int i = 0; i < numParticles; i++) @@ -158,7 +170,7 @@ void ParticleSystem::setMatrixSize(uint16_t x, uint16_t y) maxXpixel = x - 1; // last physical pixel that can be drawn to maxYpixel = y - 1; maxX = x * PS_P_RADIUS - 1; // particle system boundary for movements - maxY = y * PS_P_RADIUS - 1; // this value is often needed by FX to calculate positions + maxY = y * PS_P_RADIUS - 1; // this value is often needed (also by FX) to calculate positions } void ParticleSystem::setWrapX(bool enable) @@ -193,7 +205,7 @@ void ParticleSystem::setColorByAge(bool enable) void ParticleSystem::setMotionBlur(uint8_t bluramount) { - if(particlesize == 0) //only allwo motion blurring on default particle size + if(particlesize == 0) //only allwo motion blurring on default particle size or advanced size(cannot combine motion blur with normal blurring used for particlesize, would require another buffer) motionBlur = bluramount; } @@ -236,9 +248,9 @@ void ParticleSystem::sprayEmit(PSsource &emitter) { particles[emitIndex].x = emitter.source.x; // + random16(emitter.var) - (emitter.var >> 1); //randomness uses cpu cycles and is almost invisible, removed for now. particles[emitIndex].y = emitter.source.y; // + random16(emitter.var) - (emitter.var >> 1); - particles[emitIndex].vx = emitter.vx + random(emitter.var) - (emitter.var>>1); + particles[emitIndex].vx = emitter.vx + random(emitter.var) - (emitter.var>>1); //TODO: could use random(min,max) but need to adjust all FX as it would double the var amount particles[emitIndex].vy = emitter.vy + random(emitter.var) - (emitter.var>>1); - particles[emitIndex].ttl = random16(emitter.maxLife - emitter.minLife) + emitter.minLife; + particles[emitIndex].ttl = random(emitter.minLife, emitter.maxLife); particles[emitIndex].hue = emitter.source.hue; particles[emitIndex].sat = emitter.source.sat; particles[emitIndex].collide = emitter.source.collide; @@ -300,13 +312,14 @@ void ParticleSystem::angleEmit(PSsource &emitter, uint16_t angle, int8_t speed) // particle moves, decays and dies, if killoutofbounds is set, out of bounds particles are set to ttl=0 // uses passed settings to set bounce or wrap, if useGravity is set, it will never bounce at the top and killoutofbounds is not applied over the top -void ParticleSystem::particleMoveUpdate(PSparticle &part, PSsettings &options, PSadvancedParticle *advancedproperties) +void ParticleSystem::particleMoveUpdate(PSparticle &part, PSsettings *options, PSadvancedParticle *advancedproperties) { - + if(options == NULL) + options = &particlesettings; //use PS system settings by default if (part.ttl > 0) { - if(!part.perpetural) + if(!part.perpetual) part.ttl--; // age if (particlesettings.colorByAge) part.hue = part.ttl > 255 ? 255 : part.ttl; //set color to ttl @@ -323,7 +336,7 @@ void ParticleSystem::particleMoveUpdate(PSparticle &part, PSsettings &options, P particleHardRadius = max(PS_P_MINHARDRADIUS, (int)particlesize + (advancedproperties->size)); } //if wall collisions are enabled, bounce them before they reach the edge, it looks much nicer if the particle is not half out of view - if (options.bounceX) + if (options->bounceX) { if ((newX < particleHardRadius) || (newX > maxX - particleHardRadius)) // reached a wall bounce(part.vx, part.vy, newX, maxX); @@ -331,7 +344,7 @@ void ParticleSystem::particleMoveUpdate(PSparticle &part, PSsettings &options, P if ((newX < 0) || (newX > maxX)) // check if particle reached an edge (note: this also checks out of bounds and must not be skipped, even if bounce is enabled) { - if (options.wrapX) + if (options->wrapX) { newX = (uint16_t)newX % (maxX + 1); } @@ -347,13 +360,13 @@ void ParticleSystem::particleMoveUpdate(PSparticle &part, PSsettings &options, P if(isleaving) { part.outofbounds = 1; - if (options.killoutofbounds) + if (options->killoutofbounds) part.ttl = 0; } } } - if (options.bounceY) + if (options->bounceY) { if ((newY < particleHardRadius) || (newY > maxY - particleHardRadius)) // reached floor / ceiling { @@ -363,13 +376,13 @@ void ParticleSystem::particleMoveUpdate(PSparticle &part, PSsettings &options, P { /* //TODO: is this check really needed? is checked below. on quick tests, it crashed (but not in all animations... -> seems ok. leave it for now, need to check this later - if (options.useGravity) // do not bounce on top if using gravity (open container) if this is needed implement it in the FX + if (options->useGravity) // do not bounce on top if using gravity (open container) if this is needed implement it in the FX { if (newY > maxY + PS_P_HALFRADIUS) part.outofbounds = 1; // set out of bounds, kill out of bounds over the top does not apply if gravity is used (user can implement it in FX if needed) } else*/ - if(!options.useGravity) + if(!options->useGravity) { bounce(part.vy, part.vx, newY, maxY); } @@ -379,7 +392,7 @@ void ParticleSystem::particleMoveUpdate(PSparticle &part, PSsettings &options, P if (((newY < 0) || (newY > maxY))) // check if particle reached an edge (makes sure particles are within frame for rendering) { - if (options.wrapY) + if (options->wrapY) { newY = (uint16_t)newY % (maxY + 1); } @@ -394,11 +407,11 @@ void ParticleSystem::particleMoveUpdate(PSparticle &part, PSsettings &options, P if(isleaving) { part.outofbounds = 1; - if (options.killoutofbounds) + if (options->killoutofbounds) { if (newY < 0) // if gravity is enabled, only kill particles below ground part.ttl = 0; - else if (!options.useGravity) + else if (!options->useGravity) part.ttl = 0; } } @@ -409,24 +422,109 @@ void ParticleSystem::particleMoveUpdate(PSparticle &part, PSsettings &options, P } } +// update advanced particle size control TODO: are the counters needed? it may be good enough like this... +void ParticleSystem::updateSize(PSadvancedParticle *advprops, PSsizeControl *advsize) +{ + if(advsize == NULL) //just a safety check + return; + + //grow/shrink particle + int32_t newsize = advprops->size; + uint32_t counter = advsize->sizecounter; + uint32_t increment; + //calculate grow speed using 0-8 for low speeds and 9-15 for higher speeds + if(advsize->grow) increment = advsize->growspeed; + else if(advsize->shrink) increment = advsize->shrinkspeed; + if(increment < 9) //8 means +1 every frame + { + counter += increment; + if(counter > 7) + { + counter -= 8; + increment = 1; + } + else + increment = 0; + advsize->sizecounter = counter; + } + else{ + increment = (increment - 8) << 1; //9 means +2, 10 means +4 etc. 15 means +14 + } + if(advsize->grow) + { + if(newsize < advsize->maxsize) + { + newsize += increment; + if(newsize >= advsize->maxsize) + { + advsize->grow = false; //stop growing, shrink from now on if enabled + newsize = advsize->maxsize; //limit + if(advsize->pulsate) advsize->shrink = true; + } + } + } + else if(advsize->shrink) + { + if(newsize > advsize->minsize) + { + newsize -= increment; + if(newsize <= advsize->minsize) + { + //if(advsize->minsize == 0) part.ttl = 0; //TODO: need to pass particle or return kill instruction + advsize->shrink = false; //disable shrinking + newsize = advsize->minsize; //limit + if(advsize->pulsate) advsize->grow = true; + } + } + } + advprops->size = newsize; + //handle wobbling + if(advsize->wobble) + { + advsize->asymdir += advsize->wobblespeed; //todo: need better wobblespeed control? + } +} + +// calculate x and y size for asymmetrical particles (advanced size control) +void ParticleSystem::getParticleXYsize(PSadvancedParticle *advprops, PSsizeControl *advsize, uint32_t &xsize, uint32_t &ysize) +{ + //advsize->asymdir = 50; //!!! + if(advsize == NULL) //if advanced size is valid, also advced properties pointer is valid (handled by pointer assignment function) + return; + int32_t deviation = ((uint32_t)advprops->size * (uint32_t)advsize->asymmetry) / 255; //deviation from symmetrical size + // Calculate x and y size based on deviation and direction (0 is symmetrical, 64 is x, 128 is symmetrical, 192 is y) + if (advsize->asymdir < 64) { + deviation = ((int32_t)advsize->asymdir * deviation) / 64; + } else if (advsize->asymdir < 192) { + deviation = ((128 - (int32_t)advsize->asymdir) * deviation) / 64; + } else { + deviation = (((int32_t)advsize->asymdir - 255) * deviation) / 64; + } + // Calculate x and y size based on deviation, limit to 255 (rendering function cannot handle lareger sizes) + xsize = ((int32_t)advprops->size - deviation) > 255 ? 255 : advprops->size - deviation; + ysize = ((int32_t)advprops->size + deviation) > 255 ? 255 : advprops->size + deviation; +} + //function to bounce a particle from a wall using set parameters (wallHardness and wallRoughness) void ParticleSystem::bounce(int8_t &incomingspeed, int8_t ¶llelspeed, int32_t &position, uint16_t maxposition) { - incomingspeed = -incomingspeed; // invert speed - incomingspeed = (incomingspeed * wallHardness) / 255; // reduce speed as energy is lost on non-hard surface - if (position < particleHardRadius) - position = particleHardRadius; // fast particles will never reach the edge if position is inverted - else - position = maxposition - particleHardRadius; - if(wallRoughness) - { - //transfer an amount of incomingspeed speed to parallel speed - int32_t donatespeed = abs(incomingspeed); - donatespeed = ((random(donatespeed << 1) - donatespeed) * wallRoughness) / 255; //take random portion of + or - x speed, scaled by roughness - parallelspeed += donatespeed; - donatespeed = abs(donatespeed); - incomingspeed -= incomingspeed > 0 ? donatespeed : -donatespeed; - } + incomingspeed = -incomingspeed; // invert speed + incomingspeed = (incomingspeed * wallHardness) / 255; // reduce speed as energy is lost on non-hard surface + if (position < particleHardRadius) + position = particleHardRadius; // fast particles will never reach the edge if position is inverted + else + position = maxposition - particleHardRadius; + if(wallRoughness) + { + int32_t totalspeed = abs(incomingspeed) + abs(parallelspeed); + //transfer an amount of incomingspeed speed to parallel speed + int32_t donatespeed = abs(incomingspeed); + donatespeed = (random(-donatespeed, donatespeed) * wallRoughness) / 255; //take random portion of + or - perpendicular speed, scaled by roughness + parallelspeed = limitSpeed((int32_t)parallelspeed + donatespeed); + incomingspeed = limitSpeed((int32_t)incomingspeed - donatespeed); + donatespeed = totalspeed - abs(parallelspeed); //keep total speed the same + incomingspeed = incomingspeed > 0 ? donatespeed : -donatespeed; + } } @@ -461,12 +559,13 @@ void ParticleSystem::applyForce(uint16_t particleindex, int8_t xforce, int8_t yf } // apply a force in x,y direction to all particles +// force is in 3.4 fixed point notation (see above) void ParticleSystem::applyForce(int8_t xforce, int8_t yforce) { // for small forces, need to use a delay counter uint8_t tempcounter; - //note: this is not the most compuatationally effeicient way to do this, but it saves on duplacte code and is fast enough + //note: this is not the most compuatationally efficient way to do this, but it saves on duplacte code and is fast enough for (uint i = 0; i < usedParticles; i++) { tempcounter = forcecounter; @@ -533,7 +632,8 @@ void ParticleSystem::applyGravity(PSparticle *part) } // slow down particle by friction, the higher the speed, the higher the friction. a high friction coefficient slows them more (255 means instant stop) -void ParticleSystem::applyFriction(PSparticle *part, uint8_t coefficient) +// note: a coefficient smaller than 0 will speed them up (this is a feature, not a bug), coefficient larger than 255 inverts the speed, so don't do that +void ParticleSystem::applyFriction(PSparticle *part, int32_t coefficient) { int32_t friction = 255 - coefficient; // note: not checking if particle is dead can be done by caller (or can be omitted) @@ -543,7 +643,7 @@ void ParticleSystem::applyFriction(PSparticle *part, uint8_t coefficient) } // apply friction to all particles -void ParticleSystem::applyFriction(uint8_t coefficient) +void ParticleSystem::applyFriction(int32_t coefficient) { for (uint32_t i = 0; i < usedParticles; i++) { @@ -656,17 +756,18 @@ void ParticleSystem::ParticleSys_render(bool firemode, uint32_t fireintensity) if (useLocalBuffer) { - cli(); //no interrupts so we get one block of memory for both buffers, less fragmentation + /* Serial.print("heap: "); Serial.print(heap_caps_get_free_size(MALLOC_CAP_8BIT)); Serial.print(" block: "); Serial.println(heap_caps_get_largest_free_block(MALLOC_CAP_8BIT));*/ + //cli(); //no interrupts so we get one block of memory for both buffers, less fragmentation -> leads to flickering on S3. so not a good idea. // allocate empty memory for the local renderbuffer framebuffer = allocate2Dbuffer(maxXpixel + 1, maxYpixel + 1); if (framebuffer == NULL) { - sei(); //re enable interrupts + //sei(); //re enable interrupts Serial.println("Frame buffer alloc failed"); useLocalBuffer = false; //render to segment pixels directly if not enough memory } @@ -675,7 +776,7 @@ void ParticleSystem::ParticleSys_render(bool firemode, uint32_t fireintensity) { renderbuffer = allocate2Dbuffer(10, 10); //buffer to render individual particles to if size > 0 note: null checking is done when accessing it } - sei(); //re enable interrupts + //sei(); //re enable interrupts if (motionBlur > 0) // using SEGMENT.fadeToBlackBy is much slower, this approximately doubles the speed of fade calculation { uint32_t yflipped; @@ -736,28 +837,21 @@ void ParticleSystem::ParticleSys_render(bool firemode, uint32_t fireintensity) if(particlesize > 0) { - if (useLocalBuffer) + uint32_t passes = particlesize/64 + 1; //number of blur passes + uint32_t bluramount = particlesize; //number of blur passes + uint32_t bitshift = 0; + + for(int i = 0; i < passes; i++) //run four passes max { - //uint32_t firstblur = particlesize > 64 ? 64 : particlesize; //attempt to add motion blurring, but does not work... - blur2D(framebuffer, maxYpixel + 1, maxYpixel + 1, particlesize, particlesize); - if (particlesize > 64) - blur2D(framebuffer, maxYpixel + 1, maxYpixel + 1, particlesize - 64, particlesize - 64); - if (particlesize > 128) - blur2D(framebuffer, maxYpixel + 1, maxYpixel + 1, (particlesize - 128) << 1, (particlesize - 128) << 1); - if (particlesize > 192) - blur2D(framebuffer, maxYpixel + 1, maxYpixel + 1, (particlesize - 192) << 1, (particlesize - 192) << 1); - } - else - { - SEGMENT.blur(particlesize, true); - if (particlesize > 64) - SEGMENT.blur(particlesize - 64, true); - if (particlesize > 128) - SEGMENT.blur((particlesize - 128) << 1, true); - if (particlesize > 192) - SEGMENT.blur((particlesize - 192) << 1, true); - } - + if(i == 2) //for the last two passes, use higher amount of blur (results in a nicer brightness gradient with soft edges) + bitshift = 1; + + if (useLocalBuffer) + blur2D(framebuffer, maxXpixel + 1, maxYpixel + 1, bluramount << bitshift, bluramount << bitshift); + else + SEGMENT.blur(bluramount << bitshift, true); + bluramount -= 64; + } } if (useLocalBuffer) //transfer local buffer back to segment @@ -774,7 +868,7 @@ void ParticleSystem::ParticleSys_render(bool firemode, uint32_t fireintensity) free(framebuffer); // free buffer memory } if(renderbuffer) - free(renderbuffer); // free buffer memory + free(renderbuffer); // free buffer memory } // calculate pixel positions and brightness distribution and render the particle to local buffer or global buffer @@ -889,35 +983,38 @@ void ParticleSystem::renderParticle(CRGB **framebuffer, uint32_t particleindex, fast_color_add(renderbuffer[5][4], color, pxlbrightness[1]); fast_color_add(renderbuffer[5][5], color, pxlbrightness[2]); fast_color_add(renderbuffer[4][5], color, pxlbrightness[3]); //TODO: make this a loop somehow? needs better coordinate handling... - uint32_t rendersize = 4; - uint32_t offset = 3; //offset to zero coordinate to write/read data in renderbuffer - //TODO: add asymmetrical size support - blur2D(renderbuffer, rendersize, rendersize, advPartProps[particleindex].size, advPartProps[particleindex].size, true, offset, offset, true); //blur to 4x4 - if (advPartProps[particleindex].size > 64) + uint32_t rendersize = 2; //initialize render size, minimum is 4x4 pixels, it is incremented int he loop below to start with 4 + uint32_t offset = 4; //offset to zero coordinate to write/read data in renderbuffer (actually needs to be 3, is decremented in the loop below) + + uint32_t maxsize = advPartProps[particleindex].size; + uint32_t xsize = maxsize; + uint32_t ysize = maxsize; + if(advPartSize) //use advanced size control { - rendersize += 2; - offset--; - blur2D(renderbuffer, rendersize, rendersize, advPartProps[particleindex].size - 64, advPartProps[particleindex].size - 64, true, offset, offset, true); //blur to 6x6 + if(advPartSize[particleindex].asymmetry > 0) + getParticleXYsize(&advPartProps[particleindex], &advPartSize[particleindex], xsize, ysize); + maxsize = xsize; + if(ysize > maxsize) maxsize = ysize; //maxsize is now the bigger of the two } - if (advPartProps[particleindex].size > 128) + maxsize = maxsize/64 + 1; //number of blur passes depends on maxsize + uint32_t bitshift = 0; + for(int i = 0; i < maxsize; i++) //run four passes max { + if(i == 2) //for the last two passes, use higher amount of blur (results in a nicer brightness gradient with soft edges) + bitshift = 1; rendersize += 2; - offset--; - blur2D(renderbuffer, rendersize, rendersize, (advPartProps[particleindex].size - 128) << 1, (advPartProps[particleindex].size - 128) << 1, true, offset, offset, true); //blur to 8x8 - } - if (advPartProps[particleindex].size > 192) - { - rendersize += 2; - offset--; - blur2D(renderbuffer, rendersize, rendersize, (advPartProps[particleindex].size - 192) << 1, (advPartProps[particleindex].size - 192) << 1, true, offset, offset, true); //blur to 10x10 - } + offset--; + blur2D(renderbuffer, rendersize, rendersize, xsize << bitshift, ysize << bitshift, true, offset, offset, true); //blur to 4x4 + xsize = xsize > 64 ? xsize - 64 : 0; + ysize = ysize > 64 ? ysize - 64 : 0; + } //calculate origin coordinates to render the particle to in the framebuffer uint32_t xfb_orig = x - (rendersize>>1) + 1 - offset; uint32_t yfb_orig = y - (rendersize>>1) + 1 - offset; uint32_t xfb, yfb; //coordinates in frame buffer to write to note: by making this uint, only overflow has to be checked - //transfer renderbuffer to framebuffer + //transfer particle renderbuffer to framebuffer for(uint32_t xrb = offset; xrb < rendersize+offset; xrb++) { xfb = xfb_orig + xrb; @@ -945,7 +1042,7 @@ void ParticleSystem::renderParticle(CRGB **framebuffer, uint32_t particleindex, } } } - else + else //standard rendering { if (framebuffer) { @@ -965,45 +1062,6 @@ void ParticleSystem::renderParticle(CRGB **framebuffer, uint32_t particleindex, } } - //TODO: for advance pixels, render them to larger size in a local buffer. or better make a new function for that? - //easiest would be to create a 2x2 buffer for the original values, but that may not be as fast for smaller pixels... - //just make a new function that these colors are rendered to and then blurr it so it stays fast for normal rendering. - -/* - //uint32_t firstblur = particlesize > 64 ? 64 : particlesize; //attempt to add motion blurring, but does not work... - blur2D(framebuffer, maxYpixel + 1, maxYpixel + 1, particlesize, particlesize); //4x4 - if (particlesize > 64) - blur2D(framebuffer, maxYpixel + 1, maxYpixel + 1, particlesize - 64, particlesize - 64); //6x6 - if (particlesize > 128) - blur2D(framebuffer, maxYpixel + 1, maxYpixel + 1, (particlesize - 128) << 1, (particlesize - 128) << 1); 8x8 - if (particlesize > 192) - blur2D(framebuffer, maxYpixel + 1, maxYpixel + 1, (particlesize - 192) << 1, (particlesize - 192) << 1); 10x10 - */ - - -/* - Serial.print("x:"); - Serial.print(particle->x); - Serial.print(" y:"); - Serial.print(particle->y); - //Serial.print(" xo"); - //Serial.print(xoffset); - //Serial.print(" dx"); - //Serial.print(dx); - //Serial.print(" "); - for(uint8_t t = 0; t<4; t++) - { - Serial.print(" v"); - Serial.print(pxlbrightness[t]); - Serial.print(" x"); - Serial.print(pixco[t][0]); - Serial.print(" y"); - Serial.print(pixco[t][1]); - - Serial.print(" "); - } - Serial.println(" "); -*/ /* // debug: check coordinates if out of buffer boundaries print out some info (rendering out of bounds particle causes crash!) @@ -1401,13 +1459,13 @@ void ParticleSystem::updateSystem(void) uint32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; uint32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); setMatrixSize(cols, rows); - updatePSpointers(advPartProps != NULL); + updatePSpointers(advPartProps != NULL, advPartSize != NULL); } // set the pointers for the class (this only has to be done once and not on every FX call, only the class pointer needs to be reassigned to SEGENV.data every time) // function returns the pointer to the next byte available for the FX (if it assigned more memory for other stuff using the above allocate function) // FX handles the PSsources, need to tell this function how many there are -void ParticleSystem::updatePSpointers(bool isadvanced) +void ParticleSystem::updatePSpointers(bool isadvanced, bool sizecontrol) { //DEBUG_PRINT(F("*** PS pointers ***")); //DEBUG_PRINTF_P(PSTR("this PS %p "), this); @@ -1416,26 +1474,32 @@ void ParticleSystem::updatePSpointers(bool isadvanced) //The PS is aligned to 4 bytes, a PSparticle is aligned to 2 and a struct containing only byte sized variables is not aligned at all and may need to be padded when dividing the memoryblock. //by making sure that the number of sources and particles is a multiple of 4, padding can be skipped here as alignent is ensured, independent of struct sizes. particles = reinterpret_cast(this + 1); // pointer to particle array at data+sizeof(ParticleSystem) + sources = reinterpret_cast(particles + numParticles); // pointer to source(s) if(isadvanced) { - advPartProps = reinterpret_cast(particles + numParticles); - sources = reinterpret_cast(advPartProps + numParticles); // pointer to source(s) + advPartProps = reinterpret_cast(sources + numSources); + PSdataEnd = reinterpret_cast(advPartProps + numParticles); + if(sizecontrol) + { + advPartSize = reinterpret_cast(advPartProps + numParticles); + PSdataEnd = reinterpret_cast(advPartSize + numParticles); + } } else { - advPartProps = NULL; - sources = reinterpret_cast(particles + numParticles); // pointer to source(s) - } - PSdataEnd = reinterpret_cast(sources + numSources); // pointer to first available byte after the PS for FX additional data - - //DEBUG_PRINTF_P(PSTR(" particles %p "), particles); - //DEBUG_PRINTF_P(PSTR(" sources %p "), sources); - //DEBUG_PRINTF_P(PSTR(" adv. props %p\n"), advPartProps); - //DEBUG_PRINTF_P(PSTR("end %p\n"), PSdataEnd); + PSdataEnd = reinterpret_cast(sources + numSources); // pointer to first available byte after the PS for FX additional data + } + /* + DEBUG_PRINTF_P(PSTR(" particles %p "), particles); + DEBUG_PRINTF_P(PSTR(" sources %p "), sources); + DEBUG_PRINTF_P(PSTR(" adv. props %p "), advPartProps); + DEBUG_PRINTF_P(PSTR(" adv. ctrl %p "), advPartSize); + DEBUG_PRINTF_P(PSTR("end %p\n"), PSdataEnd); + */ } //non class functions to use for initialization -uint32_t calculateNumberOfParticles(bool isadvanced) +uint32_t calculateNumberOfParticles(bool isadvanced, bool sizecontrol) { uint32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; uint32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); @@ -1452,7 +1516,10 @@ uint32_t calculateNumberOfParticles(bool isadvanced) numberofParticles = max((uint32_t)1, min(numberofParticles, particlelimit)); if (isadvanced) // advanced property array needs ram, reduce number of particles to use the same amount numberofParticles = (numberofParticles * sizeof(PSparticle)) / (sizeof(PSparticle) + sizeof(PSadvancedParticle)); - //make sure it is a multiple of 4 for proper memory alignment + if (sizecontrol) // advanced property array needs ram, reduce number of particles to use the same amount + numberofParticles /= 8; // if size control is used, much fewer particles are needed + + //make sure it is a multiple of 4 for proper memory alignment (easier than using padding bytes) numberofParticles = ((numberofParticles+3) >> 2) << 2; return numberofParticles; } @@ -1477,13 +1544,15 @@ uint32_t calculateNumberOfSources(uint8_t requestedsources) } //allocate memory for particle system class, particles, sprays plus additional memory requested by FX -bool allocateParticleSystemMemory(uint16_t numparticles, uint16_t numsources, bool isadvanced, uint16_t additionalbytes) +bool allocateParticleSystemMemory(uint16_t numparticles, uint16_t numsources, bool isadvanced, bool sizecontrol, uint16_t additionalbytes) { uint32_t requiredmemory = sizeof(ParticleSystem); //functions above make sure these are a multiple of 4 bytes (to avoid alignment issues) requiredmemory += sizeof(PSparticle) * numparticles; if (isadvanced) requiredmemory += sizeof(PSadvancedParticle) * numparticles; + if (sizecontrol) + requiredmemory += sizeof(PSsizeControl) * numparticles; requiredmemory += sizeof(PSsource) * numsources; requiredmemory += additionalbytes; //Serial.print("allocating: "); @@ -1495,14 +1564,14 @@ bool allocateParticleSystemMemory(uint16_t numparticles, uint16_t numsources, bo } // initialize Particle System, allocate additional bytes if needed (pointer to those bytes can be read from particle system class: PSdataEnd) -bool initParticleSystem(ParticleSystem *&PartSys, uint8_t requestedsources, bool isadvanced, uint16_t additionalbytes) +bool initParticleSystem(ParticleSystem *&PartSys, uint8_t requestedsources, uint16_t additionalbytes, bool largesizes, bool sizecontrol) { //Serial.println("PS init function"); - uint32_t numparticles = calculateNumberOfParticles(isadvanced); + uint32_t numparticles = calculateNumberOfParticles(largesizes, sizecontrol); uint32_t numsources = calculateNumberOfSources(requestedsources); //Serial.print("numsources: "); //Serial.println(numsources); - if (!allocateParticleSystemMemory(numparticles, numsources, isadvanced, additionalbytes)) + if (!allocateParticleSystemMemory(numparticles, numsources, largesizes, sizecontrol, additionalbytes)) { DEBUG_PRINT(F("PS init failed: memory depleted")); return false; @@ -1512,7 +1581,7 @@ bool initParticleSystem(ParticleSystem *&PartSys, uint8_t requestedsources, bool uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); //Serial.println("calling constructor"); - PartSys = new (SEGMENT.data) ParticleSystem(cols, rows, numparticles, numsources, isadvanced); // particle system constructor TODO: why does VS studio thinkt this is bad? + PartSys = new (SEGMENT.data) ParticleSystem(cols, rows, numparticles, numsources, largesizes, sizecontrol); // particle system constructor TODO: why does VS studio thinkt this is bad? //Serial.print("PS pointer at "); //Serial.println((uintptr_t)PartSys); return true; @@ -1619,7 +1688,7 @@ void blur2D(CRGB **colorbuffer, uint32_t xsize, uint32_t ysize, uint32_t xblur, seeppart = colorbuffer[x][y]; //create copy of current color fast_color_scale(seeppart, seep); //scale it and seep to neighbours if(!smear) //fade current pixel if smear is disabled - fast_color_scale(colorbuffer[x][y], 255 - xblur); + fast_color_scale(colorbuffer[x][y], 255 - yblur); if(y > 0) { diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index 7413dccedc..ecf9c262a5 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -58,7 +58,7 @@ typedef struct { uint16_t ttl : 12; // time to live, 12 bit or 4095 max (which is 50s at 80FPS) bool outofbounds : 1; //out of bounds flag, set to true if particle is outside of display area bool collide : 1; //if set, particle takes part in collisions - bool perpetural : 1; //if set, particle does not age (TTL is not decremented in move function, it still dies from killoutofbounds) + bool perpetual : 1; //if set, particle does not age (TTL is not decremented in move function, it still dies from killoutofbounds) bool flag4 : 1; // unused flag } PSparticle; @@ -67,29 +67,24 @@ typedef struct { uint8_t size; //particle size, 255 means 10 pixels in diameter uint8_t forcecounter; //counter for applying forces to individual particles - - //bool flag1 : 1; // unused flags... for now. - //bool flag2 : 1; - //bool flag3 : 1; - //bool flag4 : 1; } PSadvancedParticle; -// struct for advanced particle size control (optional) TODO: this is currently just an idea, may not make it into final code if too slow / complex +// struct for advanced particle size control (optional) typedef struct { - uint8_t sizeasymmetry; // asymmetrical size TODO: need something better to define this? - uint8_t targetsize; // target size for growing / shrinking + uint8_t asymmetry; // asymmetrical size (0=symmetrical, 255 fully asymmetric) + uint8_t asymdir; // direction of asymmetry, 64 is x, 192 is y (0 and 128 is symmetrical) + uint8_t maxsize; // target size for growing + uint8_t minsize; // target size for shrinking + uint8_t sizecounter : 4; // counters used for size contol (grow/shrink/wobble) + uint8_t wobblecounter : 4; uint8_t growspeed : 4; - uint8_t shrinkspeed : 4; - uint8_t sizecounter; // counter that can be used for size contol TODO: need more than one? - //ideas: - //wobbleamount, rotation angle for asymmetic particles - //a flag 'usegravity' that can be set to false for selective gravity application - + uint8_t shrinkspeed : 4; + uint8_t wobblespeed : 4; bool grow : 1; // flags bool shrink : 1; - bool wobble : 1; - bool flag4 : 1; + bool pulsate : 1; //grows & shrinks & grows & ... + bool wobble : 1; //alternate x and y size } PSsizeControl; @@ -125,7 +120,7 @@ typedef union class ParticleSystem { public: - ParticleSystem(uint16_t width, uint16_t height, uint16_t numberofparticles, uint16_t numberofsources, bool isadvanced = false); // constructor + ParticleSystem(uint16_t width, uint16_t height, uint16_t numberofparticles, uint16_t numberofsources, bool isadvanced = false, bool sizecontrol = false); // constructor // note: memory is allcated in the FX function, no deconstructor needed void update(void); //update the particles according to set options and render to the matrix void updateFire(uint32_t intensity, bool renderonly = false); // update function for fire, if renderonly is set, particles are not updated (required to fix transitions with frameskips) @@ -137,7 +132,7 @@ class ParticleSystem void angleEmit(PSsource& emitter, uint16_t angle, int8_t speed); // move functions - void particleMoveUpdate(PSparticle &part, PSsettings &options, PSadvancedParticle *advancedproperties = NULL); + void particleMoveUpdate(PSparticle &part, PSsettings *options = NULL, PSadvancedParticle *advancedproperties = NULL); //particle physics void applyGravity(PSparticle *part); // applies gravity to single particle (use this for sources) void applyForce(PSparticle *part, int8_t xforce, int8_t yforce, uint8_t *counter); @@ -146,8 +141,8 @@ class ParticleSystem void applyAngleForce(PSparticle *part, int8_t force, uint16_t angle, uint8_t *counter); void applyAngleForce(uint16_t particleindex, int8_t force, uint16_t angle); // use this for advanced property particles void applyAngleForce(int8_t force, uint16_t angle); //apply angular force to all particles - void applyFriction(PSparticle *part, uint8_t coefficient); // apply friction to specific particle - void applyFriction(uint8_t coefficient); // apply friction to all used particles + void applyFriction(PSparticle *part, int32_t coefficient); // apply friction to specific particle + void applyFriction(int32_t coefficient); // apply friction to all used particles void pointAttractor(uint16_t particleindex, PSparticle *attractor, uint8_t strength, bool swallow); void lineAttractor(uint16_t particleindex, PSparticle *attractorcenter, uint16_t attractorangle, uint8_t strength); @@ -164,7 +159,7 @@ class ParticleSystem void setKillOutOfBounds(bool enable); //if enabled, particles outside of matrix instantly die void setSaturation(uint8_t sat); //set global color saturation void setColorByAge(bool enable); - void setMotionBlur(uint8_t bluramount); + void setMotionBlur(uint8_t bluramount); //note: motion blur can only be used if 'particlesize' is set to zero void setParticleSize(uint8_t size); void setGravity(int8_t force = 8); void enableParticleCollisions(bool enable, uint8_t hardness = 255); @@ -172,13 +167,13 @@ class ParticleSystem PSparticle *particles; // pointer to particle array PSsource *sources; // pointer to sources PSadvancedParticle *advPartProps; // pointer to advanced particle properties (can be NULL) - uint8_t* PSdataEnd; //points to first available byte after the PSmemory, is set in setPointers(). use this to set pointer to FX custom data + PSsizeControl *advPartSize; //pointer to advanced particle size control (can be NULL) + uint8_t* PSdataEnd; //points to first available byte after the PSmemory, is set in setPointers(). use this for FX custom data uint16_t maxX, maxY; //particle system size i.e. width-1 / height-1 in subpixels uint32_t maxXpixel, maxYpixel; // last physical pixel that can be drawn to (FX can read this to read segment size if required), equal to width-1 / height-1 uint8_t numSources; //number of sources uint16_t numParticles; // number of particles available in this system - uint16_t usedParticles; // number of particles used in animation (can be smaller then numParticles) - PSsettings particlesettings; // settings used when updating particles (can also used by FX to move sources), do not edit properties directly, use functions above + uint16_t usedParticles; // number of particles used in animation (can be smaller then numParticles) private: //rendering functions @@ -192,7 +187,9 @@ class ParticleSystem void fireParticleupdate(); //utility functions - void updatePSpointers(bool isadvanced); // update the data pointers to current segment data space + void updatePSpointers(bool isadvanced, bool sizecontrol); // update the data pointers to current segment data space + void updateSize(PSadvancedParticle *advprops, PSsizeControl *advsize); //advanced size control + void getParticleXYsize(PSadvancedParticle *advprops, PSsizeControl *advsize, uint32_t &xsize, uint32_t &ysize); void bounce(int8_t &incomingspeed, int8_t ¶llelspeed, int32_t &position, uint16_t maxposition); //bounce on a wall int16_t wraparound(uint16_t p, uint32_t maxvalue); int32_t calcForce_dv(int8_t force, uint8_t *counter); @@ -200,6 +197,7 @@ class ParticleSystem CRGB **allocate2Dbuffer(uint32_t cols, uint32_t rows); // note: variables that are accessed often are 32bit for speed + PSsettings particlesettings; // settings used when updating particles (can also used by FX to move sources), do not edit properties directly, use functions above uint32_t emitIndex; // index to count through particles to emit so searching for dead pixels is faster int32_t collisionHardness; uint8_t wallHardness; @@ -215,10 +213,10 @@ class ParticleSystem }; //initialization functions (not part of class) -bool initParticleSystem(ParticleSystem *&PartSys, uint8_t requestedsources, bool advanced = false, uint16_t additionalbytes = 0); -uint32_t calculateNumberOfParticles(bool advanced); +bool initParticleSystem(ParticleSystem *&PartSys, uint8_t requestedsources, uint16_t additionalbytes = 0, bool largesizes = false, bool sizecontrol = false); +uint32_t calculateNumberOfParticles(bool advanced, bool sizecontrol); uint32_t calculateNumberOfSources(uint8_t requestedsources); -bool allocateParticleSystemMemory(uint16_t numparticles, uint16_t numsources, bool advanced, uint16_t additionalbytes); +bool allocateParticleSystemMemory(uint16_t numparticles, uint16_t numsources, bool advanced, bool sizecontrol, uint16_t additionalbytes); //color add function void fast_color_add(CRGB &c1, CRGB &c2, uint32_t scale = 255); // fast and accurate color adding with scaling (scales c2 before adding) void fast_color_scale(CRGB &c, uint32_t scale); //fast scaling function using 32bit factor (keep it 0-255) and pointer From 255815605608a7fcacc2aa5264aeefb95d8bb4bb Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sun, 5 May 2024 11:19:02 +0200 Subject: [PATCH 075/219] code cleanup, removed some unused stuff --- wled00/FX.cpp | 54 ++-- wled00/FXparticleSystem.cpp | 486 ++++++++++++------------------------ wled00/FXparticleSystem.h | 2 +- 3 files changed, 180 insertions(+), 362 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index df953a8750..af9d828744 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -7968,8 +7968,7 @@ uint16_t mode_particlevortex(void) } } } - - //TODO: speed increment is still wrong, it only works in autochange, manual change to a lower speed does not work. need to make it better. + // set rotation direction and speed // can use direction flag to determine current direction bool direction = SEGMENT.check2; //no automatic direction change, set it to flag @@ -7977,7 +7976,6 @@ uint16_t mode_particlevortex(void) if (SEGMENT.custom2 > 0) // automatic direction change enabled { - // speedincrement = 1 + (SEGMENT.speed >> 5) + (SEGMENT.custom2 >> 2); uint16_t changeinterval = (270 - SEGMENT.custom2); direction = SEGMENT.aux1 & 0x02; //set direction according to flag @@ -8014,22 +8012,6 @@ uint16_t mode_particlevortex(void) // calculate angle offset for an even distribution uint16_t angleoffset = 0xFFFF / spraycount; - - //int32_t particlespeeddiv = ((263 - SEGMENT.intensity) >> 3); - //int32_t particlespeed = 127/particlespeeddiv; //just for testing, need to replace this with angle emit and come up with a new speed calculation - //particle speed goes from 7 to 128 (sin cos return 15bit value but with sign) - - // for (j = 0; j < spraycount; j++) //TODO: use angle emit - // { - - // 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) / particlespeeddiv; // update spray angle (rotate all sprays with angle offset) - // PartSys->sources[j].vy = (sin16(SEGMENT.aux0 + angleoffset * j) >> 8) / particlespeeddiv; // 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) - // } - - -//test to check if less particles are ok at lower speeds. uint32_t skip = PS_P_HALFRADIUS/(SEGMENT.intensity + 1) + 1; if (SEGMENT.call % skip == 0) { @@ -8072,7 +8054,7 @@ uint16_t mode_particlefireworks(void) if (SEGMENT.call == 0) // initialization { if (!initParticleSystem(PartSys, NUMBEROFSOURCES, true)) // init with advanced particle properties - return mode_static(); // allocation failed; //allocation failed + return mode_static(); // allocation failed PartSys->setKillOutOfBounds(true); //out of bounds particles dont return (except on top, taken care of by gravity setting) PartSys->setWallHardness(100); //ground bounce is fixed numRockets = min(PartSys->numSources, (uint8_t)NUMBEROFSOURCES); @@ -8095,7 +8077,6 @@ uint16_t mode_particlefireworks(void) PartSys->setWrapX(SEGMENT.check1); PartSys->setBounceY(SEGMENT.check2); - //PartSys->setWallHardness(SEGMENT.custom2); //not used anymore, can be removed PartSys->setGravity(map(SEGMENT.custom3,0,31,0,10)); // todo: make it a slider to adjust // check each rocket's state and emit particles according to its state: moving up = emit exhaust, at top = explode; falling down = standby time @@ -8168,6 +8149,7 @@ uint16_t mode_particlefireworks(void) else { /* + //TODO: this does not look good. adjust or remove completely if( PartSys->sources[j].source.vy < 0) //explosion is ongoing { if(i < (emitparticles>>2)) //set 1/4 of particles to larger size @@ -8193,7 +8175,7 @@ uint16_t mode_particlefireworks(void) { if (PartSys->sources[j].source.ttl) { - PartSys->particleMoveUpdate(PartSys->sources[j].source); //todo: need different settings for rocket? + PartSys->particleMoveUpdate(PartSys->sources[j].source); } else if (PartSys->sources[j].source.vy > 0) // rocket has died and is moving up. stop it so it will explode (is handled in the code above) { @@ -8218,7 +8200,7 @@ uint16_t mode_particlefireworks(void) PartSys->sources[j].minLife = 10; PartSys->sources[j].vx = 0; // emitting speed PartSys->sources[j].vy = 0; // emitting speed - PartSys->sources[j].var = 5; // speed variation around vx,vy (+/- var/2) + PartSys->sources[j].var = 3; // speed variation around vx,vy (+/- var/2) } } @@ -8296,7 +8278,7 @@ uint16_t mode_particlevolcano(void) PartSys->sources[i].source.vx > 0 ? SEGMENT.custom1 >> 4 : -(SEGMENT.custom1 >> 4); // set moving speed but keep the direction PartSys->sources[i].vy = SEGMENT.speed >> 2; // emitting speed PartSys->sources[i].vx = 0; - PartSys->sources[i].var = SEGMENT.custom3 | 0x01; // emiting variation = nozzle size (custom 3 goes from 0-31), only use odd numbers + PartSys->sources[i].var = SEGMENT.custom3 >> 1; // emiting variation = nozzle size (custom 3 goes from 0-31) // spray[j].source.hue = random16(); //set random color for each particle (using palette) -> does not look good PartSys->sprayEmit(PartSys->sources[i]); PartSys->particleMoveUpdate(PartSys->sources[i].source); //move the source (also applies gravity, which is corrected for above, that is a hack but easier than creating more particlesettings) @@ -8385,7 +8367,7 @@ uint16_t mode_particlefire(void) PartSys->sources[i].minLife = 4; PartSys->sources[i].vx = (int8_t)random(-3, 3); // emitting speed (sideways) PartSys->sources[i].vy = 5 + (firespeed >> 2); // emitting speed (upwards) -> this is good - PartSys->sources[i].var = (random16(2 + (firespeed >> 5)) + 3) | 0x01; // speed variation around vx,vy (+/- var/2), only use odd numbers + PartSys->sources[i].var = (random16(1 + (firespeed >> 5)) + 2); // speed variation around vx,vy (+/- var) } } @@ -8588,7 +8570,6 @@ uint16_t mode_particlewaterfall(void) PartSys->sources[i].maxLife = 400; // lifetime in frames PartSys->sources[i].minLife = 150; #endif - PartSys->sources[i].var = 7; // emiting variation } } else @@ -8627,7 +8608,7 @@ uint16_t mode_particlewaterfall(void) PartSys->sources[i].vy = -SEGMENT.speed >> 3; // emitting speed, down PartSys->sources[i].source.x = map(SEGMENT.custom3, 0, 31, 0, (PartSys->maxXpixel - numSprays * 2) * PS_P_RADIUS) + i * PS_P_RADIUS * 2; // emitter position PartSys->sources[i].source.y = PartSys->maxY + (PS_P_RADIUS * ((i<<2) + 4)); // source y position, few pixels above the top to increase spreading before entering the matrix - PartSys->sources[i].var = (SEGMENT.custom1 >> 3) | 0x01; // emiting variation 0-32, only use odd numbers + PartSys->sources[i].var = (SEGMENT.custom1 >> 3); // emiting variation 0-32 } for (i = 0; i < numSprays; i++) @@ -8918,7 +8899,7 @@ uint16_t mode_particleimpact(void) PartSys->sources[i].source.ttl = random16((255 - SEGMENT.speed)) + 10; // standby time til next launch (in frames at 42fps, max of 265 is about 6 seconds #endif PartSys->sources[i].vy = (SEGMENT.custom1 >> 2); // emitting speed y - PartSys->sources[i].var = (SEGMENT.custom1 >> 1) | 0x01; // speed variation around vx,vy (+/- var/2), only use odd numbers + PartSys->sources[i].var = (SEGMENT.custom1 >> 2); // speed variation around vx,vy (+/- var) } } } @@ -8935,7 +8916,7 @@ uint16_t mode_particleimpact(void) PartSys->sources[i].maxLife = 60; // spark particle life PartSys->sources[i].minLife = 20; PartSys->sources[i].vy = -9; // emitting speed (down) - PartSys->sources[i].var = 5; // speed variation around vx,vy (+/- var/2) + PartSys->sources[i].var = 3; // speed variation around vx,vy (+/- var) } } @@ -8977,8 +8958,7 @@ uint16_t mode_particleattractor(void) PartSys->sources[0].maxLife = 350; // lifetime in frames PartSys->sources[0].minLife = 50; #endif - PartSys->sources[0].var = 7; // emiting variation - PartSys->sources[0].size = 100; //!!! debug remove this again + PartSys->sources[0].var = 4; // emiting variation PartSys->setWallHardness(255); //bounce forever PartSys->setWallRoughness(200); //randomize wall bounce } @@ -9089,7 +9069,7 @@ uint16_t mode_particleattractor(void) #endif PartSys->sources[0].vx = 0; // emitting speed PartSys->sources[0].vy = 0; // emitting speed - PartSys->sources[0].var = 7; // emiting variation + PartSys->sources[0].var = 4; // emiting variation } else PartSys = reinterpret_cast(SEGMENT.data); // if not first call, just set the pointer to the PS @@ -9189,7 +9169,7 @@ uint16_t mode_particlespray(void) PartSys->sources[0].maxLife = 300; // lifetime in frames PartSys->sources[0].minLife = 100; PartSys->sources[0].source.collide = true; // seeded particles will collide (if enabled) - PartSys->sources[0].var = 7; + PartSys->sources[0].var = 3; } else @@ -9218,10 +9198,9 @@ uint16_t mode_particlespray(void) if (SEGMENT.call % (11 - (SEGMENT.intensity / 25)) == 0) // every nth frame, cycle color and emit particles { PartSys->sources[0].source.hue++; // = random16(); //change hue of spray source - // PartSys->sources[i].var = SEGMENT.custom3; // emiting variation = nozzle size (custom 3 goes from 0-32) + // PartSys->sources[i].var = SEGMENT.custom3; // emiting variation = nozzle size (custom 3 goes from 0-32) PartSys->sources[0].source.x = map(SEGMENT.custom1, 0, 255, 0, PartSys->maxX); PartSys->sources[0].source.y = map(SEGMENT.custom2, 0, 255, 0, PartSys->maxY); - // spray[j].source.hue = random16(); //set random color for each particle (using palette) PartSys->angleEmit(PartSys->sources[0], (256 - (((int32_t)SEGMENT.custom3 + 1) << 3)) << 8, SEGMENT.speed >> 2); } @@ -9381,8 +9360,7 @@ uint16_t mode_particleghostrider(void) // Particle System settings PartSys->updateSystem(); // update system properties (dimensions and data pointers) PartSys->setMotionBlur(SEGMENT.custom1); - //PartSys->setColorByAge(SEGMENT.check1); - PartSys->sources[0].var = (1 + (SEGMENT.custom3>>1)) | 0x01; //odd numbers only + PartSys->sources[0].var = SEGMENT.custom3 >> 1; //color by age (PS color by age always starts with hue = 255 so cannot use that) if(SEGMENT.check1) @@ -9404,7 +9382,7 @@ uint16_t mode_particleghostrider(void) int8_t newvy = ((int32_t)sin16(SEGMENT.aux0) * speed) / (int32_t)32767; PartSys->sources[0].source.vx = ((int32_t)cos16(SEGMENT.aux0) * speed) / (int32_t)32767; PartSys->sources[0].source.vy = ((int32_t)sin16(SEGMENT.aux0) * speed) / (int32_t)32767; - PartSys->sources[0].source.ttl = 500; //source never dies + PartSys->sources[0].source.ttl = 500; //source never dies (note: setting 'perpetual' is not needed if replenished each frame) PartSys->particleMoveUpdate(PartSys->sources[0].source, &ghostsettings); //set head (steal one of the particles) PartSys->particles[PartSys->usedParticles-1].x = PartSys->sources[0].source.x; diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index 1e1a41129f..f0e0e5d4ee 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -33,7 +33,7 @@ /* TODO: - -add function to 'update sources' so FX does not have to take care of that. FX can still implement its own version if so desired. config should be optional, if not set, use default config. + -add function to 'update sources' so FX does not have to take care of that. FX can still implement its own version if so desired. -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 */ @@ -70,29 +70,10 @@ ParticleSystem::ParticleSystem(uint16_t width, uint16_t height, uint16_t numbero { particles[i].sat = 255; // full saturation } - /* - Serial.println("alive particles: "); - uint32_t aliveparticles = 0; - for (int i = 0; i < numParticles; i++) - { - aliveparticles++; - } - Serial.println(aliveparticles); - for (int i = 0; i < numParticles; i++) - { - //particles[i].ttl = 0; //initialize all particles to dead - //if (particles[i].ttl) - { - Serial.print("x:"); - Serial.print(particles[i].x); - Serial.print(" y:"); - Serial.println(particles[i].y); - } - }*/ - Serial.println("PS Constructor done"); + //Serial.println("PS Constructor done"); } -//update function applies gravity, moves the particles, handles collisions and renders the particles +// update function applies gravity, moves the particles, handles collisions and renders the particles void ParticleSystem::update(void) { PSadvancedParticle *advprop = NULL; @@ -123,7 +104,6 @@ void ParticleSystem::update(void) particleMoveUpdate(particles[i], &particlesettings, advprop); } - /*TODO remove this Serial.print("alive particles: "); uint32_t aliveparticles = 0; @@ -137,7 +117,7 @@ void ParticleSystem::update(void) ParticleSys_render(); } -//update function for fire animation +// update function for fire animation void ParticleSystem::updateFire(uint32_t intensity, bool renderonly) { if(!renderonly) @@ -205,7 +185,7 @@ void ParticleSystem::setColorByAge(bool enable) void ParticleSystem::setMotionBlur(uint8_t bluramount) { - if(particlesize == 0) //only allwo motion blurring on default particle size or advanced size(cannot combine motion blur with normal blurring used for particlesize, would require another buffer) + if(particlesize == 0) // only allwo motion blurring on default particle size or advanced size(cannot combine motion blur with normal blurring used for particlesize, would require another buffer) motionBlur = bluramount; } @@ -213,8 +193,8 @@ void ParticleSystem::setMotionBlur(uint8_t bluramount) void ParticleSystem::setParticleSize(uint8_t size) { particlesize = size; - particleHardRadius = PS_P_MINHARDRADIUS + particlesize; //note: this sets size if not using advanced props - motionBlur = 0; //disable motion blur if particle size is set + particleHardRadius = PS_P_MINHARDRADIUS + particlesize; // note: this sets size if not using advanced props + motionBlur = 0; // disable motion blur if particle size is set } // enable/disable gravity, optionally, set the force (force=8 is default) can be 1-255, 0 is disable // if enabled, gravity is applied to all particles in ParticleSystemUpdate() @@ -236,8 +216,8 @@ void ParticleSystem::enableParticleCollisions(bool enable, uint8_t hardness) // collisionHardness = hardness + 1; } -// emit one particle with variation -void ParticleSystem::sprayEmit(PSsource &emitter) +// emit one particle with variation, returns index of emitted particle +uint32_t ParticleSystem::sprayEmit(PSsource &emitter) { for (uint32_t i = 0; i < usedParticles; i++) { @@ -246,30 +226,19 @@ void ParticleSystem::sprayEmit(PSsource &emitter) 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 + random(emitter.var) - (emitter.var>>1); //TODO: could use random(min,max) but need to adjust all FX as it would double the var amount - particles[emitIndex].vy = emitter.vy + random(emitter.var) - (emitter.var>>1); - particles[emitIndex].ttl = random(emitter.minLife, emitter.maxLife); + particles[emitIndex].vx = emitter.vx + random(-emitter.var, emitter.var); + particles[emitIndex].vy = emitter.vy + random(-emitter.var, emitter.var); + particles[emitIndex].x = emitter.source.x; + particles[emitIndex].y = emitter.source.y; particles[emitIndex].hue = emitter.source.hue; particles[emitIndex].sat = emitter.source.sat; particles[emitIndex].collide = emitter.source.collide; + particles[emitIndex].ttl = random(emitter.minLife, emitter.maxLife); if (advPartProps) advPartProps[emitIndex].size = emitter.size; - break; + return i; } - /* - 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("**"); } // Spray emitter for particles used for flames (particle TTL depends on source TTL) @@ -284,18 +253,22 @@ void ParticleSystem::flameEmit(PSsource &emitter) { particles[emitIndex].x = emitter.source.x + random16(PS_P_RADIUS<<1) - PS_P_RADIUS; // jitter the flame by one pixel to make the flames wider at the base particles[emitIndex].y = emitter.source.y; - particles[emitIndex].vx = emitter.vx + random16(emitter.var) - (emitter.var >> 1); //random16 is good enough for fire and much faster + particles[emitIndex].vx = emitter.vx + random16(emitter.var) - (emitter.var >> 1); // random16 is good enough for fire and much faster 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 + particles[emitIndex].ttl = random(emitter.minLife, emitter.maxLife) + emitter.source.ttl; // fire uses ttl and not hue for heat, so no need to set the hue break; // done } } + /* + // note: this attemt to save on code size turns out to be much slower as fire uses a lot of particle emits, this must be efficient. also emitter.var would need adjustment + uint32_t partidx = sprayEmit(emitter); //emit one particle + // adjust properties + particles[partidx].x += random16(PS_P_RADIUS<<1) - PS_P_RADIUS; // jitter the flame by one pixel to make the flames wider at the base + particles[partidx].ttl += emitter.source.ttl; // flame intensity dies down with emitter TTL + */ } -//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-65535 (=0-360deg), speed is also affected by emitter->var // angle = 0 means in positive x-direction (i.e. to the right) void ParticleSystem::angleEmit(PSsource &emitter, uint16_t angle, int8_t speed) @@ -303,11 +276,6 @@ void ParticleSystem::angleEmit(PSsource &emitter, uint16_t angle, int8_t speed) emitter.vx = ((int32_t)cos16(angle) * (int32_t)speed) / (int32_t)32600; // cos16() and sin16() return signed 16bit, division should be 32767 but 32600 gives slightly better rounding emitter.vy = ((int32_t)sin16(angle) * (int32_t)speed) / (int32_t)32600; // note: cannot use bit shifts as bit shifting is asymmetrical for positive and negative numbers and this needs to be accurate! sprayEmit(emitter); - /* - Serial.print(" x: "); - Serial.print(emitter.vx); - Serial.print(" y: "); - Serial.println(emitter.vy);*/ } // particle moves, decays and dies, if killoutofbounds is set, out of bounds particles are set to ttl=0 @@ -318,24 +286,23 @@ void ParticleSystem::particleMoveUpdate(PSparticle &part, PSsettings *options, P options = &particlesettings; //use PS system settings by default if (part.ttl > 0) { - if(!part.perpetual) part.ttl--; // age if (particlesettings.colorByAge) part.hue = part.ttl > 255 ? 255 : part.ttl; //set color to ttl - bool usesize = false; //particle uses individual size rendering + bool usesize = false; // particle uses individual size rendering int32_t newX = part.x + (int16_t)part.vx; int32_t 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(advancedproperties) //may be using individual particle size + if(advancedproperties) //using individual particle size? { if(advancedproperties->size > 0) - usesize = true; //note: variable eases out of frame checking below + usesize = true; // note: variable eases out of frame checking below particleHardRadius = max(PS_P_MINHARDRADIUS, (int)particlesize + (advancedproperties->size)); } - //if wall collisions are enabled, bounce them before they reach the edge, it looks much nicer if the particle is not half out of view + // if wall collisions are enabled, bounce them before they reach the edge, it looks much nicer if the particle is not half out of view if (options->bounceX) { if ((newX < particleHardRadius) || (newX > maxX - particleHardRadius)) // reached a wall @@ -351,7 +318,7 @@ void ParticleSystem::particleMoveUpdate(PSparticle &part, PSsettings *options, P else if (((newX <= -PS_P_HALFRADIUS) || (newX > maxX + PS_P_HALFRADIUS))) // particle is leaving, set out of bounds if it has fully left { bool isleaving = true; - if(usesize) //using individual particle size + if(usesize) // using individual particle size { if (((newX > -particleHardRadius) && (newX < maxX + particleHardRadius))) // large particle is not yet leaving the view - note: this is not pixel perfect but good enough isleaving = false; @@ -374,18 +341,8 @@ void ParticleSystem::particleMoveUpdate(PSparticle &part, PSsettings *options, P bounce(part.vy, part.vx, newY, maxY); else { - /* - //TODO: is this check really needed? is checked below. on quick tests, it crashed (but not in all animations... -> seems ok. leave it for now, need to check this later - if (options->useGravity) // do not bounce on top if using gravity (open container) if this is needed implement it in the FX - { - if (newY > maxY + PS_P_HALFRADIUS) - part.outofbounds = 1; // set out of bounds, kill out of bounds over the top does not apply if gravity is used (user can implement it in FX if needed) - } - else*/ if(!options->useGravity) - { bounce(part.vy, part.vx, newY, maxY); - } } } } @@ -399,9 +356,9 @@ void ParticleSystem::particleMoveUpdate(PSparticle &part, PSsettings *options, P else if (((newY <= -PS_P_HALFRADIUS) || (newY > maxY + PS_P_HALFRADIUS))) // particle is leaving, set out of bounds if it has fully left { bool isleaving = true; - if(usesize) //using individual particle size + if(usesize) // using individual particle size { - if (((newY > -particleHardRadius) && (newY < maxY + particleHardRadius))) //still withing rendering reach + if (((newY > -particleHardRadius) && (newY < maxY + particleHardRadius))) // still withing rendering reach isleaving = false; } if(isleaving) @@ -422,20 +379,19 @@ void ParticleSystem::particleMoveUpdate(PSparticle &part, PSsettings *options, P } } -// update advanced particle size control TODO: are the counters needed? it may be good enough like this... +// update advanced particle size control void ParticleSystem::updateSize(PSadvancedParticle *advprops, PSsizeControl *advsize) { - if(advsize == NULL) //just a safety check + if(advsize == NULL) // just a safety check return; - - //grow/shrink particle + // grow/shrink particle int32_t newsize = advprops->size; uint32_t counter = advsize->sizecounter; uint32_t increment; - //calculate grow speed using 0-8 for low speeds and 9-15 for higher speeds + // calculate grow speed using 0-8 for low speeds and 9-15 for higher speeds if(advsize->grow) increment = advsize->growspeed; else if(advsize->shrink) increment = advsize->shrinkspeed; - if(increment < 9) //8 means +1 every frame + if(increment < 9) // 8 means +1 every frame { counter += increment; if(counter > 7) @@ -448,7 +404,7 @@ void ParticleSystem::updateSize(PSadvancedParticle *advprops, PSsizeControl *adv advsize->sizecounter = counter; } else{ - increment = (increment - 8) << 1; //9 means +2, 10 means +4 etc. 15 means +14 + increment = (increment - 8) << 1; // 9 means +2, 10 means +4 etc. 15 means +14 } if(advsize->grow) { @@ -457,8 +413,8 @@ void ParticleSystem::updateSize(PSadvancedParticle *advprops, PSsizeControl *adv newsize += increment; if(newsize >= advsize->maxsize) { - advsize->grow = false; //stop growing, shrink from now on if enabled - newsize = advsize->maxsize; //limit + advsize->grow = false; // stop growing, shrink from now on if enabled + newsize = advsize->maxsize; // limit if(advsize->pulsate) advsize->shrink = true; } } @@ -471,27 +427,26 @@ void ParticleSystem::updateSize(PSadvancedParticle *advprops, PSsizeControl *adv if(newsize <= advsize->minsize) { //if(advsize->minsize == 0) part.ttl = 0; //TODO: need to pass particle or return kill instruction - advsize->shrink = false; //disable shrinking - newsize = advsize->minsize; //limit + advsize->shrink = false; // disable shrinking + newsize = advsize->minsize; // limit if(advsize->pulsate) advsize->grow = true; } } } advprops->size = newsize; - //handle wobbling + // handle wobbling if(advsize->wobble) { - advsize->asymdir += advsize->wobblespeed; //todo: need better wobblespeed control? + advsize->asymdir += advsize->wobblespeed; // todo: need better wobblespeed control? counter is already in the struct... } } // calculate x and y size for asymmetrical particles (advanced size control) void ParticleSystem::getParticleXYsize(PSadvancedParticle *advprops, PSsizeControl *advsize, uint32_t &xsize, uint32_t &ysize) { - //advsize->asymdir = 50; //!!! - if(advsize == NULL) //if advanced size is valid, also advced properties pointer is valid (handled by pointer assignment function) + if(advsize == NULL) // if advanced size is valid, also advced properties pointer is valid (handled by pointer assignment function) return; - int32_t deviation = ((uint32_t)advprops->size * (uint32_t)advsize->asymmetry) / 255; //deviation from symmetrical size + int32_t deviation = ((uint32_t)advprops->size * (uint32_t)advsize->asymmetry) / 255; // deviation from symmetrical size // Calculate x and y size based on deviation and direction (0 is symmetrical, 64 is x, 128 is symmetrical, 192 is y) if (advsize->asymdir < 64) { deviation = ((int32_t)advsize->asymdir * deviation) / 64; @@ -505,27 +460,26 @@ void ParticleSystem::getParticleXYsize(PSadvancedParticle *advprops, PSsizeContr ysize = ((int32_t)advprops->size + deviation) > 255 ? 255 : advprops->size + deviation; } -//function to bounce a particle from a wall using set parameters (wallHardness and wallRoughness) +// function to bounce a particle from a wall using set parameters (wallHardness and wallRoughness) void ParticleSystem::bounce(int8_t &incomingspeed, int8_t ¶llelspeed, int32_t &position, uint16_t maxposition) { - incomingspeed = -incomingspeed; // invert speed + incomingspeed = -incomingspeed; incomingspeed = (incomingspeed * wallHardness) / 255; // reduce speed as energy is lost on non-hard surface if (position < particleHardRadius) - position = particleHardRadius; // fast particles will never reach the edge if position is inverted + position = particleHardRadius; // fast particles will never reach the edge if position is inverted, this looks better else position = maxposition - particleHardRadius; if(wallRoughness) { int32_t totalspeed = abs(incomingspeed) + abs(parallelspeed); - //transfer an amount of incomingspeed speed to parallel speed + // transfer an amount of incomingspeed speed to parallel speed int32_t donatespeed = abs(incomingspeed); donatespeed = (random(-donatespeed, donatespeed) * wallRoughness) / 255; //take random portion of + or - perpendicular speed, scaled by roughness parallelspeed = limitSpeed((int32_t)parallelspeed + donatespeed); incomingspeed = limitSpeed((int32_t)incomingspeed - donatespeed); - donatespeed = totalspeed - abs(parallelspeed); //keep total speed the same + donatespeed = totalspeed - abs(parallelspeed); // keep total speed the same incomingspeed = incomingspeed > 0 ? donatespeed : -donatespeed; } - } // apply a force in x,y direction to individual particle @@ -564,8 +518,7 @@ void ParticleSystem::applyForce(int8_t xforce, int8_t yforce) { // for small forces, need to use a delay counter uint8_t tempcounter; - - //note: this is not the most compuatationally efficient way to do this, but it saves on duplacte code and is fast enough + // note: this is not the most compuatationally efficient way to do this, but it saves on duplacte code and is fast enough for (uint i = 0; i < usedParticles; i++) { tempcounter = forcecounter; @@ -592,6 +545,7 @@ void ParticleSystem::applyAngleForce(uint16_t particleindex, int8_t force, uint1 return; // no advanced properties available applyAngleForce(&particles[particleindex], force, angle, &advPartProps[particleindex].forcecounter); } + // apply a force in angular direction to all particles // angle is from 0-65535 (=0-360deg) angle = 0 means in positive x-direction (i.e. to the right) void ParticleSystem::applyAngleForce(int8_t force, uint16_t angle) @@ -601,9 +555,9 @@ void ParticleSystem::applyAngleForce(int8_t force, uint16_t angle) applyForce(xforce, yforce); } -// 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) // apply gravity to all particles using PS global gforce setting +// force is in 3.4 fixed point notation, see note above // note: faster than apply force since direction is always down and counter is fixed for all particles void ParticleSystem::applyGravity() { @@ -615,8 +569,8 @@ void ParticleSystem::applyGravity() } } -//apply gravity to single particle using system settings (use this for sources) -//function does not increment gravity counter, if gravity setting is disabled, this cannot be used +// apply gravity to single particle using system settings (use this for sources) +// function does not increment gravity counter, if gravity setting is disabled, this cannot be used void ParticleSystem::applyGravity(PSparticle *part) { int32_t dv; // velocity increase @@ -686,17 +640,18 @@ void ParticleSystem::pointAttractor(uint16_t particleindex, PSparticle *attracto applyForce(particleindex, xforce, yforce); } +/* //attract to a line (TODO: this is not yet working) void ParticleSystem::lineAttractor(uint16_t particleindex, PSparticle *attractorcenter, uint16_t attractorangle, uint8_t strength) { // Calculate the distance between the particle and the attractor if(advPartProps == NULL) - return; //no advanced properties available + return; // no advanced properties available - //calculate a second point on the line + // calculate a second point on the line int32_t x1 = attractorcenter->x + (cos16(attractorangle) >> 5); int32_t y1 = attractorcenter->y + (sin16(attractorangle) >> 5); - //calculate squared distance from particle to the line: + // calculate squared distance from particle to the line: int32_t dx = (x1 - attractorcenter->x) >> 4; int32_t dy = (y1 - attractorcenter->y) >> 4; int32_t d = ((dx * (particles[particleindex].y - attractorcenter->y)) - (dy * (particles[particleindex].x - attractorcenter->x))) >> 8; @@ -714,7 +669,7 @@ void ParticleSystem::lineAttractor(uint16_t particleindex, PSparticle *attractor //apply force in a 90° angle to the line int8_t xforce = (d > 0 ? 1 : -1) * (force * dy) / 100; // scale to a lower value, found by experimenting int8_t yforce = (d > 0 ? -1 : 1) * (force * dx) / 100; // note: cannot use bit shifts as bit shifting is asymmetrical for positive and negative numbers and this needs to be accurate! -/* + Serial.print(" partx: "); Serial.print(particles[particleindex].x); Serial.print(" party "); @@ -736,10 +691,11 @@ void ParticleSystem::lineAttractor(uint16_t particleindex, PSparticle *attractor Serial.print(" fx: "); Serial.print(xforce); Serial.print(" fy: "); - Serial.println(yforce);*/ + Serial.println(yforce); applyForce(particleindex, xforce, yforce); -} +}*/ + // 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 // warning: do not render out of bounds particles or system will crash! rendering does not check if particle is out of bounds @@ -755,19 +711,19 @@ void ParticleSystem::ParticleSys_render(bool firemode, uint32_t fireintensity) uint32_t brightness; // particle brightness, fades if dying if (useLocalBuffer) - { - + { /* + //memory fragmentation check: Serial.print("heap: "); Serial.print(heap_caps_get_free_size(MALLOC_CAP_8BIT)); Serial.print(" block: "); - Serial.println(heap_caps_get_largest_free_block(MALLOC_CAP_8BIT));*/ - //cli(); //no interrupts so we get one block of memory for both buffers, less fragmentation -> leads to flickering on S3. so not a good idea. - // allocate empty memory for the local renderbuffer + Serial.println(heap_caps_get_largest_free_block(MALLOC_CAP_8BIT)); + */ + + // allocate empty memory for the local renderbuffer framebuffer = allocate2Dbuffer(maxXpixel + 1, maxYpixel + 1); if (framebuffer == NULL) { - //sei(); //re enable interrupts Serial.println("Frame buffer alloc failed"); useLocalBuffer = false; //render to segment pixels directly if not enough memory } @@ -776,7 +732,6 @@ void ParticleSystem::ParticleSys_render(bool firemode, uint32_t fireintensity) { renderbuffer = allocate2Dbuffer(10, 10); //buffer to render individual particles to if size > 0 note: null checking is done when accessing it } - //sei(); //re enable interrupts if (motionBlur > 0) // using SEGMENT.fadeToBlackBy is much slower, this approximately doubles the speed of fade calculation { uint32_t yflipped; @@ -812,6 +767,7 @@ void ParticleSystem::ParticleSys_render(bool firemode, uint32_t fireintensity) // generate RGB values for particle if(firemode) { + //TODO: decide on a final version... //brightness = (uint32_t)particles[i].ttl * (1 + (fireintensity >> 4)) + (fireintensity >> 2); //this is good //brightness = (uint32_t)particles[i].ttl * (fireintensity >> 3) + (fireintensity >> 1); // this is experimental, also works, flamecolor is more even, does not look as good (but less puffy at lower speeds) //brightness = (((uint32_t)particles[i].ttl * (maxY + PS_P_RADIUS - particles[i].y)) >> 7) + (uint32_t)particles[i].ttl * (fireintensity >> 4) + (fireintensity >> 1); // this is experimental //multiplikation mit weniger als >>4 macht noch mehr puffs bei low speed @@ -826,9 +782,9 @@ void ParticleSystem::ParticleSys_render(bool firemode, uint32_t fireintensity) baseRGB = ColorFromPalette(SEGPALETTE, particles[i].hue, 255, LINEARBLEND); if (particles[i].sat < 255) { - CHSV baseHSV = rgb2hsv_approximate(baseRGB); //convert to hsv - baseHSV.s = particles[i].sat; - baseRGB = (CRGB)baseHSV; //convert back to RGB + CHSV baseHSV = rgb2hsv_approximate(baseRGB); //convert to HSV + baseHSV.s = particles[i].sat; //set the saturation + baseRGB = (CRGB)baseHSV; // convert back to RGB } } @@ -837,13 +793,13 @@ void ParticleSystem::ParticleSys_render(bool firemode, uint32_t fireintensity) if(particlesize > 0) { - uint32_t passes = particlesize/64 + 1; //number of blur passes - uint32_t bluramount = particlesize; //number of blur passes + uint32_t passes = particlesize/64 + 1; // number of blur passes, four passes max + uint32_t bluramount = particlesize; uint32_t bitshift = 0; - for(int i = 0; i < passes; i++) //run four passes max + for(int i = 0; i < passes; i++) { - if(i == 2) //for the last two passes, use higher amount of blur (results in a nicer brightness gradient with soft edges) + if(i == 2) // for the last two passes, use higher amount of blur (results in a nicer brightness gradient with soft edges) bitshift = 1; if (useLocalBuffer) @@ -854,7 +810,7 @@ void ParticleSystem::ParticleSys_render(bool firemode, uint32_t fireintensity) } } - if (useLocalBuffer) //transfer local buffer back to segment + if (useLocalBuffer) // transfer local buffer back to segment { uint32_t yflipped; for (int y = 0; y <= maxYpixel; y++) @@ -865,18 +821,18 @@ void ParticleSystem::ParticleSys_render(bool firemode, uint32_t fireintensity) SEGMENT.setPixelColorXY(x, yflipped, framebuffer[x][y]); } } - free(framebuffer); // free buffer memory + free(framebuffer); } if(renderbuffer) - free(renderbuffer); // free buffer memory + free(renderbuffer); } // calculate pixel positions and brightness distribution and render the particle to local buffer or global buffer void ParticleSystem::renderParticle(CRGB **framebuffer, uint32_t particleindex, uint32_t brightess, CRGB color, CRGB **renderbuffer) { - int32_t pxlbrightness[4] = {0}; //note: pxlbrightness needs to be set to 0 or checking does not work - int32_t pixco[4][2]; //physical pixel coordinates of the four pixels a particle is rendered to. x,y pairs - bool advancedrender = false; //rendering for advanced particles + int32_t pxlbrightness[4] = {0}; // note: pxlbrightness needs to be set to 0 or checking does not work + int32_t pixco[4][2]; // physical pixel coordinates of the four pixels a particle is rendered to. x,y pairs + bool advancedrender = false; // rendering for advanced particles // subtract half a radius as the rendering algorithm always starts at the bottom left, this makes calculations more efficient int32_t xoffset = particles[particleindex].x - PS_P_HALFRADIUS; @@ -886,7 +842,7 @@ void ParticleSystem::renderParticle(CRGB **framebuffer, uint32_t particleindex, 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; - //check if particle has advanced size properties and buffer is available + // check if particle has advanced size properties and buffer is available if(advPartProps) { if(advPartProps[particleindex].size > 0) @@ -894,26 +850,25 @@ void ParticleSystem::renderParticle(CRGB **framebuffer, uint32_t particleindex, if(renderbuffer) { advancedrender = true; - memset(renderbuffer[0], 0, 100 * sizeof(CRGB)); //clear the buffer, renderbuffer is 10x10 pixels + memset(renderbuffer[0], 0, 100 * sizeof(CRGB)); // clear the buffer, renderbuffer is 10x10 pixels } else - return; //cannot render without buffer, advanced size particles are allowed out of frame + return; // cannot render without buffer, advanced size particles are allowed out of frame } } // set the four raw pixel coordinates, the order is bottom left [0], bottom right[1], top right [2], top left [3] - pixco[0][0] = pixco[3][0] = x; // bottom left & top left - pixco[0][1] = pixco[1][1] = y; // bottom left & bottom right - pixco[1][0] = pixco[2][0] = x + 1; // bottom right & top right - pixco[2][1] = pixco[3][1] = y + 1; // top right & top left + pixco[0][0] = pixco[3][0] = x; // bottom left & top left + pixco[0][1] = pixco[1][1] = y; // bottom left & bottom right + pixco[1][0] = pixco[2][0] = x + 1; // bottom right & top right + pixco[2][1] = pixco[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 { 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: + // 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) { pxlbrightness[1] = pxlbrightness[2] = -1; // pixel is actually out of matrix boundaries, do not render @@ -951,7 +906,7 @@ void ParticleSystem::renderParticle(CRGB **framebuffer, uint32_t particleindex, pxlbrightness[2] = pxlbrightness[3] = -1; } - if(advancedrender) //always render full particles in advanced rendering, undo out of frame marking (faster than checking each time in code above) + if(advancedrender) // always render full particles in advanced rendering, undo out of frame marking (faster than checking each time in code above) { for(uint32_t i = 0; i < 4; i++) pxlbrightness[i] = 0; @@ -977,28 +932,26 @@ void ParticleSystem::renderParticle(CRGB **framebuffer, uint32_t particleindex, { //render particle to a bigger size //particle size to pixels: < 64 is 4x4, < 128 is 6x6, < 192 is 8x8, bigger is 10x10 - - //first, render the pixel to the renderbuffer, then apply 2D blurring + //first, render the pixel to the center of the renderbuffer, then apply 2D blurring fast_color_add(renderbuffer[4][4], color, pxlbrightness[0]); // order is: bottom left, bottom right, top right, top left fast_color_add(renderbuffer[5][4], color, pxlbrightness[1]); fast_color_add(renderbuffer[5][5], color, pxlbrightness[2]); fast_color_add(renderbuffer[4][5], color, pxlbrightness[3]); //TODO: make this a loop somehow? needs better coordinate handling... - uint32_t rendersize = 2; //initialize render size, minimum is 4x4 pixels, it is incremented int he loop below to start with 4 - uint32_t offset = 4; //offset to zero coordinate to write/read data in renderbuffer (actually needs to be 3, is decremented in the loop below) - + uint32_t rendersize = 2; // initialize render size, minimum is 4x4 pixels, it is incremented int he loop below to start with 4 + uint32_t offset = 4; // offset to zero coordinate to write/read data in renderbuffer (actually needs to be 3, is decremented in the loop below) uint32_t maxsize = advPartProps[particleindex].size; uint32_t xsize = maxsize; uint32_t ysize = maxsize; - if(advPartSize) //use advanced size control + if(advPartSize) // use advanced size control { if(advPartSize[particleindex].asymmetry > 0) getParticleXYsize(&advPartProps[particleindex], &advPartSize[particleindex], xsize, ysize); maxsize = xsize; if(ysize > maxsize) maxsize = ysize; //maxsize is now the bigger of the two } - maxsize = maxsize/64 + 1; //number of blur passes depends on maxsize + maxsize = maxsize/64 + 1; // number of blur passes depends on maxsize, four passes max uint32_t bitshift = 0; - for(int i = 0; i < maxsize; i++) //run four passes max + for(int i = 0; i < maxsize; i++) { if(i == 2) //for the last two passes, use higher amount of blur (results in a nicer brightness gradient with soft edges) bitshift = 1; @@ -1009,12 +962,12 @@ void ParticleSystem::renderParticle(CRGB **framebuffer, uint32_t particleindex, ysize = ysize > 64 ? ysize - 64 : 0; } - //calculate origin coordinates to render the particle to in the framebuffer + // calculate origin coordinates to render the particle to in the framebuffer uint32_t xfb_orig = x - (rendersize>>1) + 1 - offset; uint32_t yfb_orig = y - (rendersize>>1) + 1 - offset; - uint32_t xfb, yfb; //coordinates in frame buffer to write to note: by making this uint, only overflow has to be checked + uint32_t xfb, yfb; // coordinates in frame buffer to write to note: by making this uint, only overflow has to be checked - //transfer particle renderbuffer to framebuffer + // transfer particle renderbuffer to framebuffer for(uint32_t xrb = offset; xrb < rendersize+offset; xrb++) { xfb = xfb_orig + xrb; @@ -1036,13 +989,11 @@ void ParticleSystem::renderParticle(CRGB **framebuffer, uint32_t particleindex, else continue; } - - //if(xfb < maxXpixel +1 && yfb < maxYpixel +1) fast_color_add(framebuffer[xfb][yfb], renderbuffer[xrb][yrb]); } } } - else //standard rendering + else // standard rendering { if (framebuffer) { @@ -1155,7 +1106,7 @@ void ParticleSystem::handleCollisions() uint32_t startparticle = 0; uint32_t endparticle = usedParticles >> 1; // do half the particles, significantly speeds things up // 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 more accurate collisions are needed, just call it twice in a row if (collisioncounter & 0x01) { startparticle = endparticle; @@ -1163,23 +1114,20 @@ void ParticleSystem::handleCollisions() } collisioncounter++; - //startparticle = 0;//!!!TODO test: do all collisions every frame - //endparticle = usedParticles; - for (i = startparticle; i < endparticle; i++) { // go though all 'higher number' particles and see if any of those are in close proximity and if they are, make them collide if (particles[i].ttl > 0 && particles[i].outofbounds == 0 && particles[i].collide) // if particle is alive and does collide and is not out of view { int32_t dx, dy; // distance to other particles - for (j = i + 1; j < usedParticles; j++) - { // check against higher number particles + 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(advPartProps) //may be using individual particle size { - particleHardRadius = PS_P_MINHARDRADIUS + particlesize + (((uint32_t)advPartProps[i].size + (uint32_t)advPartProps[j].size)>>1); //collision distance + particleHardRadius = PS_P_MINHARDRADIUS + particlesize + (((uint32_t)advPartProps[i].size + (uint32_t)advPartProps[j].size)>>1); // collision distance } if (dx < particleHardRadius && dx > -particleHardRadius) // check x direction, if close, check y direction { @@ -1193,11 +1141,9 @@ void ParticleSystem::handleCollisions() } } - - // handle a collision if close proximity is detected, i.e. dx and/or dy smaller than 2*PS_P_RADIUS // takes two pointers to the particles to collide and the particle hardness (softer means more energy lost in collision, 255 means full hard) -void ParticleSystem::collideParticles(PSparticle *particle1, PSparticle *particle2) //TODO: dx,dy is calculated just above, can pass it over here to save a few CPU cycles +void ParticleSystem::collideParticles(PSparticle *particle1, PSparticle *particle2) // TODO: dx,dy is calculated just above, can pass it over here to save a few CPU cycles? { int32_t dx = particle2->x - particle1->x; int32_t dy = particle2->y - particle1->y; @@ -1225,10 +1171,9 @@ void ParticleSystem::collideParticles(PSparticle *particle1, PSparticle *particl distanceSquared = 2; //1 + 1 } - // Calculate dot product of relative velocity and relative distance - - int32_t dotProduct = (dx * relativeVx + dy * relativeVy); //is always negative if moving towards each other - int32_t notsorandom = dotProduct & 0x01; // random16(2); //dotprouct LSB should be somewhat random, so no need to calculate a random number + // Calculate dot product of relative velocity and relative distance + int32_t dotProduct = (dx * relativeVx + dy * relativeVy); // is always negative if moving towards each other + int32_t notsorandom = dotProduct & 0x01; //dotprouct LSB should be somewhat random, so no need to calculate a random number if (dotProduct < 0) // particles are moving towards each other { @@ -1238,14 +1183,14 @@ void ParticleSystem::collideParticles(PSparticle *particle1, PSparticle *particl // Calculate new velocities after collision uint32_t surfacehardness = collisionHardness < PS_P_MINSURFACEHARDNESS ? PS_P_MINSURFACEHARDNESS : collisionHardness; // if particles are soft, the impulse must stay above a limit or collisions slip through at higher speeds, 170 seems to be a good value int32_t impulse = -(((((-dotProduct) << 15) / distanceSquared) * surfacehardness) >> 8); // note: inverting before bitshift corrects for asymmetry in right-shifts (and is slightly faster) - int32_t ximpulse = ((impulse) * dx) / 32767; //cannot use bit shifts here, it can be negative, use division by 2^bitshift + int32_t ximpulse = ((impulse) * dx) / 32767; // cannot use bit shifts here, it can be negative, use division by 2^bitshift int32_t yimpulse = ((impulse) * dy) / 32767; particle1->vx += ximpulse; particle1->vy += yimpulse; particle2->vx -= ximpulse; particle2->vy -= yimpulse; - if (collisionHardness < surfacehardness) // if particles are soft, they become 'sticky' i.e. apply some friction + if (collisionHardness < surfacehardness) // if particles are soft, they become 'sticky' i.e. apply some friction (they do pile more nicely) { const uint32_t coeff = collisionHardness + (255 - PS_P_MINSURFACEHARDNESS); particle1->vx = ((int32_t)particle1->vx * coeff) / 255; @@ -1263,145 +1208,46 @@ void ParticleSystem::collideParticles(PSparticle *particle1, PSparticle *particl particle2->vy = (particle2->vy < 3 && particle2->vy > -3) ? 0 : particle2->vy; } } - - // this part is for particle piling: slow them down if they are close (they become sticky) and push them so they counteract gravity + // particles have volume, push particles apart if they are too close // tried lots of configurations, it works best if not moved but given a little velocity, it tends to oscillate less this way // a problem with giving velocity is, that on harder collisions, this adds up as it is not dampened enough, so add friction in the FX if required if (dotProduct > -250) //this means particles are slow (or really really close) so push them apart. - { - /** - //only apply friction if particles are slow or else fast moving particles (as in explosions) get slowed a lot - relativeVy *= relativeVy; //square the speed, apply friction if speed is below 10 - if (relativeVy < 100) //particles are slow in y direction -> this works but most animations look much nicer without this friction. add friction in FX if required. - { - //now check x as well (no need to check if y speed is high, this saves some computation time) - relativeVx *= relativeVx; // square the speed, apply friction if speed is below 10 - if (relativeVx < 100) // particles are slow in x direction - { - particle1->vx = ((int32_t)particle1->vx * 254) / 256; - particle2->vx = ((int32_t)particle2->vx * 254) / 256; - - particle1->vy = ((int32_t)particle1->vy * 254) / 256; - particle2->vy = ((int32_t)particle2->vy * 254) / 256; - - } - }*/ - - - - // const int32_t HARDDIAMETER = 2 * particleHardRadius; // push beyond the hard radius, helps with keeping stuff fluffed up -> not really - // int32_t push = (2 * particleHardRadius * particleHardRadius - distanceSquared) >> 6; // push a small amount, if pushing too much, it becomse chaotic as waves of pushing run through piles + { int32_t pushamount = 1 + ((250 + dotProduct) >> 6); // the closer dotproduct is to zero, the closer the particles are - int32_t push; - - // if (dx < HARDDIAMETER && dx > -HARDDIAMETER) //this is always true as it is checked before ntering this function! - { // distance is too small, push them apart - push = 0; - if (dx < 0) // particle 1 is on the right - push = pushamount; //(HARDDIAMETER + dx) / 4; - else if (dx > 0) - push = -pushamount; //-(HARDDIAMETER - dx) / 4; - else // on the same x coordinate, shift it a little so they do not stack - { - - if (notsorandom) - particle1->x++; // move it so pile collapses - else - particle1->x--; - } - - particle1->vx += push; - } - - // if (dy < HARDDIAMETER && dy > -HARDDIAMETER) //dito + int32_t push = 0; + if (dx < 0) // particle 1 is on the right + push = pushamount; + else if (dx > 0) + push = -pushamount; + else // on the same x coordinate, shift it a little so they do not stack { - push = 0; - if (dy < 0) - push = pushamount; //(HARDDIAMETER + dy) / 4; - else if (dy > 0) - push = -pushamount; //-(HARDDIAMETER - dy) / 4; - else // dy==0 - { - if (notsorandom) - particle1->y++; // move it so pile collapses - else - particle1->y--; - } - - particle1->vy += push; - } - /* - if (dx < HARDDIAMETER && dx > -HARDDIAMETER) - { // distance is too small, push them apart - push = 0; - if (dx < 0) // particle 1 is on the right - push = 2; //(HARDDIAMETER + dx) / 4; - else if (dx > 0) - push = -2; //-(HARDDIAMETER - dx) / 4; - else //on the same x coordinate, shift it a little so they do not stack - particle1->x += 2; - if (notsorandom) // chose one of the particles to push, avoids oscillations - { - if (!particle1->flag3) - { - particle1->vx += push; - particle1->flag3 = 1; // particle was pushed, is reset on next push request - } - else - particle1->flag3 = 0; //reset - } + if (notsorandom) + particle1->x++; // move it so pile collapses else - { - if (!particle2->flag3) - { - particle2->vx -= push; - particle2->flag3 = 1; // particle was pushed, is reset on next push request - } - else - particle2->flag3 = 0; // reset - } + particle1->x--; } - - if (dy < HARDDIAMETER && dy > -HARDDIAMETER) + particle1->vx += push; + push = 0; + if (dy < 0) + push = pushamount; + else if (dy > 0) + push = -pushamount; + else // dy==0 { - push = 0; - if (dy < 0) - push = 2; //(HARDDIAMETER + dy) / 4; - else if (dy > 0) - push = -2; //-(HARDDIAMETER - dy) / 4; - - if (!notsorandom) // chose one of the particles to push, avoids oscillations - { - if (!particle1->flag3) - { - particle1->vy += push; - particle1->flag3 = 1; // particle was pushed, is reset on next push request - } - else - particle1->flag3 = 0; // reset - } + if (notsorandom) + particle1->y++; // move it so pile collapses else - { - if (!particle2->flag3) - { - particle2->vy -= push; - particle2->flag3 = 1; // particle was pushed, is reset on next push request - } - else - particle2->flag3 = 0; // reset - } - }*/ - + particle1->y--; + } + particle1->vy += push; // note: pushing may push particles out of frame, if bounce is active, it will move it back as position will be limited to within frame, if bounce is disabled: bye bye } } - - } -//calculate the delta speed (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 +// calculate the delta speed (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) { if(force == 0) @@ -1426,7 +1272,7 @@ int32_t ParticleSystem::calcForce_dv(int8_t force, uint8_t* counter) return dv; } -//limit speed to prevent overflows +// limit speed to prevent overflows int32_t ParticleSystem::limitSpeed(int32_t speed) { return speed > PS_P_MAXSPEED ? PS_P_MAXSPEED : (speed < -PS_P_MAXSPEED ? -PS_P_MAXSPEED : speed); @@ -1445,14 +1291,13 @@ CRGB **ParticleSystem::allocate2Dbuffer(uint32_t cols, uint32_t rows) for (uint i = 0; i < cols; i++) { array2D[i] = start + i * rows; - } - //memset(start, 0, cols * rows * sizeof(CRGB)); // set all values to zero (TODO: remove, not needed if calloc is used) + } } return array2D; } -//update size and pointers (memory location and size can change dynamically) -//note: do not access the PS class in FX befor running this function (or it messes up SEGMENT.data) +// update size and pointers (memory location and size can change dynamically) +// note: do not access the PS class in FX befor running this function (or it messes up SEGMENT.data) void ParticleSystem::updateSystem(void) { // update matrix size @@ -1467,12 +1312,12 @@ void ParticleSystem::updateSystem(void) // FX handles the PSsources, need to tell this function how many there are void ParticleSystem::updatePSpointers(bool isadvanced, bool sizecontrol) { - //DEBUG_PRINT(F("*** PS pointers ***")); - //DEBUG_PRINTF_P(PSTR("this PS %p "), this); - //Note on memory alignment: - //a pointer MUST be 4 byte aligned. sizeof() in a struct/class is always aligned to the largest element. if it contains a 32bit, it will be padded to 4 bytes, 16bit is padded to 2byte alignment. - //The PS is aligned to 4 bytes, a PSparticle is aligned to 2 and a struct containing only byte sized variables is not aligned at all and may need to be padded when dividing the memoryblock. - //by making sure that the number of sources and particles is a multiple of 4, padding can be skipped here as alignent is ensured, independent of struct sizes. + // DEBUG_PRINT(F("*** PS pointers ***")); + // DEBUG_PRINTF_P(PSTR("this PS %p "), this); + // Note on memory alignment: + // a pointer MUST be 4 byte aligned. sizeof() in a struct/class is always aligned to the largest element. if it contains a 32bit, it will be padded to 4 bytes, 16bit is padded to 2byte alignment. + // The PS is aligned to 4 bytes, a PSparticle is aligned to 2 and a struct containing only byte sized variables is not aligned at all and may need to be padded when dividing the memoryblock. + // by making sure that the number of sources and particles is a multiple of 4, padding can be skipped here as alignent is ensured, independent of struct sizes. particles = reinterpret_cast(this + 1); // pointer to particle array at data+sizeof(ParticleSystem) sources = reinterpret_cast(particles + numParticles); // pointer to source(s) if(isadvanced) @@ -1538,7 +1383,7 @@ uint32_t calculateNumberOfSources(uint8_t requestedsources) int numberofSources = min((cols * rows) / 4, (uint32_t)requestedsources); numberofSources = max(1, min(numberofSources, ESP32_MAXSOURCES)); // limit to 1 - 64 #endif - //make sure it is a multiple of 4 for proper memory alignment + // make sure it is a multiple of 4 for proper memory alignment numberofSources = ((numberofSources+3) >> 2) << 2; return numberofSources; } @@ -1547,7 +1392,7 @@ uint32_t calculateNumberOfSources(uint8_t requestedsources) bool allocateParticleSystemMemory(uint16_t numparticles, uint16_t numsources, bool isadvanced, bool sizecontrol, uint16_t additionalbytes) { uint32_t requiredmemory = sizeof(ParticleSystem); - //functions above make sure these are a multiple of 4 bytes (to avoid alignment issues) + // functions above make sure these are a multiple of 4 bytes (to avoid alignment issues) requiredmemory += sizeof(PSparticle) * numparticles; if (isadvanced) requiredmemory += sizeof(PSadvancedParticle) * numparticles; @@ -1581,7 +1426,7 @@ bool initParticleSystem(ParticleSystem *&PartSys, uint8_t requestedsources, uint 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, largesizes, sizecontrol); // particle system constructor TODO: why does VS studio thinkt this is bad? + PartSys = new (SEGMENT.data) ParticleSystem(cols, rows, numparticles, numsources, largesizes, sizecontrol); // particle system constructor //Serial.print("PS pointer at "); //Serial.println((uintptr_t)PartSys); return true; @@ -1591,7 +1436,6 @@ bool initParticleSystem(ParticleSystem *&PartSys, uint8_t requestedsources, uint // Utility Functions // /////////////////////// - // fastled color adding is very inaccurate in color preservation // a better color add function is implemented in colors.cpp but it uses 32bit RGBW. to use it colors need to be shifted just to then be shifted back by that function, which is slow // this is a fast version for RGB (no white channel, PS does not handle white) and with native CRGB including scaling of second color (fastled scale8 can be made faster using native 32bit on ESP) @@ -1611,13 +1455,13 @@ void fast_color_add(CRGB &c1, CRGB &c2, uint32_t scale) b = c1.b + c2.b; } uint32_t max = r; - if (g > max) //note: using ? operator would be slower by 2 instructions + if (g > max) // note: using ? operator would be slower by 2 instructions max = g; if (b > max) max = b; if (max < 256) { - c1.r = r; //save result to c1 + c1.r = r; // save result to c1 c1.g = g; c1.b = b; } @@ -1629,7 +1473,7 @@ void fast_color_add(CRGB &c1, CRGB &c2, uint32_t scale) } } -//faster than fastled color scaling as it uses a 32bit scale factor and pointer +// faster than fastled color scaling as it uses a 32bit scale factor and pointer void fast_color_scale(CRGB &c, uint32_t scale) { c.r = ((c.r * scale) >> 8); @@ -1637,15 +1481,11 @@ void fast_color_scale(CRGB &c, uint32_t scale) c.b = ((c.b * scale) >> 8); } - - // blur a matrix in x and y direction, blur can be asymmetric in x and y // for speed, 32bit variables are used, make sure to limit them to 8bit (0-255) or result is undefined // to blur a subset of the buffer, change the xsize/ysize and set xstart/ystart to the desired starting coordinates (default start is 0/0) void blur2D(CRGB **colorbuffer, uint32_t xsize, uint32_t ysize, uint32_t xblur, uint32_t yblur, bool smear, uint32_t xstart, uint32_t ystart, bool isparticle) { - - //TODO: for particle rendering, first row and last row can be skipped in x blurring as it is all black, this would increase rendering speed CRGB seeppart, carryover; uint32_t seep = xblur >> 1; if(isparticle) //first and last row are always black in particle rendering @@ -1658,22 +1498,22 @@ void blur2D(CRGB **colorbuffer, uint32_t xsize, uint32_t ysize, uint32_t xblur, carryover = BLACK; for(uint32_t x = xstart; x < xstart + xsize; x++) { - seeppart = colorbuffer[x][y]; //create copy of current color - fast_color_scale(seeppart, seep); //scale it and seep to neighbours - if(!smear) //fade current pixel if smear is disabled + seeppart = colorbuffer[x][y]; // create copy of current color + fast_color_scale(seeppart, seep); // scale it and seep to neighbours + if(!smear) // fade current pixel if smear is disabled fast_color_scale(colorbuffer[x][y], 255 - xblur); if(x > 0) { fast_color_add(colorbuffer[x-1][y], seeppart); - fast_color_add(colorbuffer[x][y], carryover); //todo: could check if carryover is > 0, takes 7 instructions, add takes ~35, with lots of same color pixels (like background), it would be faster + fast_color_add(colorbuffer[x][y], carryover); // TODO: could check if carryover is > 0, takes 7 instructions, add takes ~35, with lots of same color pixels (like background), it would be faster } carryover = seeppart; } fast_color_add(colorbuffer[xsize-1][y], carryover); // set last pixel } - if(isparticle) //now also do first and last row + if(isparticle) // now also do first and last row { ystart--; ysize++; @@ -1685,15 +1525,15 @@ void blur2D(CRGB **colorbuffer, uint32_t xsize, uint32_t ysize, uint32_t xblur, carryover = BLACK; for(uint32_t y = ystart; y < ystart + ysize; y++) { - seeppart = colorbuffer[x][y]; //create copy of current color - fast_color_scale(seeppart, seep); //scale it and seep to neighbours - if(!smear) //fade current pixel if smear is disabled + seeppart = colorbuffer[x][y]; // create copy of current color + fast_color_scale(seeppart, seep); // scale it and seep to neighbours + if(!smear) // fade current pixel if smear is disabled fast_color_scale(colorbuffer[x][y], 255 - yblur); if(y > 0) { fast_color_add(colorbuffer[x][y-1], seeppart); - fast_color_add(colorbuffer[x][y], carryover); //todo: could check if carryover is > 0, takes 7 instructions, add takes ~35, with lots of same color pixels (like background), it would be faster + fast_color_add(colorbuffer[x][y], carryover); // todo: could check if carryover is > 0, takes 7 instructions, add takes ~35, with lots of same color pixels (like background), it would be faster } carryover = seeppart; } diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index ecf9c262a5..2195759eca 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -127,8 +127,8 @@ class ParticleSystem void updateSystem(void); // call at the beginning of every FX, updates pointers and dimensions // particle emitters + uint32_t sprayEmit(PSsource &emitter); void flameEmit(PSsource &emitter); - void sprayEmit(PSsource &emitter); void angleEmit(PSsource& emitter, uint16_t angle, int8_t speed); // move functions From 0b45f6652a619cdb315d437fb8dd9c0038f4b792 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sun, 5 May 2024 21:56:19 +0200 Subject: [PATCH 076/219] bugfix, sprayEmit() would not return and get stuck - forgot default return value... --- wled00/FXparticleSystem.cpp | 7 ++++--- wled00/FXparticleSystem.h | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index f0e0e5d4ee..3a8b4cf7ae 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -216,10 +216,10 @@ void ParticleSystem::enableParticleCollisions(bool enable, uint8_t hardness) // collisionHardness = hardness + 1; } -// emit one particle with variation, returns index of emitted particle -uint32_t ParticleSystem::sprayEmit(PSsource &emitter) +// emit one particle with variation, returns index of emitted particle (or -1 if no particle emitted) +int32_t ParticleSystem::sprayEmit(PSsource &emitter) { - for (uint32_t i = 0; i < usedParticles; i++) + for (int32_t i = 0; i < usedParticles; i++) { emitIndex++; if (emitIndex >= usedParticles) @@ -239,6 +239,7 @@ uint32_t ParticleSystem::sprayEmit(PSsource &emitter) return i; } } + return -1; } // Spray emitter for particles used for flames (particle TTL depends on source TTL) diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index 2195759eca..281afee228 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -127,7 +127,7 @@ class ParticleSystem void updateSystem(void); // call at the beginning of every FX, updates pointers and dimensions // particle emitters - uint32_t sprayEmit(PSsource &emitter); + int32_t sprayEmit(PSsource &emitter); void flameEmit(PSsource &emitter); void angleEmit(PSsource& emitter, uint16_t angle, int8_t speed); From a13c81ba546b4936c4d06129beb335d99eee20ca Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Mon, 6 May 2024 19:48:29 +0200 Subject: [PATCH 077/219] remove esp_random() as it is not supported on ESP8266 --- wled00/FX.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index af9d828744..c430104eb6 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -8117,7 +8117,7 @@ uint16_t mode_particlefireworks(void) currentspeed = speed; counter = 0; angleincrement = 2730 + random16(5461); // minimum 15° (=2730), + random(30°) (=5461) - angle = esp_random(); // random start angle + angle = random16(); // random start angle speedvariation = angle & 0x01; // 0 or 1, no need for a new random number // calculate the number of particles to make complete circles percircle = (uint16_t)0xFFFF / angleincrement + 1; @@ -8310,7 +8310,7 @@ uint16_t mode_particlefire(void) { if (!initParticleSystem(PartSys, 25, 4)) //maximum number of source (PS will determine the exact number based on segment size) and need 4 additional bytes for time keeping (uint32_t lastcall) return mode_static(); // allocation failed; //allocation failed - SEGMENT.aux0 = esp_random(); // aux0 is wind position (index) in the perlin noise + SEGMENT.aux0 = random(); // aux0 is wind position (index) in the perlin noise numFlames = PartSys->numSources; DEBUG_PRINTF_P(PSTR("segment data ptr in fireFX %p\n"), SEGMENT.data); } From 1e6e84e52cbc9b295157b21d299c83882964ee26 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Tue, 7 May 2024 18:27:40 +0200 Subject: [PATCH 078/219] small bugfix for ESP32, cleanup / reformatting --- wled00/FX.cpp | 334 +++-- wled00/FXparticleSystem.cpp | 2384 +++++++++++++++++------------------ wled00/FXparticleSystem.h | 127 +- 3 files changed, 1415 insertions(+), 1430 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index c430104eb6..b6c4a04603 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -7909,18 +7909,18 @@ uint16_t mode_particlevortex(void) if (SEGMENT.call == 0) // initialization { if (!initParticleSystem(PartSys, NUMBEROFSOURCES)) - return mode_static(); // allocation failed; //allocation failed + return mode_static(); // allocation failed - //SEGMENT.aux0 = 0; // starting angle + //SEGMENT.aux0 = 0; // starting angle SEGMENT.aux1 = 0x01; // check flags - #ifdef ESP8266 + #ifdef ESP8266 PartSys->setMotionBlur(150); #else PartSys->setMotionBlur(100); #endif uint8_t numSprays = min(PartSys->numSources, (uint8_t)NUMBEROFSOURCES); for (i = 0; i < numSprays; i++) - { + { PartSys->sources[i].source.x = (PartSys->maxX + 1) >> 1; // center PartSys->sources[i].source.y = (PartSys->maxY + 1) >> 1; // center PartSys->sources[i].maxLife = 900; @@ -7939,8 +7939,8 @@ uint16_t mode_particlevortex(void) PartSys->updateSystem(); // update system properties (dimensions and data pointers) uint8_t spraycount = min(PartSys->numSources, (uint8_t)(1 + (SEGMENT.custom1 >> 5))); // number of sprays to display, 1-8 - #ifdef ESP8266 - for (i = 1; i < 4; i++) //need static particles in the center to reduce blinking (would be black every other frame without this hack), just set them there fixed + #ifdef ESP8266 + for (i = 1; i < 4; i++) // need static particles in the center to reduce blinking (would be black every other frame without this hack), just set them there fixed { PartSys->particles[PartSys->numParticles - i].x = (PartSys->maxX + 1) >> 1; // center PartSys->particles[PartSys->numParticles - i].y = (PartSys->maxY + 1) >> 1; // center @@ -7948,7 +7948,7 @@ uint16_t mode_particlevortex(void) PartSys->particles[PartSys->numParticles - i].ttl = 255; //set alive } #endif - if (SEGMENT.check1 != (SEGMENT.aux1 & 0x01) || SEGMENT.call == 0) //state change + if (SEGMENT.check1 != (SEGMENT.aux1 & 0x01) || SEGMENT.call == 0) // state change { if (SEGMENT.check1) SEGMENT.aux1 |= 0x01; //set the flag @@ -7972,7 +7972,7 @@ uint16_t mode_particlevortex(void) // set rotation direction and speed // can use direction flag to determine current direction bool direction = SEGMENT.check2; //no automatic direction change, set it to flag - int32_t currentspeed = (int32_t)SEGMENT.step; // make a signed integer out of step + int32_t currentspeed = (int32_t)SEGMENT.step; // make a signed integer out of step if (SEGMENT.custom2 > 0) // automatic direction change enabled { @@ -7989,8 +7989,8 @@ uint16_t mode_particlevortex(void) SEGMENT.aux1 |= 0x04; // set the update flag (for random interval update) if (direction) SEGMENT.aux1 &= ~0x02; // clear the direction flag - else - SEGMENT.aux1 |= 0x02; // set the direction flag + else + SEGMENT.aux1 |= 0x02; // set the direction flag } } @@ -8006,7 +8006,7 @@ uint16_t mode_particlevortex(void) speedincrement = 1; } - currentspeed += speedincrement; + currentspeed += speedincrement; SEGMENT.aux0 += currentspeed; SEGMENT.step = (uint32_t)currentspeed; //save it back @@ -8055,14 +8055,14 @@ uint16_t mode_particlefireworks(void) { if (!initParticleSystem(PartSys, NUMBEROFSOURCES, true)) // init with advanced particle properties return mode_static(); // allocation failed - PartSys->setKillOutOfBounds(true); //out of bounds particles dont return (except on top, taken care of by gravity setting) - PartSys->setWallHardness(100); //ground bounce is fixed + PartSys->setKillOutOfBounds(true); //out of bounds particles dont return (except on top, taken care of by gravity setting) + PartSys->setWallHardness(100); //ground bounce is fixed numRockets = min(PartSys->numSources, (uint8_t)NUMBEROFSOURCES); for (j = 0; j < numRockets; j++) { PartSys->sources[j].source.ttl = 500 * j; // first rocket starts immediately, others follow soon PartSys->sources[j].source.vy = -1; // at negative speed, no particles are emitted and if rocket dies, it will be relaunched - } + } } else PartSys = reinterpret_cast(SEGMENT.data); // if not first call, just set the pointer to the PS @@ -8075,9 +8075,9 @@ uint16_t mode_particlefireworks(void) PartSys->updateSystem(); // update system properties (dimensions and data pointers) numRockets = min(PartSys->numSources, (uint8_t)NUMBEROFSOURCES); - PartSys->setWrapX(SEGMENT.check1); + PartSys->setWrapX(SEGMENT.check1); PartSys->setBounceY(SEGMENT.check2); - PartSys->setGravity(map(SEGMENT.custom3,0,31,0,10)); // todo: make it a slider to adjust + PartSys->setGravity(map(SEGMENT.custom3,0,31,0,10)); // check each rocket's state and emit particles according to its state: moving up = emit exhaust, at top = explode; falling down = standby time uint32_t emitparticles; // number of particles to emit for each rocket's state @@ -8105,7 +8105,7 @@ uint16_t mode_particlefireworks(void) else // speed is zero, explode! { #ifdef ESP8266 - emitparticles = random16(SEGMENT.intensity >> 3) + (SEGMENT.intensity >> 3) + 5; // defines the size of the explosion + emitparticles = random16(SEGMENT.intensity >> 3) + (SEGMENT.intensity >> 3) + 5; // defines the size of the explosion #else emitparticles = random16(SEGMENT.intensity >> 2) + (SEGMENT.intensity >> 2) + 5; // defines the size of the explosion #endif @@ -8116,7 +8116,7 @@ uint16_t mode_particlefireworks(void) speed = 2 + random16(3); currentspeed = speed; counter = 0; - angleincrement = 2730 + random16(5461); // minimum 15° (=2730), + random(30°) (=5461) + angleincrement = 2730 + random16(5461); // minimum 15° (=2730), + random(30°) (=5461) angle = random16(); // random start angle speedvariation = angle & 0x01; // 0 or 1, no need for a new random number // calculate the number of particles to make complete circles @@ -8149,7 +8149,7 @@ uint16_t mode_particlefireworks(void) else { /* - //TODO: this does not look good. adjust or remove completely + //TODO: this does not look good. adjust or remove completely if( PartSys->sources[j].source.vy < 0) //explosion is ongoing { if(i < (emitparticles>>2)) //set 1/4 of particles to larger size @@ -8167,7 +8167,7 @@ uint16_t mode_particlefireworks(void) } if(i == 0) //no particles emitted, this rocket is falling PartSys->sources[j].source.y = 1000; // set position up high so gravity wont pull it to the ground and bounce it (vy MUST stay negative until relaunch) - circularexplosion = false; //reset for next rocket + circularexplosion = false; // reset for next rocket } // update the rockets, set the speed state @@ -8192,8 +8192,8 @@ uint16_t mode_particlefireworks(void) // reinitialize rocket PartSys->sources[j].source.y = PS_P_RADIUS<<1; // start from bottom PartSys->sources[j].source.x = random(PartSys->maxX >> 2, PartSys->maxX >> 1); // centered half - PartSys->sources[j].source.vy = random16(SEGMENT.custom1 >> 3) + 5; // rocket speed depends also on rocket fuse - PartSys->sources[j].source.vx = random(-3,3); //i.e. not perfectly straight up + PartSys->sources[j].source.vy = random16(SEGMENT.custom1 >> 3) + 5; // rocket speed depends also on rocket fuse + PartSys->sources[j].source.vx = random(-3,3); // not perfectly straight up PartSys->sources[j].source.sat = 30; // low saturation -> exhaust is off-white PartSys->sources[j].source.ttl = random16(SEGMENT.custom1) + (SEGMENT.custom1 >> 1); // sets explosion height (rockets explode at the top if set too high as paticle update set speed to zero if moving out of matrix) PartSys->sources[j].maxLife = 40; // exhaust particle life @@ -8204,7 +8204,7 @@ uint16_t mode_particlefireworks(void) } } - PartSys->update(); // update and render + PartSys->update(); // update and render return FRAMETIME; } #undef NUMBEROFSOURCES @@ -8222,25 +8222,25 @@ uint16_t mode_particlevolcano(void) if (SEGLEN == 1) return mode_static(); ParticleSystem *PartSys = NULL; - uint8_t numSprays; //note: so far only one tested but more is possible + uint8_t numSprays; // note: so far only one tested but more is possible uint32_t i = 0; - if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. + if (SEGMENT.call == 0) // initialization { if (!initParticleSystem(PartSys, NUMBEROFSOURCES)) // init, no additional data needed return mode_static(); // allocation failed PartSys->setBounceY(true); PartSys->setGravity(); // enable with default gforce PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting) - PartSys->setMotionBlur(190); //anable motion blur + PartSys->setMotionBlur(190); // anable motion blur numSprays = min(PartSys->numSources, (uint8_t)NUMBEROFSOURCES); // number of sprays for (i = 0; i < numSprays; i++) { - PartSys->sources[i].source.hue = random16(); - PartSys->sources[i].source.x = PartSys->maxX / (numSprays + 1) * (i + 1); //distribute evenly + PartSys->sources[i].source.hue = random16(); + PartSys->sources[i].source.x = PartSys->maxX / (numSprays + 1) * (i + 1); // distribute evenly PartSys->sources[i].maxLife = 300; // lifetime in frames PartSys->sources[i].minLife = 250; - PartSys->sources[i].source.collide = true; // seeded particles will collide (if enabled) + PartSys->sources[i].source.collide = true; // seeded particles will collide (if enabled) PartSys->sources[i].source.perpetual = true; // source never dies } } @@ -8259,7 +8259,7 @@ uint16_t mode_particlevolcano(void) PartSys->updateSystem(); // update system properties (dimensions and data pointers) PartSys->setColorByAge(SEGMENT.check1); PartSys->setBounceX(SEGMENT.check2); - PartSys->setWallHardness(SEGMENT.custom2); + PartSys->setWallHardness(SEGMENT.custom2); if (SEGMENT.check3) // collisions enabled PartSys->enableParticleCollisions(true, SEGMENT.custom2); // enable collisions and set particle collision hardness @@ -8273,16 +8273,13 @@ uint16_t mode_particlevolcano(void) { PartSys->sources[i].source.y = PS_P_RADIUS + 5; // reset to just above the lower edge that is allowed for bouncing particles, if zero, particles already 'bounce' at start and loose speed. PartSys->sources[i].source.vy = 0; //reset speed (so no extra particlesettin is required to keep the source 'afloat') - PartSys->sources[i].source.hue++; // = random16(); //change hue of spray source - // percycle = 1+(SEGMENT.intensity>>4); //how many particles are sprayed per cycle and how fast ist the color changing - PartSys->sources[i].source.vx > 0 ? SEGMENT.custom1 >> 4 : -(SEGMENT.custom1 >> 4); // set moving speed but keep the direction + PartSys->sources[i].source.hue++; // = random16(); //change hue of spray source (note: random does not look good) + PartSys->sources[i].source.vx = PartSys->sources[i].source.vx > 0 ? SEGMENT.custom1 >> 4 : -(SEGMENT.custom1 >> 4); // set moving speed but keep the direction given by PS PartSys->sources[i].vy = SEGMENT.speed >> 2; // emitting speed PartSys->sources[i].vx = 0; - PartSys->sources[i].var = SEGMENT.custom3 >> 1; // emiting variation = nozzle size (custom 3 goes from 0-31) - // spray[j].source.hue = random16(); //set random color for each particle (using palette) -> does not look good + PartSys->sources[i].var = SEGMENT.custom3 >> 1; // emiting variation = nozzle size (custom 3 goes from 0-31) PartSys->sprayEmit(PartSys->sources[i]); PartSys->particleMoveUpdate(PartSys->sources[i].source); //move the source (also applies gravity, which is corrected for above, that is a hack but easier than creating more particlesettings) - //Serial.println("emit"); } } @@ -8304,13 +8301,13 @@ uint16_t mode_particlefire(void) ParticleSystem *PartSys = NULL; uint32_t i; // index variable - uint32_t numFlames; // number of flames: depends on fire width. for a fire width of 16 pixels, about 25-30 flames give good results + uint32_t numFlames; // number of flames: depends on fire width. for a fire width of 16 pixels, about 25-30 flames give good results if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. { if (!initParticleSystem(PartSys, 25, 4)) //maximum number of source (PS will determine the exact number based on segment size) and need 4 additional bytes for time keeping (uint32_t lastcall) return mode_static(); // allocation failed; //allocation failed - SEGMENT.aux0 = random(); // aux0 is wind position (index) in the perlin noise + SEGMENT.aux0 = random16(); // aux0 is wind position (index) in the perlin noise numFlames = PartSys->numSources; DEBUG_PRINTF_P(PSTR("segment data ptr in fireFX %p\n"), SEGMENT.data); } @@ -8331,7 +8328,7 @@ uint16_t mode_particlefire(void) if (SEGMENT.speed < 100) //slow, limit FPS { uint32_t *lastcall = reinterpret_cast(PartSys->PSdataEnd); - uint32_t period = strip.now - *lastcall; + uint32_t period = strip.now - *lastcall; if (period < (uint32_t)map(SEGMENT.speed, 0, 99, 50, 10)) // limit to 90FPS - 20FPS { SEGMENT.call--; //skipping a frame, decrement the counter (on call0, this is never executed as lastcall is 0, so its fine to not check if >0) @@ -8352,21 +8349,21 @@ uint16_t mode_particlefire(void) { if (PartSys->sources[i].source.ttl > 0) { - PartSys->sources[i].source.ttl--; + PartSys->sources[i].source.ttl--; } else // flame source is dead { - // initialize new flame: set properties of source + // initialize new flame: set properties of source if (random16(20) == 0 || SEGMENT.call == 0) // from time to time, change flame position - { - PartSys->sources[i].source.x = (PartSys->maxX >> 1) - (spread>>1) + random(spread); // distribute randomly on chosen width + { + PartSys->sources[i].source.x = (PartSys->maxX >> 1) - (spread>>1) + random(spread); // distribute randomly on chosen width } PartSys->sources[i].source.y = -PS_P_RADIUS; // set the source below the frame PartSys->sources[i].source.ttl = 5 + random16((SEGMENT.custom1 * SEGMENT.custom1) >> 7) / (2 + (firespeed >> 4)); //'hotness' of fire, faster flames reduce the effect or flame height will scale too much with speed -> new, this works! PartSys->sources[i].maxLife = random16(7) + 13; // defines flame height together with the vy speed, vy speed*maxlife/PS_P_RADIUS is the average flame height PartSys->sources[i].minLife = 4; - PartSys->sources[i].vx = (int8_t)random(-3, 3); // emitting speed (sideways) - PartSys->sources[i].vy = 5 + (firespeed >> 2); // emitting speed (upwards) -> this is good + PartSys->sources[i].vx = (int8_t)random(-3, 3); // emitting speed (sideways) + PartSys->sources[i].vy = 5 + (firespeed >> 2); // emitting speed (upwards) -> this is good PartSys->sources[i].var = (random16(1 + (firespeed >> 5)) + 2); // speed variation around vx,vy (+/- var) } @@ -8375,7 +8372,7 @@ uint16_t mode_particlefire(void) if (SEGMENT.call & 0x01) // update noise position every second frames, also add wind { SEGMENT.aux0++; // position in the perlin noise matrix for wind generation - if (SEGMENT.call & 0x02) //every third frame + if (SEGMENT.call & 0x02) // every third frame SEGMENT.aux1++; // move in noise y direction so noise does not repeat as often // add wind force to all particles int8_t windspeed = ((int16_t)(inoise8(SEGMENT.aux0, SEGMENT.aux1) - 127) * SEGMENT.custom2) >> 7; @@ -8403,7 +8400,7 @@ uint16_t mode_particlefire(void) for(i=0; i < percycle; i++) { j = (j + 1) % numFlames; - PartSys->flameEmit(PartSys->sources[j]); + PartSys->flameEmit(PartSys->sources[j]); } PartSys->updateFire(SEGMENT.intensity); // update and render the fire @@ -8427,11 +8424,11 @@ uint16_t mode_particlepit(void) if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. { - if (!initParticleSystem(PartSys, 1, 0, true)) //init, request one source (actually dont really need one TODO: test if using zero sources also works) + if (!initParticleSystem(PartSys, 1, 0, true)) // init, request one source (actually dont really need one TODO: test if using zero sources also works) return mode_static(); // allocation failed; //allocation failed PartSys->setKillOutOfBounds(true); - PartSys->setGravity(); //enable with default gravity - PartSys->setUsedParticles((PartSys->numParticles*3)/2); //use 2/3 of available particles + PartSys->setGravity(); // enable with default gravity + PartSys->setUsedParticles((PartSys->numParticles*3)/2); // use 2/3 of available particles } else PartSys = reinterpret_cast(SEGMENT.data); // if not first call, just set the pointer to the PS @@ -8444,8 +8441,8 @@ uint16_t mode_particlepit(void) PartSys->updateSystem(); // update system properties (dimensions and data pointers) PartSys->setWrapX(SEGMENT.check1); PartSys->setBounceX(SEGMENT.check2); - PartSys->setBounceY(SEGMENT.check3); - PartSys->setWallHardness(min(SEGMENT.custom2, (uint8_t)150)); //limit to 100 min (if collisions are disabled, still want bouncy) + PartSys->setBounceY(SEGMENT.check3); + PartSys->setWallHardness(min(SEGMENT.custom2, (uint8_t)150)); // limit to 100 min (if collisions are disabled, still want bouncy) if (SEGMENT.custom2>0) { PartSys->enableParticleCollisions(true, SEGMENT.custom2); // enable collisions and set particle collision hardness @@ -8454,7 +8451,7 @@ uint16_t mode_particlepit(void) PartSys->enableParticleCollisions(false); } - uint32_t i; + uint32_t i; if (SEGMENT.call % (128 - (SEGMENT.intensity >> 1)) == 0 && SEGMENT.intensity > 0) // every nth frame emit particles, stop emitting if set to zero { for (i = 0; i < PartSys->usedParticles; i++) // emit particles @@ -8467,19 +8464,19 @@ uint16_t mode_particlepit(void) PartSys->particles[i].y = (PartSys->maxY<<1); // particles appear somewhere above the matrix, maximum is double the height PartSys->particles[i].vx = (int16_t)random(SEGMENT.speed >> 1) - (SEGMENT.speed >> 2); // side speed is +/- PartSys->particles[i].vy = map(SEGMENT.speed, 0, 255, -5, -100); // downward speed - PartSys->particles[i].hue = random16(); // set random color - PartSys->particles[i].collide = true; //enable collision for particle + PartSys->particles[i].hue = random16(); // set random color + PartSys->particles[i].collide = true; // enable collision for particle PartSys->particles[i].sat = ((SEGMENT.custom3) << 3) + 7; - //set particle size + // set particle size if(SEGMENT.custom1 == 255) - { - PartSys->setParticleSize(0); //set global size to zero - PartSys->advPartProps[i].size = random(SEGMENT.custom1); //set each particle to random size + { + PartSys->setParticleSize(0); // set global size to zero + PartSys->advPartProps[i].size = random(SEGMENT.custom1); // set each particle to random size } else { - PartSys->setParticleSize(SEGMENT.custom1); //set global size - PartSys->advPartProps[i].size = 0; //use global size + PartSys->setParticleSize(SEGMENT.custom1); // set global size + PartSys->advPartProps[i].size = 0; // use global size } break; // emit only one particle per round } @@ -8492,7 +8489,7 @@ uint16_t mode_particlepit(void) //if (SEGMENT.call % (3 + (SEGMENT.custom2 >> 2)) == 0) //if (SEGMENT.call % (3 + (SEGMENT.speed >> 2)) == 0) - if (SEGMENT.call % 6 == 0)// (3 + max(3, (SEGMENT.speed >> 2))) == 0) //note: if friction is too low, hard particles uncontrollably 'wander' left and right if wrapX is enabled + if (SEGMENT.call % 6 == 0)// (3 + max(3, (SEGMENT.speed >> 2))) == 0) // note: if friction is too low, hard particles uncontrollably 'wander' left and right if wrapX is enabled PartSys->applyFriction(frictioncoefficient); PartSys->update(); // update and render @@ -8555,16 +8552,16 @@ uint16_t mode_particlewaterfall(void) { if (!initParticleSystem(PartSys, 12)) // init, request 12 sources, no additional data needed return mode_static(); // allocation failed; //allocation failed - PartSys->setGravity(); // enable with default gforce + PartSys->setGravity(); // enable with default gforce PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting) - PartSys->setMotionBlur(190); //anable motion blur + PartSys->setMotionBlur(190); // anable motion blur numSprays = min((uint32_t)PartSys->numSources, min(PartSys->maxXpixel/5, (uint32_t)2)); // number of sprays for (i = 0; i < numSprays; i++) { PartSys->sources[i].source.hue = random16(); PartSys->sources[i].source.collide = true; // seeded particles will collide #ifdef ESP8266 - PartSys->sources[i].maxLife = 250; // lifetime in frames (ESP8266 has less particles, make them short lived to keep the water flowing) + PartSys->sources[i].maxLife = 250; // lifetime in frames (ESP8266 has less particles, make them short lived to keep the water flowing) PartSys->sources[i].minLife = 100; #else PartSys->sources[i].maxLife = 400; // lifetime in frames @@ -8598,7 +8595,7 @@ uint16_t mode_particlewaterfall(void) for (i = 0; i < numSprays; i++) { - PartSys->sources[i].source.hue++; //change hue of spray source + PartSys->sources[i].source.hue++; // change hue of spray source } if (SEGMENT.call % (9 - (SEGMENT.intensity >> 5)) == 0 && SEGMENT.intensity > 0) // every nth frame, cycle color and emit particles, do not emit if intensity is zero @@ -8643,20 +8640,20 @@ uint16_t mode_particlebox(void) if (!initParticleSystem(PartSys, 1)) // init return mode_static(); // allocation failed PartSys->setBounceX(true); - PartSys->setBounceY(true); - //set max number of particles and save to aux1 for later + PartSys->setBounceY(true); + // set max number of particles and save to aux1 for later #ifdef ESP8266 SEGMENT.aux1 = min((uint16_t)((PartSys->maxXpixel * PartSys->maxYpixel) >> 1), PartSys->numParticles); #else - SEGMENT.aux1 = min((uint16_t)((PartSys->maxXpixel * PartSys->maxYpixel)), PartSys->numParticles); //max number of particles - #endif + SEGMENT.aux1 = min((uint16_t)((PartSys->maxXpixel * PartSys->maxYpixel)), PartSys->numParticles); // max number of particles + #endif for (i = 0; i < SEGMENT.aux1; i++) { - PartSys->particles[i].ttl = 500; //set all particles alive (not all are rendered though) - PartSys->particles[i].perpetual = true; //never die - PartSys->particles[i].hue = i * 3; // color range + PartSys->particles[i].ttl = 500; // set all particles alive (not all are rendered though) + PartSys->particles[i].perpetual = true; // never die + PartSys->particles[i].hue = i * 3; // color range PartSys->particles[i].x = map(i, 0, SEGMENT.aux1, 1, PartSys->maxX); // distribute along x according to color - PartSys->particles[i].y = random16(PartSys->maxY >> 2); //bottom quarter + PartSys->particles[i].y = random16(PartSys->maxY >> 2); // bottom quarter PartSys->particles[i].collide = true; // all particles collide } SEGMENT.aux0 = rand(); // position in perlin noise @@ -8674,7 +8671,7 @@ uint16_t mode_particlebox(void) PartSys->setWallHardness(min(SEGMENT.custom2, (uint8_t)200)); // wall hardness is 200 or more PartSys->enableParticleCollisions(true, max(2, (int)SEGMENT.custom2)); // enable collisions and set particle collision hardness - PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 10, SEGMENT.aux1)); //aux1 holds max number of particles to use + PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 10, SEGMENT.aux1)); // aux1 holds max number of particles to use if (SEGMENT.call % (((255 - SEGMENT.speed) >> 6) + 1) == 0 && SEGMENT.speed > 0) // how often the force is applied depends on speed setting @@ -8682,12 +8679,12 @@ uint16_t mode_particlebox(void) int32_t xgravity; int32_t ygravity; int32_t increment = (SEGMENT.speed >> 6) + 1; - if(SEGMENT.check2) //direction + if(SEGMENT.check2) // direction SEGMENT.aux0 += increment; // update counter else SEGMENT.aux0 -= increment; - if(SEGMENT.check1) //random, use perlin noise + if(SEGMENT.check1) // random, use perlin noise { xgravity = ((int16_t)inoise8(SEGMENT.aux0) - 127); ygravity = ((int16_t)inoise8(SEGMENT.aux0 + 10000) - 127); @@ -8695,12 +8692,12 @@ uint16_t mode_particlebox(void) xgravity = (xgravity * SEGMENT.custom1) / 128; ygravity = (ygravity * SEGMENT.custom1) / 128; } - else //go in a circle + else // go in a circle { xgravity = ((int32_t)(SEGMENT.custom1) * cos16(SEGMENT.aux0 << 8)) / 0xFFFF; ygravity = ((int32_t)(SEGMENT.custom1) * sin16(SEGMENT.aux0 << 8)) / 0xFFFF; } - if (SEGMENT.check3) //sloshing, y force is alwys downwards + if (SEGMENT.check3) // sloshing, y force is alwys downwards { if(ygravity > 0) ygravity = -ygravity; @@ -8732,8 +8729,8 @@ uint16_t mode_particleperlin(void) if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. { if (!initParticleSystem(PartSys, 1, 0, true)) // init with 1 source and advanced properties - return mode_static(); // allocation failed; //allocation failed - PartSys->setKillOutOfBounds(true); //should never happen, but lets make sure there are no stray particles + return mode_static(); // allocation failed; //allocation failed + PartSys->setKillOutOfBounds(true); // should never happen, but lets make sure there are no stray particles SEGMENT.aux0 = rand(); for (i = 0; i < PartSys->numParticles; i++) { @@ -8752,7 +8749,7 @@ uint16_t mode_particleperlin(void) PartSys->setWrapX(SEGMENT.check1); PartSys->setBounceX(!SEGMENT.check1); PartSys->setBounceY(true); - PartSys->setWallHardness(SEGMENT.custom1); //wall hardness + PartSys->setWallHardness(SEGMENT.custom1); // wall hardness PartSys->enableParticleCollisions(SEGMENT.check3, SEGMENT.custom1); // enable collisions and set particle collision hardness uint32_t displayparticles = map(SEGMENT.intensity, 0, 255, 10, PartSys->numParticles>>1); PartSys->setUsedParticles(displayparticles); @@ -8801,21 +8798,19 @@ uint16_t mode_particleimpact(void) ParticleSystem *PartSys = NULL; uint32_t i = 0; uint8_t MaxNumMeteors; - PSsettings meteorsettings;// = {0, 0, 0, 1, 0, 1, 0, 0}; // PS settings for meteors: bounceY and gravity enabled - meteorsettings.asByte = 0b00101000; - //uint8_t *settingsPtr = reinterpret_cast(&meteorsettings); // access settings as one byte (wmore efficient in code and speed) - //*settingsPtr = 0b00101000; // PS settings for meteors: bounceY and gravity enabled + PSsettings meteorsettings; + meteorsettings.asByte = 0b00101000; // PS settings for meteors: bounceY and gravity enabled 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, NUMBEROFSOURCES)) // init, no additional data needed return mode_static(); // allocation failed; //allocation failed - PartSys->setKillOutOfBounds(false); //explosions out of frame ar allowed, set to true to save particles (TODO: better enable it in ESP8266?) - PartSys->setGravity(); //enable default gravity - PartSys->setBounceY(true); //always use ground bounce + PartSys->setKillOutOfBounds(false); // explosions out of frame ar allowed, set to true to save particles (TODO: better enable it in ESP8266?) + PartSys->setGravity(); // enable default gravity + PartSys->setBounceY(true); // always use ground bounce MaxNumMeteors = min(PartSys->numSources, (uint8_t)NUMBEROFSOURCES); for (i = 0; i < MaxNumMeteors; i++) - { + { PartSys->sources[i].source.y = 500; PartSys->sources[i].source.ttl = random16(20 * i); // set initial delay for meteors PartSys->sources[i].source.vy = 10; // at positive speeds, no particles are emitted and if particle dies, it will be relaunched @@ -8876,7 +8871,7 @@ uint16_t mode_particleimpact(void) { if (PartSys->sources[i].source.ttl) { - PartSys->sources[i].source.ttl--; //note: this saves an if statement, but moving down particles age twice + PartSys->sources[i].source.ttl--; // note: this saves an if statement, but moving down particles age twice if (PartSys->sources[i].source.vy < 0) //move down { PartSys->applyGravity(&PartSys->sources[i].source); @@ -8884,10 +8879,9 @@ uint16_t mode_particleimpact(void) // if source reaches the bottom, set speed to 0 so it will explode on next function call (handled above) if (PartSys->sources[i].source.y < PS_P_RADIUS<<1) // reached the bottom pixel on its way down - { + { PartSys->sources[i].source.vy = 0; // set speed zero so it will explode PartSys->sources[i].source.vx = 0; - //PartSys->sources[i].source.y = 5; // offset from ground so explosion happens not out of frame (!!!TODO: still needed? the class takes care of that) PartSys->sources[i].source.collide = true; #ifdef ESP8266 PartSys->sources[i].maxLife = 130; @@ -8901,7 +8895,7 @@ uint16_t mode_particleimpact(void) PartSys->sources[i].vy = (SEGMENT.custom1 >> 2); // emitting speed y PartSys->sources[i].var = (SEGMENT.custom1 >> 2); // speed variation around vx,vy (+/- var) } - } + } } else if (PartSys->sources[i].source.vy > 0) // meteor is exploded and time is up (ttl==0 and positive speed), relaunch it { @@ -8914,13 +8908,13 @@ uint16_t mode_particleimpact(void) PartSys->sources[i].source.ttl = 500; // long life, will explode at bottom PartSys->sources[i].source.collide = false; // trail particles will not collide PartSys->sources[i].maxLife = 60; // spark particle life - PartSys->sources[i].minLife = 20; + PartSys->sources[i].minLife = 20; PartSys->sources[i].vy = -9; // emitting speed (down) - PartSys->sources[i].var = 3; // speed variation around vx,vy (+/- var) + PartSys->sources[i].var = 3; // speed variation around vx,vy (+/- var) } } - PartSys->update(); // update and render + PartSys->update(); // update and render return FRAMETIME; } #undef NUMBEROFSOURCES @@ -8937,22 +8931,22 @@ uint16_t mode_particleattractor(void) { if (SEGLEN == 1) return mode_static(); - ParticleSystem *PartSys = NULL; + ParticleSystem *PartSys = NULL; uint32_t i = 0; - PSsettings sourcesettings;// = {0, 0, 1, 1, 0, 0, 0, 0}; // PS settings for bounceY, bounceY used for source movement (it always bounces whereas particles do not) - sourcesettings.asByte = 0b00001100; - PSparticle *attractor; //particle pointer to the attractor - if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. + PSsettings sourcesettings; + sourcesettings.asByte = 0b00001100; // PS settings for bounceY, bounceY used for source movement (it always bounces whereas particles do not) + PSparticle *attractor; // particle pointer to the attractor + if (SEGMENT.call == 0) // initialization { if (!initParticleSystem(PartSys, 1, 0, true)) // init using 1 source and advanced particle settings - return mode_static(); // allocation failed; //allocation failed + return mode_static(); // allocation failed //DEBUG_PRINTF_P(PSTR("sources in FX %p\n"), &PartSys->sources[0]); - PartSys->sources[0].source.hue = random16(); - PartSys->sources[0].source.vx = -7; //will collied with wall and get random bounce direction - PartSys->sources[0].source.collide = true; // seeded particles will collide + PartSys->sources[0].source.hue = random16(); + PartSys->sources[0].source.vx = -7; // will collied with wall and get random bounce direction + PartSys->sources[0].source.collide = true; // seeded particles will collide PartSys->sources[0].source.perpetual = true; //source does not age #ifdef ESP8266 - PartSys->sources[0].maxLife = 200; // lifetime in frames (ESP8266 has less particles) + PartSys->sources[0].maxLife = 200; // lifetime in frames (ESP8266 has less particles) PartSys->sources[0].minLife = 30; #else PartSys->sources[0].maxLife = 350; // lifetime in frames @@ -8968,7 +8962,7 @@ uint16_t mode_particleattractor(void) if (PartSys == NULL) { DEBUG_PRINT(F("ERROR: FX PartSys nullpointer")); - return mode_static(); // something went wrong, no data! (TODO: ask how to handle this so it always works) + return mode_static(); // something went wrong, no data! } // Particle System settings PartSys->updateSystem(); // update system properties (dimensions and data pointers) @@ -8980,11 +8974,10 @@ uint16_t mode_particleattractor(void) PartSys->enableParticleCollisions(true, map(SEGMENT.custom2, 1, 255, 120, 255)); // enable collisions and set particle collision hardness else PartSys->enableParticleCollisions(false); - uint16_t lastusedparticle = (PartSys->numParticles * 3) >> 2; //use 3/4 of particles + uint16_t lastusedparticle = (PartSys->numParticles * 3) >> 2; // use 3/4 of particles uint32_t displayparticles = map(SEGMENT.intensity, 0, 255, 10, lastusedparticle); PartSys->setUsedParticles(displayparticles); // set pointers - //attractor = reinterpret_cast(&PartSys->particles[lastusedparticle + 1]); attractor = &PartSys->particles[lastusedparticle + 1]; if(SEGMENT.call == 0) { @@ -8993,7 +8986,7 @@ uint16_t mode_particleattractor(void) attractor->ttl = 100; attractor->perpetual = true; } - //set attractor properties + // set attractor properties if (SEGMENT.check2) { if((SEGMENT.call % 3) == 0) // move slowly @@ -9007,17 +9000,15 @@ uint16_t mode_particleattractor(void) if (SEGMENT.call % 5 == 0) PartSys->sources[0].source.hue++; - SEGMENT.aux0 += 256; //emitting angle, one full turn in 255 frames (0xFFFF is 360°) + SEGMENT.aux0 += 256; // emitting angle, one full turn in 255 frames (0xFFFF is 360°) if (SEGMENT.call % 2 == 0) // alternate direction of emit - PartSys->angleEmit(PartSys->sources[0], SEGMENT.aux0, 12); - //PartSys->angleEmit(PartSys->sources[0], SEGMENT.aux0, SEGMENT.custom1 >> 4); + PartSys->angleEmit(PartSys->sources[0], SEGMENT.aux0, 12); else PartSys->angleEmit(PartSys->sources[0], SEGMENT.aux0 + 0x7FFF, 12); // emit at 180° as well - //PartSys->angleEmit(PartSys->sources[0], SEGMENT.aux0 + 0x7FFF, SEGMENT.custom1 >> 4); // apply force for(i = 0; i < displayparticles; i++) { - PartSys->pointAttractor(i, attractor, SEGMENT.speed, SEGMENT.check3); //TODO: there was a bug here, counters was always the same pointer, check if this works. + PartSys->pointAttractor(i, attractor, SEGMENT.speed, SEGMENT.check3); } if (SEGMENT.call % (33 - SEGMENT.custom3) == 0) PartSys->applyFriction(2); @@ -9029,8 +9020,7 @@ static const char _data_FX_MODE_PARTICLEATTRACTOR[] PROGMEM = "PS Attractor@Mass /* -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 +Particle Line Attractor, an idea that is not finished and not working Uses palette for particle color by DedeHai (Damian Schneider) */ @@ -9144,6 +9134,8 @@ uint16_t mode_particleattractor(void) } static const char _data_FX_MODE_PARTICLEATTRACTOR[] PROGMEM = "PS Attractor@Mass,Particles,Emit Speed,Collisions,Friction,Color by Age,Move,Swallow;;!;2;pal=9,sx=100,ix=82,c1=190,c2=0,o1=0,o2=0,o3=0"; */ + + /* Particle Spray, just a particle spray with many parameters Uses palette for particle color @@ -9156,7 +9148,7 @@ uint16_t mode_particlespray(void) return mode_static(); ParticleSystem *PartSys = NULL; //uint8_t numSprays; - const uint8_t hardness = 200; //collision hardness is fixed + const uint8_t hardness = 200; // collision hardness is fixed if (SEGMENT.call == 0) // initialization { @@ -9186,7 +9178,7 @@ uint16_t mode_particlespray(void) PartSys->setBounceX(!SEGMENT.check2); PartSys->setWrapX(SEGMENT.check2); PartSys->setWallHardness(hardness); - PartSys->setGravity(8 * SEGMENT.check1); //enable gravity if checked (8 is default strength) + PartSys->setGravity(8 * SEGMENT.check1); // enable gravity if checked (8 is default strength) //numSprays = min(PartSys->numSources, (uint8_t)1); // number of sprays if (SEGMENT.check3) // collisions enabled @@ -9196,12 +9188,12 @@ uint16_t mode_particlespray(void) // change source properties if (SEGMENT.call % (11 - (SEGMENT.intensity / 25)) == 0) // every nth frame, cycle color and emit particles - { + { PartSys->sources[0].source.hue++; // = random16(); //change hue of spray source - // PartSys->sources[i].var = SEGMENT.custom3; // emiting variation = nozzle size (custom 3 goes from 0-32) + // PartSys->sources[i].var = SEGMENT.custom3; // emiting variation = nozzle size (custom 3 goes from 0-32) PartSys->sources[0].source.x = map(SEGMENT.custom1, 0, 255, 0, PartSys->maxX); - PartSys->sources[0].source.y = map(SEGMENT.custom2, 0, 255, 0, PartSys->maxY); - // spray[j].source.hue = random16(); //set random color for each particle (using palette) + PartSys->sources[0].source.y = map(SEGMENT.custom2, 0, 255, 0, PartSys->maxY); + // spray[j].source.hue = random16(); //set random color for each particle (using palette) PartSys->angleEmit(PartSys->sources[0], (256 - (((int32_t)SEGMENT.custom3 + 1) << 3)) << 8, SEGMENT.speed >> 2); } @@ -9227,8 +9219,8 @@ uint16_t mode_particleGEQ(void) if (SEGMENT.call == 0) // initialization { if (!initParticleSystem(PartSys, 1)) // init - return mode_static(); // allocation failed; //allocation failed - PartSys->setKillOutOfBounds(true); + return mode_static(); // allocation failed + PartSys->setKillOutOfBounds(true); PartSys->setUsedParticles((PartSys->numParticles * 3) / 2); // use 2/3 of available particles } else @@ -9241,7 +9233,7 @@ uint16_t mode_particleGEQ(void) } uint32_t i; - // set particle system properties + // set particle system properties PartSys->updateSystem(); // update system properties (dimensions and data pointers) PartSys->setWrapX(SEGMENT.check1); PartSys->setBounceX(SEGMENT.check2); @@ -9260,9 +9252,6 @@ uint16_t mode_particleGEQ(void) uint8_t *fftResult = (uint8_t *)um_data->u_data[2]; // 16 bins with FFT data, log mapped already, each band contains frequency amplitude 0-255 //map the bands into 16 positions on x axis, emit some particles according to frequency loudness - //Idea: emit 20 particles at full loudness, can use a shift for that, for example shift by 4 or 5 - //in order to also emit particles for not so loud bands, get a bunch of particles based on frame counter and current loudness? - //implement it simply first, then add complexity... need to check what looks good i = 0; uint32_t bin; //current bin uint32_t binwidth = (PartSys->maxX + 1)>>4; //emit poisition variation for one bin (+/-) is equal to width/16 (for 16 bins) @@ -9292,20 +9281,20 @@ uint16_t mode_particleGEQ(void) { if (PartSys->particles[i].ttl == 0) // find a dead particle { - //set particle properties TODO: could also use the spray... - PartSys->particles[i].ttl = 20 + map(SEGMENT.intensity, 0,255, emitspeed>>1, emitspeed + random16(emitspeed)) ; // set particle alive, particle lifespan is in number of frames - PartSys->particles[i].x = xposition + random16(binwidth) - (binwidth>>1); //position randomly, deviating half a bin width - PartSys->particles[i].y = PS_P_RADIUS; //tart at the bottom (PS_P_RADIUS is minimum position a particle is fully in frame) + //set particle properties TODO: could also use the spray... + PartSys->particles[i].ttl = 20 + map(SEGMENT.intensity, 0,255, emitspeed>>1, emitspeed + random16(emitspeed)) ; // set particle alive, particle lifespan is in number of frames + PartSys->particles[i].x = xposition + random16(binwidth) - (binwidth>>1); // position randomly, deviating half a bin width + PartSys->particles[i].y = PS_P_RADIUS; // start at the bottom (PS_P_RADIUS is minimum position a particle is fully in frame) PartSys->particles[i].vx = random16(SEGMENT.custom1>>1)-(SEGMENT.custom1>>2) ; //x-speed variation: +/- custom1/4 TODO: ok to use random16 here? PartSys->particles[i].vy = emitspeed; - PartSys->particles[i].hue = (bin<<4) + random16(17) - 8; // color from palette according to bin + PartSys->particles[i].hue = (bin<<4) + random16(17) - 8; // color from palette according to bin emitparticles--; } i++; } } - PartSys->update(); // update and render + PartSys->update(); // update and render return FRAMETIME; } static const char _data_FX_MODE_PARTICLEGEQ[] PROGMEM = "PS Equalizer@Speed,Intensity,Diverge,Bounce,Gravity,Cylinder,Walls,Floor;;!;2f;pal=0,sx=155,ix=200,c1=0,c2=128,o1=0,o2=0,o3=0"; @@ -9330,8 +9319,8 @@ uint16_t mode_particleghostrider(void) PartSys->sources[0].maxLife = 260; // lifetime in frames PartSys->sources[0].minLife = 250; PartSys->sources[0].source.x = random16(PartSys->maxX); - PartSys->sources[0].source.y = random16(PartSys->maxY); - SEGMENT.step = random(MAXANGLESTEP) - (MAXANGLESTEP>>1); //angle increment + PartSys->sources[0].source.y = random16(PartSys->maxY); + SEGMENT.step = random(MAXANGLESTEP) - (MAXANGLESTEP>>1); // angle increment } else PartSys = reinterpret_cast(SEGMENT.data); // if not first call, just set the pointer to the PS @@ -9342,7 +9331,7 @@ uint16_t mode_particleghostrider(void) return mode_static(); // something went wrong, no data! } - if(SEGMENT.intensity > 0) //spiraling + if(SEGMENT.intensity > 0) // spiraling { if(SEGMENT.aux1) { @@ -9362,7 +9351,7 @@ uint16_t mode_particleghostrider(void) PartSys->setMotionBlur(SEGMENT.custom1); PartSys->sources[0].var = SEGMENT.custom3 >> 1; - //color by age (PS color by age always starts with hue = 255 so cannot use that) + // color by age (PS 'color by age' always starts with hue = 255, don't want that here) if(SEGMENT.check1) { for(int i = 0; i < PartSys->usedParticles; i++) @@ -9371,25 +9360,25 @@ uint16_t mode_particleghostrider(void) } } - //enable/disable walls + // enable/disable walls ghostsettings.bounceX = SEGMENT.check2; ghostsettings.bounceY = SEGMENT.check2; - SEGMENT.aux0 += (int32_t)SEGMENT.step; //step is angle increment - uint16_t emitangle = SEGMENT.aux0 + 32767; //+180° + SEGMENT.aux0 += (int32_t)SEGMENT.step; // step is angle increment + uint16_t emitangle = SEGMENT.aux0 + 32767; // +180° int32_t speed = map(SEGMENT.speed, 0, 255, 12, 64); int8_t newvx = ((int32_t)cos16(SEGMENT.aux0) * speed) / (int32_t)32767; int8_t newvy = ((int32_t)sin16(SEGMENT.aux0) * speed) / (int32_t)32767; PartSys->sources[0].source.vx = ((int32_t)cos16(SEGMENT.aux0) * speed) / (int32_t)32767; - PartSys->sources[0].source.vy = ((int32_t)sin16(SEGMENT.aux0) * speed) / (int32_t)32767; - PartSys->sources[0].source.ttl = 500; //source never dies (note: setting 'perpetual' is not needed if replenished each frame) + PartSys->sources[0].source.vy = ((int32_t)sin16(SEGMENT.aux0) * speed) / (int32_t)32767; + PartSys->sources[0].source.ttl = 500; // source never dies (note: setting 'perpetual' is not needed if replenished each frame) PartSys->particleMoveUpdate(PartSys->sources[0].source, &ghostsettings); - //set head (steal one of the particles) + // set head (steal one of the particles) PartSys->particles[PartSys->usedParticles-1].x = PartSys->sources[0].source.x; PartSys->particles[PartSys->usedParticles-1].y = PartSys->sources[0].source.y; PartSys->particles[PartSys->usedParticles-1].ttl = 255; - PartSys->particles[PartSys->usedParticles-1].sat = 0; - //emit two particles + PartSys->particles[PartSys->usedParticles-1].sat = 0; //white + // emit two particles PartSys->angleEmit(PartSys->sources[0], emitangle, speed); PartSys->angleEmit(PartSys->sources[0], emitangle, speed); PartSys->sources[0].source.hue += SEGMENT.custom2 >> 3; @@ -9401,7 +9390,7 @@ static const char _data_FX_MODE_PARTICLEGHOSTRIDER[] PROGMEM = "PS Ghost Rider@S /* -PS Blobs: large particles bouncing around +PS Blobs: large particles bouncing around, changing size and form Uses palette for particle color by DedeHai (Damian Schneider) */ @@ -9414,10 +9403,9 @@ uint16_t mode_particleblobs(void) if (SEGMENT.call == 0) { if (!initParticleSystem(PartSys, 1, 0, true, true)) //init, request one source, no additional bytes, advanced size & size control (actually dont really need one TODO: test if using zero sources also works) - return mode_static(); // allocation failed; //allocation failed - //PartSys->setGravity(); //enable with default gravity + return mode_static(); // allocation failed PartSys->setBounceX(true); - PartSys->setBounceY(true); + PartSys->setBounceY(true); PartSys->setWallHardness(255); PartSys->setWallRoughness(255); PartSys->setCollisionHardness(255); @@ -9434,37 +9422,37 @@ uint16_t mode_particleblobs(void) PartSys->updateSystem(); // update system properties (dimensions and data pointers) PartSys->setUsedParticles(min(PartSys->numParticles, (uint16_t)map(SEGMENT.intensity,0 ,255, 1, (PartSys->maxXpixel * PartSys->maxYpixel)>>4))); PartSys->enableParticleCollisions(SEGMENT.check2); - + for (uint32_t i = 0; i < PartSys->usedParticles; i++) // update particles { - if(SEGMENT.aux0 != SEGMENT.speed || PartSys->particles[i].ttl == 0) //speed changed or dead - { + if(SEGMENT.aux0 != SEGMENT.speed || PartSys->particles[i].ttl == 0) // speed changed or dead + { PartSys->particles[i].vx = (int16_t)random(-(SEGMENT.speed >> 2), SEGMENT.speed >> 2); PartSys->particles[i].vy = (int16_t)random(-(SEGMENT.speed >> 2), SEGMENT.speed >> 2); } - if(SEGMENT.aux1 != SEGMENT.custom1 || PartSys->particles[i].ttl == 0) //size changed or dead - PartSys->advPartSize[i].maxsize = 60 + (SEGMENT.custom1 >> 1) + random((SEGMENT.custom1 >> 2)); //set each particle to slightly randomized size + if(SEGMENT.aux1 != SEGMENT.custom1 || PartSys->particles[i].ttl == 0) // size changed or dead + PartSys->advPartSize[i].maxsize = 60 + (SEGMENT.custom1 >> 1) + random((SEGMENT.custom1 >> 2)); // set each particle to slightly randomized size - //PartSys->particles[i].perpetual = SEGMENT.check2; //infinite life if set + //PartSys->particles[i].perpetual = SEGMENT.check2; //infinite life if set if (PartSys->particles[i].ttl == 0) // find dead particle, renitialize - { + { PartSys->particles[i].ttl = 300 + random16(((uint16_t)SEGMENT.custom2 << 3) + 100); - PartSys->particles[i].x = random(PartSys->maxX); - PartSys->particles[i].y = random16(PartSys->maxY); - PartSys->particles[i].hue = random16(); // set random color - PartSys->particles[i].collide = true; //enable collision for particle - PartSys->advPartProps[i].size = 0; //start out small + PartSys->particles[i].x = random(PartSys->maxX); + PartSys->particles[i].y = random16(PartSys->maxY); + PartSys->particles[i].hue = random16(); // set random color + PartSys->particles[i].collide = true; // enable collision for particle + PartSys->advPartProps[i].size = 0; // start out small PartSys->advPartSize[i].asymmetry = random16(220); PartSys->advPartSize[i].asymdir = random16(255); - //set advanced size control properties + // set advanced size control properties PartSys->advPartSize[i].grow = true; PartSys->advPartSize[i].growspeed = 1 + random16(9); - PartSys->advPartSize[i].shrinkspeed = 1 + random16(9); - PartSys->advPartSize[i].wobblespeed = 1 + random(3); - } + PartSys->advPartSize[i].shrinkspeed = 1 + random16(9); + PartSys->advPartSize[i].wobblespeed = 1 + random(3); + } //PartSys->advPartSize[i].asymmetry++; - PartSys->advPartSize[i].pulsate = SEGMENT.check3; - PartSys->advPartSize[i].wobble = SEGMENT.check1; + PartSys->advPartSize[i].pulsate = SEGMENT.check3; + PartSys->advPartSize[i].wobble = SEGMENT.check1; } SEGMENT.aux0 = SEGMENT.speed; //write state back SEGMENT.aux1 = SEGMENT.custom1; @@ -9478,7 +9466,7 @@ static const char _data_FX_MODE_PARTICLEBLOBS[] PROGMEM = "PS Blobs@Speed,Blobs, /* - * Particle rotating GEQ + * Particle rotating GEQ (unfinished, basically works but needs more fine-tuning) * Particles sprayed from center with a rotating spray * Uses palette for particle color * by DedeHai (Damian Schneider) @@ -9528,8 +9516,8 @@ uint16_t mode_particlecenterGEQ(void) } for (i = 0; i < numSprays; i++) { - PartSys->sources[i].source.hue = i*16; //even color distribution - PartSys->sources[i].source.sat = 255; // set saturation + PartSys->sources[i].source.hue = i*16; // even color distribution + PartSys->sources[i].source.sat = 255; // set saturation PartSys->sources[i].source.x = (cols * PS_P_RADIUS) / 2; // center PartSys->sources[i].source.y = (rows * PS_P_RADIUS) / 2; // center PartSys->sources[i].source.vx = 0; diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index 3a8b4cf7ae..b7a132e139 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -6,7 +6,7 @@ LICENSE The MIT License (MIT) - Copyright (c) 2024 Damian Schneider + Copyright (c) 2024 Damian Schneider Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights @@ -22,13 +22,12 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ /* - Note on ESP32: using 32bit integer is faster than 16bit or 8bit, each operation takes on less instruction, can be testen on https://godbolt.org/ - it does not matter if using int, unsigned int, uint32_t or int32_t, the compiler will make int into 32bit - this should be used to optimize speed but not if memory is affected much + Note on ESP32: using 32bit integer is faster than 16bit or 8bit, each operation takes on less instruction, can be testen on https://godbolt.org/ + it does not matter if using int, unsigned int, uint32_t or int32_t, the compiler will make int into 32bit + this should be used to optimize speed but not if memory is affected much */ /* @@ -45,412 +44,412 @@ ParticleSystem::ParticleSystem(uint16_t width, uint16_t height, uint16_t numberofparticles, uint16_t numberofsources, bool isadvanced, bool sizecontrol) { - //Serial.println("PS Constructor"); - numSources = numberofsources; - numParticles = numberofparticles; // set number of particles in the array - usedParticles = numberofparticles; // use all particles by default - advPartProps = NULL; //make sure we start out with null pointers (just in case memory was not cleared) - advPartSize = NULL; - updatePSpointers(isadvanced, sizecontrol); // set the particle and sources pointer (call this before accessing sprays or particles) - setMatrixSize(width, height); - setWallHardness(255); // set default wall hardness to max - setWallRoughness(0); // smooth walls by default - setGravity(0); //gravity disabled by default - setParticleSize(0); // minimum size by default - motionBlur = 0; //no fading by default - emitIndex = 0; - - //initialize some default non-zero values most FX use - for (int i = 0; i < numSources; i++) - { - sources[i].source.sat = 255; //set saturation to max by default - sources[i].source.ttl = 1; //set source alive - } - for (int i = 0; i < numParticles; i++) - { - particles[i].sat = 255; // full saturation - } - //Serial.println("PS Constructor done"); + //Serial.println("PS Constructor"); + numSources = numberofsources; + numParticles = numberofparticles; // set number of particles in the array + usedParticles = numberofparticles; // use all particles by default + advPartProps = NULL; //make sure we start out with null pointers (just in case memory was not cleared) + advPartSize = NULL; + updatePSpointers(isadvanced, sizecontrol); // set the particle and sources pointer (call this before accessing sprays or particles) + setMatrixSize(width, height); + setWallHardness(255); // set default wall hardness to max + setWallRoughness(0); // smooth walls by default + setGravity(0); //gravity disabled by default + setParticleSize(0); // minimum size by default + motionBlur = 0; //no fading by default + emitIndex = 0; + + //initialize some default non-zero values most FX use + for (int i = 0; i < numSources; i++) + { + sources[i].source.sat = 255; //set saturation to max by default + sources[i].source.ttl = 1; //set source alive + } + for (int i = 0; i < numParticles; i++) + { + particles[i].sat = 255; // full saturation + } + //Serial.println("PS Constructor done"); } // update function applies gravity, moves the particles, handles collisions and renders the particles void ParticleSystem::update(void) { - PSadvancedParticle *advprop = NULL; - //apply gravity globally if enabled - if (particlesettings.useGravity) - applyGravity(); - - //update size settings before handling collisions - if(advPartSize) - { - for (int i = 0; i < usedParticles; i++) - { - updateSize(&advPartProps[i], &advPartSize[i]); - } - } - - // handle collisions (can push particles, must be done before updating particles or they can render out of bounds, causing a crash if using local buffer for speed) - if (particlesettings.useCollisions) - handleCollisions(); - - //move all particles - for (int i = 0; i < usedParticles; i++) - { - if(advPartProps) - { - advprop = &advPartProps[i]; - } - particleMoveUpdate(particles[i], &particlesettings, advprop); - } - - /*TODO remove this - Serial.print("alive particles: "); - uint32_t aliveparticles = 0; - for (int i = 0; i < numParticles; i++) - { - if(particles[i].ttl) - aliveparticles++; - } - Serial.println(aliveparticles); - */ - ParticleSys_render(); + PSadvancedParticle *advprop = NULL; + //apply gravity globally if enabled + if (particlesettings.useGravity) + applyGravity(); + + //update size settings before handling collisions + if (advPartSize) + { + for (int i = 0; i < usedParticles; i++) + { + updateSize(&advPartProps[i], &advPartSize[i]); + } + } + + // handle collisions (can push particles, must be done before updating particles or they can render out of bounds, causing a crash if using local buffer for speed) + if (particlesettings.useCollisions) + handleCollisions(); + + //move all particles + for (int i = 0; i < usedParticles; i++) + { + if (advPartProps) + { + advprop = &advPartProps[i]; + } + particleMoveUpdate(particles[i], &particlesettings, advprop); + } + + /*TODO remove this + Serial.print("alive particles: "); + uint32_t aliveparticles = 0; + for (int i = 0; i < numParticles; i++) + { + if (particles[i].ttl) + aliveparticles++; + } + Serial.println(aliveparticles); + */ + ParticleSys_render(); } // update function for fire animation void ParticleSystem::updateFire(uint32_t intensity, bool renderonly) { - if(!renderonly) - fireParticleupdate(); - ParticleSys_render(true, intensity); + if (!renderonly) + fireParticleupdate(); + ParticleSys_render(true, intensity); } void ParticleSystem::setUsedParticles(uint16_t num) { - usedParticles = min(num, numParticles); //limit to max particles + usedParticles = min(num, numParticles); //limit to max particles } void ParticleSystem::setWallHardness(uint8_t hardness) { - wallHardness = hardness; + wallHardness = hardness; } void ParticleSystem::setWallRoughness(uint8_t roughness) { - wallRoughness = roughness; + wallRoughness = roughness; } void ParticleSystem::setCollisionHardness(uint8_t hardness) -{ - collisionHardness = hardness; +{ + collisionHardness = hardness; } void ParticleSystem::setMatrixSize(uint16_t x, uint16_t y) { - maxXpixel = x - 1; // last physical pixel that can be drawn to - maxYpixel = y - 1; - maxX = x * PS_P_RADIUS - 1; // particle system boundary for movements - maxY = y * PS_P_RADIUS - 1; // this value is often needed (also by FX) to calculate positions + maxXpixel = x - 1; // last physical pixel that can be drawn to + maxYpixel = y - 1; + maxX = x * PS_P_RADIUS - 1; // particle system boundary for movements + maxY = y * PS_P_RADIUS - 1; // this value is often needed (also by FX) to calculate positions } void ParticleSystem::setWrapX(bool enable) { - particlesettings.wrapX = enable; + particlesettings.wrapX = enable; } void ParticleSystem::setWrapY(bool enable) { - particlesettings.wrapY = enable; + particlesettings.wrapY = enable; } void ParticleSystem::setBounceX(bool enable) { - particlesettings.bounceX = enable; + particlesettings.bounceX = enable; } void ParticleSystem::setBounceY(bool enable) { - particlesettings.bounceY = enable; + particlesettings.bounceY = enable; } void ParticleSystem::setKillOutOfBounds(bool enable) { - particlesettings.killoutofbounds = enable; + particlesettings.killoutofbounds = enable; } void ParticleSystem::setColorByAge(bool enable) { - particlesettings.colorByAge = enable; + particlesettings.colorByAge = enable; } void ParticleSystem::setMotionBlur(uint8_t bluramount) { - if(particlesize == 0) // only allwo motion blurring on default particle size or advanced size(cannot combine motion blur with normal blurring used for particlesize, would require another buffer) - motionBlur = bluramount; + if (particlesize == 0) // only allwo motion blurring on default particle size or advanced size(cannot combine motion blur with normal blurring used for particlesize, would require another buffer) + motionBlur = bluramount; } // render size using smearing (see blur function) void ParticleSystem::setParticleSize(uint8_t size) { - particlesize = size; - particleHardRadius = PS_P_MINHARDRADIUS + particlesize; // note: this sets size if not using advanced props - motionBlur = 0; // disable motion blur if particle size is set + particlesize = size; + particleHardRadius = PS_P_MINHARDRADIUS + particlesize; // note: this sets size if not using advanced props + motionBlur = 0; // disable motion blur if particle size is set } // enable/disable gravity, optionally, set the force (force=8 is default) can be 1-255, 0 is disable // if enabled, gravity is applied to all particles in ParticleSystemUpdate() // 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::setGravity(int8_t force) -{ - if (force) - { - gforce = force; - particlesettings.useGravity = true; - } - else - particlesettings.useGravity = false; +{ + if (force) + { + gforce = force; + particlesettings.useGravity = true; + } + 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; + particlesettings.useCollisions = enable; + collisionHardness = hardness + 1; } // emit one particle with variation, returns index of emitted particle (or -1 if no particle emitted) int32_t ParticleSystem::sprayEmit(PSsource &emitter) { - for (int32_t i = 0; i < usedParticles; i++) - { - emitIndex++; - if (emitIndex >= usedParticles) - emitIndex = 0; - if (particles[emitIndex].ttl == 0) // find a dead particle - { - particles[emitIndex].vx = emitter.vx + random(-emitter.var, emitter.var); - particles[emitIndex].vy = emitter.vy + random(-emitter.var, emitter.var); - particles[emitIndex].x = emitter.source.x; - particles[emitIndex].y = emitter.source.y; - particles[emitIndex].hue = emitter.source.hue; - particles[emitIndex].sat = emitter.source.sat; - particles[emitIndex].collide = emitter.source.collide; - particles[emitIndex].ttl = random(emitter.minLife, emitter.maxLife); - if (advPartProps) - advPartProps[emitIndex].size = emitter.size; - return i; - } - } - return -1; + for (int32_t i = 0; i < usedParticles; i++) + { + emitIndex++; + if (emitIndex >= usedParticles) + emitIndex = 0; + if (particles[emitIndex].ttl == 0) // find a dead particle + { + particles[emitIndex].vx = emitter.vx + random(-emitter.var, emitter.var); + particles[emitIndex].vy = emitter.vy + random(-emitter.var, emitter.var); + particles[emitIndex].x = emitter.source.x; + particles[emitIndex].y = emitter.source.y; + particles[emitIndex].hue = emitter.source.hue; + particles[emitIndex].sat = emitter.source.sat; + particles[emitIndex].collide = emitter.source.collide; + particles[emitIndex].ttl = random(emitter.minLife, emitter.maxLife); + if (advPartProps) + advPartProps[emitIndex].size = emitter.size; + return i; + } + } + return -1; } // 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<<1) - PS_P_RADIUS; // jitter the flame by one pixel to make the flames wider at the base - particles[emitIndex].y = emitter.source.y; - particles[emitIndex].vx = emitter.vx + random16(emitter.var) - (emitter.var >> 1); // random16 is good enough for fire and much faster - particles[emitIndex].vy = emitter.vy + random16(emitter.var) - (emitter.var >> 1); - particles[emitIndex].ttl = random(emitter.minLife, emitter.maxLife) + emitter.source.ttl; - // fire uses ttl and not hue for heat, so no need to set the hue - break; // done - } - } - /* - // note: this attemt to save on code size turns out to be much slower as fire uses a lot of particle emits, this must be efficient. also emitter.var would need adjustment - uint32_t partidx = sprayEmit(emitter); //emit one particle - // adjust properties - particles[partidx].x += random16(PS_P_RADIUS<<1) - PS_P_RADIUS; // jitter the flame by one pixel to make the flames wider at the base - particles[partidx].ttl += emitter.source.ttl; // flame intensity dies down with emitter TTL - */ + 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<<1) - PS_P_RADIUS; // jitter the flame by one pixel to make the flames wider at the base + particles[emitIndex].y = emitter.source.y; + particles[emitIndex].vx = emitter.vx + random16(emitter.var) - (emitter.var >> 1); // random16 is good enough for fire and much faster + particles[emitIndex].vy = emitter.vy + random16(emitter.var) - (emitter.var >> 1); + particles[emitIndex].ttl = random(emitter.minLife, emitter.maxLife) + emitter.source.ttl; + // fire uses ttl and not hue for heat, so no need to set the hue + break; // done + } + } + /* + // note: this attemt to save on code size turns out to be much slower as fire uses a lot of particle emits, this must be efficient. also emitter.var would need adjustment + uint32_t partidx = sprayEmit(emitter); //emit one particle + // adjust properties + particles[partidx].x += random16(PS_P_RADIUS<<1) - PS_P_RADIUS; // jitter the flame by one pixel to make the flames wider at the base + particles[partidx].ttl += emitter.source.ttl; // flame intensity dies down with emitter TTL + */ } // Emits a particle at given angle and speed, angle is from 0-65535 (=0-360deg), speed is also affected by emitter->var // angle = 0 means in positive x-direction (i.e. to the right) void ParticleSystem::angleEmit(PSsource &emitter, uint16_t angle, int8_t speed) { - emitter.vx = ((int32_t)cos16(angle) * (int32_t)speed) / (int32_t)32600; // cos16() and sin16() return signed 16bit, division should be 32767 but 32600 gives slightly better rounding - emitter.vy = ((int32_t)sin16(angle) * (int32_t)speed) / (int32_t)32600; // note: cannot use bit shifts as bit shifting is asymmetrical for positive and negative numbers and this needs to be accurate! - sprayEmit(emitter); + emitter.vx = ((int32_t)cos16(angle) * (int32_t)speed) / (int32_t)32600; // cos16() and sin16() return signed 16bit, division should be 32767 but 32600 gives slightly better rounding + emitter.vy = ((int32_t)sin16(angle) * (int32_t)speed) / (int32_t)32600; // note: cannot use bit shifts as bit shifting is asymmetrical for positive and negative numbers and this needs to be accurate! + 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 and killoutofbounds is not applied over the top void ParticleSystem::particleMoveUpdate(PSparticle &part, PSsettings *options, PSadvancedParticle *advancedproperties) { - if(options == NULL) - options = &particlesettings; //use PS system settings by default - if (part.ttl > 0) - { - if(!part.perpetual) - part.ttl--; // age - if (particlesettings.colorByAge) - part.hue = part.ttl > 255 ? 255 : part.ttl; //set color to ttl - - bool usesize = false; // particle uses individual size rendering - int32_t newX = part.x + (int16_t)part.vx; - int32_t 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(advancedproperties) //using individual particle size? - { - if(advancedproperties->size > 0) - usesize = true; // note: variable eases out of frame checking below - particleHardRadius = max(PS_P_MINHARDRADIUS, (int)particlesize + (advancedproperties->size)); - } - // if wall collisions are enabled, bounce them before they reach the edge, it looks much nicer if the particle is not half out of view - if (options->bounceX) - { - if ((newX < particleHardRadius) || (newX > maxX - particleHardRadius)) // reached a wall - bounce(part.vx, part.vy, newX, maxX); - } - - if ((newX < 0) || (newX > maxX)) // check if particle reached an edge (note: this also checks out of bounds and must not be skipped, even if bounce is enabled) - { - if (options->wrapX) - { - newX = (uint16_t)newX % (maxX + 1); - } - else if (((newX <= -PS_P_HALFRADIUS) || (newX > maxX + PS_P_HALFRADIUS))) // particle is leaving, set out of bounds if it has fully left - { - bool isleaving = true; - if(usesize) // using individual particle size - { - if (((newX > -particleHardRadius) && (newX < maxX + particleHardRadius))) // large particle is not yet leaving the view - note: this is not pixel perfect but good enough - isleaving = false; - } - - if(isleaving) - { - part.outofbounds = 1; - if (options->killoutofbounds) - part.ttl = 0; - } - } - } - - if (options->bounceY) - { - if ((newY < particleHardRadius) || (newY > maxY - particleHardRadius)) // reached floor / ceiling - { - if (newY < particleHardRadius) // bounce at bottom - bounce(part.vy, part.vx, newY, maxY); - else - { - if(!options->useGravity) - bounce(part.vy, part.vx, newY, maxY); - } - } - } - - if (((newY < 0) || (newY > maxY))) // check if particle reached an edge (makes sure particles are within frame for rendering) - { - if (options->wrapY) - { - newY = (uint16_t)newY % (maxY + 1); - } - else if (((newY <= -PS_P_HALFRADIUS) || (newY > maxY + PS_P_HALFRADIUS))) // particle is leaving, set out of bounds if it has fully left - { - bool isleaving = true; - if(usesize) // using individual particle size - { - if (((newY > -particleHardRadius) && (newY < maxY + particleHardRadius))) // still withing rendering reach - isleaving = false; - } - if(isleaving) - { - part.outofbounds = 1; - if (options->killoutofbounds) - { - if (newY < 0) // if gravity is enabled, only kill particles below ground - part.ttl = 0; - else if (!options->useGravity) - part.ttl = 0; - } - } - } - } - part.x = (int16_t)newX; // set new position - part.y = (int16_t)newY; // set new position - } + if (options == NULL) + options = &particlesettings; //use PS system settings by default + if (part.ttl > 0) + { + if (!part.perpetual) + part.ttl--; // age + if (particlesettings.colorByAge) + part.hue = part.ttl > 255 ? 255 : part.ttl; //set color to ttl + + bool usesize = false; // particle uses individual size rendering + int32_t newX = part.x + (int16_t)part.vx; + int32_t 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 (advancedproperties) //using individual particle size? + { + if (advancedproperties->size > 0) + usesize = true; // note: variable eases out of frame checking below + particleHardRadius = max(PS_P_MINHARDRADIUS, (int)particlesize + (advancedproperties->size)); + } + // if wall collisions are enabled, bounce them before they reach the edge, it looks much nicer if the particle is not half out of view + if (options->bounceX) + { + if ((newX < particleHardRadius) || (newX > maxX - particleHardRadius)) // reached a wall + bounce(part.vx, part.vy, newX, maxX); + } + + if ((newX < 0) || (newX > maxX)) // check if particle reached an edge (note: this also checks out of bounds and must not be skipped, even if bounce is enabled) + { + if (options->wrapX) + { + newX = (uint16_t)newX % (maxX + 1); + } + else if (((newX <= -PS_P_HALFRADIUS) || (newX > maxX + PS_P_HALFRADIUS))) // particle is leaving, set out of bounds if it has fully left + { + bool isleaving = true; + if (usesize) // using individual particle size + { + if (((newX > -particleHardRadius) && (newX < maxX + particleHardRadius))) // large particle is not yet leaving the view - note: this is not pixel perfect but good enough + isleaving = false; + } + + if (isleaving) + { + part.outofbounds = 1; + if (options->killoutofbounds) + part.ttl = 0; + } + } + } + + if (options->bounceY) + { + if ((newY < particleHardRadius) || (newY > maxY - particleHardRadius)) // reached floor / ceiling + { + if (newY < particleHardRadius) // bounce at bottom + bounce(part.vy, part.vx, newY, maxY); + else + { + if (!options->useGravity) + bounce(part.vy, part.vx, newY, maxY); + } + } + } + + if (((newY < 0) || (newY > maxY))) // check if particle reached an edge (makes sure particles are within frame for rendering) + { + if (options->wrapY) + { + newY = (uint16_t)newY % (maxY + 1); + } + else if (((newY <= -PS_P_HALFRADIUS) || (newY > maxY + PS_P_HALFRADIUS))) // particle is leaving, set out of bounds if it has fully left + { + bool isleaving = true; + if (usesize) // using individual particle size + { + if (((newY > -particleHardRadius) && (newY < maxY + particleHardRadius))) // still withing rendering reach + isleaving = false; + } + if (isleaving) + { + part.outofbounds = 1; + if (options->killoutofbounds) + { + if (newY < 0) // if gravity is enabled, only kill particles below ground + part.ttl = 0; + else if (!options->useGravity) + part.ttl = 0; + } + } + } + } + part.x = (int16_t)newX; // set new position + part.y = (int16_t)newY; // set new position + } } // update advanced particle size control void ParticleSystem::updateSize(PSadvancedParticle *advprops, PSsizeControl *advsize) { - if(advsize == NULL) // just a safety check - return; - // grow/shrink particle - int32_t newsize = advprops->size; - uint32_t counter = advsize->sizecounter; - uint32_t increment; - // calculate grow speed using 0-8 for low speeds and 9-15 for higher speeds - if(advsize->grow) increment = advsize->growspeed; - else if(advsize->shrink) increment = advsize->shrinkspeed; - if(increment < 9) // 8 means +1 every frame - { - counter += increment; - if(counter > 7) - { - counter -= 8; - increment = 1; - } - else - increment = 0; - advsize->sizecounter = counter; - } - else{ - increment = (increment - 8) << 1; // 9 means +2, 10 means +4 etc. 15 means +14 - } - if(advsize->grow) - { - if(newsize < advsize->maxsize) - { - newsize += increment; - if(newsize >= advsize->maxsize) - { - advsize->grow = false; // stop growing, shrink from now on if enabled - newsize = advsize->maxsize; // limit - if(advsize->pulsate) advsize->shrink = true; - } - } - } - else if(advsize->shrink) - { - if(newsize > advsize->minsize) - { - newsize -= increment; - if(newsize <= advsize->minsize) - { - //if(advsize->minsize == 0) part.ttl = 0; //TODO: need to pass particle or return kill instruction - advsize->shrink = false; // disable shrinking - newsize = advsize->minsize; // limit - if(advsize->pulsate) advsize->grow = true; - } - } - } - advprops->size = newsize; - // handle wobbling - if(advsize->wobble) - { - advsize->asymdir += advsize->wobblespeed; // todo: need better wobblespeed control? counter is already in the struct... - } + if (advsize == NULL) // just a safety check + return; + // grow/shrink particle + int32_t newsize = advprops->size; + uint32_t counter = advsize->sizecounter; + uint32_t increment; + // calculate grow speed using 0-8 for low speeds and 9-15 for higher speeds + if (advsize->grow) increment = advsize->growspeed; + else if (advsize->shrink) increment = advsize->shrinkspeed; + if (increment < 9) // 8 means +1 every frame + { + counter += increment; + if (counter > 7) + { + counter -= 8; + increment = 1; + } + else + increment = 0; + advsize->sizecounter = counter; + } + else{ + increment = (increment - 8) << 1; // 9 means +2, 10 means +4 etc. 15 means +14 + } + if (advsize->grow) + { + if (newsize < advsize->maxsize) + { + newsize += increment; + if (newsize >= advsize->maxsize) + { + advsize->grow = false; // stop growing, shrink from now on if enabled + newsize = advsize->maxsize; // limit + if (advsize->pulsate) advsize->shrink = true; + } + } + } + else if (advsize->shrink) + { + if (newsize > advsize->minsize) + { + newsize -= increment; + if (newsize <= advsize->minsize) + { + //if (advsize->minsize == 0) part.ttl = 0; //TODO: need to pass particle or return kill instruction + advsize->shrink = false; // disable shrinking + newsize = advsize->minsize; // limit + if (advsize->pulsate) advsize->grow = true; + } + } + } + advprops->size = newsize; + // handle wobbling + if (advsize->wobble) + { + advsize->asymdir += advsize->wobblespeed; // todo: need better wobblespeed control? counter is already in the struct... + } } // calculate x and y size for asymmetrical particles (advanced size control) void ParticleSystem::getParticleXYsize(PSadvancedParticle *advprops, PSsizeControl *advsize, uint32_t &xsize, uint32_t &ysize) { - if(advsize == NULL) // if advanced size is valid, also advced properties pointer is valid (handled by pointer assignment function) - return; + if (advsize == NULL) // if advanced size is valid, also advced properties pointer is valid (handled by pointer assignment function) + return; int32_t deviation = ((uint32_t)advprops->size * (uint32_t)advsize->asymmetry) / 255; // deviation from symmetrical size // Calculate x and y size based on deviation and direction (0 is symmetrical, 64 is x, 128 is symmetrical, 192 is y) - if (advsize->asymdir < 64) { - deviation = ((int32_t)advsize->asymdir * deviation) / 64; + if (advsize->asymdir < 64) { + deviation = ((int32_t)advsize->asymdir * deviation) / 64; } else if (advsize->asymdir < 192) { deviation = ((128 - (int32_t)advsize->asymdir) * deviation) / 64; } else { @@ -464,23 +463,23 @@ void ParticleSystem::getParticleXYsize(PSadvancedParticle *advprops, PSsizeContr // function to bounce a particle from a wall using set parameters (wallHardness and wallRoughness) void ParticleSystem::bounce(int8_t &incomingspeed, int8_t ¶llelspeed, int32_t &position, uint16_t maxposition) { - incomingspeed = -incomingspeed; - incomingspeed = (incomingspeed * wallHardness) / 255; // reduce speed as energy is lost on non-hard surface - if (position < particleHardRadius) - position = particleHardRadius; // fast particles will never reach the edge if position is inverted, this looks better - else - position = maxposition - particleHardRadius; - if(wallRoughness) - { - int32_t totalspeed = abs(incomingspeed) + abs(parallelspeed); - // transfer an amount of incomingspeed speed to parallel speed - int32_t donatespeed = abs(incomingspeed); - donatespeed = (random(-donatespeed, donatespeed) * wallRoughness) / 255; //take random portion of + or - perpendicular speed, scaled by roughness - parallelspeed = limitSpeed((int32_t)parallelspeed + donatespeed); - incomingspeed = limitSpeed((int32_t)incomingspeed - donatespeed); - donatespeed = totalspeed - abs(parallelspeed); // keep total speed the same - incomingspeed = incomingspeed > 0 ? donatespeed : -donatespeed; - } + incomingspeed = -incomingspeed; + incomingspeed = (incomingspeed * wallHardness) / 255; // reduce speed as energy is lost on non-hard surface + if (position < particleHardRadius) + position = particleHardRadius; // fast particles will never reach the edge if position is inverted, this looks better + else + position = maxposition - particleHardRadius; + if (wallRoughness) + { + int32_t totalspeed = abs(incomingspeed) + abs(parallelspeed); + // transfer an amount of incomingspeed speed to parallel speed + int32_t donatespeed = abs(incomingspeed); + donatespeed = (random(-donatespeed, donatespeed) * wallRoughness) / 255; //take random portion of + or - perpendicular speed, scaled by roughness + parallelspeed = limitSpeed((int32_t)parallelspeed + donatespeed); + incomingspeed = limitSpeed((int32_t)incomingspeed - donatespeed); + donatespeed = totalspeed - abs(parallelspeed); // keep total speed the same + incomingspeed = incomingspeed > 0 ? donatespeed : -donatespeed; + } } // apply a force in x,y direction to individual particle @@ -488,44 +487,44 @@ void ParticleSystem::bounce(int8_t &incomingspeed, int8_t ¶llelspeed, int32_ // 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, 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 + // 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); + // 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 + // 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: - part->vx = limitSpeed((int32_t)part->vx + dvx); - part->vy = limitSpeed((int32_t)part->vy + dvy); + // apply the force to particle + part->vx = limitSpeed((int32_t)part->vx + dvx); + part->vy = limitSpeed((int32_t)part->vy + dvy); } // apply a force in x,y direction to individual particle using advanced particle properties void ParticleSystem::applyForce(uint16_t particleindex, int8_t xforce, int8_t yforce) { - if (advPartProps == NULL) - return; // no advanced properties available - applyForce(&particles[particleindex], xforce, yforce, &advPartProps[particleindex].forcecounter); + if (advPartProps == NULL) + return; // no advanced properties available + applyForce(&particles[particleindex], xforce, yforce, &advPartProps[particleindex].forcecounter); } // apply a force in x,y direction to all particles // force is in 3.4 fixed point notation (see above) void ParticleSystem::applyForce(int8_t xforce, int8_t yforce) { - // for small forces, need to use a delay counter - uint8_t tempcounter; - // note: this is not the most compuatationally efficient way to do this, but it saves on duplacte code and is fast enough - for (uint i = 0; i < usedParticles; i++) - { - tempcounter = forcecounter; - applyForce(&particles[i], xforce, yforce, &tempcounter); - } - forcecounter = tempcounter; //save value back + // for small forces, need to use a delay counter + uint8_t tempcounter; + // note: this is not the most compuatationally efficient way to do this, but it saves on duplacte code and is fast enough + for (uint i = 0; i < usedParticles; i++) + { + tempcounter = forcecounter; + applyForce(&particles[i], xforce, yforce, &tempcounter); + } + forcecounter = tempcounter; //save value back } // apply a force in angular direction to single particle @@ -534,26 +533,26 @@ void ParticleSystem::applyForce(int8_t xforce, int8_t yforce) // force is in 3.4 fixed point notation so force=16 means apply v+1 each frame (useful force range is +/- 127) void ParticleSystem::applyAngleForce(PSparticle *part, int8_t force, uint16_t angle, uint8_t *counter) { - int8_t xforce = ((int32_t)force * cos16(angle)) / 32767; // force is +/- 127 - int8_t yforce = ((int32_t)force * sin16(angle)) / 32767; // note: cannot use bit shifts as bit shifting is asymmetrical for positive and negative numbers and this needs to be accurate! - // note: sin16 is 10% faster than sin8() on ESP32 but on ESP8266 it is 9% slower - applyForce(part, xforce, yforce, counter); + int8_t xforce = ((int32_t)force * cos16(angle)) / 32767; // force is +/- 127 + int8_t yforce = ((int32_t)force * sin16(angle)) / 32767; // note: cannot use bit shifts as bit shifting is asymmetrical for positive and negative numbers and this needs to be accurate! + // note: sin16 is 10% faster than sin8() on ESP32 but on ESP8266 it is 9% slower + applyForce(part, xforce, yforce, counter); } void ParticleSystem::applyAngleForce(uint16_t particleindex, int8_t force, uint16_t angle) { - if (advPartProps == NULL) - return; // no advanced properties available - applyAngleForce(&particles[particleindex], force, angle, &advPartProps[particleindex].forcecounter); + if (advPartProps == NULL) + return; // no advanced properties available + applyAngleForce(&particles[particleindex], force, angle, &advPartProps[particleindex].forcecounter); } // apply a force in angular direction to all particles // angle is from 0-65535 (=0-360deg) angle = 0 means in positive x-direction (i.e. to the right) void ParticleSystem::applyAngleForce(int8_t force, uint16_t angle) { - int8_t xforce = ((int32_t)force * cos16(angle)) / 32767; // force is +/- 127 - int8_t yforce = ((int32_t)force * sin16(angle)) / 32767; // note: cannot use bit shifts as bit shifting is asymmetrical for positive and negative numbers and this needs to be accurate! - applyForce(xforce, yforce); + int8_t xforce = ((int32_t)force * cos16(angle)) / 32767; // force is +/- 127 + int8_t yforce = ((int32_t)force * sin16(angle)) / 32767; // note: cannot use bit shifts as bit shifting is asymmetrical for positive and negative numbers and this needs to be accurate! + applyForce(xforce, yforce); } @@ -562,139 +561,139 @@ void ParticleSystem::applyAngleForce(int8_t force, uint16_t angle) // note: faster than apply force since direction is always down and counter is fixed for all particles void ParticleSystem::applyGravity() { - int32_t dv = calcForce_dv(gforce, &gforcecounter); - for (uint32_t i = 0; i < usedParticles; 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 = limitSpeed((int32_t)particles[i].vy - dv); - } + int32_t dv = calcForce_dv(gforce, &gforcecounter); + for (uint32_t i = 0; i < usedParticles; 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 = limitSpeed((int32_t)particles[i].vy - dv); + } } // apply gravity to single particle using system settings (use this for sources) // function does not increment gravity counter, if gravity setting is disabled, this cannot be used void ParticleSystem::applyGravity(PSparticle *part) { - int32_t dv; // velocity increase - if (gforce > 15) - dv = (gforce >> 4); // apply the 4 MSBs - else - dv = 1; - - if (gforcecounter + gforce > 15) //counter is updated in global update when applying gravity - { - part->vy = limitSpeed((int32_t)part->vy - dv); - } + int32_t dv; // velocity increase + if (gforce > 15) + dv = (gforce >> 4); // apply the 4 MSBs + else + dv = 1; + + if (gforcecounter + gforce > 15) //counter is updated in global update when applying gravity + { + part->vy = limitSpeed((int32_t)part->vy - dv); + } } // slow down particle by friction, the higher the speed, the higher the friction. a high friction coefficient slows them more (255 means instant stop) // note: a coefficient smaller than 0 will speed them up (this is a feature, not a bug), coefficient larger than 255 inverts the speed, so don't do that void ParticleSystem::applyFriction(PSparticle *part, int32_t coefficient) { - int32_t friction = 255 - coefficient; - // note: not checking if particle is dead can be done by caller (or can be omitted) - // note2: cannot use right shifts as bit shifting in right direction is asymmetrical for positive and negative numbers and this needs to be accurate or things start to go to the left side. - part->vx = ((int16_t)part->vx * friction) / 255; - part->vy = ((int16_t)part->vy * friction) / 255; + int32_t friction = 255 - coefficient; + // note: not checking if particle is dead can be done by caller (or can be omitted) + // note2: cannot use right shifts as bit shifting in right direction is asymmetrical for positive and negative numbers and this needs to be accurate or things start to go to the left side. + part->vx = ((int16_t)part->vx * friction) / 255; + part->vy = ((int16_t)part->vy * friction) / 255; } // apply friction to all particles void ParticleSystem::applyFriction(int32_t coefficient) { - for (uint32_t i = 0; i < usedParticles; i++) - { - if(particles[i].ttl) - applyFriction(&particles[i], coefficient); - } + for (uint32_t i = 0; i < usedParticles; i++) + { + if (particles[i].ttl) + applyFriction(&particles[i], coefficient); + } } // attracts a particle to an attractor particle using the inverse square-law void ParticleSystem::pointAttractor(uint16_t particleindex, PSparticle *attractor, uint8_t strength, bool swallow) { - if (advPartProps == NULL) - return; // no advanced properties available - - // Calculate the distance between the particle and the attractor - int32_t dx = attractor->x - particles[particleindex].x; - int32_t dy = attractor->y - particles[particleindex].y; - - // Calculate the force based on inverse square law - int32_t distanceSquared = dx * dx + dy * dy; - if (distanceSquared < 8192) - { - if (swallow) // particle is close, age it fast so it fades out, do not attract further - { - if (particles[particleindex].ttl > 7) - particles[particleindex].ttl -= 8; - else - { - particles[particleindex].ttl = 0; - return; - } - } - distanceSquared = 2 * PS_P_RADIUS * PS_P_RADIUS; // limit the distance to avoid very high forces - } - - int32_t force = ((int32_t)strength << 16) / distanceSquared; - int8_t xforce = (force * dx) / 1024; // scale to a lower value, found by experimenting - int8_t yforce = (force * dy) / 1024; // note: cannot use bit shifts as bit shifting is asymmetrical for positive and negative numbers and this needs to be accurate! - - applyForce(particleindex, xforce, yforce); + if (advPartProps == NULL) + return; // no advanced properties available + + // Calculate the distance between the particle and the attractor + int32_t dx = attractor->x - particles[particleindex].x; + int32_t dy = attractor->y - particles[particleindex].y; + + // Calculate the force based on inverse square law + int32_t distanceSquared = dx * dx + dy * dy; + if (distanceSquared < 8192) + { + if (swallow) // particle is close, age it fast so it fades out, do not attract further + { + if (particles[particleindex].ttl > 7) + particles[particleindex].ttl -= 8; + else + { + particles[particleindex].ttl = 0; + return; + } + } + distanceSquared = 2 * PS_P_RADIUS * PS_P_RADIUS; // limit the distance to avoid very high forces + } + + int32_t force = ((int32_t)strength << 16) / distanceSquared; + int8_t xforce = (force * dx) / 1024; // scale to a lower value, found by experimenting + int8_t yforce = (force * dy) / 1024; // note: cannot use bit shifts as bit shifting is asymmetrical for positive and negative numbers and this needs to be accurate! + + applyForce(particleindex, xforce, yforce); } /* //attract to a line (TODO: this is not yet working) void ParticleSystem::lineAttractor(uint16_t particleindex, PSparticle *attractorcenter, uint16_t attractorangle, uint8_t strength) { - // Calculate the distance between the particle and the attractor - if(advPartProps == NULL) - return; // no advanced properties available - - // calculate a second point on the line - int32_t x1 = attractorcenter->x + (cos16(attractorangle) >> 5); - int32_t y1 = attractorcenter->y + (sin16(attractorangle) >> 5); - // calculate squared distance from particle to the line: - int32_t dx = (x1 - attractorcenter->x) >> 4; - int32_t dy = (y1 - attractorcenter->y) >> 4; - int32_t d = ((dx * (particles[particleindex].y - attractorcenter->y)) - (dy * (particles[particleindex].x - attractorcenter->x))) >> 8; - int32_t distanceSquared = (d * d) / (dx * dx + dy * dy); - - - // Calculate the force based on inverse square law - if (distanceSquared < 2) - { - distanceSquared = 1; - // distanceSquared = 4 * PS_P_RADIUS * PS_P_RADIUS; // limit the distance to avoid very high forces - } - - int32_t force = (((int32_t)strength << 16) / distanceSquared)>>10; - //apply force in a 90° angle to the line - int8_t xforce = (d > 0 ? 1 : -1) * (force * dy) / 100; // scale to a lower value, found by experimenting - int8_t yforce = (d > 0 ? -1 : 1) * (force * dx) / 100; // note: cannot use bit shifts as bit shifting is asymmetrical for positive and negative numbers and this needs to be accurate! - - Serial.print(" partx: "); - Serial.print(particles[particleindex].x); - Serial.print(" party "); - Serial.print(particles[particleindex].y); - Serial.print(" x1 "); - Serial.print(x1); - Serial.print(" y1 "); - Serial.print(y1); - Serial.print(" dx "); - Serial.print(dx); - Serial.print(" dy "); - Serial.print(dy); - Serial.print(" d: "); - Serial.print(d); - Serial.print(" dsq: "); - Serial.print(distanceSquared); - Serial.print(" force: "); - Serial.print(force); - Serial.print(" fx: "); - Serial.print(xforce); - Serial.print(" fy: "); - Serial.println(yforce); - - applyForce(particleindex, xforce, yforce); + // Calculate the distance between the particle and the attractor + if (advPartProps == NULL) + return; // no advanced properties available + + // calculate a second point on the line + int32_t x1 = attractorcenter->x + (cos16(attractorangle) >> 5); + int32_t y1 = attractorcenter->y + (sin16(attractorangle) >> 5); + // calculate squared distance from particle to the line: + int32_t dx = (x1 - attractorcenter->x) >> 4; + int32_t dy = (y1 - attractorcenter->y) >> 4; + int32_t d = ((dx * (particles[particleindex].y - attractorcenter->y)) - (dy * (particles[particleindex].x - attractorcenter->x))) >> 8; + int32_t distanceSquared = (d * d) / (dx * dx + dy * dy); + + + // Calculate the force based on inverse square law + if (distanceSquared < 2) + { + distanceSquared = 1; + // distanceSquared = 4 * PS_P_RADIUS * PS_P_RADIUS; // limit the distance to avoid very high forces + } + + int32_t force = (((int32_t)strength << 16) / distanceSquared)>>10; + //apply force in a 90° angle to the line + int8_t xforce = (d > 0 ? 1 : -1) * (force * dy) / 100; // scale to a lower value, found by experimenting + int8_t yforce = (d > 0 ? -1 : 1) * (force * dx) / 100; // note: cannot use bit shifts as bit shifting is asymmetrical for positive and negative numbers and this needs to be accurate! + + Serial.print(" partx: "); + Serial.print(particles[particleindex].x); + Serial.print(" party "); + Serial.print(particles[particleindex].y); + Serial.print(" x1 "); + Serial.print(x1); + Serial.print(" y1 "); + Serial.print(y1); + Serial.print(" dx "); + Serial.print(dx); + Serial.print(" dy "); + Serial.print(dy); + Serial.print(" d: "); + Serial.print(d); + Serial.print(" dsq: "); + Serial.print(distanceSquared); + Serial.print(" force: "); + Serial.print(force); + Serial.print(" fx: "); + Serial.print(xforce); + Serial.print(" fy: "); + Serial.println(yforce); + + applyForce(particleindex, xforce, yforce); }*/ // render particles to the LED buffer (uses palette to render the 8bit particle color value) @@ -703,355 +702,355 @@ void ParticleSystem::lineAttractor(uint16_t particleindex, PSparticle *attractor // fireintensity and firemode are optional arguments (fireintensity is only used in firemode) void ParticleSystem::ParticleSys_render(bool firemode, uint32_t fireintensity) { - - CRGB baseRGB; - bool useLocalBuffer = true; //use local rendering buffer, gives huge speed boost (at least 30% more FPS) - CRGB **framebuffer = NULL; //local frame buffer - CRGB **renderbuffer = NULL; //local particle render buffer for advanced particles - uint32_t i; - uint32_t brightness; // particle brightness, fades if dying - - if (useLocalBuffer) - { - /* - //memory fragmentation check: - Serial.print("heap: "); - Serial.print(heap_caps_get_free_size(MALLOC_CAP_8BIT)); - Serial.print(" block: "); - Serial.println(heap_caps_get_largest_free_block(MALLOC_CAP_8BIT)); - */ - - // allocate empty memory for the local renderbuffer - framebuffer = allocate2Dbuffer(maxXpixel + 1, maxYpixel + 1); - if (framebuffer == NULL) - { - Serial.println("Frame buffer alloc failed"); - useLocalBuffer = false; //render to segment pixels directly if not enough memory - } - else{ - if(advPartProps) - { - renderbuffer = allocate2Dbuffer(10, 10); //buffer to render individual particles to if size > 0 note: null checking is done when accessing it - } - if (motionBlur > 0) // using SEGMENT.fadeToBlackBy is much slower, this approximately doubles the speed of fade calculation - { - uint32_t yflipped; - for (uint32_t y = 0; y <= maxYpixel; y++) - { - yflipped = maxYpixel - y; - for (uint32_t x = 0; x <= maxXpixel; x++) - { - framebuffer[x][y] = SEGMENT.getPixelColorXY(x, yflipped); //copy to local buffer - fast_color_scale(framebuffer[x][y], motionBlur); - } - } - } - - } - - } - - if(!useLocalBuffer) //disabled or allocation above failed - { - Serial.println("NOT using local buffer!"); - if (motionBlur > 0) - SEGMENT.fadeToBlackBy(255 - motionBlur); - else - SEGMENT.fill(BLACK); //clear the buffer before rendering to it - } - // go over particles and render them to the buffer - for (i = 0; i < usedParticles; i++) - { - if (particles[i].outofbounds || particles[i].ttl == 0) - continue; - - // generate RGB values for particle - if(firemode) - { - //TODO: decide on a final version... - //brightness = (uint32_t)particles[i].ttl * (1 + (fireintensity >> 4)) + (fireintensity >> 2); //this is good - //brightness = (uint32_t)particles[i].ttl * (fireintensity >> 3) + (fireintensity >> 1); // this is experimental, also works, flamecolor is more even, does not look as good (but less puffy at lower speeds) - //brightness = (((uint32_t)particles[i].ttl * (maxY + PS_P_RADIUS - particles[i].y)) >> 7) + (uint32_t)particles[i].ttl * (fireintensity >> 4) + (fireintensity >> 1); // this is experimental //multiplikation mit weniger als >>4 macht noch mehr puffs bei low speed - //brightness = (((uint32_t)particles[i].ttl * (maxY + PS_P_RADIUS - particles[i].y)) >> 7) + particles[i].ttl + (fireintensity>>1); // this is experimental - //brightness = (((uint32_t)particles[i].ttl * (maxY + PS_P_RADIUS - particles[i].y)) >> 7) + ((particles[i].ttl * fireintensity) >> 5); // this is experimental TODO: test this -> testing... ok but not the best, bit sparky - brightness = (((uint32_t)particles[i].ttl * (maxY + PS_P_RADIUS - particles[i].y)) >> 7) + (fireintensity >> 1); // this is experimental TODO: test this -> testing... does not look too bad! - brightness = brightness > 255 ? 255 : brightness; // faster then using min() - baseRGB = ColorFromPalette(SEGPALETTE, brightness, 255, LINEARBLEND); - } - else{ - 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) - { - CHSV baseHSV = rgb2hsv_approximate(baseRGB); //convert to HSV - baseHSV.s = particles[i].sat; //set the saturation - baseRGB = (CRGB)baseHSV; // convert back to RGB - } - } - - renderParticle(framebuffer, i, brightness, baseRGB, renderbuffer); - } - - if(particlesize > 0) - { - uint32_t passes = particlesize/64 + 1; // number of blur passes, four passes max - uint32_t bluramount = particlesize; - uint32_t bitshift = 0; - - for(int i = 0; i < passes; i++) - { - if(i == 2) // for the last two passes, use higher amount of blur (results in a nicer brightness gradient with soft edges) - bitshift = 1; - - if (useLocalBuffer) - blur2D(framebuffer, maxXpixel + 1, maxYpixel + 1, bluramount << bitshift, bluramount << bitshift); - else - SEGMENT.blur(bluramount << bitshift, true); - bluramount -= 64; - } - } - - if (useLocalBuffer) // transfer local buffer back to segment - { - uint32_t yflipped; - for (int y = 0; y <= maxYpixel; y++) - { - yflipped = maxYpixel - y; - for (int x = 0; x <= maxXpixel; x++) - { - SEGMENT.setPixelColorXY(x, yflipped, framebuffer[x][y]); - } - } - free(framebuffer); - } - if(renderbuffer) - free(renderbuffer); + + CRGB baseRGB; + bool useLocalBuffer = true; //use local rendering buffer, gives huge speed boost (at least 30% more FPS) + CRGB **framebuffer = NULL; //local frame buffer + CRGB **renderbuffer = NULL; //local particle render buffer for advanced particles + uint32_t i; + uint32_t brightness; // particle brightness, fades if dying + + if (useLocalBuffer) + { + /* + //memory fragmentation check: + Serial.print("heap: "); + Serial.print(heap_caps_get_free_size(MALLOC_CAP_8BIT)); + Serial.print(" block: "); + Serial.println(heap_caps_get_largest_free_block(MALLOC_CAP_8BIT)); + */ + + // allocate empty memory for the local renderbuffer + framebuffer = allocate2Dbuffer(maxXpixel + 1, maxYpixel + 1); + if (framebuffer == NULL) + { + Serial.println("Frame buffer alloc failed"); + useLocalBuffer = false; //render to segment pixels directly if not enough memory + } + else{ + if (advPartProps) + { + renderbuffer = allocate2Dbuffer(10, 10); //buffer to render individual particles to if size > 0. note: null checking is done when accessing it + } + if (motionBlur > 0) // using SEGMENT.fadeToBlackBy is much slower, this approximately doubles the speed of fade calculation + { + uint32_t yflipped; + for (uint32_t y = 0; y <= maxYpixel; y++) + { + yflipped = maxYpixel - y; + for (uint32_t x = 0; x <= maxXpixel; x++) + { + framebuffer[x][y] = SEGMENT.getPixelColorXY(x, yflipped); //copy to local buffer + fast_color_scale(framebuffer[x][y], motionBlur); + } + } + } + + } + + } + + if (!useLocalBuffer) //disabled or allocation above failed + { + Serial.println("NOT using local buffer!"); + if (motionBlur > 0) + SEGMENT.fadeToBlackBy(255 - motionBlur); + else + SEGMENT.fill(BLACK); //clear the buffer before rendering to it + } + // go over particles and render them to the buffer + for (i = 0; i < usedParticles; i++) + { + if (particles[i].outofbounds || particles[i].ttl == 0) + continue; + + // generate RGB values for particle + if (firemode) + { + //TODO: decide on a final version... + //brightness = (uint32_t)particles[i].ttl * (1 + (fireintensity >> 4)) + (fireintensity >> 2); //this is good + //brightness = (uint32_t)particles[i].ttl * (fireintensity >> 3) + (fireintensity >> 1); // this is experimental, also works, flamecolor is more even, does not look as good (but less puffy at lower speeds) + //brightness = (((uint32_t)particles[i].ttl * (maxY + PS_P_RADIUS - particles[i].y)) >> 7) + (uint32_t)particles[i].ttl * (fireintensity >> 4) + (fireintensity >> 1); // this is experimental //multiplikation mit weniger als >>4 macht noch mehr puffs bei low speed + //brightness = (((uint32_t)particles[i].ttl * (maxY + PS_P_RADIUS - particles[i].y)) >> 7) + particles[i].ttl + (fireintensity>>1); // this is experimental + //brightness = (((uint32_t)particles[i].ttl * (maxY + PS_P_RADIUS - particles[i].y)) >> 7) + ((particles[i].ttl * fireintensity) >> 5); // this is experimental TODO: test this -> testing... ok but not the best, bit sparky + brightness = (((uint32_t)particles[i].ttl * (maxY + PS_P_RADIUS - particles[i].y)) >> 7) + (fireintensity >> 1); // this is experimental TODO: test this -> testing... does not look too bad! + brightness = brightness > 255 ? 255 : brightness; // faster then using min() + baseRGB = ColorFromPalette(SEGPALETTE, brightness, 255, LINEARBLEND); + } + else{ + 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) + { + CHSV baseHSV = rgb2hsv_approximate(baseRGB); //convert to HSV + baseHSV.s = particles[i].sat; //set the saturation + baseRGB = (CRGB)baseHSV; // convert back to RGB + } + } + + renderParticle(framebuffer, i, brightness, baseRGB, renderbuffer); + } + + if (particlesize > 0) + { + uint32_t passes = particlesize/64 + 1; // number of blur passes, four passes max + uint32_t bluramount = particlesize; + uint32_t bitshift = 0; + + for(int i = 0; i < passes; i++) + { + if (i == 2) // for the last two passes, use higher amount of blur (results in a nicer brightness gradient with soft edges) + bitshift = 1; + + if (useLocalBuffer) + blur2D(framebuffer, maxXpixel + 1, maxYpixel + 1, bluramount << bitshift, bluramount << bitshift); + else + SEGMENT.blur(bluramount << bitshift, true); + bluramount -= 64; + } + } + + if (useLocalBuffer) // transfer local buffer back to segment + { + uint32_t yflipped; + for (int y = 0; y <= maxYpixel; y++) + { + yflipped = maxYpixel - y; + for (int x = 0; x <= maxXpixel; x++) + { + SEGMENT.setPixelColorXY(x, yflipped, framebuffer[x][y]); + } + } + free(framebuffer); + } + if (renderbuffer) + free(renderbuffer); } // calculate pixel positions and brightness distribution and render the particle to local buffer or global buffer void ParticleSystem::renderParticle(CRGB **framebuffer, uint32_t particleindex, uint32_t brightess, CRGB color, CRGB **renderbuffer) { - int32_t pxlbrightness[4] = {0}; // note: pxlbrightness needs to be set to 0 or checking does not work - int32_t pixco[4][2]; // physical pixel coordinates of the four pixels a particle is rendered to. x,y pairs - bool advancedrender = false; // rendering for advanced particles - - // subtract half a radius as the rendering algorithm always starts at the bottom left, this makes calculations more efficient - int32_t xoffset = particles[particleindex].x - PS_P_HALFRADIUS; - int32_t yoffset = particles[particleindex].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; - - // check if particle has advanced size properties and buffer is available - if(advPartProps) - { - if(advPartProps[particleindex].size > 0) - { - if(renderbuffer) - { - advancedrender = true; - memset(renderbuffer[0], 0, 100 * sizeof(CRGB)); // clear the buffer, renderbuffer is 10x10 pixels - } - else - return; // cannot render without buffer, advanced size particles are allowed out of frame - } - } - - // set the four raw pixel coordinates, the order is bottom left [0], bottom right[1], top right [2], top left [3] - pixco[0][0] = pixco[3][0] = x; // bottom left & top left - pixco[0][1] = pixco[1][1] = y; // bottom left & bottom right - pixco[1][0] = pixco[2][0] = x + 1; // bottom right & top right - pixco[2][1] = pixco[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 - { - 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) - { - pxlbrightness[1] = pxlbrightness[2] = -1; // pixel is actually out of matrix boundaries, do not render - } - if (particlesettings.wrapX) // wrap x to the other side if required - pixco[0][0] = pixco[3][0] = maxXpixel; - else - pxlbrightness[0] = pxlbrightness[3] = -1; // pixel is out of matrix boundaries, do not render - } - else if (pixco[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 - pixco[1][0] = pixco[2][0] = 0; - else - pxlbrightness[1] = pxlbrightness[2] = -1; - } - - if (y < 0) // bottom pixels out of frame - { - dy = PS_P_RADIUS + dy; //see note above - if (dy == PS_P_RADIUS) - { - pxlbrightness[2] = pxlbrightness[3] = -1; // pixel is actually out of matrix boundaries, do not render - } - if (particlesettings.wrapY) // wrap y to the other side if required - pixco[0][1] = pixco[1][1] = maxYpixel; - else - pxlbrightness[0] = pxlbrightness[1] = -1; - } - else if (pixco[2][1] > maxYpixel) // top pixels - { - if (particlesettings.wrapY) // wrap y to the other side if required - pixco[2][1] = pixco[3][1] = 0; - else - pxlbrightness[2] = pxlbrightness[3] = -1; - } - - if(advancedrender) // always render full particles in advanced rendering, undo out of frame marking (faster than checking each time in code above) - { - for(uint32_t i = 0; i < 4; i++) - pxlbrightness[i] = 0; - } - - // 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 (pxlbrightness[0] >= 0) - pxlbrightness[0] = (precal1 * precal2) >> PS_P_SURFACE; // bottom left value equal to ((PS_P_RADIUS - dx) * (PS_P_RADIUS-dy) * brightess) >> PS_P_SURFACE - if (pxlbrightness[1] >= 0) - pxlbrightness[1] = (dx * precal2) >> PS_P_SURFACE; // bottom right value equal to (dx * (PS_P_RADIUS-dy) * brightess) >> PS_P_SURFACE - if (pxlbrightness[2] >= 0) - pxlbrightness[2] = (dx * precal3) >> PS_P_SURFACE; // top right value equal to (dx * dy * brightess) >> PS_P_SURFACE - if (pxlbrightness[3] >= 0) - pxlbrightness[3] = (precal1 * precal3) >> PS_P_SURFACE; // top left value equal to ((PS_P_RADIUS-dx) * dy * brightess) >> PS_P_SURFACE - - if(advancedrender) - { - //render particle to a bigger size - //particle size to pixels: < 64 is 4x4, < 128 is 6x6, < 192 is 8x8, bigger is 10x10 - //first, render the pixel to the center of the renderbuffer, then apply 2D blurring - fast_color_add(renderbuffer[4][4], color, pxlbrightness[0]); // order is: bottom left, bottom right, top right, top left - fast_color_add(renderbuffer[5][4], color, pxlbrightness[1]); - fast_color_add(renderbuffer[5][5], color, pxlbrightness[2]); - fast_color_add(renderbuffer[4][5], color, pxlbrightness[3]); //TODO: make this a loop somehow? needs better coordinate handling... - uint32_t rendersize = 2; // initialize render size, minimum is 4x4 pixels, it is incremented int he loop below to start with 4 - uint32_t offset = 4; // offset to zero coordinate to write/read data in renderbuffer (actually needs to be 3, is decremented in the loop below) - uint32_t maxsize = advPartProps[particleindex].size; - uint32_t xsize = maxsize; - uint32_t ysize = maxsize; - if(advPartSize) // use advanced size control - { - if(advPartSize[particleindex].asymmetry > 0) - getParticleXYsize(&advPartProps[particleindex], &advPartSize[particleindex], xsize, ysize); - maxsize = xsize; - if(ysize > maxsize) maxsize = ysize; //maxsize is now the bigger of the two - } - maxsize = maxsize/64 + 1; // number of blur passes depends on maxsize, four passes max - uint32_t bitshift = 0; - for(int i = 0; i < maxsize; i++) - { - if(i == 2) //for the last two passes, use higher amount of blur (results in a nicer brightness gradient with soft edges) - bitshift = 1; - rendersize += 2; - offset--; - blur2D(renderbuffer, rendersize, rendersize, xsize << bitshift, ysize << bitshift, true, offset, offset, true); //blur to 4x4 - xsize = xsize > 64 ? xsize - 64 : 0; - ysize = ysize > 64 ? ysize - 64 : 0; - } - - // calculate origin coordinates to render the particle to in the framebuffer - uint32_t xfb_orig = x - (rendersize>>1) + 1 - offset; - uint32_t yfb_orig = y - (rendersize>>1) + 1 - offset; - uint32_t xfb, yfb; // coordinates in frame buffer to write to note: by making this uint, only overflow has to be checked - - // transfer particle renderbuffer to framebuffer - for(uint32_t xrb = offset; xrb < rendersize+offset; xrb++) - { - xfb = xfb_orig + xrb; - if(xfb > maxXpixel) - { - if (particlesettings.wrapX) // wrap x to the other side if required - xfb = xfb % (maxXpixel + 1); - else - continue; - } - - for(uint32_t yrb = offset; yrb < rendersize+offset; yrb++) - { - yfb = yfb_orig + yrb; - if(yfb > maxYpixel) - { - if (particlesettings.wrapY) // wrap y to the other side if required - yfb = yfb % (maxYpixel + 1); - else - continue; - } - fast_color_add(framebuffer[xfb][yfb], renderbuffer[xrb][yrb]); - } - } - } - else // standard rendering - { - if (framebuffer) - { - for(uint32_t i = 0; i < 4; i++) - { - if (pxlbrightness[i] > 0) - fast_color_add(framebuffer[pixco[i][0]][pixco[i][1]], color, pxlbrightness[i]); // order is: bottom left, bottom right, top right, top left - } - } - else - { - for(uint32_t i = 0; i < 4; i++) - { - if (pxlbrightness[i] > 0) - SEGMENT.addPixelColorXY(pixco[i][0], maxYpixel - pixco[i][1], color.scale8((uint8_t)pxlbrightness[i])); - } - } - } + int32_t pxlbrightness[4] = {0}; // note: pxlbrightness needs to be set to 0 or checking does not work + int32_t pixco[4][2]; // physical pixel coordinates of the four pixels a particle is rendered to. x,y pairs + bool advancedrender = false; // rendering for advanced particles + + // subtract half a radius as the rendering algorithm always starts at the bottom left, this makes calculations more efficient + int32_t xoffset = particles[particleindex].x - PS_P_HALFRADIUS; + int32_t yoffset = particles[particleindex].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; + + // check if particle has advanced size properties and buffer is available + if (advPartProps) + { + if (advPartProps[particleindex].size > 0) + { + if (renderbuffer) + { + advancedrender = true; + memset(renderbuffer[0], 0, 100 * sizeof(CRGB)); // clear the buffer, renderbuffer is 10x10 pixels + } + else + return; // cannot render without buffer, advanced size particles are allowed out of frame + } + } + + // set the four raw pixel coordinates, the order is bottom left [0], bottom right[1], top right [2], top left [3] + pixco[0][0] = pixco[3][0] = x; // bottom left & top left + pixco[0][1] = pixco[1][1] = y; // bottom left & bottom right + pixco[1][0] = pixco[2][0] = x + 1; // bottom right & top right + pixco[2][1] = pixco[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 + { + 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) + { + pxlbrightness[1] = pxlbrightness[2] = -1; // pixel is actually out of matrix boundaries, do not render + } + if (particlesettings.wrapX) // wrap x to the other side if required + pixco[0][0] = pixco[3][0] = maxXpixel; + else + pxlbrightness[0] = pxlbrightness[3] = -1; // pixel is out of matrix boundaries, do not render + } + else if (pixco[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 + pixco[1][0] = pixco[2][0] = 0; + else + pxlbrightness[1] = pxlbrightness[2] = -1; + } + + if (y < 0) // bottom pixels out of frame + { + dy = PS_P_RADIUS + dy; //see note above + if (dy == PS_P_RADIUS) + { + pxlbrightness[2] = pxlbrightness[3] = -1; // pixel is actually out of matrix boundaries, do not render + } + if (particlesettings.wrapY) // wrap y to the other side if required + pixco[0][1] = pixco[1][1] = maxYpixel; + else + pxlbrightness[0] = pxlbrightness[1] = -1; + } + else if (pixco[2][1] > maxYpixel) // top pixels + { + if (particlesettings.wrapY) // wrap y to the other side if required + pixco[2][1] = pixco[3][1] = 0; + else + pxlbrightness[2] = pxlbrightness[3] = -1; + } + + if (advancedrender) // always render full particles in advanced rendering, undo out of frame marking (faster than checking each time in code above) + { + for(uint32_t i = 0; i < 4; i++) + pxlbrightness[i] = 0; + } + + // 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 (pxlbrightness[0] >= 0) + pxlbrightness[0] = (precal1 * precal2) >> PS_P_SURFACE; // bottom left value equal to ((PS_P_RADIUS - dx) * (PS_P_RADIUS-dy) * brightess) >> PS_P_SURFACE + if (pxlbrightness[1] >= 0) + pxlbrightness[1] = (dx * precal2) >> PS_P_SURFACE; // bottom right value equal to (dx * (PS_P_RADIUS-dy) * brightess) >> PS_P_SURFACE + if (pxlbrightness[2] >= 0) + pxlbrightness[2] = (dx * precal3) >> PS_P_SURFACE; // top right value equal to (dx * dy * brightess) >> PS_P_SURFACE + if (pxlbrightness[3] >= 0) + pxlbrightness[3] = (precal1 * precal3) >> PS_P_SURFACE; // top left value equal to ((PS_P_RADIUS-dx) * dy * brightess) >> PS_P_SURFACE + + if (advancedrender) + { + //render particle to a bigger size + //particle size to pixels: < 64 is 4x4, < 128 is 6x6, < 192 is 8x8, bigger is 10x10 + //first, render the pixel to the center of the renderbuffer, then apply 2D blurring + fast_color_add(renderbuffer[4][4], color, pxlbrightness[0]); // order is: bottom left, bottom right, top right, top left + fast_color_add(renderbuffer[5][4], color, pxlbrightness[1]); + fast_color_add(renderbuffer[5][5], color, pxlbrightness[2]); + fast_color_add(renderbuffer[4][5], color, pxlbrightness[3]); //TODO: make this a loop somehow? needs better coordinate handling... + uint32_t rendersize = 2; // initialize render size, minimum is 4x4 pixels, it is incremented int he loop below to start with 4 + uint32_t offset = 4; // offset to zero coordinate to write/read data in renderbuffer (actually needs to be 3, is decremented in the loop below) + uint32_t maxsize = advPartProps[particleindex].size; + uint32_t xsize = maxsize; + uint32_t ysize = maxsize; + if (advPartSize) // use advanced size control + { + if (advPartSize[particleindex].asymmetry > 0) + getParticleXYsize(&advPartProps[particleindex], &advPartSize[particleindex], xsize, ysize); + maxsize = xsize; + if (ysize > maxsize) maxsize = ysize; //maxsize is now the bigger of the two + } + maxsize = maxsize/64 + 1; // number of blur passes depends on maxsize, four passes max + uint32_t bitshift = 0; + for(int i = 0; i < maxsize; i++) + { + if (i == 2) //for the last two passes, use higher amount of blur (results in a nicer brightness gradient with soft edges) + bitshift = 1; + rendersize += 2; + offset--; + blur2D(renderbuffer, rendersize, rendersize, xsize << bitshift, ysize << bitshift, true, offset, offset, true); //blur to 4x4 + xsize = xsize > 64 ? xsize - 64 : 0; + ysize = ysize > 64 ? ysize - 64 : 0; + } + + // calculate origin coordinates to render the particle to in the framebuffer + uint32_t xfb_orig = x - (rendersize>>1) + 1 - offset; + uint32_t yfb_orig = y - (rendersize>>1) + 1 - offset; + uint32_t xfb, yfb; // coordinates in frame buffer to write to note: by making this uint, only overflow has to be checked + + // transfer particle renderbuffer to framebuffer + for(uint32_t xrb = offset; xrb < rendersize+offset; xrb++) + { + xfb = xfb_orig + xrb; + if (xfb > maxXpixel) + { + if (particlesettings.wrapX) // wrap x to the other side if required + xfb = xfb % (maxXpixel + 1); + else + continue; + } + + for(uint32_t yrb = offset; yrb < rendersize+offset; yrb++) + { + yfb = yfb_orig + yrb; + if (yfb > maxYpixel) + { + if (particlesettings.wrapY) // wrap y to the other side if required + yfb = yfb % (maxYpixel + 1); + else + continue; + } + fast_color_add(framebuffer[xfb][yfb], renderbuffer[xrb][yrb]); + } + } + } + else // standard rendering + { + if (framebuffer) + { + for(uint32_t i = 0; i < 4; i++) + { + if (pxlbrightness[i] > 0) + fast_color_add(framebuffer[pixco[i][0]][pixco[i][1]], color, pxlbrightness[i]); // order is: bottom left, bottom right, top right, top left + } + } + else + { + for(uint32_t i = 0; i < 4; i++) + { + if (pxlbrightness[i] > 0) + SEGMENT.addPixelColorXY(pixco[i][0], maxYpixel - pixco[i][1], color.scale8((uint8_t)pxlbrightness[i])); + } + } + } /* - // debug: check coordinates if out of buffer boundaries print out some info (rendering out of bounds particle causes crash!) - for (uint32_t d = 0; d < 4; d++) - { - if (pixco[d][0] < 0 || pixco[d][0] > maxXpixel) - { - //Serial.print("<"); - if (pxlbrightness[d] >= 0) - { - Serial.print("uncought out of bounds: x:"); - Serial.print(pixco[d][0]); - Serial.print(" y:"); - Serial.print(pixco[d][1]); - Serial.print("particle x="); - Serial.print(particles[particleindex].x); - Serial.print(" y="); - Serial.println(particles[particleindex].y); - pxlbrightness[d] = -1; // do not render - } - } - if (pixco[d][1] < 0 || pixco[d][1] > maxYpixel) - { - //Serial.print("^"); - if (pxlbrightness[d] >= 0) - { - Serial.print("uncought out of bounds: y:"); - Serial.print(pixco[d][0]); - Serial.print(" y:"); - Serial.print(pixco[d][1]); - Serial.print("particle x="); - Serial.print(particles[particleindex].x); - Serial.print(" y="); - Serial.println(particles[particleindex].y); - pxlbrightness[d] = -1; // do not render - } - } - } + // debug: check coordinates if out of buffer boundaries print out some info (rendering out of bounds particle causes crash!) + for (uint32_t d = 0; d < 4; d++) + { + if (pixco[d][0] < 0 || pixco[d][0] > maxXpixel) + { + //Serial.print("<"); + if (pxlbrightness[d] >= 0) + { + Serial.print("uncought out of bounds: x:"); + Serial.print(pixco[d][0]); + Serial.print(" y:"); + Serial.print(pixco[d][1]); + Serial.print("particle x="); + Serial.print(particles[particleindex].x); + Serial.print(" y="); + Serial.println(particles[particleindex].y); + pxlbrightness[d] = -1; // do not render + } + } + if (pixco[d][1] < 0 || pixco[d][1] > maxYpixel) + { + //Serial.print("^"); + if (pxlbrightness[d] >= 0) + { + Serial.print("uncought out of bounds: y:"); + Serial.print(pixco[d][0]); + Serial.print(" y:"); + Serial.print(pixco[d][1]); + Serial.print("particle x="); + Serial.print(particles[particleindex].x); + Serial.print(" y="); + Serial.println(particles[particleindex].y); + pxlbrightness[d] = -1; // do not render + } + } + } */ } @@ -1060,252 +1059,252 @@ void ParticleSystem::renderParticle(CRGB **framebuffer, uint32_t particleindex, // particles move upwards faster if ttl is high (i.e. they are hotter) void ParticleSystem::fireParticleupdate() { - //TODO: cleanup this function? check if normal move is much slower, change move function to check y first then this function just needs to add ttl to y befor calling normal move function - uint32_t i = 0; - - for (i = 0; i < usedParticles; i++) - { - if (particles[i].ttl > 0) - { - // age - particles[i].ttl--; - // apply velocity - particles[i].x = particles[i].x + (int32_t)particles[i].vx; - particles[i].y = particles[i].y + (int32_t)particles[i].vy + (particles[i].ttl >> 2); // younger particles move faster upward as they are hotter - //particles[i].y = particles[i].y + (int32_t)particles[i].vy;// + (particles[i].ttl >> 3); // younger particles move faster upward as they are hotter //this is experimental, different shifting - 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 speed, only check x direction if y is not out of bounds - // y-direction - if (particles[i].y < -PS_P_HALFRADIUS) - particles[i].outofbounds = 1; - else if (particles[i].y > maxY + PS_P_HALFRADIUS) // particle moved out at the top - particles[i].ttl = 0; - else // particle is in frame in y direction, also check x direction now - { - if ((particles[i].x < 0) || (particles[i].x > maxX)) - { - if (particlesettings.wrapX) - { - particles[i].x = (uint16_t)particles[i].x % (maxX + 1); - } - else if ((particles[i].x < -PS_P_HALFRADIUS) || (particles[i].x > maxX + PS_P_HALFRADIUS)) //if fully out of view - { - particles[i].ttl = 0; - } - } - } - } - } + //TODO: cleanup this function? check if normal move is much slower, change move function to check y first then this function just needs to add ttl to y befor calling normal move function + uint32_t i = 0; + + for (i = 0; i < usedParticles; i++) + { + if (particles[i].ttl > 0) + { + // age + particles[i].ttl--; + // apply velocity + particles[i].x = particles[i].x + (int32_t)particles[i].vx; + particles[i].y = particles[i].y + (int32_t)particles[i].vy + (particles[i].ttl >> 2); // younger particles move faster upward as they are hotter + //particles[i].y = particles[i].y + (int32_t)particles[i].vy;// + (particles[i].ttl >> 3); // younger particles move faster upward as they are hotter //this is experimental, different shifting + 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 speed, only check x direction if y is not out of bounds + // y-direction + if (particles[i].y < -PS_P_HALFRADIUS) + particles[i].outofbounds = 1; + else if (particles[i].y > maxY + PS_P_HALFRADIUS) // particle moved out at the top + particles[i].ttl = 0; + else // particle is in frame in y direction, also check x direction now + { + if ((particles[i].x < 0) || (particles[i].x > maxX)) + { + if (particlesettings.wrapX) + { + particles[i].x = (uint16_t)particles[i].x % (maxX + 1); + } + else if ((particles[i].x < -PS_P_HALFRADIUS) || (particles[i].x > maxX + PS_P_HALFRADIUS)) //if fully out of view + { + particles[i].ttl = 0; + } + } + } + } + } } // detect collisions in an array of particles and handle them void ParticleSystem::handleCollisions() { - // detect and handle collisions - uint32_t i, j; - uint32_t startparticle = 0; - uint32_t endparticle = usedParticles >> 1; // do half the particles, significantly speeds things up - // 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 more accurate collisions are needed, just call it twice in a row - if (collisioncounter & 0x01) - { - startparticle = endparticle; - 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].outofbounds == 0 && particles[i].collide) // if particle is alive and does collide and is not out of view - { - int32_t dx, dy; // distance to other particles - for (j = i + 1; j < usedParticles; j++) // check against higher number particles - { - if (particles[j].ttl > 0) // if target particle is alive - { - dx = particles[i].x - particles[j].x; - if(advPartProps) //may be using individual particle size - { - particleHardRadius = PS_P_MINHARDRADIUS + particlesize + (((uint32_t)advPartProps[i].size + (uint32_t)advPartProps[j].size)>>1); // collision distance - } - if (dx < particleHardRadius && dx > -particleHardRadius) // check x direction, if close, check y direction - { - dy = particles[i].y - particles[j].y; - if (dy < particleHardRadius && dy > -particleHardRadius) // particles are close - collideParticles(&particles[i], &particles[j]); - } - } - } - } - } + // detect and handle collisions + uint32_t i, j; + uint32_t startparticle = 0; + uint32_t endparticle = usedParticles >> 1; // do half the particles, significantly speeds things up + // 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 more accurate collisions are needed, just call it twice in a row + if (collisioncounter & 0x01) + { + startparticle = endparticle; + 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].outofbounds == 0 && particles[i].collide) // if particle is alive and does collide and is not out of view + { + int32_t dx, dy; // distance to other particles + for (j = i + 1; j < usedParticles; j++) // check against higher number particles + { + if (particles[j].ttl > 0) // if target particle is alive + { + dx = particles[i].x - particles[j].x; + if (advPartProps) //may be using individual particle size + { + particleHardRadius = PS_P_MINHARDRADIUS + particlesize + (((uint32_t)advPartProps[i].size + (uint32_t)advPartProps[j].size)>>1); // collision distance + } + if (dx < particleHardRadius && dx > -particleHardRadius) // check x direction, if close, check y direction + { + dy = particles[i].y - particles[j].y; + if (dy < particleHardRadius && dy > -particleHardRadius) // particles are close + collideParticles(&particles[i], &particles[j]); + } + } + } + } + } } // handle a collision if close proximity is detected, i.e. dx and/or dy smaller than 2*PS_P_RADIUS // takes two pointers to the particles to collide and the particle hardness (softer means more energy lost in collision, 255 means full hard) void ParticleSystem::collideParticles(PSparticle *particle1, PSparticle *particle2) // TODO: dx,dy is calculated just above, can pass it over here to save a few CPU cycles? { - int32_t dx = particle2->x - particle1->x; - int32_t dy = particle2->y - particle1->y; - int32_t distanceSquared = dx * dx + dy * dy; - // Calculate relative velocity (if it is zero, could exit but extra check does not overall speed but deminish it) - int32_t relativeVx = (int16_t)particle2->vx - (int16_t)particle1->vx; - int32_t relativeVy = (int16_t)particle2->vy - (int16_t)particle1->vy; - - // if dx and dy are zero (i.e. they meet at the center) give them an offset, if speeds are also zero, also offset them (pushes them apart if they are clumped before enabling collisions) - if (distanceSquared == 0) - { - // Adjust positions based on relative velocity direction - dx = -1; - if (relativeVx < 0) // if true, particle2 is on the right side - dx = 1; - else if(relativeVx == 0) - relativeVx = 1; - - dy = -1; - if (relativeVy < 0) - dy = 1; - else if (relativeVy == 0) - relativeVy = 1; - - distanceSquared = 2; //1 + 1 - } - - // Calculate dot product of relative velocity and relative distance - int32_t dotProduct = (dx * relativeVx + dy * relativeVy); // is always negative if moving towards each other - int32_t notsorandom = dotProduct & 0x01; //dotprouct LSB should be somewhat random, so no need to calculate a random number - - if (dotProduct < 0) // particles are moving towards each other - { - // integer math used to avoid floats. - // overflow check: dx/dy are 7bit, relativV are 8bit -> dotproduct is 15bit, dotproduct/distsquared ist 8b, multiplied by collisionhardness of 8bit. so a 16bit shift is ok, make it 15 to be sure no overflows happen - // note: cannot use right shifts as bit shifting in right direction is asymmetrical for positive and negative numbers and this needs to be accurate! the trick is: only shift positive numers - // Calculate new velocities after collision - uint32_t surfacehardness = collisionHardness < PS_P_MINSURFACEHARDNESS ? PS_P_MINSURFACEHARDNESS : collisionHardness; // if particles are soft, the impulse must stay above a limit or collisions slip through at higher speeds, 170 seems to be a good value - int32_t impulse = -(((((-dotProduct) << 15) / distanceSquared) * surfacehardness) >> 8); // note: inverting before bitshift corrects for asymmetry in right-shifts (and is slightly faster) - int32_t ximpulse = ((impulse) * dx) / 32767; // cannot use bit shifts here, it can be negative, use division by 2^bitshift - int32_t yimpulse = ((impulse) * dy) / 32767; - particle1->vx += ximpulse; - particle1->vy += yimpulse; - particle2->vx -= ximpulse; - particle2->vy -= yimpulse; - - if (collisionHardness < surfacehardness) // if particles are soft, they become 'sticky' i.e. apply some friction (they do pile more nicely) - { - const uint32_t coeff = collisionHardness + (255 - PS_P_MINSURFACEHARDNESS); - particle1->vx = ((int32_t)particle1->vx * coeff) / 255; - particle1->vy = ((int32_t)particle1->vy * coeff) / 255; - - particle2->vx = ((int32_t)particle2->vx * coeff) / 255; - particle2->vy = ((int32_t)particle2->vy * coeff) / 255; - - if (collisionHardness < 10) // if they are very soft, stop slow particles completely to make them stick to each other - { - particle1->vx = (particle1->vx < 3 && particle1->vx > -3) ? 0 : particle1->vx; - particle1->vy = (particle1->vy < 3 && particle1->vy > -3) ? 0 : particle1->vy; - - particle2->vx = (particle2->vx < 3 && particle2->vx > -3) ? 0 : particle2->vx; - particle2->vy = (particle2->vy < 3 && particle2->vy > -3) ? 0 : particle2->vy; - } - } - - // particles have volume, push particles apart if they are too close - // tried lots of configurations, it works best if not moved but given a little velocity, it tends to oscillate less this way - // a problem with giving velocity is, that on harder collisions, this adds up as it is not dampened enough, so add friction in the FX if required - if (dotProduct > -250) //this means particles are slow (or really really close) so push them apart. - { - int32_t pushamount = 1 + ((250 + dotProduct) >> 6); // the closer dotproduct is to zero, the closer the particles are - int32_t push = 0; - if (dx < 0) // particle 1 is on the right - push = pushamount; - else if (dx > 0) - push = -pushamount; - else // on the same x coordinate, shift it a little so they do not stack - { - if (notsorandom) - particle1->x++; // move it so pile collapses - else - particle1->x--; - } - particle1->vx += push; - push = 0; - if (dy < 0) - push = pushamount; - else if (dy > 0) - push = -pushamount; - else // dy==0 - { - if (notsorandom) - particle1->y++; // move it so pile collapses - else - particle1->y--; - } - particle1->vy += push; - // note: pushing may push particles out of frame, if bounce is active, it will move it back as position will be limited to within frame, if bounce is disabled: bye bye - } - } + int32_t dx = particle2->x - particle1->x; + int32_t dy = particle2->y - particle1->y; + int32_t distanceSquared = dx * dx + dy * dy; + // Calculate relative velocity (if it is zero, could exit but extra check does not overall speed but deminish it) + int32_t relativeVx = (int16_t)particle2->vx - (int16_t)particle1->vx; + int32_t relativeVy = (int16_t)particle2->vy - (int16_t)particle1->vy; + + // if dx and dy are zero (i.e. they meet at the center) give them an offset, if speeds are also zero, also offset them (pushes them apart if they are clumped before enabling collisions) + if (distanceSquared == 0) + { + // Adjust positions based on relative velocity direction + dx = -1; + if (relativeVx < 0) // if true, particle2 is on the right side + dx = 1; + else if (relativeVx == 0) + relativeVx = 1; + + dy = -1; + if (relativeVy < 0) + dy = 1; + else if (relativeVy == 0) + relativeVy = 1; + + distanceSquared = 2; //1 + 1 + } + + // Calculate dot product of relative velocity and relative distance + int32_t dotProduct = (dx * relativeVx + dy * relativeVy); // is always negative if moving towards each other + int32_t notsorandom = dotProduct & 0x01; //dotprouct LSB should be somewhat random, so no need to calculate a random number + + if (dotProduct < 0) // particles are moving towards each other + { + // integer math used to avoid floats. + // overflow check: dx/dy are 7bit, relativV are 8bit -> dotproduct is 15bit, dotproduct/distsquared ist 8b, multiplied by collisionhardness of 8bit. so a 16bit shift is ok, make it 15 to be sure no overflows happen + // note: cannot use right shifts as bit shifting in right direction is asymmetrical for positive and negative numbers and this needs to be accurate! the trick is: only shift positive numers + // Calculate new velocities after collision + uint32_t surfacehardness = collisionHardness < PS_P_MINSURFACEHARDNESS ? PS_P_MINSURFACEHARDNESS : collisionHardness; // if particles are soft, the impulse must stay above a limit or collisions slip through at higher speeds, 170 seems to be a good value + int32_t impulse = -(((((-dotProduct) << 15) / distanceSquared) * surfacehardness) >> 8); // note: inverting before bitshift corrects for asymmetry in right-shifts (and is slightly faster) + int32_t ximpulse = ((impulse) * dx) / 32767; // cannot use bit shifts here, it can be negative, use division by 2^bitshift + int32_t yimpulse = ((impulse) * dy) / 32767; + particle1->vx += ximpulse; + particle1->vy += yimpulse; + particle2->vx -= ximpulse; + particle2->vy -= yimpulse; + + if (collisionHardness < surfacehardness) // if particles are soft, they become 'sticky' i.e. apply some friction (they do pile more nicely) + { + const uint32_t coeff = collisionHardness + (255 - PS_P_MINSURFACEHARDNESS); + particle1->vx = ((int32_t)particle1->vx * coeff) / 255; + particle1->vy = ((int32_t)particle1->vy * coeff) / 255; + + particle2->vx = ((int32_t)particle2->vx * coeff) / 255; + particle2->vy = ((int32_t)particle2->vy * coeff) / 255; + + if (collisionHardness < 10) // if they are very soft, stop slow particles completely to make them stick to each other + { + particle1->vx = (particle1->vx < 3 && particle1->vx > -3) ? 0 : particle1->vx; + particle1->vy = (particle1->vy < 3 && particle1->vy > -3) ? 0 : particle1->vy; + + particle2->vx = (particle2->vx < 3 && particle2->vx > -3) ? 0 : particle2->vx; + particle2->vy = (particle2->vy < 3 && particle2->vy > -3) ? 0 : particle2->vy; + } + } + + // particles have volume, push particles apart if they are too close + // tried lots of configurations, it works best if not moved but given a little velocity, it tends to oscillate less this way + // a problem with giving velocity is, that on harder collisions, this adds up as it is not dampened enough, so add friction in the FX if required + if (dotProduct > -250) //this means particles are slow (or really really close) so push them apart. + { + int32_t pushamount = 1 + ((250 + dotProduct) >> 6); // the closer dotproduct is to zero, the closer the particles are + int32_t push = 0; + if (dx < 0) // particle 1 is on the right + push = pushamount; + else if (dx > 0) + push = -pushamount; + else // on the same x coordinate, shift it a little so they do not stack + { + if (notsorandom) + particle1->x++; // move it so pile collapses + else + particle1->x--; + } + particle1->vx += push; + push = 0; + if (dy < 0) + push = pushamount; + else if (dy > 0) + push = -pushamount; + else // dy==0 + { + if (notsorandom) + particle1->y++; // move it so pile collapses + else + particle1->y--; + } + particle1->vy += push; + // note: pushing may push particles out of frame, if bounce is active, it will move it back as position will be limited to within frame, if bounce is disabled: bye bye + } + } } // calculate the delta speed (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) { - if(force == 0) - return 0; - // 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 : 1; // force is either, 1 or -1 if it is small (zero force is handled above) - } - } - else - { - dv = force >> 4; // MSBs - } - return dv; + if (force == 0) + return 0; + // 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 : 1; // force is either, 1 or -1 if it is small (zero force is handled above) + } + } + else + { + dv = force >> 4; // MSBs + } + return dv; } // limit speed to prevent overflows int32_t ParticleSystem::limitSpeed(int32_t speed) { - return speed > PS_P_MAXSPEED ? PS_P_MAXSPEED : (speed < -PS_P_MAXSPEED ? -PS_P_MAXSPEED : speed); + return speed > PS_P_MAXSPEED ? PS_P_MAXSPEED : (speed < -PS_P_MAXSPEED ? -PS_P_MAXSPEED : speed); } // allocate memory for the 2D array in one contiguous block and set values to zero CRGB **ParticleSystem::allocate2Dbuffer(uint32_t cols, uint32_t rows) -{ - CRGB ** array2D = (CRGB **)calloc(cols, sizeof(CRGB *) + rows * sizeof(CRGB)); - if (array2D == NULL) - DEBUG_PRINT(F("PS buffer alloc failed")); - else - { - // assign pointers of 2D array - CRGB *start = (CRGB *)(array2D + cols); - for (uint i = 0; i < cols; i++) - { - array2D[i] = start + i * rows; - } - } - return array2D; +{ + CRGB ** array2D = (CRGB **)calloc(cols, sizeof(CRGB *) + rows * sizeof(CRGB)); + if (array2D == NULL) + DEBUG_PRINT(F("PS buffer alloc failed")); + else + { + // assign pointers of 2D array + CRGB *start = (CRGB *)(array2D + cols); + for (uint i = 0; i < cols; i++) + { + array2D[i] = start + i * rows; + } + } + return array2D; } // update size and pointers (memory location and size can change dynamically) // note: do not access the PS class in FX befor running this function (or it messes up SEGMENT.data) void ParticleSystem::updateSystem(void) { - // update matrix size - uint32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; - uint32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); - setMatrixSize(cols, rows); - updatePSpointers(advPartProps != NULL, advPartSize != NULL); + // update matrix size + uint32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; + uint32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); + setMatrixSize(cols, rows); + updatePSpointers(advPartProps != NULL, advPartSize != NULL); } // set the pointers for the class (this only has to be done once and not on every FX call, only the class pointer needs to be reassigned to SEGENV.data every time) @@ -1313,124 +1312,124 @@ void ParticleSystem::updateSystem(void) // FX handles the PSsources, need to tell this function how many there are void ParticleSystem::updatePSpointers(bool isadvanced, bool sizecontrol) { - // DEBUG_PRINT(F("*** PS pointers ***")); - // DEBUG_PRINTF_P(PSTR("this PS %p "), this); - // Note on memory alignment: - // a pointer MUST be 4 byte aligned. sizeof() in a struct/class is always aligned to the largest element. if it contains a 32bit, it will be padded to 4 bytes, 16bit is padded to 2byte alignment. - // The PS is aligned to 4 bytes, a PSparticle is aligned to 2 and a struct containing only byte sized variables is not aligned at all and may need to be padded when dividing the memoryblock. - // by making sure that the number of sources and particles is a multiple of 4, padding can be skipped here as alignent is ensured, independent of struct sizes. - particles = reinterpret_cast(this + 1); // pointer to particle array at data+sizeof(ParticleSystem) - sources = reinterpret_cast(particles + numParticles); // pointer to source(s) - if(isadvanced) - { - advPartProps = reinterpret_cast(sources + numSources); - PSdataEnd = reinterpret_cast(advPartProps + numParticles); - if(sizecontrol) - { - advPartSize = reinterpret_cast(advPartProps + numParticles); - PSdataEnd = reinterpret_cast(advPartSize + numParticles); - } - } - else - { - PSdataEnd = reinterpret_cast(sources + numSources); // pointer to first available byte after the PS for FX additional data - } - /* - DEBUG_PRINTF_P(PSTR(" particles %p "), particles); - DEBUG_PRINTF_P(PSTR(" sources %p "), sources); - DEBUG_PRINTF_P(PSTR(" adv. props %p "), advPartProps); - DEBUG_PRINTF_P(PSTR(" adv. ctrl %p "), advPartSize); - DEBUG_PRINTF_P(PSTR("end %p\n"), PSdataEnd); - */ + // DEBUG_PRINT(F("*** PS pointers ***")); + // DEBUG_PRINTF_P(PSTR("this PS %p "), this); + // Note on memory alignment: + // a pointer MUST be 4 byte aligned. sizeof() in a struct/class is always aligned to the largest element. if it contains a 32bit, it will be padded to 4 bytes, 16bit is padded to 2byte alignment. + // The PS is aligned to 4 bytes, a PSparticle is aligned to 2 and a struct containing only byte sized variables is not aligned at all and may need to be padded when dividing the memoryblock. + // by making sure that the number of sources and particles is a multiple of 4, padding can be skipped here as alignent is ensured, independent of struct sizes. + particles = reinterpret_cast(this + 1); // pointer to particle array at data+sizeof(ParticleSystem) + sources = reinterpret_cast(particles + numParticles); // pointer to source(s) + if (isadvanced) + { + advPartProps = reinterpret_cast(sources + numSources); + PSdataEnd = reinterpret_cast(advPartProps + numParticles); + if (sizecontrol) + { + advPartSize = reinterpret_cast(advPartProps + numParticles); + PSdataEnd = reinterpret_cast(advPartSize + numParticles); + } + } + else + { + PSdataEnd = reinterpret_cast(sources + numSources); // pointer to first available byte after the PS for FX additional data + } + /* + DEBUG_PRINTF_P(PSTR(" particles %p "), particles); + DEBUG_PRINTF_P(PSTR(" sources %p "), sources); + DEBUG_PRINTF_P(PSTR(" adv. props %p "), advPartProps); + DEBUG_PRINTF_P(PSTR(" adv. ctrl %p "), advPartSize); + DEBUG_PRINTF_P(PSTR("end %p\n"), PSdataEnd); + */ } //non class functions to use for initialization uint32_t calculateNumberOfParticles(bool isadvanced, bool sizecontrol) { - uint32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; - uint32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); + uint32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; + uint32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); #ifdef ESP8266 - uint32_t numberofParticles = (cols * rows * 3) / 4; // 0.75 particle per pixel - uint32_t particlelimit = ESP8266_MAXPARTICLES; // maximum number of paticles allowed (based on one segment of 16x16 and 4k effect ram) + uint32_t numberofParticles = (cols * rows * 3) / 4; // 0.75 particle per pixel + uint32_t particlelimit = ESP8266_MAXPARTICLES; // maximum number of paticles allowed (based on one segment of 16x16 and 4k effect ram) #elif ARDUINO_ARCH_ESP32S2 - uint32_t numberofParticles = (cols * rows); // 1 particle per pixel - uint32_t particlelimit = ESP32S2_MAXPARTICLES; // maximum number of paticles allowed (based on one segment of 32x32 and 24k effect ram) + uint32_t numberofParticles = (cols * rows); // 1 particle per pixel + uint32_t particlelimit = ESP32S2_MAXPARTICLES; // maximum number of paticles allowed (based on one segment of 32x32 and 24k effect ram) #else - uint32_t numberofParticles = (cols * rows); // 1 particle per pixel (for example 512 particles on 32x16) - uint32_t particlelimit = ESP32_MAXPARTICLES; // maximum number of paticles allowed (based on two segments of 32x32 and 40k effect ram) + uint32_t numberofParticles = (cols * rows); // 1 particle per pixel (for example 512 particles on 32x16) + uint32_t particlelimit = ESP32_MAXPARTICLES; // maximum number of paticles allowed (based on two segments of 32x32 and 40k effect ram) #endif - numberofParticles = max((uint32_t)1, min(numberofParticles, particlelimit)); - if (isadvanced) // advanced property array needs ram, reduce number of particles to use the same amount - numberofParticles = (numberofParticles * sizeof(PSparticle)) / (sizeof(PSparticle) + sizeof(PSadvancedParticle)); - if (sizecontrol) // advanced property array needs ram, reduce number of particles to use the same amount - numberofParticles /= 8; // if size control is used, much fewer particles are needed - - //make sure it is a multiple of 4 for proper memory alignment (easier than using padding bytes) - numberofParticles = ((numberofParticles+3) >> 2) << 2; - return numberofParticles; + numberofParticles = max((uint32_t)1, min(numberofParticles, particlelimit)); + if (isadvanced) // advanced property array needs ram, reduce number of particles to use the same amount + numberofParticles = (numberofParticles * sizeof(PSparticle)) / (sizeof(PSparticle) + sizeof(PSadvancedParticle)); + if (sizecontrol) // advanced property array needs ram, reduce number of particles to use the same amount + numberofParticles /= 8; // if size control is used, much fewer particles are needed + + //make sure it is a multiple of 4 for proper memory alignment (easier than using padding bytes) + numberofParticles = ((numberofParticles+3) >> 2) << 2; + return numberofParticles; } uint32_t calculateNumberOfSources(uint8_t requestedsources) { - uint32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; - uint32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); + uint32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; + uint32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); #ifdef ESP8266 - int numberofSources = min((cols * rows) / 8, (uint32_t)requestedsources); - numberofSources = max(1, min(numberofSources, ESP8266_MAXSOURCES)); // limit to 1 - 16 + int numberofSources = min((cols * rows) / 8, (uint32_t)requestedsources); + numberofSources = max(1, min(numberofSources, ESP8266_MAXSOURCES)); // limit to 1 - 16 #elif ARDUINO_ARCH_ESP32S2 - int numberofSources = min((cols * rows) / 6, (uint32_t)requestedsources); - numberofSources = max(1, min(numberofSources, ESP32S2_MAXSOURCES)); // limit to 1 - 48 + int numberofSources = min((cols * rows) / 6, (uint32_t)requestedsources); + numberofSources = max(1, min(numberofSources, ESP32S2_MAXSOURCES)); // limit to 1 - 48 #else - int numberofSources = min((cols * rows) / 4, (uint32_t)requestedsources); - numberofSources = max(1, min(numberofSources, ESP32_MAXSOURCES)); // limit to 1 - 64 + int numberofSources = min((cols * rows) / 4, (uint32_t)requestedsources); + numberofSources = max(1, min(numberofSources, ESP32_MAXSOURCES)); // limit to 1 - 64 #endif - // make sure it is a multiple of 4 for proper memory alignment - numberofSources = ((numberofSources+3) >> 2) << 2; - return numberofSources; + // make sure it is a multiple of 4 for proper memory alignment + numberofSources = ((numberofSources+3) >> 2) << 2; + return numberofSources; } //allocate memory for particle system class, particles, sprays plus additional memory requested by FX bool allocateParticleSystemMemory(uint16_t numparticles, uint16_t numsources, bool isadvanced, bool sizecontrol, uint16_t additionalbytes) { - uint32_t requiredmemory = sizeof(ParticleSystem); - // functions above make sure these are a multiple of 4 bytes (to avoid alignment issues) - requiredmemory += sizeof(PSparticle) * numparticles; - if (isadvanced) - requiredmemory += sizeof(PSadvancedParticle) * numparticles; - if (sizecontrol) - requiredmemory += sizeof(PSsizeControl) * numparticles; - requiredmemory += sizeof(PSsource) * numsources; - requiredmemory += additionalbytes; - //Serial.print("allocating: "); - //Serial.print(requiredmemory); - //Serial.println("Bytes"); - //Serial.print("allocating for segment at"); - //Serial.println((uintptr_t)SEGMENT.data); - return(SEGMENT.allocateData(requiredmemory)); + uint32_t requiredmemory = sizeof(ParticleSystem); + // functions above make sure these are a multiple of 4 bytes (to avoid alignment issues) + requiredmemory += sizeof(PSparticle) * numparticles; + if (isadvanced) + requiredmemory += sizeof(PSadvancedParticle) * numparticles; + if (sizecontrol) + requiredmemory += sizeof(PSsizeControl) * numparticles; + requiredmemory += sizeof(PSsource) * numsources; + requiredmemory += additionalbytes; + //Serial.print("allocating: "); + //Serial.print(requiredmemory); + //Serial.println("Bytes"); + //Serial.print("allocating for segment at"); + //Serial.println((uintptr_t)SEGMENT.data); + return(SEGMENT.allocateData(requiredmemory)); } // initialize Particle System, allocate additional bytes if needed (pointer to those bytes can be read from particle system class: PSdataEnd) bool initParticleSystem(ParticleSystem *&PartSys, uint8_t requestedsources, uint16_t additionalbytes, bool largesizes, bool sizecontrol) { - //Serial.println("PS init function"); - uint32_t numparticles = calculateNumberOfParticles(largesizes, sizecontrol); - uint32_t numsources = calculateNumberOfSources(requestedsources); - //Serial.print("numsources: "); - //Serial.println(numsources); - if (!allocateParticleSystemMemory(numparticles, numsources, largesizes, sizecontrol, additionalbytes)) - { - DEBUG_PRINT(F("PS init failed: memory depleted")); - return false; - } - //Serial.print("segment.data ptr"); - //Serial.println((uintptr_t)(SEGMENT.data)); - 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, largesizes, sizecontrol); // particle system constructor - //Serial.print("PS pointer at "); - //Serial.println((uintptr_t)PartSys); - return true; + //Serial.println("PS init function"); + uint32_t numparticles = calculateNumberOfParticles(largesizes, sizecontrol); + uint32_t numsources = calculateNumberOfSources(requestedsources); + //Serial.print("numsources: "); + //Serial.println(numsources); + if (!allocateParticleSystemMemory(numparticles, numsources, largesizes, sizecontrol, additionalbytes)) + { + DEBUG_PRINT(F("PS init failed: memory depleted")); + return false; + } + //Serial.print("segment.data ptr"); + //Serial.println((uintptr_t)(SEGMENT.data)); + 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, largesizes, sizecontrol); // particle system constructor + //Serial.print("PS pointer at "); + //Serial.println((uintptr_t)PartSys); + return true; } /////////////////////// @@ -1443,43 +1442,42 @@ bool initParticleSystem(ParticleSystem *&PartSys, uint8_t requestedsources, uint // note: result is stored in c1, so c1 will contain the result. not using a return value is much faster as the struct does not need to be copied upon return void fast_color_add(CRGB &c1, CRGB &c2, uint32_t scale) { - uint32_t r, g, b; - if(scale < 255) - { - r = c1.r + ((c2.r * scale) >> 8); - g = c1.g + ((c2.g * scale) >> 8); - b = c1.b + ((c2.b * scale) >> 8); - } - else{ - r = c1.r + c2.r; - g = c1.g + c2.g; - b = c1.b + c2.b; - } - uint32_t max = r; - if (g > max) // note: using ? operator would be slower by 2 instructions - max = g; - if (b > max) - max = b; - if (max < 256) - { - c1.r = r; // save result to c1 - c1.g = g; - c1.b = b; - } - else - { - c1.r = (r * 255) / max; - c1.g = (g * 255) / max; - c1.b = (b * 255) / max; - } + uint32_t r, g, b; + if (scale < 255) { + r = c1.r + ((c2.r * scale) >> 8); + g = c1.g + ((c2.g * scale) >> 8); + b = c1.b + ((c2.b * scale) >> 8); + } + else { + r = c1.r + c2.r; + g = c1.g + c2.g; + b = c1.b + c2.b; + } + uint32_t max = r; + if (g > max) // note: using ? operator would be slower by 2 instructions + max = g; + if (b > max) + max = b; + if (max < 256) + { + c1.r = r; // save result to c1 + c1.g = g; + c1.b = b; + } + else + { + c1.r = (r * 255) / max; + c1.g = (g * 255) / max; + c1.b = (b * 255) / max; + } } // faster than fastled color scaling as it uses a 32bit scale factor and pointer void fast_color_scale(CRGB &c, uint32_t scale) { - c.r = ((c.r * scale) >> 8); - c.g = ((c.g * scale) >> 8); - c.b = ((c.b * scale) >> 8); + c.r = ((c.r * scale) >> 8); + c.g = ((c.g * scale) >> 8); + c.b = ((c.b * scale) >> 8); } // blur a matrix in x and y direction, blur can be asymmetric in x and y @@ -1487,57 +1485,57 @@ void fast_color_scale(CRGB &c, uint32_t scale) // to blur a subset of the buffer, change the xsize/ysize and set xstart/ystart to the desired starting coordinates (default start is 0/0) void blur2D(CRGB **colorbuffer, uint32_t xsize, uint32_t ysize, uint32_t xblur, uint32_t yblur, bool smear, uint32_t xstart, uint32_t ystart, bool isparticle) { - CRGB seeppart, carryover; - uint32_t seep = xblur >> 1; - if(isparticle) //first and last row are always black in particle rendering - { - ystart++; - ysize--; - } - for(uint32_t y = ystart; y < ystart + ysize; y++) - { - carryover = BLACK; - for(uint32_t x = xstart; x < xstart + xsize; x++) - { - seeppart = colorbuffer[x][y]; // create copy of current color - fast_color_scale(seeppart, seep); // scale it and seep to neighbours - if(!smear) // fade current pixel if smear is disabled - fast_color_scale(colorbuffer[x][y], 255 - xblur); - - if(x > 0) - { - fast_color_add(colorbuffer[x-1][y], seeppart); - fast_color_add(colorbuffer[x][y], carryover); // TODO: could check if carryover is > 0, takes 7 instructions, add takes ~35, with lots of same color pixels (like background), it would be faster - } - carryover = seeppart; - } - fast_color_add(colorbuffer[xsize-1][y], carryover); // set last pixel - } - - if(isparticle) // now also do first and last row - { - ystart--; - ysize++; - } - - seep = yblur >> 1; - for(uint32_t x = xstart; x < xstart + xsize; x++) - { - carryover = BLACK; - for(uint32_t y = ystart; y < ystart + ysize; y++) - { - seeppart = colorbuffer[x][y]; // create copy of current color - fast_color_scale(seeppart, seep); // scale it and seep to neighbours - if(!smear) // fade current pixel if smear is disabled - fast_color_scale(colorbuffer[x][y], 255 - yblur); - - if(y > 0) - { - fast_color_add(colorbuffer[x][y-1], seeppart); - fast_color_add(colorbuffer[x][y], carryover); // todo: could check if carryover is > 0, takes 7 instructions, add takes ~35, with lots of same color pixels (like background), it would be faster - } - carryover = seeppart; - } - fast_color_add(colorbuffer[x][ysize-1], carryover); // set last pixel - } + CRGB seeppart, carryover; + uint32_t seep = xblur >> 1; + if (isparticle) //first and last row are always black in particle rendering + { + ystart++; + ysize--; + } + for(uint32_t y = ystart; y < ystart + ysize; y++) + { + carryover = BLACK; + for(uint32_t x = xstart; x < xstart + xsize; x++) + { + seeppart = colorbuffer[x][y]; // create copy of current color + fast_color_scale(seeppart, seep); // scale it and seep to neighbours + if (!smear) // fade current pixel if smear is disabled + fast_color_scale(colorbuffer[x][y], 255 - xblur); + + if (x > 0) + { + fast_color_add(colorbuffer[x-1][y], seeppart); + fast_color_add(colorbuffer[x][y], carryover); // TODO: could check if carryover is > 0, takes 7 instructions, add takes ~35, with lots of same color pixels (like background), it would be faster + } + carryover = seeppart; + } + fast_color_add(colorbuffer[xsize-1][y], carryover); // set last pixel + } + + if (isparticle) // now also do first and last row + { + ystart--; + ysize++; + } + + seep = yblur >> 1; + for(uint32_t x = xstart; x < xstart + xsize; x++) + { + carryover = BLACK; + for(uint32_t y = ystart; y < ystart + ysize; y++) + { + seeppart = colorbuffer[x][y]; // create copy of current color + fast_color_scale(seeppart, seep); // scale it and seep to neighbours + if (!smear) // fade current pixel if smear is disabled + fast_color_scale(colorbuffer[x][y], 255 - yblur); + + if (y > 0) + { + fast_color_add(colorbuffer[x][y-1], seeppart); + fast_color_add(colorbuffer[x][y], carryover); // todo: could check if carryover is > 0, takes 7 instructions, add takes ~35, with lots of same color pixels (like background), it would be faster + } + carryover = seeppart; + } + fast_color_add(colorbuffer[x][ysize-1], carryover); // set last pixel + } } \ No newline at end of file diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index 281afee228..f3da35b1bf 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -22,51 +22,50 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ #include #include "FastLED.h" -//memory allocation -#define ESP8266_MAXPARTICLES 180// // enough for one 16x16 segment with transitions +// memory allocation +#define ESP8266_MAXPARTICLES 180 // enough for one 16x16 segment with transitions #define ESP8266_MAXSOURCES 16 #define ESP32S2_MAXPARTICLES 840 // enough for four 16x16 segments #define ESP32S2_MAXSOURCES 48 -#define ESP32_MAXPARTICLES 1024 // enough for four 16x16 segments TODO: not enough for one 64x64 panel... +#define ESP32_MAXPARTICLES 1024 // enough for four 16x16 segments TODO: not enough for one 64x64 panel... #define ESP32_MAXSOURCES 64 -//particle dimensions (subpixel division) -#define PS_P_RADIUS 64 //subpixel size, each pixel is divided by this for particle movement, if this value is changed, also change the shift defines (next two lines) -#define PS_P_HALFRADIUS 32 +// particle dimensions (subpixel division) +#define PS_P_RADIUS 64 // subpixel size, each pixel is divided by this for particle movement, if this value is changed, also change the shift defines (next two lines) +#define PS_P_HALFRADIUS 32 #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_MINHARDRADIUS 70 // minimum hard surface radius -#define PS_P_MINSURFACEHARDNESS 128 //minimum hardness used in collision impulse calculation, below this hardness, particles become sticky -#define PS_P_MAXSPEED 120 //maximum speed a particle can have (vx/vy is int8) +#define PS_P_MINSURFACEHARDNESS 128 // minimum hardness used in collision impulse calculation, below this hardness, particles become sticky +#define PS_P_MAXSPEED 120 // maximum speed a particle can have (vx/vy is int8) //struct for a single particle (10 bytes) 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 + 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 - uint8_t sat; //particle color saturation - //two byte bit field: + uint8_t sat; // particle color saturation + // 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 set, particle takes part in collisions - bool perpetual : 1; //if set, particle does not age (TTL is not decremented in move function, it still dies from killoutofbounds) - bool flag4 : 1; // unused flag + bool outofbounds : 1; // out of bounds flag, set to true if particle is outside of display area + bool collide : 1; // if set, particle takes part in collisions + bool perpetual : 1; // if set, particle does not age (TTL is not decremented in move function, it still dies from killoutofbounds) + bool flag4 : 1; // unused flag } PSparticle; // struct for additional particle settings (optional) typedef struct { - uint8_t size; //particle size, 255 means 10 pixels in diameter - uint8_t forcecounter; //counter for applying forces to individual particles + uint8_t size; // particle size, 255 means 10 pixels in diameter + uint8_t forcecounter; // counter for applying forces to individual particles } PSadvancedParticle; // struct for advanced particle size control (optional) @@ -74,24 +73,24 @@ typedef struct { uint8_t asymmetry; // asymmetrical size (0=symmetrical, 255 fully asymmetric) uint8_t asymdir; // direction of asymmetry, 64 is x, 192 is y (0 and 128 is symmetrical) - uint8_t maxsize; // target size for growing - uint8_t minsize; // target size for shrinking + uint8_t maxsize; // target size for growing + uint8_t minsize; // target size for shrinking uint8_t sizecounter : 4; // counters used for size contol (grow/shrink/wobble) uint8_t wobblecounter : 4; - uint8_t growspeed : 4; - uint8_t shrinkspeed : 4; + uint8_t growspeed : 4; + uint8_t shrinkspeed : 4; uint8_t wobblespeed : 4; bool grow : 1; // flags bool shrink : 1; - bool pulsate : 1; //grows & shrinks & grows & ... - bool wobble : 1; //alternate x and y size + bool pulsate : 1; // grows & shrinks & grows & ... + bool wobble : 1; // alternate x and y size } PSsizeControl; //struct for a particle source (17 bytes) typedef struct { - uint16_t minLife; // minimum ttl of emittet particles - uint16_t maxLife; // maximum ttl of emitted particles + 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 (use odd numbers for good symmetry) int8_t vx; // emitting speed @@ -103,20 +102,20 @@ typedef struct { typedef union { struct{ - // add a one byte bit field: + // one byte bit field: bool wrapX : 1; bool wrapY : 1; bool bounceX : 1; bool bounceY : 1; bool killoutofbounds : 1; // if set, out of bound particles are killed immediately - bool useGravity : 1; //set to 1 if gravity is used, disables bounceY at the top + bool useGravity : 1; // set to 1 if gravity is used, disables bounceY at the top bool useCollisions : 1; bool colorByAge : 1; // if set, particle hue is set by ttl value in render function }; - byte asByte; //order is: LSB is first entry in the list above + byte asByte; // order is: LSB is first entry in the list above } PSsettings; -//class uses approximately 60 bytes +// class uses approximately 60 bytes class ParticleSystem { public: @@ -129,49 +128,49 @@ class ParticleSystem // particle emitters int32_t sprayEmit(PSsource &emitter); void flameEmit(PSsource &emitter); - void angleEmit(PSsource& emitter, uint16_t angle, int8_t speed); + void angleEmit(PSsource& emitter, uint16_t angle, int8_t speed); + - // move functions - void particleMoveUpdate(PSparticle &part, PSsettings *options = NULL, PSadvancedParticle *advancedproperties = NULL); - //particle physics + void particleMoveUpdate(PSparticle &part, PSsettings *options = NULL, PSadvancedParticle *advancedproperties = NULL); // move function + //particle physics void applyGravity(PSparticle *part); // applies gravity to single particle (use this for sources) void applyForce(PSparticle *part, int8_t xforce, int8_t yforce, uint8_t *counter); - void applyForce(uint16_t particleindex, int8_t xforce, int8_t yforce); //use this for advanced property particles - void applyForce(int8_t xforce, int8_t yforce); //apply a force to all particles + void applyForce(uint16_t particleindex, int8_t xforce, int8_t yforce); // use this for advanced property particles + void applyForce(int8_t xforce, int8_t yforce); // apply a force to all particles void applyAngleForce(PSparticle *part, int8_t force, uint16_t angle, uint8_t *counter); void applyAngleForce(uint16_t particleindex, int8_t force, uint16_t angle); // use this for advanced property particles - void applyAngleForce(int8_t force, uint16_t angle); //apply angular force to all particles + void applyAngleForce(int8_t force, uint16_t angle); // apply angular force to all particles void applyFriction(PSparticle *part, int32_t coefficient); // apply friction to specific particle void applyFriction(int32_t coefficient); // apply friction to all used particles void pointAttractor(uint16_t particleindex, PSparticle *attractor, uint8_t strength, bool swallow); void lineAttractor(uint16_t particleindex, PSparticle *attractorcenter, uint16_t attractorangle, uint8_t strength); - //set options + // set options void setUsedParticles(uint16_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 setWallRoughness(uint8_t roughness); //wall roughness randomizes wall collisions + 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 setWallRoughness(uint8_t roughness); // wall roughness randomizes wall collisions 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 setSaturation(uint8_t sat); //set global color saturation + void setKillOutOfBounds(bool enable); // if enabled, particles outside of matrix instantly die + void setSaturation(uint8_t sat); // set global color saturation void setColorByAge(bool enable); - void setMotionBlur(uint8_t bluramount); //note: motion blur can only be used if 'particlesize' is set to zero + void setMotionBlur(uint8_t bluramount); // note: motion blur can only be used if 'particlesize' is set to zero void setParticleSize(uint8_t size); void setGravity(int8_t force = 8); - void enableParticleCollisions(bool enable, uint8_t hardness = 255); + void enableParticleCollisions(bool enable, uint8_t hardness = 255); PSparticle *particles; // pointer to particle array PSsource *sources; // pointer to sources PSadvancedParticle *advPartProps; // pointer to advanced particle properties (can be NULL) - PSsizeControl *advPartSize; //pointer to advanced particle size control (can be NULL) - uint8_t* PSdataEnd; //points to first available byte after the PSmemory, is set in setPointers(). use this for FX custom data - uint16_t maxX, maxY; //particle system size i.e. width-1 / height-1 in subpixels + PSsizeControl *advPartSize; // pointer to advanced particle size control (can be NULL) + uint8_t* PSdataEnd; // points to first available byte after the PSmemory, is set in setPointers(). use this for FX custom data + uint16_t maxX, maxY; // particle system size i.e. width-1 / height-1 in subpixels uint32_t maxXpixel, maxYpixel; // last physical pixel that can be drawn to (FX can read this to read segment size if required), equal to width-1 / height-1 - uint8_t numSources; //number of sources + uint8_t numSources; // number of sources uint16_t numParticles; // number of particles available in this system uint16_t usedParticles; // number of particles used in animation (can be smaller then numParticles) @@ -188,9 +187,9 @@ class ParticleSystem //utility functions void updatePSpointers(bool isadvanced, bool sizecontrol); // update the data pointers to current segment data space - void updateSize(PSadvancedParticle *advprops, PSsizeControl *advsize); //advanced size control + void updateSize(PSadvancedParticle *advprops, PSsizeControl *advsize); // advanced size control void getParticleXYsize(PSadvancedParticle *advprops, PSsizeControl *advsize, uint32_t &xsize, uint32_t &ysize); - void bounce(int8_t &incomingspeed, int8_t ¶llelspeed, int32_t &position, uint16_t maxposition); //bounce on a wall + void bounce(int8_t &incomingspeed, int8_t ¶llelspeed, int32_t &position, uint16_t maxposition); // bounce on a wall int16_t wraparound(uint16_t p, uint32_t maxvalue); int32_t calcForce_dv(int8_t force, uint8_t *counter); int32_t limitSpeed(int32_t speed); @@ -202,22 +201,22 @@ class ParticleSystem int32_t collisionHardness; uint8_t wallHardness; uint8_t wallRoughness; - uint8_t gforcecounter; //counter for global gravity - int8_t gforce; //gravity strength, default is 8 (negative is allowed, positive is downwards) - uint8_t collisioncounter; //counter to handle collisions TODO: could use the SEGMENT.call? - uint8_t forcecounter; //counter for globally applied forces - //global particle properties for basic particles - uint8_t particlesize; //global particle size, 0 = 2 pixels, 255 = 10 pixels (note: this is also added to individual sized particles) + uint8_t gforcecounter; // counter for global gravity + int8_t gforce; // gravity strength, default is 8 (negative is allowed, positive is downwards) + uint8_t collisioncounter; // counter to handle collisions TODO: could use the SEGMENT.call? + uint8_t forcecounter; // counter for globally applied forces + // global particle properties for basic particles + uint8_t particlesize; // global particle size, 0 = 2 pixels, 255 = 10 pixels (note: this is also added to individual sized particles) int32_t particleHardRadius; // hard surface radius of a particle, used for collision detection - uint8_t motionBlur; //enable motion blur, values > 100 gives smoother animations. Note: motion blurring does not work if particlesize is > 0 + uint8_t motionBlur; // enable motion blur, values > 100 gives smoother animations. Note: motion blurring does not work if particlesize is > 0 }; -//initialization functions (not part of class) +// initialization functions (not part of class) bool initParticleSystem(ParticleSystem *&PartSys, uint8_t requestedsources, uint16_t additionalbytes = 0, bool largesizes = false, bool sizecontrol = false); uint32_t calculateNumberOfParticles(bool advanced, bool sizecontrol); uint32_t calculateNumberOfSources(uint8_t requestedsources); bool allocateParticleSystemMemory(uint16_t numparticles, uint16_t numsources, bool advanced, bool sizecontrol, uint16_t additionalbytes); -//color add function +// color functions void fast_color_add(CRGB &c1, CRGB &c2, uint32_t scale = 255); // fast and accurate color adding with scaling (scales c2 before adding) -void fast_color_scale(CRGB &c, uint32_t scale); //fast scaling function using 32bit factor (keep it 0-255) and pointer +void fast_color_scale(CRGB &c, uint32_t scale); // fast scaling function using 32bit variable and pointer. note: keep 'scale' within 0-255 void blur2D(CRGB **colorbuffer, uint32_t xsize, uint32_t ysize, uint32_t xblur, uint32_t yblur, bool smear = true, uint32_t xstart = 0, uint32_t ystart = 0, bool isparticle = false); \ No newline at end of file From 935c476e86265fdfac79bcb7f196e92a5866eeab Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Tue, 7 May 2024 21:36:03 +0200 Subject: [PATCH 079/219] added WLED_DISABLE_PARTICLESYSTEM option, fixed volcano movement use '-D WLED_DISABLE_PARTICLESYSTEM' to disable compiling --- wled00/FX.cpp | 17 ++++++++++++----- wled00/FXparticleSystem.cpp | 7 +++++-- wled00/FXparticleSystem.h | 5 ++++- 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index b6c4a04603..42728fed11 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -7892,6 +7892,8 @@ uint16_t mode_2Dwavingcell() { } static const char _data_FX_MODE_2DWAVINGCELL[] PROGMEM = "Waving Cell@!,,Amplitude 1,Amplitude 2,Amplitude 3;;!;2"; +#ifndef WLED_DISABLE_PARTICLESYSTEM + /* * Particle System Vortex * Particles sprayed from center with a rotating spray @@ -8222,9 +8224,12 @@ uint16_t mode_particlevolcano(void) if (SEGLEN == 1) return mode_static(); ParticleSystem *PartSys = NULL; + PSsettings volcanosettings; + volcanosettings.asByte = 0b00000100; // PS settings for volcano movement: bounceX is enabled uint8_t numSprays; // note: so far only one tested but more is possible uint32_t i = 0; + if (SEGMENT.call == 0) // initialization { if (!initParticleSystem(PartSys, NUMBEROFSOURCES)) // init, no additional data needed @@ -8274,12 +8279,12 @@ uint16_t mode_particlevolcano(void) PartSys->sources[i].source.y = PS_P_RADIUS + 5; // reset to just above the lower edge that is allowed for bouncing particles, if zero, particles already 'bounce' at start and loose speed. PartSys->sources[i].source.vy = 0; //reset speed (so no extra particlesettin is required to keep the source 'afloat') PartSys->sources[i].source.hue++; // = random16(); //change hue of spray source (note: random does not look good) - PartSys->sources[i].source.vx = PartSys->sources[i].source.vx > 0 ? SEGMENT.custom1 >> 4 : -(SEGMENT.custom1 >> 4); // set moving speed but keep the direction given by PS + PartSys->sources[i].source.vx = PartSys->sources[i].source.vx > 0 ? SEGMENT.custom1 >> 2 : -(SEGMENT.custom1 >> 2); // set moving speed but keep the direction given by PS PartSys->sources[i].vy = SEGMENT.speed >> 2; // emitting speed PartSys->sources[i].vx = 0; PartSys->sources[i].var = SEGMENT.custom3 >> 1; // emiting variation = nozzle size (custom 3 goes from 0-31) PartSys->sprayEmit(PartSys->sources[i]); - PartSys->particleMoveUpdate(PartSys->sources[i].source); //move the source (also applies gravity, which is corrected for above, that is a hack but easier than creating more particlesettings) + PartSys->particleMoveUpdate(PartSys->sources[i].source, &volcanosettings); //move the source } } @@ -9367,8 +9372,6 @@ uint16_t mode_particleghostrider(void) SEGMENT.aux0 += (int32_t)SEGMENT.step; // step is angle increment uint16_t emitangle = SEGMENT.aux0 + 32767; // +180° int32_t speed = map(SEGMENT.speed, 0, 255, 12, 64); - int8_t newvx = ((int32_t)cos16(SEGMENT.aux0) * speed) / (int32_t)32767; - int8_t newvy = ((int32_t)sin16(SEGMENT.aux0) * speed) / (int32_t)32767; PartSys->sources[0].source.vx = ((int32_t)cos16(SEGMENT.aux0) * speed) / (int32_t)32767; PartSys->sources[0].source.vy = ((int32_t)sin16(SEGMENT.aux0) * speed) / (int32_t)32767; PartSys->sources[0].source.ttl = 500; // source never dies (note: setting 'perpetual' is not needed if replenished each frame) @@ -9596,6 +9599,9 @@ uint16_t mode_particlecenterGEQ(void) } static const char _data_FX_MODE_PARTICLECCIRCULARGEQ[] PROGMEM = "PS Center GEQ@Speed,Color Change,Particle Speed,Spray Count,Nozzle Size,Random Color, Direction;;!;012;pal=56,sx=0,ix=222,c1=190,c2=200,c3=0,o1=0,o2=0"; */ + +#endif //WLED_DISABLE_PARTICLESYSTEM + #endif // WLED_DISABLE_2D @@ -9837,6 +9843,7 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_2DAKEMI, &mode_2DAkemi, _data_FX_MODE_2DAKEMI); // audio +#ifndef WLED_DISABLE_PARTICLESYSTEM addEffect(FX_MODE_PARTICLEVORTEX, &mode_particlevortex, _data_FX_MODE_PARTICLEVORTEX); addEffect(FX_MODE_PARTICLEFIREWORKS, &mode_particlefireworks, _data_FX_MODE_PARTICLEFIREWORKS); addEffect(FX_MODE_PARTICLEVOLCANO, &mode_particlevolcano, _data_FX_MODE_PARTICLEVOLCANO); @@ -9851,8 +9858,8 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_PARTICLESGEQ, &mode_particleGEQ, _data_FX_MODE_PARTICLEGEQ); addEffect(FX_MODE_PARTICLEGHOSTRIDER, &mode_particleghostrider, _data_FX_MODE_PARTICLEGHOSTRIDER); addEffect(FX_MODE_PARTICLEBLOBS, &mode_particleblobs, _data_FX_MODE_PARTICLEBLOBS); - // addEffect(FX_MODE_PARTICLECENTERGEQ, &mode_particlecenterGEQ, _data_FX_MODE_PARTICLECCIRCULARGEQ); +#endif // WLED_DISABLE_PARTICLESYSTEM #endif // WLED_DISABLE_2D diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index b7a132e139..357371853a 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -36,7 +36,9 @@ -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 */ -// sources need to be updatable by the FX, so functions are needed to apply it to a single particle that are public + +#ifndef WLED_DISABLE_PARTICLESYSTEM + #include "FXparticleSystem.h" #include "wled.h" #include "FastLED.h" @@ -1538,4 +1540,5 @@ void blur2D(CRGB **colorbuffer, uint32_t xsize, uint32_t ysize, uint32_t xblur, } fast_color_add(colorbuffer[x][ysize-1], carryover); // set last pixel } -} \ No newline at end of file +}} + diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index f3da35b1bf..2eafe40893 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -24,6 +24,7 @@ THE SOFTWARE. */ +#ifndef WLED_DISABLE_PARTICLESYSTEM #include #include "FastLED.h" @@ -219,4 +220,6 @@ bool allocateParticleSystemMemory(uint16_t numparticles, uint16_t numsources, bo // color functions void fast_color_add(CRGB &c1, CRGB &c2, uint32_t scale = 255); // fast and accurate color adding with scaling (scales c2 before adding) void fast_color_scale(CRGB &c, uint32_t scale); // fast scaling function using 32bit variable and pointer. note: keep 'scale' within 0-255 -void blur2D(CRGB **colorbuffer, uint32_t xsize, uint32_t ysize, uint32_t xblur, uint32_t yblur, bool smear = true, uint32_t xstart = 0, uint32_t ystart = 0, bool isparticle = false); \ No newline at end of file +void blur2D(CRGB **colorbuffer, uint32_t xsize, uint32_t ysize, uint32_t xblur, uint32_t yblur, bool smear = true, uint32_t xstart = 0, uint32_t ystart = 0, bool isparticle = false); + +#endif // WLED_DISABLE_PARTICLESYSTEM From 99b7384603c4bd86f6fdc0b8ecd3a7b3bd98d612 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Tue, 7 May 2024 21:38:47 +0200 Subject: [PATCH 080/219] a line got lost in the last commit --- wled00/FXparticleSystem.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index 357371853a..4787a4d7f2 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -1540,5 +1540,8 @@ void blur2D(CRGB **colorbuffer, uint32_t xsize, uint32_t ysize, uint32_t xblur, } fast_color_add(colorbuffer[x][ysize-1], carryover); // set last pixel } -}} +} + +#endif // WLED_DISABLE_PARTICLESYSTEM + From 64ffe72dd251ace895a79cbc8d36310343ffb609 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Tue, 7 May 2024 21:54:02 +0200 Subject: [PATCH 081/219] - added WLED_DISABLE_PARTICLESYSTEM option & cleanup & bugfixes - cleanup / reformatting - fixed volcano movement - small bugfix for ESP32 (random() does not work, using random16() ) From c5e7e170cb166b3c354488d91e3f0a26731fe1a3 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Thu, 9 May 2024 14:19:31 +0200 Subject: [PATCH 082/219] Added PS source option to emit more than one particle, addes AR to Blobs and Spray --- wled00/FX.cpp | 66 ++++++++++++++++++++++++++++++------- wled00/FXparticleSystem.cpp | 44 +++++++++++++------------ wled00/FXparticleSystem.h | 6 ++-- 3 files changed, 81 insertions(+), 35 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 22d9ede777..b620d242f3 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -9164,11 +9164,9 @@ uint16_t mode_particlespray(void) if (!initParticleSystem(PartSys, 1)) // init, no additional data needed return mode_static(); // allocation failed; //allocation failed PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting) - PartSys->setBounceY(true); + PartSys->setBounceY(true); PartSys->setMotionBlur(200); // anable motion blur PartSys->sources[0].source.hue = random16(); - PartSys->sources[0].maxLife = 300; // lifetime in frames - PartSys->sources[0].minLife = 100; PartSys->sources[0].source.collide = true; // seeded particles will collide (if enabled) PartSys->sources[0].var = 3; @@ -9195,21 +9193,55 @@ uint16_t mode_particlespray(void) else PartSys->enableParticleCollisions(false); + //position according to sliders + PartSys->sources[0].source.x = map(SEGMENT.custom1, 0, 255, 0, PartSys->maxX); + PartSys->sources[0].source.y = map(SEGMENT.custom2, 0, 255, 0, PartSys->maxY); + uint16_t angle = (256 - (((int32_t)SEGMENT.custom3 + 1) << 3)) << 8; + + #ifdef USERMOD_AUDIOREACTIVE + um_data_t *um_data; + if(usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) + { + uint32_t volumeSmth = (uint8_t)(*(float*) um_data->u_data[0]); //0 to 255 + uint32_t volumeRaw = *(int16_t*)um_data->u_data[1]; //0 to 255 + PartSys->sources[0].minLife = 30; + + if (SEGMENT.call % 20 == 0 || SEGMENT.call % (11 - volumeSmth / 25) == 0) // defines interval of particle emit + { + PartSys->sources[0].maxLife = (volumeSmth >> 1) + (SEGMENT.intensity >> 1); // lifetime in frames + PartSys->sources[0].var = 1 + volumeRaw >> 4; + uint32_t emitspeed = (SEGMENT.speed >> 2) + (volumeSmth >> 3); + PartSys->sources[0].source.hue += volumeSmth/30; + PartSys->angleEmit(PartSys->sources[0], angle, emitspeed); + } + } + else{ //no AR data, fall back to normal mode + // change source properties + if (SEGMENT.call % (11 - (SEGMENT.intensity / 25)) == 0) // every nth frame, cycle color and emit particles + { + PartSys->sources[0].maxLife = 300; // lifetime in frames + PartSys->sources[0].minLife = 100; + PartSys->sources[0].source.hue++; // = random16(); //change hue of spray source + PartSys->angleEmit(PartSys->sources[0], angle, SEGMENT.speed >> 2); + } + } + #else // change source properties if (SEGMENT.call % (11 - (SEGMENT.intensity / 25)) == 0) // every nth frame, cycle color and emit particles { - PartSys->sources[0].source.hue++; // = random16(); //change hue of spray source - // PartSys->sources[i].var = SEGMENT.custom3; // emiting variation = nozzle size (custom 3 goes from 0-32) - PartSys->sources[0].source.x = map(SEGMENT.custom1, 0, 255, 0, PartSys->maxX); - PartSys->sources[0].source.y = map(SEGMENT.custom2, 0, 255, 0, PartSys->maxY); - // spray[j].source.hue = random16(); //set random color for each particle (using palette) - PartSys->angleEmit(PartSys->sources[0], (256 - (((int32_t)SEGMENT.custom3 + 1) << 3)) << 8, SEGMENT.speed >> 2); + PartSys->sources[0].maxLife = 300; // lifetime in frames. note: could be done in init part, but AR moderequires this to be dynamic + PartSys->sources[0].minLife = 100; + PartSys->sources[0].source.hue++; // = random16(); //change hue of spray source + // PartSys->sources[i].var = SEGMENT.custom3; // emiting variation = nozzle size (custom 3 goes from 0-32) + // spray[j].source.hue = random16(); //set random color for each particle (using palette) + PartSys->angleEmit(PartSys->sources[0], angle, SEGMENT.speed >> 2); } + #endif PartSys->update(); // update and render return FRAMETIME; } -static const char _data_FX_MODE_PARTICLESPRAY[] PROGMEM = "PS Spray@Speed,!,Left/Right,Up/Down,Angle,Gravity,Cylinder/Square,Collisions;;!;2;pal=0,sx=150,ix=150,c1=220,c2=30,c3=21,o1=0,o2=0,o3=0"; +static const char _data_FX_MODE_PARTICLESPRAY[] PROGMEM = "PS Spray@Speed,!,Left/Right,Up/Down,Angle,Gravity,Cylinder/Square,Collisions;;!;2v;pal=0,sx=150,ix=150,c1=220,c2=30,c3=21,o1=0,o2=0,o3=0"; /* @@ -9464,12 +9496,24 @@ uint16_t mode_particleblobs(void) SEGMENT.aux0 = SEGMENT.speed; //write state back SEGMENT.aux1 = SEGMENT.custom1; + #ifdef USERMOD_AUDIOREACTIVE + um_data_t *um_data; + if(usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) + { + uint8_t volumeSmth = (uint8_t)(*(float*) um_data->u_data[0]); + for (uint32_t i = 0; i < PartSys->usedParticles; i++) // update particles + { + if(SEGMENT.check3) //pulsate selected + PartSys->advPartProps[i].size = volumeSmth; + } + } + #endif PartSys->setMotionBlur(((SEGMENT.custom3) << 3) + 7); PartSys->update(); // update and render return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEBLOBS[] PROGMEM = "PS Blobs@Speed,Blobs,Size,Life,Blur,Wobble,Collide,Pulsate;;!;2;sx=30,ix=64,c1=200,c2=130,c3=0,o1=0,o2=0,o3=1"; +static const char _data_FX_MODE_PARTICLEBLOBS[] PROGMEM = "PS Blobs@Speed,Blobs,Size,Life,Blur,Wobble,Collide,Pulsate;;!;2v;sx=30,ix=64,c1=200,c2=130,c3=0,o1=0,o2=0,o3=1"; /* diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index 4787a4d7f2..f267796b41 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -33,7 +33,6 @@ /* TODO: -add function to 'update sources' so FX does not have to take care of that. FX can still implement its own version if so desired. - -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 */ @@ -217,27 +216,30 @@ void ParticleSystem::enableParticleCollisions(bool enable, uint8_t hardness) // collisionHardness = hardness + 1; } -// emit one particle with variation, returns index of emitted particle (or -1 if no particle emitted) -int32_t ParticleSystem::sprayEmit(PSsource &emitter) +// emit one particle with variation, returns index of last emitted particle (or -1 if no particle emitted) +int32_t ParticleSystem::sprayEmit(PSsource &emitter, uint32_t amount) { - for (int32_t i = 0; i < usedParticles; i++) + for (uint32_t a = 0; a < amount; a++) { - emitIndex++; - if (emitIndex >= usedParticles) - emitIndex = 0; - if (particles[emitIndex].ttl == 0) // find a dead particle + for (int32_t i = 0; i < usedParticles; i++) { - particles[emitIndex].vx = emitter.vx + random(-emitter.var, emitter.var); - particles[emitIndex].vy = emitter.vy + random(-emitter.var, emitter.var); - particles[emitIndex].x = emitter.source.x; - particles[emitIndex].y = emitter.source.y; - particles[emitIndex].hue = emitter.source.hue; - particles[emitIndex].sat = emitter.source.sat; - particles[emitIndex].collide = emitter.source.collide; - particles[emitIndex].ttl = random(emitter.minLife, emitter.maxLife); - if (advPartProps) - advPartProps[emitIndex].size = emitter.size; - return i; + emitIndex++; + if (emitIndex >= usedParticles) + emitIndex = 0; + if (particles[emitIndex].ttl == 0) // find a dead particle + { + particles[emitIndex].vx = emitter.vx + random(-emitter.var, emitter.var); + particles[emitIndex].vy = emitter.vy + random(-emitter.var, emitter.var); + particles[emitIndex].x = emitter.source.x; + particles[emitIndex].y = emitter.source.y; + particles[emitIndex].hue = emitter.source.hue; + particles[emitIndex].sat = emitter.source.sat; + particles[emitIndex].collide = emitter.source.collide; + particles[emitIndex].ttl = random(emitter.minLife, emitter.maxLife); + if (advPartProps) + advPartProps[emitIndex].size = emitter.size; + return i; + } } } return -1; @@ -273,11 +275,11 @@ void ParticleSystem::flameEmit(PSsource &emitter) // Emits a particle at given angle and speed, angle is from 0-65535 (=0-360deg), speed is also affected by emitter->var // angle = 0 means in positive x-direction (i.e. to the right) -void ParticleSystem::angleEmit(PSsource &emitter, uint16_t angle, int8_t speed) +void ParticleSystem::angleEmit(PSsource &emitter, uint16_t angle, int8_t speed, uint32_t amount) { emitter.vx = ((int32_t)cos16(angle) * (int32_t)speed) / (int32_t)32600; // cos16() and sin16() return signed 16bit, division should be 32767 but 32600 gives slightly better rounding emitter.vy = ((int32_t)sin16(angle) * (int32_t)speed) / (int32_t)32600; // note: cannot use bit shifts as bit shifting is asymmetrical for positive and negative numbers and this needs to be accurate! - sprayEmit(emitter); + sprayEmit(emitter, amount); } // particle moves, decays and dies, if killoutofbounds is set, out of bounds particles are set to ttl=0 diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index 2eafe40893..57a54e5406 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -93,7 +93,7 @@ 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 (use odd numbers for good symmetry) + int8_t var; // variation of emitted speed (adds random(+/- var) to speed) int8_t vx; // emitting speed int8_t vy; uint8_t size; // particle size (advanced property) @@ -127,9 +127,9 @@ class ParticleSystem void updateSystem(void); // call at the beginning of every FX, updates pointers and dimensions // particle emitters - int32_t sprayEmit(PSsource &emitter); + int32_t sprayEmit(PSsource &emitter, uint32_t amount = 1); void flameEmit(PSsource &emitter); - void angleEmit(PSsource& emitter, uint16_t angle, int8_t speed); + void angleEmit(PSsource& emitter, uint16_t angle, int8_t speed, uint32_t amount = 1); void particleMoveUpdate(PSparticle &part, PSsettings *options = NULL, PSadvancedParticle *advancedproperties = NULL); // move function From 0159907cdbb11772a6e312b2e0654a5891c0c006 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Thu, 9 May 2024 21:35:12 +0200 Subject: [PATCH 083/219] added center GEQ back in, changed AR behaviour of spray a little center GEQ is a mix between the PS equalizer and vortex, tuned for AR. some more tuning may be needed, it can probably be extended and improved a little. --- wled00/FX.cpp | 228 ++++++++++++++++++++------------------------------ 1 file changed, 92 insertions(+), 136 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index b620d242f3..857cc1cce1 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -9209,8 +9209,8 @@ uint16_t mode_particlespray(void) if (SEGMENT.call % 20 == 0 || SEGMENT.call % (11 - volumeSmth / 25) == 0) // defines interval of particle emit { PartSys->sources[0].maxLife = (volumeSmth >> 1) + (SEGMENT.intensity >> 1); // lifetime in frames - PartSys->sources[0].var = 1 + volumeRaw >> 4; - uint32_t emitspeed = (SEGMENT.speed >> 2) + (volumeSmth >> 3); + PartSys->sources[0].var = 1 + ((volumeRaw * SEGMENT.speed) >> 12); + uint32_t emitspeed = (SEGMENT.speed >> 2) + (volumeRaw >> 3); PartSys->sources[0].source.hue += volumeSmth/30; PartSys->angleEmit(PartSys->sources[0], angle, emitspeed); } @@ -9340,6 +9340,95 @@ uint16_t mode_particleGEQ(void) } static const char _data_FX_MODE_PARTICLEGEQ[] PROGMEM = "PS Equalizer@Speed,Intensity,Diverge,Bounce,Gravity,Cylinder,Walls,Floor;;!;2f;pal=0,sx=155,ix=200,c1=0,c2=128,o1=0,o2=0,o3=0"; +/* + * Particle rotating GEQ (unfinished, basically works but needs more fine-tuning) + * Particles sprayed from center with a rotating spray + * Uses palette for particle color + * by DedeHai (Damian Schneider) + */ + +#define NUMBEROFSOURCES 16 +uint16_t mode_particlecenterGEQ(void) +{ +if (SEGLEN == 1) + return mode_static(); + + ParticleSystem *PartSys = NULL; + uint8_t numSprays; + uint32_t i; + + if (SEGMENT.call == 0) // initialization + { + if (!initParticleSystem(PartSys, NUMBEROFSOURCES)) // init, request 16 sources + return mode_static(); // allocation failed + numSprays = min(PartSys->numSources, (uint8_t)NUMBEROFSOURCES); + for (i = 0; i < numSprays; i++) + { + PartSys->sources[i].source.x = (PartSys->maxX + 1) >> 1; // center + PartSys->sources[i].source.y = (PartSys->maxY + 1) >> 1; // center + PartSys->sources[i].source.hue = i*16; // even color distribution + PartSys->sources[i].maxLife = 400; + PartSys->sources[i].minLife = 200; + } + PartSys->setKillOutOfBounds(true); + } + else + PartSys = reinterpret_cast(SEGMENT.data); // if not first call, just set the pointer to the PS + + if (PartSys == NULL) + { + DEBUG_PRINT(F("ERROR: FX PartSys nullpointer")); + return mode_static(); // something went wrong, no data! + } + + numSprays = min(PartSys->numSources, (uint8_t)NUMBEROFSOURCES); + + um_data_t *um_data; + if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) + um_data = simulateSound(SEGMENT.soundSim); // add support for no audio + + uint8_t *fftResult = (uint8_t *)um_data->u_data[2]; // 16 bins with FFT data, log mapped already, each band contains frequency amplitude 0-255 + uint32_t threshold = 300 - SEGMENT.intensity; + + + if (SEGMENT.check2) + SEGMENT.aux0 += SEGMENT.custom1 << 2; + else + SEGMENT.aux0 -= SEGMENT.custom1 << 2; + + uint16_t angleoffset = (uint16_t)0xFFFF / (uint16_t)numSprays; + uint32_t j = random(numSprays); // 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 < numSprays; i++) + { + if(SEGMENT.call % (32 - (SEGMENT.custom2 >> 3)) == 0 && SEGMENT.custom2 > 0) + PartSys->sources[j].source.hue += 1 + (SEGMENT.custom2 >> 4); + PartSys->sources[j].var = SEGMENT.custom3>>1; + int8_t emitspeed = 5 + (((uint32_t)fftResult[j] * ((uint32_t)SEGMENT.speed+20)) >> 10); // emit speed according to loudness of band + uint16_t emitangle = j * angleoffset + SEGMENT.aux0; + + uint32_t emitparticles = 0; + if (fftResult[j] > threshold) + { + emitparticles = 1; + } + else if (fftResult[j] > 0) // band has low value + { + uint32_t restvolume = ((threshold - fftResult[j]) >> 2) + 2; + if (random16() % restvolume == 0) + { + emitparticles = 1; + } + } + if (emitparticles) + PartSys->angleEmit(PartSys->sources[j], emitangle, emitspeed); + j = (j + 1) % numSprays; + } + + PartSys->update(); // update and render + return FRAMETIME; +} +static const char _data_FX_MODE_PARTICLECCIRCULARGEQ[] PROGMEM = "PS Center GEQ@Speed,Intensity,Rotation Speed,Color Change,Nozzle Size,,Direction;;!;012;pal=13,ix=180,c1=0,c2=0,c3=8,o1=0,o2=0"; + /* Particle replacement of Ghost Rider by DedeHai (Damian Schneider), original by stepko adapted by Blaz Kristan (AKA blazoncek) */ @@ -9515,139 +9604,6 @@ uint16_t mode_particleblobs(void) } static const char _data_FX_MODE_PARTICLEBLOBS[] PROGMEM = "PS Blobs@Speed,Blobs,Size,Life,Blur,Wobble,Collide,Pulsate;;!;2v;sx=30,ix=64,c1=200,c2=130,c3=0,o1=0,o2=0,o3=1"; - -/* - * Particle rotating GEQ (unfinished, basically works but needs more fine-tuning) - * Particles sprayed from center with a rotating spray - * Uses palette for particle color - * by DedeHai (Damian Schneider) - */ -/* -uint16_t mode_particlecenterGEQ(void) -{ - - if (SEGLEN == 1) - return mode_static(); - - const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; - const uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); - -#ifdef ESP8266 - const uint32_t numParticles = 50; // maximum number of particles -#else - const uint32_t numParticles = 500; // maximum number of particles -#endif - - const uint8_t numSprays = 16; // maximum number of sprays - - PSparticle *particles; - 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(PSsource) * (numSprays); - if (!SEGMENT.allocateData(dataSize)) - return mode_static(); // allocation failed; //allocation failed - - spray = reinterpret_cast(SEGMENT.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 - - uint32_t i = 0; - uint32_t j = 0; - //uint8_t spraycount = 1 + (SEGMENT.custom2 >> 5); // number of sprays to display, 1-8 - - if (SEGMENT.call == 0) // initialization - { - SEGMENT.aux0 = 0; // starting angle - SEGMENT.aux1 = 0xFF; // user check - for (i = 0; i < numParticles; i++) - { - PartSys->particles[i].ttl = 0; - } - for (i = 0; i < numSprays; i++) - { - PartSys->sources[i].source.hue = i*16; // even color distribution - PartSys->sources[i].source.sat = 255; // set saturation - PartSys->sources[i].source.x = (cols * PS_P_RADIUS) / 2; // center - PartSys->sources[i].source.y = (rows * PS_P_RADIUS) / 2; // center - PartSys->sources[i].source.vx = 0; - PartSys->sources[i].source.vy = 0; - PartSys->sources[i].maxLife = 400; - PartSys->sources[i].minLife = 200; - PartSys->sources[i].vx = 0; // emitting speed - PartSys->sources[i].vy = 0; // emitting speed - PartSys->sources[i].var = 0; // emitting variation - } - } - - um_data_t *um_data; - if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) - { - // add support for no audio - um_data = simulateSound(SEGMENT.soundSim); - } - - uint8_t *fftResult = (uint8_t *)um_data->u_data[2]; // 16 bins with FFT data, log mapped already, each band contains frequency amplitude 0-255 - - i = 0; - - uint32_t threshold = 300 - SEGMENT.intensity; - - i = 0; - j = random16(numSprays); // start with random spray so all get a chance to emit a particle if maximum number of particles alive is reached. - if (SEGMENT.check2) - SEGMENT.aux0 += SEGMENT.custom1; - else - SEGMENT.aux0 -= SEGMENT.custom1; - - uint32_t angleoffset = SEGMENT.aux0 >> 4; - - while (i < numParticles) - { - if (PartSys->particles[i].ttl == 0) // find a dead particle - { - uint8_t emitspeed = 5 + (((uint32_t)fftResult[j] * ((uint32_t)SEGMENT.speed+10)) >> 9); // emit speed according to loudness of band - uint8_t emitangle = j * 16 + random16(SEGMENT.custom3 >> 1) + angleoffset; - - uint32_t emitparticles = 0; - if (fftResult[j] > threshold) - { - emitparticles = 1; // + (fftResult[bin]>>6); - } - else if (fftResult[j] > 0) // band has low volue - { - uint32_t restvolume = ((threshold - fftResult[j]) >> 2) + 2; - if (random16() % restvolume == 0) - { - emitparticles = 1; - } - } - if (emitparticles) - Emitter_Angle_emit(&spray[j], &PartSys->particles[i], emitangle, emitspeed); - j = (j + 1) % numSprays; - } - i++; - //todo: could add a break if all 16 sprays have been checked, would speed it up - } - - - - for (i = 0; i < numParticles; i++) - { - Particle_Move_update(&PartSys->particles[i], true); // move the particles, kill out of bounds particles - } - - - // render the particles - ParticleSys_render(particles, numParticles, false, false); - - - return FRAMETIME; -} - static const char _data_FX_MODE_PARTICLECCIRCULARGEQ[] PROGMEM = "PS Center GEQ@Speed,Color Change,Particle Speed,Spray Count,Nozzle Size,Random Color, Direction;;!;012;pal=56,sx=0,ix=222,c1=190,c2=200,c3=0,o1=0,o2=0"; -*/ - #endif //WLED_DISABLE_PARTICLESYSTEM #endif // WLED_DISABLE_2D @@ -9906,7 +9862,7 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_PARTICLESGEQ, &mode_particleGEQ, _data_FX_MODE_PARTICLEGEQ); addEffect(FX_MODE_PARTICLEGHOSTRIDER, &mode_particleghostrider, _data_FX_MODE_PARTICLEGHOSTRIDER); addEffect(FX_MODE_PARTICLEBLOBS, &mode_particleblobs, _data_FX_MODE_PARTICLEBLOBS); - // addEffect(FX_MODE_PARTICLECENTERGEQ, &mode_particlecenterGEQ, _data_FX_MODE_PARTICLECCIRCULARGEQ); + addEffect(FX_MODE_PARTICLECENTERGEQ, &mode_particlecenterGEQ, _data_FX_MODE_PARTICLECCIRCULARGEQ); #endif // WLED_DISABLE_PARTICLESYSTEM #endif // WLED_DISABLE_2D From da80019924b4e38f25dfcb4488ce6ce734c8289e Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Mon, 27 May 2024 07:39:49 +0200 Subject: [PATCH 084/219] Bugfix and minor improvements - fixed bug in center GEQ - added '2D washing machine' mode for particle box - improved color-change rate in ghostrider - added AR to attractor (experimental, may remove again) --- wled00/FX.cpp | 52 +++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 44 insertions(+), 8 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 857cc1cce1..c9010d3129 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -7910,8 +7910,8 @@ uint16_t mode_particlevortex(void) if (SEGLEN == 1) return mode_static(); ParticleSystem *PartSys = NULL; - uint32_t i = 0; - uint32_t j = 0; + uint32_t i, j; + if (SEGMENT.call == 0) // initialization { if (!initParticleSystem(PartSys, NUMBEROFSOURCES)) @@ -8688,10 +8688,22 @@ uint16_t mode_particlebox(void) int32_t xgravity; int32_t ygravity; int32_t increment = (SEGMENT.speed >> 6) + 1; - if(SEGMENT.check2) // direction + + /*if(SEGMENT.check2) // direction SEGMENT.aux0 += increment; // update counter else SEGMENT.aux0 -= increment; + */ + + if(SEGMENT.check2) // washing machine + { + int speed = tristate_square8(strip.now >> 7, 90, 15) / ((400 - SEGMENT.speed) >> 3); + SEGMENT.aux0 += speed; + if(speed == 0) SEGMENT.aux0 = 190; //down (= 270°) + } + else + SEGMENT.aux0 -= increment; + if(SEGMENT.check1) // random, use perlin noise { @@ -8722,7 +8734,7 @@ uint16_t mode_particlebox(void) return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEBOX[] PROGMEM = "PS Box@Speed,Particles,Tilt Strength,Hardness,Friction,Random,Direction,Sloshing;;!;2;pal=53,sx=120,ix=100,c1=100,c2=210,o1=1"; +static const char _data_FX_MODE_PARTICLEBOX[] PROGMEM = "PS Box@Speed,Particles,Tilt Strength,Hardness,Friction,Random,Washing Machine,Sloshing;;!;2;pal=53,sx=120,ix=100,c1=100,c2=210,o1=1"; /* Fuzzy Noise: Perlin noise 'gravity' mapping as in particles on 'noise hills' viewed from above @@ -9015,17 +9027,36 @@ uint16_t mode_particleattractor(void) else PartSys->angleEmit(PartSys->sources[0], SEGMENT.aux0 + 0x7FFF, 12); // emit at 180° as well // apply force + #ifdef USERMOD_AUDIOREACTIVE + um_data_t *um_data; + if(usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) + { + uint8_t volumeSmth = (uint8_t)(*(float*) um_data->u_data[0]); + uint8_t strength = volumeSmth; + if(SEGMENT.check3) strength = SEGMENT.speed; //AR disabled + for (uint32_t i = 0; i < PartSys->usedParticles; i++) // update particles + { + PartSys->pointAttractor(i, attractor, strength, false); + } + } + #else for(i = 0; i < displayparticles; i++) { PartSys->pointAttractor(i, attractor, SEGMENT.speed, SEGMENT.check3); } + #endif + if (SEGMENT.call % (33 - SEGMENT.custom3) == 0) PartSys->applyFriction(2); PartSys->particleMoveUpdate(PartSys->sources[0].source, &sourcesettings); // move the source PartSys->update(); // update and render return FRAMETIME; } +#ifdef USERMOD_AUDIOREACTIVE +static const char _data_FX_MODE_PARTICLEATTRACTOR[] PROGMEM = "PS Attractor@Mass,Particles,Particle Size,Collisions,Friction,Color by Age,Move,Disable AR;;!;2v;pal=9,sx=100,ix=82,c1=0,c2=0,o1=0,o2=0,o3=0"; +#else static const char _data_FX_MODE_PARTICLEATTRACTOR[] PROGMEM = "PS Attractor@Mass,Particles,Particle Size,Collisions,Friction,Color by Age,Move,Swallow;;!;2;pal=9,sx=100,ix=82,c1=0,c2=0,o1=0,o2=0,o3=0"; +#endif /* @@ -9326,7 +9357,7 @@ uint16_t mode_particleGEQ(void) PartSys->particles[i].ttl = 20 + map(SEGMENT.intensity, 0,255, emitspeed>>1, emitspeed + random16(emitspeed)) ; // set particle alive, particle lifespan is in number of frames PartSys->particles[i].x = xposition + random16(binwidth) - (binwidth>>1); // position randomly, deviating half a bin width PartSys->particles[i].y = PS_P_RADIUS; // start at the bottom (PS_P_RADIUS is minimum position a particle is fully in frame) - PartSys->particles[i].vx = random16(SEGMENT.custom1>>1)-(SEGMENT.custom1>>2) ; //x-speed variation: +/- custom1/4 TODO: ok to use random16 here? + PartSys->particles[i].vx = random(SEGMENT.custom1>>1)-(SEGMENT.custom1>>2) ; //x-speed variation: +/- custom1/4 PartSys->particles[i].vy = emitspeed; PartSys->particles[i].hue = (bin<<4) + random16(17) - 8; // color from palette according to bin emitparticles--; @@ -9380,7 +9411,7 @@ if (SEGLEN == 1) DEBUG_PRINT(F("ERROR: FX PartSys nullpointer")); return mode_static(); // something went wrong, no data! } - + PartSys->updateSystem(); // update system properties (dimensions and data pointers) numSprays = min(PartSys->numSources, (uint8_t)NUMBEROFSOURCES); um_data_t *um_data; @@ -9427,7 +9458,7 @@ if (SEGLEN == 1) PartSys->update(); // update and render return FRAMETIME; } -static const char _data_FX_MODE_PARTICLECCIRCULARGEQ[] PROGMEM = "PS Center GEQ@Speed,Intensity,Rotation Speed,Color Change,Nozzle Size,,Direction;;!;012;pal=13,ix=180,c1=0,c2=0,c3=8,o1=0,o2=0"; +static const char _data_FX_MODE_PARTICLECCIRCULARGEQ[] PROGMEM = "PS Center GEQ@Speed,Intensity,Rotation Speed,Color Change,Nozzle Size,,Direction;;!;2f;pal=13,ix=180,c1=0,c2=0,c3=8,o1=0,o2=0"; /* Particle replacement of Ghost Rider by DedeHai (Damian Schneider), original by stepko adapted by Blaz Kristan (AKA blazoncek) @@ -9509,7 +9540,12 @@ uint16_t mode_particleghostrider(void) // emit two particles PartSys->angleEmit(PartSys->sources[0], emitangle, speed); PartSys->angleEmit(PartSys->sources[0], emitangle, speed); - PartSys->sources[0].source.hue += SEGMENT.custom2 >> 3; + if (SEGMENT.call % (11 - (SEGMENT.custom2 / 25)) == 0) // every nth frame, cycle color and emit particles //TODO: make this a segment call % SEGMENT.custom2 for better control + { + PartSys->sources[0].source.hue++; + } + if (SEGMENT.custom2 > 190) //fast color change + PartSys->sources[0].source.hue += (SEGMENT.custom2 - 190) >> 2; PartSys->update(); // update and render return FRAMETIME; From c76d60085fc1bac634acc1befe478d17af686da4 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Tue, 28 May 2024 21:07:43 +0200 Subject: [PATCH 085/219] hacked in a 1D port of the 2D PS, untested code --- wled00/FXparticleSystem.cpp | 676 +++++++++++++++++++++++++++++++++++- wled00/FXparticleSystem.h | 129 ++++++- 2 files changed, 795 insertions(+), 10 deletions(-) diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index f267796b41..1f3927a205 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -816,7 +816,7 @@ void ParticleSystem::ParticleSys_render(bool firemode, uint32_t fireintensity) if (useLocalBuffer) // transfer local buffer back to segment { - uint32_t yflipped; + int32_t yflipped; for (int y = 0; y <= maxYpixel; y++) { yflipped = maxYpixel - y; @@ -832,7 +832,7 @@ void ParticleSystem::ParticleSys_render(bool firemode, uint32_t fireintensity) } // calculate pixel positions and brightness distribution and render the particle to local buffer or global buffer -void ParticleSystem::renderParticle(CRGB **framebuffer, uint32_t particleindex, uint32_t brightess, CRGB color, CRGB **renderbuffer) +void ParticleSystem::renderParticle(CRGB **framebuffer, uint32_t particleindex, uint32_t brightness, CRGB color, CRGB **renderbuffer) { int32_t pxlbrightness[4] = {0}; // note: pxlbrightness needs to be set to 0 or checking does not work int32_t pixco[4][2]; // physical pixel coordinates of the four pixels a particle is rendered to. x,y pairs @@ -919,18 +919,18 @@ void ParticleSystem::renderParticle(CRGB **framebuffer, uint32_t particleindex, // 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; + int32_t precal2 = ((int32_t)PS_P_RADIUS - dy) * brightness; + int32_t precal3 = dy * brightness; //calculate the values for pixels that are in frame if (pxlbrightness[0] >= 0) - pxlbrightness[0] = (precal1 * precal2) >> PS_P_SURFACE; // bottom left value equal to ((PS_P_RADIUS - dx) * (PS_P_RADIUS-dy) * brightess) >> PS_P_SURFACE + pxlbrightness[0] = (precal1 * precal2) >> PS_P_SURFACE; // bottom left value equal to ((PS_P_RADIUS - dx) * (PS_P_RADIUS-dy) * brightness) >> PS_P_SURFACE if (pxlbrightness[1] >= 0) - pxlbrightness[1] = (dx * precal2) >> PS_P_SURFACE; // bottom right value equal to (dx * (PS_P_RADIUS-dy) * brightess) >> PS_P_SURFACE + pxlbrightness[1] = (dx * precal2) >> PS_P_SURFACE; // bottom right value equal to (dx * (PS_P_RADIUS-dy) * brightness) >> PS_P_SURFACE if (pxlbrightness[2] >= 0) - pxlbrightness[2] = (dx * precal3) >> PS_P_SURFACE; // top right value equal to (dx * dy * brightess) >> PS_P_SURFACE + pxlbrightness[2] = (dx * precal3) >> PS_P_SURFACE; // top right value equal to (dx * dy * brightness) >> PS_P_SURFACE if (pxlbrightness[3] >= 0) - pxlbrightness[3] = (precal1 * precal3) >> PS_P_SURFACE; // top left value equal to ((PS_P_RADIUS-dx) * dy * brightess) >> PS_P_SURFACE + pxlbrightness[3] = (precal1 * precal3) >> PS_P_SURFACE; // top left value equal to ((PS_P_RADIUS-dx) * dy * brightness) >> PS_P_SURFACE if (advancedrender) { @@ -1547,3 +1547,663 @@ void blur2D(CRGB **colorbuffer, uint32_t xsize, uint32_t ysize, uint32_t xblur, #endif // WLED_DISABLE_PARTICLESYSTEM +//////////////////////// +// 1D Particle System // +//////////////////////// + + +ParticleSystem1D::ParticleSystem1D(uint16_t length, uint16_t numberofparticles, uint16_t numberofsources) +{ + //Serial.println("PS Constructor"); + numSources = numberofsources; + numParticles = numberofparticles; // set number of particles in the array + usedParticles = numberofparticles; // use all particles by default + //advPartProps = NULL; //make sure we start out with null pointers (just in case memory was not cleared) + //advPartSize = NULL; + updatePSpointers(); // set the particle and sources pointer (call this before accessing sprays or particles) + setSize(length); + setWallHardness(255); // set default wall hardness to max + setGravity(0); //gravity disabled by default + //setParticleSize(0); // minimum size by default + motionBlur = 0; //no fading by default + emitIndex = 0; + + //initialize some default non-zero values most FX use + for (int i = 0; i < numSources; i++) + { + sources[i].source.ttl = 1; //set source alive + } + //Serial.println("PS Constructor done"); +} + +// update function applies gravity, moves the particles, handles collisions and renders the particles +void ParticleSystem1D::update(void) +{ + PSadvancedParticle *advprop = NULL; + //apply gravity globally if enabled + if (particlesettings.useGravity) + applyGravity(); + + // handle collisions (can push particles, must be done before updating particles or they can render out of bounds, causing a crash if using local buffer for speed) + if (particlesettings.useCollisions) + handleCollisions(); + + //move all particles + for (int i = 0; i < usedParticles; i++) + { + particleMoveUpdate(particles[i], &particlesettings); + } + + /*TODO remove this + Serial.print("alive particles: "); + uint32_t aliveparticles = 0; + for (int i = 0; i < numParticles; i++) + { + if (particles[i].ttl) + aliveparticles++; + } + Serial.println(aliveparticles); + */ + ParticleSys_render(); +} + + +void ParticleSystem1D::setUsedParticles(uint16_t num) +{ + usedParticles = min(num, numParticles); //limit to max particles +} + +void ParticleSystem1D::setWallHardness(uint8_t hardness) +{ + wallHardness = hardness; +} + +void ParticleSystem1D::setCollisionHardness(uint8_t hardness) +{ + collisionHardness = hardness; +} + +void ParticleSystem1D::setSize(uint16_t x) +{ + maxXpixel = x - 1; // last physical pixel that can be drawn to + maxX = x * PS_P_RADIUS - 1; // particle system boundary for movements +} + +void ParticleSystem1D::setWrap(bool enable) +{ + particlesettings.wrapX = enable; +} + +void ParticleSystem1D::setBounce(bool enable) +{ + particlesettings.bounceX = enable; +} + +void ParticleSystem1D::setKillOutOfBounds(bool enable) +{ + particlesettings.killoutofbounds = enable; +} + +void ParticleSystem1D::setColorByAge(bool enable) +{ + particlesettings.colorByAge = enable; +} + +void ParticleSystem1D::setMotionBlur(uint8_t bluramount) +{ + if (particlesize < 2) // only allwo motion blurring on default particle size or advanced size(cannot combine motion blur with normal blurring used for particlesize, would require another buffer) + motionBlur = bluramount; +} + +// render size using smearing (see blur function) +void ParticleSystem1D::setParticleSize(uint8_t size) +{ + particlesize = size; +} +// enable/disable gravity, optionally, set the force (force=8 is default) can be 1-255, 0 is disable +// if enabled, gravity is applied to all particles in ParticleSystemUpdate() +// 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 ParticleSystem1D::setGravity(int8_t force) +{ + if (force) + { + gforce = force; + particlesettings.useGravity = true; + } + else + particlesettings.useGravity = false; +} + +void ParticleSystem1D::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; +} + +// emit one particle with variation, returns index of last emitted particle (or -1 if no particle emitted) +void ParticleSystem1D::sprayEmit(PSsource1D &emitter) +{ + for (int32_t i = 0; i < usedParticles; i++) + { + emitIndex++; + if (emitIndex >= usedParticles) + emitIndex = 0; + if (particles[emitIndex].ttl == 0) // find a dead particle + { + particles[emitIndex].vx = emitter.v + random(-emitter.var, emitter.var); + particles[emitIndex].x = emitter.source.x; + particles[emitIndex].hue = emitter.source.hue; + particles[emitIndex].collide = emitter.source.collide; + particles[emitIndex].ttl = random(emitter.minLife, emitter.maxLife); + break; + } + } +} + +// particle moves, decays and dies, if killoutofbounds is set, out of bounds particles are set to ttl=0 +// uses passed settings to set bounce or wrap, if useGravity is set, it will never bounce at the top and killoutofbounds is not applied over the top +void ParticleSystem1D::particleMoveUpdate(PSparticle1D &part, PSsettings *options) +{ + if (options == NULL) + options = &particlesettings; //use PS system settings by default + if (part.ttl > 0) + { + if (!part.perpetual) + part.ttl--; // age + if (particlesettings.colorByAge) + part.hue = part.ttl > 255 ? 255 : part.ttl; //set color to ttl + + //bool usesize = false; // particle uses individual size rendering + int32_t newX = part.x + (int16_t)part.vx; + part.outofbounds = 0; // reset out of bounds (in case particle was created outside the matrix and is now moving into view) + + // if wall collisions are enabled, bounce them before they reach the edge, it looks much nicer if the particle is not half out of view + if (options->bounceX) + { + if ((newX < particleHardRadius) || (newX > maxX - particleHardRadius)) // reached a wall + part.vx = -part.vx; //invert speed + part.vx = ((int32_t)-part.vx * wallHardness) / 255; // reduce speed as energy is lost on non-hard surface + if (newX < particleHardRadius) + newX = particleHardRadius; // fast particles will never reach the edge if position is inverted, this looks better + else + newX = maxX - particleHardRadius; + } + + if ((newX < 0) || (newX > maxX)) // check if particle reached an edge (note: this also checks out of bounds and must not be skipped, even if bounce is enabled) + { + if (options->wrapX) + { + newX = (uint16_t)newX % (maxX + 1); + } + else if (((newX <= -PS_P_HALFRADIUS) || (newX > maxX + PS_P_HALFRADIUS))) // particle is leaving, set out of bounds if it has fully left + { + bool isleaving = true; + //TODO: need to update this once particle size is implemented + if (true) // using individual particle size + { + if (((newX > -particleHardRadius) && (newX < maxX + particleHardRadius))) // large particle is not yet leaving the view - note: this is not pixel perfect but good enough + isleaving = false; + } + + if (isleaving) + { + part.outofbounds = 1; + if (options->killoutofbounds) + part.ttl = 0; + } + } + } + part.x = (int16_t)newX; // set new position + } +} + + +// apply gravity to all particles using PS global gforce setting +// force is in 3.4 fixed point notation, see note above +// note: faster than apply force since direction is always down and counter is fixed for all particles +void ParticleSystem1D::applyGravity() +{ + int32_t dv = calcForce_dv(gforce, &gforcecounter); + for (uint32_t i = 0; i < usedParticles; 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].vx = limitSpeed((int32_t)particles[i].vx - dv); + } +} + +// apply gravity to single particle using system settings (use this for sources) +// function does not increment gravity counter, if gravity setting is disabled, this cannot be used +void ParticleSystem1D::applyGravity(PSparticle1D *part) +{ + int32_t dv; // velocity increase + if (gforce > 15) + dv = (gforce >> 4); // apply the 4 MSBs + else + dv = 1; + + if (gforcecounter + gforce > 15) //counter is updated in global update when applying gravity + { + part->vx = limitSpeed((int32_t)part->vx - dv); + } +} + + +// slow down particle by friction, the higher the speed, the higher the friction. a high friction coefficient slows them more (255 means instant stop) +// note: a coefficient smaller than 0 will speed them up (this is a feature, not a bug), coefficient larger than 255 inverts the speed, so don't do that +void ParticleSystem1D::applyFriction(int32_t coefficient) +{ + int32_t friction = 255 - coefficient; + for (uint32_t i = 0; i < usedParticles; i++) + { + if (particles[i].ttl) + particles[i].vx = ((int16_t)particles[i].vx * friction) / 255; + } +} + + +// 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 +// warning: do not render out of bounds particles or system will crash! rendering does not check if particle is out of bounds +void ParticleSystem1D::ParticleSys_render() +{ + + CRGB baseRGB; + bool useLocalBuffer = true; //use local rendering buffer, gives huge speed boost (at least 30% more FPS) + CRGB *framebuffer = NULL; //local frame buffer + //CRGB **renderbuffer = NULL; //local particle render buffer for advanced particles + uint32_t i; + uint32_t brightness; // particle brightness, fades if dying + + if (useLocalBuffer) + { + /* + //memory fragmentation check: + Serial.print("heap: "); + Serial.print(heap_caps_get_free_size(MALLOC_CAP_8BIT)); + Serial.print(" block: "); + Serial.println(heap_caps_get_largest_free_block(MALLOC_CAP_8BIT)); + */ + + // allocate empty memory for the local renderbuffer + framebuffer = allocate1Dbuffer(maxXpixel + 1); + if (framebuffer == NULL) + { + Serial.println("Frame buffer alloc failed"); + useLocalBuffer = false; //render to segment pixels directly if not enough memory + } + else{ + //if (advPartProps) + //{ + // renderbuffer = allocate2Dbuffer(10, 10); //buffer to render individual particles to if size > 0. note: null checking is done when accessing it + //} + if (motionBlur > 0) // using SEGMENT.fadeToBlackBy is much slower, this approximately doubles the speed of fade calculation + { + for (uint32_t x = 0; x <= maxXpixel; x++) + { + framebuffer[x] = SEGMENT.getPixelColor(x); //copy to local buffer + fast_color_scale(framebuffer[x], motionBlur); + } + } + } + } + + if (!useLocalBuffer) //disabled or allocation above failed + { + Serial.println("NOT using local buffer!"); + if (motionBlur > 0) + SEGMENT.fadeToBlackBy(255 - motionBlur); + else + SEGMENT.fill(BLACK); //clear the buffer before rendering to it + } + // go over particles and render them to the buffer + for (i = 0; i < usedParticles; i++) + { + if (particles[i].outofbounds || particles[i].ttl == 0) + continue; + + // 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) + { + CHSV baseHSV = rgb2hsv_approximate(baseRGB); //convert to HSV + baseHSV.s = particles[i].sat; //set the saturation + baseRGB = (CRGB)baseHSV; // convert back to RGB + }*/ + + renderParticle(framebuffer, i, brightness, baseRGB); + } + + if (useLocalBuffer) // transfer local buffer back to segment + { + for (int x = 0; x <= maxXpixel; x++) + { + SEGMENT.setPixelColor(x, framebuffer[x]); + } + free(framebuffer); + } +} + +// calculate pixel positions and brightness distribution and render the particle to local buffer or global buffer +void ParticleSystem1D::renderParticle(CRGB *framebuffer, uint32_t particleindex, uint32_t brightness, CRGB color) +{ + if(particlesize == 0) //single pixel particle + { + int32_t x = particles[particleindex].x >> PS_P_RADIUS_SHIFT; + //no need to check out of frame, that is done by the move function + if (framebuffer) + { + fast_color_add(framebuffer[x], color, brightness); // order is: bottom left, bottom right, top right, top left + } + else + { + SEGMENT.addPixelColor(x, color.scale8((uint8_t)brightness)); + } + } + else { //render larger particles + int32_t pxlbrightness[2] = {0}; // note: pxlbrightness needs to be set to 0 or checking does not work + int32_t pixco[2]; // physical pixel coordinates of the two pixels representing a particle + // subtract half a radius as the rendering algorithm always starts at the left, this makes calculations more efficient + int32_t xoffset = particles[particleindex].x - PS_P_HALFRADIUS; + int32_t dx = xoffset % PS_P_RADIUS; //relativ particle position in subpixel space + int32_t x = xoffset >> PS_P_RADIUS_SHIFT; // divide by PS_P_RADIUS which is 64, so can bitshift (compiler may not optimize automatically) + + // set the raw pixel coordinates + pixco[0] = x; // left pixel + pixco[1] = x + 1; // right pixel + + // 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 + { + dx = PS_P_RADIUS + dx; // if x<0, xoffset becomes negative (and so does dx), must adjust dx as modulo will flip its value + // 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) + { + pxlbrightness[1] = -1; // pixel is actually out of matrix boundaries, do not render + } + if (particlesettings.wrapX) // wrap x to the other side if required + pixco[0] = maxXpixel; + else + pxlbrightness[0] = -1; // pixel is out of matrix boundaries, do not render + } + else if (pixco[1] > maxXpixel) // right pixel, only has to be checkt if left pixel did not overflow + { + if (particlesettings.wrapX) // wrap y to the other side if required + pixco[1] = 0; + else + pxlbrightness[1] = -1; + } + + // calculate brightness values for the two pixels representing a particle using linear interpolation + + //calculate the values for pixels that are in frame + if (pxlbrightness[0] >= 0) + pxlbrightness[0] = (((int32_t)PS_P_RADIUS - dx) * brightness) >> PS_P_SURFACE_1D; + if (pxlbrightness[1] >= 0) + pxlbrightness[1] = (dx * brightness) >> PS_P_SURFACE_1D; + + // standard rendering (2 pixels per particle) + if (framebuffer) + { + for(uint32_t i = 0; i < 2; i++) + { + if (pxlbrightness[i] > 0) + fast_color_add(framebuffer[pixco[i]], color, pxlbrightness[i]); // order is: bottom left, bottom right, top right, top left + } + } + else + { + for(uint32_t i = 0; i < 2; i++) + { + if (pxlbrightness[i] > 0) + SEGMENT.addPixelColor(pixco[i], color.scale8((uint8_t)pxlbrightness[i])); + } + } + } +} + +// detect collisions in an array of particles and handle them +void ParticleSystem1D::handleCollisions() +{ + // detect and handle collisions + uint32_t i, j; + uint32_t startparticle = 0; + uint32_t endparticle = usedParticles >> 1; // do half the particles, significantly speeds things up + // 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 more accurate collisions are needed, just call it twice in a row + if (collisioncounter & 0x01) + { + startparticle = endparticle; + 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].outofbounds == 0 && particles[i].collide) // if particle is alive and does collide and is not out of view + { + int32_t dx; // distance to other particles + 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 < particleHardRadius && dx > -particleHardRadius) // check x direction, if close, check y direction + { + collideParticles(&particles[i], &particles[j]); + } + } + } + } + } +} + +// 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 ParticleSystem1D::collideParticles(PSparticle1D *particle1, PSparticle1D *particle2) // TODO: dx,dy is calculated just above, can pass it over here to save a few CPU cycles? +{ + int32_t dx = particle2->x - particle1->x; + // Calculate relative velocity (if it is zero, could exit but extra check does not overall speed but deminish it) + int32_t relativeVx = (int16_t)particle2->vx - (int16_t)particle1->vx; + + // if dx is zero (i.e. they meet at the center) give them an offset, if speeds are also zero, also offset them (pushes them apart if they are clumped before enabling collisions) + if (dx == 0) + { + // Adjust positions based on relative velocity direction + dx = -1; + if (relativeVx < 0) // if true, particle2 is on the right side + dx = 1; + else if (relativeVx == 0) + relativeVx = 1; + } + + // Calculate dot product of relative velocity and relative distance + int32_t dotProduct = (dx * relativeVx); // is always negative if moving towards each other + int32_t notsorandom = dotProduct & 0x01; //dotprouct LSB should be somewhat random, so no need to calculate a random number + + if (dotProduct < 0) // particles are moving towards each other + { + // integer math used to avoid floats. + // overflow check: dx/dy are 7bit, relativV are 8bit -> dotproduct is 15bit, dotproduct/distsquared ist 8b, multiplied by collisionhardness of 8bit. so a 16bit shift is ok, make it 15 to be sure no overflows happen + // note: cannot use right shifts as bit shifting in right direction is asymmetrical for positive and negative numbers and this needs to be accurate! the trick is: only shift positive numers + // Calculate new velocities after collision + uint32_t surfacehardness = collisionHardness < PS_P_MINSURFACEHARDNESS ? PS_P_MINSURFACEHARDNESS : collisionHardness; // if particles are soft, the impulse must stay above a limit or collisions slip through at higher speeds, 170 seems to be a good value + int32_t impulse = -(((((-dotProduct) << 15) / dx) * surfacehardness) >> 23); // note: inverting before bitshift corrects for asymmetry in right-shifts (and is slightly faster) + //int32_t ximpulse = ((impulse) * dx) / 32767; // cannot use bit shifts here, it can be negative, use division by 2^bitshift + particle1->vx += impulse; //TODO: need to flip directions? + particle2->vx -= impulse; + + // particles have volume, push particles apart if they are too close + // tried lots of configurations, it works best if not moved but given a little velocity, it tends to oscillate less this way + // a problem with giving velocity is, that on harder collisions, this adds up as it is not dampened enough, so add friction in the FX if required + if (dotProduct > -250) //this means particles are slow (or really really close) so push them apart. + { + int32_t pushamount = 1 + ((250 + dotProduct) >> 6); // the closer dotproduct is to zero, the closer the particles are + int32_t push = 0; + if (dx < 0) // particle 1 is on the right + push = pushamount; + else if (dx > 0) + push = -pushamount; + else // on the same x coordinate, shift it a little so they do not stack + { + if (notsorandom) + particle1->x++; // move it so pile collapses + else + particle1->x--; + } + particle1->vx += push; + // note: pushing may push particles out of frame, if bounce is active, it will move it back as position will be limited to within frame, if bounce is disabled: bye bye + } + } +} + + +// calculate the delta speed (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 ParticleSystem1D::calcForce_dv(int8_t force, uint8_t* counter) +{ + if (force == 0) + return 0; + // 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 : 1; // force is either, 1 or -1 if it is small (zero force is handled above) + } + } + else + { + dv = force >> 4; // MSBs + } + return dv; +} + +// limit speed to prevent overflows +int32_t ParticleSystem1D::limitSpeed(int32_t speed) +{ + return speed > PS_P_MAXSPEED ? PS_P_MAXSPEED : (speed < -PS_P_MAXSPEED ? -PS_P_MAXSPEED : speed); +} + +// allocate memory for the 2D array in one contiguous block and set values to zero +CRGB *ParticleSystem1D::allocate1Dbuffer(uint32_t length) +{ + CRGB *array = (CRGB *)calloc(length, sizeof(CRGB)); + //if (array == NULL) + // DEBUG_PRINT(F("PS 1D buffer alloc failed")); + return array; +} + +// update size and pointers (memory location and size can change dynamically) +// note: do not access the PS class in FX befor running this function (or it messes up SEGMENT.data) +void ParticleSystem1D::updateSystem(void) +{ + // update size + setSize(SEGMENT.virtualLength()); + updatePSpointers(); +} + +// 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 ParticleSystem1D::updatePSpointers(void) +{ + // DEBUG_PRINT(F("*** PS pointers ***")); + // DEBUG_PRINTF_P(PSTR("this PS %p "), this); + // Note on memory alignment: + // a pointer MUST be 4 byte aligned. sizeof() in a struct/class is always aligned to the largest element. if it contains a 32bit, it will be padded to 4 bytes, 16bit is padded to 2byte alignment. + // The PS is aligned to 4 bytes, a PSparticle is aligned to 2 and a struct containing only byte sized variables is not aligned at all and may need to be padded when dividing the memoryblock. + // by making sure that the number of sources and particles is a multiple of 4, padding can be skipped here as alignent is ensured, independent of struct sizes. + particles = reinterpret_cast(this + 1); // pointer to particle array at data+sizeof(ParticleSystem) + sources = reinterpret_cast(particles + numParticles); // pointer to source(s) + PSdataEnd = reinterpret_cast(sources + numSources); // pointer to first available byte after the PS for FX additional data + //TODO: ist das alignment wirklich korrekt (immer!)? + /* + DEBUG_PRINTF_P(PSTR(" particles %p "), particles); + DEBUG_PRINTF_P(PSTR(" sources %p "), sources); + DEBUG_PRINTF_P(PSTR(" adv. props %p "), advPartProps); + DEBUG_PRINTF_P(PSTR(" adv. ctrl %p "), advPartSize); + DEBUG_PRINTF_P(PSTR("end %p\n"), PSdataEnd); + */ +} + + +//non class functions to use for initialization +uint32_t calculateNumberOfParticles1D(void) +{ + uint32_t numberofParticles = SEGMENT.virtualLength(); // one particle per pixel (if possible) +#ifdef ESP8266 + uint32_t particlelimit = ESP8266_MAXPARTICLES_1D; // maximum number of paticles allowed (based on one segment of 16x16 and 4k effect ram) +#elif ARDUINO_ARCH_ESP32S2 + uint32_t particlelimit = ESP32S2_MAXPARTICLES_1D; // maximum number of paticles allowed (based on one segment of 32x32 and 24k effect ram) +#else + uint32_t particlelimit = ESP32_MAXPARTICLES_1D; // maximum number of paticles allowed (based on two segments of 32x32 and 40k effect ram) +#endif + numberofParticles = max((uint32_t)1, min(numberofParticles, particlelimit)); + //make sure it is a multiple of 4 for proper memory alignment (easier than using padding bytes) + numberofParticles = ((numberofParticles+3) >> 2) << 2; + return numberofParticles; +} + +uint32_t calculateNumberOfSources1D(uint8_t requestedsources) +{ +#ifdef ESP8266 + int numberofSources = max(1, min((int)requestedsources,ESP8266_MAXSOURCES_1D)); // limit to 1 - 8 +#elif ARDUINO_ARCH_ESP32S2 + int numberofSources = max(1, min((int)requestedsources, ESP32S2_MAXSOURCES_1D)); // limit to 1 - 16 +#else + int numberofSources = max(1, min((int)requestedsources, ESP32_MAXSOURCES_1D)); // limit to 1 - 32 +#endif + // make sure it is a multiple of 4 for proper memory alignment + numberofSources = ((numberofSources+3) >> 2) << 2; + return numberofSources; +} + +//allocate memory for particle system class, particles, sprays plus additional memory requested by FX +bool allocateParticleSystemMemory1D(uint16_t numparticles, uint16_t numsources, uint16_t additionalbytes) +{ + uint32_t requiredmemory = sizeof(ParticleSystem1D); + // functions above make sure these are a multiple of 4 bytes (to avoid alignment issues) + requiredmemory += sizeof(PSparticle) * numparticles; + requiredmemory += sizeof(PSsource) * numsources; + requiredmemory += additionalbytes; + //Serial.print("allocating: "); + //Serial.print(requiredmemory); + //Serial.println("Bytes"); + //Serial.print("allocating for segment at"); + //Serial.println((uintptr_t)SEGMENT.data); + return(SEGMENT.allocateData(requiredmemory)); +} + +// initialize Particle System, allocate additional bytes if needed (pointer to those bytes can be read from particle system class: PSdataEnd) +bool initParticleSystem1D(ParticleSystem1D *&PartSys, uint8_t requestedsources, uint16_t additionalbytes) +{ + //Serial.println("PS init function"); + uint32_t numparticles = calculateNumberOfParticles1D(); + uint32_t numsources = calculateNumberOfSources1D(requestedsources); + //Serial.print("numsources: "); + //Serial.println(numsources); + if (!allocateParticleSystemMemory1D(numparticles, numsources, additionalbytes)) + { + DEBUG_PRINT(F("PS init failed: memory depleted")); + return false; + } + //Serial.print("segment.data ptr"); + //Serial.println((uintptr_t)(SEGMENT.data)); + 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) ParticleSystem1D(SEGMENT.virtualLength(), 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 57a54e5406..e490e64ece 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -178,7 +178,7 @@ class ParticleSystem private: //rendering functions void ParticleSys_render(bool firemode = false, uint32_t fireintensity = 128); - void renderParticle(CRGB **framebuffer, uint32_t particleindex, uint32_t brightess, CRGB color, CRGB **renderbuffer); + void renderParticle(CRGB **framebuffer, uint32_t particleindex, uint32_t brightness, CRGB color, CRGB **renderbuffer); //paricle physics applied by system if flags are set void applyGravity(); // applies gravity to all particles @@ -217,9 +217,134 @@ bool initParticleSystem(ParticleSystem *&PartSys, uint8_t requestedsources, uint uint32_t calculateNumberOfParticles(bool advanced, bool sizecontrol); uint32_t calculateNumberOfSources(uint8_t requestedsources); bool allocateParticleSystemMemory(uint16_t numparticles, uint16_t numsources, bool advanced, bool sizecontrol, uint16_t additionalbytes); + +#endif // WLED_DISABLE_PARTICLESYSTEM + // color functions void fast_color_add(CRGB &c1, CRGB &c2, uint32_t scale = 255); // fast and accurate color adding with scaling (scales c2 before adding) void fast_color_scale(CRGB &c, uint32_t scale); // fast scaling function using 32bit variable and pointer. note: keep 'scale' within 0-255 void blur2D(CRGB **colorbuffer, uint32_t xsize, uint32_t ysize, uint32_t xblur, uint32_t yblur, bool smear = true, uint32_t xstart = 0, uint32_t ystart = 0, bool isparticle = false); -#endif // WLED_DISABLE_PARTICLESYSTEM + +//////////////////////// +// 1D Particle System // +//////////////////////// + +// memory allocation +#define ESP8266_MAXPARTICLES_1D 400 +#define ESP8266_MAXSOURCES_1D 8 +#define ESP32S2_MAXPARTICLES_1D 1900 +#define ESP32S2_MAXSOURCES_1D 16 +#define ESP32_MAXPARTICLES_1D 6000 +#define ESP32_MAXSOURCES_1D 32 + +// particle dimensions (subpixel division) +#define PS_P_RADIUS_1D 64 // subpixel size, each pixel is divided by this for particle movement, if this value is changed, also change the shift defines (next two lines) +#define PS_P_HALFRADIUS_1D 32 +#define PS_P_RADIUS_SHIFT_1D 6 //TODO: may need to adjust +#define PS_P_SURFACE_1D 6 // shift: 2^PS_P_SURFACE = PS_P_RADIUS_1D +#define PS_P_MINHARDRADIUS_1D 70 // minimum hard surface radius TODO: also needs tweaking + +//struct for a single particle (6 bytes) +typedef struct { + int16_t x; // x position in particle system + int8_t vx; // horizontal velocity + uint8_t hue; // color hue + // 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 set, particle takes part in collisions + bool perpetual : 1; // if set, particle does not age (TTL is not decremented in move function, it still dies from killoutofbounds) + bool flag4 : 1; // unused flag +} PSparticle1D; + +//struct for a particle source (17 bytes) +typedef struct { + uint16_t minLife; // minimum ttl of emittet particles + uint16_t maxLife; // maximum ttl of emitted particles + PSparticle1D source; // use a particle as the emitter source (speed, position, color) + int8_t var; // variation of emitted speed (adds random(+/- var) to speed) + int8_t v; // emitting speed + uint8_t size; // particle size (advanced property) TODO: can be removed in case this is not being implemented +} PSsource1D; + + +class ParticleSystem1D +{ +public: + ParticleSystem1D(uint16_t length, 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 updateSystem(void); // call at the beginning of every FX, updates pointers and dimensions + + // particle emitters + void sprayEmit(PSsource1D &emitter); + void particleMoveUpdate(PSparticle1D &part, PSsettings *options = NULL); // move function + //particle physics + void applyGravity(PSparticle1D *part); // applies gravity to single particle (use this for sources) + void applyFriction(int32_t coefficient); // apply friction to all used particles + + // set options + void setUsedParticles(uint16_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 setSize(uint16_t x); //set particle system size (= strip length) + void setWrap(bool enable); + void setBounce(bool enable); + void setKillOutOfBounds(bool enable); // if enabled, particles outside of matrix instantly die + // void setSaturation(uint8_t sat); // set global color saturation + void setColorByAge(bool enable); + void setMotionBlur(uint8_t bluramount); // note: motion blur can only be used if 'particlesize' is set to zero + void setParticleSize(uint8_t size); //size 0 = 1 pixel, size 1 = 2 pixels + void setGravity(int8_t force = 8); + void enableParticleCollisions(bool enable, uint8_t hardness = 255); + + PSparticle1D *particles; // pointer to particle array + PSsource1D *sources; // pointer to sources + //PSadvancedParticle *advPartProps; // pointer to advanced particle properties (can be NULL) + //PSsizeControl *advPartSize; // pointer to advanced particle size control (can be NULL) + uint8_t* PSdataEnd; // points to first available byte after the PSmemory, is set in setPointers(). use this for FX custom data + uint16_t maxX; // particle system size i.e. width-1 + uint32_t maxXpixel; // last physical pixel that can be drawn to (FX can read this to read segment size if required), equal to width-1 + uint8_t numSources; // number of sources + uint16_t numParticles; // number of particles available in this system + uint16_t usedParticles; // number of particles used in animation (can be smaller then numParticles) + +private: + //rendering functions + void ParticleSys_render(void); + void renderParticle(CRGB *framebuffer, uint32_t particleindex, uint32_t brightness, CRGB color); + + //paricle physics applied by system if flags are set + void applyGravity(); // applies gravity to all particles + void handleCollisions(); + void collideParticles(PSparticle1D *particle1, PSparticle1D *particle2); + + //utility functions + void updatePSpointers(void); // update the data pointers to current segment data space + //void updateSize(PSadvancedParticle *advprops, PSsizeControl *advsize); // advanced size control + //void getParticleXYsize(PSadvancedParticle *advprops, PSsizeControl *advsize, uint32_t &xsize, uint32_t &ysize); + void bounce(int8_t &incomingspeed, int8_t ¶llelspeed, int32_t &position, uint16_t maxposition); // bounce on a wall + CRGB *allocate1Dbuffer(uint32_t length); + int32_t calcForce_dv(int8_t force, uint8_t *counter); //TODO: same as 2D function, could share + int32_t limitSpeed(int32_t speed); //TODO: same as 2D function, could share + // note: variables that are accessed often are 32bit for speed + PSsettings particlesettings; // settings used when updating particles + uint32_t emitIndex; // index to count through particles to emit so searching for dead pixels is faster + int32_t collisionHardness; + uint8_t wallHardness; + uint8_t gforcecounter; // counter for global gravity + int8_t gforce; // gravity strength, default is 8 (negative is allowed, positive is downwards) + uint8_t forcecounter; // counter for globally applied forces + uint8_t collisioncounter; // counter to handle collisions TODO: could use the SEGMENT.call? + // global particle properties for basic particles + uint8_t particlesize; // global particle size, 0 = 1 pixel, 1 = 2 pixels, larger sizez TBD (TODO: need larger sizes?) + int32_t particleHardRadius; // hard surface radius of a particle, used for collision detection + uint8_t motionBlur; // enable motion blur, values > 100 gives smoother animations. Note: motion blurring does not work if particlesize is > 0 +}; + +bool initParticleSystem1D(ParticleSystem1D *&PartSys, uint8_t requestedsources, uint16_t additionalbytes = 0); +uint32_t calculateNumberOfParticles1D(void); +uint32_t calculateNumberOfSources1D(uint8_t requestedsources); +bool allocateParticleSystemMemory1D(uint16_t numparticles, uint16_t numsources, uint16_t additionalbytes); + From 32b16c47b72761302d3b9738044f760d77f4a8f4 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Wed, 29 May 2024 07:35:20 +0200 Subject: [PATCH 086/219] fixed first bugs in 1D system, added test FX --- wled00/FX.cpp | 78 +++++++++++++++++++++++++++++++++++++ wled00/FX.h | 3 +- wled00/FXparticleSystem.cpp | 43 ++++++++------------ 3 files changed, 97 insertions(+), 27 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 3944f5d01a..5e18cf267d 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -9652,6 +9652,81 @@ static const char _data_FX_MODE_PARTICLEBLOBS[] PROGMEM = "PS Blobs@Speed,Blobs, #endif // WLED_DISABLE_2D +//////// +// 1D PS test + + +/* +Particle Spray, just a particle spray with many parameters +Uses palette for particle color +by DedeHai (Damian Schneider) +*/ + +uint16_t mode_particle1Dtest(void) +{ + if (SEGLEN == 1) + return mode_static(); + ParticleSystem1D *PartSys = NULL; + //uint8_t numSprays; + const uint8_t hardness = 200; // collision hardness is fixed + + if (SEGMENT.call == 0) // initialization + { + if (!initParticleSystem1D(PartSys, 4)) // init, no additional data needed + return mode_static(); // allocation failed; //allocation failed + PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting) + PartSys->sources[0].source.hue = random16(); + PartSys->sources[0].source.collide = true; // seeded particles will collide (if enabled) + + + } + else + PartSys = reinterpret_cast(SEGMENT.data); // if not first call, just set the pointer to the PS + + if (PartSys == NULL) + { + DEBUG_PRINT(F("ERROR: FX PartSys nullpointer")); + return mode_static(); // something went wrong, no data! + } + + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + //PartSys->setBounce(!SEGMENT.check2); + PartSys->setWrap(SEGMENT.check2); + PartSys->setWallHardness(hardness); + PartSys->setGravity(8 * SEGMENT.check1); // enable gravity if checked (8 is default strength) + //numSprays = min(PartSys->numSources, (uint8_t)1); // number of sprays + PartSys->sources[0].var = 0;//SEGMENT.speed/16; + PartSys->sources[0].v = SEGMENT.speed/2; + // if (SEGMENT.check3) // collisions enabled + // PartSys->enableParticleCollisions(true, hardness); // enable collisions and set particle collision hardness + //else + // PartSys->enableParticleCollisions(false); + PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur + if (SEGMENT.check3) // collisions enabled + PartSys->setParticleSize(1); + else + PartSys->setParticleSize(0); + + //position according to sliders + PartSys->sources[0].source.x = map(SEGMENT.custom1, 0, 255, 0, PartSys->maxX); + + // change source properties + if (SEGMENT.call % (64 - (SEGMENT.intensity / 4)) == 0) // every nth frame, cycle color and emit particles + { + PartSys->sources[0].maxLife = 300; // lifetime in frames + PartSys->sources[0].minLife = 100; + PartSys->sources[0].source.hue++; // = random16(); //change hue of spray source + PartSys->sprayEmit(PartSys->sources[0]); + } + + PartSys->update(); // update and render + return FRAMETIME; +} +static const char _data_FX_MODE_PARTICLETEST[] PROGMEM = "PS 1D Testy@Speed,!,Left/Right,Up/Down,Angle,Gravity,Cylinder/Square,Collisions;;!;12v;pal=0,sx=150,ix=150,c1=220,c2=30,c3=21,o1=0,o2=0,o3=0"; + + + ////////////////////////////////////////////////////////////////////////////////////////// // mode data static const char _data_RESERVED[] PROGMEM = "RSVD"; @@ -9908,6 +9983,9 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_PARTICLECENTERGEQ, &mode_particlecenterGEQ, _data_FX_MODE_PARTICLECCIRCULARGEQ); #endif // WLED_DISABLE_PARTICLESYSTEM +addEffect(FX_MODE_PARTICLETEST, &mode_particle1Dtest, _data_FX_MODE_PARTICLETEST); + + #endif // WLED_DISABLE_2D } diff --git a/wled00/FX.h b/wled00/FX.h index 4ed24153df..6bbb66603d 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -332,7 +332,8 @@ #define FX_MODE_PARTICLECENTERGEQ 199 #define FX_MODE_PARTICLEGHOSTRIDER 200 #define FX_MODE_PARTICLEBLOBS 201 -#define MODE_COUNT 202 +#define FX_MODE_PARTICLETEST 202 +#define MODE_COUNT 203 typedef enum mapping1D2D { M12_Pixels = 0, diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index 1f3927a205..667172b7cd 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -1626,7 +1626,7 @@ void ParticleSystem1D::setCollisionHardness(uint8_t hardness) void ParticleSystem1D::setSize(uint16_t x) { maxXpixel = x - 1; // last physical pixel that can be drawn to - maxX = x * PS_P_RADIUS - 1; // particle system boundary for movements + maxX = x * PS_P_RADIUS_1D - 1; // particle system boundary for movements } void ParticleSystem1D::setWrap(bool enable) @@ -1658,6 +1658,7 @@ void ParticleSystem1D::setMotionBlur(uint8_t bluramount) // render size using smearing (see blur function) void ParticleSystem1D::setParticleSize(uint8_t size) { + particleHardRadius = PS_P_RADIUS_1D; //TODO: need to set this properly for particle size particlesize = size; } // enable/disable gravity, optionally, set the force (force=8 is default) can be 1-255, 0 is disable @@ -1716,17 +1717,20 @@ void ParticleSystem1D::particleMoveUpdate(PSparticle1D &part, PSsettings *option //bool usesize = false; // particle uses individual size rendering int32_t newX = part.x + (int16_t)part.vx; part.outofbounds = 0; // reset out of bounds (in case particle was created outside the matrix and is now moving into view) - + Serial.print(part.x); + Serial.print(" "); // if wall collisions are enabled, bounce them before they reach the edge, it looks much nicer if the particle is not half out of view if (options->bounceX) { if ((newX < particleHardRadius) || (newX > maxX - particleHardRadius)) // reached a wall + { part.vx = -part.vx; //invert speed - part.vx = ((int32_t)-part.vx * wallHardness) / 255; // reduce speed as energy is lost on non-hard surface + part.vx = ((int32_t)part.vx * wallHardness) / 255; // reduce speed as energy is lost on non-hard surface if (newX < particleHardRadius) newX = particleHardRadius; // fast particles will never reach the edge if position is inverted, this looks better else newX = maxX - particleHardRadius; + } } if ((newX < 0) || (newX > maxX)) // check if particle reached an edge (note: this also checks out of bounds and must not be skipped, even if bounce is enabled) @@ -1735,22 +1739,11 @@ void ParticleSystem1D::particleMoveUpdate(PSparticle1D &part, PSsettings *option { newX = (uint16_t)newX % (maxX + 1); } - else if (((newX <= -PS_P_HALFRADIUS) || (newX > maxX + PS_P_HALFRADIUS))) // particle is leaving, set out of bounds if it has fully left + else if (((newX <= -PS_P_HALFRADIUS_1D) || (newX > maxX + PS_P_HALFRADIUS_1D))) // particle is leaving, set out of bounds if it has fully left { - bool isleaving = true; - //TODO: need to update this once particle size is implemented - if (true) // using individual particle size - { - if (((newX > -particleHardRadius) && (newX < maxX + particleHardRadius))) // large particle is not yet leaving the view - note: this is not pixel perfect but good enough - isleaving = false; - } - - if (isleaving) - { part.outofbounds = 1; if (options->killoutofbounds) part.ttl = 0; - } } } part.x = (int16_t)newX; // set new position @@ -1888,18 +1881,16 @@ void ParticleSystem1D::ParticleSys_render() // calculate pixel positions and brightness distribution and render the particle to local buffer or global buffer void ParticleSystem1D::renderParticle(CRGB *framebuffer, uint32_t particleindex, uint32_t brightness, CRGB color) { - if(particlesize == 0) //single pixel particle + if(particlesize == 0) //single pixel particle, can be out of bounds as oob checking is made for 2-pixel particles { - int32_t x = particles[particleindex].x >> PS_P_RADIUS_SHIFT; - //no need to check out of frame, that is done by the move function - if (framebuffer) - { - fast_color_add(framebuffer[x], color, brightness); // order is: bottom left, bottom right, top right, top left - } - else - { - SEGMENT.addPixelColor(x, color.scale8((uint8_t)brightness)); - } + uint32_t x = particles[particleindex].x >> PS_P_RADIUS_SHIFT; + if(x <= maxXpixel) //by making x unsigned there is no need to check < 0 as it will overflow + { + if (framebuffer) + fast_color_add(framebuffer[x], color, brightness); // order is: bottom left, bottom right, top right, top left + else + SEGMENT.addPixelColor(x, color.scale8((uint8_t)brightness)); + } } else { //render larger particles int32_t pxlbrightness[2] = {0}; // note: pxlbrightness needs to be set to 0 or checking does not work From 64d138222eabf8963f6c149c8888f0999a4e7c67 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Wed, 29 May 2024 08:17:37 +0200 Subject: [PATCH 087/219] fixed bug in wrapping (also was wrong for 2D system) --- wled00/FX.cpp | 6 +++--- wled00/FXparticleSystem.cpp | 19 +++++++++++-------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 5e18cf267d..947c4c8bb5 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -9691,13 +9691,13 @@ uint16_t mode_particle1Dtest(void) // Particle System settings PartSys->updateSystem(); // update system properties (dimensions and data pointers) - //PartSys->setBounce(!SEGMENT.check2); + PartSys->setBounce(!SEGMENT.check2); PartSys->setWrap(SEGMENT.check2); PartSys->setWallHardness(hardness); PartSys->setGravity(8 * SEGMENT.check1); // enable gravity if checked (8 is default strength) //numSprays = min(PartSys->numSources, (uint8_t)1); // number of sprays PartSys->sources[0].var = 0;//SEGMENT.speed/16; - PartSys->sources[0].v = SEGMENT.speed/2; + PartSys->sources[0].v = -SEGMENT.speed/2; // if (SEGMENT.check3) // collisions enabled // PartSys->enableParticleCollisions(true, hardness); // enable collisions and set particle collision hardness //else @@ -9723,7 +9723,7 @@ uint16_t mode_particle1Dtest(void) PartSys->update(); // update and render return FRAMETIME; } -static const char _data_FX_MODE_PARTICLETEST[] PROGMEM = "PS 1D Testy@Speed,!,Left/Right,Up/Down,Angle,Gravity,Cylinder/Square,Collisions;;!;12v;pal=0,sx=150,ix=150,c1=220,c2=30,c3=21,o1=0,o2=0,o3=0"; +static const char _data_FX_MODE_PARTICLETEST[] PROGMEM = "PS 1D Testy@Speed,!,Position,Blur,Angle,Gravity,Cylinder/Square,Size;;!;12v;pal=0,sx=150,ix=150,c1=220,c2=30,c3=21,o1=0,o2=0,o3=0"; diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index 667172b7cd..18024af003 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -316,8 +316,10 @@ void ParticleSystem::particleMoveUpdate(PSparticle &part, PSsettings *options, P if ((newX < 0) || (newX > maxX)) // check if particle reached an edge (note: this also checks out of bounds and must not be skipped, even if bounce is enabled) { if (options->wrapX) - { - newX = (uint16_t)newX % (maxX + 1); + { + newX = newX % (maxX + 1); + if(newX < 0) + newX += maxX + 1; } else if (((newX <= -PS_P_HALFRADIUS) || (newX > maxX + PS_P_HALFRADIUS))) // particle is leaving, set out of bounds if it has fully left { @@ -355,7 +357,9 @@ void ParticleSystem::particleMoveUpdate(PSparticle &part, PSsettings *options, P { if (options->wrapY) { - newY = (uint16_t)newY % (maxY + 1); + newY = newY % (maxY + 1); + if(newY < 0) + newY += maxY + 1; } else if (((newY <= -PS_P_HALFRADIUS) || (newY > maxY + PS_P_HALFRADIUS))) // particle is leaving, set out of bounds if it has fully left { @@ -1717,8 +1721,6 @@ void ParticleSystem1D::particleMoveUpdate(PSparticle1D &part, PSsettings *option //bool usesize = false; // particle uses individual size rendering int32_t newX = part.x + (int16_t)part.vx; part.outofbounds = 0; // reset out of bounds (in case particle was created outside the matrix and is now moving into view) - Serial.print(part.x); - Serial.print(" "); // if wall collisions are enabled, bounce them before they reach the edge, it looks much nicer if the particle is not half out of view if (options->bounceX) { @@ -1732,12 +1734,13 @@ void ParticleSystem1D::particleMoveUpdate(PSparticle1D &part, PSsettings *option newX = maxX - particleHardRadius; } } - if ((newX < 0) || (newX > maxX)) // check if particle reached an edge (note: this also checks out of bounds and must not be skipped, even if bounce is enabled) { if (options->wrapX) - { - newX = (uint16_t)newX % (maxX + 1); + { + newX = newX % (maxX + 1); + if(newX < 0) + newX += maxX + 1; } else if (((newX <= -PS_P_HALFRADIUS_1D) || (newX > maxX + PS_P_HALFRADIUS_1D))) // particle is leaving, set out of bounds if it has fully left { From 3f6bc7d9b6cd10d307d58e55e7629208bf24ab8a Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Thu, 30 May 2024 12:02:56 +0200 Subject: [PATCH 088/219] fixed collisions, added bouncing ball replacement (work in progress) --- wled00/FX.cpp | 130 ++++++++++++++++++++++++++++++------ wled00/FX.h | 3 +- wled00/FXparticleSystem.cpp | 49 +++++++------- wled00/FXparticleSystem.h | 2 +- 4 files changed, 139 insertions(+), 45 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 947c4c8bb5..6b544410e6 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -9668,7 +9668,7 @@ uint16_t mode_particle1Dtest(void) return mode_static(); ParticleSystem1D *PartSys = NULL; //uint8_t numSprays; - const uint8_t hardness = 200; // collision hardness is fixed + const uint8_t hardness = 150; // collision hardness is fixed if (SEGMENT.call == 0) // initialization { @@ -9677,8 +9677,6 @@ uint16_t mode_particle1Dtest(void) PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting) PartSys->sources[0].source.hue = random16(); PartSys->sources[0].source.collide = true; // seeded particles will collide (if enabled) - - } else PartSys = reinterpret_cast(SEGMENT.data); // if not first call, just set the pointer to the PS @@ -9696,35 +9694,126 @@ uint16_t mode_particle1Dtest(void) PartSys->setWallHardness(hardness); PartSys->setGravity(8 * SEGMENT.check1); // enable gravity if checked (8 is default strength) //numSprays = min(PartSys->numSources, (uint8_t)1); // number of sprays - PartSys->sources[0].var = 0;//SEGMENT.speed/16; - PartSys->sources[0].v = -SEGMENT.speed/2; - // if (SEGMENT.check3) // collisions enabled - // PartSys->enableParticleCollisions(true, hardness); // enable collisions and set particle collision hardness - //else - // PartSys->enableParticleCollisions(false); + PartSys->sources[0].var = SEGMENT.custom3;//SEGMENT.speed/16; + PartSys->sources[0].v = SEGMENT.speed/2; + if (SEGMENT.check3) // collisions enabled + PartSys->enableParticleCollisions(true, SEGMENT.custom1); // enable collisions and set particle collision hardness + else + PartSys->enableParticleCollisions(false); PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur - if (SEGMENT.check3) // collisions enabled + + //if (SEGMENT.check3) // collisions enabled PartSys->setParticleSize(1); - else - PartSys->setParticleSize(0); + // else + // PartSys->setParticleSize(0); + + //position according to slider + //PartSys->sources[0].source.x = map(SEGMENT.custom1, 0, 255, 0, PartSys->maxX); - //position according to sliders - PartSys->sources[0].source.x = map(SEGMENT.custom1, 0, 255, 0, PartSys->maxX); - // change source properties - if (SEGMENT.call % (64 - (SEGMENT.intensity / 4)) == 0) // every nth frame, cycle color and emit particles + if (SEGMENT.call % (256 - (SEGMENT.intensity)) == 0) // every nth frame, cycle color and emit particles { - PartSys->sources[0].maxLife = 300; // lifetime in frames - PartSys->sources[0].minLife = 100; - PartSys->sources[0].source.hue++; // = random16(); //change hue of spray source + PartSys->sources[0].maxLife = 600; // lifetime in frames + PartSys->sources[0].minLife = 500; + PartSys->sources[0].source.hue = random16(); //change hue of spray source PartSys->sprayEmit(PartSys->sources[0]); } PartSys->update(); // update and render + uint32_t bg_color = SEGCOLOR(1); //background color, set to black to overlay + if(bg_color > 0) //if not black + { + for(uint32_t i = 0; i < PartSys->maxXpixel + 1; i++) + { + SEGMENT.addPixelColor(i,bg_color); + } + } + + + return FRAMETIME; } -static const char _data_FX_MODE_PARTICLETEST[] PROGMEM = "PS 1D Testy@Speed,!,Position,Blur,Angle,Gravity,Cylinder/Square,Size;;!;12v;pal=0,sx=150,ix=150,c1=220,c2=30,c3=21,o1=0,o2=0,o3=0"; +static const char _data_FX_MODE_PARTICLETEST[] PROGMEM = "PS 1D Testy@Speed,!,Position,Blur,Variation,Gravity,Cylinder/Square,Size;,!;!;12v;pal=0,sx=150,ix=150,c1=220,c2=30,c3=21,o1=0,o2=0,o3=0"; + + +/* +Particle Spray, just a particle spray with many parameters +Uses palette for particle color +by DedeHai (Damian Schneider) +*/ + +uint16_t mode_particleBouncingBalls(void) +{ + if (SEGLEN == 1) + return mode_static(); + ParticleSystem1D *PartSys = NULL; + const uint8_t hardness = 150; // collision hardness is fixed + + if (SEGMENT.call == 0) // initialization + { + if (!initParticleSystem1D(PartSys, 1)) // init, no additional data needed + return mode_static(); // allocation failed; //allocation failed + PartSys->sources[0].source.collide = true; // seeded particles will collide (if enabled) + PartSys->sources[0].source.x = PS_P_RADIUS_1D; //emit at bottom + PartSys->sources[0].maxLife = 700; // maximum lifetime in frames + PartSys->sources[0].minLife = PartSys->sources[0].maxLife; + PartSys->setBounce(true); + } + else + PartSys = reinterpret_cast(SEGMENT.data); // if not first call, just set the pointer to the PS + + if (PartSys == NULL) + { + DEBUG_PRINT(F("ERROR: FX PartSys nullpointer")); + return mode_static(); // something went wrong, no data! + } + + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setWallHardness(127 + (SEGMENT.custom1>>1)); + PartSys->setGravity(1 + (SEGMENT.custom3>>1)); // set gravity (8 is default strength) + PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur + if (SEGMENT.check2) PartSys->setMotionBlur(255); //full motion blurring allows overlay (motion blur does not work with overlay) + PartSys->sources[0].var = SEGMENT.speed >> 4; + PartSys->sources[0].v = SEGMENT.speed >> 1;// - SEGMENT.speed >> 4; + if (SEGMENT.check1) // collisions enabled + PartSys->enableParticleCollisions(true, 127 + (SEGMENT.custom1>>1)); // enable collisions and set particle collision hardness + else + PartSys->enableParticleCollisions(false); + if (SEGMENT.check3) + PartSys->setParticleSize(1); + else + PartSys->setParticleSize(0); + + //check for balls that are 'laying on the ground' and remove them + for(uint32_t i = 0; i < PartSys->usedParticles; i++) + { + if(PartSys->particles[i].vx == 0 && PartSys->particles[i].x < (PS_P_RADIUS_1D << 1)) + PartSys->particles[i].ttl = 0; + } + + // every nth frame emit a ball + if (SEGMENT.call % SEGMENT.aux0 == 0) + { + SEGMENT.aux0 = (300 - SEGMENT.intensity) + random(300 - SEGMENT.intensity); + PartSys->sources[0].source.hue = random16(); //set ball color + PartSys->sprayEmit(PartSys->sources[0]); + } + + PartSys->update(); // update and render + uint32_t bg_color = SEGCOLOR(1); //background color, set to black to overlay + if(bg_color > 0) //if not black + { + for(uint32_t i = 0; i < PartSys->maxXpixel + 1; i++) + { + SEGMENT.addPixelColor(i,bg_color); + } + } + + return FRAMETIME; +} +static const char _data_FX_MODE_PSBOUNCINGBALLS[] PROGMEM = "PS Bouncing Balls@Speed,!,Hardness,Blur,Gravity,Collide,Overlay,Size;,!;!;1;pal=0,sx=150,ix=150,c1=220,c2=30,c3=21,o1=0,o2=0,o3=0"; ////////////////////////////////////////////////////////////////////////////////////////// @@ -9984,6 +10073,7 @@ void WS2812FX::setupEffectData() { #endif // WLED_DISABLE_PARTICLESYSTEM addEffect(FX_MODE_PARTICLETEST, &mode_particle1Dtest, _data_FX_MODE_PARTICLETEST); +addEffect(FX_MODE_PSBOUNCINGBALLS, &mode_particleBouncingBalls, _data_FX_MODE_PSBOUNCINGBALLS); #endif // WLED_DISABLE_2D diff --git a/wled00/FX.h b/wled00/FX.h index 6bbb66603d..a0d8e976bd 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -333,7 +333,8 @@ #define FX_MODE_PARTICLEGHOSTRIDER 200 #define FX_MODE_PARTICLEBLOBS 201 #define FX_MODE_PARTICLETEST 202 -#define MODE_COUNT 203 +#define FX_MODE_PSBOUNCINGBALLS 203 +#define MODE_COUNT 204 typedef enum mapping1D2D { M12_Pixels = 0, diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index 18024af003..34cf8ae876 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -341,15 +341,9 @@ void ParticleSystem::particleMoveUpdate(PSparticle &part, PSsettings *options, P if (options->bounceY) { - if ((newY < particleHardRadius) || (newY > maxY - particleHardRadius)) // reached floor / ceiling - { - if (newY < particleHardRadius) // bounce at bottom - bounce(part.vy, part.vx, newY, maxY); - else - { - if (!options->useGravity) - bounce(part.vy, part.vx, newY, maxY); - } + if ((newY < particleHardRadius) || ((newY > maxY - particleHardRadius) && !options->useGravity)) // reached floor / ceiling + { + bounce(part.vy, part.vx, newY, maxY); } } @@ -1662,8 +1656,11 @@ void ParticleSystem1D::setMotionBlur(uint8_t bluramount) // render size using smearing (see blur function) void ParticleSystem1D::setParticleSize(uint8_t size) { - particleHardRadius = PS_P_RADIUS_1D; //TODO: need to set this properly for particle size particlesize = size; + if(particlesize) + particleHardRadius = PS_P_MINHARDRADIUS_1D + particlesize; //TODO: set lower on 1pixel particles? or set in bounce? does not bounce at boarder but boarder +1 + else + particleHardRadius = PS_P_MINHARDRADIUS_1D >> 1; //1 pixel sized particles have half the radius (for collisions & bounce) } // enable/disable gravity, optionally, set the force (force=8 is default) can be 1-255, 0 is disable // if enabled, gravity is applied to all particles in ParticleSystemUpdate() @@ -1724,7 +1721,7 @@ void ParticleSystem1D::particleMoveUpdate(PSparticle1D &part, PSsettings *option // if wall collisions are enabled, bounce them before they reach the edge, it looks much nicer if the particle is not half out of view if (options->bounceX) { - if ((newX < particleHardRadius) || (newX > maxX - particleHardRadius)) // reached a wall + if ((newX < particleHardRadius) || ((newX > maxX - particleHardRadius) && !options->useGravity)) // reached a wall { part.vx = -part.vx; //invert speed part.vx = ((int32_t)part.vx * wallHardness) / 255; // reduce speed as energy is lost on non-hard surface @@ -1746,7 +1743,12 @@ void ParticleSystem1D::particleMoveUpdate(PSparticle1D &part, PSsettings *option { part.outofbounds = 1; if (options->killoutofbounds) - part.ttl = 0; + { + if (newX < 0) // if gravity is enabled, only kill particles below ground + part.ttl = 0; + else if (!options->useGravity) + part.ttl = 0; + } } } part.x = (int16_t)newX; // set new position @@ -1964,15 +1966,15 @@ void ParticleSystem1D::handleCollisions() // detect and handle collisions uint32_t i, j; uint32_t startparticle = 0; - uint32_t endparticle = usedParticles >> 1; // do half the particles, significantly speeds things up + uint32_t endparticle = usedParticles;// >> 1; // do half the particles, significantly speeds things up // 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 more accurate collisions are needed, just call it twice in a row - if (collisioncounter & 0x01) - { - startparticle = endparticle; - endparticle = usedParticles; - } - collisioncounter++; + //if (collisioncounter & 0x01) + //{ + // startparticle = endparticle; + // endparticle = usedParticles; + //} + // collisioncounter++; for (i = startparticle; i < endparticle; i++) { @@ -1980,12 +1982,15 @@ void ParticleSystem1D::handleCollisions() if (particles[i].ttl > 0 && particles[i].outofbounds == 0 && particles[i].collide) // if particle is alive and does collide and is not out of view { int32_t dx; // distance to other particles + int32_t proximity; 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 < particleHardRadius && dx > -particleHardRadius) // check x direction, if close, check y direction + dx = particles[i].x - particles[j].x; + int32_t dv = (int32_t)particles[i].vx - (int32_t)particles[j].vx; + proximity = particleHardRadius + abs(dv); //add speed difference to catch fast particles + if (dx < proximity && dx > -proximity) // check if close { collideParticles(&particles[i], &particles[j]); } @@ -2192,8 +2197,6 @@ bool initParticleSystem1D(ParticleSystem1D *&PartSys, uint8_t requestedsources, } //Serial.print("segment.data ptr"); //Serial.println((uintptr_t)(SEGMENT.data)); - 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) ParticleSystem1D(SEGMENT.virtualLength(), numparticles, numsources); // particle system constructor //Serial.print("PS pointer at "); diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index e490e64ece..bb050baa48 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -243,7 +243,7 @@ void blur2D(CRGB **colorbuffer, uint32_t xsize, uint32_t ysize, uint32_t xblur, #define PS_P_HALFRADIUS_1D 32 #define PS_P_RADIUS_SHIFT_1D 6 //TODO: may need to adjust #define PS_P_SURFACE_1D 6 // shift: 2^PS_P_SURFACE = PS_P_RADIUS_1D -#define PS_P_MINHARDRADIUS_1D 70 // minimum hard surface radius TODO: also needs tweaking +#define PS_P_MINHARDRADIUS_1D 64 // minimum hard surface radius TODO: also needs tweaking //struct for a single particle (6 bytes) typedef struct { From 5f9819eb26232dca3d3915733c97155f9bc89c71 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Thu, 30 May 2024 16:10:17 +0200 Subject: [PATCH 089/219] replaced more FX, some tweaks to 1D PS - changed virtual particle size from 64 to 32, making them able to move faster - added 'rolling balls' option to bouncing balls (i.e. no gravity plus some tweaks) - added dancing shadows with PS (work in progress) - temporarily removed native FX: bouncing balls, rolling balls, popcorn, dancing shadows -and of course some bugfixes --- wled00/FX.cpp | 221 +++++++++++++++++++++++++++++++----- wled00/FX.h | 3 +- wled00/FXparticleSystem.cpp | 23 ++-- wled00/FXparticleSystem.h | 12 +- 4 files changed, 210 insertions(+), 49 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 6b544410e6..c34997d0a9 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -2954,17 +2954,18 @@ uint16_t mode_spots_fade() } static const char _data_FX_MODE_SPOTS_FADE[] PROGMEM = "Spots Fade@Spread,Width,,,,,Overlay;!,!;!"; - +/* //each needs 12 bytes typedef struct Ball { unsigned long lastBounceTime; float impactVelocity; float height; } ball; - +*/ /* * Bouncing Balls Effect */ +/* uint16_t mode_bouncing_balls(void) { if (SEGLEN == 1) return mode_static(); //allocate segment data @@ -3037,13 +3038,14 @@ uint16_t mode_bouncing_balls(void) { return FRAMETIME; } static const char _data_FX_MODE_BOUNCINGBALLS[] PROGMEM = "Bouncing Balls@Gravity,# of balls,,,,,Overlay;!,!,!;!;1;m12=1"; //bar - +*/ /* * bouncing balls on a track track Effect modified from Aircoookie's bouncing balls * Courtesy of pjhatch (https://github.com/pjhatch) * https://github.com/Aircoookie/WLED/pull/1039 */ + /* // modified for balltrack mode typedef struct RollingBall { unsigned long lastBounceUpdate; @@ -3138,7 +3140,7 @@ static uint16_t rolling_balls(void) { return FRAMETIME; } static const char _data_FX_MODE_ROLLINGBALLS[] PROGMEM = "Rolling Balls@!,# of balls,,,,Collisions,Overlay,Trails;!,!,!;!;1;m12=1"; //bar - +*/ /* * Sinelon stolen from FASTLED examples @@ -3248,6 +3250,7 @@ typedef struct Spark { * POPCORN * modified from https://github.com/kitesurfer1404/WS2812FX/blob/master/src/custom/Popcorn.h */ +/* uint16_t mode_popcorn(void) { if (SEGLEN == 1) return mode_static(); //allocate segment data @@ -3306,7 +3309,7 @@ uint16_t mode_popcorn(void) { return FRAMETIME; } static const char _data_FX_MODE_POPCORN[] PROGMEM = "Popcorn@!,!,,,,,Overlay;!,!,!;!;;m12=1"; //bar - +*/ //values close to 100 produce 5Hz flicker, which looks very candle-y //Inspired by https://github.com/avanhanegem/ArduinoCandleEffectNeoPixel @@ -4316,7 +4319,7 @@ uint16_t mode_chunchun(void) } static const char _data_FX_MODE_CHUNCHUN[] PROGMEM = "Chunchun@!,Gap size;!,!;!"; - +/* //13 bytes typedef struct Spotlight { float speed; @@ -4326,7 +4329,7 @@ typedef struct Spotlight { uint8_t width; uint8_t type; } spotlight; - +*/ #define SPOT_TYPE_SOLID 0 #define SPOT_TYPE_GRADIENT 1 #define SPOT_TYPE_2X_GRADIENT 2 @@ -4347,6 +4350,7 @@ typedef struct Spotlight { * * By Steve Pomeroy @xxv */ + /* uint16_t mode_dancing_shadows(void) { if (SEGLEN == 1) return mode_static(); @@ -4463,7 +4467,7 @@ uint16_t mode_dancing_shadows(void) return FRAMETIME; } static const char _data_FX_MODE_DANCING_SHADOWS[] PROGMEM = "Dancing Shadows@!,# of shadows;!;!"; - +*/ /* Imitates a washing machine, rotating same waves forward, then pause, then backward. @@ -9755,9 +9759,11 @@ uint16_t mode_particleBouncingBalls(void) return mode_static(); // allocation failed; //allocation failed PartSys->sources[0].source.collide = true; // seeded particles will collide (if enabled) PartSys->sources[0].source.x = PS_P_RADIUS_1D; //emit at bottom - PartSys->sources[0].maxLife = 700; // maximum lifetime in frames + PartSys->sources[0].maxLife = 900; // maximum lifetime in frames PartSys->sources[0].minLife = PartSys->sources[0].maxLife; PartSys->setBounce(true); + SEGMENT.aux0 = 1; + SEGMENT.aux1 = 500; //set out of speed range to ensure uptate on first call } else PartSys = reinterpret_cast(SEGMENT.data); // if not first call, just set the pointer to the PS @@ -9774,31 +9780,57 @@ uint16_t mode_particleBouncingBalls(void) PartSys->setGravity(1 + (SEGMENT.custom3>>1)); // set gravity (8 is default strength) PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur if (SEGMENT.check2) PartSys->setMotionBlur(255); //full motion blurring allows overlay (motion blur does not work with overlay) - PartSys->sources[0].var = SEGMENT.speed >> 4; - PartSys->sources[0].v = SEGMENT.speed >> 1;// - SEGMENT.speed >> 4; + PartSys->sources[0].var = SEGMENT.speed >> 3; + PartSys->sources[0].v = (SEGMENT.speed >> 1) - (SEGMENT.speed >> 3); if (SEGMENT.check1) // collisions enabled PartSys->enableParticleCollisions(true, 127 + (SEGMENT.custom1>>1)); // enable collisions and set particle collision hardness else PartSys->enableParticleCollisions(false); + PartSys->setUsedParticles( 1 + (SEGMENT.intensity >> 3)); // 1 - 32 - if (SEGMENT.check3) - PartSys->setParticleSize(1); - else - PartSys->setParticleSize(0); +// if (SEGMENT.check3) + // PartSys->setParticleSize(1); //2-pixel size (smoother for slow speeds) + //else + PartSys->setParticleSize(0); //single pixel size (classic look) - //check for balls that are 'laying on the ground' and remove them - for(uint32_t i = 0; i < PartSys->usedParticles; i++) + if(SEGMENT.check3) //rolling balls { - if(PartSys->particles[i].vx == 0 && PartSys->particles[i].x < (PS_P_RADIUS_1D << 1)) - PartSys->particles[i].ttl = 0; + PartSys->setGravity(0); + bool updatespeed = false; + if(SEGMENT.aux1 != SEGMENT.speed) + { + SEGMENT.aux1 = SEGMENT.speed; + updatespeed = true; + } + + for(uint32_t i = 0; i < PartSys->usedParticles; i++) + { + if(PartSys->particles[i].vx > 5 || PartSys->particles[i].vx < -5) //let only slow particles die (ensures no stopped particles) + PartSys->particles[i].ttl = 300; //set alive at full intensity + if(updatespeed || PartSys->particles[i].ttl == 0) //speed changed or particle died, reset TTL and speed + { + PartSys->particles[i].ttl = 300; + PartSys->particles[i].vx = random(20 + (SEGMENT.speed >> 2)) + (SEGMENT.speed >> 3); + PartSys->particles[i].hue = random16(); //set ball colors to random + } + } } + else //bouncing balls / popcorn + { + //check for balls that are 'laying on the ground' and remove them + for(uint32_t i = 0; i < PartSys->usedParticles; i++) + { + if(PartSys->particles[i].vx == 0 && PartSys->particles[i].x < (PS_P_RADIUS_1D << 1)) + PartSys->particles[i].ttl = 0; + } - // every nth frame emit a ball - if (SEGMENT.call % SEGMENT.aux0 == 0) - { - SEGMENT.aux0 = (300 - SEGMENT.intensity) + random(300 - SEGMENT.intensity); - PartSys->sources[0].source.hue = random16(); //set ball color - PartSys->sprayEmit(PartSys->sources[0]); + // every nth frame emit a ball + if (SEGMENT.call % SEGMENT.aux0 == 0) + { + SEGMENT.aux0 = (260 - SEGMENT.intensity) + random(280 - SEGMENT.intensity); + PartSys->sources[0].source.hue = random16(); //set ball color + PartSys->sprayEmit(PartSys->sources[0]); + } } PartSys->update(); // update and render @@ -9813,8 +9845,134 @@ uint16_t mode_particleBouncingBalls(void) return FRAMETIME; } -static const char _data_FX_MODE_PSBOUNCINGBALLS[] PROGMEM = "PS Bouncing Balls@Speed,!,Hardness,Blur,Gravity,Collide,Overlay,Size;,!;!;1;pal=0,sx=150,ix=150,c1=220,c2=30,c3=21,o1=0,o2=0,o3=0"; +static const char _data_FX_MODE_PSBOUNCINGBALLS[] PROGMEM = "PS Bouncing Balls@Speed,!,Hardness,Blur,Gravity,Collide,Overlay,Rolling;,!;!;1;pal=0,sx=100,c1=240,c2=0,c3=8,o1=0,o2=0,o3=1"; + +uint16_t mode_particleDancingShadows(void) +{ + if (SEGLEN == 1) + return mode_static(); + ParticleSystem1D *PartSys = NULL; + //uint8_t numSprays; + const uint8_t hardness = 150; // collision hardness is fixed + if (SEGMENT.call == 0) // initialization + { + if (!initParticleSystem1D(PartSys, 1)) // init, one source + return mode_static(); // allocation failed; //allocation failed + //PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting) + PartSys->sources[0].maxLife = 1000; //set long life + PartSys->sources[0].minLife = PartSys->sources[0].maxLife; + } + else + PartSys = reinterpret_cast(SEGMENT.data); // if not first call, just set the pointer to the PS + + if (PartSys == NULL) + { + DEBUG_PRINT(F("ERROR: FX PartSys nullpointer")); + return mode_static(); // something went wrong, no data! + } + + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + // PartSys->setBounce(!SEGMENT.check2); + // PartSys->setWrap(SEGMENT.check2); + // PartSys->setWallHardness(hardness); + // PartSys->setGravity(8 * SEGMENT.check1); // enable gravity if checked (8 is default strength) + //numSprays = min(PartSys->numSources, (uint8_t)1); // number of sprays + // PartSys->sources[0].var = SEGMENT.custom3;//SEGMENT.speed/16; + // PartSys->sources[0].v = SEGMENT.speed/2; + + //PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur + + //if (SEGMENT.check3) // collisions enabled + // PartSys->setParticleSize(1); + // else + PartSys->setParticleSize(0); + + //position according to slider + //PartSys->sources[0].source.x = map(SEGMENT.custom1, 0, 255, 0, PartSys->maxX); + + //TODO: overlay with blur=255 + //TODO: set used particles? + + //generate a spotlight: generates particles just outside of view + if (SEGMENT.call % (256 - (SEGMENT.intensity)) == 0) + { + //random color, random type + uint32_t type = random8(SPOT_TYPES_COUNT); + int8_t speed = random(20 + (SEGMENT.speed >> 2)) + (SEGMENT.speed >> 3); + uint32_t width = random8(1, 10); + uint32_t ttl = 300; //ttl is particle brightness (if perpetual is set, it does not age, i.e. ttl stays at this value) + int32_t position; + //choose random start position, left and right from the segment + if (random8(2)) { + position = PartSys->maxXpixel; + speed = -speed; + }else { + position = -width; + } + PartSys->sources[0].v = speed; //emitted particle speed + PartSys->sources[0].source.hue = random8(); //random spotlight color + for (int i = 0; i < width; i++) + { + switch (type) { + case SPOT_TYPE_SOLID: + //nothing to do + break; + + case SPOT_TYPE_GRADIENT: + ttl = cubicwave8(map(i, 0, width - 1, 0, 255)); + break; + + case SPOT_TYPE_2X_GRADIENT: + ttl = cubicwave8(2 * map(i, 0, width - 1, 0, 255)); + break; + + case SPOT_TYPE_2X_DOT: + if(i > 0) position++; //skip one pixel + i++; + break; + + case SPOT_TYPE_3X_DOT: + if(i > 0) position += 2; //skip two pixels + i+=2; + break; + + case SPOT_TYPE_4X_DOT: + if(i > 0) position += 3; //skip three pixels + i+=3; + break; + } + //emit particle + //set the particle source position: + PartSys->sources[0].source.x = position * PS_P_RADIUS_1D; + PartSys->sprayEmit(PartSys->sources[0]); + position++; //do the next pixel + } + } + + for (int i = 0; i < PartSys->usedParticles; i++) + { + PartSys->particles[i].perpetual = true; //particles do not age + if(PartSys->particles[i].outofbounds) //check if out of bounds particle move away from strip (i.e. vx < 0 && x > 0 or vx > 0 and x < 0) + { + if((int32_t)PartSys->particles[i].vx * PartSys->particles[i].x > 0) PartSys->particles[i].ttl = 0; //particle is moving away, kill it + } + } + + PartSys->update(); // update and render + uint32_t bg_color = SEGCOLOR(1); //background color, set to black to overlay + if(bg_color > 0) //if not black + { + for(uint32_t i = 0; i < PartSys->maxXpixel + 1; i++) + { + SEGMENT.addPixelColor(i,bg_color); + } + } + + return FRAMETIME; +} +static const char _data_FX_MODE_PARTICLEDANCINGSHADOWS[] PROGMEM = "PS 1D Dancing Shadows@Speed,!,Position,Blur,Variation,Gravity,Cylinder/Square,Size;,!;!;12v;pal=0,sx=150,ix=150,c1=220,c2=30,c3=21,o1=0,o2=0,o3=0"; ////////////////////////////////////////////////////////////////////////////////////////// // mode data @@ -9896,7 +10054,7 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_FIRE_FLICKER, &mode_fire_flicker, _data_FX_MODE_FIRE_FLICKER); addEffect(FX_MODE_GRADIENT, &mode_gradient, _data_FX_MODE_GRADIENT); addEffect(FX_MODE_LOADING, &mode_loading, _data_FX_MODE_LOADING); - addEffect(FX_MODE_ROLLINGBALLS, &rolling_balls, _data_FX_MODE_ROLLINGBALLS); +// addEffect(FX_MODE_ROLLINGBALLS, &rolling_balls, _data_FX_MODE_ROLLINGBALLS); addEffect(FX_MODE_FAIRY, &mode_fairy, _data_FX_MODE_FAIRY); addEffect(FX_MODE_TWO_DOTS, &mode_two_dots, _data_FX_MODE_TWO_DOTS); @@ -9940,11 +10098,11 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_CANDLE, &mode_candle, _data_FX_MODE_CANDLE); addEffect(FX_MODE_STARBURST, &mode_starburst, _data_FX_MODE_STARBURST); addEffect(FX_MODE_EXPLODING_FIREWORKS, &mode_exploding_fireworks, _data_FX_MODE_EXPLODING_FIREWORKS); - addEffect(FX_MODE_BOUNCINGBALLS, &mode_bouncing_balls, _data_FX_MODE_BOUNCINGBALLS); + // addEffect(FX_MODE_BOUNCINGBALLS, &mode_bouncing_balls, _data_FX_MODE_BOUNCINGBALLS); addEffect(FX_MODE_SINELON, &mode_sinelon, _data_FX_MODE_SINELON); addEffect(FX_MODE_SINELON_DUAL, &mode_sinelon_dual, _data_FX_MODE_SINELON_DUAL); addEffect(FX_MODE_SINELON_RAINBOW, &mode_sinelon_rainbow, _data_FX_MODE_SINELON_RAINBOW); - addEffect(FX_MODE_POPCORN, &mode_popcorn, _data_FX_MODE_POPCORN); + //addEffect(FX_MODE_POPCORN, &mode_popcorn, _data_FX_MODE_POPCORN); addEffect(FX_MODE_DRIP, &mode_drip, _data_FX_MODE_DRIP); addEffect(FX_MODE_PLASMA, &mode_plasma, _data_FX_MODE_PLASMA); addEffect(FX_MODE_PERCENT, &mode_percent, _data_FX_MODE_PERCENT); @@ -9961,7 +10119,7 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_PHASEDNOISE, &mode_phased_noise, _data_FX_MODE_PHASEDNOISE); addEffect(FX_MODE_FLOW, &mode_flow, _data_FX_MODE_FLOW); addEffect(FX_MODE_CHUNCHUN, &mode_chunchun, _data_FX_MODE_CHUNCHUN); - addEffect(FX_MODE_DANCING_SHADOWS, &mode_dancing_shadows, _data_FX_MODE_DANCING_SHADOWS); + // addEffect(FX_MODE_DANCING_SHADOWS, &mode_dancing_shadows, _data_FX_MODE_DANCING_SHADOWS); addEffect(FX_MODE_WASHING_MACHINE, &mode_washing_machine, _data_FX_MODE_WASHING_MACHINE); addEffect(FX_MODE_BLENDS, &mode_blends, _data_FX_MODE_BLENDS); @@ -10073,7 +10231,8 @@ void WS2812FX::setupEffectData() { #endif // WLED_DISABLE_PARTICLESYSTEM addEffect(FX_MODE_PARTICLETEST, &mode_particle1Dtest, _data_FX_MODE_PARTICLETEST); -addEffect(FX_MODE_PSBOUNCINGBALLS, &mode_particleBouncingBalls, _data_FX_MODE_PSBOUNCINGBALLS); +addEffect(FX_MODE_PSBOUNCINGBALLS, &mode_particleBouncingBalls, _data_FX_MODE_PSBOUNCINGBALLS); //potential replacement for: bouncing balls, rollingballs, popcorn +addEffect(FX_MODE_PSDANCINGSHADOWS, &mode_particleDancingShadows, _data_FX_MODE_PARTICLEDANCINGSHADOWS); #endif // WLED_DISABLE_2D diff --git a/wled00/FX.h b/wled00/FX.h index a0d8e976bd..424f6bb5fd 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -334,7 +334,8 @@ #define FX_MODE_PARTICLEBLOBS 201 #define FX_MODE_PARTICLETEST 202 #define FX_MODE_PSBOUNCINGBALLS 203 -#define MODE_COUNT 204 +#define FX_MODE_PSDANCINGSHADOWS 204 +#define MODE_COUNT 205 typedef enum mapping1D2D { M12_Pixels = 0, diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index 34cf8ae876..a19e86db24 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -1683,7 +1683,7 @@ void ParticleSystem1D::enableParticleCollisions(bool enable, uint8_t hardness) / } // emit one particle with variation, returns index of last emitted particle (or -1 if no particle emitted) -void ParticleSystem1D::sprayEmit(PSsource1D &emitter) +int32_t ParticleSystem1D::sprayEmit(PSsource1D &emitter) { for (int32_t i = 0; i < usedParticles; i++) { @@ -1696,10 +1696,11 @@ void ParticleSystem1D::sprayEmit(PSsource1D &emitter) particles[emitIndex].x = emitter.source.x; particles[emitIndex].hue = emitter.source.hue; particles[emitIndex].collide = emitter.source.collide; - particles[emitIndex].ttl = random(emitter.minLife, emitter.maxLife); - break; + particles[emitIndex].ttl = random16(emitter.minLife, emitter.maxLife); + return emitIndex; } - } + } + return -1; } // particle moves, decays and dies, if killoutofbounds is set, out of bounds particles are set to ttl=0 @@ -1888,7 +1889,7 @@ void ParticleSystem1D::renderParticle(CRGB *framebuffer, uint32_t particleindex, { if(particlesize == 0) //single pixel particle, can be out of bounds as oob checking is made for 2-pixel particles { - uint32_t x = particles[particleindex].x >> PS_P_RADIUS_SHIFT; + uint32_t x = particles[particleindex].x >> PS_P_RADIUS_SHIFT_1D; if(x <= maxXpixel) //by making x unsigned there is no need to check < 0 as it will overflow { if (framebuffer) @@ -1901,9 +1902,9 @@ void ParticleSystem1D::renderParticle(CRGB *framebuffer, uint32_t particleindex, int32_t pxlbrightness[2] = {0}; // note: pxlbrightness needs to be set to 0 or checking does not work int32_t pixco[2]; // physical pixel coordinates of the two pixels representing a particle // subtract half a radius as the rendering algorithm always starts at the left, this makes calculations more efficient - int32_t xoffset = particles[particleindex].x - PS_P_HALFRADIUS; - int32_t dx = xoffset % PS_P_RADIUS; //relativ particle position in subpixel space - 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 xoffset = particles[particleindex].x - PS_P_HALFRADIUS_1D; + int32_t dx = xoffset % PS_P_RADIUS_1D; //relativ particle position in subpixel space + int32_t x = xoffset >> PS_P_RADIUS_SHIFT_1D; // divide by PS_P_RADIUS which is 64, so can bitshift (compiler may not optimize automatically) // set the raw pixel coordinates pixco[0] = x; // left pixel @@ -1912,10 +1913,10 @@ void ParticleSystem1D::renderParticle(CRGB *framebuffer, uint32_t particleindex, // 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 { - dx = PS_P_RADIUS + dx; // if x<0, xoffset becomes negative (and so does dx), must adjust dx as modulo will flip its value + dx = PS_P_RADIUS_1D + dx; // if x<0, xoffset becomes negative (and so does dx), must adjust dx as modulo will flip its value // 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) + if (dx == PS_P_RADIUS_1D) { pxlbrightness[1] = -1; // pixel is actually out of matrix boundaries, do not render } @@ -1936,7 +1937,7 @@ void ParticleSystem1D::renderParticle(CRGB *framebuffer, uint32_t particleindex, //calculate the values for pixels that are in frame if (pxlbrightness[0] >= 0) - pxlbrightness[0] = (((int32_t)PS_P_RADIUS - dx) * brightness) >> PS_P_SURFACE_1D; + pxlbrightness[0] = (((int32_t)PS_P_RADIUS_1D - dx) * brightness) >> PS_P_SURFACE_1D; if (pxlbrightness[1] >= 0) pxlbrightness[1] = (dx * brightness) >> PS_P_SURFACE_1D; diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index bb050baa48..f8106863b6 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -239,11 +239,11 @@ void blur2D(CRGB **colorbuffer, uint32_t xsize, uint32_t ysize, uint32_t xblur, #define ESP32_MAXSOURCES_1D 32 // particle dimensions (subpixel division) -#define PS_P_RADIUS_1D 64 // subpixel size, each pixel is divided by this for particle movement, if this value is changed, also change the shift defines (next two lines) -#define PS_P_HALFRADIUS_1D 32 -#define PS_P_RADIUS_SHIFT_1D 6 //TODO: may need to adjust -#define PS_P_SURFACE_1D 6 // shift: 2^PS_P_SURFACE = PS_P_RADIUS_1D -#define PS_P_MINHARDRADIUS_1D 64 // minimum hard surface radius TODO: also needs tweaking +#define PS_P_RADIUS_1D 32 // subpixel size, each pixel is divided by this for particle movement, if this value is changed, also change the shift defines (next two lines) +#define PS_P_HALFRADIUS_1D 16 +#define PS_P_RADIUS_SHIFT_1D 5 //TODO: may need to adjust +#define PS_P_SURFACE_1D 5 // shift: 2^PS_P_SURFACE = PS_P_RADIUS_1D +#define PS_P_MINHARDRADIUS_1D 32 // minimum hard surface radius TODO: also needs tweaking //struct for a single particle (6 bytes) typedef struct { @@ -278,7 +278,7 @@ class ParticleSystem1D void updateSystem(void); // call at the beginning of every FX, updates pointers and dimensions // particle emitters - void sprayEmit(PSsource1D &emitter); + int32_t sprayEmit(PSsource1D &emitter); void particleMoveUpdate(PSparticle1D &part, PSsettings *options = NULL); // move function //particle physics void applyGravity(PSparticle1D *part); // applies gravity to single particle (use this for sources) From f23e8aaa9cf1dfee1ccce949ca7a7ad57f9012f8 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Thu, 30 May 2024 18:33:58 +0200 Subject: [PATCH 090/219] updated dancing shadows to more closely mimic original --- wled00/FX.cpp | 59 +++++++++++++++++++++++---------------------------- 1 file changed, 27 insertions(+), 32 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index c34997d0a9..7748280ad5 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -4319,7 +4319,7 @@ uint16_t mode_chunchun(void) } static const char _data_FX_MODE_CHUNCHUN[] PROGMEM = "Chunchun@!,Gap size;!,!;!"; -/* + //13 bytes typedef struct Spotlight { float speed; @@ -4329,7 +4329,7 @@ typedef struct Spotlight { uint8_t width; uint8_t type; } spotlight; -*/ + #define SPOT_TYPE_SOLID 0 #define SPOT_TYPE_GRADIENT 1 #define SPOT_TYPE_2X_GRADIENT 2 @@ -4350,7 +4350,7 @@ typedef struct Spotlight { * * By Steve Pomeroy @xxv */ - /* + uint16_t mode_dancing_shadows(void) { if (SEGLEN == 1) return mode_static(); @@ -4467,7 +4467,7 @@ uint16_t mode_dancing_shadows(void) return FRAMETIME; } static const char _data_FX_MODE_DANCING_SHADOWS[] PROGMEM = "Dancing Shadows@!,# of shadows;!;!"; -*/ + /* Imitates a washing machine, rotating same waves forward, then pause, then backward. @@ -9859,8 +9859,7 @@ uint16_t mode_particleDancingShadows(void) { if (!initParticleSystem1D(PartSys, 1)) // init, one source return mode_static(); // allocation failed; //allocation failed - //PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting) - PartSys->sources[0].maxLife = 1000; //set long life + PartSys->sources[0].maxLife = 1000; //set long life (kill out of bounds is done in custom way) PartSys->sources[0].minLife = PartSys->sources[0].maxLife; } else @@ -9874,33 +9873,23 @@ uint16_t mode_particleDancingShadows(void) // Particle System settings PartSys->updateSystem(); // update system properties (dimensions and data pointers) - // PartSys->setBounce(!SEGMENT.check2); - // PartSys->setWrap(SEGMENT.check2); - // PartSys->setWallHardness(hardness); - // PartSys->setGravity(8 * SEGMENT.check1); // enable gravity if checked (8 is default strength) - //numSprays = min(PartSys->numSources, (uint8_t)1); // number of sprays - // PartSys->sources[0].var = SEGMENT.custom3;//SEGMENT.speed/16; - // PartSys->sources[0].v = SEGMENT.speed/2; + if (SEGMENT.check2) + PartSys->setMotionBlur(255); //full motion blurring allows overlay (motion blur does not work with overlay) + else + PartSys->setMotionBlur(SEGMENT.custom1); + if (SEGMENT.check3) // collisions enabled + PartSys->setParticleSize(1); + else + PartSys->setParticleSize(0); - //PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur - - //if (SEGMENT.check3) // collisions enabled - // PartSys->setParticleSize(1); - // else - PartSys->setParticleSize(0); + //note: updating speed on the fly is not possible, since it is unknown which particles are assigned to which spot - //position according to slider - //PartSys->sources[0].source.x = map(SEGMENT.custom1, 0, 255, 0, PartSys->maxX); - - //TODO: overlay with blur=255 - //TODO: set used particles? - //generate a spotlight: generates particles just outside of view - if (SEGMENT.call % (256 - (SEGMENT.intensity)) == 0) + if (SEGMENT.call % (260 - (SEGMENT.intensity)) == 0) { //random color, random type uint32_t type = random8(SPOT_TYPES_COUNT); - int8_t speed = random(20 + (SEGMENT.speed >> 2)) + (SEGMENT.speed >> 3); + int8_t speed = random(5 + (SEGMENT.speed >> 2)) + (SEGMENT.speed >> 3); uint32_t width = random8(1, 10); uint32_t ttl = 300; //ttl is particle brightness (if perpetual is set, it does not age, i.e. ttl stays at this value) int32_t position; @@ -9922,10 +9911,12 @@ uint16_t mode_particleDancingShadows(void) case SPOT_TYPE_GRADIENT: ttl = cubicwave8(map(i, 0, width - 1, 0, 255)); + ttl = ttl*ttl >> 8; //make gradient more pronounced break; case SPOT_TYPE_2X_GRADIENT: ttl = cubicwave8(2 * map(i, 0, width - 1, 0, 255)); + ttl = ttl*ttl >> 8; break; case SPOT_TYPE_2X_DOT: @@ -9946,18 +9937,22 @@ uint16_t mode_particleDancingShadows(void) //emit particle //set the particle source position: PartSys->sources[0].source.x = position * PS_P_RADIUS_1D; - PartSys->sprayEmit(PartSys->sources[0]); + uint32_t partidx = PartSys->sprayEmit(PartSys->sources[0]); + PartSys->particles[partidx].ttl = ttl; position++; //do the next pixel } } - + + //kill out of bounds and moving away plus change color for (int i = 0; i < PartSys->usedParticles; i++) { - PartSys->particles[i].perpetual = true; //particles do not age if(PartSys->particles[i].outofbounds) //check if out of bounds particle move away from strip (i.e. vx < 0 && x > 0 or vx > 0 and x < 0) { if((int32_t)PartSys->particles[i].vx * PartSys->particles[i].x > 0) PartSys->particles[i].ttl = 0; //particle is moving away, kill it } + PartSys->particles[i].perpetual = true; //particles do not age + if (SEGMENT.call % (32 - (SEGMENT.custom2 >> 3)) == 0) + PartSys->particles[i].hue += 2 + SEGMENT.custom2 >> 5; } PartSys->update(); // update and render @@ -9972,7 +9967,7 @@ uint16_t mode_particleDancingShadows(void) return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEDANCINGSHADOWS[] PROGMEM = "PS 1D Dancing Shadows@Speed,!,Position,Blur,Variation,Gravity,Cylinder/Square,Size;,!;!;12v;pal=0,sx=150,ix=150,c1=220,c2=30,c3=21,o1=0,o2=0,o3=0"; +static const char _data_FX_MODE_PARTICLEDANCINGSHADOWS[] PROGMEM = "PS 1D Dancing Shadows@Speed,!,Blur,Color Cycle,,,Overlay,Smooth;,!;!;12v;pal=0,sx=100,ix=180,c1=0,c2=0,o2=0,o3=0"; ////////////////////////////////////////////////////////////////////////////////////////// // mode data @@ -10119,7 +10114,7 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_PHASEDNOISE, &mode_phased_noise, _data_FX_MODE_PHASEDNOISE); addEffect(FX_MODE_FLOW, &mode_flow, _data_FX_MODE_FLOW); addEffect(FX_MODE_CHUNCHUN, &mode_chunchun, _data_FX_MODE_CHUNCHUN); - // addEffect(FX_MODE_DANCING_SHADOWS, &mode_dancing_shadows, _data_FX_MODE_DANCING_SHADOWS); + addEffect(FX_MODE_DANCING_SHADOWS, &mode_dancing_shadows, _data_FX_MODE_DANCING_SHADOWS); addEffect(FX_MODE_WASHING_MACHINE, &mode_washing_machine, _data_FX_MODE_WASHING_MACHINE); addEffect(FX_MODE_BLENDS, &mode_blends, _data_FX_MODE_BLENDS); From 1e7cda6ee0f7835a44cdce0cb01fa9f34e9f14fc Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Fri, 31 May 2024 14:37:53 +0200 Subject: [PATCH 091/219] added drip FX, added #ifdefs, removed some common functions #ifdefs to individually disable 1D and 2D system. --- wled00/FX.cpp | 164 ++++++++++++------ wled00/FX.h | 2 +- wled00/FXparticleSystem.cpp | 334 +++++++++++++++++------------------- wled00/FXparticleSystem.h | 70 ++++---- 4 files changed, 302 insertions(+), 268 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 7748280ad5..39757940af 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -2954,18 +2954,18 @@ uint16_t mode_spots_fade() } static const char _data_FX_MODE_SPOTS_FADE[] PROGMEM = "Spots Fade@Spread,Width,,,,,Overlay;!,!;!"; -/* + //each needs 12 bytes typedef struct Ball { unsigned long lastBounceTime; float impactVelocity; float height; } ball; -*/ + /* * Bouncing Balls Effect */ -/* + uint16_t mode_bouncing_balls(void) { if (SEGLEN == 1) return mode_static(); //allocate segment data @@ -3038,7 +3038,7 @@ uint16_t mode_bouncing_balls(void) { return FRAMETIME; } static const char _data_FX_MODE_BOUNCINGBALLS[] PROGMEM = "Bouncing Balls@Gravity,# of balls,,,,,Overlay;!,!,!;!;1;m12=1"; //bar -*/ + /* * bouncing balls on a track track Effect modified from Aircoookie's bouncing balls @@ -3250,7 +3250,6 @@ typedef struct Spark { * POPCORN * modified from https://github.com/kitesurfer1404/WS2812FX/blob/master/src/custom/Popcorn.h */ -/* uint16_t mode_popcorn(void) { if (SEGLEN == 1) return mode_static(); //allocate segment data @@ -3309,7 +3308,7 @@ uint16_t mode_popcorn(void) { return FRAMETIME; } static const char _data_FX_MODE_POPCORN[] PROGMEM = "Popcorn@!,!,,,,,Overlay;!,!,!;!;;m12=1"; //bar -*/ + //values close to 100 produce 5Hz flicker, which looks very candle-y //Inspired by https://github.com/avanhanegem/ArduinoCandleEffectNeoPixel @@ -7907,7 +7906,7 @@ uint16_t mode_2Dwavingcell() { } static const char _data_FX_MODE_2DWAVINGCELL[] PROGMEM = "Waving Cell@!,,Amplitude 1,Amplitude 2,Amplitude 3;;!;2"; -#ifndef WLED_DISABLE_PARTICLESYSTEM +#ifndef WLED_DISABLE_PARTICLESYSTEM2D /* * Particle System Vortex @@ -9651,22 +9650,22 @@ uint16_t mode_particleblobs(void) } static const char _data_FX_MODE_PARTICLEBLOBS[] PROGMEM = "PS Blobs@Speed,Blobs,Size,Life,Blur,Wobble,Collide,Pulsate;;!;2v;sx=30,ix=64,c1=200,c2=130,c3=0,o1=0,o2=0,o3=1"; -#endif //WLED_DISABLE_PARTICLESYSTEM +#endif //WLED_DISABLE_PARTICLESYSTEM2D #endif // WLED_DISABLE_2D //////// -// 1D PS test - +// 1D PS tests +#ifndef WLED_DISABLE_PARTICLESYSTEM1D /* -Particle Spray, just a particle spray with many parameters +Particle Drip replacement, also replaces Rain Uses palette for particle color by DedeHai (Damian Schneider) */ -uint16_t mode_particle1Dtest(void) +uint16_t mode_particleDrip(void) { if (SEGLEN == 1) return mode_static(); @@ -9679,8 +9678,7 @@ uint16_t mode_particle1Dtest(void) if (!initParticleSystem1D(PartSys, 4)) // init, no additional data needed return mode_static(); // allocation failed; //allocation failed PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting) - PartSys->sources[0].source.hue = random16(); - PartSys->sources[0].source.collide = true; // seeded particles will collide (if enabled) + PartSys->sources[0].source.hue = random16(); } else PartSys = reinterpret_cast(SEGMENT.data); // if not first call, just set the pointer to the PS @@ -9693,35 +9691,73 @@ uint16_t mode_particle1Dtest(void) // Particle System settings PartSys->updateSystem(); // update system properties (dimensions and data pointers) - PartSys->setBounce(!SEGMENT.check2); - PartSys->setWrap(SEGMENT.check2); - PartSys->setWallHardness(hardness); - PartSys->setGravity(8 * SEGMENT.check1); // enable gravity if checked (8 is default strength) + PartSys->setBounce(true); + PartSys->setWallHardness(50); + // PartSys->setWrap(SEGMENT.check2); + //numSprays = min(PartSys->numSources, (uint8_t)1); // number of sprays - PartSys->sources[0].var = SEGMENT.custom3;//SEGMENT.speed/16; - PartSys->sources[0].v = SEGMENT.speed/2; + + PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur + PartSys->setGravity(SEGMENT.custom3>>1); // set gravity (8 is default strength) if (SEGMENT.check3) // collisions enabled - PartSys->enableParticleCollisions(true, SEGMENT.custom1); // enable collisions and set particle collision hardness + PartSys->setParticleSize(1); else - PartSys->enableParticleCollisions(false); - PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur - - //if (SEGMENT.check3) // collisions enabled - PartSys->setParticleSize(1); - // else - // PartSys->setParticleSize(0); + PartSys->setParticleSize(0); - //position according to slider - //PartSys->sources[0].source.x = map(SEGMENT.custom1, 0, 255, 0, PartSys->maxX); + PartSys->sources[0].maxLife = 300; + PartSys->sources[0].source.collide = true; - // change source properties - if (SEGMENT.call % (256 - (SEGMENT.intensity)) == 0) // every nth frame, cycle color and emit particles + if(SEGMENT.check1) //rain mode, emit at random position, short life (3-8 seconds at 50fps) { - PartSys->sources[0].maxLife = 600; // lifetime in frames - PartSys->sources[0].minLife = 500; - PartSys->sources[0].source.hue = random16(); //change hue of spray source + if(SEGMENT.custom1 == 0) //splash disabled, do not bounce raindrops + PartSys->setBounce(false); + PartSys->sources[0].var = 5; + PartSys->sources[0].v = -(8 + (SEGMENT.speed >> 2)); //speed + var must be < 128, inverted speed (=down) + // lifetime in frames + PartSys->sources[0].minLife = 100; + PartSys->sources[0].source.x = random(PartSys->maxX); //random emit position + } + else{ //drip + PartSys->sources[0].var = 0; + PartSys->sources[0].v = -(SEGMENT.speed >> 1); //speed + var must be < 128, inverted speed (=down) + PartSys->sources[0].minLife = 400; + PartSys->sources[0].source.x = PartSys->maxX; + } + + // every nth frame emit a particle + if (SEGMENT.call % SEGMENT.aux0 == 0) + { + SEGMENT.aux0 = (256 - SEGMENT.intensity) + random(260 - SEGMENT.intensity); + PartSys->sources[0].source.hue = random16(); //set random color TODO: maybe also not random but color cycling? need another slider or checkmark for this. PartSys->sprayEmit(PartSys->sources[0]); } + + for (int i = 0; i < PartSys->usedParticles; i++)//check all particles + { + + if(PartSys->particles[i].ttl && PartSys->particles[i].collide) // use collision flag to identify splash particles + { + if(SEGMENT.custom1 > 0 && PartSys->particles[i].x < (PS_P_RADIUS_1D << 1)) //splash enabled and reached bottom -> does not work this way, all splashes will splash again... need to mark particles + { + Serial.println("splsh"); + PartSys->particles[i].ttl = 0; //kill origin particle + PartSys->sources[0].maxLife = 80; + PartSys->sources[0].minLife = 20; + PartSys->sources[0].var = 10 + (SEGMENT.custom1 >> 3); + PartSys->sources[0].v = 0; + PartSys->sources[0].source.hue = PartSys->particles[i].hue; + PartSys->sources[0].source.x = PS_P_RADIUS_1D; + PartSys->sources[0].source.collide = false; + for(int j = 0; j < 2 + (SEGMENT.custom1 >> 2); j++) + { + PartSys->sprayEmit(PartSys->sources[0]); + } + } + } + //increase speed on high settings by calling the move function twice + if(SEGMENT.speed > 200) + PartSys->particleMoveUpdate(PartSys->particles[i]); + } PartSys->update(); // update and render uint32_t bg_color = SEGCOLOR(1); //background color, set to black to overlay @@ -9733,15 +9769,14 @@ uint16_t mode_particle1Dtest(void) } } - - return FRAMETIME; } -static const char _data_FX_MODE_PARTICLETEST[] PROGMEM = "PS 1D Testy@Speed,!,Position,Blur,Variation,Gravity,Cylinder/Square,Size;,!;!;12v;pal=0,sx=150,ix=150,c1=220,c2=30,c3=21,o1=0,o2=0,o3=0"; +static const char _data_FX_MODE_PARTICLEDRIP[] PROGMEM = "PS DripDrop@Speed,!,Splash,Blur/Overlay,Gravity,Rain,,Smooth;,!;!;1;pal=0,sx=150,ix=150,c1=220,c2=30,c3=21,o1=0,o2=0,o3=0"; /* -Particle Spray, just a particle spray with many parameters +Particle Replacement for "Bbouncing Balls by Aircoookie" +Also replaces rolling balls and juggle (and maybe popcorn) Uses palette for particle color by DedeHai (Damian Schneider) */ @@ -9776,10 +9811,10 @@ uint16_t mode_particleBouncingBalls(void) // Particle System settings PartSys->updateSystem(); // update system properties (dimensions and data pointers) - PartSys->setWallHardness(127 + (SEGMENT.custom1>>1)); + PartSys->setWallHardness(SEGMENT.custom1); PartSys->setGravity(1 + (SEGMENT.custom3>>1)); // set gravity (8 is default strength) PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur - if (SEGMENT.check2) PartSys->setMotionBlur(255); //full motion blurring allows overlay (motion blur does not work with overlay) + //if (SEGMENT.check2) PartSys->setMotionBlur(255); //full motion blurring allows overlay (motion blur does not work with overlay) PartSys->sources[0].var = SEGMENT.speed >> 3; PartSys->sources[0].v = (SEGMENT.speed >> 1) - (SEGMENT.speed >> 3); if (SEGMENT.check1) // collisions enabled @@ -9788,10 +9823,10 @@ uint16_t mode_particleBouncingBalls(void) PartSys->enableParticleCollisions(false); PartSys->setUsedParticles( 1 + (SEGMENT.intensity >> 3)); // 1 - 32 -// if (SEGMENT.check3) - // PartSys->setParticleSize(1); //2-pixel size (smoother for slow speeds) - //else - PartSys->setParticleSize(0); //single pixel size (classic look) + if (SEGMENT.check2) + PartSys->setParticleSize(1); //2-pixel size (smoother for slow speeds) + else + PartSys->setParticleSize(0); //single pixel size (classic look) if(SEGMENT.check3) //rolling balls { @@ -9810,7 +9845,9 @@ uint16_t mode_particleBouncingBalls(void) if(updatespeed || PartSys->particles[i].ttl == 0) //speed changed or particle died, reset TTL and speed { PartSys->particles[i].ttl = 300; - PartSys->particles[i].vx = random(20 + (SEGMENT.speed >> 2)) + (SEGMENT.speed >> 3); + PartSys->particles[i].collide = true; + int32_t newspeed = random(20 + (SEGMENT.speed >> 2)) + (SEGMENT.speed >> 3); + PartSys->particles[i].vx = PartSys->particles[i].vx > 0 ? newspeed : -newspeed; //keep the direction PartSys->particles[i].hue = random16(); //set ball colors to random } } @@ -9833,6 +9870,15 @@ uint16_t mode_particleBouncingBalls(void) } } + //increase speed on high settings by calling the move function twice + if(SEGMENT.speed > 200) + { + for (int i = 0; i < PartSys->usedParticles; i++)//move all particles + { + PartSys->particleMoveUpdate(PartSys->particles[i]); + } + } + PartSys->update(); // update and render uint32_t bg_color = SEGCOLOR(1); //background color, set to black to overlay if(bg_color > 0) //if not black @@ -9845,7 +9891,17 @@ uint16_t mode_particleBouncingBalls(void) return FRAMETIME; } -static const char _data_FX_MODE_PSBOUNCINGBALLS[] PROGMEM = "PS Bouncing Balls@Speed,!,Hardness,Blur,Gravity,Collide,Overlay,Rolling;,!;!;1;pal=0,sx=100,c1=240,c2=0,c3=8,o1=0,o2=0,o3=1"; +static const char _data_FX_MODE_PSBOUNCINGBALLS[] PROGMEM = "PS Bouncing Balls@Speed,!,Hardness,Blur/Overlay,Gravity,Collide,Smooth,Rolling;,!;!;1;pal=0,sx=100,c1=240,c2=0,c3=8,o1=0,o2=0,o3=1"; + +/* +Particle Replacement for original Dancing Shadows: +"Spotlights moving back and forth that cast dancing shadows. +Shine this through tree branches/leaves or other close-up objects that cast +interesting shadows onto a ceiling or tarp. +By Steve Pomeroy @xxv" +Uses palette for particle color +by DedeHai (Damian Schneider) +*/ uint16_t mode_particleDancingShadows(void) { @@ -9967,8 +10023,8 @@ uint16_t mode_particleDancingShadows(void) return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEDANCINGSHADOWS[] PROGMEM = "PS 1D Dancing Shadows@Speed,!,Blur,Color Cycle,,,Overlay,Smooth;,!;!;12v;pal=0,sx=100,ix=180,c1=0,c2=0,o2=0,o3=0"; - +static const char _data_FX_MODE_PARTICLEDANCINGSHADOWS[] PROGMEM = "PS Dancing Shadows@Speed,!,Blur,Color Cycle,,,Overlay,Smooth;,!;!;1;pal=0,sx=100,ix=180,c1=0,c2=0,o2=0,o3=0"; +#endif //WLED_DISABLE_PARTICLESYSTEM1D ////////////////////////////////////////////////////////////////////////////////////////// // mode data static const char _data_RESERVED[] PROGMEM = "RSVD"; @@ -10093,11 +10149,11 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_CANDLE, &mode_candle, _data_FX_MODE_CANDLE); addEffect(FX_MODE_STARBURST, &mode_starburst, _data_FX_MODE_STARBURST); addEffect(FX_MODE_EXPLODING_FIREWORKS, &mode_exploding_fireworks, _data_FX_MODE_EXPLODING_FIREWORKS); - // addEffect(FX_MODE_BOUNCINGBALLS, &mode_bouncing_balls, _data_FX_MODE_BOUNCINGBALLS); + addEffect(FX_MODE_BOUNCINGBALLS, &mode_bouncing_balls, _data_FX_MODE_BOUNCINGBALLS); addEffect(FX_MODE_SINELON, &mode_sinelon, _data_FX_MODE_SINELON); addEffect(FX_MODE_SINELON_DUAL, &mode_sinelon_dual, _data_FX_MODE_SINELON_DUAL); addEffect(FX_MODE_SINELON_RAINBOW, &mode_sinelon_rainbow, _data_FX_MODE_SINELON_RAINBOW); - //addEffect(FX_MODE_POPCORN, &mode_popcorn, _data_FX_MODE_POPCORN); + addEffect(FX_MODE_POPCORN, &mode_popcorn, _data_FX_MODE_POPCORN); addEffect(FX_MODE_DRIP, &mode_drip, _data_FX_MODE_DRIP); addEffect(FX_MODE_PLASMA, &mode_plasma, _data_FX_MODE_PLASMA); addEffect(FX_MODE_PERCENT, &mode_percent, _data_FX_MODE_PERCENT); @@ -10207,7 +10263,7 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_2DAKEMI, &mode_2DAkemi, _data_FX_MODE_2DAKEMI); // audio -#ifndef WLED_DISABLE_PARTICLESYSTEM +#ifndef WLED_DISABLE_PARTICLESYSTEM2D addEffect(FX_MODE_PARTICLEVORTEX, &mode_particlevortex, _data_FX_MODE_PARTICLEVORTEX); addEffect(FX_MODE_PARTICLEFIREWORKS, &mode_particlefireworks, _data_FX_MODE_PARTICLEFIREWORKS); addEffect(FX_MODE_PARTICLEVOLCANO, &mode_particlevolcano, _data_FX_MODE_PARTICLEVOLCANO); @@ -10223,9 +10279,9 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_PARTICLEGHOSTRIDER, &mode_particleghostrider, _data_FX_MODE_PARTICLEGHOSTRIDER); addEffect(FX_MODE_PARTICLEBLOBS, &mode_particleblobs, _data_FX_MODE_PARTICLEBLOBS); addEffect(FX_MODE_PARTICLECENTERGEQ, &mode_particlecenterGEQ, _data_FX_MODE_PARTICLECCIRCULARGEQ); -#endif // WLED_DISABLE_PARTICLESYSTEM +#endif // WLED_DISABLE_PARTICLESYSTEM2D -addEffect(FX_MODE_PARTICLETEST, &mode_particle1Dtest, _data_FX_MODE_PARTICLETEST); +addEffect(FX_MODE_PARTICLEDRIP, &mode_particleDrip, _data_FX_MODE_PARTICLEDRIP); addEffect(FX_MODE_PSBOUNCINGBALLS, &mode_particleBouncingBalls, _data_FX_MODE_PSBOUNCINGBALLS); //potential replacement for: bouncing balls, rollingballs, popcorn addEffect(FX_MODE_PSDANCINGSHADOWS, &mode_particleDancingShadows, _data_FX_MODE_PARTICLEDANCINGSHADOWS); diff --git a/wled00/FX.h b/wled00/FX.h index 424f6bb5fd..ba068ee79f 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -332,7 +332,7 @@ #define FX_MODE_PARTICLECENTERGEQ 199 #define FX_MODE_PARTICLEGHOSTRIDER 200 #define FX_MODE_PARTICLEBLOBS 201 -#define FX_MODE_PARTICLETEST 202 +#define FX_MODE_PARTICLEDRIP 202 #define FX_MODE_PSBOUNCINGBALLS 203 #define FX_MODE_PSDANCINGSHADOWS 204 #define MODE_COUNT 205 diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index a19e86db24..7a25db8c62 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -36,13 +36,15 @@ -add an x/y struct, do particle rendering using that, much easier to read */ -#ifndef WLED_DISABLE_PARTICLESYSTEM + #include "FXparticleSystem.h" #include "wled.h" #include "FastLED.h" #include "FX.h" +#ifndef WLED_DISABLE_PARTICLESYSTEM2D + ParticleSystem::ParticleSystem(uint16_t width, uint16_t height, uint16_t numberofparticles, uint16_t numberofsources, bool isadvanced, bool sizecontrol) { //Serial.println("PS Constructor"); @@ -1248,38 +1250,6 @@ void ParticleSystem::collideParticles(PSparticle *particle1, PSparticle *particl } } -// calculate the delta speed (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) -{ - if (force == 0) - return 0; - // 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 : 1; // force is either, 1 or -1 if it is small (zero force is handled above) - } - } - else - { - dv = force >> 4; // MSBs - } - return dv; -} - -// limit speed to prevent overflows -int32_t ParticleSystem::limitSpeed(int32_t speed) -{ - return speed > PS_P_MAXSPEED ? PS_P_MAXSPEED : (speed < -PS_P_MAXSPEED ? -PS_P_MAXSPEED : speed); -} - // allocate memory for the 2D array in one contiguous block and set values to zero CRGB **ParticleSystem::allocate2Dbuffer(uint32_t cols, uint32_t rows) { @@ -1345,6 +1315,67 @@ void ParticleSystem::updatePSpointers(bool isadvanced, bool sizecontrol) */ } +// blur a matrix in x and y direction, blur can be asymmetric in x and y +// for speed, 32bit variables are used, make sure to limit them to 8bit (0-255) or result is undefined +// to blur a subset of the buffer, change the xsize/ysize and set xstart/ystart to the desired starting coordinates (default start is 0/0) +void blur2D(CRGB **colorbuffer, uint32_t xsize, uint32_t ysize, uint32_t xblur, uint32_t yblur, bool smear, uint32_t xstart, uint32_t ystart, bool isparticle) +{ + CRGB seeppart, carryover; + uint32_t seep = xblur >> 1; + if (isparticle) //first and last row are always black in particle rendering + { + ystart++; + ysize--; + } + for(uint32_t y = ystart; y < ystart + ysize; y++) + { + carryover = BLACK; + for(uint32_t x = xstart; x < xstart + xsize; x++) + { + seeppart = colorbuffer[x][y]; // create copy of current color + fast_color_scale(seeppart, seep); // scale it and seep to neighbours + if (!smear) // fade current pixel if smear is disabled + fast_color_scale(colorbuffer[x][y], 255 - xblur); + + if (x > 0) + { + fast_color_add(colorbuffer[x-1][y], seeppart); + fast_color_add(colorbuffer[x][y], carryover); // TODO: could check if carryover is > 0, takes 7 instructions, add takes ~35, with lots of same color pixels (like background), it would be faster + } + carryover = seeppart; + } + fast_color_add(colorbuffer[xsize-1][y], carryover); // set last pixel + } + + if (isparticle) // now also do first and last row + { + ystart--; + ysize++; + } + + seep = yblur >> 1; + for(uint32_t x = xstart; x < xstart + xsize; x++) + { + carryover = BLACK; + for(uint32_t y = ystart; y < ystart + ysize; y++) + { + seeppart = colorbuffer[x][y]; // create copy of current color + fast_color_scale(seeppart, seep); // scale it and seep to neighbours + if (!smear) // fade current pixel if smear is disabled + fast_color_scale(colorbuffer[x][y], 255 - yblur); + + if (y > 0) + { + fast_color_add(colorbuffer[x][y-1], seeppart); + fast_color_add(colorbuffer[x][y], carryover); // todo: could check if carryover is > 0, takes 7 instructions, add takes ~35, with lots of same color pixels (like background), it would be faster + } + carryover = seeppart; + } + fast_color_add(colorbuffer[x][ysize-1], carryover); // set last pixel + } +} + + //non class functions to use for initialization uint32_t calculateNumberOfParticles(bool isadvanced, bool sizecontrol) { @@ -1434,121 +1465,13 @@ bool initParticleSystem(ParticleSystem *&PartSys, uint8_t requestedsources, uint return true; } -/////////////////////// -// Utility Functions // -/////////////////////// - -// fastled color adding is very inaccurate in color preservation -// a better color add function is implemented in colors.cpp but it uses 32bit RGBW. to use it colors need to be shifted just to then be shifted back by that function, which is slow -// this is a fast version for RGB (no white channel, PS does not handle white) and with native CRGB including scaling of second color (fastled scale8 can be made faster using native 32bit on ESP) -// note: result is stored in c1, so c1 will contain the result. not using a return value is much faster as the struct does not need to be copied upon return -void fast_color_add(CRGB &c1, CRGB &c2, uint32_t scale) -{ - uint32_t r, g, b; - if (scale < 255) { - r = c1.r + ((c2.r * scale) >> 8); - g = c1.g + ((c2.g * scale) >> 8); - b = c1.b + ((c2.b * scale) >> 8); - } - else { - r = c1.r + c2.r; - g = c1.g + c2.g; - b = c1.b + c2.b; - } - uint32_t max = r; - if (g > max) // note: using ? operator would be slower by 2 instructions - max = g; - if (b > max) - max = b; - if (max < 256) - { - c1.r = r; // save result to c1 - c1.g = g; - c1.b = b; - } - else - { - c1.r = (r * 255) / max; - c1.g = (g * 255) / max; - c1.b = (b * 255) / max; - } -} - -// faster than fastled color scaling as it uses a 32bit scale factor and pointer -void fast_color_scale(CRGB &c, uint32_t scale) -{ - c.r = ((c.r * scale) >> 8); - c.g = ((c.g * scale) >> 8); - c.b = ((c.b * scale) >> 8); -} - -// blur a matrix in x and y direction, blur can be asymmetric in x and y -// for speed, 32bit variables are used, make sure to limit them to 8bit (0-255) or result is undefined -// to blur a subset of the buffer, change the xsize/ysize and set xstart/ystart to the desired starting coordinates (default start is 0/0) -void blur2D(CRGB **colorbuffer, uint32_t xsize, uint32_t ysize, uint32_t xblur, uint32_t yblur, bool smear, uint32_t xstart, uint32_t ystart, bool isparticle) -{ - CRGB seeppart, carryover; - uint32_t seep = xblur >> 1; - if (isparticle) //first and last row are always black in particle rendering - { - ystart++; - ysize--; - } - for(uint32_t y = ystart; y < ystart + ysize; y++) - { - carryover = BLACK; - for(uint32_t x = xstart; x < xstart + xsize; x++) - { - seeppart = colorbuffer[x][y]; // create copy of current color - fast_color_scale(seeppart, seep); // scale it and seep to neighbours - if (!smear) // fade current pixel if smear is disabled - fast_color_scale(colorbuffer[x][y], 255 - xblur); - - if (x > 0) - { - fast_color_add(colorbuffer[x-1][y], seeppart); - fast_color_add(colorbuffer[x][y], carryover); // TODO: could check if carryover is > 0, takes 7 instructions, add takes ~35, with lots of same color pixels (like background), it would be faster - } - carryover = seeppart; - } - fast_color_add(colorbuffer[xsize-1][y], carryover); // set last pixel - } - - if (isparticle) // now also do first and last row - { - ystart--; - ysize++; - } - - seep = yblur >> 1; - for(uint32_t x = xstart; x < xstart + xsize; x++) - { - carryover = BLACK; - for(uint32_t y = ystart; y < ystart + ysize; y++) - { - seeppart = colorbuffer[x][y]; // create copy of current color - fast_color_scale(seeppart, seep); // scale it and seep to neighbours - if (!smear) // fade current pixel if smear is disabled - fast_color_scale(colorbuffer[x][y], 255 - yblur); - - if (y > 0) - { - fast_color_add(colorbuffer[x][y-1], seeppart); - fast_color_add(colorbuffer[x][y], carryover); // todo: could check if carryover is > 0, takes 7 instructions, add takes ~35, with lots of same color pixels (like background), it would be faster - } - carryover = seeppart; - } - fast_color_add(colorbuffer[x][ysize-1], carryover); // set last pixel - } -} - -#endif // WLED_DISABLE_PARTICLESYSTEM +#endif // WLED_DISABLE_PARTICLESYSTEM2D //////////////////////// // 1D Particle System // //////////////////////// - +#ifndef WLED_DISABLE_PARTICLESYSTEM1D ParticleSystem1D::ParticleSystem1D(uint16_t length, uint16_t numberofparticles, uint16_t numberofsources) { @@ -1577,7 +1500,7 @@ ParticleSystem1D::ParticleSystem1D(uint16_t length, uint16_t numberofparticles, // update function applies gravity, moves the particles, handles collisions and renders the particles void ParticleSystem1D::update(void) { - PSadvancedParticle *advprop = NULL; + //PSadvancedParticle *advprop = NULL; //apply gravity globally if enabled if (particlesettings.useGravity) applyGravity(); @@ -2060,39 +1983,6 @@ void ParticleSystem1D::collideParticles(PSparticle1D *particle1, PSparticle1D *p } } - -// calculate the delta speed (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 ParticleSystem1D::calcForce_dv(int8_t force, uint8_t* counter) -{ - if (force == 0) - return 0; - // 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 : 1; // force is either, 1 or -1 if it is small (zero force is handled above) - } - } - else - { - dv = force >> 4; // MSBs - } - return dv; -} - -// limit speed to prevent overflows -int32_t ParticleSystem1D::limitSpeed(int32_t speed) -{ - return speed > PS_P_MAXSPEED ? PS_P_MAXSPEED : (speed < -PS_P_MAXSPEED ? -PS_P_MAXSPEED : speed); -} - // allocate memory for the 2D array in one contiguous block and set values to zero CRGB *ParticleSystem1D::allocate1Dbuffer(uint32_t length) { @@ -2172,8 +2062,8 @@ bool allocateParticleSystemMemory1D(uint16_t numparticles, uint16_t numsources, { uint32_t requiredmemory = sizeof(ParticleSystem1D); // functions above make sure these are a multiple of 4 bytes (to avoid alignment issues) - requiredmemory += sizeof(PSparticle) * numparticles; - requiredmemory += sizeof(PSsource) * numsources; + requiredmemory += sizeof(PSparticle1D) * numparticles; + requiredmemory += sizeof(PSsource1D) * numsources; requiredmemory += additionalbytes; //Serial.print("allocating: "); //Serial.print(requiredmemory); @@ -2205,3 +2095,89 @@ bool initParticleSystem1D(ParticleSystem1D *&PartSys, uint8_t requestedsources, return true; } +#endif // WLED_DISABLE_PARTICLESYSTEM1D + + +#if !defined(WLED_DISABLE_PARTICLESYSTEM2D) || !defined(WLED_DISABLE_PARTICLESYSTEM1D) + +////////////////////////////// +// Shared Utility Functions // +////////////////////////////// + +// calculate the delta speed (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 calcForce_dv(int8_t force, uint8_t* counter) +{ + if (force == 0) + return 0; + // 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 : 1; // force is either, 1 or -1 if it is small (zero force is handled above) + } + } + else + { + dv = force >> 4; // MSBs + } + return dv; +} + +// limit speed to prevent overflows +int32_t limitSpeed(int32_t speed) +{ + return speed > PS_P_MAXSPEED ? PS_P_MAXSPEED : (speed < -PS_P_MAXSPEED ? -PS_P_MAXSPEED : speed); +} + +// fastled color adding is very inaccurate in color preservation +// a better color add function is implemented in colors.cpp but it uses 32bit RGBW. to use it colors need to be shifted just to then be shifted back by that function, which is slow +// this is a fast version for RGB (no white channel, PS does not handle white) and with native CRGB including scaling of second color (fastled scale8 can be made faster using native 32bit on ESP) +// note: result is stored in c1, so c1 will contain the result. not using a return value is much faster as the struct does not need to be copied upon return +void fast_color_add(CRGB &c1, CRGB &c2, uint32_t scale) +{ + uint32_t r, g, b; + if (scale < 255) { + r = c1.r + ((c2.r * scale) >> 8); + g = c1.g + ((c2.g * scale) >> 8); + b = c1.b + ((c2.b * scale) >> 8); + } + else { + r = c1.r + c2.r; + g = c1.g + c2.g; + b = c1.b + c2.b; + } + uint32_t max = r; + if (g > max) // note: using ? operator would be slower by 2 instructions + max = g; + if (b > max) + max = b; + if (max < 256) + { + c1.r = r; // save result to c1 + c1.g = g; + c1.b = b; + } + else + { + c1.r = (r * 255) / max; + c1.g = (g * 255) / max; + c1.b = (b * 255) / max; + } +} + +// faster than fastled color scaling as it uses a 32bit scale factor and pointer +void fast_color_scale(CRGB &c, uint32_t scale) +{ + c.r = ((c.r * scale) >> 8); + c.g = ((c.g * scale) >> 8); + c.b = ((c.b * scale) >> 8); +} + +#endif \ No newline at end of file diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index f8106863b6..f607f81be6 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -24,11 +24,40 @@ THE SOFTWARE. */ -#ifndef WLED_DISABLE_PARTICLESYSTEM + #include #include "FastLED.h" +#define PS_P_MINSURFACEHARDNESS 128 // minimum hardness used in collision impulse calculation, below this hardness, particles become sticky +#define PS_P_MAXSPEED 120 // maximum speed a particle can have (vx/vy is int8) + +#if !defined(WLED_DISABLE_PARTICLESYSTEM2D) || !defined(WLED_DISABLE_PARTICLESYSTEM1D) +//shared functions (used both in 1D and 2D system) +int32_t calcForce_dv(int8_t force, uint8_t *counter); //TODO: same as 2D function, could share +int32_t limitSpeed(int32_t speed); //TODO: same as 2D function, could share +void fast_color_add(CRGB &c1, CRGB &c2, uint32_t scale = 255); // fast and accurate color adding with scaling (scales c2 before adding) +void fast_color_scale(CRGB &c, uint32_t scale); // fast scaling function using 32bit variable and pointer. note: keep 'scale' within 0-255 + +// struct for PS settings (shared for 1D and 2D class) +typedef union +{ + struct{ + // one byte bit field: + bool wrapX : 1; + bool wrapY : 1; + bool bounceX : 1; + bool bounceY : 1; + bool killoutofbounds : 1; // if set, out of bound particles are killed immediately + bool useGravity : 1; // set to 1 if gravity is used, disables bounceY at the top + bool useCollisions : 1; + bool colorByAge : 1; // if set, particle hue is set by ttl value in render function + }; + byte asByte; // order is: LSB is first entry in the list above +} PSsettings; +#endif + +#ifndef WLED_DISABLE_PARTICLESYSTEM2D // memory allocation #define ESP8266_MAXPARTICLES 180 // enough for one 16x16 segment with transitions #define ESP8266_MAXSOURCES 16 @@ -43,8 +72,6 @@ #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_MINHARDRADIUS 70 // minimum hard surface radius -#define PS_P_MINSURFACEHARDNESS 128 // minimum hardness used in collision impulse calculation, below this hardness, particles become sticky -#define PS_P_MAXSPEED 120 // maximum speed a particle can have (vx/vy is int8) //struct for a single particle (10 bytes) typedef struct { @@ -99,23 +126,6 @@ typedef struct { uint8_t size; // particle size (advanced property) } PSsource; -// struct for PS settings -typedef union -{ - struct{ - // one byte bit field: - bool wrapX : 1; - bool wrapY : 1; - bool bounceX : 1; - bool bounceY : 1; - bool killoutofbounds : 1; // if set, out of bound particles are killed immediately - bool useGravity : 1; // set to 1 if gravity is used, disables bounceY at the top - bool useCollisions : 1; - bool colorByAge : 1; // if set, particle hue is set by ttl value in render function - }; - byte asByte; // order is: LSB is first entry in the list above -} PSsettings; - // class uses approximately 60 bytes class ParticleSystem { @@ -192,8 +202,6 @@ class ParticleSystem void getParticleXYsize(PSadvancedParticle *advprops, PSsizeControl *advsize, uint32_t &xsize, uint32_t &ysize); void bounce(int8_t &incomingspeed, int8_t ¶llelspeed, int32_t &position, uint16_t maxposition); // bounce on a wall int16_t wraparound(uint16_t p, uint32_t maxvalue); - int32_t calcForce_dv(int8_t force, uint8_t *counter); - int32_t limitSpeed(int32_t speed); CRGB **allocate2Dbuffer(uint32_t cols, uint32_t rows); // note: variables that are accessed often are 32bit for speed @@ -212,24 +220,19 @@ class ParticleSystem uint8_t motionBlur; // enable motion blur, values > 100 gives smoother animations. Note: motion blurring does not work if particlesize is > 0 }; +void blur2D(CRGB **colorbuffer, uint32_t xsize, uint32_t ysize, uint32_t xblur, uint32_t yblur, bool smear = true, uint32_t xstart = 0, uint32_t ystart = 0, bool isparticle = false); // initialization functions (not part of class) bool initParticleSystem(ParticleSystem *&PartSys, uint8_t requestedsources, uint16_t additionalbytes = 0, bool largesizes = false, bool sizecontrol = false); uint32_t calculateNumberOfParticles(bool advanced, bool sizecontrol); uint32_t calculateNumberOfSources(uint8_t requestedsources); bool allocateParticleSystemMemory(uint16_t numparticles, uint16_t numsources, bool advanced, bool sizecontrol, uint16_t additionalbytes); -#endif // WLED_DISABLE_PARTICLESYSTEM - -// color functions -void fast_color_add(CRGB &c1, CRGB &c2, uint32_t scale = 255); // fast and accurate color adding with scaling (scales c2 before adding) -void fast_color_scale(CRGB &c, uint32_t scale); // fast scaling function using 32bit variable and pointer. note: keep 'scale' within 0-255 -void blur2D(CRGB **colorbuffer, uint32_t xsize, uint32_t ysize, uint32_t xblur, uint32_t yblur, bool smear = true, uint32_t xstart = 0, uint32_t ystart = 0, bool isparticle = false); - +#endif // WLED_DISABLE_PARTICLESYSTEM2D //////////////////////// // 1D Particle System // //////////////////////// - +#ifndef WLED_DISABLE_PARTICLESYSTEM1D // memory allocation #define ESP8266_MAXPARTICLES_1D 400 #define ESP8266_MAXSOURCES_1D 8 @@ -325,9 +328,7 @@ class ParticleSystem1D //void updateSize(PSadvancedParticle *advprops, PSsizeControl *advsize); // advanced size control //void getParticleXYsize(PSadvancedParticle *advprops, PSsizeControl *advsize, uint32_t &xsize, uint32_t &ysize); void bounce(int8_t &incomingspeed, int8_t ¶llelspeed, int32_t &position, uint16_t maxposition); // bounce on a wall - CRGB *allocate1Dbuffer(uint32_t length); - int32_t calcForce_dv(int8_t force, uint8_t *counter); //TODO: same as 2D function, could share - int32_t limitSpeed(int32_t speed); //TODO: same as 2D function, could share + CRGB *allocate1Dbuffer(uint32_t length); // note: variables that are accessed often are 32bit for speed PSsettings particlesettings; // settings used when updating particles uint32_t emitIndex; // index to count through particles to emit so searching for dead pixels is faster @@ -336,7 +337,7 @@ class ParticleSystem1D uint8_t gforcecounter; // counter for global gravity int8_t gforce; // gravity strength, default is 8 (negative is allowed, positive is downwards) uint8_t forcecounter; // counter for globally applied forces - uint8_t collisioncounter; // counter to handle collisions TODO: could use the SEGMENT.call? + //uint8_t collisioncounter; // counter to handle collisions TODO: could use the SEGMENT.call? -> currently unused // global particle properties for basic particles uint8_t particlesize; // global particle size, 0 = 1 pixel, 1 = 2 pixels, larger sizez TBD (TODO: need larger sizes?) int32_t particleHardRadius; // hard surface radius of a particle, used for collision detection @@ -348,3 +349,4 @@ uint32_t calculateNumberOfParticles1D(void); uint32_t calculateNumberOfSources1D(uint8_t requestedsources); bool allocateParticleSystemMemory1D(uint16_t numparticles, uint16_t numsources, uint16_t additionalbytes); +#endif // WLED_DISABLE_PARTICLESYSTEM1D From 2f318f9ee51730a9933fe215a333f8726320e475 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Fri, 31 May 2024 16:27:18 +0200 Subject: [PATCH 092/219] fixed compile issue, removed replaced FX (again) --- wled00/FX.cpp | 67 ++++++++++++++++++++----------------- wled00/FX.h | 2 +- wled00/FXparticleSystem.cpp | 5 +-- wled00/FXparticleSystem.h | 4 +-- 4 files changed, 43 insertions(+), 35 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 39757940af..c566a8aaf9 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -29,7 +29,10 @@ #include "wled.h" #include "FX.h" #include "fcn_declare.h" + +#if !defined(WLED_DISABLE_PARTICLESYSTEM2D) || !defined(WLED_DISABLE_PARTICLESYSTEM1D) #include "FXparticleSystem.h" +#endif #define IBN 5100 @@ -1254,7 +1257,7 @@ uint16_t mode_fireworks() { } static const char _data_FX_MODE_FIREWORKS[] PROGMEM = "Fireworks@,Frequency;!,!;!;12;ix=192,pal=11"; - +/* //Twinkling LEDs running. Inspired by https://github.com/kitesurfer1404/WS2812FX/blob/master/src/custom/Rain.h uint16_t mode_rain() { if (SEGLEN == 1) return mode_static(); @@ -1288,7 +1291,7 @@ uint16_t mode_rain() { return mode_fireworks(); } static const char _data_FX_MODE_RAIN[] PROGMEM = "Rain@!,Spawning rate;!,!;!;12;ix=128,pal=0"; - +*/ /* * Fire flicker function @@ -1916,7 +1919,7 @@ uint16_t mode_pride_2015(void) { } static const char _data_FX_MODE_PRIDE_2015[] PROGMEM = "Pride 2015@!;;"; - +/* //eight colored dots, weaving in and out of sync with each other uint16_t mode_juggle(void) { if (SEGLEN == 1) return mode_static(); @@ -1934,7 +1937,7 @@ uint16_t mode_juggle(void) { return FRAMETIME; } static const char _data_FX_MODE_JUGGLE[] PROGMEM = "Juggle@!,Trail;;!;;sx=64,ix=128"; - +*/ uint16_t mode_palette() { // Set up some compile time constants so that we can handle integer and float based modes using the same code base. @@ -2953,7 +2956,7 @@ uint16_t mode_spots_fade() return spots_base(tr); } static const char _data_FX_MODE_SPOTS_FADE[] PROGMEM = "Spots Fade@Spread,Width,,,,,Overlay;!,!;!"; - +/* //each needs 12 bytes typedef struct Ball { @@ -2961,11 +2964,11 @@ typedef struct Ball { float impactVelocity; float height; } ball; - +*/ /* * Bouncing Balls Effect */ - +/* uint16_t mode_bouncing_balls(void) { if (SEGLEN == 1) return mode_static(); //allocate segment data @@ -3038,7 +3041,7 @@ uint16_t mode_bouncing_balls(void) { return FRAMETIME; } static const char _data_FX_MODE_BOUNCINGBALLS[] PROGMEM = "Bouncing Balls@Gravity,# of balls,,,,,Overlay;!,!,!;!;1;m12=1"; //bar - +*/ /* * bouncing balls on a track track Effect modified from Aircoookie's bouncing balls @@ -3250,6 +3253,7 @@ typedef struct Spark { * POPCORN * modified from https://github.com/kitesurfer1404/WS2812FX/blob/master/src/custom/Popcorn.h */ +/* uint16_t mode_popcorn(void) { if (SEGLEN == 1) return mode_static(); //allocate segment data @@ -3308,7 +3312,7 @@ uint16_t mode_popcorn(void) { return FRAMETIME; } static const char _data_FX_MODE_POPCORN[] PROGMEM = "Popcorn@!,!,,,,,Overlay;!,!,!;!;;m12=1"; //bar - +*/ //values close to 100 produce 5Hz flicker, which looks very candle-y //Inspired by https://github.com/avanhanegem/ArduinoCandleEffectNeoPixel @@ -3679,6 +3683,7 @@ static const char _data_FX_MODE_EXPLODING_FIREWORKS[] PROGMEM = "Fireworks 1D@Gr * Drip Effect * ported of: https://www.youtube.com/watch?v=sru2fXh4r7k */ + /* uint16_t mode_drip(void) { if (SEGLEN == 1) return mode_static(); @@ -3760,7 +3765,7 @@ uint16_t mode_drip(void) return FRAMETIME; } static const char _data_FX_MODE_DRIP[] PROGMEM = "Drip@Gravity,# of drips,,,,,Overlay;!,!;!;;m12=1"; //bar - +*/ /* * Tetris or Stacking (falling bricks) Effect @@ -5922,7 +5927,7 @@ static const char _data_FX_MODE_2DCRAZYBEES[] PROGMEM = "Crazy Bees@!,Blur;;;2"; // 2D Ghost Rider // ///////////////////////// //// Ghost Rider by stepko (c)2021 [https://editor.soulmatelights.com/gallery/716-ghost-rider], adapted by Blaz Kristan (AKA blazoncek) -/* + #define LIGHTERS_AM 64 // max lighters (adequate for 32x32 matrix) uint16_t mode_2Dghostrider(void) { if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up @@ -6007,13 +6012,13 @@ uint16_t mode_2Dghostrider(void) { return FRAMETIME; } static const char _data_FX_MODE_2DGHOSTRIDER[] PROGMEM = "Ghost Rider@Fade rate,Blur;;!;2"; -*/ + //////////////////////////// // 2D Floating Blobs // //////////////////////////// //// Floating Blobs by stepko (c)2021 [https://editor.soulmatelights.com/gallery/573-blobs], adapted by Blaz Kristan (AKA blazoncek) -/* + #define MAX_BLOBS 8 uint16_t mode_2Dfloatingblobs(void) { if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up @@ -6108,7 +6113,7 @@ uint16_t mode_2Dfloatingblobs(void) { } #undef MAX_BLOBS static const char _data_FX_MODE_2DBLOBS[] PROGMEM = "Blobs@!,# blobs,Blur,Trail;!;!;2;c1=8"; -*/ + //////////////////////////// // 2D Scrolling text // @@ -9655,8 +9660,9 @@ static const char _data_FX_MODE_PARTICLEBLOBS[] PROGMEM = "PS Blobs@Speed,Blobs, #endif // WLED_DISABLE_2D -//////// -// 1D PS tests +/////////////////////////// +// 1D Particle System FX // +/////////////////////////// #ifndef WLED_DISABLE_PARTICLESYSTEM1D /* @@ -10025,6 +10031,7 @@ uint16_t mode_particleDancingShadows(void) } static const char _data_FX_MODE_PARTICLEDANCINGSHADOWS[] PROGMEM = "PS Dancing Shadows@Speed,!,Blur,Color Cycle,,,Overlay,Smooth;,!;!;1;pal=0,sx=100,ix=180,c1=0,c2=0,o2=0,o3=0"; #endif //WLED_DISABLE_PARTICLESYSTEM1D + ////////////////////////////////////////////////////////////////////////////////////////// // mode data static const char _data_RESERVED[] PROGMEM = "RSVD"; @@ -10100,12 +10107,12 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_LARSON_SCANNER, &mode_larson_scanner, _data_FX_MODE_LARSON_SCANNER); addEffect(FX_MODE_COMET, &mode_comet, _data_FX_MODE_COMET); addEffect(FX_MODE_FIREWORKS, &mode_fireworks, _data_FX_MODE_FIREWORKS); - addEffect(FX_MODE_RAIN, &mode_rain, _data_FX_MODE_RAIN); + //addEffect(FX_MODE_RAIN, &mode_rain, _data_FX_MODE_RAIN); addEffect(FX_MODE_TETRIX, &mode_tetrix, _data_FX_MODE_TETRIX); addEffect(FX_MODE_FIRE_FLICKER, &mode_fire_flicker, _data_FX_MODE_FIRE_FLICKER); addEffect(FX_MODE_GRADIENT, &mode_gradient, _data_FX_MODE_GRADIENT); addEffect(FX_MODE_LOADING, &mode_loading, _data_FX_MODE_LOADING); -// addEffect(FX_MODE_ROLLINGBALLS, &rolling_balls, _data_FX_MODE_ROLLINGBALLS); + //addEffect(FX_MODE_ROLLINGBALLS, &rolling_balls, _data_FX_MODE_ROLLINGBALLS); addEffect(FX_MODE_FAIRY, &mode_fairy, _data_FX_MODE_FAIRY); addEffect(FX_MODE_TWO_DOTS, &mode_two_dots, _data_FX_MODE_TWO_DOTS); @@ -10122,7 +10129,7 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_RANDOM_CHASE, &mode_random_chase, _data_FX_MODE_RANDOM_CHASE); addEffect(FX_MODE_OSCILLATE, &mode_oscillate, _data_FX_MODE_OSCILLATE); addEffect(FX_MODE_PRIDE_2015, &mode_pride_2015, _data_FX_MODE_PRIDE_2015); - addEffect(FX_MODE_JUGGLE, &mode_juggle, _data_FX_MODE_JUGGLE); + //addEffect(FX_MODE_JUGGLE, &mode_juggle, _data_FX_MODE_JUGGLE); addEffect(FX_MODE_PALETTE, &mode_palette, _data_FX_MODE_PALETTE); addEffect(FX_MODE_FIRE_2012, &mode_fire_2012, _data_FX_MODE_FIRE_2012); addEffect(FX_MODE_COLORWAVES, &mode_colorwaves, _data_FX_MODE_COLORWAVES); @@ -10149,12 +10156,12 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_CANDLE, &mode_candle, _data_FX_MODE_CANDLE); addEffect(FX_MODE_STARBURST, &mode_starburst, _data_FX_MODE_STARBURST); addEffect(FX_MODE_EXPLODING_FIREWORKS, &mode_exploding_fireworks, _data_FX_MODE_EXPLODING_FIREWORKS); - addEffect(FX_MODE_BOUNCINGBALLS, &mode_bouncing_balls, _data_FX_MODE_BOUNCINGBALLS); + //addEffect(FX_MODE_BOUNCINGBALLS, &mode_bouncing_balls, _data_FX_MODE_BOUNCINGBALLS); addEffect(FX_MODE_SINELON, &mode_sinelon, _data_FX_MODE_SINELON); addEffect(FX_MODE_SINELON_DUAL, &mode_sinelon_dual, _data_FX_MODE_SINELON_DUAL); addEffect(FX_MODE_SINELON_RAINBOW, &mode_sinelon_rainbow, _data_FX_MODE_SINELON_RAINBOW); - addEffect(FX_MODE_POPCORN, &mode_popcorn, _data_FX_MODE_POPCORN); - addEffect(FX_MODE_DRIP, &mode_drip, _data_FX_MODE_DRIP); + //addEffect(FX_MODE_POPCORN, &mode_popcorn, _data_FX_MODE_POPCORN); + //addEffect(FX_MODE_DRIP, &mode_drip, _data_FX_MODE_DRIP); addEffect(FX_MODE_PLASMA, &mode_plasma, _data_FX_MODE_PLASMA); addEffect(FX_MODE_PERCENT, &mode_percent, _data_FX_MODE_PERCENT); addEffect(FX_MODE_RIPPLE_RAINBOW, &mode_ripple_rainbow, _data_FX_MODE_RIPPLE_RAINBOW); @@ -10170,7 +10177,7 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_PHASEDNOISE, &mode_phased_noise, _data_FX_MODE_PHASEDNOISE); addEffect(FX_MODE_FLOW, &mode_flow, _data_FX_MODE_FLOW); addEffect(FX_MODE_CHUNCHUN, &mode_chunchun, _data_FX_MODE_CHUNCHUN); - addEffect(FX_MODE_DANCING_SHADOWS, &mode_dancing_shadows, _data_FX_MODE_DANCING_SHADOWS); + //addEffect(FX_MODE_DANCING_SHADOWS, &mode_dancing_shadows, _data_FX_MODE_DANCING_SHADOWS); addEffect(FX_MODE_WASHING_MACHINE, &mode_washing_machine, _data_FX_MODE_WASHING_MACHINE); addEffect(FX_MODE_BLENDS, &mode_blends, _data_FX_MODE_BLENDS); @@ -10218,8 +10225,8 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_2DPLASMAROTOZOOM, &mode_2Dplasmarotozoom, _data_FX_MODE_2DPLASMAROTOZOOM); addEffect(FX_MODE_2DSPACESHIPS, &mode_2Dspaceships, _data_FX_MODE_2DSPACESHIPS); addEffect(FX_MODE_2DCRAZYBEES, &mode_2Dcrazybees, _data_FX_MODE_2DCRAZYBEES); - //addEffect(FX_MODE_2DGHOSTRIDER, &mode_2Dghostrider, _data_FX_MODE_2DGHOSTRIDER); - //addEffect(FX_MODE_2DBLOBS, &mode_2Dfloatingblobs, _data_FX_MODE_2DBLOBS); + addEffect(FX_MODE_2DGHOSTRIDER, &mode_2Dghostrider, _data_FX_MODE_2DGHOSTRIDER); + addEffect(FX_MODE_2DBLOBS, &mode_2Dfloatingblobs, _data_FX_MODE_2DBLOBS); addEffect(FX_MODE_2DSCROLLTEXT, &mode_2Dscrollingtext, _data_FX_MODE_2DSCROLLTEXT); addEffect(FX_MODE_2DDRIFTROSE, &mode_2Ddriftrose, _data_FX_MODE_2DDRIFTROSE); addEffect(FX_MODE_2DDISTORTIONWAVES, &mode_2Ddistortionwaves, _data_FX_MODE_2DDISTORTIONWAVES); @@ -10281,11 +10288,11 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_PARTICLECENTERGEQ, &mode_particlecenterGEQ, _data_FX_MODE_PARTICLECCIRCULARGEQ); #endif // WLED_DISABLE_PARTICLESYSTEM2D -addEffect(FX_MODE_PARTICLEDRIP, &mode_particleDrip, _data_FX_MODE_PARTICLEDRIP); -addEffect(FX_MODE_PSBOUNCINGBALLS, &mode_particleBouncingBalls, _data_FX_MODE_PSBOUNCINGBALLS); //potential replacement for: bouncing balls, rollingballs, popcorn -addEffect(FX_MODE_PSDANCINGSHADOWS, &mode_particleDancingShadows, _data_FX_MODE_PARTICLEDANCINGSHADOWS); - - #endif // WLED_DISABLE_2D +#ifndef WLED_DISABLE_PARTICLESYSTEM1D +addEffect(FX_MODE_PSDRIP, &mode_particleDrip, _data_FX_MODE_PARTICLEDRIP); +addEffect(FX_MODE_PSBOUNCINGBALLS, &mode_particleBouncingBalls, _data_FX_MODE_PSBOUNCINGBALLS); //potential replacement for: bouncing balls, rollingballs, popcorn +addEffect(FX_MODE_PSDANCINGSHADOWS, &mode_particleDancingShadows, _data_FX_MODE_PARTICLEDANCINGSHADOWS); +#endif // WLED_DISABLE_PARTICLESYSTEM1D } diff --git a/wled00/FX.h b/wled00/FX.h index ba068ee79f..948db290fd 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -332,7 +332,7 @@ #define FX_MODE_PARTICLECENTERGEQ 199 #define FX_MODE_PARTICLEGHOSTRIDER 200 #define FX_MODE_PARTICLEBLOBS 201 -#define FX_MODE_PARTICLEDRIP 202 +#define FX_MODE_PSDRIP 202 #define FX_MODE_PSBOUNCINGBALLS 203 #define FX_MODE_PSDANCINGSHADOWS 204 #define MODE_COUNT 205 diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index 7a25db8c62..e29cd40d19 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -37,11 +37,12 @@ */ - +#if !defined(WLED_DISABLE_PARTICLESYSTEM2D) || !defined(WLED_DISABLE_PARTICLESYSTEM1D) #include "FXparticleSystem.h" #include "wled.h" #include "FastLED.h" #include "FX.h" +#endif #ifndef WLED_DISABLE_PARTICLESYSTEM2D @@ -2180,4 +2181,4 @@ void fast_color_scale(CRGB &c, uint32_t scale) c.b = ((c.b * scale) >> 8); } -#endif \ No newline at end of file +#endif // !defined(WLED_DISABLE_PARTICLESYSTEM2D) || !defined(WLED_DISABLE_PARTICLESYSTEM1D) diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index f607f81be6..05421d8cb3 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -24,7 +24,7 @@ THE SOFTWARE. */ - +#if !defined(WLED_DISABLE_PARTICLESYSTEM2D) || !defined(WLED_DISABLE_PARTICLESYSTEM1D) #include #include "FastLED.h" @@ -32,7 +32,7 @@ #define PS_P_MINSURFACEHARDNESS 128 // minimum hardness used in collision impulse calculation, below this hardness, particles become sticky #define PS_P_MAXSPEED 120 // maximum speed a particle can have (vx/vy is int8) -#if !defined(WLED_DISABLE_PARTICLESYSTEM2D) || !defined(WLED_DISABLE_PARTICLESYSTEM1D) + //shared functions (used both in 1D and 2D system) int32_t calcForce_dv(int8_t force, uint8_t *counter); //TODO: same as 2D function, could share int32_t limitSpeed(int32_t speed); //TODO: same as 2D function, could share From 23a3cf895d19d269a2e11d42a2b9e0c11aa1b14c Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sun, 2 Jun 2024 08:10:09 +0200 Subject: [PATCH 093/219] added Fireworks1D replacement (unfinished) also fixed a bug in `applyForce()` using uninitialized variable. --- wled00/FX.cpp | 221 ++++++++++++++++++++++++++++-------- wled00/FX.h | 3 +- wled00/FXparticleSystem.cpp | 142 ++++++++++++++--------- wled00/FXparticleSystem.h | 40 ++++--- 4 files changed, 286 insertions(+), 120 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index c566a8aaf9..daf9950586 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -3683,7 +3683,6 @@ static const char _data_FX_MODE_EXPLODING_FIREWORKS[] PROGMEM = "Fireworks 1D@Gr * Drip Effect * ported of: https://www.youtube.com/watch?v=sru2fXh4r7k */ - /* uint16_t mode_drip(void) { if (SEGLEN == 1) return mode_static(); @@ -3765,7 +3764,6 @@ uint16_t mode_drip(void) return FRAMETIME; } static const char _data_FX_MODE_DRIP[] PROGMEM = "Drip@Gravity,# of drips,,,,,Overlay;!,!;!;;m12=1"; //bar -*/ /* * Tetris or Stacking (falling bricks) Effect @@ -7929,7 +7927,7 @@ uint16_t mode_particlevortex(void) if (SEGMENT.call == 0) // initialization { - if (!initParticleSystem(PartSys, NUMBEROFSOURCES)) + if (!initParticleSystem2D(PartSys, NUMBEROFSOURCES)) return mode_static(); // allocation failed //SEGMENT.aux0 = 0; // starting angle @@ -8074,7 +8072,7 @@ uint16_t mode_particlefireworks(void) if (SEGMENT.call == 0) // initialization { - if (!initParticleSystem(PartSys, NUMBEROFSOURCES, true)) // init with advanced particle properties + if (!initParticleSystem2D(PartSys, NUMBEROFSOURCES, true)) // init with advanced particle properties return mode_static(); // allocation failed PartSys->setKillOutOfBounds(true); //out of bounds particles dont return (except on top, taken care of by gravity setting) PartSys->setWallHardness(100); //ground bounce is fixed @@ -8251,7 +8249,7 @@ uint16_t mode_particlevolcano(void) if (SEGMENT.call == 0) // initialization { - if (!initParticleSystem(PartSys, NUMBEROFSOURCES)) // init, no additional data needed + if (!initParticleSystem2D(PartSys, NUMBEROFSOURCES)) // init, no additional data needed return mode_static(); // allocation failed PartSys->setBounceY(true); PartSys->setGravity(); // enable with default gforce @@ -8329,7 +8327,7 @@ uint16_t mode_particlefire(void) if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. { - if (!initParticleSystem(PartSys, 25, 4)) //maximum number of source (PS will determine the exact number based on segment size) and need 4 additional bytes for time keeping (uint32_t lastcall) + if (!initParticleSystem2D(PartSys, 25, 4)) //maximum number of source (PS will determine the exact number based on segment size) and need 4 additional bytes for time keeping (uint32_t lastcall) return mode_static(); // allocation failed; //allocation failed SEGMENT.aux0 = random16(); // aux0 is wind position (index) in the perlin noise numFlames = PartSys->numSources; @@ -8448,7 +8446,7 @@ uint16_t mode_particlepit(void) if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. { - if (!initParticleSystem(PartSys, 1, 0, true)) // init, request one source (actually dont really need one TODO: test if using zero sources also works) + if (!initParticleSystem2D(PartSys, 1, 0, true)) // init, request one source (actually dont really need one TODO: test if using zero sources also works) return mode_static(); // allocation failed; //allocation failed PartSys->setKillOutOfBounds(true); PartSys->setGravity(); // enable with default gravity @@ -8574,7 +8572,7 @@ uint16_t mode_particlewaterfall(void) if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. { - if (!initParticleSystem(PartSys, 12)) // init, request 12 sources, no additional data needed + if (!initParticleSystem2D(PartSys, 12)) // init, request 12 sources, no additional data needed return mode_static(); // allocation failed; //allocation failed PartSys->setGravity(); // enable with default gforce PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting) @@ -8661,7 +8659,7 @@ uint16_t mode_particlebox(void) if (SEGMENT.call == 0) // initialization { - if (!initParticleSystem(PartSys, 1)) // init + if (!initParticleSystem2D(PartSys, 1)) // init return mode_static(); // allocation failed PartSys->setBounceX(true); PartSys->setBounceY(true); @@ -8764,7 +8762,7 @@ uint16_t mode_particleperlin(void) uint32_t i; if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. { - if (!initParticleSystem(PartSys, 1, 0, true)) // init with 1 source and advanced properties + if (!initParticleSystem2D(PartSys, 1, 0, true)) // init with 1 source and advanced properties return mode_static(); // allocation failed; //allocation failed PartSys->setKillOutOfBounds(true); // should never happen, but lets make sure there are no stray particles SEGMENT.aux0 = rand(); @@ -8839,7 +8837,7 @@ uint16_t mode_particleimpact(void) if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. { - if (!initParticleSystem(PartSys, NUMBEROFSOURCES)) // init, no additional data needed + if (!initParticleSystem2D(PartSys, NUMBEROFSOURCES)) // init, no additional data needed return mode_static(); // allocation failed; //allocation failed PartSys->setKillOutOfBounds(false); // explosions out of frame ar allowed, set to true to save particles (TODO: better enable it in ESP8266?) PartSys->setGravity(); // enable default gravity @@ -8974,7 +8972,7 @@ uint16_t mode_particleattractor(void) PSparticle *attractor; // particle pointer to the attractor if (SEGMENT.call == 0) // initialization { - if (!initParticleSystem(PartSys, 1, 0, true)) // init using 1 source and advanced particle settings + if (!initParticleSystem2D(PartSys, 1, 0, true)) // init using 1 source and advanced particle settings return mode_static(); // allocation failed //DEBUG_PRINTF_P(PSTR("sources in FX %p\n"), &PartSys->sources[0]); PartSys->sources[0].source.hue = random16(); @@ -9095,7 +9093,7 @@ uint16_t mode_particleattractor(void) if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. { - if (!initParticleSystem(PartSys, 1, true)) // init, need one source. use advanced particles (with individual forces) + if (!initParticleSystem2D(PartSys, 1, true)) // init, need one source. use advanced particles (with individual forces) return mode_static(); // allocation failed; //allocation failed PartSys->sources[0].source.hue = random16(); @@ -9207,7 +9205,7 @@ uint16_t mode_particlespray(void) if (SEGMENT.call == 0) // initialization { - if (!initParticleSystem(PartSys, 1)) // init, no additional data needed + if (!initParticleSystem2D(PartSys, 1)) // init, no additional data needed return mode_static(); // allocation failed; //allocation failed PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting) PartSys->setBounceY(true); @@ -9305,7 +9303,7 @@ uint16_t mode_particleGEQ(void) if (SEGMENT.call == 0) // initialization { - if (!initParticleSystem(PartSys, 1)) // init + if (!initParticleSystem2D(PartSys, 1)) // init return mode_static(); // allocation failed PartSys->setKillOutOfBounds(true); PartSys->setUsedParticles((PartSys->numParticles * 3) / 2); // use 2/3 of available particles @@ -9405,7 +9403,7 @@ if (SEGLEN == 1) if (SEGMENT.call == 0) // initialization { - if (!initParticleSystem(PartSys, NUMBEROFSOURCES)) // init, request 16 sources + if (!initParticleSystem2D(PartSys, NUMBEROFSOURCES)) // init, request 16 sources return mode_static(); // allocation failed numSprays = min(PartSys->numSources, (uint8_t)NUMBEROFSOURCES); for (i = 0; i < numSprays; i++) @@ -9489,7 +9487,7 @@ uint16_t mode_particleghostrider(void) if (SEGMENT.call == 0) // initialization { - if (!initParticleSystem(PartSys, 1)) // init, no additional data needed + if (!initParticleSystem2D(PartSys, 1)) // init, no additional data needed return mode_static(); // allocation failed; //allocation failed PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting) PartSys->sources[0].maxLife = 260; // lifetime in frames @@ -9581,7 +9579,7 @@ uint16_t mode_particleblobs(void) if (SEGMENT.call == 0) { - if (!initParticleSystem(PartSys, 1, 0, true, true)) //init, request one source, no additional bytes, advanced size & size control (actually dont really need one TODO: test if using zero sources also works) + if (!initParticleSystem2D(PartSys, 1, 0, true, true)) //init, request one source, no additional bytes, advanced size & size control (actually dont really need one TODO: test if using zero sources also works) return mode_static(); // allocation failed PartSys->setBounceX(true); PartSys->setBounceY(true); @@ -9710,8 +9708,13 @@ uint16_t mode_particleDrip(void) else PartSys->setParticleSize(0); - PartSys->sources[0].maxLife = 300; - PartSys->sources[0].source.collide = true; + if(SEGMENT.check2) //collisions enabled + PartSys->enableParticleCollisions(true); //enable, full hardness + else + PartSys->enableParticleCollisions(false); + + + PartSys->sources[0].source.collide = false; //drops do not collide if(SEGMENT.check1) //rain mode, emit at random position, short life (3-8 seconds at 50fps) { @@ -9721,13 +9724,15 @@ uint16_t mode_particleDrip(void) PartSys->sources[0].v = -(8 + (SEGMENT.speed >> 2)); //speed + var must be < 128, inverted speed (=down) // lifetime in frames PartSys->sources[0].minLife = 100; + PartSys->sources[0].maxLife = 300; PartSys->sources[0].source.x = random(PartSys->maxX); //random emit position } else{ //drip PartSys->sources[0].var = 0; PartSys->sources[0].v = -(SEGMENT.speed >> 1); //speed + var must be < 128, inverted speed (=down) - PartSys->sources[0].minLife = 400; - PartSys->sources[0].source.x = PartSys->maxX; + PartSys->sources[0].minLife = 3000; + PartSys->sources[0].maxLife = 3000; + PartSys->sources[0].source.x = PartSys->maxX - PS_P_RADIUS_1D; } // every nth frame emit a particle @@ -9741,11 +9746,10 @@ uint16_t mode_particleDrip(void) for (int i = 0; i < PartSys->usedParticles; i++)//check all particles { - if(PartSys->particles[i].ttl && PartSys->particles[i].collide) // use collision flag to identify splash particles + if(PartSys->particles[i].ttl && PartSys->particles[i].collide == false) // use collision flag to identify splash particles { if(SEGMENT.custom1 > 0 && PartSys->particles[i].x < (PS_P_RADIUS_1D << 1)) //splash enabled and reached bottom -> does not work this way, all splashes will splash again... need to mark particles { - Serial.println("splsh"); PartSys->particles[i].ttl = 0; //kill origin particle PartSys->sources[0].maxLife = 80; PartSys->sources[0].minLife = 20; @@ -9753,7 +9757,7 @@ uint16_t mode_particleDrip(void) PartSys->sources[0].v = 0; PartSys->sources[0].source.hue = PartSys->particles[i].hue; PartSys->sources[0].source.x = PS_P_RADIUS_1D; - PartSys->sources[0].source.collide = false; + PartSys->sources[0].source.collide = true; //splashes do collide if enabled for(int j = 0; j < 2 + (SEGMENT.custom1 >> 2); j++) { PartSys->sprayEmit(PartSys->sources[0]); @@ -9767,17 +9771,9 @@ uint16_t mode_particleDrip(void) PartSys->update(); // update and render uint32_t bg_color = SEGCOLOR(1); //background color, set to black to overlay - if(bg_color > 0) //if not black - { - for(uint32_t i = 0; i < PartSys->maxXpixel + 1; i++) - { - SEGMENT.addPixelColor(i,bg_color); - } - } - return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEDRIP[] PROGMEM = "PS DripDrop@Speed,!,Splash,Blur/Overlay,Gravity,Rain,,Smooth;,!;!;1;pal=0,sx=150,ix=150,c1=220,c2=30,c3=21,o1=0,o2=0,o3=0"; +static const char _data_FX_MODE_PARTICLEDRIP[] PROGMEM = "PS DripDrop@Speed,!,Splash,Blur/Overlay,Gravity,Rain,PushSplash,Smooth;,!;!;1;pal=0,sx=150,ix=150,c1=220,c2=30,c3=21,o1=0,o2=0,o3=0"; /* @@ -9887,13 +9883,6 @@ uint16_t mode_particleBouncingBalls(void) PartSys->update(); // update and render uint32_t bg_color = SEGCOLOR(1); //background color, set to black to overlay - if(bg_color > 0) //if not black - { - for(uint32_t i = 0; i < PartSys->maxXpixel + 1; i++) - { - SEGMENT.addPixelColor(i,bg_color); - } - } return FRAMETIME; } @@ -10018,18 +10007,152 @@ uint16_t mode_particleDancingShadows(void) } PartSys->update(); // update and render - uint32_t bg_color = SEGCOLOR(1); //background color, set to black to overlay - if(bg_color > 0) //if not black + + return FRAMETIME; +} +static const char _data_FX_MODE_PARTICLEDANCINGSHADOWS[] PROGMEM = "PS Dancing Shadows@Speed,!,Blur,Color Cycle,,,Overlay,Smooth;,!;!;1;pal=0,sx=100,ix=180,c1=0,c2=0,o2=0,o3=0"; + + +/* +Particle Fireworks 1D replacement +Uses palette for particle color +by DedeHai (Damian Schneider) +*/ + +uint16_t mode_particleFireworks1D(void) +{ + if (SEGLEN == 1) + return mode_static(); + ParticleSystem1D *PartSys = NULL; + uint8_t numRockets; + uint8_t *forcecounter; + uint32_t i; + + if (SEGMENT.call == 0) // initialization { - for(uint32_t i = 0; i < PartSys->maxXpixel + 1; i++) - { - SEGMENT.addPixelColor(i,bg_color); + if (!initParticleSystem1D(PartSys, 4, 4, true)) // init, no additional data needed + return mode_static(); // allocation failed; //allocation failed + PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting) + numRockets = PartSys->numSources; + for(i = 0; i < numRockets; i++) + { + PartSys->sources[i].source.perpetual = 1; //set rocket state to standby } } + else + PartSys = reinterpret_cast(SEGMENT.data); // if not first call, just set the pointer to the PS + + if (PartSys == NULL) + { + DEBUG_PRINT(F("ERROR: FX PartSys nullpointer")); + return mode_static(); // something went wrong, no data! + } + + forcecounter = PartSys->PSdataEnd; + + numRockets = PartSys->numSources; + + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + //PartSys->setBounce(true); + //PartSys->setWallHardness(50); + //PartSys->setWrap(SEGMENT.check2); + //numSprays = min(PartSys->numSources, (uint8_t)1); // number of sprays + + PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur + + if(!SEGMENT.check1) //gravity enabled for sparks + PartSys->setGravity(0); // disable + else + PartSys->setGravity(1 + (SEGMENT.speed>>4)); // set gravity + + if (SEGMENT.check3) + PartSys->setParticleSize(1); + else + PartSys->setParticleSize(0); + + numRockets = 2; + //check Rockets, launch rockets + for(i = 0; i < numRockets; i++) + { + if(PartSys->sources[i].source.perpetual == 1) //rocket is on standby + { + PartSys->sources[i].source.ttl--; + if(PartSys->sources[i].source.ttl == 0) //time is up, relaunch + { + PartSys->sources[i].source.perpetual = 0; //flag abused for rocket state + PartSys->sources[i].source.hue = random16(); + PartSys->sources[i].var = 5; + PartSys->sources[i].v = 0; + PartSys->sources[i].minLife = 10; + PartSys->sources[i].maxLife = 30; + PartSys->sources[i].source.x = 0; //start from bottom + uint32_t speed = 8 + (((int)16 + (int)random16(20) + (int)random(SEGMENT.speed >> 3 , SEGMENT.speed >> 2)) * (int)PartSys->maxXpixel) / 140; //set speed such that rocket explods in frame, found by experimenting + PartSys->sources[i].source.vx = min(speed, (uint32_t)127); + PartSys->sources[i].source.ttl = 400;//((PS_P_RADIUS_1D * PartSys->maxXpixel)/(SEGMENT.speed >> 1))<<1; //todo: fix this properly + PartSys->sources[i].source.collide = false; //exhaust does not collide, also used to check if direction reversed + PartSys->sources[i].sat = 40; //low saturation exhaust + //odd numbered rockets launch from top + if(i & 0x01) + { + PartSys->sources[i].source.reversegrav = true; + PartSys->sources[i].source.x = PartSys->maxX; //start from top + PartSys->sources[i].source.vx = -PartSys->sources[i].source.vx; //revert direction + } + } + } + else //rocket is launched + { + int32_t rocketgravity = -(2 + (SEGMENT.speed>>4)); //-8 + int32_t speed = PartSys->sources[i].source.vx; + if(i & 0x01) //negative speed rocket + { + rocketgravity = -rocketgravity; + speed = -speed; + } + PartSys->applyForce(&PartSys->sources[i].source, rocketgravity, &forcecounter[i]); + PartSys->particleMoveUpdate(PartSys->sources[i].source); + + if(speed < 0 && PartSys->sources[i].source.collide == false) //speed has reversed and not in 'explosion mode' + { + PartSys->sources[i].source.ttl = 75 - (SEGMENT.speed >> 2); //alive for a few more frames + PartSys->sources[i].source.collide = true; //set 'explosion mode' + } + + if(PartSys->sources[i].source.ttl == 0) //explode + { + PartSys->sources[i].source.perpetual = 1; // set standby state + PartSys->sources[i].var = 10 + SEGMENT.intensity >> 2; + PartSys->sources[i].v = 0; //TODO can make global if this never changes + PartSys->sources[i].minLife = 60; + PartSys->sources[i].maxLife = 150; + if(i & 0x01) + PartSys->sources[i].source.ttl = SEGMENT.custom1 + random16((255 - SEGMENT.intensity)+SEGMENT.custom1); // standby time til next launch + else + PartSys->sources[i].source.ttl = (255 - SEGMENT.custom1) + random16((255 - SEGMENT.intensity) + (255 - SEGMENT.custom1)); + //PartSys->sources[i].source.ttl = 80 + random16(255 - SEGMENT.intensity); // + PartSys->sources[i].sat = 7 + (SEGMENT.custom3 << 3); //color saturation + uint32_t explosionsize = 10 + random(SEGMENT.intensity >> 2, SEGMENT.intensity); + for(uint32_t e = 0; e < explosionsize; e++) //emit explosion particles + { + if(SEGMENT.check2) + PartSys->sources[i].source.hue = random16(); //random color for each particle + PartSys->sprayEmit(PartSys->sources[i]); //emit a particle + } + PartSys->sources[i].source.x = -500; //set out of frame until relaunch + } + } + if(SEGMENT.call & 0x01) //every second frame + PartSys->sprayEmit(PartSys->sources[i]); //emit a particle + } + Serial.println(" *"); + PartSys->update(); // update and render + return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEDANCINGSHADOWS[] PROGMEM = "PS Dancing Shadows@Speed,!,Blur,Color Cycle,,,Overlay,Smooth;,!;!;1;pal=0,sx=100,ix=180,c1=0,c2=0,o2=0,o3=0"; +static const char _data_FX_MODE_PS_FIREWORKS1D[] PROGMEM = "PS Fireworks 1D@Gravity,Explosion,Firing side,Blur/Overlay,Saturation,Gravity,Colorful,Smooth;,!;!;1;pal=0,sx=150,ix=150,c1=220,c2=30,c3=21,o1=0,o2=0,o3=0"; + #endif //WLED_DISABLE_PARTICLESYSTEM1D ////////////////////////////////////////////////////////////////////////////////////////// @@ -10161,7 +10284,7 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_SINELON_DUAL, &mode_sinelon_dual, _data_FX_MODE_SINELON_DUAL); addEffect(FX_MODE_SINELON_RAINBOW, &mode_sinelon_rainbow, _data_FX_MODE_SINELON_RAINBOW); //addEffect(FX_MODE_POPCORN, &mode_popcorn, _data_FX_MODE_POPCORN); - //addEffect(FX_MODE_DRIP, &mode_drip, _data_FX_MODE_DRIP); + addEffect(FX_MODE_DRIP, &mode_drip, _data_FX_MODE_DRIP); addEffect(FX_MODE_PLASMA, &mode_plasma, _data_FX_MODE_PLASMA); addEffect(FX_MODE_PERCENT, &mode_percent, _data_FX_MODE_PERCENT); addEffect(FX_MODE_RIPPLE_RAINBOW, &mode_ripple_rainbow, _data_FX_MODE_RIPPLE_RAINBOW); @@ -10294,5 +10417,7 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_PSDRIP, &mode_particleDrip, _data_FX_MODE_PARTICLEDRIP); addEffect(FX_MODE_PSBOUNCINGBALLS, &mode_particleBouncingBalls, _data_FX_MODE_PSBOUNCINGBALLS); //potential replacement for: bouncing balls, rollingballs, popcorn addEffect(FX_MODE_PSDANCINGSHADOWS, &mode_particleDancingShadows, _data_FX_MODE_PARTICLEDANCINGSHADOWS); +addEffect(FX_MODE_PSFIREWORKS1D, &mode_particleFireworks1D, _data_FX_MODE_PS_FIREWORKS1D); + #endif // WLED_DISABLE_PARTICLESYSTEM1D } diff --git a/wled00/FX.h b/wled00/FX.h index 948db290fd..9161a6b1be 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -335,7 +335,8 @@ #define FX_MODE_PSDRIP 202 #define FX_MODE_PSBOUNCINGBALLS 203 #define FX_MODE_PSDANCINGSHADOWS 204 -#define MODE_COUNT 205 +#define FX_MODE_PSFIREWORKS1D 205 +#define MODE_COUNT 206 typedef enum mapping1D2D { M12_Pixels = 0, diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index e29cd40d19..73ee77f1d8 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -578,16 +578,10 @@ void ParticleSystem::applyGravity() // function does not increment gravity counter, if gravity setting is disabled, this cannot be used void ParticleSystem::applyGravity(PSparticle *part) { - int32_t dv; // velocity increase - if (gforce > 15) - dv = (gforce >> 4); // apply the 4 MSBs - else - dv = 1; - - if (gforcecounter + gforce > 15) //counter is updated in global update when applying gravity - { - part->vy = limitSpeed((int32_t)part->vy - dv); - } + uint32_t counterbkp = gforcecounter; + int32_t dv = calcForce_dv(gforce, &gforcecounter); + gforcecounter = counterbkp; //save it back + part->vy = limitSpeed((int32_t)part->vy - dv); } // slow down particle by friction, the higher the speed, the higher the friction. a high friction coefficient slows them more (255 means instant stop) @@ -1127,7 +1121,7 @@ void ParticleSystem::handleCollisions() int32_t dx, dy; // distance to other particles for (j = i + 1; j < usedParticles; j++) // check against higher number particles { - if (particles[j].ttl > 0) // if target particle is alive + if (particles[j].ttl > 0 && particles[j].collide) // if target particle is alive { dx = particles[i].x - particles[j].x; if (advPartProps) //may be using individual particle size @@ -1378,7 +1372,7 @@ void blur2D(CRGB **colorbuffer, uint32_t xsize, uint32_t ysize, uint32_t xblur, //non class functions to use for initialization -uint32_t calculateNumberOfParticles(bool isadvanced, bool sizecontrol) +uint32_t calculateNumberOfParticles2D(bool isadvanced, bool sizecontrol) { uint32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; uint32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); @@ -1403,7 +1397,7 @@ uint32_t calculateNumberOfParticles(bool isadvanced, bool sizecontrol) return numberofParticles; } -uint32_t calculateNumberOfSources(uint8_t requestedsources) +uint32_t calculateNumberOfSources2D(uint8_t requestedsources) { uint32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; uint32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); @@ -1423,7 +1417,7 @@ uint32_t calculateNumberOfSources(uint8_t requestedsources) } //allocate memory for particle system class, particles, sprays plus additional memory requested by FX -bool allocateParticleSystemMemory(uint16_t numparticles, uint16_t numsources, bool isadvanced, bool sizecontrol, uint16_t additionalbytes) +bool allocateParticleSystemMemory2D(uint16_t numparticles, uint16_t numsources, bool isadvanced, bool sizecontrol, uint16_t additionalbytes) { uint32_t requiredmemory = sizeof(ParticleSystem); // functions above make sure these are a multiple of 4 bytes (to avoid alignment issues) @@ -1443,14 +1437,14 @@ bool allocateParticleSystemMemory(uint16_t numparticles, uint16_t numsources, bo } // initialize Particle System, allocate additional bytes if needed (pointer to those bytes can be read from particle system class: PSdataEnd) -bool initParticleSystem(ParticleSystem *&PartSys, uint8_t requestedsources, uint16_t additionalbytes, bool largesizes, bool sizecontrol) +bool initParticleSystem2D(ParticleSystem *&PartSys, uint8_t requestedsources, uint16_t additionalbytes, bool advanced, bool sizecontrol) { //Serial.println("PS init function"); - uint32_t numparticles = calculateNumberOfParticles(largesizes, sizecontrol); - uint32_t numsources = calculateNumberOfSources(requestedsources); + uint32_t numparticles = calculateNumberOfParticles2D(advanced, sizecontrol); + uint32_t numsources = calculateNumberOfSources2D(requestedsources); //Serial.print("numsources: "); //Serial.println(numsources); - if (!allocateParticleSystemMemory(numparticles, numsources, largesizes, sizecontrol, additionalbytes)) + if (!allocateParticleSystemMemory2D(numparticles, numsources, advanced, sizecontrol, additionalbytes)) { DEBUG_PRINT(F("PS init failed: memory depleted")); return false; @@ -1460,7 +1454,7 @@ bool initParticleSystem(ParticleSystem *&PartSys, uint8_t requestedsources, uint 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, largesizes, sizecontrol); // particle system constructor + PartSys = new (SEGMENT.data) ParticleSystem(cols, rows, numparticles, numsources, advanced, sizecontrol); // particle system constructor //Serial.print("PS pointer at "); //Serial.println((uintptr_t)PartSys); return true; @@ -1474,19 +1468,19 @@ bool initParticleSystem(ParticleSystem *&PartSys, uint8_t requestedsources, uint //////////////////////// #ifndef WLED_DISABLE_PARTICLESYSTEM1D -ParticleSystem1D::ParticleSystem1D(uint16_t length, uint16_t numberofparticles, uint16_t numberofsources) +ParticleSystem1D::ParticleSystem1D(uint16_t length, uint16_t numberofparticles, uint16_t numberofsources, bool isadvanced) { //Serial.println("PS Constructor"); numSources = numberofsources; numParticles = numberofparticles; // set number of particles in the array usedParticles = numberofparticles; // use all particles by default - //advPartProps = NULL; //make sure we start out with null pointers (just in case memory was not cleared) + advPartProps = NULL; //make sure we start out with null pointers (just in case memory was not cleared) //advPartSize = NULL; - updatePSpointers(); // set the particle and sources pointer (call this before accessing sprays or particles) + updatePSpointers(isadvanced); // set the particle and sources pointer (call this before accessing sprays or particles) setSize(length); setWallHardness(255); // set default wall hardness to max setGravity(0); //gravity disabled by default - //setParticleSize(0); // minimum size by default + setParticleSize(0); // minimum size by default motionBlur = 0; //no fading by default emitIndex = 0; @@ -1527,6 +1521,15 @@ void ParticleSystem1D::update(void) Serial.println(aliveparticles); */ ParticleSys_render(); + + uint32_t bg_color = SEGCOLOR(1); //background color, set to black to overlay + if(bg_color > 0) //if not black + { + for(uint32_t i = 0; i < maxXpixel + 1; i++) + { + SEGMENT.addPixelColor(i,bg_color); + } + } } @@ -1620,7 +1623,12 @@ int32_t ParticleSystem1D::sprayEmit(PSsource1D &emitter) particles[emitIndex].x = emitter.source.x; particles[emitIndex].hue = emitter.source.hue; particles[emitIndex].collide = emitter.source.collide; + particles[emitIndex].reversegrav = emitter.source.reversegrav; particles[emitIndex].ttl = random16(emitter.minLife, emitter.maxLife); + if (advPartProps) + advPartProps[emitIndex].sat = emitter.sat; + return i; + return emitIndex; } } @@ -1680,15 +1688,26 @@ void ParticleSystem1D::particleMoveUpdate(PSparticle1D &part, PSsettings *option } } +// apply a force in x direction to individual particle (or source) +// caller needs to provide a 8bit counter (for each paticle) that holds its value between calls +// force is in 3.4 fixed point notation so force=16 means apply v+1 each frame default of 8 is every other frame +void ParticleSystem1D::applyForce(PSparticle1D *part, int8_t xforce, uint8_t *counter) +{ + // velocity increase + int32_t dv = calcForce_dv(xforce, counter); + // apply the force to particle + part->vx = limitSpeed((int32_t)part->vx + dv); +} // apply gravity to all particles using PS global gforce setting -// force is in 3.4 fixed point notation, see note above -// note: faster than apply force since direction is always down and counter is fixed for all particles +// gforce is in 3.4 fixed point notation, see note above void ParticleSystem1D::applyGravity() { - int32_t dv = calcForce_dv(gforce, &gforcecounter); + int32_t dv_raw = calcForce_dv(gforce, &gforcecounter); for (uint32_t i = 0; i < usedParticles; i++) { + int32_t dv = dv_raw; + if(particles[i].reversegrav) dv = -dv_raw; // 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].vx = limitSpeed((int32_t)particles[i].vx - dv); } @@ -1698,16 +1717,11 @@ void ParticleSystem1D::applyGravity() // function does not increment gravity counter, if gravity setting is disabled, this cannot be used void ParticleSystem1D::applyGravity(PSparticle1D *part) { - int32_t dv; // velocity increase - if (gforce > 15) - dv = (gforce >> 4); // apply the 4 MSBs - else - dv = 1; - - if (gforcecounter + gforce > 15) //counter is updated in global update when applying gravity - { - part->vx = limitSpeed((int32_t)part->vx - dv); - } + uint32_t counterbkp = gforcecounter; + int32_t dv = calcForce_dv(gforce, &gforcecounter); + if(part->reversegrav) dv = -dv; + gforcecounter = counterbkp; //save it back + part->vx = limitSpeed((int32_t)part->vx - dv); } @@ -1787,14 +1801,16 @@ void ParticleSystem1D::ParticleSys_render() // 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(advPartProps) //saturation is advanced property in 1D system { - CHSV baseHSV = rgb2hsv_approximate(baseRGB); //convert to HSV - baseHSV.s = particles[i].sat; //set the saturation - baseRGB = (CRGB)baseHSV; // convert back to RGB - }*/ - + if (advPartProps[i].sat < 255) + { + CHSV baseHSV = rgb2hsv_approximate(baseRGB); //convert to HSV + baseHSV.s = advPartProps[i].sat; //set the saturation + baseRGB = (CRGB)baseHSV; // convert back to RGB + } + } renderParticle(framebuffer, i, brightness, baseRGB); } @@ -1910,7 +1926,7 @@ void ParticleSystem1D::handleCollisions() int32_t proximity; for (j = i + 1; j < usedParticles; j++) // check against higher number particles { - if (particles[j].ttl > 0) // if target particle is alive + if (particles[j].ttl > 0 && particles[j].collide) // if target particle is alive { dx = particles[i].x - particles[j].x; int32_t dv = (int32_t)particles[i].vx - (int32_t)particles[j].vx; @@ -1984,7 +2000,7 @@ void ParticleSystem1D::collideParticles(PSparticle1D *particle1, PSparticle1D *p } } -// allocate memory for the 2D array in one contiguous block and set values to zero +// allocate memory for the 1D array in one contiguous block and set values to zero CRGB *ParticleSystem1D::allocate1Dbuffer(uint32_t length) { CRGB *array = (CRGB *)calloc(length, sizeof(CRGB)); @@ -1999,13 +2015,13 @@ void ParticleSystem1D::updateSystem(void) { // update size setSize(SEGMENT.virtualLength()); - updatePSpointers(); + updatePSpointers(advPartProps != NULL); } // set the pointers for the class (this only has to be done once and not on every FX call, only the class pointer needs to be reassigned to SEGENV.data every time) // function returns the pointer to the next byte available for the FX (if it assigned more memory for other stuff using the above allocate function) // FX handles the PSsources, need to tell this function how many there are -void ParticleSystem1D::updatePSpointers(void) +void ParticleSystem1D::updatePSpointers(bool isadvanced) { // DEBUG_PRINT(F("*** PS pointers ***")); // DEBUG_PRINTF_P(PSTR("this PS %p "), this); @@ -2016,7 +2032,17 @@ void ParticleSystem1D::updatePSpointers(void) particles = reinterpret_cast(this + 1); // pointer to particle array at data+sizeof(ParticleSystem) sources = reinterpret_cast(particles + numParticles); // pointer to source(s) PSdataEnd = reinterpret_cast(sources + numSources); // pointer to first available byte after the PS for FX additional data - //TODO: ist das alignment wirklich korrekt (immer!)? + if (isadvanced) + { + advPartProps = reinterpret_cast(sources + numSources); + PSdataEnd = reinterpret_cast(advPartProps + numParticles); + //if (sizecontrol) + //{ + // advPartSize = reinterpret_cast(advPartProps + numParticles); + // PSdataEnd = reinterpret_cast(advPartSize + numParticles); + //} + } + /* DEBUG_PRINTF_P(PSTR(" particles %p "), particles); DEBUG_PRINTF_P(PSTR(" sources %p "), sources); @@ -2028,7 +2054,7 @@ void ParticleSystem1D::updatePSpointers(void) //non class functions to use for initialization -uint32_t calculateNumberOfParticles1D(void) +uint32_t calculateNumberOfParticles1D(bool isadvanced) { uint32_t numberofParticles = SEGMENT.virtualLength(); // one particle per pixel (if possible) #ifdef ESP8266 @@ -2039,6 +2065,8 @@ uint32_t calculateNumberOfParticles1D(void) uint32_t particlelimit = ESP32_MAXPARTICLES_1D; // maximum number of paticles allowed (based on two segments of 32x32 and 40k effect ram) #endif numberofParticles = max((uint32_t)1, min(numberofParticles, particlelimit)); + if (isadvanced) // advanced property array needs ram, reduce number of particles to use the same amount + numberofParticles = (numberofParticles * sizeof(PSparticle1D)) / (sizeof(PSparticle1D) + sizeof(PSadvancedParticle1D)); //make sure it is a multiple of 4 for proper memory alignment (easier than using padding bytes) numberofParticles = ((numberofParticles+3) >> 2) << 2; return numberofParticles; @@ -2059,11 +2087,13 @@ uint32_t calculateNumberOfSources1D(uint8_t requestedsources) } //allocate memory for particle system class, particles, sprays plus additional memory requested by FX -bool allocateParticleSystemMemory1D(uint16_t numparticles, uint16_t numsources, uint16_t additionalbytes) +bool allocateParticleSystemMemory1D(uint16_t numparticles, uint16_t numsources, bool isadvanced, uint16_t additionalbytes) { uint32_t requiredmemory = sizeof(ParticleSystem1D); // functions above make sure these are a multiple of 4 bytes (to avoid alignment issues) requiredmemory += sizeof(PSparticle1D) * numparticles; + if (isadvanced) + requiredmemory += sizeof(PSadvancedParticle1D) * numparticles; requiredmemory += sizeof(PSsource1D) * numsources; requiredmemory += additionalbytes; //Serial.print("allocating: "); @@ -2075,14 +2105,14 @@ bool allocateParticleSystemMemory1D(uint16_t numparticles, uint16_t numsources, } // initialize Particle System, allocate additional bytes if needed (pointer to those bytes can be read from particle system class: PSdataEnd) -bool initParticleSystem1D(ParticleSystem1D *&PartSys, uint8_t requestedsources, uint16_t additionalbytes) +bool initParticleSystem1D(ParticleSystem1D *&PartSys, uint8_t requestedsources, uint16_t additionalbytes, bool advanced) { //Serial.println("PS init function"); - uint32_t numparticles = calculateNumberOfParticles1D(); + uint32_t numparticles = calculateNumberOfParticles1D(advanced); uint32_t numsources = calculateNumberOfSources1D(requestedsources); //Serial.print("numsources: "); //Serial.println(numsources); - if (!allocateParticleSystemMemory1D(numparticles, numsources, additionalbytes)) + if (!allocateParticleSystemMemory1D(numparticles, numsources, advanced, additionalbytes)) { DEBUG_PRINT(F("PS init failed: memory depleted")); return false; @@ -2090,7 +2120,7 @@ bool initParticleSystem1D(ParticleSystem1D *&PartSys, uint8_t requestedsources, //Serial.print("segment.data ptr"); //Serial.println((uintptr_t)(SEGMENT.data)); //Serial.println("calling constructor"); - PartSys = new (SEGMENT.data) ParticleSystem1D(SEGMENT.virtualLength(), numparticles, numsources); // particle system constructor + PartSys = new (SEGMENT.data) ParticleSystem1D(SEGMENT.virtualLength(), numparticles, numsources, advanced); // particle system constructor //Serial.print("PS pointer at "); //Serial.println((uintptr_t)PartSys); return true; @@ -2112,8 +2142,8 @@ int32_t calcForce_dv(int8_t force, uint8_t* counter) if (force == 0) return 0; // 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; + int32_t force_abs = abs(force); // absolute value (faster than lots of if's only 7 instructions) + int32_t dv = 0; // for small forces, need to use a delay counter, apply force only if it overflows if (force_abs < 16) { diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index 05421d8cb3..e978d34e3a 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -86,7 +86,7 @@ typedef struct { bool outofbounds : 1; // out of bounds flag, set to true if particle is outside of display area bool collide : 1; // if set, particle takes part in collisions bool perpetual : 1; // if set, particle does not age (TTL is not decremented in move function, it still dies from killoutofbounds) - bool flag4 : 1; // unused flag + bool state : 1; //can be used by FX to track state, not used in PS } PSparticle; // struct for additional particle settings (optional) @@ -135,14 +135,14 @@ class ParticleSystem void update(void); //update the particles according to set options and render to the matrix void updateFire(uint32_t intensity, bool renderonly = false); // update function for fire, if renderonly is set, particles are not updated (required to fix transitions with frameskips) void updateSystem(void); // call at the beginning of every FX, updates pointers and dimensions - + void particleMoveUpdate(PSparticle &part, PSsettings *options = NULL, PSadvancedParticle *advancedproperties = NULL); // move function + // particle emitters int32_t sprayEmit(PSsource &emitter, uint32_t amount = 1); void flameEmit(PSsource &emitter); void angleEmit(PSsource& emitter, uint16_t angle, int8_t speed, uint32_t amount = 1); - void particleMoveUpdate(PSparticle &part, PSsettings *options = NULL, PSadvancedParticle *advancedproperties = NULL); // move function //particle physics void applyGravity(PSparticle *part); // applies gravity to single particle (use this for sources) void applyForce(PSparticle *part, int8_t xforce, int8_t yforce, uint8_t *counter); @@ -222,10 +222,10 @@ class ParticleSystem void blur2D(CRGB **colorbuffer, uint32_t xsize, uint32_t ysize, uint32_t xblur, uint32_t yblur, bool smear = true, uint32_t xstart = 0, uint32_t ystart = 0, bool isparticle = false); // initialization functions (not part of class) -bool initParticleSystem(ParticleSystem *&PartSys, uint8_t requestedsources, uint16_t additionalbytes = 0, bool largesizes = false, bool sizecontrol = false); -uint32_t calculateNumberOfParticles(bool advanced, bool sizecontrol); -uint32_t calculateNumberOfSources(uint8_t requestedsources); -bool allocateParticleSystemMemory(uint16_t numparticles, uint16_t numsources, bool advanced, bool sizecontrol, uint16_t additionalbytes); +bool initParticleSystem2D(ParticleSystem *&PartSys, uint8_t requestedsources, uint16_t additionalbytes = 0, bool advanced = false, bool sizecontrol = false); +uint32_t calculateNumberOfParticles2D(bool advanced, bool sizecontrol); +uint32_t calculateNumberOfSources2D(uint8_t requestedsources); +bool allocateParticleSystemMemory2D(uint16_t numparticles, uint16_t numsources, bool advanced, bool sizecontrol, uint16_t additionalbytes); #endif // WLED_DISABLE_PARTICLESYSTEM2D @@ -258,9 +258,17 @@ typedef struct { bool outofbounds : 1; // out of bounds flag, set to true if particle is outside of display area bool collide : 1; // if set, particle takes part in collisions bool perpetual : 1; // if set, particle does not age (TTL is not decremented in move function, it still dies from killoutofbounds) - bool flag4 : 1; // unused flag + bool reversegrav : 1; // if set, gravity is reversed on this particle } PSparticle1D; +// struct for additional particle settings (optional) +typedef struct +{ + uint8_t sat; //color saturation + uint8_t size; // particle size, 255 means 10 pixels in diameter + uint8_t forcecounter; +} PSadvancedParticle1D; + //struct for a particle source (17 bytes) typedef struct { uint16_t minLife; // minimum ttl of emittet particles @@ -268,6 +276,7 @@ typedef struct { PSparticle1D source; // use a particle as the emitter source (speed, position, color) int8_t var; // variation of emitted speed (adds random(+/- var) to speed) int8_t v; // emitting speed + uint8_t sat; // color saturation (advanced property) uint8_t size; // particle size (advanced property) TODO: can be removed in case this is not being implemented } PSsource1D; @@ -275,7 +284,7 @@ typedef struct { class ParticleSystem1D { public: - ParticleSystem1D(uint16_t length, uint16_t numberofparticles, uint16_t numberofsources); // constructor + ParticleSystem1D(uint16_t length, uint16_t numberofparticles, uint16_t numberofsources, bool isadvanced = false); // constructor // note: memory is allcated in the FX function, no deconstructor needed void update(void); //update the particles according to set options and render to the matrix void updateSystem(void); // call at the beginning of every FX, updates pointers and dimensions @@ -284,6 +293,7 @@ class ParticleSystem1D int32_t sprayEmit(PSsource1D &emitter); void particleMoveUpdate(PSparticle1D &part, PSsettings *options = NULL); // move function //particle physics + void applyForce(PSparticle1D *part, int8_t xforce, uint8_t *counter); //apply a force to a single particle void applyGravity(PSparticle1D *part); // applies gravity to single particle (use this for sources) void applyFriction(int32_t coefficient); // apply friction to all used particles @@ -304,7 +314,7 @@ class ParticleSystem1D PSparticle1D *particles; // pointer to particle array PSsource1D *sources; // pointer to sources - //PSadvancedParticle *advPartProps; // pointer to advanced particle properties (can be NULL) + PSadvancedParticle1D *advPartProps; // pointer to advanced particle properties (can be NULL) //PSsizeControl *advPartSize; // pointer to advanced particle size control (can be NULL) uint8_t* PSdataEnd; // points to first available byte after the PSmemory, is set in setPointers(). use this for FX custom data uint16_t maxX; // particle system size i.e. width-1 @@ -317,14 +327,14 @@ class ParticleSystem1D //rendering functions void ParticleSys_render(void); void renderParticle(CRGB *framebuffer, uint32_t particleindex, uint32_t brightness, CRGB color); - + //paricle physics applied by system if flags are set void applyGravity(); // applies gravity to all particles void handleCollisions(); void collideParticles(PSparticle1D *particle1, PSparticle1D *particle2); //utility functions - void updatePSpointers(void); // update the data pointers to current segment data space + void updatePSpointers(bool isadvanced); // update the data pointers to current segment data space //void updateSize(PSadvancedParticle *advprops, PSsizeControl *advsize); // advanced size control //void getParticleXYsize(PSadvancedParticle *advprops, PSsizeControl *advsize, uint32_t &xsize, uint32_t &ysize); void bounce(int8_t &incomingspeed, int8_t ¶llelspeed, int32_t &position, uint16_t maxposition); // bounce on a wall @@ -344,9 +354,9 @@ class ParticleSystem1D uint8_t motionBlur; // enable motion blur, values > 100 gives smoother animations. Note: motion blurring does not work if particlesize is > 0 }; -bool initParticleSystem1D(ParticleSystem1D *&PartSys, uint8_t requestedsources, uint16_t additionalbytes = 0); -uint32_t calculateNumberOfParticles1D(void); +bool initParticleSystem1D(ParticleSystem1D *&PartSys, uint8_t requestedsources, uint16_t additionalbytes = 0, bool advanced = false); +uint32_t calculateNumberOfParticles1D(bool isadvanced); uint32_t calculateNumberOfSources1D(uint8_t requestedsources); -bool allocateParticleSystemMemory1D(uint16_t numparticles, uint16_t numsources, uint16_t additionalbytes); +bool allocateParticleSystemMemory1D(uint16_t numparticles, uint16_t numsources, bool isadvanced, uint16_t additionalbytes); #endif // WLED_DISABLE_PARTICLESYSTEM1D From 43592bc9218d1412f7e59233610602a6a7bae365 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sat, 8 Jun 2024 14:17:10 +0200 Subject: [PATCH 094/219] added sparkler and hourglass (both work in progress) tried fixing 1D collisions --- wled00/FX.cpp | 382 ++++++++++++++++++++++++++++-------- wled00/FX.h | 4 +- wled00/FXparticleSystem.cpp | 216 +++++++++++++------- wled00/FXparticleSystem.h | 22 ++- 4 files changed, 463 insertions(+), 161 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index daf9950586..8c19f0ab7c 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -10024,20 +10024,20 @@ uint16_t mode_particleFireworks1D(void) if (SEGLEN == 1) return mode_static(); ParticleSystem1D *PartSys = NULL; - uint8_t numRockets; + //uint8_t numRockets; uint8_t *forcecounter; uint32_t i; if (SEGMENT.call == 0) // initialization { if (!initParticleSystem1D(PartSys, 4, 4, true)) // init, no additional data needed - return mode_static(); // allocation failed; //allocation failed - PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting) - numRockets = PartSys->numSources; - for(i = 0; i < numRockets; i++) - { - PartSys->sources[i].source.perpetual = 1; //set rocket state to standby - } + return mode_static(); // allocation failed + PartSys->setKillOutOfBounds(true); + //numRockets = PartSys->numSources; + //for(i = 0; i < numRockets; i++) + //{ + PartSys->sources[0].source.perpetual = 1; //set rocket state to standby + //} } else PartSys = reinterpret_cast(SEGMENT.data); // if not first call, just set the pointer to the PS @@ -10049,15 +10049,10 @@ uint16_t mode_particleFireworks1D(void) } forcecounter = PartSys->PSdataEnd; - - numRockets = PartSys->numSources; + //numRockets = PartSys->numSources; // Particle System settings PartSys->updateSystem(); // update system properties (dimensions and data pointers) - //PartSys->setBounce(true); - //PartSys->setWallHardness(50); - //PartSys->setWrap(SEGMENT.check2); - //numSprays = min(PartSys->numSources, (uint8_t)1); // number of sprays PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur @@ -10071,87 +10066,307 @@ uint16_t mode_particleFireworks1D(void) else PartSys->setParticleSize(0); - numRockets = 2; - //check Rockets, launch rockets - for(i = 0; i < numRockets; i++) + if(PartSys->sources[0].source.perpetual == 1) //rocket is on standby + { + PartSys->sources[0].source.ttl--; + if(PartSys->sources[0].source.ttl == 0) //time is up, relaunch { - if(PartSys->sources[i].source.perpetual == 1) //rocket is on standby + if(random(255) < SEGMENT.custom1) //randomly choose direction according to slider, fire at start of segment if true + SEGMENT.aux0 = 0; + else + SEGMENT.aux0 = 1; //invert direction + + PartSys->sources[0].source.perpetual = 0; //flag abused for rocket state + PartSys->sources[0].source.hue = random16(); + PartSys->sources[0].var = 5; + PartSys->sources[0].v = 0; + PartSys->sources[0].minLife = 10; + PartSys->sources[0].maxLife = 30; + PartSys->sources[0].source.x = 0; //start from bottom + uint32_t speed = 8 + (((int)16 + (int)random16(20) + (int)random(SEGMENT.speed >> 3 , SEGMENT.speed >> 2)) * (int)PartSys->maxXpixel) / 150; //set speed such that rocket explods in frame, found by experimenting + PartSys->sources[0].source.vx = min(speed, (uint32_t)127); + PartSys->sources[0].source.ttl = 400; + PartSys->sources[0].source.collide = false; //exhaust does not collide, also used to check if direction reversed + PartSys->sources[0].sat = 40; //low saturation exhaust + + if(SEGMENT.aux0) //inverted rockets launch from end { - PartSys->sources[i].source.ttl--; - if(PartSys->sources[i].source.ttl == 0) //time is up, relaunch - { - PartSys->sources[i].source.perpetual = 0; //flag abused for rocket state - PartSys->sources[i].source.hue = random16(); - PartSys->sources[i].var = 5; - PartSys->sources[i].v = 0; - PartSys->sources[i].minLife = 10; - PartSys->sources[i].maxLife = 30; - PartSys->sources[i].source.x = 0; //start from bottom - uint32_t speed = 8 + (((int)16 + (int)random16(20) + (int)random(SEGMENT.speed >> 3 , SEGMENT.speed >> 2)) * (int)PartSys->maxXpixel) / 140; //set speed such that rocket explods in frame, found by experimenting - PartSys->sources[i].source.vx = min(speed, (uint32_t)127); - PartSys->sources[i].source.ttl = 400;//((PS_P_RADIUS_1D * PartSys->maxXpixel)/(SEGMENT.speed >> 1))<<1; //todo: fix this properly - PartSys->sources[i].source.collide = false; //exhaust does not collide, also used to check if direction reversed - PartSys->sources[i].sat = 40; //low saturation exhaust - //odd numbered rockets launch from top - if(i & 0x01) - { - PartSys->sources[i].source.reversegrav = true; - PartSys->sources[i].source.x = PartSys->maxX; //start from top - PartSys->sources[i].source.vx = -PartSys->sources[i].source.vx; //revert direction - } - } + PartSys->sources[0].source.reversegrav = true; + PartSys->sources[0].source.x = PartSys->maxX; //start from top + PartSys->sources[0].source.vx = -PartSys->sources[0].source.vx; //revert direction } - else //rocket is launched + } + } + else //rocket is launched + { + int32_t rocketgravity = -(2 + (SEGMENT.speed>>4)); //-8 + int32_t speed = PartSys->sources[0].source.vx; + if(SEGMENT.aux0) //negative speed rocket + { + rocketgravity = -rocketgravity; + speed = -speed; + } + PartSys->applyForce(&PartSys->sources[0].source, rocketgravity, &forcecounter[0]); + PartSys->particleMoveUpdate(PartSys->sources[0].source); + + if(speed < 0 && PartSys->sources[0].source.collide == false) //speed has reversed and not in 'explosion mode' + { + PartSys->sources[0].source.ttl = 75 - (SEGMENT.speed >> 2); //alive for a few more frames + PartSys->sources[0].source.collide = true; //set 'explosion mode' + } + + if(PartSys->sources[0].source.ttl == 0) //explode + { + PartSys->sources[0].source.perpetual = 1; // set standby state + PartSys->sources[0].var = 10 + SEGMENT.intensity >> 2; + PartSys->sources[0].v = 0; //TODO can make global if this never changes + PartSys->sources[0].minLife = 60; + PartSys->sources[0].maxLife = 150; + PartSys->sources[0].source.ttl = 100 + random16(256 - SEGMENT.intensity); // standby time til next launch + PartSys->sources[0].sat = 7 + (SEGMENT.custom3 << 3); //color saturation + uint32_t explosionsize = 10 + random(SEGMENT.intensity >> 2, SEGMENT.intensity); + for(uint32_t e = 0; e < explosionsize; e++) //emit explosion particles { - int32_t rocketgravity = -(2 + (SEGMENT.speed>>4)); //-8 - int32_t speed = PartSys->sources[i].source.vx; - if(i & 0x01) //negative speed rocket - { - rocketgravity = -rocketgravity; - speed = -speed; - } - PartSys->applyForce(&PartSys->sources[i].source, rocketgravity, &forcecounter[i]); - PartSys->particleMoveUpdate(PartSys->sources[i].source); + if(SEGMENT.check2) + PartSys->sources[0].source.hue = random16(); //random color for each particle + PartSys->sprayEmit(PartSys->sources[0]); //emit a particle + } + PartSys->sources[0].source.x = -500; //set out of frame until relaunch + } + } + if(SEGMENT.call & 0x01) //every second frame + PartSys->sprayEmit(PartSys->sources[i]); //emit a particle - if(speed < 0 && PartSys->sources[i].source.collide == false) //speed has reversed and not in 'explosion mode' - { - PartSys->sources[i].source.ttl = 75 - (SEGMENT.speed >> 2); //alive for a few more frames - PartSys->sources[i].source.collide = true; //set 'explosion mode' - } + PartSys->update(); // update and render + + return FRAMETIME; +} +static const char _data_FX_MODE_PS_FIREWORKS1D[] PROGMEM = "PS Fireworks 1D@Gravity,Explosion,Firing side,Blur/Overlay,Saturation,Gravity,Colorful,Smooth;,!;!;1;pal=0,sx=150,ix=150,c1=220,c2=30,c3=21,o1=0,o2=0,o3=0"; + + +/* +Particle based Sparkle effect +Uses palette for particle color +by DedeHai (Damian Schneider) +*/ + +uint16_t mode_particleSparkler(void) +{ + if (SEGLEN == 1) + return mode_static(); + ParticleSystem1D *PartSys = NULL; + uint8_t numSparklers; + uint32_t i; + PSsettings sparklersettings; + sparklersettings.asByte = 0; // PS settings for sparkler (set below) + + if (SEGMENT.call == 0) // initialization + { + if (!initParticleSystem1D(PartSys, 8, 0, true)) // init, no additional data needed + return mode_static(); // allocation failed + } + else + PartSys = reinterpret_cast(SEGMENT.data); // if not first call, just set the pointer to the PS + + if (PartSys == NULL) + { + DEBUG_PRINT(F("ERROR: FX PartSys nullpointer")); + return mode_static(); // something went wrong, no data! + } + + sparklersettings.wrapX = SEGMENT.check2; + sparklersettings.bounceX = !SEGMENT.check2; + + numSparklers = PartSys->numSources; + + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + + PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur + + + if (SEGMENT.check3) + PartSys->setParticleSize(1); + else + PartSys->setParticleSize(0); + + for(i = 0; i < numSparklers; i++) + { + PartSys->sources[i].source.hue = random16(); //TODO: make adjustable, maybe even colorcycle? + PartSys->sources[i].var = SEGMENT.intensity >> 4 ; + PartSys->sources[i].minLife = 150 + SEGMENT.intensity >> 1; + PartSys->sources[i].maxLife = 200 + SEGMENT.intensity; + uint32_t speed = SEGMENT.speed >> 1; + PartSys->sources[i].source.vx = PartSys->sources[i].source.vx < 0 ? -speed : speed; //update speed, do not change direction + if(SEGMENT.aux0 != SEGMENT.check1) + { + PartSys->sources[i].source.vx = -PartSys->sources[i].source.vx; //invert direction + } + PartSys->sources[i].source.ttl = 400; //replenish its life (could make it perpetual) + PartSys->sources[i].sat = SEGMENT.custom1; //color saturation + PartSys->particleMoveUpdate(PartSys->sources[i].source, &sparklersettings); //move sparkler + } + + for(i = 0; i < PartSys->usedParticles; i++) + { + if(PartSys->particles[i].ttl > 10) PartSys->particles[i].ttl -= 10; //ttl is linked to brightness, this allows to use higher brightness but still a short spark lifespan + else PartSys->particles[i].ttl = 0; + } + + numSparklers = min(1 + (SEGMENT.custom3 >> 2), (int)numSparklers); // set used sparklers, 1 to 8 + + if(SEGMENT.aux0 != SEGMENT.custom3) //number of used sparklers changed, redistribute + { + for(i = 1; i < numSparklers; i++) + { + PartSys->sources[i].source.x = (PartSys->sources[0].source.x + (PartSys->maxX / numSparklers) * i ) % PartSys->maxX; //distribute evenly + } + } + SEGMENT.aux0 = SEGMENT.custom3; + + + for(i = 0; i < numSparklers; i++) + { + if((SEGMENT.call + random16(30)) % (1 + ((255 - SEGMENT.intensity) >> 3)) == 0) + PartSys->sprayEmit(PartSys->sources[i]); //emit a particle + } + SEGMENT.aux0 = SEGMENT.check1; + + PartSys->update(); // update and render + + return FRAMETIME; +} +static const char _data_FX_MODE_PS_SPARKLER[] PROGMEM = "PS Sparkler@Speed,!,Saturation,Blur/Overlay,Sparklers,Direction,Wrap/Bounce,Smooth;,!;!;1;pal=0,sx=50,ix=200,c1=0,c2=0,c3=0,o1=0,o2=1,o3=0"; + + +/* +Particle based Hourglass, particles falling at defined intervals +Uses palette for particle color +by DedeHai (Damian Schneider) +*/ + +uint16_t mode_particleHourglass(void) +{ + if (SEGLEN == 1) + return mode_static(); + ParticleSystem1D *PartSys = NULL; + + if (SEGMENT.call == 0) // initialization + { + if (!initParticleSystem1D(PartSys, 1)) // init + return mode_static(); // allocation failed + PartSys->setBounce(true); + PartSys->setWallHardness(60); + + for(uint32_t i = 0; i < PartSys->numParticles; i++) + { + PartSys->particles[i].collide = true; + PartSys->particles[i].ttl = 500; + PartSys->particles[i].perpetual = true; + PartSys->particles[i].x = PartSys->maxX - (i * PS_P_RADIUS_1D); //place particles, one at each pixel, highest number particles is lowest //TODO: need to reinit if number of particles changes + } + SEGMENT.aux0 = 0; + } + else + PartSys = reinterpret_cast(SEGMENT.data); // if not first call, just set the pointer to the PS + + if (PartSys == NULL) + { + DEBUG_PRINT(F("ERROR: FX PartSys nullpointer")); + return mode_static(); // something went wrong, no data! + } + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + + PartSys->setUsedParticles(SEGMENT.intensity>>1);//SEGMENT.custom1); + + PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur + PartSys->setGravity(2 + SEGMENT.custom3); //TODO: limit to 16max or things start flipping + PartSys->enableParticleCollisions(true,SEGMENT.custom1); + if(SEGMENT.aux1 == 0) //initialize + { + for(uint32_t i = 0; i < PartSys->usedParticles; i++) + { + // PartSys->particles[i].x = PartSys->maxX - (i * PS_P_RADIUS_1D ); //place particles, one at each pixel, highest order particles is lowest + // PartSys->particles[i].x = PS_P_RADIUS_1D*i; + PartSys->particles[i].hue = (i%2)*100;//(i * 255) / PartSys->usedParticles; + PartSys->particles[i].reversegrav = true; + } + SEGMENT.aux0 = PartSys->usedParticles; //index of lowest particle (+1) + SEGMENT.aux1 = SEGMENT.speed<<1; + } + + if(SEGMENT.aux0 == 0) + { + for(uint32_t i = 0; i < PartSys->usedParticles; i++) + { + PartSys->particles[i].reversegrav = true; + PartSys->particles[i].fixed = false; + } + SEGMENT.aux1--; + } + + else + { + for(uint32_t i = SEGMENT.aux0; i < PartSys->usedParticles; i++) //fallen particles (but not the last few, a few particles stack nicely without pinning, after like 20 it oscillates) + { + + uint32_t targetposition = ((PartSys->usedParticles - i) * PS_P_RADIUS_1D) - (PS_P_RADIUS_1D / 2); // target resting position + if(PartSys->particles[i].x <= targetposition && PartSys->particles[i].vx == 0) //particle has come to rest, pin it (for some reason this is not needed in the opposite direction...) + PartSys->particles[i].fixed = true; + + } + } - if(PartSys->sources[i].source.ttl == 0) //explode +/* + for(uint32_t i = PartSys->usedParticles + 1; i >= SEGMENT.aux0 ; i--) //fallen particles (but not the last few, a few particles stack nicely without pinning, after like 20 it oscillates) + { + Serial.print(PartSys->particles[i].x); + Serial.print(" "); + Serial.print(PartSys->particles[i].vx); + Serial.print("|"); + } + Serial.println(" ");*/ +/* + if(SEGMENT.call % 90 == 0) + { + for(uint32_t i = 0; i < PartSys->usedParticles; i++) + { + if(PartSys->particles[i].x - PartSys->particles[i+1].x > 0) { - PartSys->sources[i].source.perpetual = 1; // set standby state - PartSys->sources[i].var = 10 + SEGMENT.intensity >> 2; - PartSys->sources[i].v = 0; //TODO can make global if this never changes - PartSys->sources[i].minLife = 60; - PartSys->sources[i].maxLife = 150; - if(i & 0x01) - PartSys->sources[i].source.ttl = SEGMENT.custom1 + random16((255 - SEGMENT.intensity)+SEGMENT.custom1); // standby time til next launch - else - PartSys->sources[i].source.ttl = (255 - SEGMENT.custom1) + random16((255 - SEGMENT.intensity) + (255 - SEGMENT.custom1)); - //PartSys->sources[i].source.ttl = 80 + random16(255 - SEGMENT.intensity); // - PartSys->sources[i].sat = 7 + (SEGMENT.custom3 << 3); //color saturation - uint32_t explosionsize = 10 + random(SEGMENT.intensity >> 2, SEGMENT.intensity); - for(uint32_t e = 0; e < explosionsize; e++) //emit explosion particles - { - if(SEGMENT.check2) - PartSys->sources[i].source.hue = random16(); //random color for each particle - PartSys->sprayEmit(PartSys->sources[i]); //emit a particle - } - PartSys->sources[i].source.x = -500; //set out of frame until relaunch + //Serial.print(i); + Serial.print("f"); } - } - if(SEGMENT.call & 0x01) //every second frame - PartSys->sprayEmit(PartSys->sources[i]); //emit a particle + if(SEGMENT.aux0 > i) Serial.print("*"); + Serial.print(" "); } - Serial.println(" *"); - PartSys->update(); // update and render + Serial.println("x"); + }*/ + + if(SEGMENT.call % (256 - SEGMENT.speed) == 0 && SEGMENT.aux0 > 0) + { + SEGMENT.aux0--; + PartSys->particles[SEGMENT.aux0].reversegrav = false; //let this particle fall + PartSys->particles[SEGMENT.aux0].hue += 100; + } + + // PartSys->particles[PartSys->usedParticles - 1].fixed = true; //fix test + // PartSys->particles[PartSys->usedParticles - 1].x = PS_P_RADIUS_1D * 5; //fix test + //if(SEGMENT.call % 4 == 0) + // PartSys->applyFriction(1); //keeps particles calm and stops mass collisions being handled improperly due to chaos + + if (SEGMENT.check3) + PartSys->setParticleSize(1); + else + PartSys->setParticleSize(0); + + PartSys->update(); // update and render return FRAMETIME; } -static const char _data_FX_MODE_PS_FIREWORKS1D[] PROGMEM = "PS Fireworks 1D@Gravity,Explosion,Firing side,Blur/Overlay,Saturation,Gravity,Colorful,Smooth;,!;!;1;pal=0,sx=150,ix=150,c1=220,c2=30,c3=21,o1=0,o2=0,o3=0"; +static const char _data_FX_MODE_PS_HOURGLASS[] PROGMEM = "PS Hourglass@Speed,!,Hardness,Blur/Overlay,Gravity,Direction,Wrap/Bounce,Smooth;,!;!;1;pal=0,sx=50,ix=200,c1=0,c2=0,c3=0,o1=0,o2=1,o3=0"; + #endif //WLED_DISABLE_PARTICLESYSTEM1D @@ -10418,6 +10633,9 @@ addEffect(FX_MODE_PSDRIP, &mode_particleDrip, _data_FX_MODE_PARTICLEDRIP); addEffect(FX_MODE_PSBOUNCINGBALLS, &mode_particleBouncingBalls, _data_FX_MODE_PSBOUNCINGBALLS); //potential replacement for: bouncing balls, rollingballs, popcorn addEffect(FX_MODE_PSDANCINGSHADOWS, &mode_particleDancingShadows, _data_FX_MODE_PARTICLEDANCINGSHADOWS); addEffect(FX_MODE_PSFIREWORKS1D, &mode_particleFireworks1D, _data_FX_MODE_PS_FIREWORKS1D); +addEffect(FX_MODE_PSSPARKLER, &mode_particleSparkler, _data_FX_MODE_PS_SPARKLER); +addEffect(FX_MODE_PSHOURGLASS, &mode_particleHourglass, _data_FX_MODE_PS_HOURGLASS); + #endif // WLED_DISABLE_PARTICLESYSTEM1D } diff --git a/wled00/FX.h b/wled00/FX.h index 9161a6b1be..6a9a835711 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -336,7 +336,9 @@ #define FX_MODE_PSBOUNCINGBALLS 203 #define FX_MODE_PSDANCINGSHADOWS 204 #define FX_MODE_PSFIREWORKS1D 205 -#define MODE_COUNT 206 +#define FX_MODE_PSSPARKLER 206 +#define FX_MODE_PSHOURGLASS 207 +#define MODE_COUNT 208 typedef enum mapping1D2D { M12_Pixels = 0, diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index 73ee77f1d8..0afc02dace 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -145,7 +145,7 @@ void ParticleSystem::setWallRoughness(uint8_t roughness) void ParticleSystem::setCollisionHardness(uint8_t hardness) { - collisionHardness = hardness; + collisionHardness = (int)hardness + 1; } void ParticleSystem::setMatrixSize(uint16_t x, uint16_t y) @@ -216,7 +216,7 @@ void ParticleSystem::setGravity(int8_t force) 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; + collisionHardness = (int)hardness + 1; } // emit one particle with variation, returns index of last emitted particle (or -1 if no particle emitted) @@ -723,7 +723,7 @@ void ParticleSystem::ParticleSys_render(bool firemode, uint32_t fireintensity) framebuffer = allocate2Dbuffer(maxXpixel + 1, maxYpixel + 1); if (framebuffer == NULL) { - Serial.println("Frame buffer alloc failed"); + //Serial.println("Frame buffer alloc failed"); useLocalBuffer = false; //render to segment pixels directly if not enough memory } else{ @@ -751,7 +751,7 @@ void ParticleSystem::ParticleSys_render(bool firemode, uint32_t fireintensity) if (!useLocalBuffer) //disabled or allocation above failed { - Serial.println("NOT using local buffer!"); + //Serial.println("NOT using local buffer!"); if (motionBlur > 0) SEGMENT.fadeToBlackBy(255 - motionBlur); else @@ -1226,7 +1226,7 @@ void ParticleSystem::collideParticles(PSparticle *particle1, PSparticle *particl else particle1->x--; } - particle1->vx += push; + particle1->vx += push; //TODO: what happens if particle2 is also pushed? in 1D it stacks better, maybe also just reverse the comparison order so they flip roles? push = 0; if (dy < 0) push = pushamount; @@ -1543,11 +1543,6 @@ void ParticleSystem1D::setWallHardness(uint8_t hardness) wallHardness = hardness; } -void ParticleSystem1D::setCollisionHardness(uint8_t hardness) -{ - collisionHardness = hardness; -} - void ParticleSystem1D::setSize(uint16_t x) { maxXpixel = x - 1; // last physical pixel that can be drawn to @@ -1647,6 +1642,12 @@ void ParticleSystem1D::particleMoveUpdate(PSparticle1D &part, PSsettings *option part.ttl--; // age if (particlesettings.colorByAge) part.hue = part.ttl > 255 ? 255 : part.ttl; //set color to ttl + + if(part.fixed) + { + part.vx = 0; //set speed to zero. note: üarticle can get speed in collisions, if unfixed, it should not speed away + return; + } //bool usesize = false; // particle uses individual size rendering int32_t newX = part.x + (int16_t)part.vx; @@ -1654,14 +1655,34 @@ void ParticleSystem1D::particleMoveUpdate(PSparticle1D &part, PSsettings *option // if wall collisions are enabled, bounce them before they reach the edge, it looks much nicer if the particle is not half out of view if (options->bounceX) { - if ((newX < particleHardRadius) || ((newX > maxX - particleHardRadius) && !options->useGravity)) // reached a wall + if ((newX < particleHardRadius) || ((newX > maxX - particleHardRadius))) // reached a wall { - part.vx = -part.vx; //invert speed - part.vx = ((int32_t)part.vx * wallHardness) / 255; // reduce speed as energy is lost on non-hard surface - if (newX < particleHardRadius) - newX = particleHardRadius; // fast particles will never reach the edge if position is inverted, this looks better - else - newX = maxX - particleHardRadius; + bool bouncethis = true; + if(options->useGravity) + { + if(part.reversegrav) //skip at x = 0 + { + if(newX < particleHardRadius) + bouncethis = false; + } + else //skip at x = max + { + if(newX > particleHardRadius) + bouncethis = false; + } + } + + if(bouncethis) + { + part.vx = -part.vx; //invert speed + part.vx = ((int32_t)part.vx * wallHardness) / 255; // reduce speed as energy is lost on non-hard surface + if (newX < particleHardRadius) + newX = particleHardRadius; // fast particles will never reach the edge if position is inverted, this looks better + else + newX = maxX - particleHardRadius; + } + + } } if ((newX < 0) || (newX > maxX)) // check if particle reached an edge (note: this also checks out of bounds and must not be skipped, even if bounce is enabled) @@ -1677,9 +1698,9 @@ void ParticleSystem1D::particleMoveUpdate(PSparticle1D &part, PSsettings *option part.outofbounds = 1; if (options->killoutofbounds) { - if (newX < 0) // if gravity is enabled, only kill particles below ground - part.ttl = 0; - else if (!options->useGravity) + // if (newX < 0) // if gravity is enabled, only kill particles below ground + // part.ttl = 0; + // else if (!options->useGravity) part.ttl = 0; } } @@ -1765,7 +1786,7 @@ void ParticleSystem1D::ParticleSys_render() framebuffer = allocate1Dbuffer(maxXpixel + 1); if (framebuffer == NULL) { - Serial.println("Frame buffer alloc failed"); + //Serial.println("Frame buffer alloc failed"); useLocalBuffer = false; //render to segment pixels directly if not enough memory } else{ @@ -1786,7 +1807,7 @@ void ParticleSystem1D::ParticleSys_render() if (!useLocalBuffer) //disabled or allocation above failed { - Serial.println("NOT using local buffer!"); + //Serial.println("NOT using local buffer!"); if (motionBlur > 0) SEGMENT.fadeToBlackBy(255 - motionBlur); else @@ -1833,7 +1854,7 @@ void ParticleSystem1D::renderParticle(CRGB *framebuffer, uint32_t particleindex, if(x <= maxXpixel) //by making x unsigned there is no need to check < 0 as it will overflow { if (framebuffer) - fast_color_add(framebuffer[x], color, brightness); // order is: bottom left, bottom right, top right, top left + fast_color_add(framebuffer[x], color, brightness); else SEGMENT.addPixelColor(x, color.scale8((uint8_t)brightness)); } @@ -1910,30 +1931,34 @@ void ParticleSystem1D::handleCollisions() uint32_t endparticle = usedParticles;// >> 1; // do half the particles, significantly speeds things up // 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 more accurate collisions are needed, just call it twice in a row - //if (collisioncounter & 0x01) - //{ - // startparticle = endparticle; - // endparticle = usedParticles; - //} - // collisioncounter++; + /*if (SEGMENT.call & 0x01) //every other frame, do the other half + { + startparticle = endparticle; + endparticle = usedParticles; + } */ + int32_t proximity = particleHardRadius; //PS_P_MINHARDRADIUS_1D; + + if(particlesize == 0) + proximity = PS_P_MINHARDRADIUS_1D; //1 pixel sized particles have half the radius, need to increase detection distance it for proper collisions (and stacking) + //proximity = particlesize; for (i = startparticle; i < endparticle; i++) { // go though all 'higher number' particles and see if any of those are in close proximity and if they are, make them collide if (particles[i].ttl > 0 && particles[i].outofbounds == 0 && particles[i].collide) // if particle is alive and does collide and is not out of view { - int32_t dx; // distance to other particles - int32_t proximity; + int32_t dx; // distance to other particles for (j = i + 1; j < usedParticles; j++) // check against higher number particles { if (particles[j].ttl > 0 && particles[j].collide) // if target particle is alive { - dx = particles[i].x - particles[j].x; - int32_t dv = (int32_t)particles[i].vx - (int32_t)particles[j].vx; - proximity = particleHardRadius + abs(dv); //add speed difference to catch fast particles + dx = particles[j].x - particles[i].x; + int32_t dv = (int32_t)particles[j].vx - (int32_t)particles[i].vx; + if(dv > proximity) //particles would go past each other in next move upate + proximity += abs(dv); //add speed difference to catch fast particles if (dx < proximity && dx > -proximity) // check if close { - collideParticles(&particles[i], &particles[j]); + collideParticles(&particles[i], &particles[j], dx, dv); } } } @@ -1943,12 +1968,13 @@ void ParticleSystem1D::handleCollisions() // handle a collision if close proximity is detected, i.e. dx and/or dy smaller than 2*PS_P_RADIUS // takes two pointers to the particles to collide and the particle hardness (softer means more energy lost in collision, 255 means full hard) -void ParticleSystem1D::collideParticles(PSparticle1D *particle1, PSparticle1D *particle2) // TODO: dx,dy is calculated just above, can pass it over here to save a few CPU cycles? +void ParticleSystem1D::collideParticles(PSparticle1D *particle1, PSparticle1D *particle2, int32_t dx, int32_t relativeVx) { - int32_t dx = particle2->x - particle1->x; + // int32_t dx = particle2->x - particle1->x; // Calculate relative velocity (if it is zero, could exit but extra check does not overall speed but deminish it) - int32_t relativeVx = (int16_t)particle2->vx - (int16_t)particle1->vx; - + //int32_t relativeVx = (int32_t)particle2->vx - (int32_t)particle1->vx; + //Serial.print(" d"); Serial.print(dx); Serial.print(" v"); Serial.print(relativeVx); +/* // if dx is zero (i.e. they meet at the center) give them an offset, if speeds are also zero, also offset them (pushes them apart if they are clumped before enabling collisions) if (dx == 0) { @@ -1958,48 +1984,102 @@ void ParticleSystem1D::collideParticles(PSparticle1D *particle1, PSparticle1D *p dx = 1; else if (relativeVx == 0) relativeVx = 1; - } + }*/ // Calculate dot product of relative velocity and relative distance int32_t dotProduct = (dx * relativeVx); // is always negative if moving towards each other - int32_t notsorandom = dotProduct & 0x01; //dotprouct LSB should be somewhat random, so no need to calculate a random number - + // int32_t notsorandom = dotProduct & 0x01; //dotprouct LSB should be somewhat random, so no need to calculate a random number + //Serial.print(" dp"); Serial.print(dotProduct); if (dotProduct < 0) // particles are moving towards each other { - // integer math used to avoid floats. - // overflow check: dx/dy are 7bit, relativV are 8bit -> dotproduct is 15bit, dotproduct/distsquared ist 8b, multiplied by collisionhardness of 8bit. so a 16bit shift is ok, make it 15 to be sure no overflows happen - // note: cannot use right shifts as bit shifting in right direction is asymmetrical for positive and negative numbers and this needs to be accurate! the trick is: only shift positive numers + // integer math used to avoid floats. // Calculate new velocities after collision - uint32_t surfacehardness = collisionHardness < PS_P_MINSURFACEHARDNESS ? PS_P_MINSURFACEHARDNESS : collisionHardness; // if particles are soft, the impulse must stay above a limit or collisions slip through at higher speeds, 170 seems to be a good value - int32_t impulse = -(((((-dotProduct) << 15) / dx) * surfacehardness) >> 23); // note: inverting before bitshift corrects for asymmetry in right-shifts (and is slightly faster) - //int32_t ximpulse = ((impulse) * dx) / 32767; // cannot use bit shifts here, it can be negative, use division by 2^bitshift - particle1->vx += impulse; //TODO: need to flip directions? + uint32_t surfacehardness = collisionHardness < PS_P_MINSURFACEHARDNESS_1D ? PS_P_MINSURFACEHARDNESS_1D : collisionHardness; // if particles are soft, the impulse must stay above a limit or collisions slip through + //TODO: if soft collisions are not needed, the above line can be done in sethardness function and skipped here. + + //particles slow down due to collision losses + particle1->vx = ((int32_t)particle1->vx * surfacehardness) / 255; + particle2->vx = ((int32_t)particle2->vx * surfacehardness) / 255; + + //calculate (soft) collision impulse and add it to the speed + //int32_t impulse = (((((relativeVx) << 15)) * surfacehardness) >> 23); //note: this calculation is biased slightly towards negative numbers due to bit shifts, which is not relevant in 1D system + //int32_t impulse = (((((relativeVx) << 15)) * surfacehardness) / 8388607); + int32_t impulse = relativeVx * surfacehardness / 255; + + particle1->vx += impulse; particle2->vx -= impulse; - - // particles have volume, push particles apart if they are too close - // tried lots of configurations, it works best if not moved but given a little velocity, it tends to oscillate less this way - // a problem with giving velocity is, that on harder collisions, this adds up as it is not dampened enough, so add friction in the FX if required - if (dotProduct > -250) //this means particles are slow (or really really close) so push them apart. + + //if one of the particles is fixed, transfer its impulse back to the other + if(particle1->fixed) + particle2->vx -= impulse; + else if(particle2->fixed) + particle1->vx += impulse; + +// particle1->vx = (particle1->vx < 5 && particle1->vx > -5) ? 0 : particle1->vx; +// particle2->vx = (particle2->vx < 5 && particle2->vx > -5) ? 0 : particle2->vx; + + + // if (collisionHardness < surfacehardness) // if particles are soft, they become 'sticky' i.e. apply some friction (they do pile more nicely) + // { + // const uint32_t coeff = collisionHardness;// + (255 - PS_P_MINSURFACEHARDNESS); + // particle1->vx = ((int32_t)particle1->vx * coeff) / 255; + // particle2->vx = ((int32_t)particle2->vx * coeff) / 255; +/* + if (collisionHardness < 100) // if they are very soft, stop slow particles completely to make them stick to each other + { + particle1->vx = (particle1->vx < 4 && particle1->vx > -4) ? 0 : particle1->vx; + particle2->vx = (particle2->vx < 4 && particle2->vx > -4) ? 0 : particle2->vx; + }*/ + // } + + + } + + uint32_t distance = abs(dx); + uint32_t collisiondistance = particleHardRadius; + if(particlesize == 0) //single pixel particles + collisiondistance = PS_P_RADIUS_1D; //single pixel particles have a radius of PS_P_RADIUS_1D/2, use diameter for proper stacking + // particles have volume, push particles apart if they are too close + // behaviour is different than in 2D, we need pixel accurate stacking here, push the top particle to full radius (direction is well defined in 1D) + // also need to give the top particle some speed to counteract gravity or stacks just collapse + if (distance < collisiondistance) //particles are too close, push the upper particle away { - int32_t pushamount = 1 + ((250 + dotProduct) >> 6); // the closer dotproduct is to zero, the closer the particles are - int32_t push = 0; - if (dx < 0) // particle 1 is on the right - push = pushamount; - else if (dx > 0) - push = -pushamount; - else // on the same x coordinate, shift it a little so they do not stack + int32_t pushamount = 1 + (collisiondistance - distance)/2; + //int32_t pushamount = collisiondistance - distance; + //if(dotProduct > -30) pushamount++; //push harder if particles are very close + // + ((150 + dotProduct) >> 4); // the closer dotproduct is to zero, the closer the particles are + // int32_t push = 0; + if (dx < 0) // particle2.x < particle1.x { - if (notsorandom) - particle1->x++; // move it so pile collapses - else - particle1->x--; + if (particle2->reversegrav) + { + particle2->x -= pushamount; + // particle2->vx--; + } + else if (particle1->reversegrav == false) + { + particle1->x += pushamount; + // particle1->vx++; + } } - particle1->vx += push; - // note: pushing may push particles out of frame, if bounce is active, it will move it back as position will be limited to within frame, if bounce is disabled: bye bye + else + { + if(particle1->reversegrav) + { + particle1->x -= pushamount; + // particle1->vx--; + } + else if (particle2->reversegrav == false) + { + particle2->x += pushamount; + // particle2->vx++; + } + } } - } } + + // allocate memory for the 1D array in one contiguous block and set values to zero CRGB *ParticleSystem1D::allocate1Dbuffer(uint32_t length) { diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index e978d34e3a..a982dc6a49 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -29,8 +29,7 @@ #include #include "FastLED.h" -#define PS_P_MINSURFACEHARDNESS 128 // minimum hardness used in collision impulse calculation, below this hardness, particles become sticky -#define PS_P_MAXSPEED 120 // maximum speed a particle can have (vx/vy is int8) +#define PS_P_MAXSPEED 127 // maximum speed a particle can have (vx/vy is int8) //shared functions (used both in 1D and 2D system) @@ -72,6 +71,7 @@ typedef union #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_MINHARDRADIUS 70 // minimum hard surface radius +#define PS_P_MINSURFACEHARDNESS 128 // minimum hardness used in collision impulse calculation, below this hardness, particles become sticky //struct for a single particle (10 bytes) typedef struct { @@ -208,12 +208,12 @@ class ParticleSystem PSsettings particlesettings; // settings used when updating particles (can also used by FX to move sources), do not edit properties directly, use functions above uint32_t emitIndex; // index to count through particles to emit so searching for dead pixels is faster int32_t collisionHardness; - uint8_t wallHardness; - uint8_t wallRoughness; + uint32_t wallHardness; + uint32_t wallRoughness; uint8_t gforcecounter; // counter for global gravity int8_t gforce; // gravity strength, default is 8 (negative is allowed, positive is downwards) - uint8_t collisioncounter; // counter to handle collisions TODO: could use the SEGMENT.call? - uint8_t forcecounter; // counter for globally applied forces + uint32_t collisioncounter; // counter to handle collisions TODO: could use the SEGMENT.call? + uint32_t forcecounter; // counter for globally applied forces // global particle properties for basic particles uint8_t particlesize; // global particle size, 0 = 2 pixels, 255 = 10 pixels (note: this is also added to individual sized particles) int32_t particleHardRadius; // hard surface radius of a particle, used for collision detection @@ -246,7 +246,8 @@ bool allocateParticleSystemMemory2D(uint16_t numparticles, uint16_t numsources, #define PS_P_HALFRADIUS_1D 16 #define PS_P_RADIUS_SHIFT_1D 5 //TODO: may need to adjust #define PS_P_SURFACE_1D 5 // shift: 2^PS_P_SURFACE = PS_P_RADIUS_1D -#define PS_P_MINHARDRADIUS_1D 32 // minimum hard surface radius TODO: also needs tweaking +#define PS_P_MINHARDRADIUS_1D 30 // minimum hard surface radius +#define PS_P_MINSURFACEHARDNESS_1D 0 // minimum hardness used in collision impulse calculation //struct for a single particle (6 bytes) typedef struct { @@ -254,11 +255,12 @@ typedef struct { int8_t vx; // horizontal velocity uint8_t hue; // color hue // two byte bit field: - uint16_t ttl : 12; // time to live, 12 bit or 4095 max (which is 50s at 80FPS) + uint16_t ttl : 11; // time to live, 11 bit or 2047 max (which is 25s at 80FPS) bool outofbounds : 1; // out of bounds flag, set to true if particle is outside of display area bool collide : 1; // if set, particle takes part in collisions bool perpetual : 1; // if set, particle does not age (TTL is not decremented in move function, it still dies from killoutofbounds) bool reversegrav : 1; // if set, gravity is reversed on this particle + bool fixed : 1; // if set, particle does not move (and collisions make other particles revert direction) } PSparticle1D; // struct for additional particle settings (optional) @@ -331,7 +333,7 @@ class ParticleSystem1D //paricle physics applied by system if flags are set void applyGravity(); // applies gravity to all particles void handleCollisions(); - void collideParticles(PSparticle1D *particle1, PSparticle1D *particle2); + void collideParticles(PSparticle1D *particle1, PSparticle1D *particle2, int32_t dx, int32_t relativeVx); //utility functions void updatePSpointers(bool isadvanced); // update the data pointers to current segment data space @@ -343,7 +345,7 @@ class ParticleSystem1D PSsettings particlesettings; // settings used when updating particles uint32_t emitIndex; // index to count through particles to emit so searching for dead pixels is faster int32_t collisionHardness; - uint8_t wallHardness; + uint32_t wallHardness; uint8_t gforcecounter; // counter for global gravity int8_t gforce; // gravity strength, default is 8 (negative is allowed, positive is downwards) uint8_t forcecounter; // counter for globally applied forces From 3171589a1506a8203ab7f923bc908ba12ede4695 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Tue, 11 Jun 2024 22:02:26 +0200 Subject: [PATCH 095/219] many improvements in 1D collisions (fine tuned), hour glass FX is almost done spent A LOT of time in fine-tuning collisions for best stacking without collipsing, ringing, oscillations or particle flips. still not perfect but probably as good as it can be with the limited time-resolution --- wled00/FX.cpp | 182 ++++++++++++++++++++---------------- wled00/FXparticleSystem.cpp | 131 +++++++++++--------------- wled00/FXparticleSystem.h | 9 +- 3 files changed, 163 insertions(+), 159 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 8c19f0ab7c..138d03468b 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -9813,14 +9813,14 @@ uint16_t mode_particleBouncingBalls(void) // Particle System settings PartSys->updateSystem(); // update system properties (dimensions and data pointers) - PartSys->setWallHardness(SEGMENT.custom1); - PartSys->setGravity(1 + (SEGMENT.custom3>>1)); // set gravity (8 is default strength) + PartSys->setWallHardness(240 + (SEGMENT.custom1>>4)); + PartSys->setGravity(1 + (SEGMENT.custom3 >> 1)); // set gravity (8 is default strength) PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur //if (SEGMENT.check2) PartSys->setMotionBlur(255); //full motion blurring allows overlay (motion blur does not work with overlay) PartSys->sources[0].var = SEGMENT.speed >> 3; PartSys->sources[0].v = (SEGMENT.speed >> 1) - (SEGMENT.speed >> 3); if (SEGMENT.check1) // collisions enabled - PartSys->enableParticleCollisions(true, 127 + (SEGMENT.custom1>>1)); // enable collisions and set particle collision hardness + PartSys->enableParticleCollisions(true, 240 + (SEGMENT.custom1>>4)); // enable collisions and set particle collision hardness else PartSys->enableParticleCollisions(false); PartSys->setUsedParticles( 1 + (SEGMENT.intensity >> 3)); // 1 - 32 @@ -9842,11 +9842,11 @@ uint16_t mode_particleBouncingBalls(void) for(uint32_t i = 0; i < PartSys->usedParticles; i++) { - if(PartSys->particles[i].vx > 5 || PartSys->particles[i].vx < -5) //let only slow particles die (ensures no stopped particles) - PartSys->particles[i].ttl = 300; //set alive at full intensity + if(PartSys->particles[i].vx > 8 || PartSys->particles[i].vx < -8) //let only slow particles die (ensures no stopped particles) + PartSys->particles[i].ttl = 260; //set alive at full intensity if(updatespeed || PartSys->particles[i].ttl == 0) //speed changed or particle died, reset TTL and speed { - PartSys->particles[i].ttl = 300; + PartSys->particles[i].ttl = 260; PartSys->particles[i].collide = true; int32_t newspeed = random(20 + (SEGMENT.speed >> 2)) + (SEGMENT.speed >> 3); PartSys->particles[i].vx = PartSys->particles[i].vx > 0 ? newspeed : -newspeed; //keep the direction @@ -10227,7 +10227,7 @@ uint16_t mode_particleSparkler(void) for(i = 0; i < numSparklers; i++) { - if((SEGMENT.call + random16(30)) % (1 + ((255 - SEGMENT.intensity) >> 3)) == 0) + if(random16() % (1 + ((255 - SEGMENT.intensity) >> 3)) == 0) PartSys->sprayEmit(PartSys->sources[i]); //emit a particle } SEGMENT.aux0 = SEGMENT.check1; @@ -10250,22 +10250,24 @@ uint16_t mode_particleHourglass(void) if (SEGLEN == 1) return mode_static(); ParticleSystem1D *PartSys = NULL; - + int32_t positionoffset; // resting position offset + bool* direction; + uint8_t* basehue; if (SEGMENT.call == 0) // initialization { - if (!initParticleSystem1D(PartSys, 1)) // init + if (!initParticleSystem1D(PartSys, 0, 2)) // init return mode_static(); // allocation failed PartSys->setBounce(true); - PartSys->setWallHardness(60); + PartSys->setWallHardness(80); for(uint32_t i = 0; i < PartSys->numParticles; i++) { PartSys->particles[i].collide = true; PartSys->particles[i].ttl = 500; - PartSys->particles[i].perpetual = true; - PartSys->particles[i].x = PartSys->maxX - (i * PS_P_RADIUS_1D); //place particles, one at each pixel, highest number particles is lowest //TODO: need to reinit if number of particles changes + PartSys->particles[i].perpetual = true; } - SEGMENT.aux0 = 0; + //SEGMENT.aux0 = 0; + SEGMENT.step = 0xFFFF; } else PartSys = reinterpret_cast(SEGMENT.data); // if not first call, just set the pointer to the PS @@ -10277,95 +10279,115 @@ uint16_t mode_particleHourglass(void) } // Particle System settings PartSys->updateSystem(); // update system properties (dimensions and data pointers) + basehue = PartSys->PSdataEnd; //assign data pointer + direction = reinterpret_cast(PartSys->PSdataEnd + 1); //assign data pointer + uint32_t numgrains = map(SEGMENT.intensity, 0, 255, 1, PartSys->maxXpixel + 1); // number of particles to use + PartSys->setUsedParticles(min(numgrains, (uint32_t)PartSys->numParticles));//SEGMENT.custom1); + PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur + PartSys->setGravity(map(SEGMENT.custom3, 0, 31, 1, 30)); + PartSys->enableParticleCollisions(true, 34); // fixed hardness, 34 is a value that works best in most settings (spent a long time optimizing) SEGMENT.custom1); - PartSys->setUsedParticles(SEGMENT.intensity>>1);//SEGMENT.custom1); + positionoffset = PS_P_RADIUS_1D / 2; + uint32_t colormode = SEGMENT.custom1 >> 5; // 0-7 - PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur - PartSys->setGravity(2 + SEGMENT.custom3); //TODO: limit to 16max or things start flipping - PartSys->enableParticleCollisions(true,SEGMENT.custom1); - if(SEGMENT.aux1 == 0) //initialize - { - for(uint32_t i = 0; i < PartSys->usedParticles; i++) - { - // PartSys->particles[i].x = PartSys->maxX - (i * PS_P_RADIUS_1D ); //place particles, one at each pixel, highest order particles is lowest - // PartSys->particles[i].x = PS_P_RADIUS_1D*i; - PartSys->particles[i].hue = (i%2)*100;//(i * 255) / PartSys->usedParticles; - PartSys->particles[i].reversegrav = true; - } - SEGMENT.aux0 = PartSys->usedParticles; //index of lowest particle (+1) - SEGMENT.aux1 = SEGMENT.speed<<1; - } - if(SEGMENT.aux0 == 0) - { + if(SEGMENT.intensity != SEGMENT.step) //initialize + { + *basehue = random16(); //choose new random color + SEGMENT.step = SEGMENT.intensity; for(uint32_t i = 0; i < PartSys->usedParticles; i++) - { + { PartSys->particles[i].reversegrav = true; - PartSys->particles[i].fixed = false; - } - SEGMENT.aux1--; + *direction = 0; + SEGMENT.aux1 = 1; //initialize below + } + SEGMENT.aux0 = PartSys->usedParticles - 1; //initial state, start with highest number particle } - else + for(uint32_t i = 0; i < PartSys->usedParticles; i++) //check if particle reached target position after falling { - for(uint32_t i = SEGMENT.aux0; i < PartSys->usedParticles; i++) //fallen particles (but not the last few, a few particles stack nicely without pinning, after like 20 it oscillates) + uint32_t targetposition; + if (PartSys->particles[i].fixed == false) { - - uint32_t targetposition = ((PartSys->usedParticles - i) * PS_P_RADIUS_1D) - (PS_P_RADIUS_1D / 2); // target resting position - if(PartSys->particles[i].x <= targetposition && PartSys->particles[i].vx == 0) //particle has come to rest, pin it (for some reason this is not needed in the opposite direction...) - PartSys->particles[i].fixed = true; - - } - } + //calculate target position depending on direction + if(PartSys->particles[i].reversegrav) + targetposition = PartSys->maxX - (i * PS_P_RADIUS_1D + positionoffset); // target resting position + else + targetposition = (PartSys->usedParticles - i) * PS_P_RADIUS_1D - positionoffset; // target resting position + if(PartSys->particles[i].x == targetposition) //particle has reached target position, pin it. if not pinned, they do not stack well on larger piles + PartSys->particles[i].fixed = true; + } + if(colormode == 7) + PartSys->particles[i].hue = (255 * (uint32_t)PartSys->particles[i].x) / PartSys->maxX; //color fixed by position + else + { + switch(colormode) { + case 0: PartSys->particles[i].hue = 120; break; //fixed at 120, if flip is activated, this can make red and green (use palette 34) + case 1: PartSys->particles[i].hue = *basehue; break; //fixed random color + case 2: + case 3: PartSys->particles[i].hue = *basehue + (i % colormode)*70; break; // interleved colors (every 2 or 3 particles) + case 4: PartSys->particles[i].hue = *basehue + (i * 255) / PartSys->usedParticles; break; // gradient palette colors + case 5: PartSys->particles[i].hue = *basehue + (i * 1024) / PartSys->usedParticles; break; // multi gradient palette colors + case 6: PartSys->particles[i].hue = i + (strip.now >> 1); break; // disco! fast moving color gradient + default: break; + } + } + if(SEGMENT.check1 && !PartSys->particles[i].reversegrav) // flip color when fallen + PartSys->particles[i].hue += 120; + } -/* - for(uint32_t i = PartSys->usedParticles + 1; i >= SEGMENT.aux0 ; i--) //fallen particles (but not the last few, a few particles stack nicely without pinning, after like 20 it oscillates) - { - Serial.print(PartSys->particles[i].x); - Serial.print(" "); - Serial.print(PartSys->particles[i].vx); - Serial.print("|"); - } - Serial.println(" ");*/ -/* - if(SEGMENT.call % 90 == 0) + + if(SEGMENT.aux1 == 1) //last countdown call before dropping starts, reset all particles { - for(uint32_t i = 0; i < PartSys->usedParticles; i++) + for(uint32_t i = 0; i < PartSys->usedParticles; i++) { - if(PartSys->particles[i].x - PartSys->particles[i+1].x > 0) - { - //Serial.print(i); - Serial.print("f"); - } - if(SEGMENT.aux0 > i) Serial.print("*"); - Serial.print(" "); + uint32_t targetposition; + //calculate target position depending on direction + if(PartSys->particles[i].reversegrav) + targetposition = PartSys->maxX - (i * PS_P_RADIUS_1D + positionoffset); // target resting position + else + targetposition = (PartSys->usedParticles - i) * PS_P_RADIUS_1D - positionoffset; // target resting position -5 - PS_P_RADIUS_1D/2 + + PartSys->particles[i].x = targetposition; + PartSys->particles[i].fixed = true; } - Serial.println("x"); - }*/ + } - if(SEGMENT.call % (256 - SEGMENT.speed) == 0 && SEGMENT.aux0 > 0) + if(SEGMENT.aux1 == 0) //countdown passed, run { - SEGMENT.aux0--; - PartSys->particles[SEGMENT.aux0].reversegrav = false; //let this particle fall - PartSys->particles[SEGMENT.aux0].hue += 100; + uint32_t interval = 257 - SEGMENT.speed; // drop interval in frames, 1 second is 'speed = (257 - FPS)' speed = 0 is one drop every 257 frames + if(SEGMENT.check3 && *direction) // fast reset + interval = 3; + if(SEGMENT.call % interval == 0) //drop a particle, do not drop more often than every second frame or particles tangle up quite badly + { + if(SEGMENT.aux0 < PartSys->usedParticles) + { + PartSys->particles[SEGMENT.aux0].reversegrav = *direction; //let this particle fall or rise + PartSys->particles[SEGMENT.aux0].fixed = false; // unpin + } + else //overflow, flip direction + { + *direction = !(*direction); + SEGMENT.aux1 = 300; //set countdown + } + if(*direction == 0) //down + SEGMENT.aux0--; + else + SEGMENT.aux0++; + } } + else if(SEGMENT.check2) //auto reset + SEGMENT.aux1--; //countdown - // PartSys->particles[PartSys->usedParticles - 1].fixed = true; //fix test - // PartSys->particles[PartSys->usedParticles - 1].x = PS_P_RADIUS_1D * 5; //fix test - - //if(SEGMENT.call % 4 == 0) - // PartSys->applyFriction(1); //keeps particles calm and stops mass collisions being handled improperly due to chaos + //if(SEGMENT.call % (SEGMENT.speed >> 5) == 0) //more friction on higher falling rate to keep particles behaved + //if(SEGMENT.call % 6 == 0) + //PartSys->applyFriction(1); //keeps particles calm and stops mass collisions being handled improperly due to chaos - if (SEGMENT.check3) - PartSys->setParticleSize(1); - else - PartSys->setParticleSize(0); - PartSys->update(); // update and render return FRAMETIME; } -static const char _data_FX_MODE_PS_HOURGLASS[] PROGMEM = "PS Hourglass@Speed,!,Hardness,Blur/Overlay,Gravity,Direction,Wrap/Bounce,Smooth;,!;!;1;pal=0,sx=50,ix=200,c1=0,c2=0,c3=0,o1=0,o2=1,o3=0"; +static const char _data_FX_MODE_PS_HOURGLASS[] PROGMEM = "PS Hourglass@Speed,!,Color,Blur/Overlay,Gravity,Colorflip,Auto Reset,Fast Reset;,!;!;1;pal=34,sx=245,ix=200,c1=140,c2=80,c3=4,o1=1,o2=1,o3=1"; #endif //WLED_DISABLE_PARTICLESYSTEM1D diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index 0afc02dace..8ca4b7f4f9 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -1580,7 +1580,7 @@ void ParticleSystem1D::setParticleSize(uint8_t size) { particlesize = size; if(particlesize) - particleHardRadius = PS_P_MINHARDRADIUS_1D + particlesize; //TODO: set lower on 1pixel particles? or set in bounce? does not bounce at boarder but boarder +1 + particleHardRadius = PS_P_MINHARDRADIUS_1D; else particleHardRadius = PS_P_MINHARDRADIUS_1D >> 1; //1 pixel sized particles have half the radius (for collisions & bounce) } @@ -1643,12 +1643,6 @@ void ParticleSystem1D::particleMoveUpdate(PSparticle1D &part, PSsettings *option if (particlesettings.colorByAge) part.hue = part.ttl > 255 ? 255 : part.ttl; //set color to ttl - if(part.fixed) - { - part.vx = 0; //set speed to zero. note: üarticle can get speed in collisions, if unfixed, it should not speed away - return; - } - //bool usesize = false; // particle uses individual size rendering int32_t newX = part.x + (int16_t)part.vx; part.outofbounds = 0; // reset out of bounds (in case particle was created outside the matrix and is now moving into view) @@ -1681,8 +1675,6 @@ void ParticleSystem1D::particleMoveUpdate(PSparticle1D &part, PSsettings *option else newX = maxX - particleHardRadius; } - - } } if ((newX < 0) || (newX > maxX)) // check if particle reached an edge (note: this also checks out of bounds and must not be skipped, even if bounce is enabled) @@ -1705,7 +1697,10 @@ void ParticleSystem1D::particleMoveUpdate(PSparticle1D &part, PSsettings *option } } } - part.x = (int16_t)newX; // set new position + if(!part.fixed) + part.x = (int16_t)newX; // set new position + else + part.vx = 0; //set speed to zero. note: particle can get speed in collisions, if unfixed, it should not speed away } } @@ -1947,14 +1942,14 @@ void ParticleSystem1D::handleCollisions() // go though all 'higher number' particles and see if any of those are in close proximity and if they are, make them collide if (particles[i].ttl > 0 && particles[i].outofbounds == 0 && particles[i].collide) // if particle is alive and does collide and is not out of view { - int32_t dx; // distance to other particles + int32_t dx; // distance to other particles for (j = i + 1; j < usedParticles; j++) // check against higher number particles { if (particles[j].ttl > 0 && particles[j].collide) // if target particle is alive { dx = particles[j].x - particles[i].x; int32_t dv = (int32_t)particles[j].vx - (int32_t)particles[i].vx; - if(dv > proximity) //particles would go past each other in next move upate + if(dv >= proximity) //particles would go past each other in next move upate proximity += abs(dv); //add speed difference to catch fast particles if (dx < proximity && dx > -proximity) // check if close { @@ -1970,21 +1965,6 @@ void ParticleSystem1D::handleCollisions() // takes two pointers to the particles to collide and the particle hardness (softer means more energy lost in collision, 255 means full hard) void ParticleSystem1D::collideParticles(PSparticle1D *particle1, PSparticle1D *particle2, int32_t dx, int32_t relativeVx) { - // int32_t dx = particle2->x - particle1->x; - // Calculate relative velocity (if it is zero, could exit but extra check does not overall speed but deminish it) - //int32_t relativeVx = (int32_t)particle2->vx - (int32_t)particle1->vx; - //Serial.print(" d"); Serial.print(dx); Serial.print(" v"); Serial.print(relativeVx); -/* - // if dx is zero (i.e. they meet at the center) give them an offset, if speeds are also zero, also offset them (pushes them apart if they are clumped before enabling collisions) - if (dx == 0) - { - // Adjust positions based on relative velocity direction - dx = -1; - if (relativeVx < 0) // if true, particle2 is on the right side - dx = 1; - else if (relativeVx == 0) - relativeVx = 1; - }*/ // Calculate dot product of relative velocity and relative distance int32_t dotProduct = (dx * relativeVx); // is always negative if moving towards each other @@ -1997,9 +1977,10 @@ void ParticleSystem1D::collideParticles(PSparticle1D *particle1, PSparticle1D *p uint32_t surfacehardness = collisionHardness < PS_P_MINSURFACEHARDNESS_1D ? PS_P_MINSURFACEHARDNESS_1D : collisionHardness; // if particles are soft, the impulse must stay above a limit or collisions slip through //TODO: if soft collisions are not needed, the above line can be done in sethardness function and skipped here. + //particles slow down due to collision losses - particle1->vx = ((int32_t)particle1->vx * surfacehardness) / 255; - particle2->vx = ((int32_t)particle2->vx * surfacehardness) / 255; + //particle1->vx = ((int32_t)particle1->vx * surfacehardness) / 255; + //particle2->vx = ((int32_t)particle2->vx * surfacehardness) / 255; //calculate (soft) collision impulse and add it to the speed //int32_t impulse = (((((relativeVx) << 15)) * surfacehardness) >> 23); //note: this calculation is biased slightly towards negative numbers due to bit shifts, which is not relevant in 1D system @@ -2009,21 +1990,23 @@ void ParticleSystem1D::collideParticles(PSparticle1D *particle1, PSparticle1D *p particle1->vx += impulse; particle2->vx -= impulse; - //if one of the particles is fixed, transfer its impulse back to the other + //if one of the particles is fixed, transfer the impulse back so it bounces if(particle1->fixed) - particle2->vx -= impulse; + particle2->vx = -particle1->vx; + //particle2->vx -= impulse; else if(particle2->fixed) - particle1->vx += impulse; + particle1->vx = -particle2->vx; + //particle1->vx += impulse; // particle1->vx = (particle1->vx < 5 && particle1->vx > -5) ? 0 : particle1->vx; // particle2->vx = (particle2->vx < 5 && particle2->vx > -5) ? 0 : particle2->vx; - - // if (collisionHardness < surfacehardness) // if particles are soft, they become 'sticky' i.e. apply some friction (they do pile more nicely) - // { - // const uint32_t coeff = collisionHardness;// + (255 - PS_P_MINSURFACEHARDNESS); - // particle1->vx = ((int32_t)particle1->vx * coeff) / 255; - // particle2->vx = ((int32_t)particle2->vx * coeff) / 255; + if (collisionHardness < surfacehardness) // if particles are soft, they become 'sticky' i.e. apply some friction (they do pile more nicely and correctly) + { + const uint32_t coeff = collisionHardness + 100;//(255 - PS_P_MINSURFACEHARDNESS_1D); + particle1->vx = ((int32_t)particle1->vx * coeff) / 255; + particle2->vx = ((int32_t)particle2->vx * coeff) / 255; + } /* if (collisionHardness < 100) // if they are very soft, stop slow particles completely to make them stick to each other { @@ -2035,47 +2018,45 @@ void ParticleSystem1D::collideParticles(PSparticle1D *particle1, PSparticle1D *p } + uint32_t distance = abs(dx); uint32_t collisiondistance = particleHardRadius; if(particlesize == 0) //single pixel particles collisiondistance = PS_P_RADIUS_1D; //single pixel particles have a radius of PS_P_RADIUS_1D/2, use diameter for proper stacking - // particles have volume, push particles apart if they are too close - // behaviour is different than in 2D, we need pixel accurate stacking here, push the top particle to full radius (direction is well defined in 1D) - // also need to give the top particle some speed to counteract gravity or stacks just collapse - if (distance < collisiondistance) //particles are too close, push the upper particle away - { - int32_t pushamount = 1 + (collisiondistance - distance)/2; - //int32_t pushamount = collisiondistance - distance; - //if(dotProduct > -30) pushamount++; //push harder if particles are very close - // + ((150 + dotProduct) >> 4); // the closer dotproduct is to zero, the closer the particles are - // int32_t push = 0; - if (dx < 0) // particle2.x < particle1.x - { - if (particle2->reversegrav) - { - particle2->x -= pushamount; - // particle2->vx--; - } - else if (particle1->reversegrav == false) - { - particle1->x += pushamount; - // particle1->vx++; - } - } - else - { - if(particle1->reversegrav) - { - particle1->x -= pushamount; - // particle1->vx--; - } - else if (particle2->reversegrav == false) + // particles have volume, push particles apart if they are too close + // behaviour is different than in 2D, we need pixel accurate stacking here, push the top particle to full radius (direction is well defined in 1D) + // also need to give the top particle some speed to counteract gravity or stacks just collapse + if (distance < collisiondistance) //particles are too close, push the upper particle away + { + int32_t pushamount = 1 + ((collisiondistance - distance) >> 1); //add half the remaining distance note: this works best, if less or more is added, it gets more chaotic + //int32_t pushamount = collisiondistance - distance; + if (dx < 0) // particle2.x < particle1.x + { + if (particle2->reversegrav && !particle2->fixed) { - particle2->x += pushamount; - // particle2->vx++; + particle2->x -= pushamount; + particle2->vx--; } - } + else if (!particle1->reversegrav && !particle1->fixed) + { + particle1->x += pushamount; + particle1->vx++; + } } + else + { + if(particle1->reversegrav && !particle1->fixed) + { + particle1->x -= pushamount; + particle1->vx--; + } + else if (!particle2->reversegrav && !particle2->fixed) + { + particle2->x += pushamount; + particle2->vx++; + } + } + } } @@ -2138,11 +2119,11 @@ uint32_t calculateNumberOfParticles1D(bool isadvanced) { uint32_t numberofParticles = SEGMENT.virtualLength(); // one particle per pixel (if possible) #ifdef ESP8266 - uint32_t particlelimit = ESP8266_MAXPARTICLES_1D; // maximum number of paticles allowed (based on one segment of 16x16 and 4k effect ram) + uint32_t particlelimit = ESP8266_MAXPARTICLES_1D; // maximum number of paticles allowed #elif ARDUINO_ARCH_ESP32S2 - uint32_t particlelimit = ESP32S2_MAXPARTICLES_1D; // maximum number of paticles allowed (based on one segment of 32x32 and 24k effect ram) + uint32_t particlelimit = ESP32S2_MAXPARTICLES_1D; // maximum number of paticles allowed #else - uint32_t particlelimit = ESP32_MAXPARTICLES_1D; // maximum number of paticles allowed (based on two segments of 32x32 and 40k effect ram) + uint32_t particlelimit = ESP32_MAXPARTICLES_1D; // maximum number of paticles allowed #endif numberofParticles = max((uint32_t)1, min(numberofParticles, particlelimit)); if (isadvanced) // advanced property array needs ram, reduce number of particles to use the same amount diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index a982dc6a49..68f0c87d4b 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -29,7 +29,7 @@ #include #include "FastLED.h" -#define PS_P_MAXSPEED 127 // maximum speed a particle can have (vx/vy is int8) +#define PS_P_MAXSPEED 120 // maximum speed a particle can have (vx/vy is int8) //shared functions (used both in 1D and 2D system) @@ -234,6 +234,7 @@ bool allocateParticleSystemMemory2D(uint16_t numparticles, uint16_t numsources, //////////////////////// #ifndef WLED_DISABLE_PARTICLESYSTEM1D // memory allocation +//MAX_SEGMENT_DATA #define ESP8266_MAXPARTICLES_1D 400 #define ESP8266_MAXSOURCES_1D 8 #define ESP32S2_MAXPARTICLES_1D 1900 @@ -246,8 +247,8 @@ bool allocateParticleSystemMemory2D(uint16_t numparticles, uint16_t numsources, #define PS_P_HALFRADIUS_1D 16 #define PS_P_RADIUS_SHIFT_1D 5 //TODO: may need to adjust #define PS_P_SURFACE_1D 5 // shift: 2^PS_P_SURFACE = PS_P_RADIUS_1D -#define PS_P_MINHARDRADIUS_1D 30 // minimum hard surface radius -#define PS_P_MINSURFACEHARDNESS_1D 0 // minimum hardness used in collision impulse calculation +#define PS_P_MINHARDRADIUS_1D 32 // minimum hard surface radius +#define PS_P_MINSURFACEHARDNESS_1D 50 // minimum hardness used in collision impulse calculation //struct for a single particle (6 bytes) typedef struct { @@ -260,7 +261,7 @@ typedef struct { bool collide : 1; // if set, particle takes part in collisions bool perpetual : 1; // if set, particle does not age (TTL is not decremented in move function, it still dies from killoutofbounds) bool reversegrav : 1; // if set, gravity is reversed on this particle - bool fixed : 1; // if set, particle does not move (and collisions make other particles revert direction) + bool fixed : 1; // if set, particle does not move (and collisions make other particles revert direction), } PSparticle1D; // struct for additional particle settings (optional) From c6202e25c034e0619198211faaf1b13f2e6aa469 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sat, 22 Jun 2024 12:28:57 +0200 Subject: [PATCH 096/219] many bugfixes in PS and FX --- wled00/FX.cpp | 98 +++++++++++++++++-------------------- wled00/FXparticleSystem.cpp | 12 ++--- 2 files changed, 52 insertions(+), 58 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 138d03468b..84754afa94 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -1257,7 +1257,7 @@ uint16_t mode_fireworks() { } static const char _data_FX_MODE_FIREWORKS[] PROGMEM = "Fireworks@,Frequency;!,!;!;12;ix=192,pal=11"; -/* + //Twinkling LEDs running. Inspired by https://github.com/kitesurfer1404/WS2812FX/blob/master/src/custom/Rain.h uint16_t mode_rain() { if (SEGLEN == 1) return mode_static(); @@ -1291,7 +1291,7 @@ uint16_t mode_rain() { return mode_fireworks(); } static const char _data_FX_MODE_RAIN[] PROGMEM = "Rain@!,Spawning rate;!,!;!;12;ix=128,pal=0"; -*/ + /* * Fire flicker function @@ -1919,7 +1919,7 @@ uint16_t mode_pride_2015(void) { } static const char _data_FX_MODE_PRIDE_2015[] PROGMEM = "Pride 2015@!;;"; -/* + //eight colored dots, weaving in and out of sync with each other uint16_t mode_juggle(void) { if (SEGLEN == 1) return mode_static(); @@ -1937,7 +1937,7 @@ uint16_t mode_juggle(void) { return FRAMETIME; } static const char _data_FX_MODE_JUGGLE[] PROGMEM = "Juggle@!,Trail;;!;;sx=64,ix=128"; -*/ + uint16_t mode_palette() { // Set up some compile time constants so that we can handle integer and float based modes using the same code base. @@ -2956,7 +2956,7 @@ uint16_t mode_spots_fade() return spots_base(tr); } static const char _data_FX_MODE_SPOTS_FADE[] PROGMEM = "Spots Fade@Spread,Width,,,,,Overlay;!,!;!"; -/* + //each needs 12 bytes typedef struct Ball { @@ -2964,11 +2964,11 @@ typedef struct Ball { float impactVelocity; float height; } ball; -*/ + /* * Bouncing Balls Effect */ -/* + uint16_t mode_bouncing_balls(void) { if (SEGLEN == 1) return mode_static(); //allocate segment data @@ -3041,14 +3041,14 @@ uint16_t mode_bouncing_balls(void) { return FRAMETIME; } static const char _data_FX_MODE_BOUNCINGBALLS[] PROGMEM = "Bouncing Balls@Gravity,# of balls,,,,,Overlay;!,!,!;!;1;m12=1"; //bar -*/ + /* * bouncing balls on a track track Effect modified from Aircoookie's bouncing balls * Courtesy of pjhatch (https://github.com/pjhatch) * https://github.com/Aircoookie/WLED/pull/1039 */ - /* + // modified for balltrack mode typedef struct RollingBall { unsigned long lastBounceUpdate; @@ -3143,7 +3143,7 @@ static uint16_t rolling_balls(void) { return FRAMETIME; } static const char _data_FX_MODE_ROLLINGBALLS[] PROGMEM = "Rolling Balls@!,# of balls,,,,Collisions,Overlay,Trails;!,!,!;!;1;m12=1"; //bar -*/ + /* * Sinelon stolen from FASTLED examples @@ -3253,7 +3253,7 @@ typedef struct Spark { * POPCORN * modified from https://github.com/kitesurfer1404/WS2812FX/blob/master/src/custom/Popcorn.h */ -/* + uint16_t mode_popcorn(void) { if (SEGLEN == 1) return mode_static(); //allocate segment data @@ -3312,7 +3312,7 @@ uint16_t mode_popcorn(void) { return FRAMETIME; } static const char _data_FX_MODE_POPCORN[] PROGMEM = "Popcorn@!,!,,,,,Overlay;!,!,!;!;;m12=1"; //bar -*/ + //values close to 100 produce 5Hz flicker, which looks very candle-y //Inspired by https://github.com/avanhanegem/ArduinoCandleEffectNeoPixel @@ -7995,7 +7995,7 @@ uint16_t mode_particlevortex(void) if (SEGMENT.custom2 > 0) // automatic direction change enabled { - uint16_t changeinterval = (270 - SEGMENT.custom2); + uint16_t changeinterval = 15 + 255 / SEGMENT.custom2; direction = SEGMENT.aux1 & 0x02; //set direction according to flag if (SEGMENT.check3) // random interval @@ -8740,7 +8740,7 @@ uint16_t mode_particlebox(void) PartSys->applyForce(xgravity, ygravity); } - if (SEGMENT.call % (32-SEGMENT.custom3) == 0) + if (SEGMENT.call % (32 - SEGMENT.custom3) == 0) PartSys->applyFriction(2); PartSys->update(); // update and render @@ -8966,7 +8966,6 @@ uint16_t mode_particleattractor(void) if (SEGLEN == 1) return mode_static(); ParticleSystem *PartSys = NULL; - uint32_t i = 0; PSsettings sourcesettings; sourcesettings.asByte = 0b00001100; // PS settings for bounceY, bounceY used for source movement (it always bounces whereas particles do not) PSparticle *attractor; // particle pointer to the attractor @@ -9467,7 +9466,6 @@ if (SEGLEN == 1) PartSys->angleEmit(PartSys->sources[j], emitangle, emitspeed); j = (j + 1) % numSprays; } - PartSys->update(); // update and render return FRAMETIME; } @@ -9584,7 +9582,7 @@ uint16_t mode_particleblobs(void) PartSys->setBounceX(true); PartSys->setBounceY(true); PartSys->setWallHardness(255); - PartSys->setWallRoughness(255); + PartSys->setWallRoughness(255); PartSys->setCollisionHardness(255); //PartSys->setParticleSize(0); //set global size to zero or motion blur cannot be used (is zero by default) } @@ -9604,8 +9602,8 @@ uint16_t mode_particleblobs(void) { if(SEGMENT.aux0 != SEGMENT.speed || PartSys->particles[i].ttl == 0) // speed changed or dead { - PartSys->particles[i].vx = (int16_t)random(-(SEGMENT.speed >> 2), SEGMENT.speed >> 2); - PartSys->particles[i].vy = (int16_t)random(-(SEGMENT.speed >> 2), SEGMENT.speed >> 2); + PartSys->particles[i].vx = (int8_t)random(-(SEGMENT.speed >> 2), SEGMENT.speed >> 2); + PartSys->particles[i].vy = (int8_t)random(-(SEGMENT.speed >> 2), SEGMENT.speed >> 2); } if(SEGMENT.aux1 != SEGMENT.custom1 || PartSys->particles[i].ttl == 0) // size changed or dead PartSys->advPartSize[i].maxsize = 60 + (SEGMENT.custom1 >> 1) + random((SEGMENT.custom1 >> 2)); // set each particle to slightly randomized size @@ -9675,14 +9673,13 @@ uint16_t mode_particleDrip(void) return mode_static(); ParticleSystem1D *PartSys = NULL; //uint8_t numSprays; - const uint8_t hardness = 150; // collision hardness is fixed - if (SEGMENT.call == 0) // initialization { if (!initParticleSystem1D(PartSys, 4)) // init, no additional data needed return mode_static(); // allocation failed; //allocation failed PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting) - PartSys->sources[0].source.hue = random16(); + PartSys->sources[0].source.hue = random16(); + SEGMENT.aux0 = 1; //must not be zero or "% 0" happens below which crashes on ESP32 } else PartSys = reinterpret_cast(SEGMENT.data); // if not first call, just set the pointer to the PS @@ -9738,14 +9735,14 @@ uint16_t mode_particleDrip(void) // every nth frame emit a particle if (SEGMENT.call % SEGMENT.aux0 == 0) { - SEGMENT.aux0 = (256 - SEGMENT.intensity) + random(260 - SEGMENT.intensity); + int32_t interval = 256 / (SEGMENT.intensity + 1); + SEGMENT.aux0 = interval + random(interval + 5); PartSys->sources[0].source.hue = random16(); //set random color TODO: maybe also not random but color cycling? need another slider or checkmark for this. PartSys->sprayEmit(PartSys->sources[0]); } for (int i = 0; i < PartSys->usedParticles; i++)//check all particles { - if(PartSys->particles[i].ttl && PartSys->particles[i].collide == false) // use collision flag to identify splash particles { if(SEGMENT.custom1 > 0 && PartSys->particles[i].x < (PS_P_RADIUS_1D << 1)) //splash enabled and reached bottom -> does not work this way, all splashes will splash again... need to mark particles @@ -9770,7 +9767,6 @@ uint16_t mode_particleDrip(void) } PartSys->update(); // update and render - uint32_t bg_color = SEGCOLOR(1); //background color, set to black to overlay return FRAMETIME; } static const char _data_FX_MODE_PARTICLEDRIP[] PROGMEM = "PS DripDrop@Speed,!,Splash,Blur/Overlay,Gravity,Rain,PushSplash,Smooth;,!;!;1;pal=0,sx=150,ix=150,c1=220,c2=30,c3=21,o1=0,o2=0,o3=0"; @@ -9787,8 +9783,7 @@ uint16_t mode_particleBouncingBalls(void) { if (SEGLEN == 1) return mode_static(); - ParticleSystem1D *PartSys = NULL; - const uint8_t hardness = 150; // collision hardness is fixed + ParticleSystem1D *PartSys = NULL; if (SEGMENT.call == 0) // initialization { @@ -9881,9 +9876,7 @@ uint16_t mode_particleBouncingBalls(void) } } - PartSys->update(); // update and render - uint32_t bg_color = SEGCOLOR(1); //background color, set to black to overlay - + PartSys->update(); // update and render return FRAMETIME; } static const char _data_FX_MODE_PSBOUNCINGBALLS[] PROGMEM = "PS Bouncing Balls@Speed,!,Hardness,Blur/Overlay,Gravity,Collide,Smooth,Rolling;,!;!;1;pal=0,sx=100,c1=240,c2=0,c3=8,o1=0,o2=0,o3=1"; @@ -9903,8 +9896,6 @@ uint16_t mode_particleDancingShadows(void) if (SEGLEN == 1) return mode_static(); ParticleSystem1D *PartSys = NULL; - //uint8_t numSprays; - const uint8_t hardness = 150; // collision hardness is fixed if (SEGMENT.call == 0) // initialization { @@ -9924,25 +9915,21 @@ uint16_t mode_particleDancingShadows(void) // Particle System settings PartSys->updateSystem(); // update system properties (dimensions and data pointers) - if (SEGMENT.check2) - PartSys->setMotionBlur(255); //full motion blurring allows overlay (motion blur does not work with overlay) - else - PartSys->setMotionBlur(SEGMENT.custom1); + PartSys->setMotionBlur(SEGMENT.custom1); if (SEGMENT.check3) // collisions enabled PartSys->setParticleSize(1); else PartSys->setParticleSize(0); - //note: updating speed on the fly is not possible, since it is unknown which particles are assigned to which spot - //generate a spotlight: generates particles just outside of view - if (SEGMENT.call % (260 - (SEGMENT.intensity)) == 0) + //if (SEGMENT.call % ((255 + 64) / (1 + SEGMENT.intensity + (SEGMENT.speed >> 4))) == 0) + if (SEGMENT.call % (256 - SEGMENT.intensity) == 0) { //random color, random type uint32_t type = random8(SPOT_TYPES_COUNT); - int8_t speed = random(5 + (SEGMENT.speed >> 2)) + (SEGMENT.speed >> 3); + int8_t speed = 2 + random(2 + (SEGMENT.speed >> 2)) + (SEGMENT.speed >> 3); uint32_t width = random8(1, 10); - uint32_t ttl = 300; //ttl is particle brightness (if perpetual is set, it does not age, i.e. ttl stays at this value) + uint32_t ttl = 300; //ttl is particle brightness (below perpetual is set so it does not age, i.e. ttl stays at this value) int32_t position; //choose random start position, left and right from the segment if (random8(2)) { @@ -10002,15 +9989,22 @@ uint16_t mode_particleDancingShadows(void) if((int32_t)PartSys->particles[i].vx * PartSys->particles[i].x > 0) PartSys->particles[i].ttl = 0; //particle is moving away, kill it } PartSys->particles[i].perpetual = true; //particles do not age - if (SEGMENT.call % (32 - (SEGMENT.custom2 >> 3)) == 0) - PartSys->particles[i].hue += 2 + SEGMENT.custom2 >> 5; + if (SEGMENT.call % (32 / (1 + (SEGMENT.custom2 >> 3))) == 0) + PartSys->particles[i].hue += 2 + (SEGMENT.custom2 >> 5); + //note: updating speed on the fly is not accurately possible, since it is unknown which particles are assigned to which spot + if(SEGMENT.aux0 != SEGMENT.speed) //speed changed + { + //update all particle speed by setting them to current value + PartSys->particles[i].vx = PartSys->particles[i].vx > 0 ? SEGMENT.speed >> 3 : -SEGMENT.speed >> 3; + } } + SEGMENT.aux0 = SEGMENT.speed; PartSys->update(); // update and render return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEDANCINGSHADOWS[] PROGMEM = "PS Dancing Shadows@Speed,!,Blur,Color Cycle,,,Overlay,Smooth;,!;!;1;pal=0,sx=100,ix=180,c1=0,c2=0,o2=0,o3=0"; +static const char _data_FX_MODE_PARTICLEDANCINGSHADOWS[] PROGMEM = "PS Dancing Shadows@Speed,!,Blur/Overlay,Color Cycle,,,,Smooth;,!;!;1;pal=0,sx=100,ix=180,c1=0,c2=0,o2=0,o3=0"; /* @@ -10118,7 +10112,7 @@ uint16_t mode_particleFireworks1D(void) if(PartSys->sources[0].source.ttl == 0) //explode { PartSys->sources[0].source.perpetual = 1; // set standby state - PartSys->sources[0].var = 10 + SEGMENT.intensity >> 2; + PartSys->sources[0].var = 10 + (SEGMENT.intensity >> 2); PartSys->sources[0].v = 0; //TODO can make global if this never changes PartSys->sources[0].minLife = 60; PartSys->sources[0].maxLife = 150; @@ -10194,7 +10188,7 @@ uint16_t mode_particleSparkler(void) { PartSys->sources[i].source.hue = random16(); //TODO: make adjustable, maybe even colorcycle? PartSys->sources[i].var = SEGMENT.intensity >> 4 ; - PartSys->sources[i].minLife = 150 + SEGMENT.intensity >> 1; + PartSys->sources[i].minLife = 150 + (SEGMENT.intensity >> 1); PartSys->sources[i].maxLife = 200 + SEGMENT.intensity; uint32_t speed = SEGMENT.speed >> 1; PartSys->sources[i].source.vx = PartSys->sources[i].source.vx < 0 ? -speed : speed; //update speed, do not change direction @@ -10467,12 +10461,12 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_LARSON_SCANNER, &mode_larson_scanner, _data_FX_MODE_LARSON_SCANNER); addEffect(FX_MODE_COMET, &mode_comet, _data_FX_MODE_COMET); addEffect(FX_MODE_FIREWORKS, &mode_fireworks, _data_FX_MODE_FIREWORKS); - //addEffect(FX_MODE_RAIN, &mode_rain, _data_FX_MODE_RAIN); + addEffect(FX_MODE_RAIN, &mode_rain, _data_FX_MODE_RAIN); addEffect(FX_MODE_TETRIX, &mode_tetrix, _data_FX_MODE_TETRIX); addEffect(FX_MODE_FIRE_FLICKER, &mode_fire_flicker, _data_FX_MODE_FIRE_FLICKER); addEffect(FX_MODE_GRADIENT, &mode_gradient, _data_FX_MODE_GRADIENT); addEffect(FX_MODE_LOADING, &mode_loading, _data_FX_MODE_LOADING); - //addEffect(FX_MODE_ROLLINGBALLS, &rolling_balls, _data_FX_MODE_ROLLINGBALLS); + addEffect(FX_MODE_ROLLINGBALLS, &rolling_balls, _data_FX_MODE_ROLLINGBALLS); addEffect(FX_MODE_FAIRY, &mode_fairy, _data_FX_MODE_FAIRY); addEffect(FX_MODE_TWO_DOTS, &mode_two_dots, _data_FX_MODE_TWO_DOTS); @@ -10489,7 +10483,7 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_RANDOM_CHASE, &mode_random_chase, _data_FX_MODE_RANDOM_CHASE); addEffect(FX_MODE_OSCILLATE, &mode_oscillate, _data_FX_MODE_OSCILLATE); addEffect(FX_MODE_PRIDE_2015, &mode_pride_2015, _data_FX_MODE_PRIDE_2015); - //addEffect(FX_MODE_JUGGLE, &mode_juggle, _data_FX_MODE_JUGGLE); + addEffect(FX_MODE_JUGGLE, &mode_juggle, _data_FX_MODE_JUGGLE); addEffect(FX_MODE_PALETTE, &mode_palette, _data_FX_MODE_PALETTE); addEffect(FX_MODE_FIRE_2012, &mode_fire_2012, _data_FX_MODE_FIRE_2012); addEffect(FX_MODE_COLORWAVES, &mode_colorwaves, _data_FX_MODE_COLORWAVES); @@ -10516,11 +10510,11 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_CANDLE, &mode_candle, _data_FX_MODE_CANDLE); addEffect(FX_MODE_STARBURST, &mode_starburst, _data_FX_MODE_STARBURST); addEffect(FX_MODE_EXPLODING_FIREWORKS, &mode_exploding_fireworks, _data_FX_MODE_EXPLODING_FIREWORKS); - //addEffect(FX_MODE_BOUNCINGBALLS, &mode_bouncing_balls, _data_FX_MODE_BOUNCINGBALLS); + addEffect(FX_MODE_BOUNCINGBALLS, &mode_bouncing_balls, _data_FX_MODE_BOUNCINGBALLS); addEffect(FX_MODE_SINELON, &mode_sinelon, _data_FX_MODE_SINELON); addEffect(FX_MODE_SINELON_DUAL, &mode_sinelon_dual, _data_FX_MODE_SINELON_DUAL); addEffect(FX_MODE_SINELON_RAINBOW, &mode_sinelon_rainbow, _data_FX_MODE_SINELON_RAINBOW); - //addEffect(FX_MODE_POPCORN, &mode_popcorn, _data_FX_MODE_POPCORN); + addEffect(FX_MODE_POPCORN, &mode_popcorn, _data_FX_MODE_POPCORN); addEffect(FX_MODE_DRIP, &mode_drip, _data_FX_MODE_DRIP); addEffect(FX_MODE_PLASMA, &mode_plasma, _data_FX_MODE_PLASMA); addEffect(FX_MODE_PERCENT, &mode_percent, _data_FX_MODE_PERCENT); @@ -10537,7 +10531,7 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_PHASEDNOISE, &mode_phased_noise, _data_FX_MODE_PHASEDNOISE); addEffect(FX_MODE_FLOW, &mode_flow, _data_FX_MODE_FLOW); addEffect(FX_MODE_CHUNCHUN, &mode_chunchun, _data_FX_MODE_CHUNCHUN); - //addEffect(FX_MODE_DANCING_SHADOWS, &mode_dancing_shadows, _data_FX_MODE_DANCING_SHADOWS); + addEffect(FX_MODE_DANCING_SHADOWS, &mode_dancing_shadows, _data_FX_MODE_DANCING_SHADOWS); addEffect(FX_MODE_WASHING_MACHINE, &mode_washing_machine, _data_FX_MODE_WASHING_MACHINE); addEffect(FX_MODE_BLENDS, &mode_blends, _data_FX_MODE_BLENDS); diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index 8ca4b7f4f9..bad34a464a 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -392,7 +392,7 @@ void ParticleSystem::updateSize(PSadvancedParticle *advprops, PSsizeControl *adv // grow/shrink particle int32_t newsize = advprops->size; uint32_t counter = advsize->sizecounter; - uint32_t increment; + uint32_t increment = 0; // calculate grow speed using 0-8 for low speeds and 9-15 for higher speeds if (advsize->grow) increment = advsize->growspeed; else if (advsize->shrink) increment = advsize->shrinkspeed; @@ -476,13 +476,13 @@ void ParticleSystem::bounce(int8_t &incomingspeed, int8_t ¶llelspeed, int32_ position = maxposition - particleHardRadius; if (wallRoughness) { - int32_t totalspeed = abs(incomingspeed) + abs(parallelspeed); + int32_t incomingspeed_abs = abs((int32_t)incomingspeed); + int32_t totalspeed = incomingspeed_abs + abs((int32_t)parallelspeed); // transfer an amount of incomingspeed speed to parallel speed - int32_t donatespeed = abs(incomingspeed); - donatespeed = (random(-donatespeed, donatespeed) * wallRoughness) / 255; //take random portion of + or - perpendicular speed, scaled by roughness + int32_t donatespeed = (random(-incomingspeed_abs, incomingspeed_abs) * (int32_t)wallRoughness) / (int32_t)255; //take random portion of + or - perpendicular speed, scaled by roughness parallelspeed = limitSpeed((int32_t)parallelspeed + donatespeed); - incomingspeed = limitSpeed((int32_t)incomingspeed - donatespeed); - donatespeed = totalspeed - abs(parallelspeed); // keep total speed the same + //give the remainder of the speed to perpendicular speed + donatespeed = int8_t(totalspeed - abs(parallelspeed)); // keep total speed the same incomingspeed = incomingspeed > 0 ? donatespeed : -donatespeed; } } From f5a6f4faec564d44a8ae2a73e6677d19fc7e6a9d Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sun, 23 Jun 2024 10:59:42 +0200 Subject: [PATCH 097/219] minor FX adjustments --- wled00/FX.cpp | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index b465db1f9b..3962d296fe 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -8460,7 +8460,7 @@ uint16_t mode_particlepit(void) { // emit particle at random position over the top of the matrix (random16 is not random enough) PartSys->particles[i].ttl = 1500 - (SEGMENT.speed << 2) + random16(500); // if speed is higher, make them die sooner - PartSys->particles[i].x = random(PartSys->maxX >> 1) + (PartSys->maxX >> 2); + PartSys->particles[i].x = random(PartSys->maxX); //random(PartSys->maxX >> 1) + (PartSys->maxX >> 2); PartSys->particles[i].y = (PartSys->maxY<<1); // particles appear somewhere above the matrix, maximum is double the height PartSys->particles[i].vx = (int16_t)random(SEGMENT.speed >> 1) - (SEGMENT.speed >> 2); // side speed is +/- PartSys->particles[i].vy = map(SEGMENT.speed, 0, 255, -5, -100); // downward speed @@ -8487,8 +8487,6 @@ uint16_t mode_particlepit(void) if (SEGMENT.speed < 50) // for low speeds, apply more friction frictioncoefficient = 50 - SEGMENT.speed; - //if (SEGMENT.call % (3 + (SEGMENT.custom2 >> 2)) == 0) - //if (SEGMENT.call % (3 + (SEGMENT.speed >> 2)) == 0) if (SEGMENT.call % 6 == 0)// (3 + max(3, (SEGMENT.speed >> 2))) == 0) // note: if friction is too low, hard particles uncontrollably 'wander' left and right if wrapX is enabled PartSys->applyFriction(frictioncoefficient); @@ -9029,8 +9027,15 @@ uint16_t mode_particleattractor(void) PartSys->pointAttractor(i, attractor, strength, false); } } + else //no data, do classic attractor + { + for(uint32_t i = 0; i < displayparticles; i++) + { + PartSys->pointAttractor(i, attractor, SEGMENT.speed, SEGMENT.check3); + } + } #else - for(i = 0; i < displayparticles; i++) + for(uint32_t i = 0; i < displayparticles; i++) { PartSys->pointAttractor(i, attractor, SEGMENT.speed, SEGMENT.check3); } From 95a051c31692a775a0bfa8bcd59f28074a5b20dd Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Mon, 24 Jun 2024 08:19:16 +0200 Subject: [PATCH 098/219] Improved 1D collisions, added 2 new 1D FX (work in progress) --- wled00/FX.cpp | 271 ++++++++++++++++++++++++++++-------- wled00/FX.h | 4 +- wled00/FXparticleSystem.cpp | 130 +++++++++++------ wled00/FXparticleSystem.h | 6 +- 4 files changed, 313 insertions(+), 98 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 3962d296fe..462e1654ba 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -8492,46 +8492,9 @@ uint16_t mode_particlepit(void) PartSys->update(); // update and render -/* -//rotat image (just a test, non working yet) - float angle = PI/3; - // Calculate sine and cosine of the angle - float cosTheta = cos(angle); - float sinTheta = sin(angle); - - // Center of rotation - int centerX = cols / 2; - int centerY = rows / 2; - - // Iterate over each pixel in the output image - for (int y = 0; y < rows; y++) - { - for (int x = 0; x < cols; x++) - { - int relX = x - centerX; - int relY = y - centerY; - - // Apply rotation using axis symmetry - int origX = round(relX * cosTheta - relY * sinTheta) + centerX; - int origY = round(relX * sinTheta + relY * cosTheta) + centerY; - - // Check if original coordinates are within bounds - if (origX >= 0 && origX < rows && origY >= 0 && origY < cols) - { - // Copy pixel value from original image to rotated image - SEGMENT.setPixelColorXY(x, y, SEGMENT.getPixelColorXY(origX, origY)); - } - - // Copy pixel values from original image to rotated image - rotatedImage[origY][origX] = image[y][x]; - rotatedImage[origY][cols - 1 - origX] = image[y][cols - 1 - x]; - rotatedImage[rows - 1 - origY][origX] = image[rows - 1 - y][x]; - rotatedImage[rows - 1 - origY][cols - 1 - origX] = image[rows - 1 - y][cols - 1 - x]; - } - }*/ return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEPIT[] PROGMEM = "PS Ballpit@Speed,Intensity,Size,Hardness,Saturation,Cylinder,Walls,Ground;;!;2;pal=11,sx=100,ix=200,c1=120,c2=130,c3=31,o1=0,o2=0,o3=1"; +static const char _data_FX_MODE_PARTICLEPIT[] PROGMEM = "PS Ballpit@Speed,Intensity,Size,Hardness,Saturation,Cylinder,Walls,Ground;;!;2;pal=11,sx=100,ix=220,c1=120,c2=130,c3=31,o1=0,o2=0,o3=1"; /* * Particle Waterfall @@ -9627,9 +9590,48 @@ uint16_t mode_particleblobs(void) } } #endif + PartSys->setMotionBlur(((SEGMENT.custom3) << 3) + 7); PartSys->update(); // update and render + /* +//rotat image (just a test, non working yet) + float angle = PI/3; + // Calculate sine and cosine of the angle + float cosTheta = cos(angle); + float sinTheta = sin(angle); + + // Center of rotation + int centerX = cols / 2; + int centerY = rows / 2; + + // Iterate over each pixel in the output image + for (int y = 0; y < rows; y++) + { + for (int x = 0; x < cols; x++) + { + int relX = x - centerX; + int relY = y - centerY; + + // Apply rotation using axis symmetry + int origX = round(relX * cosTheta - relY * sinTheta) + centerX; + int origY = round(relX * sinTheta + relY * cosTheta) + centerY; + + // Check if original coordinates are within bounds + if (origX >= 0 && origX < rows && origY >= 0 && origY < cols) + { + // Copy pixel value from original image to rotated image + SEGMENT.setPixelColorXY(x, y, SEGMENT.getPixelColorXY(origX, origY)); + } + + // Copy pixel values from original image to rotated image + rotatedImage[origY][origX] = image[y][x]; + rotatedImage[origY][cols - 1 - origX] = image[y][cols - 1 - x]; + rotatedImage[rows - 1 - origY][origX] = image[rows - 1 - y][x]; + rotatedImage[rows - 1 - origY][cols - 1 - origX] = image[rows - 1 - y][cols - 1 - x]; + } + }*/ + return FRAMETIME; } static const char _data_FX_MODE_PARTICLEBLOBS[] PROGMEM = "PS Blobs@Speed,Blobs,Size,Life,Blur,Wobble,Collide,Pulsate;;!;2v;sx=30,ix=64,c1=200,c2=130,c3=0,o1=0,o2=0,o3=1"; @@ -10025,12 +10027,10 @@ uint16_t mode_particleFireworks1D(void) return mode_static(); // something went wrong, no data! } - forcecounter = PartSys->PSdataEnd; - //numRockets = PartSys->numSources; - // Particle System settings PartSys->updateSystem(); // update system properties (dimensions and data pointers) - + forcecounter = PartSys->PSdataEnd; + //numRockets = PartSys->numSources; PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur if(!SEGMENT.check1) //gravity enabled for sparks @@ -10150,15 +10150,13 @@ uint16_t mode_particleSparkler(void) DEBUG_PRINT(F("ERROR: FX PartSys nullpointer")); return mode_static(); // something went wrong, no data! } + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) sparklersettings.wrapX = SEGMENT.check2; sparklersettings.bounceX = !SEGMENT.check2; numSparklers = PartSys->numSources; - - // Particle System settings - PartSys->updateSystem(); // update system properties (dimensions and data pointers) - PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur @@ -10174,12 +10172,10 @@ uint16_t mode_particleSparkler(void) PartSys->sources[i].minLife = 150 + (SEGMENT.intensity >> 1); PartSys->sources[i].maxLife = 200 + SEGMENT.intensity; uint32_t speed = SEGMENT.speed >> 1; - PartSys->sources[i].source.vx = PartSys->sources[i].source.vx < 0 ? -speed : speed; //update speed, do not change direction - if(SEGMENT.aux0 != SEGMENT.check1) - { - PartSys->sources[i].source.vx = -PartSys->sources[i].source.vx; //invert direction - } - PartSys->sources[i].source.ttl = 400; //replenish its life (could make it perpetual) + if(SEGMENT.check1) //invert spray speed + speed = -speed; + PartSys->sources[i].source.vx = speed; //update speed, do not change direction + PartSys->sources[i].source.ttl = 400; //replenish its life (setting it perpetual uses more code) PartSys->sources[i].sat = SEGMENT.custom1; //color saturation PartSys->particleMoveUpdate(PartSys->sources[i].source, &sparklersettings); //move sparkler } @@ -10204,16 +10200,16 @@ uint16_t mode_particleSparkler(void) for(i = 0; i < numSparklers; i++) { - if(random16() % (1 + ((255 - SEGMENT.intensity) >> 3)) == 0) + if(random() % (1 + ((255 - SEGMENT.intensity) >> 3)) == 0) PartSys->sprayEmit(PartSys->sources[i]); //emit a particle } - SEGMENT.aux0 = SEGMENT.check1; + PartSys->update(); // update and render return FRAMETIME; } -static const char _data_FX_MODE_PS_SPARKLER[] PROGMEM = "PS Sparkler@Speed,!,Saturation,Blur/Overlay,Sparklers,Direction,Wrap/Bounce,Smooth;,!;!;1;pal=0,sx=50,ix=200,c1=0,c2=0,c3=0,o1=0,o2=1,o3=0"; +static const char _data_FX_MODE_PS_SPARKLER[] PROGMEM = "PS Sparkler@Speed,!,Saturation,Blur/Overlay,Sparklers,Direction,Wrap/Bounce,Smooth;,!;!;1;pal=0,sx=50,ix=200,c1=0,c2=0,c3=0,o1=1,o2=1,o3=0"; /* @@ -10367,6 +10363,169 @@ uint16_t mode_particleHourglass(void) static const char _data_FX_MODE_PS_HOURGLASS[] PROGMEM = "PS Hourglass@Speed,!,Color,Blur/Overlay,Gravity,Colorflip,Auto Reset,Fast Reset;,!;!;1;pal=34,sx=245,ix=200,c1=140,c2=80,c3=4,o1=1,o2=1,o3=1"; + +/* +Particle based Spray effect (like a volcano, possible replacement for popcorn) +Uses palette for particle color +by DedeHai (Damian Schneider) +*/ + +uint16_t mode_particle1Dspray(void) +{ + if (SEGLEN == 1) + return mode_static(); + ParticleSystem1D *PartSys = NULL; + uint32_t i; + + if (SEGMENT.call == 0) // initialization + { + if (!initParticleSystem1D(PartSys, 1)) // init + return mode_static(); // allocation failed + PartSys->setKillOutOfBounds(true); + PartSys->setWallHardness(150); + PartSys->setParticleSize(1); + } + else + PartSys = reinterpret_cast(SEGMENT.data); // if not first call, just set the pointer to the PS + + if (PartSys == NULL) + { + DEBUG_PRINT(F("ERROR: FX PartSys nullpointer")); + return mode_static(); // something went wrong, no data! + } + + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setBounce(SEGMENT.check2); + PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur + int32_t gravity = (int32_t)SEGMENT.custom3 - 15; //gravity setting, 0-14 is negative, 16 - 31 is positive + PartSys->setGravity(abs(gravity)); // use reversgrav setting to invert gravity (for proper 'floor' and out of bounce handling) + + PartSys->sources[i].source.hue = random16(); //TODO: add colormodes like in hourglass? + PartSys->sources[0].var = 20; + PartSys->sources[0].minLife = 200;//PartSys->maxXpixel; + PartSys->sources[0].maxLife = 400;//PartSys->maxXpixel << 1; + PartSys->sources[0].source.x = map(SEGMENT.custom1, 0 , 255, 0, PartSys->maxX); // spray position + PartSys->sources[0].v = map(SEGMENT.speed, 0 , 255, -127 + PartSys->sources[0].var, 127 - PartSys->sources[0].var); // particle emit speed + PartSys->sources[0].source.reversegrav = false; + if(gravity < 0) + PartSys->sources[0].source.reversegrav = true; + + //if(SEGMENT.call % (1 + ((255 - SEGMENT.intensity) >> 2)) == 0) + // PartSys->sprayEmit(PartSys->sources[0]); //emit a particle + + if(random() % (1 + ((255 - SEGMENT.intensity) >> 3)) == 0) + PartSys->sprayEmit(PartSys->sources[i]); //emit a particle + + //update color settings + PartSys->setColorByAge(SEGMENT.check1); //overrules the color by position + + for(i = 0; i < PartSys->usedParticles; i++) + { + if(SEGMENT.check3) //color by position + PartSys->particles[i].hue = (255 * (uint32_t)PartSys->particles[i].x) / PartSys->maxX; //color fixed by position + PartSys->particles[i].reversegrav = PartSys->sources[0].source.reversegrav; //update gravity direction + } + PartSys->update(); // update and render + + return FRAMETIME; +} +static const char _data_FX_MODE_PS_1DSPRAY[] PROGMEM = "PS 1D Spray@!,!,Position,Blur/Overlay,Gravity,Color by Age,Bounce,Color by Position;,!;!;1;pal=35,sx=200,ix=220,c1=4,c2=0,c3=28,o1=1,o2=1,o3=0"; + + +/* +Particle based gravity balance (1D pendent to 2D particle box) +Uses palette for particle color +by DedeHai (Damian Schneider) +*/ + +uint16_t mode_particleBalance(void) +{ + if (SEGLEN == 1) + return mode_static(); + ParticleSystem1D *PartSys = NULL; + uint32_t i; + + if (SEGMENT.call == 0) // initialization + { + if (!initParticleSystem1D(PartSys, 1)) // init, no additional data needed + return mode_static(); // allocation failed + //PartSys->setKillOutOfBounds(true); + PartSys->setParticleSize(1); + for(i = 0; i < PartSys->numParticles; i++) + { + PartSys->particles[i].x = i * PS_P_RADIUS; + PartSys->particles[i].hue = (i * 1024) / PartSys->usedParticles; // multi gradient distribution + PartSys->particles[i].ttl = 300; + PartSys->particles[i].perpetual = true; + PartSys->particles[i].collide = true; + } + } + else + PartSys = reinterpret_cast(SEGMENT.data); // if not first call, just set the pointer to the PS + + if (PartSys == NULL) + { + DEBUG_PRINT(F("ERROR: FX PartSys nullpointer")); + return mode_static(); // something went wrong, no data! + } + //TODO: wenn collisions ausgeschaltet ist, müssen die partikel sterben und neu eingefügt werden, sonst klumpt das komplett, oder: random friction wäre auch eine möglichkeit + + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur + PartSys->setBounce(!SEGMENT.check2); + PartSys->setWrap(SEGMENT.check2); + uint8_t hardness = map(SEGMENT.custom1, 0, 255, 50, 250); + PartSys->setWallHardness(hardness); + PartSys->enableParticleCollisions(SEGMENT.custom1, hardness); // enable collisions if custom1 > 0 + PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 10, PartSys->numParticles)); + + if (SEGMENT.call % (((255 - SEGMENT.speed) >> 6) + 1) == 0) // how often the force is applied depends on speed setting + { + int32_t xgravity; + int32_t increment = (SEGMENT.speed >> 6) + 1; + + SEGMENT.aux0 += increment; + + if(SEGMENT.check3) // random, use perlin noise + xgravity = ((int16_t)inoise8(SEGMENT.aux0) - 127); + else // sinusoidal + xgravity = (int16_t)cos8(SEGMENT.aux0) - 127;//((int32_t)(SEGMENT.custom3 << 2) * cos8(SEGMENT.aux0) + + // scale the force + xgravity = (xgravity * SEGMENT.custom3 << 2) / 128; + + PartSys->applyForce(xgravity); + } + // if(SEGMENT.check2) //collisions enabled + // { + // if (SEGMENT.call % 3 == 0) + // PartSys->applyFriction(2); //apply some friction +// } + // else //no collisions, + { + uint32_t randomindex = random(PartSys->usedParticles); + PartSys->particles[randomindex].vx = ((int32_t)PartSys->particles[randomindex].vx * 200) / 255; // apply friction to random particle to reduce clumping (without collisions) + } + +//update colors + for(i = 0; i < PartSys->usedParticles; i++) + { + if(SEGMENT.check1) //color by position + PartSys->particles[i].hue = (255 * (uint32_t)PartSys->particles[i].x) / PartSys->maxX; //color fixed by position + else + PartSys->particles[i].hue = (255 * i) / PartSys->usedParticles; //color by particle index + } + + PartSys->update(); // update and render + return FRAMETIME; +} +static const char _data_FX_MODE_PS_BALANCE[] PROGMEM = "PS 1D Balance@!,!,Collisions,Blur/Overlay,Tilt,Color by Position,Wrap/Bounce,Random;,!;!;1;pal=35,sx=200,ix=220,c1=4,c2=0,c3=28,o1=1,o2=1,o3=0"; + + + + #endif //WLED_DISABLE_PARTICLESYSTEM1D ////////////////////////////////////////////////////////////////////////////////////////// @@ -10634,6 +10793,8 @@ addEffect(FX_MODE_PSDANCINGSHADOWS, &mode_particleDancingShadows, _data_FX_MODE_ addEffect(FX_MODE_PSFIREWORKS1D, &mode_particleFireworks1D, _data_FX_MODE_PS_FIREWORKS1D); addEffect(FX_MODE_PSSPARKLER, &mode_particleSparkler, _data_FX_MODE_PS_SPARKLER); addEffect(FX_MODE_PSHOURGLASS, &mode_particleHourglass, _data_FX_MODE_PS_HOURGLASS); +addEffect(FX_MODE_PS1DSPRAY, &mode_particle1Dspray, _data_FX_MODE_PS_1DSPRAY); +addEffect(FX_MODE_PSBALANCE, &mode_particleBalance, _data_FX_MODE_PS_BALANCE); #endif // WLED_DISABLE_PARTICLESYSTEM1D diff --git a/wled00/FX.h b/wled00/FX.h index 6a9a835711..6e5ad678fb 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -338,7 +338,9 @@ #define FX_MODE_PSFIREWORKS1D 205 #define FX_MODE_PSSPARKLER 206 #define FX_MODE_PSHOURGLASS 207 -#define MODE_COUNT 208 +#define FX_MODE_PS1DSPRAY 208 +#define FX_MODE_PSBALANCE 209 +#define MODE_COUNT 210 typedef enum mapping1D2D { M12_Pixels = 0, diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index bad34a464a..cafb61e9b5 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -199,7 +199,7 @@ void ParticleSystem::setParticleSize(uint8_t size) particleHardRadius = PS_P_MINHARDRADIUS + particlesize; // note: this sets size if not using advanced props motionBlur = 0; // disable motion blur if particle size is set } -// enable/disable gravity, optionally, set the force (force=8 is default) can be 1-255, 0 is disable +// enable/disable gravity, optionally, set the force (force=8 is default) can be -127 to +127, 0 is disable // if enabled, gravity is applied to all particles in ParticleSystemUpdate() // 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::setGravity(int8_t force) @@ -1189,7 +1189,7 @@ void ParticleSystem::collideParticles(PSparticle *particle1, PSparticle *particl particle2->vx -= ximpulse; particle2->vy -= yimpulse; - if (collisionHardness < surfacehardness) // if particles are soft, they become 'sticky' i.e. apply some friction (they do pile more nicely) + if (collisionHardness < surfacehardness) // if particles are soft, they become 'sticky' i.e. apply some friction (they do pile more nicely and stop sloshing around) { const uint32_t coeff = collisionHardness + (255 - PS_P_MINSURFACEHARDNESS); particle1->vx = ((int32_t)particle1->vx * coeff) / 255; @@ -1197,7 +1197,7 @@ void ParticleSystem::collideParticles(PSparticle *particle1, PSparticle *particl particle2->vx = ((int32_t)particle2->vx * coeff) / 255; particle2->vy = ((int32_t)particle2->vy * coeff) / 255; - +/* if (collisionHardness < 10) // if they are very soft, stop slow particles completely to make them stick to each other { particle1->vx = (particle1->vx < 3 && particle1->vx > -3) ? 0 : particle1->vx; @@ -1205,7 +1205,7 @@ void ParticleSystem::collideParticles(PSparticle *particle1, PSparticle *particl particle2->vx = (particle2->vx < 3 && particle2->vx > -3) ? 0 : particle2->vx; particle2->vy = (particle2->vy < 3 && particle2->vy > -3) ? 0 : particle2->vy; - } + }*/ } // particles have volume, push particles apart if they are too close @@ -1241,6 +1241,16 @@ void ParticleSystem::collideParticles(PSparticle *particle1, PSparticle *particl } particle1->vy += push; // note: pushing may push particles out of frame, if bounce is active, it will move it back as position will be limited to within frame, if bounce is disabled: bye bye + if (collisionHardness < 16) // if they are very soft, stop slow particles completely to make them stick to each other + { + particle1->vx = 0; + particle1->vy = 0; + particle2->vx = 0; + particle2->vy = 0; + //push them apart + particle1->x += push; + particle1->y += push; + } } } } @@ -1495,15 +1505,15 @@ ParticleSystem1D::ParticleSystem1D(uint16_t length, uint16_t numberofparticles, // update function applies gravity, moves the particles, handles collisions and renders the particles void ParticleSystem1D::update(void) { - //PSadvancedParticle *advprop = NULL; - //apply gravity globally if enabled - if (particlesettings.useGravity) - applyGravity(); - + // handle collisions (can push particles, must be done before updating particles or they can render out of bounds, causing a crash if using local buffer for speed) if (particlesettings.useCollisions) handleCollisions(); + //apply gravity globally if enabled + if (particlesettings.useGravity) //note: in 1D system, applying gravity after collisions also works TODO: which one is really better for stacking / oscillations? + applyGravity(); + //move all particles for (int i = 0; i < usedParticles; i++) { @@ -1584,7 +1594,7 @@ void ParticleSystem1D::setParticleSize(uint8_t size) else particleHardRadius = PS_P_MINHARDRADIUS_1D >> 1; //1 pixel sized particles have half the radius (for collisions & bounce) } -// enable/disable gravity, optionally, set the force (force=8 is default) can be 1-255, 0 is disable +// enable/disable gravity, optionally, set the force (force=8 is default) can be -127 to +127, 0 is disable // if enabled, gravity is applied to all particles in ParticleSystemUpdate() // 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 ParticleSystem1D::setGravity(int8_t force) @@ -1641,7 +1651,7 @@ void ParticleSystem1D::particleMoveUpdate(PSparticle1D &part, PSsettings *option if (!part.perpetual) part.ttl--; // age if (particlesettings.colorByAge) - part.hue = part.ttl > 255 ? 255 : part.ttl; //set color to ttl + part.hue = part.ttl > 250 ? 250 : part.ttl; //set color to ttl //bool usesize = false; // particle uses individual size rendering int32_t newX = part.x + (int16_t)part.vx; @@ -1689,11 +1699,23 @@ void ParticleSystem1D::particleMoveUpdate(PSparticle1D &part, PSsettings *option { part.outofbounds = 1; if (options->killoutofbounds) - { - // if (newX < 0) // if gravity is enabled, only kill particles below ground - // part.ttl = 0; - // else if (!options->useGravity) - part.ttl = 0; + { + bool killthis = true; + if(options->useGravity) //if gravity is used, only kill below 'floor level' + { + if(part.reversegrav) //skip at x = 0 + { + if(newX < 0) + killthis = false; + } + else //skip at x = max + { + if(newX > 0) + killthis = false; + } + } + if(killthis) + part.ttl = 0; } } } @@ -1715,6 +1737,21 @@ void ParticleSystem1D::applyForce(PSparticle1D *part, int8_t xforce, uint8_t *co part->vx = limitSpeed((int32_t)part->vx + dv); } +// apply a force to all particles +// force is in 3.4 fixed point notation (see above) +void ParticleSystem1D::applyForce(int8_t xforce) +{ + // for small forces, need to use a delay counter + uint8_t tempcounter; + // note: this is not the most compuatationally efficient way to do this, but it saves on duplacte code and is fast enough + for (uint i = 0; i < usedParticles; i++) + { + tempcounter = forcecounter; + applyForce(&particles[i], xforce, &tempcounter); + } + forcecounter = tempcounter; //save value back +} + // apply gravity to all particles using PS global gforce setting // gforce is in 3.4 fixed point notation, see note above void ParticleSystem1D::applyGravity() @@ -1986,9 +2023,12 @@ void ParticleSystem1D::collideParticles(PSparticle1D *particle1, PSparticle1D *p //int32_t impulse = (((((relativeVx) << 15)) * surfacehardness) >> 23); //note: this calculation is biased slightly towards negative numbers due to bit shifts, which is not relevant in 1D system //int32_t impulse = (((((relativeVx) << 15)) * surfacehardness) / 8388607); int32_t impulse = relativeVx * surfacehardness / 255; + //int32_t impulse = relativeVx; //note: applying full impulse and hardness afterwards results in mid-movement stopping, so no good! particle1->vx += impulse; particle2->vx -= impulse; + //particle1->vx = particle1->vx * surfacehardness / 255; //apply hardness + //particle2->vx = particle2->vx * surfacehardness / 255; //if one of the particles is fixed, transfer the impulse back so it bounces if(particle1->fixed) @@ -2001,9 +2041,9 @@ void ParticleSystem1D::collideParticles(PSparticle1D *particle1, PSparticle1D *p // particle1->vx = (particle1->vx < 5 && particle1->vx > -5) ? 0 : particle1->vx; // particle2->vx = (particle2->vx < 5 && particle2->vx > -5) ? 0 : particle2->vx; - if (collisionHardness < surfacehardness) // if particles are soft, they become 'sticky' i.e. apply some friction (they do pile more nicely and correctly) + if (collisionHardness < PS_P_MINSURFACEHARDNESS_1D) // if particles are soft, they become 'sticky' i.e. apply some friction (they do pile more nicely and correctly) { - const uint32_t coeff = collisionHardness + 100;//(255 - PS_P_MINSURFACEHARDNESS_1D); + const uint32_t coeff = collisionHardness + (255 - PS_P_MINSURFACEHARDNESS_1D); particle1->vx = ((int32_t)particle1->vx * coeff) / 255; particle2->vx = ((int32_t)particle2->vx * coeff) / 255; } @@ -2030,32 +2070,44 @@ void ParticleSystem1D::collideParticles(PSparticle1D *particle1, PSparticle1D *p { int32_t pushamount = 1 + ((collisiondistance - distance) >> 1); //add half the remaining distance note: this works best, if less or more is added, it gets more chaotic //int32_t pushamount = collisiondistance - distance; - if (dx < 0) // particle2.x < particle1.x - { - if (particle2->reversegrav && !particle2->fixed) - { - particle2->x -= pushamount; - particle2->vx--; - } - else if (!particle1->reversegrav && !particle1->fixed) - { - particle1->x += pushamount; - particle1->vx++; - } - } - else + if(particlesettings.useGravity) //using gravity, push the 'upper' particle only { - if(particle1->reversegrav && !particle1->fixed) + if (dx < 0) // particle2.x < particle1.x { - particle1->x -= pushamount; - particle1->vx--; + if (particle2->reversegrav && !particle2->fixed) + { + particle2->x -= pushamount; + particle2->vx--; + } + else if (!particle1->reversegrav && !particle1->fixed) + { + particle1->x += pushamount; + particle1->vx++; + } } - else if (!particle2->reversegrav && !particle2->fixed) + else { - particle2->x += pushamount; - particle2->vx++; + if(particle1->reversegrav && !particle1->fixed) + { + particle1->x -= pushamount; + particle1->vx--; + } + else if (!particle2->reversegrav && !particle2->fixed) + { + particle2->x += pushamount; + particle2->vx++; + } } - } + } + else //not using gravity, push both particles by applying a velocity (like in 2D system), results in much nicer stacking + { + pushamount = 1 + (pushamount >> 2); + if (dx < 0) // particle2.x < particle1.x + pushamount = -pushamount; + + particle1->vx -= pushamount; + particle2->vx += pushamount; + } } } diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index 68f0c87d4b..c9cfe8407a 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -213,7 +213,7 @@ class ParticleSystem uint8_t gforcecounter; // counter for global gravity int8_t gforce; // gravity strength, default is 8 (negative is allowed, positive is downwards) uint32_t collisioncounter; // counter to handle collisions TODO: could use the SEGMENT.call? - uint32_t forcecounter; // counter for globally applied forces + uint8_t forcecounter; // counter for globally applied forces // global particle properties for basic particles uint8_t particlesize; // global particle size, 0 = 2 pixels, 255 = 10 pixels (note: this is also added to individual sized particles) int32_t particleHardRadius; // hard surface radius of a particle, used for collision detection @@ -297,12 +297,12 @@ class ParticleSystem1D void particleMoveUpdate(PSparticle1D &part, PSsettings *options = NULL); // move function //particle physics void applyForce(PSparticle1D *part, int8_t xforce, uint8_t *counter); //apply a force to a single particle + void applyForce(int8_t xforce); // apply a force to all particles void applyGravity(PSparticle1D *part); // applies gravity to single particle (use this for sources) void applyFriction(int32_t coefficient); // apply friction to all used particles // set options - void setUsedParticles(uint16_t num); - void setCollisionHardness(uint8_t hardness); // hardness for particle collisions (255 means full hard) + void setUsedParticles(uint16_t num); void setWallHardness(uint8_t hardness); // hardness for bouncing on the wall if bounceXY is set void setSize(uint16_t x); //set particle system size (= strip length) void setWrap(bool enable); From 386131dcc0e60200431b1731a6a5a8c6150534f3 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Tue, 25 Jun 2024 19:44:47 +0200 Subject: [PATCH 099/219] bugfixes --- wled00/FX.cpp | 19 ++++++++----------- wled00/FXparticleSystem.cpp | 2 +- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 462e1654ba..41766ca100 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -10200,10 +10200,9 @@ uint16_t mode_particleSparkler(void) for(i = 0; i < numSparklers; i++) { - if(random() % (1 + ((255 - SEGMENT.intensity) >> 3)) == 0) + if(random(255) % (1 + ((255 - SEGMENT.intensity) >> 3)) == 0) PartSys->sprayEmit(PartSys->sources[i]); //emit a particle } - PartSys->update(); // update and render @@ -10414,7 +10413,7 @@ uint16_t mode_particle1Dspray(void) //if(SEGMENT.call % (1 + ((255 - SEGMENT.intensity) >> 2)) == 0) // PartSys->sprayEmit(PartSys->sources[0]); //emit a particle - if(random() % (1 + ((255 - SEGMENT.intensity) >> 3)) == 0) + if(random(255) % (1 + ((255 - SEGMENT.intensity) >> 3)) == 0) PartSys->sprayEmit(PartSys->sources[i]); //emit a particle //update color settings @@ -10477,25 +10476,23 @@ uint16_t mode_particleBalance(void) PartSys->setBounce(!SEGMENT.check2); PartSys->setWrap(SEGMENT.check2); uint8_t hardness = map(SEGMENT.custom1, 0, 255, 50, 250); - PartSys->setWallHardness(hardness); PartSys->enableParticleCollisions(SEGMENT.custom1, hardness); // enable collisions if custom1 > 0 + if(SEGMENT.custom1 == 0) //collisions disabled, make the walls hard + hardness = 200; + PartSys->setWallHardness(hardness); PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 10, PartSys->numParticles)); if (SEGMENT.call % (((255 - SEGMENT.speed) >> 6) + 1) == 0) // how often the force is applied depends on speed setting { int32_t xgravity; int32_t increment = (SEGMENT.speed >> 6) + 1; - SEGMENT.aux0 += increment; - if(SEGMENT.check3) // random, use perlin noise - xgravity = ((int16_t)inoise8(SEGMENT.aux0) - 127); + xgravity = ((int16_t)inoise8(SEGMENT.aux0) - 128); else // sinusoidal - xgravity = (int16_t)cos8(SEGMENT.aux0) - 127;//((int32_t)(SEGMENT.custom3 << 2) * cos8(SEGMENT.aux0) - + xgravity = (int16_t)cos8(SEGMENT.aux0) - 128;//((int32_t)(SEGMENT.custom3 << 2) * cos8(SEGMENT.aux0) // scale the force - xgravity = (xgravity * SEGMENT.custom3 << 2) / 128; - + xgravity = (xgravity * ((SEGMENT.custom3+1) << 2)) / 128; PartSys->applyForce(xgravity); } // if(SEGMENT.check2) //collisions enabled diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index cafb61e9b5..aa83bb8085 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -2269,7 +2269,7 @@ int32_t calcForce_dv(int8_t force, uint8_t* counter) } else { - dv = force >> 4; // MSBs + dv = force / 16; // MSBs note: cannot use bitshift as dv can be negative } return dv; } From 2c3553ac26916f773f36fa0943548704ca930f24 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Tue, 25 Jun 2024 21:44:59 +0200 Subject: [PATCH 100/219] added 'color by position' as a PS setting -made settings struct for 1D and 2D version to be different -added setting 'color by position' as that is used by multiple FX now --- wled00/FX.cpp | 37 +++++++++++------------ wled00/FXparticleSystem.cpp | 25 +++++++++------- wled00/FXparticleSystem.h | 59 ++++++++++++++++++++++++------------- 3 files changed, 71 insertions(+), 50 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 41766ca100..441a812d98 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -8219,7 +8219,7 @@ uint16_t mode_particlevolcano(void) if (SEGLEN == 1) return mode_static(); ParticleSystem *PartSys = NULL; - PSsettings volcanosettings; + PSsettings2D volcanosettings; volcanosettings.asByte = 0b00000100; // PS settings for volcano movement: bounceX is enabled uint8_t numSprays; // note: so far only one tested but more is possible uint32_t i = 0; @@ -8771,7 +8771,7 @@ uint16_t mode_particleimpact(void) ParticleSystem *PartSys = NULL; uint32_t i = 0; uint8_t MaxNumMeteors; - PSsettings meteorsettings; + PSsettings2D meteorsettings; meteorsettings.asByte = 0b00101000; // PS settings for meteors: bounceY and gravity enabled if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. @@ -8905,7 +8905,7 @@ uint16_t mode_particleattractor(void) if (SEGLEN == 1) return mode_static(); ParticleSystem *PartSys = NULL; - PSsettings sourcesettings; + PSsettings2D sourcesettings; sourcesettings.asByte = 0b00001100; // PS settings for bounceY, bounceY used for source movement (it always bounces whereas particles do not) PSparticle *attractor; // particle pointer to the attractor if (SEGMENT.call == 0) // initialization @@ -9426,7 +9426,7 @@ uint16_t mode_particleghostrider(void) if (SEGLEN == 1) return mode_static(); ParticleSystem *PartSys = NULL; - PSsettings ghostsettings; + PSsettings2D ghostsettings; ghostsettings.asByte = 0b0000011; //enable wrapX and wrapY if (SEGMENT.call == 0) // initialization @@ -10134,7 +10134,7 @@ uint16_t mode_particleSparkler(void) ParticleSystem1D *PartSys = NULL; uint8_t numSparklers; uint32_t i; - PSsettings sparklersettings; + PSsettings1D sparklersettings; sparklersettings.asByte = 0; // PS settings for sparkler (set below) if (SEGMENT.call == 0) // initialization @@ -10290,9 +10290,10 @@ uint16_t mode_particleHourglass(void) PartSys->particles[i].fixed = true; } if(colormode == 7) - PartSys->particles[i].hue = (255 * (uint32_t)PartSys->particles[i].x) / PartSys->maxX; //color fixed by position + PartSys->setColorByPosition(true); //color fixed by position else - { + { + PartSys->setColorByPosition(false); switch(colormode) { case 0: PartSys->particles[i].hue = 120; break; //fixed at 120, if flip is activated, this can make red and green (use palette 34) case 1: PartSys->particles[i].hue = *basehue; break; //fixed random color @@ -10417,12 +10418,10 @@ uint16_t mode_particle1Dspray(void) PartSys->sprayEmit(PartSys->sources[i]); //emit a particle //update color settings - PartSys->setColorByAge(SEGMENT.check1); //overrules the color by position - + PartSys->setColorByAge(SEGMENT.check1); //overruled by 'color by position' + PartSys->setColorByPosition(SEGMENT.check3); for(i = 0; i < PartSys->usedParticles; i++) - { - if(SEGMENT.check3) //color by position - PartSys->particles[i].hue = (255 * (uint32_t)PartSys->particles[i].x) / PartSys->maxX; //color fixed by position + { PartSys->particles[i].reversegrav = PartSys->sources[0].source.reversegrav; //update gravity direction } PartSys->update(); // update and render @@ -10507,14 +10506,14 @@ uint16_t mode_particleBalance(void) } //update colors - for(i = 0; i < PartSys->usedParticles; i++) - { - if(SEGMENT.check1) //color by position - PartSys->particles[i].hue = (255 * (uint32_t)PartSys->particles[i].x) / PartSys->maxX; //color fixed by position - else - PartSys->particles[i].hue = (255 * i) / PartSys->usedParticles; //color by particle index + PartSys->setColorByPosition(SEGMENT.check1); + if(!SEGMENT.check1) + { + for(i = 0; i < PartSys->usedParticles; i++) + { + PartSys->particles[i].hue = (255 * i) / PartSys->usedParticles; //color by particle index + } } - PartSys->update(); // update and render return FRAMETIME; } diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index aa83bb8085..a0ee16df12 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -287,7 +287,7 @@ void ParticleSystem::angleEmit(PSsource &emitter, uint16_t angle, int8_t speed, // particle moves, decays and dies, if killoutofbounds is set, out of bounds particles are set to ttl=0 // uses passed settings to set bounce or wrap, if useGravity is set, it will never bounce at the top and killoutofbounds is not applied over the top -void ParticleSystem::particleMoveUpdate(PSparticle &part, PSsettings *options, PSadvancedParticle *advancedproperties) +void ParticleSystem::particleMoveUpdate(PSparticle &part, PSsettings2D *options, PSadvancedParticle *advancedproperties) { if (options == NULL) options = &particlesettings; //use PS system settings by default @@ -1520,16 +1520,14 @@ void ParticleSystem1D::update(void) particleMoveUpdate(particles[i], &particlesettings); } - /*TODO remove this - Serial.print("alive particles: "); - uint32_t aliveparticles = 0; - for (int i = 0; i < numParticles; i++) + if(particlesettings.colorByPosition) { - if (particles[i].ttl) - aliveparticles++; + for (int i = 0; i < usedParticles; i++) + { + particles[i].hue = (255 * (uint32_t)particles[i].x) / maxX; + } } - Serial.println(aliveparticles); - */ + ParticleSys_render(); uint32_t bg_color = SEGCOLOR(1); //background color, set to black to overlay @@ -1579,6 +1577,11 @@ void ParticleSystem1D::setColorByAge(bool enable) particlesettings.colorByAge = enable; } +void ParticleSystem1D::setColorByPosition(bool enable) +{ + particlesettings.colorByPosition = enable; +} + void ParticleSystem1D::setMotionBlur(uint8_t bluramount) { if (particlesize < 2) // only allwo motion blurring on default particle size or advanced size(cannot combine motion blur with normal blurring used for particlesize, would require another buffer) @@ -1642,7 +1645,7 @@ int32_t ParticleSystem1D::sprayEmit(PSsource1D &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 and killoutofbounds is not applied over the top -void ParticleSystem1D::particleMoveUpdate(PSparticle1D &part, PSsettings *options) +void ParticleSystem1D::particleMoveUpdate(PSparticle1D &part, PSsettings1D *options) { if (options == NULL) options = &particlesettings; //use PS system settings by default @@ -1803,7 +1806,7 @@ void ParticleSystem1D::ParticleSys_render() //CRGB **renderbuffer = NULL; //local particle render buffer for advanced particles uint32_t i; uint32_t brightness; // particle brightness, fades if dying - + if (useLocalBuffer) { /* diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index c9cfe8407a..82bef96899 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -38,22 +38,6 @@ int32_t limitSpeed(int32_t speed); //TODO: same as 2D function, could share void fast_color_add(CRGB &c1, CRGB &c2, uint32_t scale = 255); // fast and accurate color adding with scaling (scales c2 before adding) void fast_color_scale(CRGB &c, uint32_t scale); // fast scaling function using 32bit variable and pointer. note: keep 'scale' within 0-255 -// struct for PS settings (shared for 1D and 2D class) -typedef union -{ - struct{ - // one byte bit field: - bool wrapX : 1; - bool wrapY : 1; - bool bounceX : 1; - bool bounceY : 1; - bool killoutofbounds : 1; // if set, out of bound particles are killed immediately - bool useGravity : 1; // set to 1 if gravity is used, disables bounceY at the top - bool useCollisions : 1; - bool colorByAge : 1; // if set, particle hue is set by ttl value in render function - }; - byte asByte; // order is: LSB is first entry in the list above -} PSsettings; #endif #ifndef WLED_DISABLE_PARTICLESYSTEM2D @@ -73,6 +57,23 @@ typedef union #define PS_P_MINHARDRADIUS 70 // minimum hard surface radius #define PS_P_MINSURFACEHARDNESS 128 // minimum hardness used in collision impulse calculation, below this hardness, particles become sticky +// struct for PS settings (shared for 1D and 2D class) +typedef union +{ + struct{ + // one byte bit field for 2D settings + bool wrapX : 1; + bool wrapY : 1; + bool bounceX : 1; + bool bounceY : 1; + bool killoutofbounds : 1; // if set, out of bound particles are killed immediately + bool useGravity : 1; // set to 1 if gravity is used, disables bounceY at the top + bool useCollisions : 1; + bool colorByAge : 1; // if set, particle hue is set by ttl value in render function + }; + byte asByte; // access as a byte, order is: LSB is first entry in the list above +} PSsettings2D; + //struct for a single particle (10 bytes) typedef struct { int16_t x; // x position in particle system @@ -135,7 +136,7 @@ class ParticleSystem void update(void); //update the particles according to set options and render to the matrix void updateFire(uint32_t intensity, bool renderonly = false); // update function for fire, if renderonly is set, particles are not updated (required to fix transitions with frameskips) void updateSystem(void); // call at the beginning of every FX, updates pointers and dimensions - void particleMoveUpdate(PSparticle &part, PSsettings *options = NULL, PSadvancedParticle *advancedproperties = NULL); // move function + void particleMoveUpdate(PSparticle &part, PSsettings2D *options = NULL, PSadvancedParticle *advancedproperties = NULL); // move function // particle emitters int32_t sprayEmit(PSsource &emitter, uint32_t amount = 1); @@ -205,7 +206,7 @@ class ParticleSystem CRGB **allocate2Dbuffer(uint32_t cols, uint32_t rows); // note: variables that are accessed often are 32bit for speed - PSsettings particlesettings; // settings used when updating particles (can also used by FX to move sources), do not edit properties directly, use functions above + PSsettings2D particlesettings; // settings used when updating particles (can also used by FX to move sources), do not edit properties directly, use functions above uint32_t emitIndex; // index to count through particles to emit so searching for dead pixels is faster int32_t collisionHardness; uint32_t wallHardness; @@ -250,6 +251,23 @@ bool allocateParticleSystemMemory2D(uint16_t numparticles, uint16_t numsources, #define PS_P_MINHARDRADIUS_1D 32 // minimum hard surface radius #define PS_P_MINSURFACEHARDNESS_1D 50 // minimum hardness used in collision impulse calculation +// struct for PS settings (shared for 1D and 2D class) +typedef union +{ + struct{ + // one byte bit field for 1D settings + bool wrapX : 1; + bool bounceX : 1; + bool killoutofbounds : 1; // if set, out of bound particles are killed immediately + bool useGravity : 1; // set to 1 if gravity is used, disables bounceY at the top + bool useCollisions : 1; + bool colorByAge : 1; // if set, particle hue is set by ttl value in render function + bool colorByPosition : 1; // if set, particle hue is set by its position in the strip segment + bool unused : 1; + }; + byte asByte; // access as a byte, order is: LSB is first entry in the list above +} PSsettings1D; + //struct for a single particle (6 bytes) typedef struct { int16_t x; // x position in particle system @@ -294,7 +312,7 @@ class ParticleSystem1D // particle emitters int32_t sprayEmit(PSsource1D &emitter); - void particleMoveUpdate(PSparticle1D &part, PSsettings *options = NULL); // move function + void particleMoveUpdate(PSparticle1D &part, PSsettings1D *options = NULL); // move function //particle physics void applyForce(PSparticle1D *part, int8_t xforce, uint8_t *counter); //apply a force to a single particle void applyForce(int8_t xforce); // apply a force to all particles @@ -310,6 +328,7 @@ class ParticleSystem1D void setKillOutOfBounds(bool enable); // if enabled, particles outside of matrix instantly die // void setSaturation(uint8_t sat); // set global color saturation void setColorByAge(bool enable); + void setColorByPosition(bool enable); void setMotionBlur(uint8_t bluramount); // note: motion blur can only be used if 'particlesize' is set to zero void setParticleSize(uint8_t size); //size 0 = 1 pixel, size 1 = 2 pixels void setGravity(int8_t force = 8); @@ -343,7 +362,7 @@ class ParticleSystem1D void bounce(int8_t &incomingspeed, int8_t ¶llelspeed, int32_t &position, uint16_t maxposition); // bounce on a wall CRGB *allocate1Dbuffer(uint32_t length); // note: variables that are accessed often are 32bit for speed - PSsettings particlesettings; // settings used when updating particles + PSsettings1D particlesettings; // settings used when updating particles uint32_t emitIndex; // index to count through particles to emit so searching for dead pixels is faster int32_t collisionHardness; uint32_t wallHardness; From af542d8a6a689b361af594999c382df06fadd88d Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Thu, 27 Jun 2024 08:17:06 +0200 Subject: [PATCH 101/219] added large size rendering to 1D PS, work in progress --- wled00/FX.cpp | 3 +- wled00/FXparticleSystem.cpp | 114 +++++++++++++++++++++++++++++------- wled00/FXparticleSystem.h | 6 +- 3 files changed, 98 insertions(+), 25 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 441a812d98..25d3b7b87b 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -10009,7 +10009,7 @@ uint16_t mode_particleFireworks1D(void) if (SEGMENT.call == 0) // initialization { - if (!initParticleSystem1D(PartSys, 4, 4, true)) // init, no additional data needed + if (!initParticleSystem1D(PartSys, 4, 4, true)) // init return mode_static(); // allocation failed PartSys->setKillOutOfBounds(true); //numRockets = PartSys->numSources; @@ -10065,6 +10065,7 @@ uint16_t mode_particleFireworks1D(void) PartSys->sources[0].source.ttl = 400; PartSys->sources[0].source.collide = false; //exhaust does not collide, also used to check if direction reversed PartSys->sources[0].sat = 40; //low saturation exhaust + // PartSys->sources[0].size = 200; //size render test !!! if(SEGMENT.aux0) //inverted rockets launch from end { diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index a0ee16df12..76254e57ce 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -846,13 +846,13 @@ void ParticleSystem::renderParticle(CRGB **framebuffer, uint32_t particleindex, { if (advPartProps[particleindex].size > 0) { - if (renderbuffer) + if (renderbuffer && framebuffer) { advancedrender = true; memset(renderbuffer[0], 0, 100 * sizeof(CRGB)); // clear the buffer, renderbuffer is 10x10 pixels } else - return; // cannot render without buffer, advanced size particles are allowed out of frame + return; // cannot render without buffers } } @@ -956,7 +956,7 @@ void ParticleSystem::renderParticle(CRGB **framebuffer, uint32_t particleindex, bitshift = 1; rendersize += 2; offset--; - blur2D(renderbuffer, rendersize, rendersize, xsize << bitshift, ysize << bitshift, true, offset, offset, true); //blur to 4x4 + blur2D(renderbuffer, rendersize, rendersize, xsize << bitshift, ysize << bitshift, true, offset, offset, true); xsize = xsize > 64 ? xsize - 64 : 0; ysize = ysize > 64 ? ysize - 64 : 0; } @@ -1634,7 +1634,10 @@ int32_t ParticleSystem1D::sprayEmit(PSsource1D &emitter) particles[emitIndex].reversegrav = emitter.source.reversegrav; particles[emitIndex].ttl = random16(emitter.minLife, emitter.maxLife); if (advPartProps) + { advPartProps[emitIndex].sat = emitter.sat; + advPartProps[emitIndex].size = emitter.size; + } return i; return emitIndex; @@ -1803,19 +1806,12 @@ void ParticleSystem1D::ParticleSys_render() CRGB baseRGB; bool useLocalBuffer = true; //use local rendering buffer, gives huge speed boost (at least 30% more FPS) CRGB *framebuffer = NULL; //local frame buffer - //CRGB **renderbuffer = NULL; //local particle render buffer for advanced particles + CRGB *renderbuffer = NULL; //local particle render buffer for advanced particles uint32_t i; uint32_t brightness; // particle brightness, fades if dying if (useLocalBuffer) { - /* - //memory fragmentation check: - Serial.print("heap: "); - Serial.print(heap_caps_get_free_size(MALLOC_CAP_8BIT)); - Serial.print(" block: "); - Serial.println(heap_caps_get_largest_free_block(MALLOC_CAP_8BIT)); - */ // allocate empty memory for the local renderbuffer framebuffer = allocate1Dbuffer(maxXpixel + 1); @@ -1825,10 +1821,10 @@ void ParticleSystem1D::ParticleSys_render() useLocalBuffer = false; //render to segment pixels directly if not enough memory } else{ - //if (advPartProps) - //{ - // renderbuffer = allocate2Dbuffer(10, 10); //buffer to render individual particles to if size > 0. note: null checking is done when accessing it - //} + if (advPartProps) + { + renderbuffer = allocate1Dbuffer(10); //buffer to render individual particles to if size > 0. note: null checking is done when accessing it + } if (motionBlur > 0) // using SEGMENT.fadeToBlackBy is much slower, this approximately doubles the speed of fade calculation { for (uint32_t x = 0; x <= maxXpixel; x++) @@ -1867,7 +1863,7 @@ void ParticleSystem1D::ParticleSys_render() baseRGB = (CRGB)baseHSV; // convert back to RGB } } - renderParticle(framebuffer, i, brightness, baseRGB); + renderParticle(framebuffer, i, brightness, baseRGB, renderbuffer); } if (useLocalBuffer) // transfer local buffer back to segment @@ -1881,7 +1877,7 @@ void ParticleSystem1D::ParticleSys_render() } // calculate pixel positions and brightness distribution and render the particle to local buffer or global buffer -void ParticleSystem1D::renderParticle(CRGB *framebuffer, uint32_t particleindex, uint32_t brightness, CRGB color) +void ParticleSystem1D::renderParticle(CRGB *framebuffer, uint32_t particleindex, uint32_t brightness, CRGB color, CRGB *renderbuffer) { if(particlesize == 0) //single pixel particle, can be out of bounds as oob checking is made for 2-pixel particles { @@ -1896,7 +1892,7 @@ void ParticleSystem1D::renderParticle(CRGB *framebuffer, uint32_t particleindex, } else { //render larger particles int32_t pxlbrightness[2] = {0}; // note: pxlbrightness needs to be set to 0 or checking does not work - int32_t pixco[2]; // physical pixel coordinates of the two pixels representing a particle + int32_t pixco[2]; // physical pixel coordinates of the two pixels representing a particle // subtract half a radius as the rendering algorithm always starts at the left, this makes calculations more efficient int32_t xoffset = particles[particleindex].x - PS_P_HALFRADIUS_1D; int32_t dx = xoffset % PS_P_RADIUS_1D; //relativ particle position in subpixel space @@ -1936,9 +1932,57 @@ void ParticleSystem1D::renderParticle(CRGB *framebuffer, uint32_t particleindex, pxlbrightness[0] = (((int32_t)PS_P_RADIUS_1D - dx) * brightness) >> PS_P_SURFACE_1D; if (pxlbrightness[1] >= 0) pxlbrightness[1] = (dx * brightness) >> PS_P_SURFACE_1D; - - // standard rendering (2 pixels per particle) - if (framebuffer) + + // check if particle has advanced size properties and buffer is available + if (advPartProps && advPartProps[particleindex].size > 1) + { + if (renderbuffer && framebuffer) + { + memset(renderbuffer, 0, 10 * sizeof(CRGB)); // clear the buffer, renderbuffer is 10 pixels + } + else + return; // cannot render advanced particles without buffer + + + //render par ticle to a bigger size + //particle size to pixels: < 64 is 4 pixels, < 128 is 6pixels, < 192 is 8 pixels, bigger is 10 pixels + //first, render the pixel to the center of the renderbuffer, then apply 1D blurring + fast_color_add(renderbuffer[4], color, pxlbrightness[0]); + fast_color_add(renderbuffer[5], color, pxlbrightness[1]); + uint32_t rendersize = 2; // initialize render size, minimum is 4x4 pixels, it is incremented int he loop below to start with 4 + uint32_t offset = 4; // offset to zero coordinate to write/read data in renderbuffer (actually needs to be 3, is decremented in the loop below) + uint32_t size = advPartProps[particleindex].size; + uint32_t blurpasses = size/64 + 1; // number of blur passes depends on size, four passes max + uint32_t bitshift = 0; + for(int i = 0; i < blurpasses; i++) + { + if (i == 2) //for the last two passes, use higher amount of blur (results in a nicer brightness gradient with soft edges) + bitshift = 1; + rendersize += 2; + offset--; + blur1D(renderbuffer, rendersize, size << bitshift, true, offset); + size = size > 64 ? size - 64 : 0; + } + + // calculate origin coordinates to render the particle to in the framebuffer + uint32_t xfb_orig = x - (rendersize>>1) + 1 - offset; + uint32_t xfb; // coordinates in frame buffer to write to note: by making this uint, only overflow has to be checked + + // transfer particle renderbuffer to framebuffer + for(uint32_t xrb = offset; xrb < rendersize+offset; xrb++) + { + xfb = xfb_orig + xrb; + if (xfb > maxXpixel) + { + if (particlesettings.wrapX) // wrap x to the other side if required + xfb = xfb % (maxXpixel + 1); + else + continue; + } + fast_color_add(framebuffer[xfb], renderbuffer[xrb]); + } + } + else if (framebuffer) // standard rendering (2 pixels per particle) { for(uint32_t i = 0; i < 2; i++) { @@ -2242,6 +2286,34 @@ bool initParticleSystem1D(ParticleSystem1D *&PartSys, uint8_t requestedsources, return true; } + +// blur a 1D buffer, sub-size blurring can be done using start and size +// for speed, 32bit variables are used, make sure to limit them to 8bit (0-255) or result is undefined +// to blur a subset of the buffer, change the size and set start to the desired starting coordinates (default start is 0/0) +void blur1D(CRGB *colorbuffer, uint32_t size, uint32_t blur, bool smear, uint32_t start) +{ + CRGB seeppart, carryover; + uint32_t seep = blur >> 1; + + carryover = BLACK; + for(uint32_t x = start; x < start + size; x++) + { + seeppart = colorbuffer[x]; // create copy of current color + fast_color_scale(seeppart, seep); // scale it and seep to neighbours + if (!smear) // fade current pixel if smear is disabled + fast_color_scale(colorbuffer[x], 255 - blur); + + if (x > 0) + { + fast_color_add(colorbuffer[x-1], seeppart); + fast_color_add(colorbuffer[x], carryover); + } + carryover = seeppart; + } + fast_color_add(colorbuffer[size-1], carryover); // set last pixel +} + + #endif // WLED_DISABLE_PARTICLESYSTEM1D diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index 82bef96899..c3c73dfb25 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -298,7 +298,7 @@ typedef struct { int8_t var; // variation of emitted speed (adds random(+/- var) to speed) int8_t v; // emitting speed uint8_t sat; // color saturation (advanced property) - uint8_t size; // particle size (advanced property) TODO: can be removed in case this is not being implemented + uint8_t size; // particle size (advanced property) } PSsource1D; @@ -348,7 +348,7 @@ class ParticleSystem1D private: //rendering functions void ParticleSys_render(void); - void renderParticle(CRGB *framebuffer, uint32_t particleindex, uint32_t brightness, CRGB color); + void renderParticle(CRGB *framebuffer, uint32_t particleindex, uint32_t brightness, CRGB color, CRGB *renderbuffer); //paricle physics applied by system if flags are set void applyGravity(); // applies gravity to all particles @@ -380,5 +380,5 @@ bool initParticleSystem1D(ParticleSystem1D *&PartSys, uint8_t requestedsources, uint32_t calculateNumberOfParticles1D(bool isadvanced); uint32_t calculateNumberOfSources1D(uint8_t requestedsources); bool allocateParticleSystemMemory1D(uint16_t numparticles, uint16_t numsources, bool isadvanced, uint16_t additionalbytes); - +void blur1D(CRGB *colorbuffer, uint32_t size, uint32_t blur, bool smear = true, uint32_t start = 0); #endif // WLED_DISABLE_PARTICLESYSTEM1D From 47566a47053454507d61aeca8800d4ff5d421958 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Thu, 27 Jun 2024 15:17:01 +0200 Subject: [PATCH 102/219] bugfix (forgot to free memory) also: made random size a permanent option in 1D fireworks as it looks kind of cool --- wled00/FX.cpp | 16 +++++++--------- wled00/FXparticleSystem.cpp | 4 +++- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 25d3b7b87b..13b80845ee 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -10026,7 +10026,6 @@ uint16_t mode_particleFireworks1D(void) DEBUG_PRINT(F("ERROR: FX PartSys nullpointer")); return mode_static(); // something went wrong, no data! } - // Particle System settings PartSys->updateSystem(); // update system properties (dimensions and data pointers) forcecounter = PartSys->PSdataEnd; @@ -10042,7 +10041,6 @@ uint16_t mode_particleFireworks1D(void) PartSys->setParticleSize(1); else PartSys->setParticleSize(0); - if(PartSys->sources[0].source.perpetual == 1) //rocket is on standby { PartSys->sources[0].source.ttl--; @@ -10059,13 +10057,13 @@ uint16_t mode_particleFireworks1D(void) PartSys->sources[0].v = 0; PartSys->sources[0].minLife = 10; PartSys->sources[0].maxLife = 30; - PartSys->sources[0].source.x = 0; //start from bottom + PartSys->sources[0].source.x = 0; // start from bottom uint32_t speed = 8 + (((int)16 + (int)random16(20) + (int)random(SEGMENT.speed >> 3 , SEGMENT.speed >> 2)) * (int)PartSys->maxXpixel) / 150; //set speed such that rocket explods in frame, found by experimenting PartSys->sources[0].source.vx = min(speed, (uint32_t)127); PartSys->sources[0].source.ttl = 400; - PartSys->sources[0].source.collide = false; //exhaust does not collide, also used to check if direction reversed - PartSys->sources[0].sat = 40; //low saturation exhaust - // PartSys->sources[0].size = 200; //size render test !!! + PartSys->sources[0].source.collide = false; // exhaust does not collide, also used to check if direction reversed + PartSys->sources[0].sat = 40; // low saturation exhaust + PartSys->sources[0].size = 0; // default size if(SEGMENT.aux0) //inverted rockets launch from end { @@ -10102,6 +10100,7 @@ uint16_t mode_particleFireworks1D(void) PartSys->sources[0].maxLife = 150; PartSys->sources[0].source.ttl = 100 + random16(256 - SEGMENT.intensity); // standby time til next launch PartSys->sources[0].sat = 7 + (SEGMENT.custom3 << 3); //color saturation + PartSys->sources[0].size = random16(255); // random particle size in explosion uint32_t explosionsize = 10 + random(SEGMENT.intensity >> 2, SEGMENT.intensity); for(uint32_t e = 0; e < explosionsize; e++) //emit explosion particles { @@ -10114,9 +10113,8 @@ uint16_t mode_particleFireworks1D(void) } if(SEGMENT.call & 0x01) //every second frame PartSys->sprayEmit(PartSys->sources[i]); //emit a particle - + PartSys->update(); // update and render - return FRAMETIME; } static const char _data_FX_MODE_PS_FIREWORKS1D[] PROGMEM = "PS Fireworks 1D@Gravity,Explosion,Firing side,Blur/Overlay,Saturation,Gravity,Colorful,Smooth;,!;!;1;pal=0,sx=150,ix=150,c1=220,c2=30,c3=21,o1=0,o2=0,o3=0"; @@ -10518,7 +10516,7 @@ uint16_t mode_particleBalance(void) PartSys->update(); // update and render return FRAMETIME; } -static const char _data_FX_MODE_PS_BALANCE[] PROGMEM = "PS 1D Balance@!,!,Collisions,Blur/Overlay,Tilt,Color by Position,Wrap/Bounce,Random;,!;!;1;pal=35,sx=200,ix=220,c1=4,c2=0,c3=28,o1=1,o2=1,o3=0"; +static const char _data_FX_MODE_PS_BALANCE[] PROGMEM = "PS 1D Balance@!,!,Collisions,Blur/Overlay,Tilt,Color by Position,Wrap/Bounce,Random;,!;!;1;pal=18,sx=200,ix=40,c1=200,c2=0,c3=5,o1=1,o2=0,o3=1"; diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index 76254e57ce..84f78bc304 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -1871,9 +1871,11 @@ void ParticleSystem1D::ParticleSys_render() for (int x = 0; x <= maxXpixel; x++) { SEGMENT.setPixelColor(x, framebuffer[x]); - } + } free(framebuffer); } + if (renderbuffer) + free(renderbuffer); } // calculate pixel positions and brightness distribution and render the particle to local buffer or global buffer From 55809fb2b15a2f6a88e3aec9f81c9bd110db20c7 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Fri, 28 Jun 2024 20:48:57 +0200 Subject: [PATCH 103/219] Added PS based Chase, fixed some bugs --- wled00/FX.cpp | 116 +++++++++++++++++++++++++++--------- wled00/FX.h | 3 +- wled00/FXparticleSystem.cpp | 18 ++++-- wled00/FXparticleSystem.h | 24 ++++---- 4 files changed, 114 insertions(+), 47 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 13b80845ee..b5fa885a8c 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -7915,7 +7915,7 @@ uint16_t mode_particlevortex(void) #else PartSys->setMotionBlur(100); #endif - uint8_t numSprays = min(PartSys->numSources, (uint8_t)NUMBEROFSOURCES); + uint8_t numSprays = min(PartSys->numSources, (uint32_t)NUMBEROFSOURCES); for (i = 0; i < numSprays; i++) { PartSys->sources[i].source.x = (PartSys->maxX + 1) >> 1; // center @@ -7935,7 +7935,7 @@ uint16_t mode_particlevortex(void) } PartSys->updateSystem(); // update system properties (dimensions and data pointers) - uint8_t spraycount = min(PartSys->numSources, (uint8_t)(1 + (SEGMENT.custom1 >> 5))); // number of sprays to display, 1-8 + uint8_t spraycount = min(PartSys->numSources, (uint32_t)(1 + (SEGMENT.custom1 >> 5))); // number of sprays to display, 1-8 #ifdef ESP8266 for (i = 1; i < 4; i++) // need static particles in the center to reduce blinking (would be black every other frame without this hack), just set them there fixed { @@ -8054,7 +8054,7 @@ uint16_t mode_particlefireworks(void) return mode_static(); // allocation failed PartSys->setKillOutOfBounds(true); //out of bounds particles dont return (except on top, taken care of by gravity setting) PartSys->setWallHardness(100); //ground bounce is fixed - numRockets = min(PartSys->numSources, (uint8_t)NUMBEROFSOURCES); + numRockets = min(PartSys->numSources, (uint32_t)NUMBEROFSOURCES); for (j = 0; j < numRockets; j++) { PartSys->sources[j].source.ttl = 500 * j; // first rocket starts immediately, others follow soon @@ -8070,7 +8070,7 @@ uint16_t mode_particlefireworks(void) return mode_static(); // something went wrong, no data! } PartSys->updateSystem(); // update system properties (dimensions and data pointers) - numRockets = min(PartSys->numSources, (uint8_t)NUMBEROFSOURCES); + numRockets = min(PartSys->numSources, (uint32_t)NUMBEROFSOURCES); PartSys->setWrapX(SEGMENT.check1); PartSys->setBounceY(SEGMENT.check2); @@ -8233,7 +8233,7 @@ uint16_t mode_particlevolcano(void) PartSys->setGravity(); // enable with default gforce PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting) PartSys->setMotionBlur(190); // anable motion blur - numSprays = min(PartSys->numSources, (uint8_t)NUMBEROFSOURCES); // number of sprays + numSprays = min(PartSys->numSources, (uint32_t)NUMBEROFSOURCES); // number of sprays for (i = 0; i < numSprays; i++) { PartSys->sources[i].source.hue = random16(); @@ -8253,7 +8253,7 @@ uint16_t mode_particlevolcano(void) return mode_static(); // something went wrong, no data! } - numSprays = min(PartSys->numSources, (uint8_t)NUMBEROFSOURCES); // number of sprays + numSprays = min(PartSys->numSources, (uint32_t)NUMBEROFSOURCES); // number of sprays // Particle System settings PartSys->updateSystem(); // update system properties (dimensions and data pointers) @@ -8516,7 +8516,7 @@ uint16_t mode_particlewaterfall(void) PartSys->setGravity(); // enable with default gforce PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting) PartSys->setMotionBlur(190); // anable motion blur - numSprays = min((uint32_t)PartSys->numSources, min(PartSys->maxXpixel/5, (uint32_t)2)); // number of sprays + numSprays = min((int32_t)PartSys->numSources, min(PartSys->maxXpixel/5, (int32_t)2)); // number of sprays for (i = 0; i < numSprays; i++) { PartSys->sources[i].source.hue = random16(); @@ -8544,7 +8544,7 @@ uint16_t mode_particlewaterfall(void) PartSys->setBounceX(SEGMENT.check2); // walls PartSys->setBounceY(SEGMENT.check3); // ground PartSys->setWallHardness(SEGMENT.custom2); - numSprays = min((uint32_t)PartSys->numSources, min(PartSys->maxXpixel / 5, (uint32_t)2)); // number of sprays depends on segment width + numSprays = min((int32_t)PartSys->numSources, min(PartSys->maxXpixel / 5, (int32_t)2)); // number of sprays depends on segment width if (SEGMENT.custom2 > 0) // collisions enabled PartSys->enableParticleCollisions(true, SEGMENT.custom2); // enable collisions and set particle collision hardness @@ -8606,7 +8606,7 @@ uint16_t mode_particlebox(void) #ifdef ESP8266 SEGMENT.aux1 = min((uint16_t)((PartSys->maxXpixel * PartSys->maxYpixel) >> 1), PartSys->numParticles); #else - SEGMENT.aux1 = min((uint16_t)((PartSys->maxXpixel * PartSys->maxYpixel)), PartSys->numParticles); // max number of particles + SEGMENT.aux1 = min((uint32_t)((PartSys->maxXpixel * PartSys->maxYpixel)), PartSys->numParticles); // max number of particles #endif for (i = 0; i < SEGMENT.aux1; i++) { @@ -8781,7 +8781,7 @@ uint16_t mode_particleimpact(void) PartSys->setKillOutOfBounds(false); // explosions out of frame ar allowed, set to true to save particles (TODO: better enable it in ESP8266?) PartSys->setGravity(); // enable default gravity PartSys->setBounceY(true); // always use ground bounce - MaxNumMeteors = min(PartSys->numSources, (uint8_t)NUMBEROFSOURCES); + MaxNumMeteors = min(PartSys->numSources, (uint32_t)NUMBEROFSOURCES); for (i = 0; i < MaxNumMeteors; i++) { PartSys->sources[i].source.y = 500; @@ -8804,7 +8804,7 @@ uint16_t mode_particleimpact(void) PartSys->setBounceX(SEGMENT.check2); PartSys->setWallHardness(SEGMENT.custom2); PartSys->enableParticleCollisions(SEGMENT.check3, SEGMENT.custom2); // enable collisions and set particle collision hardness - MaxNumMeteors = min(PartSys->numSources, (uint8_t)NUMBEROFSOURCES); + MaxNumMeteors = min(PartSys->numSources, (uint32_t)NUMBEROFSOURCES); uint8_t numMeteors = map(SEGMENT.custom3, 0, 31, 1, MaxNumMeteors); // number of meteors to use for animation uint32_t emitparticles; // number of particles to emit for each rocket's state @@ -9350,7 +9350,7 @@ if (SEGLEN == 1) { if (!initParticleSystem2D(PartSys, NUMBEROFSOURCES)) // init, request 16 sources return mode_static(); // allocation failed - numSprays = min(PartSys->numSources, (uint8_t)NUMBEROFSOURCES); + numSprays = min(PartSys->numSources, (uint32_t)NUMBEROFSOURCES); for (i = 0; i < numSprays; i++) { PartSys->sources[i].source.x = (PartSys->maxX + 1) >> 1; // center @@ -9370,7 +9370,7 @@ if (SEGLEN == 1) return mode_static(); // something went wrong, no data! } PartSys->updateSystem(); // update system properties (dimensions and data pointers) - numSprays = min(PartSys->numSources, (uint8_t)NUMBEROFSOURCES); + numSprays = min(PartSys->numSources, (uint32_t)NUMBEROFSOURCES); um_data_t *um_data; if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) @@ -9541,7 +9541,7 @@ uint16_t mode_particleblobs(void) return mode_static(); // something went wrong, no data! } PartSys->updateSystem(); // update system properties (dimensions and data pointers) - PartSys->setUsedParticles(min(PartSys->numParticles, (uint16_t)map(SEGMENT.intensity,0 ,255, 1, (PartSys->maxXpixel * PartSys->maxYpixel)>>4))); + PartSys->setUsedParticles(min(PartSys->numParticles, (uint32_t)map(SEGMENT.intensity,0 ,255, 1, (PartSys->maxXpixel * PartSys->maxYpixel)>>4))); PartSys->enableParticleCollisions(SEGMENT.check2); for (uint32_t i = 0; i < PartSys->usedParticles; i++) // update particles @@ -10409,9 +10409,6 @@ uint16_t mode_particle1Dspray(void) PartSys->sources[0].source.reversegrav = false; if(gravity < 0) PartSys->sources[0].source.reversegrav = true; - - //if(SEGMENT.call % (1 + ((255 - SEGMENT.intensity) >> 2)) == 0) - // PartSys->sprayEmit(PartSys->sources[0]); //emit a particle if(random(255) % (1 + ((255 - SEGMENT.intensity) >> 3)) == 0) PartSys->sprayEmit(PartSys->sources[i]); //emit a particle @@ -10466,7 +10463,6 @@ uint16_t mode_particleBalance(void) DEBUG_PRINT(F("ERROR: FX PartSys nullpointer")); return mode_static(); // something went wrong, no data! } - //TODO: wenn collisions ausgeschaltet ist, müssen die partikel sterben und neu eingefügt werden, sonst klumpt das komplett, oder: random friction wäre auch eine möglichkeit // Particle System settings PartSys->updateSystem(); // update system properties (dimensions and data pointers) @@ -10493,16 +10489,10 @@ uint16_t mode_particleBalance(void) xgravity = (xgravity * ((SEGMENT.custom3+1) << 2)) / 128; PartSys->applyForce(xgravity); } - // if(SEGMENT.check2) //collisions enabled - // { - // if (SEGMENT.call % 3 == 0) - // PartSys->applyFriction(2); //apply some friction -// } - // else //no collisions, - { - uint32_t randomindex = random(PartSys->usedParticles); - PartSys->particles[randomindex].vx = ((int32_t)PartSys->particles[randomindex].vx * 200) / 255; // apply friction to random particle to reduce clumping (without collisions) - } + + uint32_t randomindex = random(PartSys->usedParticles); + PartSys->particles[randomindex].vx = ((int32_t)PartSys->particles[randomindex].vx * 200) / 255; // apply friction to random particle to reduce clumping (without collisions) + //update colors PartSys->setColorByPosition(SEGMENT.check1); @@ -10520,6 +10510,75 @@ static const char _data_FX_MODE_PS_BALANCE[] PROGMEM = "PS 1D Balance@!,!,Collis +/* +Particle based Chase effect +Uses palette for particle color +by DedeHai (Damian Schneider) +*/ + +uint16_t mode_particleChase(void) +{ + if (SEGLEN == 1) + return mode_static(); + ParticleSystem1D *PartSys = NULL; + uint32_t i; + + if (SEGMENT.call == 0) // initialization + { + if (!initParticleSystem1D(PartSys, 1, 0, true)) // init, no additional data needed + return mode_static(); // allocation failed + PartSys->setParticleSize(1); //advanced size + PartSys->setWrap(true); + for(i = 0; i < PartSys->numParticles; i++) + { + PartSys->advPartProps[i].sat = 255; + PartSys->particles[i].ttl = 300; + PartSys->particles[i].perpetual = true; + } + SEGMENT.aux0 = 0xFFFF; // invalidate + } + else + PartSys = reinterpret_cast(SEGMENT.data); // if not first call, just set the pointer to the PS + + if (PartSys == NULL) + { + DEBUG_PRINT(F("ERROR: FX PartSys nullpointer")); + return mode_static(); // something went wrong, no data! + } + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setColorByPosition(SEGMENT.check3); + PartSys->setMotionBlur(7 + (SEGMENT.custom3 << 3)); // anable motion blur + + //PartSys->setBounce(SEGMENT.check2); + uint32_t settingssum = SEGMENT.speed + SEGMENT.intensity + SEGMENT.custom1 + SEGMENT.custom2 + SEGMENT.check1 + SEGMENT.check2 + SEGMENT.check3; + + if(SEGMENT.aux0 != settingssum) //settings changed changed, update + { + PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 1, min(PartSys->maxX / (64 + (SEGMENT.custom1 >> 1)), (int32_t)(PartSys->numParticles)))); //depends on intensity and particle size (custom1) + for(i = 0; i < PartSys->usedParticles; i++) + { + PartSys->particles[i].x = i * (PartSys->maxX / (PartSys->usedParticles)); // distribute evenly + if(SEGMENT.custom2 == 0) + PartSys->particles[i].hue = (i * 256) / PartSys->usedParticles; // gradient distribution + else if(SEGMENT.custom2 == 255) + PartSys->particles[i].hue = random16(); + else + PartSys->particles[i].hue = SEGMENT.custom2; + int32_t speed = SEGMENT.speed >> 1; + if(SEGMENT.check1) speed = -speed; + PartSys->particles[i].vx = speed; + PartSys->advPartProps[i].size = SEGMENT.custom1; + } + SEGMENT.aux0 = settingssum; + } + + PartSys->update(); // update and render + return FRAMETIME; +} +static const char _data_FX_MODE_PS_CHASE[] PROGMEM = "PS Chase@Speed,Density,Size,Color,Blur/Overlay,Direction,,Color by Position;,!;!;1;pal=53,sx=50,ix=100,c2=0,c3=0,o1=0,o2=0,o3=0"; + + #endif //WLED_DISABLE_PARTICLESYSTEM1D @@ -10790,6 +10849,7 @@ addEffect(FX_MODE_PSSPARKLER, &mode_particleSparkler, _data_FX_MODE_PS_SPARKLER) addEffect(FX_MODE_PSHOURGLASS, &mode_particleHourglass, _data_FX_MODE_PS_HOURGLASS); addEffect(FX_MODE_PS1DSPRAY, &mode_particle1Dspray, _data_FX_MODE_PS_1DSPRAY); addEffect(FX_MODE_PSBALANCE, &mode_particleBalance, _data_FX_MODE_PS_BALANCE); +addEffect(FX_MODE_PSCHASE, &mode_particleChase, _data_FX_MODE_PS_CHASE); #endif // WLED_DISABLE_PARTICLESYSTEM1D diff --git a/wled00/FX.h b/wled00/FX.h index 9fb684a7ab..f9e1f31ee4 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -340,7 +340,8 @@ #define FX_MODE_PSHOURGLASS 207 #define FX_MODE_PS1DSPRAY 208 #define FX_MODE_PSBALANCE 209 -#define MODE_COUNT 210 +#define FX_MODE_PSCHASE 210 +#define MODE_COUNT 211 typedef enum mapping1D2D { M12_Pixels = 0, diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index 84f78bc304..e006ed55dd 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -128,7 +128,7 @@ void ParticleSystem::updateFire(uint32_t intensity, bool renderonly) ParticleSys_render(true, intensity); } -void ParticleSystem::setUsedParticles(uint16_t num) +void ParticleSystem::setUsedParticles(uint32_t num) { usedParticles = min(num, numParticles); //limit to max particles } @@ -973,11 +973,11 @@ void ParticleSystem::renderParticle(CRGB **framebuffer, uint32_t particleindex, if (xfb > maxXpixel) { if (particlesettings.wrapX) // wrap x to the other side if required - xfb = xfb % (maxXpixel + 1); + xfb = xfb % (maxXpixel + 1); //TODO: this did not work in 1D system but appears to work in 2D (wrapped pixels were offset) under which conditions does this not work? else continue; } - + for(uint32_t yrb = offset; yrb < rendersize+offset; yrb++) { yfb = yfb_orig + yrb; @@ -1541,7 +1541,7 @@ void ParticleSystem1D::update(void) } -void ParticleSystem1D::setUsedParticles(uint16_t num) +void ParticleSystem1D::setUsedParticles(uint32_t num) { usedParticles = min(num, numParticles); //limit to max particles } @@ -1700,6 +1700,7 @@ void ParticleSystem1D::particleMoveUpdate(PSparticle1D &part, PSsettings1D *opti newX = newX % (maxX + 1); if(newX < 0) newX += maxX + 1; + Serial.println(newX/32); } else if (((newX <= -PS_P_HALFRADIUS_1D) || (newX > maxX + PS_P_HALFRADIUS_1D))) // particle is leaving, set out of bounds if it has fully left { @@ -1946,7 +1947,7 @@ void ParticleSystem1D::renderParticle(CRGB *framebuffer, uint32_t particleindex, return; // cannot render advanced particles without buffer - //render par ticle to a bigger size + //render particle to a bigger size //particle size to pixels: < 64 is 4 pixels, < 128 is 6pixels, < 192 is 8 pixels, bigger is 10 pixels //first, render the pixel to the center of the renderbuffer, then apply 1D blurring fast_color_add(renderbuffer[4], color, pxlbrightness[0]); @@ -1977,7 +1978,12 @@ void ParticleSystem1D::renderParticle(CRGB *framebuffer, uint32_t particleindex, if (xfb > maxXpixel) { if (particlesettings.wrapX) // wrap x to the other side if required - xfb = xfb % (maxXpixel + 1); + { + if(xfb > maxXpixel << 1) // xfb is "negative" (note: for some reason, this is needed in 1D but works without in 2D...) + xfb = (maxXpixel +1) + (int32_t)xfb; + else + xfb = xfb % (maxXpixel + 1); + } else continue; } diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index c3c73dfb25..af1a4bf54f 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -158,7 +158,7 @@ class ParticleSystem void lineAttractor(uint16_t particleindex, PSparticle *attractorcenter, uint16_t attractorangle, uint8_t strength); // set options - void setUsedParticles(uint16_t num); + 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 setWallRoughness(uint8_t roughness); // wall roughness randomizes wall collisions @@ -180,11 +180,11 @@ class ParticleSystem PSadvancedParticle *advPartProps; // pointer to advanced particle properties (can be NULL) PSsizeControl *advPartSize; // pointer to advanced particle size control (can be NULL) uint8_t* PSdataEnd; // points to first available byte after the PSmemory, is set in setPointers(). use this for FX custom data - uint16_t maxX, maxY; // particle system size i.e. width-1 / height-1 in subpixels - uint32_t maxXpixel, maxYpixel; // last physical pixel that can be drawn to (FX can read this to read segment size if required), equal to width-1 / height-1 - uint8_t numSources; // number of sources - uint16_t numParticles; // number of particles available in this system - uint16_t usedParticles; // number of particles used in animation (can be smaller then numParticles) + int32_t maxX, maxY; // particle system size i.e. width-1 / height-1 in subpixels + int32_t maxXpixel, maxYpixel; // last physical pixel that can be drawn to (FX can read this to read segment size if required), equal to width-1 / height-1 + uint32_t numSources; // number of sources + 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 @@ -320,7 +320,7 @@ class ParticleSystem1D void applyFriction(int32_t coefficient); // apply friction to all used particles // set options - void setUsedParticles(uint16_t num); + void setUsedParticles(uint32_t num); void setWallHardness(uint8_t hardness); // hardness for bouncing on the wall if bounceXY is set void setSize(uint16_t x); //set particle system size (= strip length) void setWrap(bool enable); @@ -339,11 +339,11 @@ class ParticleSystem1D PSadvancedParticle1D *advPartProps; // pointer to advanced particle properties (can be NULL) //PSsizeControl *advPartSize; // pointer to advanced particle size control (can be NULL) uint8_t* PSdataEnd; // points to first available byte after the PSmemory, is set in setPointers(). use this for FX custom data - uint16_t maxX; // particle system size i.e. width-1 - uint32_t maxXpixel; // last physical pixel that can be drawn to (FX can read this to read segment size if required), equal to width-1 - uint8_t numSources; // number of sources - uint16_t numParticles; // number of particles available in this system - uint16_t usedParticles; // number of particles used in animation (can be smaller then numParticles) + int32_t maxX; // particle system size i.e. width-1 + int32_t maxXpixel; // last physical pixel that can be drawn to (FX can read this to read segment size if required), equal to width-1 + uint32_t numSources; // number of sources + 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 From d7148c3afa4395b336a61d47f57aa3139f59cfa3 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Fri, 28 Jun 2024 21:02:29 +0200 Subject: [PATCH 104/219] minor update --- wled00/FX.cpp | 4 ++-- wled00/FXparticleSystem.cpp | 7 ++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index b5fa885a8c..4c2fe82062 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -10527,7 +10527,6 @@ uint16_t mode_particleChase(void) { if (!initParticleSystem1D(PartSys, 1, 0, true)) // init, no additional data needed return mode_static(); // allocation failed - PartSys->setParticleSize(1); //advanced size PartSys->setWrap(true); for(i = 0; i < PartSys->numParticles; i++) { @@ -10570,9 +10569,10 @@ uint16_t mode_particleChase(void) PartSys->particles[i].vx = speed; PartSys->advPartProps[i].size = SEGMENT.custom1; } + PartSys->setParticleSize(SEGMENT.custom1); // if custom1 == 0 this sets rendering size to one pixel SEGMENT.aux0 = settingssum; } - +PartSys->setParticleSize(SEGMENT.custom1); // if custom1 == 0 this sets rendering size to one pixel PartSys->update(); // update and render return FRAMETIME; } diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index e006ed55dd..42a0ba8927 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -1584,8 +1584,9 @@ void ParticleSystem1D::setColorByPosition(bool enable) void ParticleSystem1D::setMotionBlur(uint8_t bluramount) { - if (particlesize < 2) // only allwo motion blurring on default particle size or advanced size(cannot combine motion blur with normal blurring used for particlesize, would require another buffer) - motionBlur = bluramount; + //TODO: currently normal blurring is not used in 1D system. should it be added? advanced rendering is quite fast and allows for motion blurring + // if (particlesize < 2) // only allwo motion blurring on default particle size or advanced size(cannot combine motion blur with normal blurring used for particlesize, would require another buffer) + motionBlur = bluramount; } // render size using smearing (see blur function) @@ -1979,7 +1980,7 @@ void ParticleSystem1D::renderParticle(CRGB *framebuffer, uint32_t particleindex, { if (particlesettings.wrapX) // wrap x to the other side if required { - if(xfb > maxXpixel << 1) // xfb is "negative" (note: for some reason, this is needed in 1D but works without in 2D...) + if(xfb > maxXpixel << 1) // xfb is "negative" (note: for some reason, this check is needed in 1D but works without in 2D...) xfb = (maxXpixel +1) + (int32_t)xfb; else xfb = xfb % (maxXpixel + 1); From e7e4c98b74a8ee82cd222b40aef121883f402b7b Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sat, 29 Jun 2024 13:29:34 +0200 Subject: [PATCH 105/219] two new FX: Starburst and 1D GEQ --- wled00/FX.cpp | 202 +++++++++++++++++++++++++++++++++++++++++++------- wled00/FX.h | 4 +- 2 files changed, 180 insertions(+), 26 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 4c2fe82062..0c7a3de6e9 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -9685,10 +9685,7 @@ uint16_t mode_particleDrip(void) PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur PartSys->setGravity(SEGMENT.custom3>>1); // set gravity (8 is default strength) - if (SEGMENT.check3) // collisions enabled - PartSys->setParticleSize(1); - else - PartSys->setParticleSize(0); + PartSys->setParticleSize(SEGMENT.check3); // 1 or 2 pixel rendering if(SEGMENT.check2) //collisions enabled PartSys->enableParticleCollisions(true); //enable, full hardness @@ -9805,10 +9802,7 @@ uint16_t mode_particleBouncingBalls(void) PartSys->enableParticleCollisions(false); PartSys->setUsedParticles( 1 + (SEGMENT.intensity >> 3)); // 1 - 32 - if (SEGMENT.check2) - PartSys->setParticleSize(1); //2-pixel size (smoother for slow speeds) - else - PartSys->setParticleSize(0); //single pixel size (classic look) + PartSys->setParticleSize(SEGMENT.check3); // 1 or 2 pixel rendering if(SEGMENT.check3) //rolling balls { @@ -9901,10 +9895,7 @@ uint16_t mode_particleDancingShadows(void) // Particle System settings PartSys->updateSystem(); // update system properties (dimensions and data pointers) PartSys->setMotionBlur(SEGMENT.custom1); - if (SEGMENT.check3) // collisions enabled - PartSys->setParticleSize(1); - else - PartSys->setParticleSize(0); + PartSys->setParticleSize(SEGMENT.check3); // 1 or 2 pixel rendering //generate a spotlight: generates particles just outside of view //if (SEGMENT.call % ((255 + 64) / (1 + SEGMENT.intensity + (SEGMENT.speed >> 4))) == 0) @@ -10029,18 +10020,14 @@ uint16_t mode_particleFireworks1D(void) // Particle System settings PartSys->updateSystem(); // update system properties (dimensions and data pointers) forcecounter = PartSys->PSdataEnd; - //numRockets = PartSys->numSources; + PartSys->setParticleSize(SEGMENT.check3); // 1 or 2 pixel rendering PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur if(!SEGMENT.check1) //gravity enabled for sparks PartSys->setGravity(0); // disable else - PartSys->setGravity(1 + (SEGMENT.speed>>4)); // set gravity + PartSys->setGravity(1 + (SEGMENT.speed>>4)); // set gravity - if (SEGMENT.check3) - PartSys->setParticleSize(1); - else - PartSys->setParticleSize(0); if(PartSys->sources[0].source.perpetual == 1) //rocket is on standby { PartSys->sources[0].source.ttl--; @@ -10131,7 +10118,7 @@ uint16_t mode_particleSparkler(void) if (SEGLEN == 1) return mode_static(); ParticleSystem1D *PartSys = NULL; - uint8_t numSparklers; + uint32_t numSparklers; uint32_t i; PSsettings1D sparklersettings; sparklersettings.asByte = 0; // PS settings for sparkler (set below) @@ -10157,12 +10144,7 @@ uint16_t mode_particleSparkler(void) numSparklers = PartSys->numSources; PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur - - - if (SEGMENT.check3) - PartSys->setParticleSize(1); - else - PartSys->setParticleSize(0); + PartSys->setParticleSize(SEGMENT.check3); // 1 or 2 pixel rendering for(i = 0; i < numSparklers; i++) { @@ -10579,6 +10561,174 @@ PartSys->setParticleSize(SEGMENT.custom1); // if custom1 == 0 this sets renderin static const char _data_FX_MODE_PS_CHASE[] PROGMEM = "PS Chase@Speed,Density,Size,Color,Blur/Overlay,Direction,,Color by Position;,!;!;1;pal=53,sx=50,ix=100,c2=0,c3=0,o1=0,o2=0,o3=0"; +/* +Particle Fireworks Starburst replacement (smoother rendering, more settings) +Uses palette for particle color +by DedeHai (Damian Schneider) +*/ + +uint16_t mode_particleStarburst(void) +{ + if (SEGLEN == 1) + return mode_static(); + ParticleSystem1D *PartSys = NULL; + uint32_t i; + + if (SEGMENT.call == 0) // initialization + { + if (!initParticleSystem1D(PartSys, 1, 0, true)) // init + return mode_static(); // allocation failed + PartSys->setKillOutOfBounds(true); + PartSys->enableParticleCollisions(true, 250); + PartSys->sources[0].source.ttl = 1; // set initial stanby time + PartSys->sources[0].sat = 0; // emitted particles start out white + } + else + PartSys = reinterpret_cast(SEGMENT.data); // if not first call, just set the pointer to the PS + + if (PartSys == NULL) + { + DEBUG_PRINT(F("ERROR: FX PartSys nullpointer")); + return mode_static(); // something went wrong, no data! + } + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur + PartSys->setGravity(SEGMENT.check1 * 8); // enable gravity + + if(PartSys->sources[0].source.ttl-- == 0) // stanby time elapsed TODO: make it a timer? + { + uint32_t explosionsize = 4 + random(SEGMENT.intensity >> 2); + PartSys->sources[0].source.hue = random16(); + PartSys->sources[0].var = 10 + (explosionsize << 1); + PartSys->sources[0].minLife = 250; + PartSys->sources[0].maxLife = 300; + PartSys->sources[0].source.x = random(PartSys->maxX); //random explosion position + PartSys->sources[0].source.ttl = 10 + random16(255 - SEGMENT.speed); + PartSys->sources[0].size = SEGMENT.custom1; // Fragment size + PartSys->setParticleSize(SEGMENT.custom1); // enable advanced size rendering + PartSys->sources[0].source.collide = SEGMENT.check3; + for(uint32_t e = 0; e < explosionsize; e++) // emit particles + { + if(SEGMENT.check2) + PartSys->sources[0].source.hue = random16(); //random color for each particle + PartSys->sprayEmit(PartSys->sources[0]); //emit a particle + } + } + //shrink all particles + for(i = 0; i < PartSys->usedParticles; i++) + { + if(PartSys->advPartProps[i].size) + PartSys->advPartProps[i].size--; + if(PartSys->advPartProps[i].sat < 251) + PartSys->advPartProps[i].sat += 1 + (SEGMENT.custom3 >> 2); //note: it should be >> 3, the >> 2 creates overflows resulting in blinking if custom3 > 27, which is a bonus feature + } + + if(SEGMENT.call % 5 == 0) + { + PartSys->applyFriction(1); //slow down particles + } + + PartSys->update(); // update and render + return FRAMETIME; +} +static const char _data_FX_MODE_PS_STARBURST[] PROGMEM = "PS Starburst@Chance,Fragments,Fragment Size,Blur/Overlay,Cooling,Gravity,Colorful,Push;,!;!;1;pal=52,sx=150,ix=150,c1=120,c2=0,c3=21,o1=0,o2=0,o3=0"; + + + +/* +Particle based 1D GEQ effect, each frequency bin gets an emitter, distributed over the strip +Uses palette for particle color +by DedeHai (Damian Schneider) +*/ + +uint16_t mode_particle1DGEQ(void) +{ + if (SEGLEN == 1) + return mode_static(); + ParticleSystem1D *PartSys = NULL; + uint32_t numSources; + uint32_t i; + + if (SEGMENT.call == 0) // initialization + { + if (!initParticleSystem1D(PartSys, 16, 0, true)) // init, no additional data needed + return mode_static(); // allocation failed + } + else + PartSys = reinterpret_cast(SEGMENT.data); // if not first call, just set the pointer to the PS + + if (PartSys == NULL) + { + DEBUG_PRINT(F("ERROR: FX PartSys nullpointer")); + return mode_static(); // something went wrong, no data! + } + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + numSources = PartSys->numSources; + PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur + + uint32_t spacing = PartSys->maxX / numSources; + for(i = 0; i < numSources; i++) + { + PartSys->sources[i].source.hue = i * 16;//random16(); //TODO: make adjustable, maybe even colorcycle? + PartSys->sources[i].var = SEGMENT.speed >> 3; + PartSys->sources[i].minLife = 180 + (SEGMENT.intensity >> 1); + PartSys->sources[i].maxLife = 240 + SEGMENT.intensity; + PartSys->sources[i].sat = 255; + PartSys->sources[i].size = SEGMENT.custom1; + PartSys->setParticleSize(SEGMENT.custom1); + PartSys->sources[i].source.x = (spacing >> 1) + spacing * i; //distribute evenly + } + + for(i = 0; i < PartSys->usedParticles; i++) + { + if(PartSys->particles[i].ttl > 10) PartSys->particles[i].ttl -= 10; //ttl is linked to brightness, this allows to use higher brightness but still a short lifespan + else PartSys->particles[i].ttl = 0; + } + + um_data_t *um_data; + if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) + um_data = simulateSound(SEGMENT.soundSim); // add support for no audio + + uint8_t *fftResult = (uint8_t *)um_data->u_data[2]; // 16 bins with FFT data, log mapped already, each band contains frequency amplitude 0-255 + + //map the bands into 16 positions on x axis, emit some particles according to frequency loudness + i = 0; + uint32_t bin; //current bin + uint32_t threshold = 300 - SEGMENT.intensity; + + + for (bin = 0; bin < numSources; bin++) + { + uint32_t emitparticle = 0; + uint8_t emitspeed = ((uint32_t)fftResult[bin] * (uint32_t)SEGMENT.speed) >> 9; // emit speed according to loudness of band (127 max!) + if (fftResult[bin] > threshold) + { + emitparticle = 1; + } + else if(fftResult[bin] > 0)// band has low volue + { + uint32_t restvolume = ((threshold - fftResult[bin])>>2) + 2; + if (random16() % restvolume == 0) + { + emitparticle = 1; + } + } + + if(emitparticle) + { + PartSys->sprayEmit(PartSys->sources[bin]); + } + } + //TODO: add color control? + + PartSys->update(); // update and render + + return FRAMETIME; +} +static const char _data_FX_MODE_PS_1D_GEQ[] PROGMEM = "PS 1D GEQ@Speed,!,Size,Blur/Overlay,,,,;,!;!;1f;pal=0,sx=50,ix=200,c1=0,c2=0,c3=0,o1=1,o2=1,o3=0"; + #endif //WLED_DISABLE_PARTICLESYSTEM1D @@ -10850,6 +11000,8 @@ addEffect(FX_MODE_PSHOURGLASS, &mode_particleHourglass, _data_FX_MODE_PS_HOURGLA addEffect(FX_MODE_PS1DSPRAY, &mode_particle1Dspray, _data_FX_MODE_PS_1DSPRAY); addEffect(FX_MODE_PSBALANCE, &mode_particleBalance, _data_FX_MODE_PS_BALANCE); addEffect(FX_MODE_PSCHASE, &mode_particleChase, _data_FX_MODE_PS_CHASE); +addEffect(FX_MODE_PSSTARBURST, &mode_particleStarburst, _data_FX_MODE_PS_STARBURST); +addEffect(FX_MODE_PS1DGEQ, &mode_particle1DGEQ, _data_FX_MODE_PS_1D_GEQ); #endif // WLED_DISABLE_PARTICLESYSTEM1D diff --git a/wled00/FX.h b/wled00/FX.h index f9e1f31ee4..6e4eb9cbf9 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -341,7 +341,9 @@ #define FX_MODE_PS1DSPRAY 208 #define FX_MODE_PSBALANCE 209 #define FX_MODE_PSCHASE 210 -#define MODE_COUNT 211 +#define FX_MODE_PSSTARBURST 211 +#define FX_MODE_PS1DGEQ 212 +#define MODE_COUNT 213 typedef enum mapping1D2D { M12_Pixels = 0, From c954b31e7236b7c22628fb4574bb607f97d8ae1c Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sun, 30 Jun 2024 09:53:33 +0200 Subject: [PATCH 106/219] Added more advanced (and proper) size handling in collisions, work in progress - copied and adapted some stuff from 2D system (out of bounds size rendering, dynamic collision detection distance, dynamic wall bounce distance) --- wled00/FX.cpp | 14 +-- wled00/FXparticleSystem.cpp | 177 ++++++++++++++++++------------------ wled00/FXparticleSystem.h | 6 +- 3 files changed, 98 insertions(+), 99 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 0c7a3de6e9..87e2d21254 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -9796,15 +9796,11 @@ uint16_t mode_particleBouncingBalls(void) //if (SEGMENT.check2) PartSys->setMotionBlur(255); //full motion blurring allows overlay (motion blur does not work with overlay) PartSys->sources[0].var = SEGMENT.speed >> 3; PartSys->sources[0].v = (SEGMENT.speed >> 1) - (SEGMENT.speed >> 3); - if (SEGMENT.check1) // collisions enabled - PartSys->enableParticleCollisions(true, 240 + (SEGMENT.custom1>>4)); // enable collisions and set particle collision hardness - else - PartSys->enableParticleCollisions(false); + PartSys->enableParticleCollisions(SEGMENT.check1, 240 + (SEGMENT.custom1>>4)); // enable collisions and set particle collision hardness PartSys->setUsedParticles( 1 + (SEGMENT.intensity >> 3)); // 1 - 32 - PartSys->setParticleSize(SEGMENT.check3); // 1 or 2 pixel rendering - if(SEGMENT.check3) //rolling balls + if(SEGMENT.check2) //rolling balls { PartSys->setGravity(0); bool updatespeed = false; @@ -9858,7 +9854,7 @@ uint16_t mode_particleBouncingBalls(void) PartSys->update(); // update and render return FRAMETIME; } -static const char _data_FX_MODE_PSBOUNCINGBALLS[] PROGMEM = "PS Bouncing Balls@Speed,!,Hardness,Blur/Overlay,Gravity,Collide,Smooth,Rolling;,!;!;1;pal=0,sx=100,c1=240,c2=0,c3=8,o1=0,o2=0,o3=1"; +static const char _data_FX_MODE_PSBOUNCINGBALLS[] PROGMEM = "PS Bouncing Balls@Speed,!,Hardness,Blur/Overlay,Gravity,Collide,Rolling,Smooth;,!;!;1;pal=0,sx=100,c1=240,c2=0,c3=8,o1=0,o2=0,o3=1"; /* Particle Replacement for original Dancing Shadows: @@ -10488,7 +10484,7 @@ uint16_t mode_particleBalance(void) PartSys->update(); // update and render return FRAMETIME; } -static const char _data_FX_MODE_PS_BALANCE[] PROGMEM = "PS 1D Balance@!,!,Collisions,Blur/Overlay,Tilt,Color by Position,Wrap/Bounce,Random;,!;!;1;pal=18,sx=200,ix=40,c1=200,c2=0,c3=5,o1=1,o2=0,o3=1"; +static const char _data_FX_MODE_PS_BALANCE[] PROGMEM = "PS 1D Balance@!,!,Collisions,Blur/Overlay,Tilt,Color by Position,Wrap/Bounce,Random;,!;!;1;pal=18,sx=200,ix=40,c1=200,c2=0,c3=5,o1=1,o2=0,o3=0"; @@ -10579,7 +10575,7 @@ uint16_t mode_particleStarburst(void) if (!initParticleSystem1D(PartSys, 1, 0, true)) // init return mode_static(); // allocation failed PartSys->setKillOutOfBounds(true); - PartSys->enableParticleCollisions(true, 250); + PartSys->enableParticleCollisions(true, 200); PartSys->sources[0].source.ttl = 1; // set initial stanby time PartSys->sources[0].sat = 0; // emitted particles start out white } diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index 42a0ba8927..aa0563f069 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -321,7 +321,7 @@ void ParticleSystem::particleMoveUpdate(PSparticle &part, PSsettings2D *options, if (options->wrapX) { newX = newX % (maxX + 1); - if(newX < 0) + if (newX < 0) newX += maxX + 1; } else if (((newX <= -PS_P_HALFRADIUS) || (newX > maxX + PS_P_HALFRADIUS))) // particle is leaving, set out of bounds if it has fully left @@ -355,7 +355,7 @@ void ParticleSystem::particleMoveUpdate(PSparticle &part, PSsettings2D *options, if (options->wrapY) { newY = newY % (maxY + 1); - if(newY < 0) + if (newY < 0) newY += maxY + 1; } else if (((newY <= -PS_P_HALFRADIUS) || (newY > maxY + PS_P_HALFRADIUS))) // particle is leaving, set out of bounds if it has fully left @@ -1505,7 +1505,7 @@ ParticleSystem1D::ParticleSystem1D(uint16_t length, uint16_t numberofparticles, // update function applies gravity, moves the particles, handles collisions and renders the particles void ParticleSystem1D::update(void) { - + PSadvancedParticle1D *advprop = NULL; // handle collisions (can push particles, must be done before updating particles or they can render out of bounds, causing a crash if using local buffer for speed) if (particlesettings.useCollisions) handleCollisions(); @@ -1517,10 +1517,14 @@ void ParticleSystem1D::update(void) //move all particles for (int i = 0; i < usedParticles; i++) { - particleMoveUpdate(particles[i], &particlesettings); + if (advPartProps) + { + advprop = &advPartProps[i]; + } + particleMoveUpdate(particles[i], &particlesettings, advprop); } - if(particlesettings.colorByPosition) + if (particlesettings.colorByPosition) { for (int i = 0; i < usedParticles; i++) { @@ -1531,7 +1535,7 @@ void ParticleSystem1D::update(void) ParticleSys_render(); uint32_t bg_color = SEGCOLOR(1); //background color, set to black to overlay - if(bg_color > 0) //if not black + if (bg_color > 0) //if not black { for(uint32_t i = 0; i < maxXpixel + 1; i++) { @@ -1593,10 +1597,10 @@ void ParticleSystem1D::setMotionBlur(uint8_t bluramount) void ParticleSystem1D::setParticleSize(uint8_t size) { particlesize = size; - if(particlesize) - particleHardRadius = PS_P_MINHARDRADIUS_1D; - else - particleHardRadius = PS_P_MINHARDRADIUS_1D >> 1; //1 pixel sized particles have half the radius (for collisions & bounce) + particleHardRadius = PS_P_MINHARDRADIUS_1D >> 1; // 1 pixel sized particles have half the radius (for bounce, not for collisions) + if (particlesize) + particleHardRadius = particleHardRadius << 1; // 2 pixel sized particles + //TODO: since global size rendering is always 1 or 2 pixels, this could maybe be made simpler with a bool 'singlepixelsize' } // enable/disable gravity, optionally, set the force (force=8 is default) can be -127 to +127, 0 is disable // if enabled, gravity is applied to all particles in ParticleSystemUpdate() @@ -1649,7 +1653,7 @@ int32_t ParticleSystem1D::sprayEmit(PSsource1D &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 and killoutofbounds is not applied over the top -void ParticleSystem1D::particleMoveUpdate(PSparticle1D &part, PSsettings1D *options) +void ParticleSystem1D::particleMoveUpdate(PSparticle1D &part, PSsettings1D *options, PSadvancedParticle1D *advancedproperties) { if (options == NULL) options = &particlesettings; //use PS system settings by default @@ -1659,31 +1663,42 @@ void ParticleSystem1D::particleMoveUpdate(PSparticle1D &part, PSsettings1D *opti part.ttl--; // age if (particlesettings.colorByAge) part.hue = part.ttl > 250 ? 250 : part.ttl; //set color to ttl - - //bool usesize = false; // particle uses individual size rendering + + bool usesize = false; // particle uses individual size rendering int32_t newX = part.x + (int16_t)part.vx; part.outofbounds = 0; // reset out of bounds (in case particle was created outside the matrix and is now moving into view) + if (advancedproperties) //using individual particle size? + { + particleHardRadius = PS_P_MINHARDRADIUS_1D + advancedproperties->size; + if (advancedproperties->size > 1) + { + usesize = true; // note: variable eases out of frame checking below + } + else if (advancedproperties->size == 0) // single pixel particles use half the collision distance for walls + particleHardRadius = PS_P_MINHARDRADIUS_1D >> 1; + } + // if wall collisions are enabled, bounce them before they reach the edge, it looks much nicer if the particle is not half out of view if (options->bounceX) { if ((newX < particleHardRadius) || ((newX > maxX - particleHardRadius))) // reached a wall { bool bouncethis = true; - if(options->useGravity) + if (options->useGravity) { - if(part.reversegrav) //skip at x = 0 + if (part.reversegrav) //skip at x = 0 { - if(newX < particleHardRadius) + if (newX < particleHardRadius) bouncethis = false; } else //skip at x = max { - if(newX > particleHardRadius) + if (newX > particleHardRadius) bouncethis = false; } } - if(bouncethis) + if (bouncethis) { part.vx = -part.vx; //invert speed part.vx = ((int32_t)part.vx * wallHardness) / 255; // reduce speed as energy is lost on non-hard surface @@ -1699,35 +1714,44 @@ void ParticleSystem1D::particleMoveUpdate(PSparticle1D &part, PSsettings1D *opti if (options->wrapX) { newX = newX % (maxX + 1); - if(newX < 0) + if (newX < 0) newX += maxX + 1; Serial.println(newX/32); } else if (((newX <= -PS_P_HALFRADIUS_1D) || (newX > maxX + PS_P_HALFRADIUS_1D))) // particle is leaving, set out of bounds if it has fully left { - part.outofbounds = 1; - if (options->killoutofbounds) - { - bool killthis = true; - if(options->useGravity) //if gravity is used, only kill below 'floor level' - { - if(part.reversegrav) //skip at x = 0 - { - if(newX < 0) - killthis = false; - } - else //skip at x = max + bool isleaving = true; + if (usesize) // using individual particle size + { + if (((newX > -particleHardRadius) && (newX < maxX + particleHardRadius))) // large particle is not yet leaving the view - note: this is not pixel perfect but good enough + isleaving = false; + } + if (isleaving) + { + part.outofbounds = 1; + if (options->killoutofbounds) + { + bool killthis = true; + if (options->useGravity) //if gravity is used, only kill below 'floor level' { - if(newX > 0) - killthis = false; + if (part.reversegrav) //skip at x = 0 + { + if (newX < 0) + killthis = false; + } + else //skip at x = max + { + if (newX > 0) + killthis = false; + } } + if (killthis) + part.ttl = 0; } - if(killthis) - part.ttl = 0; } } } - if(!part.fixed) + if (!part.fixed) part.x = (int16_t)newX; // set new position else part.vx = 0; //set speed to zero. note: particle can get speed in collisions, if unfixed, it should not speed away @@ -1768,7 +1792,7 @@ void ParticleSystem1D::applyGravity() for (uint32_t i = 0; i < usedParticles; i++) { int32_t dv = dv_raw; - if(particles[i].reversegrav) dv = -dv_raw; + if (particles[i].reversegrav) dv = -dv_raw; // 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].vx = limitSpeed((int32_t)particles[i].vx - dv); } @@ -1780,7 +1804,7 @@ void ParticleSystem1D::applyGravity(PSparticle1D *part) { uint32_t counterbkp = gforcecounter; int32_t dv = calcForce_dv(gforce, &gforcecounter); - if(part->reversegrav) dv = -dv; + if (part->reversegrav) dv = -dv; gforcecounter = counterbkp; //save it back part->vx = limitSpeed((int32_t)part->vx - dv); } @@ -1856,7 +1880,7 @@ void ParticleSystem1D::ParticleSys_render() brightness = particles[i].ttl > 255 ? 255 : particles[i].ttl; //faster then using min() baseRGB = ColorFromPalette(SEGPALETTE, particles[i].hue, 255, LINEARBLEND); - if(advPartProps) //saturation is advanced property in 1D system + if (advPartProps) //saturation is advanced property in 1D system { if (advPartProps[i].sat < 255) { @@ -1883,10 +1907,15 @@ void ParticleSystem1D::ParticleSys_render() // calculate pixel positions and brightness distribution and render the particle to local buffer or global buffer void ParticleSystem1D::renderParticle(CRGB *framebuffer, uint32_t particleindex, uint32_t brightness, CRGB color, CRGB *renderbuffer) { - if(particlesize == 0) //single pixel particle, can be out of bounds as oob checking is made for 2-pixel particles + uint32_t size = particlesize; + if (advPartProps) // use advanced size properties + { + size = advPartProps[particleindex].size; + } + if (size == 0) //single pixel particle, can be out of bounds as oob checking is made for 2-pixel particles { uint32_t x = particles[particleindex].x >> PS_P_RADIUS_SHIFT_1D; - if(x <= maxXpixel) //by making x unsigned there is no need to check < 0 as it will overflow + if (x <= maxXpixel) //by making x unsigned there is no need to check < 0 as it will overflow { if (framebuffer) fast_color_add(framebuffer[x], color, brightness); @@ -1980,7 +2009,7 @@ void ParticleSystem1D::renderParticle(CRGB *framebuffer, uint32_t particleindex, { if (particlesettings.wrapX) // wrap x to the other side if required { - if(xfb > maxXpixel << 1) // xfb is "negative" (note: for some reason, this check is needed in 1D but works without in 2D...) + if (xfb > maxXpixel << 1) // xfb is "negative" (note: for some reason, this check is needed in 1D but works without in 2D...) xfb = (maxXpixel +1) + (int32_t)xfb; else xfb = xfb % (maxXpixel + 1); @@ -2024,12 +2053,8 @@ void ParticleSystem1D::handleCollisions() startparticle = endparticle; endparticle = usedParticles; } */ - int32_t proximity = particleHardRadius; //PS_P_MINHARDRADIUS_1D; - - if(particlesize == 0) - proximity = PS_P_MINHARDRADIUS_1D; //1 pixel sized particles have half the radius, need to increase detection distance it for proper collisions (and stacking) - //proximity = particlesize; - + int32_t collisiondistance = PS_P_MINHARDRADIUS_1D; + 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 @@ -2040,13 +2065,18 @@ void ParticleSystem1D::handleCollisions() { if (particles[j].ttl > 0 && particles[j].collide) // if target particle is alive { + if (advPartProps) // use advanced size properties + { + collisiondistance = PS_P_MINHARDRADIUS_1D + (((uint32_t)advPartProps[i].size + (uint32_t)advPartProps[j].size)>>1); + } dx = particles[j].x - particles[i].x; int32_t dv = (int32_t)particles[j].vx - (int32_t)particles[i].vx; - if(dv >= proximity) //particles would go past each other in next move upate + int32_t proximity = collisiondistance; + if (dv >= proximity) //particles would go past each other in next move upate proximity += abs(dv); //add speed difference to catch fast particles if (dx < proximity && dx > -proximity) // check if close { - collideParticles(&particles[i], &particles[j], dx, dv); + collideParticles(&particles[i], &particles[j], dx, dv, collisiondistance); } } } @@ -2056,7 +2086,7 @@ void ParticleSystem1D::handleCollisions() // handle a collision if close proximity is detected, i.e. dx and/or dy smaller than 2*PS_P_RADIUS // takes two pointers to the particles to collide and the particle hardness (softer means more energy lost in collision, 255 means full hard) -void ParticleSystem1D::collideParticles(PSparticle1D *particle1, PSparticle1D *particle2, int32_t dx, int32_t relativeVx) +void ParticleSystem1D::collideParticles(PSparticle1D *particle1, PSparticle1D *particle2, int32_t dx, int32_t relativeVx, uint32_t collisiondistance) { // Calculate dot product of relative velocity and relative distance @@ -2068,34 +2098,17 @@ void ParticleSystem1D::collideParticles(PSparticle1D *particle1, PSparticle1D *p // integer math used to avoid floats. // Calculate new velocities after collision uint32_t surfacehardness = collisionHardness < PS_P_MINSURFACEHARDNESS_1D ? PS_P_MINSURFACEHARDNESS_1D : collisionHardness; // if particles are soft, the impulse must stay above a limit or collisions slip through - //TODO: if soft collisions are not needed, the above line can be done in sethardness function and skipped here. - - - //particles slow down due to collision losses - //particle1->vx = ((int32_t)particle1->vx * surfacehardness) / 255; - //particle2->vx = ((int32_t)particle2->vx * surfacehardness) / 255; - - //calculate (soft) collision impulse and add it to the speed - //int32_t impulse = (((((relativeVx) << 15)) * surfacehardness) >> 23); //note: this calculation is biased slightly towards negative numbers due to bit shifts, which is not relevant in 1D system - //int32_t impulse = (((((relativeVx) << 15)) * surfacehardness) / 8388607); - int32_t impulse = relativeVx * surfacehardness / 255; - //int32_t impulse = relativeVx; //note: applying full impulse and hardness afterwards results in mid-movement stopping, so no good! - + //TODO: if soft collisions are not needed, the above line can be done in set hardness function and skipped here (which is what it currently looks like) + + int32_t impulse = relativeVx * surfacehardness / 255; particle1->vx += impulse; particle2->vx -= impulse; - //particle1->vx = particle1->vx * surfacehardness / 255; //apply hardness - //particle2->vx = particle2->vx * surfacehardness / 255; - + //if one of the particles is fixed, transfer the impulse back so it bounces - if(particle1->fixed) + if (particle1->fixed) particle2->vx = -particle1->vx; - //particle2->vx -= impulse; - else if(particle2->fixed) + else if (particle2->fixed) particle1->vx = -particle2->vx; - //particle1->vx += impulse; - -// particle1->vx = (particle1->vx < 5 && particle1->vx > -5) ? 0 : particle1->vx; -// particle2->vx = (particle2->vx < 5 && particle2->vx > -5) ? 0 : particle2->vx; if (collisionHardness < PS_P_MINSURFACEHARDNESS_1D) // if particles are soft, they become 'sticky' i.e. apply some friction (they do pile more nicely and correctly) { @@ -2103,22 +2116,12 @@ void ParticleSystem1D::collideParticles(PSparticle1D *particle1, PSparticle1D *p particle1->vx = ((int32_t)particle1->vx * coeff) / 255; particle2->vx = ((int32_t)particle2->vx * coeff) / 255; } -/* - if (collisionHardness < 100) // if they are very soft, stop slow particles completely to make them stick to each other - { - particle1->vx = (particle1->vx < 4 && particle1->vx > -4) ? 0 : particle1->vx; - particle2->vx = (particle2->vx < 4 && particle2->vx > -4) ? 0 : particle2->vx; - }*/ - // } - } uint32_t distance = abs(dx); - uint32_t collisiondistance = particleHardRadius; - if(particlesize == 0) //single pixel particles - collisiondistance = PS_P_RADIUS_1D; //single pixel particles have a radius of PS_P_RADIUS_1D/2, use diameter for proper stacking + // particles have volume, push particles apart if they are too close // behaviour is different than in 2D, we need pixel accurate stacking here, push the top particle to full radius (direction is well defined in 1D) // also need to give the top particle some speed to counteract gravity or stacks just collapse @@ -2126,7 +2129,7 @@ void ParticleSystem1D::collideParticles(PSparticle1D *particle1, PSparticle1D *p { int32_t pushamount = 1 + ((collisiondistance - distance) >> 1); //add half the remaining distance note: this works best, if less or more is added, it gets more chaotic //int32_t pushamount = collisiondistance - distance; - if(particlesettings.useGravity) //using gravity, push the 'upper' particle only + if (particlesettings.useGravity) //using gravity, push the 'upper' particle only { if (dx < 0) // particle2.x < particle1.x { @@ -2143,7 +2146,7 @@ void ParticleSystem1D::collideParticles(PSparticle1D *particle1, PSparticle1D *p } else { - if(particle1->reversegrav && !particle1->fixed) + if (particle1->reversegrav && !particle1->fixed) { particle1->x -= pushamount; particle1->vx--; diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index af1a4bf54f..f96b387e17 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -312,7 +312,7 @@ class ParticleSystem1D // particle emitters int32_t sprayEmit(PSsource1D &emitter); - void particleMoveUpdate(PSparticle1D &part, PSsettings1D *options = NULL); // move function + void particleMoveUpdate(PSparticle1D &part, PSsettings1D *options = NULL, PSadvancedParticle1D *advancedproperties = NULL); // move function //particle physics void applyForce(PSparticle1D *part, int8_t xforce, uint8_t *counter); //apply a force to a single particle void applyForce(int8_t xforce); // apply a force to all particles @@ -330,7 +330,7 @@ class ParticleSystem1D void setColorByAge(bool enable); void setColorByPosition(bool enable); void setMotionBlur(uint8_t bluramount); // note: motion blur can only be used if 'particlesize' is set to zero - void setParticleSize(uint8_t size); //size 0 = 1 pixel, size 1 = 2 pixels + void setParticleSize(uint8_t size); //size 0 = 1 pixel, size 1 = 2 pixels, is overruled by advanced particle size void setGravity(int8_t force = 8); void enableParticleCollisions(bool enable, uint8_t hardness = 255); @@ -353,7 +353,7 @@ class ParticleSystem1D //paricle physics applied by system if flags are set void applyGravity(); // applies gravity to all particles void handleCollisions(); - void collideParticles(PSparticle1D *particle1, PSparticle1D *particle2, int32_t dx, int32_t relativeVx); + void collideParticles(PSparticle1D *particle1, PSparticle1D *particle2, int32_t dx, int32_t relativeV, uint32_t collisiondistance); //utility functions void updatePSpointers(bool isadvanced); // update the data pointers to current segment data space From 26c11fc120d47e9fc92c4e57ca4268ae9fc85460 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sun, 30 Jun 2024 12:23:13 +0200 Subject: [PATCH 107/219] fixed some parameters in 1D FX --- wled00/FX.cpp | 32 ++++++++++++++++---------------- wled00/FXparticleSystem.cpp | 17 +++++++---------- 2 files changed, 23 insertions(+), 26 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 87e2d21254..5bfd88b588 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -9664,7 +9664,7 @@ uint16_t mode_particleDrip(void) return mode_static(); // allocation failed; //allocation failed PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting) PartSys->sources[0].source.hue = random16(); - SEGMENT.aux0 = 1; //must not be zero or "% 0" happens below which crashes on ESP32 + SEGMENT.aux1 = 0xFFFF; // invalidate } else PartSys = reinterpret_cast(SEGMENT.data); // if not first call, just set the pointer to the PS @@ -9679,9 +9679,6 @@ uint16_t mode_particleDrip(void) PartSys->updateSystem(); // update system properties (dimensions and data pointers) PartSys->setBounce(true); PartSys->setWallHardness(50); - // PartSys->setWrap(SEGMENT.check2); - - //numSprays = min(PartSys->numSources, (uint8_t)1); // number of sprays PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur PartSys->setGravity(SEGMENT.custom3>>1); // set gravity (8 is default strength) @@ -9692,7 +9689,6 @@ uint16_t mode_particleDrip(void) else PartSys->enableParticleCollisions(false); - PartSys->sources[0].source.collide = false; //drops do not collide if(SEGMENT.check1) //rain mode, emit at random position, short life (3-8 seconds at 50fps) @@ -9714,11 +9710,15 @@ uint16_t mode_particleDrip(void) PartSys->sources[0].source.x = PartSys->maxX - PS_P_RADIUS_1D; } + if(SEGMENT.aux1 != SEGMENT.intensity) //slider changed + SEGMENT.aux0 = 1; //must not be zero or "% 0" happens below which crashes on ESP32 + SEGMENT.aux1 = SEGMENT.intensity; + // every nth frame emit a particle if (SEGMENT.call % SEGMENT.aux0 == 0) { - int32_t interval = 256 / (SEGMENT.intensity + 1); - SEGMENT.aux0 = interval + random(interval + 5); + int32_t interval = 1024 / ((SEGMENT.intensity) + 1); + SEGMENT.aux0 = interval + random(interval + 5); PartSys->sources[0].source.hue = random16(); //set random color TODO: maybe also not random but color cycling? need another slider or checkmark for this. PartSys->sprayEmit(PartSys->sources[0]); } @@ -9727,7 +9727,7 @@ uint16_t mode_particleDrip(void) { if(PartSys->particles[i].ttl && PartSys->particles[i].collide == false) // use collision flag to identify splash particles { - if(SEGMENT.custom1 > 0 && PartSys->particles[i].x < (PS_P_RADIUS_1D << 1)) //splash enabled and reached bottom -> does not work this way, all splashes will splash again... need to mark particles + if(SEGMENT.custom1 > 0 && PartSys->particles[i].x < (PS_P_RADIUS_1D << 1)) //splash enabled and reached bottom { PartSys->particles[i].ttl = 0; //kill origin particle PartSys->sources[0].maxLife = 80; @@ -9751,7 +9751,7 @@ uint16_t mode_particleDrip(void) PartSys->update(); // update and render return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEDRIP[] PROGMEM = "PS DripDrop@Speed,!,Splash,Blur/Overlay,Gravity,Rain,PushSplash,Smooth;,!;!;1;pal=0,sx=150,ix=150,c1=220,c2=30,c3=21,o1=0,o2=0,o3=0"; +static const char _data_FX_MODE_PARTICLEDRIP[] PROGMEM = "PS DripDrop@Speed,!,Splash,Blur/Overlay,Gravity,Rain,PushSplash,Smooth;,!;!;1;pal=0,sx=150,ix=25,c1=220,c2=30,c3=21,o1=0,o2=0,o3=0"; /* @@ -9789,14 +9789,14 @@ uint16_t mode_particleBouncingBalls(void) } // Particle System settings + uint32_t hardness = 240 + (SEGMENT.custom1>>4); PartSys->updateSystem(); // update system properties (dimensions and data pointers) - PartSys->setWallHardness(240 + (SEGMENT.custom1>>4)); + PartSys->setWallHardness(hardness); PartSys->setGravity(1 + (SEGMENT.custom3 >> 1)); // set gravity (8 is default strength) - PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur - //if (SEGMENT.check2) PartSys->setMotionBlur(255); //full motion blurring allows overlay (motion blur does not work with overlay) + PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur PartSys->sources[0].var = SEGMENT.speed >> 3; PartSys->sources[0].v = (SEGMENT.speed >> 1) - (SEGMENT.speed >> 3); - PartSys->enableParticleCollisions(SEGMENT.check1, 240 + (SEGMENT.custom1>>4)); // enable collisions and set particle collision hardness + PartSys->enableParticleCollisions(SEGMENT.check1, hardness - 1); // enable collisions and set particle collision hardness (do not use full hardness or particles speed up due to pushing, can not be made perfectly balanced) PartSys->setUsedParticles( 1 + (SEGMENT.intensity >> 3)); // 1 - 32 PartSys->setParticleSize(SEGMENT.check3); // 1 or 2 pixel rendering @@ -9816,7 +9816,7 @@ uint16_t mode_particleBouncingBalls(void) PartSys->particles[i].ttl = 260; //set alive at full intensity if(updatespeed || PartSys->particles[i].ttl == 0) //speed changed or particle died, reset TTL and speed { - PartSys->particles[i].ttl = 260; + PartSys->particles[i].ttl = 260 + SEGMENT.speed; PartSys->particles[i].collide = true; int32_t newspeed = random(20 + (SEGMENT.speed >> 2)) + (SEGMENT.speed >> 3); PartSys->particles[i].vx = PartSys->particles[i].vx > 0 ? newspeed : -newspeed; //keep the direction @@ -9854,7 +9854,7 @@ uint16_t mode_particleBouncingBalls(void) PartSys->update(); // update and render return FRAMETIME; } -static const char _data_FX_MODE_PSBOUNCINGBALLS[] PROGMEM = "PS Bouncing Balls@Speed,!,Hardness,Blur/Overlay,Gravity,Collide,Rolling,Smooth;,!;!;1;pal=0,sx=100,c1=240,c2=0,c3=8,o1=0,o2=0,o3=1"; +static const char _data_FX_MODE_PSBOUNCINGBALLS[] PROGMEM = "PS Bouncing Balls@Speed,!,Hardness,Blur/Overlay,Gravity,Collide,Rolling,Smooth;,!;!;1;pal=0,sx=100,ix=180,c1=240,c2=0,c3=8,o1=0,o2=0,o3=1"; /* Particle Replacement for original Dancing Shadows: @@ -10100,7 +10100,7 @@ uint16_t mode_particleFireworks1D(void) PartSys->update(); // update and render return FRAMETIME; } -static const char _data_FX_MODE_PS_FIREWORKS1D[] PROGMEM = "PS Fireworks 1D@Gravity,Explosion,Firing side,Blur/Overlay,Saturation,Gravity,Colorful,Smooth;,!;!;1;pal=0,sx=150,ix=150,c1=220,c2=30,c3=21,o1=0,o2=0,o3=0"; +static const char _data_FX_MODE_PS_FIREWORKS1D[] PROGMEM = "PS Fireworks 1D@Gravity,Explosion,Firing side,Blur/Overlay,Saturation,Gravity,Colorful,Smooth;,!;!;1;pal=0,sx=150,ix=150,c1=220,c2=30,c3=21,o1=0,o2=1,o3=0"; /* diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index aa0563f069..ecda701b4b 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -1619,7 +1619,7 @@ void ParticleSystem1D::setGravity(int8_t force) void ParticleSystem1D::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; + collisionHardness = hardness; } // emit one particle with variation, returns index of last emitted particle (or -1 if no particle emitted) @@ -2067,7 +2067,7 @@ void ParticleSystem1D::handleCollisions() { if (advPartProps) // use advanced size properties { - collisiondistance = PS_P_MINHARDRADIUS_1D + (((uint32_t)advPartProps[i].size + (uint32_t)advPartProps[j].size)>>1); + collisiondistance += ((uint32_t)advPartProps[i].size + (uint32_t)advPartProps[j].size)>>1; } dx = particles[j].x - particles[i].x; int32_t dv = (int32_t)particles[j].vx - (int32_t)particles[i].vx; @@ -2115,13 +2115,10 @@ void ParticleSystem1D::collideParticles(PSparticle1D *particle1, PSparticle1D *p const uint32_t coeff = collisionHardness + (255 - PS_P_MINSURFACEHARDNESS_1D); particle1->vx = ((int32_t)particle1->vx * coeff) / 255; particle2->vx = ((int32_t)particle2->vx * coeff) / 255; - } - + } } - - uint32_t distance = abs(dx); - + uint32_t distance = abs(dx); // particles have volume, push particles apart if they are too close // behaviour is different than in 2D, we need pixel accurate stacking here, push the top particle to full radius (direction is well defined in 1D) // also need to give the top particle some speed to counteract gravity or stacks just collapse @@ -2158,11 +2155,11 @@ void ParticleSystem1D::collideParticles(PSparticle1D *particle1, PSparticle1D *p } } } - else //not using gravity, push both particles by applying a velocity (like in 2D system), results in much nicer stacking + else //not using gravity, push both particles by applying a little velocity (like in 2D system), results in much nicer stacking when applying forces { - pushamount = 1 + (pushamount >> 2); + pushamount = 1; if (dx < 0) // particle2.x < particle1.x - pushamount = -pushamount; + pushamount = -1; particle1->vx -= pushamount; particle2->vx += pushamount; From e113b17113aefd302aee2368c11c137db97fd110 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sun, 30 Jun 2024 15:48:11 +0200 Subject: [PATCH 108/219] added #ifndef to disable FX replaced by PS - define DISABLE_1D_PS_REPLACEMENTS or DISABLE_2D_PS_REPLACEMENTS --- wled00/FX.cpp | 121 +++++++++++++++++++++--------------- wled00/FXparticleSystem.cpp | 20 +++--- 2 files changed, 80 insertions(+), 61 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 6347c5a8dc..c7c6c58d32 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -1189,7 +1189,7 @@ uint16_t mode_dual_larson_scanner(void){ } static const char _data_FX_MODE_DUAL_LARSON_SCANNER[] PROGMEM = "Scanner Dual@!,Trail,Delay,,,Dual,Bi-delay;!,!,!;!;;m12=0,c1=0"; - +#ifndef DISABLE_1D_PS_REPLACEMENTS /* * Firing comets from one end. "Lighthouse" */ @@ -1216,7 +1216,7 @@ uint16_t mode_comet(void) { return FRAMETIME; } static const char _data_FX_MODE_COMET[] PROGMEM = "Lighthouse@!,Fade rate;!,!;!"; - +#endif // DISABLE_1D_PS_REPLACEMENTS /* * Fireworks function. @@ -1258,7 +1258,7 @@ uint16_t mode_fireworks() { } static const char _data_FX_MODE_FIREWORKS[] PROGMEM = "Fireworks@,Frequency;!,!;!;12;ix=192,pal=11"; - +#ifndef DISABLE_1D_PS_REPLACEMENTS //Twinkling LEDs running. Inspired by https://github.com/kitesurfer1404/WS2812FX/blob/master/src/custom/Rain.h uint16_t mode_rain() { if (SEGLEN == 1) return mode_static(); @@ -1292,7 +1292,7 @@ uint16_t mode_rain() { return mode_fireworks(); } static const char _data_FX_MODE_RAIN[] PROGMEM = "Rain@!,Spawning rate;!,!;!;12;ix=128,pal=0"; - +#endif //DISABLE_1D_PS_REPLACEMENTS /* * Fire flicker function @@ -2038,7 +2038,7 @@ uint16_t mode_palette() { } static const char _data_FX_MODE_PALETTE[] PROGMEM = "Palette@Shift,Size,Rotation,,,Animate Shift,Animate Rotation,Anamorphic;;!;12;c1=128,c2=128,c3=128,o1=1,o2=1,o3=0"; - +#ifndef DISABLE_2D_PS_REPLACEMENTS // WLED limitation: Analog Clock overlay will NOT work when Fire2012 is active // Fire2012 by Mark Kriegsman, July 2012 // as part of "Five Elements" shown here: http://youtu.be/knWiGsmgycY @@ -2120,7 +2120,7 @@ uint16_t mode_fire_2012() { return FRAMETIME; } static const char _data_FX_MODE_FIRE_2012[] PROGMEM = "Fire 2012@Cooling,Spark rate,,,Boost;;!;1;sx=64,ix=160,m12=1"; // bars - +#endif //DISABLE_2D_PS_REPLACEMENTS // ColorWavesWithPalettes by Mark Kriegsman: https://gist.github.com/kriegsman/8281905786e8b2632aeb // This function draws color waves with an ever-changing, @@ -2939,7 +2939,7 @@ uint16_t mode_spots_fade() } static const char _data_FX_MODE_SPOTS_FADE[] PROGMEM = "Spots Fade@Spread,Width,,,,,Overlay;!,!;!"; - +#ifndef DISABLE_1D_PS_REPLACEMENTS //each needs 12 bytes typedef struct Ball { unsigned long lastBounceTime; @@ -3125,7 +3125,7 @@ static uint16_t rolling_balls(void) { return FRAMETIME; } static const char _data_FX_MODE_ROLLINGBALLS[] PROGMEM = "Rolling Balls@!,# of balls,,,,Collisions,Overlay,Trails;!,!,!;!;1;m12=1"; //bar - +#endif //DISABLE_1D_PS_REPLACEMENTS /* * Sinelon stolen from FASTLED examples @@ -3220,7 +3220,7 @@ uint16_t mode_solid_glitter() } static const char _data_FX_MODE_SOLID_GLITTER[] PROGMEM = "Solid Glitter@,!;Bg,,Glitter color;;;m12=0"; - +#ifndef DISABLE_1D_PS_REPLACEMENTS //each needs 20 bytes //Spark type is used for popcorn, 1D fireworks, and drip typedef struct Spark { @@ -3297,6 +3297,7 @@ uint16_t mode_popcorn(void) { } static const char _data_FX_MODE_POPCORN[] PROGMEM = "Popcorn@!,!,,,,,Overlay;!,!,!;!;;m12=1"; //bar +#endif // DISABLE_1D_PS_REPLACEMENTS //values close to 100 produce 5Hz flicker, which looks very candle-y //Inspired by https://github.com/avanhanegem/ArduinoCandleEffectNeoPixel @@ -3390,7 +3391,7 @@ uint16_t mode_candle_multi() } static const char _data_FX_MODE_CANDLE_MULTI[] PROGMEM = "Candle Multi@!,!;!,!;!;;sx=96,ix=224,pal=0"; - +#ifndef DISABLE_1D_PS_REPLACEMENTS /* / Fireworks in starburst effect / based on the video: https://www.reddit.com/r/arduino/comments/c3sd46/i_made_this_fireworks_effect_for_my_led_strips/ @@ -3524,7 +3525,6 @@ uint16_t mode_starburst(void) { #undef STARBURST_MAX_FRAG static const char _data_FX_MODE_STARBURST[] PROGMEM = "Fireworks Starburst@Chance,Fragments,,,,,Overlay;,!;!;;pal=11,m12=0"; - /* * Exploding fireworks effect * adapted from: http://www.anirama.com/1000leds/1d-fireworks/ @@ -3662,7 +3662,6 @@ uint16_t mode_exploding_fireworks(void) #undef MAX_SPARKS static const char _data_FX_MODE_EXPLODING_FIREWORKS[] PROGMEM = "Fireworks 1D@Gravity,Firing side;!,!;!;12;pal=11,ix=128"; - /* * Drip Effect * ported of: https://www.youtube.com/watch?v=sru2fXh4r7k @@ -3748,7 +3747,7 @@ uint16_t mode_drip(void) return FRAMETIME; } static const char _data_FX_MODE_DRIP[] PROGMEM = "Drip@Gravity,# of drips,,,,,Overlay;!,!;!;;m12=1"; //bar - +#endif // DISABLE_1D_PS_REPLACEMENTS /* * Tetris or Stacking (falling bricks) Effect * by Blaz Kristan (AKA blazoncek) (https://github.com/blazoncek, https://blaz.at/home) @@ -4303,17 +4302,6 @@ uint16_t mode_chunchun(void) } static const char _data_FX_MODE_CHUNCHUN[] PROGMEM = "Chunchun@!,Gap size;!,!;!"; - -//13 bytes -typedef struct Spotlight { - float speed; - uint8_t colorIdx; - int16_t position; - unsigned long lastUpdateTime; - uint8_t width; - uint8_t type; -} spotlight; - #define SPOT_TYPE_SOLID 0 #define SPOT_TYPE_GRADIENT 1 #define SPOT_TYPE_2X_GRADIENT 2 @@ -4327,6 +4315,17 @@ typedef struct Spotlight { #define SPOT_MAX_COUNT 49 //Number of simultaneous waves #endif +#ifndef DISABLE_1D_PS_REPLACEMENTS +//13 bytes +typedef struct Spotlight { + float speed; + uint8_t colorIdx; + int16_t position; + unsigned long lastUpdateTime; + uint8_t width; + uint8_t type; +} spotlight; + /* * Spotlights moving back and forth that cast dancing shadows. * Shine this through tree branches/leaves or other close-up objects that cast @@ -4451,7 +4450,7 @@ uint16_t mode_dancing_shadows(void) return FRAMETIME; } static const char _data_FX_MODE_DANCING_SHADOWS[] PROGMEM = "Dancing Shadows@!,# of shadows;!;!"; - +#endif //DISABLE_1D_PS_REPLACEMENTS /* Imitates a washing machine, rotating same waves forward, then pause, then backward. @@ -5902,6 +5901,7 @@ uint16_t mode_2Dcrazybees(void) { static const char _data_FX_MODE_2DCRAZYBEES[] PROGMEM = "Crazy Bees@!,Blur;;;2"; #undef MAX_BEES +#ifndef DISABLE_2D_PS_REPLACEMENTS ///////////////////////// // 2D Ghost Rider // ///////////////////////// @@ -5992,7 +5992,9 @@ uint16_t mode_2Dghostrider(void) { } static const char _data_FX_MODE_2DGHOSTRIDER[] PROGMEM = "Ghost Rider@Fade rate,Blur;;!;2"; #undef LIGHTERS_AM +#endif //DISABLE_2D_PS_REPLACEMENTS +#ifndef DISABLE_2D_PS_REPLACEMENTS //////////////////////////// // 2D Floating Blobs // //////////////////////////// @@ -6092,7 +6094,7 @@ uint16_t mode_2Dfloatingblobs(void) { } static const char _data_FX_MODE_2DBLOBS[] PROGMEM = "Blobs@!,# blobs,Blur,Trail;!;!;2;c1=8"; #undef MAX_BLOBS - +#endif //DISABLE_2D_PS_REPLACEMENTS //////////////////////////// // 2D Scrolling text // @@ -8603,7 +8605,7 @@ uint16_t mode_particlebox(void) PartSys->setBounceY(true); // set max number of particles and save to aux1 for later #ifdef ESP8266 - SEGMENT.aux1 = min((uint16_t)((PartSys->maxXpixel * PartSys->maxYpixel) >> 1), PartSys->numParticles); + SEGMENT.aux1 = min((uint32_t)((PartSys->maxXpixel * PartSys->maxYpixel) >> 1), PartSys->numParticles); #else SEGMENT.aux1 = min((uint32_t)((PartSys->maxXpixel * PartSys->maxYpixel)), PartSys->numParticles); // max number of particles #endif @@ -9471,7 +9473,7 @@ uint16_t mode_particleghostrider(void) // color by age (PS 'color by age' always starts with hue = 255, don't want that here) if(SEGMENT.check1) { - for(int i = 0; i < PartSys->usedParticles; i++) + for(uint32_t i = 0; i < PartSys->usedParticles; i++) { PartSys->particles[i].hue = PartSys->sources[0].source.hue + (PartSys->particles[i].ttl<<2); } @@ -9697,8 +9699,8 @@ uint16_t mode_particleDrip(void) PartSys->sources[0].var = 5; PartSys->sources[0].v = -(8 + (SEGMENT.speed >> 2)); //speed + var must be < 128, inverted speed (=down) // lifetime in frames - PartSys->sources[0].minLife = 100; - PartSys->sources[0].maxLife = 300; + PartSys->sources[0].minLife = 30; + PartSys->sources[0].maxLife = 200; PartSys->sources[0].source.x = random(PartSys->maxX); //random emit position } else{ //drip @@ -9716,13 +9718,16 @@ uint16_t mode_particleDrip(void) // every nth frame emit a particle if (SEGMENT.call % SEGMENT.aux0 == 0) { - int32_t interval = 1024 / ((SEGMENT.intensity) + 1); + int32_t interval = 300 / ((SEGMENT.intensity) + 1); SEGMENT.aux0 = interval + random(interval + 5); + // if(SEGMENT.check1) // rain mode + // PartSys->sources[0].source.hue = 0; + // else PartSys->sources[0].source.hue = random16(); //set random color TODO: maybe also not random but color cycling? need another slider or checkmark for this. PartSys->sprayEmit(PartSys->sources[0]); } - for (int i = 0; i < PartSys->usedParticles; i++)//check all particles + for (uint32_t i = 0; i < PartSys->usedParticles; i++)//check all particles { if(PartSys->particles[i].ttl && PartSys->particles[i].collide == false) // use collision flag to identify splash particles { @@ -9741,7 +9746,13 @@ uint16_t mode_particleDrip(void) PartSys->sprayEmit(PartSys->sources[0]); } } - } + } + + if(SEGMENT.check1) //rain mode, fade hue to max + { + if(PartSys->particles[i].hue < 245) + PartSys->particles[i].hue += 8; + } //increase speed on high settings by calling the move function twice if(SEGMENT.speed > 200) PartSys->particleMoveUpdate(PartSys->particles[i]); @@ -9844,7 +9855,7 @@ uint16_t mode_particleBouncingBalls(void) //increase speed on high settings by calling the move function twice if(SEGMENT.speed > 200) { - for (int i = 0; i < PartSys->usedParticles; i++)//move all particles + for (uint32_t i = 0; i < PartSys->usedParticles; i++)//move all particles { PartSys->particleMoveUpdate(PartSys->particles[i]); } @@ -9911,7 +9922,7 @@ uint16_t mode_particleDancingShadows(void) } PartSys->sources[0].v = speed; //emitted particle speed PartSys->sources[0].source.hue = random8(); //random spotlight color - for (int i = 0; i < width; i++) + for (uint32_t i = 0; i < width; i++) { switch (type) { case SPOT_TYPE_SOLID: @@ -9953,7 +9964,7 @@ uint16_t mode_particleDancingShadows(void) } //kill out of bounds and moving away plus change color - for (int i = 0; i < PartSys->usedParticles; i++) + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { if(PartSys->particles[i].outofbounds) //check if out of bounds particle move away from strip (i.e. vx < 0 && x > 0 or vx > 0 and x < 0) { @@ -10120,7 +10131,7 @@ uint16_t mode_particleSparkler(void) if (SEGMENT.call == 0) // initialization { - if (!initParticleSystem1D(PartSys, 8, 0, true)) // init, no additional data needed + if (!initParticleSystem1D(PartSys, 16, 0, true)) // init, no additional data needed return mode_static(); // allocation failed } else @@ -10254,7 +10265,7 @@ uint16_t mode_particleHourglass(void) for(uint32_t i = 0; i < PartSys->usedParticles; i++) //check if particle reached target position after falling { - uint32_t targetposition; + int32_t targetposition; if (PartSys->particles[i].fixed == false) { //calculate target position depending on direction @@ -10379,8 +10390,8 @@ uint16_t mode_particle1Dspray(void) PartSys->sources[i].source.hue = random16(); //TODO: add colormodes like in hourglass? PartSys->sources[0].var = 20; - PartSys->sources[0].minLife = 200;//PartSys->maxXpixel; - PartSys->sources[0].maxLife = 400;//PartSys->maxXpixel << 1; + PartSys->sources[0].minLife = 200 + SEGMENT.speed; + PartSys->sources[0].maxLife = 400 + SEGMENT.speed; PartSys->sources[0].source.x = map(SEGMENT.custom1, 0 , 255, 0, PartSys->maxX); // spray position PartSys->sources[0].v = map(SEGMENT.speed, 0 , 255, -127 + PartSys->sources[0].var, 127 - PartSys->sources[0].var); // particle emit speed PartSys->sources[0].source.reversegrav = false; @@ -10425,7 +10436,7 @@ uint16_t mode_particleBalance(void) PartSys->setParticleSize(1); for(i = 0; i < PartSys->numParticles; i++) { - PartSys->particles[i].x = i * PS_P_RADIUS; + PartSys->particles[i].x = i * PS_P_RADIUS_1D; PartSys->particles[i].hue = (i * 1024) / PartSys->usedParticles; // multi gradient distribution PartSys->particles[i].ttl = 300; PartSys->particles[i].perpetual = true; @@ -10697,7 +10708,7 @@ uint16_t mode_particle1DGEQ(void) for (bin = 0; bin < numSources; bin++) { uint32_t emitparticle = 0; - uint8_t emitspeed = ((uint32_t)fftResult[bin] * (uint32_t)SEGMENT.speed) >> 9; // emit speed according to loudness of band (127 max!) + //uint8_t emitspeed = ((uint32_t)fftResult[bin] * (uint32_t)SEGMENT.speed) >> 9; // emit speed according to loudness of band (127 max!) if (fftResult[bin] > threshold) { emitparticle = 1; @@ -10800,14 +10811,16 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_AURORA, &mode_aurora, _data_FX_MODE_AURORA); addEffect(FX_MODE_RUNNING_RANDOM, &mode_running_random, _data_FX_MODE_RUNNING_RANDOM); addEffect(FX_MODE_LARSON_SCANNER, &mode_larson_scanner, _data_FX_MODE_LARSON_SCANNER); + #ifndef DISABLE_1D_PS_REPLACEMENTS addEffect(FX_MODE_COMET, &mode_comet, _data_FX_MODE_COMET); - addEffect(FX_MODE_FIREWORKS, &mode_fireworks, _data_FX_MODE_FIREWORKS); addEffect(FX_MODE_RAIN, &mode_rain, _data_FX_MODE_RAIN); + addEffect(FX_MODE_ROLLINGBALLS, &rolling_balls, _data_FX_MODE_ROLLINGBALLS); + #endif + addEffect(FX_MODE_FIREWORKS, &mode_fireworks, _data_FX_MODE_FIREWORKS); addEffect(FX_MODE_TETRIX, &mode_tetrix, _data_FX_MODE_TETRIX); addEffect(FX_MODE_FIRE_FLICKER, &mode_fire_flicker, _data_FX_MODE_FIRE_FLICKER); addEffect(FX_MODE_GRADIENT, &mode_gradient, _data_FX_MODE_GRADIENT); - addEffect(FX_MODE_LOADING, &mode_loading, _data_FX_MODE_LOADING); - addEffect(FX_MODE_ROLLINGBALLS, &rolling_balls, _data_FX_MODE_ROLLINGBALLS); + addEffect(FX_MODE_LOADING, &mode_loading, _data_FX_MODE_LOADING); addEffect(FX_MODE_FAIRY, &mode_fairy, _data_FX_MODE_FAIRY); addEffect(FX_MODE_TWO_DOTS, &mode_two_dots, _data_FX_MODE_TWO_DOTS); @@ -10826,7 +10839,9 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_PRIDE_2015, &mode_pride_2015, _data_FX_MODE_PRIDE_2015); addEffect(FX_MODE_JUGGLE, &mode_juggle, _data_FX_MODE_JUGGLE); addEffect(FX_MODE_PALETTE, &mode_palette, _data_FX_MODE_PALETTE); + #ifndef DISABLE_2D_PS_REPLACEMENTS addEffect(FX_MODE_FIRE_2012, &mode_fire_2012, _data_FX_MODE_FIRE_2012); + #endif addEffect(FX_MODE_COLORWAVES, &mode_colorwaves, _data_FX_MODE_COLORWAVES); addEffect(FX_MODE_BPM, &mode_bpm, _data_FX_MODE_BPM); addEffect(FX_MODE_FILLNOISE8, &mode_fillnoise8, _data_FX_MODE_FILLNOISE8); @@ -10849,14 +10864,17 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_SPOTS_FADE, &mode_spots_fade, _data_FX_MODE_SPOTS_FADE); addEffect(FX_MODE_GLITTER, &mode_glitter, _data_FX_MODE_GLITTER); addEffect(FX_MODE_CANDLE, &mode_candle, _data_FX_MODE_CANDLE); + #ifndef DISABLE_1D_PS_REPLACEMENTS addEffect(FX_MODE_STARBURST, &mode_starburst, _data_FX_MODE_STARBURST); - addEffect(FX_MODE_EXPLODING_FIREWORKS, &mode_exploding_fireworks, _data_FX_MODE_EXPLODING_FIREWORKS); addEffect(FX_MODE_BOUNCINGBALLS, &mode_bouncing_balls, _data_FX_MODE_BOUNCINGBALLS); - addEffect(FX_MODE_SINELON, &mode_sinelon, _data_FX_MODE_SINELON); - addEffect(FX_MODE_SINELON_DUAL, &mode_sinelon_dual, _data_FX_MODE_SINELON_DUAL); - addEffect(FX_MODE_SINELON_RAINBOW, &mode_sinelon_rainbow, _data_FX_MODE_SINELON_RAINBOW); addEffect(FX_MODE_POPCORN, &mode_popcorn, _data_FX_MODE_POPCORN); addEffect(FX_MODE_DRIP, &mode_drip, _data_FX_MODE_DRIP); + addEffect(FX_MODE_EXPLODING_FIREWORKS, &mode_exploding_fireworks, _data_FX_MODE_EXPLODING_FIREWORKS); + addEffect(FX_MODE_DANCING_SHADOWS, &mode_dancing_shadows, _data_FX_MODE_DANCING_SHADOWS); + #endif + addEffect(FX_MODE_SINELON, &mode_sinelon, _data_FX_MODE_SINELON); + addEffect(FX_MODE_SINELON_DUAL, &mode_sinelon_dual, _data_FX_MODE_SINELON_DUAL); + addEffect(FX_MODE_SINELON_RAINBOW, &mode_sinelon_rainbow, _data_FX_MODE_SINELON_RAINBOW); addEffect(FX_MODE_PLASMA, &mode_plasma, _data_FX_MODE_PLASMA); addEffect(FX_MODE_PERCENT, &mode_percent, _data_FX_MODE_PERCENT); addEffect(FX_MODE_RIPPLE_RAINBOW, &mode_ripple_rainbow, _data_FX_MODE_RIPPLE_RAINBOW); @@ -10871,8 +10889,7 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_SINEWAVE, &mode_sinewave, _data_FX_MODE_SINEWAVE); addEffect(FX_MODE_PHASEDNOISE, &mode_phased_noise, _data_FX_MODE_PHASEDNOISE); addEffect(FX_MODE_FLOW, &mode_flow, _data_FX_MODE_FLOW); - addEffect(FX_MODE_CHUNCHUN, &mode_chunchun, _data_FX_MODE_CHUNCHUN); - addEffect(FX_MODE_DANCING_SHADOWS, &mode_dancing_shadows, _data_FX_MODE_DANCING_SHADOWS); + addEffect(FX_MODE_CHUNCHUN, &mode_chunchun, _data_FX_MODE_CHUNCHUN); addEffect(FX_MODE_WASHING_MACHINE, &mode_washing_machine, _data_FX_MODE_WASHING_MACHINE); addEffect(FX_MODE_BLENDS, &mode_blends, _data_FX_MODE_BLENDS); @@ -10920,8 +10937,10 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_2DPLASMAROTOZOOM, &mode_2Dplasmarotozoom, _data_FX_MODE_2DPLASMAROTOZOOM); addEffect(FX_MODE_2DSPACESHIPS, &mode_2Dspaceships, _data_FX_MODE_2DSPACESHIPS); addEffect(FX_MODE_2DCRAZYBEES, &mode_2Dcrazybees, _data_FX_MODE_2DCRAZYBEES); + #ifndef DISABLE_2D_PS_REPLACEMENTS addEffect(FX_MODE_2DGHOSTRIDER, &mode_2Dghostrider, _data_FX_MODE_2DGHOSTRIDER); addEffect(FX_MODE_2DBLOBS, &mode_2Dfloatingblobs, _data_FX_MODE_2DBLOBS); + #endif addEffect(FX_MODE_2DSCROLLTEXT, &mode_2Dscrollingtext, _data_FX_MODE_2DSCROLLTEXT); addEffect(FX_MODE_2DDRIFTROSE, &mode_2Ddriftrose, _data_FX_MODE_2DDRIFTROSE); addEffect(FX_MODE_2DDISTORTIONWAVES, &mode_2Ddistortionwaves, _data_FX_MODE_2DDISTORTIONWAVES); diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index ecda701b4b..b83d355e36 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -64,12 +64,12 @@ ParticleSystem::ParticleSystem(uint16_t width, uint16_t height, uint16_t numbero emitIndex = 0; //initialize some default non-zero values most FX use - for (int i = 0; i < numSources; i++) + for (uint32_t i = 0; i < numSources; i++) { sources[i].source.sat = 255; //set saturation to max by default sources[i].source.ttl = 1; //set source alive } - for (int i = 0; i < numParticles; i++) + for (uint32_t i = 0; i < numParticles; i++) { particles[i].sat = 255; // full saturation } @@ -87,7 +87,7 @@ void ParticleSystem::update(void) //update size settings before handling collisions if (advPartSize) { - for (int i = 0; i < usedParticles; i++) + for (uint32_t i = 0; i < usedParticles; i++) { updateSize(&advPartProps[i], &advPartSize[i]); } @@ -98,7 +98,7 @@ void ParticleSystem::update(void) handleCollisions(); //move all particles - for (int i = 0; i < usedParticles; i++) + for (uint32_t i = 0; i < usedParticles; i++) { if (advPartProps) { @@ -224,7 +224,7 @@ int32_t ParticleSystem::sprayEmit(PSsource &emitter, uint32_t amount) { for (uint32_t a = 0; a < amount; a++) { - for (int32_t i = 0; i < usedParticles; i++) + for (uint32_t i = 0; i < usedParticles; i++) { emitIndex++; if (emitIndex >= usedParticles) @@ -734,10 +734,10 @@ void ParticleSystem::ParticleSys_render(bool firemode, uint32_t fireintensity) if (motionBlur > 0) // using SEGMENT.fadeToBlackBy is much slower, this approximately doubles the speed of fade calculation { uint32_t yflipped; - for (uint32_t y = 0; y <= maxYpixel; y++) + for (int32_t y = 0; y <= maxYpixel; y++) { yflipped = maxYpixel - y; - for (uint32_t x = 0; x <= maxXpixel; x++) + for (int32_t x = 0; x <= maxXpixel; x++) { framebuffer[x][y] = SEGMENT.getPixelColorXY(x, yflipped); //copy to local buffer fast_color_scale(framebuffer[x][y], motionBlur); @@ -796,7 +796,7 @@ void ParticleSystem::ParticleSys_render(bool firemode, uint32_t fireintensity) uint32_t bluramount = particlesize; uint32_t bitshift = 0; - for(int i = 0; i < passes; i++) + for(uint32_t i = 0; i < passes; i++) { if (i == 2) // for the last two passes, use higher amount of blur (results in a nicer brightness gradient with soft edges) bitshift = 1; @@ -812,10 +812,10 @@ void ParticleSystem::ParticleSys_render(bool firemode, uint32_t fireintensity) if (useLocalBuffer) // transfer local buffer back to segment { int32_t yflipped; - for (int y = 0; y <= maxYpixel; y++) + for (int32_t y = 0; y <= maxYpixel; y++) { yflipped = maxYpixel - y; - for (int x = 0; x <= maxXpixel; x++) + for (int32_t x = 0; x <= maxXpixel; x++) { SEGMENT.setPixelColorXY(x, yflipped, framebuffer[x][y]); } From 0f89edd125d3e5971adc1d4692a0a8437691695d Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sun, 30 Jun 2024 16:18:53 +0200 Subject: [PATCH 109/219] bugfixes --- wled00/FX.cpp | 10 +++++----- wled00/FXparticleSystem.cpp | 16 ++++++++-------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index c7c6c58d32..f2fa2ce025 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -8147,10 +8147,10 @@ uint16_t mode_particlefireworks(void) else { /* - //TODO: this does not look good. adjust or remove completely + if( PartSys->sources[j].source.vy < 0) //explosion is ongoing { - if(i < (emitparticles>>2)) //set 1/4 of particles to larger size + if(i < (emitparticles>>2)) //set 1/4 of particles to larger size //TODO: this does not look good. adjust or remove completely PartSys->sources[j].size = 50+random16(140); else PartSys->sources[j].size = 0; @@ -10388,10 +10388,10 @@ uint16_t mode_particle1Dspray(void) int32_t gravity = (int32_t)SEGMENT.custom3 - 15; //gravity setting, 0-14 is negative, 16 - 31 is positive PartSys->setGravity(abs(gravity)); // use reversgrav setting to invert gravity (for proper 'floor' and out of bounce handling) - PartSys->sources[i].source.hue = random16(); //TODO: add colormodes like in hourglass? + PartSys->sources[0].source.hue = random16(); //TODO: add colormodes like in hourglass? PartSys->sources[0].var = 20; - PartSys->sources[0].minLife = 200 + SEGMENT.speed; - PartSys->sources[0].maxLife = 400 + SEGMENT.speed; + PartSys->sources[0].minLife = 200; + PartSys->sources[0].maxLife = 400; PartSys->sources[0].source.x = map(SEGMENT.custom1, 0 , 255, 0, PartSys->maxX); // spray position PartSys->sources[0].v = map(SEGMENT.speed, 0 , 255, -127 + PartSys->sources[0].var, 127 - PartSys->sources[0].var); // particle emit speed PartSys->sources[0].source.reversegrav = false; diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index b83d355e36..7272004ae7 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -950,7 +950,7 @@ void ParticleSystem::renderParticle(CRGB **framebuffer, uint32_t particleindex, } maxsize = maxsize/64 + 1; // number of blur passes depends on maxsize, four passes max uint32_t bitshift = 0; - for(int i = 0; i < maxsize; i++) + for(uint32_t i = 0; i < maxsize; i++) { if (i == 2) //for the last two passes, use higher amount of blur (results in a nicer brightness gradient with soft edges) bitshift = 1; @@ -964,7 +964,7 @@ void ParticleSystem::renderParticle(CRGB **framebuffer, uint32_t particleindex, // calculate origin coordinates to render the particle to in the framebuffer uint32_t xfb_orig = x - (rendersize>>1) + 1 - offset; uint32_t yfb_orig = y - (rendersize>>1) + 1 - offset; - uint32_t xfb, yfb; // coordinates in frame buffer to write to note: by making this uint, only overflow has to be checked + uint32_t xfb, yfb; // coordinates in frame buffer to write to note: by making this uint, only overflow has to be checked (spits a warning though) // transfer particle renderbuffer to framebuffer for(uint32_t xrb = offset; xrb < rendersize+offset; xrb++) @@ -1180,7 +1180,7 @@ void ParticleSystem::collideParticles(PSparticle *particle1, PSparticle *particl // overflow check: dx/dy are 7bit, relativV are 8bit -> dotproduct is 15bit, dotproduct/distsquared ist 8b, multiplied by collisionhardness of 8bit. so a 16bit shift is ok, make it 15 to be sure no overflows happen // note: cannot use right shifts as bit shifting in right direction is asymmetrical for positive and negative numbers and this needs to be accurate! the trick is: only shift positive numers // Calculate new velocities after collision - uint32_t surfacehardness = collisionHardness < PS_P_MINSURFACEHARDNESS ? PS_P_MINSURFACEHARDNESS : collisionHardness; // if particles are soft, the impulse must stay above a limit or collisions slip through at higher speeds, 170 seems to be a good value + int32_t surfacehardness = collisionHardness < PS_P_MINSURFACEHARDNESS ? PS_P_MINSURFACEHARDNESS : collisionHardness; // if particles are soft, the impulse must stay above a limit or collisions slip through at higher speeds, 170 seems to be a good value int32_t impulse = -(((((-dotProduct) << 15) / distanceSquared) * surfacehardness) >> 8); // note: inverting before bitshift corrects for asymmetry in right-shifts (and is slightly faster) int32_t ximpulse = ((impulse) * dx) / 32767; // cannot use bit shifts here, it can be negative, use division by 2^bitshift int32_t yimpulse = ((impulse) * dy) / 32767; @@ -1495,7 +1495,7 @@ ParticleSystem1D::ParticleSystem1D(uint16_t length, uint16_t numberofparticles, emitIndex = 0; //initialize some default non-zero values most FX use - for (int i = 0; i < numSources; i++) + for (uint32_t i = 0; i < numSources; i++) { sources[i].source.ttl = 1; //set source alive } @@ -1515,7 +1515,7 @@ void ParticleSystem1D::update(void) applyGravity(); //move all particles - for (int i = 0; i < usedParticles; i++) + for (uint32_t i = 0; i < usedParticles; i++) { if (advPartProps) { @@ -1526,7 +1526,7 @@ void ParticleSystem1D::update(void) if (particlesettings.colorByPosition) { - for (int i = 0; i < usedParticles; i++) + for (uint32_t i = 0; i < usedParticles; i++) { particles[i].hue = (255 * (uint32_t)particles[i].x) / maxX; } @@ -1537,7 +1537,7 @@ void ParticleSystem1D::update(void) uint32_t bg_color = SEGCOLOR(1); //background color, set to black to overlay if (bg_color > 0) //if not black { - for(uint32_t i = 0; i < maxXpixel + 1; i++) + for(int32_t i = 0; i < maxXpixel + 1; i++) { SEGMENT.addPixelColor(i,bg_color); } @@ -1914,7 +1914,7 @@ void ParticleSystem1D::renderParticle(CRGB *framebuffer, uint32_t particleindex, } if (size == 0) //single pixel particle, can be out of bounds as oob checking is made for 2-pixel particles { - uint32_t x = particles[particleindex].x >> PS_P_RADIUS_SHIFT_1D; + int32_t x = particles[particleindex].x >> PS_P_RADIUS_SHIFT_1D; if (x <= maxXpixel) //by making x unsigned there is no need to check < 0 as it will overflow { if (framebuffer) From 9206ba23e4c086a9523d0dbd2ce839721a0ead11 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sun, 30 Jun 2024 17:05:09 +0200 Subject: [PATCH 110/219] default parameter change --- wled00/FX.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index f2fa2ce025..a6ffe5d968 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -10416,7 +10416,7 @@ static const char _data_FX_MODE_PS_1DSPRAY[] PROGMEM = "PS 1D Spray@!,!,Position /* -Particle based gravity balance (1D pendent to 2D particle box) +Particle based balance: particles move back and forth (1D pendent to 2D particle box) Uses palette for particle color by DedeHai (Damian Schneider) */ @@ -10494,7 +10494,7 @@ uint16_t mode_particleBalance(void) PartSys->update(); // update and render return FRAMETIME; } -static const char _data_FX_MODE_PS_BALANCE[] PROGMEM = "PS 1D Balance@!,!,Collisions,Blur/Overlay,Tilt,Color by Position,Wrap/Bounce,Random;,!;!;1;pal=18,sx=200,ix=40,c1=200,c2=0,c3=5,o1=1,o2=0,o3=0"; +static const char _data_FX_MODE_PS_BALANCE[] PROGMEM = "PS 1D Balance@!,!,Collisions,Blur/Overlay,Tilt,Color by Position,Wrap/Bounce,Random;,!;!;1;pal=18,sx=100,ix=40,c1=200,c2=0,c3=5,o1=1,o2=0,o3=0"; From ef7131cca9973520cedaf6f2e88a4f2c1d39e52b Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sun, 30 Jun 2024 21:38:07 +0200 Subject: [PATCH 111/219] explicit cast to fix compile error --- wled00/FXparticleSystem.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index 7272004ae7..bac411439f 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -817,7 +817,7 @@ void ParticleSystem::ParticleSys_render(bool firemode, uint32_t fireintensity) yflipped = maxYpixel - y; for (int32_t x = 0; x <= maxXpixel; x++) { - SEGMENT.setPixelColorXY(x, yflipped, framebuffer[x][y]); + SEGMENT.setPixelColorXY((int)x, (int)yflipped, framebuffer[x][y]); } } free(framebuffer); From fb3fff71ab711659eb745944d97a2a4a093dac29 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Tue, 2 Jul 2024 18:29:08 +0200 Subject: [PATCH 112/219] Bugfixes --- wled00/FX.cpp | 19 +++++++------------ wled00/FXparticleSystem.cpp | 5 ++--- 2 files changed, 9 insertions(+), 15 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index a6ffe5d968..ba0c9a4895 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -8517,8 +8517,7 @@ uint16_t mode_particlewaterfall(void) PartSys->setGravity(); // enable with default gforce PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting) PartSys->setMotionBlur(190); // anable motion blur - numSprays = min((int32_t)PartSys->numSources, min(PartSys->maxXpixel/5, (int32_t)2)); // number of sprays - for (i = 0; i < numSprays; i++) + for (i = 0; i < PartSys->numSources; i++) { PartSys->sources[i].source.hue = random16(); PartSys->sources[i].source.collide = true; // seeded particles will collide @@ -8545,8 +8544,7 @@ uint16_t mode_particlewaterfall(void) PartSys->setBounceX(SEGMENT.check2); // walls PartSys->setBounceY(SEGMENT.check3); // ground PartSys->setWallHardness(SEGMENT.custom2); - numSprays = min((int32_t)PartSys->numSources, min(PartSys->maxXpixel / 5, (int32_t)2)); // number of sprays depends on segment width - + numSprays = min((int32_t)PartSys->numSources, max(PartSys->maxXpixel / 6, (int32_t)2)); // number of sprays depends on segment width if (SEGMENT.custom2 > 0) // collisions enabled PartSys->enableParticleCollisions(true, SEGMENT.custom2); // enable collisions and set particle collision hardness else @@ -8565,16 +8563,14 @@ uint16_t mode_particlewaterfall(void) for (i = 0; i < numSprays; i++) { PartSys->sources[i].vy = -SEGMENT.speed >> 3; // emitting speed, down - PartSys->sources[i].source.x = map(SEGMENT.custom3, 0, 31, 0, (PartSys->maxXpixel - numSprays * 2) * PS_P_RADIUS) + i * PS_P_RADIUS * 2; // emitter position + //PartSys->sources[i].source.x = map(SEGMENT.custom3, 0, 31, 0, (PartSys->maxXpixel - numSprays * 2) * PS_P_RADIUS) + i * PS_P_RADIUS * 2; // emitter position + PartSys->sources[i].source.x = map(SEGMENT.custom3, 0, 31, 0, (PartSys->maxXpixel - numSprays) * PS_P_RADIUS) + i * PS_P_RADIUS * 2; // emitter position PartSys->sources[i].source.y = PartSys->maxY + (PS_P_RADIUS * ((i<<2) + 4)); // source y position, few pixels above the top to increase spreading before entering the matrix PartSys->sources[i].var = (SEGMENT.custom1 >> 3); // emiting variation 0-32 - } - - for (i = 0; i < numSprays; i++) - { PartSys->sprayEmit(PartSys->sources[i]); } } + if (SEGMENT.call % 20 == 0) PartSys->applyFriction(1); // add just a tiny amount of friction to help smooth things @@ -10362,7 +10358,6 @@ uint16_t mode_particle1Dspray(void) if (SEGLEN == 1) return mode_static(); ParticleSystem1D *PartSys = NULL; - uint32_t i; if (SEGMENT.call == 0) // initialization { @@ -10399,12 +10394,12 @@ uint16_t mode_particle1Dspray(void) PartSys->sources[0].source.reversegrav = true; if(random(255) % (1 + ((255 - SEGMENT.intensity) >> 3)) == 0) - PartSys->sprayEmit(PartSys->sources[i]); //emit a particle + PartSys->sprayEmit(PartSys->sources[0]); //emit a particle //update color settings PartSys->setColorByAge(SEGMENT.check1); //overruled by 'color by position' PartSys->setColorByPosition(SEGMENT.check3); - for(i = 0; i < PartSys->usedParticles; i++) + for(uint i = 0; i < PartSys->usedParticles; i++) { PartSys->particles[i].reversegrav = PartSys->sources[0].source.reversegrav; //update gravity direction } diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index bac411439f..c267870c57 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -1058,7 +1058,7 @@ void ParticleSystem::renderParticle(CRGB **framebuffer, uint32_t particleindex, // particles move upwards faster if ttl is high (i.e. they are hotter) void ParticleSystem::fireParticleupdate() { - //TODO: cleanup this function? check if normal move is much slower, change move function to check y first then this function just needs to add ttl to y befor calling normal move function + //TODO: cleanup this function? check if normal move is much slower, change move function to check y first then this function just needs to add ttl to y befor calling normal move function (this function uses 274bytes of flash) uint32_t i = 0; for (i = 0; i < usedParticles; i++) @@ -1716,7 +1716,6 @@ void ParticleSystem1D::particleMoveUpdate(PSparticle1D &part, PSsettings1D *opti newX = newX % (maxX + 1); if (newX < 0) newX += maxX + 1; - Serial.println(newX/32); } else if (((newX <= -PS_P_HALFRADIUS_1D) || (newX > maxX + PS_P_HALFRADIUS_1D))) // particle is leaving, set out of bounds if it has fully left { @@ -1914,7 +1913,7 @@ void ParticleSystem1D::renderParticle(CRGB *framebuffer, uint32_t particleindex, } if (size == 0) //single pixel particle, can be out of bounds as oob checking is made for 2-pixel particles { - int32_t x = particles[particleindex].x >> PS_P_RADIUS_SHIFT_1D; + uint32_t x = particles[particleindex].x >> PS_P_RADIUS_SHIFT_1D; if (x <= maxXpixel) //by making x unsigned there is no need to check < 0 as it will overflow { if (framebuffer) From d18216e13c7787cd0fc909aae3a804c7abf15e78 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Tue, 2 Jul 2024 21:57:30 +0200 Subject: [PATCH 113/219] added color-waves to PS chase, some parameter tuning plus bugfixes --- wled00/FX.cpp | 45 +++++++++++++++++++++++++++++---------------- 1 file changed, 29 insertions(+), 16 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index ba0c9a4895..e2e7238fae 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -10540,26 +10540,38 @@ uint16_t mode_particleChase(void) PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 1, min(PartSys->maxX / (64 + (SEGMENT.custom1 >> 1)), (int32_t)(PartSys->numParticles)))); //depends on intensity and particle size (custom1) for(i = 0; i < PartSys->usedParticles; i++) { - PartSys->particles[i].x = i * (PartSys->maxX / (PartSys->usedParticles)); // distribute evenly - if(SEGMENT.custom2 == 0) - PartSys->particles[i].hue = (i * 256) / PartSys->usedParticles; // gradient distribution - else if(SEGMENT.custom2 == 255) + PartSys->particles[i].x = i * (PartSys->maxX / (PartSys->usedParticles - 1)); // distribute evenly + if(SEGMENT.custom2 < 255) + PartSys->particles[i].hue = (i * (SEGMENT.custom2 << 3)) / PartSys->usedParticles; // gradient distribution + else PartSys->particles[i].hue = random16(); - else - PartSys->particles[i].hue = SEGMENT.custom2; - int32_t speed = SEGMENT.speed >> 1; - if(SEGMENT.check1) speed = -speed; - PartSys->particles[i].vx = speed; + + PartSys->particles[i].vx = SEGMENT.speed >> 1; PartSys->advPartProps[i].size = SEGMENT.custom1; } PartSys->setParticleSize(SEGMENT.custom1); // if custom1 == 0 this sets rendering size to one pixel SEGMENT.aux0 = settingssum; } + if(SEGMENT.check1) // pride rainbow colors + { + for(i = 0; i < PartSys->usedParticles; i++) + { + + } + } + if((SEGMENT.check2 || SEGMENT.check1) && SEGMENT.call % (192 / ((SEGMENT.speed >> 2) + 128)) == 0) // color waves + { + for(i = 0; i < PartSys->usedParticles; i++) + { + PartSys->particles[i].hue -= 2; + } + } + PartSys->setParticleSize(SEGMENT.custom1); // if custom1 == 0 this sets rendering size to one pixel PartSys->update(); // update and render return FRAMETIME; } -static const char _data_FX_MODE_PS_CHASE[] PROGMEM = "PS Chase@Speed,Density,Size,Color,Blur/Overlay,Direction,,Color by Position;,!;!;1;pal=53,sx=50,ix=100,c2=0,c3=0,o1=0,o2=0,o3=0"; +static const char _data_FX_MODE_PS_CHASE[] PROGMEM = "PS Chase@Speed,Density,Size,Hue,Blur/Overlay,Pride,Color Waves,Color by Position;,!;!;1;pal=53,sx=50,ix=100,c2=0,c3=0,o1=0,o2=0,o3=0"; /* @@ -10684,7 +10696,7 @@ uint16_t mode_particle1DGEQ(void) for(i = 0; i < PartSys->usedParticles; i++) { - if(PartSys->particles[i].ttl > 10) PartSys->particles[i].ttl -= 10; //ttl is linked to brightness, this allows to use higher brightness but still a short lifespan + if(PartSys->particles[i].ttl > 20) PartSys->particles[i].ttl -= 20; //ttl is linked to brightness, this allows to use higher brightness but still a short lifespan else PartSys->particles[i].ttl = 0; } @@ -10696,14 +10708,15 @@ uint16_t mode_particle1DGEQ(void) //map the bands into 16 positions on x axis, emit some particles according to frequency loudness i = 0; - uint32_t bin; //current bin + uint32_t bin = random(numSources);; //current bin , start with random one to distribute available particles fairly uint32_t threshold = 300 - SEGMENT.intensity; - - for (bin = 0; bin < numSources; bin++) - { + for (i = 0; i < numSources; i++) + { + bin ++; + bin = bin % numSources; uint32_t emitparticle = 0; - //uint8_t emitspeed = ((uint32_t)fftResult[bin] * (uint32_t)SEGMENT.speed) >> 9; // emit speed according to loudness of band (127 max!) + uint8_t emitspeed = ((uint32_t)fftResult[bin] * (uint32_t)SEGMENT.speed) >> 10; // emit speed according to loudness of band (127 max!) if (fftResult[bin] > threshold) { emitparticle = 1; From d8cfd7745cfe5e6c5df3d3e70a7df256c34e184a Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Thu, 4 Jul 2024 22:01:43 +0200 Subject: [PATCH 114/219] FX update and bugfixes - fixed wall collisions on larger particle sizes - update to bouncing balls: - added size setting - added proper updating on parameter change - bugfixes added support for 'colorwaves' and 'pride' to PS Chase to save on code size. currently broken... --- wled00/FX.cpp | 122 +++++++++++++++++++++++------------- wled00/FXparticleSystem.cpp | 11 ++-- 2 files changed, 82 insertions(+), 51 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index e2e7238fae..3e55e0d6cc 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -9775,13 +9775,13 @@ uint16_t mode_particleBouncingBalls(void) if (SEGMENT.call == 0) // initialization { - if (!initParticleSystem1D(PartSys, 1)) // init, no additional data needed + if (!initParticleSystem1D(PartSys, 1, 0, true)) // init return mode_static(); // allocation failed; //allocation failed PartSys->sources[0].source.collide = true; // seeded particles will collide (if enabled) PartSys->sources[0].source.x = PS_P_RADIUS_1D; //emit at bottom PartSys->sources[0].maxLife = 900; // maximum lifetime in frames PartSys->sources[0].minLife = PartSys->sources[0].maxLife; - PartSys->setBounce(true); + PartSys->setBounce(true); SEGMENT.aux0 = 1; SEGMENT.aux1 = 500; //set out of speed range to ensure uptate on first call } @@ -9795,43 +9795,43 @@ uint16_t mode_particleBouncingBalls(void) } // Particle System settings - uint32_t hardness = 240 + (SEGMENT.custom1>>4); - PartSys->updateSystem(); // update system properties (dimensions and data pointers) - PartSys->setWallHardness(hardness); + //uint32_t hardness = 240 + (SEGMENT.custom1>>4); + PartSys->updateSystem(); // update system properties (dimensions and data pointers) PartSys->setGravity(1 + (SEGMENT.custom3 >> 1)); // set gravity (8 is default strength) PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur PartSys->sources[0].var = SEGMENT.speed >> 3; PartSys->sources[0].v = (SEGMENT.speed >> 1) - (SEGMENT.speed >> 3); - PartSys->enableParticleCollisions(SEGMENT.check1, hardness - 1); // enable collisions and set particle collision hardness (do not use full hardness or particles speed up due to pushing, can not be made perfectly balanced) - PartSys->setUsedParticles( 1 + (SEGMENT.intensity >> 3)); // 1 - 32 - PartSys->setParticleSize(SEGMENT.check3); // 1 or 2 pixel rendering + PartSys->enableParticleCollisions(SEGMENT.check1, 254); // enable collisions and set particle collision hardness (do not use full hardness or particles speed up due to pushing, can not be made perfectly balanced) + PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 1, PartSys->numParticles >> 1)); + PartSys->setColorByPosition(SEGMENT.check3); if(SEGMENT.check2) //rolling balls { PartSys->setGravity(0); - bool updatespeed = false; - if(SEGMENT.aux1 != SEGMENT.speed) - { - SEGMENT.aux1 = SEGMENT.speed; - updatespeed = true; - } - + PartSys->setWallHardness(255); + bool updateballs = false; + if(SEGMENT.aux1 != SEGMENT.speed + SEGMENT.check2 + SEGMENT.custom1) //speed changed or check was just enabled or size changed + updateballs = true; + for(uint32_t i = 0; i < PartSys->usedParticles; i++) - { - if(PartSys->particles[i].vx > 8 || PartSys->particles[i].vx < -8) //let only slow particles die (ensures no stopped particles) + { + if((PartSys->particles[i].vx > 8 || PartSys->particles[i].vx < -8) && PartSys->particles[i].ttl > 150) //let only slow particles die (ensures no stopped particles) PartSys->particles[i].ttl = 260; //set alive at full intensity - if(updatespeed || PartSys->particles[i].ttl == 0) //speed changed or particle died, reset TTL and speed + if(updateballs || PartSys->particles[i].ttl == 0) //speed changed or particle died, set particle properties { PartSys->particles[i].ttl = 260 + SEGMENT.speed; PartSys->particles[i].collide = true; int32_t newspeed = random(20 + (SEGMENT.speed >> 2)) + (SEGMENT.speed >> 3); PartSys->particles[i].vx = PartSys->particles[i].vx > 0 ? newspeed : -newspeed; //keep the direction - PartSys->particles[i].hue = random16(); //set ball colors to random + PartSys->particles[i].hue = random16(); //set ball colors to random + PartSys->advPartProps[i].sat = 255; + PartSys->advPartProps[i].size = random16(SEGMENT.custom1 >> 1, SEGMENT.custom1); } } } - else //bouncing balls / popcorn + else //bouncing balls { + PartSys->setWallHardness(220); //check for balls that are 'laying on the ground' and remove them for(uint32_t i = 0; i < PartSys->usedParticles; i++) { @@ -9844,23 +9844,24 @@ uint16_t mode_particleBouncingBalls(void) { SEGMENT.aux0 = (260 - SEGMENT.intensity) + random(280 - SEGMENT.intensity); PartSys->sources[0].source.hue = random16(); //set ball color + PartSys->sources[0].sat = 255; + PartSys->sources[0].size = random16(SEGMENT.custom1 >> 1, SEGMENT.custom1); PartSys->sprayEmit(PartSys->sources[0]); } } - - //increase speed on high settings by calling the move function twice - if(SEGMENT.speed > 200) - { - for (uint32_t i = 0; i < PartSys->usedParticles; i++)//move all particles - { - PartSys->particleMoveUpdate(PartSys->particles[i]); - } + SEGMENT.aux1 = SEGMENT.speed + SEGMENT.check2 + SEGMENT.custom1; + for (uint32_t i = 0; i < PartSys->usedParticles; i++) + { + + if(SEGMENT.speed > 200) + PartSys->particleMoveUpdate(PartSys->particles[i]); //increase speed on high settings by calling the move function twice } + PartSys->update(); // update and render return FRAMETIME; } -static const char _data_FX_MODE_PSBOUNCINGBALLS[] PROGMEM = "PS Bouncing Balls@Speed,!,Hardness,Blur/Overlay,Gravity,Collide,Rolling,Smooth;,!;!;1;pal=0,sx=100,ix=180,c1=240,c2=0,c3=8,o1=0,o2=0,o3=1"; +static const char _data_FX_MODE_PSBOUNCINGBALLS[] PROGMEM = "PS Bouncing Balls@Speed,!,Size,Blur/Overlay,Gravity,Collide,Rolling,Color by Position;,!;!;1;pal=0,sx=100,ix=180,c1=240,c2=0,c3=8,o1=0,o2=0,o3=0"; /* Particle Replacement for original Dancing Shadows: @@ -10508,7 +10509,7 @@ uint16_t mode_particleChase(void) if (SEGMENT.call == 0) // initialization { - if (!initParticleSystem1D(PartSys, 1, 0, true)) // init, no additional data needed + if (!initParticleSystem1D(PartSys, 1, 3, true)) // init return mode_static(); // allocation failed PartSys->setWrap(true); for(i = 0; i < PartSys->numParticles; i++) @@ -10518,6 +10519,8 @@ uint16_t mode_particleChase(void) PartSys->particles[i].perpetual = true; } SEGMENT.aux0 = 0xFFFF; // invalidate + *PartSys->PSdataEnd = 1; + *(PartSys->PSdataEnd + 1) = 1; } else PartSys = reinterpret_cast(SEGMENT.data); // if not first call, just set the pointer to the PS @@ -10530,48 +10533,79 @@ uint16_t mode_particleChase(void) // Particle System settings PartSys->updateSystem(); // update system properties (dimensions and data pointers) PartSys->setColorByPosition(SEGMENT.check3); - PartSys->setMotionBlur(7 + (SEGMENT.custom3 << 3)); // anable motion blur + PartSys->setMotionBlur(7 + (SEGMENT.custom3 << 3)); // anable motion blur + uint8_t* basehue = (PartSys->PSdataEnd + 2); //assign data pointer //PartSys->setBounce(SEGMENT.check2); uint32_t settingssum = SEGMENT.speed + SEGMENT.intensity + SEGMENT.custom1 + SEGMENT.custom2 + SEGMENT.check1 + SEGMENT.check2 + SEGMENT.check3; - if(SEGMENT.aux0 != settingssum) //settings changed changed, update { PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 1, min(PartSys->maxX / (64 + (SEGMENT.custom1 >> 1)), (int32_t)(PartSys->numParticles)))); //depends on intensity and particle size (custom1) + SEGMENT.step = ((PartSys->maxX << 8) / (PartSys->usedParticles)) >> 8; //spacing between particles + uint32_t remainder = PartSys->maxX - ((PartSys->usedParticles) * SEGMENT.step); // unused spacing, distribute this for(i = 0; i < PartSys->usedParticles; i++) { - PartSys->particles[i].x = i * (PartSys->maxX / (PartSys->usedParticles - 1)); // distribute evenly - if(SEGMENT.custom2 < 255) - PartSys->particles[i].hue = (i * (SEGMENT.custom2 << 3)) / PartSys->usedParticles; // gradient distribution - else - PartSys->particles[i].hue = random16(); - + PartSys->particles[i].x = (i - 1) * SEGMENT.step + (((i + 1) * remainder) / PartSys->usedParticles); // distribute evenly PartSys->particles[i].vx = SEGMENT.speed >> 1; PartSys->advPartProps[i].size = SEGMENT.custom1; } PartSys->setParticleSize(SEGMENT.custom1); // if custom1 == 0 this sets rendering size to one pixel SEGMENT.aux0 = settingssum; } + if(SEGMENT.check1) // pride rainbow colors - { - for(i = 0; i < PartSys->usedParticles; i++) + { + int8_t* huedir = reinterpret_cast(PartSys->PSdataEnd); //assign data pointer + int8_t* sizedir = reinterpret_cast(PartSys->PSdataEnd + 1); //assign data pointer + int32_t sizechange = 0; + + if(PartSys->advPartProps[0].size >= 254) + *sizedir = -1; + else if(PartSys->advPartProps[0].size <= SEGMENT.custom1 / 3) + *sizedir = 1; + + if(SEGMENT.aux1 > 2048) + *huedir = -1; + else if(SEGMENT.aux1 < 1) + *huedir = 1; + + if(SEGMENT.call % (255 / (SEGMENT.speed >> 3)) == 0) + SEGMENT.aux1 += *huedir; + if(SEGMENT.call % (255 / (SEGMENT.speed >> 2)) == 0) + sizechange = *sizedir; + + for(i = 0; i < PartSys->usedParticles; i++) { - + PartSys->particles[i].hue = *basehue + (i * (SEGMENT.aux1)) / PartSys->usedParticles; // gradient distribution + PartSys->advPartProps[i].size += sizechange; } } if((SEGMENT.check2 || SEGMENT.check1) && SEGMENT.call % (192 / ((SEGMENT.speed >> 2) + 128)) == 0) // color waves { - for(i = 0; i < PartSys->usedParticles; i++) + for(i = 0; i < PartSys->usedParticles; i++) { - PartSys->particles[i].hue -= 2; + PartSys->particles[i].hue -= 2; } } + // wrap around (cannot use particle system wrap if distributing colors manually, it also wraps rendering which does not look good) + // TODO: disable wrap, start at -1 and go to maxX + 1, wrap manually, need to set x to particle[i+1].x - spacing when wrapping + for(i = 0; i < PartSys->usedParticles; i++) + { + //if(PartSys->particles[i].x > ; + + if(SEGMENT.custom2 < 255) + PartSys->particles[i].hue = (i * (SEGMENT.custom2 << 3)) / PartSys->usedParticles; // gradient distribution + else + PartSys->particles[i].hue = random16(); + + } + //SEGMENT.step PartSys->setParticleSize(SEGMENT.custom1); // if custom1 == 0 this sets rendering size to one pixel PartSys->update(); // update and render return FRAMETIME; } -static const char _data_FX_MODE_PS_CHASE[] PROGMEM = "PS Chase@Speed,Density,Size,Hue,Blur/Overlay,Pride,Color Waves,Color by Position;,!;!;1;pal=53,sx=50,ix=100,c2=0,c3=0,o1=0,o2=0,o3=0"; +static const char _data_FX_MODE_PS_CHASE[] PROGMEM = "PS Chase@Speed,Density,Size,Hue,Blur/Overlay,Pride,Color Waves,Color by Position;,!;!;1;pal=11,sx=50,ix=100,c2=0,c3=0,o1=0,o2=0,o3=0"; /* diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index c267870c57..442a8cac70 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -1669,7 +1669,7 @@ void ParticleSystem1D::particleMoveUpdate(PSparticle1D &part, PSsettings1D *opti part.outofbounds = 0; // reset out of bounds (in case particle was created outside the matrix and is now moving into view) if (advancedproperties) //using individual particle size? { - particleHardRadius = PS_P_MINHARDRADIUS_1D + advancedproperties->size; + particleHardRadius = PS_P_MINHARDRADIUS_1D + (advancedproperties->size >> 1); if (advancedproperties->size > 1) { usesize = true; // note: variable eases out of frame checking below @@ -1983,7 +1983,6 @@ void ParticleSystem1D::renderParticle(CRGB *framebuffer, uint32_t particleindex, fast_color_add(renderbuffer[5], color, pxlbrightness[1]); uint32_t rendersize = 2; // initialize render size, minimum is 4x4 pixels, it is incremented int he loop below to start with 4 uint32_t offset = 4; // offset to zero coordinate to write/read data in renderbuffer (actually needs to be 3, is decremented in the loop below) - uint32_t size = advPartProps[particleindex].size; uint32_t blurpasses = size/64 + 1; // number of blur passes depends on size, four passes max uint32_t bitshift = 0; for(int i = 0; i < blurpasses; i++) @@ -2066,7 +2065,7 @@ void ParticleSystem1D::handleCollisions() { if (advPartProps) // use advanced size properties { - collisiondistance += ((uint32_t)advPartProps[i].size + (uint32_t)advPartProps[j].size)>>1; + collisiondistance = PS_P_MINHARDRADIUS_1D + ((uint32_t)advPartProps[i].size + (uint32_t)advPartProps[j].size)>>1; } dx = particles[j].x - particles[i].x; int32_t dv = (int32_t)particles[j].vx - (int32_t)particles[i].vx; @@ -2310,18 +2309,16 @@ void blur1D(CRGB *colorbuffer, uint32_t size, uint32_t blur, bool smear, uint32_ fast_color_scale(seeppart, seep); // scale it and seep to neighbours if (!smear) // fade current pixel if smear is disabled fast_color_scale(colorbuffer[x], 255 - blur); - if (x > 0) { - fast_color_add(colorbuffer[x-1], seeppart); - fast_color_add(colorbuffer[x], carryover); + fast_color_add(colorbuffer[x-1], seeppart); + fast_color_add(colorbuffer[x], carryover); // is black on first pass } carryover = seeppart; } fast_color_add(colorbuffer[size-1], carryover); // set last pixel } - #endif // WLED_DISABLE_PARTICLESYSTEM1D From e7d860696c1860823234c1f6deabfb643f7897a2 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Thu, 4 Jul 2024 23:15:17 +0200 Subject: [PATCH 115/219] minor update to rolling balls, fixed stuff in Chase, pride option still not working --- wled00/FX.cpp | 50 ++++++++++++++++++++++++++++++-------------------- 1 file changed, 30 insertions(+), 20 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 3e55e0d6cc..89b3049b4e 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -9810,7 +9810,7 @@ uint16_t mode_particleBouncingBalls(void) PartSys->setGravity(0); PartSys->setWallHardness(255); bool updateballs = false; - if(SEGMENT.aux1 != SEGMENT.speed + SEGMENT.check2 + SEGMENT.custom1) //speed changed or check was just enabled or size changed + if(SEGMENT.aux1 != SEGMENT.speed + SEGMENT.intensity + SEGMENT.check2 + SEGMENT.custom1) // user settings change updateballs = true; for(uint32_t i = 0; i < PartSys->usedParticles; i++) @@ -9825,7 +9825,7 @@ uint16_t mode_particleBouncingBalls(void) PartSys->particles[i].vx = PartSys->particles[i].vx > 0 ? newspeed : -newspeed; //keep the direction PartSys->particles[i].hue = random16(); //set ball colors to random PartSys->advPartProps[i].sat = 255; - PartSys->advPartProps[i].size = random16(SEGMENT.custom1 >> 1, SEGMENT.custom1); + PartSys->advPartProps[i].size = random16(SEGMENT.custom1 >> 2, SEGMENT.custom1); } } } @@ -9845,11 +9845,11 @@ uint16_t mode_particleBouncingBalls(void) SEGMENT.aux0 = (260 - SEGMENT.intensity) + random(280 - SEGMENT.intensity); PartSys->sources[0].source.hue = random16(); //set ball color PartSys->sources[0].sat = 255; - PartSys->sources[0].size = random16(SEGMENT.custom1 >> 1, SEGMENT.custom1); + PartSys->sources[0].size = random16(SEGMENT.custom1 >> 2, SEGMENT.custom1); PartSys->sprayEmit(PartSys->sources[0]); } } - SEGMENT.aux1 = SEGMENT.speed + SEGMENT.check2 + SEGMENT.custom1; + SEGMENT.aux1 = SEGMENT.speed + SEGMENT.intensity + SEGMENT.check2 + SEGMENT.custom1; for (uint32_t i = 0; i < PartSys->usedParticles; i++) { @@ -9861,7 +9861,7 @@ uint16_t mode_particleBouncingBalls(void) PartSys->update(); // update and render return FRAMETIME; } -static const char _data_FX_MODE_PSBOUNCINGBALLS[] PROGMEM = "PS Bouncing Balls@Speed,!,Size,Blur/Overlay,Gravity,Collide,Rolling,Color by Position;,!;!;1;pal=0,sx=100,ix=180,c1=240,c2=0,c3=8,o1=0,o2=0,o3=0"; +static const char _data_FX_MODE_PSBOUNCINGBALLS[] PROGMEM = "PS Bouncing Balls@Speed,!,Size,Blur/Overlay,Gravity,Collide,Rolling,Color by Position;,!;!;1;pal=0,sx=100,ix=85,c1=30,c2=0,c3=8,o1=0,o2=0,o3=0"; /* Particle Replacement for original Dancing Shadows: @@ -10505,13 +10505,13 @@ uint16_t mode_particleChase(void) if (SEGLEN == 1) return mode_static(); ParticleSystem1D *PartSys = NULL; - uint32_t i; + int32_t i; if (SEGMENT.call == 0) // initialization { if (!initParticleSystem1D(PartSys, 1, 3, true)) // init return mode_static(); // allocation failed - PartSys->setWrap(true); + // PartSys->setWrap(true); for(i = 0; i < PartSys->numParticles; i++) { PartSys->advPartProps[i].sat = 255; @@ -10541,15 +10541,19 @@ uint16_t mode_particleChase(void) if(SEGMENT.aux0 != settingssum) //settings changed changed, update { PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 1, min(PartSys->maxX / (64 + (SEGMENT.custom1 >> 1)), (int32_t)(PartSys->numParticles)))); //depends on intensity and particle size (custom1) - SEGMENT.step = ((PartSys->maxX << 8) / (PartSys->usedParticles)) >> 8; //spacing between particles - uint32_t remainder = PartSys->maxX - ((PartSys->usedParticles) * SEGMENT.step); // unused spacing, distribute this + SEGMENT.step = (PartSys->maxX + (PS_P_RADIUS_1D << 4)) / PartSys->usedParticles; //spacing between particles + // uint32_t remainder = PartSys->maxX - ((PartSys->usedParticles) * SEGMENT.step); // unused spacing, distribute this for(i = 0; i < PartSys->usedParticles; i++) { - PartSys->particles[i].x = (i - 1) * SEGMENT.step + (((i + 1) * remainder) / PartSys->usedParticles); // distribute evenly + //PartSys->particles[i].x = (i - 1) * SEGMENT.step + (((i + 1) * remainder) / PartSys->usedParticles); // distribute evenly + PartSys->particles[i].x = (i - 1) * SEGMENT.step; // distribute evenly PartSys->particles[i].vx = SEGMENT.speed >> 1; PartSys->advPartProps[i].size = SEGMENT.custom1; + if(SEGMENT.custom2 < 255) + PartSys->particles[i].hue = (i * (SEGMENT.custom2 << 3)) / PartSys->usedParticles; // gradient distribution + else + PartSys->particles[i].hue = random16(); } - PartSys->setParticleSize(SEGMENT.custom1); // if custom1 == 0 this sets rendering size to one pixel SEGMENT.aux0 = settingssum; } @@ -10579,6 +10583,7 @@ uint16_t mode_particleChase(void) PartSys->particles[i].hue = *basehue + (i * (SEGMENT.aux1)) / PartSys->usedParticles; // gradient distribution PartSys->advPartProps[i].size += sizechange; } + *basehue++; } if((SEGMENT.check2 || SEGMENT.check1) && SEGMENT.call % (192 / ((SEGMENT.speed >> 2) + 128)) == 0) // color waves { @@ -10589,23 +10594,28 @@ uint16_t mode_particleChase(void) } // wrap around (cannot use particle system wrap if distributing colors manually, it also wraps rendering which does not look good) // TODO: disable wrap, start at -1 and go to maxX + 1, wrap manually, need to set x to particle[i+1].x - spacing when wrapping + + uint32_t huestep = (((uint32_t)SEGMENT.custom2 << 19) / PartSys->usedParticles) >> 16; // hue increment + for(i = 0; i < PartSys->usedParticles; i++) { - //if(PartSys->particles[i].x > ; - - if(SEGMENT.custom2 < 255) - PartSys->particles[i].hue = (i * (SEGMENT.custom2 << 3)) / PartSys->usedParticles; // gradient distribution - else - PartSys->particles[i].hue = random16(); - + if(PartSys->particles[i].x > PartSys->maxX + PS_P_RADIUS_1D + PartSys->advPartProps[i].size) // wrap it around + { + uint32_t nextindex = (i + 1) % PartSys->usedParticles; + PartSys->particles[i].x = PartSys->particles[nextindex].x - SEGMENT.step; + if(SEGMENT.custom2 < 255) + PartSys->particles[i].hue = PartSys->particles[nextindex].hue - huestep; + else + PartSys->particles[i].hue = random16(); + } } - //SEGMENT.step + PartSys->setParticleSize(SEGMENT.custom1); // if custom1 == 0 this sets rendering size to one pixel PartSys->update(); // update and render return FRAMETIME; } -static const char _data_FX_MODE_PS_CHASE[] PROGMEM = "PS Chase@Speed,Density,Size,Hue,Blur/Overlay,Pride,Color Waves,Color by Position;,!;!;1;pal=11,sx=50,ix=100,c2=0,c3=0,o1=0,o2=0,o3=0"; +static const char _data_FX_MODE_PS_CHASE[] PROGMEM = "PS Chase@Speed,Density,Size,Hue,Blur/Overlay,Pride,Color Waves,Color by Position;,!;!;1;pal=11,sx=50,ix=100,c2=5,c3=0,o1=0,o2=0,o3=0"; /* From 6f41ab708def9d6d428163f98fda2598f5823789 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Fri, 5 Jul 2024 08:21:35 +0200 Subject: [PATCH 116/219] update on 'pride' now working again but needs finetuning --- wled00/FX.cpp | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 89b3049b4e..96cce11fa3 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -10102,7 +10102,7 @@ uint16_t mode_particleFireworks1D(void) } } if(SEGMENT.call & 0x01) //every second frame - PartSys->sprayEmit(PartSys->sources[i]); //emit a particle + PartSys->sprayEmit(PartSys->sources[0]); //emit a particle PartSys->update(); // update and render return FRAMETIME; @@ -10535,12 +10535,12 @@ uint16_t mode_particleChase(void) PartSys->setColorByPosition(SEGMENT.check3); PartSys->setMotionBlur(7 + (SEGMENT.custom3 << 3)); // anable motion blur uint8_t* basehue = (PartSys->PSdataEnd + 2); //assign data pointer - + uint32_t huestep = (((uint32_t)SEGMENT.custom2 << 19) / PartSys->usedParticles) >> 16; // hue increment //PartSys->setBounce(SEGMENT.check2); uint32_t settingssum = SEGMENT.speed + SEGMENT.intensity + SEGMENT.custom1 + SEGMENT.custom2 + SEGMENT.check1 + SEGMENT.check2 + SEGMENT.check3; if(SEGMENT.aux0 != settingssum) //settings changed changed, update { - PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 1, min(PartSys->maxX / (64 + (SEGMENT.custom1 >> 1)), (int32_t)(PartSys->numParticles)))); //depends on intensity and particle size (custom1) + PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 1, min(PartSys->maxX / (32 + (SEGMENT.custom1 >> 1)), (int32_t)(PartSys->numParticles)))); //depends on intensity and particle size (custom1) SEGMENT.step = (PartSys->maxX + (PS_P_RADIUS_1D << 4)) / PartSys->usedParticles; //spacing between particles // uint32_t remainder = PartSys->maxX - ((PartSys->usedParticles) * SEGMENT.step); // unused spacing, distribute this for(i = 0; i < PartSys->usedParticles; i++) @@ -10557,46 +10557,51 @@ uint16_t mode_particleChase(void) SEGMENT.aux0 = settingssum; } + if(SEGMENT.check1) // pride rainbow colors { + //TODO: orignal FX also changes movement speed + // also the color change is too fast int8_t* huedir = reinterpret_cast(PartSys->PSdataEnd); //assign data pointer int8_t* sizedir = reinterpret_cast(PartSys->PSdataEnd + 1); //assign data pointer int32_t sizechange = 0; if(PartSys->advPartProps[0].size >= 254) *sizedir = -1; - else if(PartSys->advPartProps[0].size <= SEGMENT.custom1 / 3) + else if(PartSys->advPartProps[0].size <= (SEGMENT.custom1 >> 2)) *sizedir = 1; - if(SEGMENT.aux1 > 2048) + if(SEGMENT.aux1 > 64) *huedir = -1; else if(SEGMENT.aux1 < 1) *huedir = 1; - if(SEGMENT.call % (255 / (SEGMENT.speed >> 3)) == 0) + if(SEGMENT.call % (1024 / (1 + (SEGMENT.speed >> 3))) == 0) SEGMENT.aux1 += *huedir; - if(SEGMENT.call % (255 / (SEGMENT.speed >> 2)) == 0) + huestep = SEGMENT.aux1; // changes gradient spread + + if(SEGMENT.call % (255 / (1 + (SEGMENT.speed >> 2))) == 0) sizechange = *sizedir; for(i = 0; i < PartSys->usedParticles; i++) { - PartSys->particles[i].hue = *basehue + (i * (SEGMENT.aux1)) / PartSys->usedParticles; // gradient distribution + // PartSys->particles[i].hue = *basehue + (i * (SEGMENT.aux1)) / PartSys->usedParticles; // gradient distribution PartSys->advPartProps[i].size += sizechange; } - *basehue++; } - if((SEGMENT.check2 || SEGMENT.check1) && SEGMENT.call % (192 / ((SEGMENT.speed >> 2) + 128)) == 0) // color waves + if((SEGMENT.check2 || SEGMENT.check1) && SEGMENT.call % (160 / ((SEGMENT.speed >> 3) + 128)) == 0) // color waves { + int32_t decrement = 2; + if(SEGMENT.check1) + decrement = 1; //slower hue change in pride mode for(i = 0; i < PartSys->usedParticles; i++) { - PartSys->particles[i].hue -= 2; + PartSys->particles[i].hue -= decrement; } } // wrap around (cannot use particle system wrap if distributing colors manually, it also wraps rendering which does not look good) // TODO: disable wrap, start at -1 and go to maxX + 1, wrap manually, need to set x to particle[i+1].x - spacing when wrapping - uint32_t huestep = (((uint32_t)SEGMENT.custom2 << 19) / PartSys->usedParticles) >> 16; // hue increment - for(i = 0; i < PartSys->usedParticles; i++) { if(PartSys->particles[i].x > PartSys->maxX + PS_P_RADIUS_1D + PartSys->advPartProps[i].size) // wrap it around From 02397acb0a9639740864cfdb7049bb1ea19ad209 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sat, 6 Jul 2024 08:09:59 +0200 Subject: [PATCH 117/219] SEGMENT -> SEGENV on data, aux0, aux1, step also removed debug outputs --- wled00/FX.cpp | 401 +++++++++++++++--------------------- wled00/FXparticleSystem.cpp | 20 +- 2 files changed, 177 insertions(+), 244 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 96cce11fa3..d9e7dd8ce6 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -7741,9 +7741,9 @@ uint16_t mode_2Dsoap() { } } // init also if dimensions changed - if (SEGENV.call == 0 || SEGMENT.aux0 != cols || SEGMENT.aux1 != rows) { - SEGMENT.aux0 = cols; - SEGMENT.aux1 = rows; + if (SEGENV.call == 0 || SEGENV.aux0 != cols || SEGENV.aux1 != rows) { + SEGENV.aux0 = cols; + SEGENV.aux1 = rows; for (int i = 0; i < cols; i++) { for (int j = 0; j < rows; j++) { SEGMENT.setPixelColorXY(i, j, ColorFromPalette(SEGPALETTE,~noise3d[XY(i,j)]*3)); @@ -7909,8 +7909,7 @@ uint16_t mode_particlevortex(void) if (!initParticleSystem2D(PartSys, NUMBEROFSOURCES)) return mode_static(); // allocation failed - //SEGMENT.aux0 = 0; // starting angle - SEGMENT.aux1 = 0x01; // check flags + SEGENV.aux1 = 0x01; // check flags #ifdef ESP8266 PartSys->setMotionBlur(150); #else @@ -7927,13 +7926,10 @@ uint16_t mode_particlevortex(void) PartSys->setKillOutOfBounds(true); } else - PartSys = reinterpret_cast(SEGMENT.data); // if not first call, just set the pointer to the PS + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS if (PartSys == NULL) - { - DEBUG_PRINT(F("ERROR: FX PartSys nullpointer")); return mode_static(); // something went wrong, no data! - } PartSys->updateSystem(); // update system properties (dimensions and data pointers) uint8_t spraycount = min(PartSys->numSources, (uint32_t)(1 + (SEGMENT.custom1 >> 5))); // number of sprays to display, 1-8 @@ -7946,12 +7942,12 @@ uint16_t mode_particlevortex(void) PartSys->particles[PartSys->numParticles - i].ttl = 255; //set alive } #endif - if (SEGMENT.check1 != (SEGMENT.aux1 & 0x01) || SEGMENT.call == 0) // state change + if (SEGMENT.check1 != (SEGENV.aux1 & 0x01) || SEGMENT.call == 0) // state change { if (SEGMENT.check1) - SEGMENT.aux1 |= 0x01; //set the flag + SEGENV.aux1 |= 0x01; //set the flag else - SEGMENT.aux1 &= ~0x01; // clear the flag + SEGENV.aux1 &= ~0x01; // clear the flag for (i = 0; i < spraycount; i++) { @@ -7970,12 +7966,12 @@ uint16_t mode_particlevortex(void) // set rotation direction and speed // can use direction flag to determine current direction bool direction = SEGMENT.check2; //no automatic direction change, set it to flag - int32_t currentspeed = (int32_t)SEGMENT.step; // make a signed integer out of step + int32_t currentspeed = (int32_t)SEGENV.step; // make a signed integer out of step if (SEGMENT.custom2 > 0) // automatic direction change enabled { uint16_t changeinterval = 15 + 255 / SEGMENT.custom2; - direction = SEGMENT.aux1 & 0x02; //set direction according to flag + direction = SEGENV.aux1 & 0x02; //set direction according to flag if (SEGMENT.check3) // random interval { @@ -7984,11 +7980,11 @@ uint16_t mode_particlevortex(void) if (SEGMENT.call % changeinterval == 0) //flip direction on next frame { - SEGMENT.aux1 |= 0x04; // set the update flag (for random interval update) + SEGENV.aux1 |= 0x04; // set the update flag (for random interval update) if (direction) - SEGMENT.aux1 &= ~0x02; // clear the direction flag + SEGENV.aux1 &= ~0x02; // clear the direction flag else - SEGMENT.aux1 |= 0x02; // set the direction flag + SEGENV.aux1 |= 0x02; // set the direction flag } } @@ -8005,8 +8001,8 @@ uint16_t mode_particlevortex(void) } currentspeed += speedincrement; - SEGMENT.aux0 += currentspeed; - SEGMENT.step = (uint32_t)currentspeed; //save it back + SEGENV.aux0 += currentspeed; + SEGENV.step = (uint32_t)currentspeed; //save it back // calculate angle offset for an even distribution uint16_t angleoffset = 0xFFFF / spraycount; @@ -8020,7 +8016,7 @@ uint16_t mode_particlevortex(void) #ifdef ESP8266 if (SEGMENT.call & 0x01) // every other frame, do not emit to save particles #endif - PartSys->angleEmit(PartSys->sources[j], SEGMENT.aux0 + angleoffset * j, (SEGMENT.intensity >> 2)+1); + PartSys->angleEmit(PartSys->sources[j], SEGENV.aux0 + angleoffset * j, (SEGMENT.intensity >> 2)+1); //PartSys->sprayEmit(PartSys->sources[j]); j = (j + 1) % spraycount; } @@ -8063,13 +8059,11 @@ uint16_t mode_particlefireworks(void) } } else - PartSys = reinterpret_cast(SEGMENT.data); // if not first call, just set the pointer to the PS + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS if (PartSys == NULL) - { - DEBUG_PRINT(F("ERROR: FX PartSys nullpointer")); return mode_static(); // something went wrong, no data! - } + PartSys->updateSystem(); // update system properties (dimensions and data pointers) numRockets = min(PartSys->numSources, (uint32_t)NUMBEROFSOURCES); @@ -8246,13 +8240,10 @@ uint16_t mode_particlevolcano(void) } } else - PartSys = reinterpret_cast(SEGMENT.data); // if not first call, just set the pointer to the PS + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS if (PartSys == NULL) - { - DEBUG_PRINT(F("ERROR: FX PartSys nullpointer")); return mode_static(); // something went wrong, no data! - } numSprays = min(PartSys->numSources, (uint32_t)NUMBEROFSOURCES); // number of sprays @@ -8308,18 +8299,15 @@ uint16_t mode_particlefire(void) { if (!initParticleSystem2D(PartSys, 25, 4)) //maximum number of source (PS will determine the exact number based on segment size) and need 4 additional bytes for time keeping (uint32_t lastcall) return mode_static(); // allocation failed; //allocation failed - SEGMENT.aux0 = random16(); // aux0 is wind position (index) in the perlin noise + SEGENV.aux0 = random16(); // aux0 is wind position (index) in the perlin noise numFlames = PartSys->numSources; - DEBUG_PRINTF_P(PSTR("segment data ptr in fireFX %p\n"), SEGMENT.data); } else - PartSys = reinterpret_cast(SEGMENT.data); // if not first call, just set the pointer to the PS + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS if (PartSys == NULL) - { - DEBUG_PRINT(F("ERROR: FX PartSys nullpointer")); return mode_static(); // something went wrong, no data! - } + PartSys->updateSystem(); // update system properties (dimensions and data pointers) PartSys->setWrapX(SEGMENT.check2); @@ -8372,14 +8360,14 @@ uint16_t mode_particlefire(void) if (SEGMENT.call & 0x01) // update noise position every second frames, also add wind { - SEGMENT.aux0++; // position in the perlin noise matrix for wind generation + SEGENV.aux0++; // position in the perlin noise matrix for wind generation if (SEGMENT.call & 0x02) // every third frame - SEGMENT.aux1++; // move in noise y direction so noise does not repeat as often + SEGENV.aux1++; // move in noise y direction so noise does not repeat as often // add wind force to all particles - int8_t windspeed = ((int16_t)(inoise8(SEGMENT.aux0, SEGMENT.aux1) - 127) * SEGMENT.custom2) >> 7; + int8_t windspeed = ((int16_t)(inoise8(SEGENV.aux0, SEGENV.aux1) - 127) * SEGMENT.custom2) >> 7; PartSys->applyForce(windspeed, 0); } - SEGMENT.step++; + SEGENV.step++; if (SEGMENT.check3) //add turbulance (parameters and algorithm found by experimentation) { @@ -8389,7 +8377,7 @@ uint16_t mode_particlefire(void) { if (PartSys->particles[i].y < PartSys->maxY/4) // do not apply turbulance everywhere -> bottom quarter seems a good balance { - int32_t curl = ((int32_t)inoise8(PartSys->particles[i].x, PartSys->particles[i].y , SEGMENT.step << 4) - 127); + int32_t curl = ((int32_t)inoise8(PartSys->particles[i].x, PartSys->particles[i].y , SEGENV.step << 4) - 127); PartSys->particles[i].vx += (curl * (firespeed + 10)) >> 9; } @@ -8432,13 +8420,11 @@ uint16_t mode_particlepit(void) PartSys->setUsedParticles((PartSys->numParticles*3)/2); // use 2/3 of available particles } else - PartSys = reinterpret_cast(SEGMENT.data); // if not first call, just set the pointer to the PS + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS if (PartSys == NULL) - { - DEBUG_PRINT(F("ERROR: FX PartSys nullpointer")); return mode_static(); // something went wrong, no data! - } + PartSys->updateSystem(); // update system properties (dimensions and data pointers) PartSys->setWrapX(SEGMENT.check1); PartSys->setBounceX(SEGMENT.check2); @@ -8531,13 +8517,11 @@ uint16_t mode_particlewaterfall(void) } } else - PartSys = reinterpret_cast(SEGMENT.data); // if not first call, just set the pointer to the PS + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS if (PartSys == NULL) - { - DEBUG_PRINT(F("ERROR: FX PartSys nullpointer")); return mode_static(); // something went wrong, no data! (TODO: ask how to handle this so it always works) - } + // Particle System settings PartSys->updateSystem(); // update system properties (dimensions and data pointers) PartSys->setWrapX(SEGMENT.check1); // cylinder @@ -8601,35 +8585,32 @@ uint16_t mode_particlebox(void) PartSys->setBounceY(true); // set max number of particles and save to aux1 for later #ifdef ESP8266 - SEGMENT.aux1 = min((uint32_t)((PartSys->maxXpixel * PartSys->maxYpixel) >> 1), PartSys->numParticles); + SEGENV.aux1 = min((uint32_t)((PartSys->maxXpixel * PartSys->maxYpixel) >> 1), PartSys->numParticles); #else - SEGMENT.aux1 = min((uint32_t)((PartSys->maxXpixel * PartSys->maxYpixel)), PartSys->numParticles); // max number of particles + SEGENV.aux1 = min((uint32_t)((PartSys->maxXpixel * PartSys->maxYpixel)), PartSys->numParticles); // max number of particles #endif - for (i = 0; i < SEGMENT.aux1; i++) + for (i = 0; i < SEGENV.aux1; i++) { PartSys->particles[i].ttl = 500; // set all particles alive (not all are rendered though) PartSys->particles[i].perpetual = true; // never die PartSys->particles[i].hue = i * 3; // color range - PartSys->particles[i].x = map(i, 0, SEGMENT.aux1, 1, PartSys->maxX); // distribute along x according to color + PartSys->particles[i].x = map(i, 0, SEGENV.aux1, 1, PartSys->maxX); // distribute along x according to color PartSys->particles[i].y = random16(PartSys->maxY >> 2); // bottom quarter PartSys->particles[i].collide = true; // all particles collide } - SEGMENT.aux0 = rand(); // position in perlin noise + SEGENV.aux0 = rand(); // position in perlin noise } else - PartSys = reinterpret_cast(SEGMENT.data); // if not first call, just set the pointer to the PS + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS if (PartSys == NULL) - { - DEBUG_PRINT(F("ERROR: FX PartSys nullpointer")); return mode_static(); // something went wrong, no data! - } PartSys->updateSystem(); // update system properties (dimensions and data pointers) PartSys->setWallHardness(min(SEGMENT.custom2, (uint8_t)200)); // wall hardness is 200 or more PartSys->enableParticleCollisions(true, max(2, (int)SEGMENT.custom2)); // enable collisions and set particle collision hardness - PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 10, SEGMENT.aux1)); // aux1 holds max number of particles to use + PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 10, SEGENV.aux1)); // aux1 holds max number of particles to use if (SEGMENT.call % (((255 - SEGMENT.speed) >> 6) + 1) == 0 && SEGMENT.speed > 0) // how often the force is applied depends on speed setting @@ -8639,33 +8620,33 @@ uint16_t mode_particlebox(void) int32_t increment = (SEGMENT.speed >> 6) + 1; /*if(SEGMENT.check2) // direction - SEGMENT.aux0 += increment; // update counter + SEGENV.aux0 += increment; // update counter else - SEGMENT.aux0 -= increment; + SEGENV.aux0 -= increment; */ if(SEGMENT.check2) // washing machine { int speed = tristate_square8(strip.now >> 7, 90, 15) / ((400 - SEGMENT.speed) >> 3); - SEGMENT.aux0 += speed; - if(speed == 0) SEGMENT.aux0 = 190; //down (= 270°) + SEGENV.aux0 += speed; + if(speed == 0) SEGENV.aux0 = 190; //down (= 270°) } else - SEGMENT.aux0 -= increment; + SEGENV.aux0 -= increment; if(SEGMENT.check1) // random, use perlin noise { - xgravity = ((int16_t)inoise8(SEGMENT.aux0) - 127); - ygravity = ((int16_t)inoise8(SEGMENT.aux0 + 10000) - 127); + xgravity = ((int16_t)inoise8(SEGENV.aux0) - 127); + ygravity = ((int16_t)inoise8(SEGENV.aux0 + 10000) - 127); // scale the gravity force xgravity = (xgravity * SEGMENT.custom1) / 128; ygravity = (ygravity * SEGMENT.custom1) / 128; } else // go in a circle { - xgravity = ((int32_t)(SEGMENT.custom1) * cos16(SEGMENT.aux0 << 8)) / 0xFFFF; - ygravity = ((int32_t)(SEGMENT.custom1) * sin16(SEGMENT.aux0 << 8)) / 0xFFFF; + xgravity = ((int32_t)(SEGMENT.custom1) * cos16(SEGENV.aux0 << 8)) / 0xFFFF; + ygravity = ((int32_t)(SEGMENT.custom1) * sin16(SEGENV.aux0 << 8)) / 0xFFFF; } if (SEGMENT.check3) // sloshing, y force is alwys downwards { @@ -8701,20 +8682,18 @@ uint16_t mode_particleperlin(void) if (!initParticleSystem2D(PartSys, 1, 0, true)) // init with 1 source and advanced properties return mode_static(); // allocation failed; //allocation failed PartSys->setKillOutOfBounds(true); // should never happen, but lets make sure there are no stray particles - SEGMENT.aux0 = rand(); + SEGENV.aux0 = rand(); for (i = 0; i < PartSys->numParticles; i++) { PartSys->particles[i].collide = true; // all particles colllide } } else - PartSys = reinterpret_cast(SEGMENT.data); // if not first call, just set the pointer to the PS + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS if (PartSys == NULL) - { - DEBUG_PRINT(F("ERROR: FX PartSys nullpointer")); return mode_static(); // something went wrong, no data! - } + PartSys->updateSystem(); // update system properties (dimensions and data pointers) PartSys->setWrapX(SEGMENT.check1); PartSys->setBounceX(!SEGMENT.check1); @@ -8725,7 +8704,7 @@ uint16_t mode_particleperlin(void) PartSys->setUsedParticles(displayparticles); PartSys->setMotionBlur(230); // anable motion blur // apply 'gravity' from a 2D perlin noise map - SEGMENT.aux0 += 1+(SEGMENT.speed >> 5); // noise z-position + SEGENV.aux0 += 1 + (SEGMENT.speed >> 5); // noise z-position // update position in noise for (i = 0; i < displayparticles; i++) { @@ -8738,12 +8717,12 @@ uint16_t mode_particleperlin(void) uint32_t scale = 16 - ((31 - SEGMENT.custom3) >> 1); uint16_t xnoise = PartSys->particles[i].x / scale; // position in perlin noise, scaled by slider uint16_t ynoise = PartSys->particles[i].y / scale; - int16_t baseheight = inoise8(xnoise, ynoise, SEGMENT.aux0); // noise value at particle position + int16_t baseheight = inoise8(xnoise, ynoise, SEGENV.aux0); // noise value at particle position PartSys->particles[i].hue = baseheight; // color particles to perlin noise value if (SEGMENT.call % 8 == 0) // do not apply the force every frame, is too chaotic { - int8_t xslope = (baseheight + (int16_t)inoise8(xnoise - 10, ynoise, SEGMENT.aux0)); - int8_t yslope = (baseheight + (int16_t)inoise8(xnoise, ynoise - 10, SEGMENT.aux0)); + int8_t xslope = (baseheight + (int16_t)inoise8(xnoise - 10, ynoise, SEGENV.aux0)); + int8_t yslope = (baseheight + (int16_t)inoise8(xnoise, ynoise - 10, SEGENV.aux0)); PartSys->applyForce(i, xslope, yslope); } } @@ -8787,13 +8766,10 @@ uint16_t mode_particleimpact(void) } } else - PartSys = reinterpret_cast(SEGMENT.data); // if not first call, just set the pointer to the PS + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS if (PartSys == NULL) - { - DEBUG_PRINT(F("ERROR: FX PartSys nullpointer")); return mode_static(); // something went wrong, no data! (TODO: ask how to handle this so it always works) - } // Particle System settings PartSys->updateSystem(); // update system properties (dimensions and data pointers) @@ -8926,13 +8902,11 @@ uint16_t mode_particleattractor(void) PartSys->setWallRoughness(200); //randomize wall bounce } else - PartSys = reinterpret_cast(SEGMENT.data); // if not first call, just set the pointer to the PS + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS if (PartSys == NULL) - { - DEBUG_PRINT(F("ERROR: FX PartSys nullpointer")); return mode_static(); // something went wrong, no data! - } + // Particle System settings PartSys->updateSystem(); // update system properties (dimensions and data pointers) @@ -8969,11 +8943,11 @@ uint16_t mode_particleattractor(void) if (SEGMENT.call % 5 == 0) PartSys->sources[0].source.hue++; - SEGMENT.aux0 += 256; // emitting angle, one full turn in 255 frames (0xFFFF is 360°) + SEGENV.aux0 += 256; // emitting angle, one full turn in 255 frames (0xFFFF is 360°) if (SEGMENT.call % 2 == 0) // alternate direction of emit - PartSys->angleEmit(PartSys->sources[0], SEGMENT.aux0, 12); + PartSys->angleEmit(PartSys->sources[0], SEGENV.aux0, 12); else - PartSys->angleEmit(PartSys->sources[0], SEGMENT.aux0 + 0x7FFF, 12); // emit at 180° as well + PartSys->angleEmit(PartSys->sources[0], SEGENV.aux0 + 0x7FFF, 12); // emit at 180° as well // apply force #ifdef USERMOD_AUDIOREACTIVE um_data_t *um_data; @@ -9057,13 +9031,11 @@ uint16_t mode_particleattractor(void) PartSys->sources[0].var = 4; // emiting variation } else - PartSys = reinterpret_cast(SEGMENT.data); // if not first call, just set the pointer to the PS + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS if (PartSys == NULL) - { - DEBUG_PRINT(F("ERROR: FX PartSys nullpointer")); return mode_static(); // something went wrong, no data! - } + // Particle System settings PartSys->updateSystem(); // update system properties (dimensions and data pointers) PartSys->setWallHardness(230); // walls are always same hardness @@ -9099,18 +9071,18 @@ uint16_t mode_particleattractor(void) PartSys->sources[0].source.ttl = 100; // spray never dies } - SEGMENT.aux0 += 256; // emitting angle, one full turn in 255 frames (0xFFFF is 360°) + SEGENV.aux0 += 256; // emitting angle, one full turn in 255 frames (0xFFFF is 360°) if (SEGMENT.call % 2 == 0) // alternate direction of emit - PartSys->angleEmit(PartSys->sources[0], SEGMENT.aux0, SEGMENT.custom1 >> 4); + PartSys->angleEmit(PartSys->sources[0], SEGENV.aux0, SEGMENT.custom1 >> 4); else - PartSys->angleEmit(PartSys->sources[0], SEGMENT.aux0 + 0x7FFF, SEGMENT.custom1 >> 4); // emit at 180° as well + PartSys->angleEmit(PartSys->sources[0], SEGENV.aux0 + 0x7FFF, SEGMENT.custom1 >> 4); // emit at 180° as well - SEGMENT.aux1 = 0;//++; //line attractor angle + SEGENV.aux1 = 0;//++; //line attractor angle // apply force if(SEGMENT.call % 2 == 0) for (i = 0; i < displayparticles; i++) { - //PartSys->lineAttractor(&PartSys->particles[i], attractor, SEGMENT.aux1, &counters[i], SEGMENT.speed); //TODO: upate this to advanced particles!!! + //PartSys->lineAttractor(&PartSys->particles[i], attractor, SEGENV.aux1, &counters[i], SEGMENT.speed); //TODO: upate this to advanced particles!!! } if (SEGMENT.call % (33 - SEGMENT.custom3) == 0) PartSys->applyFriction(2); @@ -9158,13 +9130,10 @@ uint16_t mode_particlespray(void) } else - PartSys = reinterpret_cast(SEGMENT.data); // if not first call, just set the pointer to the PS + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS if (PartSys == NULL) - { - DEBUG_PRINT(F("ERROR: FX PartSys nullpointer")); return mode_static(); // something went wrong, no data! - } // Particle System settings PartSys->updateSystem(); // update system properties (dimensions and data pointers) @@ -9251,13 +9220,11 @@ uint16_t mode_particleGEQ(void) PartSys->setUsedParticles((PartSys->numParticles * 3) / 2); // use 2/3 of available particles } else - PartSys = reinterpret_cast(SEGMENT.data); // if not first call, just set the pointer to the PS + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS if (PartSys == NULL) - { - DEBUG_PRINT(F("ERROR: FX PartSys nullpointer")); return mode_static(); // something went wrong, no data! - } + uint32_t i; // set particle system properties @@ -9359,14 +9326,12 @@ if (SEGLEN == 1) PartSys->setKillOutOfBounds(true); } else - PartSys = reinterpret_cast(SEGMENT.data); // if not first call, just set the pointer to the PS + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS if (PartSys == NULL) - { - DEBUG_PRINT(F("ERROR: FX PartSys nullpointer")); return mode_static(); // something went wrong, no data! - } - PartSys->updateSystem(); // update system properties (dimensions and data pointers) + + PartSys->updateSystem(); // update system properties (dimensions and data pointers) numSprays = min(PartSys->numSources, (uint32_t)NUMBEROFSOURCES); um_data_t *um_data; @@ -9378,9 +9343,9 @@ if (SEGLEN == 1) if (SEGMENT.check2) - SEGMENT.aux0 += SEGMENT.custom1 << 2; + SEGENV.aux0 += SEGMENT.custom1 << 2; else - SEGMENT.aux0 -= SEGMENT.custom1 << 2; + SEGENV.aux0 -= SEGMENT.custom1 << 2; uint16_t angleoffset = (uint16_t)0xFFFF / (uint16_t)numSprays; uint32_t j = random(numSprays); // start with random spray so all get a chance to emit a particle if maximum number of particles alive is reached. @@ -9390,7 +9355,7 @@ if (SEGLEN == 1) PartSys->sources[j].source.hue += 1 + (SEGMENT.custom2 >> 4); PartSys->sources[j].var = SEGMENT.custom3>>1; int8_t emitspeed = 5 + (((uint32_t)fftResult[j] * ((uint32_t)SEGMENT.speed+20)) >> 10); // emit speed according to loudness of band - uint16_t emitangle = j * angleoffset + SEGMENT.aux0; + uint16_t emitangle = j * angleoffset + SEGENV.aux0; uint32_t emitparticles = 0; if (fftResult[j] > threshold) @@ -9435,30 +9400,28 @@ uint16_t mode_particleghostrider(void) PartSys->sources[0].minLife = 250; PartSys->sources[0].source.x = random16(PartSys->maxX); PartSys->sources[0].source.y = random16(PartSys->maxY); - SEGMENT.step = random(MAXANGLESTEP) - (MAXANGLESTEP>>1); // angle increment + SEGENV.step = random(MAXANGLESTEP) - (MAXANGLESTEP>>1); // angle increment } else - PartSys = reinterpret_cast(SEGMENT.data); // if not first call, just set the pointer to the PS + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS if (PartSys == NULL) - { - DEBUG_PRINT(F("ERROR: FX PartSys nullpointer")); return mode_static(); // something went wrong, no data! - } + if(SEGMENT.intensity > 0) // spiraling { - if(SEGMENT.aux1) + if(SEGENV.aux1) { - SEGMENT.step += SEGMENT.intensity>>3; - if((int32_t)SEGMENT.step > MAXANGLESTEP) - SEGMENT.aux1 = 0; + SEGENV.step += SEGMENT.intensity>>3; + if((int32_t)SEGENV.step > MAXANGLESTEP) + SEGENV.aux1 = 0; } else { - SEGMENT.step -= SEGMENT.intensity>>3; - if((int32_t)SEGMENT.step < -MAXANGLESTEP) - SEGMENT.aux1 = 1; + SEGENV.step -= SEGMENT.intensity>>3; + if((int32_t)SEGENV.step < -MAXANGLESTEP) + SEGENV.aux1 = 1; } } // Particle System settings @@ -9479,11 +9442,11 @@ uint16_t mode_particleghostrider(void) ghostsettings.bounceX = SEGMENT.check2; ghostsettings.bounceY = SEGMENT.check2; - SEGMENT.aux0 += (int32_t)SEGMENT.step; // step is angle increment - uint16_t emitangle = SEGMENT.aux0 + 32767; // +180° + SEGENV.aux0 += (int32_t)SEGENV.step; // step is angle increment + uint16_t emitangle = SEGENV.aux0 + 32767; // +180° int32_t speed = map(SEGMENT.speed, 0, 255, 12, 64); - PartSys->sources[0].source.vx = ((int32_t)cos16(SEGMENT.aux0) * speed) / (int32_t)32767; - PartSys->sources[0].source.vy = ((int32_t)sin16(SEGMENT.aux0) * speed) / (int32_t)32767; + PartSys->sources[0].source.vx = ((int32_t)cos16(SEGENV.aux0) * speed) / (int32_t)32767; + PartSys->sources[0].source.vy = ((int32_t)sin16(SEGENV.aux0) * speed) / (int32_t)32767; PartSys->sources[0].source.ttl = 500; // source never dies (note: setting 'perpetual' is not needed if replenished each frame) PartSys->particleMoveUpdate(PartSys->sources[0].source, &ghostsettings); // set head (steal one of the particles) @@ -9530,25 +9493,23 @@ uint16_t mode_particleblobs(void) //PartSys->setParticleSize(0); //set global size to zero or motion blur cannot be used (is zero by default) } else - PartSys = reinterpret_cast(SEGMENT.data); // if not first call, just set the pointer to the PS + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS if (PartSys == NULL) - { - DEBUG_PRINT(F("ERROR: FX PartSys nullpointer")); return mode_static(); // something went wrong, no data! - } + PartSys->updateSystem(); // update system properties (dimensions and data pointers) PartSys->setUsedParticles(min(PartSys->numParticles, (uint32_t)map(SEGMENT.intensity,0 ,255, 1, (PartSys->maxXpixel * PartSys->maxYpixel)>>4))); PartSys->enableParticleCollisions(SEGMENT.check2); for (uint32_t i = 0; i < PartSys->usedParticles; i++) // update particles { - if(SEGMENT.aux0 != SEGMENT.speed || PartSys->particles[i].ttl == 0) // speed changed or dead + if(SEGENV.aux0 != SEGMENT.speed || PartSys->particles[i].ttl == 0) // speed changed or dead { PartSys->particles[i].vx = (int8_t)random(-(SEGMENT.speed >> 2), SEGMENT.speed >> 2); PartSys->particles[i].vy = (int8_t)random(-(SEGMENT.speed >> 2), SEGMENT.speed >> 2); } - if(SEGMENT.aux1 != SEGMENT.custom1 || PartSys->particles[i].ttl == 0) // size changed or dead + if(SEGENV.aux1 != SEGMENT.custom1 || PartSys->particles[i].ttl == 0) // size changed or dead PartSys->advPartSize[i].maxsize = 60 + (SEGMENT.custom1 >> 1) + random((SEGMENT.custom1 >> 2)); // set each particle to slightly randomized size //PartSys->particles[i].perpetual = SEGMENT.check2; //infinite life if set @@ -9572,8 +9533,8 @@ uint16_t mode_particleblobs(void) PartSys->advPartSize[i].pulsate = SEGMENT.check3; PartSys->advPartSize[i].wobble = SEGMENT.check1; } - SEGMENT.aux0 = SEGMENT.speed; //write state back - SEGMENT.aux1 = SEGMENT.custom1; + SEGENV.aux0 = SEGMENT.speed; //write state back + SEGENV.aux1 = SEGMENT.custom1; #ifdef USERMOD_AUDIOREACTIVE um_data_t *um_data; @@ -9661,16 +9622,13 @@ uint16_t mode_particleDrip(void) return mode_static(); // allocation failed; //allocation failed PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting) PartSys->sources[0].source.hue = random16(); - SEGMENT.aux1 = 0xFFFF; // invalidate + SEGENV.aux1 = 0xFFFF; // invalidate } else - PartSys = reinterpret_cast(SEGMENT.data); // if not first call, just set the pointer to the PS + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS if (PartSys == NULL) - { - DEBUG_PRINT(F("ERROR: FX PartSys nullpointer")); return mode_static(); // something went wrong, no data! - } // Particle System settings PartSys->updateSystem(); // update system properties (dimensions and data pointers) @@ -9707,15 +9665,15 @@ uint16_t mode_particleDrip(void) PartSys->sources[0].source.x = PartSys->maxX - PS_P_RADIUS_1D; } - if(SEGMENT.aux1 != SEGMENT.intensity) //slider changed - SEGMENT.aux0 = 1; //must not be zero or "% 0" happens below which crashes on ESP32 - SEGMENT.aux1 = SEGMENT.intensity; + if(SEGENV.aux1 != SEGMENT.intensity) //slider changed + SEGENV.aux0 = 1; //must not be zero or "% 0" happens below which crashes on ESP32 + SEGENV.aux1 = SEGMENT.intensity; // every nth frame emit a particle - if (SEGMENT.call % SEGMENT.aux0 == 0) + if (SEGMENT.call % SEGENV.aux0 == 0) { int32_t interval = 300 / ((SEGMENT.intensity) + 1); - SEGMENT.aux0 = interval + random(interval + 5); + SEGENV.aux0 = interval + random(interval + 5); // if(SEGMENT.check1) // rain mode // PartSys->sources[0].source.hue = 0; // else @@ -9782,17 +9740,14 @@ uint16_t mode_particleBouncingBalls(void) PartSys->sources[0].maxLife = 900; // maximum lifetime in frames PartSys->sources[0].minLife = PartSys->sources[0].maxLife; PartSys->setBounce(true); - SEGMENT.aux0 = 1; - SEGMENT.aux1 = 500; //set out of speed range to ensure uptate on first call + SEGENV.aux0 = 1; + SEGENV.aux1 = 500; //set out of speed range to ensure uptate on first call } else - PartSys = reinterpret_cast(SEGMENT.data); // if not first call, just set the pointer to the PS + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS if (PartSys == NULL) - { - DEBUG_PRINT(F("ERROR: FX PartSys nullpointer")); return mode_static(); // something went wrong, no data! - } // Particle System settings //uint32_t hardness = 240 + (SEGMENT.custom1>>4); @@ -9810,7 +9765,7 @@ uint16_t mode_particleBouncingBalls(void) PartSys->setGravity(0); PartSys->setWallHardness(255); bool updateballs = false; - if(SEGMENT.aux1 != SEGMENT.speed + SEGMENT.intensity + SEGMENT.check2 + SEGMENT.custom1) // user settings change + if(SEGENV.aux1 != SEGMENT.speed + SEGMENT.intensity + SEGMENT.check2 + SEGMENT.custom1) // user settings change updateballs = true; for(uint32_t i = 0; i < PartSys->usedParticles; i++) @@ -9840,16 +9795,16 @@ uint16_t mode_particleBouncingBalls(void) } // every nth frame emit a ball - if (SEGMENT.call % SEGMENT.aux0 == 0) + if (SEGMENT.call % SEGENV.aux0 == 0) { - SEGMENT.aux0 = (260 - SEGMENT.intensity) + random(280 - SEGMENT.intensity); + SEGENV.aux0 = (260 - SEGMENT.intensity) + random(280 - SEGMENT.intensity); PartSys->sources[0].source.hue = random16(); //set ball color PartSys->sources[0].sat = 255; PartSys->sources[0].size = random16(SEGMENT.custom1 >> 2, SEGMENT.custom1); PartSys->sprayEmit(PartSys->sources[0]); } } - SEGMENT.aux1 = SEGMENT.speed + SEGMENT.intensity + SEGMENT.check2 + SEGMENT.custom1; + SEGENV.aux1 = SEGMENT.speed + SEGMENT.intensity + SEGMENT.check2 + SEGMENT.custom1; for (uint32_t i = 0; i < PartSys->usedParticles; i++) { @@ -9887,13 +9842,11 @@ uint16_t mode_particleDancingShadows(void) PartSys->sources[0].minLife = PartSys->sources[0].maxLife; } else - PartSys = reinterpret_cast(SEGMENT.data); // if not first call, just set the pointer to the PS + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS if (PartSys == NULL) - { - DEBUG_PRINT(F("ERROR: FX PartSys nullpointer")); return mode_static(); // something went wrong, no data! - } + // Particle System settings PartSys->updateSystem(); // update system properties (dimensions and data pointers) @@ -9971,13 +9924,13 @@ uint16_t mode_particleDancingShadows(void) if (SEGMENT.call % (32 / (1 + (SEGMENT.custom2 >> 3))) == 0) PartSys->particles[i].hue += 2 + (SEGMENT.custom2 >> 5); //note: updating speed on the fly is not accurately possible, since it is unknown which particles are assigned to which spot - if(SEGMENT.aux0 != SEGMENT.speed) //speed changed + if(SEGENV.aux0 != SEGMENT.speed) //speed changed { //update all particle speed by setting them to current value PartSys->particles[i].vx = PartSys->particles[i].vx > 0 ? SEGMENT.speed >> 3 : -SEGMENT.speed >> 3; } } - SEGMENT.aux0 = SEGMENT.speed; + SEGENV.aux0 = SEGMENT.speed; PartSys->update(); // update and render @@ -10013,13 +9966,11 @@ uint16_t mode_particleFireworks1D(void) //} } else - PartSys = reinterpret_cast(SEGMENT.data); // if not first call, just set the pointer to the PS + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS if (PartSys == NULL) - { - DEBUG_PRINT(F("ERROR: FX PartSys nullpointer")); return mode_static(); // something went wrong, no data! - } + // Particle System settings PartSys->updateSystem(); // update system properties (dimensions and data pointers) forcecounter = PartSys->PSdataEnd; @@ -10037,9 +9988,9 @@ uint16_t mode_particleFireworks1D(void) if(PartSys->sources[0].source.ttl == 0) //time is up, relaunch { if(random(255) < SEGMENT.custom1) //randomly choose direction according to slider, fire at start of segment if true - SEGMENT.aux0 = 0; + SEGENV.aux0 = 0; else - SEGMENT.aux0 = 1; //invert direction + SEGENV.aux0 = 1; //invert direction PartSys->sources[0].source.perpetual = 0; //flag abused for rocket state PartSys->sources[0].source.hue = random16(); @@ -10055,7 +10006,7 @@ uint16_t mode_particleFireworks1D(void) PartSys->sources[0].sat = 40; // low saturation exhaust PartSys->sources[0].size = 0; // default size - if(SEGMENT.aux0) //inverted rockets launch from end + if(SEGENV.aux0) //inverted rockets launch from end { PartSys->sources[0].source.reversegrav = true; PartSys->sources[0].source.x = PartSys->maxX; //start from top @@ -10067,7 +10018,7 @@ uint16_t mode_particleFireworks1D(void) { int32_t rocketgravity = -(2 + (SEGMENT.speed>>4)); //-8 int32_t speed = PartSys->sources[0].source.vx; - if(SEGMENT.aux0) //negative speed rocket + if(SEGENV.aux0) //negative speed rocket { rocketgravity = -rocketgravity; speed = -speed; @@ -10132,13 +10083,11 @@ uint16_t mode_particleSparkler(void) return mode_static(); // allocation failed } else - PartSys = reinterpret_cast(SEGMENT.data); // if not first call, just set the pointer to the PS + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS if (PartSys == NULL) - { - DEBUG_PRINT(F("ERROR: FX PartSys nullpointer")); return mode_static(); // something went wrong, no data! - } + // Particle System settings PartSys->updateSystem(); // update system properties (dimensions and data pointers) @@ -10172,14 +10121,14 @@ uint16_t mode_particleSparkler(void) numSparklers = min(1 + (SEGMENT.custom3 >> 2), (int)numSparklers); // set used sparklers, 1 to 8 - if(SEGMENT.aux0 != SEGMENT.custom3) //number of used sparklers changed, redistribute + if(SEGENV.aux0 != SEGMENT.custom3) //number of used sparklers changed, redistribute { for(i = 1; i < numSparklers; i++) { PartSys->sources[i].source.x = (PartSys->sources[0].source.x + (PartSys->maxX / numSparklers) * i ) % PartSys->maxX; //distribute evenly } } - SEGMENT.aux0 = SEGMENT.custom3; + SEGENV.aux0 = SEGMENT.custom3; for(i = 0; i < numSparklers; i++) @@ -10222,17 +10171,14 @@ uint16_t mode_particleHourglass(void) PartSys->particles[i].ttl = 500; PartSys->particles[i].perpetual = true; } - //SEGMENT.aux0 = 0; - SEGMENT.step = 0xFFFF; + SEGENV.step = 0xFFFF; } else - PartSys = reinterpret_cast(SEGMENT.data); // if not first call, just set the pointer to the PS + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS if (PartSys == NULL) - { - DEBUG_PRINT(F("ERROR: FX PartSys nullpointer")); return mode_static(); // something went wrong, no data! - } + // Particle System settings PartSys->updateSystem(); // update system properties (dimensions and data pointers) basehue = PartSys->PSdataEnd; //assign data pointer @@ -10247,17 +10193,17 @@ uint16_t mode_particleHourglass(void) uint32_t colormode = SEGMENT.custom1 >> 5; // 0-7 - if(SEGMENT.intensity != SEGMENT.step) //initialize + if(SEGMENT.intensity != SEGENV.step) //initialize { *basehue = random16(); //choose new random color - SEGMENT.step = SEGMENT.intensity; + SEGENV.step = SEGMENT.intensity; for(uint32_t i = 0; i < PartSys->usedParticles; i++) { PartSys->particles[i].reversegrav = true; *direction = 0; - SEGMENT.aux1 = 1; //initialize below + SEGENV.aux1 = 1; //initialize below } - SEGMENT.aux0 = PartSys->usedParticles - 1; //initial state, start with highest number particle + SEGENV.aux0 = PartSys->usedParticles - 1; //initial state, start with highest number particle } for(uint32_t i = 0; i < PartSys->usedParticles; i++) //check if particle reached target position after falling @@ -10294,7 +10240,7 @@ uint16_t mode_particleHourglass(void) } - if(SEGMENT.aux1 == 1) //last countdown call before dropping starts, reset all particles + if(SEGENV.aux1 == 1) //last countdown call before dropping starts, reset all particles { for(uint32_t i = 0; i < PartSys->usedParticles; i++) { @@ -10310,31 +10256,31 @@ uint16_t mode_particleHourglass(void) } } - if(SEGMENT.aux1 == 0) //countdown passed, run + if(SEGENV.aux1 == 0) //countdown passed, run { uint32_t interval = 257 - SEGMENT.speed; // drop interval in frames, 1 second is 'speed = (257 - FPS)' speed = 0 is one drop every 257 frames if(SEGMENT.check3 && *direction) // fast reset interval = 3; if(SEGMENT.call % interval == 0) //drop a particle, do not drop more often than every second frame or particles tangle up quite badly { - if(SEGMENT.aux0 < PartSys->usedParticles) + if(SEGENV.aux0 < PartSys->usedParticles) { - PartSys->particles[SEGMENT.aux0].reversegrav = *direction; //let this particle fall or rise - PartSys->particles[SEGMENT.aux0].fixed = false; // unpin + PartSys->particles[SEGENV.aux0].reversegrav = *direction; //let this particle fall or rise + PartSys->particles[SEGENV.aux0].fixed = false; // unpin } else //overflow, flip direction { *direction = !(*direction); - SEGMENT.aux1 = 300; //set countdown + SEGENV.aux1 = 300; //set countdown } if(*direction == 0) //down - SEGMENT.aux0--; + SEGENV.aux0--; else - SEGMENT.aux0++; + SEGENV.aux0++; } } else if(SEGMENT.check2) //auto reset - SEGMENT.aux1--; //countdown + SEGENV.aux1--; //countdown //if(SEGMENT.call % (SEGMENT.speed >> 5) == 0) //more friction on higher falling rate to keep particles behaved //if(SEGMENT.call % 6 == 0) @@ -10369,13 +10315,10 @@ uint16_t mode_particle1Dspray(void) PartSys->setParticleSize(1); } else - PartSys = reinterpret_cast(SEGMENT.data); // if not first call, just set the pointer to the PS + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS if (PartSys == NULL) - { - DEBUG_PRINT(F("ERROR: FX PartSys nullpointer")); return mode_static(); // something went wrong, no data! - } // Particle System settings PartSys->updateSystem(); // update system properties (dimensions and data pointers) @@ -10440,13 +10383,10 @@ uint16_t mode_particleBalance(void) } } else - PartSys = reinterpret_cast(SEGMENT.data); // if not first call, just set the pointer to the PS + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS if (PartSys == NULL) - { - DEBUG_PRINT(F("ERROR: FX PartSys nullpointer")); return mode_static(); // something went wrong, no data! - } // Particle System settings PartSys->updateSystem(); // update system properties (dimensions and data pointers) @@ -10464,11 +10404,11 @@ uint16_t mode_particleBalance(void) { int32_t xgravity; int32_t increment = (SEGMENT.speed >> 6) + 1; - SEGMENT.aux0 += increment; + SEGENV.aux0 += increment; if(SEGMENT.check3) // random, use perlin noise - xgravity = ((int16_t)inoise8(SEGMENT.aux0) - 128); + xgravity = ((int16_t)inoise8(SEGENV.aux0) - 128); else // sinusoidal - xgravity = (int16_t)cos8(SEGMENT.aux0) - 128;//((int32_t)(SEGMENT.custom3 << 2) * cos8(SEGMENT.aux0) + xgravity = (int16_t)cos8(SEGENV.aux0) - 128;//((int32_t)(SEGMENT.custom3 << 2) * cos8(SEGENV.aux0) // scale the force xgravity = (xgravity * ((SEGMENT.custom3+1) << 2)) / 128; PartSys->applyForce(xgravity); @@ -10518,18 +10458,16 @@ uint16_t mode_particleChase(void) PartSys->particles[i].ttl = 300; PartSys->particles[i].perpetual = true; } - SEGMENT.aux0 = 0xFFFF; // invalidate + SEGENV.aux0 = 0xFFFF; // invalidate *PartSys->PSdataEnd = 1; *(PartSys->PSdataEnd + 1) = 1; } else - PartSys = reinterpret_cast(SEGMENT.data); // if not first call, just set the pointer to the PS + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS if (PartSys == NULL) - { - DEBUG_PRINT(F("ERROR: FX PartSys nullpointer")); return mode_static(); // something went wrong, no data! - } + // Particle System settings PartSys->updateSystem(); // update system properties (dimensions and data pointers) PartSys->setColorByPosition(SEGMENT.check3); @@ -10538,15 +10476,15 @@ uint16_t mode_particleChase(void) uint32_t huestep = (((uint32_t)SEGMENT.custom2 << 19) / PartSys->usedParticles) >> 16; // hue increment //PartSys->setBounce(SEGMENT.check2); uint32_t settingssum = SEGMENT.speed + SEGMENT.intensity + SEGMENT.custom1 + SEGMENT.custom2 + SEGMENT.check1 + SEGMENT.check2 + SEGMENT.check3; - if(SEGMENT.aux0 != settingssum) //settings changed changed, update + if(SEGENV.aux0 != settingssum) //settings changed changed, update { PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 1, min(PartSys->maxX / (32 + (SEGMENT.custom1 >> 1)), (int32_t)(PartSys->numParticles)))); //depends on intensity and particle size (custom1) - SEGMENT.step = (PartSys->maxX + (PS_P_RADIUS_1D << 4)) / PartSys->usedParticles; //spacing between particles - // uint32_t remainder = PartSys->maxX - ((PartSys->usedParticles) * SEGMENT.step); // unused spacing, distribute this + SEGENV.step = (PartSys->maxX + (PS_P_RADIUS_1D << 4)) / PartSys->usedParticles; //spacing between particles + // uint32_t remainder = PartSys->maxX - ((PartSys->usedParticles) * SEGENV.step); // unused spacing, distribute this for(i = 0; i < PartSys->usedParticles; i++) { - //PartSys->particles[i].x = (i - 1) * SEGMENT.step + (((i + 1) * remainder) / PartSys->usedParticles); // distribute evenly - PartSys->particles[i].x = (i - 1) * SEGMENT.step; // distribute evenly + //PartSys->particles[i].x = (i - 1) * SEGENV.step + (((i + 1) * remainder) / PartSys->usedParticles); // distribute evenly + PartSys->particles[i].x = (i - 1) * SEGENV.step; // distribute evenly PartSys->particles[i].vx = SEGMENT.speed >> 1; PartSys->advPartProps[i].size = SEGMENT.custom1; if(SEGMENT.custom2 < 255) @@ -10554,7 +10492,7 @@ uint16_t mode_particleChase(void) else PartSys->particles[i].hue = random16(); } - SEGMENT.aux0 = settingssum; + SEGENV.aux0 = settingssum; } @@ -10571,21 +10509,21 @@ uint16_t mode_particleChase(void) else if(PartSys->advPartProps[0].size <= (SEGMENT.custom1 >> 2)) *sizedir = 1; - if(SEGMENT.aux1 > 64) + if(SEGENV.aux1 > 64) *huedir = -1; - else if(SEGMENT.aux1 < 1) + else if(SEGENV.aux1 < 1) *huedir = 1; if(SEGMENT.call % (1024 / (1 + (SEGMENT.speed >> 3))) == 0) - SEGMENT.aux1 += *huedir; - huestep = SEGMENT.aux1; // changes gradient spread + SEGENV.aux1 += *huedir; + huestep = SEGENV.aux1; // changes gradient spread if(SEGMENT.call % (255 / (1 + (SEGMENT.speed >> 2))) == 0) sizechange = *sizedir; for(i = 0; i < PartSys->usedParticles; i++) { - // PartSys->particles[i].hue = *basehue + (i * (SEGMENT.aux1)) / PartSys->usedParticles; // gradient distribution + // PartSys->particles[i].hue = *basehue + (i * (SEGENV.aux1)) / PartSys->usedParticles; // gradient distribution PartSys->advPartProps[i].size += sizechange; } } @@ -10599,15 +10537,14 @@ uint16_t mode_particleChase(void) PartSys->particles[i].hue -= decrement; } } - // wrap around (cannot use particle system wrap if distributing colors manually, it also wraps rendering which does not look good) - // TODO: disable wrap, start at -1 and go to maxX + 1, wrap manually, need to set x to particle[i+1].x - spacing when wrapping + // wrap around (cannot use particle system wrap if distributing colors manually, it also wraps rendering which does not look good) for(i = 0; i < PartSys->usedParticles; i++) { if(PartSys->particles[i].x > PartSys->maxX + PS_P_RADIUS_1D + PartSys->advPartProps[i].size) // wrap it around { uint32_t nextindex = (i + 1) % PartSys->usedParticles; - PartSys->particles[i].x = PartSys->particles[nextindex].x - SEGMENT.step; + PartSys->particles[i].x = PartSys->particles[nextindex].x - SEGENV.step; if(SEGMENT.custom2 < 255) PartSys->particles[i].hue = PartSys->particles[nextindex].hue - huestep; else @@ -10646,13 +10583,11 @@ uint16_t mode_particleStarburst(void) PartSys->sources[0].sat = 0; // emitted particles start out white } else - PartSys = reinterpret_cast(SEGMENT.data); // if not first call, just set the pointer to the PS + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS if (PartSys == NULL) - { - DEBUG_PRINT(F("ERROR: FX PartSys nullpointer")); return mode_static(); // something went wrong, no data! - } + // Particle System settings PartSys->updateSystem(); // update system properties (dimensions and data pointers) PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur @@ -10718,13 +10653,11 @@ uint16_t mode_particle1DGEQ(void) return mode_static(); // allocation failed } else - PartSys = reinterpret_cast(SEGMENT.data); // if not first call, just set the pointer to the PS + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS if (PartSys == NULL) - { - DEBUG_PRINT(F("ERROR: FX PartSys nullpointer")); return mode_static(); // something went wrong, no data! - } + // Particle System settings PartSys->updateSystem(); // update system properties (dimensions and data pointers) numSources = PartSys->numSources; @@ -10872,6 +10805,8 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_COMET, &mode_comet, _data_FX_MODE_COMET); addEffect(FX_MODE_RAIN, &mode_rain, _data_FX_MODE_RAIN); addEffect(FX_MODE_ROLLINGBALLS, &rolling_balls, _data_FX_MODE_ROLLINGBALLS); + addEffect(FX_MODE_PRIDE_2015, &mode_pride_2015, _data_FX_MODE_PRIDE_2015); + addEffect(FX_MODE_COLORWAVES, &mode_colorwaves, _data_FX_MODE_COLORWAVES); #endif addEffect(FX_MODE_FIREWORKS, &mode_fireworks, _data_FX_MODE_FIREWORKS); addEffect(FX_MODE_TETRIX, &mode_tetrix, _data_FX_MODE_TETRIX); @@ -10893,13 +10828,11 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_DUAL_LARSON_SCANNER, &mode_dual_larson_scanner, _data_FX_MODE_DUAL_LARSON_SCANNER); addEffect(FX_MODE_RANDOM_CHASE, &mode_random_chase, _data_FX_MODE_RANDOM_CHASE); addEffect(FX_MODE_OSCILLATE, &mode_oscillate, _data_FX_MODE_OSCILLATE); - addEffect(FX_MODE_PRIDE_2015, &mode_pride_2015, _data_FX_MODE_PRIDE_2015); addEffect(FX_MODE_JUGGLE, &mode_juggle, _data_FX_MODE_JUGGLE); addEffect(FX_MODE_PALETTE, &mode_palette, _data_FX_MODE_PALETTE); #ifndef DISABLE_2D_PS_REPLACEMENTS addEffect(FX_MODE_FIRE_2012, &mode_fire_2012, _data_FX_MODE_FIRE_2012); #endif - addEffect(FX_MODE_COLORWAVES, &mode_colorwaves, _data_FX_MODE_COLORWAVES); addEffect(FX_MODE_BPM, &mode_bpm, _data_FX_MODE_BPM); addEffect(FX_MODE_FILLNOISE8, &mode_fillnoise8, _data_FX_MODE_FILLNOISE8); addEffect(FX_MODE_NOISE16_1, &mode_noise16_1, _data_FX_MODE_NOISE16_1); diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index 442a8cac70..24148bf0b4 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -1274,7 +1274,7 @@ CRGB **ParticleSystem::allocate2Dbuffer(uint32_t cols, uint32_t rows) } // update size and pointers (memory location and size can change dynamically) -// note: do not access the PS class in FX befor running this function (or it messes up SEGMENT.data) +// note: do not access the PS class in FX befor running this function (or it messes up SEGENV.data) void ParticleSystem::updateSystem(void) { // update matrix size @@ -1442,7 +1442,7 @@ bool allocateParticleSystemMemory2D(uint16_t numparticles, uint16_t numsources, //Serial.print(requiredmemory); //Serial.println("Bytes"); //Serial.print("allocating for segment at"); - //Serial.println((uintptr_t)SEGMENT.data); + //Serial.println((uintptr_t)SEGENV.data); return(SEGMENT.allocateData(requiredmemory)); } @@ -1459,12 +1459,12 @@ bool initParticleSystem2D(ParticleSystem *&PartSys, uint8_t requestedsources, ui DEBUG_PRINT(F("PS init failed: memory depleted")); return false; } - //Serial.print("segment.data ptr"); - //Serial.println((uintptr_t)(SEGMENT.data)); + //Serial.print("SEGENV.data ptr"); + //Serial.println((uintptr_t)(SEGENV.data)); 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, advanced, sizecontrol); // particle system constructor + PartSys = new (SEGENV.data) ParticleSystem(cols, rows, numparticles, numsources, advanced, sizecontrol); // particle system constructor //Serial.print("PS pointer at "); //Serial.println((uintptr_t)PartSys); return true; @@ -2177,7 +2177,7 @@ CRGB *ParticleSystem1D::allocate1Dbuffer(uint32_t length) } // update size and pointers (memory location and size can change dynamically) -// note: do not access the PS class in FX befor running this function (or it messes up SEGMENT.data) +// note: do not access the PS class in FX befor running this function (or it messes up SEGENV.data) void ParticleSystem1D::updateSystem(void) { // update size @@ -2267,7 +2267,7 @@ bool allocateParticleSystemMemory1D(uint16_t numparticles, uint16_t numsources, //Serial.print(requiredmemory); //Serial.println("Bytes"); //Serial.print("allocating for segment at"); - //Serial.println((uintptr_t)SEGMENT.data); + //Serial.println((uintptr_t)SEGENV.data); return(SEGMENT.allocateData(requiredmemory)); } @@ -2284,10 +2284,10 @@ bool initParticleSystem1D(ParticleSystem1D *&PartSys, uint8_t requestedsources, DEBUG_PRINT(F("PS init failed: memory depleted")); return false; } - //Serial.print("segment.data ptr"); - //Serial.println((uintptr_t)(SEGMENT.data)); + //Serial.print("SEGENV.data ptr"); + //Serial.println((uintptr_t)(SEGENV.data)); //Serial.println("calling constructor"); - PartSys = new (SEGMENT.data) ParticleSystem1D(SEGMENT.virtualLength(), numparticles, numsources, advanced); // particle system constructor + PartSys = new (SEGENV.data) ParticleSystem1D(SEGMENT.virtualLength(), numparticles, numsources, advanced); // particle system constructor //Serial.print("PS pointer at "); //Serial.println((uintptr_t)PartSys); return true; From 49f3dfe4c06a7a800ba0f0529bfae81f9d1e3898 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sat, 6 Jul 2024 08:21:55 +0200 Subject: [PATCH 118/219] added pride and colorwaves to 1D replacement list --- wled00/FX.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index d9e7dd8ce6..749639779b 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -1878,7 +1878,7 @@ uint16_t mode_lightning(void) { } static const char _data_FX_MODE_LIGHTNING[] PROGMEM = "Lightning@!,!,,,,,Overlay;!,!;!"; - +#ifndef DISABLE_1D_PS_REPLACEMENTS // Pride2015 // Animated, ever-changing rainbows. // by Mark Kriegsman: https://gist.github.com/kriegsman/964de772d64c502760e5 @@ -1919,7 +1919,7 @@ uint16_t mode_pride_2015(void) { return FRAMETIME; } static const char _data_FX_MODE_PRIDE_2015[] PROGMEM = "Pride 2015@!;;"; - +#endif // DISABLE_1D_PS_REPLACEMENTS //eight colored dots, weaving in and out of sync with each other uint16_t mode_juggle(void) { @@ -2122,6 +2122,7 @@ uint16_t mode_fire_2012() { static const char _data_FX_MODE_FIRE_2012[] PROGMEM = "Fire 2012@Cooling,Spark rate,,,Boost;;!;1;sx=64,ix=160,m12=1"; // bars #endif //DISABLE_2D_PS_REPLACEMENTS +#ifndef DISABLE_1D_PS_REPLACEMENTS // ColorWavesWithPalettes by Mark Kriegsman: https://gist.github.com/kriegsman/8281905786e8b2632aeb // This function draws color waves with an ever-changing, // widely-varying set of parameters, using a color palette. @@ -2166,7 +2167,7 @@ uint16_t mode_colorwaves() { return FRAMETIME; } static const char _data_FX_MODE_COLORWAVES[] PROGMEM = "Colorwaves@!,Hue;!;!"; - +#endif // DISABLE_1D_PS_REPLACEMENTS // colored stripes pulsing at a defined Beats-Per-Minute (BPM) uint16_t mode_bpm() { From 8f3c5e439961e73cd0b1491109262458e65a149b Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sat, 6 Jul 2024 08:26:26 +0200 Subject: [PATCH 119/219] removed unused variables --- wled00/FX.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 749639779b..db79958163 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -9953,7 +9953,6 @@ uint16_t mode_particleFireworks1D(void) ParticleSystem1D *PartSys = NULL; //uint8_t numRockets; uint8_t *forcecounter; - uint32_t i; if (SEGMENT.call == 0) // initialization { @@ -10473,7 +10472,7 @@ uint16_t mode_particleChase(void) PartSys->updateSystem(); // update system properties (dimensions and data pointers) PartSys->setColorByPosition(SEGMENT.check3); PartSys->setMotionBlur(7 + (SEGMENT.custom3 << 3)); // anable motion blur - uint8_t* basehue = (PartSys->PSdataEnd + 2); //assign data pointer + //uint8_t* basehue = (PartSys->PSdataEnd + 2); //assign data pointer uint32_t huestep = (((uint32_t)SEGMENT.custom2 << 19) / PartSys->usedParticles) >> 16; // hue increment //PartSys->setBounce(SEGMENT.check2); uint32_t settingssum = SEGMENT.speed + SEGMENT.intensity + SEGMENT.custom1 + SEGMENT.custom2 + SEGMENT.check1 + SEGMENT.check2 + SEGMENT.check3; @@ -10699,7 +10698,7 @@ uint16_t mode_particle1DGEQ(void) bin ++; bin = bin % numSources; uint32_t emitparticle = 0; - uint8_t emitspeed = ((uint32_t)fftResult[bin] * (uint32_t)SEGMENT.speed) >> 10; // emit speed according to loudness of band (127 max!) + // uint8_t emitspeed = ((uint32_t)fftResult[bin] * (uint32_t)SEGMENT.speed) >> 10; // emit speed according to loudness of band (127 max!) if (fftResult[bin] > threshold) { emitparticle = 1; From 922e5c50f9504ceefedea1ff3c4810dcd200e996 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sun, 7 Jul 2024 13:31:02 +0200 Subject: [PATCH 120/219] added variable to set number of particles if not all are required (saves ram) also fixed a nasty render bug --- wled00/FX.cpp | 24 ++++++------- wled00/FXparticleSystem.cpp | 68 +++++++++++++++++++++---------------- wled00/FXparticleSystem.h | 2 +- 3 files changed, 52 insertions(+), 42 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 1c1f49d63a..fbbc3e582d 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -9629,7 +9629,7 @@ uint16_t mode_particleDrip(void) //uint8_t numSprays; if (SEGMENT.call == 0) // initialization { - if (!initParticleSystem1D(PartSys, 4)) // init, no additional data needed + if (!initParticleSystem1D(PartSys, 4)) // init return mode_static(); // allocation failed; //allocation failed PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting) PartSys->sources[0].source.hue = random16(); @@ -9744,7 +9744,7 @@ uint16_t mode_particleBouncingBalls(void) if (SEGMENT.call == 0) // initialization { - if (!initParticleSystem1D(PartSys, 1, 0, true)) // init + if (!initParticleSystem1D(PartSys, 1, 128, 0, true)) // init return mode_static(); // allocation failed; //allocation failed PartSys->sources[0].source.collide = true; // seeded particles will collide (if enabled) PartSys->sources[0].source.x = PS_P_RADIUS_1D; //emit at bottom @@ -9768,7 +9768,7 @@ uint16_t mode_particleBouncingBalls(void) PartSys->sources[0].var = SEGMENT.speed >> 3; PartSys->sources[0].v = (SEGMENT.speed >> 1) - (SEGMENT.speed >> 3); PartSys->enableParticleCollisions(SEGMENT.check1, 254); // enable collisions and set particle collision hardness (do not use full hardness or particles speed up due to pushing, can not be made perfectly balanced) - PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 1, PartSys->numParticles >> 1)); + PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 1, PartSys->numParticles)); PartSys->setColorByPosition(SEGMENT.check3); if(SEGMENT.check2) //rolling balls @@ -9966,7 +9966,7 @@ uint16_t mode_particleFireworks1D(void) if (SEGMENT.call == 0) // initialization { - if (!initParticleSystem1D(PartSys, 4, 4, true)) // init + if (!initParticleSystem1D(PartSys, 4, 150, 4, true)) // init return mode_static(); // allocation failed PartSys->setKillOutOfBounds(true); //numRockets = PartSys->numSources; @@ -10089,7 +10089,7 @@ uint16_t mode_particleSparkler(void) if (SEGMENT.call == 0) // initialization { - if (!initParticleSystem1D(PartSys, 16, 0, true)) // init, no additional data needed + if (!initParticleSystem1D(PartSys, 16, 128 ,0, true)) // init, no additional data needed return mode_static(); // allocation failed } else @@ -10170,7 +10170,7 @@ uint16_t mode_particleHourglass(void) uint8_t* basehue; if (SEGMENT.call == 0) // initialization { - if (!initParticleSystem1D(PartSys, 0, 2)) // init + if (!initParticleSystem1D(PartSys, 0, 255, 2, false)) // init return mode_static(); // allocation failed PartSys->setBounce(true); PartSys->setWallHardness(80); @@ -10459,9 +10459,8 @@ uint16_t mode_particleChase(void) if (SEGMENT.call == 0) // initialization { - if (!initParticleSystem1D(PartSys, 1, 3, true)) // init + if (!initParticleSystem1D(PartSys, 1, 255, 3, true)) // init return mode_static(); // allocation failed - // PartSys->setWrap(true); for(i = 0; i < PartSys->numParticles; i++) { PartSys->advPartProps[i].sat = 255; @@ -10481,9 +10480,9 @@ uint16_t mode_particleChase(void) // Particle System settings PartSys->updateSystem(); // update system properties (dimensions and data pointers) PartSys->setColorByPosition(SEGMENT.check3); - PartSys->setMotionBlur(7 + (SEGMENT.custom3 << 3)); // anable motion blur + PartSys->setMotionBlur(SEGMENT.custom3 << 3); // anable motion blur //uint8_t* basehue = (PartSys->PSdataEnd + 2); //assign data pointer - uint32_t huestep = (((uint32_t)SEGMENT.custom2 << 19) / PartSys->usedParticles) >> 16; // hue increment + //PartSys->setBounce(SEGMENT.check2); uint32_t settingssum = SEGMENT.speed + SEGMENT.intensity + SEGMENT.custom1 + SEGMENT.custom2 + SEGMENT.check1 + SEGMENT.check2 + SEGMENT.check3; if(SEGENV.aux0 != settingssum) //settings changed changed, update @@ -10505,6 +10504,7 @@ uint16_t mode_particleChase(void) SEGENV.aux0 = settingssum; } + uint32_t huestep = (((uint32_t)SEGMENT.custom2 << 19) / PartSys->usedParticles) >> 16; // hue increment if(SEGMENT.check1) // pride rainbow colors { @@ -10585,7 +10585,7 @@ uint16_t mode_particleStarburst(void) if (SEGMENT.call == 0) // initialization { - if (!initParticleSystem1D(PartSys, 1, 0, true)) // init + if (!initParticleSystem1D(PartSys, 1, 200, 0, true)) // init return mode_static(); // allocation failed PartSys->setKillOutOfBounds(true); PartSys->enableParticleCollisions(true, 200); @@ -10659,7 +10659,7 @@ uint16_t mode_particle1DGEQ(void) if (SEGMENT.call == 0) // initialization { - if (!initParticleSystem1D(PartSys, 16, 0, true)) // init, no additional data needed + if (!initParticleSystem1D(PartSys, 16, 255, 0, true)) // init, no additional data needed return mode_static(); // allocation failed } else diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index 24148bf0b4..2c33ff3bf5 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -871,6 +871,7 @@ void ParticleSystem::renderParticle(CRGB **framebuffer, uint32_t particleindex, if (dx == PS_P_RADIUS) { pxlbrightness[1] = pxlbrightness[2] = -1; // pixel is actually out of matrix boundaries, do not render + dx = 2; // fix for advanced renderer (it does render slightly out of frame particles) } if (particlesettings.wrapX) // wrap x to the other side if required pixco[0][0] = pixco[3][0] = maxXpixel; @@ -891,6 +892,7 @@ void ParticleSystem::renderParticle(CRGB **framebuffer, uint32_t particleindex, if (dy == PS_P_RADIUS) { pxlbrightness[2] = pxlbrightness[3] = -1; // pixel is actually out of matrix boundaries, do not render + dy = 2; // fix for advanced renderer (it does render slightly out of frame particles) } if (particlesettings.wrapY) // wrap y to the other side if required pixco[0][1] = pixco[1][1] = maxYpixel; @@ -1901,6 +1903,7 @@ void ParticleSystem1D::ParticleSys_render() } if (renderbuffer) free(renderbuffer); + Serial.println("*"); } // calculate pixel positions and brightness distribution and render the particle to local buffer or global buffer @@ -1923,13 +1926,15 @@ void ParticleSystem1D::renderParticle(CRGB *framebuffer, uint32_t particleindex, } } else { //render larger particles - int32_t pxlbrightness[2] = {0}; // note: pxlbrightness needs to be set to 0 or checking does not work + bool pxlisinframe[2] = {true, true}; + int32_t pxlbrightness[2]; int32_t pixco[2]; // physical pixel coordinates of the two pixels representing a particle // subtract half a radius as the rendering algorithm always starts at the left, this makes calculations more efficient int32_t xoffset = particles[particleindex].x - PS_P_HALFRADIUS_1D; int32_t dx = xoffset % PS_P_RADIUS_1D; //relativ particle position in subpixel space - int32_t x = xoffset >> PS_P_RADIUS_SHIFT_1D; // divide by PS_P_RADIUS which is 64, so can bitshift (compiler may not optimize automatically) + int32_t x = xoffset >> PS_P_RADIUS_SHIFT_1D; // divide by PS_P_RADIUS, bitshift of negative number stays negative -> checking below for x < 0 works (but does not when using division) + // set the raw pixel coordinates pixco[0] = x; // left pixel pixco[1] = x + 1; // right pixel @@ -1938,32 +1943,39 @@ void ParticleSystem1D::renderParticle(CRGB *framebuffer, uint32_t particleindex, if (x < 0) // left pixels out of frame { dx = PS_P_RADIUS_1D + dx; // if x<0, xoffset becomes negative (and so does dx), must adjust dx as modulo will flip its value - // 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: + // note: due to inverted shift math, a particel at position -16 (xoffset = -32, dx = 32) is rendered at the wrong pixel position (it should be out of frame) + // checking this above would make this algorithm slower (in frame pixels do not have to be checked), so just correct for it here: if (dx == PS_P_RADIUS_1D) { - pxlbrightness[1] = -1; // pixel is actually out of matrix boundaries, do not render + pxlisinframe[1] = false; // pixel is actually out of matrix boundaries, do not render + dx = 0; // fix for out of frame advanced particles (dx=0 is changed to dx=PS_P_RADIUS_1D in above statement, 0 is correct) } if (particlesettings.wrapX) // wrap x to the other side if required pixco[0] = maxXpixel; else - pxlbrightness[0] = -1; // pixel is out of matrix boundaries, do not render + pxlisinframe[0] = false; // pixel is out of matrix boundaries, do not render } else if (pixco[1] > maxXpixel) // right pixel, only has to be checkt if left pixel did not overflow { if (particlesettings.wrapX) // wrap y to the other side if required pixco[1] = 0; else - pxlbrightness[1] = -1; + pxlisinframe[1] = false; } - // calculate brightness values for the two pixels representing a particle using linear interpolation + if(xoffset < 500) + { + Serial.print(xoffset); + Serial.print(" "); + Serial.print(dx); + Serial.print(" "); + Serial.print(x); + Serial.print("/"); + } - //calculate the values for pixels that are in frame - if (pxlbrightness[0] >= 0) - pxlbrightness[0] = (((int32_t)PS_P_RADIUS_1D - dx) * brightness) >> PS_P_SURFACE_1D; - if (pxlbrightness[1] >= 0) - pxlbrightness[1] = (dx * brightness) >> PS_P_SURFACE_1D; + //calculate the brightness values for both pixels using linear interpolation (note: in standard rendering out of frame pixels could be skipped but if checks add more clock cycles over all) + pxlbrightness[0] = (((int32_t)PS_P_RADIUS_1D - dx) * brightness) >> PS_P_SURFACE_1D; + pxlbrightness[1] = (dx * brightness) >> PS_P_SURFACE_1D; // check if particle has advanced size properties and buffer is available if (advPartProps && advPartProps[particleindex].size > 1) @@ -1977,11 +1989,11 @@ void ParticleSystem1D::renderParticle(CRGB *framebuffer, uint32_t particleindex, //render particle to a bigger size - //particle size to pixels: < 64 is 4 pixels, < 128 is 6pixels, < 192 is 8 pixels, bigger is 10 pixels + //particle size to pixels: 2 - 63 is 4 pixels, < 128 is 6pixels, < 192 is 8 pixels, bigger is 10 pixels //first, render the pixel to the center of the renderbuffer, then apply 1D blurring fast_color_add(renderbuffer[4], color, pxlbrightness[0]); fast_color_add(renderbuffer[5], color, pxlbrightness[1]); - uint32_t rendersize = 2; // initialize render size, minimum is 4x4 pixels, it is incremented int he loop below to start with 4 + uint32_t rendersize = 2; // initialize render size, minimum is 4 pixels, it is incremented int he loop below to start with 4 uint32_t offset = 4; // offset to zero coordinate to write/read data in renderbuffer (actually needs to be 3, is decremented in the loop below) uint32_t blurpasses = size/64 + 1; // number of blur passes depends on size, four passes max uint32_t bitshift = 0; @@ -1996,7 +2008,7 @@ void ParticleSystem1D::renderParticle(CRGB *framebuffer, uint32_t particleindex, } // calculate origin coordinates to render the particle to in the framebuffer - uint32_t xfb_orig = x - (rendersize>>1) + 1 - offset; + uint32_t xfb_orig = x - (rendersize>>1) + 1 - offset; //note: using uint is fine uint32_t xfb; // coordinates in frame buffer to write to note: by making this uint, only overflow has to be checked // transfer particle renderbuffer to framebuffer @@ -2018,20 +2030,17 @@ void ParticleSystem1D::renderParticle(CRGB *framebuffer, uint32_t particleindex, fast_color_add(framebuffer[xfb], renderbuffer[xrb]); } } - else if (framebuffer) // standard rendering (2 pixels per particle) + else // standard rendering (2 pixels per particle) { for(uint32_t i = 0; i < 2; i++) { - if (pxlbrightness[i] > 0) - fast_color_add(framebuffer[pixco[i]], color, pxlbrightness[i]); // order is: bottom left, bottom right, top right, top left - } - } - else - { - for(uint32_t i = 0; i < 2; i++) - { - if (pxlbrightness[i] > 0) - SEGMENT.addPixelColor(pixco[i], color.scale8((uint8_t)pxlbrightness[i])); + if (pxlisinframe[i]) + { + if (framebuffer) + fast_color_add(framebuffer[pixco[i]], color, pxlbrightness[i]); + else + SEGMENT.addPixelColor(pixco[i], color.scale8((uint8_t)pxlbrightness[i])); + } } } } @@ -2272,10 +2281,11 @@ bool allocateParticleSystemMemory1D(uint16_t numparticles, uint16_t numsources, } // initialize Particle System, allocate additional bytes if needed (pointer to those bytes can be read from particle system class: PSdataEnd) -bool initParticleSystem1D(ParticleSystem1D *&PartSys, uint8_t requestedsources, uint16_t additionalbytes, bool advanced) +// note: requestedparticles is relative, 127 = 50%, 255 = 100% (deafaults to 100% meaning one particle per pixel) +bool initParticleSystem1D(ParticleSystem1D *&PartSys, uint32_t requestedsources, uint32_t requestedparticles, uint16_t additionalbytes, bool advanced) { //Serial.println("PS init function"); - uint32_t numparticles = calculateNumberOfParticles1D(advanced); + uint32_t numparticles = (requestedparticles * calculateNumberOfParticles1D(advanced)) / 255; uint32_t numsources = calculateNumberOfSources1D(requestedsources); //Serial.print("numsources: "); //Serial.println(numsources); diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index f96b387e17..e17bfa423c 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -376,7 +376,7 @@ class ParticleSystem1D uint8_t motionBlur; // enable motion blur, values > 100 gives smoother animations. Note: motion blurring does not work if particlesize is > 0 }; -bool initParticleSystem1D(ParticleSystem1D *&PartSys, uint8_t requestedsources, uint16_t additionalbytes = 0, bool advanced = false); +bool initParticleSystem1D(ParticleSystem1D *&PartSys, uint32_t requestedsources, uint32_t requestedparticles = 255, uint16_t additionalbytes = 0, bool advanced = false); uint32_t calculateNumberOfParticles1D(bool isadvanced); uint32_t calculateNumberOfSources1D(uint8_t requestedsources); bool allocateParticleSystemMemory1D(uint16_t numparticles, uint16_t numsources, bool isadvanced, uint16_t additionalbytes); From 7efe5f84ec25e19c0788c5f45f37b18daf15f1f5 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Wed, 17 Jul 2024 18:51:03 +0200 Subject: [PATCH 121/219] added preliminary 1D fire function, unfinished --- wled00/FX.cpp | 66 ++++++++++++++++++++++++++++++++++++- wled00/FX.h | 3 +- wled00/FXparticleSystem.cpp | 11 ------- 3 files changed, 67 insertions(+), 13 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index fbbc3e582d..d4123ae993 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -8333,7 +8333,7 @@ uint16_t mode_particlefire(void) { SEGMENT.call--; //skipping a frame, decrement the counter (on call0, this is never executed as lastcall is 0, so its fine to not check if >0) //still need to render the frame or flickering will occur in transitions - PartSys->updateFire(SEGMENT.intensity, true); // render the fire without updating it + PartSys->updateFire(SEGMENT.intensity, true); // render the fire without updating particles (render only) return FRAMETIME; //do not update this frame } *lastcall = strip.now; @@ -10736,6 +10736,69 @@ uint16_t mode_particle1DGEQ(void) static const char _data_FX_MODE_PS_1D_GEQ[] PROGMEM = "PS 1D GEQ@Speed,!,Size,Blur/Overlay,,,,;,!;!;1f;pal=0,sx=50,ix=200,c1=0,c2=0,c3=0,o1=1,o2=1,o3=0"; +/* +Particle based Fire effect +Uses palette for particle color +by DedeHai (Damian Schneider) +*/ + +uint16_t mode_particleFire1D(void) +{ + if (SEGLEN == 1) + return mode_static(); + ParticleSystem1D *PartSys = NULL; + + if (SEGMENT.call == 0) // initialization + { + if (!initParticleSystem1D(PartSys, 4)) // init + return mode_static(); // allocation failed + PartSys->setKillOutOfBounds(true); + PartSys->setParticleSize(1); + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + + if (PartSys == NULL) + return mode_static(); // something went wrong, no data! + + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur + for(uint i = 0; i < PartSys->numSources; i++) + { + PartSys->sources[i].var = 1 + (SEGMENT.speed >> 4); + PartSys->sources[i].minLife = 50 + SEGMENT.intensity; + PartSys->sources[i].maxLife = 200 + (SEGMENT.intensity << 1); + PartSys->sources[i].source.x = map(SEGMENT.custom1, 0 , 255, 0, PartSys->maxX); // spray position + //PartSys->sources[0].v = map(SEGMENT.speed, 0 , 255, 1, (inoise16(SEGENV.call) >> 8)); // particle emit speed + //PartSys->sources[i].v = 2 + ((((SEGMENT.speed >> (2 + i))) * (int16_t)inoise8(SEGENV.aux0)) >> 7); + PartSys->sources[i].v = 2 + (SEGMENT.speed >> (2 + (i<<1))); + //if(random(4) == 0) + PartSys->sprayEmit(PartSys->sources[i]); //emit a particle + } +if (SEGMENT.call & 0x01) // update noise position every second frames, also add wind + { + SEGENV.aux0++; + } + + //update color settings + PartSys->setColorByAge(SEGMENT.check1); + // PartSys->setColorByPosition(SEGMENT.check3); + for(uint i = 0; i < PartSys->usedParticles; i++) + { + PartSys->particles[i].x += PartSys->particles[i].ttl >> 7; // 'hot' particles are faster, apply some extra velocity + if(PartSys->particles[i].ttl > 150) + PartSys->particles[i].ttl -= map(SEGMENT.intensity, 0, 255, 5 , 0); // age faster + } + + PartSys->update(); // update and render + + return FRAMETIME; +} +static const char _data_FX_MODE_PS_FIRE1D[] PROGMEM = "PS Fire 1D@!,!,Position,Blur/Overlay,Gravity,Color by Age,Bounce,Color by Position;,!;!;1;pal=35,sx=200,ix=220,c1=4,c2=0,c3=28,o1=1,o2=1,o3=0"; + + + #endif //WLED_DISABLE_PARTICLESYSTEM1D ////////////////////////////////////////////////////////////////////////////////////////// @@ -11016,6 +11079,7 @@ addEffect(FX_MODE_PSBALANCE, &mode_particleBalance, _data_FX_MODE_PS_BALANCE); addEffect(FX_MODE_PSCHASE, &mode_particleChase, _data_FX_MODE_PS_CHASE); addEffect(FX_MODE_PSSTARBURST, &mode_particleStarburst, _data_FX_MODE_PS_STARBURST); addEffect(FX_MODE_PS1DGEQ, &mode_particle1DGEQ, _data_FX_MODE_PS_1D_GEQ); +addEffect(FX_MODE_PSFIRE1D, &mode_particleFire1D, _data_FX_MODE_PS_FIRE1D); #endif // WLED_DISABLE_PARTICLESYSTEM1D diff --git a/wled00/FX.h b/wled00/FX.h index 6e4eb9cbf9..71cadbd3d9 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -343,7 +343,8 @@ #define FX_MODE_PSCHASE 210 #define FX_MODE_PSSTARBURST 211 #define FX_MODE_PS1DGEQ 212 -#define MODE_COUNT 213 +#define FX_MODE_PSFIRE1D 213 +#define MODE_COUNT 214 typedef enum mapping1D2D { M12_Pixels = 0, diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index 2c33ff3bf5..4236ea55b3 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -1903,7 +1903,6 @@ void ParticleSystem1D::ParticleSys_render() } if (renderbuffer) free(renderbuffer); - Serial.println("*"); } // calculate pixel positions and brightness distribution and render the particle to local buffer or global buffer @@ -1963,16 +1962,6 @@ void ParticleSystem1D::renderParticle(CRGB *framebuffer, uint32_t particleindex, pxlisinframe[1] = false; } - if(xoffset < 500) - { - Serial.print(xoffset); - Serial.print(" "); - Serial.print(dx); - Serial.print(" "); - Serial.print(x); - Serial.print("/"); - } - //calculate the brightness values for both pixels using linear interpolation (note: in standard rendering out of frame pixels could be skipped but if checks add more clock cycles over all) pxlbrightness[0] = (((int32_t)PS_P_RADIUS_1D - dx) * brightness) >> PS_P_SURFACE_1D; pxlbrightness[1] = (dx * brightness) >> PS_P_SURFACE_1D; From 2e50883158248b285c47c3a16a75a14b610c2f9c Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Fri, 19 Jul 2024 20:25:31 +0200 Subject: [PATCH 122/219] added fractal FX test, fixed bugs in emit function --- wled00/FX.cpp | 88 ++++++++++++++++++++++++++++++++++++- wled00/FX.h | 3 +- wled00/FXparticleSystem.cpp | 19 +++++--- wled00/FXparticleSystem.h | 2 +- 4 files changed, 101 insertions(+), 11 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index d4123ae993..6c817f0e04 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -9388,7 +9388,7 @@ if (SEGLEN == 1) PartSys->update(); // update and render return FRAMETIME; } -static const char _data_FX_MODE_PARTICLECCIRCULARGEQ[] PROGMEM = "PS Center GEQ@Speed,Intensity,Rotation Speed,Color Change,Nozzle Size,,Direction;;!;2f;pal=13,ix=180,c1=0,c2=0,c3=8,o1=0,o2=0"; +static const char _data_FX_MODE_PARTICLECIRCULARGEQ[] PROGMEM = "PS Center GEQ@Speed,Intensity,Rotation Speed,Color Change,Nozzle Size,,Direction;;!;2f;pal=13,ix=180,c1=0,c2=0,c3=8,o1=0,o2=0"; /* Particle replacement of Ghost Rider by DedeHai (Damian Schneider), original by stepko adapted by Blaz Kristan (AKA blazoncek) @@ -9605,6 +9605,88 @@ uint16_t mode_particleblobs(void) } static const char _data_FX_MODE_PARTICLEBLOBS[] PROGMEM = "PS Blobs@Speed,Blobs,Size,Life,Blur,Wobble,Collide,Pulsate;;!;2v;sx=30,ix=64,c1=200,c2=130,c3=0,o1=0,o2=0,o3=1"; +/* + * Particle Fractal + * particles move, then split to form a fractal tree EXPERIMENTAL! + * by DedeHai (Damian Schneider) + */ + +uint16_t mode_particlefractal(void) +{ +if (SEGLEN == 1) + return mode_static(); + + ParticleSystem *PartSys = NULL; + uint32_t i; + + if (SEGMENT.call == 0) // initialization + { + if (!initParticleSystem2D(PartSys, 1, 0, true, false)) // init, use advanced particles + return mode_static(); // allocation failed + PartSys->setKillOutOfBounds(true); + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + + if (PartSys == NULL) + return mode_static(); // something went wrong, no data! + + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + + if (SEGMENT.check2) + SEGENV.aux0 += SEGMENT.custom1 << 2; + else + SEGENV.aux0 -= SEGMENT.custom1 << 2; + + int16_t angleoffset = SEGMENT.custom2 << 6; + int8_t emitspeed = SEGMENT.speed >> 2; + + //check particle age, emit 2 particles at the end of the branch + for (i = 0; i < PartSys->numParticles; i++) + { + if(PartSys->particles[i].ttl > 0 && PartSys->particles[i].ttl < 260) //alive and ripe + { + PartSys->particles[i].ttl = 0; + uint16_t currentangle = ((uint32_t)PartSys->advPartProps[i].forcecounter) << 7; // abuse forcecounter to track the angle + PartSys->sources[0].source.x = PartSys->particles[i].x; + PartSys->sources[0].source.y = PartSys->particles[i].y;; + PartSys->sources[0].source.hue = PartSys->particles[i].hue + 50; // todo: make color schemes + uint16_t angle = currentangle - angleoffset; + int32_t index = PartSys->angleEmit(PartSys->sources[0], angle, emitspeed); //upward TODO: make angle adjustable + Serial.print("branch emit1 at idx = "); + Serial.println(index); + //TODO: check if index >=0!!! + PartSys->advPartProps[index].forcecounter = angle >> 7; + angle = currentangle + angleoffset; + index = PartSys->angleEmit(PartSys->sources[0], angle, emitspeed); + Serial.print("branch emit2 at idx = "); + Serial.println(index); + PartSys->advPartProps[index].forcecounter = angle >> 7; + } + + + } + if(SEGENV.call % (256-SEGMENT.intensity) == 0) + { + PartSys->sources[0].source.x = (PartSys->maxX + 1) >> 1; + PartSys->sources[0].source.y = 5; + PartSys->sources[0].source.hue = 0; // todo: make color schemes + PartSys->sources[0].maxLife = 275; + PartSys->sources[0].minLife = 270; + uint32_t angle = ((uint32_t)SEGMENT.custom1) << 7; //16 bit angle, 0° to 180° + int32_t index = PartSys->angleEmit(PartSys->sources[0], angle, emitspeed); //upward TODO: make angle adjustable + Serial.print("base emit at idx = "); + Serial.println(index); + //set the forcecounter to track the angle (only 8 bit precision...) + PartSys->advPartProps[index].forcecounter = angle >> 7; + } + + PartSys->setMotionBlur(((SEGMENT.custom3) << 3) + 7); + PartSys->update(); // update and render + return FRAMETIME; +} +static const char _data_FX_MODE_PARTICLEFRACTAL[] PROGMEM = "PS fractal (exp)@Speed,Intensity,Base angle,branch angle,Blur,,Direction;;!;2f;pal=13,ix=180,c1=0,c2=0,c3=8,o1=0,o2=0"; + #endif //WLED_DISABLE_PARTICLESYSTEM2D #endif // WLED_DISABLE_2D @@ -11062,7 +11144,9 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_PARTICLESGEQ, &mode_particleGEQ, _data_FX_MODE_PARTICLEGEQ); addEffect(FX_MODE_PARTICLEGHOSTRIDER, &mode_particleghostrider, _data_FX_MODE_PARTICLEGHOSTRIDER); addEffect(FX_MODE_PARTICLEBLOBS, &mode_particleblobs, _data_FX_MODE_PARTICLEBLOBS); - addEffect(FX_MODE_PARTICLECENTERGEQ, &mode_particlecenterGEQ, _data_FX_MODE_PARTICLECCIRCULARGEQ); + addEffect(FX_MODE_PARTICLECENTERGEQ, &mode_particlecenterGEQ, _data_FX_MODE_PARTICLECIRCULARGEQ); + addEffect(FX_MODE_PSFRACTAL, &mode_particlefractal, _data_FX_MODE_PARTICLEFRACTAL); + #endif // WLED_DISABLE_PARTICLESYSTEM2D #endif // WLED_DISABLE_2D diff --git a/wled00/FX.h b/wled00/FX.h index 71cadbd3d9..5b49c99f39 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -344,7 +344,8 @@ #define FX_MODE_PSSTARBURST 211 #define FX_MODE_PS1DGEQ 212 #define FX_MODE_PSFIRE1D 213 -#define MODE_COUNT 214 +#define FX_MODE_PSFRACTAL 214 +#define MODE_COUNT 215 typedef enum mapping1D2D { M12_Pixels = 0, diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index 4236ea55b3..d0fe079b92 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -67,7 +67,7 @@ ParticleSystem::ParticleSystem(uint16_t width, uint16_t height, uint16_t numbero for (uint32_t i = 0; i < numSources; i++) { sources[i].source.sat = 255; //set saturation to max by default - sources[i].source.ttl = 1; //set source alive + sources[i].source.ttl = 255; //set source alive } for (uint32_t i = 0; i < numParticles; i++) { @@ -222,7 +222,8 @@ void ParticleSystem::enableParticleCollisions(bool enable, uint8_t hardness) // // emit one particle with variation, returns index of last emitted particle (or -1 if no particle emitted) int32_t ParticleSystem::sprayEmit(PSsource &emitter, uint32_t amount) { - for (uint32_t a = 0; a < amount; a++) + bool success = false; + for (uint32_t a = 0; a < amount; a++) { for (uint32_t i = 0; i < usedParticles; i++) { @@ -231,6 +232,7 @@ int32_t ParticleSystem::sprayEmit(PSsource &emitter, uint32_t amount) emitIndex = 0; if (particles[emitIndex].ttl == 0) // find a dead particle { + success = true; particles[emitIndex].vx = emitter.vx + random(-emitter.var, emitter.var); particles[emitIndex].vy = emitter.vy + random(-emitter.var, emitter.var); particles[emitIndex].x = emitter.source.x; @@ -240,12 +242,15 @@ int32_t ParticleSystem::sprayEmit(PSsource &emitter, uint32_t amount) particles[emitIndex].collide = emitter.source.collide; particles[emitIndex].ttl = random(emitter.minLife, emitter.maxLife); if (advPartProps) - advPartProps[emitIndex].size = emitter.size; - return i; + advPartProps[emitIndex].size = emitter.size; + break; } } } - return -1; + if(success) + return emitIndex; + else + return -1; } // Spray emitter for particles used for flames (particle TTL depends on source TTL) @@ -278,11 +283,11 @@ void ParticleSystem::flameEmit(PSsource &emitter) // Emits a particle at given angle and speed, angle is from 0-65535 (=0-360deg), speed is also affected by emitter->var // angle = 0 means in positive x-direction (i.e. to the right) -void ParticleSystem::angleEmit(PSsource &emitter, uint16_t angle, int8_t speed, uint32_t amount) +int32_t ParticleSystem::angleEmit(PSsource &emitter, uint16_t angle, int8_t speed, uint32_t amount) { emitter.vx = ((int32_t)cos16(angle) * (int32_t)speed) / (int32_t)32600; // cos16() and sin16() return signed 16bit, division should be 32767 but 32600 gives slightly better rounding emitter.vy = ((int32_t)sin16(angle) * (int32_t)speed) / (int32_t)32600; // note: cannot use bit shifts as bit shifting is asymmetrical for positive and negative numbers and this needs to be accurate! - sprayEmit(emitter, amount); + return sprayEmit(emitter, amount); } // particle moves, decays and dies, if killoutofbounds is set, out of bounds particles are set to ttl=0 diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index e17bfa423c..d175b4feed 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -141,7 +141,7 @@ class ParticleSystem // particle emitters int32_t sprayEmit(PSsource &emitter, uint32_t amount = 1); void flameEmit(PSsource &emitter); - void angleEmit(PSsource& emitter, uint16_t angle, int8_t speed, uint32_t amount = 1); + int32_t angleEmit(PSsource& emitter, uint16_t angle, int8_t speed, uint32_t amount = 1); //particle physics From a07f29e8ff661b14e0a66b81990aad50148923e2 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Tue, 20 Aug 2024 19:50:32 +0200 Subject: [PATCH 123/219] tweaked 1D fire a little, still work in progress --- wled00/FX.cpp | 21 ++++++++++----------- wled00/FXparticleSystem.cpp | 4 ++-- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 6c817f0e04..3c57e94abf 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -10832,9 +10832,9 @@ uint16_t mode_particleFire1D(void) if (SEGMENT.call == 0) // initialization { - if (!initParticleSystem1D(PartSys, 4)) // init + if (!initParticleSystem1D(PartSys, 8)) // init return mode_static(); // allocation failed - PartSys->setKillOutOfBounds(true); + //PartSys->setKillOutOfBounds(true); PartSys->setParticleSize(1); } else @@ -10845,17 +10845,16 @@ uint16_t mode_particleFire1D(void) // Particle System settings PartSys->updateSystem(); // update system properties (dimensions and data pointers) - PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur + PartSys->setMotionBlur(128 + (SEGMENT.custom2 >> 1)); // anable motion blur + PartSys->setColorByAge(true); for(uint i = 0; i < PartSys->numSources; i++) { PartSys->sources[i].var = 1 + (SEGMENT.speed >> 4); - PartSys->sources[i].minLife = 50 + SEGMENT.intensity; - PartSys->sources[i].maxLife = 200 + (SEGMENT.intensity << 1); - PartSys->sources[i].source.x = map(SEGMENT.custom1, 0 , 255, 0, PartSys->maxX); // spray position - //PartSys->sources[0].v = map(SEGMENT.speed, 0 , 255, 1, (inoise16(SEGENV.call) >> 8)); // particle emit speed - //PartSys->sources[i].v = 2 + ((((SEGMENT.speed >> (2 + i))) * (int16_t)inoise8(SEGENV.aux0)) >> 7); + PartSys->sources[i].minLife = 200 + SEGMENT.intensity + (i << 3); + PartSys->sources[i].maxLife = 300 + SEGMENT.intensity + (i << 4); + PartSys->sources[i].source.x = -256; // source position below strip start PartSys->sources[i].v = 2 + (SEGMENT.speed >> (2 + (i<<1))); - //if(random(4) == 0) + if(SEGMENT.call % 3 == 0) PartSys->sprayEmit(PartSys->sources[i]); //emit a particle } if (SEGMENT.call & 0x01) // update noise position every second frames, also add wind @@ -10864,7 +10863,7 @@ if (SEGMENT.call & 0x01) // update noise position every second frames, also add } //update color settings - PartSys->setColorByAge(SEGMENT.check1); + // PartSys->setColorByPosition(SEGMENT.check3); for(uint i = 0; i < PartSys->usedParticles; i++) { @@ -10877,7 +10876,7 @@ if (SEGMENT.call & 0x01) // update noise position every second frames, also add return FRAMETIME; } -static const char _data_FX_MODE_PS_FIRE1D[] PROGMEM = "PS Fire 1D@!,!,Position,Blur/Overlay,Gravity,Color by Age,Bounce,Color by Position;,!;!;1;pal=35,sx=200,ix=220,c1=4,c2=0,c3=28,o1=1,o2=1,o3=0"; +static const char _data_FX_MODE_PS_FIRE1D[] PROGMEM = "PS Fire 1D@!,!,,Blur/Overlay;,!;!;1;pal=35,sx=200,ix=220,c1=4,c2=0,c3=28,o1=1,o2=1,o3=0"; diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index d0fe079b92..377ad96791 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -1600,14 +1600,14 @@ void ParticleSystem1D::setMotionBlur(uint8_t bluramount) motionBlur = bluramount; } -// render size using smearing (see blur function) +// render size, 0 = 1 pixel, 1 = 2 pixel (interpolated), bigger sizes require adanced properties +// note: if size is set larger than 1 without advanced properties, weird things may happen void ParticleSystem1D::setParticleSize(uint8_t size) { particlesize = size; particleHardRadius = PS_P_MINHARDRADIUS_1D >> 1; // 1 pixel sized particles have half the radius (for bounce, not for collisions) if (particlesize) particleHardRadius = particleHardRadius << 1; // 2 pixel sized particles - //TODO: since global size rendering is always 1 or 2 pixels, this could maybe be made simpler with a bool 'singlepixelsize' } // enable/disable gravity, optionally, set the force (force=8 is default) can be -127 to +127, 0 is disable // if enabled, gravity is applied to all particles in ParticleSystemUpdate() From 7545064ba6932069b453c09144dcfcdcb88431a4 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Thu, 22 Aug 2024 20:52:24 +0200 Subject: [PATCH 124/219] minor tuning on 1D fire to avoid 'oscillation' at start --- wled00/FX.cpp | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 9a24d4b07b..723b4e8e36 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -10759,12 +10759,12 @@ uint16_t mode_particleFire1D(void) PartSys->setColorByAge(true); for(uint i = 0; i < PartSys->numSources; i++) { - PartSys->sources[i].var = 1 + (SEGMENT.speed >> 4); + PartSys->sources[i].var = 2 + (SEGMENT.speed >> 4); PartSys->sources[i].minLife = 200 + SEGMENT.intensity + (i << 3); PartSys->sources[i].maxLife = 300 + SEGMENT.intensity + (i << 4); PartSys->sources[i].source.x = -256; // source position below strip start - PartSys->sources[i].v = 2 + (SEGMENT.speed >> (2 + (i<<1))); - if(SEGMENT.call % 3 == 0) + PartSys->sources[i].v = (SEGMENT.speed >> (2 + (i<<1))); + if(SEGMENT.call % 4 == 0) PartSys->sprayEmit(PartSys->sources[i]); //emit a particle } if (SEGMENT.call & 0x01) // update noise position every second frames, also add wind @@ -10787,9 +10787,6 @@ if (SEGMENT.call & 0x01) // update noise position every second frames, also add return FRAMETIME; } static const char _data_FX_MODE_PS_FIRE1D[] PROGMEM = "PS Fire 1D@!,!,,Blur/Overlay;,!;!;1;pal=35,sx=200,ix=220,c1=4,c2=0,c3=28,o1=1,o2=1,o3=0"; - - - #endif //WLED_DISABLE_PARTICLESYSTEM1D ////////////////////////////////////////////////////////////////////////////////////////// From 3e0eaa01d493f25244f31354445823672baa42f5 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sun, 25 Aug 2024 22:54:16 +0200 Subject: [PATCH 125/219] improved 1D particle fire, much more natural now. --- wled00/FX.cpp | 56 ++++++++++++++++++++++++++++++++++----------------- 1 file changed, 37 insertions(+), 19 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 723b4e8e36..472dd073c3 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -10742,9 +10742,9 @@ uint16_t mode_particleFire1D(void) if (SEGMENT.call == 0) // initialization { - if (!initParticleSystem1D(PartSys, 8)) // init + if (!initParticleSystem1D(PartSys, 5)) // init return mode_static(); // allocation failed - //PartSys->setKillOutOfBounds(true); + PartSys->setKillOutOfBounds(true); PartSys->setParticleSize(1); } else @@ -10757,36 +10757,54 @@ uint16_t mode_particleFire1D(void) PartSys->updateSystem(); // update system properties (dimensions and data pointers) PartSys->setMotionBlur(128 + (SEGMENT.custom2 >> 1)); // anable motion blur PartSys->setColorByAge(true); + uint32_t emitparticles = 1; + uint32_t j = random16(); + for(uint i = 0; i < 3; i++) + { + if(PartSys->sources[i].source.ttl > 50) + PartSys->sources[i].source.ttl -= 10; + else + PartSys->sources[i].source.ttl = 100 + random16(200); + } + + for(uint i = 0; i < PartSys->numSources; i++) { - PartSys->sources[i].var = 2 + (SEGMENT.speed >> 4); - PartSys->sources[i].minLife = 200 + SEGMENT.intensity + (i << 3); - PartSys->sources[i].maxLife = 300 + SEGMENT.intensity + (i << 4); - PartSys->sources[i].source.x = -256; // source position below strip start - PartSys->sources[i].v = (SEGMENT.speed >> (2 + (i<<1))); - if(SEGMENT.call % 4 == 0) - PartSys->sprayEmit(PartSys->sources[i]); //emit a particle - } -if (SEGMENT.call & 0x01) // update noise position every second frames, also add wind - { - SEGENV.aux0++; + j = (j + 1) % PartSys->numSources; + PartSys->sources[j].source.x = 0; + PartSys->sources[j].var = 2 + (SEGMENT.speed >> 4); + //base flames + if(j > 2) { + PartSys->sources[j].minLife = 150 + SEGMENT.intensity + (j << 2); + PartSys->sources[j].maxLife = 200 + SEGMENT.intensity + (j << 3); + PartSys->sources[j].v = (SEGMENT.speed >> (2 + (j<<1))); + if(emitparticles) + { + emitparticles--; + PartSys->sprayEmit(PartSys->sources[j]); //emit a particle + } + } + else{ + PartSys->sources[j].minLife = PartSys->sources[j].source.ttl + SEGMENT.intensity; + PartSys->sources[j].maxLife = PartSys->sources[j].minLife + 50; + PartSys->sources[j].v = SEGMENT.speed >> 2; + if(SEGENV.call & 0x01) //every second frame + PartSys->sprayEmit(PartSys->sources[j]); //emit a particle + } } - //update color settings - - // PartSys->setColorByPosition(SEGMENT.check3); for(uint i = 0; i < PartSys->usedParticles; i++) { PartSys->particles[i].x += PartSys->particles[i].ttl >> 7; // 'hot' particles are faster, apply some extra velocity - if(PartSys->particles[i].ttl > 150) - PartSys->particles[i].ttl -= map(SEGMENT.intensity, 0, 255, 5 , 0); // age faster + if(PartSys->particles[i].ttl > 3 + ((255 - SEGMENT.custom1) >> 1)) + PartSys->particles[i].ttl -= map(SEGMENT.custom1, 0, 255, 1 , 3); // age faster } PartSys->update(); // update and render return FRAMETIME; } -static const char _data_FX_MODE_PS_FIRE1D[] PROGMEM = "PS Fire 1D@!,!,,Blur/Overlay;,!;!;1;pal=35,sx=200,ix=220,c1=4,c2=0,c3=28,o1=1,o2=1,o3=0"; +static const char _data_FX_MODE_PS_FIRE1D[] PROGMEM = "PS Fire 1D@!,!,Cooling,Blur/Overlay;,!;!;1;pal=35,sx=100,ix=50,c1=80,c2=100,c3=28,o1=1,o2=1,o3=0"; #endif //WLED_DISABLE_PARTICLESYSTEM1D ////////////////////////////////////////////////////////////////////////////////////////// From 6b674caacd0f4770a976cbc1bcc88f6dfe6baeb0 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sun, 8 Sep 2024 20:28:43 +0200 Subject: [PATCH 126/219] updated 2D fire, fixed init bug - source init was wrong making fire init weirdly - changed parameters on 2D fire making it look better and improving flame height for larger setups --- wled00/FX.cpp | 37 ++++++++++++++++--------------------- wled00/FXparticleSystem.cpp | 17 ++++++----------- 2 files changed, 22 insertions(+), 32 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 472dd073c3..dad67da7a4 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -8218,7 +8218,7 @@ uint16_t mode_particlefire(void) if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. { - if (!initParticleSystem2D(PartSys, 25, 4)) //maximum number of source (PS will determine the exact number based on segment size) and need 4 additional bytes for time keeping (uint32_t lastcall) + if (!initParticleSystem2D(PartSys, SEGMENT.virtualWidth(), 4)) //maximum number of source (PS may limit based on segment size); need 4 additional bytes for time keeping (uint32_t lastcall) return mode_static(); // allocation failed; //allocation failed SEGENV.aux0 = random16(); // aux0 is wind position (index) in the perlin noise numFlames = PartSys->numSources; @@ -8232,7 +8232,7 @@ uint16_t mode_particlefire(void) PartSys->updateSystem(); // update system properties (dimensions and data pointers) PartSys->setWrapX(SEGMENT.check2); - PartSys->setMotionBlur(SEGMENT.check1 * 120); // anable/disable motion blur + PartSys->setMotionBlur(SEGMENT.check1 * 180); // anable/disable motion blur uint32_t firespeed = max((uint8_t)100, SEGMENT.speed); //limit speed to 100 minimum, reduce frame rate to make it slower (slower speeds than 100 do not look nice) if (SEGMENT.speed < 100) //slow, limit FPS @@ -8250,8 +8250,8 @@ uint16_t mode_particlefire(void) } uint32_t spread = (PartSys->maxX >> 5) * (SEGMENT.custom3 + 1); //fire around segment center (in subpixel points) - numFlames = min((uint32_t)PartSys->numSources, (2 + ((spread / PS_P_RADIUS) << 1))); // number of flames used depends on spread with, good value is (fire width in pixel) * 2 - uint32_t percycle = numFlames*2/3;// / 2; // maximum number of particles emitted per cycle (TODO: for ESP826 maybe use flames/2) + numFlames = min((uint32_t)PartSys->numSources, (4 + ((spread / PS_P_RADIUS) << 1))); // number of flames used depends on spread with, good value is (fire width in pixel) * 2 + uint32_t percycle = (numFlames * 2) / 3;// / 2; // maximum number of particles emitted per cycle (TODO: for ESP826 maybe use flames/2) // percycle = map(SEGMENT.intensity,0,255, 2, (numFlames*3) / 2); //TODO: does this give better flames or worse? // update the flame sprays: @@ -8261,28 +8261,23 @@ uint16_t mode_particlefire(void) { PartSys->sources[i].source.ttl--; } - else // flame source is dead + else // flame source is dead: initialize new flame: set properties of source { - // initialize new flame: set properties of source - if (random16(20) == 0 || SEGMENT.call == 0) // from time to time, change flame position - { - PartSys->sources[i].source.x = (PartSys->maxX >> 1) - (spread>>1) + random(spread); // distribute randomly on chosen width - } - PartSys->sources[i].source.y = -PS_P_RADIUS; // set the source below the frame - PartSys->sources[i].source.ttl = 5 + random16((SEGMENT.custom1 * SEGMENT.custom1) >> 7) / (2 + (firespeed >> 4)); //'hotness' of fire, faster flames reduce the effect or flame height will scale too much with speed -> new, this works! - PartSys->sources[i].maxLife = random16(7) + 13; // defines flame height together with the vy speed, vy speed*maxlife/PS_P_RADIUS is the average flame height - PartSys->sources[i].minLife = 4; - PartSys->sources[i].vx = (int8_t)random(-3, 3); // emitting speed (sideways) - PartSys->sources[i].vy = 5 + (firespeed >> 2); // emitting speed (upwards) -> this is good - PartSys->sources[i].var = (random16(1 + (firespeed >> 5)) + 2); // speed variation around vx,vy (+/- var) + PartSys->sources[i].source.x = (PartSys->maxX >> 1) - (spread>>1) + random(spread); // change flame position: distribute randomly on chosen width + PartSys->sources[i].source.y = -(PS_P_RADIUS<<1); // set the source below the frame + PartSys->sources[i].source.ttl = 16 + random((SEGMENT.custom1 * SEGMENT.custom1) >> 7) / (1 + (firespeed >> 5)); //'hotness' of fire, faster flames reduce the effect or flame height will scale too much with speed + PartSys->sources[i].maxLife = random(SEGMENT.virtualHeight() / 2) + 16; // defines flame height together with the vy speed, vy speed*maxlife/PS_P_RADIUS is the average flame height + PartSys->sources[i].minLife = PartSys->sources[i].maxLife >> 1; + PartSys->sources[i].vx = random16(4) - 2; // emitting speed (sideways) + PartSys->sources[i].vy = (SEGMENT.virtualHeight() >> 1) + (firespeed >> 4) + (SEGMENT.custom1>>4); // emitting speed (upwards) + PartSys->sources[i].var = 2 + random16(2 + (firespeed >> 4)); // speed variation around vx,vy (+/- var) } - } - if (SEGMENT.call & 0x01) // update noise position every second frames, also add wind + if (SEGMENT.call % 3 == 0) // update noise position and add wind { SEGENV.aux0++; // position in the perlin noise matrix for wind generation - if (SEGMENT.call & 0x02) // every third frame + if (SEGMENT.call % 10 == 0) SEGENV.aux1++; // move in noise y direction so noise does not repeat as often // add wind force to all particles int8_t windspeed = ((int16_t)(inoise8(SEGENV.aux0, SEGENV.aux1) - 127) * SEGMENT.custom2) >> 7; @@ -8317,7 +8312,7 @@ uint16_t mode_particlefire(void) return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEFIRE[] PROGMEM = "PS Fire@Speed,Intensity,Base Heat,Wind,Spread,Smooth,Cylinder,Turbulence;;!;2;pal=35,sx=110,c1=110,c2=50,c3=31,o1=1"; +static const char _data_FX_MODE_PARTICLEFIRE[] PROGMEM = "PS Fire@Speed,Intensity,Flame Height,Wind,Spread,Smooth,Cylinder,Turbulence;;!;2;pal=35,sx=110,c1=110,c2=50,c3=31,o1=1"; /* PS Ballpit: particles falling down, user can enable these three options: X-wraparound, side bounce, ground bounce diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index 377ad96791..680bc5e7fe 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -67,7 +67,7 @@ ParticleSystem::ParticleSystem(uint16_t width, uint16_t height, uint16_t numbero for (uint32_t i = 0; i < numSources; i++) { sources[i].source.sat = 255; //set saturation to max by default - sources[i].source.ttl = 255; //set source alive + sources[i].source.ttl = 1; //set source alive } for (uint32_t i = 0; i < numParticles; i++) { @@ -263,7 +263,8 @@ void ParticleSystem::flameEmit(PSsource &emitter) emitIndex = 0; if (particles[emitIndex].ttl == 0) // find a dead particle { - particles[emitIndex].x = emitter.source.x + random16(PS_P_RADIUS<<1) - PS_P_RADIUS; // jitter the flame by one pixel to make the flames wider at the base + //particles[emitIndex].x = emitter.source.x + random16(PS_P_RADIUS<<1) - PS_P_RADIUS; // jitter the flame by one pixel to make the flames wider at the base + particles[emitIndex].x = emitter.source.x; particles[emitIndex].y = emitter.source.y; particles[emitIndex].vx = emitter.vx + random16(emitter.var) - (emitter.var >> 1); // random16 is good enough for fire and much faster particles[emitIndex].vy = emitter.vy + random16(emitter.var) - (emitter.var >> 1); @@ -771,19 +772,13 @@ void ParticleSystem::ParticleSys_render(bool firemode, uint32_t fireintensity) // generate RGB values for particle if (firemode) { - //TODO: decide on a final version... - //brightness = (uint32_t)particles[i].ttl * (1 + (fireintensity >> 4)) + (fireintensity >> 2); //this is good - //brightness = (uint32_t)particles[i].ttl * (fireintensity >> 3) + (fireintensity >> 1); // this is experimental, also works, flamecolor is more even, does not look as good (but less puffy at lower speeds) - //brightness = (((uint32_t)particles[i].ttl * (maxY + PS_P_RADIUS - particles[i].y)) >> 7) + (uint32_t)particles[i].ttl * (fireintensity >> 4) + (fireintensity >> 1); // this is experimental //multiplikation mit weniger als >>4 macht noch mehr puffs bei low speed - //brightness = (((uint32_t)particles[i].ttl * (maxY + PS_P_RADIUS - particles[i].y)) >> 7) + particles[i].ttl + (fireintensity>>1); // this is experimental - //brightness = (((uint32_t)particles[i].ttl * (maxY + PS_P_RADIUS - particles[i].y)) >> 7) + ((particles[i].ttl * fireintensity) >> 5); // this is experimental TODO: test this -> testing... ok but not the best, bit sparky - brightness = (((uint32_t)particles[i].ttl * (maxY + PS_P_RADIUS - particles[i].y)) >> 7) + (fireintensity >> 1); // this is experimental TODO: test this -> testing... does not look too bad! + brightness = (uint32_t)particles[i].ttl*(3 + (fireintensity >> 5)) + 20; brightness = brightness > 255 ? 255 : brightness; // faster then using min() - baseRGB = ColorFromPalette(SEGPALETTE, brightness, 255, LINEARBLEND); + baseRGB = ColorFromPalette(SEGPALETTE, brightness, 255); } else{ brightness = particles[i].ttl > 255 ? 255 : particles[i].ttl; //faster then using min() - baseRGB = ColorFromPalette(SEGPALETTE, particles[i].hue, 255, LINEARBLEND); + baseRGB = ColorFromPalette(SEGPALETTE, particles[i].hue, 255); if (particles[i].sat < 255) { CHSV baseHSV = rgb2hsv_approximate(baseRGB); //convert to HSV From 153257d9fffef29cdabf7ca392977be89fa84bbd Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sun, 8 Sep 2024 21:08:57 +0200 Subject: [PATCH 127/219] fixed 'nervours' fire --- wled00/FX.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index dad67da7a4..239dc958dc 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -8257,15 +8257,15 @@ uint16_t mode_particlefire(void) // update the flame sprays: for (i = 0; i < numFlames; i++) { - if (PartSys->sources[i].source.ttl > 0) + if (SEGMENT.call & 1 && PartSys->sources[i].source.ttl > 0) // every second frame { PartSys->sources[i].source.ttl--; } else // flame source is dead: initialize new flame: set properties of source { PartSys->sources[i].source.x = (PartSys->maxX >> 1) - (spread>>1) + random(spread); // change flame position: distribute randomly on chosen width - PartSys->sources[i].source.y = -(PS_P_RADIUS<<1); // set the source below the frame - PartSys->sources[i].source.ttl = 16 + random((SEGMENT.custom1 * SEGMENT.custom1) >> 7) / (1 + (firespeed >> 5)); //'hotness' of fire, faster flames reduce the effect or flame height will scale too much with speed + PartSys->sources[i].source.y = -(PS_P_RADIUS<<2); // set the source below the frame + PartSys->sources[i].source.ttl = 20 + random((SEGMENT.custom1 * SEGMENT.custom1) >> 8) / (1 + (firespeed >> 5)); //'hotness' of fire, faster flames reduce the effect or flame height will scale too much with speed PartSys->sources[i].maxLife = random(SEGMENT.virtualHeight() / 2) + 16; // defines flame height together with the vy speed, vy speed*maxlife/PS_P_RADIUS is the average flame height PartSys->sources[i].minLife = PartSys->sources[i].maxLife >> 1; PartSys->sources[i].vx = random16(4) - 2; // emitting speed (sideways) From 70ea9d75e934741f174e61d13c3aa298b9b5c292 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Wed, 11 Sep 2024 18:54:05 +0200 Subject: [PATCH 128/219] slight improvements and some cleanup --- wled00/FX.cpp | 49 +++++++++++++++++-------------------- wled00/FXparticleSystem.cpp | 9 ++----- wled00/FXparticleSystem.h | 2 +- 3 files changed, 26 insertions(+), 34 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 239dc958dc..812223e96a 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -8228,7 +8228,6 @@ uint16_t mode_particlefire(void) if (PartSys == NULL) return mode_static(); // something went wrong, no data! - PartSys->updateSystem(); // update system properties (dimensions and data pointers) PartSys->setWrapX(SEGMENT.check2); @@ -8257,7 +8256,7 @@ uint16_t mode_particlefire(void) // update the flame sprays: for (i = 0; i < numFlames; i++) { - if (SEGMENT.call & 1 && PartSys->sources[i].source.ttl > 0) // every second frame + if (SEGMENT.call & 1 && PartSys->sources[i].source.ttl > 0) // every second frame { PartSys->sources[i].source.ttl--; } @@ -8865,26 +8864,26 @@ uint16_t mode_particleattractor(void) else PartSys->angleEmit(PartSys->sources[0], SEGENV.aux0 + 0x7FFF, 12); // emit at 180° as well // apply force - #ifdef USERMOD_AUDIOREACTIVE + #ifdef USERMOD_AUDIOREACTIVE + uint32_t strength = SEGMENT.speed; um_data_t *um_data; - if(usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) - { - uint8_t volumeSmth = (uint8_t)(*(float*) um_data->u_data[0]); - uint8_t strength = volumeSmth; - if(SEGMENT.check3) strength = SEGMENT.speed; //AR disabled - for (uint32_t i = 0; i < PartSys->usedParticles; i++) // update particles - { - PartSys->pointAttractor(i, attractor, strength, false); - } - } - else //no data, do classic attractor + if(!SEGMENT.check3) //AR enabled { - for(uint32_t i = 0; i < displayparticles; i++) - { - PartSys->pointAttractor(i, attractor, SEGMENT.speed, SEGMENT.check3); + if(usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) + { + uint8_t volumeSmth = (uint8_t)(*(float*) um_data->u_data[0]); + uint32_t strength = volumeSmth; + for (uint32_t i = 0; i < PartSys->usedParticles; i++) // update particles + { + PartSys->pointAttractor(i, attractor, strength, false); + } } } - #else + for(uint32_t i = 0; i < displayparticles; i++) + { + PartSys->pointAttractor(i, attractor, strength, false); + } + #else // no AR for(uint32_t i = 0; i < displayparticles; i++) { PartSys->pointAttractor(i, attractor, SEGMENT.speed, SEGMENT.check3); @@ -10757,12 +10756,10 @@ uint16_t mode_particleFire1D(void) for(uint i = 0; i < 3; i++) { if(PartSys->sources[i].source.ttl > 50) - PartSys->sources[i].source.ttl -= 10; + PartSys->sources[i].source.ttl -= 10; //TODO: in 2D making the source fade out slow results in much smoother flames, need to check if it can be done the same else - PartSys->sources[i].source.ttl = 100 + random16(200); - } - - + PartSys->sources[i].source.ttl = 100 + random16(200); // base flame + } for(uint i = 0; i < PartSys->numSources; i++) { j = (j + 1) % PartSys->numSources; @@ -10770,7 +10767,7 @@ uint16_t mode_particleFire1D(void) PartSys->sources[j].var = 2 + (SEGMENT.speed >> 4); //base flames if(j > 2) { - PartSys->sources[j].minLife = 150 + SEGMENT.intensity + (j << 2); + PartSys->sources[j].minLife = 150 + SEGMENT.intensity + (j << 2); //TODO: in 2D, min life is maxlife/2 and that looks very nice PartSys->sources[j].maxLife = 200 + SEGMENT.intensity + (j << 3); PartSys->sources[j].v = (SEGMENT.speed >> (2 + (j<<1))); if(emitparticles) @@ -10780,11 +10777,11 @@ uint16_t mode_particleFire1D(void) } } else{ - PartSys->sources[j].minLife = PartSys->sources[j].source.ttl + SEGMENT.intensity; + PartSys->sources[j].minLife = PartSys->sources[j].source.ttl + SEGMENT.intensity; //TODO: in 2D, emitted particle ttl depends on source TTL, mimic here the same way? OR: change 2D to the same way it is done here and ditch special fire treatment in emit? PartSys->sources[j].maxLife = PartSys->sources[j].minLife + 50; PartSys->sources[j].v = SEGMENT.speed >> 2; if(SEGENV.call & 0x01) //every second frame - PartSys->sprayEmit(PartSys->sources[j]); //emit a particle + PartSys->sprayEmit(PartSys->sources[j]); //emit a particle } } diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index 680bc5e7fe..8da1a58e68 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -39,9 +39,6 @@ #if !defined(WLED_DISABLE_PARTICLESYSTEM2D) || !defined(WLED_DISABLE_PARTICLESYSTEM1D) #include "FXparticleSystem.h" -#include "wled.h" -#include "FastLED.h" -#include "FX.h" #endif #ifndef WLED_DISABLE_PARTICLESYSTEM2D @@ -750,9 +747,7 @@ void ParticleSystem::ParticleSys_render(bool firemode, uint32_t fireintensity) } } } - } - } if (!useLocalBuffer) //disabled or allocation above failed @@ -785,8 +780,7 @@ void ParticleSystem::ParticleSys_render(bool firemode, uint32_t fireintensity) baseHSV.s = particles[i].sat; //set the saturation baseRGB = (CRGB)baseHSV; // convert back to RGB } - } - + } renderParticle(framebuffer, i, brightness, baseRGB, renderbuffer); } @@ -2365,6 +2359,7 @@ int32_t limitSpeed(int32_t speed) // note: result is stored in c1, so c1 will contain the result. not using a return value is much faster as the struct does not need to be copied upon return void fast_color_add(CRGB &c1, CRGB &c2, uint32_t scale) { + //note: function is manly used to add scaled colors, so checking if one color is black is slower uint32_t r, g, b; if (scale < 255) { r = c1.r + ((c2.r * scale) >> 8); diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index d175b4feed..8c324de2e9 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -27,7 +27,7 @@ #if !defined(WLED_DISABLE_PARTICLESYSTEM2D) || !defined(WLED_DISABLE_PARTICLESYSTEM1D) #include -#include "FastLED.h" +#include "wled.h" #define PS_P_MAXSPEED 120 // maximum speed a particle can have (vx/vy is int8) From 0d9e8da594bb6c9b0cf47a37bed8e550fa7b9da1 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sun, 22 Sep 2024 17:24:23 +0200 Subject: [PATCH 129/219] removed douplicate code for fire emit (tradeoff for some speed), removed unfinished fractal FX - with the duplicate code removed, the fire particle emits are a bit slower but it saves on code. it is not really noticeable, its slower by about 1FPS --- wled00/FX.cpp | 2 +- wled00/FX.h | 2 +- wled00/FXparticleSystem.cpp | 38 ++++++------------------------------- 3 files changed, 8 insertions(+), 34 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 8390f040ae..fabdb660e6 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -11061,7 +11061,7 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_PARTICLEGHOSTRIDER, &mode_particleghostrider, _data_FX_MODE_PARTICLEGHOSTRIDER); addEffect(FX_MODE_PARTICLEBLOBS, &mode_particleblobs, _data_FX_MODE_PARTICLEBLOBS); addEffect(FX_MODE_PARTICLECENTERGEQ, &mode_particlecenterGEQ, _data_FX_MODE_PARTICLECIRCULARGEQ); - addEffect(FX_MODE_PSFRACTAL, &mode_particlefractal, _data_FX_MODE_PARTICLEFRACTAL); + // addEffect(FX_MODE_PSFRACTAL, &mode_particlefractal, _data_FX_MODE_PARTICLEFRACTAL); #endif // WLED_DISABLE_PARTICLESYSTEM2D diff --git a/wled00/FX.h b/wled00/FX.h index 80665ce440..7cc054a488 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -344,7 +344,7 @@ #define FX_MODE_PSSTARBURST 211 #define FX_MODE_PS1DGEQ 212 #define FX_MODE_PSFIRE1D 213 -#define FX_MODE_PSFRACTAL 214 +//#define FX_MODE_PSFRACTAL 214 #define MODE_COUNT 215 typedef enum mapping1D2D { diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index 8da1a58e68..94ad66b679 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -253,30 +253,8 @@ int32_t ParticleSystem::sprayEmit(PSsource &emitter, uint32_t amount) // 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<<1) - PS_P_RADIUS; // jitter the flame by one pixel to make the flames wider at the base - particles[emitIndex].x = emitter.source.x; - particles[emitIndex].y = emitter.source.y; - particles[emitIndex].vx = emitter.vx + random16(emitter.var) - (emitter.var >> 1); // random16 is good enough for fire and much faster - particles[emitIndex].vy = emitter.vy + random16(emitter.var) - (emitter.var >> 1); - particles[emitIndex].ttl = random(emitter.minLife, emitter.maxLife) + emitter.source.ttl; - // fire uses ttl and not hue for heat, so no need to set the hue - break; // done - } - } - /* - // note: this attemt to save on code size turns out to be much slower as fire uses a lot of particle emits, this must be efficient. also emitter.var would need adjustment - uint32_t partidx = sprayEmit(emitter); //emit one particle - // adjust properties - particles[partidx].x += random16(PS_P_RADIUS<<1) - PS_P_RADIUS; // jitter the flame by one pixel to make the flames wider at the base - particles[partidx].ttl += emitter.source.ttl; // flame intensity dies down with emitter TTL - */ + int emitIndex = sprayEmit(emitter); + if(emitIndex > 0) particles[emitIndex].ttl += emitter.source.ttl; } // Emits a particle at given angle and speed, angle is from 0-65535 (=0-360deg), speed is also affected by emitter->var @@ -2372,18 +2350,14 @@ void fast_color_add(CRGB &c1, CRGB &c2, uint32_t scale) b = c1.b + c2.b; } uint32_t max = r; - if (g > max) // note: using ? operator would be slower by 2 instructions - max = g; - if (b > max) - max = b; - if (max < 256) - { + if (g > max) max = g; + if (b > max) max = b; + if (max < 256) { c1.r = r; // save result to c1 c1.g = g; c1.b = b; } - else - { + else { c1.r = (r * 255) / max; c1.g = (g * 255) / max; c1.b = (b * 255) / max; From 3f3f801b57ce6a73f8fd079a69f84f4dccb6cccf Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Tue, 22 Oct 2024 04:53:05 +0200 Subject: [PATCH 130/219] Re-license from MIT to EUPL --- wled00/FXparticleSystem.cpp | 26 ++------------------------ wled00/FXparticleSystem.h | 20 ++------------------ 2 files changed, 4 insertions(+), 42 deletions(-) diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index 94ad66b679..8d1cfcb41d 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -4,30 +4,8 @@ Particle system with functions for particle generation, particle movement and particle rendering to RGB matrix. by DedeHai (Damian Schneider) 2013-2024 - LICENSE - The MIT License (MIT) - Copyright (c) 2024 Damian Schneider - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - The above copyright notice and this permission notice shall be included in - all copies or substantial portions of the Software. - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - THE SOFTWARE. -*/ - -/* - Note on ESP32: using 32bit integer is faster than 16bit or 8bit, each operation takes on less instruction, can be testen on https://godbolt.org/ - it does not matter if using int, unsigned int, uint32_t or int32_t, the compiler will make int into 32bit - this should be used to optimize speed but not if memory is affected much + Copyright (c) 2024 Damian Schneider + Licensed under the EUPL v. 1.2 or later */ /* diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index 8c324de2e9..83e6789c41 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -4,24 +4,8 @@ Particle system with functions for particle generation, particle movement and particle rendering to RGB matrix. by DedeHai (Damian Schneider) 2013-2024 - LICENSE - The MIT License (MIT) - Copyright (c) 2024 Damian Schneider - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - The above copyright notice and this permission notice shall be included in - all copies or substantial portions of the Software. - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - THE SOFTWARE. + Copyright (c) 2024 Damian Schneider + Licensed under the EUPL v. 1.2 or later */ #if !defined(WLED_DISABLE_PARTICLESYSTEM2D) || !defined(WLED_DISABLE_PARTICLESYSTEM1D) From 8a1364086e154f0ed20935ba0211b905eb33c07c Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Tue, 22 Oct 2024 22:01:39 +0200 Subject: [PATCH 131/219] Started cleanup, speed improvement to rendering, renamed class - removed 'smar' parameter in blur functions as smear is always used - improved particle rendering (passing by reference, passing wrap parameters for faster access) - renamed class to ParticleSystem2D - removed some whitespaces - some reformating, removed some comments - minor tweaks - removed non-working line-attractor function --- wled00/FX.cpp | 72 +-- wled00/FXparticleSystem.cpp | 890 ++++++++++++++++-------------------- wled00/FXparticleSystem.h | 127 +++-- 3 files changed, 473 insertions(+), 616 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 439af30235..115f7d2820 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -7806,7 +7806,7 @@ uint16_t mode_particlevortex(void) { if (SEGLEN == 1) return mode_static(); - ParticleSystem *PartSys = NULL; + ParticleSystem2D *PartSys = NULL; uint32_t i, j; if (SEGMENT.call == 0) // initialization @@ -7831,7 +7831,7 @@ uint16_t mode_particlevortex(void) PartSys->setKillOutOfBounds(true); } else - PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS if (PartSys == NULL) return mode_static(); // something went wrong, no data! @@ -7945,7 +7945,7 @@ uint16_t mode_particlefireworks(void) { if (SEGLEN == 1) return mode_static(); - ParticleSystem *PartSys = NULL; + ParticleSystem2D *PartSys = NULL; uint8_t numRockets; uint32_t i = 0; uint32_t j = 0; @@ -7964,7 +7964,7 @@ uint16_t mode_particlefireworks(void) } } else - PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS if (PartSys == NULL) return mode_static(); // something went wrong, no data! @@ -8118,7 +8118,7 @@ uint16_t mode_particlevolcano(void) { if (SEGLEN == 1) return mode_static(); - ParticleSystem *PartSys = NULL; + ParticleSystem2D *PartSys = NULL; PSsettings2D volcanosettings; volcanosettings.asByte = 0b00000100; // PS settings for volcano movement: bounceX is enabled uint8_t numSprays; // note: so far only one tested but more is possible @@ -8145,7 +8145,7 @@ uint16_t mode_particlevolcano(void) } } else - PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS if (PartSys == NULL) return mode_static(); // something went wrong, no data! @@ -8196,7 +8196,7 @@ uint16_t mode_particlefire(void) if (SEGLEN == 1) return mode_static(); - ParticleSystem *PartSys = NULL; + ParticleSystem2D *PartSys = NULL; uint32_t i; // index variable uint32_t numFlames; // number of flames: depends on fire width. for a fire width of 16 pixels, about 25-30 flames give good results @@ -8208,7 +8208,7 @@ uint16_t mode_particlefire(void) numFlames = PartSys->numSources; } else - PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS if (PartSys == NULL) return mode_static(); // something went wrong, no data! @@ -8308,7 +8308,7 @@ uint16_t mode_particlepit(void) { if (SEGLEN == 1) return mode_static(); - ParticleSystem *PartSys = NULL; + ParticleSystem2D *PartSys = NULL; if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. { @@ -8319,7 +8319,7 @@ uint16_t mode_particlepit(void) PartSys->setUsedParticles((PartSys->numParticles*3)/2); // use 2/3 of available particles } else - PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS if (PartSys == NULL) return mode_static(); // something went wrong, no data! @@ -8391,7 +8391,7 @@ uint16_t mode_particlewaterfall(void) { if (SEGLEN == 1) return mode_static(); - ParticleSystem *PartSys = NULL; + ParticleSystem2D *PartSys = NULL; uint8_t numSprays; uint32_t i = 0; @@ -8416,7 +8416,7 @@ uint16_t mode_particlewaterfall(void) } } else - PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS if (PartSys == NULL) return mode_static(); // something went wrong, no data! (TODO: ask how to handle this so it always works) @@ -8473,7 +8473,7 @@ uint16_t mode_particlebox(void) if (SEGLEN == 1) return mode_static(); - ParticleSystem *PartSys = NULL; + ParticleSystem2D *PartSys = NULL; uint32_t i; if (SEGMENT.call == 0) // initialization @@ -8500,7 +8500,7 @@ uint16_t mode_particlebox(void) SEGENV.aux0 = rand(); // position in perlin noise } else - PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS if (PartSys == NULL) return mode_static(); // something went wrong, no data! @@ -8574,7 +8574,7 @@ uint16_t mode_particleperlin(void) { if (SEGLEN == 1) return mode_static(); - ParticleSystem *PartSys = NULL; + ParticleSystem2D *PartSys = NULL; uint32_t i; if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. { @@ -8588,7 +8588,7 @@ uint16_t mode_particleperlin(void) } } else - PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS if (PartSys == NULL) return mode_static(); // something went wrong, no data! @@ -8643,7 +8643,7 @@ uint16_t mode_particleimpact(void) { if (SEGLEN == 1) return mode_static(); - ParticleSystem *PartSys = NULL; + ParticleSystem2D *PartSys = NULL; uint32_t i = 0; uint8_t MaxNumMeteors; PSsettings2D meteorsettings; @@ -8665,7 +8665,7 @@ uint16_t mode_particleimpact(void) } } else - PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS if (PartSys == NULL) return mode_static(); // something went wrong, no data! (TODO: ask how to handle this so it always works) @@ -8776,7 +8776,7 @@ uint16_t mode_particleattractor(void) { if (SEGLEN == 1) return mode_static(); - ParticleSystem *PartSys = NULL; + ParticleSystem2D *PartSys = NULL; PSsettings2D sourcesettings; sourcesettings.asByte = 0b00001100; // PS settings for bounceY, bounceY used for source movement (it always bounces whereas particles do not) PSparticle *attractor; // particle pointer to the attractor @@ -8801,7 +8801,7 @@ uint16_t mode_particleattractor(void) PartSys->setWallRoughness(200); //randomize wall bounce } else - PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS if (PartSys == NULL) return mode_static(); // something went wrong, no data! @@ -8897,7 +8897,7 @@ uint16_t mode_particleattractor(void) { if (SEGLEN == 1) return mode_static(); - ParticleSystem *PartSys = NULL; + ParticleSystem2D *PartSys = NULL; uint32_t i = 0; PSparticle *attractor; // particle pointer to the attractor uint8_t *counters; // counters for the applied force @@ -8930,7 +8930,7 @@ uint16_t mode_particleattractor(void) PartSys->sources[0].var = 4; // emiting variation } else - PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS if (PartSys == NULL) return mode_static(); // something went wrong, no data! @@ -9012,7 +9012,7 @@ uint16_t mode_particlespray(void) { if (SEGLEN == 1) return mode_static(); - ParticleSystem *PartSys = NULL; + ParticleSystem2D *PartSys = NULL; //uint8_t numSprays; const uint8_t hardness = 200; // collision hardness is fixed @@ -9029,7 +9029,7 @@ uint16_t mode_particlespray(void) } else - PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS if (PartSys == NULL) return mode_static(); // something went wrong, no data! @@ -9109,7 +9109,7 @@ uint16_t mode_particleGEQ(void) if (SEGLEN == 1) return mode_static(); - ParticleSystem *PartSys = NULL; + ParticleSystem2D *PartSys = NULL; if (SEGMENT.call == 0) // initialization { @@ -9119,7 +9119,7 @@ uint16_t mode_particleGEQ(void) PartSys->setUsedParticles((PartSys->numParticles * 3) / 2); // use 2/3 of available particles } else - PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS if (PartSys == NULL) return mode_static(); // something went wrong, no data! @@ -9205,7 +9205,7 @@ uint16_t mode_particlecenterGEQ(void) if (SEGLEN == 1) return mode_static(); - ParticleSystem *PartSys = NULL; + ParticleSystem2D *PartSys = NULL; uint8_t numSprays; uint32_t i; @@ -9225,7 +9225,7 @@ if (SEGLEN == 1) PartSys->setKillOutOfBounds(true); } else - PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS if (PartSys == NULL) return mode_static(); // something went wrong, no data! @@ -9286,7 +9286,7 @@ uint16_t mode_particleghostrider(void) { if (SEGLEN == 1) return mode_static(); - ParticleSystem *PartSys = NULL; + ParticleSystem2D *PartSys = NULL; PSsettings2D ghostsettings; ghostsettings.asByte = 0b0000011; //enable wrapX and wrapY @@ -9302,7 +9302,7 @@ uint16_t mode_particleghostrider(void) SEGENV.step = random(MAXANGLESTEP) - (MAXANGLESTEP>>1); // angle increment } else - PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS if (PartSys == NULL) return mode_static(); // something went wrong, no data! @@ -9378,7 +9378,7 @@ uint16_t mode_particleblobs(void) { if (SEGLEN == 1) return mode_static(); - ParticleSystem *PartSys = NULL; + ParticleSystem2D *PartSys = NULL; if (SEGMENT.call == 0) { @@ -9392,7 +9392,7 @@ uint16_t mode_particleblobs(void) //PartSys->setParticleSize(0); //set global size to zero or motion blur cannot be used (is zero by default) } else - PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS if (PartSys == NULL) return mode_static(); // something went wrong, no data! @@ -9504,7 +9504,7 @@ uint16_t mode_particlefractal(void) if (SEGLEN == 1) return mode_static(); - ParticleSystem *PartSys = NULL; + ParticleSystem2D *PartSys = NULL; uint32_t i; if (SEGMENT.call == 0) // initialization @@ -9514,7 +9514,7 @@ if (SEGLEN == 1) PartSys->setKillOutOfBounds(true); } else - PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS if (PartSys == NULL) return mode_static(); // something went wrong, no data! @@ -10071,8 +10071,8 @@ uint16_t mode_particleSparkler(void) // Particle System settings PartSys->updateSystem(); // update system properties (dimensions and data pointers) - sparklersettings.wrapX = SEGMENT.check2; - sparklersettings.bounceX = !SEGMENT.check2; + sparklersettings.wrap = SEGMENT.check2; + sparklersettings.bounce = !SEGMENT.check2; numSparklers = PartSys->numSources; PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index 8d1cfcb41d..55551f2236 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -10,18 +10,18 @@ /* TODO: - -add function to 'update sources' so FX does not have to take care of that. FX can still implement its own version if so desired. + -add function to 'update sources' so FX does not have to take care of that. FX can still implement its own version if so desired. -add an x/y struct, do particle rendering using that, much easier to read */ -#if !defined(WLED_DISABLE_PARTICLESYSTEM2D) || !defined(WLED_DISABLE_PARTICLESYSTEM1D) +#if !defined(WLED_DISABLE_PARTICLESYSTEM2D) || !defined(WLED_DISABLE_PARTICLESYSTEM1D) #include "FXparticleSystem.h" #endif #ifndef WLED_DISABLE_PARTICLESYSTEM2D -ParticleSystem::ParticleSystem(uint16_t width, uint16_t height, uint16_t numberofparticles, uint16_t numberofsources, bool isadvanced, bool sizecontrol) +ParticleSystem2D::ParticleSystem2D(uint16_t width, uint16_t height, uint16_t numberofparticles, uint16_t numberofsources, bool isadvanced, bool sizecontrol) { //Serial.println("PS Constructor"); numSources = numberofsources; @@ -52,9 +52,9 @@ ParticleSystem::ParticleSystem(uint16_t width, uint16_t height, uint16_t numbero } // update function applies gravity, moves the particles, handles collisions and renders the particles -void ParticleSystem::update(void) +void ParticleSystem2D::update(void) { - PSadvancedParticle *advprop = NULL; + PSadvancedParticle *advprop = NULL; //apply gravity globally if enabled if (particlesettings.useGravity) applyGravity(); @@ -63,11 +63,11 @@ void ParticleSystem::update(void) if (advPartSize) { for (uint32_t i = 0; i < usedParticles; i++) - { + { updateSize(&advPartProps[i], &advPartSize[i]); } } - + // handle collisions (can push particles, must be done before updating particles or they can render out of bounds, causing a crash if using local buffer for speed) if (particlesettings.useCollisions) handleCollisions(); @@ -96,34 +96,34 @@ void ParticleSystem::update(void) } // update function for fire animation -void ParticleSystem::updateFire(uint32_t intensity, bool renderonly) +void ParticleSystem2D::updateFire(uint32_t intensity, bool renderonly) { if (!renderonly) fireParticleupdate(); ParticleSys_render(true, intensity); } -void ParticleSystem::setUsedParticles(uint32_t num) +void ParticleSystem2D::setUsedParticles(uint32_t num) { usedParticles = min(num, numParticles); //limit to max particles } -void ParticleSystem::setWallHardness(uint8_t hardness) +void ParticleSystem2D::setWallHardness(uint8_t hardness) { wallHardness = hardness; } -void ParticleSystem::setWallRoughness(uint8_t roughness) +void ParticleSystem2D::setWallRoughness(uint8_t roughness) { wallRoughness = roughness; } -void ParticleSystem::setCollisionHardness(uint8_t hardness) -{ +void ParticleSystem2D::setCollisionHardness(uint8_t hardness) +{ collisionHardness = (int)hardness + 1; } -void ParticleSystem::setMatrixSize(uint16_t x, uint16_t y) +void ParticleSystem2D::setMatrixSize(uint16_t x, uint16_t y) { maxXpixel = x - 1; // last physical pixel that can be drawn to maxYpixel = y - 1; @@ -131,44 +131,44 @@ void ParticleSystem::setMatrixSize(uint16_t x, uint16_t y) maxY = y * PS_P_RADIUS - 1; // this value is often needed (also by FX) to calculate positions } -void ParticleSystem::setWrapX(bool enable) +void ParticleSystem2D::setWrapX(bool enable) { particlesettings.wrapX = enable; } -void ParticleSystem::setWrapY(bool enable) +void ParticleSystem2D::setWrapY(bool enable) { particlesettings.wrapY = enable; } -void ParticleSystem::setBounceX(bool enable) +void ParticleSystem2D::setBounceX(bool enable) { particlesettings.bounceX = enable; } -void ParticleSystem::setBounceY(bool enable) +void ParticleSystem2D::setBounceY(bool enable) { particlesettings.bounceY = enable; } -void ParticleSystem::setKillOutOfBounds(bool enable) +void ParticleSystem2D::setKillOutOfBounds(bool enable) { particlesettings.killoutofbounds = enable; } -void ParticleSystem::setColorByAge(bool enable) +void ParticleSystem2D::setColorByAge(bool enable) { particlesettings.colorByAge = enable; } -void ParticleSystem::setMotionBlur(uint8_t bluramount) +void ParticleSystem2D::setMotionBlur(uint8_t bluramount) { if (particlesize == 0) // only allwo motion blurring on default particle size or advanced size(cannot combine motion blur with normal blurring used for particlesize, would require another buffer) motionBlur = bluramount; } // render size using smearing (see blur function) -void ParticleSystem::setParticleSize(uint8_t size) +void ParticleSystem2D::setParticleSize(uint8_t size) { particlesize = size; particleHardRadius = PS_P_MINHARDRADIUS + particlesize; // note: this sets size if not using advanced props @@ -177,25 +177,25 @@ void ParticleSystem::setParticleSize(uint8_t size) // enable/disable gravity, optionally, set the force (force=8 is default) can be -127 to +127, 0 is disable // if enabled, gravity is applied to all particles in ParticleSystemUpdate() // 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::setGravity(int8_t force) -{ +void ParticleSystem2D::setGravity(int8_t force) +{ if (force) { gforce = force; particlesettings.useGravity = true; } - else + 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 +void ParticleSystem2D::enableParticleCollisions(bool enable, uint8_t hardness) // enable/disable gravity, optionally, set the force (force=8 is default) can be 1-255, 0 is also disable { particlesettings.useCollisions = enable; collisionHardness = (int)hardness + 1; } // emit one particle with variation, returns index of last emitted particle (or -1 if no particle emitted) -int32_t ParticleSystem::sprayEmit(PSsource &emitter, uint32_t amount) +int32_t ParticleSystem2D::sprayEmit(PSsource &emitter, uint32_t amount) { bool success = false; for (uint32_t a = 0; a < amount; a++) @@ -208,16 +208,16 @@ int32_t ParticleSystem::sprayEmit(PSsource &emitter, uint32_t amount) if (particles[emitIndex].ttl == 0) // find a dead particle { success = true; - particles[emitIndex].vx = emitter.vx + random(-emitter.var, emitter.var); + particles[emitIndex].vx = emitter.vx + random(-emitter.var, emitter.var); particles[emitIndex].vy = emitter.vy + random(-emitter.var, emitter.var); - particles[emitIndex].x = emitter.source.x; - particles[emitIndex].y = emitter.source.y; + particles[emitIndex].x = emitter.source.x; + particles[emitIndex].y = emitter.source.y; particles[emitIndex].hue = emitter.source.hue; particles[emitIndex].sat = emitter.source.sat; particles[emitIndex].collide = emitter.source.collide; particles[emitIndex].ttl = random(emitter.minLife, emitter.maxLife); if (advPartProps) - advPartProps[emitIndex].size = emitter.size; + advPartProps[emitIndex].size = emitter.size; break; } } @@ -229,7 +229,7 @@ int32_t ParticleSystem::sprayEmit(PSsource &emitter, uint32_t amount) } // Spray emitter for particles used for flames (particle TTL depends on source TTL) -void ParticleSystem::flameEmit(PSsource &emitter) +void ParticleSystem2D::flameEmit(PSsource &emitter) { int emitIndex = sprayEmit(emitter); if(emitIndex > 0) particles[emitIndex].ttl += emitter.source.ttl; @@ -237,22 +237,22 @@ void ParticleSystem::flameEmit(PSsource &emitter) // Emits a particle at given angle and speed, angle is from 0-65535 (=0-360deg), speed is also affected by emitter->var // angle = 0 means in positive x-direction (i.e. to the right) -int32_t ParticleSystem::angleEmit(PSsource &emitter, uint16_t angle, int8_t speed, uint32_t amount) +int32_t ParticleSystem2D::angleEmit(PSsource &emitter, uint16_t angle, int8_t speed, uint32_t amount) { - emitter.vx = ((int32_t)cos16(angle) * (int32_t)speed) / (int32_t)32600; // cos16() and sin16() return signed 16bit, division should be 32767 but 32600 gives slightly better rounding + emitter.vx = ((int32_t)cos16(angle) * (int32_t)speed) / (int32_t)32600; // cos16() and sin16() return signed 16bit, division should be 32767 but 32600 gives slightly better rounding emitter.vy = ((int32_t)sin16(angle) * (int32_t)speed) / (int32_t)32600; // note: cannot use bit shifts as bit shifting is asymmetrical for positive and negative numbers and this needs to be accurate! return sprayEmit(emitter, amount); } // particle moves, decays and dies, if killoutofbounds is set, out of bounds particles are set to ttl=0 // uses passed settings to set bounce or wrap, if useGravity is set, it will never bounce at the top and killoutofbounds is not applied over the top -void ParticleSystem::particleMoveUpdate(PSparticle &part, PSsettings2D *options, PSadvancedParticle *advancedproperties) +void ParticleSystem2D::particleMoveUpdate(PSparticle &part, PSsettings2D *options, PSadvancedParticle *advancedproperties) { if (options == NULL) options = &particlesettings; //use PS system settings by default if (part.ttl > 0) { - if (!part.perpetual) + if (!part.perpetual) part.ttl--; // age if (particlesettings.colorByAge) part.hue = part.ttl > 255 ? 255 : part.ttl; //set color to ttl @@ -263,25 +263,25 @@ void ParticleSystem::particleMoveUpdate(PSparticle &part, PSsettings2D *options, part.outofbounds = 0; // reset out of bounds (in case particle was created outside the matrix and is now moving into view) if (advancedproperties) //using individual particle size? - { + { if (advancedproperties->size > 0) usesize = true; // note: variable eases out of frame checking below particleHardRadius = max(PS_P_MINHARDRADIUS, (int)particlesize + (advancedproperties->size)); } // if wall collisions are enabled, bounce them before they reach the edge, it looks much nicer if the particle is not half out of view - if (options->bounceX) + if (options->bounceX) { if ((newX < particleHardRadius) || (newX > maxX - particleHardRadius)) // reached a wall bounce(part.vx, part.vy, newX, maxX); } - + if ((newX < 0) || (newX > maxX)) // check if particle reached an edge (note: this also checks out of bounds and must not be skipped, even if bounce is enabled) - { + { if (options->wrapX) - { - newX = newX % (maxX + 1); + { + newX = newX % (maxX + 1); if (newX < 0) - newX += maxX + 1; + newX += maxX + 1; } else if (((newX <= -PS_P_HALFRADIUS) || (newX > maxX + PS_P_HALFRADIUS))) // particle is leaving, set out of bounds if it has fully left { @@ -289,9 +289,9 @@ void ParticleSystem::particleMoveUpdate(PSparticle &part, PSsettings2D *options, if (usesize) // using individual particle size { if (((newX > -particleHardRadius) && (newX < maxX + particleHardRadius))) // large particle is not yet leaving the view - note: this is not pixel perfect but good enough - isleaving = false; + isleaving = false; } - + if (isleaving) { part.outofbounds = 1; @@ -301,21 +301,21 @@ void ParticleSystem::particleMoveUpdate(PSparticle &part, PSsettings2D *options, } } - if (options->bounceY) + if (options->bounceY) { if ((newY < particleHardRadius) || ((newY > maxY - particleHardRadius) && !options->useGravity)) // reached floor / ceiling - { - bounce(part.vy, part.vx, newY, maxY); + { + bounce(part.vy, part.vx, newY, maxY); } } - + if (((newY < 0) || (newY > maxY))) // check if particle reached an edge (makes sure particles are within frame for rendering) { if (options->wrapY) { - newY = newY % (maxY + 1); + newY = newY % (maxY + 1); if (newY < 0) - newY += maxY + 1; + newY += maxY + 1; } else if (((newY <= -PS_P_HALFRADIUS) || (newY > maxY + PS_P_HALFRADIUS))) // particle is leaving, set out of bounds if it has fully left { @@ -323,7 +323,7 @@ void ParticleSystem::particleMoveUpdate(PSparticle &part, PSsettings2D *options, if (usesize) // using individual particle size { if (((newY > -particleHardRadius) && (newY < maxY + particleHardRadius))) // still withing rendering reach - isleaving = false; + isleaving = false; } if (isleaving) { @@ -343,10 +343,9 @@ void ParticleSystem::particleMoveUpdate(PSparticle &part, PSsettings2D *options, } } -// update advanced particle size control -void ParticleSystem::updateSize(PSadvancedParticle *advprops, PSsizeControl *advsize) -{ - if (advsize == NULL) // just a safety check +// update advanced particle size control +void ParticleSystem2D::updateSize(PSadvancedParticle *advprops, PSsizeControl *advsize) { + if (advsize == NULL) // safety check return; // grow/shrink particle int32_t newsize = advprops->size; @@ -355,77 +354,68 @@ void ParticleSystem::updateSize(PSadvancedParticle *advprops, PSsizeControl *adv // calculate grow speed using 0-8 for low speeds and 9-15 for higher speeds if (advsize->grow) increment = advsize->growspeed; else if (advsize->shrink) increment = advsize->shrinkspeed; - if (increment < 9) // 8 means +1 every frame - { + if (increment < 9) { // 8 means +1 every frame counter += increment; - if (counter > 7) - { + if (counter > 7) { counter -= 8; increment = 1; - } - else + } else increment = 0; - advsize->sizecounter = counter; - } - else{ + advsize->sizecounter = counter; + } else { increment = (increment - 8) << 1; // 9 means +2, 10 means +4 etc. 15 means +14 } - if (advsize->grow) - { - if (newsize < advsize->maxsize) - { - newsize += increment; - if (newsize >= advsize->maxsize) - { + + if (advsize->grow) { + if (newsize < advsize->maxsize) { + newsize += increment; + if (newsize >= advsize->maxsize) { advsize->grow = false; // stop growing, shrink from now on if enabled newsize = advsize->maxsize; // limit if (advsize->pulsate) advsize->shrink = true; } } - } - else if (advsize->shrink) - { - if (newsize > advsize->minsize) - { - newsize -= increment; - if (newsize <= advsize->minsize) - { - //if (advsize->minsize == 0) part.ttl = 0; //TODO: need to pass particle or return kill instruction + } else if (advsize->shrink) { + if (newsize > advsize->minsize) { + newsize -= increment; + if (newsize <= advsize->minsize) { + //if (advsize->minsize == 0) part.ttl = 0; //TODO: need to pass particle or return kill instruction advsize->shrink = false; // disable shrinking - newsize = advsize->minsize; // limit + newsize = advsize->minsize; // limit if (advsize->pulsate) advsize->grow = true; } } } advprops->size = newsize; // handle wobbling - if (advsize->wobble) - { + if (advsize->wobble) { advsize->asymdir += advsize->wobblespeed; // todo: need better wobblespeed control? counter is already in the struct... } } // calculate x and y size for asymmetrical particles (advanced size control) -void ParticleSystem::getParticleXYsize(PSadvancedParticle *advprops, PSsizeControl *advsize, uint32_t &xsize, uint32_t &ysize) +void ParticleSystem2D::getParticleXYsize(PSadvancedParticle *advprops, PSsizeControl *advsize, uint32_t &xsize, uint32_t &ysize) { - if (advsize == NULL) // if advanced size is valid, also advced properties pointer is valid (handled by pointer assignment function) + if (advsize == NULL) // if advsize is valid, also advanced properties pointer is valid (handled by updatePSpointers()) return; - int32_t deviation = ((uint32_t)advprops->size * (uint32_t)advsize->asymmetry) / 255; // deviation from symmetrical size + int32_t size = advprops->size; + int32_t asymdir = advsize->asymdir; + int32_t deviation = ((uint32_t)size * (uint32_t)advsize->asymmetry) / 255; // deviation from symmetrical size // Calculate x and y size based on deviation and direction (0 is symmetrical, 64 is x, 128 is symmetrical, 192 is y) - if (advsize->asymdir < 64) { - deviation = ((int32_t)advsize->asymdir * deviation) / 64; - } else if (advsize->asymdir < 192) { - deviation = ((128 - (int32_t)advsize->asymdir) * deviation) / 64; + if (asymdir < 64) { + deviation = (asymdir * deviation) / 64; + } else if (asymdir < 192) { + deviation = ((128 - asymdir) * deviation) / 64; } else { - deviation = (((int32_t)advsize->asymdir - 255) * deviation) / 64; + deviation = ((asymdir - 255) * deviation) / 64; } // Calculate x and y size based on deviation, limit to 255 (rendering function cannot handle lareger sizes) - xsize = ((int32_t)advprops->size - deviation) > 255 ? 255 : advprops->size - deviation; - ysize = ((int32_t)advprops->size + deviation) > 255 ? 255 : advprops->size + deviation; + xsize = (size - deviation) > 255 ? 255 : size - deviation; + ysize = (size + deviation) > 255 ? 255 : size + deviation; } // function to bounce a particle from a wall using set parameters (wallHardness and wallRoughness) -void ParticleSystem::bounce(int8_t &incomingspeed, int8_t ¶llelspeed, int32_t &position, uint16_t maxposition) +void ParticleSystem2D::bounce(int8_t &incomingspeed, int8_t ¶llelspeed, int32_t &position, uint16_t maxposition) { incomingspeed = -incomingspeed; incomingspeed = (incomingspeed * wallHardness) / 255; // reduce speed as energy is lost on non-hard surface @@ -433,12 +423,11 @@ void ParticleSystem::bounce(int8_t &incomingspeed, int8_t ¶llelspeed, int32_ position = particleHardRadius; // fast particles will never reach the edge if position is inverted, this looks better else position = maxposition - particleHardRadius; - if (wallRoughness) - { + if (wallRoughness) { int32_t incomingspeed_abs = abs((int32_t)incomingspeed); int32_t totalspeed = incomingspeed_abs + abs((int32_t)parallelspeed); // transfer an amount of incomingspeed speed to parallel speed - int32_t donatespeed = (random(-incomingspeed_abs, incomingspeed_abs) * (int32_t)wallRoughness) / (int32_t)255; //take random portion of + or - perpendicular speed, scaled by roughness + int32_t donatespeed = (random(-incomingspeed_abs, incomingspeed_abs) * (int32_t)wallRoughness) / (int32_t)255; //take random portion of + or - perpendicular speed, scaled by roughness parallelspeed = limitSpeed((int32_t)parallelspeed + donatespeed); //give the remainder of the speed to perpendicular speed donatespeed = int8_t(totalspeed - abs(parallelspeed)); // keep total speed the same @@ -449,7 +438,7 @@ void ParticleSystem::bounce(int8_t &incomingspeed, int8_t ¶llelspeed, int32_ // apply a force in x,y direction to individual particle // caller needs to provide a 8bit counter (for each paticle) that holds its value between calls // 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, int8_t xforce, int8_t yforce, uint8_t *counter) +void ParticleSystem2D::applyForce(PSparticle *part, 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 @@ -468,8 +457,8 @@ void ParticleSystem::applyForce(PSparticle *part, int8_t xforce, int8_t yforce, part->vy = limitSpeed((int32_t)part->vy + dvy); } -// apply a force in x,y direction to individual particle using advanced particle properties -void ParticleSystem::applyForce(uint16_t particleindex, int8_t xforce, int8_t yforce) +// apply a force in x,y direction to individual particle using advanced particle properties +void ParticleSystem2D::applyForce(uint16_t particleindex, int8_t xforce, int8_t yforce) { if (advPartProps == NULL) return; // no advanced properties available @@ -478,7 +467,7 @@ void ParticleSystem::applyForce(uint16_t particleindex, int8_t xforce, int8_t yf // apply a force in x,y direction to all particles // force is in 3.4 fixed point notation (see above) -void ParticleSystem::applyForce(int8_t xforce, int8_t yforce) +void ParticleSystem2D::applyForce(int8_t xforce, int8_t yforce) { // for small forces, need to use a delay counter uint8_t tempcounter; @@ -487,7 +476,7 @@ void ParticleSystem::applyForce(int8_t xforce, int8_t yforce) { tempcounter = forcecounter; applyForce(&particles[i], xforce, yforce, &tempcounter); - } + } forcecounter = tempcounter; //save value back } @@ -495,7 +484,7 @@ void ParticleSystem::applyForce(int8_t xforce, int8_t yforce) // caller needs to provide a 8bit counter that holds its value between calls (if using single particles, a counter for each particle is needed) // angle is from 0-65535 (=0-360deg) angle = 0 means in positive x-direction (i.e. to the right) // force is in 3.4 fixed point notation so force=16 means apply v+1 each frame (useful force range is +/- 127) -void ParticleSystem::applyAngleForce(PSparticle *part, int8_t force, uint16_t angle, uint8_t *counter) +void ParticleSystem2D::applyAngleForce(PSparticle *part, int8_t force, uint16_t angle, uint8_t *counter) { int8_t xforce = ((int32_t)force * cos16(angle)) / 32767; // force is +/- 127 int8_t yforce = ((int32_t)force * sin16(angle)) / 32767; // note: cannot use bit shifts as bit shifting is asymmetrical for positive and negative numbers and this needs to be accurate! @@ -503,7 +492,7 @@ void ParticleSystem::applyAngleForce(PSparticle *part, int8_t force, uint16_t an applyForce(part, xforce, yforce, counter); } -void ParticleSystem::applyAngleForce(uint16_t particleindex, int8_t force, uint16_t angle) +void ParticleSystem2D::applyAngleForce(uint16_t particleindex, int8_t force, uint16_t angle) { if (advPartProps == NULL) return; // no advanced properties available @@ -512,7 +501,7 @@ void ParticleSystem::applyAngleForce(uint16_t particleindex, int8_t force, uint1 // apply a force in angular direction to all particles // angle is from 0-65535 (=0-360deg) angle = 0 means in positive x-direction (i.e. to the right) -void ParticleSystem::applyAngleForce(int8_t force, uint16_t angle) +void ParticleSystem2D::applyAngleForce(int8_t force, uint16_t angle) { int8_t xforce = ((int32_t)force * cos16(angle)) / 32767; // force is +/- 127 int8_t yforce = ((int32_t)force * sin16(angle)) / 32767; // note: cannot use bit shifts as bit shifting is asymmetrical for positive and negative numbers and this needs to be accurate! @@ -523,39 +512,39 @@ void ParticleSystem::applyAngleForce(int8_t force, uint16_t angle) // apply gravity to all particles using PS global gforce setting // force is in 3.4 fixed point notation, see note above // note: faster than apply force since direction is always down and counter is fixed for all particles -void ParticleSystem::applyGravity() +void ParticleSystem2D::applyGravity() { int32_t dv = calcForce_dv(gforce, &gforcecounter); for (uint32_t i = 0; i < usedParticles; 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 = limitSpeed((int32_t)particles[i].vy - dv); - } + } } // apply gravity to single particle using system settings (use this for sources) // function does not increment gravity counter, if gravity setting is disabled, this cannot be used -void ParticleSystem::applyGravity(PSparticle *part) +void ParticleSystem2D::applyGravity(PSparticle *part) { uint32_t counterbkp = gforcecounter; int32_t dv = calcForce_dv(gforce, &gforcecounter); - gforcecounter = counterbkp; //save it back + gforcecounter = counterbkp; //save it back part->vy = limitSpeed((int32_t)part->vy - dv); } // slow down particle by friction, the higher the speed, the higher the friction. a high friction coefficient slows them more (255 means instant stop) // note: a coefficient smaller than 0 will speed them up (this is a feature, not a bug), coefficient larger than 255 inverts the speed, so don't do that -void ParticleSystem::applyFriction(PSparticle *part, int32_t coefficient) +void ParticleSystem2D::applyFriction(PSparticle *part, int32_t coefficient) { int32_t friction = 255 - coefficient; // note: not checking if particle is dead can be done by caller (or can be omitted) // note2: cannot use right shifts as bit shifting in right direction is asymmetrical for positive and negative numbers and this needs to be accurate or things start to go to the left side. - part->vx = ((int16_t)part->vx * friction) / 255; - part->vy = ((int16_t)part->vy * friction) / 255; + part->vx = ((int16_t)part->vx * friction) / 255; + part->vy = ((int16_t)part->vy * friction) / 255; } // apply friction to all particles -void ParticleSystem::applyFriction(int32_t coefficient) +void ParticleSystem2D::applyFriction(int32_t coefficient) { for (uint32_t i = 0; i < usedParticles; i++) { @@ -565,7 +554,7 @@ void ParticleSystem::applyFriction(int32_t coefficient) } // attracts a particle to an attractor particle using the inverse square-law -void ParticleSystem::pointAttractor(uint16_t particleindex, PSparticle *attractor, uint8_t strength, bool swallow) +void ParticleSystem2D::pointAttractor(uint16_t particleindex, PSparticle *attractor, uint8_t strength, bool swallow) { if (advPartProps == NULL) return; // no advanced properties available @@ -579,9 +568,9 @@ void ParticleSystem::pointAttractor(uint16_t particleindex, PSparticle *attracto if (distanceSquared < 8192) { if (swallow) // particle is close, age it fast so it fades out, do not attract further - { + { if (particles[particleindex].ttl > 7) - particles[particleindex].ttl -= 8; + particles[particleindex].ttl -= 8; else { particles[particleindex].ttl = 0; @@ -598,69 +587,13 @@ void ParticleSystem::pointAttractor(uint16_t particleindex, PSparticle *attracto applyForce(particleindex, xforce, yforce); } -/* -//attract to a line (TODO: this is not yet working) -void ParticleSystem::lineAttractor(uint16_t particleindex, PSparticle *attractorcenter, uint16_t attractorangle, uint8_t strength) -{ - // Calculate the distance between the particle and the attractor - if (advPartProps == NULL) - return; // no advanced properties available - - // calculate a second point on the line - int32_t x1 = attractorcenter->x + (cos16(attractorangle) >> 5); - int32_t y1 = attractorcenter->y + (sin16(attractorangle) >> 5); - // calculate squared distance from particle to the line: - int32_t dx = (x1 - attractorcenter->x) >> 4; - int32_t dy = (y1 - attractorcenter->y) >> 4; - int32_t d = ((dx * (particles[particleindex].y - attractorcenter->y)) - (dy * (particles[particleindex].x - attractorcenter->x))) >> 8; - int32_t distanceSquared = (d * d) / (dx * dx + dy * dy); - - - // Calculate the force based on inverse square law - if (distanceSquared < 2) - { - distanceSquared = 1; - // distanceSquared = 4 * PS_P_RADIUS * PS_P_RADIUS; // limit the distance to avoid very high forces - } - - int32_t force = (((int32_t)strength << 16) / distanceSquared)>>10; - //apply force in a 90° angle to the line - int8_t xforce = (d > 0 ? 1 : -1) * (force * dy) / 100; // scale to a lower value, found by experimenting - int8_t yforce = (d > 0 ? -1 : 1) * (force * dx) / 100; // note: cannot use bit shifts as bit shifting is asymmetrical for positive and negative numbers and this needs to be accurate! - - Serial.print(" partx: "); - Serial.print(particles[particleindex].x); - Serial.print(" party "); - Serial.print(particles[particleindex].y); - Serial.print(" x1 "); - Serial.print(x1); - Serial.print(" y1 "); - Serial.print(y1); - Serial.print(" dx "); - Serial.print(dx); - Serial.print(" dy "); - Serial.print(dy); - Serial.print(" d: "); - Serial.print(d); - Serial.print(" dsq: "); - Serial.print(distanceSquared); - Serial.print(" force: "); - Serial.print(force); - Serial.print(" fx: "); - Serial.print(xforce); - Serial.print(" fy: "); - Serial.println(yforce); - - applyForce(particleindex, xforce, yforce); -}*/ - // 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 // warning: do not render out of bounds particles or system will crash! rendering does not check if particle is out of bounds // fireintensity and firemode are optional arguments (fireintensity is only used in firemode) -void ParticleSystem::ParticleSys_render(bool firemode, uint32_t fireintensity) +void ParticleSystem2D::ParticleSys_render(bool firemode, uint32_t fireintensity) { - + CRGB baseRGB; bool useLocalBuffer = true; //use local rendering buffer, gives huge speed boost (at least 30% more FPS) CRGB **framebuffer = NULL; //local frame buffer @@ -669,7 +602,7 @@ void ParticleSystem::ParticleSys_render(bool firemode, uint32_t fireintensity) uint32_t brightness; // particle brightness, fades if dying if (useLocalBuffer) - { + { /* //memory fragmentation check: Serial.print("heap: "); @@ -687,7 +620,7 @@ void ParticleSystem::ParticleSys_render(bool firemode, uint32_t fireintensity) } else{ if (advPartProps) - { + { renderbuffer = allocate2Dbuffer(10, 10); //buffer to render individual particles to if size > 0. note: null checking is done when accessing it } if (motionBlur > 0) // using SEGMENT.fadeToBlackBy is much slower, this approximately doubles the speed of fade calculation @@ -705,15 +638,16 @@ void ParticleSystem::ParticleSys_render(bool firemode, uint32_t fireintensity) } } } - - if (!useLocalBuffer) //disabled or allocation above failed - { + + if (!useLocalBuffer) { //disabled or allocation above failed //Serial.println("NOT using local buffer!"); if (motionBlur > 0) SEGMENT.fadeToBlackBy(255 - motionBlur); else - SEGMENT.fill(BLACK); //clear the buffer before rendering to it + SEGMENT.fill(BLACK); //clear the buffer before rendering to it } + bool wrapX = particlesettings.wrapX; // use local variables for faster access + bool wrapY = particlesettings.wrapY; // go over particles and render them to the buffer for (i = 0; i < usedParticles; i++) { @@ -723,34 +657,34 @@ void ParticleSystem::ParticleSys_render(bool firemode, uint32_t fireintensity) // generate RGB values for particle if (firemode) { - brightness = (uint32_t)particles[i].ttl*(3 + (fireintensity >> 5)) + 20; - brightness = brightness > 255 ? 255 : brightness; // faster then using min() + brightness = (uint32_t)particles[i].ttl*(3 + (fireintensity >> 5)) + 20; + brightness = brightness > 255 ? 255 : brightness; baseRGB = ColorFromPalette(SEGPALETTE, brightness, 255); } else{ - brightness = particles[i].ttl > 255 ? 255 : particles[i].ttl; //faster then using min() + brightness = particles[i].ttl > 255 ? 255 : particles[i].ttl; baseRGB = ColorFromPalette(SEGPALETTE, particles[i].hue, 255); - if (particles[i].sat < 255) + if (particles[i].sat < 255) { CHSV baseHSV = rgb2hsv_approximate(baseRGB); //convert to HSV baseHSV.s = particles[i].sat; //set the saturation baseRGB = (CRGB)baseHSV; // convert back to RGB } - } - renderParticle(framebuffer, i, brightness, baseRGB, renderbuffer); + } + renderParticle(framebuffer, i, brightness, baseRGB, renderbuffer, wrapX, wrapY); } if (particlesize > 0) { uint32_t passes = particlesize/64 + 1; // number of blur passes, four passes max - uint32_t bluramount = particlesize; + uint32_t bluramount = particlesize; uint32_t bitshift = 0; - + for(uint32_t i = 0; i < passes; i++) { if (i == 2) // for the last two passes, use higher amount of blur (results in a nicer brightness gradient with soft edges) bitshift = 1; - + if (useLocalBuffer) blur2D(framebuffer, maxXpixel + 1, maxYpixel + 1, bluramount << bitshift, bluramount << bitshift); else @@ -770,15 +704,14 @@ void ParticleSystem::ParticleSys_render(bool firemode, uint32_t fireintensity) SEGMENT.setPixelColorXY((int)x, (int)yflipped, framebuffer[x][y]); } } - free(framebuffer); + free(framebuffer); } if (renderbuffer) - free(renderbuffer); + free(renderbuffer); } // calculate pixel positions and brightness distribution and render the particle to local buffer or global buffer -void ParticleSystem::renderParticle(CRGB **framebuffer, uint32_t particleindex, uint32_t brightness, CRGB color, CRGB **renderbuffer) -{ +void ParticleSystem2D::renderParticle(CRGB **framebuffer, const uint32_t& particleindex, const uint32_t& brightness, const CRGB& color, CRGB **renderbuffer, const bool& wrapX, const bool& wrapY) { int32_t pxlbrightness[4] = {0}; // note: pxlbrightness needs to be set to 0 or checking does not work int32_t pixco[4][2]; // physical pixel coordinates of the four pixels a particle is rendered to. x,y pairs bool advancedrender = false; // rendering for advanced particles @@ -786,81 +719,72 @@ void ParticleSystem::renderParticle(CRGB **framebuffer, uint32_t particleindex, // subtract half a radius as the rendering algorithm always starts at the bottom left, this makes calculations more efficient int32_t xoffset = particles[particleindex].x - PS_P_HALFRADIUS; int32_t yoffset = particles[particleindex].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 dx = xoffset & (PS_P_RADIUS - 1); //relativ particle position in subpixel space + int32_t dy = yoffset & (PS_P_RADIUS - 1); // modulo replaced with bitwise AND, as radius is always a power of 2 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; // check if particle has advanced size properties and buffer is available - if (advPartProps) - { - if (advPartProps[particleindex].size > 0) - { - if (renderbuffer && framebuffer) - { + if (advPartProps && advPartProps[particleindex].size > 0) { + if (renderbuffer && framebuffer) { advancedrender = true; memset(renderbuffer[0], 0, 100 * sizeof(CRGB)); // clear the buffer, renderbuffer is 10x10 pixels } - else - return; // cannot render without buffers - } + else return; // cannot render without buffers } - // set the four raw pixel coordinates, the order is bottom left [0], bottom right[1], top right [2], top left [3] pixco[0][0] = pixco[3][0] = x; // bottom left & top left pixco[0][1] = pixco[1][1] = y; // bottom left & bottom right pixco[1][0] = pixco[2][0] = x + 1; // bottom right & top right pixco[2][1] = pixco[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 - { - 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) + // 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 + dx += PS_P_RADIUS; // 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) - { + if (dx == PS_P_RADIUS) { pxlbrightness[1] = pxlbrightness[2] = -1; // pixel is actually out of matrix boundaries, do not render dx = 2; // fix for advanced renderer (it does render slightly out of frame particles) } - if (particlesettings.wrapX) // wrap x to the other side if required + if (wrapX) { // wrap x to the other side if required pixco[0][0] = pixco[3][0] = maxXpixel; - else + } else { pxlbrightness[0] = pxlbrightness[3] = -1; // pixel is out of matrix boundaries, do not render + } } - else if (pixco[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 + else if (pixco[1][0] > maxXpixel) { // right pixels, only has to be checkt if left pixels did not overflow + if (wrapX) { // wrap y to the other side if required pixco[1][0] = pixco[2][0] = 0; - else + } else { pxlbrightness[1] = pxlbrightness[2] = -1; + } } - if (y < 0) // bottom pixels out of frame - { - dy = PS_P_RADIUS + dy; //see note above - if (dy == PS_P_RADIUS) - { + if (y < 0) { // bottom pixels out of frame + dy += PS_P_RADIUS; //see note above + if (dy == PS_P_RADIUS) { pxlbrightness[2] = pxlbrightness[3] = -1; // pixel is actually out of matrix boundaries, do not render dy = 2; // fix for advanced renderer (it does render slightly out of frame particles) } - if (particlesettings.wrapY) // wrap y to the other side if required + if (wrapY) { // wrap y to the other side if required pixco[0][1] = pixco[1][1] = maxYpixel; - else + } else { pxlbrightness[0] = pxlbrightness[1] = -1; + } } - else if (pixco[2][1] > maxYpixel) // top pixels - { - if (particlesettings.wrapY) // wrap y to the other side if required + else if (pixco[2][1] > maxYpixel) { // top pixels + if (wrapY) { // wrap y to the other side if required pixco[2][1] = pixco[3][1] = 0; - else + } else { pxlbrightness[2] = pxlbrightness[3] = -1; + } } - - if (advancedrender) // always render full particles in advanced rendering, undo out of frame marking (faster than checking each time in code above) - { - for(uint32_t i = 0; i < 4; i++) + + if (advancedrender) { // always render full particles in advanced rendering, undo out of frame marking (faster than checking each time in code above) + for(uint32_t i = 0; i < 4; i++) { pxlbrightness[i] = 0; + } } // calculate brightness values for all four pixels representing a particle using linear interpolation @@ -870,19 +794,14 @@ void ParticleSystem::renderParticle(CRGB **framebuffer, uint32_t particleindex, int32_t precal3 = dy * brightness; //calculate the values for pixels that are in frame - if (pxlbrightness[0] >= 0) - pxlbrightness[0] = (precal1 * precal2) >> PS_P_SURFACE; // bottom left value equal to ((PS_P_RADIUS - dx) * (PS_P_RADIUS-dy) * brightness) >> PS_P_SURFACE - if (pxlbrightness[1] >= 0) - pxlbrightness[1] = (dx * precal2) >> PS_P_SURFACE; // bottom right value equal to (dx * (PS_P_RADIUS-dy) * brightness) >> PS_P_SURFACE - if (pxlbrightness[2] >= 0) - pxlbrightness[2] = (dx * precal3) >> PS_P_SURFACE; // top right value equal to (dx * dy * brightness) >> PS_P_SURFACE - if (pxlbrightness[3] >= 0) - pxlbrightness[3] = (precal1 * precal3) >> PS_P_SURFACE; // top left value equal to ((PS_P_RADIUS-dx) * dy * brightness) >> PS_P_SURFACE - - if (advancedrender) - { + if (pxlbrightness[0] >= 0) pxlbrightness[0] = (precal1 * precal2) >> PS_P_SURFACE; // bottom left value equal to ((PS_P_RADIUS - dx) * (PS_P_RADIUS-dy) * brightness) >> PS_P_SURFACE + if (pxlbrightness[1] >= 0) pxlbrightness[1] = (dx * precal2) >> PS_P_SURFACE; // bottom right value equal to (dx * (PS_P_RADIUS-dy) * brightness) >> PS_P_SURFACE + if (pxlbrightness[2] >= 0) pxlbrightness[2] = (dx * precal3) >> PS_P_SURFACE; // top right value equal to (dx * dy * brightness) >> PS_P_SURFACE + if (pxlbrightness[3] >= 0) pxlbrightness[3] = (precal1 * precal3) >> PS_P_SURFACE; // top left value equal to ((PS_P_RADIUS-dx) * dy * brightness) >> PS_P_SURFACE + + if (advancedrender) { //render particle to a bigger size - //particle size to pixels: < 64 is 4x4, < 128 is 6x6, < 192 is 8x8, bigger is 10x10 + //particle size to pixels: < 64 is 4x4, < 128 is 6x6, < 192 is 8x8, bigger is 10x10 //first, render the pixel to the center of the renderbuffer, then apply 2D blurring fast_color_add(renderbuffer[4][4], color, pxlbrightness[0]); // order is: bottom left, bottom right, top right, top left fast_color_add(renderbuffer[5][4], color, pxlbrightness[1]); @@ -893,12 +812,10 @@ void ParticleSystem::renderParticle(CRGB **framebuffer, uint32_t particleindex, uint32_t maxsize = advPartProps[particleindex].size; uint32_t xsize = maxsize; uint32_t ysize = maxsize; - if (advPartSize) // use advanced size control - { + if (advPartSize) { // use advanced size control if (advPartSize[particleindex].asymmetry > 0) getParticleXYsize(&advPartProps[particleindex], &advPartSize[particleindex], xsize, ysize); - maxsize = xsize; - if (ysize > maxsize) maxsize = ysize; //maxsize is now the bigger of the two + maxsize = (xsize > ysize) ? xsize : ysize; // choose the bigger of the two } maxsize = maxsize/64 + 1; // number of blur passes depends on maxsize, four passes max uint32_t bitshift = 0; @@ -908,11 +825,11 @@ void ParticleSystem::renderParticle(CRGB **framebuffer, uint32_t particleindex, bitshift = 1; rendersize += 2; offset--; - blur2D(renderbuffer, rendersize, rendersize, xsize << bitshift, ysize << bitshift, true, offset, offset, true); - xsize = xsize > 64 ? xsize - 64 : 0; + blur2D(renderbuffer, rendersize, rendersize, xsize << bitshift, ysize << bitshift, offset, offset, true); + xsize = xsize > 64 ? xsize - 64 : 0; ysize = ysize > 64 ? ysize - 64 : 0; } - + // calculate origin coordinates to render the particle to in the framebuffer uint32_t xfb_orig = x - (rendersize>>1) + 1 - offset; uint32_t yfb_orig = y - (rendersize>>1) + 1 - offset; @@ -924,7 +841,7 @@ void ParticleSystem::renderParticle(CRGB **framebuffer, uint32_t particleindex, xfb = xfb_orig + xrb; if (xfb > maxXpixel) { - if (particlesettings.wrapX) // wrap x to the other side if required + if (wrapX) // wrap x to the other side if required xfb = xfb % (maxXpixel + 1); //TODO: this did not work in 1D system but appears to work in 2D (wrapped pixels were offset) under which conditions does this not work? else continue; @@ -935,15 +852,15 @@ void ParticleSystem::renderParticle(CRGB **framebuffer, uint32_t particleindex, yfb = yfb_orig + yrb; if (yfb > maxYpixel) { - if (particlesettings.wrapY) // wrap y to the other side if required + if (wrapY) // wrap y to the other side if required yfb = yfb % (maxYpixel + 1); else continue; } - fast_color_add(framebuffer[xfb][yfb], renderbuffer[xrb][yrb]); + fast_color_add(framebuffer[xfb][yfb], renderbuffer[xrb][yrb]); } } - } + } else // standard rendering { if (framebuffer) @@ -955,11 +872,11 @@ void ParticleSystem::renderParticle(CRGB **framebuffer, uint32_t particleindex, } } else - { + { for(uint32_t i = 0; i < 4; i++) { if (pxlbrightness[i] > 0) - SEGMENT.addPixelColorXY(pixco[i][0], maxYpixel - pixco[i][1], color.scale8((uint8_t)pxlbrightness[i])); + SEGMENT.addPixelColorXY(pixco[i][0], maxYpixel - pixco[i][1], color.scale8((uint8_t)pxlbrightness[i])); } } } @@ -989,7 +906,7 @@ void ParticleSystem::renderParticle(CRGB **framebuffer, uint32_t particleindex, { //Serial.print("^"); if (pxlbrightness[d] >= 0) - { + { Serial.print("uncought out of bounds: y:"); Serial.print(pixco[d][0]); Serial.print(" y:"); @@ -1008,7 +925,7 @@ void ParticleSystem::renderParticle(CRGB **framebuffer, uint32_t particleindex, // 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 ParticleSystem::fireParticleupdate() +void ParticleSystem2D::fireParticleupdate() { //TODO: cleanup this function? check if normal move is much slower, change move function to check y first then this function just needs to add ttl to y befor calling normal move function (this function uses 274bytes of flash) uint32_t i = 0; @@ -1021,7 +938,7 @@ void ParticleSystem::fireParticleupdate() particles[i].ttl--; // apply velocity particles[i].x = particles[i].x + (int32_t)particles[i].vx; - particles[i].y = particles[i].y + (int32_t)particles[i].vy + (particles[i].ttl >> 2); // younger particles move faster upward as they are hotter + particles[i].y = particles[i].y + (int32_t)particles[i].vy + (particles[i].ttl >> 2); // younger particles move faster upward as they are hotter //particles[i].y = particles[i].y + (int32_t)particles[i].vy;// + (particles[i].ttl >> 3); // younger particles move faster upward as they are hotter //this is experimental, different shifting particles[i].outofbounds = 0; // check if particle is out of bounds, wrap x around to other side if wrapping is enabled @@ -1037,7 +954,7 @@ void ParticleSystem::fireParticleupdate() { if (particlesettings.wrapX) { - particles[i].x = (uint16_t)particles[i].x % (maxX + 1); + particles[i].x = (uint16_t)particles[i].x % (maxX + 1); } else if ((particles[i].x < -PS_P_HALFRADIUS) || (particles[i].x > maxX + PS_P_HALFRADIUS)) //if fully out of view { @@ -1050,7 +967,7 @@ void ParticleSystem::fireParticleupdate() } // detect collisions in an array of particles and handle them -void ParticleSystem::handleCollisions() +void ParticleSystem2D::handleCollisions() { // detect and handle collisions uint32_t i, j; @@ -1058,11 +975,11 @@ void ParticleSystem::handleCollisions() uint32_t endparticle = usedParticles >> 1; // do half the particles, significantly speeds things up // 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 more accurate collisions are needed, just call it twice in a row - if (collisioncounter & 0x01) - { + if (collisioncounter & 0x01) + { startparticle = endparticle; endparticle = usedParticles; - } + } collisioncounter++; for (i = startparticle; i < endparticle; i++) @@ -1072,14 +989,14 @@ void ParticleSystem::handleCollisions() { int32_t dx, dy; // distance to other particles for (j = i + 1; j < usedParticles; j++) // check against higher number particles - { + { if (particles[j].ttl > 0 && particles[j].collide) // if target particle is alive { dx = particles[i].x - particles[j].x; if (advPartProps) //may be using individual particle size - { + { particleHardRadius = PS_P_MINHARDRADIUS + particlesize + (((uint32_t)advPartProps[i].size + (uint32_t)advPartProps[j].size)>>1); // collision distance - } + } if (dx < particleHardRadius && dx > -particleHardRadius) // check x direction, if close, check y direction { dy = particles[i].y - particles[j].y; @@ -1094,23 +1011,23 @@ void ParticleSystem::handleCollisions() // handle a collision if close proximity is detected, i.e. dx and/or dy smaller than 2*PS_P_RADIUS // takes two pointers to the particles to collide and the particle hardness (softer means more energy lost in collision, 255 means full hard) -void ParticleSystem::collideParticles(PSparticle *particle1, PSparticle *particle2) // TODO: dx,dy is calculated just above, can pass it over here to save a few CPU cycles? +void ParticleSystem2D::collideParticles(PSparticle *particle1, PSparticle *particle2) // TODO: dx,dy is calculated just above, can pass it over here to save a few CPU cycles? { int32_t dx = particle2->x - particle1->x; - int32_t dy = particle2->y - particle1->y; + int32_t dy = particle2->y - particle1->y; int32_t distanceSquared = dx * dx + dy * dy; // Calculate relative velocity (if it is zero, could exit but extra check does not overall speed but deminish it) int32_t relativeVx = (int16_t)particle2->vx - (int16_t)particle1->vx; int32_t relativeVy = (int16_t)particle2->vy - (int16_t)particle1->vy; // if dx and dy are zero (i.e. they meet at the center) give them an offset, if speeds are also zero, also offset them (pushes them apart if they are clumped before enabling collisions) - if (distanceSquared == 0) + if (distanceSquared == 0) { - // Adjust positions based on relative velocity direction - dx = -1; + // Adjust positions based on relative velocity direction + dx = -1; if (relativeVx < 0) // if true, particle2 is on the right side dx = 1; - else if (relativeVx == 0) + else if (relativeVx == 0) relativeVx = 1; dy = -1; @@ -1128,7 +1045,7 @@ void ParticleSystem::collideParticles(PSparticle *particle1, PSparticle *particl if (dotProduct < 0) // particles are moving towards each other { - // integer math used to avoid floats. + // integer math used to avoid floats. // overflow check: dx/dy are 7bit, relativV are 8bit -> dotproduct is 15bit, dotproduct/distsquared ist 8b, multiplied by collisionhardness of 8bit. so a 16bit shift is ok, make it 15 to be sure no overflows happen // note: cannot use right shifts as bit shifting in right direction is asymmetrical for positive and negative numbers and this needs to be accurate! the trick is: only shift positive numers // Calculate new velocities after collision @@ -1144,7 +1061,7 @@ void ParticleSystem::collideParticles(PSparticle *particle1, PSparticle *particl if (collisionHardness < surfacehardness) // if particles are soft, they become 'sticky' i.e. apply some friction (they do pile more nicely and stop sloshing around) { const uint32_t coeff = collisionHardness + (255 - PS_P_MINSURFACEHARDNESS); - particle1->vx = ((int32_t)particle1->vx * coeff) / 255; + particle1->vx = ((int32_t)particle1->vx * coeff) / 255; particle1->vy = ((int32_t)particle1->vy * coeff) / 255; particle2->vx = ((int32_t)particle2->vx * coeff) / 255; @@ -1159,18 +1076,18 @@ void ParticleSystem::collideParticles(PSparticle *particle1, PSparticle *particl particle2->vy = (particle2->vy < 3 && particle2->vy > -3) ? 0 : particle2->vy; }*/ } - + // particles have volume, push particles apart if they are too close // tried lots of configurations, it works best if not moved but given a little velocity, it tends to oscillate less this way // a problem with giving velocity is, that on harder collisions, this adds up as it is not dampened enough, so add friction in the FX if required if (dotProduct > -250) //this means particles are slow (or really really close) so push them apart. - { + { int32_t pushamount = 1 + ((250 + dotProduct) >> 6); // the closer dotproduct is to zero, the closer the particles are int32_t push = 0; if (dx < 0) // particle 1 is on the right - push = pushamount; + push = pushamount; else if (dx > 0) - push = -pushamount; + push = -pushamount; else // on the same x coordinate, shift it a little so they do not stack { if (notsorandom) @@ -1181,9 +1098,9 @@ void ParticleSystem::collideParticles(PSparticle *particle1, PSparticle *particl particle1->vx += push; //TODO: what happens if particle2 is also pushed? in 1D it stacks better, maybe also just reverse the comparison order so they flip roles? push = 0; if (dy < 0) - push = pushamount; + push = pushamount; else if (dy > 0) - push = -pushamount; + push = -pushamount; else // dy==0 { if (notsorandom) @@ -1197,7 +1114,7 @@ void ParticleSystem::collideParticles(PSparticle *particle1, PSparticle *particl { particle1->vx = 0; particle1->vy = 0; - particle2->vx = 0; + particle2->vx = 0; particle2->vy = 0; //push them apart particle1->x += push; @@ -1208,8 +1125,8 @@ void ParticleSystem::collideParticles(PSparticle *particle1, PSparticle *particl } // allocate memory for the 2D array in one contiguous block and set values to zero -CRGB **ParticleSystem::allocate2Dbuffer(uint32_t cols, uint32_t rows) -{ +CRGB **ParticleSystem2D::allocate2Dbuffer(uint32_t cols, uint32_t rows) +{ CRGB ** array2D = (CRGB **)calloc(cols, sizeof(CRGB *) + rows * sizeof(CRGB)); if (array2D == NULL) DEBUG_PRINT(F("PS buffer alloc failed")); @@ -1227,7 +1144,7 @@ CRGB **ParticleSystem::allocate2Dbuffer(uint32_t cols, uint32_t rows) // update size and pointers (memory location and size can change dynamically) // note: do not access the PS class in FX befor running this function (or it messes up SEGENV.data) -void ParticleSystem::updateSystem(void) +void ParticleSystem2D::updateSystem(void) { // update matrix size uint32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; @@ -1239,28 +1156,24 @@ void ParticleSystem::updateSystem(void) // set the pointers for the class (this only has to be done once and not on every FX call, only the class pointer needs to be reassigned to SEGENV.data every time) // function returns the pointer to the next byte available for the FX (if it assigned more memory for other stuff using the above allocate function) // FX handles the PSsources, need to tell this function how many there are -void ParticleSystem::updatePSpointers(bool isadvanced, bool sizecontrol) -{ +void ParticleSystem2D::updatePSpointers(bool isadvanced, bool sizecontrol) { // DEBUG_PRINT(F("*** PS pointers ***")); // DEBUG_PRINTF_P(PSTR("this PS %p "), this); // Note on memory alignment: // a pointer MUST be 4 byte aligned. sizeof() in a struct/class is always aligned to the largest element. if it contains a 32bit, it will be padded to 4 bytes, 16bit is padded to 2byte alignment. // The PS is aligned to 4 bytes, a PSparticle is aligned to 2 and a struct containing only byte sized variables is not aligned at all and may need to be padded when dividing the memoryblock. // by making sure that the number of sources and particles is a multiple of 4, padding can be skipped here as alignent is ensured, independent of struct sizes. - particles = reinterpret_cast(this + 1); // pointer to particle array at data+sizeof(ParticleSystem) + particles = reinterpret_cast(this + 1); // pointer to particle array at data+sizeof(ParticleSystem2D) sources = reinterpret_cast(particles + numParticles); // pointer to source(s) - if (isadvanced) - { + if (isadvanced) { advPartProps = reinterpret_cast(sources + numSources); PSdataEnd = reinterpret_cast(advPartProps + numParticles); - if (sizecontrol) - { + if (sizecontrol) { advPartSize = reinterpret_cast(advPartProps + numParticles); PSdataEnd = reinterpret_cast(advPartSize + numParticles); - } + } } - else - { + else { PSdataEnd = reinterpret_cast(sources + numSources); // pointer to first available byte after the PS for FX additional data } /* @@ -1273,29 +1186,21 @@ void ParticleSystem::updatePSpointers(bool isadvanced, bool sizecontrol) } // blur a matrix in x and y direction, blur can be asymmetric in x and y -// for speed, 32bit variables are used, make sure to limit them to 8bit (0-255) or result is undefined +// for speed, 32bit variables are used, make sure to limit them to 8bit (0-255) or result is undefined // to blur a subset of the buffer, change the xsize/ysize and set xstart/ystart to the desired starting coordinates (default start is 0/0) -void blur2D(CRGB **colorbuffer, uint32_t xsize, uint32_t ysize, uint32_t xblur, uint32_t yblur, bool smear, uint32_t xstart, uint32_t ystart, bool isparticle) -{ +void blur2D(CRGB **colorbuffer, uint32_t xsize, uint32_t ysize, uint32_t xblur, uint32_t yblur, uint32_t xstart, uint32_t ystart, bool isparticle) { CRGB seeppart, carryover; - uint32_t seep = xblur >> 1; - if (isparticle) //first and last row are always black in particle rendering - { + uint32_t seep = xblur >> 1; + if (isparticle) { //first and last row are always black in particle rendering ystart++; ysize--; } - for(uint32_t y = ystart; y < ystart + ysize; y++) - { + for(uint32_t y = ystart; y < ystart + ysize; y++) { carryover = BLACK; - for(uint32_t x = xstart; x < xstart + xsize; x++) - { + for(uint32_t x = xstart; x < xstart + xsize; x++) { seeppart = colorbuffer[x][y]; // create copy of current color fast_color_scale(seeppart, seep); // scale it and seep to neighbours - if (!smear) // fade current pixel if smear is disabled - fast_color_scale(colorbuffer[x][y], 255 - xblur); - - if (x > 0) - { + if (x > 0) { fast_color_add(colorbuffer[x-1][y], seeppart); fast_color_add(colorbuffer[x][y], carryover); // TODO: could check if carryover is > 0, takes 7 instructions, add takes ~35, with lots of same color pixels (like background), it would be faster } @@ -1304,25 +1209,18 @@ void blur2D(CRGB **colorbuffer, uint32_t xsize, uint32_t ysize, uint32_t xblur, fast_color_add(colorbuffer[xsize-1][y], carryover); // set last pixel } - if (isparticle) // now also do first and last row - { + if (isparticle) { // now also do first and last row ystart--; ysize++; } seep = yblur >> 1; - for(uint32_t x = xstart; x < xstart + xsize; x++) - { + for(uint32_t x = xstart; x < xstart + xsize; x++) { carryover = BLACK; - for(uint32_t y = ystart; y < ystart + ysize; y++) - { + for(uint32_t y = ystart; y < ystart + ysize; y++) { seeppart = colorbuffer[x][y]; // create copy of current color fast_color_scale(seeppart, seep); // scale it and seep to neighbours - if (!smear) // fade current pixel if smear is disabled - fast_color_scale(colorbuffer[x][y], 255 - yblur); - - if (y > 0) - { + if (y > 0) { fast_color_add(colorbuffer[x][y-1], seeppart); fast_color_add(colorbuffer[x][y], carryover); // todo: could check if carryover is > 0, takes 7 instructions, add takes ~35, with lots of same color pixels (like background), it would be faster } @@ -1352,7 +1250,7 @@ uint32_t calculateNumberOfParticles2D(bool isadvanced, bool sizecontrol) if (isadvanced) // advanced property array needs ram, reduce number of particles to use the same amount numberofParticles = (numberofParticles * sizeof(PSparticle)) / (sizeof(PSparticle) + sizeof(PSadvancedParticle)); if (sizecontrol) // advanced property array needs ram, reduce number of particles to use the same amount - numberofParticles /= 8; // if size control is used, much fewer particles are needed + numberofParticles /= 8; // if size control is used, much fewer particles are needed //make sure it is a multiple of 4 for proper memory alignment (easier than using padding bytes) numberofParticles = ((numberofParticles+3) >> 2) << 2; @@ -1381,7 +1279,7 @@ uint32_t calculateNumberOfSources2D(uint8_t requestedsources) //allocate memory for particle system class, particles, sprays plus additional memory requested by FX bool allocateParticleSystemMemory2D(uint16_t numparticles, uint16_t numsources, bool isadvanced, bool sizecontrol, uint16_t additionalbytes) { - uint32_t requiredmemory = sizeof(ParticleSystem); + uint32_t requiredmemory = sizeof(ParticleSystem2D); // functions above make sure these are a multiple of 4 bytes (to avoid alignment issues) requiredmemory += sizeof(PSparticle) * numparticles; if (isadvanced) @@ -1399,7 +1297,7 @@ bool allocateParticleSystemMemory2D(uint16_t numparticles, uint16_t numsources, } // initialize Particle System, allocate additional bytes if needed (pointer to those bytes can be read from particle system class: PSdataEnd) -bool initParticleSystem2D(ParticleSystem *&PartSys, uint8_t requestedsources, uint16_t additionalbytes, bool advanced, bool sizecontrol) +bool initParticleSystem2D(ParticleSystem2D *&PartSys, uint8_t requestedsources, uint16_t additionalbytes, bool advanced, bool sizecontrol) { //Serial.println("PS init function"); uint32_t numparticles = calculateNumberOfParticles2D(advanced, sizecontrol); @@ -1416,7 +1314,7 @@ bool initParticleSystem2D(ParticleSystem *&PartSys, uint8_t requestedsources, ui uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); //Serial.println("calling constructor"); - PartSys = new (SEGENV.data) ParticleSystem(cols, rows, numparticles, numsources, advanced, sizecontrol); // particle system constructor + PartSys = new (SEGENV.data) ParticleSystem2D(cols, rows, numparticles, numsources, advanced, sizecontrol); // particle system constructor //Serial.print("PS pointer at "); //Serial.println((uintptr_t)PartSys); return true; @@ -1430,7 +1328,7 @@ bool initParticleSystem2D(ParticleSystem *&PartSys, uint8_t requestedsources, ui //////////////////////// #ifndef WLED_DISABLE_PARTICLESYSTEM1D -ParticleSystem1D::ParticleSystem1D(uint16_t length, uint16_t numberofparticles, uint16_t numberofsources, bool isadvanced) +ParticleSystem1D::ParticleSystem1D(uint16_t length, uint16_t numberofparticles, uint16_t numberofsources, bool isadvanced) { //Serial.println("PS Constructor"); numSources = numberofsources; @@ -1440,7 +1338,7 @@ ParticleSystem1D::ParticleSystem1D(uint16_t length, uint16_t numberofparticles, //advPartSize = NULL; updatePSpointers(isadvanced); // set the particle and sources pointer (call this before accessing sprays or particles) setSize(length); - setWallHardness(255); // set default wall hardness to max + setWallHardness(255); // set default wall hardness to max setGravity(0); //gravity disabled by default setParticleSize(0); // minimum size by default motionBlur = 0; //no fading by default @@ -1448,16 +1346,16 @@ ParticleSystem1D::ParticleSystem1D(uint16_t length, uint16_t numberofparticles, //initialize some default non-zero values most FX use for (uint32_t i = 0; i < numSources; i++) - { + { sources[i].source.ttl = 1; //set source alive - } + } //Serial.println("PS Constructor done"); } // update function applies gravity, moves the particles, handles collisions and renders the particles void ParticleSystem1D::update(void) { - PSadvancedParticle1D *advprop = NULL; + PSadvancedParticle1D *advprop = NULL; // handle collisions (can push particles, must be done before updating particles or they can render out of bounds, causing a crash if using local buffer for speed) if (particlesettings.useCollisions) handleCollisions(); @@ -1490,8 +1388,8 @@ void ParticleSystem1D::update(void) if (bg_color > 0) //if not black { for(int32_t i = 0; i < maxXpixel + 1; i++) - { - SEGMENT.addPixelColor(i,bg_color); + { + SEGMENT.addPixelColor(i,bg_color); } } } @@ -1515,12 +1413,12 @@ void ParticleSystem1D::setSize(uint16_t x) void ParticleSystem1D::setWrap(bool enable) { - particlesettings.wrapX = enable; + particlesettings.wrap = enable; } void ParticleSystem1D::setBounce(bool enable) { - particlesettings.bounceX = enable; + particlesettings.bounce = enable; } void ParticleSystem1D::setKillOutOfBounds(bool enable) @@ -1542,29 +1440,29 @@ void ParticleSystem1D::setMotionBlur(uint8_t bluramount) { //TODO: currently normal blurring is not used in 1D system. should it be added? advanced rendering is quite fast and allows for motion blurring // if (particlesize < 2) // only allwo motion blurring on default particle size or advanced size(cannot combine motion blur with normal blurring used for particlesize, would require another buffer) - motionBlur = bluramount; + motionBlur = bluramount; } -// render size, 0 = 1 pixel, 1 = 2 pixel (interpolated), bigger sizes require adanced properties +// render size, 0 = 1 pixel, 1 = 2 pixel (interpolated), bigger sizes require adanced properties // note: if size is set larger than 1 without advanced properties, weird things may happen void ParticleSystem1D::setParticleSize(uint8_t size) { particlesize = size; particleHardRadius = PS_P_MINHARDRADIUS_1D >> 1; // 1 pixel sized particles have half the radius (for bounce, not for collisions) if (particlesize) - particleHardRadius = particleHardRadius << 1; // 2 pixel sized particles + particleHardRadius = particleHardRadius << 1; // 2 pixel sized particles } // enable/disable gravity, optionally, set the force (force=8 is default) can be -127 to +127, 0 is disable // if enabled, gravity is applied to all particles in ParticleSystemUpdate() // 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 ParticleSystem1D::setGravity(int8_t force) -{ +void ParticleSystem1D::setGravity(int8_t force) +{ if (force) { gforce = force; particlesettings.useGravity = true; } - else + else particlesettings.useGravity = false; } @@ -1584,11 +1482,11 @@ int32_t ParticleSystem1D::sprayEmit(PSsource1D &emitter) emitIndex = 0; if (particles[emitIndex].ttl == 0) // find a dead particle { - particles[emitIndex].vx = emitter.v + random(-emitter.var, emitter.var); - particles[emitIndex].x = emitter.source.x; - particles[emitIndex].hue = emitter.source.hue; + particles[emitIndex].vx = emitter.v + random(-emitter.var, emitter.var); + particles[emitIndex].x = emitter.source.x; + particles[emitIndex].hue = emitter.source.hue; particles[emitIndex].collide = emitter.source.collide; - particles[emitIndex].reversegrav = emitter.source.reversegrav; + particles[emitIndex].reversegrav = emitter.source.reversegrav; particles[emitIndex].ttl = random16(emitter.minLife, emitter.maxLife); if (advPartProps) { @@ -1600,7 +1498,7 @@ int32_t ParticleSystem1D::sprayEmit(PSsource1D &emitter) return emitIndex; } } - return -1; + return -1; } // particle moves, decays and dies, if killoutofbounds is set, out of bounds particles are set to ttl=0 @@ -1611,16 +1509,16 @@ void ParticleSystem1D::particleMoveUpdate(PSparticle1D &part, PSsettings1D *opti options = &particlesettings; //use PS system settings by default if (part.ttl > 0) { - if (!part.perpetual) + if (!part.perpetual) part.ttl--; // age if (particlesettings.colorByAge) - part.hue = part.ttl > 250 ? 250 : part.ttl; //set color to ttl - - bool usesize = false; // particle uses individual size rendering + part.hue = part.ttl > 250 ? 250 : part.ttl; //set color to ttl + + bool usesize = false; // particle uses individual size rendering int32_t newX = part.x + (int16_t)part.vx; part.outofbounds = 0; // reset out of bounds (in case particle was created outside the matrix and is now moving into view) if (advancedproperties) //using individual particle size? - { + { particleHardRadius = PS_P_MINHARDRADIUS_1D + (advancedproperties->size >> 1); if (advancedproperties->size > 1) { @@ -1631,25 +1529,25 @@ void ParticleSystem1D::particleMoveUpdate(PSparticle1D &part, PSsettings1D *opti } // if wall collisions are enabled, bounce them before they reach the edge, it looks much nicer if the particle is not half out of view - if (options->bounceX) + if (options->bounce) { if ((newX < particleHardRadius) || ((newX > maxX - particleHardRadius))) // reached a wall { bool bouncethis = true; if (options->useGravity) { - if (part.reversegrav) //skip at x = 0 + if (part.reversegrav) //skip at x = 0 { - if (newX < particleHardRadius) - bouncethis = false; + if (newX < particleHardRadius) + bouncethis = false; } - else //skip at x = max + else //skip at x = max { - if (newX > particleHardRadius) + if (newX > particleHardRadius) bouncethis = false; } } - + if (bouncethis) { part.vx = -part.vx; //invert speed @@ -1662,12 +1560,12 @@ void ParticleSystem1D::particleMoveUpdate(PSparticle1D &part, PSsettings1D *opti } } if ((newX < 0) || (newX > maxX)) // check if particle reached an edge (note: this also checks out of bounds and must not be skipped, even if bounce is enabled) - { - if (options->wrapX) - { - newX = newX % (maxX + 1); + { + if (options->wrap) + { + newX = newX % (maxX + 1); if (newX < 0) - newX += maxX + 1; + newX += maxX + 1; } else if (((newX <= -PS_P_HALFRADIUS_1D) || (newX > maxX + PS_P_HALFRADIUS_1D))) // particle is leaving, set out of bounds if it has fully left { @@ -1675,29 +1573,29 @@ void ParticleSystem1D::particleMoveUpdate(PSparticle1D &part, PSsettings1D *opti if (usesize) // using individual particle size { if (((newX > -particleHardRadius) && (newX < maxX + particleHardRadius))) // large particle is not yet leaving the view - note: this is not pixel perfect but good enough - isleaving = false; + isleaving = false; } if (isleaving) { part.outofbounds = 1; if (options->killoutofbounds) - { + { bool killthis = true; if (options->useGravity) //if gravity is used, only kill below 'floor level' { - if (part.reversegrav) //skip at x = 0 + if (part.reversegrav) //skip at x = 0 { - if (newX < 0) - killthis = false; + if (newX < 0) + killthis = false; } - else //skip at x = max + else //skip at x = max { - if (newX > 0) + if (newX > 0) killthis = false; } } if (killthis) - part.ttl = 0; + part.ttl = 0; } } } @@ -1731,7 +1629,7 @@ void ParticleSystem1D::applyForce(int8_t xforce) { tempcounter = forcecounter; applyForce(&particles[i], xforce, &tempcounter); - } + } forcecounter = tempcounter; //save value back } @@ -1746,7 +1644,7 @@ void ParticleSystem1D::applyGravity() if (particles[i].reversegrav) dv = -dv_raw; // 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].vx = limitSpeed((int32_t)particles[i].vx - dv); - } + } } // apply gravity to single particle using system settings (use this for sources) @@ -1755,8 +1653,8 @@ void ParticleSystem1D::applyGravity(PSparticle1D *part) { uint32_t counterbkp = gforcecounter; int32_t dv = calcForce_dv(gforce, &gforcecounter); - if (part->reversegrav) dv = -dv; - gforcecounter = counterbkp; //save it back + if (part->reversegrav) dv = -dv; + gforcecounter = counterbkp; //save it back part->vx = limitSpeed((int32_t)part->vx - dv); } @@ -1769,7 +1667,7 @@ void ParticleSystem1D::applyFriction(int32_t coefficient) for (uint32_t i = 0; i < usedParticles; i++) { if (particles[i].ttl) - particles[i].vx = ((int16_t)particles[i].vx * friction) / 255; + particles[i].vx = ((int16_t)particles[i].vx * friction) / 255; } } @@ -1779,16 +1677,16 @@ void ParticleSystem1D::applyFriction(int32_t coefficient) // warning: do not render out of bounds particles or system will crash! rendering does not check if particle is out of bounds void ParticleSystem1D::ParticleSys_render() { - + CRGB baseRGB; bool useLocalBuffer = true; //use local rendering buffer, gives huge speed boost (at least 30% more FPS) CRGB *framebuffer = NULL; //local frame buffer CRGB *renderbuffer = NULL; //local particle render buffer for advanced particles uint32_t i; uint32_t brightness; // particle brightness, fades if dying - + if (useLocalBuffer) - { + { // allocate empty memory for the local renderbuffer framebuffer = allocate1Dbuffer(maxXpixel + 1); @@ -1799,11 +1697,11 @@ void ParticleSystem1D::ParticleSys_render() } else{ if (advPartProps) - { + { renderbuffer = allocate1Dbuffer(10); //buffer to render individual particles to if size > 0. note: null checking is done when accessing it } if (motionBlur > 0) // using SEGMENT.fadeToBlackBy is much slower, this approximately doubles the speed of fade calculation - { + { for (uint32_t x = 0; x <= maxXpixel; x++) { framebuffer[x] = SEGMENT.getPixelColor(x); //copy to local buffer @@ -1812,15 +1710,16 @@ void ParticleSystem1D::ParticleSys_render() } } } - + if (!useLocalBuffer) //disabled or allocation above failed { //Serial.println("NOT using local buffer!"); if (motionBlur > 0) SEGMENT.fadeToBlackBy(255 - motionBlur); else - SEGMENT.fill(BLACK); //clear the buffer before rendering to it + SEGMENT.fill(BLACK); //clear the buffer before rendering to it } + bool wrap = particlesettings.wrap; // local copy for speed // go over particles and render them to the buffer for (i = 0; i < usedParticles; i++) { @@ -1830,17 +1729,17 @@ void ParticleSystem1D::ParticleSys_render() // 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 (advPartProps) //saturation is advanced property in 1D system { - if (advPartProps[i].sat < 255) + if (advPartProps[i].sat < 255) { CHSV baseHSV = rgb2hsv_approximate(baseRGB); //convert to HSV baseHSV.s = advPartProps[i].sat; //set the saturation baseRGB = (CRGB)baseHSV; // convert back to RGB } } - renderParticle(framebuffer, i, brightness, baseRGB, renderbuffer); + renderParticle(framebuffer, i, brightness, baseRGB, renderbuffer, wrap); } if (useLocalBuffer) // transfer local buffer back to segment @@ -1848,137 +1747,120 @@ void ParticleSystem1D::ParticleSys_render() for (int x = 0; x <= maxXpixel; x++) { SEGMENT.setPixelColor(x, framebuffer[x]); - } - free(framebuffer); + } + free(framebuffer); } if (renderbuffer) - free(renderbuffer); + free(renderbuffer); } // calculate pixel positions and brightness distribution and render the particle to local buffer or global buffer -void ParticleSystem1D::renderParticle(CRGB *framebuffer, uint32_t particleindex, uint32_t brightness, CRGB color, CRGB *renderbuffer) -{ +void ParticleSystem1D::renderParticle(CRGB *framebuffer, const uint32_t &particleindex, const uint32_t &brightness, const CRGB &color, CRGB *renderbuffer, const bool &wrap) { uint32_t size = particlesize; - if (advPartProps) // use advanced size properties - { + if (advPartProps) {// use advanced size properties size = advPartProps[particleindex].size; } if (size == 0) //single pixel particle, can be out of bounds as oob checking is made for 2-pixel particles { uint32_t x = particles[particleindex].x >> PS_P_RADIUS_SHIFT_1D; - if (x <= maxXpixel) //by making x unsigned there is no need to check < 0 as it will overflow - { - if (framebuffer) - fast_color_add(framebuffer[x], color, brightness); - else - SEGMENT.addPixelColor(x, color.scale8((uint8_t)brightness)); - } + if (x <= maxXpixel) { //by making x unsigned there is no need to check < 0 as it will overflow + if (framebuffer) + fast_color_add(framebuffer[x], color, brightness); + else + SEGMENT.addPixelColor(x, color.scale8((uint8_t)brightness)); + } } else { //render larger particles bool pxlisinframe[2] = {true, true}; - int32_t pxlbrightness[2]; - int32_t pixco[2]; // physical pixel coordinates of the two pixels representing a particle + int32_t pxlbrightness[2]; + int32_t pixco[2]; // physical pixel coordinates of the two pixels representing a particle // subtract half a radius as the rendering algorithm always starts at the left, this makes calculations more efficient int32_t xoffset = particles[particleindex].x - PS_P_HALFRADIUS_1D; int32_t dx = xoffset % PS_P_RADIUS_1D; //relativ particle position in subpixel space int32_t x = xoffset >> PS_P_RADIUS_SHIFT_1D; // divide by PS_P_RADIUS, bitshift of negative number stays negative -> checking below for x < 0 works (but does not when using division) - // set the raw pixel coordinates pixco[0] = x; // left pixel pixco[1] = x + 1; // right pixel - - // 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 - { - dx = PS_P_RADIUS_1D + dx; // if x<0, xoffset becomes negative (and so does dx), must adjust dx as modulo will flip its value + + // 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 + dx = PS_P_RADIUS_1D + dx; // if x<0, xoffset becomes negative (and so does dx), must adjust dx as modulo will flip its value // note: due to inverted shift math, a particel at position -16 (xoffset = -32, dx = 32) is rendered at the wrong pixel position (it should be out of frame) // checking this above would make this algorithm slower (in frame pixels do not have to be checked), so just correct for it here: - if (dx == PS_P_RADIUS_1D) - { + if (dx == PS_P_RADIUS_1D) { pxlisinframe[1] = false; // pixel is actually out of matrix boundaries, do not render dx = 0; // fix for out of frame advanced particles (dx=0 is changed to dx=PS_P_RADIUS_1D in above statement, 0 is correct) } - if (particlesettings.wrapX) // wrap x to the other side if required + if (wrap) // wrap x to the other side if required pixco[0] = maxXpixel; else pxlisinframe[0] = false; // pixel is out of matrix boundaries, do not render } - else if (pixco[1] > maxXpixel) // right pixel, only has to be checkt if left pixel did not overflow - { - if (particlesettings.wrapX) // wrap y to the other side if required + else if (pixco[1] > maxXpixel) { // right pixel, only has to be checkt if left pixel did not overflow + if (wrap) // wrap y to the other side if required pixco[1] = 0; else pxlisinframe[1] = false; } //calculate the brightness values for both pixels using linear interpolation (note: in standard rendering out of frame pixels could be skipped but if checks add more clock cycles over all) - pxlbrightness[0] = (((int32_t)PS_P_RADIUS_1D - dx) * brightness) >> PS_P_SURFACE_1D; - pxlbrightness[1] = (dx * brightness) >> PS_P_SURFACE_1D; + pxlbrightness[0] = (((int32_t)PS_P_RADIUS_1D - dx) * brightness) >> PS_P_SURFACE_1D; + pxlbrightness[1] = (dx * brightness) >> PS_P_SURFACE_1D; // check if particle has advanced size properties and buffer is available - if (advPartProps && advPartProps[particleindex].size > 1) - { - if (renderbuffer && framebuffer) - { + if (advPartProps && advPartProps[particleindex].size > 1) { + if (renderbuffer && framebuffer) { memset(renderbuffer, 0, 10 * sizeof(CRGB)); // clear the buffer, renderbuffer is 10 pixels } else return; // cannot render advanced particles without buffer - //render particle to a bigger size //particle size to pixels: 2 - 63 is 4 pixels, < 128 is 6pixels, < 192 is 8 pixels, bigger is 10 pixels //first, render the pixel to the center of the renderbuffer, then apply 1D blurring - fast_color_add(renderbuffer[4], color, pxlbrightness[0]); - fast_color_add(renderbuffer[5], color, pxlbrightness[1]); + fast_color_add(renderbuffer[4], color, pxlbrightness[0]); + fast_color_add(renderbuffer[5], color, pxlbrightness[1]); uint32_t rendersize = 2; // initialize render size, minimum is 4 pixels, it is incremented int he loop below to start with 4 uint32_t offset = 4; // offset to zero coordinate to write/read data in renderbuffer (actually needs to be 3, is decremented in the loop below) uint32_t blurpasses = size/64 + 1; // number of blur passes depends on size, four passes max uint32_t bitshift = 0; - for(int i = 0; i < blurpasses; i++) - { + for(int i = 0; i < blurpasses; i++) { if (i == 2) //for the last two passes, use higher amount of blur (results in a nicer brightness gradient with soft edges) bitshift = 1; rendersize += 2; offset--; - blur1D(renderbuffer, rendersize, size << bitshift, true, offset); - size = size > 64 ? size - 64 : 0; + blur1D(renderbuffer, rendersize, size << bitshift, offset); + size = size > 64 ? size - 64 : 0; } - + // calculate origin coordinates to render the particle to in the framebuffer uint32_t xfb_orig = x - (rendersize>>1) + 1 - offset; //note: using uint is fine uint32_t xfb; // coordinates in frame buffer to write to note: by making this uint, only overflow has to be checked // transfer particle renderbuffer to framebuffer - for(uint32_t xrb = offset; xrb < rendersize+offset; xrb++) - { + for(uint32_t xrb = offset; xrb < rendersize+offset; xrb++) { xfb = xfb_orig + xrb; - if (xfb > maxXpixel) - { - if (particlesettings.wrapX) // wrap x to the other side if required - { + if (xfb > maxXpixel) { + if (wrap) { // wrap x to the other side if required if (xfb > maxXpixel << 1) // xfb is "negative" (note: for some reason, this check is needed in 1D but works without in 2D...) xfb = (maxXpixel +1) + (int32_t)xfb; else - xfb = xfb % (maxXpixel + 1); + xfb = xfb % (maxXpixel + 1); } else continue; } - fast_color_add(framebuffer[xfb], renderbuffer[xrb]); + fast_color_add(framebuffer[xfb], renderbuffer[xrb]); } } - else // standard rendering (2 pixels per particle) - { - for(uint32_t i = 0; i < 2; i++) - { - if (pxlisinframe[i]) - { + else { // standard rendering (2 pixels per particle) + for(uint32_t i = 0; i < 2; i++) { + if (pxlisinframe[i]) { if (framebuffer) - fast_color_add(framebuffer[pixco[i]], color, pxlbrightness[i]); + fast_color_add(framebuffer[pixco[i]], color, pxlbrightness[i]); else - SEGMENT.addPixelColor(pixco[i], color.scale8((uint8_t)pxlbrightness[i])); + SEGMENT.addPixelColor(pixco[i], color.scale8((uint8_t)pxlbrightness[i])); } } } @@ -1995,33 +1877,33 @@ void ParticleSystem1D::handleCollisions() // 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 more accurate collisions are needed, just call it twice in a row /*if (SEGMENT.call & 0x01) //every other frame, do the other half - { + { startparticle = endparticle; endparticle = usedParticles; } */ int32_t collisiondistance = PS_P_MINHARDRADIUS_1D; - + for (i = startparticle; i < endparticle; i++) { // go though all 'higher number' particles and see if any of those are in close proximity and if they are, make them collide if (particles[i].ttl > 0 && particles[i].outofbounds == 0 && particles[i].collide) // if particle is alive and does collide and is not out of view { - int32_t dx; // distance to other particles + int32_t dx; // distance to other particles for (j = i + 1; j < usedParticles; j++) // check against higher number particles - { + { if (particles[j].ttl > 0 && particles[j].collide) // if target particle is alive { if (advPartProps) // use advanced size properties { collisiondistance = PS_P_MINHARDRADIUS_1D + ((uint32_t)advPartProps[i].size + (uint32_t)advPartProps[j].size)>>1; } - dx = particles[j].x - particles[i].x; - int32_t dv = (int32_t)particles[j].vx - (int32_t)particles[i].vx; + dx = particles[j].x - particles[i].x; + int32_t dv = (int32_t)particles[j].vx - (int32_t)particles[i].vx; int32_t proximity = collisiondistance; if (dv >= proximity) //particles would go past each other in next move upate proximity += abs(dv); //add speed difference to catch fast particles if (dx < proximity && dx > -proximity) // check if close - { + { collideParticles(&particles[i], &particles[j], dx, dv, collisiondistance); } } @@ -2032,24 +1914,24 @@ void ParticleSystem1D::handleCollisions() // handle a collision if close proximity is detected, i.e. dx and/or dy smaller than 2*PS_P_RADIUS // takes two pointers to the particles to collide and the particle hardness (softer means more energy lost in collision, 255 means full hard) -void ParticleSystem1D::collideParticles(PSparticle1D *particle1, PSparticle1D *particle2, int32_t dx, int32_t relativeVx, uint32_t collisiondistance) +void ParticleSystem1D::collideParticles(PSparticle1D *particle1, PSparticle1D *particle2, int32_t dx, int32_t relativeVx, uint32_t collisiondistance) { // Calculate dot product of relative velocity and relative distance int32_t dotProduct = (dx * relativeVx); // is always negative if moving towards each other // int32_t notsorandom = dotProduct & 0x01; //dotprouct LSB should be somewhat random, so no need to calculate a random number - //Serial.print(" dp"); Serial.print(dotProduct); + //Serial.print(" dp"); Serial.print(dotProduct); if (dotProduct < 0) // particles are moving towards each other { - // integer math used to avoid floats. + // integer math used to avoid floats. // Calculate new velocities after collision uint32_t surfacehardness = collisionHardness < PS_P_MINSURFACEHARDNESS_1D ? PS_P_MINSURFACEHARDNESS_1D : collisionHardness; // if particles are soft, the impulse must stay above a limit or collisions slip through //TODO: if soft collisions are not needed, the above line can be done in set hardness function and skipped here (which is what it currently looks like) - - int32_t impulse = relativeVx * surfacehardness / 255; - particle1->vx += impulse; + + int32_t impulse = relativeVx * surfacehardness / 255; + particle1->vx += impulse; particle2->vx -= impulse; - + //if one of the particles is fixed, transfer the impulse back so it bounces if (particle1->fixed) particle2->vx = -particle1->vx; @@ -2059,17 +1941,17 @@ void ParticleSystem1D::collideParticles(PSparticle1D *particle1, PSparticle1D *p if (collisionHardness < PS_P_MINSURFACEHARDNESS_1D) // if particles are soft, they become 'sticky' i.e. apply some friction (they do pile more nicely and correctly) { const uint32_t coeff = collisionHardness + (255 - PS_P_MINSURFACEHARDNESS_1D); - particle1->vx = ((int32_t)particle1->vx * coeff) / 255; + particle1->vx = ((int32_t)particle1->vx * coeff) / 255; particle2->vx = ((int32_t)particle2->vx * coeff) / 255; - } + } } - uint32_t distance = abs(dx); - // particles have volume, push particles apart if they are too close + uint32_t distance = abs(dx); + // particles have volume, push particles apart if they are too close // behaviour is different than in 2D, we need pixel accurate stacking here, push the top particle to full radius (direction is well defined in 1D) // also need to give the top particle some speed to counteract gravity or stacks just collapse if (distance < collisiondistance) //particles are too close, push the upper particle away - { + { int32_t pushamount = 1 + ((collisiondistance - distance) >> 1); //add half the remaining distance note: this works best, if less or more is added, it gets more chaotic //int32_t pushamount = collisiondistance - distance; if (particlesettings.useGravity) //using gravity, push the 'upper' particle only @@ -2082,10 +1964,10 @@ void ParticleSystem1D::collideParticles(PSparticle1D *particle1, PSparticle1D *p particle2->vx--; } else if (!particle1->reversegrav && !particle1->fixed) - { + { particle1->x += pushamount; particle1->vx++; - } + } } else { @@ -2109,7 +1991,7 @@ void ParticleSystem1D::collideParticles(PSparticle1D *particle1, PSparticle1D *p particle1->vx -= pushamount; particle2->vx += pushamount; - } + } } } @@ -2117,10 +1999,10 @@ void ParticleSystem1D::collideParticles(PSparticle1D *particle1, PSparticle1D *p // allocate memory for the 1D array in one contiguous block and set values to zero CRGB *ParticleSystem1D::allocate1Dbuffer(uint32_t length) -{ +{ CRGB *array = (CRGB *)calloc(length, sizeof(CRGB)); //if (array == NULL) - // DEBUG_PRINT(F("PS 1D buffer alloc failed")); + // DEBUG_PRINT(F("PS 1D buffer alloc failed")); return array; } @@ -2155,9 +2037,9 @@ void ParticleSystem1D::updatePSpointers(bool isadvanced) //{ // advPartSize = reinterpret_cast(advPartProps + numParticles); // PSdataEnd = reinterpret_cast(advPartSize + numParticles); - //} + //} } - + /* DEBUG_PRINTF_P(PSTR(" particles %p "), particles); DEBUG_PRINTF_P(PSTR(" sources %p "), sources); @@ -2173,11 +2055,11 @@ uint32_t calculateNumberOfParticles1D(bool isadvanced) { uint32_t numberofParticles = SEGMENT.virtualLength(); // one particle per pixel (if possible) #ifdef ESP8266 - uint32_t particlelimit = ESP8266_MAXPARTICLES_1D; // maximum number of paticles allowed + uint32_t particlelimit = ESP8266_MAXPARTICLES_1D; // maximum number of paticles allowed #elif ARDUINO_ARCH_ESP32S2 - uint32_t particlelimit = ESP32S2_MAXPARTICLES_1D; // maximum number of paticles allowed + uint32_t particlelimit = ESP32S2_MAXPARTICLES_1D; // maximum number of paticles allowed #else - uint32_t particlelimit = ESP32_MAXPARTICLES_1D; // maximum number of paticles allowed + uint32_t particlelimit = ESP32_MAXPARTICLES_1D; // maximum number of paticles allowed #endif numberofParticles = max((uint32_t)1, min(numberofParticles, particlelimit)); if (isadvanced) // advanced property array needs ram, reduce number of particles to use the same amount @@ -2188,12 +2070,12 @@ uint32_t calculateNumberOfParticles1D(bool isadvanced) } uint32_t calculateNumberOfSources1D(uint8_t requestedsources) -{ -#ifdef ESP8266 +{ +#ifdef ESP8266 int numberofSources = max(1, min((int)requestedsources,ESP8266_MAXSOURCES_1D)); // limit to 1 - 8 #elif ARDUINO_ARCH_ESP32S2 int numberofSources = max(1, min((int)requestedsources, ESP32S2_MAXSOURCES_1D)); // limit to 1 - 16 -#else +#else int numberofSources = max(1, min((int)requestedsources, ESP32_MAXSOURCES_1D)); // limit to 1 - 32 #endif // make sure it is a multiple of 4 for proper memory alignment @@ -2243,35 +2125,30 @@ bool initParticleSystem1D(ParticleSystem1D *&PartSys, uint32_t requestedsources, } -// blur a 1D buffer, sub-size blurring can be done using start and size -// for speed, 32bit variables are used, make sure to limit them to 8bit (0-255) or result is undefined -// to blur a subset of the buffer, change the size and set start to the desired starting coordinates (default start is 0/0) -void blur1D(CRGB *colorbuffer, uint32_t size, uint32_t blur, bool smear, uint32_t start) +// blur a 1D buffer, sub-size blurring can be done using start and size +// for speed, 32bit variables are used, make sure to limit them to 8bit (0-255) or result is undefined +// to blur a subset of the buffer, change the size and set start to the desired starting coordinates +void blur1D(CRGB *colorbuffer, uint32_t size, uint32_t blur, uint32_t start) { CRGB seeppart, carryover; - uint32_t seep = blur >> 1; - - carryover = BLACK; - for(uint32_t x = start; x < start + size; x++) - { - seeppart = colorbuffer[x]; // create copy of current color - fast_color_scale(seeppart, seep); // scale it and seep to neighbours - if (!smear) // fade current pixel if smear is disabled - fast_color_scale(colorbuffer[x], 255 - blur); - if (x > 0) - { - fast_color_add(colorbuffer[x-1], seeppart); - fast_color_add(colorbuffer[x], carryover); // is black on first pass - } - carryover = seeppart; + uint32_t seep = blur >> 1; + carryover = BLACK; + for(uint32_t x = start; x < start + size; x++) { + seeppart = colorbuffer[x]; // create copy of current color + fast_color_scale(seeppart, seep); // scale it and seep to neighbours + if (x > 0) { + fast_color_add(colorbuffer[x-1], seeppart); + fast_color_add(colorbuffer[x], carryover); // is black on first pass } - fast_color_add(colorbuffer[size-1], carryover); // set last pixel + carryover = seeppart; + } + fast_color_add(colorbuffer[size-1], carryover); // set last pixel } #endif // WLED_DISABLE_PARTICLESYSTEM1D -#if !defined(WLED_DISABLE_PARTICLESYSTEM2D) || !defined(WLED_DISABLE_PARTICLESYSTEM1D) +#if !defined(WLED_DISABLE_PARTICLESYSTEM2D) || !defined(WLED_DISABLE_PARTICLESYSTEM1D) ////////////////////////////// // Shared Utility Functions // @@ -2281,10 +2158,10 @@ void blur1D(CRGB *colorbuffer, uint32_t size, uint32_t blur, bool smear, uint32_ // force is in 3.4 fixedpoint notation, +/-127 int32_t calcForce_dv(int8_t force, uint8_t* counter) { - if (force == 0) + if (force == 0) return 0; // 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 force_abs = abs(force); // absolute value (faster than lots of if's only 7 instructions) int32_t dv = 0; // for small forces, need to use a delay counter, apply force only if it overflows if (force_abs < 16) @@ -2313,8 +2190,7 @@ int32_t limitSpeed(int32_t speed) // a better color add function is implemented in colors.cpp but it uses 32bit RGBW. to use it colors need to be shifted just to then be shifted back by that function, which is slow // this is a fast version for RGB (no white channel, PS does not handle white) and with native CRGB including scaling of second color (fastled scale8 can be made faster using native 32bit on ESP) // note: result is stored in c1, so c1 will contain the result. not using a return value is much faster as the struct does not need to be copied upon return -void fast_color_add(CRGB &c1, CRGB &c2, uint32_t scale) -{ +void fast_color_add(CRGB &c1, const CRGB &c2, uint32_t scale) { //note: function is manly used to add scaled colors, so checking if one color is black is slower uint32_t r, g, b; if (scale < 255) { @@ -2336,15 +2212,15 @@ void fast_color_add(CRGB &c1, CRGB &c2, uint32_t scale) c1.b = b; } else { - c1.r = (r * 255) / max; - c1.g = (g * 255) / max; - c1.b = (b * 255) / max; + uint32_t scale = (255 << 16) / max; // to avoid multiple divisions + c1.r = (r * scale) >> 16; // (c * 255) / max; + c1.g = (g * scale) >> 16; + c1.b = (b * scale) >> 16; } } // faster than fastled color scaling as it uses a 32bit scale factor and pointer -void fast_color_scale(CRGB &c, uint32_t scale) -{ +void fast_color_scale(CRGB &c, uint32_t scale) { c.r = ((c.r * scale) >> 8); c.g = ((c.g * scale) >> 8); c.b = ((c.b * scale) >> 8); diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index 83e6789c41..a14751bf33 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -8,20 +8,18 @@ Licensed under the EUPL v. 1.2 or later */ -#if !defined(WLED_DISABLE_PARTICLESYSTEM2D) || !defined(WLED_DISABLE_PARTICLESYSTEM1D) +#if !defined(WLED_DISABLE_PARTICLESYSTEM2D) || !defined(WLED_DISABLE_PARTICLESYSTEM1D) #include #include "wled.h" #define PS_P_MAXSPEED 120 // maximum speed a particle can have (vx/vy is int8) - //shared functions (used both in 1D and 2D system) -int32_t calcForce_dv(int8_t force, uint8_t *counter); //TODO: same as 2D function, could share -int32_t limitSpeed(int32_t speed); //TODO: same as 2D function, could share -void fast_color_add(CRGB &c1, CRGB &c2, uint32_t scale = 255); // fast and accurate color adding with scaling (scales c2 before adding) +int32_t calcForce_dv(int8_t force, uint8_t *counter); +int32_t limitSpeed(int32_t speed); +void fast_color_add(CRGB &c1, const CRGB &c2, uint32_t scale = 255); // fast and accurate color adding with scaling (scales c2 before adding) void fast_color_scale(CRGB &c, uint32_t scale); // fast scaling function using 32bit variable and pointer. note: keep 'scale' within 0-255 - #endif #ifndef WLED_DISABLE_PARTICLESYSTEM2D @@ -34,7 +32,7 @@ void fast_color_scale(CRGB &c, uint32_t scale); // fast scaling function using 3 #define ESP32_MAXSOURCES 64 // particle dimensions (subpixel division) -#define PS_P_RADIUS 64 // subpixel size, each pixel is divided by this for particle movement, if this value is changed, also change the shift defines (next two lines) +#define PS_P_RADIUS 64 // subpixel size, each pixel is divided by this for particle movement (must be a power of 2) #define PS_P_HALFRADIUS 32 #define PS_P_RADIUS_SHIFT 6 // shift for RADIUS #define PS_P_SURFACE 12 // shift: 2^PS_P_SURFACE = (PS_P_RADIUS)^2 @@ -42,11 +40,10 @@ void fast_color_scale(CRGB &c, uint32_t scale); // fast scaling function using 3 #define PS_P_MINSURFACEHARDNESS 128 // minimum hardness used in collision impulse calculation, below this hardness, particles become sticky // struct for PS settings (shared for 1D and 2D class) -typedef union -{ +typedef union { struct{ // one byte bit field for 2D settings - bool wrapX : 1; + bool wrapX : 1; bool wrapY : 1; bool bounceX : 1; bool bounceY : 1; @@ -61,7 +58,7 @@ typedef union //struct for a single particle (10 bytes) typedef struct { int16_t x; // x position in particle system - int16_t y; // y 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 @@ -75,21 +72,19 @@ typedef struct { } PSparticle; // struct for additional particle settings (optional) -typedef struct -{ +typedef struct { uint8_t size; // particle size, 255 means 10 pixels in diameter uint8_t forcecounter; // counter for applying forces to individual particles } PSadvancedParticle; -// struct for advanced particle size control (optional) -typedef struct -{ +// struct for advanced particle size control (optional) +typedef struct { uint8_t asymmetry; // asymmetrical size (0=symmetrical, 255 fully asymmetric) uint8_t asymdir; // direction of asymmetry, 64 is x, 192 is y (0 and 128 is symmetrical) - uint8_t maxsize; // target size for growing + uint8_t maxsize; // target size for growing uint8_t minsize; // target size for shrinking uint8_t sizecounter : 4; // counters used for size contol (grow/shrink/wobble) - uint8_t wobblecounter : 4; + uint8_t wobblecounter : 4; uint8_t growspeed : 4; uint8_t shrinkspeed : 4; uint8_t wobblespeed : 4; @@ -107,27 +102,23 @@ typedef struct { PSparticle source; // use a particle as the emitter source (speed, position, color) int8_t var; // variation of emitted speed (adds random(+/- var) to speed) int8_t vx; // emitting speed - int8_t vy; + int8_t vy; uint8_t size; // particle size (advanced property) -} PSsource; +} PSsource; // class uses approximately 60 bytes -class ParticleSystem -{ +class ParticleSystem2D { public: - ParticleSystem(uint16_t width, uint16_t height, uint16_t numberofparticles, uint16_t numberofsources, bool isadvanced = false, bool sizecontrol = false); // constructor + ParticleSystem2D(uint16_t width, uint16_t height, uint16_t numberofparticles, uint16_t numberofsources, bool isadvanced = false, bool sizecontrol = false); // constructor // note: memory is allcated in the FX function, no deconstructor needed void update(void); //update the particles according to set options and render to the matrix void updateFire(uint32_t intensity, bool renderonly = false); // update function for fire, if renderonly is set, particles are not updated (required to fix transitions with frameskips) void updateSystem(void); // call at the beginning of every FX, updates pointers and dimensions void particleMoveUpdate(PSparticle &part, PSsettings2D *options = NULL, PSadvancedParticle *advancedproperties = NULL); // move function - // particle emitters int32_t sprayEmit(PSsource &emitter, uint32_t amount = 1); void flameEmit(PSsource &emitter); int32_t angleEmit(PSsource& emitter, uint16_t angle, int8_t speed, uint32_t amount = 1); - - //particle physics void applyGravity(PSparticle *part); // applies gravity to single particle (use this for sources) void applyForce(PSparticle *part, int8_t xforce, int8_t yforce, uint8_t *counter); @@ -140,7 +131,6 @@ class ParticleSystem void applyFriction(int32_t coefficient); // apply friction to all used particles void pointAttractor(uint16_t particleindex, PSparticle *attractor, uint8_t strength, bool swallow); void lineAttractor(uint16_t particleindex, PSparticle *attractorcenter, uint16_t attractorangle, uint8_t strength); - // set options void setUsedParticles(uint32_t num); void setCollisionHardness(uint8_t hardness); // hardness for particle collisions (255 means full hard) @@ -154,11 +144,11 @@ class ParticleSystem void setKillOutOfBounds(bool enable); // if enabled, particles outside of matrix instantly die void setSaturation(uint8_t sat); // set global color saturation void setColorByAge(bool enable); - void setMotionBlur(uint8_t bluramount); // note: motion blur can only be used if 'particlesize' is set to zero + void setMotionBlur(uint8_t bluramount); // note: motion blur can only be used if 'particlesize' is set to zero void setParticleSize(uint8_t size); void setGravity(int8_t force = 8); void enableParticleCollisions(bool enable, uint8_t hardness = 255); - + PSparticle *particles; // pointer to particle array PSsource *sources; // pointer to sources PSadvancedParticle *advPartProps; // pointer to advanced particle properties (can be NULL) @@ -168,19 +158,17 @@ class ParticleSystem int32_t maxXpixel, maxYpixel; // last physical pixel that can be drawn to (FX can read this to read segment size if required), equal to width-1 / height-1 uint32_t numSources; // number of sources uint32_t numParticles; // number of particles available in this system - uint32_t usedParticles; // number of particles used in animation (can be smaller then numParticles) + uint32_t usedParticles; // number of particles used in animation (can be smaller then numParticles) -private: +private: //rendering functions void ParticleSys_render(bool firemode = false, uint32_t fireintensity = 128); - void renderParticle(CRGB **framebuffer, uint32_t particleindex, uint32_t brightness, CRGB color, CRGB **renderbuffer); - + void renderParticle(CRGB **framebuffer, const uint32_t &particleindex, const uint32_t &brightness, const CRGB &color, CRGB **renderbuffer, const bool &wrapX, const bool &wrapY); //paricle physics applied by system if flags are set void applyGravity(); // applies gravity to all particles void handleCollisions(); void collideParticles(PSparticle *particle1, PSparticle *particle2); void fireParticleupdate(); - //utility functions void updatePSpointers(bool isadvanced, bool sizecontrol); // update the data pointers to current segment data space void updateSize(PSadvancedParticle *advprops, PSsizeControl *advsize); // advanced size control @@ -188,7 +176,6 @@ class ParticleSystem void bounce(int8_t &incomingspeed, int8_t ¶llelspeed, int32_t &position, uint16_t maxposition); // bounce on a wall int16_t wraparound(uint16_t p, uint32_t maxvalue); CRGB **allocate2Dbuffer(uint32_t cols, uint32_t rows); - // note: variables that are accessed often are 32bit for speed PSsettings2D particlesettings; // settings used when updating particles (can also used by FX to move sources), do not edit properties directly, use functions above uint32_t emitIndex; // index to count through particles to emit so searching for dead pixels is faster @@ -205,13 +192,12 @@ class ParticleSystem uint8_t motionBlur; // enable motion blur, values > 100 gives smoother animations. Note: motion blurring does not work if particlesize is > 0 }; -void blur2D(CRGB **colorbuffer, uint32_t xsize, uint32_t ysize, uint32_t xblur, uint32_t yblur, bool smear = true, uint32_t xstart = 0, uint32_t ystart = 0, bool isparticle = false); +void blur2D(CRGB **colorbuffer, uint32_t xsize, uint32_t ysize, uint32_t xblur, uint32_t yblur, uint32_t xstart = 0, uint32_t ystart = 0, bool isparticle = false); // initialization functions (not part of class) -bool initParticleSystem2D(ParticleSystem *&PartSys, uint8_t requestedsources, uint16_t additionalbytes = 0, bool advanced = false, bool sizecontrol = false); +bool initParticleSystem2D(ParticleSystem2D *&PartSys, uint8_t requestedsources, uint16_t additionalbytes = 0, bool advanced = false, bool sizecontrol = false); uint32_t calculateNumberOfParticles2D(bool advanced, bool sizecontrol); uint32_t calculateNumberOfSources2D(uint8_t requestedsources); bool allocateParticleSystemMemory2D(uint16_t numparticles, uint16_t numsources, bool advanced, bool sizecontrol, uint16_t additionalbytes); - #endif // WLED_DISABLE_PARTICLESYSTEM2D //////////////////////// @@ -219,8 +205,7 @@ bool allocateParticleSystemMemory2D(uint16_t numparticles, uint16_t numsources, //////////////////////// #ifndef WLED_DISABLE_PARTICLESYSTEM1D // memory allocation -//MAX_SEGMENT_DATA -#define ESP8266_MAXPARTICLES_1D 400 +#define ESP8266_MAXPARTICLES_1D 400 #define ESP8266_MAXSOURCES_1D 8 #define ESP32S2_MAXPARTICLES_1D 1900 #define ESP32S2_MAXSOURCES_1D 16 @@ -232,16 +217,15 @@ bool allocateParticleSystemMemory2D(uint16_t numparticles, uint16_t numsources, #define PS_P_HALFRADIUS_1D 16 #define PS_P_RADIUS_SHIFT_1D 5 //TODO: may need to adjust #define PS_P_SURFACE_1D 5 // shift: 2^PS_P_SURFACE = PS_P_RADIUS_1D -#define PS_P_MINHARDRADIUS_1D 32 // minimum hard surface radius +#define PS_P_MINHARDRADIUS_1D 32 // minimum hard surface radius #define PS_P_MINSURFACEHARDNESS_1D 50 // minimum hardness used in collision impulse calculation // struct for PS settings (shared for 1D and 2D class) -typedef union -{ +typedef union { struct{ // one byte bit field for 1D settings - bool wrapX : 1; - bool bounceX : 1; + bool wrap : 1; + bool bounce : 1; bool killoutofbounds : 1; // if set, out of bound particles are killed immediately bool useGravity : 1; // set to 1 if gravity is used, disables bounceY at the top bool useCollisions : 1; @@ -255,7 +239,7 @@ typedef union //struct for a single particle (6 bytes) typedef struct { int16_t x; // x position in particle system - int8_t vx; // horizontal velocity + int8_t vx; // horizontal velocity uint8_t hue; // color hue // two byte bit field: uint16_t ttl : 11; // time to live, 11 bit or 2047 max (which is 25s at 80FPS) @@ -263,14 +247,13 @@ typedef struct { bool collide : 1; // if set, particle takes part in collisions bool perpetual : 1; // if set, particle does not age (TTL is not decremented in move function, it still dies from killoutofbounds) bool reversegrav : 1; // if set, gravity is reversed on this particle - bool fixed : 1; // if set, particle does not move (and collisions make other particles revert direction), + bool fixed : 1; // if set, particle does not move (and collisions make other particles revert direction), } PSparticle1D; // struct for additional particle settings (optional) -typedef struct -{ +typedef struct { uint8_t sat; //color saturation - uint8_t size; // particle size, 255 means 10 pixels in diameter + uint8_t size; // particle size, 255 means 10 pixels in diameter uint8_t forcecounter; } PSadvancedParticle1D; @@ -282,8 +265,8 @@ typedef struct { int8_t var; // variation of emitted speed (adds random(+/- var) to speed) int8_t v; // emitting speed uint8_t sat; // color saturation (advanced property) - uint8_t size; // particle size (advanced property) -} PSsource1D; + uint8_t size; // particle size (advanced property) +} PSsource1D; class ParticleSystem1D @@ -291,67 +274,65 @@ class ParticleSystem1D public: ParticleSystem1D(uint16_t length, uint16_t numberofparticles, uint16_t numberofsources, bool isadvanced = false); // constructor // note: memory is allcated in the FX function, no deconstructor needed - void update(void); //update the particles according to set options and render to the matrix + void update(void); //update the particles according to set options and render to the matrix void updateSystem(void); // call at the beginning of every FX, updates pointers and dimensions - // particle emitters - int32_t sprayEmit(PSsource1D &emitter); + int32_t sprayEmit(PSsource1D &emitter); void particleMoveUpdate(PSparticle1D &part, PSsettings1D *options = NULL, PSadvancedParticle1D *advancedproperties = NULL); // move function //particle physics void applyForce(PSparticle1D *part, int8_t xforce, uint8_t *counter); //apply a force to a single particle void applyForce(int8_t xforce); // apply a force to all particles - void applyGravity(PSparticle1D *part); // applies gravity to single particle (use this for sources) + void applyGravity(PSparticle1D *part); // applies gravity to single particle (use this for sources) void applyFriction(int32_t coefficient); // apply friction to all used particles - // set options - void setUsedParticles(uint32_t num); - void setWallHardness(uint8_t hardness); // hardness for bouncing on the wall if bounceXY is set + void setUsedParticles(uint32_t num); + void setWallHardness(uint8_t hardness); // hardness for bouncing on the wall if bounceXY is set void setSize(uint16_t x); //set particle system size (= strip length) void setWrap(bool enable); - void setBounce(bool enable); + void setBounce(bool enable); void setKillOutOfBounds(bool enable); // if enabled, particles outside of matrix instantly die // void setSaturation(uint8_t sat); // set global color saturation void setColorByAge(bool enable); void setColorByPosition(bool enable); - void setMotionBlur(uint8_t bluramount); // note: motion blur can only be used if 'particlesize' is set to zero + void setMotionBlur(uint8_t bluramount); // note: motion blur can only be used if 'particlesize' is set to zero void setParticleSize(uint8_t size); //size 0 = 1 pixel, size 1 = 2 pixels, is overruled by advanced particle size void setGravity(int8_t force = 8); void enableParticleCollisions(bool enable, uint8_t hardness = 255); - + PSparticle1D *particles; // pointer to particle array PSsource1D *sources; // pointer to sources PSadvancedParticle1D *advPartProps; // pointer to advanced particle properties (can be NULL) //PSsizeControl *advPartSize; // pointer to advanced particle size control (can be NULL) uint8_t* PSdataEnd; // points to first available byte after the PSmemory, is set in setPointers(). use this for FX custom data - int32_t maxX; // particle system size i.e. width-1 - int32_t maxXpixel; // last physical pixel that can be drawn to (FX can read this to read segment size if required), equal to width-1 + int32_t maxX; // particle system size i.e. width-1 + int32_t maxXpixel; // last physical pixel that can be drawn to (FX can read this to read segment size if required), equal to width-1 uint32_t numSources; // number of sources uint32_t numParticles; // number of particles available in this system - uint32_t usedParticles; // number of particles used in animation (can be smaller then numParticles) + uint32_t usedParticles; // number of particles used in animation (can be smaller then numParticles) -private: +private: //rendering functions void ParticleSys_render(void); - void renderParticle(CRGB *framebuffer, uint32_t particleindex, uint32_t brightness, CRGB color, CRGB *renderbuffer); + void renderParticle(CRGB *framebuffer, const uint32_t &particleindex, const uint32_t &brightness, const CRGB &color, CRGB *renderbuffer, const bool &wrap); //paricle physics applied by system if flags are set void applyGravity(); // applies gravity to all particles void handleCollisions(); - void collideParticles(PSparticle1D *particle1, PSparticle1D *particle2, int32_t dx, int32_t relativeV, uint32_t collisiondistance); + void collideParticles(PSparticle1D *particle1, PSparticle1D *particle2, int32_t dx, int32_t relativeV, uint32_t collisiondistance); //utility functions void updatePSpointers(bool isadvanced); // update the data pointers to current segment data space //void updateSize(PSadvancedParticle *advprops, PSsizeControl *advsize); // advanced size control //void getParticleXYsize(PSadvancedParticle *advprops, PSsizeControl *advsize, uint32_t &xsize, uint32_t &ysize); void bounce(int8_t &incomingspeed, int8_t ¶llelspeed, int32_t &position, uint16_t maxposition); // bounce on a wall - CRGB *allocate1Dbuffer(uint32_t length); + CRGB *allocate1Dbuffer(uint32_t length); // note: variables that are accessed often are 32bit for speed - PSsettings1D particlesettings; // settings used when updating particles + PSsettings1D particlesettings; // settings used when updating particles uint32_t emitIndex; // index to count through particles to emit so searching for dead pixels is faster int32_t collisionHardness; - uint32_t wallHardness; + uint32_t wallHardness; uint8_t gforcecounter; // counter for global gravity - int8_t gforce; // gravity strength, default is 8 (negative is allowed, positive is downwards) + int8_t gforce; // gravity strength, default is 8 (negative is allowed, positive is downwards) uint8_t forcecounter; // counter for globally applied forces //uint8_t collisioncounter; // counter to handle collisions TODO: could use the SEGMENT.call? -> currently unused // global particle properties for basic particles @@ -364,5 +345,5 @@ bool initParticleSystem1D(ParticleSystem1D *&PartSys, uint32_t requestedsources, uint32_t calculateNumberOfParticles1D(bool isadvanced); uint32_t calculateNumberOfSources1D(uint8_t requestedsources); bool allocateParticleSystemMemory1D(uint16_t numparticles, uint16_t numsources, bool isadvanced, uint16_t additionalbytes); -void blur1D(CRGB *colorbuffer, uint32_t size, uint32_t blur, bool smear = true, uint32_t start = 0); +void blur1D(CRGB *colorbuffer, uint32_t size, uint32_t blur, uint32_t start = 0); #endif // WLED_DISABLE_PARTICLESYSTEM1D From 2e371ce20f107ee2f80abe398e06742ff585f2dc Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sat, 26 Oct 2024 17:06:34 +0200 Subject: [PATCH 132/219] Optimization and bugfixes - added out of bounds checking function - fixed rendering at boundaries - various small changes and cleanup - bugfixes with comparing ints of different signeness - fixed bounce radius --- wled00/FX.cpp | 122 +----------- wled00/FXparticleSystem.cpp | 384 +++++++++++++----------------------- wled00/FXparticleSystem.h | 25 +-- 3 files changed, 158 insertions(+), 373 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 115f7d2820..dc208c49a4 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -8427,7 +8427,7 @@ uint16_t mode_particlewaterfall(void) PartSys->setBounceX(SEGMENT.check2); // walls PartSys->setBounceY(SEGMENT.check3); // ground PartSys->setWallHardness(SEGMENT.custom2); - numSprays = min((int32_t)PartSys->numSources, max(PartSys->maxXpixel / 6, (int32_t)2)); // number of sprays depends on segment width + numSprays = min(PartSys->numSources, max(PartSys->maxXpixel / 6, (uint32_t)2)); // number of sprays depends on segment width if (SEGMENT.custom2 > 0) // collisions enabled PartSys->enableParticleCollisions(true, SEGMENT.custom2); // enable collisions and set particle collision hardness else @@ -8886,122 +8886,6 @@ static const char _data_FX_MODE_PARTICLEATTRACTOR[] PROGMEM = "PS Attractor@Mass static const char _data_FX_MODE_PARTICLEATTRACTOR[] PROGMEM = "PS Attractor@Mass,Particles,Particle Size,Collisions,Friction,Color by Age,Move,Swallow;;!;2;pal=9,sx=100,ix=82,c1=0,c2=0,o1=0,o2=0,o3=0"; #endif - -/* -Particle Line Attractor, an idea that is not finished and not working -Uses palette for particle color -by DedeHai (Damian Schneider) -*/ -/* -uint16_t mode_particleattractor(void) -{ - if (SEGLEN == 1) - return mode_static(); - ParticleSystem2D *PartSys = NULL; - uint32_t i = 0; - PSparticle *attractor; // particle pointer to the attractor - uint8_t *counters; // counters for the applied force - PSsettings sourcesettings = {0, 0, 1, 1, 0, 0, 0, 0}; // PS settings for bounceY, bounceY used for source movement (it always bounces whereas particles do not) - PSsettings sourcesettings; - uint8_t *settingsPtr = reinterpret_cast(&sourcesettings); // access settings as one byte (wmore efficient in code and speed) - *settingsPtr = 0b00001100; // PS settings for bounceY, bounceY used for source movement (it always bounces whereas particles do not) - - if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. - { - if (!initParticleSystem2D(PartSys, 1, true)) // init, need one source. use advanced particles (with individual forces) - return mode_static(); // allocation failed; //allocation failed - - PartSys->sources[0].source.hue = random16(); - PartSys->sources[0].source.x = PS_P_RADIUS; // start out in bottom left corner - PartSys->sources[0].source.y = PS_P_RADIUS << 1; - PartSys->sources[0].source.vx = random16(5) + 3; - PartSys->sources[0].source.vy = PartSys->sources[0].source.vx - 2; // move slower in y - PartSys->sources[0].source.collide = true; // seeded particles will collide - PartSys->sources[0].source.ttl = 100; // is replenished below, it never dies -#ifdef ESP8266 - PartSys->sources[0].maxLife = 200; // lifetime in frames (ESP8266 has less particles) - PartSys->sources[0].minLife = 30; -#else - PartSys->sources[0].maxLife = 350; // lifetime in frames - PartSys->sources[0].minLife = 50; -#endif - PartSys->sources[0].vx = 0; // emitting speed - PartSys->sources[0].vy = 0; // emitting speed - PartSys->sources[0].var = 4; // emiting variation - } - else - PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS - - if (PartSys == NULL) - return mode_static(); // something went wrong, no data! - - // Particle System settings - PartSys->updateSystem(); // update system properties (dimensions and data pointers) - PartSys->setWallHardness(230); // walls are always same hardness - PartSys->setColorByAge(SEGMENT.check1); - - if (SEGMENT.custom2 > 0) // collisions enabled - PartSys->enableParticleCollisions(true, SEGMENT.custom2); // enable collisions and set particle collision hardness - else - PartSys->enableParticleCollisions(false); - - uint16_t lastusedparticle = (PartSys->numParticles * 2) / 3; //only use 2/3 of the available particles to keep things fast - uint32_t displayparticles = map(SEGMENT.intensity, 0, 255, 10, lastusedparticle); - PartSys->setUsedParticles(displayparticles); - - // set pointers - attractor = reinterpret_cast(&PartSys->particles[lastusedparticle + 1]); - // set attractor properties - if (SEGMENT.check2) // move attractor - { - attractor->vx = PartSys->sources[0].source.vy; // set to spray movemement but reverse x and y - attractor->vy = PartSys->sources[0].source.vx; - PartSys->particleMoveUpdate(*attractor, sourcesettings); // move the attractor - } - else - { - attractor->x = PartSys->maxX >> 1; // center - attractor->y = PartSys->maxY >> 1; - } - - if (SEGMENT.call % 5 == 0) - { - PartSys->sources[0].source.hue++; - PartSys->sources[0].source.ttl = 100; // spray never dies - } - - SEGENV.aux0 += 256; // emitting angle, one full turn in 255 frames (0xFFFF is 360°) - if (SEGMENT.call % 2 == 0) // alternate direction of emit - PartSys->angleEmit(PartSys->sources[0], SEGENV.aux0, SEGMENT.custom1 >> 4); - else - PartSys->angleEmit(PartSys->sources[0], SEGENV.aux0 + 0x7FFF, SEGMENT.custom1 >> 4); // emit at 180° as well - - SEGENV.aux1 = 0;//++; //line attractor angle - // apply force - if(SEGMENT.call % 2 == 0) - for (i = 0; i < displayparticles; i++) - { - //PartSys->lineAttractor(&PartSys->particles[i], attractor, SEGENV.aux1, &counters[i], SEGMENT.speed); //TODO: upate this to advanced particles!!! - } - if (SEGMENT.call % (33 - SEGMENT.custom3) == 0) - PartSys->applyFriction(2); - - PartSys->particleMoveUpdate(PartSys->sources[0].source, sourcesettings); // move the source - Serial.print("vx:"); - Serial.print(attractor->vx); - Serial.print("vy:"); - Serial.print(attractor->vy); - Serial.print("x:"); - Serial.print(attractor->x); - Serial.print("y:"); - Serial.println(attractor->y); - PartSys->update(); // update and render - return FRAMETIME; -} -static const char _data_FX_MODE_PARTICLEATTRACTOR[] PROGMEM = "PS Attractor@Mass,Particles,Emit Speed,Collisions,Friction,Color by Age,Move,Swallow;;!;2;pal=9,sx=100,ix=82,c1=190,c2=0,o1=0,o2=0,o3=0"; -*/ - - /* Particle Spray, just a particle spray with many parameters Uses palette for particle color @@ -10425,7 +10309,7 @@ uint16_t mode_particleChase(void) if (SEGLEN == 1) return mode_static(); ParticleSystem1D *PartSys = NULL; - int32_t i; + uint32_t i; if (SEGMENT.call == 0) // initialization { @@ -10457,7 +10341,7 @@ uint16_t mode_particleChase(void) uint32_t settingssum = SEGMENT.speed + SEGMENT.intensity + SEGMENT.custom1 + SEGMENT.custom2 + SEGMENT.check1 + SEGMENT.check2 + SEGMENT.check3; if(SEGENV.aux0 != settingssum) //settings changed changed, update { - PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 1, min(PartSys->maxX / (32 + (SEGMENT.custom1 >> 1)), (int32_t)(PartSys->numParticles)))); //depends on intensity and particle size (custom1) + PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 1, min(PartSys->maxX / (32 + (SEGMENT.custom1 >> 1)), (PartSys->numParticles)))); //depends on intensity and particle size (custom1) SEGENV.step = (PartSys->maxX + (PS_P_RADIUS_1D << 4)) / PartSys->usedParticles; //spacing between particles // uint32_t remainder = PartSys->maxX - ((PartSys->usedParticles) * SEGENV.step); // unused spacing, distribute this for(i = 0; i < PartSys->usedParticles; i++) diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index 55551f2236..1e46edee43 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -12,18 +12,16 @@ TODO: -add function to 'update sources' so FX does not have to take care of that. FX can still implement its own version if so desired. -add an x/y struct, do particle rendering using that, much easier to read + -add underscore to private variables */ - #if !defined(WLED_DISABLE_PARTICLESYSTEM2D) || !defined(WLED_DISABLE_PARTICLESYSTEM1D) #include "FXparticleSystem.h" #endif #ifndef WLED_DISABLE_PARTICLESYSTEM2D -ParticleSystem2D::ParticleSystem2D(uint16_t width, uint16_t height, uint16_t numberofparticles, uint16_t numberofsources, bool isadvanced, bool sizecontrol) -{ - //Serial.println("PS Constructor"); +ParticleSystem2D::ParticleSystem2D(uint16_t width, uint16_t height, uint16_t numberofparticles, uint16_t numberofsources, bool isadvanced, bool sizecontrol) { numSources = numberofsources; numParticles = numberofparticles; // set number of particles in the array usedParticles = numberofparticles; // use all particles by default @@ -39,16 +37,13 @@ ParticleSystem2D::ParticleSystem2D(uint16_t width, uint16_t height, uint16_t num emitIndex = 0; //initialize some default non-zero values most FX use - for (uint32_t i = 0; i < numSources; i++) - { + for (uint32_t i = 0; i < numSources; i++) { sources[i].source.sat = 255; //set saturation to max by default sources[i].source.ttl = 1; //set source alive } - for (uint32_t i = 0; i < numParticles; i++) - { + for (uint32_t i = 0; i < numParticles; i++) { particles[i].sat = 255; // full saturation } - //Serial.println("PS Constructor done"); } // update function applies gravity, moves the particles, handles collisions and renders the particles @@ -60,10 +55,8 @@ void ParticleSystem2D::update(void) applyGravity(); //update size settings before handling collisions - if (advPartSize) - { - for (uint32_t i = 0; i < usedParticles; i++) - { + if (advPartSize) { + for (uint32_t i = 0; i < usedParticles; i++) { updateSize(&advPartProps[i], &advPartSize[i]); } } @@ -82,16 +75,6 @@ void ParticleSystem2D::update(void) particleMoveUpdate(particles[i], &particlesettings, advprop); } - /*TODO remove this - Serial.print("alive particles: "); - uint32_t aliveparticles = 0; - for (int i = 0; i < numParticles; i++) - { - if (particles[i].ttl) - aliveparticles++; - } - Serial.println(aliveparticles); - */ ParticleSys_render(); } @@ -171,7 +154,7 @@ void ParticleSystem2D::setMotionBlur(uint8_t bluramount) void ParticleSystem2D::setParticleSize(uint8_t size) { particlesize = size; - particleHardRadius = PS_P_MINHARDRADIUS + particlesize; // note: this sets size if not using advanced props + particleHardRadius = PS_P_MINHARDRADIUS + (particlesize >> 1); // radius used for wall collisions & particle collisions motionBlur = 0; // disable motion blur if particle size is set } // enable/disable gravity, optionally, set the force (force=8 is default) can be -127 to +127, 0 is disable @@ -229,113 +212,67 @@ int32_t ParticleSystem2D::sprayEmit(PSsource &emitter, uint32_t amount) } // Spray emitter for particles used for flames (particle TTL depends on source TTL) -void ParticleSystem2D::flameEmit(PSsource &emitter) -{ +void ParticleSystem2D::flameEmit(PSsource &emitter) { int emitIndex = sprayEmit(emitter); if(emitIndex > 0) particles[emitIndex].ttl += emitter.source.ttl; } // Emits a particle at given angle and speed, angle is from 0-65535 (=0-360deg), speed is also affected by emitter->var // angle = 0 means in positive x-direction (i.e. to the right) -int32_t ParticleSystem2D::angleEmit(PSsource &emitter, uint16_t angle, int8_t speed, uint32_t amount) -{ +int32_t ParticleSystem2D::angleEmit(PSsource &emitter, uint16_t angle, int8_t speed, uint32_t amount) { emitter.vx = ((int32_t)cos16(angle) * (int32_t)speed) / (int32_t)32600; // cos16() and sin16() return signed 16bit, division should be 32767 but 32600 gives slightly better rounding emitter.vy = ((int32_t)sin16(angle) * (int32_t)speed) / (int32_t)32600; // note: cannot use bit shifts as bit shifting is asymmetrical for positive and negative numbers and this needs to be accurate! return sprayEmit(emitter, amount); } // particle moves, decays and dies, if killoutofbounds is set, out of bounds particles are set to ttl=0 -// uses passed settings to set bounce or wrap, if useGravity is set, it will never bounce at the top and killoutofbounds is not applied over the top -void ParticleSystem2D::particleMoveUpdate(PSparticle &part, PSsettings2D *options, PSadvancedParticle *advancedproperties) -{ +// uses passed settings to set bounce or wrap, if useGravity is enabled, it will never bounce at the top and killoutofbounds is not applied over the top +void ParticleSystem2D::particleMoveUpdate(PSparticle &part, PSsettings2D *options, PSadvancedParticle *advancedproperties) { if (options == NULL) options = &particlesettings; //use PS system settings by default - if (part.ttl > 0) - { + + if (part.ttl > 0) { if (!part.perpetual) part.ttl--; // age - if (particlesettings.colorByAge) + if (options->colorByAge) part.hue = part.ttl > 255 ? 255 : part.ttl; //set color to ttl - bool usesize = false; // particle uses individual size rendering - int32_t newX = part.x + (int16_t)part.vx; - int32_t 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) + int32_t renderradius = PS_P_HALFRADIUS; // used to check out of bounds + int32_t newX = part.x + (int32_t)part.vx; + int32_t newY = part.y + (int32_t)part.vy; + part.outofbounds = false; // reset out of bounds (in case particle was created outside the matrix and is now moving into view) - if (advancedproperties) //using individual particle size? - { - if (advancedproperties->size > 0) - usesize = true; // note: variable eases out of frame checking below - particleHardRadius = max(PS_P_MINHARDRADIUS, (int)particlesize + (advancedproperties->size)); + if (advancedproperties) { //using individual particle size? + if (advancedproperties->size > 0) { + particleHardRadius = max(PS_P_MINHARDRADIUS, (int)particlesize + (advancedproperties->size)); // update radius + renderradius = particleHardRadius; + } } // if wall collisions are enabled, bounce them before they reach the edge, it looks much nicer if the particle is not half out of view - if (options->bounceX) - { - if ((newX < particleHardRadius) || (newX > maxX - particleHardRadius)) // reached a wall + if (options->bounceX) { + if ((newX < (int32_t)particleHardRadius) || (newX > (int32_t)(maxX - particleHardRadius))) // reached a wall bounce(part.vx, part.vy, newX, maxX); } - if ((newX < 0) || (newX > maxX)) // check if particle reached an edge (note: this also checks out of bounds and must not be skipped, even if bounce is enabled) - { - if (options->wrapX) - { - newX = newX % (maxX + 1); - if (newX < 0) - newX += maxX + 1; - } - else if (((newX <= -PS_P_HALFRADIUS) || (newX > maxX + PS_P_HALFRADIUS))) // particle is leaving, set out of bounds if it has fully left - { - bool isleaving = true; - if (usesize) // using individual particle size - { - if (((newX > -particleHardRadius) && (newX < maxX + particleHardRadius))) // large particle is not yet leaving the view - note: this is not pixel perfect but good enough - isleaving = false; - } - - if (isleaving) - { - part.outofbounds = 1; - if (options->killoutofbounds) - part.ttl = 0; - } - } + if(!checkBoundsAndWrap(newX, maxX, renderradius, options->wrapX)) { // check out of bounds note: this must not be skipped or it can lead to crashes TODO: is this still true? + part.outofbounds = true; + if (options->killoutofbounds) + part.ttl = 0; } - if (options->bounceY) - { - if ((newY < particleHardRadius) || ((newY > maxY - particleHardRadius) && !options->useGravity)) // reached floor / ceiling - { + if (options->bounceY) { + if ((newY < (int32_t)particleHardRadius) || ((newY > (int32_t)(maxY - particleHardRadius)) && !options->useGravity)) { // reached floor / ceiling bounce(part.vy, part.vx, newY, maxY); } } - if (((newY < 0) || (newY > maxY))) // check if particle reached an edge (makes sure particles are within frame for rendering) - { - if (options->wrapY) - { - newY = newY % (maxY + 1); - if (newY < 0) - newY += maxY + 1; - } - else if (((newY <= -PS_P_HALFRADIUS) || (newY > maxY + PS_P_HALFRADIUS))) // particle is leaving, set out of bounds if it has fully left - { - bool isleaving = true; - if (usesize) // using individual particle size - { - if (((newY > -particleHardRadius) && (newY < maxY + particleHardRadius))) // still withing rendering reach - isleaving = false; - } - if (isleaving) - { - part.outofbounds = 1; - if (options->killoutofbounds) - { - if (newY < 0) // if gravity is enabled, only kill particles below ground - part.ttl = 0; - else if (!options->useGravity) - part.ttl = 0; - } - } + if(!checkBoundsAndWrap(newY, maxY, renderradius, options->wrapY)) { // check out of bounds note: this must not be skipped or it can lead to crashes TODO: is this still true? + part.outofbounds = true; + if (options->killoutofbounds) { + if (newY < 0) // if gravity is enabled, only kill particles below ground + part.ttl = 0; + else if (!options->useGravity) + part.ttl = 0; } } part.x = (int16_t)newX; // set new position @@ -601,8 +538,6 @@ void ParticleSystem2D::ParticleSys_render(bool firemode, uint32_t fireintensity) uint32_t i; uint32_t brightness; // particle brightness, fades if dying - if (useLocalBuffer) - { /* //memory fragmentation check: Serial.print("heap: "); @@ -611,6 +546,8 @@ void ParticleSystem2D::ParticleSys_render(bool firemode, uint32_t fireintensity) Serial.println(heap_caps_get_largest_free_block(MALLOC_CAP_8BIT)); */ + if (useLocalBuffer) + { // allocate empty memory for the local renderbuffer framebuffer = allocate2Dbuffer(maxXpixel + 1, maxYpixel + 1); if (framebuffer == NULL) @@ -619,10 +556,6 @@ void ParticleSystem2D::ParticleSys_render(bool firemode, uint32_t fireintensity) useLocalBuffer = false; //render to segment pixels directly if not enough memory } else{ - if (advPartProps) - { - renderbuffer = allocate2Dbuffer(10, 10); //buffer to render individual particles to if size > 0. note: null checking is done when accessing it - } if (motionBlur > 0) // using SEGMENT.fadeToBlackBy is much slower, this approximately doubles the speed of fade calculation { uint32_t yflipped; @@ -638,9 +571,11 @@ void ParticleSystem2D::ParticleSys_render(bool firemode, uint32_t fireintensity) } } } + if (advPartProps) { + renderbuffer = allocate2Dbuffer(10, 10); //buffer to render individual particles to if size > 0. note: null checking is done when accessing it + } if (!useLocalBuffer) { //disabled or allocation above failed - //Serial.println("NOT using local buffer!"); if (motionBlur > 0) SEGMENT.fadeToBlackBy(255 - motionBlur); else @@ -687,8 +622,9 @@ void ParticleSystem2D::ParticleSys_render(bool firemode, uint32_t fireintensity) if (useLocalBuffer) blur2D(framebuffer, maxXpixel + 1, maxYpixel + 1, bluramount << bitshift, bluramount << bitshift); - else + else{ SEGMENT.blur(bluramount << bitshift, true); + } bluramount -= 64; } } @@ -711,42 +647,36 @@ void ParticleSystem2D::ParticleSys_render(bool firemode, uint32_t fireintensity) } // calculate pixel positions and brightness distribution and render the particle to local buffer or global buffer -void ParticleSystem2D::renderParticle(CRGB **framebuffer, const uint32_t& particleindex, const uint32_t& brightness, const CRGB& color, CRGB **renderbuffer, const bool& wrapX, const bool& wrapY) { +void ParticleSystem2D::renderParticle(CRGB **framebuffer, const uint32_t particleindex, const uint32_t brightness, const CRGB& color, CRGB **renderbuffer, const bool wrapX, const bool wrapY) { int32_t pxlbrightness[4] = {0}; // note: pxlbrightness needs to be set to 0 or checking does not work int32_t pixco[4][2]; // physical pixel coordinates of the four pixels a particle is rendered to. x,y pairs bool advancedrender = false; // rendering for advanced particles - - // subtract half a radius as the rendering algorithm always starts at the bottom left, this makes calculations more efficient - int32_t xoffset = particles[particleindex].x - PS_P_HALFRADIUS; - int32_t yoffset = particles[particleindex].y - PS_P_HALFRADIUS; - int32_t dx = xoffset & (PS_P_RADIUS - 1); //relativ particle position in subpixel space - int32_t dy = yoffset & (PS_P_RADIUS - 1); // modulo replaced with bitwise AND, as radius is always a power of 2 - 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; - // check if particle has advanced size properties and buffer is available if (advPartProps && advPartProps[particleindex].size > 0) { - if (renderbuffer && framebuffer) { + if (renderbuffer) { advancedrender = true; memset(renderbuffer[0], 0, 100 * sizeof(CRGB)); // clear the buffer, renderbuffer is 10x10 pixels } else return; // cannot render without buffers } + // add half a radius as the rendering algorithm always starts at the bottom left, this leaves things positive, so shifts can be used, then shift coordinate by a full pixel (x--/y-- below) + int32_t xoffset = particles[particleindex].x + PS_P_HALFRADIUS; + int32_t yoffset = particles[particleindex].y + PS_P_HALFRADIUS; + int32_t dx = xoffset & (PS_P_RADIUS - 1); // relativ particle position in subpixel space + int32_t dy = yoffset & (PS_P_RADIUS - 1); // modulo replaced with bitwise AND, as radius is always a power of 2 + int32_t x = (xoffset >> PS_P_RADIUS_SHIFT); // divide by PS_P_RADIUS which is 64, so can bitshift (compiler can not optimize integer) + 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] + pixco[1][0] = pixco[2][0] = x; // bottom right & top right + pixco[2][1] = pixco[3][1] = y; // top right & top left + x--; // shift by a full pixel here, this is skipped above to not do -1 and then +1 + y--; pixco[0][0] = pixco[3][0] = x; // bottom left & top left pixco[0][1] = pixco[1][1] = y; // bottom left & bottom right - pixco[1][0] = pixco[2][0] = x + 1; // bottom right & top right - pixco[2][1] = pixco[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 - dx += PS_P_RADIUS; // 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) { - pxlbrightness[1] = pxlbrightness[2] = -1; // pixel is actually out of matrix boundaries, do not render - dx = 2; // fix for advanced renderer (it does render slightly out of frame particles) - } if (wrapX) { // wrap x to the other side if required pixco[0][0] = pixco[3][0] = maxXpixel; } else { @@ -762,11 +692,6 @@ void ParticleSystem2D::renderParticle(CRGB **framebuffer, const uint32_t& partic } if (y < 0) { // bottom pixels out of frame - dy += PS_P_RADIUS; //see note above - if (dy == PS_P_RADIUS) { - pxlbrightness[2] = pxlbrightness[3] = -1; // pixel is actually out of matrix boundaries, do not render - dy = 2; // fix for advanced renderer (it does render slightly out of frame particles) - } if (wrapY) { // wrap y to the other side if required pixco[0][1] = pixco[1][1] = maxYpixel; } else { @@ -857,7 +782,10 @@ void ParticleSystem2D::renderParticle(CRGB **framebuffer, const uint32_t& partic else continue; } - fast_color_add(framebuffer[xfb][yfb], renderbuffer[xrb][yrb]); + if (framebuffer) + fast_color_add(framebuffer[xfb][yfb], renderbuffer[xrb][yrb]); + else + SEGMENT.addPixelColorXY(xfb, maxYpixel - yfb, renderbuffer[xrb][yrb]); } } } @@ -967,40 +895,32 @@ void ParticleSystem2D::fireParticleupdate() } // detect collisions in an array of particles and handle them -void ParticleSystem2D::handleCollisions() -{ +void ParticleSystem2D::handleCollisions() { // detect and handle collisions uint32_t i, j; uint32_t startparticle = 0; uint32_t endparticle = usedParticles >> 1; // do half the particles, significantly speeds things up + int32_t collisiondistance = particleHardRadius << 1; // 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 more accurate collisions are needed, just call it twice in a row - if (collisioncounter & 0x01) - { + if (collisioncounter & 0x01) { startparticle = endparticle; 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].outofbounds == 0 && particles[i].collide) // if particle is alive and does collide and is not out of view - { + for (i = startparticle; i < endparticle; i++) { // go though all 'higher number' particles and see if any of those are in close proximity and if they are, make them collide + if (particles[i].ttl > 0 && particles[i].outofbounds == 0 && particles[i].collide) { // if particle is alive and does collide and is not out of view int32_t dx, dy; // distance to other particles - for (j = i + 1; j < usedParticles; j++) // check against higher number particles - { - if (particles[j].ttl > 0 && particles[j].collide) // if target particle is alive - { + for (j = i + 1; j < usedParticles; j++) { // check against higher number particles + if (particles[j].ttl > 0 && particles[j].collide) { // if target particle is alive dx = particles[i].x - particles[j].x; + if (advPartProps) //may be using individual particle size - { - particleHardRadius = PS_P_MINHARDRADIUS + particlesize + (((uint32_t)advPartProps[i].size + (uint32_t)advPartProps[j].size)>>1); // collision distance - } - if (dx < particleHardRadius && dx > -particleHardRadius) // check x direction, if close, check y direction - { + collisiondistance = PS_P_MINHARDRADIUS + particlesize + (((uint32_t)advPartProps[i].size + (uint32_t)advPartProps[j].size)>>1); // collision distance + + if (dx < collisiondistance && dx > -collisiondistance) { // check x direction, if close, check y direction dy = particles[i].y - particles[j].y; - if (dy < particleHardRadius && dy > -particleHardRadius) // particles are close + if (dy < collisiondistance && dy > -collisiondistance) // particles are close collideParticles(&particles[i], &particles[j]); } } @@ -1246,7 +1166,7 @@ uint32_t calculateNumberOfParticles2D(bool isadvanced, bool sizecontrol) uint32_t numberofParticles = (cols * rows); // 1 particle per pixel (for example 512 particles on 32x16) uint32_t particlelimit = ESP32_MAXPARTICLES; // maximum number of paticles allowed (based on two segments of 32x32 and 40k effect ram) #endif - numberofParticles = max((uint32_t)1, min(numberofParticles, particlelimit)); + numberofParticles = max((uint32_t)4, min(numberofParticles, particlelimit)); if (isadvanced) // advanced property array needs ram, reduce number of particles to use the same amount numberofParticles = (numberofParticles * sizeof(PSparticle)) / (sizeof(PSparticle) + sizeof(PSadvancedParticle)); if (sizecontrol) // advanced property array needs ram, reduce number of particles to use the same amount @@ -1288,6 +1208,7 @@ bool allocateParticleSystemMemory2D(uint16_t numparticles, uint16_t numsources, requiredmemory += sizeof(PSsizeControl) * numparticles; requiredmemory += sizeof(PSsource) * numsources; requiredmemory += additionalbytes; + requiredmemory += 64; //!!! just a test, remove this line looking for crash reasons //Serial.print("allocating: "); //Serial.print(requiredmemory); //Serial.println("Bytes"); @@ -1447,10 +1368,11 @@ void ParticleSystem1D::setMotionBlur(uint8_t bluramount) // note: if size is set larger than 1 without advanced properties, weird things may happen void ParticleSystem1D::setParticleSize(uint8_t size) { - particlesize = size; - particleHardRadius = PS_P_MINHARDRADIUS_1D >> 1; // 1 pixel sized particles have half the radius (for bounce, not for collisions) + particlesize = size; // TODO: add support for global sizes? if (particlesize) - particleHardRadius = particleHardRadius << 1; // 2 pixel sized particles + particleHardRadius = PS_P_MINHARDRADIUS_1D; // 2 pixel sized particles + else + particleHardRadius = PS_P_MINHARDRADIUS_1D >> 1; // 1 pixel sized particles have half the radius (for bounce, not for collisions) } // enable/disable gravity, optionally, set the force (force=8 is default) can be -127 to +127, 0 is disable // if enabled, gravity is applied to all particles in ParticleSystemUpdate() @@ -1507,99 +1429,69 @@ void ParticleSystem1D::particleMoveUpdate(PSparticle1D &part, PSsettings1D *opti { if (options == NULL) options = &particlesettings; //use PS system settings by default - if (part.ttl > 0) - { + + if (part.ttl > 0) { if (!part.perpetual) part.ttl--; // age - if (particlesettings.colorByAge) - part.hue = part.ttl > 250 ? 250 : part.ttl; //set color to ttl + if (options->colorByAge) + part.hue = part.ttl > 255 ? 255 : part.ttl; //set color to ttl - bool usesize = false; // particle uses individual size rendering - int32_t newX = part.x + (int16_t)part.vx; - part.outofbounds = 0; // reset out of bounds (in case particle was created outside the matrix and is now moving into view) - if (advancedproperties) //using individual particle size? - { - particleHardRadius = PS_P_MINHARDRADIUS_1D + (advancedproperties->size >> 1); + int32_t renderradius = PS_P_HALFRADIUS_1D; // used to check out of bounds, default for 2 pixel rendering + int32_t newX = part.x + (int32_t)part.vx; + part.outofbounds = false; // reset out of bounds (in case particle was created outside the matrix and is now moving into view) + + if (advancedproperties) { //using individual particle size? if (advancedproperties->size > 1) - { - usesize = true; // note: variable eases out of frame checking below - } - else if (advancedproperties->size == 0) // single pixel particles use half the collision distance for walls + particleHardRadius = PS_P_MINHARDRADIUS_1D + (advancedproperties->size >> 1); //TODO: this may need optimization, radius and diameter is still a mess in 1D system. + else // single pixel particles use half the collision distance for walls particleHardRadius = PS_P_MINHARDRADIUS_1D >> 1; + renderradius = particleHardRadius; // note: for single pixel particles, it should be zero, but it does not matter as out of bounds checking is done in rendering function } // if wall collisions are enabled, bounce them before they reach the edge, it looks much nicer if the particle is not half out of view - if (options->bounce) - { - if ((newX < particleHardRadius) || ((newX > maxX - particleHardRadius))) // reached a wall - { + if (options->bounce) { + if ((newX < (int32_t)particleHardRadius) || ((newX > (int32_t)(maxX - particleHardRadius)))) { // reached a wall bool bouncethis = true; - if (options->useGravity) - { - if (part.reversegrav) //skip at x = 0 - { + if (options->useGravity) { + if (part.reversegrav) { // skip bouncing at x = 0 if (newX < particleHardRadius) bouncethis = false; } - else //skip at x = max - { - if (newX > particleHardRadius) + else if (newX > particleHardRadius){ //skip bouncing at x = max bouncethis = false; } } - - if (bouncethis) - { + if (bouncethis) { part.vx = -part.vx; //invert speed part.vx = ((int32_t)part.vx * wallHardness) / 255; // reduce speed as energy is lost on non-hard surface - if (newX < particleHardRadius) + if (newX < (int32_t)particleHardRadius) newX = particleHardRadius; // fast particles will never reach the edge if position is inverted, this looks better else newX = maxX - particleHardRadius; } } } - if ((newX < 0) || (newX > maxX)) // check if particle reached an edge (note: this also checks out of bounds and must not be skipped, even if bounce is enabled) - { - if (options->wrap) - { - newX = newX % (maxX + 1); - if (newX < 0) - newX += maxX + 1; - } - else if (((newX <= -PS_P_HALFRADIUS_1D) || (newX > maxX + PS_P_HALFRADIUS_1D))) // particle is leaving, set out of bounds if it has fully left + + if(!checkBoundsAndWrap(newX, maxX, renderradius, options->wrap)) { // check out of bounds note: this must not be skipped or it can lead to crashes TODO: is this still true? + part.outofbounds = true; + if (options->killoutofbounds) { - bool isleaving = true; - if (usesize) // using individual particle size - { - if (((newX > -particleHardRadius) && (newX < maxX + particleHardRadius))) // large particle is not yet leaving the view - note: this is not pixel perfect but good enough - isleaving = false; + bool killthis = true; + if (options->useGravity) { //if gravity is used, only kill below 'floor level' + if (part.reversegrav) { //skip at x = 0 + if (newX < 0) + killthis = false; } - if (isleaving) - { - part.outofbounds = 1; - if (options->killoutofbounds) - { - bool killthis = true; - if (options->useGravity) //if gravity is used, only kill below 'floor level' - { - if (part.reversegrav) //skip at x = 0 - { - if (newX < 0) - killthis = false; - } - else //skip at x = max - { - if (newX > 0) - killthis = false; - } - } - if (killthis) - part.ttl = 0; - } + else { //skip at x = max + if (newX > 0) + killthis = false; } + } + if (killthis) + part.ttl = 0; } } + if (!part.fixed) part.x = (int16_t)newX; // set new position else @@ -1755,13 +1647,12 @@ void ParticleSystem1D::ParticleSys_render() } // calculate pixel positions and brightness distribution and render the particle to local buffer or global buffer -void ParticleSystem1D::renderParticle(CRGB *framebuffer, const uint32_t &particleindex, const uint32_t &brightness, const CRGB &color, CRGB *renderbuffer, const bool &wrap) { +void ParticleSystem1D::renderParticle(CRGB *framebuffer, const uint32_t particleindex, const uint32_t brightness, const CRGB &color, CRGB *renderbuffer, const bool wrap) { uint32_t size = particlesize; if (advPartProps) {// use advanced size properties size = advPartProps[particleindex].size; } - if (size == 0) //single pixel particle, can be out of bounds as oob checking is made for 2-pixel particles - { + if (size == 0) { //single pixel particle, can be out of bounds as oob checking is made for 2-pixel particles uint32_t x = particles[particleindex].x >> PS_P_RADIUS_SHIFT_1D; if (x <= maxXpixel) { //by making x unsigned there is no need to check < 0 as it will overflow if (framebuffer) @@ -1774,24 +1665,19 @@ void ParticleSystem1D::renderParticle(CRGB *framebuffer, const uint32_t &particl bool pxlisinframe[2] = {true, true}; int32_t pxlbrightness[2]; int32_t pixco[2]; // physical pixel coordinates of the two pixels representing a particle - // subtract half a radius as the rendering algorithm always starts at the left, this makes calculations more efficient - int32_t xoffset = particles[particleindex].x - PS_P_HALFRADIUS_1D; - int32_t dx = xoffset % PS_P_RADIUS_1D; //relativ particle position in subpixel space + + // add half a radius as the rendering algorithm always starts at the bottom left, this leaves things positive, so shifts can be used, then shift coordinate by a full pixel (x-- below) + int32_t xoffset = particles[particleindex].x + PS_P_HALFRADIUS_1D; + int32_t dx = xoffset & (PS_P_RADIUS_1D - 1); //relativ particle position in subpixel space, modulo replaced with bitwise AND int32_t x = xoffset >> PS_P_RADIUS_SHIFT_1D; // divide by PS_P_RADIUS, bitshift of negative number stays negative -> checking below for x < 0 works (but does not when using division) // set the raw pixel coordinates - pixco[0] = x; // left pixel - pixco[1] = x + 1; // right pixel + pixco[1] = x; // right pixel + x--; // shift by a full pixel here, this is skipped above to not do -1 and then +1 + pixco[0] = x; // left pixel // 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 - dx = PS_P_RADIUS_1D + dx; // if x<0, xoffset becomes negative (and so does dx), must adjust dx as modulo will flip its value - // note: due to inverted shift math, a particel at position -16 (xoffset = -32, dx = 32) is rendered at the wrong pixel position (it should be out of frame) - // checking this above would make this algorithm slower (in frame pixels do not have to be checked), so just correct for it here: - if (dx == PS_P_RADIUS_1D) { - pxlisinframe[1] = false; // pixel is actually out of matrix boundaries, do not render - dx = 0; // fix for out of frame advanced particles (dx=0 is changed to dx=PS_P_RADIUS_1D in above statement, 0 is correct) - } if (wrap) // wrap x to the other side if required pixco[0] = maxXpixel; else @@ -1810,7 +1696,7 @@ void ParticleSystem1D::renderParticle(CRGB *framebuffer, const uint32_t &particl // check if particle has advanced size properties and buffer is available if (advPartProps && advPartProps[particleindex].size > 1) { - if (renderbuffer && framebuffer) { + if (renderbuffer && framebuffer) { // TODO: add unbuffered large size rendering like in 2D system memset(renderbuffer, 0, 10 * sizeof(CRGB)); // clear the buffer, renderbuffer is 10 pixels } else @@ -1851,7 +1737,7 @@ void ParticleSystem1D::renderParticle(CRGB *framebuffer, const uint32_t &particl else continue; } - fast_color_add(framebuffer[xfb], renderbuffer[xrb]); + fast_color_add(framebuffer[xfb], renderbuffer[xrb]); // TODO: add unbuffered large size rendering like in 2D system } } else { // standard rendering (2 pixels per particle) @@ -1895,7 +1781,7 @@ void ParticleSystem1D::handleCollisions() { if (advPartProps) // use advanced size properties { - collisiondistance = PS_P_MINHARDRADIUS_1D + ((uint32_t)advPartProps[i].size + (uint32_t)advPartProps[j].size)>>1; + collisiondistance = PS_P_MINHARDRADIUS_1D + (((uint32_t)advPartProps[i].size + (uint32_t)advPartProps[j].size)>>1); } dx = particles[j].x - particles[i].x; int32_t dv = (int32_t)particles[j].vx - (int32_t)particles[i].vx; @@ -2186,6 +2072,20 @@ int32_t limitSpeed(int32_t speed) return speed > PS_P_MAXSPEED ? PS_P_MAXSPEED : (speed < -PS_P_MAXSPEED ? -PS_P_MAXSPEED : speed); } +// check if particle is out of bounds and wrap it around if required +bool checkBoundsAndWrap(int32_t &position, const int32_t max, const int32_t particleradius, bool wrap) { + if ((uint32_t)position > max) { // check if particle reached an edge + if (wrap) { + position = position % (max + 1); // note: cannot optimize modulo, particles can be far out of bounds when wrap is enabled + if (position < 0) + position += max + 1; + } + else if (((position < -particleradius) || (position > max + particleradius))) // particle is leaving boundaries, out of bounds if it has fully left + return false; // out of bounds + } + return true; // particle is in bounds +} + // fastled color adding is very inaccurate in color preservation // a better color add function is implemented in colors.cpp but it uses 32bit RGBW. to use it colors need to be shifted just to then be shifted back by that function, which is slow // this is a fast version for RGB (no white channel, PS does not handle white) and with native CRGB including scaling of second color (fastled scale8 can be made faster using native 32bit on ESP) diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index a14751bf33..ea524315cf 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -18,6 +18,7 @@ //shared functions (used both in 1D and 2D system) int32_t calcForce_dv(int8_t force, uint8_t *counter); int32_t limitSpeed(int32_t speed); +bool checkBoundsAndWrap(int32_t &position, const int32_t max, const int32_t particleradius, bool wrap); // returns false if out of bounds by more than particleradius void fast_color_add(CRGB &c1, const CRGB &c2, uint32_t scale = 255); // fast and accurate color adding with scaling (scales c2 before adding) void fast_color_scale(CRGB &c, uint32_t scale); // fast scaling function using 32bit variable and pointer. note: keep 'scale' within 0-255 #endif @@ -33,10 +34,10 @@ void fast_color_scale(CRGB &c, uint32_t scale); // fast scaling function using 3 // particle dimensions (subpixel division) #define PS_P_RADIUS 64 // subpixel size, each pixel is divided by this for particle movement (must be a power of 2) -#define PS_P_HALFRADIUS 32 +#define PS_P_HALFRADIUS (PS_P_RADIUS >> 1) #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_MINHARDRADIUS 70 // minimum hard surface radius +#define PS_P_MINHARDRADIUS 70 // minimum hard surface radius for collisions #define PS_P_MINSURFACEHARDNESS 128 // minimum hardness used in collision impulse calculation, below this hardness, particles become sticky // struct for PS settings (shared for 1D and 2D class) @@ -154,8 +155,8 @@ class ParticleSystem2D { PSadvancedParticle *advPartProps; // pointer to advanced particle properties (can be NULL) PSsizeControl *advPartSize; // pointer to advanced particle size control (can be NULL) uint8_t* PSdataEnd; // points to first available byte after the PSmemory, is set in setPointers(). use this for FX custom data - int32_t maxX, maxY; // particle system size i.e. width-1 / height-1 in subpixels - int32_t maxXpixel, maxYpixel; // last physical pixel that can be drawn to (FX can read this to read segment size if required), equal to width-1 / height-1 + uint32_t maxX, maxY; // particle system size i.e. width-1 / height-1 in subpixels + uint32_t maxXpixel, maxYpixel; // last physical pixel that can be drawn to (FX can read this to read segment size if required), equal to width-1 / height-1 uint32_t numSources; // number of sources uint32_t numParticles; // number of particles available in this system uint32_t usedParticles; // number of particles used in animation (can be smaller then numParticles) @@ -163,7 +164,7 @@ class ParticleSystem2D { private: //rendering functions void ParticleSys_render(bool firemode = false, uint32_t fireintensity = 128); - void renderParticle(CRGB **framebuffer, const uint32_t &particleindex, const uint32_t &brightness, const CRGB &color, CRGB **renderbuffer, const bool &wrapX, const bool &wrapY); + void renderParticle(CRGB **framebuffer, const uint32_t particleindex, const uint32_t brightness, const CRGB& color, CRGB **renderbuffer, const bool wrapX, const bool wrapY); //paricle physics applied by system if flags are set void applyGravity(); // applies gravity to all particles void handleCollisions(); @@ -188,7 +189,7 @@ class ParticleSystem2D { uint8_t forcecounter; // counter for globally applied forces // global particle properties for basic particles uint8_t particlesize; // global particle size, 0 = 2 pixels, 255 = 10 pixels (note: this is also added to individual sized particles) - int32_t particleHardRadius; // hard surface radius of a particle, used for collision detection + uint32_t particleHardRadius; // hard surface radius of a particle, used for collision detection uint8_t motionBlur; // enable motion blur, values > 100 gives smoother animations. Note: motion blurring does not work if particlesize is > 0 }; @@ -214,8 +215,8 @@ bool allocateParticleSystemMemory2D(uint16_t numparticles, uint16_t numsources, // particle dimensions (subpixel division) #define PS_P_RADIUS_1D 32 // subpixel size, each pixel is divided by this for particle movement, if this value is changed, also change the shift defines (next two lines) -#define PS_P_HALFRADIUS_1D 16 -#define PS_P_RADIUS_SHIFT_1D 5 //TODO: may need to adjust +#define PS_P_HALFRADIUS_1D (PS_P_RADIUS_1D >> 1) +#define PS_P_RADIUS_SHIFT_1D 5 #define PS_P_SURFACE_1D 5 // shift: 2^PS_P_SURFACE = PS_P_RADIUS_1D #define PS_P_MINHARDRADIUS_1D 32 // minimum hard surface radius #define PS_P_MINSURFACEHARDNESS_1D 50 // minimum hardness used in collision impulse calculation @@ -304,8 +305,8 @@ class ParticleSystem1D PSadvancedParticle1D *advPartProps; // pointer to advanced particle properties (can be NULL) //PSsizeControl *advPartSize; // pointer to advanced particle size control (can be NULL) uint8_t* PSdataEnd; // points to first available byte after the PSmemory, is set in setPointers(). use this for FX custom data - int32_t maxX; // particle system size i.e. width-1 - int32_t maxXpixel; // last physical pixel that can be drawn to (FX can read this to read segment size if required), equal to width-1 + uint32_t maxX; // particle system size i.e. width-1 + uint32_t maxXpixel; // last physical pixel that can be drawn to (FX can read this to read segment size if required), equal to width-1 uint32_t numSources; // number of sources uint32_t numParticles; // number of particles available in this system uint32_t usedParticles; // number of particles used in animation (can be smaller then numParticles) @@ -313,7 +314,7 @@ class ParticleSystem1D private: //rendering functions void ParticleSys_render(void); - void renderParticle(CRGB *framebuffer, const uint32_t &particleindex, const uint32_t &brightness, const CRGB &color, CRGB *renderbuffer, const bool &wrap); + void renderParticle(CRGB *framebuffer, const uint32_t particleindex, const uint32_t brightness, const CRGB &color, CRGB *renderbuffer, const bool wrap); //paricle physics applied by system if flags are set void applyGravity(); // applies gravity to all particles @@ -337,7 +338,7 @@ class ParticleSystem1D //uint8_t collisioncounter; // counter to handle collisions TODO: could use the SEGMENT.call? -> currently unused // global particle properties for basic particles uint8_t particlesize; // global particle size, 0 = 1 pixel, 1 = 2 pixels, larger sizez TBD (TODO: need larger sizes?) - int32_t particleHardRadius; // hard surface radius of a particle, used for collision detection + uint32_t particleHardRadius; // hard surface radius of a particle, used for collision detection uint8_t motionBlur; // enable motion blur, values > 100 gives smoother animations. Note: motion blurring does not work if particlesize is > 0 }; From 10c2f0c27d88a160c1072a2580ca2f5dd9b89d2d Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sun, 27 Oct 2024 13:05:04 +0100 Subject: [PATCH 133/219] Cleanup, bugfixes, speed improvements, refactoring - lots of code refactoring / reformating - fixed bug in fire particle update and improved speed a bit - refactoring of pixel rendering - removed 2D buffers, replaced with 1D buffer for faster access - bugfix in blur2D - many small improvements to 2D system --- wled00/FX.cpp | 4 +- wled00/FX.h | 4 +- wled00/FXparticleSystem.cpp | 721 +++++++++++++++--------------------- wled00/FXparticleSystem.h | 27 +- 4 files changed, 320 insertions(+), 436 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index dc208c49a4..5f2a396e02 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -8427,7 +8427,7 @@ uint16_t mode_particlewaterfall(void) PartSys->setBounceX(SEGMENT.check2); // walls PartSys->setBounceY(SEGMENT.check3); // ground PartSys->setWallHardness(SEGMENT.custom2); - numSprays = min(PartSys->numSources, max(PartSys->maxXpixel / 6, (uint32_t)2)); // number of sprays depends on segment width + numSprays = min((int32_t)PartSys->numSources, max(PartSys->maxXpixel / 6, (int32_t)2)); // number of sprays depends on segment width if (SEGMENT.custom2 > 0) // collisions enabled PartSys->enableParticleCollisions(true, SEGMENT.custom2); // enable collisions and set particle collision hardness else @@ -10341,7 +10341,7 @@ uint16_t mode_particleChase(void) uint32_t settingssum = SEGMENT.speed + SEGMENT.intensity + SEGMENT.custom1 + SEGMENT.custom2 + SEGMENT.check1 + SEGMENT.check2 + SEGMENT.check3; if(SEGENV.aux0 != settingssum) //settings changed changed, update { - PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 1, min(PartSys->maxX / (32 + (SEGMENT.custom1 >> 1)), (PartSys->numParticles)))); //depends on intensity and particle size (custom1) + PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 1, min(PartSys->maxX / (32 + (SEGMENT.custom1 >> 1)), int32_t(PartSys->numParticles)))); //depends on intensity and particle size (custom1) SEGENV.step = (PartSys->maxX + (PS_P_RADIUS_1D << 4)) / PartSys->usedParticles; //spacing between particles // uint32_t remainder = PartSys->maxX - ((PartSys->usedParticles) * SEGENV.step); // unused spacing, distribute this for(i = 0; i < PartSys->usedParticles; i++) diff --git a/wled00/FX.h b/wled00/FX.h index 30142e9c9b..53f01253c1 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -316,8 +316,8 @@ #define FX_MODE_PARTICLESPRAY 197 #define FX_MODE_PARTICLESGEQ 198 #define FX_MODE_PARTICLECENTERGEQ 199 -#define FX_MODE_PARTICLEGHOSTRIDER 200 -#define FX_MODE_PARTICLEBLOBS 201 +#define FX_MODE_PARTICLEGHOSTRIDER 200 +#define FX_MODE_PARTICLEBLOBS 201 #define FX_MODE_PSDRIP 202 #define FX_MODE_PSBOUNCINGBALLS 203 #define FX_MODE_PSDANCINGSHADOWS 204 diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index 1e46edee43..981c3bac84 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -47,8 +47,7 @@ ParticleSystem2D::ParticleSystem2D(uint16_t width, uint16_t height, uint16_t num } // update function applies gravity, moves the particles, handles collisions and renders the particles -void ParticleSystem2D::update(void) -{ +void ParticleSystem2D::update(void) { PSadvancedParticle *advprop = NULL; //apply gravity globally if enabled if (particlesettings.useGravity) @@ -66,10 +65,8 @@ void ParticleSystem2D::update(void) handleCollisions(); //move all particles - for (uint32_t i = 0; i < usedParticles; i++) - { - if (advPartProps) - { + for (uint32_t i = 0; i < usedParticles; i++) { + if (advPartProps) { advprop = &advPartProps[i]; } particleMoveUpdate(particles[i], &particlesettings, advprop); @@ -79,117 +76,97 @@ void ParticleSystem2D::update(void) } // update function for fire animation -void ParticleSystem2D::updateFire(uint32_t intensity, bool renderonly) -{ +void ParticleSystem2D::updateFire(uint32_t intensity, bool renderonly) { if (!renderonly) fireParticleupdate(); ParticleSys_render(true, intensity); } -void ParticleSystem2D::setUsedParticles(uint32_t num) -{ +void ParticleSystem2D::setUsedParticles(uint32_t num) { usedParticles = min(num, numParticles); //limit to max particles } -void ParticleSystem2D::setWallHardness(uint8_t hardness) -{ +void ParticleSystem2D::setWallHardness(uint8_t hardness) { wallHardness = hardness; } -void ParticleSystem2D::setWallRoughness(uint8_t roughness) -{ +void ParticleSystem2D::setWallRoughness(uint8_t roughness) { wallRoughness = roughness; } -void ParticleSystem2D::setCollisionHardness(uint8_t hardness) -{ +void ParticleSystem2D::setCollisionHardness(uint8_t hardness) { collisionHardness = (int)hardness + 1; } -void ParticleSystem2D::setMatrixSize(uint16_t x, uint16_t y) -{ +void ParticleSystem2D::setMatrixSize(uint16_t x, uint16_t y) { maxXpixel = x - 1; // last physical pixel that can be drawn to maxYpixel = y - 1; maxX = x * PS_P_RADIUS - 1; // particle system boundary for movements maxY = y * PS_P_RADIUS - 1; // this value is often needed (also by FX) to calculate positions } -void ParticleSystem2D::setWrapX(bool enable) -{ +void ParticleSystem2D::setWrapX(bool enable) { particlesettings.wrapX = enable; } -void ParticleSystem2D::setWrapY(bool enable) -{ +void ParticleSystem2D::setWrapY(bool enable) { particlesettings.wrapY = enable; } -void ParticleSystem2D::setBounceX(bool enable) -{ +void ParticleSystem2D::setBounceX(bool enable) { particlesettings.bounceX = enable; } -void ParticleSystem2D::setBounceY(bool enable) -{ +void ParticleSystem2D::setBounceY(bool enable) { particlesettings.bounceY = enable; } -void ParticleSystem2D::setKillOutOfBounds(bool enable) -{ +void ParticleSystem2D::setKillOutOfBounds(bool enable) { particlesettings.killoutofbounds = enable; } -void ParticleSystem2D::setColorByAge(bool enable) -{ +void ParticleSystem2D::setColorByAge(bool enable) { particlesettings.colorByAge = enable; } -void ParticleSystem2D::setMotionBlur(uint8_t bluramount) -{ - if (particlesize == 0) // only allwo motion blurring on default particle size or advanced size(cannot combine motion blur with normal blurring used for particlesize, would require another buffer) +void ParticleSystem2D::setMotionBlur(uint8_t bluramount) { + if (particlesize == 0) // only allow motion blurring on default particle size or advanced size (cannot combine motion blur with normal blurring used for particlesize, would require another buffer) motionBlur = bluramount; } // render size using smearing (see blur function) -void ParticleSystem2D::setParticleSize(uint8_t size) -{ +void ParticleSystem2D::setParticleSize(uint8_t size) { particlesize = size; particleHardRadius = PS_P_MINHARDRADIUS + (particlesize >> 1); // radius used for wall collisions & particle collisions motionBlur = 0; // disable motion blur if particle size is set } + // enable/disable gravity, optionally, set the force (force=8 is default) can be -127 to +127, 0 is disable // if enabled, gravity is applied to all particles in ParticleSystemUpdate() // 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 ParticleSystem2D::setGravity(int8_t force) -{ - if (force) - { +void ParticleSystem2D::setGravity(int8_t force) { + if (force) { gforce = force; particlesettings.useGravity = true; - } - else + } else { particlesettings.useGravity = false; + } } -void ParticleSystem2D::enableParticleCollisions(bool enable, uint8_t hardness) // enable/disable gravity, optionally, set the force (force=8 is default) can be 1-255, 0 is also disable -{ +void ParticleSystem2D::enableParticleCollisions(bool enable, uint8_t hardness) { // enable/disable gravity, optionally, set the force (force=8 is default) can be 1-255, 0 is also disable particlesettings.useCollisions = enable; collisionHardness = (int)hardness + 1; } // emit one particle with variation, returns index of last emitted particle (or -1 if no particle emitted) -int32_t ParticleSystem2D::sprayEmit(PSsource &emitter, uint32_t amount) -{ +int32_t ParticleSystem2D::sprayEmit(PSsource &emitter, uint32_t amount) { bool success = false; - for (uint32_t a = 0; a < amount; a++) - { - for (uint32_t i = 0; i < usedParticles; i++) - { + for (uint32_t a = 0; a < amount; a++) { + for (uint32_t i = 0; i < usedParticles; i++) { emitIndex++; if (emitIndex >= usedParticles) emitIndex = 0; - if (particles[emitIndex].ttl == 0) // find a dead particle - { + if (particles[emitIndex].ttl == 0) { // find a dead particle success = true; particles[emitIndex].vx = emitter.vx + random(-emitter.var, emitter.var); particles[emitIndex].vy = emitter.vy + random(-emitter.var, emitter.var); @@ -205,7 +182,7 @@ int32_t ParticleSystem2D::sprayEmit(PSsource &emitter, uint32_t amount) } } } - if(success) + if (success) return emitIndex; else return -1; @@ -248,25 +225,14 @@ void ParticleSystem2D::particleMoveUpdate(PSparticle &part, PSsettings2D *option renderradius = particleHardRadius; } } - // if wall collisions are enabled, bounce them before they reach the edge, it looks much nicer if the particle is not half out of view - if (options->bounceX) { - if ((newX < (int32_t)particleHardRadius) || (newX > (int32_t)(maxX - particleHardRadius))) // reached a wall - bounce(part.vx, part.vy, newX, maxX); - } - - if(!checkBoundsAndWrap(newX, maxX, renderradius, options->wrapX)) { // check out of bounds note: this must not be skipped or it can lead to crashes TODO: is this still true? - part.outofbounds = true; - if (options->killoutofbounds) - part.ttl = 0; - } - + // note: if wall collisions are enabled, bounce them before they reach the edge, it looks much nicer if the particle does not go half out of view if (options->bounceY) { if ((newY < (int32_t)particleHardRadius) || ((newY > (int32_t)(maxY - particleHardRadius)) && !options->useGravity)) { // reached floor / ceiling bounce(part.vy, part.vx, newY, maxY); } } - if(!checkBoundsAndWrap(newY, maxY, renderradius, options->wrapY)) { // check out of bounds note: this must not be skipped or it can lead to crashes TODO: is this still true? + if(!checkBoundsAndWrap(newY, maxY, renderradius, options->wrapY)) { // check out of bounds note: this must not be skipped, if gravity is enabled, particles will never bounce at the top part.outofbounds = true; if (options->killoutofbounds) { if (newY < 0) // if gravity is enabled, only kill particles below ground @@ -275,11 +241,67 @@ void ParticleSystem2D::particleMoveUpdate(PSparticle &part, PSsettings2D *option part.ttl = 0; } } + + if(part.ttl) { //check x direction only if still alive + if (options->bounceX) { + if ((newX < (int32_t)particleHardRadius) || (newX > (int32_t)(maxX - particleHardRadius))) // reached a wall + bounce(part.vx, part.vy, newX, maxX); + } + else if(!checkBoundsAndWrap(newX, maxX, renderradius, options->wrapX)) { // check out of bounds TODO: not checking out of bounds when bounce is enabled used to lead to crashes, seems fixed now. test more. + part.outofbounds = true; + if (options->killoutofbounds) + part.ttl = 0; + } + } + part.x = (int16_t)newX; // set new position part.y = (int16_t)newY; // set new position } } +// move function for fire particles +void ParticleSystem2D::fireParticleupdate() { + for (uint32_t i = 0; i < usedParticles; i++) { + if (particles[i].ttl > 0) + { + particles[i].ttl--; // age + int32_t newY = particles[i].y + (int32_t)particles[i].vy + (particles[i].ttl >> 2); // younger particles move faster upward as they are hotter + particles[i].outofbounds = 0; // reset out of bounds flag + // 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 speed, only check x direction if y is not out of bounds + if (newY < -PS_P_HALFRADIUS) + particles[i].outofbounds = 1; + else if (newY > int32_t(maxY + PS_P_HALFRADIUS)) // particle moved out at the top + particles[i].ttl = 0; + else // particle is in frame in y direction, also check x direction now Note: using checkBoundsAndWrap() is slower, only saves a few bytes + { + int32_t newX = particles[i].x + (int32_t)particles[i].vx; + if ((newX < 0) || (newX > (int32_t)maxX)) { // handle out of bounds & wrap + if (particlesettings.wrapX) { + newX = newX % (maxX + 1); + if (newX < 0) // handle negative modulo + newX += maxX + 1; + } + else if ((newX < -PS_P_HALFRADIUS) || (newX > int32_t(maxX + PS_P_HALFRADIUS))) { //if fully out of view + particles[i].ttl = 0; + } + } + particles[i].x = newX; + } + particles[i].y = newY; + } + } +/* + // this loop saves 150 bytes of flash but is 5% slower + for (uint32_t i = 0; i < usedParticles; i++) { + if (particles[i].ttl > 0) { + particles[i].y += (particles[i].ttl >> 2); // younger particles move faster upward as they are hotter + particleMoveUpdate(particles[i]); + } + } +*/ +} + // update advanced particle size control void ParticleSystem2D::updateSize(PSadvancedParticle *advprops, PSsizeControl *advsize) { if (advsize == NULL) // safety check @@ -331,29 +353,27 @@ void ParticleSystem2D::updateSize(PSadvancedParticle *advprops, PSsizeControl *a } // calculate x and y size for asymmetrical particles (advanced size control) -void ParticleSystem2D::getParticleXYsize(PSadvancedParticle *advprops, PSsizeControl *advsize, uint32_t &xsize, uint32_t &ysize) -{ +void ParticleSystem2D::getParticleXYsize(PSadvancedParticle *advprops, PSsizeControl *advsize, uint32_t &xsize, uint32_t &ysize) { if (advsize == NULL) // if advsize is valid, also advanced properties pointer is valid (handled by updatePSpointers()) return; int32_t size = advprops->size; int32_t asymdir = advsize->asymdir; int32_t deviation = ((uint32_t)size * (uint32_t)advsize->asymmetry) / 255; // deviation from symmetrical size - // Calculate x and y size based on deviation and direction (0 is symmetrical, 64 is x, 128 is symmetrical, 192 is y) + // Calculate x and y size based on deviation and direction (0 is symmetrical, 64 is x, 128 is symmetrical, 192 is y) if (asymdir < 64) { - deviation = (asymdir * deviation) / 64; - } else if (asymdir < 192) { - deviation = ((128 - asymdir) * deviation) / 64; - } else { - deviation = ((asymdir - 255) * deviation) / 64; - } - // Calculate x and y size based on deviation, limit to 255 (rendering function cannot handle lareger sizes) - xsize = (size - deviation) > 255 ? 255 : size - deviation; - ysize = (size + deviation) > 255 ? 255 : size + deviation; + deviation = (asymdir * deviation) / 64; + } else if (asymdir < 192) { + deviation = ((128 - asymdir) * deviation) / 64; + } else { + deviation = ((asymdir - 255) * deviation) / 64; + } + // Calculate x and y size based on deviation, limit to 255 (rendering function cannot handle larger sizes) + xsize = (size - deviation) > 255 ? 255 : size - deviation; + ysize = (size + deviation) > 255 ? 255 : size + deviation; } // function to bounce a particle from a wall using set parameters (wallHardness and wallRoughness) -void ParticleSystem2D::bounce(int8_t &incomingspeed, int8_t ¶llelspeed, int32_t &position, uint16_t maxposition) -{ +void ParticleSystem2D::bounce(int8_t &incomingspeed, int8_t ¶llelspeed, int32_t &position, uint16_t maxposition) { incomingspeed = -incomingspeed; incomingspeed = (incomingspeed * wallHardness) / 255; // reduce speed as energy is lost on non-hard surface if (position < particleHardRadius) @@ -361,22 +381,21 @@ void ParticleSystem2D::bounce(int8_t &incomingspeed, int8_t ¶llelspeed, int3 else position = maxposition - particleHardRadius; if (wallRoughness) { - int32_t incomingspeed_abs = abs((int32_t)incomingspeed); + int32_t incomingspeed_abs = abs((int32_t)incomingspeed); int32_t totalspeed = incomingspeed_abs + abs((int32_t)parallelspeed); // transfer an amount of incomingspeed speed to parallel speed - int32_t donatespeed = (random(-incomingspeed_abs, incomingspeed_abs) * (int32_t)wallRoughness) / (int32_t)255; //take random portion of + or - perpendicular speed, scaled by roughness + int32_t donatespeed = (random(-incomingspeed_abs, incomingspeed_abs) * (int32_t)wallRoughness) / (int32_t)255; // take random portion of + or - perpendicular speed, scaled by roughness parallelspeed = limitSpeed((int32_t)parallelspeed + donatespeed); - //give the remainder of the speed to perpendicular speed + // give the remainder of the speed to perpendicular speed donatespeed = int8_t(totalspeed - abs(parallelspeed)); // keep total speed the same incomingspeed = incomingspeed > 0 ? donatespeed : -donatespeed; } } // apply a force in x,y direction to individual particle -// caller needs to provide a 8bit counter (for each paticle) that holds its value between calls +// caller needs to provide a 8bit counter (for each particle) that holds its value between calls // 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 ParticleSystem2D::applyForce(PSparticle *part, int8_t xforce, int8_t yforce, uint8_t *counter) -{ +void ParticleSystem2D::applyForce(PSparticle *part, 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 @@ -395,8 +414,7 @@ void ParticleSystem2D::applyForce(PSparticle *part, int8_t xforce, int8_t yforce } // apply a force in x,y direction to individual particle using advanced particle properties -void ParticleSystem2D::applyForce(uint16_t particleindex, int8_t xforce, int8_t yforce) -{ +void ParticleSystem2D::applyForce(uint16_t particleindex, int8_t xforce, int8_t yforce) { if (advPartProps == NULL) return; // no advanced properties available applyForce(&particles[particleindex], xforce, yforce, &advPartProps[particleindex].forcecounter); @@ -404,33 +422,29 @@ void ParticleSystem2D::applyForce(uint16_t particleindex, int8_t xforce, int8_t // apply a force in x,y direction to all particles // force is in 3.4 fixed point notation (see above) -void ParticleSystem2D::applyForce(int8_t xforce, int8_t yforce) -{ +void ParticleSystem2D::applyForce(int8_t xforce, int8_t yforce) { // for small forces, need to use a delay counter uint8_t tempcounter; - // note: this is not the most compuatationally efficient way to do this, but it saves on duplacte code and is fast enough - for (uint i = 0; i < usedParticles; i++) - { + // note: this is not the most computationally efficient way to do this, but it saves on duplicate code and is fast enough + for (uint i = 0; i < usedParticles; i++) { tempcounter = forcecounter; applyForce(&particles[i], xforce, yforce, &tempcounter); } - forcecounter = tempcounter; //save value back + forcecounter = tempcounter; // save value back } // apply a force in angular direction to single particle // caller needs to provide a 8bit counter that holds its value between calls (if using single particles, a counter for each particle is needed) // angle is from 0-65535 (=0-360deg) angle = 0 means in positive x-direction (i.e. to the right) // force is in 3.4 fixed point notation so force=16 means apply v+1 each frame (useful force range is +/- 127) -void ParticleSystem2D::applyAngleForce(PSparticle *part, int8_t force, uint16_t angle, uint8_t *counter) -{ +void ParticleSystem2D::applyAngleForce(PSparticle *part, int8_t force, uint16_t angle, uint8_t *counter) { int8_t xforce = ((int32_t)force * cos16(angle)) / 32767; // force is +/- 127 int8_t yforce = ((int32_t)force * sin16(angle)) / 32767; // note: cannot use bit shifts as bit shifting is asymmetrical for positive and negative numbers and this needs to be accurate! // note: sin16 is 10% faster than sin8() on ESP32 but on ESP8266 it is 9% slower applyForce(part, xforce, yforce, counter); } -void ParticleSystem2D::applyAngleForce(uint16_t particleindex, int8_t force, uint16_t angle) -{ +void ParticleSystem2D::applyAngleForce(uint16_t particleindex, int8_t force, uint16_t angle) { if (advPartProps == NULL) return; // no advanced properties available applyAngleForce(&particles[particleindex], force, angle, &advPartProps[particleindex].forcecounter); @@ -438,32 +452,28 @@ void ParticleSystem2D::applyAngleForce(uint16_t particleindex, int8_t force, uin // apply a force in angular direction to all particles // angle is from 0-65535 (=0-360deg) angle = 0 means in positive x-direction (i.e. to the right) -void ParticleSystem2D::applyAngleForce(int8_t force, uint16_t angle) -{ +void ParticleSystem2D::applyAngleForce(int8_t force, uint16_t angle) { int8_t xforce = ((int32_t)force * cos16(angle)) / 32767; // force is +/- 127 int8_t yforce = ((int32_t)force * sin16(angle)) / 32767; // note: cannot use bit shifts as bit shifting is asymmetrical for positive and negative numbers and this needs to be accurate! applyForce(xforce, yforce); } - // apply gravity to all particles using PS global gforce setting // force is in 3.4 fixed point notation, see note above // note: faster than apply force since direction is always down and counter is fixed for all particles -void ParticleSystem2D::applyGravity() -{ +void ParticleSystem2D::applyGravity() { int32_t dv = calcForce_dv(gforce, &gforcecounter); - for (uint32_t i = 0; i < usedParticles; i++) - { - // note: not checking if particle is dead is faster as most are usually alive and if few are alive, rendering is fast anyways + if(dv == 0) return; + for (uint32_t i = 0; i < usedParticles; 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 = limitSpeed((int32_t)particles[i].vy - dv); } } // apply gravity to single particle using system settings (use this for sources) // function does not increment gravity counter, if gravity setting is disabled, this cannot be used -void ParticleSystem2D::applyGravity(PSparticle *part) -{ - uint32_t counterbkp = gforcecounter; +void ParticleSystem2D::applyGravity(PSparticle *part) { + uint32_t counterbkp = gforcecounter; // backup PS gravity counter int32_t dv = calcForce_dv(gforce, &gforcecounter); gforcecounter = counterbkp; //save it back part->vy = limitSpeed((int32_t)part->vy - dv); @@ -471,28 +481,24 @@ void ParticleSystem2D::applyGravity(PSparticle *part) // slow down particle by friction, the higher the speed, the higher the friction. a high friction coefficient slows them more (255 means instant stop) // note: a coefficient smaller than 0 will speed them up (this is a feature, not a bug), coefficient larger than 255 inverts the speed, so don't do that -void ParticleSystem2D::applyFriction(PSparticle *part, int32_t coefficient) -{ +void ParticleSystem2D::applyFriction(PSparticle *part, int32_t coefficient) { int32_t friction = 255 - coefficient; // note: not checking if particle is dead can be done by caller (or can be omitted) - // note2: cannot use right shifts as bit shifting in right direction is asymmetrical for positive and negative numbers and this needs to be accurate or things start to go to the left side. + // note2: cannot use right shifts as bit shifting in right direction is asymmetrical for positive and negative numbers and this needs to be accurate part->vx = ((int16_t)part->vx * friction) / 255; part->vy = ((int16_t)part->vy * friction) / 255; } // apply friction to all particles -void ParticleSystem2D::applyFriction(int32_t coefficient) -{ - for (uint32_t i = 0; i < usedParticles; i++) - { +void ParticleSystem2D::applyFriction(int32_t coefficient) { + for (uint32_t i = 0; i < usedParticles; i++) { if (particles[i].ttl) applyFriction(&particles[i], coefficient); } } // attracts a particle to an attractor particle using the inverse square-law -void ParticleSystem2D::pointAttractor(uint16_t particleindex, PSparticle *attractor, uint8_t strength, bool swallow) -{ +void ParticleSystem2D::pointAttractor(uint16_t particleindex, PSparticle *attractor, uint8_t strength, bool swallow) { if (advPartProps == NULL) return; // no advanced properties available @@ -502,14 +508,11 @@ void ParticleSystem2D::pointAttractor(uint16_t particleindex, PSparticle *attrac // Calculate the force based on inverse square law int32_t distanceSquared = dx * dx + dy * dy; - if (distanceSquared < 8192) - { - if (swallow) // particle is close, age it fast so it fades out, do not attract further - { + if (distanceSquared < 8192) { + if (swallow) { // particle is close, age it fast so it fades out, do not attract further if (particles[particleindex].ttl > 7) particles[particleindex].ttl -= 8; - else - { + else { particles[particleindex].ttl = 0; return; } @@ -520,7 +523,6 @@ void ParticleSystem2D::pointAttractor(uint16_t particleindex, PSparticle *attrac int32_t force = ((int32_t)strength << 16) / distanceSquared; int8_t xforce = (force * dx) / 1024; // scale to a lower value, found by experimenting int8_t yforce = (force * dy) / 1024; // note: cannot use bit shifts as bit shifting is asymmetrical for positive and negative numbers and this needs to be accurate! - applyForce(particleindex, xforce, yforce); } @@ -528,51 +530,46 @@ void ParticleSystem2D::pointAttractor(uint16_t particleindex, PSparticle *attrac // if wrap is set, particles half out of bounds are rendered to the other side of the matrix // warning: do not render out of bounds particles or system will crash! rendering does not check if particle is out of bounds // fireintensity and firemode are optional arguments (fireintensity is only used in firemode) -void ParticleSystem2D::ParticleSys_render(bool firemode, uint32_t fireintensity) -{ - +void ParticleSystem2D::ParticleSys_render(bool firemode, uint32_t fireintensity) { CRGB baseRGB; bool useLocalBuffer = true; //use local rendering buffer, gives huge speed boost (at least 30% more FPS) - CRGB **framebuffer = NULL; //local frame buffer - CRGB **renderbuffer = NULL; //local particle render buffer for advanced particles + CRGB *framebuffer = NULL; //local frame buffer, Note: 1D array access is faster, especially when accessing in order + CRGB *renderbuffer = NULL; //local particle render buffer for advanced particles uint32_t i; uint32_t brightness; // particle brightness, fades if dying - /* - //memory fragmentation check: - Serial.print("heap: "); - Serial.print(heap_caps_get_free_size(MALLOC_CAP_8BIT)); - Serial.print(" block: "); - Serial.println(heap_caps_get_largest_free_block(MALLOC_CAP_8BIT)); - */ + /* + //memory fragmentation check: + Serial.print("heap: "); + Serial.print(heap_caps_get_free_size(MALLOC_CAP_8BIT)); + Serial.print(" block: "); + Serial.println(heap_caps_get_largest_free_block(MALLOC_CAP_8BIT)); + */ - if (useLocalBuffer) - { + if (useLocalBuffer) { // allocate empty memory for the local renderbuffer - framebuffer = allocate2Dbuffer(maxXpixel + 1, maxYpixel + 1); - if (framebuffer == NULL) - { + framebuffer = allocate1Dbuffer((maxXpixel + 1) * (maxYpixel + 1)); + if (framebuffer == NULL) { //Serial.println("Frame buffer alloc failed"); useLocalBuffer = false; //render to segment pixels directly if not enough memory } - else{ - if (motionBlur > 0) // using SEGMENT.fadeToBlackBy is much slower, this approximately doubles the speed of fade calculation - { + else { + if (motionBlur > 0) { // using SEGMENT.fadeToBlackBy is much slower, this approximately doubles the speed of fade calculation uint32_t yflipped; - for (int32_t y = 0; y <= maxYpixel; y++) - { + for (uint32_t y = 0; y <= maxYpixel; y++) { yflipped = maxYpixel - y; - for (int32_t x = 0; x <= maxXpixel; x++) - { - framebuffer[x][y] = SEGMENT.getPixelColorXY(x, yflipped); //copy to local buffer - fast_color_scale(framebuffer[x][y], motionBlur); + int index = y * (maxXpixel + 1); // current row index for 1D buffer + for (uint32_t x = 0; x <= maxXpixel; x++) { + framebuffer[index] = SEGMENT.getPixelColorXY(x, yflipped); //copy to local buffer + fast_color_scale(framebuffer[index], motionBlur); + index++; } } } } } if (advPartProps) { - renderbuffer = allocate2Dbuffer(10, 10); //buffer to render individual particles to if size > 0. note: null checking is done when accessing it + renderbuffer = allocate1Dbuffer(10*10); //buffer to render individual particles to if size > 0. note: null checking is done when accessing it } if (!useLocalBuffer) { //disabled or allocation above failed @@ -584,23 +581,20 @@ void ParticleSystem2D::ParticleSys_render(bool firemode, uint32_t fireintensity) bool wrapX = particlesettings.wrapX; // use local variables for faster access bool wrapY = particlesettings.wrapY; // go over particles and render them to the buffer - for (i = 0; i < usedParticles; i++) - { + for (i = 0; i < usedParticles; i++) { if (particles[i].outofbounds || particles[i].ttl == 0) continue; // generate RGB values for particle - if (firemode) - { - brightness = (uint32_t)particles[i].ttl*(3 + (fireintensity >> 5)) + 20; + if (firemode) { + brightness = (uint32_t)particles[i].ttl * (3 + (fireintensity >> 5)) + 20; brightness = brightness > 255 ? 255 : brightness; baseRGB = ColorFromPalette(SEGPALETTE, brightness, 255); } - else{ + else { brightness = particles[i].ttl > 255 ? 255 : particles[i].ttl; baseRGB = ColorFromPalette(SEGPALETTE, particles[i].hue, 255); - if (particles[i].sat < 255) - { + if (particles[i].sat < 255) { CHSV baseHSV = rgb2hsv_approximate(baseRGB); //convert to HSV baseHSV.s = particles[i].sat; //set the saturation baseRGB = (CRGB)baseHSV; // convert back to RGB @@ -609,35 +603,31 @@ void ParticleSystem2D::ParticleSys_render(bool firemode, uint32_t fireintensity) renderParticle(framebuffer, i, brightness, baseRGB, renderbuffer, wrapX, wrapY); } - if (particlesize > 0) - { - uint32_t passes = particlesize/64 + 1; // number of blur passes, four passes max + if (particlesize > 0) { + uint32_t passes = particlesize / 64 + 1; // number of blur passes, four passes max uint32_t bluramount = particlesize; uint32_t bitshift = 0; - for(uint32_t i = 0; i < passes; i++) - { + for (uint32_t i = 0; i < passes; i++) { if (i == 2) // for the last two passes, use higher amount of blur (results in a nicer brightness gradient with soft edges) bitshift = 1; if (useLocalBuffer) blur2D(framebuffer, maxXpixel + 1, maxYpixel + 1, bluramount << bitshift, bluramount << bitshift); - else{ + else { SEGMENT.blur(bluramount << bitshift, true); } bluramount -= 64; } } - if (useLocalBuffer) // transfer local buffer back to segment - { + if (useLocalBuffer) { // transfer local buffer back to segment int32_t yflipped; - for (int32_t y = 0; y <= maxYpixel; y++) - { + for (uint32_t y = 0; y <= maxYpixel; y++) { yflipped = maxYpixel - y; - for (int32_t x = 0; x <= maxXpixel; x++) - { - SEGMENT.setPixelColorXY((int)x, (int)yflipped, framebuffer[x][y]); + int index = y * (maxXpixel + 1); // current row index for 1D buffer + for (uint32_t x = 0; x <= maxXpixel; x++) { + SEGMENT.setPixelColorXY((int)x, (int)yflipped, framebuffer[index++]); } } free(framebuffer); @@ -647,15 +637,16 @@ void ParticleSystem2D::ParticleSys_render(bool firemode, uint32_t fireintensity) } // calculate pixel positions and brightness distribution and render the particle to local buffer or global buffer -void ParticleSystem2D::renderParticle(CRGB **framebuffer, const uint32_t particleindex, const uint32_t brightness, const CRGB& color, CRGB **renderbuffer, const bool wrapX, const bool wrapY) { - int32_t pxlbrightness[4] = {0}; // note: pxlbrightness needs to be set to 0 or checking does not work +void ParticleSystem2D::renderParticle(CRGB *framebuffer, const uint32_t particleindex, const uint32_t brightness, const CRGB& color, CRGB *renderbuffer, const bool wrapX, const bool wrapY) { + int32_t pxlbrightness[4]; // brightness values for the four pixels representing a particle int32_t pixco[4][2]; // physical pixel coordinates of the four pixels a particle is rendered to. x,y pairs + bool pixelvalid[4] = {true, true, true, true}; // is set to false if pixel is out of bounds bool advancedrender = false; // rendering for advanced particles // check if particle has advanced size properties and buffer is available if (advPartProps && advPartProps[particleindex].size > 0) { if (renderbuffer) { advancedrender = true; - memset(renderbuffer[0], 0, 100 * sizeof(CRGB)); // clear the buffer, renderbuffer is 10x10 pixels + memset(renderbuffer, 0, 100 * sizeof(CRGB)); // clear the buffer, renderbuffer is 10x10 pixels } else return; // cannot render without buffers } @@ -675,63 +666,25 @@ void ParticleSystem2D::renderParticle(CRGB **framebuffer, const uint32_t particl pixco[0][0] = pixco[3][0] = x; // bottom left & top left pixco[0][1] = pixco[1][1] = y; // bottom left & bottom right - // 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 (wrapX) { // wrap x to the other side if required - pixco[0][0] = pixco[3][0] = maxXpixel; - } else { - pxlbrightness[0] = pxlbrightness[3] = -1; // pixel is out of matrix boundaries, do not render - } - } - else if (pixco[1][0] > maxXpixel) { // right pixels, only has to be checkt if left pixels did not overflow - if (wrapX) { // wrap y to the other side if required - pixco[1][0] = pixco[2][0] = 0; - } else { - pxlbrightness[1] = pxlbrightness[2] = -1; - } - } - - if (y < 0) { // bottom pixels out of frame - if (wrapY) { // wrap y to the other side if required - pixco[0][1] = pixco[1][1] = maxYpixel; - } else { - pxlbrightness[0] = pxlbrightness[1] = -1; - } - } - else if (pixco[2][1] > maxYpixel) { // top pixels - if (wrapY) { // wrap y to the other side if required - pixco[2][1] = pixco[3][1] = 0; - } else { - pxlbrightness[2] = pxlbrightness[3] = -1; - } - } - - if (advancedrender) { // always render full particles in advanced rendering, undo out of frame marking (faster than checking each time in code above) - for(uint32_t i = 0; i < 4; i++) { - pxlbrightness[i] = 0; - } - } - // calculate brightness values for all four pixels representing a particle using linear interpolation + // could check for out of frame pixels here but calculating them is faster (very few are out) // precalculate values for speed optimization int32_t precal1 = (int32_t)PS_P_RADIUS - dx; int32_t precal2 = ((int32_t)PS_P_RADIUS - dy) * brightness; int32_t precal3 = dy * brightness; - - //calculate the values for pixels that are in frame - if (pxlbrightness[0] >= 0) pxlbrightness[0] = (precal1 * precal2) >> PS_P_SURFACE; // bottom left value equal to ((PS_P_RADIUS - dx) * (PS_P_RADIUS-dy) * brightness) >> PS_P_SURFACE - if (pxlbrightness[1] >= 0) pxlbrightness[1] = (dx * precal2) >> PS_P_SURFACE; // bottom right value equal to (dx * (PS_P_RADIUS-dy) * brightness) >> PS_P_SURFACE - if (pxlbrightness[2] >= 0) pxlbrightness[2] = (dx * precal3) >> PS_P_SURFACE; // top right value equal to (dx * dy * brightness) >> PS_P_SURFACE - if (pxlbrightness[3] >= 0) pxlbrightness[3] = (precal1 * precal3) >> PS_P_SURFACE; // top left value equal to ((PS_P_RADIUS-dx) * dy * brightness) >> PS_P_SURFACE + pxlbrightness[0] = (precal1 * precal2) >> PS_P_SURFACE; // bottom left value equal to ((PS_P_RADIUS - dx) * (PS_P_RADIUS-dy) * brightness) >> PS_P_SURFACE + pxlbrightness[1] = (dx * precal2) >> PS_P_SURFACE; // bottom right value equal to (dx * (PS_P_RADIUS-dy) * brightness) >> PS_P_SURFACE + pxlbrightness[2] = (dx * precal3) >> PS_P_SURFACE; // top right value equal to (dx * dy * brightness) >> PS_P_SURFACE + pxlbrightness[3] = (precal1 * precal3) >> PS_P_SURFACE; // top left value equal to ((PS_P_RADIUS-dx) * dy * brightness) >> PS_P_SURFACE if (advancedrender) { //render particle to a bigger size //particle size to pixels: < 64 is 4x4, < 128 is 6x6, < 192 is 8x8, bigger is 10x10 //first, render the pixel to the center of the renderbuffer, then apply 2D blurring - fast_color_add(renderbuffer[4][4], color, pxlbrightness[0]); // order is: bottom left, bottom right, top right, top left - fast_color_add(renderbuffer[5][4], color, pxlbrightness[1]); - fast_color_add(renderbuffer[5][5], color, pxlbrightness[2]); - fast_color_add(renderbuffer[4][5], color, pxlbrightness[3]); //TODO: make this a loop somehow? needs better coordinate handling... + fast_color_add(renderbuffer[4 + (4 * 10)], color, pxlbrightness[0]); // order is: bottom left, bottom right, top right, top left + fast_color_add(renderbuffer[5 + (4 * 10)], color, pxlbrightness[1]); + fast_color_add(renderbuffer[5 + (5 * 10)], color, pxlbrightness[2]); + fast_color_add(renderbuffer[4 + (5 * 10)], color, pxlbrightness[3]); uint32_t rendersize = 2; // initialize render size, minimum is 4x4 pixels, it is incremented int he loop below to start with 4 uint32_t offset = 4; // offset to zero coordinate to write/read data in renderbuffer (actually needs to be 3, is decremented in the loop below) uint32_t maxsize = advPartProps[particleindex].size; @@ -744,8 +697,7 @@ void ParticleSystem2D::renderParticle(CRGB **framebuffer, const uint32_t particl } maxsize = maxsize/64 + 1; // number of blur passes depends on maxsize, four passes max uint32_t bitshift = 0; - for(uint32_t i = 0; i < maxsize; i++) - { + for(uint32_t i = 0; i < maxsize; i++) { if (i == 2) //for the last two passes, use higher amount of blur (results in a nicer brightness gradient with soft edges) bitshift = 1; rendersize += 2; @@ -761,54 +713,73 @@ void ParticleSystem2D::renderParticle(CRGB **framebuffer, const uint32_t particl uint32_t xfb, yfb; // coordinates in frame buffer to write to note: by making this uint, only overflow has to be checked (spits a warning though) // transfer particle renderbuffer to framebuffer - for(uint32_t xrb = offset; xrb < rendersize+offset; xrb++) - { + for (uint32_t xrb = offset; xrb < rendersize + offset; xrb++) { xfb = xfb_orig + xrb; - if (xfb > maxXpixel) - { - if (wrapX) // wrap x to the other side if required - xfb = xfb % (maxXpixel + 1); //TODO: this did not work in 1D system but appears to work in 2D (wrapped pixels were offset) under which conditions does this not work? - else - continue; + if (xfb > maxXpixel) { + if (wrapX) // wrap x to the other side if required + xfb = xfb % (maxXpixel + 1); // TODO: this did not work in 1D system but appears to work in 2D (wrapped pixels were offset) under which conditions does this not work? + else + continue; } - for(uint32_t yrb = offset; yrb < rendersize+offset; yrb++) - { - yfb = yfb_orig + yrb; - if (yfb > maxYpixel) - { - if (wrapY) // wrap y to the other side if required - yfb = yfb % (maxYpixel + 1); - else - continue; - } - if (framebuffer) - fast_color_add(framebuffer[xfb][yfb], renderbuffer[xrb][yrb]); + for (uint32_t yrb = offset; yrb < rendersize + offset; yrb++) { + yfb = yfb_orig + yrb; + if (yfb > maxYpixel) { + if (wrapY) // wrap y to the other side if required + yfb = yfb % (maxYpixel + 1); else - SEGMENT.addPixelColorXY(xfb, maxYpixel - yfb, renderbuffer[xrb][yrb]); + continue; + } + if (framebuffer) + fast_color_add(framebuffer[xfb + yfb * (maxXpixel + 1)], renderbuffer[xrb + yrb * 10]); + else + SEGMENT.addPixelColorXY(xfb, maxYpixel - yfb, renderbuffer[xrb + yrb * 10]); } } - } - else // standard rendering - { - if (framebuffer) - { - for(uint32_t i = 0; i < 4; i++) - { - if (pxlbrightness[i] > 0) - fast_color_add(framebuffer[pixco[i][0]][pixco[i][1]], color, pxlbrightness[i]); // order is: bottom left, bottom right, top right, top left + } else { // standard rendering + // check for out of frame pixels and wrap them if required + if (x < 0) { // left pixels out of frame + if (wrapX) { // wrap x to the other side if required + pixco[0][0] = pixco[3][0] = maxXpixel; + } else { + pixelvalid[0] = pixelvalid[3] = false; // out of bounds } } - else - { - for(uint32_t i = 0; i < 4; i++) - { - if (pxlbrightness[i] > 0) - SEGMENT.addPixelColorXY(pixco[i][0], maxYpixel - pixco[i][1], color.scale8((uint8_t)pxlbrightness[i])); + else if (pixco[1][0] > (int32_t)maxXpixel) { // right pixels, only has to be checkt if left pixels did not overflow + if (wrapX) { // wrap y to the other side if required + pixco[1][0] = pixco[2][0] = 0; + } else { + pixelvalid[1] = pixelvalid[2] = false; // out of bounds } } - } + if (y < 0) { // bottom pixels out of frame + if (wrapY) { // wrap y to the other side if required + pixco[0][1] = pixco[1][1] = maxYpixel; + } else { + pixelvalid[0] = pixelvalid[1] = false; // out of bounds + } + } + else if (pixco[2][1] > maxYpixel) { // top pixels + if (wrapY) { // wrap y to the other side if required + pixco[2][1] = pixco[3][1] = 0; + } else { + pixelvalid[2] = pixelvalid[3] = false; // out of bounds + } + } + if (framebuffer) { + for (uint32_t i = 0; i < 4; i++) { + if (pixelvalid[i]) + fast_color_add(framebuffer[pixco[i][0] + pixco[i][1] * (maxXpixel + 1)], color, pxlbrightness[i]); // order is: bottom left, bottom right, top right, top left + } + } + else { + for (uint32_t i = 0; i < 4; i++) { + if (pixelvalid[i]) + SEGMENT.addPixelColorXY(pixco[i][0], maxYpixel - pixco[i][1], color.scale8((uint8_t)pxlbrightness[i])); + } + } + } /* // debug: check coordinates if out of buffer boundaries print out some info (rendering out of bounds particle causes crash!) @@ -848,51 +819,8 @@ void ParticleSystem2D::renderParticle(CRGB **framebuffer, const uint32_t particl } } */ - } -// 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 ParticleSystem2D::fireParticleupdate() -{ - //TODO: cleanup this function? check if normal move is much slower, change move function to check y first then this function just needs to add ttl to y befor calling normal move function (this function uses 274bytes of flash) - uint32_t i = 0; - - for (i = 0; i < usedParticles; i++) - { - if (particles[i].ttl > 0) - { - // age - particles[i].ttl--; - // apply velocity - particles[i].x = particles[i].x + (int32_t)particles[i].vx; - particles[i].y = particles[i].y + (int32_t)particles[i].vy + (particles[i].ttl >> 2); // younger particles move faster upward as they are hotter - //particles[i].y = particles[i].y + (int32_t)particles[i].vy;// + (particles[i].ttl >> 3); // younger particles move faster upward as they are hotter //this is experimental, different shifting - 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 speed, only check x direction if y is not out of bounds - // y-direction - if (particles[i].y < -PS_P_HALFRADIUS) - particles[i].outofbounds = 1; - else if (particles[i].y > maxY + PS_P_HALFRADIUS) // particle moved out at the top - particles[i].ttl = 0; - else // particle is in frame in y direction, also check x direction now - { - if ((particles[i].x < 0) || (particles[i].x > maxX)) - { - if (particlesettings.wrapX) - { - particles[i].x = (uint16_t)particles[i].x % (maxX + 1); - } - else if ((particles[i].x < -PS_P_HALFRADIUS) || (particles[i].x > maxX + PS_P_HALFRADIUS)) //if fully out of view - { - particles[i].ttl = 0; - } - } - } - } - } -} // detect collisions in an array of particles and handle them void ParticleSystem2D::handleCollisions() { @@ -900,7 +828,8 @@ void ParticleSystem2D::handleCollisions() { uint32_t i, j; uint32_t startparticle = 0; uint32_t endparticle = usedParticles >> 1; // do half the particles, significantly speeds things up - int32_t collisiondistance = particleHardRadius << 1; + int32_t collDistSq = particleHardRadius << 1; + collDistSq = collDistSq * collDistSq; // square it for faster comparison (square is one operation) // 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 more accurate collisions are needed, just call it twice in a row if (collisioncounter & 0x01) { @@ -909,19 +838,21 @@ void ParticleSystem2D::handleCollisions() { } 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].outofbounds == 0 && particles[i].collide) { // if particle is alive and does collide and is not out of view + if (particles[i].ttl > 0 && particles[i].outofbounds == 0 && particles[i].collide) { // if particle is alive and is not out of view and does collide int32_t dx, dy; // distance to other particles for (j = i + 1; j < usedParticles; j++) { // check against higher number particles if (particles[j].ttl > 0 && particles[j].collide) { // if target particle is alive - dx = particles[i].x - particles[j].x; - if (advPartProps) //may be using individual particle size - collisiondistance = PS_P_MINHARDRADIUS + particlesize + (((uint32_t)advPartProps[i].size + (uint32_t)advPartProps[j].size)>>1); // collision distance + if (advPartProps) { //may be using individual particle size + collDistSq = PS_P_MINHARDRADIUS + particlesize + (((uint32_t)advPartProps[i].size + (uint32_t)advPartProps[j].size)>>1); // collision distance + collDistSq = collDistSq * collDistSq; // square it for faster comparison + } - if (dx < collisiondistance && dx > -collisiondistance) { // check x direction, if close, check y direction - dy = particles[i].y - particles[j].y; - if (dy < collisiondistance && dy > -collisiondistance) // particles are close - collideParticles(&particles[i], &particles[j]); + dx = particles[j].x - particles[i].x; + if (dx * dx < collDistSq) { // check x direction, if close, check y direction (squaring is faster than abs() or dual compare) + dy = particles[j].y - particles[i].y; + if (dy * dy < collDistSq) // particles are close + collideParticles(&particles[i], &particles[j], dx, dy); } } } @@ -931,18 +862,14 @@ void ParticleSystem2D::handleCollisions() { // handle a collision if close proximity is detected, i.e. dx and/or dy smaller than 2*PS_P_RADIUS // takes two pointers to the particles to collide and the particle hardness (softer means more energy lost in collision, 255 means full hard) -void ParticleSystem2D::collideParticles(PSparticle *particle1, PSparticle *particle2) // TODO: dx,dy is calculated just above, can pass it over here to save a few CPU cycles? -{ - int32_t dx = particle2->x - particle1->x; - int32_t dy = particle2->y - particle1->y; +void ParticleSystem2D::collideParticles(PSparticle *particle1, PSparticle *particle2, int32_t dx, int32_t dy) { // TODO: dx,dy is calculated just above, can pass it over here to save a few CPU cycles? int32_t distanceSquared = dx * dx + dy * dy; // Calculate relative velocity (if it is zero, could exit but extra check does not overall speed but deminish it) int32_t relativeVx = (int16_t)particle2->vx - (int16_t)particle1->vx; int32_t relativeVy = (int16_t)particle2->vy - (int16_t)particle1->vy; - // if dx and dy are zero (i.e. they meet at the center) give them an offset, if speeds are also zero, also offset them (pushes them apart if they are clumped before enabling collisions) - if (distanceSquared == 0) - { + // if dx and dy are zero (i.e. same position) give them an offset, if speeds are also zero, also offset them (pushes particles apart if they are clumped before enabling collisions) + if (distanceSquared == 0) { // Adjust positions based on relative velocity direction dx = -1; if (relativeVx < 0) // if true, particle2 is on the right side @@ -956,15 +883,13 @@ void ParticleSystem2D::collideParticles(PSparticle *particle1, PSparticle *parti else if (relativeVy == 0) relativeVy = 1; - distanceSquared = 2; //1 + 1 + distanceSquared = 2; // 1 + 1 } // Calculate dot product of relative velocity and relative distance int32_t dotProduct = (dx * relativeVx + dy * relativeVy); // is always negative if moving towards each other - int32_t notsorandom = dotProduct & 0x01; //dotprouct LSB should be somewhat random, so no need to calculate a random number - if (dotProduct < 0) // particles are moving towards each other - { + if (dotProduct < 0) {// particles are moving towards each other // integer math used to avoid floats. // overflow check: dx/dy are 7bit, relativV are 8bit -> dotproduct is 15bit, dotproduct/distsquared ist 8b, multiplied by collisionhardness of 8bit. so a 16bit shift is ok, make it 15 to be sure no overflows happen // note: cannot use right shifts as bit shifting in right direction is asymmetrical for positive and negative numbers and this needs to be accurate! the trick is: only shift positive numers @@ -978,38 +903,27 @@ void ParticleSystem2D::collideParticles(PSparticle *particle1, PSparticle *parti particle2->vx -= ximpulse; particle2->vy -= yimpulse; - if (collisionHardness < surfacehardness) // if particles are soft, they become 'sticky' i.e. apply some friction (they do pile more nicely and stop sloshing around) - { - const uint32_t coeff = collisionHardness + (255 - PS_P_MINSURFACEHARDNESS); + if (collisionHardness < surfacehardness) { // if particles are soft, they become 'sticky' i.e. apply some friction (they do pile more nicely and stop sloshing around) + const uint32_t coeff = collisionHardness + (255 - PS_P_MINSURFACEHARDNESS); // Note: could call applyFriction, but this is faster and speed is key here particle1->vx = ((int32_t)particle1->vx * coeff) / 255; particle1->vy = ((int32_t)particle1->vy * coeff) / 255; particle2->vx = ((int32_t)particle2->vx * coeff) / 255; particle2->vy = ((int32_t)particle2->vy * coeff) / 255; -/* - if (collisionHardness < 10) // if they are very soft, stop slow particles completely to make them stick to each other - { - particle1->vx = (particle1->vx < 3 && particle1->vx > -3) ? 0 : particle1->vx; - particle1->vy = (particle1->vy < 3 && particle1->vy > -3) ? 0 : particle1->vy; - - particle2->vx = (particle2->vx < 3 && particle2->vx > -3) ? 0 : particle2->vx; - particle2->vy = (particle2->vy < 3 && particle2->vy > -3) ? 0 : particle2->vy; - }*/ } // particles have volume, push particles apart if they are too close // tried lots of configurations, it works best if not moved but given a little velocity, it tends to oscillate less this way // a problem with giving velocity is, that on harder collisions, this adds up as it is not dampened enough, so add friction in the FX if required - if (dotProduct > -250) //this means particles are slow (or really really close) so push them apart. - { + if (dotProduct > -250) { //this means particles are slow (or really really close) so push them apart. + int32_t notsorandom = dotProduct & 0x01; //dotprouct LSB should be somewhat random, so no need to calculate a random number int32_t pushamount = 1 + ((250 + dotProduct) >> 6); // the closer dotproduct is to zero, the closer the particles are int32_t push = 0; if (dx < 0) // particle 1 is on the right push = pushamount; else if (dx > 0) push = -pushamount; - else // on the same x coordinate, shift it a little so they do not stack - { + else { // on the same x coordinate, shift it a little so they do not stack if (notsorandom) particle1->x++; // move it so pile collapses else @@ -1021,8 +935,7 @@ void ParticleSystem2D::collideParticles(PSparticle *particle1, PSparticle *parti push = pushamount; else if (dy > 0) push = -pushamount; - else // dy==0 - { + else { // dy==0 if (notsorandom) particle1->y++; // move it so pile collapses else @@ -1030,8 +943,7 @@ void ParticleSystem2D::collideParticles(PSparticle *particle1, PSparticle *parti } particle1->vy += push; // note: pushing may push particles out of frame, if bounce is active, it will move it back as position will be limited to within frame, if bounce is disabled: bye bye - if (collisionHardness < 16) // if they are very soft, stop slow particles completely to make them stick to each other - { + if (collisionHardness < 16) { // if they are very soft, stop slow particles completely to make them stick to each other particle1->vx = 0; particle1->vy = 0; particle2->vx = 0; @@ -1044,28 +956,9 @@ void ParticleSystem2D::collideParticles(PSparticle *particle1, PSparticle *parti } } -// allocate memory for the 2D array in one contiguous block and set values to zero -CRGB **ParticleSystem2D::allocate2Dbuffer(uint32_t cols, uint32_t rows) -{ - CRGB ** array2D = (CRGB **)calloc(cols, sizeof(CRGB *) + rows * sizeof(CRGB)); - if (array2D == NULL) - DEBUG_PRINT(F("PS buffer alloc failed")); - else - { - // assign pointers of 2D array - CRGB *start = (CRGB *)(array2D + cols); - for (uint i = 0; i < cols; i++) - { - array2D[i] = start + i * rows; - } - } - return array2D; -} - // update size and pointers (memory location and size can change dynamically) // note: do not access the PS class in FX befor running this function (or it messes up SEGENV.data) -void ParticleSystem2D::updateSystem(void) -{ +void ParticleSystem2D::updateSystem(void) { // update matrix size uint32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; uint32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); @@ -1106,30 +999,36 @@ void ParticleSystem2D::updatePSpointers(bool isadvanced, bool sizecontrol) { } // blur a matrix in x and y direction, blur can be asymmetric in x and y -// for speed, 32bit variables are used, make sure to limit them to 8bit (0-255) or result is undefined +// for speed, 1D array and 32bit variables are used, make sure to limit them to 8bit (0-255) or result is undefined // to blur a subset of the buffer, change the xsize/ysize and set xstart/ystart to the desired starting coordinates (default start is 0/0) -void blur2D(CRGB **colorbuffer, uint32_t xsize, uint32_t ysize, uint32_t xblur, uint32_t yblur, uint32_t xstart, uint32_t ystart, bool isparticle) { +// subset blurring only works on 10x10 buffer (single particle rendering), if other sizes are needed, buffer width must be passed as parameter +void blur2D(CRGB *colorbuffer, uint32_t xsize, uint32_t ysize, uint32_t xblur, uint32_t yblur, uint32_t xstart, uint32_t ystart, bool isparticle) { CRGB seeppart, carryover; uint32_t seep = xblur >> 1; - if (isparticle) { //first and last row are always black in particle rendering + uint32_t width = xsize; // width of the buffer, used to calculate the index of the pixel + + if (isparticle) { //first and last row are always black in first pass of particle rendering ystart++; ysize--; + width = 10; // buffer size is 10x10 } + for(uint32_t y = ystart; y < ystart + ysize; y++) { carryover = BLACK; + uint32_t indexXY = xstart + y * width; for(uint32_t x = xstart; x < xstart + xsize; x++) { - seeppart = colorbuffer[x][y]; // create copy of current color + seeppart = colorbuffer[indexXY]; // create copy of current color fast_color_scale(seeppart, seep); // scale it and seep to neighbours if (x > 0) { - fast_color_add(colorbuffer[x-1][y], seeppart); - fast_color_add(colorbuffer[x][y], carryover); // TODO: could check if carryover is > 0, takes 7 instructions, add takes ~35, with lots of same color pixels (like background), it would be faster + fast_color_add(colorbuffer[indexXY - 1], seeppart); + fast_color_add(colorbuffer[indexXY], carryover); // TODO: could check if carryover is > 0, takes 7 instructions, add takes ~35, with lots of same color pixels (like background), it would be faster } carryover = seeppart; + indexXY++; // next pixel in x direction } - fast_color_add(colorbuffer[xsize-1][y], carryover); // set last pixel } - if (isparticle) { // now also do first and last row + if (isparticle) { // first and last row are now smeared ystart--; ysize++; } @@ -1137,16 +1036,17 @@ void blur2D(CRGB **colorbuffer, uint32_t xsize, uint32_t ysize, uint32_t xblur, seep = yblur >> 1; for(uint32_t x = xstart; x < xstart + xsize; x++) { carryover = BLACK; + uint32_t indexXY = x + ystart * width; for(uint32_t y = ystart; y < ystart + ysize; y++) { - seeppart = colorbuffer[x][y]; // create copy of current color + seeppart = colorbuffer[indexXY]; // create copy of current color fast_color_scale(seeppart, seep); // scale it and seep to neighbours if (y > 0) { - fast_color_add(colorbuffer[x][y-1], seeppart); - fast_color_add(colorbuffer[x][y], carryover); // todo: could check if carryover is > 0, takes 7 instructions, add takes ~35, with lots of same color pixels (like background), it would be faster + fast_color_add(colorbuffer[indexXY - width], seeppart); + fast_color_add(colorbuffer[indexXY], carryover); // todo: could check if carryover is > 0, takes 7 instructions, add takes ~35, with lots of same color pixels (like background), it would be faster } carryover = seeppart; + indexXY += width; // next pixel in y direction } - fast_color_add(colorbuffer[x][ysize-1], carryover); // set last pixel } } @@ -1286,31 +1186,26 @@ void ParticleSystem1D::update(void) applyGravity(); //move all particles - for (uint32_t i = 0; i < usedParticles; i++) - { + for (uint32_t i = 0; i < usedParticles; i++) { if (advPartProps) - { advprop = &advPartProps[i]; - } + particleMoveUpdate(particles[i], &particlesettings, advprop); } - if (particlesettings.colorByPosition) - { - for (uint32_t i = 0; i < usedParticles; i++) - { - particles[i].hue = (255 * (uint32_t)particles[i].x) / maxX; + if (particlesettings.colorByPosition) { + uint32_t scale = (255 << 16) / maxX; // speed improvement: multiplication is faster than division + for (uint32_t i = 0; i < usedParticles; i++) { + particles[i].hue = (scale * (uint32_t)particles[i].x) >> 16; } } ParticleSys_render(); uint32_t bg_color = SEGCOLOR(1); //background color, set to black to overlay - if (bg_color > 0) //if not black - { - for(int32_t i = 0; i < maxXpixel + 1; i++) - { - SEGMENT.addPixelColor(i,bg_color); + if (bg_color > 0) { //if not black + for(uint32_t i = 0; i < maxXpixel + 1; i++) { + SEGMENT.addPixelColor(i,bg_color); // TODO: can this be done in rendering function using local buffer? } } } @@ -1636,7 +1531,7 @@ void ParticleSystem1D::ParticleSys_render() if (useLocalBuffer) // transfer local buffer back to segment { - for (int x = 0; x <= maxXpixel; x++) + for (unsigned x = 0; x <= maxXpixel; x++) { SEGMENT.setPixelColor(x, framebuffer[x]); } @@ -1683,7 +1578,7 @@ void ParticleSystem1D::renderParticle(CRGB *framebuffer, const uint32_t particle else pxlisinframe[0] = false; // pixel is out of matrix boundaries, do not render } - else if (pixco[1] > maxXpixel) { // right pixel, only has to be checkt if left pixel did not overflow + else if (pixco[1] > (int32_t)maxXpixel) { // right pixel, only has to be checkt if left pixel did not overflow if (wrap) // wrap y to the other side if required pixco[1] = 0; else @@ -1730,9 +1625,9 @@ void ParticleSystem1D::renderParticle(CRGB *framebuffer, const uint32_t particle if (xfb > maxXpixel) { if (wrap) { // wrap x to the other side if required if (xfb > maxXpixel << 1) // xfb is "negative" (note: for some reason, this check is needed in 1D but works without in 2D...) - xfb = (maxXpixel +1) + (int32_t)xfb; + xfb = (maxXpixel +1) + (int32_t)xfb; //TODO: remove this again and see if it works now (changed maxxpixel to unsigned) else - xfb = xfb % (maxXpixel + 1); + xfb = xfb % (maxXpixel + 1); //TODO: can modulo be avoided? } else continue; @@ -1881,17 +1776,6 @@ void ParticleSystem1D::collideParticles(PSparticle1D *particle1, PSparticle1D *p } } - - -// allocate memory for the 1D array in one contiguous block and set values to zero -CRGB *ParticleSystem1D::allocate1Dbuffer(uint32_t length) -{ - CRGB *array = (CRGB *)calloc(length, sizeof(CRGB)); - //if (array == NULL) - // DEBUG_PRINT(F("PS 1D buffer alloc failed")); - return array; -} - // update size and pointers (memory location and size can change dynamically) // note: do not access the PS class in FX befor running this function (or it messes up SEGENV.data) void ParticleSystem1D::updateSystem(void) @@ -2028,7 +1912,6 @@ void blur1D(CRGB *colorbuffer, uint32_t size, uint32_t blur, uint32_t start) } carryover = seeppart; } - fast_color_add(colorbuffer[size-1], carryover); // set last pixel } #endif // WLED_DISABLE_PARTICLESYSTEM1D @@ -2042,7 +1925,7 @@ void blur1D(CRGB *colorbuffer, uint32_t size, uint32_t blur, uint32_t start) // calculate the delta speed (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 calcForce_dv(int8_t force, uint8_t* counter) +static int32_t calcForce_dv(int8_t force, uint8_t* counter) { if (force == 0) return 0; @@ -2050,30 +1933,26 @@ int32_t calcForce_dv(int8_t force, uint8_t* counter) int32_t force_abs = abs(force); // absolute value (faster than lots of if's only 7 instructions) int32_t dv = 0; // for small forces, need to use a delay counter, apply force only if it overflows - if (force_abs < 16) - { + if (force_abs < 16) { *counter += force_abs; - if (*counter > 15) - { + if (*counter > 15) { *counter -= 16; dv = force < 0 ? -1 : 1; // force is either, 1 or -1 if it is small (zero force is handled above) } } else - { dv = force / 16; // MSBs note: cannot use bitshift as dv can be negative - } + return dv; } // limit speed to prevent overflows -int32_t limitSpeed(int32_t speed) -{ +static int32_t limitSpeed(int32_t speed) { return speed > PS_P_MAXSPEED ? PS_P_MAXSPEED : (speed < -PS_P_MAXSPEED ? -PS_P_MAXSPEED : speed); } // check if particle is out of bounds and wrap it around if required -bool checkBoundsAndWrap(int32_t &position, const int32_t max, const int32_t particleradius, bool wrap) { +static bool checkBoundsAndWrap(int32_t &position, const int32_t max, const int32_t particleradius, bool wrap) { if ((uint32_t)position > max) { // check if particle reached an edge if (wrap) { position = position % (max + 1); // note: cannot optimize modulo, particles can be far out of bounds when wrap is enabled @@ -2090,7 +1969,7 @@ bool checkBoundsAndWrap(int32_t &position, const int32_t max, const int32_t part // a better color add function is implemented in colors.cpp but it uses 32bit RGBW. to use it colors need to be shifted just to then be shifted back by that function, which is slow // this is a fast version for RGB (no white channel, PS does not handle white) and with native CRGB including scaling of second color (fastled scale8 can be made faster using native 32bit on ESP) // note: result is stored in c1, so c1 will contain the result. not using a return value is much faster as the struct does not need to be copied upon return -void fast_color_add(CRGB &c1, const CRGB &c2, uint32_t scale) { +static void fast_color_add(CRGB &c1, const CRGB &c2, uint32_t scale) { //note: function is manly used to add scaled colors, so checking if one color is black is slower uint32_t r, g, b; if (scale < 255) { @@ -2120,10 +1999,16 @@ void fast_color_add(CRGB &c1, const CRGB &c2, uint32_t scale) { } // faster than fastled color scaling as it uses a 32bit scale factor and pointer -void fast_color_scale(CRGB &c, uint32_t scale) { +static void fast_color_scale(CRGB &c, uint32_t scale) { c.r = ((c.r * scale) >> 8); c.g = ((c.g * scale) >> 8); c.b = ((c.b * scale) >> 8); } +// allocate memory for the 1D CRGB array in one contiguous block and set values to zero +static CRGB* allocate1Dbuffer(uint32_t length) { + CRGB *array = (CRGB *)calloc(length, sizeof(CRGB)); + return array; +} + #endif // !defined(WLED_DISABLE_PARTICLESYSTEM2D) || !defined(WLED_DISABLE_PARTICLESYSTEM1D) diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index ea524315cf..6cb1a7975a 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -16,11 +16,12 @@ #define PS_P_MAXSPEED 120 // maximum speed a particle can have (vx/vy is int8) //shared functions (used both in 1D and 2D system) -int32_t calcForce_dv(int8_t force, uint8_t *counter); -int32_t limitSpeed(int32_t speed); -bool checkBoundsAndWrap(int32_t &position, const int32_t max, const int32_t particleradius, bool wrap); // returns false if out of bounds by more than particleradius -void fast_color_add(CRGB &c1, const CRGB &c2, uint32_t scale = 255); // fast and accurate color adding with scaling (scales c2 before adding) -void fast_color_scale(CRGB &c, uint32_t scale); // fast scaling function using 32bit variable and pointer. note: keep 'scale' within 0-255 +static int32_t calcForce_dv(int8_t force, uint8_t *counter); +static int32_t limitSpeed(int32_t speed); +static bool checkBoundsAndWrap(int32_t &position, const int32_t max, const int32_t particleradius, bool wrap); // returns false if out of bounds by more than particleradius +static void fast_color_add(CRGB &c1, const CRGB &c2, uint32_t scale = 255); // fast and accurate color adding with scaling (scales c2 before adding) +static void fast_color_scale(CRGB &c, uint32_t scale); // fast scaling function using 32bit variable and pointer. note: keep 'scale' within 0-255 +static CRGB *allocate1Dbuffer(uint32_t length); #endif #ifndef WLED_DISABLE_PARTICLESYSTEM2D @@ -155,8 +156,8 @@ class ParticleSystem2D { PSadvancedParticle *advPartProps; // pointer to advanced particle properties (can be NULL) PSsizeControl *advPartSize; // pointer to advanced particle size control (can be NULL) uint8_t* PSdataEnd; // points to first available byte after the PSmemory, is set in setPointers(). use this for FX custom data - uint32_t maxX, maxY; // particle system size i.e. width-1 / height-1 in subpixels - uint32_t maxXpixel, maxYpixel; // last physical pixel that can be drawn to (FX can read this to read segment size if required), equal to width-1 / height-1 + int32_t maxX, maxY; // particle system size i.e. width-1 / height-1 in subpixels, Note: all "max" variables must be signed to compare to coordinates (which are signed) + int32_t maxXpixel, maxYpixel; // last physical pixel that can be drawn to (FX can read this to read segment size if required), equal to width-1 / height-1 uint32_t numSources; // number of sources uint32_t numParticles; // number of particles available in this system uint32_t usedParticles; // number of particles used in animation (can be smaller then numParticles) @@ -164,11 +165,11 @@ class ParticleSystem2D { private: //rendering functions void ParticleSys_render(bool firemode = false, uint32_t fireintensity = 128); - void renderParticle(CRGB **framebuffer, const uint32_t particleindex, const uint32_t brightness, const CRGB& color, CRGB **renderbuffer, const bool wrapX, const bool wrapY); + void renderParticle(CRGB *framebuffer, const uint32_t particleindex, const uint32_t brightness, const CRGB& color, CRGB *renderbuffer, const bool wrapX, const bool wrapY); //paricle physics applied by system if flags are set void applyGravity(); // applies gravity to all particles void handleCollisions(); - void collideParticles(PSparticle *particle1, PSparticle *particle2); + void collideParticles(PSparticle *particle1, PSparticle *particle2, int32_t dx, int32_t dy); void fireParticleupdate(); //utility functions void updatePSpointers(bool isadvanced, bool sizecontrol); // update the data pointers to current segment data space @@ -176,7 +177,6 @@ class ParticleSystem2D { void getParticleXYsize(PSadvancedParticle *advprops, PSsizeControl *advsize, uint32_t &xsize, uint32_t &ysize); void bounce(int8_t &incomingspeed, int8_t ¶llelspeed, int32_t &position, uint16_t maxposition); // bounce on a wall int16_t wraparound(uint16_t p, uint32_t maxvalue); - CRGB **allocate2Dbuffer(uint32_t cols, uint32_t rows); // note: variables that are accessed often are 32bit for speed PSsettings2D particlesettings; // settings used when updating particles (can also used by FX to move sources), do not edit properties directly, use functions above uint32_t emitIndex; // index to count through particles to emit so searching for dead pixels is faster @@ -193,7 +193,7 @@ class ParticleSystem2D { uint8_t motionBlur; // enable motion blur, values > 100 gives smoother animations. Note: motion blurring does not work if particlesize is > 0 }; -void blur2D(CRGB **colorbuffer, uint32_t xsize, uint32_t ysize, uint32_t xblur, uint32_t yblur, uint32_t xstart = 0, uint32_t ystart = 0, bool isparticle = false); +void blur2D(CRGB *colorbuffer, uint32_t xsize, uint32_t ysize, uint32_t xblur, uint32_t yblur, uint32_t xstart = 0, uint32_t ystart = 0, bool isparticle = false); // initialization functions (not part of class) bool initParticleSystem2D(ParticleSystem2D *&PartSys, uint8_t requestedsources, uint16_t additionalbytes = 0, bool advanced = false, bool sizecontrol = false); uint32_t calculateNumberOfParticles2D(bool advanced, bool sizecontrol); @@ -305,8 +305,8 @@ class ParticleSystem1D PSadvancedParticle1D *advPartProps; // pointer to advanced particle properties (can be NULL) //PSsizeControl *advPartSize; // pointer to advanced particle size control (can be NULL) uint8_t* PSdataEnd; // points to first available byte after the PSmemory, is set in setPointers(). use this for FX custom data - uint32_t maxX; // particle system size i.e. width-1 - uint32_t maxXpixel; // last physical pixel that can be drawn to (FX can read this to read segment size if required), equal to width-1 + int32_t maxX; // particle system size i.e. width-1, Note: all "max" variables must be signed to compare to coordinates (which are signed) + int32_t maxXpixel; // last physical pixel that can be drawn to (FX can read this to read segment size if required), equal to width-1 uint32_t numSources; // number of sources uint32_t numParticles; // number of particles available in this system uint32_t usedParticles; // number of particles used in animation (can be smaller then numParticles) @@ -326,7 +326,6 @@ class ParticleSystem1D //void updateSize(PSadvancedParticle *advprops, PSsizeControl *advsize); // advanced size control //void getParticleXYsize(PSadvancedParticle *advprops, PSsizeControl *advsize, uint32_t &xsize, uint32_t &ysize); void bounce(int8_t &incomingspeed, int8_t ¶llelspeed, int32_t &position, uint16_t maxposition); // bounce on a wall - CRGB *allocate1Dbuffer(uint32_t length); // note: variables that are accessed often are 32bit for speed PSsettings1D particlesettings; // settings used when updating particles uint32_t emitIndex; // index to count through particles to emit so searching for dead pixels is faster From 716b294ef2c8983a3fdef8bfe4f2b65840ae5e77 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Mon, 28 Oct 2024 20:43:52 +0100 Subject: [PATCH 134/219] Refactoring and cleanup of 1D PS, removed debug printouts - code reformating for consistency and readability - replaced "'?" operator with min / max (produces the same code, more readable) - minor changes for speed (random16 used, can later be replaced with hardware random function) - renamed PS Equalizer to PS 2D GEQ --- wled00/FX.cpp | 2 +- wled00/FXparticleSystem.cpp | 480 ++++++++++++------------------------ 2 files changed, 161 insertions(+), 321 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 5f2a396e02..0df15364e3 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -9074,7 +9074,7 @@ uint16_t mode_particleGEQ(void) PartSys->update(); // update and render return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEGEQ[] PROGMEM = "PS Equalizer@Speed,Intensity,Diverge,Bounce,Gravity,Cylinder,Walls,Floor;;!;2f;pal=0,sx=155,ix=200,c1=0,c2=128,o1=0,o2=0,o3=0"; +static const char _data_FX_MODE_PARTICLEGEQ[] PROGMEM = "PS 2D GEQ@Speed,Intensity,Diverge,Bounce,Gravity,Cylinder,Walls,Floor;;!;2f;pal=0,sx=155,ix=200,c1=0,c2=128,o1=0,o2=0,o3=0"; /* * Particle rotating GEQ (unfinished, basically works but needs more fine-tuning) diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index 981c3bac84..5d91d49623 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -168,14 +168,14 @@ int32_t ParticleSystem2D::sprayEmit(PSsource &emitter, uint32_t amount) { emitIndex = 0; if (particles[emitIndex].ttl == 0) { // find a dead particle success = true; - particles[emitIndex].vx = emitter.vx + random(-emitter.var, emitter.var); - particles[emitIndex].vy = emitter.vy + random(-emitter.var, emitter.var); + particles[emitIndex].vx = emitter.vx + random16(emitter.var << 1) - emitter.var; // random(-var, var) + particles[emitIndex].vy = emitter.vy + random16(emitter.var << 1) - emitter.var; // random(-var, var) particles[emitIndex].x = emitter.source.x; particles[emitIndex].y = emitter.source.y; particles[emitIndex].hue = emitter.source.hue; particles[emitIndex].sat = emitter.source.sat; particles[emitIndex].collide = emitter.source.collide; - particles[emitIndex].ttl = random(emitter.minLife, emitter.maxLife); + particles[emitIndex].ttl = random16(emitter.minLife, emitter.maxLife); if (advPartProps) advPartProps[emitIndex].size = emitter.size; break; @@ -212,7 +212,7 @@ void ParticleSystem2D::particleMoveUpdate(PSparticle &part, PSsettings2D *option if (!part.perpetual) part.ttl--; // age if (options->colorByAge) - part.hue = part.ttl > 255 ? 255 : part.ttl; //set color to ttl + part.hue = min(part.ttl, (uint16_t)255); //set color to ttl int32_t renderradius = PS_P_HALFRADIUS; // used to check out of bounds int32_t newX = part.x + (int32_t)part.vx; @@ -368,8 +368,8 @@ void ParticleSystem2D::getParticleXYsize(PSadvancedParticle *advprops, PSsizeCon deviation = ((asymdir - 255) * deviation) / 64; } // Calculate x and y size based on deviation, limit to 255 (rendering function cannot handle larger sizes) - xsize = (size - deviation) > 255 ? 255 : size - deviation; - ysize = (size + deviation) > 255 ? 255 : size + deviation; + xsize = min((size - deviation), (int32_t)255); + ysize = min((size + deviation), (int32_t)255);; } // function to bounce a particle from a wall using set parameters (wallHardness and wallRoughness) @@ -384,7 +384,7 @@ void ParticleSystem2D::bounce(int8_t &incomingspeed, int8_t ¶llelspeed, int3 int32_t incomingspeed_abs = abs((int32_t)incomingspeed); int32_t totalspeed = incomingspeed_abs + abs((int32_t)parallelspeed); // transfer an amount of incomingspeed speed to parallel speed - int32_t donatespeed = (random(-incomingspeed_abs, incomingspeed_abs) * (int32_t)wallRoughness) / (int32_t)255; // take random portion of + or - perpendicular speed, scaled by roughness + int32_t donatespeed = ((random16(incomingspeed_abs << 1) - incomingspeed_abs) * (int32_t)wallRoughness) / (int32_t)255; // take random portion of + or - perpendicular speed, scaled by roughness parallelspeed = limitSpeed((int32_t)parallelspeed + donatespeed); // give the remainder of the speed to perpendicular speed donatespeed = int8_t(totalspeed - abs(parallelspeed)); // keep total speed the same @@ -426,7 +426,7 @@ void ParticleSystem2D::applyForce(int8_t xforce, int8_t yforce) { // for small forces, need to use a delay counter uint8_t tempcounter; // note: this is not the most computationally efficient way to do this, but it saves on duplicate code and is fast enough - for (uint i = 0; i < usedParticles; i++) { + for (uint32_t i = 0; i < usedParticles; i++) { tempcounter = forcecounter; applyForce(&particles[i], xforce, yforce, &tempcounter); } @@ -485,8 +485,8 @@ void ParticleSystem2D::applyFriction(PSparticle *part, int32_t coefficient) { int32_t friction = 255 - coefficient; // note: not checking if particle is dead can be done by caller (or can be omitted) // note2: cannot use right shifts as bit shifting in right direction is asymmetrical for positive and negative numbers and this needs to be accurate - part->vx = ((int16_t)part->vx * friction) / 255; - part->vy = ((int16_t)part->vy * friction) / 255; + part->vx = ((int32_t)part->vx * friction) / 255; + part->vy = ((int32_t)part->vy * friction) / 255; } // apply friction to all particles @@ -588,11 +588,11 @@ void ParticleSystem2D::ParticleSys_render(bool firemode, uint32_t fireintensity) // generate RGB values for particle if (firemode) { brightness = (uint32_t)particles[i].ttl * (3 + (fireintensity >> 5)) + 20; - brightness = brightness > 255 ? 255 : brightness; + brightness = min(brightness, (uint32_t)255); baseRGB = ColorFromPalette(SEGPALETTE, brightness, 255); } else { - brightness = particles[i].ttl > 255 ? 255 : particles[i].ttl; + brightness = min(particles[i].ttl, (uint16_t)255); baseRGB = ColorFromPalette(SEGPALETTE, particles[i].hue, 255); if (particles[i].sat < 255) { CHSV baseHSV = rgb2hsv_approximate(baseRGB); //convert to HSV @@ -865,8 +865,8 @@ void ParticleSystem2D::handleCollisions() { void ParticleSystem2D::collideParticles(PSparticle *particle1, PSparticle *particle2, int32_t dx, int32_t dy) { // TODO: dx,dy is calculated just above, can pass it over here to save a few CPU cycles? int32_t distanceSquared = dx * dx + dy * dy; // Calculate relative velocity (if it is zero, could exit but extra check does not overall speed but deminish it) - int32_t relativeVx = (int16_t)particle2->vx - (int16_t)particle1->vx; - int32_t relativeVy = (int16_t)particle2->vy - (int16_t)particle1->vy; + int32_t relativeVx = (int32_t)particle2->vx - (int32_t)particle1->vx; + int32_t relativeVy = (int32_t)particle2->vy - (int32_t)particle1->vy; // if dx and dy are zero (i.e. same position) give them an offset, if speeds are also zero, also offset them (pushes particles apart if they are clumped before enabling collisions) if (distanceSquared == 0) { @@ -894,7 +894,7 @@ void ParticleSystem2D::collideParticles(PSparticle *particle1, PSparticle *parti // overflow check: dx/dy are 7bit, relativV are 8bit -> dotproduct is 15bit, dotproduct/distsquared ist 8b, multiplied by collisionhardness of 8bit. so a 16bit shift is ok, make it 15 to be sure no overflows happen // note: cannot use right shifts as bit shifting in right direction is asymmetrical for positive and negative numbers and this needs to be accurate! the trick is: only shift positive numers // Calculate new velocities after collision - int32_t surfacehardness = collisionHardness < PS_P_MINSURFACEHARDNESS ? PS_P_MINSURFACEHARDNESS : collisionHardness; // if particles are soft, the impulse must stay above a limit or collisions slip through at higher speeds, 170 seems to be a good value + int32_t surfacehardness = max(collisionHardness, (int32_t)PS_P_MINSURFACEHARDNESS); // if particles are soft, the impulse must stay above a limit or collisions slip through at higher speeds, 170 seems to be a good value int32_t impulse = -(((((-dotProduct) << 15) / distanceSquared) * surfacehardness) >> 8); // note: inverting before bitshift corrects for asymmetry in right-shifts (and is slightly faster) int32_t ximpulse = ((impulse) * dx) / 32767; // cannot use bit shifts here, it can be negative, use division by 2^bitshift int32_t yimpulse = ((impulse) * dy) / 32767; @@ -1052,8 +1052,7 @@ void blur2D(CRGB *colorbuffer, uint32_t xsize, uint32_t ysize, uint32_t xblur, u //non class functions to use for initialization -uint32_t calculateNumberOfParticles2D(bool isadvanced, bool sizecontrol) -{ +uint32_t calculateNumberOfParticles2D(bool isadvanced, bool sizecontrol) { uint32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; uint32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); #ifdef ESP8266 @@ -1077,8 +1076,7 @@ uint32_t calculateNumberOfParticles2D(bool isadvanced, bool sizecontrol) return numberofParticles; } -uint32_t calculateNumberOfSources2D(uint8_t requestedsources) -{ +uint32_t calculateNumberOfSources2D(uint8_t requestedsources) { uint32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; uint32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); #ifdef ESP8266 @@ -1097,8 +1095,7 @@ uint32_t calculateNumberOfSources2D(uint8_t requestedsources) } //allocate memory for particle system class, particles, sprays plus additional memory requested by FX -bool allocateParticleSystemMemory2D(uint16_t numparticles, uint16_t numsources, bool isadvanced, bool sizecontrol, uint16_t additionalbytes) -{ +bool allocateParticleSystemMemory2D(uint16_t numparticles, uint16_t numsources, bool isadvanced, bool sizecontrol, uint16_t additionalbytes) { uint32_t requiredmemory = sizeof(ParticleSystem2D); // functions above make sure these are a multiple of 4 bytes (to avoid alignment issues) requiredmemory += sizeof(PSparticle) * numparticles; @@ -1108,36 +1105,21 @@ bool allocateParticleSystemMemory2D(uint16_t numparticles, uint16_t numsources, requiredmemory += sizeof(PSsizeControl) * numparticles; requiredmemory += sizeof(PSsource) * numsources; requiredmemory += additionalbytes; - requiredmemory += 64; //!!! just a test, remove this line looking for crash reasons - //Serial.print("allocating: "); - //Serial.print(requiredmemory); - //Serial.println("Bytes"); - //Serial.print("allocating for segment at"); - //Serial.println((uintptr_t)SEGENV.data); return(SEGMENT.allocateData(requiredmemory)); } // initialize Particle System, allocate additional bytes if needed (pointer to those bytes can be read from particle system class: PSdataEnd) -bool initParticleSystem2D(ParticleSystem2D *&PartSys, uint8_t requestedsources, uint16_t additionalbytes, bool advanced, bool sizecontrol) -{ - //Serial.println("PS init function"); +bool initParticleSystem2D(ParticleSystem2D *&PartSys, uint8_t requestedsources, uint16_t additionalbytes, bool advanced, bool sizecontrol) { uint32_t numparticles = calculateNumberOfParticles2D(advanced, sizecontrol); uint32_t numsources = calculateNumberOfSources2D(requestedsources); - //Serial.print("numsources: "); - //Serial.println(numsources); if (!allocateParticleSystemMemory2D(numparticles, numsources, advanced, sizecontrol, additionalbytes)) { DEBUG_PRINT(F("PS init failed: memory depleted")); return false; } - //Serial.print("SEGENV.data ptr"); - //Serial.println((uintptr_t)(SEGENV.data)); uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); - //Serial.println("calling constructor"); PartSys = new (SEGENV.data) ParticleSystem2D(cols, rows, numparticles, numsources, advanced, sizecontrol); // particle system constructor - //Serial.print("PS pointer at "); - //Serial.println((uintptr_t)PartSys); return true; } @@ -1149,9 +1131,7 @@ bool initParticleSystem2D(ParticleSystem2D *&PartSys, uint8_t requestedsources, //////////////////////// #ifndef WLED_DISABLE_PARTICLESYSTEM1D -ParticleSystem1D::ParticleSystem1D(uint16_t length, uint16_t numberofparticles, uint16_t numberofsources, bool isadvanced) -{ - //Serial.println("PS Constructor"); +ParticleSystem1D::ParticleSystem1D(uint16_t length, uint16_t numberofparticles, uint16_t numberofsources, bool isadvanced) { numSources = numberofsources; numParticles = numberofparticles; // set number of particles in the array usedParticles = numberofparticles; // use all particles by default @@ -1165,17 +1145,14 @@ ParticleSystem1D::ParticleSystem1D(uint16_t length, uint16_t numberofparticles, motionBlur = 0; //no fading by default emitIndex = 0; - //initialize some default non-zero values most FX use - for (uint32_t i = 0; i < numSources; i++) - { + // initialize some default non-zero values most FX use + for (uint32_t i = 0; i < numSources; i++) { sources[i].source.ttl = 1; //set source alive } - //Serial.println("PS Constructor done"); } // update function applies gravity, moves the particles, handles collisions and renders the particles -void ParticleSystem1D::update(void) -{ +void ParticleSystem1D::update(void) { PSadvancedParticle1D *advprop = NULL; // handle collisions (can push particles, must be done before updating particles or they can render out of bounds, causing a crash if using local buffer for speed) if (particlesettings.useCollisions) @@ -1189,7 +1166,6 @@ void ParticleSystem1D::update(void) for (uint32_t i = 0; i < usedParticles; i++) { if (advPartProps) advprop = &advPartProps[i]; - particleMoveUpdate(particles[i], &particlesettings, advprop); } @@ -1204,78 +1180,65 @@ void ParticleSystem1D::update(void) uint32_t bg_color = SEGCOLOR(1); //background color, set to black to overlay if (bg_color > 0) { //if not black - for(uint32_t i = 0; i < maxXpixel + 1; i++) { + for(int32_t i = 0; i < maxXpixel + 1; i++) { SEGMENT.addPixelColor(i,bg_color); // TODO: can this be done in rendering function using local buffer? } } } - -void ParticleSystem1D::setUsedParticles(uint32_t num) -{ +void ParticleSystem1D::setUsedParticles(uint32_t num) { usedParticles = min(num, numParticles); //limit to max particles } -void ParticleSystem1D::setWallHardness(uint8_t hardness) -{ +void ParticleSystem1D::setWallHardness(uint8_t hardness) { wallHardness = hardness; } -void ParticleSystem1D::setSize(uint16_t x) -{ +void ParticleSystem1D::setSize(uint16_t x) { maxXpixel = x - 1; // last physical pixel that can be drawn to maxX = x * PS_P_RADIUS_1D - 1; // particle system boundary for movements } -void ParticleSystem1D::setWrap(bool enable) -{ +void ParticleSystem1D::setWrap(bool enable) { particlesettings.wrap = enable; } -void ParticleSystem1D::setBounce(bool enable) -{ +void ParticleSystem1D::setBounce(bool enable) { particlesettings.bounce = enable; } -void ParticleSystem1D::setKillOutOfBounds(bool enable) -{ +void ParticleSystem1D::setKillOutOfBounds(bool enable) { particlesettings.killoutofbounds = enable; } -void ParticleSystem1D::setColorByAge(bool enable) -{ +void ParticleSystem1D::setColorByAge(bool enable) { particlesettings.colorByAge = enable; } -void ParticleSystem1D::setColorByPosition(bool enable) -{ +void ParticleSystem1D::setColorByPosition(bool enable) { particlesettings.colorByPosition = enable; } -void ParticleSystem1D::setMotionBlur(uint8_t bluramount) -{ +void ParticleSystem1D::setMotionBlur(uint8_t bluramount) { //TODO: currently normal blurring is not used in 1D system. should it be added? advanced rendering is quite fast and allows for motion blurring - // if (particlesize < 2) // only allwo motion blurring on default particle size or advanced size(cannot combine motion blur with normal blurring used for particlesize, would require another buffer) - motionBlur = bluramount; + // if (particlesize < 2) // only allow motion blurring on default particle size or advanced size (cannot combine motion blur with normal blurring used for particlesize, would require another buffer) + motionBlur = bluramount; } // render size, 0 = 1 pixel, 1 = 2 pixel (interpolated), bigger sizes require adanced properties -// note: if size is set larger than 1 without advanced properties, weird things may happen -void ParticleSystem1D::setParticleSize(uint8_t size) -{ - particlesize = size; // TODO: add support for global sizes? +void ParticleSystem1D::setParticleSize(uint8_t size) { + particlesize = size > 0 ? 1 : 0; // TODO: add support for global sizes? see not abover (motion blur) if (particlesize) particleHardRadius = PS_P_MINHARDRADIUS_1D; // 2 pixel sized particles else particleHardRadius = PS_P_MINHARDRADIUS_1D >> 1; // 1 pixel sized particles have half the radius (for bounce, not for collisions) } + // enable/disable gravity, optionally, set the force (force=8 is default) can be -127 to +127, 0 is disable // if enabled, gravity is applied to all particles in ParticleSystemUpdate() // 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 ParticleSystem1D::setGravity(int8_t force) -{ - if (force) - { +void ParticleSystem1D::setGravity(int8_t force) { + if (force) { gforce = force; particlesettings.useGravity = true; } @@ -1283,35 +1246,28 @@ void ParticleSystem1D::setGravity(int8_t force) particlesettings.useGravity = false; } -void ParticleSystem1D::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 -{ +void ParticleSystem1D::enableParticleCollisions(bool enable, uint8_t hardness) { particlesettings.useCollisions = enable; collisionHardness = hardness; } // emit one particle with variation, returns index of last emitted particle (or -1 if no particle emitted) -int32_t ParticleSystem1D::sprayEmit(PSsource1D &emitter) -{ - for (int32_t i = 0; i < usedParticles; i++) - { +int32_t ParticleSystem1D::sprayEmit(PSsource1D &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].vx = emitter.v + random(-emitter.var, emitter.var); + if (particles[emitIndex].ttl == 0) { // find a dead particle + particles[emitIndex].vx = emitter.v + random16(emitter.var << 1) - emitter.var; // random(-var,var) particles[emitIndex].x = emitter.source.x; particles[emitIndex].hue = emitter.source.hue; particles[emitIndex].collide = emitter.source.collide; particles[emitIndex].reversegrav = emitter.source.reversegrav; particles[emitIndex].ttl = random16(emitter.minLife, emitter.maxLife); - if (advPartProps) - { + if (advPartProps) { advPartProps[emitIndex].sat = emitter.sat; advPartProps[emitIndex].size = emitter.size; } - return i; - return emitIndex; } } @@ -1320,24 +1276,23 @@ int32_t ParticleSystem1D::sprayEmit(PSsource1D &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 and killoutofbounds is not applied over the top -void ParticleSystem1D::particleMoveUpdate(PSparticle1D &part, PSsettings1D *options, PSadvancedParticle1D *advancedproperties) -{ +void ParticleSystem1D::particleMoveUpdate(PSparticle1D &part, PSsettings1D *options, PSadvancedParticle1D *advancedproperties) { if (options == NULL) - options = &particlesettings; //use PS system settings by default + options = &particlesettings; // use PS system settings by default if (part.ttl > 0) { if (!part.perpetual) part.ttl--; // age if (options->colorByAge) - part.hue = part.ttl > 255 ? 255 : part.ttl; //set color to ttl + part.hue = min(part.ttl, (uint16_t)255); // set color to ttl int32_t renderradius = PS_P_HALFRADIUS_1D; // used to check out of bounds, default for 2 pixel rendering int32_t newX = part.x + (int32_t)part.vx; part.outofbounds = false; // reset out of bounds (in case particle was created outside the matrix and is now moving into view) - if (advancedproperties) { //using individual particle size? + if (advancedproperties) { // using individual particle size? if (advancedproperties->size > 1) - particleHardRadius = PS_P_MINHARDRADIUS_1D + (advancedproperties->size >> 1); //TODO: this may need optimization, radius and diameter is still a mess in 1D system. + particleHardRadius = PS_P_MINHARDRADIUS_1D + (advancedproperties->size >> 1); // TODO: this may need optimization, radius and diameter is still a mess in 1D system. else // single pixel particles use half the collision distance for walls particleHardRadius = PS_P_MINHARDRADIUS_1D >> 1; renderradius = particleHardRadius; // note: for single pixel particles, it should be zero, but it does not matter as out of bounds checking is done in rendering function @@ -1346,38 +1301,35 @@ void ParticleSystem1D::particleMoveUpdate(PSparticle1D &part, PSsettings1D *opti // if wall collisions are enabled, bounce them before they reach the edge, it looks much nicer if the particle is not half out of view if (options->bounce) { if ((newX < (int32_t)particleHardRadius) || ((newX > (int32_t)(maxX - particleHardRadius)))) { // reached a wall - bool bouncethis = true; - if (options->useGravity) { - if (part.reversegrav) { // skip bouncing at x = 0 - if (newX < particleHardRadius) - bouncethis = false; - } - else if (newX > particleHardRadius){ //skip bouncing at x = max - bouncethis = false; - } - } - if (bouncethis) { - part.vx = -part.vx; //invert speed - part.vx = ((int32_t)part.vx * wallHardness) / 255; // reduce speed as energy is lost on non-hard surface - if (newX < (int32_t)particleHardRadius) - newX = particleHardRadius; // fast particles will never reach the edge if position is inverted, this looks better - else - newX = maxX - particleHardRadius; + bool bouncethis = true; + if (options->useGravity) { + if (part.reversegrav) { // skip bouncing at x = 0 + if (newX < particleHardRadius) + bouncethis = false; + } else if (newX > particleHardRadius) { // skip bouncing at x = max + bouncethis = false; } + } + if (bouncethis) { + part.vx = -part.vx; // invert speed + part.vx = ((int32_t)part.vx * (int32_t)wallHardness) / 255; // reduce speed as energy is lost on non-hard surface + if (newX < (int32_t)particleHardRadius) + newX = particleHardRadius; // fast particles will never reach the edge if position is inverted, this looks better + else + newX = maxX - particleHardRadius; + } } } - if(!checkBoundsAndWrap(newX, maxX, renderradius, options->wrap)) { // check out of bounds note: this must not be skipped or it can lead to crashes TODO: is this still true? + if (!checkBoundsAndWrap(newX, maxX, renderradius, options->wrap)) { // check out of bounds note: this must not be skipped or it can lead to crashes TODO: is this still true? part.outofbounds = true; - if (options->killoutofbounds) - { + if (options->killoutofbounds) { bool killthis = true; - if (options->useGravity) { //if gravity is used, only kill below 'floor level' - if (part.reversegrav) { //skip at x = 0 + if (options->useGravity) { // if gravity is used, only kill below 'floor level' + if (part.reversegrav) { // skip at x = 0 if (newX < 0) killthis = false; - } - else { //skip at x = max + } else { // skip at x = max if (newX > 0) killthis = false; } @@ -1390,54 +1342,42 @@ void ParticleSystem1D::particleMoveUpdate(PSparticle1D &part, PSsettings1D *opti if (!part.fixed) part.x = (int16_t)newX; // set new position else - part.vx = 0; //set speed to zero. note: particle can get speed in collisions, if unfixed, it should not speed away + part.vx = 0; // set speed to zero. note: particle can get speed in collisions, if unfixed, it should not speed away } } // apply a force in x direction to individual particle (or source) // caller needs to provide a 8bit counter (for each paticle) that holds its value between calls // force is in 3.4 fixed point notation so force=16 means apply v+1 each frame default of 8 is every other frame -void ParticleSystem1D::applyForce(PSparticle1D *part, int8_t xforce, uint8_t *counter) -{ - // velocity increase - int32_t dv = calcForce_dv(xforce, counter); - // apply the force to particle - part->vx = limitSpeed((int32_t)part->vx + dv); +void ParticleSystem1D::applyForce(PSparticle1D *part, int8_t xforce, uint8_t *counter) { + int32_t dv = calcForce_dv(xforce, counter); // velocity increase + part->vx = limitSpeed((int32_t)part->vx + dv); // apply the force to particle } // apply a force to all particles // force is in 3.4 fixed point notation (see above) -void ParticleSystem1D::applyForce(int8_t xforce) -{ - // for small forces, need to use a delay counter - uint8_t tempcounter; - // note: this is not the most compuatationally efficient way to do this, but it saves on duplacte code and is fast enough - for (uint i = 0; i < usedParticles; i++) - { - tempcounter = forcecounter; - applyForce(&particles[i], xforce, &tempcounter); +void ParticleSystem1D::applyForce(int8_t xforce) { + int32_t dv = calcForce_dv(xforce, &forcecounter); // velocity increase + for (uint32_t i = 0; i < usedParticles; i++) { + particles[i].vx = limitSpeed((int32_t)particles[i].vx + dv); } - forcecounter = tempcounter; //save value back } // apply gravity to all particles using PS global gforce setting // gforce is in 3.4 fixed point notation, see note above -void ParticleSystem1D::applyGravity() -{ +void ParticleSystem1D::applyGravity() { int32_t dv_raw = calcForce_dv(gforce, &gforcecounter); - for (uint32_t i = 0; i < usedParticles; i++) - { + for (uint32_t i = 0; i < usedParticles; i++) { int32_t dv = dv_raw; if (particles[i].reversegrav) dv = -dv_raw; - // note: not checking if particle is dead is faster as most are usually alive and if few are alive, rendering is fast anyways + // note: not checking if particle is dead is omitted as most are usually alive and if few are alive, rendering is fast anyways particles[i].vx = limitSpeed((int32_t)particles[i].vx - dv); } } // apply gravity to single particle using system settings (use this for sources) // function does not increment gravity counter, if gravity setting is disabled, this cannot be used -void ParticleSystem1D::applyGravity(PSparticle1D *part) -{ +void ParticleSystem1D::applyGravity(PSparticle1D *part) { uint32_t counterbkp = gforcecounter; int32_t dv = calcForce_dv(gforce, &gforcecounter); if (part->reversegrav) dv = -dv; @@ -1448,13 +1388,11 @@ void ParticleSystem1D::applyGravity(PSparticle1D *part) // slow down particle by friction, the higher the speed, the higher the friction. a high friction coefficient slows them more (255 means instant stop) // note: a coefficient smaller than 0 will speed them up (this is a feature, not a bug), coefficient larger than 255 inverts the speed, so don't do that -void ParticleSystem1D::applyFriction(int32_t coefficient) -{ +void ParticleSystem1D::applyFriction(int32_t coefficient) { int32_t friction = 255 - coefficient; - for (uint32_t i = 0; i < usedParticles; i++) - { + for (uint32_t i = 0; i < usedParticles; i++) { if (particles[i].ttl) - particles[i].vx = ((int16_t)particles[i].vx * friction) / 255; + particles[i].vx = ((int32_t)particles[i].vx * friction) / 255; // note: cannot use bitshift as vx can be negative } } @@ -1462,65 +1400,49 @@ void ParticleSystem1D::applyFriction(int32_t coefficient) // 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 // warning: do not render out of bounds particles or system will crash! rendering does not check if particle is out of bounds -void ParticleSystem1D::ParticleSys_render() -{ - +void ParticleSystem1D::ParticleSys_render() { CRGB baseRGB; bool useLocalBuffer = true; //use local rendering buffer, gives huge speed boost (at least 30% more FPS) CRGB *framebuffer = NULL; //local frame buffer CRGB *renderbuffer = NULL; //local particle render buffer for advanced particles - uint32_t i; uint32_t brightness; // particle brightness, fades if dying - if (useLocalBuffer) - { - - // allocate empty memory for the local renderbuffer - framebuffer = allocate1Dbuffer(maxXpixel + 1); - if (framebuffer == NULL) - { - //Serial.println("Frame buffer alloc failed"); - useLocalBuffer = false; //render to segment pixels directly if not enough memory + if (useLocalBuffer) { + framebuffer = allocate1Dbuffer(maxXpixel + 1); // allocate memory for the local renderbuffer + if (framebuffer == NULL) { + DEBUG_PRINT(F("Frame buffer alloc failed")); + useLocalBuffer = false; // render to segment pixels directly if not enough memory } - else{ + else { if (advPartProps) - { - renderbuffer = allocate1Dbuffer(10); //buffer to render individual particles to if size > 0. note: null checking is done when accessing it - } - if (motionBlur > 0) // using SEGMENT.fadeToBlackBy is much slower, this approximately doubles the speed of fade calculation - { - for (uint32_t x = 0; x <= maxXpixel; x++) - { - framebuffer[x] = SEGMENT.getPixelColor(x); //copy to local buffer + renderbuffer = allocate1Dbuffer(10); // buffer to render individual particles to if size > 0. note: null checking is done when accessing it + if (motionBlur > 0) { // using SEGMENT.fadeToBlackBy is much slower, this approximately doubles the speed of fade calculation + for (uint32_t x = 0; x <= maxXpixel; x++) { + framebuffer[x] = SEGMENT.getPixelColor(x); // copy to local buffer fast_color_scale(framebuffer[x], motionBlur); } } } } - if (!useLocalBuffer) //disabled or allocation above failed - { - //Serial.println("NOT using local buffer!"); + if (!useLocalBuffer) { // disabled or allocation above failed if (motionBlur > 0) SEGMENT.fadeToBlackBy(255 - motionBlur); else - SEGMENT.fill(BLACK); //clear the buffer before rendering to it + SEGMENT.fill(BLACK); // clear the buffer before rendering to it } bool wrap = particlesettings.wrap; // local copy for speed // go over particles and render them to the buffer - for (i = 0; i < usedParticles; i++) - { + for (uint32_t i = 0; i < usedParticles; i++) { if (particles[i].outofbounds || particles[i].ttl == 0) continue; // generate RGB values for particle - brightness = particles[i].ttl > 255 ? 255 : particles[i].ttl; //faster then using min() + brightness = min(particles[i].ttl, (uint16_t)255); baseRGB = ColorFromPalette(SEGPALETTE, particles[i].hue, 255, LINEARBLEND); - if (advPartProps) //saturation is advanced property in 1D system - { - if (advPartProps[i].sat < 255) - { + if (advPartProps) { //saturation is advanced property in 1D system + if (advPartProps[i].sat < 255) { CHSV baseHSV = rgb2hsv_approximate(baseRGB); //convert to HSV baseHSV.s = advPartProps[i].sat; //set the saturation baseRGB = (CRGB)baseHSV; // convert back to RGB @@ -1529,10 +1451,8 @@ void ParticleSystem1D::ParticleSys_render() renderParticle(framebuffer, i, brightness, baseRGB, renderbuffer, wrap); } - if (useLocalBuffer) // transfer local buffer back to segment - { - for (unsigned x = 0; x <= maxXpixel; x++) - { + if (useLocalBuffer) { // transfer local buffer back to segment + for (unsigned x = 0; x <= maxXpixel; x++) { SEGMENT.setPixelColor(x, framebuffer[x]); } free(framebuffer); @@ -1547,7 +1467,7 @@ void ParticleSystem1D::renderParticle(CRGB *framebuffer, const uint32_t particle if (advPartProps) {// use advanced size properties size = advPartProps[particleindex].size; } - if (size == 0) { //single pixel particle, can be out of bounds as oob checking is made for 2-pixel particles + if (size == 0) { //single pixel particle, can be out of bounds as oob checking is made for 2-pixel particles (and updating it uses more code) uint32_t x = particles[particleindex].x >> PS_P_RADIUS_SHIFT_1D; if (x <= maxXpixel) { //by making x unsigned there is no need to check < 0 as it will overflow if (framebuffer) @@ -1606,7 +1526,7 @@ void ParticleSystem1D::renderParticle(CRGB *framebuffer, const uint32_t particle uint32_t offset = 4; // offset to zero coordinate to write/read data in renderbuffer (actually needs to be 3, is decremented in the loop below) uint32_t blurpasses = size/64 + 1; // number of blur passes depends on size, four passes max uint32_t bitshift = 0; - for(int i = 0; i < blurpasses; i++) { + for(uint32_t i = 0; i < blurpasses; i++) { if (i == 2) //for the last two passes, use higher amount of blur (results in a nicer brightness gradient with soft edges) bitshift = 1; rendersize += 2; @@ -1649,43 +1569,26 @@ void ParticleSystem1D::renderParticle(CRGB *framebuffer, const uint32_t particle } // detect collisions in an array of particles and handle them -void ParticleSystem1D::handleCollisions() -{ - // detect and handle collisions +void ParticleSystem1D::handleCollisions() { uint32_t i, j; - uint32_t startparticle = 0; - uint32_t endparticle = usedParticles;// >> 1; // do half the particles, significantly speeds things up - // 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 more accurate collisions are needed, just call it twice in a row - /*if (SEGMENT.call & 0x01) //every other frame, do the other half - { - startparticle = endparticle; - endparticle = usedParticles; - } */ int32_t collisiondistance = PS_P_MINHARDRADIUS_1D; - for (i = startparticle; i < endparticle; i++) - { + for (i = 0; i < usedParticles; i++) { // go though all 'higher number' particles and see if any of those are in close proximity and if they are, make them collide - if (particles[i].ttl > 0 && particles[i].outofbounds == 0 && particles[i].collide) // if particle is alive and does collide and is not out of view - { + if (particles[i].ttl > 0 && particles[i].outofbounds == 0 && particles[i].collide) { // if particle is alive and does collide and is not out of view int32_t dx; // distance to other particles - for (j = i + 1; j < usedParticles; j++) // check against higher number particles - { - if (particles[j].ttl > 0 && particles[j].collide) // if target particle is alive - { - if (advPartProps) // use advanced size properties - { - collisiondistance = PS_P_MINHARDRADIUS_1D + (((uint32_t)advPartProps[i].size + (uint32_t)advPartProps[j].size)>>1); + for (j = i + 1; j < usedParticles; j++) { // check against higher number particles + if (particles[j].ttl > 0 && particles[j].collide) { // if target particle is alive and collides + if (advPartProps) { // use advanced size properties + collisiondistance = PS_P_MINHARDRADIUS_1D + (((uint32_t)advPartProps[i].size + (uint32_t)advPartProps[j].size) >> 1); } dx = particles[j].x - particles[i].x; - int32_t dv = (int32_t)particles[j].vx - (int32_t)particles[i].vx; + int32_t dv = (int32_t)particles[j].vx - (int32_t)particles[i].vx; int32_t proximity = collisiondistance; - if (dv >= proximity) //particles would go past each other in next move upate - proximity += abs(dv); //add speed difference to catch fast particles - if (dx < proximity && dx > -proximity) // check if close - { - collideParticles(&particles[i], &particles[j], dx, dv, collisiondistance); + if (dv >= proximity) // particles would go past each other in next move update + proximity += abs(dv); // add speed difference to catch fast particles + if (dx < proximity && dx > -proximity) { // check if close + collideParticles(&particles[i], &particles[j], dx, dv, collisiondistance); } } } @@ -1695,32 +1598,23 @@ void ParticleSystem1D::handleCollisions() // handle a collision if close proximity is detected, i.e. dx and/or dy smaller than 2*PS_P_RADIUS // takes two pointers to the particles to collide and the particle hardness (softer means more energy lost in collision, 255 means full hard) -void ParticleSystem1D::collideParticles(PSparticle1D *particle1, PSparticle1D *particle2, int32_t dx, int32_t relativeVx, uint32_t collisiondistance) -{ - - // Calculate dot product of relative velocity and relative distance +void ParticleSystem1D::collideParticles(PSparticle1D *particle1, PSparticle1D *particle2, int32_t dx, int32_t relativeVx, uint32_t collisiondistance) { int32_t dotProduct = (dx * relativeVx); // is always negative if moving towards each other - // int32_t notsorandom = dotProduct & 0x01; //dotprouct LSB should be somewhat random, so no need to calculate a random number - //Serial.print(" dp"); Serial.print(dotProduct); - if (dotProduct < 0) // particles are moving towards each other - { - // integer math used to avoid floats. + if (dotProduct < 0) { // particles are moving towards each other + uint32_t surfacehardness = max(collisionHardness, (int32_t)PS_P_MINSURFACEHARDNESS_1D); // if particles are soft, the impulse must stay above a limit or collisions slip through + // TODO: if soft collisions are not needed, the above line can be done in set hardness function and skipped here (which is what it currently looks like) // Calculate new velocities after collision - uint32_t surfacehardness = collisionHardness < PS_P_MINSURFACEHARDNESS_1D ? PS_P_MINSURFACEHARDNESS_1D : collisionHardness; // if particles are soft, the impulse must stay above a limit or collisions slip through - //TODO: if soft collisions are not needed, the above line can be done in set hardness function and skipped here (which is what it currently looks like) - int32_t impulse = relativeVx * surfacehardness / 255; particle1->vx += impulse; particle2->vx -= impulse; - //if one of the particles is fixed, transfer the impulse back so it bounces + // if one of the particles is fixed, transfer the impulse back so it bounces if (particle1->fixed) particle2->vx = -particle1->vx; else if (particle2->fixed) particle1->vx = -particle2->vx; - if (collisionHardness < PS_P_MINSURFACEHARDNESS_1D) // if particles are soft, they become 'sticky' i.e. apply some friction (they do pile more nicely and correctly) - { + if (collisionHardness < PS_P_MINSURFACEHARDNESS_1D) { // if particles are soft, they become 'sticky' i.e. apply some friction (they do pile more nicely and correctly) const uint32_t coeff = collisionHardness + (255 - PS_P_MINSURFACEHARDNESS_1D); particle1->vx = ((int32_t)particle1->vx * coeff) / 255; particle2->vx = ((int32_t)particle2->vx * coeff) / 255; @@ -1731,45 +1625,32 @@ void ParticleSystem1D::collideParticles(PSparticle1D *particle1, PSparticle1D *p // particles have volume, push particles apart if they are too close // behaviour is different than in 2D, we need pixel accurate stacking here, push the top particle to full radius (direction is well defined in 1D) // also need to give the top particle some speed to counteract gravity or stacks just collapse - if (distance < collisiondistance) //particles are too close, push the upper particle away - { + if (distance < collisiondistance) { //particles are too close, push the upper particle away int32_t pushamount = 1 + ((collisiondistance - distance) >> 1); //add half the remaining distance note: this works best, if less or more is added, it gets more chaotic //int32_t pushamount = collisiondistance - distance; - if (particlesettings.useGravity) //using gravity, push the 'upper' particle only - { - if (dx < 0) // particle2.x < particle1.x - { - if (particle2->reversegrav && !particle2->fixed) - { - particle2->x -= pushamount; - particle2->vx--; - } - else if (!particle1->reversegrav && !particle1->fixed) - { - particle1->x += pushamount; - particle1->vx++; - } + if (particlesettings.useGravity) { //using gravity, push the 'upper' particle only + if (dx < 0) { // particle2.x < particle1.x + if (particle2->reversegrav && !particle2->fixed) { + particle2->x -= pushamount; + particle2->vx--; + } else if (!particle1->reversegrav && !particle1->fixed) { + particle1->x += pushamount; + particle1->vx++; + } + } else { + if (particle1->reversegrav && !particle1->fixed) { + particle1->x -= pushamount; + particle1->vx--; + } else if (!particle2->reversegrav && !particle2->fixed) { + particle2->x += pushamount; + particle2->vx++; } - else - { - if (particle1->reversegrav && !particle1->fixed) - { - particle1->x -= pushamount; - particle1->vx--; - } - else if (!particle2->reversegrav && !particle2->fixed) - { - particle2->x += pushamount; - particle2->vx++; - } } } - else //not using gravity, push both particles by applying a little velocity (like in 2D system), results in much nicer stacking when applying forces - { + else { //not using gravity, push both particles by applying a little velocity (like in 2D system), results in much nicer stacking when applying forces pushamount = 1; if (dx < 0) // particle2.x < particle1.x pushamount = -1; - particle1->vx -= pushamount; particle2->vx += pushamount; } @@ -1778,20 +1659,15 @@ void ParticleSystem1D::collideParticles(PSparticle1D *particle1, PSparticle1D *p // update size and pointers (memory location and size can change dynamically) // note: do not access the PS class in FX befor running this function (or it messes up SEGENV.data) -void ParticleSystem1D::updateSystem(void) -{ - // update size - setSize(SEGMENT.virtualLength()); +void ParticleSystem1D::updateSystem(void) { + setSize(SEGMENT.virtualLength()); // update size updatePSpointers(advPartProps != NULL); } // set the pointers for the class (this only has to be done once and not on every FX call, only the class pointer needs to be reassigned to SEGENV.data every time) // function returns the pointer to the next byte available for the FX (if it assigned more memory for other stuff using the above allocate function) // FX handles the PSsources, need to tell this function how many there are -void ParticleSystem1D::updatePSpointers(bool isadvanced) -{ - // DEBUG_PRINT(F("*** PS pointers ***")); - // DEBUG_PRINTF_P(PSTR("this PS %p "), this); +void ParticleSystem1D::updatePSpointers(bool isadvanced) { // Note on memory alignment: // a pointer MUST be 4 byte aligned. sizeof() in a struct/class is always aligned to the largest element. if it contains a 32bit, it will be padded to 4 bytes, 16bit is padded to 2byte alignment. // The PS is aligned to 4 bytes, a PSparticle is aligned to 2 and a struct containing only byte sized variables is not aligned at all and may need to be padded when dividing the memoryblock. @@ -1799,31 +1675,15 @@ void ParticleSystem1D::updatePSpointers(bool isadvanced) particles = reinterpret_cast(this + 1); // pointer to particle array at data+sizeof(ParticleSystem) sources = reinterpret_cast(particles + numParticles); // pointer to source(s) PSdataEnd = reinterpret_cast(sources + numSources); // pointer to first available byte after the PS for FX additional data - if (isadvanced) - { + if (isadvanced) { advPartProps = reinterpret_cast(sources + numSources); PSdataEnd = reinterpret_cast(advPartProps + numParticles); - //if (sizecontrol) - //{ - // advPartSize = reinterpret_cast(advPartProps + numParticles); - // PSdataEnd = reinterpret_cast(advPartSize + numParticles); - //} } - - /* - DEBUG_PRINTF_P(PSTR(" particles %p "), particles); - DEBUG_PRINTF_P(PSTR(" sources %p "), sources); - DEBUG_PRINTF_P(PSTR(" adv. props %p "), advPartProps); - DEBUG_PRINTF_P(PSTR(" adv. ctrl %p "), advPartSize); - DEBUG_PRINTF_P(PSTR("end %p\n"), PSdataEnd); - */ } - //non class functions to use for initialization -uint32_t calculateNumberOfParticles1D(bool isadvanced) -{ - uint32_t numberofParticles = SEGMENT.virtualLength(); // one particle per pixel (if possible) +uint32_t calculateNumberOfParticles1D(bool isadvanced) { + uint32_t numberofParticles = SEGMENT.virtualLength(); // one particle per pixel (if possible) #ifdef ESP8266 uint32_t particlelimit = ESP8266_MAXPARTICLES_1D; // maximum number of paticles allowed #elif ARDUINO_ARCH_ESP32S2 @@ -1839,8 +1699,7 @@ uint32_t calculateNumberOfParticles1D(bool isadvanced) return numberofParticles; } -uint32_t calculateNumberOfSources1D(uint8_t requestedsources) -{ +uint32_t calculateNumberOfSources1D(uint8_t requestedsources) { #ifdef ESP8266 int numberofSources = max(1, min((int)requestedsources,ESP8266_MAXSOURCES_1D)); // limit to 1 - 8 #elif ARDUINO_ARCH_ESP32S2 @@ -1854,8 +1713,7 @@ uint32_t calculateNumberOfSources1D(uint8_t requestedsources) } //allocate memory for particle system class, particles, sprays plus additional memory requested by FX -bool allocateParticleSystemMemory1D(uint16_t numparticles, uint16_t numsources, bool isadvanced, uint16_t additionalbytes) -{ +bool allocateParticleSystemMemory1D(uint16_t numparticles, uint16_t numsources, bool isadvanced, uint16_t additionalbytes) { uint32_t requiredmemory = sizeof(ParticleSystem1D); // functions above make sure these are a multiple of 4 bytes (to avoid alignment issues) requiredmemory += sizeof(PSparticle1D) * numparticles; @@ -1863,38 +1721,22 @@ bool allocateParticleSystemMemory1D(uint16_t numparticles, uint16_t numsources, requiredmemory += sizeof(PSadvancedParticle1D) * numparticles; requiredmemory += sizeof(PSsource1D) * numsources; requiredmemory += additionalbytes; - //Serial.print("allocating: "); - //Serial.print(requiredmemory); - //Serial.println("Bytes"); - //Serial.print("allocating for segment at"); - //Serial.println((uintptr_t)SEGENV.data); return(SEGMENT.allocateData(requiredmemory)); } // initialize Particle System, allocate additional bytes if needed (pointer to those bytes can be read from particle system class: PSdataEnd) // note: requestedparticles is relative, 127 = 50%, 255 = 100% (deafaults to 100% meaning one particle per pixel) -bool initParticleSystem1D(ParticleSystem1D *&PartSys, uint32_t requestedsources, uint32_t requestedparticles, uint16_t additionalbytes, bool advanced) -{ - //Serial.println("PS init function"); +bool initParticleSystem1D(ParticleSystem1D *&PartSys, uint32_t requestedsources, uint32_t requestedparticles, uint16_t additionalbytes, bool advanced) { uint32_t numparticles = (requestedparticles * calculateNumberOfParticles1D(advanced)) / 255; uint32_t numsources = calculateNumberOfSources1D(requestedsources); - //Serial.print("numsources: "); - //Serial.println(numsources); - if (!allocateParticleSystemMemory1D(numparticles, numsources, advanced, additionalbytes)) - { + if (!allocateParticleSystemMemory1D(numparticles, numsources, advanced, additionalbytes)) { DEBUG_PRINT(F("PS init failed: memory depleted")); return false; } - //Serial.print("SEGENV.data ptr"); - //Serial.println((uintptr_t)(SEGENV.data)); - //Serial.println("calling constructor"); PartSys = new (SEGENV.data) ParticleSystem1D(SEGMENT.virtualLength(), numparticles, numsources, advanced); // particle system constructor - //Serial.print("PS pointer at "); - //Serial.println((uintptr_t)PartSys); return true; } - // blur a 1D buffer, sub-size blurring can be done using start and size // for speed, 32bit variables are used, make sure to limit them to 8bit (0-255) or result is undefined // to blur a subset of the buffer, change the size and set start to the desired starting coordinates @@ -1913,11 +1755,9 @@ void blur1D(CRGB *colorbuffer, uint32_t size, uint32_t blur, uint32_t start) carryover = seeppart; } } - #endif // WLED_DISABLE_PARTICLESYSTEM1D - -#if !defined(WLED_DISABLE_PARTICLESYSTEM2D) || !defined(WLED_DISABLE_PARTICLESYSTEM1D) +#if !defined(WLED_DISABLE_PARTICLESYSTEM2D) || !defined(WLED_DISABLE_PARTICLESYSTEM1D) ////////////////////////////// // Shared Utility Functions // @@ -1925,8 +1765,7 @@ void blur1D(CRGB *colorbuffer, uint32_t size, uint32_t blur, uint32_t start) // calculate the delta speed (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 -static int32_t calcForce_dv(int8_t force, uint8_t* counter) -{ +static int32_t calcForce_dv(int8_t force, uint8_t* counter) { if (force == 0) return 0; // for small forces, need to use a delay counter @@ -1937,18 +1776,19 @@ static int32_t calcForce_dv(int8_t force, uint8_t* counter) *counter += force_abs; if (*counter > 15) { *counter -= 16; - dv = force < 0 ? -1 : 1; // force is either, 1 or -1 if it is small (zero force is handled above) + dv = force < 0 ? -1 : 1; // force is either 1 or -1 if it is small (zero force is handled above) } } else - dv = force / 16; // MSBs note: cannot use bitshift as dv can be negative + dv = force / 16; // MSBs, note: cannot use bitshift as dv can be negative return dv; } // limit speed to prevent overflows static int32_t limitSpeed(int32_t speed) { - return speed > PS_P_MAXSPEED ? PS_P_MAXSPEED : (speed < -PS_P_MAXSPEED ? -PS_P_MAXSPEED : speed); + return min((int32_t)PS_P_MAXSPEED, max((int32_t)-PS_P_MAXSPEED, speed)); + //return speed > PS_P_MAXSPEED ? PS_P_MAXSPEED : (speed < -PS_P_MAXSPEED ? -PS_P_MAXSPEED : speed); // note: this uses more code, not sure due to speed or inlining } // check if particle is out of bounds and wrap it around if required From 6e58404023b991ada8454022be6dfc3f6e2aa571 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Thu, 31 Oct 2024 09:04:17 +0100 Subject: [PATCH 135/219] Cleanup and some refactoring of particle FX - moved 2D / length check to PS initi - code reformating - removed some comments --- wled00/FX.cpp | 1796 ++++++++++++++--------------------- wled00/FXparticleSystem.cpp | 10 +- 2 files changed, 709 insertions(+), 1097 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 0df15364e3..d03c5776fb 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -16,7 +16,7 @@ #include "FX.h" #include "fcn_declare.h" -#if !defined(WLED_DISABLE_PARTICLESYSTEM2D) || !defined(WLED_DISABLE_PARTICLESYSTEM1D) +#if !defined(WLED_DISABLE_PARTICLESYSTEM2D) || !defined(WLED_DISABLE_PARTICLESYSTEM1D) #include "FXparticleSystem.h" #endif @@ -7796,37 +7796,33 @@ static const char _data_FX_MODE_2DWAVINGCELL[] PROGMEM = "Waving Cell@!,,Amplitu #ifndef WLED_DISABLE_PARTICLESYSTEM2D /* - * Particle System Vortex + * Particle System Vortex * Particles sprayed from center with a rotating spray * Uses palette for particle color * by DedeHai (Damian Schneider) */ #define NUMBEROFSOURCES 8 -uint16_t mode_particlevortex(void) -{ +uint16_t mode_particlevortex(void) { if (SEGLEN == 1) return mode_static(); ParticleSystem2D *PartSys = NULL; uint32_t i, j; - if (SEGMENT.call == 0) // initialization - { + if (SEGMENT.call == 0) { // initialization if (!initParticleSystem2D(PartSys, NUMBEROFSOURCES)) return mode_static(); // allocation failed - SEGENV.aux1 = 0x01; // check flags #ifdef ESP8266 - PartSys->setMotionBlur(150); + PartSys->setMotionBlur(180); #else - PartSys->setMotionBlur(100); + PartSys->setMotionBlur(130); #endif uint8_t numSprays = min(PartSys->numSources, (uint32_t)NUMBEROFSOURCES); - for (i = 0; i < numSprays; i++) - { + for (i = 0; i < numSprays; i++) { PartSys->sources[i].source.x = (PartSys->maxX + 1) >> 1; // center PartSys->sources[i].source.y = (PartSys->maxY + 1) >> 1; // center PartSys->sources[i].maxLife = 900; - PartSys->sources[i].minLife = 800; + PartSys->sources[i].minLife = 800; } PartSys->setKillOutOfBounds(true); } @@ -7835,59 +7831,48 @@ uint16_t mode_particlevortex(void) if (PartSys == NULL) return mode_static(); // something went wrong, no data! - + PartSys->updateSystem(); // update system properties (dimensions and data pointers) uint8_t spraycount = min(PartSys->numSources, (uint32_t)(1 + (SEGMENT.custom1 >> 5))); // number of sprays to display, 1-8 #ifdef ESP8266 - for (i = 1; i < 4; i++) // need static particles in the center to reduce blinking (would be black every other frame without this hack), just set them there fixed - { + for (i = 1; i < 4; i++) { // need static particles in the center to reduce blinking (would be black every other frame without this hack), just set them there fixed PartSys->particles[PartSys->numParticles - i].x = (PartSys->maxX + 1) >> 1; // center PartSys->particles[PartSys->numParticles - i].y = (PartSys->maxY + 1) >> 1; // center - PartSys->particles[PartSys->numParticles - i].sat = 230; + PartSys->particles[PartSys->numParticles - i].sat = 230; PartSys->particles[PartSys->numParticles - i].ttl = 255; //set alive } #endif - if (SEGMENT.check1 != (SEGENV.aux1 & 0x01) || SEGMENT.call == 0) // state change - { + if (SEGMENT.check1 != (SEGENV.aux1 & 0x01) || SEGMENT.call == 0) { // state change if (SEGMENT.check1) SEGENV.aux1 |= 0x01; //set the flag else SEGENV.aux1 &= ~0x01; // clear the flag - for (i = 0; i < spraycount; i++) - { + for (i = 0; i < spraycount; i++) { if (SEGMENT.check1) // random color is checked - { PartSys->sources[i].source.hue = random16(); - } - else - { + else { uint8_t coloroffset = 0xFF / spraycount; PartSys->sources[i].source.hue = coloroffset * i; } } } - - // set rotation direction and speed + // set rotation direction and speed // can use direction flag to determine current direction bool direction = SEGMENT.check2; //no automatic direction change, set it to flag int32_t currentspeed = (int32_t)SEGENV.step; // make a signed integer out of step - - if (SEGMENT.custom2 > 0) // automatic direction change enabled - { + + if (SEGMENT.custom2 > 0) { // automatic direction change enabled uint16_t changeinterval = 15 + 255 / SEGMENT.custom2; direction = SEGENV.aux1 & 0x02; //set direction according to flag if (SEGMENT.check3) // random interval - { changeinterval = 20 + changeinterval + random16(changeinterval); - } - if (SEGMENT.call % changeinterval == 0) //flip direction on next frame - { + if (SEGMENT.call % changeinterval == 0) { //flip direction on next frame SEGENV.aux1 |= 0x04; // set the update flag (for random interval update) - if (direction) - SEGENV.aux1 &= ~0x02; // clear the direction flag + if (direction) + SEGENV.aux1 &= ~0x02; // clear the direction flag else SEGENV.aux1 |= 0x02; // set the direction flag } @@ -7896,39 +7881,32 @@ uint16_t mode_particlevortex(void) int32_t targetspeed = (direction ? 1 : -1) * (SEGMENT.speed << 2); int32_t speeddiff = targetspeed - currentspeed; int32_t speedincrement = speeddiff / 50; - - if (speedincrement == 0) //if speeddiff is not zero, make the increment at least 1 so it reaches target speed - { + + if (speedincrement == 0) { //if speeddiff is not zero, make the increment at least 1 so it reaches target speed if(speeddiff < 0) speedincrement = -1; else if (speeddiff > 0) speedincrement = 1; } - + currentspeed += speedincrement; SEGENV.aux0 += currentspeed; SEGENV.step = (uint32_t)currentspeed; //save it back - // calculate angle offset for an even distribution - uint16_t angleoffset = 0xFFFF / spraycount; - uint32_t skip = PS_P_HALFRADIUS/(SEGMENT.intensity + 1) + 1; - if (SEGMENT.call % skip == 0) - { + uint16_t angleoffset = 0xFFFF / spraycount; // angle offset for an even distribution + uint32_t skip = PS_P_HALFRADIUS / (SEGMENT.intensity + 1) + 1; // intensity is emit speed, emit less on low speeds + if (SEGMENT.call % skip == 0) { j = random(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) - { + for (i = 0; i < spraycount; i++) { // emit one particle per spray (if available) PartSys->sources[j].var = (SEGMENT.custom3 >> 1); //update speed variation #ifdef ESP8266 if (SEGMENT.call & 0x01) // every other frame, do not emit to save particles #endif PartSys->angleEmit(PartSys->sources[j], SEGENV.aux0 + angleoffset * j, (SEGMENT.intensity >> 2)+1); - //PartSys->sprayEmit(PartSys->sources[j]); j = (j + 1) % spraycount; } } PartSys->update(); //update all particles and render to frame - - SEGMENT.blur(50); //TODO: put this in particle system for faster rendering return FRAMETIME; } #undef NUMBEROFSOURCES @@ -7941,24 +7919,20 @@ static const char _data_FX_MODE_PARTICLEVORTEX[] PROGMEM = "PS Vortex@Rotation S * by DedeHai (Damian Schneider) */ #define NUMBEROFSOURCES 4 -uint16_t mode_particlefireworks(void) -{ - if (SEGLEN == 1) - return mode_static(); +uint16_t mode_particlefireworks(void) { ParticleSystem2D *PartSys = NULL; uint8_t numRockets; uint32_t i = 0; uint32_t j = 0; - if (SEGMENT.call == 0) // initialization - { - if (!initParticleSystem2D(PartSys, NUMBEROFSOURCES, true)) // init with advanced particle properties + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem2D(PartSys, NUMBEROFSOURCES, true)) return mode_static(); // allocation failed - PartSys->setKillOutOfBounds(true); //out of bounds particles dont return (except on top, taken care of by gravity setting) - PartSys->setWallHardness(100); //ground bounce is fixed + + PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting) + PartSys->setWallHardness(100); // ground bounce is fixed numRockets = min(PartSys->numSources, (uint32_t)NUMBEROFSOURCES); - for (j = 0; j < numRockets; j++) - { + for (j = 0; j < numRockets; j++) { PartSys->sources[j].source.ttl = 500 * j; // first rocket starts immediately, others follow soon PartSys->sources[j].source.vy = -1; // at negative speed, no particles are emitted and if rocket dies, it will be relaunched } @@ -7967,14 +7941,14 @@ uint16_t mode_particlefireworks(void) PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS if (PartSys == NULL) - return mode_static(); // something went wrong, no data! - + return mode_static(); // something went wrong, no data! + PartSys->updateSystem(); // update system properties (dimensions and data pointers) numRockets = min(PartSys->numSources, (uint32_t)NUMBEROFSOURCES); PartSys->setWrapX(SEGMENT.check1); PartSys->setBounceY(SEGMENT.check2); - PartSys->setGravity(map(SEGMENT.custom3,0,31,0,10)); + PartSys->setGravity(map(SEGMENT.custom3, 0, 31, 0, 10)); // check each rocket's state and emit particles according to its state: moving up = emit exhaust, at top = explode; falling down = standby time uint32_t emitparticles; // number of particles to emit for each rocket's state @@ -7988,27 +7962,24 @@ uint16_t mode_particlefireworks(void) uint8_t percircle; uint8_t speedvariation; bool circularexplosion = false; - for (j = 0; j < numRockets; j++) - { + for (j = 0; j < numRockets; j++) { // determine rocket state by its speed: - if (PartSys->sources[j].source.vy > 0) - { // moving up, emit exhaust + if (PartSys->sources[j].source.vy > 0) { + // moving up, emit exhaust emitparticles = 1; } - else if (PartSys->sources[j].source.vy < 0) - { // falling down + else if (PartSys->sources[j].source.vy < 0) { + // falling down emitparticles = 0; } - else // speed is zero, explode! - { + else { // speed is zero, explode! #ifdef ESP8266 emitparticles = random16(SEGMENT.intensity >> 3) + (SEGMENT.intensity >> 3) + 5; // defines the size of the explosion #else emitparticles = random16(SEGMENT.intensity >> 2) + (SEGMENT.intensity >> 2) + 5; // defines the size of the explosion #endif PartSys->sources[j].source.vy = -1; // set speed negative so it will emit no more particles after this explosion until relaunch - if(random16(4) == 0) - { + if (random16(4) == 0) { circularexplosion = true; speed = 2 + random16(3); currentspeed = speed; @@ -8020,84 +7991,70 @@ uint16_t mode_particlefireworks(void) percircle = (uint16_t)0xFFFF / angleincrement + 1; int circles = (SEGMENT.intensity >> 6) + 1; emitparticles = percircle * circles; - PartSys->sources[j].var = 0; //no variation for nicer circles + PartSys->sources[j].var = 0; // no variation for nicer circles } } - for (i = 0; i < emitparticles; i++) - { + for (i = 0; i < emitparticles; i++) { if (circularexplosion) // do circle emit { if (counter & 0x01) // make every second particle a lower speed currentspeed = speed - speedvariation; else currentspeed = speed; - PartSys->angleEmit(PartSys->sources[j], angle, currentspeed); //note: compiler warnings can be ignored, variables are set just above + PartSys->angleEmit(PartSys->sources[j], angle, currentspeed); // note: compiler warnings can be ignored, variables are set just above counter++; if (counter > percircle) // full circle completed, increase speed { counter = 0; - speed += 5; //increase speed to form a second circle + speed += 5; // increase speed to form a second circle speedvariation = speedvariation ? speedvariation + random16(4) : 0; // double speed variation PartSys->sources[j].source.hue = random16(); // new color for next circle - PartSys->sources[j].source.sat = min((uint16_t)150,random16()); + PartSys->sources[j].source.sat = min((uint16_t)150, random16()); } angle += angleincrement; // set angle for next particle } - else - { - /* - - if( PartSys->sources[j].source.vy < 0) //explosion is ongoing - { - if(i < (emitparticles>>2)) //set 1/4 of particles to larger size //TODO: this does not look good. adjust or remove completely - PartSys->sources[j].size = 50+random16(140); - else - PartSys->sources[j].size = 0; - }*/ + else { PartSys->sprayEmit(PartSys->sources[j]); - if ((j % 3) == 0) - { - PartSys->sources[j].source.hue = random16(); // random color for each particle (this is also true for exhaust, but that is white anyways) - // PartSys->sources[j].source.sat = min((uint16_t)150, random16()); //dont change saturation, this can also be exhaust! + if ((j % 3) == 0) { + PartSys->sources[j].source.hue = random16(); // random color for each particle (this is also true for exhaust, but that is white anyways) + // PartSys->sources[j].source.sat = min((uint16_t)150, random16()); // dont change saturation, this can also be exhaust! } } } - if(i == 0) //no particles emitted, this rocket is falling + if (i == 0) // no particles emitted, this rocket is falling PartSys->sources[j].source.y = 1000; // set position up high so gravity wont pull it to the ground and bounce it (vy MUST stay negative until relaunch) circularexplosion = false; // reset for next rocket } // update the rockets, set the speed state - for (j = 0; j < numRockets; j++) - { - if (PartSys->sources[j].source.ttl) - { + for (j = 0; j < numRockets; j++) { + if (PartSys->sources[j].source.ttl) { PartSys->particleMoveUpdate(PartSys->sources[j].source); } else if (PartSys->sources[j].source.vy > 0) // rocket has died and is moving up. stop it so it will explode (is handled in the code above) { - PartSys->sources[j].source.vy = 0; // set speed to zero so code above will recognize this as an exploding rocket - PartSys->sources[j].source.hue = random16(); // random color - PartSys->sources[j].source.sat = random16(55) + 200; - PartSys->sources[j].maxLife = 200; - PartSys->sources[j].minLife = 100; - PartSys->sources[j].source.ttl = random16((1200 - ((uint32_t)SEGMENT.speed << 2))) + 550 - (SEGMENT.speed << 1); // standby time til next launch - PartSys->sources[j].var = ((SEGMENT.intensity >> 3) + 10) | 0x01; // speed variation around vx,vy (+/- var/2), only use odd nubmers - } - else if ( PartSys->sources[j].source.vy < 0) // rocket is exploded and time is up (ttl=0 and negative speed), relaunch it + PartSys->sources[j].source.vy = 0; // set speed to zero so code above will recognize this as an exploding rocket + PartSys->sources[j].source.hue = random16(); // random color + PartSys->sources[j].source.sat = random16(55) + 200; + PartSys->sources[j].maxLife = 200; + PartSys->sources[j].minLife = 100; + PartSys->sources[j].source.ttl = random16((1200 - ((uint32_t)SEGMENT.speed << 2))) + 550 - (SEGMENT.speed << 1); // standby time til next launch + PartSys->sources[j].var = ((SEGMENT.intensity >> 3) + 10) | 0x01; // speed variation around vx,vy (+/- var/2), only use odd numbers + } + else if (PartSys->sources[j].source.vy < 0) // rocket is exploded and time is up (ttl=0 and negative speed), relaunch it { // reinitialize rocket - PartSys->sources[j].source.y = PS_P_RADIUS<<1; // start from bottom - PartSys->sources[j].source.x = random(PartSys->maxX >> 2, PartSys->maxX >> 1); // centered half - PartSys->sources[j].source.vy = random16(SEGMENT.custom1 >> 3) + 5; // rocket speed depends also on rocket fuse - PartSys->sources[j].source.vx = random(-3,3); // not perfectly straight up - PartSys->sources[j].source.sat = 30; // low saturation -> exhaust is off-white - PartSys->sources[j].source.ttl = random16(SEGMENT.custom1) + (SEGMENT.custom1 >> 1); // sets explosion height (rockets explode at the top if set too high as paticle update set speed to zero if moving out of matrix) - PartSys->sources[j].maxLife = 40; // exhaust particle life - PartSys->sources[j].minLife = 10; - PartSys->sources[j].vx = 0; // emitting speed - PartSys->sources[j].vy = 0; // emitting speed - PartSys->sources[j].var = 3; // speed variation around vx,vy (+/- var/2) + PartSys->sources[j].source.y = PS_P_RADIUS << 1; // start from bottom + PartSys->sources[j].source.x = random(PartSys->maxX >> 2, PartSys->maxX >> 1); // centered half + PartSys->sources[j].source.vy = random16(SEGMENT.custom1 >> 3) + 5; // rocket speed depends also on rocket fuse + PartSys->sources[j].source.vx = random(-3, 3); // not perfectly straight up + PartSys->sources[j].source.sat = 30; // low saturation -> exhaust is off-white + PartSys->sources[j].source.ttl = random16(SEGMENT.custom1) + (SEGMENT.custom1 >> 1); // sets explosion height (rockets explode at the top if set too high as paticle update set speed to zero if moving out of matrix) + PartSys->sources[j].maxLife = 40; // exhaust particle life + PartSys->sources[j].minLife = 10; + PartSys->sources[j].vx = 0; // emitting speed + PartSys->sources[j].vy = 0; // emitting speed + PartSys->sources[j].var = 3; // speed variation around vx,vy (+/- var/2) } } @@ -8108,40 +8065,36 @@ uint16_t mode_particlefireworks(void) static const char _data_FX_MODE_PARTICLEFIREWORKS[] PROGMEM = "PS Fireworks@Launches,Explosion Size,Fuse,,Gravity,Cylinder,Ground,;;!;2;pal=11,sx=100,ix=50,c1=84,c2=128,c3=12,o1=0,o2=0,o3=0"; /* - * Particle Volcano + * Particle Volcano * Particles are sprayed from below, spray moves back and forth if option is set * Uses palette for particle color * by DedeHai (Damian Schneider) */ #define NUMBEROFSOURCES 1 -uint16_t mode_particlevolcano(void) -{ - if (SEGLEN == 1) - return mode_static(); +uint16_t mode_particlevolcano(void) { ParticleSystem2D *PartSys = NULL; - PSsettings2D volcanosettings; + PSsettings2D volcanosettings; volcanosettings.asByte = 0b00000100; // PS settings for volcano movement: bounceX is enabled uint8_t numSprays; // note: so far only one tested but more is possible uint32_t i = 0; - - if (SEGMENT.call == 0) // initialization - { + if (SEGMENT.call == 0) { // initialization if (!initParticleSystem2D(PartSys, NUMBEROFSOURCES)) // init, no additional data needed - return mode_static(); // allocation failed + return mode_static(); // allocation failed or not 2D + PartSys->setBounceY(true); PartSys->setGravity(); // enable with default gforce PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting) PartSys->setMotionBlur(190); // anable motion blur + numSprays = min(PartSys->numSources, (uint32_t)NUMBEROFSOURCES); // number of sprays - for (i = 0; i < numSprays; i++) - { - PartSys->sources[i].source.hue = random16(); - PartSys->sources[i].source.x = PartSys->maxX / (numSprays + 1) * (i + 1); // distribute evenly - PartSys->sources[i].maxLife = 300; // lifetime in frames - PartSys->sources[i].minLife = 250; - PartSys->sources[i].source.collide = true; // seeded particles will collide (if enabled) - PartSys->sources[i].source.perpetual = true; // source never dies + for (i = 0; i < numSprays; i++) { + PartSys->sources[i].source.hue = random16(); + PartSys->sources[i].source.x = PartSys->maxX / (numSprays + 1) * (i + 1); // distribute evenly + PartSys->sources[i].maxLife = 300; // lifetime in frames + PartSys->sources[i].minLife = 250; + PartSys->sources[i].source.collide = true; // seeded particles will collide (if enabled) + PartSys->sources[i].source.perpetual = true; // source never dies } } else @@ -8164,19 +8117,17 @@ uint16_t mode_particlevolcano(void) PartSys->enableParticleCollisions(false); // change source emitting color from time to time, emit one particle per spray - if (SEGMENT.call % (11 - (SEGMENT.intensity / 25)) == 0) // every nth frame, cycle color and emit particles (and update the sources) - { - for (i = 0; i < numSprays; i++) - { + if (SEGMENT.call % (11 - (SEGMENT.intensity / 25)) == 0) { // every nth frame, cycle color and emit particles (and update the sources) + for (i = 0; i < numSprays; i++) { PartSys->sources[i].source.y = PS_P_RADIUS + 5; // reset to just above the lower edge that is allowed for bouncing particles, if zero, particles already 'bounce' at start and loose speed. PartSys->sources[i].source.vy = 0; //reset speed (so no extra particlesettin is required to keep the source 'afloat') PartSys->sources[i].source.hue++; // = random16(); //change hue of spray source (note: random does not look good) PartSys->sources[i].source.vx = PartSys->sources[i].source.vx > 0 ? SEGMENT.custom1 >> 2 : -(SEGMENT.custom1 >> 2); // set moving speed but keep the direction given by PS PartSys->sources[i].vy = SEGMENT.speed >> 2; // emitting speed - PartSys->sources[i].vx = 0; + PartSys->sources[i].vx = 0; PartSys->sources[i].var = SEGMENT.custom3 >> 1; // emiting variation = nozzle size (custom 3 goes from 0-31) PartSys->sprayEmit(PartSys->sources[i]); - PartSys->particleMoveUpdate(PartSys->sources[i].source, &volcanosettings); //move the source + PartSys->particleMoveUpdate(PartSys->sources[i].source, &volcanosettings); //move the source } } @@ -8191,21 +8142,16 @@ static const char _data_FX_MODE_PARTICLEVOLCANO[] PROGMEM = "PS Volcano@Speed,In * realistic fire effect using particles. heat based and using perlin-noise for wind * by DedeHai (Damian Schneider) */ -uint16_t mode_particlefire(void) -{ - if (SEGLEN == 1) - return mode_static(); - +uint16_t mode_particlefire(void) { ParticleSystem2D *PartSys = NULL; uint32_t i; // index variable uint32_t numFlames; // number of flames: depends on fire width. for a fire width of 16 pixels, about 25-30 flames give good results - if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. - { + if (SEGMENT.call == 0) { // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. if (!initParticleSystem2D(PartSys, SEGMENT.virtualWidth(), 4)) //maximum number of source (PS may limit based on segment size); need 4 additional bytes for time keeping (uint32_t lastcall) - return mode_static(); // allocation failed; //allocation failed + return mode_static(); // allocation failed or not 2D SEGENV.aux0 = random16(); // aux0 is wind position (index) in the perlin noise - numFlames = PartSys->numSources; + numFlames = PartSys->numSources; } else PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS @@ -8217,9 +8163,8 @@ uint16_t mode_particlefire(void) PartSys->setWrapX(SEGMENT.check2); PartSys->setMotionBlur(SEGMENT.check1 * 180); // anable/disable motion blur - uint32_t firespeed = max((uint8_t)100, SEGMENT.speed); //limit speed to 100 minimum, reduce frame rate to make it slower (slower speeds than 100 do not look nice) - if (SEGMENT.speed < 100) //slow, limit FPS - { + uint32_t firespeed = max((uint8_t)100, SEGMENT.speed); //limit speed to 100 minimum, reduce frame rate to make it slower (slower speeds than 100 do not look nice) + if (SEGMENT.speed < 100) { //slow, limit FPS uint32_t *lastcall = reinterpret_cast(PartSys->PSdataEnd); uint32_t period = strip.now - *lastcall; if (period < (uint32_t)map(SEGMENT.speed, 0, 99, 50, 10)) // limit to 90FPS - 20FPS @@ -8234,33 +8179,30 @@ uint16_t mode_particlefire(void) uint32_t spread = (PartSys->maxX >> 5) * (SEGMENT.custom3 + 1); //fire around segment center (in subpixel points) numFlames = min((uint32_t)PartSys->numSources, (4 + ((spread / PS_P_RADIUS) << 1))); // number of flames used depends on spread with, good value is (fire width in pixel) * 2 - uint32_t percycle = (numFlames * 2) / 3;// / 2; // maximum number of particles emitted per cycle (TODO: for ESP826 maybe use flames/2) - // percycle = map(SEGMENT.intensity,0,255, 2, (numFlames*3) / 2); //TODO: does this give better flames or worse? + uint32_t percycle = (numFlames * 2) / 3; // maximum number of particles emitted per cycle (TODO: for ESP826 maybe use flames/2) // update the flame sprays: - for (i = 0; i < numFlames; i++) - { - if (SEGMENT.call & 1 && PartSys->sources[i].source.ttl > 0) // every second frame + for (i = 0; i < numFlames; i++) { + if (SEGMENT.call & 1 && PartSys->sources[i].source.ttl > 0) // every second frame { PartSys->sources[i].source.ttl--; } else // flame source is dead: initialize new flame: set properties of source { - PartSys->sources[i].source.x = (PartSys->maxX >> 1) - (spread>>1) + random(spread); // change flame position: distribute randomly on chosen width - PartSys->sources[i].source.y = -(PS_P_RADIUS<<2); // set the source below the frame - PartSys->sources[i].source.ttl = 20 + random((SEGMENT.custom1 * SEGMENT.custom1) >> 8) / (1 + (firespeed >> 5)); //'hotness' of fire, faster flames reduce the effect or flame height will scale too much with speed - PartSys->sources[i].maxLife = random(SEGMENT.virtualHeight() / 2) + 16; // defines flame height together with the vy speed, vy speed*maxlife/PS_P_RADIUS is the average flame height - PartSys->sources[i].minLife = PartSys->sources[i].maxLife >> 1; - PartSys->sources[i].vx = random16(4) - 2; // emitting speed (sideways) - PartSys->sources[i].vy = (SEGMENT.virtualHeight() >> 1) + (firespeed >> 4) + (SEGMENT.custom1>>4); // emitting speed (upwards) - PartSys->sources[i].var = 2 + random16(2 + (firespeed >> 4)); // speed variation around vx,vy (+/- var) + PartSys->sources[i].source.x = (PartSys->maxX >> 1) - (spread >> 1) + random(spread); // change flame position: distribute randomly on chosen width + PartSys->sources[i].source.y = -(PS_P_RADIUS << 2); // set the source below the frame + PartSys->sources[i].source.ttl = 20 + random((SEGMENT.custom1 * SEGMENT.custom1) >> 8) / (1 + (firespeed >> 5)); //'hotness' of fire, faster flames reduce the effect or flame height will scale too much with speed + PartSys->sources[i].maxLife = random(SEGMENT.virtualHeight() / 2) + 16; // defines flame height together with the vy speed, vy speed*maxlife/PS_P_RADIUS is the average flame height + PartSys->sources[i].minLife = PartSys->sources[i].maxLife >> 1; + PartSys->sources[i].vx = random16(4) - 2; // emitting speed (sideways) + PartSys->sources[i].vy = (SEGMENT.virtualHeight() >> 1) + (firespeed >> 4) + (SEGMENT.custom1 >> 4); // emitting speed (upwards) + PartSys->sources[i].var = 2 + random16(2 + (firespeed >> 4)); // speed variation around vx,vy (+/- var) } } - - if (SEGMENT.call % 3 == 0) // update noise position and add wind - { + + if (SEGMENT.call % 3 == 0) { // update noise position and add wind SEGENV.aux0++; // position in the perlin noise matrix for wind generation - if (SEGMENT.call % 10 == 0) + if (SEGMENT.call % 10 == 0) SEGENV.aux1++; // move in noise y direction so noise does not repeat as often // add wind force to all particles int8_t windspeed = ((int16_t)(inoise8(SEGENV.aux0, SEGENV.aux1) - 127) * SEGMENT.custom2) >> 7; @@ -8268,29 +8210,23 @@ uint16_t mode_particlefire(void) } SEGENV.step++; - if (SEGMENT.check3) //add turbulance (parameters and algorithm found by experimentation) - { - if (SEGMENT.call % map(firespeed,0,255,4,15)==0) - { - for (i = 0; i < PartSys->usedParticles; i++) - { - if (PartSys->particles[i].y < PartSys->maxY/4) // do not apply turbulance everywhere -> bottom quarter seems a good balance - { - int32_t curl = ((int32_t)inoise8(PartSys->particles[i].x, PartSys->particles[i].y , SEGENV.step << 4) - 127); - PartSys->particles[i].vx += (curl * (firespeed + 10)) >> 9; - + if (SEGMENT.check3) { //add turbulance (parameters and algorithm found by experimentation) + if (SEGMENT.call % map(firespeed, 0, 255, 4, 15) == 0) { + for (i = 0; i < PartSys->usedParticles; i++) { + if (PartSys->particles[i].y < PartSys->maxY / 4) { // do not apply turbulance everywhere -> bottom quarter seems a good balance + int32_t curl = ((int32_t)inoise8(PartSys->particles[i].x, PartSys->particles[i].y, SEGENV.step << 4) - 127); + PartSys->particles[i].vx += (curl * (firespeed + 10)) >> 9; } } } } uint8_t j = random16(); // start with a random flame (so each flame gets the chance to emit a particle if available particles is smaller than number of flames) - for(i=0; i < percycle; i++) - { + for (i = 0; i < percycle; i++) { j = (j + 1) % numFlames; PartSys->flameEmit(PartSys->sources[j]); } - + PartSys->updateFire(SEGMENT.intensity); // update and render the fire return FRAMETIME; @@ -8304,63 +8240,53 @@ 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_particlepit(void) -{ - if (SEGLEN == 1) - return mode_static(); +uint16_t mode_particlepit(void) { ParticleSystem2D *PartSys = NULL; - if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. - { + if (SEGMENT.call == 0) { // initialization if (!initParticleSystem2D(PartSys, 1, 0, true)) // init, request one source (actually dont really need one TODO: test if using zero sources also works) - return mode_static(); // allocation failed; //allocation failed + return mode_static(); // allocation failed or not 2D PartSys->setKillOutOfBounds(true); PartSys->setGravity(); // enable with default gravity - PartSys->setUsedParticles((PartSys->numParticles*3)/2); // use 2/3 of available particles + PartSys->setUsedParticles((PartSys->numParticles * 3) / 2); // use 2/3 of available particles } else PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS - if (PartSys == NULL) - return mode_static(); // something went wrong, no data! - + return mode_static(); // something went wrong, no data! + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setWrapX(SEGMENT.check1); PartSys->setBounceX(SEGMENT.check2); PartSys->setBounceY(SEGMENT.check3); PartSys->setWallHardness(min(SEGMENT.custom2, (uint8_t)150)); // limit to 100 min (if collisions are disabled, still want bouncy) - if (SEGMENT.custom2>0) - { + if (SEGMENT.custom2 > 0) { PartSys->enableParticleCollisions(true, SEGMENT.custom2); // enable collisions and set particle collision hardness } - else{ + else { PartSys->enableParticleCollisions(false); } uint32_t i; - if (SEGMENT.call % (128 - (SEGMENT.intensity >> 1)) == 0 && SEGMENT.intensity > 0) // every nth frame emit particles, stop emitting if set to zero - { - for (i = 0; i < PartSys->usedParticles; i++) // emit particles - { - if (PartSys->particles[i].ttl == 0) // find a dead particle - { + if (SEGMENT.call % (128 - (SEGMENT.intensity >> 1)) == 0 && SEGMENT.intensity > 0) { // every nth frame emit particles, stop emitting if set to zero + for (i = 0; i < PartSys->usedParticles; i++) { // emit particles + if (PartSys->particles[i].ttl == 0) { // find a dead particle // emit particle at random position over the top of the matrix (random16 is not random enough) PartSys->particles[i].ttl = 1500 - (SEGMENT.speed << 2) + random16(500); // if speed is higher, make them die sooner PartSys->particles[i].x = random(PartSys->maxX); //random(PartSys->maxX >> 1) + (PartSys->maxX >> 2); - PartSys->particles[i].y = (PartSys->maxY<<1); // particles appear somewhere above the matrix, maximum is double the height - PartSys->particles[i].vx = (int16_t)random(SEGMENT.speed >> 1) - (SEGMENT.speed >> 2); // side speed is +/- + PartSys->particles[i].y = (PartSys->maxY << 1); // particles appear somewhere above the matrix, maximum is double the height + PartSys->particles[i].vx = (int16_t)random(SEGMENT.speed >> 1) - (SEGMENT.speed >> 2); // side speed is +/- PartSys->particles[i].vy = map(SEGMENT.speed, 0, 255, -5, -100); // downward speed PartSys->particles[i].hue = random16(); // set random color PartSys->particles[i].collide = true; // enable collision for particle PartSys->particles[i].sat = ((SEGMENT.custom3) << 3) + 7; // set particle size - if(SEGMENT.custom1 == 255) - { + if (SEGMENT.custom1 == 255) { PartSys->setParticleSize(0); // set global size to zero PartSys->advPartProps[i].size = random(SEGMENT.custom1); // set each particle to random size } - else - { + else { PartSys->setParticleSize(SEGMENT.custom1); // set global size PartSys->advPartProps[i].size = 0; // use global size } @@ -8368,7 +8294,7 @@ uint16_t mode_particlepit(void) } } } - + uint32_t frictioncoefficient = 1 + SEGMENT.check1; //need more friction if wrapX is set, see below note if (SEGMENT.speed < 50) // for low speeds, apply more friction frictioncoefficient = 50 - SEGMENT.speed; @@ -8391,19 +8317,19 @@ uint16_t mode_particlewaterfall(void) { if (SEGLEN == 1) return mode_static(); +uint16_t mode_particlewaterfall(void) { ParticleSystem2D *PartSys = NULL; uint8_t numSprays; uint32_t i = 0; - if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. - { + if (SEGMENT.call == 0) { // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. if (!initParticleSystem2D(PartSys, 12)) // init, request 12 sources, no additional data needed - return mode_static(); // allocation failed; //allocation failed + return mode_static(); // allocation failed or not 2D + PartSys->setGravity(); // enable with default gforce PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting) PartSys->setMotionBlur(190); // anable motion blur - for (i = 0; i < PartSys->numSources; i++) - { + for (i = 0; i < PartSys->numSources; i++) { PartSys->sources[i].source.hue = random16(); PartSys->sources[i].source.collide = true; // seeded particles will collide #ifdef ESP8266 @@ -8417,10 +8343,9 @@ uint16_t mode_particlewaterfall(void) } else PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS - if (PartSys == NULL) return mode_static(); // something went wrong, no data! (TODO: ask how to handle this so it always works) - + // Particle System settings PartSys->updateSystem(); // update system properties (dimensions and data pointers) PartSys->setWrapX(SEGMENT.check1); // cylinder @@ -8430,30 +8355,25 @@ uint16_t mode_particlewaterfall(void) numSprays = min((int32_t)PartSys->numSources, max(PartSys->maxXpixel / 6, (int32_t)2)); // number of sprays depends on segment width if (SEGMENT.custom2 > 0) // collisions enabled PartSys->enableParticleCollisions(true, SEGMENT.custom2); // enable collisions and set particle collision hardness - else - { + else { PartSys->enableParticleCollisions(false); PartSys->setWallHardness(120); // set hardness (for ground bounce) to fixed value if not using collisions } - for (i = 0; i < numSprays; i++) - { + for (i = 0; i < numSprays; i++) { PartSys->sources[i].source.hue++; // change hue of spray source } - if (SEGMENT.call % (9 - (SEGMENT.intensity >> 5)) == 0 && SEGMENT.intensity > 0) // every nth frame, cycle color and emit particles, do not emit if intensity is zero - { - for (i = 0; i < numSprays; i++) - { + if (SEGMENT.call % (9 - (SEGMENT.intensity >> 5)) == 0 && SEGMENT.intensity > 0) { // every nth frame, cycle color and emit particles, do not emit if intensity is zero + for (i = 0; i < numSprays; i++) { PartSys->sources[i].vy = -SEGMENT.speed >> 3; // emitting speed, down //PartSys->sources[i].source.x = map(SEGMENT.custom3, 0, 31, 0, (PartSys->maxXpixel - numSprays * 2) * PS_P_RADIUS) + i * PS_P_RADIUS * 2; // emitter position PartSys->sources[i].source.x = map(SEGMENT.custom3, 0, 31, 0, (PartSys->maxXpixel - numSprays) * PS_P_RADIUS) + i * PS_P_RADIUS * 2; // emitter position PartSys->sources[i].source.y = PartSys->maxY + (PS_P_RADIUS * ((i<<2) + 4)); // source y position, few pixels above the top to increase spreading before entering the matrix PartSys->sources[i].var = (SEGMENT.custom1 >> 3); // emiting variation 0-32 - PartSys->sprayEmit(PartSys->sources[i]); + PartSys->sprayEmit(PartSys->sources[i]); } } - if (SEGMENT.call % 20 == 0) PartSys->applyFriction(1); // add just a tiny amount of friction to help smooth things @@ -8468,28 +8388,22 @@ Particle Box, applies gravity to particles in either a random direction or rando Uses palette for particle color by DedeHai (Damian Schneider) */ -uint16_t mode_particlebox(void) -{ - - if (SEGLEN == 1) - return mode_static(); +uint16_t mode_particlebox(void) { ParticleSystem2D *PartSys = NULL; uint32_t i; - if (SEGMENT.call == 0) // initialization - { + if (SEGMENT.call == 0) { // initialization if (!initParticleSystem2D(PartSys, 1)) // init - return mode_static(); // allocation failed + return mode_static(); // allocation failed or not 2D PartSys->setBounceX(true); PartSys->setBounceY(true); // set max number of particles and save to aux1 for later #ifdef ESP8266 - SEGENV.aux1 = min((uint32_t)((PartSys->maxXpixel * PartSys->maxYpixel) >> 1), PartSys->numParticles); + SEGENV.aux1 = min((uint32_t)((PartSys->maxXpixel * PartSys->maxYpixel) >> 1), PartSys->numParticles); #else SEGENV.aux1 = min((uint32_t)((PartSys->maxXpixel * PartSys->maxYpixel)), PartSys->numParticles); // max number of particles #endif - for (i = 0; i < SEGENV.aux1; i++) - { + for (i = 0; i < SEGENV.aux1; i++) { PartSys->particles[i].ttl = 500; // set all particles alive (not all are rendered though) PartSys->particles[i].perpetual = true; // never die PartSys->particles[i].hue = i * 3; // color range @@ -8503,52 +8417,39 @@ uint16_t mode_particlebox(void) PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS if (PartSys == NULL) - return mode_static(); // something went wrong, no data! + return mode_static(); // something went wrong, no data! PartSys->updateSystem(); // update system properties (dimensions and data pointers) - + PartSys->setWallHardness(min(SEGMENT.custom2, (uint8_t)200)); // wall hardness is 200 or more PartSys->enableParticleCollisions(true, max(2, (int)SEGMENT.custom2)); // enable collisions and set particle collision hardness PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 10, SEGENV.aux1)); // aux1 holds max number of particles to use - - if (SEGMENT.call % (((255 - SEGMENT.speed) >> 6) + 1) == 0 && SEGMENT.speed > 0) // how often the force is applied depends on speed setting - { + if (SEGMENT.call % (((255 - SEGMENT.speed) >> 6) + 1) == 0 && SEGMENT.speed > 0) { // how often the force is applied depends on speed setting int32_t xgravity; int32_t ygravity; int32_t increment = (SEGMENT.speed >> 6) + 1; - - /*if(SEGMENT.check2) // direction - SEGENV.aux0 += increment; // update counter - else - SEGENV.aux0 -= increment; - */ - - if(SEGMENT.check2) // washing machine - { + + if (SEGMENT.check2) { // washing machine int speed = tristate_square8(strip.now >> 7, 90, 15) / ((400 - SEGMENT.speed) >> 3); SEGENV.aux0 += speed; - if(speed == 0) SEGENV.aux0 = 190; //down (= 270°) + if(speed == 0) SEGENV.aux0 = 190; //down (= 270°) } else SEGENV.aux0 -= increment; - - if(SEGMENT.check1) // random, use perlin noise - { - xgravity = ((int16_t)inoise8(SEGENV.aux0) - 127); + if (SEGMENT.check1) { // random, use perlin noise + xgravity = ((int16_t)inoise8(SEGENV.aux0) - 127); ygravity = ((int16_t)inoise8(SEGENV.aux0 + 10000) - 127); - // scale the gravity force - xgravity = (xgravity * SEGMENT.custom1) / 128; + // scale the gravity force + xgravity = (xgravity * SEGMENT.custom1) / 128; ygravity = (ygravity * SEGMENT.custom1) / 128; } - else // go in a circle - { + else { // go in a circle xgravity = ((int32_t)(SEGMENT.custom1) * cos16(SEGENV.aux0 << 8)) / 0xFFFF; ygravity = ((int32_t)(SEGMENT.custom1) * sin16(SEGENV.aux0 << 8)) / 0xFFFF; } - if (SEGMENT.check3) // sloshing, y force is alwys downwards - { + if (SEGMENT.check3) { // sloshing, y force is always downwards if(ygravity > 0) ygravity = -ygravity; } @@ -8567,23 +8468,20 @@ static const char _data_FX_MODE_PARTICLEBOX[] PROGMEM = "PS Box@Speed,Particles, /* 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 +calculates slope gradient at the particle positions and applies 'downhill' force, resulting in a fuzzy perlin noise display by DedeHai (Damian Schneider) */ -uint16_t mode_particleperlin(void) -{ - if (SEGLEN == 1) - return mode_static(); +uint16_t mode_particleperlin(void) { ParticleSystem2D *PartSys = NULL; uint32_t i; - if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. - { + + if (SEGMENT.call == 0) { // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. if (!initParticleSystem2D(PartSys, 1, 0, true)) // init with 1 source and advanced properties - return mode_static(); // allocation failed; //allocation failed + return mode_static(); // allocation failed or not 2D + PartSys->setKillOutOfBounds(true); // should never happen, but lets make sure there are no stray particles SEGENV.aux0 = rand(); - for (i = 0; i < PartSys->numParticles; i++) - { + for (i = 0; i < PartSys->numParticles; i++) { PartSys->particles[i].collide = true; // all particles colllide } } @@ -8592,44 +8490,41 @@ uint16_t mode_particleperlin(void) if (PartSys == NULL) return mode_static(); // something went wrong, no data! - + PartSys->updateSystem(); // update system properties (dimensions and data pointers) PartSys->setWrapX(SEGMENT.check1); PartSys->setBounceX(!SEGMENT.check1); PartSys->setBounceY(true); PartSys->setWallHardness(SEGMENT.custom1); // wall hardness PartSys->enableParticleCollisions(SEGMENT.check3, SEGMENT.custom1); // enable collisions and set particle collision hardness - uint32_t displayparticles = map(SEGMENT.intensity, 0, 255, 10, PartSys->numParticles>>1); + uint32_t displayparticles = map(SEGMENT.intensity, 0, 255, 10, PartSys->numParticles >> 1); PartSys->setUsedParticles(displayparticles); PartSys->setMotionBlur(230); // anable motion blur // apply 'gravity' from a 2D perlin noise map SEGENV.aux0 += 1 + (SEGMENT.speed >> 5); // noise z-position // update position in noise - for (i = 0; i < displayparticles; i++) - { - if (PartSys->particles[i].ttl == 0) // revive dead particles (do not keep them alive forever, they can clump up, need to reseed) - { - PartSys->particles[i].ttl = random16(500) + 200; - PartSys->particles[i].x = random(PartSys->maxX); - PartSys->particles[i].y = random(PartSys->maxY); + for (i = 0; i < displayparticles; i++) { + if (PartSys->particles[i].ttl == 0) { // revive dead particles (do not keep them alive forever, they can clump up, need to reseed) + PartSys->particles[i].ttl = random16(500) + 200; + PartSys->particles[i].x = random(PartSys->maxX); + PartSys->particles[i].y = random(PartSys->maxY); } uint32_t scale = 16 - ((31 - SEGMENT.custom3) >> 1); uint16_t xnoise = PartSys->particles[i].x / scale; // position in perlin noise, scaled by slider uint16_t ynoise = PartSys->particles[i].y / scale; int16_t baseheight = inoise8(xnoise, ynoise, SEGENV.aux0); // noise value at particle position PartSys->particles[i].hue = baseheight; // color particles to perlin noise value - if (SEGMENT.call % 8 == 0) // do not apply the force every frame, is too chaotic - { + if (SEGMENT.call % 8 == 0) { // do not apply the force every frame, is too chaotic int8_t xslope = (baseheight + (int16_t)inoise8(xnoise - 10, ynoise, SEGENV.aux0)); int8_t yslope = (baseheight + (int16_t)inoise8(xnoise, ynoise - 10, SEGENV.aux0)); - PartSys->applyForce(i, xslope, yslope); + PartSys->applyForce(i, xslope, yslope); } } if (SEGMENT.call % (16 - (SEGMENT.custom2 >> 4)) == 0) PartSys->applyFriction(2); - PartSys->update(); // update and render + PartSys->update(); // update and render return FRAMETIME; } static const char _data_FX_MODE_PARTICLEPERLIN[] PROGMEM = "PS Fuzzy Noise@Speed,Particles,Bounce,Friction,Scale,Cylinder,,Collisions;;!;2;pal=64,sx=50,ix=200,c1=130,c2=30,c3=5,o1=0,o3=1"; @@ -8639,26 +8534,21 @@ static const char _data_FX_MODE_PARTICLEPERLIN[] PROGMEM = "PS Fuzzy Noise@Speed * by DedeHai (Damian Schneider) */ #define NUMBEROFSOURCES 8 -uint16_t mode_particleimpact(void) -{ - if (SEGLEN == 1) - return mode_static(); +uint16_t mode_particleimpact(void) { ParticleSystem2D *PartSys = NULL; uint32_t i = 0; uint8_t MaxNumMeteors; - PSsettings2D meteorsettings; - meteorsettings.asByte = 0b00101000; // PS settings for meteors: bounceY and gravity enabled + PSsettings2D meteorsettings; + meteorsettings.asByte = 0b00101000; // PS settings for meteors: bounceY and gravity enabled - if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. - { + if (SEGMENT.call == 0) { // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. if (!initParticleSystem2D(PartSys, NUMBEROFSOURCES)) // init, no additional data needed - return mode_static(); // allocation failed; //allocation failed + return mode_static(); // allocation failed or not 2D PartSys->setKillOutOfBounds(false); // explosions out of frame ar allowed, set to true to save particles (TODO: better enable it in ESP8266?) PartSys->setGravity(); // enable default gravity PartSys->setBounceY(true); // always use ground bounce MaxNumMeteors = min(PartSys->numSources, (uint32_t)NUMBEROFSOURCES); - for (i = 0; i < MaxNumMeteors; i++) - { + for (i = 0; i < MaxNumMeteors; i++) { PartSys->sources[i].source.y = 500; PartSys->sources[i].source.ttl = random16(20 * i); // set initial delay for meteors PartSys->sources[i].source.vy = 10; // at positive speeds, no particles are emitted and if particle dies, it will be relaunched @@ -8674,18 +8564,16 @@ uint16_t mode_particleimpact(void) PartSys->updateSystem(); // update system properties (dimensions and data pointers) PartSys->setWrapX(SEGMENT.check1); PartSys->setBounceX(SEGMENT.check2); - PartSys->setWallHardness(SEGMENT.custom2); + PartSys->setWallHardness(SEGMENT.custom2); PartSys->enableParticleCollisions(SEGMENT.check3, SEGMENT.custom2); // enable collisions and set particle collision hardness MaxNumMeteors = min(PartSys->numSources, (uint32_t)NUMBEROFSOURCES); uint8_t numMeteors = map(SEGMENT.custom3, 0, 31, 1, MaxNumMeteors); // number of meteors to use for animation uint32_t emitparticles; // number of particles to emit for each rocket's state - - for (i = 0; i < numMeteors; i++) - { + + for (i = 0; i < numMeteors; i++) { // determine meteor state by its speed: - if ( PartSys->sources[i].source.vy < 0) // moving down, emit sparks - { + if ( PartSys->sources[i].source.vy < 0) { // moving down, emit sparks #ifdef ESP8266 emitparticles = 1; #else @@ -8693,11 +8581,8 @@ uint16_t mode_particleimpact(void) #endif } else if ( PartSys->sources[i].source.vy > 0) // moving up means meteor is on 'standby' - { emitparticles = 0; - } - else // speed is zero, explode! - { + else { // speed is zero, explode! PartSys->sources[i].source.vy = 10; // set source speed positive so it goes into timeout and launches again #ifdef ESP8266 emitparticles = random16(SEGMENT.intensity >> 3) + 5; // defines the size of the explosion @@ -8705,31 +8590,26 @@ uint16_t mode_particleimpact(void) emitparticles = map(SEGMENT.intensity, 0, 255, 10, random16(PartSys->numParticles>>2)); // defines the size of the explosion !!!TODO: check if this works on ESP8266, drop esp8266 def if it does #endif } - for (int e = emitparticles; e > 0; e--) - { + for (int e = emitparticles; e > 0; e--) { PartSys->sprayEmit(PartSys->sources[i]); } } // update the meteors, set the speed state - for (i = 0; i < numMeteors; i++) - { - if (PartSys->sources[i].source.ttl) - { + for (i = 0; i < numMeteors; i++) { + if (PartSys->sources[i].source.ttl) { PartSys->sources[i].source.ttl--; // note: this saves an if statement, but moving down particles age twice - if (PartSys->sources[i].source.vy < 0) //move down - { + if (PartSys->sources[i].source.vy < 0) { //move down PartSys->applyGravity(&PartSys->sources[i].source); PartSys->particleMoveUpdate(PartSys->sources[i].source, &meteorsettings); - + // if source reaches the bottom, set speed to 0 so it will explode on next function call (handled above) - if (PartSys->sources[i].source.y < PS_P_RADIUS<<1) // reached the bottom pixel on its way down - { + if (PartSys->sources[i].source.y < PS_P_RADIUS<<1) { // reached the bottom pixel on its way down PartSys->sources[i].source.vy = 0; // set speed zero so it will explode PartSys->sources[i].source.vx = 0; PartSys->sources[i].source.collide = true; #ifdef ESP8266 - PartSys->sources[i].maxLife = 130; + PartSys->sources[i].maxLife = 130; PartSys->sources[i].minLife = 20; PartSys->sources[i].source.ttl = random16(255 - (SEGMENT.speed>>1)) + 10; // standby time til next launch (in frames at 42fps, max of 265 is about 6 seconds #else @@ -8742,20 +8622,19 @@ uint16_t mode_particleimpact(void) } } } - else if (PartSys->sources[i].source.vy > 0) // meteor is exploded and time is up (ttl==0 and positive speed), relaunch it - { + else if (PartSys->sources[i].source.vy > 0) { // meteor is exploded and time is up (ttl==0 and positive speed), relaunch it // reinitialize meteor - PartSys->sources[i].source.y = PartSys->maxY + (PS_P_RADIUS << 2); // start 4 pixels above the top - PartSys->sources[i].source.x = random(PartSys->maxX); - PartSys->sources[i].source.vy = -random16(30) - 30; // meteor downward speed - PartSys->sources[i].source.vx = random(30) - 15; - PartSys->sources[i].source.hue = random16(); // random color - PartSys->sources[i].source.ttl = 500; // long life, will explode at bottom - PartSys->sources[i].source.collide = false; // trail particles will not collide - PartSys->sources[i].maxLife = 60; // spark particle life - PartSys->sources[i].minLife = 20; - PartSys->sources[i].vy = -9; // emitting speed (down) - PartSys->sources[i].var = 3; // speed variation around vx,vy (+/- var) + PartSys->sources[i].source.y = PartSys->maxY + (PS_P_RADIUS << 2); // start 4 pixels above the top + PartSys->sources[i].source.x = random(PartSys->maxX); + PartSys->sources[i].source.vy = -random16(30) - 30; // meteor downward speed + PartSys->sources[i].source.vx = random(30) - 15; + PartSys->sources[i].source.hue = random16(); // random color + PartSys->sources[i].source.ttl = 500; // long life, will explode at bottom + PartSys->sources[i].source.collide = false; // trail particles will not collide + PartSys->sources[i].maxLife = 60; // spark particle life + PartSys->sources[i].minLife = 20; + PartSys->sources[i].vy = -9; // emitting speed (down) + PartSys->sources[i].var = 3; // speed variation around vx,vy (+/- var) } } @@ -8772,19 +8651,14 @@ Uses palette for particle color by DedeHai (Damian Schneider) */ -uint16_t mode_particleattractor(void) -{ - if (SEGLEN == 1) - return mode_static(); +uint16_t mode_particleattractor(void) { ParticleSystem2D *PartSys = NULL; PSsettings2D sourcesettings; sourcesettings.asByte = 0b00001100; // PS settings for bounceY, bounceY used for source movement (it always bounces whereas particles do not) PSparticle *attractor; // particle pointer to the attractor - if (SEGMENT.call == 0) // initialization - { + if (SEGMENT.call == 0) { // initialization if (!initParticleSystem2D(PartSys, 1, 0, true)) // init using 1 source and advanced particle settings - return mode_static(); // allocation failed - //DEBUG_PRINTF_P(PSTR("sources in FX %p\n"), &PartSys->sources[0]); + return mode_static(); // allocation failed or not 2D PartSys->sources[0].source.hue = random16(); PartSys->sources[0].source.vx = -7; // will collied with wall and get random bounce direction PartSys->sources[0].source.collide = true; // seeded particles will collide @@ -8800,12 +8674,13 @@ uint16_t mode_particleattractor(void) PartSys->setWallHardness(255); //bounce forever PartSys->setWallRoughness(200); //randomize wall bounce } - else + else { PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + } if (PartSys == NULL) return mode_static(); // something went wrong, no data! - + // Particle System settings PartSys->updateSystem(); // update system properties (dimensions and data pointers) @@ -8816,60 +8691,57 @@ uint16_t mode_particleattractor(void) PartSys->enableParticleCollisions(true, map(SEGMENT.custom2, 1, 255, 120, 255)); // enable collisions and set particle collision hardness else PartSys->enableParticleCollisions(false); + uint16_t lastusedparticle = (PartSys->numParticles * 3) >> 2; // use 3/4 of particles uint32_t displayparticles = map(SEGMENT.intensity, 0, 255, 10, lastusedparticle); PartSys->setUsedParticles(displayparticles); + // set pointers attractor = &PartSys->particles[lastusedparticle + 1]; - if(SEGMENT.call == 0) - { + + if (SEGMENT.call == 0) { attractor->vx = PartSys->sources[0].source.vy; // set to spray movemement but reverse x and y attractor->vy = PartSys->sources[0].source.vx; attractor->ttl = 100; - attractor->perpetual = true; + attractor->perpetual = true; } + // set attractor properties - if (SEGMENT.check2) - { - if((SEGMENT.call % 3) == 0) // move slowly + if (SEGMENT.check2) { + if ((SEGMENT.call % 3) == 0) // move slowly PartSys->particleMoveUpdate(*attractor, &sourcesettings); // move the attractor - } - else{ + else { attractor->x = PartSys->maxX >> 1; // set to center attractor->y = PartSys->maxY >> 1; } - if (SEGMENT.call % 5 == 0) - PartSys->sources[0].source.hue++; - + + if (SEGMENT.call % 5 == 0) + PartSys->sources[0].source.hue++; + SEGENV.aux0 += 256; // emitting angle, one full turn in 255 frames (0xFFFF is 360°) if (SEGMENT.call % 2 == 0) // alternate direction of emit PartSys->angleEmit(PartSys->sources[0], SEGENV.aux0, 12); else PartSys->angleEmit(PartSys->sources[0], SEGENV.aux0 + 0x7FFF, 12); // emit at 180° as well // apply force - #ifdef USERMOD_AUDIOREACTIVE - uint32_t strength = SEGMENT.speed; + #ifdef USERMOD_AUDIOREACTIVE + uint32_t strength = SEGMENT.speed; um_data_t *um_data; - if(!SEGMENT.check3) //AR enabled - { - if(usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) - { + if(!SEGMENT.check3) { //AR enabled + if(usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { uint8_t volumeSmth = (uint8_t)(*(float*) um_data->u_data[0]); - uint32_t strength = volumeSmth; - for (uint32_t i = 0; i < PartSys->usedParticles; i++) // update particles - { - PartSys->pointAttractor(i, attractor, strength, false); - } + uint32_t strength = volumeSmth; + for (uint32_t i = 0; i < PartSys->usedParticles; i++) {// update particles + PartSys->pointAttractor(i, attractor, strength, false); + } } } - for(uint32_t i = 0; i < displayparticles; i++) - { - PartSys->pointAttractor(i, attractor, strength, false); - } + for(uint32_t i = 0; i < displayparticles; i++) { + PartSys->pointAttractor(i, attractor, strength, false); + } #else // no AR - for(uint32_t i = 0; i < displayparticles; i++) - { + for (uint32_t i = 0; i < displayparticles; i++) { PartSys->pointAttractor(i, attractor, SEGMENT.speed, SEGMENT.check3); } #endif @@ -8880,7 +8752,7 @@ uint16_t mode_particleattractor(void) PartSys->update(); // update and render return FRAMETIME; } -#ifdef USERMOD_AUDIOREACTIVE +#ifdef USERMOD_AUDIOREACTIVE static const char _data_FX_MODE_PARTICLEATTRACTOR[] PROGMEM = "PS Attractor@Mass,Particles,Particle Size,Collisions,Friction,Color by Age,Move,Disable AR;;!;2v;pal=9,sx=100,ix=82,c1=0,c2=0,o1=0,o2=0,o3=0"; #else static const char _data_FX_MODE_PARTICLEATTRACTOR[] PROGMEM = "PS Attractor@Mass,Particles,Particle Size,Collisions,Friction,Color by Age,Move,Swallow;;!;2;pal=9,sx=100,ix=82,c1=0,c2=0,o1=0,o2=0,o3=0"; @@ -8892,25 +8764,20 @@ Uses palette for particle color by DedeHai (Damian Schneider) */ -uint16_t mode_particlespray(void) -{ - if (SEGLEN == 1) - return mode_static(); +uint16_t mode_particlespray(void) { ParticleSystem2D *PartSys = NULL; //uint8_t numSprays; const uint8_t hardness = 200; // collision hardness is fixed - if (SEGMENT.call == 0) // initialization - { + if (SEGMENT.call == 0) { // initialization if (!initParticleSystem2D(PartSys, 1)) // init, no additional data needed - return mode_static(); // allocation failed; //allocation failed + return mode_static(); // allocation failed or not 2D PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting) - PartSys->setBounceY(true); + PartSys->setBounceY(true); PartSys->setMotionBlur(200); // anable motion blur PartSys->sources[0].source.hue = random16(); PartSys->sources[0].source.collide = true; // seeded particles will collide (if enabled) PartSys->sources[0].var = 3; - } else PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS @@ -8934,39 +8801,35 @@ uint16_t mode_particlespray(void) //position according to sliders PartSys->sources[0].source.x = map(SEGMENT.custom1, 0, 255, 0, PartSys->maxX); PartSys->sources[0].source.y = map(SEGMENT.custom2, 0, 255, 0, PartSys->maxY); - uint16_t angle = (256 - (((int32_t)SEGMENT.custom3 + 1) << 3)) << 8; - - #ifdef USERMOD_AUDIOREACTIVE + uint16_t angle = (256 - (((int32_t)SEGMENT.custom3 + 1) << 3)) << 8; + + #ifdef USERMOD_AUDIOREACTIVE um_data_t *um_data; - if(usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) - { + if (usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { uint32_t volumeSmth = (uint8_t)(*(float*) um_data->u_data[0]); //0 to 255 - uint32_t volumeRaw = *(int16_t*)um_data->u_data[1]; //0 to 255 - PartSys->sources[0].minLife = 30; + uint32_t volumeRaw = *(int16_t*)um_data->u_data[1]; //0 to 255 + PartSys->sources[0].minLife = 30; - if (SEGMENT.call % 20 == 0 || SEGMENT.call % (11 - volumeSmth / 25) == 0) // defines interval of particle emit - { + if (SEGMENT.call % 20 == 0 || SEGMENT.call % (11 - volumeSmth / 25) == 0) { // defines interval of particle emit PartSys->sources[0].maxLife = (volumeSmth >> 1) + (SEGMENT.intensity >> 1); // lifetime in frames - PartSys->sources[0].var = 1 + ((volumeRaw * SEGMENT.speed) >> 12); + PartSys->sources[0].var = 1 + ((volumeRaw * SEGMENT.speed) >> 12); uint32_t emitspeed = (SEGMENT.speed >> 2) + (volumeRaw >> 3); - PartSys->sources[0].source.hue += volumeSmth/30; - PartSys->angleEmit(PartSys->sources[0], angle, emitspeed); + PartSys->sources[0].source.hue += volumeSmth/30; + PartSys->angleEmit(PartSys->sources[0], angle, emitspeed); } } - else{ //no AR data, fall back to normal mode + else { //no AR data, fall back to normal mode // change source properties - if (SEGMENT.call % (11 - (SEGMENT.intensity / 25)) == 0) // every nth frame, cycle color and emit particles - { + if (SEGMENT.call % (11 - (SEGMENT.intensity / 25)) == 0) { // every nth frame, cycle color and emit particles PartSys->sources[0].maxLife = 300; // lifetime in frames PartSys->sources[0].minLife = 100; - PartSys->sources[0].source.hue++; // = random16(); //change hue of spray source + PartSys->sources[0].source.hue++; // = random16(); //change hue of spray source PartSys->angleEmit(PartSys->sources[0], angle, SEGMENT.speed >> 2); } } #else // change source properties - if (SEGMENT.call % (11 - (SEGMENT.intensity / 25)) == 0) // every nth frame, cycle color and emit particles - { + if (SEGMENT.call % (11 - (SEGMENT.intensity / 25)) == 0) { // every nth frame, cycle color and emit particles PartSys->sources[0].maxLife = 300; // lifetime in frames. note: could be done in init part, but AR moderequires this to be dynamic PartSys->sources[0].minLife = 100; PartSys->sources[0].source.hue++; // = random16(); //change hue of spray source @@ -8988,26 +8851,19 @@ Uses palette for particle color by DedeHai (Damian Schneider) */ -uint16_t mode_particleGEQ(void) -{ - if (SEGLEN == 1) - return mode_static(); - +uint16_t mode_particleGEQ(void) { ParticleSystem2D *PartSys = NULL; - if (SEGMENT.call == 0) // initialization - { - if (!initParticleSystem2D(PartSys, 1)) // init - return mode_static(); // allocation failed - PartSys->setKillOutOfBounds(true); + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem2D(PartSys, 1)) + return mode_static(); // allocation failed or not 2D + PartSys->setKillOutOfBounds(true); PartSys->setUsedParticles((PartSys->numParticles * 3) / 2); // use 2/3 of available particles } else PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS - if (PartSys == NULL) - return mode_static(); // something went wrong, no data! - + return mode_static(); // something went wrong, no data! uint32_t i; // set particle system properties @@ -9015,49 +8871,38 @@ uint16_t mode_particleGEQ(void) PartSys->setWrapX(SEGMENT.check1); PartSys->setBounceX(SEGMENT.check2); PartSys->setBounceY(SEGMENT.check3); - PartSys->enableParticleCollisions(false); + //PartSys->enableParticleCollisions(false); PartSys->setWallHardness(SEGMENT.custom2); PartSys->setGravity(SEGMENT.custom3 << 2); // set gravity strength um_data_t *um_data; if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) - { - // add support for no audio - um_data = simulateSound(SEGMENT.soundSim); - } + um_data = simulateSound(SEGMENT.soundSim); // add support for no audio uint8_t *fftResult = (uint8_t *)um_data->u_data[2]; // 16 bins with FFT data, log mapped already, each band contains frequency amplitude 0-255 //map the bands into 16 positions on x axis, emit some particles according to frequency loudness i = 0; - uint32_t bin; //current bin uint32_t binwidth = (PartSys->maxX + 1)>>4; //emit poisition variation for one bin (+/-) is equal to width/16 (for 16 bins) uint32_t threshold = 300 - SEGMENT.intensity; uint32_t emitparticles = 0; - for (bin = 0; bin < 16; bin++) - { + for (uint32_t bin = 0; bin < 16; bin++) { uint32_t xposition = binwidth*bin + (binwidth>>1); // emit position according to frequency band uint8_t emitspeed = ((uint32_t)fftResult[bin] * (uint32_t)SEGMENT.speed) >> 9; // emit speed according to loudness of band (127 max!) emitparticles = 0; - if (fftResult[bin] > threshold) - { + if (fftResult[bin] > threshold) { emitparticles = 1;// + (fftResult[bin]>>6); } - else if(fftResult[bin] > 0)// band has low volue - { + else if(fftResult[bin] > 0) { // band has low volue uint32_t restvolume = ((threshold - fftResult[bin])>>2) + 2; if (random16() % restvolume == 0) - { emitparticles = 1; - } } - while (i < PartSys->usedParticles && emitparticles > 0) // emit particles if there are any left, low frequencies take priority - { - if (PartSys->particles[i].ttl == 0) // find a dead particle - { + while (i < PartSys->usedParticles && emitparticles > 0) { // emit particles if there are any left, low frequencies take priority + if (PartSys->particles[i].ttl == 0) { // find a dead particle //set particle properties TODO: could also use the spray... PartSys->particles[i].ttl = 20 + map(SEGMENT.intensity, 0,255, emitspeed>>1, emitspeed + random16(emitspeed)) ; // set particle alive, particle lifespan is in number of frames PartSys->particles[i].x = xposition + random16(binwidth) - (binwidth>>1); // position randomly, deviating half a bin width @@ -9074,6 +8919,7 @@ uint16_t mode_particleGEQ(void) PartSys->update(); // update and render return FRAMETIME; } + static const char _data_FX_MODE_PARTICLEGEQ[] PROGMEM = "PS 2D GEQ@Speed,Intensity,Diverge,Bounce,Gravity,Cylinder,Walls,Floor;;!;2f;pal=0,sx=155,ix=200,c1=0,c2=128,o1=0,o2=0,o3=0"; /* @@ -9084,47 +8930,41 @@ static const char _data_FX_MODE_PARTICLEGEQ[] PROGMEM = "PS 2D GEQ@Speed,Intensi */ #define NUMBEROFSOURCES 16 -uint16_t mode_particlecenterGEQ(void) -{ -if (SEGLEN == 1) - return mode_static(); - +uint16_t mode_particlecenterGEQ(void) { ParticleSystem2D *PartSys = NULL; uint8_t numSprays; uint32_t i; - if (SEGMENT.call == 0) // initialization - { - if (!initParticleSystem2D(PartSys, NUMBEROFSOURCES)) // init, request 16 sources - return mode_static(); // allocation failed + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem2D(PartSys, NUMBEROFSOURCES)) // init, request 16 sources + return mode_static(); // allocation failed or not 2D + numSprays = min(PartSys->numSources, (uint32_t)NUMBEROFSOURCES); - for (i = 0; i < numSprays; i++) - { + for (i = 0; i < numSprays; i++) { PartSys->sources[i].source.x = (PartSys->maxX + 1) >> 1; // center PartSys->sources[i].source.y = (PartSys->maxY + 1) >> 1; // center - PartSys->sources[i].source.hue = i*16; // even color distribution + PartSys->sources[i].source.hue = i * 16; // even color distribution PartSys->sources[i].maxLife = 400; PartSys->sources[i].minLife = 200; } - PartSys->setKillOutOfBounds(true); + PartSys->setKillOutOfBounds(true); } else PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS if (PartSys == NULL) - return mode_static(); // something went wrong, no data! + return mode_static(); // something went wrong, no data! PartSys->updateSystem(); // update system properties (dimensions and data pointers) numSprays = min(PartSys->numSources, (uint32_t)NUMBEROFSOURCES); um_data_t *um_data; if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) - um_data = simulateSound(SEGMENT.soundSim); // add support for no audio + um_data = simulateSound(SEGMENT.soundSim); // add support for no audio uint8_t *fftResult = (uint8_t *)um_data->u_data[2]; // 16 bins with FFT data, log mapped already, each band contains frequency amplitude 0-255 uint32_t threshold = 300 - SEGMENT.intensity; - if (SEGMENT.check2) SEGENV.aux0 += SEGMENT.custom1 << 2; else @@ -9132,29 +8972,25 @@ if (SEGLEN == 1) uint16_t angleoffset = (uint16_t)0xFFFF / (uint16_t)numSprays; uint32_t j = random(numSprays); // 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 < numSprays; i++) - { - if(SEGMENT.call % (32 - (SEGMENT.custom2 >> 3)) == 0 && SEGMENT.custom2 > 0) + for (i = 0; i < numSprays; i++) { + if (SEGMENT.call % (32 - (SEGMENT.custom2 >> 3)) == 0 && SEGMENT.custom2 > 0) PartSys->sources[j].source.hue += 1 + (SEGMENT.custom2 >> 4); - PartSys->sources[j].var = SEGMENT.custom3>>1; - int8_t emitspeed = 5 + (((uint32_t)fftResult[j] * ((uint32_t)SEGMENT.speed+20)) >> 10); // emit speed according to loudness of band + + PartSys->sources[j].var = SEGMENT.custom3 >> 1; + int8_t emitspeed = 5 + (((uint32_t)fftResult[j] * ((uint32_t)SEGMENT.speed + 20)) >> 10); // emit speed according to loudness of band uint16_t emitangle = j * angleoffset + SEGENV.aux0; uint32_t emitparticles = 0; if (fftResult[j] > threshold) - { - emitparticles = 1; - } - else if (fftResult[j] > 0) // band has low value - { + emitparticles = 1; + else if (fftResult[j] > 0) { // band has low value uint32_t restvolume = ((threshold - fftResult[j]) >> 2) + 2; if (random16() % restvolume == 0) - { emitparticles = 1; - } } if (emitparticles) - PartSys->angleEmit(PartSys->sources[j], emitangle, emitspeed); + PartSys->angleEmit(PartSys->sources[j], emitangle, emitspeed); + j = (j + 1) % numSprays; } PartSys->update(); // update and render @@ -9166,59 +9002,50 @@ static const char _data_FX_MODE_PARTICLECIRCULARGEQ[] PROGMEM = "PS Center GEQ@S Particle replacement of Ghost Rider by DedeHai (Damian Schneider), original by stepko adapted by Blaz Kristan (AKA blazoncek) */ #define MAXANGLESTEP 2200 //32767 means 180° -uint16_t mode_particleghostrider(void) -{ - if (SEGLEN == 1) - return mode_static(); +uint16_t mode_particleghostrider(void) { ParticleSystem2D *PartSys = NULL; PSsettings2D ghostsettings; ghostsettings.asByte = 0b0000011; //enable wrapX and wrapY - if (SEGMENT.call == 0) // initialization - { + if (SEGMENT.call == 0) { // initialization if (!initParticleSystem2D(PartSys, 1)) // init, no additional data needed - return mode_static(); // allocation failed; //allocation failed + return mode_static(); // allocation failed or not 2D PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting) PartSys->sources[0].maxLife = 260; // lifetime in frames PartSys->sources[0].minLife = 250; PartSys->sources[0].source.x = random16(PartSys->maxX); - PartSys->sources[0].source.y = random16(PartSys->maxY); - SEGENV.step = random(MAXANGLESTEP) - (MAXANGLESTEP>>1); // angle increment + PartSys->sources[0].source.y = random16(PartSys->maxY); + SEGENV.step = random(MAXANGLESTEP) - (MAXANGLESTEP>>1); // angle increment } - else + else { PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + } if (PartSys == NULL) return mode_static(); // something went wrong, no data! - - if(SEGMENT.intensity > 0) // spiraling - { - if(SEGENV.aux1) - { + if(SEGMENT.intensity > 0) { // spiraling + if(SEGENV.aux1) { SEGENV.step += SEGMENT.intensity>>3; if((int32_t)SEGENV.step > MAXANGLESTEP) SEGENV.aux1 = 0; } - else - { - SEGENV.step -= SEGMENT.intensity>>3; + else { + SEGENV.step -= SEGMENT.intensity>>3; if((int32_t)SEGENV.step < -MAXANGLESTEP) SEGENV.aux1 = 1; } } // Particle System settings PartSys->updateSystem(); // update system properties (dimensions and data pointers) - PartSys->setMotionBlur(SEGMENT.custom1); + PartSys->setMotionBlur(SEGMENT.custom1); PartSys->sources[0].var = SEGMENT.custom3 >> 1; // color by age (PS 'color by age' always starts with hue = 255, don't want that here) - if(SEGMENT.check1) - { - for(uint32_t i = 0; i < PartSys->usedParticles; i++) - { + if(SEGMENT.check1) { + for(uint32_t i = 0; i < PartSys->usedParticles; i++) { PartSys->particles[i].hue = PartSys->sources[0].source.hue + (PartSys->particles[i].ttl<<2); - } + } } // enable/disable walls @@ -9228,7 +9055,7 @@ uint16_t mode_particleghostrider(void) SEGENV.aux0 += (int32_t)SEGENV.step; // step is angle increment uint16_t emitangle = SEGENV.aux0 + 32767; // +180° int32_t speed = map(SEGMENT.speed, 0, 255, 12, 64); - PartSys->sources[0].source.vx = ((int32_t)cos16(SEGENV.aux0) * speed) / (int32_t)32767; + PartSys->sources[0].source.vx = ((int32_t)cos16(SEGENV.aux0) * speed) / (int32_t)32767; PartSys->sources[0].source.vy = ((int32_t)sin16(SEGENV.aux0) * speed) / (int32_t)32767; PartSys->sources[0].source.ttl = 500; // source never dies (note: setting 'perpetual' is not needed if replenished each frame) PartSys->particleMoveUpdate(PartSys->sources[0].source, &ghostsettings); @@ -9240,75 +9067,66 @@ uint16_t mode_particleghostrider(void) // emit two particles PartSys->angleEmit(PartSys->sources[0], emitangle, speed); PartSys->angleEmit(PartSys->sources[0], emitangle, speed); - if (SEGMENT.call % (11 - (SEGMENT.custom2 / 25)) == 0) // every nth frame, cycle color and emit particles //TODO: make this a segment call % SEGMENT.custom2 for better control - { - PartSys->sources[0].source.hue++; + if (SEGMENT.call % (11 - (SEGMENT.custom2 / 25)) == 0) { // every nth frame, cycle color and emit particles //TODO: make this a segment call % SEGMENT.custom2 for better control + PartSys->sources[0].source.hue++; } if (SEGMENT.custom2 > 190) //fast color change - PartSys->sources[0].source.hue += (SEGMENT.custom2 - 190) >> 2; + PartSys->sources[0].source.hue += (SEGMENT.custom2 - 190) >> 2; PartSys->update(); // update and render return FRAMETIME; } static const char _data_FX_MODE_PARTICLEGHOSTRIDER[] PROGMEM = "PS Ghost Rider@Speed,Spiral,Blur,Color Cycle,Spread,Color by age,Walls;;!;2;pal=1,sx=70,ix=0,c1=220,c2=30,c3=21,o1=1,o2=0,o3=0"; - /* PS Blobs: large particles bouncing around, changing size and form Uses palette for particle color by DedeHai (Damian Schneider) */ -uint16_t mode_particleblobs(void) -{ - if (SEGLEN == 1) - return mode_static(); +uint16_t mode_particleblobs(void) { ParticleSystem2D *PartSys = NULL; - if (SEGMENT.call == 0) - { + if (SEGMENT.call == 0) { if (!initParticleSystem2D(PartSys, 1, 0, true, true)) //init, request one source, no additional bytes, advanced size & size control (actually dont really need one TODO: test if using zero sources also works) - return mode_static(); // allocation failed - PartSys->setBounceX(true); - PartSys->setBounceY(true); - PartSys->setWallHardness(255); - PartSys->setWallRoughness(255); - PartSys->setCollisionHardness(255); - //PartSys->setParticleSize(0); //set global size to zero or motion blur cannot be used (is zero by default) + return mode_static(); // allocation failed or not 2D + PartSys->setBounceX(true); + PartSys->setBounceY(true); + PartSys->setWallHardness(255); + PartSys->setWallRoughness(255); + PartSys->setCollisionHardness(255); + //PartSys->setParticleSize(0); //set global size to zero or motion blur cannot be used (is zero by default) } else PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS if (PartSys == NULL) - return mode_static(); // something went wrong, no data! - + return mode_static(); // something went wrong, no data! + PartSys->updateSystem(); // update system properties (dimensions and data pointers) - PartSys->setUsedParticles(min(PartSys->numParticles, (uint32_t)map(SEGMENT.intensity,0 ,255, 1, (PartSys->maxXpixel * PartSys->maxYpixel)>>4))); + PartSys->setUsedParticles(min(PartSys->numParticles, (uint32_t)map(SEGMENT.intensity, 0, 255, 1, (PartSys->maxXpixel * PartSys->maxYpixel) >> 4))); PartSys->enableParticleCollisions(SEGMENT.check2); - for (uint32_t i = 0; i < PartSys->usedParticles; i++) // update particles - { - if(SEGENV.aux0 != SEGMENT.speed || PartSys->particles[i].ttl == 0) // speed changed or dead - { - PartSys->particles[i].vx = (int8_t)random(-(SEGMENT.speed >> 2), SEGMENT.speed >> 2); + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { // update particles + if (SEGENV.aux0 != SEGMENT.speed || PartSys->particles[i].ttl == 0) { // speed changed or dead + PartSys->particles[i].vx = (int8_t)random(-(SEGMENT.speed >> 2), SEGMENT.speed >> 2); PartSys->particles[i].vy = (int8_t)random(-(SEGMENT.speed >> 2), SEGMENT.speed >> 2); } - if(SEGENV.aux1 != SEGMENT.custom1 || PartSys->particles[i].ttl == 0) // size changed or dead + if (SEGENV.aux1 != SEGMENT.custom1 || PartSys->particles[i].ttl == 0) // size changed or dead PartSys->advPartSize[i].maxsize = 60 + (SEGMENT.custom1 >> 1) + random((SEGMENT.custom1 >> 2)); // set each particle to slightly randomized size //PartSys->particles[i].perpetual = SEGMENT.check2; //infinite life if set - if (PartSys->particles[i].ttl == 0) // find dead particle, renitialize - { + if (PartSys->particles[i].ttl == 0) { // find dead particle, renitialize PartSys->particles[i].ttl = 300 + random16(((uint16_t)SEGMENT.custom2 << 3) + 100); PartSys->particles[i].x = random(PartSys->maxX); PartSys->particles[i].y = random16(PartSys->maxY); PartSys->particles[i].hue = random16(); // set random color PartSys->particles[i].collide = true; // enable collision for particle - PartSys->advPartProps[i].size = 0; // start out small - PartSys->advPartSize[i].asymmetry = random16(220); - PartSys->advPartSize[i].asymdir = random16(255); + PartSys->advPartProps[i].size = 0; // start out small + PartSys->advPartSize[i].asymmetry = random16(220); + PartSys->advPartSize[i].asymdir = random16(255); // set advanced size control properties PartSys->advPartSize[i].grow = true; - PartSys->advPartSize[i].growspeed = 1 + random16(9); + PartSys->advPartSize[i].growspeed = 1 + random16(9); PartSys->advPartSize[i].shrinkspeed = 1 + random16(9); PartSys->advPartSize[i].wobblespeed = 1 + random(3); } @@ -9319,60 +9137,20 @@ uint16_t mode_particleblobs(void) SEGENV.aux0 = SEGMENT.speed; //write state back SEGENV.aux1 = SEGMENT.custom1; - #ifdef USERMOD_AUDIOREACTIVE + #ifdef USERMOD_AUDIOREACTIVE um_data_t *um_data; - if(usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) - { - uint8_t volumeSmth = (uint8_t)(*(float*) um_data->u_data[0]); - for (uint32_t i = 0; i < PartSys->usedParticles; i++) // update particles - { - if(SEGMENT.check3) //pulsate selected - PartSys->advPartProps[i].size = volumeSmth; - } + if (usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { + uint8_t volumeSmth = (uint8_t)(*(float*)um_data->u_data[0]); + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { // update particles + if (SEGMENT.check3) //pulsate selected + PartSys->advPartProps[i].size = volumeSmth; + } } #endif PartSys->setMotionBlur(((SEGMENT.custom3) << 3) + 7); PartSys->update(); // update and render - /* -//rotat image (just a test, non working yet) - float angle = PI/3; - // Calculate sine and cosine of the angle - float cosTheta = cos(angle); - float sinTheta = sin(angle); - - // Center of rotation - int centerX = cols / 2; - int centerY = rows / 2; - - // Iterate over each pixel in the output image - for (int y = 0; y < rows; y++) - { - for (int x = 0; x < cols; x++) - { - int relX = x - centerX; - int relY = y - centerY; - - // Apply rotation using axis symmetry - int origX = round(relX * cosTheta - relY * sinTheta) + centerX; - int origY = round(relX * sinTheta + relY * cosTheta) + centerY; - - // Check if original coordinates are within bounds - if (origX >= 0 && origX < rows && origY >= 0 && origY < cols) - { - // Copy pixel value from original image to rotated image - SEGMENT.setPixelColorXY(x, y, SEGMENT.getPixelColorXY(origX, origY)); - } - - // Copy pixel values from original image to rotated image - rotatedImage[origY][origX] = image[y][x]; - rotatedImage[origY][cols - 1 - origX] = image[y][cols - 1 - x]; - rotatedImage[rows - 1 - origY][origX] = image[rows - 1 - y][x]; - rotatedImage[rows - 1 - origY][cols - 1 - origX] = image[rows - 1 - y][cols - 1 - x]; - } - }*/ - return FRAMETIME; } static const char _data_FX_MODE_PARTICLEBLOBS[] PROGMEM = "PS Blobs@Speed,Blobs,Size,Life,Blur,Wobble,Collide,Pulsate;;!;2v;sx=30,ix=64,c1=200,c2=130,c3=0,o1=0,o2=0,o3=1"; @@ -9382,26 +9160,20 @@ static const char _data_FX_MODE_PARTICLEBLOBS[] PROGMEM = "PS Blobs@Speed,Blobs, * particles move, then split to form a fractal tree EXPERIMENTAL! * by DedeHai (Damian Schneider) */ - -uint16_t mode_particlefractal(void) -{ -if (SEGLEN == 1) - return mode_static(); - +uint16_t mode_particlefractal(void) { ParticleSystem2D *PartSys = NULL; uint32_t i; - if (SEGMENT.call == 0) // initialization - { + if (SEGMENT.call == 0) { // initialization if (!initParticleSystem2D(PartSys, 1, 0, true, false)) // init, use advanced particles - return mode_static(); // allocation failed - PartSys->setKillOutOfBounds(true); + return mode_static(); // allocation failed or not 2D + PartSys->setKillOutOfBounds(true); } else PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS if (PartSys == NULL) - return mode_static(); // something went wrong, no data! + return mode_static(); // something went wrong, no data! PartSys->updateSystem(); // update system properties (dimensions and data pointers) @@ -9412,17 +9184,15 @@ if (SEGLEN == 1) int16_t angleoffset = SEGMENT.custom2 << 6; int8_t emitspeed = SEGMENT.speed >> 2; - + //check particle age, emit 2 particles at the end of the branch - for (i = 0; i < PartSys->numParticles; i++) - { - if(PartSys->particles[i].ttl > 0 && PartSys->particles[i].ttl < 260) //alive and ripe - { + for (i = 0; i < PartSys->numParticles; i++) { + if(PartSys->particles[i].ttl > 0 && PartSys->particles[i].ttl < 260) { //alive and ripe PartSys->particles[i].ttl = 0; uint16_t currentangle = ((uint32_t)PartSys->advPartProps[i].forcecounter) << 7; // abuse forcecounter to track the angle PartSys->sources[0].source.x = PartSys->particles[i].x; PartSys->sources[0].source.y = PartSys->particles[i].y;; - PartSys->sources[0].source.hue = PartSys->particles[i].hue + 50; // todo: make color schemes + PartSys->sources[0].source.hue = PartSys->particles[i].hue + 50; // todo: make color schemes uint16_t angle = currentangle - angleoffset; int32_t index = PartSys->angleEmit(PartSys->sources[0], angle, emitspeed); //upward TODO: make angle adjustable Serial.print("branch emit1 at idx = "); @@ -9430,16 +9200,14 @@ if (SEGLEN == 1) //TODO: check if index >=0!!! PartSys->advPartProps[index].forcecounter = angle >> 7; angle = currentangle + angleoffset; - index = PartSys->angleEmit(PartSys->sources[0], angle, emitspeed); + index = PartSys->angleEmit(PartSys->sources[0], angle, emitspeed); Serial.print("branch emit2 at idx = "); Serial.println(index); PartSys->advPartProps[index].forcecounter = angle >> 7; } - - } - if(SEGENV.call % (256-SEGMENT.intensity) == 0) - { + + if(SEGENV.call % (256-SEGMENT.intensity) == 0) { PartSys->sources[0].source.x = (PartSys->maxX + 1) >> 1; PartSys->sources[0].source.y = 5; PartSys->sources[0].source.hue = 0; // todo: make color schemes @@ -9451,12 +9219,13 @@ if (SEGLEN == 1) Serial.println(index); //set the forcecounter to track the angle (only 8 bit precision...) PartSys->advPartProps[index].forcecounter = angle >> 7; - } - + } + PartSys->setMotionBlur(((SEGMENT.custom3) << 3) + 7); PartSys->update(); // update and render return FRAMETIME; } + static const char _data_FX_MODE_PARTICLEFRACTAL[] PROGMEM = "PS fractal (exp)@Speed,Intensity,Base angle,branch angle,Blur,,Direction;;!;2f;pal=13,ix=180,c1=0,c2=0,c3=8,o1=0,o2=0"; #endif //WLED_DISABLE_PARTICLESYSTEM2D @@ -9475,17 +9244,13 @@ Uses palette for particle color by DedeHai (Damian Schneider) */ -uint16_t mode_particleDrip(void) -{ - if (SEGLEN == 1) - return mode_static(); +uint16_t mode_particleDrip(void) { ParticleSystem1D *PartSys = NULL; //uint8_t numSprays; - if (SEGMENT.call == 0) // initialization - { + if (SEGMENT.call == 0) { // initialization if (!initParticleSystem1D(PartSys, 4)) // init - return mode_static(); // allocation failed; //allocation failed - PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting) + return mode_static(); // allocation failed or single pixel + PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting) PartSys->sources[0].source.hue = random16(); SEGENV.aux1 = 0xFFFF; // invalidate } @@ -9501,57 +9266,54 @@ uint16_t mode_particleDrip(void) PartSys->setWallHardness(50); PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur - PartSys->setGravity(SEGMENT.custom3>>1); // set gravity (8 is default strength) + PartSys->setGravity(SEGMENT.custom3 >> 1); // set gravity (8 is default strength) PartSys->setParticleSize(SEGMENT.check3); // 1 or 2 pixel rendering - if(SEGMENT.check2) //collisions enabled + if (SEGMENT.check2) { //collisions enabled PartSys->enableParticleCollisions(true); //enable, full hardness + } else PartSys->enableParticleCollisions(false); PartSys->sources[0].source.collide = false; //drops do not collide - if(SEGMENT.check1) //rain mode, emit at random position, short life (3-8 seconds at 50fps) - { - if(SEGMENT.custom1 == 0) //splash disabled, do not bounce raindrops - PartSys->setBounce(false); + if (SEGMENT.check1) { //rain mode, emit at random position, short life (3-8 seconds at 50fps) + if (SEGMENT.custom1 == 0) //splash disabled, do not bounce raindrops + PartSys->setBounce(false); PartSys->sources[0].var = 5; - PartSys->sources[0].v = -(8 + (SEGMENT.speed >> 2)); //speed + var must be < 128, inverted speed (=down) - // lifetime in frames + PartSys->sources[0].v = -(8 + (SEGMENT.speed >> 2)); //speed + var must be < 128, inverted speed (=down) + // lifetime in frames PartSys->sources[0].minLife = 30; PartSys->sources[0].maxLife = 200; PartSys->sources[0].source.x = random(PartSys->maxX); //random emit position } - else{ //drip + else { //drip PartSys->sources[0].var = 0; - PartSys->sources[0].v = -(SEGMENT.speed >> 1); //speed + var must be < 128, inverted speed (=down) - PartSys->sources[0].minLife = 3000; + PartSys->sources[0].v = -(SEGMENT.speed >> 1); //speed + var must be < 128, inverted speed (=down) + PartSys->sources[0].minLife = 3000; PartSys->sources[0].maxLife = 3000; - PartSys->sources[0].source.x = PartSys->maxX - PS_P_RADIUS_1D; - } + PartSys->sources[0].source.x = PartSys->maxX - PS_P_RADIUS_1D; + } - if(SEGENV.aux1 != SEGMENT.intensity) //slider changed + if (SEGENV.aux1 != SEGMENT.intensity) //slider changed SEGENV.aux0 = 1; //must not be zero or "% 0" happens below which crashes on ESP32 - SEGENV.aux1 = SEGMENT.intensity; - - // every nth frame emit a particle - if (SEGMENT.call % SEGENV.aux0 == 0) - { - int32_t interval = 300 / ((SEGMENT.intensity) + 1); - SEGENV.aux0 = interval + random(interval + 5); - // if(SEGMENT.check1) // rain mode - // PartSys->sources[0].source.hue = 0; - // else + + SEGENV.aux1 = SEGMENT.intensity; // save state + + // every nth frame emit a particle + if (SEGMENT.call % SEGENV.aux0 == 0) { + int32_t interval = 300 / ((SEGMENT.intensity) + 1); + SEGENV.aux0 = interval + random(interval + 5); + // if(SEGMENT.check1) // rain mode + // PartSys->sources[0].source.hue = 0; + // else PartSys->sources[0].source.hue = random16(); //set random color TODO: maybe also not random but color cycling? need another slider or checkmark for this. PartSys->sprayEmit(PartSys->sources[0]); } - - for (uint32_t i = 0; i < PartSys->usedParticles; i++)//check all particles - { - if(PartSys->particles[i].ttl && PartSys->particles[i].collide == false) // use collision flag to identify splash particles - { - if(SEGMENT.custom1 > 0 && PartSys->particles[i].x < (PS_P_RADIUS_1D << 1)) //splash enabled and reached bottom - { + + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { //check all particles + if (PartSys->particles[i].ttl && PartSys->particles[i].collide == false) { // use collision flag to identify splash particles + if (SEGMENT.custom1 > 0 && PartSys->particles[i].x < (PS_P_RADIUS_1D << 1)) { //splash enabled and reached bottom PartSys->particles[i].ttl = 0; //kill origin particle PartSys->sources[0].maxLife = 80; PartSys->sources[0].minLife = 20; @@ -9559,22 +9321,20 @@ uint16_t mode_particleDrip(void) PartSys->sources[0].v = 0; PartSys->sources[0].source.hue = PartSys->particles[i].hue; PartSys->sources[0].source.x = PS_P_RADIUS_1D; - PartSys->sources[0].source.collide = true; //splashes do collide if enabled - for(int j = 0; j < 2 + (SEGMENT.custom1 >> 2); j++) - { - PartSys->sprayEmit(PartSys->sources[0]); + PartSys->sources[0].source.collide = true; //splashes do collide if enabled + for (int j = 0; j < 2 + (SEGMENT.custom1 >> 2); j++) { + PartSys->sprayEmit(PartSys->sources[0]); } } } - if(SEGMENT.check1) //rain mode, fade hue to max - { - if(PartSys->particles[i].hue < 245) + if (SEGMENT.check1) { //rain mode, fade hue to max + if (PartSys->particles[i].hue < 245) PartSys->particles[i].hue += 8; } //increase speed on high settings by calling the move function twice - if(SEGMENT.speed > 200) - PartSys->particleMoveUpdate(PartSys->particles[i]); + if (SEGMENT.speed > 200) + PartSys->particleMoveUpdate(PartSys->particles[i]); } PartSys->update(); // update and render @@ -9590,21 +9350,17 @@ Uses palette for particle color by DedeHai (Damian Schneider) */ -uint16_t mode_particleBouncingBalls(void) -{ - if (SEGLEN == 1) - return mode_static(); - ParticleSystem1D *PartSys = NULL; +uint16_t mode_particleBouncingBalls(void) { + ParticleSystem1D *PartSys = NULL; - if (SEGMENT.call == 0) // initialization - { + if (SEGMENT.call == 0) { // initialization if (!initParticleSystem1D(PartSys, 1, 128, 0, true)) // init - return mode_static(); // allocation failed; //allocation failed + return mode_static(); // allocation failed or is single pixel PartSys->sources[0].source.collide = true; // seeded particles will collide (if enabled) - PartSys->sources[0].source.x = PS_P_RADIUS_1D; //emit at bottom + PartSys->sources[0].source.x = PS_P_RADIUS_1D; //emit at bottom PartSys->sources[0].maxLife = 900; // maximum lifetime in frames PartSys->sources[0].minLife = PartSys->sources[0].maxLife; - PartSys->setBounce(true); + PartSys->setBounce(true); SEGENV.aux0 = 1; SEGENV.aux1 = 500; //set out of speed range to ensure uptate on first call } @@ -9616,69 +9372,60 @@ uint16_t mode_particleBouncingBalls(void) // Particle System settings //uint32_t hardness = 240 + (SEGMENT.custom1>>4); - PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->updateSystem(); // update system properties (dimensions and data pointers) PartSys->setGravity(1 + (SEGMENT.custom3 >> 1)); // set gravity (8 is default strength) - PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur + PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur PartSys->sources[0].var = SEGMENT.speed >> 3; PartSys->sources[0].v = (SEGMENT.speed >> 1) - (SEGMENT.speed >> 3); PartSys->enableParticleCollisions(SEGMENT.check1, 254); // enable collisions and set particle collision hardness (do not use full hardness or particles speed up due to pushing, can not be made perfectly balanced) - PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 1, PartSys->numParticles)); - PartSys->setColorByPosition(SEGMENT.check3); + PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 1, PartSys->numParticles)); + PartSys->setColorByPosition(SEGMENT.check3); - if(SEGMENT.check2) //rolling balls - { + if (SEGMENT.check2) { //rolling balls PartSys->setGravity(0); PartSys->setWallHardness(255); bool updateballs = false; - if(SEGENV.aux1 != SEGMENT.speed + SEGMENT.intensity + SEGMENT.check2 + SEGMENT.custom1) // user settings change + if (SEGENV.aux1 != SEGMENT.speed + SEGMENT.intensity + SEGMENT.check2 + SEGMENT.custom1) // user settings change updateballs = true; - for(uint32_t i = 0; i < PartSys->usedParticles; i++) - { - if((PartSys->particles[i].vx > 8 || PartSys->particles[i].vx < -8) && PartSys->particles[i].ttl > 150) //let only slow particles die (ensures no stopped particles) + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { + if ((PartSys->particles[i].vx > 8 || PartSys->particles[i].vx < -8) && PartSys->particles[i].ttl > 150) //let only slow particles die (ensures no stopped particles) PartSys->particles[i].ttl = 260; //set alive at full intensity - if(updateballs || PartSys->particles[i].ttl == 0) //speed changed or particle died, set particle properties - { + if (updateballs || PartSys->particles[i].ttl == 0) { //speed changed or particle died, set particle properties PartSys->particles[i].ttl = 260 + SEGMENT.speed; PartSys->particles[i].collide = true; int32_t newspeed = random(20 + (SEGMENT.speed >> 2)) + (SEGMENT.speed >> 3); PartSys->particles[i].vx = PartSys->particles[i].vx > 0 ? newspeed : -newspeed; //keep the direction PartSys->particles[i].hue = random16(); //set ball colors to random PartSys->advPartProps[i].sat = 255; - PartSys->advPartProps[i].size = random16(SEGMENT.custom1 >> 2, SEGMENT.custom1); + PartSys->advPartProps[i].size = random16(SEGMENT.custom1 >> 2, SEGMENT.custom1); } } } - else //bouncing balls - { + else { //bouncing balls PartSys->setWallHardness(220); //check for balls that are 'laying on the ground' and remove them - for(uint32_t i = 0; i < PartSys->usedParticles; i++) - { - if(PartSys->particles[i].vx == 0 && PartSys->particles[i].x < (PS_P_RADIUS_1D << 1)) + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { + if (PartSys->particles[i].vx == 0 && PartSys->particles[i].x < (PS_P_RADIUS_1D << 1)) PartSys->particles[i].ttl = 0; } // every nth frame emit a ball - if (SEGMENT.call % SEGENV.aux0 == 0) - { + if (SEGMENT.call % SEGENV.aux0 == 0) { SEGENV.aux0 = (260 - SEGMENT.intensity) + random(280 - SEGMENT.intensity); - PartSys->sources[0].source.hue = random16(); //set ball color + PartSys->sources[0].source.hue = random16(); //set ball color PartSys->sources[0].sat = 255; PartSys->sources[0].size = random16(SEGMENT.custom1 >> 2, SEGMENT.custom1); PartSys->sprayEmit(PartSys->sources[0]); } } - SEGENV.aux1 = SEGMENT.speed + SEGMENT.intensity + SEGMENT.check2 + SEGMENT.custom1; - for (uint32_t i = 0; i < PartSys->usedParticles; i++) - { - - if(SEGMENT.speed > 200) + SEGENV.aux1 = SEGMENT.speed + SEGMENT.intensity + SEGMENT.check2 + SEGMENT.custom1; + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { + if (SEGMENT.speed > 200) PartSys->particleMoveUpdate(PartSys->particles[i]); //increase speed on high settings by calling the move function twice } - - PartSys->update(); // update and render + PartSys->update(); // update and render return FRAMETIME; } static const char _data_FX_MODE_PSBOUNCINGBALLS[] PROGMEM = "PS Bouncing Balls@Speed,!,Size,Blur/Overlay,Gravity,Collide,Rolling,Color by Position;,!;!;1;pal=0,sx=100,ix=85,c1=30,c2=0,c3=8,o1=0,o2=0,o3=0"; @@ -9693,25 +9440,21 @@ Uses palette for particle color by DedeHai (Damian Schneider) */ -uint16_t mode_particleDancingShadows(void) -{ - if (SEGLEN == 1) - return mode_static(); +uint16_t mode_particleDancingShadows(void) { ParticleSystem1D *PartSys = NULL; - if (SEGMENT.call == 0) // initialization - { + if (SEGMENT.call == 0) { // initialization if (!initParticleSystem1D(PartSys, 1)) // init, one source - return mode_static(); // allocation failed; //allocation failed - PartSys->sources[0].maxLife = 1000; //set long life (kill out of bounds is done in custom way) - PartSys->sources[0].minLife = PartSys->sources[0].maxLife; + return mode_static(); // allocation failed or is single pixel + PartSys->sources[0].maxLife = 1000; //set long life (kill out of bounds is done in custom way) + PartSys->sources[0].minLife = PartSys->sources[0].maxLife; } - else + else { PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + } if (PartSys == NULL) return mode_static(); // something went wrong, no data! - // Particle System settings PartSys->updateSystem(); // update system properties (dimensions and data pointers) @@ -9719,9 +9462,7 @@ uint16_t mode_particleDancingShadows(void) PartSys->setParticleSize(SEGMENT.check3); // 1 or 2 pixel rendering //generate a spotlight: generates particles just outside of view - //if (SEGMENT.call % ((255 + 64) / (1 + SEGMENT.intensity + (SEGMENT.speed >> 4))) == 0) - if (SEGMENT.call % (256 - SEGMENT.intensity) == 0) - { + if (SEGMENT.call % (256 - SEGMENT.intensity) == 0) { //random color, random type uint32_t type = random8(SPOT_TYPES_COUNT); int8_t speed = 2 + random(2 + (SEGMENT.speed >> 2)) + (SEGMENT.speed >> 3); @@ -9732,44 +9473,44 @@ uint16_t mode_particleDancingShadows(void) if (random8(2)) { position = PartSys->maxXpixel; speed = -speed; - }else { - position = -width; } + else + position = -width; + PartSys->sources[0].v = speed; //emitted particle speed PartSys->sources[0].source.hue = random8(); //random spotlight color - for (uint32_t i = 0; i < width; i++) - { + for (uint32_t i = 0; i < width; i++) { switch (type) { case SPOT_TYPE_SOLID: - //nothing to do - break; + //nothing to do + break; case SPOT_TYPE_GRADIENT: - ttl = cubicwave8(map(i, 0, width - 1, 0, 255)); + ttl = cubicwave8(map(i, 0, width - 1, 0, 255)); ttl = ttl*ttl >> 8; //make gradient more pronounced - break; + break; case SPOT_TYPE_2X_GRADIENT: - ttl = cubicwave8(2 * map(i, 0, width - 1, 0, 255)); + ttl = cubicwave8(2 * map(i, 0, width - 1, 0, 255)); ttl = ttl*ttl >> 8; - break; + break; case SPOT_TYPE_2X_DOT: if(i > 0) position++; //skip one pixel i++; - break; + break; case SPOT_TYPE_3X_DOT: if(i > 0) position += 2; //skip two pixels i+=2; - break; + break; case SPOT_TYPE_4X_DOT: if(i > 0) position += 3; //skip three pixels i+=3; - break; + break; } - //emit particle + //emit particle //set the particle source position: PartSys->sources[0].source.x = position * PS_P_RADIUS_1D; uint32_t partidx = PartSys->sprayEmit(PartSys->sources[0]); @@ -9777,61 +9518,47 @@ uint16_t mode_particleDancingShadows(void) position++; //do the next pixel } } - - //kill out of bounds and moving away plus change color - for (uint32_t i = 0; i < PartSys->usedParticles; i++) - { - if(PartSys->particles[i].outofbounds) //check if out of bounds particle move away from strip (i.e. vx < 0 && x > 0 or vx > 0 and x < 0) - { + + //kill out of bounds and moving away plus change color + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { + if(PartSys->particles[i].outofbounds) { //check if out of bounds particle move away from strip (i.e. vx < 0 && x > 0 or vx > 0 and x < 0) if((int32_t)PartSys->particles[i].vx * PartSys->particles[i].x > 0) PartSys->particles[i].ttl = 0; //particle is moving away, kill it } - PartSys->particles[i].perpetual = true; //particles do not age - if (SEGMENT.call % (32 / (1 + (SEGMENT.custom2 >> 3))) == 0) + PartSys->particles[i].perpetual = true; //particles do not age + if (SEGMENT.call % (32 / (1 + (SEGMENT.custom2 >> 3))) == 0) PartSys->particles[i].hue += 2 + (SEGMENT.custom2 >> 5); //note: updating speed on the fly is not accurately possible, since it is unknown which particles are assigned to which spot - if(SEGENV.aux0 != SEGMENT.speed) //speed changed - { - //update all particle speed by setting them to current value + if(SEGENV.aux0 != SEGMENT.speed) { //speed changed + //update all particle speed by setting them to current value PartSys->particles[i].vx = PartSys->particles[i].vx > 0 ? SEGMENT.speed >> 3 : -SEGMENT.speed >> 3; } } SEGENV.aux0 = SEGMENT.speed; PartSys->update(); // update and render - + return FRAMETIME; } static const char _data_FX_MODE_PARTICLEDANCINGSHADOWS[] PROGMEM = "PS Dancing Shadows@Speed,!,Blur/Overlay,Color Cycle,,,,Smooth;,!;!;1;pal=0,sx=100,ix=180,c1=0,c2=0,o2=0,o3=0"; - /* Particle Fireworks 1D replacement Uses palette for particle color by DedeHai (Damian Schneider) */ -uint16_t mode_particleFireworks1D(void) -{ - if (SEGLEN == 1) - return mode_static(); +uint16_t mode_particleFireworks1D(void) { ParticleSystem1D *PartSys = NULL; - //uint8_t numRockets; uint8_t *forcecounter; - if (SEGMENT.call == 0) // initialization - { + if (SEGMENT.call == 0) { // initialization if (!initParticleSystem1D(PartSys, 4, 150, 4, true)) // init - return mode_static(); // allocation failed + return mode_static(); // allocation failed or is single pixel PartSys->setKillOutOfBounds(true); - //numRockets = PartSys->numSources; - //for(i = 0; i < numRockets; i++) - //{ PartSys->sources[0].source.perpetual = 1; //set rocket state to standby - //} } else PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS - if (PartSys == NULL) return mode_static(); // something went wrong, no data! @@ -9840,82 +9567,74 @@ uint16_t mode_particleFireworks1D(void) forcecounter = PartSys->PSdataEnd; PartSys->setParticleSize(SEGMENT.check3); // 1 or 2 pixel rendering PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur - + if(!SEGMENT.check1) //gravity enabled for sparks PartSys->setGravity(0); // disable else - PartSys->setGravity(1 + (SEGMENT.speed>>4)); // set gravity + PartSys->setGravity(1 + (SEGMENT.speed>>4)); // set gravity - if(PartSys->sources[0].source.perpetual == 1) //rocket is on standby - { + if(PartSys->sources[0].source.perpetual == 1) { //rocket is on standby PartSys->sources[0].source.ttl--; - if(PartSys->sources[0].source.ttl == 0) //time is up, relaunch - { + if(PartSys->sources[0].source.ttl == 0) { //time is up, relaunch if(random(255) < SEGMENT.custom1) //randomly choose direction according to slider, fire at start of segment if true SEGENV.aux0 = 0; else SEGENV.aux0 = 1; //invert direction - + PartSys->sources[0].source.perpetual = 0; //flag abused for rocket state - PartSys->sources[0].source.hue = random16(); + PartSys->sources[0].source.hue = random16(); PartSys->sources[0].var = 5; PartSys->sources[0].v = 0; PartSys->sources[0].minLife = 10; PartSys->sources[0].maxLife = 30; PartSys->sources[0].source.x = 0; // start from bottom - uint32_t speed = 8 + (((int)16 + (int)random16(20) + (int)random(SEGMENT.speed >> 3 , SEGMENT.speed >> 2)) * (int)PartSys->maxXpixel) / 150; //set speed such that rocket explods in frame, found by experimenting + uint32_t speed = 8 + (((int)16 + (int)random16(20) + (int)random(SEGMENT.speed >> 3 , SEGMENT.speed >> 2)) * (int)PartSys->maxXpixel) / 150; //set speed such that rocket explods in frame, found by experimenting PartSys->sources[0].source.vx = min(speed, (uint32_t)127); PartSys->sources[0].source.ttl = 400; PartSys->sources[0].source.collide = false; // exhaust does not collide, also used to check if direction reversed - PartSys->sources[0].sat = 40; // low saturation exhaust + PartSys->sources[0].sat = 40; // low saturation exhaust PartSys->sources[0].size = 0; // default size - - if(SEGENV.aux0) //inverted rockets launch from end - { + + if(SEGENV.aux0) { //inverted rockets launch from end PartSys->sources[0].source.reversegrav = true; PartSys->sources[0].source.x = PartSys->maxX; //start from top PartSys->sources[0].source.vx = -PartSys->sources[0].source.vx; //revert direction } } } - else //rocket is launched - { + else { //rocket is launched int32_t rocketgravity = -(2 + (SEGMENT.speed>>4)); //-8 int32_t speed = PartSys->sources[0].source.vx; - if(SEGENV.aux0) //negative speed rocket - { + if(SEGENV.aux0) { //negative speed rocket rocketgravity = -rocketgravity; speed = -speed; } PartSys->applyForce(&PartSys->sources[0].source, rocketgravity, &forcecounter[0]); PartSys->particleMoveUpdate(PartSys->sources[0].source); - if(speed < 0 && PartSys->sources[0].source.collide == false) //speed has reversed and not in 'explosion mode' - { + if(speed < 0 && PartSys->sources[0].source.collide == false) { //speed has reversed and not in 'explosion mode' PartSys->sources[0].source.ttl = 75 - (SEGMENT.speed >> 2); //alive for a few more frames PartSys->sources[0].source.collide = true; //set 'explosion mode' - } + } - if(PartSys->sources[0].source.ttl == 0) //explode - { - PartSys->sources[0].source.perpetual = 1; // set standby state + if(PartSys->sources[0].source.ttl == 0) { //explode + PartSys->sources[0].source.perpetual = 1; // set standby state PartSys->sources[0].var = 10 + (SEGMENT.intensity >> 2); PartSys->sources[0].v = 0; //TODO can make global if this never changes PartSys->sources[0].minLife = 60; PartSys->sources[0].maxLife = 150; - PartSys->sources[0].source.ttl = 100 + random16(256 - SEGMENT.intensity); // standby time til next launch + PartSys->sources[0].source.ttl = 100 + random16(256 - SEGMENT.intensity); // standby time til next launch PartSys->sources[0].sat = 7 + (SEGMENT.custom3 << 3); //color saturation PartSys->sources[0].size = random16(255); // random particle size in explosion uint32_t explosionsize = 10 + random(SEGMENT.intensity >> 2, SEGMENT.intensity); - for(uint32_t e = 0; e < explosionsize; e++) //emit explosion particles - { + for(uint32_t e = 0; e < explosionsize; e++) { //emit explosion particles if(SEGMENT.check2) - PartSys->sources[0].source.hue = random16(); //random color for each particle + PartSys->sources[0].source.hue = random16(); //random color for each particle PartSys->sprayEmit(PartSys->sources[0]); //emit a particle } PartSys->sources[0].source.x = -500; //set out of frame until relaunch } - } + } if(SEGMENT.call & 0x01) //every second frame PartSys->sprayEmit(PartSys->sources[0]); //emit a particle @@ -9924,125 +9643,104 @@ uint16_t mode_particleFireworks1D(void) } static const char _data_FX_MODE_PS_FIREWORKS1D[] PROGMEM = "PS Fireworks 1D@Gravity,Explosion,Firing side,Blur/Overlay,Saturation,Gravity,Colorful,Smooth;,!;!;1;pal=0,sx=150,ix=150,c1=220,c2=30,c3=21,o1=0,o2=1,o3=0"; - /* Particle based Sparkle effect Uses palette for particle color by DedeHai (Damian Schneider) */ -uint16_t mode_particleSparkler(void) -{ - if (SEGLEN == 1) - return mode_static(); +uint16_t mode_particleSparkler(void) { ParticleSystem1D *PartSys = NULL; uint32_t numSparklers; uint32_t i; PSsettings1D sparklersettings; sparklersettings.asByte = 0; // PS settings for sparkler (set below) - if (SEGMENT.call == 0) // initialization - { + if (SEGMENT.call == 0) { // initialization if (!initParticleSystem1D(PartSys, 16, 128 ,0, true)) // init, no additional data needed - return mode_static(); // allocation failed - } - else + return mode_static(); // allocation failed or is single pixel + } else PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS - if (PartSys == NULL) return mode_static(); // something went wrong, no data! // Particle System settings PartSys->updateSystem(); // update system properties (dimensions and data pointers) - sparklersettings.wrap = SEGMENT.check2; - sparklersettings.bounce = !SEGMENT.check2; + sparklersettings.wrap = SEGMENT.check2; + sparklersettings.bounce = !SEGMENT.check2; numSparklers = PartSys->numSources; PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur PartSys->setParticleSize(SEGMENT.check3); // 1 or 2 pixel rendering - for(i = 0; i < numSparklers; i++) - { + for(i = 0; i < numSparklers; i++) { PartSys->sources[i].source.hue = random16(); //TODO: make adjustable, maybe even colorcycle? PartSys->sources[i].var = SEGMENT.intensity >> 4 ; PartSys->sources[i].minLife = 150 + (SEGMENT.intensity >> 1); - PartSys->sources[i].maxLife = 200 + SEGMENT.intensity; - uint32_t speed = SEGMENT.speed >> 1; + PartSys->sources[i].maxLife = 200 + SEGMENT.intensity; + uint32_t speed = SEGMENT.speed >> 1; if(SEGMENT.check1) //invert spray speed - speed = -speed; + speed = -speed; PartSys->sources[i].source.vx = speed; //update speed, do not change direction PartSys->sources[i].source.ttl = 400; //replenish its life (setting it perpetual uses more code) PartSys->sources[i].sat = SEGMENT.custom1; //color saturation - PartSys->particleMoveUpdate(PartSys->sources[i].source, &sparklersettings); //move sparkler + PartSys->particleMoveUpdate(PartSys->sources[i].source, &sparklersettings); //move sparkler } - for(i = 0; i < PartSys->usedParticles; i++) - { - if(PartSys->particles[i].ttl > 10) PartSys->particles[i].ttl -= 10; //ttl is linked to brightness, this allows to use higher brightness but still a short spark lifespan + for(i = 0; i < PartSys->usedParticles; i++) { + if(PartSys->particles[i].ttl > 10) PartSys->particles[i].ttl -= 10; //ttl is linked to brightness, this allows to use higher brightness but still a short spark lifespan else PartSys->particles[i].ttl = 0; } numSparklers = min(1 + (SEGMENT.custom3 >> 2), (int)numSparklers); // set used sparklers, 1 to 8 - - if(SEGENV.aux0 != SEGMENT.custom3) //number of used sparklers changed, redistribute - { - for(i = 1; i < numSparklers; i++) - { - PartSys->sources[i].source.x = (PartSys->sources[0].source.x + (PartSys->maxX / numSparklers) * i ) % PartSys->maxX; //distribute evenly + + if(SEGENV.aux0 != SEGMENT.custom3) { //number of used sparklers changed, redistribute + for(i = 1; i < numSparklers; i++) { + PartSys->sources[i].source.x = (PartSys->sources[0].source.x + (PartSys->maxX / numSparklers) * i ) % PartSys->maxX; //distribute evenly } } SEGENV.aux0 = SEGMENT.custom3; - - for(i = 0; i < numSparklers; i++) - { - if(random(255) % (1 + ((255 - SEGMENT.intensity) >> 3)) == 0) - PartSys->sprayEmit(PartSys->sources[i]); //emit a particle + for(i = 0; i < numSparklers; i++) { + if(random(255) % (1 + ((255 - SEGMENT.intensity) >> 3)) == 0) + PartSys->sprayEmit(PartSys->sources[i]); //emit a particle } - + PartSys->update(); // update and render - + return FRAMETIME; } static const char _data_FX_MODE_PS_SPARKLER[] PROGMEM = "PS Sparkler@Speed,!,Saturation,Blur/Overlay,Sparklers,Direction,Wrap/Bounce,Smooth;,!;!;1;pal=0,sx=50,ix=200,c1=0,c2=0,c3=0,o1=1,o2=1,o3=0"; - - /* Particle based Hourglass, particles falling at defined intervals Uses palette for particle color by DedeHai (Damian Schneider) */ -uint16_t mode_particleHourglass(void) -{ - if (SEGLEN == 1) - return mode_static(); +uint16_t mode_particleHourglass(void) { ParticleSystem1D *PartSys = NULL; - int32_t positionoffset; // resting position offset + int32_t positionoffset; // resting position offset bool* direction; uint8_t* basehue; - if (SEGMENT.call == 0) // initialization - { + if (SEGMENT.call == 0) { // initialization if (!initParticleSystem1D(PartSys, 0, 255, 2, false)) // init - return mode_static(); // allocation failed + return mode_static(); // allocation failed or is single pixel PartSys->setBounce(true); PartSys->setWallHardness(80); - for(uint32_t i = 0; i < PartSys->numParticles; i++) - { - PartSys->particles[i].collide = true; - PartSys->particles[i].ttl = 500; - PartSys->particles[i].perpetual = true; + for(uint32_t i = 0; i < PartSys->numParticles; i++) { + PartSys->particles[i].collide = true; + PartSys->particles[i].ttl = 500; + PartSys->particles[i].perpetual = true; } SEGENV.step = 0xFFFF; } else PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS - if (PartSys == NULL) return mode_static(); // something went wrong, no data! - + // Particle System settings PartSys->updateSystem(); // update system properties (dimensions and data pointers) basehue = PartSys->PSdataEnd; //assign data pointer @@ -10050,96 +9748,83 @@ uint16_t mode_particleHourglass(void) uint32_t numgrains = map(SEGMENT.intensity, 0, 255, 1, PartSys->maxXpixel + 1); // number of particles to use PartSys->setUsedParticles(min(numgrains, (uint32_t)PartSys->numParticles));//SEGMENT.custom1); PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur - PartSys->setGravity(map(SEGMENT.custom3, 0, 31, 1, 30)); + PartSys->setGravity(map(SEGMENT.custom3, 0, 31, 1, 30)); PartSys->enableParticleCollisions(true, 34); // fixed hardness, 34 is a value that works best in most settings (spent a long time optimizing) SEGMENT.custom1); - + positionoffset = PS_P_RADIUS_1D / 2; - uint32_t colormode = SEGMENT.custom1 >> 5; // 0-7 - + uint32_t colormode = SEGMENT.custom1 >> 5; // 0-7 - if(SEGMENT.intensity != SEGENV.step) //initialize - { - *basehue = random16(); //choose new random color + if(SEGMENT.intensity != SEGENV.step) { //initialize + *basehue = random16(); //choose new random color SEGENV.step = SEGMENT.intensity; - for(uint32_t i = 0; i < PartSys->usedParticles; i++) - { + for(uint32_t i = 0; i < PartSys->usedParticles; i++) { PartSys->particles[i].reversegrav = true; *direction = 0; SEGENV.aux1 = 1; //initialize below } SEGENV.aux0 = PartSys->usedParticles - 1; //initial state, start with highest number particle } - - for(uint32_t i = 0; i < PartSys->usedParticles; i++) //check if particle reached target position after falling - { + + for(uint32_t i = 0; i < PartSys->usedParticles; i++) { //check if particle reached target position after falling int32_t targetposition; - if (PartSys->particles[i].fixed == false) - { - //calculate target position depending on direction + if (PartSys->particles[i].fixed == false) { + //calculate target position depending on direction if(PartSys->particles[i].reversegrav) targetposition = PartSys->maxX - (i * PS_P_RADIUS_1D + positionoffset); // target resting position else - targetposition = (PartSys->usedParticles - i) * PS_P_RADIUS_1D - positionoffset; // target resting position + targetposition = (PartSys->usedParticles - i) * PS_P_RADIUS_1D - positionoffset; // target resting position if(PartSys->particles[i].x == targetposition) //particle has reached target position, pin it. if not pinned, they do not stack well on larger piles PartSys->particles[i].fixed = true; } if(colormode == 7) PartSys->setColorByPosition(true); //color fixed by position - else - { - PartSys->setColorByPosition(false); + else { + PartSys->setColorByPosition(false); switch(colormode) { case 0: PartSys->particles[i].hue = 120; break; //fixed at 120, if flip is activated, this can make red and green (use palette 34) - case 1: PartSys->particles[i].hue = *basehue; break; //fixed random color - case 2: + case 1: PartSys->particles[i].hue = *basehue; break; //fixed random color + case 2: case 3: PartSys->particles[i].hue = *basehue + (i % colormode)*70; break; // interleved colors (every 2 or 3 particles) case 4: PartSys->particles[i].hue = *basehue + (i * 255) / PartSys->usedParticles; break; // gradient palette colors case 5: PartSys->particles[i].hue = *basehue + (i * 1024) / PartSys->usedParticles; break; // multi gradient palette colors case 6: PartSys->particles[i].hue = i + (strip.now >> 1); break; // disco! fast moving color gradient - default: break; + default: break; } } if(SEGMENT.check1 && !PartSys->particles[i].reversegrav) // flip color when fallen - PartSys->particles[i].hue += 120; - } - + PartSys->particles[i].hue += 120; + } - if(SEGENV.aux1 == 1) //last countdown call before dropping starts, reset all particles - { - for(uint32_t i = 0; i < PartSys->usedParticles; i++) - { + if(SEGENV.aux1 == 1) { //last countdown call before dropping starts, reset all particles + for(uint32_t i = 0; i < PartSys->usedParticles; i++) { uint32_t targetposition; - //calculate target position depending on direction + //calculate target position depending on direction if(PartSys->particles[i].reversegrav) - targetposition = PartSys->maxX - (i * PS_P_RADIUS_1D + positionoffset); // target resting position + targetposition = PartSys->maxX - (i * PS_P_RADIUS_1D + positionoffset); // target resting position else targetposition = (PartSys->usedParticles - i) * PS_P_RADIUS_1D - positionoffset; // target resting position -5 - PS_P_RADIUS_1D/2 - + PartSys->particles[i].x = targetposition; - PartSys->particles[i].fixed = true; + PartSys->particles[i].fixed = true; } } - if(SEGENV.aux1 == 0) //countdown passed, run - { + if(SEGENV.aux1 == 0) { //countdown passed, run uint32_t interval = 257 - SEGMENT.speed; // drop interval in frames, 1 second is 'speed = (257 - FPS)' speed = 0 is one drop every 257 frames if(SEGMENT.check3 && *direction) // fast reset interval = 3; - if(SEGMENT.call % interval == 0) //drop a particle, do not drop more often than every second frame or particles tangle up quite badly - { - if(SEGENV.aux0 < PartSys->usedParticles) - { + if(SEGMENT.call % interval == 0) { //drop a particle, do not drop more often than every second frame or particles tangle up quite badly + if(SEGENV.aux0 < PartSys->usedParticles) { PartSys->particles[SEGENV.aux0].reversegrav = *direction; //let this particle fall or rise PartSys->particles[SEGENV.aux0].fixed = false; // unpin } - else //overflow, flip direction - { + else { //overflow, flip direction *direction = !(*direction); - SEGENV.aux1 = 300; //set countdown + SEGENV.aux1 = 300; //set countdown } - if(*direction == 0) //down - SEGENV.aux0--; - else + if(*direction == 0) //down + SEGENV.aux0--; + else SEGENV.aux0++; } } @@ -10151,36 +9836,29 @@ uint16_t mode_particleHourglass(void) //PartSys->applyFriction(1); //keeps particles calm and stops mass collisions being handled improperly due to chaos PartSys->update(); // update and render - + return FRAMETIME; } static const char _data_FX_MODE_PS_HOURGLASS[] PROGMEM = "PS Hourglass@Speed,!,Color,Blur/Overlay,Gravity,Colorflip,Auto Reset,Fast Reset;,!;!;1;pal=34,sx=245,ix=200,c1=140,c2=80,c3=4,o1=1,o2=1,o3=1"; - - /* Particle based Spray effect (like a volcano, possible replacement for popcorn) Uses palette for particle color by DedeHai (Damian Schneider) */ -uint16_t mode_particle1Dspray(void) -{ - if (SEGLEN == 1) - return mode_static(); - ParticleSystem1D *PartSys = NULL; +uint16_t mode_particle1Dspray(void) { + ParticleSystem1D *PartSys = NULL; - if (SEGMENT.call == 0) // initialization - { - if (!initParticleSystem1D(PartSys, 1)) // init - return mode_static(); // allocation failed + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem1D(PartSys, 1)) + return mode_static(); // allocation failed or is single pixel PartSys->setKillOutOfBounds(true); PartSys->setWallHardness(150); PartSys->setParticleSize(1); } else PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS - if (PartSys == NULL) return mode_static(); // something went wrong, no data! @@ -10192,63 +9870,54 @@ uint16_t mode_particle1Dspray(void) PartSys->setGravity(abs(gravity)); // use reversgrav setting to invert gravity (for proper 'floor' and out of bounce handling) PartSys->sources[0].source.hue = random16(); //TODO: add colormodes like in hourglass? - PartSys->sources[0].var = 20; - PartSys->sources[0].minLife = 200; - PartSys->sources[0].maxLife = 400; + PartSys->sources[0].var = 20; + PartSys->sources[0].minLife = 200; + PartSys->sources[0].maxLife = 400; PartSys->sources[0].source.x = map(SEGMENT.custom1, 0 , 255, 0, PartSys->maxX); // spray position PartSys->sources[0].v = map(SEGMENT.speed, 0 , 255, -127 + PartSys->sources[0].var, 127 - PartSys->sources[0].var); // particle emit speed PartSys->sources[0].source.reversegrav = false; - if(gravity < 0) + if(gravity < 0) PartSys->sources[0].source.reversegrav = true; - - if(random(255) % (1 + ((255 - SEGMENT.intensity) >> 3)) == 0) + + if(random(255) % (1 + ((255 - SEGMENT.intensity) >> 3)) == 0) PartSys->sprayEmit(PartSys->sources[0]); //emit a particle //update color settings PartSys->setColorByAge(SEGMENT.check1); //overruled by 'color by position' - PartSys->setColorByPosition(SEGMENT.check3); - for(uint i = 0; i < PartSys->usedParticles; i++) - { + PartSys->setColorByPosition(SEGMENT.check3); + for(uint i = 0; i < PartSys->usedParticles; i++) { PartSys->particles[i].reversegrav = PartSys->sources[0].source.reversegrav; //update gravity direction } PartSys->update(); // update and render - + return FRAMETIME; } static const char _data_FX_MODE_PS_1DSPRAY[] PROGMEM = "PS 1D Spray@!,!,Position,Blur/Overlay,Gravity,Color by Age,Bounce,Color by Position;,!;!;1;pal=35,sx=200,ix=220,c1=4,c2=0,c3=28,o1=1,o2=1,o3=0"; - - /* Particle based balance: particles move back and forth (1D pendent to 2D particle box) Uses palette for particle color by DedeHai (Damian Schneider) */ -uint16_t mode_particleBalance(void) -{ - if (SEGLEN == 1) - return mode_static(); - ParticleSystem1D *PartSys = NULL; +uint16_t mode_particleBalance(void) { + ParticleSystem1D *PartSys = NULL; uint32_t i; - if (SEGMENT.call == 0) // initialization - { + if (SEGMENT.call == 0) { // initialization if (!initParticleSystem1D(PartSys, 1)) // init, no additional data needed - return mode_static(); // allocation failed - //PartSys->setKillOutOfBounds(true); + return mode_static(); // allocation failed or is single pixel + //PartSys->setKillOutOfBounds(true); PartSys->setParticleSize(1); - for(i = 0; i < PartSys->numParticles; i++) - { + for(i = 0; i < PartSys->numParticles; i++) { PartSys->particles[i].x = i * PS_P_RADIUS_1D; PartSys->particles[i].hue = (i * 1024) / PartSys->usedParticles; // multi gradient distribution PartSys->particles[i].ttl = 300; PartSys->particles[i].perpetual = true; - PartSys->particles[i].collide = true; + PartSys->particles[i].collide = true; } } else PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS - if (PartSys == NULL) return mode_static(); // something went wrong, no data! @@ -10262,33 +9931,29 @@ uint16_t mode_particleBalance(void) if(SEGMENT.custom1 == 0) //collisions disabled, make the walls hard hardness = 200; PartSys->setWallHardness(hardness); - PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 10, PartSys->numParticles)); + PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 10, PartSys->numParticles)); - if (SEGMENT.call % (((255 - SEGMENT.speed) >> 6) + 1) == 0) // how often the force is applied depends on speed setting - { - int32_t xgravity; + if (SEGMENT.call % (((255 - SEGMENT.speed) >> 6) + 1) == 0) { // how often the force is applied depends on speed setting + int32_t xgravity; int32_t increment = (SEGMENT.speed >> 6) + 1; SEGENV.aux0 += increment; - if(SEGMENT.check3) // random, use perlin noise - xgravity = ((int16_t)inoise8(SEGENV.aux0) - 128); - else // sinusoidal + if(SEGMENT.check3) // random, use perlin noise + xgravity = ((int16_t)inoise8(SEGENV.aux0) - 128); + else // sinusoidal xgravity = (int16_t)cos8(SEGENV.aux0) - 128;//((int32_t)(SEGMENT.custom3 << 2) * cos8(SEGENV.aux0) - // scale the force - xgravity = (xgravity * ((SEGMENT.custom3+1) << 2)) / 128; + // scale the force + xgravity = (xgravity * ((SEGMENT.custom3+1) << 2)) / 128; PartSys->applyForce(xgravity); } uint32_t randomindex = random(PartSys->usedParticles); - PartSys->particles[randomindex].vx = ((int32_t)PartSys->particles[randomindex].vx * 200) / 255; // apply friction to random particle to reduce clumping (without collisions) - + PartSys->particles[randomindex].vx = ((int32_t)PartSys->particles[randomindex].vx * 200) / 255; // apply friction to random particle to reduce clumping (without collisions) -//update colors - PartSys->setColorByPosition(SEGMENT.check1); - if(!SEGMENT.check1) - { - for(i = 0; i < PartSys->usedParticles; i++) - { - PartSys->particles[i].hue = (255 * i) / PartSys->usedParticles; //color by particle index + //update colors + PartSys->setColorByPosition(SEGMENT.check1); + if(!SEGMENT.check1) { + for(i = 0; i < PartSys->usedParticles; i++) { + PartSys->particles[i].hue = (255 * i) / PartSys->usedParticles; //color by particle index } } PartSys->update(); // update and render @@ -10296,30 +9961,23 @@ uint16_t mode_particleBalance(void) } static const char _data_FX_MODE_PS_BALANCE[] PROGMEM = "PS 1D Balance@!,!,Collisions,Blur/Overlay,Tilt,Color by Position,Wrap/Bounce,Random;,!;!;1;pal=18,sx=100,ix=40,c1=200,c2=0,c3=5,o1=1,o2=0,o3=0"; - - /* Particle based Chase effect Uses palette for particle color by DedeHai (Damian Schneider) */ -uint16_t mode_particleChase(void) -{ - if (SEGLEN == 1) - return mode_static(); +uint16_t mode_particleChase(void) { ParticleSystem1D *PartSys = NULL; uint32_t i; - if (SEGMENT.call == 0) // initialization - { + if (SEGMENT.call == 0) { // initialization if (!initParticleSystem1D(PartSys, 1, 255, 3, true)) // init - return mode_static(); // allocation failed - for(i = 0; i < PartSys->numParticles; i++) - { + return mode_static(); // allocation failed or is single pixel + for(i = 0; i < PartSys->numParticles; i++) { PartSys->advPartProps[i].sat = 255; - PartSys->particles[i].ttl = 300; - PartSys->particles[i].perpetual = true; + PartSys->particles[i].ttl = 300; + PartSys->particles[i].perpetual = true; } SEGENV.aux0 = 0xFFFF; // invalidate *PartSys->PSdataEnd = 1; @@ -10327,45 +9985,41 @@ uint16_t mode_particleChase(void) } else PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS - if (PartSys == NULL) return mode_static(); // something went wrong, no data! - + // Particle System settings PartSys->updateSystem(); // update system properties (dimensions and data pointers) - PartSys->setColorByPosition(SEGMENT.check3); + PartSys->setColorByPosition(SEGMENT.check3); PartSys->setMotionBlur(SEGMENT.custom3 << 3); // anable motion blur - //uint8_t* basehue = (PartSys->PSdataEnd + 2); //assign data pointer - - //PartSys->setBounce(SEGMENT.check2); - uint32_t settingssum = SEGMENT.speed + SEGMENT.intensity + SEGMENT.custom1 + SEGMENT.custom2 + SEGMENT.check1 + SEGMENT.check2 + SEGMENT.check3; - if(SEGENV.aux0 != settingssum) //settings changed changed, update - { + //uint8_t* basehue = (PartSys->PSdataEnd + 2); //assign data pointer + + //PartSys->setBounce(SEGMENT.check2); + uint32_t settingssum = SEGMENT.speed + SEGMENT.intensity + SEGMENT.custom1 + SEGMENT.custom2 + SEGMENT.check1 + SEGMENT.check2 + SEGMENT.check3; + if(SEGENV.aux0 != settingssum) { //settings changed changed, update PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 1, min(PartSys->maxX / (32 + (SEGMENT.custom1 >> 1)), int32_t(PartSys->numParticles)))); //depends on intensity and particle size (custom1) SEGENV.step = (PartSys->maxX + (PS_P_RADIUS_1D << 4)) / PartSys->usedParticles; //spacing between particles - // uint32_t remainder = PartSys->maxX - ((PartSys->usedParticles) * SEGENV.step); // unused spacing, distribute this - for(i = 0; i < PartSys->usedParticles; i++) - { - //PartSys->particles[i].x = (i - 1) * SEGENV.step + (((i + 1) * remainder) / PartSys->usedParticles); // distribute evenly - PartSys->particles[i].x = (i - 1) * SEGENV.step; // distribute evenly + // uint32_t remainder = PartSys->maxX - ((PartSys->usedParticles) * SEGENV.step); // unused spacing, distribute this + for(i = 0; i < PartSys->usedParticles; i++) { + //PartSys->particles[i].x = (i - 1) * SEGENV.step + (((i + 1) * remainder) / PartSys->usedParticles); // distribute evenly + PartSys->particles[i].x = (i - 1) * SEGENV.step; // distribute evenly PartSys->particles[i].vx = SEGMENT.speed >> 1; PartSys->advPartProps[i].size = SEGMENT.custom1; - if(SEGMENT.custom2 < 255) + if(SEGMENT.custom2 < 255) PartSys->particles[i].hue = (i * (SEGMENT.custom2 << 3)) / PartSys->usedParticles; // gradient distribution - else + else PartSys->particles[i].hue = random16(); - } - SEGENV.aux0 = settingssum; + } + SEGENV.aux0 = settingssum; } uint32_t huestep = (((uint32_t)SEGMENT.custom2 << 19) / PartSys->usedParticles) >> 16; // hue increment - if(SEGMENT.check1) // pride rainbow colors - { + if(SEGMENT.check1) { // pride rainbow colors //TODO: orignal FX also changes movement speed // also the color change is too fast - int8_t* huedir = reinterpret_cast(PartSys->PSdataEnd); //assign data pointer - int8_t* sizedir = reinterpret_cast(PartSys->PSdataEnd + 1); //assign data pointer + int8_t* huedir = reinterpret_cast(PartSys->PSdataEnd); //assign data pointer + int8_t* sizedir = reinterpret_cast(PartSys->PSdataEnd + 1); //assign data pointer int32_t sizechange = 0; if(PartSys->advPartProps[0].size >= 254) @@ -10378,115 +10032,98 @@ uint16_t mode_particleChase(void) else if(SEGENV.aux1 < 1) *huedir = 1; - if(SEGMENT.call % (1024 / (1 + (SEGMENT.speed >> 3))) == 0) + if(SEGMENT.call % (1024 / (1 + (SEGMENT.speed >> 3))) == 0) SEGENV.aux1 += *huedir; huestep = SEGENV.aux1; // changes gradient spread - - if(SEGMENT.call % (255 / (1 + (SEGMENT.speed >> 2))) == 0) + + if(SEGMENT.call % (255 / (1 + (SEGMENT.speed >> 2))) == 0) sizechange = *sizedir; - - for(i = 0; i < PartSys->usedParticles; i++) - { - // PartSys->particles[i].hue = *basehue + (i * (SEGENV.aux1)) / PartSys->usedParticles; // gradient distribution + + for(i = 0; i < PartSys->usedParticles; i++) { + // PartSys->particles[i].hue = *basehue + (i * (SEGENV.aux1)) / PartSys->usedParticles; // gradient distribution PartSys->advPartProps[i].size += sizechange; } } - if((SEGMENT.check2 || SEGMENT.check1) && SEGMENT.call % (160 / ((SEGMENT.speed >> 3) + 128)) == 0) // color waves - { + if((SEGMENT.check2 || SEGMENT.check1) && SEGMENT.call % (160 / ((SEGMENT.speed >> 3) + 128)) == 0) { // color waves int32_t decrement = 2; if(SEGMENT.check1) decrement = 1; //slower hue change in pride mode - for(i = 0; i < PartSys->usedParticles; i++) - { - PartSys->particles[i].hue -= decrement; + for(i = 0; i < PartSys->usedParticles; i++) { + PartSys->particles[i].hue -= decrement; } } // wrap around (cannot use particle system wrap if distributing colors manually, it also wraps rendering which does not look good) - for(i = 0; i < PartSys->usedParticles; i++) - { - if(PartSys->particles[i].x > PartSys->maxX + PS_P_RADIUS_1D + PartSys->advPartProps[i].size) // wrap it around - { + for(i = 0; i < PartSys->usedParticles; i++) { + if(PartSys->particles[i].x > PartSys->maxX + PS_P_RADIUS_1D + PartSys->advPartProps[i].size) { // wrap it around uint32_t nextindex = (i + 1) % PartSys->usedParticles; PartSys->particles[i].x = PartSys->particles[nextindex].x - SEGENV.step; - if(SEGMENT.custom2 < 255) + if(SEGMENT.custom2 < 255) PartSys->particles[i].hue = PartSys->particles[nextindex].hue - huestep; - else + else PartSys->particles[i].hue = random16(); - } + } } - -PartSys->setParticleSize(SEGMENT.custom1); // if custom1 == 0 this sets rendering size to one pixel + PartSys->setParticleSize(SEGMENT.custom1); // if custom1 == 0 this sets rendering size to one pixel PartSys->update(); // update and render return FRAMETIME; } static const char _data_FX_MODE_PS_CHASE[] PROGMEM = "PS Chase@Speed,Density,Size,Hue,Blur/Overlay,Pride,Color Waves,Color by Position;,!;!;1;pal=11,sx=50,ix=100,c2=5,c3=0,o1=0,o2=0,o3=0"; - - /* Particle Fireworks Starburst replacement (smoother rendering, more settings) Uses palette for particle color by DedeHai (Damian Schneider) */ -uint16_t mode_particleStarburst(void) -{ - if (SEGLEN == 1) - return mode_static(); - ParticleSystem1D *PartSys = NULL; +uint16_t mode_particleStarburst(void) { + ParticleSystem1D *PartSys = NULL; uint32_t i; - if (SEGMENT.call == 0) // initialization - { + if (SEGMENT.call == 0) { // initialization if (!initParticleSystem1D(PartSys, 1, 200, 0, true)) // init - return mode_static(); // allocation failed - PartSys->setKillOutOfBounds(true); - PartSys->enableParticleCollisions(true, 200); + return mode_static(); // allocation failed or is single pixel + PartSys->setKillOutOfBounds(true); + PartSys->enableParticleCollisions(true, 200); PartSys->sources[0].source.ttl = 1; // set initial stanby time PartSys->sources[0].sat = 0; // emitted particles start out white } else PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS - if (PartSys == NULL) return mode_static(); // something went wrong, no data! // Particle System settings - PartSys->updateSystem(); // update system properties (dimensions and data pointers) - PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur PartSys->setGravity(SEGMENT.check1 * 8); // enable gravity - if(PartSys->sources[0].source.ttl-- == 0) // stanby time elapsed TODO: make it a timer? - { - uint32_t explosionsize = 4 + random(SEGMENT.intensity >> 2); - PartSys->sources[0].source.hue = random16(); - PartSys->sources[0].var = 10 + (explosionsize << 1); - PartSys->sources[0].minLife = 250; - PartSys->sources[0].maxLife = 300; - PartSys->sources[0].source.x = random(PartSys->maxX); //random explosion position - PartSys->sources[0].source.ttl = 10 + random16(255 - SEGMENT.speed); - PartSys->sources[0].size = SEGMENT.custom1; // Fragment size - PartSys->setParticleSize(SEGMENT.custom1); // enable advanced size rendering - PartSys->sources[0].source.collide = SEGMENT.check3; - for(uint32_t e = 0; e < explosionsize; e++) // emit particles - { - if(SEGMENT.check2) - PartSys->sources[0].source.hue = random16(); //random color for each particle - PartSys->sprayEmit(PartSys->sources[0]); //emit a particle - } + if (PartSys->sources[0].source.ttl-- == 0) { // stanby time elapsed TODO: make it a timer? + uint32_t explosionsize = 4 + random(SEGMENT.intensity >> 2); + PartSys->sources[0].source.hue = random16(); + PartSys->sources[0].var = 10 + (explosionsize << 1); + PartSys->sources[0].minLife = 250; + PartSys->sources[0].maxLife = 300; + PartSys->sources[0].source.x = random(PartSys->maxX); //random explosion position + PartSys->sources[0].source.ttl = 10 + random16(255 - SEGMENT.speed); + PartSys->sources[0].size = SEGMENT.custom1; // Fragment size + PartSys->setParticleSize(SEGMENT.custom1); // enable advanced size rendering + PartSys->sources[0].source.collide = SEGMENT.check3; + for (uint32_t e = 0; e < explosionsize; e++) { // emit particles + if (SEGMENT.check2) + PartSys->sources[0].source.hue = random16(); //random color for each particle + PartSys->sprayEmit(PartSys->sources[0]); //emit a particle + } } //shrink all particles - for(i = 0; i < PartSys->usedParticles; i++) - { - if(PartSys->advPartProps[i].size) + for (i = 0; i < PartSys->usedParticles; i++) { + if (PartSys->advPartProps[i].size) PartSys->advPartProps[i].size--; - if(PartSys->advPartProps[i].sat < 251) - PartSys->advPartProps[i].sat += 1 + (SEGMENT.custom3 >> 2); //note: it should be >> 3, the >> 2 creates overflows resulting in blinking if custom3 > 27, which is a bonus feature - } - - if(SEGMENT.call % 5 == 0) - { + if (PartSys->advPartProps[i].sat < 251) + PartSys->advPartProps[i].sat += 1 + (SEGMENT.custom3 >> 2); //note: it should be >> 3, the >> 2 creates overflows resulting in blinking if custom3 > 27, which is a bonus feature + } + + if (SEGMENT.call % 5 == 0) { PartSys->applyFriction(1); //slow down particles } @@ -10495,177 +10132,150 @@ uint16_t mode_particleStarburst(void) } static const char _data_FX_MODE_PS_STARBURST[] PROGMEM = "PS Starburst@Chance,Fragments,Fragment Size,Blur/Overlay,Cooling,Gravity,Colorful,Push;,!;!;1;pal=52,sx=150,ix=150,c1=120,c2=0,c3=21,o1=0,o2=0,o3=0"; - - /* Particle based 1D GEQ effect, each frequency bin gets an emitter, distributed over the strip Uses palette for particle color by DedeHai (Damian Schneider) */ -uint16_t mode_particle1DGEQ(void) -{ - if (SEGLEN == 1) - return mode_static(); +uint16_t mode_particle1DGEQ(void) { ParticleSystem1D *PartSys = NULL; uint32_t numSources; uint32_t i; - if (SEGMENT.call == 0) // initialization - { + if (SEGMENT.call == 0) { // initialization if (!initParticleSystem1D(PartSys, 16, 255, 0, true)) // init, no additional data needed - return mode_static(); // allocation failed + return mode_static(); // allocation failed or is single pixel } else PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS - if (PartSys == NULL) return mode_static(); // something went wrong, no data! // Particle System settings PartSys->updateSystem(); // update system properties (dimensions and data pointers) numSources = PartSys->numSources; - PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur + PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur uint32_t spacing = PartSys->maxX / numSources; - for(i = 0; i < numSources; i++) - { - PartSys->sources[i].source.hue = i * 16;//random16(); //TODO: make adjustable, maybe even colorcycle? + for (i = 0; i < numSources; i++) { + PartSys->sources[i].source.hue = i * 16; //random16(); //TODO: make adjustable, maybe even colorcycle? PartSys->sources[i].var = SEGMENT.speed >> 3; PartSys->sources[i].minLife = 180 + (SEGMENT.intensity >> 1); PartSys->sources[i].maxLife = 240 + SEGMENT.intensity; PartSys->sources[i].sat = 255; PartSys->sources[i].size = SEGMENT.custom1; PartSys->setParticleSize(SEGMENT.custom1); - PartSys->sources[i].source.x = (spacing >> 1) + spacing * i; //distribute evenly + PartSys->sources[i].source.x = (spacing >> 1) + spacing * i; //distribute evenly } - for(i = 0; i < PartSys->usedParticles; i++) - { - if(PartSys->particles[i].ttl > 20) PartSys->particles[i].ttl -= 20; //ttl is linked to brightness, this allows to use higher brightness but still a short lifespan + for (i = 0; i < PartSys->usedParticles; i++) { + if (PartSys->particles[i].ttl > 20) PartSys->particles[i].ttl -= 20; //ttl is linked to brightness, this allows to use higher brightness but still a short lifespan else PartSys->particles[i].ttl = 0; } - + um_data_t *um_data; - if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) + if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) um_data = simulateSound(SEGMENT.soundSim); // add support for no audio - + uint8_t *fftResult = (uint8_t *)um_data->u_data[2]; // 16 bins with FFT data, log mapped already, each band contains frequency amplitude 0-255 //map the bands into 16 positions on x axis, emit some particles according to frequency loudness i = 0; - uint32_t bin = random(numSources);; //current bin , start with random one to distribute available particles fairly + uint32_t bin = random(numSources); //current bin , start with random one to distribute available particles fairly uint32_t threshold = 300 - SEGMENT.intensity; - for (i = 0; i < numSources; i++) - { - bin ++; + for (i = 0; i < numSources; i++) { + bin++; bin = bin % numSources; uint32_t emitparticle = 0; - // uint8_t emitspeed = ((uint32_t)fftResult[bin] * (uint32_t)SEGMENT.speed) >> 10; // emit speed according to loudness of band (127 max!) - if (fftResult[bin] > threshold) - { + // uint8_t emitspeed = ((uint32_t)fftResult[bin] * (uint32_t)SEGMENT.speed) >> 10; // emit speed according to loudness of band (127 max!) + if (fftResult[bin] > threshold) { emitparticle = 1; } - else if(fftResult[bin] > 0)// band has low volue - { - uint32_t restvolume = ((threshold - fftResult[bin])>>2) + 2; - if (random16() % restvolume == 0) - { + else if (fftResult[bin] > 0) { // band has low volue + uint32_t restvolume = ((threshold - fftResult[bin]) >> 2) + 2; + if (random16() % restvolume == 0) { emitparticle = 1; } } - if(emitparticle) - { - PartSys->sprayEmit(PartSys->sources[bin]); - } + if (emitparticle) + PartSys->sprayEmit(PartSys->sources[bin]); } //TODO: add color control? - + PartSys->update(); // update and render - + return FRAMETIME; } static const char _data_FX_MODE_PS_1D_GEQ[] PROGMEM = "PS 1D GEQ@Speed,!,Size,Blur/Overlay,,,,;,!;!;1f;pal=0,sx=50,ix=200,c1=0,c2=0,c3=0,o1=1,o2=1,o3=0"; - - /* -Particle based Fire effect +Particle based Fire effect Uses palette for particle color by DedeHai (Damian Schneider) */ +uint16_t mode_particleFire1D(void) { + ParticleSystem1D *PartSys = NULL; -uint16_t mode_particleFire1D(void) -{ - if (SEGLEN == 1) - return mode_static(); - ParticleSystem1D *PartSys = NULL; - - if (SEGMENT.call == 0) // initialization - { + if (SEGMENT.call == 0) { // initialization if (!initParticleSystem1D(PartSys, 5)) // init - return mode_static(); // allocation failed + return mode_static(); // allocation failed or is single pixel PartSys->setKillOutOfBounds(true); PartSys->setParticleSize(1); } else PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS - if (PartSys == NULL) return mode_static(); // something went wrong, no data! // Particle System settings PartSys->updateSystem(); // update system properties (dimensions and data pointers) - PartSys->setMotionBlur(128 + (SEGMENT.custom2 >> 1)); // anable motion blur - PartSys->setColorByAge(true); + PartSys->setMotionBlur(128 + (SEGMENT.custom2 >> 1)); // enable motion blur + PartSys->setColorByAge(true); uint32_t emitparticles = 1; uint32_t j = random16(); - for(uint i = 0; i < 3; i++) - { - if(PartSys->sources[i].source.ttl > 50) - PartSys->sources[i].source.ttl -= 10; //TODO: in 2D making the source fade out slow results in much smoother flames, need to check if it can be done the same - else - PartSys->sources[i].source.ttl = 100 + random16(200); // base flame - } - for(uint i = 0; i < PartSys->numSources; i++) - { + for (uint i = 0; i < 3; i++) { + if (PartSys->sources[i].source.ttl > 50) + PartSys->sources[i].source.ttl -= 10; // TODO: in 2D making the source fade out slow results in much smoother flames, need to check if it can be done the same + else + PartSys->sources[i].source.ttl = 100 + random16(200); // base flame + } + for (uint i = 0; i < PartSys->numSources; i++) { j = (j + 1) % PartSys->numSources; - PartSys->sources[j].source.x = 0; - PartSys->sources[j].var = 2 + (SEGMENT.speed >> 4); - //base flames - if(j > 2) { - PartSys->sources[j].minLife = 150 + SEGMENT.intensity + (j << 2); //TODO: in 2D, min life is maxlife/2 and that looks very nice - PartSys->sources[j].maxLife = 200 + SEGMENT.intensity + (j << 3); - PartSys->sources[j].v = (SEGMENT.speed >> (2 + (j<<1))); - if(emitparticles) - { + PartSys->sources[j].source.x = 0; + PartSys->sources[j].var = 2 + (SEGMENT.speed >> 4); + // base flames + if (j > 2) { + PartSys->sources[j].minLife = 150 + SEGMENT.intensity + (j << 2); // TODO: in 2D, min life is maxlife/2 and that looks very nice + PartSys->sources[j].maxLife = 200 + SEGMENT.intensity + (j << 3); + PartSys->sources[j].v = (SEGMENT.speed >> (2 + (j << 1))); + if (emitparticles) { emitparticles--; - PartSys->sprayEmit(PartSys->sources[j]); //emit a particle + PartSys->sprayEmit(PartSys->sources[j]); // emit a particle } } - else{ - PartSys->sources[j].minLife = PartSys->sources[j].source.ttl + SEGMENT.intensity; //TODO: in 2D, emitted particle ttl depends on source TTL, mimic here the same way? OR: change 2D to the same way it is done here and ditch special fire treatment in emit? - PartSys->sources[j].maxLife = PartSys->sources[j].minLife + 50; + else { + PartSys->sources[j].minLife = PartSys->sources[j].source.ttl + SEGMENT.intensity; // TODO: in 2D, emitted particle ttl depends on source TTL, mimic here the same way? OR: change 2D to the same way it is done here and ditch special fire treatment in emit? + PartSys->sources[j].maxLife = PartSys->sources[j].minLife + 50; PartSys->sources[j].v = SEGMENT.speed >> 2; - if(SEGENV.call & 0x01) //every second frame - PartSys->sprayEmit(PartSys->sources[j]); //emit a particle + if (SEGENV.call & 0x01) // every second frame + PartSys->sprayEmit(PartSys->sources[j]); // emit a particle } } - for(uint i = 0; i < PartSys->usedParticles; i++) - { + for (uint i = 0; i < PartSys->usedParticles; i++) { PartSys->particles[i].x += PartSys->particles[i].ttl >> 7; // 'hot' particles are faster, apply some extra velocity - if(PartSys->particles[i].ttl > 3 + ((255 - SEGMENT.custom1) >> 1)) - PartSys->particles[i].ttl -= map(SEGMENT.custom1, 0, 255, 1 , 3); // age faster + if (PartSys->particles[i].ttl > 3 + ((255 - SEGMENT.custom1) >> 1)) + PartSys->particles[i].ttl -= map(SEGMENT.custom1, 0, 255, 1, 3); // age faster } PartSys->update(); // update and render - + return FRAMETIME; } static const char _data_FX_MODE_PS_FIRE1D[] PROGMEM = "PS Fire 1D@!,!,Cooling,Blur/Overlay;,!;!;1;pal=35,sx=100,ix=50,c1=80,c2=100,c3=28,o1=1,o2=1,o3=0"; -#endif //WLED_DISABLE_PARTICLESYSTEM1D +#endif // WLED_DISABLE_PARTICLESYSTEM1D ////////////////////////////////////////////////////////////////////////////////////////// // mode data diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index 5d91d49623..d52e7e7822 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -1053,8 +1053,8 @@ void blur2D(CRGB *colorbuffer, uint32_t xsize, uint32_t ysize, uint32_t xblur, u //non class functions to use for initialization uint32_t calculateNumberOfParticles2D(bool isadvanced, bool sizecontrol) { - uint32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; - uint32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); + uint32_t cols = SEGMENT.virtualWidth(); + uint32_t rows = SEGMENT.virtualHeight(); #ifdef ESP8266 uint32_t numberofParticles = (cols * rows * 3) / 4; // 0.75 particle per pixel uint32_t particlelimit = ESP8266_MAXPARTICLES; // maximum number of paticles allowed (based on one segment of 16x16 and 4k effect ram) @@ -1077,8 +1077,8 @@ uint32_t calculateNumberOfParticles2D(bool isadvanced, bool sizecontrol) { } uint32_t calculateNumberOfSources2D(uint8_t requestedsources) { - uint32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; - uint32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); + uint32_t cols = SEGMENT.virtualWidth(); + uint32_t rows = SEGMENT.virtualHeight(); #ifdef ESP8266 int numberofSources = min((cols * rows) / 8, (uint32_t)requestedsources); numberofSources = max(1, min(numberofSources, ESP8266_MAXSOURCES)); // limit to 1 - 16 @@ -1110,6 +1110,7 @@ bool allocateParticleSystemMemory2D(uint16_t numparticles, uint16_t numsources, // initialize Particle System, allocate additional bytes if needed (pointer to those bytes can be read from particle system class: PSdataEnd) bool initParticleSystem2D(ParticleSystem2D *&PartSys, uint8_t requestedsources, uint16_t additionalbytes, bool advanced, bool sizecontrol) { + if(!strip.isMatrix) return false; // only for 2D uint32_t numparticles = calculateNumberOfParticles2D(advanced, sizecontrol); uint32_t numsources = calculateNumberOfSources2D(requestedsources); if (!allocateParticleSystemMemory2D(numparticles, numsources, advanced, sizecontrol, additionalbytes)) @@ -1727,6 +1728,7 @@ bool allocateParticleSystemMemory1D(uint16_t numparticles, uint16_t numsources, // initialize Particle System, allocate additional bytes if needed (pointer to those bytes can be read from particle system class: PSdataEnd) // note: requestedparticles is relative, 127 = 50%, 255 = 100% (deafaults to 100% meaning one particle per pixel) bool initParticleSystem1D(ParticleSystem1D *&PartSys, uint32_t requestedsources, uint32_t requestedparticles, uint16_t additionalbytes, bool advanced) { + if (SEGLEN == 1) return false; // single pixel not supported uint32_t numparticles = (requestedparticles * calculateNumberOfParticles1D(advanced)) / 255; uint32_t numsources = calculateNumberOfSources1D(requestedsources); if (!allocateParticleSystemMemory1D(numparticles, numsources, advanced, additionalbytes)) { From 09ffeca2c517f541b4a007ff8afdc181b5227e7b Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Thu, 31 Oct 2024 09:06:24 +0100 Subject: [PATCH 136/219] missed a spot --- wled00/FX.cpp | 4 ---- 1 file changed, 4 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index d03c5776fb..2547ac8937 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -8313,10 +8313,6 @@ static const char _data_FX_MODE_PARTICLEPIT[] PROGMEM = "PS Ballpit@Speed,Intens * 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(); uint16_t mode_particlewaterfall(void) { ParticleSystem2D *PartSys = NULL; uint8_t numSprays; From e2ab1c7026102c3f4296042b04925690909de51a Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sat, 2 Nov 2024 12:59:43 +0100 Subject: [PATCH 137/219] minor tweaks to fast_color_add() --- wled00/FXparticleSystem.cpp | 34 ++++++++++++++++------------------ wled00/FXparticleSystem.h | 2 +- 2 files changed, 17 insertions(+), 19 deletions(-) diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index d52e7e7822..cec5e80dce 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -1807,41 +1807,39 @@ static bool checkBoundsAndWrap(int32_t &position, const int32_t max, const int32 return true; // particle is in bounds } -// fastled color adding is very inaccurate in color preservation +// fastled color adding is very inaccurate in color preservation (but it is fast) // a better color add function is implemented in colors.cpp but it uses 32bit RGBW. to use it colors need to be shifted just to then be shifted back by that function, which is slow -// this is a fast version for RGB (no white channel, PS does not handle white) and with native CRGB including scaling of second color (fastled scale8 can be made faster using native 32bit on ESP) -// note: result is stored in c1, so c1 will contain the result. not using a return value is much faster as the struct does not need to be copied upon return -static void fast_color_add(CRGB &c1, const CRGB &c2, uint32_t scale) { - //note: function is manly used to add scaled colors, so checking if one color is black is slower +// this is a fast version for RGB (no white channel, PS does not handle white) and with native CRGB including scaling of second color +// note: result is stored in c1, not using a return value is faster as the CRGB struct does not need to be copied upon return +// note2: function is mainly used to add scaled colors, so checking if one color is black is slower +// note3: scale is 255 when using blur, checking for that makes blur faster +static void fast_color_add(CRGB &c1, const CRGB &c2, const uint32_t scale) { uint32_t r, g, b; if (scale < 255) { r = c1.r + ((c2.r * scale) >> 8); g = c1.g + ((c2.g * scale) >> 8); b = c1.b + ((c2.b * scale) >> 8); - } - else { + } else { r = c1.r + c2.r; g = c1.g + c2.g; b = c1.b + c2.b; } - uint32_t max = r; - if (g > max) max = g; - if (b > max) max = b; + + uint32_t max = std::max(r,g); // check for overflow, using max() is faster as the compiler can optimize + max = std::max(max,b); if (max < 256) { c1.r = r; // save result to c1 c1.g = g; c1.b = b; - } - else { - uint32_t scale = (255 << 16) / max; // to avoid multiple divisions - c1.r = (r * scale) >> 16; // (c * 255) / max; - c1.g = (g * scale) >> 16; - c1.b = (b * scale) >> 16; + } else { + c1.r = (r * 255) / max; // note: compile optimizes the divisions, no need to manually optimize + c1.g = (g * 255) / max; + c1.b = (b * 255) / max; } } -// faster than fastled color scaling as it uses a 32bit scale factor and pointer -static void fast_color_scale(CRGB &c, uint32_t scale) { +// faster than fastled color scaling as it does in place scaling +static void fast_color_scale(CRGB &c, const uint32_t scale) { c.r = ((c.r * scale) >> 8); c.g = ((c.g * scale) >> 8); c.b = ((c.b * scale) >> 8); diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index 6cb1a7975a..15ab649b92 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -20,7 +20,7 @@ static int32_t calcForce_dv(int8_t force, uint8_t *counter); static int32_t limitSpeed(int32_t speed); static bool checkBoundsAndWrap(int32_t &position, const int32_t max, const int32_t particleradius, bool wrap); // returns false if out of bounds by more than particleradius static void fast_color_add(CRGB &c1, const CRGB &c2, uint32_t scale = 255); // fast and accurate color adding with scaling (scales c2 before adding) -static void fast_color_scale(CRGB &c, uint32_t scale); // fast scaling function using 32bit variable and pointer. note: keep 'scale' within 0-255 +static void fast_color_scale(CRGB &c, const uint32_t scale); // fast scaling function using 32bit variable and pointer. note: keep 'scale' within 0-255 static CRGB *allocate1Dbuffer(uint32_t length); #endif From f23da9f356572e6d07add21832f7561eaa53da3d Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sun, 3 Nov 2024 10:48:17 +0100 Subject: [PATCH 138/219] Work in progress: update to fireworks, needs more testing & finetuning also bugs fixed and some minor cleanup --- wled00/FX.cpp | 158 ++++++++++++++++-------------------- wled00/FXparticleSystem.cpp | 13 ++- wled00/FXparticleSystem.h | 4 +- 3 files changed, 78 insertions(+), 97 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 2547ac8937..7871ae9483 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -7915,22 +7915,21 @@ static const char _data_FX_MODE_PARTICLEVORTEX[] PROGMEM = "PS Vortex@Rotation S /* * Particle Fireworks * Rockets shoot up and explode in a random color, sometimes in a defined pattern - * Uses ranbow palette as default * by DedeHai (Damian Schneider) */ #define NUMBEROFSOURCES 4 + uint16_t mode_particlefireworks(void) { ParticleSystem2D *PartSys = NULL; - uint8_t numRockets; - uint32_t i = 0; - uint32_t j = 0; + uint32_t numRockets; + uint32_t i, j; if (SEGMENT.call == 0) { // initialization if (!initParticleSystem2D(PartSys, NUMBEROFSOURCES, true)) return mode_static(); // allocation failed PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting) - PartSys->setWallHardness(100); // ground bounce is fixed + PartSys->setWallHardness(120); // ground bounce is fixed numRockets = min(PartSys->numSources, (uint32_t)NUMBEROFSOURCES); for (j = 0; j < numRockets; j++) { PartSys->sources[j].source.ttl = 500 * j; // first rocket starts immediately, others follow soon @@ -7948,116 +7947,106 @@ uint16_t mode_particlefireworks(void) { PartSys->setWrapX(SEGMENT.check1); PartSys->setBounceY(SEGMENT.check2); - PartSys->setGravity(map(SEGMENT.custom3, 0, 31, 0, 10)); + PartSys->setGravity(map(SEGMENT.custom3, 0, 31, SEGMENT.check2 ? 1 : 0, 10)); // if bounded, set gravity to minimum of 1 or they will bounce at top + // update the rockets, set the speed state + for (j = 0; j < numRockets; j++) { + PartSys->applyGravity(PartSys->sources[j].source); + PartSys->particleMoveUpdate(PartSys->sources[j].source); + if (PartSys->sources[j].source.ttl == 0) { + if (PartSys->sources[j].source.vy > 0) { // rocket has died and is moving up. stop it so it will explode (is handled in the code below) + PartSys->sources[j].source.vy = 0; + } + else if (PartSys->sources[j].source.vy < 0) { // rocket is exploded and time is up (ttl=0 and negative speed), relaunch it + PartSys->sources[j].source.y = PS_P_RADIUS; // start from bottom + PartSys->sources[j].source.x = (PartSys->maxX >> 2) + random(PartSys->maxX >> 1); // centered half + PartSys->sources[j].source.vy = (SEGMENT.custom3) + random16(SEGMENT.custom1 >> 3) + 5; // rocket speed TODO: need to adjust for segment height + PartSys->sources[j].source.vx = random(-3, 3); // not perfectly straight up + PartSys->sources[j].source.sat = 30; // low saturation -> exhaust is off-white + PartSys->sources[j].source.ttl = random16(SEGMENT.custom1) + (SEGMENT.custom1 >> 1); // set fuse time + PartSys->sources[j].maxLife = 40; // exhaust particle life + PartSys->sources[j].minLife = 10; + PartSys->sources[j].vx = 0; // emitting speed + PartSys->sources[j].vy = -5; // emitting speed + PartSys->sources[j].var = 4; // speed variation around vx,vy (+/- var) + } + } + } // check each rocket's state and emit particles according to its state: moving up = emit exhaust, at top = explode; falling down = standby time - uint32_t emitparticles; // number of particles to emit for each rocket's state - - // variables for circle explosions - uint8_t speed; - uint8_t currentspeed; + uint32_t emitparticles, frequency, baseangle, hueincrement; // number of particles to emit for each rocket's state + // variables for circular explosions + int32_t speed, currentspeed, speedvariation, counter, percircle; uint16_t angle; - uint8_t counter; - uint16_t angleincrement; - uint8_t percircle; - uint8_t speedvariation; + unsigned angleincrement; bool circularexplosion = false; + + // emit particles for each rocket for (j = 0; j < numRockets; j++) { // determine rocket state by its speed: - if (PartSys->sources[j].source.vy > 0) { - // moving up, emit exhaust + if (PartSys->sources[j].source.vy > 0) { // moving up, emit exhaust emitparticles = 1; } - else if (PartSys->sources[j].source.vy < 0) { - // falling down + else if (PartSys->sources[j].source.vy < 0) { // falling down, standby time emitparticles = 0; } else { // speed is zero, explode! + PartSys->sources[j].source.hue = random16(); // random color + PartSys->sources[j].source.sat = random16(55) + 200; + PartSys->sources[j].maxLife = 200; + PartSys->sources[j].minLife = 100; + PartSys->sources[j].source.ttl = random16((2000 - ((uint32_t)SEGMENT.speed << 2))) + 550 - (SEGMENT.speed << 1); // standby time til next launch + PartSys->sources[j].var = ((SEGMENT.intensity >> 4) + 5); // speed variation around vx,vy (+/- var) + PartSys->sources[j].source.vy = -1; // set speed negative so it will emit no more particles after this explosion until relaunch #ifdef ESP8266 emitparticles = random16(SEGMENT.intensity >> 3) + (SEGMENT.intensity >> 3) + 5; // defines the size of the explosion #else emitparticles = random16(SEGMENT.intensity >> 2) + (SEGMENT.intensity >> 2) + 5; // defines the size of the explosion #endif - PartSys->sources[j].source.vy = -1; // set speed negative so it will emit no more particles after this explosion until relaunch - if (random16(4) == 0) { + + if (random16() & 1) + { // 50% chance for circular explosion circularexplosion = true; - speed = 2 + random16(3); + speed = 2 + random16(3) + ((SEGMENT.intensity >> 6)); currentspeed = speed; counter = 0; - angleincrement = 2730 + random16(5461); // minimum 15° (=2730), + random(30°) (=5461) + angleincrement = 2730 + random16(5461); // minimum 15° + random(30°) angle = random16(); // random start angle - speedvariation = angle & 0x01; // 0 or 1, no need for a new random number - // calculate the number of particles to make complete circles - percircle = (uint16_t)0xFFFF / angleincrement + 1; - int circles = (SEGMENT.intensity >> 6) + 1; + baseangle = angle; // save base angle for modulation + percircle = 0xFFFF / angleincrement + 1; // number of particles to make complete circles + hueincrement = random16() & 127; // &127 is equivalent to %128 + int circles = 1 + random16(3) + ((SEGMENT.intensity >> 6)); + frequency = random16() & 127; // modulation frequency (= "waves per circle"), x.4 fixed point emitparticles = percircle * circles; - PartSys->sources[j].var = 0; // no variation for nicer circles + PartSys->sources[j].var = angle & 1; // 0 or 1 variation, angle is random } } + for (i = 0; i < emitparticles; i++) { - if (circularexplosion) // do circle emit - { - if (counter & 0x01) // make every second particle a lower speed - currentspeed = speed - speedvariation; - else - currentspeed = speed; + if (circularexplosion) { + int32_t sineMod = 0xEFFF + sin16((uint16_t)(((angle * frequency) >> 4) + baseangle)); // shifted to positive values + currentspeed = (speed/2 + ((sineMod * speed) >> 16)) >> 1; // sine modulation on speed based on emit angle PartSys->angleEmit(PartSys->sources[j], angle, currentspeed); // note: compiler warnings can be ignored, variables are set just above counter++; - if (counter > percircle) // full circle completed, increase speed - { + if (counter > percircle) { // full circle completed, increase speed counter = 0; - speed += 5; // increase speed to form a second circle - speedvariation = speedvariation ? speedvariation + random16(4) : 0; // double speed variation - PartSys->sources[j].source.hue = random16(); // new color for next circle + speed += 3 + ((SEGMENT.intensity >> 6)); // increase speed to form a second wave + PartSys->sources[j].source.hue += hueincrement; // new color for next circle PartSys->sources[j].source.sat = min((uint16_t)150, random16()); } angle += angleincrement; // set angle for next particle } - else { + else { // random explosion or exhaust PartSys->sprayEmit(PartSys->sources[j]); if ((j % 3) == 0) { PartSys->sources[j].source.hue = random16(); // random color for each particle (this is also true for exhaust, but that is white anyways) - // PartSys->sources[j].source.sat = min((uint16_t)150, random16()); // dont change saturation, this can also be exhaust! } } } if (i == 0) // no particles emitted, this rocket is falling - PartSys->sources[j].source.y = 1000; // set position up high so gravity wont pull it to the ground and bounce it (vy MUST stay negative until relaunch) + PartSys->sources[j].source.y = 1000; // reset position so gravity wont pull it to the ground and bounce it (vy MUST stay negative until relaunch) circularexplosion = false; // reset for next rocket } - // update the rockets, set the speed state - for (j = 0; j < numRockets; j++) { - if (PartSys->sources[j].source.ttl) { - PartSys->particleMoveUpdate(PartSys->sources[j].source); - } - else if (PartSys->sources[j].source.vy > 0) // rocket has died and is moving up. stop it so it will explode (is handled in the code above) - { - PartSys->sources[j].source.vy = 0; // set speed to zero so code above will recognize this as an exploding rocket - PartSys->sources[j].source.hue = random16(); // random color - PartSys->sources[j].source.sat = random16(55) + 200; - PartSys->sources[j].maxLife = 200; - PartSys->sources[j].minLife = 100; - PartSys->sources[j].source.ttl = random16((1200 - ((uint32_t)SEGMENT.speed << 2))) + 550 - (SEGMENT.speed << 1); // standby time til next launch - PartSys->sources[j].var = ((SEGMENT.intensity >> 3) + 10) | 0x01; // speed variation around vx,vy (+/- var/2), only use odd numbers - } - else if (PartSys->sources[j].source.vy < 0) // rocket is exploded and time is up (ttl=0 and negative speed), relaunch it - { - // reinitialize rocket - PartSys->sources[j].source.y = PS_P_RADIUS << 1; // start from bottom - PartSys->sources[j].source.x = random(PartSys->maxX >> 2, PartSys->maxX >> 1); // centered half - PartSys->sources[j].source.vy = random16(SEGMENT.custom1 >> 3) + 5; // rocket speed depends also on rocket fuse - PartSys->sources[j].source.vx = random(-3, 3); // not perfectly straight up - PartSys->sources[j].source.sat = 30; // low saturation -> exhaust is off-white - PartSys->sources[j].source.ttl = random16(SEGMENT.custom1) + (SEGMENT.custom1 >> 1); // sets explosion height (rockets explode at the top if set too high as paticle update set speed to zero if moving out of matrix) - PartSys->sources[j].maxLife = 40; // exhaust particle life - PartSys->sources[j].minLife = 10; - PartSys->sources[j].vx = 0; // emitting speed - PartSys->sources[j].vy = 0; // emitting speed - PartSys->sources[j].var = 3; // speed variation around vx,vy (+/- var/2) - } - } - PartSys->update(); // update and render return FRAMETIME; } @@ -8167,8 +8156,7 @@ uint16_t mode_particlefire(void) { if (SEGMENT.speed < 100) { //slow, limit FPS uint32_t *lastcall = reinterpret_cast(PartSys->PSdataEnd); uint32_t period = strip.now - *lastcall; - if (period < (uint32_t)map(SEGMENT.speed, 0, 99, 50, 10)) // limit to 90FPS - 20FPS - { + if (period < (uint32_t)map(SEGMENT.speed, 0, 99, 50, 10)) { // limit to 90FPS - 20FPS SEGMENT.call--; //skipping a frame, decrement the counter (on call0, this is never executed as lastcall is 0, so its fine to not check if >0) //still need to render the frame or flickering will occur in transitions PartSys->updateFire(SEGMENT.intensity, true); // render the fire without updating particles (render only) @@ -8183,12 +8171,9 @@ uint16_t mode_particlefire(void) { // update the flame sprays: for (i = 0; i < numFlames; i++) { - if (SEGMENT.call & 1 && PartSys->sources[i].source.ttl > 0) // every second frame - { + if (SEGMENT.call & 1 && PartSys->sources[i].source.ttl > 0) { // every second frame PartSys->sources[i].source.ttl--; - } - else // flame source is dead: initialize new flame: set properties of source - { + } else { // flame source is dead: initialize new flame: set properties of source PartSys->sources[i].source.x = (PartSys->maxX >> 1) - (spread >> 1) + random(spread); // change flame position: distribute randomly on chosen width PartSys->sources[i].source.y = -(PS_P_RADIUS << 2); // set the source below the frame PartSys->sources[i].source.ttl = 20 + random((SEGMENT.custom1 * SEGMENT.custom1) >> 8) / (1 + (firespeed >> 5)); //'hotness' of fire, faster flames reduce the effect or flame height will scale too much with speed @@ -8261,12 +8246,10 @@ uint16_t mode_particlepit(void) { PartSys->setBounceX(SEGMENT.check2); PartSys->setBounceY(SEGMENT.check3); PartSys->setWallHardness(min(SEGMENT.custom2, (uint8_t)150)); // limit to 100 min (if collisions are disabled, still want bouncy) - if (SEGMENT.custom2 > 0) { + if (SEGMENT.custom2 > 0) PartSys->enableParticleCollisions(true, SEGMENT.custom2); // enable collisions and set particle collision hardness - } - else { + else PartSys->enableParticleCollisions(false); - } uint32_t i; if (SEGMENT.call % (128 - (SEGMENT.intensity >> 1)) == 0 && SEGMENT.intensity > 0) { // every nth frame emit particles, stop emitting if set to zero @@ -8285,8 +8268,7 @@ uint16_t mode_particlepit(void) { if (SEGMENT.custom1 == 255) { PartSys->setParticleSize(0); // set global size to zero PartSys->advPartProps[i].size = random(SEGMENT.custom1); // set each particle to random size - } - else { + } else { PartSys->setParticleSize(SEGMENT.custom1); // set global size PartSys->advPartProps[i].size = 0; // use global size } @@ -8596,7 +8578,7 @@ uint16_t mode_particleimpact(void) { if (PartSys->sources[i].source.ttl) { PartSys->sources[i].source.ttl--; // note: this saves an if statement, but moving down particles age twice if (PartSys->sources[i].source.vy < 0) { //move down - PartSys->applyGravity(&PartSys->sources[i].source); + PartSys->applyGravity(PartSys->sources[i].source); PartSys->particleMoveUpdate(PartSys->sources[i].source, &meteorsettings); // if source reaches the bottom, set speed to 0 so it will explode on next function call (handled above) diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index cec5e80dce..8dfe280e61 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -10,8 +10,7 @@ /* TODO: - -add function to 'update sources' so FX does not have to take care of that. FX can still implement its own version if so desired. - -add an x/y struct, do particle rendering using that, much easier to read + - change passing particle pointers to references (if possible) -add underscore to private variables */ @@ -196,9 +195,9 @@ void ParticleSystem2D::flameEmit(PSsource &emitter) { // Emits a particle at given angle and speed, angle is from 0-65535 (=0-360deg), speed is also affected by emitter->var // angle = 0 means in positive x-direction (i.e. to the right) -int32_t ParticleSystem2D::angleEmit(PSsource &emitter, uint16_t angle, int8_t speed, uint32_t amount) { - emitter.vx = ((int32_t)cos16(angle) * (int32_t)speed) / (int32_t)32600; // cos16() and sin16() return signed 16bit, division should be 32767 but 32600 gives slightly better rounding - emitter.vy = ((int32_t)sin16(angle) * (int32_t)speed) / (int32_t)32600; // note: cannot use bit shifts as bit shifting is asymmetrical for positive and negative numbers and this needs to be accurate! +int32_t ParticleSystem2D::angleEmit(PSsource &emitter, uint16_t angle, int32_t speed, uint32_t amount) { + emitter.vx = ((int32_t)cos16(angle) * speed) / (int32_t)32600; // cos16() and sin16() return signed 16bit, division should be 32767 but 32600 gives slightly better rounding + emitter.vy = ((int32_t)sin16(angle) * speed) / (int32_t)32600; // note: cannot use bit shifts as bit shifting is asymmetrical for positive and negative numbers and this needs to be accurate! return sprayEmit(emitter, amount); } @@ -472,11 +471,11 @@ void ParticleSystem2D::applyGravity() { // apply gravity to single particle using system settings (use this for sources) // function does not increment gravity counter, if gravity setting is disabled, this cannot be used -void ParticleSystem2D::applyGravity(PSparticle *part) { +void ParticleSystem2D::applyGravity(PSparticle &part) { uint32_t counterbkp = gforcecounter; // backup PS gravity counter int32_t dv = calcForce_dv(gforce, &gforcecounter); gforcecounter = counterbkp; //save it back - part->vy = limitSpeed((int32_t)part->vy - dv); + part.vy = limitSpeed((int32_t)part.vy - dv); } // slow down particle by friction, the higher the speed, the higher the friction. a high friction coefficient slows them more (255 means instant stop) diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index 15ab649b92..a42cea3f9b 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -120,9 +120,9 @@ class ParticleSystem2D { // particle emitters int32_t sprayEmit(PSsource &emitter, uint32_t amount = 1); void flameEmit(PSsource &emitter); - int32_t angleEmit(PSsource& emitter, uint16_t angle, int8_t speed, uint32_t amount = 1); + int32_t angleEmit(PSsource& emitter, uint16_t angle, int32_t speed, uint32_t amount = 1); //particle physics - void applyGravity(PSparticle *part); // applies gravity to single particle (use this for sources) + void applyGravity(PSparticle &part); // applies gravity to single particle (use this for sources) void applyForce(PSparticle *part, int8_t xforce, int8_t yforce, uint8_t *counter); void applyForce(uint16_t particleindex, int8_t xforce, int8_t yforce); // use this for advanced property particles void applyForce(int8_t xforce, int8_t yforce); // apply a force to all particles From 45f8158ca5586258c28e5169a76e057e201cb4e1 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Tue, 12 Nov 2024 20:57:23 +0100 Subject: [PATCH 139/219] Work in Progress: added particle memory manager for transitions - uses only one, persistant buffer to render all present particle systems - Buffer for particles is shared for one segment (may allow interacitng systems in the future) - updated some of the FX to handle the new transfer, 1D FX still not done - updated parameters for particle impact FX --- wled00/FX.cpp | 159 ++++----- wled00/FX.h | 8 +- wled00/FX_fcn.cpp | 35 +- wled00/FXparticleSystem.cpp | 679 ++++++++++++++++++++++++++++-------- wled00/FXparticleSystem.h | 111 ++++-- wled00/palettes.h | 2 + wled00/wled.cpp | 2 + 7 files changed, 710 insertions(+), 286 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 7871ae9483..e4e2fcc872 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -7817,8 +7817,7 @@ uint16_t mode_particlevortex(void) { #else PartSys->setMotionBlur(130); #endif - uint8_t numSprays = min(PartSys->numSources, (uint32_t)NUMBEROFSOURCES); - for (i = 0; i < numSprays; i++) { + for (i = 0; i < min(PartSys->numSources, (uint32_t)NUMBEROFSOURCES); i++) { PartSys->sources[i].source.x = (PartSys->maxX + 1) >> 1; // center PartSys->sources[i].source.y = (PartSys->maxY + 1) >> 1; // center PartSys->sources[i].maxLife = 900; @@ -7836,10 +7835,13 @@ uint16_t mode_particlevortex(void) { uint8_t spraycount = min(PartSys->numSources, (uint32_t)(1 + (SEGMENT.custom1 >> 5))); // number of sprays to display, 1-8 #ifdef ESP8266 for (i = 1; i < 4; i++) { // need static particles in the center to reduce blinking (would be black every other frame without this hack), just set them there fixed - PartSys->particles[PartSys->numParticles - i].x = (PartSys->maxX + 1) >> 1; // center - PartSys->particles[PartSys->numParticles - i].y = (PartSys->maxY + 1) >> 1; // center - PartSys->particles[PartSys->numParticles - i].sat = 230; - PartSys->particles[PartSys->numParticles - i].ttl = 255; //set alive + int partindex = (int)PartSys->usedParticles - (int)i; + if(partindex >= 0) { + PartSys->particles[partindex].x = (PartSys->maxX + 1) >> 1; // center + PartSys->particles[partindex].y = (PartSys->maxY + 1) >> 1; // center + PartSys->particles[partindex].sat = 230; + PartSys->particles[partindex].ttl = 256; //keep alive + } } #endif if (SEGMENT.check1 != (SEGENV.aux1 & 0x01) || SEGMENT.call == 0) { // state change @@ -7975,9 +7977,10 @@ uint16_t mode_particlefireworks(void) { // check each rocket's state and emit particles according to its state: moving up = emit exhaust, at top = explode; falling down = standby time uint32_t emitparticles, frequency, baseangle, hueincrement; // number of particles to emit for each rocket's state // variables for circular explosions - int32_t speed, currentspeed, speedvariation, counter, percircle; - uint16_t angle; - unsigned angleincrement; + [[maybe_unused]] int32_t speed, currentspeed, speedvariation, percircle; + int32_t counter = 0; + [[maybe_unused]] uint16_t angle; + [[maybe_unused]] unsigned angleincrement; bool circularexplosion = false; // emit particles for each rocket @@ -8008,7 +8011,6 @@ uint16_t mode_particlefireworks(void) { circularexplosion = true; speed = 2 + random16(3) + ((SEGMENT.intensity >> 6)); currentspeed = speed; - counter = 0; angleincrement = 2730 + random16(5461); // minimum 15° + random(30°) angle = random16(); // random start angle baseangle = angle; // save base angle for modulation @@ -8233,7 +8235,7 @@ uint16_t mode_particlepit(void) { return mode_static(); // allocation failed or not 2D PartSys->setKillOutOfBounds(true); PartSys->setGravity(); // enable with default gravity - PartSys->setUsedParticles((PartSys->numParticles * 3) / 2); // use 2/3 of available particles + PartSys->setUsedParticles(170); // use 75% of available particles } else PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS @@ -8375,21 +8377,8 @@ uint16_t mode_particlebox(void) { return mode_static(); // allocation failed or not 2D PartSys->setBounceX(true); PartSys->setBounceY(true); - // set max number of particles and save to aux1 for later - #ifdef ESP8266 - SEGENV.aux1 = min((uint32_t)((PartSys->maxXpixel * PartSys->maxYpixel) >> 1), PartSys->numParticles); - #else - SEGENV.aux1 = min((uint32_t)((PartSys->maxXpixel * PartSys->maxYpixel)), PartSys->numParticles); // max number of particles - #endif - for (i = 0; i < SEGENV.aux1; i++) { - PartSys->particles[i].ttl = 500; // set all particles alive (not all are rendered though) - PartSys->particles[i].perpetual = true; // never die - PartSys->particles[i].hue = i * 3; // color range - PartSys->particles[i].x = map(i, 0, SEGENV.aux1, 1, PartSys->maxX); // distribute along x according to color - PartSys->particles[i].y = random16(PartSys->maxY >> 2); // bottom quarter - PartSys->particles[i].collide = true; // all particles collide - } - SEGENV.aux0 = rand(); // position in perlin noise + SEGENV.aux1 = 0; // number currently used particles + SEGENV.aux0 = random16(); // position in perlin noise } else PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS @@ -8401,7 +8390,18 @@ uint16_t mode_particlebox(void) { PartSys->setWallHardness(min(SEGMENT.custom2, (uint8_t)200)); // wall hardness is 200 or more PartSys->enableParticleCollisions(true, max(2, (int)SEGMENT.custom2)); // enable collisions and set particle collision hardness - PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 10, SEGENV.aux1)); // aux1 holds max number of particles to use + PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 25, 255)); // 10%-100% + if(PartSys->usedParticles > SEGENV.aux1) { // if more particles are needed, emit them + for (i = SEGENV.aux1; i < PartSys->usedParticles; i++) { + PartSys->particles[i].ttl = 260; // full brigthness + PartSys->particles[i].perpetual = true; // never die + PartSys->particles[i].x = random(PartSys->maxX); + PartSys->particles[i].y = random(PartSys->maxY); + PartSys->particles[i].hue = i * 3; // color range + PartSys->particles[i].collide = true; // all particles colllide + } + SEGENV.aux1 = PartSys->usedParticles; + } if (SEGMENT.call % (((255 - SEGMENT.speed) >> 6) + 1) == 0 && SEGMENT.speed > 0) { // how often the force is applied depends on speed setting int32_t xgravity; @@ -8459,9 +8459,6 @@ uint16_t mode_particleperlin(void) { PartSys->setKillOutOfBounds(true); // should never happen, but lets make sure there are no stray particles SEGENV.aux0 = rand(); - for (i = 0; i < PartSys->numParticles; i++) { - PartSys->particles[i].collide = true; // all particles colllide - } } else PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS @@ -8475,17 +8472,17 @@ uint16_t mode_particleperlin(void) { PartSys->setBounceY(true); PartSys->setWallHardness(SEGMENT.custom1); // wall hardness PartSys->enableParticleCollisions(SEGMENT.check3, SEGMENT.custom1); // enable collisions and set particle collision hardness - uint32_t displayparticles = map(SEGMENT.intensity, 0, 255, 10, PartSys->numParticles >> 1); - PartSys->setUsedParticles(displayparticles); + PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 25, 128)); // min is 10%, max is 50% PartSys->setMotionBlur(230); // anable motion blur // apply 'gravity' from a 2D perlin noise map SEGENV.aux0 += 1 + (SEGMENT.speed >> 5); // noise z-position // update position in noise - for (i = 0; i < displayparticles; i++) { + for (i = 0; i < PartSys->usedParticles; i++) { if (PartSys->particles[i].ttl == 0) { // revive dead particles (do not keep them alive forever, they can clump up, need to reseed) PartSys->particles[i].ttl = random16(500) + 200; PartSys->particles[i].x = random(PartSys->maxX); PartSys->particles[i].y = random(PartSys->maxY); + PartSys->particles[i].collide = true; // particle colllides } uint32_t scale = 16 - ((31 - SEGMENT.custom3) >> 1); uint16_t xnoise = PartSys->particles[i].x / scale; // position in perlin noise, scaled by slider @@ -8522,13 +8519,14 @@ uint16_t mode_particleimpact(void) { if (SEGMENT.call == 0) { // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. if (!initParticleSystem2D(PartSys, NUMBEROFSOURCES)) // init, no additional data needed return mode_static(); // allocation failed or not 2D - PartSys->setKillOutOfBounds(false); // explosions out of frame ar allowed, set to true to save particles (TODO: better enable it in ESP8266?) + PartSys->setKillOutOfBounds(true); PartSys->setGravity(); // enable default gravity PartSys->setBounceY(true); // always use ground bounce + PartSys->setWallRoughness(220); // high roughness MaxNumMeteors = min(PartSys->numSources, (uint32_t)NUMBEROFSOURCES); for (i = 0; i < MaxNumMeteors; i++) { - PartSys->sources[i].source.y = 500; - PartSys->sources[i].source.ttl = random16(20 * i); // set initial delay for meteors + // PartSys->sources[i].source.y = 500; + PartSys->sources[i].source.ttl = random16(10 * i); // set initial delay for meteors PartSys->sources[i].source.vy = 10; // at positive speeds, no particles are emitted and if particle dies, it will be relaunched } } @@ -8542,10 +8540,12 @@ uint16_t mode_particleimpact(void) { PartSys->updateSystem(); // update system properties (dimensions and data pointers) PartSys->setWrapX(SEGMENT.check1); PartSys->setBounceX(SEGMENT.check2); - PartSys->setWallHardness(SEGMENT.custom2); - PartSys->enableParticleCollisions(SEGMENT.check3, SEGMENT.custom2); // enable collisions and set particle collision hardness + PartSys->setMotionBlur(SEGMENT.custom3<<3); + uint8_t hardness = map(SEGMENT.custom2, 0, 255, 127, 255); + PartSys->setWallHardness(hardness); + PartSys->enableParticleCollisions(SEGMENT.check3, hardness); // enable collisions and set particle collision hardness MaxNumMeteors = min(PartSys->numSources, (uint32_t)NUMBEROFSOURCES); - uint8_t numMeteors = map(SEGMENT.custom3, 0, 31, 1, MaxNumMeteors); // number of meteors to use for animation + uint8_t numMeteors = MaxNumMeteors; // TODO: clean this up map(SEGMENT.custom3, 0, 31, 1, MaxNumMeteors); // number of meteors to use for animation uint32_t emitparticles; // number of particles to emit for each rocket's state @@ -8565,8 +8565,8 @@ uint16_t mode_particleimpact(void) { #ifdef ESP8266 emitparticles = random16(SEGMENT.intensity >> 3) + 5; // defines the size of the explosion #else - emitparticles = map(SEGMENT.intensity, 0, 255, 10, random16(PartSys->numParticles>>2)); // defines the size of the explosion !!!TODO: check if this works on ESP8266, drop esp8266 def if it does -#endif + emitparticles = map(SEGMENT.intensity, 0, 255, 10, random16(PartSys->usedParticles>>2)); // defines the size of the explosion !!!TODO: check if this works on ESP8266, drop esp8266 def if it does + #endif } for (int e = emitparticles; e > 0; e--) { PartSys->sprayEmit(PartSys->sources[i]); @@ -8577,7 +8577,7 @@ uint16_t mode_particleimpact(void) { for (i = 0; i < numMeteors; i++) { if (PartSys->sources[i].source.ttl) { PartSys->sources[i].source.ttl--; // note: this saves an if statement, but moving down particles age twice - if (PartSys->sources[i].source.vy < 0) { //move down + if (PartSys->sources[i].source.vy < 0) { // move down PartSys->applyGravity(PartSys->sources[i].source); PartSys->particleMoveUpdate(PartSys->sources[i].source, &meteorsettings); @@ -8587,14 +8587,13 @@ uint16_t mode_particleimpact(void) { PartSys->sources[i].source.vx = 0; PartSys->sources[i].source.collide = true; #ifdef ESP8266 - PartSys->sources[i].maxLife = 130; + PartSys->sources[i].maxLife = 180; PartSys->sources[i].minLife = 20; - PartSys->sources[i].source.ttl = random16(255 - (SEGMENT.speed>>1)) + 10; // standby time til next launch (in frames at 42fps, max of 265 is about 6 seconds #else - PartSys->sources[i].maxLife = 160; + PartSys->sources[i].maxLife = 250; PartSys->sources[i].minLife = 50; - PartSys->sources[i].source.ttl = random16((255 - SEGMENT.speed)) + 10; // standby time til next launch (in frames at 42fps, max of 265 is about 6 seconds #endif + PartSys->sources[i].source.ttl = random16((512 - (SEGMENT.speed << 1))) + 40; // standby time til next launch (in frames) PartSys->sources[i].vy = (SEGMENT.custom1 >> 2); // emitting speed y PartSys->sources[i].var = (SEGMENT.custom1 >> 2); // speed variation around vx,vy (+/- var) } @@ -8605,7 +8604,7 @@ uint16_t mode_particleimpact(void) { PartSys->sources[i].source.y = PartSys->maxY + (PS_P_RADIUS << 2); // start 4 pixels above the top PartSys->sources[i].source.x = random(PartSys->maxX); PartSys->sources[i].source.vy = -random16(30) - 30; // meteor downward speed - PartSys->sources[i].source.vx = random(30) - 15; + PartSys->sources[i].source.vx = random(50) - 25; // TODO: make this dependent on position so they do not move out of frame PartSys->sources[i].source.hue = random16(); // random color PartSys->sources[i].source.ttl = 500; // long life, will explode at bottom PartSys->sources[i].source.collide = false; // trail particles will not collide @@ -8620,7 +8619,7 @@ uint16_t mode_particleimpact(void) { return FRAMETIME; } #undef NUMBEROFSOURCES -static const char _data_FX_MODE_PARTICLEIMPACT[] PROGMEM = "PS Impact@Launches,Explosion Size,Explosion Force,Bounce,Meteors,Cylinder,Walls,Collisions;;!;2;pal=0,sx=32,ix=85,c1=70,c2=130,c3=8,o1=0,o2=0,o3=1"; +static const char _data_FX_MODE_PARTICLEIMPACT[] PROGMEM = "PS Impact@Launches,Explosion Size,Explosion Force,Hardness,Blur,Cylinder,Walls,Collisions;;!;2;pal=0,sx=32,ix=85,c1=70,c2=130,c3=0,o1=0,o2=0,o3=1"; /* Particle Attractor, a particle attractor sits in the matrix center, a spray bounces around and seeds particles @@ -8635,7 +8634,7 @@ uint16_t mode_particleattractor(void) { sourcesettings.asByte = 0b00001100; // PS settings for bounceY, bounceY used for source movement (it always bounces whereas particles do not) PSparticle *attractor; // particle pointer to the attractor if (SEGMENT.call == 0) { // initialization - if (!initParticleSystem2D(PartSys, 1, 0, true)) // init using 1 source and advanced particle settings + if (!initParticleSystem2D(PartSys, 1, sizeof(PSparticle), true)) // init using 1 source and advanced particle settings return mode_static(); // allocation failed or not 2D PartSys->sources[0].source.hue = random16(); PartSys->sources[0].source.vx = -7; // will collied with wall and get random bounce direction @@ -8661,22 +8660,17 @@ uint16_t mode_particleattractor(void) { // Particle System settings PartSys->updateSystem(); // update system properties (dimensions and data pointers) + attractor = reinterpret_cast(PartSys->PSdataEnd); PartSys->setColorByAge(SEGMENT.check1); PartSys->setParticleSize(SEGMENT.custom1 >> 1); //set size globally + PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 25, 190)); if (SEGMENT.custom2 > 0) // collisions enabled PartSys->enableParticleCollisions(true, map(SEGMENT.custom2, 1, 255, 120, 255)); // enable collisions and set particle collision hardness else PartSys->enableParticleCollisions(false); - uint16_t lastusedparticle = (PartSys->numParticles * 3) >> 2; // use 3/4 of particles - uint32_t displayparticles = map(SEGMENT.intensity, 0, 255, 10, lastusedparticle); - PartSys->setUsedParticles(displayparticles); - - // set pointers - attractor = &PartSys->particles[lastusedparticle + 1]; - if (SEGMENT.call == 0) { attractor->vx = PartSys->sources[0].source.vy; // set to spray movemement but reverse x and y attractor->vy = PartSys->sources[0].source.vx; @@ -8715,11 +8709,11 @@ uint16_t mode_particleattractor(void) { } } } - for(uint32_t i = 0; i < displayparticles; i++) { + for(uint32_t i = 0; i < PartSys->usedParticles; i++) { PartSys->pointAttractor(i, attractor, strength, false); } #else // no AR - for (uint32_t i = 0; i < displayparticles; i++) { + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { PartSys->pointAttractor(i, attractor, SEGMENT.speed, SEGMENT.check3); } #endif @@ -8836,7 +8830,7 @@ uint16_t mode_particleGEQ(void) { if (!initParticleSystem2D(PartSys, 1)) return mode_static(); // allocation failed or not 2D PartSys->setKillOutOfBounds(true); - PartSys->setUsedParticles((PartSys->numParticles * 3) / 2); // use 2/3 of available particles + PartSys->setUsedParticles(170); // use 2/3 of available particles } else PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS @@ -9081,7 +9075,7 @@ uint16_t mode_particleblobs(void) { return mode_static(); // something went wrong, no data! PartSys->updateSystem(); // update system properties (dimensions and data pointers) - PartSys->setUsedParticles(min(PartSys->numParticles, (uint32_t)map(SEGMENT.intensity, 0, 255, 1, (PartSys->maxXpixel * PartSys->maxYpixel) >> 4))); + PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 25, 128)); // minimum 10%, maximum 50% of available particles (note: PS ensures at least 1) PartSys->enableParticleCollisions(SEGMENT.check2); for (uint32_t i = 0; i < PartSys->usedParticles; i++) { // update particles @@ -9138,6 +9132,7 @@ static const char _data_FX_MODE_PARTICLEBLOBS[] PROGMEM = "PS Blobs@Speed,Blobs, * particles move, then split to form a fractal tree EXPERIMENTAL! * by DedeHai (Damian Schneider) */ + uint16_t mode_particlefractal(void) { ParticleSystem2D *PartSys = NULL; uint32_t i; @@ -9164,7 +9159,7 @@ uint16_t mode_particlefractal(void) { int8_t emitspeed = SEGMENT.speed >> 2; //check particle age, emit 2 particles at the end of the branch - for (i = 0; i < PartSys->numParticles; i++) { + for (i = 0; i < PartSys->usedParticles; i++) { if(PartSys->particles[i].ttl > 0 && PartSys->particles[i].ttl < 260) { //alive and ripe PartSys->particles[i].ttl = 0; uint16_t currentangle = ((uint32_t)PartSys->advPartProps[i].forcecounter) << 7; // abuse forcecounter to track the angle @@ -9356,7 +9351,7 @@ uint16_t mode_particleBouncingBalls(void) { PartSys->sources[0].var = SEGMENT.speed >> 3; PartSys->sources[0].v = (SEGMENT.speed >> 1) - (SEGMENT.speed >> 3); PartSys->enableParticleCollisions(SEGMENT.check1, 254); // enable collisions and set particle collision hardness (do not use full hardness or particles speed up due to pushing, can not be made perfectly balanced) - PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 1, PartSys->numParticles)); + PartSys->setUsedParticles(SEGMENT.intensity); PartSys->setColorByPosition(SEGMENT.check3); if (SEGMENT.check2) { //rolling balls @@ -9706,12 +9701,6 @@ uint16_t mode_particleHourglass(void) { return mode_static(); // allocation failed or is single pixel PartSys->setBounce(true); PartSys->setWallHardness(80); - - for(uint32_t i = 0; i < PartSys->numParticles; i++) { - PartSys->particles[i].collide = true; - PartSys->particles[i].ttl = 500; - PartSys->particles[i].perpetual = true; - } SEGENV.step = 0xFFFF; } else @@ -9723,8 +9712,7 @@ uint16_t mode_particleHourglass(void) { PartSys->updateSystem(); // update system properties (dimensions and data pointers) basehue = PartSys->PSdataEnd; //assign data pointer direction = reinterpret_cast(PartSys->PSdataEnd + 1); //assign data pointer - uint32_t numgrains = map(SEGMENT.intensity, 0, 255, 1, PartSys->maxXpixel + 1); // number of particles to use - PartSys->setUsedParticles(min(numgrains, (uint32_t)PartSys->numParticles));//SEGMENT.custom1); + PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 1, 255)); PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur PartSys->setGravity(map(SEGMENT.custom3, 0, 31, 1, 30)); PartSys->enableParticleCollisions(true, 34); // fixed hardness, 34 is a value that works best in most settings (spent a long time optimizing) SEGMENT.custom1); @@ -9775,6 +9763,8 @@ uint16_t mode_particleHourglass(void) { if(SEGENV.aux1 == 1) { //last countdown call before dropping starts, reset all particles for(uint32_t i = 0; i < PartSys->usedParticles; i++) { + PartSys->particles[i].collide = true; + PartSys->particles[i].ttl = 260; uint32_t targetposition; //calculate target position depending on direction if(PartSys->particles[i].reversegrav) @@ -9886,13 +9876,7 @@ uint16_t mode_particleBalance(void) { return mode_static(); // allocation failed or is single pixel //PartSys->setKillOutOfBounds(true); PartSys->setParticleSize(1); - for(i = 0; i < PartSys->numParticles; i++) { - PartSys->particles[i].x = i * PS_P_RADIUS_1D; - PartSys->particles[i].hue = (i * 1024) / PartSys->usedParticles; // multi gradient distribution - PartSys->particles[i].ttl = 300; - PartSys->particles[i].perpetual = true; - PartSys->particles[i].collide = true; - } + SEGENV.aux0 = 0; // to track particle initialization } else PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS @@ -9909,7 +9893,16 @@ uint16_t mode_particleBalance(void) { if(SEGMENT.custom1 == 0) //collisions disabled, make the walls hard hardness = 200; PartSys->setWallHardness(hardness); - PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 10, PartSys->numParticles)); + PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 10, 255)); + if(PartSys->usedParticles > SEGENV.aux1) { // more particles, reinitialize + for (i = 0; i < PartSys->usedParticles; i++) { + PartSys->particles[i].x = i * PS_P_RADIUS_1D; + PartSys->particles[i].hue = (i * 1024) / PartSys->usedParticles; // multi gradient distribution + PartSys->particles[i].ttl = 300; + PartSys->particles[i].collide = true; + } + } + SEGENV.aux1 = PartSys->usedParticles; if (SEGMENT.call % (((255 - SEGMENT.speed) >> 6) + 1) == 0) { // how often the force is applied depends on speed setting int32_t xgravity; @@ -9952,11 +9945,6 @@ uint16_t mode_particleChase(void) { if (SEGMENT.call == 0) { // initialization if (!initParticleSystem1D(PartSys, 1, 255, 3, true)) // init return mode_static(); // allocation failed or is single pixel - for(i = 0; i < PartSys->numParticles; i++) { - PartSys->advPartProps[i].sat = 255; - PartSys->particles[i].ttl = 300; - PartSys->particles[i].perpetual = true; - } SEGENV.aux0 = 0xFFFF; // invalidate *PartSys->PSdataEnd = 1; *(PartSys->PSdataEnd + 1) = 1; @@ -9975,10 +9963,13 @@ uint16_t mode_particleChase(void) { //PartSys->setBounce(SEGMENT.check2); uint32_t settingssum = SEGMENT.speed + SEGMENT.intensity + SEGMENT.custom1 + SEGMENT.custom2 + SEGMENT.check1 + SEGMENT.check2 + SEGMENT.check3; if(SEGENV.aux0 != settingssum) { //settings changed changed, update - PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 1, min(PartSys->maxX / (32 + (SEGMENT.custom1 >> 1)), int32_t(PartSys->numParticles)))); //depends on intensity and particle size (custom1) + PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 1, min(PartSys->maxX / (32 + (SEGMENT.custom1 >> 1)), int32_t(PartSys->usedParticles)))); //depends on intensity and particle size (custom1) !!! TODO: this needs an update to relative number SEGENV.step = (PartSys->maxX + (PS_P_RADIUS_1D << 4)) / PartSys->usedParticles; //spacing between particles // uint32_t remainder = PartSys->maxX - ((PartSys->usedParticles) * SEGENV.step); // unused spacing, distribute this for(i = 0; i < PartSys->usedParticles; i++) { + PartSys->advPartProps[i].sat = 255; + PartSys->particles[i].ttl = 300; + PartSys->particles[i].perpetual = true; //PartSys->particles[i].x = (i - 1) * SEGENV.step + (((i + 1) * remainder) / PartSys->usedParticles); // distribute evenly PartSys->particles[i].x = (i - 1) * SEGENV.step; // distribute evenly PartSys->particles[i].vx = SEGMENT.speed >> 1; diff --git a/wled00/FX.h b/wled00/FX.h index 53f01253c1..1255c10b34 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -445,20 +445,22 @@ typedef struct Segment { #ifndef WLED_DISABLE_MODE_BLEND tmpsegd_t _segT; // previous segment environment uint8_t _modeT; // previous mode/effect + uint8_t _palette; // previous palette #else uint32_t _colorT[NUM_COLORS]; #endif uint8_t _briT; // temporary brightness uint8_t _cctT; // temporary CCT CRGBPalette16 _palT; // temporary palette - uint8_t _prevPaletteBlends; // number of previous palette blends (there are max 255 blends possible) unsigned long _start; // must accommodate millis() uint16_t _dur; + uint8_t _prevPaletteBlends; // number of previous palette blends (there are max 255 blends possible) + // -> here is one byte of padding Transition(uint16_t dur=750) : _palT(CRGBPalette16(CRGB::Black)) - , _prevPaletteBlends(0) , _start(millis()) , _dur(dur) + , _prevPaletteBlends(0) {} } *_t; @@ -587,7 +589,7 @@ typedef struct Segment { uint8_t currentMode() const; // currently active effect/mode (while in transition) [[gnu::hot]] uint32_t currentColor(uint8_t slot) const; // currently active segment color (blended while in transition) CRGBPalette16 &loadPalette(CRGBPalette16 &tgt, uint8_t pal); - void setCurrentPalette(); + void setCurrentPalette(bool loadOldPalette = false); // 1D strip [[gnu::hot]] uint16_t virtualLength() const; diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index e7a0a8729c..a5e3690496 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -11,6 +11,7 @@ */ #include "wled.h" #include "FX.h" +#include "FXparticleSystem.h" // TODO: better define the required function (mem service) in FX.h? #include "palettes.h" /* @@ -166,13 +167,15 @@ bool IRAM_ATTR_YN Segment::allocateData(size_t len) { void IRAM_ATTR_YN Segment::deallocateData() { if (!data) { _dataLen = 0; return; } - //DEBUG_PRINTF_P(PSTR("--- Released data (%p): %d/%d -> %p\n"), this, _dataLen, Segment::getUsedSegmentData(), data); + Serial.printf(PSTR("--- Released data (%p): %d/%d -> %p\n"), this, _dataLen, Segment::getUsedSegmentData(), data); if ((Segment::getUsedSegmentData() > 0) && (_dataLen > 0)) { // check that we don't have a dangling / inconsistent data pointer + Serial.println("releasing segment data"); free(data); } else { DEBUG_PRINTF_P(PSTR("---- Released data (%p): inconsistent UsedSegmentData (%d/%d), cowardly refusing to free nothing.\n"), this, _dataLen, Segment::getUsedSegmentData()); } data = nullptr; + Serial.println("reducing used data by" + String(_dataLen)); Segment::addUsedSegmentData(_dataLen <= Segment::getUsedSegmentData() ? -_dataLen : -Segment::getUsedSegmentData()); _dataLen = 0; } @@ -277,6 +280,7 @@ void Segment::startTransition(uint16_t dur) { //DEBUG_PRINTF_P(PSTR("-- Started transition: %p (%p)\n"), this, _t); loadPalette(_t->_palT, palette); + _t->_palette = palette; _t->_briT = on ? opacity : 0; _t->_cctT = cct; #ifndef WLED_DISABLE_MODE_BLEND @@ -424,16 +428,23 @@ uint32_t IRAM_ATTR_YN Segment::currentColor(uint8_t slot) const { #endif } -void Segment::setCurrentPalette() { - loadPalette(_currentPalette, palette); - unsigned prog = progress(); - if (strip.paletteFade && prog < 0xFFFFU) { - // blend palettes - // there are about 255 blend passes of 48 "blends" to completely blend two palettes (in _dur time) - // minimum blend time is 100ms maximum is 65535ms - unsigned noOfBlends = ((255U * prog) / 0xFFFFU) - _t->_prevPaletteBlends; - for (unsigned i = 0; i < noOfBlends; i++, _t->_prevPaletteBlends++) nblendPaletteTowardPalette(_t->_palT, _currentPalette, 48); - _currentPalette = _t->_palT; // copy transitioning/temporary palette +void Segment::setCurrentPalette(bool loadOldPalette) { + if (isInTransition()) { + if(loadOldPalette) { // load palette of old effect + loadPalette(_currentPalette, _t->_palette); + return; + } + if(strip.paletteFade) { + // blend palettes + // there are about 255 blend passes of 48 "blends" to completely blend two palettes (in _dur time) + // minimum blend time is 100ms maximum is 65535ms + loadPalette(_currentPalette, palette); + unsigned noOfBlends = ((255U * progress()) / 0xFFFFU) - _t->_prevPaletteBlends; + for (unsigned i = 0; i < noOfBlends; i++, _t->_prevPaletteBlends++) nblendPaletteTowardPalette(_t->_palT, _currentPalette, 48); + _currentPalette = _t->_palT; // copy transitioning/temporary palette + } + } else { + loadPalette(_currentPalette, palette); } } @@ -698,7 +709,6 @@ void IRAM_ATTR_YN Segment::setPixelColor(int i, uint32_t col) int vStrip = i>>16; // hack to allow running on virtual strips (2D segment columns/rows) #endif i &= 0xFFFF; - if (i >= virtualLength() || i<0) return; // if pixel would fall out of segment just exit #ifndef WLED_DISABLE_2D @@ -1374,6 +1384,7 @@ void WS2812FX::service() { seg.call++; if (seg.isInTransition() && delay > FRAMETIME) delay = FRAMETIME; // force faster updates during transition BusManager::setSegmentCCT(oldCCT); // restore old CCT for ABL adjustments + servicePSmem(_segment_index); // handle segment's particle system memory } seg.next_time = nowUp + delay; diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index 8dfe280e61..88a7eb68fc 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -20,10 +20,27 @@ #ifndef WLED_DISABLE_PARTICLESYSTEM2D -ParticleSystem2D::ParticleSystem2D(uint16_t width, uint16_t height, uint16_t numberofparticles, uint16_t numberofsources, bool isadvanced, bool sizecontrol) { - numSources = numberofsources; - numParticles = numberofparticles; // set number of particles in the array - usedParticles = numberofparticles; // use all particles by default +// local shared functions (used both in 1D and 2D system) +static int32_t calcForce_dv(int8_t force, uint8_t *counter); +static int32_t limitSpeed(int32_t speed); +static bool checkBoundsAndWrap(int32_t &position, const int32_t max, const int32_t particleradius, bool wrap); // returns false if out of bounds by more than particleradius +static void fast_color_add(CRGB &c1, const CRGB &c2, uint32_t scale = 255); // fast and accurate color adding with scaling (scales c2 before adding) +static void fast_color_scale(CRGB &c, const uint32_t scale); // fast scaling function using 32bit variable and pointer. note: keep 'scale' within 0-255 +//static CRGB *allocateCRGBbuffer(uint32_t length); + +// global variables for memory management +std::vector partMemList; // list of particle memory pointers +CRGB *framebuffer = nullptr; // local frame buffer for rendering +CRGB *renderbuffer = nullptr; // local particle render buffer for advanced particles +uint16_t frameBufferSize = 0; // size in pixels, used to check if framebuffer is large enough for current segment +uint16_t renderBufferSize = 0; // size in pixels, if allcoated by a 1D system it needs to be updated for 2D +uint8_t renderSolo = 0; // is set to >0 if this is the only particle system using the so it can use the buffer continuously (faster blurring) + +ParticleSystem2D::ParticleSystem2D(uint32_t width, uint32_t height, uint32_t numberofparticles, uint32_t numberofsources, bool isadvanced, bool sizecontrol) { + effectID = SEGMENT.mode; + numSources = numberofsources; // number of sources allocated in init + numParticles = numberofparticles; // number of particles allocated in init + usedpercentage = 255; // use all particles by default, usedParticles is updated in updatePSpointers() advPartProps = NULL; //make sure we start out with null pointers (just in case memory was not cleared) advPartSize = NULL; updatePSpointers(isadvanced, sizecontrol); // set the particle and sources pointer (call this before accessing sprays or particles) @@ -35,13 +52,15 @@ ParticleSystem2D::ParticleSystem2D(uint16_t width, uint16_t height, uint16_t num motionBlur = 0; //no fading by default emitIndex = 0; + //initialize some default non-zero values most FX use for (uint32_t i = 0; i < numSources; i++) { sources[i].source.sat = 255; //set saturation to max by default sources[i].source.ttl = 1; //set source alive } for (uint32_t i = 0; i < numParticles; i++) { - particles[i].sat = 255; // full saturation + //particles[i].ttl = 0; // set all particles to dead TODO: do this in manager! + particles[i].sat = 255; // full saturation } } @@ -81,8 +100,18 @@ void ParticleSystem2D::updateFire(uint32_t intensity, bool renderonly) { ParticleSys_render(true, intensity); } -void ParticleSystem2D::setUsedParticles(uint32_t num) { - usedParticles = min(num, numParticles); //limit to max particles +// set percentage of used particles as uint8_t i.e 127 means 50% for example +void ParticleSystem2D::setUsedParticles(uint8_t percentage) { + usedpercentage = percentage; // note usedParticles is updated in memory manager + updateUsedParticles(numParticles, availableParticles, usedpercentage, usedParticles); + PSPRINT(" allocated particles: "); + PSPRINT(numParticles); + PSPRINT(" available particles: "); + PSPRINT(availableParticles); + PSPRINT(" ,used percentage: "); + PSPRINT(usedpercentage); + PSPRINT(" ,used particles: "); + PSPRINTLN(usedParticles); } void ParticleSystem2D::setWallHardness(uint8_t hardness) { @@ -531,47 +560,28 @@ void ParticleSystem2D::pointAttractor(uint16_t particleindex, PSparticle *attrac // fireintensity and firemode are optional arguments (fireintensity is only used in firemode) void ParticleSystem2D::ParticleSys_render(bool firemode, uint32_t fireintensity) { CRGB baseRGB; - bool useLocalBuffer = true; //use local rendering buffer, gives huge speed boost (at least 30% more FPS) - CRGB *framebuffer = NULL; //local frame buffer, Note: 1D array access is faster, especially when accessing in order - CRGB *renderbuffer = NULL; //local particle render buffer for advanced particles - uint32_t i; uint32_t brightness; // particle brightness, fades if dying - /* - //memory fragmentation check: - Serial.print("heap: "); - Serial.print(heap_caps_get_free_size(MALLOC_CAP_8BIT)); - Serial.print(" block: "); - Serial.println(heap_caps_get_largest_free_block(MALLOC_CAP_8BIT)); - */ - - if (useLocalBuffer) { - // allocate empty memory for the local renderbuffer - framebuffer = allocate1Dbuffer((maxXpixel + 1) * (maxYpixel + 1)); - if (framebuffer == NULL) { - //Serial.println("Frame buffer alloc failed"); - useLocalBuffer = false; //render to segment pixels directly if not enough memory - } - else { - if (motionBlur > 0) { // using SEGMENT.fadeToBlackBy is much slower, this approximately doubles the speed of fade calculation - uint32_t yflipped; - for (uint32_t y = 0; y <= maxYpixel; y++) { - yflipped = maxYpixel - y; - int index = y * (maxXpixel + 1); // current row index for 1D buffer - for (uint32_t x = 0; x <= maxXpixel; x++) { + if (framebuffer) { + if (motionBlur > 0) { // using SEGMENT.fadeToBlackBy is much slower, this approximately doubles the speed of fade calculation + uint32_t yflipped; + for (uint32_t y = 0; y <= maxYpixel; y++) { + yflipped = maxYpixel - y; + int index = y * (maxXpixel + 1); // current row index for 1D buffer + for (uint32_t x = 0; x <= maxXpixel; x++) { + if(renderSolo < 2) // there are/were other systems using the buffer, read data from segment framebuffer[index] = SEGMENT.getPixelColorXY(x, yflipped); //copy to local buffer - fast_color_scale(framebuffer[index], motionBlur); - index++; - } + fast_color_scale(framebuffer[index], motionBlur); + index++; } } } + else if(renderSolo) { // no blurring and no other systems, clear the buffer (manager skips clearing to enable faster blur) + memset(framebuffer, 0, frameBufferSize * sizeof(CRGB)); // clear the buffer + PSPRINT(" PS buffer cleared "); + } } - if (advPartProps) { - renderbuffer = allocate1Dbuffer(10*10); //buffer to render individual particles to if size > 0. note: null checking is done when accessing it - } - - if (!useLocalBuffer) { //disabled or allocation above failed + else { // no local buffer available if (motionBlur > 0) SEGMENT.fadeToBlackBy(255 - motionBlur); else @@ -580,7 +590,7 @@ void ParticleSystem2D::ParticleSys_render(bool firemode, uint32_t fireintensity) bool wrapX = particlesettings.wrapX; // use local variables for faster access bool wrapY = particlesettings.wrapY; // go over particles and render them to the buffer - for (i = 0; i < usedParticles; i++) { + for (uint32_t i = 0; i < usedParticles; i++) { if (particles[i].outofbounds || particles[i].ttl == 0) continue; @@ -592,14 +602,14 @@ void ParticleSystem2D::ParticleSys_render(bool firemode, uint32_t fireintensity) } else { brightness = min(particles[i].ttl, (uint16_t)255); - baseRGB = ColorFromPalette(SEGPALETTE, particles[i].hue, 255); + baseRGB = ColorFromPalette(SEGPALETTE, particles[i].hue, 255); // TODO: use loadPalette(CRGBPalette16 &targetPalette, SEGMENT.palette), .palette should be updated immediately at palette change, only use local palette during FX transitions, not during normal transitions. -> why not always? if (particles[i].sat < 255) { CHSV baseHSV = rgb2hsv_approximate(baseRGB); //convert to HSV baseHSV.s = particles[i].sat; //set the saturation baseRGB = (CRGB)baseHSV; // convert back to RGB } } - renderParticle(framebuffer, i, brightness, baseRGB, renderbuffer, wrapX, wrapY); + renderParticle(i, brightness, baseRGB, wrapX, wrapY); } if (particlesize > 0) { @@ -611,7 +621,7 @@ void ParticleSystem2D::ParticleSys_render(bool firemode, uint32_t fireintensity) if (i == 2) // for the last two passes, use higher amount of blur (results in a nicer brightness gradient with soft edges) bitshift = 1; - if (useLocalBuffer) + if (framebuffer) blur2D(framebuffer, maxXpixel + 1, maxYpixel + 1, bluramount << bitshift, bluramount << bitshift); else { SEGMENT.blur(bluramount << bitshift, true); @@ -620,23 +630,13 @@ void ParticleSystem2D::ParticleSys_render(bool firemode, uint32_t fireintensity) } } - if (useLocalBuffer) { // transfer local buffer back to segment - int32_t yflipped; - for (uint32_t y = 0; y <= maxYpixel; y++) { - yflipped = maxYpixel - y; - int index = y * (maxXpixel + 1); // current row index for 1D buffer - for (uint32_t x = 0; x <= maxXpixel; x++) { - SEGMENT.setPixelColorXY((int)x, (int)yflipped, framebuffer[index++]); - } - } - free(framebuffer); - } - if (renderbuffer) - free(renderbuffer); + // transfer local buffer back to segment (if available) + transferBuffer((maxXpixel + 1), (maxYpixel + 1), effectID); + } // calculate pixel positions and brightness distribution and render the particle to local buffer or global buffer -void ParticleSystem2D::renderParticle(CRGB *framebuffer, const uint32_t particleindex, const uint32_t brightness, const CRGB& color, CRGB *renderbuffer, const bool wrapX, const bool wrapY) { +void ParticleSystem2D::renderParticle(const uint32_t particleindex, const uint32_t brightness, const CRGB& color, const bool wrapX, const bool wrapY) { int32_t pxlbrightness[4]; // brightness values for the four pixels representing a particle int32_t pixco[4][2]; // physical pixel coordinates of the four pixels a particle is rendered to. x,y pairs bool pixelvalid[4] = {true, true, true, true}; // is set to false if pixel is out of bounds @@ -786,33 +786,33 @@ void ParticleSystem2D::renderParticle(CRGB *framebuffer, const uint32_t particle { if (pixco[d][0] < 0 || pixco[d][0] > maxXpixel) { - //Serial.print("<"); + //PSPRINT("<"); if (pxlbrightness[d] >= 0) { - Serial.print("uncought out of bounds: x:"); - Serial.print(pixco[d][0]); - Serial.print(" y:"); - Serial.print(pixco[d][1]); - Serial.print("particle x="); - Serial.print(particles[particleindex].x); - Serial.print(" y="); - Serial.println(particles[particleindex].y); + PSPRINT("uncought out of bounds: x:"); + PSPRINT(pixco[d][0]); + PSPRINT(" y:"); + PSPRINT(pixco[d][1]); + PSPRINT("particle x="); + PSPRINT(particles[particleindex].x); + PSPRINT(" y="); + PSPRINTLN(particles[particleindex].y); pxlbrightness[d] = -1; // do not render } } if (pixco[d][1] < 0 || pixco[d][1] > maxYpixel) { - //Serial.print("^"); + //PSPRINT("^"); if (pxlbrightness[d] >= 0) { - Serial.print("uncought out of bounds: y:"); - Serial.print(pixco[d][0]); - Serial.print(" y:"); - Serial.print(pixco[d][1]); - Serial.print("particle x="); - Serial.print(particles[particleindex].x); - Serial.print(" y="); - Serial.println(particles[particleindex].y); + PSPRINT("uncought out of bounds: y:"); + PSPRINT(pixco[d][0]); + PSPRINT(" y:"); + PSPRINT(pixco[d][1]); + PSPRINT("particle x="); + PSPRINT(particles[particleindex].x); + PSPRINT(" y="); + PSPRINTLN(particles[particleindex].y); pxlbrightness[d] = -1; // do not render } } @@ -958,25 +958,37 @@ void ParticleSystem2D::collideParticles(PSparticle *particle1, PSparticle *parti // update size and pointers (memory location and size can change dynamically) // note: do not access the PS class in FX befor running this function (or it messes up SEGENV.data) void ParticleSystem2D::updateSystem(void) { - // update matrix size - uint32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; - uint32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); + + PSPRINTLN("updateSystem2D"); + uint32_t cols = SEGMENT.virtualWidth(); // update matrix size + uint32_t rows = SEGMENT.virtualHeight(); setMatrixSize(cols, rows); - updatePSpointers(advPartProps != NULL, advPartSize != NULL); + updatePSpointers(advPartProps != NULL, advPartSize != NULL); // update pointers to PS data, also updates availableParticles + setUsedParticles(usedpercentage); // update used particles based on percentage TODO: this does not need to be called for each frame, it only changes during transitions. can optimize? + if (partMemList.size() == 1 && !SEGMENT.isInTransition()) // if number of vector elements is one, this is the only system !!!TODO: only set to multi if it is an FX transition. -> why? whats bad about it? + { + PSPRINTLN("rendersolo"); + if(renderSolo < 2) renderSolo++; // increment: there is one transition frame when enabling render solo where local buffer is still blank and cant be used for blurring + } + else + { + PSPRINTLN("rendermulti"); + renderSolo = 0; + } } // 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 ParticleSystem2D::updatePSpointers(bool isadvanced, bool sizecontrol) { +PSPRINTLN("updatePSpointers"); // DEBUG_PRINT(F("*** PS pointers ***")); // DEBUG_PRINTF_P(PSTR("this PS %p "), this); // Note on memory alignment: // a pointer MUST be 4 byte aligned. sizeof() in a struct/class is always aligned to the largest element. if it contains a 32bit, it will be padded to 4 bytes, 16bit is padded to 2byte alignment. // The PS is aligned to 4 bytes, a PSparticle is aligned to 2 and a struct containing only byte sized variables is not aligned at all and may need to be padded when dividing the memoryblock. // by making sure that the number of sources and particles is a multiple of 4, padding can be skipped here as alignent is ensured, independent of struct sizes. - particles = reinterpret_cast(this + 1); // pointer to particle array at data+sizeof(ParticleSystem2D) - sources = reinterpret_cast(particles + numParticles); // pointer to source(s) + sources = reinterpret_cast(this + 1); // pointer to source(s) at data+sizeof(ParticleSystem2D) if (isadvanced) { advPartProps = reinterpret_cast(sources + numSources); PSdataEnd = reinterpret_cast(advPartProps + numParticles); @@ -988,6 +1000,7 @@ void ParticleSystem2D::updatePSpointers(bool isadvanced, bool sizecontrol) { else { PSdataEnd = reinterpret_cast(sources + numSources); // pointer to first available byte after the PS for FX additional data } + particles = reinterpret_cast(getUpdatedParticlePointer(0, sizeof(PSparticle), availableParticles, usedParticles, usedpercentage, effectID)); // get memory, leave buffer size as is (request 0) /* DEBUG_PRINTF_P(PSTR(" particles %p "), particles); DEBUG_PRINTF_P(PSTR(" sources %p "), sources); @@ -1051,41 +1064,37 @@ void blur2D(CRGB *colorbuffer, uint32_t xsize, uint32_t ysize, uint32_t xblur, u //non class functions to use for initialization -uint32_t calculateNumberOfParticles2D(bool isadvanced, bool sizecontrol) { - uint32_t cols = SEGMENT.virtualWidth(); - uint32_t rows = SEGMENT.virtualHeight(); +uint32_t calculateNumberOfParticles2D(uint32_t pixels, bool isadvanced, bool sizecontrol) { #ifdef ESP8266 - uint32_t numberofParticles = (cols * rows * 3) / 4; // 0.75 particle per pixel + uint32_t numberofParticles = 1 + (pixels * 3) / 4; // 0.75 particle per pixel, ensure a minimum of 1 uint32_t particlelimit = ESP8266_MAXPARTICLES; // maximum number of paticles allowed (based on one segment of 16x16 and 4k effect ram) #elif ARDUINO_ARCH_ESP32S2 - uint32_t numberofParticles = (cols * rows); // 1 particle per pixel + uint32_t numberofParticles = (pixels); // 1 particle per pixel uint32_t particlelimit = ESP32S2_MAXPARTICLES; // maximum number of paticles allowed (based on one segment of 32x32 and 24k effect ram) #else - uint32_t numberofParticles = (cols * rows); // 1 particle per pixel (for example 512 particles on 32x16) + uint32_t numberofParticles = pixels; // 1 particle per pixel (for example 512 particles on 32x16) uint32_t particlelimit = ESP32_MAXPARTICLES; // maximum number of paticles allowed (based on two segments of 32x32 and 40k effect ram) #endif numberofParticles = max((uint32_t)4, min(numberofParticles, particlelimit)); if (isadvanced) // advanced property array needs ram, reduce number of particles to use the same amount numberofParticles = (numberofParticles * sizeof(PSparticle)) / (sizeof(PSparticle) + sizeof(PSadvancedParticle)); - if (sizecontrol) // advanced property array needs ram, reduce number of particles to use the same amount - numberofParticles /= 8; // if size control is used, much fewer particles are needed + if (sizecontrol) // advanced property array needs ram, reduce number of particles + numberofParticles /= 8; // if advanced size control is used, much fewer particles are needed note: if changing this number, adjust FX using this accordingly //make sure it is a multiple of 4 for proper memory alignment (easier than using padding bytes) numberofParticles = ((numberofParticles+3) >> 2) << 2; return numberofParticles; } -uint32_t calculateNumberOfSources2D(uint8_t requestedsources) { - uint32_t cols = SEGMENT.virtualWidth(); - uint32_t rows = SEGMENT.virtualHeight(); +uint32_t calculateNumberOfSources2D(uint32_t pixels, uint32_t requestedsources) { #ifdef ESP8266 - int numberofSources = min((cols * rows) / 8, (uint32_t)requestedsources); + int numberofSources = min((pixels) / 8, (uint32_t)requestedsources); numberofSources = max(1, min(numberofSources, ESP8266_MAXSOURCES)); // limit to 1 - 16 #elif ARDUINO_ARCH_ESP32S2 - int numberofSources = min((cols * rows) / 6, (uint32_t)requestedsources); + int numberofSources = min((cpixels) / 6, (uint32_t)requestedsources); numberofSources = max(1, min(numberofSources, ESP32S2_MAXSOURCES)); // limit to 1 - 48 #else - int numberofSources = min((cols * rows) / 4, (uint32_t)requestedsources); + int numberofSources = min((pixels) / 4, (uint32_t)requestedsources); numberofSources = max(1, min(numberofSources, ESP32_MAXSOURCES)); // limit to 1 - 64 #endif // make sure it is a multiple of 4 for proper memory alignment @@ -1093,11 +1102,16 @@ uint32_t calculateNumberOfSources2D(uint8_t requestedsources) { return numberofSources; } -//allocate memory for particle system class, particles, sprays plus additional memory requested by FX -bool allocateParticleSystemMemory2D(uint16_t numparticles, uint16_t numsources, bool isadvanced, bool sizecontrol, uint16_t additionalbytes) { +//allocate memory for particle system class, particles, sprays plus additional memory requested by FX //TODO: add percentofparticles like in 1D to reduce memory footprint of some FX? +bool allocateParticleSystemMemory2D(uint32_t numparticles, uint32_t numsources, bool isadvanced, bool sizecontrol, uint32_t additionalbytes) { + PSPRINTLN("PS 2D alloc"); uint32_t requiredmemory = sizeof(ParticleSystem2D); + uint32_t availableparticles; // dummy variable + uint32_t usedparticles = 0; // dummy variable + if((getUpdatedParticlePointer(numparticles, sizeof(PSparticle), availableparticles, usedparticles, 0, 0)) == nullptr) // allocate memory for particles + return false; // not enough memory, function ensures a minimum of numparticles are available + // functions above make sure these are a multiple of 4 bytes (to avoid alignment issues) - requiredmemory += sizeof(PSparticle) * numparticles; if (isadvanced) requiredmemory += sizeof(PSadvancedParticle) * numparticles; if (sizecontrol) @@ -1108,18 +1122,40 @@ bool allocateParticleSystemMemory2D(uint16_t numparticles, uint16_t numsources, } // initialize Particle System, allocate additional bytes if needed (pointer to those bytes can be read from particle system class: PSdataEnd) -bool initParticleSystem2D(ParticleSystem2D *&PartSys, uint8_t requestedsources, uint16_t additionalbytes, bool advanced, bool sizecontrol) { +bool initParticleSystem2D(ParticleSystem2D *&PartSys, uint32_t requestedsources, uint32_t additionalbytes, bool advanced, bool sizecontrol) { + PSPRINTLN("PS 2D init"); if(!strip.isMatrix) return false; // only for 2D - uint32_t numparticles = calculateNumberOfParticles2D(advanced, sizecontrol); - uint32_t numsources = calculateNumberOfSources2D(requestedsources); + uint32_t cols = SEGMENT.virtualWidth(); + uint32_t rows = SEGMENT.virtualHeight(); + uint32_t pixels = cols * rows; + uint32_t numparticles = calculateNumberOfParticles2D(pixels, advanced, sizecontrol); + PSPRINTLN("request numparticles:" + String(numparticles)); + uint32_t numsources = calculateNumberOfSources2D(pixels, requestedsources); + // allocate rendering buffer (if this fails, it will render to segment buffer directly) + updateRenderingBuffer(framebuffer, pixels, true); + if(advanced) + updateRenderingBuffer(renderbuffer, 100, false); + if (!allocateParticleSystemMemory2D(numparticles, numsources, advanced, sizecontrol, additionalbytes)) { DEBUG_PRINT(F("PS init failed: memory depleted")); return false; } - uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; - uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); + PartSys = new (SEGENV.data) ParticleSystem2D(cols, rows, numparticles, numsources, advanced, sizecontrol); // particle system constructor + + PSPRINTLN("******init done, pointers:"); + #ifdef WLED_DEBUG_PS + PSPRINT("framebfr size:"); + PSPRINT(frameBufferSize); + PSPRINT(" @ addr: 0x"); + Serial.println((uintptr_t)framebuffer, HEX); + + PSPRINT("renderbfr size:"); + PSPRINT(renderBufferSize); + PSPRINT(" @ addr: 0x"); + Serial.println((uintptr_t)renderbuffer, HEX); + #endif return true; } @@ -1131,10 +1167,11 @@ bool initParticleSystem2D(ParticleSystem2D *&PartSys, uint8_t requestedsources, //////////////////////// #ifndef WLED_DISABLE_PARTICLESYSTEM1D -ParticleSystem1D::ParticleSystem1D(uint16_t length, uint16_t numberofparticles, uint16_t numberofsources, bool isadvanced) { +ParticleSystem1D::ParticleSystem1D(uint32_t length, uint32_t numberofparticles, uint32_t numberofsources, bool isadvanced) { numSources = numberofsources; - numParticles = numberofparticles; // set number of particles in the array - usedParticles = numberofparticles; // use all particles by default + numParticles = numberofparticles; // number of particles allocated in init + usedpercentage = 255; // use all particles by default + setUsedParticles(usedpercentage); // use all particles by default advPartProps = NULL; //make sure we start out with null pointers (just in case memory was not cleared) //advPartSize = NULL; updatePSpointers(isadvanced); // set the particle and sources pointer (call this before accessing sprays or particles) @@ -1144,6 +1181,7 @@ ParticleSystem1D::ParticleSystem1D(uint16_t length, uint16_t numberofparticles, setParticleSize(0); // minimum size by default motionBlur = 0; //no fading by default emitIndex = 0; + effectID = SEGMENT.mode; // initialize some default non-zero values most FX use for (uint32_t i = 0; i < numSources; i++) { @@ -1186,8 +1224,16 @@ void ParticleSystem1D::update(void) { } } -void ParticleSystem1D::setUsedParticles(uint32_t num) { - usedParticles = min(num, numParticles); //limit to max particles +// set percentage of used particles as uint8_t i.e 127 means 50% for example +void ParticleSystem1D::setUsedParticles(uint8_t percentage) { + usedpercentage = percentage; + PSPRINT(" available particles: "); + PSPRINT(availableParticles); + PSPRINT(" ,used percentage: "); + PSPRINT(usedpercentage); + PSPRINT(" ,used particles: "); + PSPRINTLN(usedParticles); + updateUsedParticles(numParticles, availableParticles, usedpercentage, usedParticles); } void ParticleSystem1D::setWallHardness(uint8_t hardness) { @@ -1402,30 +1448,19 @@ void ParticleSystem1D::applyFriction(int32_t coefficient) { // warning: do not render out of bounds particles or system will crash! rendering does not check if particle is out of bounds void ParticleSystem1D::ParticleSys_render() { CRGB baseRGB; - bool useLocalBuffer = true; //use local rendering buffer, gives huge speed boost (at least 30% more FPS) - CRGB *framebuffer = NULL; //local frame buffer - CRGB *renderbuffer = NULL; //local particle render buffer for advanced particles uint32_t brightness; // particle brightness, fades if dying - if (useLocalBuffer) { - framebuffer = allocate1Dbuffer(maxXpixel + 1); // allocate memory for the local renderbuffer - if (framebuffer == NULL) { - DEBUG_PRINT(F("Frame buffer alloc failed")); - useLocalBuffer = false; // render to segment pixels directly if not enough memory - } - else { - if (advPartProps) - renderbuffer = allocate1Dbuffer(10); // buffer to render individual particles to if size > 0. note: null checking is done when accessing it - if (motionBlur > 0) { // using SEGMENT.fadeToBlackBy is much slower, this approximately doubles the speed of fade calculation - for (uint32_t x = 0; x <= maxXpixel; x++) { - framebuffer[x] = SEGMENT.getPixelColor(x); // copy to local buffer - fast_color_scale(framebuffer[x], motionBlur); - } + if (framebuffer) { + if (motionBlur > 0) { // using SEGMENT.fadeToBlackBy is much slower, this approximately doubles the speed of fade calculation + for (uint32_t x = 0; x <= maxXpixel; x++) { //!!! TODO: add renderSolo handling here + framebuffer[x] = SEGMENT.getPixelColor(x); // copy to local buffer + fast_color_scale(framebuffer[x], motionBlur); } } + else if(renderSolo) // no blurring and no other systems, clear the buffer (manager skips clearing if there is only one PS) + memset(framebuffer, 0, frameBufferSize * sizeof(CRGB)); // clear the buffer } - - if (!useLocalBuffer) { // disabled or allocation above failed + else { // no local buffer available if (motionBlur > 0) SEGMENT.fadeToBlackBy(255 - motionBlur); else @@ -1448,21 +1483,15 @@ void ParticleSystem1D::ParticleSys_render() { baseRGB = (CRGB)baseHSV; // convert back to RGB } } - renderParticle(framebuffer, i, brightness, baseRGB, renderbuffer, wrap); + renderParticle(i, brightness, baseRGB, wrap); } + // transfer local buffer back to segment (if available) + transferBuffer(maxXpixel + 1, 0, effectID); - if (useLocalBuffer) { // transfer local buffer back to segment - for (unsigned x = 0; x <= maxXpixel; x++) { - SEGMENT.setPixelColor(x, framebuffer[x]); - } - free(framebuffer); - } - if (renderbuffer) - free(renderbuffer); } // calculate pixel positions and brightness distribution and render the particle to local buffer or global buffer -void ParticleSystem1D::renderParticle(CRGB *framebuffer, const uint32_t particleindex, const uint32_t brightness, const CRGB &color, CRGB *renderbuffer, const bool wrap) { +void ParticleSystem1D::renderParticle(const uint32_t particleindex, const uint32_t brightness, const CRGB &color, const bool wrap) { uint32_t size = particlesize; if (advPartProps) {// use advanced size properties size = advPartProps[particleindex].size; @@ -1526,7 +1555,7 @@ void ParticleSystem1D::renderParticle(CRGB *framebuffer, const uint32_t particle uint32_t offset = 4; // offset to zero coordinate to write/read data in renderbuffer (actually needs to be 3, is decremented in the loop below) uint32_t blurpasses = size/64 + 1; // number of blur passes depends on size, four passes max uint32_t bitshift = 0; - for(uint32_t i = 0; i < blurpasses; i++) { + for (uint32_t i = 0; i < blurpasses; i++) { if (i == 2) //for the last two passes, use higher amount of blur (results in a nicer brightness gradient with soft edges) bitshift = 1; rendersize += 2; @@ -1540,7 +1569,7 @@ void ParticleSystem1D::renderParticle(CRGB *framebuffer, const uint32_t particle uint32_t xfb; // coordinates in frame buffer to write to note: by making this uint, only overflow has to be checked // transfer particle renderbuffer to framebuffer - for(uint32_t xrb = offset; xrb < rendersize+offset; xrb++) { + for (uint32_t xrb = offset; xrb < rendersize+offset; xrb++) { xfb = xfb_orig + xrb; if (xfb > maxXpixel) { if (wrap) { // wrap x to the other side if required @@ -1662,6 +1691,11 @@ void ParticleSystem1D::collideParticles(PSparticle1D *particle1, PSparticle1D *p void ParticleSystem1D::updateSystem(void) { setSize(SEGMENT.virtualLength()); // update size updatePSpointers(advPartProps != NULL); + setUsedParticles(usedpercentage); // update used particles based on percentage TODO: this does not need to be called for each frame, it only changes during transitions. can optimize? how to update after transition is finished? + if (partMemList.size() == 1 && !SEGMENT.isInTransition()) // if number of vector elements is one, this is the only system + renderSolo = true; // TODO: do as in 2D system once that works + else + renderSolo = false; } // 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) @@ -1672,13 +1706,15 @@ void ParticleSystem1D::updatePSpointers(bool isadvanced) { // a pointer MUST be 4 byte aligned. sizeof() in a struct/class is always aligned to the largest element. if it contains a 32bit, it will be padded to 4 bytes, 16bit is padded to 2byte alignment. // The PS is aligned to 4 bytes, a PSparticle is aligned to 2 and a struct containing only byte sized variables is not aligned at all and may need to be padded when dividing the memoryblock. // by making sure that the number of sources and particles is a multiple of 4, padding can be skipped here as alignent is ensured, independent of struct sizes. - particles = reinterpret_cast(this + 1); // pointer to particle array at data+sizeof(ParticleSystem) - sources = reinterpret_cast(particles + numParticles); // pointer to source(s) + particles = reinterpret_cast(getUpdatedParticlePointer(0, sizeof(PSparticle1D), availableParticles, usedParticles, usedpercentage, effectID)); // get memory, leave buffer size as is (request 0) + sources = reinterpret_cast(this + 1); // pointer to source(s) PSdataEnd = reinterpret_cast(sources + numSources); // pointer to first available byte after the PS for FX additional data if (isadvanced) { advPartProps = reinterpret_cast(sources + numSources); PSdataEnd = reinterpret_cast(advPartProps + numParticles); } + else + PSdataEnd = reinterpret_cast(sources + numParticles); } //non class functions to use for initialization @@ -1699,7 +1735,7 @@ uint32_t calculateNumberOfParticles1D(bool isadvanced) { return numberofParticles; } -uint32_t calculateNumberOfSources1D(uint8_t requestedsources) { +uint32_t calculateNumberOfSources1D(uint32_t requestedsources) { #ifdef ESP8266 int numberofSources = max(1, min((int)requestedsources,ESP8266_MAXSOURCES_1D)); // limit to 1 - 8 #elif ARDUINO_ARCH_ESP32S2 @@ -1713,10 +1749,13 @@ uint32_t calculateNumberOfSources1D(uint8_t requestedsources) { } //allocate memory for particle system class, particles, sprays plus additional memory requested by FX -bool allocateParticleSystemMemory1D(uint16_t numparticles, uint16_t numsources, bool isadvanced, uint16_t additionalbytes) { +bool allocateParticleSystemMemory1D(uint32_t numparticles, uint32_t numsources, bool isadvanced, uint32_t additionalbytes) { uint32_t requiredmemory = sizeof(ParticleSystem1D); + uint32_t availableparticles; // dummy variable + uint32_t usedparticles = 0; // dummy variable + if(getUpdatedParticlePointer(numparticles, sizeof(PSparticle1D), availableparticles, usedparticles, 0, 0) == nullptr) // allocate memory for particles + return false; // not enough memory, function ensures a minimum of numparticles are avialable // functions above make sure these are a multiple of 4 bytes (to avoid alignment issues) - requiredmemory += sizeof(PSparticle1D) * numparticles; if (isadvanced) requiredmemory += sizeof(PSadvancedParticle1D) * numparticles; requiredmemory += sizeof(PSsource1D) * numsources; @@ -1725,15 +1764,19 @@ bool allocateParticleSystemMemory1D(uint16_t numparticles, uint16_t numsources, } // initialize Particle System, allocate additional bytes if needed (pointer to those bytes can be read from particle system class: PSdataEnd) -// note: requestedparticles is relative, 127 = 50%, 255 = 100% (deafaults to 100% meaning one particle per pixel) -bool initParticleSystem1D(ParticleSystem1D *&PartSys, uint32_t requestedsources, uint32_t requestedparticles, uint16_t additionalbytes, bool advanced) { +// note: percentofparticles is in uint8_t, for example 191 means 75%, (deafaults to 255 or 100% meaning one particle per pixel), can be more than 100% (but not recommended, can cause out of memory) +bool initParticleSystem1D(ParticleSystem1D *&PartSys, uint32_t requestedsources, uint32_t percentofparticles, uint32_t additionalbytes, bool advanced) { if (SEGLEN == 1) return false; // single pixel not supported - uint32_t numparticles = (requestedparticles * calculateNumberOfParticles1D(advanced)) / 255; + uint32_t numparticles = (percentofparticles * calculateNumberOfParticles1D(advanced)) / 100; uint32_t numsources = calculateNumberOfSources1D(requestedsources); if (!allocateParticleSystemMemory1D(numparticles, numsources, advanced, additionalbytes)) { DEBUG_PRINT(F("PS init failed: memory depleted")); return false; } + // allocat rendering buffers + updateRenderingBuffer(framebuffer, SEGMENT.virtualLength(), true); + if(advanced) + updateRenderingBuffer(renderbuffer, 10, false); PartSys = new (SEGENV.data) ParticleSystem1D(SEGMENT.virtualLength(), numparticles, numsources, advanced); // particle system constructor return true; } @@ -1845,9 +1888,345 @@ static void fast_color_scale(CRGB &c, const uint32_t scale) { } // allocate memory for the 1D CRGB array in one contiguous block and set values to zero -static CRGB* allocate1Dbuffer(uint32_t length) { +/* +static CRGB* allocateCRGBbuffer(uint32_t length) { CRGB *array = (CRGB *)calloc(length, sizeof(CRGB)); return array; } +*/ + +////////////////////////////////////////////////// +// memory and transition management for particle system + +// note: these functions can only be called while strip is servicing + +// allocate memory using the FX data limit, if overridelimit is set, temporarily ignore the limit +void* allocatePSmemory(size_t size, bool overridelimit) { + PSPRINT(" PS mem alloc: "); + PSPRINTLN(size); + // buffer uses effect data, check if there is enough space + if (!overridelimit && Segment::getUsedSegmentData() + size > MAX_SEGMENT_DATA) { + // not enough memory + DEBUG_PRINT(F("!!! Effect RAM depleted: ")); + DEBUG_PRINTF_P(PSTR("%d/%d !!!\n"), size, Segment::getUsedSegmentData()); + errorFlag = ERR_NORAM; + return nullptr; + } + void* buffer = calloc(size, sizeof(byte)); + if (buffer == nullptr) { + DEBUG_PRINT(F("!!! Memory allocation failed !!!")); + errorFlag = ERR_NORAM; + return nullptr; + } + Segment::addUsedSegmentData(size); + #ifdef WLED_DEBUG_PS + PSPRINT("Pointer address: 0x"); + Serial.println((uintptr_t)buffer, HEX); + #endif + return buffer; +} + +// deallocate memory and update data usage, use with care! +void deallocatePSmemory(void* dataptr, uint32_t size) { + PSPRINTLN("deallocating memory:" + String(size)); + free(dataptr); // note: setting pointer null must be done by caller, passing a reference to a cast void pointer is not possible + Segment::addUsedSegmentData(size <= Segment::getUsedSegmentData() ? -size : -Segment::getUsedSegmentData()); +} + +/* +// TODO: +- usedparticles is now relative, update it in updatePS function (need a new variable as well) +- irgendwo muss man noch detektieren, ob eine transition gerade startet. wie macht man das am besten? +im getparticlepointer weiss man ja, dass ein system schon existiert, muss aber nicht, denn der watchdog kann ja noch aktiv sein und den memory halten... +beim ersten call von getpointer von diesem segment ist es der neue effekt, der das beantragt. man weiss also, welcher der beiden es ist und man weiss ob es in transition ist +ist also in transition und es ist der neue effekt und der watchdog ist auf null (oder eins, je nachdem wann man den kickt) dann sind zwei systeme aktiv. + if(SEGMENT.currentMode() != SEGMENT.mode) { // if true, this is the new particle effect +*/ + +// handle particle pointer, creates/extends buffer if needed and handles transition handover +// function is called in PS setup and updatepointer function +void* getUpdatedParticlePointer(const uint32_t requestedParticles, size_t structSize, uint32_t &availableToPS, uint32_t &usedbyPS, const uint8_t percentused, const uint8_t effectID) { // TODO: usedbyPS and percentused are currently unused, can remove if not required for transition + partMem *pmem = getPartMem(); + PSPRINT(" getParticlePointer "); + void* buffer = nullptr; + if (pmem) { // segment has a buffer + PSPRINT(" buffer found "); + if (requestedParticles) { // request for a new buffer, this is an init call + PSPRINT(" requested particles: " + String(requestedParticles)); + uint32_t requestsize = structSize * requestedParticles; // required buffer size + uint32_t currentsize = pmem->numParticles * pmem->sizeOfParticle; + if (requestsize > currentsize) { // request is larger than buffer, try to extend it + if (Segment::getUsedSegmentData() + requestsize - currentsize <= MAX_SEGMENT_DATA) { // enough memory available to extend buffer + PSPRINT(" extending particle buffer "); + buffer = allocatePSmemory(structSize * requestedParticles, true); // calloc new memory in FX data, override limit (temporary buffer) + if (buffer) { // allocaction successful, copy old particles to new buffer + memcpy(buffer, pmem->particleMemPointer, currentsize); // copy old particle buffer note: only required if transition but copy is fast and rarely happens + deallocatePSmemory( pmem->particleMemPointer, currentsize); // free old memory + pmem->particleMemPointer = buffer; // set new buffer + pmem->numParticles = requestedParticles; // update number of particles + pmem->sizeOfParticle = structSize; // update memory size of a particle + } // if buffer was not extended, the old, smaller buffer is used + } + } + /* + if (pmem->watchdog == 1) { // if a PS already exists during particle request, it kicked the watchdog in last frame, servicePSmem() adds 1 afterwards + PSPRINT(" PS is in transition "); + pmem->inTransition = true; // TODO: inTransition is only used for buffer xfer, is there a better way to do this? or is it required somewhere else? + //TODO: sizeOfParticle must be the same for both systems, otherwise the buffer cannot be transferred. or: it can be transferred but memory must be cleared. + //TODO2: handle memory initialization properly. if not in transition, particles must be initialized to TTL=0 and sat=255 + //TODO3: add palettes. man kann beim PS init die palette so kopieren: loadPalette( + } + else { // no watchdog, this is a new PS + // availableToPS = requestedParticles; // all particles are available todo: THIS IS TBD, probably ok to feed particles slowly + // pmem->inTransition = false; // no transition TODO: does this need to be set here? this function is called again in updatePS, can deactivate it then, right? + } + */ + + availableToPS = 2; // only give 2 particles to a new PS so old particles keep their settings and are not reeinitialized + PSPRINT(" available to NEW PS: "); + PSPRINT(availableToPS); + PSPRINT(" ,used pcnt: "); + PSPRINT(percentused); + PSPRINT(" ,used abs: "); + PSPRINTLN(usedbyPS); + return pmem->particleMemPointer; // return the available buffer on init call TODO: maybe split this into two functions, one for init and one for get? + } + PSPRINT(" use existing, "); + pmem->watchdog = 0; // kick watchdog + buffer = pmem->particleMemPointer; // buffer is already allocated + } + else { // if the id was not found create a buffer and add an element to the list + PSPRINT(" allocating particle buffer "); + buffer = allocatePSmemory(structSize * requestedParticles, false); // allocate new memory + if (buffer) PSPRINTLN(" bfr allocated"); + else PSPRINTLN(" brf alloc failed"); + + if (buffer) + partMemList.push_back({buffer, (uint16_t)requestedParticles, (uint8_t)structSize, strip.getCurrSegmentId(), 0, 0}); // add buffer to list note: if pushback fails, it may crash + else + return nullptr; // there is no memory available !!! TODO: if localbuffer is allocated, free it and try again, its no use having a buffer but no particles + pmem = getPartMem(); // get the pointer to the new element (check that it was added) + if (!pmem) { // something went wrong + free(buffer); + return nullptr; + } + //availableToPS = pmem->numParticles; + availableToPS = 2; // TODO: need to decide if a new system gets all particles or only a few + return pmem->particleMemPointer; // directly return the buffer on init call + } + //PSPRINT("particle buffer address: 0x"); + //PSPRINTLN((uintptr_t)buffer, HEX); + + // now we have a valid buffer, check if we are in transition + if (SEGMENT.isInTransition()) { + bool effectchanged = (SEGMENT.currentMode() != SEGMENT.mode); // FX changed, transition the particle buffer + + if (effectchanged) { + PSPRINT(" FX changed "); + PSPRINT(" this mode: " + String(effectID)); + PSPRINT("/ old mode: " + String(SEGMENT.currentMode())); + PSPRINTLN(" new mode: " + String(SEGMENT.mode)); + uint32_t progress = SEGMENT.progress(); // transition progress + uint32_t newAvailable = 0; + uint32_t totransfer = 0; // number of particles to transfer + uint32_t transferstartidx = 0; // indes of the first particle to transfer + if (SEGMENT.mode == effectID) { // mode returns the new effect ID -> function was called from new FX + PSPRINT(" new FX"); + PSPRINT(" progress: " + String(progress)); + newAvailable = (pmem->numParticles * progress) >> 16; // update total particles available to this PS + if(newAvailable < 2) newAvailable = 2; // always give a minimum amount (this can lead to overlap, currently not a problem but some new FX may not like it) + //TODO: maybe reduce percentage on first call? SEGMENT.call==0? + //PSPRINTLN("trans: available particles: " + String(availableToPS)); + // note: no need to change buffer pointer, new segment gets the start of the buffer + transferstartidx = availableToPS; // start after already transferred particles + totransfer = newAvailable - availableToPS; // number of particles to transfer increases with progress + } + else if (SEGMENT.currentMode() == effectID) { // seg.currentMode() is the old effect ID during transitions, this was called from old FX + SEGMENT.setCurrentPalette(true); // load the old palette into segment + PSPRINT(" progress: " + String(progress)); + progress = 0xFFFFU - progress; // inverted transition progress + PSPRINT(" inv.prog: " + String(progress)); + + newAvailable = (pmem->numParticles * progress) >> 16; + PSPRINT(" newAvailable: " + String(newAvailable)); + PSPRINT(" oldAvailable: " + String(availableToPS)); + if(newAvailable < 2) newAvailable = 2; // always give a minimum amount + PSPRINT(" old FX"); + //PSPRINTLN("trans: available particles: " + String(availableToPS)); + buffer = (void*)((uint8_t*)buffer + (pmem->numParticles - newAvailable) * structSize); // old effect gets the end of the buffer + // note: startindex is start of buffer (transferred from end to start, so end particles are the old ones) + totransfer = availableToPS - newAvailable; // number of particles to transfer decreases with progress + } + PSPRINT("a"); + // initialize newly transferred particles note: to have PS interact during transition, this must be changed. could initialize TTL and perpetual only TODO: need to be more clever: need to clear buffer if 2D->1D or 1D->2D transition or settings are nonsensical. + + PSPRINT(" transferstartidx: " + String(transferstartidx)); + PSPRINT(" totransfer: " + String(totransfer)); + if(totransfer <= availableToPS) { // overflow check TODO: why do overflows happen? + if(structSize == sizeof(PSparticle)) { // 2D particle + PSparticle *particles = (PSparticle*)buffer; + for (uint32_t i = transferstartidx; i < transferstartidx + totransfer; i++) { + particles[i].perpetual = false; // particle ages + if(particles[i].ttl > 50) particles[i].ttl = 50; // reduce TTL so it will die soon + } + } + else { // 1D particle system + PSparticle1D *particles = (PSparticle1D*)buffer; + for (uint32_t i = transferstartidx; i < transferstartidx + totransfer; i++) { + particles[i].perpetual = false; // particle ages + if(particles[i].ttl > 50) particles[i].ttl = 50; // reduce TTL so it will die soon + } + } + } + availableToPS = newAvailable; + // TODO: if this is correct, the first if statement can be removed and availabletoPS updated here or: this part of the function can be optimized still + } + else { // same effect transition + PSPRINT(" same FX "); + availableToPS = pmem->numParticles; // no transition, full buffer available + // pmem->inTransition = false; + } + + } else { // no transition, full buffer available + availableToPS = pmem->numParticles; // no transition, full buffer available + PSPRINTLN(" no trans, no of partcls: " + String(availableToPS)); + //pmem->inTransition = false; + } + PSPRINTLN("END getParticlePointer "); + return buffer; +} + +// function to update the framebuffer and renderbuffer +void updateRenderingBuffer(CRGB* buffer, uint32_t requiredpixels, bool isFramebuffer) { + PSPRINTLN("updateRenderingBuffer"); + uint32_t currentBufferSize = isFramebuffer ? frameBufferSize : renderBufferSize; + if(currentBufferSize < requiredpixels) { // check current buffer size + if(buffer) deallocatePSmemory((void*)buffer, currentBufferSize * sizeof(CRGB)); + buffer = (CRGB *)allocatePSmemory(requiredpixels * sizeof(CRGB), false); + if(buffer) { + if(isFramebuffer) { + framebuffer = buffer; + frameBufferSize = requiredpixels; + memset(framebuffer, 0, frameBufferSize * sizeof(CRGB)); + } else { + renderbuffer = buffer; + renderBufferSize = requiredpixels; + } + return; + } else { + if(isFramebuffer) { + framebuffer = nullptr; + frameBufferSize = 0; + } else { + renderbuffer = nullptr; + renderBufferSize = 0; + } + } + } +} + +// get the pointer to the particle memory for the segment +partMem* getPartMem(void) { // TODO: maybe there is a better/faster way than using vectors? + uint8_t segID = strip.getCurrSegmentId(); + for (partMem &pmem : partMemList) { + if (pmem.id == segID) { + return &pmem; + } + } + return nullptr; +} + +// service the particle system memory, free memory if idle too long +// note: doing it this way makes it independent of the implementation of segment management but is not the most memory efficient way +void servicePSmem(uint8_t idx) { + // Increment watchdog for each entry and deallocate if idle too long (i.e. no PS running on the segment) + if(partMemList.size() > 0) { + for (size_t i = 0; i < partMemList.size(); i++) { + if(partMemList[i].id == idx) + { + partMemList[i].watchdog++; // Increment watchdog counter + PSPRINT("pmem servic. list size: "); + PSPRINT(partMemList.size()); + PSPRINT(" element: "); + PSPRINT(i); + PSPRINT(" watchdog: "); + PSPRINTLN(partMemList[i].watchdog); + if (partMemList[i].watchdog > MAX_MEMIDLE) { + PSPRINTLN("psmem free"); //deallocating memory:2560 + deallocatePSmemory(partMemList[i].particleMemPointer, partMemList[i].numParticles * partMemList[i].sizeOfParticle); // Free memory + partMemList.erase(partMemList.begin() + i); // Remove entry + partMemList.shrink_to_fit(); // partMemList is small, memory operations should be unproblematic + } + break; + } + } + } + else { // no particle system running, release buffer memory + if(framebuffer) { + deallocatePSmemory((void*)framebuffer, frameBufferSize * sizeof(CRGB)); // free the buffers + framebuffer = nullptr; + frameBufferSize = 0; + } + if(renderbuffer) { + deallocatePSmemory((void*)renderbuffer, renderBufferSize * sizeof(CRGB)); + renderbuffer = nullptr; + renderBufferSize = 0; + } + } +} + +// transfer the frame buffer to the segment and handle transitional rendering (both FX render to the same buffer so they mix) +void transferBuffer(uint32_t width, uint32_t height, uint8_t effectID) { + PSPRINT(" xfer buf "); + if(!framebuffer) return; // no buffer, nothing to transfer + /* + if(SEGMENT.isInTransition()) { + // check if PS is also in transition + partMem *pmem = getPartMem(); + if (pmem && pmem->inTransition) // PS to PS transition + { + if(SEGMENT.mode == effectID) { // segment.mode() returns new FX, new FX is rendered first, skip transfer + PSPRINTLN("skip xfer"); + // return; // leave the buffer as is, let the old effect render to it as well before transferring + } + } + }*/ + if(height) { // is 2D, 1D passes height = 0 + int32_t yflipped; + height--; // reduce by one for easier flip calculation below (instead of counting on compiler optimization) + PSPRINT("xfer 2D"); + for (uint32_t y = 0; y <= height; y++) { + yflipped = height - y; + int index = y * width; // current row index for 1D buffer + for (uint32_t x = 0; x < width; x++) { + SEGMENT.setPixelColorXY((int)x, (int)yflipped, framebuffer[index++]); + } + } + } else { + for (uint32_t x = 0; x < width; x++) { + SEGMENT.setPixelColor(x, framebuffer[x]); + } + } + if(!renderSolo) { // there are other segments with particle systems, clear the buffer (PS takes over if rendersolo is true) + memset(framebuffer, 0, frameBufferSize * sizeof(CRGB)); // clear the buffer TODO: also need to clear during transitions? + PSPRINT(" buffer cleared "); + } + + #ifdef WLED_DEBUG_PS + PSPRINT(" done. framebfr addr: 0x"); + Serial.println((uintptr_t)framebuffer, HEX); + #endif + +} + + /* + //memory fragmentation check: + PSPRINT("heap: "); + PSPRINT(heap_caps_get_free_size(MALLOC_CAP_8BIT)); + PSPRINT(" block: "); + PSPRINTLN(heap_caps_get_largest_free_block(MALLOC_CAP_8BIT)); + */ + + #endif // !defined(WLED_DISABLE_PARTICLESYSTEM2D) || !defined(WLED_DISABLE_PARTICLESYSTEM1D) diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index a42cea3f9b..fdda50a225 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -14,14 +14,45 @@ #include "wled.h" #define PS_P_MAXSPEED 120 // maximum speed a particle can have (vx/vy is int8) +#define MAX_MEMIDLE 200 // max idle time (in frames) before memory is deallocated (if deallocated during an effect, it will crash!) note: setting this to a high 8bit value prevents fragmentation + +#define WLED_DEBUG_PS + +#ifdef WLED_DEBUG_PS + #define PSPRINT(x) Serial.print(x) + #define PSPRINTLN(x) Serial.println(x) +#else + #define PSPRINT(x) + #define PSPRINTLN(x) +#endif + +// memory and transition manager +struct partMem { + void* particleMemPointer; // pointer to particle memory + uint32_t numParticles; // number of particles that fit in memory note: could be a uint16_t but padding will increase the struct size so 12 bytes anyway + uint8_t sizeOfParticle; // size of the particle struct in this buffer + uint8_t id; // ID of segment this memory belongs to + uint8_t inTransition; // to track transitions TODO: probably not be needed anymore + uint8_t watchdog; // counter to handle deallocation +}; + + +void* getUpdatedParticlePointer(const uint32_t requestedParticles, size_t structSize, uint32_t &availableToPS, uint32_t &usedbyPS, const uint8_t percentused, const uint8_t effectID); // update particle memory pointer, handles transitions +//extern CRGB *framebuffer; // local frame buffer for rendering +//extern CRGB *renderbuffer; // local particle render buffer for advanced particles +//extern uint16_t frameBufferSize; // size in pixels, used to check if framebuffer is large enough for current segment TODO: make this in bytes, not in pixels +//extern uint16_t renderBufferSize; // size in pixels, if allcoated by a 1D system it needs to be updated for 2D +partMem* getPartMem(void); // returns pointer to memory struct for current segment or nullptr +void updateRenderingBuffer(CRGB* buffer, uint32_t requiredsize, bool isFramebuffer); // allocate CRGB rendering buffer, update size if needed +void transferBuffer(uint32_t width, uint32_t height, uint8_t effectID); // transfer the buffer to the segment +//TODO: add 1D version +void servicePSmem(uint8_t idx); // increments watchdog, frees memory if idle too long + +// update number of particles to use, must never be more than allocated (= particles allocated by the calling system) +inline void updateUsedParticles(const uint32_t allocated, const uint32_t available, const uint8_t percentage, uint32_t &used) { + used = max((uint32_t)1, (min(allocated, available) * ((uint32_t)percentage + 1)) >> 8); // always use a minimum of 1 particle +} -//shared functions (used both in 1D and 2D system) -static int32_t calcForce_dv(int8_t force, uint8_t *counter); -static int32_t limitSpeed(int32_t speed); -static bool checkBoundsAndWrap(int32_t &position, const int32_t max, const int32_t particleradius, bool wrap); // returns false if out of bounds by more than particleradius -static void fast_color_add(CRGB &c1, const CRGB &c2, uint32_t scale = 255); // fast and accurate color adding with scaling (scales c2 before adding) -static void fast_color_scale(CRGB &c, const uint32_t scale); // fast scaling function using 32bit variable and pointer. note: keep 'scale' within 0-255 -static CRGB *allocate1Dbuffer(uint32_t length); #endif #ifndef WLED_DISABLE_PARTICLESYSTEM2D @@ -57,8 +88,8 @@ typedef union { byte asByte; // access as a byte, order is: LSB is first entry in the list above } PSsettings2D; -//struct for a single particle (10 bytes) -typedef struct { +//struct for a single particle +typedef struct { // 10 bytes int16_t x; // x position in particle system int16_t y; // y position in particle system int8_t vx; // horizontal velocity @@ -73,14 +104,14 @@ typedef struct { bool state : 1; //can be used by FX to track state, not used in PS } PSparticle; -// struct for additional particle settings (optional) -typedef struct { +// struct for additional particle settings (option) +typedef struct { // 2 bytes uint8_t size; // particle size, 255 means 10 pixels in diameter uint8_t forcecounter; // counter for applying forces to individual particles } PSadvancedParticle; -// struct for advanced particle size control (optional) -typedef struct { +// struct for advanced particle size control (option) +typedef struct { // 7 bytes uint8_t asymmetry; // asymmetrical size (0=symmetrical, 255 fully asymmetric) uint8_t asymdir; // direction of asymmetry, 64 is x, 192 is y (0 and 128 is symmetrical) uint8_t maxsize; // target size for growing @@ -111,7 +142,7 @@ typedef struct { // class uses approximately 60 bytes class ParticleSystem2D { public: - ParticleSystem2D(uint16_t width, uint16_t height, uint16_t numberofparticles, uint16_t numberofsources, bool isadvanced = false, bool sizecontrol = false); // constructor + ParticleSystem2D(uint32_t width, uint32_t height, uint32_t numberofparticles, uint32_t numberofsources, bool isadvanced = false, bool sizecontrol = false); // constructor // note: memory is allcated in the FX function, no deconstructor needed void update(void); //update the particles according to set options and render to the matrix void updateFire(uint32_t intensity, bool renderonly = false); // update function for fire, if renderonly is set, particles are not updated (required to fix transitions with frameskips) @@ -134,7 +165,7 @@ class ParticleSystem2D { void pointAttractor(uint16_t particleindex, PSparticle *attractor, uint8_t strength, bool swallow); void lineAttractor(uint16_t particleindex, PSparticle *attractorcenter, uint16_t attractorangle, uint8_t strength); // set options - void setUsedParticles(uint32_t num); + void setUsedParticles(uint8_t percentage); 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 setWallRoughness(uint8_t roughness); // wall roughness randomizes wall collisions @@ -159,13 +190,13 @@ class ParticleSystem2D { int32_t maxX, maxY; // particle system size i.e. width-1 / height-1 in subpixels, Note: all "max" variables must be signed to compare to coordinates (which are signed) int32_t maxXpixel, maxYpixel; // last physical pixel that can be drawn to (FX can read this to read segment size if required), equal to width-1 / height-1 uint32_t numSources; // number of sources - uint32_t numParticles; // number of particles available in this system - uint32_t usedParticles; // number of particles used in animation (can be smaller then numParticles) + uint32_t usedParticles; // number of particles used in animation, is relative to 'availableParticles' + //note: some variables are 32bit for speed and code size at the cost of ram private: //rendering functions void ParticleSys_render(bool firemode = false, uint32_t fireintensity = 128); - void renderParticle(CRGB *framebuffer, const uint32_t particleindex, const uint32_t brightness, const CRGB& color, CRGB *renderbuffer, const bool wrapX, const bool wrapY); + void renderParticle(const uint32_t particleindex, const uint32_t brightness, const CRGB& color, const bool wrapX, const bool wrapY); //paricle physics applied by system if flags are set void applyGravity(); // applies gravity to all particles void handleCollisions(); @@ -179,26 +210,30 @@ class ParticleSystem2D { int16_t wraparound(uint16_t p, uint32_t maxvalue); // note: variables that are accessed often are 32bit for speed PSsettings2D particlesettings; // settings used when updating particles (can also used by FX to move sources), do not edit properties directly, use functions above + uint32_t numParticles; // total number of particles allocated by this system note: during transitions, less are available, use availableParticles + uint32_t availableParticles; // number of particles available for use (can be more or less than numParticles, assigned by memory manager) uint32_t emitIndex; // index to count through particles to emit so searching for dead pixels is faster int32_t collisionHardness; uint32_t wallHardness; - uint32_t wallRoughness; - uint8_t gforcecounter; // counter for global gravity - int8_t gforce; // gravity strength, default is 8 (negative is allowed, positive is downwards) + uint32_t wallRoughness; // randomizes wall collisions uint32_t collisioncounter; // counter to handle collisions TODO: could use the SEGMENT.call? + uint32_t particleHardRadius; // hard surface radius of a particle, used for collision detection (32bit for speed) uint8_t forcecounter; // counter for globally applied forces + uint8_t gforcecounter; // counter for global gravity + int8_t gforce; // gravity strength, default is 8 (negative is allowed, positive is downwards) // global particle properties for basic particles uint8_t particlesize; // global particle size, 0 = 2 pixels, 255 = 10 pixels (note: this is also added to individual sized particles) - uint32_t particleHardRadius; // hard surface radius of a particle, used for collision detection uint8_t motionBlur; // enable motion blur, values > 100 gives smoother animations. Note: motion blurring does not work if particlesize is > 0 + uint8_t effectID; // ID of the effect that is using this particle system, used for transitions + uint8_t usedpercentage; // percentage of particles used in the system, used during transition updates }; void blur2D(CRGB *colorbuffer, uint32_t xsize, uint32_t ysize, uint32_t xblur, uint32_t yblur, uint32_t xstart = 0, uint32_t ystart = 0, bool isparticle = false); // initialization functions (not part of class) -bool initParticleSystem2D(ParticleSystem2D *&PartSys, uint8_t requestedsources, uint16_t additionalbytes = 0, bool advanced = false, bool sizecontrol = false); -uint32_t calculateNumberOfParticles2D(bool advanced, bool sizecontrol); -uint32_t calculateNumberOfSources2D(uint8_t requestedsources); -bool allocateParticleSystemMemory2D(uint16_t numparticles, uint16_t numsources, bool advanced, bool sizecontrol, uint16_t additionalbytes); +bool initParticleSystem2D(ParticleSystem2D *&PartSys, uint32_t requestedsources, uint32_t additionalbytes = 0, bool advanced = false, bool sizecontrol = false); +uint32_t calculateNumberOfParticles2D(uint32_t pixels, bool advanced, bool sizecontrol); +uint32_t calculateNumberOfSources2D(uint32_t pixels, uint32_t requestedsources); +bool allocateParticleSystemMemory2D(uint32_t numparticles, uint32_t numsources, bool advanced, bool sizecontrol, uint32_t additionalbytes); #endif // WLED_DISABLE_PARTICLESYSTEM2D //////////////////////// @@ -273,7 +308,7 @@ typedef struct { class ParticleSystem1D { public: - ParticleSystem1D(uint16_t length, uint16_t numberofparticles, uint16_t numberofsources, bool isadvanced = false); // constructor + ParticleSystem1D(uint32_t length, uint32_t numberofparticles, uint32_t numberofsources, bool isadvanced = false); // constructor // note: memory is allcated in the FX function, no deconstructor needed void update(void); //update the particles according to set options and render to the matrix void updateSystem(void); // call at the beginning of every FX, updates pointers and dimensions @@ -286,7 +321,7 @@ class ParticleSystem1D void applyGravity(PSparticle1D *part); // applies gravity to single particle (use this for sources) void applyFriction(int32_t coefficient); // apply friction to all used particles // set options - void setUsedParticles(uint32_t num); + void setUsedParticles(uint8_t percentage); void setWallHardness(uint8_t hardness); // hardness for bouncing on the wall if bounceXY is set void setSize(uint16_t x); //set particle system size (= strip length) void setWrap(bool enable); @@ -308,13 +343,12 @@ class ParticleSystem1D int32_t maxX; // particle system size i.e. width-1, Note: all "max" variables must be signed to compare to coordinates (which are signed) int32_t maxXpixel; // last physical pixel that can be drawn to (FX can read this to read segment size if required), equal to width-1 uint32_t numSources; // number of sources - uint32_t numParticles; // number of particles available in this system - uint32_t usedParticles; // number of particles used in animation (can be smaller then numParticles) + uint32_t usedParticles; // number of particles used in animation, is relative to 'availableParticles' private: //rendering functions void ParticleSys_render(void); - void renderParticle(CRGB *framebuffer, const uint32_t particleindex, const uint32_t brightness, const CRGB &color, CRGB *renderbuffer, const bool wrap); + void renderParticle(const uint32_t particleindex, const uint32_t brightness, const CRGB &color, const bool wrap); //paricle physics applied by system if flags are set void applyGravity(); // applies gravity to all particles @@ -324,26 +358,29 @@ class ParticleSystem1D //utility functions void updatePSpointers(bool isadvanced); // update the data pointers to current segment data space //void updateSize(PSadvancedParticle *advprops, PSsizeControl *advsize); // advanced size control - //void getParticleXYsize(PSadvancedParticle *advprops, PSsizeControl *advsize, uint32_t &xsize, uint32_t &ysize); void bounce(int8_t &incomingspeed, int8_t ¶llelspeed, int32_t &position, uint16_t maxposition); // bounce on a wall // note: variables that are accessed often are 32bit for speed PSsettings1D particlesettings; // settings used when updating particles + uint32_t numParticles; // total number of particles allocated by this system note: never use more than this, even if more are available (only this many advanced particles are allocated) + uint32_t availableParticles; // number of particles available for use (can be more or less than numParticles, assigned by memory manager) uint32_t emitIndex; // index to count through particles to emit so searching for dead pixels is faster int32_t collisionHardness; + uint32_t particleHardRadius; // hard surface radius of a particle, used for collision detection uint32_t wallHardness; uint8_t gforcecounter; // counter for global gravity int8_t gforce; // gravity strength, default is 8 (negative is allowed, positive is downwards) uint8_t forcecounter; // counter for globally applied forces //uint8_t collisioncounter; // counter to handle collisions TODO: could use the SEGMENT.call? -> currently unused - // global particle properties for basic particles + //global particle properties for basic particles uint8_t particlesize; // global particle size, 0 = 1 pixel, 1 = 2 pixels, larger sizez TBD (TODO: need larger sizes?) - uint32_t particleHardRadius; // hard surface radius of a particle, used for collision detection uint8_t motionBlur; // enable motion blur, values > 100 gives smoother animations. Note: motion blurring does not work if particlesize is > 0 + uint8_t effectID; // ID of the effect that is using this particle system, used for transitions + uint8_t usedpercentage; // percentage of particles used in the system, used for transitions }; -bool initParticleSystem1D(ParticleSystem1D *&PartSys, uint32_t requestedsources, uint32_t requestedparticles = 255, uint16_t additionalbytes = 0, bool advanced = false); +bool initParticleSystem1D(ParticleSystem1D *&PartSys, uint32_t requestedsources, uint32_t percentofparticles = 255, uint32_t additionalbytes = 0, bool advanced = false); uint32_t calculateNumberOfParticles1D(bool isadvanced); -uint32_t calculateNumberOfSources1D(uint8_t requestedsources); -bool allocateParticleSystemMemory1D(uint16_t numparticles, uint16_t numsources, bool isadvanced, uint16_t additionalbytes); +uint32_t calculateNumberOfSources1D(uint32_t requestedsources); +bool allocateParticleSystemMemory1D(uint32_t numparticles, uint32_t numsources, bool isadvanced, uint32_t additionalbytes); void blur1D(CRGB *colorbuffer, uint32_t size, uint32_t blur, uint32_t start = 0); #endif // WLED_DISABLE_PARTICLESYSTEM1D diff --git a/wled00/palettes.h b/wled00/palettes.h index 41dfbbc163..021c4a55c2 100644 --- a/wled00/palettes.h +++ b/wled00/palettes.h @@ -13,6 +13,8 @@ #ifndef PalettesWLED_h #define PalettesWLED_h +#include "colorpalettes.h" + const byte ib_jul01_gp[] PROGMEM = { 0, 194, 1, 1, 94, 1, 29, 18, diff --git a/wled00/wled.cpp b/wled00/wled.cpp index 13d43218af..9855ca1e8c 100644 --- a/wled00/wled.cpp +++ b/wled00/wled.cpp @@ -360,6 +360,8 @@ void WLED::setup() #if !defined(WLED_DEBUG) && defined(ARDUINO_ARCH_ESP32) && !defined(WLED_DEBUG_HOST) && ARDUINO_USB_CDC_ON_BOOT Serial.setDebugOutput(false); // switch off kernel messages when using USBCDC #endif + Serial.println("size of WS2812FX " + String(sizeof(WS2812FX))); + Serial.println("size of Segmen t" + String(sizeof(Segment))); DEBUG_PRINTLN(); DEBUG_PRINTF_P(PSTR("---WLED %s %u INIT---\n"), versionString, VERSION); DEBUG_PRINTLN(); From 3e0822dc7e0a2725d730a2d6a07192a5e3a71a11 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Wed, 20 Nov 2024 08:22:25 +0100 Subject: [PATCH 140/219] work in progress: lots of changes & fixes, transitions now work correctly in 2D - still fragile code with lots of cleanup to do --- wled00/FX.h | 1 + wled00/FXparticleSystem.cpp | 353 +++++++++++++++++++++++++----------- wled00/FXparticleSystem.h | 14 +- 3 files changed, 258 insertions(+), 110 deletions(-) diff --git a/wled00/FX.h b/wled00/FX.h index 1255c10b34..4e4048ad1f 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -549,6 +549,7 @@ typedef struct Segment { inline static void addUsedSegmentData(int len) { _usedSegmentData += len; } #ifndef WLED_DISABLE_MODE_BLEND inline static void modeBlend(bool blend) { _modeBlend = blend; } + inline static bool getmodeBlend(void) { return _modeBlend; } #endif static void handleRandomPalette(); inline static const CRGBPalette16 &getCurrentPalette() { return Segment::_currentPalette; } diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index 88a7eb68fc..5f66b6493f 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -30,14 +30,17 @@ static void fast_color_scale(CRGB &c, const uint32_t scale); // fast scaling fun // global variables for memory management std::vector partMemList; // list of particle memory pointers +partMem *pmem = nullptr; // pointer to particle memory of current segment, updated in getUpdatedParticlePointer() CRGB *framebuffer = nullptr; // local frame buffer for rendering CRGB *renderbuffer = nullptr; // local particle render buffer for advanced particles uint16_t frameBufferSize = 0; // size in pixels, used to check if framebuffer is large enough for current segment uint16_t renderBufferSize = 0; // size in pixels, if allcoated by a 1D system it needs to be updated for 2D uint8_t renderSolo = 0; // is set to >0 if this is the only particle system using the so it can use the buffer continuously (faster blurring) +uint8_t globalBlur = 0; // blur to apply if multiple PS are using the buffer +bool transferflag = false; //DEBUG test !!! do it right ParticleSystem2D::ParticleSystem2D(uint32_t width, uint32_t height, uint32_t numberofparticles, uint32_t numberofsources, bool isadvanced, bool sizecontrol) { - effectID = SEGMENT.mode; + effectID = SEGMENT.mode; // new FX called init, save the effect ID numSources = numberofsources; // number of sources allocated in init numParticles = numberofparticles; // number of particles allocated in init usedpercentage = 255; // use all particles by default, usedParticles is updated in updatePSpointers() @@ -52,16 +55,12 @@ ParticleSystem2D::ParticleSystem2D(uint32_t width, uint32_t height, uint32_t num motionBlur = 0; //no fading by default emitIndex = 0; - //initialize some default non-zero values most FX use for (uint32_t i = 0; i < numSources; i++) { sources[i].source.sat = 255; //set saturation to max by default sources[i].source.ttl = 1; //set source alive } - for (uint32_t i = 0; i < numParticles; i++) { - //particles[i].ttl = 0; // set all particles to dead TODO: do this in manager! - particles[i].sat = 255; // full saturation - } + } // update function applies gravity, moves the particles, handles collisions and renders the particles @@ -104,7 +103,7 @@ void ParticleSystem2D::updateFire(uint32_t intensity, bool renderonly) { void ParticleSystem2D::setUsedParticles(uint8_t percentage) { usedpercentage = percentage; // note usedParticles is updated in memory manager updateUsedParticles(numParticles, availableParticles, usedpercentage, usedParticles); - PSPRINT(" allocated particles: "); + PSPRINT(" SetUsedpaticles: allocated particles: "); PSPRINT(numParticles); PSPRINT(" available particles: "); PSPRINT(availableParticles); @@ -561,39 +560,76 @@ void ParticleSystem2D::pointAttractor(uint16_t particleindex, PSparticle *attrac void ParticleSystem2D::ParticleSys_render(bool firemode, uint32_t fireintensity) { CRGB baseRGB; uint32_t brightness; // particle brightness, fades if dying +/* + if(transferflag) { + transferflag = false; + transferBuffer((maxXpixel + 1), (maxYpixel + 1), effectID); + }*/ + Serial.print(" render "); + if (framebuffer) { - if (motionBlur > 0) { // using SEGMENT.fadeToBlackBy is much slower, this approximately doubles the speed of fade calculation - uint32_t yflipped; - for (uint32_t y = 0; y <= maxYpixel; y++) { - yflipped = maxYpixel - y; - int index = y * (maxXpixel + 1); // current row index for 1D buffer - for (uint32_t x = 0; x <= maxXpixel; x++) { - if(renderSolo < 2) // there are/were other systems using the buffer, read data from segment - framebuffer[index] = SEGMENT.getPixelColorXY(x, yflipped); //copy to local buffer - fast_color_scale(framebuffer[index], motionBlur); - index++; - } + // update global blur (used for blur transitions) + if(motionBlur > 0) { + uint32_t bluramount = motionBlur; + if(pmem->inTransition) { // FX transition + uint32_t progress = SEGMENT.progress(); // transition progress + if (pmem->inTransition != effectID) // inTransition is set to new effectID so this is true if we are to old FX + progress = 0xFFFFU - progress; // inverted transition progress for old FX + bluramount = (bluramount * progress) >> 16; // fade the blur during transitions } + if(globalBlur < bluramount) globalBlur = bluramount; // keep track of highest blur amount TODO: transition could be made better, i.e. take the diff from old to new and transition that? could not clear the global blur and derive it from that plus new FX blur } - else if(renderSolo) { // no blurring and no other systems, clear the buffer (manager skips clearing to enable faster blur) - memset(framebuffer, 0, frameBufferSize * sizeof(CRGB)); // clear the buffer - PSPRINT(" PS buffer cleared "); + + /* + Note on blurring / clearing + when rendersolo is active, blurring can be done on the buffer directly, if this is a transition, skip blurring if this is the old FX (i.e. blur before new FX is rendered) + if rendersolo is not set, the buffer must be either cleared (if no blurring and no pmem->inTransition) or read from segment (if blur is active AND this is not the old FX i.e. pmem->inTransition != fxID) + if multiple segments are used, rendersolor is false, if then transitioning from classic to PS fx, blurring is applied. if transitioning away from PS, blurring is skipped (as this is the old FX) + + // first check if rendersolo is active, if yes, check if this is a transition + // if rendersolo is active and not a transition, blur the buffer or clear it + // if this is a transition, only clear or blur if this is the new FX (which is rendered first) + // if this is the new FX in a transition and rendersolo is not active, read the buffer from the segment befor applying blur (clear otherwise) + + TODO: blurring during transitions is now completely disabled, there is a bug in the below code that skips blurring and probably does clearing instead. +*/ + // handle buffer blurring or clearing + bool bufferNeedsUpdate = (!pmem->inTransition || pmem->inTransition == effectID); // not a transition; or new FX: update buffer (blur, or clear) + if(bufferNeedsUpdate) { + if (globalBlur > 0) { // blurring active: if not a transition or is newFX, read data from segment before blurring (old FX can render to it afterwards) + Serial.println(" blurring: " + globalBlur); + for (uint32_t y = 0; y <= maxYpixel; y++) { + int index = y * (maxXpixel + 1); + for (uint32_t x = 0; x <= maxXpixel; x++) { + if (renderSolo < 2) { // sharing the framebuffer: read from segment + uint32_t yflipped = maxYpixel - y; + framebuffer[index] = SEGMENT.getPixelColorXY(x, yflipped); // read from segment + } + fast_color_scale(framebuffer[index], globalBlur); + index++; + } + } + globalBlur = 0; // reset global blur for next frame + } + else if (bufferNeedsUpdate) { // no blurring and not a transition or is new FX + memset(framebuffer, 0, frameBufferSize * sizeof(CRGB)); + } } } - else { // no local buffer available - if (motionBlur > 0) + else { // no local buffer available, apply blur to segment TODO: this could also be done in 2D blur function but must be called by first PS rendering to it, may be complex to find out which on is the first (in overlay, in transitions its easy) + if (motionBlur > 0) // TODO2: blurring during transitions (and maybe also overlay) could be done in the mem-manager, it is called in update pointers and knows which PS is the first one to render. SEGMENT.fadeToBlackBy(255 - motionBlur); else - SEGMENT.fill(BLACK); //clear the buffer before rendering to it + SEGMENT.fill(BLACK); //clear the buffer before rendering next frame } + bool wrapX = particlesettings.wrapX; // use local variables for faster access bool wrapY = particlesettings.wrapY; // go over particles and render them to the buffer for (uint32_t i = 0; i < usedParticles; i++) { if (particles[i].outofbounds || particles[i].ttl == 0) continue; - // generate RGB values for particle if (firemode) { brightness = (uint32_t)particles[i].ttl * (3 + (fireintensity >> 5)) + 20; @@ -613,6 +649,18 @@ void ParticleSystem2D::ParticleSys_render(bool firemode, uint32_t fireintensity) } if (particlesize > 0) { + // TODO: if global particle size is used, need to transfer the buffer NOW, clear it, render to it, then add the 'background' again from the segment. + /* + for (uint32_t y = 0; y <= maxYpixel; y++) { + uint32_t yflipped = maxYpixel - y; + int index = y * (maxXpixel + 1); // current row index for 1D buffer + for (uint32_t x = 0; x <= maxXpixel; x++) { + CRGB sourcecolor = SEGMENT.getPixelColorXY(x, yflipped); + fast_color_add(sourcecolor, framebuffer[index]); + index++; + } + } + */ uint32_t passes = particlesize / 64 + 1; // number of blur passes, four passes max uint32_t bluramount = particlesize; uint32_t bitshift = 0; @@ -630,8 +678,29 @@ void ParticleSystem2D::ParticleSys_render(bool firemode, uint32_t fireintensity) } } - // transfer local buffer back to segment (if available) - transferBuffer((maxXpixel + 1), (maxYpixel + 1), effectID); + // transfer framebuffer to segment if available + if (framebuffer && pmem->inTransition != effectID) { // not in transition or is old FX TODO: how to handle this for overlay rendering or multiple segment rendering? need to check for rendersolo as well? + #ifndef WLED_DISABLE_MODE_BLEND + bool tempBlend = SEGMENT.getmodeBlend(); + if (pmem->inTransition) + SEGMENT.modeBlend(false); // temporarily disable FX blending in PS to PS transition (using local buffer to do PS blending) + #endif + int yflipped; + for (uint32_t y = 0; y <= maxYpixel; y++) { + yflipped = maxYpixel - y; + int index = y * (maxXpixel + 1); // current row index for 1D buffer + for (uint32_t x = 0; x <= maxXpixel; x++) { + CRGB *c = &framebuffer[index++]; + uint32_t clr = RGBW32(c->r,c->g,c->b,0); // convert to 32bit color + //if(clr > 0) // not black TODO: not transferring black is faster and enables overlay, but requries proper handling of buffer clearing, which is quite complex and probably needs a change to SEGMENT handling. + SEGMENT.setPixelColorXY((int)x, (int)yflipped, clr); + } + } + #ifndef WLED_DISABLE_MODE_BLEND + SEGMENT.modeBlend(tempBlend); + #endif + } + else PSPRINTLN("skip xfer"); } @@ -965,7 +1034,7 @@ void ParticleSystem2D::updateSystem(void) { setMatrixSize(cols, rows); updatePSpointers(advPartProps != NULL, advPartSize != NULL); // update pointers to PS data, also updates availableParticles setUsedParticles(usedpercentage); // update used particles based on percentage TODO: this does not need to be called for each frame, it only changes during transitions. can optimize? - if (partMemList.size() == 1 && !SEGMENT.isInTransition()) // if number of vector elements is one, this is the only system !!!TODO: only set to multi if it is an FX transition. -> why? whats bad about it? + if (partMemList.size() == 1) // if number of vector elements is one, this is the only system !!!TODO: does this need more special case handling? { PSPRINTLN("rendersolo"); if(renderSolo < 2) renderSolo++; // increment: there is one transition frame when enabling render solo where local buffer is still blank and cant be used for blurring @@ -1062,7 +1131,6 @@ void blur2D(CRGB *colorbuffer, uint32_t xsize, uint32_t ysize, uint32_t xblur, u } } - //non class functions to use for initialization uint32_t calculateNumberOfParticles2D(uint32_t pixels, bool isadvanced, bool sizecontrol) { #ifdef ESP8266 @@ -1108,7 +1176,7 @@ bool allocateParticleSystemMemory2D(uint32_t numparticles, uint32_t numsources, uint32_t requiredmemory = sizeof(ParticleSystem2D); uint32_t availableparticles; // dummy variable uint32_t usedparticles = 0; // dummy variable - if((getUpdatedParticlePointer(numparticles, sizeof(PSparticle), availableparticles, usedparticles, 0, 0)) == nullptr) // allocate memory for particles + if((getUpdatedParticlePointer(numparticles, sizeof(PSparticle), availableparticles, usedparticles, 0, SEGMENT.mode)) == nullptr) // allocate memory for particles return false; // not enough memory, function ensures a minimum of numparticles are available // functions above make sure these are a multiple of 4 bytes (to avoid alignment issues) @@ -1486,7 +1554,7 @@ void ParticleSystem1D::ParticleSys_render() { renderParticle(i, brightness, baseRGB, wrap); } // transfer local buffer back to segment (if available) - transferBuffer(maxXpixel + 1, 0, effectID); + transferBuffer(maxXpixel + 1, 0); } @@ -1943,10 +2011,11 @@ ist also in transition und es ist der neue effekt und der watchdog ist auf null if(SEGMENT.currentMode() != SEGMENT.mode) { // if true, this is the new particle effect */ + // handle particle pointer, creates/extends buffer if needed and handles transition handover // function is called in PS setup and updatepointer function void* getUpdatedParticlePointer(const uint32_t requestedParticles, size_t structSize, uint32_t &availableToPS, uint32_t &usedbyPS, const uint8_t percentused, const uint8_t effectID) { // TODO: usedbyPS and percentused are currently unused, can remove if not required for transition - partMem *pmem = getPartMem(); + pmem = getPartMem(); PSPRINT(" getParticlePointer "); void* buffer = nullptr; if (pmem) { // segment has a buffer @@ -1968,11 +2037,9 @@ void* getUpdatedParticlePointer(const uint32_t requestedParticles, size_t struct } // if buffer was not extended, the old, smaller buffer is used } } - /* - if (pmem->watchdog == 1) { // if a PS already exists during particle request, it kicked the watchdog in last frame, servicePSmem() adds 1 afterwards - PSPRINT(" PS is in transition "); - pmem->inTransition = true; // TODO: inTransition is only used for buffer xfer, is there a better way to do this? or is it required somewhere else? - //TODO: sizeOfParticle must be the same for both systems, otherwise the buffer cannot be transferred. or: it can be transferred but memory must be cleared. + if (pmem->watchdog == 1) { // if a PS already exists during particle request, it kicked the watchdog in last frame, servicePSmem() adds 1 afterwards -> PS to PS transition + PSPRINTLN("********** PS is in transition, new FX:" + String(effectID)); + pmem->inTransition = effectID; // save the ID of the new effect (required to determine blur amount in rendering function, it will not work without this so dont optimize) //TODO2: handle memory initialization properly. if not in transition, particles must be initialized to TTL=0 and sat=255 //TODO3: add palettes. man kann beim PS init die palette so kopieren: loadPalette( } @@ -1980,7 +2047,6 @@ void* getUpdatedParticlePointer(const uint32_t requestedParticles, size_t struct // availableToPS = requestedParticles; // all particles are available todo: THIS IS TBD, probably ok to feed particles slowly // pmem->inTransition = false; // no transition TODO: does this need to be set here? this function is called again in updatePS, can deactivate it then, right? } - */ availableToPS = 2; // only give 2 particles to a new PS so old particles keep their settings and are not reeinitialized PSPRINT(" available to NEW PS: "); @@ -2010,91 +2076,120 @@ void* getUpdatedParticlePointer(const uint32_t requestedParticles, size_t struct free(buffer); return nullptr; } - //availableToPS = pmem->numParticles; - availableToPS = 2; // TODO: need to decide if a new system gets all particles or only a few - return pmem->particleMemPointer; // directly return the buffer on init call + // todo: need to initialize the first particles to default values? or just return 0 particles and let the transition below initialize? + availableToPS = 2; + return buffer; // directly return the buffer on init call } - //PSPRINT("particle buffer address: 0x"); - //PSPRINTLN((uintptr_t)buffer, HEX); + #ifdef WLED_DEBUG_PS + PSPRINT("particle buffer address: 0x"); + Serial.println((uintptr_t)buffer, HEX); + #endif - // now we have a valid buffer, check if we are in transition - if (SEGMENT.isInTransition()) { + // now we have a valid buffer, check if we are in transition (pmem->inTransition is still true for one frame after transition is finished to allow for transfer of remaining particles) + if (SEGMENT.isInTransition() || pmem->inTransition) { bool effectchanged = (SEGMENT.currentMode() != SEGMENT.mode); // FX changed, transition the particle buffer - - if (effectchanged) { + if (effectchanged || pmem->inTransition) { // transfer particles to the new system, starting from the end of the buffer (old one loses particles at the end, new one gets pointer from near the end) TODO: if new available > old avialable (system increased) need to not update numparticles until enough are transferred or FX can jump in particle count PSPRINT(" FX changed "); PSPRINT(" this mode: " + String(effectID)); - PSPRINT("/ old mode: " + String(SEGMENT.currentMode())); - PSPRINTLN(" new mode: " + String(SEGMENT.mode)); + PSPRINT("/ oldmode: " + String(SEGMENT.currentMode())); + PSPRINTLN(" newmode: " + String(SEGMENT.mode)); uint32_t progress = SEGMENT.progress(); // transition progress uint32_t newAvailable = 0; - uint32_t totransfer = 0; // number of particles to transfer - uint32_t transferstartidx = 0; // indes of the first particle to transfer - if (SEGMENT.mode == effectID) { // mode returns the new effect ID -> function was called from new FX + if (SEGMENT.mode == effectID) { // new effect ID -> function was called from new FX PSPRINT(" new FX"); PSPRINT(" progress: " + String(progress)); - newAvailable = (pmem->numParticles * progress) >> 16; // update total particles available to this PS + newAvailable = (pmem->numParticles * (progress + 1)) >> 16; // update total particles available to this PS if(newAvailable < 2) newAvailable = 2; // always give a minimum amount (this can lead to overlap, currently not a problem but some new FX may not like it) - //TODO: maybe reduce percentage on first call? SEGMENT.call==0? - //PSPRINTLN("trans: available particles: " + String(availableToPS)); - // note: no need to change buffer pointer, new segment gets the start of the buffer - transferstartidx = availableToPS; // start after already transferred particles - totransfer = newAvailable - availableToPS; // number of particles to transfer increases with progress + //uint32_t brforigin = (uintptr_t)buffer; // save old buffer pointer for !!!!DEBUG + buffer = (void*)((uint8_t*)buffer + (pmem->numParticles - newAvailable) * structSize); // new effect gets the end of the buffer + //uint32_t newbrfaddr = (uintptr_t)buffer; + #ifdef WLED_DEBUG_PS + //PSPRINT(" new buffer startaddress: 0x"); + //Serial.println((uintptr_t)buffer, HEX); + //PSPRINT("new bfrstart in particles "); + //Serial.println((newbrfaddr-brforigin)/structSize); + PSPRINT(" particle start: " + String(pmem->numParticles - newAvailable)); + #endif + uint32_t transferstartidx = 0; // start at beginning of new buffer pointer + uint32_t totransfer = newAvailable - availableToPS; // number of particles to transfer in this transition update + //TODO: maybe memcopy the buffer? if usedparticle number is small, end of the buffer holds alive but unused particles... copy would erase old particles though. need to think about a good way to do it. + // initialize newly transferred particles note: to have PS interact during transition, this must be changed. could initialize TTL and perpetual only TODO: need to be more clever: need to clear buffer if 2D->1D or 1D->2D transition or settings are nonsensical. + PSPRINT(" totransfer: " + String(totransfer)); + if(totransfer <= newAvailable) { // overflow check TODO: why do overflows happen? does it still happen with the new calculation? -> not in normal transfer, need to check quick transfer changes -> seems ok TODO: can this be removed? + if(structSize == sizeof(PSparticle)) { // 2D particle + PSparticle *particles = (PSparticle*)buffer; + for (uint32_t i = transferstartidx; i < transferstartidx + totransfer; i++) { + particles[i].perpetual = false; // particle ages + if(particles[i].outofbounds) particles[i].ttl = 0; // kill out of bounds + else if(particles[i].ttl > 200) particles[i].ttl = 200; // reduce TTL so it will die soon + //else if(particles[i].ttl) particles[i].ttl += 100; // !!! debug + particles[i].sat = 255; // full saturation + particles[i].collide = true; // enable collisions (in case new FX uses them) + } + } + else { // 1D particle system + PSparticle1D *particles = (PSparticle1D*)buffer; + for (uint32_t i = transferstartidx; i < transferstartidx + totransfer; i++) { + particles[i].perpetual = false; // particle ages + if(particles[i].outofbounds) particles[i].ttl = 0; // kill out of bounds + else if(particles[i].ttl > 50) particles[i].ttl = 50; // reduce TTL so it will die soon + } + } + } + else { + Serial.println(" overflow in transfer ****"); + } } - else if (SEGMENT.currentMode() == effectID) { // seg.currentMode() is the old effect ID during transitions, this was called from old FX + else { // if (SEGMENT.currentMode() == effectID) { // seg.currentMode() is the old effect ID during transitions, this was called from old FX + PSPRINT(" old FX"); + PSPRINT(" progress: " + String(progress)); SEGMENT.setCurrentPalette(true); // load the old palette into segment - PSPRINT(" progress: " + String(progress)); progress = 0xFFFFU - progress; // inverted transition progress PSPRINT(" inv.prog: " + String(progress)); newAvailable = (pmem->numParticles * progress) >> 16; PSPRINT(" newAvailable: " + String(newAvailable)); PSPRINT(" oldAvailable: " + String(availableToPS)); - if(newAvailable < 2) newAvailable = 2; // always give a minimum amount - PSPRINT(" old FX"); + if(newAvailable > availableToPS) newAvailable = availableToPS; // do not increase available particles (if memory was extended) //PSPRINTLN("trans: available particles: " + String(availableToPS)); - buffer = (void*)((uint8_t*)buffer + (pmem->numParticles - newAvailable) * structSize); // old effect gets the end of the buffer // note: startindex is start of buffer (transferred from end to start, so end particles are the old ones) - totransfer = availableToPS - newAvailable; // number of particles to transfer decreases with progress - } - PSPRINT("a"); - // initialize newly transferred particles note: to have PS interact during transition, this must be changed. could initialize TTL and perpetual only TODO: need to be more clever: need to clear buffer if 2D->1D or 1D->2D transition or settings are nonsensical. - - PSPRINT(" transferstartidx: " + String(transferstartidx)); - PSPRINT(" totransfer: " + String(totransfer)); - if(totransfer <= availableToPS) { // overflow check TODO: why do overflows happen? - if(structSize == sizeof(PSparticle)) { // 2D particle - PSparticle *particles = (PSparticle*)buffer; - for (uint32_t i = transferstartidx; i < transferstartidx + totransfer; i++) { - particles[i].perpetual = false; // particle ages - if(particles[i].ttl > 50) particles[i].ttl = 50; // reduce TTL so it will die soon - } - } - else { // 1D particle system - PSparticle1D *particles = (PSparticle1D*)buffer; - for (uint32_t i = transferstartidx; i < transferstartidx + totransfer; i++) { - particles[i].perpetual = false; // particle ages - if(particles[i].ttl > 50) particles[i].ttl = 50; // reduce TTL so it will die soon - } - } + } + availableToPS = newAvailable; + PSPRINT(" final available to PS: " + String(availableToPS)); // TODO: if this is correct, the first if statement can be removed and availabletoPS updated here or: this part of the function can be optimized still } + /* else { // same effect transition PSPRINT(" same FX "); availableToPS = pmem->numParticles; // no transition, full buffer available // pmem->inTransition = false; - } - - } else { // no transition, full buffer available + }*/ + if(!SEGMENT.isInTransition()) { // transition ended, cleanup + pmem->inTransition = false; + PSPRINTLN(" ** "); + PSPRINTLN("****** TRANSITION ENDED ******"); + // Transfer the last buffer state in PS before rendering + transferflag = true; + } + } else { // no PS transition, full buffer available TODO: need to check if previously not full buffer was available and transfer the rest of the particles in a clever, code saving way, maybe make a transfer function to handle this availableToPS = pmem->numParticles; // no transition, full buffer available PSPRINTLN(" no trans, no of partcls: " + String(availableToPS)); - //pmem->inTransition = false; - } - PSPRINTLN("END getParticlePointer "); + pmem->inTransition = false; + /* + PSparticle *particles = (PSparticle*)buffer; + Serial.println("**"); + + for (uint32_t i = 0; i < pmem->numParticles; i++) { + Serial.print(particles[i].ttl) ; + Serial.print(" "); + } + Serial.println(" ");*/ + } + PSPRINTLN(" END getPartPointer "); return buffer; -} +} //TODO: final few particles are not tranferred properly if FX uses perpetual particles, need to fix this // function to update the framebuffer and renderbuffer void updateRenderingBuffer(CRGB* buffer, uint32_t requiredpixels, bool isFramebuffer) { @@ -2107,7 +2202,6 @@ void updateRenderingBuffer(CRGB* buffer, uint32_t requiredpixels, bool isFramebu if(isFramebuffer) { framebuffer = buffer; frameBufferSize = requiredpixels; - memset(framebuffer, 0, frameBufferSize * sizeof(CRGB)); } else { renderbuffer = buffer; renderBufferSize = requiredpixels; @@ -2175,9 +2269,43 @@ void servicePSmem(uint8_t idx) { } } + + +// apply globalBlur to framebuffer, clear buffer if not blurring (call this after transferring the buffer to the segment) +static void applyBlurOrClear2D(uint32_t width, uint32_t height, uint8_t fxID) { + if (framebuffer) { + if (globalBlur > 0) { // using SEGMENT.fadeToBlackBy is much slower, this approximately doubles the speed of fade calculation + PSPRINTLN(" blurring: " + String(globalBlur)); + Serial.println(" blur: " + String(globalBlur)); + uint32_t yflipped; + for (uint32_t y = 0; y < height; y++) { + int index = y * width; // current row index for 1D buffer + if(renderSolo < 2 && pmem->inTransition != fxID) { // there are/were other systems using the buffer and this is not the old FX in a transition: read data from segment + yflipped = height - y - 1; + Serial.print(" cpy "); + for (uint32_t x = 0; x < width; x++) { + framebuffer[index++] = SEGMENT.getPixelColorXY(x, yflipped); //copy to local buffer + } + index = y * width; // reset index + } + for (uint32_t x = 0; x < width; x++) { + fast_color_scale(framebuffer[index], globalBlur); + index++; + } + } + globalBlur = 0; // reset for next frame (updated by PS) + } + else { // no blurring, clear the buffer + Serial.println(" clear "); + memset(framebuffer, 0, frameBufferSize * sizeof(CRGB)); + } + } +} + // transfer the frame buffer to the segment and handle transitional rendering (both FX render to the same buffer so they mix) -void transferBuffer(uint32_t width, uint32_t height, uint8_t effectID) { +void transferBuffer(uint32_t width, uint32_t height) { PSPRINT(" xfer buf "); + Serial.print(" xfer "); if(!framebuffer) return; // no buffer, nothing to transfer /* if(SEGMENT.isInTransition()) { @@ -2193,32 +2321,49 @@ void transferBuffer(uint32_t width, uint32_t height, uint8_t effectID) { }*/ if(height) { // is 2D, 1D passes height = 0 int32_t yflipped; - height--; // reduce by one for easier flip calculation below (instead of counting on compiler optimization) PSPRINT("xfer 2D"); + #ifndef WLED_DISABLE_MODE_BLEND + bool tempBlend = SEGMENT.getmodeBlend(); + if (pmem->inTransition) + SEGMENT.modeBlend(false); // temporarily disable FX blending in PS to PS transition (using local buffer to do PS blending) + #endif for (uint32_t y = 0; y <= height; y++) { - yflipped = height - y; + yflipped = height - y - 1; int index = y * width; // current row index for 1D buffer for (uint32_t x = 0; x < width; x++) { - SEGMENT.setPixelColorXY((int)x, (int)yflipped, framebuffer[index++]); + CRGB *c = &framebuffer[index++]; + uint32_t clr = RGBW32(c->r,c->g,c->b,0); // convert to 32bit color + //if(clr > 0) // not black TODO: not transferring black is faster and enables overlay, but requries proper handling of buffer clearing, which is quite complex and probably needs a change to SEGMENT handling. + SEGMENT.setPixelColorXY((int)x, (int)yflipped, clr); } } - } else { + #ifndef WLED_DISABLE_MODE_BLEND + SEGMENT.modeBlend(tempBlend); + #endif + //applyBlurOrClear2D(width, height, fxID); // apply blur (or clear buffer) after transferring to be ready for next frame + } else { // 1D system for (uint32_t x = 0; x < width; x++) { - SEGMENT.setPixelColor(x, framebuffer[x]); + CRGB *c = &framebuffer[x]; + uint32_t color = RGBW32(c->r,c->g,c->b,0); + //if(color > 0) // not black + SEGMENT.setPixelColor(x, color); } } + /* if(!renderSolo) { // there are other segments with particle systems, clear the buffer (PS takes over if rendersolo is true) - memset(framebuffer, 0, frameBufferSize * sizeof(CRGB)); // clear the buffer TODO: also need to clear during transitions? - PSPRINT(" buffer cleared "); - } - + //memset(framebuffer, 0, frameBufferSize * sizeof(CRGB)); // clear the buffer TODO: !!! add this back in + PSPRINTLN(" buffer cleared "); + }*/ +/* #ifdef WLED_DEBUG_PS PSPRINT(" done. framebfr addr: 0x"); Serial.println((uintptr_t)framebuffer, HEX); #endif - +*/ } + + /* //memory fragmentation check: PSPRINT("heap: "); diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index fdda50a225..645554dc44 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -16,7 +16,7 @@ #define PS_P_MAXSPEED 120 // maximum speed a particle can have (vx/vy is int8) #define MAX_MEMIDLE 200 // max idle time (in frames) before memory is deallocated (if deallocated during an effect, it will crash!) note: setting this to a high 8bit value prevents fragmentation -#define WLED_DEBUG_PS +//#define WLED_DEBUG_PS #ifdef WLED_DEBUG_PS #define PSPRINT(x) Serial.print(x) @@ -32,7 +32,7 @@ struct partMem { uint32_t numParticles; // number of particles that fit in memory note: could be a uint16_t but padding will increase the struct size so 12 bytes anyway uint8_t sizeOfParticle; // size of the particle struct in this buffer uint8_t id; // ID of segment this memory belongs to - uint8_t inTransition; // to track transitions TODO: probably not be needed anymore + uint8_t inTransition; // to track transitions uint8_t watchdog; // counter to handle deallocation }; @@ -44,7 +44,7 @@ void* getUpdatedParticlePointer(const uint32_t requestedParticles, size_t struct //extern uint16_t renderBufferSize; // size in pixels, if allcoated by a 1D system it needs to be updated for 2D partMem* getPartMem(void); // returns pointer to memory struct for current segment or nullptr void updateRenderingBuffer(CRGB* buffer, uint32_t requiredsize, bool isFramebuffer); // allocate CRGB rendering buffer, update size if needed -void transferBuffer(uint32_t width, uint32_t height, uint8_t effectID); // transfer the buffer to the segment +void transferBuffer(uint32_t width, uint32_t height); // transfer the buffer to the segment //TODO: add 1D version void servicePSmem(uint8_t idx); // increments watchdog, frees memory if idle too long @@ -55,6 +55,8 @@ inline void updateUsedParticles(const uint32_t allocated, const uint32_t availab #endif +//TODO: updated ESP32_MAXPARTICLES, can have more with new mem manager, revisit the calculation +// TODO: maybe update PS_P_MINSURFACEHARDNESS for 2D? its a bit too sticky already at hardness 100 #ifndef WLED_DISABLE_PARTICLESYSTEM2D // memory allocation #define ESP8266_MAXPARTICLES 180 // enough for one 16x16 segment with transitions @@ -89,15 +91,15 @@ typedef union { } PSsettings2D; //struct for a single particle -typedef struct { // 10 bytes +typedef struct { // 11 bytes 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 uint8_t sat; // particle color saturation - // two byte bit field: - uint16_t ttl : 12; // time to live, 12 bit or 4095 max (which is 50s at 80FPS) + //uint16_t ttl : 12; // time to live, 12 bit or 4095 max (which is 50s at 80FPS) + uint16_t ttl; // time to live bool outofbounds : 1; // out of bounds flag, set to true if particle is outside of display area bool collide : 1; // if set, particle takes part in collisions bool perpetual : 1; // if set, particle does not age (TTL is not decremented in move function, it still dies from killoutofbounds) From 75e49a7ad6e3f1742af3563e52cecec318204c3c Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sun, 24 Nov 2024 10:09:22 +0100 Subject: [PATCH 141/219] added transitions for big-size rendering - transitions now work with FX that use global large size rendering. this is done by handling the buffer correctly. to avoid creating a second buffer, it is transferred back and forth to the segment. this is a bit slow but a compromise solution. - multiple segment handling is still untested --- wled00/FX.h | 2 +- wled00/FXparticleSystem.cpp | 114 +++++++++++++++++++++--------------- wled00/wled.cpp | 2 - 3 files changed, 69 insertions(+), 49 deletions(-) diff --git a/wled00/FX.h b/wled00/FX.h index 4e4048ad1f..5c72ff33f1 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -549,7 +549,7 @@ typedef struct Segment { inline static void addUsedSegmentData(int len) { _usedSegmentData += len; } #ifndef WLED_DISABLE_MODE_BLEND inline static void modeBlend(bool blend) { _modeBlend = blend; } - inline static bool getmodeBlend(void) { return _modeBlend; } + inline static bool getmodeBlend(void) { return _modeBlend; } #endif static void handleRandomPalette(); inline static const CRGBPalette16 &getCurrentPalette() { return Segment::_currentPalette; } diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index 5f66b6493f..3cc4238939 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -560,45 +560,37 @@ void ParticleSystem2D::pointAttractor(uint16_t particleindex, PSparticle *attrac void ParticleSystem2D::ParticleSys_render(bool firemode, uint32_t fireintensity) { CRGB baseRGB; uint32_t brightness; // particle brightness, fades if dying -/* - if(transferflag) { - transferflag = false; - transferBuffer((maxXpixel + 1), (maxYpixel + 1), effectID); - }*/ - Serial.print(" render "); - - + static bool useAdditiveTransfer = false; // use add instead of set for buffer transferring + if(!pmem->inTransition) useAdditiveTransfer = false; // additive rendering is only used in transitions + // handle blurring and framebuffer update if (framebuffer) { // update global blur (used for blur transitions) - if(motionBlur > 0) { - uint32_t bluramount = motionBlur; - if(pmem->inTransition) { // FX transition - uint32_t progress = SEGMENT.progress(); // transition progress - if (pmem->inTransition != effectID) // inTransition is set to new effectID so this is true if we are to old FX - progress = 0xFFFFU - progress; // inverted transition progress for old FX - bluramount = (bluramount * progress) >> 16; // fade the blur during transitions + int32_t bluramount = motionBlur; + if(pmem->inTransition) { // FX transition, fade blur amount or skip setting new blur if particlesize is used + if(pmem->inTransition == effectID) { // FX transition and new FX: fade blur amount + bluramount = globalBlur + (((bluramount - globalBlur) * (int)SEGMENT.progress()) >> 16); // fade from old blur to new blur during transitions } - if(globalBlur < bluramount) globalBlur = bluramount; // keep track of highest blur amount TODO: transition could be made better, i.e. take the diff from old to new and transition that? could not clear the global blur and derive it from that plus new FX blur } + globalBlur = bluramount; /* Note on blurring / clearing when rendersolo is active, blurring can be done on the buffer directly, if this is a transition, skip blurring if this is the old FX (i.e. blur before new FX is rendered) if rendersolo is not set, the buffer must be either cleared (if no blurring and no pmem->inTransition) or read from segment (if blur is active AND this is not the old FX i.e. pmem->inTransition != fxID) if multiple segments are used, rendersolor is false, if then transitioning from classic to PS fx, blurring is applied. if transitioning away from PS, blurring is skipped (as this is the old FX) - + // first check if rendersolo is active, if yes, check if this is a transition // if rendersolo is active and not a transition, blur the buffer or clear it // if this is a transition, only clear or blur if this is the new FX (which is rendered first) // if this is the new FX in a transition and rendersolo is not active, read the buffer from the segment befor applying blur (clear otherwise) - - TODO: blurring during transitions is now completely disabled, there is a bug in the below code that skips blurring and probably does clearing instead. */ + // handle buffer blurring or clearing bool bufferNeedsUpdate = (!pmem->inTransition || pmem->inTransition == effectID); // not a transition; or new FX: update buffer (blur, or clear) + if(bufferNeedsUpdate) { if (globalBlur > 0) { // blurring active: if not a transition or is newFX, read data from segment before blurring (old FX can render to it afterwards) - Serial.println(" blurring: " + globalBlur); + //Serial.print(" blurring: " + String(globalBlur)); for (uint32_t y = 0; y <= maxYpixel; y++) { int index = y * (maxXpixel + 1); for (uint32_t x = 0; x <= maxXpixel; x++) { @@ -610,12 +602,40 @@ void ParticleSystem2D::ParticleSys_render(bool firemode, uint32_t fireintensity) index++; } } - globalBlur = 0; // reset global blur for next frame } - else if (bufferNeedsUpdate) { // no blurring and not a transition or is new FX + else { // no blurring: clear buffer memset(framebuffer, 0, frameBufferSize * sizeof(CRGB)); } } + if(particlesize > 0 && pmem->inTransition) { // if particle size is used by FX we need a clean buffer + // new FX already rendered to the buffer, transfer it to segment and clear it TODO: make buffer transfer a function again + if(bufferNeedsUpdate && !globalBlur) { // transfer only if buffer was not cleared above (happens if this is the new FX and other FX does not use blurring) + useAdditiveTransfer = false; // no blurring and big size particle FX is the new FX, can just render normally + } + else { + #ifndef WLED_DISABLE_MODE_BLEND + bool tempBlend = SEGMENT.getmodeBlend(); + SEGMENT.modeBlend(false); // temporarily disable FX blending in PS to PS transition (local buffer is used to do PS blending) + #endif + int yflipped; + for (uint32_t y = 0; y <= maxYpixel; y++) { + yflipped = maxYpixel - y; + int index = y * (maxXpixel + 1); // current row index for 1D buffer + for (uint32_t x = 0; x <= maxXpixel; x++) { + //if(globalBlur) fast_color_scale(framebuffer[index], globalBlur); // apply motion blurring + CRGB *c = &framebuffer[index++]; + uint32_t clr = RGBW32(c->r,c->g,c->b,0); // convert to 32bit color + //if(clr > 0) // not black TODO: not transferring black is faster and enables overlay, but requries proper handling of buffer clearing, which is quite complex and probably needs a change to SEGMENT handling. + SEGMENT.setPixelColorXY((int)x, (int)yflipped, clr); + } + } + #ifndef WLED_DISABLE_MODE_BLEND + SEGMENT.modeBlend(tempBlend); + #endif + memset(framebuffer, 0, frameBufferSize * sizeof(CRGB)); // clear the buffer after transfer + useAdditiveTransfer = true; // add buffer content to segment after rendering + } + } } else { // no local buffer available, apply blur to segment TODO: this could also be done in 2D blur function but must be called by first PS rendering to it, may be complex to find out which on is the first (in overlay, in transitions its easy) if (motionBlur > 0) // TODO2: blurring during transitions (and maybe also overlay) could be done in the mem-manager, it is called in update pointers and knows which PS is the first one to render. @@ -649,18 +669,6 @@ void ParticleSystem2D::ParticleSys_render(bool firemode, uint32_t fireintensity) } if (particlesize > 0) { - // TODO: if global particle size is used, need to transfer the buffer NOW, clear it, render to it, then add the 'background' again from the segment. - /* - for (uint32_t y = 0; y <= maxYpixel; y++) { - uint32_t yflipped = maxYpixel - y; - int index = y * (maxXpixel + 1); // current row index for 1D buffer - for (uint32_t x = 0; x <= maxXpixel; x++) { - CRGB sourcecolor = SEGMENT.getPixelColorXY(x, yflipped); - fast_color_add(sourcecolor, framebuffer[index]); - index++; - } - } - */ uint32_t passes = particlesize / 64 + 1; // number of blur passes, four passes max uint32_t bluramount = particlesize; uint32_t bitshift = 0; @@ -683,7 +691,7 @@ void ParticleSystem2D::ParticleSys_render(bool firemode, uint32_t fireintensity) #ifndef WLED_DISABLE_MODE_BLEND bool tempBlend = SEGMENT.getmodeBlend(); if (pmem->inTransition) - SEGMENT.modeBlend(false); // temporarily disable FX blending in PS to PS transition (using local buffer to do PS blending) + SEGMENT.modeBlend(false); // temporarily disable FX blending in PS to PS transition (local buffer is used to do PS blending) #endif int yflipped; for (uint32_t y = 0; y <= maxYpixel; y++) { @@ -692,8 +700,22 @@ void ParticleSystem2D::ParticleSys_render(bool firemode, uint32_t fireintensity) for (uint32_t x = 0; x <= maxXpixel; x++) { CRGB *c = &framebuffer[index++]; uint32_t clr = RGBW32(c->r,c->g,c->b,0); // convert to 32bit color + if(useAdditiveTransfer) { + uint32_t segmentcolor = SEGMENT.getPixelColorXY((int)x, (int)yflipped); + CRGB segmentRGB = CRGB(segmentcolor); + if(clr == 0) // frame buffer is black, just update the framebuffer TODO: could check if segmentcolor is also black and skip + *c = segmentRGB; + else { // not black + if(segmentcolor) { + fast_color_add(*c, segmentRGB); // add segment color back to buffer if not black TODO: since both are 32bit, this could be made faster using the new improved wled 32bit adding (see speed improvements PR) + clr = RGBW32(c->r,c->g,c->b,0); // convert to 32bit color (again) + } + SEGMENT.setPixelColorXY((int)x, (int)yflipped, clr); // save back to segment after adding local buffer + } + } //if(clr > 0) // not black TODO: not transferring black is faster and enables overlay, but requries proper handling of buffer clearing, which is quite complex and probably needs a change to SEGMENT handling. - SEGMENT.setPixelColorXY((int)x, (int)yflipped, clr); + else + SEGMENT.setPixelColorXY((int)x, (int)yflipped, clr); } } #ifndef WLED_DISABLE_MODE_BLEND @@ -701,7 +723,6 @@ void ParticleSystem2D::ParticleSys_render(bool firemode, uint32_t fireintensity) #endif } else PSPRINTLN("skip xfer"); - } // calculate pixel positions and brightness distribution and render the particle to local buffer or global buffer @@ -801,7 +822,7 @@ void ParticleSystem2D::renderParticle(const uint32_t particleindex, const uint32 if (framebuffer) fast_color_add(framebuffer[xfb + yfb * (maxXpixel + 1)], renderbuffer[xrb + yrb * 10]); else - SEGMENT.addPixelColorXY(xfb, maxYpixel - yfb, renderbuffer[xrb + yrb * 10]); + SEGMENT.addPixelColorXY(xfb, maxYpixel - yfb, renderbuffer[xrb + yrb * 10],true); } } } else { // standard rendering @@ -844,7 +865,7 @@ void ParticleSystem2D::renderParticle(const uint32_t particleindex, const uint32 else { for (uint32_t i = 0; i < 4; i++) { if (pixelvalid[i]) - SEGMENT.addPixelColorXY(pixco[i][0], maxYpixel - pixco[i][1], color.scale8((uint8_t)pxlbrightness[i])); + SEGMENT.addPixelColorXY(pixco[i][0], maxYpixel - pixco[i][1], color.scale8((uint8_t)pxlbrightness[i]), true); } } } @@ -1032,7 +1053,7 @@ void ParticleSystem2D::updateSystem(void) { uint32_t cols = SEGMENT.virtualWidth(); // update matrix size uint32_t rows = SEGMENT.virtualHeight(); setMatrixSize(cols, rows); - updatePSpointers(advPartProps != NULL, advPartSize != NULL); // update pointers to PS data, also updates availableParticles + updatePSpointers(advPartProps != nullptr, advPartSize != nullptr); // update pointers to PS data, also updates availableParticles setUsedParticles(usedpercentage); // update used particles based on percentage TODO: this does not need to be called for each frame, it only changes during transitions. can optimize? if (partMemList.size() == 1) // if number of vector elements is one, this is the only system !!!TODO: does this need more special case handling? { @@ -1191,13 +1212,14 @@ bool allocateParticleSystemMemory2D(uint32_t numparticles, uint32_t numsources, // initialize Particle System, allocate additional bytes if needed (pointer to those bytes can be read from particle system class: PSdataEnd) bool initParticleSystem2D(ParticleSystem2D *&PartSys, uint32_t requestedsources, uint32_t additionalbytes, bool advanced, bool sizecontrol) { - PSPRINTLN("PS 2D init"); + PSPRINT("PS 2D init "); if(!strip.isMatrix) return false; // only for 2D uint32_t cols = SEGMENT.virtualWidth(); uint32_t rows = SEGMENT.virtualHeight(); uint32_t pixels = cols * rows; uint32_t numparticles = calculateNumberOfParticles2D(pixels, advanced, sizecontrol); - PSPRINTLN("request numparticles:" + String(numparticles)); + PSPRINT(" segmentsize:" + String(cols) + " " + String(rows)); + PSPRINT(" request numparticles:" + String(numparticles)); uint32_t numsources = calculateNumberOfSources2D(pixels, requestedsources); // allocate rendering buffer (if this fails, it will render to segment buffer directly) updateRenderingBuffer(framebuffer, pixels, true); @@ -1287,7 +1309,7 @@ void ParticleSystem1D::update(void) { uint32_t bg_color = SEGCOLOR(1); //background color, set to black to overlay if (bg_color > 0) { //if not black for(int32_t i = 0; i < maxXpixel + 1; i++) { - SEGMENT.addPixelColor(i,bg_color); // TODO: can this be done in rendering function using local buffer? + SEGMENT.addPixelColor(i, bg_color, true); // TODO: can this be done in rendering function using local buffer? } } } @@ -1570,7 +1592,7 @@ void ParticleSystem1D::renderParticle(const uint32_t particleindex, const uint32 if (framebuffer) fast_color_add(framebuffer[x], color, brightness); else - SEGMENT.addPixelColor(x, color.scale8((uint8_t)brightness)); + SEGMENT.addPixelColor(x, color.scale8((uint8_t)brightness), true); } } else { //render larger particles @@ -1658,7 +1680,7 @@ void ParticleSystem1D::renderParticle(const uint32_t particleindex, const uint32 if (framebuffer) fast_color_add(framebuffer[pixco[i]], color, pxlbrightness[i]); else - SEGMENT.addPixelColor(pixco[i], color.scale8((uint8_t)pxlbrightness[i])); + SEGMENT.addPixelColor(pixco[i], color.scale8((uint8_t)pxlbrightness[i]), true); } } } @@ -2346,7 +2368,7 @@ void transferBuffer(uint32_t width, uint32_t height) { CRGB *c = &framebuffer[x]; uint32_t color = RGBW32(c->r,c->g,c->b,0); //if(color > 0) // not black - SEGMENT.setPixelColor(x, color); + SEGMENT.setPixelColor((int)x, color); } } /* diff --git a/wled00/wled.cpp b/wled00/wled.cpp index 9855ca1e8c..13d43218af 100644 --- a/wled00/wled.cpp +++ b/wled00/wled.cpp @@ -360,8 +360,6 @@ void WLED::setup() #if !defined(WLED_DEBUG) && defined(ARDUINO_ARCH_ESP32) && !defined(WLED_DEBUG_HOST) && ARDUINO_USB_CDC_ON_BOOT Serial.setDebugOutput(false); // switch off kernel messages when using USBCDC #endif - Serial.println("size of WS2812FX " + String(sizeof(WS2812FX))); - Serial.println("size of Segmen t" + String(sizeof(Segment))); DEBUG_PRINTLN(); DEBUG_PRINTF_P(PSTR("---WLED %s %u INIT---\n"), versionString, VERSION); DEBUG_PRINTLN(); From fe069ee87149d846194dc2a10757a897f51e8a7b Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Mon, 23 Dec 2024 08:21:44 +0100 Subject: [PATCH 142/219] speed improvement to fast_color_add, added preliminary buffer transfer function --- wled00/FXparticleSystem.cpp | 67 ++++++++++++++++++++++++------------- wled00/FXparticleSystem.h | 1 + 2 files changed, 45 insertions(+), 23 deletions(-) diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index 3cc4238939..241af85ff3 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -703,7 +703,7 @@ void ParticleSystem2D::ParticleSys_render(bool firemode, uint32_t fireintensity) if(useAdditiveTransfer) { uint32_t segmentcolor = SEGMENT.getPixelColorXY((int)x, (int)yflipped); CRGB segmentRGB = CRGB(segmentcolor); - if(clr == 0) // frame buffer is black, just update the framebuffer TODO: could check if segmentcolor is also black and skip + if(clr == 0) // frame buffer is black, just update the framebuffer *c = segmentRGB; else { // not black if(segmentcolor) { @@ -1067,6 +1067,37 @@ void ParticleSystem2D::updateSystem(void) { } } +void ParticleSystem2D::transferBuffer(bool additive) { + if (framebuffer) { + int yflipped; + for (uint32_t y = 0; y <= maxYpixel; y++) { + yflipped = maxYpixel - y; + int index = y * (maxXpixel + 1); // current row index for 1D buffer + for (uint32_t x = 0; x <= maxXpixel; x++) { + CRGB *c = &framebuffer[index++]; + uint32_t clr = RGBW32(c->r,c->g,c->b,0); // convert to 32bit color + if (additive) { + uint32_t segmentcolor = SEGMENT.getPixelColorXY((int)x, (int)yflipped); + CRGB segmentRGB = CRGB(segmentcolor); + if(clr == 0) // frame buffer is black, just update the framebuffer TODO: could check if segmentcolor is also black and skip + *c = segmentRGB; + else { // not black + if(segmentcolor) { + fast_color_add(*c, segmentRGB); // add segment color back to buffer if not black TODO: since both are 32bit, this could be made faster using the new improved wled 32bit adding (see speed improvements PR) + clr = RGBW32(c->r,c->g,c->b,0); // convert to 32bit color (again) + } + SEGMENT.setPixelColorXY((int)x, (int)yflipped, clr); // save back to segment after adding local buffer + } + } + //if(clr > 0) // not black TODO: not transferring black is faster and enables overlay, but requries proper handling of buffer clearing, which is quite complex and probably needs a change to SEGMENT handling. + else + SEGMENT.setPixelColorXY((int)x, (int)yflipped, clr); + } + } + } + else PSPRINTLN("skip xfer"); +} + // 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 @@ -1964,9 +1995,10 @@ static void fast_color_add(CRGB &c1, const CRGB &c2, const uint32_t scale) { c1.g = g; c1.b = b; } else { - c1.r = (r * 255) / max; // note: compile optimizes the divisions, no need to manually optimize - c1.g = (g * 255) / max; - c1.b = (b * 255) / max; + uint32_t scale = (255U << 16) / max; + c1.r = (r * scale) >> 16; + c1.g = (g * scale) >> 16; + c1.b = (b * scale) >> 16; } } @@ -2061,9 +2093,11 @@ void* getUpdatedParticlePointer(const uint32_t requestedParticles, size_t struct } if (pmem->watchdog == 1) { // if a PS already exists during particle request, it kicked the watchdog in last frame, servicePSmem() adds 1 afterwards -> PS to PS transition PSPRINTLN("********** PS is in transition, new FX:" + String(effectID)); - pmem->inTransition = effectID; // save the ID of the new effect (required to determine blur amount in rendering function, it will not work without this so dont optimize) - //TODO2: handle memory initialization properly. if not in transition, particles must be initialized to TTL=0 and sat=255 - //TODO3: add palettes. man kann beim PS init die palette so kopieren: loadPalette( + //Serial.print(" inTransition = " + String(pmem->inTransition)); + if(pmem->inTransition) // there is already a transition going on, multi transitions lead to weird behaviour (missin particles, missing transitions) + pmem->inTransition = 1; // set to an invalid FX number so transitions work (the calling "new FX" is recognized as the old FX) TODO: this is a dirty hack and needs fixing but there is a bug in segment handling, not copying the segment.data... + else + pmem->inTransition = effectID; // save the ID of the new effect (required to determine blur amount in rendering function, it will not work without this so dont optimize) } else { // no watchdog, this is a new PS // availableToPS = requestedParticles; // all particles are available todo: THIS IS TBD, probably ok to feed particles slowly @@ -2159,10 +2193,10 @@ void* getUpdatedParticlePointer(const uint32_t requestedParticles, size_t struct } } else { - Serial.println(" overflow in transfer ****"); + PSPRINTLN(" Particle transfer overflow! "); } } - else { // if (SEGMENT.currentMode() == effectID) { // seg.currentMode() is the old effect ID during transitions, this was called from old FX + else { // this was called from old FX PSPRINT(" old FX"); PSPRINT(" progress: " + String(progress)); SEGMENT.setCurrentPalette(true); // load the old palette into segment @@ -2199,19 +2233,10 @@ void* getUpdatedParticlePointer(const uint32_t requestedParticles, size_t struct availableToPS = pmem->numParticles; // no transition, full buffer available PSPRINTLN(" no trans, no of partcls: " + String(availableToPS)); pmem->inTransition = false; - /* - PSparticle *particles = (PSparticle*)buffer; - Serial.println("**"); - - for (uint32_t i = 0; i < pmem->numParticles; i++) { - Serial.print(particles[i].ttl) ; - Serial.print(" "); - } - Serial.println(" ");*/ } PSPRINTLN(" END getPartPointer "); return buffer; -} //TODO: final few particles are not tranferred properly if FX uses perpetual particles, need to fix this +} // function to update the framebuffer and renderbuffer void updateRenderingBuffer(CRGB* buffer, uint32_t requiredpixels, bool isFramebuffer) { @@ -2298,13 +2323,11 @@ static void applyBlurOrClear2D(uint32_t width, uint32_t height, uint8_t fxID) { if (framebuffer) { if (globalBlur > 0) { // using SEGMENT.fadeToBlackBy is much slower, this approximately doubles the speed of fade calculation PSPRINTLN(" blurring: " + String(globalBlur)); - Serial.println(" blur: " + String(globalBlur)); uint32_t yflipped; for (uint32_t y = 0; y < height; y++) { int index = y * width; // current row index for 1D buffer if(renderSolo < 2 && pmem->inTransition != fxID) { // there are/were other systems using the buffer and this is not the old FX in a transition: read data from segment yflipped = height - y - 1; - Serial.print(" cpy "); for (uint32_t x = 0; x < width; x++) { framebuffer[index++] = SEGMENT.getPixelColorXY(x, yflipped); //copy to local buffer } @@ -2318,7 +2341,6 @@ static void applyBlurOrClear2D(uint32_t width, uint32_t height, uint8_t fxID) { globalBlur = 0; // reset for next frame (updated by PS) } else { // no blurring, clear the buffer - Serial.println(" clear "); memset(framebuffer, 0, frameBufferSize * sizeof(CRGB)); } } @@ -2327,7 +2349,6 @@ static void applyBlurOrClear2D(uint32_t width, uint32_t height, uint8_t fxID) { // transfer the frame buffer to the segment and handle transitional rendering (both FX render to the same buffer so they mix) void transferBuffer(uint32_t width, uint32_t height) { PSPRINT(" xfer buf "); - Serial.print(" xfer "); if(!framebuffer) return; // no buffer, nothing to transfer /* if(SEGMENT.isInTransition()) { diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index 645554dc44..f4d94fc24d 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -205,6 +205,7 @@ class ParticleSystem2D { void collideParticles(PSparticle *particle1, PSparticle *particle2, int32_t dx, int32_t dy); void fireParticleupdate(); //utility functions + void transferBuffer(bool additive = false); // transfer the framebuffer to the segment void updatePSpointers(bool isadvanced, bool sizecontrol); // update the data pointers to current segment data space void updateSize(PSadvancedParticle *advprops, PSsizeControl *advsize); // advanced size control void getParticleXYsize(PSadvancedParticle *advprops, PSsizeControl *advsize, uint32_t &xsize, uint32_t &ysize); From 82e0fa2a2bfadadf8773910cee2d5683c570a22d Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Mon, 23 Dec 2024 09:47:16 +0100 Subject: [PATCH 143/219] merge fixes --- wled00/FX.cpp | 125 -------------------------------------------------- wled00/FX.h | 1 + 2 files changed, 1 insertion(+), 125 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 9d16a8a352..a1c3a11ce3 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -20,11 +20,6 @@ #include "FXparticleSystem.h" #endif -#if !defined(WLED_DISABLE_PARTICLESYSTEM2D) || !defined(WLED_DISABLE_PARTICLESYSTEM1D) -#include "FXparticleSystem.h" -#endif - - ////////////// // DEV INFO // ////////////// @@ -7778,126 +7773,6 @@ uint16_t mode_particlevortex(void) { PartSys->update(); //update all particles and render to frame return FRAMETIME; } -static const char _data_FX_MODE_2DWAVINGCELL[] PROGMEM = "Waving Cell@!,Blur,Amplitude 1,Amplitude 2,Amplitude 3,,Flow;;!;2;ix=0"; - -#ifndef WLED_DISABLE_PARTICLESYSTEM2D - -/* - * Particle System Vortex - * Particles sprayed from center with a rotating spray - * Uses palette for particle color - * by DedeHai (Damian Schneider) - */ -#define NUMBEROFSOURCES 8 -uint16_t mode_particlevortex(void) { - if (SEGLEN == 1) - return mode_static(); - ParticleSystem2D *PartSys = NULL; - uint32_t i, j; - - if (SEGMENT.call == 0) { // initialization - if (!initParticleSystem2D(PartSys, NUMBEROFSOURCES)) - return mode_static(); // allocation failed - SEGENV.aux1 = 0x01; // check flags - #ifdef ESP8266 - PartSys->setMotionBlur(180); - #else - PartSys->setMotionBlur(130); - #endif - for (i = 0; i < min(PartSys->numSources, (uint32_t)NUMBEROFSOURCES); i++) { - PartSys->sources[i].source.x = (PartSys->maxX + 1) >> 1; // center - PartSys->sources[i].source.y = (PartSys->maxY + 1) >> 1; // center - PartSys->sources[i].maxLife = 900; - PartSys->sources[i].minLife = 800; - } - PartSys->setKillOutOfBounds(true); - } - else - PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS - - if (PartSys == NULL) - return mode_static(); // something went wrong, no data! - - PartSys->updateSystem(); // update system properties (dimensions and data pointers) - uint8_t spraycount = min(PartSys->numSources, (uint32_t)(1 + (SEGMENT.custom1 >> 5))); // number of sprays to display, 1-8 - #ifdef ESP8266 - for (i = 1; i < 4; i++) { // need static particles in the center to reduce blinking (would be black every other frame without this hack), just set them there fixed - int partindex = (int)PartSys->usedParticles - (int)i; - if(partindex >= 0) { - PartSys->particles[partindex].x = (PartSys->maxX + 1) >> 1; // center - PartSys->particles[partindex].y = (PartSys->maxY + 1) >> 1; // center - PartSys->particles[partindex].sat = 230; - PartSys->particles[partindex].ttl = 256; //keep alive - } - } - #endif - if (SEGMENT.check1 != (SEGENV.aux1 & 0x01) || SEGMENT.call == 0) { // state change - if (SEGMENT.check1) - SEGENV.aux1 |= 0x01; //set the flag - else - SEGENV.aux1 &= ~0x01; // clear the flag - - for (i = 0; i < spraycount; i++) { - 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; - } - } - } - // set rotation direction and speed - // can use direction flag to determine current direction - bool direction = SEGMENT.check2; //no automatic direction change, set it to flag - int32_t currentspeed = (int32_t)SEGENV.step; // make a signed integer out of step - - if (SEGMENT.custom2 > 0) { // automatic direction change enabled - uint16_t changeinterval = 15 + 255 / SEGMENT.custom2; - direction = SEGENV.aux1 & 0x02; //set direction according to flag - - if (SEGMENT.check3) // random interval - changeinterval = 20 + changeinterval + random16(changeinterval); - - if (SEGMENT.call % changeinterval == 0) { //flip direction on next frame - SEGENV.aux1 |= 0x04; // set the update flag (for random interval update) - if (direction) - SEGENV.aux1 &= ~0x02; // clear the direction flag - else - SEGENV.aux1 |= 0x02; // set the direction flag - } - } - - int32_t targetspeed = (direction ? 1 : -1) * (SEGMENT.speed << 2); - int32_t speeddiff = targetspeed - currentspeed; - int32_t speedincrement = speeddiff / 50; - - if (speedincrement == 0) { //if speeddiff is not zero, make the increment at least 1 so it reaches target speed - if(speeddiff < 0) - speedincrement = -1; - else if (speeddiff > 0) - speedincrement = 1; - } - - currentspeed += speedincrement; - SEGENV.aux0 += currentspeed; - SEGENV.step = (uint32_t)currentspeed; //save it back - - uint16_t angleoffset = 0xFFFF / spraycount; // angle offset for an even distribution - uint32_t skip = PS_P_HALFRADIUS / (SEGMENT.intensity + 1) + 1; // intensity is emit speed, emit less on low speeds - if (SEGMENT.call % skip == 0) { - j = random(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) - PartSys->sources[j].var = (SEGMENT.custom3 >> 1); //update speed variation - #ifdef ESP8266 - if (SEGMENT.call & 0x01) // every other frame, do not emit to save particles - #endif - PartSys->angleEmit(PartSys->sources[j], SEGENV.aux0 + angleoffset * j, (SEGMENT.intensity >> 2)+1); - j = (j + 1) % spraycount; - } - } - PartSys->update(); //update all particles and render to frame - return FRAMETIME; -} #undef NUMBEROFSOURCES static const char _data_FX_MODE_PARTICLEVORTEX[] PROGMEM = "PS Vortex@Rotation Speed,Particle Speed,Arms,Auto Flip,Nozzle,Random Color, Direction, Random Flip;;!;2;pal=56,sx=200,ix=190,c1=200,c2=0,c3=0,o1=0,o2=0,o3=0"; diff --git a/wled00/FX.h b/wled00/FX.h index 5d64493904..30a9124885 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -578,6 +578,7 @@ typedef struct Segment { inline static void addUsedSegmentData(int len) { Segment::_usedSegmentData += len; } #ifndef WLED_DISABLE_MODE_BLEND inline static void modeBlend(bool blend) { _modeBlend = blend; } + inline static bool getmodeBlend(void) { return _modeBlend; } #endif inline static unsigned vLength() { return Segment::_vLength; } inline static unsigned vWidth() { return Segment::_vWidth; } From 636d074ea3f8d0ec8fba2f2f8526338dc902401f Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Tue, 24 Dec 2024 11:59:20 +0100 Subject: [PATCH 144/219] bugfix in setCurrentPalette, corrected order of 2D memory allocation, increased max particles/sources - fixed timing for vortex --- wled00/FX.cpp | 3 +-- wled00/FX_fcn.cpp | 27 ++++++++++++--------------- wled00/FXparticleSystem.cpp | 9 ++++----- wled00/FXparticleSystem.h | 14 +++++++------- 4 files changed, 24 insertions(+), 29 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index a1c3a11ce3..ef1b67b8bd 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -7727,7 +7727,7 @@ uint16_t mode_particlevortex(void) { int32_t currentspeed = (int32_t)SEGENV.step; // make a signed integer out of step if (SEGMENT.custom2 > 0) { // automatic direction change enabled - uint16_t changeinterval = 15 + 255 / SEGMENT.custom2; + uint16_t changeinterval = 1040 - ((uint32_t)SEGMENT.custom2 << 2); direction = SEGENV.aux1 & 0x02; //set direction according to flag if (SEGMENT.check3) // random interval @@ -8004,7 +8004,6 @@ uint16_t mode_particlefire(void) { if (!initParticleSystem2D(PartSys, SEGMENT.virtualWidth(), 4)) //maximum number of source (PS may limit based on segment size); need 4 additional bytes for time keeping (uint32_t lastcall) return mode_static(); // allocation failed or not 2D SEGENV.aux0 = random16(); // aux0 is wind position (index) in the perlin noise - numFlames = PartSys->numSources; } else PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index c736a2b82b..1e410a8051 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -430,22 +430,19 @@ void Segment::beginDraw() { } void Segment::setCurrentPalette(bool loadOldPalette) { - if (isInTransition()) { - if(loadOldPalette) { // load palette of old effect - loadPalette(_currentPalette, _t->_palette); - return; - } - if(strip.paletteFade) { - // blend palettes - // there are about 255 blend passes of 48 "blends" to completely blend two palettes (in _dur time) - // minimum blend time is 100ms maximum is 65535ms - loadPalette(_currentPalette, palette); - unsigned noOfBlends = ((255U * progress()) / 0xFFFFU) - _t->_prevPaletteBlends; - for (unsigned i = 0; i < noOfBlends; i++, _t->_prevPaletteBlends++) nblendPaletteTowardPalette(_t->_palT, _currentPalette, 48); - _currentPalette = _t->_palT; // copy transitioning/temporary palette - } - } else { + if(loadOldPalette) { // load palette of old effect, used in particle system + loadPalette(_currentPalette, _t->_palette); + return; + } + else loadPalette(_currentPalette, palette); + if(strip.paletteFade && progress() < 0xFFFFU) { + // blend palettes + // there are about 255 blend passes of 48 "blends" to completely blend two palettes (in _dur time) + // minimum blend time is 100ms maximum is 65535ms + unsigned noOfBlends = ((255U * progress()) / 0xFFFFU) - _t->_prevPaletteBlends; + for (unsigned i = 0; i < noOfBlends; i++, _t->_prevPaletteBlends++) nblendPaletteTowardPalette(_t->_palT, _currentPalette, 48); + _currentPalette = _t->_palT; // copy transitioning/temporary palette } } diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index 241af85ff3..e267795b4d 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -1252,16 +1252,15 @@ bool initParticleSystem2D(ParticleSystem2D *&PartSys, uint32_t requestedsources, PSPRINT(" segmentsize:" + String(cols) + " " + String(rows)); PSPRINT(" request numparticles:" + String(numparticles)); uint32_t numsources = calculateNumberOfSources2D(pixels, requestedsources); - // allocate rendering buffer (if this fails, it will render to segment buffer directly) - updateRenderingBuffer(framebuffer, pixels, true); - if(advanced) - updateRenderingBuffer(renderbuffer, 100, false); - if (!allocateParticleSystemMemory2D(numparticles, numsources, advanced, sizecontrol, additionalbytes)) { DEBUG_PRINT(F("PS init failed: memory depleted")); return false; } + // allocate rendering buffer (if this fails, it will render to segment buffer directly) + updateRenderingBuffer(framebuffer, pixels, true); // allocate a rendering buffer + if(advanced) + updateRenderingBuffer(renderbuffer, 100, false); // allocate a 10x10 buffer for rendering advanced particles PartSys = new (SEGENV.data) ParticleSystem2D(cols, rows, numparticles, numsources, advanced, sizecontrol); // particle system constructor diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index f4d94fc24d..0bce9b346e 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -59,12 +59,12 @@ inline void updateUsedParticles(const uint32_t allocated, const uint32_t availab // TODO: maybe update PS_P_MINSURFACEHARDNESS for 2D? its a bit too sticky already at hardness 100 #ifndef WLED_DISABLE_PARTICLESYSTEM2D // memory allocation -#define ESP8266_MAXPARTICLES 180 // enough for one 16x16 segment with transitions -#define ESP8266_MAXSOURCES 16 -#define ESP32S2_MAXPARTICLES 840 // enough for four 16x16 segments -#define ESP32S2_MAXSOURCES 48 -#define ESP32_MAXPARTICLES 1024 // enough for four 16x16 segments TODO: not enough for one 64x64 panel... -#define ESP32_MAXSOURCES 64 +#define ESP8266_MAXPARTICLES 300 // enough up to 20x20 pixels +#define ESP8266_MAXSOURCES 24 +#define ESP32S2_MAXPARTICLES 1024 // enough up to 32x32 pixels +#define ESP32S2_MAXSOURCES 64 +#define ESP32_MAXPARTICLES 2048 // enough up to 64x32 pixels +#define ESP32_MAXSOURCES 128 // particle dimensions (subpixel division) #define PS_P_RADIUS 64 // subpixel size, each pixel is divided by this for particle movement (must be a power of 2) @@ -99,7 +99,7 @@ typedef struct { // 11 bytes uint8_t hue; // color hue uint8_t sat; // particle color saturation //uint16_t ttl : 12; // time to live, 12 bit or 4095 max (which is 50s at 80FPS) - uint16_t ttl; // time to live + uint16_t ttl; // time to live bool outofbounds : 1; // out of bounds flag, set to true if particle is outside of display area bool collide : 1; // if set, particle takes part in collisions bool perpetual : 1; // if set, particle does not age (TTL is not decremented in move function, it still dies from killoutofbounds) From 24e73f3e5fb1c1e3774a30520400f839cffdbce4 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Thu, 26 Dec 2024 09:39:10 +0100 Subject: [PATCH 145/219] fixed particle buffer handover, added 2D blur option, increased particle limits, various fixes - fixed bug in memory watchdog, now works and also works when freezing segments - fixed PS Box to work again with particle handover - added smear blurring to waterfall - added smear blurring to vortex, fixed color distribution, removed random color distribution option - replaced all random() calls with hw_random() - transitions now work but only if FX transitions are enabled and not when chaning segment size --- wled00/FX.cpp | 295 ++++++++++++++++++------------------ wled00/FX_fcn.cpp | 2 +- wled00/FXparticleSystem.cpp | 187 +++++++++++++---------- wled00/FXparticleSystem.h | 29 ++-- 4 files changed, 270 insertions(+), 243 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index ef1b67b8bd..83af458693 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -7673,7 +7673,6 @@ uint16_t mode_particlevortex(void) { if (SEGMENT.call == 0) { // initialization if (!initParticleSystem2D(PartSys, NUMBEROFSOURCES)) return mode_static(); // allocation failed - SEGENV.aux1 = 0x01; // check flags #ifdef ESP8266 PartSys->setMotionBlur(180); #else @@ -7694,7 +7693,7 @@ uint16_t mode_particlevortex(void) { return mode_static(); // something went wrong, no data! PartSys->updateSystem(); // update system properties (dimensions and data pointers) - uint8_t spraycount = min(PartSys->numSources, (uint32_t)(1 + (SEGMENT.custom1 >> 5))); // number of sprays to display, 1-8 + uint32_t spraycount = min(PartSys->numSources, (uint32_t)(1 + (SEGMENT.custom1 >> 5))); // number of sprays to display, 1-8 #ifdef ESP8266 for (i = 1; i < 4; i++) { // need static particles in the center to reduce blinking (would be black every other frame without this hack), just set them there fixed int partindex = (int)PartSys->usedParticles - (int)i; @@ -7706,43 +7705,40 @@ uint16_t mode_particlevortex(void) { } } #endif - if (SEGMENT.check1 != (SEGENV.aux1 & 0x01) || SEGMENT.call == 0) { // state change - if (SEGMENT.check1) - SEGENV.aux1 |= 0x01; //set the flag - else - SEGENV.aux1 &= ~0x01; // clear the flag - - for (i = 0; i < spraycount; i++) { - 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; - } - } + + if (SEGMENT.check1) + PartSys->setSmearBlur(90); // enable smear blur + else + PartSys->setSmearBlur(0); // disable smear blur + + // update colors of the sprays + for (i = 0; i < spraycount; i++) { + uint32_t coloroffset = 0xFF / spraycount; + PartSys->sources[i].source.hue = coloroffset * i; } + // set rotation direction and speed // can use direction flag to determine current direction bool direction = SEGMENT.check2; //no automatic direction change, set it to flag int32_t currentspeed = (int32_t)SEGENV.step; // make a signed integer out of step if (SEGMENT.custom2 > 0) { // automatic direction change enabled - uint16_t changeinterval = 1040 - ((uint32_t)SEGMENT.custom2 << 2); - direction = SEGENV.aux1 & 0x02; //set direction according to flag + uint32_t changeinterval = 1040 - ((uint32_t)SEGMENT.custom2 << 2); + direction = SEGENV.aux1 & 0x01; //set direction according to flag if (SEGMENT.check3) // random interval - changeinterval = 20 + changeinterval + random16(changeinterval); + changeinterval = 20 + changeinterval + hw_random16(changeinterval); if (SEGMENT.call % changeinterval == 0) { //flip direction on next frame - SEGENV.aux1 |= 0x04; // set the update flag (for random interval update) + SEGENV.aux1 |= 0x02; // set the update flag (for random interval update) if (direction) - SEGENV.aux1 &= ~0x02; // clear the direction flag + SEGENV.aux1 &= ~0x01; // clear the direction flag else - SEGENV.aux1 |= 0x02; // set the direction flag + SEGENV.aux1 |= 0x01; // set the direction flag } } - int32_t targetspeed = (direction ? 1 : -1) * (SEGMENT.speed << 2); + int32_t targetspeed = (direction ? 1 : -1) * (SEGMENT.speed << 3); int32_t speeddiff = targetspeed - currentspeed; int32_t speedincrement = speeddiff / 50; @@ -7760,7 +7756,7 @@ uint16_t mode_particlevortex(void) { uint16_t angleoffset = 0xFFFF / spraycount; // angle offset for an even distribution uint32_t skip = PS_P_HALFRADIUS / (SEGMENT.intensity + 1) + 1; // intensity is emit speed, emit less on low speeds if (SEGMENT.call % skip == 0) { - j = random(spraycount); // start with random spray so all get a chance to emit a particle if maximum number of particles alive is reached. + j = hw_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) PartSys->sources[j].var = (SEGMENT.custom3 >> 1); //update speed variation #ifdef ESP8266 @@ -7774,7 +7770,7 @@ uint16_t mode_particlevortex(void) { return FRAMETIME; } #undef NUMBEROFSOURCES -static const char _data_FX_MODE_PARTICLEVORTEX[] PROGMEM = "PS Vortex@Rotation Speed,Particle Speed,Arms,Auto Flip,Nozzle,Random Color, Direction, Random Flip;;!;2;pal=56,sx=200,ix=190,c1=200,c2=0,c3=0,o1=0,o2=0,o3=0"; +static const char _data_FX_MODE_PARTICLEVORTEX[] PROGMEM = "PS Vortex@Rotation Speed,Particle Speed,Arms,Auto Flip,Nozzle,Smear,Direction,Random Flip;;!;2;pal=56,sx=200,ix=190,c1=200,c2=0,c3=0,o1=0,o2=0,o3=0"; /* * Particle Fireworks @@ -7823,11 +7819,11 @@ uint16_t mode_particlefireworks(void) { } else if (PartSys->sources[j].source.vy < 0) { // rocket is exploded and time is up (ttl=0 and negative speed), relaunch it PartSys->sources[j].source.y = PS_P_RADIUS; // start from bottom - PartSys->sources[j].source.x = (PartSys->maxX >> 2) + random(PartSys->maxX >> 1); // centered half + PartSys->sources[j].source.x = (PartSys->maxX >> 2) + hw_random(PartSys->maxX >> 1); // centered half PartSys->sources[j].source.vy = (SEGMENT.custom3) + random16(SEGMENT.custom1 >> 3) + 5; // rocket speed TODO: need to adjust for segment height - PartSys->sources[j].source.vx = random(-3, 3); // not perfectly straight up + PartSys->sources[j].source.vx = hw_random16(7) - 3; // not perfectly straight up PartSys->sources[j].source.sat = 30; // low saturation -> exhaust is off-white - PartSys->sources[j].source.ttl = random16(SEGMENT.custom1) + (SEGMENT.custom1 >> 1); // set fuse time + PartSys->sources[j].source.ttl = hw_random16(SEGMENT.custom1) + (SEGMENT.custom1 >> 1); // set fuse time PartSys->sources[j].maxLife = 40; // exhaust particle life PartSys->sources[j].minLife = 10; PartSys->sources[j].vx = 0; // emitting speed @@ -7855,31 +7851,31 @@ uint16_t mode_particlefireworks(void) { emitparticles = 0; } else { // speed is zero, explode! - PartSys->sources[j].source.hue = random16(); // random color - PartSys->sources[j].source.sat = random16(55) + 200; + PartSys->sources[j].source.hue = hw_random16(); // random color + PartSys->sources[j].source.sat = hw_random16(55) + 200; PartSys->sources[j].maxLife = 200; PartSys->sources[j].minLife = 100; - PartSys->sources[j].source.ttl = random16((2000 - ((uint32_t)SEGMENT.speed << 2))) + 550 - (SEGMENT.speed << 1); // standby time til next launch + PartSys->sources[j].source.ttl = hw_random16((2000 - ((uint32_t)SEGMENT.speed << 2))) + 550 - (SEGMENT.speed << 1); // standby time til next launch PartSys->sources[j].var = ((SEGMENT.intensity >> 4) + 5); // speed variation around vx,vy (+/- var) PartSys->sources[j].source.vy = -1; // set speed negative so it will emit no more particles after this explosion until relaunch #ifdef ESP8266 - emitparticles = random16(SEGMENT.intensity >> 3) + (SEGMENT.intensity >> 3) + 5; // defines the size of the explosion + emitparticles = hw_random16(SEGMENT.intensity >> 3) + (SEGMENT.intensity >> 3) + 5; // defines the size of the explosion #else - emitparticles = random16(SEGMENT.intensity >> 2) + (SEGMENT.intensity >> 2) + 5; // defines the size of the explosion + emitparticles = hw_random16(SEGMENT.intensity >> 2) + (SEGMENT.intensity >> 2) + 5; // defines the size of the explosion #endif if (random16() & 1) { // 50% chance for circular explosion circularexplosion = true; - speed = 2 + random16(3) + ((SEGMENT.intensity >> 6)); + speed = 2 + hw_random16(3) + ((SEGMENT.intensity >> 6)); currentspeed = speed; - angleincrement = 2730 + random16(5461); // minimum 15° + random(30°) - angle = random16(); // random start angle + angleincrement = 2730 + hw_random16(5461); // minimum 15° + random(30°) + angle = hw_random16(); // random start angle baseangle = angle; // save base angle for modulation percircle = 0xFFFF / angleincrement + 1; // number of particles to make complete circles - hueincrement = random16() & 127; // &127 is equivalent to %128 - int circles = 1 + random16(3) + ((SEGMENT.intensity >> 6)); - frequency = random16() & 127; // modulation frequency (= "waves per circle"), x.4 fixed point + hueincrement = hw_random16() & 127; // &127 is equivalent to %128 + int circles = 1 + hw_random16(3) + ((SEGMENT.intensity >> 6)); + frequency = hw_random16() & 127; // modulation frequency (= "waves per circle"), x.4 fixed point emitparticles = percircle * circles; PartSys->sources[j].var = angle & 1; // 0 or 1 variation, angle is random } @@ -7902,7 +7898,7 @@ uint16_t mode_particlefireworks(void) { else { // random explosion or exhaust PartSys->sprayEmit(PartSys->sources[j]); if ((j % 3) == 0) { - PartSys->sources[j].source.hue = random16(); // random color for each particle (this is also true for exhaust, but that is white anyways) + PartSys->sources[j].source.hue = hw_random16(); // random color for each particle (this is also true for exhaust, but that is white anyways) } } } @@ -7942,7 +7938,7 @@ uint16_t mode_particlevolcano(void) { numSprays = min(PartSys->numSources, (uint32_t)NUMBEROFSOURCES); // number of sprays for (i = 0; i < numSprays; i++) { - PartSys->sources[i].source.hue = random16(); + PartSys->sources[i].source.hue = hw_random16(); PartSys->sources[i].source.x = PartSys->maxX / (numSprays + 1) * (i + 1); // distribute evenly PartSys->sources[i].maxLife = 300; // lifetime in frames PartSys->sources[i].minLife = 250; @@ -7974,7 +7970,7 @@ uint16_t mode_particlevolcano(void) { for (i = 0; i < numSprays; i++) { PartSys->sources[i].source.y = PS_P_RADIUS + 5; // reset to just above the lower edge that is allowed for bouncing particles, if zero, particles already 'bounce' at start and loose speed. PartSys->sources[i].source.vy = 0; //reset speed (so no extra particlesettin is required to keep the source 'afloat') - PartSys->sources[i].source.hue++; // = random16(); //change hue of spray source (note: random does not look good) + PartSys->sources[i].source.hue++; // = hw_random16(); //change hue of spray source (note: random does not look good) PartSys->sources[i].source.vx = PartSys->sources[i].source.vx > 0 ? SEGMENT.custom1 >> 2 : -(SEGMENT.custom1 >> 2); // set moving speed but keep the direction given by PS PartSys->sources[i].vy = SEGMENT.speed >> 2; // emitting speed PartSys->sources[i].vx = 0; @@ -8003,7 +7999,7 @@ uint16_t mode_particlefire(void) { if (SEGMENT.call == 0) { // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. if (!initParticleSystem2D(PartSys, SEGMENT.virtualWidth(), 4)) //maximum number of source (PS may limit based on segment size); need 4 additional bytes for time keeping (uint32_t lastcall) return mode_static(); // allocation failed or not 2D - SEGENV.aux0 = random16(); // aux0 is wind position (index) in the perlin noise + SEGENV.aux0 = hw_random16(); // aux0 is wind position (index) in the perlin noise } else PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS @@ -8037,14 +8033,14 @@ uint16_t mode_particlefire(void) { if (SEGMENT.call & 1 && PartSys->sources[i].source.ttl > 0) { // every second frame PartSys->sources[i].source.ttl--; } else { // flame source is dead: initialize new flame: set properties of source - PartSys->sources[i].source.x = (PartSys->maxX >> 1) - (spread >> 1) + random(spread); // change flame position: distribute randomly on chosen width + PartSys->sources[i].source.x = (PartSys->maxX >> 1) - (spread >> 1) + hw_random(spread); // change flame position: distribute randomly on chosen width PartSys->sources[i].source.y = -(PS_P_RADIUS << 2); // set the source below the frame - PartSys->sources[i].source.ttl = 20 + random((SEGMENT.custom1 * SEGMENT.custom1) >> 8) / (1 + (firespeed >> 5)); //'hotness' of fire, faster flames reduce the effect or flame height will scale too much with speed - PartSys->sources[i].maxLife = random(SEGMENT.virtualHeight() / 2) + 16; // defines flame height together with the vy speed, vy speed*maxlife/PS_P_RADIUS is the average flame height + PartSys->sources[i].source.ttl = 20 + hw_random16((SEGMENT.custom1 * SEGMENT.custom1) >> 8) / (1 + (firespeed >> 5)); //'hotness' of fire, faster flames reduce the effect or flame height will scale too much with speed + PartSys->sources[i].maxLife = hw_random16(SEGMENT.virtualHeight() >> 1) + 16; // defines flame height together with the vy speed, vy speed*maxlife/PS_P_RADIUS is the average flame height PartSys->sources[i].minLife = PartSys->sources[i].maxLife >> 1; - PartSys->sources[i].vx = random16(4) - 2; // emitting speed (sideways) + PartSys->sources[i].vx = hw_random16(4) - 2; // emitting speed (sideways) PartSys->sources[i].vy = (SEGMENT.virtualHeight() >> 1) + (firespeed >> 4) + (SEGMENT.custom1 >> 4); // emitting speed (upwards) - PartSys->sources[i].var = 2 + random16(2 + (firespeed >> 4)); // speed variation around vx,vy (+/- var) + PartSys->sources[i].var = 2 + hw_random16(2 + (firespeed >> 4)); // speed variation around vx,vy (+/- var) } } @@ -8069,7 +8065,7 @@ uint16_t mode_particlefire(void) { } } - uint8_t j = random16(); // start with a random flame (so each flame gets the chance to emit a particle if available particles is smaller than number of flames) + uint8_t j = hw_random16(); // start with a random flame (so each flame gets the chance to emit a particle if available particles is smaller than number of flames) for (i = 0; i < percycle; i++) { j = (j + 1) % numFlames; PartSys->flameEmit(PartSys->sources[j]); @@ -8119,18 +8115,18 @@ uint16_t mode_particlepit(void) { for (i = 0; i < PartSys->usedParticles; i++) { // emit particles if (PartSys->particles[i].ttl == 0) { // find a dead particle // emit particle at random position over the top of the matrix (random16 is not random enough) - PartSys->particles[i].ttl = 1500 - (SEGMENT.speed << 2) + random16(500); // if speed is higher, make them die sooner - PartSys->particles[i].x = random(PartSys->maxX); //random(PartSys->maxX >> 1) + (PartSys->maxX >> 2); + PartSys->particles[i].ttl = 1500 - (SEGMENT.speed << 2) + hw_random16(500); // if speed is higher, make them die sooner + PartSys->particles[i].x = hw_random(PartSys->maxX); //random(PartSys->maxX >> 1) + (PartSys->maxX >> 2); PartSys->particles[i].y = (PartSys->maxY << 1); // particles appear somewhere above the matrix, maximum is double the height - PartSys->particles[i].vx = (int16_t)random(SEGMENT.speed >> 1) - (SEGMENT.speed >> 2); // side speed is +/- + PartSys->particles[i].vx = (int16_t)hw_random16(SEGMENT.speed >> 1) - (SEGMENT.speed >> 2); // side speed is +/- PartSys->particles[i].vy = map(SEGMENT.speed, 0, 255, -5, -100); // downward speed - PartSys->particles[i].hue = random16(); // set random color + PartSys->particles[i].hue = hw_random16(); // set random color PartSys->particles[i].collide = true; // enable collision for particle PartSys->particles[i].sat = ((SEGMENT.custom3) << 3) + 7; // set particle size if (SEGMENT.custom1 == 255) { PartSys->setParticleSize(0); // set global size to zero - PartSys->advPartProps[i].size = random(SEGMENT.custom1); // set each particle to random size + PartSys->advPartProps[i].size =hw_random16(SEGMENT.custom1); // set each particle to random size } else { PartSys->setParticleSize(SEGMENT.custom1); // set global size PartSys->advPartProps[i].size = 0; // use global size @@ -8169,17 +8165,18 @@ uint16_t mode_particlewaterfall(void) { PartSys->setGravity(); // enable with default gforce PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting) - PartSys->setMotionBlur(190); // anable motion blur + PartSys->setMotionBlur(175); // anable motion blur + PartSys->setSmearBlur(45); // enable 2D blurring (smearing) for (i = 0; i < PartSys->numSources; i++) { - PartSys->sources[i].source.hue = random16(); + PartSys->sources[i].source.hue = i*90; PartSys->sources[i].source.collide = true; // seeded particles will collide -#ifdef ESP8266 + #ifdef ESP8266 PartSys->sources[i].maxLife = 250; // lifetime in frames (ESP8266 has less particles, make them short lived to keep the water flowing) PartSys->sources[i].minLife = 100; -#else + #else PartSys->sources[i].maxLife = 400; // lifetime in frames PartSys->sources[i].minLife = 150; -#endif + #endif } } else @@ -8202,10 +8199,10 @@ uint16_t mode_particlewaterfall(void) { } for (i = 0; i < numSprays; i++) { - PartSys->sources[i].source.hue++; // change hue of spray source + PartSys->sources[i].source.hue += 1 + hw_random16(SEGMENT.custom1>>1); // change hue of spray source } - if (SEGMENT.call % (9 - (SEGMENT.intensity >> 5)) == 0 && SEGMENT.intensity > 0) { // every nth frame, cycle color and emit particles, do not emit if intensity is zero + if (SEGMENT.call % (9 - (SEGMENT.intensity >> 5)) == 0 && SEGMENT.intensity > 0) { // every nth frame, emit particles, do not emit if intensity is zero for (i = 0; i < numSprays; i++) { PartSys->sources[i].vy = -SEGMENT.speed >> 3; // emitting speed, down //PartSys->sources[i].source.x = map(SEGMENT.custom3, 0, 31, 0, (PartSys->maxXpixel - numSprays * 2) * PS_P_RADIUS) + i * PS_P_RADIUS * 2; // emitter position @@ -8222,7 +8219,7 @@ uint16_t mode_particlewaterfall(void) { PartSys->update(); // update and render return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEWATERFALL[] PROGMEM = "PS Waterfall@Speed,Intensity,Variation,Collisions,Position,Cylinder,Walls,Ground;;!;2;pal=9,sx=15,ix=200,c1=60,c2=160,c3=17,o1=0,o2=0,o3=1"; +static const char _data_FX_MODE_PARTICLEWATERFALL[] PROGMEM = "PS Waterfall@Speed,Intensity,Variation,Collisions,Position,Cylinder,Walls,Ground;;!;2;pal=9,sx=15,ix=200,c1=64,c2=160,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) @@ -8238,8 +8235,7 @@ uint16_t mode_particlebox(void) { return mode_static(); // allocation failed or not 2D PartSys->setBounceX(true); PartSys->setBounceY(true); - SEGENV.aux1 = 0; // number currently used particles - SEGENV.aux0 = random16(); // position in perlin noise + SEGENV.aux0 = hw_random16(); // position in perlin noise } else PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS @@ -8251,17 +8247,18 @@ uint16_t mode_particlebox(void) { PartSys->setWallHardness(min(SEGMENT.custom2, (uint8_t)200)); // wall hardness is 200 or more PartSys->enableParticleCollisions(true, max(2, (int)SEGMENT.custom2)); // enable collisions and set particle collision hardness - PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 25, 255)); // 10%-100% - if(PartSys->usedParticles > SEGENV.aux1) { // if more particles are needed, emit them - for (i = SEGENV.aux1; i < PartSys->usedParticles; i++) { + PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 5, 153)); // 2% - 60% + // add in new particles if amount has changed + for (i = 0; i < PartSys->usedParticles; i++) { + if(PartSys->particles[i].ttl < 260) { // initialize handed over particles and dead particles PartSys->particles[i].ttl = 260; // full brigthness PartSys->particles[i].perpetual = true; // never die - PartSys->particles[i].x = random(PartSys->maxX); - PartSys->particles[i].y = random(PartSys->maxY); - PartSys->particles[i].hue = i * 3; // color range + PartSys->particles[i].x = hw_random16(PartSys->maxX); + PartSys->particles[i].y = hw_random16(PartSys->maxY); + PartSys->particles[i].hue = hw_random8(); // make it colorful PartSys->particles[i].collide = true; // all particles colllide + break; // only spawn one particle per frame for less chaotic transitions } - SEGENV.aux1 = PartSys->usedParticles; } if (SEGMENT.call % (((255 - SEGMENT.speed) >> 6) + 1) == 0 && SEGMENT.speed > 0) { // how often the force is applied depends on speed setting @@ -8340,9 +8337,9 @@ uint16_t mode_particleperlin(void) { // update position in noise for (i = 0; i < PartSys->usedParticles; i++) { if (PartSys->particles[i].ttl == 0) { // revive dead particles (do not keep them alive forever, they can clump up, need to reseed) - PartSys->particles[i].ttl = random16(500) + 200; - PartSys->particles[i].x = random(PartSys->maxX); - PartSys->particles[i].y = random(PartSys->maxY); + PartSys->particles[i].ttl = hw_random16(500) + 200; + PartSys->particles[i].x = hw_random(PartSys->maxX); + PartSys->particles[i].y = hw_random(PartSys->maxY); PartSys->particles[i].collide = true; // particle colllides } uint32_t scale = 16 - ((31 - SEGMENT.custom3) >> 1); @@ -8387,7 +8384,7 @@ uint16_t mode_particleimpact(void) { MaxNumMeteors = min(PartSys->numSources, (uint32_t)NUMBEROFSOURCES); for (i = 0; i < MaxNumMeteors; i++) { // PartSys->sources[i].source.y = 500; - PartSys->sources[i].source.ttl = random16(10 * i); // set initial delay for meteors + PartSys->sources[i].source.ttl = hw_random16(10 * i); // set initial delay for meteors PartSys->sources[i].source.vy = 10; // at positive speeds, no particles are emitted and if particle dies, it will be relaunched } } @@ -8424,9 +8421,9 @@ uint16_t mode_particleimpact(void) { else { // speed is zero, explode! PartSys->sources[i].source.vy = 10; // set source speed positive so it goes into timeout and launches again #ifdef ESP8266 - emitparticles = random16(SEGMENT.intensity >> 3) + 5; // defines the size of the explosion + emitparticles = hw_random16(SEGMENT.intensity >> 3) + 5; // defines the size of the explosion #else - emitparticles = map(SEGMENT.intensity, 0, 255, 10, random16(PartSys->usedParticles>>2)); // defines the size of the explosion !!!TODO: check if this works on ESP8266, drop esp8266 def if it does + emitparticles = map(SEGMENT.intensity, 0, 255, 10, hw_random16(PartSys->usedParticles>>2)); // defines the size of the explosion !!!TODO: check if this works on ESP8266, drop esp8266 def if it does #endif } for (int e = emitparticles; e > 0; e--) { @@ -8454,7 +8451,7 @@ uint16_t mode_particleimpact(void) { PartSys->sources[i].maxLife = 250; PartSys->sources[i].minLife = 50; #endif - PartSys->sources[i].source.ttl = random16((512 - (SEGMENT.speed << 1))) + 40; // standby time til next launch (in frames) + PartSys->sources[i].source.ttl = hw_random16((512 - (SEGMENT.speed << 1))) + 40; // standby time til next launch (in frames) PartSys->sources[i].vy = (SEGMENT.custom1 >> 2); // emitting speed y PartSys->sources[i].var = (SEGMENT.custom1 >> 2); // speed variation around vx,vy (+/- var) } @@ -8463,10 +8460,10 @@ uint16_t mode_particleimpact(void) { else if (PartSys->sources[i].source.vy > 0) { // meteor is exploded and time is up (ttl==0 and positive speed), relaunch it // reinitialize meteor PartSys->sources[i].source.y = PartSys->maxY + (PS_P_RADIUS << 2); // start 4 pixels above the top - PartSys->sources[i].source.x = random(PartSys->maxX); - PartSys->sources[i].source.vy = -random16(30) - 30; // meteor downward speed - PartSys->sources[i].source.vx = random(50) - 25; // TODO: make this dependent on position so they do not move out of frame - PartSys->sources[i].source.hue = random16(); // random color + PartSys->sources[i].source.x = hw_random(PartSys->maxX); + PartSys->sources[i].source.vy = -hw_random16(30) - 30; // meteor downward speed + PartSys->sources[i].source.vx = hw_random16(50) - 25; // TODO: make this dependent on position so they do not move out of frame + PartSys->sources[i].source.hue = hw_random16(); // random color PartSys->sources[i].source.ttl = 500; // long life, will explode at bottom PartSys->sources[i].source.collide = false; // trail particles will not collide PartSys->sources[i].maxLife = 60; // spark particle life @@ -8497,7 +8494,7 @@ uint16_t mode_particleattractor(void) { if (SEGMENT.call == 0) { // initialization if (!initParticleSystem2D(PartSys, 1, sizeof(PSparticle), true)) // init using 1 source and advanced particle settings return mode_static(); // allocation failed or not 2D - PartSys->sources[0].source.hue = random16(); + PartSys->sources[0].source.hue = hw_random16(); PartSys->sources[0].source.vx = -7; // will collied with wall and get random bounce direction PartSys->sources[0].source.collide = true; // seeded particles will collide PartSys->sources[0].source.perpetual = true; //source does not age @@ -8608,7 +8605,7 @@ uint16_t mode_particlespray(void) { PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting) PartSys->setBounceY(true); PartSys->setMotionBlur(200); // anable motion blur - PartSys->sources[0].source.hue = random16(); + PartSys->sources[0].source.hue = hw_random16(); PartSys->sources[0].source.collide = true; // seeded particles will collide (if enabled) PartSys->sources[0].var = 3; } @@ -8656,7 +8653,7 @@ uint16_t mode_particlespray(void) { if (SEGMENT.call % (11 - (SEGMENT.intensity / 25)) == 0) { // every nth frame, cycle color and emit particles PartSys->sources[0].maxLife = 300; // lifetime in frames PartSys->sources[0].minLife = 100; - PartSys->sources[0].source.hue++; // = random16(); //change hue of spray source + PartSys->sources[0].source.hue++; // = hw_random16(); //change hue of spray source PartSys->angleEmit(PartSys->sources[0], angle, SEGMENT.speed >> 2); } } @@ -8665,9 +8662,9 @@ uint16_t mode_particlespray(void) { if (SEGMENT.call % (11 - (SEGMENT.intensity / 25)) == 0) { // every nth frame, cycle color and emit particles PartSys->sources[0].maxLife = 300; // lifetime in frames. note: could be done in init part, but AR moderequires this to be dynamic PartSys->sources[0].minLife = 100; - PartSys->sources[0].source.hue++; // = random16(); //change hue of spray source + PartSys->sources[0].source.hue++; // = hw_random16(); //change hue of spray source // PartSys->sources[i].var = SEGMENT.custom3; // emiting variation = nozzle size (custom 3 goes from 0-32) - // spray[j].source.hue = random16(); //set random color for each particle (using palette) + // spray[j].source.hue = hw_random16(); //set random color for each particle (using palette) PartSys->angleEmit(PartSys->sources[0], angle, SEGMENT.speed >> 2); } #endif @@ -8730,19 +8727,19 @@ uint16_t mode_particleGEQ(void) { } else if(fftResult[bin] > 0) { // band has low volue uint32_t restvolume = ((threshold - fftResult[bin])>>2) + 2; - if (random16() % restvolume == 0) + if (hw_random16() % restvolume == 0) emitparticles = 1; } while (i < PartSys->usedParticles && emitparticles > 0) { // emit particles if there are any left, low frequencies take priority if (PartSys->particles[i].ttl == 0) { // find a dead particle //set particle properties TODO: could also use the spray... - PartSys->particles[i].ttl = 20 + map(SEGMENT.intensity, 0,255, emitspeed>>1, emitspeed + random16(emitspeed)) ; // set particle alive, particle lifespan is in number of frames - PartSys->particles[i].x = xposition + random16(binwidth) - (binwidth>>1); // position randomly, deviating half a bin width + PartSys->particles[i].ttl = 20 + map(SEGMENT.intensity, 0,255, emitspeed>>1, emitspeed + hw_random16(emitspeed)) ; // set particle alive, particle lifespan is in number of frames + PartSys->particles[i].x = xposition + hw_random16(binwidth) - (binwidth>>1); // position randomly, deviating half a bin width PartSys->particles[i].y = PS_P_RADIUS; // start at the bottom (PS_P_RADIUS is minimum position a particle is fully in frame) - PartSys->particles[i].vx = random(SEGMENT.custom1>>1)-(SEGMENT.custom1>>2) ; //x-speed variation: +/- custom1/4 + PartSys->particles[i].vx = hw_random16(SEGMENT.custom1>>1)-(SEGMENT.custom1>>2) ; //x-speed variation: +/- custom1/4 PartSys->particles[i].vy = emitspeed; - PartSys->particles[i].hue = (bin<<4) + random16(17) - 8; // color from palette according to bin + PartSys->particles[i].hue = (bin<<4) + hw_random16(17) - 8; // color from palette according to bin emitparticles--; } i++; @@ -8804,7 +8801,7 @@ uint16_t mode_particlecenterGEQ(void) { SEGENV.aux0 -= SEGMENT.custom1 << 2; uint16_t angleoffset = (uint16_t)0xFFFF / (uint16_t)numSprays; - uint32_t j = random(numSprays); // start with random spray so all get a chance to emit a particle if maximum number of particles alive is reached. + uint32_t j = hw_random16(numSprays); // 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 < numSprays; i++) { if (SEGMENT.call % (32 - (SEGMENT.custom2 >> 3)) == 0 && SEGMENT.custom2 > 0) PartSys->sources[j].source.hue += 1 + (SEGMENT.custom2 >> 4); @@ -8818,7 +8815,7 @@ uint16_t mode_particlecenterGEQ(void) { emitparticles = 1; else if (fftResult[j] > 0) { // band has low value uint32_t restvolume = ((threshold - fftResult[j]) >> 2) + 2; - if (random16() % restvolume == 0) + if (hw_random16() % restvolume == 0) emitparticles = 1; } if (emitparticles) @@ -8846,9 +8843,9 @@ uint16_t mode_particleghostrider(void) { PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting) PartSys->sources[0].maxLife = 260; // lifetime in frames PartSys->sources[0].minLife = 250; - PartSys->sources[0].source.x = random16(PartSys->maxX); - PartSys->sources[0].source.y = random16(PartSys->maxY); - SEGENV.step = random(MAXANGLESTEP) - (MAXANGLESTEP>>1); // angle increment + PartSys->sources[0].source.x = hw_random16(PartSys->maxX); + PartSys->sources[0].source.y = hw_random16(PartSys->maxY); + SEGENV.step = hw_random16(MAXANGLESTEP) - (MAXANGLESTEP>>1); // angle increment } else { PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS @@ -8941,27 +8938,27 @@ uint16_t mode_particleblobs(void) { for (uint32_t i = 0; i < PartSys->usedParticles; i++) { // update particles if (SEGENV.aux0 != SEGMENT.speed || PartSys->particles[i].ttl == 0) { // speed changed or dead - PartSys->particles[i].vx = (int8_t)random(-(SEGMENT.speed >> 2), SEGMENT.speed >> 2); - PartSys->particles[i].vy = (int8_t)random(-(SEGMENT.speed >> 2), SEGMENT.speed >> 2); + PartSys->particles[i].vx = (int8_t)hw_random16(SEGMENT.speed >> 1) - (SEGMENT.speed >> 2); // +/- speed/4 + PartSys->particles[i].vy = (int8_t)hw_random16(SEGMENT.speed >> 1) - (SEGMENT.speed >> 2); } if (SEGENV.aux1 != SEGMENT.custom1 || PartSys->particles[i].ttl == 0) // size changed or dead - PartSys->advPartSize[i].maxsize = 60 + (SEGMENT.custom1 >> 1) + random((SEGMENT.custom1 >> 2)); // set each particle to slightly randomized size + PartSys->advPartSize[i].maxsize = 60 + (SEGMENT.custom1 >> 1) + hw_random16((SEGMENT.custom1 >> 2)); // set each particle to slightly randomized size //PartSys->particles[i].perpetual = SEGMENT.check2; //infinite life if set if (PartSys->particles[i].ttl == 0) { // find dead particle, renitialize - PartSys->particles[i].ttl = 300 + random16(((uint16_t)SEGMENT.custom2 << 3) + 100); - PartSys->particles[i].x = random(PartSys->maxX); - PartSys->particles[i].y = random16(PartSys->maxY); - PartSys->particles[i].hue = random16(); // set random color + PartSys->particles[i].ttl = 300 + hw_random16(((uint16_t)SEGMENT.custom2 << 3) + 100); + PartSys->particles[i].x = hw_random(PartSys->maxX); + PartSys->particles[i].y = hw_random16(PartSys->maxY); + PartSys->particles[i].hue = hw_random16(); // set random color PartSys->particles[i].collide = true; // enable collision for particle PartSys->advPartProps[i].size = 0; // start out small - PartSys->advPartSize[i].asymmetry = random16(220); - PartSys->advPartSize[i].asymdir = random16(255); + PartSys->advPartSize[i].asymmetry = hw_random16(220); + PartSys->advPartSize[i].asymdir = hw_random16(255); // set advanced size control properties PartSys->advPartSize[i].grow = true; - PartSys->advPartSize[i].growspeed = 1 + random16(9); - PartSys->advPartSize[i].shrinkspeed = 1 + random16(9); - PartSys->advPartSize[i].wobblespeed = 1 + random(3); + PartSys->advPartSize[i].growspeed = 1 + hw_random16(9); + PartSys->advPartSize[i].shrinkspeed = 1 + hw_random16(9); + PartSys->advPartSize[i].wobblespeed = 1 + hw_random16(3); } //PartSys->advPartSize[i].asymmetry++; PartSys->advPartSize[i].pulsate = SEGMENT.check3; @@ -9085,7 +9082,7 @@ uint16_t mode_particleDrip(void) { if (!initParticleSystem1D(PartSys, 4)) // init return mode_static(); // allocation failed or single pixel PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting) - PartSys->sources[0].source.hue = random16(); + PartSys->sources[0].source.hue = hw_random16(); SEGENV.aux1 = 0xFFFF; // invalidate } else @@ -9119,7 +9116,7 @@ uint16_t mode_particleDrip(void) { // lifetime in frames PartSys->sources[0].minLife = 30; PartSys->sources[0].maxLife = 200; - PartSys->sources[0].source.x = random(PartSys->maxX); //random emit position + PartSys->sources[0].source.x = hw_random(PartSys->maxX); //random emit position } else { //drip PartSys->sources[0].var = 0; @@ -9137,11 +9134,11 @@ uint16_t mode_particleDrip(void) { // every nth frame emit a particle if (SEGMENT.call % SEGENV.aux0 == 0) { int32_t interval = 300 / ((SEGMENT.intensity) + 1); - SEGENV.aux0 = interval + random(interval + 5); + SEGENV.aux0 = interval + hw_random(interval + 5); // if(SEGMENT.check1) // rain mode // PartSys->sources[0].source.hue = 0; // else - PartSys->sources[0].source.hue = random16(); //set random color TODO: maybe also not random but color cycling? need another slider or checkmark for this. + PartSys->sources[0].source.hue = hw_random8(); //set random color TODO: maybe also not random but color cycling? need another slider or checkmark for this. PartSys->sprayEmit(PartSys->sources[0]); } @@ -9228,11 +9225,11 @@ uint16_t mode_particleBouncingBalls(void) { if (updateballs || PartSys->particles[i].ttl == 0) { //speed changed or particle died, set particle properties PartSys->particles[i].ttl = 260 + SEGMENT.speed; PartSys->particles[i].collide = true; - int32_t newspeed = random(20 + (SEGMENT.speed >> 2)) + (SEGMENT.speed >> 3); + int32_t newspeed = hw_random16(20 + (SEGMENT.speed >> 2)) + (SEGMENT.speed >> 3); PartSys->particles[i].vx = PartSys->particles[i].vx > 0 ? newspeed : -newspeed; //keep the direction - PartSys->particles[i].hue = random16(); //set ball colors to random + PartSys->particles[i].hue = hw_random8(); //set ball colors to random PartSys->advPartProps[i].sat = 255; - PartSys->advPartProps[i].size = random16(SEGMENT.custom1 >> 2, SEGMENT.custom1); + PartSys->advPartProps[i].size = hw_random16(SEGMENT.custom1 >> 2, SEGMENT.custom1); } } } @@ -9246,10 +9243,10 @@ uint16_t mode_particleBouncingBalls(void) { // every nth frame emit a ball if (SEGMENT.call % SEGENV.aux0 == 0) { - SEGENV.aux0 = (260 - SEGMENT.intensity) + random(280 - SEGMENT.intensity); - PartSys->sources[0].source.hue = random16(); //set ball color + SEGENV.aux0 = (260 - SEGMENT.intensity) + hw_random16(280 - SEGMENT.intensity); + PartSys->sources[0].source.hue = hw_random16(); //set ball color PartSys->sources[0].sat = 255; - PartSys->sources[0].size = random16(SEGMENT.custom1 >> 2, SEGMENT.custom1); + PartSys->sources[0].size = hw_random16(SEGMENT.custom1 >> 2, SEGMENT.custom1); PartSys->sprayEmit(PartSys->sources[0]); } } @@ -9298,13 +9295,13 @@ uint16_t mode_particleDancingShadows(void) { //generate a spotlight: generates particles just outside of view if (SEGMENT.call % (256 - SEGMENT.intensity) == 0) { //random color, random type - uint32_t type = random8(SPOT_TYPES_COUNT); - int8_t speed = 2 + random(2 + (SEGMENT.speed >> 2)) + (SEGMENT.speed >> 3); - uint32_t width = random8(1, 10); + uint32_t type = hw_random16(SPOT_TYPES_COUNT); + int8_t speed = 2 + hw_random16(2 + (SEGMENT.speed >> 2)) + (SEGMENT.speed >> 3); + uint32_t width = hw_random16(1, 10); uint32_t ttl = 300; //ttl is particle brightness (below perpetual is set so it does not age, i.e. ttl stays at this value) int32_t position; //choose random start position, left and right from the segment - if (random8(2)) { + if (hw_random() & 0x01) { position = PartSys->maxXpixel; speed = -speed; } @@ -9312,7 +9309,7 @@ uint16_t mode_particleDancingShadows(void) { position = -width; PartSys->sources[0].v = speed; //emitted particle speed - PartSys->sources[0].source.hue = random8(); //random spotlight color + PartSys->sources[0].source.hue = hw_random8(); //random spotlight color for (uint32_t i = 0; i < width; i++) { switch (type) { case SPOT_TYPE_SOLID: @@ -9410,19 +9407,19 @@ uint16_t mode_particleFireworks1D(void) { if(PartSys->sources[0].source.perpetual == 1) { //rocket is on standby PartSys->sources[0].source.ttl--; if(PartSys->sources[0].source.ttl == 0) { //time is up, relaunch - if(random(255) < SEGMENT.custom1) //randomly choose direction according to slider, fire at start of segment if true + if(hw_random8() < SEGMENT.custom1) //randomly choose direction according to slider, fire at start of segment if true SEGENV.aux0 = 0; else SEGENV.aux0 = 1; //invert direction PartSys->sources[0].source.perpetual = 0; //flag abused for rocket state - PartSys->sources[0].source.hue = random16(); + PartSys->sources[0].source.hue = hw_random16(); PartSys->sources[0].var = 5; PartSys->sources[0].v = 0; PartSys->sources[0].minLife = 10; PartSys->sources[0].maxLife = 30; PartSys->sources[0].source.x = 0; // start from bottom - uint32_t speed = 8 + (((int)16 + (int)random16(20) + (int)random(SEGMENT.speed >> 3 , SEGMENT.speed >> 2)) * (int)PartSys->maxXpixel) / 150; //set speed such that rocket explods in frame, found by experimenting + uint32_t speed = 8 + (((int)16 + (int)random16(20) + (int)hw_random16(SEGMENT.speed >> 3 , SEGMENT.speed >> 2)) * (int)PartSys->maxXpixel) / 150; //set speed such that rocket explods in frame, found by experimenting PartSys->sources[0].source.vx = min(speed, (uint32_t)127); PartSys->sources[0].source.ttl = 400; PartSys->sources[0].source.collide = false; // exhaust does not collide, also used to check if direction reversed @@ -9457,13 +9454,13 @@ uint16_t mode_particleFireworks1D(void) { PartSys->sources[0].v = 0; //TODO can make global if this never changes PartSys->sources[0].minLife = 60; PartSys->sources[0].maxLife = 150; - PartSys->sources[0].source.ttl = 100 + random16(256 - SEGMENT.intensity); // standby time til next launch + PartSys->sources[0].source.ttl = 100 + hw_random16(256 - SEGMENT.intensity); // standby time til next launch PartSys->sources[0].sat = 7 + (SEGMENT.custom3 << 3); //color saturation - PartSys->sources[0].size = random16(255); // random particle size in explosion - uint32_t explosionsize = 10 + random(SEGMENT.intensity >> 2, SEGMENT.intensity); + PartSys->sources[0].size = hw_random16(255); // random particle size in explosion + uint32_t explosionsize = 10 + hw_random16(SEGMENT.intensity >> 2, SEGMENT.intensity); for(uint32_t e = 0; e < explosionsize; e++) { //emit explosion particles if(SEGMENT.check2) - PartSys->sources[0].source.hue = random16(); //random color for each particle + PartSys->sources[0].source.hue = hw_random16(); //random color for each particle PartSys->sprayEmit(PartSys->sources[0]); //emit a particle } PartSys->sources[0].source.x = -500; //set out of frame until relaunch @@ -9509,7 +9506,7 @@ uint16_t mode_particleSparkler(void) { PartSys->setParticleSize(SEGMENT.check3); // 1 or 2 pixel rendering for(i = 0; i < numSparklers; i++) { - PartSys->sources[i].source.hue = random16(); //TODO: make adjustable, maybe even colorcycle? + PartSys->sources[i].source.hue = hw_random16(); //TODO: make adjustable, maybe even colorcycle? PartSys->sources[i].var = SEGMENT.intensity >> 4 ; PartSys->sources[i].minLife = 150 + (SEGMENT.intensity >> 1); PartSys->sources[i].maxLife = 200 + SEGMENT.intensity; @@ -9537,7 +9534,7 @@ uint16_t mode_particleSparkler(void) { SEGENV.aux0 = SEGMENT.custom3; for(i = 0; i < numSparklers; i++) { - if(random(255) % (1 + ((255 - SEGMENT.intensity) >> 3)) == 0) + if(hw_random() % (1 + ((255 - SEGMENT.intensity) >> 3)) == 0) PartSys->sprayEmit(PartSys->sources[i]); //emit a particle } @@ -9582,7 +9579,7 @@ uint16_t mode_particleHourglass(void) { uint32_t colormode = SEGMENT.custom1 >> 5; // 0-7 if(SEGMENT.intensity != SEGENV.step) { //initialize - *basehue = random16(); //choose new random color + *basehue = hw_random16(); //choose new random color SEGENV.step = SEGMENT.intensity; for(uint32_t i = 0; i < PartSys->usedParticles; i++) { PartSys->particles[i].reversegrav = true; @@ -9698,7 +9695,7 @@ uint16_t mode_particle1Dspray(void) { int32_t gravity = (int32_t)SEGMENT.custom3 - 15; //gravity setting, 0-14 is negative, 16 - 31 is positive PartSys->setGravity(abs(gravity)); // use reversgrav setting to invert gravity (for proper 'floor' and out of bounce handling) - PartSys->sources[0].source.hue = random16(); //TODO: add colormodes like in hourglass? + PartSys->sources[0].source.hue = hw_random16(); //TODO: add colormodes like in hourglass? PartSys->sources[0].var = 20; PartSys->sources[0].minLife = 200; PartSys->sources[0].maxLife = 400; @@ -9708,7 +9705,7 @@ uint16_t mode_particle1Dspray(void) { if(gravity < 0) PartSys->sources[0].source.reversegrav = true; - if(random(255) % (1 + ((255 - SEGMENT.intensity) >> 3)) == 0) + if(hw_random() % (1 + ((255 - SEGMENT.intensity) >> 3)) == 0) PartSys->sprayEmit(PartSys->sources[0]); //emit a particle //update color settings @@ -9778,7 +9775,7 @@ uint16_t mode_particleBalance(void) { PartSys->applyForce(xgravity); } - uint32_t randomindex = random(PartSys->usedParticles); + uint32_t randomindex = hw_random16(PartSys->usedParticles); PartSys->particles[randomindex].vx = ((int32_t)PartSys->particles[randomindex].vx * 200) / 255; // apply friction to random particle to reduce clumping (without collisions) //update colors @@ -9838,7 +9835,7 @@ uint16_t mode_particleChase(void) { if(SEGMENT.custom2 < 255) PartSys->particles[i].hue = (i * (SEGMENT.custom2 << 3)) / PartSys->usedParticles; // gradient distribution else - PartSys->particles[i].hue = random16(); + PartSys->particles[i].hue = hw_random16(); } SEGENV.aux0 = settingssum; } @@ -9891,7 +9888,7 @@ uint16_t mode_particleChase(void) { if(SEGMENT.custom2 < 255) PartSys->particles[i].hue = PartSys->particles[nextindex].hue - huestep; else - PartSys->particles[i].hue = random16(); + PartSys->particles[i].hue = hw_random16(); } } @@ -9929,19 +9926,19 @@ uint16_t mode_particleStarburst(void) { PartSys->setGravity(SEGMENT.check1 * 8); // enable gravity if (PartSys->sources[0].source.ttl-- == 0) { // stanby time elapsed TODO: make it a timer? - uint32_t explosionsize = 4 + random(SEGMENT.intensity >> 2); - PartSys->sources[0].source.hue = random16(); + uint32_t explosionsize = 4 + hw_random16(SEGMENT.intensity >> 2); + PartSys->sources[0].source.hue = hw_random16(); PartSys->sources[0].var = 10 + (explosionsize << 1); PartSys->sources[0].minLife = 250; PartSys->sources[0].maxLife = 300; - PartSys->sources[0].source.x = random(PartSys->maxX); //random explosion position - PartSys->sources[0].source.ttl = 10 + random16(255 - SEGMENT.speed); + PartSys->sources[0].source.x = hw_random(PartSys->maxX); //random explosion position + PartSys->sources[0].source.ttl = 10 + hw_random16(255 - SEGMENT.speed); PartSys->sources[0].size = SEGMENT.custom1; // Fragment size PartSys->setParticleSize(SEGMENT.custom1); // enable advanced size rendering PartSys->sources[0].source.collide = SEGMENT.check3; for (uint32_t e = 0; e < explosionsize; e++) { // emit particles if (SEGMENT.check2) - PartSys->sources[0].source.hue = random16(); //random color for each particle + PartSys->sources[0].source.hue = hw_random16(); //random color for each particle PartSys->sprayEmit(PartSys->sources[0]); //emit a particle } } @@ -9989,7 +9986,7 @@ uint16_t mode_particle1DGEQ(void) { uint32_t spacing = PartSys->maxX / numSources; for (i = 0; i < numSources; i++) { - PartSys->sources[i].source.hue = i * 16; //random16(); //TODO: make adjustable, maybe even colorcycle? + PartSys->sources[i].source.hue = i * 16; // hw_random16(); //TODO: make adjustable, maybe even colorcycle? PartSys->sources[i].var = SEGMENT.speed >> 3; PartSys->sources[i].minLife = 180 + (SEGMENT.intensity >> 1); PartSys->sources[i].maxLife = 240 + SEGMENT.intensity; @@ -10012,7 +10009,7 @@ uint16_t mode_particle1DGEQ(void) { //map the bands into 16 positions on x axis, emit some particles according to frequency loudness i = 0; - uint32_t bin = random(numSources); //current bin , start with random one to distribute available particles fairly + uint32_t bin = hw_random16(numSources); //current bin , start with random one to distribute available particles fairly uint32_t threshold = 300 - SEGMENT.intensity; for (i = 0; i < numSources; i++) { @@ -10025,7 +10022,7 @@ uint16_t mode_particle1DGEQ(void) { } else if (fftResult[bin] > 0) { // band has low volue uint32_t restvolume = ((threshold - fftResult[bin]) >> 2) + 2; - if (random16() % restvolume == 0) { + if (hw_random() % restvolume == 0) { emitparticle = 1; } } @@ -10064,12 +10061,12 @@ uint16_t mode_particleFire1D(void) { PartSys->setMotionBlur(128 + (SEGMENT.custom2 >> 1)); // enable motion blur PartSys->setColorByAge(true); uint32_t emitparticles = 1; - uint32_t j = random16(); + uint32_t j = hw_random16(); for (uint i = 0; i < 3; i++) { if (PartSys->sources[i].source.ttl > 50) PartSys->sources[i].source.ttl -= 10; // TODO: in 2D making the source fade out slow results in much smoother flames, need to check if it can be done the same else - PartSys->sources[i].source.ttl = 100 + random16(200); // base flame + PartSys->sources[i].source.ttl = 100 + hw_random16(200); // base flame } for (uint i = 0; i < PartSys->numSources; i++) { j = (j + 1) % PartSys->numSources; diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index 1e410a8051..54295b633a 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -1403,13 +1403,13 @@ void WS2812FX::service() { seg.call++; if (seg.isInTransition() && frameDelay > FRAMETIME) frameDelay = FRAMETIME; // force faster updates during transition BusManager::setSegmentCCT(oldCCT); // restore old CCT for ABL adjustments - servicePSmem(_segment_index); // handle segment's particle system memory } seg.next_time = nowUp + frameDelay; } _segment_index++; } + servicePSmem(); // handle segment particle system memory _isServicing = false; _triggered = false; diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index e267795b4d..15a9de0430 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -30,13 +30,13 @@ static void fast_color_scale(CRGB &c, const uint32_t scale); // fast scaling fun // global variables for memory management std::vector partMemList; // list of particle memory pointers -partMem *pmem = nullptr; // pointer to particle memory of current segment, updated in getUpdatedParticlePointer() +partMem *pmem = nullptr; // pointer to particle memory of current segment, updated in particleMemoryManager() CRGB *framebuffer = nullptr; // local frame buffer for rendering CRGB *renderbuffer = nullptr; // local particle render buffer for advanced particles uint16_t frameBufferSize = 0; // size in pixels, used to check if framebuffer is large enough for current segment uint16_t renderBufferSize = 0; // size in pixels, if allcoated by a 1D system it needs to be updated for 2D uint8_t renderSolo = 0; // is set to >0 if this is the only particle system using the so it can use the buffer continuously (faster blurring) -uint8_t globalBlur = 0; // blur to apply if multiple PS are using the buffer +uint8_t globalBlur = 0; // motion blur to apply if multiple PS are using the buffer bool transferflag = false; //DEBUG test !!! do it right ParticleSystem2D::ParticleSystem2D(uint32_t width, uint32_t height, uint32_t numberofparticles, uint32_t numberofsources, bool isadvanced, bool sizecontrol) { @@ -161,6 +161,11 @@ void ParticleSystem2D::setMotionBlur(uint8_t bluramount) { motionBlur = bluramount; } +void ParticleSystem2D::setSmearBlur(uint8_t bluramount) { + smearBlur = bluramount; +} + + // render size using smearing (see blur function) void ParticleSystem2D::setParticleSize(uint8_t size) { particlesize = size; @@ -195,14 +200,14 @@ int32_t ParticleSystem2D::sprayEmit(PSsource &emitter, uint32_t amount) { emitIndex = 0; if (particles[emitIndex].ttl == 0) { // find a dead particle success = true; - particles[emitIndex].vx = emitter.vx + random16(emitter.var << 1) - emitter.var; // random(-var, var) - particles[emitIndex].vy = emitter.vy + random16(emitter.var << 1) - emitter.var; // random(-var, var) + particles[emitIndex].vx = emitter.vx + hw_random16(emitter.var << 1) - emitter.var; // random(-var, var) + particles[emitIndex].vy = emitter.vy + hw_random16(emitter.var << 1) - emitter.var; // random(-var, var) particles[emitIndex].x = emitter.source.x; particles[emitIndex].y = emitter.source.y; particles[emitIndex].hue = emitter.source.hue; particles[emitIndex].sat = emitter.source.sat; particles[emitIndex].collide = emitter.source.collide; - particles[emitIndex].ttl = random16(emitter.minLife, emitter.maxLife); + particles[emitIndex].ttl = hw_random16(emitter.minLife, emitter.maxLife); if (advPartProps) advPartProps[emitIndex].size = emitter.size; break; @@ -411,7 +416,7 @@ void ParticleSystem2D::bounce(int8_t &incomingspeed, int8_t ¶llelspeed, int3 int32_t incomingspeed_abs = abs((int32_t)incomingspeed); int32_t totalspeed = incomingspeed_abs + abs((int32_t)parallelspeed); // transfer an amount of incomingspeed speed to parallel speed - int32_t donatespeed = ((random16(incomingspeed_abs << 1) - incomingspeed_abs) * (int32_t)wallRoughness) / (int32_t)255; // take random portion of + or - perpendicular speed, scaled by roughness + int32_t donatespeed = ((hw_random16(incomingspeed_abs << 1) - incomingspeed_abs) * (int32_t)wallRoughness) / (int32_t)255; // take random portion of + or - perpendicular speed, scaled by roughness parallelspeed = limitSpeed((int32_t)parallelspeed + donatespeed); // give the remainder of the speed to perpendicular speed donatespeed = int8_t(totalspeed - abs(parallelspeed)); // keep total speed the same @@ -566,11 +571,11 @@ void ParticleSystem2D::ParticleSys_render(bool firemode, uint32_t fireintensity) if (framebuffer) { // update global blur (used for blur transitions) int32_t bluramount = motionBlur; - if(pmem->inTransition) { // FX transition, fade blur amount or skip setting new blur if particlesize is used - if(pmem->inTransition == effectID) { // FX transition and new FX: fade blur amount + // if(pmem->inTransition) { // FX transition, fade blur amount or skip setting new blur if particlesize is used + if(pmem->inTransition == effectID) { // FX transition and this is the new FX: fade blur amount bluramount = globalBlur + (((bluramount - globalBlur) * (int)SEGMENT.progress()) >> 16); // fade from old blur to new blur during transitions } - } + // } globalBlur = bluramount; /* @@ -686,6 +691,15 @@ void ParticleSystem2D::ParticleSys_render(bool firemode, uint32_t fireintensity) } } + // apply 2D blur to rendered frame TODO: this needs proper transition handling, maybe combine it with motion blur handling? + if(smearBlur > 0) { + if (framebuffer) + blur2D(framebuffer, maxXpixel + 1, maxYpixel + 1, smearBlur, smearBlur); + else + SEGMENT.blur(smearBlur, true); + } + + // transfer framebuffer to segment if available if (framebuffer && pmem->inTransition != effectID) { // not in transition or is old FX TODO: how to handle this for overlay rendering or multiple segment rendering? need to check for rendersolo as well? #ifndef WLED_DISABLE_MODE_BLEND @@ -1050,8 +1064,8 @@ void ParticleSystem2D::collideParticles(PSparticle *particle1, PSparticle *parti void ParticleSystem2D::updateSystem(void) { PSPRINTLN("updateSystem2D"); - uint32_t cols = SEGMENT.virtualWidth(); // update matrix size - uint32_t rows = SEGMENT.virtualHeight(); + uint32_t cols = SEGMENT.vWidth(); // update matrix size + uint32_t rows = SEGMENT.vHeight(); setMatrixSize(cols, rows); updatePSpointers(advPartProps != nullptr, advPartSize != nullptr); // update pointers to PS data, also updates availableParticles setUsedParticles(usedpercentage); // update used particles based on percentage TODO: this does not need to be called for each frame, it only changes during transitions. can optimize? @@ -1121,7 +1135,7 @@ PSPRINTLN("updatePSpointers"); else { PSdataEnd = reinterpret_cast(sources + numSources); // pointer to first available byte after the PS for FX additional data } - particles = reinterpret_cast(getUpdatedParticlePointer(0, sizeof(PSparticle), availableParticles, usedParticles, usedpercentage, effectID)); // get memory, leave buffer size as is (request 0) + particles = reinterpret_cast(particleMemoryManager(0, sizeof(PSparticle), availableParticles, effectID)); // get memory, leave buffer size as is (request 0) /* DEBUG_PRINTF_P(PSTR(" particles %p "), particles); DEBUG_PRINTF_P(PSTR(" sources %p "), sources); @@ -1185,14 +1199,12 @@ void blur2D(CRGB *colorbuffer, uint32_t xsize, uint32_t ysize, uint32_t xblur, u //non class functions to use for initialization uint32_t calculateNumberOfParticles2D(uint32_t pixels, bool isadvanced, bool sizecontrol) { + uint32_t numberofParticles = pixels; // 1 particle per pixel (for example 512 particles on 32x16) #ifdef ESP8266 - uint32_t numberofParticles = 1 + (pixels * 3) / 4; // 0.75 particle per pixel, ensure a minimum of 1 uint32_t particlelimit = ESP8266_MAXPARTICLES; // maximum number of paticles allowed (based on one segment of 16x16 and 4k effect ram) #elif ARDUINO_ARCH_ESP32S2 - uint32_t numberofParticles = (pixels); // 1 particle per pixel uint32_t particlelimit = ESP32S2_MAXPARTICLES; // maximum number of paticles allowed (based on one segment of 32x32 and 24k effect ram) #else - uint32_t numberofParticles = pixels; // 1 particle per pixel (for example 512 particles on 32x16) uint32_t particlelimit = ESP32_MAXPARTICLES; // maximum number of paticles allowed (based on two segments of 32x32 and 40k effect ram) #endif numberofParticles = max((uint32_t)4, min(numberofParticles, particlelimit)); @@ -1227,8 +1239,7 @@ bool allocateParticleSystemMemory2D(uint32_t numparticles, uint32_t numsources, PSPRINTLN("PS 2D alloc"); uint32_t requiredmemory = sizeof(ParticleSystem2D); uint32_t availableparticles; // dummy variable - uint32_t usedparticles = 0; // dummy variable - if((getUpdatedParticlePointer(numparticles, sizeof(PSparticle), availableparticles, usedparticles, 0, SEGMENT.mode)) == nullptr) // allocate memory for particles + if((particleMemoryManager(numparticles, sizeof(PSparticle), availableparticles, SEGMENT.mode)) == nullptr) // allocate memory for particles return false; // not enough memory, function ensures a minimum of numparticles are available // functions above make sure these are a multiple of 4 bytes (to avoid alignment issues) @@ -1424,12 +1435,12 @@ int32_t ParticleSystem1D::sprayEmit(PSsource1D &emitter) { if (emitIndex >= usedParticles) emitIndex = 0; if (particles[emitIndex].ttl == 0) { // find a dead particle - particles[emitIndex].vx = emitter.v + random16(emitter.var << 1) - emitter.var; // random(-var,var) + particles[emitIndex].vx = emitter.v + hw_random16(emitter.var << 1) - emitter.var; // random(-var,var) particles[emitIndex].x = emitter.source.x; particles[emitIndex].hue = emitter.source.hue; particles[emitIndex].collide = emitter.source.collide; particles[emitIndex].reversegrav = emitter.source.reversegrav; - particles[emitIndex].ttl = random16(emitter.minLife, emitter.maxLife); + particles[emitIndex].ttl = hw_random16(emitter.minLife, emitter.maxLife); if (advPartProps) { advPartProps[emitIndex].sat = emitter.sat; advPartProps[emitIndex].size = emitter.size; @@ -1809,7 +1820,7 @@ void ParticleSystem1D::collideParticles(PSparticle1D *particle1, PSparticle1D *p // update size and pointers (memory location and size can change dynamically) // note: do not access the PS class in FX befor running this function (or it messes up SEGENV.data) void ParticleSystem1D::updateSystem(void) { - setSize(SEGMENT.virtualLength()); // update size + setSize(SEGMENT.vLength()); // update size updatePSpointers(advPartProps != NULL); setUsedParticles(usedpercentage); // update used particles based on percentage TODO: this does not need to be called for each frame, it only changes during transitions. can optimize? how to update after transition is finished? if (partMemList.size() == 1 && !SEGMENT.isInTransition()) // if number of vector elements is one, this is the only system @@ -1826,7 +1837,7 @@ void ParticleSystem1D::updatePSpointers(bool isadvanced) { // a pointer MUST be 4 byte aligned. sizeof() in a struct/class is always aligned to the largest element. if it contains a 32bit, it will be padded to 4 bytes, 16bit is padded to 2byte alignment. // The PS is aligned to 4 bytes, a PSparticle is aligned to 2 and a struct containing only byte sized variables is not aligned at all and may need to be padded when dividing the memoryblock. // by making sure that the number of sources and particles is a multiple of 4, padding can be skipped here as alignent is ensured, independent of struct sizes. - particles = reinterpret_cast(getUpdatedParticlePointer(0, sizeof(PSparticle1D), availableParticles, usedParticles, usedpercentage, effectID)); // get memory, leave buffer size as is (request 0) + particles = reinterpret_cast(particleMemoryManager(0, sizeof(PSparticle1D), availableParticles, effectID)); // get memory, leave buffer size as is (request 0) sources = reinterpret_cast(this + 1); // pointer to source(s) PSdataEnd = reinterpret_cast(sources + numSources); // pointer to first available byte after the PS for FX additional data if (isadvanced) { @@ -1872,8 +1883,7 @@ uint32_t calculateNumberOfSources1D(uint32_t requestedsources) { bool allocateParticleSystemMemory1D(uint32_t numparticles, uint32_t numsources, bool isadvanced, uint32_t additionalbytes) { uint32_t requiredmemory = sizeof(ParticleSystem1D); uint32_t availableparticles; // dummy variable - uint32_t usedparticles = 0; // dummy variable - if(getUpdatedParticlePointer(numparticles, sizeof(PSparticle1D), availableparticles, usedparticles, 0, 0) == nullptr) // allocate memory for particles + if(particleMemoryManager(numparticles, sizeof(PSparticle1D), availableparticles, SEGMENT.mode) == nullptr) // allocate memory for particles return false; // not enough memory, function ensures a minimum of numparticles are avialable // functions above make sure these are a multiple of 4 bytes (to avoid alignment issues) if (isadvanced) @@ -2067,7 +2077,7 @@ ist also in transition und es ist der neue effekt und der watchdog ist auf null // handle particle pointer, creates/extends buffer if needed and handles transition handover // function is called in PS setup and updatepointer function -void* getUpdatedParticlePointer(const uint32_t requestedParticles, size_t structSize, uint32_t &availableToPS, uint32_t &usedbyPS, const uint8_t percentused, const uint8_t effectID) { // TODO: usedbyPS and percentused are currently unused, can remove if not required for transition +void* particleMemoryManager(const uint32_t requestedParticles, size_t structSize, uint32_t &availableToPS, const uint8_t effectID) { pmem = getPartMem(); PSPRINT(" getParticlePointer "); void* buffer = nullptr; @@ -2075,6 +2085,7 @@ void* getUpdatedParticlePointer(const uint32_t requestedParticles, size_t struct PSPRINT(" buffer found "); if (requestedParticles) { // request for a new buffer, this is an init call PSPRINT(" requested particles: " + String(requestedParticles)); + pmem->transferParticles = true; // set flag to transfer particles uint32_t requestsize = structSize * requestedParticles; // required buffer size uint32_t currentsize = pmem->numParticles * pmem->sizeOfParticle; if (requestsize > currentsize) { // request is larger than buffer, try to extend it @@ -2093,7 +2104,7 @@ void* getUpdatedParticlePointer(const uint32_t requestedParticles, size_t struct if (pmem->watchdog == 1) { // if a PS already exists during particle request, it kicked the watchdog in last frame, servicePSmem() adds 1 afterwards -> PS to PS transition PSPRINTLN("********** PS is in transition, new FX:" + String(effectID)); //Serial.print(" inTransition = " + String(pmem->inTransition)); - if(pmem->inTransition) // there is already a transition going on, multi transitions lead to weird behaviour (missin particles, missing transitions) + if(pmem->inTransition) // there is already a transition going on, multi transitions lead to weird behaviour (missing particles, missing transitions) pmem->inTransition = 1; // set to an invalid FX number so transitions work (the calling "new FX" is recognized as the old FX) TODO: this is a dirty hack and needs fixing but there is a bug in segment handling, not copying the segment.data... else pmem->inTransition = effectID; // save the ID of the new effect (required to determine blur amount in rendering function, it will not work without this so dont optimize) @@ -2102,14 +2113,7 @@ void* getUpdatedParticlePointer(const uint32_t requestedParticles, size_t struct // availableToPS = requestedParticles; // all particles are available todo: THIS IS TBD, probably ok to feed particles slowly // pmem->inTransition = false; // no transition TODO: does this need to be set here? this function is called again in updatePS, can deactivate it then, right? } - - availableToPS = 2; // only give 2 particles to a new PS so old particles keep their settings and are not reeinitialized - PSPRINT(" available to NEW PS: "); - PSPRINT(availableToPS); - PSPRINT(" ,used pcnt: "); - PSPRINT(percentused); - PSPRINT(" ,used abs: "); - PSPRINTLN(usedbyPS); + availableToPS = 0; // start out with zero particles, transition below will initialize and transfer tehm return pmem->particleMemPointer; // return the available buffer on init call TODO: maybe split this into two functions, one for init and one for get? } PSPRINT(" use existing, "); @@ -2131,8 +2135,8 @@ void* getUpdatedParticlePointer(const uint32_t requestedParticles, size_t struct free(buffer); return nullptr; } - // todo: need to initialize the first particles to default values? or just return 0 particles and let the transition below initialize? - availableToPS = 2; + pmem->transferParticles = true; // set flag to transfer particles + availableToPS = 0; // new PS starts with zero particles, they are transferred in the next call return buffer; // directly return the buffer on init call } #ifdef WLED_DEBUG_PS @@ -2143,7 +2147,7 @@ void* getUpdatedParticlePointer(const uint32_t requestedParticles, size_t struct // now we have a valid buffer, check if we are in transition (pmem->inTransition is still true for one frame after transition is finished to allow for transfer of remaining particles) if (SEGMENT.isInTransition() || pmem->inTransition) { bool effectchanged = (SEGMENT.currentMode() != SEGMENT.mode); // FX changed, transition the particle buffer - if (effectchanged || pmem->inTransition) { // transfer particles to the new system, starting from the end of the buffer (old one loses particles at the end, new one gets pointer from near the end) TODO: if new available > old avialable (system increased) need to not update numparticles until enough are transferred or FX can jump in particle count + if (effectchanged || pmem->transferParticles) { PSPRINT(" FX changed "); PSPRINT(" this mode: " + String(effectID)); PSPRINT("/ oldmode: " + String(SEGMENT.currentMode())); @@ -2165,31 +2169,13 @@ void* getUpdatedParticlePointer(const uint32_t requestedParticles, size_t struct //Serial.println((newbrfaddr-brforigin)/structSize); PSPRINT(" particle start: " + String(pmem->numParticles - newAvailable)); #endif - uint32_t transferstartidx = 0; // start at beginning of new buffer pointer + uint32_t transferstartidx = 0; // start at beginning of new buffer pointer TODO: not used anymore, alway zero, can be removed. uint32_t totransfer = newAvailable - availableToPS; // number of particles to transfer in this transition update //TODO: maybe memcopy the buffer? if usedparticle number is small, end of the buffer holds alive but unused particles... copy would erase old particles though. need to think about a good way to do it. // initialize newly transferred particles note: to have PS interact during transition, this must be changed. could initialize TTL and perpetual only TODO: need to be more clever: need to clear buffer if 2D->1D or 1D->2D transition or settings are nonsensical. PSPRINT(" totransfer: " + String(totransfer)); if(totransfer <= newAvailable) { // overflow check TODO: why do overflows happen? does it still happen with the new calculation? -> not in normal transfer, need to check quick transfer changes -> seems ok TODO: can this be removed? - if(structSize == sizeof(PSparticle)) { // 2D particle - PSparticle *particles = (PSparticle*)buffer; - for (uint32_t i = transferstartidx; i < transferstartidx + totransfer; i++) { - particles[i].perpetual = false; // particle ages - if(particles[i].outofbounds) particles[i].ttl = 0; // kill out of bounds - else if(particles[i].ttl > 200) particles[i].ttl = 200; // reduce TTL so it will die soon - //else if(particles[i].ttl) particles[i].ttl += 100; // !!! debug - particles[i].sat = 255; // full saturation - particles[i].collide = true; // enable collisions (in case new FX uses them) - } - } - else { // 1D particle system - PSparticle1D *particles = (PSparticle1D*)buffer; - for (uint32_t i = transferstartidx; i < transferstartidx + totransfer; i++) { - particles[i].perpetual = false; // particle ages - if(particles[i].outofbounds) particles[i].ttl = 0; // kill out of bounds - else if(particles[i].ttl > 50) particles[i].ttl = 50; // reduce TTL so it will die soon - } - } + particleHandover(buffer, structSize, totransfer); } else { PSPRINTLN(" Particle transfer overflow! "); @@ -2215,12 +2201,12 @@ void* getUpdatedParticlePointer(const uint32_t requestedParticles, size_t struct PSPRINT(" final available to PS: " + String(availableToPS)); // TODO: if this is correct, the first if statement can be removed and availabletoPS updated here or: this part of the function can be optimized still } - /* else { // same effect transition PSPRINT(" same FX "); - availableToPS = pmem->numParticles; // no transition, full buffer available - // pmem->inTransition = false; - }*/ + availableToPS = pmem->numParticles; // no transition, full buffer available TODO: need to restrict to segment size? test this with different size but same FX (large to small) + pmem->inTransition = false; + } + if(!SEGMENT.isInTransition()) { // transition ended, cleanup pmem->inTransition = false; PSPRINTLN(" ** "); @@ -2228,15 +2214,57 @@ void* getUpdatedParticlePointer(const uint32_t requestedParticles, size_t struct // Transfer the last buffer state in PS before rendering transferflag = true; } - } else { // no PS transition, full buffer available TODO: need to check if previously not full buffer was available and transfer the rest of the particles in a clever, code saving way, maybe make a transfer function to handle this - availableToPS = pmem->numParticles; // no transition, full buffer available + } else { // no transition, full buffer available + if(pmem->transferParticles) { // transition ended (or blending is disabled) -> transfer all remaining particles + uint32_t numPixels = SEGMENT.vWidth() * SEGMENT.vHeight(); // works for 1D and 2D, height is 1 in 1D + uint32_t finalAvailable = min(pmem->numParticles, numPixels); // limit to segment size in case a larger segment initialized the buffer + if (finalAvailable > availableToPS) { // not all particles transferred yet + uint32_t totransfer = finalAvailable - availableToPS; // number of particles to transfer in this transition update + particleHandover(buffer, structSize, totransfer); + } + availableToPS = finalAvailable; // update available particles + pmem->transferParticles = false; + } PSPRINTLN(" no trans, no of partcls: " + String(availableToPS)); pmem->inTransition = false; } PSPRINTLN(" END getPartPointer "); + return buffer; } +// (re)initialize particles in the particle buffer for use in the new FX +void particleHandover(void *buffer, size_t structSize, uint32_t numToTransfer) +{ + if (structSize == sizeof(PSparticle)) + { // 2D particle + PSparticle *particles = (PSparticle *)buffer; + for (uint32_t i = 0; i < numToTransfer; i++) + { + particles[i].perpetual = false; // particle ages + if (particles[i].outofbounds) + particles[i].ttl = 0; // kill out of bounds + else if (particles[i].ttl > 200) + particles[i].ttl = 200; // reduce TTL so it will die soon + // else if(particles[i].ttl) particles[i].ttl += 100; // !!! debug test + particles[i].sat = 255; // full saturation + particles[i].collide = true; // enable collisions (in case new FX uses them) + } + } + else + { // 1D particle system + PSparticle1D *particles = (PSparticle1D *)buffer; + for (uint32_t i = 0; i < numToTransfer; i++) + { + particles[i].perpetual = false; // particle ages + if (particles[i].outofbounds) + particles[i].ttl = 0; // kill out of bounds + else if (particles[i].ttl > 50) + particles[i].ttl = 50; // reduce TTL so it will die soon + } + } +} + // function to update the framebuffer and renderbuffer void updateRenderingBuffer(CRGB* buffer, uint32_t requiredpixels, bool isFramebuffer) { PSPRINTLN("updateRenderingBuffer"); @@ -2278,26 +2306,25 @@ partMem* getPartMem(void) { // TODO: maybe there is a better/faster way than usi // service the particle system memory, free memory if idle too long // note: doing it this way makes it independent of the implementation of segment management but is not the most memory efficient way -void servicePSmem(uint8_t idx) { - // Increment watchdog for each entry and deallocate if idle too long (i.e. no PS running on the segment) +void servicePSmem() { + // Increment watchdog for each entry and deallocate if idle too long (i.e. no PS running on that segment) if(partMemList.size() > 0) { for (size_t i = 0; i < partMemList.size(); i++) { - if(partMemList[i].id == idx) - { - partMemList[i].watchdog++; // Increment watchdog counter - PSPRINT("pmem servic. list size: "); - PSPRINT(partMemList.size()); - PSPRINT(" element: "); - PSPRINT(i); - PSPRINT(" watchdog: "); - PSPRINTLN(partMemList[i].watchdog); - if (partMemList[i].watchdog > MAX_MEMIDLE) { - PSPRINTLN("psmem free"); //deallocating memory:2560 - deallocatePSmemory(partMemList[i].particleMemPointer, partMemList[i].numParticles * partMemList[i].sizeOfParticle); // Free memory - partMemList.erase(partMemList.begin() + i); // Remove entry - partMemList.shrink_to_fit(); // partMemList is small, memory operations should be unproblematic - } - break; + if(strip.getSegmentsNum() > i) { // segment still exists + if(strip._segments[i].freeze) continue; // skip frozen segments (incrementing watchdog will delete memory, leading to crash) + } + partMemList[i].watchdog++; // Increment watchdog counter + PSPRINT("pmem servic. list size: "); + PSPRINT(partMemList.size()); + PSPRINT(" element: "); + PSPRINT(i); + PSPRINT(" watchdog: "); + PSPRINTLN(partMemList[i].watchdog); + if (partMemList[i].watchdog > MAX_MEMIDLE) { + PSPRINTLN("psmem free"); //deallocating memory:2560 + deallocatePSmemory(partMemList[i].particleMemPointer, partMemList[i].numParticles * partMemList[i].sizeOfParticle); // Free memory + partMemList.erase(partMemList.begin() + i); // Remove entry + partMemList.shrink_to_fit(); // partMemList is small, memory operations should be unproblematic } } } diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index 0bce9b346e..555bdedbcd 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -14,7 +14,7 @@ #include "wled.h" #define PS_P_MAXSPEED 120 // maximum speed a particle can have (vx/vy is int8) -#define MAX_MEMIDLE 200 // max idle time (in frames) before memory is deallocated (if deallocated during an effect, it will crash!) note: setting this to a high 8bit value prevents fragmentation +#define MAX_MEMIDLE 50 // max idle time (in frames) before memory is deallocated (if deallocated during an effect, it will crash!) //#define WLED_DEBUG_PS @@ -32,12 +32,13 @@ struct partMem { uint32_t numParticles; // number of particles that fit in memory note: could be a uint16_t but padding will increase the struct size so 12 bytes anyway uint8_t sizeOfParticle; // size of the particle struct in this buffer uint8_t id; // ID of segment this memory belongs to - uint8_t inTransition; // to track transitions uint8_t watchdog; // counter to handle deallocation + uint8_t inTransition; // to track transitions (is set to new FX ID during transitions) + bool transferParticles; // if set, particles in buffer are transferred to new FX }; - -void* getUpdatedParticlePointer(const uint32_t requestedParticles, size_t structSize, uint32_t &availableToPS, uint32_t &usedbyPS, const uint8_t percentused, const uint8_t effectID); // update particle memory pointer, handles transitions +void* particleMemoryManager(const uint32_t requestedParticles, size_t structSize, uint32_t &availableToPS, const uint8_t effectID); // update particle memory pointer, handles transitions +void particleHandover(void *buffer, size_t structSize, uint32_t numParticles); //extern CRGB *framebuffer; // local frame buffer for rendering //extern CRGB *renderbuffer; // local particle render buffer for advanced particles //extern uint16_t frameBufferSize; // size in pixels, used to check if framebuffer is large enough for current segment TODO: make this in bytes, not in pixels @@ -46,7 +47,7 @@ partMem* getPartMem(void); // returns pointer to memory struct for current segme void updateRenderingBuffer(CRGB* buffer, uint32_t requiredsize, bool isFramebuffer); // allocate CRGB rendering buffer, update size if needed void transferBuffer(uint32_t width, uint32_t height); // transfer the buffer to the segment //TODO: add 1D version -void servicePSmem(uint8_t idx); // increments watchdog, frees memory if idle too long +void servicePSmem(); // increments watchdog, frees memory if idle too long // update number of particles to use, must never be more than allocated (= particles allocated by the calling system) inline void updateUsedParticles(const uint32_t allocated, const uint32_t available, const uint8_t percentage, uint32_t &used) { @@ -180,6 +181,7 @@ class ParticleSystem2D { void setSaturation(uint8_t sat); // set global color saturation void setColorByAge(bool enable); void setMotionBlur(uint8_t bluramount); // note: motion blur can only be used if 'particlesize' is set to zero + void setSmearBlur(uint8_t bluramount); // enable 2D smeared blurring of full frame void setParticleSize(uint8_t size); void setGravity(int8_t force = 8); void enableParticleCollisions(bool enable, uint8_t hardness = 255); @@ -226,7 +228,8 @@ class ParticleSystem2D { int8_t gforce; // gravity strength, default is 8 (negative is allowed, positive is downwards) // global particle properties for basic particles uint8_t particlesize; // global particle size, 0 = 2 pixels, 255 = 10 pixels (note: this is also added to individual sized particles) - uint8_t motionBlur; // enable motion blur, values > 100 gives smoother animations. Note: motion blurring does not work if particlesize is > 0 + uint8_t motionBlur; // motion blur, values > 100 gives smoother animations. Note: motion blurring does not work if particlesize is > 0 + uint8_t smearBlur; // 2D smeared blurring of full frame uint8_t effectID; // ID of the effect that is using this particle system, used for transitions uint8_t usedpercentage; // percentage of particles used in the system, used during transition updates }; @@ -244,12 +247,12 @@ bool allocateParticleSystemMemory2D(uint32_t numparticles, uint32_t numsources, //////////////////////// #ifndef WLED_DISABLE_PARTICLESYSTEM1D // memory allocation -#define ESP8266_MAXPARTICLES_1D 400 -#define ESP8266_MAXSOURCES_1D 8 -#define ESP32S2_MAXPARTICLES_1D 1900 -#define ESP32S2_MAXSOURCES_1D 16 -#define ESP32_MAXPARTICLES_1D 6000 -#define ESP32_MAXSOURCES_1D 32 +#define ESP8266_MAXPARTICLES_1D 450 +#define ESP8266_MAXSOURCES_1D 16 +#define ESP32S2_MAXPARTICLES_1D 1600 +#define ESP32S2_MAXSOURCES_1D 32 +#define ESP32_MAXPARTICLES_1D 3200 +#define ESP32_MAXSOURCES_1D 64 // particle dimensions (subpixel division) #define PS_P_RADIUS_1D 32 // subpixel size, each pixel is divided by this for particle movement, if this value is changed, also change the shift defines (next two lines) @@ -275,7 +278,7 @@ typedef union { byte asByte; // access as a byte, order is: LSB is first entry in the list above } PSsettings1D; -//struct for a single particle (6 bytes) +//struct for a single particle (7 bytes) typedef struct { int16_t x; // x position in particle system int8_t vx; // horizontal velocity From eeed168a6d7d3ccc3d45e72fbfec023121d4dfb8 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Thu, 26 Dec 2024 19:02:27 +0100 Subject: [PATCH 146/219] implemented correct 1D<->2D buffer handover+bugfixes work in progress --- wled00/FX.cpp | 2 +- wled00/FXparticleSystem.cpp | 208 ++++++++++++++++-------------------- wled00/FXparticleSystem.h | 6 +- 3 files changed, 96 insertions(+), 120 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 83af458693..81e3876188 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -8219,7 +8219,7 @@ uint16_t mode_particlewaterfall(void) { PartSys->update(); // update and render return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEWATERFALL[] PROGMEM = "PS Waterfall@Speed,Intensity,Variation,Collisions,Position,Cylinder,Walls,Ground;;!;2;pal=9,sx=15,ix=200,c1=64,c2=160,c3=17,o1=0,o2=0,o3=1"; +static const char _data_FX_MODE_PARTICLEWATERFALL[] PROGMEM = "PS Waterfall@Speed,Intensity,Variation,Collisions,Position,Cylinder,Walls,Ground;;!;2;pal=9,sx=15,ix=200,c1=32,c2=160,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) diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index 15a9de0430..bd408a8fdc 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -37,7 +37,6 @@ uint16_t frameBufferSize = 0; // size in pixels, used to check if framebuffer is uint16_t renderBufferSize = 0; // size in pixels, if allcoated by a 1D system it needs to be updated for 2D uint8_t renderSolo = 0; // is set to >0 if this is the only particle system using the so it can use the buffer continuously (faster blurring) uint8_t globalBlur = 0; // motion blur to apply if multiple PS are using the buffer -bool transferflag = false; //DEBUG test !!! do it right ParticleSystem2D::ParticleSystem2D(uint32_t width, uint32_t height, uint32_t numberofparticles, uint32_t numberofsources, bool isadvanced, bool sizecontrol) { effectID = SEGMENT.mode; // new FX called init, save the effect ID @@ -566,7 +565,7 @@ void ParticleSystem2D::ParticleSys_render(bool firemode, uint32_t fireintensity) CRGB baseRGB; uint32_t brightness; // particle brightness, fades if dying static bool useAdditiveTransfer = false; // use add instead of set for buffer transferring - if(!pmem->inTransition) useAdditiveTransfer = false; // additive rendering is only used in transitions + if(!pmem->inTransition) useAdditiveTransfer = false; // additive rendering is only used in PS FX transitions // handle blurring and framebuffer update if (framebuffer) { // update global blur (used for blur transitions) @@ -698,7 +697,6 @@ void ParticleSystem2D::ParticleSys_render(bool firemode, uint32_t fireintensity) else SEGMENT.blur(smearBlur, true); } - // transfer framebuffer to segment if available if (framebuffer && pmem->inTransition != effectID) { // not in transition or is old FX TODO: how to handle this for overlay rendering or multiple segment rendering? need to check for rendersolo as well? @@ -1249,6 +1247,7 @@ bool allocateParticleSystemMemory2D(uint32_t numparticles, uint32_t numsources, requiredmemory += sizeof(PSsizeControl) * numparticles; requiredmemory += sizeof(PSsource) * numsources; requiredmemory += additionalbytes; + PSPRINTLN("mem alloc: " + String(requiredmemory)); return(SEGMENT.allocateData(requiredmemory)); } @@ -2087,27 +2086,22 @@ void* particleMemoryManager(const uint32_t requestedParticles, size_t structSize PSPRINT(" requested particles: " + String(requestedParticles)); pmem->transferParticles = true; // set flag to transfer particles uint32_t requestsize = structSize * requestedParticles; // required buffer size - uint32_t currentsize = pmem->numParticles * pmem->sizeOfParticle; - if (requestsize > currentsize) { // request is larger than buffer, try to extend it - if (Segment::getUsedSegmentData() + requestsize - currentsize <= MAX_SEGMENT_DATA) { // enough memory available to extend buffer + if (requestsize > pmem->buffersize) { // request is larger than buffer, try to extend it + if (Segment::getUsedSegmentData() + requestsize - pmem->buffersize <= MAX_SEGMENT_DATA) { // enough memory available to extend buffer PSPRINT(" extending particle buffer "); - buffer = allocatePSmemory(structSize * requestedParticles, true); // calloc new memory in FX data, override limit (temporary buffer) + buffer = allocatePSmemory(requestsize, true); // calloc new memory in FX data, override limit (temporary buffer) if (buffer) { // allocaction successful, copy old particles to new buffer - memcpy(buffer, pmem->particleMemPointer, currentsize); // copy old particle buffer note: only required if transition but copy is fast and rarely happens - deallocatePSmemory( pmem->particleMemPointer, currentsize); // free old memory + memcpy(buffer, pmem->particleMemPointer, pmem->buffersize); // copy old particle buffer note: only required if transition but copy is fast and rarely happens + deallocatePSmemory( pmem->particleMemPointer, pmem->buffersize); // free old memory pmem->particleMemPointer = buffer; // set new buffer - pmem->numParticles = requestedParticles; // update number of particles - pmem->sizeOfParticle = structSize; // update memory size of a particle + pmem->buffersize = requestsize; // update buffer size } // if buffer was not extended, the old, smaller buffer is used } } if (pmem->watchdog == 1) { // if a PS already exists during particle request, it kicked the watchdog in last frame, servicePSmem() adds 1 afterwards -> PS to PS transition PSPRINTLN("********** PS is in transition, new FX:" + String(effectID)); - //Serial.print(" inTransition = " + String(pmem->inTransition)); - if(pmem->inTransition) // there is already a transition going on, multi transitions lead to weird behaviour (missing particles, missing transitions) - pmem->inTransition = 1; // set to an invalid FX number so transitions work (the calling "new FX" is recognized as the old FX) TODO: this is a dirty hack and needs fixing but there is a bug in segment handling, not copying the segment.data... - else - pmem->inTransition = effectID; // save the ID of the new effect (required to determine blur amount in rendering function, it will not work without this so dont optimize) + PSPRINTLN(" inTransition = " + String(pmem->inTransition)); + pmem->inTransition = effectID; // save the ID of the new effect (required to determine blur amount in rendering function) } else { // no watchdog, this is a new PS // availableToPS = requestedParticles; // all particles are available todo: THIS IS TBD, probably ok to feed particles slowly @@ -2122,12 +2116,13 @@ void* particleMemoryManager(const uint32_t requestedParticles, size_t structSize } else { // if the id was not found create a buffer and add an element to the list PSPRINT(" allocating particle buffer "); - buffer = allocatePSmemory(structSize * requestedParticles, false); // allocate new memory + uint32_t requestsize = structSize * requestedParticles; // required buffer size + buffer = allocatePSmemory(requestsize, false); // allocate new memory if (buffer) PSPRINTLN(" bfr allocated"); else PSPRINTLN(" brf alloc failed"); if (buffer) - partMemList.push_back({buffer, (uint16_t)requestedParticles, (uint8_t)structSize, strip.getCurrSegmentId(), 0, 0}); // add buffer to list note: if pushback fails, it may crash + partMemList.push_back({buffer, requestsize, 0, strip.getCurrSegmentId(), 0, 0, true}); // add buffer to list, set flag to transfer/init the particles note: if pushback fails, it may crash else return nullptr; // there is no memory available !!! TODO: if localbuffer is allocated, free it and try again, its no use having a buffer but no particles pmem = getPartMem(); // get the pointer to the new element (check that it was added) @@ -2135,7 +2130,6 @@ void* particleMemoryManager(const uint32_t requestedParticles, size_t structSize free(buffer); return nullptr; } - pmem->transferParticles = true; // set flag to transfer particles availableToPS = 0; // new PS starts with zero particles, they are transferred in the next call return buffer; // directly return the buffer on init call } @@ -2144,88 +2138,74 @@ void* particleMemoryManager(const uint32_t requestedParticles, size_t structSize Serial.println((uintptr_t)buffer, HEX); #endif - // now we have a valid buffer, check if we are in transition (pmem->inTransition is still true for one frame after transition is finished to allow for transfer of remaining particles) - if (SEGMENT.isInTransition() || pmem->inTransition) { - bool effectchanged = (SEGMENT.currentMode() != SEGMENT.mode); // FX changed, transition the particle buffer - if (effectchanged || pmem->transferParticles) { - PSPRINT(" FX changed "); - PSPRINT(" this mode: " + String(effectID)); - PSPRINT("/ oldmode: " + String(SEGMENT.currentMode())); - PSPRINTLN(" newmode: " + String(SEGMENT.mode)); - uint32_t progress = SEGMENT.progress(); // transition progress - uint32_t newAvailable = 0; - if (SEGMENT.mode == effectID) { // new effect ID -> function was called from new FX - PSPRINT(" new FX"); - PSPRINT(" progress: " + String(progress)); - newAvailable = (pmem->numParticles * (progress + 1)) >> 16; // update total particles available to this PS - if(newAvailable < 2) newAvailable = 2; // always give a minimum amount (this can lead to overlap, currently not a problem but some new FX may not like it) - //uint32_t brforigin = (uintptr_t)buffer; // save old buffer pointer for !!!!DEBUG - buffer = (void*)((uint8_t*)buffer + (pmem->numParticles - newAvailable) * structSize); // new effect gets the end of the buffer - //uint32_t newbrfaddr = (uintptr_t)buffer; - #ifdef WLED_DEBUG_PS - //PSPRINT(" new buffer startaddress: 0x"); - //Serial.println((uintptr_t)buffer, HEX); - //PSPRINT("new bfrstart in particles "); - //Serial.println((newbrfaddr-brforigin)/structSize); - PSPRINT(" particle start: " + String(pmem->numParticles - newAvailable)); - #endif - uint32_t transferstartidx = 0; // start at beginning of new buffer pointer TODO: not used anymore, alway zero, can be removed. - uint32_t totransfer = newAvailable - availableToPS; // number of particles to transfer in this transition update - //TODO: maybe memcopy the buffer? if usedparticle number is small, end of the buffer holds alive but unused particles... copy would erase old particles though. need to think about a good way to do it. - // initialize newly transferred particles note: to have PS interact during transition, this must be changed. could initialize TTL and perpetual only TODO: need to be more clever: need to clear buffer if 2D->1D or 1D->2D transition or settings are nonsensical. - PSPRINT(" totransfer: " + String(totransfer)); - if(totransfer <= newAvailable) { // overflow check TODO: why do overflows happen? does it still happen with the new calculation? -> not in normal transfer, need to check quick transfer changes -> seems ok TODO: can this be removed? - particleHandover(buffer, structSize, totransfer); - } - else { - PSPRINTLN(" Particle transfer overflow! "); - } + // now we have a valid buffer, if this is a PS to PS FX transition: transfer particles slowly to new FX + bool effectchanged = (SEGMENT.currentMode() != SEGMENT.mode); // FX changed, transition the particle buffer + if (effectchanged && pmem->inTransition) { + PSPRINT(" FX changed "); + PSPRINT(" this mode: " + String(effectID)); + PSPRINT("/ oldmode: " + String(SEGMENT.currentMode())); + PSPRINTLN(" newmode: " + String(SEGMENT.mode)); + uint32_t maxParticles = pmem->buffersize / structSize; // maximum number of particles that fit in the buffer + uint32_t progress = SEGMENT.progress(); // transition progress + uint32_t newAvailable = 0; + if (SEGMENT.mode == effectID) { // new effect ID -> function was called from new FX + PSPRINT(" new FX"); + PSPRINT(" progress: " + String(progress)); + newAvailable = (maxParticles * (progress + 1)) >> 16; // update total particles available to this PS + //if(newAvailable < 2) newAvailable = 2; // always give a minimum amount (this can lead to overlap, currently not a problem but some new FX may not like it) + //uint32_t brforigin = (uintptr_t)buffer; // save old buffer pointer for !!!!DEBUG + buffer = (void*)((uint8_t*)buffer + (maxParticles - newAvailable) * structSize); // new effect gets the end of the buffer + //uint32_t newbrfaddr = (uintptr_t)buffer; + #ifdef WLED_DEBUG_PS + PSPRINT(" new buffer startaddress: 0x"); + Serial.println((uintptr_t)buffer, HEX); + PSPRINT("new bfrstart in particles "); + Serial.println((newbrfaddr-brforigin)/structSize); + PSPRINT(" particle start: " + String(maxParticles - newAvailable)); + #endif + uint32_t totransfer = newAvailable - availableToPS; // number of particles to transfer in this transition update + //TODO: maybe memcopy the buffer? if usedparticle number is small, end of the buffer holds alive but unused particles... copy would erase old particles though. need to think about a good way to do it. + + // initialize newly transferred particles note: to have PS interact during transition, this must be changed. could initialize TTL and perpetual only + + PSPRINT(" totransfer: " + String(totransfer)); + if(totransfer <= newAvailable) { // overflow check TODO: why do overflows happen? does it still happen with the new calculation? -> not in normal transfer, need to check quick transfer changes -> seems ok TODO: can this be removed? + particleHandover(buffer, structSize, totransfer); } - else { // this was called from old FX - PSPRINT(" old FX"); - PSPRINT(" progress: " + String(progress)); - SEGMENT.setCurrentPalette(true); // load the old palette into segment - progress = 0xFFFFU - progress; // inverted transition progress - PSPRINT(" inv.prog: " + String(progress)); - - newAvailable = (pmem->numParticles * progress) >> 16; - PSPRINT(" newAvailable: " + String(newAvailable)); - PSPRINT(" oldAvailable: " + String(availableToPS)); - if(newAvailable > availableToPS) newAvailable = availableToPS; // do not increase available particles (if memory was extended) - //PSPRINTLN("trans: available particles: " + String(availableToPS)); - // note: startindex is start of buffer (transferred from end to start, so end particles are the old ones) - + else { + PSPRINTLN(" Particle transfer overflow! "); } - - availableToPS = newAvailable; - PSPRINT(" final available to PS: " + String(availableToPS)); - // TODO: if this is correct, the first if statement can be removed and availabletoPS updated here or: this part of the function can be optimized still - } - else { // same effect transition - PSPRINT(" same FX "); - availableToPS = pmem->numParticles; // no transition, full buffer available TODO: need to restrict to segment size? test this with different size but same FX (large to small) - pmem->inTransition = false; - } - - if(!SEGMENT.isInTransition()) { // transition ended, cleanup - pmem->inTransition = false; - PSPRINTLN(" ** "); - PSPRINTLN("****** TRANSITION ENDED ******"); - // Transfer the last buffer state in PS before rendering - transferflag = true; } - } else { // no transition, full buffer available + else { // this was called from the old FX + PSPRINT(" old FX"); + PSPRINT(" progress: " + String(progress)); + SEGMENT.setCurrentPalette(true); // load the old palette into segment + progress = 0xFFFFU - progress; // inverted transition progress + PSPRINT(" inv.prog: " + String(progress)); + newAvailable = ((maxParticles * progress) >> 16); + if(newAvailable > 0) newAvailable--; // -1 to avoid overlapping memory in 1D<->2D transitions + //newAvailable = min(newAvailable, numPixels); // limit to segment size in case a larger segment initialized the buffer -> not needed during transfer, looks better without this + PSPRINT(" newAvailable: " + String(newAvailable)); + PSPRINT(" oldAvailable: " + String(availableToPS)); + if(newAvailable > availableToPS) newAvailable = availableToPS; // do not increase available particles (if memory was extended) + //PSPRINTLN("trans: available particles: " + String(availableToPS)); + // note: startindex is start of buffer (transferred from end to start, so end particles are the old ones) + } + availableToPS = newAvailable; + PSPRINT(" final available to PS: " + String(availableToPS)); + } else { // no PS transition, full buffer available if(pmem->transferParticles) { // transition ended (or blending is disabled) -> transfer all remaining particles uint32_t numPixels = SEGMENT.vWidth() * SEGMENT.vHeight(); // works for 1D and 2D, height is 1 in 1D - uint32_t finalAvailable = min(pmem->numParticles, numPixels); // limit to segment size in case a larger segment initialized the buffer - if (finalAvailable > availableToPS) { // not all particles transferred yet - uint32_t totransfer = finalAvailable - availableToPS; // number of particles to transfer in this transition update + uint32_t maxParticles = pmem->buffersize / structSize; // maximum number of particles that fit in the buffer + if (maxParticles > availableToPS) { // not all particles transferred yet + uint32_t totransfer = maxParticles - availableToPS; // transfer all remaining particles particleHandover(buffer, structSize, totransfer); } - availableToPS = finalAvailable; // update available particles + availableToPS = min(maxParticles, numPixels);; // update available particles limit to segment size in case a larger segment initialized the buffer (FX depend may depend on it) + pmem->particleType = structSize; // update particle type pmem->transferParticles = false; } - PSPRINTLN(" no trans, no of partcls: " + String(availableToPS)); + PSPRINTLN(" no trans, nr of partcls: " + String(availableToPS)); pmem->inTransition = false; } PSPRINTLN(" END getPartPointer "); @@ -2234,13 +2214,15 @@ void* particleMemoryManager(const uint32_t requestedParticles, size_t structSize } // (re)initialize particles in the particle buffer for use in the new FX -void particleHandover(void *buffer, size_t structSize, uint32_t numToTransfer) -{ - if (structSize == sizeof(PSparticle)) - { // 2D particle +void particleHandover(void *buffer, size_t structSize, uint32_t numToTransfer) { + // TODO: need to be more clever: need to clear buffer if 2D->1D or 1D->2D transition or settings are nonsensical. + #ifndef WLED_DISABLE_PARTICLESYSTEM2D + if (structSize == sizeof(PSparticle)) { // 2D particle PSparticle *particles = (PSparticle *)buffer; - for (uint32_t i = 0; i < numToTransfer; i++) - { + if (pmem->particleType != sizeof(PSparticle)) { // check if we are being handed over from a 1D system, clear buffer if so + memset(buffer, 0, numToTransfer * sizeof(PSparticle)); // clear buffer + } + for (uint32_t i = 0; i < numToTransfer; i++) { particles[i].perpetual = false; // particle ages if (particles[i].outofbounds) particles[i].ttl = 0; // kill out of bounds @@ -2251,17 +2233,23 @@ void particleHandover(void *buffer, size_t structSize, uint32_t numToTransfer) particles[i].collide = true; // enable collisions (in case new FX uses them) } } - else - { // 1D particle system + else // 1D particle system + #endif + { + #ifndef WLED_DISABLE_PARTICLESYSTEM1D PSparticle1D *particles = (PSparticle1D *)buffer; - for (uint32_t i = 0; i < numToTransfer; i++) - { + // check if we are being handed over from a 2D system, clear buffer if so + if (pmem->particleType != sizeof(PSparticle1D)) { + memset(buffer, 0, numToTransfer * sizeof(PSparticle1D)); // clear buffer + } + for (uint32_t i = 0; i < numToTransfer; i++) { particles[i].perpetual = false; // particle ages if (particles[i].outofbounds) particles[i].ttl = 0; // kill out of bounds else if (particles[i].ttl > 50) particles[i].ttl = 50; // reduce TTL so it will die soon } + #endif } } @@ -2322,7 +2310,7 @@ void servicePSmem() { PSPRINTLN(partMemList[i].watchdog); if (partMemList[i].watchdog > MAX_MEMIDLE) { PSPRINTLN("psmem free"); //deallocating memory:2560 - deallocatePSmemory(partMemList[i].particleMemPointer, partMemList[i].numParticles * partMemList[i].sizeOfParticle); // Free memory + deallocatePSmemory(partMemList[i].particleMemPointer, partMemList[i].buffersize); // Free memory partMemList.erase(partMemList.begin() + i); // Remove entry partMemList.shrink_to_fit(); // partMemList is small, memory operations should be unproblematic } @@ -2343,7 +2331,7 @@ void servicePSmem() { } - +//!!! TODO: either use this function or remove it // apply globalBlur to framebuffer, clear buffer if not blurring (call this after transferring the buffer to the segment) static void applyBlurOrClear2D(uint32_t width, uint32_t height, uint8_t fxID) { if (framebuffer) { @@ -2376,18 +2364,6 @@ static void applyBlurOrClear2D(uint32_t width, uint32_t height, uint8_t fxID) { void transferBuffer(uint32_t width, uint32_t height) { PSPRINT(" xfer buf "); if(!framebuffer) return; // no buffer, nothing to transfer - /* - if(SEGMENT.isInTransition()) { - // check if PS is also in transition - partMem *pmem = getPartMem(); - if (pmem && pmem->inTransition) // PS to PS transition - { - if(SEGMENT.mode == effectID) { // segment.mode() returns new FX, new FX is rendered first, skip transfer - PSPRINTLN("skip xfer"); - // return; // leave the buffer as is, let the old effect render to it as well before transferring - } - } - }*/ if(height) { // is 2D, 1D passes height = 0 int32_t yflipped; PSPRINT("xfer 2D"); diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index 555bdedbcd..fb68cd3d53 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -29,11 +29,11 @@ // memory and transition manager struct partMem { void* particleMemPointer; // pointer to particle memory - uint32_t numParticles; // number of particles that fit in memory note: could be a uint16_t but padding will increase the struct size so 12 bytes anyway - uint8_t sizeOfParticle; // size of the particle struct in this buffer + uint32_t buffersize; // buffer size in bytes + uint8_t particleType; // type of particles currently in memory: 0 = none, particle struct size otherwise (required for 1D<->2D transitions) uint8_t id; // ID of segment this memory belongs to uint8_t watchdog; // counter to handle deallocation - uint8_t inTransition; // to track transitions (is set to new FX ID during transitions) + uint8_t inTransition; // to track PS to PS FX transitions (is set to new FX ID during transitions), not set if not both FX are PS FX bool transferParticles; // if set, particles in buffer are transferred to new FX }; From 0f31e4825a7ac7e069e1c9af0743c0d670708695 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Thu, 26 Dec 2024 19:42:18 +0100 Subject: [PATCH 147/219] added smear to fuzzy noise, some cleanup --- wled00/FX.cpp | 50 +++++++++++++++++++------------------ wled00/FXparticleSystem.cpp | 5 +--- 2 files changed, 27 insertions(+), 28 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 81e3876188..aa82d2249b 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -7770,7 +7770,7 @@ uint16_t mode_particlevortex(void) { return FRAMETIME; } #undef NUMBEROFSOURCES -static const char _data_FX_MODE_PARTICLEVORTEX[] PROGMEM = "PS Vortex@Rotation Speed,Particle Speed,Arms,Auto Flip,Nozzle,Smear,Direction,Random Flip;;!;2;pal=56,sx=200,ix=190,c1=200,c2=0,c3=0,o1=0,o2=0,o3=0"; +static const char _data_FX_MODE_PARTICLEVORTEX[] PROGMEM = "PS Vortex@Rotation Speed,Particle Speed,Arms,Auto Flip,Nozzle,Smear,Direction,Random Flip;;!;2;pal=56,sx=200,ix=190,c1=200,c2=0,c3=0"; /* * Particle Fireworks @@ -8316,6 +8316,8 @@ uint16_t mode_particleperlin(void) { return mode_static(); // allocation failed or not 2D PartSys->setKillOutOfBounds(true); // should never happen, but lets make sure there are no stray particles + PartSys->setMotionBlur(230); // anable motion blur + PartSys->setBounceY(true); SEGENV.aux0 = rand(); } else @@ -8327,11 +8329,11 @@ uint16_t mode_particleperlin(void) { PartSys->updateSystem(); // update system properties (dimensions and data pointers) PartSys->setWrapX(SEGMENT.check1); PartSys->setBounceX(!SEGMENT.check1); - PartSys->setBounceY(true); PartSys->setWallHardness(SEGMENT.custom1); // wall hardness PartSys->enableParticleCollisions(SEGMENT.check3, SEGMENT.custom1); // enable collisions and set particle collision hardness PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 25, 128)); // min is 10%, max is 50% - PartSys->setMotionBlur(230); // anable motion blur + PartSys->setSmearBlur(SEGMENT.check2 * 15); // enable 2D blurring (smearing) + // apply 'gravity' from a 2D perlin noise map SEGENV.aux0 += 1 + (SEGMENT.speed >> 5); // noise z-position // update position in noise @@ -8360,7 +8362,7 @@ uint16_t mode_particleperlin(void) { PartSys->update(); // update and render return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEPERLIN[] PROGMEM = "PS Fuzzy Noise@Speed,Particles,Bounce,Friction,Scale,Cylinder,,Collisions;;!;2;pal=64,sx=50,ix=200,c1=130,c2=30,c3=5,o1=0,o3=1"; +static const char _data_FX_MODE_PARTICLEPERLIN[] PROGMEM = "PS Fuzzy Noise@Speed,Particles,Bounce,Friction,Scale,Cylinder,Smear,Collisions;;!;2;pal=64,sx=50,ix=200,c1=130,c2=30,c3=5,o3=1"; /* * Particle smashing down like meteors and exploding as they hit the ground, has many parameters to play with @@ -8477,7 +8479,7 @@ uint16_t mode_particleimpact(void) { return FRAMETIME; } #undef NUMBEROFSOURCES -static const char _data_FX_MODE_PARTICLEIMPACT[] PROGMEM = "PS Impact@Launches,Explosion Size,Explosion Force,Hardness,Blur,Cylinder,Walls,Collisions;;!;2;pal=0,sx=32,ix=85,c1=70,c2=130,c3=0,o1=0,o2=0,o3=1"; +static const char _data_FX_MODE_PARTICLEIMPACT[] PROGMEM = "PS Impact@Launches,Explosion Size,Explosion Force,Hardness,Blur,Cylinder,Walls,Collisions;;!;2;pal=0,sx=32,ix=85,c1=70,c2=130,c3=0,o3=1"; /* Particle Attractor, a particle attractor sits in the matrix center, a spray bounces around and seeds particles @@ -8583,9 +8585,9 @@ uint16_t mode_particleattractor(void) { return FRAMETIME; } #ifdef USERMOD_AUDIOREACTIVE -static const char _data_FX_MODE_PARTICLEATTRACTOR[] PROGMEM = "PS Attractor@Mass,Particles,Particle Size,Collisions,Friction,Color by Age,Move,Disable AR;;!;2v;pal=9,sx=100,ix=82,c1=0,c2=0,o1=0,o2=0,o3=0"; +static const char _data_FX_MODE_PARTICLEATTRACTOR[] PROGMEM = "PS Attractor@Mass,Particles,Particle Size,Collisions,Friction,Color by Age,Move,Disable AR;;!;2v;pal=9,sx=100,ix=82,c1=0,c2=0,"; #else -static const char _data_FX_MODE_PARTICLEATTRACTOR[] PROGMEM = "PS Attractor@Mass,Particles,Particle Size,Collisions,Friction,Color by Age,Move,Swallow;;!;2;pal=9,sx=100,ix=82,c1=0,c2=0,o1=0,o2=0,o3=0"; +static const char _data_FX_MODE_PARTICLEATTRACTOR[] PROGMEM = "PS Attractor@Mass,Particles,Particle Size,Collisions,Friction,Color by Age,Move,Swallow;;!;2;pal=9,sx=100,ix=82,c1=0,c2=0"; #endif /* @@ -8672,7 +8674,7 @@ uint16_t mode_particlespray(void) { PartSys->update(); // update and render return FRAMETIME; } -static const char _data_FX_MODE_PARTICLESPRAY[] PROGMEM = "PS Spray@Speed,!,Left/Right,Up/Down,Angle,Gravity,Cylinder/Square,Collisions;;!;2v;pal=0,sx=150,ix=150,c1=220,c2=30,c3=21,o1=0,o2=0,o3=0"; +static const char _data_FX_MODE_PARTICLESPRAY[] PROGMEM = "PS Spray@Speed,!,Left/Right,Up/Down,Angle,Gravity,Cylinder/Square,Collisions;;!;2v;pal=0,sx=150,ix=150,c1=220,c2=30,c3=21"; /* @@ -8750,7 +8752,7 @@ uint16_t mode_particleGEQ(void) { return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEGEQ[] PROGMEM = "PS 2D GEQ@Speed,Intensity,Diverge,Bounce,Gravity,Cylinder,Walls,Floor;;!;2f;pal=0,sx=155,ix=200,c1=0,c2=128,o1=0,o2=0,o3=0"; +static const char _data_FX_MODE_PARTICLEGEQ[] PROGMEM = "PS 2D GEQ@Speed,Intensity,Diverge,Bounce,Gravity,Cylinder,Walls,Floor;;!;2f;pal=0,sx=155,ix=200,c1=0,c2=128"; /* * Particle rotating GEQ (unfinished, basically works but needs more fine-tuning) @@ -8826,7 +8828,7 @@ uint16_t mode_particlecenterGEQ(void) { PartSys->update(); // update and render return FRAMETIME; } -static const char _data_FX_MODE_PARTICLECIRCULARGEQ[] PROGMEM = "PS Center GEQ@Speed,Intensity,Rotation Speed,Color Change,Nozzle Size,,Direction;;!;2f;pal=13,ix=180,c1=0,c2=0,c3=8,o1=0,o2=0"; +static const char _data_FX_MODE_PARTICLECIRCULARGEQ[] PROGMEM = "PS Center GEQ@Speed,Intensity,Rotation Speed,Color Change,Nozzle Size,,Direction;;!;2f;pal=13,ix=180,c1=0,c2=0,c3=8"; /* Particle replacement of Ghost Rider by DedeHai (Damian Schneider), original by stepko adapted by Blaz Kristan (AKA blazoncek) @@ -8906,7 +8908,7 @@ uint16_t mode_particleghostrider(void) { PartSys->update(); // update and render return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEGHOSTRIDER[] PROGMEM = "PS Ghost Rider@Speed,Spiral,Blur,Color Cycle,Spread,Color by age,Walls;;!;2;pal=1,sx=70,ix=0,c1=220,c2=30,c3=21,o1=1,o2=0,o3=0"; +static const char _data_FX_MODE_PARTICLEGHOSTRIDER[] PROGMEM = "PS Ghost Rider@Speed,Spiral,Blur,Color Cycle,Spread,Color by age,Walls;;!;2;pal=1,sx=70,ix=0,c1=220,c2=30,c3=21,o1=1"; /* PS Blobs: large particles bouncing around, changing size and form @@ -8983,7 +8985,7 @@ uint16_t mode_particleblobs(void) { return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEBLOBS[] PROGMEM = "PS Blobs@Speed,Blobs,Size,Life,Blur,Wobble,Collide,Pulsate;;!;2v;sx=30,ix=64,c1=200,c2=130,c3=0,o1=0,o2=0,o3=1"; +static const char _data_FX_MODE_PARTICLEBLOBS[] PROGMEM = "PS Blobs@Speed,Blobs,Size,Life,Blur,Wobble,Collide,Pulsate;;!;2v;sx=30,ix=64,c1=200,c2=130,c3=0,o3=1"; /* * Particle Fractal @@ -9057,7 +9059,7 @@ uint16_t mode_particlefractal(void) { return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEFRACTAL[] PROGMEM = "PS fractal (exp)@Speed,Intensity,Base angle,branch angle,Blur,,Direction;;!;2f;pal=13,ix=180,c1=0,c2=0,c3=8,o1=0,o2=0"; +static const char _data_FX_MODE_PARTICLEFRACTAL[] PROGMEM = "PS fractal (exp)@Speed,Intensity,Base angle,branch angle,Blur,,Direction;;!;2f;pal=13,ix=180,c1=0,c2=0,c3=8"; #endif //WLED_DISABLE_PARTICLESYSTEM2D @@ -9171,7 +9173,7 @@ uint16_t mode_particleDrip(void) { PartSys->update(); // update and render return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEDRIP[] PROGMEM = "PS DripDrop@Speed,!,Splash,Blur/Overlay,Gravity,Rain,PushSplash,Smooth;,!;!;1;pal=0,sx=150,ix=25,c1=220,c2=30,c3=21,o1=0,o2=0,o3=0"; +static const char _data_FX_MODE_PARTICLEDRIP[] PROGMEM = "PS DripDrop@Speed,!,Splash,Blur/Overlay,Gravity,Rain,PushSplash,Smooth;,!;!;1;pal=0,sx=150,ix=25,c1=220,c2=30,c3=21"; /* @@ -9259,7 +9261,7 @@ uint16_t mode_particleBouncingBalls(void) { PartSys->update(); // update and render return FRAMETIME; } -static const char _data_FX_MODE_PSBOUNCINGBALLS[] PROGMEM = "PS Bouncing Balls@Speed,!,Size,Blur/Overlay,Gravity,Collide,Rolling,Color by Position;,!;!;1;pal=0,sx=100,ix=85,c1=30,c2=0,c3=8,o1=0,o2=0,o3=0"; +static const char _data_FX_MODE_PSBOUNCINGBALLS[] PROGMEM = "PS Bouncing Balls@Speed,!,Size,Blur/Overlay,Gravity,Collide,Rolling,Color by Position;,!;!;1;pal=0,sx=100,ix=85,c1=30,c2=0,c3=8"; /* Particle Replacement for original Dancing Shadows: @@ -9370,7 +9372,7 @@ uint16_t mode_particleDancingShadows(void) { return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEDANCINGSHADOWS[] PROGMEM = "PS Dancing Shadows@Speed,!,Blur/Overlay,Color Cycle,,,,Smooth;,!;!;1;pal=0,sx=100,ix=180,c1=0,c2=0,o2=0,o3=0"; +static const char _data_FX_MODE_PARTICLEDANCINGSHADOWS[] PROGMEM = "PS Dancing Shadows@Speed,!,Blur/Overlay,Color Cycle,,,,Smooth;,!;!;1;pal=0,sx=100,ix=180,c1=0,c2=0"; /* Particle Fireworks 1D replacement @@ -9472,7 +9474,7 @@ uint16_t mode_particleFireworks1D(void) { PartSys->update(); // update and render return FRAMETIME; } -static const char _data_FX_MODE_PS_FIREWORKS1D[] PROGMEM = "PS Fireworks 1D@Gravity,Explosion,Firing side,Blur/Overlay,Saturation,Gravity,Colorful,Smooth;,!;!;1;pal=0,sx=150,ix=150,c1=220,c2=30,c3=21,o1=0,o2=1,o3=0"; +static const char _data_FX_MODE_PS_FIREWORKS1D[] PROGMEM = "PS Fireworks 1D@Gravity,Explosion,Firing side,Blur/Overlay,Saturation,Gravity,Colorful,Smooth;,!;!;1;pal=0,sx=150,ix=150,c1=220,c2=30,c3=21,o2=1"; /* Particle based Sparkle effect @@ -9542,7 +9544,7 @@ uint16_t mode_particleSparkler(void) { return FRAMETIME; } -static const char _data_FX_MODE_PS_SPARKLER[] PROGMEM = "PS Sparkler@Speed,!,Saturation,Blur/Overlay,Sparklers,Direction,Wrap/Bounce,Smooth;,!;!;1;pal=0,sx=50,ix=200,c1=0,c2=0,c3=0,o1=1,o2=1,o3=0"; +static const char _data_FX_MODE_PS_SPARKLER[] PROGMEM = "PS Sparkler@Speed,!,Saturation,Blur/Overlay,Sparklers,Direction,Wrap/Bounce,Smooth;,!;!;1;pal=0,sx=50,ix=200,c1=0,c2=0,c3=0,o1=1,o2=1"; /* Particle based Hourglass, particles falling at defined intervals Uses palette for particle color @@ -9718,7 +9720,7 @@ uint16_t mode_particle1Dspray(void) { return FRAMETIME; } -static const char _data_FX_MODE_PS_1DSPRAY[] PROGMEM = "PS 1D Spray@!,!,Position,Blur/Overlay,Gravity,Color by Age,Bounce,Color by Position;,!;!;1;pal=35,sx=200,ix=220,c1=4,c2=0,c3=28,o1=1,o2=1,o3=0"; +static const char _data_FX_MODE_PS_1DSPRAY[] PROGMEM = "PS 1D Spray@!,!,Position,Blur/Overlay,Gravity,Color by Age,Bounce,Color by Position;,!;!;1;pal=35,sx=200,ix=220,c1=4,c2=0,c3=28,o1=1,o2=1"; /* Particle based balance: particles move back and forth (1D pendent to 2D particle box) Uses palette for particle color @@ -9788,7 +9790,7 @@ uint16_t mode_particleBalance(void) { PartSys->update(); // update and render return FRAMETIME; } -static const char _data_FX_MODE_PS_BALANCE[] PROGMEM = "PS 1D Balance@!,!,Collisions,Blur/Overlay,Tilt,Color by Position,Wrap/Bounce,Random;,!;!;1;pal=18,sx=100,ix=40,c1=200,c2=0,c3=5,o1=1,o2=0,o3=0"; +static const char _data_FX_MODE_PS_BALANCE[] PROGMEM = "PS 1D Balance@!,!,Collisions,Blur/Overlay,Tilt,Color by Position,Wrap/Bounce,Random;,!;!;1;pal=18,sx=100,ix=40,c1=200,c2=0,c3=5,o1=1"; /* Particle based Chase effect @@ -9896,7 +9898,7 @@ uint16_t mode_particleChase(void) { PartSys->update(); // update and render return FRAMETIME; } -static const char _data_FX_MODE_PS_CHASE[] PROGMEM = "PS Chase@Speed,Density,Size,Hue,Blur/Overlay,Pride,Color Waves,Color by Position;,!;!;1;pal=11,sx=50,ix=100,c2=5,c3=0,o1=0,o2=0,o3=0"; +static const char _data_FX_MODE_PS_CHASE[] PROGMEM = "PS Chase@Speed,Density,Size,Hue,Blur/Overlay,Pride,Color Waves,Color by Position;,!;!;1;pal=11,sx=50,ix=100,c2=5,c3=0"; /* Particle Fireworks Starburst replacement (smoother rendering, more settings) Uses palette for particle color @@ -9957,7 +9959,7 @@ uint16_t mode_particleStarburst(void) { PartSys->update(); // update and render return FRAMETIME; } -static const char _data_FX_MODE_PS_STARBURST[] PROGMEM = "PS Starburst@Chance,Fragments,Fragment Size,Blur/Overlay,Cooling,Gravity,Colorful,Push;,!;!;1;pal=52,sx=150,ix=150,c1=120,c2=0,c3=21,o1=0,o2=0,o3=0"; +static const char _data_FX_MODE_PS_STARBURST[] PROGMEM = "PS Starburst@Chance,Fragments,Fragment Size,Blur/Overlay,Cooling,Gravity,Colorful,Push;,!;!;1;pal=52,sx=150,ix=150,c1=120,c2=0,c3=21"; /* Particle based 1D GEQ effect, each frequency bin gets an emitter, distributed over the strip @@ -10036,7 +10038,7 @@ uint16_t mode_particle1DGEQ(void) { return FRAMETIME; } -static const char _data_FX_MODE_PS_1D_GEQ[] PROGMEM = "PS 1D GEQ@Speed,!,Size,Blur/Overlay,,,,;,!;!;1f;pal=0,sx=50,ix=200,c1=0,c2=0,c3=0,o1=1,o2=1,o3=0"; +static const char _data_FX_MODE_PS_1D_GEQ[] PROGMEM = "PS 1D GEQ@Speed,!,Size,Blur/Overlay,,,,;,!;!;1f;pal=0,sx=50,ix=200,c1=0,c2=0,c3=0,o1=1,o2=1"; /* Particle based Fire effect Uses palette for particle color @@ -10101,7 +10103,7 @@ uint16_t mode_particleFire1D(void) { return FRAMETIME; } -static const char _data_FX_MODE_PS_FIRE1D[] PROGMEM = "PS Fire 1D@!,!,Cooling,Blur/Overlay;,!;!;1;pal=35,sx=100,ix=50,c1=80,c2=100,c3=28,o1=1,o2=1,o3=0"; +static const char _data_FX_MODE_PS_FIRE1D[] PROGMEM = "PS Fire 1D@!,!,Cooling,Blur/Overlay;,!;!;1;pal=35,sx=100,ix=50,c1=80,c2=100,c3=28,o1=1,o2=1"; #endif // WLED_DISABLE_PARTICLESYSTEM1D ////////////////////////////////////////////////////////////////////////////////////////// diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index bd408a8fdc..0bdac8e18b 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -1067,7 +1067,7 @@ void ParticleSystem2D::updateSystem(void) { setMatrixSize(cols, rows); updatePSpointers(advPartProps != nullptr, advPartSize != nullptr); // update pointers to PS data, also updates availableParticles setUsedParticles(usedpercentage); // update used particles based on percentage TODO: this does not need to be called for each frame, it only changes during transitions. can optimize? - if (partMemList.size() == 1) // if number of vector elements is one, this is the only system !!!TODO: does this need more special case handling? + if (partMemList.size() == 1) // if number of vector elements is one, this is the only system { PSPRINTLN("rendersolo"); if(renderSolo < 2) renderSolo++; // increment: there is one transition frame when enabling render solo where local buffer is still blank and cant be used for blurring @@ -2152,8 +2152,6 @@ void* particleMemoryManager(const uint32_t requestedParticles, size_t structSize PSPRINT(" new FX"); PSPRINT(" progress: " + String(progress)); newAvailable = (maxParticles * (progress + 1)) >> 16; // update total particles available to this PS - //if(newAvailable < 2) newAvailable = 2; // always give a minimum amount (this can lead to overlap, currently not a problem but some new FX may not like it) - //uint32_t brforigin = (uintptr_t)buffer; // save old buffer pointer for !!!!DEBUG buffer = (void*)((uint8_t*)buffer + (maxParticles - newAvailable) * structSize); // new effect gets the end of the buffer //uint32_t newbrfaddr = (uintptr_t)buffer; #ifdef WLED_DEBUG_PS @@ -2228,7 +2226,6 @@ void particleHandover(void *buffer, size_t structSize, uint32_t numToTransfer) { particles[i].ttl = 0; // kill out of bounds else if (particles[i].ttl > 200) particles[i].ttl = 200; // reduce TTL so it will die soon - // else if(particles[i].ttl) particles[i].ttl += 100; // !!! debug test particles[i].sat = 255; // full saturation particles[i].collide = true; // enable collisions (in case new FX uses them) } From 164bcc5e716204e829428db3e4aa3e8cfa98e2ef Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sat, 28 Dec 2024 12:09:31 +0100 Subject: [PATCH 148/219] added blurring option to PS 2D fireworks, work in progress in finding that nasty bug that crashes all --- wled00/FX.cpp | 18 +- wled00/FX_fcn.cpp | 1 - wled00/FXparticleSystem.cpp | 412 ++++++++++++++++++------------------ wled00/FXparticleSystem.h | 30 ++- 4 files changed, 229 insertions(+), 232 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index aa82d2249b..c26e043814 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -7808,6 +7808,11 @@ uint16_t mode_particlefireworks(void) { PartSys->setWrapX(SEGMENT.check1); PartSys->setBounceY(SEGMENT.check2); PartSys->setGravity(map(SEGMENT.custom3, 0, 31, SEGMENT.check2 ? 1 : 0, 10)); // if bounded, set gravity to minimum of 1 or they will bounce at top + PartSys->setMotionBlur(map(SEGMENT.custom2, 0, 255, 0, 170)); // anable motion blur + uint8_t smearing = 0; + if(SEGMENT.custom2 > 200) + smearing = SEGMENT.custom2 - 200; + PartSys->setSmearBlur(smearing); // enable 2D blurring (smearing) // update the rockets, set the speed state for (j = 0; j < numRockets; j++) { @@ -7911,7 +7916,7 @@ uint16_t mode_particlefireworks(void) { return FRAMETIME; } #undef NUMBEROFSOURCES -static const char _data_FX_MODE_PARTICLEFIREWORKS[] PROGMEM = "PS Fireworks@Launches,Explosion Size,Fuse,,Gravity,Cylinder,Ground,;;!;2;pal=11,sx=100,ix=50,c1=84,c2=128,c3=12,o1=0,o2=0,o3=0"; +static const char _data_FX_MODE_PARTICLEFIREWORKS[] PROGMEM = "PS Fireworks@Launches,Explosion Size,Fuse,Blur,Gravity,Cylinder,Ground,;;!;2;pal=11,sx=100,ix=50,c1=84,c2=0,c3=12,o1=0,o2=0,o3=0"; /* * Particle Volcano @@ -9806,8 +9811,8 @@ uint16_t mode_particleChase(void) { if (!initParticleSystem1D(PartSys, 1, 255, 3, true)) // init return mode_static(); // allocation failed or is single pixel SEGENV.aux0 = 0xFFFF; // invalidate - *PartSys->PSdataEnd = 1; - *(PartSys->PSdataEnd + 1) = 1; + *PartSys->PSdataEnd = 1; // huedir + *(PartSys->PSdataEnd + 1) = 1; // sizedir } else PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS @@ -9821,9 +9826,10 @@ uint16_t mode_particleChase(void) { //uint8_t* basehue = (PartSys->PSdataEnd + 2); //assign data pointer //PartSys->setBounce(SEGMENT.check2); - uint32_t settingssum = SEGMENT.speed + SEGMENT.intensity + SEGMENT.custom1 + SEGMENT.custom2 + SEGMENT.check1 + SEGMENT.check2 + SEGMENT.check3; + uint32_t settingssum = SEGMENT.speed + SEGMENT.intensity + SEGMENT.custom1 + SEGMENT.custom2 + SEGMENT.check1 + SEGMENT.check2 + SEGMENT.check3; // note: progress is used to enforce update during transitions if(SEGENV.aux0 != settingssum) { //settings changed changed, update - PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 1, min(PartSys->maxX / (32 + (SEGMENT.custom1 >> 1)), int32_t(PartSys->usedParticles)))); //depends on intensity and particle size (custom1) !!! TODO: this needs an update to relative number + //PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 1, min(PartSys->maxX / (32 + (SEGMENT.custom1 >> 1)), int32_t(PartSys->usedParticles)))); //depends on intensity and particle size (custom1) !!! TODO: is this fine now? + PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 1, 255 / (1 + (SEGMENT.custom1 >> 6)))); //depends on intensity and particle size (custom1) SEGENV.step = (PartSys->maxX + (PS_P_RADIUS_1D << 4)) / PartSys->usedParticles; //spacing between particles // uint32_t remainder = PartSys->maxX - ((PartSys->usedParticles) * SEGENV.step); // unused spacing, distribute this for(i = 0; i < PartSys->usedParticles; i++) { @@ -9831,7 +9837,7 @@ uint16_t mode_particleChase(void) { PartSys->particles[i].ttl = 300; PartSys->particles[i].perpetual = true; //PartSys->particles[i].x = (i - 1) * SEGENV.step + (((i + 1) * remainder) / PartSys->usedParticles); // distribute evenly - PartSys->particles[i].x = (i - 1) * SEGENV.step; // distribute evenly + PartSys->particles[i].x = (i - 1) * SEGENV.step; // distribute evenly (starts out of frame for i=0) PartSys->particles[i].vx = SEGMENT.speed >> 1; PartSys->advPartProps[i].size = SEGMENT.custom1; if(SEGMENT.custom2 < 255) diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index 54295b633a..570a1b1424 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -1412,7 +1412,6 @@ void WS2812FX::service() { servicePSmem(); // handle segment particle system memory _isServicing = false; _triggered = false; - #ifdef WLED_DEBUG if ((_targetFps != FPS_UNLIMITED) && (millis() - nowUp > _frametime)) DEBUG_PRINTF_P(PSTR("Slow effects %u/%d.\n"), (unsigned)(millis()-nowUp), (int)_frametime); #endif diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index 0bdac8e18b..d2bebb6b70 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -31,18 +31,23 @@ static void fast_color_scale(CRGB &c, const uint32_t scale); // fast scaling fun // global variables for memory management std::vector partMemList; // list of particle memory pointers partMem *pmem = nullptr; // pointer to particle memory of current segment, updated in particleMemoryManager() -CRGB *framebuffer = nullptr; // local frame buffer for rendering -CRGB *renderbuffer = nullptr; // local particle render buffer for advanced particles -uint16_t frameBufferSize = 0; // size in pixels, used to check if framebuffer is large enough for current segment -uint16_t renderBufferSize = 0; // size in pixels, if allcoated by a 1D system it needs to be updated for 2D -uint8_t renderSolo = 0; // is set to >0 if this is the only particle system using the so it can use the buffer continuously (faster blurring) -uint8_t globalBlur = 0; // motion blur to apply if multiple PS are using the buffer +//CRGB *framebuffer = nullptr; // local frame buffer for rendering +//CRGB *renderbuffer = nullptr; // local particle render buffer for advanced particles +CRGB framebuffer[512]; // local frame buffer for rendering !!!! just a test +CRGB renderbuffer[100]; // local particle render buffer for advanced particles +uint16_t frameBufferSize = 512; //!!!0 size in pixels, used to check if framebuffer is large enough for current segment +uint16_t renderBufferSize = 100; //!!!0 size in pixels, if allcoated by a 1D system it needs to be updated for 2D +bool renderSolo = false; // is set to true if this is the only particle system using the so it can use the buffer continuously (faster blurring) +int32_t globalBlur = 0; // motion blur to apply if multiple PS are using the buffer +int32_t globalSmear = 0; // smear-blur to apply if multiple PS are using the buffer ParticleSystem2D::ParticleSystem2D(uint32_t width, uint32_t height, uint32_t numberofparticles, uint32_t numberofsources, bool isadvanced, bool sizecontrol) { + PSPRINTLN("\n ParticleSystem2D constructor"); effectID = SEGMENT.mode; // new FX called init, save the effect ID numSources = numberofsources; // number of sources allocated in init numParticles = numberofparticles; // number of particles allocated in init - usedpercentage = 255; // use all particles by default, usedParticles is updated in updatePSpointers() + availableParticles = 0; // let the memory manager assign + fractionOfParticlesUsed = 255; // use all particles by default, usedParticles is updated in updatePSpointers() advPartProps = NULL; //make sure we start out with null pointers (just in case memory was not cleared) advPartSize = NULL; updatePSpointers(isadvanced, sizecontrol); // set the particle and sources pointer (call this before accessing sprays or particles) @@ -100,14 +105,14 @@ void ParticleSystem2D::updateFire(uint32_t intensity, bool renderonly) { // set percentage of used particles as uint8_t i.e 127 means 50% for example void ParticleSystem2D::setUsedParticles(uint8_t percentage) { - usedpercentage = percentage; // note usedParticles is updated in memory manager - updateUsedParticles(numParticles, availableParticles, usedpercentage, usedParticles); + fractionOfParticlesUsed = percentage; // note usedParticles is updated in memory manager + updateUsedParticles(numParticles, availableParticles, fractionOfParticlesUsed, usedParticles); PSPRINT(" SetUsedpaticles: allocated particles: "); PSPRINT(numParticles); PSPRINT(" available particles: "); PSPRINT(availableParticles); PSPRINT(" ,used percentage: "); - PSPRINT(usedpercentage); + PSPRINT(fractionOfParticlesUsed); PSPRINT(" ,used particles: "); PSPRINTLN(usedParticles); } @@ -569,19 +574,20 @@ void ParticleSystem2D::ParticleSys_render(bool firemode, uint32_t fireintensity) // handle blurring and framebuffer update if (framebuffer) { // update global blur (used for blur transitions) - int32_t bluramount = motionBlur; - // if(pmem->inTransition) { // FX transition, fade blur amount or skip setting new blur if particlesize is used - if(pmem->inTransition == effectID) { // FX transition and this is the new FX: fade blur amount - bluramount = globalBlur + (((bluramount - globalBlur) * (int)SEGMENT.progress()) >> 16); // fade from old blur to new blur during transitions - } - // } - globalBlur = bluramount; + int32_t motionbluramount = motionBlur; + int32_t smearamount = smearBlur; + if(pmem->inTransition == effectID) { // FX transition and this is the new FX: fade blur amount + motionbluramount = globalBlur + (((motionbluramount - globalBlur) * (int)SEGMENT.progress()) >> 16); // fade from old blur to new blur during transitions + smearamount = globalSmear + (((smearamount - globalSmear) * (int)SEGMENT.progress()) >> 16); + } + globalBlur = motionbluramount; + globalSmear = smearamount; /* Note on blurring / clearing when rendersolo is active, blurring can be done on the buffer directly, if this is a transition, skip blurring if this is the old FX (i.e. blur before new FX is rendered) if rendersolo is not set, the buffer must be either cleared (if no blurring and no pmem->inTransition) or read from segment (if blur is active AND this is not the old FX i.e. pmem->inTransition != fxID) - if multiple segments are used, rendersolor is false, if then transitioning from classic to PS fx, blurring is applied. if transitioning away from PS, blurring is skipped (as this is the old FX) + if multiple segments are used, rendersolor is false, if then transitioning from classic to PS FX, blurring is applied. if transitioning away from PS, blurring is skipped (as this is the old FX) // first check if rendersolo is active, if yes, check if this is a transition // if rendersolo is active and not a transition, blur the buffer or clear it @@ -592,35 +598,39 @@ void ParticleSystem2D::ParticleSys_render(bool firemode, uint32_t fireintensity) // handle buffer blurring or clearing bool bufferNeedsUpdate = (!pmem->inTransition || pmem->inTransition == effectID); // not a transition; or new FX: update buffer (blur, or clear) + if(pmem->inTransition == effectID) Serial.print(" new FX "); + else Serial.print(" old FX "); + if(bufferNeedsUpdate) { - if (globalBlur > 0) { // blurring active: if not a transition or is newFX, read data from segment before blurring (old FX can render to it afterwards) - //Serial.print(" blurring: " + String(globalBlur)); + if (globalBlur > 0 || globalSmear > 0) { // blurring active: if not a transition or is newFX, read data from segment before blurring (old FX can render to it afterwards) + Serial.println(" blurring: " + String(globalBlur)); for (uint32_t y = 0; y <= maxYpixel; y++) { int index = y * (maxXpixel + 1); for (uint32_t x = 0; x <= maxXpixel; x++) { - if (renderSolo < 2) { // sharing the framebuffer: read from segment + if (!renderSolo) { // sharing the framebuffer with another segment: read buffer back from segment uint32_t yflipped = maxYpixel - y; framebuffer[index] = SEGMENT.getPixelColorXY(x, yflipped); // read from segment } - fast_color_scale(framebuffer[index], globalBlur); + fast_color_scale(framebuffer[index], globalBlur); // note: could skip if only globalsmear is active but usually they are both active and scaling is fast enough index++; } } } else { // no blurring: clear buffer memset(framebuffer, 0, frameBufferSize * sizeof(CRGB)); + Serial.print(" clearing "); } } if(particlesize > 0 && pmem->inTransition) { // if particle size is used by FX we need a clean buffer - // new FX already rendered to the buffer, transfer it to segment and clear it TODO: make buffer transfer a function again if(bufferNeedsUpdate && !globalBlur) { // transfer only if buffer was not cleared above (happens if this is the new FX and other FX does not use blurring) - useAdditiveTransfer = false; // no blurring and big size particle FX is the new FX, can just render normally + useAdditiveTransfer = false; // no blurring and big size particle FX is the new FX (rendered first after clearing), can just render normally } - else { + else { // this is the old FX (rendering second) new FX already rendered to the buffer, transfer it to segment and clear it #ifndef WLED_DISABLE_MODE_BLEND bool tempBlend = SEGMENT.getmodeBlend(); SEGMENT.modeBlend(false); // temporarily disable FX blending in PS to PS transition (local buffer is used to do PS blending) #endif + Serial.print(" intermediate xfer "); int yflipped; for (uint32_t y = 0; y <= maxYpixel; y++) { yflipped = maxYpixel - y; @@ -636,13 +646,14 @@ void ParticleSystem2D::ParticleSys_render(bool firemode, uint32_t fireintensity) #ifndef WLED_DISABLE_MODE_BLEND SEGMENT.modeBlend(tempBlend); #endif + Serial.println(" clearing2 "); memset(framebuffer, 0, frameBufferSize * sizeof(CRGB)); // clear the buffer after transfer - useAdditiveTransfer = true; // add buffer content to segment after rendering + useAdditiveTransfer = true; // additive transfer reads from segment, adds that to the frame-buffer and writes back to segment, after transfer, segment and buffer are identical } } } - else { // no local buffer available, apply blur to segment TODO: this could also be done in 2D blur function but must be called by first PS rendering to it, may be complex to find out which on is the first (in overlay, in transitions its easy) - if (motionBlur > 0) // TODO2: blurring during transitions (and maybe also overlay) could be done in the mem-manager, it is called in update pointers and knows which PS is the first one to render. + else { // no local buffer available, apply blur to segment + if (motionBlur > 0) SEGMENT.fadeToBlackBy(255 - motionBlur); else SEGMENT.fill(BLACK); //clear the buffer before rendering next frame @@ -664,7 +675,7 @@ void ParticleSystem2D::ParticleSys_render(bool firemode, uint32_t fireintensity) brightness = min(particles[i].ttl, (uint16_t)255); baseRGB = ColorFromPalette(SEGPALETTE, particles[i].hue, 255); // TODO: use loadPalette(CRGBPalette16 &targetPalette, SEGMENT.palette), .palette should be updated immediately at palette change, only use local palette during FX transitions, not during normal transitions. -> why not always? if (particles[i].sat < 255) { - CHSV baseHSV = rgb2hsv_approximate(baseRGB); //convert to HSV + CHSV baseHSV = rgb2hsv_approximate(baseRGB); //convert to HSV //!!! TODO: use new hsv to rgb function. baseHSV.s = particles[i].sat; //set the saturation baseRGB = (CRGB)baseHSV; // convert back to RGB } @@ -691,7 +702,7 @@ void ParticleSystem2D::ParticleSys_render(bool firemode, uint32_t fireintensity) } // apply 2D blur to rendered frame TODO: this needs proper transition handling, maybe combine it with motion blur handling? - if(smearBlur > 0) { + if(globalSmear > 0) { if (framebuffer) blur2D(framebuffer, maxXpixel + 1, maxYpixel + 1, smearBlur, smearBlur); else @@ -699,40 +710,8 @@ void ParticleSystem2D::ParticleSys_render(bool firemode, uint32_t fireintensity) } // transfer framebuffer to segment if available - if (framebuffer && pmem->inTransition != effectID) { // not in transition or is old FX TODO: how to handle this for overlay rendering or multiple segment rendering? need to check for rendersolo as well? - #ifndef WLED_DISABLE_MODE_BLEND - bool tempBlend = SEGMENT.getmodeBlend(); - if (pmem->inTransition) - SEGMENT.modeBlend(false); // temporarily disable FX blending in PS to PS transition (local buffer is used to do PS blending) - #endif - int yflipped; - for (uint32_t y = 0; y <= maxYpixel; y++) { - yflipped = maxYpixel - y; - int index = y * (maxXpixel + 1); // current row index for 1D buffer - for (uint32_t x = 0; x <= maxXpixel; x++) { - CRGB *c = &framebuffer[index++]; - uint32_t clr = RGBW32(c->r,c->g,c->b,0); // convert to 32bit color - if(useAdditiveTransfer) { - uint32_t segmentcolor = SEGMENT.getPixelColorXY((int)x, (int)yflipped); - CRGB segmentRGB = CRGB(segmentcolor); - if(clr == 0) // frame buffer is black, just update the framebuffer - *c = segmentRGB; - else { // not black - if(segmentcolor) { - fast_color_add(*c, segmentRGB); // add segment color back to buffer if not black TODO: since both are 32bit, this could be made faster using the new improved wled 32bit adding (see speed improvements PR) - clr = RGBW32(c->r,c->g,c->b,0); // convert to 32bit color (again) - } - SEGMENT.setPixelColorXY((int)x, (int)yflipped, clr); // save back to segment after adding local buffer - } - } - //if(clr > 0) // not black TODO: not transferring black is faster and enables overlay, but requries proper handling of buffer clearing, which is quite complex and probably needs a change to SEGMENT handling. - else - SEGMENT.setPixelColorXY((int)x, (int)yflipped, clr); - } - } - #ifndef WLED_DISABLE_MODE_BLEND - SEGMENT.modeBlend(tempBlend); - #endif + if (framebuffer && pmem->inTransition != effectID) { // not in transition or is old FX + transferBuffer(maxXpixel + 1, maxYpixel + 1, useAdditiveTransfer); } else PSPRINTLN("skip xfer"); } @@ -1062,52 +1041,14 @@ void ParticleSystem2D::collideParticles(PSparticle *particle1, PSparticle *parti void ParticleSystem2D::updateSystem(void) { PSPRINTLN("updateSystem2D"); - uint32_t cols = SEGMENT.vWidth(); // update matrix size - uint32_t rows = SEGMENT.vHeight(); - setMatrixSize(cols, rows); + setMatrixSize(SEGMENT.vWidth(), SEGMENT.vHeight()); updatePSpointers(advPartProps != nullptr, advPartSize != nullptr); // update pointers to PS data, also updates availableParticles - setUsedParticles(usedpercentage); // update used particles based on percentage TODO: this does not need to be called for each frame, it only changes during transitions. can optimize? + setUsedParticles(fractionOfParticlesUsed); // update used particles based on percentage (can change during transitions, execute each frame for code simplicity) if (partMemList.size() == 1) // if number of vector elements is one, this is the only system - { - PSPRINTLN("rendersolo"); - if(renderSolo < 2) renderSolo++; // increment: there is one transition frame when enabling render solo where local buffer is still blank and cant be used for blurring - } + renderSolo = true; else - { - PSPRINTLN("rendermulti"); - renderSolo = 0; - } -} - -void ParticleSystem2D::transferBuffer(bool additive) { - if (framebuffer) { - int yflipped; - for (uint32_t y = 0; y <= maxYpixel; y++) { - yflipped = maxYpixel - y; - int index = y * (maxXpixel + 1); // current row index for 1D buffer - for (uint32_t x = 0; x <= maxXpixel; x++) { - CRGB *c = &framebuffer[index++]; - uint32_t clr = RGBW32(c->r,c->g,c->b,0); // convert to 32bit color - if (additive) { - uint32_t segmentcolor = SEGMENT.getPixelColorXY((int)x, (int)yflipped); - CRGB segmentRGB = CRGB(segmentcolor); - if(clr == 0) // frame buffer is black, just update the framebuffer TODO: could check if segmentcolor is also black and skip - *c = segmentRGB; - else { // not black - if(segmentcolor) { - fast_color_add(*c, segmentRGB); // add segment color back to buffer if not black TODO: since both are 32bit, this could be made faster using the new improved wled 32bit adding (see speed improvements PR) - clr = RGBW32(c->r,c->g,c->b,0); // convert to 32bit color (again) - } - SEGMENT.setPixelColorXY((int)x, (int)yflipped, clr); // save back to segment after adding local buffer - } - } - //if(clr > 0) // not black TODO: not transferring black is faster and enables overlay, but requries proper handling of buffer clearing, which is quite complex and probably needs a change to SEGMENT handling. - else - SEGMENT.setPixelColorXY((int)x, (int)yflipped, clr); - } - } - } - else PSPRINTLN("skip xfer"); + renderSolo = false; + PSPRINTLN("\n END update System2D, running FX..."); } // 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) @@ -1121,7 +1062,9 @@ PSPRINTLN("updatePSpointers"); // a pointer MUST be 4 byte aligned. sizeof() in a struct/class is always aligned to the largest element. if it contains a 32bit, it will be padded to 4 bytes, 16bit is padded to 2byte alignment. // The PS is aligned to 4 bytes, a PSparticle is aligned to 2 and a struct containing only byte sized variables is not aligned at all and may need to be padded when dividing the memoryblock. // by making sure that the number of sources and particles is a multiple of 4, padding can be skipped here as alignent is ensured, independent of struct sizes. + particles = reinterpret_cast(particleMemoryManager(0, sizeof(PSparticle), availableParticles, fractionOfParticlesUsed, effectID)); // get memory, leave buffer size as is (request 0) sources = reinterpret_cast(this + 1); // pointer to source(s) at data+sizeof(ParticleSystem2D) + PSdataEnd = reinterpret_cast(sources + numSources); // pointer to first available byte after the PS for FX additional data if (isadvanced) { advPartProps = reinterpret_cast(sources + numSources); PSdataEnd = reinterpret_cast(advPartProps + numParticles); @@ -1130,17 +1073,14 @@ PSPRINTLN("updatePSpointers"); PSdataEnd = reinterpret_cast(advPartSize + numParticles); } } - else { - PSdataEnd = reinterpret_cast(sources + numSources); // pointer to first available byte after the PS for FX additional data - } - particles = reinterpret_cast(particleMemoryManager(0, sizeof(PSparticle), availableParticles, effectID)); // get memory, leave buffer size as is (request 0) - /* - DEBUG_PRINTF_P(PSTR(" particles %p "), particles); - DEBUG_PRINTF_P(PSTR(" sources %p "), sources); - DEBUG_PRINTF_P(PSTR(" adv. props %p "), advPartProps); - DEBUG_PRINTF_P(PSTR(" adv. ctrl %p "), advPartSize); - DEBUG_PRINTF_P(PSTR("end %p\n"), PSdataEnd); - */ +//#ifdef DEBUG_PS + Serial.printf_P(PSTR(" particles %p "), particles); + Serial.printf_P(PSTR(" sources %p "), sources); + Serial.printf_P(PSTR(" adv. props %p "), advPartProps); + Serial.printf_P(PSTR(" adv. ctrl %p "), advPartSize); + Serial.printf_P(PSTR("end %p\n"), PSdataEnd); + // #endif + } // blur a matrix in x and y direction, blur can be asymmetric in x and y @@ -1212,7 +1152,7 @@ uint32_t calculateNumberOfParticles2D(uint32_t pixels, bool isadvanced, bool siz numberofParticles /= 8; // if advanced size control is used, much fewer particles are needed note: if changing this number, adjust FX using this accordingly //make sure it is a multiple of 4 for proper memory alignment (easier than using padding bytes) - numberofParticles = ((numberofParticles+3) >> 2) << 2; + numberofParticles = ((numberofParticles+3) >> 2) << 2; // TODO: with a separate particle buffer, this is unnecessary return numberofParticles; } @@ -1221,7 +1161,7 @@ uint32_t calculateNumberOfSources2D(uint32_t pixels, uint32_t requestedsources) int numberofSources = min((pixels) / 8, (uint32_t)requestedsources); numberofSources = max(1, min(numberofSources, ESP8266_MAXSOURCES)); // limit to 1 - 16 #elif ARDUINO_ARCH_ESP32S2 - int numberofSources = min((cpixels) / 6, (uint32_t)requestedsources); + int numberofSources = min((pixels) / 6, (uint32_t)requestedsources); numberofSources = max(1, min(numberofSources, ESP32S2_MAXSOURCES)); // limit to 1 - 48 #else int numberofSources = min((pixels) / 4, (uint32_t)requestedsources); @@ -1236,8 +1176,8 @@ uint32_t calculateNumberOfSources2D(uint32_t pixels, uint32_t requestedsources) bool allocateParticleSystemMemory2D(uint32_t numparticles, uint32_t numsources, bool isadvanced, bool sizecontrol, uint32_t additionalbytes) { PSPRINTLN("PS 2D alloc"); uint32_t requiredmemory = sizeof(ParticleSystem2D); - uint32_t availableparticles; // dummy variable - if((particleMemoryManager(numparticles, sizeof(PSparticle), availableparticles, SEGMENT.mode)) == nullptr) // allocate memory for particles + uint32_t dummy; // dummy variable + if((particleMemoryManager(numparticles, sizeof(PSparticle), dummy, dummy, SEGMENT.mode)) == nullptr) // allocate memory for particles return false; // not enough memory, function ensures a minimum of numparticles are available // functions above make sure these are a multiple of 4 bytes (to avoid alignment issues) @@ -1258,6 +1198,12 @@ bool initParticleSystem2D(ParticleSystem2D *&PartSys, uint32_t requestedsources, uint32_t cols = SEGMENT.virtualWidth(); uint32_t rows = SEGMENT.virtualHeight(); uint32_t pixels = cols * rows; + + // allocate rendering buffer (if this fails, it will render to segment buffer directly) + updateRenderingBuffer(framebuffer, pixels, true); // allocate a rendering buffer + if(advanced) + updateRenderingBuffer(renderbuffer, 100, false); // allocate a 10x10 buffer for rendering advanced particles + uint32_t numparticles = calculateNumberOfParticles2D(pixels, advanced, sizecontrol); PSPRINT(" segmentsize:" + String(cols) + " " + String(rows)); PSPRINT(" request numparticles:" + String(numparticles)); @@ -1267,10 +1213,6 @@ bool initParticleSystem2D(ParticleSystem2D *&PartSys, uint32_t requestedsources, DEBUG_PRINT(F("PS init failed: memory depleted")); return false; } - // allocate rendering buffer (if this fails, it will render to segment buffer directly) - updateRenderingBuffer(framebuffer, pixels, true); // allocate a rendering buffer - if(advanced) - updateRenderingBuffer(renderbuffer, 100, false); // allocate a 10x10 buffer for rendering advanced particles PartSys = new (SEGENV.data) ParticleSystem2D(cols, rows, numparticles, numsources, advanced, sizecontrol); // particle system constructor @@ -1300,8 +1242,8 @@ bool initParticleSystem2D(ParticleSystem2D *&PartSys, uint32_t requestedsources, ParticleSystem1D::ParticleSystem1D(uint32_t length, uint32_t numberofparticles, uint32_t numberofsources, bool isadvanced) { numSources = numberofsources; numParticles = numberofparticles; // number of particles allocated in init - usedpercentage = 255; // use all particles by default - setUsedParticles(usedpercentage); // use all particles by default + availableParticles = 0; // let the memory manager assign + fractionOfParticlesUsed = 255; // use all particles by default advPartProps = NULL; //make sure we start out with null pointers (just in case memory was not cleared) //advPartSize = NULL; updatePSpointers(isadvanced); // set the particle and sources pointer (call this before accessing sprays or particles) @@ -1356,14 +1298,14 @@ void ParticleSystem1D::update(void) { // set percentage of used particles as uint8_t i.e 127 means 50% for example void ParticleSystem1D::setUsedParticles(uint8_t percentage) { - usedpercentage = percentage; + fractionOfParticlesUsed = percentage; PSPRINT(" available particles: "); PSPRINT(availableParticles); PSPRINT(" ,used percentage: "); - PSPRINT(usedpercentage); + PSPRINT(fractionOfParticlesUsed); PSPRINT(" ,used particles: "); PSPRINTLN(usedParticles); - updateUsedParticles(numParticles, availableParticles, usedpercentage, usedParticles); + updateUsedParticles(numParticles, availableParticles, fractionOfParticlesUsed, usedParticles); } void ParticleSystem1D::setWallHardness(uint8_t hardness) { @@ -1581,14 +1523,36 @@ void ParticleSystem1D::ParticleSys_render() { uint32_t brightness; // particle brightness, fades if dying if (framebuffer) { - if (motionBlur > 0) { // using SEGMENT.fadeToBlackBy is much slower, this approximately doubles the speed of fade calculation - for (uint32_t x = 0; x <= maxXpixel; x++) { //!!! TODO: add renderSolo handling here - framebuffer[x] = SEGMENT.getPixelColor(x); // copy to local buffer - fast_color_scale(framebuffer[x], motionBlur); + // update global blur (used for blur transitions) + int32_t motionbluramount = motionBlur; + //int32_t smearamount = smearBlur; + if(pmem->inTransition == effectID) { // FX transition and this is the new FX: fade blur amount + motionbluramount = globalBlur + (((motionbluramount - globalBlur) * (int)SEGMENT.progress()) >> 16); // fade from old blur to new blur during transitions + //smearamount = globalSmear + (((smearamount - globalSmear) * (int)SEGMENT.progress()) >> 16); + } + globalBlur = motionbluramount; + //globalSmear = smearamount; + + // handle buffer blurring or clearing + bool bufferNeedsUpdate = (!pmem->inTransition || pmem->inTransition == effectID); // not a transition; or new FX: update buffer (blur, or clear) + + if(pmem->inTransition == effectID) Serial.print(" new FX "); + else Serial.print(" old FX "); + + if(bufferNeedsUpdate) { + if (globalBlur > 0 || globalSmear > 0) { // blurring active: if not a transition or is newFX, read data from segment before blurring (old FX can render to it afterwards) + Serial.println(" blurring: " + String(globalBlur)); + for (uint32_t x = 0; x <= maxXpixel; x++) { + if (!renderSolo) // sharing the framebuffer with another segment: read buffer back from segment + framebuffer[x] = SEGMENT.getPixelColor(x); // copy to local buffer + fast_color_scale(framebuffer[x], motionBlur); + } + } + else { // no blurring: clear buffer + memset(framebuffer, 0, frameBufferSize * sizeof(CRGB)); + Serial.print(" clearing "); } } - else if(renderSolo) // no blurring and no other systems, clear the buffer (manager skips clearing if there is only one PS) - memset(framebuffer, 0, frameBufferSize * sizeof(CRGB)); // clear the buffer } else { // no local buffer available if (motionBlur > 0) @@ -1604,11 +1568,11 @@ void ParticleSystem1D::ParticleSys_render() { // generate RGB values for particle brightness = min(particles[i].ttl, (uint16_t)255); - baseRGB = ColorFromPalette(SEGPALETTE, particles[i].hue, 255, LINEARBLEND); + baseRGB = ColorFromPalette(SEGPALETTE, particles[i].hue, 255); if (advPartProps) { //saturation is advanced property in 1D system if (advPartProps[i].sat < 255) { - CHSV baseHSV = rgb2hsv_approximate(baseRGB); //convert to HSV + CHSV baseHSV = rgb2hsv_approximate(baseRGB); //convert to HSV //!!!TODO: replace with new rgb2hsv baseHSV.s = advPartProps[i].sat; //set the saturation baseRGB = (CRGB)baseHSV; // convert back to RGB } @@ -1821,9 +1785,9 @@ void ParticleSystem1D::collideParticles(PSparticle1D *particle1, PSparticle1D *p void ParticleSystem1D::updateSystem(void) { setSize(SEGMENT.vLength()); // update size updatePSpointers(advPartProps != NULL); - setUsedParticles(usedpercentage); // update used particles based on percentage TODO: this does not need to be called for each frame, it only changes during transitions. can optimize? how to update after transition is finished? - if (partMemList.size() == 1 && !SEGMENT.isInTransition()) // if number of vector elements is one, this is the only system - renderSolo = true; // TODO: do as in 2D system once that works + setUsedParticles(fractionOfParticlesUsed); // update used particles based on percentage (can change during transitions, execute each frame for code simplicity) + if (partMemList.size() == 1) // if number of vector elements is one, this is the only system + renderSolo = true; else renderSolo = false; } @@ -1836,19 +1800,17 @@ void ParticleSystem1D::updatePSpointers(bool isadvanced) { // a pointer MUST be 4 byte aligned. sizeof() in a struct/class is always aligned to the largest element. if it contains a 32bit, it will be padded to 4 bytes, 16bit is padded to 2byte alignment. // The PS is aligned to 4 bytes, a PSparticle is aligned to 2 and a struct containing only byte sized variables is not aligned at all and may need to be padded when dividing the memoryblock. // by making sure that the number of sources and particles is a multiple of 4, padding can be skipped here as alignent is ensured, independent of struct sizes. - particles = reinterpret_cast(particleMemoryManager(0, sizeof(PSparticle1D), availableParticles, effectID)); // get memory, leave buffer size as is (request 0) + particles = reinterpret_cast(particleMemoryManager(0, sizeof(PSparticle1D), availableParticles, fractionOfParticlesUsed, effectID)); // get memory, leave buffer size as is (request 0) sources = reinterpret_cast(this + 1); // pointer to source(s) PSdataEnd = reinterpret_cast(sources + numSources); // pointer to first available byte after the PS for FX additional data if (isadvanced) { advPartProps = reinterpret_cast(sources + numSources); PSdataEnd = reinterpret_cast(advPartProps + numParticles); } - else - PSdataEnd = reinterpret_cast(sources + numParticles); } -//non class functions to use for initialization -uint32_t calculateNumberOfParticles1D(bool isadvanced) { +//non class functions to use for initialization, fraction is uint8_t: 255 means 100% +uint32_t calculateNumberOfParticles1D(uint32_t fraction, bool isadvanced) { uint32_t numberofParticles = SEGMENT.virtualLength(); // one particle per pixel (if possible) #ifdef ESP8266 uint32_t particlelimit = ESP8266_MAXPARTICLES_1D; // maximum number of paticles allowed @@ -1860,8 +1822,9 @@ uint32_t calculateNumberOfParticles1D(bool isadvanced) { numberofParticles = max((uint32_t)1, min(numberofParticles, particlelimit)); if (isadvanced) // advanced property array needs ram, reduce number of particles to use the same amount numberofParticles = (numberofParticles * sizeof(PSparticle1D)) / (sizeof(PSparticle1D) + sizeof(PSadvancedParticle1D)); + numberofParticles = (numberofParticles * (fraction + 1)) >> 8; // calculate fraction of particles //make sure it is a multiple of 4 for proper memory alignment (easier than using padding bytes) - numberofParticles = ((numberofParticles+3) >> 2) << 2; + numberofParticles = ((numberofParticles+3) >> 2) << 2; // TODO: with a separate particle buffer, this is unnecessary return numberofParticles; } @@ -1881,8 +1844,8 @@ uint32_t calculateNumberOfSources1D(uint32_t requestedsources) { //allocate memory for particle system class, particles, sprays plus additional memory requested by FX bool allocateParticleSystemMemory1D(uint32_t numparticles, uint32_t numsources, bool isadvanced, uint32_t additionalbytes) { uint32_t requiredmemory = sizeof(ParticleSystem1D); - uint32_t availableparticles; // dummy variable - if(particleMemoryManager(numparticles, sizeof(PSparticle1D), availableparticles, SEGMENT.mode) == nullptr) // allocate memory for particles + uint32_t dummy; // dummy variable + if(particleMemoryManager(numparticles, sizeof(PSparticle1D), dummy, dummy, SEGMENT.mode) == nullptr) // allocate memory for particles return false; // not enough memory, function ensures a minimum of numparticles are avialable // functions above make sure these are a multiple of 4 bytes (to avoid alignment issues) if (isadvanced) @@ -1894,18 +1857,20 @@ bool allocateParticleSystemMemory1D(uint32_t numparticles, uint32_t numsources, // initialize Particle System, allocate additional bytes if needed (pointer to those bytes can be read from particle system class: PSdataEnd) // note: percentofparticles is in uint8_t, for example 191 means 75%, (deafaults to 255 or 100% meaning one particle per pixel), can be more than 100% (but not recommended, can cause out of memory) -bool initParticleSystem1D(ParticleSystem1D *&PartSys, uint32_t requestedsources, uint32_t percentofparticles, uint32_t additionalbytes, bool advanced) { +bool initParticleSystem1D(ParticleSystem1D *&PartSys, uint32_t requestedsources, uint8_t fractionofparticles, uint32_t additionalbytes, bool advanced) { if (SEGLEN == 1) return false; // single pixel not supported - uint32_t numparticles = (percentofparticles * calculateNumberOfParticles1D(advanced)) / 100; + // allocat rendering buffers + updateRenderingBuffer(framebuffer, SEGMENT.virtualLength(), true); + if(advanced) + updateRenderingBuffer(renderbuffer, 10, false); + + uint32_t numparticles = calculateNumberOfParticles1D(fractionofparticles, advanced); uint32_t numsources = calculateNumberOfSources1D(requestedsources); if (!allocateParticleSystemMemory1D(numparticles, numsources, advanced, additionalbytes)) { DEBUG_PRINT(F("PS init failed: memory depleted")); return false; } - // allocat rendering buffers - updateRenderingBuffer(framebuffer, SEGMENT.virtualLength(), true); - if(advanced) - updateRenderingBuffer(renderbuffer, 10, false); + PartSys = new (SEGENV.data) ParticleSystem1D(SEGMENT.virtualLength(), numparticles, numsources, advanced); // particle system constructor return true; } @@ -2058,7 +2023,8 @@ void* allocatePSmemory(size_t size, bool overridelimit) { // deallocate memory and update data usage, use with care! void deallocatePSmemory(void* dataptr, uint32_t size) { - PSPRINTLN("deallocating memory:" + String(size)); + PSPRINTLN("deallocating PSmemory:" + String(size)); + if(dataptr == nullptr) return; // safety check free(dataptr); // note: setting pointer null must be done by caller, passing a reference to a cast void pointer is not possible Segment::addUsedSegmentData(size <= Segment::getUsedSegmentData() ? -size : -Segment::getUsedSegmentData()); } @@ -2076,10 +2042,11 @@ ist also in transition und es ist der neue effekt und der watchdog ist auf null // handle particle pointer, creates/extends buffer if needed and handles transition handover // function is called in PS setup and updatepointer function -void* particleMemoryManager(const uint32_t requestedParticles, size_t structSize, uint32_t &availableToPS, const uint8_t effectID) { +void* particleMemoryManager(const uint32_t requestedParticles, size_t structSize, uint32_t &availableToPS, uint8_t fractionUsed, const uint8_t effectID) { pmem = getPartMem(); PSPRINT(" getParticlePointer "); void* buffer = nullptr; + if (pmem) { // segment has a buffer PSPRINT(" buffer found "); if (requestedParticles) { // request for a new buffer, this is an init call @@ -2092,11 +2059,13 @@ void* particleMemoryManager(const uint32_t requestedParticles, size_t structSize buffer = allocatePSmemory(requestsize, true); // calloc new memory in FX data, override limit (temporary buffer) if (buffer) { // allocaction successful, copy old particles to new buffer memcpy(buffer, pmem->particleMemPointer, pmem->buffersize); // copy old particle buffer note: only required if transition but copy is fast and rarely happens - deallocatePSmemory( pmem->particleMemPointer, pmem->buffersize); // free old memory + deallocatePSmemory(pmem->particleMemPointer, pmem->buffersize); // free old memory pmem->particleMemPointer = buffer; // set new buffer pmem->buffersize = requestsize; // update buffer size } // if buffer was not extended, the old, smaller buffer is used - } + else + return nullptr; // no memory available + } } if (pmem->watchdog == 1) { // if a PS already exists during particle request, it kicked the watchdog in last frame, servicePSmem() adds 1 afterwards -> PS to PS transition PSPRINTLN("********** PS is in transition, new FX:" + String(effectID)); @@ -2107,7 +2076,7 @@ void* particleMemoryManager(const uint32_t requestedParticles, size_t structSize // availableToPS = requestedParticles; // all particles are available todo: THIS IS TBD, probably ok to feed particles slowly // pmem->inTransition = false; // no transition TODO: does this need to be set here? this function is called again in updatePS, can deactivate it then, right? } - availableToPS = 0; // start out with zero particles, transition below will initialize and transfer tehm + availableToPS = 0; // start out with zero particles, transition below will initialize and transfer them (this line is not really needed, could be removed?) return pmem->particleMemPointer; // return the available buffer on init call TODO: maybe split this into two functions, one for init and one for get? } PSPRINT(" use existing, "); @@ -2130,7 +2099,7 @@ void* particleMemoryManager(const uint32_t requestedParticles, size_t structSize free(buffer); return nullptr; } - availableToPS = 0; // new PS starts with zero particles, they are transferred in the next call + availableToPS = 0; // new PS starts with zero particles, they are transferred in the next call (this line is not really needed, could be removed?) return buffer; // directly return the buffer on init call } #ifdef WLED_DEBUG_PS @@ -2151,9 +2120,10 @@ void* particleMemoryManager(const uint32_t requestedParticles, size_t structSize if (SEGMENT.mode == effectID) { // new effect ID -> function was called from new FX PSPRINT(" new FX"); PSPRINT(" progress: " + String(progress)); - newAvailable = (maxParticles * (progress + 1)) >> 16; // update total particles available to this PS - buffer = (void*)((uint8_t*)buffer + (maxParticles - newAvailable) * structSize); // new effect gets the end of the buffer - //uint32_t newbrfaddr = (uintptr_t)buffer; + newAvailable = (maxParticles * progress) >> 16; // update total particles available to this PS + uint32_t brforigin = (uintptr_t)buffer; // save old buffer pointer for !!!!DEBUG + //!!! buffer = (void*)((uint8_t*)buffer + ((maxParticles - newAvailable) * structSize)); // new effect gets the end of the buffer + uint32_t newbrfaddr = (uintptr_t)buffer; #ifdef WLED_DEBUG_PS PSPRINT(" new buffer startaddress: 0x"); Serial.println((uintptr_t)buffer, HEX); @@ -2161,18 +2131,9 @@ void* particleMemoryManager(const uint32_t requestedParticles, size_t structSize Serial.println((newbrfaddr-brforigin)/structSize); PSPRINT(" particle start: " + String(maxParticles - newAvailable)); #endif - uint32_t totransfer = newAvailable - availableToPS; // number of particles to transfer in this transition update - //TODO: maybe memcopy the buffer? if usedparticle number is small, end of the buffer holds alive but unused particles... copy would erase old particles though. need to think about a good way to do it. - - // initialize newly transferred particles note: to have PS interact during transition, this must be changed. could initialize TTL and perpetual only - + int32_t totransfer = newAvailable - availableToPS; // number of particles to transfer in this transition update PSPRINT(" totransfer: " + String(totransfer)); - if(totransfer <= newAvailable) { // overflow check TODO: why do overflows happen? does it still happen with the new calculation? -> not in normal transfer, need to check quick transfer changes -> seems ok TODO: can this be removed? - particleHandover(buffer, structSize, totransfer); - } - else { - PSPRINTLN(" Particle transfer overflow! "); - } + //particleHandover(buffer, structSize, totransfer); } else { // this was called from the old FX PSPRINT(" old FX"); @@ -2182,7 +2143,6 @@ void* particleMemoryManager(const uint32_t requestedParticles, size_t structSize PSPRINT(" inv.prog: " + String(progress)); newAvailable = ((maxParticles * progress) >> 16); if(newAvailable > 0) newAvailable--; // -1 to avoid overlapping memory in 1D<->2D transitions - //newAvailable = min(newAvailable, numPixels); // limit to segment size in case a larger segment initialized the buffer -> not needed during transfer, looks better without this PSPRINT(" newAvailable: " + String(newAvailable)); PSPRINT(" oldAvailable: " + String(availableToPS)); if(newAvailable > availableToPS) newAvailable = availableToPS; // do not increase available particles (if memory was extended) @@ -2193,13 +2153,31 @@ void* particleMemoryManager(const uint32_t requestedParticles, size_t structSize PSPRINT(" final available to PS: " + String(availableToPS)); } else { // no PS transition, full buffer available if(pmem->transferParticles) { // transition ended (or blending is disabled) -> transfer all remaining particles - uint32_t numPixels = SEGMENT.vWidth() * SEGMENT.vHeight(); // works for 1D and 2D, height is 1 in 1D uint32_t maxParticles = pmem->buffersize / structSize; // maximum number of particles that fit in the buffer if (maxParticles > availableToPS) { // not all particles transferred yet - uint32_t totransfer = maxParticles - availableToPS; // transfer all remaining particles - particleHandover(buffer, structSize, totransfer); + int32_t totransfer = maxParticles - availableToPS; // transfer all remaining particles + //particleHandover(buffer, structSize, totransfer); + } + availableToPS = maxParticles; // update available particles + // kill unused particles to they do not re-appear when transitioning to next FX + uint32_t used = (maxParticles * fractionUsed) >> 8; // calculate number of particles used (this may not always be accurate but good enough) + #ifndef WLED_DISABLE_PARTICLESYSTEM2D + if (structSize == sizeof(PSparticle)) { // 2D particle + PSparticle *particles = (PSparticle *)buffer; + for (uint32_t i = used; i < maxParticles; i++) { + particles[i].ttl = 0; // kill unused particles + } + } + else // 1D particle system + #endif + { + #ifndef WLED_DISABLE_PARTICLESYSTEM1D + PSparticle1D *particles = (PSparticle1D *)buffer; + for (uint32_t i = used; i < maxParticles; i++) { + particles[i].ttl = 0; // kill unused particles + } + #endif } - availableToPS = min(maxParticles, numPixels);; // update available particles limit to segment size in case a larger segment initialized the buffer (FX depend may depend on it) pmem->particleType = structSize; // update particle type pmem->transferParticles = false; } @@ -2212,7 +2190,7 @@ void* particleMemoryManager(const uint32_t requestedParticles, size_t structSize } // (re)initialize particles in the particle buffer for use in the new FX -void particleHandover(void *buffer, size_t structSize, uint32_t numToTransfer) { +void particleHandover(void *buffer, size_t structSize, int32_t numToTransfer) { // TODO: need to be more clever: need to clear buffer if 2D->1D or 1D->2D transition or settings are nonsensical. #ifndef WLED_DISABLE_PARTICLESYSTEM2D if (structSize == sizeof(PSparticle)) { // 2D particle @@ -2220,7 +2198,7 @@ void particleHandover(void *buffer, size_t structSize, uint32_t numToTransfer) { if (pmem->particleType != sizeof(PSparticle)) { // check if we are being handed over from a 1D system, clear buffer if so memset(buffer, 0, numToTransfer * sizeof(PSparticle)); // clear buffer } - for (uint32_t i = 0; i < numToTransfer; i++) { + for (int32_t i = 0; i < numToTransfer; i++) { particles[i].perpetual = false; // particle ages if (particles[i].outofbounds) particles[i].ttl = 0; // kill out of bounds @@ -2239,12 +2217,12 @@ void particleHandover(void *buffer, size_t structSize, uint32_t numToTransfer) { if (pmem->particleType != sizeof(PSparticle1D)) { memset(buffer, 0, numToTransfer * sizeof(PSparticle1D)); // clear buffer } - for (uint32_t i = 0; i < numToTransfer; i++) { + for (int32_t i = 0; i < numToTransfer; i++) { particles[i].perpetual = false; // particle ages if (particles[i].outofbounds) particles[i].ttl = 0; // kill out of bounds - else if (particles[i].ttl > 50) - particles[i].ttl = 50; // reduce TTL so it will die soon + else if (particles[i].ttl > 200) + particles[i].ttl = 200; // reduce TTL so it will die soon } #endif } @@ -2252,11 +2230,12 @@ void particleHandover(void *buffer, size_t structSize, uint32_t numToTransfer) { // function to update the framebuffer and renderbuffer void updateRenderingBuffer(CRGB* buffer, uint32_t requiredpixels, bool isFramebuffer) { +/* PSPRINTLN("updateRenderingBuffer"); uint32_t currentBufferSize = isFramebuffer ? frameBufferSize : renderBufferSize; if(currentBufferSize < requiredpixels) { // check current buffer size if(buffer) deallocatePSmemory((void*)buffer, currentBufferSize * sizeof(CRGB)); - buffer = (CRGB *)allocatePSmemory(requiredpixels * sizeof(CRGB), false); + buffer = reinterpret_cast(allocatePSmemory(requiredpixels * sizeof(CRGB), false)); if(buffer) { if(isFramebuffer) { framebuffer = buffer; @@ -2267,6 +2246,7 @@ void updateRenderingBuffer(CRGB* buffer, uint32_t requiredpixels, bool isFramebu } return; } else { + Serial.println("!!! Memory allocation failed for rendering buffer !!!"); if(isFramebuffer) { framebuffer = nullptr; frameBufferSize = 0; @@ -2275,7 +2255,7 @@ void updateRenderingBuffer(CRGB* buffer, uint32_t requiredpixels, bool isFramebu renderBufferSize = 0; } } - } + }*/ } // get the pointer to the particle memory for the segment @@ -2306,14 +2286,16 @@ void servicePSmem() { PSPRINT(" watchdog: "); PSPRINTLN(partMemList[i].watchdog); if (partMemList[i].watchdog > MAX_MEMIDLE) { - PSPRINTLN("psmem free"); //deallocating memory:2560 + PSPRINTLN("psmem free"); //deallocating memory deallocatePSmemory(partMemList[i].particleMemPointer, partMemList[i].buffersize); // Free memory partMemList.erase(partMemList.begin() + i); // Remove entry - partMemList.shrink_to_fit(); // partMemList is small, memory operations should be unproblematic + //partMemList.shrink_to_fit(); // partMemList is small, memory operations should be unproblematic (this may lead to mem fragmentation, removed for now) } } } else { // no particle system running, release buffer memory + /* + Serial.println("ps buffer free"); //deallocating memory if(framebuffer) { deallocatePSmemory((void*)framebuffer, frameBufferSize * sizeof(CRGB)); // free the buffers framebuffer = nullptr; @@ -2323,8 +2305,9 @@ void servicePSmem() { deallocatePSmemory((void*)renderbuffer, renderBufferSize * sizeof(CRGB)); renderbuffer = nullptr; renderBufferSize = 0; - } + }*/ } + } @@ -2358,31 +2341,44 @@ static void applyBlurOrClear2D(uint32_t width, uint32_t height, uint8_t fxID) { } // transfer the frame buffer to the segment and handle transitional rendering (both FX render to the same buffer so they mix) -void transferBuffer(uint32_t width, uint32_t height) { +void transferBuffer(uint32_t width, uint32_t height, bool useAdditiveTransfer) { PSPRINT(" xfer buf "); if(!framebuffer) return; // no buffer, nothing to transfer + + #ifndef WLED_DISABLE_MODE_BLEND + bool tempBlend = SEGMENT.getmodeBlend(); + if (pmem->inTransition) + SEGMENT.modeBlend(false); // temporarily disable FX blending in PS to PS transition (using local buffer to do PS blending) + #endif + if(height) { // is 2D, 1D passes height = 0 int32_t yflipped; + height--; // height is number of pixels, convert to array index PSPRINT("xfer 2D"); - #ifndef WLED_DISABLE_MODE_BLEND - bool tempBlend = SEGMENT.getmodeBlend(); - if (pmem->inTransition) - SEGMENT.modeBlend(false); // temporarily disable FX blending in PS to PS transition (using local buffer to do PS blending) - #endif for (uint32_t y = 0; y <= height; y++) { - yflipped = height - y - 1; + yflipped = height - y; int index = y * width; // current row index for 1D buffer for (uint32_t x = 0; x < width; x++) { CRGB *c = &framebuffer[index++]; uint32_t clr = RGBW32(c->r,c->g,c->b,0); // convert to 32bit color - //if(clr > 0) // not black TODO: not transferring black is faster and enables overlay, but requries proper handling of buffer clearing, which is quite complex and probably needs a change to SEGMENT handling. - SEGMENT.setPixelColorXY((int)x, (int)yflipped, clr); + if(useAdditiveTransfer) { + uint32_t segmentcolor = SEGMENT.getPixelColorXY((int)x, (int)yflipped); + CRGB segmentRGB = CRGB(segmentcolor); + if(clr == 0) // frame buffer is black, just update the framebuffer + *c = segmentRGB; + else { // not black + if(segmentcolor) { + fast_color_add(*c, segmentRGB); // add segment color back to buffer if not black TODO: since both are 32bit, this could be made faster using the new improved wled 32bit adding (see speed improvements PR) + clr = RGBW32(c->r,c->g,c->b,0); // convert to 32bit color (again) + } + SEGMENT.setPixelColorXY((int)x, (int)yflipped, clr); // save back to segment after adding local buffer + } + } + //if(clr > 0) // not black TODO: not transferring black is faster and enables overlay, but requires proper handling of buffer clearing, which is quite complex and probably needs a change to SEGMENT handling. + else + SEGMENT.setPixelColorXY((int)x, (int)yflipped, clr); } } - #ifndef WLED_DISABLE_MODE_BLEND - SEGMENT.modeBlend(tempBlend); - #endif - //applyBlurOrClear2D(width, height, fxID); // apply blur (or clear buffer) after transferring to be ready for next frame } else { // 1D system for (uint32_t x = 0; x < width; x++) { CRGB *c = &framebuffer[x]; @@ -2391,11 +2387,9 @@ void transferBuffer(uint32_t width, uint32_t height) { SEGMENT.setPixelColor((int)x, color); } } - /* - if(!renderSolo) { // there are other segments with particle systems, clear the buffer (PS takes over if rendersolo is true) - //memset(framebuffer, 0, frameBufferSize * sizeof(CRGB)); // clear the buffer TODO: !!! add this back in - PSPRINTLN(" buffer cleared "); - }*/ + #ifndef WLED_DISABLE_MODE_BLEND + SEGMENT.modeBlend(tempBlend); // restore blending mode + #endif /* #ifdef WLED_DEBUG_PS PSPRINT(" done. framebfr addr: 0x"); diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index fb68cd3d53..1f1e2df3d5 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -14,7 +14,7 @@ #include "wled.h" #define PS_P_MAXSPEED 120 // maximum speed a particle can have (vx/vy is int8) -#define MAX_MEMIDLE 50 // max idle time (in frames) before memory is deallocated (if deallocated during an effect, it will crash!) +#define MAX_MEMIDLE 10 // max idle time (in frames) before memory is deallocated (if deallocated during an effect, it will crash!) //#define WLED_DEBUG_PS @@ -37,16 +37,16 @@ struct partMem { bool transferParticles; // if set, particles in buffer are transferred to new FX }; -void* particleMemoryManager(const uint32_t requestedParticles, size_t structSize, uint32_t &availableToPS, const uint8_t effectID); // update particle memory pointer, handles transitions -void particleHandover(void *buffer, size_t structSize, uint32_t numParticles); +void* particleMemoryManager(const uint32_t requestedParticles, size_t structSize, uint32_t &availableToPS, uint8_t fractionUsed, const uint8_t effectID); // update particle memory pointer, handles memory transitions +void particleHandover(void *buffer, size_t structSize, int32_t numParticles); //extern CRGB *framebuffer; // local frame buffer for rendering //extern CRGB *renderbuffer; // local particle render buffer for advanced particles //extern uint16_t frameBufferSize; // size in pixels, used to check if framebuffer is large enough for current segment TODO: make this in bytes, not in pixels //extern uint16_t renderBufferSize; // size in pixels, if allcoated by a 1D system it needs to be updated for 2D partMem* getPartMem(void); // returns pointer to memory struct for current segment or nullptr void updateRenderingBuffer(CRGB* buffer, uint32_t requiredsize, bool isFramebuffer); // allocate CRGB rendering buffer, update size if needed -void transferBuffer(uint32_t width, uint32_t height); // transfer the buffer to the segment -//TODO: add 1D version +void transferBuffer(uint32_t width, uint32_t height, bool useAdditiveTransfer = false); // transfer the buffer to the segment (supports 1D and 2D) +//TODO: add 1D version? or remove 2D version? void servicePSmem(); // increments watchdog, frees memory if idle too long // update number of particles to use, must never be more than allocated (= particles allocated by the calling system) @@ -56,8 +56,7 @@ inline void updateUsedParticles(const uint32_t allocated, const uint32_t availab #endif -//TODO: updated ESP32_MAXPARTICLES, can have more with new mem manager, revisit the calculation -// TODO: maybe update PS_P_MINSURFACEHARDNESS for 2D? its a bit too sticky already at hardness 100 +//TODO: maybe update PS_P_MINSURFACEHARDNESS for 2D? its a bit too sticky already at hardness 100 #ifndef WLED_DISABLE_PARTICLESYSTEM2D // memory allocation #define ESP8266_MAXPARTICLES 300 // enough up to 20x20 pixels @@ -92,15 +91,15 @@ typedef union { } PSsettings2D; //struct for a single particle -typedef struct { // 11 bytes +typedef struct { // 12 bytes int16_t x; // x position in particle system int16_t y; // y position in particle system + uint16_t ttl; // time to live int8_t vx; // horizontal velocity int8_t vy; // vertical velocity uint8_t hue; // color hue uint8_t sat; // particle color saturation //uint16_t ttl : 12; // time to live, 12 bit or 4095 max (which is 50s at 80FPS) - uint16_t ttl; // time to live bool outofbounds : 1; // out of bounds flag, set to true if particle is outside of display area bool collide : 1; // if set, particle takes part in collisions bool perpetual : 1; // if set, particle does not age (TTL is not decremented in move function, it still dies from killoutofbounds) @@ -168,7 +167,7 @@ class ParticleSystem2D { void pointAttractor(uint16_t particleindex, PSparticle *attractor, uint8_t strength, bool swallow); void lineAttractor(uint16_t particleindex, PSparticle *attractorcenter, uint16_t attractorangle, uint8_t strength); // set options - void setUsedParticles(uint8_t percentage); + void setUsedParticles(uint8_t percentage); // set the percentage of particles used in the system, 255=100% 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 setWallRoughness(uint8_t roughness); // wall roughness randomizes wall collisions @@ -207,7 +206,6 @@ class ParticleSystem2D { void collideParticles(PSparticle *particle1, PSparticle *particle2, int32_t dx, int32_t dy); void fireParticleupdate(); //utility functions - void transferBuffer(bool additive = false); // transfer the framebuffer to the segment void updatePSpointers(bool isadvanced, bool sizecontrol); // update the data pointers to current segment data space void updateSize(PSadvancedParticle *advprops, PSsizeControl *advsize); // advanced size control void getParticleXYsize(PSadvancedParticle *advprops, PSsizeControl *advsize, uint32_t &xsize, uint32_t &ysize); @@ -217,6 +215,7 @@ class ParticleSystem2D { PSsettings2D particlesettings; // settings used when updating particles (can also used by FX to move sources), do not edit properties directly, use functions above uint32_t numParticles; // total number of particles allocated by this system note: during transitions, less are available, use availableParticles uint32_t availableParticles; // number of particles available for use (can be more or less than numParticles, assigned by memory manager) + uint8_t fractionOfParticlesUsed; // percentage of particles used in the system (255=100%), used during transition updates uint32_t emitIndex; // index to count through particles to emit so searching for dead pixels is faster int32_t collisionHardness; uint32_t wallHardness; @@ -231,7 +230,6 @@ class ParticleSystem2D { uint8_t motionBlur; // motion blur, values > 100 gives smoother animations. Note: motion blurring does not work if particlesize is > 0 uint8_t smearBlur; // 2D smeared blurring of full frame uint8_t effectID; // ID of the effect that is using this particle system, used for transitions - uint8_t usedpercentage; // percentage of particles used in the system, used during transition updates }; void blur2D(CRGB *colorbuffer, uint32_t xsize, uint32_t ysize, uint32_t xblur, uint32_t yblur, uint32_t xstart = 0, uint32_t ystart = 0, bool isparticle = false); @@ -327,7 +325,7 @@ class ParticleSystem1D void applyGravity(PSparticle1D *part); // applies gravity to single particle (use this for sources) void applyFriction(int32_t coefficient); // apply friction to all used particles // set options - void setUsedParticles(uint8_t percentage); + void setUsedParticles(uint8_t percentage); // set the percentage of particles used in the system, 255=100% void setWallHardness(uint8_t hardness); // hardness for bouncing on the wall if bounceXY is set void setSize(uint16_t x); //set particle system size (= strip length) void setWrap(bool enable); @@ -369,6 +367,7 @@ class ParticleSystem1D PSsettings1D particlesettings; // settings used when updating particles uint32_t numParticles; // total number of particles allocated by this system note: never use more than this, even if more are available (only this many advanced particles are allocated) uint32_t availableParticles; // number of particles available for use (can be more or less than numParticles, assigned by memory manager) + uint8_t fractionOfParticlesUsed; // percentage of particles used in the system (255=100%), used during transition updates uint32_t emitIndex; // index to count through particles to emit so searching for dead pixels is faster int32_t collisionHardness; uint32_t particleHardRadius; // hard surface radius of a particle, used for collision detection @@ -381,11 +380,10 @@ class ParticleSystem1D uint8_t particlesize; // global particle size, 0 = 1 pixel, 1 = 2 pixels, larger sizez TBD (TODO: need larger sizes?) uint8_t motionBlur; // enable motion blur, values > 100 gives smoother animations. Note: motion blurring does not work if particlesize is > 0 uint8_t effectID; // ID of the effect that is using this particle system, used for transitions - uint8_t usedpercentage; // percentage of particles used in the system, used for transitions }; -bool initParticleSystem1D(ParticleSystem1D *&PartSys, uint32_t requestedsources, uint32_t percentofparticles = 255, uint32_t additionalbytes = 0, bool advanced = false); -uint32_t calculateNumberOfParticles1D(bool isadvanced); +bool initParticleSystem1D(ParticleSystem1D *&PartSys, uint32_t requestedsources, uint8_t fractionofparticles = 255, uint32_t additionalbytes = 0, bool advanced = false); +uint32_t calculateNumberOfParticles1D(uint32_t fraction, bool isadvanced); uint32_t calculateNumberOfSources1D(uint32_t requestedsources); bool allocateParticleSystemMemory1D(uint32_t numparticles, uint32_t numsources, bool isadvanced, uint32_t additionalbytes); void blur1D(CRGB *colorbuffer, uint32_t size, uint32_t blur, uint32_t start = 0); From 72525a20f6078a7083c1c65a71a49084b7f0d6bb Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sat, 28 Dec 2024 17:32:02 +0100 Subject: [PATCH 149/219] BUGFIX: finally found and squased the memory bug, no more random crashes also: removed debug outputs and minor cleanup --- wled00/FXparticleSystem.cpp | 321 +++++++----------------------------- wled00/FXparticleSystem.h | 7 +- 2 files changed, 61 insertions(+), 267 deletions(-) diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index d2bebb6b70..e8bfb0beea 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -31,12 +31,10 @@ static void fast_color_scale(CRGB &c, const uint32_t scale); // fast scaling fun // global variables for memory management std::vector partMemList; // list of particle memory pointers partMem *pmem = nullptr; // pointer to particle memory of current segment, updated in particleMemoryManager() -//CRGB *framebuffer = nullptr; // local frame buffer for rendering -//CRGB *renderbuffer = nullptr; // local particle render buffer for advanced particles -CRGB framebuffer[512]; // local frame buffer for rendering !!!! just a test -CRGB renderbuffer[100]; // local particle render buffer for advanced particles -uint16_t frameBufferSize = 512; //!!!0 size in pixels, used to check if framebuffer is large enough for current segment -uint16_t renderBufferSize = 100; //!!!0 size in pixels, if allcoated by a 1D system it needs to be updated for 2D +CRGB *framebuffer = nullptr; // local frame buffer for rendering +CRGB *renderbuffer = nullptr; // local particle render buffer for advanced particles +uint16_t frameBufferSize = 0; // size in pixels, used to check if framebuffer is large enough for current segment +uint16_t renderBufferSize = 0; // size in pixels, if allcoated by a 1D system it needs to be updated for 2D bool renderSolo = false; // is set to true if this is the only particle system using the so it can use the buffer continuously (faster blurring) int32_t globalBlur = 0; // motion blur to apply if multiple PS are using the buffer int32_t globalSmear = 0; // smear-blur to apply if multiple PS are using the buffer @@ -565,45 +563,30 @@ void ParticleSystem2D::pointAttractor(uint16_t particleindex, PSparticle *attrac // 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 // warning: do not render out of bounds particles or system will crash! rendering does not check if particle is out of bounds -// fireintensity and firemode are optional arguments (fireintensity is only used in firemode) +// firemode is only used for PS Fire FX void ParticleSystem2D::ParticleSys_render(bool firemode, uint32_t fireintensity) { CRGB baseRGB; uint32_t brightness; // particle brightness, fades if dying static bool useAdditiveTransfer = false; // use add instead of set for buffer transferring - if(!pmem->inTransition) useAdditiveTransfer = false; // additive rendering is only used in PS FX transitions - // handle blurring and framebuffer update - if (framebuffer) { - // update global blur (used for blur transitions) - int32_t motionbluramount = motionBlur; - int32_t smearamount = smearBlur; - if(pmem->inTransition == effectID) { // FX transition and this is the new FX: fade blur amount - motionbluramount = globalBlur + (((motionbluramount - globalBlur) * (int)SEGMENT.progress()) >> 16); // fade from old blur to new blur during transitions - smearamount = globalSmear + (((smearamount - globalSmear) * (int)SEGMENT.progress()) >> 16); - } - globalBlur = motionbluramount; - globalSmear = smearamount; - /* - Note on blurring / clearing - when rendersolo is active, blurring can be done on the buffer directly, if this is a transition, skip blurring if this is the old FX (i.e. blur before new FX is rendered) - if rendersolo is not set, the buffer must be either cleared (if no blurring and no pmem->inTransition) or read from segment (if blur is active AND this is not the old FX i.e. pmem->inTransition != fxID) - if multiple segments are used, rendersolor is false, if then transitioning from classic to PS FX, blurring is applied. if transitioning away from PS, blurring is skipped (as this is the old FX) - - // first check if rendersolo is active, if yes, check if this is a transition - // if rendersolo is active and not a transition, blur the buffer or clear it - // if this is a transition, only clear or blur if this is the new FX (which is rendered first) - // if this is the new FX in a transition and rendersolo is not active, read the buffer from the segment befor applying blur (clear otherwise) -*/ + // update global blur (used for blur transitions) + int32_t motionbluramount = motionBlur; + int32_t smearamount = smearBlur; + if(pmem->inTransition == effectID) { // FX transition and this is the new FX: fade blur amount + motionbluramount = globalBlur + (((motionbluramount - globalBlur) * (int)SEGMENT.progress()) >> 16); // fade from old blur to new blur during transitions + smearamount = globalSmear + (((smearamount - globalSmear) * (int)SEGMENT.progress()) >> 16); + } + globalBlur = motionbluramount; + globalSmear = smearamount; + // handle blurring and framebuffer update + if (framebuffer) { + if(!pmem->inTransition) useAdditiveTransfer = false; // additive rendering is only used in PS FX transitions // handle buffer blurring or clearing bool bufferNeedsUpdate = (!pmem->inTransition || pmem->inTransition == effectID); // not a transition; or new FX: update buffer (blur, or clear) - if(pmem->inTransition == effectID) Serial.print(" new FX "); - else Serial.print(" old FX "); - if(bufferNeedsUpdate) { if (globalBlur > 0 || globalSmear > 0) { // blurring active: if not a transition or is newFX, read data from segment before blurring (old FX can render to it afterwards) - Serial.println(" blurring: " + String(globalBlur)); for (uint32_t y = 0; y <= maxYpixel; y++) { int index = y * (maxXpixel + 1); for (uint32_t x = 0; x <= maxXpixel; x++) { @@ -618,7 +601,6 @@ void ParticleSystem2D::ParticleSys_render(bool firemode, uint32_t fireintensity) } else { // no blurring: clear buffer memset(framebuffer, 0, frameBufferSize * sizeof(CRGB)); - Serial.print(" clearing "); } } if(particlesize > 0 && pmem->inTransition) { // if particle size is used by FX we need a clean buffer @@ -630,13 +612,11 @@ void ParticleSystem2D::ParticleSys_render(bool firemode, uint32_t fireintensity) bool tempBlend = SEGMENT.getmodeBlend(); SEGMENT.modeBlend(false); // temporarily disable FX blending in PS to PS transition (local buffer is used to do PS blending) #endif - Serial.print(" intermediate xfer "); int yflipped; for (uint32_t y = 0; y <= maxYpixel; y++) { yflipped = maxYpixel - y; int index = y * (maxXpixel + 1); // current row index for 1D buffer for (uint32_t x = 0; x <= maxXpixel; x++) { - //if(globalBlur) fast_color_scale(framebuffer[index], globalBlur); // apply motion blurring CRGB *c = &framebuffer[index++]; uint32_t clr = RGBW32(c->r,c->g,c->b,0); // convert to 32bit color //if(clr > 0) // not black TODO: not transferring black is faster and enables overlay, but requries proper handling of buffer clearing, which is quite complex and probably needs a change to SEGMENT handling. @@ -646,7 +626,6 @@ void ParticleSystem2D::ParticleSys_render(bool firemode, uint32_t fireintensity) #ifndef WLED_DISABLE_MODE_BLEND SEGMENT.modeBlend(tempBlend); #endif - Serial.println(" clearing2 "); memset(framebuffer, 0, frameBufferSize * sizeof(CRGB)); // clear the buffer after transfer useAdditiveTransfer = true; // additive transfer reads from segment, adds that to the frame-buffer and writes back to segment, after transfer, segment and buffer are identical } @@ -687,7 +666,6 @@ void ParticleSystem2D::ParticleSys_render(bool firemode, uint32_t fireintensity) uint32_t passes = particlesize / 64 + 1; // number of blur passes, four passes max uint32_t bluramount = particlesize; uint32_t bitshift = 0; - for (uint32_t i = 0; i < passes; i++) { if (i == 2) // for the last two passes, use higher amount of blur (results in a nicer brightness gradient with soft edges) bitshift = 1; @@ -700,7 +678,6 @@ void ParticleSystem2D::ParticleSys_render(bool firemode, uint32_t fireintensity) bluramount -= 64; } } - // apply 2D blur to rendered frame TODO: this needs proper transition handling, maybe combine it with motion blur handling? if(globalSmear > 0) { if (framebuffer) @@ -708,12 +685,10 @@ void ParticleSystem2D::ParticleSys_render(bool firemode, uint32_t fireintensity) else SEGMENT.blur(smearBlur, true); } - // transfer framebuffer to segment if available if (framebuffer && pmem->inTransition != effectID) { // not in transition or is old FX transferBuffer(maxXpixel + 1, maxYpixel + 1, useAdditiveTransfer); } - else PSPRINTLN("skip xfer"); } // calculate pixel positions and brightness distribution and render the particle to local buffer or global buffer @@ -860,51 +835,10 @@ void ParticleSystem2D::renderParticle(const uint32_t particleindex, const uint32 } } } - -/* - // debug: check coordinates if out of buffer boundaries print out some info (rendering out of bounds particle causes crash!) - for (uint32_t d = 0; d < 4; d++) - { - if (pixco[d][0] < 0 || pixco[d][0] > maxXpixel) - { - //PSPRINT("<"); - if (pxlbrightness[d] >= 0) - { - PSPRINT("uncought out of bounds: x:"); - PSPRINT(pixco[d][0]); - PSPRINT(" y:"); - PSPRINT(pixco[d][1]); - PSPRINT("particle x="); - PSPRINT(particles[particleindex].x); - PSPRINT(" y="); - PSPRINTLN(particles[particleindex].y); - pxlbrightness[d] = -1; // do not render - } - } - if (pixco[d][1] < 0 || pixco[d][1] > maxYpixel) - { - //PSPRINT("^"); - if (pxlbrightness[d] >= 0) - { - PSPRINT("uncought out of bounds: y:"); - PSPRINT(pixco[d][0]); - PSPRINT(" y:"); - PSPRINT(pixco[d][1]); - PSPRINT("particle x="); - PSPRINT(particles[particleindex].x); - PSPRINT(" y="); - PSPRINTLN(particles[particleindex].y); - pxlbrightness[d] = -1; // do not render - } - } - } -*/ } - // detect collisions in an array of particles and handle them void ParticleSystem2D::handleCollisions() { - // detect and handle collisions uint32_t i, j; uint32_t startparticle = 0; uint32_t endparticle = usedParticles >> 1; // do half the particles, significantly speeds things up @@ -1039,9 +973,9 @@ void ParticleSystem2D::collideParticles(PSparticle *particle1, PSparticle *parti // update size and pointers (memory location and size can change dynamically) // note: do not access the PS class in FX befor running this function (or it messes up SEGENV.data) void ParticleSystem2D::updateSystem(void) { - PSPRINTLN("updateSystem2D"); setMatrixSize(SEGMENT.vWidth(), SEGMENT.vHeight()); + updateRenderingBuffer(SEGMENT.vWidth() * SEGMENT.vHeight(), true); // update or create rendering buffer (segment size can change at any time) updatePSpointers(advPartProps != nullptr, advPartSize != nullptr); // update pointers to PS data, also updates availableParticles setUsedParticles(fractionOfParticlesUsed); // update used particles based on percentage (can change during transitions, execute each frame for code simplicity) if (partMemList.size() == 1) // if number of vector elements is one, this is the only system @@ -1073,13 +1007,13 @@ PSPRINTLN("updatePSpointers"); PSdataEnd = reinterpret_cast(advPartSize + numParticles); } } -//#ifdef DEBUG_PS +#ifdef DEBUG_PS Serial.printf_P(PSTR(" particles %p "), particles); Serial.printf_P(PSTR(" sources %p "), sources); Serial.printf_P(PSTR(" adv. props %p "), advPartProps); Serial.printf_P(PSTR(" adv. ctrl %p "), advPartSize); Serial.printf_P(PSTR("end %p\n"), PSdataEnd); - // #endif + #endif } @@ -1198,11 +1132,8 @@ bool initParticleSystem2D(ParticleSystem2D *&PartSys, uint32_t requestedsources, uint32_t cols = SEGMENT.virtualWidth(); uint32_t rows = SEGMENT.virtualHeight(); uint32_t pixels = cols * rows; - - // allocate rendering buffer (if this fails, it will render to segment buffer directly) - updateRenderingBuffer(framebuffer, pixels, true); // allocate a rendering buffer if(advanced) - updateRenderingBuffer(renderbuffer, 100, false); // allocate a 10x10 buffer for rendering advanced particles + updateRenderingBuffer(100, false); // allocate a 10x10 buffer for rendering advanced particles uint32_t numparticles = calculateNumberOfParticles2D(pixels, advanced, sizecontrol); PSPRINT(" segmentsize:" + String(cols) + " " + String(rows)); @@ -1246,8 +1177,8 @@ ParticleSystem1D::ParticleSystem1D(uint32_t length, uint32_t numberofparticles, fractionOfParticlesUsed = 255; // use all particles by default advPartProps = NULL; //make sure we start out with null pointers (just in case memory was not cleared) //advPartSize = NULL; - updatePSpointers(isadvanced); // set the particle and sources pointer (call this before accessing sprays or particles) setSize(length); + updatePSpointers(isadvanced); // set the particle and sources pointer (call this before accessing sprays or particles) setWallHardness(255); // set default wall hardness to max setGravity(0); //gravity disabled by default setParticleSize(0); // minimum size by default @@ -1541,7 +1472,7 @@ void ParticleSystem1D::ParticleSys_render() { if(bufferNeedsUpdate) { if (globalBlur > 0 || globalSmear > 0) { // blurring active: if not a transition or is newFX, read data from segment before blurring (old FX can render to it afterwards) - Serial.println(" blurring: " + String(globalBlur)); + // Serial.println(" blurring: " + String(globalBlur)); for (uint32_t x = 0; x <= maxXpixel; x++) { if (!renderSolo) // sharing the framebuffer with another segment: read buffer back from segment framebuffer[x] = SEGMENT.getPixelColor(x); // copy to local buffer @@ -1550,7 +1481,7 @@ void ParticleSystem1D::ParticleSys_render() { } else { // no blurring: clear buffer memset(framebuffer, 0, frameBufferSize * sizeof(CRGB)); - Serial.print(" clearing "); + //Serial.print(" clearing "); } } } @@ -1784,6 +1715,7 @@ void ParticleSystem1D::collideParticles(PSparticle1D *particle1, PSparticle1D *p // note: do not access the PS class in FX befor running this function (or it messes up SEGENV.data) void ParticleSystem1D::updateSystem(void) { setSize(SEGMENT.vLength()); // update size + updateRenderingBuffer(SEGMENT.vLength(), true); // update or create rendering buffer (segment size can change at any time) updatePSpointers(advPartProps != NULL); setUsedParticles(fractionOfParticlesUsed); // update used particles based on percentage (can change during transitions, execute each frame for code simplicity) if (partMemList.size() == 1) // if number of vector elements is one, this is the only system @@ -1859,11 +1791,8 @@ bool allocateParticleSystemMemory1D(uint32_t numparticles, uint32_t numsources, // note: percentofparticles is in uint8_t, for example 191 means 75%, (deafaults to 255 or 100% meaning one particle per pixel), can be more than 100% (but not recommended, can cause out of memory) bool initParticleSystem1D(ParticleSystem1D *&PartSys, uint32_t requestedsources, uint8_t fractionofparticles, uint32_t additionalbytes, bool advanced) { if (SEGLEN == 1) return false; // single pixel not supported - // allocat rendering buffers - updateRenderingBuffer(framebuffer, SEGMENT.virtualLength(), true); if(advanced) - updateRenderingBuffer(renderbuffer, 10, false); - + updateRenderingBuffer(10, false); // buffer for advanced particles, fixed size uint32_t numparticles = calculateNumberOfParticles1D(fractionofparticles, advanced); uint32_t numsources = calculateNumberOfSources1D(requestedsources); if (!allocateParticleSystemMemory1D(numparticles, numsources, advanced, additionalbytes)) { @@ -1982,17 +1911,10 @@ static void fast_color_scale(CRGB &c, const uint32_t scale) { c.b = ((c.b * scale) >> 8); } -// allocate memory for the 1D CRGB array in one contiguous block and set values to zero -/* -static CRGB* allocateCRGBbuffer(uint32_t length) { - CRGB *array = (CRGB *)calloc(length, sizeof(CRGB)); - return array; -} -*/ - -////////////////////////////////////////////////// -// memory and transition management for particle system +////////////////////////////////////////////////////////// +// memory and transition management for particle system // +////////////////////////////////////////////////////////// // note: these functions can only be called while strip is servicing // allocate memory using the FX data limit, if overridelimit is set, temporarily ignore the limit @@ -2029,33 +1951,18 @@ void deallocatePSmemory(void* dataptr, uint32_t size) { Segment::addUsedSegmentData(size <= Segment::getUsedSegmentData() ? -size : -Segment::getUsedSegmentData()); } -/* -// TODO: -- usedparticles is now relative, update it in updatePS function (need a new variable as well) -- irgendwo muss man noch detektieren, ob eine transition gerade startet. wie macht man das am besten? -im getparticlepointer weiss man ja, dass ein system schon existiert, muss aber nicht, denn der watchdog kann ja noch aktiv sein und den memory halten... -beim ersten call von getpointer von diesem segment ist es der neue effekt, der das beantragt. man weiss also, welcher der beiden es ist und man weiss ob es in transition ist -ist also in transition und es ist der neue effekt und der watchdog ist auf null (oder eins, je nachdem wann man den kickt) dann sind zwei systeme aktiv. - if(SEGMENT.currentMode() != SEGMENT.mode) { // if true, this is the new particle effect -*/ - - -// handle particle pointer, creates/extends buffer if needed and handles transition handover -// function is called in PS setup and updatepointer function +// Particle transition manager, creates/extends buffer if needed and handles transition memory-handover void* particleMemoryManager(const uint32_t requestedParticles, size_t structSize, uint32_t &availableToPS, uint8_t fractionUsed, const uint8_t effectID) { pmem = getPartMem(); - PSPRINT(" getParticlePointer "); void* buffer = nullptr; if (pmem) { // segment has a buffer - PSPRINT(" buffer found "); if (requestedParticles) { // request for a new buffer, this is an init call - PSPRINT(" requested particles: " + String(requestedParticles)); pmem->transferParticles = true; // set flag to transfer particles + availableToPS = 0; // start out with zero particles, transition below will initialize and transfer them uint32_t requestsize = structSize * requestedParticles; // required buffer size if (requestsize > pmem->buffersize) { // request is larger than buffer, try to extend it if (Segment::getUsedSegmentData() + requestsize - pmem->buffersize <= MAX_SEGMENT_DATA) { // enough memory available to extend buffer - PSPRINT(" extending particle buffer "); buffer = allocatePSmemory(requestsize, true); // calloc new memory in FX data, override limit (temporary buffer) if (buffer) { // allocaction successful, copy old particles to new buffer memcpy(buffer, pmem->particleMemPointer, pmem->buffersize); // copy old particle buffer note: only required if transition but copy is fast and rarely happens @@ -2063,33 +1970,21 @@ void* particleMemoryManager(const uint32_t requestedParticles, size_t structSize pmem->particleMemPointer = buffer; // set new buffer pmem->buffersize = requestsize; // update buffer size } // if buffer was not extended, the old, smaller buffer is used - else + else return nullptr; // no memory available - } + } } - if (pmem->watchdog == 1) { // if a PS already exists during particle request, it kicked the watchdog in last frame, servicePSmem() adds 1 afterwards -> PS to PS transition - PSPRINTLN("********** PS is in transition, new FX:" + String(effectID)); - PSPRINTLN(" inTransition = " + String(pmem->inTransition)); + if (pmem->watchdog == 1) // if a PS already exists during particle request, it kicked the watchdog in last frame, servicePSmem() adds 1 afterwards -> PS to PS transition pmem->inTransition = effectID; // save the ID of the new effect (required to determine blur amount in rendering function) - } - else { // no watchdog, this is a new PS - // availableToPS = requestedParticles; // all particles are available todo: THIS IS TBD, probably ok to feed particles slowly - // pmem->inTransition = false; // no transition TODO: does this need to be set here? this function is called again in updatePS, can deactivate it then, right? - } - availableToPS = 0; // start out with zero particles, transition below will initialize and transfer them (this line is not really needed, could be removed?) return pmem->particleMemPointer; // return the available buffer on init call TODO: maybe split this into two functions, one for init and one for get? } - PSPRINT(" use existing, "); pmem->watchdog = 0; // kick watchdog buffer = pmem->particleMemPointer; // buffer is already allocated } else { // if the id was not found create a buffer and add an element to the list - PSPRINT(" allocating particle buffer "); + availableToPS = 0; // new PS starts with zero particles, they are transferred in the next call uint32_t requestsize = structSize * requestedParticles; // required buffer size buffer = allocatePSmemory(requestsize, false); // allocate new memory - if (buffer) PSPRINTLN(" bfr allocated"); - else PSPRINTLN(" brf alloc failed"); - if (buffer) partMemList.push_back({buffer, requestsize, 0, strip.getCurrSegmentId(), 0, 0, true}); // add buffer to list, set flag to transfer/init the particles note: if pushback fails, it may crash else @@ -2099,64 +1994,39 @@ void* particleMemoryManager(const uint32_t requestedParticles, size_t structSize free(buffer); return nullptr; } - availableToPS = 0; // new PS starts with zero particles, they are transferred in the next call (this line is not really needed, could be removed?) return buffer; // directly return the buffer on init call } - #ifdef WLED_DEBUG_PS - PSPRINT("particle buffer address: 0x"); - Serial.println((uintptr_t)buffer, HEX); - #endif // now we have a valid buffer, if this is a PS to PS FX transition: transfer particles slowly to new FX bool effectchanged = (SEGMENT.currentMode() != SEGMENT.mode); // FX changed, transition the particle buffer if (effectchanged && pmem->inTransition) { - PSPRINT(" FX changed "); - PSPRINT(" this mode: " + String(effectID)); - PSPRINT("/ oldmode: " + String(SEGMENT.currentMode())); - PSPRINTLN(" newmode: " + String(SEGMENT.mode)); uint32_t maxParticles = pmem->buffersize / structSize; // maximum number of particles that fit in the buffer - uint32_t progress = SEGMENT.progress(); // transition progress + uint16_t progress = SEGMENT.progress(); // transition progress uint32_t newAvailable = 0; if (SEGMENT.mode == effectID) { // new effect ID -> function was called from new FX - PSPRINT(" new FX"); - PSPRINT(" progress: " + String(progress)); - newAvailable = (maxParticles * progress) >> 16; // update total particles available to this PS - uint32_t brforigin = (uintptr_t)buffer; // save old buffer pointer for !!!!DEBUG - //!!! buffer = (void*)((uint8_t*)buffer + ((maxParticles - newAvailable) * structSize)); // new effect gets the end of the buffer - uint32_t newbrfaddr = (uintptr_t)buffer; - #ifdef WLED_DEBUG_PS - PSPRINT(" new buffer startaddress: 0x"); - Serial.println((uintptr_t)buffer, HEX); - PSPRINT("new bfrstart in particles "); - Serial.println((newbrfaddr-brforigin)/structSize); - PSPRINT(" particle start: " + String(maxParticles - newAvailable)); - #endif + newAvailable = (maxParticles * progress) >> 16; // update total particles available to this PS (newAvailable is guaranteed to be smaller than maxParticles) + uint32_t bufferoffset = (maxParticles - 1) - newAvailable; // offset to new effect particles + if(bufferoffset < maxParticles) // safety check + buffer = (void*)((uint8_t*)buffer + bufferoffset * structSize); // new effect gets the end of the buffer int32_t totransfer = newAvailable - availableToPS; // number of particles to transfer in this transition update - PSPRINT(" totransfer: " + String(totransfer)); - //particleHandover(buffer, structSize, totransfer); + if(totransfer < 0) totransfer = 0; // safety check + particleHandover(buffer, structSize, totransfer); } else { // this was called from the old FX - PSPRINT(" old FX"); - PSPRINT(" progress: " + String(progress)); SEGMENT.setCurrentPalette(true); // load the old palette into segment progress = 0xFFFFU - progress; // inverted transition progress - PSPRINT(" inv.prog: " + String(progress)); - newAvailable = ((maxParticles * progress) >> 16); + newAvailable = ((maxParticles * progress) >> 16); // result is guaranteed to be smaller than maxParticles if(newAvailable > 0) newAvailable--; // -1 to avoid overlapping memory in 1D<->2D transitions - PSPRINT(" newAvailable: " + String(newAvailable)); - PSPRINT(" oldAvailable: " + String(availableToPS)); - if(newAvailable > availableToPS) newAvailable = availableToPS; // do not increase available particles (if memory was extended) - //PSPRINTLN("trans: available particles: " + String(availableToPS)); - // note: startindex is start of buffer (transferred from end to start, so end particles are the old ones) + // note: buffer pointer stays the same, number of available particles is reduced } availableToPS = newAvailable; - PSPRINT(" final available to PS: " + String(availableToPS)); } else { // no PS transition, full buffer available if(pmem->transferParticles) { // transition ended (or blending is disabled) -> transfer all remaining particles uint32_t maxParticles = pmem->buffersize / structSize; // maximum number of particles that fit in the buffer if (maxParticles > availableToPS) { // not all particles transferred yet int32_t totransfer = maxParticles - availableToPS; // transfer all remaining particles - //particleHandover(buffer, structSize, totransfer); + if(totransfer < 0) totransfer = 0; // safety check + particleHandover(buffer, structSize, totransfer); } availableToPS = maxParticles; // update available particles // kill unused particles to they do not re-appear when transitioning to next FX @@ -2181,17 +2051,13 @@ void* particleMemoryManager(const uint32_t requestedParticles, size_t structSize pmem->particleType = structSize; // update particle type pmem->transferParticles = false; } - PSPRINTLN(" no trans, nr of partcls: " + String(availableToPS)); pmem->inTransition = false; } - PSPRINTLN(" END getPartPointer "); - return buffer; } // (re)initialize particles in the particle buffer for use in the new FX void particleHandover(void *buffer, size_t structSize, int32_t numToTransfer) { - // TODO: need to be more clever: need to clear buffer if 2D->1D or 1D->2D transition or settings are nonsensical. #ifndef WLED_DISABLE_PARTICLESYSTEM2D if (structSize == sizeof(PSparticle)) { // 2D particle PSparticle *particles = (PSparticle *)buffer; @@ -2229,33 +2095,19 @@ void particleHandover(void *buffer, size_t structSize, int32_t numToTransfer) { } // function to update the framebuffer and renderbuffer -void updateRenderingBuffer(CRGB* buffer, uint32_t requiredpixels, bool isFramebuffer) { -/* +void updateRenderingBuffer(uint32_t requiredpixels, bool isFramebuffer) { PSPRINTLN("updateRenderingBuffer"); - uint32_t currentBufferSize = isFramebuffer ? frameBufferSize : renderBufferSize; - if(currentBufferSize < requiredpixels) { // check current buffer size - if(buffer) deallocatePSmemory((void*)buffer, currentBufferSize * sizeof(CRGB)); - buffer = reinterpret_cast(allocatePSmemory(requiredpixels * sizeof(CRGB), false)); - if(buffer) { - if(isFramebuffer) { - framebuffer = buffer; - frameBufferSize = requiredpixels; - } else { - renderbuffer = buffer; - renderBufferSize = requiredpixels; - } - return; - } else { - Serial.println("!!! Memory allocation failed for rendering buffer !!!"); - if(isFramebuffer) { - framebuffer = nullptr; - frameBufferSize = 0; - } else { - renderbuffer = nullptr; - renderBufferSize = 0; - } - } - }*/ + CRGB** targetBuffer = isFramebuffer ? &framebuffer : &renderbuffer; // pointer to target buffer + uint16_t& targetBufferSize = isFramebuffer ? frameBufferSize : renderBufferSize; // corresponding buffer size + if(targetBufferSize < requiredpixels) { // check current buffer size + if(*targetBuffer) // buffer exists, free it + deallocatePSmemory((void*)(*targetBuffer), targetBufferSize * sizeof(CRGB)); + *targetBuffer = reinterpret_cast(allocatePSmemory(requiredpixels * sizeof(CRGB), false)); + if(*targetBuffer) + targetBufferSize = requiredpixels; + else + targetBufferSize = 0; + } } // get the pointer to the particle memory for the segment @@ -2286,7 +2138,6 @@ void servicePSmem() { PSPRINT(" watchdog: "); PSPRINTLN(partMemList[i].watchdog); if (partMemList[i].watchdog > MAX_MEMIDLE) { - PSPRINTLN("psmem free"); //deallocating memory deallocatePSmemory(partMemList[i].particleMemPointer, partMemList[i].buffersize); // Free memory partMemList.erase(partMemList.begin() + i); // Remove entry //partMemList.shrink_to_fit(); // partMemList is small, memory operations should be unproblematic (this may lead to mem fragmentation, removed for now) @@ -2294,8 +2145,6 @@ void servicePSmem() { } } else { // no particle system running, release buffer memory - /* - Serial.println("ps buffer free"); //deallocating memory if(framebuffer) { deallocatePSmemory((void*)framebuffer, frameBufferSize * sizeof(CRGB)); // free the buffers framebuffer = nullptr; @@ -2305,37 +2154,6 @@ void servicePSmem() { deallocatePSmemory((void*)renderbuffer, renderBufferSize * sizeof(CRGB)); renderbuffer = nullptr; renderBufferSize = 0; - }*/ - } - -} - - -//!!! TODO: either use this function or remove it -// apply globalBlur to framebuffer, clear buffer if not blurring (call this after transferring the buffer to the segment) -static void applyBlurOrClear2D(uint32_t width, uint32_t height, uint8_t fxID) { - if (framebuffer) { - if (globalBlur > 0) { // using SEGMENT.fadeToBlackBy is much slower, this approximately doubles the speed of fade calculation - PSPRINTLN(" blurring: " + String(globalBlur)); - uint32_t yflipped; - for (uint32_t y = 0; y < height; y++) { - int index = y * width; // current row index for 1D buffer - if(renderSolo < 2 && pmem->inTransition != fxID) { // there are/were other systems using the buffer and this is not the old FX in a transition: read data from segment - yflipped = height - y - 1; - for (uint32_t x = 0; x < width; x++) { - framebuffer[index++] = SEGMENT.getPixelColorXY(x, yflipped); //copy to local buffer - } - index = y * width; // reset index - } - for (uint32_t x = 0; x < width; x++) { - fast_color_scale(framebuffer[index], globalBlur); - index++; - } - } - globalBlur = 0; // reset for next frame (updated by PS) - } - else { // no blurring, clear the buffer - memset(framebuffer, 0, frameBufferSize * sizeof(CRGB)); } } } @@ -2354,7 +2172,6 @@ void transferBuffer(uint32_t width, uint32_t height, bool useAdditiveTransfer) { if(height) { // is 2D, 1D passes height = 0 int32_t yflipped; height--; // height is number of pixels, convert to array index - PSPRINT("xfer 2D"); for (uint32_t y = 0; y <= height; y++) { yflipped = height - y; int index = y * width; // current row index for 1D buffer @@ -2368,8 +2185,8 @@ void transferBuffer(uint32_t width, uint32_t height, bool useAdditiveTransfer) { *c = segmentRGB; else { // not black if(segmentcolor) { - fast_color_add(*c, segmentRGB); // add segment color back to buffer if not black TODO: since both are 32bit, this could be made faster using the new improved wled 32bit adding (see speed improvements PR) - clr = RGBW32(c->r,c->g,c->b,0); // convert to 32bit color (again) + fast_color_add(*c, segmentRGB); // add segment color back to buffer if not black + clr = RGBW32(c->r,c->g,c->b,0); // convert to 32bit color (again) TODO: could convert first, then use 32bit adding function color_add() from colors.cpp } SEGMENT.setPixelColorXY((int)x, (int)yflipped, clr); // save back to segment after adding local buffer } @@ -2390,24 +2207,6 @@ void transferBuffer(uint32_t width, uint32_t height, bool useAdditiveTransfer) { #ifndef WLED_DISABLE_MODE_BLEND SEGMENT.modeBlend(tempBlend); // restore blending mode #endif -/* - #ifdef WLED_DEBUG_PS - PSPRINT(" done. framebfr addr: 0x"); - Serial.println((uintptr_t)framebuffer, HEX); - #endif -*/ } - - - /* - //memory fragmentation check: - PSPRINT("heap: "); - PSPRINT(heap_caps_get_free_size(MALLOC_CAP_8BIT)); - PSPRINT(" block: "); - PSPRINTLN(heap_caps_get_largest_free_block(MALLOC_CAP_8BIT)); - */ - - - #endif // !defined(WLED_DISABLE_PARTICLESYSTEM2D) || !defined(WLED_DISABLE_PARTICLESYSTEM1D) diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index 1f1e2df3d5..553ea64aa8 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -39,14 +39,9 @@ struct partMem { void* particleMemoryManager(const uint32_t requestedParticles, size_t structSize, uint32_t &availableToPS, uint8_t fractionUsed, const uint8_t effectID); // update particle memory pointer, handles memory transitions void particleHandover(void *buffer, size_t structSize, int32_t numParticles); -//extern CRGB *framebuffer; // local frame buffer for rendering -//extern CRGB *renderbuffer; // local particle render buffer for advanced particles -//extern uint16_t frameBufferSize; // size in pixels, used to check if framebuffer is large enough for current segment TODO: make this in bytes, not in pixels -//extern uint16_t renderBufferSize; // size in pixels, if allcoated by a 1D system it needs to be updated for 2D partMem* getPartMem(void); // returns pointer to memory struct for current segment or nullptr -void updateRenderingBuffer(CRGB* buffer, uint32_t requiredsize, bool isFramebuffer); // allocate CRGB rendering buffer, update size if needed +void updateRenderingBuffer(uint32_t requiredsize, bool isFramebuffer); // allocate CRGB rendering buffer, update size if needed void transferBuffer(uint32_t width, uint32_t height, bool useAdditiveTransfer = false); // transfer the buffer to the segment (supports 1D and 2D) -//TODO: add 1D version? or remove 2D version? void servicePSmem(); // increments watchdog, frees memory if idle too long // update number of particles to use, must never be more than allocated (= particles allocated by the calling system) From 696b4c140b920c6f4d58866ef0e7bf4192cc67a8 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sun, 29 Dec 2024 12:12:07 +0100 Subject: [PATCH 150/219] minor cleanup and fixed a bug 1D system had a lot of crashes, some out of bounds memory issue. fixed it but I have no idea which change did that, maybe the order in 1D initialization --- wled00/FX.cpp | 10 ++-- wled00/FXparticleSystem.cpp | 106 +++++++++++++++++++++++------------- wled00/FXparticleSystem.h | 17 ++++-- 3 files changed, 84 insertions(+), 49 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index c26e043814..80fa8bafe7 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -9708,12 +9708,10 @@ uint16_t mode_particle1Dspray(void) { PartSys->sources[0].maxLife = 400; PartSys->sources[0].source.x = map(SEGMENT.custom1, 0 , 255, 0, PartSys->maxX); // spray position PartSys->sources[0].v = map(SEGMENT.speed, 0 , 255, -127 + PartSys->sources[0].var, 127 - PartSys->sources[0].var); // particle emit speed - PartSys->sources[0].source.reversegrav = false; - if(gravity < 0) - PartSys->sources[0].source.reversegrav = true; + PartSys->sources[0].source.reversegrav = gravity < 0 ? true : false; if(hw_random() % (1 + ((255 - SEGMENT.intensity) >> 3)) == 0) - PartSys->sprayEmit(PartSys->sources[0]); //emit a particle + PartSys->sprayEmit(PartSys->sources[0]); //emit a particle //update color settings PartSys->setColorByAge(SEGMENT.check1); //overruled by 'color by position' @@ -9722,6 +9720,7 @@ uint16_t mode_particle1Dspray(void) { PartSys->particles[i].reversegrav = PartSys->sources[0].source.reversegrav; //update gravity direction } PartSys->update(); // update and render + Serial.println("used by FX: " + String(PartSys->usedParticles)); return FRAMETIME; } @@ -9741,7 +9740,7 @@ uint16_t mode_particleBalance(void) { return mode_static(); // allocation failed or is single pixel //PartSys->setKillOutOfBounds(true); PartSys->setParticleSize(1); - SEGENV.aux0 = 0; // to track particle initialization + SEGENV.aux0 = 0; // } else PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS @@ -9764,6 +9763,7 @@ uint16_t mode_particleBalance(void) { PartSys->particles[i].x = i * PS_P_RADIUS_1D; PartSys->particles[i].hue = (i * 1024) / PartSys->usedParticles; // multi gradient distribution PartSys->particles[i].ttl = 300; + PartSys->particles[i].perpetual = true; // TODO: is this a good idea? need to check how to handle it in transitions PartSys->particles[i].collide = true; } } diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index e8bfb0beea..79b445e165 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -55,6 +55,7 @@ ParticleSystem2D::ParticleSystem2D(uint32_t width, uint32_t height, uint32_t num setGravity(0); //gravity disabled by default setParticleSize(0); // minimum size by default motionBlur = 0; //no fading by default + smearBlur = 0; //no smearing by default emitIndex = 0; //initialize some default non-zero values most FX use @@ -410,7 +411,7 @@ void ParticleSystem2D::getParticleXYsize(PSadvancedParticle *advprops, PSsizeCon void ParticleSystem2D::bounce(int8_t &incomingspeed, int8_t ¶llelspeed, int32_t &position, uint16_t maxposition) { incomingspeed = -incomingspeed; incomingspeed = (incomingspeed * wallHardness) / 255; // reduce speed as energy is lost on non-hard surface - if (position < particleHardRadius) + if (position < (int32_t)particleHardRadius) position = particleHardRadius; // fast particles will never reach the edge if position is inverted, this looks better else position = maxposition - particleHardRadius; @@ -577,7 +578,7 @@ void ParticleSystem2D::ParticleSys_render(bool firemode, uint32_t fireintensity) smearamount = globalSmear + (((smearamount - globalSmear) * (int)SEGMENT.progress()) >> 16); } globalBlur = motionbluramount; - globalSmear = smearamount; + globalSmear = smearamount; // handle blurring and framebuffer update if (framebuffer) { @@ -587,9 +588,9 @@ void ParticleSystem2D::ParticleSys_render(bool firemode, uint32_t fireintensity) if(bufferNeedsUpdate) { if (globalBlur > 0 || globalSmear > 0) { // blurring active: if not a transition or is newFX, read data from segment before blurring (old FX can render to it afterwards) - for (uint32_t y = 0; y <= maxYpixel; y++) { + for (int32_t y = 0; y <= maxYpixel; y++) { int index = y * (maxXpixel + 1); - for (uint32_t x = 0; x <= maxXpixel; x++) { + for (int32_t x = 0; x <= maxXpixel; x++) { if (!renderSolo) { // sharing the framebuffer with another segment: read buffer back from segment uint32_t yflipped = maxYpixel - y; framebuffer[index] = SEGMENT.getPixelColorXY(x, yflipped); // read from segment @@ -613,10 +614,10 @@ void ParticleSystem2D::ParticleSys_render(bool firemode, uint32_t fireintensity) SEGMENT.modeBlend(false); // temporarily disable FX blending in PS to PS transition (local buffer is used to do PS blending) #endif int yflipped; - for (uint32_t y = 0; y <= maxYpixel; y++) { + for (int32_t y = 0; y <= maxYpixel; y++) { yflipped = maxYpixel - y; int index = y * (maxXpixel + 1); // current row index for 1D buffer - for (uint32_t x = 0; x <= maxXpixel; x++) { + for (int32_t x = 0; x <= maxXpixel; x++) { CRGB *c = &framebuffer[index++]; uint32_t clr = RGBW32(c->r,c->g,c->b,0); // convert to 32bit color //if(clr > 0) // not black TODO: not transferring black is faster and enables overlay, but requries proper handling of buffer clearing, which is quite complex and probably needs a change to SEGMENT handling. @@ -678,12 +679,12 @@ void ParticleSystem2D::ParticleSys_render(bool firemode, uint32_t fireintensity) bluramount -= 64; } } - // apply 2D blur to rendered frame TODO: this needs proper transition handling, maybe combine it with motion blur handling? + // apply 2D blur to rendered frame if(globalSmear > 0) { if (framebuffer) - blur2D(framebuffer, maxXpixel + 1, maxYpixel + 1, smearBlur, smearBlur); + blur2D(framebuffer, maxXpixel + 1, maxYpixel + 1, globalSmear, globalSmear); else - SEGMENT.blur(smearBlur, true); + SEGMENT.blur(globalSmear, true); } // transfer framebuffer to segment if available if (framebuffer && pmem->inTransition != effectID) { // not in transition or is old FX @@ -770,7 +771,7 @@ void ParticleSystem2D::renderParticle(const uint32_t particleindex, const uint32 // transfer particle renderbuffer to framebuffer for (uint32_t xrb = offset; xrb < rendersize + offset; xrb++) { xfb = xfb_orig + xrb; - if (xfb > maxXpixel) { + if (xfb > (uint32_t)maxXpixel) { if (wrapX) // wrap x to the other side if required xfb = xfb % (maxXpixel + 1); // TODO: this did not work in 1D system but appears to work in 2D (wrapped pixels were offset) under which conditions does this not work? else @@ -779,7 +780,7 @@ void ParticleSystem2D::renderParticle(const uint32_t particleindex, const uint32 for (uint32_t yrb = offset; yrb < rendersize + offset; yrb++) { yfb = yfb_orig + yrb; - if (yfb > maxYpixel) { + if (yfb > (uint32_t)maxYpixel) { if (wrapY) // wrap y to the other side if required yfb = yfb % (maxYpixel + 1); else @@ -1171,20 +1172,21 @@ bool initParticleSystem2D(ParticleSystem2D *&PartSys, uint32_t requestedsources, #ifndef WLED_DISABLE_PARTICLESYSTEM1D ParticleSystem1D::ParticleSystem1D(uint32_t length, uint32_t numberofparticles, uint32_t numberofsources, bool isadvanced) { + effectID = SEGMENT.mode; numSources = numberofsources; numParticles = numberofparticles; // number of particles allocated in init availableParticles = 0; // let the memory manager assign fractionOfParticlesUsed = 255; // use all particles by default advPartProps = NULL; //make sure we start out with null pointers (just in case memory was not cleared) //advPartSize = NULL; - setSize(length); updatePSpointers(isadvanced); // set the particle and sources pointer (call this before accessing sprays or particles) + setSize(length); setWallHardness(255); // set default wall hardness to max setGravity(0); //gravity disabled by default setParticleSize(0); // minimum size by default motionBlur = 0; //no fading by default + smearBlur = 0; //no smearing by default emitIndex = 0; - effectID = SEGMENT.mode; // initialize some default non-zero values most FX use for (uint32_t i = 0; i < numSources; i++) { @@ -1221,7 +1223,7 @@ void ParticleSystem1D::update(void) { uint32_t bg_color = SEGCOLOR(1); //background color, set to black to overlay if (bg_color > 0) { //if not black - for(int32_t i = 0; i < maxXpixel + 1; i++) { + for(int32_t i = 0; i <= maxXpixel; i++) { SEGMENT.addPixelColor(i, bg_color, true); // TODO: can this be done in rendering function using local buffer? } } @@ -1229,14 +1231,16 @@ void ParticleSystem1D::update(void) { // set percentage of used particles as uint8_t i.e 127 means 50% for example void ParticleSystem1D::setUsedParticles(uint8_t percentage) { - fractionOfParticlesUsed = percentage; + fractionOfParticlesUsed = percentage; // note usedParticles is updated in memory manager + updateUsedParticles(numParticles, availableParticles, fractionOfParticlesUsed, usedParticles); + PSPRINT(" SetUsedpaticles: allocated particles: "); + PSPRINT(numParticles); PSPRINT(" available particles: "); PSPRINT(availableParticles); PSPRINT(" ,used percentage: "); PSPRINT(fractionOfParticlesUsed); PSPRINT(" ,used particles: "); PSPRINTLN(usedParticles); - updateUsedParticles(numParticles, availableParticles, fractionOfParticlesUsed, usedParticles); } void ParticleSystem1D::setWallHardness(uint8_t hardness) { @@ -1269,11 +1273,13 @@ void ParticleSystem1D::setColorByPosition(bool enable) { } void ParticleSystem1D::setMotionBlur(uint8_t bluramount) { - //TODO: currently normal blurring is not used in 1D system. should it be added? advanced rendering is quite fast and allows for motion blurring - // if (particlesize < 2) // only allow motion blurring on default particle size or advanced size (cannot combine motion blur with normal blurring used for particlesize, would require another buffer) motionBlur = bluramount; } +void ParticleSystem1D::setSmearBlur(uint8_t bluramount) { + smearBlur = bluramount; +} + // render size, 0 = 1 pixel, 1 = 2 pixel (interpolated), bigger sizes require adanced properties void ParticleSystem1D::setParticleSize(uint8_t size) { particlesize = size > 0 ? 1 : 0; // TODO: add support for global sizes? see not abover (motion blur) @@ -1353,9 +1359,9 @@ void ParticleSystem1D::particleMoveUpdate(PSparticle1D &part, PSsettings1D *opti bool bouncethis = true; if (options->useGravity) { if (part.reversegrav) { // skip bouncing at x = 0 - if (newX < particleHardRadius) + if (newX < (int32_t)particleHardRadius) bouncethis = false; - } else if (newX > particleHardRadius) { // skip bouncing at x = max + } else if (newX > (int32_t)particleHardRadius) { // skip bouncing at x = max bouncethis = false; } } @@ -1370,7 +1376,7 @@ void ParticleSystem1D::particleMoveUpdate(PSparticle1D &part, PSsettings1D *opti } } - if (!checkBoundsAndWrap(newX, maxX, renderradius, options->wrap)) { // check out of bounds note: this must not be skipped or it can lead to crashes TODO: is this still true? + if (!checkBoundsAndWrap(newX, maxX, renderradius, options->wrap)) { // check out of bounds note: this must not be skipped or it can lead to crashes part.outofbounds = true; if (options->killoutofbounds) { bool killthis = true; @@ -1453,17 +1459,17 @@ void ParticleSystem1D::ParticleSys_render() { CRGB baseRGB; uint32_t brightness; // particle brightness, fades if dying - if (framebuffer) { - // update global blur (used for blur transitions) - int32_t motionbluramount = motionBlur; - //int32_t smearamount = smearBlur; - if(pmem->inTransition == effectID) { // FX transition and this is the new FX: fade blur amount - motionbluramount = globalBlur + (((motionbluramount - globalBlur) * (int)SEGMENT.progress()) >> 16); // fade from old blur to new blur during transitions - //smearamount = globalSmear + (((smearamount - globalSmear) * (int)SEGMENT.progress()) >> 16); - } - globalBlur = motionbluramount; - //globalSmear = smearamount; + // update global blur (used for blur transitions) + int32_t motionbluramount = motionBlur; + int32_t smearamount = smearBlur; + if(pmem->inTransition == effectID) { // FX transition and this is the new FX: fade blur amount + motionbluramount = globalBlur + (((motionbluramount - globalBlur) * (int)SEGMENT.progress()) >> 16); // fade from old blur to new blur during transitions + smearamount = globalSmear + (((smearamount - globalSmear) * (int)SEGMENT.progress()) >> 16); + } + globalBlur = motionbluramount; + globalSmear = smearamount; + if (framebuffer) { // handle buffer blurring or clearing bool bufferNeedsUpdate = (!pmem->inTransition || pmem->inTransition == effectID); // not a transition; or new FX: update buffer (blur, or clear) @@ -1473,7 +1479,7 @@ void ParticleSystem1D::ParticleSys_render() { if(bufferNeedsUpdate) { if (globalBlur > 0 || globalSmear > 0) { // blurring active: if not a transition or is newFX, read data from segment before blurring (old FX can render to it afterwards) // Serial.println(" blurring: " + String(globalBlur)); - for (uint32_t x = 0; x <= maxXpixel; x++) { + for (int32_t x = 0; x <= maxXpixel; x++) { if (!renderSolo) // sharing the framebuffer with another segment: read buffer back from segment framebuffer[x] = SEGMENT.getPixelColor(x); // copy to local buffer fast_color_scale(framebuffer[x], motionBlur); @@ -1510,6 +1516,13 @@ void ParticleSystem1D::ParticleSys_render() { } renderParticle(i, brightness, baseRGB, wrap); } + // apply smear-blur to rendered frame + if(globalSmear > 0) { + if (framebuffer) + blur1D(framebuffer, maxXpixel + 1, globalSmear); + else + SEGMENT.blur(globalSmear, true); + } // transfer local buffer back to segment (if available) transferBuffer(maxXpixel + 1, 0); @@ -1523,7 +1536,7 @@ void ParticleSystem1D::renderParticle(const uint32_t particleindex, const uint32 } if (size == 0) { //single pixel particle, can be out of bounds as oob checking is made for 2-pixel particles (and updating it uses more code) uint32_t x = particles[particleindex].x >> PS_P_RADIUS_SHIFT_1D; - if (x <= maxXpixel) { //by making x unsigned there is no need to check < 0 as it will overflow + if (x <= (uint32_t)maxXpixel) { //by making x unsigned there is no need to check < 0 as it will overflow if (framebuffer) fast_color_add(framebuffer[x], color, brightness); else @@ -1596,9 +1609,9 @@ void ParticleSystem1D::renderParticle(const uint32_t particleindex, const uint32 // transfer particle renderbuffer to framebuffer for (uint32_t xrb = offset; xrb < rendersize+offset; xrb++) { xfb = xfb_orig + xrb; - if (xfb > maxXpixel) { + if (xfb > (uint32_t)maxXpixel) { if (wrap) { // wrap x to the other side if required - if (xfb > maxXpixel << 1) // xfb is "negative" (note: for some reason, this check is needed in 1D but works without in 2D...) + if (xfb > (uint32_t)maxXpixel << 1) // xfb is "negative" (note: for some reason, this check is needed in 1D but works without in 2D...) xfb = (maxXpixel +1) + (int32_t)xfb; //TODO: remove this again and see if it works now (changed maxxpixel to unsigned) else xfb = xfb % (maxXpixel + 1); //TODO: can modulo be avoided? @@ -1739,6 +1752,15 @@ void ParticleSystem1D::updatePSpointers(bool isadvanced) { advPartProps = reinterpret_cast(sources + numSources); PSdataEnd = reinterpret_cast(advPartProps + numParticles); } + #ifdef WLED_DEBUG_PS + PSPRINTLN(" PS Pointers: "); + PSPRINT(" PS : 0x"); + Serial.println((uintptr_t)this, HEX); + PSPRINT(" Sources : 0x"); + Serial.println((uintptr_t)sources, HEX); + PSPRINT(" Particles : 0x"); + Serial.println((uintptr_t)particles, HEX); + #endif } //non class functions to use for initialization, fraction is uint8_t: 255 means 100% @@ -1858,9 +1880,9 @@ static int32_t limitSpeed(int32_t speed) { //return speed > PS_P_MAXSPEED ? PS_P_MAXSPEED : (speed < -PS_P_MAXSPEED ? -PS_P_MAXSPEED : speed); // note: this uses more code, not sure due to speed or inlining } -// check if particle is out of bounds and wrap it around if required +// check if particle is out of bounds and wrap it around if required, returns true if out of bounds static bool checkBoundsAndWrap(int32_t &position, const int32_t max, const int32_t particleradius, bool wrap) { - if ((uint32_t)position > max) { // check if particle reached an edge + if (position > max) { // check if particle reached an edge if (wrap) { position = position % (max + 1); // note: cannot optimize modulo, particles can be far out of bounds when wrap is enabled if (position < 0) @@ -1955,9 +1977,10 @@ void deallocatePSmemory(void* dataptr, uint32_t size) { void* particleMemoryManager(const uint32_t requestedParticles, size_t structSize, uint32_t &availableToPS, uint8_t fractionUsed, const uint8_t effectID) { pmem = getPartMem(); void* buffer = nullptr; - + PSPRINTLN("PS MemManager"); if (pmem) { // segment has a buffer if (requestedParticles) { // request for a new buffer, this is an init call + PSPRINTLN("Buffer exists, request for particles: " + String(requestedParticles)); pmem->transferParticles = true; // set flag to transfer particles availableToPS = 0; // start out with zero particles, transition below will initialize and transfer them uint32_t requestsize = structSize * requestedParticles; // required buffer size @@ -1983,6 +2006,7 @@ void* particleMemoryManager(const uint32_t requestedParticles, size_t structSize } else { // if the id was not found create a buffer and add an element to the list availableToPS = 0; // new PS starts with zero particles, they are transferred in the next call + PSPRINTLN("New particle buffer request: " + String(requestedParticles)); uint32_t requestsize = structSize * requestedParticles; // required buffer size buffer = allocatePSmemory(requestsize, false); // allocate new memory if (buffer) @@ -2022,6 +2046,7 @@ void* particleMemoryManager(const uint32_t requestedParticles, size_t structSize availableToPS = newAvailable; } else { // no PS transition, full buffer available if(pmem->transferParticles) { // transition ended (or blending is disabled) -> transfer all remaining particles + PSPRINTLN("PS transition ended, final particle handover"); uint32_t maxParticles = pmem->buffersize / structSize; // maximum number of particles that fit in the buffer if (maxParticles > availableToPS) { // not all particles transferred yet int32_t totransfer = maxParticles - availableToPS; // transfer all remaining particles @@ -2029,6 +2054,7 @@ void* particleMemoryManager(const uint32_t requestedParticles, size_t structSize particleHandover(buffer, structSize, totransfer); } availableToPS = maxParticles; // update available particles + PSPRINTLN("final available particles: " + String(availableToPS)); // kill unused particles to they do not re-appear when transitioning to next FX uint32_t used = (maxParticles * fractionUsed) >> 8; // calculate number of particles used (this may not always be accurate but good enough) #ifndef WLED_DISABLE_PARTICLESYSTEM2D @@ -2053,6 +2079,10 @@ void* particleMemoryManager(const uint32_t requestedParticles, size_t structSize } pmem->inTransition = false; } + #ifdef WLED_DEBUG_PS + PSPRINT(" Particle memory Pointer address: 0x"); + Serial.println((uintptr_t)buffer, HEX); + #endif return buffer; } diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index 553ea64aa8..cf83c35a38 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -16,7 +16,7 @@ #define PS_P_MAXSPEED 120 // maximum speed a particle can have (vx/vy is int8) #define MAX_MEMIDLE 10 // max idle time (in frames) before memory is deallocated (if deallocated during an effect, it will crash!) -//#define WLED_DEBUG_PS +#define WLED_DEBUG_PS #ifdef WLED_DEBUG_PS #define PSPRINT(x) Serial.print(x) @@ -89,11 +89,11 @@ typedef union { typedef struct { // 12 bytes int16_t x; // x position in particle system int16_t y; // y position in particle system - uint16_t ttl; // time to live int8_t vx; // horizontal velocity int8_t vy; // vertical velocity uint8_t hue; // color hue uint8_t sat; // particle color saturation + uint16_t ttl; // time to live in frames //uint16_t ttl : 12; // time to live, 12 bit or 4095 max (which is 50s at 80FPS) bool outofbounds : 1; // out of bounds flag, set to true if particle is outside of display area bool collide : 1; // if set, particle takes part in collisions @@ -271,18 +271,21 @@ typedef union { byte asByte; // access as a byte, order is: LSB is first entry in the list above } PSsettings1D; -//struct for a single particle (7 bytes) +//struct for a single particle (8 bytes) typedef struct { int16_t x; // x position in particle system int8_t vx; // horizontal velocity uint8_t hue; // color hue // two byte bit field: - uint16_t ttl : 11; // time to live, 11 bit or 2047 max (which is 25s at 80FPS) + //uint16_t ttl : 11; // time to live, 11 bit or 2047 max (which is 25s at 80FPS) + uint16_t ttl; // time to live in frames bool outofbounds : 1; // out of bounds flag, set to true if particle is outside of display area bool collide : 1; // if set, particle takes part in collisions bool perpetual : 1; // if set, particle does not age (TTL is not decremented in move function, it still dies from killoutofbounds) bool reversegrav : 1; // if set, gravity is reversed on this particle bool fixed : 1; // if set, particle does not move (and collisions make other particles revert direction), + // note: there is on byte of padding added here, making TTL a 16bit variable saves 500bytes of flash so much faster than a bit field + // TODO: can this be optimized? wastes a lot of ram... } PSparticle1D; // struct for additional particle settings (optional) @@ -292,7 +295,7 @@ typedef struct { uint8_t forcecounter; } PSadvancedParticle1D; -//struct for a particle source (17 bytes) +//struct for a particle source (16 bytes) typedef struct { uint16_t minLife; // minimum ttl of emittet particles uint16_t maxLife; // maximum ttl of emitted particles @@ -330,6 +333,7 @@ class ParticleSystem1D void setColorByAge(bool enable); void setColorByPosition(bool enable); void setMotionBlur(uint8_t bluramount); // note: motion blur can only be used if 'particlesize' is set to zero + void setSmearBlur(uint8_t bluramount); // enable 1D smeared blurring of full frame void setParticleSize(uint8_t size); //size 0 = 1 pixel, size 1 = 2 pixels, is overruled by advanced particle size void setGravity(int8_t force = 8); void enableParticleCollisions(bool enable, uint8_t hardness = 255); @@ -373,7 +377,8 @@ class ParticleSystem1D //uint8_t collisioncounter; // counter to handle collisions TODO: could use the SEGMENT.call? -> currently unused //global particle properties for basic particles uint8_t particlesize; // global particle size, 0 = 1 pixel, 1 = 2 pixels, larger sizez TBD (TODO: need larger sizes?) - uint8_t motionBlur; // enable motion blur, values > 100 gives smoother animations. Note: motion blurring does not work if particlesize is > 0 + uint8_t motionBlur; // enable motion blur, values > 100 gives smoother animations + uint8_t smearBlur; // smeared blurring of full frame uint8_t effectID; // ID of the effect that is using this particle system, used for transitions }; From c546cc2a53cfa200b4152276b92ffd2e448fb649 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Mon, 30 Dec 2024 09:49:03 +0100 Subject: [PATCH 151/219] BUGFIX: out of bounds checking was wrong, leading to crashes --- wled00/FXparticleSystem.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index 79b445e165..dd79ed4679 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -793,7 +793,7 @@ void ParticleSystem2D::renderParticle(const uint32_t particleindex, const uint32 } } } else { // standard rendering - // check for out of frame pixels and wrap them if required + // check for out of frame pixels and wrap them if required: x,y is bottom left pixel coordinate of the particle if (x < 0) { // left pixels out of frame if (wrapX) { // wrap x to the other side if required pixco[0][0] = pixco[3][0] = maxXpixel; @@ -801,7 +801,7 @@ void ParticleSystem2D::renderParticle(const uint32_t particleindex, const uint32 pixelvalid[0] = pixelvalid[3] = false; // out of bounds } } - else if (pixco[1][0] > (int32_t)maxXpixel) { // right pixels, only has to be checkt if left pixels did not overflow + else if (pixco[1][0] > (int32_t)maxXpixel) { // right pixels, only has to be checked if left pixel is in frame if (wrapX) { // wrap y to the other side if required pixco[1][0] = pixco[2][0] = 0; } else { @@ -1880,9 +1880,9 @@ static int32_t limitSpeed(int32_t speed) { //return speed > PS_P_MAXSPEED ? PS_P_MAXSPEED : (speed < -PS_P_MAXSPEED ? -PS_P_MAXSPEED : speed); // note: this uses more code, not sure due to speed or inlining } -// check if particle is out of bounds and wrap it around if required, returns true if out of bounds +// check if particle is out of bounds and wrap it around if required, returns false if out of bounds static bool checkBoundsAndWrap(int32_t &position, const int32_t max, const int32_t particleradius, bool wrap) { - if (position > max) { // check if particle reached an edge + if ((uint32_t)position > max) { // check if particle reached an edge, cast to uint32_t to save negative checking if (wrap) { position = position % (max + 1); // note: cannot optimize modulo, particles can be far out of bounds when wrap is enabled if (position < 0) @@ -1946,14 +1946,14 @@ void* allocatePSmemory(size_t size, bool overridelimit) { // buffer uses effect data, check if there is enough space if (!overridelimit && Segment::getUsedSegmentData() + size > MAX_SEGMENT_DATA) { // not enough memory - DEBUG_PRINT(F("!!! Effect RAM depleted: ")); + PSPRINT(F("!!! Effect RAM depleted: ")); DEBUG_PRINTF_P(PSTR("%d/%d !!!\n"), size, Segment::getUsedSegmentData()); errorFlag = ERR_NORAM; return nullptr; } void* buffer = calloc(size, sizeof(byte)); if (buffer == nullptr) { - DEBUG_PRINT(F("!!! Memory allocation failed !!!")); + PSPRINT(F("!!! Memory allocation failed !!!")); errorFlag = ERR_NORAM; return nullptr; } From 371645e8cf7963e913fa188b8a1c9d7892c3f815 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Mon, 30 Dec 2024 14:04:08 +0100 Subject: [PATCH 152/219] better handling of used particles in transitions for FX that do not use all particles, transitions were constantly shifting the pointer, resulting in newly generated particles and weird particle flickering, this is not fixed by keeping the pointer constant once the number of used particles is reached. --- wled00/FXparticleSystem.cpp | 55 ++++++++++++++++++++++++------------- wled00/FXparticleSystem.h | 9 ++---- 2 files changed, 38 insertions(+), 26 deletions(-) diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index dd79ed4679..463c044947 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -997,7 +997,10 @@ PSPRINTLN("updatePSpointers"); // a pointer MUST be 4 byte aligned. sizeof() in a struct/class is always aligned to the largest element. if it contains a 32bit, it will be padded to 4 bytes, 16bit is padded to 2byte alignment. // The PS is aligned to 4 bytes, a PSparticle is aligned to 2 and a struct containing only byte sized variables is not aligned at all and may need to be padded when dividing the memoryblock. // by making sure that the number of sources and particles is a multiple of 4, padding can be skipped here as alignent is ensured, independent of struct sizes. - particles = reinterpret_cast(particleMemoryManager(0, sizeof(PSparticle), availableParticles, fractionOfParticlesUsed, effectID)); // get memory, leave buffer size as is (request 0) + + // memory manager needs to know how many particles the FX wants to use so transitions can be handled properly (i.e. pointer will stop changing if enough particles are available during transitions) + uint32_t usedByFX = (numParticles * ((uint32_t)fractionOfParticlesUsed + 1)) >> 8; // final number of particles the FX wants to use (fractionOfParticlesUsed is 0-255) + particles = reinterpret_cast(particleMemoryManager(0, sizeof(PSparticle), availableParticles, usedByFX, effectID)); // get memory, leave buffer size as is (request 0) sources = reinterpret_cast(this + 1); // pointer to source(s) at data+sizeof(ParticleSystem2D) PSdataEnd = reinterpret_cast(sources + numSources); // pointer to first available byte after the PS for FX additional data if (isadvanced) { @@ -1745,7 +1748,10 @@ void ParticleSystem1D::updatePSpointers(bool isadvanced) { // a pointer MUST be 4 byte aligned. sizeof() in a struct/class is always aligned to the largest element. if it contains a 32bit, it will be padded to 4 bytes, 16bit is padded to 2byte alignment. // The PS is aligned to 4 bytes, a PSparticle is aligned to 2 and a struct containing only byte sized variables is not aligned at all and may need to be padded when dividing the memoryblock. // by making sure that the number of sources and particles is a multiple of 4, padding can be skipped here as alignent is ensured, independent of struct sizes. - particles = reinterpret_cast(particleMemoryManager(0, sizeof(PSparticle1D), availableParticles, fractionOfParticlesUsed, effectID)); // get memory, leave buffer size as is (request 0) + + // memory manager needs to know how many particles the FX wants to use so transitions can be handled properly (i.e. pointer will stop changing if enough particles are available during transitions) + uint32_t usedByFX = (numParticles * ((uint32_t)fractionOfParticlesUsed + 1)) >> 8; // final number of particles the FX wants to use (fractionOfParticlesUsed is 0-255) + particles = reinterpret_cast(particleMemoryManager(0, sizeof(PSparticle1D), availableParticles, usedByFX, effectID)); // get memory, leave buffer size as is (request 0) sources = reinterpret_cast(this + 1); // pointer to source(s) PSdataEnd = reinterpret_cast(sources + numSources); // pointer to first available byte after the PS for FX additional data if (isadvanced) { @@ -1974,7 +1980,7 @@ void deallocatePSmemory(void* dataptr, uint32_t size) { } // Particle transition manager, creates/extends buffer if needed and handles transition memory-handover -void* particleMemoryManager(const uint32_t requestedParticles, size_t structSize, uint32_t &availableToPS, uint8_t fractionUsed, const uint8_t effectID) { +void* particleMemoryManager(const uint32_t requestedParticles, size_t structSize, uint32_t &availableToPS, uint32_t numParticlesUsed, const uint8_t effectID) { pmem = getPartMem(); void* buffer = nullptr; PSPRINTLN("PS MemManager"); @@ -2029,6 +2035,7 @@ void* particleMemoryManager(const uint32_t requestedParticles, size_t structSize uint32_t newAvailable = 0; if (SEGMENT.mode == effectID) { // new effect ID -> function was called from new FX newAvailable = (maxParticles * progress) >> 16; // update total particles available to this PS (newAvailable is guaranteed to be smaller than maxParticles) + if(newAvailable > numParticlesUsed) newAvailable = numParticlesUsed; // limit to number of particles used by the new FX and do not move the pointer anymore (will be set to base in final handover) uint32_t bufferoffset = (maxParticles - 1) - newAvailable; // offset to new effect particles if(bufferoffset < maxParticles) // safety check buffer = (void*)((uint8_t*)buffer + bufferoffset * structSize); // new effect gets the end of the buffer @@ -2052,15 +2059,17 @@ void* particleMemoryManager(const uint32_t requestedParticles, size_t structSize int32_t totransfer = maxParticles - availableToPS; // transfer all remaining particles if(totransfer < 0) totransfer = 0; // safety check particleHandover(buffer, structSize, totransfer); + // move the already existing particles to the beginning of the buffer + uint32_t usedbytes = availableToPS * structSize; + uint32_t bufferoffset = (maxParticles - 1) - availableToPS; // offset to existing particles (see above) + void* currentBuffer = (void*)((uint8_t*)buffer + bufferoffset * structSize); // pointer to current buffer start + memmove(buffer, currentBuffer, usedbytes); // move the existing particles to the beginning of the buffer } - availableToPS = maxParticles; // update available particles - PSPRINTLN("final available particles: " + String(availableToPS)); // kill unused particles to they do not re-appear when transitioning to next FX - uint32_t used = (maxParticles * fractionUsed) >> 8; // calculate number of particles used (this may not always be accurate but good enough) #ifndef WLED_DISABLE_PARTICLESYSTEM2D if (structSize == sizeof(PSparticle)) { // 2D particle PSparticle *particles = (PSparticle *)buffer; - for (uint32_t i = used; i < maxParticles; i++) { + for (uint32_t i = availableToPS; i < maxParticles; i++) { particles[i].ttl = 0; // kill unused particles } } @@ -2069,11 +2078,13 @@ void* particleMemoryManager(const uint32_t requestedParticles, size_t structSize { #ifndef WLED_DISABLE_PARTICLESYSTEM1D PSparticle1D *particles = (PSparticle1D *)buffer; - for (uint32_t i = used; i < maxParticles; i++) { + for (uint32_t i = availableToPS; i < maxParticles; i++) { particles[i].ttl = 0; // kill unused particles } #endif } + availableToPS = maxParticles; // now all particles are available to new FX + PSPRINTLN("final available particles: " + String(availableToPS)); pmem->particleType = structSize; // update particle type pmem->transferParticles = false; } @@ -2124,6 +2135,23 @@ void particleHandover(void *buffer, size_t structSize, int32_t numToTransfer) { } } +// update number of particles to use, limit to allocated (= particles allocated by the calling system) in case more are available in the buffer +void updateUsedParticles(const uint32_t allocated, const uint32_t available, const uint8_t percentage, uint32_t &used) { + uint32_t wantsToUse = (allocated * ((uint32_t)percentage + 1)) >> 8; + used = min(available, wantsToUse); // limit to available particles +} + +// get the pointer to the particle memory for the segment +partMem* getPartMem(void) { // TODO: maybe there is a better/faster way than using vectors? + uint8_t segID = strip.getCurrSegmentId(); + for (partMem &pmem : partMemList) { + if (pmem.id == segID) { + return &pmem; + } + } + return nullptr; +} + // function to update the framebuffer and renderbuffer void updateRenderingBuffer(uint32_t requiredpixels, bool isFramebuffer) { PSPRINTLN("updateRenderingBuffer"); @@ -2140,17 +2168,6 @@ void updateRenderingBuffer(uint32_t requiredpixels, bool isFramebuffer) { } } -// get the pointer to the particle memory for the segment -partMem* getPartMem(void) { // TODO: maybe there is a better/faster way than using vectors? - uint8_t segID = strip.getCurrSegmentId(); - for (partMem &pmem : partMemList) { - if (pmem.id == segID) { - return &pmem; - } - } - return nullptr; -} - // service the particle system memory, free memory if idle too long // note: doing it this way makes it independent of the implementation of segment management but is not the most memory efficient way void servicePSmem() { diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index cf83c35a38..8f4ee91e08 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -37,18 +37,13 @@ struct partMem { bool transferParticles; // if set, particles in buffer are transferred to new FX }; -void* particleMemoryManager(const uint32_t requestedParticles, size_t structSize, uint32_t &availableToPS, uint8_t fractionUsed, const uint8_t effectID); // update particle memory pointer, handles memory transitions +void* particleMemoryManager(const uint32_t requestedParticles, size_t structSize, uint32_t &availableToPS, uint32_t numParticlesUsed, const uint8_t effectID); // update particle memory pointer, handles memory transitions void particleHandover(void *buffer, size_t structSize, int32_t numParticles); +void updateUsedParticles(const uint32_t allocated, const uint32_t available, const uint8_t percentage, uint32_t &used); partMem* getPartMem(void); // returns pointer to memory struct for current segment or nullptr void updateRenderingBuffer(uint32_t requiredsize, bool isFramebuffer); // allocate CRGB rendering buffer, update size if needed void transferBuffer(uint32_t width, uint32_t height, bool useAdditiveTransfer = false); // transfer the buffer to the segment (supports 1D and 2D) void servicePSmem(); // increments watchdog, frees memory if idle too long - -// update number of particles to use, must never be more than allocated (= particles allocated by the calling system) -inline void updateUsedParticles(const uint32_t allocated, const uint32_t available, const uint8_t percentage, uint32_t &used) { - used = max((uint32_t)1, (min(allocated, available) * ((uint32_t)percentage + 1)) >> 8); // always use a minimum of 1 particle -} - #endif //TODO: maybe update PS_P_MINSURFACEHARDNESS for 2D? its a bit too sticky already at hardness 100 From 8fb48d6444bfe2303f49ea351c3cb047f6204eb6 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Mon, 30 Dec 2024 18:03:02 +0100 Subject: [PATCH 153/219] improved particle transition for low-count FX, updated 1D FX now most 1D FX work again, still needs some fine-tuning --- wled00/FX.cpp | 25 +++++++++++++------------ wled00/FXparticleSystem.cpp | 24 ++++++++++++++++-------- wled00/FXparticleSystem.h | 4 +++- 3 files changed, 32 insertions(+), 21 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 80fa8bafe7..2dde6c70ea 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -8014,7 +8014,7 @@ uint16_t mode_particlefire(void) { PartSys->updateSystem(); // update system properties (dimensions and data pointers) PartSys->setWrapX(SEGMENT.check2); - PartSys->setMotionBlur(SEGMENT.check1 * 180); // anable/disable motion blur + PartSys->setMotionBlur(SEGMENT.check1 * 170); // anable/disable motion blur uint32_t firespeed = max((uint8_t)100, SEGMENT.speed); //limit speed to 100 minimum, reduce frame rate to make it slower (slower speeds than 100 do not look nice) if (SEGMENT.speed < 100) { //slow, limit FPS @@ -8170,7 +8170,7 @@ uint16_t mode_particlewaterfall(void) { PartSys->setGravity(); // enable with default gforce PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting) - PartSys->setMotionBlur(175); // anable motion blur + PartSys->setMotionBlur(170); // anable motion blur PartSys->setSmearBlur(45); // enable 2D blurring (smearing) for (i = 0; i < PartSys->numSources; i++) { PartSys->sources[i].source.hue = i*90; @@ -9236,7 +9236,7 @@ uint16_t mode_particleBouncingBalls(void) { PartSys->particles[i].vx = PartSys->particles[i].vx > 0 ? newspeed : -newspeed; //keep the direction PartSys->particles[i].hue = hw_random8(); //set ball colors to random PartSys->advPartProps[i].sat = 255; - PartSys->advPartProps[i].size = hw_random16(SEGMENT.custom1 >> 2, SEGMENT.custom1); + PartSys->advPartProps[i].size = SEGMENT.custom1; } } } @@ -9253,7 +9253,7 @@ uint16_t mode_particleBouncingBalls(void) { SEGENV.aux0 = (260 - SEGMENT.intensity) + hw_random16(280 - SEGMENT.intensity); PartSys->sources[0].source.hue = hw_random16(); //set ball color PartSys->sources[0].sat = 255; - PartSys->sources[0].size = hw_random16(SEGMENT.custom1 >> 2, SEGMENT.custom1); + PartSys->sources[0].size = SEGMENT.custom1; PartSys->sprayEmit(PartSys->sources[0]); } } @@ -9618,7 +9618,7 @@ uint16_t mode_particleHourglass(void) { case 3: PartSys->particles[i].hue = *basehue + (i % colormode)*70; break; // interleved colors (every 2 or 3 particles) case 4: PartSys->particles[i].hue = *basehue + (i * 255) / PartSys->usedParticles; break; // gradient palette colors case 5: PartSys->particles[i].hue = *basehue + (i * 1024) / PartSys->usedParticles; break; // multi gradient palette colors - case 6: PartSys->particles[i].hue = i + (strip.now >> 1); break; // disco! fast moving color gradient + case 6: PartSys->particles[i].hue = i + (strip.now >> 3); break; // disco! moving color gradient default: break; } } @@ -9629,6 +9629,7 @@ uint16_t mode_particleHourglass(void) { if(SEGENV.aux1 == 1) { //last countdown call before dropping starts, reset all particles for(uint32_t i = 0; i < PartSys->usedParticles; i++) { PartSys->particles[i].collide = true; + PartSys->particles[i].perpetual = true; PartSys->particles[i].ttl = 260; uint32_t targetposition; //calculate target position depending on direction @@ -9699,10 +9700,10 @@ uint16_t mode_particle1Dspray(void) { PartSys->updateSystem(); // update system properties (dimensions and data pointers) PartSys->setBounce(SEGMENT.check2); PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur - int32_t gravity = (int32_t)SEGMENT.custom3 - 15; //gravity setting, 0-14 is negative, 16 - 31 is positive + int32_t gravity = (int32_t)SEGMENT.custom3 - 15; // gravity setting, 0-14 is negative, 16 - 31 is positive PartSys->setGravity(abs(gravity)); // use reversgrav setting to invert gravity (for proper 'floor' and out of bounce handling) - PartSys->sources[0].source.hue = hw_random16(); //TODO: add colormodes like in hourglass? + PartSys->sources[0].source.hue = hw_random16(); // TODO: add colormodes like in hourglass? PartSys->sources[0].var = 20; PartSys->sources[0].minLife = 200; PartSys->sources[0].maxLife = 400; @@ -9711,13 +9712,13 @@ uint16_t mode_particle1Dspray(void) { PartSys->sources[0].source.reversegrav = gravity < 0 ? true : false; if(hw_random() % (1 + ((255 - SEGMENT.intensity) >> 3)) == 0) - PartSys->sprayEmit(PartSys->sources[0]); //emit a particle + PartSys->sprayEmit(PartSys->sources[0]); // emit a particle //update color settings - PartSys->setColorByAge(SEGMENT.check1); //overruled by 'color by position' + PartSys->setColorByAge(SEGMENT.check1); // overruled by 'color by position' PartSys->setColorByPosition(SEGMENT.check3); for(uint i = 0; i < PartSys->usedParticles; i++) { - PartSys->particles[i].reversegrav = PartSys->sources[0].source.reversegrav; //update gravity direction + PartSys->particles[i].reversegrav = PartSys->sources[0].source.reversegrav; // update gravity direction } PartSys->update(); // update and render Serial.println("used by FX: " + String(PartSys->usedParticles)); @@ -9826,11 +9827,11 @@ uint16_t mode_particleChase(void) { //uint8_t* basehue = (PartSys->PSdataEnd + 2); //assign data pointer //PartSys->setBounce(SEGMENT.check2); - uint32_t settingssum = SEGMENT.speed + SEGMENT.intensity + SEGMENT.custom1 + SEGMENT.custom2 + SEGMENT.check1 + SEGMENT.check2 + SEGMENT.check3; // note: progress is used to enforce update during transitions + uint32_t settingssum = SEGMENT.speed + SEGMENT.intensity + SEGMENT.custom1 + SEGMENT.custom2 + SEGMENT.check1 + SEGMENT.check2 + SEGMENT.check3 + PartSys->getUsedParticles(); // note: progress is used to enforce update during transitions if(SEGENV.aux0 != settingssum) { //settings changed changed, update //PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 1, min(PartSys->maxX / (32 + (SEGMENT.custom1 >> 1)), int32_t(PartSys->usedParticles)))); //depends on intensity and particle size (custom1) !!! TODO: is this fine now? PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 1, 255 / (1 + (SEGMENT.custom1 >> 6)))); //depends on intensity and particle size (custom1) - SEGENV.step = (PartSys->maxX + (PS_P_RADIUS_1D << 4)) / PartSys->usedParticles; //spacing between particles + SEGENV.step = (PartSys->maxX + (PS_P_RADIUS_1D << 5)) / PartSys->usedParticles; //spacing between particles // uint32_t remainder = PartSys->maxX - ((PartSys->usedParticles) * SEGENV.step); // unused spacing, distribute this for(i = 0; i < PartSys->usedParticles; i++) { PartSys->advPartProps[i].sat = 255; diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index 463c044947..b718865781 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -1195,6 +1195,12 @@ ParticleSystem1D::ParticleSystem1D(uint32_t length, uint32_t numberofparticles, for (uint32_t i = 0; i < numSources; i++) { sources[i].source.ttl = 1; //set source alive } + + if(isadvanced) { + for (uint32_t i = 0; i < numParticles; i++) { + advPartProps[i].sat = 255; // set full saturation (for particles that are transferred from non-advanced system) + } + } } // update function applies gravity, moves the particles, handles collisions and renders the particles @@ -2035,7 +2041,7 @@ void* particleMemoryManager(const uint32_t requestedParticles, size_t structSize uint32_t newAvailable = 0; if (SEGMENT.mode == effectID) { // new effect ID -> function was called from new FX newAvailable = (maxParticles * progress) >> 16; // update total particles available to this PS (newAvailable is guaranteed to be smaller than maxParticles) - if(newAvailable > numParticlesUsed) newAvailable = numParticlesUsed; // limit to number of particles used by the new FX and do not move the pointer anymore (will be set to base in final handover) + if(maxParticles / numParticlesUsed > 3 && newAvailable > numParticlesUsed) newAvailable = numParticlesUsed; // limit to number of particles used for FX using a small amount, do not move the pointer anymore (will be set to base in final handover) uint32_t bufferoffset = (maxParticles - 1) - newAvailable; // offset to new effect particles if(bufferoffset < maxParticles) // safety check buffer = (void*)((uint8_t*)buffer + bufferoffset * structSize); // new effect gets the end of the buffer @@ -2059,11 +2065,12 @@ void* particleMemoryManager(const uint32_t requestedParticles, size_t structSize int32_t totransfer = maxParticles - availableToPS; // transfer all remaining particles if(totransfer < 0) totransfer = 0; // safety check particleHandover(buffer, structSize, totransfer); - // move the already existing particles to the beginning of the buffer - uint32_t usedbytes = availableToPS * structSize; - uint32_t bufferoffset = (maxParticles - 1) - availableToPS; // offset to existing particles (see above) - void* currentBuffer = (void*)((uint8_t*)buffer + bufferoffset * structSize); // pointer to current buffer start - memmove(buffer, currentBuffer, usedbytes); // move the existing particles to the beginning of the buffer + if(maxParticles / numParticlesUsed > 3) { // FX uses less than 25%: move the already existing particles to the beginning of the buffer + uint32_t usedbytes = availableToPS * structSize; + uint32_t bufferoffset = (maxParticles - 1) - availableToPS; // offset to existing particles (see above) + void* currentBuffer = (void*)((uint8_t*)buffer + bufferoffset * structSize); // pointer to current buffer start + memmove(buffer, currentBuffer, usedbytes); // move the existing particles to the beginning of the buffer + } } // kill unused particles to they do not re-appear when transitioning to next FX #ifndef WLED_DISABLE_PARTICLESYSTEM2D @@ -2110,7 +2117,7 @@ void particleHandover(void *buffer, size_t structSize, int32_t numToTransfer) { if (particles[i].outofbounds) particles[i].ttl = 0; // kill out of bounds else if (particles[i].ttl > 200) - particles[i].ttl = 200; // reduce TTL so it will die soon + particles[i].ttl = 150 + hw_random16(50); // reduce TTL so it will die soon particles[i].sat = 255; // full saturation particles[i].collide = true; // enable collisions (in case new FX uses them) } @@ -2126,10 +2133,11 @@ void particleHandover(void *buffer, size_t structSize, int32_t numToTransfer) { } for (int32_t i = 0; i < numToTransfer; i++) { particles[i].perpetual = false; // particle ages + particles[i].fixed = false; // unfix all particles if (particles[i].outofbounds) particles[i].ttl = 0; // kill out of bounds else if (particles[i].ttl > 200) - particles[i].ttl = 200; // reduce TTL so it will die soon + particles[i].ttl = 150 + hw_random16(50); // reduce TTL so it will die soon } #endif } diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index 8f4ee91e08..fa8079a48e 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -16,7 +16,7 @@ #define PS_P_MAXSPEED 120 // maximum speed a particle can have (vx/vy is int8) #define MAX_MEMIDLE 10 // max idle time (in frames) before memory is deallocated (if deallocated during an effect, it will crash!) -#define WLED_DEBUG_PS +//#define WLED_DEBUG_PS #ifdef WLED_DEBUG_PS #define PSPRINT(x) Serial.print(x) @@ -158,6 +158,7 @@ class ParticleSystem2D { void lineAttractor(uint16_t particleindex, PSparticle *attractorcenter, uint16_t attractorangle, uint8_t strength); // set options void setUsedParticles(uint8_t percentage); // set the percentage of particles used in the system, 255=100% + inline uint32_t getUsedParticles(void) { return usedParticles; } 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 setWallRoughness(uint8_t roughness); // wall roughness randomizes wall collisions @@ -319,6 +320,7 @@ class ParticleSystem1D void applyFriction(int32_t coefficient); // apply friction to all used particles // set options void setUsedParticles(uint8_t percentage); // set the percentage of particles used in the system, 255=100% + inline uint32_t getUsedParticles(void) { return usedParticles; } void setWallHardness(uint8_t hardness); // hardness for bouncing on the wall if bounceXY is set void setSize(uint16_t x); //set particle system size (= strip length) void setWrap(bool enable); From 2db41363fee3549708ec159dce96136f66f88e36 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Wed, 1 Jan 2025 20:43:40 +0100 Subject: [PATCH 154/219] Rework on Fireworks 1D, some minor fixes - fireworks 1D now matches as closely as possible to the old FX still not identical but as good as it gets. --- wled00/FX.cpp | 150 ++++++++++++++++++++---------------- wled00/FXparticleSystem.cpp | 19 ++--- wled00/FXparticleSystem.h | 2 +- 3 files changed, 92 insertions(+), 79 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 2dde6c70ea..f8001d9020 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -9178,7 +9178,7 @@ uint16_t mode_particleDrip(void) { PartSys->update(); // update and render return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEDRIP[] PROGMEM = "PS DripDrop@Speed,!,Splash,Blur/Overlay,Gravity,Rain,PushSplash,Smooth;,!;!;1;pal=0,sx=150,ix=25,c1=220,c2=30,c3=21"; +static const char _data_FX_MODE_PARTICLEDRIP[] PROGMEM = "PS DripDrop@Speed,!,Splash,Blur,Gravity,Rain,PushSplash,Smooth;,!;!;1;pal=0,sx=150,ix=25,c1=220,c2=30,c3=21"; /* @@ -9266,7 +9266,7 @@ uint16_t mode_particleBouncingBalls(void) { PartSys->update(); // update and render return FRAMETIME; } -static const char _data_FX_MODE_PSBOUNCINGBALLS[] PROGMEM = "PS Bouncing Balls@Speed,!,Size,Blur/Overlay,Gravity,Collide,Rolling,Color by Position;,!;!;1;pal=0,sx=100,ix=85,c1=30,c2=0,c3=8"; +static const char _data_FX_MODE_PSBOUNCINGBALLS[] PROGMEM = "PS Bouncing Balls@Speed,!,Size,Blur,Gravity,Collide,Rolling,Color by Position;,!;!;1;pal=0,sx=100,ix=85,c1=30,c2=0,c3=8"; /* Particle Replacement for original Dancing Shadows: @@ -9297,13 +9297,37 @@ uint16_t mode_particleDancingShadows(void) { // Particle System settings PartSys->updateSystem(); // update system properties (dimensions and data pointers) PartSys->setMotionBlur(SEGMENT.custom1); + if (SEGMENT.check1) + PartSys->setSmearBlur(120); // enable smear blur + else + PartSys->setSmearBlur(0); // disable smear blur PartSys->setParticleSize(SEGMENT.check3); // 1 or 2 pixel rendering + PartSys->setColorByPosition(SEGMENT.check2); // color fixed by position + PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 10, 255)); // set percentage of particles to use + + uint32_t deadparticles = 0; + //kill out of bounds and moving away plus change color + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { + if(((SEGMENT.call & 0x07) == 0) && PartSys->particles[i].outofbounds) { //check if out of bounds particle move away from strip, only update every 8th frame + if((int32_t)PartSys->particles[i].vx * PartSys->particles[i].x > 0) PartSys->particles[i].ttl = 0; //particle is moving away, kill it + } + PartSys->particles[i].perpetual = true; //particles do not age + if (SEGMENT.call % (32 / (1 + (SEGMENT.custom2 >> 3))) == 0) + PartSys->particles[i].hue += 2 + (SEGMENT.custom2 >> 5); + //note: updating speed on the fly is not accurately possible, since it is unknown which particles are assigned to which spot + if(SEGENV.aux0 != SEGMENT.speed) { //speed changed + //update all particle speed by setting them to current value + PartSys->particles[i].vx = PartSys->particles[i].vx > 0 ? SEGMENT.speed >> 3 : -SEGMENT.speed >> 3; + } + if(PartSys->particles[i].ttl == 0) deadparticles++; // count dead particles + } + SEGENV.aux0 = SEGMENT.speed; //generate a spotlight: generates particles just outside of view - if (SEGMENT.call % (256 - SEGMENT.intensity) == 0) { + if (deadparticles > 5 && (SEGMENT.call & 0x03) == 0) { //random color, random type uint32_t type = hw_random16(SPOT_TYPES_COUNT); - int8_t speed = 2 + hw_random16(2 + (SEGMENT.speed >> 2)) + (SEGMENT.speed >> 3); + int8_t speed = 2 + hw_random16(2 + (SEGMENT.speed >> 1)) + (SEGMENT.speed >> 4); uint32_t width = hw_random16(1, 10); uint32_t ttl = 300; //ttl is particle brightness (below perpetual is set so it does not age, i.e. ttl stays at this value) int32_t position; @@ -9357,27 +9381,11 @@ uint16_t mode_particleDancingShadows(void) { } } - //kill out of bounds and moving away plus change color - for (uint32_t i = 0; i < PartSys->usedParticles; i++) { - if(PartSys->particles[i].outofbounds) { //check if out of bounds particle move away from strip (i.e. vx < 0 && x > 0 or vx > 0 and x < 0) - if((int32_t)PartSys->particles[i].vx * PartSys->particles[i].x > 0) PartSys->particles[i].ttl = 0; //particle is moving away, kill it - } - PartSys->particles[i].perpetual = true; //particles do not age - if (SEGMENT.call % (32 / (1 + (SEGMENT.custom2 >> 3))) == 0) - PartSys->particles[i].hue += 2 + (SEGMENT.custom2 >> 5); - //note: updating speed on the fly is not accurately possible, since it is unknown which particles are assigned to which spot - if(SEGENV.aux0 != SEGMENT.speed) { //speed changed - //update all particle speed by setting them to current value - PartSys->particles[i].vx = PartSys->particles[i].vx > 0 ? SEGMENT.speed >> 3 : -SEGMENT.speed >> 3; - } - } - SEGENV.aux0 = SEGMENT.speed; - PartSys->update(); // update and render return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEDANCINGSHADOWS[] PROGMEM = "PS Dancing Shadows@Speed,!,Blur/Overlay,Color Cycle,,,,Smooth;,!;!;1;pal=0,sx=100,ix=180,c1=0,c2=0"; +static const char _data_FX_MODE_PARTICLEDANCINGSHADOWS[] PROGMEM = "PS Dancing Shadows@Speed,!,Blur,Color Cycle,,Smear,Color by position,Smooth;,!;!;1;sx=100,ix=180,c1=0,c2=0"; /* Particle Fireworks 1D replacement @@ -9393,7 +9401,7 @@ uint16_t mode_particleFireworks1D(void) { if (!initParticleSystem1D(PartSys, 4, 150, 4, true)) // init return mode_static(); // allocation failed or is single pixel PartSys->setKillOutOfBounds(true); - PartSys->sources[0].source.perpetual = 1; //set rocket state to standby + PartSys->sources[0].source.perpetual = 1; // set rocket state to standby } else PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS @@ -9406,80 +9414,88 @@ uint16_t mode_particleFireworks1D(void) { PartSys->setParticleSize(SEGMENT.check3); // 1 or 2 pixel rendering PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur - if(!SEGMENT.check1) //gravity enabled for sparks + int32_t gravity = (1 + (SEGMENT.speed >> 3)); + if(!SEGMENT.check1) // gravity enabled for sparks PartSys->setGravity(0); // disable else - PartSys->setGravity(1 + (SEGMENT.speed>>4)); // set gravity + PartSys->setGravity(gravity); // set gravity - if(PartSys->sources[0].source.perpetual == 1) { //rocket is on standby + if(PartSys->sources[0].source.perpetual == 1) { // rocket is on standby PartSys->sources[0].source.ttl--; - if(PartSys->sources[0].source.ttl == 0) { //time is up, relaunch - if(hw_random8() < SEGMENT.custom1) //randomly choose direction according to slider, fire at start of segment if true - SEGENV.aux0 = 0; + if(PartSys->sources[0].source.ttl == 0) { // time is up, relaunch + + if(hw_random8() < SEGMENT.custom1) // randomly choose direction according to slider, fire at start of segment if true + SEGENV.aux0 = 1; else - SEGENV.aux0 = 1; //invert direction + SEGENV.aux0 = 0; PartSys->sources[0].source.perpetual = 0; //flag abused for rocket state PartSys->sources[0].source.hue = hw_random16(); - PartSys->sources[0].var = 5; - PartSys->sources[0].v = 0; + PartSys->sources[0].var = 10; PartSys->sources[0].minLife = 10; PartSys->sources[0].maxLife = 30; PartSys->sources[0].source.x = 0; // start from bottom - uint32_t speed = 8 + (((int)16 + (int)random16(20) + (int)hw_random16(SEGMENT.speed >> 3 , SEGMENT.speed >> 2)) * (int)PartSys->maxXpixel) / 150; //set speed such that rocket explods in frame, found by experimenting + uint32_t speed = sqrt((gravity * ((PartSys->maxX >> 2) + hw_random16(PartSys->maxX >> 1))) >> 4); // set speed such that rocket explods in frame PartSys->sources[0].source.vx = min(speed, (uint32_t)127); - PartSys->sources[0].source.ttl = 400; - PartSys->sources[0].source.collide = false; // exhaust does not collide, also used to check if direction reversed - PartSys->sources[0].sat = 40; // low saturation exhaust + PartSys->sources[0].source.ttl = 4000; + PartSys->sources[0].sat = 30; // low saturation exhaust PartSys->sources[0].size = 0; // default size + PartSys->sources[0].source.reversegrav = false ; // normal gravity - if(SEGENV.aux0) { //inverted rockets launch from end + if(SEGENV.aux0) { // inverted rockets launch from end PartSys->sources[0].source.reversegrav = true; - PartSys->sources[0].source.x = PartSys->maxX; //start from top - PartSys->sources[0].source.vx = -PartSys->sources[0].source.vx; //revert direction + PartSys->sources[0].source.x = PartSys->maxX; // start from top + PartSys->sources[0].source.vx = -PartSys->sources[0].source.vx; // revert direction } } } - else { //rocket is launched - int32_t rocketgravity = -(2 + (SEGMENT.speed>>4)); //-8 + else { // rocket is launched + int32_t rocketgravity = -gravity; int32_t speed = PartSys->sources[0].source.vx; - if(SEGENV.aux0) { //negative speed rocket + if(SEGENV.aux0) { // negative speed rocket rocketgravity = -rocketgravity; speed = -speed; } PartSys->applyForce(&PartSys->sources[0].source, rocketgravity, &forcecounter[0]); PartSys->particleMoveUpdate(PartSys->sources[0].source); + PartSys->particleMoveUpdate(PartSys->sources[0].source); // increase speed by calling the move function twice, also ages twice + uint32_t rocketheight = SEGENV.aux0 ? PartSys->maxX - PartSys->sources[0].source.x : PartSys->sources[0].source.x; - if(speed < 0 && PartSys->sources[0].source.collide == false) { //speed has reversed and not in 'explosion mode' - PartSys->sources[0].source.ttl = 75 - (SEGMENT.speed >> 2); //alive for a few more frames - PartSys->sources[0].source.collide = true; //set 'explosion mode' - } + if(speed < 0 && PartSys->sources[0].source.ttl > 50) // reached apogee + PartSys->sources[0].source.ttl = min((uint32_t)50, rocketheight >> (PS_P_RADIUS_SHIFT_1D + 3)); // alive for a few more frames - if(PartSys->sources[0].source.ttl == 0) { //explode + if(PartSys->sources[0].source.ttl < 2) { // explode PartSys->sources[0].source.perpetual = 1; // set standby state - PartSys->sources[0].var = 10 + (SEGMENT.intensity >> 2); - PartSys->sources[0].v = 0; //TODO can make global if this never changes + PartSys->sources[0].var = 5 + ((((PartSys->maxX >> 1) + rocketheight) * (200 + SEGMENT.intensity)) / (PartSys->maxX << 2)); // set explosion particle speed PartSys->sources[0].minLife = 60; - PartSys->sources[0].maxLife = 150; - PartSys->sources[0].source.ttl = 100 + hw_random16(256 - SEGMENT.intensity); // standby time til next launch - PartSys->sources[0].sat = 7 + (SEGMENT.custom3 << 3); //color saturation - PartSys->sources[0].size = hw_random16(255); // random particle size in explosion - uint32_t explosionsize = 10 + hw_random16(SEGMENT.intensity >> 2, SEGMENT.intensity); - for(uint32_t e = 0; e < explosionsize; e++) { //emit explosion particles + PartSys->sources[0].maxLife = 130; + PartSys->sources[0].source.ttl = 100 + hw_random16(64 - (SEGMENT.speed >> 2)); // standby time til next launch + PartSys->sources[0].sat = 7 + (SEGMENT.custom3 << 3); //color saturation TODO: replace saturation with something more useful? + PartSys->sources[0].size = hw_random16(64); // random particle size in explosion + uint32_t explosionsize = 8 + (PartSys->maxXpixel >> 2) + (PartSys->sources[0].source.x >> (PS_P_RADIUS_SHIFT_1D - 1)); + explosionsize += hw_random16((explosionsize * SEGMENT.intensity) >> 8); + Serial.println("Explosion size: " + String(explosionsize)); + for(uint32_t e = 0; e < explosionsize; e++) { // emit explosion particles if(SEGMENT.check2) PartSys->sources[0].source.hue = hw_random16(); //random color for each particle - PartSys->sprayEmit(PartSys->sources[0]); //emit a particle + PartSys->sprayEmit(PartSys->sources[0]); // emit a particle } - PartSys->sources[0].source.x = -500; //set out of frame until relaunch + //!!! DEBUG, remove: + //PartSys->sources[0].source.x = -500; // set out of frame until relaunch + // for(unsigned i = 0; i < PartSys->usedParticles; i++) { // TODO: this can probably be removed now, was a bug patch + // Serial.println("particle " + String(i) + " ttl: " + String(PartSys->particles[i].ttl) + " x: " + String(PartSys->particles[i].x >> 5) + " vx: " + String(PartSys->particles[i].vx) + " sat: " + String(PartSys->advPartProps[i].sat) + " size: " + String(PartSys->advPartProps[i].size)); + // } } } - if(SEGMENT.call & 0x01) //every second frame - PartSys->sprayEmit(PartSys->sources[0]); //emit a particle + if((SEGMENT.call & 0x01) == 0 && PartSys->sources[0].source.perpetual == false) // every second frame and not in standby + PartSys->sprayEmit(PartSys->sources[0]); // emit exhaust particle + if((SEGMENT.call & 0x03) == 0) // every fourth frame + PartSys->applyFriction(1); // apply friction to all particles PartSys->update(); // update and render return FRAMETIME; } -static const char _data_FX_MODE_PS_FIREWORKS1D[] PROGMEM = "PS Fireworks 1D@Gravity,Explosion,Firing side,Blur/Overlay,Saturation,Gravity,Colorful,Smooth;,!;!;1;pal=0,sx=150,ix=150,c1=220,c2=30,c3=21,o2=1"; +static const char _data_FX_MODE_PS_FIREWORKS1D[] PROGMEM = "PS Fireworks 1D@Gravity,Explosion,Firing side,Blur,Saturation,Gravity,Colorful,Smooth;,!;!;1;pal=0,sx=150,ix=150,c1=220,c2=30,c3=21,o2=1"; /* Particle based Sparkle effect @@ -9549,7 +9565,7 @@ uint16_t mode_particleSparkler(void) { return FRAMETIME; } -static const char _data_FX_MODE_PS_SPARKLER[] PROGMEM = "PS Sparkler@Speed,!,Saturation,Blur/Overlay,Sparklers,Direction,Wrap/Bounce,Smooth;,!;!;1;pal=0,sx=50,ix=200,c1=0,c2=0,c3=0,o1=1,o2=1"; +static const char _data_FX_MODE_PS_SPARKLER[] PROGMEM = "PS Sparkler@Speed,!,Saturation,Blur,Sparklers,Direction,Wrap/Bounce,Smooth;,!;!;1;pal=0,sx=50,ix=200,c1=0,c2=0,c3=0,o1=1,o2=1"; /* Particle based Hourglass, particles falling at defined intervals Uses palette for particle color @@ -9673,7 +9689,7 @@ uint16_t mode_particleHourglass(void) { return FRAMETIME; } -static const char _data_FX_MODE_PS_HOURGLASS[] PROGMEM = "PS Hourglass@Speed,!,Color,Blur/Overlay,Gravity,Colorflip,Auto Reset,Fast Reset;,!;!;1;pal=34,sx=245,ix=200,c1=140,c2=80,c3=4,o1=1,o2=1,o3=1"; +static const char _data_FX_MODE_PS_HOURGLASS[] PROGMEM = "PS Hourglass@Speed,!,Color,Blur,Gravity,Colorflip,Auto Reset,Fast Reset;,!;!;1;pal=34,sx=245,ix=200,c1=140,c2=80,c3=4,o1=1,o2=1,o3=1"; /* Particle based Spray effect (like a volcano, possible replacement for popcorn) @@ -9725,7 +9741,7 @@ uint16_t mode_particle1Dspray(void) { return FRAMETIME; } -static const char _data_FX_MODE_PS_1DSPRAY[] PROGMEM = "PS 1D Spray@!,!,Position,Blur/Overlay,Gravity,Color by Age,Bounce,Color by Position;,!;!;1;pal=35,sx=200,ix=220,c1=4,c2=0,c3=28,o1=1,o2=1"; +static const char _data_FX_MODE_PS_1DSPRAY[] PROGMEM = "PS 1D Spray@!,!,Position,Blur,Gravity,Color by Age,Bounce,Color by Position;,!;!;1;pal=35,sx=200,ix=220,c1=4,c2=0,c3=28,o1=1,o2=1"; /* Particle based balance: particles move back and forth (1D pendent to 2D particle box) Uses palette for particle color @@ -9796,7 +9812,7 @@ uint16_t mode_particleBalance(void) { PartSys->update(); // update and render return FRAMETIME; } -static const char _data_FX_MODE_PS_BALANCE[] PROGMEM = "PS 1D Balance@!,!,Collisions,Blur/Overlay,Tilt,Color by Position,Wrap/Bounce,Random;,!;!;1;pal=18,sx=100,ix=40,c1=200,c2=0,c3=5,o1=1"; +static const char _data_FX_MODE_PS_BALANCE[] PROGMEM = "PS 1D Balance@!,!,Collisions,Blur,Tilt,Color by Position,Wrap/Bounce,Random;,!;!;1;pal=18,sx=100,ix=40,c1=200,c2=0,c3=5,o1=1"; /* Particle based Chase effect @@ -9905,7 +9921,7 @@ uint16_t mode_particleChase(void) { PartSys->update(); // update and render return FRAMETIME; } -static const char _data_FX_MODE_PS_CHASE[] PROGMEM = "PS Chase@Speed,Density,Size,Hue,Blur/Overlay,Pride,Color Waves,Color by Position;,!;!;1;pal=11,sx=50,ix=100,c2=5,c3=0"; +static const char _data_FX_MODE_PS_CHASE[] PROGMEM = "PS Chase@Speed,Density,Size,Hue,Blur,Pride,Color Waves,Color by Position;,!;!;1;pal=11,sx=50,ix=100,c2=5,c3=0"; /* Particle Fireworks Starburst replacement (smoother rendering, more settings) Uses palette for particle color @@ -9966,7 +9982,7 @@ uint16_t mode_particleStarburst(void) { PartSys->update(); // update and render return FRAMETIME; } -static const char _data_FX_MODE_PS_STARBURST[] PROGMEM = "PS Starburst@Chance,Fragments,Fragment Size,Blur/Overlay,Cooling,Gravity,Colorful,Push;,!;!;1;pal=52,sx=150,ix=150,c1=120,c2=0,c3=21"; +static const char _data_FX_MODE_PS_STARBURST[] PROGMEM = "PS Starburst@Chance,Fragments,Fragment Size,Blur,Cooling,Gravity,Colorful,Push;,!;!;1;pal=52,sx=150,ix=150,c1=120,c2=0,c3=21"; /* Particle based 1D GEQ effect, each frequency bin gets an emitter, distributed over the strip @@ -10045,7 +10061,7 @@ uint16_t mode_particle1DGEQ(void) { return FRAMETIME; } -static const char _data_FX_MODE_PS_1D_GEQ[] PROGMEM = "PS 1D GEQ@Speed,!,Size,Blur/Overlay,,,,;,!;!;1f;pal=0,sx=50,ix=200,c1=0,c2=0,c3=0,o1=1,o2=1"; +static const char _data_FX_MODE_PS_1D_GEQ[] PROGMEM = "PS 1D GEQ@Speed,!,Size,Blur,,,,;,!;!;1f;pal=0,sx=50,ix=200,c1=0,c2=0,c3=0,o1=1,o2=1"; /* Particle based Fire effect Uses palette for particle color @@ -10110,7 +10126,7 @@ uint16_t mode_particleFire1D(void) { return FRAMETIME; } -static const char _data_FX_MODE_PS_FIRE1D[] PROGMEM = "PS Fire 1D@!,!,Cooling,Blur/Overlay;,!;!;1;pal=35,sx=100,ix=50,c1=80,c2=100,c3=28,o1=1,o2=1"; +static const char _data_FX_MODE_PS_FIRE1D[] PROGMEM = "PS Fire 1D@!,!,Cooling,Blur;,!;!;1;pal=35,sx=100,ix=50,c1=80,c2=100,c3=28,o1=1,o2=1"; #endif // WLED_DISABLE_PARTICLESYSTEM1D ////////////////////////////////////////////////////////////////////////////////////////// diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index b718865781..16bbf24cec 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -1083,7 +1083,7 @@ uint32_t calculateNumberOfParticles2D(uint32_t pixels, bool isadvanced, bool siz #else uint32_t particlelimit = ESP32_MAXPARTICLES; // maximum number of paticles allowed (based on two segments of 32x32 and 40k effect ram) #endif - numberofParticles = max((uint32_t)4, min(numberofParticles, particlelimit)); + numberofParticles = max((uint32_t)4, min(numberofParticles, particlelimit)); // limit to 4 - particlelimit if (isadvanced) // advanced property array needs ram, reduce number of particles to use the same amount numberofParticles = (numberofParticles * sizeof(PSparticle)) / (sizeof(PSparticle) + sizeof(PSadvancedParticle)); if (sizecontrol) // advanced property array needs ram, reduce number of particles @@ -1390,11 +1390,11 @@ void ParticleSystem1D::particleMoveUpdate(PSparticle1D &part, PSsettings1D *opti if (options->killoutofbounds) { bool killthis = true; if (options->useGravity) { // if gravity is used, only kill below 'floor level' - if (part.reversegrav) { // skip at x = 0 - if (newX < 0) + if (part.reversegrav) { // skip at x = 0, do not skip far out of bounds + if (newX < 0 || newX > maxX << 2) killthis = false; - } else { // skip at x = max - if (newX > 0) + } else { // skip at x = max, do not skip far out of bounds + if (newX > 0 && newX < maxX << 2) killthis = false; } } @@ -1481,10 +1481,6 @@ void ParticleSystem1D::ParticleSys_render() { if (framebuffer) { // handle buffer blurring or clearing bool bufferNeedsUpdate = (!pmem->inTransition || pmem->inTransition == effectID); // not a transition; or new FX: update buffer (blur, or clear) - - if(pmem->inTransition == effectID) Serial.print(" new FX "); - else Serial.print(" old FX "); - if(bufferNeedsUpdate) { if (globalBlur > 0 || globalSmear > 0) { // blurring active: if not a transition or is newFX, read data from segment before blurring (old FX can render to it afterwards) // Serial.println(" blurring: " + String(globalBlur)); @@ -1785,10 +1781,11 @@ uint32_t calculateNumberOfParticles1D(uint32_t fraction, bool isadvanced) { #else uint32_t particlelimit = ESP32_MAXPARTICLES_1D; // maximum number of paticles allowed #endif - numberofParticles = max((uint32_t)1, min(numberofParticles, particlelimit)); + numberofParticles = min(numberofParticles, particlelimit); // limit to particlelimit if (isadvanced) // advanced property array needs ram, reduce number of particles to use the same amount numberofParticles = (numberofParticles * sizeof(PSparticle1D)) / (sizeof(PSparticle1D) + sizeof(PSadvancedParticle1D)); numberofParticles = (numberofParticles * (fraction + 1)) >> 8; // calculate fraction of particles + numberofParticles = numberofParticles < 20 ? 20 : numberofParticles; // 20 minimum //make sure it is a multiple of 4 for proper memory alignment (easier than using padding bytes) numberofParticles = ((numberofParticles+3) >> 2) << 2; // TODO: with a separate particle buffer, this is unnecessary return numberofParticles; @@ -1802,7 +1799,7 @@ uint32_t calculateNumberOfSources1D(uint32_t requestedsources) { #else int numberofSources = max(1, min((int)requestedsources, ESP32_MAXSOURCES_1D)); // limit to 1 - 32 #endif - // make sure it is a multiple of 4 for proper memory alignment + // make sure it is a multiple of 4 for proper memory alignment (so minimum is acutally 4) numberofSources = ((numberofSources+3) >> 2) << 2; return numberofSources; } diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index fa8079a48e..c0de1b982c 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -246,7 +246,7 @@ bool allocateParticleSystemMemory2D(uint32_t numparticles, uint32_t numsources, // particle dimensions (subpixel division) #define PS_P_RADIUS_1D 32 // subpixel size, each pixel is divided by this for particle movement, if this value is changed, also change the shift defines (next two lines) #define PS_P_HALFRADIUS_1D (PS_P_RADIUS_1D >> 1) -#define PS_P_RADIUS_SHIFT_1D 5 +#define PS_P_RADIUS_SHIFT_1D 5 // 1 << PS_P_RADIUS_SHIFT = PS_P_RADIUS #define PS_P_SURFACE_1D 5 // shift: 2^PS_P_SURFACE = PS_P_RADIUS_1D #define PS_P_MINHARDRADIUS_1D 32 // minimum hard surface radius #define PS_P_MINSURFACEHARDNESS_1D 50 // minimum hardness used in collision impulse calculation From 73dee4764cb64c1904fd2c4069e75e82127fdc09 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Thu, 2 Jan 2025 09:25:18 +0100 Subject: [PATCH 155/219] bugfix in PS Chase, increased brightness of 1D fireworks --- wled00/FX.cpp | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index f8001d9020..0d81c9ef05 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -9432,8 +9432,8 @@ uint16_t mode_particleFireworks1D(void) { PartSys->sources[0].source.perpetual = 0; //flag abused for rocket state PartSys->sources[0].source.hue = hw_random16(); PartSys->sources[0].var = 10; - PartSys->sources[0].minLife = 10; - PartSys->sources[0].maxLife = 30; + PartSys->sources[0].minLife = 100; + PartSys->sources[0].maxLife = 300; PartSys->sources[0].source.x = 0; // start from bottom uint32_t speed = sqrt((gravity * ((PartSys->maxX >> 2) + hw_random16(PartSys->maxX >> 1))) >> 4); // set speed such that rocket explods in frame PartSys->sources[0].source.vx = min(speed, (uint32_t)127); @@ -9467,14 +9467,13 @@ uint16_t mode_particleFireworks1D(void) { if(PartSys->sources[0].source.ttl < 2) { // explode PartSys->sources[0].source.perpetual = 1; // set standby state PartSys->sources[0].var = 5 + ((((PartSys->maxX >> 1) + rocketheight) * (200 + SEGMENT.intensity)) / (PartSys->maxX << 2)); // set explosion particle speed - PartSys->sources[0].minLife = 60; - PartSys->sources[0].maxLife = 130; + PartSys->sources[0].minLife = 600; + PartSys->sources[0].maxLife = 1300; PartSys->sources[0].source.ttl = 100 + hw_random16(64 - (SEGMENT.speed >> 2)); // standby time til next launch PartSys->sources[0].sat = 7 + (SEGMENT.custom3 << 3); //color saturation TODO: replace saturation with something more useful? PartSys->sources[0].size = hw_random16(64); // random particle size in explosion uint32_t explosionsize = 8 + (PartSys->maxXpixel >> 2) + (PartSys->sources[0].source.x >> (PS_P_RADIUS_SHIFT_1D - 1)); explosionsize += hw_random16((explosionsize * SEGMENT.intensity) >> 8); - Serial.println("Explosion size: " + String(explosionsize)); for(uint32_t e = 0; e < explosionsize; e++) { // emit explosion particles if(SEGMENT.check2) PartSys->sources[0].source.hue = hw_random16(); //random color for each particle @@ -9493,6 +9492,12 @@ uint16_t mode_particleFireworks1D(void) { PartSys->applyFriction(1); // apply friction to all particles PartSys->update(); // update and render + + for(uint32_t i = 0; i < PartSys->usedParticles; i++) { + if(PartSys->particles[i].ttl > 10) PartSys->particles[i].ttl -= 10; //ttl is linked to brightness, this allows to use higher brightness but still a short spark lifespan + else PartSys->particles[i].ttl = 0; + } + return FRAMETIME; } static const char _data_FX_MODE_PS_FIREWORKS1D[] PROGMEM = "PS Fireworks 1D@Gravity,Explosion,Firing side,Blur,Saturation,Gravity,Colorful,Smooth;,!;!;1;pal=0,sx=150,ix=150,c1=220,c2=30,c3=21,o2=1"; @@ -9737,7 +9742,6 @@ uint16_t mode_particle1Dspray(void) { PartSys->particles[i].reversegrav = PartSys->sources[0].source.reversegrav; // update gravity direction } PartSys->update(); // update and render - Serial.println("used by FX: " + String(PartSys->usedParticles)); return FRAMETIME; } @@ -9822,7 +9826,7 @@ by DedeHai (Damian Schneider) uint16_t mode_particleChase(void) { ParticleSystem1D *PartSys = NULL; - uint32_t i; + int32_t i; if (SEGMENT.call == 0) { // initialization if (!initParticleSystem1D(PartSys, 1, 255, 3, true)) // init @@ -9851,8 +9855,6 @@ uint16_t mode_particleChase(void) { // uint32_t remainder = PartSys->maxX - ((PartSys->usedParticles) * SEGENV.step); // unused spacing, distribute this for(i = 0; i < PartSys->usedParticles; i++) { PartSys->advPartProps[i].sat = 255; - PartSys->particles[i].ttl = 300; - PartSys->particles[i].perpetual = true; //PartSys->particles[i].x = (i - 1) * SEGENV.step + (((i + 1) * remainder) / PartSys->usedParticles); // distribute evenly PartSys->particles[i].x = (i - 1) * SEGENV.step; // distribute evenly (starts out of frame for i=0) PartSys->particles[i].vx = SEGMENT.speed >> 1; @@ -9865,7 +9867,7 @@ uint16_t mode_particleChase(void) { SEGENV.aux0 = settingssum; } - uint32_t huestep = (((uint32_t)SEGMENT.custom2 << 19) / PartSys->usedParticles) >> 16; // hue increment + int32_t huestep = (((uint32_t)SEGMENT.custom2 << 19) / PartSys->usedParticles) >> 16; // hue increment if(SEGMENT.check1) { // pride rainbow colors //TODO: orignal FX also changes movement speed @@ -9904,17 +9906,17 @@ uint16_t mode_particleChase(void) { PartSys->particles[i].hue -= decrement; } } - // wrap around (cannot use particle system wrap if distributing colors manually, it also wraps rendering which does not look good) - for(i = 0; i < PartSys->usedParticles; i++) { + for(i = PartSys->usedParticles - 1; i >= 0; i--) { // check from the back, last particle wraps first, multiple particles can overrun per frame if(PartSys->particles[i].x > PartSys->maxX + PS_P_RADIUS_1D + PartSys->advPartProps[i].size) { // wrap it around uint32_t nextindex = (i + 1) % PartSys->usedParticles; - PartSys->particles[i].x = PartSys->particles[nextindex].x - SEGENV.step; + PartSys->particles[i].x = PartSys->particles[nextindex].x - (int)SEGENV.step; if(SEGMENT.custom2 < 255) PartSys->particles[i].hue = PartSys->particles[nextindex].hue - huestep; else PartSys->particles[i].hue = hw_random16(); } + PartSys->particles[i].ttl = 300; // reset ttl, cannot use perpetual because memmanager can change pointer at any time } PartSys->setParticleSize(SEGMENT.custom1); // if custom1 == 0 this sets rendering size to one pixel From 78362354870dd082a5cd79ae61bdce461fd3f20e Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Thu, 2 Jan 2025 10:05:39 +0100 Subject: [PATCH 156/219] replaced sin16/cos16 with new versions --- wled00/FX.cpp | 10 +++++----- wled00/FXparticleSystem.cpp | 15 ++++++++------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 0d81c9ef05..a18db3ff9a 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -7888,7 +7888,7 @@ uint16_t mode_particlefireworks(void) { for (i = 0; i < emitparticles; i++) { if (circularexplosion) { - int32_t sineMod = 0xEFFF + sin16((uint16_t)(((angle * frequency) >> 4) + baseangle)); // shifted to positive values + int32_t sineMod = 0xEFFF + sin16_t((uint16_t)(((angle * frequency) >> 4) + baseangle)); // shifted to positive values currentspeed = (speed/2 + ((sineMod * speed) >> 16)) >> 1; // sine modulation on speed based on emit angle PartSys->angleEmit(PartSys->sources[j], angle, currentspeed); // note: compiler warnings can be ignored, variables are set just above counter++; @@ -8287,8 +8287,8 @@ uint16_t mode_particlebox(void) { ygravity = (ygravity * SEGMENT.custom1) / 128; } else { // go in a circle - xgravity = ((int32_t)(SEGMENT.custom1) * cos16(SEGENV.aux0 << 8)) / 0xFFFF; - ygravity = ((int32_t)(SEGMENT.custom1) * sin16(SEGENV.aux0 << 8)) / 0xFFFF; + xgravity = ((int32_t)(SEGMENT.custom1) * cos16_t(SEGENV.aux0 << 8)) / 0xFFFF; + ygravity = ((int32_t)(SEGMENT.custom1) * sin16_t(SEGENV.aux0 << 8)) / 0xFFFF; } if (SEGMENT.check3) { // sloshing, y force is always downwards if(ygravity > 0) @@ -8892,8 +8892,8 @@ uint16_t mode_particleghostrider(void) { SEGENV.aux0 += (int32_t)SEGENV.step; // step is angle increment uint16_t emitangle = SEGENV.aux0 + 32767; // +180° int32_t speed = map(SEGMENT.speed, 0, 255, 12, 64); - PartSys->sources[0].source.vx = ((int32_t)cos16(SEGENV.aux0) * speed) / (int32_t)32767; - PartSys->sources[0].source.vy = ((int32_t)sin16(SEGENV.aux0) * speed) / (int32_t)32767; + PartSys->sources[0].source.vx = ((int32_t)cos16_t(SEGENV.aux0) * speed) / (int32_t)32767; + PartSys->sources[0].source.vy = ((int32_t)sin16_t(SEGENV.aux0) * speed) / (int32_t)32767; PartSys->sources[0].source.ttl = 500; // source never dies (note: setting 'perpetual' is not needed if replenished each frame) PartSys->particleMoveUpdate(PartSys->sources[0].source, &ghostsettings); // set head (steal one of the particles) diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index 16bbf24cec..89169fc3aa 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -232,8 +232,8 @@ void ParticleSystem2D::flameEmit(PSsource &emitter) { // Emits a particle at given angle and speed, angle is from 0-65535 (=0-360deg), speed is also affected by emitter->var // angle = 0 means in positive x-direction (i.e. to the right) int32_t ParticleSystem2D::angleEmit(PSsource &emitter, uint16_t angle, int32_t speed, uint32_t amount) { - emitter.vx = ((int32_t)cos16(angle) * speed) / (int32_t)32600; // cos16() and sin16() return signed 16bit, division should be 32767 but 32600 gives slightly better rounding - emitter.vy = ((int32_t)sin16(angle) * speed) / (int32_t)32600; // note: cannot use bit shifts as bit shifting is asymmetrical for positive and negative numbers and this needs to be accurate! + emitter.vx = ((int32_t)cos16_t(angle) * speed) / (int32_t)32600; // cos16_t() and sin16_t() return signed 16bit, division should be 32767 but 32600 gives slightly better rounding + emitter.vy = ((int32_t)sin16_t(angle) * speed) / (int32_t)32600; // note: cannot use bit shifts as bit shifting is asymmetrical for positive and negative numbers and this needs to be accurate! return sprayEmit(emitter, amount); } @@ -473,9 +473,8 @@ void ParticleSystem2D::applyForce(int8_t xforce, int8_t yforce) { // angle is from 0-65535 (=0-360deg) angle = 0 means in positive x-direction (i.e. to the right) // force is in 3.4 fixed point notation so force=16 means apply v+1 each frame (useful force range is +/- 127) void ParticleSystem2D::applyAngleForce(PSparticle *part, int8_t force, uint16_t angle, uint8_t *counter) { - int8_t xforce = ((int32_t)force * cos16(angle)) / 32767; // force is +/- 127 - int8_t yforce = ((int32_t)force * sin16(angle)) / 32767; // note: cannot use bit shifts as bit shifting is asymmetrical for positive and negative numbers and this needs to be accurate! - // note: sin16 is 10% faster than sin8() on ESP32 but on ESP8266 it is 9% slower + int8_t xforce = ((int32_t)force * cos16_t(angle)) / 32767; // force is +/- 127 + int8_t yforce = ((int32_t)force * sin16_t(angle)) / 32767; // note: cannot use bit shifts as bit shifting is asymmetrical for positive and negative numbers and this needs to be accurate! applyForce(part, xforce, yforce, counter); } @@ -488,8 +487,8 @@ void ParticleSystem2D::applyAngleForce(uint16_t particleindex, int8_t force, uin // apply a force in angular direction to all particles // angle is from 0-65535 (=0-360deg) angle = 0 means in positive x-direction (i.e. to the right) void ParticleSystem2D::applyAngleForce(int8_t force, uint16_t angle) { - int8_t xforce = ((int32_t)force * cos16(angle)) / 32767; // force is +/- 127 - int8_t yforce = ((int32_t)force * sin16(angle)) / 32767; // note: cannot use bit shifts as bit shifting is asymmetrical for positive and negative numbers and this needs to be accurate! + int8_t xforce = ((int32_t)force * cos16_t(angle)) / 32767; // force is +/- 127 + int8_t yforce = ((int32_t)force * sin16_t(angle)) / 32767; // note: cannot use bit shifts as bit shifting is asymmetrical for positive and negative numbers and this needs to be accurate! applyForce(xforce, yforce); } @@ -2038,6 +2037,7 @@ void* particleMemoryManager(const uint32_t requestedParticles, size_t structSize uint32_t newAvailable = 0; if (SEGMENT.mode == effectID) { // new effect ID -> function was called from new FX newAvailable = (maxParticles * progress) >> 16; // update total particles available to this PS (newAvailable is guaranteed to be smaller than maxParticles) + if(newAvailable < 2) newAvailable = 2; // give 2 particle minimum (some FX may crash with less as they do i+1 access) if(maxParticles / numParticlesUsed > 3 && newAvailable > numParticlesUsed) newAvailable = numParticlesUsed; // limit to number of particles used for FX using a small amount, do not move the pointer anymore (will be set to base in final handover) uint32_t bufferoffset = (maxParticles - 1) - newAvailable; // offset to new effect particles if(bufferoffset < maxParticles) // safety check @@ -2051,6 +2051,7 @@ void* particleMemoryManager(const uint32_t requestedParticles, size_t structSize progress = 0xFFFFU - progress; // inverted transition progress newAvailable = ((maxParticles * progress) >> 16); // result is guaranteed to be smaller than maxParticles if(newAvailable > 0) newAvailable--; // -1 to avoid overlapping memory in 1D<->2D transitions + if(newAvailable < 2) newAvailable = 2; // give 2 particle minimum (some FX may crash with less as they do i+1 access) // note: buffer pointer stays the same, number of available particles is reduced } availableToPS = newAvailable; From e556aef1f55c73230bc770ab0d699883cd393659 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Thu, 2 Jan 2025 14:14:12 +0100 Subject: [PATCH 157/219] bugfixes --- wled00/FX.cpp | 78 ++++++++++++++++++------------------- wled00/FXparticleSystem.cpp | 15 +++---- 2 files changed, 45 insertions(+), 48 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index a18db3ff9a..7b9a1e2028 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -8994,7 +8994,7 @@ static const char _data_FX_MODE_PARTICLEBLOBS[] PROGMEM = "PS Blobs@Speed,Blobs, /* * Particle Fractal - * particles move, then split to form a fractal tree EXPERIMENTAL! + * particles move, then split to form a fractal tree EXPERIMENTAL and non working! * by DedeHai (Damian Schneider) */ @@ -9328,7 +9328,7 @@ uint16_t mode_particleDancingShadows(void) { //random color, random type uint32_t type = hw_random16(SPOT_TYPES_COUNT); int8_t speed = 2 + hw_random16(2 + (SEGMENT.speed >> 1)) + (SEGMENT.speed >> 4); - uint32_t width = hw_random16(1, 10); + int32_t width = hw_random16(1, 10); uint32_t ttl = 300; //ttl is particle brightness (below perpetual is set so it does not age, i.e. ttl stays at this value) int32_t position; //choose random start position, left and right from the segment @@ -9341,36 +9341,38 @@ uint16_t mode_particleDancingShadows(void) { PartSys->sources[0].v = speed; //emitted particle speed PartSys->sources[0].source.hue = hw_random8(); //random spotlight color - for (uint32_t i = 0; i < width; i++) { - switch (type) { - case SPOT_TYPE_SOLID: - //nothing to do - break; - - case SPOT_TYPE_GRADIENT: - ttl = cubicwave8(map(i, 0, width - 1, 0, 255)); - ttl = ttl*ttl >> 8; //make gradient more pronounced - break; - - case SPOT_TYPE_2X_GRADIENT: - ttl = cubicwave8(2 * map(i, 0, width - 1, 0, 255)); - ttl = ttl*ttl >> 8; - break; - - case SPOT_TYPE_2X_DOT: - if(i > 0) position++; //skip one pixel - i++; - break; - - case SPOT_TYPE_3X_DOT: - if(i > 0) position += 2; //skip two pixels - i+=2; - break; - - case SPOT_TYPE_4X_DOT: - if(i > 0) position += 3; //skip three pixels - i+=3; - break; + for (int32_t i = 0; i < width; i++) { + if(width > 1) { + switch (type) { + case SPOT_TYPE_SOLID: + //nothing to do + break; + + case SPOT_TYPE_GRADIENT: + ttl = cubicwave8(map(i, 0, width - 1, 0, 255)); + ttl = ttl*ttl >> 8; //make gradient more pronounced + break; + + case SPOT_TYPE_2X_GRADIENT: + ttl = cubicwave8(2 * map(i, 0, width - 1, 0, 255)); + ttl = ttl*ttl >> 8; + break; + + case SPOT_TYPE_2X_DOT: + if(i > 0) position++; //skip one pixel + i++; + break; + + case SPOT_TYPE_3X_DOT: + if(i > 0) position += 2; //skip two pixels + i+=2; + break; + + case SPOT_TYPE_4X_DOT: + if(i > 0) position += 3; //skip three pixels + i+=3; + break; + } } //emit particle //set the particle source position: @@ -9826,8 +9828,6 @@ by DedeHai (Damian Schneider) uint16_t mode_particleChase(void) { ParticleSystem1D *PartSys = NULL; - int32_t i; - if (SEGMENT.call == 0) { // initialization if (!initParticleSystem1D(PartSys, 1, 255, 3, true)) // init return mode_static(); // allocation failed or is single pixel @@ -9850,10 +9850,10 @@ uint16_t mode_particleChase(void) { uint32_t settingssum = SEGMENT.speed + SEGMENT.intensity + SEGMENT.custom1 + SEGMENT.custom2 + SEGMENT.check1 + SEGMENT.check2 + SEGMENT.check3 + PartSys->getUsedParticles(); // note: progress is used to enforce update during transitions if(SEGENV.aux0 != settingssum) { //settings changed changed, update //PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 1, min(PartSys->maxX / (32 + (SEGMENT.custom1 >> 1)), int32_t(PartSys->usedParticles)))); //depends on intensity and particle size (custom1) !!! TODO: is this fine now? - PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 1, 255 / (1 + (SEGMENT.custom1 >> 6)))); //depends on intensity and particle size (custom1) + PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 30, 255 / (1 + (SEGMENT.custom1 >> 6)))); //depends on intensity and particle size (custom1) SEGENV.step = (PartSys->maxX + (PS_P_RADIUS_1D << 5)) / PartSys->usedParticles; //spacing between particles // uint32_t remainder = PartSys->maxX - ((PartSys->usedParticles) * SEGENV.step); // unused spacing, distribute this - for(i = 0; i < PartSys->usedParticles; i++) { + for(int32_t i = 0; i < (int32_t)PartSys->usedParticles; i++) { PartSys->advPartProps[i].sat = 255; //PartSys->particles[i].x = (i - 1) * SEGENV.step + (((i + 1) * remainder) / PartSys->usedParticles); // distribute evenly PartSys->particles[i].x = (i - 1) * SEGENV.step; // distribute evenly (starts out of frame for i=0) @@ -9893,7 +9893,7 @@ uint16_t mode_particleChase(void) { if(SEGMENT.call % (255 / (1 + (SEGMENT.speed >> 2))) == 0) sizechange = *sizedir; - for(i = 0; i < PartSys->usedParticles; i++) { + for(uint32_t i = 0; i < PartSys->usedParticles; i++) { // PartSys->particles[i].hue = *basehue + (i * (SEGENV.aux1)) / PartSys->usedParticles; // gradient distribution PartSys->advPartProps[i].size += sizechange; } @@ -9902,12 +9902,12 @@ uint16_t mode_particleChase(void) { int32_t decrement = 2; if(SEGMENT.check1) decrement = 1; //slower hue change in pride mode - for(i = 0; i < PartSys->usedParticles; i++) { + for(uint32_t i = 0; i < PartSys->usedParticles; i++) { PartSys->particles[i].hue -= decrement; } } // wrap around (cannot use particle system wrap if distributing colors manually, it also wraps rendering which does not look good) - for(i = PartSys->usedParticles - 1; i >= 0; i--) { // check from the back, last particle wraps first, multiple particles can overrun per frame + for(int32_t i = (int32_t)PartSys->usedParticles - 1; i >= 0; i--) { // check from the back, last particle wraps first, multiple particles can overrun per frame if(PartSys->particles[i].x > PartSys->maxX + PS_P_RADIUS_1D + PartSys->advPartProps[i].size) { // wrap it around uint32_t nextindex = (i + 1) % PartSys->usedParticles; PartSys->particles[i].x = PartSys->particles[nextindex].x - (int)SEGENV.step; diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index 89169fc3aa..48f37cfbcb 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -686,7 +686,7 @@ void ParticleSystem2D::ParticleSys_render(bool firemode, uint32_t fireintensity) SEGMENT.blur(globalSmear, true); } // transfer framebuffer to segment if available - if (framebuffer && pmem->inTransition != effectID) { // not in transition or is old FX + if (pmem->inTransition != effectID) { // not in transition or is old FX transferBuffer(maxXpixel + 1, maxYpixel + 1, useAdditiveTransfer); } } @@ -1890,7 +1890,7 @@ static int32_t limitSpeed(int32_t speed) { // check if particle is out of bounds and wrap it around if required, returns false if out of bounds static bool checkBoundsAndWrap(int32_t &position, const int32_t max, const int32_t particleradius, bool wrap) { - if ((uint32_t)position > max) { // check if particle reached an edge, cast to uint32_t to save negative checking + if ((uint32_t)position > (uint32_t)max) { // check if particle reached an edge, cast to uint32_t to save negative checking (max is always positive) if (wrap) { position = position % (max + 1); // note: cannot optimize modulo, particles can be far out of bounds when wrap is enabled if (position < 0) @@ -1990,7 +1990,6 @@ void* particleMemoryManager(const uint32_t requestedParticles, size_t structSize if (requestedParticles) { // request for a new buffer, this is an init call PSPRINTLN("Buffer exists, request for particles: " + String(requestedParticles)); pmem->transferParticles = true; // set flag to transfer particles - availableToPS = 0; // start out with zero particles, transition below will initialize and transfer them uint32_t requestsize = structSize * requestedParticles; // required buffer size if (requestsize > pmem->buffersize) { // request is larger than buffer, try to extend it if (Segment::getUsedSegmentData() + requestsize - pmem->buffersize <= MAX_SEGMENT_DATA) { // enough memory available to extend buffer @@ -2000,20 +1999,19 @@ void* particleMemoryManager(const uint32_t requestedParticles, size_t structSize deallocatePSmemory(pmem->particleMemPointer, pmem->buffersize); // free old memory pmem->particleMemPointer = buffer; // set new buffer pmem->buffersize = requestsize; // update buffer size - } // if buffer was not extended, the old, smaller buffer is used + } else return nullptr; // no memory available } } if (pmem->watchdog == 1) // if a PS already exists during particle request, it kicked the watchdog in last frame, servicePSmem() adds 1 afterwards -> PS to PS transition pmem->inTransition = effectID; // save the ID of the new effect (required to determine blur amount in rendering function) - return pmem->particleMemPointer; // return the available buffer on init call TODO: maybe split this into two functions, one for init and one for get? + return pmem->particleMemPointer; // return the available buffer on init call } pmem->watchdog = 0; // kick watchdog buffer = pmem->particleMemPointer; // buffer is already allocated } else { // if the id was not found create a buffer and add an element to the list - availableToPS = 0; // new PS starts with zero particles, they are transferred in the next call PSPRINTLN("New particle buffer request: " + String(requestedParticles)); uint32_t requestsize = structSize * requestedParticles; // required buffer size buffer = allocatePSmemory(requestsize, false); // allocate new memory @@ -2144,7 +2142,7 @@ void particleHandover(void *buffer, size_t structSize, int32_t numToTransfer) { // update number of particles to use, limit to allocated (= particles allocated by the calling system) in case more are available in the buffer void updateUsedParticles(const uint32_t allocated, const uint32_t available, const uint8_t percentage, uint32_t &used) { uint32_t wantsToUse = (allocated * ((uint32_t)percentage + 1)) >> 8; - used = min(available, wantsToUse); // limit to available particles + used = max((uint32_t)2, min(available, wantsToUse)); // limit to available particles, use a minimum of 2 } // get the pointer to the particle memory for the segment @@ -2213,9 +2211,8 @@ void servicePSmem() { // transfer the frame buffer to the segment and handle transitional rendering (both FX render to the same buffer so they mix) void transferBuffer(uint32_t width, uint32_t height, bool useAdditiveTransfer) { - PSPRINT(" xfer buf "); if(!framebuffer) return; // no buffer, nothing to transfer - + PSPRINT(" xfer buf "); #ifndef WLED_DISABLE_MODE_BLEND bool tempBlend = SEGMENT.getmodeBlend(); if (pmem->inTransition) From af5b96f998764343df27b0e41766f1df2d9b2807 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Thu, 2 Jan 2025 19:23:15 +0100 Subject: [PATCH 158/219] fixed hourglass init, rearranged PS FX init --- wled00/FX.cpp | 26 ++++++++++++-------------- wled00/FX.h | 2 +- wled00/FXparticleSystem.h | 8 ++++---- 3 files changed, 17 insertions(+), 19 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 7b9a1e2028..cb21c03b3f 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -9608,9 +9608,10 @@ uint16_t mode_particleHourglass(void) { positionoffset = PS_P_RADIUS_1D / 2; uint32_t colormode = SEGMENT.custom1 >> 5; // 0-7 - if(SEGMENT.intensity != SEGENV.step) { //initialize - *basehue = hw_random16(); //choose new random color - SEGENV.step = SEGMENT.intensity; + if((SEGMENT.intensity | (PartSys->getAvailableParticles() << 8)) != SEGENV.step) { //initialize, getAvailableParticles changes while in FX transition + if(PartSys->getAvailableParticles() == SEGENV.step >> 8) // only intensity slider changed or first call + *basehue = hw_random16(); //choose new random color + SEGENV.step = SEGMENT.intensity | (PartSys->getAvailableParticles() << 8); for(uint32_t i = 0; i < PartSys->usedParticles; i++) { PartSys->particles[i].reversegrav = true; *direction = 0; @@ -9847,9 +9848,8 @@ uint16_t mode_particleChase(void) { //uint8_t* basehue = (PartSys->PSdataEnd + 2); //assign data pointer //PartSys->setBounce(SEGMENT.check2); - uint32_t settingssum = SEGMENT.speed + SEGMENT.intensity + SEGMENT.custom1 + SEGMENT.custom2 + SEGMENT.check1 + SEGMENT.check2 + SEGMENT.check3 + PartSys->getUsedParticles(); // note: progress is used to enforce update during transitions + uint32_t settingssum = SEGMENT.speed + SEGMENT.intensity + SEGMENT.custom1 + SEGMENT.custom2 + SEGMENT.check1 + SEGMENT.check2 + SEGMENT.check3 + PartSys->getAvailableParticles(); // note: getAvailableParticles is used to enforce update during transitions if(SEGENV.aux0 != settingssum) { //settings changed changed, update - //PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 1, min(PartSys->maxX / (32 + (SEGMENT.custom1 >> 1)), int32_t(PartSys->usedParticles)))); //depends on intensity and particle size (custom1) !!! TODO: is this fine now? PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 30, 255 / (1 + (SEGMENT.custom1 >> 6)))); //depends on intensity and particle size (custom1) SEGENV.step = (PartSys->maxX + (PS_P_RADIUS_1D << 5)) / PartSys->usedParticles; //spacing between particles // uint32_t remainder = PartSys->maxX - ((PartSys->usedParticles) * SEGENV.step); // unused spacing, distribute this @@ -10383,25 +10383,23 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_2DAKEMI, &mode_2DAkemi, _data_FX_MODE_2DAKEMI); // audio #ifndef WLED_DISABLE_PARTICLESYSTEM2D - addEffect(FX_MODE_PARTICLEVORTEX, &mode_particlevortex, _data_FX_MODE_PARTICLEVORTEX); - addEffect(FX_MODE_PARTICLEFIREWORKS, &mode_particlefireworks, _data_FX_MODE_PARTICLEFIREWORKS); 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_PARTICLEVORTEX, &mode_particlevortex, _data_FX_MODE_PARTICLEVORTEX); + addEffect(FX_MODE_PARTICLEPERLIN, &mode_particleperlin, _data_FX_MODE_PARTICLEPERLIN); addEffect(FX_MODE_PARTICLEPIT, &mode_particlepit, _data_FX_MODE_PARTICLEPIT); - addEffect(FX_MODE_PARTICLEWATERFALL, &mode_particlewaterfall, _data_FX_MODE_PARTICLEWATERFALL); addEffect(FX_MODE_PARTICLEBOX, &mode_particlebox, _data_FX_MODE_PARTICLEBOX); - addEffect(FX_MODE_PARTICLEPERLIN, &mode_particleperlin, _data_FX_MODE_PARTICLEPERLIN); - addEffect(FX_MODE_PARTICLEIMPACT, &mode_particleimpact, _data_FX_MODE_PARTICLEIMPACT); addEffect(FX_MODE_PARTICLEATTRACTOR, &mode_particleattractor, _data_FX_MODE_PARTICLEATTRACTOR); + addEffect(FX_MODE_PARTICLEIMPACT, &mode_particleimpact, _data_FX_MODE_PARTICLEIMPACT); + addEffect(FX_MODE_PARTICLEWATERFALL, &mode_particlewaterfall, _data_FX_MODE_PARTICLEWATERFALL); 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_PARTICLECIRCULARGEQ); addEffect(FX_MODE_PARTICLEGHOSTRIDER, &mode_particleghostrider, _data_FX_MODE_PARTICLEGHOSTRIDER); addEffect(FX_MODE_PARTICLEBLOBS, &mode_particleblobs, _data_FX_MODE_PARTICLEBLOBS); - addEffect(FX_MODE_PARTICLECENTERGEQ, &mode_particlecenterGEQ, _data_FX_MODE_PARTICLECIRCULARGEQ); // addEffect(FX_MODE_PSFRACTAL, &mode_particlefractal, _data_FX_MODE_PARTICLEFRACTAL); - #endif // WLED_DISABLE_PARTICLESYSTEM2D - #endif // WLED_DISABLE_2D #ifndef WLED_DISABLE_PARTICLESYSTEM1D @@ -10416,8 +10414,8 @@ addEffect(FX_MODE_PSBALANCE, &mode_particleBalance, _data_FX_MODE_PS_BALANCE); addEffect(FX_MODE_PSCHASE, &mode_particleChase, _data_FX_MODE_PS_CHASE); addEffect(FX_MODE_PSSTARBURST, &mode_particleStarburst, _data_FX_MODE_PS_STARBURST); addEffect(FX_MODE_PS1DGEQ, &mode_particle1DGEQ, _data_FX_MODE_PS_1D_GEQ); +Serial.println("Adding PSFIRE1D"); addEffect(FX_MODE_PSFIRE1D, &mode_particleFire1D, _data_FX_MODE_PS_FIRE1D); - #endif // WLED_DISABLE_PARTICLESYSTEM1D } diff --git a/wled00/FX.h b/wled00/FX.h index 30a9124885..14bdf975ed 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -350,7 +350,7 @@ extern byte realtimeMode; // used in getMappedPixelIndex() #define FX_MODE_PS1DGEQ 212 #define FX_MODE_PSFIRE1D 213 //#define FX_MODE_PSFRACTAL 214 -#define MODE_COUNT 215 +#define MODE_COUNT 214 typedef enum mapping1D2D { M12_Pixels = 0, diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index c0de1b982c..59491d9f43 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -158,7 +158,7 @@ class ParticleSystem2D { void lineAttractor(uint16_t particleindex, PSparticle *attractorcenter, uint16_t attractorangle, uint8_t strength); // set options void setUsedParticles(uint8_t percentage); // set the percentage of particles used in the system, 255=100% - inline uint32_t getUsedParticles(void) { return usedParticles; } + inline uint32_t getAvailableParticles(void) { return availableParticles; } // available particles in the buffer, use this to check if buffer changed during FX init 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 setWallRoughness(uint8_t roughness); // wall roughness randomizes wall collisions @@ -184,7 +184,7 @@ class ParticleSystem2D { int32_t maxX, maxY; // particle system size i.e. width-1 / height-1 in subpixels, Note: all "max" variables must be signed to compare to coordinates (which are signed) int32_t maxXpixel, maxYpixel; // last physical pixel that can be drawn to (FX can read this to read segment size if required), equal to width-1 / height-1 uint32_t numSources; // number of sources - uint32_t usedParticles; // number of particles used in animation, is relative to 'availableParticles' + uint32_t usedParticles; // number of particles used in animation, is relative to 'numParticles' //note: some variables are 32bit for speed and code size at the cost of ram private: @@ -320,7 +320,7 @@ class ParticleSystem1D void applyFriction(int32_t coefficient); // apply friction to all used particles // set options void setUsedParticles(uint8_t percentage); // set the percentage of particles used in the system, 255=100% - inline uint32_t getUsedParticles(void) { return usedParticles; } + inline uint32_t getAvailableParticles(void) { return availableParticles; } // available particles in the buffer, use this to check if buffer changed during FX init void setWallHardness(uint8_t hardness); // hardness for bouncing on the wall if bounceXY is set void setSize(uint16_t x); //set particle system size (= strip length) void setWrap(bool enable); @@ -343,7 +343,7 @@ class ParticleSystem1D int32_t maxX; // particle system size i.e. width-1, Note: all "max" variables must be signed to compare to coordinates (which are signed) int32_t maxXpixel; // last physical pixel that can be drawn to (FX can read this to read segment size if required), equal to width-1 uint32_t numSources; // number of sources - uint32_t usedParticles; // number of particles used in animation, is relative to 'availableParticles' + uint32_t usedParticles; // number of particles used in animation, is relative to 'numParticles' private: //rendering functions From 245ed2c959f9fd88b629404eefd8b56da310d058 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Fri, 3 Jan 2025 08:23:13 +0100 Subject: [PATCH 159/219] speed optimization - moved out of bounds calculation to proper spot - removed repeated allocation attempts if initial buffer alloc fails --- wled00/FXparticleSystem.cpp | 57 +++++++++++++++++++------------------ wled00/FXparticleSystem.h | 2 +- 2 files changed, 31 insertions(+), 28 deletions(-) diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index 48f37cfbcb..c98244c48b 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -975,7 +975,7 @@ void ParticleSystem2D::collideParticles(PSparticle *particle1, PSparticle *parti void ParticleSystem2D::updateSystem(void) { PSPRINTLN("updateSystem2D"); setMatrixSize(SEGMENT.vWidth(), SEGMENT.vHeight()); - updateRenderingBuffer(SEGMENT.vWidth() * SEGMENT.vHeight(), true); // update or create rendering buffer (segment size can change at any time) + updateRenderingBuffer(SEGMENT.vWidth() * SEGMENT.vHeight(), true, false); // update rendering buffer (segment size can change at any time) updatePSpointers(advPartProps != nullptr, advPartSize != nullptr); // update pointers to PS data, also updates availableParticles setUsedParticles(fractionOfParticlesUsed); // update used particles based on percentage (can change during transitions, execute each frame for code simplicity) if (partMemList.size() == 1) // if number of vector elements is one, this is the only system @@ -1135,8 +1135,9 @@ bool initParticleSystem2D(ParticleSystem2D *&PartSys, uint32_t requestedsources, uint32_t cols = SEGMENT.virtualWidth(); uint32_t rows = SEGMENT.virtualHeight(); uint32_t pixels = cols * rows; + updateRenderingBuffer(SEGMENT.vWidth() * SEGMENT.vHeight(), true, true); // update or create rendering buffer if(advanced) - updateRenderingBuffer(100, false); // allocate a 10x10 buffer for rendering advanced particles + updateRenderingBuffer(100, false, true); // allocate a 10x10 buffer for rendering advanced particles uint32_t numparticles = calculateNumberOfParticles2D(pixels, advanced, sizecontrol); PSPRINT(" segmentsize:" + String(cols) + " " + String(rows)); @@ -1562,20 +1563,6 @@ void ParticleSystem1D::renderParticle(const uint32_t particleindex, const uint32 x--; // shift by a full pixel here, this is skipped above to not do -1 and then +1 pixco[0] = x; // left pixel - // 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 (wrap) // wrap x to the other side if required - pixco[0] = maxXpixel; - else - pxlisinframe[0] = false; // pixel is out of matrix boundaries, do not render - } - else if (pixco[1] > (int32_t)maxXpixel) { // right pixel, only has to be checkt if left pixel did not overflow - if (wrap) // wrap y to the other side if required - pixco[1] = 0; - else - pxlisinframe[1] = false; - } - //calculate the brightness values for both pixels using linear interpolation (note: in standard rendering out of frame pixels could be skipped but if checks add more clock cycles over all) pxlbrightness[0] = (((int32_t)PS_P_RADIUS_1D - dx) * brightness) >> PS_P_SURFACE_1D; pxlbrightness[1] = (dx * brightness) >> PS_P_SURFACE_1D; @@ -1627,6 +1614,19 @@ void ParticleSystem1D::renderParticle(const uint32_t particleindex, const uint32 } } else { // standard rendering (2 pixels per particle) + // check if any pixels are out of frame + if (x < 0) { // left pixels out of frame + if (wrap) // wrap x to the other side if required + pixco[0] = maxXpixel; + else + pxlisinframe[0] = false; // pixel is out of matrix boundaries, do not render + } + else if (pixco[1] > (int32_t)maxXpixel) { // right pixel, only has to be checkt if left pixel did not overflow + if (wrap) // wrap y to the other side if required + pixco[1] = 0; + else + pxlisinframe[1] = false; + } for(uint32_t i = 0; i < 2; i++) { if (pxlisinframe[i]) { if (framebuffer) @@ -1732,7 +1732,7 @@ void ParticleSystem1D::collideParticles(PSparticle1D *particle1, PSparticle1D *p // note: do not access the PS class in FX befor running this function (or it messes up SEGENV.data) void ParticleSystem1D::updateSystem(void) { setSize(SEGMENT.vLength()); // update size - updateRenderingBuffer(SEGMENT.vLength(), true); // update or create rendering buffer (segment size can change at any time) + updateRenderingBuffer(SEGMENT.vLength(), true, false); // update rendering buffer (segment size can change at any time) updatePSpointers(advPartProps != NULL); setUsedParticles(fractionOfParticlesUsed); // update used particles based on percentage (can change during transitions, execute each frame for code simplicity) if (partMemList.size() == 1) // if number of vector elements is one, this is the only system @@ -1821,8 +1821,9 @@ bool allocateParticleSystemMemory1D(uint32_t numparticles, uint32_t numsources, // note: percentofparticles is in uint8_t, for example 191 means 75%, (deafaults to 255 or 100% meaning one particle per pixel), can be more than 100% (but not recommended, can cause out of memory) bool initParticleSystem1D(ParticleSystem1D *&PartSys, uint32_t requestedsources, uint8_t fractionofparticles, uint32_t additionalbytes, bool advanced) { if (SEGLEN == 1) return false; // single pixel not supported + updateRenderingBuffer(SEGMENT.vLength(), true, true); // update/create frame rendering buffer if(advanced) - updateRenderingBuffer(10, false); // buffer for advanced particles, fixed size + updateRenderingBuffer(10, false, true); // buffer for advanced particles, fixed size uint32_t numparticles = calculateNumberOfParticles1D(fractionofparticles, advanced); uint32_t numsources = calculateNumberOfSources1D(requestedsources); if (!allocateParticleSystemMemory1D(numparticles, numsources, advanced, additionalbytes)) { @@ -2157,18 +2158,20 @@ partMem* getPartMem(void) { // TODO: maybe there is a better/faster way than usi } // function to update the framebuffer and renderbuffer -void updateRenderingBuffer(uint32_t requiredpixels, bool isFramebuffer) { +void updateRenderingBuffer(uint32_t requiredpixels, bool isFramebuffer, bool initialize) { PSPRINTLN("updateRenderingBuffer"); - CRGB** targetBuffer = isFramebuffer ? &framebuffer : &renderbuffer; // pointer to target buffer uint16_t& targetBufferSize = isFramebuffer ? frameBufferSize : renderBufferSize; // corresponding buffer size if(targetBufferSize < requiredpixels) { // check current buffer size - if(*targetBuffer) // buffer exists, free it - deallocatePSmemory((void*)(*targetBuffer), targetBufferSize * sizeof(CRGB)); - *targetBuffer = reinterpret_cast(allocatePSmemory(requiredpixels * sizeof(CRGB), false)); - if(*targetBuffer) - targetBufferSize = requiredpixels; - else - targetBufferSize = 0; + CRGB** targetBuffer = isFramebuffer ? &framebuffer : &renderbuffer; // pointer to target buffer + if(*targetBuffer || initialize) { // update only if initilizing or if buffer exists (prevents repeatet allocation attempts if initial alloc failed) + if(*targetBuffer) // buffer exists, free it + deallocatePSmemory((void*)(*targetBuffer), targetBufferSize * sizeof(CRGB)); + *targetBuffer = reinterpret_cast(allocatePSmemory(requiredpixels * sizeof(CRGB), false)); + if(*targetBuffer) + targetBufferSize = requiredpixels; + else + targetBufferSize = 0; + } } } diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index 59491d9f43..d780a1c742 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -41,7 +41,7 @@ void* particleMemoryManager(const uint32_t requestedParticles, size_t structSize void particleHandover(void *buffer, size_t structSize, int32_t numParticles); void updateUsedParticles(const uint32_t allocated, const uint32_t available, const uint8_t percentage, uint32_t &used); partMem* getPartMem(void); // returns pointer to memory struct for current segment or nullptr -void updateRenderingBuffer(uint32_t requiredsize, bool isFramebuffer); // allocate CRGB rendering buffer, update size if needed +void updateRenderingBuffer(uint32_t requiredpixels, bool isFramebuffer, bool initialize); // allocate CRGB rendering buffer, update size if needed void transferBuffer(uint32_t width, uint32_t height, bool useAdditiveTransfer = false); // transfer the buffer to the segment (supports 1D and 2D) void servicePSmem(); // increments watchdog, frees memory if idle too long #endif From 553e7db068f5fb3a7d6445c07c2fdf698a757fdc Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sun, 5 Jan 2025 15:43:11 +0100 Subject: [PATCH 160/219] Added Sonic Stream FX, some cleanup, bugfix in emitter --- wled00/FX.cpp | 232 ++++++++++++++++++++++++++++-------- wled00/FX.h | 5 +- wled00/FXparticleSystem.cpp | 1 + 3 files changed, 185 insertions(+), 53 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index cb21c03b3f..80bf42ce9e 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -3147,7 +3147,7 @@ static uint16_t rolling_balls(void) { return FRAMETIME; } -static const char _data_FX_MODE_ROLLINGBALLS[] PROGMEM = "Rolling Balls@!,# of balls,,,,Collisions,Overlay,Trails;!,!,!;!;1;m12=1"; //bar +static const char _data_FX_MODE_ROLLINGBALLS[] PROGMEM = "Rolling Balls@!,# of balls,,,,Collide,Overlay,Trails;!,!,!;!;1;m12=1"; //bar #endif //DISABLE_1D_PS_REPLACEMENTS /* @@ -7916,7 +7916,7 @@ uint16_t mode_particlefireworks(void) { return FRAMETIME; } #undef NUMBEROFSOURCES -static const char _data_FX_MODE_PARTICLEFIREWORKS[] PROGMEM = "PS Fireworks@Launches,Explosion Size,Fuse,Blur,Gravity,Cylinder,Ground,;;!;2;pal=11,sx=100,ix=50,c1=84,c2=0,c3=12,o1=0,o2=0,o3=0"; +static const char _data_FX_MODE_PARTICLEFIREWORKS[] PROGMEM = "PS Fireworks@Launches,Explosion Size,Fuse,Blur,Gravity,Cylinder,Ground,;;!;2;pal=11,sx=100,ix=50,c1=84,c2=0,c3=12"; /* * Particle Volcano @@ -7989,7 +7989,7 @@ uint16_t mode_particlevolcano(void) { return FRAMETIME; } #undef NUMBEROFSOURCES -static const char _data_FX_MODE_PARTICLEVOLCANO[] PROGMEM = "PS Volcano@Speed,Intensity,Move,Bounce,Spread,Color by Age,Walls,Collisions;;!;2;pal=35,sx=100,ix=190,c1=0,c2=160,c3=6,o1=1,o2=0,o3=0"; +static const char _data_FX_MODE_PARTICLEVOLCANO[] PROGMEM = "PS Volcano@Speed,Intensity,Move,Bounce,Spread,AgeColor,Walls,Collide;;!;2;pal=35,sx=100,ix=190,c1=0,c2=160,c3=6,o1=1"; /* * Particle Fire @@ -8152,7 +8152,7 @@ uint16_t mode_particlepit(void) { return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEPIT[] PROGMEM = "PS Ballpit@Speed,Intensity,Size,Hardness,Saturation,Cylinder,Walls,Ground;;!;2;pal=11,sx=100,ix=220,c1=120,c2=130,c3=31,o1=0,o2=0,o3=1"; +static const char _data_FX_MODE_PARTICLEPIT[] PROGMEM = "PS Ballpit@Speed,Intensity,Size,Hardness,Saturation,Cylinder,Walls,Ground;;!;2;pal=11,sx=100,ix=220,c1=120,c2=130,c3=31,o3=1"; /* * Particle Waterfall @@ -8224,7 +8224,7 @@ uint16_t mode_particlewaterfall(void) { PartSys->update(); // update and render return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEWATERFALL[] PROGMEM = "PS Waterfall@Speed,Intensity,Variation,Collisions,Position,Cylinder,Walls,Ground;;!;2;pal=9,sx=15,ix=200,c1=32,c2=160,c3=17,o1=0,o2=0,o3=1"; +static const char _data_FX_MODE_PARTICLEWATERFALL[] PROGMEM = "PS Waterfall@Speed,Intensity,Variation,Collide,Position,Cylinder,Walls,Ground;;!;2;pal=9,sx=15,ix=200,c1=32,c2=160,c3=17,o3=1"; /* Particle Box, applies gravity to particles in either a random direction or random but only downwards (sloshing) @@ -8367,7 +8367,7 @@ uint16_t mode_particleperlin(void) { PartSys->update(); // update and render return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEPERLIN[] PROGMEM = "PS Fuzzy Noise@Speed,Particles,Bounce,Friction,Scale,Cylinder,Smear,Collisions;;!;2;pal=64,sx=50,ix=200,c1=130,c2=30,c3=5,o3=1"; +static const char _data_FX_MODE_PARTICLEPERLIN[] PROGMEM = "PS Fuzzy Noise@Speed,Particles,Bounce,Friction,Scale,Cylinder,Smear,Collide;;!;2;pal=64,sx=50,ix=200,c1=130,c2=30,c3=5,o3=1"; /* * Particle smashing down like meteors and exploding as they hit the ground, has many parameters to play with @@ -8484,7 +8484,7 @@ uint16_t mode_particleimpact(void) { return FRAMETIME; } #undef NUMBEROFSOURCES -static const char _data_FX_MODE_PARTICLEIMPACT[] PROGMEM = "PS Impact@Launches,Explosion Size,Explosion Force,Hardness,Blur,Cylinder,Walls,Collisions;;!;2;pal=0,sx=32,ix=85,c1=70,c2=130,c3=0,o3=1"; +static const char _data_FX_MODE_PARTICLEIMPACT[] PROGMEM = "PS Impact@Launches,Size,Force,Hardness,Blur,Cylinder,Walls,Collide;;!;2;pal=0,sx=32,ix=85,c1=70,c2=130,c3=0,o3=1"; /* Particle Attractor, a particle attractor sits in the matrix center, a spray bounces around and seeds particles @@ -8590,9 +8590,9 @@ uint16_t mode_particleattractor(void) { return FRAMETIME; } #ifdef USERMOD_AUDIOREACTIVE -static const char _data_FX_MODE_PARTICLEATTRACTOR[] PROGMEM = "PS Attractor@Mass,Particles,Particle Size,Collisions,Friction,Color by Age,Move,Disable AR;;!;2v;pal=9,sx=100,ix=82,c1=0,c2=0,"; +static const char _data_FX_MODE_PARTICLEATTRACTOR[] PROGMEM = "PS Attractor@Mass,Particles,Size,Collide,Friction,AgeColor,Move,Disable AR;;!;2v;pal=9,sx=100,ix=82,c1=0,c2=0,"; #else -static const char _data_FX_MODE_PARTICLEATTRACTOR[] PROGMEM = "PS Attractor@Mass,Particles,Particle Size,Collisions,Friction,Color by Age,Move,Swallow;;!;2;pal=9,sx=100,ix=82,c1=0,c2=0"; +static const char _data_FX_MODE_PARTICLEATTRACTOR[] PROGMEM = "PS Attractor@Mass,Particles,Size,Collide,Friction,AgeColor,Move,Swallow;;!;2;pal=9,sx=100,ix=82,c1=0,c2=0"; #endif /* @@ -8679,7 +8679,7 @@ uint16_t mode_particlespray(void) { PartSys->update(); // update and render return FRAMETIME; } -static const char _data_FX_MODE_PARTICLESPRAY[] PROGMEM = "PS Spray@Speed,!,Left/Right,Up/Down,Angle,Gravity,Cylinder/Square,Collisions;;!;2v;pal=0,sx=150,ix=150,c1=220,c2=30,c3=21"; +static const char _data_FX_MODE_PARTICLESPRAY[] PROGMEM = "PS Spray@Speed,!,Left/Right,Up/Down,Angle,Gravity,Cylinder/Square,Collide;;!;2v;pal=0,sx=150,ix=150,c1=220,c2=30,c3=21"; /* @@ -8757,7 +8757,7 @@ uint16_t mode_particleGEQ(void) { return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEGEQ[] PROGMEM = "PS 2D GEQ@Speed,Intensity,Diverge,Bounce,Gravity,Cylinder,Walls,Floor;;!;2f;pal=0,sx=155,ix=200,c1=0,c2=128"; +static const char _data_FX_MODE_PARTICLEGEQ[] PROGMEM = "PS 2D GEQ@Speed,Intensity,Diverge,Bounce,Gravity,Cylinder,Walls,Floor;;!;2f;pal=0,sx=155,ix=200,c1=0"; /* * Particle rotating GEQ (unfinished, basically works but needs more fine-tuning) @@ -8833,7 +8833,7 @@ uint16_t mode_particlecenterGEQ(void) { PartSys->update(); // update and render return FRAMETIME; } -static const char _data_FX_MODE_PARTICLECIRCULARGEQ[] PROGMEM = "PS Center GEQ@Speed,Intensity,Rotation Speed,Color Change,Nozzle Size,,Direction;;!;2f;pal=13,ix=180,c1=0,c2=0,c3=8"; +static const char _data_FX_MODE_PARTICLECIRCULARGEQ[] PROGMEM = "PS Center GEQ@Speed,Intensity,Rotation Speed,Color Change,Nozzle,,Direction;;!;2f;pal=13,ix=180,c1=0,c2=0,c3=8"; /* Particle replacement of Ghost Rider by DedeHai (Damian Schneider), original by stepko adapted by Blaz Kristan (AKA blazoncek) @@ -8913,7 +8913,7 @@ uint16_t mode_particleghostrider(void) { PartSys->update(); // update and render return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEGHOSTRIDER[] PROGMEM = "PS Ghost Rider@Speed,Spiral,Blur,Color Cycle,Spread,Color by age,Walls;;!;2;pal=1,sx=70,ix=0,c1=220,c2=30,c3=21,o1=1"; +static const char _data_FX_MODE_PARTICLEGHOSTRIDER[] PROGMEM = "PS Ghost Rider@Speed,Spiral,Blur,Color Cycle,Spread,AgeColor,Walls;;!;2;pal=1,sx=70,ix=0,c1=220,c2=30,c3=21,o1=1"; /* PS Blobs: large particles bouncing around, changing size and form @@ -9266,7 +9266,7 @@ uint16_t mode_particleBouncingBalls(void) { PartSys->update(); // update and render return FRAMETIME; } -static const char _data_FX_MODE_PSBOUNCINGBALLS[] PROGMEM = "PS Bouncing Balls@Speed,!,Size,Blur,Gravity,Collide,Rolling,Color by Position;,!;!;1;pal=0,sx=100,ix=85,c1=30,c2=0,c3=8"; +static const char _data_FX_MODE_PSBOUNCINGBALLS[] PROGMEM = "PS Bouncing Balls@Speed,!,Size,Blur,Gravity,Collide,Rolling,Position Color;,!;!;1;pal=0,sx=100,ix=85,c1=30,c2=0,c3=8"; /* Particle Replacement for original Dancing Shadows: @@ -9387,7 +9387,7 @@ uint16_t mode_particleDancingShadows(void) { return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEDANCINGSHADOWS[] PROGMEM = "PS Dancing Shadows@Speed,!,Blur,Color Cycle,,Smear,Color by position,Smooth;,!;!;1;sx=100,ix=180,c1=0,c2=0"; +static const char _data_FX_MODE_PARTICLEDANCINGSHADOWS[] PROGMEM = "PS Dancing Shadows@Speed,!,Blur,Color Cycle,,Smear,Position Color,Smooth;,!;!;1;sx=100,ix=180,c1=0,c2=0"; /* Particle Fireworks 1D replacement @@ -9697,7 +9697,7 @@ uint16_t mode_particleHourglass(void) { return FRAMETIME; } -static const char _data_FX_MODE_PS_HOURGLASS[] PROGMEM = "PS Hourglass@Speed,!,Color,Blur,Gravity,Colorflip,Auto Reset,Fast Reset;,!;!;1;pal=34,sx=245,ix=200,c1=140,c2=80,c3=4,o1=1,o2=1,o3=1"; +static const char _data_FX_MODE_PS_HOURGLASS[] PROGMEM = "PS Hourglass@Speed,!,Color,Blur,Gravity,Colorflip,Start,Fast Reset;,!;!;1;pal=34,sx=245,ix=200,c1=140,c2=80,c3=4,o1=1,o2=1,o3=1"; /* Particle based Spray effect (like a volcano, possible replacement for popcorn) @@ -9748,7 +9748,7 @@ uint16_t mode_particle1Dspray(void) { return FRAMETIME; } -static const char _data_FX_MODE_PS_1DSPRAY[] PROGMEM = "PS 1D Spray@!,!,Position,Blur,Gravity,Color by Age,Bounce,Color by Position;,!;!;1;pal=35,sx=200,ix=220,c1=4,c2=0,c3=28,o1=1,o2=1"; +static const char _data_FX_MODE_PS_1DSPRAY[] PROGMEM = "PS 1D Spray@!,!,Position,Blur,Gravity,AgeColor,Bounce,Position Color;,!;!;1;pal=35,sx=200,ix=220,c1=4,c2=0,c3=28,o1=1,o2=1"; /* Particle based balance: particles move back and forth (1D pendent to 2D particle box) Uses palette for particle color @@ -9819,7 +9819,7 @@ uint16_t mode_particleBalance(void) { PartSys->update(); // update and render return FRAMETIME; } -static const char _data_FX_MODE_PS_BALANCE[] PROGMEM = "PS 1D Balance@!,!,Collisions,Blur,Tilt,Color by Position,Wrap/Bounce,Random;,!;!;1;pal=18,sx=100,ix=40,c1=200,c2=0,c3=5,o1=1"; +static const char _data_FX_MODE_PS_BALANCE[] PROGMEM = "PS 1D Balance@!,!,Collide,Blur,Tilt,Position Color,Wrap,Random;,!;!;1;pal=18,sx=100,ix=40,c1=200,c2=0,c3=5,o1=1"; /* Particle based Chase effect @@ -9845,21 +9845,20 @@ uint16_t mode_particleChase(void) { PartSys->updateSystem(); // update system properties (dimensions and data pointers) PartSys->setColorByPosition(SEGMENT.check3); PartSys->setMotionBlur(SEGMENT.custom3 << 3); // anable motion blur - //uint8_t* basehue = (PartSys->PSdataEnd + 2); //assign data pointer + // uint8_t* basehue = (PartSys->PSdataEnd + 2); //assign data pointer - //PartSys->setBounce(SEGMENT.check2); uint32_t settingssum = SEGMENT.speed + SEGMENT.intensity + SEGMENT.custom1 + SEGMENT.custom2 + SEGMENT.check1 + SEGMENT.check2 + SEGMENT.check3 + PartSys->getAvailableParticles(); // note: getAvailableParticles is used to enforce update during transitions - if(SEGENV.aux0 != settingssum) { //settings changed changed, update - PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 30, 255 / (1 + (SEGMENT.custom1 >> 6)))); //depends on intensity and particle size (custom1) - SEGENV.step = (PartSys->maxX + (PS_P_RADIUS_1D << 5)) / PartSys->usedParticles; //spacing between particles - // uint32_t remainder = PartSys->maxX - ((PartSys->usedParticles) * SEGENV.step); // unused spacing, distribute this - for(int32_t i = 0; i < (int32_t)PartSys->usedParticles; i++) { + if (SEGENV.aux0 != settingssum) { // settings changed changed, update + uint32_t numParticles = map(SEGMENT.intensity, 0, 255, 2, 255 / (1 + (SEGMENT.custom1 >> 6))); // depends on intensity and particle size (custom1) + if (numParticles == 0) numParticles = 1; // minimum 1 particle + PartSys->setUsedParticles(numParticles); + SEGENV.step = (PartSys->maxX + (PS_P_RADIUS_1D << 5)) / PartSys->usedParticles; // spacing between particles + for (int32_t i = 0; i < (int32_t)PartSys->usedParticles; i++) { PartSys->advPartProps[i].sat = 255; - //PartSys->particles[i].x = (i - 1) * SEGENV.step + (((i + 1) * remainder) / PartSys->usedParticles); // distribute evenly PartSys->particles[i].x = (i - 1) * SEGENV.step; // distribute evenly (starts out of frame for i=0) PartSys->particles[i].vx = SEGMENT.speed >> 1; PartSys->advPartProps[i].size = SEGMENT.custom1; - if(SEGMENT.custom2 < 255) + if (SEGMENT.custom2 < 255) PartSys->particles[i].hue = (i * (SEGMENT.custom2 << 3)) / PartSys->usedParticles; // gradient distribution else PartSys->particles[i].hue = hw_random16(); @@ -9869,49 +9868,49 @@ uint16_t mode_particleChase(void) { int32_t huestep = (((uint32_t)SEGMENT.custom2 << 19) / PartSys->usedParticles) >> 16; // hue increment - if(SEGMENT.check1) { // pride rainbow colors - //TODO: orignal FX also changes movement speed + if (SEGMENT.check1) { // pride rainbow colors + // TODO: orignal FX also changes movement speed // also the color change is too fast - int8_t* huedir = reinterpret_cast(PartSys->PSdataEnd); //assign data pointer - int8_t* sizedir = reinterpret_cast(PartSys->PSdataEnd + 1); //assign data pointer + int8_t* huedir = reinterpret_cast(PartSys->PSdataEnd); // assign data pointer + int8_t* sizedir = reinterpret_cast(PartSys->PSdataEnd + 1); int32_t sizechange = 0; - if(PartSys->advPartProps[0].size >= 254) + if (PartSys->advPartProps[0].size >= 254) *sizedir = -1; - else if(PartSys->advPartProps[0].size <= (SEGMENT.custom1 >> 2)) + else if (PartSys->advPartProps[0].size <= (SEGMENT.custom1 >> 2)) *sizedir = 1; - if(SEGENV.aux1 > 64) + if (SEGENV.aux1 > 64) *huedir = -1; - else if(SEGENV.aux1 < 1) + else if (SEGENV.aux1 < 1) *huedir = 1; - if(SEGMENT.call % (1024 / (1 + (SEGMENT.speed >> 3))) == 0) + if (SEGMENT.call % (1024 / (1 + (SEGMENT.speed >> 3))) == 0) SEGENV.aux1 += *huedir; huestep = SEGENV.aux1; // changes gradient spread - if(SEGMENT.call % (255 / (1 + (SEGMENT.speed >> 2))) == 0) + if (SEGMENT.call % (255 / (1 + (SEGMENT.speed >> 2))) == 0) sizechange = *sizedir; - for(uint32_t i = 0; i < PartSys->usedParticles; i++) { + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { // PartSys->particles[i].hue = *basehue + (i * (SEGENV.aux1)) / PartSys->usedParticles; // gradient distribution PartSys->advPartProps[i].size += sizechange; } } - if((SEGMENT.check2 || SEGMENT.check1) && SEGMENT.call % (160 / ((SEGMENT.speed >> 3) + 128)) == 0) { // color waves + if ((SEGMENT.check2 || SEGMENT.check1) && SEGMENT.call % (160 / ((SEGMENT.speed >> 3) + 128)) == 0) { // color waves int32_t decrement = 2; - if(SEGMENT.check1) - decrement = 1; //slower hue change in pride mode - for(uint32_t i = 0; i < PartSys->usedParticles; i++) { + if (SEGMENT.check1) + decrement = 1; // slower hue change in pride mode + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { PartSys->particles[i].hue -= decrement; } } // wrap around (cannot use particle system wrap if distributing colors manually, it also wraps rendering which does not look good) - for(int32_t i = (int32_t)PartSys->usedParticles - 1; i >= 0; i--) { // check from the back, last particle wraps first, multiple particles can overrun per frame - if(PartSys->particles[i].x > PartSys->maxX + PS_P_RADIUS_1D + PartSys->advPartProps[i].size) { // wrap it around + for (int32_t i = (int32_t)PartSys->usedParticles - 1; i >= 0; i--) { // check from the back, last particle wraps first, multiple particles can overrun per frame + if (PartSys->particles[i].x > PartSys->maxX + PS_P_RADIUS_1D + PartSys->advPartProps[i].size) { // wrap it around uint32_t nextindex = (i + 1) % PartSys->usedParticles; PartSys->particles[i].x = PartSys->particles[nextindex].x - (int)SEGENV.step; - if(SEGMENT.custom2 < 255) + if (SEGMENT.custom2 < 255) PartSys->particles[i].hue = PartSys->particles[nextindex].hue - huestep; else PartSys->particles[i].hue = hw_random16(); @@ -9923,7 +9922,7 @@ uint16_t mode_particleChase(void) { PartSys->update(); // update and render return FRAMETIME; } -static const char _data_FX_MODE_PS_CHASE[] PROGMEM = "PS Chase@Speed,Density,Size,Hue,Blur,Pride,Color Waves,Color by Position;,!;!;1;pal=11,sx=50,ix=100,c2=5,c3=0"; +static const char _data_FX_MODE_PS_CHASE[] PROGMEM = "PS Chase@!,Density,Size,Hue,Blur,Pride,Color Waves,Position Color;,!;!;1;pal=11,sx=50,ix=100,c2=5,c3=0"; /* Particle Fireworks Starburst replacement (smoother rendering, more settings) Uses palette for particle color @@ -9984,7 +9983,7 @@ uint16_t mode_particleStarburst(void) { PartSys->update(); // update and render return FRAMETIME; } -static const char _data_FX_MODE_PS_STARBURST[] PROGMEM = "PS Starburst@Chance,Fragments,Fragment Size,Blur,Cooling,Gravity,Colorful,Push;,!;!;1;pal=52,sx=150,ix=150,c1=120,c2=0,c3=21"; +static const char _data_FX_MODE_PS_STARBURST[] PROGMEM = "PS Starburst@Chance,Fragments,Size,Blur,Cooling,Gravity,Colorful,Push;,!;!;1;pal=52,sx=150,ix=150,c1=120,c2=0,c3=21"; /* Particle based 1D GEQ effect, each frequency bin gets an emitter, distributed over the strip @@ -10014,7 +10013,7 @@ uint16_t mode_particle1DGEQ(void) { uint32_t spacing = PartSys->maxX / numSources; for (i = 0; i < numSources; i++) { PartSys->sources[i].source.hue = i * 16; // hw_random16(); //TODO: make adjustable, maybe even colorcycle? - PartSys->sources[i].var = SEGMENT.speed >> 3; + PartSys->sources[i].var = SEGMENT.speed >> 2; PartSys->sources[i].minLife = 180 + (SEGMENT.intensity >> 1); PartSys->sources[i].maxLife = 240 + SEGMENT.intensity; PartSys->sources[i].sat = 255; @@ -10064,6 +10063,7 @@ uint16_t mode_particle1DGEQ(void) { return FRAMETIME; } static const char _data_FX_MODE_PS_1D_GEQ[] PROGMEM = "PS 1D GEQ@Speed,!,Size,Blur,,,,;,!;!;1f;pal=0,sx=50,ix=200,c1=0,c2=0,c3=0,o1=1,o2=1"; + /* Particle based Fire effect Uses palette for particle color @@ -10089,11 +10089,11 @@ uint16_t mode_particleFire1D(void) { PartSys->setColorByAge(true); uint32_t emitparticles = 1; uint32_t j = hw_random16(); - for (uint i = 0; i < 3; i++) { + for (uint i = 0; i < 3; i++) { // 3 base flames TODO: check if this is ok or needs adjustments if (PartSys->sources[i].source.ttl > 50) PartSys->sources[i].source.ttl -= 10; // TODO: in 2D making the source fade out slow results in much smoother flames, need to check if it can be done the same else - PartSys->sources[i].source.ttl = 100 + hw_random16(200); // base flame + PartSys->sources[i].source.ttl = 100 + hw_random16(200); } for (uint i = 0; i < PartSys->numSources; i++) { j = (j + 1) % PartSys->numSources; @@ -10129,6 +10129,135 @@ uint16_t mode_particleFire1D(void) { return FRAMETIME; } static const char _data_FX_MODE_PS_FIRE1D[] PROGMEM = "PS Fire 1D@!,!,Cooling,Blur;,!;!;1;pal=35,sx=100,ix=50,c1=80,c2=100,c3=28,o1=1,o2=1"; + +/* +Particle based AR effect, swoop particles along the strip with selected frequency loudness +Uses palette for particle color +by DedeHai (Damian Schneider) +*/ + +uint16_t mode_particle1Dsonicstream(void) { + ParticleSystem1D *PartSys = NULL; + + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem1D(PartSys, 1, 255, 0, true)) // init, no additional data needed + return mode_static(); // allocation failed or is single pixel + PartSys->setKillOutOfBounds(true); + PartSys->sources[0].source.x = 0; // at start + //PartSys->sources[1].source.x = PartSys->maxX; // at end + PartSys->sources[0].var = 0;//SEGMENT.custom1 >> 3; + PartSys->sources[0].sat = 255; + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + if (PartSys == NULL) + return mode_static(); // something went wrong, no data! + + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setMotionBlur(20 + SEGMENT.custom2>>1); // anable motion blur + PartSys->setSmearBlur(200); // smooth out the edges + + PartSys->sources[0].v = 5 + SEGMENT.speed >> 2; + + // FFT processing + um_data_t *um_data; + if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) + um_data = simulateSound(SEGMENT.soundSim); // add support for no audio + + uint8_t *fftResult = (uint8_t *)um_data->u_data[2]; // 16 bins with FFT data, log mapped already, each band contains frequency amplitude 0-255 + uint32_t loudness; + uint32_t baseBin = SEGMENT.custom3 >> 1; // 0 - 15 map(SEGMENT.custom3, 0, 31, 0, 14); + + loudness = fftResult[baseBin];// + fftResult[baseBin + 1]; + int mids = sqrt16((int)fftResult[5] + (int)fftResult[6] + (int)fftResult[7] + (int)fftResult[8] + (int)fftResult[9] + (int)fftResult[10]); // average the mids, bin 5 is ~500Hz, bin 10 is ~2kHz (see audio_reactive.h) + if(baseBin > 12) + loudness = loudness << 2; // double loudness for high frequencies (better detecion) + + uint32_t threshold = 150 - (SEGMENT.intensity >> 1); + if(SEGMENT.check2) { // enable low pass filter for dynamic threshold + SEGMENT.step = (SEGMENT.step * 31500 + loudness * (32768 - 31500)) >> 15; // low pass filter for simple beat detection: add average to base threshold + threshold = 20 + (threshold >> 1) + SEGMENT.step; // add average to threshold + } + + // color + uint32_t hueincrement = (SEGMENT.custom1 >> 3); // 0-31 + if(SEGMENT.custom1 < 255) + PartSys->setColorByPosition(false); + else + PartSys->setColorByPosition(true); + + // particle manipulation + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { + if(PartSys->sources[0].source.perpetual == false) { // age faster if not perpetual + if (PartSys->particles[i].ttl > 2) { + PartSys->particles[i].ttl -= 2; //ttl is linked to brightness, this allows to use higher brightness but still a short lifespan + } + else PartSys->particles[i].ttl = 0; + } + if(SEGMENT.check1) // modulate colors by mid frequencies + PartSys->particles[i].hue += (mids * inoise8(PartSys->particles[i].x << 2, SEGMENT.step << 2)) >> 9; // color by perlin noise from mid frequencies + } + + if (loudness > threshold) { + SEGMENT.aux0 += hueincrement; // change color + PartSys->sources[0].minLife = 100 + (((unsigned)SEGMENT.intensity * loudness * loudness) >> 13); + PartSys->sources[0].maxLife = PartSys->sources[0].minLife; + PartSys->sources[0].source.hue = SEGMENT.aux0; + PartSys->sources[0].size = SEGMENT.speed; + if(PartSys->particles[SEGMENT.aux1].x > 3 * PS_P_RADIUS_1D || PartSys->particles[SEGMENT.aux1].ttl == 0) { // only emit if last particle is far enough away or dead + int partindex = PartSys->sprayEmit(PartSys->sources[0]); // emit a particle + if(partindex >= 0) SEGMENT.aux1 = partindex; // track last emitted particle + } + } + else loudness = 0; // required for push mode + + PartSys->update(); // update and render (needs to be done before manipulation for initial particle spacing to be right) + + if(SEGMENT.check3) { // push mode + PartSys->sources[0].source.perpetual = true; // emitted particles dont age + PartSys->applyFriction(1); //slow down particles + int32_t movestep = (((int)SEGMENT.speed + 2) * loudness) >> 10; + if(movestep) { + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { + if (PartSys->particles[i].ttl) { + PartSys->particles[i].x += movestep; // push particles + PartSys->particles[i].vx = 10 + (SEGMENT.speed >> 4) ; // give particles some speed for smooth movement (friction will slow them down) + } + } + } + } + else { + PartSys->sources[0].source.perpetual = false; // emitted particles age + //move all particles (again) to allow faster speeds + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { + if (PartSys->particles[i].vx == 0) + PartSys->particles[i].vx = PartSys->sources[0].v; // move static particles (after disabling push mode) + PartSys->particleMoveUpdate(PartSys->particles[i], nullptr, &PartSys->advPartProps[i]); + } + } + /* + Serial.print(fftResult[0]); Serial.print(" "); + Serial.print(fftResult[1]); Serial.print(" "); + Serial.print(fftResult[2]); Serial.print(" "); + Serial.print(fftResult[3]); Serial.print(" "); + Serial.print(fftResult[4]); Serial.print(" "); + Serial.print(fftResult[5]); Serial.print(" "); + Serial.print(fftResult[6]); Serial.print(" "); + Serial.print(fftResult[7]); Serial.print(" "); + Serial.print(fftResult[8]); Serial.print(" "); + Serial.print(fftResult[9]); Serial.print(" "); + Serial.print(fftResult[10]); Serial.print(" "); + Serial.print(fftResult[11]); Serial.print(" "); + Serial.print(fftResult[12]); Serial.print(" "); + Serial.print(fftResult[13]); Serial.print(" "); + Serial.print(fftResult[14]); Serial.print(" "); + Serial.print(fftResult[15]); Serial.println(" "); +*/ + + return FRAMETIME; +} +static const char _data_FX_MODE_PS_SONICSTREAM[] PROGMEM = "PS Sonic Stream@!,!,Color,Blur,Bin,Mod,Filter,Push;,!;!;1f;c3=0,o2=1"; #endif // WLED_DISABLE_PARTICLESYSTEM1D ////////////////////////////////////////////////////////////////////////////////////////// @@ -10414,8 +10543,9 @@ addEffect(FX_MODE_PSBALANCE, &mode_particleBalance, _data_FX_MODE_PS_BALANCE); addEffect(FX_MODE_PSCHASE, &mode_particleChase, _data_FX_MODE_PS_CHASE); addEffect(FX_MODE_PSSTARBURST, &mode_particleStarburst, _data_FX_MODE_PS_STARBURST); addEffect(FX_MODE_PS1DGEQ, &mode_particle1DGEQ, _data_FX_MODE_PS_1D_GEQ); -Serial.println("Adding PSFIRE1D"); addEffect(FX_MODE_PSFIRE1D, &mode_particleFire1D, _data_FX_MODE_PS_FIRE1D); +addEffect(FX_MODE_PS1DSONICSTREAM, &mode_particle1Dsonicstream, _data_FX_MODE_PS_SONICSTREAM); + #endif // WLED_DISABLE_PARTICLESYSTEM1D } diff --git a/wled00/FX.h b/wled00/FX.h index 14bdf975ed..156fb49152 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -349,8 +349,9 @@ extern byte realtimeMode; // used in getMappedPixelIndex() #define FX_MODE_PSSTARBURST 211 #define FX_MODE_PS1DGEQ 212 #define FX_MODE_PSFIRE1D 213 -//#define FX_MODE_PSFRACTAL 214 -#define MODE_COUNT 214 +#define FX_MODE_PS1DSONICSTREAM 214 +//#define FX_MODE_PSFRACTAL 215 +#define MODE_COUNT 215 typedef enum mapping1D2D { M12_Pixels = 0, diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index c98244c48b..ffdbf3f2d0 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -1328,6 +1328,7 @@ int32_t ParticleSystem1D::sprayEmit(PSsource1D &emitter) { particles[emitIndex].collide = emitter.source.collide; particles[emitIndex].reversegrav = emitter.source.reversegrav; particles[emitIndex].ttl = hw_random16(emitter.minLife, emitter.maxLife); + particles[emitIndex].perpetual = emitter.source.perpetual; if (advPartProps) { advPartProps[emitIndex].sat = emitter.sat; advPartProps[emitIndex].size = emitter.size; From e27305355e127acab41a5818678891d9cf45707d Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Mon, 6 Jan 2025 20:57:46 +0100 Subject: [PATCH 161/219] changed TTL to brightness calculation, added collision binning - brightness is now doubled as some FX were really dim due to low TTL - added binning in x-direction giving a huge speed advantage on larger matrix sizes --- wled00/FX.cpp | 2 +- wled00/FXparticleSystem.cpp | 75 ++++++++++++++++++++++++------------- wled00/FXparticleSystem.h | 2 +- 3 files changed, 50 insertions(+), 29 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 80bf42ce9e..36e52d6c2b 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -8093,7 +8093,7 @@ uint16_t mode_particlepit(void) { ParticleSystem2D *PartSys = NULL; if (SEGMENT.call == 0) { // initialization - if (!initParticleSystem2D(PartSys, 1, 0, true)) // init, request one source (actually dont really need one TODO: test if using zero sources also works) + if (!initParticleSystem2D(PartSys, 1, 0, true, false)) // init, request one source (actually dont really need one TODO: test if using zero sources also works) return mode_static(); // allocation failed or not 2D PartSys->setKillOutOfBounds(true); PartSys->setGravity(); // enable with default gravity diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index ffdbf3f2d0..18cc8f759a 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -57,6 +57,7 @@ ParticleSystem2D::ParticleSystem2D(uint32_t width, uint32_t height, uint32_t num motionBlur = 0; //no fading by default smearBlur = 0; //no smearing by default emitIndex = 0; + collisionStartIdx = 0; //initialize some default non-zero values most FX use for (uint32_t i = 0; i < numSources; i++) { @@ -651,7 +652,7 @@ void ParticleSystem2D::ParticleSys_render(bool firemode, uint32_t fireintensity) baseRGB = ColorFromPalette(SEGPALETTE, brightness, 255); } else { - brightness = min(particles[i].ttl, (uint16_t)255); + brightness = min((particles[i].ttl << 1), (int)255); baseRGB = ColorFromPalette(SEGPALETTE, particles[i].hue, 255); // TODO: use loadPalette(CRGBPalette16 &targetPalette, SEGMENT.palette), .palette should be updated immediately at palette change, only use local palette during FX transitions, not during normal transitions. -> why not always? if (particles[i].sat < 255) { CHSV baseHSV = rgb2hsv_approximate(baseRGB); //convert to HSV //!!! TODO: use new hsv to rgb function. @@ -838,40 +839,59 @@ void ParticleSystem2D::renderParticle(const uint32_t particleindex, const uint32 } // detect collisions in an array of particles and handle them +// uses binning by dividing the frame into slices in x direction which is efficient if using gravity in y direction (but less efficient for FX that use forces in x direction) +// for code simplicity, no y slicing is done, making very tall matrix configurations less efficient void ParticleSystem2D::handleCollisions() { - uint32_t i, j; - uint32_t startparticle = 0; - uint32_t endparticle = usedParticles >> 1; // do half the particles, significantly speeds things up int32_t collDistSq = particleHardRadius << 1; collDistSq = collDistSq * collDistSq; // square it for faster comparison (square is one operation) - // 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 more accurate collisions are needed, just call it twice in a row - if (collisioncounter & 0x01) { - startparticle = endparticle; - 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].outofbounds == 0 && particles[i].collide) { // if particle is alive and is not out of view and does collide - int32_t dx, dy; // distance to other particles - for (j = i + 1; j < usedParticles; j++) { // check against higher number particles - if (particles[j].ttl > 0 && particles[j].collide) { // if target particle is alive - - if (advPartProps) { //may be using individual particle size - collDistSq = PS_P_MINHARDRADIUS + particlesize + (((uint32_t)advPartProps[i].size + (uint32_t)advPartProps[j].size)>>1); // collision distance - collDistSq = collDistSq * collDistSq; // square it for faster comparison + // note: partices are binned in x-axis, assumption is that no more than half of the particles are in the same bin + // if they are, collisionStartIdx is increased so each particle collides at least every second frame (which still gives decent collisions) + constexpr uint32_t BIN_WIDTH = 6 * PS_P_RADIUS; // width of a bin in sub-pixels + uint32_t maxBinParticles = (usedParticles + 1) / 2; // assume no more than half of the particles are in the same bin + uint32_t numBins = (maxX + 1) / BIN_WIDTH; // number of bins in x direction + uint16_t binIndices[maxBinParticles]; // creat array on stack for indices, 2kB max for 1024 particles (ESP32_MAXPARTICLES/2) + uint32_t binParticleCount; // number of particles in the current bin + uint32_t nextFrameStartIdx = 0; // index of the first particle in the next frame (set if bin overflow) + + // Loop through each bin + for (uint32_t bin = 0; bin < numBins; bin++) { + binParticleCount = 0; // Reset particle count for this bin + + // Compute bin bounds + uint32_t binStart = bin * BIN_WIDTH; + uint32_t binEnd = binStart + BIN_WIDTH; + + // Fill the binIndices array for this bin + for (uint32_t i = collisionStartIdx; i < usedParticles; ++i) { + if (particles[i].ttl > 0 && particles[i].outofbounds == 0 && particles[i].collide) { // colliding particle + if (particles[i].x >= binStart && particles[i].x <= binEnd) { // >= and <= to include particles on the edge of the bin (overlap to ensure boarder particles collide with adjacent bins) + if (binParticleCount >= maxBinParticles) { // bin is full, more particles in this bin so do the rest next frame + nextFrameStartIdx = i; // bin overflow can only happen once as bin size is at least half of the particles (or half +1) + break; } + binIndices[binParticleCount++] = i; + } + } + } - dx = particles[j].x - particles[i].x; - if (dx * dx < collDistSq) { // check x direction, if close, check y direction (squaring is faster than abs() or dual compare) - dy = particles[j].y - particles[i].y; - if (dy * dy < collDistSq) // particles are close - collideParticles(&particles[i], &particles[j], dx, dy); - } + for (uint32_t i = 0; i < binParticleCount; i++) { // go though all 'higher number' particles in this bin and see if any of those are in close proximity and if they are, make them collide + uint32_t idx_i = binIndices[i]; + for (uint32_t j = i + 1; j < binParticleCount; j++) { // check against higher number particles + uint32_t idx_j = binIndices[j]; + if (advPartProps) { //may be using individual particle size + collDistSq = PS_P_MINHARDRADIUS + particlesize + (((uint32_t)advPartProps[idx_i].size + (uint32_t)advPartProps[idx_j].size) >> 1); // collision distance + collDistSq = collDistSq * collDistSq; // square it for faster comparison + } + int32_t dx = particles[idx_j].x - particles[idx_i].x; + if (dx * dx < collDistSq) { // check x direction, if close, check y direction (squaring is faster than abs() or dual compare) + int32_t dy = particles[idx_j].y - particles[idx_i].y; + if (dy * dy < collDistSq) // particles are close + collideParticles(&particles[idx_i], &particles[idx_j], dx, dy); } } } } + collisionStartIdx = nextFrameStartIdx; // set the start index for the next frame } // handle a collision if close proximity is detected, i.e. dx and/or dy smaller than 2*PS_P_RADIUS @@ -917,6 +937,7 @@ void ParticleSystem2D::collideParticles(PSparticle *particle1, PSparticle *parti particle2->vx -= ximpulse; particle2->vy -= yimpulse; + // TODO: this makes them way too sticky. maybe apply friction only every x frames? could do (SEGMENT.call & 0x03) == 0 or even 0x07 if (collisionHardness < surfacehardness) { // if particles are soft, they become 'sticky' i.e. apply some friction (they do pile more nicely and stop sloshing around) const uint32_t coeff = collisionHardness + (255 - PS_P_MINSURFACEHARDNESS); // Note: could call applyFriction, but this is faster and speed is key here particle1->vx = ((int32_t)particle1->vx * coeff) / 255; @@ -1510,7 +1531,7 @@ void ParticleSystem1D::ParticleSys_render() { continue; // generate RGB values for particle - brightness = min(particles[i].ttl, (uint16_t)255); + brightness = min(particles[i].ttl << 1, (int)255); baseRGB = ColorFromPalette(SEGPALETTE, particles[i].hue, 255); if (advPartProps) { //saturation is advanced property in 1D system diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index d780a1c742..08250c094e 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -211,7 +211,7 @@ class ParticleSystem2D { int32_t collisionHardness; uint32_t wallHardness; uint32_t wallRoughness; // randomizes wall collisions - uint32_t collisioncounter; // counter to handle collisions TODO: could use the SEGMENT.call? + uint32_t collisionStartIdx; // particle array start index for collision detection uint32_t particleHardRadius; // hard surface radius of a particle, used for collision detection (32bit for speed) uint8_t forcecounter; // counter for globally applied forces uint8_t gforcecounter; // counter for global gravity From ffb17c3651de4d051214eb026bd00572dbc65018 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Mon, 6 Jan 2025 22:14:30 +0100 Subject: [PATCH 162/219] collision binning bugfix, made particles less sticky, update to waterfall - updated waterfall intensity and blurring --- wled00/FX.cpp | 8 ++++---- wled00/FXparticleSystem.cpp | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 36e52d6c2b..44469f9561 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -8170,8 +8170,8 @@ uint16_t mode_particlewaterfall(void) { PartSys->setGravity(); // enable with default gforce PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting) - PartSys->setMotionBlur(170); // anable motion blur - PartSys->setSmearBlur(45); // enable 2D blurring (smearing) + PartSys->setMotionBlur(180); // anable motion blur + PartSys->setSmearBlur(30); // enable 2D blurring (smearing) for (i = 0; i < PartSys->numSources; i++) { PartSys->sources[i].source.hue = i*90; PartSys->sources[i].source.collide = true; // seeded particles will collide @@ -8207,7 +8207,7 @@ uint16_t mode_particlewaterfall(void) { PartSys->sources[i].source.hue += 1 + hw_random16(SEGMENT.custom1>>1); // change hue of spray source } - if (SEGMENT.call % (9 - (SEGMENT.intensity >> 5)) == 0 && SEGMENT.intensity > 0) { // every nth frame, emit particles, do not emit if intensity is zero + if (SEGMENT.call % (12 - (SEGMENT.intensity >> 5)) == 0 && SEGMENT.intensity > 0) { // every nth frame, emit particles, do not emit if intensity is zero for (i = 0; i < numSprays; i++) { PartSys->sources[i].vy = -SEGMENT.speed >> 3; // emitting speed, down //PartSys->sources[i].source.x = map(SEGMENT.custom3, 0, 31, 0, (PartSys->maxXpixel - numSprays * 2) * PS_P_RADIUS) + i * PS_P_RADIUS * 2; // emitter position @@ -8224,7 +8224,7 @@ uint16_t mode_particlewaterfall(void) { PartSys->update(); // update and render return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEWATERFALL[] PROGMEM = "PS Waterfall@Speed,Intensity,Variation,Collide,Position,Cylinder,Walls,Ground;;!;2;pal=9,sx=15,ix=200,c1=32,c2=160,c3=17,o3=1"; +static const char _data_FX_MODE_PARTICLEWATERFALL[] PROGMEM = "PS Waterfall@Speed,Intensity,Variation,Collide,Position,Cylinder,Walls,Ground;;!;2;pal=9,sx=15,ix=200,c1=32,c2=160,o3=1"; /* Particle Box, applies gravity to particles in either a random direction or random but only downwards (sloshing) diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index 18cc8f759a..9b072a3601 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -841,6 +841,7 @@ void ParticleSystem2D::renderParticle(const uint32_t particleindex, const uint32 // detect collisions in an array of particles and handle them // uses binning by dividing the frame into slices in x direction which is efficient if using gravity in y direction (but less efficient for FX that use forces in x direction) // for code simplicity, no y slicing is done, making very tall matrix configurations less efficient +// note: also tested adding y slicing, it gives diminishing returns, some FX even get slower. FX not using gravity would benefit with a 10% FPS improvement void ParticleSystem2D::handleCollisions() { int32_t collDistSq = particleHardRadius << 1; collDistSq = collDistSq * collDistSq; // square it for faster comparison (square is one operation) @@ -848,7 +849,7 @@ void ParticleSystem2D::handleCollisions() { // if they are, collisionStartIdx is increased so each particle collides at least every second frame (which still gives decent collisions) constexpr uint32_t BIN_WIDTH = 6 * PS_P_RADIUS; // width of a bin in sub-pixels uint32_t maxBinParticles = (usedParticles + 1) / 2; // assume no more than half of the particles are in the same bin - uint32_t numBins = (maxX + 1) / BIN_WIDTH; // number of bins in x direction + uint32_t numBins = (maxX + (BIN_WIDTH -1)) / BIN_WIDTH; // number of bins in x direction uint16_t binIndices[maxBinParticles]; // creat array on stack for indices, 2kB max for 1024 particles (ESP32_MAXPARTICLES/2) uint32_t binParticleCount; // number of particles in the current bin uint32_t nextFrameStartIdx = 0; // index of the first particle in the next frame (set if bin overflow) @@ -937,8 +938,7 @@ void ParticleSystem2D::collideParticles(PSparticle *particle1, PSparticle *parti particle2->vx -= ximpulse; particle2->vy -= yimpulse; - // TODO: this makes them way too sticky. maybe apply friction only every x frames? could do (SEGMENT.call & 0x03) == 0 or even 0x07 - if (collisionHardness < surfacehardness) { // if particles are soft, they become 'sticky' i.e. apply some friction (they do pile more nicely and stop sloshing around) + if (collisionHardness < surfacehardness && (SEGMENT.call & 0x03) == 0) { // if particles are soft, they become 'sticky' i.e. apply some friction (they do pile more nicely and stop sloshing around) const uint32_t coeff = collisionHardness + (255 - PS_P_MINSURFACEHARDNESS); // Note: could call applyFriction, but this is faster and speed is key here particle1->vx = ((int32_t)particle1->vx * coeff) / 255; particle1->vy = ((int32_t)particle1->vy * coeff) / 255; From 5d0daa7ba4aea9b22c747e4ec13add70bf21fcc9 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Wed, 8 Jan 2025 07:49:05 +0100 Subject: [PATCH 163/219] WIP: fixed 1D system for over 1000 pixels, added collision binning for 1D - making x coordinate 32bit allows for larger strips but uses a lot of ram due to struct memory alignment (12bytes instead of 8 bytes), this needs some more work to properly fix. - adding collision binning significantly speeds things up, about a factor of 2 on most FX using collision - there are still some bugs in FX or 1D memory handling (or both) on large setups --- wled00/FX.cpp | 4 +- wled00/FXparticleSystem.cpp | 87 ++++++++++++++++++++++--------------- wled00/FXparticleSystem.h | 10 ++--- 3 files changed, 59 insertions(+), 42 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 44469f9561..480b6ddd49 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -10155,10 +10155,10 @@ uint16_t mode_particle1Dsonicstream(void) { // Particle System settings PartSys->updateSystem(); // update system properties (dimensions and data pointers) - PartSys->setMotionBlur(20 + SEGMENT.custom2>>1); // anable motion blur + PartSys->setMotionBlur(20 + (SEGMENT.custom2 >> 1)); // anable motion blur PartSys->setSmearBlur(200); // smooth out the edges - PartSys->sources[0].v = 5 + SEGMENT.speed >> 2; + PartSys->sources[0].v = 5 + (SEGMENT.speed >> 2); // FFT processing um_data_t *um_data; diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index 9b072a3601..7898ac01fc 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -852,18 +852,15 @@ void ParticleSystem2D::handleCollisions() { uint32_t numBins = (maxX + (BIN_WIDTH -1)) / BIN_WIDTH; // number of bins in x direction uint16_t binIndices[maxBinParticles]; // creat array on stack for indices, 2kB max for 1024 particles (ESP32_MAXPARTICLES/2) uint32_t binParticleCount; // number of particles in the current bin - uint32_t nextFrameStartIdx = 0; // index of the first particle in the next frame (set if bin overflow) + uint16_t nextFrameStartIdx = 0; // index of the first particle in the next frame (set if bin overflow) - // Loop through each bin for (uint32_t bin = 0; bin < numBins; bin++) { - binParticleCount = 0; // Reset particle count for this bin + binParticleCount = 0; // reset for this bin + int32_t binStart = bin * BIN_WIDTH; + int32_t binEnd = binStart + BIN_WIDTH; - // Compute bin bounds - uint32_t binStart = bin * BIN_WIDTH; - uint32_t binEnd = binStart + BIN_WIDTH; - - // Fill the binIndices array for this bin - for (uint32_t i = collisionStartIdx; i < usedParticles; ++i) { + // fill the binIndices array for this bin + for (uint32_t i = collisionStartIdx; i < usedParticles; i++) { if (particles[i].ttl > 0 && particles[i].outofbounds == 0 && particles[i].collide) { // colliding particle if (particles[i].x >= binStart && particles[i].x <= binEnd) { // >= and <= to include particles on the edge of the bin (overlap to ensure boarder particles collide with adjacent bins) if (binParticleCount >= maxBinParticles) { // bin is full, more particles in this bin so do the rest next frame @@ -1245,7 +1242,7 @@ void ParticleSystem1D::update(void) { if (particlesettings.colorByPosition) { uint32_t scale = (255 << 16) / maxX; // speed improvement: multiplication is faster than division for (uint32_t i = 0; i < usedParticles; i++) { - particles[i].hue = (scale * (uint32_t)particles[i].x) >> 16; + particles[i].hue = (scale * particles[i].x) >> 16; // note: x is > 0 if not out of bounds } } @@ -1426,7 +1423,7 @@ void ParticleSystem1D::particleMoveUpdate(PSparticle1D &part, PSsettings1D *opti } if (!part.fixed) - part.x = (int16_t)newX; // set new position + part.x = newX; // set new position else part.vx = 0; // set speed to zero. note: particle can get speed in collisions, if unfixed, it should not speed away } @@ -1663,26 +1660,47 @@ void ParticleSystem1D::renderParticle(const uint32_t particleindex, const uint32 // detect collisions in an array of particles and handle them void ParticleSystem1D::handleCollisions() { - uint32_t i, j; int32_t collisiondistance = PS_P_MINHARDRADIUS_1D; - for (i = 0; i < usedParticles; i++) { - // go though all 'higher number' particles and see if any of those are in close proximity and if they are, make them collide - if (particles[i].ttl > 0 && particles[i].outofbounds == 0 && particles[i].collide) { // if particle is alive and does collide and is not out of view - int32_t dx; // distance to other particles - for (j = i + 1; j < usedParticles; j++) { // check against higher number particles - if (particles[j].ttl > 0 && particles[j].collide) { // if target particle is alive and collides - if (advPartProps) { // use advanced size properties - collisiondistance = PS_P_MINHARDRADIUS_1D + (((uint32_t)advPartProps[i].size + (uint32_t)advPartProps[j].size) >> 1); - } - dx = particles[j].x - particles[i].x; - int32_t dv = (int32_t)particles[j].vx - (int32_t)particles[i].vx; - int32_t proximity = collisiondistance; - if (dv >= proximity) // particles would go past each other in next move update - proximity += abs(dv); // add speed difference to catch fast particles - if (dx < proximity && dx > -proximity) { // check if close - collideParticles(&particles[i], &particles[j], dx, dv, collisiondistance); + constexpr uint32_t BIN_WIDTH = 32 * PS_P_RADIUS_1D; // width of each bin, 32 pixels gives good results, smaller is slower, larger also gets slower, 48 is also still ok + uint32_t maxBinParticles = (usedParticles + 1) / 4; // assume no more than 1/4 of the particles are in the same bin + uint32_t numBins = (maxX + 1) / BIN_WIDTH; // calculate number of bins + uint16_t binIndices[maxBinParticles]; // array to store indices of particles in a bin + uint32_t binParticleCount; // number of particles in the current bin + uint16_t nextFrameStartIdx = 0; // index of the first particle in the next frame (set if bin overflow) + + for (uint32_t bin = 0; bin < numBins; bin++) { + binParticleCount = 0; // reset for this bin + int32_t binStart = bin * BIN_WIDTH; + int32_t binEnd = binStart + BIN_WIDTH; + + // fill the binIndices array for this bin + for (uint32_t i = collisionStartIdx; i < usedParticles; i++) { + if (particles[i].ttl > 0 && particles[i].outofbounds == 0 && particles[i].collide) { // colliding particle + if (particles[i].x >= binStart && particles[i].x <= binEnd) { // >= and <= to include particles on the edge of the bin (overlap to ensure boarder particles collide with adjacent bins) + if (binParticleCount >= maxBinParticles) { // bin is full, more particles in this bin so do the rest next frame + nextFrameStartIdx = i; // bin overflow can only happen once as bin size is at least half of the particles (or half +1) + break; } + binIndices[binParticleCount++] = i; + } + } + } + + for (uint32_t i = 0; i < binParticleCount; i++) { // go though all 'higher number' particles and see if any of those are in close proximity and if they are, make them collide + uint32_t idx_i = binIndices[i]; + for (uint32_t j = i + 1; j < binParticleCount; j++) { // check against higher number particles + uint32_t idx_j = binIndices[j]; + if (advPartProps) { // use advanced size properties + collisiondistance = PS_P_MINHARDRADIUS_1D + (((uint32_t)advPartProps[idx_i].size + (uint32_t)advPartProps[idx_j].size) >> 1); + } + int32_t dx = particles[idx_j].x - particles[idx_i].x; + int32_t dv = (int32_t)particles[idx_j].vx - (int32_t)particles[idx_i].vx; + int32_t proximity = collisiondistance; + if (dv >= proximity) // particles would go past each other in next move update + proximity += abs(dv); // add speed difference to catch fast particles + if (dx < proximity && dx > -proximity) { // check if close + collideParticles(&particles[idx_i], &particles[idx_j], dx, dv, collisiondistance); } } } @@ -2084,6 +2102,9 @@ void* particleMemoryManager(const uint32_t requestedParticles, size_t structSize int32_t totransfer = maxParticles - availableToPS; // transfer all remaining particles if(totransfer < 0) totransfer = 0; // safety check particleHandover(buffer, structSize, totransfer); + + //TODO: there is a bug here, in 1D system, this does not really work right. maybe an alignment problem??? (2D seems to work fine) + // -> bug seems magically fixed? if(maxParticles / numParticlesUsed > 3) { // FX uses less than 25%: move the already existing particles to the beginning of the buffer uint32_t usedbytes = availableToPS * structSize; uint32_t bufferoffset = (maxParticles - 1) - availableToPS; // offset to existing particles (see above) @@ -2125,12 +2146,12 @@ void* particleMemoryManager(const uint32_t requestedParticles, size_t structSize // (re)initialize particles in the particle buffer for use in the new FX void particleHandover(void *buffer, size_t structSize, int32_t numToTransfer) { + if (pmem->particleType != structSize) { // check if we are being handed over from a different system (1D<->2D), clear buffer if so + memset(buffer, 0, numToTransfer * structSize); // clear buffer + } #ifndef WLED_DISABLE_PARTICLESYSTEM2D if (structSize == sizeof(PSparticle)) { // 2D particle PSparticle *particles = (PSparticle *)buffer; - if (pmem->particleType != sizeof(PSparticle)) { // check if we are being handed over from a 1D system, clear buffer if so - memset(buffer, 0, numToTransfer * sizeof(PSparticle)); // clear buffer - } for (int32_t i = 0; i < numToTransfer; i++) { particles[i].perpetual = false; // particle ages if (particles[i].outofbounds) @@ -2146,10 +2167,6 @@ void particleHandover(void *buffer, size_t structSize, int32_t numToTransfer) { { #ifndef WLED_DISABLE_PARTICLESYSTEM1D PSparticle1D *particles = (PSparticle1D *)buffer; - // check if we are being handed over from a 2D system, clear buffer if so - if (pmem->particleType != sizeof(PSparticle1D)) { - memset(buffer, 0, numToTransfer * sizeof(PSparticle1D)); // clear buffer - } for (int32_t i = 0; i < numToTransfer; i++) { particles[i].perpetual = false; // particle ages particles[i].fixed = false; // unfix all particles diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index 08250c094e..ff7f282be9 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -211,7 +211,7 @@ class ParticleSystem2D { int32_t collisionHardness; uint32_t wallHardness; uint32_t wallRoughness; // randomizes wall collisions - uint32_t collisionStartIdx; // particle array start index for collision detection + uint16_t collisionStartIdx; // particle array start index for collision detection uint32_t particleHardRadius; // hard surface radius of a particle, used for collision detection (32bit for speed) uint8_t forcecounter; // counter for globally applied forces uint8_t gforcecounter; // counter for global gravity @@ -269,19 +269,19 @@ typedef union { //struct for a single particle (8 bytes) typedef struct { - int16_t x; // x position in particle system + int32_t x; // x position in particle system + uint16_t ttl; // time to live in frames int8_t vx; // horizontal velocity uint8_t hue; // color hue // two byte bit field: //uint16_t ttl : 11; // time to live, 11 bit or 2047 max (which is 25s at 80FPS) - uint16_t ttl; // time to live in frames bool outofbounds : 1; // out of bounds flag, set to true if particle is outside of display area bool collide : 1; // if set, particle takes part in collisions bool perpetual : 1; // if set, particle does not age (TTL is not decremented in move function, it still dies from killoutofbounds) bool reversegrav : 1; // if set, gravity is reversed on this particle bool fixed : 1; // if set, particle does not move (and collisions make other particles revert direction), // note: there is on byte of padding added here, making TTL a 16bit variable saves 500bytes of flash so much faster than a bit field - // TODO: can this be optimized? wastes a lot of ram... + // TODO: can this be optimized? wastes a lot of ram... -> yes, TODO: make the flags a seperate struct array and handle it everywhere. } PSparticle1D; // struct for additional particle settings (optional) @@ -371,7 +371,7 @@ class ParticleSystem1D uint8_t gforcecounter; // counter for global gravity int8_t gforce; // gravity strength, default is 8 (negative is allowed, positive is downwards) uint8_t forcecounter; // counter for globally applied forces - //uint8_t collisioncounter; // counter to handle collisions TODO: could use the SEGMENT.call? -> currently unused + uint16_t collisionStartIdx; // particle array start index for collision detection //global particle properties for basic particles uint8_t particlesize; // global particle size, 0 = 1 pixel, 1 = 2 pixels, larger sizez TBD (TODO: need larger sizes?) uint8_t motionBlur; // enable motion blur, values > 100 gives smoother animations From bcea39d21ac514e9381d746aee1ee7a1fc70823c Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Fri, 10 Jan 2025 20:17:07 +0100 Subject: [PATCH 164/219] moved particle flags to seperate array to save on RAM, refactoring - moving the flags optimizes ram alignment, saving memory for each particle - changed lots of parameters to `const` - moved fire intensity to a variable instead of passing it to every render() call - changed passing pointers to passing reference where possible - saves a total of 340 bytes of flash --- wled00/FX.cpp | 150 +++++++-------- wled00/FXparticleSystem.cpp | 370 ++++++++++++++++++------------------ wled00/FXparticleSystem.h | 230 ++++++++++++---------- 3 files changed, 379 insertions(+), 371 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 480b6ddd49..6ad69cb377 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -7810,14 +7810,14 @@ uint16_t mode_particlefireworks(void) { PartSys->setGravity(map(SEGMENT.custom3, 0, 31, SEGMENT.check2 ? 1 : 0, 10)); // if bounded, set gravity to minimum of 1 or they will bounce at top PartSys->setMotionBlur(map(SEGMENT.custom2, 0, 255, 0, 170)); // anable motion blur uint8_t smearing = 0; - if(SEGMENT.custom2 > 200) + if(SEGMENT.custom2 > 200) smearing = SEGMENT.custom2 - 200; PartSys->setSmearBlur(smearing); // enable 2D blurring (smearing) // update the rockets, set the speed state for (j = 0; j < numRockets; j++) { PartSys->applyGravity(PartSys->sources[j].source); - PartSys->particleMoveUpdate(PartSys->sources[j].source); + PartSys->particleMoveUpdate(PartSys->sources[j].source, PartSys->sources[j].sourceFlags); if (PartSys->sources[j].source.ttl == 0) { if (PartSys->sources[j].source.vy > 0) { // rocket has died and is moving up. stop it so it will explode (is handled in the code below) PartSys->sources[j].source.vy = 0; @@ -7947,8 +7947,8 @@ uint16_t mode_particlevolcano(void) { PartSys->sources[i].source.x = PartSys->maxX / (numSprays + 1) * (i + 1); // distribute evenly PartSys->sources[i].maxLife = 300; // lifetime in frames PartSys->sources[i].minLife = 250; - PartSys->sources[i].source.collide = true; // seeded particles will collide (if enabled) - PartSys->sources[i].source.perpetual = true; // source never dies + PartSys->sources[i].sourceFlags.collide = true; // seeded particles will collide (if enabled) + PartSys->sources[i].sourceFlags.perpetual = true; // source never dies } } else @@ -7981,7 +7981,7 @@ uint16_t mode_particlevolcano(void) { PartSys->sources[i].vx = 0; PartSys->sources[i].var = SEGMENT.custom3 >> 1; // emiting variation = nozzle size (custom 3 goes from 0-31) PartSys->sprayEmit(PartSys->sources[i]); - PartSys->particleMoveUpdate(PartSys->sources[i].source, &volcanosettings); //move the source + PartSys->particleMoveUpdate(PartSys->sources[i].source, PartSys->sources[i].sourceFlags, &volcanosettings); //move the source } } @@ -8076,7 +8076,7 @@ uint16_t mode_particlefire(void) { PartSys->flameEmit(PartSys->sources[j]); } - PartSys->updateFire(SEGMENT.intensity); // update and render the fire + PartSys->updateFire(SEGMENT.intensity, false); // update and render the fire return FRAMETIME; } @@ -8126,7 +8126,7 @@ uint16_t mode_particlepit(void) { PartSys->particles[i].vx = (int16_t)hw_random16(SEGMENT.speed >> 1) - (SEGMENT.speed >> 2); // side speed is +/- PartSys->particles[i].vy = map(SEGMENT.speed, 0, 255, -5, -100); // downward speed PartSys->particles[i].hue = hw_random16(); // set random color - PartSys->particles[i].collide = true; // enable collision for particle + PartSys->particleFlags[i].collide = true; // enable collision for particle PartSys->particles[i].sat = ((SEGMENT.custom3) << 3) + 7; // set particle size if (SEGMENT.custom1 == 255) { @@ -8174,7 +8174,7 @@ uint16_t mode_particlewaterfall(void) { PartSys->setSmearBlur(30); // enable 2D blurring (smearing) for (i = 0; i < PartSys->numSources; i++) { PartSys->sources[i].source.hue = i*90; - PartSys->sources[i].source.collide = true; // seeded particles will collide + PartSys->sources[i].sourceFlags.collide = true; // seeded particles will collide #ifdef ESP8266 PartSys->sources[i].maxLife = 250; // lifetime in frames (ESP8266 has less particles, make them short lived to keep the water flowing) PartSys->sources[i].minLife = 100; @@ -8257,11 +8257,11 @@ uint16_t mode_particlebox(void) { for (i = 0; i < PartSys->usedParticles; i++) { if(PartSys->particles[i].ttl < 260) { // initialize handed over particles and dead particles PartSys->particles[i].ttl = 260; // full brigthness - PartSys->particles[i].perpetual = true; // never die PartSys->particles[i].x = hw_random16(PartSys->maxX); PartSys->particles[i].y = hw_random16(PartSys->maxY); PartSys->particles[i].hue = hw_random8(); // make it colorful - PartSys->particles[i].collide = true; // all particles colllide + PartSys->particleFlags[i].perpetual = true; // never die + PartSys->particleFlags[i].collide = true; // all particles colllide break; // only spawn one particle per frame for less chaotic transitions } } @@ -8347,7 +8347,7 @@ uint16_t mode_particleperlin(void) { PartSys->particles[i].ttl = hw_random16(500) + 200; PartSys->particles[i].x = hw_random(PartSys->maxX); PartSys->particles[i].y = hw_random(PartSys->maxY); - PartSys->particles[i].collide = true; // particle colllides + PartSys->particleFlags[i].collide = true; // particle colllides } uint32_t scale = 16 - ((31 - SEGMENT.custom3) >> 1); uint16_t xnoise = PartSys->particles[i].x / scale; // position in perlin noise, scaled by slider @@ -8444,13 +8444,13 @@ uint16_t mode_particleimpact(void) { PartSys->sources[i].source.ttl--; // note: this saves an if statement, but moving down particles age twice if (PartSys->sources[i].source.vy < 0) { // move down PartSys->applyGravity(PartSys->sources[i].source); - PartSys->particleMoveUpdate(PartSys->sources[i].source, &meteorsettings); + PartSys->particleMoveUpdate(PartSys->sources[i].source, PartSys->sources[i].sourceFlags, &meteorsettings); // if source reaches the bottom, set speed to 0 so it will explode on next function call (handled above) if (PartSys->sources[i].source.y < PS_P_RADIUS<<1) { // reached the bottom pixel on its way down PartSys->sources[i].source.vy = 0; // set speed zero so it will explode PartSys->sources[i].source.vx = 0; - PartSys->sources[i].source.collide = true; + PartSys->sources[i].sourceFlags.collide = true; #ifdef ESP8266 PartSys->sources[i].maxLife = 180; PartSys->sources[i].minLife = 20; @@ -8472,7 +8472,7 @@ uint16_t mode_particleimpact(void) { PartSys->sources[i].source.vx = hw_random16(50) - 25; // TODO: make this dependent on position so they do not move out of frame PartSys->sources[i].source.hue = hw_random16(); // random color PartSys->sources[i].source.ttl = 500; // long life, will explode at bottom - PartSys->sources[i].source.collide = false; // trail particles will not collide + PartSys->sources[i].sourceFlags.collide = false; // trail particles will not collide PartSys->sources[i].maxLife = 60; // spark particle life PartSys->sources[i].minLife = 20; PartSys->sources[i].vy = -9; // emitting speed (down) @@ -8497,14 +8497,16 @@ uint16_t mode_particleattractor(void) { ParticleSystem2D *PartSys = NULL; PSsettings2D sourcesettings; sourcesettings.asByte = 0b00001100; // PS settings for bounceY, bounceY used for source movement (it always bounces whereas particles do not) + PSparticleFlags attractorFlags; + attractorFlags.asByte = 0; // no flags set PSparticle *attractor; // particle pointer to the attractor if (SEGMENT.call == 0) { // initialization if (!initParticleSystem2D(PartSys, 1, sizeof(PSparticle), true)) // init using 1 source and advanced particle settings return mode_static(); // allocation failed or not 2D PartSys->sources[0].source.hue = hw_random16(); PartSys->sources[0].source.vx = -7; // will collied with wall and get random bounce direction - PartSys->sources[0].source.collide = true; // seeded particles will collide - PartSys->sources[0].source.perpetual = true; //source does not age + PartSys->sources[0].sourceFlags.collide = true; // seeded particles will collide + PartSys->sources[0].sourceFlags.perpetual = true; //source does not age #ifdef ESP8266 PartSys->sources[0].maxLife = 200; // lifetime in frames (ESP8266 has less particles) PartSys->sources[0].minLife = 30; @@ -8539,14 +8541,13 @@ uint16_t mode_particleattractor(void) { if (SEGMENT.call == 0) { attractor->vx = PartSys->sources[0].source.vy; // set to spray movemement but reverse x and y attractor->vy = PartSys->sources[0].source.vx; - attractor->ttl = 100; - attractor->perpetual = true; } // set attractor properties + attractor->ttl = 100; // never dies if (SEGMENT.check2) { if ((SEGMENT.call % 3) == 0) // move slowly - PartSys->particleMoveUpdate(*attractor, &sourcesettings); // move the attractor + PartSys->particleMoveUpdate(*attractor, attractorFlags, &sourcesettings); // move the attractor } else { attractor->x = PartSys->maxX >> 1; // set to center @@ -8579,13 +8580,13 @@ uint16_t mode_particleattractor(void) { } #else // no AR for (uint32_t i = 0; i < PartSys->usedParticles; i++) { - PartSys->pointAttractor(i, attractor, SEGMENT.speed, SEGMENT.check3); + PartSys->pointAttractor(i, *attractor, SEGMENT.speed, SEGMENT.check3); } #endif if (SEGMENT.call % (33 - SEGMENT.custom3) == 0) PartSys->applyFriction(2); - PartSys->particleMoveUpdate(PartSys->sources[0].source, &sourcesettings); // move the source + PartSys->particleMoveUpdate(PartSys->sources[0].source, PartSys->sources[0].sourceFlags, &sourcesettings); // move the source PartSys->update(); // update and render return FRAMETIME; } @@ -8595,6 +8596,7 @@ static const char _data_FX_MODE_PARTICLEATTRACTOR[] PROGMEM = "PS Attractor@Mass static const char _data_FX_MODE_PARTICLEATTRACTOR[] PROGMEM = "PS Attractor@Mass,Particles,Size,Collide,Friction,AgeColor,Move,Swallow;;!;2;pal=9,sx=100,ix=82,c1=0,c2=0"; #endif + /* Particle Spray, just a particle spray with many parameters Uses palette for particle color @@ -8613,7 +8615,7 @@ uint16_t mode_particlespray(void) { PartSys->setBounceY(true); PartSys->setMotionBlur(200); // anable motion blur PartSys->sources[0].source.hue = hw_random16(); - PartSys->sources[0].source.collide = true; // seeded particles will collide (if enabled) + PartSys->sources[0].sourceFlags.collide = true; // seeded particles will collide (if enabled) PartSys->sources[0].var = 3; } else @@ -8895,7 +8897,7 @@ uint16_t mode_particleghostrider(void) { PartSys->sources[0].source.vx = ((int32_t)cos16_t(SEGENV.aux0) * speed) / (int32_t)32767; PartSys->sources[0].source.vy = ((int32_t)sin16_t(SEGENV.aux0) * speed) / (int32_t)32767; PartSys->sources[0].source.ttl = 500; // source never dies (note: setting 'perpetual' is not needed if replenished each frame) - PartSys->particleMoveUpdate(PartSys->sources[0].source, &ghostsettings); + PartSys->particleMoveUpdate(PartSys->sources[0].source, PartSys->sources[0].sourceFlags, &ghostsettings); // set head (steal one of the particles) PartSys->particles[PartSys->usedParticles-1].x = PartSys->sources[0].source.x; PartSys->particles[PartSys->usedParticles-1].y = PartSys->sources[0].source.y; @@ -8920,6 +8922,7 @@ PS Blobs: large particles bouncing around, changing size and form Uses palette for particle color by DedeHai (Damian Schneider) */ + uint16_t mode_particleblobs(void) { ParticleSystem2D *PartSys = NULL; @@ -8957,7 +8960,7 @@ uint16_t mode_particleblobs(void) { PartSys->particles[i].x = hw_random(PartSys->maxX); PartSys->particles[i].y = hw_random16(PartSys->maxY); PartSys->particles[i].hue = hw_random16(); // set random color - PartSys->particles[i].collide = true; // enable collision for particle + PartSys->particleFlags[i].collide = true; // enable collision for particle PartSys->advPartProps[i].size = 0; // start out small PartSys->advPartSize[i].asymmetry = hw_random16(220); PartSys->advPartSize[i].asymdir = hw_random16(255); @@ -8997,7 +9000,7 @@ static const char _data_FX_MODE_PARTICLEBLOBS[] PROGMEM = "PS Blobs@Speed,Blobs, * particles move, then split to form a fractal tree EXPERIMENTAL and non working! * by DedeHai (Damian Schneider) */ - + uint16_t mode_particlefractal(void) { ParticleSystem2D *PartSys = NULL; uint32_t i; @@ -9033,14 +9036,10 @@ uint16_t mode_particlefractal(void) { PartSys->sources[0].source.hue = PartSys->particles[i].hue + 50; // todo: make color schemes uint16_t angle = currentangle - angleoffset; int32_t index = PartSys->angleEmit(PartSys->sources[0], angle, emitspeed); //upward TODO: make angle adjustable - Serial.print("branch emit1 at idx = "); - Serial.println(index); //TODO: check if index >=0!!! PartSys->advPartProps[index].forcecounter = angle >> 7; angle = currentangle + angleoffset; index = PartSys->angleEmit(PartSys->sources[0], angle, emitspeed); - Serial.print("branch emit2 at idx = "); - Serial.println(index); PartSys->advPartProps[index].forcecounter = angle >> 7; } } @@ -9053,8 +9052,6 @@ uint16_t mode_particlefractal(void) { PartSys->sources[0].minLife = 270; uint32_t angle = ((uint32_t)SEGMENT.custom1) << 7; //16 bit angle, 0° to 180° int32_t index = PartSys->angleEmit(PartSys->sources[0], angle, emitspeed); //upward TODO: make angle adjustable - Serial.print("base emit at idx = "); - Serial.println(index); //set the forcecounter to track the angle (only 8 bit precision...) PartSys->advPartProps[index].forcecounter = angle >> 7; } @@ -9113,7 +9110,7 @@ uint16_t mode_particleDrip(void) { else PartSys->enableParticleCollisions(false); - PartSys->sources[0].source.collide = false; //drops do not collide + PartSys->sources[0].sourceFlags.collide = false; //drops do not collide if (SEGMENT.check1) { //rain mode, emit at random position, short life (3-8 seconds at 50fps) if (SEGMENT.custom1 == 0) //splash disabled, do not bounce raindrops @@ -9150,7 +9147,7 @@ uint16_t mode_particleDrip(void) { } for (uint32_t i = 0; i < PartSys->usedParticles; i++) { //check all particles - if (PartSys->particles[i].ttl && PartSys->particles[i].collide == false) { // use collision flag to identify splash particles + if (PartSys->particles[i].ttl && PartSys->particleFlags[i].collide == false) { // use collision flag to identify splash particles if (SEGMENT.custom1 > 0 && PartSys->particles[i].x < (PS_P_RADIUS_1D << 1)) { //splash enabled and reached bottom PartSys->particles[i].ttl = 0; //kill origin particle PartSys->sources[0].maxLife = 80; @@ -9159,7 +9156,7 @@ uint16_t mode_particleDrip(void) { PartSys->sources[0].v = 0; PartSys->sources[0].source.hue = PartSys->particles[i].hue; PartSys->sources[0].source.x = PS_P_RADIUS_1D; - PartSys->sources[0].source.collide = true; //splashes do collide if enabled + PartSys->sources[0].sourceFlags.collide = true; //splashes do collide if enabled for (int j = 0; j < 2 + (SEGMENT.custom1 >> 2); j++) { PartSys->sprayEmit(PartSys->sources[0]); } @@ -9172,7 +9169,7 @@ uint16_t mode_particleDrip(void) { } //increase speed on high settings by calling the move function twice if (SEGMENT.speed > 200) - PartSys->particleMoveUpdate(PartSys->particles[i]); + PartSys->particleMoveUpdate(PartSys->particles[i], PartSys->particleFlags[i]); } PartSys->update(); // update and render @@ -9194,7 +9191,7 @@ uint16_t mode_particleBouncingBalls(void) { if (SEGMENT.call == 0) { // initialization if (!initParticleSystem1D(PartSys, 1, 128, 0, true)) // init return mode_static(); // allocation failed or is single pixel - PartSys->sources[0].source.collide = true; // seeded particles will collide (if enabled) + PartSys->sources[0].sourceFlags.collide = true; // seeded particles will collide (if enabled) PartSys->sources[0].source.x = PS_P_RADIUS_1D; //emit at bottom PartSys->sources[0].maxLife = 900; // maximum lifetime in frames PartSys->sources[0].minLife = PartSys->sources[0].maxLife; @@ -9231,7 +9228,7 @@ uint16_t mode_particleBouncingBalls(void) { PartSys->particles[i].ttl = 260; //set alive at full intensity if (updateballs || PartSys->particles[i].ttl == 0) { //speed changed or particle died, set particle properties PartSys->particles[i].ttl = 260 + SEGMENT.speed; - PartSys->particles[i].collide = true; + PartSys->particleFlags[i].collide = true; int32_t newspeed = hw_random16(20 + (SEGMENT.speed >> 2)) + (SEGMENT.speed >> 3); PartSys->particles[i].vx = PartSys->particles[i].vx > 0 ? newspeed : -newspeed; //keep the direction PartSys->particles[i].hue = hw_random8(); //set ball colors to random @@ -9260,7 +9257,7 @@ uint16_t mode_particleBouncingBalls(void) { SEGENV.aux1 = SEGMENT.speed + SEGMENT.intensity + SEGMENT.check2 + SEGMENT.custom1; for (uint32_t i = 0; i < PartSys->usedParticles; i++) { if (SEGMENT.speed > 200) - PartSys->particleMoveUpdate(PartSys->particles[i]); //increase speed on high settings by calling the move function twice + PartSys->particleMoveUpdate(PartSys->particles[i], PartSys->particleFlags[i]); //increase speed on high settings by calling the move function twice } PartSys->update(); // update and render @@ -9308,10 +9305,10 @@ uint16_t mode_particleDancingShadows(void) { uint32_t deadparticles = 0; //kill out of bounds and moving away plus change color for (uint32_t i = 0; i < PartSys->usedParticles; i++) { - if(((SEGMENT.call & 0x07) == 0) && PartSys->particles[i].outofbounds) { //check if out of bounds particle move away from strip, only update every 8th frame + if(((SEGMENT.call & 0x07) == 0) && PartSys->particleFlags[i].outofbounds) { //check if out of bounds particle move away from strip, only update every 8th frame if((int32_t)PartSys->particles[i].vx * PartSys->particles[i].x > 0) PartSys->particles[i].ttl = 0; //particle is moving away, kill it } - PartSys->particles[i].perpetual = true; //particles do not age + PartSys->particleFlags[i].perpetual = true; //particles do not age if (SEGMENT.call % (32 / (1 + (SEGMENT.custom2 >> 3))) == 0) PartSys->particles[i].hue += 2 + (SEGMENT.custom2 >> 5); //note: updating speed on the fly is not accurately possible, since it is unknown which particles are assigned to which spot @@ -9403,7 +9400,7 @@ uint16_t mode_particleFireworks1D(void) { if (!initParticleSystem1D(PartSys, 4, 150, 4, true)) // init return mode_static(); // allocation failed or is single pixel PartSys->setKillOutOfBounds(true); - PartSys->sources[0].source.perpetual = 1; // set rocket state to standby + PartSys->sources[0].sourceFlags.perpetual = 1; // set rocket state to standby } else PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS @@ -9422,7 +9419,7 @@ uint16_t mode_particleFireworks1D(void) { else PartSys->setGravity(gravity); // set gravity - if(PartSys->sources[0].source.perpetual == 1) { // rocket is on standby + if(PartSys->sources[0].sourceFlags.custom1 == 1) { // rocket is on standby PartSys->sources[0].source.ttl--; if(PartSys->sources[0].source.ttl == 0) { // time is up, relaunch @@ -9431,7 +9428,7 @@ uint16_t mode_particleFireworks1D(void) { else SEGENV.aux0 = 0; - PartSys->sources[0].source.perpetual = 0; //flag abused for rocket state + PartSys->sources[0].sourceFlags.custom1 = 0; //flag used for rocket state PartSys->sources[0].source.hue = hw_random16(); PartSys->sources[0].var = 10; PartSys->sources[0].minLife = 100; @@ -9442,10 +9439,10 @@ uint16_t mode_particleFireworks1D(void) { PartSys->sources[0].source.ttl = 4000; PartSys->sources[0].sat = 30; // low saturation exhaust PartSys->sources[0].size = 0; // default size - PartSys->sources[0].source.reversegrav = false ; // normal gravity + PartSys->sources[0].sourceFlags.reversegrav = false ; // normal gravity if(SEGENV.aux0) { // inverted rockets launch from end - PartSys->sources[0].source.reversegrav = true; + PartSys->sources[0].sourceFlags.reversegrav = true; PartSys->sources[0].source.x = PartSys->maxX; // start from top PartSys->sources[0].source.vx = -PartSys->sources[0].source.vx; // revert direction } @@ -9458,16 +9455,16 @@ uint16_t mode_particleFireworks1D(void) { rocketgravity = -rocketgravity; speed = -speed; } - PartSys->applyForce(&PartSys->sources[0].source, rocketgravity, &forcecounter[0]); - PartSys->particleMoveUpdate(PartSys->sources[0].source); - PartSys->particleMoveUpdate(PartSys->sources[0].source); // increase speed by calling the move function twice, also ages twice + PartSys->applyForce(PartSys->sources[0].source, rocketgravity, forcecounter[0]); + PartSys->particleMoveUpdate(PartSys->sources[0].source, PartSys->sources[0].sourceFlags); + PartSys->particleMoveUpdate(PartSys->sources[0].source, PartSys->sources[0].sourceFlags); // increase speed by calling the move function twice, also ages twice uint32_t rocketheight = SEGENV.aux0 ? PartSys->maxX - PartSys->sources[0].source.x : PartSys->sources[0].source.x; if(speed < 0 && PartSys->sources[0].source.ttl > 50) // reached apogee PartSys->sources[0].source.ttl = min((uint32_t)50, rocketheight >> (PS_P_RADIUS_SHIFT_1D + 3)); // alive for a few more frames if(PartSys->sources[0].source.ttl < 2) { // explode - PartSys->sources[0].source.perpetual = 1; // set standby state + PartSys->sources[0].sourceFlags.custom1 = 1; // set standby state PartSys->sources[0].var = 5 + ((((PartSys->maxX >> 1) + rocketheight) * (200 + SEGMENT.intensity)) / (PartSys->maxX << 2)); // set explosion particle speed PartSys->sources[0].minLife = 600; PartSys->sources[0].maxLife = 1300; @@ -9481,14 +9478,9 @@ uint16_t mode_particleFireworks1D(void) { PartSys->sources[0].source.hue = hw_random16(); //random color for each particle PartSys->sprayEmit(PartSys->sources[0]); // emit a particle } - //!!! DEBUG, remove: - //PartSys->sources[0].source.x = -500; // set out of frame until relaunch - // for(unsigned i = 0; i < PartSys->usedParticles; i++) { // TODO: this can probably be removed now, was a bug patch - // Serial.println("particle " + String(i) + " ttl: " + String(PartSys->particles[i].ttl) + " x: " + String(PartSys->particles[i].x >> 5) + " vx: " + String(PartSys->particles[i].vx) + " sat: " + String(PartSys->advPartProps[i].sat) + " size: " + String(PartSys->advPartProps[i].size)); - // } } } - if((SEGMENT.call & 0x01) == 0 && PartSys->sources[0].source.perpetual == false) // every second frame and not in standby + if((SEGMENT.call & 0x01) == 0 && PartSys->sources[0].sourceFlags.perpetual == false) // every second frame and not in standby PartSys->sprayEmit(PartSys->sources[0]); // emit exhaust particle if((SEGMENT.call & 0x03) == 0) // every fourth frame PartSys->applyFriction(1); // apply friction to all particles @@ -9546,7 +9538,7 @@ uint16_t mode_particleSparkler(void) { PartSys->sources[i].source.vx = speed; //update speed, do not change direction PartSys->sources[i].source.ttl = 400; //replenish its life (setting it perpetual uses more code) PartSys->sources[i].sat = SEGMENT.custom1; //color saturation - PartSys->particleMoveUpdate(PartSys->sources[i].source, &sparklersettings); //move sparkler + PartSys->particleMoveUpdate(PartSys->sources[i].source, PartSys->sources[i].sourceFlags, &sparklersettings); //move sparkler } for(i = 0; i < PartSys->usedParticles; i++) { @@ -9613,7 +9605,7 @@ uint16_t mode_particleHourglass(void) { *basehue = hw_random16(); //choose new random color SEGENV.step = SEGMENT.intensity | (PartSys->getAvailableParticles() << 8); for(uint32_t i = 0; i < PartSys->usedParticles; i++) { - PartSys->particles[i].reversegrav = true; + PartSys->particleFlags[i].reversegrav = true; *direction = 0; SEGENV.aux1 = 1; //initialize below } @@ -9622,14 +9614,14 @@ uint16_t mode_particleHourglass(void) { for(uint32_t i = 0; i < PartSys->usedParticles; i++) { //check if particle reached target position after falling int32_t targetposition; - if (PartSys->particles[i].fixed == false) { + if (PartSys->particleFlags[i].fixed == false) { //calculate target position depending on direction - if(PartSys->particles[i].reversegrav) + if(PartSys->particleFlags[i].reversegrav) targetposition = PartSys->maxX - (i * PS_P_RADIUS_1D + positionoffset); // target resting position else targetposition = (PartSys->usedParticles - i) * PS_P_RADIUS_1D - positionoffset; // target resting position if(PartSys->particles[i].x == targetposition) //particle has reached target position, pin it. if not pinned, they do not stack well on larger piles - PartSys->particles[i].fixed = true; + PartSys->particleFlags[i].fixed = true; } if(colormode == 7) PartSys->setColorByPosition(true); //color fixed by position @@ -9646,24 +9638,24 @@ uint16_t mode_particleHourglass(void) { default: break; } } - if(SEGMENT.check1 && !PartSys->particles[i].reversegrav) // flip color when fallen + if(SEGMENT.check1 && !PartSys->particleFlags[i].reversegrav) // flip color when fallen PartSys->particles[i].hue += 120; } if(SEGENV.aux1 == 1) { //last countdown call before dropping starts, reset all particles for(uint32_t i = 0; i < PartSys->usedParticles; i++) { - PartSys->particles[i].collide = true; - PartSys->particles[i].perpetual = true; + PartSys->particleFlags[i].collide = true; + PartSys->particleFlags[i].perpetual = true; PartSys->particles[i].ttl = 260; uint32_t targetposition; //calculate target position depending on direction - if(PartSys->particles[i].reversegrav) + if(PartSys->particleFlags[i].reversegrav) targetposition = PartSys->maxX - (i * PS_P_RADIUS_1D + positionoffset); // target resting position else targetposition = (PartSys->usedParticles - i) * PS_P_RADIUS_1D - positionoffset; // target resting position -5 - PS_P_RADIUS_1D/2 PartSys->particles[i].x = targetposition; - PartSys->particles[i].fixed = true; + PartSys->particleFlags[i].fixed = true; } } @@ -9673,8 +9665,8 @@ uint16_t mode_particleHourglass(void) { interval = 3; if(SEGMENT.call % interval == 0) { //drop a particle, do not drop more often than every second frame or particles tangle up quite badly if(SEGENV.aux0 < PartSys->usedParticles) { - PartSys->particles[SEGENV.aux0].reversegrav = *direction; //let this particle fall or rise - PartSys->particles[SEGENV.aux0].fixed = false; // unpin + PartSys->particleFlags[SEGENV.aux0].reversegrav = *direction; //let this particle fall or rise + PartSys->particleFlags[SEGENV.aux0].fixed = false; // unpin } else { //overflow, flip direction *direction = !(*direction); @@ -9733,7 +9725,7 @@ uint16_t mode_particle1Dspray(void) { PartSys->sources[0].maxLife = 400; PartSys->sources[0].source.x = map(SEGMENT.custom1, 0 , 255, 0, PartSys->maxX); // spray position PartSys->sources[0].v = map(SEGMENT.speed, 0 , 255, -127 + PartSys->sources[0].var, 127 - PartSys->sources[0].var); // particle emit speed - PartSys->sources[0].source.reversegrav = gravity < 0 ? true : false; + PartSys->sources[0].sourceFlags.reversegrav = gravity < 0 ? true : false; if(hw_random() % (1 + ((255 - SEGMENT.intensity) >> 3)) == 0) PartSys->sprayEmit(PartSys->sources[0]); // emit a particle @@ -9742,7 +9734,7 @@ uint16_t mode_particle1Dspray(void) { PartSys->setColorByAge(SEGMENT.check1); // overruled by 'color by position' PartSys->setColorByPosition(SEGMENT.check3); for(uint i = 0; i < PartSys->usedParticles; i++) { - PartSys->particles[i].reversegrav = PartSys->sources[0].source.reversegrav; // update gravity direction + PartSys->particleFlags[i].reversegrav = PartSys->sources[0].sourceFlags.reversegrav; // update gravity direction } PartSys->update(); // update and render @@ -9764,7 +9756,7 @@ uint16_t mode_particleBalance(void) { return mode_static(); // allocation failed or is single pixel //PartSys->setKillOutOfBounds(true); PartSys->setParticleSize(1); - SEGENV.aux0 = 0; // + SEGENV.aux0 = 0; } else PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS @@ -9787,8 +9779,8 @@ uint16_t mode_particleBalance(void) { PartSys->particles[i].x = i * PS_P_RADIUS_1D; PartSys->particles[i].hue = (i * 1024) / PartSys->usedParticles; // multi gradient distribution PartSys->particles[i].ttl = 300; - PartSys->particles[i].perpetual = true; // TODO: is this a good idea? need to check how to handle it in transitions - PartSys->particles[i].collide = true; + PartSys->particleFlags[i].perpetual = true; // TODO: is this a good idea? need to check how to handle it in transitions + PartSys->particleFlags[i].collide = true; } } SEGENV.aux1 = PartSys->usedParticles; @@ -9961,7 +9953,7 @@ uint16_t mode_particleStarburst(void) { PartSys->sources[0].source.ttl = 10 + hw_random16(255 - SEGMENT.speed); PartSys->sources[0].size = SEGMENT.custom1; // Fragment size PartSys->setParticleSize(SEGMENT.custom1); // enable advanced size rendering - PartSys->sources[0].source.collide = SEGMENT.check3; + PartSys->sources[0].sourceFlags.collide = SEGMENT.check3; for (uint32_t e = 0; e < explosionsize; e++) { // emit particles if (SEGMENT.check2) PartSys->sources[0].source.hue = hw_random16(); //random color for each particle @@ -10189,7 +10181,7 @@ uint16_t mode_particle1Dsonicstream(void) { // particle manipulation for (uint32_t i = 0; i < PartSys->usedParticles; i++) { - if(PartSys->sources[0].source.perpetual == false) { // age faster if not perpetual + if(PartSys->sources[0].sourceFlags.perpetual == false) { // age faster if not perpetual if (PartSys->particles[i].ttl > 2) { PartSys->particles[i].ttl -= 2; //ttl is linked to brightness, this allows to use higher brightness but still a short lifespan } @@ -10215,7 +10207,7 @@ uint16_t mode_particle1Dsonicstream(void) { PartSys->update(); // update and render (needs to be done before manipulation for initial particle spacing to be right) if(SEGMENT.check3) { // push mode - PartSys->sources[0].source.perpetual = true; // emitted particles dont age + PartSys->sources[0].sourceFlags.perpetual = true; // emitted particles dont age PartSys->applyFriction(1); //slow down particles int32_t movestep = (((int)SEGMENT.speed + 2) * loudness) >> 10; if(movestep) { @@ -10228,12 +10220,12 @@ uint16_t mode_particle1Dsonicstream(void) { } } else { - PartSys->sources[0].source.perpetual = false; // emitted particles age + PartSys->sources[0].sourceFlags.perpetual = false; // emitted particles age //move all particles (again) to allow faster speeds for (uint32_t i = 0; i < PartSys->usedParticles; i++) { if (PartSys->particles[i].vx == 0) PartSys->particles[i].vx = PartSys->sources[0].v; // move static particles (after disabling push mode) - PartSys->particleMoveUpdate(PartSys->particles[i], nullptr, &PartSys->advPartProps[i]); + PartSys->particleMoveUpdate(PartSys->particles[i], PartSys->particleFlags[i], nullptr, &PartSys->advPartProps[i]); } } /* @@ -10466,7 +10458,7 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_2DCRAZYBEES, &mode_2Dcrazybees, _data_FX_MODE_2DCRAZYBEES); #ifndef DISABLE_2D_PS_REPLACEMENTS addEffect(FX_MODE_2DGHOSTRIDER, &mode_2Dghostrider, _data_FX_MODE_2DGHOSTRIDER); - addEffect(FX_MODE_2DBLOBS, &mode_2Dfloatingblobs, _data_FX_MODE_2DBLOBS); + //addEffect(FX_MODE_2DBLOBS, &mode_2Dfloatingblobs, _data_FX_MODE_2DBLOBS); #endif addEffect(FX_MODE_2DSCROLLTEXT, &mode_2Dscrollingtext, _data_FX_MODE_2DSCROLLTEXT); addEffect(FX_MODE_2DDRIFTROSE, &mode_2Ddriftrose, _data_FX_MODE_2DDRIFTROSE); @@ -10519,7 +10511,7 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_PARTICLEPERLIN, &mode_particleperlin, _data_FX_MODE_PARTICLEPERLIN); addEffect(FX_MODE_PARTICLEPIT, &mode_particlepit, _data_FX_MODE_PARTICLEPIT); addEffect(FX_MODE_PARTICLEBOX, &mode_particlebox, _data_FX_MODE_PARTICLEBOX); - addEffect(FX_MODE_PARTICLEATTRACTOR, &mode_particleattractor, _data_FX_MODE_PARTICLEATTRACTOR); + addEffect(FX_MODE_PARTICLEATTRACTOR, &mode_particleattractor, _data_FX_MODE_PARTICLEATTRACTOR); // 872 bytes addEffect(FX_MODE_PARTICLEIMPACT, &mode_particleimpact, _data_FX_MODE_PARTICLEIMPACT); addEffect(FX_MODE_PARTICLEWATERFALL, &mode_particlewaterfall, _data_FX_MODE_PARTICLEWATERFALL); addEffect(FX_MODE_PARTICLESPRAY, &mode_particlespray, _data_FX_MODE_PARTICLESPRAY); diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index 7898ac01fc..a5fbd41650 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -21,9 +21,9 @@ #ifndef WLED_DISABLE_PARTICLESYSTEM2D // local shared functions (used both in 1D and 2D system) -static int32_t calcForce_dv(int8_t force, uint8_t *counter); +static int32_t calcForce_dv(const int8_t force, uint8_t &counter); static int32_t limitSpeed(int32_t speed); -static bool checkBoundsAndWrap(int32_t &position, const int32_t max, const int32_t particleradius, bool wrap); // returns false if out of bounds by more than particleradius +static bool checkBoundsAndWrap(int32_t &position, const int32_t max, const int32_t particleradius, const bool wrap); // returns false if out of bounds by more than particleradius static void fast_color_add(CRGB &c1, const CRGB &c2, uint32_t scale = 255); // fast and accurate color adding with scaling (scales c2 before adding) static void fast_color_scale(CRGB &c, const uint32_t scale); // fast scaling function using 32bit variable and pointer. note: keep 'scale' within 0-255 //static CRGB *allocateCRGBbuffer(uint32_t length); @@ -86,21 +86,21 @@ void ParticleSystem2D::update(void) { handleCollisions(); //move all particles + //TODO: split this loop into two separate loops? avoids repeated null checking which is faster. + for (uint32_t i = 0; i < usedParticles; i++) { - if (advPartProps) { - advprop = &advPartProps[i]; - } - particleMoveUpdate(particles[i], &particlesettings, advprop); + particleMoveUpdate(particles[i], particleFlags[i], &particlesettings, advPartProps ? &advPartProps[i] : nullptr); } ParticleSys_render(); } // update function for fire animation -void ParticleSystem2D::updateFire(uint32_t intensity, bool renderonly) { +void ParticleSystem2D::updateFire(const uint8_t intensity,const bool renderonly) { if (!renderonly) fireParticleupdate(); - ParticleSys_render(true, intensity); + fireIntesity = intensity > 0 ? intensity : 1; // minimum of 1, zero checking is used in render function + ParticleSys_render(); } // set percentage of used particles as uint8_t i.e 127 means 50% for example @@ -117,6 +117,7 @@ void ParticleSystem2D::setUsedParticles(uint8_t percentage) { PSPRINTLN(usedParticles); } +//TODO: inline these functions void ParticleSystem2D::setWallHardness(uint8_t hardness) { wallHardness = hardness; } @@ -129,7 +130,7 @@ void ParticleSystem2D::setCollisionHardness(uint8_t hardness) { collisionHardness = (int)hardness + 1; } -void ParticleSystem2D::setMatrixSize(uint16_t x, uint16_t y) { +void ParticleSystem2D::setMatrixSize(uint32_t x, uint32_t y) { maxXpixel = x - 1; // last physical pixel that can be drawn to maxYpixel = y - 1; maxX = x * PS_P_RADIUS - 1; // particle system boundary for movements @@ -194,28 +195,26 @@ void ParticleSystem2D::enableParticleCollisions(bool enable, uint8_t hardness) { collisionHardness = (int)hardness + 1; } -// emit one particle with variation, returns index of last emitted particle (or -1 if no particle emitted) -int32_t ParticleSystem2D::sprayEmit(PSsource &emitter, uint32_t amount) { +// emit one particle with variation, returns index of emitted particle (or -1 if no particle emitted) +int32_t ParticleSystem2D::sprayEmit(const PSsource &emitter) { bool success = false; - for (uint32_t a = 0; a < amount; a++) { - for (uint32_t i = 0; i < usedParticles; i++) { - emitIndex++; - if (emitIndex >= usedParticles) - emitIndex = 0; - if (particles[emitIndex].ttl == 0) { // find a dead particle - success = true; - particles[emitIndex].vx = emitter.vx + hw_random16(emitter.var << 1) - emitter.var; // random(-var, var) - particles[emitIndex].vy = emitter.vy + hw_random16(emitter.var << 1) - emitter.var; // random(-var, var) - particles[emitIndex].x = emitter.source.x; - particles[emitIndex].y = emitter.source.y; - particles[emitIndex].hue = emitter.source.hue; - particles[emitIndex].sat = emitter.source.sat; - particles[emitIndex].collide = emitter.source.collide; - particles[emitIndex].ttl = hw_random16(emitter.minLife, emitter.maxLife); - if (advPartProps) - advPartProps[emitIndex].size = emitter.size; - break; - } + for (uint32_t i = 0; i < usedParticles; i++) { + emitIndex++; + if (emitIndex >= usedParticles) + emitIndex = 0; + if (particles[emitIndex].ttl == 0) { // find a dead particle + success = true; + particles[emitIndex].vx = emitter.vx + hw_random16(emitter.var << 1) - emitter.var; // random(-var, var) + particles[emitIndex].vy = emitter.vy + hw_random16(emitter.var << 1) - emitter.var; // random(-var, var) + particles[emitIndex].x = emitter.source.x; + particles[emitIndex].y = emitter.source.y; + particles[emitIndex].hue = emitter.source.hue; + particles[emitIndex].sat = emitter.source.sat; + particleFlags[emitIndex].collide = emitter.sourceFlags.collide; + particles[emitIndex].ttl = hw_random16(emitter.minLife, emitter.maxLife); + if (advPartProps) + advPartProps[emitIndex].size = emitter.size; + break; } } if (success) @@ -225,27 +224,27 @@ int32_t ParticleSystem2D::sprayEmit(PSsource &emitter, uint32_t amount) { } // Spray emitter for particles used for flames (particle TTL depends on source TTL) -void ParticleSystem2D::flameEmit(PSsource &emitter) { +void ParticleSystem2D::flameEmit(const PSsource &emitter) { int emitIndex = sprayEmit(emitter); - if(emitIndex > 0) particles[emitIndex].ttl += emitter.source.ttl; + if(emitIndex > 0) particles[emitIndex].ttl += emitter.source.ttl; } // Emits a particle at given angle and speed, angle is from 0-65535 (=0-360deg), speed is also affected by emitter->var // angle = 0 means in positive x-direction (i.e. to the right) -int32_t ParticleSystem2D::angleEmit(PSsource &emitter, uint16_t angle, int32_t speed, uint32_t amount) { +int32_t ParticleSystem2D::angleEmit(PSsource &emitter, const uint16_t angle, const int32_t speed) { emitter.vx = ((int32_t)cos16_t(angle) * speed) / (int32_t)32600; // cos16_t() and sin16_t() return signed 16bit, division should be 32767 but 32600 gives slightly better rounding emitter.vy = ((int32_t)sin16_t(angle) * speed) / (int32_t)32600; // note: cannot use bit shifts as bit shifting is asymmetrical for positive and negative numbers and this needs to be accurate! - return sprayEmit(emitter, amount); + return 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 enabled, it will never bounce at the top and killoutofbounds is not applied over the top -void ParticleSystem2D::particleMoveUpdate(PSparticle &part, PSsettings2D *options, PSadvancedParticle *advancedproperties) { +void ParticleSystem2D::particleMoveUpdate(PSparticle &part, PSparticleFlags &partFlags, PSsettings2D *options, PSadvancedParticle *advancedproperties) { if (options == NULL) options = &particlesettings; //use PS system settings by default if (part.ttl > 0) { - if (!part.perpetual) + if (!partFlags.perpetual) part.ttl--; // age if (options->colorByAge) part.hue = min(part.ttl, (uint16_t)255); //set color to ttl @@ -253,7 +252,7 @@ void ParticleSystem2D::particleMoveUpdate(PSparticle &part, PSsettings2D *option int32_t renderradius = PS_P_HALFRADIUS; // used to check out of bounds int32_t newX = part.x + (int32_t)part.vx; int32_t newY = part.y + (int32_t)part.vy; - part.outofbounds = false; // reset out of bounds (in case particle was created outside the matrix and is now moving into view) + partFlags.outofbounds = false; // reset out of bounds (in case particle was created outside the matrix and is now moving into view) TODO: move this below, setting a flag is slow, only set if actually in bounds if (advancedproperties) { //using individual particle size? if (advancedproperties->size > 0) { @@ -269,7 +268,7 @@ void ParticleSystem2D::particleMoveUpdate(PSparticle &part, PSsettings2D *option } if(!checkBoundsAndWrap(newY, maxY, renderradius, options->wrapY)) { // check out of bounds note: this must not be skipped, if gravity is enabled, particles will never bounce at the top - part.outofbounds = true; + partFlags.outofbounds = true; if (options->killoutofbounds) { if (newY < 0) // if gravity is enabled, only kill particles below ground part.ttl = 0; @@ -284,7 +283,7 @@ void ParticleSystem2D::particleMoveUpdate(PSparticle &part, PSsettings2D *option bounce(part.vx, part.vy, newX, maxX); } else if(!checkBoundsAndWrap(newX, maxX, renderradius, options->wrapX)) { // check out of bounds TODO: not checking out of bounds when bounce is enabled used to lead to crashes, seems fixed now. test more. - part.outofbounds = true; + partFlags.outofbounds = true; if (options->killoutofbounds) part.ttl = 0; } @@ -302,16 +301,16 @@ void ParticleSystem2D::fireParticleupdate() { { particles[i].ttl--; // age int32_t newY = particles[i].y + (int32_t)particles[i].vy + (particles[i].ttl >> 2); // younger particles move faster upward as they are hotter - particles[i].outofbounds = 0; // reset out of bounds flag + int32_t newX = particles[i].x + (int32_t)particles[i].vx; + particleFlags[i].outofbounds = 0; // reset out of bounds flag //TODO: can this be moved to else statements below? // 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 speed, only check x direction if y is not out of bounds if (newY < -PS_P_HALFRADIUS) - particles[i].outofbounds = 1; + particleFlags[i].outofbounds = 1; else if (newY > int32_t(maxY + PS_P_HALFRADIUS)) // particle moved out at the top particles[i].ttl = 0; else // particle is in frame in y direction, also check x direction now Note: using checkBoundsAndWrap() is slower, only saves a few bytes { - int32_t newX = particles[i].x + (int32_t)particles[i].vx; if ((newX < 0) || (newX > (int32_t)maxX)) { // handle out of bounds & wrap if (particlesettings.wrapX) { newX = newX % (maxX + 1); @@ -409,7 +408,7 @@ void ParticleSystem2D::getParticleXYsize(PSadvancedParticle *advprops, PSsizeCon } // function to bounce a particle from a wall using set parameters (wallHardness and wallRoughness) -void ParticleSystem2D::bounce(int8_t &incomingspeed, int8_t ¶llelspeed, int32_t &position, uint16_t maxposition) { +void ParticleSystem2D::bounce(int8_t &incomingspeed, int8_t ¶llelspeed, int32_t &position, const uint32_t maxposition) { incomingspeed = -incomingspeed; incomingspeed = (incomingspeed * wallHardness) / 255; // reduce speed as energy is lost on non-hard surface if (position < (int32_t)particleHardRadius) @@ -431,40 +430,40 @@ void ParticleSystem2D::bounce(int8_t &incomingspeed, int8_t ¶llelspeed, int3 // apply a force in x,y direction to individual particle // caller needs to provide a 8bit counter (for each particle) that holds its value between calls // 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 ParticleSystem2D::applyForce(PSparticle *part, int8_t xforce, int8_t yforce, uint8_t *counter) { +void ParticleSystem2D::applyForce(PSparticle &part, const int8_t xforce, const 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 + 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); + 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 + 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 - part->vx = limitSpeed((int32_t)part->vx + dvx); - part->vy = limitSpeed((int32_t)part->vy + dvy); + part.vx = limitSpeed((int32_t)part.vx + dvx); + part.vy = limitSpeed((int32_t)part.vy + dvy); } // apply a force in x,y direction to individual particle using advanced particle properties -void ParticleSystem2D::applyForce(uint16_t particleindex, int8_t xforce, int8_t yforce) { +void ParticleSystem2D::applyForce(const uint32_t particleindex, const int8_t xforce, const int8_t yforce) { if (advPartProps == NULL) return; // no advanced properties available - applyForce(&particles[particleindex], xforce, yforce, &advPartProps[particleindex].forcecounter); + applyForce(particles[particleindex], xforce, yforce, advPartProps[particleindex].forcecounter); } // apply a force in x,y direction to all particles // force is in 3.4 fixed point notation (see above) -void ParticleSystem2D::applyForce(int8_t xforce, int8_t yforce) { +void ParticleSystem2D::applyForce(const int8_t xforce, const int8_t yforce) { // for small forces, need to use a delay counter uint8_t tempcounter; // note: this is not the most computationally efficient way to do this, but it saves on duplicate code and is fast enough for (uint32_t i = 0; i < usedParticles; i++) { tempcounter = forcecounter; - applyForce(&particles[i], xforce, yforce, &tempcounter); + applyForce(particles[i], xforce, yforce, tempcounter); } forcecounter = tempcounter; // save value back } @@ -473,21 +472,21 @@ void ParticleSystem2D::applyForce(int8_t xforce, int8_t yforce) { // caller needs to provide a 8bit counter that holds its value between calls (if using single particles, a counter for each particle is needed) // angle is from 0-65535 (=0-360deg) angle = 0 means in positive x-direction (i.e. to the right) // force is in 3.4 fixed point notation so force=16 means apply v+1 each frame (useful force range is +/- 127) -void ParticleSystem2D::applyAngleForce(PSparticle *part, int8_t force, uint16_t angle, uint8_t *counter) { +void ParticleSystem2D::applyAngleForce(PSparticle &part, const int8_t force, const uint16_t angle, uint8_t &counter) { int8_t xforce = ((int32_t)force * cos16_t(angle)) / 32767; // force is +/- 127 int8_t yforce = ((int32_t)force * sin16_t(angle)) / 32767; // note: cannot use bit shifts as bit shifting is asymmetrical for positive and negative numbers and this needs to be accurate! applyForce(part, xforce, yforce, counter); } -void ParticleSystem2D::applyAngleForce(uint16_t particleindex, int8_t force, uint16_t angle) { +void ParticleSystem2D::applyAngleForce(const uint32_t particleindex, const int8_t force, const uint16_t angle) { if (advPartProps == NULL) return; // no advanced properties available - applyAngleForce(&particles[particleindex], force, angle, &advPartProps[particleindex].forcecounter); + applyAngleForce(particles[particleindex], force, angle, advPartProps[particleindex].forcecounter); } // apply a force in angular direction to all particles // angle is from 0-65535 (=0-360deg) angle = 0 means in positive x-direction (i.e. to the right) -void ParticleSystem2D::applyAngleForce(int8_t force, uint16_t angle) { +void ParticleSystem2D::applyAngleForce(const int8_t force, const uint16_t angle) { int8_t xforce = ((int32_t)force * cos16_t(angle)) / 32767; // force is +/- 127 int8_t yforce = ((int32_t)force * sin16_t(angle)) / 32767; // note: cannot use bit shifts as bit shifting is asymmetrical for positive and negative numbers and this needs to be accurate! applyForce(xforce, yforce); @@ -497,7 +496,7 @@ void ParticleSystem2D::applyAngleForce(int8_t force, uint16_t angle) { // force is in 3.4 fixed point notation, see note above // note: faster than apply force since direction is always down and counter is fixed for all particles void ParticleSystem2D::applyGravity() { - int32_t dv = calcForce_dv(gforce, &gforcecounter); + int32_t dv = calcForce_dv(gforce, gforcecounter); if(dv == 0) return; for (uint32_t i = 0; i < usedParticles; i++) { // Note: not checking if particle is dead is faster as most are usually alive and if few are alive, rendering is fast anyways @@ -509,37 +508,39 @@ void ParticleSystem2D::applyGravity() { // function does not increment gravity counter, if gravity setting is disabled, this cannot be used void ParticleSystem2D::applyGravity(PSparticle &part) { uint32_t counterbkp = gforcecounter; // backup PS gravity counter - int32_t dv = calcForce_dv(gforce, &gforcecounter); + int32_t dv = calcForce_dv(gforce, gforcecounter); gforcecounter = counterbkp; //save it back part.vy = limitSpeed((int32_t)part.vy - dv); } // slow down particle by friction, the higher the speed, the higher the friction. a high friction coefficient slows them more (255 means instant stop) // note: a coefficient smaller than 0 will speed them up (this is a feature, not a bug), coefficient larger than 255 inverts the speed, so don't do that -void ParticleSystem2D::applyFriction(PSparticle *part, int32_t coefficient) { +void ParticleSystem2D::applyFriction(PSparticle &part, const int32_t coefficient) { int32_t friction = 255 - coefficient; // note: not checking if particle is dead can be done by caller (or can be omitted) // note2: cannot use right shifts as bit shifting in right direction is asymmetrical for positive and negative numbers and this needs to be accurate - part->vx = ((int32_t)part->vx * friction) / 255; - part->vy = ((int32_t)part->vy * friction) / 255; + part.vx = ((int32_t)part.vx * friction) / 255; + part.vy = ((int32_t)part.vy * friction) / 255; } // apply friction to all particles -void ParticleSystem2D::applyFriction(int32_t coefficient) { +void ParticleSystem2D::applyFriction(const int32_t coefficient) { + int32_t friction = 255 - coefficient; for (uint32_t i = 0; i < usedParticles; i++) { - if (particles[i].ttl) - applyFriction(&particles[i], coefficient); + // 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].vx = ((int32_t)particles[i].vx * friction) / 255; + particles[i].vy = ((int32_t)particles[i].vy * friction) / 255; } } // attracts a particle to an attractor particle using the inverse square-law -void ParticleSystem2D::pointAttractor(uint16_t particleindex, PSparticle *attractor, uint8_t strength, bool swallow) { +void ParticleSystem2D::pointAttractor(const uint32_t particleindex, PSparticle &attractor, const uint8_t strength, const bool swallow) { if (advPartProps == NULL) return; // no advanced properties available // Calculate the distance between the particle and the attractor - int32_t dx = attractor->x - particles[particleindex].x; - int32_t dy = attractor->y - particles[particleindex].y; + int32_t dx = attractor.x - particles[particleindex].x; + int32_t dy = attractor.y - particles[particleindex].y; // Calculate the force based on inverse square law int32_t distanceSquared = dx * dx + dy * dy; @@ -565,7 +566,7 @@ void ParticleSystem2D::pointAttractor(uint16_t particleindex, PSparticle *attrac // if wrap is set, particles half out of bounds are rendered to the other side of the matrix // warning: do not render out of bounds particles or system will crash! rendering does not check if particle is out of bounds // firemode is only used for PS Fire FX -void ParticleSystem2D::ParticleSys_render(bool firemode, uint32_t fireintensity) { +void ParticleSystem2D::ParticleSys_render() { CRGB baseRGB; uint32_t brightness; // particle brightness, fades if dying static bool useAdditiveTransfer = false; // use add instead of set for buffer transferring @@ -643,11 +644,11 @@ void ParticleSystem2D::ParticleSys_render(bool firemode, uint32_t fireintensity) bool wrapY = particlesettings.wrapY; // go over particles and render them to the buffer for (uint32_t i = 0; i < usedParticles; i++) { - if (particles[i].outofbounds || particles[i].ttl == 0) + if (particles[i].ttl == 0 || particleFlags[i].outofbounds) continue; // generate RGB values for particle - if (firemode) { - brightness = (uint32_t)particles[i].ttl * (3 + (fireintensity >> 5)) + 20; + if (fireIntesity) { + brightness = (uint32_t)particles[i].ttl * (3 + (fireIntesity >> 5)) + 20; brightness = min(brightness, (uint32_t)255); baseRGB = ColorFromPalette(SEGPALETTE, brightness, 255); } @@ -861,7 +862,7 @@ void ParticleSystem2D::handleCollisions() { // fill the binIndices array for this bin for (uint32_t i = collisionStartIdx; i < usedParticles; i++) { - if (particles[i].ttl > 0 && particles[i].outofbounds == 0 && particles[i].collide) { // colliding particle + if (particles[i].ttl > 0 && particleFlags[i].outofbounds == 0 && particleFlags[i].collide) { // colliding particle if (particles[i].x >= binStart && particles[i].x <= binEnd) { // >= and <= to include particles on the edge of the bin (overlap to ensure boarder particles collide with adjacent bins) if (binParticleCount >= maxBinParticles) { // bin is full, more particles in this bin so do the rest next frame nextFrameStartIdx = i; // bin overflow can only happen once as bin size is at least half of the particles (or half +1) @@ -884,7 +885,7 @@ void ParticleSystem2D::handleCollisions() { if (dx * dx < collDistSq) { // check x direction, if close, check y direction (squaring is faster than abs() or dual compare) int32_t dy = particles[idx_j].y - particles[idx_i].y; if (dy * dy < collDistSq) // particles are close - collideParticles(&particles[idx_i], &particles[idx_j], dx, dy); + collideParticles(particles[idx_i], particles[idx_j], dx, dy); } } } @@ -894,11 +895,11 @@ void ParticleSystem2D::handleCollisions() { // handle a collision if close proximity is detected, i.e. dx and/or dy smaller than 2*PS_P_RADIUS // takes two pointers to the particles to collide and the particle hardness (softer means more energy lost in collision, 255 means full hard) -void ParticleSystem2D::collideParticles(PSparticle *particle1, PSparticle *particle2, int32_t dx, int32_t dy) { // TODO: dx,dy is calculated just above, can pass it over here to save a few CPU cycles? +void ParticleSystem2D::collideParticles(PSparticle &particle1, PSparticle &particle2, int32_t dx, int32_t dy) { int32_t distanceSquared = dx * dx + dy * dy; // Calculate relative velocity (if it is zero, could exit but extra check does not overall speed but deminish it) - int32_t relativeVx = (int32_t)particle2->vx - (int32_t)particle1->vx; - int32_t relativeVy = (int32_t)particle2->vy - (int32_t)particle1->vy; + int32_t relativeVx = (int32_t)particle2.vx - (int32_t)particle1.vx; + int32_t relativeVy = (int32_t)particle2.vy - (int32_t)particle1.vy; // if dx and dy are zero (i.e. same position) give them an offset, if speeds are also zero, also offset them (pushes particles apart if they are clumped before enabling collisions) if (distanceSquared == 0) { @@ -930,18 +931,18 @@ void ParticleSystem2D::collideParticles(PSparticle *particle1, PSparticle *parti int32_t impulse = -(((((-dotProduct) << 15) / distanceSquared) * surfacehardness) >> 8); // note: inverting before bitshift corrects for asymmetry in right-shifts (and is slightly faster) int32_t ximpulse = ((impulse) * dx) / 32767; // cannot use bit shifts here, it can be negative, use division by 2^bitshift int32_t yimpulse = ((impulse) * dy) / 32767; - particle1->vx += ximpulse; - particle1->vy += yimpulse; - particle2->vx -= ximpulse; - particle2->vy -= yimpulse; + particle1.vx += ximpulse; + particle1.vy += yimpulse; + particle2.vx -= ximpulse; + particle2.vy -= yimpulse; if (collisionHardness < surfacehardness && (SEGMENT.call & 0x03) == 0) { // if particles are soft, they become 'sticky' i.e. apply some friction (they do pile more nicely and stop sloshing around) const uint32_t coeff = collisionHardness + (255 - PS_P_MINSURFACEHARDNESS); // Note: could call applyFriction, but this is faster and speed is key here - particle1->vx = ((int32_t)particle1->vx * coeff) / 255; - particle1->vy = ((int32_t)particle1->vy * coeff) / 255; + particle1.vx = ((int32_t)particle1.vx * coeff) / 255; + particle1.vy = ((int32_t)particle1.vy * coeff) / 255; - particle2->vx = ((int32_t)particle2->vx * coeff) / 255; - particle2->vy = ((int32_t)particle2->vy * coeff) / 255; + particle2.vx = ((int32_t)particle2.vx * coeff) / 255; + particle2.vy = ((int32_t)particle2.vy * coeff) / 255; } // particles have volume, push particles apart if they are too close @@ -957,11 +958,11 @@ void ParticleSystem2D::collideParticles(PSparticle *particle1, PSparticle *parti push = -pushamount; else { // on the same x coordinate, shift it a little so they do not stack if (notsorandom) - particle1->x++; // move it so pile collapses + particle1.x++; // move it so pile collapses else - particle1->x--; + particle1.x--; } - particle1->vx += push; //TODO: what happens if particle2 is also pushed? in 1D it stacks better, maybe also just reverse the comparison order so they flip roles? + particle1.vx += push; //TODO: what happens if particle2 is also pushed? in 1D it stacks better, maybe also just reverse the comparison order so they flip roles? push = 0; if (dy < 0) push = pushamount; @@ -969,20 +970,20 @@ void ParticleSystem2D::collideParticles(PSparticle *particle1, PSparticle *parti push = -pushamount; else { // dy==0 if (notsorandom) - particle1->y++; // move it so pile collapses + particle1.y++; // move it so pile collapses else - particle1->y--; + particle1.y--; } - particle1->vy += push; + particle1.vy += push; // note: pushing may push particles out of frame, if bounce is active, it will move it back as position will be limited to within frame, if bounce is disabled: bye bye if (collisionHardness < 16) { // if they are very soft, stop slow particles completely to make them stick to each other - particle1->vx = 0; - particle1->vy = 0; - particle2->vx = 0; - particle2->vy = 0; + particle1.vx = 0; + particle1.vy = 0; + particle2.vx = 0; + particle2.vy = 0; //push them apart - particle1->x += push; - particle1->y += push; + particle1.x += push; + particle1.y += push; } } } @@ -1007,7 +1008,7 @@ void ParticleSystem2D::updateSystem(void) { // 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 ParticleSystem2D::updatePSpointers(bool isadvanced, bool sizecontrol) { -PSPRINTLN("updatePSpointers"); + PSPRINTLN("updatePSpointers"); // DEBUG_PRINT(F("*** PS pointers ***")); // DEBUG_PRINTF_P(PSTR("this PS %p "), this); // Note on memory alignment: @@ -1018,7 +1019,8 @@ PSPRINTLN("updatePSpointers"); // memory manager needs to know how many particles the FX wants to use so transitions can be handled properly (i.e. pointer will stop changing if enough particles are available during transitions) uint32_t usedByFX = (numParticles * ((uint32_t)fractionOfParticlesUsed + 1)) >> 8; // final number of particles the FX wants to use (fractionOfParticlesUsed is 0-255) particles = reinterpret_cast(particleMemoryManager(0, sizeof(PSparticle), availableParticles, usedByFX, effectID)); // get memory, leave buffer size as is (request 0) - sources = reinterpret_cast(this + 1); // pointer to source(s) at data+sizeof(ParticleSystem2D) + particleFlags = reinterpret_cast(this + 1); // pointer to particle flags + sources = reinterpret_cast(particleFlags + numParticles); // pointer to source(s) at data+sizeof(ParticleSystem2D) PSdataEnd = reinterpret_cast(sources + numSources); // pointer to first available byte after the PS for FX additional data if (isadvanced) { advPartProps = reinterpret_cast(sources + numSources); @@ -1091,7 +1093,7 @@ void blur2D(CRGB *colorbuffer, uint32_t xsize, uint32_t ysize, uint32_t xblur, u } //non class functions to use for initialization -uint32_t calculateNumberOfParticles2D(uint32_t pixels, bool isadvanced, bool sizecontrol) { +uint32_t calculateNumberOfParticles2D(uint32_t const pixels, const bool isadvanced, const bool sizecontrol) { uint32_t numberofParticles = pixels; // 1 particle per pixel (for example 512 particles on 32x16) #ifdef ESP8266 uint32_t particlelimit = ESP8266_MAXPARTICLES; // maximum number of paticles allowed (based on one segment of 16x16 and 4k effect ram) @@ -1136,6 +1138,7 @@ bool allocateParticleSystemMemory2D(uint32_t numparticles, uint32_t numsources, return false; // not enough memory, function ensures a minimum of numparticles are available // functions above make sure these are a multiple of 4 bytes (to avoid alignment issues) + requiredmemory += sizeof(PSparticleFlags) * numparticles; if (isadvanced) requiredmemory += sizeof(PSadvancedParticle) * numparticles; if (sizecontrol) @@ -1234,9 +1237,7 @@ void ParticleSystem1D::update(void) { //move all particles for (uint32_t i = 0; i < usedParticles; i++) { - if (advPartProps) - advprop = &advPartProps[i]; - particleMoveUpdate(particles[i], &particlesettings, advprop); + particleMoveUpdate(particles[i], particleFlags[i], &particlesettings, advPartProps ? &advPartProps[i] : nullptr); } if (particlesettings.colorByPosition) { @@ -1257,7 +1258,7 @@ void ParticleSystem1D::update(void) { } // set percentage of used particles as uint8_t i.e 127 means 50% for example -void ParticleSystem1D::setUsedParticles(uint8_t percentage) { +void ParticleSystem1D::setUsedParticles(const uint8_t percentage) { fractionOfParticlesUsed = percentage; // note usedParticles is updated in memory manager updateUsedParticles(numParticles, availableParticles, fractionOfParticlesUsed, usedParticles); PSPRINT(" SetUsedpaticles: allocated particles: "); @@ -1270,45 +1271,45 @@ void ParticleSystem1D::setUsedParticles(uint8_t percentage) { PSPRINTLN(usedParticles); } -void ParticleSystem1D::setWallHardness(uint8_t hardness) { +void ParticleSystem1D::setWallHardness(const uint8_t hardness) { wallHardness = hardness; } -void ParticleSystem1D::setSize(uint16_t x) { +void ParticleSystem1D::setSize(const uint32_t x) { maxXpixel = x - 1; // last physical pixel that can be drawn to maxX = x * PS_P_RADIUS_1D - 1; // particle system boundary for movements } -void ParticleSystem1D::setWrap(bool enable) { +void ParticleSystem1D::setWrap(const bool enable) { particlesettings.wrap = enable; } -void ParticleSystem1D::setBounce(bool enable) { +void ParticleSystem1D::setBounce(const bool enable) { particlesettings.bounce = enable; } -void ParticleSystem1D::setKillOutOfBounds(bool enable) { +void ParticleSystem1D::setKillOutOfBounds(const bool enable) { particlesettings.killoutofbounds = enable; } -void ParticleSystem1D::setColorByAge(bool enable) { +void ParticleSystem1D::setColorByAge(const bool enable) { particlesettings.colorByAge = enable; } -void ParticleSystem1D::setColorByPosition(bool enable) { +void ParticleSystem1D::setColorByPosition(const bool enable) { particlesettings.colorByPosition = enable; } -void ParticleSystem1D::setMotionBlur(uint8_t bluramount) { +void ParticleSystem1D::setMotionBlur(const uint8_t bluramount) { motionBlur = bluramount; } -void ParticleSystem1D::setSmearBlur(uint8_t bluramount) { +void ParticleSystem1D::setSmearBlur(const uint8_t bluramount) { smearBlur = bluramount; } // render size, 0 = 1 pixel, 1 = 2 pixel (interpolated), bigger sizes require adanced properties -void ParticleSystem1D::setParticleSize(uint8_t size) { +void ParticleSystem1D::setParticleSize(const uint8_t size) { particlesize = size > 0 ? 1 : 0; // TODO: add support for global sizes? see not abover (motion blur) if (particlesize) particleHardRadius = PS_P_MINHARDRADIUS_1D; // 2 pixel sized particles @@ -1319,7 +1320,7 @@ void ParticleSystem1D::setParticleSize(uint8_t size) { // enable/disable gravity, optionally, set the force (force=8 is default) can be -127 to +127, 0 is disable // if enabled, gravity is applied to all particles in ParticleSystemUpdate() // 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 ParticleSystem1D::setGravity(int8_t force) { +void ParticleSystem1D::setGravity(const int8_t force) { if (force) { gforce = force; particlesettings.useGravity = true; @@ -1328,13 +1329,13 @@ void ParticleSystem1D::setGravity(int8_t force) { particlesettings.useGravity = false; } -void ParticleSystem1D::enableParticleCollisions(bool enable, uint8_t hardness) { +void ParticleSystem1D::enableParticleCollisions(const bool enable, const uint8_t hardness) { particlesettings.useCollisions = enable; collisionHardness = hardness; } // emit one particle with variation, returns index of last emitted particle (or -1 if no particle emitted) -int32_t ParticleSystem1D::sprayEmit(PSsource1D &emitter) { +int32_t ParticleSystem1D::sprayEmit(const PSsource1D &emitter) { for (uint32_t i = 0; i < usedParticles; i++) { emitIndex++; if (emitIndex >= usedParticles) @@ -1343,10 +1344,10 @@ int32_t ParticleSystem1D::sprayEmit(PSsource1D &emitter) { particles[emitIndex].vx = emitter.v + hw_random16(emitter.var << 1) - emitter.var; // random(-var,var) particles[emitIndex].x = emitter.source.x; particles[emitIndex].hue = emitter.source.hue; - particles[emitIndex].collide = emitter.source.collide; - particles[emitIndex].reversegrav = emitter.source.reversegrav; particles[emitIndex].ttl = hw_random16(emitter.minLife, emitter.maxLife); - particles[emitIndex].perpetual = emitter.source.perpetual; + particleFlags[emitIndex].collide = emitter.sourceFlags.collide; + particleFlags[emitIndex].reversegrav = emitter.sourceFlags.reversegrav; + particleFlags[emitIndex].perpetual = emitter.sourceFlags.perpetual; if (advPartProps) { advPartProps[emitIndex].sat = emitter.sat; advPartProps[emitIndex].size = emitter.size; @@ -1359,19 +1360,19 @@ int32_t ParticleSystem1D::sprayEmit(PSsource1D &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 and killoutofbounds is not applied over the top -void ParticleSystem1D::particleMoveUpdate(PSparticle1D &part, PSsettings1D *options, PSadvancedParticle1D *advancedproperties) { +void ParticleSystem1D::particleMoveUpdate(PSparticle1D &part, PSparticleFlags1D &partFlags, PSsettings1D *options, PSadvancedParticle1D *advancedproperties) { if (options == NULL) options = &particlesettings; // use PS system settings by default if (part.ttl > 0) { - if (!part.perpetual) + if (!partFlags.perpetual) part.ttl--; // age if (options->colorByAge) part.hue = min(part.ttl, (uint16_t)255); // set color to ttl int32_t renderradius = PS_P_HALFRADIUS_1D; // used to check out of bounds, default for 2 pixel rendering int32_t newX = part.x + (int32_t)part.vx; - part.outofbounds = false; // reset out of bounds (in case particle was created outside the matrix and is now moving into view) + partFlags.outofbounds = false; // reset out of bounds (in case particle was created outside the matrix and is now moving into view) if (advancedproperties) { // using individual particle size? if (advancedproperties->size > 1) @@ -1386,7 +1387,7 @@ void ParticleSystem1D::particleMoveUpdate(PSparticle1D &part, PSsettings1D *opti if ((newX < (int32_t)particleHardRadius) || ((newX > (int32_t)(maxX - particleHardRadius)))) { // reached a wall bool bouncethis = true; if (options->useGravity) { - if (part.reversegrav) { // skip bouncing at x = 0 + if (partFlags.reversegrav) { // skip bouncing at x = 0 if (newX < (int32_t)particleHardRadius) bouncethis = false; } else if (newX > (int32_t)particleHardRadius) { // skip bouncing at x = max @@ -1405,11 +1406,11 @@ void ParticleSystem1D::particleMoveUpdate(PSparticle1D &part, PSsettings1D *opti } if (!checkBoundsAndWrap(newX, maxX, renderradius, options->wrap)) { // check out of bounds note: this must not be skipped or it can lead to crashes - part.outofbounds = true; + partFlags.outofbounds = true; if (options->killoutofbounds) { bool killthis = true; if (options->useGravity) { // if gravity is used, only kill below 'floor level' - if (part.reversegrav) { // skip at x = 0, do not skip far out of bounds + if (partFlags.reversegrav) { // skip at x = 0, do not skip far out of bounds if (newX < 0 || newX > maxX << 2) killthis = false; } else { // skip at x = max, do not skip far out of bounds @@ -1422,7 +1423,7 @@ void ParticleSystem1D::particleMoveUpdate(PSparticle1D &part, PSsettings1D *opti } } - if (!part.fixed) + if (!partFlags.fixed) part.x = newX; // set new position else part.vx = 0; // set speed to zero. note: particle can get speed in collisions, if unfixed, it should not speed away @@ -1432,15 +1433,15 @@ void ParticleSystem1D::particleMoveUpdate(PSparticle1D &part, PSsettings1D *opti // apply a force in x direction to individual particle (or source) // caller needs to provide a 8bit counter (for each paticle) that holds its value between calls // force is in 3.4 fixed point notation so force=16 means apply v+1 each frame default of 8 is every other frame -void ParticleSystem1D::applyForce(PSparticle1D *part, int8_t xforce, uint8_t *counter) { +void ParticleSystem1D::applyForce(PSparticle1D &part, const int8_t xforce, uint8_t &counter) { int32_t dv = calcForce_dv(xforce, counter); // velocity increase - part->vx = limitSpeed((int32_t)part->vx + dv); // apply the force to particle + part.vx = limitSpeed((int32_t)part.vx + dv); // apply the force to particle } // apply a force to all particles // force is in 3.4 fixed point notation (see above) -void ParticleSystem1D::applyForce(int8_t xforce) { - int32_t dv = calcForce_dv(xforce, &forcecounter); // velocity increase +void ParticleSystem1D::applyForce(const int8_t xforce) { + int32_t dv = calcForce_dv(xforce, forcecounter); // velocity increase for (uint32_t i = 0; i < usedParticles; i++) { particles[i].vx = limitSpeed((int32_t)particles[i].vx + dv); } @@ -1449,10 +1450,10 @@ void ParticleSystem1D::applyForce(int8_t xforce) { // apply gravity to all particles using PS global gforce setting // gforce is in 3.4 fixed point notation, see note above void ParticleSystem1D::applyGravity() { - int32_t dv_raw = calcForce_dv(gforce, &gforcecounter); + int32_t dv_raw = calcForce_dv(gforce, gforcecounter); for (uint32_t i = 0; i < usedParticles; i++) { int32_t dv = dv_raw; - if (particles[i].reversegrav) dv = -dv_raw; + if (particleFlags[i].reversegrav) dv = -dv_raw; // note: not checking if particle is dead is omitted as most are usually alive and if few are alive, rendering is fast anyways particles[i].vx = limitSpeed((int32_t)particles[i].vx - dv); } @@ -1460,12 +1461,12 @@ void ParticleSystem1D::applyGravity() { // apply gravity to single particle using system settings (use this for sources) // function does not increment gravity counter, if gravity setting is disabled, this cannot be used -void ParticleSystem1D::applyGravity(PSparticle1D *part) { +void ParticleSystem1D::applyGravity(PSparticle1D &part, bool reverse) { uint32_t counterbkp = gforcecounter; - int32_t dv = calcForce_dv(gforce, &gforcecounter); - if (part->reversegrav) dv = -dv; + int32_t dv = calcForce_dv(gforce, gforcecounter); + if (reverse) dv = -dv; gforcecounter = counterbkp; //save it back - part->vx = limitSpeed((int32_t)part->vx - dv); + part.vx = limitSpeed((int32_t)part.vx - dv); } @@ -1524,7 +1525,7 @@ void ParticleSystem1D::ParticleSys_render() { bool wrap = particlesettings.wrap; // local copy for speed // go over particles and render them to the buffer for (uint32_t i = 0; i < usedParticles; i++) { - if (particles[i].outofbounds || particles[i].ttl == 0) + if ( particles[i].ttl == 0 || particleFlags[i].outofbounds) continue; // generate RGB values for particle @@ -1543,7 +1544,7 @@ void ParticleSystem1D::ParticleSys_render() { // apply smear-blur to rendered frame if(globalSmear > 0) { if (framebuffer) - blur1D(framebuffer, maxXpixel + 1, globalSmear); + blur1D(framebuffer, maxXpixel + 1, globalSmear, 0); else SEGMENT.blur(globalSmear, true); } @@ -1676,7 +1677,7 @@ void ParticleSystem1D::handleCollisions() { // fill the binIndices array for this bin for (uint32_t i = collisionStartIdx; i < usedParticles; i++) { - if (particles[i].ttl > 0 && particles[i].outofbounds == 0 && particles[i].collide) { // colliding particle + if (particles[i].ttl > 0 && particleFlags[i].outofbounds == 0 && particleFlags[i].collide) { // colliding particle if (particles[i].x >= binStart && particles[i].x <= binEnd) { // >= and <= to include particles on the edge of the bin (overlap to ensure boarder particles collide with adjacent bins) if (binParticleCount >= maxBinParticles) { // bin is full, more particles in this bin so do the rest next frame nextFrameStartIdx = i; // bin overflow can only happen once as bin size is at least half of the particles (or half +1) @@ -1700,7 +1701,7 @@ void ParticleSystem1D::handleCollisions() { if (dv >= proximity) // particles would go past each other in next move update proximity += abs(dv); // add speed difference to catch fast particles if (dx < proximity && dx > -proximity) { // check if close - collideParticles(&particles[idx_i], &particles[idx_j], dx, dv, collisiondistance); + collideParticles(particles[idx_i], particleFlags[idx_i], particles[idx_j], particleFlags[idx_j], dx, dv, collisiondistance); } } } @@ -1709,26 +1710,26 @@ void ParticleSystem1D::handleCollisions() { // handle a collision if close proximity is detected, i.e. dx and/or dy smaller than 2*PS_P_RADIUS // takes two pointers to the particles to collide and the particle hardness (softer means more energy lost in collision, 255 means full hard) -void ParticleSystem1D::collideParticles(PSparticle1D *particle1, PSparticle1D *particle2, int32_t dx, int32_t relativeVx, uint32_t collisiondistance) { +void ParticleSystem1D::collideParticles(PSparticle1D &particle1, const PSparticleFlags1D &particle1flags, PSparticle1D &particle2, const PSparticleFlags1D &particle2flags, int32_t dx, int32_t relativeVx, uint32_t collisiondistance) { int32_t dotProduct = (dx * relativeVx); // is always negative if moving towards each other if (dotProduct < 0) { // particles are moving towards each other uint32_t surfacehardness = max(collisionHardness, (int32_t)PS_P_MINSURFACEHARDNESS_1D); // if particles are soft, the impulse must stay above a limit or collisions slip through // TODO: if soft collisions are not needed, the above line can be done in set hardness function and skipped here (which is what it currently looks like) // Calculate new velocities after collision int32_t impulse = relativeVx * surfacehardness / 255; - particle1->vx += impulse; - particle2->vx -= impulse; + particle1.vx += impulse; + particle2.vx -= impulse; // if one of the particles is fixed, transfer the impulse back so it bounces - if (particle1->fixed) - particle2->vx = -particle1->vx; - else if (particle2->fixed) - particle1->vx = -particle2->vx; + if (particle1flags.fixed) + particle2.vx = -particle1.vx; + else if (particle2flags.fixed) + particle1.vx = -particle2.vx; if (collisionHardness < PS_P_MINSURFACEHARDNESS_1D) { // if particles are soft, they become 'sticky' i.e. apply some friction (they do pile more nicely and correctly) const uint32_t coeff = collisionHardness + (255 - PS_P_MINSURFACEHARDNESS_1D); - particle1->vx = ((int32_t)particle1->vx * coeff) / 255; - particle2->vx = ((int32_t)particle2->vx * coeff) / 255; + particle1.vx = ((int32_t)particle1.vx * coeff) / 255; + particle2.vx = ((int32_t)particle2.vx * coeff) / 255; } } @@ -1741,20 +1742,20 @@ void ParticleSystem1D::collideParticles(PSparticle1D *particle1, PSparticle1D *p //int32_t pushamount = collisiondistance - distance; if (particlesettings.useGravity) { //using gravity, push the 'upper' particle only if (dx < 0) { // particle2.x < particle1.x - if (particle2->reversegrav && !particle2->fixed) { - particle2->x -= pushamount; - particle2->vx--; - } else if (!particle1->reversegrav && !particle1->fixed) { - particle1->x += pushamount; - particle1->vx++; + if (particle2flags.reversegrav && !particle2flags.fixed) { + particle2.x -= pushamount; + particle2.vx--; + } else if (!particle1flags.reversegrav && !particle1flags.fixed) { + particle1.x += pushamount; + particle1.vx++; } } else { - if (particle1->reversegrav && !particle1->fixed) { - particle1->x -= pushamount; - particle1->vx--; - } else if (!particle2->reversegrav && !particle2->fixed) { - particle2->x += pushamount; - particle2->vx++; + if (particle1flags.reversegrav && !particle1flags.fixed) { + particle1.x -= pushamount; + particle1.vx--; + } else if (!particle2flags.reversegrav && !particle2flags.fixed) { + particle2.x += pushamount; + particle2.vx++; } } } @@ -1762,8 +1763,8 @@ void ParticleSystem1D::collideParticles(PSparticle1D *particle1, PSparticle1D *p pushamount = 1; if (dx < 0) // particle2.x < particle1.x pushamount = -1; - particle1->vx -= pushamount; - particle2->vx += pushamount; + particle1.vx -= pushamount; + particle2.vx += pushamount; } } } @@ -1793,7 +1794,8 @@ void ParticleSystem1D::updatePSpointers(bool isadvanced) { // memory manager needs to know how many particles the FX wants to use so transitions can be handled properly (i.e. pointer will stop changing if enough particles are available during transitions) uint32_t usedByFX = (numParticles * ((uint32_t)fractionOfParticlesUsed + 1)) >> 8; // final number of particles the FX wants to use (fractionOfParticlesUsed is 0-255) particles = reinterpret_cast(particleMemoryManager(0, sizeof(PSparticle1D), availableParticles, usedByFX, effectID)); // get memory, leave buffer size as is (request 0) - sources = reinterpret_cast(this + 1); // pointer to source(s) + particleFlags = reinterpret_cast(this + 1); // pointer to particle flags + sources = reinterpret_cast(particleFlags + numParticles); // pointer to source(s) PSdataEnd = reinterpret_cast(sources + numSources); // pointer to first available byte after the PS for FX additional data if (isadvanced) { advPartProps = reinterpret_cast(sources + numSources); @@ -1811,7 +1813,7 @@ void ParticleSystem1D::updatePSpointers(bool isadvanced) { } //non class functions to use for initialization, fraction is uint8_t: 255 means 100% -uint32_t calculateNumberOfParticles1D(uint32_t fraction, bool isadvanced) { +uint32_t calculateNumberOfParticles1D(const uint32_t fraction, const bool isadvanced) { uint32_t numberofParticles = SEGMENT.virtualLength(); // one particle per pixel (if possible) #ifdef ESP8266 uint32_t particlelimit = ESP8266_MAXPARTICLES_1D; // maximum number of paticles allowed @@ -1830,7 +1832,7 @@ uint32_t calculateNumberOfParticles1D(uint32_t fraction, bool isadvanced) { return numberofParticles; } -uint32_t calculateNumberOfSources1D(uint32_t requestedsources) { +uint32_t calculateNumberOfSources1D(const uint32_t requestedsources) { #ifdef ESP8266 int numberofSources = max(1, min((int)requestedsources,ESP8266_MAXSOURCES_1D)); // limit to 1 - 8 #elif ARDUINO_ARCH_ESP32S2 @@ -1844,12 +1846,13 @@ uint32_t calculateNumberOfSources1D(uint32_t requestedsources) { } //allocate memory for particle system class, particles, sprays plus additional memory requested by FX -bool allocateParticleSystemMemory1D(uint32_t numparticles, uint32_t numsources, bool isadvanced, uint32_t additionalbytes) { +bool allocateParticleSystemMemory1D(const uint32_t numparticles, const uint32_t numsources, const bool isadvanced, const uint32_t additionalbytes) { uint32_t requiredmemory = sizeof(ParticleSystem1D); uint32_t dummy; // dummy variable if(particleMemoryManager(numparticles, sizeof(PSparticle1D), dummy, dummy, SEGMENT.mode) == nullptr) // allocate memory for particles return false; // not enough memory, function ensures a minimum of numparticles are avialable // functions above make sure these are a multiple of 4 bytes (to avoid alignment issues) + requiredmemory += sizeof(PSparticleFlags1D) * numparticles; if (isadvanced) requiredmemory += sizeof(PSadvancedParticle1D) * numparticles; requiredmemory += sizeof(PSsource1D) * numsources; @@ -1859,7 +1862,7 @@ bool allocateParticleSystemMemory1D(uint32_t numparticles, uint32_t numsources, // initialize Particle System, allocate additional bytes if needed (pointer to those bytes can be read from particle system class: PSdataEnd) // note: percentofparticles is in uint8_t, for example 191 means 75%, (deafaults to 255 or 100% meaning one particle per pixel), can be more than 100% (but not recommended, can cause out of memory) -bool initParticleSystem1D(ParticleSystem1D *&PartSys, uint32_t requestedsources, uint8_t fractionofparticles, uint32_t additionalbytes, bool advanced) { +bool initParticleSystem1D(ParticleSystem1D *&PartSys, const uint32_t requestedsources, const uint8_t fractionofparticles, const uint32_t additionalbytes, const bool advanced) { if (SEGLEN == 1) return false; // single pixel not supported updateRenderingBuffer(SEGMENT.vLength(), true, true); // update/create frame rendering buffer if(advanced) @@ -1903,7 +1906,7 @@ void blur1D(CRGB *colorbuffer, uint32_t size, uint32_t blur, uint32_t start) // calculate the delta speed (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 -static int32_t calcForce_dv(int8_t force, uint8_t* counter) { +static int32_t calcForce_dv(const int8_t force, uint8_t &counter) { if (force == 0) return 0; // for small forces, need to use a delay counter @@ -1911,9 +1914,9 @@ static int32_t calcForce_dv(int8_t force, uint8_t* counter) { int32_t dv = 0; // 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; + counter += force_abs; + if (counter > 15) { + counter -= 16; dv = force < 0 ? -1 : 1; // force is either 1 or -1 if it is small (zero force is handled above) } } @@ -1924,13 +1927,14 @@ static int32_t calcForce_dv(int8_t force, uint8_t* counter) { } // limit speed to prevent overflows +//TODO: inline this function? check if that uses a lot more flash. static int32_t limitSpeed(int32_t speed) { return min((int32_t)PS_P_MAXSPEED, max((int32_t)-PS_P_MAXSPEED, speed)); //return speed > PS_P_MAXSPEED ? PS_P_MAXSPEED : (speed < -PS_P_MAXSPEED ? -PS_P_MAXSPEED : speed); // note: this uses more code, not sure due to speed or inlining } // check if particle is out of bounds and wrap it around if required, returns false if out of bounds -static bool checkBoundsAndWrap(int32_t &position, const int32_t max, const int32_t particleradius, bool wrap) { +static bool checkBoundsAndWrap(int32_t &position, const int32_t max, const int32_t particleradius, const bool wrap) { if ((uint32_t)position > (uint32_t)max) { // check if particle reached an edge, cast to uint32_t to save negative checking (max is always positive) if (wrap) { position = position % (max + 1); // note: cannot optimize modulo, particles can be far out of bounds when wrap is enabled @@ -2153,13 +2157,9 @@ void particleHandover(void *buffer, size_t structSize, int32_t numToTransfer) { if (structSize == sizeof(PSparticle)) { // 2D particle PSparticle *particles = (PSparticle *)buffer; for (int32_t i = 0; i < numToTransfer; i++) { - particles[i].perpetual = false; // particle ages - if (particles[i].outofbounds) - particles[i].ttl = 0; // kill out of bounds - else if (particles[i].ttl > 200) + if (particles[i].ttl > 200) particles[i].ttl = 150 + hw_random16(50); // reduce TTL so it will die soon particles[i].sat = 255; // full saturation - particles[i].collide = true; // enable collisions (in case new FX uses them) } } else // 1D particle system @@ -2168,11 +2168,7 @@ void particleHandover(void *buffer, size_t structSize, int32_t numToTransfer) { #ifndef WLED_DISABLE_PARTICLESYSTEM1D PSparticle1D *particles = (PSparticle1D *)buffer; for (int32_t i = 0; i < numToTransfer; i++) { - particles[i].perpetual = false; // particle ages - particles[i].fixed = false; // unfix all particles - if (particles[i].outofbounds) - particles[i].ttl = 0; // kill out of bounds - else if (particles[i].ttl > 200) + if (particles[i].ttl > 200) particles[i].ttl = 150 + hw_random16(50); // reduce TTL so it will die soon } #endif diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index ff7f282be9..acf48504b5 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -16,7 +16,7 @@ #define PS_P_MAXSPEED 120 // maximum speed a particle can have (vx/vy is int8) #define MAX_MEMIDLE 10 // max idle time (in frames) before memory is deallocated (if deallocated during an effect, it will crash!) -//#define WLED_DEBUG_PS +#define WLED_DEBUG_PS #ifdef WLED_DEBUG_PS #define PSPRINT(x) Serial.print(x) @@ -66,35 +66,44 @@ void servicePSmem(); // increments watchdog, frees memory if idle too long // struct for PS settings (shared for 1D and 2D class) typedef union { - struct{ - // one byte bit field for 2D settings - bool wrapX : 1; - bool wrapY : 1; - bool bounceX : 1; - bool bounceY : 1; - bool killoutofbounds : 1; // if set, out of bound particles are killed immediately - bool useGravity : 1; // set to 1 if gravity is used, disables bounceY at the top - bool useCollisions : 1; - bool colorByAge : 1; // if set, particle hue is set by ttl value in render function + struct{ // one byte bit field for 2D settings + bool wrapX : 1; + bool wrapY : 1; + bool bounceX : 1; + bool bounceY : 1; + bool killoutofbounds : 1; // if set, out of bound particles are killed immediately + bool useGravity : 1; // set to 1 if gravity is used, disables bounceY at the top + bool useCollisions : 1; + bool colorByAge : 1; // if set, particle hue is set by ttl value in render function }; byte asByte; // access as a byte, order is: LSB is first entry in the list above } PSsettings2D; //struct for a single particle -typedef struct { // 12 bytes - 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 - uint8_t sat; // particle color saturation - uint16_t ttl; // time to live in frames - //uint16_t ttl : 12; // time to live, 12 bit or 4095 max (which is 50s at 80FPS) +typedef struct { // 10 bytes + int16_t x; // x position in particle system + int16_t y; // y position in particle system + uint16_t ttl; // time to live in frames + int8_t vx; // horizontal velocity + int8_t vy; // vertical velocity + uint8_t hue; // color hue + uint8_t sat; // particle color saturation +} PSparticle; + +//struct for particle flags note: this is separate from the particle struct to save memory (ram alignment) +typedef union { + struct { // 1 byte bool outofbounds : 1; // out of bounds flag, set to true if particle is outside of display area bool collide : 1; // if set, particle takes part in collisions bool perpetual : 1; // if set, particle does not age (TTL is not decremented in move function, it still dies from killoutofbounds) - bool state : 1; //can be used by FX to track state, not used in PS -} PSparticle; + bool custom1 : 1; // unused custom flags, can be used by FX to track particle states + bool custom2 : 1; + bool custom3 : 1; + bool custom4 : 1; + bool custom5 : 1; + }; + byte asByte; // access as a byte, order is: LSB is first entry in the list above +} PSparticleFlags; // struct for additional particle settings (option) typedef struct { // 2 bytes @@ -125,6 +134,7 @@ 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) + PSparticleFlags sourceFlags; // flags for the source particle int8_t var; // variation of emitted speed (adds random(+/- var) to speed) int8_t vx; // emitting speed int8_t vy; @@ -134,49 +144,49 @@ typedef struct { // class uses approximately 60 bytes class ParticleSystem2D { public: - ParticleSystem2D(uint32_t width, uint32_t height, uint32_t numberofparticles, uint32_t numberofsources, bool isadvanced = false, bool sizecontrol = false); // constructor + ParticleSystem2D(const uint32_t width, const uint32_t height, const uint32_t numberofparticles, const uint32_t numberofsources, const bool isadvanced = false, const bool sizecontrol = false); // constructor // note: memory is allcated in the FX function, no deconstructor needed void update(void); //update the particles according to set options and render to the matrix - void updateFire(uint32_t intensity, bool renderonly = false); // update function for fire, if renderonly is set, particles are not updated (required to fix transitions with frameskips) + void updateFire(const uint8_t intensity, const bool renderonly); // update function for fire, if renderonly is set, particles are not updated (required to fix transitions with frameskips) void updateSystem(void); // call at the beginning of every FX, updates pointers and dimensions - void particleMoveUpdate(PSparticle &part, PSsettings2D *options = NULL, PSadvancedParticle *advancedproperties = NULL); // move function + void particleMoveUpdate(PSparticle &part, PSparticleFlags &partFlags, PSsettings2D *options = NULL, PSadvancedParticle *advancedproperties = NULL); // move function // particle emitters - int32_t sprayEmit(PSsource &emitter, uint32_t amount = 1); - void flameEmit(PSsource &emitter); - int32_t angleEmit(PSsource& emitter, uint16_t angle, int32_t speed, uint32_t amount = 1); + int32_t sprayEmit(const PSsource &emitter); + void flameEmit(const PSsource &emitter); + int32_t angleEmit(PSsource& emitter, const uint16_t angle, const int32_t speed); //particle physics void applyGravity(PSparticle &part); // applies gravity to single particle (use this for sources) - void applyForce(PSparticle *part, int8_t xforce, int8_t yforce, uint8_t *counter); - void applyForce(uint16_t particleindex, int8_t xforce, int8_t yforce); // use this for advanced property particles - void applyForce(int8_t xforce, int8_t yforce); // apply a force to all particles - void applyAngleForce(PSparticle *part, int8_t force, uint16_t angle, uint8_t *counter); - void applyAngleForce(uint16_t particleindex, int8_t force, uint16_t angle); // use this for advanced property particles - void applyAngleForce(int8_t force, uint16_t angle); // apply angular force to all particles - void applyFriction(PSparticle *part, int32_t coefficient); // apply friction to specific particle - void applyFriction(int32_t coefficient); // apply friction to all used particles - void pointAttractor(uint16_t particleindex, PSparticle *attractor, uint8_t strength, bool swallow); - void lineAttractor(uint16_t particleindex, PSparticle *attractorcenter, uint16_t attractorangle, uint8_t strength); + void applyForce(PSparticle &part, const int8_t xforce, const int8_t yforce, uint8_t &counter); + void applyForce(const uint32_t particleindex, const int8_t xforce, const int8_t yforce); // use this for advanced property particles + void applyForce(const int8_t xforce, const int8_t yforce); // apply a force to all particles + void applyAngleForce(PSparticle &part, const int8_t force, const uint16_t angle, uint8_t &counter); + void applyAngleForce(const uint32_t particleindex, const int8_t force, const uint16_t angle); // use this for advanced property particles + void applyAngleForce(const int8_t force, const uint16_t angle); // apply angular force to all particles + void applyFriction(PSparticle &part, const int32_t coefficient); // apply friction to specific particle + void applyFriction(const int32_t coefficient); // apply friction to all used particles + void pointAttractor(const uint32_t particleindex, PSparticle &attractor, const uint8_t strength, const bool swallow); // set options - void setUsedParticles(uint8_t percentage); // set the percentage of particles used in the system, 255=100% + void setUsedParticles(const uint8_t percentage); // set the percentage of particles used in the system, 255=100% inline uint32_t getAvailableParticles(void) { return availableParticles; } // available particles in the buffer, use this to check if buffer changed during FX init - 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 setWallRoughness(uint8_t roughness); // wall roughness randomizes wall collisions - 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 setSaturation(uint8_t sat); // set global color saturation - void setColorByAge(bool enable); - void setMotionBlur(uint8_t bluramount); // note: motion blur can only be used if 'particlesize' is set to zero - void setSmearBlur(uint8_t bluramount); // enable 2D smeared blurring of full frame - void setParticleSize(uint8_t size); - void setGravity(int8_t force = 8); - void enableParticleCollisions(bool enable, uint8_t hardness = 255); + void setCollisionHardness(const uint8_t hardness); // hardness for particle collisions (255 means full hard) + void setWallHardness(const uint8_t hardness); // hardness for bouncing on the wall if bounceXY is set + void setWallRoughness(const uint8_t roughness); // wall roughness randomizes wall collisions + void setMatrixSize(const uint32_t x, const uint32_t y); + void setWrapX(const bool enable); + void setWrapY(const bool enable); + void setBounceX(const bool enable); + void setBounceY(const bool enable); + void setKillOutOfBounds(const bool enable); // if enabled, particles outside of matrix instantly die + void setSaturation(const uint8_t sat); // set global color saturation + void setColorByAge(const bool enable); + void setMotionBlur(const uint8_t bluramount); // note: motion blur can only be used if 'particlesize' is set to zero + void setSmearBlur(const uint8_t bluramount); // enable 2D smeared blurring of full frame + void setParticleSize(const uint8_t size); + void setGravity(const int8_t force = 8); + void enableParticleCollisions(const bool enable, const uint8_t hardness = 255); PSparticle *particles; // pointer to particle array + PSparticleFlags *particleFlags; // pointer to particle flags array PSsource *sources; // pointer to sources PSadvancedParticle *advPartProps; // pointer to advanced particle properties (can be NULL) PSsizeControl *advPartSize; // pointer to advanced particle size control (can be NULL) @@ -189,30 +199,30 @@ class ParticleSystem2D { private: //rendering functions - void ParticleSys_render(bool firemode = false, uint32_t fireintensity = 128); + void ParticleSys_render(); void renderParticle(const uint32_t particleindex, const uint32_t brightness, const CRGB& color, const bool wrapX, const bool wrapY); //paricle physics applied by system if flags are set void applyGravity(); // applies gravity to all particles void handleCollisions(); - void collideParticles(PSparticle *particle1, PSparticle *particle2, int32_t dx, int32_t dy); + void collideParticles(PSparticle &particle1, PSparticle &particle2, const int32_t dx, const int32_t dy); void fireParticleupdate(); //utility functions - void updatePSpointers(bool isadvanced, bool sizecontrol); // update the data pointers to current segment data space + void updatePSpointers(const bool isadvanced, const bool sizecontrol); // update the data pointers to current segment data space void updateSize(PSadvancedParticle *advprops, PSsizeControl *advsize); // advanced size control void getParticleXYsize(PSadvancedParticle *advprops, PSsizeControl *advsize, uint32_t &xsize, uint32_t &ysize); - void bounce(int8_t &incomingspeed, int8_t ¶llelspeed, int32_t &position, uint16_t maxposition); // bounce on a wall - int16_t wraparound(uint16_t p, uint32_t maxvalue); + void bounce(int8_t &incomingspeed, int8_t ¶llelspeed, int32_t &position, const uint32_t maxposition); // bounce on a wall // note: variables that are accessed often are 32bit for speed PSsettings2D particlesettings; // settings used when updating particles (can also used by FX to move sources), do not edit properties directly, use functions above uint32_t numParticles; // total number of particles allocated by this system note: during transitions, less are available, use availableParticles uint32_t availableParticles; // number of particles available for use (can be more or less than numParticles, assigned by memory manager) - uint8_t fractionOfParticlesUsed; // percentage of particles used in the system (255=100%), used during transition updates uint32_t emitIndex; // index to count through particles to emit so searching for dead pixels is faster int32_t collisionHardness; uint32_t wallHardness; - uint32_t wallRoughness; // randomizes wall collisions - uint16_t collisionStartIdx; // particle array start index for collision detection + uint32_t wallRoughness; // randomizes wall collisions uint32_t particleHardRadius; // hard surface radius of a particle, used for collision detection (32bit for speed) + uint16_t collisionStartIdx; // particle array start index for collision detection + uint8_t fireIntesity = 0; // fire intensity, used for fire mode (flash use optimization, better than passing an argument to render function) + uint8_t fractionOfParticlesUsed; // percentage of particles used in the system (255=100%), used during transition updates uint8_t forcecounter; // counter for globally applied forces uint8_t gforcecounter; // counter for global gravity int8_t gforce; // gravity strength, default is 8 (negative is allowed, positive is downwards) @@ -223,12 +233,12 @@ class ParticleSystem2D { uint8_t effectID; // ID of the effect that is using this particle system, used for transitions }; -void blur2D(CRGB *colorbuffer, uint32_t xsize, uint32_t ysize, uint32_t xblur, uint32_t yblur, uint32_t xstart = 0, uint32_t ystart = 0, bool isparticle = false); +void blur2D(CRGB *colorbuffer, const uint32_t xsize, uint32_t ysize, const uint32_t xblur, const uint32_t yblur, const uint32_t xstart = 0, uint32_t ystart = 0, const bool isparticle = false); // initialization functions (not part of class) -bool initParticleSystem2D(ParticleSystem2D *&PartSys, uint32_t requestedsources, uint32_t additionalbytes = 0, bool advanced = false, bool sizecontrol = false); -uint32_t calculateNumberOfParticles2D(uint32_t pixels, bool advanced, bool sizecontrol); -uint32_t calculateNumberOfSources2D(uint32_t pixels, uint32_t requestedsources); -bool allocateParticleSystemMemory2D(uint32_t numparticles, uint32_t numsources, bool advanced, bool sizecontrol, uint32_t additionalbytes); +bool initParticleSystem2D(ParticleSystem2D *&PartSys, const uint32_t requestedsources, const uint32_t additionalbytes = 0, const bool advanced = false, const bool sizecontrol = false); +uint32_t calculateNumberOfParticles2D(const uint32_t pixels, const bool advanced, const bool sizecontrol); +uint32_t calculateNumberOfSources2D(const uint32_t pixels, const uint32_t requestedsources); +bool allocateParticleSystemMemory2D(const uint32_t numparticles, const uint32_t numsources, const bool advanced, const bool sizecontrol, const uint32_t additionalbytes); #endif // WLED_DISABLE_PARTICLESYSTEM2D //////////////////////// @@ -269,20 +279,26 @@ typedef union { //struct for a single particle (8 bytes) typedef struct { - int32_t x; // x position in particle system - uint16_t ttl; // time to live in frames - int8_t vx; // horizontal velocity - uint8_t hue; // color hue - // two byte bit field: - //uint16_t ttl : 11; // time to live, 11 bit or 2047 max (which is 25s at 80FPS) + int32_t x; // x position in particle system + uint16_t ttl; // time to live in frames + int8_t vx; // horizontal velocity + uint8_t hue; // color hue +} PSparticle1D; + +//struct for particle flags +typedef union { + struct { // 1 byte bool outofbounds : 1; // out of bounds flag, set to true if particle is outside of display area bool collide : 1; // if set, particle takes part in collisions bool perpetual : 1; // if set, particle does not age (TTL is not decremented in move function, it still dies from killoutofbounds) bool reversegrav : 1; // if set, gravity is reversed on this particle bool fixed : 1; // if set, particle does not move (and collisions make other particles revert direction), - // note: there is on byte of padding added here, making TTL a 16bit variable saves 500bytes of flash so much faster than a bit field - // TODO: can this be optimized? wastes a lot of ram... -> yes, TODO: make the flags a seperate struct array and handle it everywhere. -} PSparticle1D; + bool custom1 : 1; // unused custom flags, can be used by FX to track particle states + bool custom2 : 1; + bool custom3 : 1; + }; + byte asByte; // access as a byte, order is: LSB is first entry in the list above +} PSparticleFlags1D; // struct for additional particle settings (optional) typedef struct { @@ -296,46 +312,50 @@ typedef struct { uint16_t minLife; // minimum ttl of emittet particles uint16_t maxLife; // maximum ttl of emitted particles PSparticle1D source; // use a particle as the emitter source (speed, position, color) + PSparticleFlags1D sourceFlags; // flags for the source particle int8_t var; // variation of emitted speed (adds random(+/- var) to speed) int8_t v; // emitting speed uint8_t sat; // color saturation (advanced property) uint8_t size; // particle size (advanced property) } PSsource1D; - +//TODO: 1D function cleanup: add const where possible, replace pointers with reference where possible +//TODO: match all functions with declarations in header file for consistency +// TODO: make all 1-line function inline (check first if this makes code larger or smaller with one) class ParticleSystem1D { public: - ParticleSystem1D(uint32_t length, uint32_t numberofparticles, uint32_t numberofsources, bool isadvanced = false); // constructor + ParticleSystem1D(const uint32_t length, const uint32_t numberofparticles, const uint32_t numberofsources, const bool isadvanced = false); // constructor // note: memory is allcated in the FX function, no deconstructor needed void update(void); //update the particles according to set options and render to the matrix void updateSystem(void); // call at the beginning of every FX, updates pointers and dimensions // particle emitters - int32_t sprayEmit(PSsource1D &emitter); - void particleMoveUpdate(PSparticle1D &part, PSsettings1D *options = NULL, PSadvancedParticle1D *advancedproperties = NULL); // move function + int32_t sprayEmit(const PSsource1D &emitter); + void particleMoveUpdate(PSparticle1D &part, PSparticleFlags1D &partFlags, PSsettings1D *options = NULL, PSadvancedParticle1D *advancedproperties = NULL); // move function //particle physics - void applyForce(PSparticle1D *part, int8_t xforce, uint8_t *counter); //apply a force to a single particle - void applyForce(int8_t xforce); // apply a force to all particles - void applyGravity(PSparticle1D *part); // applies gravity to single particle (use this for sources) - void applyFriction(int32_t coefficient); // apply friction to all used particles + void applyForce(PSparticle1D &part, const int8_t xforce, uint8_t &counter); //apply a force to a single particle + void applyForce(const int8_t xforce); // apply a force to all particles + void applyGravity(PSparticle1D &part,const bool reverse); // applies gravity to single particle (use this for sources) + void applyFriction(const int32_t coefficient); // apply friction to all used particles // set options - void setUsedParticles(uint8_t percentage); // set the percentage of particles used in the system, 255=100% + void setUsedParticles(const uint8_t percentage); // set the percentage of particles used in the system, 255=100% inline uint32_t getAvailableParticles(void) { return availableParticles; } // available particles in the buffer, use this to check if buffer changed during FX init - void setWallHardness(uint8_t hardness); // hardness for bouncing on the wall if bounceXY is set - void setSize(uint16_t x); //set particle system size (= strip length) - void setWrap(bool enable); - void setBounce(bool enable); - void setKillOutOfBounds(bool enable); // if enabled, particles outside of matrix instantly die + void setWallHardness(const uint8_t hardness); // hardness for bouncing on the wall if bounceXY is set + void setSize(const uint32_t x); //set particle system size (= strip length) + void setWrap(const bool enable); + void setBounce(const bool enable); + void setKillOutOfBounds(const bool enable); // if enabled, particles outside of matrix instantly die // void setSaturation(uint8_t sat); // set global color saturation - void setColorByAge(bool enable); - void setColorByPosition(bool enable); - void setMotionBlur(uint8_t bluramount); // note: motion blur can only be used if 'particlesize' is set to zero - void setSmearBlur(uint8_t bluramount); // enable 1D smeared blurring of full frame - void setParticleSize(uint8_t size); //size 0 = 1 pixel, size 1 = 2 pixels, is overruled by advanced particle size + void setColorByAge(const bool enable); + void setColorByPosition(const bool enable); + void setMotionBlur(const uint8_t bluramount); // note: motion blur can only be used if 'particlesize' is set to zero + void setSmearBlur(const uint8_t bluramount); // enable 1D smeared blurring of full frame + void setParticleSize(const uint8_t size); //size 0 = 1 pixel, size 1 = 2 pixels, is overruled by advanced particle size void setGravity(int8_t force = 8); - void enableParticleCollisions(bool enable, uint8_t hardness = 255); + void enableParticleCollisions(bool enable, const uint8_t hardness = 255); PSparticle1D *particles; // pointer to particle array + PSparticleFlags1D *particleFlags; // pointer to particle flags array PSsource1D *sources; // pointer to sources PSadvancedParticle1D *advPartProps; // pointer to advanced particle properties (can be NULL) //PSsizeControl *advPartSize; // pointer to advanced particle size control (can be NULL) @@ -353,12 +373,12 @@ class ParticleSystem1D //paricle physics applied by system if flags are set void applyGravity(); // applies gravity to all particles void handleCollisions(); - void collideParticles(PSparticle1D *particle1, PSparticle1D *particle2, int32_t dx, int32_t relativeV, uint32_t collisiondistance); + void collideParticles(PSparticle1D &particle1, const PSparticleFlags1D &particle1flags, PSparticle1D &particle2, const PSparticleFlags1D &particle2flags, int32_t dx, int32_t relativeVx, uint32_t collisiondistance); //utility functions - void updatePSpointers(bool isadvanced); // update the data pointers to current segment data space + void updatePSpointers(const bool isadvanced); // update the data pointers to current segment data space //void updateSize(PSadvancedParticle *advprops, PSsizeControl *advsize); // advanced size control - void bounce(int8_t &incomingspeed, int8_t ¶llelspeed, int32_t &position, uint16_t maxposition); // bounce on a wall + void bounce(int8_t &incomingspeed, int8_t ¶llelspeed, int32_t &position, const uint32_t maxposition); // bounce on a wall // note: variables that are accessed often are 32bit for speed PSsettings1D particlesettings; // settings used when updating particles uint32_t numParticles; // total number of particles allocated by this system note: never use more than this, even if more are available (only this many advanced particles are allocated) @@ -379,9 +399,9 @@ class ParticleSystem1D uint8_t effectID; // ID of the effect that is using this particle system, used for transitions }; -bool initParticleSystem1D(ParticleSystem1D *&PartSys, uint32_t requestedsources, uint8_t fractionofparticles = 255, uint32_t additionalbytes = 0, bool advanced = false); -uint32_t calculateNumberOfParticles1D(uint32_t fraction, bool isadvanced); -uint32_t calculateNumberOfSources1D(uint32_t requestedsources); -bool allocateParticleSystemMemory1D(uint32_t numparticles, uint32_t numsources, bool isadvanced, uint32_t additionalbytes); -void blur1D(CRGB *colorbuffer, uint32_t size, uint32_t blur, uint32_t start = 0); +bool initParticleSystem1D(ParticleSystem1D *&PartSys, const uint32_t requestedsources, const uint8_t fractionofparticles = 255, const uint32_t additionalbytes = 0, const bool advanced = false); +uint32_t calculateNumberOfParticles1D(const uint32_t fraction, const bool isadvanced); +uint32_t calculateNumberOfSources1D(const uint32_t requestedsources); +bool allocateParticleSystemMemory1D(const uint32_t numparticles, const uint32_t numsources, const bool isadvanced, const uint32_t additionalbytes); +void blur1D(CRGB *colorbuffer, uint32_t size, uint32_t blur, uint32_t start); #endif // WLED_DISABLE_PARTICLESYSTEM1D From ca54d819650509b000037042e737b579f118e7be Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Fri, 10 Jan 2025 21:04:22 +0100 Subject: [PATCH 165/219] removed todos after some checks, minor improvements --- wled00/FXparticleSystem.cpp | 17 ++++++++++------- wled00/FXparticleSystem.h | 15 ++++++++------- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index a5fbd41650..b3ffd66152 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -22,7 +22,6 @@ // local shared functions (used both in 1D and 2D system) static int32_t calcForce_dv(const int8_t force, uint8_t &counter); -static int32_t limitSpeed(int32_t speed); static bool checkBoundsAndWrap(int32_t &position, const int32_t max, const int32_t particleradius, const bool wrap); // returns false if out of bounds by more than particleradius static void fast_color_add(CRGB &c1, const CRGB &c2, uint32_t scale = 255); // fast and accurate color adding with scaling (scales c2 before adding) static void fast_color_scale(CRGB &c, const uint32_t scale); // fast scaling function using 32bit variable and pointer. note: keep 'scale' within 0-255 @@ -252,7 +251,7 @@ void ParticleSystem2D::particleMoveUpdate(PSparticle &part, PSparticleFlags &par int32_t renderradius = PS_P_HALFRADIUS; // used to check out of bounds int32_t newX = part.x + (int32_t)part.vx; int32_t newY = part.y + (int32_t)part.vy; - partFlags.outofbounds = false; // reset out of bounds (in case particle was created outside the matrix and is now moving into view) TODO: move this below, setting a flag is slow, only set if actually in bounds + partFlags.outofbounds = false; // reset out of bounds (in case particle was created outside the matrix and is now moving into view) note: moving this to checks below adds code and is not faster if (advancedproperties) { //using individual particle size? if (advancedproperties->size > 0) { @@ -302,7 +301,7 @@ void ParticleSystem2D::fireParticleupdate() { particles[i].ttl--; // age int32_t newY = particles[i].y + (int32_t)particles[i].vy + (particles[i].ttl >> 2); // younger particles move faster upward as they are hotter int32_t newX = particles[i].x + (int32_t)particles[i].vx; - particleFlags[i].outofbounds = 0; // reset out of bounds flag //TODO: can this be moved to else statements below? + particleFlags[i].outofbounds = 0; // reset out of bounds flag note: moving this to checks below is not faster but adds code // 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 speed, only check x direction if y is not out of bounds if (newY < -PS_P_HALFRADIUS) @@ -1063,7 +1062,8 @@ void blur2D(CRGB *colorbuffer, uint32_t xsize, uint32_t ysize, uint32_t xblur, u fast_color_scale(seeppart, seep); // scale it and seep to neighbours if (x > 0) { fast_color_add(colorbuffer[indexXY - 1], seeppart); - fast_color_add(colorbuffer[indexXY], carryover); // TODO: could check if carryover is > 0, takes 7 instructions, add takes ~35, with lots of same color pixels (like background), it would be faster + if(carryover) // note: check adds overhead but is faster on average + fast_color_add(colorbuffer[indexXY], carryover); } carryover = seeppart; indexXY++; // next pixel in x direction @@ -1084,7 +1084,8 @@ void blur2D(CRGB *colorbuffer, uint32_t xsize, uint32_t ysize, uint32_t xblur, u fast_color_scale(seeppart, seep); // scale it and seep to neighbours if (y > 0) { fast_color_add(colorbuffer[indexXY - width], seeppart); - fast_color_add(colorbuffer[indexXY], carryover); // todo: could check if carryover is > 0, takes 7 instructions, add takes ~35, with lots of same color pixels (like background), it would be faster + if(carryover) // note: check adds overhead but is faster on average + fast_color_add(colorbuffer[indexXY], carryover); } carryover = seeppart; indexXY += width; // next pixel in y direction @@ -1891,7 +1892,8 @@ void blur1D(CRGB *colorbuffer, uint32_t size, uint32_t blur, uint32_t start) fast_color_scale(seeppart, seep); // scale it and seep to neighbours if (x > 0) { fast_color_add(colorbuffer[x-1], seeppart); - fast_color_add(colorbuffer[x], carryover); // is black on first pass + if(carryover) // note: check adds overhead but is faster on average + fast_color_add(colorbuffer[x], carryover); // is black on first pass } carryover = seeppart; } @@ -1928,10 +1930,11 @@ static int32_t calcForce_dv(const int8_t force, uint8_t &counter) { // limit speed to prevent overflows //TODO: inline this function? check if that uses a lot more flash. +/* static int32_t limitSpeed(int32_t speed) { return min((int32_t)PS_P_MAXSPEED, max((int32_t)-PS_P_MAXSPEED, speed)); //return speed > PS_P_MAXSPEED ? PS_P_MAXSPEED : (speed < -PS_P_MAXSPEED ? -PS_P_MAXSPEED : speed); // note: this uses more code, not sure due to speed or inlining -} +}*/ // check if particle is out of bounds and wrap it around if required, returns false if out of bounds static bool checkBoundsAndWrap(int32_t &position, const int32_t max, const int32_t particleradius, const bool wrap) { diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index acf48504b5..f9c44ccac1 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -16,7 +16,7 @@ #define PS_P_MAXSPEED 120 // maximum speed a particle can have (vx/vy is int8) #define MAX_MEMIDLE 10 // max idle time (in frames) before memory is deallocated (if deallocated during an effect, it will crash!) -#define WLED_DEBUG_PS +//#define WLED_DEBUG_PS // note: enabling debug uses ~3k of flash #ifdef WLED_DEBUG_PS #define PSPRINT(x) Serial.print(x) @@ -44,9 +44,13 @@ partMem* getPartMem(void); // returns pointer to memory struct for current segme void updateRenderingBuffer(uint32_t requiredpixels, bool isFramebuffer, bool initialize); // allocate CRGB rendering buffer, update size if needed void transferBuffer(uint32_t width, uint32_t height, bool useAdditiveTransfer = false); // transfer the buffer to the segment (supports 1D and 2D) void servicePSmem(); // increments watchdog, frees memory if idle too long + +// limit speed of particles (used in 1D and 2D) +static inline int32_t limitSpeed(int32_t speed) { + return speed > PS_P_MAXSPEED ? PS_P_MAXSPEED : (speed < -PS_P_MAXSPEED ? -PS_P_MAXSPEED : speed); // note: this is slightly faster than using min/max at the cost of 50bytes of flash +} #endif -//TODO: maybe update PS_P_MINSURFACEHARDNESS for 2D? its a bit too sticky already at hardness 100 #ifndef WLED_DISABLE_PARTICLESYSTEM2D // memory allocation #define ESP8266_MAXPARTICLES 300 // enough up to 20x20 pixels @@ -165,7 +169,7 @@ class ParticleSystem2D { void applyFriction(PSparticle &part, const int32_t coefficient); // apply friction to specific particle void applyFriction(const int32_t coefficient); // apply friction to all used particles void pointAttractor(const uint32_t particleindex, PSparticle &attractor, const uint8_t strength, const bool swallow); - // set options + // set options note: inlining the set function uses more flash so dont optimize void setUsedParticles(const uint8_t percentage); // set the percentage of particles used in the system, 255=100% inline uint32_t getAvailableParticles(void) { return availableParticles; } // available particles in the buffer, use this to check if buffer changed during FX init void setCollisionHardness(const uint8_t hardness); // hardness for particle collisions (255 means full hard) @@ -319,9 +323,6 @@ typedef struct { uint8_t size; // particle size (advanced property) } PSsource1D; -//TODO: 1D function cleanup: add const where possible, replace pointers with reference where possible -//TODO: match all functions with declarations in header file for consistency -// TODO: make all 1-line function inline (check first if this makes code larger or smaller with one) class ParticleSystem1D { public: @@ -393,7 +394,7 @@ class ParticleSystem1D uint8_t forcecounter; // counter for globally applied forces uint16_t collisionStartIdx; // particle array start index for collision detection //global particle properties for basic particles - uint8_t particlesize; // global particle size, 0 = 1 pixel, 1 = 2 pixels, larger sizez TBD (TODO: need larger sizes?) + uint8_t particlesize; // global particle size, 0 = 1 pixel, 1 = 2 pixels uint8_t motionBlur; // enable motion blur, values > 100 gives smoother animations uint8_t smearBlur; // smeared blurring of full frame uint8_t effectID; // ID of the effect that is using this particle system, used for transitions From 0ef58b7bc3ba5843d5768988eaec4272cf83af5a Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sun, 12 Jan 2025 11:59:49 +0100 Subject: [PATCH 166/219] inverted y axis in 2D render, 1D collision improvements, cleanup and fixes - inverting the y-axis in the buffer instead of in buffer transfer fixes the FX flipping when transitiononing 1D<->2D - improved particle binning for collisions - added hard-pushing also when not using gravity so piles close to an edge dont collapse - some improvments to "balance" FX - renaming and cleanup --- wled00/FX.cpp | 167 ++++++++++++++++++------------------ wled00/FX_fcn.cpp | 4 +- wled00/FXparticleSystem.cpp | 130 ++++++++++++++++------------ wled00/FXparticleSystem.h | 21 ++--- 4 files changed, 167 insertions(+), 155 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 6ad69cb377..2aaaf4f9db 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -7782,16 +7782,15 @@ static const char _data_FX_MODE_PARTICLEVORTEX[] PROGMEM = "PS Vortex@Rotation S uint16_t mode_particlefireworks(void) { ParticleSystem2D *PartSys = NULL; uint32_t numRockets; - uint32_t i, j; if (SEGMENT.call == 0) { // initialization - if (!initParticleSystem2D(PartSys, NUMBEROFSOURCES, true)) + if (!initParticleSystem2D(PartSys, NUMBEROFSOURCES)) return mode_static(); // allocation failed PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting) PartSys->setWallHardness(120); // ground bounce is fixed numRockets = min(PartSys->numSources, (uint32_t)NUMBEROFSOURCES); - for (j = 0; j < numRockets; j++) { + for (uint32_t j = 0; j < numRockets; j++) { PartSys->sources[j].source.ttl = 500 * j; // first rocket starts immediately, others follow soon PartSys->sources[j].source.vy = -1; // at negative speed, no particles are emitted and if rocket dies, it will be relaunched } @@ -7815,7 +7814,7 @@ uint16_t mode_particlefireworks(void) { PartSys->setSmearBlur(smearing); // enable 2D blurring (smearing) // update the rockets, set the speed state - for (j = 0; j < numRockets; j++) { + for (uint32_t j = 0; j < numRockets; j++) { PartSys->applyGravity(PartSys->sources[j].source); PartSys->particleMoveUpdate(PartSys->sources[j].source, PartSys->sources[j].sourceFlags); if (PartSys->sources[j].source.ttl == 0) { @@ -7847,7 +7846,7 @@ uint16_t mode_particlefireworks(void) { bool circularexplosion = false; // emit particles for each rocket - for (j = 0; j < numRockets; j++) { + for (uint32_t j = 0; j < numRockets; j++) { // determine rocket state by its speed: if (PartSys->sources[j].source.vy > 0) { // moving up, emit exhaust emitparticles = 1; @@ -7869,8 +7868,7 @@ uint16_t mode_particlefireworks(void) { emitparticles = hw_random16(SEGMENT.intensity >> 2) + (SEGMENT.intensity >> 2) + 5; // defines the size of the explosion #endif - if (random16() & 1) - { // 50% chance for circular explosion + if (random16() & 1) { // 50% chance for circular explosion circularexplosion = true; speed = 2 + hw_random16(3) + ((SEGMENT.intensity >> 6)); currentspeed = speed; @@ -7885,8 +7883,8 @@ uint16_t mode_particlefireworks(void) { PartSys->sources[j].var = angle & 1; // 0 or 1 variation, angle is random } } - - for (i = 0; i < emitparticles; i++) { + uint32_t i = 0; + for (i; i < emitparticles; i++) { if (circularexplosion) { int32_t sineMod = 0xEFFF + sin16_t((uint16_t)(((angle * frequency) >> 4) + baseangle)); // shifted to positive values currentspeed = (speed/2 + ((sineMod * speed) >> 16)) >> 1; // sine modulation on speed based on emit angle @@ -7911,12 +7909,16 @@ uint16_t mode_particlefireworks(void) { PartSys->sources[j].source.y = 1000; // reset position so gravity wont pull it to the ground and bounce it (vy MUST stay negative until relaunch) circularexplosion = false; // reset for next rocket } - + if(SEGMENT.check3) { // fast speed, move particles twice + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { + PartSys->particleMoveUpdate(PartSys->particles[i], PartSys->particleFlags[i], nullptr, nullptr); + } + } PartSys->update(); // update and render return FRAMETIME; } #undef NUMBEROFSOURCES -static const char _data_FX_MODE_PARTICLEFIREWORKS[] PROGMEM = "PS Fireworks@Launches,Explosion Size,Fuse,Blur,Gravity,Cylinder,Ground,;;!;2;pal=11,sx=100,ix=50,c1=84,c2=0,c3=12"; +static const char _data_FX_MODE_PARTICLEFIREWORKS[] PROGMEM = "PS Fireworks@Launches,Explosion Size,Fuse,Blur,Gravity,Cylinder,Ground,Fast;;!;2;pal=11,sx=100,ix=50,c1=40,c2=0,c3=12"; /* * Particle Volcano @@ -7992,9 +7994,9 @@ uint16_t mode_particlevolcano(void) { static const char _data_FX_MODE_PARTICLEVOLCANO[] PROGMEM = "PS Volcano@Speed,Intensity,Move,Bounce,Spread,AgeColor,Walls,Collide;;!;2;pal=35,sx=100,ix=190,c1=0,c2=160,c3=6,o1=1"; /* -* Particle Fire -* realistic fire effect using particles. heat based and using perlin-noise for wind -* by DedeHai (Damian Schneider) + Particle Fire + realistic fire effect using particles. heat based and using perlin-noise for wind + by DedeHai (Damian Schneider) */ uint16_t mode_particlefire(void) { ParticleSystem2D *PartSys = NULL; @@ -8083,11 +8085,11 @@ uint16_t mode_particlefire(void) { static const char _data_FX_MODE_PARTICLEFIRE[] PROGMEM = "PS Fire@Speed,Intensity,Flame Height,Wind,Spread,Smooth,Cylinder,Turbulence;;!;2;pal=35,sx=110,c1=110,c2=50,c3=31,o1=1"; /* -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 -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) + 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 + 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_particlepit(void) { ParticleSystem2D *PartSys = NULL; @@ -8155,10 +8157,10 @@ uint16_t mode_particlepit(void) { static const char _data_FX_MODE_PARTICLEPIT[] PROGMEM = "PS Ballpit@Speed,Intensity,Size,Hardness,Saturation,Cylinder,Walls,Ground;;!;2;pal=11,sx=100,ix=220,c1=120,c2=130,c3=31,o3=1"; /* - * Particle Waterfall - * Uses palette for particle color, spray source at top emitting particles, many config options - * by DedeHai (Damian Schneider) - */ + 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) { ParticleSystem2D *PartSys = NULL; uint8_t numSprays; @@ -8227,9 +8229,9 @@ uint16_t mode_particlewaterfall(void) { static const char _data_FX_MODE_PARTICLEWATERFALL[] PROGMEM = "PS Waterfall@Speed,Intensity,Variation,Collide,Position,Cylinder,Walls,Ground;;!;2;pal=9,sx=15,ix=200,c1=32,c2=160,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) + 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) { ParticleSystem2D *PartSys = NULL; @@ -8308,9 +8310,9 @@ uint16_t mode_particlebox(void) { static const char _data_FX_MODE_PARTICLEBOX[] PROGMEM = "PS Box@Speed,Particles,Tilt Strength,Hardness,Friction,Random,Washing Machine,Sloshing;;!;2;pal=53,sx=120,ix=100,c1=100,c2=210,o1=1"; /* -Fuzzy Noise: Perlin noise 'gravity' mapping as in particles on 'noise hills' viewed from above -calculates slope gradient at the particle positions and applies 'downhill' force, resulting in a fuzzy perlin noise display -by DedeHai (Damian Schneider) + 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, resulting in a fuzzy perlin noise display + by DedeHai (Damian Schneider) */ uint16_t mode_particleperlin(void) { ParticleSystem2D *PartSys = NULL; @@ -8370,9 +8372,9 @@ uint16_t mode_particleperlin(void) { static const char _data_FX_MODE_PARTICLEPERLIN[] PROGMEM = "PS Fuzzy Noise@Speed,Particles,Bounce,Friction,Scale,Cylinder,Smear,Collide;;!;2;pal=64,sx=50,ix=200,c1=130,c2=30,c3=5,o3=1"; /* - * Particle smashing down like meteors and exploding as they hit the ground, has many parameters to play with - * by DedeHai (Damian Schneider) - */ + Particle smashing down like meteors and exploding as they hit the ground, has many parameters to play with + by DedeHai (Damian Schneider) +*/ #define NUMBEROFSOURCES 8 uint16_t mode_particleimpact(void) { ParticleSystem2D *PartSys = NULL; @@ -8492,7 +8494,6 @@ uses inverse square law like in planetary motion Uses palette for particle color by DedeHai (Damian Schneider) */ - uint16_t mode_particleattractor(void) { ParticleSystem2D *PartSys = NULL; PSsettings2D sourcesettings; @@ -8602,7 +8603,6 @@ Particle Spray, just a particle spray with many parameters Uses palette for particle color by DedeHai (Damian Schneider) */ - uint16_t mode_particlespray(void) { ParticleSystem2D *PartSys = NULL; //uint8_t numSprays; @@ -8689,7 +8689,6 @@ Particle base Graphical Equalizer Uses palette for particle color by DedeHai (Damian Schneider) */ - uint16_t mode_particleGEQ(void) { ParticleSystem2D *PartSys = NULL; @@ -8759,15 +8758,14 @@ uint16_t mode_particleGEQ(void) { return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEGEQ[] PROGMEM = "PS 2D GEQ@Speed,Intensity,Diverge,Bounce,Gravity,Cylinder,Walls,Floor;;!;2f;pal=0,sx=155,ix=200,c1=0"; +static const char _data_FX_MODE_PARTICLEGEQ[] PROGMEM = "PS GEQ 2D@Speed,Intensity,Diverge,Bounce,Gravity,Cylinder,Walls,Floor;;!;2f;pal=0,sx=155,ix=200,c1=0"; /* - * Particle rotating GEQ (unfinished, basically works but needs more fine-tuning) - * Particles sprayed from center with a rotating spray + * Particle rotating GEQ + * Particles sprayed from center with rotating spray * Uses palette for particle color * by DedeHai (Damian Schneider) */ - #define NUMBEROFSOURCES 16 uint16_t mode_particlecenterGEQ(void) { ParticleSystem2D *PartSys = NULL; @@ -8815,7 +8813,7 @@ uint16_t mode_particlecenterGEQ(void) { if (SEGMENT.call % (32 - (SEGMENT.custom2 >> 3)) == 0 && SEGMENT.custom2 > 0) PartSys->sources[j].source.hue += 1 + (SEGMENT.custom2 >> 4); - PartSys->sources[j].var = SEGMENT.custom3 >> 1; + PartSys->sources[j].var = SEGMENT.custom3 >> 2; int8_t emitspeed = 5 + (((uint32_t)fftResult[j] * ((uint32_t)SEGMENT.speed + 20)) >> 10); // emit speed according to loudness of band uint16_t emitangle = j * angleoffset + SEGENV.aux0; @@ -8835,7 +8833,7 @@ uint16_t mode_particlecenterGEQ(void) { PartSys->update(); // update and render return FRAMETIME; } -static const char _data_FX_MODE_PARTICLECIRCULARGEQ[] PROGMEM = "PS Center GEQ@Speed,Intensity,Rotation Speed,Color Change,Nozzle,,Direction;;!;2f;pal=13,ix=180,c1=0,c2=0,c3=8"; +static const char _data_FX_MODE_PARTICLECIRCULARGEQ[] PROGMEM = "PS GEQ Nova@Speed,Intensity,Rotation Speed,Color Change,Nozzle,,Direction;;!;2f;pal=13,ix=180,c1=0,c2=0,c3=8"; /* Particle replacement of Ghost Rider by DedeHai (Damian Schneider), original by stepko adapted by Blaz Kristan (AKA blazoncek) @@ -8922,7 +8920,6 @@ PS Blobs: large particles bouncing around, changing size and form Uses palette for particle color by DedeHai (Damian Schneider) */ - uint16_t mode_particleblobs(void) { ParticleSystem2D *PartSys = NULL; @@ -9000,7 +8997,6 @@ static const char _data_FX_MODE_PARTICLEBLOBS[] PROGMEM = "PS Blobs@Speed,Blobs, * particles move, then split to form a fractal tree EXPERIMENTAL and non working! * by DedeHai (Damian Schneider) */ - uint16_t mode_particlefractal(void) { ParticleSystem2D *PartSys = NULL; uint32_t i; @@ -9060,7 +9056,6 @@ uint16_t mode_particlefractal(void) { PartSys->update(); // update and render return FRAMETIME; } - static const char _data_FX_MODE_PARTICLEFRACTAL[] PROGMEM = "PS fractal (exp)@Speed,Intensity,Base angle,branch angle,Blur,,Direction;;!;2f;pal=13,ix=180,c1=0,c2=0,c3=8"; #endif //WLED_DISABLE_PARTICLESYSTEM2D @@ -9078,7 +9073,6 @@ Particle Drip replacement, also replaces Rain Uses palette for particle color by DedeHai (Damian Schneider) */ - uint16_t mode_particleDrip(void) { ParticleSystem1D *PartSys = NULL; //uint8_t numSprays; @@ -9184,7 +9178,6 @@ Also replaces rolling balls and juggle (and maybe popcorn) Uses palette for particle color by DedeHai (Damian Schneider) */ - uint16_t mode_particleBouncingBalls(void) { ParticleSystem1D *PartSys = NULL; @@ -9274,7 +9267,6 @@ By Steve Pomeroy @xxv" Uses palette for particle color by DedeHai (Damian Schneider) */ - uint16_t mode_particleDancingShadows(void) { ParticleSystem1D *PartSys = NULL; @@ -9391,7 +9383,6 @@ Particle Fireworks 1D replacement Uses palette for particle color by DedeHai (Damian Schneider) */ - uint16_t mode_particleFireworks1D(void) { ParticleSystem1D *PartSys = NULL; uint8_t *forcecounter; @@ -9400,7 +9391,7 @@ uint16_t mode_particleFireworks1D(void) { if (!initParticleSystem1D(PartSys, 4, 150, 4, true)) // init return mode_static(); // allocation failed or is single pixel PartSys->setKillOutOfBounds(true); - PartSys->sources[0].sourceFlags.perpetual = 1; // set rocket state to standby + PartSys->sources[0].sourceFlags.custom1 = 1; // set rocket state to standby } else PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS @@ -9455,7 +9446,7 @@ uint16_t mode_particleFireworks1D(void) { rocketgravity = -rocketgravity; speed = -speed; } - PartSys->applyForce(PartSys->sources[0].source, rocketgravity, forcecounter[0]); + PartSys->applyForce(PartSys->sources[0].source, PartSys->sources[0].sourceFlags, rocketgravity, forcecounter[0]); PartSys->particleMoveUpdate(PartSys->sources[0].source, PartSys->sources[0].sourceFlags); PartSys->particleMoveUpdate(PartSys->sources[0].source, PartSys->sources[0].sourceFlags); // increase speed by calling the move function twice, also ages twice uint32_t rocketheight = SEGENV.aux0 ? PartSys->maxX - PartSys->sources[0].source.x : PartSys->sources[0].source.x; @@ -9480,7 +9471,7 @@ uint16_t mode_particleFireworks1D(void) { } } } - if((SEGMENT.call & 0x01) == 0 && PartSys->sources[0].sourceFlags.perpetual == false) // every second frame and not in standby + if((SEGMENT.call & 0x01) == 0 && PartSys->sources[0].sourceFlags.custom1 == false) // every second frame and not in standby PartSys->sprayEmit(PartSys->sources[0]); // emit exhaust particle if((SEGMENT.call & 0x03) == 0) // every fourth frame PartSys->applyFriction(1); // apply friction to all particles @@ -9501,7 +9492,6 @@ Particle based Sparkle effect Uses palette for particle color by DedeHai (Damian Schneider) */ - uint16_t mode_particleSparkler(void) { ParticleSystem1D *PartSys = NULL; uint32_t numSparklers; @@ -9565,12 +9555,12 @@ uint16_t mode_particleSparkler(void) { return FRAMETIME; } static const char _data_FX_MODE_PS_SPARKLER[] PROGMEM = "PS Sparkler@Speed,!,Saturation,Blur,Sparklers,Direction,Wrap/Bounce,Smooth;,!;!;1;pal=0,sx=50,ix=200,c1=0,c2=0,c3=0,o1=1,o2=1"; + /* Particle based Hourglass, particles falling at defined intervals Uses palette for particle color by DedeHai (Damian Schneider) */ - uint16_t mode_particleHourglass(void) { ParticleSystem1D *PartSys = NULL; int32_t positionoffset; // resting position offset @@ -9595,41 +9585,46 @@ uint16_t mode_particleHourglass(void) { PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 1, 255)); PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur PartSys->setGravity(map(SEGMENT.custom3, 0, 31, 1, 30)); - PartSys->enableParticleCollisions(true, 34); // fixed hardness, 34 is a value that works best in most settings (spent a long time optimizing) SEGMENT.custom1); + PartSys->enableParticleCollisions(true, 34); // fixed hardness, 34 is a value that works best in most settings (spent a long time optimizing) positionoffset = PS_P_RADIUS_1D / 2; uint32_t colormode = SEGMENT.custom1 >> 5; // 0-7 - if((SEGMENT.intensity | (PartSys->getAvailableParticles() << 8)) != SEGENV.step) { //initialize, getAvailableParticles changes while in FX transition + if((SEGMENT.intensity | (PartSys->getAvailableParticles() << 8)) != SEGENV.step) { // initialize, getAvailableParticles changes while in FX transition if(PartSys->getAvailableParticles() == SEGENV.step >> 8) // only intensity slider changed or first call *basehue = hw_random16(); //choose new random color SEGENV.step = SEGMENT.intensity | (PartSys->getAvailableParticles() << 8); for(uint32_t i = 0; i < PartSys->usedParticles; i++) { PartSys->particleFlags[i].reversegrav = true; *direction = 0; - SEGENV.aux1 = 1; //initialize below + SEGENV.aux1 = 1; // initialize below } - SEGENV.aux0 = PartSys->usedParticles - 1; //initial state, start with highest number particle + SEGENV.aux0 = PartSys->usedParticles - 1; // initial state, start with highest number particle } - for(uint32_t i = 0; i < PartSys->usedParticles; i++) { //check if particle reached target position after falling + for(uint32_t i = 0; i < PartSys->usedParticles; i++) { // check if particle reached target position after falling int32_t targetposition; if (PartSys->particleFlags[i].fixed == false) { - //calculate target position depending on direction - if(PartSys->particleFlags[i].reversegrav) + // calculate target position depending on direction + if(PartSys->particleFlags[i].reversegrav) { targetposition = PartSys->maxX - (i * PS_P_RADIUS_1D + positionoffset); // target resting position - else + if(PartSys->particles[i].x >= targetposition) // particle has reached target position, pin it. if not pinned, they do not stack well on larger piles + PartSys->particleFlags[i].fixed = true; + } + else { targetposition = (PartSys->usedParticles - i) * PS_P_RADIUS_1D - positionoffset; // target resting position - if(PartSys->particles[i].x == targetposition) //particle has reached target position, pin it. if not pinned, they do not stack well on larger piles - PartSys->particleFlags[i].fixed = true; + if(PartSys->particles[i].x <= targetposition) // particle has reached target position, pin it. if not pinned, they do not stack well on larger piles + PartSys->particleFlags[i].fixed = true; + } + } if(colormode == 7) - PartSys->setColorByPosition(true); //color fixed by position + PartSys->setColorByPosition(true); // color fixed by position else { PartSys->setColorByPosition(false); switch(colormode) { - case 0: PartSys->particles[i].hue = 120; break; //fixed at 120, if flip is activated, this can make red and green (use palette 34) - case 1: PartSys->particles[i].hue = *basehue; break; //fixed random color + case 0: PartSys->particles[i].hue = 120; break; // fixed at 120, if flip is activated, this can make red and green (use palette 34) + case 1: PartSys->particles[i].hue = *basehue; break; // fixed random color case 2: case 3: PartSys->particles[i].hue = *basehue + (i % colormode)*70; break; // interleved colors (every 2 or 3 particles) case 4: PartSys->particles[i].hue = *basehue + (i * 255) / PartSys->usedParticles; break; // gradient palette colors @@ -9642,7 +9637,7 @@ uint16_t mode_particleHourglass(void) { PartSys->particles[i].hue += 120; } - if(SEGENV.aux1 == 1) { //last countdown call before dropping starts, reset all particles + if(SEGENV.aux1 == 1) { // last countdown call before dropping starts, reset all particles for(uint32_t i = 0; i < PartSys->usedParticles; i++) { PartSys->particleFlags[i].collide = true; PartSys->particleFlags[i].perpetual = true; @@ -9659,31 +9654,31 @@ uint16_t mode_particleHourglass(void) { } } - if(SEGENV.aux1 == 0) { //countdown passed, run + if(SEGENV.aux1 == 0) { // countdown passed, run uint32_t interval = 257 - SEGMENT.speed; // drop interval in frames, 1 second is 'speed = (257 - FPS)' speed = 0 is one drop every 257 frames if(SEGMENT.check3 && *direction) // fast reset interval = 3; - if(SEGMENT.call % interval == 0) { //drop a particle, do not drop more often than every second frame or particles tangle up quite badly + if(SEGMENT.call % interval == 0) { // drop a particle, do not drop more often than every second frame or particles tangle up quite badly if(SEGENV.aux0 < PartSys->usedParticles) { - PartSys->particleFlags[SEGENV.aux0].reversegrav = *direction; //let this particle fall or rise + PartSys->particleFlags[SEGENV.aux0].reversegrav = *direction; // let this particle fall or rise PartSys->particleFlags[SEGENV.aux0].fixed = false; // unpin } - else { //overflow, flip direction + else { // overflow, flip direction *direction = !(*direction); - SEGENV.aux1 = 300; //set countdown + SEGENV.aux1 = SEGMENT.virtualLength() + 100; // set countdown } - if(*direction == 0) //down + if(*direction == 0) // down SEGENV.aux0--; else SEGENV.aux0++; } } - else if(SEGMENT.check2) //auto reset - SEGENV.aux1--; //countdown + else if(SEGMENT.check2) // auto reset + SEGENV.aux1--; // countdown //if(SEGMENT.call % (SEGMENT.speed >> 5) == 0) //more friction on higher falling rate to keep particles behaved //if(SEGMENT.call % 6 == 0) - //PartSys->applyFriction(1); //keeps particles calm and stops mass collisions being handled improperly due to chaos + //PartSys->applyFriction(1); //keeps particles calm and stops mass collisions being handled improperly due to chaos TODO: can this be removed? seems to work fine without. PartSys->update(); // update and render @@ -9696,7 +9691,6 @@ Particle based Spray effect (like a volcano, possible replacement for popcorn) Uses palette for particle color by DedeHai (Damian Schneider) */ - uint16_t mode_particle1Dspray(void) { ParticleSystem1D *PartSys = NULL; @@ -9741,22 +9735,23 @@ uint16_t mode_particle1Dspray(void) { return FRAMETIME; } static const char _data_FX_MODE_PS_1DSPRAY[] PROGMEM = "PS 1D Spray@!,!,Position,Blur,Gravity,AgeColor,Bounce,Position Color;,!;!;1;pal=35,sx=200,ix=220,c1=4,c2=0,c3=28,o1=1,o2=1"; + /* Particle based balance: particles move back and forth (1D pendent to 2D particle box) Uses palette for particle color by DedeHai (Damian Schneider) */ - uint16_t mode_particleBalance(void) { ParticleSystem1D *PartSys = NULL; uint32_t i; if (SEGMENT.call == 0) { // initialization - if (!initParticleSystem1D(PartSys, 1)) // init, no additional data needed + if (!initParticleSystem1D(PartSys, 1, 128)) // init, no additional data needed, use half of max particles return mode_static(); // allocation failed or is single pixel //PartSys->setKillOutOfBounds(true); PartSys->setParticleSize(1); SEGENV.aux0 = 0; + SEGENV.aux1 = 0; //TODO: really need to set to zero or is it calloc'd? } else PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS @@ -9768,10 +9763,8 @@ uint16_t mode_particleBalance(void) { PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur PartSys->setBounce(!SEGMENT.check2); PartSys->setWrap(SEGMENT.check2); - uint8_t hardness = map(SEGMENT.custom1, 0, 255, 50, 250); + uint8_t hardness = SEGMENT.custom1 > 0 ? map(SEGMENT.custom1, 0, 255, 50, 250) : 200; // set hardness, make the walls hard if collisions are disabled PartSys->enableParticleCollisions(SEGMENT.custom1, hardness); // enable collisions if custom1 > 0 - if(SEGMENT.custom1 == 0) //collisions disabled, make the walls hard - hardness = 200; PartSys->setWallHardness(hardness); PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 10, 255)); if(PartSys->usedParticles > SEGENV.aux1) { // more particles, reinitialize @@ -9794,13 +9787,17 @@ uint16_t mode_particleBalance(void) { else // sinusoidal xgravity = (int16_t)cos8(SEGENV.aux0) - 128;//((int32_t)(SEGMENT.custom3 << 2) * cos8(SEGENV.aux0) // scale the force - xgravity = (xgravity * ((SEGMENT.custom3+1) << 2)) / 128; + xgravity = (xgravity * ((SEGMENT.custom3+1) << 2)) / 128; // xgravity: -127 to +127 PartSys->applyForce(xgravity); } uint32_t randomindex = hw_random16(PartSys->usedParticles); PartSys->particles[randomindex].vx = ((int32_t)PartSys->particles[randomindex].vx * 200) / 255; // apply friction to random particle to reduce clumping (without collisions) + //if(SEGMENT.check2 && (SEGMENT.call & 0x07) == 0) // no walls, apply friction to smooth things out + if((SEGMENT.call & 0x0F) == 0) // apply friction every 16th frame to smooth things out + PartSys->applyFriction(1); // apply friction to all particles + //update colors PartSys->setColorByPosition(SEGMENT.check1); if(!SEGMENT.check1) { @@ -9811,7 +9808,7 @@ uint16_t mode_particleBalance(void) { PartSys->update(); // update and render return FRAMETIME; } -static const char _data_FX_MODE_PS_BALANCE[] PROGMEM = "PS 1D Balance@!,!,Collide,Blur,Tilt,Position Color,Wrap,Random;,!;!;1;pal=18,sx=100,ix=40,c1=200,c2=0,c3=5,o1=1"; +static const char _data_FX_MODE_PS_BALANCE[] PROGMEM = "PS 1D Balance@!,!,Hardness,Blur,Tilt,Position Color,Wrap,Random;,!;!;1;pal=18,sx=100,ix=40,c1=200,c2=0,c3=5,o1=1"; /* Particle based Chase effect @@ -10054,7 +10051,7 @@ uint16_t mode_particle1DGEQ(void) { return FRAMETIME; } -static const char _data_FX_MODE_PS_1D_GEQ[] PROGMEM = "PS 1D GEQ@Speed,!,Size,Blur,,,,;,!;!;1f;pal=0,sx=50,ix=200,c1=0,c2=0,c3=0,o1=1,o2=1"; +static const char _data_FX_MODE_PS_1D_GEQ[] PROGMEM = "PS GEQ 1D@Speed,!,Size,Blur,,,,;,!;!;1f;pal=0,sx=50,ix=200,c1=0,c2=0,c3=0,o1=1,o2=1"; /* Particle based Fire effect diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index 570a1b1424..27f854d88c 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -173,15 +173,13 @@ bool IRAM_ATTR_YN Segment::allocateData(size_t len) { void IRAM_ATTR_YN Segment::deallocateData() { if (!data) { _dataLen = 0; return; } - Serial.printf(PSTR("--- Released data (%p): %d/%d -> %p\n"), this, _dataLen, Segment::getUsedSegmentData(), data); + //DEBUG_PRINTF_P(PSTR("--- Released data (%p): %d/%d -> %p\n"), this, _dataLen, Segment::getUsedSegmentData(), data); if ((Segment::getUsedSegmentData() > 0) && (_dataLen > 0)) { // check that we don't have a dangling / inconsistent data pointer - Serial.println("releasing segment data"); free(data); } else { DEBUG_PRINTF_P(PSTR("---- Released data (%p): inconsistent UsedSegmentData (%d/%d), cowardly refusing to free nothing.\n"), this, _dataLen, Segment::getUsedSegmentData()); } data = nullptr; - Serial.println("reducing used data by" + String(_dataLen)); Segment::addUsedSegmentData(_dataLen <= Segment::getUsedSegmentData() ? -_dataLen : -Segment::getUsedSegmentData()); _dataLen = 0; } diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index b3ffd66152..fac9532af7 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -85,10 +85,8 @@ void ParticleSystem2D::update(void) { handleCollisions(); //move all particles - //TODO: split this loop into two separate loops? avoids repeated null checking which is faster. - for (uint32_t i = 0; i < usedParticles; i++) { - particleMoveUpdate(particles[i], particleFlags[i], &particlesettings, advPartProps ? &advPartProps[i] : nullptr); + particleMoveUpdate(particles[i], particleFlags[i], nullptr, advPartProps ? &advPartProps[i] : nullptr); // note: splitting this into two loops is slower and uses more flash } ParticleSys_render(); @@ -768,6 +766,9 @@ void ParticleSystem2D::renderParticle(const uint32_t particleindex, const uint32 uint32_t yfb_orig = y - (rendersize>>1) + 1 - offset; uint32_t xfb, yfb; // coordinates in frame buffer to write to note: by making this uint, only overflow has to be checked (spits a warning though) + //note on y-axis flip: WLED has the y-axis defined from top to bottom, so y coordinates must be flipped. doing this in the buffer xfer clashes with 1D/2D combined rendering, which does not invert y + // transferring the 1D buffer in inverted fashion will flip the x-axis of overlaid 2D FX, so the y-axis flip is done here so the buffer is flipped in y, giving correct results + // transfer particle renderbuffer to framebuffer for (uint32_t xrb = offset; xrb < rendersize + offset; xrb++) { xfb = xfb_orig + xrb; @@ -787,7 +788,7 @@ void ParticleSystem2D::renderParticle(const uint32_t particleindex, const uint32 continue; } if (framebuffer) - fast_color_add(framebuffer[xfb + yfb * (maxXpixel + 1)], renderbuffer[xrb + yrb * 10]); + fast_color_add(framebuffer[xfb + (maxYpixel - yfb) * (maxXpixel + 1)], renderbuffer[xrb + yrb * 10]); else SEGMENT.addPixelColorXY(xfb, maxYpixel - yfb, renderbuffer[xrb + yrb * 10],true); } @@ -826,7 +827,7 @@ void ParticleSystem2D::renderParticle(const uint32_t particleindex, const uint32 if (framebuffer) { for (uint32_t i = 0; i < 4; i++) { if (pixelvalid[i]) - fast_color_add(framebuffer[pixco[i][0] + pixco[i][1] * (maxXpixel + 1)], color, pxlbrightness[i]); // order is: bottom left, bottom right, top right, top left + fast_color_add(framebuffer[pixco[i][0] + (maxYpixel - pixco[i][1]) * (maxXpixel + 1)], color, pxlbrightness[i]); // order is: bottom left, bottom right, top right, top left } } else { @@ -848,28 +849,32 @@ void ParticleSystem2D::handleCollisions() { // note: partices are binned in x-axis, assumption is that no more than half of the particles are in the same bin // if they are, collisionStartIdx is increased so each particle collides at least every second frame (which still gives decent collisions) constexpr uint32_t BIN_WIDTH = 6 * PS_P_RADIUS; // width of a bin in sub-pixels - uint32_t maxBinParticles = (usedParticles + 1) / 2; // assume no more than half of the particles are in the same bin - uint32_t numBins = (maxX + (BIN_WIDTH -1)) / BIN_WIDTH; // number of bins in x direction + uint32_t maxBinParticles = max((uint32_t)50, (usedParticles + 1) / 2); // assume no more than half of the particles are in the same bin, do not bin small amounts of particles + uint32_t numBins = (maxX + (BIN_WIDTH - 1)) / BIN_WIDTH; // number of bins in x direction uint16_t binIndices[maxBinParticles]; // creat array on stack for indices, 2kB max for 1024 particles (ESP32_MAXPARTICLES/2) uint32_t binParticleCount; // number of particles in the current bin uint16_t nextFrameStartIdx = 0; // index of the first particle in the next frame (set if bin overflow) - + uint32_t pidx = collisionStartIdx; //start index in case a bin is full, process remaining particles next frame + + // fill the binIndices array for this bin for (uint32_t bin = 0; bin < numBins; bin++) { binParticleCount = 0; // reset for this bin int32_t binStart = bin * BIN_WIDTH; int32_t binEnd = binStart + BIN_WIDTH; // fill the binIndices array for this bin - for (uint32_t i = collisionStartIdx; i < usedParticles; i++) { - if (particles[i].ttl > 0 && particleFlags[i].outofbounds == 0 && particleFlags[i].collide) { // colliding particle - if (particles[i].x >= binStart && particles[i].x <= binEnd) { // >= and <= to include particles on the edge of the bin (overlap to ensure boarder particles collide with adjacent bins) + for (uint32_t i = 0; i < usedParticles; i++) { + if (particles[pidx].ttl > 0 && particleFlags[pidx].outofbounds == 0 && particleFlags[pidx].collide) { // colliding particle + if (particles[pidx].x >= binStart && particles[pidx].x <= binEnd) { // >= and <= to include particles on the edge of the bin (overlap to ensure boarder particles collide with adjacent bins) if (binParticleCount >= maxBinParticles) { // bin is full, more particles in this bin so do the rest next frame - nextFrameStartIdx = i; // bin overflow can only happen once as bin size is at least half of the particles (or half +1) + nextFrameStartIdx = pidx; // bin overflow can only happen once as bin size is at least half of the particles (or half +1) break; } - binIndices[binParticleCount++] = i; + binIndices[binParticleCount++] = pidx; } } + pidx++; + if (pidx >= usedParticles) pidx = 0; // wrap around } for (uint32_t i = 0; i < binParticleCount; i++) { // go though all 'higher number' particles in this bin and see if any of those are in close proximity and if they are, make them collide @@ -961,7 +966,7 @@ void ParticleSystem2D::collideParticles(PSparticle &particle1, PSparticle &parti else particle1.x--; } - particle1.vx += push; //TODO: what happens if particle2 is also pushed? in 1D it stacks better, maybe also just reverse the comparison order so they flip roles? + particle1.vx += push; push = 0; if (dy < 0) push = pushamount; @@ -1212,7 +1217,7 @@ ParticleSystem1D::ParticleSystem1D(uint32_t length, uint32_t numberofparticles, motionBlur = 0; //no fading by default smearBlur = 0; //no smearing by default emitIndex = 0; - + collisionStartIdx = 0; // initialize some default non-zero values most FX use for (uint32_t i = 0; i < numSources; i++) { sources[i].source.ttl = 1; //set source alive @@ -1238,7 +1243,7 @@ void ParticleSystem1D::update(void) { //move all particles for (uint32_t i = 0; i < usedParticles; i++) { - particleMoveUpdate(particles[i], particleFlags[i], &particlesettings, advPartProps ? &advPartProps[i] : nullptr); + particleMoveUpdate(particles[i], particleFlags[i], nullptr, advPartProps ? &advPartProps[i] : nullptr); } if (particlesettings.colorByPosition) { @@ -1457,17 +1462,19 @@ void ParticleSystem1D::applyGravity() { if (particleFlags[i].reversegrav) dv = -dv_raw; // note: not checking if particle is dead is omitted as most are usually alive and if few are alive, rendering is fast anyways particles[i].vx = limitSpeed((int32_t)particles[i].vx - dv); + particleFlags[i].forcedirection = particleFlags[i].reversegrav; // set force direction flag (for collisions) } } // apply gravity to single particle using system settings (use this for sources) // function does not increment gravity counter, if gravity setting is disabled, this cannot be used -void ParticleSystem1D::applyGravity(PSparticle1D &part, bool reverse) { +void ParticleSystem1D::applyGravity(PSparticle1D &part, PSparticleFlags1D &partFlags) { uint32_t counterbkp = gforcecounter; int32_t dv = calcForce_dv(gforce, gforcecounter); - if (reverse) dv = -dv; + if (partFlags.reversegrav) dv = -dv; gforcecounter = counterbkp; //save it back part.vx = limitSpeed((int32_t)part.vx - dv); + partFlags.forcedirection = partFlags.reversegrav; // set force direction flag (for collisions) } @@ -1504,7 +1511,6 @@ void ParticleSystem1D::ParticleSys_render() { bool bufferNeedsUpdate = (!pmem->inTransition || pmem->inTransition == effectID); // not a transition; or new FX: update buffer (blur, or clear) if(bufferNeedsUpdate) { if (globalBlur > 0 || globalSmear > 0) { // blurring active: if not a transition or is newFX, read data from segment before blurring (old FX can render to it afterwards) - // Serial.println(" blurring: " + String(globalBlur)); for (int32_t x = 0; x <= maxXpixel; x++) { if (!renderSolo) // sharing the framebuffer with another segment: read buffer back from segment framebuffer[x] = SEGMENT.getPixelColor(x); // copy to local buffer @@ -1513,7 +1519,6 @@ void ParticleSystem1D::ParticleSys_render() { } else { // no blurring: clear buffer memset(framebuffer, 0, frameBufferSize * sizeof(CRGB)); - //Serial.print(" clearing "); } } } @@ -1663,30 +1668,40 @@ void ParticleSystem1D::renderParticle(const uint32_t particleindex, const uint32 // detect collisions in an array of particles and handle them void ParticleSystem1D::handleCollisions() { int32_t collisiondistance = PS_P_MINHARDRADIUS_1D; - - constexpr uint32_t BIN_WIDTH = 32 * PS_P_RADIUS_1D; // width of each bin, 32 pixels gives good results, smaller is slower, larger also gets slower, 48 is also still ok - uint32_t maxBinParticles = (usedParticles + 1) / 4; // assume no more than 1/4 of the particles are in the same bin - uint32_t numBins = (maxX + 1) / BIN_WIDTH; // calculate number of bins + // note: partices are binned by position, assumption is that no more than half of the particles are in the same bin + // if they are, collisionStartIdx is increased so each particle collides at least every second frame (which still gives decent collisions) + constexpr uint32_t BIN_WIDTH = 32 * PS_P_RADIUS_1D; // width of each bin, a compromise between speed and accuracy (lareger bins are faster but collapse more) + uint32_t maxBinParticles = max((uint32_t)50, (usedParticles + 1) / 4); // do not bin small amounts, limit max to 1/2 of particles + uint32_t numBins = (maxX + (BIN_WIDTH - 1)) / BIN_WIDTH; // calculate number of bins uint16_t binIndices[maxBinParticles]; // array to store indices of particles in a bin uint32_t binParticleCount; // number of particles in the current bin uint16_t nextFrameStartIdx = 0; // index of the first particle in the next frame (set if bin overflow) - + uint32_t pidx = collisionStartIdx; //start index in case a bin is full, process remaining particles next frame for (uint32_t bin = 0; bin < numBins; bin++) { binParticleCount = 0; // reset for this bin int32_t binStart = bin * BIN_WIDTH; int32_t binEnd = binStart + BIN_WIDTH; // fill the binIndices array for this bin - for (uint32_t i = collisionStartIdx; i < usedParticles; i++) { - if (particles[i].ttl > 0 && particleFlags[i].outofbounds == 0 && particleFlags[i].collide) { // colliding particle - if (particles[i].x >= binStart && particles[i].x <= binEnd) { // >= and <= to include particles on the edge of the bin (overlap to ensure boarder particles collide with adjacent bins) + for (uint32_t i = 0; i < usedParticles; i++) { + if (particles[pidx].ttl > 0 && particleFlags[pidx].outofbounds == 0 && particleFlags[pidx].collide) { // colliding particle + // if gravity is not used and wall bounce is enabled: particles in the first or last bin use fixed force direction (no collapsing, no push inversion) + if (!particlesettings.useGravity && particlesettings.bounce) { + if (particles[pidx].x < BIN_WIDTH) + particleFlags[pidx].forcedirection = false; + else if (particles[pidx].x > (maxX - BIN_WIDTH)) + particleFlags[pidx].forcedirection = true; + } + if (particles[pidx].x >= binStart && particles[pidx].x <= binEnd) { // >= and <= to include particles on the edge of the bin (overlap to ensure boarder particles collide with adjacent bins) if (binParticleCount >= maxBinParticles) { // bin is full, more particles in this bin so do the rest next frame - nextFrameStartIdx = i; // bin overflow can only happen once as bin size is at least half of the particles (or half +1) + nextFrameStartIdx = pidx; // bin overflow can only happen once as bin size is at least half of the particles (or half +1) break; } - binIndices[binParticleCount++] = i; + binIndices[binParticleCount++] = pidx; } } + pidx++; + if (pidx >= usedParticles) pidx = 0; // wrap around } for (uint32_t i = 0; i < binParticleCount; i++) { // go though all 'higher number' particles and see if any of those are in close proximity and if they are, make them collide @@ -1707,15 +1722,14 @@ void ParticleSystem1D::handleCollisions() { } } } + collisionStartIdx = nextFrameStartIdx; // set the start index for the next frame } - // 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 ParticleSystem1D::collideParticles(PSparticle1D &particle1, const PSparticleFlags1D &particle1flags, PSparticle1D &particle2, const PSparticleFlags1D &particle2flags, int32_t dx, int32_t relativeVx, uint32_t collisiondistance) { int32_t dotProduct = (dx * relativeVx); // is always negative if moving towards each other if (dotProduct < 0) { // particles are moving towards each other uint32_t surfacehardness = max(collisionHardness, (int32_t)PS_P_MINSURFACEHARDNESS_1D); // if particles are soft, the impulse must stay above a limit or collisions slip through - // TODO: if soft collisions are not needed, the above line can be done in set hardness function and skipped here (which is what it currently looks like) // Calculate new velocities after collision int32_t impulse = relativeVx * surfacehardness / 255; particle1.vx += impulse; @@ -1727,8 +1741,8 @@ void ParticleSystem1D::collideParticles(PSparticle1D &particle1, const PSparticl else if (particle2flags.fixed) particle1.vx = -particle2.vx; - if (collisionHardness < PS_P_MINSURFACEHARDNESS_1D) { // if particles are soft, they become 'sticky' i.e. apply some friction (they do pile more nicely and correctly) - const uint32_t coeff = collisionHardness + (255 - PS_P_MINSURFACEHARDNESS_1D); + if (collisionHardness < PS_P_MINSURFACEHARDNESS_1D) { // if particles are soft, they become 'sticky' i.e. apply some friction + const uint32_t coeff = collisionHardness + (250 - PS_P_MINSURFACEHARDNESS_1D); particle1.vx = ((int32_t)particle1.vx * coeff) / 255; particle2.vx = ((int32_t)particle2.vx * coeff) / 255; } @@ -1738,34 +1752,36 @@ void ParticleSystem1D::collideParticles(PSparticle1D &particle1, const PSparticl // particles have volume, push particles apart if they are too close // behaviour is different than in 2D, we need pixel accurate stacking here, push the top particle to full radius (direction is well defined in 1D) // also need to give the top particle some speed to counteract gravity or stacks just collapse - if (distance < collisiondistance) { //particles are too close, push the upper particle away + if (distance < collisiondistance) { // particles are too close, push the upper particle away int32_t pushamount = 1 + ((collisiondistance - distance) >> 1); //add half the remaining distance note: this works best, if less or more is added, it gets more chaotic - //int32_t pushamount = collisiondistance - distance; - if (particlesettings.useGravity) { //using gravity, push the 'upper' particle only - if (dx < 0) { // particle2.x < particle1.x - if (particle2flags.reversegrav && !particle2flags.fixed) { + + // Only force-push if particles use gravity or are not really close or are in the outer quarter of the strip + if (particlesettings.bounce && (particlesettings.useGravity || distance > 3 || particle1.x < (maxX >> 2) || particle1.x > (maxX - (maxX >> 2)))) { + // use force direction flag to push the 'upper' particle only, avoids stack-collapse + if (dx < 0) { // particle2.x < particle1.x, dx = p2.x - p1.x + if (particle2flags.forcedirection && !particle2flags.fixed) { particle2.x -= pushamount; particle2.vx--; - } else if (!particle1flags.reversegrav && !particle1flags.fixed) { + } else if (!particle1flags.forcedirection && !particle1flags.fixed) { particle1.x += pushamount; particle1.vx++; } - } else { - if (particle1flags.reversegrav && !particle1flags.fixed) { + } else { // particle1.x < particle2.x, dx = p2.x - p1.x + if (particle1flags.forcedirection && !particle1flags.fixed) { particle1.x -= pushamount; particle1.vx--; - } else if (!particle2flags.reversegrav && !particle2flags.fixed) { + } else if (!particle2flags.forcedirection && !particle2flags.fixed) { particle2.x += pushamount; particle2.vx++; } - } } - else { //not using gravity, push both particles by applying a little velocity (like in 2D system), results in much nicer stacking when applying forces - pushamount = 1; - if (dx < 0) // particle2.x < particle1.x - pushamount = -1; - particle1.vx -= pushamount; - particle2.vx += pushamount; + } + else { // no wall bounce, not using gravity, push both particles by applying a little velocity (like in 2D system) + pushamount = 2; + if (dx < 0) // particle2.x < particle1.x + pushamount = -pushamount; + particle1.vx -= pushamount; + particle2.vx += pushamount; } } } @@ -2199,6 +2215,9 @@ partMem* getPartMem(void) { // TODO: maybe there is a better/faster way than usi void updateRenderingBuffer(uint32_t requiredpixels, bool isFramebuffer, bool initialize) { PSPRINTLN("updateRenderingBuffer"); uint16_t& targetBufferSize = isFramebuffer ? frameBufferSize : renderBufferSize; // corresponding buffer size + + //if(isFramebuffer) return; // debug only: disable frame-buffer buffer + if(targetBufferSize < requiredpixels) { // check current buffer size CRGB** targetBuffer = isFramebuffer ? &framebuffer : &renderbuffer; // pointer to target buffer if(*targetBuffer || initialize) { // update only if initilizing or if buffer exists (prevents repeatet allocation attempts if initial alloc failed) @@ -2261,16 +2280,13 @@ void transferBuffer(uint32_t width, uint32_t height, bool useAdditiveTransfer) { #endif if(height) { // is 2D, 1D passes height = 0 - int32_t yflipped; - height--; // height is number of pixels, convert to array index - for (uint32_t y = 0; y <= height; y++) { - yflipped = height - y; + for (uint32_t y = 0; y < height; y++) { int index = y * width; // current row index for 1D buffer for (uint32_t x = 0; x < width; x++) { CRGB *c = &framebuffer[index++]; uint32_t clr = RGBW32(c->r,c->g,c->b,0); // convert to 32bit color if(useAdditiveTransfer) { - uint32_t segmentcolor = SEGMENT.getPixelColorXY((int)x, (int)yflipped); + uint32_t segmentcolor = SEGMENT.getPixelColorXY((int)x, (int)y); CRGB segmentRGB = CRGB(segmentcolor); if(clr == 0) // frame buffer is black, just update the framebuffer *c = segmentRGB; @@ -2279,12 +2295,12 @@ void transferBuffer(uint32_t width, uint32_t height, bool useAdditiveTransfer) { fast_color_add(*c, segmentRGB); // add segment color back to buffer if not black clr = RGBW32(c->r,c->g,c->b,0); // convert to 32bit color (again) TODO: could convert first, then use 32bit adding function color_add() from colors.cpp } - SEGMENT.setPixelColorXY((int)x, (int)yflipped, clr); // save back to segment after adding local buffer + SEGMENT.setPixelColorXY((int)x, (int)y, clr); // save back to segment after adding local buffer } } //if(clr > 0) // not black TODO: not transferring black is faster and enables overlay, but requires proper handling of buffer clearing, which is quite complex and probably needs a change to SEGMENT handling. else - SEGMENT.setPixelColorXY((int)x, (int)yflipped, clr); + SEGMENT.setPixelColorXY((int)x, (int)y, clr); } } } else { // 1D system diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index f9c44ccac1..f1066f2fd7 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -116,7 +116,7 @@ typedef struct { // 2 bytes } PSadvancedParticle; // struct for advanced particle size control (option) -typedef struct { // 7 bytes +typedef struct { // 8 bytes uint8_t asymmetry; // asymmetrical size (0=symmetrical, 255 fully asymmetric) uint8_t asymdir; // direction of asymmetry, 64 is x, 192 is y (0 and 128 is symmetrical) uint8_t maxsize; // target size for growing @@ -133,7 +133,7 @@ typedef struct { // 7 bytes } PSsizeControl; -//struct for a particle source (17 bytes) +//struct for a particle source (20 bytes) typedef struct { uint16_t minLife; // minimum ttl of emittet particles uint16_t maxLife; // maximum ttl of emitted particles @@ -252,9 +252,9 @@ bool allocateParticleSystemMemory2D(const uint32_t numparticles, const uint32_t // memory allocation #define ESP8266_MAXPARTICLES_1D 450 #define ESP8266_MAXSOURCES_1D 16 -#define ESP32S2_MAXPARTICLES_1D 1600 +#define ESP32S2_MAXPARTICLES_1D 1300 #define ESP32S2_MAXSOURCES_1D 32 -#define ESP32_MAXPARTICLES_1D 3200 +#define ESP32_MAXPARTICLES_1D 2600 #define ESP32_MAXSOURCES_1D 64 // particle dimensions (subpixel division) @@ -262,8 +262,8 @@ bool allocateParticleSystemMemory2D(const uint32_t numparticles, const uint32_t #define PS_P_HALFRADIUS_1D (PS_P_RADIUS_1D >> 1) #define PS_P_RADIUS_SHIFT_1D 5 // 1 << PS_P_RADIUS_SHIFT = PS_P_RADIUS #define PS_P_SURFACE_1D 5 // shift: 2^PS_P_SURFACE = PS_P_RADIUS_1D -#define PS_P_MINHARDRADIUS_1D 32 // minimum hard surface radius -#define PS_P_MINSURFACEHARDNESS_1D 50 // minimum hardness used in collision impulse calculation +#define PS_P_MINHARDRADIUS_1D 32 // minimum hard surface radius note: do not change or hourglass effect will be broken +#define PS_P_MINSURFACEHARDNESS_1D 60 // minimum hardness used in collision impulse calculation // struct for PS settings (shared for 1D and 2D class) typedef union { @@ -296,10 +296,10 @@ typedef union { bool collide : 1; // if set, particle takes part in collisions bool perpetual : 1; // if set, particle does not age (TTL is not decremented in move function, it still dies from killoutofbounds) bool reversegrav : 1; // if set, gravity is reversed on this particle + bool forcedirection : 1; // direction the force was applied, 1 is positive x-direction (used for collision stacking, similar to reversegrav) bool fixed : 1; // if set, particle does not move (and collisions make other particles revert direction), bool custom1 : 1; // unused custom flags, can be used by FX to track particle states bool custom2 : 1; - bool custom3 : 1; }; byte asByte; // access as a byte, order is: LSB is first entry in the list above } PSparticleFlags1D; @@ -311,7 +311,7 @@ typedef struct { uint8_t forcecounter; } PSadvancedParticle1D; -//struct for a particle source (16 bytes) +//struct for a particle source (20 bytes) typedef struct { uint16_t minLife; // minimum ttl of emittet particles uint16_t maxLife; // maximum ttl of emitted particles @@ -321,6 +321,7 @@ typedef struct { int8_t v; // emitting speed uint8_t sat; // color saturation (advanced property) uint8_t size; // particle size (advanced property) + // note: there is 3 bytes of padding added here } PSsource1D; class ParticleSystem1D @@ -334,9 +335,9 @@ class ParticleSystem1D int32_t sprayEmit(const PSsource1D &emitter); void particleMoveUpdate(PSparticle1D &part, PSparticleFlags1D &partFlags, PSsettings1D *options = NULL, PSadvancedParticle1D *advancedproperties = NULL); // move function //particle physics - void applyForce(PSparticle1D &part, const int8_t xforce, uint8_t &counter); //apply a force to a single particle + void applyForce(PSparticle1D &part, PSparticleFlags1D &partFlags, const int8_t xforce, uint8_t &counter); //apply a force to a single particle void applyForce(const int8_t xforce); // apply a force to all particles - void applyGravity(PSparticle1D &part,const bool reverse); // applies gravity to single particle (use this for sources) + void applyGravity(PSparticle1D &part, PSparticleFlags1D &partFlags); // applies gravity to single particle (use this for sources) void applyFriction(const int32_t coefficient); // apply friction to all used particles // set options void setUsedParticles(const uint8_t percentage); // set the percentage of particles used in the system, 255=100% From eacaf2d392b8777c35b1cb867cdf7cb3a309c276 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sun, 12 Jan 2025 13:41:17 +0100 Subject: [PATCH 167/219] FX fixes and minor tweaks --- wled00/FX.cpp | 39 +++++++++++++-------------------------- wled00/FXparticleSystem.h | 2 +- 2 files changed, 14 insertions(+), 27 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 2aaaf4f9db..8b8d79228d 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -7777,7 +7777,7 @@ static const char _data_FX_MODE_PARTICLEVORTEX[] PROGMEM = "PS Vortex@Rotation S * Rockets shoot up and explode in a random color, sometimes in a defined pattern * by DedeHai (Damian Schneider) */ -#define NUMBEROFSOURCES 4 +#define NUMBEROFSOURCES 8 uint16_t mode_particlefireworks(void) { ParticleSystem2D *PartSys = NULL; @@ -7802,7 +7802,7 @@ uint16_t mode_particlefireworks(void) { return mode_static(); // something went wrong, no data! PartSys->updateSystem(); // update system properties (dimensions and data pointers) - numRockets = min(PartSys->numSources, (uint32_t)NUMBEROFSOURCES); + numRockets = map(SEGMENT.speed, 0 , 255, 3, min(PartSys->numSources, (uint32_t)NUMBEROFSOURCES)); PartSys->setWrapX(SEGMENT.check1); PartSys->setBounceY(SEGMENT.check2); @@ -8564,26 +8564,18 @@ uint16_t mode_particleattractor(void) { else PartSys->angleEmit(PartSys->sources[0], SEGENV.aux0 + 0x7FFF, 12); // emit at 180° as well // apply force - #ifdef USERMOD_AUDIOREACTIVE uint32_t strength = SEGMENT.speed; + #ifdef USERMOD_AUDIOREACTIVE um_data_t *um_data; - if(!SEGMENT.check3) { //AR enabled - if(usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { - uint8_t volumeSmth = (uint8_t)(*(float*) um_data->u_data[0]); - uint32_t strength = volumeSmth; - for (uint32_t i = 0; i < PartSys->usedParticles; i++) {// update particles - PartSys->pointAttractor(i, attractor, strength, false); - } - } - } - for(uint32_t i = 0; i < PartSys->usedParticles; i++) { - PartSys->pointAttractor(i, attractor, strength, false); + if(usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { + uint32_t volumeSmth = (uint32_t)(*(float*) um_data->u_data[0]); // 0-255 + strength = (SEGMENT.speed * volumeSmth) >> 8; } - #else // no AR + #endif for (uint32_t i = 0; i < PartSys->usedParticles; i++) { - PartSys->pointAttractor(i, *attractor, SEGMENT.speed, SEGMENT.check3); + PartSys->pointAttractor(i, *attractor, strength, SEGMENT.check3); } - #endif + if (SEGMENT.call % (33 - SEGMENT.custom3) == 0) PartSys->applyFriction(2); @@ -8591,12 +8583,7 @@ uint16_t mode_particleattractor(void) { PartSys->update(); // update and render return FRAMETIME; } -#ifdef USERMOD_AUDIOREACTIVE -static const char _data_FX_MODE_PARTICLEATTRACTOR[] PROGMEM = "PS Attractor@Mass,Particles,Size,Collide,Friction,AgeColor,Move,Disable AR;;!;2v;pal=9,sx=100,ix=82,c1=0,c2=0,"; -#else static const char _data_FX_MODE_PARTICLEATTRACTOR[] PROGMEM = "PS Attractor@Mass,Particles,Size,Collide,Friction,AgeColor,Move,Swallow;;!;2;pal=9,sx=100,ix=82,c1=0,c2=0"; -#endif - /* Particle Spray, just a particle spray with many parameters @@ -8605,7 +8592,6 @@ by DedeHai (Damian Schneider) */ uint16_t mode_particlespray(void) { ParticleSystem2D *PartSys = NULL; - //uint8_t numSprays; const uint8_t hardness = 200; // collision hardness is fixed if (SEGMENT.call == 0) { // initialization @@ -8614,6 +8600,7 @@ uint16_t mode_particlespray(void) { PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting) PartSys->setBounceY(true); PartSys->setMotionBlur(200); // anable motion blur + PartSys->setSmearBlur(10); // anable motion blur PartSys->sources[0].source.hue = hw_random16(); PartSys->sources[0].sourceFlags.collide = true; // seeded particles will collide (if enabled) PartSys->sources[0].var = 3; @@ -8660,8 +8647,8 @@ uint16_t mode_particlespray(void) { else { //no AR data, fall back to normal mode // change source properties if (SEGMENT.call % (11 - (SEGMENT.intensity / 25)) == 0) { // every nth frame, cycle color and emit particles - PartSys->sources[0].maxLife = 300; // lifetime in frames - PartSys->sources[0].minLife = 100; + PartSys->sources[0].maxLife = 300 + SEGMENT.intensity; // lifetime in frames + PartSys->sources[0].minLife = 150 + SEGMENT.intensity; PartSys->sources[0].source.hue++; // = hw_random16(); //change hue of spray source PartSys->angleEmit(PartSys->sources[0], angle, SEGMENT.speed >> 2); } @@ -9446,7 +9433,7 @@ uint16_t mode_particleFireworks1D(void) { rocketgravity = -rocketgravity; speed = -speed; } - PartSys->applyForce(PartSys->sources[0].source, PartSys->sources[0].sourceFlags, rocketgravity, forcecounter[0]); + PartSys->applyForce(PartSys->sources[0].source, rocketgravity, forcecounter[0]); PartSys->particleMoveUpdate(PartSys->sources[0].source, PartSys->sources[0].sourceFlags); PartSys->particleMoveUpdate(PartSys->sources[0].source, PartSys->sources[0].sourceFlags); // increase speed by calling the move function twice, also ages twice uint32_t rocketheight = SEGENV.aux0 ? PartSys->maxX - PartSys->sources[0].source.x : PartSys->sources[0].source.x; diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index f1066f2fd7..59d8b13221 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -335,7 +335,7 @@ class ParticleSystem1D int32_t sprayEmit(const PSsource1D &emitter); void particleMoveUpdate(PSparticle1D &part, PSparticleFlags1D &partFlags, PSsettings1D *options = NULL, PSadvancedParticle1D *advancedproperties = NULL); // move function //particle physics - void applyForce(PSparticle1D &part, PSparticleFlags1D &partFlags, const int8_t xforce, uint8_t &counter); //apply a force to a single particle + void applyForce(PSparticle1D &part, const int8_t xforce, uint8_t &counter); //apply a force to a single particle void applyForce(const int8_t xforce); // apply a force to all particles void applyGravity(PSparticle1D &part, PSparticleFlags1D &partFlags); // applies gravity to single particle (use this for sources) void applyFriction(const int32_t coefficient); // apply friction to all used particles From 078478066310a0f307b5c556d02e9ed6ae39a95b Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sun, 12 Jan 2025 13:43:38 +0100 Subject: [PATCH 168/219] increased min rockets --- wled00/FX.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 8b8d79228d..3cdaa156d9 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -7802,7 +7802,7 @@ uint16_t mode_particlefireworks(void) { return mode_static(); // something went wrong, no data! PartSys->updateSystem(); // update system properties (dimensions and data pointers) - numRockets = map(SEGMENT.speed, 0 , 255, 3, min(PartSys->numSources, (uint32_t)NUMBEROFSOURCES)); + numRockets = map(SEGMENT.speed, 0 , 255, 4, min(PartSys->numSources, (uint32_t)NUMBEROFSOURCES)); PartSys->setWrapX(SEGMENT.check1); PartSys->setBounceY(SEGMENT.check2); From 32919fe6ae57838a28d3da66f745461df1b72e17 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sun, 12 Jan 2025 17:23:22 +0100 Subject: [PATCH 169/219] fixed #ifdefs --- wled00/FX.cpp | 13 ++++++++----- wled00/FX_fcn.cpp | 2 ++ wled00/FXparticleSystem.cpp | 13 ++++++------- wled00/FXparticleSystem.h | 2 +- 4 files changed, 17 insertions(+), 13 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 3cdaa156d9..0b4999a368 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -16,8 +16,11 @@ #include "FX.h" #include "fcn_declare.h" -#if !defined(WLED_DISABLE_PARTICLESYSTEM2D) || !defined(WLED_DISABLE_PARTICLESYSTEM1D) +#if !(defined(WLED_DISABLE_PARTICLESYSTEM2D) && defined(WLED_DISABLE_PARTICLESYSTEM1D)) #include "FXparticleSystem.h" +#else +#undef DISABLE_1D_PS_REPLACEMENTS +#undef DISABLE_2D_PS_REPLACEMENTS #endif ////////////// @@ -2064,7 +2067,7 @@ uint16_t mode_palette() { } static const char _data_FX_MODE_PALETTE[] PROGMEM = "Palette@Shift,Size,Rotation,,,Animate Shift,Animate Rotation,Anamorphic;;!;12;ix=112,c1=0,o1=1,o2=0,o3=1"; -#ifndef DISABLE_2D_PS_REPLACEMENTS +#if !(defined(WLED_DISABLE_PARTICLESYSTEM2D) && defined(WLED_DISABLE_PARTICLESYSTEM1D)) // 1D or 2D PS enabled // WLED limitation: Analog Clock overlay will NOT work when Fire2012 is active // Fire2012 by Mark Kriegsman, July 2012 // as part of "Five Elements" shown here: http://youtu.be/knWiGsmgycY @@ -2151,7 +2154,7 @@ uint16_t mode_fire_2012() { return FRAMETIME; } static const char _data_FX_MODE_FIRE_2012[] PROGMEM = "Fire 2012@Cooling,Spark rate,,2D Blur,Boost;;!;1;pal=35,sx=64,ix=160,m12=1,c2=128"; // bars -#endif //DISABLE_2D_PS_REPLACEMENTS +#endif // !defined(DISABLE_2D_PS_REPLACEMENTS) && !defined(DISABLE_1D_PS_REPLACEMENTS) #ifndef DISABLE_1D_PS_REPLACEMENTS // ColorWavesWithPalettes by Mark Kriegsman: https://gist.github.com/kriegsman/8281905786e8b2632aeb @@ -10343,7 +10346,7 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_OSCILLATE, &mode_oscillate, _data_FX_MODE_OSCILLATE); addEffect(FX_MODE_JUGGLE, &mode_juggle, _data_FX_MODE_JUGGLE); addEffect(FX_MODE_PALETTE, &mode_palette, _data_FX_MODE_PALETTE); - #ifndef DISABLE_2D_PS_REPLACEMENTS + #if !defined(DISABLE_2D_PS_REPLACEMENTS) && !defined(DISABLE_1D_PS_REPLACEMENTS) addEffect(FX_MODE_FIRE_2012, &mode_fire_2012, _data_FX_MODE_FIRE_2012); #endif addEffect(FX_MODE_BPM, &mode_bpm, _data_FX_MODE_BPM); @@ -10442,7 +10445,7 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_2DCRAZYBEES, &mode_2Dcrazybees, _data_FX_MODE_2DCRAZYBEES); #ifndef DISABLE_2D_PS_REPLACEMENTS addEffect(FX_MODE_2DGHOSTRIDER, &mode_2Dghostrider, _data_FX_MODE_2DGHOSTRIDER); - //addEffect(FX_MODE_2DBLOBS, &mode_2Dfloatingblobs, _data_FX_MODE_2DBLOBS); + addEffect(FX_MODE_2DBLOBS, &mode_2Dfloatingblobs, _data_FX_MODE_2DBLOBS); #endif addEffect(FX_MODE_2DSCROLLTEXT, &mode_2Dscrollingtext, _data_FX_MODE_2DSCROLLTEXT); addEffect(FX_MODE_2DDRIFTROSE, &mode_2Ddriftrose, _data_FX_MODE_2DDRIFTROSE); diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index 27f854d88c..2352334c47 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -1407,7 +1407,9 @@ void WS2812FX::service() { } _segment_index++; } + #if !(defined(WLED_DISABLE_PARTICLESYSTEM2D) && defined(WLED_DISABLE_PARTICLESYSTEM1D)) servicePSmem(); // handle segment particle system memory + #endif _isServicing = false; _triggered = false; #ifdef WLED_DEBUG diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index fac9532af7..60d333c837 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -14,11 +14,8 @@ -add underscore to private variables */ -#if !defined(WLED_DISABLE_PARTICLESYSTEM2D) || !defined(WLED_DISABLE_PARTICLESYSTEM1D) +#if !(defined(WLED_DISABLE_PARTICLESYSTEM2D) && defined(WLED_DISABLE_PARTICLESYSTEM1D)) // not both disabled #include "FXparticleSystem.h" -#endif - -#ifndef WLED_DISABLE_PARTICLESYSTEM2D // local shared functions (used both in 1D and 2D system) static int32_t calcForce_dv(const int8_t force, uint8_t &counter); @@ -37,7 +34,9 @@ uint16_t renderBufferSize = 0; // size in pixels, if allcoated by a 1D system it bool renderSolo = false; // is set to true if this is the only particle system using the so it can use the buffer continuously (faster blurring) int32_t globalBlur = 0; // motion blur to apply if multiple PS are using the buffer int32_t globalSmear = 0; // smear-blur to apply if multiple PS are using the buffer +#endif +#ifndef WLED_DISABLE_PARTICLESYSTEM2D ParticleSystem2D::ParticleSystem2D(uint32_t width, uint32_t height, uint32_t numberofparticles, uint32_t numberofsources, bool isadvanced, bool sizecontrol) { PSPRINTLN("\n ParticleSystem2D constructor"); effectID = SEGMENT.mode; // new FX called init, save the effect ID @@ -1916,7 +1915,7 @@ void blur1D(CRGB *colorbuffer, uint32_t size, uint32_t blur, uint32_t start) } #endif // WLED_DISABLE_PARTICLESYSTEM1D -#if !defined(WLED_DISABLE_PARTICLESYSTEM2D) || !defined(WLED_DISABLE_PARTICLESYSTEM1D) +#if !(defined(WLED_DISABLE_PARTICLESYSTEM2D) && defined(WLED_DISABLE_PARTICLESYSTEM1D)) // not both disabled ////////////////////////////// // Shared Utility Functions // @@ -2215,7 +2214,7 @@ partMem* getPartMem(void) { // TODO: maybe there is a better/faster way than usi void updateRenderingBuffer(uint32_t requiredpixels, bool isFramebuffer, bool initialize) { PSPRINTLN("updateRenderingBuffer"); uint16_t& targetBufferSize = isFramebuffer ? frameBufferSize : renderBufferSize; // corresponding buffer size - + //if(isFramebuffer) return; // debug only: disable frame-buffer buffer if(targetBufferSize < requiredpixels) { // check current buffer size @@ -2316,4 +2315,4 @@ void transferBuffer(uint32_t width, uint32_t height, bool useAdditiveTransfer) { #endif } -#endif // !defined(WLED_DISABLE_PARTICLESYSTEM2D) || !defined(WLED_DISABLE_PARTICLESYSTEM1D) +#endif // !(defined(WLED_DISABLE_PARTICLESYSTEM2D) && defined(WLED_DISABLE_PARTICLESYSTEM1D)) diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index 59d8b13221..d9ba98770b 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -8,7 +8,7 @@ Licensed under the EUPL v. 1.2 or later */ -#if !defined(WLED_DISABLE_PARTICLESYSTEM2D) || !defined(WLED_DISABLE_PARTICLESYSTEM1D) +#if !(defined(WLED_DISABLE_PARTICLESYSTEM2D) && defined(WLED_DISABLE_PARTICLESYSTEM1D)) // not both disabled #include #include "wled.h" From dbf9d0bc4f7d1782bc6ccb8d201b26d5d84ced16 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sun, 12 Jan 2025 17:57:32 +0100 Subject: [PATCH 170/219] another fix --- wled00/FX.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index a79fa4150d..e17d929fab 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -2067,7 +2067,7 @@ uint16_t mode_palette() { } static const char _data_FX_MODE_PALETTE[] PROGMEM = "Palette@Shift,Size,Rotation,,,Animate Shift,Animate Rotation,Anamorphic;;!;12;ix=112,c1=0,o1=1,o2=0,o3=1"; -#if !(defined(WLED_DISABLE_PARTICLESYSTEM2D) && defined(WLED_DISABLE_PARTICLESYSTEM1D)) // 1D or 2D PS enabled +#if !(defined(DISABLE_2D_PS_REPLACEMENTS) || defined(DISABLE_1D_PS_REPLACEMENTS)) // disabled if 1D or 2D PS replacements are enabled // WLED limitation: Analog Clock overlay will NOT work when Fire2012 is active // Fire2012 by Mark Kriegsman, July 2012 // as part of "Five Elements" shown here: http://youtu.be/knWiGsmgycY @@ -2154,7 +2154,7 @@ uint16_t mode_fire_2012() { return FRAMETIME; } static const char _data_FX_MODE_FIRE_2012[] PROGMEM = "Fire 2012@Cooling,Spark rate,,2D Blur,Boost;;!;1;pal=35,sx=64,ix=160,m12=1,c2=128"; // bars -#endif // !defined(DISABLE_2D_PS_REPLACEMENTS) && !defined(DISABLE_1D_PS_REPLACEMENTS) +#endif // !defined(DISABLE_2D_PS_REPLACEMENTS) || !defined(DISABLE_1D_PS_REPLACEMENTS) #ifndef DISABLE_1D_PS_REPLACEMENTS // ColorWavesWithPalettes by Mark Kriegsman: https://gist.github.com/kriegsman/8281905786e8b2632aeb @@ -10346,7 +10346,7 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_OSCILLATE, &mode_oscillate, _data_FX_MODE_OSCILLATE); addEffect(FX_MODE_JUGGLE, &mode_juggle, _data_FX_MODE_JUGGLE); addEffect(FX_MODE_PALETTE, &mode_palette, _data_FX_MODE_PALETTE); - #if !defined(DISABLE_2D_PS_REPLACEMENTS) && !defined(DISABLE_1D_PS_REPLACEMENTS) + #if !(defined(DISABLE_2D_PS_REPLACEMENTS) || defined(DISABLE_1D_PS_REPLACEMENTS)) addEffect(FX_MODE_FIRE_2012, &mode_fire_2012, _data_FX_MODE_FIRE_2012); #endif addEffect(FX_MODE_BPM, &mode_bpm, _data_FX_MODE_BPM); From 1ece44cdd767d52e493feb4e264d392aad20d9c0 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sun, 12 Jan 2025 18:19:35 +0100 Subject: [PATCH 171/219] revert unnecessary changes to base code --- wled00/FX_fcn.cpp | 6 +++--- wled00/palettes.h | 2 -- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index 2352334c47..4236e8356e 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -149,11 +149,10 @@ Segment& Segment::operator= (Segment &&orig) noexcept { bool IRAM_ATTR_YN Segment::allocateData(size_t len) { if (len == 0) return false; // nothing to do if (data && _dataLen >= len) { // already allocated enough (reduce fragmentation) - if (call == 0) memset(data, 0, len); // erase buffer if called during effect initialisation + if (call == 0) memset(data, 0, len); // erase buffer if called during effect initialisation return true; } - DEBUG_PRINT(F("Allocating Data")); - // DEBUG_PRINTF_P(PSTR("-- Allocating data (%d): %p\n", len, this); + //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 @@ -1412,6 +1411,7 @@ void WS2812FX::service() { #endif _isServicing = false; _triggered = false; + #ifdef WLED_DEBUG if ((_targetFps != FPS_UNLIMITED) && (millis() - nowUp > _frametime)) DEBUG_PRINTF_P(PSTR("Slow effects %u/%d.\n"), (unsigned)(millis()-nowUp), (int)_frametime); #endif diff --git a/wled00/palettes.h b/wled00/palettes.h index f005f010f0..c84c1fb97a 100644 --- a/wled00/palettes.h +++ b/wled00/palettes.h @@ -14,8 +14,6 @@ #ifndef PalettesWLED_h #define PalettesWLED_h -#include "colorpalettes.h" - const byte ib_jul01_gp[] PROGMEM = { 0, 194, 1, 1, 94, 1, 29, 18, From 9d7647d7f5056e0295c2bb00c8aec05c0fc51491 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Mon, 13 Jan 2025 08:17:59 +0100 Subject: [PATCH 172/219] merge fix --- wled00/FX.cpp | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index e17d929fab..ba21f55a29 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -37,7 +37,7 @@ uint8_t *fftResult = nullptr; float *fftBin = nullptr; um_data_t *um_data; - if (usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { + if (UsermodManager::getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { volumeSmth = *(float*) um_data->u_data[0]; volumeRaw = *(float*) um_data->u_data[1]; fftResult = (uint8_t*) um_data->u_data[2]; @@ -8570,7 +8570,7 @@ uint16_t mode_particleattractor(void) { uint32_t strength = SEGMENT.speed; #ifdef USERMOD_AUDIOREACTIVE um_data_t *um_data; - if(usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { + if(UsermodManager::getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { uint32_t volumeSmth = (uint32_t)(*(float*) um_data->u_data[0]); // 0-255 strength = (SEGMENT.speed * volumeSmth) >> 8; } @@ -8634,7 +8634,7 @@ uint16_t mode_particlespray(void) { #ifdef USERMOD_AUDIOREACTIVE um_data_t *um_data; - if (usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { + if (UsermodManager::getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { uint32_t volumeSmth = (uint8_t)(*(float*) um_data->u_data[0]); //0 to 255 uint32_t volumeRaw = *(int16_t*)um_data->u_data[1]; //0 to 255 PartSys->sources[0].minLife = 30; @@ -8704,7 +8704,7 @@ uint16_t mode_particleGEQ(void) { PartSys->setGravity(SEGMENT.custom3 << 2); // set gravity strength um_data_t *um_data; - if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) + if (!UsermodManager::getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) um_data = simulateSound(SEGMENT.soundSim); // add support for no audio uint8_t *fftResult = (uint8_t *)um_data->u_data[2]; // 16 bins with FFT data, log mapped already, each band contains frequency amplitude 0-255 @@ -8786,7 +8786,7 @@ uint16_t mode_particlecenterGEQ(void) { numSprays = min(PartSys->numSources, (uint32_t)NUMBEROFSOURCES); um_data_t *um_data; - if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) + if (!UsermodManager::getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) um_data = simulateSound(SEGMENT.soundSim); // add support for no audio uint8_t *fftResult = (uint8_t *)um_data->u_data[2]; // 16 bins with FFT data, log mapped already, each band contains frequency amplitude 0-255 @@ -8966,7 +8966,7 @@ uint16_t mode_particleblobs(void) { #ifdef USERMOD_AUDIOREACTIVE um_data_t *um_data; - if (usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { + if (UsermodManager::getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { uint8_t volumeSmth = (uint8_t)(*(float*)um_data->u_data[0]); for (uint32_t i = 0; i < PartSys->usedParticles; i++) { // update particles if (SEGMENT.check3) //pulsate selected @@ -10007,7 +10007,7 @@ uint16_t mode_particle1DGEQ(void) { } um_data_t *um_data; - if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) + if (!UsermodManager::getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) um_data = simulateSound(SEGMENT.soundSim); // add support for no audio uint8_t *fftResult = (uint8_t *)um_data->u_data[2]; // 16 bins with FFT data, log mapped already, each band contains frequency amplitude 0-255 @@ -10141,7 +10141,7 @@ uint16_t mode_particle1Dsonicstream(void) { // FFT processing um_data_t *um_data; - if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) + if (!UsermodManager::getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) um_data = simulateSound(SEGMENT.soundSim); // add support for no audio uint8_t *fftResult = (uint8_t *)um_data->u_data[2]; // 16 bins with FFT data, log mapped already, each band contains frequency amplitude 0-255 From 46c884ca336e43b0db55f2b83827cd2c466dd610 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Tue, 14 Jan 2025 19:20:41 +0100 Subject: [PATCH 173/219] minor tweak --- wled00/FX.cpp | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index ba21f55a29..8e9e77664a 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -9724,7 +9724,7 @@ uint16_t mode_particle1Dspray(void) { return FRAMETIME; } -static const char _data_FX_MODE_PS_1DSPRAY[] PROGMEM = "PS 1D Spray@!,!,Position,Blur,Gravity,AgeColor,Bounce,Position Color;,!;!;1;pal=35,sx=200,ix=220,c1=4,c2=0,c3=28,o1=1,o2=1"; +static const char _data_FX_MODE_PS_1DSPRAY[] PROGMEM = "PS 1D Spray@!,!,Position,Blur,Gravity,AgeColor,Bounce,Position Color;,!;!;1;sx=200,ix=220,c1=4,c2=0,c3=28,o1=1,o2=1"; /* Particle based balance: particles move back and forth (1D pendent to 2D particle box) @@ -9760,9 +9760,8 @@ uint16_t mode_particleBalance(void) { if(PartSys->usedParticles > SEGENV.aux1) { // more particles, reinitialize for (i = 0; i < PartSys->usedParticles; i++) { PartSys->particles[i].x = i * PS_P_RADIUS_1D; - PartSys->particles[i].hue = (i * 1024) / PartSys->usedParticles; // multi gradient distribution PartSys->particles[i].ttl = 300; - PartSys->particleFlags[i].perpetual = true; // TODO: is this a good idea? need to check how to handle it in transitions + PartSys->particleFlags[i].perpetual = true; PartSys->particleFlags[i].collide = true; } } @@ -9792,7 +9791,7 @@ uint16_t mode_particleBalance(void) { PartSys->setColorByPosition(SEGMENT.check1); if(!SEGMENT.check1) { for(i = 0; i < PartSys->usedParticles; i++) { - PartSys->particles[i].hue = (255 * i) / PartSys->usedParticles; //color by particle index + PartSys->particles[i].hue = (1024 * i) / PartSys->usedParticles; // color by particle index } } PartSys->update(); // update and render From dab86219a8fe31ac3458fc6aa4b7c454ce73e852 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Tue, 14 Jan 2025 21:10:36 +0100 Subject: [PATCH 174/219] tweaked sparkler FX, some cleanup --- wled00/FX.cpp | 21 ++++++++++++--------- wled00/FXparticleSystem.cpp | 2 +- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 8e9e77664a..de5e0f51be 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -9411,7 +9411,8 @@ uint16_t mode_particleFireworks1D(void) { PartSys->sources[0].sourceFlags.custom1 = 0; //flag used for rocket state PartSys->sources[0].source.hue = hw_random16(); - PartSys->sources[0].var = 10; + PartSys->sources[0].var = 10; // emit variation + PartSys->sources[0].v = -10; // emit speed PartSys->sources[0].minLife = 100; PartSys->sources[0].maxLife = 300; PartSys->sources[0].source.x = 0; // start from bottom @@ -9426,6 +9427,7 @@ uint16_t mode_particleFireworks1D(void) { PartSys->sources[0].sourceFlags.reversegrav = true; PartSys->sources[0].source.x = PartSys->maxX; // start from top PartSys->sources[0].source.vx = -PartSys->sources[0].source.vx; // revert direction + PartSys->sources[0].v = -PartSys->sources[0].v; // invert exhaust emit speed } } } @@ -9475,7 +9477,7 @@ uint16_t mode_particleFireworks1D(void) { return FRAMETIME; } -static const char _data_FX_MODE_PS_FIREWORKS1D[] PROGMEM = "PS Fireworks 1D@Gravity,Explosion,Firing side,Blur,Saturation,Gravity,Colorful,Smooth;,!;!;1;pal=0,sx=150,ix=150,c1=220,c2=30,c3=21,o2=1"; +static const char _data_FX_MODE_PS_FIREWORKS1D[] PROGMEM = "PS Fireworks 1D@Gravity,Explosion,Firing side,Blur,Saturation,,Colorful,Smooth;,!;!;1;pal=0,sx=150,c2=30,c3=21,o2=1"; /* Particle based Sparkle effect @@ -9505,19 +9507,20 @@ uint16_t mode_particleSparkler(void) { numSparklers = PartSys->numSources; PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur - PartSys->setParticleSize(SEGMENT.check3); // 1 or 2 pixel rendering + //PartSys->setParticleSize(SEGMENT.check3); // 1 or 2 pixel rendering for(i = 0; i < numSparklers; i++) { - PartSys->sources[i].source.hue = hw_random16(); //TODO: make adjustable, maybe even colorcycle? + PartSys->sources[i].source.hue = hw_random16(); PartSys->sources[i].var = SEGMENT.intensity >> 4 ; PartSys->sources[i].minLife = 150 + (SEGMENT.intensity >> 1); PartSys->sources[i].maxLife = 200 + SEGMENT.intensity; uint32_t speed = SEGMENT.speed >> 1; - if(SEGMENT.check1) //invert spray speed + if(SEGMENT.check1) // invert spray speed speed = -speed; - PartSys->sources[i].source.vx = speed; //update speed, do not change direction - PartSys->sources[i].source.ttl = 400; //replenish its life (setting it perpetual uses more code) - PartSys->sources[i].sat = SEGMENT.custom1; //color saturation + PartSys->sources[i].source.vx = speed; // update speed, do not change direction + PartSys->sources[i].source.ttl = 400; // replenish its life (setting it perpetual uses more code) + PartSys->sources[i].sat = SEGMENT.custom1; // color saturation + PartSys->sources[i].size = SEGMENT.check3 ? 120 : 0; PartSys->particleMoveUpdate(PartSys->sources[i].source, PartSys->sources[i].sourceFlags, &sparklersettings); //move sparkler } @@ -9544,7 +9547,7 @@ uint16_t mode_particleSparkler(void) { return FRAMETIME; } -static const char _data_FX_MODE_PS_SPARKLER[] PROGMEM = "PS Sparkler@Speed,!,Saturation,Blur,Sparklers,Direction,Wrap/Bounce,Smooth;,!;!;1;pal=0,sx=50,ix=200,c1=0,c2=0,c3=0,o1=1,o2=1"; +static const char _data_FX_MODE_PS_SPARKLER[] PROGMEM = "PS Sparkler@Speed,!,Saturation,Blur,Sparklers,Direction,Wrap/Bounce,Large;,!;!;1;pal=0,sx=50,ix=200,c1=0,c2=0,c3=0,o1=1,o2=1"; /* Particle based Hourglass, particles falling at defined intervals diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index 60d333c837..c3d30bf266 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -1315,7 +1315,7 @@ void ParticleSystem1D::setSmearBlur(const uint8_t bluramount) { // render size, 0 = 1 pixel, 1 = 2 pixel (interpolated), bigger sizes require adanced properties void ParticleSystem1D::setParticleSize(const uint8_t size) { - particlesize = size > 0 ? 1 : 0; // TODO: add support for global sizes? see not abover (motion blur) + particlesize = size > 0 ? 1 : 0; // TODO: add support for global sizes? see note above (motion blur) if (particlesize) particleHardRadius = PS_P_MINHARDRADIUS_1D; // 2 pixel sized particles else From db4eae777a57e7b05d6831b773c418b4a395854d Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Wed, 15 Jan 2025 08:21:09 +0100 Subject: [PATCH 175/219] Fix in volcano FX, changed blurring of volcano and waterfall --- wled00/FX.cpp | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index de5e0f51be..3755834bec 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -7944,7 +7944,7 @@ uint16_t mode_particlevolcano(void) { PartSys->setBounceY(true); PartSys->setGravity(); // enable with default gforce PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting) - PartSys->setMotionBlur(190); // anable motion blur + PartSys->setMotionBlur(220); // anable motion blur numSprays = min(PartSys->numSources, (uint32_t)NUMBEROFSOURCES); // number of sprays for (i = 0; i < numSprays; i++) { @@ -7962,18 +7962,7 @@ uint16_t mode_particlevolcano(void) { if (PartSys == NULL) return mode_static(); // something went wrong, no data! - numSprays = min(PartSys->numSources, (uint32_t)NUMBEROFSOURCES); // number of sprays - - // Particle System settings - PartSys->updateSystem(); // update system properties (dimensions and data pointers) - PartSys->setColorByAge(SEGMENT.check1); - PartSys->setBounceX(SEGMENT.check2); - PartSys->setWallHardness(SEGMENT.custom2); - - if (SEGMENT.check3) // collisions enabled - PartSys->enableParticleCollisions(true, SEGMENT.custom2); // enable collisions and set particle collision hardness - else - PartSys->enableParticleCollisions(false); + numSprays = min(PartSys->numSources, (uint32_t)NUMBEROFSOURCES); // number of volcanoes // change source emitting color from time to time, emit one particle per spray if (SEGMENT.call % (11 - (SEGMENT.intensity / 25)) == 0) { // every nth frame, cycle color and emit particles (and update the sources) @@ -7981,15 +7970,27 @@ uint16_t mode_particlevolcano(void) { PartSys->sources[i].source.y = PS_P_RADIUS + 5; // reset to just above the lower edge that is allowed for bouncing particles, if zero, particles already 'bounce' at start and loose speed. PartSys->sources[i].source.vy = 0; //reset speed (so no extra particlesettin is required to keep the source 'afloat') PartSys->sources[i].source.hue++; // = hw_random16(); //change hue of spray source (note: random does not look good) - PartSys->sources[i].source.vx = PartSys->sources[i].source.vx > 0 ? SEGMENT.custom1 >> 2 : -(SEGMENT.custom1 >> 2); // set moving speed but keep the direction given by PS - PartSys->sources[i].vy = SEGMENT.speed >> 2; // emitting speed + PartSys->sources[i].source.vx = PartSys->sources[i].source.vx > 0 ? (SEGMENT.custom1 >> 2) : -(SEGMENT.custom1 >> 2); // set moving speed but keep the direction given by PS + PartSys->sources[i].vy = SEGMENT.speed >> 2; // emitting speed (upwards) PartSys->sources[i].vx = 0; PartSys->sources[i].var = SEGMENT.custom3 >> 1; // emiting variation = nozzle size (custom 3 goes from 0-31) PartSys->sprayEmit(PartSys->sources[i]); + PartSys->setWallHardness(255); // full hardness for source bounce PartSys->particleMoveUpdate(PartSys->sources[i].source, PartSys->sources[i].sourceFlags, &volcanosettings); //move the source } } + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setColorByAge(SEGMENT.check1); + PartSys->setBounceX(SEGMENT.check2); + PartSys->setWallHardness(SEGMENT.custom2); + + if (SEGMENT.check3) // collisions enabled + PartSys->enableParticleCollisions(true, SEGMENT.custom2); // enable collisions and set particle collision hardness + else + PartSys->enableParticleCollisions(false); + PartSys->update(); // update and render return FRAMETIME; } @@ -8175,7 +8176,7 @@ uint16_t mode_particlewaterfall(void) { PartSys->setGravity(); // enable with default gforce PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting) - PartSys->setMotionBlur(180); // anable motion blur + PartSys->setMotionBlur(190); // anable motion blur PartSys->setSmearBlur(30); // enable 2D blurring (smearing) for (i = 0; i < PartSys->numSources; i++) { PartSys->sources[i].source.hue = i*90; From 3aa8fa5c7e8d120fd5d6094e3d83738676f27379 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Wed, 15 Jan 2025 18:51:06 +0100 Subject: [PATCH 176/219] added ifdefs --- wled00/FX.cpp | 4 ++-- wled00/FXparticleSystem.cpp | 3 +++ wled00/FXparticleSystem.h | 4 ++++ 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 3755834bec..7a24037e7e 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -7944,7 +7944,7 @@ uint16_t mode_particlevolcano(void) { PartSys->setBounceY(true); PartSys->setGravity(); // enable with default gforce PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting) - PartSys->setMotionBlur(220); // anable motion blur + PartSys->setMotionBlur(230); // anable motion blur numSprays = min(PartSys->numSources, (uint32_t)NUMBEROFSOURCES); // number of sprays for (i = 0; i < numSprays; i++) { @@ -9826,7 +9826,7 @@ uint16_t mode_particleChase(void) { // Particle System settings PartSys->updateSystem(); // update system properties (dimensions and data pointers) PartSys->setColorByPosition(SEGMENT.check3); - PartSys->setMotionBlur(SEGMENT.custom3 << 3); // anable motion blur + PartSys->setMotionBlur((SEGMENT.custom3 + 1) << 3); // anable motion blur // uint8_t* basehue = (PartSys->PSdataEnd + 2); //assign data pointer uint32_t settingssum = SEGMENT.speed + SEGMENT.intensity + SEGMENT.custom1 + SEGMENT.custom2 + SEGMENT.check1 + SEGMENT.check2 + SEGMENT.check3 + PartSys->getAvailableParticles(); // note: getAvailableParticles is used to enforce update during transitions diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index c3d30bf266..aa08df6f13 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -13,6 +13,9 @@ - change passing particle pointers to references (if possible) -add underscore to private variables */ +#ifdef WLED_DISABLE_2D +#define WLED_DISABLE_PARTICLESYSTEM2D +#endif #if !(defined(WLED_DISABLE_PARTICLESYSTEM2D) && defined(WLED_DISABLE_PARTICLESYSTEM1D)) // not both disabled #include "FXparticleSystem.h" diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index d9ba98770b..a5793007ba 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -8,6 +8,10 @@ Licensed under the EUPL v. 1.2 or later */ +#ifdef WLED_DISABLE_2D +#define WLED_DISABLE_PARTICLESYSTEM2D +#endif + #if !(defined(WLED_DISABLE_PARTICLESYSTEM2D) && defined(WLED_DISABLE_PARTICLESYSTEM1D)) // not both disabled #include From 4294834db07b441a0fa5bfc95c357202abf84ca2 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Wed, 15 Jan 2025 20:26:05 +0100 Subject: [PATCH 177/219] minor tweaks, increased 1D minsurfacehardness --- wled00/FX.cpp | 19 ++++++++++--------- wled00/FXparticleSystem.h | 4 ++-- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index b30b15cd8a..424573f567 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -9488,7 +9488,6 @@ by DedeHai (Damian Schneider) uint16_t mode_particleSparkler(void) { ParticleSystem1D *PartSys = NULL; uint32_t numSparklers; - uint32_t i; PSsettings1D sparklersettings; sparklersettings.asByte = 0; // PS settings for sparkler (set below) @@ -9510,7 +9509,7 @@ uint16_t mode_particleSparkler(void) { PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur //PartSys->setParticleSize(SEGMENT.check3); // 1 or 2 pixel rendering - for(i = 0; i < numSparklers; i++) { + for (uint32_t i = 0; i < numSparklers; i++) { PartSys->sources[i].source.hue = hw_random16(); PartSys->sources[i].var = SEGMENT.intensity >> 4 ; PartSys->sources[i].minLife = 150 + (SEGMENT.intensity >> 1); @@ -9522,25 +9521,27 @@ uint16_t mode_particleSparkler(void) { PartSys->sources[i].source.ttl = 400; // replenish its life (setting it perpetual uses more code) PartSys->sources[i].sat = SEGMENT.custom1; // color saturation PartSys->sources[i].size = SEGMENT.check3 ? 120 : 0; - PartSys->particleMoveUpdate(PartSys->sources[i].source, PartSys->sources[i].sourceFlags, &sparklersettings); //move sparkler + for(uint32_t j = 0; j < 2; j++) { // move twice for higher speeds + PartSys->particleMoveUpdate(PartSys->sources[i].source, PartSys->sources[i].sourceFlags, &sparklersettings); //move sparkler 1243972- + } } - for(i = 0; i < PartSys->usedParticles; i++) { - if(PartSys->particles[i].ttl > 10) PartSys->particles[i].ttl -= 10; //ttl is linked to brightness, this allows to use higher brightness but still a short spark lifespan + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { + if (PartSys->particles[i].ttl > 10) PartSys->particles[i].ttl -= 10; //ttl is linked to brightness, this allows to use higher brightness but still a short spark lifespan else PartSys->particles[i].ttl = 0; } numSparklers = min(1 + (SEGMENT.custom3 >> 2), (int)numSparklers); // set used sparklers, 1 to 8 - if(SEGENV.aux0 != SEGMENT.custom3) { //number of used sparklers changed, redistribute - for(i = 1; i < numSparklers; i++) { + if (SEGENV.aux0 != SEGMENT.custom3) { //number of used sparklers changed, redistribute + for (uint32_t i = 1; i < numSparklers; i++) { PartSys->sources[i].source.x = (PartSys->sources[0].source.x + (PartSys->maxX / numSparklers) * i ) % PartSys->maxX; //distribute evenly } } SEGENV.aux0 = SEGMENT.custom3; - for(i = 0; i < numSparklers; i++) { - if(hw_random() % (1 + ((255 - SEGMENT.intensity) >> 3)) == 0) + for (uint32_t i = 0; i < numSparklers; i++) { + if (hw_random() % (1 + ((255 - SEGMENT.intensity) >> 3)) == 0) PartSys->sprayEmit(PartSys->sources[i]); //emit a particle } diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index a5793007ba..65a18813aa 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -50,7 +50,7 @@ void transferBuffer(uint32_t width, uint32_t height, bool useAdditiveTransfer = void servicePSmem(); // increments watchdog, frees memory if idle too long // limit speed of particles (used in 1D and 2D) -static inline int32_t limitSpeed(int32_t speed) { +static inline int32_t limitSpeed(const int32_t speed) { return speed > PS_P_MAXSPEED ? PS_P_MAXSPEED : (speed < -PS_P_MAXSPEED ? -PS_P_MAXSPEED : speed); // note: this is slightly faster than using min/max at the cost of 50bytes of flash } #endif @@ -267,7 +267,7 @@ bool allocateParticleSystemMemory2D(const uint32_t numparticles, const uint32_t #define PS_P_RADIUS_SHIFT_1D 5 // 1 << PS_P_RADIUS_SHIFT = PS_P_RADIUS #define PS_P_SURFACE_1D 5 // shift: 2^PS_P_SURFACE = PS_P_RADIUS_1D #define PS_P_MINHARDRADIUS_1D 32 // minimum hard surface radius note: do not change or hourglass effect will be broken -#define PS_P_MINSURFACEHARDNESS_1D 60 // minimum hardness used in collision impulse calculation +#define PS_P_MINSURFACEHARDNESS_1D 120 // minimum hardness used in collision impulse calculation // struct for PS settings (shared for 1D and 2D class) typedef union { From fc8960a4725176a52b9de30f715db4e79907003b Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Thu, 16 Jan 2025 11:28:08 +0100 Subject: [PATCH 178/219] improved sparkler FX, made overlay possible (1D not yet), cleanup --- wled00/FX.cpp | 39 +++++++++++++++++++------------------ wled00/FXparticleSystem.cpp | 15 +++++++------- 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 424573f567..635264be02 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -9502,36 +9502,32 @@ uint16_t mode_particleSparkler(void) { // Particle System settings PartSys->updateSystem(); // update system properties (dimensions and data pointers) - sparklersettings.wrap = SEGMENT.check2; - sparklersettings.bounce = !SEGMENT.check2; + sparklersettings.wrap = !SEGMENT.check2; + sparklersettings.bounce = SEGMENT.check2; // note: bounce always takes priority over wrap numSparklers = PartSys->numSources; - PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur - //PartSys->setParticleSize(SEGMENT.check3); // 1 or 2 pixel rendering + PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur/overlay + //PartSys->setSmearBlur(SEGMENT.custom2); // anable smearing blur for (uint32_t i = 0; i < numSparklers; i++) { PartSys->sources[i].source.hue = hw_random16(); - PartSys->sources[i].var = SEGMENT.intensity >> 4 ; - PartSys->sources[i].minLife = 150 + (SEGMENT.intensity >> 1); - PartSys->sources[i].maxLife = 200 + SEGMENT.intensity; + PartSys->sources[i].var = 0; // sparks stationary + PartSys->sources[i].minLife = 150 + SEGMENT.intensity; + PartSys->sources[i].maxLife = 250 + (SEGMENT.intensity << 2); uint32_t speed = SEGMENT.speed >> 1; - if(SEGMENT.check1) // invert spray speed - speed = -speed; + if(SEGMENT.check1) // sparks move (slide option) + PartSys->sources[i].var = SEGMENT.intensity >> 3; PartSys->sources[i].source.vx = speed; // update speed, do not change direction PartSys->sources[i].source.ttl = 400; // replenish its life (setting it perpetual uses more code) PartSys->sources[i].sat = SEGMENT.custom1; // color saturation PartSys->sources[i].size = SEGMENT.check3 ? 120 : 0; - for(uint32_t j = 0; j < 2; j++) { // move twice for higher speeds - PartSys->particleMoveUpdate(PartSys->sources[i].source, PartSys->sources[i].sourceFlags, &sparklersettings); //move sparkler 1243972- - } - } - - for (uint32_t i = 0; i < PartSys->usedParticles; i++) { - if (PartSys->particles[i].ttl > 10) PartSys->particles[i].ttl -= 10; //ttl is linked to brightness, this allows to use higher brightness but still a short spark lifespan - else PartSys->particles[i].ttl = 0; + if(SEGMENT.speed == 255) // random position at highest speed setting + PartSys->sources[i].source.x = hw_random16(PartSys->maxX); + else + PartSys->particleMoveUpdate(PartSys->sources[i].source, PartSys->sources[i].sourceFlags, &sparklersettings); //move sparkler } - numSparklers = min(1 + (SEGMENT.custom3 >> 2), (int)numSparklers); // set used sparklers, 1 to 8 + numSparklers = min(1 + (SEGMENT.custom3 >> 1), (int)numSparklers); // set used sparklers, 1 to 16 if (SEGENV.aux0 != SEGMENT.custom3) { //number of used sparklers changed, redistribute for (uint32_t i = 1; i < numSparklers; i++) { @@ -9547,9 +9543,14 @@ uint16_t mode_particleSparkler(void) { PartSys->update(); // update and render + for(uint32_t i = 0; i < PartSys->usedParticles; i++) { + if(PartSys->particles[i].ttl > (64 - (SEGMENT.intensity >> 2))) PartSys->particles[i].ttl -= (64 - (SEGMENT.intensity >> 2)); //ttl is linked to brightness, this allows to use higher brightness but still a short spark lifespan + else PartSys->particles[i].ttl = 0; + } + return FRAMETIME; } -static const char _data_FX_MODE_PS_SPARKLER[] PROGMEM = "PS Sparkler@Speed,!,Saturation,Blur,Sparklers,Direction,Wrap/Bounce,Large;,!;!;1;pal=0,sx=50,ix=200,c1=0,c2=0,c3=0,o1=1,o2=1"; +static const char _data_FX_MODE_PS_SPARKLER[] PROGMEM = "PS Sparkler@Speed,!,Saturation,Blur,Sparklers,Slide,Bounce,Large;,!;!;1;pal=0,sx=255,ix=200,c1=0,c2=0,c3=6"; /* Particle based Hourglass, particles falling at defined intervals diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index aa08df6f13..ffc641a175 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -582,7 +582,9 @@ void ParticleSystem2D::ParticleSys_render() { // handle blurring and framebuffer update if (framebuffer) { - if(!pmem->inTransition) useAdditiveTransfer = false; // additive rendering is only used in PS FX transitions + //if(!pmem->inTransition) useAdditiveTransfer = false; // additive rendering is only used in PS FX transitions + if(strip.getCurrSegmentId() > 0) useAdditiveTransfer = true; //!!! test for overlay rendering + else useAdditiveTransfer = false; // handle buffer blurring or clearing bool bufferNeedsUpdate = (!pmem->inTransition || pmem->inTransition == effectID); // not a transition; or new FX: update buffer (blur, or clear) @@ -592,8 +594,7 @@ void ParticleSystem2D::ParticleSys_render() { int index = y * (maxXpixel + 1); for (int32_t x = 0; x <= maxXpixel; x++) { if (!renderSolo) { // sharing the framebuffer with another segment: read buffer back from segment - uint32_t yflipped = maxYpixel - y; - framebuffer[index] = SEGMENT.getPixelColorXY(x, yflipped); // read from segment + framebuffer[index] = SEGMENT.getPixelColorXY(x, y); // read from segment } fast_color_scale(framebuffer[index], globalBlur); // note: could skip if only globalsmear is active but usually they are both active and scaling is fast enough index++; @@ -639,8 +640,6 @@ void ParticleSystem2D::ParticleSys_render() { SEGMENT.fill(BLACK); //clear the buffer before rendering next frame } - bool wrapX = particlesettings.wrapX; // use local variables for faster access - bool wrapY = particlesettings.wrapY; // go over particles and render them to the buffer for (uint32_t i = 0; i < usedParticles; i++) { if (particles[i].ttl == 0 || particleFlags[i].outofbounds) @@ -660,7 +659,7 @@ void ParticleSystem2D::ParticleSys_render() { baseRGB = (CRGB)baseHSV; // convert back to RGB } } - renderParticle(i, brightness, baseRGB, wrapX, wrapY); + renderParticle(i, brightness, baseRGB, particlesettings.wrapX, particlesettings.wrapY); } if (particlesize > 0) { @@ -1530,7 +1529,7 @@ void ParticleSystem1D::ParticleSys_render() { else SEGMENT.fill(BLACK); // clear the buffer before rendering to it } - bool wrap = particlesettings.wrap; // local copy for speed + // go over particles and render them to the buffer for (uint32_t i = 0; i < usedParticles; i++) { if ( particles[i].ttl == 0 || particleFlags[i].outofbounds) @@ -1547,7 +1546,7 @@ void ParticleSystem1D::ParticleSys_render() { baseRGB = (CRGB)baseHSV; // convert back to RGB } } - renderParticle(i, brightness, baseRGB, wrap); + renderParticle(i, brightness, baseRGB, particlesettings.wrap); } // apply smear-blur to rendered frame if(globalSmear > 0) { From e7b893d0d876934fc48612317f2a9e95cfca8e08 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Thu, 16 Jan 2025 20:49:13 +0100 Subject: [PATCH 179/219] cleanup, now using new hsv2rgb/rgb2hsv, add overlay rendering to 1D - new hsv2rgb is a tiny bit faster - removed redundant code (using transferBuffer instead) - tweked parameters in vortex, removed unneeded slider defaults --- wled00/FX.cpp | 2 +- wled00/FXparticleSystem.cpp | 71 ++++++++++++++++++++----------------- 2 files changed, 39 insertions(+), 34 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 056761fbd4..965bcaf819 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -7783,7 +7783,7 @@ uint16_t mode_particlevortex(void) { return FRAMETIME; } #undef NUMBEROFSOURCES -static const char _data_FX_MODE_PARTICLEVORTEX[] PROGMEM = "PS Vortex@Rotation Speed,Particle Speed,Arms,Auto Flip,Nozzle,Smear,Direction,Random Flip;;!;2;pal=56,sx=200,ix=190,c1=200,c2=0,c3=0"; +static const char _data_FX_MODE_PARTICLEVORTEX[] PROGMEM = "PS Vortex@Rotation Speed,Particle Speed,Arms,Flip,Nozzle,Smear,Direction,Random Flip;;!;2;pal=27,c1=200,c2=0,c3=0"; /* * Particle Fireworks diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index ffc641a175..98cb4e44a0 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -582,8 +582,7 @@ void ParticleSystem2D::ParticleSys_render() { // handle blurring and framebuffer update if (framebuffer) { - //if(!pmem->inTransition) useAdditiveTransfer = false; // additive rendering is only used in PS FX transitions - if(strip.getCurrSegmentId() > 0) useAdditiveTransfer = true; //!!! test for overlay rendering + if(strip.getCurrSegmentId() > 0) useAdditiveTransfer = true; // overlay rendering else useAdditiveTransfer = false; // handle buffer blurring or clearing bool bufferNeedsUpdate = (!pmem->inTransition || pmem->inTransition == effectID); // not a transition; or new FX: update buffer (blur, or clear) @@ -593,7 +592,7 @@ void ParticleSystem2D::ParticleSys_render() { for (int32_t y = 0; y <= maxYpixel; y++) { int index = y * (maxXpixel + 1); for (int32_t x = 0; x <= maxXpixel; x++) { - if (!renderSolo) { // sharing the framebuffer with another segment: read buffer back from segment + if (!renderSolo) { // sharing the framebuffer with another segment: update buffer by reading back from segment framebuffer[index] = SEGMENT.getPixelColorXY(x, y); // read from segment } fast_color_scale(framebuffer[index], globalBlur); // note: could skip if only globalsmear is active but usually they are both active and scaling is fast enough @@ -609,25 +608,8 @@ void ParticleSystem2D::ParticleSys_render() { if(bufferNeedsUpdate && !globalBlur) { // transfer only if buffer was not cleared above (happens if this is the new FX and other FX does not use blurring) useAdditiveTransfer = false; // no blurring and big size particle FX is the new FX (rendered first after clearing), can just render normally } - else { // this is the old FX (rendering second) new FX already rendered to the buffer, transfer it to segment and clear it - #ifndef WLED_DISABLE_MODE_BLEND - bool tempBlend = SEGMENT.getmodeBlend(); - SEGMENT.modeBlend(false); // temporarily disable FX blending in PS to PS transition (local buffer is used to do PS blending) - #endif - int yflipped; - for (int32_t y = 0; y <= maxYpixel; y++) { - yflipped = maxYpixel - y; - int index = y * (maxXpixel + 1); // current row index for 1D buffer - for (int32_t x = 0; x <= maxXpixel; x++) { - CRGB *c = &framebuffer[index++]; - uint32_t clr = RGBW32(c->r,c->g,c->b,0); // convert to 32bit color - //if(clr > 0) // not black TODO: not transferring black is faster and enables overlay, but requries proper handling of buffer clearing, which is quite complex and probably needs a change to SEGMENT handling. - SEGMENT.setPixelColorXY((int)x, (int)yflipped, clr); - } - } - #ifndef WLED_DISABLE_MODE_BLEND - SEGMENT.modeBlend(tempBlend); - #endif + else { // this is the old FX (rendering second) or blurring is active: new FX already rendered to the buffer and blurring was applied above; transfer it to segment and clear it + transferBuffer(maxXpixel + 1, maxYpixel + 1, useAdditiveTransfer); memset(framebuffer, 0, frameBufferSize * sizeof(CRGB)); // clear the buffer after transfer useAdditiveTransfer = true; // additive transfer reads from segment, adds that to the frame-buffer and writes back to segment, after transfer, segment and buffer are identical } @@ -654,9 +636,12 @@ void ParticleSystem2D::ParticleSys_render() { brightness = min((particles[i].ttl << 1), (int)255); baseRGB = ColorFromPalette(SEGPALETTE, particles[i].hue, 255); // TODO: use loadPalette(CRGBPalette16 &targetPalette, SEGMENT.palette), .palette should be updated immediately at palette change, only use local palette during FX transitions, not during normal transitions. -> why not always? if (particles[i].sat < 255) { - CHSV baseHSV = rgb2hsv_approximate(baseRGB); //convert to HSV //!!! TODO: use new hsv to rgb function. - baseHSV.s = particles[i].sat; //set the saturation - baseRGB = (CRGB)baseHSV; // convert back to RGB + CHSV32 baseHSV; + rgb2hsv((uint32_t((byte(baseRGB.r) << 16) | (byte(baseRGB.g) << 8) | (byte(baseRGB.b)))), baseHSV); // convert to HSV + baseHSV.s = particles[i].sat; // set the saturation + uint32_t tempcolor; + hsv2rgb(baseHSV, tempcolor); // convert back to RGB + baseRGB = (CRGB)tempcolor; } } renderParticle(i, brightness, baseRGB, particlesettings.wrapX, particlesettings.wrapY); @@ -686,7 +671,7 @@ void ParticleSystem2D::ParticleSys_render() { SEGMENT.blur(globalSmear, true); } // transfer framebuffer to segment if available - if (pmem->inTransition != effectID) { // not in transition or is old FX + if (pmem->inTransition != effectID) { // not in transition or is old FX (rendered second) transferBuffer(maxXpixel + 1, maxYpixel + 1, useAdditiveTransfer); } } @@ -1496,6 +1481,7 @@ void ParticleSystem1D::applyFriction(int32_t coefficient) { void ParticleSystem1D::ParticleSys_render() { CRGB baseRGB; uint32_t brightness; // particle brightness, fades if dying + static bool useAdditiveTransfer; // use add instead of set for buffer transferring // update global blur (used for blur transitions) int32_t motionbluramount = motionBlur; @@ -1508,6 +1494,8 @@ void ParticleSystem1D::ParticleSys_render() { globalSmear = smearamount; if (framebuffer) { + if(strip.getCurrSegmentId() > 0) useAdditiveTransfer = true; // overlay rendering + else useAdditiveTransfer = false; // handle buffer blurring or clearing bool bufferNeedsUpdate = (!pmem->inTransition || pmem->inTransition == effectID); // not a transition; or new FX: update buffer (blur, or clear) if(bufferNeedsUpdate) { @@ -1541,9 +1529,12 @@ void ParticleSystem1D::ParticleSys_render() { if (advPartProps) { //saturation is advanced property in 1D system if (advPartProps[i].sat < 255) { - CHSV baseHSV = rgb2hsv_approximate(baseRGB); //convert to HSV //!!!TODO: replace with new rgb2hsv - baseHSV.s = advPartProps[i].sat; //set the saturation - baseRGB = (CRGB)baseHSV; // convert back to RGB + CHSV32 baseHSV; + rgb2hsv((uint32_t((byte(baseRGB.r) << 16) | (byte(baseRGB.g) << 8) | (byte(baseRGB.b)))), baseHSV); // convert to HSV + baseHSV.s = advPartProps[i].sat; // set the saturation + uint32_t tempcolor; + hsv2rgb(baseHSV, tempcolor); // convert back to RGB + baseRGB = (CRGB)tempcolor; } } renderParticle(i, brightness, baseRGB, particlesettings.wrap); @@ -1556,7 +1547,7 @@ void ParticleSystem1D::ParticleSys_render() { SEGMENT.blur(globalSmear, true); } // transfer local buffer back to segment (if available) - transferBuffer(maxXpixel + 1, 0); + transferBuffer(maxXpixel + 1, 0, useAdditiveTransfer); } @@ -2291,7 +2282,7 @@ void transferBuffer(uint32_t width, uint32_t height, bool useAdditiveTransfer) { CRGB segmentRGB = CRGB(segmentcolor); if(clr == 0) // frame buffer is black, just update the framebuffer *c = segmentRGB; - else { // not black + else { // color to add to segment is not black if(segmentcolor) { fast_color_add(*c, segmentRGB); // add segment color back to buffer if not black clr = RGBW32(c->r,c->g,c->b,0); // convert to 32bit color (again) TODO: could convert first, then use 32bit adding function color_add() from colors.cpp @@ -2307,9 +2298,23 @@ void transferBuffer(uint32_t width, uint32_t height, bool useAdditiveTransfer) { } else { // 1D system for (uint32_t x = 0; x < width; x++) { CRGB *c = &framebuffer[x]; - uint32_t color = RGBW32(c->r,c->g,c->b,0); + uint32_t clr = RGBW32(c->r,c->g,c->b,0); + if(useAdditiveTransfer) { + uint32_t segmentcolor = SEGMENT.getPixelColor((int)x);; + CRGB segmentRGB = CRGB(segmentcolor); + if(clr == 0) // frame buffer is black, just load the color (for next frame) + *c = segmentRGB; + else { // color to add to segment is not black + if(segmentcolor) { + fast_color_add(*c, segmentRGB); // add segment color back to buffer if not black + clr = RGBW32(c->r,c->g,c->b,0); // convert to 32bit color (again) TODO: could convert first, then use 32bit adding function color_add() from colors.cpp + } + SEGMENT.setPixelColor((int)x, clr); // save back to segment after adding local buffer + } + } //if(color > 0) // not black - SEGMENT.setPixelColor((int)x, color); + else + SEGMENT.setPixelColor((int)x, clr); } } #ifndef WLED_DISABLE_MODE_BLEND From 6198abe21b936d31d7faca7e87cee9a039e1a7c1 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Fri, 17 Jan 2025 06:56:22 +0100 Subject: [PATCH 180/219] fix for non or partially overlapping segments - can not use overlay rendering if no segment clears the buffer, it will all add up to white eventually - now additive transfer i.e. overlay mode is only activated if the segment is fully overlapping an underlying segment --- wled00/FXparticleSystem.cpp | 16 ++++++++++++++-- wled00/FXparticleSystem.h | 1 + 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index 98cb4e44a0..43acfbd236 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -582,7 +582,7 @@ void ParticleSystem2D::ParticleSys_render() { // handle blurring and framebuffer update if (framebuffer) { - if(strip.getCurrSegmentId() > 0) useAdditiveTransfer = true; // overlay rendering + if(segmentIsOverlay()) useAdditiveTransfer = true; // overlay rendering else useAdditiveTransfer = false; // handle buffer blurring or clearing bool bufferNeedsUpdate = (!pmem->inTransition || pmem->inTransition == effectID); // not a transition; or new FX: update buffer (blur, or clear) @@ -1494,7 +1494,7 @@ void ParticleSystem1D::ParticleSys_render() { globalSmear = smearamount; if (framebuffer) { - if(strip.getCurrSegmentId() > 0) useAdditiveTransfer = true; // overlay rendering + if(segmentIsOverlay()) useAdditiveTransfer = true; // overlay rendering else useAdditiveTransfer = false; // handle buffer blurring or clearing bool bufferNeedsUpdate = (!pmem->inTransition || pmem->inTransition == effectID); // not a transition; or new FX: update buffer (blur, or clear) @@ -2192,6 +2192,18 @@ void updateUsedParticles(const uint32_t allocated, const uint32_t available, con used = max((uint32_t)2, min(available, wantsToUse)); // limit to available particles, use a minimum of 2 } +// check if a segment is fully overlapping with an underlying segment (used to enable overlay rendering i.e. adding instead of overwriting pixels) +bool segmentIsOverlay(void) { + unsigned segID = strip.getCurrSegmentId(); + if(segID > 0) { // lower number segments exist, check coordinates of underlying segments + for (unsigned i = 0; i < segID; i++) { + if(strip._segments[i].start <= strip._segments[segID].start && strip._segments[i].stop >= strip._segments[segID].stop && + strip._segments[i].startY <= strip._segments[segID].startY && strip._segments[i].stopY >= strip._segments[segID].stopY) + return true; + } + } + return false; +} // get the pointer to the particle memory for the segment partMem* getPartMem(void) { // TODO: maybe there is a better/faster way than using vectors? uint8_t segID = strip.getCurrSegmentId(); diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index 65a18813aa..21856b174b 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -44,6 +44,7 @@ struct partMem { void* particleMemoryManager(const uint32_t requestedParticles, size_t structSize, uint32_t &availableToPS, uint32_t numParticlesUsed, const uint8_t effectID); // update particle memory pointer, handles memory transitions void particleHandover(void *buffer, size_t structSize, int32_t numParticles); void updateUsedParticles(const uint32_t allocated, const uint32_t available, const uint8_t percentage, uint32_t &used); +bool segmentIsOverlapping(void); // check if segment is fully overlapping with at least one underlying segment partMem* getPartMem(void); // returns pointer to memory struct for current segment or nullptr void updateRenderingBuffer(uint32_t requiredpixels, bool isFramebuffer, bool initialize); // allocate CRGB rendering buffer, update size if needed void transferBuffer(uint32_t width, uint32_t height, bool useAdditiveTransfer = false); // transfer the buffer to the segment (supports 1D and 2D) From 7c5637321edd395825bd213cef53d468fb2817bb Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Fri, 17 Jan 2025 07:48:15 +0100 Subject: [PATCH 181/219] change got lost... --- wled00/FXparticleSystem.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index 21856b174b..d94e36621a 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -44,7 +44,7 @@ struct partMem { void* particleMemoryManager(const uint32_t requestedParticles, size_t structSize, uint32_t &availableToPS, uint32_t numParticlesUsed, const uint8_t effectID); // update particle memory pointer, handles memory transitions void particleHandover(void *buffer, size_t structSize, int32_t numParticles); void updateUsedParticles(const uint32_t allocated, const uint32_t available, const uint8_t percentage, uint32_t &used); -bool segmentIsOverlapping(void); // check if segment is fully overlapping with at least one underlying segment +bool segmentIsOverlay(void); // check if segment is fully overlapping with at least one underlying segment partMem* getPartMem(void); // returns pointer to memory struct for current segment or nullptr void updateRenderingBuffer(uint32_t requiredpixels, bool isFramebuffer, bool initialize); // allocate CRGB rendering buffer, update size if needed void transferBuffer(uint32_t width, uint32_t height, bool useAdditiveTransfer = false); // transfer the buffer to the segment (supports 1D and 2D) From 82269abc10b4df72f35215506554fdbaa98a30fd Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Fri, 17 Jan 2025 08:19:31 +0100 Subject: [PATCH 182/219] increased sparkler intensity, some cleanup --- wled00/FX.cpp | 6 +++--- wled00/FXparticleSystem.cpp | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 965bcaf819..a24189226e 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -9523,7 +9523,7 @@ uint16_t mode_particleSparkler(void) { PartSys->sources[i].source.hue = hw_random16(); PartSys->sources[i].var = 0; // sparks stationary PartSys->sources[i].minLife = 150 + SEGMENT.intensity; - PartSys->sources[i].maxLife = 250 + (SEGMENT.intensity << 2); + PartSys->sources[i].maxLife = 250 + (SEGMENT.intensity << 1); uint32_t speed = SEGMENT.speed >> 1; if(SEGMENT.check1) // sparks move (slide option) PartSys->sources[i].var = SEGMENT.intensity >> 3; @@ -9547,7 +9547,7 @@ uint16_t mode_particleSparkler(void) { SEGENV.aux0 = SEGMENT.custom3; for (uint32_t i = 0; i < numSparklers; i++) { - if (hw_random() % (1 + ((255 - SEGMENT.intensity) >> 3)) == 0) + if (hw_random() % (((271 - SEGMENT.intensity) >> 4)) == 0) PartSys->sprayEmit(PartSys->sources[i]); //emit a particle } @@ -9560,7 +9560,7 @@ uint16_t mode_particleSparkler(void) { return FRAMETIME; } -static const char _data_FX_MODE_PS_SPARKLER[] PROGMEM = "PS Sparkler@Speed,!,Saturation,Blur,Sparklers,Slide,Bounce,Large;,!;!;1;pal=0,sx=255,ix=200,c1=0,c2=0,c3=6"; +static const char _data_FX_MODE_PS_SPARKLER[] PROGMEM = "PS Sparkler@Move,!,Saturation,Blur,Sparklers,Slide,Bounce,Large;,!;!;1;pal=0,sx=255,c1=0,c2=0,c3=6"; /* Particle based Hourglass, particles falling at defined intervals diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index 43acfbd236..70e5544cfc 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -2193,13 +2193,13 @@ void updateUsedParticles(const uint32_t allocated, const uint32_t available, con } // check if a segment is fully overlapping with an underlying segment (used to enable overlay rendering i.e. adding instead of overwriting pixels) -bool segmentIsOverlay(void) { +bool segmentIsOverlay(void) { // TODO: this only needs to be checked when segment is created, could move this to segment class or PS init unsigned segID = strip.getCurrSegmentId(); if(segID > 0) { // lower number segments exist, check coordinates of underlying segments for (unsigned i = 0; i < segID; i++) { if(strip._segments[i].start <= strip._segments[segID].start && strip._segments[i].stop >= strip._segments[segID].stop && strip._segments[i].startY <= strip._segments[segID].startY && strip._segments[i].stopY >= strip._segments[segID].stopY) - return true; + return true; } } return false; From 2034ea3529480cf7e81e8a198c0c7f01757485db Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Fri, 17 Jan 2025 18:27:31 +0100 Subject: [PATCH 183/219] replaced #ifdefs, removed 1D replacements that have 2D version, removed notes --- wled00/FX.cpp | 120 +++++++++++------------------------- wled00/FXparticleSystem.cpp | 5 -- wled00/FXparticleSystem.h | 16 ++--- 3 files changed, 44 insertions(+), 97 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index a24189226e..29db412cf6 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -19,8 +19,8 @@ #if !(defined(WLED_DISABLE_PARTICLESYSTEM2D) && defined(WLED_DISABLE_PARTICLESYSTEM1D)) #include "FXparticleSystem.h" #else -#undef DISABLE_1D_PS_REPLACEMENTS -#undef DISABLE_2D_PS_REPLACEMENTS +#define ENABLE_1D_PS_REPLACEMENTS +#define ENABLE_2D_PS_REPLACEMENTS #endif ////////////// @@ -1223,7 +1223,6 @@ uint16_t mode_dual_larson_scanner(void){ } static const char _data_FX_MODE_DUAL_LARSON_SCANNER[] PROGMEM = "Scanner Dual@!,Trail,Delay,,,Dual,Bi-delay;!,!,!;!;;m12=0,c1=0"; -#ifndef DISABLE_1D_PS_REPLACEMENTS /* * Firing comets from one end. "Lighthouse" */ @@ -1250,7 +1249,6 @@ uint16_t mode_comet(void) { return FRAMETIME; } static const char _data_FX_MODE_COMET[] PROGMEM = "Lighthouse@!,Fade rate;!,!;!"; -#endif // DISABLE_1D_PS_REPLACEMENTS /* * Fireworks function. @@ -1295,7 +1293,6 @@ uint16_t mode_fireworks() { } static const char _data_FX_MODE_FIREWORKS[] PROGMEM = "Fireworks@,Frequency;!,!;!;12;ix=192,pal=11"; -#ifndef DISABLE_1D_PS_REPLACEMENTS //Twinkling LEDs running. Inspired by https://github.com/kitesurfer1404/WS2812FX/blob/master/src/custom/Rain.h uint16_t mode_rain() { if (SEGLEN == 1) return mode_static(); @@ -1329,7 +1326,6 @@ uint16_t mode_rain() { return mode_fireworks(); } static const char _data_FX_MODE_RAIN[] PROGMEM = "Rain@!,Spawning rate;!,!;!;12;ix=128,pal=0"; -#endif //DISABLE_1D_PS_REPLACEMENTS /* * Fire flicker function @@ -1901,7 +1897,6 @@ uint16_t mode_lightning(void) { } static const char _data_FX_MODE_LIGHTNING[] PROGMEM = "Lightning@!,!,,,,,Overlay;!,!;!"; -#ifndef DISABLE_1D_PS_REPLACEMENTS // Pride2015 // Animated, ever-changing rainbows. // by Mark Kriegsman: https://gist.github.com/kriegsman/964de772d64c502760e5 @@ -1942,7 +1937,6 @@ uint16_t mode_pride_2015(void) { return FRAMETIME; } static const char _data_FX_MODE_PRIDE_2015[] PROGMEM = "Pride 2015@!;;"; -#endif // DISABLE_1D_PS_REPLACEMENTS //eight colored dots, weaving in and out of sync with each other uint16_t mode_juggle(void) { @@ -2061,7 +2055,7 @@ uint16_t mode_palette() { } static const char _data_FX_MODE_PALETTE[] PROGMEM = "Palette@Shift,Size,Rotation,,,Animate Shift,Animate Rotation,Anamorphic;;!;12;ix=112,c1=0,o1=1,o2=0,o3=1"; -#if !(defined(DISABLE_2D_PS_REPLACEMENTS) || defined(DISABLE_1D_PS_REPLACEMENTS)) // disabled if 1D or 2D PS replacements are enabled +#if (defined(ENABLE_2D_PS_REPLACEMENTS) || defined(ENABLE_1D_PS_REPLACEMENTS)) // WLED limitation: Analog Clock overlay will NOT work when Fire2012 is active // Fire2012 by Mark Kriegsman, July 2012 // as part of "Five Elements" shown here: http://youtu.be/knWiGsmgycY @@ -2148,9 +2142,8 @@ uint16_t mode_fire_2012() { return FRAMETIME; } static const char _data_FX_MODE_FIRE_2012[] PROGMEM = "Fire 2012@Cooling,Spark rate,,2D Blur,Boost;;!;1;pal=35,sx=64,ix=160,m12=1,c2=128"; // bars -#endif // !defined(DISABLE_2D_PS_REPLACEMENTS) || !defined(DISABLE_1D_PS_REPLACEMENTS) +#endif // ENABLE_2D_PS_REPLACEMENTS || ENABLE_1D_PS_REPLACEMENTS -#ifndef DISABLE_1D_PS_REPLACEMENTS // ColorWavesWithPalettes by Mark Kriegsman: https://gist.github.com/kriegsman/8281905786e8b2632aeb // This function draws color waves with an ever-changing, // widely-varying set of parameters, using a color palette. @@ -2195,7 +2188,6 @@ uint16_t mode_colorwaves() { return FRAMETIME; } static const char _data_FX_MODE_COLORWAVES[] PROGMEM = "Colorwaves@!,Hue;!;!;;pal=26"; -#endif // DISABLE_1D_PS_REPLACEMENTS // colored stripes pulsing at a defined Beats-Per-Minute (BPM) uint16_t mode_bpm() { @@ -2959,7 +2951,6 @@ uint16_t mode_spots_fade() } static const char _data_FX_MODE_SPOTS_FADE[] PROGMEM = "Spots Fade@Spread,Width,,,,,Overlay;!,!;!"; -#ifndef DISABLE_1D_PS_REPLACEMENTS //each needs 12 bytes typedef struct Ball { unsigned long lastBounceTime; @@ -3044,13 +3035,13 @@ uint16_t mode_bouncing_balls(void) { } static const char _data_FX_MODE_BOUNCINGBALLS[] PROGMEM = "Bouncing Balls@Gravity,# of balls,,,,,Overlay;!,!,!;!;1;m12=1"; //bar - +#ifdef ENABLE_1D_PS_REPLACEMENTS /* * bouncing balls on a track track Effect modified from Aircoookie's bouncing balls * Courtesy of pjhatch (https://github.com/pjhatch) * https://github.com/Aircoookie/WLED/pull/1039 */ - + // modified for balltrack mode typedef struct RollingBall { unsigned long lastBounceUpdate; @@ -3145,7 +3136,7 @@ static uint16_t rolling_balls(void) { return FRAMETIME; } static const char _data_FX_MODE_ROLLINGBALLS[] PROGMEM = "Rolling Balls@!,# of balls,,,,Collide,Overlay,Trails;!,!,!;!;1;m12=1"; //bar -#endif //DISABLE_1D_PS_REPLACEMENTS +#endif // ENABLE_1D_PS_REPLACEMENTS /* * Sinelon stolen from FASTLED examples @@ -3240,7 +3231,6 @@ uint16_t mode_solid_glitter() } static const char _data_FX_MODE_SOLID_GLITTER[] PROGMEM = "Solid Glitter@,!;Bg,,Glitter color;;;m12=0"; -#ifndef DISABLE_1D_PS_REPLACEMENTS //each needs 20 bytes //Spark type is used for popcorn, 1D fireworks, and drip typedef struct Spark { @@ -3317,8 +3307,6 @@ uint16_t mode_popcorn(void) { } static const char _data_FX_MODE_POPCORN[] PROGMEM = "Popcorn@!,!,,,,,Overlay;!,!,!;!;;m12=1"; //bar -#endif // DISABLE_1D_PS_REPLACEMENTS - //values close to 100 produce 5Hz flicker, which looks very candle-y //Inspired by https://github.com/avanhanegem/ArduinoCandleEffectNeoPixel //and https://cpldcpu.wordpress.com/2016/01/05/reverse-engineering-a-real-candle/ @@ -3411,7 +3399,7 @@ uint16_t mode_candle_multi() } static const char _data_FX_MODE_CANDLE_MULTI[] PROGMEM = "Candle Multi@!,!;!,!;!;;sx=96,ix=224,pal=0"; -#ifndef DISABLE_1D_PS_REPLACEMENTS +#ifdef ENABLE_1D_PS_REPLACEMENTS /* / Fireworks in starburst effect / based on the video: https://www.reddit.com/r/arduino/comments/c3sd46/i_made_this_fireworks_effect_for_my_led_strips/ @@ -3543,7 +3531,9 @@ uint16_t mode_starburst(void) { } #undef STARBURST_MAX_FRAG static const char _data_FX_MODE_STARBURST[] PROGMEM = "Fireworks Starburst@Chance,Fragments,,,,,Overlay;,!;!;;pal=11,m12=0"; +#endif // ENABLE_1D_PS_REPLACEMENTS + #if (defined(ENABLE_1D_PS_REPLACEMENTS) || defined(ENABLE_2D_PS_REPLACEMENTS)) /* * Exploding fireworks effect * adapted from: http://www.anirama.com/1000leds/1d-fireworks/ @@ -3681,7 +3671,7 @@ uint16_t mode_exploding_fireworks(void) } #undef MAX_SPARKS static const char _data_FX_MODE_EXPLODING_FIREWORKS[] PROGMEM = "Fireworks 1D@Gravity,Firing side;!,!;!;12;pal=11,ix=128"; - +#endif // ENABLE_1D_PS_REPLACEMENTS || ENABLE_2D_PS_REPLACEMENTS /* * Drip Effect @@ -3768,7 +3758,7 @@ uint16_t mode_drip(void) return FRAMETIME; } static const char _data_FX_MODE_DRIP[] PROGMEM = "Drip@Gravity,# of drips,,,,,Overlay;!,!;!;;m12=1"; //bar -#endif // DISABLE_1D_PS_REPLACEMENTS + /* * Tetris or Stacking (falling bricks) Effect * by Blaz Kristan (AKA blazoncek) (https://github.com/blazoncek, https://blaz.at/home) @@ -4329,7 +4319,7 @@ static const char _data_FX_MODE_CHUNCHUN[] PROGMEM = "Chunchun@!,Gap size;!,!;!" #define SPOT_MAX_COUNT 49 //Number of simultaneous waves #endif -#ifndef DISABLE_1D_PS_REPLACEMENTS +#ifdef ENABLE_1D_PS_REPLACEMENTS //13 bytes typedef struct Spotlight { float speed; @@ -4464,7 +4454,7 @@ uint16_t mode_dancing_shadows(void) return FRAMETIME; } static const char _data_FX_MODE_DANCING_SHADOWS[] PROGMEM = "Dancing Shadows@!,# of shadows;!;!"; -#endif //DISABLE_1D_PS_REPLACEMENTS +#endif // ENABLE_1D_PS_REPLACEMENTS /* Imitates a washing machine, rotating same waves forward, then pause, then backward. @@ -5893,7 +5883,7 @@ uint16_t mode_2Dcrazybees(void) { static const char _data_FX_MODE_2DCRAZYBEES[] PROGMEM = "Crazy Bees@!,Blur,,,,Smear;;!;2;pal=11,ix=0"; #undef MAX_BEES -#ifndef DISABLE_2D_PS_REPLACEMENTS +#ifdef ENABLE_2D_PS_REPLACEMENTS ///////////////////////// // 2D Ghost Rider // ///////////////////////// @@ -5983,9 +5973,8 @@ uint16_t mode_2Dghostrider(void) { } static const char _data_FX_MODE_2DGHOSTRIDER[] PROGMEM = "Ghost Rider@Fade rate,Blur;;!;2"; #undef LIGHTERS_AM -#endif //DISABLE_2D_PS_REPLACEMENTS -#ifndef DISABLE_2D_PS_REPLACEMENTS + //////////////////////////// // 2D Floating Blobs // //////////////////////////// @@ -6085,7 +6074,7 @@ uint16_t mode_2Dfloatingblobs(void) { } static const char _data_FX_MODE_2DBLOBS[] PROGMEM = "Blobs@!,# blobs,Blur,Trail;!;!;2;c1=8"; #undef MAX_BLOBS -#endif //DISABLE_2D_PS_REPLACEMENTS +#endif // ENABLE_2D_PS_REPLACEMENTS //////////////////////////// // 2D Scrolling text // @@ -10223,31 +10212,13 @@ uint16_t mode_particle1Dsonicstream(void) { } else { PartSys->sources[0].sourceFlags.perpetual = false; // emitted particles age - //move all particles (again) to allow faster speeds + // move all particles (again) to allow faster speeds for (uint32_t i = 0; i < PartSys->usedParticles; i++) { if (PartSys->particles[i].vx == 0) PartSys->particles[i].vx = PartSys->sources[0].v; // move static particles (after disabling push mode) PartSys->particleMoveUpdate(PartSys->particles[i], PartSys->particleFlags[i], nullptr, &PartSys->advPartProps[i]); } } - /* - Serial.print(fftResult[0]); Serial.print(" "); - Serial.print(fftResult[1]); Serial.print(" "); - Serial.print(fftResult[2]); Serial.print(" "); - Serial.print(fftResult[3]); Serial.print(" "); - Serial.print(fftResult[4]); Serial.print(" "); - Serial.print(fftResult[5]); Serial.print(" "); - Serial.print(fftResult[6]); Serial.print(" "); - Serial.print(fftResult[7]); Serial.print(" "); - Serial.print(fftResult[8]); Serial.print(" "); - Serial.print(fftResult[9]); Serial.print(" "); - Serial.print(fftResult[10]); Serial.print(" "); - Serial.print(fftResult[11]); Serial.print(" "); - Serial.print(fftResult[12]); Serial.print(" "); - Serial.print(fftResult[13]); Serial.print(" "); - Serial.print(fftResult[14]); Serial.print(" "); - Serial.print(fftResult[15]); Serial.println(" "); -*/ return FRAMETIME; } @@ -10311,7 +10282,6 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_TWINKLE, &mode_twinkle, _data_FX_MODE_TWINKLE); addEffect(FX_MODE_DISSOLVE, &mode_dissolve, _data_FX_MODE_DISSOLVE); addEffect(FX_MODE_DISSOLVE_RANDOM, &mode_dissolve_random, _data_FX_MODE_DISSOLVE_RANDOM); - addEffect(FX_MODE_SPARKLE, &mode_sparkle, _data_FX_MODE_SPARKLE); addEffect(FX_MODE_FLASH_SPARKLE, &mode_flash_sparkle, _data_FX_MODE_FLASH_SPARKLE); addEffect(FX_MODE_HYPER_SPARKLE, &mode_hyper_sparkle, _data_FX_MODE_HYPER_SPARKLE); addEffect(FX_MODE_STROBE, &mode_strobe, _data_FX_MODE_STROBE); @@ -10332,24 +10302,19 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_AURORA, &mode_aurora, _data_FX_MODE_AURORA); addEffect(FX_MODE_RUNNING_RANDOM, &mode_running_random, _data_FX_MODE_RUNNING_RANDOM); addEffect(FX_MODE_LARSON_SCANNER, &mode_larson_scanner, _data_FX_MODE_LARSON_SCANNER); - #ifndef DISABLE_1D_PS_REPLACEMENTS - addEffect(FX_MODE_COMET, &mode_comet, _data_FX_MODE_COMET); addEffect(FX_MODE_RAIN, &mode_rain, _data_FX_MODE_RAIN); - addEffect(FX_MODE_ROLLINGBALLS, &rolling_balls, _data_FX_MODE_ROLLINGBALLS); + addEffect(FX_MODE_COMET, &mode_comet, _data_FX_MODE_COMET); addEffect(FX_MODE_PRIDE_2015, &mode_pride_2015, _data_FX_MODE_PRIDE_2015); addEffect(FX_MODE_COLORWAVES, &mode_colorwaves, _data_FX_MODE_COLORWAVES); - #endif addEffect(FX_MODE_FIREWORKS, &mode_fireworks, _data_FX_MODE_FIREWORKS); addEffect(FX_MODE_TETRIX, &mode_tetrix, _data_FX_MODE_TETRIX); addEffect(FX_MODE_FIRE_FLICKER, &mode_fire_flicker, _data_FX_MODE_FIRE_FLICKER); addEffect(FX_MODE_GRADIENT, &mode_gradient, _data_FX_MODE_GRADIENT); - addEffect(FX_MODE_LOADING, &mode_loading, _data_FX_MODE_LOADING); - + addEffect(FX_MODE_LOADING, &mode_loading, _data_FX_MODE_LOADING); addEffect(FX_MODE_FAIRY, &mode_fairy, _data_FX_MODE_FAIRY); addEffect(FX_MODE_TWO_DOTS, &mode_two_dots, _data_FX_MODE_TWO_DOTS); addEffect(FX_MODE_FAIRYTWINKLE, &mode_fairytwinkle, _data_FX_MODE_FAIRYTWINKLE); addEffect(FX_MODE_RUNNING_DUAL, &mode_running_dual, _data_FX_MODE_RUNNING_DUAL); - addEffect(FX_MODE_TRICOLOR_CHASE, &mode_tricolor_chase, _data_FX_MODE_TRICOLOR_CHASE); addEffect(FX_MODE_TRICOLOR_WIPE, &mode_tricolor_wipe, _data_FX_MODE_TRICOLOR_WIPE); addEffect(FX_MODE_TRICOLOR_FADE, &mode_tricolor_fade, _data_FX_MODE_TRICOLOR_FADE); @@ -10361,9 +10326,6 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_OSCILLATE, &mode_oscillate, _data_FX_MODE_OSCILLATE); addEffect(FX_MODE_JUGGLE, &mode_juggle, _data_FX_MODE_JUGGLE); addEffect(FX_MODE_PALETTE, &mode_palette, _data_FX_MODE_PALETTE); - #if !(defined(DISABLE_2D_PS_REPLACEMENTS) || defined(DISABLE_1D_PS_REPLACEMENTS)) - addEffect(FX_MODE_FIRE_2012, &mode_fire_2012, _data_FX_MODE_FIRE_2012); - #endif addEffect(FX_MODE_BPM, &mode_bpm, _data_FX_MODE_BPM); addEffect(FX_MODE_FILLNOISE8, &mode_fillnoise8, _data_FX_MODE_FILLNOISE8); addEffect(FX_MODE_NOISE16_1, &mode_noise16_1, _data_FX_MODE_NOISE16_1); @@ -10383,26 +10345,31 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_TRI_STATIC_PATTERN, &mode_tri_static_pattern, _data_FX_MODE_TRI_STATIC_PATTERN); addEffect(FX_MODE_SPOTS, &mode_spots, _data_FX_MODE_SPOTS); addEffect(FX_MODE_SPOTS_FADE, &mode_spots_fade, _data_FX_MODE_SPOTS_FADE); + #ifdef ENABLE_1D_PS_REPLACEMENTS + addEffect(FX_MODE_ROLLINGBALLS, &rolling_balls, _data_FX_MODE_ROLLINGBALLS); + addEffect(FX_MODE_SPARKLE, &mode_sparkle, _data_FX_MODE_SPARKLE); addEffect(FX_MODE_GLITTER, &mode_glitter, _data_FX_MODE_GLITTER); - addEffect(FX_MODE_CANDLE, &mode_candle, _data_FX_MODE_CANDLE); - #ifndef DISABLE_1D_PS_REPLACEMENTS + addEffect(FX_MODE_SOLID_GLITTER, &mode_solid_glitter, _data_FX_MODE_SOLID_GLITTER); addEffect(FX_MODE_STARBURST, &mode_starburst, _data_FX_MODE_STARBURST); + addEffect(FX_MODE_DANCING_SHADOWS, &mode_dancing_shadows, _data_FX_MODE_DANCING_SHADOWS); + #endif + #if (defined(ENABLE_1D_PS_REPLACEMENTS) || defined(ENABLE_2D_PS_REPLACEMENTS)) + addEffect(FX_MODE_FIRE_2012, &mode_fire_2012, _data_FX_MODE_FIRE_2012); + addEffect(FX_MODE_EXPLODING_FIREWORKS, &mode_exploding_fireworks, _data_FX_MODE_EXPLODING_FIREWORKS); + #endif + addEffect(FX_MODE_CANDLE, &mode_candle, _data_FX_MODE_CANDLE); addEffect(FX_MODE_BOUNCINGBALLS, &mode_bouncing_balls, _data_FX_MODE_BOUNCINGBALLS); addEffect(FX_MODE_POPCORN, &mode_popcorn, _data_FX_MODE_POPCORN); addEffect(FX_MODE_DRIP, &mode_drip, _data_FX_MODE_DRIP); - addEffect(FX_MODE_EXPLODING_FIREWORKS, &mode_exploding_fireworks, _data_FX_MODE_EXPLODING_FIREWORKS); - addEffect(FX_MODE_DANCING_SHADOWS, &mode_dancing_shadows, _data_FX_MODE_DANCING_SHADOWS); - #endif addEffect(FX_MODE_SINELON, &mode_sinelon, _data_FX_MODE_SINELON); addEffect(FX_MODE_SINELON_DUAL, &mode_sinelon_dual, _data_FX_MODE_SINELON_DUAL); - addEffect(FX_MODE_SINELON_RAINBOW, &mode_sinelon_rainbow, _data_FX_MODE_SINELON_RAINBOW); + addEffect(FX_MODE_SINELON_RAINBOW, &mode_sinelon_rainbow, _data_FX_MODE_SINELON_RAINBOW); addEffect(FX_MODE_PLASMA, &mode_plasma, _data_FX_MODE_PLASMA); addEffect(FX_MODE_PERCENT, &mode_percent, _data_FX_MODE_PERCENT); addEffect(FX_MODE_RIPPLE_RAINBOW, &mode_ripple_rainbow, _data_FX_MODE_RIPPLE_RAINBOW); addEffect(FX_MODE_HEARTBEAT, &mode_heartbeat, _data_FX_MODE_HEARTBEAT); addEffect(FX_MODE_PACIFICA, &mode_pacifica, _data_FX_MODE_PACIFICA); addEffect(FX_MODE_CANDLE_MULTI, &mode_candle_multi, _data_FX_MODE_CANDLE_MULTI); - addEffect(FX_MODE_SOLID_GLITTER, &mode_solid_glitter, _data_FX_MODE_SOLID_GLITTER); addEffect(FX_MODE_SUNRISE, &mode_sunrise, _data_FX_MODE_SUNRISE); addEffect(FX_MODE_PHASED, &mode_phased, _data_FX_MODE_PHASED); addEffect(FX_MODE_TWINKLEUP, &mode_twinkleup, _data_FX_MODE_TWINKLEUP); @@ -10412,7 +10379,6 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_FLOW, &mode_flow, _data_FX_MODE_FLOW); addEffect(FX_MODE_CHUNCHUN, &mode_chunchun, _data_FX_MODE_CHUNCHUN); addEffect(FX_MODE_WASHING_MACHINE, &mode_washing_machine, _data_FX_MODE_WASHING_MACHINE); - addEffect(FX_MODE_BLENDS, &mode_blends, _data_FX_MODE_BLENDS); addEffect(FX_MODE_TV_SIMULATOR, &mode_tv_simulator, _data_FX_MODE_TV_SIMULATOR); addEffect(FX_MODE_DYNAMIC_SMOOTH, &mode_dynamic_smooth, _data_FX_MODE_DYNAMIC_SMOOTH); @@ -10429,27 +10395,20 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_NOISEMETER, &mode_noisemeter, _data_FX_MODE_NOISEMETER); addEffect(FX_MODE_FREQWAVE, &mode_freqwave, _data_FX_MODE_FREQWAVE); addEffect(FX_MODE_FREQMATRIX, &mode_freqmatrix, _data_FX_MODE_FREQMATRIX); - addEffect(FX_MODE_WATERFALL, &mode_waterfall, _data_FX_MODE_WATERFALL); addEffect(FX_MODE_FREQPIXELS, &mode_freqpixels, _data_FX_MODE_FREQPIXELS); - addEffect(FX_MODE_NOISEFIRE, &mode_noisefire, _data_FX_MODE_NOISEFIRE); addEffect(FX_MODE_PUDDLEPEAK, &mode_puddlepeak, _data_FX_MODE_PUDDLEPEAK); addEffect(FX_MODE_NOISEMOVE, &mode_noisemove, _data_FX_MODE_NOISEMOVE); - addEffect(FX_MODE_PERLINMOVE, &mode_perlinmove, _data_FX_MODE_PERLINMOVE); addEffect(FX_MODE_RIPPLEPEAK, &mode_ripplepeak, _data_FX_MODE_RIPPLEPEAK); - addEffect(FX_MODE_FREQMAP, &mode_freqmap, _data_FX_MODE_FREQMAP); addEffect(FX_MODE_GRAVCENTER, &mode_gravcenter, _data_FX_MODE_GRAVCENTER); addEffect(FX_MODE_GRAVCENTRIC, &mode_gravcentric, _data_FX_MODE_GRAVCENTRIC); addEffect(FX_MODE_GRAVFREQ, &mode_gravfreq, _data_FX_MODE_GRAVFREQ); addEffect(FX_MODE_DJLIGHT, &mode_DJLight, _data_FX_MODE_DJLIGHT); - addEffect(FX_MODE_BLURZ, &mode_blurz, _data_FX_MODE_BLURZ); - addEffect(FX_MODE_FLOWSTRIPE, &mode_FlowStripe, _data_FX_MODE_FLOWSTRIPE); - addEffect(FX_MODE_WAVESINS, &mode_wavesins, _data_FX_MODE_WAVESINS); addEffect(FX_MODE_ROCKTAVES, &mode_rocktaves, _data_FX_MODE_ROCKTAVES); @@ -10458,18 +10417,17 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_2DPLASMAROTOZOOM, &mode_2Dplasmarotozoom, _data_FX_MODE_2DPLASMAROTOZOOM); addEffect(FX_MODE_2DSPACESHIPS, &mode_2Dspaceships, _data_FX_MODE_2DSPACESHIPS); addEffect(FX_MODE_2DCRAZYBEES, &mode_2Dcrazybees, _data_FX_MODE_2DCRAZYBEES); - #ifndef DISABLE_2D_PS_REPLACEMENTS + + #ifdef ENABLE_2D_PS_REPLACEMENTS addEffect(FX_MODE_2DGHOSTRIDER, &mode_2Dghostrider, _data_FX_MODE_2DGHOSTRIDER); addEffect(FX_MODE_2DBLOBS, &mode_2Dfloatingblobs, _data_FX_MODE_2DBLOBS); #endif + addEffect(FX_MODE_2DSCROLLTEXT, &mode_2Dscrollingtext, _data_FX_MODE_2DSCROLLTEXT); addEffect(FX_MODE_2DDRIFTROSE, &mode_2Ddriftrose, _data_FX_MODE_2DDRIFTROSE); addEffect(FX_MODE_2DDISTORTIONWAVES, &mode_2Ddistortionwaves, _data_FX_MODE_2DDISTORTIONWAVES); - addEffect(FX_MODE_2DGEQ, &mode_2DGEQ, _data_FX_MODE_2DGEQ); // audio - addEffect(FX_MODE_2DNOISE, &mode_2Dnoise, _data_FX_MODE_2DNOISE); - addEffect(FX_MODE_2DFIRENOISE, &mode_2Dfirenoise, _data_FX_MODE_2DFIRENOISE); addEffect(FX_MODE_2DSQUAREDSWIRL, &mode_2Dsquaredswirl, _data_FX_MODE_2DSQUAREDSWIRL); @@ -10478,15 +10436,12 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_2DMATRIX, &mode_2Dmatrix, _data_FX_MODE_2DMATRIX); addEffect(FX_MODE_2DMETABALLS, &mode_2Dmetaballs, _data_FX_MODE_2DMETABALLS); addEffect(FX_MODE_2DFUNKYPLANK, &mode_2DFunkyPlank, _data_FX_MODE_2DFUNKYPLANK); // audio - addEffect(FX_MODE_2DPULSER, &mode_2DPulser, _data_FX_MODE_2DPULSER); - addEffect(FX_MODE_2DDRIFT, &mode_2DDrift, _data_FX_MODE_2DDRIFT); addEffect(FX_MODE_2DWAVERLY, &mode_2DWaverly, _data_FX_MODE_2DWAVERLY); // audio addEffect(FX_MODE_2DSUNRADIATION, &mode_2DSunradiation, _data_FX_MODE_2DSUNRADIATION); addEffect(FX_MODE_2DCOLOREDBURSTS, &mode_2DColoredBursts, _data_FX_MODE_2DCOLOREDBURSTS); addEffect(FX_MODE_2DJULIA, &mode_2DJulia, _data_FX_MODE_2DJULIA); - addEffect(FX_MODE_2DGAMEOFLIFE, &mode_2Dgameoflife, _data_FX_MODE_2DGAMEOFLIFE); addEffect(FX_MODE_2DTARTAN, &mode_2Dtartan, _data_FX_MODE_2DTARTAN); addEffect(FX_MODE_2DPOLARLIGHTS, &mode_2DPolarLights, _data_FX_MODE_2DPOLARLIGHTS); @@ -10494,7 +10449,6 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_2DLISSAJOUS, &mode_2DLissajous, _data_FX_MODE_2DLISSAJOUS); addEffect(FX_MODE_2DFRIZZLES, &mode_2DFrizzles, _data_FX_MODE_2DFRIZZLES); addEffect(FX_MODE_2DPLASMABALL, &mode_2DPlasmaball, _data_FX_MODE_2DPLASMABALL); - addEffect(FX_MODE_2DHIPHOTIC, &mode_2DHiphotic, _data_FX_MODE_2DHIPHOTIC); addEffect(FX_MODE_2DSINDOTS, &mode_2DSindots, _data_FX_MODE_2DSINDOTS); addEffect(FX_MODE_2DDNASPIRAL, &mode_2DDNASpiral, _data_FX_MODE_2DDNASPIRAL); @@ -10502,7 +10456,6 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_2DSOAP, &mode_2Dsoap, _data_FX_MODE_2DSOAP); addEffect(FX_MODE_2DOCTOPUS, &mode_2Doctopus, _data_FX_MODE_2DOCTOPUS); addEffect(FX_MODE_2DWAVINGCELL, &mode_2Dwavingcell, _data_FX_MODE_2DWAVINGCELL); - addEffect(FX_MODE_2DAKEMI, &mode_2DAkemi, _data_FX_MODE_2DAKEMI); // audio #ifndef WLED_DISABLE_PARTICLESYSTEM2D @@ -10539,7 +10492,6 @@ addEffect(FX_MODE_PSSTARBURST, &mode_particleStarburst, _data_FX_MODE_PS_STARBUR addEffect(FX_MODE_PS1DGEQ, &mode_particle1DGEQ, _data_FX_MODE_PS_1D_GEQ); addEffect(FX_MODE_PSFIRE1D, &mode_particleFire1D, _data_FX_MODE_PS_FIRE1D); addEffect(FX_MODE_PS1DSONICSTREAM, &mode_particle1Dsonicstream, _data_FX_MODE_PS_SONICSTREAM); - - #endif // WLED_DISABLE_PARTICLESYSTEM1D + } diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index 70e5544cfc..77aaba2628 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -8,11 +8,6 @@ Licensed under the EUPL v. 1.2 or later */ -/* - TODO: - - change passing particle pointers to references (if possible) - -add underscore to private variables -*/ #ifdef WLED_DISABLE_2D #define WLED_DISABLE_PARTICLESYSTEM2D #endif diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index d94e36621a..61c85d2a5f 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -165,8 +165,8 @@ class ParticleSystem2D { int32_t angleEmit(PSsource& emitter, const uint16_t angle, const int32_t speed); //particle physics void applyGravity(PSparticle &part); // applies gravity to single particle (use this for sources) - void applyForce(PSparticle &part, const int8_t xforce, const int8_t yforce, uint8_t &counter); - void applyForce(const uint32_t particleindex, const int8_t xforce, const int8_t yforce); // use this for advanced property particles + [[gnu::hot]] void applyForce(PSparticle &part, const int8_t xforce, const int8_t yforce, uint8_t &counter); + [[gnu::hot]] void applyForce(const uint32_t particleindex, const int8_t xforce, const int8_t yforce); // use this for advanced property particles void applyForce(const int8_t xforce, const int8_t yforce); // apply a force to all particles void applyAngleForce(PSparticle &part, const int8_t force, const uint16_t angle, uint8_t &counter); void applyAngleForce(const uint32_t particleindex, const int8_t force, const uint16_t angle); // use this for advanced property particles @@ -209,17 +209,17 @@ class ParticleSystem2D { private: //rendering functions void ParticleSys_render(); - void renderParticle(const uint32_t particleindex, const uint32_t brightness, const CRGB& color, const bool wrapX, const bool wrapY); + [[gnu::hot]] void renderParticle(const uint32_t particleindex, const uint32_t brightness, const CRGB& color, const bool wrapX, const bool wrapY); //paricle physics applied by system if flags are set void applyGravity(); // applies gravity to all particles void handleCollisions(); - void collideParticles(PSparticle &particle1, PSparticle &particle2, const int32_t dx, const int32_t dy); + [[gnu::hot]] void collideParticles(PSparticle &particle1, PSparticle &particle2, const int32_t dx, const int32_t dy); void fireParticleupdate(); //utility functions void updatePSpointers(const bool isadvanced, const bool sizecontrol); // update the data pointers to current segment data space void updateSize(PSadvancedParticle *advprops, PSsizeControl *advsize); // advanced size control void getParticleXYsize(PSadvancedParticle *advprops, PSsizeControl *advsize, uint32_t &xsize, uint32_t &ysize); - void bounce(int8_t &incomingspeed, int8_t ¶llelspeed, int32_t &position, const uint32_t maxposition); // bounce on a wall + [[gnu::hot]] void bounce(int8_t &incomingspeed, int8_t ¶llelspeed, int32_t &position, const uint32_t maxposition); // bounce on a wall // note: variables that are accessed often are 32bit for speed PSsettings2D particlesettings; // settings used when updating particles (can also used by FX to move sources), do not edit properties directly, use functions above uint32_t numParticles; // total number of particles allocated by this system note: during transitions, less are available, use availableParticles @@ -340,7 +340,7 @@ class ParticleSystem1D int32_t sprayEmit(const PSsource1D &emitter); void particleMoveUpdate(PSparticle1D &part, PSparticleFlags1D &partFlags, PSsettings1D *options = NULL, PSadvancedParticle1D *advancedproperties = NULL); // move function //particle physics - void applyForce(PSparticle1D &part, const int8_t xforce, uint8_t &counter); //apply a force to a single particle + [[gnu::hot]] void applyForce(PSparticle1D &part, const int8_t xforce, uint8_t &counter); //apply a force to a single particle void applyForce(const int8_t xforce); // apply a force to all particles void applyGravity(PSparticle1D &part, PSparticleFlags1D &partFlags); // applies gravity to single particle (use this for sources) void applyFriction(const int32_t coefficient); // apply friction to all used particles @@ -380,12 +380,12 @@ class ParticleSystem1D //paricle physics applied by system if flags are set void applyGravity(); // applies gravity to all particles void handleCollisions(); - void collideParticles(PSparticle1D &particle1, const PSparticleFlags1D &particle1flags, PSparticle1D &particle2, const PSparticleFlags1D &particle2flags, int32_t dx, int32_t relativeVx, uint32_t collisiondistance); + [[gnu::hot]] void collideParticles(PSparticle1D &particle1, const PSparticleFlags1D &particle1flags, PSparticle1D &particle2, const PSparticleFlags1D &particle2flags, int32_t dx, int32_t relativeVx, uint32_t collisiondistance); //utility functions void updatePSpointers(const bool isadvanced); // update the data pointers to current segment data space //void updateSize(PSadvancedParticle *advprops, PSsizeControl *advsize); // advanced size control - void bounce(int8_t &incomingspeed, int8_t ¶llelspeed, int32_t &position, const uint32_t maxposition); // bounce on a wall + [[gnu::hot]] void bounce(int8_t &incomingspeed, int8_t ¶llelspeed, int32_t &position, const uint32_t maxposition); // bounce on a wall // note: variables that are accessed often are 32bit for speed PSsettings1D particlesettings; // settings used when updating particles uint32_t numParticles; // total number of particles allocated by this system note: never use more than this, even if more are available (only this many advanced particles are allocated) From c63f83992d1c61589bdd5a47b9fc30765431b36c Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sat, 18 Jan 2025 13:14:27 +0100 Subject: [PATCH 184/219] cleanup, improvements to PS bouncing ball, replaced multicomet - bouncing balls is now named pinball and has settings/parameters updated to be a replacement for multicomet --- wled00/FX.cpp | 55 +++++++++++++++++++++++-------------- wled00/FX.h | 2 +- wled00/FXparticleSystem.cpp | 43 +++++++++++++---------------- wled00/FXparticleSystem.h | 2 +- 4 files changed, 55 insertions(+), 47 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 29db412cf6..aa202ba8f0 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -1223,6 +1223,7 @@ uint16_t mode_dual_larson_scanner(void){ } static const char _data_FX_MODE_DUAL_LARSON_SCANNER[] PROGMEM = "Scanner Dual@!,Trail,Delay,,,Dual,Bi-delay;!,!,!;!;;m12=0,c1=0"; +#ifdef ENABLE_1D_PS_REPLACEMENTS /* * Firing comets from one end. "Lighthouse" */ @@ -1249,6 +1250,7 @@ uint16_t mode_comet(void) { return FRAMETIME; } static const char _data_FX_MODE_COMET[] PROGMEM = "Lighthouse@!,Fade rate;!,!;!"; +#endif // ENABLE_1D_PS_REPLACEMENTS /* * Fireworks function. @@ -9168,17 +9170,17 @@ Also replaces rolling balls and juggle (and maybe popcorn) Uses palette for particle color by DedeHai (Damian Schneider) */ -uint16_t mode_particleBouncingBalls(void) { +uint16_t mode_particlePinball(void) { ParticleSystem1D *PartSys = NULL; if (SEGMENT.call == 0) { // initialization if (!initParticleSystem1D(PartSys, 1, 128, 0, true)) // init return mode_static(); // allocation failed or is single pixel PartSys->sources[0].sourceFlags.collide = true; // seeded particles will collide (if enabled) + PartSys->setKillOutOfBounds(true); // out of bounds particles dont return PartSys->sources[0].source.x = PS_P_RADIUS_1D; //emit at bottom - PartSys->sources[0].maxLife = 900; // maximum lifetime in frames + PartSys->sources[0].maxLife = 0xFFFF; // maximum lifetime in frames (long but not infinite to avoid perpetual handling, this is enough to travel 4000 pixels at min speed) PartSys->sources[0].minLife = PartSys->sources[0].maxLife; - PartSys->setBounce(true); SEGENV.aux0 = 1; SEGENV.aux1 = 500; //set out of speed range to ensure uptate on first call } @@ -9191,26 +9193,26 @@ uint16_t mode_particleBouncingBalls(void) { // Particle System settings //uint32_t hardness = 240 + (SEGMENT.custom1>>4); PartSys->updateSystem(); // update system properties (dimensions and data pointers) - PartSys->setGravity(1 + (SEGMENT.custom3 >> 1)); // set gravity (8 is default strength) + PartSys->setGravity(map(SEGMENT.custom3, 0 , 31, 0 , 16)); // set gravity (8 is default strength) + PartSys->setBounce(SEGMENT.custom3); // disables bounce if no gravity is used PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur - PartSys->sources[0].var = SEGMENT.speed >> 3; - PartSys->sources[0].v = (SEGMENT.speed >> 1) - (SEGMENT.speed >> 3); PartSys->enableParticleCollisions(SEGMENT.check1, 254); // enable collisions and set particle collision hardness (do not use full hardness or particles speed up due to pushing, can not be made perfectly balanced) PartSys->setUsedParticles(SEGMENT.intensity); PartSys->setColorByPosition(SEGMENT.check3); + bool updateballs = false; + if (SEGENV.aux1 != SEGMENT.speed + SEGMENT.intensity + SEGMENT.check2 + SEGMENT.custom1) // user settings change + updateballs = true; + if (SEGMENT.check2) { //rolling balls PartSys->setGravity(0); PartSys->setWallHardness(255); - bool updateballs = false; - if (SEGENV.aux1 != SEGMENT.speed + SEGMENT.intensity + SEGMENT.check2 + SEGMENT.custom1) // user settings change - updateballs = true; for (uint32_t i = 0; i < PartSys->usedParticles; i++) { - if ((PartSys->particles[i].vx > 8 || PartSys->particles[i].vx < -8) && PartSys->particles[i].ttl > 150) //let only slow particles die (ensures no stopped particles) + if ((PartSys->particles[i].vx > 8 || PartSys->particles[i].vx < -8) && PartSys->particles[i].ttl > 200) //let only slow particles die (ensures no stopped particles) PartSys->particles[i].ttl = 260; //set alive at full intensity if (updateballs || PartSys->particles[i].ttl == 0) { //speed changed or particle died, set particle properties - PartSys->particles[i].ttl = 260 + SEGMENT.speed; + PartSys->particles[i].ttl = 260; PartSys->particleFlags[i].collide = true; int32_t newspeed = hw_random16(20 + (SEGMENT.speed >> 2)) + (SEGMENT.speed >> 3); PartSys->particles[i].vx = PartSys->particles[i].vx > 0 ? newspeed : -newspeed; //keep the direction @@ -9222,15 +9224,24 @@ uint16_t mode_particleBouncingBalls(void) { } else { //bouncing balls PartSys->setWallHardness(220); + PartSys->sources[0].var = SEGMENT.speed >> 3; + int32_t newspeed = 2 + (SEGMENT.speed >> 1) - (SEGMENT.speed >> 3); + PartSys->sources[0].v = newspeed; //check for balls that are 'laying on the ground' and remove them for (uint32_t i = 0; i < PartSys->usedParticles; i++) { - if (PartSys->particles[i].vx == 0 && PartSys->particles[i].x < (PS_P_RADIUS_1D << 1)) + if (PartSys->particles[i].vx == 0 && PartSys->particles[i].x < (PS_P_RADIUS_1D + SEGMENT.custom1)) PartSys->particles[i].ttl = 0; + if (updateballs && SEGMENT.custom3 == 0) { + PartSys->advPartProps[i].size = SEGMENT.custom1; + if(SEGMENT.custom3 == 0) //gravity off, update speed + PartSys->particles[i].vx = PartSys->particles[i].vx > 0 ? newspeed : -newspeed; //keep the direction + } } // every nth frame emit a ball - if (SEGMENT.call % SEGENV.aux0 == 0) { - SEGENV.aux0 = (260 - SEGMENT.intensity) + hw_random16(280 - SEGMENT.intensity); + if (SEGMENT.call > SEGENV.step) { + int interval = 520 - ((int)SEGMENT.intensity << 1); + SEGENV.step += interval + hw_random16(interval); PartSys->sources[0].source.hue = hw_random16(); //set ball color PartSys->sources[0].sat = 255; PartSys->sources[0].size = SEGMENT.custom1; @@ -9239,14 +9250,14 @@ uint16_t mode_particleBouncingBalls(void) { } SEGENV.aux1 = SEGMENT.speed + SEGMENT.intensity + SEGMENT.check2 + SEGMENT.custom1; for (uint32_t i = 0; i < PartSys->usedParticles; i++) { - if (SEGMENT.speed > 200) + //if (SEGMENT.speed > 200) PartSys->particleMoveUpdate(PartSys->particles[i], PartSys->particleFlags[i]); //increase speed on high settings by calling the move function twice } PartSys->update(); // update and render return FRAMETIME; } -static const char _data_FX_MODE_PSBOUNCINGBALLS[] PROGMEM = "PS Bouncing Balls@Speed,!,Size,Blur,Gravity,Collide,Rolling,Position Color;,!;!;1;pal=0,sx=100,ix=85,c1=30,c2=0,c3=8"; +static const char _data_FX_MODE_PSPINBALL[] PROGMEM = "PS Pinball@Speed,!,Size,Blur,Gravity,Collide,Rolling,Position Color;,!;!;1;pal=0,c1=30,c2=0,c3=8"; /* Particle Replacement for original Dancing Shadows: @@ -9708,7 +9719,7 @@ uint16_t mode_particle1Dspray(void) { int32_t gravity = (int32_t)SEGMENT.custom3 - 15; // gravity setting, 0-14 is negative, 16 - 31 is positive PartSys->setGravity(abs(gravity)); // use reversgrav setting to invert gravity (for proper 'floor' and out of bounce handling) - PartSys->sources[0].source.hue = hw_random16(); // TODO: add colormodes like in hourglass? + PartSys->sources[0].source.hue = SEGMENT.aux0; // hw_random16(); PartSys->sources[0].var = 20; PartSys->sources[0].minLife = 200; PartSys->sources[0].maxLife = 400; @@ -9716,8 +9727,10 @@ uint16_t mode_particle1Dspray(void) { PartSys->sources[0].v = map(SEGMENT.speed, 0 , 255, -127 + PartSys->sources[0].var, 127 - PartSys->sources[0].var); // particle emit speed PartSys->sources[0].sourceFlags.reversegrav = gravity < 0 ? true : false; - if(hw_random() % (1 + ((255 - SEGMENT.intensity) >> 3)) == 0) + if(hw_random() % (1 + ((255 - SEGMENT.intensity) >> 3)) == 0) { PartSys->sprayEmit(PartSys->sources[0]); // emit a particle + SEGMENT.aux0++; // increment hue + } //update color settings PartSys->setColorByAge(SEGMENT.check1); // overruled by 'color by position' @@ -9729,7 +9742,7 @@ uint16_t mode_particle1Dspray(void) { return FRAMETIME; } -static const char _data_FX_MODE_PS_1DSPRAY[] PROGMEM = "PS 1D Spray@!,!,Position,Blur,Gravity,AgeColor,Bounce,Position Color;,!;!;1;sx=200,ix=220,c1=4,c2=0,c3=28,o1=1,o2=1"; +static const char _data_FX_MODE_PS_1DSPRAY[] PROGMEM = "PS 1D Spray@Speed(+/-),!,Position,Blur,Gravity(+/-),AgeColor,Bounce,Position Color;,!;!;1;sx=200,ix=220,c1=4,c2=0,o1=1,o2=1"; /* Particle based balance: particles move back and forth (1D pendent to 2D particle box) @@ -10303,7 +10316,6 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_RUNNING_RANDOM, &mode_running_random, _data_FX_MODE_RUNNING_RANDOM); addEffect(FX_MODE_LARSON_SCANNER, &mode_larson_scanner, _data_FX_MODE_LARSON_SCANNER); addEffect(FX_MODE_RAIN, &mode_rain, _data_FX_MODE_RAIN); - addEffect(FX_MODE_COMET, &mode_comet, _data_FX_MODE_COMET); addEffect(FX_MODE_PRIDE_2015, &mode_pride_2015, _data_FX_MODE_PRIDE_2015); addEffect(FX_MODE_COLORWAVES, &mode_colorwaves, _data_FX_MODE_COLORWAVES); addEffect(FX_MODE_FIREWORKS, &mode_fireworks, _data_FX_MODE_FIREWORKS); @@ -10346,6 +10358,7 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_SPOTS, &mode_spots, _data_FX_MODE_SPOTS); addEffect(FX_MODE_SPOTS_FADE, &mode_spots_fade, _data_FX_MODE_SPOTS_FADE); #ifdef ENABLE_1D_PS_REPLACEMENTS + addEffect(FX_MODE_COMET, &mode_comet, _data_FX_MODE_COMET); addEffect(FX_MODE_ROLLINGBALLS, &rolling_balls, _data_FX_MODE_ROLLINGBALLS); addEffect(FX_MODE_SPARKLE, &mode_sparkle, _data_FX_MODE_SPARKLE); addEffect(FX_MODE_GLITTER, &mode_glitter, _data_FX_MODE_GLITTER); @@ -10480,7 +10493,7 @@ void WS2812FX::setupEffectData() { #ifndef WLED_DISABLE_PARTICLESYSTEM1D addEffect(FX_MODE_PSDRIP, &mode_particleDrip, _data_FX_MODE_PARTICLEDRIP); -addEffect(FX_MODE_PSBOUNCINGBALLS, &mode_particleBouncingBalls, _data_FX_MODE_PSBOUNCINGBALLS); //potential replacement for: bouncing balls, rollingballs, popcorn +addEffect(FX_MODE_PSPINBALL, &mode_particlePinball, _data_FX_MODE_PSPINBALL); //potential replacement for: bouncing balls, rollingballs, popcorn addEffect(FX_MODE_PSDANCINGSHADOWS, &mode_particleDancingShadows, _data_FX_MODE_PARTICLEDANCINGSHADOWS); addEffect(FX_MODE_PSFIREWORKS1D, &mode_particleFireworks1D, _data_FX_MODE_PS_FIREWORKS1D); addEffect(FX_MODE_PSSPARKLER, &mode_particleSparkler, _data_FX_MODE_PS_SPARKLER); diff --git a/wled00/FX.h b/wled00/FX.h index 156fb49152..562954cb12 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -338,7 +338,7 @@ extern byte realtimeMode; // used in getMappedPixelIndex() #define FX_MODE_PARTICLEGHOSTRIDER 200 #define FX_MODE_PARTICLEBLOBS 201 #define FX_MODE_PSDRIP 202 -#define FX_MODE_PSBOUNCINGBALLS 203 +#define FX_MODE_PSPINBALL 203 #define FX_MODE_PSDANCINGSHADOWS 204 #define FX_MODE_PSFIREWORKS1D 205 #define FX_MODE_PSSPARKLER 206 diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index 77aaba2628..06e1b36c83 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -73,7 +73,9 @@ void ParticleSystem2D::update(void) { //update size settings before handling collisions if (advPartSize) { for (uint32_t i = 0; i < usedParticles; i++) { - updateSize(&advPartProps[i], &advPartSize[i]); + if(updateSize(&advPartProps[i], &advPartSize[i]) == false) { // if particle shrinks to 0 size + particles[i].ttl = 0; // kill particle + } } } @@ -111,7 +113,6 @@ void ParticleSystem2D::setUsedParticles(uint8_t percentage) { PSPRINTLN(usedParticles); } -//TODO: inline these functions void ParticleSystem2D::setWallHardness(uint8_t hardness) { wallHardness = hardness; } @@ -276,7 +277,7 @@ void ParticleSystem2D::particleMoveUpdate(PSparticle &part, PSparticleFlags &par if ((newX < (int32_t)particleHardRadius) || (newX > (int32_t)(maxX - particleHardRadius))) // reached a wall bounce(part.vx, part.vy, newX, maxX); } - else if(!checkBoundsAndWrap(newX, maxX, renderradius, options->wrapX)) { // check out of bounds TODO: not checking out of bounds when bounce is enabled used to lead to crashes, seems fixed now. test more. + else if(!checkBoundsAndWrap(newX, maxX, renderradius, options->wrapX)) { // check out of bounds partFlags.outofbounds = true; if (options->killoutofbounds) part.ttl = 0; @@ -320,21 +321,12 @@ void ParticleSystem2D::fireParticleupdate() { particles[i].y = newY; } } -/* - // this loop saves 150 bytes of flash but is 5% slower - for (uint32_t i = 0; i < usedParticles; i++) { - if (particles[i].ttl > 0) { - particles[i].y += (particles[i].ttl >> 2); // younger particles move faster upward as they are hotter - particleMoveUpdate(particles[i]); - } - } -*/ } -// update advanced particle size control -void ParticleSystem2D::updateSize(PSadvancedParticle *advprops, PSsizeControl *advsize) { +// update advanced particle size control, returns false if particle shrinks to 0 size +bool ParticleSystem2D::updateSize(PSadvancedParticle *advprops, PSsizeControl *advsize) { if (advsize == NULL) // safety check - return; + return false; // grow/shrink particle int32_t newsize = advprops->size; uint32_t counter = advsize->sizecounter; @@ -367,7 +359,8 @@ void ParticleSystem2D::updateSize(PSadvancedParticle *advprops, PSsizeControl *a if (newsize > advsize->minsize) { newsize -= increment; if (newsize <= advsize->minsize) { - //if (advsize->minsize == 0) part.ttl = 0; //TODO: need to pass particle or return kill instruction + if (advsize->minsize == 0) + return false; // particle shrunk to zero advsize->shrink = false; // disable shrinking newsize = advsize->minsize; // limit if (advsize->pulsate) advsize->grow = true; @@ -377,8 +370,9 @@ void ParticleSystem2D::updateSize(PSadvancedParticle *advprops, PSsizeControl *a advprops->size = newsize; // handle wobbling if (advsize->wobble) { - advsize->asymdir += advsize->wobblespeed; // todo: need better wobblespeed control? counter is already in the struct... + advsize->asymdir += advsize->wobblespeed; // note: if need better wobblespeed control a counter is already in the struct } + return true; } // calculate x and y size for asymmetrical particles (advanced size control) @@ -629,7 +623,7 @@ void ParticleSystem2D::ParticleSys_render() { } else { brightness = min((particles[i].ttl << 1), (int)255); - baseRGB = ColorFromPalette(SEGPALETTE, particles[i].hue, 255); // TODO: use loadPalette(CRGBPalette16 &targetPalette, SEGMENT.palette), .palette should be updated immediately at palette change, only use local palette during FX transitions, not during normal transitions. -> why not always? + baseRGB = ColorFromPalette(SEGPALETTE, particles[i].hue, 255); if (particles[i].sat < 255) { CHSV32 baseHSV; rgb2hsv((uint32_t((byte(baseRGB.r) << 16) | (byte(baseRGB.g) << 8) | (byte(baseRGB.b)))), baseHSV); // convert to HSV @@ -1096,7 +1090,7 @@ uint32_t calculateNumberOfParticles2D(uint32_t const pixels, const bool isadvanc numberofParticles /= 8; // if advanced size control is used, much fewer particles are needed note: if changing this number, adjust FX using this accordingly //make sure it is a multiple of 4 for proper memory alignment (easier than using padding bytes) - numberofParticles = ((numberofParticles+3) >> 2) << 2; // TODO: with a separate particle buffer, this is unnecessary + numberofParticles = ((numberofParticles+3) >> 2) << 2; // note: with a separate particle buffer, this is probably unnecessary return numberofParticles; } @@ -1214,14 +1208,15 @@ ParticleSystem1D::ParticleSystem1D(uint32_t length, uint32_t numberofparticles, // update function applies gravity, moves the particles, handles collisions and renders the particles void ParticleSystem1D::update(void) { PSadvancedParticle1D *advprop = NULL; - // handle collisions (can push particles, must be done before updating particles or they can render out of bounds, causing a crash if using local buffer for speed) - if (particlesettings.useCollisions) - handleCollisions(); //apply gravity globally if enabled - if (particlesettings.useGravity) //note: in 1D system, applying gravity after collisions also works TODO: which one is really better for stacking / oscillations? + if (particlesettings.useGravity) //note: in 1D system, applying gravity after collisions also works but may be worse applyGravity(); + // handle collisions (can push particles, must be done before updating particles or they can render out of bounds, causing a crash if using local buffer for speed) + if (particlesettings.useCollisions) + handleCollisions(); + //move all particles for (uint32_t i = 0; i < usedParticles; i++) { particleMoveUpdate(particles[i], particleFlags[i], nullptr, advPartProps ? &advPartProps[i] : nullptr); @@ -2183,7 +2178,7 @@ void particleHandover(void *buffer, size_t structSize, int32_t numToTransfer) { // update number of particles to use, limit to allocated (= particles allocated by the calling system) in case more are available in the buffer void updateUsedParticles(const uint32_t allocated, const uint32_t available, const uint8_t percentage, uint32_t &used) { - uint32_t wantsToUse = (allocated * ((uint32_t)percentage + 1)) >> 8; + uint32_t wantsToUse = 1 + ((allocated * ((uint32_t)percentage + 1)) >> 8); // always give 1 particle minimum used = max((uint32_t)2, min(available, wantsToUse)); // limit to available particles, use a minimum of 2 } diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index 61c85d2a5f..b9ad170c2f 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -217,7 +217,7 @@ class ParticleSystem2D { void fireParticleupdate(); //utility functions void updatePSpointers(const bool isadvanced, const bool sizecontrol); // update the data pointers to current segment data space - void updateSize(PSadvancedParticle *advprops, PSsizeControl *advsize); // advanced size control + bool updateSize(PSadvancedParticle *advprops, PSsizeControl *advsize); // advanced size control void getParticleXYsize(PSadvancedParticle *advprops, PSsizeControl *advsize, uint32_t &xsize, uint32_t &ysize); [[gnu::hot]] void bounce(int8_t &incomingspeed, int8_t ¶llelspeed, int32_t &position, const uint32_t maxposition); // bounce on a wall // note: variables that are accessed often are 32bit for speed From bb6d21096cf1d69bd69a2d831f63df8448631cf7 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sat, 18 Jan 2025 14:40:16 +0100 Subject: [PATCH 185/219] cleanup, improvements, bugfixes - large size rendering now works without framebuffer - background adding is now done on buffer (if avilable) instead of segment - fixed overflow bug in 2D large particle rendering (it worked only for powers of 2 size) --- wled00/FX.cpp | 4 +- wled00/FXparticleSystem.cpp | 99 ++++++++++++++++++++----------------- 2 files changed, 55 insertions(+), 48 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index aa202ba8f0..cead084c6f 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -9716,7 +9716,7 @@ uint16_t mode_particle1Dspray(void) { PartSys->updateSystem(); // update system properties (dimensions and data pointers) PartSys->setBounce(SEGMENT.check2); PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur - int32_t gravity = (int32_t)SEGMENT.custom3 - 15; // gravity setting, 0-14 is negative, 16 - 31 is positive + int32_t gravity = -((int32_t)SEGMENT.custom3 - 16); // gravity setting, 0-15 is positive (down), 17 - 31 is negative (up) PartSys->setGravity(abs(gravity)); // use reversgrav setting to invert gravity (for proper 'floor' and out of bounce handling) PartSys->sources[0].source.hue = SEGMENT.aux0; // hw_random16(); @@ -9742,7 +9742,7 @@ uint16_t mode_particle1Dspray(void) { return FRAMETIME; } -static const char _data_FX_MODE_PS_1DSPRAY[] PROGMEM = "PS 1D Spray@Speed(+/-),!,Position,Blur,Gravity(+/-),AgeColor,Bounce,Position Color;,!;!;1;sx=200,ix=220,c1=4,c2=0,o1=1,o2=1"; +static const char _data_FX_MODE_PS_1DSPRAY[] PROGMEM = "PS Spray 1D@Speed(+/-),!,Position,Blur,Gravity(+/-),AgeColor,Bounce,Position Color;,!;!;1;sx=200,ix=220,c1=0,c2=0,o1=1"; /* Particle based balance: particles move back and forth (1D pendent to 2D particle box) diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index 06e1b36c83..f05582bae5 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -748,24 +748,32 @@ void ParticleSystem2D::renderParticle(const uint32_t particleindex, const uint32 for (uint32_t xrb = offset; xrb < rendersize + offset; xrb++) { xfb = xfb_orig + xrb; if (xfb > (uint32_t)maxXpixel) { - if (wrapX) // wrap x to the other side if required - xfb = xfb % (maxXpixel + 1); // TODO: this did not work in 1D system but appears to work in 2D (wrapped pixels were offset) under which conditions does this not work? + if (wrapX) { // wrap x to the other side if required + if (xfb > (uint32_t)maxXpixel << 1) // xfb is "negative", handle it + xfb = (maxXpixel + 1) + (int32_t)xfb; // this always overflows to within bounds + else + xfb = xfb % (maxXpixel + 1); // note: without the above "negative" check, this works only for powers of 2 + } else continue; } for (uint32_t yrb = offset; yrb < rendersize + offset; yrb++) { - yfb = yfb_orig + yrb; - if (yfb > (uint32_t)maxYpixel) { - if (wrapY) // wrap y to the other side if required - yfb = yfb % (maxYpixel + 1); + yfb = yfb_orig + yrb; + if (yfb > (uint32_t)maxYpixel) { + if (wrapY) {// wrap y to the other side if required + if (yfb > (uint32_t)maxYpixel << 1) // yfb is "negative", handle it + yfb = (maxYpixel + 1) + (int32_t)yfb; // this always overflows to within bounds + else + yfb = yfb % (maxYpixel + 1); // note: without the above "negative" check, this works only for powers of 2 + } + else + continue; + } + if (framebuffer) + fast_color_add(framebuffer[xfb + (maxYpixel - yfb) * (maxXpixel + 1)], renderbuffer[xrb + yrb * 10]); else - continue; - } - if (framebuffer) - fast_color_add(framebuffer[xfb + (maxYpixel - yfb) * (maxXpixel + 1)], renderbuffer[xrb + yrb * 10]); - else - SEGMENT.addPixelColorXY(xfb, maxYpixel - yfb, renderbuffer[xrb + yrb * 10],true); + SEGMENT.addPixelColorXY(xfb, maxYpixel - yfb, renderbuffer[xrb + yrb * 10],true); } } } else { // standard rendering @@ -1230,13 +1238,6 @@ void ParticleSystem1D::update(void) { } ParticleSys_render(); - - uint32_t bg_color = SEGCOLOR(1); //background color, set to black to overlay - if (bg_color > 0) { //if not black - for(int32_t i = 0; i <= maxXpixel; i++) { - SEGMENT.addPixelColor(i, bg_color, true); // TODO: can this be done in rendering function using local buffer? - } - } } // set percentage of used particles as uint8_t i.e 127 means 50% for example @@ -1358,7 +1359,7 @@ void ParticleSystem1D::particleMoveUpdate(PSparticle1D &part, PSparticleFlags1D if (advancedproperties) { // using individual particle size? if (advancedproperties->size > 1) - particleHardRadius = PS_P_MINHARDRADIUS_1D + (advancedproperties->size >> 1); // TODO: this may need optimization, radius and diameter is still a mess in 1D system. + particleHardRadius = PS_P_MINHARDRADIUS_1D + (advancedproperties->size >> 1); else // single pixel particles use half the collision distance for walls particleHardRadius = PS_P_MINHARDRADIUS_1D >> 1; renderradius = particleHardRadius; // note: for single pixel particles, it should be zero, but it does not matter as out of bounds checking is done in rendering function @@ -1536,9 +1537,19 @@ void ParticleSystem1D::ParticleSys_render() { else SEGMENT.blur(globalSmear, true); } + + // add background color + uint32_t bg_color = SEGCOLOR(1); + if (bg_color > 0) { //if not black + for(int32_t i = 0; i <= maxXpixel; i++) { + if (framebuffer) + fast_color_add(framebuffer[i], bg_color); + else + SEGMENT.addPixelColor(i, bg_color, true); + } + } // transfer local buffer back to segment (if available) transferBuffer(maxXpixel + 1, 0, useAdditiveTransfer); - } // calculate pixel positions and brightness distribution and render the particle to local buffer or global buffer @@ -1577,7 +1588,7 @@ void ParticleSystem1D::renderParticle(const uint32_t particleindex, const uint32 // check if particle has advanced size properties and buffer is available if (advPartProps && advPartProps[particleindex].size > 1) { - if (renderbuffer && framebuffer) { // TODO: add unbuffered large size rendering like in 2D system + if (renderbuffer) { memset(renderbuffer, 0, 10 * sizeof(CRGB)); // clear the buffer, renderbuffer is 10 pixels } else @@ -1610,15 +1621,18 @@ void ParticleSystem1D::renderParticle(const uint32_t particleindex, const uint32 xfb = xfb_orig + xrb; if (xfb > (uint32_t)maxXpixel) { if (wrap) { // wrap x to the other side if required - if (xfb > (uint32_t)maxXpixel << 1) // xfb is "negative" (note: for some reason, this check is needed in 1D but works without in 2D...) - xfb = (maxXpixel +1) + (int32_t)xfb; //TODO: remove this again and see if it works now (changed maxxpixel to unsigned) + if (xfb > (uint32_t)maxXpixel << 1) // xfb is "negative" + xfb = (maxXpixel + 1) + (int32_t)xfb; // this always overflows to within bounds else - xfb = xfb % (maxXpixel + 1); //TODO: can modulo be avoided? + xfb = xfb % (maxXpixel + 1); // note: without the above "negative" check, this works only for powers of 2 } else continue; } - fast_color_add(framebuffer[xfb], renderbuffer[xrb]); // TODO: add unbuffered large size rendering like in 2D system + if (framebuffer) + fast_color_add(framebuffer[xfb], renderbuffer[xrb]); + else + SEGMENT.addPixelColor(xfb, renderbuffer[xrb]); } } else { // standard rendering (2 pixels per particle) @@ -1827,7 +1841,7 @@ uint32_t calculateNumberOfParticles1D(const uint32_t fraction, const bool isadva numberofParticles = (numberofParticles * (fraction + 1)) >> 8; // calculate fraction of particles numberofParticles = numberofParticles < 20 ? 20 : numberofParticles; // 20 minimum //make sure it is a multiple of 4 for proper memory alignment (easier than using padding bytes) - numberofParticles = ((numberofParticles+3) >> 2) << 2; // TODO: with a separate particle buffer, this is unnecessary + numberofParticles = ((numberofParticles+3) >> 2) << 2; // note: with a separate particle buffer, this is probably unnecessary return numberofParticles; } @@ -1926,14 +1940,6 @@ static int32_t calcForce_dv(const int8_t force, uint8_t &counter) { return dv; } -// limit speed to prevent overflows -//TODO: inline this function? check if that uses a lot more flash. -/* -static int32_t limitSpeed(int32_t speed) { - return min((int32_t)PS_P_MAXSPEED, max((int32_t)-PS_P_MAXSPEED, speed)); - //return speed > PS_P_MAXSPEED ? PS_P_MAXSPEED : (speed < -PS_P_MAXSPEED ? -PS_P_MAXSPEED : speed); // note: this uses more code, not sure due to speed or inlining -}*/ - // check if particle is out of bounds and wrap it around if required, returns false if out of bounds static bool checkBoundsAndWrap(int32_t &position, const int32_t max, const int32_t particleradius, const bool wrap) { if ((uint32_t)position > (uint32_t)max) { // check if particle reached an edge, cast to uint32_t to save negative checking (max is always positive) @@ -2064,7 +2070,7 @@ void* particleMemoryManager(const uint32_t requestedParticles, size_t structSize if (buffer) partMemList.push_back({buffer, requestsize, 0, strip.getCurrSegmentId(), 0, 0, true}); // add buffer to list, set flag to transfer/init the particles note: if pushback fails, it may crash else - return nullptr; // there is no memory available !!! TODO: if localbuffer is allocated, free it and try again, its no use having a buffer but no particles + return nullptr; // there is no memory available TODO: if localbuffer is allocated, free it and try again, its no use having a buffer but no particles pmem = getPartMem(); // get the pointer to the new element (check that it was added) if (!pmem) { // something went wrong free(buffer); @@ -2083,7 +2089,7 @@ void* particleMemoryManager(const uint32_t requestedParticles, size_t structSize newAvailable = (maxParticles * progress) >> 16; // update total particles available to this PS (newAvailable is guaranteed to be smaller than maxParticles) if(newAvailable < 2) newAvailable = 2; // give 2 particle minimum (some FX may crash with less as they do i+1 access) if(maxParticles / numParticlesUsed > 3 && newAvailable > numParticlesUsed) newAvailable = numParticlesUsed; // limit to number of particles used for FX using a small amount, do not move the pointer anymore (will be set to base in final handover) - uint32_t bufferoffset = (maxParticles - 1) - newAvailable; // offset to new effect particles + uint32_t bufferoffset = (maxParticles - 1) - newAvailable; // offset to new effect particles (in particle structs, not bytes) if(bufferoffset < maxParticles) // safety check buffer = (void*)((uint8_t*)buffer + bufferoffset * structSize); // new effect gets the end of the buffer int32_t totransfer = newAvailable - availableToPS; // number of particles to transfer in this transition update @@ -2108,13 +2114,13 @@ void* particleMemoryManager(const uint32_t requestedParticles, size_t structSize if(totransfer < 0) totransfer = 0; // safety check particleHandover(buffer, structSize, totransfer); - //TODO: there is a bug here, in 1D system, this does not really work right. maybe an alignment problem??? (2D seems to work fine) - // -> bug seems magically fixed? if(maxParticles / numParticlesUsed > 3) { // FX uses less than 25%: move the already existing particles to the beginning of the buffer uint32_t usedbytes = availableToPS * structSize; - uint32_t bufferoffset = (maxParticles - 1) - availableToPS; // offset to existing particles (see above) - void* currentBuffer = (void*)((uint8_t*)buffer + bufferoffset * structSize); // pointer to current buffer start - memmove(buffer, currentBuffer, usedbytes); // move the existing particles to the beginning of the buffer + int32_t bufferoffset = (maxParticles - 1) - availableToPS; // offset to existing particles (see above) + if(bufferoffset < maxParticles) { // safety check + void* currentBuffer = (void*)((uint8_t*)buffer + bufferoffset * structSize); // pointer to current buffer start + memmove(buffer, currentBuffer, usedbytes); // move the existing particles to the beginning of the buffer + } } } // kill unused particles to they do not re-appear when transitioning to next FX @@ -2194,8 +2200,9 @@ bool segmentIsOverlay(void) { // TODO: this only needs to be checked when segmen } return false; } + // get the pointer to the particle memory for the segment -partMem* getPartMem(void) { // TODO: maybe there is a better/faster way than using vectors? +partMem* getPartMem(void) { uint8_t segID = strip.getCurrSegmentId(); for (partMem &pmem : partMemList) { if (pmem.id == segID) { @@ -2210,7 +2217,7 @@ void updateRenderingBuffer(uint32_t requiredpixels, bool isFramebuffer, bool ini PSPRINTLN("updateRenderingBuffer"); uint16_t& targetBufferSize = isFramebuffer ? frameBufferSize : renderBufferSize; // corresponding buffer size - //if(isFramebuffer) return; // debug only: disable frame-buffer buffer + // if(isFramebuffer) return; // debug/testing only: disable frame-buffer if(targetBufferSize < requiredpixels) { // check current buffer size CRGB** targetBuffer = isFramebuffer ? &framebuffer : &renderbuffer; // pointer to target buffer @@ -2287,7 +2294,7 @@ void transferBuffer(uint32_t width, uint32_t height, bool useAdditiveTransfer) { else { // color to add to segment is not black if(segmentcolor) { fast_color_add(*c, segmentRGB); // add segment color back to buffer if not black - clr = RGBW32(c->r,c->g,c->b,0); // convert to 32bit color (again) TODO: could convert first, then use 32bit adding function color_add() from colors.cpp + clr = RGBW32(c->r,c->g,c->b,0); // convert to 32bit color (again) and set the segment } SEGMENT.setPixelColorXY((int)x, (int)y, clr); // save back to segment after adding local buffer } @@ -2309,7 +2316,7 @@ void transferBuffer(uint32_t width, uint32_t height, bool useAdditiveTransfer) { else { // color to add to segment is not black if(segmentcolor) { fast_color_add(*c, segmentRGB); // add segment color back to buffer if not black - clr = RGBW32(c->r,c->g,c->b,0); // convert to 32bit color (again) TODO: could convert first, then use 32bit adding function color_add() from colors.cpp + clr = RGBW32(c->r,c->g,c->b,0); // convert to 32bit color (again) } SEGMENT.setPixelColor((int)x, clr); // save back to segment after adding local buffer } From 3df8b18176afd772f8ffcdf2d6913d5f8199c1e7 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sat, 18 Jan 2025 18:05:23 +0100 Subject: [PATCH 186/219] potential bugfix, compiler warning fix --- wled00/FX_fcn.cpp | 14 +++++++------- wled00/FXparticleSystem.cpp | 8 ++++---- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index 4236e8356e..39aa3f41c5 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -427,18 +427,18 @@ void Segment::beginDraw() { } void Segment::setCurrentPalette(bool loadOldPalette) { - if(loadOldPalette) { // load palette of old effect, used in particle system - loadPalette(_currentPalette, _t->_palette); + if(loadOldPalette && isInTransition()) { + loadPalette(_currentPalette, _t->_palette); // load palette of old effect, used in particle system return; } - else - loadPalette(_currentPalette, palette); - if(strip.paletteFade && progress() < 0xFFFFU) { + loadPalette(_currentPalette, palette); + + if(strip.paletteFade && isInTransition() && progress() < 0xFFFFU) { // blend palettes // there are about 255 blend passes of 48 "blends" to completely blend two palettes (in _dur time) // minimum blend time is 100ms maximum is 65535ms - unsigned noOfBlends = ((255U * progress()) / 0xFFFFU) - _t->_prevPaletteBlends; - for (unsigned i = 0; i < noOfBlends; i++, _t->_prevPaletteBlends++) nblendPaletteTowardPalette(_t->_palT, _currentPalette, 48); + int noOfBlends = ((255U * progress()) / 0xFFFFU) - _t->_prevPaletteBlends; + for (int i = 0; i < noOfBlends; i++, _t->_prevPaletteBlends++) nblendPaletteTowardPalette(_t->_palT, _currentPalette, 48); _currentPalette = _t->_palT; // copy transitioning/temporary palette } } diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index f05582bae5..755e322c15 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -1979,10 +1979,10 @@ static void fast_color_add(CRGB &c1, const CRGB &c2, const uint32_t scale) { c1.g = g; c1.b = b; } else { - uint32_t scale = (255U << 16) / max; - c1.r = (r * scale) >> 16; - c1.g = (g * scale) >> 16; - c1.b = (b * scale) >> 16; + uint32_t newscale = (255U << 16) / max; + c1.r = (r * newscale) >> 16; + c1.g = (g * newscale) >> 16; + c1.b = (b * newscale) >> 16; } } From f427bf9344c22bf8935247dd0f2ed1d659c1a18e Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sun, 19 Jan 2025 09:11:01 +0100 Subject: [PATCH 187/219] removed colorwaves and pride option from chase (not worthy replacements) --- wled00/FX.cpp | 40 ++-------------------------------------- 1 file changed, 2 insertions(+), 38 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 85be0c9398..d14937bad7 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -9864,43 +9864,6 @@ uint16_t mode_particleChase(void) { int32_t huestep = (((uint32_t)SEGMENT.custom2 << 19) / PartSys->usedParticles) >> 16; // hue increment - if (SEGMENT.check1) { // pride rainbow colors - // TODO: orignal FX also changes movement speed - // also the color change is too fast - int8_t* huedir = reinterpret_cast(PartSys->PSdataEnd); // assign data pointer - int8_t* sizedir = reinterpret_cast(PartSys->PSdataEnd + 1); - int32_t sizechange = 0; - - if (PartSys->advPartProps[0].size >= 254) - *sizedir = -1; - else if (PartSys->advPartProps[0].size <= (SEGMENT.custom1 >> 2)) - *sizedir = 1; - - if (SEGENV.aux1 > 64) - *huedir = -1; - else if (SEGENV.aux1 < 1) - *huedir = 1; - - if (SEGMENT.call % (1024 / (1 + (SEGMENT.speed >> 3))) == 0) - SEGENV.aux1 += *huedir; - huestep = SEGENV.aux1; // changes gradient spread - - if (SEGMENT.call % (255 / (1 + (SEGMENT.speed >> 2))) == 0) - sizechange = *sizedir; - - for (uint32_t i = 0; i < PartSys->usedParticles; i++) { - // PartSys->particles[i].hue = *basehue + (i * (SEGENV.aux1)) / PartSys->usedParticles; // gradient distribution - PartSys->advPartProps[i].size += sizechange; - } - } - if ((SEGMENT.check2 || SEGMENT.check1) && SEGMENT.call % (160 / ((SEGMENT.speed >> 3) + 128)) == 0) { // color waves - int32_t decrement = 2; - if (SEGMENT.check1) - decrement = 1; // slower hue change in pride mode - for (uint32_t i = 0; i < PartSys->usedParticles; i++) { - PartSys->particles[i].hue -= decrement; - } - } // wrap around (cannot use particle system wrap if distributing colors manually, it also wraps rendering which does not look good) for (int32_t i = (int32_t)PartSys->usedParticles - 1; i >= 0; i--) { // check from the back, last particle wraps first, multiple particles can overrun per frame if (PartSys->particles[i].x > PartSys->maxX + PS_P_RADIUS_1D + PartSys->advPartProps[i].size) { // wrap it around @@ -9918,7 +9881,8 @@ uint16_t mode_particleChase(void) { PartSys->update(); // update and render return FRAMETIME; } -static const char _data_FX_MODE_PS_CHASE[] PROGMEM = "PS Chase@!,Density,Size,Hue,Blur,Pride,Color Waves,Position Color;,!;!;1;pal=11,sx=50,ix=100,c2=5,c3=0"; +static const char _data_FX_MODE_PS_CHASE[] PROGMEM = "PS Chase@!,Density,Size,Hue,Blur,,,Position Color;,!;!;1;pal=11,sx=50,ix=100,c2=5,c3=0"; + /* Particle Fireworks Starburst replacement (smoother rendering, more settings) Uses palette for particle color From bb92f08de93c70d5a8d31444969384b4980a5386 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sun, 19 Jan 2025 09:35:14 +0100 Subject: [PATCH 188/219] updated #defines, removed PS from 1M and 2M builds --- platformio.ini | 12 ++++++ wled00/FX.cpp | 112 ++++++++----------------------------------------- 2 files changed, 30 insertions(+), 94 deletions(-) diff --git a/platformio.ini b/platformio.ini index ac1d12d64a..f0b70a3a95 100644 --- a/platformio.ini +++ b/platformio.ini @@ -381,6 +381,8 @@ platform_packages = ${common.platform_packages} board_build.ldscript = ${common.ldscript_2m512k} build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=\"ESP02\" + -D WLED_DISABLE_PARTICLESYSTEM2D + -D WLED_DISABLE_PARTICLESYSTEM1D lib_deps = ${esp8266.lib_deps} [env:esp8266_2m_compat] @@ -389,12 +391,16 @@ extends = env:esp8266_2m platform = ${esp8266.platform_compat} platform_packages = ${esp8266.platform_packages_compat} build_flags = ${common.build_flags} ${esp8266.build_flags_compat} -D WLED_RELEASE_NAME=\"ESP02_compat\" #-DWLED_DISABLE_2D + -D WLED_DISABLE_PARTICLESYSTEM1D + -D WLED_DISABLE_PARTICLESYSTEM2D [env:esp8266_2m_160] extends = env:esp8266_2m board_build.f_cpu = 160000000L build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=\"ESP02_160\" -D USERMOD_AUDIOREACTIVE + -D WLED_DISABLE_PARTICLESYSTEM1D + -D WLED_DISABLE_PARTICLESYSTEM2D [env:esp01_1m_full] board = esp01_1m @@ -404,6 +410,8 @@ board_build.ldscript = ${common.ldscript_1m128k} build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=\"ESP01\" -D WLED_DISABLE_OTA ; -D WLED_USE_REAL_MATH ;; may fix wrong sunset/sunrise times, at the cost of 7064 bytes FLASH and 975 bytes RAM + -D WLED_DISABLE_PARTICLESYSTEM1D + -D WLED_DISABLE_PARTICLESYSTEM2D lib_deps = ${esp8266.lib_deps} [env:esp01_1m_full_compat] @@ -412,6 +420,8 @@ extends = env:esp01_1m_full platform = ${esp8266.platform_compat} platform_packages = ${esp8266.platform_packages_compat} build_flags = ${common.build_flags} ${esp8266.build_flags_compat} -D WLED_RELEASE_NAME=\"ESP01_compat\" -D WLED_DISABLE_OTA #-DWLED_DISABLE_2D + -D WLED_DISABLE_PARTICLESYSTEM1D + -D WLED_DISABLE_PARTICLESYSTEM2D [env:esp01_1m_full_160] extends = env:esp01_1m_full @@ -419,6 +429,8 @@ board_build.f_cpu = 160000000L build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=\"ESP01_160\" -D WLED_DISABLE_OTA -D USERMOD_AUDIOREACTIVE ; -D WLED_USE_REAL_MATH ;; may fix wrong sunset/sunrise times, at the cost of 7064 bytes FLASH and 975 bytes RAM + -D WLED_DISABLE_PARTICLESYSTEM1D + -D WLED_DISABLE_PARTICLESYSTEM2D [env:esp32dev] board = esp32dev diff --git a/wled00/FX.cpp b/wled00/FX.cpp index aedd5462ad..e5fca37b4f 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -19,8 +19,7 @@ #if !(defined(WLED_DISABLE_PARTICLESYSTEM2D) && defined(WLED_DISABLE_PARTICLESYSTEM1D)) #include "FXparticleSystem.h" #else -#define ENABLE_1D_PS_REPLACEMENTS -#define ENABLE_2D_PS_REPLACEMENTS +#define WLED_PS_DONT_REPLACE_FX #endif ////////////// @@ -1223,7 +1222,7 @@ uint16_t mode_dual_larson_scanner(void){ } static const char _data_FX_MODE_DUAL_LARSON_SCANNER[] PROGMEM = "Scanner Dual@!,Trail,Delay,,,Dual,Bi-delay;!,!,!;!;;m12=0,c1=0"; -#ifdef ENABLE_1D_PS_REPLACEMENTS +#ifdef WLED_PS_DONT_REPLACE_FX /* * Firing comets from one end. "Lighthouse" */ @@ -1250,7 +1249,7 @@ uint16_t mode_comet(void) { return FRAMETIME; } static const char _data_FX_MODE_COMET[] PROGMEM = "Lighthouse@!,Fade rate;!,!;!"; -#endif // ENABLE_1D_PS_REPLACEMENTS +#endif // WLED_PS_DONT_REPLACE_FX /* * Fireworks function. @@ -2083,7 +2082,7 @@ uint16_t mode_palette() { } static const char _data_FX_MODE_PALETTE[] PROGMEM = "Palette@Shift,Size,Rotation,,,Animate Shift,Animate Rotation,Anamorphic;;!;12;ix=112,c1=0,o1=1,o2=0,o3=1"; -#if (defined(ENABLE_2D_PS_REPLACEMENTS) || defined(ENABLE_1D_PS_REPLACEMENTS)) +#ifdef WLED_PS_DONT_REPLACE_FX // WLED limitation: Analog Clock overlay will NOT work when Fire2012 is active // Fire2012 by Mark Kriegsman, July 2012 // as part of "Five Elements" shown here: http://youtu.be/knWiGsmgycY @@ -2170,7 +2169,7 @@ uint16_t mode_fire_2012() { return FRAMETIME; } static const char _data_FX_MODE_FIRE_2012[] PROGMEM = "Fire 2012@Cooling,Spark rate,,2D Blur,Boost;;!;1;pal=35,sx=64,ix=160,m12=1,c2=128"; // bars -#endif // ENABLE_2D_PS_REPLACEMENTS || ENABLE_1D_PS_REPLACEMENTS +#endif // WLED_PS_DONT_REPLACE_FX // colored stripes pulsing at a defined Beats-Per-Minute (BPM) uint16_t mode_bpm() { @@ -3018,7 +3017,7 @@ uint16_t mode_bouncing_balls(void) { } static const char _data_FX_MODE_BOUNCINGBALLS[] PROGMEM = "Bouncing Balls@Gravity,# of balls,,,,,Overlay;!,!,!;!;1;m12=1"; //bar -#ifdef ENABLE_1D_PS_REPLACEMENTS +#ifdef WLED_PS_DONT_REPLACE_FX /* * bouncing balls on a track track Effect modified from Aircoookie's bouncing balls * Courtesy of pjhatch (https://github.com/pjhatch) @@ -3119,7 +3118,7 @@ static uint16_t rolling_balls(void) { return FRAMETIME; } static const char _data_FX_MODE_ROLLINGBALLS[] PROGMEM = "Rolling Balls@!,# of balls,,,,Collide,Overlay,Trails;!,!,!;!;1;m12=1"; //bar -#endif // ENABLE_1D_PS_REPLACEMENTS +#endif // WLED_PS_DONT_REPLACE_FX /* * Sinelon stolen from FASTLED examples @@ -3382,7 +3381,7 @@ uint16_t mode_candle_multi() } static const char _data_FX_MODE_CANDLE_MULTI[] PROGMEM = "Candle Multi@!,!;!,!;!;;sx=96,ix=224,pal=0"; -#ifdef ENABLE_1D_PS_REPLACEMENTS +#ifdef WLED_PS_DONT_REPLACE_FX /* / Fireworks in starburst effect / based on the video: https://www.reddit.com/r/arduino/comments/c3sd46/i_made_this_fireworks_effect_for_my_led_strips/ @@ -3514,9 +3513,9 @@ uint16_t mode_starburst(void) { } #undef STARBURST_MAX_FRAG static const char _data_FX_MODE_STARBURST[] PROGMEM = "Fireworks Starburst@Chance,Fragments,,,,,Overlay;,!;!;;pal=11,m12=0"; -#endif // ENABLE_1D_PS_REPLACEMENTS +#endif // WLED_PS_DONT_REPLACE_FX - #if (defined(ENABLE_1D_PS_REPLACEMENTS) || defined(ENABLE_2D_PS_REPLACEMENTS)) + #ifdef WLED_PS_DONT_REPLACE_FX /* * Exploding fireworks effect * adapted from: http://www.anirama.com/1000leds/1d-fireworks/ @@ -3654,7 +3653,7 @@ uint16_t mode_exploding_fireworks(void) } #undef MAX_SPARKS static const char _data_FX_MODE_EXPLODING_FIREWORKS[] PROGMEM = "Fireworks 1D@Gravity,Firing side;!,!;!;12;pal=11,ix=128"; -#endif // ENABLE_1D_PS_REPLACEMENTS || ENABLE_2D_PS_REPLACEMENTS +#endif // WLED_PS_DONT_REPLACE_FX /* * Drip Effect @@ -4302,7 +4301,7 @@ static const char _data_FX_MODE_CHUNCHUN[] PROGMEM = "Chunchun@!,Gap size;!,!;!" #define SPOT_MAX_COUNT 49 //Number of simultaneous waves #endif -#ifdef ENABLE_1D_PS_REPLACEMENTS +#ifdef WLED_PS_DONT_REPLACE_FX //13 bytes typedef struct Spotlight { float speed; @@ -4437,7 +4436,7 @@ uint16_t mode_dancing_shadows(void) return FRAMETIME; } static const char _data_FX_MODE_DANCING_SHADOWS[] PROGMEM = "Dancing Shadows@!,# of shadows;!;!"; -#endif // ENABLE_1D_PS_REPLACEMENTS +#endif // WLED_PS_DONT_REPLACE_FX /* Imitates a washing machine, rotating same waves forward, then pause, then backward. @@ -5866,7 +5865,7 @@ uint16_t mode_2Dcrazybees(void) { static const char _data_FX_MODE_2DCRAZYBEES[] PROGMEM = "Crazy Bees@!,Blur,,,,Smear;;!;2;pal=11,ix=0"; #undef MAX_BEES -#ifdef ENABLE_2D_PS_REPLACEMENTS +#ifdef WLED_PS_DONT_REPLACE_FX ///////////////////////// // 2D Ghost Rider // ///////////////////////// @@ -6057,7 +6056,7 @@ uint16_t mode_2Dfloatingblobs(void) { } static const char _data_FX_MODE_2DBLOBS[] PROGMEM = "Blobs@!,# blobs,Blur,Trail;!;!;2;c1=8"; #undef MAX_BLOBS -#endif // ENABLE_2D_PS_REPLACEMENTS +#endif // WLED_PS_DONT_REPLACE_FX //////////////////////////// // 2D Scrolling text // @@ -8964,73 +8963,6 @@ uint16_t mode_particleblobs(void) { return FRAMETIME; } static const char _data_FX_MODE_PARTICLEBLOBS[] PROGMEM = "PS Blobs@Speed,Blobs,Size,Life,Blur,Wobble,Collide,Pulsate;;!;2v;sx=30,ix=64,c1=200,c2=130,c3=0,o3=1"; - -/* - * Particle Fractal - * particles move, then split to form a fractal tree EXPERIMENTAL and non working! - * by DedeHai (Damian Schneider) - */ -uint16_t mode_particlefractal(void) { - ParticleSystem2D *PartSys = NULL; - uint32_t i; - - if (SEGMENT.call == 0) { // initialization - if (!initParticleSystem2D(PartSys, 1, 0, true, false)) // init, use advanced particles - return mode_static(); // allocation failed or not 2D - PartSys->setKillOutOfBounds(true); - } - else - PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS - - if (PartSys == NULL) - return mode_static(); // something went wrong, no data! - - PartSys->updateSystem(); // update system properties (dimensions and data pointers) - - if (SEGMENT.check2) - SEGENV.aux0 += SEGMENT.custom1 << 2; - else - SEGENV.aux0 -= SEGMENT.custom1 << 2; - - int16_t angleoffset = SEGMENT.custom2 << 6; - int8_t emitspeed = SEGMENT.speed >> 2; - - //check particle age, emit 2 particles at the end of the branch - for (i = 0; i < PartSys->usedParticles; i++) { - if(PartSys->particles[i].ttl > 0 && PartSys->particles[i].ttl < 260) { //alive and ripe - PartSys->particles[i].ttl = 0; - uint16_t currentangle = ((uint32_t)PartSys->advPartProps[i].forcecounter) << 7; // abuse forcecounter to track the angle - PartSys->sources[0].source.x = PartSys->particles[i].x; - PartSys->sources[0].source.y = PartSys->particles[i].y;; - PartSys->sources[0].source.hue = PartSys->particles[i].hue + 50; // todo: make color schemes - uint16_t angle = currentangle - angleoffset; - int32_t index = PartSys->angleEmit(PartSys->sources[0], angle, emitspeed); //upward TODO: make angle adjustable - //TODO: check if index >=0!!! - PartSys->advPartProps[index].forcecounter = angle >> 7; - angle = currentangle + angleoffset; - index = PartSys->angleEmit(PartSys->sources[0], angle, emitspeed); - PartSys->advPartProps[index].forcecounter = angle >> 7; - } - } - - if(SEGENV.call % (256-SEGMENT.intensity) == 0) { - PartSys->sources[0].source.x = (PartSys->maxX + 1) >> 1; - PartSys->sources[0].source.y = 5; - PartSys->sources[0].source.hue = 0; // todo: make color schemes - PartSys->sources[0].maxLife = 275; - PartSys->sources[0].minLife = 270; - uint32_t angle = ((uint32_t)SEGMENT.custom1) << 7; //16 bit angle, 0° to 180° - int32_t index = PartSys->angleEmit(PartSys->sources[0], angle, emitspeed); //upward TODO: make angle adjustable - //set the forcecounter to track the angle (only 8 bit precision...) - PartSys->advPartProps[index].forcecounter = angle >> 7; - } - - PartSys->setMotionBlur(((SEGMENT.custom3) << 3) + 7); - PartSys->update(); // update and render - return FRAMETIME; -} -static const char _data_FX_MODE_PARTICLEFRACTAL[] PROGMEM = "PS fractal (exp)@Speed,Intensity,Base angle,branch angle,Blur,,Direction;;!;2f;pal=13,ix=180,c1=0,c2=0,c3=8"; - #endif //WLED_DISABLE_PARTICLESYSTEM2D #endif // WLED_DISABLE_2D @@ -9042,7 +8974,7 @@ static const char _data_FX_MODE_PARTICLEFRACTAL[] PROGMEM = "PS fractal (exp)@Sp #ifndef WLED_DISABLE_PARTICLESYSTEM1D /* -Particle Drip replacement, also replaces Rain +Particle version of Drip and Rain Uses palette for particle color by DedeHai (Damian Schneider) */ @@ -9663,10 +9595,6 @@ uint16_t mode_particleHourglass(void) { else if(SEGMENT.check2) // auto reset SEGENV.aux1--; // countdown - //if(SEGMENT.call % (SEGMENT.speed >> 5) == 0) //more friction on higher falling rate to keep particles behaved - //if(SEGMENT.call % 6 == 0) - //PartSys->applyFriction(1); //keeps particles calm and stops mass collisions being handled improperly due to chaos TODO: can this be removed? seems to work fine without. - PartSys->update(); // update and render return FRAMETIME; @@ -9803,7 +9731,6 @@ Particle based Chase effect Uses palette for particle color by DedeHai (Damian Schneider) */ - uint16_t mode_particleChase(void) { ParticleSystem1D *PartSys = NULL; if (SEGMENT.call == 0) { // initialization @@ -10302,7 +10229,7 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_TRI_STATIC_PATTERN, &mode_tri_static_pattern, _data_FX_MODE_TRI_STATIC_PATTERN); addEffect(FX_MODE_SPOTS, &mode_spots, _data_FX_MODE_SPOTS); addEffect(FX_MODE_SPOTS_FADE, &mode_spots_fade, _data_FX_MODE_SPOTS_FADE); - #ifdef ENABLE_1D_PS_REPLACEMENTS + #ifdef WLED_PS_DONT_REPLACE_FX addEffect(FX_MODE_COMET, &mode_comet, _data_FX_MODE_COMET); addEffect(FX_MODE_ROLLINGBALLS, &rolling_balls, _data_FX_MODE_ROLLINGBALLS); addEffect(FX_MODE_SPARKLE, &mode_sparkle, _data_FX_MODE_SPARKLE); @@ -10310,8 +10237,6 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_SOLID_GLITTER, &mode_solid_glitter, _data_FX_MODE_SOLID_GLITTER); addEffect(FX_MODE_STARBURST, &mode_starburst, _data_FX_MODE_STARBURST); addEffect(FX_MODE_DANCING_SHADOWS, &mode_dancing_shadows, _data_FX_MODE_DANCING_SHADOWS); - #endif - #if (defined(ENABLE_1D_PS_REPLACEMENTS) || defined(ENABLE_2D_PS_REPLACEMENTS)) addEffect(FX_MODE_FIRE_2012, &mode_fire_2012, _data_FX_MODE_FIRE_2012); addEffect(FX_MODE_EXPLODING_FIREWORKS, &mode_exploding_fireworks, _data_FX_MODE_EXPLODING_FIREWORKS); #endif @@ -10376,7 +10301,7 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_2DSPACESHIPS, &mode_2Dspaceships, _data_FX_MODE_2DSPACESHIPS); addEffect(FX_MODE_2DCRAZYBEES, &mode_2Dcrazybees, _data_FX_MODE_2DCRAZYBEES); - #ifdef ENABLE_2D_PS_REPLACEMENTS + #ifdef WLED_PS_DONT_REPLACE_FX addEffect(FX_MODE_2DGHOSTRIDER, &mode_2Dghostrider, _data_FX_MODE_2DGHOSTRIDER); addEffect(FX_MODE_2DBLOBS, &mode_2Dfloatingblobs, _data_FX_MODE_2DBLOBS); #endif @@ -10432,7 +10357,6 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_PARTICLECENTERGEQ, &mode_particlecenterGEQ, _data_FX_MODE_PARTICLECIRCULARGEQ); addEffect(FX_MODE_PARTICLEGHOSTRIDER, &mode_particleghostrider, _data_FX_MODE_PARTICLEGHOSTRIDER); addEffect(FX_MODE_PARTICLEBLOBS, &mode_particleblobs, _data_FX_MODE_PARTICLEBLOBS); - // addEffect(FX_MODE_PSFRACTAL, &mode_particlefractal, _data_FX_MODE_PARTICLEFRACTAL); #endif // WLED_DISABLE_PARTICLESYSTEM2D #endif // WLED_DISABLE_2D From eeacc61a8ca0a0c0959fc137f9aafdb6155f9196 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sat, 3 Feb 2024 10:45:34 +0100 Subject: [PATCH 189/219] Adding Particle System and PS FX Adding all 187 commits from particle system dev branch --- platformio.ini | 17 +- wled00/FX.cpp | 2959 ++++++++++++++++++++++++++++++++--- wled00/FX.h | 42 +- wled00/FX_fcn.cpp | 22 +- wled00/FXparticleSystem.cpp | 2334 +++++++++++++++++++++++++++ wled00/FXparticleSystem.h | 414 +++++ wled00/cfg.cpp | 3 +- wled00/fcn_declare.h | 1 - 8 files changed, 5547 insertions(+), 245 deletions(-) create mode 100644 wled00/FXparticleSystem.cpp create mode 100644 wled00/FXparticleSystem.h diff --git a/platformio.ini b/platformio.ini index 5e0c01db79..f0b70a3a95 100644 --- a/platformio.ini +++ b/platformio.ini @@ -15,8 +15,7 @@ default_envs = nodemcuv2, esp8266_2m, esp01_1m_full, nodemcuv2_160, esp8266_2m_1 src_dir = ./wled00 data_dir = ./wled00/data build_cache_dir = ~/.buildcache -extra_configs = - platformio_override.ini +extra_configs = platformio_override.ini [common] # ------------------------------------------------------------------------------ @@ -382,6 +381,8 @@ platform_packages = ${common.platform_packages} board_build.ldscript = ${common.ldscript_2m512k} build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=\"ESP02\" + -D WLED_DISABLE_PARTICLESYSTEM2D + -D WLED_DISABLE_PARTICLESYSTEM1D lib_deps = ${esp8266.lib_deps} [env:esp8266_2m_compat] @@ -390,12 +391,16 @@ extends = env:esp8266_2m platform = ${esp8266.platform_compat} platform_packages = ${esp8266.platform_packages_compat} build_flags = ${common.build_flags} ${esp8266.build_flags_compat} -D WLED_RELEASE_NAME=\"ESP02_compat\" #-DWLED_DISABLE_2D + -D WLED_DISABLE_PARTICLESYSTEM1D + -D WLED_DISABLE_PARTICLESYSTEM2D [env:esp8266_2m_160] extends = env:esp8266_2m board_build.f_cpu = 160000000L build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=\"ESP02_160\" -D USERMOD_AUDIOREACTIVE + -D WLED_DISABLE_PARTICLESYSTEM1D + -D WLED_DISABLE_PARTICLESYSTEM2D [env:esp01_1m_full] board = esp01_1m @@ -405,6 +410,8 @@ board_build.ldscript = ${common.ldscript_1m128k} build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=\"ESP01\" -D WLED_DISABLE_OTA ; -D WLED_USE_REAL_MATH ;; may fix wrong sunset/sunrise times, at the cost of 7064 bytes FLASH and 975 bytes RAM + -D WLED_DISABLE_PARTICLESYSTEM1D + -D WLED_DISABLE_PARTICLESYSTEM2D lib_deps = ${esp8266.lib_deps} [env:esp01_1m_full_compat] @@ -413,6 +420,8 @@ extends = env:esp01_1m_full platform = ${esp8266.platform_compat} platform_packages = ${esp8266.platform_packages_compat} build_flags = ${common.build_flags} ${esp8266.build_flags_compat} -D WLED_RELEASE_NAME=\"ESP01_compat\" -D WLED_DISABLE_OTA #-DWLED_DISABLE_2D + -D WLED_DISABLE_PARTICLESYSTEM1D + -D WLED_DISABLE_PARTICLESYSTEM2D [env:esp01_1m_full_160] extends = env:esp01_1m_full @@ -420,6 +429,8 @@ board_build.f_cpu = 160000000L build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=\"ESP01_160\" -D WLED_DISABLE_OTA -D USERMOD_AUDIOREACTIVE ; -D WLED_USE_REAL_MATH ;; may fix wrong sunset/sunrise times, at the cost of 7064 bytes FLASH and 975 bytes RAM + -D WLED_DISABLE_PARTICLESYSTEM1D + -D WLED_DISABLE_PARTICLESYSTEM2D [env:esp32dev] board = esp32dev @@ -528,7 +539,7 @@ build_flags = ${common.build_flags} ${esp32c3.build_flags} -D WLED_RELEASE_NAME= -DLOLIN_WIFI_FIX ; seems to work much better with this -DARDUINO_USB_CDC_ON_BOOT=1 ;; for virtual CDC USB ;-DARDUINO_USB_CDC_ON_BOOT=0 ;; for serial-to-USB chip -upload_speed = 460800 +upload_speed = 460800 ; 115200 230400 460800 build_unflags = ${common.build_unflags} lib_deps = ${esp32c3.lib_deps} diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 2655d7daab..e5fca37b4f 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -10,10 +10,17 @@ Modified heavily for WLED */ +// information for custom FX metadata strings: https://kno.wled.ge/interfaces/json-api/#effect-metadata + #include "wled.h" #include "FX.h" #include "fcn_declare.h" +#if !(defined(WLED_DISABLE_PARTICLESYSTEM2D) && defined(WLED_DISABLE_PARTICLESYSTEM1D)) +#include "FXparticleSystem.h" +#else +#define WLED_PS_DONT_REPLACE_FX +#endif ////////////// // DEV INFO // @@ -29,7 +36,7 @@ uint8_t *fftResult = nullptr; float *fftBin = nullptr; um_data_t *um_data; - if (usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { + if (UsermodManager::getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { volumeSmth = *(float*) um_data->u_data[0]; volumeRaw = *(float*) um_data->u_data[1]; fftResult = (uint8_t*) um_data->u_data[2]; @@ -1215,7 +1222,7 @@ uint16_t mode_dual_larson_scanner(void){ } static const char _data_FX_MODE_DUAL_LARSON_SCANNER[] PROGMEM = "Scanner Dual@!,Trail,Delay,,,Dual,Bi-delay;!,!,!;!;;m12=0,c1=0"; - +#ifdef WLED_PS_DONT_REPLACE_FX /* * Firing comets from one end. "Lighthouse" */ @@ -1242,7 +1249,7 @@ uint16_t mode_comet(void) { return FRAMETIME; } static const char _data_FX_MODE_COMET[] PROGMEM = "Lighthouse@!,Fade rate;!,!;!"; - +#endif // WLED_PS_DONT_REPLACE_FX /* * Fireworks function. @@ -1287,7 +1294,6 @@ uint16_t mode_fireworks() { } static const char _data_FX_MODE_FIREWORKS[] PROGMEM = "Fireworks@,Frequency;!,!;!;12;ix=192,pal=11"; - //Twinkling LEDs running. Inspired by https://github.com/kitesurfer1404/WS2812FX/blob/master/src/custom/Rain.h uint16_t mode_rain() { if (SEGLEN <= 1) return mode_static(); @@ -1322,7 +1328,6 @@ uint16_t mode_rain() { } static const char _data_FX_MODE_RAIN[] PROGMEM = "Rain@!,Spawning rate;!,!;!;12;ix=128,pal=0"; - /* * Fire flicker function */ @@ -2077,7 +2082,7 @@ uint16_t mode_palette() { } static const char _data_FX_MODE_PALETTE[] PROGMEM = "Palette@Shift,Size,Rotation,,,Animate Shift,Animate Rotation,Anamorphic;;!;12;ix=112,c1=0,o1=1,o2=0,o3=1"; - +#ifdef WLED_PS_DONT_REPLACE_FX // WLED limitation: Analog Clock overlay will NOT work when Fire2012 is active // Fire2012 by Mark Kriegsman, July 2012 // as part of "Five Elements" shown here: http://youtu.be/knWiGsmgycY @@ -2164,6 +2169,7 @@ uint16_t mode_fire_2012() { return FRAMETIME; } static const char _data_FX_MODE_FIRE_2012[] PROGMEM = "Fire 2012@Cooling,Spark rate,,2D Blur,Boost;;!;1;pal=35,sx=64,ix=160,m12=1,c2=128"; // bars +#endif // WLED_PS_DONT_REPLACE_FX // colored stripes pulsing at a defined Beats-Per-Minute (BPM) uint16_t mode_bpm() { @@ -2927,7 +2933,6 @@ uint16_t mode_spots_fade() } static const char _data_FX_MODE_SPOTS_FADE[] PROGMEM = "Spots Fade@Spread,Width,,,,,Overlay;!,!;!"; - //each needs 12 bytes typedef struct Ball { unsigned long lastBounceTime; @@ -2938,6 +2943,7 @@ typedef struct Ball { /* * Bouncing Balls Effect */ + uint16_t mode_bouncing_balls(void) { if (SEGLEN <= 1) return mode_static(); //allocate segment data @@ -3011,12 +3017,13 @@ uint16_t mode_bouncing_balls(void) { } static const char _data_FX_MODE_BOUNCINGBALLS[] PROGMEM = "Bouncing Balls@Gravity,# of balls,,,,,Overlay;!,!,!;!;1;m12=1"; //bar - +#ifdef WLED_PS_DONT_REPLACE_FX /* * bouncing balls on a track track Effect modified from Aircoookie's bouncing balls * Courtesy of pjhatch (https://github.com/pjhatch) * https://github.com/Aircoookie/WLED/pull/1039 */ + // modified for balltrack mode typedef struct RollingBall { unsigned long lastBounceUpdate; @@ -3110,8 +3117,8 @@ static uint16_t rolling_balls(void) { return FRAMETIME; } -static const char _data_FX_MODE_ROLLINGBALLS[] PROGMEM = "Rolling Balls@!,# of balls,,,,Collisions,Overlay,Trails;!,!,!;!;1;m12=1"; //bar - +static const char _data_FX_MODE_ROLLINGBALLS[] PROGMEM = "Rolling Balls@!,# of balls,,,,Collide,Overlay,Trails;!,!,!;!;1;m12=1"; //bar +#endif // WLED_PS_DONT_REPLACE_FX /* * Sinelon stolen from FASTLED examples @@ -3206,7 +3213,6 @@ uint16_t mode_solid_glitter() } static const char _data_FX_MODE_SOLID_GLITTER[] PROGMEM = "Solid Glitter@,!;Bg,,Glitter color;;;m12=0"; - //each needs 20 bytes //Spark type is used for popcorn, 1D fireworks, and drip typedef struct Spark { @@ -3221,6 +3227,7 @@ typedef struct Spark { * POPCORN * modified from https://github.com/kitesurfer1404/WS2812FX/blob/master/src/custom/Popcorn.h */ + uint16_t mode_popcorn(void) { if (SEGLEN <= 1) return mode_static(); //allocate segment data @@ -3282,7 +3289,6 @@ uint16_t mode_popcorn(void) { } static const char _data_FX_MODE_POPCORN[] PROGMEM = "Popcorn@!,!,,,,,Overlay;!,!,!;!;;m12=1"; //bar - //values close to 100 produce 5Hz flicker, which looks very candle-y //Inspired by https://github.com/avanhanegem/ArduinoCandleEffectNeoPixel //and https://cpldcpu.wordpress.com/2016/01/05/reverse-engineering-a-real-candle/ @@ -3375,7 +3381,7 @@ uint16_t mode_candle_multi() } static const char _data_FX_MODE_CANDLE_MULTI[] PROGMEM = "Candle Multi@!,!;!,!;!;;sx=96,ix=224,pal=0"; - +#ifdef WLED_PS_DONT_REPLACE_FX /* / Fireworks in starburst effect / based on the video: https://www.reddit.com/r/arduino/comments/c3sd46/i_made_this_fireworks_effect_for_my_led_strips/ @@ -3507,8 +3513,9 @@ uint16_t mode_starburst(void) { } #undef STARBURST_MAX_FRAG static const char _data_FX_MODE_STARBURST[] PROGMEM = "Fireworks Starburst@Chance,Fragments,,,,,Overlay;,!;!;;pal=11,m12=0"; +#endif // WLED_PS_DONT_REPLACE_FX - + #ifdef WLED_PS_DONT_REPLACE_FX /* * Exploding fireworks effect * adapted from: http://www.anirama.com/1000leds/1d-fireworks/ @@ -3645,8 +3652,8 @@ uint16_t mode_exploding_fireworks(void) return FRAMETIME; } #undef MAX_SPARKS -static const char _data_FX_MODE_EXPLODING_FIREWORKS[] PROGMEM = "Fireworks 1D@Gravity,Firing side,,,,,,Blur;!,!;!;12;pal=11,ix=128"; - +static const char _data_FX_MODE_EXPLODING_FIREWORKS[] PROGMEM = "Fireworks 1D@Gravity,Firing side;!,!;!;12;pal=11,ix=128"; +#endif // WLED_PS_DONT_REPLACE_FX /* * Drip Effect @@ -3734,7 +3741,6 @@ uint16_t mode_drip(void) } static const char _data_FX_MODE_DRIP[] PROGMEM = "Drip@Gravity,# of drips,,,,,Overlay;!,!;!;;m12=1"; //bar - /* * Tetris or Stacking (falling bricks) Effect * by Blaz Kristan (AKA blazoncek) (https://github.com/blazoncek, https://blaz.at/home) @@ -4282,17 +4288,6 @@ uint16_t mode_chunchun(void) } static const char _data_FX_MODE_CHUNCHUN[] PROGMEM = "Chunchun@!,Gap size;!,!;!"; - -//13 bytes -typedef struct Spotlight { - float speed; - uint8_t colorIdx; - int16_t position; - unsigned long lastUpdateTime; - uint8_t width; - uint8_t type; -} spotlight; - #define SPOT_TYPE_SOLID 0 #define SPOT_TYPE_GRADIENT 1 #define SPOT_TYPE_2X_GRADIENT 2 @@ -4306,6 +4301,17 @@ typedef struct Spotlight { #define SPOT_MAX_COUNT 49 //Number of simultaneous waves #endif +#ifdef WLED_PS_DONT_REPLACE_FX +//13 bytes +typedef struct Spotlight { + float speed; + uint8_t colorIdx; + int16_t position; + unsigned long lastUpdateTime; + uint8_t width; + uint8_t type; +} spotlight; + /* * Spotlights moving back and forth that cast dancing shadows. * Shine this through tree branches/leaves or other close-up objects that cast @@ -4313,6 +4319,7 @@ typedef struct Spotlight { * * By Steve Pomeroy @xxv */ + uint16_t mode_dancing_shadows(void) { if (SEGLEN <= 1) return mode_static(); @@ -4429,7 +4436,7 @@ uint16_t mode_dancing_shadows(void) return FRAMETIME; } static const char _data_FX_MODE_DANCING_SHADOWS[] PROGMEM = "Dancing Shadows@!,# of shadows;!;!"; - +#endif // WLED_PS_DONT_REPLACE_FX /* Imitates a washing machine, rotating same waves forward, then pause, then backward. @@ -5858,11 +5865,12 @@ uint16_t mode_2Dcrazybees(void) { static const char _data_FX_MODE_2DCRAZYBEES[] PROGMEM = "Crazy Bees@!,Blur,,,,Smear;;!;2;pal=11,ix=0"; #undef MAX_BEES - +#ifdef WLED_PS_DONT_REPLACE_FX ///////////////////////// // 2D Ghost Rider // ///////////////////////// //// Ghost Rider by stepko (c)2021 [https://editor.soulmatelights.com/gallery/716-ghost-rider], adapted by Blaz Kristan (AKA blazoncek) + #define LIGHTERS_AM 64 // max lighters (adequate for 32x32 matrix) uint16_t mode_2Dghostrider(void) { if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up @@ -5948,10 +5956,12 @@ uint16_t mode_2Dghostrider(void) { static const char _data_FX_MODE_2DGHOSTRIDER[] PROGMEM = "Ghost Rider@Fade rate,Blur;;!;2"; #undef LIGHTERS_AM + //////////////////////////// // 2D Floating Blobs // //////////////////////////// //// Floating Blobs by stepko (c)2021 [https://editor.soulmatelights.com/gallery/573-blobs], adapted by Blaz Kristan (AKA blazoncek) + #define MAX_BLOBS 8 uint16_t mode_2Dfloatingblobs(void) { if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up @@ -6046,7 +6056,7 @@ uint16_t mode_2Dfloatingblobs(void) { } static const char _data_FX_MODE_2DBLOBS[] PROGMEM = "Blobs@!,# blobs,Blur,Trail;!;!;2;c1=8"; #undef MAX_BLOBS - +#endif // WLED_PS_DONT_REPLACE_FX //////////////////////////// // 2D Scrolling text // @@ -7476,9 +7486,9 @@ uint16_t mode_2Dsoap() { } } // init also if dimensions changed - if (SEGENV.call == 0 || SEGMENT.aux0 != cols || SEGMENT.aux1 != rows) { - SEGMENT.aux0 = cols; - SEGMENT.aux1 = rows; + if (SEGENV.call == 0 || SEGENV.aux0 != cols || SEGENV.aux1 != rows) { + SEGENV.aux0 = cols; + SEGENV.aux1 = rows; for (int i = 0; i < cols; i++) { for (int j = 0; j < rows; j++) { SEGMENT.setPixelColorXY(i, j, ColorFromPalette(SEGPALETTE,~noise3d[XY(i,j)]*3)); @@ -7629,218 +7639,2678 @@ uint16_t mode_2Dwavingcell() { } static const char _data_FX_MODE_2DWAVINGCELL[] PROGMEM = "Waving Cell@!,Blur,Amplitude 1,Amplitude 2,Amplitude 3,,Flow;;!;2;ix=0"; +#ifndef WLED_DISABLE_PARTICLESYSTEM2D -#endif // WLED_DISABLE_2D - +/* + * Particle System Vortex + * Particles sprayed from center with a rotating spray + * Uses palette for particle color + * by DedeHai (Damian Schneider) + */ +#define NUMBEROFSOURCES 8 +uint16_t mode_particlevortex(void) { + if (SEGLEN == 1) + return mode_static(); + ParticleSystem2D *PartSys = NULL; + uint32_t i, j; + + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem2D(PartSys, NUMBEROFSOURCES)) + return mode_static(); // allocation failed + #ifdef ESP8266 + PartSys->setMotionBlur(180); + #else + PartSys->setMotionBlur(130); + #endif + for (i = 0; i < min(PartSys->numSources, (uint32_t)NUMBEROFSOURCES); i++) { + PartSys->sources[i].source.x = (PartSys->maxX + 1) >> 1; // center + PartSys->sources[i].source.y = (PartSys->maxY + 1) >> 1; // center + PartSys->sources[i].maxLife = 900; + PartSys->sources[i].minLife = 800; + } + PartSys->setKillOutOfBounds(true); + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS -////////////////////////////////////////////////////////////////////////////////////////// -// mode data -static const char _data_RESERVED[] PROGMEM = "RSVD"; + if (PartSys == NULL) + return mode_static(); // something went wrong, no data! -// add (or replace reserved) effect mode and data into vector -// use id==255 to find unallocated gaps (with "Reserved" data string) -// if vector size() is smaller than id (single) data is appended at the end (regardless of id) -// return the actual id used for the effect or 255 if the add failed. -uint8_t WS2812FX::addEffect(uint8_t id, mode_ptr mode_fn, const char *mode_name) { - if (id == 255) { // find empty slot - for (size_t i=1; i<_mode.size(); i++) if (_modeData[i] == _data_RESERVED) { id = i; break; } - } - if (id < _mode.size()) { - if (_modeData[id] != _data_RESERVED) return 255; // do not overwrite an already added effect - _mode[id] = mode_fn; - _modeData[id] = mode_name; - return id; - } else if(_mode.size() < 255) { // 255 is reserved for indicating the effect wasn't added - _mode.push_back(mode_fn); - _modeData.push_back(mode_name); - if (_modeCount < _mode.size()) _modeCount++; - return _mode.size() - 1; - } else { - return 255; // The vector is full so return 255 + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + uint32_t spraycount = min(PartSys->numSources, (uint32_t)(1 + (SEGMENT.custom1 >> 5))); // number of sprays to display, 1-8 + #ifdef ESP8266 + for (i = 1; i < 4; i++) { // need static particles in the center to reduce blinking (would be black every other frame without this hack), just set them there fixed + int partindex = (int)PartSys->usedParticles - (int)i; + if(partindex >= 0) { + PartSys->particles[partindex].x = (PartSys->maxX + 1) >> 1; // center + PartSys->particles[partindex].y = (PartSys->maxY + 1) >> 1; // center + PartSys->particles[partindex].sat = 230; + PartSys->particles[partindex].ttl = 256; //keep alive + } } -} + #endif -void WS2812FX::setupEffectData() { - // Solid must be first! (assuming vector is empty upon call to setup) - _mode.push_back(&mode_static); - _modeData.push_back(_data_FX_MODE_STATIC); - // fill reserved word in case there will be any gaps in the array - for (size_t i=1; i<_modeCount; i++) { - _mode.push_back(&mode_static); - _modeData.push_back(_data_RESERVED); + if (SEGMENT.check1) + PartSys->setSmearBlur(90); // enable smear blur + else + PartSys->setSmearBlur(0); // disable smear blur + + // update colors of the sprays + for (i = 0; i < spraycount; i++) { + uint32_t coloroffset = 0xFF / spraycount; + PartSys->sources[i].source.hue = coloroffset * i; } - // now replace all pre-allocated effects - // --- 1D non-audio effects --- - addEffect(FX_MODE_BLINK, &mode_blink, _data_FX_MODE_BLINK); - addEffect(FX_MODE_BREATH, &mode_breath, _data_FX_MODE_BREATH); - addEffect(FX_MODE_COLOR_WIPE, &mode_color_wipe, _data_FX_MODE_COLOR_WIPE); - addEffect(FX_MODE_COLOR_WIPE_RANDOM, &mode_color_wipe_random, _data_FX_MODE_COLOR_WIPE_RANDOM); - addEffect(FX_MODE_RANDOM_COLOR, &mode_random_color, _data_FX_MODE_RANDOM_COLOR); - addEffect(FX_MODE_COLOR_SWEEP, &mode_color_sweep, _data_FX_MODE_COLOR_SWEEP); - addEffect(FX_MODE_DYNAMIC, &mode_dynamic, _data_FX_MODE_DYNAMIC); - addEffect(FX_MODE_RAINBOW, &mode_rainbow, _data_FX_MODE_RAINBOW); - addEffect(FX_MODE_RAINBOW_CYCLE, &mode_rainbow_cycle, _data_FX_MODE_RAINBOW_CYCLE); - addEffect(FX_MODE_SCAN, &mode_scan, _data_FX_MODE_SCAN); - addEffect(FX_MODE_DUAL_SCAN, &mode_dual_scan, _data_FX_MODE_DUAL_SCAN); - addEffect(FX_MODE_FADE, &mode_fade, _data_FX_MODE_FADE); - addEffect(FX_MODE_THEATER_CHASE, &mode_theater_chase, _data_FX_MODE_THEATER_CHASE); - addEffect(FX_MODE_THEATER_CHASE_RAINBOW, &mode_theater_chase_rainbow, _data_FX_MODE_THEATER_CHASE_RAINBOW); - addEffect(FX_MODE_RUNNING_LIGHTS, &mode_running_lights, _data_FX_MODE_RUNNING_LIGHTS); - addEffect(FX_MODE_SAW, &mode_saw, _data_FX_MODE_SAW); - addEffect(FX_MODE_TWINKLE, &mode_twinkle, _data_FX_MODE_TWINKLE); - addEffect(FX_MODE_DISSOLVE, &mode_dissolve, _data_FX_MODE_DISSOLVE); - addEffect(FX_MODE_DISSOLVE_RANDOM, &mode_dissolve_random, _data_FX_MODE_DISSOLVE_RANDOM); - addEffect(FX_MODE_SPARKLE, &mode_sparkle, _data_FX_MODE_SPARKLE); - addEffect(FX_MODE_FLASH_SPARKLE, &mode_flash_sparkle, _data_FX_MODE_FLASH_SPARKLE); - addEffect(FX_MODE_HYPER_SPARKLE, &mode_hyper_sparkle, _data_FX_MODE_HYPER_SPARKLE); - addEffect(FX_MODE_STROBE, &mode_strobe, _data_FX_MODE_STROBE); - addEffect(FX_MODE_STROBE_RAINBOW, &mode_strobe_rainbow, _data_FX_MODE_STROBE_RAINBOW); - addEffect(FX_MODE_MULTI_STROBE, &mode_multi_strobe, _data_FX_MODE_MULTI_STROBE); - addEffect(FX_MODE_BLINK_RAINBOW, &mode_blink_rainbow, _data_FX_MODE_BLINK_RAINBOW); - addEffect(FX_MODE_ANDROID, &mode_android, _data_FX_MODE_ANDROID); - addEffect(FX_MODE_CHASE_COLOR, &mode_chase_color, _data_FX_MODE_CHASE_COLOR); - addEffect(FX_MODE_CHASE_RANDOM, &mode_chase_random, _data_FX_MODE_CHASE_RANDOM); - addEffect(FX_MODE_CHASE_RAINBOW, &mode_chase_rainbow, _data_FX_MODE_CHASE_RAINBOW); - addEffect(FX_MODE_CHASE_FLASH, &mode_chase_flash, _data_FX_MODE_CHASE_FLASH); - addEffect(FX_MODE_CHASE_FLASH_RANDOM, &mode_chase_flash_random, _data_FX_MODE_CHASE_FLASH_RANDOM); - addEffect(FX_MODE_CHASE_RAINBOW_WHITE, &mode_chase_rainbow_white, _data_FX_MODE_CHASE_RAINBOW_WHITE); - addEffect(FX_MODE_COLORFUL, &mode_colorful, _data_FX_MODE_COLORFUL); - addEffect(FX_MODE_TRAFFIC_LIGHT, &mode_traffic_light, _data_FX_MODE_TRAFFIC_LIGHT); - addEffect(FX_MODE_COLOR_SWEEP_RANDOM, &mode_color_sweep_random, _data_FX_MODE_COLOR_SWEEP_RANDOM); - addEffect(FX_MODE_RUNNING_COLOR, &mode_running_color, _data_FX_MODE_RUNNING_COLOR); - addEffect(FX_MODE_AURORA, &mode_aurora, _data_FX_MODE_AURORA); - addEffect(FX_MODE_RUNNING_RANDOM, &mode_running_random, _data_FX_MODE_RUNNING_RANDOM); - addEffect(FX_MODE_LARSON_SCANNER, &mode_larson_scanner, _data_FX_MODE_LARSON_SCANNER); - addEffect(FX_MODE_COMET, &mode_comet, _data_FX_MODE_COMET); - addEffect(FX_MODE_FIREWORKS, &mode_fireworks, _data_FX_MODE_FIREWORKS); - addEffect(FX_MODE_RAIN, &mode_rain, _data_FX_MODE_RAIN); - addEffect(FX_MODE_TETRIX, &mode_tetrix, _data_FX_MODE_TETRIX); - addEffect(FX_MODE_FIRE_FLICKER, &mode_fire_flicker, _data_FX_MODE_FIRE_FLICKER); - addEffect(FX_MODE_GRADIENT, &mode_gradient, _data_FX_MODE_GRADIENT); - addEffect(FX_MODE_LOADING, &mode_loading, _data_FX_MODE_LOADING); - addEffect(FX_MODE_ROLLINGBALLS, &rolling_balls, _data_FX_MODE_ROLLINGBALLS); - addEffect(FX_MODE_FAIRY, &mode_fairy, _data_FX_MODE_FAIRY); - addEffect(FX_MODE_TWO_DOTS, &mode_two_dots, _data_FX_MODE_TWO_DOTS); - addEffect(FX_MODE_FAIRYTWINKLE, &mode_fairytwinkle, _data_FX_MODE_FAIRYTWINKLE); - addEffect(FX_MODE_RUNNING_DUAL, &mode_running_dual, _data_FX_MODE_RUNNING_DUAL); + // set rotation direction and speed + // can use direction flag to determine current direction + bool direction = SEGMENT.check2; //no automatic direction change, set it to flag + int32_t currentspeed = (int32_t)SEGENV.step; // make a signed integer out of step - addEffect(FX_MODE_TRICOLOR_CHASE, &mode_tricolor_chase, _data_FX_MODE_TRICOLOR_CHASE); - addEffect(FX_MODE_TRICOLOR_WIPE, &mode_tricolor_wipe, _data_FX_MODE_TRICOLOR_WIPE); - addEffect(FX_MODE_TRICOLOR_FADE, &mode_tricolor_fade, _data_FX_MODE_TRICOLOR_FADE); - addEffect(FX_MODE_LIGHTNING, &mode_lightning, _data_FX_MODE_LIGHTNING); - addEffect(FX_MODE_ICU, &mode_icu, _data_FX_MODE_ICU); - addEffect(FX_MODE_MULTI_COMET, &mode_multi_comet, _data_FX_MODE_MULTI_COMET); - addEffect(FX_MODE_DUAL_LARSON_SCANNER, &mode_dual_larson_scanner, _data_FX_MODE_DUAL_LARSON_SCANNER); - addEffect(FX_MODE_RANDOM_CHASE, &mode_random_chase, _data_FX_MODE_RANDOM_CHASE); - addEffect(FX_MODE_OSCILLATE, &mode_oscillate, _data_FX_MODE_OSCILLATE); - addEffect(FX_MODE_PRIDE_2015, &mode_pride_2015, _data_FX_MODE_PRIDE_2015); - addEffect(FX_MODE_JUGGLE, &mode_juggle, _data_FX_MODE_JUGGLE); - addEffect(FX_MODE_PALETTE, &mode_palette, _data_FX_MODE_PALETTE); - addEffect(FX_MODE_FIRE_2012, &mode_fire_2012, _data_FX_MODE_FIRE_2012); - addEffect(FX_MODE_COLORWAVES, &mode_colorwaves, _data_FX_MODE_COLORWAVES); - addEffect(FX_MODE_BPM, &mode_bpm, _data_FX_MODE_BPM); - addEffect(FX_MODE_FILLNOISE8, &mode_fillnoise8, _data_FX_MODE_FILLNOISE8); - addEffect(FX_MODE_NOISE16_1, &mode_noise16_1, _data_FX_MODE_NOISE16_1); - addEffect(FX_MODE_NOISE16_2, &mode_noise16_2, _data_FX_MODE_NOISE16_2); - addEffect(FX_MODE_NOISE16_3, &mode_noise16_3, _data_FX_MODE_NOISE16_3); - addEffect(FX_MODE_NOISE16_4, &mode_noise16_4, _data_FX_MODE_NOISE16_4); - addEffect(FX_MODE_COLORTWINKLE, &mode_colortwinkle, _data_FX_MODE_COLORTWINKLE); - addEffect(FX_MODE_LAKE, &mode_lake, _data_FX_MODE_LAKE); - addEffect(FX_MODE_METEOR, &mode_meteor, _data_FX_MODE_METEOR); - //addEffect(FX_MODE_METEOR_SMOOTH, &mode_meteor_smooth, _data_FX_MODE_METEOR_SMOOTH); // merged with mode_meteor - addEffect(FX_MODE_RAILWAY, &mode_railway, _data_FX_MODE_RAILWAY); - addEffect(FX_MODE_RIPPLE, &mode_ripple, _data_FX_MODE_RIPPLE); - addEffect(FX_MODE_TWINKLEFOX, &mode_twinklefox, _data_FX_MODE_TWINKLEFOX); - addEffect(FX_MODE_TWINKLECAT, &mode_twinklecat, _data_FX_MODE_TWINKLECAT); - addEffect(FX_MODE_HALLOWEEN_EYES, &mode_halloween_eyes, _data_FX_MODE_HALLOWEEN_EYES); - addEffect(FX_MODE_STATIC_PATTERN, &mode_static_pattern, _data_FX_MODE_STATIC_PATTERN); - addEffect(FX_MODE_TRI_STATIC_PATTERN, &mode_tri_static_pattern, _data_FX_MODE_TRI_STATIC_PATTERN); - addEffect(FX_MODE_SPOTS, &mode_spots, _data_FX_MODE_SPOTS); - addEffect(FX_MODE_SPOTS_FADE, &mode_spots_fade, _data_FX_MODE_SPOTS_FADE); - addEffect(FX_MODE_GLITTER, &mode_glitter, _data_FX_MODE_GLITTER); - addEffect(FX_MODE_CANDLE, &mode_candle, _data_FX_MODE_CANDLE); - addEffect(FX_MODE_STARBURST, &mode_starburst, _data_FX_MODE_STARBURST); - addEffect(FX_MODE_EXPLODING_FIREWORKS, &mode_exploding_fireworks, _data_FX_MODE_EXPLODING_FIREWORKS); - addEffect(FX_MODE_BOUNCINGBALLS, &mode_bouncing_balls, _data_FX_MODE_BOUNCINGBALLS); - addEffect(FX_MODE_SINELON, &mode_sinelon, _data_FX_MODE_SINELON); - addEffect(FX_MODE_SINELON_DUAL, &mode_sinelon_dual, _data_FX_MODE_SINELON_DUAL); - addEffect(FX_MODE_SINELON_RAINBOW, &mode_sinelon_rainbow, _data_FX_MODE_SINELON_RAINBOW); - addEffect(FX_MODE_POPCORN, &mode_popcorn, _data_FX_MODE_POPCORN); - addEffect(FX_MODE_DRIP, &mode_drip, _data_FX_MODE_DRIP); - addEffect(FX_MODE_PLASMA, &mode_plasma, _data_FX_MODE_PLASMA); - addEffect(FX_MODE_PERCENT, &mode_percent, _data_FX_MODE_PERCENT); - addEffect(FX_MODE_RIPPLE_RAINBOW, &mode_ripple_rainbow, _data_FX_MODE_RIPPLE_RAINBOW); - addEffect(FX_MODE_HEARTBEAT, &mode_heartbeat, _data_FX_MODE_HEARTBEAT); - addEffect(FX_MODE_PACIFICA, &mode_pacifica, _data_FX_MODE_PACIFICA); - addEffect(FX_MODE_CANDLE_MULTI, &mode_candle_multi, _data_FX_MODE_CANDLE_MULTI); - addEffect(FX_MODE_SOLID_GLITTER, &mode_solid_glitter, _data_FX_MODE_SOLID_GLITTER); - addEffect(FX_MODE_SUNRISE, &mode_sunrise, _data_FX_MODE_SUNRISE); - addEffect(FX_MODE_PHASED, &mode_phased, _data_FX_MODE_PHASED); - addEffect(FX_MODE_TWINKLEUP, &mode_twinkleup, _data_FX_MODE_TWINKLEUP); - addEffect(FX_MODE_NOISEPAL, &mode_noisepal, _data_FX_MODE_NOISEPAL); - addEffect(FX_MODE_SINEWAVE, &mode_sinewave, _data_FX_MODE_SINEWAVE); - addEffect(FX_MODE_PHASEDNOISE, &mode_phased_noise, _data_FX_MODE_PHASEDNOISE); - addEffect(FX_MODE_FLOW, &mode_flow, _data_FX_MODE_FLOW); - addEffect(FX_MODE_CHUNCHUN, &mode_chunchun, _data_FX_MODE_CHUNCHUN); - addEffect(FX_MODE_DANCING_SHADOWS, &mode_dancing_shadows, _data_FX_MODE_DANCING_SHADOWS); - addEffect(FX_MODE_WASHING_MACHINE, &mode_washing_machine, _data_FX_MODE_WASHING_MACHINE); + if (SEGMENT.custom2 > 0) { // automatic direction change enabled + uint32_t changeinterval = 1040 - ((uint32_t)SEGMENT.custom2 << 2); + direction = SEGENV.aux1 & 0x01; //set direction according to flag - addEffect(FX_MODE_BLENDS, &mode_blends, _data_FX_MODE_BLENDS); - addEffect(FX_MODE_TV_SIMULATOR, &mode_tv_simulator, _data_FX_MODE_TV_SIMULATOR); - addEffect(FX_MODE_DYNAMIC_SMOOTH, &mode_dynamic_smooth, _data_FX_MODE_DYNAMIC_SMOOTH); + if (SEGMENT.check3) // random interval + changeinterval = 20 + changeinterval + hw_random16(changeinterval); - // --- 1D audio effects --- - addEffect(FX_MODE_PIXELS, &mode_pixels, _data_FX_MODE_PIXELS); - addEffect(FX_MODE_PIXELWAVE, &mode_pixelwave, _data_FX_MODE_PIXELWAVE); - addEffect(FX_MODE_JUGGLES, &mode_juggles, _data_FX_MODE_JUGGLES); - addEffect(FX_MODE_MATRIPIX, &mode_matripix, _data_FX_MODE_MATRIPIX); - addEffect(FX_MODE_GRAVIMETER, &mode_gravimeter, _data_FX_MODE_GRAVIMETER); - addEffect(FX_MODE_PLASMOID, &mode_plasmoid, _data_FX_MODE_PLASMOID); - addEffect(FX_MODE_PUDDLES, &mode_puddles, _data_FX_MODE_PUDDLES); - addEffect(FX_MODE_MIDNOISE, &mode_midnoise, _data_FX_MODE_MIDNOISE); - addEffect(FX_MODE_NOISEMETER, &mode_noisemeter, _data_FX_MODE_NOISEMETER); - addEffect(FX_MODE_FREQWAVE, &mode_freqwave, _data_FX_MODE_FREQWAVE); - addEffect(FX_MODE_FREQMATRIX, &mode_freqmatrix, _data_FX_MODE_FREQMATRIX); + if (SEGMENT.call % changeinterval == 0) { //flip direction on next frame + SEGENV.aux1 |= 0x02; // set the update flag (for random interval update) + if (direction) + SEGENV.aux1 &= ~0x01; // clear the direction flag + else + SEGENV.aux1 |= 0x01; // set the direction flag + } + } - addEffect(FX_MODE_WATERFALL, &mode_waterfall, _data_FX_MODE_WATERFALL); - addEffect(FX_MODE_FREQPIXELS, &mode_freqpixels, _data_FX_MODE_FREQPIXELS); + int32_t targetspeed = (direction ? 1 : -1) * (SEGMENT.speed << 3); + int32_t speeddiff = targetspeed - currentspeed; + int32_t speedincrement = speeddiff / 50; - addEffect(FX_MODE_NOISEFIRE, &mode_noisefire, _data_FX_MODE_NOISEFIRE); - addEffect(FX_MODE_PUDDLEPEAK, &mode_puddlepeak, _data_FX_MODE_PUDDLEPEAK); - addEffect(FX_MODE_NOISEMOVE, &mode_noisemove, _data_FX_MODE_NOISEMOVE); + if (speedincrement == 0) { //if speeddiff is not zero, make the increment at least 1 so it reaches target speed + if(speeddiff < 0) + speedincrement = -1; + else if (speeddiff > 0) + speedincrement = 1; + } - addEffect(FX_MODE_PERLINMOVE, &mode_perlinmove, _data_FX_MODE_PERLINMOVE); - addEffect(FX_MODE_RIPPLEPEAK, &mode_ripplepeak, _data_FX_MODE_RIPPLEPEAK); + currentspeed += speedincrement; + SEGENV.aux0 += currentspeed; + SEGENV.step = (uint32_t)currentspeed; //save it back - addEffect(FX_MODE_FREQMAP, &mode_freqmap, _data_FX_MODE_FREQMAP); - addEffect(FX_MODE_GRAVCENTER, &mode_gravcenter, _data_FX_MODE_GRAVCENTER); - addEffect(FX_MODE_GRAVCENTRIC, &mode_gravcentric, _data_FX_MODE_GRAVCENTRIC); - addEffect(FX_MODE_GRAVFREQ, &mode_gravfreq, _data_FX_MODE_GRAVFREQ); - addEffect(FX_MODE_DJLIGHT, &mode_DJLight, _data_FX_MODE_DJLIGHT); + uint16_t angleoffset = 0xFFFF / spraycount; // angle offset for an even distribution + uint32_t skip = PS_P_HALFRADIUS / (SEGMENT.intensity + 1) + 1; // intensity is emit speed, emit less on low speeds + if (SEGMENT.call % skip == 0) { + j = hw_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) + PartSys->sources[j].var = (SEGMENT.custom3 >> 1); //update speed variation + #ifdef ESP8266 + if (SEGMENT.call & 0x01) // every other frame, do not emit to save particles + #endif + PartSys->angleEmit(PartSys->sources[j], SEGENV.aux0 + angleoffset * j, (SEGMENT.intensity >> 2)+1); + j = (j + 1) % spraycount; + } + } + PartSys->update(); //update all particles and render to frame + return FRAMETIME; +} +#undef NUMBEROFSOURCES +static const char _data_FX_MODE_PARTICLEVORTEX[] PROGMEM = "PS Vortex@Rotation Speed,Particle Speed,Arms,Flip,Nozzle,Smear,Direction,Random Flip;;!;2;pal=27,c1=200,c2=0,c3=0"; - addEffect(FX_MODE_BLURZ, &mode_blurz, _data_FX_MODE_BLURZ); +/* + * Particle Fireworks + * Rockets shoot up and explode in a random color, sometimes in a defined pattern + * by DedeHai (Damian Schneider) + */ +#define NUMBEROFSOURCES 8 - addEffect(FX_MODE_FLOWSTRIPE, &mode_FlowStripe, _data_FX_MODE_FLOWSTRIPE); +uint16_t mode_particlefireworks(void) { + ParticleSystem2D *PartSys = NULL; + uint32_t numRockets; - addEffect(FX_MODE_WAVESINS, &mode_wavesins, _data_FX_MODE_WAVESINS); - addEffect(FX_MODE_ROCKTAVES, &mode_rocktaves, _data_FX_MODE_ROCKTAVES); + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem2D(PartSys, NUMBEROFSOURCES)) + return mode_static(); // allocation failed - // --- 2D effects --- -#ifndef WLED_DISABLE_2D - addEffect(FX_MODE_2DPLASMAROTOZOOM, &mode_2Dplasmarotozoom, _data_FX_MODE_2DPLASMAROTOZOOM); - addEffect(FX_MODE_2DSPACESHIPS, &mode_2Dspaceships, _data_FX_MODE_2DSPACESHIPS); - addEffect(FX_MODE_2DCRAZYBEES, &mode_2Dcrazybees, _data_FX_MODE_2DCRAZYBEES); - addEffect(FX_MODE_2DGHOSTRIDER, &mode_2Dghostrider, _data_FX_MODE_2DGHOSTRIDER); - addEffect(FX_MODE_2DBLOBS, &mode_2Dfloatingblobs, _data_FX_MODE_2DBLOBS); - addEffect(FX_MODE_2DSCROLLTEXT, &mode_2Dscrollingtext, _data_FX_MODE_2DSCROLLTEXT); - addEffect(FX_MODE_2DDRIFTROSE, &mode_2Ddriftrose, _data_FX_MODE_2DDRIFTROSE); - addEffect(FX_MODE_2DDISTORTIONWAVES, &mode_2Ddistortionwaves, _data_FX_MODE_2DDISTORTIONWAVES); + PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting) + PartSys->setWallHardness(120); // ground bounce is fixed + numRockets = min(PartSys->numSources, (uint32_t)NUMBEROFSOURCES); + for (uint32_t j = 0; j < numRockets; j++) { + PartSys->sources[j].source.ttl = 500 * j; // first rocket starts immediately, others follow soon + PartSys->sources[j].source.vy = -1; // at negative speed, no particles are emitted and if rocket dies, it will be relaunched + } + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + + if (PartSys == NULL) + return mode_static(); // something went wrong, no data! + + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + numRockets = map(SEGMENT.speed, 0 , 255, 4, min(PartSys->numSources, (uint32_t)NUMBEROFSOURCES)); + + PartSys->setWrapX(SEGMENT.check1); + PartSys->setBounceY(SEGMENT.check2); + PartSys->setGravity(map(SEGMENT.custom3, 0, 31, SEGMENT.check2 ? 1 : 0, 10)); // if bounded, set gravity to minimum of 1 or they will bounce at top + PartSys->setMotionBlur(map(SEGMENT.custom2, 0, 255, 0, 170)); // anable motion blur + uint8_t smearing = 0; + if(SEGMENT.custom2 > 200) + smearing = SEGMENT.custom2 - 200; + PartSys->setSmearBlur(smearing); // enable 2D blurring (smearing) + + // update the rockets, set the speed state + for (uint32_t j = 0; j < numRockets; j++) { + PartSys->applyGravity(PartSys->sources[j].source); + PartSys->particleMoveUpdate(PartSys->sources[j].source, PartSys->sources[j].sourceFlags); + if (PartSys->sources[j].source.ttl == 0) { + if (PartSys->sources[j].source.vy > 0) { // rocket has died and is moving up. stop it so it will explode (is handled in the code below) + PartSys->sources[j].source.vy = 0; + } + else if (PartSys->sources[j].source.vy < 0) { // rocket is exploded and time is up (ttl=0 and negative speed), relaunch it + PartSys->sources[j].source.y = PS_P_RADIUS; // start from bottom + PartSys->sources[j].source.x = (PartSys->maxX >> 2) + hw_random(PartSys->maxX >> 1); // centered half + PartSys->sources[j].source.vy = (SEGMENT.custom3) + random16(SEGMENT.custom1 >> 3) + 5; // rocket speed TODO: need to adjust for segment height + PartSys->sources[j].source.vx = hw_random16(7) - 3; // not perfectly straight up + PartSys->sources[j].source.sat = 30; // low saturation -> exhaust is off-white + PartSys->sources[j].source.ttl = hw_random16(SEGMENT.custom1) + (SEGMENT.custom1 >> 1); // set fuse time + PartSys->sources[j].maxLife = 40; // exhaust particle life + PartSys->sources[j].minLife = 10; + PartSys->sources[j].vx = 0; // emitting speed + PartSys->sources[j].vy = -5; // emitting speed + PartSys->sources[j].var = 4; // speed variation around vx,vy (+/- var) + } + } + } + // check each rocket's state and emit particles according to its state: moving up = emit exhaust, at top = explode; falling down = standby time + uint32_t emitparticles, frequency, baseangle, hueincrement; // number of particles to emit for each rocket's state + // variables for circular explosions + [[maybe_unused]] int32_t speed, currentspeed, speedvariation, percircle; + int32_t counter = 0; + [[maybe_unused]] uint16_t angle; + [[maybe_unused]] unsigned angleincrement; + bool circularexplosion = false; + + // emit particles for each rocket + for (uint32_t j = 0; j < numRockets; j++) { + // determine rocket state by its speed: + if (PartSys->sources[j].source.vy > 0) { // moving up, emit exhaust + emitparticles = 1; + } + else if (PartSys->sources[j].source.vy < 0) { // falling down, standby time + emitparticles = 0; + } + else { // speed is zero, explode! + PartSys->sources[j].source.hue = hw_random16(); // random color + PartSys->sources[j].source.sat = hw_random16(55) + 200; + PartSys->sources[j].maxLife = 200; + PartSys->sources[j].minLife = 100; + PartSys->sources[j].source.ttl = hw_random16((2000 - ((uint32_t)SEGMENT.speed << 2))) + 550 - (SEGMENT.speed << 1); // standby time til next launch + PartSys->sources[j].var = ((SEGMENT.intensity >> 4) + 5); // speed variation around vx,vy (+/- var) + PartSys->sources[j].source.vy = -1; // set speed negative so it will emit no more particles after this explosion until relaunch + #ifdef ESP8266 + emitparticles = hw_random16(SEGMENT.intensity >> 3) + (SEGMENT.intensity >> 3) + 5; // defines the size of the explosion + #else + emitparticles = hw_random16(SEGMENT.intensity >> 2) + (SEGMENT.intensity >> 2) + 5; // defines the size of the explosion + #endif + + if (random16() & 1) { // 50% chance for circular explosion + circularexplosion = true; + speed = 2 + hw_random16(3) + ((SEGMENT.intensity >> 6)); + currentspeed = speed; + angleincrement = 2730 + hw_random16(5461); // minimum 15° + random(30°) + angle = hw_random16(); // random start angle + baseangle = angle; // save base angle for modulation + percircle = 0xFFFF / angleincrement + 1; // number of particles to make complete circles + hueincrement = hw_random16() & 127; // &127 is equivalent to %128 + int circles = 1 + hw_random16(3) + ((SEGMENT.intensity >> 6)); + frequency = hw_random16() & 127; // modulation frequency (= "waves per circle"), x.4 fixed point + emitparticles = percircle * circles; + PartSys->sources[j].var = angle & 1; // 0 or 1 variation, angle is random + } + } + uint32_t i = 0; + for (i; i < emitparticles; i++) { + if (circularexplosion) { + int32_t sineMod = 0xEFFF + sin16_t((uint16_t)(((angle * frequency) >> 4) + baseangle)); // shifted to positive values + currentspeed = (speed/2 + ((sineMod * speed) >> 16)) >> 1; // sine modulation on speed based on emit angle + PartSys->angleEmit(PartSys->sources[j], angle, currentspeed); // note: compiler warnings can be ignored, variables are set just above + counter++; + if (counter > percircle) { // full circle completed, increase speed + counter = 0; + speed += 3 + ((SEGMENT.intensity >> 6)); // increase speed to form a second wave + PartSys->sources[j].source.hue += hueincrement; // new color for next circle + PartSys->sources[j].source.sat = min((uint16_t)150, random16()); + } + angle += angleincrement; // set angle for next particle + } + else { // random explosion or exhaust + PartSys->sprayEmit(PartSys->sources[j]); + if ((j % 3) == 0) { + PartSys->sources[j].source.hue = hw_random16(); // random color for each particle (this is also true for exhaust, but that is white anyways) + } + } + } + if (i == 0) // no particles emitted, this rocket is falling + PartSys->sources[j].source.y = 1000; // reset position so gravity wont pull it to the ground and bounce it (vy MUST stay negative until relaunch) + circularexplosion = false; // reset for next rocket + } + if(SEGMENT.check3) { // fast speed, move particles twice + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { + PartSys->particleMoveUpdate(PartSys->particles[i], PartSys->particleFlags[i], nullptr, nullptr); + } + } + PartSys->update(); // update and render + return FRAMETIME; +} +#undef NUMBEROFSOURCES +static const char _data_FX_MODE_PARTICLEFIREWORKS[] PROGMEM = "PS Fireworks@Launches,Explosion Size,Fuse,Blur,Gravity,Cylinder,Ground,Fast;;!;2;pal=11,sx=100,ix=50,c1=40,c2=0,c3=12"; + +/* + * Particle Volcano + * Particles are sprayed from below, spray moves back and forth if option is set + * Uses palette for particle color + * by DedeHai (Damian Schneider) + */ +#define NUMBEROFSOURCES 1 +uint16_t mode_particlevolcano(void) { + ParticleSystem2D *PartSys = NULL; + PSsettings2D volcanosettings; + volcanosettings.asByte = 0b00000100; // PS settings for volcano movement: bounceX is enabled + uint8_t numSprays; // note: so far only one tested but more is possible + uint32_t i = 0; + + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem2D(PartSys, NUMBEROFSOURCES)) // init, no additional data needed + return mode_static(); // allocation failed or not 2D + + PartSys->setBounceY(true); + PartSys->setGravity(); // enable with default gforce + PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting) + PartSys->setMotionBlur(230); // anable motion blur + + numSprays = min(PartSys->numSources, (uint32_t)NUMBEROFSOURCES); // number of sprays + for (i = 0; i < numSprays; i++) { + PartSys->sources[i].source.hue = hw_random16(); + PartSys->sources[i].source.x = PartSys->maxX / (numSprays + 1) * (i + 1); // distribute evenly + PartSys->sources[i].maxLife = 300; // lifetime in frames + PartSys->sources[i].minLife = 250; + PartSys->sources[i].sourceFlags.collide = true; // seeded particles will collide (if enabled) + PartSys->sources[i].sourceFlags.perpetual = true; // source never dies + } + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + + if (PartSys == NULL) + return mode_static(); // something went wrong, no data! + + numSprays = min(PartSys->numSources, (uint32_t)NUMBEROFSOURCES); // number of volcanoes + + // change source emitting color from time to time, emit one particle per spray + if (SEGMENT.call % (11 - (SEGMENT.intensity / 25)) == 0) { // every nth frame, cycle color and emit particles (and update the sources) + for (i = 0; i < numSprays; i++) { + PartSys->sources[i].source.y = PS_P_RADIUS + 5; // reset to just above the lower edge that is allowed for bouncing particles, if zero, particles already 'bounce' at start and loose speed. + PartSys->sources[i].source.vy = 0; //reset speed (so no extra particlesettin is required to keep the source 'afloat') + PartSys->sources[i].source.hue++; // = hw_random16(); //change hue of spray source (note: random does not look good) + PartSys->sources[i].source.vx = PartSys->sources[i].source.vx > 0 ? (SEGMENT.custom1 >> 2) : -(SEGMENT.custom1 >> 2); // set moving speed but keep the direction given by PS + PartSys->sources[i].vy = SEGMENT.speed >> 2; // emitting speed (upwards) + PartSys->sources[i].vx = 0; + PartSys->sources[i].var = SEGMENT.custom3 >> 1; // emiting variation = nozzle size (custom 3 goes from 0-31) + PartSys->sprayEmit(PartSys->sources[i]); + PartSys->setWallHardness(255); // full hardness for source bounce + PartSys->particleMoveUpdate(PartSys->sources[i].source, PartSys->sources[i].sourceFlags, &volcanosettings); //move the source + } + } + + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setColorByAge(SEGMENT.check1); + PartSys->setBounceX(SEGMENT.check2); + PartSys->setWallHardness(SEGMENT.custom2); + + if (SEGMENT.check3) // collisions enabled + PartSys->enableParticleCollisions(true, SEGMENT.custom2); // enable collisions and set particle collision hardness + else + PartSys->enableParticleCollisions(false); + + PartSys->update(); // update and render + return FRAMETIME; +} +#undef NUMBEROFSOURCES +static const char _data_FX_MODE_PARTICLEVOLCANO[] PROGMEM = "PS Volcano@Speed,Intensity,Move,Bounce,Spread,AgeColor,Walls,Collide;;!;2;pal=35,sx=100,ix=190,c1=0,c2=160,c3=6,o1=1"; + +/* + Particle Fire + realistic fire effect using particles. heat based and using perlin-noise for wind + by DedeHai (Damian Schneider) +*/ +uint16_t mode_particlefire(void) { + ParticleSystem2D *PartSys = NULL; + uint32_t i; // index variable + uint32_t numFlames; // number of flames: depends on fire width. for a fire width of 16 pixels, about 25-30 flames give good results + + if (SEGMENT.call == 0) { // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. + if (!initParticleSystem2D(PartSys, SEGMENT.virtualWidth(), 4)) //maximum number of source (PS may limit based on segment size); need 4 additional bytes for time keeping (uint32_t lastcall) + return mode_static(); // allocation failed or not 2D + SEGENV.aux0 = hw_random16(); // aux0 is wind position (index) in the perlin noise + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + + if (PartSys == NULL) + return mode_static(); // something went wrong, no data! + + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setWrapX(SEGMENT.check2); + PartSys->setMotionBlur(SEGMENT.check1 * 170); // anable/disable motion blur + + uint32_t firespeed = max((uint8_t)100, SEGMENT.speed); //limit speed to 100 minimum, reduce frame rate to make it slower (slower speeds than 100 do not look nice) + if (SEGMENT.speed < 100) { //slow, limit FPS + uint32_t *lastcall = reinterpret_cast(PartSys->PSdataEnd); + uint32_t period = strip.now - *lastcall; + if (period < (uint32_t)map(SEGMENT.speed, 0, 99, 50, 10)) { // limit to 90FPS - 20FPS + SEGMENT.call--; //skipping a frame, decrement the counter (on call0, this is never executed as lastcall is 0, so its fine to not check if >0) + //still need to render the frame or flickering will occur in transitions + PartSys->updateFire(SEGMENT.intensity, true); // render the fire without updating particles (render only) + return FRAMETIME; //do not update this frame + } + *lastcall = strip.now; + } + + uint32_t spread = (PartSys->maxX >> 5) * (SEGMENT.custom3 + 1); //fire around segment center (in subpixel points) + numFlames = min((uint32_t)PartSys->numSources, (4 + ((spread / PS_P_RADIUS) << 1))); // number of flames used depends on spread with, good value is (fire width in pixel) * 2 + uint32_t percycle = (numFlames * 2) / 3; // maximum number of particles emitted per cycle (TODO: for ESP826 maybe use flames/2) + + // update the flame sprays: + for (i = 0; i < numFlames; i++) { + if (SEGMENT.call & 1 && PartSys->sources[i].source.ttl > 0) { // every second frame + PartSys->sources[i].source.ttl--; + } else { // flame source is dead: initialize new flame: set properties of source + PartSys->sources[i].source.x = (PartSys->maxX >> 1) - (spread >> 1) + hw_random(spread); // change flame position: distribute randomly on chosen width + PartSys->sources[i].source.y = -(PS_P_RADIUS << 2); // set the source below the frame + PartSys->sources[i].source.ttl = 20 + hw_random16((SEGMENT.custom1 * SEGMENT.custom1) >> 8) / (1 + (firespeed >> 5)); //'hotness' of fire, faster flames reduce the effect or flame height will scale too much with speed + PartSys->sources[i].maxLife = hw_random16(SEGMENT.virtualHeight() >> 1) + 16; // defines flame height together with the vy speed, vy speed*maxlife/PS_P_RADIUS is the average flame height + PartSys->sources[i].minLife = PartSys->sources[i].maxLife >> 1; + PartSys->sources[i].vx = hw_random16(4) - 2; // emitting speed (sideways) + PartSys->sources[i].vy = (SEGMENT.virtualHeight() >> 1) + (firespeed >> 4) + (SEGMENT.custom1 >> 4); // emitting speed (upwards) + PartSys->sources[i].var = 2 + hw_random16(2 + (firespeed >> 4)); // speed variation around vx,vy (+/- var) + } + } + + if (SEGMENT.call % 3 == 0) { // update noise position and add wind + SEGENV.aux0++; // position in the perlin noise matrix for wind generation + if (SEGMENT.call % 10 == 0) + SEGENV.aux1++; // move in noise y direction so noise does not repeat as often + // add wind force to all particles + int8_t windspeed = ((int16_t)(inoise8(SEGENV.aux0, SEGENV.aux1) - 127) * SEGMENT.custom2) >> 7; + PartSys->applyForce(windspeed, 0); + } + SEGENV.step++; + + if (SEGMENT.check3) { //add turbulance (parameters and algorithm found by experimentation) + if (SEGMENT.call % map(firespeed, 0, 255, 4, 15) == 0) { + for (i = 0; i < PartSys->usedParticles; i++) { + if (PartSys->particles[i].y < PartSys->maxY / 4) { // do not apply turbulance everywhere -> bottom quarter seems a good balance + int32_t curl = ((int32_t)inoise8(PartSys->particles[i].x, PartSys->particles[i].y, SEGENV.step << 4) - 127); + PartSys->particles[i].vx += (curl * (firespeed + 10)) >> 9; + } + } + } + } + + uint8_t j = hw_random16(); // start with a random flame (so each flame gets the chance to emit a particle if available particles is smaller than number of flames) + for (i = 0; i < percycle; i++) { + j = (j + 1) % numFlames; + PartSys->flameEmit(PartSys->sources[j]); + } + + PartSys->updateFire(SEGMENT.intensity, false); // update and render the fire + + return FRAMETIME; +} +static const char _data_FX_MODE_PARTICLEFIRE[] PROGMEM = "PS Fire@Speed,Intensity,Flame Height,Wind,Spread,Smooth,Cylinder,Turbulence;;!;2;pal=35,sx=110,c1=110,c2=50,c3=31,o1=1"; + +/* + 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 + 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_particlepit(void) { + ParticleSystem2D *PartSys = NULL; + + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem2D(PartSys, 1, 0, true, false)) // init, request one source (actually dont really need one TODO: test if using zero sources also works) + return mode_static(); // allocation failed or not 2D + PartSys->setKillOutOfBounds(true); + PartSys->setGravity(); // enable with default gravity + PartSys->setUsedParticles(170); // use 75% of available particles + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + if (PartSys == NULL) + return mode_static(); // something went wrong, no data! + + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + + PartSys->setWrapX(SEGMENT.check1); + PartSys->setBounceX(SEGMENT.check2); + PartSys->setBounceY(SEGMENT.check3); + PartSys->setWallHardness(min(SEGMENT.custom2, (uint8_t)150)); // limit to 100 min (if collisions are disabled, still want bouncy) + if (SEGMENT.custom2 > 0) + PartSys->enableParticleCollisions(true, SEGMENT.custom2); // enable collisions and set particle collision hardness + else + PartSys->enableParticleCollisions(false); + + uint32_t i; + if (SEGMENT.call % (128 - (SEGMENT.intensity >> 1)) == 0 && SEGMENT.intensity > 0) { // every nth frame emit particles, stop emitting if set to zero + for (i = 0; i < PartSys->usedParticles; i++) { // emit particles + if (PartSys->particles[i].ttl == 0) { // find a dead particle + // emit particle at random position over the top of the matrix (random16 is not random enough) + PartSys->particles[i].ttl = 1500 - (SEGMENT.speed << 2) + hw_random16(500); // if speed is higher, make them die sooner + PartSys->particles[i].x = hw_random(PartSys->maxX); //random(PartSys->maxX >> 1) + (PartSys->maxX >> 2); + PartSys->particles[i].y = (PartSys->maxY << 1); // particles appear somewhere above the matrix, maximum is double the height + PartSys->particles[i].vx = (int16_t)hw_random16(SEGMENT.speed >> 1) - (SEGMENT.speed >> 2); // side speed is +/- + PartSys->particles[i].vy = map(SEGMENT.speed, 0, 255, -5, -100); // downward speed + PartSys->particles[i].hue = hw_random16(); // set random color + PartSys->particleFlags[i].collide = true; // enable collision for particle + PartSys->particles[i].sat = ((SEGMENT.custom3) << 3) + 7; + // set particle size + if (SEGMENT.custom1 == 255) { + PartSys->setParticleSize(0); // set global size to zero + PartSys->advPartProps[i].size =hw_random16(SEGMENT.custom1); // set each particle to random size + } else { + PartSys->setParticleSize(SEGMENT.custom1); // set global size + PartSys->advPartProps[i].size = 0; // use global size + } + break; // emit only one particle per round + } + } + } + + uint32_t frictioncoefficient = 1 + SEGMENT.check1; //need more friction if wrapX is set, see below note + if (SEGMENT.speed < 50) // for low speeds, apply more friction + frictioncoefficient = 50 - SEGMENT.speed; + + if (SEGMENT.call % 6 == 0)// (3 + max(3, (SEGMENT.speed >> 2))) == 0) // note: if friction is too low, hard particles uncontrollably 'wander' left and right if wrapX is enabled + PartSys->applyFriction(frictioncoefficient); + + PartSys->update(); // update and render + + return FRAMETIME; +} +static const char _data_FX_MODE_PARTICLEPIT[] PROGMEM = "PS Ballpit@Speed,Intensity,Size,Hardness,Saturation,Cylinder,Walls,Ground;;!;2;pal=11,sx=100,ix=220,c1=120,c2=130,c3=31,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) { + ParticleSystem2D *PartSys = NULL; + uint8_t numSprays; + uint32_t i = 0; + + if (SEGMENT.call == 0) { // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. + if (!initParticleSystem2D(PartSys, 12)) // init, request 12 sources, no additional data needed + return mode_static(); // allocation failed or not 2D + + PartSys->setGravity(); // enable with default gforce + PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting) + PartSys->setMotionBlur(190); // anable motion blur + PartSys->setSmearBlur(30); // enable 2D blurring (smearing) + for (i = 0; i < PartSys->numSources; i++) { + PartSys->sources[i].source.hue = i*90; + PartSys->sources[i].sourceFlags.collide = true; // seeded particles will collide + #ifdef ESP8266 + PartSys->sources[i].maxLife = 250; // lifetime in frames (ESP8266 has less particles, make them short lived to keep the water flowing) + PartSys->sources[i].minLife = 100; + #else + PartSys->sources[i].maxLife = 400; // lifetime in frames + PartSys->sources[i].minLife = 150; + #endif + } + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + if (PartSys == NULL) + return mode_static(); // something went wrong, no data! (TODO: ask how to handle this so it always works) + + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setWrapX(SEGMENT.check1); // cylinder + PartSys->setBounceX(SEGMENT.check2); // walls + PartSys->setBounceY(SEGMENT.check3); // ground + PartSys->setWallHardness(SEGMENT.custom2); + numSprays = min((int32_t)PartSys->numSources, max(PartSys->maxXpixel / 6, (int32_t)2)); // number of sprays depends on segment width + if (SEGMENT.custom2 > 0) // collisions enabled + PartSys->enableParticleCollisions(true, SEGMENT.custom2); // enable collisions and set particle collision hardness + else { + PartSys->enableParticleCollisions(false); + PartSys->setWallHardness(120); // set hardness (for ground bounce) to fixed value if not using collisions + } + + for (i = 0; i < numSprays; i++) { + PartSys->sources[i].source.hue += 1 + hw_random16(SEGMENT.custom1>>1); // change hue of spray source + } + + if (SEGMENT.call % (12 - (SEGMENT.intensity >> 5)) == 0 && SEGMENT.intensity > 0) { // every nth frame, emit particles, do not emit if intensity is zero + for (i = 0; i < numSprays; i++) { + PartSys->sources[i].vy = -SEGMENT.speed >> 3; // emitting speed, down + //PartSys->sources[i].source.x = map(SEGMENT.custom3, 0, 31, 0, (PartSys->maxXpixel - numSprays * 2) * PS_P_RADIUS) + i * PS_P_RADIUS * 2; // emitter position + PartSys->sources[i].source.x = map(SEGMENT.custom3, 0, 31, 0, (PartSys->maxXpixel - numSprays) * PS_P_RADIUS) + i * PS_P_RADIUS * 2; // emitter position + PartSys->sources[i].source.y = PartSys->maxY + (PS_P_RADIUS * ((i<<2) + 4)); // source y position, few pixels above the top to increase spreading before entering the matrix + PartSys->sources[i].var = (SEGMENT.custom1 >> 3); // emiting variation 0-32 + PartSys->sprayEmit(PartSys->sources[i]); + } + } + + if (SEGMENT.call % 20 == 0) + PartSys->applyFriction(1); // add just a tiny amount of friction to help smooth things + + PartSys->update(); // update and render + return FRAMETIME; +} +static const char _data_FX_MODE_PARTICLEWATERFALL[] PROGMEM = "PS Waterfall@Speed,Intensity,Variation,Collide,Position,Cylinder,Walls,Ground;;!;2;pal=9,sx=15,ix=200,c1=32,c2=160,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) { + ParticleSystem2D *PartSys = NULL; + uint32_t i; + + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem2D(PartSys, 1)) // init + return mode_static(); // allocation failed or not 2D + PartSys->setBounceX(true); + PartSys->setBounceY(true); + SEGENV.aux0 = hw_random16(); // position in perlin noise + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + + if (PartSys == NULL) + return mode_static(); // something went wrong, no data! + + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + + PartSys->setWallHardness(min(SEGMENT.custom2, (uint8_t)200)); // wall hardness is 200 or more + PartSys->enableParticleCollisions(true, max(2, (int)SEGMENT.custom2)); // enable collisions and set particle collision hardness + PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 5, 153)); // 2% - 60% + // add in new particles if amount has changed + for (i = 0; i < PartSys->usedParticles; i++) { + if(PartSys->particles[i].ttl < 260) { // initialize handed over particles and dead particles + PartSys->particles[i].ttl = 260; // full brigthness + PartSys->particles[i].x = hw_random16(PartSys->maxX); + PartSys->particles[i].y = hw_random16(PartSys->maxY); + PartSys->particles[i].hue = hw_random8(); // make it colorful + PartSys->particleFlags[i].perpetual = true; // never die + PartSys->particleFlags[i].collide = true; // all particles colllide + break; // only spawn one particle per frame for less chaotic transitions + } + } + + if (SEGMENT.call % (((255 - SEGMENT.speed) >> 6) + 1) == 0 && SEGMENT.speed > 0) { // how often the force is applied depends on speed setting + int32_t xgravity; + int32_t ygravity; + int32_t increment = (SEGMENT.speed >> 6) + 1; + + if (SEGMENT.check2) { // washing machine + int speed = tristate_square8(strip.now >> 7, 90, 15) / ((400 - SEGMENT.speed) >> 3); + SEGENV.aux0 += speed; + if(speed == 0) SEGENV.aux0 = 190; //down (= 270°) + } + else + SEGENV.aux0 -= increment; + + if (SEGMENT.check1) { // random, use perlin noise + xgravity = ((int16_t)inoise8(SEGENV.aux0) - 127); + ygravity = ((int16_t)inoise8(SEGENV.aux0 + 10000) - 127); + // scale the gravity force + xgravity = (xgravity * SEGMENT.custom1) / 128; + ygravity = (ygravity * SEGMENT.custom1) / 128; + } + else { // go in a circle + xgravity = ((int32_t)(SEGMENT.custom1) * cos16_t(SEGENV.aux0 << 8)) / 0xFFFF; + ygravity = ((int32_t)(SEGMENT.custom1) * sin16_t(SEGENV.aux0 << 8)) / 0xFFFF; + } + if (SEGMENT.check3) { // sloshing, y force is always downwards + if(ygravity > 0) + ygravity = -ygravity; + } + + PartSys->applyForce(xgravity, ygravity); + } + + if (SEGMENT.call % (32 - SEGMENT.custom3) == 0) + PartSys->applyFriction(2); + + PartSys->update(); // update and render + + return FRAMETIME; +} +static const char _data_FX_MODE_PARTICLEBOX[] PROGMEM = "PS Box@Speed,Particles,Tilt Strength,Hardness,Friction,Random,Washing Machine,Sloshing;;!;2;pal=53,sx=120,ix=100,c1=100,c2=210,o1=1"; + +/* + Fuzzy Noise: Perlin noise 'gravity' mapping as in particles on 'noise hills' viewed from above + calculates slope gradient at the particle positions and applies 'downhill' force, resulting in a fuzzy perlin noise display + by DedeHai (Damian Schneider) +*/ +uint16_t mode_particleperlin(void) { + ParticleSystem2D *PartSys = NULL; + uint32_t i; + + if (SEGMENT.call == 0) { // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. + if (!initParticleSystem2D(PartSys, 1, 0, true)) // init with 1 source and advanced properties + return mode_static(); // allocation failed or not 2D + + PartSys->setKillOutOfBounds(true); // should never happen, but lets make sure there are no stray particles + PartSys->setMotionBlur(230); // anable motion blur + PartSys->setBounceY(true); + SEGENV.aux0 = rand(); + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + + if (PartSys == NULL) + return mode_static(); // something went wrong, no data! + + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setWrapX(SEGMENT.check1); + PartSys->setBounceX(!SEGMENT.check1); + PartSys->setWallHardness(SEGMENT.custom1); // wall hardness + PartSys->enableParticleCollisions(SEGMENT.check3, SEGMENT.custom1); // enable collisions and set particle collision hardness + PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 25, 128)); // min is 10%, max is 50% + PartSys->setSmearBlur(SEGMENT.check2 * 15); // enable 2D blurring (smearing) + + // apply 'gravity' from a 2D perlin noise map + SEGENV.aux0 += 1 + (SEGMENT.speed >> 5); // noise z-position + // update position in noise + for (i = 0; i < PartSys->usedParticles; i++) { + if (PartSys->particles[i].ttl == 0) { // revive dead particles (do not keep them alive forever, they can clump up, need to reseed) + PartSys->particles[i].ttl = hw_random16(500) + 200; + PartSys->particles[i].x = hw_random(PartSys->maxX); + PartSys->particles[i].y = hw_random(PartSys->maxY); + PartSys->particleFlags[i].collide = true; // particle colllides + } + uint32_t scale = 16 - ((31 - SEGMENT.custom3) >> 1); + uint16_t xnoise = PartSys->particles[i].x / scale; // position in perlin noise, scaled by slider + uint16_t ynoise = PartSys->particles[i].y / scale; + int16_t baseheight = inoise8(xnoise, ynoise, SEGENV.aux0); // noise value at particle position + PartSys->particles[i].hue = baseheight; // color particles to perlin noise value + if (SEGMENT.call % 8 == 0) { // do not apply the force every frame, is too chaotic + int8_t xslope = (baseheight + (int16_t)inoise8(xnoise - 10, ynoise, SEGENV.aux0)); + int8_t yslope = (baseheight + (int16_t)inoise8(xnoise, ynoise - 10, SEGENV.aux0)); + PartSys->applyForce(i, xslope, yslope); + } + } + + if (SEGMENT.call % (16 - (SEGMENT.custom2 >> 4)) == 0) + PartSys->applyFriction(2); + + PartSys->update(); // update and render + return FRAMETIME; +} +static const char _data_FX_MODE_PARTICLEPERLIN[] PROGMEM = "PS Fuzzy Noise@Speed,Particles,Bounce,Friction,Scale,Cylinder,Smear,Collide;;!;2;pal=64,sx=50,ix=200,c1=130,c2=30,c3=5,o3=1"; + +/* + Particle smashing down like meteors and exploding as they hit the ground, has many parameters to play with + by DedeHai (Damian Schneider) +*/ +#define NUMBEROFSOURCES 8 +uint16_t mode_particleimpact(void) { + ParticleSystem2D *PartSys = NULL; + uint32_t i = 0; + uint8_t MaxNumMeteors; + PSsettings2D meteorsettings; + meteorsettings.asByte = 0b00101000; // PS settings for meteors: bounceY and gravity enabled + + if (SEGMENT.call == 0) { // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. + if (!initParticleSystem2D(PartSys, NUMBEROFSOURCES)) // init, no additional data needed + return mode_static(); // allocation failed or not 2D + PartSys->setKillOutOfBounds(true); + PartSys->setGravity(); // enable default gravity + PartSys->setBounceY(true); // always use ground bounce + PartSys->setWallRoughness(220); // high roughness + MaxNumMeteors = min(PartSys->numSources, (uint32_t)NUMBEROFSOURCES); + for (i = 0; i < MaxNumMeteors; i++) { + // PartSys->sources[i].source.y = 500; + PartSys->sources[i].source.ttl = hw_random16(10 * i); // set initial delay for meteors + PartSys->sources[i].source.vy = 10; // at positive speeds, no particles are emitted and if particle dies, it will be relaunched + } + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + + if (PartSys == NULL) + return mode_static(); // something went wrong, no data! (TODO: ask how to handle this so it always works) + + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setWrapX(SEGMENT.check1); + PartSys->setBounceX(SEGMENT.check2); + PartSys->setMotionBlur(SEGMENT.custom3<<3); + uint8_t hardness = map(SEGMENT.custom2, 0, 255, 127, 255); + PartSys->setWallHardness(hardness); + PartSys->enableParticleCollisions(SEGMENT.check3, hardness); // enable collisions and set particle collision hardness + MaxNumMeteors = min(PartSys->numSources, (uint32_t)NUMBEROFSOURCES); + uint8_t numMeteors = MaxNumMeteors; // TODO: clean this up map(SEGMENT.custom3, 0, 31, 1, MaxNumMeteors); // number of meteors to use for animation + + uint32_t emitparticles; // number of particles to emit for each rocket's state + + for (i = 0; i < numMeteors; i++) { + // determine meteor state by its speed: + if ( PartSys->sources[i].source.vy < 0) { // moving down, emit sparks + #ifdef ESP8266 + emitparticles = 1; + #else + emitparticles = 2; + #endif + } + else if ( PartSys->sources[i].source.vy > 0) // moving up means meteor is on 'standby' + emitparticles = 0; + else { // speed is zero, explode! + PartSys->sources[i].source.vy = 10; // set source speed positive so it goes into timeout and launches again + #ifdef ESP8266 + emitparticles = hw_random16(SEGMENT.intensity >> 3) + 5; // defines the size of the explosion + #else + emitparticles = map(SEGMENT.intensity, 0, 255, 10, hw_random16(PartSys->usedParticles>>2)); // defines the size of the explosion !!!TODO: check if this works on ESP8266, drop esp8266 def if it does + #endif + } + for (int e = emitparticles; e > 0; e--) { + PartSys->sprayEmit(PartSys->sources[i]); + } + } + + // update the meteors, set the speed state + for (i = 0; i < numMeteors; i++) { + if (PartSys->sources[i].source.ttl) { + PartSys->sources[i].source.ttl--; // note: this saves an if statement, but moving down particles age twice + if (PartSys->sources[i].source.vy < 0) { // move down + PartSys->applyGravity(PartSys->sources[i].source); + PartSys->particleMoveUpdate(PartSys->sources[i].source, PartSys->sources[i].sourceFlags, &meteorsettings); + + // if source reaches the bottom, set speed to 0 so it will explode on next function call (handled above) + if (PartSys->sources[i].source.y < PS_P_RADIUS<<1) { // reached the bottom pixel on its way down + PartSys->sources[i].source.vy = 0; // set speed zero so it will explode + PartSys->sources[i].source.vx = 0; + PartSys->sources[i].sourceFlags.collide = true; + #ifdef ESP8266 + PartSys->sources[i].maxLife = 180; + PartSys->sources[i].minLife = 20; + #else + PartSys->sources[i].maxLife = 250; + PartSys->sources[i].minLife = 50; + #endif + PartSys->sources[i].source.ttl = hw_random16((512 - (SEGMENT.speed << 1))) + 40; // standby time til next launch (in frames) + PartSys->sources[i].vy = (SEGMENT.custom1 >> 2); // emitting speed y + PartSys->sources[i].var = (SEGMENT.custom1 >> 2); // speed variation around vx,vy (+/- var) + } + } + } + else if (PartSys->sources[i].source.vy > 0) { // meteor is exploded and time is up (ttl==0 and positive speed), relaunch it + // reinitialize meteor + PartSys->sources[i].source.y = PartSys->maxY + (PS_P_RADIUS << 2); // start 4 pixels above the top + PartSys->sources[i].source.x = hw_random(PartSys->maxX); + PartSys->sources[i].source.vy = -hw_random16(30) - 30; // meteor downward speed + PartSys->sources[i].source.vx = hw_random16(50) - 25; // TODO: make this dependent on position so they do not move out of frame + PartSys->sources[i].source.hue = hw_random16(); // random color + PartSys->sources[i].source.ttl = 500; // long life, will explode at bottom + PartSys->sources[i].sourceFlags.collide = false; // trail particles will not collide + PartSys->sources[i].maxLife = 60; // spark particle life + PartSys->sources[i].minLife = 20; + PartSys->sources[i].vy = -9; // emitting speed (down) + PartSys->sources[i].var = 3; // speed variation around vx,vy (+/- var) + } + } + + PartSys->update(); // update and render + return FRAMETIME; +} +#undef NUMBEROFSOURCES +static const char _data_FX_MODE_PARTICLEIMPACT[] PROGMEM = "PS Impact@Launches,Size,Force,Hardness,Blur,Cylinder,Walls,Collide;;!;2;pal=0,sx=32,ix=85,c1=70,c2=130,c3=0,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) { + ParticleSystem2D *PartSys = NULL; + PSsettings2D sourcesettings; + sourcesettings.asByte = 0b00001100; // PS settings for bounceY, bounceY used for source movement (it always bounces whereas particles do not) + PSparticleFlags attractorFlags; + attractorFlags.asByte = 0; // no flags set + PSparticle *attractor; // particle pointer to the attractor + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem2D(PartSys, 1, sizeof(PSparticle), true)) // init using 1 source and advanced particle settings + return mode_static(); // allocation failed or not 2D + PartSys->sources[0].source.hue = hw_random16(); + PartSys->sources[0].source.vx = -7; // will collied with wall and get random bounce direction + PartSys->sources[0].sourceFlags.collide = true; // seeded particles will collide + PartSys->sources[0].sourceFlags.perpetual = true; //source does not age + #ifdef ESP8266 + PartSys->sources[0].maxLife = 200; // lifetime in frames (ESP8266 has less particles) + PartSys->sources[0].minLife = 30; + #else + PartSys->sources[0].maxLife = 350; // lifetime in frames + PartSys->sources[0].minLife = 50; + #endif + PartSys->sources[0].var = 4; // emiting variation + PartSys->setWallHardness(255); //bounce forever + PartSys->setWallRoughness(200); //randomize wall bounce + } + else { + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + } + + if (PartSys == NULL) + return mode_static(); // something went wrong, no data! + + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + attractor = reinterpret_cast(PartSys->PSdataEnd); + + PartSys->setColorByAge(SEGMENT.check1); + PartSys->setParticleSize(SEGMENT.custom1 >> 1); //set size globally + PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 25, 190)); + + if (SEGMENT.custom2 > 0) // collisions enabled + PartSys->enableParticleCollisions(true, map(SEGMENT.custom2, 1, 255, 120, 255)); // enable collisions and set particle collision hardness + else + PartSys->enableParticleCollisions(false); + + if (SEGMENT.call == 0) { + attractor->vx = PartSys->sources[0].source.vy; // set to spray movemement but reverse x and y + attractor->vy = PartSys->sources[0].source.vx; + } + + // set attractor properties + attractor->ttl = 100; // never dies + if (SEGMENT.check2) { + if ((SEGMENT.call % 3) == 0) // move slowly + PartSys->particleMoveUpdate(*attractor, attractorFlags, &sourcesettings); // move the attractor + } + else { + attractor->x = PartSys->maxX >> 1; // set to center + attractor->y = PartSys->maxY >> 1; + } + + if (SEGMENT.call % 5 == 0) + PartSys->sources[0].source.hue++; + + SEGENV.aux0 += 256; // emitting angle, one full turn in 255 frames (0xFFFF is 360°) + if (SEGMENT.call % 2 == 0) // alternate direction of emit + PartSys->angleEmit(PartSys->sources[0], SEGENV.aux0, 12); + else + PartSys->angleEmit(PartSys->sources[0], SEGENV.aux0 + 0x7FFF, 12); // emit at 180° as well + // apply force + uint32_t strength = SEGMENT.speed; + #ifdef USERMOD_AUDIOREACTIVE + um_data_t *um_data; + if(UsermodManager::getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { + uint32_t volumeSmth = (uint32_t)(*(float*) um_data->u_data[0]); // 0-255 + strength = (SEGMENT.speed * volumeSmth) >> 8; + } + #endif + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { + PartSys->pointAttractor(i, *attractor, strength, SEGMENT.check3); + } + + + if (SEGMENT.call % (33 - SEGMENT.custom3) == 0) + PartSys->applyFriction(2); + PartSys->particleMoveUpdate(PartSys->sources[0].source, PartSys->sources[0].sourceFlags, &sourcesettings); // move the source + PartSys->update(); // update and render + return FRAMETIME; +} +static const char _data_FX_MODE_PARTICLEATTRACTOR[] PROGMEM = "PS Attractor@Mass,Particles,Size,Collide,Friction,AgeColor,Move,Swallow;;!;2;pal=9,sx=100,ix=82,c1=0,c2=0"; + +/* +Particle Spray, just a particle spray with many parameters +Uses palette for particle color +by DedeHai (Damian Schneider) +*/ +uint16_t mode_particlespray(void) { + ParticleSystem2D *PartSys = NULL; + const uint8_t hardness = 200; // collision hardness is fixed + + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem2D(PartSys, 1)) // init, no additional data needed + return mode_static(); // allocation failed or not 2D + PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting) + PartSys->setBounceY(true); + PartSys->setMotionBlur(200); // anable motion blur + PartSys->setSmearBlur(10); // anable motion blur + PartSys->sources[0].source.hue = hw_random16(); + PartSys->sources[0].sourceFlags.collide = true; // seeded particles will collide (if enabled) + PartSys->sources[0].var = 3; + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + + if (PartSys == NULL) + return mode_static(); // something went wrong, no data! + + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setBounceX(!SEGMENT.check2); + PartSys->setWrapX(SEGMENT.check2); + PartSys->setWallHardness(hardness); + PartSys->setGravity(8 * SEGMENT.check1); // enable gravity if checked (8 is default strength) + //numSprays = min(PartSys->numSources, (uint8_t)1); // number of sprays + + if (SEGMENT.check3) // collisions enabled + PartSys->enableParticleCollisions(true, hardness); // enable collisions and set particle collision hardness + else + PartSys->enableParticleCollisions(false); + + //position according to sliders + PartSys->sources[0].source.x = map(SEGMENT.custom1, 0, 255, 0, PartSys->maxX); + PartSys->sources[0].source.y = map(SEGMENT.custom2, 0, 255, 0, PartSys->maxY); + uint16_t angle = (256 - (((int32_t)SEGMENT.custom3 + 1) << 3)) << 8; + + #ifdef USERMOD_AUDIOREACTIVE + um_data_t *um_data; + if (UsermodManager::getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { + uint32_t volumeSmth = (uint8_t)(*(float*) um_data->u_data[0]); //0 to 255 + uint32_t volumeRaw = *(int16_t*)um_data->u_data[1]; //0 to 255 + PartSys->sources[0].minLife = 30; + + if (SEGMENT.call % 20 == 0 || SEGMENT.call % (11 - volumeSmth / 25) == 0) { // defines interval of particle emit + PartSys->sources[0].maxLife = (volumeSmth >> 1) + (SEGMENT.intensity >> 1); // lifetime in frames + PartSys->sources[0].var = 1 + ((volumeRaw * SEGMENT.speed) >> 12); + uint32_t emitspeed = (SEGMENT.speed >> 2) + (volumeRaw >> 3); + PartSys->sources[0].source.hue += volumeSmth/30; + PartSys->angleEmit(PartSys->sources[0], angle, emitspeed); + } + } + else { //no AR data, fall back to normal mode + // change source properties + if (SEGMENT.call % (11 - (SEGMENT.intensity / 25)) == 0) { // every nth frame, cycle color and emit particles + PartSys->sources[0].maxLife = 300 + SEGMENT.intensity; // lifetime in frames + PartSys->sources[0].minLife = 150 + SEGMENT.intensity; + PartSys->sources[0].source.hue++; // = hw_random16(); //change hue of spray source + PartSys->angleEmit(PartSys->sources[0], angle, SEGMENT.speed >> 2); + } + } + #else + // change source properties + if (SEGMENT.call % (11 - (SEGMENT.intensity / 25)) == 0) { // every nth frame, cycle color and emit particles + PartSys->sources[0].maxLife = 300; // lifetime in frames. note: could be done in init part, but AR moderequires this to be dynamic + PartSys->sources[0].minLife = 100; + PartSys->sources[0].source.hue++; // = hw_random16(); //change hue of spray source + // PartSys->sources[i].var = SEGMENT.custom3; // emiting variation = nozzle size (custom 3 goes from 0-32) + // spray[j].source.hue = hw_random16(); //set random color for each particle (using palette) + PartSys->angleEmit(PartSys->sources[0], angle, SEGMENT.speed >> 2); + } + #endif + + PartSys->update(); // update and render + return FRAMETIME; +} +static const char _data_FX_MODE_PARTICLESPRAY[] PROGMEM = "PS Spray@Speed,!,Left/Right,Up/Down,Angle,Gravity,Cylinder/Square,Collide;;!;2v;pal=0,sx=150,ix=150,c1=220,c2=30,c3=21"; + + +/* +Particle base Graphical Equalizer +Uses palette for particle color +by DedeHai (Damian Schneider) +*/ +uint16_t mode_particleGEQ(void) { + ParticleSystem2D *PartSys = NULL; + + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem2D(PartSys, 1)) + return mode_static(); // allocation failed or not 2D + PartSys->setKillOutOfBounds(true); + PartSys->setUsedParticles(170); // use 2/3 of available particles + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + if (PartSys == NULL) + return mode_static(); // something went wrong, no data! + + uint32_t i; + // set particle system properties + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setWrapX(SEGMENT.check1); + PartSys->setBounceX(SEGMENT.check2); + PartSys->setBounceY(SEGMENT.check3); + //PartSys->enableParticleCollisions(false); + PartSys->setWallHardness(SEGMENT.custom2); + PartSys->setGravity(SEGMENT.custom3 << 2); // set gravity strength + + um_data_t *um_data; + if (!UsermodManager::getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) + um_data = simulateSound(SEGMENT.soundSim); // add support for no audio + + uint8_t *fftResult = (uint8_t *)um_data->u_data[2]; // 16 bins with FFT data, log mapped already, each band contains frequency amplitude 0-255 + + //map the bands into 16 positions on x axis, emit some particles according to frequency loudness + i = 0; + uint32_t binwidth = (PartSys->maxX + 1)>>4; //emit poisition variation for one bin (+/-) is equal to width/16 (for 16 bins) + uint32_t threshold = 300 - SEGMENT.intensity; + uint32_t emitparticles = 0; + + for (uint32_t bin = 0; bin < 16; bin++) { + uint32_t xposition = binwidth*bin + (binwidth>>1); // emit position according to frequency band + uint8_t emitspeed = ((uint32_t)fftResult[bin] * (uint32_t)SEGMENT.speed) >> 9; // emit speed according to loudness of band (127 max!) + emitparticles = 0; + + if (fftResult[bin] > threshold) { + emitparticles = 1;// + (fftResult[bin]>>6); + } + else if(fftResult[bin] > 0) { // band has low volue + uint32_t restvolume = ((threshold - fftResult[bin])>>2) + 2; + if (hw_random16() % restvolume == 0) + emitparticles = 1; + } + + while (i < PartSys->usedParticles && emitparticles > 0) { // emit particles if there are any left, low frequencies take priority + if (PartSys->particles[i].ttl == 0) { // find a dead particle + //set particle properties TODO: could also use the spray... + PartSys->particles[i].ttl = 20 + map(SEGMENT.intensity, 0,255, emitspeed>>1, emitspeed + hw_random16(emitspeed)) ; // set particle alive, particle lifespan is in number of frames + PartSys->particles[i].x = xposition + hw_random16(binwidth) - (binwidth>>1); // position randomly, deviating half a bin width + PartSys->particles[i].y = PS_P_RADIUS; // start at the bottom (PS_P_RADIUS is minimum position a particle is fully in frame) + PartSys->particles[i].vx = hw_random16(SEGMENT.custom1>>1)-(SEGMENT.custom1>>2) ; //x-speed variation: +/- custom1/4 + PartSys->particles[i].vy = emitspeed; + PartSys->particles[i].hue = (bin<<4) + hw_random16(17) - 8; // color from palette according to bin + emitparticles--; + } + i++; + } + } + + PartSys->update(); // update and render + return FRAMETIME; +} + +static const char _data_FX_MODE_PARTICLEGEQ[] PROGMEM = "PS GEQ 2D@Speed,Intensity,Diverge,Bounce,Gravity,Cylinder,Walls,Floor;;!;2f;pal=0,sx=155,ix=200,c1=0"; + +/* + * Particle rotating GEQ + * Particles sprayed from center with rotating spray + * Uses palette for particle color + * by DedeHai (Damian Schneider) + */ +#define NUMBEROFSOURCES 16 +uint16_t mode_particlecenterGEQ(void) { + ParticleSystem2D *PartSys = NULL; + uint8_t numSprays; + uint32_t i; + + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem2D(PartSys, NUMBEROFSOURCES)) // init, request 16 sources + return mode_static(); // allocation failed or not 2D + + numSprays = min(PartSys->numSources, (uint32_t)NUMBEROFSOURCES); + for (i = 0; i < numSprays; i++) { + PartSys->sources[i].source.x = (PartSys->maxX + 1) >> 1; // center + PartSys->sources[i].source.y = (PartSys->maxY + 1) >> 1; // center + PartSys->sources[i].source.hue = i * 16; // even color distribution + PartSys->sources[i].maxLife = 400; + PartSys->sources[i].minLife = 200; + } + PartSys->setKillOutOfBounds(true); + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + + if (PartSys == NULL) + return mode_static(); // something went wrong, no data! + + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + numSprays = min(PartSys->numSources, (uint32_t)NUMBEROFSOURCES); + + um_data_t *um_data; + if (!UsermodManager::getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) + um_data = simulateSound(SEGMENT.soundSim); // add support for no audio + + uint8_t *fftResult = (uint8_t *)um_data->u_data[2]; // 16 bins with FFT data, log mapped already, each band contains frequency amplitude 0-255 + uint32_t threshold = 300 - SEGMENT.intensity; + + if (SEGMENT.check2) + SEGENV.aux0 += SEGMENT.custom1 << 2; + else + SEGENV.aux0 -= SEGMENT.custom1 << 2; + + uint16_t angleoffset = (uint16_t)0xFFFF / (uint16_t)numSprays; + uint32_t j = hw_random16(numSprays); // 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 < numSprays; i++) { + if (SEGMENT.call % (32 - (SEGMENT.custom2 >> 3)) == 0 && SEGMENT.custom2 > 0) + PartSys->sources[j].source.hue += 1 + (SEGMENT.custom2 >> 4); + + PartSys->sources[j].var = SEGMENT.custom3 >> 2; + int8_t emitspeed = 5 + (((uint32_t)fftResult[j] * ((uint32_t)SEGMENT.speed + 20)) >> 10); // emit speed according to loudness of band + uint16_t emitangle = j * angleoffset + SEGENV.aux0; + + uint32_t emitparticles = 0; + if (fftResult[j] > threshold) + emitparticles = 1; + else if (fftResult[j] > 0) { // band has low value + uint32_t restvolume = ((threshold - fftResult[j]) >> 2) + 2; + if (hw_random16() % restvolume == 0) + emitparticles = 1; + } + if (emitparticles) + PartSys->angleEmit(PartSys->sources[j], emitangle, emitspeed); + + j = (j + 1) % numSprays; + } + PartSys->update(); // update and render + return FRAMETIME; +} +static const char _data_FX_MODE_PARTICLECIRCULARGEQ[] PROGMEM = "PS GEQ Nova@Speed,Intensity,Rotation Speed,Color Change,Nozzle,,Direction;;!;2f;pal=13,ix=180,c1=0,c2=0,c3=8"; + +/* +Particle replacement of Ghost Rider by DedeHai (Damian Schneider), original by stepko adapted by Blaz Kristan (AKA blazoncek) +*/ +#define MAXANGLESTEP 2200 //32767 means 180° +uint16_t mode_particleghostrider(void) { + ParticleSystem2D *PartSys = NULL; + PSsettings2D ghostsettings; + ghostsettings.asByte = 0b0000011; //enable wrapX and wrapY + + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem2D(PartSys, 1)) // init, no additional data needed + return mode_static(); // allocation failed or not 2D + PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting) + PartSys->sources[0].maxLife = 260; // lifetime in frames + PartSys->sources[0].minLife = 250; + PartSys->sources[0].source.x = hw_random16(PartSys->maxX); + PartSys->sources[0].source.y = hw_random16(PartSys->maxY); + SEGENV.step = hw_random16(MAXANGLESTEP) - (MAXANGLESTEP>>1); // angle increment + } + else { + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + } + + if (PartSys == NULL) + return mode_static(); // something went wrong, no data! + + if(SEGMENT.intensity > 0) { // spiraling + if(SEGENV.aux1) { + SEGENV.step += SEGMENT.intensity>>3; + if((int32_t)SEGENV.step > MAXANGLESTEP) + SEGENV.aux1 = 0; + } + else { + SEGENV.step -= SEGMENT.intensity>>3; + if((int32_t)SEGENV.step < -MAXANGLESTEP) + SEGENV.aux1 = 1; + } + } + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setMotionBlur(SEGMENT.custom1); + PartSys->sources[0].var = SEGMENT.custom3 >> 1; + + // color by age (PS 'color by age' always starts with hue = 255, don't want that here) + if(SEGMENT.check1) { + for(uint32_t i = 0; i < PartSys->usedParticles; i++) { + PartSys->particles[i].hue = PartSys->sources[0].source.hue + (PartSys->particles[i].ttl<<2); + } + } + + // enable/disable walls + ghostsettings.bounceX = SEGMENT.check2; + ghostsettings.bounceY = SEGMENT.check2; + + SEGENV.aux0 += (int32_t)SEGENV.step; // step is angle increment + uint16_t emitangle = SEGENV.aux0 + 32767; // +180° + int32_t speed = map(SEGMENT.speed, 0, 255, 12, 64); + PartSys->sources[0].source.vx = ((int32_t)cos16_t(SEGENV.aux0) * speed) / (int32_t)32767; + PartSys->sources[0].source.vy = ((int32_t)sin16_t(SEGENV.aux0) * speed) / (int32_t)32767; + PartSys->sources[0].source.ttl = 500; // source never dies (note: setting 'perpetual' is not needed if replenished each frame) + PartSys->particleMoveUpdate(PartSys->sources[0].source, PartSys->sources[0].sourceFlags, &ghostsettings); + // set head (steal one of the particles) + PartSys->particles[PartSys->usedParticles-1].x = PartSys->sources[0].source.x; + PartSys->particles[PartSys->usedParticles-1].y = PartSys->sources[0].source.y; + PartSys->particles[PartSys->usedParticles-1].ttl = 255; + PartSys->particles[PartSys->usedParticles-1].sat = 0; //white + // emit two particles + PartSys->angleEmit(PartSys->sources[0], emitangle, speed); + PartSys->angleEmit(PartSys->sources[0], emitangle, speed); + if (SEGMENT.call % (11 - (SEGMENT.custom2 / 25)) == 0) { // every nth frame, cycle color and emit particles //TODO: make this a segment call % SEGMENT.custom2 for better control + PartSys->sources[0].source.hue++; + } + if (SEGMENT.custom2 > 190) //fast color change + PartSys->sources[0].source.hue += (SEGMENT.custom2 - 190) >> 2; + + PartSys->update(); // update and render + return FRAMETIME; +} +static const char _data_FX_MODE_PARTICLEGHOSTRIDER[] PROGMEM = "PS Ghost Rider@Speed,Spiral,Blur,Color Cycle,Spread,AgeColor,Walls;;!;2;pal=1,sx=70,ix=0,c1=220,c2=30,c3=21,o1=1"; + +/* +PS Blobs: large particles bouncing around, changing size and form +Uses palette for particle color +by DedeHai (Damian Schneider) +*/ +uint16_t mode_particleblobs(void) { + ParticleSystem2D *PartSys = NULL; + + if (SEGMENT.call == 0) { + if (!initParticleSystem2D(PartSys, 1, 0, true, true)) //init, request one source, no additional bytes, advanced size & size control (actually dont really need one TODO: test if using zero sources also works) + return mode_static(); // allocation failed or not 2D + PartSys->setBounceX(true); + PartSys->setBounceY(true); + PartSys->setWallHardness(255); + PartSys->setWallRoughness(255); + PartSys->setCollisionHardness(255); + //PartSys->setParticleSize(0); //set global size to zero or motion blur cannot be used (is zero by default) + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + + if (PartSys == NULL) + return mode_static(); // something went wrong, no data! + + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 25, 128)); // minimum 10%, maximum 50% of available particles (note: PS ensures at least 1) + PartSys->enableParticleCollisions(SEGMENT.check2); + + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { // update particles + if (SEGENV.aux0 != SEGMENT.speed || PartSys->particles[i].ttl == 0) { // speed changed or dead + PartSys->particles[i].vx = (int8_t)hw_random16(SEGMENT.speed >> 1) - (SEGMENT.speed >> 2); // +/- speed/4 + PartSys->particles[i].vy = (int8_t)hw_random16(SEGMENT.speed >> 1) - (SEGMENT.speed >> 2); + } + if (SEGENV.aux1 != SEGMENT.custom1 || PartSys->particles[i].ttl == 0) // size changed or dead + PartSys->advPartSize[i].maxsize = 60 + (SEGMENT.custom1 >> 1) + hw_random16((SEGMENT.custom1 >> 2)); // set each particle to slightly randomized size + + //PartSys->particles[i].perpetual = SEGMENT.check2; //infinite life if set + if (PartSys->particles[i].ttl == 0) { // find dead particle, renitialize + PartSys->particles[i].ttl = 300 + hw_random16(((uint16_t)SEGMENT.custom2 << 3) + 100); + PartSys->particles[i].x = hw_random(PartSys->maxX); + PartSys->particles[i].y = hw_random16(PartSys->maxY); + PartSys->particles[i].hue = hw_random16(); // set random color + PartSys->particleFlags[i].collide = true; // enable collision for particle + PartSys->advPartProps[i].size = 0; // start out small + PartSys->advPartSize[i].asymmetry = hw_random16(220); + PartSys->advPartSize[i].asymdir = hw_random16(255); + // set advanced size control properties + PartSys->advPartSize[i].grow = true; + PartSys->advPartSize[i].growspeed = 1 + hw_random16(9); + PartSys->advPartSize[i].shrinkspeed = 1 + hw_random16(9); + PartSys->advPartSize[i].wobblespeed = 1 + hw_random16(3); + } + //PartSys->advPartSize[i].asymmetry++; + PartSys->advPartSize[i].pulsate = SEGMENT.check3; + PartSys->advPartSize[i].wobble = SEGMENT.check1; + } + SEGENV.aux0 = SEGMENT.speed; //write state back + SEGENV.aux1 = SEGMENT.custom1; + + #ifdef USERMOD_AUDIOREACTIVE + um_data_t *um_data; + if (UsermodManager::getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { + uint8_t volumeSmth = (uint8_t)(*(float*)um_data->u_data[0]); + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { // update particles + if (SEGMENT.check3) //pulsate selected + PartSys->advPartProps[i].size = volumeSmth; + } + } + #endif + + PartSys->setMotionBlur(((SEGMENT.custom3) << 3) + 7); + PartSys->update(); // update and render + + return FRAMETIME; +} +static const char _data_FX_MODE_PARTICLEBLOBS[] PROGMEM = "PS Blobs@Speed,Blobs,Size,Life,Blur,Wobble,Collide,Pulsate;;!;2v;sx=30,ix=64,c1=200,c2=130,c3=0,o3=1"; +#endif //WLED_DISABLE_PARTICLESYSTEM2D + +#endif // WLED_DISABLE_2D + + +/////////////////////////// +// 1D Particle System FX // +/////////////////////////// + +#ifndef WLED_DISABLE_PARTICLESYSTEM1D +/* +Particle version of Drip and Rain +Uses palette for particle color +by DedeHai (Damian Schneider) +*/ +uint16_t mode_particleDrip(void) { + ParticleSystem1D *PartSys = NULL; + //uint8_t numSprays; + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem1D(PartSys, 4)) // init + return mode_static(); // allocation failed or single pixel + PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting) + PartSys->sources[0].source.hue = hw_random16(); + SEGENV.aux1 = 0xFFFF; // invalidate + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + + if (PartSys == NULL) + return mode_static(); // something went wrong, no data! + + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setBounce(true); + PartSys->setWallHardness(50); + + PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur + PartSys->setGravity(SEGMENT.custom3 >> 1); // set gravity (8 is default strength) + PartSys->setParticleSize(SEGMENT.check3); // 1 or 2 pixel rendering + + if (SEGMENT.check2) { //collisions enabled + PartSys->enableParticleCollisions(true); //enable, full hardness + } + else + PartSys->enableParticleCollisions(false); + + PartSys->sources[0].sourceFlags.collide = false; //drops do not collide + + if (SEGMENT.check1) { //rain mode, emit at random position, short life (3-8 seconds at 50fps) + if (SEGMENT.custom1 == 0) //splash disabled, do not bounce raindrops + PartSys->setBounce(false); + PartSys->sources[0].var = 5; + PartSys->sources[0].v = -(8 + (SEGMENT.speed >> 2)); //speed + var must be < 128, inverted speed (=down) + // lifetime in frames + PartSys->sources[0].minLife = 30; + PartSys->sources[0].maxLife = 200; + PartSys->sources[0].source.x = hw_random(PartSys->maxX); //random emit position + } + else { //drip + PartSys->sources[0].var = 0; + PartSys->sources[0].v = -(SEGMENT.speed >> 1); //speed + var must be < 128, inverted speed (=down) + PartSys->sources[0].minLife = 3000; + PartSys->sources[0].maxLife = 3000; + PartSys->sources[0].source.x = PartSys->maxX - PS_P_RADIUS_1D; + } + + if (SEGENV.aux1 != SEGMENT.intensity) //slider changed + SEGENV.aux0 = 1; //must not be zero or "% 0" happens below which crashes on ESP32 + + SEGENV.aux1 = SEGMENT.intensity; // save state + + // every nth frame emit a particle + if (SEGMENT.call % SEGENV.aux0 == 0) { + int32_t interval = 300 / ((SEGMENT.intensity) + 1); + SEGENV.aux0 = interval + hw_random(interval + 5); + // if(SEGMENT.check1) // rain mode + // PartSys->sources[0].source.hue = 0; + // else + PartSys->sources[0].source.hue = hw_random8(); //set random color TODO: maybe also not random but color cycling? need another slider or checkmark for this. + PartSys->sprayEmit(PartSys->sources[0]); + } + + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { //check all particles + if (PartSys->particles[i].ttl && PartSys->particleFlags[i].collide == false) { // use collision flag to identify splash particles + if (SEGMENT.custom1 > 0 && PartSys->particles[i].x < (PS_P_RADIUS_1D << 1)) { //splash enabled and reached bottom + PartSys->particles[i].ttl = 0; //kill origin particle + PartSys->sources[0].maxLife = 80; + PartSys->sources[0].minLife = 20; + PartSys->sources[0].var = 10 + (SEGMENT.custom1 >> 3); + PartSys->sources[0].v = 0; + PartSys->sources[0].source.hue = PartSys->particles[i].hue; + PartSys->sources[0].source.x = PS_P_RADIUS_1D; + PartSys->sources[0].sourceFlags.collide = true; //splashes do collide if enabled + for (int j = 0; j < 2 + (SEGMENT.custom1 >> 2); j++) { + PartSys->sprayEmit(PartSys->sources[0]); + } + } + } + + if (SEGMENT.check1) { //rain mode, fade hue to max + if (PartSys->particles[i].hue < 245) + PartSys->particles[i].hue += 8; + } + //increase speed on high settings by calling the move function twice + if (SEGMENT.speed > 200) + PartSys->particleMoveUpdate(PartSys->particles[i], PartSys->particleFlags[i]); + } + + PartSys->update(); // update and render + return FRAMETIME; +} +static const char _data_FX_MODE_PARTICLEDRIP[] PROGMEM = "PS DripDrop@Speed,!,Splash,Blur,Gravity,Rain,PushSplash,Smooth;,!;!;1;pal=0,sx=150,ix=25,c1=220,c2=30,c3=21"; + + +/* +Particle Replacement for "Bbouncing Balls by Aircoookie" +Also replaces rolling balls and juggle (and maybe popcorn) +Uses palette for particle color +by DedeHai (Damian Schneider) +*/ +uint16_t mode_particlePinball(void) { + ParticleSystem1D *PartSys = NULL; + + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem1D(PartSys, 1, 128, 0, true)) // init + return mode_static(); // allocation failed or is single pixel + PartSys->sources[0].sourceFlags.collide = true; // seeded particles will collide (if enabled) + PartSys->setKillOutOfBounds(true); // out of bounds particles dont return + PartSys->sources[0].source.x = PS_P_RADIUS_1D; //emit at bottom + PartSys->sources[0].maxLife = 0xFFFF; // maximum lifetime in frames (long but not infinite to avoid perpetual handling, this is enough to travel 4000 pixels at min speed) + PartSys->sources[0].minLife = PartSys->sources[0].maxLife; + SEGENV.aux0 = 1; + SEGENV.aux1 = 500; //set out of speed range to ensure uptate on first call + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + + if (PartSys == NULL) + return mode_static(); // something went wrong, no data! + + // Particle System settings + //uint32_t hardness = 240 + (SEGMENT.custom1>>4); + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setGravity(map(SEGMENT.custom3, 0 , 31, 0 , 16)); // set gravity (8 is default strength) + PartSys->setBounce(SEGMENT.custom3); // disables bounce if no gravity is used + PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur + PartSys->enableParticleCollisions(SEGMENT.check1, 254); // enable collisions and set particle collision hardness (do not use full hardness or particles speed up due to pushing, can not be made perfectly balanced) + PartSys->setUsedParticles(SEGMENT.intensity); + PartSys->setColorByPosition(SEGMENT.check3); + + bool updateballs = false; + if (SEGENV.aux1 != SEGMENT.speed + SEGMENT.intensity + SEGMENT.check2 + SEGMENT.custom1) // user settings change + updateballs = true; + + if (SEGMENT.check2) { //rolling balls + PartSys->setGravity(0); + PartSys->setWallHardness(255); + + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { + if ((PartSys->particles[i].vx > 8 || PartSys->particles[i].vx < -8) && PartSys->particles[i].ttl > 200) //let only slow particles die (ensures no stopped particles) + PartSys->particles[i].ttl = 260; //set alive at full intensity + if (updateballs || PartSys->particles[i].ttl == 0) { //speed changed or particle died, set particle properties + PartSys->particles[i].ttl = 260; + PartSys->particleFlags[i].collide = true; + int32_t newspeed = hw_random16(20 + (SEGMENT.speed >> 2)) + (SEGMENT.speed >> 3); + PartSys->particles[i].vx = PartSys->particles[i].vx > 0 ? newspeed : -newspeed; //keep the direction + PartSys->particles[i].hue = hw_random8(); //set ball colors to random + PartSys->advPartProps[i].sat = 255; + PartSys->advPartProps[i].size = SEGMENT.custom1; + } + } + } + else { //bouncing balls + PartSys->setWallHardness(220); + PartSys->sources[0].var = SEGMENT.speed >> 3; + int32_t newspeed = 2 + (SEGMENT.speed >> 1) - (SEGMENT.speed >> 3); + PartSys->sources[0].v = newspeed; + //check for balls that are 'laying on the ground' and remove them + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { + if (PartSys->particles[i].vx == 0 && PartSys->particles[i].x < (PS_P_RADIUS_1D + SEGMENT.custom1)) + PartSys->particles[i].ttl = 0; + if (updateballs && SEGMENT.custom3 == 0) { + PartSys->advPartProps[i].size = SEGMENT.custom1; + if(SEGMENT.custom3 == 0) //gravity off, update speed + PartSys->particles[i].vx = PartSys->particles[i].vx > 0 ? newspeed : -newspeed; //keep the direction + } + } + + // every nth frame emit a ball + if (SEGMENT.call > SEGENV.step) { + int interval = 520 - ((int)SEGMENT.intensity << 1); + SEGENV.step += interval + hw_random16(interval); + PartSys->sources[0].source.hue = hw_random16(); //set ball color + PartSys->sources[0].sat = 255; + PartSys->sources[0].size = SEGMENT.custom1; + PartSys->sprayEmit(PartSys->sources[0]); + } + } + SEGENV.aux1 = SEGMENT.speed + SEGMENT.intensity + SEGMENT.check2 + SEGMENT.custom1; + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { + //if (SEGMENT.speed > 200) + PartSys->particleMoveUpdate(PartSys->particles[i], PartSys->particleFlags[i]); //increase speed on high settings by calling the move function twice + } + + PartSys->update(); // update and render + return FRAMETIME; +} +static const char _data_FX_MODE_PSPINBALL[] PROGMEM = "PS Pinball@Speed,!,Size,Blur,Gravity,Collide,Rolling,Position Color;,!;!;1;pal=0,c1=30,c2=0,c3=8"; + +/* +Particle Replacement for original Dancing Shadows: +"Spotlights moving back and forth that cast dancing shadows. +Shine this through tree branches/leaves or other close-up objects that cast +interesting shadows onto a ceiling or tarp. +By Steve Pomeroy @xxv" +Uses palette for particle color +by DedeHai (Damian Schneider) +*/ +uint16_t mode_particleDancingShadows(void) { + ParticleSystem1D *PartSys = NULL; + + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem1D(PartSys, 1)) // init, one source + return mode_static(); // allocation failed or is single pixel + PartSys->sources[0].maxLife = 1000; //set long life (kill out of bounds is done in custom way) + PartSys->sources[0].minLife = PartSys->sources[0].maxLife; + } + else { + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + } + + if (PartSys == NULL) + return mode_static(); // something went wrong, no data! + + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setMotionBlur(SEGMENT.custom1); + if (SEGMENT.check1) + PartSys->setSmearBlur(120); // enable smear blur + else + PartSys->setSmearBlur(0); // disable smear blur + PartSys->setParticleSize(SEGMENT.check3); // 1 or 2 pixel rendering + PartSys->setColorByPosition(SEGMENT.check2); // color fixed by position + PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 10, 255)); // set percentage of particles to use + + uint32_t deadparticles = 0; + //kill out of bounds and moving away plus change color + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { + if(((SEGMENT.call & 0x07) == 0) && PartSys->particleFlags[i].outofbounds) { //check if out of bounds particle move away from strip, only update every 8th frame + if((int32_t)PartSys->particles[i].vx * PartSys->particles[i].x > 0) PartSys->particles[i].ttl = 0; //particle is moving away, kill it + } + PartSys->particleFlags[i].perpetual = true; //particles do not age + if (SEGMENT.call % (32 / (1 + (SEGMENT.custom2 >> 3))) == 0) + PartSys->particles[i].hue += 2 + (SEGMENT.custom2 >> 5); + //note: updating speed on the fly is not accurately possible, since it is unknown which particles are assigned to which spot + if(SEGENV.aux0 != SEGMENT.speed) { //speed changed + //update all particle speed by setting them to current value + PartSys->particles[i].vx = PartSys->particles[i].vx > 0 ? SEGMENT.speed >> 3 : -SEGMENT.speed >> 3; + } + if(PartSys->particles[i].ttl == 0) deadparticles++; // count dead particles + } + SEGENV.aux0 = SEGMENT.speed; + + //generate a spotlight: generates particles just outside of view + if (deadparticles > 5 && (SEGMENT.call & 0x03) == 0) { + //random color, random type + uint32_t type = hw_random16(SPOT_TYPES_COUNT); + int8_t speed = 2 + hw_random16(2 + (SEGMENT.speed >> 1)) + (SEGMENT.speed >> 4); + int32_t width = hw_random16(1, 10); + uint32_t ttl = 300; //ttl is particle brightness (below perpetual is set so it does not age, i.e. ttl stays at this value) + int32_t position; + //choose random start position, left and right from the segment + if (hw_random() & 0x01) { + position = PartSys->maxXpixel; + speed = -speed; + } + else + position = -width; + + PartSys->sources[0].v = speed; //emitted particle speed + PartSys->sources[0].source.hue = hw_random8(); //random spotlight color + for (int32_t i = 0; i < width; i++) { + if(width > 1) { + switch (type) { + case SPOT_TYPE_SOLID: + //nothing to do + break; + + case SPOT_TYPE_GRADIENT: + ttl = cubicwave8(map(i, 0, width - 1, 0, 255)); + ttl = ttl*ttl >> 8; //make gradient more pronounced + break; + + case SPOT_TYPE_2X_GRADIENT: + ttl = cubicwave8(2 * map(i, 0, width - 1, 0, 255)); + ttl = ttl*ttl >> 8; + break; + + case SPOT_TYPE_2X_DOT: + if(i > 0) position++; //skip one pixel + i++; + break; + + case SPOT_TYPE_3X_DOT: + if(i > 0) position += 2; //skip two pixels + i+=2; + break; + + case SPOT_TYPE_4X_DOT: + if(i > 0) position += 3; //skip three pixels + i+=3; + break; + } + } + //emit particle + //set the particle source position: + PartSys->sources[0].source.x = position * PS_P_RADIUS_1D; + uint32_t partidx = PartSys->sprayEmit(PartSys->sources[0]); + PartSys->particles[partidx].ttl = ttl; + position++; //do the next pixel + } + } + + PartSys->update(); // update and render + + return FRAMETIME; +} +static const char _data_FX_MODE_PARTICLEDANCINGSHADOWS[] PROGMEM = "PS Dancing Shadows@Speed,!,Blur,Color Cycle,,Smear,Position Color,Smooth;,!;!;1;sx=100,ix=180,c1=0,c2=0"; + +/* +Particle Fireworks 1D replacement +Uses palette for particle color +by DedeHai (Damian Schneider) +*/ +uint16_t mode_particleFireworks1D(void) { + ParticleSystem1D *PartSys = NULL; + uint8_t *forcecounter; + + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem1D(PartSys, 4, 150, 4, true)) // init + return mode_static(); // allocation failed or is single pixel + PartSys->setKillOutOfBounds(true); + PartSys->sources[0].sourceFlags.custom1 = 1; // set rocket state to standby + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + if (PartSys == NULL) + return mode_static(); // something went wrong, no data! + + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + forcecounter = PartSys->PSdataEnd; + PartSys->setParticleSize(SEGMENT.check3); // 1 or 2 pixel rendering + PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur + + int32_t gravity = (1 + (SEGMENT.speed >> 3)); + if(!SEGMENT.check1) // gravity enabled for sparks + PartSys->setGravity(0); // disable + else + PartSys->setGravity(gravity); // set gravity + + if(PartSys->sources[0].sourceFlags.custom1 == 1) { // rocket is on standby + PartSys->sources[0].source.ttl--; + if(PartSys->sources[0].source.ttl == 0) { // time is up, relaunch + + if(hw_random8() < SEGMENT.custom1) // randomly choose direction according to slider, fire at start of segment if true + SEGENV.aux0 = 1; + else + SEGENV.aux0 = 0; + + PartSys->sources[0].sourceFlags.custom1 = 0; //flag used for rocket state + PartSys->sources[0].source.hue = hw_random16(); + PartSys->sources[0].var = 10; // emit variation + PartSys->sources[0].v = -10; // emit speed + PartSys->sources[0].minLife = 100; + PartSys->sources[0].maxLife = 300; + PartSys->sources[0].source.x = 0; // start from bottom + uint32_t speed = sqrt((gravity * ((PartSys->maxX >> 2) + hw_random16(PartSys->maxX >> 1))) >> 4); // set speed such that rocket explods in frame + PartSys->sources[0].source.vx = min(speed, (uint32_t)127); + PartSys->sources[0].source.ttl = 4000; + PartSys->sources[0].sat = 30; // low saturation exhaust + PartSys->sources[0].size = 0; // default size + PartSys->sources[0].sourceFlags.reversegrav = false ; // normal gravity + + if(SEGENV.aux0) { // inverted rockets launch from end + PartSys->sources[0].sourceFlags.reversegrav = true; + PartSys->sources[0].source.x = PartSys->maxX; // start from top + PartSys->sources[0].source.vx = -PartSys->sources[0].source.vx; // revert direction + PartSys->sources[0].v = -PartSys->sources[0].v; // invert exhaust emit speed + } + } + } + else { // rocket is launched + int32_t rocketgravity = -gravity; + int32_t speed = PartSys->sources[0].source.vx; + if(SEGENV.aux0) { // negative speed rocket + rocketgravity = -rocketgravity; + speed = -speed; + } + PartSys->applyForce(PartSys->sources[0].source, rocketgravity, forcecounter[0]); + PartSys->particleMoveUpdate(PartSys->sources[0].source, PartSys->sources[0].sourceFlags); + PartSys->particleMoveUpdate(PartSys->sources[0].source, PartSys->sources[0].sourceFlags); // increase speed by calling the move function twice, also ages twice + uint32_t rocketheight = SEGENV.aux0 ? PartSys->maxX - PartSys->sources[0].source.x : PartSys->sources[0].source.x; + + if(speed < 0 && PartSys->sources[0].source.ttl > 50) // reached apogee + PartSys->sources[0].source.ttl = min((uint32_t)50, rocketheight >> (PS_P_RADIUS_SHIFT_1D + 3)); // alive for a few more frames + + if(PartSys->sources[0].source.ttl < 2) { // explode + PartSys->sources[0].sourceFlags.custom1 = 1; // set standby state + PartSys->sources[0].var = 5 + ((((PartSys->maxX >> 1) + rocketheight) * (200 + SEGMENT.intensity)) / (PartSys->maxX << 2)); // set explosion particle speed + PartSys->sources[0].minLife = 600; + PartSys->sources[0].maxLife = 1300; + PartSys->sources[0].source.ttl = 100 + hw_random16(64 - (SEGMENT.speed >> 2)); // standby time til next launch + PartSys->sources[0].sat = 7 + (SEGMENT.custom3 << 3); //color saturation TODO: replace saturation with something more useful? + PartSys->sources[0].size = hw_random16(64); // random particle size in explosion + uint32_t explosionsize = 8 + (PartSys->maxXpixel >> 2) + (PartSys->sources[0].source.x >> (PS_P_RADIUS_SHIFT_1D - 1)); + explosionsize += hw_random16((explosionsize * SEGMENT.intensity) >> 8); + for(uint32_t e = 0; e < explosionsize; e++) { // emit explosion particles + if(SEGMENT.check2) + PartSys->sources[0].source.hue = hw_random16(); //random color for each particle + PartSys->sprayEmit(PartSys->sources[0]); // emit a particle + } + } + } + if((SEGMENT.call & 0x01) == 0 && PartSys->sources[0].sourceFlags.custom1 == false) // every second frame and not in standby + PartSys->sprayEmit(PartSys->sources[0]); // emit exhaust particle + if((SEGMENT.call & 0x03) == 0) // every fourth frame + PartSys->applyFriction(1); // apply friction to all particles + + PartSys->update(); // update and render + + for(uint32_t i = 0; i < PartSys->usedParticles; i++) { + if(PartSys->particles[i].ttl > 10) PartSys->particles[i].ttl -= 10; //ttl is linked to brightness, this allows to use higher brightness but still a short spark lifespan + else PartSys->particles[i].ttl = 0; + } + + return FRAMETIME; +} +static const char _data_FX_MODE_PS_FIREWORKS1D[] PROGMEM = "PS Fireworks 1D@Gravity,Explosion,Firing side,Blur,Saturation,,Colorful,Smooth;,!;!;1;pal=0,sx=150,c2=30,c3=21,o2=1"; + +/* +Particle based Sparkle effect +Uses palette for particle color +by DedeHai (Damian Schneider) +*/ +uint16_t mode_particleSparkler(void) { + ParticleSystem1D *PartSys = NULL; + uint32_t numSparklers; + PSsettings1D sparklersettings; + sparklersettings.asByte = 0; // PS settings for sparkler (set below) + + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem1D(PartSys, 16, 128 ,0, true)) // init, no additional data needed + return mode_static(); // allocation failed or is single pixel + } else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + if (PartSys == NULL) + return mode_static(); // something went wrong, no data! + + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + + sparklersettings.wrap = !SEGMENT.check2; + sparklersettings.bounce = SEGMENT.check2; // note: bounce always takes priority over wrap + + numSparklers = PartSys->numSources; + PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur/overlay + //PartSys->setSmearBlur(SEGMENT.custom2); // anable smearing blur + + for (uint32_t i = 0; i < numSparklers; i++) { + PartSys->sources[i].source.hue = hw_random16(); + PartSys->sources[i].var = 0; // sparks stationary + PartSys->sources[i].minLife = 150 + SEGMENT.intensity; + PartSys->sources[i].maxLife = 250 + (SEGMENT.intensity << 1); + uint32_t speed = SEGMENT.speed >> 1; + if(SEGMENT.check1) // sparks move (slide option) + PartSys->sources[i].var = SEGMENT.intensity >> 3; + PartSys->sources[i].source.vx = speed; // update speed, do not change direction + PartSys->sources[i].source.ttl = 400; // replenish its life (setting it perpetual uses more code) + PartSys->sources[i].sat = SEGMENT.custom1; // color saturation + PartSys->sources[i].size = SEGMENT.check3 ? 120 : 0; + if(SEGMENT.speed == 255) // random position at highest speed setting + PartSys->sources[i].source.x = hw_random16(PartSys->maxX); + else + PartSys->particleMoveUpdate(PartSys->sources[i].source, PartSys->sources[i].sourceFlags, &sparklersettings); //move sparkler + } + + numSparklers = min(1 + (SEGMENT.custom3 >> 1), (int)numSparklers); // set used sparklers, 1 to 16 + + if (SEGENV.aux0 != SEGMENT.custom3) { //number of used sparklers changed, redistribute + for (uint32_t i = 1; i < numSparklers; i++) { + PartSys->sources[i].source.x = (PartSys->sources[0].source.x + (PartSys->maxX / numSparklers) * i ) % PartSys->maxX; //distribute evenly + } + } + SEGENV.aux0 = SEGMENT.custom3; + + for (uint32_t i = 0; i < numSparklers; i++) { + if (hw_random() % (((271 - SEGMENT.intensity) >> 4)) == 0) + PartSys->sprayEmit(PartSys->sources[i]); //emit a particle + } + + PartSys->update(); // update and render + + for(uint32_t i = 0; i < PartSys->usedParticles; i++) { + if(PartSys->particles[i].ttl > (64 - (SEGMENT.intensity >> 2))) PartSys->particles[i].ttl -= (64 - (SEGMENT.intensity >> 2)); //ttl is linked to brightness, this allows to use higher brightness but still a short spark lifespan + else PartSys->particles[i].ttl = 0; + } + + return FRAMETIME; +} +static const char _data_FX_MODE_PS_SPARKLER[] PROGMEM = "PS Sparkler@Move,!,Saturation,Blur,Sparklers,Slide,Bounce,Large;,!;!;1;pal=0,sx=255,c1=0,c2=0,c3=6"; + +/* +Particle based Hourglass, particles falling at defined intervals +Uses palette for particle color +by DedeHai (Damian Schneider) +*/ +uint16_t mode_particleHourglass(void) { + ParticleSystem1D *PartSys = NULL; + int32_t positionoffset; // resting position offset + bool* direction; + uint8_t* basehue; + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem1D(PartSys, 0, 255, 2, false)) // init + return mode_static(); // allocation failed or is single pixel + PartSys->setBounce(true); + PartSys->setWallHardness(80); + SEGENV.step = 0xFFFF; + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + if (PartSys == NULL) + return mode_static(); // something went wrong, no data! + + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + basehue = PartSys->PSdataEnd; //assign data pointer + direction = reinterpret_cast(PartSys->PSdataEnd + 1); //assign data pointer + PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 1, 255)); + PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur + PartSys->setGravity(map(SEGMENT.custom3, 0, 31, 1, 30)); + PartSys->enableParticleCollisions(true, 34); // fixed hardness, 34 is a value that works best in most settings (spent a long time optimizing) + + positionoffset = PS_P_RADIUS_1D / 2; + uint32_t colormode = SEGMENT.custom1 >> 5; // 0-7 + + if((SEGMENT.intensity | (PartSys->getAvailableParticles() << 8)) != SEGENV.step) { // initialize, getAvailableParticles changes while in FX transition + if(PartSys->getAvailableParticles() == SEGENV.step >> 8) // only intensity slider changed or first call + *basehue = hw_random16(); //choose new random color + SEGENV.step = SEGMENT.intensity | (PartSys->getAvailableParticles() << 8); + for(uint32_t i = 0; i < PartSys->usedParticles; i++) { + PartSys->particleFlags[i].reversegrav = true; + *direction = 0; + SEGENV.aux1 = 1; // initialize below + } + SEGENV.aux0 = PartSys->usedParticles - 1; // initial state, start with highest number particle + } + + for(uint32_t i = 0; i < PartSys->usedParticles; i++) { // check if particle reached target position after falling + int32_t targetposition; + if (PartSys->particleFlags[i].fixed == false) { + // calculate target position depending on direction + if(PartSys->particleFlags[i].reversegrav) { + targetposition = PartSys->maxX - (i * PS_P_RADIUS_1D + positionoffset); // target resting position + if(PartSys->particles[i].x >= targetposition) // particle has reached target position, pin it. if not pinned, they do not stack well on larger piles + PartSys->particleFlags[i].fixed = true; + } + else { + targetposition = (PartSys->usedParticles - i) * PS_P_RADIUS_1D - positionoffset; // target resting position + if(PartSys->particles[i].x <= targetposition) // particle has reached target position, pin it. if not pinned, they do not stack well on larger piles + PartSys->particleFlags[i].fixed = true; + } + + } + if(colormode == 7) + PartSys->setColorByPosition(true); // color fixed by position + else { + PartSys->setColorByPosition(false); + switch(colormode) { + case 0: PartSys->particles[i].hue = 120; break; // fixed at 120, if flip is activated, this can make red and green (use palette 34) + case 1: PartSys->particles[i].hue = *basehue; break; // fixed random color + case 2: + case 3: PartSys->particles[i].hue = *basehue + (i % colormode)*70; break; // interleved colors (every 2 or 3 particles) + case 4: PartSys->particles[i].hue = *basehue + (i * 255) / PartSys->usedParticles; break; // gradient palette colors + case 5: PartSys->particles[i].hue = *basehue + (i * 1024) / PartSys->usedParticles; break; // multi gradient palette colors + case 6: PartSys->particles[i].hue = i + (strip.now >> 3); break; // disco! moving color gradient + default: break; + } + } + if(SEGMENT.check1 && !PartSys->particleFlags[i].reversegrav) // flip color when fallen + PartSys->particles[i].hue += 120; + } + + if(SEGENV.aux1 == 1) { // last countdown call before dropping starts, reset all particles + for(uint32_t i = 0; i < PartSys->usedParticles; i++) { + PartSys->particleFlags[i].collide = true; + PartSys->particleFlags[i].perpetual = true; + PartSys->particles[i].ttl = 260; + uint32_t targetposition; + //calculate target position depending on direction + if(PartSys->particleFlags[i].reversegrav) + targetposition = PartSys->maxX - (i * PS_P_RADIUS_1D + positionoffset); // target resting position + else + targetposition = (PartSys->usedParticles - i) * PS_P_RADIUS_1D - positionoffset; // target resting position -5 - PS_P_RADIUS_1D/2 + + PartSys->particles[i].x = targetposition; + PartSys->particleFlags[i].fixed = true; + } + } + + if(SEGENV.aux1 == 0) { // countdown passed, run + uint32_t interval = 257 - SEGMENT.speed; // drop interval in frames, 1 second is 'speed = (257 - FPS)' speed = 0 is one drop every 257 frames + if(SEGMENT.check3 && *direction) // fast reset + interval = 3; + if(SEGMENT.call % interval == 0) { // drop a particle, do not drop more often than every second frame or particles tangle up quite badly + if(SEGENV.aux0 < PartSys->usedParticles) { + PartSys->particleFlags[SEGENV.aux0].reversegrav = *direction; // let this particle fall or rise + PartSys->particleFlags[SEGENV.aux0].fixed = false; // unpin + } + else { // overflow, flip direction + *direction = !(*direction); + SEGENV.aux1 = SEGMENT.virtualLength() + 100; // set countdown + } + if(*direction == 0) // down + SEGENV.aux0--; + else + SEGENV.aux0++; + } + } + else if(SEGMENT.check2) // auto reset + SEGENV.aux1--; // countdown + + PartSys->update(); // update and render + + return FRAMETIME; +} +static const char _data_FX_MODE_PS_HOURGLASS[] PROGMEM = "PS Hourglass@Speed,!,Color,Blur,Gravity,Colorflip,Start,Fast Reset;,!;!;1;pal=34,sx=245,ix=200,c1=140,c2=80,c3=4,o1=1,o2=1,o3=1"; + +/* +Particle based Spray effect (like a volcano, possible replacement for popcorn) +Uses palette for particle color +by DedeHai (Damian Schneider) +*/ +uint16_t mode_particle1Dspray(void) { + ParticleSystem1D *PartSys = NULL; + + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem1D(PartSys, 1)) + return mode_static(); // allocation failed or is single pixel + PartSys->setKillOutOfBounds(true); + PartSys->setWallHardness(150); + PartSys->setParticleSize(1); + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + if (PartSys == NULL) + return mode_static(); // something went wrong, no data! + + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setBounce(SEGMENT.check2); + PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur + int32_t gravity = -((int32_t)SEGMENT.custom3 - 16); // gravity setting, 0-15 is positive (down), 17 - 31 is negative (up) + PartSys->setGravity(abs(gravity)); // use reversgrav setting to invert gravity (for proper 'floor' and out of bounce handling) + + PartSys->sources[0].source.hue = SEGMENT.aux0; // hw_random16(); + PartSys->sources[0].var = 20; + PartSys->sources[0].minLife = 200; + PartSys->sources[0].maxLife = 400; + PartSys->sources[0].source.x = map(SEGMENT.custom1, 0 , 255, 0, PartSys->maxX); // spray position + PartSys->sources[0].v = map(SEGMENT.speed, 0 , 255, -127 + PartSys->sources[0].var, 127 - PartSys->sources[0].var); // particle emit speed + PartSys->sources[0].sourceFlags.reversegrav = gravity < 0 ? true : false; + + if(hw_random() % (1 + ((255 - SEGMENT.intensity) >> 3)) == 0) { + PartSys->sprayEmit(PartSys->sources[0]); // emit a particle + SEGMENT.aux0++; // increment hue + } + + //update color settings + PartSys->setColorByAge(SEGMENT.check1); // overruled by 'color by position' + PartSys->setColorByPosition(SEGMENT.check3); + for(uint i = 0; i < PartSys->usedParticles; i++) { + PartSys->particleFlags[i].reversegrav = PartSys->sources[0].sourceFlags.reversegrav; // update gravity direction + } + PartSys->update(); // update and render + + return FRAMETIME; +} +static const char _data_FX_MODE_PS_1DSPRAY[] PROGMEM = "PS Spray 1D@Speed(+/-),!,Position,Blur,Gravity(+/-),AgeColor,Bounce,Position Color;,!;!;1;sx=200,ix=220,c1=0,c2=0,o1=1"; + +/* +Particle based balance: particles move back and forth (1D pendent to 2D particle box) +Uses palette for particle color +by DedeHai (Damian Schneider) +*/ +uint16_t mode_particleBalance(void) { + ParticleSystem1D *PartSys = NULL; + uint32_t i; + + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem1D(PartSys, 1, 128)) // init, no additional data needed, use half of max particles + return mode_static(); // allocation failed or is single pixel + //PartSys->setKillOutOfBounds(true); + PartSys->setParticleSize(1); + SEGENV.aux0 = 0; + SEGENV.aux1 = 0; //TODO: really need to set to zero or is it calloc'd? + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + if (PartSys == NULL) + return mode_static(); // something went wrong, no data! + + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur + PartSys->setBounce(!SEGMENT.check2); + PartSys->setWrap(SEGMENT.check2); + uint8_t hardness = SEGMENT.custom1 > 0 ? map(SEGMENT.custom1, 0, 255, 50, 250) : 200; // set hardness, make the walls hard if collisions are disabled + PartSys->enableParticleCollisions(SEGMENT.custom1, hardness); // enable collisions if custom1 > 0 + PartSys->setWallHardness(hardness); + PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 10, 255)); + if(PartSys->usedParticles > SEGENV.aux1) { // more particles, reinitialize + for (i = 0; i < PartSys->usedParticles; i++) { + PartSys->particles[i].x = i * PS_P_RADIUS_1D; + PartSys->particles[i].ttl = 300; + PartSys->particleFlags[i].perpetual = true; + PartSys->particleFlags[i].collide = true; + } + } + SEGENV.aux1 = PartSys->usedParticles; + + if (SEGMENT.call % (((255 - SEGMENT.speed) >> 6) + 1) == 0) { // how often the force is applied depends on speed setting + int32_t xgravity; + int32_t increment = (SEGMENT.speed >> 6) + 1; + SEGENV.aux0 += increment; + if(SEGMENT.check3) // random, use perlin noise + xgravity = ((int16_t)inoise8(SEGENV.aux0) - 128); + else // sinusoidal + xgravity = (int16_t)cos8(SEGENV.aux0) - 128;//((int32_t)(SEGMENT.custom3 << 2) * cos8(SEGENV.aux0) + // scale the force + xgravity = (xgravity * ((SEGMENT.custom3+1) << 2)) / 128; // xgravity: -127 to +127 + PartSys->applyForce(xgravity); + } + + uint32_t randomindex = hw_random16(PartSys->usedParticles); + PartSys->particles[randomindex].vx = ((int32_t)PartSys->particles[randomindex].vx * 200) / 255; // apply friction to random particle to reduce clumping (without collisions) + + //if(SEGMENT.check2 && (SEGMENT.call & 0x07) == 0) // no walls, apply friction to smooth things out + if((SEGMENT.call & 0x0F) == 0) // apply friction every 16th frame to smooth things out + PartSys->applyFriction(1); // apply friction to all particles + + //update colors + PartSys->setColorByPosition(SEGMENT.check1); + if(!SEGMENT.check1) { + for(i = 0; i < PartSys->usedParticles; i++) { + PartSys->particles[i].hue = (1024 * i) / PartSys->usedParticles; // color by particle index + } + } + PartSys->update(); // update and render + return FRAMETIME; +} +static const char _data_FX_MODE_PS_BALANCE[] PROGMEM = "PS 1D Balance@!,!,Hardness,Blur,Tilt,Position Color,Wrap,Random;,!;!;1;pal=18,sx=100,ix=40,c1=200,c2=0,c3=5,o1=1"; + +/* +Particle based Chase effect +Uses palette for particle color +by DedeHai (Damian Schneider) +*/ +uint16_t mode_particleChase(void) { + ParticleSystem1D *PartSys = NULL; + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem1D(PartSys, 1, 255, 3, true)) // init + return mode_static(); // allocation failed or is single pixel + SEGENV.aux0 = 0xFFFF; // invalidate + *PartSys->PSdataEnd = 1; // huedir + *(PartSys->PSdataEnd + 1) = 1; // sizedir + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + if (PartSys == NULL) + return mode_static(); // something went wrong, no data! + + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setColorByPosition(SEGMENT.check3); + PartSys->setMotionBlur((SEGMENT.custom3 + 1) << 3); // anable motion blur + // uint8_t* basehue = (PartSys->PSdataEnd + 2); //assign data pointer + + uint32_t settingssum = SEGMENT.speed + SEGMENT.intensity + SEGMENT.custom1 + SEGMENT.custom2 + SEGMENT.check1 + SEGMENT.check2 + SEGMENT.check3 + PartSys->getAvailableParticles(); // note: getAvailableParticles is used to enforce update during transitions + if (SEGENV.aux0 != settingssum) { // settings changed changed, update + uint32_t numParticles = map(SEGMENT.intensity, 0, 255, 2, 255 / (1 + (SEGMENT.custom1 >> 6))); // depends on intensity and particle size (custom1) + if (numParticles == 0) numParticles = 1; // minimum 1 particle + PartSys->setUsedParticles(numParticles); + SEGENV.step = (PartSys->maxX + (PS_P_RADIUS_1D << 5)) / PartSys->usedParticles; // spacing between particles + for (int32_t i = 0; i < (int32_t)PartSys->usedParticles; i++) { + PartSys->advPartProps[i].sat = 255; + PartSys->particles[i].x = (i - 1) * SEGENV.step; // distribute evenly (starts out of frame for i=0) + PartSys->particles[i].vx = SEGMENT.speed >> 1; + PartSys->advPartProps[i].size = SEGMENT.custom1; + if (SEGMENT.custom2 < 255) + PartSys->particles[i].hue = (i * (SEGMENT.custom2 << 3)) / PartSys->usedParticles; // gradient distribution + else + PartSys->particles[i].hue = hw_random16(); + } + SEGENV.aux0 = settingssum; + } + + int32_t huestep = (((uint32_t)SEGMENT.custom2 << 19) / PartSys->usedParticles) >> 16; // hue increment + + // wrap around (cannot use particle system wrap if distributing colors manually, it also wraps rendering which does not look good) + for (int32_t i = (int32_t)PartSys->usedParticles - 1; i >= 0; i--) { // check from the back, last particle wraps first, multiple particles can overrun per frame + if (PartSys->particles[i].x > PartSys->maxX + PS_P_RADIUS_1D + PartSys->advPartProps[i].size) { // wrap it around + uint32_t nextindex = (i + 1) % PartSys->usedParticles; + PartSys->particles[i].x = PartSys->particles[nextindex].x - (int)SEGENV.step; + if (SEGMENT.custom2 < 255) + PartSys->particles[i].hue = PartSys->particles[nextindex].hue - huestep; + else + PartSys->particles[i].hue = hw_random16(); + } + PartSys->particles[i].ttl = 300; // reset ttl, cannot use perpetual because memmanager can change pointer at any time + } + + PartSys->setParticleSize(SEGMENT.custom1); // if custom1 == 0 this sets rendering size to one pixel + PartSys->update(); // update and render + return FRAMETIME; +} +static const char _data_FX_MODE_PS_CHASE[] PROGMEM = "PS Chase@!,Density,Size,Hue,Blur,,,Position Color;,!;!;1;pal=11,sx=50,ix=100,c2=5,c3=0"; + +/* +Particle Fireworks Starburst replacement (smoother rendering, more settings) +Uses palette for particle color +by DedeHai (Damian Schneider) +*/ + +uint16_t mode_particleStarburst(void) { + ParticleSystem1D *PartSys = NULL; + uint32_t i; + + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem1D(PartSys, 1, 200, 0, true)) // init + return mode_static(); // allocation failed or is single pixel + PartSys->setKillOutOfBounds(true); + PartSys->enableParticleCollisions(true, 200); + PartSys->sources[0].source.ttl = 1; // set initial stanby time + PartSys->sources[0].sat = 0; // emitted particles start out white + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + if (PartSys == NULL) + return mode_static(); // something went wrong, no data! + + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur + PartSys->setGravity(SEGMENT.check1 * 8); // enable gravity + + if (PartSys->sources[0].source.ttl-- == 0) { // stanby time elapsed TODO: make it a timer? + uint32_t explosionsize = 4 + hw_random16(SEGMENT.intensity >> 2); + PartSys->sources[0].source.hue = hw_random16(); + PartSys->sources[0].var = 10 + (explosionsize << 1); + PartSys->sources[0].minLife = 250; + PartSys->sources[0].maxLife = 300; + PartSys->sources[0].source.x = hw_random(PartSys->maxX); //random explosion position + PartSys->sources[0].source.ttl = 10 + hw_random16(255 - SEGMENT.speed); + PartSys->sources[0].size = SEGMENT.custom1; // Fragment size + PartSys->setParticleSize(SEGMENT.custom1); // enable advanced size rendering + PartSys->sources[0].sourceFlags.collide = SEGMENT.check3; + for (uint32_t e = 0; e < explosionsize; e++) { // emit particles + if (SEGMENT.check2) + PartSys->sources[0].source.hue = hw_random16(); //random color for each particle + PartSys->sprayEmit(PartSys->sources[0]); //emit a particle + } + } + //shrink all particles + for (i = 0; i < PartSys->usedParticles; i++) { + if (PartSys->advPartProps[i].size) + PartSys->advPartProps[i].size--; + if (PartSys->advPartProps[i].sat < 251) + PartSys->advPartProps[i].sat += 1 + (SEGMENT.custom3 >> 2); //note: it should be >> 3, the >> 2 creates overflows resulting in blinking if custom3 > 27, which is a bonus feature + } + + if (SEGMENT.call % 5 == 0) { + PartSys->applyFriction(1); //slow down particles + } + + PartSys->update(); // update and render + return FRAMETIME; +} +static const char _data_FX_MODE_PS_STARBURST[] PROGMEM = "PS Starburst@Chance,Fragments,Size,Blur,Cooling,Gravity,Colorful,Push;,!;!;1;pal=52,sx=150,ix=150,c1=120,c2=0,c3=21"; + +/* +Particle based 1D GEQ effect, each frequency bin gets an emitter, distributed over the strip +Uses palette for particle color +by DedeHai (Damian Schneider) +*/ + +uint16_t mode_particle1DGEQ(void) { + ParticleSystem1D *PartSys = NULL; + uint32_t numSources; + uint32_t i; + + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem1D(PartSys, 16, 255, 0, true)) // init, no additional data needed + return mode_static(); // allocation failed or is single pixel + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + if (PartSys == NULL) + return mode_static(); // something went wrong, no data! + + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + numSources = PartSys->numSources; + PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur + + uint32_t spacing = PartSys->maxX / numSources; + for (i = 0; i < numSources; i++) { + PartSys->sources[i].source.hue = i * 16; // hw_random16(); //TODO: make adjustable, maybe even colorcycle? + PartSys->sources[i].var = SEGMENT.speed >> 2; + PartSys->sources[i].minLife = 180 + (SEGMENT.intensity >> 1); + PartSys->sources[i].maxLife = 240 + SEGMENT.intensity; + PartSys->sources[i].sat = 255; + PartSys->sources[i].size = SEGMENT.custom1; + PartSys->setParticleSize(SEGMENT.custom1); + PartSys->sources[i].source.x = (spacing >> 1) + spacing * i; //distribute evenly + } + + for (i = 0; i < PartSys->usedParticles; i++) { + if (PartSys->particles[i].ttl > 20) PartSys->particles[i].ttl -= 20; //ttl is linked to brightness, this allows to use higher brightness but still a short lifespan + else PartSys->particles[i].ttl = 0; + } + + um_data_t *um_data; + if (!UsermodManager::getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) + um_data = simulateSound(SEGMENT.soundSim); // add support for no audio + + uint8_t *fftResult = (uint8_t *)um_data->u_data[2]; // 16 bins with FFT data, log mapped already, each band contains frequency amplitude 0-255 + + //map the bands into 16 positions on x axis, emit some particles according to frequency loudness + i = 0; + uint32_t bin = hw_random16(numSources); //current bin , start with random one to distribute available particles fairly + uint32_t threshold = 300 - SEGMENT.intensity; + + for (i = 0; i < numSources; i++) { + bin++; + bin = bin % numSources; + uint32_t emitparticle = 0; + // uint8_t emitspeed = ((uint32_t)fftResult[bin] * (uint32_t)SEGMENT.speed) >> 10; // emit speed according to loudness of band (127 max!) + if (fftResult[bin] > threshold) { + emitparticle = 1; + } + else if (fftResult[bin] > 0) { // band has low volue + uint32_t restvolume = ((threshold - fftResult[bin]) >> 2) + 2; + if (hw_random() % restvolume == 0) { + emitparticle = 1; + } + } + + if (emitparticle) + PartSys->sprayEmit(PartSys->sources[bin]); + } + //TODO: add color control? + + PartSys->update(); // update and render + + return FRAMETIME; +} +static const char _data_FX_MODE_PS_1D_GEQ[] PROGMEM = "PS GEQ 1D@Speed,!,Size,Blur,,,,;,!;!;1f;pal=0,sx=50,ix=200,c1=0,c2=0,c3=0,o1=1,o2=1"; + +/* +Particle based Fire effect +Uses palette for particle color +by DedeHai (Damian Schneider) +*/ +uint16_t mode_particleFire1D(void) { + ParticleSystem1D *PartSys = NULL; - addEffect(FX_MODE_2DGEQ, &mode_2DGEQ, _data_FX_MODE_2DGEQ); // audio + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem1D(PartSys, 5)) // init + return mode_static(); // allocation failed or is single pixel + PartSys->setKillOutOfBounds(true); + PartSys->setParticleSize(1); + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + if (PartSys == NULL) + return mode_static(); // something went wrong, no data! + + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setMotionBlur(128 + (SEGMENT.custom2 >> 1)); // enable motion blur + PartSys->setColorByAge(true); + uint32_t emitparticles = 1; + uint32_t j = hw_random16(); + for (uint i = 0; i < 3; i++) { // 3 base flames TODO: check if this is ok or needs adjustments + if (PartSys->sources[i].source.ttl > 50) + PartSys->sources[i].source.ttl -= 10; // TODO: in 2D making the source fade out slow results in much smoother flames, need to check if it can be done the same + else + PartSys->sources[i].source.ttl = 100 + hw_random16(200); + } + for (uint i = 0; i < PartSys->numSources; i++) { + j = (j + 1) % PartSys->numSources; + PartSys->sources[j].source.x = 0; + PartSys->sources[j].var = 2 + (SEGMENT.speed >> 4); + // base flames + if (j > 2) { + PartSys->sources[j].minLife = 150 + SEGMENT.intensity + (j << 2); // TODO: in 2D, min life is maxlife/2 and that looks very nice + PartSys->sources[j].maxLife = 200 + SEGMENT.intensity + (j << 3); + PartSys->sources[j].v = (SEGMENT.speed >> (2 + (j << 1))); + if (emitparticles) { + emitparticles--; + PartSys->sprayEmit(PartSys->sources[j]); // emit a particle + } + } + else { + PartSys->sources[j].minLife = PartSys->sources[j].source.ttl + SEGMENT.intensity; // TODO: in 2D, emitted particle ttl depends on source TTL, mimic here the same way? OR: change 2D to the same way it is done here and ditch special fire treatment in emit? + PartSys->sources[j].maxLife = PartSys->sources[j].minLife + 50; + PartSys->sources[j].v = SEGMENT.speed >> 2; + if (SEGENV.call & 0x01) // every second frame + PartSys->sprayEmit(PartSys->sources[j]); // emit a particle + } + } - addEffect(FX_MODE_2DNOISE, &mode_2Dnoise, _data_FX_MODE_2DNOISE); + for (uint i = 0; i < PartSys->usedParticles; i++) { + PartSys->particles[i].x += PartSys->particles[i].ttl >> 7; // 'hot' particles are faster, apply some extra velocity + if (PartSys->particles[i].ttl > 3 + ((255 - SEGMENT.custom1) >> 1)) + PartSys->particles[i].ttl -= map(SEGMENT.custom1, 0, 255, 1, 3); // age faster + } + + PartSys->update(); // update and render + + return FRAMETIME; +} +static const char _data_FX_MODE_PS_FIRE1D[] PROGMEM = "PS Fire 1D@!,!,Cooling,Blur;,!;!;1;pal=35,sx=100,ix=50,c1=80,c2=100,c3=28,o1=1,o2=1"; + +/* +Particle based AR effect, swoop particles along the strip with selected frequency loudness +Uses palette for particle color +by DedeHai (Damian Schneider) +*/ + +uint16_t mode_particle1Dsonicstream(void) { + ParticleSystem1D *PartSys = NULL; + + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem1D(PartSys, 1, 255, 0, true)) // init, no additional data needed + return mode_static(); // allocation failed or is single pixel + PartSys->setKillOutOfBounds(true); + PartSys->sources[0].source.x = 0; // at start + //PartSys->sources[1].source.x = PartSys->maxX; // at end + PartSys->sources[0].var = 0;//SEGMENT.custom1 >> 3; + PartSys->sources[0].sat = 255; + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + if (PartSys == NULL) + return mode_static(); // something went wrong, no data! + + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setMotionBlur(20 + (SEGMENT.custom2 >> 1)); // anable motion blur + PartSys->setSmearBlur(200); // smooth out the edges + + PartSys->sources[0].v = 5 + (SEGMENT.speed >> 2); + + // FFT processing + um_data_t *um_data; + if (!UsermodManager::getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) + um_data = simulateSound(SEGMENT.soundSim); // add support for no audio + + uint8_t *fftResult = (uint8_t *)um_data->u_data[2]; // 16 bins with FFT data, log mapped already, each band contains frequency amplitude 0-255 + uint32_t loudness; + uint32_t baseBin = SEGMENT.custom3 >> 1; // 0 - 15 map(SEGMENT.custom3, 0, 31, 0, 14); + + loudness = fftResult[baseBin];// + fftResult[baseBin + 1]; + int mids = sqrt16((int)fftResult[5] + (int)fftResult[6] + (int)fftResult[7] + (int)fftResult[8] + (int)fftResult[9] + (int)fftResult[10]); // average the mids, bin 5 is ~500Hz, bin 10 is ~2kHz (see audio_reactive.h) + if(baseBin > 12) + loudness = loudness << 2; // double loudness for high frequencies (better detecion) + + uint32_t threshold = 150 - (SEGMENT.intensity >> 1); + if(SEGMENT.check2) { // enable low pass filter for dynamic threshold + SEGMENT.step = (SEGMENT.step * 31500 + loudness * (32768 - 31500)) >> 15; // low pass filter for simple beat detection: add average to base threshold + threshold = 20 + (threshold >> 1) + SEGMENT.step; // add average to threshold + } + + // color + uint32_t hueincrement = (SEGMENT.custom1 >> 3); // 0-31 + if(SEGMENT.custom1 < 255) + PartSys->setColorByPosition(false); + else + PartSys->setColorByPosition(true); + + // particle manipulation + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { + if(PartSys->sources[0].sourceFlags.perpetual == false) { // age faster if not perpetual + if (PartSys->particles[i].ttl > 2) { + PartSys->particles[i].ttl -= 2; //ttl is linked to brightness, this allows to use higher brightness but still a short lifespan + } + else PartSys->particles[i].ttl = 0; + } + if(SEGMENT.check1) // modulate colors by mid frequencies + PartSys->particles[i].hue += (mids * inoise8(PartSys->particles[i].x << 2, SEGMENT.step << 2)) >> 9; // color by perlin noise from mid frequencies + } + + if (loudness > threshold) { + SEGMENT.aux0 += hueincrement; // change color + PartSys->sources[0].minLife = 100 + (((unsigned)SEGMENT.intensity * loudness * loudness) >> 13); + PartSys->sources[0].maxLife = PartSys->sources[0].minLife; + PartSys->sources[0].source.hue = SEGMENT.aux0; + PartSys->sources[0].size = SEGMENT.speed; + if(PartSys->particles[SEGMENT.aux1].x > 3 * PS_P_RADIUS_1D || PartSys->particles[SEGMENT.aux1].ttl == 0) { // only emit if last particle is far enough away or dead + int partindex = PartSys->sprayEmit(PartSys->sources[0]); // emit a particle + if(partindex >= 0) SEGMENT.aux1 = partindex; // track last emitted particle + } + } + else loudness = 0; // required for push mode + + PartSys->update(); // update and render (needs to be done before manipulation for initial particle spacing to be right) + + if(SEGMENT.check3) { // push mode + PartSys->sources[0].sourceFlags.perpetual = true; // emitted particles dont age + PartSys->applyFriction(1); //slow down particles + int32_t movestep = (((int)SEGMENT.speed + 2) * loudness) >> 10; + if(movestep) { + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { + if (PartSys->particles[i].ttl) { + PartSys->particles[i].x += movestep; // push particles + PartSys->particles[i].vx = 10 + (SEGMENT.speed >> 4) ; // give particles some speed for smooth movement (friction will slow them down) + } + } + } + } + else { + PartSys->sources[0].sourceFlags.perpetual = false; // emitted particles age + // move all particles (again) to allow faster speeds + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { + if (PartSys->particles[i].vx == 0) + PartSys->particles[i].vx = PartSys->sources[0].v; // move static particles (after disabling push mode) + PartSys->particleMoveUpdate(PartSys->particles[i], PartSys->particleFlags[i], nullptr, &PartSys->advPartProps[i]); + } + } + + return FRAMETIME; +} +static const char _data_FX_MODE_PS_SONICSTREAM[] PROGMEM = "PS Sonic Stream@!,!,Color,Blur,Bin,Mod,Filter,Push;,!;!;1f;c3=0,o2=1"; +#endif // WLED_DISABLE_PARTICLESYSTEM1D + +////////////////////////////////////////////////////////////////////////////////////////// +// mode data +static const char _data_RESERVED[] PROGMEM = "RSVD"; + +// add (or replace reserved) effect mode and data into vector +// use id==255 to find unallocated gaps (with "Reserved" data string) +// if vector size() is smaller than id (single) data is appended at the end (regardless of id) +// return the actual id used for the effect or 255 if the add failed. +uint8_t WS2812FX::addEffect(uint8_t id, mode_ptr mode_fn, const char *mode_name) { + if (id == 255) { // find empty slot + for (size_t i=1; i<_mode.size(); i++) if (_modeData[i] == _data_RESERVED) { id = i; break; } + } + if (id < _mode.size()) { + if (_modeData[id] != _data_RESERVED) return 255; // do not overwrite an already added effect + _mode[id] = mode_fn; + _modeData[id] = mode_name; + return id; + } else if(_mode.size() < 255) { // 255 is reserved for indicating the effect wasn't added + _mode.push_back(mode_fn); + _modeData.push_back(mode_name); + if (_modeCount < _mode.size()) _modeCount++; + return _mode.size() - 1; + } else { + return 255; // The vector is full so return 255 + } +} + +void WS2812FX::setupEffectData() { + // Solid must be first! (assuming vector is empty upon call to setup) + _mode.push_back(&mode_static); + _modeData.push_back(_data_FX_MODE_STATIC); + // fill reserved word in case there will be any gaps in the array + for (size_t i=1; i<_modeCount; i++) { + _mode.push_back(&mode_static); + _modeData.push_back(_data_RESERVED); + } + // now replace all pre-allocated effects + // --- 1D non-audio effects --- + addEffect(FX_MODE_BLINK, &mode_blink, _data_FX_MODE_BLINK); + addEffect(FX_MODE_BREATH, &mode_breath, _data_FX_MODE_BREATH); + addEffect(FX_MODE_COLOR_WIPE, &mode_color_wipe, _data_FX_MODE_COLOR_WIPE); + addEffect(FX_MODE_COLOR_WIPE_RANDOM, &mode_color_wipe_random, _data_FX_MODE_COLOR_WIPE_RANDOM); + addEffect(FX_MODE_RANDOM_COLOR, &mode_random_color, _data_FX_MODE_RANDOM_COLOR); + addEffect(FX_MODE_COLOR_SWEEP, &mode_color_sweep, _data_FX_MODE_COLOR_SWEEP); + addEffect(FX_MODE_DYNAMIC, &mode_dynamic, _data_FX_MODE_DYNAMIC); + addEffect(FX_MODE_RAINBOW, &mode_rainbow, _data_FX_MODE_RAINBOW); + addEffect(FX_MODE_RAINBOW_CYCLE, &mode_rainbow_cycle, _data_FX_MODE_RAINBOW_CYCLE); + addEffect(FX_MODE_SCAN, &mode_scan, _data_FX_MODE_SCAN); + addEffect(FX_MODE_DUAL_SCAN, &mode_dual_scan, _data_FX_MODE_DUAL_SCAN); + addEffect(FX_MODE_FADE, &mode_fade, _data_FX_MODE_FADE); + addEffect(FX_MODE_THEATER_CHASE, &mode_theater_chase, _data_FX_MODE_THEATER_CHASE); + addEffect(FX_MODE_THEATER_CHASE_RAINBOW, &mode_theater_chase_rainbow, _data_FX_MODE_THEATER_CHASE_RAINBOW); + addEffect(FX_MODE_RUNNING_LIGHTS, &mode_running_lights, _data_FX_MODE_RUNNING_LIGHTS); + addEffect(FX_MODE_SAW, &mode_saw, _data_FX_MODE_SAW); + addEffect(FX_MODE_TWINKLE, &mode_twinkle, _data_FX_MODE_TWINKLE); + addEffect(FX_MODE_DISSOLVE, &mode_dissolve, _data_FX_MODE_DISSOLVE); + addEffect(FX_MODE_DISSOLVE_RANDOM, &mode_dissolve_random, _data_FX_MODE_DISSOLVE_RANDOM); + addEffect(FX_MODE_FLASH_SPARKLE, &mode_flash_sparkle, _data_FX_MODE_FLASH_SPARKLE); + addEffect(FX_MODE_HYPER_SPARKLE, &mode_hyper_sparkle, _data_FX_MODE_HYPER_SPARKLE); + addEffect(FX_MODE_STROBE, &mode_strobe, _data_FX_MODE_STROBE); + addEffect(FX_MODE_STROBE_RAINBOW, &mode_strobe_rainbow, _data_FX_MODE_STROBE_RAINBOW); + addEffect(FX_MODE_MULTI_STROBE, &mode_multi_strobe, _data_FX_MODE_MULTI_STROBE); + addEffect(FX_MODE_BLINK_RAINBOW, &mode_blink_rainbow, _data_FX_MODE_BLINK_RAINBOW); + addEffect(FX_MODE_ANDROID, &mode_android, _data_FX_MODE_ANDROID); + addEffect(FX_MODE_CHASE_COLOR, &mode_chase_color, _data_FX_MODE_CHASE_COLOR); + addEffect(FX_MODE_CHASE_RANDOM, &mode_chase_random, _data_FX_MODE_CHASE_RANDOM); + addEffect(FX_MODE_CHASE_RAINBOW, &mode_chase_rainbow, _data_FX_MODE_CHASE_RAINBOW); + addEffect(FX_MODE_CHASE_FLASH, &mode_chase_flash, _data_FX_MODE_CHASE_FLASH); + addEffect(FX_MODE_CHASE_FLASH_RANDOM, &mode_chase_flash_random, _data_FX_MODE_CHASE_FLASH_RANDOM); + addEffect(FX_MODE_CHASE_RAINBOW_WHITE, &mode_chase_rainbow_white, _data_FX_MODE_CHASE_RAINBOW_WHITE); + addEffect(FX_MODE_COLORFUL, &mode_colorful, _data_FX_MODE_COLORFUL); + addEffect(FX_MODE_TRAFFIC_LIGHT, &mode_traffic_light, _data_FX_MODE_TRAFFIC_LIGHT); + addEffect(FX_MODE_COLOR_SWEEP_RANDOM, &mode_color_sweep_random, _data_FX_MODE_COLOR_SWEEP_RANDOM); + addEffect(FX_MODE_RUNNING_COLOR, &mode_running_color, _data_FX_MODE_RUNNING_COLOR); + addEffect(FX_MODE_AURORA, &mode_aurora, _data_FX_MODE_AURORA); + addEffect(FX_MODE_RUNNING_RANDOM, &mode_running_random, _data_FX_MODE_RUNNING_RANDOM); + addEffect(FX_MODE_LARSON_SCANNER, &mode_larson_scanner, _data_FX_MODE_LARSON_SCANNER); + addEffect(FX_MODE_RAIN, &mode_rain, _data_FX_MODE_RAIN); + addEffect(FX_MODE_PRIDE_2015, &mode_pride_2015, _data_FX_MODE_PRIDE_2015); + addEffect(FX_MODE_COLORWAVES, &mode_colorwaves, _data_FX_MODE_COLORWAVES); + addEffect(FX_MODE_FIREWORKS, &mode_fireworks, _data_FX_MODE_FIREWORKS); + addEffect(FX_MODE_TETRIX, &mode_tetrix, _data_FX_MODE_TETRIX); + addEffect(FX_MODE_FIRE_FLICKER, &mode_fire_flicker, _data_FX_MODE_FIRE_FLICKER); + addEffect(FX_MODE_GRADIENT, &mode_gradient, _data_FX_MODE_GRADIENT); + addEffect(FX_MODE_LOADING, &mode_loading, _data_FX_MODE_LOADING); + addEffect(FX_MODE_FAIRY, &mode_fairy, _data_FX_MODE_FAIRY); + addEffect(FX_MODE_TWO_DOTS, &mode_two_dots, _data_FX_MODE_TWO_DOTS); + addEffect(FX_MODE_FAIRYTWINKLE, &mode_fairytwinkle, _data_FX_MODE_FAIRYTWINKLE); + addEffect(FX_MODE_RUNNING_DUAL, &mode_running_dual, _data_FX_MODE_RUNNING_DUAL); + addEffect(FX_MODE_TRICOLOR_CHASE, &mode_tricolor_chase, _data_FX_MODE_TRICOLOR_CHASE); + addEffect(FX_MODE_TRICOLOR_WIPE, &mode_tricolor_wipe, _data_FX_MODE_TRICOLOR_WIPE); + addEffect(FX_MODE_TRICOLOR_FADE, &mode_tricolor_fade, _data_FX_MODE_TRICOLOR_FADE); + addEffect(FX_MODE_LIGHTNING, &mode_lightning, _data_FX_MODE_LIGHTNING); + addEffect(FX_MODE_ICU, &mode_icu, _data_FX_MODE_ICU); + addEffect(FX_MODE_MULTI_COMET, &mode_multi_comet, _data_FX_MODE_MULTI_COMET); + addEffect(FX_MODE_DUAL_LARSON_SCANNER, &mode_dual_larson_scanner, _data_FX_MODE_DUAL_LARSON_SCANNER); + addEffect(FX_MODE_RANDOM_CHASE, &mode_random_chase, _data_FX_MODE_RANDOM_CHASE); + addEffect(FX_MODE_OSCILLATE, &mode_oscillate, _data_FX_MODE_OSCILLATE); + addEffect(FX_MODE_JUGGLE, &mode_juggle, _data_FX_MODE_JUGGLE); + addEffect(FX_MODE_PALETTE, &mode_palette, _data_FX_MODE_PALETTE); + addEffect(FX_MODE_BPM, &mode_bpm, _data_FX_MODE_BPM); + addEffect(FX_MODE_FILLNOISE8, &mode_fillnoise8, _data_FX_MODE_FILLNOISE8); + addEffect(FX_MODE_NOISE16_1, &mode_noise16_1, _data_FX_MODE_NOISE16_1); + addEffect(FX_MODE_NOISE16_2, &mode_noise16_2, _data_FX_MODE_NOISE16_2); + addEffect(FX_MODE_NOISE16_3, &mode_noise16_3, _data_FX_MODE_NOISE16_3); + addEffect(FX_MODE_NOISE16_4, &mode_noise16_4, _data_FX_MODE_NOISE16_4); + addEffect(FX_MODE_COLORTWINKLE, &mode_colortwinkle, _data_FX_MODE_COLORTWINKLE); + addEffect(FX_MODE_LAKE, &mode_lake, _data_FX_MODE_LAKE); + addEffect(FX_MODE_METEOR, &mode_meteor, _data_FX_MODE_METEOR); + //addEffect(FX_MODE_METEOR_SMOOTH, &mode_meteor_smooth, _data_FX_MODE_METEOR_SMOOTH); // merged with mode_meteor + addEffect(FX_MODE_RAILWAY, &mode_railway, _data_FX_MODE_RAILWAY); + addEffect(FX_MODE_RIPPLE, &mode_ripple, _data_FX_MODE_RIPPLE); + addEffect(FX_MODE_TWINKLEFOX, &mode_twinklefox, _data_FX_MODE_TWINKLEFOX); + addEffect(FX_MODE_TWINKLECAT, &mode_twinklecat, _data_FX_MODE_TWINKLECAT); + addEffect(FX_MODE_HALLOWEEN_EYES, &mode_halloween_eyes, _data_FX_MODE_HALLOWEEN_EYES); + addEffect(FX_MODE_STATIC_PATTERN, &mode_static_pattern, _data_FX_MODE_STATIC_PATTERN); + addEffect(FX_MODE_TRI_STATIC_PATTERN, &mode_tri_static_pattern, _data_FX_MODE_TRI_STATIC_PATTERN); + addEffect(FX_MODE_SPOTS, &mode_spots, _data_FX_MODE_SPOTS); + addEffect(FX_MODE_SPOTS_FADE, &mode_spots_fade, _data_FX_MODE_SPOTS_FADE); + #ifdef WLED_PS_DONT_REPLACE_FX + addEffect(FX_MODE_COMET, &mode_comet, _data_FX_MODE_COMET); + addEffect(FX_MODE_ROLLINGBALLS, &rolling_balls, _data_FX_MODE_ROLLINGBALLS); + addEffect(FX_MODE_SPARKLE, &mode_sparkle, _data_FX_MODE_SPARKLE); + addEffect(FX_MODE_GLITTER, &mode_glitter, _data_FX_MODE_GLITTER); + addEffect(FX_MODE_SOLID_GLITTER, &mode_solid_glitter, _data_FX_MODE_SOLID_GLITTER); + addEffect(FX_MODE_STARBURST, &mode_starburst, _data_FX_MODE_STARBURST); + addEffect(FX_MODE_DANCING_SHADOWS, &mode_dancing_shadows, _data_FX_MODE_DANCING_SHADOWS); + addEffect(FX_MODE_FIRE_2012, &mode_fire_2012, _data_FX_MODE_FIRE_2012); + addEffect(FX_MODE_EXPLODING_FIREWORKS, &mode_exploding_fireworks, _data_FX_MODE_EXPLODING_FIREWORKS); + #endif + addEffect(FX_MODE_CANDLE, &mode_candle, _data_FX_MODE_CANDLE); + addEffect(FX_MODE_BOUNCINGBALLS, &mode_bouncing_balls, _data_FX_MODE_BOUNCINGBALLS); + addEffect(FX_MODE_POPCORN, &mode_popcorn, _data_FX_MODE_POPCORN); + addEffect(FX_MODE_DRIP, &mode_drip, _data_FX_MODE_DRIP); + addEffect(FX_MODE_SINELON, &mode_sinelon, _data_FX_MODE_SINELON); + addEffect(FX_MODE_SINELON_DUAL, &mode_sinelon_dual, _data_FX_MODE_SINELON_DUAL); + addEffect(FX_MODE_SINELON_RAINBOW, &mode_sinelon_rainbow, _data_FX_MODE_SINELON_RAINBOW); + addEffect(FX_MODE_PLASMA, &mode_plasma, _data_FX_MODE_PLASMA); + addEffect(FX_MODE_PERCENT, &mode_percent, _data_FX_MODE_PERCENT); + addEffect(FX_MODE_RIPPLE_RAINBOW, &mode_ripple_rainbow, _data_FX_MODE_RIPPLE_RAINBOW); + addEffect(FX_MODE_HEARTBEAT, &mode_heartbeat, _data_FX_MODE_HEARTBEAT); + addEffect(FX_MODE_PACIFICA, &mode_pacifica, _data_FX_MODE_PACIFICA); + addEffect(FX_MODE_CANDLE_MULTI, &mode_candle_multi, _data_FX_MODE_CANDLE_MULTI); + addEffect(FX_MODE_SUNRISE, &mode_sunrise, _data_FX_MODE_SUNRISE); + addEffect(FX_MODE_PHASED, &mode_phased, _data_FX_MODE_PHASED); + addEffect(FX_MODE_TWINKLEUP, &mode_twinkleup, _data_FX_MODE_TWINKLEUP); + addEffect(FX_MODE_NOISEPAL, &mode_noisepal, _data_FX_MODE_NOISEPAL); + addEffect(FX_MODE_SINEWAVE, &mode_sinewave, _data_FX_MODE_SINEWAVE); + addEffect(FX_MODE_PHASEDNOISE, &mode_phased_noise, _data_FX_MODE_PHASEDNOISE); + addEffect(FX_MODE_FLOW, &mode_flow, _data_FX_MODE_FLOW); + addEffect(FX_MODE_CHUNCHUN, &mode_chunchun, _data_FX_MODE_CHUNCHUN); + addEffect(FX_MODE_WASHING_MACHINE, &mode_washing_machine, _data_FX_MODE_WASHING_MACHINE); + addEffect(FX_MODE_BLENDS, &mode_blends, _data_FX_MODE_BLENDS); + addEffect(FX_MODE_TV_SIMULATOR, &mode_tv_simulator, _data_FX_MODE_TV_SIMULATOR); + addEffect(FX_MODE_DYNAMIC_SMOOTH, &mode_dynamic_smooth, _data_FX_MODE_DYNAMIC_SMOOTH); + + // --- 1D audio effects --- + addEffect(FX_MODE_PIXELS, &mode_pixels, _data_FX_MODE_PIXELS); + addEffect(FX_MODE_PIXELWAVE, &mode_pixelwave, _data_FX_MODE_PIXELWAVE); + addEffect(FX_MODE_JUGGLES, &mode_juggles, _data_FX_MODE_JUGGLES); + addEffect(FX_MODE_MATRIPIX, &mode_matripix, _data_FX_MODE_MATRIPIX); + addEffect(FX_MODE_GRAVIMETER, &mode_gravimeter, _data_FX_MODE_GRAVIMETER); + addEffect(FX_MODE_PLASMOID, &mode_plasmoid, _data_FX_MODE_PLASMOID); + addEffect(FX_MODE_PUDDLES, &mode_puddles, _data_FX_MODE_PUDDLES); + addEffect(FX_MODE_MIDNOISE, &mode_midnoise, _data_FX_MODE_MIDNOISE); + addEffect(FX_MODE_NOISEMETER, &mode_noisemeter, _data_FX_MODE_NOISEMETER); + addEffect(FX_MODE_FREQWAVE, &mode_freqwave, _data_FX_MODE_FREQWAVE); + addEffect(FX_MODE_FREQMATRIX, &mode_freqmatrix, _data_FX_MODE_FREQMATRIX); + addEffect(FX_MODE_WATERFALL, &mode_waterfall, _data_FX_MODE_WATERFALL); + addEffect(FX_MODE_FREQPIXELS, &mode_freqpixels, _data_FX_MODE_FREQPIXELS); + addEffect(FX_MODE_NOISEFIRE, &mode_noisefire, _data_FX_MODE_NOISEFIRE); + addEffect(FX_MODE_PUDDLEPEAK, &mode_puddlepeak, _data_FX_MODE_PUDDLEPEAK); + addEffect(FX_MODE_NOISEMOVE, &mode_noisemove, _data_FX_MODE_NOISEMOVE); + addEffect(FX_MODE_PERLINMOVE, &mode_perlinmove, _data_FX_MODE_PERLINMOVE); + addEffect(FX_MODE_RIPPLEPEAK, &mode_ripplepeak, _data_FX_MODE_RIPPLEPEAK); + addEffect(FX_MODE_FREQMAP, &mode_freqmap, _data_FX_MODE_FREQMAP); + addEffect(FX_MODE_GRAVCENTER, &mode_gravcenter, _data_FX_MODE_GRAVCENTER); + addEffect(FX_MODE_GRAVCENTRIC, &mode_gravcentric, _data_FX_MODE_GRAVCENTRIC); + addEffect(FX_MODE_GRAVFREQ, &mode_gravfreq, _data_FX_MODE_GRAVFREQ); + addEffect(FX_MODE_DJLIGHT, &mode_DJLight, _data_FX_MODE_DJLIGHT); + addEffect(FX_MODE_BLURZ, &mode_blurz, _data_FX_MODE_BLURZ); + addEffect(FX_MODE_FLOWSTRIPE, &mode_FlowStripe, _data_FX_MODE_FLOWSTRIPE); + addEffect(FX_MODE_WAVESINS, &mode_wavesins, _data_FX_MODE_WAVESINS); + addEffect(FX_MODE_ROCKTAVES, &mode_rocktaves, _data_FX_MODE_ROCKTAVES); + + // --- 2D effects --- +#ifndef WLED_DISABLE_2D + addEffect(FX_MODE_2DPLASMAROTOZOOM, &mode_2Dplasmarotozoom, _data_FX_MODE_2DPLASMAROTOZOOM); + addEffect(FX_MODE_2DSPACESHIPS, &mode_2Dspaceships, _data_FX_MODE_2DSPACESHIPS); + addEffect(FX_MODE_2DCRAZYBEES, &mode_2Dcrazybees, _data_FX_MODE_2DCRAZYBEES); + #ifdef WLED_PS_DONT_REPLACE_FX + addEffect(FX_MODE_2DGHOSTRIDER, &mode_2Dghostrider, _data_FX_MODE_2DGHOSTRIDER); + addEffect(FX_MODE_2DBLOBS, &mode_2Dfloatingblobs, _data_FX_MODE_2DBLOBS); + #endif + + addEffect(FX_MODE_2DSCROLLTEXT, &mode_2Dscrollingtext, _data_FX_MODE_2DSCROLLTEXT); + addEffect(FX_MODE_2DDRIFTROSE, &mode_2Ddriftrose, _data_FX_MODE_2DDRIFTROSE); + addEffect(FX_MODE_2DDISTORTIONWAVES, &mode_2Ddistortionwaves, _data_FX_MODE_2DDISTORTIONWAVES); + addEffect(FX_MODE_2DGEQ, &mode_2DGEQ, _data_FX_MODE_2DGEQ); // audio + addEffect(FX_MODE_2DNOISE, &mode_2Dnoise, _data_FX_MODE_2DNOISE); addEffect(FX_MODE_2DFIRENOISE, &mode_2Dfirenoise, _data_FX_MODE_2DFIRENOISE); addEffect(FX_MODE_2DSQUAREDSWIRL, &mode_2Dsquaredswirl, _data_FX_MODE_2DSQUAREDSWIRL); @@ -7849,15 +10319,12 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_2DMATRIX, &mode_2Dmatrix, _data_FX_MODE_2DMATRIX); addEffect(FX_MODE_2DMETABALLS, &mode_2Dmetaballs, _data_FX_MODE_2DMETABALLS); addEffect(FX_MODE_2DFUNKYPLANK, &mode_2DFunkyPlank, _data_FX_MODE_2DFUNKYPLANK); // audio - addEffect(FX_MODE_2DPULSER, &mode_2DPulser, _data_FX_MODE_2DPULSER); - addEffect(FX_MODE_2DDRIFT, &mode_2DDrift, _data_FX_MODE_2DDRIFT); addEffect(FX_MODE_2DWAVERLY, &mode_2DWaverly, _data_FX_MODE_2DWAVERLY); // audio addEffect(FX_MODE_2DSUNRADIATION, &mode_2DSunradiation, _data_FX_MODE_2DSUNRADIATION); addEffect(FX_MODE_2DCOLOREDBURSTS, &mode_2DColoredBursts, _data_FX_MODE_2DCOLOREDBURSTS); addEffect(FX_MODE_2DJULIA, &mode_2DJulia, _data_FX_MODE_2DJULIA); - addEffect(FX_MODE_2DGAMEOFLIFE, &mode_2Dgameoflife, _data_FX_MODE_2DGAMEOFLIFE); addEffect(FX_MODE_2DTARTAN, &mode_2Dtartan, _data_FX_MODE_2DTARTAN); addEffect(FX_MODE_2DPOLARLIGHTS, &mode_2DPolarLights, _data_FX_MODE_2DPOLARLIGHTS); @@ -7865,7 +10332,6 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_2DLISSAJOUS, &mode_2DLissajous, _data_FX_MODE_2DLISSAJOUS); addEffect(FX_MODE_2DFRIZZLES, &mode_2DFrizzles, _data_FX_MODE_2DFRIZZLES); addEffect(FX_MODE_2DPLASMABALL, &mode_2DPlasmaball, _data_FX_MODE_2DPLASMABALL); - addEffect(FX_MODE_2DHIPHOTIC, &mode_2DHiphotic, _data_FX_MODE_2DHIPHOTIC); addEffect(FX_MODE_2DSINDOTS, &mode_2DSindots, _data_FX_MODE_2DSINDOTS); addEffect(FX_MODE_2DDNASPIRAL, &mode_2DDNASpiral, _data_FX_MODE_2DDNASPIRAL); @@ -7873,8 +10339,41 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_2DSOAP, &mode_2Dsoap, _data_FX_MODE_2DSOAP); addEffect(FX_MODE_2DOCTOPUS, &mode_2Doctopus, _data_FX_MODE_2DOCTOPUS); addEffect(FX_MODE_2DWAVINGCELL, &mode_2Dwavingcell, _data_FX_MODE_2DWAVINGCELL); - addEffect(FX_MODE_2DAKEMI, &mode_2DAkemi, _data_FX_MODE_2DAKEMI); // audio + +#ifndef WLED_DISABLE_PARTICLESYSTEM2D + 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_PARTICLEVORTEX, &mode_particlevortex, _data_FX_MODE_PARTICLEVORTEX); + addEffect(FX_MODE_PARTICLEPERLIN, &mode_particleperlin, _data_FX_MODE_PARTICLEPERLIN); + addEffect(FX_MODE_PARTICLEPIT, &mode_particlepit, _data_FX_MODE_PARTICLEPIT); + addEffect(FX_MODE_PARTICLEBOX, &mode_particlebox, _data_FX_MODE_PARTICLEBOX); + addEffect(FX_MODE_PARTICLEATTRACTOR, &mode_particleattractor, _data_FX_MODE_PARTICLEATTRACTOR); // 872 bytes + addEffect(FX_MODE_PARTICLEIMPACT, &mode_particleimpact, _data_FX_MODE_PARTICLEIMPACT); + addEffect(FX_MODE_PARTICLEWATERFALL, &mode_particlewaterfall, _data_FX_MODE_PARTICLEWATERFALL); + 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_PARTICLECIRCULARGEQ); + addEffect(FX_MODE_PARTICLEGHOSTRIDER, &mode_particleghostrider, _data_FX_MODE_PARTICLEGHOSTRIDER); + addEffect(FX_MODE_PARTICLEBLOBS, &mode_particleblobs, _data_FX_MODE_PARTICLEBLOBS); +#endif // WLED_DISABLE_PARTICLESYSTEM2D #endif // WLED_DISABLE_2D +#ifndef WLED_DISABLE_PARTICLESYSTEM1D +addEffect(FX_MODE_PSDRIP, &mode_particleDrip, _data_FX_MODE_PARTICLEDRIP); +addEffect(FX_MODE_PSPINBALL, &mode_particlePinball, _data_FX_MODE_PSPINBALL); //potential replacement for: bouncing balls, rollingballs, popcorn +addEffect(FX_MODE_PSDANCINGSHADOWS, &mode_particleDancingShadows, _data_FX_MODE_PARTICLEDANCINGSHADOWS); +addEffect(FX_MODE_PSFIREWORKS1D, &mode_particleFireworks1D, _data_FX_MODE_PS_FIREWORKS1D); +addEffect(FX_MODE_PSSPARKLER, &mode_particleSparkler, _data_FX_MODE_PS_SPARKLER); +addEffect(FX_MODE_PSHOURGLASS, &mode_particleHourglass, _data_FX_MODE_PS_HOURGLASS); +addEffect(FX_MODE_PS1DSPRAY, &mode_particle1Dspray, _data_FX_MODE_PS_1DSPRAY); +addEffect(FX_MODE_PSBALANCE, &mode_particleBalance, _data_FX_MODE_PS_BALANCE); +addEffect(FX_MODE_PSCHASE, &mode_particleChase, _data_FX_MODE_PS_CHASE); +addEffect(FX_MODE_PSSTARBURST, &mode_particleStarburst, _data_FX_MODE_PS_STARBURST); +addEffect(FX_MODE_PS1DGEQ, &mode_particle1DGEQ, _data_FX_MODE_PS_1D_GEQ); +addEffect(FX_MODE_PSFIRE1D, &mode_particleFire1D, _data_FX_MODE_PS_FIRE1D); +addEffect(FX_MODE_PS1DSONICSTREAM, &mode_particle1Dsonicstream, _data_FX_MODE_PS_SONICSTREAM); +#endif // WLED_DISABLE_PARTICLESYSTEM1D + } diff --git a/wled00/FX.h b/wled00/FX.h index f54b3ba5c7..7532100d8a 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -322,8 +322,36 @@ extern byte realtimeMode; // used in getMappedPixelIndex() #define FX_MODE_WAVESINS 184 #define FX_MODE_ROCKTAVES 185 #define FX_MODE_2DAKEMI 186 - -#define MODE_COUNT 187 +#define FX_MODE_PARTICLEVOLCANO 187 +#define FX_MODE_PARTICLEFIRE 188 +#define FX_MODE_PARTICLEFIREWORKS 189 +#define FX_MODE_PARTICLEVORTEX 190 +#define FX_MODE_PARTICLEPERLIN 191 +#define FX_MODE_PARTICLEPIT 192 +#define FX_MODE_PARTICLEBOX 193 +#define FX_MODE_PARTICLEATTRACTOR 194 +#define FX_MODE_PARTICLEIMPACT 195 +#define FX_MODE_PARTICLEWATERFALL 196 +#define FX_MODE_PARTICLESPRAY 197 +#define FX_MODE_PARTICLESGEQ 198 +#define FX_MODE_PARTICLECENTERGEQ 199 +#define FX_MODE_PARTICLEGHOSTRIDER 200 +#define FX_MODE_PARTICLEBLOBS 201 +#define FX_MODE_PSDRIP 202 +#define FX_MODE_PSPINBALL 203 +#define FX_MODE_PSDANCINGSHADOWS 204 +#define FX_MODE_PSFIREWORKS1D 205 +#define FX_MODE_PSSPARKLER 206 +#define FX_MODE_PSHOURGLASS 207 +#define FX_MODE_PS1DSPRAY 208 +#define FX_MODE_PSBALANCE 209 +#define FX_MODE_PSCHASE 210 +#define FX_MODE_PSSTARBURST 211 +#define FX_MODE_PS1DGEQ 212 +#define FX_MODE_PSFIRE1D 213 +#define FX_MODE_PS1DSONICSTREAM 214 +//#define FX_MODE_PSFRACTAL 215 +#define MODE_COUNT 215 typedef enum mapping1D2D { M12_Pixels = 0, @@ -443,20 +471,22 @@ typedef struct Segment { #ifndef WLED_DISABLE_MODE_BLEND tmpsegd_t _segT; // previous segment environment uint8_t _modeT; // previous mode/effect + uint8_t _palette; // previous palette #else uint32_t _colorT[NUM_COLORS]; #endif uint8_t _briT; // temporary brightness uint8_t _cctT; // temporary CCT CRGBPalette16 _palT; // temporary palette - uint8_t _prevPaletteBlends; // number of previous palette blends (there are max 255 blends possible) unsigned long _start; // must accommodate millis() uint16_t _dur; + uint8_t _prevPaletteBlends; // number of previous palette blends (there are max 255 blends possible) + // -> here is one byte of padding Transition(uint16_t dur=750) : _palT(CRGBPalette16(CRGB::Black)) - , _prevPaletteBlends(0) , _start(millis()) , _dur(dur) + , _prevPaletteBlends(0) {} } *_t; @@ -548,6 +578,7 @@ typedef struct Segment { inline static void addUsedSegmentData(int len) { Segment::_usedSegmentData += len; } #ifndef WLED_DISABLE_MODE_BLEND inline static void modeBlend(bool blend) { _modeBlend = blend; } + inline static bool getmodeBlend(void) { return _modeBlend; } #endif inline static unsigned vLength() { return Segment::_vLength; } inline static unsigned vWidth() { return Segment::_vWidth; } @@ -595,6 +626,7 @@ typedef struct Segment { uint8_t currentMode() const; // currently active effect/mode (while in transition) [[gnu::hot]] uint32_t currentColor(uint8_t slot) const; // currently active segment color (blended while in transition) CRGBPalette16 &loadPalette(CRGBPalette16 &tgt, uint8_t pal); + void setCurrentPalette(bool loadOldPalette = false); // 1D strip [[gnu::hot]] uint16_t virtualLength() const; @@ -977,4 +1009,4 @@ class WS2812FX { // 96 bytes extern const char JSON_mode_names[]; extern const char JSON_palette_names[]; -#endif +#endif \ No newline at end of file diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index 5ad2314df8..dfb170dec3 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -11,6 +11,7 @@ */ #include "wled.h" #include "FX.h" +#include "FXparticleSystem.h" // TODO: better define the required function (mem service) in FX.h? #include "palettes.h" /* @@ -258,6 +259,7 @@ void Segment::startTransition(uint16_t dur) { //DEBUG_PRINTF_P(PSTR("-- Started transition: %p (%p)\n"), this, _t); loadPalette(_t->_palT, palette); + _t->_palette = palette; _t->_briT = on ? opacity : 0; _t->_cctT = cct; #ifndef WLED_DISABLE_MODE_BLEND @@ -423,13 +425,22 @@ void Segment::beginDraw() { _currentColors[i] = gamma32(col); } // load palette into _currentPalette + setCurrentPalette(); +} + +void Segment::setCurrentPalette(bool loadOldPalette) { + if(loadOldPalette && isInTransition()) { + loadPalette(_currentPalette, _t->_palette); // load palette of old effect, used in particle system + return; + } loadPalette(_currentPalette, palette); - if (strip.paletteFade && prog < 0xFFFFU) { + + if(strip.paletteFade && isInTransition() && progress() < 0xFFFFU) { // blend palettes // there are about 255 blend passes of 48 "blends" to completely blend two palettes (in _dur time) // minimum blend time is 100ms maximum is 65535ms - unsigned noOfBlends = ((255U * prog) / 0xFFFFU) - _t->_prevPaletteBlends; - for (unsigned i = 0; i < noOfBlends; i++, _t->_prevPaletteBlends++) nblendPaletteTowardPalette(_t->_palT, _currentPalette, 48); + int noOfBlends = ((255U * progress()) / 0xFFFFU) - _t->_prevPaletteBlends; + for (int i = 0; i < noOfBlends; i++, _t->_prevPaletteBlends++) nblendPaletteTowardPalette(_t->_palT, _currentPalette, 48); _currentPalette = _t->_palT; // copy transitioning/temporary palette } } @@ -1397,6 +1408,9 @@ void WS2812FX::service() { } _segment_index++; } + #if !(defined(WLED_DISABLE_PARTICLESYSTEM2D) && defined(WLED_DISABLE_PARTICLESYSTEM1D)) + servicePSmem(); // handle segment particle system memory + #endif _isServicing = false; _triggered = false; @@ -1841,4 +1855,4 @@ const char JSON_palette_names[] PROGMEM = R"=====([ "Aurora","Atlantica","C9 2","C9 New","Temperature","Aurora 2","Retro Clown","Candy","Toxy Reaf","Fairy Reaf", "Semi Blue","Pink Candy","Red Reaf","Aqua Flash","Yelblu Hot","Lite Light","Red Flash","Blink Red","Red Shift","Red Tide", "Candy2","Traffic Light" -])====="; +])====="; \ No newline at end of file diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp new file mode 100644 index 0000000000..755e322c15 --- /dev/null +++ b/wled00/FXparticleSystem.cpp @@ -0,0 +1,2334 @@ +/* + FXparticleSystem.cpp + + Particle system with functions for particle generation, particle movement and particle rendering to RGB matrix. + by DedeHai (Damian Schneider) 2013-2024 + + Copyright (c) 2024 Damian Schneider + Licensed under the EUPL v. 1.2 or later +*/ + +#ifdef WLED_DISABLE_2D +#define WLED_DISABLE_PARTICLESYSTEM2D +#endif + +#if !(defined(WLED_DISABLE_PARTICLESYSTEM2D) && defined(WLED_DISABLE_PARTICLESYSTEM1D)) // not both disabled +#include "FXparticleSystem.h" + +// local shared functions (used both in 1D and 2D system) +static int32_t calcForce_dv(const int8_t force, uint8_t &counter); +static bool checkBoundsAndWrap(int32_t &position, const int32_t max, const int32_t particleradius, const bool wrap); // returns false if out of bounds by more than particleradius +static void fast_color_add(CRGB &c1, const CRGB &c2, uint32_t scale = 255); // fast and accurate color adding with scaling (scales c2 before adding) +static void fast_color_scale(CRGB &c, const uint32_t scale); // fast scaling function using 32bit variable and pointer. note: keep 'scale' within 0-255 +//static CRGB *allocateCRGBbuffer(uint32_t length); + +// global variables for memory management +std::vector partMemList; // list of particle memory pointers +partMem *pmem = nullptr; // pointer to particle memory of current segment, updated in particleMemoryManager() +CRGB *framebuffer = nullptr; // local frame buffer for rendering +CRGB *renderbuffer = nullptr; // local particle render buffer for advanced particles +uint16_t frameBufferSize = 0; // size in pixels, used to check if framebuffer is large enough for current segment +uint16_t renderBufferSize = 0; // size in pixels, if allcoated by a 1D system it needs to be updated for 2D +bool renderSolo = false; // is set to true if this is the only particle system using the so it can use the buffer continuously (faster blurring) +int32_t globalBlur = 0; // motion blur to apply if multiple PS are using the buffer +int32_t globalSmear = 0; // smear-blur to apply if multiple PS are using the buffer +#endif + +#ifndef WLED_DISABLE_PARTICLESYSTEM2D +ParticleSystem2D::ParticleSystem2D(uint32_t width, uint32_t height, uint32_t numberofparticles, uint32_t numberofsources, bool isadvanced, bool sizecontrol) { + PSPRINTLN("\n ParticleSystem2D constructor"); + effectID = SEGMENT.mode; // new FX called init, save the effect ID + numSources = numberofsources; // number of sources allocated in init + numParticles = numberofparticles; // number of particles allocated in init + availableParticles = 0; // let the memory manager assign + fractionOfParticlesUsed = 255; // use all particles by default, usedParticles is updated in updatePSpointers() + advPartProps = NULL; //make sure we start out with null pointers (just in case memory was not cleared) + advPartSize = NULL; + updatePSpointers(isadvanced, sizecontrol); // set the particle and sources pointer (call this before accessing sprays or particles) + setMatrixSize(width, height); + setWallHardness(255); // set default wall hardness to max + setWallRoughness(0); // smooth walls by default + setGravity(0); //gravity disabled by default + setParticleSize(0); // minimum size by default + motionBlur = 0; //no fading by default + smearBlur = 0; //no smearing by default + emitIndex = 0; + collisionStartIdx = 0; + + //initialize some default non-zero values most FX use + for (uint32_t i = 0; i < numSources; i++) { + sources[i].source.sat = 255; //set saturation to max by default + sources[i].source.ttl = 1; //set source alive + } + +} + +// update function applies gravity, moves the particles, handles collisions and renders the particles +void ParticleSystem2D::update(void) { + PSadvancedParticle *advprop = NULL; + //apply gravity globally if enabled + if (particlesettings.useGravity) + applyGravity(); + + //update size settings before handling collisions + if (advPartSize) { + for (uint32_t i = 0; i < usedParticles; i++) { + if(updateSize(&advPartProps[i], &advPartSize[i]) == false) { // if particle shrinks to 0 size + particles[i].ttl = 0; // kill particle + } + } + } + + // handle collisions (can push particles, must be done before updating particles or they can render out of bounds, causing a crash if using local buffer for speed) + if (particlesettings.useCollisions) + handleCollisions(); + + //move all particles + for (uint32_t i = 0; i < usedParticles; i++) { + particleMoveUpdate(particles[i], particleFlags[i], nullptr, advPartProps ? &advPartProps[i] : nullptr); // note: splitting this into two loops is slower and uses more flash + } + + ParticleSys_render(); +} + +// update function for fire animation +void ParticleSystem2D::updateFire(const uint8_t intensity,const bool renderonly) { + if (!renderonly) + fireParticleupdate(); + fireIntesity = intensity > 0 ? intensity : 1; // minimum of 1, zero checking is used in render function + ParticleSys_render(); +} + +// set percentage of used particles as uint8_t i.e 127 means 50% for example +void ParticleSystem2D::setUsedParticles(uint8_t percentage) { + fractionOfParticlesUsed = percentage; // note usedParticles is updated in memory manager + updateUsedParticles(numParticles, availableParticles, fractionOfParticlesUsed, usedParticles); + PSPRINT(" SetUsedpaticles: allocated particles: "); + PSPRINT(numParticles); + PSPRINT(" available particles: "); + PSPRINT(availableParticles); + PSPRINT(" ,used percentage: "); + PSPRINT(fractionOfParticlesUsed); + PSPRINT(" ,used particles: "); + PSPRINTLN(usedParticles); +} + +void ParticleSystem2D::setWallHardness(uint8_t hardness) { + wallHardness = hardness; +} + +void ParticleSystem2D::setWallRoughness(uint8_t roughness) { + wallRoughness = roughness; +} + +void ParticleSystem2D::setCollisionHardness(uint8_t hardness) { + collisionHardness = (int)hardness + 1; +} + +void ParticleSystem2D::setMatrixSize(uint32_t x, uint32_t y) { + maxXpixel = x - 1; // last physical pixel that can be drawn to + maxYpixel = y - 1; + maxX = x * PS_P_RADIUS - 1; // particle system boundary for movements + maxY = y * PS_P_RADIUS - 1; // this value is often needed (also by FX) to calculate positions +} + +void ParticleSystem2D::setWrapX(bool enable) { + particlesettings.wrapX = enable; +} + +void ParticleSystem2D::setWrapY(bool enable) { + particlesettings.wrapY = enable; +} + +void ParticleSystem2D::setBounceX(bool enable) { + particlesettings.bounceX = enable; +} + +void ParticleSystem2D::setBounceY(bool enable) { + particlesettings.bounceY = enable; +} + +void ParticleSystem2D::setKillOutOfBounds(bool enable) { + particlesettings.killoutofbounds = enable; +} + +void ParticleSystem2D::setColorByAge(bool enable) { + particlesettings.colorByAge = enable; +} + +void ParticleSystem2D::setMotionBlur(uint8_t bluramount) { + if (particlesize == 0) // only allow motion blurring on default particle size or advanced size (cannot combine motion blur with normal blurring used for particlesize, would require another buffer) + motionBlur = bluramount; +} + +void ParticleSystem2D::setSmearBlur(uint8_t bluramount) { + smearBlur = bluramount; +} + + +// render size using smearing (see blur function) +void ParticleSystem2D::setParticleSize(uint8_t size) { + particlesize = size; + particleHardRadius = PS_P_MINHARDRADIUS + (particlesize >> 1); // radius used for wall collisions & particle collisions + motionBlur = 0; // disable motion blur if particle size is set +} + +// enable/disable gravity, optionally, set the force (force=8 is default) can be -127 to +127, 0 is disable +// if enabled, gravity is applied to all particles in ParticleSystemUpdate() +// 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 ParticleSystem2D::setGravity(int8_t force) { + if (force) { + gforce = force; + particlesettings.useGravity = true; + } else { + particlesettings.useGravity = false; + } +} + +void ParticleSystem2D::enableParticleCollisions(bool enable, uint8_t hardness) { // enable/disable gravity, optionally, set the force (force=8 is default) can be 1-255, 0 is also disable + particlesettings.useCollisions = enable; + collisionHardness = (int)hardness + 1; +} + +// emit one particle with variation, returns index of emitted particle (or -1 if no particle emitted) +int32_t ParticleSystem2D::sprayEmit(const PSsource &emitter) { + bool success = false; + for (uint32_t i = 0; i < usedParticles; i++) { + emitIndex++; + if (emitIndex >= usedParticles) + emitIndex = 0; + if (particles[emitIndex].ttl == 0) { // find a dead particle + success = true; + particles[emitIndex].vx = emitter.vx + hw_random16(emitter.var << 1) - emitter.var; // random(-var, var) + particles[emitIndex].vy = emitter.vy + hw_random16(emitter.var << 1) - emitter.var; // random(-var, var) + particles[emitIndex].x = emitter.source.x; + particles[emitIndex].y = emitter.source.y; + particles[emitIndex].hue = emitter.source.hue; + particles[emitIndex].sat = emitter.source.sat; + particleFlags[emitIndex].collide = emitter.sourceFlags.collide; + particles[emitIndex].ttl = hw_random16(emitter.minLife, emitter.maxLife); + if (advPartProps) + advPartProps[emitIndex].size = emitter.size; + break; + } + } + if (success) + return emitIndex; + else + return -1; +} + +// Spray emitter for particles used for flames (particle TTL depends on source TTL) +void ParticleSystem2D::flameEmit(const PSsource &emitter) { + int emitIndex = sprayEmit(emitter); + if(emitIndex > 0) particles[emitIndex].ttl += emitter.source.ttl; +} + +// Emits a particle at given angle and speed, angle is from 0-65535 (=0-360deg), speed is also affected by emitter->var +// angle = 0 means in positive x-direction (i.e. to the right) +int32_t ParticleSystem2D::angleEmit(PSsource &emitter, const uint16_t angle, const int32_t speed) { + emitter.vx = ((int32_t)cos16_t(angle) * speed) / (int32_t)32600; // cos16_t() and sin16_t() return signed 16bit, division should be 32767 but 32600 gives slightly better rounding + emitter.vy = ((int32_t)sin16_t(angle) * speed) / (int32_t)32600; // note: cannot use bit shifts as bit shifting is asymmetrical for positive and negative numbers and this needs to be accurate! + return 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 enabled, it will never bounce at the top and killoutofbounds is not applied over the top +void ParticleSystem2D::particleMoveUpdate(PSparticle &part, PSparticleFlags &partFlags, PSsettings2D *options, PSadvancedParticle *advancedproperties) { + if (options == NULL) + options = &particlesettings; //use PS system settings by default + + if (part.ttl > 0) { + if (!partFlags.perpetual) + part.ttl--; // age + if (options->colorByAge) + part.hue = min(part.ttl, (uint16_t)255); //set color to ttl + + int32_t renderradius = PS_P_HALFRADIUS; // used to check out of bounds + int32_t newX = part.x + (int32_t)part.vx; + int32_t newY = part.y + (int32_t)part.vy; + partFlags.outofbounds = false; // reset out of bounds (in case particle was created outside the matrix and is now moving into view) note: moving this to checks below adds code and is not faster + + if (advancedproperties) { //using individual particle size? + if (advancedproperties->size > 0) { + particleHardRadius = max(PS_P_MINHARDRADIUS, (int)particlesize + (advancedproperties->size)); // update radius + renderradius = particleHardRadius; + } + } + // note: if wall collisions are enabled, bounce them before they reach the edge, it looks much nicer if the particle does not go half out of view + if (options->bounceY) { + if ((newY < (int32_t)particleHardRadius) || ((newY > (int32_t)(maxY - particleHardRadius)) && !options->useGravity)) { // reached floor / ceiling + bounce(part.vy, part.vx, newY, maxY); + } + } + + if(!checkBoundsAndWrap(newY, maxY, renderradius, options->wrapY)) { // check out of bounds note: this must not be skipped, if gravity is enabled, particles will never bounce at the top + partFlags.outofbounds = true; + if (options->killoutofbounds) { + if (newY < 0) // if gravity is enabled, only kill particles below ground + part.ttl = 0; + else if (!options->useGravity) + part.ttl = 0; + } + } + + if(part.ttl) { //check x direction only if still alive + if (options->bounceX) { + if ((newX < (int32_t)particleHardRadius) || (newX > (int32_t)(maxX - particleHardRadius))) // reached a wall + bounce(part.vx, part.vy, newX, maxX); + } + else if(!checkBoundsAndWrap(newX, maxX, renderradius, options->wrapX)) { // check out of bounds + partFlags.outofbounds = true; + if (options->killoutofbounds) + part.ttl = 0; + } + } + + part.x = (int16_t)newX; // set new position + part.y = (int16_t)newY; // set new position + } +} + +// move function for fire particles +void ParticleSystem2D::fireParticleupdate() { + for (uint32_t i = 0; i < usedParticles; i++) { + if (particles[i].ttl > 0) + { + particles[i].ttl--; // age + int32_t newY = particles[i].y + (int32_t)particles[i].vy + (particles[i].ttl >> 2); // younger particles move faster upward as they are hotter + int32_t newX = particles[i].x + (int32_t)particles[i].vx; + particleFlags[i].outofbounds = 0; // reset out of bounds flag note: moving this to checks below is not faster but adds code + // 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 speed, only check x direction if y is not out of bounds + if (newY < -PS_P_HALFRADIUS) + particleFlags[i].outofbounds = 1; + else if (newY > int32_t(maxY + PS_P_HALFRADIUS)) // particle moved out at the top + particles[i].ttl = 0; + else // particle is in frame in y direction, also check x direction now Note: using checkBoundsAndWrap() is slower, only saves a few bytes + { + if ((newX < 0) || (newX > (int32_t)maxX)) { // handle out of bounds & wrap + if (particlesettings.wrapX) { + newX = newX % (maxX + 1); + if (newX < 0) // handle negative modulo + newX += maxX + 1; + } + else if ((newX < -PS_P_HALFRADIUS) || (newX > int32_t(maxX + PS_P_HALFRADIUS))) { //if fully out of view + particles[i].ttl = 0; + } + } + particles[i].x = newX; + } + particles[i].y = newY; + } + } +} + +// update advanced particle size control, returns false if particle shrinks to 0 size +bool ParticleSystem2D::updateSize(PSadvancedParticle *advprops, PSsizeControl *advsize) { + if (advsize == NULL) // safety check + return false; + // grow/shrink particle + int32_t newsize = advprops->size; + uint32_t counter = advsize->sizecounter; + uint32_t increment = 0; + // calculate grow speed using 0-8 for low speeds and 9-15 for higher speeds + if (advsize->grow) increment = advsize->growspeed; + else if (advsize->shrink) increment = advsize->shrinkspeed; + if (increment < 9) { // 8 means +1 every frame + counter += increment; + if (counter > 7) { + counter -= 8; + increment = 1; + } else + increment = 0; + advsize->sizecounter = counter; + } else { + increment = (increment - 8) << 1; // 9 means +2, 10 means +4 etc. 15 means +14 + } + + if (advsize->grow) { + if (newsize < advsize->maxsize) { + newsize += increment; + if (newsize >= advsize->maxsize) { + advsize->grow = false; // stop growing, shrink from now on if enabled + newsize = advsize->maxsize; // limit + if (advsize->pulsate) advsize->shrink = true; + } + } + } else if (advsize->shrink) { + if (newsize > advsize->minsize) { + newsize -= increment; + if (newsize <= advsize->minsize) { + if (advsize->minsize == 0) + return false; // particle shrunk to zero + advsize->shrink = false; // disable shrinking + newsize = advsize->minsize; // limit + if (advsize->pulsate) advsize->grow = true; + } + } + } + advprops->size = newsize; + // handle wobbling + if (advsize->wobble) { + advsize->asymdir += advsize->wobblespeed; // note: if need better wobblespeed control a counter is already in the struct + } + return true; +} + +// calculate x and y size for asymmetrical particles (advanced size control) +void ParticleSystem2D::getParticleXYsize(PSadvancedParticle *advprops, PSsizeControl *advsize, uint32_t &xsize, uint32_t &ysize) { + if (advsize == NULL) // if advsize is valid, also advanced properties pointer is valid (handled by updatePSpointers()) + return; + int32_t size = advprops->size; + int32_t asymdir = advsize->asymdir; + int32_t deviation = ((uint32_t)size * (uint32_t)advsize->asymmetry) / 255; // deviation from symmetrical size + // Calculate x and y size based on deviation and direction (0 is symmetrical, 64 is x, 128 is symmetrical, 192 is y) + if (asymdir < 64) { + deviation = (asymdir * deviation) / 64; + } else if (asymdir < 192) { + deviation = ((128 - asymdir) * deviation) / 64; + } else { + deviation = ((asymdir - 255) * deviation) / 64; + } + // Calculate x and y size based on deviation, limit to 255 (rendering function cannot handle larger sizes) + xsize = min((size - deviation), (int32_t)255); + ysize = min((size + deviation), (int32_t)255);; +} + +// function to bounce a particle from a wall using set parameters (wallHardness and wallRoughness) +void ParticleSystem2D::bounce(int8_t &incomingspeed, int8_t ¶llelspeed, int32_t &position, const uint32_t maxposition) { + incomingspeed = -incomingspeed; + incomingspeed = (incomingspeed * wallHardness) / 255; // reduce speed as energy is lost on non-hard surface + if (position < (int32_t)particleHardRadius) + position = particleHardRadius; // fast particles will never reach the edge if position is inverted, this looks better + else + position = maxposition - particleHardRadius; + if (wallRoughness) { + int32_t incomingspeed_abs = abs((int32_t)incomingspeed); + int32_t totalspeed = incomingspeed_abs + abs((int32_t)parallelspeed); + // transfer an amount of incomingspeed speed to parallel speed + int32_t donatespeed = ((hw_random16(incomingspeed_abs << 1) - incomingspeed_abs) * (int32_t)wallRoughness) / (int32_t)255; // take random portion of + or - perpendicular speed, scaled by roughness + parallelspeed = limitSpeed((int32_t)parallelspeed + donatespeed); + // give the remainder of the speed to perpendicular speed + donatespeed = int8_t(totalspeed - abs(parallelspeed)); // keep total speed the same + incomingspeed = incomingspeed > 0 ? donatespeed : -donatespeed; + } +} + +// apply a force in x,y direction to individual particle +// caller needs to provide a 8bit counter (for each particle) that holds its value between calls +// 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 ParticleSystem2D::applyForce(PSparticle &part, const int8_t xforce, const 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 + part.vx = limitSpeed((int32_t)part.vx + dvx); + part.vy = limitSpeed((int32_t)part.vy + dvy); +} + +// apply a force in x,y direction to individual particle using advanced particle properties +void ParticleSystem2D::applyForce(const uint32_t particleindex, const int8_t xforce, const int8_t yforce) { + if (advPartProps == NULL) + return; // no advanced properties available + applyForce(particles[particleindex], xforce, yforce, advPartProps[particleindex].forcecounter); +} + +// apply a force in x,y direction to all particles +// force is in 3.4 fixed point notation (see above) +void ParticleSystem2D::applyForce(const int8_t xforce, const int8_t yforce) { + // for small forces, need to use a delay counter + uint8_t tempcounter; + // note: this is not the most computationally efficient way to do this, but it saves on duplicate code and is fast enough + for (uint32_t i = 0; i < usedParticles; i++) { + tempcounter = forcecounter; + applyForce(particles[i], xforce, yforce, tempcounter); + } + forcecounter = tempcounter; // save value back +} + +// apply a force in angular direction to single particle +// caller needs to provide a 8bit counter that holds its value between calls (if using single particles, a counter for each particle is needed) +// angle is from 0-65535 (=0-360deg) angle = 0 means in positive x-direction (i.e. to the right) +// force is in 3.4 fixed point notation so force=16 means apply v+1 each frame (useful force range is +/- 127) +void ParticleSystem2D::applyAngleForce(PSparticle &part, const int8_t force, const uint16_t angle, uint8_t &counter) { + int8_t xforce = ((int32_t)force * cos16_t(angle)) / 32767; // force is +/- 127 + int8_t yforce = ((int32_t)force * sin16_t(angle)) / 32767; // note: cannot use bit shifts as bit shifting is asymmetrical for positive and negative numbers and this needs to be accurate! + applyForce(part, xforce, yforce, counter); +} + +void ParticleSystem2D::applyAngleForce(const uint32_t particleindex, const int8_t force, const uint16_t angle) { + if (advPartProps == NULL) + return; // no advanced properties available + applyAngleForce(particles[particleindex], force, angle, advPartProps[particleindex].forcecounter); +} + +// apply a force in angular direction to all particles +// angle is from 0-65535 (=0-360deg) angle = 0 means in positive x-direction (i.e. to the right) +void ParticleSystem2D::applyAngleForce(const int8_t force, const uint16_t angle) { + int8_t xforce = ((int32_t)force * cos16_t(angle)) / 32767; // force is +/- 127 + int8_t yforce = ((int32_t)force * sin16_t(angle)) / 32767; // note: cannot use bit shifts as bit shifting is asymmetrical for positive and negative numbers and this needs to be accurate! + applyForce(xforce, yforce); +} + +// apply gravity to all particles using PS global gforce setting +// force is in 3.4 fixed point notation, see note above +// note: faster than apply force since direction is always down and counter is fixed for all particles +void ParticleSystem2D::applyGravity() { + int32_t dv = calcForce_dv(gforce, gforcecounter); + if(dv == 0) return; + for (uint32_t i = 0; i < usedParticles; 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 = limitSpeed((int32_t)particles[i].vy - dv); + } +} + +// apply gravity to single particle using system settings (use this for sources) +// function does not increment gravity counter, if gravity setting is disabled, this cannot be used +void ParticleSystem2D::applyGravity(PSparticle &part) { + uint32_t counterbkp = gforcecounter; // backup PS gravity counter + int32_t dv = calcForce_dv(gforce, gforcecounter); + gforcecounter = counterbkp; //save it back + part.vy = limitSpeed((int32_t)part.vy - dv); +} + +// slow down particle by friction, the higher the speed, the higher the friction. a high friction coefficient slows them more (255 means instant stop) +// note: a coefficient smaller than 0 will speed them up (this is a feature, not a bug), coefficient larger than 255 inverts the speed, so don't do that +void ParticleSystem2D::applyFriction(PSparticle &part, const int32_t coefficient) { + int32_t friction = 255 - coefficient; + // note: not checking if particle is dead can be done by caller (or can be omitted) + // note2: cannot use right shifts as bit shifting in right direction is asymmetrical for positive and negative numbers and this needs to be accurate + part.vx = ((int32_t)part.vx * friction) / 255; + part.vy = ((int32_t)part.vy * friction) / 255; +} + +// apply friction to all particles +void ParticleSystem2D::applyFriction(const int32_t coefficient) { + int32_t friction = 255 - coefficient; + for (uint32_t i = 0; i < usedParticles; 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].vx = ((int32_t)particles[i].vx * friction) / 255; + particles[i].vy = ((int32_t)particles[i].vy * friction) / 255; + } +} + +// attracts a particle to an attractor particle using the inverse square-law +void ParticleSystem2D::pointAttractor(const uint32_t particleindex, PSparticle &attractor, const uint8_t strength, const bool swallow) { + if (advPartProps == NULL) + return; // no advanced properties available + + // Calculate the distance between the particle and the attractor + int32_t dx = attractor.x - particles[particleindex].x; + int32_t dy = attractor.y - particles[particleindex].y; + + // Calculate the force based on inverse square law + int32_t distanceSquared = dx * dx + dy * dy; + if (distanceSquared < 8192) { + if (swallow) { // particle is close, age it fast so it fades out, do not attract further + if (particles[particleindex].ttl > 7) + particles[particleindex].ttl -= 8; + else { + particles[particleindex].ttl = 0; + return; + } + } + distanceSquared = 2 * PS_P_RADIUS * PS_P_RADIUS; // limit the distance to avoid very high forces + } + + int32_t force = ((int32_t)strength << 16) / distanceSquared; + int8_t xforce = (force * dx) / 1024; // scale to a lower value, found by experimenting + int8_t yforce = (force * dy) / 1024; // note: cannot use bit shifts as bit shifting is asymmetrical for positive and negative numbers and this needs to be accurate! + applyForce(particleindex, xforce, yforce); +} + +// 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 +// warning: do not render out of bounds particles or system will crash! rendering does not check if particle is out of bounds +// firemode is only used for PS Fire FX +void ParticleSystem2D::ParticleSys_render() { + CRGB baseRGB; + uint32_t brightness; // particle brightness, fades if dying + static bool useAdditiveTransfer = false; // use add instead of set for buffer transferring + + // update global blur (used for blur transitions) + int32_t motionbluramount = motionBlur; + int32_t smearamount = smearBlur; + if(pmem->inTransition == effectID) { // FX transition and this is the new FX: fade blur amount + motionbluramount = globalBlur + (((motionbluramount - globalBlur) * (int)SEGMENT.progress()) >> 16); // fade from old blur to new blur during transitions + smearamount = globalSmear + (((smearamount - globalSmear) * (int)SEGMENT.progress()) >> 16); + } + globalBlur = motionbluramount; + globalSmear = smearamount; + + // handle blurring and framebuffer update + if (framebuffer) { + if(segmentIsOverlay()) useAdditiveTransfer = true; // overlay rendering + else useAdditiveTransfer = false; + // handle buffer blurring or clearing + bool bufferNeedsUpdate = (!pmem->inTransition || pmem->inTransition == effectID); // not a transition; or new FX: update buffer (blur, or clear) + + if(bufferNeedsUpdate) { + if (globalBlur > 0 || globalSmear > 0) { // blurring active: if not a transition or is newFX, read data from segment before blurring (old FX can render to it afterwards) + for (int32_t y = 0; y <= maxYpixel; y++) { + int index = y * (maxXpixel + 1); + for (int32_t x = 0; x <= maxXpixel; x++) { + if (!renderSolo) { // sharing the framebuffer with another segment: update buffer by reading back from segment + framebuffer[index] = SEGMENT.getPixelColorXY(x, y); // read from segment + } + fast_color_scale(framebuffer[index], globalBlur); // note: could skip if only globalsmear is active but usually they are both active and scaling is fast enough + index++; + } + } + } + else { // no blurring: clear buffer + memset(framebuffer, 0, frameBufferSize * sizeof(CRGB)); + } + } + if(particlesize > 0 && pmem->inTransition) { // if particle size is used by FX we need a clean buffer + if(bufferNeedsUpdate && !globalBlur) { // transfer only if buffer was not cleared above (happens if this is the new FX and other FX does not use blurring) + useAdditiveTransfer = false; // no blurring and big size particle FX is the new FX (rendered first after clearing), can just render normally + } + else { // this is the old FX (rendering second) or blurring is active: new FX already rendered to the buffer and blurring was applied above; transfer it to segment and clear it + transferBuffer(maxXpixel + 1, maxYpixel + 1, useAdditiveTransfer); + memset(framebuffer, 0, frameBufferSize * sizeof(CRGB)); // clear the buffer after transfer + useAdditiveTransfer = true; // additive transfer reads from segment, adds that to the frame-buffer and writes back to segment, after transfer, segment and buffer are identical + } + } + } + else { // no local buffer available, apply blur to segment + if (motionBlur > 0) + SEGMENT.fadeToBlackBy(255 - motionBlur); + else + SEGMENT.fill(BLACK); //clear the buffer before rendering next frame + } + + // go over particles and render them to the buffer + for (uint32_t i = 0; i < usedParticles; i++) { + if (particles[i].ttl == 0 || particleFlags[i].outofbounds) + continue; + // generate RGB values for particle + if (fireIntesity) { + brightness = (uint32_t)particles[i].ttl * (3 + (fireIntesity >> 5)) + 20; + brightness = min(brightness, (uint32_t)255); + baseRGB = ColorFromPalette(SEGPALETTE, brightness, 255); + } + else { + brightness = min((particles[i].ttl << 1), (int)255); + baseRGB = ColorFromPalette(SEGPALETTE, particles[i].hue, 255); + if (particles[i].sat < 255) { + CHSV32 baseHSV; + rgb2hsv((uint32_t((byte(baseRGB.r) << 16) | (byte(baseRGB.g) << 8) | (byte(baseRGB.b)))), baseHSV); // convert to HSV + baseHSV.s = particles[i].sat; // set the saturation + uint32_t tempcolor; + hsv2rgb(baseHSV, tempcolor); // convert back to RGB + baseRGB = (CRGB)tempcolor; + } + } + renderParticle(i, brightness, baseRGB, particlesettings.wrapX, particlesettings.wrapY); + } + + if (particlesize > 0) { + uint32_t passes = particlesize / 64 + 1; // number of blur passes, four passes max + uint32_t bluramount = particlesize; + uint32_t bitshift = 0; + for (uint32_t i = 0; i < passes; i++) { + if (i == 2) // for the last two passes, use higher amount of blur (results in a nicer brightness gradient with soft edges) + bitshift = 1; + + if (framebuffer) + blur2D(framebuffer, maxXpixel + 1, maxYpixel + 1, bluramount << bitshift, bluramount << bitshift); + else { + SEGMENT.blur(bluramount << bitshift, true); + } + bluramount -= 64; + } + } + // apply 2D blur to rendered frame + if(globalSmear > 0) { + if (framebuffer) + blur2D(framebuffer, maxXpixel + 1, maxYpixel + 1, globalSmear, globalSmear); + else + SEGMENT.blur(globalSmear, true); + } + // transfer framebuffer to segment if available + if (pmem->inTransition != effectID) { // not in transition or is old FX (rendered second) + transferBuffer(maxXpixel + 1, maxYpixel + 1, useAdditiveTransfer); + } +} + +// calculate pixel positions and brightness distribution and render the particle to local buffer or global buffer +void ParticleSystem2D::renderParticle(const uint32_t particleindex, const uint32_t brightness, const CRGB& color, const bool wrapX, const bool wrapY) { + int32_t pxlbrightness[4]; // brightness values for the four pixels representing a particle + int32_t pixco[4][2]; // physical pixel coordinates of the four pixels a particle is rendered to. x,y pairs + bool pixelvalid[4] = {true, true, true, true}; // is set to false if pixel is out of bounds + bool advancedrender = false; // rendering for advanced particles + // check if particle has advanced size properties and buffer is available + if (advPartProps && advPartProps[particleindex].size > 0) { + if (renderbuffer) { + advancedrender = true; + memset(renderbuffer, 0, 100 * sizeof(CRGB)); // clear the buffer, renderbuffer is 10x10 pixels + } + else return; // cannot render without buffers + } + // add half a radius as the rendering algorithm always starts at the bottom left, this leaves things positive, so shifts can be used, then shift coordinate by a full pixel (x--/y-- below) + int32_t xoffset = particles[particleindex].x + PS_P_HALFRADIUS; + int32_t yoffset = particles[particleindex].y + PS_P_HALFRADIUS; + int32_t dx = xoffset & (PS_P_RADIUS - 1); // relativ particle position in subpixel space + int32_t dy = yoffset & (PS_P_RADIUS - 1); // modulo replaced with bitwise AND, as radius is always a power of 2 + int32_t x = (xoffset >> PS_P_RADIUS_SHIFT); // divide by PS_P_RADIUS which is 64, so can bitshift (compiler can not optimize integer) + 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] + pixco[1][0] = pixco[2][0] = x; // bottom right & top right + pixco[2][1] = pixco[3][1] = y; // top right & top left + x--; // shift by a full pixel here, this is skipped above to not do -1 and then +1 + y--; + pixco[0][0] = pixco[3][0] = x; // bottom left & top left + pixco[0][1] = pixco[1][1] = y; // bottom left & bottom right + + // calculate brightness values for all four pixels representing a particle using linear interpolation + // could check for out of frame pixels here but calculating them is faster (very few are out) + // precalculate values for speed optimization + int32_t precal1 = (int32_t)PS_P_RADIUS - dx; + int32_t precal2 = ((int32_t)PS_P_RADIUS - dy) * brightness; + int32_t precal3 = dy * brightness; + pxlbrightness[0] = (precal1 * precal2) >> PS_P_SURFACE; // bottom left value equal to ((PS_P_RADIUS - dx) * (PS_P_RADIUS-dy) * brightness) >> PS_P_SURFACE + pxlbrightness[1] = (dx * precal2) >> PS_P_SURFACE; // bottom right value equal to (dx * (PS_P_RADIUS-dy) * brightness) >> PS_P_SURFACE + pxlbrightness[2] = (dx * precal3) >> PS_P_SURFACE; // top right value equal to (dx * dy * brightness) >> PS_P_SURFACE + pxlbrightness[3] = (precal1 * precal3) >> PS_P_SURFACE; // top left value equal to ((PS_P_RADIUS-dx) * dy * brightness) >> PS_P_SURFACE + + if (advancedrender) { + //render particle to a bigger size + //particle size to pixels: < 64 is 4x4, < 128 is 6x6, < 192 is 8x8, bigger is 10x10 + //first, render the pixel to the center of the renderbuffer, then apply 2D blurring + fast_color_add(renderbuffer[4 + (4 * 10)], color, pxlbrightness[0]); // order is: bottom left, bottom right, top right, top left + fast_color_add(renderbuffer[5 + (4 * 10)], color, pxlbrightness[1]); + fast_color_add(renderbuffer[5 + (5 * 10)], color, pxlbrightness[2]); + fast_color_add(renderbuffer[4 + (5 * 10)], color, pxlbrightness[3]); + uint32_t rendersize = 2; // initialize render size, minimum is 4x4 pixels, it is incremented int he loop below to start with 4 + uint32_t offset = 4; // offset to zero coordinate to write/read data in renderbuffer (actually needs to be 3, is decremented in the loop below) + uint32_t maxsize = advPartProps[particleindex].size; + uint32_t xsize = maxsize; + uint32_t ysize = maxsize; + if (advPartSize) { // use advanced size control + if (advPartSize[particleindex].asymmetry > 0) + getParticleXYsize(&advPartProps[particleindex], &advPartSize[particleindex], xsize, ysize); + maxsize = (xsize > ysize) ? xsize : ysize; // choose the bigger of the two + } + maxsize = maxsize/64 + 1; // number of blur passes depends on maxsize, four passes max + uint32_t bitshift = 0; + for(uint32_t i = 0; i < maxsize; i++) { + if (i == 2) //for the last two passes, use higher amount of blur (results in a nicer brightness gradient with soft edges) + bitshift = 1; + rendersize += 2; + offset--; + blur2D(renderbuffer, rendersize, rendersize, xsize << bitshift, ysize << bitshift, offset, offset, true); + xsize = xsize > 64 ? xsize - 64 : 0; + ysize = ysize > 64 ? ysize - 64 : 0; + } + + // calculate origin coordinates to render the particle to in the framebuffer + uint32_t xfb_orig = x - (rendersize>>1) + 1 - offset; + uint32_t yfb_orig = y - (rendersize>>1) + 1 - offset; + uint32_t xfb, yfb; // coordinates in frame buffer to write to note: by making this uint, only overflow has to be checked (spits a warning though) + + //note on y-axis flip: WLED has the y-axis defined from top to bottom, so y coordinates must be flipped. doing this in the buffer xfer clashes with 1D/2D combined rendering, which does not invert y + // transferring the 1D buffer in inverted fashion will flip the x-axis of overlaid 2D FX, so the y-axis flip is done here so the buffer is flipped in y, giving correct results + + // transfer particle renderbuffer to framebuffer + for (uint32_t xrb = offset; xrb < rendersize + offset; xrb++) { + xfb = xfb_orig + xrb; + if (xfb > (uint32_t)maxXpixel) { + if (wrapX) { // wrap x to the other side if required + if (xfb > (uint32_t)maxXpixel << 1) // xfb is "negative", handle it + xfb = (maxXpixel + 1) + (int32_t)xfb; // this always overflows to within bounds + else + xfb = xfb % (maxXpixel + 1); // note: without the above "negative" check, this works only for powers of 2 + } + else + continue; + } + + for (uint32_t yrb = offset; yrb < rendersize + offset; yrb++) { + yfb = yfb_orig + yrb; + if (yfb > (uint32_t)maxYpixel) { + if (wrapY) {// wrap y to the other side if required + if (yfb > (uint32_t)maxYpixel << 1) // yfb is "negative", handle it + yfb = (maxYpixel + 1) + (int32_t)yfb; // this always overflows to within bounds + else + yfb = yfb % (maxYpixel + 1); // note: without the above "negative" check, this works only for powers of 2 + } + else + continue; + } + if (framebuffer) + fast_color_add(framebuffer[xfb + (maxYpixel - yfb) * (maxXpixel + 1)], renderbuffer[xrb + yrb * 10]); + else + SEGMENT.addPixelColorXY(xfb, maxYpixel - yfb, renderbuffer[xrb + yrb * 10],true); + } + } + } else { // standard rendering + // check for out of frame pixels and wrap them if required: x,y is bottom left pixel coordinate of the particle + if (x < 0) { // left pixels out of frame + if (wrapX) { // wrap x to the other side if required + pixco[0][0] = pixco[3][0] = maxXpixel; + } else { + pixelvalid[0] = pixelvalid[3] = false; // out of bounds + } + } + else if (pixco[1][0] > (int32_t)maxXpixel) { // right pixels, only has to be checked if left pixel is in frame + if (wrapX) { // wrap y to the other side if required + pixco[1][0] = pixco[2][0] = 0; + } else { + pixelvalid[1] = pixelvalid[2] = false; // out of bounds + } + } + + if (y < 0) { // bottom pixels out of frame + if (wrapY) { // wrap y to the other side if required + pixco[0][1] = pixco[1][1] = maxYpixel; + } else { + pixelvalid[0] = pixelvalid[1] = false; // out of bounds + } + } + else if (pixco[2][1] > maxYpixel) { // top pixels + if (wrapY) { // wrap y to the other side if required + pixco[2][1] = pixco[3][1] = 0; + } else { + pixelvalid[2] = pixelvalid[3] = false; // out of bounds + } + } + if (framebuffer) { + for (uint32_t i = 0; i < 4; i++) { + if (pixelvalid[i]) + fast_color_add(framebuffer[pixco[i][0] + (maxYpixel - pixco[i][1]) * (maxXpixel + 1)], color, pxlbrightness[i]); // order is: bottom left, bottom right, top right, top left + } + } + else { + for (uint32_t i = 0; i < 4; i++) { + if (pixelvalid[i]) + SEGMENT.addPixelColorXY(pixco[i][0], maxYpixel - pixco[i][1], color.scale8((uint8_t)pxlbrightness[i]), true); + } + } + } +} + +// detect collisions in an array of particles and handle them +// uses binning by dividing the frame into slices in x direction which is efficient if using gravity in y direction (but less efficient for FX that use forces in x direction) +// for code simplicity, no y slicing is done, making very tall matrix configurations less efficient +// note: also tested adding y slicing, it gives diminishing returns, some FX even get slower. FX not using gravity would benefit with a 10% FPS improvement +void ParticleSystem2D::handleCollisions() { + int32_t collDistSq = particleHardRadius << 1; + collDistSq = collDistSq * collDistSq; // square it for faster comparison (square is one operation) + // note: partices are binned in x-axis, assumption is that no more than half of the particles are in the same bin + // if they are, collisionStartIdx is increased so each particle collides at least every second frame (which still gives decent collisions) + constexpr uint32_t BIN_WIDTH = 6 * PS_P_RADIUS; // width of a bin in sub-pixels + uint32_t maxBinParticles = max((uint32_t)50, (usedParticles + 1) / 2); // assume no more than half of the particles are in the same bin, do not bin small amounts of particles + uint32_t numBins = (maxX + (BIN_WIDTH - 1)) / BIN_WIDTH; // number of bins in x direction + uint16_t binIndices[maxBinParticles]; // creat array on stack for indices, 2kB max for 1024 particles (ESP32_MAXPARTICLES/2) + uint32_t binParticleCount; // number of particles in the current bin + uint16_t nextFrameStartIdx = 0; // index of the first particle in the next frame (set if bin overflow) + uint32_t pidx = collisionStartIdx; //start index in case a bin is full, process remaining particles next frame + + // fill the binIndices array for this bin + for (uint32_t bin = 0; bin < numBins; bin++) { + binParticleCount = 0; // reset for this bin + int32_t binStart = bin * BIN_WIDTH; + int32_t binEnd = binStart + BIN_WIDTH; + + // fill the binIndices array for this bin + for (uint32_t i = 0; i < usedParticles; i++) { + if (particles[pidx].ttl > 0 && particleFlags[pidx].outofbounds == 0 && particleFlags[pidx].collide) { // colliding particle + if (particles[pidx].x >= binStart && particles[pidx].x <= binEnd) { // >= and <= to include particles on the edge of the bin (overlap to ensure boarder particles collide with adjacent bins) + if (binParticleCount >= maxBinParticles) { // bin is full, more particles in this bin so do the rest next frame + nextFrameStartIdx = pidx; // bin overflow can only happen once as bin size is at least half of the particles (or half +1) + break; + } + binIndices[binParticleCount++] = pidx; + } + } + pidx++; + if (pidx >= usedParticles) pidx = 0; // wrap around + } + + for (uint32_t i = 0; i < binParticleCount; i++) { // go though all 'higher number' particles in this bin and see if any of those are in close proximity and if they are, make them collide + uint32_t idx_i = binIndices[i]; + for (uint32_t j = i + 1; j < binParticleCount; j++) { // check against higher number particles + uint32_t idx_j = binIndices[j]; + if (advPartProps) { //may be using individual particle size + collDistSq = PS_P_MINHARDRADIUS + particlesize + (((uint32_t)advPartProps[idx_i].size + (uint32_t)advPartProps[idx_j].size) >> 1); // collision distance + collDistSq = collDistSq * collDistSq; // square it for faster comparison + } + int32_t dx = particles[idx_j].x - particles[idx_i].x; + if (dx * dx < collDistSq) { // check x direction, if close, check y direction (squaring is faster than abs() or dual compare) + int32_t dy = particles[idx_j].y - particles[idx_i].y; + if (dy * dy < collDistSq) // particles are close + collideParticles(particles[idx_i], particles[idx_j], dx, dy); + } + } + } + } + collisionStartIdx = nextFrameStartIdx; // set the start index for the next frame +} + +// 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 ParticleSystem2D::collideParticles(PSparticle &particle1, PSparticle &particle2, int32_t dx, int32_t dy) { + int32_t distanceSquared = dx * dx + dy * dy; + // Calculate relative velocity (if it is zero, could exit but extra check does not overall speed but deminish it) + int32_t relativeVx = (int32_t)particle2.vx - (int32_t)particle1.vx; + int32_t relativeVy = (int32_t)particle2.vy - (int32_t)particle1.vy; + + // if dx and dy are zero (i.e. same position) give them an offset, if speeds are also zero, also offset them (pushes particles apart if they are clumped before enabling collisions) + if (distanceSquared == 0) { + // Adjust positions based on relative velocity direction + dx = -1; + if (relativeVx < 0) // if true, particle2 is on the right side + dx = 1; + else if (relativeVx == 0) + relativeVx = 1; + + dy = -1; + if (relativeVy < 0) + dy = 1; + else if (relativeVy == 0) + relativeVy = 1; + + distanceSquared = 2; // 1 + 1 + } + + // Calculate dot product of relative velocity and relative distance + int32_t dotProduct = (dx * relativeVx + dy * relativeVy); // is always negative if moving towards each other + + if (dotProduct < 0) {// particles are moving towards each other + // integer math used to avoid floats. + // overflow check: dx/dy are 7bit, relativV are 8bit -> dotproduct is 15bit, dotproduct/distsquared ist 8b, multiplied by collisionhardness of 8bit. so a 16bit shift is ok, make it 15 to be sure no overflows happen + // note: cannot use right shifts as bit shifting in right direction is asymmetrical for positive and negative numbers and this needs to be accurate! the trick is: only shift positive numers + // Calculate new velocities after collision + int32_t surfacehardness = max(collisionHardness, (int32_t)PS_P_MINSURFACEHARDNESS); // if particles are soft, the impulse must stay above a limit or collisions slip through at higher speeds, 170 seems to be a good value + int32_t impulse = -(((((-dotProduct) << 15) / distanceSquared) * surfacehardness) >> 8); // note: inverting before bitshift corrects for asymmetry in right-shifts (and is slightly faster) + int32_t ximpulse = ((impulse) * dx) / 32767; // cannot use bit shifts here, it can be negative, use division by 2^bitshift + int32_t yimpulse = ((impulse) * dy) / 32767; + particle1.vx += ximpulse; + particle1.vy += yimpulse; + particle2.vx -= ximpulse; + particle2.vy -= yimpulse; + + if (collisionHardness < surfacehardness && (SEGMENT.call & 0x03) == 0) { // if particles are soft, they become 'sticky' i.e. apply some friction (they do pile more nicely and stop sloshing around) + const uint32_t coeff = collisionHardness + (255 - PS_P_MINSURFACEHARDNESS); // Note: could call applyFriction, but this is faster and speed is key here + particle1.vx = ((int32_t)particle1.vx * coeff) / 255; + particle1.vy = ((int32_t)particle1.vy * coeff) / 255; + + particle2.vx = ((int32_t)particle2.vx * coeff) / 255; + particle2.vy = ((int32_t)particle2.vy * coeff) / 255; + } + + // particles have volume, push particles apart if they are too close + // tried lots of configurations, it works best if not moved but given a little velocity, it tends to oscillate less this way + // a problem with giving velocity is, that on harder collisions, this adds up as it is not dampened enough, so add friction in the FX if required + if (dotProduct > -250) { //this means particles are slow (or really really close) so push them apart. + int32_t notsorandom = dotProduct & 0x01; //dotprouct LSB should be somewhat random, so no need to calculate a random number + int32_t pushamount = 1 + ((250 + dotProduct) >> 6); // the closer dotproduct is to zero, the closer the particles are + int32_t push = 0; + if (dx < 0) // particle 1 is on the right + push = pushamount; + else if (dx > 0) + push = -pushamount; + else { // on the same x coordinate, shift it a little so they do not stack + if (notsorandom) + particle1.x++; // move it so pile collapses + else + particle1.x--; + } + particle1.vx += push; + push = 0; + if (dy < 0) + push = pushamount; + else if (dy > 0) + push = -pushamount; + else { // dy==0 + if (notsorandom) + particle1.y++; // move it so pile collapses + else + particle1.y--; + } + particle1.vy += push; + // note: pushing may push particles out of frame, if bounce is active, it will move it back as position will be limited to within frame, if bounce is disabled: bye bye + if (collisionHardness < 16) { // if they are very soft, stop slow particles completely to make them stick to each other + particle1.vx = 0; + particle1.vy = 0; + particle2.vx = 0; + particle2.vy = 0; + //push them apart + particle1.x += push; + particle1.y += push; + } + } + } +} + +// update size and pointers (memory location and size can change dynamically) +// note: do not access the PS class in FX befor running this function (or it messes up SEGENV.data) +void ParticleSystem2D::updateSystem(void) { + PSPRINTLN("updateSystem2D"); + setMatrixSize(SEGMENT.vWidth(), SEGMENT.vHeight()); + updateRenderingBuffer(SEGMENT.vWidth() * SEGMENT.vHeight(), true, false); // update rendering buffer (segment size can change at any time) + updatePSpointers(advPartProps != nullptr, advPartSize != nullptr); // update pointers to PS data, also updates availableParticles + setUsedParticles(fractionOfParticlesUsed); // update used particles based on percentage (can change during transitions, execute each frame for code simplicity) + if (partMemList.size() == 1) // if number of vector elements is one, this is the only system + renderSolo = true; + else + renderSolo = false; + PSPRINTLN("\n END update System2D, running FX..."); +} + +// 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 ParticleSystem2D::updatePSpointers(bool isadvanced, bool sizecontrol) { + PSPRINTLN("updatePSpointers"); + // DEBUG_PRINT(F("*** PS pointers ***")); + // DEBUG_PRINTF_P(PSTR("this PS %p "), this); + // Note on memory alignment: + // a pointer MUST be 4 byte aligned. sizeof() in a struct/class is always aligned to the largest element. if it contains a 32bit, it will be padded to 4 bytes, 16bit is padded to 2byte alignment. + // The PS is aligned to 4 bytes, a PSparticle is aligned to 2 and a struct containing only byte sized variables is not aligned at all and may need to be padded when dividing the memoryblock. + // by making sure that the number of sources and particles is a multiple of 4, padding can be skipped here as alignent is ensured, independent of struct sizes. + + // memory manager needs to know how many particles the FX wants to use so transitions can be handled properly (i.e. pointer will stop changing if enough particles are available during transitions) + uint32_t usedByFX = (numParticles * ((uint32_t)fractionOfParticlesUsed + 1)) >> 8; // final number of particles the FX wants to use (fractionOfParticlesUsed is 0-255) + particles = reinterpret_cast(particleMemoryManager(0, sizeof(PSparticle), availableParticles, usedByFX, effectID)); // get memory, leave buffer size as is (request 0) + particleFlags = reinterpret_cast(this + 1); // pointer to particle flags + sources = reinterpret_cast(particleFlags + numParticles); // pointer to source(s) at data+sizeof(ParticleSystem2D) + PSdataEnd = reinterpret_cast(sources + numSources); // pointer to first available byte after the PS for FX additional data + if (isadvanced) { + advPartProps = reinterpret_cast(sources + numSources); + PSdataEnd = reinterpret_cast(advPartProps + numParticles); + if (sizecontrol) { + advPartSize = reinterpret_cast(advPartProps + numParticles); + PSdataEnd = reinterpret_cast(advPartSize + numParticles); + } + } +#ifdef DEBUG_PS + Serial.printf_P(PSTR(" particles %p "), particles); + Serial.printf_P(PSTR(" sources %p "), sources); + Serial.printf_P(PSTR(" adv. props %p "), advPartProps); + Serial.printf_P(PSTR(" adv. ctrl %p "), advPartSize); + Serial.printf_P(PSTR("end %p\n"), PSdataEnd); + #endif + +} + +// blur a matrix in x and y direction, blur can be asymmetric in x and y +// for speed, 1D array and 32bit variables are used, make sure to limit them to 8bit (0-255) or result is undefined +// to blur a subset of the buffer, change the xsize/ysize and set xstart/ystart to the desired starting coordinates (default start is 0/0) +// subset blurring only works on 10x10 buffer (single particle rendering), if other sizes are needed, buffer width must be passed as parameter +void blur2D(CRGB *colorbuffer, uint32_t xsize, uint32_t ysize, uint32_t xblur, uint32_t yblur, uint32_t xstart, uint32_t ystart, bool isparticle) { + CRGB seeppart, carryover; + uint32_t seep = xblur >> 1; + uint32_t width = xsize; // width of the buffer, used to calculate the index of the pixel + + if (isparticle) { //first and last row are always black in first pass of particle rendering + ystart++; + ysize--; + width = 10; // buffer size is 10x10 + } + + for(uint32_t y = ystart; y < ystart + ysize; y++) { + carryover = BLACK; + uint32_t indexXY = xstart + y * width; + for(uint32_t x = xstart; x < xstart + xsize; x++) { + seeppart = colorbuffer[indexXY]; // create copy of current color + fast_color_scale(seeppart, seep); // scale it and seep to neighbours + if (x > 0) { + fast_color_add(colorbuffer[indexXY - 1], seeppart); + if(carryover) // note: check adds overhead but is faster on average + fast_color_add(colorbuffer[indexXY], carryover); + } + carryover = seeppart; + indexXY++; // next pixel in x direction + } + } + + if (isparticle) { // first and last row are now smeared + ystart--; + ysize++; + } + + seep = yblur >> 1; + for(uint32_t x = xstart; x < xstart + xsize; x++) { + carryover = BLACK; + uint32_t indexXY = x + ystart * width; + for(uint32_t y = ystart; y < ystart + ysize; y++) { + seeppart = colorbuffer[indexXY]; // create copy of current color + fast_color_scale(seeppart, seep); // scale it and seep to neighbours + if (y > 0) { + fast_color_add(colorbuffer[indexXY - width], seeppart); + if(carryover) // note: check adds overhead but is faster on average + fast_color_add(colorbuffer[indexXY], carryover); + } + carryover = seeppart; + indexXY += width; // next pixel in y direction + } + } +} + +//non class functions to use for initialization +uint32_t calculateNumberOfParticles2D(uint32_t const pixels, const bool isadvanced, const bool sizecontrol) { + uint32_t numberofParticles = pixels; // 1 particle per pixel (for example 512 particles on 32x16) +#ifdef ESP8266 + uint32_t particlelimit = ESP8266_MAXPARTICLES; // maximum number of paticles allowed (based on one segment of 16x16 and 4k effect ram) +#elif ARDUINO_ARCH_ESP32S2 + uint32_t particlelimit = ESP32S2_MAXPARTICLES; // maximum number of paticles allowed (based on one segment of 32x32 and 24k effect ram) +#else + uint32_t particlelimit = ESP32_MAXPARTICLES; // maximum number of paticles allowed (based on two segments of 32x32 and 40k effect ram) +#endif + numberofParticles = max((uint32_t)4, min(numberofParticles, particlelimit)); // limit to 4 - particlelimit + if (isadvanced) // advanced property array needs ram, reduce number of particles to use the same amount + numberofParticles = (numberofParticles * sizeof(PSparticle)) / (sizeof(PSparticle) + sizeof(PSadvancedParticle)); + if (sizecontrol) // advanced property array needs ram, reduce number of particles + numberofParticles /= 8; // if advanced size control is used, much fewer particles are needed note: if changing this number, adjust FX using this accordingly + + //make sure it is a multiple of 4 for proper memory alignment (easier than using padding bytes) + numberofParticles = ((numberofParticles+3) >> 2) << 2; // note: with a separate particle buffer, this is probably unnecessary + return numberofParticles; +} + +uint32_t calculateNumberOfSources2D(uint32_t pixels, uint32_t requestedsources) { +#ifdef ESP8266 + int numberofSources = min((pixels) / 8, (uint32_t)requestedsources); + numberofSources = max(1, min(numberofSources, ESP8266_MAXSOURCES)); // limit to 1 - 16 +#elif ARDUINO_ARCH_ESP32S2 + int numberofSources = min((pixels) / 6, (uint32_t)requestedsources); + numberofSources = max(1, min(numberofSources, ESP32S2_MAXSOURCES)); // limit to 1 - 48 +#else + int numberofSources = min((pixels) / 4, (uint32_t)requestedsources); + numberofSources = max(1, min(numberofSources, ESP32_MAXSOURCES)); // limit to 1 - 64 +#endif + // make sure it is a multiple of 4 for proper memory alignment + numberofSources = ((numberofSources+3) >> 2) << 2; + return numberofSources; +} + +//allocate memory for particle system class, particles, sprays plus additional memory requested by FX //TODO: add percentofparticles like in 1D to reduce memory footprint of some FX? +bool allocateParticleSystemMemory2D(uint32_t numparticles, uint32_t numsources, bool isadvanced, bool sizecontrol, uint32_t additionalbytes) { + PSPRINTLN("PS 2D alloc"); + uint32_t requiredmemory = sizeof(ParticleSystem2D); + uint32_t dummy; // dummy variable + if((particleMemoryManager(numparticles, sizeof(PSparticle), dummy, dummy, SEGMENT.mode)) == nullptr) // allocate memory for particles + return false; // not enough memory, function ensures a minimum of numparticles are available + + // functions above make sure these are a multiple of 4 bytes (to avoid alignment issues) + requiredmemory += sizeof(PSparticleFlags) * numparticles; + if (isadvanced) + requiredmemory += sizeof(PSadvancedParticle) * numparticles; + if (sizecontrol) + requiredmemory += sizeof(PSsizeControl) * numparticles; + requiredmemory += sizeof(PSsource) * numsources; + requiredmemory += additionalbytes; + PSPRINTLN("mem alloc: " + String(requiredmemory)); + return(SEGMENT.allocateData(requiredmemory)); +} + +// initialize Particle System, allocate additional bytes if needed (pointer to those bytes can be read from particle system class: PSdataEnd) +bool initParticleSystem2D(ParticleSystem2D *&PartSys, uint32_t requestedsources, uint32_t additionalbytes, bool advanced, bool sizecontrol) { + PSPRINT("PS 2D init "); + if(!strip.isMatrix) return false; // only for 2D + uint32_t cols = SEGMENT.virtualWidth(); + uint32_t rows = SEGMENT.virtualHeight(); + uint32_t pixels = cols * rows; + updateRenderingBuffer(SEGMENT.vWidth() * SEGMENT.vHeight(), true, true); // update or create rendering buffer + if(advanced) + updateRenderingBuffer(100, false, true); // allocate a 10x10 buffer for rendering advanced particles + + uint32_t numparticles = calculateNumberOfParticles2D(pixels, advanced, sizecontrol); + PSPRINT(" segmentsize:" + String(cols) + " " + String(rows)); + PSPRINT(" request numparticles:" + String(numparticles)); + uint32_t numsources = calculateNumberOfSources2D(pixels, requestedsources); + if (!allocateParticleSystemMemory2D(numparticles, numsources, advanced, sizecontrol, additionalbytes)) + { + DEBUG_PRINT(F("PS init failed: memory depleted")); + return false; + } + + PartSys = new (SEGENV.data) ParticleSystem2D(cols, rows, numparticles, numsources, advanced, sizecontrol); // particle system constructor + + PSPRINTLN("******init done, pointers:"); + #ifdef WLED_DEBUG_PS + PSPRINT("framebfr size:"); + PSPRINT(frameBufferSize); + PSPRINT(" @ addr: 0x"); + Serial.println((uintptr_t)framebuffer, HEX); + + PSPRINT("renderbfr size:"); + PSPRINT(renderBufferSize); + PSPRINT(" @ addr: 0x"); + Serial.println((uintptr_t)renderbuffer, HEX); + #endif + return true; +} + +#endif // WLED_DISABLE_PARTICLESYSTEM2D + + +//////////////////////// +// 1D Particle System // +//////////////////////// +#ifndef WLED_DISABLE_PARTICLESYSTEM1D + +ParticleSystem1D::ParticleSystem1D(uint32_t length, uint32_t numberofparticles, uint32_t numberofsources, bool isadvanced) { + effectID = SEGMENT.mode; + numSources = numberofsources; + numParticles = numberofparticles; // number of particles allocated in init + availableParticles = 0; // let the memory manager assign + fractionOfParticlesUsed = 255; // use all particles by default + advPartProps = NULL; //make sure we start out with null pointers (just in case memory was not cleared) + //advPartSize = NULL; + updatePSpointers(isadvanced); // set the particle and sources pointer (call this before accessing sprays or particles) + setSize(length); + setWallHardness(255); // set default wall hardness to max + setGravity(0); //gravity disabled by default + setParticleSize(0); // minimum size by default + motionBlur = 0; //no fading by default + smearBlur = 0; //no smearing by default + emitIndex = 0; + collisionStartIdx = 0; + // initialize some default non-zero values most FX use + for (uint32_t i = 0; i < numSources; i++) { + sources[i].source.ttl = 1; //set source alive + } + + if(isadvanced) { + for (uint32_t i = 0; i < numParticles; i++) { + advPartProps[i].sat = 255; // set full saturation (for particles that are transferred from non-advanced system) + } + } +} + +// update function applies gravity, moves the particles, handles collisions and renders the particles +void ParticleSystem1D::update(void) { + PSadvancedParticle1D *advprop = NULL; + + //apply gravity globally if enabled + if (particlesettings.useGravity) //note: in 1D system, applying gravity after collisions also works but may be worse + applyGravity(); + + // handle collisions (can push particles, must be done before updating particles or they can render out of bounds, causing a crash if using local buffer for speed) + if (particlesettings.useCollisions) + handleCollisions(); + + //move all particles + for (uint32_t i = 0; i < usedParticles; i++) { + particleMoveUpdate(particles[i], particleFlags[i], nullptr, advPartProps ? &advPartProps[i] : nullptr); + } + + if (particlesettings.colorByPosition) { + uint32_t scale = (255 << 16) / maxX; // speed improvement: multiplication is faster than division + for (uint32_t i = 0; i < usedParticles; i++) { + particles[i].hue = (scale * particles[i].x) >> 16; // note: x is > 0 if not out of bounds + } + } + + ParticleSys_render(); +} + +// set percentage of used particles as uint8_t i.e 127 means 50% for example +void ParticleSystem1D::setUsedParticles(const uint8_t percentage) { + fractionOfParticlesUsed = percentage; // note usedParticles is updated in memory manager + updateUsedParticles(numParticles, availableParticles, fractionOfParticlesUsed, usedParticles); + PSPRINT(" SetUsedpaticles: allocated particles: "); + PSPRINT(numParticles); + PSPRINT(" available particles: "); + PSPRINT(availableParticles); + PSPRINT(" ,used percentage: "); + PSPRINT(fractionOfParticlesUsed); + PSPRINT(" ,used particles: "); + PSPRINTLN(usedParticles); +} + +void ParticleSystem1D::setWallHardness(const uint8_t hardness) { + wallHardness = hardness; +} + +void ParticleSystem1D::setSize(const uint32_t x) { + maxXpixel = x - 1; // last physical pixel that can be drawn to + maxX = x * PS_P_RADIUS_1D - 1; // particle system boundary for movements +} + +void ParticleSystem1D::setWrap(const bool enable) { + particlesettings.wrap = enable; +} + +void ParticleSystem1D::setBounce(const bool enable) { + particlesettings.bounce = enable; +} + +void ParticleSystem1D::setKillOutOfBounds(const bool enable) { + particlesettings.killoutofbounds = enable; +} + +void ParticleSystem1D::setColorByAge(const bool enable) { + particlesettings.colorByAge = enable; +} + +void ParticleSystem1D::setColorByPosition(const bool enable) { + particlesettings.colorByPosition = enable; +} + +void ParticleSystem1D::setMotionBlur(const uint8_t bluramount) { + motionBlur = bluramount; +} + +void ParticleSystem1D::setSmearBlur(const uint8_t bluramount) { + smearBlur = bluramount; +} + +// render size, 0 = 1 pixel, 1 = 2 pixel (interpolated), bigger sizes require adanced properties +void ParticleSystem1D::setParticleSize(const uint8_t size) { + particlesize = size > 0 ? 1 : 0; // TODO: add support for global sizes? see note above (motion blur) + if (particlesize) + particleHardRadius = PS_P_MINHARDRADIUS_1D; // 2 pixel sized particles + else + particleHardRadius = PS_P_MINHARDRADIUS_1D >> 1; // 1 pixel sized particles have half the radius (for bounce, not for collisions) +} + +// enable/disable gravity, optionally, set the force (force=8 is default) can be -127 to +127, 0 is disable +// if enabled, gravity is applied to all particles in ParticleSystemUpdate() +// 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 ParticleSystem1D::setGravity(const int8_t force) { + if (force) { + gforce = force; + particlesettings.useGravity = true; + } + else + particlesettings.useGravity = false; +} + +void ParticleSystem1D::enableParticleCollisions(const bool enable, const uint8_t hardness) { + particlesettings.useCollisions = enable; + collisionHardness = hardness; +} + +// emit one particle with variation, returns index of last emitted particle (or -1 if no particle emitted) +int32_t ParticleSystem1D::sprayEmit(const PSsource1D &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].vx = emitter.v + hw_random16(emitter.var << 1) - emitter.var; // random(-var,var) + particles[emitIndex].x = emitter.source.x; + particles[emitIndex].hue = emitter.source.hue; + particles[emitIndex].ttl = hw_random16(emitter.minLife, emitter.maxLife); + particleFlags[emitIndex].collide = emitter.sourceFlags.collide; + particleFlags[emitIndex].reversegrav = emitter.sourceFlags.reversegrav; + particleFlags[emitIndex].perpetual = emitter.sourceFlags.perpetual; + if (advPartProps) { + advPartProps[emitIndex].sat = emitter.sat; + advPartProps[emitIndex].size = emitter.size; + } + return emitIndex; + } + } + return -1; +} + +// particle moves, decays and dies, if killoutofbounds is set, out of bounds particles are set to ttl=0 +// uses passed settings to set bounce or wrap, if useGravity is set, it will never bounce at the top and killoutofbounds is not applied over the top +void ParticleSystem1D::particleMoveUpdate(PSparticle1D &part, PSparticleFlags1D &partFlags, PSsettings1D *options, PSadvancedParticle1D *advancedproperties) { + if (options == NULL) + options = &particlesettings; // use PS system settings by default + + if (part.ttl > 0) { + if (!partFlags.perpetual) + part.ttl--; // age + if (options->colorByAge) + part.hue = min(part.ttl, (uint16_t)255); // set color to ttl + + int32_t renderradius = PS_P_HALFRADIUS_1D; // used to check out of bounds, default for 2 pixel rendering + int32_t newX = part.x + (int32_t)part.vx; + partFlags.outofbounds = false; // reset out of bounds (in case particle was created outside the matrix and is now moving into view) + + if (advancedproperties) { // using individual particle size? + if (advancedproperties->size > 1) + particleHardRadius = PS_P_MINHARDRADIUS_1D + (advancedproperties->size >> 1); + else // single pixel particles use half the collision distance for walls + particleHardRadius = PS_P_MINHARDRADIUS_1D >> 1; + renderradius = particleHardRadius; // note: for single pixel particles, it should be zero, but it does not matter as out of bounds checking is done in rendering function + } + + // if wall collisions are enabled, bounce them before they reach the edge, it looks much nicer if the particle is not half out of view + if (options->bounce) { + if ((newX < (int32_t)particleHardRadius) || ((newX > (int32_t)(maxX - particleHardRadius)))) { // reached a wall + bool bouncethis = true; + if (options->useGravity) { + if (partFlags.reversegrav) { // skip bouncing at x = 0 + if (newX < (int32_t)particleHardRadius) + bouncethis = false; + } else if (newX > (int32_t)particleHardRadius) { // skip bouncing at x = max + bouncethis = false; + } + } + if (bouncethis) { + part.vx = -part.vx; // invert speed + part.vx = ((int32_t)part.vx * (int32_t)wallHardness) / 255; // reduce speed as energy is lost on non-hard surface + if (newX < (int32_t)particleHardRadius) + newX = particleHardRadius; // fast particles will never reach the edge if position is inverted, this looks better + else + newX = maxX - particleHardRadius; + } + } + } + + if (!checkBoundsAndWrap(newX, maxX, renderradius, options->wrap)) { // check out of bounds note: this must not be skipped or it can lead to crashes + partFlags.outofbounds = true; + if (options->killoutofbounds) { + bool killthis = true; + if (options->useGravity) { // if gravity is used, only kill below 'floor level' + if (partFlags.reversegrav) { // skip at x = 0, do not skip far out of bounds + if (newX < 0 || newX > maxX << 2) + killthis = false; + } else { // skip at x = max, do not skip far out of bounds + if (newX > 0 && newX < maxX << 2) + killthis = false; + } + } + if (killthis) + part.ttl = 0; + } + } + + if (!partFlags.fixed) + part.x = newX; // set new position + else + part.vx = 0; // set speed to zero. note: particle can get speed in collisions, if unfixed, it should not speed away + } +} + +// apply a force in x direction to individual particle (or source) +// caller needs to provide a 8bit counter (for each paticle) that holds its value between calls +// force is in 3.4 fixed point notation so force=16 means apply v+1 each frame default of 8 is every other frame +void ParticleSystem1D::applyForce(PSparticle1D &part, const int8_t xforce, uint8_t &counter) { + int32_t dv = calcForce_dv(xforce, counter); // velocity increase + part.vx = limitSpeed((int32_t)part.vx + dv); // apply the force to particle +} + +// apply a force to all particles +// force is in 3.4 fixed point notation (see above) +void ParticleSystem1D::applyForce(const int8_t xforce) { + int32_t dv = calcForce_dv(xforce, forcecounter); // velocity increase + for (uint32_t i = 0; i < usedParticles; i++) { + particles[i].vx = limitSpeed((int32_t)particles[i].vx + dv); + } +} + +// apply gravity to all particles using PS global gforce setting +// gforce is in 3.4 fixed point notation, see note above +void ParticleSystem1D::applyGravity() { + int32_t dv_raw = calcForce_dv(gforce, gforcecounter); + for (uint32_t i = 0; i < usedParticles; i++) { + int32_t dv = dv_raw; + if (particleFlags[i].reversegrav) dv = -dv_raw; + // note: not checking if particle is dead is omitted as most are usually alive and if few are alive, rendering is fast anyways + particles[i].vx = limitSpeed((int32_t)particles[i].vx - dv); + particleFlags[i].forcedirection = particleFlags[i].reversegrav; // set force direction flag (for collisions) + } +} + +// apply gravity to single particle using system settings (use this for sources) +// function does not increment gravity counter, if gravity setting is disabled, this cannot be used +void ParticleSystem1D::applyGravity(PSparticle1D &part, PSparticleFlags1D &partFlags) { + uint32_t counterbkp = gforcecounter; + int32_t dv = calcForce_dv(gforce, gforcecounter); + if (partFlags.reversegrav) dv = -dv; + gforcecounter = counterbkp; //save it back + part.vx = limitSpeed((int32_t)part.vx - dv); + partFlags.forcedirection = partFlags.reversegrav; // set force direction flag (for collisions) +} + + +// slow down particle by friction, the higher the speed, the higher the friction. a high friction coefficient slows them more (255 means instant stop) +// note: a coefficient smaller than 0 will speed them up (this is a feature, not a bug), coefficient larger than 255 inverts the speed, so don't do that +void ParticleSystem1D::applyFriction(int32_t coefficient) { + int32_t friction = 255 - coefficient; + for (uint32_t i = 0; i < usedParticles; i++) { + if (particles[i].ttl) + particles[i].vx = ((int32_t)particles[i].vx * friction) / 255; // note: cannot use bitshift as vx can be negative + } +} + + +// 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 +// warning: do not render out of bounds particles or system will crash! rendering does not check if particle is out of bounds +void ParticleSystem1D::ParticleSys_render() { + CRGB baseRGB; + uint32_t brightness; // particle brightness, fades if dying + static bool useAdditiveTransfer; // use add instead of set for buffer transferring + + // update global blur (used for blur transitions) + int32_t motionbluramount = motionBlur; + int32_t smearamount = smearBlur; + if(pmem->inTransition == effectID) { // FX transition and this is the new FX: fade blur amount + motionbluramount = globalBlur + (((motionbluramount - globalBlur) * (int)SEGMENT.progress()) >> 16); // fade from old blur to new blur during transitions + smearamount = globalSmear + (((smearamount - globalSmear) * (int)SEGMENT.progress()) >> 16); + } + globalBlur = motionbluramount; + globalSmear = smearamount; + + if (framebuffer) { + if(segmentIsOverlay()) useAdditiveTransfer = true; // overlay rendering + else useAdditiveTransfer = false; + // handle buffer blurring or clearing + bool bufferNeedsUpdate = (!pmem->inTransition || pmem->inTransition == effectID); // not a transition; or new FX: update buffer (blur, or clear) + if(bufferNeedsUpdate) { + if (globalBlur > 0 || globalSmear > 0) { // blurring active: if not a transition or is newFX, read data from segment before blurring (old FX can render to it afterwards) + for (int32_t x = 0; x <= maxXpixel; x++) { + if (!renderSolo) // sharing the framebuffer with another segment: read buffer back from segment + framebuffer[x] = SEGMENT.getPixelColor(x); // copy to local buffer + fast_color_scale(framebuffer[x], motionBlur); + } + } + else { // no blurring: clear buffer + memset(framebuffer, 0, frameBufferSize * sizeof(CRGB)); + } + } + } + else { // no local buffer available + if (motionBlur > 0) + SEGMENT.fadeToBlackBy(255 - motionBlur); + else + SEGMENT.fill(BLACK); // clear the buffer before rendering to it + } + + // go over particles and render them to the buffer + for (uint32_t i = 0; i < usedParticles; i++) { + if ( particles[i].ttl == 0 || particleFlags[i].outofbounds) + continue; + + // generate RGB values for particle + brightness = min(particles[i].ttl << 1, (int)255); + baseRGB = ColorFromPalette(SEGPALETTE, particles[i].hue, 255); + + if (advPartProps) { //saturation is advanced property in 1D system + if (advPartProps[i].sat < 255) { + CHSV32 baseHSV; + rgb2hsv((uint32_t((byte(baseRGB.r) << 16) | (byte(baseRGB.g) << 8) | (byte(baseRGB.b)))), baseHSV); // convert to HSV + baseHSV.s = advPartProps[i].sat; // set the saturation + uint32_t tempcolor; + hsv2rgb(baseHSV, tempcolor); // convert back to RGB + baseRGB = (CRGB)tempcolor; + } + } + renderParticle(i, brightness, baseRGB, particlesettings.wrap); + } + // apply smear-blur to rendered frame + if(globalSmear > 0) { + if (framebuffer) + blur1D(framebuffer, maxXpixel + 1, globalSmear, 0); + else + SEGMENT.blur(globalSmear, true); + } + + // add background color + uint32_t bg_color = SEGCOLOR(1); + if (bg_color > 0) { //if not black + for(int32_t i = 0; i <= maxXpixel; i++) { + if (framebuffer) + fast_color_add(framebuffer[i], bg_color); + else + SEGMENT.addPixelColor(i, bg_color, true); + } + } + // transfer local buffer back to segment (if available) + transferBuffer(maxXpixel + 1, 0, useAdditiveTransfer); +} + +// calculate pixel positions and brightness distribution and render the particle to local buffer or global buffer +void ParticleSystem1D::renderParticle(const uint32_t particleindex, const uint32_t brightness, const CRGB &color, const bool wrap) { + uint32_t size = particlesize; + if (advPartProps) {// use advanced size properties + size = advPartProps[particleindex].size; + } + if (size == 0) { //single pixel particle, can be out of bounds as oob checking is made for 2-pixel particles (and updating it uses more code) + uint32_t x = particles[particleindex].x >> PS_P_RADIUS_SHIFT_1D; + if (x <= (uint32_t)maxXpixel) { //by making x unsigned there is no need to check < 0 as it will overflow + if (framebuffer) + fast_color_add(framebuffer[x], color, brightness); + else + SEGMENT.addPixelColor(x, color.scale8((uint8_t)brightness), true); + } + } + else { //render larger particles + bool pxlisinframe[2] = {true, true}; + int32_t pxlbrightness[2]; + int32_t pixco[2]; // physical pixel coordinates of the two pixels representing a particle + + // add half a radius as the rendering algorithm always starts at the bottom left, this leaves things positive, so shifts can be used, then shift coordinate by a full pixel (x-- below) + int32_t xoffset = particles[particleindex].x + PS_P_HALFRADIUS_1D; + int32_t dx = xoffset & (PS_P_RADIUS_1D - 1); //relativ particle position in subpixel space, modulo replaced with bitwise AND + int32_t x = xoffset >> PS_P_RADIUS_SHIFT_1D; // divide by PS_P_RADIUS, bitshift of negative number stays negative -> checking below for x < 0 works (but does not when using division) + + // set the raw pixel coordinates + pixco[1] = x; // right pixel + x--; // shift by a full pixel here, this is skipped above to not do -1 and then +1 + pixco[0] = x; // left pixel + + //calculate the brightness values for both pixels using linear interpolation (note: in standard rendering out of frame pixels could be skipped but if checks add more clock cycles over all) + pxlbrightness[0] = (((int32_t)PS_P_RADIUS_1D - dx) * brightness) >> PS_P_SURFACE_1D; + pxlbrightness[1] = (dx * brightness) >> PS_P_SURFACE_1D; + + // check if particle has advanced size properties and buffer is available + if (advPartProps && advPartProps[particleindex].size > 1) { + if (renderbuffer) { + memset(renderbuffer, 0, 10 * sizeof(CRGB)); // clear the buffer, renderbuffer is 10 pixels + } + else + return; // cannot render advanced particles without buffer + + //render particle to a bigger size + //particle size to pixels: 2 - 63 is 4 pixels, < 128 is 6pixels, < 192 is 8 pixels, bigger is 10 pixels + //first, render the pixel to the center of the renderbuffer, then apply 1D blurring + fast_color_add(renderbuffer[4], color, pxlbrightness[0]); + fast_color_add(renderbuffer[5], color, pxlbrightness[1]); + uint32_t rendersize = 2; // initialize render size, minimum is 4 pixels, it is incremented int he loop below to start with 4 + uint32_t offset = 4; // offset to zero coordinate to write/read data in renderbuffer (actually needs to be 3, is decremented in the loop below) + uint32_t blurpasses = size/64 + 1; // number of blur passes depends on size, four passes max + uint32_t bitshift = 0; + for (uint32_t i = 0; i < blurpasses; i++) { + if (i == 2) //for the last two passes, use higher amount of blur (results in a nicer brightness gradient with soft edges) + bitshift = 1; + rendersize += 2; + offset--; + blur1D(renderbuffer, rendersize, size << bitshift, offset); + size = size > 64 ? size - 64 : 0; + } + + // calculate origin coordinates to render the particle to in the framebuffer + uint32_t xfb_orig = x - (rendersize>>1) + 1 - offset; //note: using uint is fine + uint32_t xfb; // coordinates in frame buffer to write to note: by making this uint, only overflow has to be checked + + // transfer particle renderbuffer to framebuffer + for (uint32_t xrb = offset; xrb < rendersize+offset; xrb++) { + xfb = xfb_orig + xrb; + if (xfb > (uint32_t)maxXpixel) { + if (wrap) { // wrap x to the other side if required + if (xfb > (uint32_t)maxXpixel << 1) // xfb is "negative" + xfb = (maxXpixel + 1) + (int32_t)xfb; // this always overflows to within bounds + else + xfb = xfb % (maxXpixel + 1); // note: without the above "negative" check, this works only for powers of 2 + } + else + continue; + } + if (framebuffer) + fast_color_add(framebuffer[xfb], renderbuffer[xrb]); + else + SEGMENT.addPixelColor(xfb, renderbuffer[xrb]); + } + } + else { // standard rendering (2 pixels per particle) + // check if any pixels are out of frame + if (x < 0) { // left pixels out of frame + if (wrap) // wrap x to the other side if required + pixco[0] = maxXpixel; + else + pxlisinframe[0] = false; // pixel is out of matrix boundaries, do not render + } + else if (pixco[1] > (int32_t)maxXpixel) { // right pixel, only has to be checkt if left pixel did not overflow + if (wrap) // wrap y to the other side if required + pixco[1] = 0; + else + pxlisinframe[1] = false; + } + for(uint32_t i = 0; i < 2; i++) { + if (pxlisinframe[i]) { + if (framebuffer) + fast_color_add(framebuffer[pixco[i]], color, pxlbrightness[i]); + else + SEGMENT.addPixelColor(pixco[i], color.scale8((uint8_t)pxlbrightness[i]), true); + } + } + } + } +} + +// detect collisions in an array of particles and handle them +void ParticleSystem1D::handleCollisions() { + int32_t collisiondistance = PS_P_MINHARDRADIUS_1D; + // note: partices are binned by position, assumption is that no more than half of the particles are in the same bin + // if they are, collisionStartIdx is increased so each particle collides at least every second frame (which still gives decent collisions) + constexpr uint32_t BIN_WIDTH = 32 * PS_P_RADIUS_1D; // width of each bin, a compromise between speed and accuracy (lareger bins are faster but collapse more) + uint32_t maxBinParticles = max((uint32_t)50, (usedParticles + 1) / 4); // do not bin small amounts, limit max to 1/2 of particles + uint32_t numBins = (maxX + (BIN_WIDTH - 1)) / BIN_WIDTH; // calculate number of bins + uint16_t binIndices[maxBinParticles]; // array to store indices of particles in a bin + uint32_t binParticleCount; // number of particles in the current bin + uint16_t nextFrameStartIdx = 0; // index of the first particle in the next frame (set if bin overflow) + uint32_t pidx = collisionStartIdx; //start index in case a bin is full, process remaining particles next frame + for (uint32_t bin = 0; bin < numBins; bin++) { + binParticleCount = 0; // reset for this bin + int32_t binStart = bin * BIN_WIDTH; + int32_t binEnd = binStart + BIN_WIDTH; + + // fill the binIndices array for this bin + for (uint32_t i = 0; i < usedParticles; i++) { + if (particles[pidx].ttl > 0 && particleFlags[pidx].outofbounds == 0 && particleFlags[pidx].collide) { // colliding particle + // if gravity is not used and wall bounce is enabled: particles in the first or last bin use fixed force direction (no collapsing, no push inversion) + if (!particlesettings.useGravity && particlesettings.bounce) { + if (particles[pidx].x < BIN_WIDTH) + particleFlags[pidx].forcedirection = false; + else if (particles[pidx].x > (maxX - BIN_WIDTH)) + particleFlags[pidx].forcedirection = true; + } + if (particles[pidx].x >= binStart && particles[pidx].x <= binEnd) { // >= and <= to include particles on the edge of the bin (overlap to ensure boarder particles collide with adjacent bins) + if (binParticleCount >= maxBinParticles) { // bin is full, more particles in this bin so do the rest next frame + nextFrameStartIdx = pidx; // bin overflow can only happen once as bin size is at least half of the particles (or half +1) + break; + } + binIndices[binParticleCount++] = pidx; + } + } + pidx++; + if (pidx >= usedParticles) pidx = 0; // wrap around + } + + for (uint32_t i = 0; i < binParticleCount; i++) { // go though all 'higher number' particles and see if any of those are in close proximity and if they are, make them collide + uint32_t idx_i = binIndices[i]; + for (uint32_t j = i + 1; j < binParticleCount; j++) { // check against higher number particles + uint32_t idx_j = binIndices[j]; + if (advPartProps) { // use advanced size properties + collisiondistance = PS_P_MINHARDRADIUS_1D + (((uint32_t)advPartProps[idx_i].size + (uint32_t)advPartProps[idx_j].size) >> 1); + } + int32_t dx = particles[idx_j].x - particles[idx_i].x; + int32_t dv = (int32_t)particles[idx_j].vx - (int32_t)particles[idx_i].vx; + int32_t proximity = collisiondistance; + if (dv >= proximity) // particles would go past each other in next move update + proximity += abs(dv); // add speed difference to catch fast particles + if (dx < proximity && dx > -proximity) { // check if close + collideParticles(particles[idx_i], particleFlags[idx_i], particles[idx_j], particleFlags[idx_j], dx, dv, collisiondistance); + } + } + } + } + collisionStartIdx = nextFrameStartIdx; // set the start index for the next frame +} +// 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 ParticleSystem1D::collideParticles(PSparticle1D &particle1, const PSparticleFlags1D &particle1flags, PSparticle1D &particle2, const PSparticleFlags1D &particle2flags, int32_t dx, int32_t relativeVx, uint32_t collisiondistance) { + int32_t dotProduct = (dx * relativeVx); // is always negative if moving towards each other + if (dotProduct < 0) { // particles are moving towards each other + uint32_t surfacehardness = max(collisionHardness, (int32_t)PS_P_MINSURFACEHARDNESS_1D); // if particles are soft, the impulse must stay above a limit or collisions slip through + // Calculate new velocities after collision + int32_t impulse = relativeVx * surfacehardness / 255; + particle1.vx += impulse; + particle2.vx -= impulse; + + // if one of the particles is fixed, transfer the impulse back so it bounces + if (particle1flags.fixed) + particle2.vx = -particle1.vx; + else if (particle2flags.fixed) + particle1.vx = -particle2.vx; + + if (collisionHardness < PS_P_MINSURFACEHARDNESS_1D) { // if particles are soft, they become 'sticky' i.e. apply some friction + const uint32_t coeff = collisionHardness + (250 - PS_P_MINSURFACEHARDNESS_1D); + particle1.vx = ((int32_t)particle1.vx * coeff) / 255; + particle2.vx = ((int32_t)particle2.vx * coeff) / 255; + } + } + + uint32_t distance = abs(dx); + // particles have volume, push particles apart if they are too close + // behaviour is different than in 2D, we need pixel accurate stacking here, push the top particle to full radius (direction is well defined in 1D) + // also need to give the top particle some speed to counteract gravity or stacks just collapse + if (distance < collisiondistance) { // particles are too close, push the upper particle away + int32_t pushamount = 1 + ((collisiondistance - distance) >> 1); //add half the remaining distance note: this works best, if less or more is added, it gets more chaotic + + // Only force-push if particles use gravity or are not really close or are in the outer quarter of the strip + if (particlesettings.bounce && (particlesettings.useGravity || distance > 3 || particle1.x < (maxX >> 2) || particle1.x > (maxX - (maxX >> 2)))) { + // use force direction flag to push the 'upper' particle only, avoids stack-collapse + if (dx < 0) { // particle2.x < particle1.x, dx = p2.x - p1.x + if (particle2flags.forcedirection && !particle2flags.fixed) { + particle2.x -= pushamount; + particle2.vx--; + } else if (!particle1flags.forcedirection && !particle1flags.fixed) { + particle1.x += pushamount; + particle1.vx++; + } + } else { // particle1.x < particle2.x, dx = p2.x - p1.x + if (particle1flags.forcedirection && !particle1flags.fixed) { + particle1.x -= pushamount; + particle1.vx--; + } else if (!particle2flags.forcedirection && !particle2flags.fixed) { + particle2.x += pushamount; + particle2.vx++; + } + } + } + else { // no wall bounce, not using gravity, push both particles by applying a little velocity (like in 2D system) + pushamount = 2; + if (dx < 0) // particle2.x < particle1.x + pushamount = -pushamount; + particle1.vx -= pushamount; + particle2.vx += pushamount; + } + } +} + +// update size and pointers (memory location and size can change dynamically) +// note: do not access the PS class in FX befor running this function (or it messes up SEGENV.data) +void ParticleSystem1D::updateSystem(void) { + setSize(SEGMENT.vLength()); // update size + updateRenderingBuffer(SEGMENT.vLength(), true, false); // update rendering buffer (segment size can change at any time) + updatePSpointers(advPartProps != NULL); + setUsedParticles(fractionOfParticlesUsed); // update used particles based on percentage (can change during transitions, execute each frame for code simplicity) + if (partMemList.size() == 1) // if number of vector elements is one, this is the only system + renderSolo = true; + else + renderSolo = false; +} + +// 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 ParticleSystem1D::updatePSpointers(bool isadvanced) { + // Note on memory alignment: + // a pointer MUST be 4 byte aligned. sizeof() in a struct/class is always aligned to the largest element. if it contains a 32bit, it will be padded to 4 bytes, 16bit is padded to 2byte alignment. + // The PS is aligned to 4 bytes, a PSparticle is aligned to 2 and a struct containing only byte sized variables is not aligned at all and may need to be padded when dividing the memoryblock. + // by making sure that the number of sources and particles is a multiple of 4, padding can be skipped here as alignent is ensured, independent of struct sizes. + + // memory manager needs to know how many particles the FX wants to use so transitions can be handled properly (i.e. pointer will stop changing if enough particles are available during transitions) + uint32_t usedByFX = (numParticles * ((uint32_t)fractionOfParticlesUsed + 1)) >> 8; // final number of particles the FX wants to use (fractionOfParticlesUsed is 0-255) + particles = reinterpret_cast(particleMemoryManager(0, sizeof(PSparticle1D), availableParticles, usedByFX, effectID)); // get memory, leave buffer size as is (request 0) + particleFlags = reinterpret_cast(this + 1); // pointer to particle flags + sources = reinterpret_cast(particleFlags + numParticles); // pointer to source(s) + PSdataEnd = reinterpret_cast(sources + numSources); // pointer to first available byte after the PS for FX additional data + if (isadvanced) { + advPartProps = reinterpret_cast(sources + numSources); + PSdataEnd = reinterpret_cast(advPartProps + numParticles); + } + #ifdef WLED_DEBUG_PS + PSPRINTLN(" PS Pointers: "); + PSPRINT(" PS : 0x"); + Serial.println((uintptr_t)this, HEX); + PSPRINT(" Sources : 0x"); + Serial.println((uintptr_t)sources, HEX); + PSPRINT(" Particles : 0x"); + Serial.println((uintptr_t)particles, HEX); + #endif +} + +//non class functions to use for initialization, fraction is uint8_t: 255 means 100% +uint32_t calculateNumberOfParticles1D(const uint32_t fraction, const bool isadvanced) { + uint32_t numberofParticles = SEGMENT.virtualLength(); // one particle per pixel (if possible) +#ifdef ESP8266 + uint32_t particlelimit = ESP8266_MAXPARTICLES_1D; // maximum number of paticles allowed +#elif ARDUINO_ARCH_ESP32S2 + uint32_t particlelimit = ESP32S2_MAXPARTICLES_1D; // maximum number of paticles allowed +#else + uint32_t particlelimit = ESP32_MAXPARTICLES_1D; // maximum number of paticles allowed +#endif + numberofParticles = min(numberofParticles, particlelimit); // limit to particlelimit + if (isadvanced) // advanced property array needs ram, reduce number of particles to use the same amount + numberofParticles = (numberofParticles * sizeof(PSparticle1D)) / (sizeof(PSparticle1D) + sizeof(PSadvancedParticle1D)); + numberofParticles = (numberofParticles * (fraction + 1)) >> 8; // calculate fraction of particles + numberofParticles = numberofParticles < 20 ? 20 : numberofParticles; // 20 minimum + //make sure it is a multiple of 4 for proper memory alignment (easier than using padding bytes) + numberofParticles = ((numberofParticles+3) >> 2) << 2; // note: with a separate particle buffer, this is probably unnecessary + return numberofParticles; +} + +uint32_t calculateNumberOfSources1D(const uint32_t requestedsources) { +#ifdef ESP8266 + int numberofSources = max(1, min((int)requestedsources,ESP8266_MAXSOURCES_1D)); // limit to 1 - 8 +#elif ARDUINO_ARCH_ESP32S2 + int numberofSources = max(1, min((int)requestedsources, ESP32S2_MAXSOURCES_1D)); // limit to 1 - 16 +#else + int numberofSources = max(1, min((int)requestedsources, ESP32_MAXSOURCES_1D)); // limit to 1 - 32 +#endif + // make sure it is a multiple of 4 for proper memory alignment (so minimum is acutally 4) + numberofSources = ((numberofSources+3) >> 2) << 2; + return numberofSources; +} + +//allocate memory for particle system class, particles, sprays plus additional memory requested by FX +bool allocateParticleSystemMemory1D(const uint32_t numparticles, const uint32_t numsources, const bool isadvanced, const uint32_t additionalbytes) { + uint32_t requiredmemory = sizeof(ParticleSystem1D); + uint32_t dummy; // dummy variable + if(particleMemoryManager(numparticles, sizeof(PSparticle1D), dummy, dummy, SEGMENT.mode) == nullptr) // allocate memory for particles + return false; // not enough memory, function ensures a minimum of numparticles are avialable + // functions above make sure these are a multiple of 4 bytes (to avoid alignment issues) + requiredmemory += sizeof(PSparticleFlags1D) * numparticles; + if (isadvanced) + requiredmemory += sizeof(PSadvancedParticle1D) * numparticles; + requiredmemory += sizeof(PSsource1D) * numsources; + requiredmemory += additionalbytes; + return(SEGMENT.allocateData(requiredmemory)); +} + +// initialize Particle System, allocate additional bytes if needed (pointer to those bytes can be read from particle system class: PSdataEnd) +// note: percentofparticles is in uint8_t, for example 191 means 75%, (deafaults to 255 or 100% meaning one particle per pixel), can be more than 100% (but not recommended, can cause out of memory) +bool initParticleSystem1D(ParticleSystem1D *&PartSys, const uint32_t requestedsources, const uint8_t fractionofparticles, const uint32_t additionalbytes, const bool advanced) { + if (SEGLEN == 1) return false; // single pixel not supported + updateRenderingBuffer(SEGMENT.vLength(), true, true); // update/create frame rendering buffer + if(advanced) + updateRenderingBuffer(10, false, true); // buffer for advanced particles, fixed size + uint32_t numparticles = calculateNumberOfParticles1D(fractionofparticles, advanced); + uint32_t numsources = calculateNumberOfSources1D(requestedsources); + if (!allocateParticleSystemMemory1D(numparticles, numsources, advanced, additionalbytes)) { + DEBUG_PRINT(F("PS init failed: memory depleted")); + return false; + } + + PartSys = new (SEGENV.data) ParticleSystem1D(SEGMENT.virtualLength(), numparticles, numsources, advanced); // particle system constructor + return true; +} + +// blur a 1D buffer, sub-size blurring can be done using start and size +// for speed, 32bit variables are used, make sure to limit them to 8bit (0-255) or result is undefined +// to blur a subset of the buffer, change the size and set start to the desired starting coordinates +void blur1D(CRGB *colorbuffer, uint32_t size, uint32_t blur, uint32_t start) +{ + CRGB seeppart, carryover; + uint32_t seep = blur >> 1; + carryover = BLACK; + for(uint32_t x = start; x < start + size; x++) { + seeppart = colorbuffer[x]; // create copy of current color + fast_color_scale(seeppart, seep); // scale it and seep to neighbours + if (x > 0) { + fast_color_add(colorbuffer[x-1], seeppart); + if(carryover) // note: check adds overhead but is faster on average + fast_color_add(colorbuffer[x], carryover); // is black on first pass + } + carryover = seeppart; + } +} +#endif // WLED_DISABLE_PARTICLESYSTEM1D + +#if !(defined(WLED_DISABLE_PARTICLESYSTEM2D) && defined(WLED_DISABLE_PARTICLESYSTEM1D)) // not both disabled + +////////////////////////////// +// Shared Utility Functions // +////////////////////////////// + +// calculate the delta speed (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 +static int32_t calcForce_dv(const int8_t force, uint8_t &counter) { + if (force == 0) + return 0; + // 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 = 0; + // 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 : 1; // force is either 1 or -1 if it is small (zero force is handled above) + } + } + else + dv = force / 16; // MSBs, note: cannot use bitshift as dv can be negative + + return dv; +} + +// check if particle is out of bounds and wrap it around if required, returns false if out of bounds +static bool checkBoundsAndWrap(int32_t &position, const int32_t max, const int32_t particleradius, const bool wrap) { + if ((uint32_t)position > (uint32_t)max) { // check if particle reached an edge, cast to uint32_t to save negative checking (max is always positive) + if (wrap) { + position = position % (max + 1); // note: cannot optimize modulo, particles can be far out of bounds when wrap is enabled + if (position < 0) + position += max + 1; + } + else if (((position < -particleradius) || (position > max + particleradius))) // particle is leaving boundaries, out of bounds if it has fully left + return false; // out of bounds + } + return true; // particle is in bounds +} + +// fastled color adding is very inaccurate in color preservation (but it is fast) +// a better color add function is implemented in colors.cpp but it uses 32bit RGBW. to use it colors need to be shifted just to then be shifted back by that function, which is slow +// this is a fast version for RGB (no white channel, PS does not handle white) and with native CRGB including scaling of second color +// note: result is stored in c1, not using a return value is faster as the CRGB struct does not need to be copied upon return +// note2: function is mainly used to add scaled colors, so checking if one color is black is slower +// note3: scale is 255 when using blur, checking for that makes blur faster +static void fast_color_add(CRGB &c1, const CRGB &c2, const uint32_t scale) { + uint32_t r, g, b; + if (scale < 255) { + r = c1.r + ((c2.r * scale) >> 8); + g = c1.g + ((c2.g * scale) >> 8); + b = c1.b + ((c2.b * scale) >> 8); + } else { + r = c1.r + c2.r; + g = c1.g + c2.g; + b = c1.b + c2.b; + } + + uint32_t max = std::max(r,g); // check for overflow, using max() is faster as the compiler can optimize + max = std::max(max,b); + if (max < 256) { + c1.r = r; // save result to c1 + c1.g = g; + c1.b = b; + } else { + uint32_t newscale = (255U << 16) / max; + c1.r = (r * newscale) >> 16; + c1.g = (g * newscale) >> 16; + c1.b = (b * newscale) >> 16; + } +} + +// faster than fastled color scaling as it does in place scaling +static void fast_color_scale(CRGB &c, const uint32_t scale) { + c.r = ((c.r * scale) >> 8); + c.g = ((c.g * scale) >> 8); + c.b = ((c.b * scale) >> 8); +} + + +////////////////////////////////////////////////////////// +// memory and transition management for particle system // +////////////////////////////////////////////////////////// +// note: these functions can only be called while strip is servicing + +// allocate memory using the FX data limit, if overridelimit is set, temporarily ignore the limit +void* allocatePSmemory(size_t size, bool overridelimit) { + PSPRINT(" PS mem alloc: "); + PSPRINTLN(size); + // buffer uses effect data, check if there is enough space + if (!overridelimit && Segment::getUsedSegmentData() + size > MAX_SEGMENT_DATA) { + // not enough memory + PSPRINT(F("!!! Effect RAM depleted: ")); + DEBUG_PRINTF_P(PSTR("%d/%d !!!\n"), size, Segment::getUsedSegmentData()); + errorFlag = ERR_NORAM; + return nullptr; + } + void* buffer = calloc(size, sizeof(byte)); + if (buffer == nullptr) { + PSPRINT(F("!!! Memory allocation failed !!!")); + errorFlag = ERR_NORAM; + return nullptr; + } + Segment::addUsedSegmentData(size); + #ifdef WLED_DEBUG_PS + PSPRINT("Pointer address: 0x"); + Serial.println((uintptr_t)buffer, HEX); + #endif + return buffer; +} + +// deallocate memory and update data usage, use with care! +void deallocatePSmemory(void* dataptr, uint32_t size) { + PSPRINTLN("deallocating PSmemory:" + String(size)); + if(dataptr == nullptr) return; // safety check + free(dataptr); // note: setting pointer null must be done by caller, passing a reference to a cast void pointer is not possible + Segment::addUsedSegmentData(size <= Segment::getUsedSegmentData() ? -size : -Segment::getUsedSegmentData()); +} + +// Particle transition manager, creates/extends buffer if needed and handles transition memory-handover +void* particleMemoryManager(const uint32_t requestedParticles, size_t structSize, uint32_t &availableToPS, uint32_t numParticlesUsed, const uint8_t effectID) { + pmem = getPartMem(); + void* buffer = nullptr; + PSPRINTLN("PS MemManager"); + if (pmem) { // segment has a buffer + if (requestedParticles) { // request for a new buffer, this is an init call + PSPRINTLN("Buffer exists, request for particles: " + String(requestedParticles)); + pmem->transferParticles = true; // set flag to transfer particles + uint32_t requestsize = structSize * requestedParticles; // required buffer size + if (requestsize > pmem->buffersize) { // request is larger than buffer, try to extend it + if (Segment::getUsedSegmentData() + requestsize - pmem->buffersize <= MAX_SEGMENT_DATA) { // enough memory available to extend buffer + buffer = allocatePSmemory(requestsize, true); // calloc new memory in FX data, override limit (temporary buffer) + if (buffer) { // allocaction successful, copy old particles to new buffer + memcpy(buffer, pmem->particleMemPointer, pmem->buffersize); // copy old particle buffer note: only required if transition but copy is fast and rarely happens + deallocatePSmemory(pmem->particleMemPointer, pmem->buffersize); // free old memory + pmem->particleMemPointer = buffer; // set new buffer + pmem->buffersize = requestsize; // update buffer size + } + else + return nullptr; // no memory available + } + } + if (pmem->watchdog == 1) // if a PS already exists during particle request, it kicked the watchdog in last frame, servicePSmem() adds 1 afterwards -> PS to PS transition + pmem->inTransition = effectID; // save the ID of the new effect (required to determine blur amount in rendering function) + return pmem->particleMemPointer; // return the available buffer on init call + } + pmem->watchdog = 0; // kick watchdog + buffer = pmem->particleMemPointer; // buffer is already allocated + } + else { // if the id was not found create a buffer and add an element to the list + PSPRINTLN("New particle buffer request: " + String(requestedParticles)); + uint32_t requestsize = structSize * requestedParticles; // required buffer size + buffer = allocatePSmemory(requestsize, false); // allocate new memory + if (buffer) + partMemList.push_back({buffer, requestsize, 0, strip.getCurrSegmentId(), 0, 0, true}); // add buffer to list, set flag to transfer/init the particles note: if pushback fails, it may crash + else + return nullptr; // there is no memory available TODO: if localbuffer is allocated, free it and try again, its no use having a buffer but no particles + pmem = getPartMem(); // get the pointer to the new element (check that it was added) + if (!pmem) { // something went wrong + free(buffer); + return nullptr; + } + return buffer; // directly return the buffer on init call + } + + // now we have a valid buffer, if this is a PS to PS FX transition: transfer particles slowly to new FX + bool effectchanged = (SEGMENT.currentMode() != SEGMENT.mode); // FX changed, transition the particle buffer + if (effectchanged && pmem->inTransition) { + uint32_t maxParticles = pmem->buffersize / structSize; // maximum number of particles that fit in the buffer + uint16_t progress = SEGMENT.progress(); // transition progress + uint32_t newAvailable = 0; + if (SEGMENT.mode == effectID) { // new effect ID -> function was called from new FX + newAvailable = (maxParticles * progress) >> 16; // update total particles available to this PS (newAvailable is guaranteed to be smaller than maxParticles) + if(newAvailable < 2) newAvailable = 2; // give 2 particle minimum (some FX may crash with less as they do i+1 access) + if(maxParticles / numParticlesUsed > 3 && newAvailable > numParticlesUsed) newAvailable = numParticlesUsed; // limit to number of particles used for FX using a small amount, do not move the pointer anymore (will be set to base in final handover) + uint32_t bufferoffset = (maxParticles - 1) - newAvailable; // offset to new effect particles (in particle structs, not bytes) + if(bufferoffset < maxParticles) // safety check + buffer = (void*)((uint8_t*)buffer + bufferoffset * structSize); // new effect gets the end of the buffer + int32_t totransfer = newAvailable - availableToPS; // number of particles to transfer in this transition update + if(totransfer < 0) totransfer = 0; // safety check + particleHandover(buffer, structSize, totransfer); + } + else { // this was called from the old FX + SEGMENT.setCurrentPalette(true); // load the old palette into segment + progress = 0xFFFFU - progress; // inverted transition progress + newAvailable = ((maxParticles * progress) >> 16); // result is guaranteed to be smaller than maxParticles + if(newAvailable > 0) newAvailable--; // -1 to avoid overlapping memory in 1D<->2D transitions + if(newAvailable < 2) newAvailable = 2; // give 2 particle minimum (some FX may crash with less as they do i+1 access) + // note: buffer pointer stays the same, number of available particles is reduced + } + availableToPS = newAvailable; + } else { // no PS transition, full buffer available + if(pmem->transferParticles) { // transition ended (or blending is disabled) -> transfer all remaining particles + PSPRINTLN("PS transition ended, final particle handover"); + uint32_t maxParticles = pmem->buffersize / structSize; // maximum number of particles that fit in the buffer + if (maxParticles > availableToPS) { // not all particles transferred yet + int32_t totransfer = maxParticles - availableToPS; // transfer all remaining particles + if(totransfer < 0) totransfer = 0; // safety check + particleHandover(buffer, structSize, totransfer); + + if(maxParticles / numParticlesUsed > 3) { // FX uses less than 25%: move the already existing particles to the beginning of the buffer + uint32_t usedbytes = availableToPS * structSize; + int32_t bufferoffset = (maxParticles - 1) - availableToPS; // offset to existing particles (see above) + if(bufferoffset < maxParticles) { // safety check + void* currentBuffer = (void*)((uint8_t*)buffer + bufferoffset * structSize); // pointer to current buffer start + memmove(buffer, currentBuffer, usedbytes); // move the existing particles to the beginning of the buffer + } + } + } + // kill unused particles to they do not re-appear when transitioning to next FX + #ifndef WLED_DISABLE_PARTICLESYSTEM2D + if (structSize == sizeof(PSparticle)) { // 2D particle + PSparticle *particles = (PSparticle *)buffer; + for (uint32_t i = availableToPS; i < maxParticles; i++) { + particles[i].ttl = 0; // kill unused particles + } + } + else // 1D particle system + #endif + { + #ifndef WLED_DISABLE_PARTICLESYSTEM1D + PSparticle1D *particles = (PSparticle1D *)buffer; + for (uint32_t i = availableToPS; i < maxParticles; i++) { + particles[i].ttl = 0; // kill unused particles + } + #endif + } + availableToPS = maxParticles; // now all particles are available to new FX + PSPRINTLN("final available particles: " + String(availableToPS)); + pmem->particleType = structSize; // update particle type + pmem->transferParticles = false; + } + pmem->inTransition = false; + } + #ifdef WLED_DEBUG_PS + PSPRINT(" Particle memory Pointer address: 0x"); + Serial.println((uintptr_t)buffer, HEX); + #endif + return buffer; +} + +// (re)initialize particles in the particle buffer for use in the new FX +void particleHandover(void *buffer, size_t structSize, int32_t numToTransfer) { + if (pmem->particleType != structSize) { // check if we are being handed over from a different system (1D<->2D), clear buffer if so + memset(buffer, 0, numToTransfer * structSize); // clear buffer + } + #ifndef WLED_DISABLE_PARTICLESYSTEM2D + if (structSize == sizeof(PSparticle)) { // 2D particle + PSparticle *particles = (PSparticle *)buffer; + for (int32_t i = 0; i < numToTransfer; i++) { + if (particles[i].ttl > 200) + particles[i].ttl = 150 + hw_random16(50); // reduce TTL so it will die soon + particles[i].sat = 255; // full saturation + } + } + else // 1D particle system + #endif + { + #ifndef WLED_DISABLE_PARTICLESYSTEM1D + PSparticle1D *particles = (PSparticle1D *)buffer; + for (int32_t i = 0; i < numToTransfer; i++) { + if (particles[i].ttl > 200) + particles[i].ttl = 150 + hw_random16(50); // reduce TTL so it will die soon + } + #endif + } +} + +// update number of particles to use, limit to allocated (= particles allocated by the calling system) in case more are available in the buffer +void updateUsedParticles(const uint32_t allocated, const uint32_t available, const uint8_t percentage, uint32_t &used) { + uint32_t wantsToUse = 1 + ((allocated * ((uint32_t)percentage + 1)) >> 8); // always give 1 particle minimum + used = max((uint32_t)2, min(available, wantsToUse)); // limit to available particles, use a minimum of 2 +} + +// check if a segment is fully overlapping with an underlying segment (used to enable overlay rendering i.e. adding instead of overwriting pixels) +bool segmentIsOverlay(void) { // TODO: this only needs to be checked when segment is created, could move this to segment class or PS init + unsigned segID = strip.getCurrSegmentId(); + if(segID > 0) { // lower number segments exist, check coordinates of underlying segments + for (unsigned i = 0; i < segID; i++) { + if(strip._segments[i].start <= strip._segments[segID].start && strip._segments[i].stop >= strip._segments[segID].stop && + strip._segments[i].startY <= strip._segments[segID].startY && strip._segments[i].stopY >= strip._segments[segID].stopY) + return true; + } + } + return false; +} + +// get the pointer to the particle memory for the segment +partMem* getPartMem(void) { + uint8_t segID = strip.getCurrSegmentId(); + for (partMem &pmem : partMemList) { + if (pmem.id == segID) { + return &pmem; + } + } + return nullptr; +} + +// function to update the framebuffer and renderbuffer +void updateRenderingBuffer(uint32_t requiredpixels, bool isFramebuffer, bool initialize) { + PSPRINTLN("updateRenderingBuffer"); + uint16_t& targetBufferSize = isFramebuffer ? frameBufferSize : renderBufferSize; // corresponding buffer size + + // if(isFramebuffer) return; // debug/testing only: disable frame-buffer + + if(targetBufferSize < requiredpixels) { // check current buffer size + CRGB** targetBuffer = isFramebuffer ? &framebuffer : &renderbuffer; // pointer to target buffer + if(*targetBuffer || initialize) { // update only if initilizing or if buffer exists (prevents repeatet allocation attempts if initial alloc failed) + if(*targetBuffer) // buffer exists, free it + deallocatePSmemory((void*)(*targetBuffer), targetBufferSize * sizeof(CRGB)); + *targetBuffer = reinterpret_cast(allocatePSmemory(requiredpixels * sizeof(CRGB), false)); + if(*targetBuffer) + targetBufferSize = requiredpixels; + else + targetBufferSize = 0; + } + } +} + +// service the particle system memory, free memory if idle too long +// note: doing it this way makes it independent of the implementation of segment management but is not the most memory efficient way +void servicePSmem() { + // Increment watchdog for each entry and deallocate if idle too long (i.e. no PS running on that segment) + if(partMemList.size() > 0) { + for (size_t i = 0; i < partMemList.size(); i++) { + if(strip.getSegmentsNum() > i) { // segment still exists + if(strip._segments[i].freeze) continue; // skip frozen segments (incrementing watchdog will delete memory, leading to crash) + } + partMemList[i].watchdog++; // Increment watchdog counter + PSPRINT("pmem servic. list size: "); + PSPRINT(partMemList.size()); + PSPRINT(" element: "); + PSPRINT(i); + PSPRINT(" watchdog: "); + PSPRINTLN(partMemList[i].watchdog); + if (partMemList[i].watchdog > MAX_MEMIDLE) { + deallocatePSmemory(partMemList[i].particleMemPointer, partMemList[i].buffersize); // Free memory + partMemList.erase(partMemList.begin() + i); // Remove entry + //partMemList.shrink_to_fit(); // partMemList is small, memory operations should be unproblematic (this may lead to mem fragmentation, removed for now) + } + } + } + else { // no particle system running, release buffer memory + if(framebuffer) { + deallocatePSmemory((void*)framebuffer, frameBufferSize * sizeof(CRGB)); // free the buffers + framebuffer = nullptr; + frameBufferSize = 0; + } + if(renderbuffer) { + deallocatePSmemory((void*)renderbuffer, renderBufferSize * sizeof(CRGB)); + renderbuffer = nullptr; + renderBufferSize = 0; + } + } +} + +// transfer the frame buffer to the segment and handle transitional rendering (both FX render to the same buffer so they mix) +void transferBuffer(uint32_t width, uint32_t height, bool useAdditiveTransfer) { + if(!framebuffer) return; // no buffer, nothing to transfer + PSPRINT(" xfer buf "); + #ifndef WLED_DISABLE_MODE_BLEND + bool tempBlend = SEGMENT.getmodeBlend(); + if (pmem->inTransition) + SEGMENT.modeBlend(false); // temporarily disable FX blending in PS to PS transition (using local buffer to do PS blending) + #endif + + if(height) { // is 2D, 1D passes height = 0 + for (uint32_t y = 0; y < height; y++) { + int index = y * width; // current row index for 1D buffer + for (uint32_t x = 0; x < width; x++) { + CRGB *c = &framebuffer[index++]; + uint32_t clr = RGBW32(c->r,c->g,c->b,0); // convert to 32bit color + if(useAdditiveTransfer) { + uint32_t segmentcolor = SEGMENT.getPixelColorXY((int)x, (int)y); + CRGB segmentRGB = CRGB(segmentcolor); + if(clr == 0) // frame buffer is black, just update the framebuffer + *c = segmentRGB; + else { // color to add to segment is not black + if(segmentcolor) { + fast_color_add(*c, segmentRGB); // add segment color back to buffer if not black + clr = RGBW32(c->r,c->g,c->b,0); // convert to 32bit color (again) and set the segment + } + SEGMENT.setPixelColorXY((int)x, (int)y, clr); // save back to segment after adding local buffer + } + } + //if(clr > 0) // not black TODO: not transferring black is faster and enables overlay, but requires proper handling of buffer clearing, which is quite complex and probably needs a change to SEGMENT handling. + else + SEGMENT.setPixelColorXY((int)x, (int)y, clr); + } + } + } else { // 1D system + for (uint32_t x = 0; x < width; x++) { + CRGB *c = &framebuffer[x]; + uint32_t clr = RGBW32(c->r,c->g,c->b,0); + if(useAdditiveTransfer) { + uint32_t segmentcolor = SEGMENT.getPixelColor((int)x);; + CRGB segmentRGB = CRGB(segmentcolor); + if(clr == 0) // frame buffer is black, just load the color (for next frame) + *c = segmentRGB; + else { // color to add to segment is not black + if(segmentcolor) { + fast_color_add(*c, segmentRGB); // add segment color back to buffer if not black + clr = RGBW32(c->r,c->g,c->b,0); // convert to 32bit color (again) + } + SEGMENT.setPixelColor((int)x, clr); // save back to segment after adding local buffer + } + } + //if(color > 0) // not black + else + SEGMENT.setPixelColor((int)x, clr); + } + } + #ifndef WLED_DISABLE_MODE_BLEND + SEGMENT.modeBlend(tempBlend); // restore blending mode + #endif +} + +#endif // !(defined(WLED_DISABLE_PARTICLESYSTEM2D) && defined(WLED_DISABLE_PARTICLESYSTEM1D)) diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h new file mode 100644 index 0000000000..b9ad170c2f --- /dev/null +++ b/wled00/FXparticleSystem.h @@ -0,0 +1,414 @@ +/* + FXparticleSystem.cpp + + Particle system with functions for particle generation, particle movement and particle rendering to RGB matrix. + by DedeHai (Damian Schneider) 2013-2024 + + Copyright (c) 2024 Damian Schneider + Licensed under the EUPL v. 1.2 or later +*/ + +#ifdef WLED_DISABLE_2D +#define WLED_DISABLE_PARTICLESYSTEM2D +#endif + +#if !(defined(WLED_DISABLE_PARTICLESYSTEM2D) && defined(WLED_DISABLE_PARTICLESYSTEM1D)) // not both disabled + +#include +#include "wled.h" + +#define PS_P_MAXSPEED 120 // maximum speed a particle can have (vx/vy is int8) +#define MAX_MEMIDLE 10 // max idle time (in frames) before memory is deallocated (if deallocated during an effect, it will crash!) + +//#define WLED_DEBUG_PS // note: enabling debug uses ~3k of flash + +#ifdef WLED_DEBUG_PS + #define PSPRINT(x) Serial.print(x) + #define PSPRINTLN(x) Serial.println(x) +#else + #define PSPRINT(x) + #define PSPRINTLN(x) +#endif + +// memory and transition manager +struct partMem { + void* particleMemPointer; // pointer to particle memory + uint32_t buffersize; // buffer size in bytes + uint8_t particleType; // type of particles currently in memory: 0 = none, particle struct size otherwise (required for 1D<->2D transitions) + uint8_t id; // ID of segment this memory belongs to + uint8_t watchdog; // counter to handle deallocation + uint8_t inTransition; // to track PS to PS FX transitions (is set to new FX ID during transitions), not set if not both FX are PS FX + bool transferParticles; // if set, particles in buffer are transferred to new FX +}; + +void* particleMemoryManager(const uint32_t requestedParticles, size_t structSize, uint32_t &availableToPS, uint32_t numParticlesUsed, const uint8_t effectID); // update particle memory pointer, handles memory transitions +void particleHandover(void *buffer, size_t structSize, int32_t numParticles); +void updateUsedParticles(const uint32_t allocated, const uint32_t available, const uint8_t percentage, uint32_t &used); +bool segmentIsOverlay(void); // check if segment is fully overlapping with at least one underlying segment +partMem* getPartMem(void); // returns pointer to memory struct for current segment or nullptr +void updateRenderingBuffer(uint32_t requiredpixels, bool isFramebuffer, bool initialize); // allocate CRGB rendering buffer, update size if needed +void transferBuffer(uint32_t width, uint32_t height, bool useAdditiveTransfer = false); // transfer the buffer to the segment (supports 1D and 2D) +void servicePSmem(); // increments watchdog, frees memory if idle too long + +// limit speed of particles (used in 1D and 2D) +static inline int32_t limitSpeed(const int32_t speed) { + return speed > PS_P_MAXSPEED ? PS_P_MAXSPEED : (speed < -PS_P_MAXSPEED ? -PS_P_MAXSPEED : speed); // note: this is slightly faster than using min/max at the cost of 50bytes of flash +} +#endif + +#ifndef WLED_DISABLE_PARTICLESYSTEM2D +// memory allocation +#define ESP8266_MAXPARTICLES 300 // enough up to 20x20 pixels +#define ESP8266_MAXSOURCES 24 +#define ESP32S2_MAXPARTICLES 1024 // enough up to 32x32 pixels +#define ESP32S2_MAXSOURCES 64 +#define ESP32_MAXPARTICLES 2048 // enough up to 64x32 pixels +#define ESP32_MAXSOURCES 128 + +// particle dimensions (subpixel division) +#define PS_P_RADIUS 64 // subpixel size, each pixel is divided by this for particle movement (must be a power of 2) +#define PS_P_HALFRADIUS (PS_P_RADIUS >> 1) +#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_MINHARDRADIUS 70 // minimum hard surface radius for collisions +#define PS_P_MINSURFACEHARDNESS 128 // minimum hardness used in collision impulse calculation, below this hardness, particles become sticky + +// struct for PS settings (shared for 1D and 2D class) +typedef union { + struct{ // one byte bit field for 2D settings + bool wrapX : 1; + bool wrapY : 1; + bool bounceX : 1; + bool bounceY : 1; + bool killoutofbounds : 1; // if set, out of bound particles are killed immediately + bool useGravity : 1; // set to 1 if gravity is used, disables bounceY at the top + bool useCollisions : 1; + bool colorByAge : 1; // if set, particle hue is set by ttl value in render function + }; + byte asByte; // access as a byte, order is: LSB is first entry in the list above +} PSsettings2D; + +//struct for a single particle +typedef struct { // 10 bytes + int16_t x; // x position in particle system + int16_t y; // y position in particle system + uint16_t ttl; // time to live in frames + int8_t vx; // horizontal velocity + int8_t vy; // vertical velocity + uint8_t hue; // color hue + uint8_t sat; // particle color saturation +} PSparticle; + +//struct for particle flags note: this is separate from the particle struct to save memory (ram alignment) +typedef union { + struct { // 1 byte + bool outofbounds : 1; // out of bounds flag, set to true if particle is outside of display area + bool collide : 1; // if set, particle takes part in collisions + bool perpetual : 1; // if set, particle does not age (TTL is not decremented in move function, it still dies from killoutofbounds) + bool custom1 : 1; // unused custom flags, can be used by FX to track particle states + bool custom2 : 1; + bool custom3 : 1; + bool custom4 : 1; + bool custom5 : 1; + }; + byte asByte; // access as a byte, order is: LSB is first entry in the list above +} PSparticleFlags; + +// struct for additional particle settings (option) +typedef struct { // 2 bytes + uint8_t size; // particle size, 255 means 10 pixels in diameter + uint8_t forcecounter; // counter for applying forces to individual particles +} PSadvancedParticle; + +// struct for advanced particle size control (option) +typedef struct { // 8 bytes + uint8_t asymmetry; // asymmetrical size (0=symmetrical, 255 fully asymmetric) + uint8_t asymdir; // direction of asymmetry, 64 is x, 192 is y (0 and 128 is symmetrical) + uint8_t maxsize; // target size for growing + uint8_t minsize; // target size for shrinking + uint8_t sizecounter : 4; // counters used for size contol (grow/shrink/wobble) + uint8_t wobblecounter : 4; + uint8_t growspeed : 4; + uint8_t shrinkspeed : 4; + uint8_t wobblespeed : 4; + bool grow : 1; // flags + bool shrink : 1; + bool pulsate : 1; // grows & shrinks & grows & ... + bool wobble : 1; // alternate x and y size +} PSsizeControl; + + +//struct for a particle source (20 bytes) +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) + PSparticleFlags sourceFlags; // flags for the source particle + int8_t var; // variation of emitted speed (adds random(+/- var) to speed) + int8_t vx; // emitting speed + int8_t vy; + uint8_t size; // particle size (advanced property) +} PSsource; + +// class uses approximately 60 bytes +class ParticleSystem2D { +public: + ParticleSystem2D(const uint32_t width, const uint32_t height, const uint32_t numberofparticles, const uint32_t numberofsources, const bool isadvanced = false, const bool sizecontrol = false); // constructor + // note: memory is allcated in the FX function, no deconstructor needed + void update(void); //update the particles according to set options and render to the matrix + void updateFire(const uint8_t intensity, const bool renderonly); // update function for fire, if renderonly is set, particles are not updated (required to fix transitions with frameskips) + void updateSystem(void); // call at the beginning of every FX, updates pointers and dimensions + void particleMoveUpdate(PSparticle &part, PSparticleFlags &partFlags, PSsettings2D *options = NULL, PSadvancedParticle *advancedproperties = NULL); // move function + // particle emitters + int32_t sprayEmit(const PSsource &emitter); + void flameEmit(const PSsource &emitter); + int32_t angleEmit(PSsource& emitter, const uint16_t angle, const int32_t speed); + //particle physics + void applyGravity(PSparticle &part); // applies gravity to single particle (use this for sources) + [[gnu::hot]] void applyForce(PSparticle &part, const int8_t xforce, const int8_t yforce, uint8_t &counter); + [[gnu::hot]] void applyForce(const uint32_t particleindex, const int8_t xforce, const int8_t yforce); // use this for advanced property particles + void applyForce(const int8_t xforce, const int8_t yforce); // apply a force to all particles + void applyAngleForce(PSparticle &part, const int8_t force, const uint16_t angle, uint8_t &counter); + void applyAngleForce(const uint32_t particleindex, const int8_t force, const uint16_t angle); // use this for advanced property particles + void applyAngleForce(const int8_t force, const uint16_t angle); // apply angular force to all particles + void applyFriction(PSparticle &part, const int32_t coefficient); // apply friction to specific particle + void applyFriction(const int32_t coefficient); // apply friction to all used particles + void pointAttractor(const uint32_t particleindex, PSparticle &attractor, const uint8_t strength, const bool swallow); + // set options note: inlining the set function uses more flash so dont optimize + void setUsedParticles(const uint8_t percentage); // set the percentage of particles used in the system, 255=100% + inline uint32_t getAvailableParticles(void) { return availableParticles; } // available particles in the buffer, use this to check if buffer changed during FX init + void setCollisionHardness(const uint8_t hardness); // hardness for particle collisions (255 means full hard) + void setWallHardness(const uint8_t hardness); // hardness for bouncing on the wall if bounceXY is set + void setWallRoughness(const uint8_t roughness); // wall roughness randomizes wall collisions + void setMatrixSize(const uint32_t x, const uint32_t y); + void setWrapX(const bool enable); + void setWrapY(const bool enable); + void setBounceX(const bool enable); + void setBounceY(const bool enable); + void setKillOutOfBounds(const bool enable); // if enabled, particles outside of matrix instantly die + void setSaturation(const uint8_t sat); // set global color saturation + void setColorByAge(const bool enable); + void setMotionBlur(const uint8_t bluramount); // note: motion blur can only be used if 'particlesize' is set to zero + void setSmearBlur(const uint8_t bluramount); // enable 2D smeared blurring of full frame + void setParticleSize(const uint8_t size); + void setGravity(const int8_t force = 8); + void enableParticleCollisions(const bool enable, const uint8_t hardness = 255); + + PSparticle *particles; // pointer to particle array + PSparticleFlags *particleFlags; // pointer to particle flags array + PSsource *sources; // pointer to sources + PSadvancedParticle *advPartProps; // pointer to advanced particle properties (can be NULL) + PSsizeControl *advPartSize; // pointer to advanced particle size control (can be NULL) + uint8_t* PSdataEnd; // points to first available byte after the PSmemory, is set in setPointers(). use this for FX custom data + int32_t maxX, maxY; // particle system size i.e. width-1 / height-1 in subpixels, Note: all "max" variables must be signed to compare to coordinates (which are signed) + int32_t maxXpixel, maxYpixel; // last physical pixel that can be drawn to (FX can read this to read segment size if required), equal to width-1 / height-1 + uint32_t numSources; // number of sources + uint32_t usedParticles; // number of particles used in animation, is relative to 'numParticles' + //note: some variables are 32bit for speed and code size at the cost of ram + +private: + //rendering functions + void ParticleSys_render(); + [[gnu::hot]] void renderParticle(const uint32_t particleindex, const uint32_t brightness, const CRGB& color, const bool wrapX, const bool wrapY); + //paricle physics applied by system if flags are set + void applyGravity(); // applies gravity to all particles + void handleCollisions(); + [[gnu::hot]] void collideParticles(PSparticle &particle1, PSparticle &particle2, const int32_t dx, const int32_t dy); + void fireParticleupdate(); + //utility functions + void updatePSpointers(const bool isadvanced, const bool sizecontrol); // update the data pointers to current segment data space + bool updateSize(PSadvancedParticle *advprops, PSsizeControl *advsize); // advanced size control + void getParticleXYsize(PSadvancedParticle *advprops, PSsizeControl *advsize, uint32_t &xsize, uint32_t &ysize); + [[gnu::hot]] void bounce(int8_t &incomingspeed, int8_t ¶llelspeed, int32_t &position, const uint32_t maxposition); // bounce on a wall + // note: variables that are accessed often are 32bit for speed + PSsettings2D particlesettings; // settings used when updating particles (can also used by FX to move sources), do not edit properties directly, use functions above + uint32_t numParticles; // total number of particles allocated by this system note: during transitions, less are available, use availableParticles + uint32_t availableParticles; // number of particles available for use (can be more or less than numParticles, assigned by memory manager) + uint32_t emitIndex; // index to count through particles to emit so searching for dead pixels is faster + int32_t collisionHardness; + uint32_t wallHardness; + uint32_t wallRoughness; // randomizes wall collisions + uint32_t particleHardRadius; // hard surface radius of a particle, used for collision detection (32bit for speed) + uint16_t collisionStartIdx; // particle array start index for collision detection + uint8_t fireIntesity = 0; // fire intensity, used for fire mode (flash use optimization, better than passing an argument to render function) + uint8_t fractionOfParticlesUsed; // percentage of particles used in the system (255=100%), used during transition updates + uint8_t forcecounter; // counter for globally applied forces + uint8_t gforcecounter; // counter for global gravity + int8_t gforce; // gravity strength, default is 8 (negative is allowed, positive is downwards) + // global particle properties for basic particles + uint8_t particlesize; // global particle size, 0 = 2 pixels, 255 = 10 pixels (note: this is also added to individual sized particles) + uint8_t motionBlur; // motion blur, values > 100 gives smoother animations. Note: motion blurring does not work if particlesize is > 0 + uint8_t smearBlur; // 2D smeared blurring of full frame + uint8_t effectID; // ID of the effect that is using this particle system, used for transitions +}; + +void blur2D(CRGB *colorbuffer, const uint32_t xsize, uint32_t ysize, const uint32_t xblur, const uint32_t yblur, const uint32_t xstart = 0, uint32_t ystart = 0, const bool isparticle = false); +// initialization functions (not part of class) +bool initParticleSystem2D(ParticleSystem2D *&PartSys, const uint32_t requestedsources, const uint32_t additionalbytes = 0, const bool advanced = false, const bool sizecontrol = false); +uint32_t calculateNumberOfParticles2D(const uint32_t pixels, const bool advanced, const bool sizecontrol); +uint32_t calculateNumberOfSources2D(const uint32_t pixels, const uint32_t requestedsources); +bool allocateParticleSystemMemory2D(const uint32_t numparticles, const uint32_t numsources, const bool advanced, const bool sizecontrol, const uint32_t additionalbytes); +#endif // WLED_DISABLE_PARTICLESYSTEM2D + +//////////////////////// +// 1D Particle System // +//////////////////////// +#ifndef WLED_DISABLE_PARTICLESYSTEM1D +// memory allocation +#define ESP8266_MAXPARTICLES_1D 450 +#define ESP8266_MAXSOURCES_1D 16 +#define ESP32S2_MAXPARTICLES_1D 1300 +#define ESP32S2_MAXSOURCES_1D 32 +#define ESP32_MAXPARTICLES_1D 2600 +#define ESP32_MAXSOURCES_1D 64 + +// particle dimensions (subpixel division) +#define PS_P_RADIUS_1D 32 // subpixel size, each pixel is divided by this for particle movement, if this value is changed, also change the shift defines (next two lines) +#define PS_P_HALFRADIUS_1D (PS_P_RADIUS_1D >> 1) +#define PS_P_RADIUS_SHIFT_1D 5 // 1 << PS_P_RADIUS_SHIFT = PS_P_RADIUS +#define PS_P_SURFACE_1D 5 // shift: 2^PS_P_SURFACE = PS_P_RADIUS_1D +#define PS_P_MINHARDRADIUS_1D 32 // minimum hard surface radius note: do not change or hourglass effect will be broken +#define PS_P_MINSURFACEHARDNESS_1D 120 // minimum hardness used in collision impulse calculation + +// struct for PS settings (shared for 1D and 2D class) +typedef union { + struct{ + // one byte bit field for 1D settings + bool wrap : 1; + bool bounce : 1; + bool killoutofbounds : 1; // if set, out of bound particles are killed immediately + bool useGravity : 1; // set to 1 if gravity is used, disables bounceY at the top + bool useCollisions : 1; + bool colorByAge : 1; // if set, particle hue is set by ttl value in render function + bool colorByPosition : 1; // if set, particle hue is set by its position in the strip segment + bool unused : 1; + }; + byte asByte; // access as a byte, order is: LSB is first entry in the list above +} PSsettings1D; + +//struct for a single particle (8 bytes) +typedef struct { + int32_t x; // x position in particle system + uint16_t ttl; // time to live in frames + int8_t vx; // horizontal velocity + uint8_t hue; // color hue +} PSparticle1D; + +//struct for particle flags +typedef union { + struct { // 1 byte + bool outofbounds : 1; // out of bounds flag, set to true if particle is outside of display area + bool collide : 1; // if set, particle takes part in collisions + bool perpetual : 1; // if set, particle does not age (TTL is not decremented in move function, it still dies from killoutofbounds) + bool reversegrav : 1; // if set, gravity is reversed on this particle + bool forcedirection : 1; // direction the force was applied, 1 is positive x-direction (used for collision stacking, similar to reversegrav) + bool fixed : 1; // if set, particle does not move (and collisions make other particles revert direction), + bool custom1 : 1; // unused custom flags, can be used by FX to track particle states + bool custom2 : 1; + }; + byte asByte; // access as a byte, order is: LSB is first entry in the list above +} PSparticleFlags1D; + +// struct for additional particle settings (optional) +typedef struct { + uint8_t sat; //color saturation + uint8_t size; // particle size, 255 means 10 pixels in diameter + uint8_t forcecounter; +} PSadvancedParticle1D; + +//struct for a particle source (20 bytes) +typedef struct { + uint16_t minLife; // minimum ttl of emittet particles + uint16_t maxLife; // maximum ttl of emitted particles + PSparticle1D source; // use a particle as the emitter source (speed, position, color) + PSparticleFlags1D sourceFlags; // flags for the source particle + int8_t var; // variation of emitted speed (adds random(+/- var) to speed) + int8_t v; // emitting speed + uint8_t sat; // color saturation (advanced property) + uint8_t size; // particle size (advanced property) + // note: there is 3 bytes of padding added here +} PSsource1D; + +class ParticleSystem1D +{ +public: + ParticleSystem1D(const uint32_t length, const uint32_t numberofparticles, const uint32_t numberofsources, const bool isadvanced = false); // constructor + // note: memory is allcated in the FX function, no deconstructor needed + void update(void); //update the particles according to set options and render to the matrix + void updateSystem(void); // call at the beginning of every FX, updates pointers and dimensions + // particle emitters + int32_t sprayEmit(const PSsource1D &emitter); + void particleMoveUpdate(PSparticle1D &part, PSparticleFlags1D &partFlags, PSsettings1D *options = NULL, PSadvancedParticle1D *advancedproperties = NULL); // move function + //particle physics + [[gnu::hot]] void applyForce(PSparticle1D &part, const int8_t xforce, uint8_t &counter); //apply a force to a single particle + void applyForce(const int8_t xforce); // apply a force to all particles + void applyGravity(PSparticle1D &part, PSparticleFlags1D &partFlags); // applies gravity to single particle (use this for sources) + void applyFriction(const int32_t coefficient); // apply friction to all used particles + // set options + void setUsedParticles(const uint8_t percentage); // set the percentage of particles used in the system, 255=100% + inline uint32_t getAvailableParticles(void) { return availableParticles; } // available particles in the buffer, use this to check if buffer changed during FX init + void setWallHardness(const uint8_t hardness); // hardness for bouncing on the wall if bounceXY is set + void setSize(const uint32_t x); //set particle system size (= strip length) + void setWrap(const bool enable); + void setBounce(const bool enable); + void setKillOutOfBounds(const bool enable); // if enabled, particles outside of matrix instantly die + // void setSaturation(uint8_t sat); // set global color saturation + void setColorByAge(const bool enable); + void setColorByPosition(const bool enable); + void setMotionBlur(const uint8_t bluramount); // note: motion blur can only be used if 'particlesize' is set to zero + void setSmearBlur(const uint8_t bluramount); // enable 1D smeared blurring of full frame + void setParticleSize(const uint8_t size); //size 0 = 1 pixel, size 1 = 2 pixels, is overruled by advanced particle size + void setGravity(int8_t force = 8); + void enableParticleCollisions(bool enable, const uint8_t hardness = 255); + + PSparticle1D *particles; // pointer to particle array + PSparticleFlags1D *particleFlags; // pointer to particle flags array + PSsource1D *sources; // pointer to sources + PSadvancedParticle1D *advPartProps; // pointer to advanced particle properties (can be NULL) + //PSsizeControl *advPartSize; // pointer to advanced particle size control (can be NULL) + uint8_t* PSdataEnd; // points to first available byte after the PSmemory, is set in setPointers(). use this for FX custom data + int32_t maxX; // particle system size i.e. width-1, Note: all "max" variables must be signed to compare to coordinates (which are signed) + int32_t maxXpixel; // last physical pixel that can be drawn to (FX can read this to read segment size if required), equal to width-1 + uint32_t numSources; // number of sources + uint32_t usedParticles; // number of particles used in animation, is relative to 'numParticles' + +private: + //rendering functions + void ParticleSys_render(void); + void renderParticle(const uint32_t particleindex, const uint32_t brightness, const CRGB &color, const bool wrap); + + //paricle physics applied by system if flags are set + void applyGravity(); // applies gravity to all particles + void handleCollisions(); + [[gnu::hot]] void collideParticles(PSparticle1D &particle1, const PSparticleFlags1D &particle1flags, PSparticle1D &particle2, const PSparticleFlags1D &particle2flags, int32_t dx, int32_t relativeVx, uint32_t collisiondistance); + + //utility functions + void updatePSpointers(const bool isadvanced); // update the data pointers to current segment data space + //void updateSize(PSadvancedParticle *advprops, PSsizeControl *advsize); // advanced size control + [[gnu::hot]] void bounce(int8_t &incomingspeed, int8_t ¶llelspeed, int32_t &position, const uint32_t maxposition); // bounce on a wall + // note: variables that are accessed often are 32bit for speed + PSsettings1D particlesettings; // settings used when updating particles + uint32_t numParticles; // total number of particles allocated by this system note: never use more than this, even if more are available (only this many advanced particles are allocated) + uint32_t availableParticles; // number of particles available for use (can be more or less than numParticles, assigned by memory manager) + uint8_t fractionOfParticlesUsed; // percentage of particles used in the system (255=100%), used during transition updates + uint32_t emitIndex; // index to count through particles to emit so searching for dead pixels is faster + int32_t collisionHardness; + uint32_t particleHardRadius; // hard surface radius of a particle, used for collision detection + uint32_t wallHardness; + uint8_t gforcecounter; // counter for global gravity + int8_t gforce; // gravity strength, default is 8 (negative is allowed, positive is downwards) + uint8_t forcecounter; // counter for globally applied forces + uint16_t collisionStartIdx; // particle array start index for collision detection + //global particle properties for basic particles + uint8_t particlesize; // global particle size, 0 = 1 pixel, 1 = 2 pixels + uint8_t motionBlur; // enable motion blur, values > 100 gives smoother animations + uint8_t smearBlur; // smeared blurring of full frame + uint8_t effectID; // ID of the effect that is using this particle system, used for transitions +}; + +bool initParticleSystem1D(ParticleSystem1D *&PartSys, const uint32_t requestedsources, const uint8_t fractionofparticles = 255, const uint32_t additionalbytes = 0, const bool advanced = false); +uint32_t calculateNumberOfParticles1D(const uint32_t fraction, const bool isadvanced); +uint32_t calculateNumberOfSources1D(const uint32_t requestedsources); +bool allocateParticleSystemMemory1D(const uint32_t numparticles, const uint32_t numsources, const bool isadvanced, const uint32_t additionalbytes); +void blur1D(CRGB *colorbuffer, uint32_t size, uint32_t blur, uint32_t start); +#endif // WLED_DISABLE_PARTICLESYSTEM1D diff --git a/wled00/cfg.cpp b/wled00/cfg.cpp index 443f16c73b..0fb67e2523 100644 --- a/wled00/cfg.cpp +++ b/wled00/cfg.cpp @@ -255,7 +255,6 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { // read multiple button configuration JsonObject btn_obj = hw["btn"]; - CJSON(touchThreshold, btn_obj[F("tt")]); bool pull = btn_obj[F("pull")] | (!disablePullUp); // if true, pullup is enabled disablePullUp = !pull; JsonArray hw_btn_ins = btn_obj["ins"]; @@ -352,7 +351,7 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { } } } - + CJSON(touchThreshold,btn_obj[F("tt")]); CJSON(buttonPublishMqtt,btn_obj["mqtt"]); #ifndef WLED_DISABLE_INFRARED diff --git a/wled00/fcn_declare.h b/wled00/fcn_declare.h index fcfa1bdcc0..976e78c8aa 100644 --- a/wled00/fcn_declare.h +++ b/wled00/fcn_declare.h @@ -20,7 +20,6 @@ void doublePressAction(uint8_t b=0); bool isButtonPressed(uint8_t b=0); void handleButton(); void handleIO(); -void IRAM_ATTR touchButtonISR(); //cfg.cpp bool deserializeConfig(JsonObject doc, bool fromFS = false); From 9ca46c6f28f69169d80d44c823d5b593dbc3607d Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sun, 19 Jan 2025 11:04:39 +0100 Subject: [PATCH 190/219] reverted some accidental changes --- platformio.ini | 2 +- wled00/FX.cpp | 15 +++------------ wled00/FX.h | 11 +++++------ wled00/FX_fcn.cpp | 2 +- 4 files changed, 10 insertions(+), 20 deletions(-) diff --git a/platformio.ini b/platformio.ini index f0b70a3a95..8700aac2e1 100644 --- a/platformio.ini +++ b/platformio.ini @@ -15,7 +15,7 @@ default_envs = nodemcuv2, esp8266_2m, esp01_1m_full, nodemcuv2_160, esp8266_2m_1 src_dir = ./wled00 data_dir = ./wled00/data build_cache_dir = ~/.buildcache -extra_configs = platformio_override.ini +extra_configs = platformio_override.ini [common] # ------------------------------------------------------------------------------ diff --git a/wled00/FX.cpp b/wled00/FX.cpp index e5fca37b4f..cdd47a82a7 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -10,8 +10,6 @@ Modified heavily for WLED */ -// information for custom FX metadata strings: https://kno.wled.ge/interfaces/json-api/#effect-metadata - #include "wled.h" #include "FX.h" #include "fcn_declare.h" @@ -2943,7 +2941,6 @@ typedef struct Ball { /* * Bouncing Balls Effect */ - uint16_t mode_bouncing_balls(void) { if (SEGLEN <= 1) return mode_static(); //allocate segment data @@ -3023,7 +3020,6 @@ static const char _data_FX_MODE_BOUNCINGBALLS[] PROGMEM = "Bouncing Balls@Gravit * Courtesy of pjhatch (https://github.com/pjhatch) * https://github.com/Aircoookie/WLED/pull/1039 */ - // modified for balltrack mode typedef struct RollingBall { unsigned long lastBounceUpdate; @@ -3227,7 +3223,6 @@ typedef struct Spark { * POPCORN * modified from https://github.com/kitesurfer1404/WS2812FX/blob/master/src/custom/Popcorn.h */ - uint16_t mode_popcorn(void) { if (SEGLEN <= 1) return mode_static(); //allocate segment data @@ -4319,7 +4314,6 @@ typedef struct Spotlight { * * By Steve Pomeroy @xxv */ - uint16_t mode_dancing_shadows(void) { if (SEGLEN <= 1) return mode_static(); @@ -5870,7 +5864,6 @@ static const char _data_FX_MODE_2DCRAZYBEES[] PROGMEM = "Crazy Bees@!,Blur,,,,Sm // 2D Ghost Rider // ///////////////////////// //// Ghost Rider by stepko (c)2021 [https://editor.soulmatelights.com/gallery/716-ghost-rider], adapted by Blaz Kristan (AKA blazoncek) - #define LIGHTERS_AM 64 // max lighters (adequate for 32x32 matrix) uint16_t mode_2Dghostrider(void) { if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up @@ -5956,12 +5949,10 @@ uint16_t mode_2Dghostrider(void) { static const char _data_FX_MODE_2DGHOSTRIDER[] PROGMEM = "Ghost Rider@Fade rate,Blur;;!;2"; #undef LIGHTERS_AM - //////////////////////////// // 2D Floating Blobs // //////////////////////////// //// Floating Blobs by stepko (c)2021 [https://editor.soulmatelights.com/gallery/573-blobs], adapted by Blaz Kristan (AKA blazoncek) - #define MAX_BLOBS 8 uint16_t mode_2Dfloatingblobs(void) { if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up @@ -7486,9 +7477,9 @@ uint16_t mode_2Dsoap() { } } // init also if dimensions changed - if (SEGENV.call == 0 || SEGENV.aux0 != cols || SEGENV.aux1 != rows) { - SEGENV.aux0 = cols; - SEGENV.aux1 = rows; + if (SEGENV.call == 0 || SEGMENT.aux0 != cols || SEGMENT.aux1 != rows) { + SEGMENT.aux0 = cols; + SEGMENT.aux1 = rows; for (int i = 0; i < cols; i++) { for (int j = 0; j < rows; j++) { SEGMENT.setPixelColorXY(i, j, ColorFromPalette(SEGPALETTE,~noise3d[XY(i,j)]*3)); diff --git a/wled00/FX.h b/wled00/FX.h index 8fe0d05834..b7b3c861e7 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -350,7 +350,6 @@ extern byte realtimeMode; // used in getMappedPixelIndex() #define FX_MODE_PS1DGEQ 212 #define FX_MODE_PSFIRE1D 213 #define FX_MODE_PS1DSONICSTREAM 214 -//#define FX_MODE_PSFRACTAL 215 #define MODE_COUNT 215 typedef enum mapping1D2D { @@ -478,15 +477,15 @@ typedef struct Segment { uint8_t _briT; // temporary brightness uint8_t _cctT; // temporary CCT CRGBPalette16 _palT; // temporary palette - unsigned long _start; // must accommodate millis() - uint16_t _dur; uint8_t _prevPaletteBlends; // number of previous palette blends (there are max 255 blends possible) + unsigned long _start; // must accommodate millis() + uint16_t _dur; // -> here is one byte of padding Transition(uint16_t dur=750) : _palT(CRGBPalette16(CRGB::Black)) - , _start(millis()) - , _dur(dur) , _prevPaletteBlends(0) + , _start(millis()) + , _dur(dur) {} } *_t; @@ -1009,4 +1008,4 @@ class WS2812FX { // 96 bytes extern const char JSON_mode_names[]; extern const char JSON_palette_names[]; -#endif +#endif \ No newline at end of file diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index fe24173ea2..dfb170dec3 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -1855,4 +1855,4 @@ const char JSON_palette_names[] PROGMEM = R"=====([ "Aurora","Atlantica","C9 2","C9 New","Temperature","Aurora 2","Retro Clown","Candy","Toxy Reaf","Fairy Reaf", "Semi Blue","Pink Candy","Red Reaf","Aqua Flash","Yelblu Hot","Lite Light","Red Flash","Blink Red","Red Shift","Red Tide", "Candy2","Traffic Light" -])====="; +])====="; \ No newline at end of file From 5c2b8e19aeb5b2450a8c2af7af97d2700f4ca835 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sun, 19 Jan 2025 11:04:39 +0100 Subject: [PATCH 191/219] reverted some accidental changes --- platformio.ini | 2 +- wled00/FX.cpp | 15 +++------------ wled00/FX.h | 9 ++++----- 3 files changed, 8 insertions(+), 18 deletions(-) diff --git a/platformio.ini b/platformio.ini index f0b70a3a95..8700aac2e1 100644 --- a/platformio.ini +++ b/platformio.ini @@ -15,7 +15,7 @@ default_envs = nodemcuv2, esp8266_2m, esp01_1m_full, nodemcuv2_160, esp8266_2m_1 src_dir = ./wled00 data_dir = ./wled00/data build_cache_dir = ~/.buildcache -extra_configs = platformio_override.ini +extra_configs = platformio_override.ini [common] # ------------------------------------------------------------------------------ diff --git a/wled00/FX.cpp b/wled00/FX.cpp index e5fca37b4f..cdd47a82a7 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -10,8 +10,6 @@ Modified heavily for WLED */ -// information for custom FX metadata strings: https://kno.wled.ge/interfaces/json-api/#effect-metadata - #include "wled.h" #include "FX.h" #include "fcn_declare.h" @@ -2943,7 +2941,6 @@ typedef struct Ball { /* * Bouncing Balls Effect */ - uint16_t mode_bouncing_balls(void) { if (SEGLEN <= 1) return mode_static(); //allocate segment data @@ -3023,7 +3020,6 @@ static const char _data_FX_MODE_BOUNCINGBALLS[] PROGMEM = "Bouncing Balls@Gravit * Courtesy of pjhatch (https://github.com/pjhatch) * https://github.com/Aircoookie/WLED/pull/1039 */ - // modified for balltrack mode typedef struct RollingBall { unsigned long lastBounceUpdate; @@ -3227,7 +3223,6 @@ typedef struct Spark { * POPCORN * modified from https://github.com/kitesurfer1404/WS2812FX/blob/master/src/custom/Popcorn.h */ - uint16_t mode_popcorn(void) { if (SEGLEN <= 1) return mode_static(); //allocate segment data @@ -4319,7 +4314,6 @@ typedef struct Spotlight { * * By Steve Pomeroy @xxv */ - uint16_t mode_dancing_shadows(void) { if (SEGLEN <= 1) return mode_static(); @@ -5870,7 +5864,6 @@ static const char _data_FX_MODE_2DCRAZYBEES[] PROGMEM = "Crazy Bees@!,Blur,,,,Sm // 2D Ghost Rider // ///////////////////////// //// Ghost Rider by stepko (c)2021 [https://editor.soulmatelights.com/gallery/716-ghost-rider], adapted by Blaz Kristan (AKA blazoncek) - #define LIGHTERS_AM 64 // max lighters (adequate for 32x32 matrix) uint16_t mode_2Dghostrider(void) { if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up @@ -5956,12 +5949,10 @@ uint16_t mode_2Dghostrider(void) { static const char _data_FX_MODE_2DGHOSTRIDER[] PROGMEM = "Ghost Rider@Fade rate,Blur;;!;2"; #undef LIGHTERS_AM - //////////////////////////// // 2D Floating Blobs // //////////////////////////// //// Floating Blobs by stepko (c)2021 [https://editor.soulmatelights.com/gallery/573-blobs], adapted by Blaz Kristan (AKA blazoncek) - #define MAX_BLOBS 8 uint16_t mode_2Dfloatingblobs(void) { if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up @@ -7486,9 +7477,9 @@ uint16_t mode_2Dsoap() { } } // init also if dimensions changed - if (SEGENV.call == 0 || SEGENV.aux0 != cols || SEGENV.aux1 != rows) { - SEGENV.aux0 = cols; - SEGENV.aux1 = rows; + if (SEGENV.call == 0 || SEGMENT.aux0 != cols || SEGMENT.aux1 != rows) { + SEGMENT.aux0 = cols; + SEGMENT.aux1 = rows; for (int i = 0; i < cols; i++) { for (int j = 0; j < rows; j++) { SEGMENT.setPixelColorXY(i, j, ColorFromPalette(SEGPALETTE,~noise3d[XY(i,j)]*3)); diff --git a/wled00/FX.h b/wled00/FX.h index 7532100d8a..b7b3c861e7 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -350,7 +350,6 @@ extern byte realtimeMode; // used in getMappedPixelIndex() #define FX_MODE_PS1DGEQ 212 #define FX_MODE_PSFIRE1D 213 #define FX_MODE_PS1DSONICSTREAM 214 -//#define FX_MODE_PSFRACTAL 215 #define MODE_COUNT 215 typedef enum mapping1D2D { @@ -478,15 +477,15 @@ typedef struct Segment { uint8_t _briT; // temporary brightness uint8_t _cctT; // temporary CCT CRGBPalette16 _palT; // temporary palette - unsigned long _start; // must accommodate millis() - uint16_t _dur; uint8_t _prevPaletteBlends; // number of previous palette blends (there are max 255 blends possible) + unsigned long _start; // must accommodate millis() + uint16_t _dur; // -> here is one byte of padding Transition(uint16_t dur=750) : _palT(CRGBPalette16(CRGB::Black)) - , _start(millis()) - , _dur(dur) , _prevPaletteBlends(0) + , _start(millis()) + , _dur(dur) {} } *_t; From ddc588bb911e5be056b50fe0512415da1ec20957 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sun, 19 Jan 2025 11:14:33 +0100 Subject: [PATCH 192/219] merge fixes --- platformio.ini | 2 +- wled00/cfg.cpp | 5 +++-- wled00/fcn_declare.h | 1 + 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/platformio.ini b/platformio.ini index 8700aac2e1..24cb7c9fc1 100644 --- a/platformio.ini +++ b/platformio.ini @@ -539,7 +539,7 @@ build_flags = ${common.build_flags} ${esp32c3.build_flags} -D WLED_RELEASE_NAME= -DLOLIN_WIFI_FIX ; seems to work much better with this -DARDUINO_USB_CDC_ON_BOOT=1 ;; for virtual CDC USB ;-DARDUINO_USB_CDC_ON_BOOT=0 ;; for serial-to-USB chip -upload_speed = 460800 ; 115200 230400 460800 +upload_speed = 460800 build_unflags = ${common.build_unflags} lib_deps = ${esp32c3.lib_deps} diff --git a/wled00/cfg.cpp b/wled00/cfg.cpp index 0fb67e2523..72bedcbbbb 100644 --- a/wled00/cfg.cpp +++ b/wled00/cfg.cpp @@ -255,6 +255,7 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { // read multiple button configuration JsonObject btn_obj = hw["btn"]; + CJSON(touchThreshold, btn_obj[F("tt")]); bool pull = btn_obj[F("pull")] | (!disablePullUp); // if true, pullup is enabled disablePullUp = !pull; JsonArray hw_btn_ins = btn_obj["ins"]; @@ -351,7 +352,7 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { } } } - CJSON(touchThreshold,btn_obj[F("tt")]); + CJSON(buttonPublishMqtt,btn_obj["mqtt"]); #ifndef WLED_DISABLE_INFRARED @@ -1239,4 +1240,4 @@ void serializeConfigSec() { if (f) serializeJson(root, f); f.close(); releaseJSONBufferLock(); -} +} \ No newline at end of file diff --git a/wled00/fcn_declare.h b/wled00/fcn_declare.h index 976e78c8aa..fcfa1bdcc0 100644 --- a/wled00/fcn_declare.h +++ b/wled00/fcn_declare.h @@ -20,6 +20,7 @@ void doublePressAction(uint8_t b=0); bool isButtonPressed(uint8_t b=0); void handleButton(); void handleIO(); +void IRAM_ATTR touchButtonISR(); //cfg.cpp bool deserializeConfig(JsonObject doc, bool fromFS = false); From 8f5fe616198be825611b761d73ca900c99ec038f Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sun, 19 Jan 2025 11:57:55 +0100 Subject: [PATCH 193/219] changed replacement: multicomet instead of comet (lighthouse) --- wled00/FX.cpp | 9 ++++----- wled00/FX.h | 4 ++-- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index cdd47a82a7..cc577b2167 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -1220,7 +1220,6 @@ uint16_t mode_dual_larson_scanner(void){ } static const char _data_FX_MODE_DUAL_LARSON_SCANNER[] PROGMEM = "Scanner Dual@!,Trail,Delay,,,Dual,Bi-delay;!,!,!;!;;m12=0,c1=0"; -#ifdef WLED_PS_DONT_REPLACE_FX /* * Firing comets from one end. "Lighthouse" */ @@ -1247,7 +1246,6 @@ uint16_t mode_comet(void) { return FRAMETIME; } static const char _data_FX_MODE_COMET[] PROGMEM = "Lighthouse@!,Fade rate;!,!;!"; -#endif // WLED_PS_DONT_REPLACE_FX /* * Fireworks function. @@ -1716,7 +1714,7 @@ uint16_t mode_tricolor_fade(void) { } static const char _data_FX_MODE_TRICOLOR_FADE[] PROGMEM = "Tri Fade@!;1,2,3;!"; - +#ifdef WLED_PS_DONT_REPLACE_FX /* * Creates random comets * Custom mode by Keith Lord: https://github.com/kitesurfer1404/WS2812FX/blob/master/src/custom/MultiComet.h @@ -1755,6 +1753,7 @@ uint16_t mode_multi_comet(void) { } static const char _data_FX_MODE_MULTI_COMET[] PROGMEM = "Multi Comet@!,Fade;!,!;!;1"; #undef MAX_COMETS +#endif // WLED_PS_DONT_REPLACE_FX /* * Running random pixels ("Stream 2") @@ -10195,7 +10194,6 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_TRICOLOR_FADE, &mode_tricolor_fade, _data_FX_MODE_TRICOLOR_FADE); addEffect(FX_MODE_LIGHTNING, &mode_lightning, _data_FX_MODE_LIGHTNING); addEffect(FX_MODE_ICU, &mode_icu, _data_FX_MODE_ICU); - addEffect(FX_MODE_MULTI_COMET, &mode_multi_comet, _data_FX_MODE_MULTI_COMET); addEffect(FX_MODE_DUAL_LARSON_SCANNER, &mode_dual_larson_scanner, _data_FX_MODE_DUAL_LARSON_SCANNER); addEffect(FX_MODE_RANDOM_CHASE, &mode_random_chase, _data_FX_MODE_RANDOM_CHASE); addEffect(FX_MODE_OSCILLATE, &mode_oscillate, _data_FX_MODE_OSCILLATE); @@ -10220,8 +10218,9 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_TRI_STATIC_PATTERN, &mode_tri_static_pattern, _data_FX_MODE_TRI_STATIC_PATTERN); addEffect(FX_MODE_SPOTS, &mode_spots, _data_FX_MODE_SPOTS); addEffect(FX_MODE_SPOTS_FADE, &mode_spots_fade, _data_FX_MODE_SPOTS_FADE); - #ifdef WLED_PS_DONT_REPLACE_FX addEffect(FX_MODE_COMET, &mode_comet, _data_FX_MODE_COMET); + #ifdef WLED_PS_DONT_REPLACE_FX + addEffect(FX_MODE_MULTI_COMET, &mode_multi_comet, _data_FX_MODE_MULTI_COMET); addEffect(FX_MODE_ROLLINGBALLS, &rolling_balls, _data_FX_MODE_ROLLINGBALLS); addEffect(FX_MODE_SPARKLE, &mode_sparkle, _data_FX_MODE_SPARKLE); addEffect(FX_MODE_GLITTER, &mode_glitter, _data_FX_MODE_GLITTER); diff --git a/wled00/FX.h b/wled00/FX.h index b7b3c861e7..d848f07aef 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -479,13 +479,13 @@ typedef struct Segment { CRGBPalette16 _palT; // temporary palette uint8_t _prevPaletteBlends; // number of previous palette blends (there are max 255 blends possible) unsigned long _start; // must accommodate millis() - uint16_t _dur; + uint16_t _dur; // -> here is one byte of padding Transition(uint16_t dur=750) : _palT(CRGBPalette16(CRGB::Black)) , _prevPaletteBlends(0) , _start(millis()) - , _dur(dur) + , _dur(dur) {} } *_t; From 1d552a2ceeb4883c207439f000d23bbbd759709f Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sun, 19 Jan 2025 11:57:55 +0100 Subject: [PATCH 194/219] changed replacement: multicomet instead of comet (lighthouse) --- wled00/FX.cpp | 9 ++++----- wled00/FX.h | 4 ++-- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index cdd47a82a7..cc577b2167 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -1220,7 +1220,6 @@ uint16_t mode_dual_larson_scanner(void){ } static const char _data_FX_MODE_DUAL_LARSON_SCANNER[] PROGMEM = "Scanner Dual@!,Trail,Delay,,,Dual,Bi-delay;!,!,!;!;;m12=0,c1=0"; -#ifdef WLED_PS_DONT_REPLACE_FX /* * Firing comets from one end. "Lighthouse" */ @@ -1247,7 +1246,6 @@ uint16_t mode_comet(void) { return FRAMETIME; } static const char _data_FX_MODE_COMET[] PROGMEM = "Lighthouse@!,Fade rate;!,!;!"; -#endif // WLED_PS_DONT_REPLACE_FX /* * Fireworks function. @@ -1716,7 +1714,7 @@ uint16_t mode_tricolor_fade(void) { } static const char _data_FX_MODE_TRICOLOR_FADE[] PROGMEM = "Tri Fade@!;1,2,3;!"; - +#ifdef WLED_PS_DONT_REPLACE_FX /* * Creates random comets * Custom mode by Keith Lord: https://github.com/kitesurfer1404/WS2812FX/blob/master/src/custom/MultiComet.h @@ -1755,6 +1753,7 @@ uint16_t mode_multi_comet(void) { } static const char _data_FX_MODE_MULTI_COMET[] PROGMEM = "Multi Comet@!,Fade;!,!;!;1"; #undef MAX_COMETS +#endif // WLED_PS_DONT_REPLACE_FX /* * Running random pixels ("Stream 2") @@ -10195,7 +10194,6 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_TRICOLOR_FADE, &mode_tricolor_fade, _data_FX_MODE_TRICOLOR_FADE); addEffect(FX_MODE_LIGHTNING, &mode_lightning, _data_FX_MODE_LIGHTNING); addEffect(FX_MODE_ICU, &mode_icu, _data_FX_MODE_ICU); - addEffect(FX_MODE_MULTI_COMET, &mode_multi_comet, _data_FX_MODE_MULTI_COMET); addEffect(FX_MODE_DUAL_LARSON_SCANNER, &mode_dual_larson_scanner, _data_FX_MODE_DUAL_LARSON_SCANNER); addEffect(FX_MODE_RANDOM_CHASE, &mode_random_chase, _data_FX_MODE_RANDOM_CHASE); addEffect(FX_MODE_OSCILLATE, &mode_oscillate, _data_FX_MODE_OSCILLATE); @@ -10220,8 +10218,9 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_TRI_STATIC_PATTERN, &mode_tri_static_pattern, _data_FX_MODE_TRI_STATIC_PATTERN); addEffect(FX_MODE_SPOTS, &mode_spots, _data_FX_MODE_SPOTS); addEffect(FX_MODE_SPOTS_FADE, &mode_spots_fade, _data_FX_MODE_SPOTS_FADE); - #ifdef WLED_PS_DONT_REPLACE_FX addEffect(FX_MODE_COMET, &mode_comet, _data_FX_MODE_COMET); + #ifdef WLED_PS_DONT_REPLACE_FX + addEffect(FX_MODE_MULTI_COMET, &mode_multi_comet, _data_FX_MODE_MULTI_COMET); addEffect(FX_MODE_ROLLINGBALLS, &rolling_balls, _data_FX_MODE_ROLLINGBALLS); addEffect(FX_MODE_SPARKLE, &mode_sparkle, _data_FX_MODE_SPARKLE); addEffect(FX_MODE_GLITTER, &mode_glitter, _data_FX_MODE_GLITTER); diff --git a/wled00/FX.h b/wled00/FX.h index b7b3c861e7..d848f07aef 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -479,13 +479,13 @@ typedef struct Segment { CRGBPalette16 _palT; // temporary palette uint8_t _prevPaletteBlends; // number of previous palette blends (there are max 255 blends possible) unsigned long _start; // must accommodate millis() - uint16_t _dur; + uint16_t _dur; // -> here is one byte of padding Transition(uint16_t dur=750) : _palT(CRGBPalette16(CRGB::Black)) , _prevPaletteBlends(0) , _start(millis()) - , _dur(dur) + , _dur(dur) {} } *_t; From 58c25df601a0742691b03112a508fe106fb80cbb Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sun, 19 Jan 2025 18:40:19 +0100 Subject: [PATCH 195/219] disable 2D PS for ESP8266, some cleanup, improved pinball FX parameters, bugfixes --- platformio.ini | 3 + wled00/FX.cpp | 182 ++++++++++++++++++------------------ wled00/FXparticleSystem.cpp | 21 ++--- 3 files changed, 100 insertions(+), 106 deletions(-) diff --git a/platformio.ini b/platformio.ini index 8700aac2e1..65637a10a6 100644 --- a/platformio.ini +++ b/platformio.ini @@ -357,6 +357,7 @@ platform_packages = ${common.platform_packages} board_build.ldscript = ${common.ldscript_4m1m} build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=\"ESP8266\" #-DWLED_DISABLE_2D + -D WLED_DISABLE_PARTICLESYSTEM2D lib_deps = ${esp8266.lib_deps} monitor_filters = esp8266_exception_decoder @@ -366,6 +367,7 @@ extends = env:nodemcuv2 platform = ${esp8266.platform_compat} platform_packages = ${esp8266.platform_packages_compat} build_flags = ${common.build_flags} ${esp8266.build_flags_compat} -D WLED_RELEASE_NAME=\"ESP8266_compat\" #-DWLED_DISABLE_2D + -D WLED_DISABLE_PARTICLESYSTEM2D ;; lib_deps = ${esp8266.lib_deps_compat} ;; experimental - use older NeoPixelBus 2.7.9 [env:nodemcuv2_160] @@ -373,6 +375,7 @@ extends = env:nodemcuv2 board_build.f_cpu = 160000000L build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=\"ESP8266_160\" #-DWLED_DISABLE_2D -D USERMOD_AUDIOREACTIVE + -D WLED_DISABLE_PARTICLESYSTEM2D [env:esp8266_2m] board = esp_wroom_02 diff --git a/wled00/FX.cpp b/wled00/FX.cpp index cc577b2167..4c0f737005 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -7632,11 +7632,11 @@ static const char _data_FX_MODE_2DWAVINGCELL[] PROGMEM = "Waving Cell@!,Blur,Amp #ifndef WLED_DISABLE_PARTICLESYSTEM2D /* - * Particle System Vortex - * Particles sprayed from center with a rotating spray - * Uses palette for particle color - * by DedeHai (Damian Schneider) - */ + Particle System Vortex + Particles sprayed from center with a rotating spray + Uses palette for particle color + by DedeHai (Damian Schneider) +*/ #define NUMBEROFSOURCES 8 uint16_t mode_particlevortex(void) { if (SEGLEN == 1) @@ -7747,10 +7747,10 @@ uint16_t mode_particlevortex(void) { static const char _data_FX_MODE_PARTICLEVORTEX[] PROGMEM = "PS Vortex@Rotation Speed,Particle Speed,Arms,Flip,Nozzle,Smear,Direction,Random Flip;;!;2;pal=27,c1=200,c2=0,c3=0"; /* - * Particle Fireworks - * Rockets shoot up and explode in a random color, sometimes in a defined pattern - * by DedeHai (Damian Schneider) - */ + Particle Fireworks + Rockets shoot up and explode in a random color, sometimes in a defined pattern + by DedeHai (Damian Schneider) +*/ #define NUMBEROFSOURCES 8 uint16_t mode_particlefireworks(void) { @@ -7857,8 +7857,8 @@ uint16_t mode_particlefireworks(void) { PartSys->sources[j].var = angle & 1; // 0 or 1 variation, angle is random } } - uint32_t i = 0; - for (i; i < emitparticles; i++) { + uint32_t i; + for (i = 0; i < emitparticles; i++) { if (circularexplosion) { int32_t sineMod = 0xEFFF + sin16_t((uint16_t)(((angle * frequency) >> 4) + baseangle)); // shifted to positive values currentspeed = (speed/2 + ((sineMod * speed) >> 16)) >> 1; // sine modulation on speed based on emit angle @@ -7895,11 +7895,11 @@ uint16_t mode_particlefireworks(void) { static const char _data_FX_MODE_PARTICLEFIREWORKS[] PROGMEM = "PS Fireworks@Launches,Explosion Size,Fuse,Blur,Gravity,Cylinder,Ground,Fast;;!;2;pal=11,sx=100,ix=50,c1=40,c2=0,c3=12"; /* - * Particle Volcano - * Particles are sprayed from below, spray moves back and forth if option is set - * Uses palette for particle color - * by DedeHai (Damian Schneider) - */ + Particle Volcano + Particles are sprayed from below, spray moves back and forth if option is set + Uses palette for particle color + by DedeHai (Damian Schneider) +*/ #define NUMBEROFSOURCES 1 uint16_t mode_particlevolcano(void) { ParticleSystem2D *PartSys = NULL; @@ -8464,10 +8464,10 @@ uint16_t mode_particleimpact(void) { static const char _data_FX_MODE_PARTICLEIMPACT[] PROGMEM = "PS Impact@Launches,Size,Force,Hardness,Blur,Cylinder,Walls,Collide;;!;2;pal=0,sx=32,ix=85,c1=70,c2=130,c3=0,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) + 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) { ParticleSystem2D *PartSys = NULL; @@ -8561,9 +8561,9 @@ uint16_t mode_particleattractor(void) { static const char _data_FX_MODE_PARTICLEATTRACTOR[] PROGMEM = "PS Attractor@Mass,Particles,Size,Collide,Friction,AgeColor,Move,Swallow;;!;2;pal=9,sx=100,ix=82,c1=0,c2=0"; /* -Particle Spray, just a particle spray with many parameters -Uses palette for particle color -by DedeHai (Damian Schneider) + Particle Spray, just a particle spray with many parameters + Uses palette for particle color + by DedeHai (Damian Schneider) */ uint16_t mode_particlespray(void) { ParticleSystem2D *PartSys = NULL; @@ -8647,9 +8647,9 @@ static const char _data_FX_MODE_PARTICLESPRAY[] PROGMEM = "PS Spray@Speed,!,Left /* -Particle base Graphical Equalizer -Uses palette for particle color -by DedeHai (Damian Schneider) + Particle base Graphical Equalizer + Uses palette for particle color + by DedeHai (Damian Schneider) */ uint16_t mode_particleGEQ(void) { ParticleSystem2D *PartSys = NULL; @@ -8723,11 +8723,11 @@ uint16_t mode_particleGEQ(void) { static const char _data_FX_MODE_PARTICLEGEQ[] PROGMEM = "PS GEQ 2D@Speed,Intensity,Diverge,Bounce,Gravity,Cylinder,Walls,Floor;;!;2f;pal=0,sx=155,ix=200,c1=0"; /* - * Particle rotating GEQ - * Particles sprayed from center with rotating spray - * Uses palette for particle color - * by DedeHai (Damian Schneider) - */ + Particle rotating GEQ + Particles sprayed from center with rotating spray + Uses palette for particle color + by DedeHai (Damian Schneider) +*/ #define NUMBEROFSOURCES 16 uint16_t mode_particlecenterGEQ(void) { ParticleSystem2D *PartSys = NULL; @@ -8798,7 +8798,7 @@ uint16_t mode_particlecenterGEQ(void) { static const char _data_FX_MODE_PARTICLECIRCULARGEQ[] PROGMEM = "PS GEQ Nova@Speed,Intensity,Rotation Speed,Color Change,Nozzle,,Direction;;!;2f;pal=13,ix=180,c1=0,c2=0,c3=8"; /* -Particle replacement of Ghost Rider by DedeHai (Damian Schneider), original by stepko adapted by Blaz Kristan (AKA blazoncek) + Particle replacement of Ghost Rider by DedeHai (Damian Schneider), original FX by stepko adapted by Blaz Kristan (AKA blazoncek) */ #define MAXANGLESTEP 2200 //32767 means 180° uint16_t mode_particleghostrider(void) { @@ -8878,9 +8878,9 @@ uint16_t mode_particleghostrider(void) { static const char _data_FX_MODE_PARTICLEGHOSTRIDER[] PROGMEM = "PS Ghost Rider@Speed,Spiral,Blur,Color Cycle,Spread,AgeColor,Walls;;!;2;pal=1,sx=70,ix=0,c1=220,c2=30,c3=21,o1=1"; /* -PS Blobs: large particles bouncing around, changing size and form -Uses palette for particle color -by DedeHai (Damian Schneider) + PS Blobs: large particles bouncing around, changing size and form + Uses palette for particle color + by DedeHai (Damian Schneider) */ uint16_t mode_particleblobs(void) { ParticleSystem2D *PartSys = NULL; @@ -8954,19 +8954,17 @@ uint16_t mode_particleblobs(void) { } static const char _data_FX_MODE_PARTICLEBLOBS[] PROGMEM = "PS Blobs@Speed,Blobs,Size,Life,Blur,Wobble,Collide,Pulsate;;!;2v;sx=30,ix=64,c1=200,c2=130,c3=0,o3=1"; #endif //WLED_DISABLE_PARTICLESYSTEM2D - #endif // WLED_DISABLE_2D - /////////////////////////// // 1D Particle System FX // /////////////////////////// #ifndef WLED_DISABLE_PARTICLESYSTEM1D /* -Particle version of Drip and Rain -Uses palette for particle color -by DedeHai (Damian Schneider) + Particle version of Drip and Rain + Uses palette for particle color + by DedeHai (Damian Schneider) */ uint16_t mode_particleDrip(void) { ParticleSystem1D *PartSys = NULL; @@ -9068,10 +9066,10 @@ static const char _data_FX_MODE_PARTICLEDRIP[] PROGMEM = "PS DripDrop@Speed,!,Sp /* -Particle Replacement for "Bbouncing Balls by Aircoookie" -Also replaces rolling balls and juggle (and maybe popcorn) -Uses palette for particle color -by DedeHai (Damian Schneider) + Particle Replacement for "Bbouncing Balls by Aircoookie" + Also replaces rolling balls and juggle (and maybe popcorn) + Uses palette for particle color + by DedeHai (Damian Schneider) */ uint16_t mode_particlePinball(void) { ParticleSystem1D *PartSys = NULL; @@ -9082,8 +9080,6 @@ uint16_t mode_particlePinball(void) { PartSys->sources[0].sourceFlags.collide = true; // seeded particles will collide (if enabled) PartSys->setKillOutOfBounds(true); // out of bounds particles dont return PartSys->sources[0].source.x = PS_P_RADIUS_1D; //emit at bottom - PartSys->sources[0].maxLife = 0xFFFF; // maximum lifetime in frames (long but not infinite to avoid perpetual handling, this is enough to travel 4000 pixels at min speed) - PartSys->sources[0].minLife = PartSys->sources[0].maxLife; SEGENV.aux0 = 1; SEGENV.aux1 = 500; //set out of speed range to ensure uptate on first call } @@ -9104,8 +9100,12 @@ uint16_t mode_particlePinball(void) { PartSys->setColorByPosition(SEGMENT.check3); bool updateballs = false; - if (SEGENV.aux1 != SEGMENT.speed + SEGMENT.intensity + SEGMENT.check2 + SEGMENT.custom1) // user settings change + if (SEGENV.aux1 != SEGMENT.speed + SEGMENT.intensity + SEGMENT.check2 + SEGMENT.custom1) { // user settings change + SEGENV.step = SEGMENT.call; // reset delay updateballs = true; + PartSys->sources[0].maxLife = SEGMENT.custom3 ? 1000 : 0xFFFF; // maximum lifetime in frames (very long if not using gravity, this is enough to travel 4000 pixels at min speed) + PartSys->sources[0].minLife = PartSys->sources[0].maxLife >> 1; + } if (SEGMENT.check2) { //rolling balls PartSys->setGravity(0); @@ -9127,14 +9127,14 @@ uint16_t mode_particlePinball(void) { } else { //bouncing balls PartSys->setWallHardness(220); - PartSys->sources[0].var = SEGMENT.speed >> 3; - int32_t newspeed = 2 + (SEGMENT.speed >> 1) - (SEGMENT.speed >> 3); - PartSys->sources[0].v = newspeed; + PartSys->sources[0].var = SEGMENT.speed >> 3; + int32_t newspeed = 2 + (SEGMENT.speed >> 1) - (SEGMENT.speed >> 3); + PartSys->sources[0].v = newspeed; //check for balls that are 'laying on the ground' and remove them for (uint32_t i = 0; i < PartSys->usedParticles; i++) { if (PartSys->particles[i].vx == 0 && PartSys->particles[i].x < (PS_P_RADIUS_1D + SEGMENT.custom1)) PartSys->particles[i].ttl = 0; - if (updateballs && SEGMENT.custom3 == 0) { + if (updateballs) { PartSys->advPartProps[i].size = SEGMENT.custom1; if(SEGMENT.custom3 == 0) //gravity off, update speed PartSys->particles[i].vx = PartSys->particles[i].vx > 0 ? newspeed : -newspeed; //keep the direction @@ -9143,7 +9143,7 @@ uint16_t mode_particlePinball(void) { // every nth frame emit a ball if (SEGMENT.call > SEGENV.step) { - int interval = 520 - ((int)SEGMENT.intensity << 1); + int interval = 260 - ((int)SEGMENT.intensity); SEGENV.step += interval + hw_random16(interval); PartSys->sources[0].source.hue = hw_random16(); //set ball color PartSys->sources[0].sat = 255; @@ -9153,23 +9153,22 @@ uint16_t mode_particlePinball(void) { } SEGENV.aux1 = SEGMENT.speed + SEGMENT.intensity + SEGMENT.check2 + SEGMENT.custom1; for (uint32_t i = 0; i < PartSys->usedParticles; i++) { - //if (SEGMENT.speed > 200) - PartSys->particleMoveUpdate(PartSys->particles[i], PartSys->particleFlags[i]); //increase speed on high settings by calling the move function twice + PartSys->particleMoveUpdate(PartSys->particles[i], PartSys->particleFlags[i]); // double the speed } PartSys->update(); // update and render return FRAMETIME; } -static const char _data_FX_MODE_PSPINBALL[] PROGMEM = "PS Pinball@Speed,!,Size,Blur,Gravity,Collide,Rolling,Position Color;,!;!;1;pal=0,c1=30,c2=0,c3=8"; +static const char _data_FX_MODE_PSPINBALL[] PROGMEM = "PS Pinball@Speed,!,Size,Blur,Gravity,Collide,Rolling,Position Color;,!;!;1;pal=0,ix=220,c2=0,c3=8,o1=1"; /* -Particle Replacement for original Dancing Shadows: -"Spotlights moving back and forth that cast dancing shadows. -Shine this through tree branches/leaves or other close-up objects that cast -interesting shadows onto a ceiling or tarp. -By Steve Pomeroy @xxv" -Uses palette for particle color -by DedeHai (Damian Schneider) + Particle Replacement for original Dancing Shadows: + "Spotlights moving back and forth that cast dancing shadows. + Shine this through tree branches/leaves or other close-up objects that cast + interesting shadows onto a ceiling or tarp. + By Steve Pomeroy @xxv" + Uses palette for particle color + by DedeHai (Damian Schneider) */ uint16_t mode_particleDancingShadows(void) { ParticleSystem1D *PartSys = NULL; @@ -9283,9 +9282,9 @@ uint16_t mode_particleDancingShadows(void) { static const char _data_FX_MODE_PARTICLEDANCINGSHADOWS[] PROGMEM = "PS Dancing Shadows@Speed,!,Blur,Color Cycle,,Smear,Position Color,Smooth;,!;!;1;sx=100,ix=180,c1=0,c2=0"; /* -Particle Fireworks 1D replacement -Uses palette for particle color -by DedeHai (Damian Schneider) + Particle Fireworks 1D replacement + Uses palette for particle color + by DedeHai (Damian Schneider) */ uint16_t mode_particleFireworks1D(void) { ParticleSystem1D *PartSys = NULL; @@ -9394,9 +9393,9 @@ uint16_t mode_particleFireworks1D(void) { static const char _data_FX_MODE_PS_FIREWORKS1D[] PROGMEM = "PS Fireworks 1D@Gravity,Explosion,Firing side,Blur,Saturation,,Colorful,Smooth;,!;!;1;pal=0,sx=150,c2=30,c3=21,o2=1"; /* -Particle based Sparkle effect -Uses palette for particle color -by DedeHai (Damian Schneider) + Particle based Sparkle effect + Uses palette for particle color + by DedeHai (Damian Schneider) */ uint16_t mode_particleSparkler(void) { ParticleSystem1D *PartSys = NULL; @@ -9466,9 +9465,9 @@ uint16_t mode_particleSparkler(void) { static const char _data_FX_MODE_PS_SPARKLER[] PROGMEM = "PS Sparkler@Move,!,Saturation,Blur,Sparklers,Slide,Bounce,Large;,!;!;1;pal=0,sx=255,c1=0,c2=0,c3=6"; /* -Particle based Hourglass, particles falling at defined intervals -Uses palette for particle color -by DedeHai (Damian Schneider) + Particle based Hourglass, particles falling at defined intervals + Uses palette for particle color + by DedeHai (Damian Schneider) */ uint16_t mode_particleHourglass(void) { ParticleSystem1D *PartSys = NULL; @@ -9592,9 +9591,9 @@ uint16_t mode_particleHourglass(void) { static const char _data_FX_MODE_PS_HOURGLASS[] PROGMEM = "PS Hourglass@Speed,!,Color,Blur,Gravity,Colorflip,Start,Fast Reset;,!;!;1;pal=34,sx=245,ix=200,c1=140,c2=80,c3=4,o1=1,o2=1,o3=1"; /* -Particle based Spray effect (like a volcano, possible replacement for popcorn) -Uses palette for particle color -by DedeHai (Damian Schneider) + Particle based Spray effect (like a volcano, possible replacement for popcorn) + Uses palette for particle color + by DedeHai (Damian Schneider) */ uint16_t mode_particle1Dspray(void) { ParticleSystem1D *PartSys = NULL; @@ -9644,9 +9643,9 @@ uint16_t mode_particle1Dspray(void) { static const char _data_FX_MODE_PS_1DSPRAY[] PROGMEM = "PS Spray 1D@Speed(+/-),!,Position,Blur,Gravity(+/-),AgeColor,Bounce,Position Color;,!;!;1;sx=200,ix=220,c1=0,c2=0,o1=1"; /* -Particle based balance: particles move back and forth (1D pendent to 2D particle box) -Uses palette for particle color -by DedeHai (Damian Schneider) + Particle based balance: particles move back and forth (1D pendent to 2D particle box) + Uses palette for particle color + by DedeHai (Damian Schneider) */ uint16_t mode_particleBalance(void) { ParticleSystem1D *PartSys = NULL; @@ -9779,14 +9778,13 @@ uint16_t mode_particleChase(void) { PartSys->update(); // update and render return FRAMETIME; } -static const char _data_FX_MODE_PS_CHASE[] PROGMEM = "PS Chase@!,Density,Size,Hue,Blur,,,Position Color;,!;!;1;pal=11,sx=50,ix=100,c2=5,c3=0"; +static const char _data_FX_MODE_PS_CHASE[] PROGMEM = "PS Chase@!,Density,Size,Hue,Blur,,,Position Color;,!;!;1;pal=11,sx=50,c2=5,c3=0"; /* -Particle Fireworks Starburst replacement (smoother rendering, more settings) -Uses palette for particle color -by DedeHai (Damian Schneider) + Particle Fireworks Starburst replacement (smoother rendering, more settings) + Uses palette for particle color + by DedeHai (Damian Schneider) */ - uint16_t mode_particleStarburst(void) { ParticleSystem1D *PartSys = NULL; uint32_t i; @@ -9844,11 +9842,10 @@ uint16_t mode_particleStarburst(void) { static const char _data_FX_MODE_PS_STARBURST[] PROGMEM = "PS Starburst@Chance,Fragments,Size,Blur,Cooling,Gravity,Colorful,Push;,!;!;1;pal=52,sx=150,ix=150,c1=120,c2=0,c3=21"; /* -Particle based 1D GEQ effect, each frequency bin gets an emitter, distributed over the strip -Uses palette for particle color -by DedeHai (Damian Schneider) + Particle based 1D GEQ effect, each frequency bin gets an emitter, distributed over the strip + Uses palette for particle color + by DedeHai (Damian Schneider) */ - uint16_t mode_particle1DGEQ(void) { ParticleSystem1D *PartSys = NULL; uint32_t numSources; @@ -9923,9 +9920,9 @@ uint16_t mode_particle1DGEQ(void) { static const char _data_FX_MODE_PS_1D_GEQ[] PROGMEM = "PS GEQ 1D@Speed,!,Size,Blur,,,,;,!;!;1f;pal=0,sx=50,ix=200,c1=0,c2=0,c3=0,o1=1,o2=1"; /* -Particle based Fire effect -Uses palette for particle color -by DedeHai (Damian Schneider) + Particle based Fire effect + Uses palette for particle color + by DedeHai (Damian Schneider) */ uint16_t mode_particleFire1D(void) { ParticleSystem1D *PartSys = NULL; @@ -9989,11 +9986,10 @@ uint16_t mode_particleFire1D(void) { static const char _data_FX_MODE_PS_FIRE1D[] PROGMEM = "PS Fire 1D@!,!,Cooling,Blur;,!;!;1;pal=35,sx=100,ix=50,c1=80,c2=100,c3=28,o1=1,o2=1"; /* -Particle based AR effect, swoop particles along the strip with selected frequency loudness -Uses palette for particle color -by DedeHai (Damian Schneider) + Particle based AR effect, swoop particles along the strip with selected frequency loudness + Uses palette for particle color + by DedeHai (Damian Schneider) */ - uint16_t mode_particle1Dsonicstream(void) { ParticleSystem1D *PartSys = NULL; diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index 755e322c15..e2b307daa1 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -65,7 +65,6 @@ ParticleSystem2D::ParticleSystem2D(uint32_t width, uint32_t height, uint32_t num // update function applies gravity, moves the particles, handles collisions and renders the particles void ParticleSystem2D::update(void) { - PSadvancedParticle *advprop = NULL; //apply gravity globally if enabled if (particlesettings.useGravity) applyGravity(); @@ -831,7 +830,7 @@ void ParticleSystem2D::handleCollisions() { collDistSq = collDistSq * collDistSq; // square it for faster comparison (square is one operation) // note: partices are binned in x-axis, assumption is that no more than half of the particles are in the same bin // if they are, collisionStartIdx is increased so each particle collides at least every second frame (which still gives decent collisions) - constexpr uint32_t BIN_WIDTH = 6 * PS_P_RADIUS; // width of a bin in sub-pixels + constexpr int32_t BIN_WIDTH = 6 * PS_P_RADIUS; // width of a bin in sub-pixels uint32_t maxBinParticles = max((uint32_t)50, (usedParticles + 1) / 2); // assume no more than half of the particles are in the same bin, do not bin small amounts of particles uint32_t numBins = (maxX + (BIN_WIDTH - 1)) / BIN_WIDTH; // number of bins in x direction uint16_t binIndices[maxBinParticles]; // creat array on stack for indices, 2kB max for 1024 particles (ESP32_MAXPARTICLES/2) @@ -1145,10 +1144,9 @@ bool initParticleSystem2D(ParticleSystem2D *&PartSys, uint32_t requestedsources, uint32_t cols = SEGMENT.virtualWidth(); uint32_t rows = SEGMENT.virtualHeight(); uint32_t pixels = cols * rows; - updateRenderingBuffer(SEGMENT.vWidth() * SEGMENT.vHeight(), true, true); // update or create rendering buffer + if(advanced) updateRenderingBuffer(100, false, true); // allocate a 10x10 buffer for rendering advanced particles - uint32_t numparticles = calculateNumberOfParticles2D(pixels, advanced, sizecontrol); PSPRINT(" segmentsize:" + String(cols) + " " + String(rows)); PSPRINT(" request numparticles:" + String(numparticles)); @@ -1160,14 +1158,14 @@ bool initParticleSystem2D(ParticleSystem2D *&PartSys, uint32_t requestedsources, } PartSys = new (SEGENV.data) ParticleSystem2D(cols, rows, numparticles, numsources, advanced, sizecontrol); // particle system constructor - + updateRenderingBuffer(SEGMENT.vWidth() * SEGMENT.vHeight(), true, true); // update or create rendering buffer note: for fragmentation it might be better to allocate this first, but if memory is scarce, system has a buffer but no particles and will return false + PSPRINTLN("******init done, pointers:"); #ifdef WLED_DEBUG_PS PSPRINT("framebfr size:"); PSPRINT(frameBufferSize); PSPRINT(" @ addr: 0x"); Serial.println((uintptr_t)framebuffer, HEX); - PSPRINT("renderbfr size:"); PSPRINT(renderBufferSize); PSPRINT(" @ addr: 0x"); @@ -1215,8 +1213,6 @@ ParticleSystem1D::ParticleSystem1D(uint32_t length, uint32_t numberofparticles, // update function applies gravity, moves the particles, handles collisions and renders the particles void ParticleSystem1D::update(void) { - PSadvancedParticle1D *advprop = NULL; - //apply gravity globally if enabled if (particlesettings.useGravity) //note: in 1D system, applying gravity after collisions also works but may be worse applyGravity(); @@ -1666,7 +1662,7 @@ void ParticleSystem1D::handleCollisions() { int32_t collisiondistance = PS_P_MINHARDRADIUS_1D; // note: partices are binned by position, assumption is that no more than half of the particles are in the same bin // if they are, collisionStartIdx is increased so each particle collides at least every second frame (which still gives decent collisions) - constexpr uint32_t BIN_WIDTH = 32 * PS_P_RADIUS_1D; // width of each bin, a compromise between speed and accuracy (lareger bins are faster but collapse more) + constexpr int32_t BIN_WIDTH = 32 * PS_P_RADIUS_1D; // width of each bin, a compromise between speed and accuracy (lareger bins are faster but collapse more) uint32_t maxBinParticles = max((uint32_t)50, (usedParticles + 1) / 4); // do not bin small amounts, limit max to 1/2 of particles uint32_t numBins = (maxX + (BIN_WIDTH - 1)) / BIN_WIDTH; // calculate number of bins uint16_t binIndices[maxBinParticles]; // array to store indices of particles in a bin @@ -1876,8 +1872,7 @@ bool allocateParticleSystemMemory1D(const uint32_t numparticles, const uint32_t // initialize Particle System, allocate additional bytes if needed (pointer to those bytes can be read from particle system class: PSdataEnd) // note: percentofparticles is in uint8_t, for example 191 means 75%, (deafaults to 255 or 100% meaning one particle per pixel), can be more than 100% (but not recommended, can cause out of memory) bool initParticleSystem1D(ParticleSystem1D *&PartSys, const uint32_t requestedsources, const uint8_t fractionofparticles, const uint32_t additionalbytes, const bool advanced) { - if (SEGLEN == 1) return false; // single pixel not supported - updateRenderingBuffer(SEGMENT.vLength(), true, true); // update/create frame rendering buffer + if (SEGLEN == 1) return false; // single pixel not supported if(advanced) updateRenderingBuffer(10, false, true); // buffer for advanced particles, fixed size uint32_t numparticles = calculateNumberOfParticles1D(fractionofparticles, advanced); @@ -1886,8 +1881,8 @@ bool initParticleSystem1D(ParticleSystem1D *&PartSys, const uint32_t requestedso DEBUG_PRINT(F("PS init failed: memory depleted")); return false; } - PartSys = new (SEGENV.data) ParticleSystem1D(SEGMENT.virtualLength(), numparticles, numsources, advanced); // particle system constructor + updateRenderingBuffer(SEGMENT.vLength(), true, true); // update/create frame rendering buffer note: for fragmentation it might be better to allocate this first, but if memory is scarce, system has a buffer but no particles and will return false return true; } @@ -2117,7 +2112,7 @@ void* particleMemoryManager(const uint32_t requestedParticles, size_t structSize if(maxParticles / numParticlesUsed > 3) { // FX uses less than 25%: move the already existing particles to the beginning of the buffer uint32_t usedbytes = availableToPS * structSize; int32_t bufferoffset = (maxParticles - 1) - availableToPS; // offset to existing particles (see above) - if(bufferoffset < maxParticles) { // safety check + if(bufferoffset < (int)maxParticles) { // safety check void* currentBuffer = (void*)((uint8_t*)buffer + bufferoffset * structSize); // pointer to current buffer start memmove(buffer, currentBuffer, usedbytes); // move the existing particles to the beginning of the buffer } From 3cf5b814a4ee659c7fb9eaf2a8e3c4f7af965c68 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Mon, 20 Jan 2025 20:12:55 +0100 Subject: [PATCH 196/219] Improved collision binning for large particles, improved pinball FX --- wled00/FX.cpp | 36 ++++++++++++++++++++++-------------- wled00/FXparticleSystem.cpp | 24 +++++++++++++----------- 2 files changed, 35 insertions(+), 25 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 4c0f737005..ea77ee93b3 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -9078,10 +9078,11 @@ uint16_t mode_particlePinball(void) { if (!initParticleSystem1D(PartSys, 1, 128, 0, true)) // init return mode_static(); // allocation failed or is single pixel PartSys->sources[0].sourceFlags.collide = true; // seeded particles will collide (if enabled) - PartSys->setKillOutOfBounds(true); // out of bounds particles dont return PartSys->sources[0].source.x = PS_P_RADIUS_1D; //emit at bottom + PartSys->setKillOutOfBounds(true); // out of bounds particles dont return + PartSys->setUsedParticles(255); // use all available particles for init SEGENV.aux0 = 1; - SEGENV.aux1 = 500; //set out of speed range to ensure uptate on first call + SEGENV.aux1 = 5000; //set out of range to ensure uptate on first call } else PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS @@ -9095,34 +9096,41 @@ uint16_t mode_particlePinball(void) { PartSys->setGravity(map(SEGMENT.custom3, 0 , 31, 0 , 16)); // set gravity (8 is default strength) PartSys->setBounce(SEGMENT.custom3); // disables bounce if no gravity is used PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur - PartSys->enableParticleCollisions(SEGMENT.check1, 254); // enable collisions and set particle collision hardness (do not use full hardness or particles speed up due to pushing, can not be made perfectly balanced) + PartSys->enableParticleCollisions(SEGMENT.check1, 254); // enable collisions and set particle collision to high hardness PartSys->setUsedParticles(SEGMENT.intensity); PartSys->setColorByPosition(SEGMENT.check3); bool updateballs = false; - if (SEGENV.aux1 != SEGMENT.speed + SEGMENT.intensity + SEGMENT.check2 + SEGMENT.custom1) { // user settings change + if (SEGENV.aux1 != SEGMENT.speed + SEGMENT.intensity + SEGMENT.check2 + SEGMENT.custom1 + PartSys->usedParticles) { // user settings change or more particles are available SEGENV.step = SEGMENT.call; // reset delay updateballs = true; - PartSys->sources[0].maxLife = SEGMENT.custom3 ? 1000 : 0xFFFF; // maximum lifetime in frames (very long if not using gravity, this is enough to travel 4000 pixels at min speed) + PartSys->sources[0].maxLife = SEGMENT.custom3 ? 5000 : 0xFFFF; // maximum lifetime in frames/2 (very long if not using gravity, this is enough to travel 4000 pixels at min speed) PartSys->sources[0].minLife = PartSys->sources[0].maxLife >> 1; } if (SEGMENT.check2) { //rolling balls PartSys->setGravity(0); PartSys->setWallHardness(255); - + int speedsum = 0; for (uint32_t i = 0; i < PartSys->usedParticles; i++) { - if ((PartSys->particles[i].vx > 8 || PartSys->particles[i].vx < -8) && PartSys->particles[i].ttl > 200) //let only slow particles die (ensures no stopped particles) - PartSys->particles[i].ttl = 260; //set alive at full intensity - if (updateballs || PartSys->particles[i].ttl == 0) { //speed changed or particle died, set particle properties - PartSys->particles[i].ttl = 260; + PartSys->particles[i].ttl = 260; // keep particles alive + if (updateballs) { //speed changed or particle is dead, set particle properties PartSys->particleFlags[i].collide = true; - int32_t newspeed = hw_random16(20 + (SEGMENT.speed >> 2)) + (SEGMENT.speed >> 3); + if(PartSys->particles[i].x == 0) { // still at initial position (when not switching from a PS) + PartSys->particles[i].x = hw_random16(PartSys->maxX); // random initial position for all particles + PartSys->particles[i].vx = (hw_random16() & 0x01) ? 1 : -1; // random initial direction + } + int8_t newspeed = 2 + hw_random16(SEGMENT.speed >> 2); PartSys->particles[i].vx = PartSys->particles[i].vx > 0 ? newspeed : -newspeed; //keep the direction PartSys->particles[i].hue = hw_random8(); //set ball colors to random PartSys->advPartProps[i].sat = 255; PartSys->advPartProps[i].size = SEGMENT.custom1; } + speedsum += abs(PartSys->particles[i].vx); + } + if(speedsum / PartSys->usedParticles < 3 + (SEGMENT.speed >> 2)) { // if balls are slow, speed up one of them at random to keep the animation going + int idx = hw_random16(PartSys->usedParticles); + PartSys->particles[idx].vx += PartSys->particles[idx].vx > 0 ? 2 : -2; // keep direction } } else { //bouncing balls @@ -9151,7 +9159,7 @@ uint16_t mode_particlePinball(void) { PartSys->sprayEmit(PartSys->sources[0]); } } - SEGENV.aux1 = SEGMENT.speed + SEGMENT.intensity + SEGMENT.check2 + SEGMENT.custom1; + SEGENV.aux1 = SEGMENT.speed + SEGMENT.intensity + SEGMENT.check2 + SEGMENT.custom1 + PartSys->usedParticles; for (uint32_t i = 0; i < PartSys->usedParticles; i++) { PartSys->particleMoveUpdate(PartSys->particles[i], PartSys->particleFlags[i]); // double the speed } @@ -9390,7 +9398,7 @@ uint16_t mode_particleFireworks1D(void) { return FRAMETIME; } -static const char _data_FX_MODE_PS_FIREWORKS1D[] PROGMEM = "PS Fireworks 1D@Gravity,Explosion,Firing side,Blur,Saturation,,Colorful,Smooth;,!;!;1;pal=0,sx=150,c2=30,c3=21,o2=1"; +static const char _data_FX_MODE_PS_FIREWORKS1D[] PROGMEM = "PS Fireworks 1D@Gravity,Explosion,Firing side,Blur,Saturation,,Colorful,Smooth;,!;!;1;sx=150,c2=30,c3=31,o2=1"; /* Particle based Sparkle effect @@ -9737,7 +9745,7 @@ uint16_t mode_particleChase(void) { // Particle System settings PartSys->updateSystem(); // update system properties (dimensions and data pointers) PartSys->setColorByPosition(SEGMENT.check3); - PartSys->setMotionBlur((SEGMENT.custom3 + 1) << 3); // anable motion blur + PartSys->setMotionBlur(8 + ((SEGMENT.custom3) << 3)); // anable motion blur // uint8_t* basehue = (PartSys->PSdataEnd + 2); //assign data pointer uint32_t settingssum = SEGMENT.speed + SEGMENT.intensity + SEGMENT.custom1 + SEGMENT.custom2 + SEGMENT.check1 + SEGMENT.check2 + SEGMENT.check3 + PartSys->getAvailableParticles(); // note: getAvailableParticles is used to enforce update during transitions diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index e2b307daa1..ccfc5288e5 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -830,19 +830,21 @@ void ParticleSystem2D::handleCollisions() { collDistSq = collDistSq * collDistSq; // square it for faster comparison (square is one operation) // note: partices are binned in x-axis, assumption is that no more than half of the particles are in the same bin // if they are, collisionStartIdx is increased so each particle collides at least every second frame (which still gives decent collisions) - constexpr int32_t BIN_WIDTH = 6 * PS_P_RADIUS; // width of a bin in sub-pixels + int32_t binWidth = 6 * (PS_P_MINHARDRADIUS + particlesize); // width of a bin in sub-pixels + if (advPartProps) //may be using individual particle size + binWidth += 128; // add half of max radius uint32_t maxBinParticles = max((uint32_t)50, (usedParticles + 1) / 2); // assume no more than half of the particles are in the same bin, do not bin small amounts of particles - uint32_t numBins = (maxX + (BIN_WIDTH - 1)) / BIN_WIDTH; // number of bins in x direction + uint32_t numBins = (maxX + (binWidth - 1)) / binWidth; // number of bins in x direction uint16_t binIndices[maxBinParticles]; // creat array on stack for indices, 2kB max for 1024 particles (ESP32_MAXPARTICLES/2) uint32_t binParticleCount; // number of particles in the current bin uint16_t nextFrameStartIdx = 0; // index of the first particle in the next frame (set if bin overflow) uint32_t pidx = collisionStartIdx; //start index in case a bin is full, process remaining particles next frame - + // fill the binIndices array for this bin for (uint32_t bin = 0; bin < numBins; bin++) { binParticleCount = 0; // reset for this bin - int32_t binStart = bin * BIN_WIDTH; - int32_t binEnd = binStart + BIN_WIDTH; + int32_t binStart = bin * binWidth; + int32_t binEnd = binStart + binWidth; // fill the binIndices array for this bin for (uint32_t i = 0; i < usedParticles; i++) { @@ -1662,26 +1664,26 @@ void ParticleSystem1D::handleCollisions() { int32_t collisiondistance = PS_P_MINHARDRADIUS_1D; // note: partices are binned by position, assumption is that no more than half of the particles are in the same bin // if they are, collisionStartIdx is increased so each particle collides at least every second frame (which still gives decent collisions) - constexpr int32_t BIN_WIDTH = 32 * PS_P_RADIUS_1D; // width of each bin, a compromise between speed and accuracy (lareger bins are faster but collapse more) + int32_t binWidth = 32 * (PS_P_MINHARDRADIUS_1D + particlesize); // width of each bin, a compromise between speed and accuracy (lareger bins are faster but collapse more) uint32_t maxBinParticles = max((uint32_t)50, (usedParticles + 1) / 4); // do not bin small amounts, limit max to 1/2 of particles - uint32_t numBins = (maxX + (BIN_WIDTH - 1)) / BIN_WIDTH; // calculate number of bins + uint32_t numBins = (maxX + (binWidth - 1)) / binWidth; // calculate number of bins uint16_t binIndices[maxBinParticles]; // array to store indices of particles in a bin uint32_t binParticleCount; // number of particles in the current bin uint16_t nextFrameStartIdx = 0; // index of the first particle in the next frame (set if bin overflow) uint32_t pidx = collisionStartIdx; //start index in case a bin is full, process remaining particles next frame for (uint32_t bin = 0; bin < numBins; bin++) { binParticleCount = 0; // reset for this bin - int32_t binStart = bin * BIN_WIDTH; - int32_t binEnd = binStart + BIN_WIDTH; + int32_t binStart = bin * binWidth; + int32_t binEnd = binStart + binWidth; // fill the binIndices array for this bin for (uint32_t i = 0; i < usedParticles; i++) { if (particles[pidx].ttl > 0 && particleFlags[pidx].outofbounds == 0 && particleFlags[pidx].collide) { // colliding particle // if gravity is not used and wall bounce is enabled: particles in the first or last bin use fixed force direction (no collapsing, no push inversion) if (!particlesettings.useGravity && particlesettings.bounce) { - if (particles[pidx].x < BIN_WIDTH) + if (particles[pidx].x < binWidth) particleFlags[pidx].forcedirection = false; - else if (particles[pidx].x > (maxX - BIN_WIDTH)) + else if (particles[pidx].x > (maxX - binWidth)) particleFlags[pidx].forcedirection = true; } if (particles[pidx].x >= binStart && particles[pidx].x <= binEnd) { // >= and <= to include particles on the edge of the bin (overlap to ensure boarder particles collide with adjacent bins) From 83c1af3968d196bcd91fb2b1483ada8d1b821252 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Tue, 21 Jan 2025 07:46:52 +0100 Subject: [PATCH 197/219] improved speed handling in pinball FX - rolling --- wled00/FX.cpp | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index ea77ee93b3..ab64999781 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -9120,18 +9120,22 @@ uint16_t mode_particlePinball(void) { PartSys->particles[i].x = hw_random16(PartSys->maxX); // random initial position for all particles PartSys->particles[i].vx = (hw_random16() & 0x01) ? 1 : -1; // random initial direction } - int8_t newspeed = 2 + hw_random16(SEGMENT.speed >> 2); - PartSys->particles[i].vx = PartSys->particles[i].vx > 0 ? newspeed : -newspeed; //keep the direction PartSys->particles[i].hue = hw_random8(); //set ball colors to random PartSys->advPartProps[i].sat = 255; PartSys->advPartProps[i].size = SEGMENT.custom1; } speedsum += abs(PartSys->particles[i].vx); } - if(speedsum / PartSys->usedParticles < 3 + (SEGMENT.speed >> 2)) { // if balls are slow, speed up one of them at random to keep the animation going - int idx = hw_random16(PartSys->usedParticles); - PartSys->particles[idx].vx += PartSys->particles[idx].vx > 0 ? 2 : -2; // keep direction + int32_t avgSpeed = speedsum / PartSys->usedParticles; + int32_t setSpeed = 3 + (SEGMENT.speed >> 2); + if (avgSpeed < setSpeed) { // if balls are slow, speed up some of them at random to keep the animation going + for (int i = 0; i < setSpeed - avgSpeed; i++) { + int idx = hw_random16(PartSys->usedParticles); + PartSys->particles[idx].vx += PartSys->particles[idx].vx > 0 ? 1 : -1; // keep direction + } } + else if (avgSpeed > setSpeed + 2) // if avg speed is too high, apply friction to slow them down + PartSys->applyFriction(1); } else { //bouncing balls PartSys->setWallHardness(220); From 8cadca54c1d040d1955c2100e2bf56ab319d35e9 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sun, 19 Jan 2025 18:40:19 +0100 Subject: [PATCH 198/219] disable 2D PS for ESP8266, some cleanup, improved pinball FX parameters, bugfixes --- platformio.ini | 3 + wled00/FX.cpp | 182 ++++++++++++++++++------------------ wled00/FXparticleSystem.cpp | 21 ++--- 3 files changed, 100 insertions(+), 106 deletions(-) diff --git a/platformio.ini b/platformio.ini index 24cb7c9fc1..78aaa6eb62 100644 --- a/platformio.ini +++ b/platformio.ini @@ -357,6 +357,7 @@ platform_packages = ${common.platform_packages} board_build.ldscript = ${common.ldscript_4m1m} build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=\"ESP8266\" #-DWLED_DISABLE_2D + -D WLED_DISABLE_PARTICLESYSTEM2D lib_deps = ${esp8266.lib_deps} monitor_filters = esp8266_exception_decoder @@ -366,6 +367,7 @@ extends = env:nodemcuv2 platform = ${esp8266.platform_compat} platform_packages = ${esp8266.platform_packages_compat} build_flags = ${common.build_flags} ${esp8266.build_flags_compat} -D WLED_RELEASE_NAME=\"ESP8266_compat\" #-DWLED_DISABLE_2D + -D WLED_DISABLE_PARTICLESYSTEM2D ;; lib_deps = ${esp8266.lib_deps_compat} ;; experimental - use older NeoPixelBus 2.7.9 [env:nodemcuv2_160] @@ -373,6 +375,7 @@ extends = env:nodemcuv2 board_build.f_cpu = 160000000L build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=\"ESP8266_160\" #-DWLED_DISABLE_2D -D USERMOD_AUDIOREACTIVE + -D WLED_DISABLE_PARTICLESYSTEM2D [env:esp8266_2m] board = esp_wroom_02 diff --git a/wled00/FX.cpp b/wled00/FX.cpp index cc577b2167..4c0f737005 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -7632,11 +7632,11 @@ static const char _data_FX_MODE_2DWAVINGCELL[] PROGMEM = "Waving Cell@!,Blur,Amp #ifndef WLED_DISABLE_PARTICLESYSTEM2D /* - * Particle System Vortex - * Particles sprayed from center with a rotating spray - * Uses palette for particle color - * by DedeHai (Damian Schneider) - */ + Particle System Vortex + Particles sprayed from center with a rotating spray + Uses palette for particle color + by DedeHai (Damian Schneider) +*/ #define NUMBEROFSOURCES 8 uint16_t mode_particlevortex(void) { if (SEGLEN == 1) @@ -7747,10 +7747,10 @@ uint16_t mode_particlevortex(void) { static const char _data_FX_MODE_PARTICLEVORTEX[] PROGMEM = "PS Vortex@Rotation Speed,Particle Speed,Arms,Flip,Nozzle,Smear,Direction,Random Flip;;!;2;pal=27,c1=200,c2=0,c3=0"; /* - * Particle Fireworks - * Rockets shoot up and explode in a random color, sometimes in a defined pattern - * by DedeHai (Damian Schneider) - */ + Particle Fireworks + Rockets shoot up and explode in a random color, sometimes in a defined pattern + by DedeHai (Damian Schneider) +*/ #define NUMBEROFSOURCES 8 uint16_t mode_particlefireworks(void) { @@ -7857,8 +7857,8 @@ uint16_t mode_particlefireworks(void) { PartSys->sources[j].var = angle & 1; // 0 or 1 variation, angle is random } } - uint32_t i = 0; - for (i; i < emitparticles; i++) { + uint32_t i; + for (i = 0; i < emitparticles; i++) { if (circularexplosion) { int32_t sineMod = 0xEFFF + sin16_t((uint16_t)(((angle * frequency) >> 4) + baseangle)); // shifted to positive values currentspeed = (speed/2 + ((sineMod * speed) >> 16)) >> 1; // sine modulation on speed based on emit angle @@ -7895,11 +7895,11 @@ uint16_t mode_particlefireworks(void) { static const char _data_FX_MODE_PARTICLEFIREWORKS[] PROGMEM = "PS Fireworks@Launches,Explosion Size,Fuse,Blur,Gravity,Cylinder,Ground,Fast;;!;2;pal=11,sx=100,ix=50,c1=40,c2=0,c3=12"; /* - * Particle Volcano - * Particles are sprayed from below, spray moves back and forth if option is set - * Uses palette for particle color - * by DedeHai (Damian Schneider) - */ + Particle Volcano + Particles are sprayed from below, spray moves back and forth if option is set + Uses palette for particle color + by DedeHai (Damian Schneider) +*/ #define NUMBEROFSOURCES 1 uint16_t mode_particlevolcano(void) { ParticleSystem2D *PartSys = NULL; @@ -8464,10 +8464,10 @@ uint16_t mode_particleimpact(void) { static const char _data_FX_MODE_PARTICLEIMPACT[] PROGMEM = "PS Impact@Launches,Size,Force,Hardness,Blur,Cylinder,Walls,Collide;;!;2;pal=0,sx=32,ix=85,c1=70,c2=130,c3=0,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) + 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) { ParticleSystem2D *PartSys = NULL; @@ -8561,9 +8561,9 @@ uint16_t mode_particleattractor(void) { static const char _data_FX_MODE_PARTICLEATTRACTOR[] PROGMEM = "PS Attractor@Mass,Particles,Size,Collide,Friction,AgeColor,Move,Swallow;;!;2;pal=9,sx=100,ix=82,c1=0,c2=0"; /* -Particle Spray, just a particle spray with many parameters -Uses palette for particle color -by DedeHai (Damian Schneider) + Particle Spray, just a particle spray with many parameters + Uses palette for particle color + by DedeHai (Damian Schneider) */ uint16_t mode_particlespray(void) { ParticleSystem2D *PartSys = NULL; @@ -8647,9 +8647,9 @@ static const char _data_FX_MODE_PARTICLESPRAY[] PROGMEM = "PS Spray@Speed,!,Left /* -Particle base Graphical Equalizer -Uses palette for particle color -by DedeHai (Damian Schneider) + Particle base Graphical Equalizer + Uses palette for particle color + by DedeHai (Damian Schneider) */ uint16_t mode_particleGEQ(void) { ParticleSystem2D *PartSys = NULL; @@ -8723,11 +8723,11 @@ uint16_t mode_particleGEQ(void) { static const char _data_FX_MODE_PARTICLEGEQ[] PROGMEM = "PS GEQ 2D@Speed,Intensity,Diverge,Bounce,Gravity,Cylinder,Walls,Floor;;!;2f;pal=0,sx=155,ix=200,c1=0"; /* - * Particle rotating GEQ - * Particles sprayed from center with rotating spray - * Uses palette for particle color - * by DedeHai (Damian Schneider) - */ + Particle rotating GEQ + Particles sprayed from center with rotating spray + Uses palette for particle color + by DedeHai (Damian Schneider) +*/ #define NUMBEROFSOURCES 16 uint16_t mode_particlecenterGEQ(void) { ParticleSystem2D *PartSys = NULL; @@ -8798,7 +8798,7 @@ uint16_t mode_particlecenterGEQ(void) { static const char _data_FX_MODE_PARTICLECIRCULARGEQ[] PROGMEM = "PS GEQ Nova@Speed,Intensity,Rotation Speed,Color Change,Nozzle,,Direction;;!;2f;pal=13,ix=180,c1=0,c2=0,c3=8"; /* -Particle replacement of Ghost Rider by DedeHai (Damian Schneider), original by stepko adapted by Blaz Kristan (AKA blazoncek) + Particle replacement of Ghost Rider by DedeHai (Damian Schneider), original FX by stepko adapted by Blaz Kristan (AKA blazoncek) */ #define MAXANGLESTEP 2200 //32767 means 180° uint16_t mode_particleghostrider(void) { @@ -8878,9 +8878,9 @@ uint16_t mode_particleghostrider(void) { static const char _data_FX_MODE_PARTICLEGHOSTRIDER[] PROGMEM = "PS Ghost Rider@Speed,Spiral,Blur,Color Cycle,Spread,AgeColor,Walls;;!;2;pal=1,sx=70,ix=0,c1=220,c2=30,c3=21,o1=1"; /* -PS Blobs: large particles bouncing around, changing size and form -Uses palette for particle color -by DedeHai (Damian Schneider) + PS Blobs: large particles bouncing around, changing size and form + Uses palette for particle color + by DedeHai (Damian Schneider) */ uint16_t mode_particleblobs(void) { ParticleSystem2D *PartSys = NULL; @@ -8954,19 +8954,17 @@ uint16_t mode_particleblobs(void) { } static const char _data_FX_MODE_PARTICLEBLOBS[] PROGMEM = "PS Blobs@Speed,Blobs,Size,Life,Blur,Wobble,Collide,Pulsate;;!;2v;sx=30,ix=64,c1=200,c2=130,c3=0,o3=1"; #endif //WLED_DISABLE_PARTICLESYSTEM2D - #endif // WLED_DISABLE_2D - /////////////////////////// // 1D Particle System FX // /////////////////////////// #ifndef WLED_DISABLE_PARTICLESYSTEM1D /* -Particle version of Drip and Rain -Uses palette for particle color -by DedeHai (Damian Schneider) + Particle version of Drip and Rain + Uses palette for particle color + by DedeHai (Damian Schneider) */ uint16_t mode_particleDrip(void) { ParticleSystem1D *PartSys = NULL; @@ -9068,10 +9066,10 @@ static const char _data_FX_MODE_PARTICLEDRIP[] PROGMEM = "PS DripDrop@Speed,!,Sp /* -Particle Replacement for "Bbouncing Balls by Aircoookie" -Also replaces rolling balls and juggle (and maybe popcorn) -Uses palette for particle color -by DedeHai (Damian Schneider) + Particle Replacement for "Bbouncing Balls by Aircoookie" + Also replaces rolling balls and juggle (and maybe popcorn) + Uses palette for particle color + by DedeHai (Damian Schneider) */ uint16_t mode_particlePinball(void) { ParticleSystem1D *PartSys = NULL; @@ -9082,8 +9080,6 @@ uint16_t mode_particlePinball(void) { PartSys->sources[0].sourceFlags.collide = true; // seeded particles will collide (if enabled) PartSys->setKillOutOfBounds(true); // out of bounds particles dont return PartSys->sources[0].source.x = PS_P_RADIUS_1D; //emit at bottom - PartSys->sources[0].maxLife = 0xFFFF; // maximum lifetime in frames (long but not infinite to avoid perpetual handling, this is enough to travel 4000 pixels at min speed) - PartSys->sources[0].minLife = PartSys->sources[0].maxLife; SEGENV.aux0 = 1; SEGENV.aux1 = 500; //set out of speed range to ensure uptate on first call } @@ -9104,8 +9100,12 @@ uint16_t mode_particlePinball(void) { PartSys->setColorByPosition(SEGMENT.check3); bool updateballs = false; - if (SEGENV.aux1 != SEGMENT.speed + SEGMENT.intensity + SEGMENT.check2 + SEGMENT.custom1) // user settings change + if (SEGENV.aux1 != SEGMENT.speed + SEGMENT.intensity + SEGMENT.check2 + SEGMENT.custom1) { // user settings change + SEGENV.step = SEGMENT.call; // reset delay updateballs = true; + PartSys->sources[0].maxLife = SEGMENT.custom3 ? 1000 : 0xFFFF; // maximum lifetime in frames (very long if not using gravity, this is enough to travel 4000 pixels at min speed) + PartSys->sources[0].minLife = PartSys->sources[0].maxLife >> 1; + } if (SEGMENT.check2) { //rolling balls PartSys->setGravity(0); @@ -9127,14 +9127,14 @@ uint16_t mode_particlePinball(void) { } else { //bouncing balls PartSys->setWallHardness(220); - PartSys->sources[0].var = SEGMENT.speed >> 3; - int32_t newspeed = 2 + (SEGMENT.speed >> 1) - (SEGMENT.speed >> 3); - PartSys->sources[0].v = newspeed; + PartSys->sources[0].var = SEGMENT.speed >> 3; + int32_t newspeed = 2 + (SEGMENT.speed >> 1) - (SEGMENT.speed >> 3); + PartSys->sources[0].v = newspeed; //check for balls that are 'laying on the ground' and remove them for (uint32_t i = 0; i < PartSys->usedParticles; i++) { if (PartSys->particles[i].vx == 0 && PartSys->particles[i].x < (PS_P_RADIUS_1D + SEGMENT.custom1)) PartSys->particles[i].ttl = 0; - if (updateballs && SEGMENT.custom3 == 0) { + if (updateballs) { PartSys->advPartProps[i].size = SEGMENT.custom1; if(SEGMENT.custom3 == 0) //gravity off, update speed PartSys->particles[i].vx = PartSys->particles[i].vx > 0 ? newspeed : -newspeed; //keep the direction @@ -9143,7 +9143,7 @@ uint16_t mode_particlePinball(void) { // every nth frame emit a ball if (SEGMENT.call > SEGENV.step) { - int interval = 520 - ((int)SEGMENT.intensity << 1); + int interval = 260 - ((int)SEGMENT.intensity); SEGENV.step += interval + hw_random16(interval); PartSys->sources[0].source.hue = hw_random16(); //set ball color PartSys->sources[0].sat = 255; @@ -9153,23 +9153,22 @@ uint16_t mode_particlePinball(void) { } SEGENV.aux1 = SEGMENT.speed + SEGMENT.intensity + SEGMENT.check2 + SEGMENT.custom1; for (uint32_t i = 0; i < PartSys->usedParticles; i++) { - //if (SEGMENT.speed > 200) - PartSys->particleMoveUpdate(PartSys->particles[i], PartSys->particleFlags[i]); //increase speed on high settings by calling the move function twice + PartSys->particleMoveUpdate(PartSys->particles[i], PartSys->particleFlags[i]); // double the speed } PartSys->update(); // update and render return FRAMETIME; } -static const char _data_FX_MODE_PSPINBALL[] PROGMEM = "PS Pinball@Speed,!,Size,Blur,Gravity,Collide,Rolling,Position Color;,!;!;1;pal=0,c1=30,c2=0,c3=8"; +static const char _data_FX_MODE_PSPINBALL[] PROGMEM = "PS Pinball@Speed,!,Size,Blur,Gravity,Collide,Rolling,Position Color;,!;!;1;pal=0,ix=220,c2=0,c3=8,o1=1"; /* -Particle Replacement for original Dancing Shadows: -"Spotlights moving back and forth that cast dancing shadows. -Shine this through tree branches/leaves or other close-up objects that cast -interesting shadows onto a ceiling or tarp. -By Steve Pomeroy @xxv" -Uses palette for particle color -by DedeHai (Damian Schneider) + Particle Replacement for original Dancing Shadows: + "Spotlights moving back and forth that cast dancing shadows. + Shine this through tree branches/leaves or other close-up objects that cast + interesting shadows onto a ceiling or tarp. + By Steve Pomeroy @xxv" + Uses palette for particle color + by DedeHai (Damian Schneider) */ uint16_t mode_particleDancingShadows(void) { ParticleSystem1D *PartSys = NULL; @@ -9283,9 +9282,9 @@ uint16_t mode_particleDancingShadows(void) { static const char _data_FX_MODE_PARTICLEDANCINGSHADOWS[] PROGMEM = "PS Dancing Shadows@Speed,!,Blur,Color Cycle,,Smear,Position Color,Smooth;,!;!;1;sx=100,ix=180,c1=0,c2=0"; /* -Particle Fireworks 1D replacement -Uses palette for particle color -by DedeHai (Damian Schneider) + Particle Fireworks 1D replacement + Uses palette for particle color + by DedeHai (Damian Schneider) */ uint16_t mode_particleFireworks1D(void) { ParticleSystem1D *PartSys = NULL; @@ -9394,9 +9393,9 @@ uint16_t mode_particleFireworks1D(void) { static const char _data_FX_MODE_PS_FIREWORKS1D[] PROGMEM = "PS Fireworks 1D@Gravity,Explosion,Firing side,Blur,Saturation,,Colorful,Smooth;,!;!;1;pal=0,sx=150,c2=30,c3=21,o2=1"; /* -Particle based Sparkle effect -Uses palette for particle color -by DedeHai (Damian Schneider) + Particle based Sparkle effect + Uses palette for particle color + by DedeHai (Damian Schneider) */ uint16_t mode_particleSparkler(void) { ParticleSystem1D *PartSys = NULL; @@ -9466,9 +9465,9 @@ uint16_t mode_particleSparkler(void) { static const char _data_FX_MODE_PS_SPARKLER[] PROGMEM = "PS Sparkler@Move,!,Saturation,Blur,Sparklers,Slide,Bounce,Large;,!;!;1;pal=0,sx=255,c1=0,c2=0,c3=6"; /* -Particle based Hourglass, particles falling at defined intervals -Uses palette for particle color -by DedeHai (Damian Schneider) + Particle based Hourglass, particles falling at defined intervals + Uses palette for particle color + by DedeHai (Damian Schneider) */ uint16_t mode_particleHourglass(void) { ParticleSystem1D *PartSys = NULL; @@ -9592,9 +9591,9 @@ uint16_t mode_particleHourglass(void) { static const char _data_FX_MODE_PS_HOURGLASS[] PROGMEM = "PS Hourglass@Speed,!,Color,Blur,Gravity,Colorflip,Start,Fast Reset;,!;!;1;pal=34,sx=245,ix=200,c1=140,c2=80,c3=4,o1=1,o2=1,o3=1"; /* -Particle based Spray effect (like a volcano, possible replacement for popcorn) -Uses palette for particle color -by DedeHai (Damian Schneider) + Particle based Spray effect (like a volcano, possible replacement for popcorn) + Uses palette for particle color + by DedeHai (Damian Schneider) */ uint16_t mode_particle1Dspray(void) { ParticleSystem1D *PartSys = NULL; @@ -9644,9 +9643,9 @@ uint16_t mode_particle1Dspray(void) { static const char _data_FX_MODE_PS_1DSPRAY[] PROGMEM = "PS Spray 1D@Speed(+/-),!,Position,Blur,Gravity(+/-),AgeColor,Bounce,Position Color;,!;!;1;sx=200,ix=220,c1=0,c2=0,o1=1"; /* -Particle based balance: particles move back and forth (1D pendent to 2D particle box) -Uses palette for particle color -by DedeHai (Damian Schneider) + Particle based balance: particles move back and forth (1D pendent to 2D particle box) + Uses palette for particle color + by DedeHai (Damian Schneider) */ uint16_t mode_particleBalance(void) { ParticleSystem1D *PartSys = NULL; @@ -9779,14 +9778,13 @@ uint16_t mode_particleChase(void) { PartSys->update(); // update and render return FRAMETIME; } -static const char _data_FX_MODE_PS_CHASE[] PROGMEM = "PS Chase@!,Density,Size,Hue,Blur,,,Position Color;,!;!;1;pal=11,sx=50,ix=100,c2=5,c3=0"; +static const char _data_FX_MODE_PS_CHASE[] PROGMEM = "PS Chase@!,Density,Size,Hue,Blur,,,Position Color;,!;!;1;pal=11,sx=50,c2=5,c3=0"; /* -Particle Fireworks Starburst replacement (smoother rendering, more settings) -Uses palette for particle color -by DedeHai (Damian Schneider) + Particle Fireworks Starburst replacement (smoother rendering, more settings) + Uses palette for particle color + by DedeHai (Damian Schneider) */ - uint16_t mode_particleStarburst(void) { ParticleSystem1D *PartSys = NULL; uint32_t i; @@ -9844,11 +9842,10 @@ uint16_t mode_particleStarburst(void) { static const char _data_FX_MODE_PS_STARBURST[] PROGMEM = "PS Starburst@Chance,Fragments,Size,Blur,Cooling,Gravity,Colorful,Push;,!;!;1;pal=52,sx=150,ix=150,c1=120,c2=0,c3=21"; /* -Particle based 1D GEQ effect, each frequency bin gets an emitter, distributed over the strip -Uses palette for particle color -by DedeHai (Damian Schneider) + Particle based 1D GEQ effect, each frequency bin gets an emitter, distributed over the strip + Uses palette for particle color + by DedeHai (Damian Schneider) */ - uint16_t mode_particle1DGEQ(void) { ParticleSystem1D *PartSys = NULL; uint32_t numSources; @@ -9923,9 +9920,9 @@ uint16_t mode_particle1DGEQ(void) { static const char _data_FX_MODE_PS_1D_GEQ[] PROGMEM = "PS GEQ 1D@Speed,!,Size,Blur,,,,;,!;!;1f;pal=0,sx=50,ix=200,c1=0,c2=0,c3=0,o1=1,o2=1"; /* -Particle based Fire effect -Uses palette for particle color -by DedeHai (Damian Schneider) + Particle based Fire effect + Uses palette for particle color + by DedeHai (Damian Schneider) */ uint16_t mode_particleFire1D(void) { ParticleSystem1D *PartSys = NULL; @@ -9989,11 +9986,10 @@ uint16_t mode_particleFire1D(void) { static const char _data_FX_MODE_PS_FIRE1D[] PROGMEM = "PS Fire 1D@!,!,Cooling,Blur;,!;!;1;pal=35,sx=100,ix=50,c1=80,c2=100,c3=28,o1=1,o2=1"; /* -Particle based AR effect, swoop particles along the strip with selected frequency loudness -Uses palette for particle color -by DedeHai (Damian Schneider) + Particle based AR effect, swoop particles along the strip with selected frequency loudness + Uses palette for particle color + by DedeHai (Damian Schneider) */ - uint16_t mode_particle1Dsonicstream(void) { ParticleSystem1D *PartSys = NULL; diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index 755e322c15..e2b307daa1 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -65,7 +65,6 @@ ParticleSystem2D::ParticleSystem2D(uint32_t width, uint32_t height, uint32_t num // update function applies gravity, moves the particles, handles collisions and renders the particles void ParticleSystem2D::update(void) { - PSadvancedParticle *advprop = NULL; //apply gravity globally if enabled if (particlesettings.useGravity) applyGravity(); @@ -831,7 +830,7 @@ void ParticleSystem2D::handleCollisions() { collDistSq = collDistSq * collDistSq; // square it for faster comparison (square is one operation) // note: partices are binned in x-axis, assumption is that no more than half of the particles are in the same bin // if they are, collisionStartIdx is increased so each particle collides at least every second frame (which still gives decent collisions) - constexpr uint32_t BIN_WIDTH = 6 * PS_P_RADIUS; // width of a bin in sub-pixels + constexpr int32_t BIN_WIDTH = 6 * PS_P_RADIUS; // width of a bin in sub-pixels uint32_t maxBinParticles = max((uint32_t)50, (usedParticles + 1) / 2); // assume no more than half of the particles are in the same bin, do not bin small amounts of particles uint32_t numBins = (maxX + (BIN_WIDTH - 1)) / BIN_WIDTH; // number of bins in x direction uint16_t binIndices[maxBinParticles]; // creat array on stack for indices, 2kB max for 1024 particles (ESP32_MAXPARTICLES/2) @@ -1145,10 +1144,9 @@ bool initParticleSystem2D(ParticleSystem2D *&PartSys, uint32_t requestedsources, uint32_t cols = SEGMENT.virtualWidth(); uint32_t rows = SEGMENT.virtualHeight(); uint32_t pixels = cols * rows; - updateRenderingBuffer(SEGMENT.vWidth() * SEGMENT.vHeight(), true, true); // update or create rendering buffer + if(advanced) updateRenderingBuffer(100, false, true); // allocate a 10x10 buffer for rendering advanced particles - uint32_t numparticles = calculateNumberOfParticles2D(pixels, advanced, sizecontrol); PSPRINT(" segmentsize:" + String(cols) + " " + String(rows)); PSPRINT(" request numparticles:" + String(numparticles)); @@ -1160,14 +1158,14 @@ bool initParticleSystem2D(ParticleSystem2D *&PartSys, uint32_t requestedsources, } PartSys = new (SEGENV.data) ParticleSystem2D(cols, rows, numparticles, numsources, advanced, sizecontrol); // particle system constructor - + updateRenderingBuffer(SEGMENT.vWidth() * SEGMENT.vHeight(), true, true); // update or create rendering buffer note: for fragmentation it might be better to allocate this first, but if memory is scarce, system has a buffer but no particles and will return false + PSPRINTLN("******init done, pointers:"); #ifdef WLED_DEBUG_PS PSPRINT("framebfr size:"); PSPRINT(frameBufferSize); PSPRINT(" @ addr: 0x"); Serial.println((uintptr_t)framebuffer, HEX); - PSPRINT("renderbfr size:"); PSPRINT(renderBufferSize); PSPRINT(" @ addr: 0x"); @@ -1215,8 +1213,6 @@ ParticleSystem1D::ParticleSystem1D(uint32_t length, uint32_t numberofparticles, // update function applies gravity, moves the particles, handles collisions and renders the particles void ParticleSystem1D::update(void) { - PSadvancedParticle1D *advprop = NULL; - //apply gravity globally if enabled if (particlesettings.useGravity) //note: in 1D system, applying gravity after collisions also works but may be worse applyGravity(); @@ -1666,7 +1662,7 @@ void ParticleSystem1D::handleCollisions() { int32_t collisiondistance = PS_P_MINHARDRADIUS_1D; // note: partices are binned by position, assumption is that no more than half of the particles are in the same bin // if they are, collisionStartIdx is increased so each particle collides at least every second frame (which still gives decent collisions) - constexpr uint32_t BIN_WIDTH = 32 * PS_P_RADIUS_1D; // width of each bin, a compromise between speed and accuracy (lareger bins are faster but collapse more) + constexpr int32_t BIN_WIDTH = 32 * PS_P_RADIUS_1D; // width of each bin, a compromise between speed and accuracy (lareger bins are faster but collapse more) uint32_t maxBinParticles = max((uint32_t)50, (usedParticles + 1) / 4); // do not bin small amounts, limit max to 1/2 of particles uint32_t numBins = (maxX + (BIN_WIDTH - 1)) / BIN_WIDTH; // calculate number of bins uint16_t binIndices[maxBinParticles]; // array to store indices of particles in a bin @@ -1876,8 +1872,7 @@ bool allocateParticleSystemMemory1D(const uint32_t numparticles, const uint32_t // initialize Particle System, allocate additional bytes if needed (pointer to those bytes can be read from particle system class: PSdataEnd) // note: percentofparticles is in uint8_t, for example 191 means 75%, (deafaults to 255 or 100% meaning one particle per pixel), can be more than 100% (but not recommended, can cause out of memory) bool initParticleSystem1D(ParticleSystem1D *&PartSys, const uint32_t requestedsources, const uint8_t fractionofparticles, const uint32_t additionalbytes, const bool advanced) { - if (SEGLEN == 1) return false; // single pixel not supported - updateRenderingBuffer(SEGMENT.vLength(), true, true); // update/create frame rendering buffer + if (SEGLEN == 1) return false; // single pixel not supported if(advanced) updateRenderingBuffer(10, false, true); // buffer for advanced particles, fixed size uint32_t numparticles = calculateNumberOfParticles1D(fractionofparticles, advanced); @@ -1886,8 +1881,8 @@ bool initParticleSystem1D(ParticleSystem1D *&PartSys, const uint32_t requestedso DEBUG_PRINT(F("PS init failed: memory depleted")); return false; } - PartSys = new (SEGENV.data) ParticleSystem1D(SEGMENT.virtualLength(), numparticles, numsources, advanced); // particle system constructor + updateRenderingBuffer(SEGMENT.vLength(), true, true); // update/create frame rendering buffer note: for fragmentation it might be better to allocate this first, but if memory is scarce, system has a buffer but no particles and will return false return true; } @@ -2117,7 +2112,7 @@ void* particleMemoryManager(const uint32_t requestedParticles, size_t structSize if(maxParticles / numParticlesUsed > 3) { // FX uses less than 25%: move the already existing particles to the beginning of the buffer uint32_t usedbytes = availableToPS * structSize; int32_t bufferoffset = (maxParticles - 1) - availableToPS; // offset to existing particles (see above) - if(bufferoffset < maxParticles) { // safety check + if(bufferoffset < (int)maxParticles) { // safety check void* currentBuffer = (void*)((uint8_t*)buffer + bufferoffset * structSize); // pointer to current buffer start memmove(buffer, currentBuffer, usedbytes); // move the existing particles to the beginning of the buffer } From cbbc08584e00f0b1923e3eedb981d49a5b15e2ea Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Thu, 23 Jan 2025 16:27:47 +0100 Subject: [PATCH 199/219] improved hourglass: millis instead of frame timing, better stacking. and cleanup. --- wled00/FX.cpp | 298 ++++++++++++++++++++++++-------------------------- 1 file changed, 144 insertions(+), 154 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index ab64999781..a0709ea2e8 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -33,24 +33,18 @@ float FFT_MajorPeak = 1.0; uint8_t *fftResult = nullptr; float *fftBin = nullptr; - um_data_t *um_data; - if (UsermodManager::getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { - volumeSmth = *(float*) um_data->u_data[0]; - volumeRaw = *(float*) um_data->u_data[1]; - fftResult = (uint8_t*) um_data->u_data[2]; - samplePeak = *(uint8_t*) um_data->u_data[3]; - FFT_MajorPeak = *(float*) um_data->u_data[4]; - my_magnitude = *(float*) um_data->u_data[5]; - maxVol = (uint8_t*) um_data->u_data[6]; // requires UI element (SEGMENT.customX?), changes source element - binNum = (uint8_t*) um_data->u_data[7]; // requires UI element (SEGMENT.customX?), changes source element - fftBin = (float*) um_data->u_data[8]; - } else { - // add support for no audio data - um_data = simulateSound(SEGMENT.soundSim); - } + um_data_t *um_data = getAudioData(); + volumeSmth = *(float*) um_data->u_data[0]; + volumeRaw = *(float*) um_data->u_data[1]; + fftResult = (uint8_t*) um_data->u_data[2]; + samplePeak = *(uint8_t*) um_data->u_data[3]; + FFT_MajorPeak = *(float*) um_data->u_data[4]; + my_magnitude = *(float*) um_data->u_data[5]; + maxVol = (uint8_t*) um_data->u_data[6]; // requires UI element (SEGMENT.customX?), changes source element + binNum = (uint8_t*) um_data->u_data[7]; // requires UI element (SEGMENT.customX?), changes source element + fftBin = (float*) um_data->u_data[8]; */ - #define IBN 5100 // paletteBlend: 0 - wrap when moving, 1 - always wrap, 2 - never wrap, 3 - none (undefined) #define PALETTE_SOLID_WRAP (strip.paletteBlend == 1 || strip.paletteBlend == 3) @@ -711,7 +705,7 @@ static const char _data_FX_MODE_DISSOLVE_RANDOM[] PROGMEM = "Dissolve Rnd@Repeat * Inspired by www.tweaking4all.com/hardware/arduino/adruino-led-strip-effects/ */ uint16_t mode_sparkle(void) { - if (!SEGMENT.check2) for(unsigned i = 0; i < SEGLEN; i++) { + if (!SEGMENT.check2) for (unsigned i = 0; i < SEGLEN; i++) { SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 1)); } uint32_t cycleTime = 10 + (255 - SEGMENT.speed)*2; @@ -3244,7 +3238,7 @@ uint16_t mode_popcorn(void) { unsigned numPopcorn = SEGMENT.intensity * usablePopcorns / 255; if (numPopcorn == 0) numPopcorn = 1; - for(unsigned i = 0; i < numPopcorn; i++) { + for (unsigned i = 0; i < numPopcorn; i++) { if (popcorn[i].pos >= 0.0f) { // if kernel is active, update its position popcorn[i].pos += popcorn[i].vel; popcorn[i].vel += gravity; @@ -7671,7 +7665,7 @@ uint16_t mode_particlevortex(void) { #ifdef ESP8266 for (i = 1; i < 4; i++) { // need static particles in the center to reduce blinking (would be black every other frame without this hack), just set them there fixed int partindex = (int)PartSys->usedParticles - (int)i; - if(partindex >= 0) { + if (partindex >= 0) { PartSys->particles[partindex].x = (PartSys->maxX + 1) >> 1; // center PartSys->particles[partindex].y = (PartSys->maxY + 1) >> 1; // center PartSys->particles[partindex].sat = 230; @@ -7717,7 +7711,7 @@ uint16_t mode_particlevortex(void) { int32_t speedincrement = speeddiff / 50; if (speedincrement == 0) { //if speeddiff is not zero, make the increment at least 1 so it reaches target speed - if(speeddiff < 0) + if (speeddiff < 0) speedincrement = -1; else if (speeddiff > 0) speedincrement = 1; @@ -7783,7 +7777,7 @@ uint16_t mode_particlefireworks(void) { PartSys->setGravity(map(SEGMENT.custom3, 0, 31, SEGMENT.check2 ? 1 : 0, 10)); // if bounded, set gravity to minimum of 1 or they will bounce at top PartSys->setMotionBlur(map(SEGMENT.custom2, 0, 255, 0, 170)); // anable motion blur uint8_t smearing = 0; - if(SEGMENT.custom2 > 200) + if (SEGMENT.custom2 > 200) smearing = SEGMENT.custom2 - 200; PartSys->setSmearBlur(smearing); // enable 2D blurring (smearing) @@ -7883,7 +7877,7 @@ uint16_t mode_particlefireworks(void) { PartSys->sources[j].source.y = 1000; // reset position so gravity wont pull it to the ground and bounce it (vy MUST stay negative until relaunch) circularexplosion = false; // reset for next rocket } - if(SEGMENT.check3) { // fast speed, move particles twice + if (SEGMENT.check3) { // fast speed, move particles twice for (uint32_t i = 0; i < PartSys->usedParticles; i++) { PartSys->particleMoveUpdate(PartSys->particles[i], PartSys->particleFlags[i], nullptr, nullptr); } @@ -8232,7 +8226,7 @@ uint16_t mode_particlebox(void) { PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 5, 153)); // 2% - 60% // add in new particles if amount has changed for (i = 0; i < PartSys->usedParticles; i++) { - if(PartSys->particles[i].ttl < 260) { // initialize handed over particles and dead particles + if (PartSys->particles[i].ttl < 260) { // initialize handed over particles and dead particles PartSys->particles[i].ttl = 260; // full brigthness PartSys->particles[i].x = hw_random16(PartSys->maxX); PartSys->particles[i].y = hw_random16(PartSys->maxY); @@ -8251,7 +8245,7 @@ uint16_t mode_particlebox(void) { if (SEGMENT.check2) { // washing machine int speed = tristate_square8(strip.now >> 7, 90, 15) / ((400 - SEGMENT.speed) >> 3); SEGENV.aux0 += speed; - if(speed == 0) SEGENV.aux0 = 190; //down (= 270°) + if (speed == 0) SEGENV.aux0 = 190; //down (= 270°) } else SEGENV.aux0 -= increment; @@ -8268,7 +8262,7 @@ uint16_t mode_particlebox(void) { ygravity = ((int32_t)(SEGMENT.custom1) * sin16_t(SEGENV.aux0 << 8)) / 0xFFFF; } if (SEGMENT.check3) { // sloshing, y force is always downwards - if(ygravity > 0) + if (ygravity > 0) ygravity = -ygravity; } @@ -8542,7 +8536,7 @@ uint16_t mode_particleattractor(void) { uint32_t strength = SEGMENT.speed; #ifdef USERMOD_AUDIOREACTIVE um_data_t *um_data; - if(UsermodManager::getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { + if (UsermodManager::getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { // AR active, do not use simulated data uint32_t volumeSmth = (uint32_t)(*(float*) um_data->u_data[0]); // 0-255 strength = (SEGMENT.speed * volumeSmth) >> 8; } @@ -8606,7 +8600,7 @@ uint16_t mode_particlespray(void) { #ifdef USERMOD_AUDIOREACTIVE um_data_t *um_data; - if (UsermodManager::getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { + if (UsermodManager::getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { // get AR data, do not use simulated data uint32_t volumeSmth = (uint8_t)(*(float*) um_data->u_data[0]); //0 to 255 uint32_t volumeRaw = *(int16_t*)um_data->u_data[1]; //0 to 255 PartSys->sources[0].minLife = 30; @@ -8675,10 +8669,7 @@ uint16_t mode_particleGEQ(void) { PartSys->setWallHardness(SEGMENT.custom2); PartSys->setGravity(SEGMENT.custom3 << 2); // set gravity strength - um_data_t *um_data; - if (!UsermodManager::getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) - um_data = simulateSound(SEGMENT.soundSim); // add support for no audio - + um_data_t *um_data = getAudioData(); uint8_t *fftResult = (uint8_t *)um_data->u_data[2]; // 16 bins with FFT data, log mapped already, each band contains frequency amplitude 0-255 //map the bands into 16 positions on x axis, emit some particles according to frequency loudness @@ -8695,7 +8686,7 @@ uint16_t mode_particleGEQ(void) { if (fftResult[bin] > threshold) { emitparticles = 1;// + (fftResult[bin]>>6); } - else if(fftResult[bin] > 0) { // band has low volue + else if (fftResult[bin] > 0) { // band has low volue uint32_t restvolume = ((threshold - fftResult[bin])>>2) + 2; if (hw_random16() % restvolume == 0) emitparticles = 1; @@ -8757,10 +8748,7 @@ uint16_t mode_particlecenterGEQ(void) { PartSys->updateSystem(); // update system properties (dimensions and data pointers) numSprays = min(PartSys->numSources, (uint32_t)NUMBEROFSOURCES); - um_data_t *um_data; - if (!UsermodManager::getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) - um_data = simulateSound(SEGMENT.soundSim); // add support for no audio - + um_data_t *um_data = getAudioData(); uint8_t *fftResult = (uint8_t *)um_data->u_data[2]; // 16 bins with FFT data, log mapped already, each band contains frequency amplitude 0-255 uint32_t threshold = 300 - SEGMENT.intensity; @@ -8823,15 +8811,15 @@ uint16_t mode_particleghostrider(void) { if (PartSys == NULL) return mode_static(); // something went wrong, no data! - if(SEGMENT.intensity > 0) { // spiraling - if(SEGENV.aux1) { + if (SEGMENT.intensity > 0) { // spiraling + if (SEGENV.aux1) { SEGENV.step += SEGMENT.intensity>>3; - if((int32_t)SEGENV.step > MAXANGLESTEP) + if ((int32_t)SEGENV.step > MAXANGLESTEP) SEGENV.aux1 = 0; } else { SEGENV.step -= SEGMENT.intensity>>3; - if((int32_t)SEGENV.step < -MAXANGLESTEP) + if ((int32_t)SEGENV.step < -MAXANGLESTEP) SEGENV.aux1 = 1; } } @@ -8841,8 +8829,8 @@ uint16_t mode_particleghostrider(void) { PartSys->sources[0].var = SEGMENT.custom3 >> 1; // color by age (PS 'color by age' always starts with hue = 255, don't want that here) - if(SEGMENT.check1) { - for(uint32_t i = 0; i < PartSys->usedParticles; i++) { + if (SEGMENT.check1) { + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { PartSys->particles[i].hue = PartSys->sources[0].source.hue + (PartSys->particles[i].ttl<<2); } } @@ -8938,7 +8926,7 @@ uint16_t mode_particleblobs(void) { #ifdef USERMOD_AUDIOREACTIVE um_data_t *um_data; - if (UsermodManager::getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { + if (UsermodManager::getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { // get AR data, do not use simulated data uint8_t volumeSmth = (uint8_t)(*(float*)um_data->u_data[0]); for (uint32_t i = 0; i < PartSys->usedParticles; i++) { // update particles if (SEGMENT.check3) //pulsate selected @@ -9026,7 +9014,7 @@ uint16_t mode_particleDrip(void) { if (SEGMENT.call % SEGENV.aux0 == 0) { int32_t interval = 300 / ((SEGMENT.intensity) + 1); SEGENV.aux0 = interval + hw_random(interval + 5); - // if(SEGMENT.check1) // rain mode + // if (SEGMENT.check1) // rain mode // PartSys->sources[0].source.hue = 0; // else PartSys->sources[0].source.hue = hw_random8(); //set random color TODO: maybe also not random but color cycling? need another slider or checkmark for this. @@ -9096,7 +9084,7 @@ uint16_t mode_particlePinball(void) { PartSys->setGravity(map(SEGMENT.custom3, 0 , 31, 0 , 16)); // set gravity (8 is default strength) PartSys->setBounce(SEGMENT.custom3); // disables bounce if no gravity is used PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur - PartSys->enableParticleCollisions(SEGMENT.check1, 254); // enable collisions and set particle collision to high hardness + PartSys->enableParticleCollisions(SEGMENT.check1, 255); // enable collisions and set particle collision to high hardness PartSys->setUsedParticles(SEGMENT.intensity); PartSys->setColorByPosition(SEGMENT.check3); @@ -9116,7 +9104,7 @@ uint16_t mode_particlePinball(void) { PartSys->particles[i].ttl = 260; // keep particles alive if (updateballs) { //speed changed or particle is dead, set particle properties PartSys->particleFlags[i].collide = true; - if(PartSys->particles[i].x == 0) { // still at initial position (when not switching from a PS) + if (PartSys->particles[i].x == 0) { // still at initial position (when not switching from a PS) PartSys->particles[i].x = hw_random16(PartSys->maxX); // random initial position for all particles PartSys->particles[i].vx = (hw_random16() & 0x01) ? 1 : -1; // random initial direction } @@ -9127,14 +9115,14 @@ uint16_t mode_particlePinball(void) { speedsum += abs(PartSys->particles[i].vx); } int32_t avgSpeed = speedsum / PartSys->usedParticles; - int32_t setSpeed = 3 + (SEGMENT.speed >> 2); + int32_t setSpeed = 2 + (SEGMENT.speed >> 3); if (avgSpeed < setSpeed) { // if balls are slow, speed up some of them at random to keep the animation going for (int i = 0; i < setSpeed - avgSpeed; i++) { int idx = hw_random16(PartSys->usedParticles); - PartSys->particles[idx].vx += PartSys->particles[idx].vx > 0 ? 1 : -1; // keep direction + PartSys->particles[idx].vx += PartSys->particles[idx].vx >= 0 ? 1 : -1; // add 1, keep direction } } - else if (avgSpeed > setSpeed + 2) // if avg speed is too high, apply friction to slow them down + else if (avgSpeed > setSpeed + 8) // if avg speed is too high, apply friction to slow them down PartSys->applyFriction(1); } else { //bouncing balls @@ -9148,7 +9136,7 @@ uint16_t mode_particlePinball(void) { PartSys->particles[i].ttl = 0; if (updateballs) { PartSys->advPartProps[i].size = SEGMENT.custom1; - if(SEGMENT.custom3 == 0) //gravity off, update speed + if (SEGMENT.custom3 == 0) //gravity off, update speed PartSys->particles[i].vx = PartSys->particles[i].vx > 0 ? newspeed : -newspeed; //keep the direction } } @@ -9212,18 +9200,18 @@ uint16_t mode_particleDancingShadows(void) { uint32_t deadparticles = 0; //kill out of bounds and moving away plus change color for (uint32_t i = 0; i < PartSys->usedParticles; i++) { - if(((SEGMENT.call & 0x07) == 0) && PartSys->particleFlags[i].outofbounds) { //check if out of bounds particle move away from strip, only update every 8th frame - if((int32_t)PartSys->particles[i].vx * PartSys->particles[i].x > 0) PartSys->particles[i].ttl = 0; //particle is moving away, kill it + if (((SEGMENT.call & 0x07) == 0) && PartSys->particleFlags[i].outofbounds) { //check if out of bounds particle move away from strip, only update every 8th frame + if ((int32_t)PartSys->particles[i].vx * PartSys->particles[i].x > 0) PartSys->particles[i].ttl = 0; //particle is moving away, kill it } PartSys->particleFlags[i].perpetual = true; //particles do not age if (SEGMENT.call % (32 / (1 + (SEGMENT.custom2 >> 3))) == 0) PartSys->particles[i].hue += 2 + (SEGMENT.custom2 >> 5); //note: updating speed on the fly is not accurately possible, since it is unknown which particles are assigned to which spot - if(SEGENV.aux0 != SEGMENT.speed) { //speed changed + if (SEGENV.aux0 != SEGMENT.speed) { //speed changed //update all particle speed by setting them to current value PartSys->particles[i].vx = PartSys->particles[i].vx > 0 ? SEGMENT.speed >> 3 : -SEGMENT.speed >> 3; } - if(PartSys->particles[i].ttl == 0) deadparticles++; // count dead particles + if (PartSys->particles[i].ttl == 0) deadparticles++; // count dead particles } SEGENV.aux0 = SEGMENT.speed; @@ -9246,7 +9234,7 @@ uint16_t mode_particleDancingShadows(void) { PartSys->sources[0].v = speed; //emitted particle speed PartSys->sources[0].source.hue = hw_random8(); //random spotlight color for (int32_t i = 0; i < width; i++) { - if(width > 1) { + if (width > 1) { switch (type) { case SPOT_TYPE_SOLID: //nothing to do @@ -9263,17 +9251,17 @@ uint16_t mode_particleDancingShadows(void) { break; case SPOT_TYPE_2X_DOT: - if(i > 0) position++; //skip one pixel + if (i > 0) position++; //skip one pixel i++; break; case SPOT_TYPE_3X_DOT: - if(i > 0) position += 2; //skip two pixels + if (i > 0) position += 2; //skip two pixels i+=2; break; case SPOT_TYPE_4X_DOT: - if(i > 0) position += 3; //skip three pixels + if (i > 0) position += 3; //skip three pixels i+=3; break; } @@ -9320,16 +9308,16 @@ uint16_t mode_particleFireworks1D(void) { PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur int32_t gravity = (1 + (SEGMENT.speed >> 3)); - if(!SEGMENT.check1) // gravity enabled for sparks + if (!SEGMENT.check1) // gravity enabled for sparks PartSys->setGravity(0); // disable else PartSys->setGravity(gravity); // set gravity - if(PartSys->sources[0].sourceFlags.custom1 == 1) { // rocket is on standby + if (PartSys->sources[0].sourceFlags.custom1 == 1) { // rocket is on standby PartSys->sources[0].source.ttl--; - if(PartSys->sources[0].source.ttl == 0) { // time is up, relaunch + if (PartSys->sources[0].source.ttl == 0) { // time is up, relaunch - if(hw_random8() < SEGMENT.custom1) // randomly choose direction according to slider, fire at start of segment if true + if (hw_random8() < SEGMENT.custom1) // randomly choose direction according to slider, fire at start of segment if true SEGENV.aux0 = 1; else SEGENV.aux0 = 0; @@ -9348,7 +9336,7 @@ uint16_t mode_particleFireworks1D(void) { PartSys->sources[0].size = 0; // default size PartSys->sources[0].sourceFlags.reversegrav = false ; // normal gravity - if(SEGENV.aux0) { // inverted rockets launch from end + if (SEGENV.aux0) { // inverted rockets launch from end PartSys->sources[0].sourceFlags.reversegrav = true; PartSys->sources[0].source.x = PartSys->maxX; // start from top PartSys->sources[0].source.vx = -PartSys->sources[0].source.vx; // revert direction @@ -9359,7 +9347,7 @@ uint16_t mode_particleFireworks1D(void) { else { // rocket is launched int32_t rocketgravity = -gravity; int32_t speed = PartSys->sources[0].source.vx; - if(SEGENV.aux0) { // negative speed rocket + if (SEGENV.aux0) { // negative speed rocket rocketgravity = -rocketgravity; speed = -speed; } @@ -9368,10 +9356,10 @@ uint16_t mode_particleFireworks1D(void) { PartSys->particleMoveUpdate(PartSys->sources[0].source, PartSys->sources[0].sourceFlags); // increase speed by calling the move function twice, also ages twice uint32_t rocketheight = SEGENV.aux0 ? PartSys->maxX - PartSys->sources[0].source.x : PartSys->sources[0].source.x; - if(speed < 0 && PartSys->sources[0].source.ttl > 50) // reached apogee + if (speed < 0 && PartSys->sources[0].source.ttl > 50) // reached apogee PartSys->sources[0].source.ttl = min((uint32_t)50, rocketheight >> (PS_P_RADIUS_SHIFT_1D + 3)); // alive for a few more frames - if(PartSys->sources[0].source.ttl < 2) { // explode + if (PartSys->sources[0].source.ttl < 2) { // explode PartSys->sources[0].sourceFlags.custom1 = 1; // set standby state PartSys->sources[0].var = 5 + ((((PartSys->maxX >> 1) + rocketheight) * (200 + SEGMENT.intensity)) / (PartSys->maxX << 2)); // set explosion particle speed PartSys->sources[0].minLife = 600; @@ -9381,22 +9369,22 @@ uint16_t mode_particleFireworks1D(void) { PartSys->sources[0].size = hw_random16(64); // random particle size in explosion uint32_t explosionsize = 8 + (PartSys->maxXpixel >> 2) + (PartSys->sources[0].source.x >> (PS_P_RADIUS_SHIFT_1D - 1)); explosionsize += hw_random16((explosionsize * SEGMENT.intensity) >> 8); - for(uint32_t e = 0; e < explosionsize; e++) { // emit explosion particles - if(SEGMENT.check2) + for (uint32_t e = 0; e < explosionsize; e++) { // emit explosion particles + if (SEGMENT.check2) PartSys->sources[0].source.hue = hw_random16(); //random color for each particle PartSys->sprayEmit(PartSys->sources[0]); // emit a particle } } } - if((SEGMENT.call & 0x01) == 0 && PartSys->sources[0].sourceFlags.custom1 == false) // every second frame and not in standby + if ((SEGMENT.call & 0x01) == 0 && PartSys->sources[0].sourceFlags.custom1 == false) // every second frame and not in standby PartSys->sprayEmit(PartSys->sources[0]); // emit exhaust particle - if((SEGMENT.call & 0x03) == 0) // every fourth frame + if ((SEGMENT.call & 0x03) == 0) // every fourth frame PartSys->applyFriction(1); // apply friction to all particles PartSys->update(); // update and render - for(uint32_t i = 0; i < PartSys->usedParticles; i++) { - if(PartSys->particles[i].ttl > 10) PartSys->particles[i].ttl -= 10; //ttl is linked to brightness, this allows to use higher brightness but still a short spark lifespan + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { + if (PartSys->particles[i].ttl > 10) PartSys->particles[i].ttl -= 10; //ttl is linked to brightness, this allows to use higher brightness but still a short spark lifespan else PartSys->particles[i].ttl = 0; } @@ -9439,13 +9427,13 @@ uint16_t mode_particleSparkler(void) { PartSys->sources[i].minLife = 150 + SEGMENT.intensity; PartSys->sources[i].maxLife = 250 + (SEGMENT.intensity << 1); uint32_t speed = SEGMENT.speed >> 1; - if(SEGMENT.check1) // sparks move (slide option) + if (SEGMENT.check1) // sparks move (slide option) PartSys->sources[i].var = SEGMENT.intensity >> 3; PartSys->sources[i].source.vx = speed; // update speed, do not change direction PartSys->sources[i].source.ttl = 400; // replenish its life (setting it perpetual uses more code) PartSys->sources[i].sat = SEGMENT.custom1; // color saturation PartSys->sources[i].size = SEGMENT.check3 ? 120 : 0; - if(SEGMENT.speed == 255) // random position at highest speed setting + if (SEGMENT.speed == 255) // random position at highest speed setting PartSys->sources[i].source.x = hw_random16(PartSys->maxX); else PartSys->particleMoveUpdate(PartSys->sources[i].source, PartSys->sources[i].sourceFlags, &sparklersettings); //move sparkler @@ -9467,8 +9455,8 @@ uint16_t mode_particleSparkler(void) { PartSys->update(); // update and render - for(uint32_t i = 0; i < PartSys->usedParticles; i++) { - if(PartSys->particles[i].ttl > (64 - (SEGMENT.intensity >> 2))) PartSys->particles[i].ttl -= (64 - (SEGMENT.intensity >> 2)); //ttl is linked to brightness, this allows to use higher brightness but still a short spark lifespan + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { + if (PartSys->particles[i].ttl > (64 - (SEGMENT.intensity >> 2))) PartSys->particles[i].ttl -= (64 - (SEGMENT.intensity >> 2)); //ttl is linked to brightness, this allows to use higher brightness but still a short spark lifespan else PartSys->particles[i].ttl = 0; } @@ -9483,15 +9471,14 @@ static const char _data_FX_MODE_PS_SPARKLER[] PROGMEM = "PS Sparkler@Move,!,Satu */ uint16_t mode_particleHourglass(void) { ParticleSystem1D *PartSys = NULL; - int32_t positionoffset; // resting position offset + constexpr int positionOffset = PS_P_RADIUS_1D / 2;; // resting position offset bool* direction; - uint8_t* basehue; + uint32_t* settingTracker; if (SEGMENT.call == 0) { // initialization - if (!initParticleSystem1D(PartSys, 0, 255, 2, false)) // init + if (!initParticleSystem1D(PartSys, 0, 255, 8, false)) // init return mode_static(); // allocation failed or is single pixel PartSys->setBounce(true); - PartSys->setWallHardness(80); - SEGENV.step = 0xFFFF; + PartSys->setWallHardness(100); } else PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS @@ -9500,107 +9487,116 @@ uint16_t mode_particleHourglass(void) { // Particle System settings PartSys->updateSystem(); // update system properties (dimensions and data pointers) - basehue = PartSys->PSdataEnd; //assign data pointer - direction = reinterpret_cast(PartSys->PSdataEnd + 1); //assign data pointer + settingTracker = reinterpret_cast(PartSys->PSdataEnd); //assign data pointer + direction = reinterpret_cast(PartSys->PSdataEnd + 4); //assign data pointer PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 1, 255)); PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur PartSys->setGravity(map(SEGMENT.custom3, 0, 31, 1, 30)); - PartSys->enableParticleCollisions(true, 34); // fixed hardness, 34 is a value that works best in most settings (spent a long time optimizing) + PartSys->enableParticleCollisions(true, 16); // hardness value found by experimentation on different settings - positionoffset = PS_P_RADIUS_1D / 2; uint32_t colormode = SEGMENT.custom1 >> 5; // 0-7 - if((SEGMENT.intensity | (PartSys->getAvailableParticles() << 8)) != SEGENV.step) { // initialize, getAvailableParticles changes while in FX transition - if(PartSys->getAvailableParticles() == SEGENV.step >> 8) // only intensity slider changed or first call - *basehue = hw_random16(); //choose new random color - SEGENV.step = SEGMENT.intensity | (PartSys->getAvailableParticles() << 8); - for(uint32_t i = 0; i < PartSys->usedParticles; i++) { + if ((SEGMENT.intensity | (PartSys->getAvailableParticles() << 8)) != *settingTracker) { // initialize, getAvailableParticles changes while in FX transition + *settingTracker = SEGMENT.intensity | (PartSys->getAvailableParticles() << 8); + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { PartSys->particleFlags[i].reversegrav = true; - *direction = 0; + *direction = 0; // down SEGENV.aux1 = 1; // initialize below } SEGENV.aux0 = PartSys->usedParticles - 1; // initial state, start with highest number particle } - for(uint32_t i = 0; i < PartSys->usedParticles; i++) { // check if particle reached target position after falling + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { // check if particle reached target position after falling int32_t targetposition; - if (PartSys->particleFlags[i].fixed == false) { + if (PartSys->particleFlags[i].fixed == false) { // && abs(PartSys->particles[i].vx) < 8) { // calculate target position depending on direction - if(PartSys->particleFlags[i].reversegrav) { - targetposition = PartSys->maxX - (i * PS_P_RADIUS_1D + positionoffset); // target resting position - if(PartSys->particles[i].x >= targetposition) // particle has reached target position, pin it. if not pinned, they do not stack well on larger piles - PartSys->particleFlags[i].fixed = true; + bool closeToTarget = false; + bool reachedTarget = false; + if (PartSys->particleFlags[i].reversegrav) { // up + targetposition = PartSys->maxX - (i * PS_P_RADIUS_1D) - positionOffset; // target resting position + if (targetposition - PartSys->particles[i].x <= 5 * PS_P_RADIUS_1D) + closeToTarget = true; + if (PartSys->particles[i].x >= targetposition) // particle has reached target position, pin it. if not pinned, they do not stack well on larger piles + reachedTarget = true; } - else { - targetposition = (PartSys->usedParticles - i) * PS_P_RADIUS_1D - positionoffset; // target resting position - if(PartSys->particles[i].x <= targetposition) // particle has reached target position, pin it. if not pinned, they do not stack well on larger piles - PartSys->particleFlags[i].fixed = true; + else { // down, highest index particle drops first + targetposition = (PartSys->usedParticles - i) * PS_P_RADIUS_1D - positionOffset; // target resting position note: using -offset instead of -1 + offset + if (PartSys->particles[i].x - targetposition <= 5 * PS_P_RADIUS_1D) + closeToTarget = true; + if (PartSys->particles[i].x <= targetposition) // particle has reached target position, pin it. if not pinned, they do not stack well on larger piles + reachedTarget = true; + } + if (reachedTarget || (closeToTarget && abs(PartSys->particles[i].vx) < 10)) { // reached target or close to target and slow speed + PartSys->particles[i].x = targetposition; // set exact position + PartSys->particleFlags[i].fixed = true; // pin particle } - } - if(colormode == 7) + if (colormode == 7) PartSys->setColorByPosition(true); // color fixed by position else { PartSys->setColorByPosition(false); + uint8_t basehue = ((SEGMENT.custom1 & 0x1F) << 3); // use 5 LSBs to select color switch(colormode) { case 0: PartSys->particles[i].hue = 120; break; // fixed at 120, if flip is activated, this can make red and green (use palette 34) - case 1: PartSys->particles[i].hue = *basehue; break; // fixed random color - case 2: - case 3: PartSys->particles[i].hue = *basehue + (i % colormode)*70; break; // interleved colors (every 2 or 3 particles) - case 4: PartSys->particles[i].hue = *basehue + (i * 255) / PartSys->usedParticles; break; // gradient palette colors - case 5: PartSys->particles[i].hue = *basehue + (i * 1024) / PartSys->usedParticles; break; // multi gradient palette colors + case 1: PartSys->particles[i].hue = basehue; break; // fixed selectable color + case 2: // 2 colors inverleaved (same code as 3) + case 3: PartSys->particles[i].hue = ((SEGMENT.custom1 & 0x1F) << 1) + (i % colormode)*74; break; // interleved colors (every 2 or 3 particles) + case 4: PartSys->particles[i].hue = basehue + (i * 255) / PartSys->usedParticles; break; // gradient palette colors + case 5: PartSys->particles[i].hue = basehue + (i * 1024) / PartSys->usedParticles; break; // multi gradient palette colors case 6: PartSys->particles[i].hue = i + (strip.now >> 3); break; // disco! moving color gradient default: break; } } - if(SEGMENT.check1 && !PartSys->particleFlags[i].reversegrav) // flip color when fallen + if (SEGMENT.check1 && !PartSys->particleFlags[i].reversegrav) // flip color when fallen PartSys->particles[i].hue += 120; } - if(SEGENV.aux1 == 1) { // last countdown call before dropping starts, reset all particles - for(uint32_t i = 0; i < PartSys->usedParticles; i++) { + if (SEGENV.aux1 == 1) { // last countdown call before dropping starts, reset all particles + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { PartSys->particleFlags[i].collide = true; PartSys->particleFlags[i].perpetual = true; PartSys->particles[i].ttl = 260; uint32_t targetposition; //calculate target position depending on direction - if(PartSys->particleFlags[i].reversegrav) - targetposition = PartSys->maxX - (i * PS_P_RADIUS_1D + positionoffset); // target resting position + if (PartSys->particleFlags[i].reversegrav) + targetposition = PartSys->maxX - (i * PS_P_RADIUS_1D + positionOffset); // target resting position else - targetposition = (PartSys->usedParticles - i) * PS_P_RADIUS_1D - positionoffset; // target resting position -5 - PS_P_RADIUS_1D/2 + targetposition = (PartSys->usedParticles - i) * PS_P_RADIUS_1D - positionOffset; // target resting position -5 - PS_P_RADIUS_1D/2 PartSys->particles[i].x = targetposition; PartSys->particleFlags[i].fixed = true; } } - if(SEGENV.aux1 == 0) { // countdown passed, run - uint32_t interval = 257 - SEGMENT.speed; // drop interval in frames, 1 second is 'speed = (257 - FPS)' speed = 0 is one drop every 257 frames - if(SEGMENT.check3 && *direction) // fast reset - interval = 3; - if(SEGMENT.call % interval == 0) { // drop a particle, do not drop more often than every second frame or particles tangle up quite badly - if(SEGENV.aux0 < PartSys->usedParticles) { + if (SEGENV.aux1 == 0) { // countdown passed, run + if (strip.now >= SEGENV.step) { // drop a particle, do not drop more often than every second frame or particles tangle up quite badly + // set next drop time + if (SEGMENT.check3 && *direction) // fast reset + SEGENV.step = strip.now + 100; // drop one particle every 100ms + else // normal interval + SEGENV.step = strip.now + max(20, SEGMENT.speed * 20); // map speed slider from 0.1s to 5s + if (SEGENV.aux0 < PartSys->usedParticles) { PartSys->particleFlags[SEGENV.aux0].reversegrav = *direction; // let this particle fall or rise PartSys->particleFlags[SEGENV.aux0].fixed = false; // unpin } - else { // overflow, flip direction - *direction = !(*direction); - SEGENV.aux1 = SEGMENT.virtualLength() + 100; // set countdown + else { // overflow + *direction = !(*direction); // flip direction + SEGENV.aux1 = SEGMENT.virtualLength() + 100; // set countdown } - if(*direction == 0) // down - SEGENV.aux0--; + if (*direction == 0) // down, start dropping the highest number particle + SEGENV.aux0--; // next particle else SEGENV.aux0++; } } - else if(SEGMENT.check2) // auto reset + else if (SEGMENT.check2) // auto reset SEGENV.aux1--; // countdown PartSys->update(); // update and render return FRAMETIME; } -static const char _data_FX_MODE_PS_HOURGLASS[] PROGMEM = "PS Hourglass@Speed,!,Color,Blur,Gravity,Colorflip,Start,Fast Reset;,!;!;1;pal=34,sx=245,ix=200,c1=140,c2=80,c3=4,o1=1,o2=1,o3=1"; +static const char _data_FX_MODE_PS_HOURGLASS[] PROGMEM = "PS Hourglass@Interval,!,Color,Blur,Gravity,Colorflip,Start,Fast Reset;,!;!;1;pal=34,sx=50,ix=200,c1=140,c2=80,c3=4,o1=1,o2=1,o3=1"; /* Particle based Spray effect (like a volcano, possible replacement for popcorn) @@ -9637,7 +9633,7 @@ uint16_t mode_particle1Dspray(void) { PartSys->sources[0].v = map(SEGMENT.speed, 0 , 255, -127 + PartSys->sources[0].var, 127 - PartSys->sources[0].var); // particle emit speed PartSys->sources[0].sourceFlags.reversegrav = gravity < 0 ? true : false; - if(hw_random() % (1 + ((255 - SEGMENT.intensity) >> 3)) == 0) { + if (hw_random() % (1 + ((255 - SEGMENT.intensity) >> 3)) == 0) { PartSys->sprayEmit(PartSys->sources[0]); // emit a particle SEGMENT.aux0++; // increment hue } @@ -9645,7 +9641,7 @@ uint16_t mode_particle1Dspray(void) { //update color settings PartSys->setColorByAge(SEGMENT.check1); // overruled by 'color by position' PartSys->setColorByPosition(SEGMENT.check3); - for(uint i = 0; i < PartSys->usedParticles; i++) { + for (uint i = 0; i < PartSys->usedParticles; i++) { PartSys->particleFlags[i].reversegrav = PartSys->sources[0].sourceFlags.reversegrav; // update gravity direction } PartSys->update(); // update and render @@ -9683,9 +9679,9 @@ uint16_t mode_particleBalance(void) { PartSys->setWrap(SEGMENT.check2); uint8_t hardness = SEGMENT.custom1 > 0 ? map(SEGMENT.custom1, 0, 255, 50, 250) : 200; // set hardness, make the walls hard if collisions are disabled PartSys->enableParticleCollisions(SEGMENT.custom1, hardness); // enable collisions if custom1 > 0 - PartSys->setWallHardness(hardness); + PartSys->setWallHardness(200); PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 10, 255)); - if(PartSys->usedParticles > SEGENV.aux1) { // more particles, reinitialize + if (PartSys->usedParticles > SEGENV.aux1) { // more particles, reinitialize for (i = 0; i < PartSys->usedParticles; i++) { PartSys->particles[i].x = i * PS_P_RADIUS_1D; PartSys->particles[i].ttl = 300; @@ -9699,7 +9695,7 @@ uint16_t mode_particleBalance(void) { int32_t xgravity; int32_t increment = (SEGMENT.speed >> 6) + 1; SEGENV.aux0 += increment; - if(SEGMENT.check3) // random, use perlin noise + if (SEGMENT.check3) // random, use perlin noise xgravity = ((int16_t)inoise8(SEGENV.aux0) - 128); else // sinusoidal xgravity = (int16_t)cos8(SEGENV.aux0) - 128;//((int32_t)(SEGMENT.custom3 << 2) * cos8(SEGENV.aux0) @@ -9711,14 +9707,14 @@ uint16_t mode_particleBalance(void) { uint32_t randomindex = hw_random16(PartSys->usedParticles); PartSys->particles[randomindex].vx = ((int32_t)PartSys->particles[randomindex].vx * 200) / 255; // apply friction to random particle to reduce clumping (without collisions) - //if(SEGMENT.check2 && (SEGMENT.call & 0x07) == 0) // no walls, apply friction to smooth things out - if((SEGMENT.call & 0x0F) == 0) // apply friction every 16th frame to smooth things out + //if (SEGMENT.check2 && (SEGMENT.call & 0x07) == 0) // no walls, apply friction to smooth things out + if ((SEGMENT.call & 0x0F) == 0 && SEGMENT.custom3 > 2) // apply friction every 16th frame to smooth things out (except for low tilt) PartSys->applyFriction(1); // apply friction to all particles //update colors PartSys->setColorByPosition(SEGMENT.check1); - if(!SEGMENT.check1) { - for(i = 0; i < PartSys->usedParticles; i++) { + if (!SEGMENT.check1) { + for (i = 0; i < PartSys->usedParticles; i++) { PartSys->particles[i].hue = (1024 * i) / PartSys->usedParticles; // color by particle index } } @@ -9894,10 +9890,7 @@ uint16_t mode_particle1DGEQ(void) { else PartSys->particles[i].ttl = 0; } - um_data_t *um_data; - if (!UsermodManager::getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) - um_data = simulateSound(SEGMENT.soundSim); // add support for no audio - + um_data_t *um_data = getAudioData(); uint8_t *fftResult = (uint8_t *)um_data->u_data[2]; // 16 bins with FFT data, log mapped already, each band contains frequency amplitude 0-255 //map the bands into 16 positions on x axis, emit some particles according to frequency loudness @@ -10027,41 +10020,38 @@ uint16_t mode_particle1Dsonicstream(void) { PartSys->sources[0].v = 5 + (SEGMENT.speed >> 2); // FFT processing - um_data_t *um_data; - if (!UsermodManager::getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) - um_data = simulateSound(SEGMENT.soundSim); // add support for no audio - + um_data_t *um_data = getAudioData(); uint8_t *fftResult = (uint8_t *)um_data->u_data[2]; // 16 bins with FFT data, log mapped already, each band contains frequency amplitude 0-255 uint32_t loudness; uint32_t baseBin = SEGMENT.custom3 >> 1; // 0 - 15 map(SEGMENT.custom3, 0, 31, 0, 14); loudness = fftResult[baseBin];// + fftResult[baseBin + 1]; int mids = sqrt16((int)fftResult[5] + (int)fftResult[6] + (int)fftResult[7] + (int)fftResult[8] + (int)fftResult[9] + (int)fftResult[10]); // average the mids, bin 5 is ~500Hz, bin 10 is ~2kHz (see audio_reactive.h) - if(baseBin > 12) + if (baseBin > 12) loudness = loudness << 2; // double loudness for high frequencies (better detecion) uint32_t threshold = 150 - (SEGMENT.intensity >> 1); - if(SEGMENT.check2) { // enable low pass filter for dynamic threshold + if (SEGMENT.check2) { // enable low pass filter for dynamic threshold SEGMENT.step = (SEGMENT.step * 31500 + loudness * (32768 - 31500)) >> 15; // low pass filter for simple beat detection: add average to base threshold threshold = 20 + (threshold >> 1) + SEGMENT.step; // add average to threshold } // color uint32_t hueincrement = (SEGMENT.custom1 >> 3); // 0-31 - if(SEGMENT.custom1 < 255) + if (SEGMENT.custom1 < 255) PartSys->setColorByPosition(false); else PartSys->setColorByPosition(true); // particle manipulation for (uint32_t i = 0; i < PartSys->usedParticles; i++) { - if(PartSys->sources[0].sourceFlags.perpetual == false) { // age faster if not perpetual + if (PartSys->sources[0].sourceFlags.perpetual == false) { // age faster if not perpetual if (PartSys->particles[i].ttl > 2) { PartSys->particles[i].ttl -= 2; //ttl is linked to brightness, this allows to use higher brightness but still a short lifespan } else PartSys->particles[i].ttl = 0; } - if(SEGMENT.check1) // modulate colors by mid frequencies + if (SEGMENT.check1) // modulate colors by mid frequencies PartSys->particles[i].hue += (mids * inoise8(PartSys->particles[i].x << 2, SEGMENT.step << 2)) >> 9; // color by perlin noise from mid frequencies } @@ -10071,20 +10061,20 @@ uint16_t mode_particle1Dsonicstream(void) { PartSys->sources[0].maxLife = PartSys->sources[0].minLife; PartSys->sources[0].source.hue = SEGMENT.aux0; PartSys->sources[0].size = SEGMENT.speed; - if(PartSys->particles[SEGMENT.aux1].x > 3 * PS_P_RADIUS_1D || PartSys->particles[SEGMENT.aux1].ttl == 0) { // only emit if last particle is far enough away or dead + if (PartSys->particles[SEGMENT.aux1].x > 3 * PS_P_RADIUS_1D || PartSys->particles[SEGMENT.aux1].ttl == 0) { // only emit if last particle is far enough away or dead int partindex = PartSys->sprayEmit(PartSys->sources[0]); // emit a particle - if(partindex >= 0) SEGMENT.aux1 = partindex; // track last emitted particle + if (partindex >= 0) SEGMENT.aux1 = partindex; // track last emitted particle } } else loudness = 0; // required for push mode PartSys->update(); // update and render (needs to be done before manipulation for initial particle spacing to be right) - if(SEGMENT.check3) { // push mode + if (SEGMENT.check3) { // push mode PartSys->sources[0].sourceFlags.perpetual = true; // emitted particles dont age PartSys->applyFriction(1); //slow down particles int32_t movestep = (((int)SEGMENT.speed + 2) * loudness) >> 10; - if(movestep) { + if (movestep) { for (uint32_t i = 0; i < PartSys->usedParticles; i++) { if (PartSys->particles[i].ttl) { PartSys->particles[i].x += movestep; // push particles @@ -10125,7 +10115,7 @@ uint8_t WS2812FX::addEffect(uint8_t id, mode_ptr mode_fn, const char *mode_name) _mode[id] = mode_fn; _modeData[id] = mode_name; return id; - } else if(_mode.size() < 255) { // 255 is reserved for indicating the effect wasn't added + } else if (_mode.size() < 255) { // 255 is reserved for indicating the effect wasn't added _mode.push_back(mode_fn); _modeData.push_back(mode_name); if (_modeCount < _mode.size()) _modeCount++; From 56fbf21a30f5e3934ed2071d7de80a88fb8b07e5 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Thu, 23 Jan 2025 16:28:05 +0100 Subject: [PATCH 200/219] revert whitespaces --- platformio.ini | 3 ++- wled00/cfg.cpp | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/platformio.ini b/platformio.ini index 65637a10a6..ef2a1daeab 100644 --- a/platformio.ini +++ b/platformio.ini @@ -15,7 +15,8 @@ default_envs = nodemcuv2, esp8266_2m, esp01_1m_full, nodemcuv2_160, esp8266_2m_1 src_dir = ./wled00 data_dir = ./wled00/data build_cache_dir = ~/.buildcache -extra_configs = platformio_override.ini +extra_configs = + platformio_override.ini [common] # ------------------------------------------------------------------------------ diff --git a/wled00/cfg.cpp b/wled00/cfg.cpp index 443f16c73b..72bedcbbbb 100644 --- a/wled00/cfg.cpp +++ b/wled00/cfg.cpp @@ -1240,4 +1240,4 @@ void serializeConfigSec() { if (f) serializeJson(root, f); f.close(); releaseJSONBufferLock(); -} +} \ No newline at end of file From f879bcc7ddaa351f7285e50ec9791b578d3dfba5 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Thu, 23 Jan 2025 16:30:01 +0100 Subject: [PATCH 201/219] simplified 1D collisions, improved binning for larger particles - ran a lot of experiments with collisions in 1D, the new much simpler approach seems to be a good compromise with regards to stacking and normal collisions. --- wled00/FXparticleSystem.cpp | 85 +++++++++++++++---------------------- wled00/FXparticleSystem.h | 2 +- 2 files changed, 36 insertions(+), 51 deletions(-) diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index ccfc5288e5..cd5774bcf7 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -830,11 +830,12 @@ void ParticleSystem2D::handleCollisions() { collDistSq = collDistSq * collDistSq; // square it for faster comparison (square is one operation) // note: partices are binned in x-axis, assumption is that no more than half of the particles are in the same bin // if they are, collisionStartIdx is increased so each particle collides at least every second frame (which still gives decent collisions) - int32_t binWidth = 6 * (PS_P_MINHARDRADIUS + particlesize); // width of a bin in sub-pixels + constexpr int BIN_WIDTH = 6 * PS_P_MINHARDRADIUS; // width of a bin in sub-pixels + int32_t overlap = PS_P_MINHARDRADIUS + particlesize; // overlap bins to include edge particles to neighbouring bins if (advPartProps) //may be using individual particle size - binWidth += 128; // add half of max radius + overlap += 128; // add max radius uint32_t maxBinParticles = max((uint32_t)50, (usedParticles + 1) / 2); // assume no more than half of the particles are in the same bin, do not bin small amounts of particles - uint32_t numBins = (maxX + (binWidth - 1)) / binWidth; // number of bins in x direction + uint32_t numBins = (maxX + (BIN_WIDTH - 1)) / BIN_WIDTH; // number of bins in x direction uint16_t binIndices[maxBinParticles]; // creat array on stack for indices, 2kB max for 1024 particles (ESP32_MAXPARTICLES/2) uint32_t binParticleCount; // number of particles in the current bin uint16_t nextFrameStartIdx = 0; // index of the first particle in the next frame (set if bin overflow) @@ -843,8 +844,8 @@ void ParticleSystem2D::handleCollisions() { // fill the binIndices array for this bin for (uint32_t bin = 0; bin < numBins; bin++) { binParticleCount = 0; // reset for this bin - int32_t binStart = bin * binWidth; - int32_t binEnd = binStart + binWidth; + int32_t binStart = bin * BIN_WIDTH - overlap; // note: first bin will extend to negative, but that is ok as out of bounds particles are ignored + int32_t binEnd = binStart + BIN_WIDTH + overlap; // note: last bin can be out of bounds, see above; // fill the binIndices array for this bin for (uint32_t i = 0; i < usedParticles; i++) { @@ -1437,7 +1438,6 @@ void ParticleSystem1D::applyGravity() { if (particleFlags[i].reversegrav) dv = -dv_raw; // note: not checking if particle is dead is omitted as most are usually alive and if few are alive, rendering is fast anyways particles[i].vx = limitSpeed((int32_t)particles[i].vx - dv); - particleFlags[i].forcedirection = particleFlags[i].reversegrav; // set force direction flag (for collisions) } } @@ -1449,7 +1449,6 @@ void ParticleSystem1D::applyGravity(PSparticle1D &part, PSparticleFlags1D &partF if (partFlags.reversegrav) dv = -dv; gforcecounter = counterbkp; //save it back part.vx = limitSpeed((int32_t)part.vx - dv); - partFlags.forcedirection = partFlags.reversegrav; // set force direction flag (for collisions) } @@ -1664,28 +1663,24 @@ void ParticleSystem1D::handleCollisions() { int32_t collisiondistance = PS_P_MINHARDRADIUS_1D; // note: partices are binned by position, assumption is that no more than half of the particles are in the same bin // if they are, collisionStartIdx is increased so each particle collides at least every second frame (which still gives decent collisions) - int32_t binWidth = 32 * (PS_P_MINHARDRADIUS_1D + particlesize); // width of each bin, a compromise between speed and accuracy (lareger bins are faster but collapse more) + constexpr int BIN_WIDTH = 32 * PS_P_MINHARDRADIUS_1D; // width of each bin, a compromise between speed and accuracy (lareger bins are faster but collapse more) + int32_t overlap = PS_P_MINHARDRADIUS_1D; // overlap bins to include edge particles to neighbouring bins + if (advPartProps) //may be using individual particle size + overlap += 128; // add max radius uint32_t maxBinParticles = max((uint32_t)50, (usedParticles + 1) / 4); // do not bin small amounts, limit max to 1/2 of particles - uint32_t numBins = (maxX + (binWidth - 1)) / binWidth; // calculate number of bins + uint32_t numBins = (maxX + (BIN_WIDTH - 1)) / BIN_WIDTH; // calculate number of bins uint16_t binIndices[maxBinParticles]; // array to store indices of particles in a bin uint32_t binParticleCount; // number of particles in the current bin uint16_t nextFrameStartIdx = 0; // index of the first particle in the next frame (set if bin overflow) uint32_t pidx = collisionStartIdx; //start index in case a bin is full, process remaining particles next frame for (uint32_t bin = 0; bin < numBins; bin++) { binParticleCount = 0; // reset for this bin - int32_t binStart = bin * binWidth; - int32_t binEnd = binStart + binWidth; + int32_t binStart = bin * BIN_WIDTH - overlap; // note: first bin will extend to negative, but that is ok as out of bounds particles are ignored + int32_t binEnd = binStart + BIN_WIDTH + overlap; // note: last bin can be out of bounds, see above // fill the binIndices array for this bin for (uint32_t i = 0; i < usedParticles; i++) { if (particles[pidx].ttl > 0 && particleFlags[pidx].outofbounds == 0 && particleFlags[pidx].collide) { // colliding particle - // if gravity is not used and wall bounce is enabled: particles in the first or last bin use fixed force direction (no collapsing, no push inversion) - if (!particlesettings.useGravity && particlesettings.bounce) { - if (particles[pidx].x < binWidth) - particleFlags[pidx].forcedirection = false; - else if (particles[pidx].x > (maxX - binWidth)) - particleFlags[pidx].forcedirection = true; - } if (particles[pidx].x >= binStart && particles[pidx].x <= binEnd) { // >= and <= to include particles on the edge of the bin (overlap to ensure boarder particles collide with adjacent bins) if (binParticleCount >= maxBinParticles) { // bin is full, more particles in this bin so do the rest next frame nextFrameStartIdx = pidx; // bin overflow can only happen once as bin size is at least half of the particles (or half +1) @@ -1710,7 +1705,7 @@ void ParticleSystem1D::handleCollisions() { int32_t proximity = collisiondistance; if (dv >= proximity) // particles would go past each other in next move update proximity += abs(dv); // add speed difference to catch fast particles - if (dx < proximity && dx > -proximity) { // check if close + if (dx <= proximity && dx >= -proximity) { // collide if close collideParticles(particles[idx_i], particleFlags[idx_i], particles[idx_j], particleFlags[idx_j], dx, dv, collisiondistance); } } @@ -1722,6 +1717,7 @@ void ParticleSystem1D::handleCollisions() { // takes two pointers to the particles to collide and the particle hardness (softer means more energy lost in collision, 255 means full hard) void ParticleSystem1D::collideParticles(PSparticle1D &particle1, const PSparticleFlags1D &particle1flags, PSparticle1D &particle2, const PSparticleFlags1D &particle2flags, int32_t dx, int32_t relativeVx, uint32_t collisiondistance) { int32_t dotProduct = (dx * relativeVx); // is always negative if moving towards each other + uint32_t distance = abs(dx); if (dotProduct < 0) { // particles are moving towards each other uint32_t surfacehardness = max(collisionHardness, (int32_t)PS_P_MINSURFACEHARDNESS_1D); // if particles are soft, the impulse must stay above a limit or collisions slip through // Calculate new velocities after collision @@ -1741,41 +1737,30 @@ void ParticleSystem1D::collideParticles(PSparticle1D &particle1, const PSparticl particle2.vx = ((int32_t)particle2.vx * coeff) / 255; } } - - uint32_t distance = abs(dx); - // particles have volume, push particles apart if they are too close - // behaviour is different than in 2D, we need pixel accurate stacking here, push the top particle to full radius (direction is well defined in 1D) - // also need to give the top particle some speed to counteract gravity or stacks just collapse - if (distance < collisiondistance) { // particles are too close, push the upper particle away - int32_t pushamount = 1 + ((collisiondistance - distance) >> 1); //add half the remaining distance note: this works best, if less or more is added, it gets more chaotic - - // Only force-push if particles use gravity or are not really close or are in the outer quarter of the strip - if (particlesettings.bounce && (particlesettings.useGravity || distance > 3 || particle1.x < (maxX >> 2) || particle1.x > (maxX - (maxX >> 2)))) { - // use force direction flag to push the 'upper' particle only, avoids stack-collapse - if (dx < 0) { // particle2.x < particle1.x, dx = p2.x - p1.x - if (particle2flags.forcedirection && !particle2flags.fixed) { - particle2.x -= pushamount; - particle2.vx--; - } else if (!particle1flags.forcedirection && !particle1flags.fixed) { - particle1.x += pushamount; - particle1.vx++; - } - } else { // particle1.x < particle2.x, dx = p2.x - p1.x - if (particle1flags.forcedirection && !particle1flags.fixed) { - particle1.x -= pushamount; - particle1.vx--; - } else if (!particle2flags.forcedirection && !particle2flags.fixed) { - particle2.x += pushamount; - particle2.vx++; - } - } - } - else { // no wall bounce, not using gravity, push both particles by applying a little velocity (like in 2D system) - pushamount = 2; + else if (distance < collisiondistance || relativeVx == 0) // moving apart or moving along and/or distance too close, push particles apart + { + // particles have volume, push particles apart if they are too close + // behaviour is different than in 2D, we need pixel accurate stacking here, push the top particle to full radius (direction is well defined in 1D) + int32_t pushamount = 1; if (dx < 0) // particle2.x < particle1.x pushamount = -pushamount; particle1.vx -= pushamount; particle2.vx += pushamount; + + if(distance < collisiondistance >> 1 ) { // too close, force push particles + pushamount = (collisiondistance - distance); + if(particle1.x < (maxX >> 1)) { // lower half, push particle with larger x in positive direction + if (dx < 0 && !particle1flags.fixed) // particle2.x < particle1.x -> push particle 1 + particle1.x += pushamount; + else if (!particle2flags.fixed) // particle1.x < particle2.x -> push particle 2 + particle2.x += pushamount; + } + else { // upper half, push particle with smaller x + if (dx < 0 && !particle2flags.fixed) // particle2.x < particle1.x -> push particle 2 + particle2.x -= pushamount; + else if (!particle2flags.fixed) // particle1.x < particle2.x -> push particle 1 + particle1.x -= pushamount; + } } } } diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index b9ad170c2f..8be4bfb917 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -301,7 +301,7 @@ typedef union { bool collide : 1; // if set, particle takes part in collisions bool perpetual : 1; // if set, particle does not age (TTL is not decremented in move function, it still dies from killoutofbounds) bool reversegrav : 1; // if set, gravity is reversed on this particle - bool forcedirection : 1; // direction the force was applied, 1 is positive x-direction (used for collision stacking, similar to reversegrav) + bool forcedirection : 1; // direction the force was applied, 1 is positive x-direction (used for collision stacking, similar to reversegrav) TODO: not used anymore, can be removed bool fixed : 1; // if set, particle does not move (and collisions make other particles revert direction), bool custom1 : 1; // unused custom flags, can be used by FX to track particle states bool custom2 : 1; From ba8c91b7171826042d574b367d03d3af393f0763 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Mon, 20 Jan 2025 20:12:55 +0100 Subject: [PATCH 202/219] Improved collision binning for large particles, improved pinball FX --- wled00/FX.cpp | 36 ++++++++++++++++++++++-------------- wled00/FXparticleSystem.cpp | 24 +++++++++++++----------- 2 files changed, 35 insertions(+), 25 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 4c0f737005..ea77ee93b3 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -9078,10 +9078,11 @@ uint16_t mode_particlePinball(void) { if (!initParticleSystem1D(PartSys, 1, 128, 0, true)) // init return mode_static(); // allocation failed or is single pixel PartSys->sources[0].sourceFlags.collide = true; // seeded particles will collide (if enabled) - PartSys->setKillOutOfBounds(true); // out of bounds particles dont return PartSys->sources[0].source.x = PS_P_RADIUS_1D; //emit at bottom + PartSys->setKillOutOfBounds(true); // out of bounds particles dont return + PartSys->setUsedParticles(255); // use all available particles for init SEGENV.aux0 = 1; - SEGENV.aux1 = 500; //set out of speed range to ensure uptate on first call + SEGENV.aux1 = 5000; //set out of range to ensure uptate on first call } else PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS @@ -9095,34 +9096,41 @@ uint16_t mode_particlePinball(void) { PartSys->setGravity(map(SEGMENT.custom3, 0 , 31, 0 , 16)); // set gravity (8 is default strength) PartSys->setBounce(SEGMENT.custom3); // disables bounce if no gravity is used PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur - PartSys->enableParticleCollisions(SEGMENT.check1, 254); // enable collisions and set particle collision hardness (do not use full hardness or particles speed up due to pushing, can not be made perfectly balanced) + PartSys->enableParticleCollisions(SEGMENT.check1, 254); // enable collisions and set particle collision to high hardness PartSys->setUsedParticles(SEGMENT.intensity); PartSys->setColorByPosition(SEGMENT.check3); bool updateballs = false; - if (SEGENV.aux1 != SEGMENT.speed + SEGMENT.intensity + SEGMENT.check2 + SEGMENT.custom1) { // user settings change + if (SEGENV.aux1 != SEGMENT.speed + SEGMENT.intensity + SEGMENT.check2 + SEGMENT.custom1 + PartSys->usedParticles) { // user settings change or more particles are available SEGENV.step = SEGMENT.call; // reset delay updateballs = true; - PartSys->sources[0].maxLife = SEGMENT.custom3 ? 1000 : 0xFFFF; // maximum lifetime in frames (very long if not using gravity, this is enough to travel 4000 pixels at min speed) + PartSys->sources[0].maxLife = SEGMENT.custom3 ? 5000 : 0xFFFF; // maximum lifetime in frames/2 (very long if not using gravity, this is enough to travel 4000 pixels at min speed) PartSys->sources[0].minLife = PartSys->sources[0].maxLife >> 1; } if (SEGMENT.check2) { //rolling balls PartSys->setGravity(0); PartSys->setWallHardness(255); - + int speedsum = 0; for (uint32_t i = 0; i < PartSys->usedParticles; i++) { - if ((PartSys->particles[i].vx > 8 || PartSys->particles[i].vx < -8) && PartSys->particles[i].ttl > 200) //let only slow particles die (ensures no stopped particles) - PartSys->particles[i].ttl = 260; //set alive at full intensity - if (updateballs || PartSys->particles[i].ttl == 0) { //speed changed or particle died, set particle properties - PartSys->particles[i].ttl = 260; + PartSys->particles[i].ttl = 260; // keep particles alive + if (updateballs) { //speed changed or particle is dead, set particle properties PartSys->particleFlags[i].collide = true; - int32_t newspeed = hw_random16(20 + (SEGMENT.speed >> 2)) + (SEGMENT.speed >> 3); + if(PartSys->particles[i].x == 0) { // still at initial position (when not switching from a PS) + PartSys->particles[i].x = hw_random16(PartSys->maxX); // random initial position for all particles + PartSys->particles[i].vx = (hw_random16() & 0x01) ? 1 : -1; // random initial direction + } + int8_t newspeed = 2 + hw_random16(SEGMENT.speed >> 2); PartSys->particles[i].vx = PartSys->particles[i].vx > 0 ? newspeed : -newspeed; //keep the direction PartSys->particles[i].hue = hw_random8(); //set ball colors to random PartSys->advPartProps[i].sat = 255; PartSys->advPartProps[i].size = SEGMENT.custom1; } + speedsum += abs(PartSys->particles[i].vx); + } + if(speedsum / PartSys->usedParticles < 3 + (SEGMENT.speed >> 2)) { // if balls are slow, speed up one of them at random to keep the animation going + int idx = hw_random16(PartSys->usedParticles); + PartSys->particles[idx].vx += PartSys->particles[idx].vx > 0 ? 2 : -2; // keep direction } } else { //bouncing balls @@ -9151,7 +9159,7 @@ uint16_t mode_particlePinball(void) { PartSys->sprayEmit(PartSys->sources[0]); } } - SEGENV.aux1 = SEGMENT.speed + SEGMENT.intensity + SEGMENT.check2 + SEGMENT.custom1; + SEGENV.aux1 = SEGMENT.speed + SEGMENT.intensity + SEGMENT.check2 + SEGMENT.custom1 + PartSys->usedParticles; for (uint32_t i = 0; i < PartSys->usedParticles; i++) { PartSys->particleMoveUpdate(PartSys->particles[i], PartSys->particleFlags[i]); // double the speed } @@ -9390,7 +9398,7 @@ uint16_t mode_particleFireworks1D(void) { return FRAMETIME; } -static const char _data_FX_MODE_PS_FIREWORKS1D[] PROGMEM = "PS Fireworks 1D@Gravity,Explosion,Firing side,Blur,Saturation,,Colorful,Smooth;,!;!;1;pal=0,sx=150,c2=30,c3=21,o2=1"; +static const char _data_FX_MODE_PS_FIREWORKS1D[] PROGMEM = "PS Fireworks 1D@Gravity,Explosion,Firing side,Blur,Saturation,,Colorful,Smooth;,!;!;1;sx=150,c2=30,c3=31,o2=1"; /* Particle based Sparkle effect @@ -9737,7 +9745,7 @@ uint16_t mode_particleChase(void) { // Particle System settings PartSys->updateSystem(); // update system properties (dimensions and data pointers) PartSys->setColorByPosition(SEGMENT.check3); - PartSys->setMotionBlur((SEGMENT.custom3 + 1) << 3); // anable motion blur + PartSys->setMotionBlur(8 + ((SEGMENT.custom3) << 3)); // anable motion blur // uint8_t* basehue = (PartSys->PSdataEnd + 2); //assign data pointer uint32_t settingssum = SEGMENT.speed + SEGMENT.intensity + SEGMENT.custom1 + SEGMENT.custom2 + SEGMENT.check1 + SEGMENT.check2 + SEGMENT.check3 + PartSys->getAvailableParticles(); // note: getAvailableParticles is used to enforce update during transitions diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index e2b307daa1..ccfc5288e5 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -830,19 +830,21 @@ void ParticleSystem2D::handleCollisions() { collDistSq = collDistSq * collDistSq; // square it for faster comparison (square is one operation) // note: partices are binned in x-axis, assumption is that no more than half of the particles are in the same bin // if they are, collisionStartIdx is increased so each particle collides at least every second frame (which still gives decent collisions) - constexpr int32_t BIN_WIDTH = 6 * PS_P_RADIUS; // width of a bin in sub-pixels + int32_t binWidth = 6 * (PS_P_MINHARDRADIUS + particlesize); // width of a bin in sub-pixels + if (advPartProps) //may be using individual particle size + binWidth += 128; // add half of max radius uint32_t maxBinParticles = max((uint32_t)50, (usedParticles + 1) / 2); // assume no more than half of the particles are in the same bin, do not bin small amounts of particles - uint32_t numBins = (maxX + (BIN_WIDTH - 1)) / BIN_WIDTH; // number of bins in x direction + uint32_t numBins = (maxX + (binWidth - 1)) / binWidth; // number of bins in x direction uint16_t binIndices[maxBinParticles]; // creat array on stack for indices, 2kB max for 1024 particles (ESP32_MAXPARTICLES/2) uint32_t binParticleCount; // number of particles in the current bin uint16_t nextFrameStartIdx = 0; // index of the first particle in the next frame (set if bin overflow) uint32_t pidx = collisionStartIdx; //start index in case a bin is full, process remaining particles next frame - + // fill the binIndices array for this bin for (uint32_t bin = 0; bin < numBins; bin++) { binParticleCount = 0; // reset for this bin - int32_t binStart = bin * BIN_WIDTH; - int32_t binEnd = binStart + BIN_WIDTH; + int32_t binStart = bin * binWidth; + int32_t binEnd = binStart + binWidth; // fill the binIndices array for this bin for (uint32_t i = 0; i < usedParticles; i++) { @@ -1662,26 +1664,26 @@ void ParticleSystem1D::handleCollisions() { int32_t collisiondistance = PS_P_MINHARDRADIUS_1D; // note: partices are binned by position, assumption is that no more than half of the particles are in the same bin // if they are, collisionStartIdx is increased so each particle collides at least every second frame (which still gives decent collisions) - constexpr int32_t BIN_WIDTH = 32 * PS_P_RADIUS_1D; // width of each bin, a compromise between speed and accuracy (lareger bins are faster but collapse more) + int32_t binWidth = 32 * (PS_P_MINHARDRADIUS_1D + particlesize); // width of each bin, a compromise between speed and accuracy (lareger bins are faster but collapse more) uint32_t maxBinParticles = max((uint32_t)50, (usedParticles + 1) / 4); // do not bin small amounts, limit max to 1/2 of particles - uint32_t numBins = (maxX + (BIN_WIDTH - 1)) / BIN_WIDTH; // calculate number of bins + uint32_t numBins = (maxX + (binWidth - 1)) / binWidth; // calculate number of bins uint16_t binIndices[maxBinParticles]; // array to store indices of particles in a bin uint32_t binParticleCount; // number of particles in the current bin uint16_t nextFrameStartIdx = 0; // index of the first particle in the next frame (set if bin overflow) uint32_t pidx = collisionStartIdx; //start index in case a bin is full, process remaining particles next frame for (uint32_t bin = 0; bin < numBins; bin++) { binParticleCount = 0; // reset for this bin - int32_t binStart = bin * BIN_WIDTH; - int32_t binEnd = binStart + BIN_WIDTH; + int32_t binStart = bin * binWidth; + int32_t binEnd = binStart + binWidth; // fill the binIndices array for this bin for (uint32_t i = 0; i < usedParticles; i++) { if (particles[pidx].ttl > 0 && particleFlags[pidx].outofbounds == 0 && particleFlags[pidx].collide) { // colliding particle // if gravity is not used and wall bounce is enabled: particles in the first or last bin use fixed force direction (no collapsing, no push inversion) if (!particlesettings.useGravity && particlesettings.bounce) { - if (particles[pidx].x < BIN_WIDTH) + if (particles[pidx].x < binWidth) particleFlags[pidx].forcedirection = false; - else if (particles[pidx].x > (maxX - BIN_WIDTH)) + else if (particles[pidx].x > (maxX - binWidth)) particleFlags[pidx].forcedirection = true; } if (particles[pidx].x >= binStart && particles[pidx].x <= binEnd) { // >= and <= to include particles on the edge of the bin (overlap to ensure boarder particles collide with adjacent bins) From b8f33857d0a68ec214729f381a8ee073ba68355a Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Tue, 21 Jan 2025 07:46:52 +0100 Subject: [PATCH 203/219] improved speed handling in pinball FX - rolling --- wled00/FX.cpp | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index ea77ee93b3..ab64999781 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -9120,18 +9120,22 @@ uint16_t mode_particlePinball(void) { PartSys->particles[i].x = hw_random16(PartSys->maxX); // random initial position for all particles PartSys->particles[i].vx = (hw_random16() & 0x01) ? 1 : -1; // random initial direction } - int8_t newspeed = 2 + hw_random16(SEGMENT.speed >> 2); - PartSys->particles[i].vx = PartSys->particles[i].vx > 0 ? newspeed : -newspeed; //keep the direction PartSys->particles[i].hue = hw_random8(); //set ball colors to random PartSys->advPartProps[i].sat = 255; PartSys->advPartProps[i].size = SEGMENT.custom1; } speedsum += abs(PartSys->particles[i].vx); } - if(speedsum / PartSys->usedParticles < 3 + (SEGMENT.speed >> 2)) { // if balls are slow, speed up one of them at random to keep the animation going - int idx = hw_random16(PartSys->usedParticles); - PartSys->particles[idx].vx += PartSys->particles[idx].vx > 0 ? 2 : -2; // keep direction + int32_t avgSpeed = speedsum / PartSys->usedParticles; + int32_t setSpeed = 3 + (SEGMENT.speed >> 2); + if (avgSpeed < setSpeed) { // if balls are slow, speed up some of them at random to keep the animation going + for (int i = 0; i < setSpeed - avgSpeed; i++) { + int idx = hw_random16(PartSys->usedParticles); + PartSys->particles[idx].vx += PartSys->particles[idx].vx > 0 ? 1 : -1; // keep direction + } } + else if (avgSpeed > setSpeed + 2) // if avg speed is too high, apply friction to slow them down + PartSys->applyFriction(1); } else { //bouncing balls PartSys->setWallHardness(220); From a312db710dba3b1560ff146958fe594632b8df0b Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Thu, 23 Jan 2025 16:27:47 +0100 Subject: [PATCH 204/219] improved hourglass: millis instead of frame timing, better stacking. and cleanup. --- wled00/FX.cpp | 298 ++++++++++++++++++++++++-------------------------- 1 file changed, 144 insertions(+), 154 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index ab64999781..a0709ea2e8 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -33,24 +33,18 @@ float FFT_MajorPeak = 1.0; uint8_t *fftResult = nullptr; float *fftBin = nullptr; - um_data_t *um_data; - if (UsermodManager::getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { - volumeSmth = *(float*) um_data->u_data[0]; - volumeRaw = *(float*) um_data->u_data[1]; - fftResult = (uint8_t*) um_data->u_data[2]; - samplePeak = *(uint8_t*) um_data->u_data[3]; - FFT_MajorPeak = *(float*) um_data->u_data[4]; - my_magnitude = *(float*) um_data->u_data[5]; - maxVol = (uint8_t*) um_data->u_data[6]; // requires UI element (SEGMENT.customX?), changes source element - binNum = (uint8_t*) um_data->u_data[7]; // requires UI element (SEGMENT.customX?), changes source element - fftBin = (float*) um_data->u_data[8]; - } else { - // add support for no audio data - um_data = simulateSound(SEGMENT.soundSim); - } + um_data_t *um_data = getAudioData(); + volumeSmth = *(float*) um_data->u_data[0]; + volumeRaw = *(float*) um_data->u_data[1]; + fftResult = (uint8_t*) um_data->u_data[2]; + samplePeak = *(uint8_t*) um_data->u_data[3]; + FFT_MajorPeak = *(float*) um_data->u_data[4]; + my_magnitude = *(float*) um_data->u_data[5]; + maxVol = (uint8_t*) um_data->u_data[6]; // requires UI element (SEGMENT.customX?), changes source element + binNum = (uint8_t*) um_data->u_data[7]; // requires UI element (SEGMENT.customX?), changes source element + fftBin = (float*) um_data->u_data[8]; */ - #define IBN 5100 // paletteBlend: 0 - wrap when moving, 1 - always wrap, 2 - never wrap, 3 - none (undefined) #define PALETTE_SOLID_WRAP (strip.paletteBlend == 1 || strip.paletteBlend == 3) @@ -711,7 +705,7 @@ static const char _data_FX_MODE_DISSOLVE_RANDOM[] PROGMEM = "Dissolve Rnd@Repeat * Inspired by www.tweaking4all.com/hardware/arduino/adruino-led-strip-effects/ */ uint16_t mode_sparkle(void) { - if (!SEGMENT.check2) for(unsigned i = 0; i < SEGLEN; i++) { + if (!SEGMENT.check2) for (unsigned i = 0; i < SEGLEN; i++) { SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 1)); } uint32_t cycleTime = 10 + (255 - SEGMENT.speed)*2; @@ -3244,7 +3238,7 @@ uint16_t mode_popcorn(void) { unsigned numPopcorn = SEGMENT.intensity * usablePopcorns / 255; if (numPopcorn == 0) numPopcorn = 1; - for(unsigned i = 0; i < numPopcorn; i++) { + for (unsigned i = 0; i < numPopcorn; i++) { if (popcorn[i].pos >= 0.0f) { // if kernel is active, update its position popcorn[i].pos += popcorn[i].vel; popcorn[i].vel += gravity; @@ -7671,7 +7665,7 @@ uint16_t mode_particlevortex(void) { #ifdef ESP8266 for (i = 1; i < 4; i++) { // need static particles in the center to reduce blinking (would be black every other frame without this hack), just set them there fixed int partindex = (int)PartSys->usedParticles - (int)i; - if(partindex >= 0) { + if (partindex >= 0) { PartSys->particles[partindex].x = (PartSys->maxX + 1) >> 1; // center PartSys->particles[partindex].y = (PartSys->maxY + 1) >> 1; // center PartSys->particles[partindex].sat = 230; @@ -7717,7 +7711,7 @@ uint16_t mode_particlevortex(void) { int32_t speedincrement = speeddiff / 50; if (speedincrement == 0) { //if speeddiff is not zero, make the increment at least 1 so it reaches target speed - if(speeddiff < 0) + if (speeddiff < 0) speedincrement = -1; else if (speeddiff > 0) speedincrement = 1; @@ -7783,7 +7777,7 @@ uint16_t mode_particlefireworks(void) { PartSys->setGravity(map(SEGMENT.custom3, 0, 31, SEGMENT.check2 ? 1 : 0, 10)); // if bounded, set gravity to minimum of 1 or they will bounce at top PartSys->setMotionBlur(map(SEGMENT.custom2, 0, 255, 0, 170)); // anable motion blur uint8_t smearing = 0; - if(SEGMENT.custom2 > 200) + if (SEGMENT.custom2 > 200) smearing = SEGMENT.custom2 - 200; PartSys->setSmearBlur(smearing); // enable 2D blurring (smearing) @@ -7883,7 +7877,7 @@ uint16_t mode_particlefireworks(void) { PartSys->sources[j].source.y = 1000; // reset position so gravity wont pull it to the ground and bounce it (vy MUST stay negative until relaunch) circularexplosion = false; // reset for next rocket } - if(SEGMENT.check3) { // fast speed, move particles twice + if (SEGMENT.check3) { // fast speed, move particles twice for (uint32_t i = 0; i < PartSys->usedParticles; i++) { PartSys->particleMoveUpdate(PartSys->particles[i], PartSys->particleFlags[i], nullptr, nullptr); } @@ -8232,7 +8226,7 @@ uint16_t mode_particlebox(void) { PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 5, 153)); // 2% - 60% // add in new particles if amount has changed for (i = 0; i < PartSys->usedParticles; i++) { - if(PartSys->particles[i].ttl < 260) { // initialize handed over particles and dead particles + if (PartSys->particles[i].ttl < 260) { // initialize handed over particles and dead particles PartSys->particles[i].ttl = 260; // full brigthness PartSys->particles[i].x = hw_random16(PartSys->maxX); PartSys->particles[i].y = hw_random16(PartSys->maxY); @@ -8251,7 +8245,7 @@ uint16_t mode_particlebox(void) { if (SEGMENT.check2) { // washing machine int speed = tristate_square8(strip.now >> 7, 90, 15) / ((400 - SEGMENT.speed) >> 3); SEGENV.aux0 += speed; - if(speed == 0) SEGENV.aux0 = 190; //down (= 270°) + if (speed == 0) SEGENV.aux0 = 190; //down (= 270°) } else SEGENV.aux0 -= increment; @@ -8268,7 +8262,7 @@ uint16_t mode_particlebox(void) { ygravity = ((int32_t)(SEGMENT.custom1) * sin16_t(SEGENV.aux0 << 8)) / 0xFFFF; } if (SEGMENT.check3) { // sloshing, y force is always downwards - if(ygravity > 0) + if (ygravity > 0) ygravity = -ygravity; } @@ -8542,7 +8536,7 @@ uint16_t mode_particleattractor(void) { uint32_t strength = SEGMENT.speed; #ifdef USERMOD_AUDIOREACTIVE um_data_t *um_data; - if(UsermodManager::getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { + if (UsermodManager::getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { // AR active, do not use simulated data uint32_t volumeSmth = (uint32_t)(*(float*) um_data->u_data[0]); // 0-255 strength = (SEGMENT.speed * volumeSmth) >> 8; } @@ -8606,7 +8600,7 @@ uint16_t mode_particlespray(void) { #ifdef USERMOD_AUDIOREACTIVE um_data_t *um_data; - if (UsermodManager::getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { + if (UsermodManager::getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { // get AR data, do not use simulated data uint32_t volumeSmth = (uint8_t)(*(float*) um_data->u_data[0]); //0 to 255 uint32_t volumeRaw = *(int16_t*)um_data->u_data[1]; //0 to 255 PartSys->sources[0].minLife = 30; @@ -8675,10 +8669,7 @@ uint16_t mode_particleGEQ(void) { PartSys->setWallHardness(SEGMENT.custom2); PartSys->setGravity(SEGMENT.custom3 << 2); // set gravity strength - um_data_t *um_data; - if (!UsermodManager::getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) - um_data = simulateSound(SEGMENT.soundSim); // add support for no audio - + um_data_t *um_data = getAudioData(); uint8_t *fftResult = (uint8_t *)um_data->u_data[2]; // 16 bins with FFT data, log mapped already, each band contains frequency amplitude 0-255 //map the bands into 16 positions on x axis, emit some particles according to frequency loudness @@ -8695,7 +8686,7 @@ uint16_t mode_particleGEQ(void) { if (fftResult[bin] > threshold) { emitparticles = 1;// + (fftResult[bin]>>6); } - else if(fftResult[bin] > 0) { // band has low volue + else if (fftResult[bin] > 0) { // band has low volue uint32_t restvolume = ((threshold - fftResult[bin])>>2) + 2; if (hw_random16() % restvolume == 0) emitparticles = 1; @@ -8757,10 +8748,7 @@ uint16_t mode_particlecenterGEQ(void) { PartSys->updateSystem(); // update system properties (dimensions and data pointers) numSprays = min(PartSys->numSources, (uint32_t)NUMBEROFSOURCES); - um_data_t *um_data; - if (!UsermodManager::getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) - um_data = simulateSound(SEGMENT.soundSim); // add support for no audio - + um_data_t *um_data = getAudioData(); uint8_t *fftResult = (uint8_t *)um_data->u_data[2]; // 16 bins with FFT data, log mapped already, each band contains frequency amplitude 0-255 uint32_t threshold = 300 - SEGMENT.intensity; @@ -8823,15 +8811,15 @@ uint16_t mode_particleghostrider(void) { if (PartSys == NULL) return mode_static(); // something went wrong, no data! - if(SEGMENT.intensity > 0) { // spiraling - if(SEGENV.aux1) { + if (SEGMENT.intensity > 0) { // spiraling + if (SEGENV.aux1) { SEGENV.step += SEGMENT.intensity>>3; - if((int32_t)SEGENV.step > MAXANGLESTEP) + if ((int32_t)SEGENV.step > MAXANGLESTEP) SEGENV.aux1 = 0; } else { SEGENV.step -= SEGMENT.intensity>>3; - if((int32_t)SEGENV.step < -MAXANGLESTEP) + if ((int32_t)SEGENV.step < -MAXANGLESTEP) SEGENV.aux1 = 1; } } @@ -8841,8 +8829,8 @@ uint16_t mode_particleghostrider(void) { PartSys->sources[0].var = SEGMENT.custom3 >> 1; // color by age (PS 'color by age' always starts with hue = 255, don't want that here) - if(SEGMENT.check1) { - for(uint32_t i = 0; i < PartSys->usedParticles; i++) { + if (SEGMENT.check1) { + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { PartSys->particles[i].hue = PartSys->sources[0].source.hue + (PartSys->particles[i].ttl<<2); } } @@ -8938,7 +8926,7 @@ uint16_t mode_particleblobs(void) { #ifdef USERMOD_AUDIOREACTIVE um_data_t *um_data; - if (UsermodManager::getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { + if (UsermodManager::getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { // get AR data, do not use simulated data uint8_t volumeSmth = (uint8_t)(*(float*)um_data->u_data[0]); for (uint32_t i = 0; i < PartSys->usedParticles; i++) { // update particles if (SEGMENT.check3) //pulsate selected @@ -9026,7 +9014,7 @@ uint16_t mode_particleDrip(void) { if (SEGMENT.call % SEGENV.aux0 == 0) { int32_t interval = 300 / ((SEGMENT.intensity) + 1); SEGENV.aux0 = interval + hw_random(interval + 5); - // if(SEGMENT.check1) // rain mode + // if (SEGMENT.check1) // rain mode // PartSys->sources[0].source.hue = 0; // else PartSys->sources[0].source.hue = hw_random8(); //set random color TODO: maybe also not random but color cycling? need another slider or checkmark for this. @@ -9096,7 +9084,7 @@ uint16_t mode_particlePinball(void) { PartSys->setGravity(map(SEGMENT.custom3, 0 , 31, 0 , 16)); // set gravity (8 is default strength) PartSys->setBounce(SEGMENT.custom3); // disables bounce if no gravity is used PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur - PartSys->enableParticleCollisions(SEGMENT.check1, 254); // enable collisions and set particle collision to high hardness + PartSys->enableParticleCollisions(SEGMENT.check1, 255); // enable collisions and set particle collision to high hardness PartSys->setUsedParticles(SEGMENT.intensity); PartSys->setColorByPosition(SEGMENT.check3); @@ -9116,7 +9104,7 @@ uint16_t mode_particlePinball(void) { PartSys->particles[i].ttl = 260; // keep particles alive if (updateballs) { //speed changed or particle is dead, set particle properties PartSys->particleFlags[i].collide = true; - if(PartSys->particles[i].x == 0) { // still at initial position (when not switching from a PS) + if (PartSys->particles[i].x == 0) { // still at initial position (when not switching from a PS) PartSys->particles[i].x = hw_random16(PartSys->maxX); // random initial position for all particles PartSys->particles[i].vx = (hw_random16() & 0x01) ? 1 : -1; // random initial direction } @@ -9127,14 +9115,14 @@ uint16_t mode_particlePinball(void) { speedsum += abs(PartSys->particles[i].vx); } int32_t avgSpeed = speedsum / PartSys->usedParticles; - int32_t setSpeed = 3 + (SEGMENT.speed >> 2); + int32_t setSpeed = 2 + (SEGMENT.speed >> 3); if (avgSpeed < setSpeed) { // if balls are slow, speed up some of them at random to keep the animation going for (int i = 0; i < setSpeed - avgSpeed; i++) { int idx = hw_random16(PartSys->usedParticles); - PartSys->particles[idx].vx += PartSys->particles[idx].vx > 0 ? 1 : -1; // keep direction + PartSys->particles[idx].vx += PartSys->particles[idx].vx >= 0 ? 1 : -1; // add 1, keep direction } } - else if (avgSpeed > setSpeed + 2) // if avg speed is too high, apply friction to slow them down + else if (avgSpeed > setSpeed + 8) // if avg speed is too high, apply friction to slow them down PartSys->applyFriction(1); } else { //bouncing balls @@ -9148,7 +9136,7 @@ uint16_t mode_particlePinball(void) { PartSys->particles[i].ttl = 0; if (updateballs) { PartSys->advPartProps[i].size = SEGMENT.custom1; - if(SEGMENT.custom3 == 0) //gravity off, update speed + if (SEGMENT.custom3 == 0) //gravity off, update speed PartSys->particles[i].vx = PartSys->particles[i].vx > 0 ? newspeed : -newspeed; //keep the direction } } @@ -9212,18 +9200,18 @@ uint16_t mode_particleDancingShadows(void) { uint32_t deadparticles = 0; //kill out of bounds and moving away plus change color for (uint32_t i = 0; i < PartSys->usedParticles; i++) { - if(((SEGMENT.call & 0x07) == 0) && PartSys->particleFlags[i].outofbounds) { //check if out of bounds particle move away from strip, only update every 8th frame - if((int32_t)PartSys->particles[i].vx * PartSys->particles[i].x > 0) PartSys->particles[i].ttl = 0; //particle is moving away, kill it + if (((SEGMENT.call & 0x07) == 0) && PartSys->particleFlags[i].outofbounds) { //check if out of bounds particle move away from strip, only update every 8th frame + if ((int32_t)PartSys->particles[i].vx * PartSys->particles[i].x > 0) PartSys->particles[i].ttl = 0; //particle is moving away, kill it } PartSys->particleFlags[i].perpetual = true; //particles do not age if (SEGMENT.call % (32 / (1 + (SEGMENT.custom2 >> 3))) == 0) PartSys->particles[i].hue += 2 + (SEGMENT.custom2 >> 5); //note: updating speed on the fly is not accurately possible, since it is unknown which particles are assigned to which spot - if(SEGENV.aux0 != SEGMENT.speed) { //speed changed + if (SEGENV.aux0 != SEGMENT.speed) { //speed changed //update all particle speed by setting them to current value PartSys->particles[i].vx = PartSys->particles[i].vx > 0 ? SEGMENT.speed >> 3 : -SEGMENT.speed >> 3; } - if(PartSys->particles[i].ttl == 0) deadparticles++; // count dead particles + if (PartSys->particles[i].ttl == 0) deadparticles++; // count dead particles } SEGENV.aux0 = SEGMENT.speed; @@ -9246,7 +9234,7 @@ uint16_t mode_particleDancingShadows(void) { PartSys->sources[0].v = speed; //emitted particle speed PartSys->sources[0].source.hue = hw_random8(); //random spotlight color for (int32_t i = 0; i < width; i++) { - if(width > 1) { + if (width > 1) { switch (type) { case SPOT_TYPE_SOLID: //nothing to do @@ -9263,17 +9251,17 @@ uint16_t mode_particleDancingShadows(void) { break; case SPOT_TYPE_2X_DOT: - if(i > 0) position++; //skip one pixel + if (i > 0) position++; //skip one pixel i++; break; case SPOT_TYPE_3X_DOT: - if(i > 0) position += 2; //skip two pixels + if (i > 0) position += 2; //skip two pixels i+=2; break; case SPOT_TYPE_4X_DOT: - if(i > 0) position += 3; //skip three pixels + if (i > 0) position += 3; //skip three pixels i+=3; break; } @@ -9320,16 +9308,16 @@ uint16_t mode_particleFireworks1D(void) { PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur int32_t gravity = (1 + (SEGMENT.speed >> 3)); - if(!SEGMENT.check1) // gravity enabled for sparks + if (!SEGMENT.check1) // gravity enabled for sparks PartSys->setGravity(0); // disable else PartSys->setGravity(gravity); // set gravity - if(PartSys->sources[0].sourceFlags.custom1 == 1) { // rocket is on standby + if (PartSys->sources[0].sourceFlags.custom1 == 1) { // rocket is on standby PartSys->sources[0].source.ttl--; - if(PartSys->sources[0].source.ttl == 0) { // time is up, relaunch + if (PartSys->sources[0].source.ttl == 0) { // time is up, relaunch - if(hw_random8() < SEGMENT.custom1) // randomly choose direction according to slider, fire at start of segment if true + if (hw_random8() < SEGMENT.custom1) // randomly choose direction according to slider, fire at start of segment if true SEGENV.aux0 = 1; else SEGENV.aux0 = 0; @@ -9348,7 +9336,7 @@ uint16_t mode_particleFireworks1D(void) { PartSys->sources[0].size = 0; // default size PartSys->sources[0].sourceFlags.reversegrav = false ; // normal gravity - if(SEGENV.aux0) { // inverted rockets launch from end + if (SEGENV.aux0) { // inverted rockets launch from end PartSys->sources[0].sourceFlags.reversegrav = true; PartSys->sources[0].source.x = PartSys->maxX; // start from top PartSys->sources[0].source.vx = -PartSys->sources[0].source.vx; // revert direction @@ -9359,7 +9347,7 @@ uint16_t mode_particleFireworks1D(void) { else { // rocket is launched int32_t rocketgravity = -gravity; int32_t speed = PartSys->sources[0].source.vx; - if(SEGENV.aux0) { // negative speed rocket + if (SEGENV.aux0) { // negative speed rocket rocketgravity = -rocketgravity; speed = -speed; } @@ -9368,10 +9356,10 @@ uint16_t mode_particleFireworks1D(void) { PartSys->particleMoveUpdate(PartSys->sources[0].source, PartSys->sources[0].sourceFlags); // increase speed by calling the move function twice, also ages twice uint32_t rocketheight = SEGENV.aux0 ? PartSys->maxX - PartSys->sources[0].source.x : PartSys->sources[0].source.x; - if(speed < 0 && PartSys->sources[0].source.ttl > 50) // reached apogee + if (speed < 0 && PartSys->sources[0].source.ttl > 50) // reached apogee PartSys->sources[0].source.ttl = min((uint32_t)50, rocketheight >> (PS_P_RADIUS_SHIFT_1D + 3)); // alive for a few more frames - if(PartSys->sources[0].source.ttl < 2) { // explode + if (PartSys->sources[0].source.ttl < 2) { // explode PartSys->sources[0].sourceFlags.custom1 = 1; // set standby state PartSys->sources[0].var = 5 + ((((PartSys->maxX >> 1) + rocketheight) * (200 + SEGMENT.intensity)) / (PartSys->maxX << 2)); // set explosion particle speed PartSys->sources[0].minLife = 600; @@ -9381,22 +9369,22 @@ uint16_t mode_particleFireworks1D(void) { PartSys->sources[0].size = hw_random16(64); // random particle size in explosion uint32_t explosionsize = 8 + (PartSys->maxXpixel >> 2) + (PartSys->sources[0].source.x >> (PS_P_RADIUS_SHIFT_1D - 1)); explosionsize += hw_random16((explosionsize * SEGMENT.intensity) >> 8); - for(uint32_t e = 0; e < explosionsize; e++) { // emit explosion particles - if(SEGMENT.check2) + for (uint32_t e = 0; e < explosionsize; e++) { // emit explosion particles + if (SEGMENT.check2) PartSys->sources[0].source.hue = hw_random16(); //random color for each particle PartSys->sprayEmit(PartSys->sources[0]); // emit a particle } } } - if((SEGMENT.call & 0x01) == 0 && PartSys->sources[0].sourceFlags.custom1 == false) // every second frame and not in standby + if ((SEGMENT.call & 0x01) == 0 && PartSys->sources[0].sourceFlags.custom1 == false) // every second frame and not in standby PartSys->sprayEmit(PartSys->sources[0]); // emit exhaust particle - if((SEGMENT.call & 0x03) == 0) // every fourth frame + if ((SEGMENT.call & 0x03) == 0) // every fourth frame PartSys->applyFriction(1); // apply friction to all particles PartSys->update(); // update and render - for(uint32_t i = 0; i < PartSys->usedParticles; i++) { - if(PartSys->particles[i].ttl > 10) PartSys->particles[i].ttl -= 10; //ttl is linked to brightness, this allows to use higher brightness but still a short spark lifespan + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { + if (PartSys->particles[i].ttl > 10) PartSys->particles[i].ttl -= 10; //ttl is linked to brightness, this allows to use higher brightness but still a short spark lifespan else PartSys->particles[i].ttl = 0; } @@ -9439,13 +9427,13 @@ uint16_t mode_particleSparkler(void) { PartSys->sources[i].minLife = 150 + SEGMENT.intensity; PartSys->sources[i].maxLife = 250 + (SEGMENT.intensity << 1); uint32_t speed = SEGMENT.speed >> 1; - if(SEGMENT.check1) // sparks move (slide option) + if (SEGMENT.check1) // sparks move (slide option) PartSys->sources[i].var = SEGMENT.intensity >> 3; PartSys->sources[i].source.vx = speed; // update speed, do not change direction PartSys->sources[i].source.ttl = 400; // replenish its life (setting it perpetual uses more code) PartSys->sources[i].sat = SEGMENT.custom1; // color saturation PartSys->sources[i].size = SEGMENT.check3 ? 120 : 0; - if(SEGMENT.speed == 255) // random position at highest speed setting + if (SEGMENT.speed == 255) // random position at highest speed setting PartSys->sources[i].source.x = hw_random16(PartSys->maxX); else PartSys->particleMoveUpdate(PartSys->sources[i].source, PartSys->sources[i].sourceFlags, &sparklersettings); //move sparkler @@ -9467,8 +9455,8 @@ uint16_t mode_particleSparkler(void) { PartSys->update(); // update and render - for(uint32_t i = 0; i < PartSys->usedParticles; i++) { - if(PartSys->particles[i].ttl > (64 - (SEGMENT.intensity >> 2))) PartSys->particles[i].ttl -= (64 - (SEGMENT.intensity >> 2)); //ttl is linked to brightness, this allows to use higher brightness but still a short spark lifespan + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { + if (PartSys->particles[i].ttl > (64 - (SEGMENT.intensity >> 2))) PartSys->particles[i].ttl -= (64 - (SEGMENT.intensity >> 2)); //ttl is linked to brightness, this allows to use higher brightness but still a short spark lifespan else PartSys->particles[i].ttl = 0; } @@ -9483,15 +9471,14 @@ static const char _data_FX_MODE_PS_SPARKLER[] PROGMEM = "PS Sparkler@Move,!,Satu */ uint16_t mode_particleHourglass(void) { ParticleSystem1D *PartSys = NULL; - int32_t positionoffset; // resting position offset + constexpr int positionOffset = PS_P_RADIUS_1D / 2;; // resting position offset bool* direction; - uint8_t* basehue; + uint32_t* settingTracker; if (SEGMENT.call == 0) { // initialization - if (!initParticleSystem1D(PartSys, 0, 255, 2, false)) // init + if (!initParticleSystem1D(PartSys, 0, 255, 8, false)) // init return mode_static(); // allocation failed or is single pixel PartSys->setBounce(true); - PartSys->setWallHardness(80); - SEGENV.step = 0xFFFF; + PartSys->setWallHardness(100); } else PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS @@ -9500,107 +9487,116 @@ uint16_t mode_particleHourglass(void) { // Particle System settings PartSys->updateSystem(); // update system properties (dimensions and data pointers) - basehue = PartSys->PSdataEnd; //assign data pointer - direction = reinterpret_cast(PartSys->PSdataEnd + 1); //assign data pointer + settingTracker = reinterpret_cast(PartSys->PSdataEnd); //assign data pointer + direction = reinterpret_cast(PartSys->PSdataEnd + 4); //assign data pointer PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 1, 255)); PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur PartSys->setGravity(map(SEGMENT.custom3, 0, 31, 1, 30)); - PartSys->enableParticleCollisions(true, 34); // fixed hardness, 34 is a value that works best in most settings (spent a long time optimizing) + PartSys->enableParticleCollisions(true, 16); // hardness value found by experimentation on different settings - positionoffset = PS_P_RADIUS_1D / 2; uint32_t colormode = SEGMENT.custom1 >> 5; // 0-7 - if((SEGMENT.intensity | (PartSys->getAvailableParticles() << 8)) != SEGENV.step) { // initialize, getAvailableParticles changes while in FX transition - if(PartSys->getAvailableParticles() == SEGENV.step >> 8) // only intensity slider changed or first call - *basehue = hw_random16(); //choose new random color - SEGENV.step = SEGMENT.intensity | (PartSys->getAvailableParticles() << 8); - for(uint32_t i = 0; i < PartSys->usedParticles; i++) { + if ((SEGMENT.intensity | (PartSys->getAvailableParticles() << 8)) != *settingTracker) { // initialize, getAvailableParticles changes while in FX transition + *settingTracker = SEGMENT.intensity | (PartSys->getAvailableParticles() << 8); + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { PartSys->particleFlags[i].reversegrav = true; - *direction = 0; + *direction = 0; // down SEGENV.aux1 = 1; // initialize below } SEGENV.aux0 = PartSys->usedParticles - 1; // initial state, start with highest number particle } - for(uint32_t i = 0; i < PartSys->usedParticles; i++) { // check if particle reached target position after falling + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { // check if particle reached target position after falling int32_t targetposition; - if (PartSys->particleFlags[i].fixed == false) { + if (PartSys->particleFlags[i].fixed == false) { // && abs(PartSys->particles[i].vx) < 8) { // calculate target position depending on direction - if(PartSys->particleFlags[i].reversegrav) { - targetposition = PartSys->maxX - (i * PS_P_RADIUS_1D + positionoffset); // target resting position - if(PartSys->particles[i].x >= targetposition) // particle has reached target position, pin it. if not pinned, they do not stack well on larger piles - PartSys->particleFlags[i].fixed = true; + bool closeToTarget = false; + bool reachedTarget = false; + if (PartSys->particleFlags[i].reversegrav) { // up + targetposition = PartSys->maxX - (i * PS_P_RADIUS_1D) - positionOffset; // target resting position + if (targetposition - PartSys->particles[i].x <= 5 * PS_P_RADIUS_1D) + closeToTarget = true; + if (PartSys->particles[i].x >= targetposition) // particle has reached target position, pin it. if not pinned, they do not stack well on larger piles + reachedTarget = true; } - else { - targetposition = (PartSys->usedParticles - i) * PS_P_RADIUS_1D - positionoffset; // target resting position - if(PartSys->particles[i].x <= targetposition) // particle has reached target position, pin it. if not pinned, they do not stack well on larger piles - PartSys->particleFlags[i].fixed = true; + else { // down, highest index particle drops first + targetposition = (PartSys->usedParticles - i) * PS_P_RADIUS_1D - positionOffset; // target resting position note: using -offset instead of -1 + offset + if (PartSys->particles[i].x - targetposition <= 5 * PS_P_RADIUS_1D) + closeToTarget = true; + if (PartSys->particles[i].x <= targetposition) // particle has reached target position, pin it. if not pinned, they do not stack well on larger piles + reachedTarget = true; + } + if (reachedTarget || (closeToTarget && abs(PartSys->particles[i].vx) < 10)) { // reached target or close to target and slow speed + PartSys->particles[i].x = targetposition; // set exact position + PartSys->particleFlags[i].fixed = true; // pin particle } - } - if(colormode == 7) + if (colormode == 7) PartSys->setColorByPosition(true); // color fixed by position else { PartSys->setColorByPosition(false); + uint8_t basehue = ((SEGMENT.custom1 & 0x1F) << 3); // use 5 LSBs to select color switch(colormode) { case 0: PartSys->particles[i].hue = 120; break; // fixed at 120, if flip is activated, this can make red and green (use palette 34) - case 1: PartSys->particles[i].hue = *basehue; break; // fixed random color - case 2: - case 3: PartSys->particles[i].hue = *basehue + (i % colormode)*70; break; // interleved colors (every 2 or 3 particles) - case 4: PartSys->particles[i].hue = *basehue + (i * 255) / PartSys->usedParticles; break; // gradient palette colors - case 5: PartSys->particles[i].hue = *basehue + (i * 1024) / PartSys->usedParticles; break; // multi gradient palette colors + case 1: PartSys->particles[i].hue = basehue; break; // fixed selectable color + case 2: // 2 colors inverleaved (same code as 3) + case 3: PartSys->particles[i].hue = ((SEGMENT.custom1 & 0x1F) << 1) + (i % colormode)*74; break; // interleved colors (every 2 or 3 particles) + case 4: PartSys->particles[i].hue = basehue + (i * 255) / PartSys->usedParticles; break; // gradient palette colors + case 5: PartSys->particles[i].hue = basehue + (i * 1024) / PartSys->usedParticles; break; // multi gradient palette colors case 6: PartSys->particles[i].hue = i + (strip.now >> 3); break; // disco! moving color gradient default: break; } } - if(SEGMENT.check1 && !PartSys->particleFlags[i].reversegrav) // flip color when fallen + if (SEGMENT.check1 && !PartSys->particleFlags[i].reversegrav) // flip color when fallen PartSys->particles[i].hue += 120; } - if(SEGENV.aux1 == 1) { // last countdown call before dropping starts, reset all particles - for(uint32_t i = 0; i < PartSys->usedParticles; i++) { + if (SEGENV.aux1 == 1) { // last countdown call before dropping starts, reset all particles + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { PartSys->particleFlags[i].collide = true; PartSys->particleFlags[i].perpetual = true; PartSys->particles[i].ttl = 260; uint32_t targetposition; //calculate target position depending on direction - if(PartSys->particleFlags[i].reversegrav) - targetposition = PartSys->maxX - (i * PS_P_RADIUS_1D + positionoffset); // target resting position + if (PartSys->particleFlags[i].reversegrav) + targetposition = PartSys->maxX - (i * PS_P_RADIUS_1D + positionOffset); // target resting position else - targetposition = (PartSys->usedParticles - i) * PS_P_RADIUS_1D - positionoffset; // target resting position -5 - PS_P_RADIUS_1D/2 + targetposition = (PartSys->usedParticles - i) * PS_P_RADIUS_1D - positionOffset; // target resting position -5 - PS_P_RADIUS_1D/2 PartSys->particles[i].x = targetposition; PartSys->particleFlags[i].fixed = true; } } - if(SEGENV.aux1 == 0) { // countdown passed, run - uint32_t interval = 257 - SEGMENT.speed; // drop interval in frames, 1 second is 'speed = (257 - FPS)' speed = 0 is one drop every 257 frames - if(SEGMENT.check3 && *direction) // fast reset - interval = 3; - if(SEGMENT.call % interval == 0) { // drop a particle, do not drop more often than every second frame or particles tangle up quite badly - if(SEGENV.aux0 < PartSys->usedParticles) { + if (SEGENV.aux1 == 0) { // countdown passed, run + if (strip.now >= SEGENV.step) { // drop a particle, do not drop more often than every second frame or particles tangle up quite badly + // set next drop time + if (SEGMENT.check3 && *direction) // fast reset + SEGENV.step = strip.now + 100; // drop one particle every 100ms + else // normal interval + SEGENV.step = strip.now + max(20, SEGMENT.speed * 20); // map speed slider from 0.1s to 5s + if (SEGENV.aux0 < PartSys->usedParticles) { PartSys->particleFlags[SEGENV.aux0].reversegrav = *direction; // let this particle fall or rise PartSys->particleFlags[SEGENV.aux0].fixed = false; // unpin } - else { // overflow, flip direction - *direction = !(*direction); - SEGENV.aux1 = SEGMENT.virtualLength() + 100; // set countdown + else { // overflow + *direction = !(*direction); // flip direction + SEGENV.aux1 = SEGMENT.virtualLength() + 100; // set countdown } - if(*direction == 0) // down - SEGENV.aux0--; + if (*direction == 0) // down, start dropping the highest number particle + SEGENV.aux0--; // next particle else SEGENV.aux0++; } } - else if(SEGMENT.check2) // auto reset + else if (SEGMENT.check2) // auto reset SEGENV.aux1--; // countdown PartSys->update(); // update and render return FRAMETIME; } -static const char _data_FX_MODE_PS_HOURGLASS[] PROGMEM = "PS Hourglass@Speed,!,Color,Blur,Gravity,Colorflip,Start,Fast Reset;,!;!;1;pal=34,sx=245,ix=200,c1=140,c2=80,c3=4,o1=1,o2=1,o3=1"; +static const char _data_FX_MODE_PS_HOURGLASS[] PROGMEM = "PS Hourglass@Interval,!,Color,Blur,Gravity,Colorflip,Start,Fast Reset;,!;!;1;pal=34,sx=50,ix=200,c1=140,c2=80,c3=4,o1=1,o2=1,o3=1"; /* Particle based Spray effect (like a volcano, possible replacement for popcorn) @@ -9637,7 +9633,7 @@ uint16_t mode_particle1Dspray(void) { PartSys->sources[0].v = map(SEGMENT.speed, 0 , 255, -127 + PartSys->sources[0].var, 127 - PartSys->sources[0].var); // particle emit speed PartSys->sources[0].sourceFlags.reversegrav = gravity < 0 ? true : false; - if(hw_random() % (1 + ((255 - SEGMENT.intensity) >> 3)) == 0) { + if (hw_random() % (1 + ((255 - SEGMENT.intensity) >> 3)) == 0) { PartSys->sprayEmit(PartSys->sources[0]); // emit a particle SEGMENT.aux0++; // increment hue } @@ -9645,7 +9641,7 @@ uint16_t mode_particle1Dspray(void) { //update color settings PartSys->setColorByAge(SEGMENT.check1); // overruled by 'color by position' PartSys->setColorByPosition(SEGMENT.check3); - for(uint i = 0; i < PartSys->usedParticles; i++) { + for (uint i = 0; i < PartSys->usedParticles; i++) { PartSys->particleFlags[i].reversegrav = PartSys->sources[0].sourceFlags.reversegrav; // update gravity direction } PartSys->update(); // update and render @@ -9683,9 +9679,9 @@ uint16_t mode_particleBalance(void) { PartSys->setWrap(SEGMENT.check2); uint8_t hardness = SEGMENT.custom1 > 0 ? map(SEGMENT.custom1, 0, 255, 50, 250) : 200; // set hardness, make the walls hard if collisions are disabled PartSys->enableParticleCollisions(SEGMENT.custom1, hardness); // enable collisions if custom1 > 0 - PartSys->setWallHardness(hardness); + PartSys->setWallHardness(200); PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 10, 255)); - if(PartSys->usedParticles > SEGENV.aux1) { // more particles, reinitialize + if (PartSys->usedParticles > SEGENV.aux1) { // more particles, reinitialize for (i = 0; i < PartSys->usedParticles; i++) { PartSys->particles[i].x = i * PS_P_RADIUS_1D; PartSys->particles[i].ttl = 300; @@ -9699,7 +9695,7 @@ uint16_t mode_particleBalance(void) { int32_t xgravity; int32_t increment = (SEGMENT.speed >> 6) + 1; SEGENV.aux0 += increment; - if(SEGMENT.check3) // random, use perlin noise + if (SEGMENT.check3) // random, use perlin noise xgravity = ((int16_t)inoise8(SEGENV.aux0) - 128); else // sinusoidal xgravity = (int16_t)cos8(SEGENV.aux0) - 128;//((int32_t)(SEGMENT.custom3 << 2) * cos8(SEGENV.aux0) @@ -9711,14 +9707,14 @@ uint16_t mode_particleBalance(void) { uint32_t randomindex = hw_random16(PartSys->usedParticles); PartSys->particles[randomindex].vx = ((int32_t)PartSys->particles[randomindex].vx * 200) / 255; // apply friction to random particle to reduce clumping (without collisions) - //if(SEGMENT.check2 && (SEGMENT.call & 0x07) == 0) // no walls, apply friction to smooth things out - if((SEGMENT.call & 0x0F) == 0) // apply friction every 16th frame to smooth things out + //if (SEGMENT.check2 && (SEGMENT.call & 0x07) == 0) // no walls, apply friction to smooth things out + if ((SEGMENT.call & 0x0F) == 0 && SEGMENT.custom3 > 2) // apply friction every 16th frame to smooth things out (except for low tilt) PartSys->applyFriction(1); // apply friction to all particles //update colors PartSys->setColorByPosition(SEGMENT.check1); - if(!SEGMENT.check1) { - for(i = 0; i < PartSys->usedParticles; i++) { + if (!SEGMENT.check1) { + for (i = 0; i < PartSys->usedParticles; i++) { PartSys->particles[i].hue = (1024 * i) / PartSys->usedParticles; // color by particle index } } @@ -9894,10 +9890,7 @@ uint16_t mode_particle1DGEQ(void) { else PartSys->particles[i].ttl = 0; } - um_data_t *um_data; - if (!UsermodManager::getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) - um_data = simulateSound(SEGMENT.soundSim); // add support for no audio - + um_data_t *um_data = getAudioData(); uint8_t *fftResult = (uint8_t *)um_data->u_data[2]; // 16 bins with FFT data, log mapped already, each band contains frequency amplitude 0-255 //map the bands into 16 positions on x axis, emit some particles according to frequency loudness @@ -10027,41 +10020,38 @@ uint16_t mode_particle1Dsonicstream(void) { PartSys->sources[0].v = 5 + (SEGMENT.speed >> 2); // FFT processing - um_data_t *um_data; - if (!UsermodManager::getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) - um_data = simulateSound(SEGMENT.soundSim); // add support for no audio - + um_data_t *um_data = getAudioData(); uint8_t *fftResult = (uint8_t *)um_data->u_data[2]; // 16 bins with FFT data, log mapped already, each band contains frequency amplitude 0-255 uint32_t loudness; uint32_t baseBin = SEGMENT.custom3 >> 1; // 0 - 15 map(SEGMENT.custom3, 0, 31, 0, 14); loudness = fftResult[baseBin];// + fftResult[baseBin + 1]; int mids = sqrt16((int)fftResult[5] + (int)fftResult[6] + (int)fftResult[7] + (int)fftResult[8] + (int)fftResult[9] + (int)fftResult[10]); // average the mids, bin 5 is ~500Hz, bin 10 is ~2kHz (see audio_reactive.h) - if(baseBin > 12) + if (baseBin > 12) loudness = loudness << 2; // double loudness for high frequencies (better detecion) uint32_t threshold = 150 - (SEGMENT.intensity >> 1); - if(SEGMENT.check2) { // enable low pass filter for dynamic threshold + if (SEGMENT.check2) { // enable low pass filter for dynamic threshold SEGMENT.step = (SEGMENT.step * 31500 + loudness * (32768 - 31500)) >> 15; // low pass filter for simple beat detection: add average to base threshold threshold = 20 + (threshold >> 1) + SEGMENT.step; // add average to threshold } // color uint32_t hueincrement = (SEGMENT.custom1 >> 3); // 0-31 - if(SEGMENT.custom1 < 255) + if (SEGMENT.custom1 < 255) PartSys->setColorByPosition(false); else PartSys->setColorByPosition(true); // particle manipulation for (uint32_t i = 0; i < PartSys->usedParticles; i++) { - if(PartSys->sources[0].sourceFlags.perpetual == false) { // age faster if not perpetual + if (PartSys->sources[0].sourceFlags.perpetual == false) { // age faster if not perpetual if (PartSys->particles[i].ttl > 2) { PartSys->particles[i].ttl -= 2; //ttl is linked to brightness, this allows to use higher brightness but still a short lifespan } else PartSys->particles[i].ttl = 0; } - if(SEGMENT.check1) // modulate colors by mid frequencies + if (SEGMENT.check1) // modulate colors by mid frequencies PartSys->particles[i].hue += (mids * inoise8(PartSys->particles[i].x << 2, SEGMENT.step << 2)) >> 9; // color by perlin noise from mid frequencies } @@ -10071,20 +10061,20 @@ uint16_t mode_particle1Dsonicstream(void) { PartSys->sources[0].maxLife = PartSys->sources[0].minLife; PartSys->sources[0].source.hue = SEGMENT.aux0; PartSys->sources[0].size = SEGMENT.speed; - if(PartSys->particles[SEGMENT.aux1].x > 3 * PS_P_RADIUS_1D || PartSys->particles[SEGMENT.aux1].ttl == 0) { // only emit if last particle is far enough away or dead + if (PartSys->particles[SEGMENT.aux1].x > 3 * PS_P_RADIUS_1D || PartSys->particles[SEGMENT.aux1].ttl == 0) { // only emit if last particle is far enough away or dead int partindex = PartSys->sprayEmit(PartSys->sources[0]); // emit a particle - if(partindex >= 0) SEGMENT.aux1 = partindex; // track last emitted particle + if (partindex >= 0) SEGMENT.aux1 = partindex; // track last emitted particle } } else loudness = 0; // required for push mode PartSys->update(); // update and render (needs to be done before manipulation for initial particle spacing to be right) - if(SEGMENT.check3) { // push mode + if (SEGMENT.check3) { // push mode PartSys->sources[0].sourceFlags.perpetual = true; // emitted particles dont age PartSys->applyFriction(1); //slow down particles int32_t movestep = (((int)SEGMENT.speed + 2) * loudness) >> 10; - if(movestep) { + if (movestep) { for (uint32_t i = 0; i < PartSys->usedParticles; i++) { if (PartSys->particles[i].ttl) { PartSys->particles[i].x += movestep; // push particles @@ -10125,7 +10115,7 @@ uint8_t WS2812FX::addEffect(uint8_t id, mode_ptr mode_fn, const char *mode_name) _mode[id] = mode_fn; _modeData[id] = mode_name; return id; - } else if(_mode.size() < 255) { // 255 is reserved for indicating the effect wasn't added + } else if (_mode.size() < 255) { // 255 is reserved for indicating the effect wasn't added _mode.push_back(mode_fn); _modeData.push_back(mode_name); if (_modeCount < _mode.size()) _modeCount++; From e409cbc5ae717708afb7ebc9691996f83f5ecf51 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Thu, 23 Jan 2025 16:28:05 +0100 Subject: [PATCH 205/219] revert whitespaces --- platformio.ini | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/platformio.ini b/platformio.ini index 78aaa6eb62..8cf7fbd99b 100644 --- a/platformio.ini +++ b/platformio.ini @@ -15,7 +15,8 @@ default_envs = nodemcuv2, esp8266_2m, esp01_1m_full, nodemcuv2_160, esp8266_2m_1 src_dir = ./wled00 data_dir = ./wled00/data build_cache_dir = ~/.buildcache -extra_configs = platformio_override.ini +extra_configs = + platformio_override.ini [common] # ------------------------------------------------------------------------------ From b1f3eaf3ce845aa0647a3494e10455337cd9fb0e Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Thu, 23 Jan 2025 17:47:08 +0100 Subject: [PATCH 206/219] prohibit use of 1D and 2D system simultaneously on ESP8266 --- wled00/FX.cpp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index a0709ea2e8..ffb35a4c52 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -15,9 +15,14 @@ #include "fcn_declare.h" #if !(defined(WLED_DISABLE_PARTICLESYSTEM2D) && defined(WLED_DISABLE_PARTICLESYSTEM1D)) -#include "FXparticleSystem.h" + #include "FXparticleSystem.h" + #ifdef ESP8266 + #if !defined(WLED_DISABLE_PARTICLESYSTEM2D) && !defined(WLED_DISABLE_PARTICLESYSTEM1D) + #error ESP8266 does not support 1D and 2D particle systems simultaneously. Please disable one of them. + #endif + #endif #else -#define WLED_PS_DONT_REPLACE_FX + #define WLED_PS_DONT_REPLACE_FX #endif ////////////// From ebd787ff60000351b10d29ffd84d2736b6c2d67b Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Thu, 23 Jan 2025 17:47:08 +0100 Subject: [PATCH 207/219] prohibit use of 1D and 2D system simultaneously on ESP8266 --- wled00/FX.cpp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index a0709ea2e8..ffb35a4c52 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -15,9 +15,14 @@ #include "fcn_declare.h" #if !(defined(WLED_DISABLE_PARTICLESYSTEM2D) && defined(WLED_DISABLE_PARTICLESYSTEM1D)) -#include "FXparticleSystem.h" + #include "FXparticleSystem.h" + #ifdef ESP8266 + #if !defined(WLED_DISABLE_PARTICLESYSTEM2D) && !defined(WLED_DISABLE_PARTICLESYSTEM1D) + #error ESP8266 does not support 1D and 2D particle systems simultaneously. Please disable one of them. + #endif + #endif #else -#define WLED_PS_DONT_REPLACE_FX + #define WLED_PS_DONT_REPLACE_FX #endif ////////////// From b16d68dcc6068a3358e0dcedca576b005628e2d4 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Fri, 24 Jan 2025 16:28:07 +0100 Subject: [PATCH 208/219] update to handle blending styles --- wled00/FX.h | 3 +- wled00/FX_fcn.cpp | 14 +++---- wled00/FXparticleSystem.cpp | 74 ++++++++++++++++++++++++------------- wled00/FXparticleSystem.h | 1 + 4 files changed, 56 insertions(+), 36 deletions(-) diff --git a/wled00/FX.h b/wled00/FX.h index 4825906e37..9c03441763 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -497,7 +497,6 @@ typedef struct Segment { #ifndef WLED_DISABLE_MODE_BLEND tmpsegd_t _segT; // previous segment environment uint8_t _modeT; // previous mode/effect - uint8_t _palette; // previous palette #else uint32_t _colorT[NUM_COLORS]; #endif @@ -653,7 +652,7 @@ typedef struct Segment { uint8_t currentMode() const; // currently active effect/mode (while in transition) [[gnu::hot]] uint32_t currentColor(uint8_t slot) const; // currently active segment color (blended while in transition) CRGBPalette16 &loadPalette(CRGBPalette16 &tgt, uint8_t pal); - void setCurrentPalette(bool loadOldPalette = false); + void loadOldPalette(); // loads old FX palette into _currentPalette // 1D strip [[gnu::hot]] uint16_t virtualLength() const; diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index 243bd68fb3..51fae3b1a6 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -449,14 +449,6 @@ void Segment::beginDraw() { _currentColors[i] = gamma32(col); } // load palette into _currentPalette - setCurrentPalette(); -} - -void Segment::setCurrentPalette(bool loadOldPalette) { - if(loadOldPalette && isInTransition()) { - loadPalette(_currentPalette, _t->_palette); // load palette of old effect, used in particle system - return; - } loadPalette(_currentPalette, palette); if (prog < 0xFFFFU) { #ifndef WLED_DISABLE_MODE_BLEND @@ -476,6 +468,12 @@ void Segment::setCurrentPalette(bool loadOldPalette) { } } +// loads palette of the old FX during transitions (used by particle system) +void Segment::loadOldPalette(void) { + if(isInTransition()) + loadPalette(_currentPalette, _t->_palTid); +} + // relies on WS2812FX::service() to call it for each frame void Segment::handleRandomPalette() { // is it time to generate a new palette? diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index cd5774bcf7..f00320b21a 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -556,12 +556,12 @@ void ParticleSystem2D::pointAttractor(const uint32_t particleindex, PSparticle & void ParticleSystem2D::ParticleSys_render() { CRGB baseRGB; uint32_t brightness; // particle brightness, fades if dying - static bool useAdditiveTransfer = false; // use add instead of set for buffer transferring + static bool useAdditiveTransfer = false; // use add instead of set for buffer transferring TODO: why static? // update global blur (used for blur transitions) int32_t motionbluramount = motionBlur; int32_t smearamount = smearBlur; - if(pmem->inTransition == effectID) { // FX transition and this is the new FX: fade blur amount + if(pmem->inTransition == effectID && blendingStyle == BLEND_STYLE_FADE) { // FX transition and this is the new FX: fade blur amount but only if using fade style motionbluramount = globalBlur + (((motionbluramount - globalBlur) * (int)SEGMENT.progress()) >> 16); // fade from old blur to new blur during transitions smearamount = globalSmear + (((smearamount - globalSmear) * (int)SEGMENT.progress()) >> 16); } @@ -573,14 +573,14 @@ void ParticleSystem2D::ParticleSys_render() { if(segmentIsOverlay()) useAdditiveTransfer = true; // overlay rendering else useAdditiveTransfer = false; // handle buffer blurring or clearing - bool bufferNeedsUpdate = (!pmem->inTransition || pmem->inTransition == effectID); // not a transition; or new FX: update buffer (blur, or clear) - + bool bufferNeedsUpdate = (!pmem->inTransition || pmem->inTransition == effectID || blendingStyle != BLEND_STYLE_FADE); // not a transition; or new FX or not fading style: update buffer (blur, or clear) if(bufferNeedsUpdate) { + bool loadfromSegment = !renderSolo || blendingStyle != BLEND_STYLE_FADE; if (globalBlur > 0 || globalSmear > 0) { // blurring active: if not a transition or is newFX, read data from segment before blurring (old FX can render to it afterwards) for (int32_t y = 0; y <= maxYpixel; y++) { int index = y * (maxXpixel + 1); for (int32_t x = 0; x <= maxXpixel; x++) { - if (!renderSolo) { // sharing the framebuffer with another segment: update buffer by reading back from segment + if (loadfromSegment) { // sharing the framebuffer with another segment or not using fade style blending: update buffer by reading back from segment framebuffer[index] = SEGMENT.getPixelColorXY(x, y); // read from segment } fast_color_scale(framebuffer[index], globalBlur); // note: could skip if only globalsmear is active but usually they are both active and scaling is fast enough @@ -615,16 +615,16 @@ void ParticleSystem2D::ParticleSys_render() { if (particles[i].ttl == 0 || particleFlags[i].outofbounds) continue; // generate RGB values for particle - if (fireIntesity) { + if (fireIntesity) { // fire mode brightness = (uint32_t)particles[i].ttl * (3 + (fireIntesity >> 5)) + 20; brightness = min(brightness, (uint32_t)255); - baseRGB = ColorFromPalette(SEGPALETTE, brightness, 255); + baseRGB = ColorFromPaletteWLED(SEGPALETTE, brightness, 255); } else { brightness = min((particles[i].ttl << 1), (int)255); - baseRGB = ColorFromPalette(SEGPALETTE, particles[i].hue, 255); + baseRGB = ColorFromPaletteWLED(SEGPALETTE, particles[i].hue, 255); if (particles[i].sat < 255) { - CHSV32 baseHSV; + CHSV32 baseHSV; rgb2hsv((uint32_t((byte(baseRGB.r) << 16) | (byte(baseRGB.g) << 8) | (byte(baseRGB.b)))), baseHSV); // convert to HSV baseHSV.s = particles[i].sat; // set the saturation uint32_t tempcolor; @@ -659,9 +659,8 @@ void ParticleSystem2D::ParticleSys_render() { SEGMENT.blur(globalSmear, true); } // transfer framebuffer to segment if available - if (pmem->inTransition != effectID) { // not in transition or is old FX (rendered second) + if (pmem->inTransition != effectID || blendingStyle != BLEND_STYLE_FADE) // not in transition or is old FX (rendered second) or not fade style transferBuffer(maxXpixel + 1, maxYpixel + 1, useAdditiveTransfer); - } } // calculate pixel positions and brightness distribution and render the particle to local buffer or global buffer @@ -1513,7 +1512,7 @@ void ParticleSystem1D::ParticleSys_render() { // generate RGB values for particle brightness = min(particles[i].ttl << 1, (int)255); - baseRGB = ColorFromPalette(SEGPALETTE, particles[i].hue, 255); + baseRGB = ColorFromPaletteWLED(SEGPALETTE, particles[i].hue, 255); if (advPartProps) { //saturation is advanced property in 1D system if (advPartProps[i].sat < 255) { @@ -2027,6 +2026,7 @@ void* particleMemoryManager(const uint32_t requestedParticles, size_t structSize uint32_t requestsize = structSize * requestedParticles; // required buffer size if (requestsize > pmem->buffersize) { // request is larger than buffer, try to extend it if (Segment::getUsedSegmentData() + requestsize - pmem->buffersize <= MAX_SEGMENT_DATA) { // enough memory available to extend buffer + PSPRINTLN("Extending buffer"); buffer = allocatePSmemory(requestsize, true); // calloc new memory in FX data, override limit (temporary buffer) if (buffer) { // allocaction successful, copy old particles to new buffer memcpy(buffer, pmem->particleMemPointer, pmem->buffersize); // copy old particle buffer note: only required if transition but copy is fast and rarely happens @@ -2038,8 +2038,13 @@ void* particleMemoryManager(const uint32_t requestedParticles, size_t structSize return nullptr; // no memory available } } - if (pmem->watchdog == 1) // if a PS already exists during particle request, it kicked the watchdog in last frame, servicePSmem() adds 1 afterwards -> PS to PS transition - pmem->inTransition = effectID; // save the ID of the new effect (required to determine blur amount in rendering function) + if (pmem->watchdog == 1) { // if a PS already exists during particle request, it kicked the watchdog in last frame, servicePSmem() adds 1 afterwards -> PS to PS transition + if(pmem->currentFX == effectID) // if the new effect is the same as the current one, do not transition: transferParticles is set above, so this will transfer all particles back if called during transition + pmem->inTransition = false; // reset transition flag + else + pmem->inTransition = effectID; // save the ID of the new effect (required to determine blur amount in rendering function) + PSPRINTLN("PS to PS transition"); + } return pmem->particleMemPointer; // return the available buffer on init call } pmem->watchdog = 0; // kick watchdog @@ -2050,7 +2055,7 @@ void* particleMemoryManager(const uint32_t requestedParticles, size_t structSize uint32_t requestsize = structSize * requestedParticles; // required buffer size buffer = allocatePSmemory(requestsize, false); // allocate new memory if (buffer) - partMemList.push_back({buffer, requestsize, 0, strip.getCurrSegmentId(), 0, 0, true}); // add buffer to list, set flag to transfer/init the particles note: if pushback fails, it may crash + partMemList.push_back({buffer, requestsize, 0, strip.getCurrSegmentId(), 0, 0, 0, true}); // add buffer to list, set flag to transfer/init the particles note: if pushback fails, it may crash else return nullptr; // there is no memory available TODO: if localbuffer is allocated, free it and try again, its no use having a buffer but no particles pmem = getPartMem(); // get the pointer to the new element (check that it was added) @@ -2061,13 +2066,17 @@ void* particleMemoryManager(const uint32_t requestedParticles, size_t structSize return buffer; // directly return the buffer on init call } + + // now we have a valid buffer, if this is a PS to PS FX transition: transfer particles slowly to new FX - bool effectchanged = (SEGMENT.currentMode() != SEGMENT.mode); // FX changed, transition the particle buffer - if (effectchanged && pmem->inTransition) { + if(!SEGMENT.isInTransition()) pmem->inTransition = false; // transition has ended, update pmem + //TODO: if going back to old FX during a transition, there is not init call. need to detect that somehow or rendering does not work + if (pmem->inTransition) { uint32_t maxParticles = pmem->buffersize / structSize; // maximum number of particles that fit in the buffer uint16_t progress = SEGMENT.progress(); // transition progress uint32_t newAvailable = 0; if (SEGMENT.mode == effectID) { // new effect ID -> function was called from new FX + PSPRINTLN("new effect"); newAvailable = (maxParticles * progress) >> 16; // update total particles available to this PS (newAvailable is guaranteed to be smaller than maxParticles) if(newAvailable < 2) newAvailable = 2; // give 2 particle minimum (some FX may crash with less as they do i+1 access) if(maxParticles / numParticlesUsed > 3 && newAvailable > numParticlesUsed) newAvailable = numParticlesUsed; // limit to number of particles used for FX using a small amount, do not move the pointer anymore (will be set to base in final handover) @@ -2079,7 +2088,8 @@ void* particleMemoryManager(const uint32_t requestedParticles, size_t structSize particleHandover(buffer, structSize, totransfer); } else { // this was called from the old FX - SEGMENT.setCurrentPalette(true); // load the old palette into segment + PSPRINTLN("old effect"); + SEGMENT.loadOldPalette(); // load the old palette into segment palette progress = 0xFFFFU - progress; // inverted transition progress newAvailable = ((maxParticles * progress) >> 16); // result is guaranteed to be smaller than maxParticles if(newAvailable > 0) newAvailable--; // -1 to avoid overlapping memory in 1D<->2D transitions @@ -2127,8 +2137,8 @@ void* particleMemoryManager(const uint32_t requestedParticles, size_t structSize PSPRINTLN("final available particles: " + String(availableToPS)); pmem->particleType = structSize; // update particle type pmem->transferParticles = false; + pmem->currentFX = effectID; // FX has now settled in, update the FX ID to track future transitions } - pmem->inTransition = false; } #ifdef WLED_DEBUG_PS PSPRINT(" Particle memory Pointer address: 0x"); @@ -2142,13 +2152,20 @@ void particleHandover(void *buffer, size_t structSize, int32_t numToTransfer) { if (pmem->particleType != structSize) { // check if we are being handed over from a different system (1D<->2D), clear buffer if so memset(buffer, 0, numToTransfer * structSize); // clear buffer } + uint16_t maxTTL = 0; + uint32_t TTLrandom = 0; + maxTTL = ((unsigned)strip.getTransition() << 1) / FRAMETIME_FIXED; // tie TTL to transition time: limit to double the transition time + some randomness #ifndef WLED_DISABLE_PARTICLESYSTEM2D if (structSize == sizeof(PSparticle)) { // 2D particle PSparticle *particles = (PSparticle *)buffer; for (int32_t i = 0; i < numToTransfer; i++) { - if (particles[i].ttl > 200) - particles[i].ttl = 150 + hw_random16(50); // reduce TTL so it will die soon - particles[i].sat = 255; // full saturation + if (blendingStyle == BLEND_STYLE_FADE) { + if(particles[i].ttl > maxTTL) + particles[i].ttl = maxTTL + hw_random16(150); // reduce TTL so it will die soon + } + else + particles[i].ttl = 0; // kill transferred particles if not using fade blending style + particles[i].sat = 255; // full saturation } } else // 1D particle system @@ -2157,8 +2174,12 @@ void particleHandover(void *buffer, size_t structSize, int32_t numToTransfer) { #ifndef WLED_DISABLE_PARTICLESYSTEM1D PSparticle1D *particles = (PSparticle1D *)buffer; for (int32_t i = 0; i < numToTransfer; i++) { - if (particles[i].ttl > 200) - particles[i].ttl = 150 + hw_random16(50); // reduce TTL so it will die soon + if (blendingStyle == BLEND_STYLE_FADE) { + if(particles[i].ttl > maxTTL) + particles[i].ttl = maxTTL + hw_random16(150); // reduce TTL so it will die soon + } + else + particles[i].ttl = 0; // kill transferred particles if not using fade blending style } #endif } @@ -2258,8 +2279,9 @@ void transferBuffer(uint32_t width, uint32_t height, bool useAdditiveTransfer) { PSPRINT(" xfer buf "); #ifndef WLED_DISABLE_MODE_BLEND bool tempBlend = SEGMENT.getmodeBlend(); - if (pmem->inTransition) - SEGMENT.modeBlend(false); // temporarily disable FX blending in PS to PS transition (using local buffer to do PS blending) + if(pmem->inTransition && blendingStyle == BLEND_STYLE_FADE) { + SEGMENT.modeBlend(false); // temporarily disable FX blending in PS to PS transition (using local buffer to do PS blending) + } #endif if(height) { // is 2D, 1D passes height = 0 diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index 8be4bfb917..2e74cad25b 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -38,6 +38,7 @@ struct partMem { uint8_t id; // ID of segment this memory belongs to uint8_t watchdog; // counter to handle deallocation uint8_t inTransition; // to track PS to PS FX transitions (is set to new FX ID during transitions), not set if not both FX are PS FX + uint8_t currentFX; // current FX ID, is set when transition is complete, used to detect back and forth transitions bool transferParticles; // if set, particles in buffer are transferred to new FX }; From f674a12bf43f54b390d54f995cf16114728973d4 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sun, 26 Jan 2025 10:12:15 +0100 Subject: [PATCH 209/219] fixed bugs, improved new transition handling --- wled00/FX.cpp | 4 +- wled00/FXparticleSystem.cpp | 127 +++++++++++++++++++----------------- wled00/FXparticleSystem.h | 1 + 3 files changed, 71 insertions(+), 61 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 28ea81f54a..058e65d89f 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -7901,7 +7901,7 @@ uint16_t mode_particlefireworks(void) { return FRAMETIME; } #undef NUMBEROFSOURCES -static const char _data_FX_MODE_PARTICLEFIREWORKS[] PROGMEM = "PS Fireworks@Launches,Explosion Size,Fuse,Blur,Gravity,Cylinder,Ground,Fast;;!;2;pal=11,sx=100,ix=50,c1=40,c2=0,c3=12"; +static const char _data_FX_MODE_PARTICLEFIREWORKS[] PROGMEM = "PS Fireworks@Launches,Explosion Size,Fuse,Blur,Gravity,Cylinder,Ground,Fast;;!;2;pal=11,ix=50,c1=40,c2=0,c3=12"; /* Particle Volcano @@ -8291,7 +8291,7 @@ uint16_t mode_particlebox(void) { return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEBOX[] PROGMEM = "PS Box@Speed,Particles,Tilt Strength,Hardness,Friction,Random,Washing Machine,Sloshing;;!;2;pal=53,sx=120,ix=100,c1=100,c2=210,o1=1"; +static const char _data_FX_MODE_PARTICLEBOX[] PROGMEM = "PS Box@!,Particles,Force,Hardness,Friction,Random,Washing Machine,Sloshing;;!;2;pal=53,ix=50,o1=1"; /* Fuzzy Noise: Perlin noise 'gravity' mapping as in particles on 'noise hills' viewed from above diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index f00320b21a..de87072370 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -556,7 +556,10 @@ void ParticleSystem2D::pointAttractor(const uint32_t particleindex, PSparticle & void ParticleSystem2D::ParticleSys_render() { CRGB baseRGB; uint32_t brightness; // particle brightness, fades if dying - static bool useAdditiveTransfer = false; // use add instead of set for buffer transferring TODO: why static? + static bool useAdditiveTransfer = false; // use add instead of set for buffer transferring (must persist between calls) + //bool isNonFadeTransition = (SEGMENT.isInTransition() && blendingStyle != BLEND_STYLE_FADE); + bool isNonFadeTransition = (pmem->inTransition || pmem->finalTransfer) && blendingStyle != BLEND_STYLE_FADE; + bool isOverlay = segmentIsOverlay(); // update global blur (used for blur transitions) int32_t motionbluramount = motionBlur; @@ -570,12 +573,12 @@ void ParticleSystem2D::ParticleSys_render() { // handle blurring and framebuffer update if (framebuffer) { - if(segmentIsOverlay()) useAdditiveTransfer = true; // overlay rendering - else useAdditiveTransfer = false; + if(!pmem->inTransition) + useAdditiveTransfer = false; // additive transfer is only usd in transitions (or in overlay) // handle buffer blurring or clearing - bool bufferNeedsUpdate = (!pmem->inTransition || pmem->inTransition == effectID || blendingStyle != BLEND_STYLE_FADE); // not a transition; or new FX or not fading style: update buffer (blur, or clear) + bool bufferNeedsUpdate = (!pmem->inTransition || pmem->inTransition == effectID || isNonFadeTransition); // not a transition; or new FX or not fading style: update buffer (blur, or clear) if(bufferNeedsUpdate) { - bool loadfromSegment = !renderSolo || blendingStyle != BLEND_STYLE_FADE; + bool loadfromSegment = !renderSolo || isNonFadeTransition; if (globalBlur > 0 || globalSmear > 0) { // blurring active: if not a transition or is newFX, read data from segment before blurring (old FX can render to it afterwards) for (int32_t y = 0; y <= maxYpixel; y++) { int index = y * (maxXpixel + 1); @@ -592,12 +595,13 @@ void ParticleSystem2D::ParticleSys_render() { memset(framebuffer, 0, frameBufferSize * sizeof(CRGB)); } } + // handle buffer for global large particle size rendering if(particlesize > 0 && pmem->inTransition) { // if particle size is used by FX we need a clean buffer - if(bufferNeedsUpdate && !globalBlur) { // transfer only if buffer was not cleared above (happens if this is the new FX and other FX does not use blurring) + if(bufferNeedsUpdate && !globalBlur) { // transfer without adding if buffer was not cleared above (happens if this is the new FX and other FX does not use blurring) useAdditiveTransfer = false; // no blurring and big size particle FX is the new FX (rendered first after clearing), can just render normally } else { // this is the old FX (rendering second) or blurring is active: new FX already rendered to the buffer and blurring was applied above; transfer it to segment and clear it - transferBuffer(maxXpixel + 1, maxYpixel + 1, useAdditiveTransfer); + transferBuffer(maxXpixel + 1, maxYpixel + 1, isOverlay); memset(framebuffer, 0, frameBufferSize * sizeof(CRGB)); // clear the buffer after transfer useAdditiveTransfer = true; // additive transfer reads from segment, adds that to the frame-buffer and writes back to segment, after transfer, segment and buffer are identical } @@ -659,8 +663,8 @@ void ParticleSystem2D::ParticleSys_render() { SEGMENT.blur(globalSmear, true); } // transfer framebuffer to segment if available - if (pmem->inTransition != effectID || blendingStyle != BLEND_STYLE_FADE) // not in transition or is old FX (rendered second) or not fade style - transferBuffer(maxXpixel + 1, maxYpixel + 1, useAdditiveTransfer); + if (pmem->inTransition != effectID || isNonFadeTransition) // not in transition or is old FX (rendered second) or not fade style + transferBuffer(maxXpixel + 1, maxYpixel + 1, useAdditiveTransfer | isOverlay); } // calculate pixel positions and brightness distribution and render the particle to local buffer or global buffer @@ -2055,7 +2059,7 @@ void* particleMemoryManager(const uint32_t requestedParticles, size_t structSize uint32_t requestsize = structSize * requestedParticles; // required buffer size buffer = allocatePSmemory(requestsize, false); // allocate new memory if (buffer) - partMemList.push_back({buffer, requestsize, 0, strip.getCurrSegmentId(), 0, 0, 0, true}); // add buffer to list, set flag to transfer/init the particles note: if pushback fails, it may crash + partMemList.push_back({buffer, requestsize, 0, strip.getCurrSegmentId(), 0, 0, 0, false, true}); // add buffer to list, set flag to transfer/init the particles note: if pushback fails, it may crash else return nullptr; // there is no memory available TODO: if localbuffer is allocated, free it and try again, its no use having a buffer but no particles pmem = getPartMem(); // get the pointer to the new element (check that it was added) @@ -2066,11 +2070,8 @@ void* particleMemoryManager(const uint32_t requestedParticles, size_t structSize return buffer; // directly return the buffer on init call } - - // now we have a valid buffer, if this is a PS to PS FX transition: transfer particles slowly to new FX - if(!SEGMENT.isInTransition()) pmem->inTransition = false; // transition has ended, update pmem - //TODO: if going back to old FX during a transition, there is not init call. need to detect that somehow or rendering does not work + if(!SEGMENT.isInTransition()) pmem->inTransition = false; // transition has ended, invoke final transfer if (pmem->inTransition) { uint32_t maxParticles = pmem->buffersize / structSize; // maximum number of particles that fit in the buffer uint16_t progress = SEGMENT.progress(); // transition progress @@ -2079,13 +2080,13 @@ void* particleMemoryManager(const uint32_t requestedParticles, size_t structSize PSPRINTLN("new effect"); newAvailable = (maxParticles * progress) >> 16; // update total particles available to this PS (newAvailable is guaranteed to be smaller than maxParticles) if(newAvailable < 2) newAvailable = 2; // give 2 particle minimum (some FX may crash with less as they do i+1 access) - if(maxParticles / numParticlesUsed > 3 && newAvailable > numParticlesUsed) newAvailable = numParticlesUsed; // limit to number of particles used for FX using a small amount, do not move the pointer anymore (will be set to base in final handover) + if(newAvailable > numParticlesUsed) newAvailable = numParticlesUsed; // limit to number of particles used for FX using a small amount, do not move the pointer anymore (will be set to base in final handover) uint32_t bufferoffset = (maxParticles - 1) - newAvailable; // offset to new effect particles (in particle structs, not bytes) if(bufferoffset < maxParticles) // safety check buffer = (void*)((uint8_t*)buffer + bufferoffset * structSize); // new effect gets the end of the buffer int32_t totransfer = newAvailable - availableToPS; // number of particles to transfer in this transition update - if(totransfer < 0) totransfer = 0; // safety check - particleHandover(buffer, structSize, totransfer); + if(totransfer > 0) // safety check + particleHandover(buffer, structSize, totransfer); } else { // this was called from the old FX PSPRINTLN("old effect"); @@ -2097,49 +2098,52 @@ void* particleMemoryManager(const uint32_t requestedParticles, size_t structSize // note: buffer pointer stays the same, number of available particles is reduced } availableToPS = newAvailable; - } else { // no PS transition, full buffer available - if(pmem->transferParticles) { // transition ended (or blending is disabled) -> transfer all remaining particles - PSPRINTLN("PS transition ended, final particle handover"); - uint32_t maxParticles = pmem->buffersize / structSize; // maximum number of particles that fit in the buffer - if (maxParticles > availableToPS) { // not all particles transferred yet - int32_t totransfer = maxParticles - availableToPS; // transfer all remaining particles - if(totransfer < 0) totransfer = 0; // safety check + } else if(pmem->transferParticles) { // no PS transition, full buffer available + // transition ended (or blending is disabled) -> transfer all remaining particles + PSPRINTLN("PS transition ended, final particle handover"); + uint32_t maxParticles = pmem->buffersize / structSize; // maximum number of particles that fit in the buffer + if (maxParticles > availableToPS) { // not all particles transferred yet + int32_t totransfer = maxParticles - availableToPS; // transfer all remaining particles + if(totransfer > 0) // safety check particleHandover(buffer, structSize, totransfer); - - if(maxParticles / numParticlesUsed > 3) { // FX uses less than 25%: move the already existing particles to the beginning of the buffer - uint32_t usedbytes = availableToPS * structSize; - int32_t bufferoffset = (maxParticles - 1) - availableToPS; // offset to existing particles (see above) - if(bufferoffset < (int)maxParticles) { // safety check - void* currentBuffer = (void*)((uint8_t*)buffer + bufferoffset * structSize); // pointer to current buffer start - memmove(buffer, currentBuffer, usedbytes); // move the existing particles to the beginning of the buffer - } + if(maxParticles > numParticlesUsed) { // FX uses less than max: move the already existing particles to the beginning of the buffer + uint32_t usedbytes = availableToPS * structSize; + int32_t bufferoffset = (maxParticles - 1) - availableToPS; // offset to existing particles (see above) + if(bufferoffset < (int)maxParticles) { // safety check + void* currentBuffer = (void*)((uint8_t*)buffer + bufferoffset * structSize); // pointer to current buffer start + memmove(buffer, currentBuffer, usedbytes); // move the existing particles to the beginning of the buffer } } - // kill unused particles to they do not re-appear when transitioning to next FX - #ifndef WLED_DISABLE_PARTICLESYSTEM2D - if (structSize == sizeof(PSparticle)) { // 2D particle - PSparticle *particles = (PSparticle *)buffer; - for (uint32_t i = availableToPS; i < maxParticles; i++) { - particles[i].ttl = 0; // kill unused particles - } + } + // kill unused particles so they do not re-appear when transitioning to next FX + //TODO: can this be done in the handover function? + #ifndef WLED_DISABLE_PARTICLESYSTEM2D + if (structSize == sizeof(PSparticle)) { // 2D particle + PSparticle *particles = (PSparticle*)buffer; + for (uint32_t i = availableToPS; i < maxParticles; i++) { + particles[i].ttl = 0; // kill unused particles } - else // 1D particle system - #endif - { - #ifndef WLED_DISABLE_PARTICLESYSTEM1D - PSparticle1D *particles = (PSparticle1D *)buffer; - for (uint32_t i = availableToPS; i < maxParticles; i++) { - particles[i].ttl = 0; // kill unused particles - } - #endif + } + else // 1D particle system + #endif + { + #ifndef WLED_DISABLE_PARTICLESYSTEM1D + PSparticle1D *particles = (PSparticle1D*)buffer; + for (uint32_t i = availableToPS; i < maxParticles; i++) { + particles[i].ttl = 0; // kill unused particles } - availableToPS = maxParticles; // now all particles are available to new FX - PSPRINTLN("final available particles: " + String(availableToPS)); - pmem->particleType = structSize; // update particle type - pmem->transferParticles = false; - pmem->currentFX = effectID; // FX has now settled in, update the FX ID to track future transitions + #endif } + availableToPS = maxParticles; // now all particles are available to new FX + PSPRINTLN("final available particles: " + String(availableToPS)); + pmem->particleType = structSize; // update particle type + pmem->transferParticles = false; + pmem->finalTransfer = true; // let rendering function update its buffer if required + pmem->currentFX = effectID; // FX has now settled in, update the FX ID to track future transitions } + else // no transition + pmem->finalTransfer = false; + #ifdef WLED_DEBUG_PS PSPRINT(" Particle memory Pointer address: 0x"); Serial.println((uintptr_t)buffer, HEX); @@ -2195,11 +2199,17 @@ void updateUsedParticles(const uint32_t allocated, const uint32_t available, con bool segmentIsOverlay(void) { // TODO: this only needs to be checked when segment is created, could move this to segment class or PS init unsigned segID = strip.getCurrSegmentId(); if(segID > 0) { // lower number segments exist, check coordinates of underlying segments - for (unsigned i = 0; i < segID; i++) { - if(strip._segments[i].start <= strip._segments[segID].start && strip._segments[i].stop >= strip._segments[segID].stop && - strip._segments[i].startY <= strip._segments[segID].startY && strip._segments[i].stopY >= strip._segments[segID].stopY) - return true; - } + unsigned xMin, yMin = 0xFFFF; // note: overlay is activated even if there is gaps in underlying segments, this is an intentional choice + unsigned xMax, yMax = 0; + for (unsigned i = 0; i < segID; i++) { + xMin = strip._segments[i].start < xMin ? strip._segments[i].start : xMin; + yMin = strip._segments[i].startY < yMin ? strip._segments[i].startY : yMin; + xMax = strip._segments[i].stop > xMax ? strip._segments[i].stop : xMax; + yMax = strip._segments[i].stopY > yMax ? strip._segments[i].stopY : yMax; + if(xMin <= strip._segments[segID].start && xMax >= strip._segments[segID].stop && + yMin <= strip._segments[segID].startY && yMax >= strip._segments[segID].stopY) + return true; + } } return false; } @@ -2283,7 +2293,6 @@ void transferBuffer(uint32_t width, uint32_t height, bool useAdditiveTransfer) { SEGMENT.modeBlend(false); // temporarily disable FX blending in PS to PS transition (using local buffer to do PS blending) } #endif - if(height) { // is 2D, 1D passes height = 0 for (uint32_t y = 0; y < height; y++) { int index = y * width; // current row index for 1D buffer diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index 2e74cad25b..7cf6fa4a1a 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -39,6 +39,7 @@ struct partMem { uint8_t watchdog; // counter to handle deallocation uint8_t inTransition; // to track PS to PS FX transitions (is set to new FX ID during transitions), not set if not both FX are PS FX uint8_t currentFX; // current FX ID, is set when transition is complete, used to detect back and forth transitions + bool finalTransfer; // used to update buffer in rendering function after transition has ended bool transferParticles; // if set, particles in buffer are transferred to new FX }; From 6becad7e076c490fbf6094a90b821c9146ba3289 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sun, 26 Jan 2025 11:45:38 +0100 Subject: [PATCH 210/219] updated 1D system to work with new transitions, replaced NULL with nullptr --- wled00/FX.cpp | 113 ++++++++++++++++++------------------ wled00/FXparticleSystem.cpp | 57 ++++++++++-------- 2 files changed, 88 insertions(+), 82 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 058e65d89f..ea75fda049 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -7650,7 +7650,7 @@ static const char _data_FX_MODE_2DWAVINGCELL[] PROGMEM = "Waving Cell@!,Blur,Amp uint16_t mode_particlevortex(void) { if (SEGLEN == 1) return mode_static(); - ParticleSystem2D *PartSys = NULL; + ParticleSystem2D *PartSys = nullptr; uint32_t i, j; if (SEGMENT.call == 0) { // initialization @@ -7672,7 +7672,7 @@ uint16_t mode_particlevortex(void) { else PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS - if (PartSys == NULL) + if (PartSys == nullptr) return mode_static(); // something went wrong, no data! PartSys->updateSystem(); // update system properties (dimensions and data pointers) @@ -7763,7 +7763,7 @@ static const char _data_FX_MODE_PARTICLEVORTEX[] PROGMEM = "PS Vortex@Rotation S #define NUMBEROFSOURCES 8 uint16_t mode_particlefireworks(void) { - ParticleSystem2D *PartSys = NULL; + ParticleSystem2D *PartSys = nullptr; uint32_t numRockets; if (SEGMENT.call == 0) { // initialization @@ -7781,7 +7781,7 @@ uint16_t mode_particlefireworks(void) { else PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS - if (PartSys == NULL) + if (PartSys == nullptr) return mode_static(); // something went wrong, no data! PartSys->updateSystem(); // update system properties (dimensions and data pointers) @@ -7911,7 +7911,7 @@ static const char _data_FX_MODE_PARTICLEFIREWORKS[] PROGMEM = "PS Fireworks@Laun */ #define NUMBEROFSOURCES 1 uint16_t mode_particlevolcano(void) { - ParticleSystem2D *PartSys = NULL; + ParticleSystem2D *PartSys = nullptr; PSsettings2D volcanosettings; volcanosettings.asByte = 0b00000100; // PS settings for volcano movement: bounceX is enabled uint8_t numSprays; // note: so far only one tested but more is possible @@ -7939,7 +7939,7 @@ uint16_t mode_particlevolcano(void) { else PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS - if (PartSys == NULL) + if (PartSys == nullptr) return mode_static(); // something went wrong, no data! numSprays = min(PartSys->numSources, (uint32_t)NUMBEROFSOURCES); // number of volcanoes @@ -7983,7 +7983,7 @@ static const char _data_FX_MODE_PARTICLEVOLCANO[] PROGMEM = "PS Volcano@Speed,In by DedeHai (Damian Schneider) */ uint16_t mode_particlefire(void) { - ParticleSystem2D *PartSys = NULL; + ParticleSystem2D *PartSys = nullptr; uint32_t i; // index variable uint32_t numFlames; // number of flames: depends on fire width. for a fire width of 16 pixels, about 25-30 flames give good results @@ -7995,7 +7995,7 @@ uint16_t mode_particlefire(void) { else PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS - if (PartSys == NULL) + if (PartSys == nullptr) return mode_static(); // something went wrong, no data! PartSys->updateSystem(); // update system properties (dimensions and data pointers) @@ -8076,7 +8076,7 @@ static const char _data_FX_MODE_PARTICLEFIRE[] PROGMEM = "PS Fire@Speed,Intensit by DedeHai (Damian Schneider) */ uint16_t mode_particlepit(void) { - ParticleSystem2D *PartSys = NULL; + ParticleSystem2D *PartSys = nullptr; if (SEGMENT.call == 0) { // initialization if (!initParticleSystem2D(PartSys, 1, 0, true, false)) // init, request one source (actually dont really need one TODO: test if using zero sources also works) @@ -8087,7 +8087,7 @@ uint16_t mode_particlepit(void) { } else PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS - if (PartSys == NULL) + if (PartSys == nullptr) return mode_static(); // something went wrong, no data! PartSys->updateSystem(); // update system properties (dimensions and data pointers) @@ -8146,7 +8146,7 @@ static const char _data_FX_MODE_PARTICLEPIT[] PROGMEM = "PS Ballpit@Speed,Intens by DedeHai (Damian Schneider) */ uint16_t mode_particlewaterfall(void) { - ParticleSystem2D *PartSys = NULL; + ParticleSystem2D *PartSys = nullptr; uint8_t numSprays; uint32_t i = 0; @@ -8172,7 +8172,7 @@ uint16_t mode_particlewaterfall(void) { } else PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS - if (PartSys == NULL) + if (PartSys == nullptr) return mode_static(); // something went wrong, no data! (TODO: ask how to handle this so it always works) // Particle System settings @@ -8218,7 +8218,7 @@ static const char _data_FX_MODE_PARTICLEWATERFALL[] PROGMEM = "PS Waterfall@Spee by DedeHai (Damian Schneider) */ uint16_t mode_particlebox(void) { - ParticleSystem2D *PartSys = NULL; + ParticleSystem2D *PartSys = nullptr; uint32_t i; if (SEGMENT.call == 0) { // initialization @@ -8231,7 +8231,7 @@ uint16_t mode_particlebox(void) { else PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS - if (PartSys == NULL) + if (PartSys == nullptr) return mode_static(); // something went wrong, no data! PartSys->updateSystem(); // update system properties (dimensions and data pointers) @@ -8299,7 +8299,7 @@ static const char _data_FX_MODE_PARTICLEBOX[] PROGMEM = "PS Box@!,Particles,Forc by DedeHai (Damian Schneider) */ uint16_t mode_particleperlin(void) { - ParticleSystem2D *PartSys = NULL; + ParticleSystem2D *PartSys = nullptr; uint32_t i; if (SEGMENT.call == 0) { // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. @@ -8314,7 +8314,7 @@ uint16_t mode_particleperlin(void) { else PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS - if (PartSys == NULL) + if (PartSys == nullptr) return mode_static(); // something went wrong, no data! PartSys->updateSystem(); // update system properties (dimensions and data pointers) @@ -8361,7 +8361,7 @@ static const char _data_FX_MODE_PARTICLEPERLIN[] PROGMEM = "PS Fuzzy Noise@Speed */ #define NUMBEROFSOURCES 8 uint16_t mode_particleimpact(void) { - ParticleSystem2D *PartSys = NULL; + ParticleSystem2D *PartSys = nullptr; uint32_t i = 0; uint8_t MaxNumMeteors; PSsettings2D meteorsettings; @@ -8384,7 +8384,7 @@ uint16_t mode_particleimpact(void) { else PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS - if (PartSys == NULL) + if (PartSys == nullptr) return mode_static(); // something went wrong, no data! (TODO: ask how to handle this so it always works) // Particle System settings @@ -8479,7 +8479,7 @@ static const char _data_FX_MODE_PARTICLEIMPACT[] PROGMEM = "PS Impact@Launches,S by DedeHai (Damian Schneider) */ uint16_t mode_particleattractor(void) { - ParticleSystem2D *PartSys = NULL; + ParticleSystem2D *PartSys = nullptr; PSsettings2D sourcesettings; sourcesettings.asByte = 0b00001100; // PS settings for bounceY, bounceY used for source movement (it always bounces whereas particles do not) PSparticleFlags attractorFlags; @@ -8507,7 +8507,7 @@ uint16_t mode_particleattractor(void) { PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS } - if (PartSys == NULL) + if (PartSys == nullptr) return mode_static(); // something went wrong, no data! // Particle System settings @@ -8575,7 +8575,7 @@ static const char _data_FX_MODE_PARTICLEATTRACTOR[] PROGMEM = "PS Attractor@Mass by DedeHai (Damian Schneider) */ uint16_t mode_particlespray(void) { - ParticleSystem2D *PartSys = NULL; + ParticleSystem2D *PartSys = nullptr; const uint8_t hardness = 200; // collision hardness is fixed if (SEGMENT.call == 0) { // initialization @@ -8592,7 +8592,7 @@ uint16_t mode_particlespray(void) { else PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS - if (PartSys == NULL) + if (PartSys == nullptr) return mode_static(); // something went wrong, no data! // Particle System settings @@ -8661,7 +8661,7 @@ static const char _data_FX_MODE_PARTICLESPRAY[] PROGMEM = "PS Spray@Speed,!,Left by DedeHai (Damian Schneider) */ uint16_t mode_particleGEQ(void) { - ParticleSystem2D *PartSys = NULL; + ParticleSystem2D *PartSys = nullptr; if (SEGMENT.call == 0) { // initialization if (!initParticleSystem2D(PartSys, 1)) @@ -8671,7 +8671,7 @@ uint16_t mode_particleGEQ(void) { } else PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS - if (PartSys == NULL) + if (PartSys == nullptr) return mode_static(); // something went wrong, no data! uint32_t i; @@ -8736,7 +8736,7 @@ static const char _data_FX_MODE_PARTICLEGEQ[] PROGMEM = "PS GEQ 2D@Speed,Intensi */ #define NUMBEROFSOURCES 16 uint16_t mode_particlecenterGEQ(void) { - ParticleSystem2D *PartSys = NULL; + ParticleSystem2D *PartSys = nullptr; uint8_t numSprays; uint32_t i; @@ -8757,7 +8757,7 @@ uint16_t mode_particlecenterGEQ(void) { else PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS - if (PartSys == NULL) + if (PartSys == nullptr) return mode_static(); // something went wrong, no data! PartSys->updateSystem(); // update system properties (dimensions and data pointers) @@ -8805,7 +8805,7 @@ static const char _data_FX_MODE_PARTICLECIRCULARGEQ[] PROGMEM = "PS GEQ Nova@Spe */ #define MAXANGLESTEP 2200 //32767 means 180° uint16_t mode_particleghostrider(void) { - ParticleSystem2D *PartSys = NULL; + ParticleSystem2D *PartSys = nullptr; PSsettings2D ghostsettings; ghostsettings.asByte = 0b0000011; //enable wrapX and wrapY @@ -8823,7 +8823,7 @@ uint16_t mode_particleghostrider(void) { PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS } - if (PartSys == NULL) + if (PartSys == nullptr) return mode_static(); // something went wrong, no data! if (SEGMENT.intensity > 0) { // spiraling @@ -8886,7 +8886,7 @@ static const char _data_FX_MODE_PARTICLEGHOSTRIDER[] PROGMEM = "PS Ghost Rider@S by DedeHai (Damian Schneider) */ uint16_t mode_particleblobs(void) { - ParticleSystem2D *PartSys = NULL; + ParticleSystem2D *PartSys = nullptr; if (SEGMENT.call == 0) { if (!initParticleSystem2D(PartSys, 1, 0, true, true)) //init, request one source, no additional bytes, advanced size & size control (actually dont really need one TODO: test if using zero sources also works) @@ -8901,7 +8901,7 @@ uint16_t mode_particleblobs(void) { else PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS - if (PartSys == NULL) + if (PartSys == nullptr) return mode_static(); // something went wrong, no data! PartSys->updateSystem(); // update system properties (dimensions and data pointers) @@ -8970,7 +8970,7 @@ static const char _data_FX_MODE_PARTICLEBLOBS[] PROGMEM = "PS Blobs@Speed,Blobs, by DedeHai (Damian Schneider) */ uint16_t mode_particleDrip(void) { - ParticleSystem1D *PartSys = NULL; + ParticleSystem1D *PartSys = nullptr; //uint8_t numSprays; if (SEGMENT.call == 0) { // initialization if (!initParticleSystem1D(PartSys, 4)) // init @@ -8982,7 +8982,7 @@ uint16_t mode_particleDrip(void) { else PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS - if (PartSys == NULL) + if (PartSys == nullptr) return mode_static(); // something went wrong, no data! // Particle System settings @@ -9075,7 +9075,7 @@ static const char _data_FX_MODE_PARTICLEDRIP[] PROGMEM = "PS DripDrop@Speed,!,Sp by DedeHai (Damian Schneider) */ uint16_t mode_particlePinball(void) { - ParticleSystem1D *PartSys = NULL; + ParticleSystem1D *PartSys = nullptr; if (SEGMENT.call == 0) { // initialization if (!initParticleSystem1D(PartSys, 1, 128, 0, true)) // init @@ -9090,7 +9090,7 @@ uint16_t mode_particlePinball(void) { else PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS - if (PartSys == NULL) + if (PartSys == nullptr) return mode_static(); // something went wrong, no data! // Particle System settings @@ -9186,7 +9186,7 @@ static const char _data_FX_MODE_PSPINBALL[] PROGMEM = "PS Pinball@Speed,!,Size,B by DedeHai (Damian Schneider) */ uint16_t mode_particleDancingShadows(void) { - ParticleSystem1D *PartSys = NULL; + ParticleSystem1D *PartSys = nullptr; if (SEGMENT.call == 0) { // initialization if (!initParticleSystem1D(PartSys, 1)) // init, one source @@ -9198,7 +9198,7 @@ uint16_t mode_particleDancingShadows(void) { PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS } - if (PartSys == NULL) + if (PartSys == nullptr) return mode_static(); // something went wrong, no data! // Particle System settings @@ -9302,7 +9302,7 @@ static const char _data_FX_MODE_PARTICLEDANCINGSHADOWS[] PROGMEM = "PS Dancing S by DedeHai (Damian Schneider) */ uint16_t mode_particleFireworks1D(void) { - ParticleSystem1D *PartSys = NULL; + ParticleSystem1D *PartSys = nullptr; uint8_t *forcecounter; if (SEGMENT.call == 0) { // initialization @@ -9313,7 +9313,7 @@ uint16_t mode_particleFireworks1D(void) { } else PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS - if (PartSys == NULL) + if (PartSys == nullptr) return mode_static(); // something went wrong, no data! // Particle System settings @@ -9413,7 +9413,7 @@ static const char _data_FX_MODE_PS_FIREWORKS1D[] PROGMEM = "PS Fireworks 1D@Grav by DedeHai (Damian Schneider) */ uint16_t mode_particleSparkler(void) { - ParticleSystem1D *PartSys = NULL; + ParticleSystem1D *PartSys = nullptr; uint32_t numSparklers; PSsettings1D sparklersettings; sparklersettings.asByte = 0; // PS settings for sparkler (set below) @@ -9423,7 +9423,7 @@ uint16_t mode_particleSparkler(void) { return mode_static(); // allocation failed or is single pixel } else PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS - if (PartSys == NULL) + if (PartSys == nullptr) return mode_static(); // something went wrong, no data! // Particle System settings @@ -9485,7 +9485,7 @@ static const char _data_FX_MODE_PS_SPARKLER[] PROGMEM = "PS Sparkler@Move,!,Satu by DedeHai (Damian Schneider) */ uint16_t mode_particleHourglass(void) { - ParticleSystem1D *PartSys = NULL; + ParticleSystem1D *PartSys = nullptr; constexpr int positionOffset = PS_P_RADIUS_1D / 2;; // resting position offset bool* direction; uint32_t* settingTracker; @@ -9497,7 +9497,7 @@ uint16_t mode_particleHourglass(void) { } else PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS - if (PartSys == NULL) + if (PartSys == nullptr) return mode_static(); // something went wrong, no data! // Particle System settings @@ -9619,7 +9619,7 @@ static const char _data_FX_MODE_PS_HOURGLASS[] PROGMEM = "PS Hourglass@Interval, by DedeHai (Damian Schneider) */ uint16_t mode_particle1Dspray(void) { - ParticleSystem1D *PartSys = NULL; + ParticleSystem1D *PartSys = nullptr; if (SEGMENT.call == 0) { // initialization if (!initParticleSystem1D(PartSys, 1)) @@ -9630,7 +9630,7 @@ uint16_t mode_particle1Dspray(void) { } else PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS - if (PartSys == NULL) + if (PartSys == nullptr) return mode_static(); // something went wrong, no data! // Particle System settings @@ -9671,7 +9671,7 @@ static const char _data_FX_MODE_PS_1DSPRAY[] PROGMEM = "PS Spray 1D@Speed(+/-),! by DedeHai (Damian Schneider) */ uint16_t mode_particleBalance(void) { - ParticleSystem1D *PartSys = NULL; + ParticleSystem1D *PartSys = nullptr; uint32_t i; if (SEGMENT.call == 0) { // initialization @@ -9684,7 +9684,7 @@ uint16_t mode_particleBalance(void) { } else PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS - if (PartSys == NULL) + if (PartSys == nullptr) return mode_static(); // something went wrong, no data! // Particle System settings @@ -9744,7 +9744,7 @@ Uses palette for particle color by DedeHai (Damian Schneider) */ uint16_t mode_particleChase(void) { - ParticleSystem1D *PartSys = NULL; + ParticleSystem1D *PartSys = nullptr; if (SEGMENT.call == 0) { // initialization if (!initParticleSystem1D(PartSys, 1, 255, 3, true)) // init return mode_static(); // allocation failed or is single pixel @@ -9754,7 +9754,7 @@ uint16_t mode_particleChase(void) { } else PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS - if (PartSys == NULL) + if (PartSys == nullptr) return mode_static(); // something went wrong, no data! // Particle System settings @@ -9809,8 +9809,7 @@ static const char _data_FX_MODE_PS_CHASE[] PROGMEM = "PS Chase@!,Density,Size,Hu by DedeHai (Damian Schneider) */ uint16_t mode_particleStarburst(void) { - ParticleSystem1D *PartSys = NULL; - uint32_t i; + ParticleSystem1D *PartSys = nullptr; if (SEGMENT.call == 0) { // initialization if (!initParticleSystem1D(PartSys, 1, 200, 0, true)) // init @@ -9822,7 +9821,7 @@ uint16_t mode_particleStarburst(void) { } else PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS - if (PartSys == NULL) + if (PartSys == nullptr) return mode_static(); // something went wrong, no data! // Particle System settings @@ -9870,7 +9869,7 @@ static const char _data_FX_MODE_PS_STARBURST[] PROGMEM = "PS Starburst@Chance,Fr by DedeHai (Damian Schneider) */ uint16_t mode_particle1DGEQ(void) { - ParticleSystem1D *PartSys = NULL; + ParticleSystem1D *PartSys = nullptr; uint32_t numSources; uint32_t i; @@ -9880,7 +9879,7 @@ uint16_t mode_particle1DGEQ(void) { } else PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS - if (PartSys == NULL) + if (PartSys == nullptr) return mode_static(); // something went wrong, no data! // Particle System settings @@ -9945,7 +9944,7 @@ static const char _data_FX_MODE_PS_1D_GEQ[] PROGMEM = "PS GEQ 1D@Speed,!,Size,Bl by DedeHai (Damian Schneider) */ uint16_t mode_particleFire1D(void) { - ParticleSystem1D *PartSys = NULL; + ParticleSystem1D *PartSys = nullptr; if (SEGMENT.call == 0) { // initialization if (!initParticleSystem1D(PartSys, 5)) // init @@ -9955,7 +9954,7 @@ uint16_t mode_particleFire1D(void) { } else PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS - if (PartSys == NULL) + if (PartSys == nullptr) return mode_static(); // something went wrong, no data! // Particle System settings @@ -10011,7 +10010,7 @@ static const char _data_FX_MODE_PS_FIRE1D[] PROGMEM = "PS Fire 1D@!,!,Cooling,Bl by DedeHai (Damian Schneider) */ uint16_t mode_particle1Dsonicstream(void) { - ParticleSystem1D *PartSys = NULL; + ParticleSystem1D *PartSys = nullptr; if (SEGMENT.call == 0) { // initialization if (!initParticleSystem1D(PartSys, 1, 255, 0, true)) // init, no additional data needed @@ -10024,7 +10023,7 @@ uint16_t mode_particle1Dsonicstream(void) { } else PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS - if (PartSys == NULL) + if (PartSys == nullptr) return mode_static(); // something went wrong, no data! // Particle System settings diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index de87072370..a7b2f8b6f8 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -42,8 +42,8 @@ ParticleSystem2D::ParticleSystem2D(uint32_t width, uint32_t height, uint32_t num numParticles = numberofparticles; // number of particles allocated in init availableParticles = 0; // let the memory manager assign fractionOfParticlesUsed = 255; // use all particles by default, usedParticles is updated in updatePSpointers() - advPartProps = NULL; //make sure we start out with null pointers (just in case memory was not cleared) - advPartSize = NULL; + advPartProps = nullptr; //make sure we start out with null pointers (just in case memory was not cleared) + advPartSize = nullptr; updatePSpointers(isadvanced, sizecontrol); // set the particle and sources pointer (call this before accessing sprays or particles) setMatrixSize(width, height); setWallHardness(255); // set default wall hardness to max @@ -234,7 +234,7 @@ int32_t ParticleSystem2D::angleEmit(PSsource &emitter, const uint16_t angle, con // 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 enabled, it will never bounce at the top and killoutofbounds is not applied over the top void ParticleSystem2D::particleMoveUpdate(PSparticle &part, PSparticleFlags &partFlags, PSsettings2D *options, PSadvancedParticle *advancedproperties) { - if (options == NULL) + if (options == nullptr) options = &particlesettings; //use PS system settings by default if (part.ttl > 0) { @@ -324,7 +324,7 @@ void ParticleSystem2D::fireParticleupdate() { // update advanced particle size control, returns false if particle shrinks to 0 size bool ParticleSystem2D::updateSize(PSadvancedParticle *advprops, PSsizeControl *advsize) { - if (advsize == NULL) // safety check + if (advsize == nullptr) // safety check return false; // grow/shrink particle int32_t newsize = advprops->size; @@ -376,7 +376,7 @@ bool ParticleSystem2D::updateSize(PSadvancedParticle *advprops, PSsizeControl *a // calculate x and y size for asymmetrical particles (advanced size control) void ParticleSystem2D::getParticleXYsize(PSadvancedParticle *advprops, PSsizeControl *advsize, uint32_t &xsize, uint32_t &ysize) { - if (advsize == NULL) // if advsize is valid, also advanced properties pointer is valid (handled by updatePSpointers()) + if (advsize == nullptr) // if advsize is valid, also advanced properties pointer is valid (handled by updatePSpointers()) return; int32_t size = advprops->size; int32_t asymdir = advsize->asymdir; @@ -437,7 +437,7 @@ void ParticleSystem2D::applyForce(PSparticle &part, const int8_t xforce, const i // apply a force in x,y direction to individual particle using advanced particle properties void ParticleSystem2D::applyForce(const uint32_t particleindex, const int8_t xforce, const int8_t yforce) { - if (advPartProps == NULL) + if (advPartProps == nullptr) return; // no advanced properties available applyForce(particles[particleindex], xforce, yforce, advPartProps[particleindex].forcecounter); } @@ -466,7 +466,7 @@ void ParticleSystem2D::applyAngleForce(PSparticle &part, const int8_t force, con } void ParticleSystem2D::applyAngleForce(const uint32_t particleindex, const int8_t force, const uint16_t angle) { - if (advPartProps == NULL) + if (advPartProps == nullptr) return; // no advanced properties available applyAngleForce(particles[particleindex], force, angle, advPartProps[particleindex].forcecounter); } @@ -522,7 +522,7 @@ void ParticleSystem2D::applyFriction(const int32_t coefficient) { // attracts a particle to an attractor particle using the inverse square-law void ParticleSystem2D::pointAttractor(const uint32_t particleindex, PSparticle &attractor, const uint8_t strength, const bool swallow) { - if (advPartProps == NULL) + if (advPartProps == nullptr) return; // no advanced properties available // Calculate the distance between the particle and the attractor @@ -557,7 +557,6 @@ void ParticleSystem2D::ParticleSys_render() { CRGB baseRGB; uint32_t brightness; // particle brightness, fades if dying static bool useAdditiveTransfer = false; // use add instead of set for buffer transferring (must persist between calls) - //bool isNonFadeTransition = (SEGMENT.isInTransition() && blendingStyle != BLEND_STYLE_FADE); bool isNonFadeTransition = (pmem->inTransition || pmem->finalTransfer) && blendingStyle != BLEND_STYLE_FADE; bool isOverlay = segmentIsOverlay(); @@ -566,17 +565,21 @@ void ParticleSystem2D::ParticleSys_render() { int32_t smearamount = smearBlur; if(pmem->inTransition == effectID && blendingStyle == BLEND_STYLE_FADE) { // FX transition and this is the new FX: fade blur amount but only if using fade style motionbluramount = globalBlur + (((motionbluramount - globalBlur) * (int)SEGMENT.progress()) >> 16); // fade from old blur to new blur during transitions - smearamount = globalSmear + (((smearamount - globalSmear) * (int)SEGMENT.progress()) >> 16); + smearamount = globalSmear + (((smearamount - globalSmear) * (int)SEGMENT.progress()) >> 16); } globalBlur = motionbluramount; globalSmear = smearamount; + if(isOverlay) { + globalSmear = 0; // do not apply smear or blur in overlay or it turns everything into a blurry mess + globalBlur = 0; + } // handle blurring and framebuffer update if (framebuffer) { if(!pmem->inTransition) useAdditiveTransfer = false; // additive transfer is only usd in transitions (or in overlay) // handle buffer blurring or clearing - bool bufferNeedsUpdate = (!pmem->inTransition || pmem->inTransition == effectID || isNonFadeTransition); // not a transition; or new FX or not fading style: update buffer (blur, or clear) + bool bufferNeedsUpdate = !pmem->inTransition || pmem->inTransition == effectID || isNonFadeTransition; // not a transition; or new FX or not fading style: update buffer (blur, or clear) if(bufferNeedsUpdate) { bool loadfromSegment = !renderSolo || isNonFadeTransition; if (globalBlur > 0 || globalSmear > 0) { // blurring active: if not a transition or is newFX, read data from segment before blurring (old FX can render to it afterwards) @@ -1194,8 +1197,8 @@ ParticleSystem1D::ParticleSystem1D(uint32_t length, uint32_t numberofparticles, numParticles = numberofparticles; // number of particles allocated in init availableParticles = 0; // let the memory manager assign fractionOfParticlesUsed = 255; // use all particles by default - advPartProps = NULL; //make sure we start out with null pointers (just in case memory was not cleared) - //advPartSize = NULL; + advPartProps = nullptr; //make sure we start out with null pointers (just in case memory was not cleared) + //advPartSize = nullptr; updatePSpointers(isadvanced); // set the particle and sources pointer (call this before accessing sprays or particles) setSize(length); setWallHardness(255); // set default wall hardness to max @@ -1346,7 +1349,7 @@ int32_t ParticleSystem1D::sprayEmit(const PSsource1D &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 and killoutofbounds is not applied over the top void ParticleSystem1D::particleMoveUpdate(PSparticle1D &part, PSparticleFlags1D &partFlags, PSsettings1D *options, PSadvancedParticle1D *advancedproperties) { - if (options == NULL) + if (options == nullptr) options = &particlesettings; // use PS system settings by default if (part.ttl > 0) { @@ -1472,7 +1475,9 @@ void ParticleSystem1D::applyFriction(int32_t coefficient) { void ParticleSystem1D::ParticleSys_render() { CRGB baseRGB; uint32_t brightness; // particle brightness, fades if dying - static bool useAdditiveTransfer; // use add instead of set for buffer transferring + // bool useAdditiveTransfer; // use add instead of set for buffer transferring + bool isNonFadeTransition = (pmem->inTransition || pmem->finalTransfer) && blendingStyle != BLEND_STYLE_FADE; + bool isOverlay = segmentIsOverlay(); // update global blur (used for blur transitions) int32_t motionbluramount = motionBlur; @@ -1485,14 +1490,13 @@ void ParticleSystem1D::ParticleSys_render() { globalSmear = smearamount; if (framebuffer) { - if(segmentIsOverlay()) useAdditiveTransfer = true; // overlay rendering - else useAdditiveTransfer = false; // handle buffer blurring or clearing - bool bufferNeedsUpdate = (!pmem->inTransition || pmem->inTransition == effectID); // not a transition; or new FX: update buffer (blur, or clear) + bool bufferNeedsUpdate = !pmem->inTransition || pmem->inTransition == effectID || isNonFadeTransition; // not a transition; or new FX: update buffer (blur, or clear) if(bufferNeedsUpdate) { + bool loadfromSegment = !renderSolo || isNonFadeTransition; if (globalBlur > 0 || globalSmear > 0) { // blurring active: if not a transition or is newFX, read data from segment before blurring (old FX can render to it afterwards) for (int32_t x = 0; x <= maxXpixel; x++) { - if (!renderSolo) // sharing the framebuffer with another segment: read buffer back from segment + if (loadfromSegment) // sharing the framebuffer with another segment: read buffer back from segment framebuffer[x] = SEGMENT.getPixelColor(x); // copy to local buffer fast_color_scale(framebuffer[x], motionBlur); } @@ -1549,7 +1553,8 @@ void ParticleSystem1D::ParticleSys_render() { } } // transfer local buffer back to segment (if available) - transferBuffer(maxXpixel + 1, 0, useAdditiveTransfer); + if (pmem->inTransition != effectID || isNonFadeTransition) + transferBuffer(maxXpixel + 1, 0, isOverlay); } // calculate pixel positions and brightness distribution and render the particle to local buffer or global buffer @@ -1773,7 +1778,7 @@ void ParticleSystem1D::collideParticles(PSparticle1D &particle1, const PSparticl void ParticleSystem1D::updateSystem(void) { setSize(SEGMENT.vLength()); // update size updateRenderingBuffer(SEGMENT.vLength(), true, false); // update rendering buffer (segment size can change at any time) - updatePSpointers(advPartProps != NULL); + updatePSpointers(advPartProps != nullptr); setUsedParticles(fractionOfParticlesUsed); // update used particles based on percentage (can change during transitions, execute each frame for code simplicity) if (partMemList.size() == 1) // if number of vector elements is one, this is the only system renderSolo = true; @@ -1852,10 +1857,10 @@ bool allocateParticleSystemMemory1D(const uint32_t numparticles, const uint32_t return false; // not enough memory, function ensures a minimum of numparticles are avialable // functions above make sure these are a multiple of 4 bytes (to avoid alignment issues) requiredmemory += sizeof(PSparticleFlags1D) * numparticles; - if (isadvanced) - requiredmemory += sizeof(PSadvancedParticle1D) * numparticles; requiredmemory += sizeof(PSsource1D) * numsources; requiredmemory += additionalbytes; + if (isadvanced) + requiredmemory += sizeof(PSadvancedParticle1D) * numparticles; return(SEGMENT.allocateData(requiredmemory)); } @@ -2080,7 +2085,7 @@ void* particleMemoryManager(const uint32_t requestedParticles, size_t structSize PSPRINTLN("new effect"); newAvailable = (maxParticles * progress) >> 16; // update total particles available to this PS (newAvailable is guaranteed to be smaller than maxParticles) if(newAvailable < 2) newAvailable = 2; // give 2 particle minimum (some FX may crash with less as they do i+1 access) - if(newAvailable > numParticlesUsed) newAvailable = numParticlesUsed; // limit to number of particles used for FX using a small amount, do not move the pointer anymore (will be set to base in final handover) + if(newAvailable > numParticlesUsed) newAvailable = numParticlesUsed; // limit to number of particles used, do not move the pointer anymore (will be set to base in final handover) uint32_t bufferoffset = (maxParticles - 1) - newAvailable; // offset to new effect particles (in particle structs, not bytes) if(bufferoffset < maxParticles) // safety check buffer = (void*)((uint8_t*)buffer + bufferoffset * structSize); // new effect gets the end of the buffer @@ -2116,7 +2121,9 @@ void* particleMemoryManager(const uint32_t requestedParticles, size_t structSize } } // kill unused particles so they do not re-appear when transitioning to next FX - //TODO: can this be done in the handover function? + //TODO: should this be done in the handover function? maybe with a "cleanup" parameter? + //TODO2: the memmove above should be done here (or in handover function): it should copy all alive particles to the beginning of the buffer (to TTL=0 particles maybe?) + // -> currently when moving form blobs to ballpit particles disappear #ifndef WLED_DISABLE_PARTICLESYSTEM2D if (structSize == sizeof(PSparticle)) { // 2D particle PSparticle *particles = (PSparticle*)buffer; From f7ca8fa93e26f0fe3664f9db1036c03bdbeab93a Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sun, 26 Jan 2025 12:40:57 +0100 Subject: [PATCH 211/219] merge fixes --- platformio.ini | 2 +- wled00/FX.cpp | 2 +- wled00/FX.h | 1 - wled00/FX_fcn.cpp | 6 ------ 4 files changed, 2 insertions(+), 9 deletions(-) diff --git a/platformio.ini b/platformio.ini index ef2a1daeab..8cf7fbd99b 100644 --- a/platformio.ini +++ b/platformio.ini @@ -543,7 +543,7 @@ build_flags = ${common.build_flags} ${esp32c3.build_flags} -D WLED_RELEASE_NAME= -DLOLIN_WIFI_FIX ; seems to work much better with this -DARDUINO_USB_CDC_ON_BOOT=1 ;; for virtual CDC USB ;-DARDUINO_USB_CDC_ON_BOOT=0 ;; for serial-to-USB chip -upload_speed = 460800 ; 115200 230400 460800 +upload_speed = 460800 build_unflags = ${common.build_unflags} lib_deps = ${esp32c3.lib_deps} diff --git a/wled00/FX.cpp b/wled00/FX.cpp index ea75fda049..454bd2568f 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -9847,7 +9847,7 @@ uint16_t mode_particleStarburst(void) { } } //shrink all particles - for (i = 0; i < PartSys->usedParticles; i++) { + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { if (PartSys->advPartProps[i].size) PartSys->advPartProps[i].size--; if (PartSys->advPartProps[i].sat < 251) diff --git a/wled00/FX.h b/wled00/FX.h index d0f9293dcc..9c03441763 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -497,7 +497,6 @@ typedef struct Segment { #ifndef WLED_DISABLE_MODE_BLEND tmpsegd_t _segT; // previous segment environment uint8_t _modeT; // previous mode/effect - uint8_t _palette; // previous palette #else uint32_t _colorT[NUM_COLORS]; #endif diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index 9e8bfffdad..51fae3b1a6 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -474,12 +474,6 @@ void Segment::loadOldPalette(void) { loadPalette(_currentPalette, _t->_palTid); } -// loads palette of the old FX during transitions (used by particle system) -void Segment::loadOldPalette(void) { - if(isInTransition()) - loadPalette(_currentPalette, _t->_palTid); -} - // relies on WS2812FX::service() to call it for each frame void Segment::handleRandomPalette() { // is it time to generate a new palette? From a6124fb968aa132de82dfed4311c0f831a9f7dc0 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Tue, 28 Jan 2025 08:24:30 +0100 Subject: [PATCH 212/219] added single pixel particle rendering for 2D system, adjusted FX to work with it --- wled00/FX.cpp | 14 +-- wled00/FXparticleSystem.cpp | 189 ++++++++++++++++++++---------------- wled00/FXparticleSystem.h | 4 +- 3 files changed, 112 insertions(+), 95 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 454bd2568f..ab52171bdc 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -8235,10 +8235,10 @@ uint16_t mode_particlebox(void) { return mode_static(); // something went wrong, no data! PartSys->updateSystem(); // update system properties (dimensions and data pointers) - + PartSys->setParticleSize(SEGMENT.custom3<<3); PartSys->setWallHardness(min(SEGMENT.custom2, (uint8_t)200)); // wall hardness is 200 or more PartSys->enableParticleCollisions(true, max(2, (int)SEGMENT.custom2)); // enable collisions and set particle collision hardness - PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 5, 153)); // 2% - 60% + PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 2, 153)); // 1% - 60% // add in new particles if amount has changed for (i = 0; i < PartSys->usedParticles; i++) { if (PartSys->particles[i].ttl < 260) { // initialize handed over particles and dead particles @@ -8284,14 +8284,14 @@ uint16_t mode_particlebox(void) { PartSys->applyForce(xgravity, ygravity); } - if (SEGMENT.call % (32 - SEGMENT.custom3) == 0) - PartSys->applyFriction(2); + if ((SEGMENT.call & 0x0F) == 0) // every 16th frame + PartSys->applyFriction(1); PartSys->update(); // update and render return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEBOX[] PROGMEM = "PS Box@!,Particles,Force,Hardness,Friction,Random,Washing Machine,Sloshing;;!;2;pal=53,ix=50,o1=1"; +static const char _data_FX_MODE_PARTICLEBOX[] PROGMEM = "PS Box@!,Particles,Force,Hardness,Size,Random,Washing Machine,Sloshing;;!;2;pal=53,ix=50,c3=1,o1=1"; /* Fuzzy Noise: Perlin noise 'gravity' mapping as in particles on 'noise hills' viewed from above @@ -8392,7 +8392,7 @@ uint16_t mode_particleimpact(void) { PartSys->setWrapX(SEGMENT.check1); PartSys->setBounceX(SEGMENT.check2); PartSys->setMotionBlur(SEGMENT.custom3<<3); - uint8_t hardness = map(SEGMENT.custom2, 0, 255, 127, 255); + uint8_t hardness = map(SEGMENT.custom2, 0, 255, PS_P_MINSURFACEHARDNESS - 2, 255); PartSys->setWallHardness(hardness); PartSys->enableParticleCollisions(SEGMENT.check3, hardness); // enable collisions and set particle collision hardness MaxNumMeteors = min(PartSys->numSources, (uint32_t)NUMBEROFSOURCES); @@ -8470,7 +8470,7 @@ uint16_t mode_particleimpact(void) { return FRAMETIME; } #undef NUMBEROFSOURCES -static const char _data_FX_MODE_PARTICLEIMPACT[] PROGMEM = "PS Impact@Launches,Size,Force,Hardness,Blur,Cylinder,Walls,Collide;;!;2;pal=0,sx=32,ix=85,c1=70,c2=130,c3=0,o3=1"; +static const char _data_FX_MODE_PARTICLEIMPACT[] PROGMEM = "PS Impact@Launches,!,Force,Hardness,Blur,Cylinder,Walls,Collide;;!;2;pal=0,sx=32,ix=85,c1=70,c2=130,c3=0,o3=1"; /* Particle Attractor, a particle attractor sits in the matrix center, a spray bounces around and seeds particles diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index a7b2f8b6f8..1dfaf1cacb 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -49,7 +49,7 @@ ParticleSystem2D::ParticleSystem2D(uint32_t width, uint32_t height, uint32_t num setWallHardness(255); // set default wall hardness to max setWallRoughness(0); // smooth walls by default setGravity(0); //gravity disabled by default - setParticleSize(0); // minimum size by default + setParticleSize(1); // 2x2 rendering size by default motionBlur = 0; //no fading by default smearBlur = 0; //no smearing by default emitIndex = 0; @@ -156,7 +156,7 @@ void ParticleSystem2D::setColorByAge(bool enable) { } void ParticleSystem2D::setMotionBlur(uint8_t bluramount) { - if (particlesize == 0) // only allow motion blurring on default particle size or advanced size (cannot combine motion blur with normal blurring used for particlesize, would require another buffer) + if (particlesize < 2) // only allow motion blurring on default particle sizes or advanced size (cannot combine motion blur with normal blurring used for particlesize, would require another buffer) motionBlur = bluramount; } @@ -168,8 +168,13 @@ void ParticleSystem2D::setSmearBlur(uint8_t bluramount) { // render size using smearing (see blur function) void ParticleSystem2D::setParticleSize(uint8_t size) { particlesize = size; - particleHardRadius = PS_P_MINHARDRADIUS + (particlesize >> 1); // radius used for wall collisions & particle collisions - motionBlur = 0; // disable motion blur if particle size is set + particleHardRadius = PS_P_MINHARDRADIUS; + if(particlesize > 1) { + particleHardRadius += particlesize >> 1; // radius used for wall collisions & particle collisions + motionBlur = 0; // disable motion blur if particle size is set + } + else if (particlesize == 0) + particleHardRadius = particleHardRadius >> 1; // single pixel particles have half the radius } // enable/disable gravity, optionally, set the force (force=8 is default) can be -127 to +127, 0 is disable @@ -599,7 +604,7 @@ void ParticleSystem2D::ParticleSys_render() { } } // handle buffer for global large particle size rendering - if(particlesize > 0 && pmem->inTransition) { // if particle size is used by FX we need a clean buffer + if(particlesize > 1 && pmem->inTransition) { // if particle size is used by FX we need a clean buffer if(bufferNeedsUpdate && !globalBlur) { // transfer without adding if buffer was not cleared above (happens if this is the new FX and other FX does not use blurring) useAdditiveTransfer = false; // no blurring and big size particle FX is the new FX (rendered first after clearing), can just render normally } @@ -642,7 +647,7 @@ void ParticleSystem2D::ParticleSys_render() { renderParticle(i, brightness, baseRGB, particlesettings.wrapX, particlesettings.wrapY); } - if (particlesize > 0) { + if (particlesize > 1) { uint32_t passes = particlesize / 64 + 1; // number of blur passes, four passes max uint32_t bluramount = particlesize; uint32_t bitshift = 0; @@ -672,6 +677,17 @@ void ParticleSystem2D::ParticleSys_render() { // calculate pixel positions and brightness distribution and render the particle to local buffer or global buffer void ParticleSystem2D::renderParticle(const uint32_t particleindex, const uint32_t brightness, const CRGB& color, const bool wrapX, const bool wrapY) { + if(particlesize == 0 && !advPartProps) { // single pixel rendering + uint32_t x = particles[particleindex].x >> PS_P_RADIUS_SHIFT; + uint32_t y = particles[particleindex].y >> PS_P_RADIUS_SHIFT; + if (x <= (uint32_t)maxXpixel && y <= (uint32_t)maxYpixel) { + if (framebuffer) + fast_color_add(framebuffer[x + (maxYpixel - y) * (maxXpixel + 1)], color, brightness); + else + SEGMENT.addPixelColorXY(x, maxYpixel - y, color.scale8(brightness), true); + } + return; + } int32_t pxlbrightness[4]; // brightness values for the four pixels representing a particle int32_t pixco[4][2]; // physical pixel coordinates of the four pixels a particle is rendered to. x,y pairs bool pixelvalid[4] = {true, true, true, true}; // is set to false if pixel is out of bounds @@ -781,7 +797,7 @@ void ParticleSystem2D::renderParticle(const uint32_t particleindex, const uint32 SEGMENT.addPixelColorXY(xfb, maxYpixel - yfb, renderbuffer[xrb + yrb * 10],true); } } - } else { // standard rendering + } else { // standard rendering (2x2 pixels) // check for out of frame pixels and wrap them if required: x,y is bottom left pixel coordinate of the particle if (x < 0) { // left pixels out of frame if (wrapX) { // wrap x to the other side if required @@ -1203,7 +1219,7 @@ ParticleSystem1D::ParticleSystem1D(uint32_t length, uint32_t numberofparticles, setSize(length); setWallHardness(255); // set default wall hardness to max setGravity(0); //gravity disabled by default - setParticleSize(0); // minimum size by default + setParticleSize(0); // 1 pixel size by default motionBlur = 0; //no fading by default smearBlur = 0; //no smearing by default emitIndex = 0; @@ -1571,99 +1587,100 @@ void ParticleSystem1D::renderParticle(const uint32_t particleindex, const uint32 else SEGMENT.addPixelColor(x, color.scale8((uint8_t)brightness), true); } + return; } - else { //render larger particles - bool pxlisinframe[2] = {true, true}; - int32_t pxlbrightness[2]; - int32_t pixco[2]; // physical pixel coordinates of the two pixels representing a particle + //render larger particles + bool pxlisinframe[2] = {true, true}; + int32_t pxlbrightness[2]; + int32_t pixco[2]; // physical pixel coordinates of the two pixels representing a particle - // add half a radius as the rendering algorithm always starts at the bottom left, this leaves things positive, so shifts can be used, then shift coordinate by a full pixel (x-- below) - int32_t xoffset = particles[particleindex].x + PS_P_HALFRADIUS_1D; - int32_t dx = xoffset & (PS_P_RADIUS_1D - 1); //relativ particle position in subpixel space, modulo replaced with bitwise AND - int32_t x = xoffset >> PS_P_RADIUS_SHIFT_1D; // divide by PS_P_RADIUS, bitshift of negative number stays negative -> checking below for x < 0 works (but does not when using division) + // add half a radius as the rendering algorithm always starts at the bottom left, this leaves things positive, so shifts can be used, then shift coordinate by a full pixel (x-- below) + int32_t xoffset = particles[particleindex].x + PS_P_HALFRADIUS_1D; + int32_t dx = xoffset & (PS_P_RADIUS_1D - 1); //relativ particle position in subpixel space, modulo replaced with bitwise AND + int32_t x = xoffset >> PS_P_RADIUS_SHIFT_1D; // divide by PS_P_RADIUS, bitshift of negative number stays negative -> checking below for x < 0 works (but does not when using division) - // set the raw pixel coordinates - pixco[1] = x; // right pixel - x--; // shift by a full pixel here, this is skipped above to not do -1 and then +1 - pixco[0] = x; // left pixel + // set the raw pixel coordinates + pixco[1] = x; // right pixel + x--; // shift by a full pixel here, this is skipped above to not do -1 and then +1 + pixco[0] = x; // left pixel - //calculate the brightness values for both pixels using linear interpolation (note: in standard rendering out of frame pixels could be skipped but if checks add more clock cycles over all) - pxlbrightness[0] = (((int32_t)PS_P_RADIUS_1D - dx) * brightness) >> PS_P_SURFACE_1D; - pxlbrightness[1] = (dx * brightness) >> PS_P_SURFACE_1D; + //calculate the brightness values for both pixels using linear interpolation (note: in standard rendering out of frame pixels could be skipped but if checks add more clock cycles over all) + pxlbrightness[0] = (((int32_t)PS_P_RADIUS_1D - dx) * brightness) >> PS_P_SURFACE_1D; + pxlbrightness[1] = (dx * brightness) >> PS_P_SURFACE_1D; - // check if particle has advanced size properties and buffer is available - if (advPartProps && advPartProps[particleindex].size > 1) { - if (renderbuffer) { - memset(renderbuffer, 0, 10 * sizeof(CRGB)); // clear the buffer, renderbuffer is 10 pixels - } - else - return; // cannot render advanced particles without buffer - - //render particle to a bigger size - //particle size to pixels: 2 - 63 is 4 pixels, < 128 is 6pixels, < 192 is 8 pixels, bigger is 10 pixels - //first, render the pixel to the center of the renderbuffer, then apply 1D blurring - fast_color_add(renderbuffer[4], color, pxlbrightness[0]); - fast_color_add(renderbuffer[5], color, pxlbrightness[1]); - uint32_t rendersize = 2; // initialize render size, minimum is 4 pixels, it is incremented int he loop below to start with 4 - uint32_t offset = 4; // offset to zero coordinate to write/read data in renderbuffer (actually needs to be 3, is decremented in the loop below) - uint32_t blurpasses = size/64 + 1; // number of blur passes depends on size, four passes max - uint32_t bitshift = 0; - for (uint32_t i = 0; i < blurpasses; i++) { - if (i == 2) //for the last two passes, use higher amount of blur (results in a nicer brightness gradient with soft edges) - bitshift = 1; - rendersize += 2; - offset--; - blur1D(renderbuffer, rendersize, size << bitshift, offset); - size = size > 64 ? size - 64 : 0; - } + // check if particle has advanced size properties and buffer is available + if (advPartProps && advPartProps[particleindex].size > 1) { + if (renderbuffer) { + memset(renderbuffer, 0, 10 * sizeof(CRGB)); // clear the buffer, renderbuffer is 10 pixels + } + else + return; // cannot render advanced particles without buffer - // calculate origin coordinates to render the particle to in the framebuffer - uint32_t xfb_orig = x - (rendersize>>1) + 1 - offset; //note: using uint is fine - uint32_t xfb; // coordinates in frame buffer to write to note: by making this uint, only overflow has to be checked - - // transfer particle renderbuffer to framebuffer - for (uint32_t xrb = offset; xrb < rendersize+offset; xrb++) { - xfb = xfb_orig + xrb; - if (xfb > (uint32_t)maxXpixel) { - if (wrap) { // wrap x to the other side if required - if (xfb > (uint32_t)maxXpixel << 1) // xfb is "negative" - xfb = (maxXpixel + 1) + (int32_t)xfb; // this always overflows to within bounds - else - xfb = xfb % (maxXpixel + 1); // note: without the above "negative" check, this works only for powers of 2 - } + //render particle to a bigger size + //particle size to pixels: 2 - 63 is 4 pixels, < 128 is 6pixels, < 192 is 8 pixels, bigger is 10 pixels + //first, render the pixel to the center of the renderbuffer, then apply 1D blurring + fast_color_add(renderbuffer[4], color, pxlbrightness[0]); + fast_color_add(renderbuffer[5], color, pxlbrightness[1]); + uint32_t rendersize = 2; // initialize render size, minimum is 4 pixels, it is incremented int he loop below to start with 4 + uint32_t offset = 4; // offset to zero coordinate to write/read data in renderbuffer (actually needs to be 3, is decremented in the loop below) + uint32_t blurpasses = size/64 + 1; // number of blur passes depends on size, four passes max + uint32_t bitshift = 0; + for (uint32_t i = 0; i < blurpasses; i++) { + if (i == 2) //for the last two passes, use higher amount of blur (results in a nicer brightness gradient with soft edges) + bitshift = 1; + rendersize += 2; + offset--; + blur1D(renderbuffer, rendersize, size << bitshift, offset); + size = size > 64 ? size - 64 : 0; + } + + // calculate origin coordinates to render the particle to in the framebuffer + uint32_t xfb_orig = x - (rendersize>>1) + 1 - offset; //note: using uint is fine + uint32_t xfb; // coordinates in frame buffer to write to note: by making this uint, only overflow has to be checked + + // transfer particle renderbuffer to framebuffer + for (uint32_t xrb = offset; xrb < rendersize+offset; xrb++) { + xfb = xfb_orig + xrb; + if (xfb > (uint32_t)maxXpixel) { + if (wrap) { // wrap x to the other side if required + if (xfb > (uint32_t)maxXpixel << 1) // xfb is "negative" + xfb = (maxXpixel + 1) + (int32_t)xfb; // this always overflows to within bounds else - continue; + xfb = xfb % (maxXpixel + 1); // note: without the above "negative" check, this works only for powers of 2 } - if (framebuffer) - fast_color_add(framebuffer[xfb], renderbuffer[xrb]); else - SEGMENT.addPixelColor(xfb, renderbuffer[xrb]); + continue; } + if (framebuffer) + fast_color_add(framebuffer[xfb], renderbuffer[xrb]); + else + SEGMENT.addPixelColor(xfb, renderbuffer[xrb]); } - else { // standard rendering (2 pixels per particle) - // check if any pixels are out of frame - if (x < 0) { // left pixels out of frame - if (wrap) // wrap x to the other side if required - pixco[0] = maxXpixel; - else - pxlisinframe[0] = false; // pixel is out of matrix boundaries, do not render - } - else if (pixco[1] > (int32_t)maxXpixel) { // right pixel, only has to be checkt if left pixel did not overflow - if (wrap) // wrap y to the other side if required - pixco[1] = 0; + } + else { // standard rendering (2 pixels per particle) + // check if any pixels are out of frame + if (x < 0) { // left pixels out of frame + if (wrap) // wrap x to the other side if required + pixco[0] = maxXpixel; + else + pxlisinframe[0] = false; // pixel is out of matrix boundaries, do not render + } + else if (pixco[1] > (int32_t)maxXpixel) { // right pixel, only has to be checkt if left pixel did not overflow + if (wrap) // wrap y to the other side if required + pixco[1] = 0; + else + pxlisinframe[1] = false; + } + for(uint32_t i = 0; i < 2; i++) { + if (pxlisinframe[i]) { + if (framebuffer) + fast_color_add(framebuffer[pixco[i]], color, pxlbrightness[i]); else - pxlisinframe[1] = false; - } - for(uint32_t i = 0; i < 2; i++) { - if (pxlisinframe[i]) { - if (framebuffer) - fast_color_add(framebuffer[pixco[i]], color, pxlbrightness[i]); - else - SEGMENT.addPixelColor(pixco[i], color.scale8((uint8_t)pxlbrightness[i]), true); - } + SEGMENT.addPixelColor(pixco[i], color.scale8((uint8_t)pxlbrightness[i]), true); } } } + } // detect collisions in an array of particles and handle them diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index 7cf6fa4a1a..86e9c6f5b2 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -72,7 +72,7 @@ static inline int32_t limitSpeed(const int32_t speed) { #define PS_P_HALFRADIUS (PS_P_RADIUS >> 1) #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_MINHARDRADIUS 70 // minimum hard surface radius for collisions +#define PS_P_MINHARDRADIUS 64 // minimum hard surface radius for collisions #define PS_P_MINSURFACEHARDNESS 128 // minimum hardness used in collision impulse calculation, below this hardness, particles become sticky // struct for PS settings (shared for 1D and 2D class) @@ -238,7 +238,7 @@ class ParticleSystem2D { uint8_t gforcecounter; // counter for global gravity int8_t gforce; // gravity strength, default is 8 (negative is allowed, positive is downwards) // global particle properties for basic particles - uint8_t particlesize; // global particle size, 0 = 2 pixels, 255 = 10 pixels (note: this is also added to individual sized particles) + uint8_t particlesize; // global particle size, 0 = 1 pixel, 1 = 2 pixels, 255 = 10 pixels (note: this is also added to individual sized particles) uint8_t motionBlur; // motion blur, values > 100 gives smoother animations. Note: motion blurring does not work if particlesize is > 0 uint8_t smearBlur; // 2D smeared blurring of full frame uint8_t effectID; // ID of the effect that is using this particle system, used for transitions From 77eb88fc5a04dd6e185167d83d46c74bf11b9482 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Fri, 31 Jan 2025 21:40:32 +0100 Subject: [PATCH 213/219] improved collisions in 1D and 2D, some bugfixes in radius calculation, minor tweaks - collisions are now also velocity based in 1D, there was a bug that prevented that from working well (wrong collision distance calculation) - improvement and bugfix in 2D collision distance calculation - added distance based pushing in 2D (instead of only using the dotproduct) the combination of improved distance calculation and proper pushing make collisions a lot better in all tested FX --- wled00/FX.cpp | 19 +++++---- wled00/FXparticleSystem.cpp | 78 +++++++++++++++++++------------------ wled00/FXparticleSystem.h | 4 +- 3 files changed, 51 insertions(+), 50 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index ab52171bdc..6a26e75cb5 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -7790,11 +7790,11 @@ uint16_t mode_particlefireworks(void) { PartSys->setWrapX(SEGMENT.check1); PartSys->setBounceY(SEGMENT.check2); PartSys->setGravity(map(SEGMENT.custom3, 0, 31, SEGMENT.check2 ? 1 : 0, 10)); // if bounded, set gravity to minimum of 1 or they will bounce at top - PartSys->setMotionBlur(map(SEGMENT.custom2, 0, 255, 0, 170)); // anable motion blur + PartSys->setMotionBlur(SEGMENT.custom2);//map(SEGMENT.custom2, 0, 255, 0, 170)); // anable motion blur uint8_t smearing = 0; if (SEGMENT.custom2 > 200) smearing = SEGMENT.custom2 - 200; - PartSys->setSmearBlur(smearing); // enable 2D blurring (smearing) + //PartSys->setSmearBlur(smearing); // enable 2D blurring (smearing) // update the rockets, set the speed state for (uint32_t j = 0; j < numRockets; j++) { @@ -8116,8 +8116,8 @@ uint16_t mode_particlepit(void) { PartSys->particles[i].sat = ((SEGMENT.custom3) << 3) + 7; // set particle size if (SEGMENT.custom1 == 255) { - PartSys->setParticleSize(0); // set global size to zero - PartSys->advPartProps[i].size =hw_random16(SEGMENT.custom1); // set each particle to random size + PartSys->setParticleSize(1); // set global size to 1 for advanced rendering + PartSys->advPartProps[i].size = hw_random16(SEGMENT.custom1); // set each particle to random size } else { PartSys->setParticleSize(SEGMENT.custom1); // set global size PartSys->advPartProps[i].size = 0; // use global size @@ -8291,7 +8291,7 @@ uint16_t mode_particlebox(void) { return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEBOX[] PROGMEM = "PS Box@!,Particles,Force,Hardness,Size,Random,Washing Machine,Sloshing;;!;2;pal=53,ix=50,c3=1,o1=1"; +static const char _data_FX_MODE_PARTICLEBOX[] PROGMEM = "PS Box@!,Particles,Tilt,Hardness,Size,Random,Washing Machine,Sloshing;;!;2;pal=53,ix=50,c3=1,o1=1"; /* Fuzzy Noise: Perlin noise 'gravity' mapping as in particles on 'noise hills' viewed from above @@ -8567,7 +8567,7 @@ uint16_t mode_particleattractor(void) { PartSys->update(); // update and render return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEATTRACTOR[] PROGMEM = "PS Attractor@Mass,Particles,Size,Collide,Friction,AgeColor,Move,Swallow;;!;2;pal=9,sx=100,ix=82,c1=0,c2=0"; +static const char _data_FX_MODE_PARTICLEATTRACTOR[] PROGMEM = "PS Attractor@Mass,Particles,Size,Collide,Friction,AgeColor,Move,Swallow;;!;2;pal=9,sx=100,ix=82,c1=2,c2=0"; /* Particle Spray, just a particle spray with many parameters @@ -8896,7 +8896,6 @@ uint16_t mode_particleblobs(void) { PartSys->setWallHardness(255); PartSys->setWallRoughness(255); PartSys->setCollisionHardness(255); - //PartSys->setParticleSize(0); //set global size to zero or motion blur cannot be used (is zero by default) } else PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS @@ -9507,7 +9506,7 @@ uint16_t mode_particleHourglass(void) { PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 1, 255)); PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur PartSys->setGravity(map(SEGMENT.custom3, 0, 31, 1, 30)); - PartSys->enableParticleCollisions(true, 16); // hardness value found by experimentation on different settings + PartSys->enableParticleCollisions(true, 34); // hardness value found by experimentation on different settings uint32_t colormode = SEGMENT.custom1 >> 5; // 0-7 @@ -9663,7 +9662,7 @@ uint16_t mode_particle1Dspray(void) { return FRAMETIME; } -static const char _data_FX_MODE_PS_1DSPRAY[] PROGMEM = "PS Spray 1D@Speed(+/-),!,Position,Blur,Gravity(+/-),AgeColor,Bounce,Position Color;,!;!;1;sx=200,ix=220,c1=0,c2=0,o1=1"; +static const char _data_FX_MODE_PS_1DSPRAY[] PROGMEM = "PS Spray 1D@Speed(+/-),!,Position,Blur,Gravity(+/-),AgeColor,Bounce,Position Color;,!;!;1;sx=200,ix=220,c1=0,c2=0"; /* Particle based balance: particles move back and forth (1D pendent to 2D particle box) @@ -9736,7 +9735,7 @@ uint16_t mode_particleBalance(void) { PartSys->update(); // update and render return FRAMETIME; } -static const char _data_FX_MODE_PS_BALANCE[] PROGMEM = "PS 1D Balance@!,!,Hardness,Blur,Tilt,Position Color,Wrap,Random;,!;!;1;pal=18,sx=100,ix=40,c1=200,c2=0,c3=5,o1=1"; +static const char _data_FX_MODE_PS_BALANCE[] PROGMEM = "PS 1D Balance@!,!,Hardness,Blur,Tilt,Position Color,Wrap,Random;,!;!;1;pal=18,sx=64,c1=200,c2=0,c3=5,o1=1"; /* Particle based Chase effect diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index 1dfaf1cacb..6578e1a4a9 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -168,13 +168,13 @@ void ParticleSystem2D::setSmearBlur(uint8_t bluramount) { // render size using smearing (see blur function) void ParticleSystem2D::setParticleSize(uint8_t size) { particlesize = size; - particleHardRadius = PS_P_MINHARDRADIUS; + particleHardRadius = PS_P_MINHARDRADIUS; // ~1 pixel if(particlesize > 1) { - particleHardRadius += particlesize >> 1; // radius used for wall collisions & particle collisions + particleHardRadius = max(particleHardRadius, (uint32_t)particlesize); // radius used for wall collisions & particle collisions motionBlur = 0; // disable motion blur if particle size is set } else if (particlesize == 0) - particleHardRadius = particleHardRadius >> 1; // single pixel particles have half the radius + particleHardRadius = particleHardRadius >> 1; // single pixel particles have half the radius (i.e. 1/2 pixel) } // enable/disable gravity, optionally, set the force (force=8 is default) can be -127 to +127, 0 is disable @@ -254,8 +254,9 @@ void ParticleSystem2D::particleMoveUpdate(PSparticle &part, PSparticleFlags &par partFlags.outofbounds = false; // reset out of bounds (in case particle was created outside the matrix and is now moving into view) note: moving this to checks below adds code and is not faster if (advancedproperties) { //using individual particle size? - if (advancedproperties->size > 0) { - particleHardRadius = max(PS_P_MINHARDRADIUS, (int)particlesize + (advancedproperties->size)); // update radius + setParticleSize(particlesize); // updates default particleHardRadius + if (advancedproperties->size > PS_P_MINHARDRADIUS) { + particleHardRadius += (advancedproperties->size - PS_P_MINHARDRADIUS); // update radius renderradius = particleHardRadius; } } @@ -266,7 +267,7 @@ void ParticleSystem2D::particleMoveUpdate(PSparticle &part, PSparticleFlags &par } } - if(!checkBoundsAndWrap(newY, maxY, renderradius, options->wrapY)) { // check out of bounds note: this must not be skipped, if gravity is enabled, particles will never bounce at the top + if(!checkBoundsAndWrap(newY, maxY, renderradius, options->wrapY)) { // check out of bounds note: this must not be skipped. if gravity is enabled, particles will never bounce at the top partFlags.outofbounds = true; if (options->killoutofbounds) { if (newY < 0) // if gravity is enabled, only kill particles below ground @@ -677,7 +678,7 @@ void ParticleSystem2D::ParticleSys_render() { // calculate pixel positions and brightness distribution and render the particle to local buffer or global buffer void ParticleSystem2D::renderParticle(const uint32_t particleindex, const uint32_t brightness, const CRGB& color, const bool wrapX, const bool wrapY) { - if(particlesize == 0 && !advPartProps) { // single pixel rendering + if(particlesize == 0) { // single pixel rendering uint32_t x = particles[particleindex].x >> PS_P_RADIUS_SHIFT; uint32_t y = particles[particleindex].y >> PS_P_RADIUS_SHIFT; if (x <= (uint32_t)maxXpixel && y <= (uint32_t)maxYpixel) { @@ -848,19 +849,19 @@ void ParticleSystem2D::renderParticle(const uint32_t particleindex, const uint32 // for code simplicity, no y slicing is done, making very tall matrix configurations less efficient // note: also tested adding y slicing, it gives diminishing returns, some FX even get slower. FX not using gravity would benefit with a 10% FPS improvement void ParticleSystem2D::handleCollisions() { - int32_t collDistSq = particleHardRadius << 1; + int32_t collDistSq = particleHardRadius << 1; // distance is double the radius note: particleHardRadius is updated when setting global particle size collDistSq = collDistSq * collDistSq; // square it for faster comparison (square is one operation) // note: partices are binned in x-axis, assumption is that no more than half of the particles are in the same bin // if they are, collisionStartIdx is increased so each particle collides at least every second frame (which still gives decent collisions) - constexpr int BIN_WIDTH = 6 * PS_P_MINHARDRADIUS; // width of a bin in sub-pixels - int32_t overlap = PS_P_MINHARDRADIUS + particlesize; // overlap bins to include edge particles to neighbouring bins + constexpr int BIN_WIDTH = 6 * PS_P_RADIUS; // width of a bin in sub-pixels + int32_t overlap = particleHardRadius << 1; // overlap bins to include edge particles to neighbouring bins if (advPartProps) //may be using individual particle size - overlap += 128; // add max radius + overlap += 512; // add 2 * max radius (approximately) uint32_t maxBinParticles = max((uint32_t)50, (usedParticles + 1) / 2); // assume no more than half of the particles are in the same bin, do not bin small amounts of particles uint32_t numBins = (maxX + (BIN_WIDTH - 1)) / BIN_WIDTH; // number of bins in x direction uint16_t binIndices[maxBinParticles]; // creat array on stack for indices, 2kB max for 1024 particles (ESP32_MAXPARTICLES/2) uint32_t binParticleCount; // number of particles in the current bin - uint16_t nextFrameStartIdx = 0; // index of the first particle in the next frame (set if bin overflow) + uint16_t nextFrameStartIdx = hw_random16(usedParticles); // index of the first particle in the next frame (set to fixed value if bin overflow) uint32_t pidx = collisionStartIdx; //start index in case a bin is full, process remaining particles next frame // fill the binIndices array for this bin @@ -889,14 +890,15 @@ void ParticleSystem2D::handleCollisions() { for (uint32_t j = i + 1; j < binParticleCount; j++) { // check against higher number particles uint32_t idx_j = binIndices[j]; if (advPartProps) { //may be using individual particle size - collDistSq = PS_P_MINHARDRADIUS + particlesize + (((uint32_t)advPartProps[idx_i].size + (uint32_t)advPartProps[idx_j].size) >> 1); // collision distance + setParticleSize(particlesize); // updates base particleHardRadius + collDistSq = (particleHardRadius << 1) + (((uint32_t)advPartProps[idx_i].size + (uint32_t)advPartProps[idx_j].size) >> 1); // collision distance note: not 100% clear why the >> 1 is needed, but it is. collDistSq = collDistSq * collDistSq; // square it for faster comparison } int32_t dx = particles[idx_j].x - particles[idx_i].x; if (dx * dx < collDistSq) { // check x direction, if close, check y direction (squaring is faster than abs() or dual compare) int32_t dy = particles[idx_j].y - particles[idx_i].y; if (dy * dy < collDistSq) // particles are close - collideParticles(particles[idx_i], particles[idx_j], dx, dy); + collideParticles(particles[idx_i], particles[idx_j], dx, dy, collDistSq); } } } @@ -906,7 +908,7 @@ void ParticleSystem2D::handleCollisions() { // handle a collision if close proximity is detected, i.e. dx and/or dy smaller than 2*PS_P_RADIUS // takes two pointers to the particles to collide and the particle hardness (softer means more energy lost in collision, 255 means full hard) -void ParticleSystem2D::collideParticles(PSparticle &particle1, PSparticle &particle2, int32_t dx, int32_t dy) { +void ParticleSystem2D::collideParticles(PSparticle &particle1, PSparticle &particle2, int32_t dx, int32_t dy, const int32_t collDistSq) { int32_t distanceSquared = dx * dx + dy * dy; // Calculate relative velocity (if it is zero, could exit but extra check does not overall speed but deminish it) int32_t relativeVx = (int32_t)particle2.vx - (int32_t)particle1.vx; @@ -947,9 +949,9 @@ void ParticleSystem2D::collideParticles(PSparticle &particle1, PSparticle &parti particle2.vx -= ximpulse; particle2.vy -= yimpulse; - if (collisionHardness < surfacehardness && (SEGMENT.call & 0x03) == 0) { // if particles are soft, they become 'sticky' i.e. apply some friction (they do pile more nicely and stop sloshing around) - const uint32_t coeff = collisionHardness + (255 - PS_P_MINSURFACEHARDNESS); // Note: could call applyFriction, but this is faster and speed is key here - particle1.vx = ((int32_t)particle1.vx * coeff) / 255; + if (collisionHardness < PS_P_MINSURFACEHARDNESS && (SEGMENT.call & 0x07) == 0) { // if particles are soft, they become 'sticky' i.e. apply some friction (they do pile more nicely and stop sloshing around) + const uint32_t coeff = collisionHardness + (255 - PS_P_MINSURFACEHARDNESS); + particle1.vx = ((int32_t)particle1.vx * coeff) / 255; // Note: could call applyFriction, but this is faster and speed is key here particle1.vy = ((int32_t)particle1.vy * coeff) / 255; particle2.vx = ((int32_t)particle2.vx * coeff) / 255; @@ -958,8 +960,9 @@ void ParticleSystem2D::collideParticles(PSparticle &particle1, PSparticle &parti // particles have volume, push particles apart if they are too close // tried lots of configurations, it works best if not moved but given a little velocity, it tends to oscillate less this way + // when hard pushing by offsetting position, they sink into each other under gravity // a problem with giving velocity is, that on harder collisions, this adds up as it is not dampened enough, so add friction in the FX if required - if (dotProduct > -250) { //this means particles are slow (or really really close) so push them apart. + if(distanceSquared < collDistSq && dotProduct > -250) { // too close and also slow, push them apart int32_t notsorandom = dotProduct & 0x01; //dotprouct LSB should be somewhat random, so no need to calculate a random number int32_t pushamount = 1 + ((250 + dotProduct) >> 6); // the closer dotproduct is to zero, the closer the particles are int32_t push = 0; @@ -986,8 +989,9 @@ void ParticleSystem2D::collideParticles(PSparticle &particle1, PSparticle &parti particle1.y--; } particle1.vy += push; + // note: pushing may push particles out of frame, if bounce is active, it will move it back as position will be limited to within frame, if bounce is disabled: bye bye - if (collisionHardness < 16) { // if they are very soft, stop slow particles completely to make them stick to each other + if (collisionHardness < 5) { // if they are very soft, stop slow particles completely to make them stick to each other particle1.vx = 0; particle1.vy = 0; particle2.vx = 0; @@ -1315,10 +1319,7 @@ void ParticleSystem1D::setSmearBlur(const uint8_t bluramount) { // render size, 0 = 1 pixel, 1 = 2 pixel (interpolated), bigger sizes require adanced properties void ParticleSystem1D::setParticleSize(const uint8_t size) { particlesize = size > 0 ? 1 : 0; // TODO: add support for global sizes? see note above (motion blur) - if (particlesize) - particleHardRadius = PS_P_MINHARDRADIUS_1D; // 2 pixel sized particles - else - particleHardRadius = PS_P_MINHARDRADIUS_1D >> 1; // 1 pixel sized particles have half the radius (for bounce, not for collisions) + particleHardRadius = PS_P_MINHARDRADIUS_1D >> (!particlesize); // 2 pixel sized particles or single pixel sized particles } // enable/disable gravity, optionally, set the force (force=8 is default) can be -127 to +127, 0 is disable @@ -1685,18 +1686,18 @@ void ParticleSystem1D::renderParticle(const uint32_t particleindex, const uint32 // detect collisions in an array of particles and handle them void ParticleSystem1D::handleCollisions() { - int32_t collisiondistance = PS_P_MINHARDRADIUS_1D; + int32_t collisiondistance = particleHardRadius << 1; // note: partices are binned by position, assumption is that no more than half of the particles are in the same bin // if they are, collisionStartIdx is increased so each particle collides at least every second frame (which still gives decent collisions) - constexpr int BIN_WIDTH = 32 * PS_P_MINHARDRADIUS_1D; // width of each bin, a compromise between speed and accuracy (lareger bins are faster but collapse more) - int32_t overlap = PS_P_MINHARDRADIUS_1D; // overlap bins to include edge particles to neighbouring bins + constexpr int BIN_WIDTH = 32 * PS_P_RADIUS_1D; // width of each bin, a compromise between speed and accuracy (lareger bins are faster but collapse more) + int32_t overlap = particleHardRadius << 1; // overlap bins to include edge particles to neighbouring bins if (advPartProps) //may be using individual particle size - overlap += 128; // add max radius + overlap += 256; // add 2 * max radius (approximately) uint32_t maxBinParticles = max((uint32_t)50, (usedParticles + 1) / 4); // do not bin small amounts, limit max to 1/2 of particles uint32_t numBins = (maxX + (BIN_WIDTH - 1)) / BIN_WIDTH; // calculate number of bins uint16_t binIndices[maxBinParticles]; // array to store indices of particles in a bin uint32_t binParticleCount; // number of particles in the current bin - uint16_t nextFrameStartIdx = 0; // index of the first particle in the next frame (set if bin overflow) + uint16_t nextFrameStartIdx = hw_random16(usedParticles); // index of the first particle in the next frame (set to fixed value if bin overflow) uint32_t pidx = collisionStartIdx; //start index in case a bin is full, process remaining particles next frame for (uint32_t bin = 0; bin < numBins; bin++) { binParticleCount = 0; // reset for this bin @@ -1723,7 +1724,7 @@ void ParticleSystem1D::handleCollisions() { for (uint32_t j = i + 1; j < binParticleCount; j++) { // check against higher number particles uint32_t idx_j = binIndices[j]; if (advPartProps) { // use advanced size properties - collisiondistance = PS_P_MINHARDRADIUS_1D + (((uint32_t)advPartProps[idx_i].size + (uint32_t)advPartProps[idx_j].size) >> 1); + collisiondistance = (PS_P_MINHARDRADIUS_1D << particlesize) + (((uint32_t)advPartProps[idx_i].size + (uint32_t)advPartProps[idx_j].size) >> 1); } int32_t dx = particles[idx_j].x - particles[idx_i].x; int32_t dv = (int32_t)particles[idx_j].vx - (int32_t)particles[idx_i].vx; @@ -1740,7 +1741,7 @@ void ParticleSystem1D::handleCollisions() { } // handle a collision if close proximity is detected, i.e. dx and/or dy smaller than 2*PS_P_RADIUS // takes two pointers to the particles to collide and the particle hardness (softer means more energy lost in collision, 255 means full hard) -void ParticleSystem1D::collideParticles(PSparticle1D &particle1, const PSparticleFlags1D &particle1flags, PSparticle1D &particle2, const PSparticleFlags1D &particle2flags, int32_t dx, int32_t relativeVx, uint32_t collisiondistance) { +void ParticleSystem1D::collideParticles(PSparticle1D &particle1, const PSparticleFlags1D &particle1flags, PSparticle1D &particle2, const PSparticleFlags1D &particle2flags, int32_t dx, int32_t relativeVx, const int32_t collisiondistance) { int32_t dotProduct = (dx * relativeVx); // is always negative if moving towards each other uint32_t distance = abs(dx); if (dotProduct < 0) { // particles are moving towards each other @@ -1756,7 +1757,7 @@ void ParticleSystem1D::collideParticles(PSparticle1D &particle1, const PSparticl else if (particle2flags.fixed) particle1.vx = -particle2.vx; - if (collisionHardness < PS_P_MINSURFACEHARDNESS_1D) { // if particles are soft, they become 'sticky' i.e. apply some friction + if (collisionHardness < PS_P_MINSURFACEHARDNESS_1D && (SEGMENT.call & 0x07) == 0) { // if particles are soft, they become 'sticky' i.e. apply some friction const uint32_t coeff = collisionHardness + (250 - PS_P_MINSURFACEHARDNESS_1D); particle1.vx = ((int32_t)particle1.vx * coeff) / 255; particle2.vx = ((int32_t)particle2.vx * coeff) / 255; @@ -1765,7 +1766,8 @@ void ParticleSystem1D::collideParticles(PSparticle1D &particle1, const PSparticl else if (distance < collisiondistance || relativeVx == 0) // moving apart or moving along and/or distance too close, push particles apart { // particles have volume, push particles apart if they are too close - // behaviour is different than in 2D, we need pixel accurate stacking here, push the top particle to full radius (direction is well defined in 1D) + // behaviour is different than in 2D, we need pixel accurate stacking here, push the top particle + // note: like in 2D, pushing by a distance makes softer piles collapse, giving particles speed prevents that and looks nicer int32_t pushamount = 1; if (dx < 0) // particle2.x < particle1.x pushamount = -pushamount; @@ -1773,18 +1775,18 @@ void ParticleSystem1D::collideParticles(PSparticle1D &particle1, const PSparticl particle2.vx += pushamount; if(distance < collisiondistance >> 1 ) { // too close, force push particles - pushamount = (collisiondistance - distance); + pushamount = (collisiondistance - distance) >> 3; // note: push amount found by experimentation if(particle1.x < (maxX >> 1)) { // lower half, push particle with larger x in positive direction if (dx < 0 && !particle1flags.fixed) // particle2.x < particle1.x -> push particle 1 - particle1.x += pushamount; + particle1.vx += pushamount; else if (!particle2flags.fixed) // particle1.x < particle2.x -> push particle 2 - particle2.x += pushamount; + particle2.vx += pushamount; } else { // upper half, push particle with smaller x if (dx < 0 && !particle2flags.fixed) // particle2.x < particle1.x -> push particle 2 - particle2.x -= pushamount; + particle2.vx -= pushamount; else if (!particle2flags.fixed) // particle1.x < particle2.x -> push particle 1 - particle1.x -= pushamount; + particle1.vx -= pushamount; } } } diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index 86e9c6f5b2..23f5aae985 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -215,7 +215,7 @@ class ParticleSystem2D { //paricle physics applied by system if flags are set void applyGravity(); // applies gravity to all particles void handleCollisions(); - [[gnu::hot]] void collideParticles(PSparticle &particle1, PSparticle &particle2, const int32_t dx, const int32_t dy); + [[gnu::hot]] void collideParticles(PSparticle &particle1, PSparticle &particle2, const int32_t dx, const int32_t dy, const int32_t collDistSq); void fireParticleupdate(); //utility functions void updatePSpointers(const bool isadvanced, const bool sizecontrol); // update the data pointers to current segment data space @@ -382,7 +382,7 @@ class ParticleSystem1D //paricle physics applied by system if flags are set void applyGravity(); // applies gravity to all particles void handleCollisions(); - [[gnu::hot]] void collideParticles(PSparticle1D &particle1, const PSparticleFlags1D &particle1flags, PSparticle1D &particle2, const PSparticleFlags1D &particle2flags, int32_t dx, int32_t relativeVx, uint32_t collisiondistance); + [[gnu::hot]] void collideParticles(PSparticle1D &particle1, const PSparticleFlags1D &particle1flags, PSparticle1D &particle2, const PSparticleFlags1D &particle2flags, int32_t dx, int32_t relativeVx, const int32_t collisiondistance); //utility functions void updatePSpointers(const bool isadvanced); // update the data pointers to current segment data space From b062e7ee9114a928233aac8886d1f916e95fdcd4 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Mon, 3 Feb 2025 06:20:20 +0100 Subject: [PATCH 214/219] minor fix --- wled00/FXparticleSystem.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index 6578e1a4a9..910263f1d3 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -2127,8 +2127,8 @@ void* particleMemoryManager(const uint32_t requestedParticles, size_t structSize PSPRINTLN("PS transition ended, final particle handover"); uint32_t maxParticles = pmem->buffersize / structSize; // maximum number of particles that fit in the buffer if (maxParticles > availableToPS) { // not all particles transferred yet - int32_t totransfer = maxParticles - availableToPS; // transfer all remaining particles - if(totransfer > 0) // safety check + uint32_t totransfer = maxParticles - availableToPS; // transfer all remaining particles + if(totransfer <= maxParticles) // safety check particleHandover(buffer, structSize, totransfer); if(maxParticles > numParticlesUsed) { // FX uses less than max: move the already existing particles to the beginning of the buffer uint32_t usedbytes = availableToPS * structSize; @@ -2184,7 +2184,7 @@ void particleHandover(void *buffer, size_t structSize, int32_t numToTransfer) { } uint16_t maxTTL = 0; uint32_t TTLrandom = 0; - maxTTL = ((unsigned)strip.getTransition() << 1) / FRAMETIME_FIXED; // tie TTL to transition time: limit to double the transition time + some randomness + maxTTL = ((unsigned)strip.getTransition() << 1) / FRAMETIME_FIXED; // tie TTL to transition time: limit to double the transition time + some randomness #ifndef WLED_DISABLE_PARTICLESYSTEM2D if (structSize == sizeof(PSparticle)) { // 2D particle PSparticle *particles = (PSparticle *)buffer; From 00d908b3625543d227c990ee901b3afba95b335b Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Fri, 7 Feb 2025 20:02:46 +0100 Subject: [PATCH 215/219] fixed overlay detection, checking for partial overlay if a PS FX is partially overlapping, it will render in overlay mode --- wled00/FXparticleSystem.cpp | 37 ++++++++++++++++++++++--------------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index 910263f1d3..4aca510410 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -2221,23 +2221,30 @@ void updateUsedParticles(const uint32_t allocated, const uint32_t available, con used = max((uint32_t)2, min(available, wantsToUse)); // limit to available particles, use a minimum of 2 } -// check if a segment is fully overlapping with an underlying segment (used to enable overlay rendering i.e. adding instead of overwriting pixels) +// check if a segment is partially overlapping with an underlying segment (used to enable overlay rendering i.e. adding instead of overwriting pixels) bool segmentIsOverlay(void) { // TODO: this only needs to be checked when segment is created, could move this to segment class or PS init unsigned segID = strip.getCurrSegmentId(); - if(segID > 0) { // lower number segments exist, check coordinates of underlying segments - unsigned xMin, yMin = 0xFFFF; // note: overlay is activated even if there is gaps in underlying segments, this is an intentional choice - unsigned xMax, yMax = 0; - for (unsigned i = 0; i < segID; i++) { - xMin = strip._segments[i].start < xMin ? strip._segments[i].start : xMin; - yMin = strip._segments[i].startY < yMin ? strip._segments[i].startY : yMin; - xMax = strip._segments[i].stop > xMax ? strip._segments[i].stop : xMax; - yMax = strip._segments[i].stopY > yMax ? strip._segments[i].stopY : yMax; - if(xMin <= strip._segments[segID].start && xMax >= strip._segments[segID].stop && - yMin <= strip._segments[segID].startY && yMax >= strip._segments[segID].stopY) - return true; - } - } - return false; + if (segID == 0) return false; // is base segment, no overlay + unsigned newStartX = strip._segments[segID].start; + unsigned newEndX = strip._segments[segID].stop; + unsigned newStartY = strip._segments[segID].startY; + unsigned newEndY = strip._segments[segID].stopY; + + // Check for overlap with all previous segments + for (unsigned i = 0; i < segID; i++) { + if(strip._segments[i].freeze) continue; // skip inactive segments + unsigned startX = strip._segments[i].start; + unsigned endX = strip._segments[i].stop; + unsigned startY = strip._segments[i].startY; + unsigned endY = strip._segments[i].stopY; + + if (newStartX < endX && newEndX > startX && // x-range overlap + newStartY < endY && newEndY > startY) { // y-range overlap + return true; + } + } + + return false; // No overlap detected } // get the pointer to the particle memory for the segment From bb0de152fd5b98216a3a590f62b04f41f7c88a97 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Fri, 7 Feb 2025 21:20:52 +0100 Subject: [PATCH 216/219] better blur range in PS Firworks --- wled00/FX.cpp | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 82c361bdb2..bb5c821218 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -7810,11 +7810,7 @@ uint16_t mode_particlefireworks(void) { PartSys->setWrapX(SEGMENT.check1); PartSys->setBounceY(SEGMENT.check2); PartSys->setGravity(map(SEGMENT.custom3, 0, 31, SEGMENT.check2 ? 1 : 0, 10)); // if bounded, set gravity to minimum of 1 or they will bounce at top - PartSys->setMotionBlur(SEGMENT.custom2);//map(SEGMENT.custom2, 0, 255, 0, 170)); // anable motion blur - uint8_t smearing = 0; - if (SEGMENT.custom2 > 200) - smearing = SEGMENT.custom2 - 200; - //PartSys->setSmearBlur(smearing); // enable 2D blurring (smearing) + PartSys->setMotionBlur(map(SEGMENT.custom2, 0, 255, 0, 245)); // anable motion blur // update the rockets, set the speed state for (uint32_t j = 0; j < numRockets; j++) { From d0b1fb73c02f1cc31891a7810de146c9ac464f55 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Mon, 10 Feb 2025 07:42:38 +0100 Subject: [PATCH 217/219] minor code consolidation --- wled00/FX.cpp | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index bb5c821218..4c99fadf33 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -10067,10 +10067,7 @@ uint16_t mode_particle1Dsonicstream(void) { // color uint32_t hueincrement = (SEGMENT.custom1 >> 3); // 0-31 - if (SEGMENT.custom1 < 255) - PartSys->setColorByPosition(false); - else - PartSys->setColorByPosition(true); + PartSys->setColorByPosition(SEGMENT.custom1 == 255); // particle manipulation for (uint32_t i = 0; i < PartSys->usedParticles; i++) { From 1f6623d1b3d284a8450de06f3fdd0a56da3be620 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Thu, 13 Feb 2025 20:12:10 +0100 Subject: [PATCH 218/219] updated 1D collisions (yet again), improved 2D collision speed - slight improvement to 2D collision code efficiency - added faster "division" to C3/ESP8266 by using a right shift trick (biasing towards 0 also for negative numbers by applying proper rounding) --- wled00/FXparticleSystem.cpp | 97 +++++++++++++++++++++++++++---------- 1 file changed, 71 insertions(+), 26 deletions(-) diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index 4aca510410..3aaaa95378 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -509,21 +509,34 @@ void ParticleSystem2D::applyGravity(PSparticle &part) { // slow down particle by friction, the higher the speed, the higher the friction. a high friction coefficient slows them more (255 means instant stop) // note: a coefficient smaller than 0 will speed them up (this is a feature, not a bug), coefficient larger than 255 inverts the speed, so don't do that void ParticleSystem2D::applyFriction(PSparticle &part, const int32_t coefficient) { - int32_t friction = 255 - coefficient; // note: not checking if particle is dead can be done by caller (or can be omitted) - // note2: cannot use right shifts as bit shifting in right direction is asymmetrical for positive and negative numbers and this needs to be accurate + #if defined(CONFIG_IDF_TARGET_ESP32C3) || defined(ESP8266) // use bitshifts with rounding instead of division (2x faster) + int32_t friction = 256 - coefficient; + part.vx = ((int32_t)part.vx * friction + (((int32_t)part.vx >> 31) & 0xFF)) >> 8; // note: (v>>31) & 0xFF)) extracts the sign and adds 255 if negative for correct rounding using shifts + part.vy = ((int32_t)part.vy * friction + (((int32_t)part.vy >> 31) & 0xFF)) >> 8; + #else // division is faster on ESP32, S2 and S3 + int32_t friction = 255 - coefficient; part.vx = ((int32_t)part.vx * friction) / 255; part.vy = ((int32_t)part.vy * friction) / 255; + #endif } // apply friction to all particles +// note: not checking if particle is dead is faster as most are usually alive and if few are alive, rendering is fast anyways void ParticleSystem2D::applyFriction(const int32_t coefficient) { + #if defined(CONFIG_IDF_TARGET_ESP32C3) || defined(ESP8266) // use bitshifts with rounding instead of division (2x faster) + int32_t friction = 256 - coefficient; + for (uint32_t i = 0; i < usedParticles; i++) { + particles[i].vx = ((int32_t)particles[i].vx * friction + (((int32_t)particles[i].vx >> 31) & 0xFF)) >> 8; // note: (v>>31) & 0xFF)) extracts the sign and adds 255 if negative for correct rounding using shifts + particles[i].vy = ((int32_t)particles[i].vy * friction + (((int32_t)particles[i].vy >> 31) & 0xFF)) >> 8; + } + #else // division is faster on ESP32, S2 and S3 int32_t friction = 255 - coefficient; for (uint32_t i = 0; i < usedParticles; 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].vx = ((int32_t)particles[i].vx * friction) / 255; particles[i].vy = ((int32_t)particles[i].vy * friction) / 255; } + #endif } // attracts a particle to an attractor particle using the inverse square-law @@ -940,22 +953,35 @@ void ParticleSystem2D::collideParticles(PSparticle &particle1, PSparticle &parti // overflow check: dx/dy are 7bit, relativV are 8bit -> dotproduct is 15bit, dotproduct/distsquared ist 8b, multiplied by collisionhardness of 8bit. so a 16bit shift is ok, make it 15 to be sure no overflows happen // note: cannot use right shifts as bit shifting in right direction is asymmetrical for positive and negative numbers and this needs to be accurate! the trick is: only shift positive numers // Calculate new velocities after collision - int32_t surfacehardness = max(collisionHardness, (int32_t)PS_P_MINSURFACEHARDNESS); // if particles are soft, the impulse must stay above a limit or collisions slip through at higher speeds, 170 seems to be a good value - int32_t impulse = -(((((-dotProduct) << 15) / distanceSquared) * surfacehardness) >> 8); // note: inverting before bitshift corrects for asymmetry in right-shifts (and is slightly faster) - int32_t ximpulse = ((impulse) * dx) / 32767; // cannot use bit shifts here, it can be negative, use division by 2^bitshift - int32_t yimpulse = ((impulse) * dy) / 32767; - particle1.vx += ximpulse; - particle1.vy += yimpulse; - particle2.vx -= ximpulse; - particle2.vy -= yimpulse; + int32_t surfacehardness = 1 + max(collisionHardness, (int32_t)PS_P_MINSURFACEHARDNESS); // if particles are soft, the impulse must stay above a limit or collisions slip through at higher speeds, 170 seems to be a good value + int32_t impulse = (((((-dotProduct) << 15) / distanceSquared) * surfacehardness) >> 8); // note: inverting before bitshift corrects for asymmetry in right-shifts (is slightly faster) + + #if defined(CONFIG_IDF_TARGET_ESP32C3) || defined(ESP8266) // use bitshifts with rounding instead of division (2x faster) + int32_t ximpulse = (impulse * dx + ((dx >> 31) & 32767)) >> 15; // note: extracting sign bit and adding rounding value to correct for asymmetry in right shifts + int32_t yimpulse = (impulse * dy + ((dy >> 31) & 32767)) >> 15; + #else + int32_t ximpulse = (impulse * dx) / 32767; + int32_t yimpulse = (impulse * dy) / 32767; + #endif + particle1.vx -= ximpulse; // note: impulse is inverted, so subtracting it + particle1.vy -= yimpulse; + particle2.vx += ximpulse; + particle2.vy += yimpulse; if (collisionHardness < PS_P_MINSURFACEHARDNESS && (SEGMENT.call & 0x07) == 0) { // if particles are soft, they become 'sticky' i.e. apply some friction (they do pile more nicely and stop sloshing around) const uint32_t coeff = collisionHardness + (255 - PS_P_MINSURFACEHARDNESS); - particle1.vx = ((int32_t)particle1.vx * coeff) / 255; // Note: could call applyFriction, but this is faster and speed is key here + // Note: could call applyFriction, but this is faster and speed is key here + #if defined(CONFIG_IDF_TARGET_ESP32C3) || defined(ESP8266) // use bitshifts with rounding instead of division (2x faster) + particle1.vx = ((int32_t)particle1.vx * coeff + (((int32_t)particle1.vx >> 31) & 0xFF)) >> 8; // note: (v>>31) & 0xFF)) extracts the sign and adds 255 if negative for correct rounding using shifts + particle1.vy = ((int32_t)particle1.vy * coeff + (((int32_t)particle1.vy >> 31) & 0xFF)) >> 8; + particle2.vx = ((int32_t)particle2.vx * coeff + (((int32_t)particle2.vx >> 31) & 0xFF)) >> 8; + particle2.vy = ((int32_t)particle2.vy * coeff + (((int32_t)particle2.vy >> 31) & 0xFF)) >> 8; + #else // division is faster on ESP32, S2 and S3 + particle1.vx = ((int32_t)particle1.vx * coeff) / 255; particle1.vy = ((int32_t)particle1.vy * coeff) / 255; - particle2.vx = ((int32_t)particle2.vx * coeff) / 255; particle2.vy = ((int32_t)particle2.vy * coeff) / 255; + #endif } // particles have volume, push particles apart if they are too close @@ -1478,11 +1504,20 @@ void ParticleSystem1D::applyGravity(PSparticle1D &part, PSparticleFlags1D &partF // slow down particle by friction, the higher the speed, the higher the friction. a high friction coefficient slows them more (255 means instant stop) // note: a coefficient smaller than 0 will speed them up (this is a feature, not a bug), coefficient larger than 255 inverts the speed, so don't do that void ParticleSystem1D::applyFriction(int32_t coefficient) { + #if defined(CONFIG_IDF_TARGET_ESP32C3) || defined(ESP8266) // use bitshifts with rounding instead of division (2x faster) + int32_t friction = 256 - coefficient; + for (uint32_t i = 0; i < usedParticles; i++) { + if (particles[i].ttl) + particles[i].vx = ((int32_t)particles[i].vx * friction + (((int32_t)particles[i].vx >> 31) & 0xFF)) >> 8; // note: (v>>31) & 0xFF)) extracts the sign and adds 255 if negative for correct rounding using shifts + } + #else // division is faster on ESP32, S2 and S3 int32_t friction = 255 - coefficient; for (uint32_t i = 0; i < usedParticles; i++) { if (particles[i].ttl) - particles[i].vx = ((int32_t)particles[i].vx * friction) / 255; // note: cannot use bitshift as vx can be negative + particles[i].vx = ((int32_t)particles[i].vx * friction) / 255; } + #endif + } @@ -1747,7 +1782,7 @@ void ParticleSystem1D::collideParticles(PSparticle1D &particle1, const PSparticl if (dotProduct < 0) { // particles are moving towards each other uint32_t surfacehardness = max(collisionHardness, (int32_t)PS_P_MINSURFACEHARDNESS_1D); // if particles are soft, the impulse must stay above a limit or collisions slip through // Calculate new velocities after collision - int32_t impulse = relativeVx * surfacehardness / 255; + int32_t impulse = relativeVx * surfacehardness / 255; // note: not using dot product like in 2D as impulse is purely speed depnedent particle1.vx += impulse; particle2.vx -= impulse; @@ -1763,7 +1798,8 @@ void ParticleSystem1D::collideParticles(PSparticle1D &particle1, const PSparticl particle2.vx = ((int32_t)particle2.vx * coeff) / 255; } } - else if (distance < collisiondistance || relativeVx == 0) // moving apart or moving along and/or distance too close, push particles apart + + if (distance < (collisiondistance - 8) && abs(relativeVx) < 5) // overlapping and moving slowly { // particles have volume, push particles apart if they are too close // behaviour is different than in 2D, we need pixel accurate stacking here, push the top particle @@ -1774,19 +1810,28 @@ void ParticleSystem1D::collideParticles(PSparticle1D &particle1, const PSparticl particle1.vx -= pushamount; particle2.vx += pushamount; - if(distance < collisiondistance >> 1 ) { // too close, force push particles - pushamount = (collisiondistance - distance) >> 3; // note: push amount found by experimentation + if(distance < collisiondistance >> 1) { // too close, force push particles so they dont collapse + pushamount = 1 + ((collisiondistance - distance) >> 3); // note: push amount found by experimentation + if(particle1.x < (maxX >> 1)) { // lower half, push particle with larger x in positive direction - if (dx < 0 && !particle1flags.fixed) // particle2.x < particle1.x -> push particle 1 - particle1.vx += pushamount; - else if (!particle2flags.fixed) // particle1.x < particle2.x -> push particle 2 - particle2.vx += pushamount; + if (dx < 0 && !particle1flags.fixed) { // particle2.x < particle1.x -> push particle 1 + particle1.vx++;// += pushamount; + particle1.x += pushamount; + } + else if (!particle2flags.fixed) { // particle1.x < particle2.x -> push particle 2 + particle2.vx++;// += pushamount; + particle2.x += pushamount; + } } else { // upper half, push particle with smaller x - if (dx < 0 && !particle2flags.fixed) // particle2.x < particle1.x -> push particle 2 - particle2.vx -= pushamount; - else if (!particle2flags.fixed) // particle1.x < particle2.x -> push particle 1 - particle1.vx -= pushamount; + if (dx < 0 && !particle2flags.fixed) { // particle2.x < particle1.x -> push particle 2 + particle2.vx--;// -= pushamount; + particle2.x -= pushamount; + } + else if (!particle2flags.fixed) { // particle1.x < particle2.x -> push particle 1 + particle1.vx--;// -= pushamount; + particle1.x -= pushamount; + } } } } From 131022e5bc066582103acb31424e8a333b9a188d Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Mon, 17 Feb 2025 18:28:31 +0100 Subject: [PATCH 219/219] minor tweak in PS balance --- wled00/FX.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 4c99fadf33..ff5cb9cf55 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -9738,7 +9738,7 @@ uint16_t mode_particleBalance(void) { PartSys->particles[randomindex].vx = ((int32_t)PartSys->particles[randomindex].vx * 200) / 255; // apply friction to random particle to reduce clumping (without collisions) //if (SEGMENT.check2 && (SEGMENT.call & 0x07) == 0) // no walls, apply friction to smooth things out - if ((SEGMENT.call & 0x0F) == 0 && SEGMENT.custom3 > 2) // apply friction every 16th frame to smooth things out (except for low tilt) + if ((SEGMENT.call & 0x0F) == 0 && SEGMENT.custom3 > 4) // apply friction every 16th frame to smooth things out (except for low tilt) PartSys->applyFriction(1); // apply friction to all particles //update colors @@ -9751,7 +9751,7 @@ uint16_t mode_particleBalance(void) { PartSys->update(); // update and render return FRAMETIME; } -static const char _data_FX_MODE_PS_BALANCE[] PROGMEM = "PS 1D Balance@!,!,Hardness,Blur,Tilt,Position Color,Wrap,Random;,!;!;1;pal=18,sx=64,c1=200,c2=0,c3=5,o1=1"; +static const char _data_FX_MODE_PS_BALANCE[] PROGMEM = "PS 1D Balance@!,!,Hardness,Blur,Tilt,Position Color,Wrap,Random;,!;!;1;pal=18,c2=0,c3=4,o1=1"; /* Particle based Chase effect