Updates to particle system (#4630)

* added Sonic Boom AR FX, some tweaks to Sonic Stream
* added white color option to Sonic Stream
* improvements to collisions (speed look-ahead)
* code prettified
* added "playful" mode to PS Chase plus some minor speed optimizations
* Adding new FX: PS Springy with many config options
This commit is contained in:
Damian Schneider 2025-04-15 19:07:21 +02:00 committed by GitHub
parent e979c58c98
commit 02f14baad4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 482 additions and 180 deletions

View File

@ -7818,7 +7818,7 @@ 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 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.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.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.vy = (SEGMENT.custom3) + hw_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.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.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].source.ttl = hw_random16(SEGMENT.custom1) + (SEGMENT.custom1 >> 1); // set fuse time
@ -7888,7 +7888,7 @@ uint16_t mode_particlefireworks(void) {
counter = 0; counter = 0;
speed += 3 + ((SEGMENT.intensity >> 6)); // increase speed to form a second wave 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.hue += hueincrement; // new color for next circle
PartSys->sources[j].source.sat = min((uint16_t)150, random16()); PartSys->sources[j].source.sat = 100 + hw_random16(156);
} }
angle += angleincrement; // set angle for next particle angle += angleincrement; // set angle for next particle
} }
@ -9514,44 +9514,36 @@ uint16_t mode_particleHourglass(void) {
PartSys->updateSystem(); // update system properties (dimensions and data pointers) PartSys->updateSystem(); // update system properties (dimensions and data pointers)
settingTracker = reinterpret_cast<uint32_t *>(PartSys->PSdataEnd); //assign data pointer settingTracker = reinterpret_cast<uint32_t *>(PartSys->PSdataEnd); //assign data pointer
direction = reinterpret_cast<bool *>(PartSys->PSdataEnd + 4); //assign data pointer direction = reinterpret_cast<bool *>(PartSys->PSdataEnd + 4); //assign data pointer
PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 1, 255)); PartSys->setUsedParticles(1 + ((SEGMENT.intensity * 255) >> 8));
PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur 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); // hardness value found by experimentation on different settings PartSys->enableParticleCollisions(true, 32); // hardness value found by experimentation on different settings
uint32_t colormode = SEGMENT.custom1 >> 5; // 0-7 uint32_t colormode = SEGMENT.custom1 >> 5; // 0-7
if ((SEGMENT.intensity | (PartSys->getAvailableParticles() << 8)) != *settingTracker) { // initialize, getAvailableParticles changes while in FX transition if ((SEGMENT.intensity | (PartSys->getAvailableParticles() << 8)) != *settingTracker) { // initialize, getAvailableParticles changes while in FX transition
*settingTracker = SEGMENT.intensity | (PartSys->getAvailableParticles() << 8); *settingTracker = SEGMENT.intensity | (PartSys->getAvailableParticles() << 8);
for (uint32_t i = 0; i < PartSys->usedParticles; i++) { for (uint32_t i = 0; i < PartSys->usedParticles; i++) {
PartSys->particleFlags[i].reversegrav = true; PartSys->particleFlags[i].reversegrav = true; // resting particles dont fall
*direction = 0; // down *direction = 0; // down
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
} }
// calculate target position depending on direction
auto calcTargetPos = [&](size_t i) {
return PartSys->particleFlags[i].reversegrav ?
PartSys->maxX - i * PS_P_RADIUS_1D - positionOffset
: (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 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 && abs(PartSys->particles[i].vx) < 5) {
if (PartSys->particleFlags[i].fixed == false) { // && abs(PartSys->particles[i].vx) < 8) { int32_t targetposition = calcTargetPos(i);
// calculate target position depending on direction bool closeToTarget = abs(targetposition - PartSys->particles[i].x) < 3 * PS_P_RADIUS_1D;
bool closeToTarget = false; if (closeToTarget) { // close to target and slow speed
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 { // 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->particles[i].x = targetposition; // set exact position
PartSys->particleFlags[i].fixed = true; // pin particle PartSys->particleFlags[i].fixed = true; // pin particle
} }
@ -9576,19 +9568,20 @@ uint16_t mode_particleHourglass(void) {
PartSys->particles[i].hue += 120; PartSys->particles[i].hue += 120;
} }
// re-order particles in case collisions flipped particles (highest number index particle is on the "bottom")
for (int 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 if (SEGENV.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++) {
PartSys->particleFlags[i].collide = true; PartSys->particleFlags[i].collide = true;
PartSys->particleFlags[i].perpetual = true; PartSys->particleFlags[i].perpetual = true;
PartSys->particles[i].ttl = 260; PartSys->particles[i].ttl = 260;
uint32_t targetposition; PartSys->particles[i].x = calcTargetPos(i);
//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; PartSys->particleFlags[i].fixed = true;
} }
} }
@ -9699,7 +9692,7 @@ uint16_t mode_particleBalance(void) {
// Particle System settings // Particle System settings
PartSys->updateSystem(); // update system properties (dimensions and data pointers) PartSys->updateSystem(); // update system properties (dimensions and data pointers)
PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur PartSys->setMotionBlur(SEGMENT.custom2); // enable motion blur
PartSys->setBounce(!SEGMENT.check2); PartSys->setBounce(!SEGMENT.check2);
PartSys->setWrap(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 uint8_t hardness = SEGMENT.custom1 > 0 ? map(SEGMENT.custom1, 0, 255, 50, 250) : 200; // set hardness, make the walls hard if collisions are disabled
@ -9716,6 +9709,17 @@ uint16_t mode_particleBalance(void) {
} }
SEGENV.aux1 = PartSys->usedParticles; SEGENV.aux1 = PartSys->usedParticles;
// re-order particles in case collisions flipped particles
for (i = 0; i < PartSys->usedParticles - 1; i++) {
if (PartSys->particles[i].x > PartSys->particles[i+1].x) {
if (SEGMENT.check2) { // check for wrap around
if (PartSys->particles[i].x - PartSys->particles[i+1].x > 3 * PS_P_RADIUS_1D)
continue;
}
std::swap(PartSys->particles[i].x, PartSys->particles[i+1].x);
}
}
if (SEGMENT.call % (((255 - SEGMENT.speed) >> 6) + 1) == 0) { // how often the force is applied depends on speed setting if (SEGMENT.call % (((255 - SEGMENT.speed) >> 6) + 1) == 0) { // how often the force is applied depends on speed setting
int32_t xgravity; int32_t xgravity;
int32_t increment = (SEGMENT.speed >> 6) + 1; int32_t increment = (SEGMENT.speed >> 6) + 1;
@ -9756,7 +9760,7 @@ by DedeHai (Damian Schneider)
uint16_t mode_particleChase(void) { uint16_t mode_particleChase(void) {
ParticleSystem1D *PartSys = nullptr; ParticleSystem1D *PartSys = nullptr;
if (SEGMENT.call == 0) { // initialization if (SEGMENT.call == 0) { // initialization
if (!initParticleSystem1D(PartSys, 1, 255, 3, true)) // init if (!initParticleSystem1D(PartSys, 1, 255, 2, true)) // init
return mode_static(); // allocation failed or is single pixel return mode_static(); // allocation failed or is single pixel
SEGENV.aux0 = 0xFFFF; // invalidate SEGENV.aux0 = 0xFFFF; // invalidate
*PartSys->PSdataEnd = 1; // huedir *PartSys->PSdataEnd = 1; // huedir
@ -9766,39 +9770,43 @@ uint16_t mode_particleChase(void) {
PartSys = reinterpret_cast<ParticleSystem1D *>(SEGENV.data); // if not first call, just set the pointer to the PS PartSys = reinterpret_cast<ParticleSystem1D *>(SEGENV.data); // if not first call, just set the pointer to the PS
if (PartSys == nullptr) if (PartSys == nullptr)
return mode_static(); // something went wrong, no data! return mode_static(); // something went wrong, no data!
// Particle System settings // Particle System settings
PartSys->updateSystem(); // update system properties (dimensions and data pointers) PartSys->updateSystem(); // update system properties (dimensions and data pointers)
PartSys->setColorByPosition(SEGMENT.check3); PartSys->setColorByPosition(SEGMENT.check3);
PartSys->setMotionBlur(8 + ((SEGMENT.custom3) << 3)); // anable motion blur PartSys->setMotionBlur(7 + ((SEGMENT.custom3) << 3)); // anable motion blur
// uint8_t* basehue = (PartSys->PSdataEnd + 2); //assign data pointer uint32_t numParticles = 1 + map(SEGMENT.intensity, 0, 255, 2, 255 / (1 + (SEGMENT.custom1 >> 6))); // 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 + PartSys->getAvailableParticles(); // note: getAvailableParticles 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 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 (SEGMENT.check1)
if (numParticles == 0) numParticles = 1; // minimum 1 particle SEGENV.step = PartSys->advPartProps[0].size / 2 + (PartSys->maxX / numParticles);
PartSys->setUsedParticles(numParticles); else
SEGENV.step = (PartSys->maxX + (PS_P_RADIUS_1D << 5)) / PartSys->usedParticles; // spacing between particles SEGENV.step = (PartSys->maxX + (PS_P_RADIUS_1D << 5)) / numParticles; // spacing between particles
for (int32_t i = 0; i < (int32_t)PartSys->usedParticles; i++) { for (int32_t i = 0; i < (int32_t)PartSys->usedParticles; i++) {
PartSys->advPartProps[i].sat = 255; 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].x = (i - 1) * SEGENV.step; // distribute evenly (starts out of frame for i=0)
PartSys->particles[i].vx = SEGMENT.speed >> 1; PartSys->particles[i].vx = SEGMENT.speed >> 2;
PartSys->advPartProps[i].size = SEGMENT.custom1; 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 PartSys->particles[i].hue = i * huestep; // gradient distribution
else else
PartSys->particles[i].hue = hw_random16(); PartSys->particles[i].hue = hw_random16();
} }
SEGENV.aux0 = settingssum; SEGENV.aux0 = settingssum;
} }
int32_t huestep = (((uint32_t)SEGMENT.custom2 << 19) / PartSys->usedParticles) >> 16; // hue increment if(SEGMENT.check1) {
huestep = 1 + (max((int)huestep, 3) * ((int(sin16_t(strip.now * 3) + 32767))) >> 15); // changes gradient spread (scale hue step)
}
// wrap around (cannot use particle system wrap if distributing colors manually, it also wraps rendering which does not look good) // 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 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 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; uint32_t nextindex = (i + 1) % PartSys->usedParticles;
PartSys->particles[i].x = PartSys->particles[nextindex].x - (int)SEGENV.step; PartSys->particles[i].x = PartSys->particles[nextindex].x - (int)SEGENV.step;
if(SEGMENT.check1) // playful mode, vary size
PartSys->advPartProps[i].size = max(1 + (SEGMENT.custom1 >> 1), ((int(sin16_t(strip.now << 1) + 32767)) >> 8)); // cycle size
if (SEGMENT.custom2 < 255) if (SEGMENT.custom2 < 255)
PartSys->particles[i].hue = PartSys->particles[nextindex].hue - huestep; PartSys->particles[i].hue = PartSys->particles[nextindex].hue - huestep;
else else
@ -9807,11 +9815,37 @@ uint16_t mode_particleChase(void) {
PartSys->particles[i].ttl = 300; // reset ttl, cannot use perpetual because memmanager can change pointer at any time PartSys->particles[i].ttl = 300; // reset ttl, cannot use perpetual because memmanager can change pointer at any time
} }
if (SEGMENT.check1) { // playful mode, changes hue, size, speed, density dynamically
int8_t* huedir = reinterpret_cast<int8_t *>(PartSys->PSdataEnd); //assign data pointer
int8_t* stepdir = reinterpret_cast<int8_t *>(PartSys->PSdataEnd + 1);
if(*stepdir == 0) *stepdir = 1; // initialize directions
if(*huedir == 0) *huedir = 1;
if (SEGENV.step >= (PartSys->advPartProps[0].size + PS_P_RADIUS_1D * 4) + PartSys->maxX / numParticles)
*stepdir = -1; // increase density (decrease space between particles)
else if (SEGENV.step <= (PartSys->advPartProps[0].size >> 1) + ((PartSys->maxX / numParticles)))
*stepdir = 1; // decrease density
if (SEGENV.aux1 > 512)
*huedir = -1;
else if (SEGENV.aux1 < 50)
*huedir = 1;
if (SEGMENT.call % (1024 / (1 + (SEGMENT.speed >> 2))) == 0)
SEGENV.aux1 += *huedir;
int8_t globalhuestep = 0; // global hue increment
if (SEGMENT.call % (1 + (int(sin16_t(strip.now) + 32767) >> 12)) == 0)
globalhuestep = 2; // global hue change to add some color variation
if ((SEGMENT.call & 0x1F) == 0)
SEGENV.step += *stepdir; // change density
for(int32_t i = 0; i < PartSys->usedParticles; i++) {
PartSys->particles[i].hue -= globalhuestep; // shift global hue (both directions)
PartSys->particles[i].vx = 1 + (SEGMENT.speed >> 2) + ((int32_t(sin16_t(strip.now >> 1) + 32767) * (SEGMENT.speed >> 2)) >> 16);
}
}
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 PartSys->update(); // update and render
return FRAMETIME; return FRAMETIME;
} }
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"; static const char _data_FX_MODE_PS_CHASE[] PROGMEM = "PS Chase@!,Density,Size,Hue,Blur,Playful,,Position Color;,!;!;1;pal=11,sx=50,c2=5,c3=0";
/* /*
Particle Fireworks Starburst replacement (smoother rendering, more settings) Particle Fireworks Starburst replacement (smoother rendering, more settings)
@ -10016,10 +10050,9 @@ static const char _data_FX_MODE_PS_FIRE1D[] PROGMEM = "PS Fire 1D@!,!,Cooling,Bl
/* /*
Particle based AR effect, swoop particles along the strip with selected frequency loudness Particle based AR effect, swoop particles along the strip with selected frequency loudness
Uses palette for particle color
by DedeHai (Damian Schneider) by DedeHai (Damian Schneider)
*/ */
uint16_t mode_particle1Dsonicstream(void) { uint16_t mode_particle1DsonicStream(void) {
ParticleSystem1D *PartSys = nullptr; ParticleSystem1D *PartSys = nullptr;
if (SEGMENT.call == 0) { // initialization if (SEGMENT.call == 0) { // initialization
@ -10029,7 +10062,6 @@ uint16_t mode_particle1Dsonicstream(void) {
PartSys->sources[0].source.x = 0; // at start PartSys->sources[0].source.x = 0; // at start
//PartSys->sources[1].source.x = PartSys->maxX; // at end //PartSys->sources[1].source.x = PartSys->maxX; // at end
PartSys->sources[0].var = 0;//SEGMENT.custom1 >> 3; PartSys->sources[0].var = 0;//SEGMENT.custom1 >> 3;
PartSys->sources[0].sat = 255;
} }
else else
PartSys = reinterpret_cast<ParticleSystem1D *>(SEGENV.data); // if not first call, just set the pointer to the PS PartSys = reinterpret_cast<ParticleSystem1D *>(SEGENV.data); // if not first call, just set the pointer to the PS
@ -10040,7 +10072,6 @@ uint16_t mode_particle1Dsonicstream(void) {
PartSys->updateSystem(); // update system properties (dimensions and data pointers) 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->setSmearBlur(200); // smooth out the edges
PartSys->sources[0].v = 5 + (SEGMENT.speed >> 2); PartSys->sources[0].v = 5 + (SEGMENT.speed >> 2);
// FFT processing // FFT processing
@ -10050,11 +10081,10 @@ uint16_t mode_particle1Dsonicstream(void) {
uint32_t baseBin = SEGMENT.custom3 >> 1; // 0 - 15 map(SEGMENT.custom3, 0, 31, 0, 14); uint32_t baseBin = SEGMENT.custom3 >> 1; // 0 - 15 map(SEGMENT.custom3, 0, 31, 0, 14);
loudness = fftResult[baseBin];// + fftResult[baseBin + 1]; 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) loudness = loudness << 2; // double loudness for high frequencies (better detecion)
uint32_t threshold = 150 - (SEGMENT.intensity >> 1); uint32_t threshold = 140 - (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 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 threshold = 20 + (threshold >> 1) + SEGMENT.step; // add average to threshold
@ -10062,6 +10092,7 @@ uint16_t mode_particle1Dsonicstream(void) {
// color // color
uint32_t hueincrement = (SEGMENT.custom1 >> 3); // 0-31 uint32_t hueincrement = (SEGMENT.custom1 >> 3); // 0-31
PartSys->sources[0].sat = SEGMENT.custom1 > 0 ? 255 : 0; // color slider at zero: set to white
PartSys->setColorByPosition(SEGMENT.custom1 == 255); PartSys->setColorByPosition(SEGMENT.custom1 == 255);
// particle manipulation // particle manipulation
@ -10072,8 +10103,10 @@ uint16_t mode_particle1Dsonicstream(void) {
} }
else PartSys->particles[i].ttl = 0; else PartSys->particles[i].ttl = 0;
} }
if (SEGMENT.check1) // modulate colors by mid frequencies if (SEGMENT.check1) { // modulate colors by mid frequencies
int mids = sqrt32_bw((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)
PartSys->particles[i].hue += (mids * perlin8(PartSys->particles[i].x << 2, SEGMENT.step << 2)) >> 9; // color by perlin noise from mid frequencies PartSys->particles[i].hue += (mids * perlin8(PartSys->particles[i].x << 2, SEGMENT.step << 2)) >> 9; // color by perlin noise from mid frequencies
}
} }
if (loudness > threshold) { if (loudness > threshold) {
@ -10117,6 +10150,266 @@ uint16_t mode_particle1Dsonicstream(void) {
return FRAMETIME; 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"; static const char _data_FX_MODE_PS_SONICSTREAM[] PROGMEM = "PS Sonic Stream@!,!,Color,Blur,Bin,Mod,Filter,Push;,!;!;1f;c3=0,o2=1";
/*
Particle based AR effect, creates exploding particles on beats
by DedeHai (Damian Schneider)
*/
uint16_t mode_particle1DsonicBoom(void) {
ParticleSystem1D *PartSys = nullptr;
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);
}
else
PartSys = reinterpret_cast<ParticleSystem1D *>(SEGENV.data); // if not first call, just set the pointer to the PS
if (PartSys == nullptr)
return mode_static(); // something went wrong, no data!
// Particle System settings
PartSys->updateSystem(); // update system properties (dimensions and data pointers)
PartSys->setMotionBlur(180 * SEGMENT.check3);
PartSys->setSmearBlur(64 * SEGMENT.check3);
PartSys->sources[0].var = map(SEGMENT.speed, 0, 255, 10, 127);
// FFT processing
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];
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
}
// particle manipulation
for (uint32_t i = 0; i < PartSys->usedParticles; i++) {
if (SEGMENT.check1) { // modulate colors by mid frequencies
int mids = sqrt32_bw((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)
PartSys->particles[i].hue += (mids * perlin8(PartSys->particles[i].x << 2, SEGMENT.step << 2)) >> 9; // color by perlin noise from mid frequencies
}
if (PartSys->particles[i].ttl > 16) {
PartSys->particles[i].ttl -= 16; //ttl is linked to brightness, this allows to use higher brightness but still a (very) short lifespan
}
}
if (loudness > threshold) {
if (SEGMENT.aux1 == 0) { // edge detected, code only runs once per "beat"
// update position
if (SEGMENT.custom2 < 128) // fixed position
PartSys->sources[0].source.x = map(SEGMENT.custom2, 0, 127, 0, PartSys->maxX);
else if (SEGMENT.custom2 < 255) { // advances on each "beat"
int32_t step = PartSys->maxX / (((270 - SEGMENT.custom2) >> 3)); // step: 2 - 33 steps for full segment width
PartSys->sources[0].source.x = (PartSys->sources[0].source.x + step) % PartSys->maxX;
if (PartSys->sources[0].source.x < step) // align to be symmetrical by making the first position half a step from start
PartSys->sources[0].source.x = step >> 1;
}
else // position set to max, use random postion per beat
PartSys->sources[0].source.x = hw_random(PartSys->maxX);
// update color
//PartSys->setColorByPosition(SEGMENT.custom1 == 255); // color slider at max: particle color by position
PartSys->sources[0].sat = SEGMENT.custom1 > 0 ? 255 : 0; // color slider at zero: set to white
if (SEGMENT.custom1 == 255) // emit color by position
SEGMENT.aux0 = map(PartSys->sources[0].source.x , 0, PartSys->maxX, 0, 255);
else if (SEGMENT.custom1 > 0)
SEGMENT.aux0 += (SEGMENT.custom1 >> 1); // change emit color per "beat"
}
SEGMENT.aux1 = 1; // track edge detection
PartSys->sources[0].minLife = 200;
PartSys->sources[0].maxLife = PartSys->sources[0].minLife + (((unsigned)SEGMENT.intensity * loudness * loudness) >> 13);
PartSys->sources[0].source.hue = SEGMENT.aux0;
PartSys->sources[0].size = 1; //SEGMENT.speed>>3;
uint32_t explosionsize = 4 + (PartSys->maxXpixel >> 2);
explosionsize = hw_random16((explosionsize * loudness) >> 10);
for (uint32_t e = 0; e < explosionsize; e++) { // emit explosion particles
PartSys->sprayEmit(PartSys->sources[0]); // emit a particle
}
}
else
SEGMENT.aux1 = 0; // reset edge detection
PartSys->update(); // update and render (needs to be done before manipulation for initial particle spacing to be right)
return FRAMETIME;
}
static const char _data_FX_MODE_PS_SONICBOOM[] PROGMEM = "PS Sonic Boom@!,!,Color,Position,Bin,Mod,Filter,Blur;,!;!;1f;c2=63,c3=0,o2=1";
/*
Particles bound by springs
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
return mode_static(); // allocation failed or is single pixel
SEGENV.aux0 = SEGENV.aux1 = 0xFFFF; // invalidate settings
}
else
PartSys = reinterpret_cast<ParticleSystem1D *>(SEGENV.data); // if not first call, just set the pointer to the PS
if (PartSys == nullptr)
return mode_static(); // something went wrong, no data!
// Particle System settings
PartSys->updateSystem(); // update system properties (dimensions and data pointers)
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.
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->getAvailableParticles(); // note: getAvailableParticles is used to enforce update during transitions
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->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
}
SEGENV.aux0 = settingssum;
}
int dxlimit = (2 + ((255 - SEGMENT.speed) >> 5)) * springlength; // limit for spring length to avoid overstretching
int springforce[PartSys->usedParticles]; // spring forces
memset(springforce, 0, PartSys->usedParticles * sizeof(int32_t)); // reset spring forces
// calculate spring forces and limit particle positions
if (PartSys->particles[0].x < -springlength)
PartSys->particles[0].x = -springlength; // limit the spring length
else if (PartSys->particles[0].x > dxlimit)
PartSys->particles[0].x = dxlimit; // limit the spring length
springforce[0] += ((springlength >> 1) - (PartSys->particles[0].x)) * springK; // first particle anchors to x=0
for (int32_t i = 1; i < PartSys->usedParticles; i++) {
// reorder particles if they are out of order to prevent chaos
if (PartSys->particles[i].x < PartSys->particles[i-1].x)
std::swap(PartSys->particles[i].x, PartSys->particles[i-1].x); // swap particle positions to maintain order
int dx = PartSys->particles[i].x - PartSys->particles[i-1].x; // distance, always positive
if (dx > dxlimit) { // limit the spring length
PartSys->particles[i].x = PartSys->particles[i-1].x + dxlimit;
dx = dxlimit;
}
int dxleft = (springlength - dx); // offset from spring resting position
springforce[i] += dxleft * springK;
springforce[i-1] -= dxleft * springK;
if (i == (PartSys->usedParticles - 1)) {
if (PartSys->particles[i].x >= PartSys->maxX + springlength)
PartSys->particles[i].x = PartSys->maxX + springlength;
int dxright = (springlength >> 1) - (PartSys->maxX - PartSys->particles[i].x); // last particle anchors to x=maxX
springforce[i] -= dxright * springK;
}
}
// apply spring forces to particles
bool dampenoscillations = (SEGMENT.call % (9 - (SEGMENT.speed >> 5))) == 0; // dampen oscillation if particles are slow, more damping on stiffer springs
for (int32_t i = 0; i < PartSys->usedParticles; i++) {
springforce[i] = springforce[i] / 64; // scale spring force (cannot use shifts because of negative values)
int maxforce = 120; // limit spring force
springforce[i] = springforce[i] > maxforce ? maxforce : springforce[i] < -maxforce ? -maxforce : springforce[i]; // limit spring force
PartSys->applyForce(PartSys->particles[i], springforce[i], PartSys->advPartProps[i].forcecounter);
//dampen slow particles to avoid persisting oscillations on higher stiffness
if (dampenoscillations) {
if (abs(PartSys->particles[i].vx) < 3 && abs(springforce[i]) < (springK >> 2))
PartSys->particles[i].vx = (PartSys->particles[i].vx * 254) / 256; // take out some energy
}
PartSys->particles[i].ttl = 300; // reset ttl, cannot use perpetual
}
if (SEGMENT.call % ((65 - ((SEGMENT.intensity * (1 + (SEGMENT.speed>>3))) >> 7))) == 0) // more damping for higher stiffness
PartSys->applyFriction((SEGMENT.intensity >> 2));
// add a small resetting force so particles return to resting position even under high damping
for (int32_t i = 1; i < PartSys->usedParticles - 1; i++) {
int restposition = (springlength >> 1) + i * springlength; // resting position
int dx = restposition - PartSys->particles[i].x; // distance, always positive
PartSys->applyForce(PartSys->particles[i], dx > 0 ? 1 : (dx < 0 ? -1 : 0), PartSys->advPartProps[i].forcecounter);
}
// Modes
if (SEGMENT.check3) { // use AR, custom 3 becomes frequency band to use, applies velocity to center particle according to loudness
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 baseBin = map(SEGMENT.custom3, 0, 31, 0, 14);
uint32_t loudness = fftResult[baseBin] + fftResult[baseBin+1];
uint32_t threshold = 80; //150 - (SEGMENT.intensity >> 1);
if (loudness > threshold) {
int offset = (PartSys->maxX >> 1) - PartSys->particles[PartSys->usedParticles>>1].x; // offset from center
if (abs(offset) < PartSys->maxX >> 5) // push particle around in center sector
PartSys->particles[PartSys->usedParticles>>1].vx = ((PartSys->particles[PartSys->usedParticles>>1].vx > 0 ? 1 : -1)) * (loudness >> 3);
}
}
else{
if (SEGMENT.custom3 <= 10) { // periodic pulse: 0-5 apply at start, 6-10 apply at center
if (strip.now > SEGMENT.step) {
int speed = (SEGMENT.custom3 > 5) ? (SEGMENT.custom3 - 6) : SEGMENT.custom3;
SEGMENT.step = strip.now + 7500 - ((SEGMENT.speed << 3) + (speed << 10));
int amplitude = 40 + (SEGMENT.custom1 >> 2);
int index = (SEGMENT.custom3 > 5) ? (PartSys->usedParticles / 2) : 0; // center or start particle
PartSys->particles[index].vx += amplitude;
}
}
else if (SEGMENT.custom3 <= 30) { // sinusoidal wave: 11-20 apply at start, 21-30 apply at center
int index = (SEGMENT.custom3 > 20) ? (PartSys->usedParticles / 2) : 0; // center or start particle
int restposition = 0;
if (index > 0) restposition = PartSys->maxX >> 1; // center
//int amplitude = 5 + (SEGMENT.speed >> 3) + (SEGMENT.custom1 >> 2); // amplitude depends on density
int amplitude = 5 + (SEGMENT.custom1 >> 2); // amplitude depends on density
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 {
if (hw_random16() < 656) { // ~1% chance to add a pulse
int amplitude = 60;
if (SEGMENT.check2) amplitude <<= 1; // double amplitude for XL particles
PartSys->particles[PartSys->usedParticles >> 1].vx += hw_random16(amplitude << 1) - amplitude; // apply acceleration
}
}
}
for (int32_t i = 0; i < PartSys->usedParticles; i++) {
if (SEGMENT.custom2 == 255) { // map speed to hue
int speedclr = ((int8_t(abs(PartSys->particles[i].vx))) >> 2) << 4; // scale for greater color variation, dump small values to avoid flickering
//int speed = PartSys->particles[i].vx << 2; // +/- 512
if (speedclr > 240) speedclr = 240; // limit color to non-wrapping part of palette
PartSys->particles[i].hue = speedclr;
}
else if (SEGMENT.custom2 > 0)
PartSys->particles[i].hue = i * (SEGMENT.custom2 >> 2); // gradient distribution
else {
// map hue to particle density
int deviation;
if (i == 0) // First particle: measure density based on distance to anchor point
deviation = springlength/2 - PartSys->particles[i].x;
else if (i == PartSys->usedParticles - 1) // Last particle: measure density based on distance to right boundary
deviation = springlength/2 - (PartSys->maxX - PartSys->particles[i].x);
else {
// Middle particles: average of compression/expansion from both sides
int leftDx = PartSys->particles[i].x - PartSys->particles[i-1].x;
int rightDx = PartSys->particles[i+1].x - PartSys->particles[i].x;
int avgDistance = (leftDx + rightDx) >> 1;
if (avgDistance < 0) avgDistance = 0; // avoid negative distances (not sure why this happens)
deviation = (springlength - avgDistance);
}
deviation = constrain(deviation, -127, 112); // limit deviation to -127..112 (do not go intwo wrapping part of palette)
PartSys->particles[i].hue = 127 + deviation; // map density to hue
}
}
PartSys->update(); // update and render
return FRAMETIME;
}
static const char _data_FX_MODE_PS_SPRINGY[] PROGMEM = "PS Springy@Stiffness,Damping,Density,Hue,Mode,Smear,XL,AR;,!;!;1f;pal=54,c2=0,c3=23";
#endif // WLED_DISABLE_PARTICLESYSTEM1D #endif // WLED_DISABLE_PARTICLESYSTEM1D
////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////
@ -10385,7 +10678,9 @@ 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_PSSTARBURST, &mode_particleStarburst, _data_FX_MODE_PS_STARBURST);
addEffect(FX_MODE_PS1DGEQ, &mode_particle1DGEQ, _data_FX_MODE_PS_1D_GEQ); 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_PSFIRE1D, &mode_particleFire1D, _data_FX_MODE_PS_FIRE1D);
addEffect(FX_MODE_PS1DSONICSTREAM, &mode_particle1Dsonicstream, _data_FX_MODE_PS_SONICSTREAM); addEffect(FX_MODE_PS1DSONICSTREAM, &mode_particle1DsonicStream, _data_FX_MODE_PS_SONICSTREAM);
addEffect(FX_MODE_PS1DSONICBOOM, &mode_particle1DsonicBoom, _data_FX_MODE_PS_SONICBOOM);
addEffect(FX_MODE_PS1DSPRINGY, &mode_particleSpringy, _data_FX_MODE_PS_SPRINGY);
#endif // WLED_DISABLE_PARTICLESYSTEM1D #endif // WLED_DISABLE_PARTICLESYSTEM1D
} }

View File

@ -351,7 +351,9 @@ extern byte realtimeMode; // used in getMappedPixelIndex()
#define FX_MODE_PS1DGEQ 212 #define FX_MODE_PS1DGEQ 212
#define FX_MODE_PSFIRE1D 213 #define FX_MODE_PSFIRE1D 213
#define FX_MODE_PS1DSONICSTREAM 214 #define FX_MODE_PS1DSONICSTREAM 214
#define MODE_COUNT 215 #define FX_MODE_PS1DSONICBOOM 215
#define FX_MODE_PS1DSPRINGY 216
#define MODE_COUNT 217
#define BLEND_STYLE_FADE 0x00 // universal #define BLEND_STYLE_FADE 0x00 // universal

View File

@ -18,8 +18,8 @@
// local shared functions (used both in 1D and 2D system) // 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 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 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_add(CRGB &c1, const CRGB &c2, uint8_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 void fast_color_scale(CRGB &c, const uint8_t scale); // fast scaling function using 32bit variable and pointer. note: keep 'scale' within 0-255
//static CRGB *allocateCRGBbuffer(uint32_t length); //static CRGB *allocateCRGBbuffer(uint32_t length);
// global variables for memory management // global variables for memory management
@ -73,7 +73,7 @@ void ParticleSystem2D::update(void) {
//update size settings before handling collisions //update size settings before handling collisions
if (advPartSize) { if (advPartSize) {
for (uint32_t i = 0; i < usedParticles; i++) { for (uint32_t i = 0; i < usedParticles; i++) {
if(updateSize(&advPartProps[i], &advPartSize[i]) == false) { // if particle shrinks to 0 size if (updateSize(&advPartProps[i], &advPartSize[i]) == false) { // if particle shrinks to 0 size
particles[i].ttl = 0; // kill particle particles[i].ttl = 0; // kill particle
} }
} }
@ -170,7 +170,7 @@ void ParticleSystem2D::setSmearBlur(uint8_t bluramount) {
void ParticleSystem2D::setParticleSize(uint8_t size) { void ParticleSystem2D::setParticleSize(uint8_t size) {
particlesize = size; particlesize = size;
particleHardRadius = PS_P_MINHARDRADIUS; // ~1 pixel particleHardRadius = PS_P_MINHARDRADIUS; // ~1 pixel
if(particlesize > 1) { if (particlesize > 1) {
particleHardRadius = max(particleHardRadius, (uint32_t)particlesize); // 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 motionBlur = 0; // disable motion blur if particle size is set
} }
@ -226,7 +226,7 @@ int32_t ParticleSystem2D::sprayEmit(const PSsource &emitter) {
// Spray emitter for particles used for flames (particle TTL depends on source TTL) // Spray emitter for particles used for flames (particle TTL depends on source TTL)
void ParticleSystem2D::flameEmit(const PSsource &emitter) { void ParticleSystem2D::flameEmit(const PSsource &emitter) {
int emitIndex = sprayEmit(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 // Emits a particle at given angle and speed, angle is from 0-65535 (=0-360deg), speed is also affected by emitter->var
@ -268,7 +268,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; partFlags.outofbounds = true;
if (options->killoutofbounds) { if (options->killoutofbounds) {
if (newY < 0) // if gravity is enabled, only kill particles below ground if (newY < 0) // if gravity is enabled, only kill particles below ground
@ -278,12 +278,12 @@ void ParticleSystem2D::particleMoveUpdate(PSparticle &part, PSparticleFlags &par
} }
} }
if(part.ttl) { //check x direction only if still alive if (part.ttl) { //check x direction only if still alive
if (options->bounceX) { if (options->bounceX) {
if ((newX < (int32_t)particleHardRadius) || (newX > (int32_t)(maxX - particleHardRadius))) // reached a wall if ((newX < (int32_t)particleHardRadius) || (newX > (int32_t)(maxX - particleHardRadius))) // reached a wall
bounce(part.vx, part.vy, newX, maxX); bounce(part.vx, part.vy, newX, maxX);
} }
else if(!checkBoundsAndWrap(newX, maxX, renderradius, options->wrapX)) { // check out of bounds else if (!checkBoundsAndWrap(newX, maxX, renderradius, options->wrapX)) { // check out of bounds
partFlags.outofbounds = true; partFlags.outofbounds = true;
if (options->killoutofbounds) if (options->killoutofbounds)
part.ttl = 0; part.ttl = 0;
@ -387,14 +387,14 @@ void ParticleSystem2D::getParticleXYsize(PSadvancedParticle *advprops, PSsizeCon
return; return;
int32_t size = advprops->size; int32_t size = advprops->size;
int32_t asymdir = advsize->asymdir; int32_t asymdir = advsize->asymdir;
int32_t deviation = ((uint32_t)size * (uint32_t)advsize->asymmetry) / 255; // deviation from symmetrical size int32_t deviation = ((uint32_t)size * (uint32_t)advsize->asymmetry + 255) >> 8; // 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) { if (asymdir < 64) {
deviation = (asymdir * deviation) / 64; deviation = (asymdir * deviation) >> 6;
} else if (asymdir < 192) { } else if (asymdir < 192) {
deviation = ((128 - asymdir) * deviation) / 64; deviation = ((128 - asymdir) * deviation) >> 6;
} else { } else {
deviation = ((asymdir - 255) * deviation) / 64; deviation = ((asymdir - 255) * deviation) >> 6;
} }
// Calculate x and y size based on deviation, limit to 255 (rendering function cannot handle larger sizes) // Calculate x and y size based on deviation, limit to 255 (rendering function cannot handle larger sizes)
xsize = min((size - deviation), (int32_t)255); xsize = min((size - deviation), (int32_t)255);
@ -404,7 +404,7 @@ void ParticleSystem2D::getParticleXYsize(PSadvancedParticle *advprops, PSsizeCon
// 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 ParticleSystem2D::bounce(int8_t &incomingspeed, int8_t &parallelspeed, int32_t &position, const uint32_t maxposition) { void ParticleSystem2D::bounce(int8_t &incomingspeed, int8_t &parallelspeed, int32_t &position, const uint32_t maxposition) {
incomingspeed = -incomingspeed; incomingspeed = -incomingspeed;
incomingspeed = (incomingspeed * wallHardness) / 255; // reduce speed as energy is lost on non-hard surface incomingspeed = (incomingspeed * wallHardness + 128) >> 8; // reduce speed as energy is lost on non-hard surface
if (position < (int32_t)particleHardRadius) if (position < (int32_t)particleHardRadius)
position = particleHardRadius; // fast particles will never reach the edge if position is inverted, this looks better position = particleHardRadius; // fast particles will never reach the edge if position is inverted, this looks better
else else
@ -491,7 +491,7 @@ void ParticleSystem2D::applyAngleForce(const int8_t force, const uint16_t angle)
// note: faster than apply force since direction is always down and counter is fixed for all particles // 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); int32_t dv = calcForce_dv(gforce, gforcecounter);
if(dv == 0) return; if (dv == 0) return;
for (uint32_t i = 0; i < usedParticles; i++) { 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 // 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); particles[i].vy = limitSpeed((int32_t)particles[i].vy - dv);
@ -574,7 +574,7 @@ void ParticleSystem2D::pointAttractor(const uint32_t particleindex, PSparticle &
// warning: do not render out of bounds particles or system will crash! rendering does not check if particle is out of bounds // 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 // firemode is only used for PS Fire FX
void ParticleSystem2D::ParticleSys_render() { void ParticleSystem2D::ParticleSys_render() {
if(blendingStyle == BLEND_STYLE_FADE && SEGMENT.isInTransition() && lastRender + (strip.getFrameTime() >> 1) > strip.now) // fixes speedup during transitions TODO: find a better solution if (blendingStyle == BLEND_STYLE_FADE && SEGMENT.isInTransition() && lastRender + (strip.getFrameTime() >> 1) > strip.now) // fixes speedup during transitions TODO: find a better solution
return; return;
lastRender = strip.now; lastRender = strip.now;
CRGB baseRGB; CRGB baseRGB;
@ -586,24 +586,24 @@ void ParticleSystem2D::ParticleSys_render() {
// update global blur (used for blur transitions) // update global blur (used for blur transitions)
int32_t motionbluramount = motionBlur; int32_t motionbluramount = motionBlur;
int32_t smearamount = smearBlur; 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 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 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; globalBlur = motionbluramount;
globalSmear = smearamount; globalSmear = smearamount;
if(isOverlay) { if (isOverlay) {
globalSmear = 0; // do not apply smear or blur in overlay or it turns everything into a blurry mess globalSmear = 0; // do not apply smear or blur in overlay or it turns everything into a blurry mess
globalBlur = 0; globalBlur = 0;
} }
// handle blurring and framebuffer update // handle blurring and framebuffer update
if (framebuffer) { if (framebuffer) {
if(!pmem->inTransition) if (!pmem->inTransition)
useAdditiveTransfer = false; // additive transfer is only usd in transitions (or in overlay) useAdditiveTransfer = false; // additive transfer is only usd in transitions (or in overlay)
// handle buffer blurring or clearing // 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) { if (bufferNeedsUpdate) {
bool loadfromSegment = !renderSolo || isNonFadeTransition; 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) 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++) { for (int32_t y = 0; y <= maxYpixel; y++) {
@ -622,8 +622,8 @@ void ParticleSystem2D::ParticleSys_render() {
} }
} }
// handle buffer for global large particle size rendering // handle buffer for global large particle size rendering
if(particlesize > 1 && 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) 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 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 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
@ -682,7 +682,7 @@ void ParticleSystem2D::ParticleSys_render() {
} }
} }
// apply 2D blur to rendered frame // apply 2D blur to rendered frame
if(globalSmear > 0) { if (globalSmear > 0) {
if (framebuffer) if (framebuffer)
blur2D(framebuffer, maxXpixel + 1, maxYpixel + 1, globalSmear, globalSmear); blur2D(framebuffer, maxXpixel + 1, maxYpixel + 1, globalSmear, globalSmear);
else else
@ -694,8 +694,8 @@ void ParticleSystem2D::ParticleSys_render() {
} }
// calculate pixel positions and brightness distribution and render the particle to local buffer or global buffer // 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) { void ParticleSystem2D::renderParticle(const uint32_t particleindex, const uint8_t brightness, const CRGB& color, const bool wrapX, const bool wrapY) {
if(particlesize == 0) { // single pixel rendering if (particlesize == 0) { // single pixel rendering
uint32_t x = particles[particleindex].x >> PS_P_RADIUS_SHIFT; uint32_t x = particles[particleindex].x >> PS_P_RADIUS_SHIFT;
uint32_t y = particles[particleindex].y >> PS_P_RADIUS_SHIFT; uint32_t y = particles[particleindex].y >> PS_P_RADIUS_SHIFT;
if (x <= (uint32_t)maxXpixel && y <= (uint32_t)maxYpixel) { if (x <= (uint32_t)maxXpixel && y <= (uint32_t)maxYpixel) {
@ -706,7 +706,7 @@ void ParticleSystem2D::renderParticle(const uint32_t particleindex, const uint32
} }
return; return;
} }
int32_t pxlbrightness[4]; // brightness values for the four pixels representing a particle uint8_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 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 pixelvalid[4] = {true, true, true, true}; // is set to false if pixel is out of bounds
bool advancedrender = false; // rendering for advanced particles bool advancedrender = false; // rendering for advanced particles
@ -765,7 +765,7 @@ void ParticleSystem2D::renderParticle(const uint32_t particleindex, const uint32
} }
maxsize = maxsize/64 + 1; // number of blur passes depends on maxsize, four passes max maxsize = maxsize/64 + 1; // number of blur passes depends on maxsize, four passes max
uint32_t bitshift = 0; 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) if (i == 2) //for the last two passes, use higher amount of blur (results in a nicer brightness gradient with soft edges)
bitshift = 1; bitshift = 1;
rendersize += 2; rendersize += 2;
@ -911,9 +911,9 @@ void ParticleSystem2D::handleCollisions() {
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 = (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 collDistSq = collDistSq * collDistSq; // square it for faster comparison
} }
int32_t dx = particles[idx_j].x - particles[idx_i].x; int32_t dx = (particles[idx_j].x + particles[idx_j].vx) - (particles[idx_i].x + particles[idx_i].vx); // distance with lookahead
if (dx * dx < collDistSq) { // check x direction, if close, check y direction (squaring is faster than abs() or dual compare) 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; int32_t dy = (particles[idx_j].y + particles[idx_j].vy) - (particles[idx_i].y + particles[idx_i].vy); // distance with lookahead
if (dy * dy < collDistSq) // particles are close if (dy * dy < collDistSq) // particles are close
collideParticles(particles[idx_i], particles[idx_j], dx, dy, collDistSq); collideParticles(particles[idx_i], particles[idx_j], dx, dy, collDistSq);
} }
@ -927,7 +927,7 @@ void ParticleSystem2D::handleCollisions() {
// takes two pointers to the particles to collide and the particle hardness (softer means more energy lost in collision, 255 means full hard) // 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, const int32_t collDistSq) { void ParticleSystem2D::collideParticles(PSparticle &particle1, PSparticle &particle2, int32_t dx, int32_t dy, const int32_t collDistSq) {
int32_t distanceSquared = dx * dx + dy * 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) // Calculate relative velocity note: could zero check but that does not improve overall speed but deminish it as that is rarely the case and pushing is still required
int32_t relativeVx = (int32_t)particle2.vx - (int32_t)particle1.vx; int32_t relativeVx = (int32_t)particle2.vx - (int32_t)particle1.vx;
int32_t relativeVy = (int32_t)particle2.vy - (int32_t)particle1.vy; int32_t relativeVy = (int32_t)particle2.vy - (int32_t)particle1.vy;
@ -992,7 +992,7 @@ void ParticleSystem2D::collideParticles(PSparticle &particle1, PSparticle &parti
// tried lots of configurations, it works best if not moved but given a little velocity, it tends to oscillate less this way // 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 // 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 // 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(distanceSquared < collDistSq && dotProduct > -250) { // too close and also slow, 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 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 pushamount = 1 + ((250 + dotProduct) >> 6); // the closer dotproduct is to zero, the closer the particles are
int32_t push = 0; int32_t push = 0;
@ -1100,15 +1100,15 @@ void blur2D(CRGB *colorbuffer, uint32_t xsize, uint32_t ysize, uint32_t xblur, u
width = 10; // buffer size is 10x10 width = 10; // buffer size is 10x10
} }
for(uint32_t y = ystart; y < ystart + ysize; y++) { for (uint32_t y = ystart; y < ystart + ysize; y++) {
carryover = BLACK; carryover = BLACK;
uint32_t indexXY = xstart + y * width; uint32_t indexXY = xstart + y * width;
for(uint32_t x = xstart; x < xstart + xsize; x++) { for (uint32_t x = xstart; x < xstart + xsize; x++) {
seeppart = colorbuffer[indexXY]; // create copy of current color seeppart = colorbuffer[indexXY]; // create copy of current color
fast_color_scale(seeppart, seep); // scale it and seep to neighbours fast_color_scale(seeppart, seep); // scale it and seep to neighbours
if (x > 0) { if (x > 0) {
fast_color_add(colorbuffer[indexXY - 1], seeppart); fast_color_add(colorbuffer[indexXY - 1], seeppart);
if(carryover) // note: check adds overhead but is faster on average if (carryover) // note: check adds overhead but is faster on average
fast_color_add(colorbuffer[indexXY], carryover); fast_color_add(colorbuffer[indexXY], carryover);
} }
carryover = seeppart; carryover = seeppart;
@ -1122,15 +1122,15 @@ void blur2D(CRGB *colorbuffer, uint32_t xsize, uint32_t ysize, uint32_t xblur, u
} }
seep = yblur >> 1; seep = yblur >> 1;
for(uint32_t x = xstart; x < xstart + xsize; x++) { for (uint32_t x = xstart; x < xstart + xsize; x++) {
carryover = BLACK; carryover = BLACK;
uint32_t indexXY = x + ystart * width; uint32_t indexXY = x + ystart * width;
for(uint32_t y = ystart; y < ystart + ysize; y++) { for (uint32_t y = ystart; y < ystart + ysize; y++) {
seeppart = colorbuffer[indexXY]; // create copy of current color seeppart = colorbuffer[indexXY]; // create copy of current color
fast_color_scale(seeppart, seep); // scale it and seep to neighbours fast_color_scale(seeppart, seep); // scale it and seep to neighbours
if (y > 0) { if (y > 0) {
fast_color_add(colorbuffer[indexXY - width], seeppart); fast_color_add(colorbuffer[indexXY - width], seeppart);
if(carryover) // note: check adds overhead but is faster on average if (carryover) // note: check adds overhead but is faster on average
fast_color_add(colorbuffer[indexXY], carryover); fast_color_add(colorbuffer[indexXY], carryover);
} }
carryover = seeppart; carryover = seeppart;
@ -1181,7 +1181,7 @@ bool allocateParticleSystemMemory2D(uint32_t numparticles, uint32_t numsources,
PSPRINTLN("PS 2D alloc"); PSPRINTLN("PS 2D alloc");
uint32_t requiredmemory = sizeof(ParticleSystem2D); uint32_t requiredmemory = sizeof(ParticleSystem2D);
uint32_t dummy; // dummy variable uint32_t dummy; // dummy variable
if((particleMemoryManager(numparticles, sizeof(PSparticle), dummy, dummy, SEGMENT.mode)) == nullptr) // allocate memory for particles 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 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) // functions above make sure these are a multiple of 4 bytes (to avoid alignment issues)
@ -1199,12 +1199,12 @@ 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) // 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) { bool initParticleSystem2D(ParticleSystem2D *&PartSys, uint32_t requestedsources, uint32_t additionalbytes, bool advanced, bool sizecontrol) {
PSPRINT("PS 2D init "); PSPRINT("PS 2D init ");
if(!strip.isMatrix) return false; // only for 2D if (!strip.isMatrix) return false; // only for 2D
uint32_t cols = SEGMENT.virtualWidth(); uint32_t cols = SEGMENT.virtualWidth();
uint32_t rows = SEGMENT.virtualHeight(); uint32_t rows = SEGMENT.virtualHeight();
uint32_t pixels = cols * rows; uint32_t pixels = cols * rows;
if(advanced) if (advanced)
updateRenderingBuffer(100, false, true); // 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); uint32_t numparticles = calculateNumberOfParticles2D(pixels, advanced, sizecontrol);
PSPRINT(" segmentsize:" + String(cols) + " " + String(rows)); PSPRINT(" segmentsize:" + String(cols) + " " + String(rows));
@ -1218,7 +1218,7 @@ bool initParticleSystem2D(ParticleSystem2D *&PartSys, uint32_t requestedsources,
PartSys = new (SEGENV.data) ParticleSystem2D(cols, rows, numparticles, numsources, advanced, sizecontrol); // particle system constructor 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 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:"); PSPRINTLN("******init done, pointers:");
#ifdef WLED_DEBUG_PS #ifdef WLED_DEBUG_PS
PSPRINT("framebfr size:"); PSPRINT("framebfr size:");
@ -1249,7 +1249,7 @@ ParticleSystem1D::ParticleSystem1D(uint32_t length, uint32_t numberofparticles,
fractionOfParticlesUsed = 255; // use all particles by default fractionOfParticlesUsed = 255; // use all particles by default
advPartProps = nullptr; //make sure we start out with null pointers (just in case memory was not cleared) advPartProps = nullptr; //make sure we start out with null pointers (just in case memory was not cleared)
//advPartSize = nullptr; //advPartSize = nullptr;
updatePSpointers(isadvanced); // 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); setSize(length);
setWallHardness(255); // set default wall hardness to max setWallHardness(255); // set default wall hardness to max
setGravity(0); //gravity disabled by default setGravity(0); //gravity disabled by default
@ -1264,7 +1264,7 @@ ParticleSystem1D::ParticleSystem1D(uint32_t length, uint32_t numberofparticles,
sources[i].source.ttl = 1; //set source alive sources[i].source.ttl = 1; //set source alive
} }
if(isadvanced) { if (isadvanced) {
for (uint32_t i = 0; i < numParticles; i++) { for (uint32_t i = 0; i < numParticles; i++) {
advPartProps[i].sat = 255; // set full saturation (for particles that are transferred from non-advanced system) advPartProps[i].sat = 255; // set full saturation (for particles that are transferred from non-advanced system)
} }
@ -1522,7 +1522,6 @@ void ParticleSystem1D::applyFriction(int32_t coefficient) {
particles[i].vx = ((int32_t)particles[i].vx * friction) / 255; particles[i].vx = ((int32_t)particles[i].vx * friction) / 255;
} }
#endif #endif
} }
@ -1530,7 +1529,7 @@ void ParticleSystem1D::applyFriction(int32_t coefficient) {
// if wrap is set, particles half out of bounds are rendered to the other side of the matrix // 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 // 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() {
if(blendingStyle == BLEND_STYLE_FADE && SEGMENT.isInTransition() && lastRender + (strip.getFrameTime() >> 1) > strip.now) // fixes speedup during transitions TODO: find a better solution if (blendingStyle == BLEND_STYLE_FADE && SEGMENT.isInTransition() && lastRender + (strip.getFrameTime() >> 1) > strip.now) // fixes speedup during transitions TODO: find a better solution
return; return;
lastRender = strip.now; lastRender = strip.now;
CRGB baseRGB; CRGB baseRGB;
@ -1542,7 +1541,7 @@ void ParticleSystem1D::ParticleSys_render() {
// update global blur (used for blur transitions) // update global blur (used for blur transitions)
int32_t motionbluramount = motionBlur; int32_t motionbluramount = motionBlur;
int32_t smearamount = smearBlur; int32_t smearamount = smearBlur;
if(pmem->inTransition == effectID) { // FX transition and this is the new FX: fade blur amount 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 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);
} }
@ -1552,7 +1551,7 @@ void ParticleSystem1D::ParticleSys_render() {
if (framebuffer) { if (framebuffer) {
// handle buffer blurring or clearing // handle buffer blurring or clearing
bool bufferNeedsUpdate = !pmem->inTransition || pmem->inTransition == effectID || isNonFadeTransition; // 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) { if (bufferNeedsUpdate) {
bool loadfromSegment = !renderSolo || isNonFadeTransition; 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) 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++) { for (int32_t x = 0; x <= maxXpixel; x++) {
@ -1595,7 +1594,7 @@ void ParticleSystem1D::ParticleSys_render() {
renderParticle(i, brightness, baseRGB, particlesettings.wrap); renderParticle(i, brightness, baseRGB, particlesettings.wrap);
} }
// apply smear-blur to rendered frame // apply smear-blur to rendered frame
if(globalSmear > 0) { if (globalSmear > 0) {
if (framebuffer) if (framebuffer)
blur1D(framebuffer, maxXpixel + 1, globalSmear, 0); blur1D(framebuffer, maxXpixel + 1, globalSmear, 0);
else else
@ -1605,7 +1604,7 @@ void ParticleSystem1D::ParticleSys_render() {
// add background color // add background color
uint32_t bg_color = SEGCOLOR(1); uint32_t bg_color = SEGCOLOR(1);
if (bg_color > 0) { //if not black if (bg_color > 0) { //if not black
for(int32_t i = 0; i <= maxXpixel; i++) { for (int32_t i = 0; i <= maxXpixel; i++) {
if (framebuffer) if (framebuffer)
fast_color_add(framebuffer[i], bg_color); fast_color_add(framebuffer[i], bg_color);
else else
@ -1618,9 +1617,9 @@ void ParticleSystem1D::ParticleSys_render() {
} }
// calculate pixel positions and brightness distribution and render the particle to local buffer or global buffer // 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) { void ParticleSystem1D::renderParticle(const uint32_t particleindex, const uint8_t brightness, const CRGB &color, const bool wrap) {
uint32_t size = particlesize; uint32_t size = particlesize;
if (advPartProps) {// use advanced size properties if (advPartProps) { // use advanced size properties
size = advPartProps[particleindex].size; 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) 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)
@ -1629,7 +1628,7 @@ void ParticleSystem1D::renderParticle(const uint32_t particleindex, const uint32
if (framebuffer) if (framebuffer)
fast_color_add(framebuffer[x], color, brightness); fast_color_add(framebuffer[x], color, brightness);
else else
SEGMENT.addPixelColor(x, color.scale8((uint8_t)brightness), true); SEGMENT.addPixelColor(x, color.scale8(brightness), true);
} }
return; return;
} }
@ -1715,7 +1714,7 @@ void ParticleSystem1D::renderParticle(const uint32_t particleindex, const uint32
else else
pxlisinframe[1] = false; pxlisinframe[1] = false;
} }
for(uint32_t i = 0; i < 2; i++) { for (uint32_t i = 0; i < 2; i++) {
if (pxlisinframe[i]) { if (pxlisinframe[i]) {
if (framebuffer) if (framebuffer)
fast_color_add(framebuffer[pixco[i]], color, pxlbrightness[i]); fast_color_add(framebuffer[pixco[i]], color, pxlbrightness[i]);
@ -1736,7 +1735,7 @@ void ParticleSystem1D::handleCollisions() {
int32_t overlap = particleHardRadius << 1; // overlap bins to include edge particles to neighbouring bins int32_t overlap = particleHardRadius << 1; // overlap bins to include edge particles to neighbouring bins
if (advPartProps) //may be using individual particle size if (advPartProps) //may be using individual particle size
overlap += 256; // add 2 * max radius (approximately) 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 maxBinParticles = max((uint32_t)50, (usedParticles + 1) / 4); // do not bin small amounts, limit max to 1/4 of particles
uint32_t numBins = (maxX + (BIN_WIDTH - 1)) / BIN_WIDTH; // 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 uint16_t binIndices[maxBinParticles]; // array to store indices of particles in a bin
uint32_t binParticleCount; // number of particles in the current bin uint32_t binParticleCount; // number of particles in the current bin
@ -1767,15 +1766,12 @@ void ParticleSystem1D::handleCollisions() {
for (uint32_t j = i + 1; j < binParticleCount; j++) { // check against higher number particles for (uint32_t j = i + 1; j < binParticleCount; j++) { // check against higher number particles
uint32_t idx_j = binIndices[j]; uint32_t idx_j = binIndices[j];
if (advPartProps) { // use advanced size properties if (advPartProps) { // use advanced size properties
collisiondistance = (PS_P_MINHARDRADIUS_1D << particlesize) + (((uint32_t)advPartProps[idx_i].size + (uint32_t)advPartProps[idx_j].size) >> 1); collisiondistance = (PS_P_MINHARDRADIUS_1D << particlesize) + ((advPartProps[idx_i].size + advPartProps[idx_j].size) >> 1);
} }
int32_t dx = particles[idx_j].x - particles[idx_i].x; int32_t dx = (particles[idx_j].x + particles[idx_j].vx) - (particles[idx_i].x + particles[idx_i].vx); // distance between particles with lookahead
int32_t dv = (int32_t)particles[idx_j].vx - (int32_t)particles[idx_i].vx; uint32_t dx_abs = abs(dx);
int32_t proximity = collisiondistance; if (dx_abs <= collisiondistance) { // collide if close
if (dv >= proximity) // particles would go past each other in next move update collideParticles(particles[idx_i], particleFlags[idx_i], particles[idx_j], particleFlags[idx_j], dx, dx_abs, collisiondistance);
proximity += abs(dv); // add speed difference to catch fast particles
if (dx <= proximity && dx >= -proximity) { // collide if close
collideParticles(particles[idx_i], particleFlags[idx_i], particles[idx_j], particleFlags[idx_j], dx, dv, collisiondistance);
} }
} }
} }
@ -1784,13 +1780,18 @@ void ParticleSystem1D::handleCollisions() {
} }
// handle a collision if close proximity is detected, i.e. dx and/or dy smaller than 2*PS_P_RADIUS // 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) // 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, const int32_t collisiondistance) { void ParticleSystem1D::collideParticles(PSparticle1D &particle1, const PSparticleFlags1D &particle1flags, PSparticle1D &particle2, const PSparticleFlags1D &particle2flags, const int32_t dx, const uint32_t dx_abs, const int32_t collisiondistance) {
int32_t dotProduct = (dx * relativeVx); // is always negative if moving towards each other int32_t dv = particle2.vx - particle1.vx;
uint32_t distance = abs(dx); int32_t dotProduct = (dx * dv); // is always negative if moving towards each other
if (dotProduct < 0) { // particles are 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 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 // Calculate new velocities after collision note: not using dot product like in 2D as impulse is purely speed depnedent
int32_t impulse = relativeVx * surfacehardness / 255; // note: not using dot product like in 2D as impulse is purely speed depnedent #if defined(CONFIG_IDF_TARGET_ESP32C3) || defined(ESP8266) // use bitshifts with rounding instead of division (2x faster)
int32_t impulse = ((dv * surfacehardness) + ((dv >> 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 impulse = (dv * surfacehardness) / 255;
#endif
particle1.vx += impulse; particle1.vx += impulse;
particle2.vx -= impulse; particle2.vx -= impulse;
@ -1802,13 +1803,17 @@ void ParticleSystem1D::collideParticles(PSparticle1D &particle1, const PSparticl
if (collisionHardness < PS_P_MINSURFACEHARDNESS_1D && (SEGMENT.call & 0x07) == 0) { // 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); const uint32_t coeff = collisionHardness + (250 - PS_P_MINSURFACEHARDNESS_1D);
#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
particle2.vx = ((int32_t)particle2.vx * coeff + (((int32_t)particle2.vx >> 31) & 0xFF)) >> 8;
#else // division is faster on ESP32, S2 and S3
particle1.vx = ((int32_t)particle1.vx * coeff) / 255; particle1.vx = ((int32_t)particle1.vx * coeff) / 255;
particle2.vx = ((int32_t)particle2.vx * coeff) / 255; particle2.vx = ((int32_t)particle2.vx * coeff) / 255;
#endif
} }
} }
if (distance < (collisiondistance - 8) && abs(relativeVx) < 5) // overlapping and moving slowly if (dx_abs < (collisiondistance - 8) && abs(dv) < 5) { // overlapping and moving slowly
{
// particles have volume, push particles apart if they are too close // 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 // 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 // note: like in 2D, pushing by a distance makes softer piles collapse, giving particles speed prevents that and looks nicer
@ -1818,10 +1823,10 @@ void ParticleSystem1D::collideParticles(PSparticle1D &particle1, const PSparticl
particle1.vx -= pushamount; particle1.vx -= pushamount;
particle2.vx += pushamount; particle2.vx += pushamount;
if(distance < collisiondistance >> 1) { // too close, force push particles so they dont collapse if (dx_abs < collisiondistance >> 1) { // too close, force push particles so they dont collapse
pushamount = 1 + ((collisiondistance - distance) >> 3); // note: push amount found by experimentation pushamount = 1 + ((collisiondistance - dx_abs) >> 3); // note: push amount found by experimentation
if(particle1.x < (maxX >> 1)) { // lower half, push particle with larger x in positive direction 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 if (dx < 0 && !particle1flags.fixed) { // particle2.x < particle1.x -> push particle 1
particle1.vx++;// += pushamount; particle1.vx++;// += pushamount;
particle1.x += pushamount; particle1.x += pushamount;
@ -1836,7 +1841,7 @@ void ParticleSystem1D::collideParticles(PSparticle1D &particle1, const PSparticl
particle2.vx--;// -= pushamount; particle2.vx--;// -= pushamount;
particle2.x -= pushamount; particle2.x -= pushamount;
} }
else if (!particle2flags.fixed) { // particle1.x < particle2.x -> push particle 1 else if (!particle1flags.fixed) { // particle1.x < particle2.x -> push particle 1
particle1.vx--;// -= pushamount; particle1.vx--;// -= pushamount;
particle1.x -= pushamount; particle1.x -= pushamount;
} }
@ -1925,7 +1930,7 @@ uint32_t calculateNumberOfSources1D(const uint32_t requestedsources) {
bool allocateParticleSystemMemory1D(const uint32_t numparticles, const uint32_t numsources, const bool isadvanced, const 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 requiredmemory = sizeof(ParticleSystem1D);
uint32_t dummy; // dummy variable uint32_t dummy; // dummy variable
if(particleMemoryManager(numparticles, sizeof(PSparticle1D), dummy, dummy, SEGMENT.mode) == nullptr) // allocate memory for particles 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 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) // functions above make sure these are a multiple of 4 bytes (to avoid alignment issues)
requiredmemory += sizeof(PSparticleFlags1D) * numparticles; requiredmemory += sizeof(PSparticleFlags1D) * numparticles;
@ -1940,7 +1945,7 @@ bool allocateParticleSystemMemory1D(const uint32_t numparticles, const uint32_t
// 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) // 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) { 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 if (SEGLEN == 1) return false; // single pixel not supported
if(advanced) if (advanced)
updateRenderingBuffer(10, false, true); // buffer for advanced particles, fixed size updateRenderingBuffer(10, false, true); // buffer for advanced particles, fixed size
uint32_t numparticles = calculateNumberOfParticles1D(fractionofparticles, advanced); uint32_t numparticles = calculateNumberOfParticles1D(fractionofparticles, advanced);
uint32_t numsources = calculateNumberOfSources1D(requestedsources); uint32_t numsources = calculateNumberOfSources1D(requestedsources);
@ -1961,12 +1966,12 @@ void blur1D(CRGB *colorbuffer, uint32_t size, uint32_t blur, uint32_t start)
CRGB seeppart, carryover; CRGB seeppart, carryover;
uint32_t seep = blur >> 1; uint32_t seep = blur >> 1;
carryover = BLACK; carryover = BLACK;
for(uint32_t x = start; x < start + size; x++) { for (uint32_t x = start; x < start + size; x++) {
seeppart = colorbuffer[x]; // create copy of current color seeppart = colorbuffer[x]; // create copy of current color
fast_color_scale(seeppart, seep); // scale it and seep to neighbours fast_color_scale(seeppart, seep); // scale it and seep to neighbours
if (x > 0) { if (x > 0) {
fast_color_add(colorbuffer[x-1], seeppart); fast_color_add(colorbuffer[x-1], seeppart);
if(carryover) // note: check adds overhead but is faster on average if (carryover) // note: check adds overhead but is faster on average
fast_color_add(colorbuffer[x], carryover); // is black on first pass fast_color_add(colorbuffer[x], carryover); // is black on first pass
} }
carryover = seeppart; carryover = seeppart;
@ -2022,7 +2027,7 @@ static bool checkBoundsAndWrap(int32_t &position, const int32_t max, const int32
// 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 // 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 // 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 // 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) { __attribute__((optimize("O2"))) static void fast_color_add(CRGB &c1, const CRGB &c2, const uint8_t scale) {
uint32_t r, g, b; uint32_t r, g, b;
if (scale < 255) { if (scale < 255) {
r = c1.r + ((c2.r * scale) >> 8); r = c1.r + ((c2.r * scale) >> 8);
@ -2034,9 +2039,9 @@ static void fast_color_add(CRGB &c1, const CRGB &c2, const uint32_t scale) {
b = c1.b + c2.b; b = c1.b + c2.b;
} }
uint32_t max = std::max(r,g); // check for overflow, using max() is faster as the compiler can optimize // note: this chained comparison is the fastest method for max of 3 values (faster than std:max() or using xor)
max = std::max(max,b); uint32_t max = (r > g) ? ((r > b) ? r : b) : ((g > b) ? g : b);
if (max < 256) { if (max <= 255) {
c1.r = r; // save result to c1 c1.r = r; // save result to c1
c1.g = g; c1.g = g;
c1.b = b; c1.b = b;
@ -2049,7 +2054,7 @@ static void fast_color_add(CRGB &c1, const CRGB &c2, const uint32_t scale) {
} }
// faster than fastled color scaling as it does in place scaling // faster than fastled color scaling as it does in place scaling
static void fast_color_scale(CRGB &c, const uint32_t scale) { __attribute__((optimize("O2"))) static void fast_color_scale(CRGB &c, const uint8_t scale) {
c.r = ((c.r * scale) >> 8); c.r = ((c.r * scale) >> 8);
c.g = ((c.g * scale) >> 8); c.g = ((c.g * scale) >> 8);
c.b = ((c.b * scale) >> 8); c.b = ((c.b * scale) >> 8);
@ -2090,7 +2095,7 @@ void* allocatePSmemory(size_t size, bool overridelimit) {
// deallocate memory and update data usage, use with care! // deallocate memory and update data usage, use with care!
void deallocatePSmemory(void* dataptr, uint32_t size) { void deallocatePSmemory(void* dataptr, uint32_t size) {
PSPRINTLN("deallocating PSmemory:" + String(size)); PSPRINTLN("deallocating PSmemory:" + String(size));
if(dataptr == nullptr) return; // safety check 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 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()); Segment::addUsedSegmentData(size <= Segment::getUsedSegmentData() ? -size : -Segment::getUsedSegmentData());
} }
@ -2120,7 +2125,7 @@ void* particleMemoryManager(const uint32_t requestedParticles, size_t structSize
} }
} }
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->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 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 pmem->inTransition = false; // reset transition flag
else else
pmem->inTransition = effectID; // save the ID of the new effect (required to determine blur amount in rendering function) pmem->inTransition = effectID; // save the ID of the new effect (required to determine blur amount in rendering function)
@ -2148,7 +2153,7 @@ void* particleMemoryManager(const uint32_t requestedParticles, size_t structSize
} }
// now we have a valid buffer, if this is a PS to PS FX transition: transfer particles slowly to new FX // 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, invoke final transfer if (!SEGMENT.isInTransition()) pmem->inTransition = false; // transition has ended, invoke final transfer
if (pmem->inTransition) { if (pmem->inTransition) {
uint32_t maxParticles = pmem->buffersize / structSize; // maximum number of particles that fit in the buffer uint32_t maxParticles = pmem->buffersize / structSize; // maximum number of particles that fit in the buffer
uint16_t progress = SEGMENT.progress(); // transition progress uint16_t progress = SEGMENT.progress(); // transition progress
@ -2156,13 +2161,13 @@ void* particleMemoryManager(const uint32_t requestedParticles, size_t structSize
if (SEGMENT.mode == effectID) { // new effect ID -> function was called from new FX if (SEGMENT.mode == effectID) { // new effect ID -> function was called from new FX
PSPRINTLN("new effect"); PSPRINTLN("new effect");
newAvailable = (maxParticles * progress) >> 16; // update total particles available to this PS (newAvailable is guaranteed to be smaller than maxParticles) 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 < 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, 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) uint32_t bufferoffset = (maxParticles - 1) - newAvailable; // offset to new effect particles (in particle structs, not bytes)
if(bufferoffset < maxParticles) // safety check if (bufferoffset < maxParticles) // safety check
buffer = (void*)((uint8_t*)buffer + bufferoffset * structSize); // new effect gets the end of the buffer 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 int32_t totransfer = newAvailable - availableToPS; // number of particles to transfer in this transition update
if(totransfer > 0) // safety check if (totransfer > 0) // safety check
particleHandover(buffer, structSize, totransfer); particleHandover(buffer, structSize, totransfer);
} }
else { // this was called from the old FX else { // this was called from the old FX
@ -2170,23 +2175,23 @@ void* particleMemoryManager(const uint32_t requestedParticles, size_t structSize
SEGMENT.loadOldPalette(); // load the old palette into segment palette SEGMENT.loadOldPalette(); // load the old palette into segment palette
progress = 0xFFFFU - progress; // inverted transition progress progress = 0xFFFFU - progress; // inverted transition progress
newAvailable = ((maxParticles * progress) >> 16); // result is guaranteed to be smaller than maxParticles 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 > 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) 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 // note: buffer pointer stays the same, number of available particles is reduced
} }
availableToPS = newAvailable; availableToPS = newAvailable;
} else if(pmem->transferParticles) { // no PS transition, full buffer available } else if (pmem->transferParticles) { // no PS transition, full buffer available
// transition ended (or blending is disabled) -> transfer all remaining particles // transition ended (or blending is disabled) -> transfer all remaining particles
PSPRINTLN("PS transition ended, final particle handover"); PSPRINTLN("PS transition ended, final particle handover");
uint32_t maxParticles = pmem->buffersize / structSize; // maximum number of particles that fit in the buffer uint32_t maxParticles = pmem->buffersize / structSize; // maximum number of particles that fit in the buffer
if (maxParticles > availableToPS) { // not all particles transferred yet if (maxParticles > availableToPS) { // not all particles transferred yet
uint32_t totransfer = maxParticles - availableToPS; // transfer all remaining particles uint32_t totransfer = maxParticles - availableToPS; // transfer all remaining particles
if(totransfer <= maxParticles) // safety check if (totransfer <= maxParticles) // safety check
particleHandover(buffer, structSize, totransfer); particleHandover(buffer, structSize, totransfer);
if(maxParticles > numParticlesUsed) { // FX uses less than max: move the already 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; uint32_t usedbytes = availableToPS * structSize;
int32_t bufferoffset = (maxParticles - 1) - availableToPS; // offset to existing particles (see above) int32_t bufferoffset = (maxParticles - 1) - availableToPS; // offset to existing particles (see above)
if(bufferoffset < (int)maxParticles) { // safety check if (bufferoffset < (int)maxParticles) { // safety check
void* currentBuffer = (void*)((uint8_t*)buffer + bufferoffset * structSize); // pointer to current buffer start 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 memmove(buffer, currentBuffer, usedbytes); // move the existing particles to the beginning of the buffer
} }
@ -2243,7 +2248,7 @@ void particleHandover(void *buffer, size_t structSize, int32_t numToTransfer) {
PSparticle *particles = (PSparticle *)buffer; PSparticle *particles = (PSparticle *)buffer;
for (int32_t i = 0; i < numToTransfer; i++) { for (int32_t i = 0; i < numToTransfer; i++) {
if (blendingStyle == BLEND_STYLE_FADE) { if (blendingStyle == BLEND_STYLE_FADE) {
if(particles[i].ttl > maxTTL) if (particles[i].ttl > maxTTL)
particles[i].ttl = maxTTL + hw_random16(150); // reduce TTL so it will die soon particles[i].ttl = maxTTL + hw_random16(150); // reduce TTL so it will die soon
} }
else else
@ -2258,7 +2263,7 @@ void particleHandover(void *buffer, size_t structSize, int32_t numToTransfer) {
PSparticle1D *particles = (PSparticle1D *)buffer; PSparticle1D *particles = (PSparticle1D *)buffer;
for (int32_t i = 0; i < numToTransfer; i++) { for (int32_t i = 0; i < numToTransfer; i++) {
if (blendingStyle == BLEND_STYLE_FADE) { if (blendingStyle == BLEND_STYLE_FADE) {
if(particles[i].ttl > maxTTL) if (particles[i].ttl > maxTTL)
particles[i].ttl = maxTTL + hw_random16(150); // reduce TTL so it will die soon particles[i].ttl = maxTTL + hw_random16(150); // reduce TTL so it will die soon
} }
else else
@ -2285,7 +2290,7 @@ bool segmentIsOverlay(void) { // TODO: this only needs to be checked when segmen
// Check for overlap with all previous segments // Check for overlap with all previous segments
for (unsigned i = 0; i < segID; i++) { for (unsigned i = 0; i < segID; i++) {
if(strip._segments[i].freeze) continue; // skip inactive segments if (strip._segments[i].freeze) continue; // skip inactive segments
unsigned startX = strip._segments[i].start; unsigned startX = strip._segments[i].start;
unsigned endX = strip._segments[i].stop; unsigned endX = strip._segments[i].stop;
unsigned startY = strip._segments[i].startY; unsigned startY = strip._segments[i].startY;
@ -2316,15 +2321,15 @@ void updateRenderingBuffer(uint32_t requiredpixels, bool isFramebuffer, bool ini
PSPRINTLN("updateRenderingBuffer"); PSPRINTLN("updateRenderingBuffer");
uint16_t& targetBufferSize = isFramebuffer ? frameBufferSize : renderBufferSize; // corresponding buffer size uint16_t& targetBufferSize = isFramebuffer ? frameBufferSize : renderBufferSize; // corresponding buffer size
// if(isFramebuffer) return; // debug/testing only: disable frame-buffer // if (isFramebuffer) return; // debug/testing only: disable frame-buffer
if(targetBufferSize < requiredpixels) { // check current buffer size if (targetBufferSize < requiredpixels) { // check current buffer size
CRGB** targetBuffer = isFramebuffer ? &framebuffer : &renderbuffer; // pointer to target buffer 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 || initialize) { // update only if initilizing or if buffer exists (prevents repeatet allocation attempts if initial alloc failed)
if(*targetBuffer) // buffer exists, free it if (*targetBuffer) // buffer exists, free it
deallocatePSmemory((void*)(*targetBuffer), targetBufferSize * sizeof(CRGB)); deallocatePSmemory((void*)(*targetBuffer), targetBufferSize * sizeof(CRGB));
*targetBuffer = reinterpret_cast<CRGB *>(allocatePSmemory(requiredpixels * sizeof(CRGB), false)); *targetBuffer = reinterpret_cast<CRGB *>(allocatePSmemory(requiredpixels * sizeof(CRGB), false));
if(*targetBuffer) if (*targetBuffer)
targetBufferSize = requiredpixels; targetBufferSize = requiredpixels;
else else
targetBufferSize = 0; targetBufferSize = 0;
@ -2336,10 +2341,10 @@ void updateRenderingBuffer(uint32_t requiredpixels, bool isFramebuffer, bool ini
// note: doing it this way makes it independent of the implementation of segment management but is not the most memory efficient way // note: doing it this way makes it independent of the implementation of segment management but is not the most memory efficient way
void servicePSmem() { void servicePSmem() {
// Increment watchdog for each entry and deallocate if idle too long (i.e. no PS running on that segment) // Increment watchdog for each entry and deallocate if idle too long (i.e. no PS running on that segment)
if(partMemList.size() > 0) { if (partMemList.size() > 0) {
for (size_t i = 0; i < partMemList.size(); i++) { for (size_t i = 0; i < partMemList.size(); i++) {
if(strip.getSegmentsNum() > i) { // segment still exists if (strip.getSegmentsNum() > i) { // segment still exists
if(strip._segments[i].freeze) continue; // skip frozen segments (incrementing watchdog will delete memory, leading to crash) if (strip._segments[i].freeze) continue; // skip frozen segments (incrementing watchdog will delete memory, leading to crash)
} }
partMemList[i].watchdog++; // Increment watchdog counter partMemList[i].watchdog++; // Increment watchdog counter
PSPRINT("pmem servic. list size: "); PSPRINT("pmem servic. list size: ");
@ -2356,12 +2361,12 @@ void servicePSmem() {
} }
} }
else { // no particle system running, release buffer memory else { // no particle system running, release buffer memory
if(framebuffer) { if (framebuffer) {
deallocatePSmemory((void*)framebuffer, frameBufferSize * sizeof(CRGB)); // free the buffers deallocatePSmemory((void*)framebuffer, frameBufferSize * sizeof(CRGB)); // free the buffers
framebuffer = nullptr; framebuffer = nullptr;
frameBufferSize = 0; frameBufferSize = 0;
} }
if(renderbuffer) { if (renderbuffer) {
deallocatePSmemory((void*)renderbuffer, renderBufferSize * sizeof(CRGB)); deallocatePSmemory((void*)renderbuffer, renderBufferSize * sizeof(CRGB));
renderbuffer = nullptr; renderbuffer = nullptr;
renderBufferSize = 0; renderBufferSize = 0;
@ -2371,34 +2376,34 @@ void servicePSmem() {
// transfer the frame buffer to the segment and handle transitional rendering (both FX render to the same buffer so they mix) // 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) { void transferBuffer(uint32_t width, uint32_t height, bool useAdditiveTransfer) {
if(!framebuffer) return; // no buffer, nothing to transfer if (!framebuffer) return; // no buffer, nothing to transfer
PSPRINT(" xfer buf "); PSPRINT(" xfer buf ");
#ifndef WLED_DISABLE_MODE_BLEND #ifndef WLED_DISABLE_MODE_BLEND
bool tempBlend = SEGMENT.getmodeBlend(); bool tempBlend = SEGMENT.getmodeBlend();
if(pmem->inTransition && blendingStyle == BLEND_STYLE_FADE) { 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) SEGMENT.modeBlend(false); // temporarily disable FX blending in PS to PS transition (using local buffer to do PS blending)
} }
#endif #endif
if(height) { // is 2D, 1D passes height = 0 if (height) { // is 2D, 1D passes height = 0
for (uint32_t y = 0; y < height; y++) { for (uint32_t y = 0; y < height; y++) {
int index = y * width; // current row index for 1D buffer int index = y * width; // current row index for 1D buffer
for (uint32_t x = 0; x < width; x++) { for (uint32_t x = 0; x < width; x++) {
CRGB *c = &framebuffer[index++]; CRGB *c = &framebuffer[index++];
uint32_t clr = RGBW32(c->r,c->g,c->b,0); // convert to 32bit color uint32_t clr = RGBW32(c->r,c->g,c->b,0); // convert to 32bit color
if(useAdditiveTransfer) { if (useAdditiveTransfer) {
uint32_t segmentcolor = SEGMENT.getPixelColorXY((int)x, (int)y); uint32_t segmentcolor = SEGMENT.getPixelColorXY((int)x, (int)y);
CRGB segmentRGB = CRGB(segmentcolor); CRGB segmentRGB = CRGB(segmentcolor);
if(clr == 0) // frame buffer is black, just update the framebuffer if (clr == 0) // frame buffer is black, just update the framebuffer
*c = segmentRGB; *c = segmentRGB;
else { // color to add to segment is not black else { // color to add to segment is not black
if(segmentcolor) { if (segmentcolor) {
fast_color_add(*c, segmentRGB); // add segment color back to buffer if not black 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 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 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. //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 else
SEGMENT.setPixelColorXY((int)x, (int)y, clr); SEGMENT.setPixelColorXY((int)x, (int)y, clr);
} }
@ -2407,20 +2412,20 @@ void transferBuffer(uint32_t width, uint32_t height, bool useAdditiveTransfer) {
for (uint32_t x = 0; x < width; x++) { for (uint32_t x = 0; x < width; x++) {
CRGB *c = &framebuffer[x]; CRGB *c = &framebuffer[x];
uint32_t clr = RGBW32(c->r,c->g,c->b,0); uint32_t clr = RGBW32(c->r,c->g,c->b,0);
if(useAdditiveTransfer) { if (useAdditiveTransfer) {
uint32_t segmentcolor = SEGMENT.getPixelColor((int)x);; uint32_t segmentcolor = SEGMENT.getPixelColor((int)x);;
CRGB segmentRGB = CRGB(segmentcolor); CRGB segmentRGB = CRGB(segmentcolor);
if(clr == 0) // frame buffer is black, just load the color (for next frame) if (clr == 0) // frame buffer is black, just load the color (for next frame)
*c = segmentRGB; *c = segmentRGB;
else { // color to add to segment is not black else { // color to add to segment is not black
if(segmentcolor) { if (segmentcolor) {
fast_color_add(*c, segmentRGB); // add segment color back to buffer if not black 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) 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 SEGMENT.setPixelColor((int)x, clr); // save back to segment after adding local buffer
} }
} }
//if(color > 0) // not black //if (color > 0) // not black
else else
SEGMENT.setPixelColor((int)x, clr); SEGMENT.setPixelColor((int)x, clr);
} }

View File

@ -211,7 +211,7 @@ public:
private: private:
//rendering functions //rendering functions
void ParticleSys_render(); void ParticleSys_render();
[[gnu::hot]] 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 uint8_t brightness, const CRGB& color, const bool wrapX, const bool wrapY);
//paricle physics applied by system if flags are set //paricle physics applied by system if flags are set
void applyGravity(); // applies gravity to all particles void applyGravity(); // applies gravity to all particles
void handleCollisions(); void handleCollisions();
@ -343,7 +343,7 @@ public:
int32_t sprayEmit(const PSsource1D &emitter); int32_t sprayEmit(const PSsource1D &emitter);
void particleMoveUpdate(PSparticle1D &part, PSparticleFlags1D &partFlags, PSsettings1D *options = NULL, PSadvancedParticle1D *advancedproperties = NULL); // move function void particleMoveUpdate(PSparticle1D &part, PSparticleFlags1D &partFlags, PSsettings1D *options = NULL, PSadvancedParticle1D *advancedproperties = NULL); // move function
//particle physics //particle physics
[[gnu::hot]] 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 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 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 void applyFriction(const int32_t coefficient); // apply friction to all used particles
@ -378,12 +378,12 @@ public:
private: private:
//rendering functions //rendering functions
void ParticleSys_render(void); void ParticleSys_render(void);
void renderParticle(const uint32_t particleindex, const uint32_t brightness, const CRGB &color, const bool wrap); [[gnu::hot]] void renderParticle(const uint32_t particleindex, const uint8_t brightness, const CRGB &color, const bool wrap);
//paricle physics applied by system if flags are set //paricle physics applied by system if flags are set
void applyGravity(); // applies gravity to all particles void applyGravity(); // applies gravity to all particles
void handleCollisions(); void handleCollisions();
[[gnu::hot]] void collideParticles(PSparticle1D &particle1, const PSparticleFlags1D &particle1flags, PSparticle1D &particle2, const PSparticleFlags1D &particle2flags, int32_t dx, int32_t relativeVx, const int32_t collisiondistance); [[gnu::hot]] void collideParticles(PSparticle1D &particle1, const PSparticleFlags1D &particle1flags, PSparticle1D &particle2, const PSparticleFlags1D &particle2flags, const int32_t dx, const uint32_t dx_abs, const int32_t collisiondistance);
//utility functions //utility functions
void updatePSpointers(const 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