Merge pull request #5188 from DedeHai/PS_ellipseRendering

Enhanced particle system rendering & 1D collision handling
- added improved and faster large size rendering to 1D and 2D system
- better size control as size is now exactly mappable to pixels so it can be matched exactly to the collision distance
- no more gaps due to collision distance mismatch
- much faster: saw up to 30% improvement in FPS (2D system)
- adding mass-ratio to collisions for different sized particles
- also adjusted some of the FX to make better use of the new rendering
- added per-particle size rendering to 1D system
- improved and simplified collision handling in 1D system, much more accurate and (almost) no pass-throughs
- removed local blurring functions in PS as they are not needed anymore for particle rendering
- fixed outdated AR handling in PS FX
- fixed infinite loop if not enough memory
- updated PS Hourglass drop interval to simpler math: speed / 10 = time in seconds and improved particle handling
- reduced speed in PS Pinball to fix collision slip-through
- PS Box now auto-adjusts number of particles based on matrix size and particle size
- added safety check to 2D particle rendering to not crash if something goes wrong with out-of bounds particle rendering
- improved binning for particle collisions: dont use binning for small number of particles (faster)
- Some cleanup
This commit is contained in:
Damian Schneider
2025-12-13 23:14:08 +01:00
committed by GitHub
3 changed files with 539 additions and 501 deletions

View File

