From 02f14baad44d52f222c27944c372efbc41a404fc Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Tue, 15 Apr 2025 19:07:21 +0200 Subject: [PATCH] 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 --- wled00/FX.cpp | 407 +++++++++++++++++++++++++++++++----- wled00/FX.h | 4 +- wled00/FXparticleSystem.cpp | 243 ++++++++++----------- wled00/FXparticleSystem.h | 8 +- 4 files changed, 482 insertions(+), 180 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 832c2e404..f439e4c66 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -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 PartSys->sources[j].source.y = PS_P_RADIUS; // start from bottom PartSys->sources[j].source.x = (PartSys->maxX >> 2) + hw_random(PartSys->maxX >> 1); // centered half - PartSys->sources[j].source.vy = (SEGMENT.custom3) + random16(SEGMENT.custom1 >> 3) + 5; // rocket speed TODO: need to adjust for segment height + PartSys->sources[j].source.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.sat = 30; // low saturation -> exhaust is off-white 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; speed += 3 + ((SEGMENT.intensity >> 6)); // increase speed to form a second wave PartSys->sources[j].source.hue += hueincrement; // new color for next circle - PartSys->sources[j].source.sat = min((uint16_t)150, random16()); + PartSys->sources[j].source.sat = 100 + hw_random16(156); } 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) settingTracker = reinterpret_cast(PartSys->PSdataEnd); //assign data pointer direction = reinterpret_cast(PartSys->PSdataEnd + 4); //assign data pointer - PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 1, 255)); + PartSys->setUsedParticles(1 + ((SEGMENT.intensity * 255) >> 8)); PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur PartSys->setGravity(map(SEGMENT.custom3, 0, 31, 1, 30)); - PartSys->enableParticleCollisions(true, 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 if ((SEGMENT.intensity | (PartSys->getAvailableParticles() << 8)) != *settingTracker) { // initialize, getAvailableParticles changes while in FX transition *settingTracker = SEGMENT.intensity | (PartSys->getAvailableParticles() << 8); for (uint32_t i = 0; i < PartSys->usedParticles; i++) { - PartSys->particleFlags[i].reversegrav = true; + PartSys->particleFlags[i].reversegrav = true; // resting particles dont fall *direction = 0; // down SEGENV.aux1 = 1; // initialize below } 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 - int32_t targetposition; - if (PartSys->particleFlags[i].fixed == false) { // && abs(PartSys->particles[i].vx) < 8) { - // calculate target position depending on direction - bool closeToTarget = false; - bool reachedTarget = false; - if (PartSys->particleFlags[i].reversegrav) { // up - targetposition = PartSys->maxX - (i * PS_P_RADIUS_1D) - positionOffset; // target resting position - if (targetposition - PartSys->particles[i].x <= 5 * PS_P_RADIUS_1D) - closeToTarget = true; - if (PartSys->particles[i].x >= targetposition) // particle has reached target position, pin it. if not pinned, they do not stack well on larger piles - reachedTarget = true; - } - else { // 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 + if (PartSys->particleFlags[i].fixed == false && abs(PartSys->particles[i].vx) < 5) { + int32_t targetposition = calcTargetPos(i); + bool closeToTarget = abs(targetposition - PartSys->particles[i].x) < 3 * PS_P_RADIUS_1D; + if (closeToTarget) { // close to target and slow speed PartSys->particles[i].x = targetposition; // set exact position PartSys->particleFlags[i].fixed = true; // pin particle } @@ -9576,19 +9568,20 @@ uint16_t mode_particleHourglass(void) { 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 for (uint32_t i = 0; i < PartSys->usedParticles; i++) { PartSys->particleFlags[i].collide = true; PartSys->particleFlags[i].perpetual = true; PartSys->particles[i].ttl = 260; - uint32_t targetposition; - //calculate target position depending on direction - if (PartSys->particleFlags[i].reversegrav) - targetposition = PartSys->maxX - (i * PS_P_RADIUS_1D + positionOffset); // target resting position - else - targetposition = (PartSys->usedParticles - i) * PS_P_RADIUS_1D - positionOffset; // target resting position -5 - PS_P_RADIUS_1D/2 - - PartSys->particles[i].x = targetposition; + PartSys->particles[i].x = calcTargetPos(i); PartSys->particleFlags[i].fixed = true; } } @@ -9699,7 +9692,7 @@ uint16_t mode_particleBalance(void) { // Particle System settings 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->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 @@ -9716,6 +9709,17 @@ uint16_t mode_particleBalance(void) { } 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 int32_t xgravity; int32_t increment = (SEGMENT.speed >> 6) + 1; @@ -9756,7 +9760,7 @@ by DedeHai (Damian Schneider) uint16_t mode_particleChase(void) { ParticleSystem1D *PartSys = nullptr; 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 SEGENV.aux0 = 0xFFFF; // invalidate *PartSys->PSdataEnd = 1; // huedir @@ -9766,39 +9770,43 @@ uint16_t mode_particleChase(void) { PartSys = reinterpret_cast(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->setColorByPosition(SEGMENT.check3); - PartSys->setMotionBlur(8 + ((SEGMENT.custom3) << 3)); // anable motion blur - // uint8_t* basehue = (PartSys->PSdataEnd + 2); //assign data pointer - + PartSys->setMotionBlur(7 + ((SEGMENT.custom3) << 3)); // anable motion blur + uint32_t numParticles = 1 + map(SEGMENT.intensity, 0, 255, 2, 255 / (1 + (SEGMENT.custom1 >> 6))); // depends on intensity and particle size (custom1), minimum 1 + 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 if (SEGENV.aux0 != settingssum) { // settings changed changed, update - uint32_t numParticles = map(SEGMENT.intensity, 0, 255, 2, 255 / (1 + (SEGMENT.custom1 >> 6))); // depends on intensity and particle size (custom1) - if (numParticles == 0) numParticles = 1; // minimum 1 particle - PartSys->setUsedParticles(numParticles); - SEGENV.step = (PartSys->maxX + (PS_P_RADIUS_1D << 5)) / PartSys->usedParticles; // spacing between particles + if (SEGMENT.check1) + SEGENV.step = PartSys->advPartProps[0].size / 2 + (PartSys->maxX / numParticles); + else + SEGENV.step = (PartSys->maxX + (PS_P_RADIUS_1D << 5)) / numParticles; // spacing between particles for (int32_t i = 0; i < (int32_t)PartSys->usedParticles; i++) { PartSys->advPartProps[i].sat = 255; PartSys->particles[i].x = (i - 1) * SEGENV.step; // distribute evenly (starts out of frame for i=0) - PartSys->particles[i].vx = SEGMENT.speed >> 1; + PartSys->particles[i].vx = SEGMENT.speed >> 2; PartSys->advPartProps[i].size = SEGMENT.custom1; 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 PartSys->particles[i].hue = hw_random16(); } 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) for (int32_t i = (int32_t)PartSys->usedParticles - 1; i >= 0; i--) { // check from the back, last particle wraps first, multiple particles can overrun per frame if (PartSys->particles[i].x > PartSys->maxX + PS_P_RADIUS_1D + PartSys->advPartProps[i].size) { // wrap it around uint32_t nextindex = (i + 1) % PartSys->usedParticles; - PartSys->particles[i].x = PartSys->particles[nextindex].x - (int)SEGENV.step; + 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) PartSys->particles[i].hue = PartSys->particles[nextindex].hue - huestep; 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 } + if (SEGMENT.check1) { // playful mode, changes hue, size, speed, density dynamically + int8_t* huedir = reinterpret_cast(PartSys->PSdataEnd); //assign data pointer + int8_t* stepdir = reinterpret_cast(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->update(); // update and render return FRAMETIME; } -static const char _data_FX_MODE_PS_CHASE[] PROGMEM = "PS Chase@!,Density,Size,Hue,Blur,,,Position Color;,!;!;1;pal=11,sx=50,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) @@ -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 - Uses palette for particle color by DedeHai (Damian Schneider) */ -uint16_t mode_particle1Dsonicstream(void) { +uint16_t mode_particle1DsonicStream(void) { ParticleSystem1D *PartSys = nullptr; if (SEGMENT.call == 0) { // initialization @@ -10029,7 +10062,6 @@ uint16_t mode_particle1Dsonicstream(void) { PartSys->sources[0].source.x = 0; // at start //PartSys->sources[1].source.x = PartSys->maxX; // at end PartSys->sources[0].var = 0;//SEGMENT.custom1 >> 3; - PartSys->sources[0].sat = 255; } else PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS @@ -10040,7 +10072,6 @@ uint16_t mode_particle1Dsonicstream(void) { PartSys->updateSystem(); // update system properties (dimensions and data pointers) PartSys->setMotionBlur(20 + (SEGMENT.custom2 >> 1)); // anable motion blur PartSys->setSmearBlur(200); // smooth out the edges - PartSys->sources[0].v = 5 + (SEGMENT.speed >> 2); // FFT processing @@ -10050,11 +10081,10 @@ uint16_t mode_particle1Dsonicstream(void) { uint32_t baseBin = SEGMENT.custom3 >> 1; // 0 - 15 map(SEGMENT.custom3, 0, 31, 0, 14); loudness = fftResult[baseBin];// + fftResult[baseBin + 1]; - int mids = sqrt16((int)fftResult[5] + (int)fftResult[6] + (int)fftResult[7] + (int)fftResult[8] + (int)fftResult[9] + (int)fftResult[10]); // average the mids, bin 5 is ~500Hz, bin 10 is ~2kHz (see audio_reactive.h) if (baseBin > 12) loudness = loudness << 2; // double loudness for high frequencies (better detecion) - uint32_t threshold = 150 - (SEGMENT.intensity >> 1); + uint32_t threshold = 140 - (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 @@ -10062,6 +10092,7 @@ uint16_t mode_particle1Dsonicstream(void) { // color 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); // particle manipulation @@ -10072,8 +10103,10 @@ uint16_t mode_particle1Dsonicstream(void) { } 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 + } } if (loudness > threshold) { @@ -10117,6 +10150,266 @@ uint16_t mode_particle1Dsonicstream(void) { 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"; + + +/* + 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(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(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 ////////////////////////////////////////////////////////////////////////////////////////// @@ -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_PS1DGEQ, &mode_particle1DGEQ, _data_FX_MODE_PS_1D_GEQ); addEffect(FX_MODE_PSFIRE1D, &mode_particleFire1D, _data_FX_MODE_PS_FIRE1D); -addEffect(FX_MODE_PS1DSONICSTREAM, &mode_particle1Dsonicstream, _data_FX_MODE_PS_SONICSTREAM); +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 } diff --git a/wled00/FX.h b/wled00/FX.h index 1f8a315a6..6481ff757 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -351,7 +351,9 @@ extern byte realtimeMode; // used in getMappedPixelIndex() #define FX_MODE_PS1DGEQ 212 #define FX_MODE_PSFIRE1D 213 #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 diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index fde07be76..cff534256 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -18,8 +18,8 @@ // local shared functions (used both in 1D and 2D system) static int32_t calcForce_dv(const int8_t force, uint8_t &counter); static bool checkBoundsAndWrap(int32_t &position, const int32_t max, const int32_t particleradius, const bool wrap); // returns false if out of bounds by more than particleradius -static void fast_color_add(CRGB &c1, const CRGB &c2, uint32_t scale = 255); // fast and accurate color adding with scaling (scales c2 before adding) -static void fast_color_scale(CRGB &c, const uint32_t scale); // fast scaling function using 32bit variable and pointer. note: keep 'scale' within 0-255 +static 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 uint8_t scale); // fast scaling function using 32bit variable and pointer. note: keep 'scale' within 0-255 //static CRGB *allocateCRGBbuffer(uint32_t length); // global variables for memory management @@ -73,7 +73,7 @@ void ParticleSystem2D::update(void) { //update size settings before handling collisions if (advPartSize) { for (uint32_t i = 0; i < usedParticles; i++) { - if(updateSize(&advPartProps[i], &advPartSize[i]) == false) { // if particle shrinks to 0 size + if (updateSize(&advPartProps[i], &advPartSize[i]) == false) { // if particle shrinks to 0 size particles[i].ttl = 0; // kill particle } } @@ -170,7 +170,7 @@ void ParticleSystem2D::setSmearBlur(uint8_t bluramount) { void ParticleSystem2D::setParticleSize(uint8_t size) { particlesize = size; 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 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) void ParticleSystem2D::flameEmit(const PSsource &emitter) { int emitIndex = sprayEmit(emitter); - if(emitIndex > 0) particles[emitIndex].ttl += emitter.source.ttl; + if (emitIndex > 0) particles[emitIndex].ttl += emitter.source.ttl; } // Emits a particle at given angle and speed, angle is from 0-65535 (=0-360deg), speed is also affected by emitter->var @@ -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; if (options->killoutofbounds) { 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 ((newX < (int32_t)particleHardRadius) || (newX > (int32_t)(maxX - particleHardRadius))) // reached a wall bounce(part.vx, part.vy, newX, maxX); } - else if(!checkBoundsAndWrap(newX, maxX, renderradius, options->wrapX)) { // check out of bounds + else if (!checkBoundsAndWrap(newX, maxX, renderradius, options->wrapX)) { // check out of bounds partFlags.outofbounds = true; if (options->killoutofbounds) part.ttl = 0; @@ -387,14 +387,14 @@ void ParticleSystem2D::getParticleXYsize(PSadvancedParticle *advprops, PSsizeCon return; int32_t size = advprops->size; int32_t asymdir = advsize->asymdir; - int32_t deviation = ((uint32_t)size * (uint32_t)advsize->asymmetry) / 255; // deviation from symmetrical size + 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) if (asymdir < 64) { - deviation = (asymdir * deviation) / 64; + deviation = (asymdir * deviation) >> 6; } else if (asymdir < 192) { - deviation = ((128 - asymdir) * deviation) / 64; + deviation = ((128 - asymdir) * deviation) >> 6; } 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) 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) void ParticleSystem2D::bounce(int8_t &incomingspeed, int8_t ¶llelspeed, int32_t &position, const uint32_t maxposition) { incomingspeed = -incomingspeed; - incomingspeed = (incomingspeed * wallHardness) / 255; // reduce speed as energy is lost on non-hard surface + incomingspeed = (incomingspeed * wallHardness + 128) >> 8; // reduce speed as energy is lost on non-hard surface if (position < (int32_t)particleHardRadius) position = particleHardRadius; // fast particles will never reach the edge if position is inverted, this looks better else @@ -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 void ParticleSystem2D::applyGravity() { int32_t dv = calcForce_dv(gforce, gforcecounter); - if(dv == 0) return; + if (dv == 0) return; for (uint32_t i = 0; i < usedParticles; i++) { // Note: not checking if particle is dead is faster as most are usually alive and if few are alive, rendering is fast anyways particles[i].vy = limitSpeed((int32_t)particles[i].vy - dv); @@ -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 // firemode is only used for PS Fire FX 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; lastRender = strip.now; CRGB baseRGB; @@ -586,24 +586,24 @@ void ParticleSystem2D::ParticleSys_render() { // update global blur (used for blur transitions) int32_t motionbluramount = motionBlur; 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 - smearamount = globalSmear + (((smearamount - globalSmear) * (int)SEGMENT.progress()) >> 16); + smearamount = globalSmear + (((smearamount - globalSmear) * (int)SEGMENT.progress()) >> 16); } globalBlur = motionbluramount; globalSmear = smearamount; - if(isOverlay) { + if (isOverlay) { globalSmear = 0; // do not apply smear or blur in overlay or it turns everything into a blurry mess globalBlur = 0; } // handle blurring and framebuffer update if (framebuffer) { - if(!pmem->inTransition) + if (!pmem->inTransition) useAdditiveTransfer = false; // additive transfer is only usd in transitions (or in overlay) // handle buffer blurring or clearing bool bufferNeedsUpdate = !pmem->inTransition || pmem->inTransition == effectID || isNonFadeTransition; // not a transition; or new FX or not fading style: update buffer (blur, or clear) - if(bufferNeedsUpdate) { + if (bufferNeedsUpdate) { bool loadfromSegment = !renderSolo || isNonFadeTransition; if (globalBlur > 0 || globalSmear > 0) { // blurring active: if not a transition or is newFX, read data from segment before blurring (old FX can render to it afterwards) for (int32_t y = 0; y <= maxYpixel; y++) { @@ -622,8 +622,8 @@ void ParticleSystem2D::ParticleSys_render() { } } // 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(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 (particlesize > 1 && pmem->inTransition) { // if particle size is used by FX we need a clean buffer + if (bufferNeedsUpdate && !globalBlur) { // transfer without adding if buffer was not cleared above (happens if this is the new FX and other FX does not use blurring) useAdditiveTransfer = false; // no blurring and big size particle FX is the new FX (rendered first after clearing), can just render normally } 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 - if(globalSmear > 0) { + if (globalSmear > 0) { if (framebuffer) blur2D(framebuffer, maxXpixel + 1, maxYpixel + 1, globalSmear, globalSmear); 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 -void ParticleSystem2D::renderParticle(const uint32_t particleindex, const uint32_t brightness, const CRGB& color, const bool wrapX, const bool wrapY) { - if(particlesize == 0) { // single pixel rendering +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 uint32_t x = particles[particleindex].x >> PS_P_RADIUS_SHIFT; uint32_t y = particles[particleindex].y >> PS_P_RADIUS_SHIFT; if (x <= (uint32_t)maxXpixel && y <= (uint32_t)maxYpixel) { @@ -706,7 +706,7 @@ void ParticleSystem2D::renderParticle(const uint32_t particleindex, const uint32 } 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 bool pixelvalid[4] = {true, true, true, true}; // is set to false if pixel is out of bounds 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 uint32_t bitshift = 0; - for(uint32_t i = 0; i < maxsize; i++) { + for (uint32_t i = 0; i < maxsize; i++) { if (i == 2) //for the last two passes, use higher amount of blur (results in a nicer brightness gradient with soft edges) bitshift = 1; rendersize += 2; @@ -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 = 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) - 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 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) void ParticleSystem2D::collideParticles(PSparticle &particle1, PSparticle &particle2, int32_t dx, int32_t dy, const int32_t collDistSq) { int32_t distanceSquared = dx * dx + dy * dy; - // Calculate relative velocity (if it is zero, could exit but extra check does not overall speed but deminish it) + // 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 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 // when hard pushing by offsetting position, they sink into each other under gravity // a problem with giving velocity is, that on harder collisions, this adds up as it is not dampened enough, so add friction in the FX if required - if(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 pushamount = 1 + ((250 + dotProduct) >> 6); // the closer dotproduct is to zero, the closer the particles are 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 } - for(uint32_t y = ystart; y < ystart + ysize; y++) { + for (uint32_t y = ystart; y < ystart + ysize; y++) { carryover = BLACK; uint32_t indexXY = xstart + y * width; - for(uint32_t x = xstart; x < xstart + xsize; x++) { + for (uint32_t x = xstart; x < xstart + xsize; x++) { seeppart = colorbuffer[indexXY]; // create copy of current color fast_color_scale(seeppart, seep); // scale it and seep to neighbours if (x > 0) { fast_color_add(colorbuffer[indexXY - 1], seeppart); - if(carryover) // note: check adds overhead but is faster on average + if (carryover) // note: check adds overhead but is faster on average fast_color_add(colorbuffer[indexXY], carryover); } carryover = seeppart; @@ -1122,15 +1122,15 @@ void blur2D(CRGB *colorbuffer, uint32_t xsize, uint32_t ysize, uint32_t xblur, u } seep = yblur >> 1; - for(uint32_t x = xstart; x < xstart + xsize; x++) { + for (uint32_t x = xstart; x < xstart + xsize; x++) { carryover = BLACK; uint32_t indexXY = x + ystart * width; - for(uint32_t y = ystart; y < ystart + ysize; y++) { + for (uint32_t y = ystart; y < ystart + ysize; y++) { seeppart = colorbuffer[indexXY]; // create copy of current color fast_color_scale(seeppart, seep); // scale it and seep to neighbours if (y > 0) { fast_color_add(colorbuffer[indexXY - width], seeppart); - if(carryover) // note: check adds overhead but is faster on average + if (carryover) // note: check adds overhead but is faster on average fast_color_add(colorbuffer[indexXY], carryover); } carryover = seeppart; @@ -1181,7 +1181,7 @@ bool allocateParticleSystemMemory2D(uint32_t numparticles, uint32_t numsources, PSPRINTLN("PS 2D alloc"); uint32_t requiredmemory = sizeof(ParticleSystem2D); uint32_t dummy; // dummy variable - if((particleMemoryManager(numparticles, sizeof(PSparticle), dummy, dummy, SEGMENT.mode)) == nullptr) // allocate memory for particles + if ((particleMemoryManager(numparticles, sizeof(PSparticle), dummy, dummy, SEGMENT.mode)) == nullptr) // allocate memory for particles return false; // not enough memory, function ensures a minimum of numparticles are available // functions above make sure these are a multiple of 4 bytes (to avoid alignment issues) @@ -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) bool initParticleSystem2D(ParticleSystem2D *&PartSys, uint32_t requestedsources, uint32_t additionalbytes, bool advanced, bool sizecontrol) { PSPRINT("PS 2D init "); - if(!strip.isMatrix) return false; // only for 2D + if (!strip.isMatrix) return false; // only for 2D uint32_t cols = SEGMENT.virtualWidth(); uint32_t rows = SEGMENT.virtualHeight(); uint32_t pixels = cols * rows; - if(advanced) + if (advanced) updateRenderingBuffer(100, false, true); // allocate a 10x10 buffer for rendering advanced particles uint32_t numparticles = calculateNumberOfParticles2D(pixels, advanced, sizecontrol); PSPRINT(" segmentsize:" + String(cols) + " " + String(rows)); @@ -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 updateRenderingBuffer(SEGMENT.vWidth() * SEGMENT.vHeight(), true, true); // update or create rendering buffer note: for fragmentation it might be better to allocate this first, but if memory is scarce, system has a buffer but no particles and will return false - + PSPRINTLN("******init done, pointers:"); #ifdef WLED_DEBUG_PS PSPRINT("framebfr size:"); @@ -1249,7 +1249,7 @@ ParticleSystem1D::ParticleSystem1D(uint32_t length, uint32_t numberofparticles, fractionOfParticlesUsed = 255; // use all particles by default advPartProps = nullptr; //make sure we start out with null pointers (just in case memory was not cleared) //advPartSize = nullptr; - updatePSpointers(isadvanced); // set the particle and sources pointer (call this before accessing sprays or particles) + updatePSpointers(isadvanced); // set the particle and sources pointer (call this before accessing sprays or particles) setSize(length); setWallHardness(255); // set default wall hardness to max setGravity(0); //gravity disabled by default @@ -1264,7 +1264,7 @@ ParticleSystem1D::ParticleSystem1D(uint32_t length, uint32_t numberofparticles, sources[i].source.ttl = 1; //set source alive } - if(isadvanced) { + if (isadvanced) { for (uint32_t i = 0; i < numParticles; i++) { advPartProps[i].sat = 255; // set full saturation (for particles that are transferred from non-advanced system) } @@ -1522,7 +1522,6 @@ void ParticleSystem1D::applyFriction(int32_t coefficient) { particles[i].vx = ((int32_t)particles[i].vx * friction) / 255; } #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 // 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() { - 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; lastRender = strip.now; CRGB baseRGB; @@ -1542,7 +1541,7 @@ void ParticleSystem1D::ParticleSys_render() { // update global blur (used for blur transitions) int32_t motionbluramount = motionBlur; int32_t smearamount = smearBlur; - if(pmem->inTransition == effectID) { // FX transition and this is the new FX: fade blur amount + if (pmem->inTransition == effectID) { // FX transition and this is the new FX: fade blur amount motionbluramount = globalBlur + (((motionbluramount - globalBlur) * (int)SEGMENT.progress()) >> 16); // fade from old blur to new blur during transitions smearamount = globalSmear + (((smearamount - globalSmear) * (int)SEGMENT.progress()) >> 16); } @@ -1552,7 +1551,7 @@ void ParticleSystem1D::ParticleSys_render() { if (framebuffer) { // handle buffer blurring or clearing 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; 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++) { @@ -1595,7 +1594,7 @@ void ParticleSystem1D::ParticleSys_render() { renderParticle(i, brightness, baseRGB, particlesettings.wrap); } // apply smear-blur to rendered frame - if(globalSmear > 0) { + if (globalSmear > 0) { if (framebuffer) blur1D(framebuffer, maxXpixel + 1, globalSmear, 0); else @@ -1605,7 +1604,7 @@ void ParticleSystem1D::ParticleSys_render() { // add background color uint32_t bg_color = SEGCOLOR(1); 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) fast_color_add(framebuffer[i], bg_color); 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 -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; - if (advPartProps) {// use advanced size properties + if (advPartProps) { // use advanced size properties size = advPartProps[particleindex].size; } if (size == 0) { //single pixel particle, can be out of bounds as oob checking is made for 2-pixel particles (and updating it uses more code) @@ -1629,7 +1628,7 @@ void ParticleSystem1D::renderParticle(const uint32_t particleindex, const uint32 if (framebuffer) fast_color_add(framebuffer[x], color, brightness); else - SEGMENT.addPixelColor(x, color.scale8((uint8_t)brightness), true); + SEGMENT.addPixelColor(x, color.scale8(brightness), true); } return; } @@ -1715,7 +1714,7 @@ void ParticleSystem1D::renderParticle(const uint32_t particleindex, const uint32 else pxlisinframe[1] = false; } - for(uint32_t i = 0; i < 2; i++) { + for (uint32_t i = 0; i < 2; i++) { if (pxlisinframe[i]) { if (framebuffer) 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 if (advPartProps) //may be using individual particle size 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 uint16_t binIndices[maxBinParticles]; // array to store indices of particles in a 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 uint32_t idx_j = binIndices[j]; 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 dv = (int32_t)particles[idx_j].vx - (int32_t)particles[idx_i].vx; - int32_t proximity = collisiondistance; - if (dv >= proximity) // particles would go past each other in next move update - proximity += abs(dv); // add speed difference to catch fast particles - if (dx <= proximity && dx >= -proximity) { // collide if close - collideParticles(particles[idx_i], particleFlags[idx_i], particles[idx_j], particleFlags[idx_j], dx, dv, collisiondistance); + int32_t dx = (particles[idx_j].x + particles[idx_j].vx) - (particles[idx_i].x + particles[idx_i].vx); // distance between particles with lookahead + uint32_t dx_abs = abs(dx); + if (dx_abs <= collisiondistance) { // collide if close + collideParticles(particles[idx_i], particleFlags[idx_i], particles[idx_j], particleFlags[idx_j], dx, dx_abs, 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 // 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) { - int32_t dotProduct = (dx * relativeVx); // is always negative if moving towards each other - uint32_t distance = abs(dx); +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 dv = particle2.vx - particle1.vx; + int32_t dotProduct = (dx * dv); // is always negative if moving towards each other + if (dotProduct < 0) { // particles are moving towards each other uint32_t surfacehardness = max(collisionHardness, (int32_t)PS_P_MINSURFACEHARDNESS_1D); // if particles are soft, the impulse must stay above a limit or collisions slip through - // Calculate new velocities after collision - int32_t impulse = relativeVx * surfacehardness / 255; // note: not using dot product like in 2D as impulse is purely speed depnedent + // Calculate new velocities after collision 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; 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 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; 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 // 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 @@ -1818,10 +1823,10 @@ void ParticleSystem1D::collideParticles(PSparticle1D &particle1, const PSparticl particle1.vx -= pushamount; particle2.vx += pushamount; - if(distance < collisiondistance >> 1) { // too close, force push particles so they dont collapse - pushamount = 1 + ((collisiondistance - distance) >> 3); // note: push amount found by experimentation + if (dx_abs < collisiondistance >> 1) { // too close, force push particles so they dont collapse + 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 particle1.vx++;// += pushamount; particle1.x += pushamount; @@ -1836,7 +1841,7 @@ void ParticleSystem1D::collideParticles(PSparticle1D &particle1, const PSparticl particle2.vx--;// -= 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.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) { uint32_t requiredmemory = sizeof(ParticleSystem1D); 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 // functions above make sure these are a multiple of 4 bytes (to avoid alignment issues) 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) 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(advanced) + if (advanced) updateRenderingBuffer(10, false, true); // buffer for advanced particles, fixed size uint32_t numparticles = calculateNumberOfParticles1D(fractionofparticles, advanced); 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; uint32_t seep = blur >> 1; 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 fast_color_scale(seeppart, seep); // scale it and seep to neighbours if (x > 0) { fast_color_add(colorbuffer[x-1], seeppart); - if(carryover) // note: check adds overhead but is faster on average + if (carryover) // note: check adds overhead but is faster on average fast_color_add(colorbuffer[x], carryover); // is black on first pass } 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 // note2: function is mainly used to add scaled colors, so checking if one color is black is slower // note3: scale is 255 when using blur, checking for that makes blur faster -static void fast_color_add(CRGB &c1, const CRGB &c2, const uint32_t scale) { + __attribute__((optimize("O2"))) static void fast_color_add(CRGB &c1, const CRGB &c2, const uint8_t scale) { uint32_t r, g, b; if (scale < 255) { 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; } - uint32_t max = std::max(r,g); // check for overflow, using max() is faster as the compiler can optimize - max = std::max(max,b); - if (max < 256) { + // note: this chained comparison is the fastest method for max of 3 values (faster than std:max() or using xor) + uint32_t max = (r > g) ? ((r > b) ? r : b) : ((g > b) ? g : b); + if (max <= 255) { c1.r = r; // save result to c1 c1.g = g; 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 -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.g = ((c.g * 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! void deallocatePSmemory(void* dataptr, uint32_t 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 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->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 else 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 - 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) { uint32_t maxParticles = pmem->buffersize / structSize; // maximum number of particles that fit in the buffer 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 PSPRINTLN("new effect"); newAvailable = (maxParticles * progress) >> 16; // update total particles available to this PS (newAvailable is guaranteed to be smaller than maxParticles) - if(newAvailable < 2) newAvailable = 2; // give 2 particle minimum (some FX may crash with less as they do i+1 access) - if(newAvailable > numParticlesUsed) newAvailable = numParticlesUsed; // limit to number of particles used, do not move the pointer anymore (will be set to base in final handover) + 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) 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 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); } 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 progress = 0xFFFFU - progress; // inverted transition progress newAvailable = ((maxParticles * progress) >> 16); // result is guaranteed to be smaller than maxParticles - if(newAvailable > 0) newAvailable--; // -1 to avoid overlapping memory in 1D<->2D transitions - if(newAvailable < 2) newAvailable = 2; // give 2 particle minimum (some FX may crash with less as they do i+1 access) + if (newAvailable > 0) newAvailable--; // -1 to avoid overlapping memory in 1D<->2D transitions + if (newAvailable < 2) newAvailable = 2; // give 2 particle minimum (some FX may crash with less as they do i+1 access) // note: buffer pointer stays the same, number of available particles is reduced } availableToPS = newAvailable; - } else 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 PSPRINTLN("PS transition ended, final particle handover"); uint32_t maxParticles = pmem->buffersize / structSize; // maximum number of particles that fit in the buffer if (maxParticles > availableToPS) { // not all particles transferred yet uint32_t totransfer = maxParticles - availableToPS; // transfer all remaining particles - if(totransfer <= maxParticles) // safety check + if (totransfer <= maxParticles) // safety check particleHandover(buffer, structSize, totransfer); - if(maxParticles > numParticlesUsed) { // FX uses less than max: move the already existing particles to the beginning of the buffer + if (maxParticles > numParticlesUsed) { // FX uses less than max: move the already existing particles to the beginning of the buffer uint32_t usedbytes = availableToPS * structSize; int32_t bufferoffset = (maxParticles - 1) - availableToPS; // offset to existing particles (see above) - if(bufferoffset < (int)maxParticles) { // safety check + if (bufferoffset < (int)maxParticles) { // safety check void* currentBuffer = (void*)((uint8_t*)buffer + bufferoffset * structSize); // pointer to current buffer start memmove(buffer, currentBuffer, usedbytes); // move the existing particles to the beginning of the buffer } @@ -2243,7 +2248,7 @@ void particleHandover(void *buffer, size_t structSize, int32_t numToTransfer) { PSparticle *particles = (PSparticle *)buffer; for (int32_t i = 0; i < numToTransfer; i++) { 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 } else @@ -2258,7 +2263,7 @@ void particleHandover(void *buffer, size_t structSize, int32_t numToTransfer) { PSparticle1D *particles = (PSparticle1D *)buffer; for (int32_t i = 0; i < numToTransfer; i++) { 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 } else @@ -2285,7 +2290,7 @@ bool segmentIsOverlay(void) { // TODO: this only needs to be checked when segmen // Check for overlap with all previous segments 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 endX = strip._segments[i].stop; unsigned startY = strip._segments[i].startY; @@ -2316,15 +2321,15 @@ void updateRenderingBuffer(uint32_t requiredpixels, bool isFramebuffer, bool ini PSPRINTLN("updateRenderingBuffer"); 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 - 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 || initialize) { // update only if initilizing or if buffer exists (prevents repeatet allocation attempts if initial alloc failed) + if (*targetBuffer) // buffer exists, free it deallocatePSmemory((void*)(*targetBuffer), targetBufferSize * sizeof(CRGB)); *targetBuffer = reinterpret_cast(allocatePSmemory(requiredpixels * sizeof(CRGB), false)); - if(*targetBuffer) + if (*targetBuffer) targetBufferSize = requiredpixels; else 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 void servicePSmem() { // Increment watchdog for each entry and deallocate if idle too long (i.e. no PS running on that segment) - if(partMemList.size() > 0) { + if (partMemList.size() > 0) { for (size_t i = 0; i < partMemList.size(); i++) { - if(strip.getSegmentsNum() > i) { // segment still exists - if(strip._segments[i].freeze) continue; // skip frozen segments (incrementing watchdog will delete memory, leading to crash) + if (strip.getSegmentsNum() > i) { // segment still exists + if (strip._segments[i].freeze) continue; // skip frozen segments (incrementing watchdog will delete memory, leading to crash) } partMemList[i].watchdog++; // Increment watchdog counter PSPRINT("pmem servic. list size: "); @@ -2356,12 +2361,12 @@ void servicePSmem() { } } else { // no particle system running, release buffer memory - if(framebuffer) { + if (framebuffer) { deallocatePSmemory((void*)framebuffer, frameBufferSize * sizeof(CRGB)); // free the buffers framebuffer = nullptr; frameBufferSize = 0; } - if(renderbuffer) { + if (renderbuffer) { deallocatePSmemory((void*)renderbuffer, renderBufferSize * sizeof(CRGB)); renderbuffer = nullptr; 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) 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 "); #ifndef WLED_DISABLE_MODE_BLEND 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) } #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++) { int index = y * width; // current row index for 1D buffer for (uint32_t x = 0; x < width; x++) { CRGB *c = &framebuffer[index++]; uint32_t clr = RGBW32(c->r,c->g,c->b,0); // convert to 32bit color - if(useAdditiveTransfer) { + if (useAdditiveTransfer) { uint32_t segmentcolor = SEGMENT.getPixelColorXY((int)x, (int)y); CRGB segmentRGB = CRGB(segmentcolor); - if(clr == 0) // frame buffer is black, just update the framebuffer + if (clr == 0) // frame buffer is black, just update the framebuffer *c = segmentRGB; 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 clr = RGBW32(c->r,c->g,c->b,0); // convert to 32bit color (again) and set the segment } SEGMENT.setPixelColorXY((int)x, (int)y, clr); // save back to segment after adding local buffer } } - //if(clr > 0) // not black TODO: not transferring black is faster and enables overlay, but requires proper handling of buffer clearing, which is quite complex and probably needs a change to SEGMENT handling. + //if (clr > 0) // not black TODO: not transferring black is faster and enables overlay, but requires proper handling of buffer clearing, which is quite complex and probably needs a change to SEGMENT handling. else SEGMENT.setPixelColorXY((int)x, (int)y, clr); } @@ -2407,20 +2412,20 @@ void transferBuffer(uint32_t width, uint32_t height, bool useAdditiveTransfer) { for (uint32_t x = 0; x < width; x++) { CRGB *c = &framebuffer[x]; uint32_t clr = RGBW32(c->r,c->g,c->b,0); - if(useAdditiveTransfer) { + if (useAdditiveTransfer) { uint32_t segmentcolor = SEGMENT.getPixelColor((int)x);; CRGB segmentRGB = CRGB(segmentcolor); - if(clr == 0) // frame buffer is black, just load the color (for next frame) + if (clr == 0) // frame buffer is black, just load the color (for next frame) *c = segmentRGB; else { // color to add to segment is not black - if(segmentcolor) { + if (segmentcolor) { fast_color_add(*c, segmentRGB); // add segment color back to buffer if not black clr = RGBW32(c->r,c->g,c->b,0); // convert to 32bit color (again) } SEGMENT.setPixelColor((int)x, clr); // save back to segment after adding local buffer } } - //if(color > 0) // not black + //if (color > 0) // not black else SEGMENT.setPixelColor((int)x, clr); } diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index a91ebe25e..099b96a85 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -211,7 +211,7 @@ public: private: //rendering functions void ParticleSys_render(); - [[gnu::hot]] void renderParticle(const uint32_t particleindex, const uint32_t brightness, const CRGB& color, const bool wrapX, const bool wrapY); + [[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 void applyGravity(); // applies gravity to all particles void handleCollisions(); @@ -343,7 +343,7 @@ public: int32_t sprayEmit(const PSsource1D &emitter); void particleMoveUpdate(PSparticle1D &part, PSparticleFlags1D &partFlags, PSsettings1D *options = NULL, PSadvancedParticle1D *advancedproperties = NULL); // move function //particle physics - [[gnu::hot]] void applyForce(PSparticle1D &part, const int8_t xforce, uint8_t &counter); //apply a force to a single particle + [[gnu::hot]] void applyForce(PSparticle1D &part, const int8_t xforce, uint8_t &counter); //apply a force to a single particle void applyForce(const int8_t xforce); // apply a force to all particles void applyGravity(PSparticle1D &part, PSparticleFlags1D &partFlags); // applies gravity to single particle (use this for sources) void applyFriction(const int32_t coefficient); // apply friction to all used particles @@ -378,12 +378,12 @@ public: private: //rendering functions 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 void applyGravity(); // applies gravity to all particles void handleCollisions(); - [[gnu::hot]] void collideParticles(PSparticle1D &particle1, const PSparticleFlags1D &particle1flags, PSparticle1D &particle2, const PSparticleFlags1D &particle2flags, int32_t dx, int32_t relativeVx, 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 void updatePSpointers(const bool isadvanced); // update the data pointers to current segment data space