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
} }
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) < 8) {
// calculate target position depending on direction // calculate target position depending on direction
bool closeToTarget = false; auto calcTargetPos = [&](size_t i) {
bool reachedTarget = false; return PartSys->particleFlags[i].reversegrav ?
if (PartSys->particleFlags[i].reversegrav) { // up PartSys->maxX - i * PS_P_RADIUS_1D - positionOffset
targetposition = PartSys->maxX - (i * PS_P_RADIUS_1D) - positionOffset; // target resting position : (PartSys->usedParticles - i) * PS_P_RADIUS_1D - positionOffset;
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; for (uint32_t i = 0; i < PartSys->usedParticles; i++) { // check if particle reached target position after falling
} if (PartSys->particleFlags[i].fixed == false && abs(PartSys->particles[i].vx) < 5) {
else { // down, highest index particle drops first int32_t targetposition = calcTargetPos(i);
targetposition = (PartSys->usedParticles - i) * PS_P_RADIUS_1D - positionOffset; // target resting position note: using -offset instead of -1 + offset bool closeToTarget = abs(targetposition - PartSys->particles[i].x) < 3 * PS_P_RADIUS_1D;
if (PartSys->particles[i].x - targetposition <= 5 * PS_P_RADIUS_1D) if (closeToTarget) { // close to target and slow speed
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,9 +10103,11 @@ 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) {
SEGMENT.aux0 += hueincrement; // change color SEGMENT.aux0 += hueincrement; // change color
@ -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
@ -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
@ -694,7 +694,7 @@ 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;
@ -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
@ -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;
@ -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
} }
@ -1618,7 +1617,7 @@ 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;
@ -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;
} }
@ -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,8 +1823,8 @@ 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
@ -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;
} }
@ -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);

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();
@ -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