@@ -8243,7 +8243,7 @@ uint16_t mode_particlefire(void) {
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
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)
if (!initParticleSystem2D(PartSys, SEGMENT.vWidth(), 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
}
@@ -8283,10 +8283,10 @@ uint16_t mode_particlefire(void) {
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].maxLife = hw_random16(SEGMENT.vHeight() >> 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(5) - 2; // emitting speed (sideways)
PartSys->sources[i].vy = (SEGMENT.virtualHeight() >> 1) + (firespeed >> 4) + (SEGMENT.custom1 >> 4); // emitting speed (upwards)
PartSys->sources[i].vy = (SEGMENT.vHeight() >> 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)
}
}
@@ -8316,11 +8316,11 @@ uint16_t mode_particlefire(void) {
if(hw_random8() < 10 + (SEGMENT.intensity >> 2)) {
for (i = 0; i < PartSys->usedParticles; i++) {
if (PartSys->particles[i].ttl == 0) { // find a dead particle
PartSys->particles[i].ttl = hw_random16(SEGMENT.virtualHeight()) + 30;
PartSys->particles[i].ttl = hw_random16(SEGMENT.vHeight()) + 30;
PartSys->particles[i].x = PartSys->sources[0].source.x;
PartSys->particles[i].y = PartSys->sources[0].source.y;
PartSys->particles[i].vx = PartSys->sources[0].source.vx;
PartSys->particles[i].vy = (SEGMENT.virtualHeight() >> 1) + (firespeed >> 4) + ((30 + (SEGMENT.intensity >> 1) + SEGMENT.custom1) >> 4); // emitting speed (upwards)
PartSys->particles[i].vy = (SEGMENT.vHeight() >> 1) + (firespeed >> 4) + ((30 + (SEGMENT.intensity >> 1) + SEGMENT.custom1) >> 4); // emitting speed (upwards)
break; // emit only one particle
}
}
@@ -8386,11 +8386,12 @@ uint16_t mode_particlepit(void) {
PartSys->particles[i].sat = ((SEGMENT.custom3) << 3) + 7;
// set particle size
if (SEGMENT.custom1 == 255) {
PartSys->setParticleSize(1); // set global size to 1 for advanced rendering (no single pixel particles)
PartSys->perParticleSize = true;
PartSys->advPartProps[i].size = hw_random16(SEGMENT.custom1); // set each particle to random size
} else {
PartSys->perParticleSize = false;
PartSys->setParticleSize(SEGMENT.custom1); // set global size
PartSys->advPartProps[i].size = 0; // use global size
PartSys->advPartProps[i].size = SEGMENT.custom1; // also set individual size for consistency
}
break; // emit only one particle per round
}
@@ -8408,7 +8409,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,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=70,c2=180,c3=31,o3=1";
/*
Particle Waterfall
@@ -8492,7 +8493,7 @@ uint16_t mode_particlebox(void) {
uint32_t i;
if (SEGMENT.call == 0) { // initialization
if (!initParticleSystem2D(PartSys, 1)) // init
if (!initParticleSystem2D(PartSys, 1, 0, true)) // init
return mode_static(); // allocation failed or not 2D
PartSys->setBounceX(true);
PartSys->setBounceY(true);
@@ -8505,19 +8506,26 @@ 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, 2, 153)); // 1% - 60%
int maxParticleSize = min(((SEGMENT.vWidth() * SEGMENT.vHeight()) >> 2), 255U); // max particle size based on matrix size
unsigned currentParticleSize = map(SEGMENT.custom3, 0, 31, 0, maxParticleSize);
PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 2, 153) / (1 + (currentParticleSize >> 4))); // 1% - 60%, reduce if using larger size
if (SEGMENT.custom3 < 31)
PartSys->setParticleSize(currentParticleSize); // set global size if not max (resets perParticleSize)
else
PartSys->perParticleSize = true; // per particle size, uses advPartProps.size (randomized below)
// 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 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
PartSys->advPartProps[i].size = hw_random8(maxParticleSize); // random size, used only if size is set to max (SEGMENT.custom3=31)
break; // only spawn one particle per frame for less chaotic transitions
}
}
@@ -8773,22 +8781,10 @@ uint16_t mode_particleattractor(void) {
// Particle System settings
PartSys->updateSystem(); // update system properties (dimensions and data pointers)
attractor = reinterpret_cast<PSparticle *>(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;
}
attractor = reinterpret_cast<PSparticle *>(PartSys->PSdataEnd);
// set attractor properties
attractor->ttl = 100; // never dies
if (SEGMENT.check2) {
@@ -8799,6 +8795,15 @@ uint16_t mode_particleattractor(void) {
attractor->x = PartSys->maxX >> 1; // set to center
attractor->y = PartSys->maxY >> 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;
}
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 % 5 == 0)
PartSys->sources[0].source.hue++;
@@ -8810,13 +8815,11 @@ uint16_t mode_particleattractor(void) {
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)) { // 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;
}
#endif
for (uint32_t i = 0; i < PartSys->usedParticles; i++) {
PartSys->pointAttractor(i, *attractor, strength, SEGMENT.check3);
}
@@ -8828,6 +8831,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=1,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";
/*
@@ -8874,7 +8878,6 @@ uint16_t mode_particlespray(void) {
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)) { // get AR data, do not use simulated data
uint32_t volumeSmth = (uint8_t)(*(float*) um_data->u_data[0]); //0 to 255
@@ -8898,17 +8901,6 @@ uint16_t mode_particlespray(void) {
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;
@@ -9157,6 +9149,7 @@ uint16_t mode_particleblobs(void) {
PartSys->setWallHardness(255);
PartSys->setWallRoughness(255);
PartSys->setCollisionHardness(255);
PartSys->perParticleSize = true; // enable per particle size control
}
else
PartSys = reinterpret_cast<ParticleSystem2D *>(SEGENV.data); // if not first call, just set the pointer to the PS
@@ -9199,16 +9192,14 @@ uint16_t mode_particleblobs(void) {
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)) { // get AR data, do not use simulated data
if (UsermodManager::getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { // get AR data if available, 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
PartSys->advPartProps[i].size = volumeSmth;
}
}
#endif
PartSys->setMotionBlur(((SEGMENT.custom3) << 3) + 7);
PartSys->update(); // update and render
@@ -9247,8 +9238,6 @@ uint16_t mode_particlegalaxy(void) {
// Particle System settings
PartSys->updateSystem(); // update system properties (dimensions and data pointers)
uint8_t particlesize = SEGMENT.custom1;
if(SEGMENT.check3)
particlesize = SEGMENT.custom1 ? 1 : 0; // set size to 0 (single pixel) or 1 (quad pixel) so motion blur works and adds streaks
PartSys->setParticleSize(particlesize); // set size globally
PartSys->setMotionBlur(250 * SEGMENT.check3); // adds trails to single/quad pixel particles, no effect if size > 1
@@ -9316,7 +9305,7 @@ uint16_t mode_particlegalaxy(void) {
PartSys->update(); // update and render
return FRAMETIME;
}
static const char _data_FX_MODE_PARTICLEGALAXY[] PROGMEM = "PS Galaxy@!,!,Size,,Color,,Starfield,Trace;;!;2;pal=59,sx=80,c1=2,c3=4";
static const char _data_FX_MODE_PARTICLEGALAXY[] PROGMEM = "PS Galaxy@!,!,Size,,Color,,Starfield,Trace;;!;2;pal=59,sx=80,c1=1,c3=4";
#endif //WLED_DISABLE_PARTICLESYSTEM2D
#endif // WLED_DISABLE_2D
@@ -9419,7 +9408,7 @@ uint16_t mode_particleDrip(void) {
if (PartSys->particles[i].hue < 245)
PartSys->particles[i].hue += 8;
}
//increase speed on high settings by calling the move function twice
//increase speed on high settings by calling the move function twice note: this can lead to missed collisions
if (SEGMENT.speed > 200)
PartSys->particleMoveUpdate(PartSys->particles[i], PartSys->particleFlags[i]);
}
@@ -9431,8 +9420,8 @@ 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)
Particle Version of "Bouncing Balls by Aircoookie"
Also does rolling balls and juggle (and popcorn)
Uses palette for particle color
by DedeHai (Damian Schneider)
*/
@@ -9443,10 +9432,10 @@ 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->sources[0].source.x = PS_P_RADIUS_1D; //emit at bottom
PartSys->setKillOutOfBounds(true); // out of bounds particles dont return
PartSys->sources[0].source.x = -1000; // shoot up from below
//PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting)
SEGENV.aux0 = 1;
SEGENV.aux1 = 5000; //set out of range to ensure uptate on first call
SEGENV.aux1 = 5000; // set settings out of range to ensure uptate on first call
}
else
PartSys = reinterpret_cast<ParticleSystem1D *>(SEGENV.data); // if not first call, just set the pointer to the PS
@@ -9457,62 +9446,71 @@ uint16_t mode_particlePinball(void) {
// 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->setGravity(map(SEGMENT.custom3, 0 , 31, 0 , 8)); // 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, 255); // enable collisions and set particle collision to high hardness
PartSys->setUsedParticles(SEGMENT.intensity);
PartSys->setColorByPosition(SEGMENT.check3);
uint32_t maxParticles = max(20, SEGMENT.intensity / (1 + (SEGMENT.check2 * (SEGMENT.custom1 >> 5)))); // max particles depends on intensity and rolling balls mode + size
if (SEGMENT.custom1 < 255)
PartSys->setParticleSize(SEGMENT.custom1); // set size globally
else {
PartSys->perParticleSize = true; // use random individual particle size (see below)
maxParticles *= 2; // use more particles if individual s ize is used as there is more space
}
PartSys->setUsedParticles(maxParticles); // reduce if using larger size and rolling balls mode
bool updateballs = false;
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 ? 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].maxLife = SEGMENT.custom3 ? 1000 : 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
if (SEGMENT.check2) { // rolling balls
PartSys->setGravity(0);
PartSys->setWallHardness(255);
int speedsum = 0;
for (uint32_t i = 0; i < PartSys->usedParticles; i++) {
PartSys->particles[i].ttl = 260; // keep particles alive
if (updateballs) { //speed changed or particle is dead, set particle properties
PartSys->particles[i].ttl = 500; // 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
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
}
PartSys->particles[i].hue = hw_random8(); //set ball colors to random
PartSys->advPartProps[i].sat = 255;
PartSys->advPartProps[i].size = SEGMENT.custom1;
PartSys->advPartProps[i].size = hw_random8(); // set ball size for individual size mode
}
speedsum += abs(PartSys->particles[i].vx);
}
int32_t avgSpeed = speedsum / PartSys->usedParticles;
int32_t setSpeed = 2 + (SEGMENT.speed >> 3);
int32_t setSpeed = 2 + (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; // add 1, keep direction
if (abs(PartSys->particles[idx].vx) < PS_P_MAXSPEED)
PartSys->particles[idx].vx += PartSys->particles[idx].vx >= 0 ? 1 : -1; // add 1, keep direction
}
}
else if (avgSpeed > setSpeed + 8) // if avg speed is too high, apply friction to slow them down
PartSys->applyFriction(1);
}
else { //bouncing balls
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 (PartSys->particles[i].ttl < 50) PartSys->particles[i].ttl = 0; // no dark particles
else if (PartSys->particles[i].vx == 0 && PartSys->particles[i].x < (PS_P_RADIUS_1D + SEGMENT.custom1))
PartSys->particles[i].ttl -= 50; // age fast
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
}
}
@@ -9523,14 +9521,14 @@ uint16_t mode_particlePinball(void) {
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->sources[0].size = hw_random8(); //set ball size
PartSys->sprayEmit(PartSys->sources[0]);
}
}
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
}
//for (uint32_t i = 0; i < PartSys->usedParticles; i++) {
// PartSys->particleMoveUpdate(PartSys->particles[i], PartSys->particleFlags[i]); // double the speed note: this leads to bad collisions, also need to run collision detection before
//}
PartSys->update(); // update and render
return FRAMETIME;
@@ -9878,7 +9876,7 @@ uint16_t mode_particleHourglass(void) {
PartSys->setUsedParticles(1 + ((SEGMENT.intensity * 255) >> 8));
PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur
PartSys->setGravity(map(SEGMENT.custom3, 0, 31, 1, 30));
PartSys->enableParticleCollisions(true, 32); // hardness value found by experimentation on different settings
PartSys->enableParticleCollisions(true, 64); // hardness value (found by experimentation on different settings)
uint32_t colormode = SEGMENT.custom1 >> 5; // 0-7
@@ -9892,6 +9890,12 @@ uint16_t mode_particleHourglass(void) {
SEGENV.aux0 = PartSys->usedParticles - 1; // initial state, start with highest number particle
}
// re-order particles in case heavy collisions flipped particles (highest number index particle is on the "bottom")
for (uint32_t i = 0; i < PartSys->usedParticles - 1; i++) {
if (PartSys->particles[i].x < PartSys->particles[i+1].x && PartSys->particleFlags[i].fixed == false && PartSys->particleFlags[i+1].fixed == false) {
std::swap(PartSys->particles[i].x, PartSys->particles[i+1].x);
}
}
// calculate target position depending on direction
auto calcTargetPos = [&](size_t i) {
return PartSys->particleFlags[i].reversegrav ?
@@ -9899,12 +9903,12 @@ uint16_t mode_particleHourglass(void) {
: (PartSys->usedParticles - i) * PS_P_RADIUS_1D - positionOffset;
};
for (uint32_t i = 0; i < PartSys->usedParticles; i++) { // check if particle reached target position after falling
if (PartSys->particleFlags[i].fixed == false && abs(PartSys->particles[i].vx) < 5) {
int32_t targetposition = calcTargetPos(i);
bool closeToTarget = abs(targetposition - PartSys->particles[i].x) < 3 * PS_P_RADIUS_1D;
if (closeToTarget) { // close to target and slow speed
bool belowtarget = PartSys->particleFlags[i].reversegrav ? (PartSys->particles[i].x > targetposition) : (PartSys->particles[i].x < targetposition);
bool closeToTarget = abs(targetposition - PartSys->particles[i].x) < PS_P_RADIUS_1D;
if (belowtarget || closeToTarget) { // overshot target or close to target and slow speed
PartSys->particles[i].x = targetposition; // set exact position
PartSys->particleFlags[i].fixed = true; // pin particle
}
@@ -9918,25 +9922,17 @@ uint16_t mode_particleHourglass(void) {
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 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 3: PartSys->particles[i].hue = ((SEGMENT.custom1 & 0x1F) << 1) + (i % 3)*74; break; // 3 interleved colors
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;
default: break; // use color by position
}
}
if (SEGMENT.check1 && !PartSys->particleFlags[i].reversegrav) // flip color when fallen
PartSys->particles[i].hue += 120;
}
// re-order particles in case collisions flipped particles (highest number index particle is on the "bottom")
for (uint32_t i = 0; i < PartSys->usedParticles - 1; i++) {
if (PartSys->particles[i].x < PartSys->particles[i+1].x && PartSys->particleFlags[i].fixed == false && PartSys->particleFlags[i+1].fixed == false) {
std::swap(PartSys->particles[i].x, PartSys->particles[i+1].x);
}
}
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;
@@ -9948,19 +9944,19 @@ uint16_t mode_particleHourglass(void) {
}
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
if (strip.now >= SEGENV.step) { // drop a particle
// 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
SEGENV.step = strip.now + max(100, SEGMENT.speed * 100); // map speed slider from 0.1s to 25.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
*direction = !(*direction); // flip direction
SEGENV.aux1 = SEGMENT.virtualLength() + 100; // set countdown
SEGENV.aux1 = (SEGMENT.check2) * SEGMENT.vLength() + 100; // set restart countdown, make it short if auto start is unchecked
}
if (*direction == 0) // down, start dropping the highest number particle
SEGENV.aux0--; // next particle
@@ -9968,14 +9964,14 @@ uint16_t mode_particleHourglass(void) {
SEGENV.aux0++;
}
}
else if (SEGMENT.check2) // auto reset
else if (SEGMENT.check2) // auto start/reset
SEGENV.aux1--; // countdown
PartSys->update(); // update and render
return FRAMETIME;
}
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";
static const char _data_FX_MODE_PS_HOURGLASS[] PROGMEM = "PS Hourglass@Interval,!,Color,Blur,Gravity,Colorflip,Start,Fast Reset;,!;!;1;pal=34,sx=5,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)
@@ -10092,7 +10088,7 @@ 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)
PartSys->particles[randomindex].vx = ((int32_t)PartSys->particles[randomindex].vx * 200) / 255; // apply friction to random particle to reduce clumping
//if (SEGMENT.check2 && (SEGMENT.call & 0x07) == 0) // no walls, apply friction to smooth things out
if ((SEGMENT.call & 0x0F) == 0 && SEGMENT.custom3 > 4) // apply friction every 16th frame to smooth things out (except for low tilt)
@@ -10118,7 +10114,7 @@ by DedeHai (Damian Schneider)
uint16_t mode_particleChase(void) {
ParticleSystem1D *PartSys = nullptr;
if (SEGMENT.call == 0) { // initialization
if (!initParticleSystem1D(PartSys, 1, 255, 2, true)) // init
if (!initParticleSystem1D(PartSys, 1, 191, 2, true)) // init
return mode_static(); // allocation failed or is single pixel
SEGENV.aux0 = 0xFFFF; // invalidate
*PartSys->PSdataEnd = 1; // huedir
@@ -10132,15 +10128,17 @@ 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
uint32_t numParticles = 1 + map(SEGMENT.intensity, 0, 255, 2, 255 / (1 + (SEGMENT.custom1 >> 6))); // depends on intensity and particle size (custom1), minimum 1
uint32_t numParticles = 1 + map(SEGMENT.intensity, 0, 255, 0, PartSys->usedParticles / (1 + (SEGMENT.custom1 >> 5))); // depends on intensity and particle size (custom1), minimum 1
numParticles = min(numParticles, PartSys->usedParticles); // limit to available particles
int32_t huestep = 1 + ((((uint32_t)SEGMENT.custom2 << 19) / numParticles) >> 16); // hue increment
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
if (SEGMENT.check1)
SEGENV.step = PartSys->advPartProps[0].size / 2 + (PartSys->maxX / numParticles);
else
SEGENV.step = (PartSys->maxX + (PS_P_RADIUS_1D << 5)) / numParticles; // spacing between particles
else {
SEGENV.step = (PartSys->maxX + (PS_P_RADIUS_1D << 6)) / numParticles; // spacing between particles
SEGENV.step = (SEGENV.step / PS_P_RADIUS_1D) * PS_P_RADIUS_1D; // round down to nearest multiple of particle subpixel unit to align to pixel grid (makes them move in union)
}
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)
@@ -10606,7 +10604,7 @@ by DedeHai (Damian Schneider)
uint16_t mode_particleSpringy(void) {
ParticleSystem1D *PartSys = nullptr;
if (SEGMENT.call == 0) { // initialization
if (!initParticleSystem1D(PartSys, 1, 128, 0, true)) // init
if (!initParticleSystem1D(PartSys, 1, 128, 0, true)) // init with advanced properties (used for spring forces)
return mode_static(); // allocation failed or is single pixel
SEGENV.aux0 = SEGENV.aux1 = 0xFFFF; // invalidate settings
}
@@ -10619,18 +10617,20 @@ uint16_t mode_particleSpringy(void) {
PartSys->setMotionBlur(220 * SEGMENT.check1); // anable motion blur
PartSys->setSmearBlur(50); // smear a little
PartSys->setUsedParticles(map(SEGMENT.custom1, 0, 255, 30 >> SEGMENT.check2, 255 >> (SEGMENT.check2*2))); // depends on density and particle size
// PartSys->enableParticleCollisions(true, 140); // enable particle collisions, can not be set too hard or impulses will not strech the springs if soft.
//PartSys->enableParticleCollisions(true, 140); // enable particle collisions, can not be set too hard or impulses will not strech the springs if soft.
int32_t springlength = PartSys->maxX / (PartSys->usedParticles); // spring length (spacing between particles)
int32_t springK = map(SEGMENT.speed, 0, 255, 5, 35); // spring constant (stiffness)
uint32_t settingssum = SEGMENT.custom1 + SEGMENT.check2;
PartSys->setParticleSize(SEGMENT.check2 ? 120 : 1); // large or small particles
if (SEGENV.aux0 != settingssum) { // number of particles changed, update distribution
for (int32_t i = 0; i < (int32_t)PartSys->usedParticles; i++) {
PartSys->advPartProps[i].sat = 255; // full saturation
//PartSys->particleFlags[i].collide = true; // enable collision for particles
//PartSys->particleFlags[i].collide = true; // enable collision for particles -> results in chaos, removed for now
PartSys->particles[i].x = (i+1) * ((PartSys->maxX) / (PartSys->usedParticles)); // distribute
//PartSys->particles[i].vx = 0; //reset speed
PartSys->advPartProps[i].size = SEGMENT.check2 ? 190 : 2; // set size, small or big
//PartSys->advPartProps[i].size = SEGMENT.check2 ? 190 : 2; // set size, small or big -> use global size
}
SEGENV.aux0 = settingssum;
}
@@ -10722,7 +10722,6 @@ uint16_t mode_particleSpringy(void) {
int speed = SEGMENT.custom3 - 10 - (index ? 10 : 0); // map 11-20 and 21-30 to 1-10
int phase = strip.now * ((1 + (SEGMENT.speed >> 4)) * speed);
if (SEGMENT.check2) amplitude <<= 1; // double amplitude for XL particles
//PartSys->applyForce(PartSys->particles[index], (sin16_t(phase) * amplitude) >> 15, PartSys->advPartProps[index].forcecounter); // apply acceleration
PartSys->particles[index].x = restposition + ((sin16_t(phase) * amplitude) >> 12); // apply position
}
else {

File diff suppressed because it is too large Load Diff

View File

@@ -17,7 +17,7 @@
#include <stdint.h>
#include "wled.h"
#define PS_P_MAXSPEED 120 // 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), limiting below 127 to avoid overflows in collisions due to rounding errors
#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
@@ -103,7 +103,7 @@ typedef union {
// struct for additional particle settings (option)
typedef struct { // 2 bytes
uint8_t size; // particle size, 255 means 10 pixels in diameter, 0 means use global size (including single pixel rendering)
uint8_t size; // particle size, 255 means 10 pixels in diameter, set perParticleSize = true to enable
uint8_t forcecounter; // counter for applying forces to individual particles
} PSadvancedParticle;
@@ -190,16 +190,18 @@ public:
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'
bool perParticleSize; // if true, uses individual particle sizes from advPartProps if available (disabled when calling setParticleSize())
//note: some variables are 32bit for speed and code size at the cost of ram
private:
//rendering functions
void render();
[[gnu::hot]] void renderParticle(const uint32_t particleindex, const uint8_t brightness, const CRGBW& color, const bool wrapX, const bool wrapY);
void renderLargeParticle(const uint32_t size, const uint32_t particleindex, const uint8_t brightness, const CRGBW& 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, const uint32_t collDistSq);
void collideParticles(PSparticle &particle1, PSparticle &particle2, int32_t dx, int32_t dy, const uint32_t collDistSq, int32_t massratio1, int32_t massratio2);
void fireParticleupdate();
//utility functions
void updatePSpointers(const bool isadvanced, const bool sizecontrol); // update the data pointers to current segment data space
@@ -226,12 +228,26 @@ private:
uint8_t smearBlur; // 2D smeared blurring of full frame
};
void blur2D(uint32_t *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);
// distance-based brightness for ellipse rendering, returns brightness (0-255) based on distance from ellipse center
inline uint8_t calculateEllipseBrightness(int32_t dx, int32_t dy, int32_t rxsq, int32_t rysq, uint8_t maxBrightness) {
// square the distances
uint32_t dx_sq = dx * dx;
uint32_t dy_sq = dy * dy;
uint32_t dist_sq = ((dx_sq << 8) / rxsq) + ((dy_sq << 8) / rysq); // normalized squared distance in fixed point: (dx²/rx²) * 256 + (dy²/ry²) * 256
if (dist_sq >= 256) return 0; // pixel is outside the ellipse, unit radius in fixed point: 256 = 1.0
//if (dist_sq <= 96) return maxBrightness; // core at full brightness
int32_t falloff = 256 - dist_sq;
return (maxBrightness * falloff) >> 8; // linear falloff
//return (maxBrightness * falloff * falloff) >> 16; // squared falloff for even softer edges
}
#endif // WLED_DISABLE_PARTICLESYSTEM2D
////////////////////////
@@ -301,9 +317,9 @@ typedef union {
// struct for additional particle settings (optional)
typedef struct {
uint8_t sat; //color saturation
uint8_t sat; // color saturation
uint8_t size; // particle size, 255 means 10 pixels in diameter, this overrides global size setting
uint8_t forcecounter;
uint8_t forcecounter; // counter for applying forces to individual particles
} PSadvancedParticle1D;
//struct for a particle source (20 bytes)
@@ -346,10 +362,11 @@ public:
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 if advanced particle is used
void setParticleSize(const uint8_t size); // particle diameter: size 0 = 1 pixel, size 1 = 2 pixels, size = 255 = 10 pixels, disables per particle size control if called
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
@@ -360,16 +377,18 @@ public:
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'
bool perParticleSize; // if true, uses individual particle sizes from advPartProps if available (disabled when calling setParticleSize())
private:
//rendering functions
void render(void);
[[gnu::hot]] void renderParticle(const uint32_t particleindex, const uint8_t brightness, const CRGBW &color, const bool wrap);
void renderParticle(const uint32_t particleindex, const uint8_t brightness, const CRGBW &color, const bool wrap);
void renderLargeParticle(const uint32_t size, const uint32_t particleindex, const uint8_t brightness, const CRGBW& 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, const int32_t dx, const uint32_t dx_abs, const uint32_t collisiondistance);
void collideParticles(uint32_t partIdx1, uint32_t partIdx2, int32_t dx, uint32_t collisiondistance);
//utility functions
void updatePSpointers(const bool isadvanced); // update the data pointers to current segment data space
@@ -397,5 +416,5 @@ bool initParticleSystem1D(ParticleSystem1D *&PartSys, const uint32_t requestedso
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(uint32_t *colorbuffer, uint32_t size, uint32_t blur, uint32_t start);
#endif // WLED_DISABLE_PARTICLESYSTEM1D