WLED/wled00/FXparticleSystem.cpp

1269 wiersze
52 KiB
C++

/*
FXparticleSystem.cpp
Particle system with functions for particle generation, particle movement and particle rendering to RGB matrix.
by DedeHai (Damian Schneider) 2013-2024
LICENSE
The MIT License (MIT)
Copyright (c) 2024 Damian Schneider
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
/*
Note on ESP32: using 32bit integer is faster than 16bit or 8bit, each operation takes on less instruction, can be testen on https://godbolt.org/
it does not matter if using int, unsigned int, uint32_t or int32_t, the compiler will make int into 32bit
this should be used to optimize speed but not if memory is affected much
*/
/*
TODO:
-add function to 'update sources' so FX does not have to take care of that. FX can still implement its own version if so desired. config should be optional, if not set, use default config.
-add possiblity to emit more than one particle, just pass a source and the amount to emit or even add several sources and the amount, function decides if it should do it fair or not
-add an x/y struct, do particle rendering using that, much easier to read
-extend rendering to more than 2x2, 3x2 (fire) should be easy, 3x3 maybe also doable without using much math (need to see if it looks good)
*/
// sources need to be updatable by the FX, so functions are needed to apply it to a single particle that are public
#include "FXparticleSystem.h"
#include "wled.h"
#include "FastLED.h"
#include "FX.h"
ParticleSystem::ParticleSystem(uint16_t width, uint16_t height, uint16_t numberofparticles, uint16_t numberofsources)
{
Serial.println("PS Constructor");
numSources = numberofsources;
numParticles = numberofparticles; // set number of particles in the array
usedParticles = numberofparticles; // use all particles by default
//particlesettings = {false, false, false, false, false, false, false, false}; // all settings off by default
updatePSpointers(); // set the particle and sources pointer (call this before accessing sprays or particles)
setMatrixSize(width, height);
setWallHardness(255); // set default wall hardness to max
emitIndex = 0;
/*
Serial.println("alive particles: ");
for (int i = 0; i < numParticles; i++)
{
//particles[i].ttl = 0; //initialize all particles to dead
//if (particles[i].ttl)
{
Serial.print("x:");
Serial.print(particles[i].x);
Serial.print(" y:");
Serial.println(particles[i].y);
}
}*/
Serial.println("PS Constructor done");
}
//update function applies gravity, moves the particles, handles collisions and renders the particles
void ParticleSystem::update(void)
{
//apply gravity globally if enabled
if (particlesettings.useGravity)
applyGravity(particles, usedParticles, gforce, &gforcecounter);
// handle collisions (can push particles, must be done before updating particles or they can render out of bounds, causing a crash if using local buffer for speed)
if (particlesettings.useCollisions)
handleCollisions();
//move all particles
for (int i = 0; i < usedParticles; i++)
{
particleMoveUpdate(particles[i], particlesettings);
}
ParticleSys_render();
}
//update function for fire animation
void ParticleSystem::updateFire(uint32_t intensity)
{
fireParticleupdate();
ParticleSys_render(true, intensity);
}
void ParticleSystem::setUsedParticles(uint16_t num)
{
usedParticles = min(num, numParticles); //limit to max particles
}
void ParticleSystem::setWallHardness(uint8_t hardness)
{
wallHardness = hardness;
}
void ParticleSystem::setCollisionHardness(uint8_t hardness)
{
collisionHardness = hardness;
}
void ParticleSystem::setMatrixSize(uint16_t x, uint16_t y)
{
maxXpixel = x - 1; // last physical pixel that can be drawn to
maxYpixel = y - 1;
maxX = x * PS_P_RADIUS - 1; // particle system boundary for movements
maxY = y * PS_P_RADIUS - 1; // this value is often needed by FX to calculate positions
}
void ParticleSystem::setWrapX(bool enable)
{
particlesettings.wrapX = enable;
}
void ParticleSystem::setWrapY(bool enable)
{
particlesettings.wrapY = enable;
}
void ParticleSystem::setBounceX(bool enable)
{
particlesettings.bounceX = enable;
}
void ParticleSystem::setBounceY(bool enable)
{
particlesettings.bounceY = enable;
}
void ParticleSystem::setKillOutOfBounds(bool enable)
{
particlesettings.killoutofbounds = enable;
}
void ParticleSystem::setColorByAge(bool enable)
{
particlesettings.colorByAge = enable;
}
// enable/disable gravity, optionally, set the force (force=8 is default) can be 1-255, 0 is also disable
// if enabled, gravity is applied to all particles in ParticleSystemUpdate()
void ParticleSystem::enableGravity(bool enable, uint8_t force)
{
particlesettings.useGravity = enable;
if (force > 0)
gforce = force;
else
particlesettings.useGravity = false;
}
void ParticleSystem::enableParticleCollisions(bool enable, uint8_t hardness) // enable/disable gravity, optionally, set the force (force=8 is default) can be 1-255, 0 is also disable
{
particlesettings.useCollisions = enable;
collisionHardness = hardness + 1;
}
// emit one particle with variation
void ParticleSystem::sprayEmit(PSsource &emitter)
{
for (uint32_t i = 0; i < usedParticles; i++)
{
emitIndex++;
if (emitIndex >= usedParticles)
emitIndex = 0;
if (particles[emitIndex].ttl == 0) // find a dead particle
{
particles[emitIndex].x = emitter.source.x; // + random16(emitter.var) - (emitter.var >> 1); //randomness uses cpu cycles and is almost invisible, removed for now.
particles[emitIndex].y = emitter.source.y; // + random16(emitter.var) - (emitter.var >> 1);
particles[emitIndex].vx = emitter.vx + random(emitter.var) - (emitter.var>>1);
particles[emitIndex].vy = emitter.vy + random(emitter.var) - (emitter.var>>1);
particles[emitIndex].ttl = random16(emitter.maxLife - emitter.minLife) + emitter.minLife;
particles[emitIndex].hue = emitter.source.hue;
particles[emitIndex].sat = emitter.source.sat;
particles[emitIndex].collide = emitter.source.collide;
break;
}
/*
if (emitIndex < 2)
{
Serial.print(" ");
Serial.print(particles[emitIndex].ttl);
Serial.print(" ");
Serial.print(particles[emitIndex].x);
Serial.print(" ");
Serial.print(particles[emitIndex].y);
}*/
}
//Serial.println("**");
}
// Spray emitter for particles used for flames (particle TTL depends on source TTL)
void ParticleSystem::flameEmit(PSsource &emitter)
{
for (uint32_t i = 0; i < usedParticles; i++)
{
emitIndex++;
if (emitIndex >= usedParticles)
emitIndex = 0;
if (particles[emitIndex].ttl == 0) // find a dead particle
{
particles[emitIndex].x = emitter.source.x + random16(PS_P_RADIUS<<1) - PS_P_RADIUS; // jitter the flame by one pixel to make the flames wider at the base
particles[emitIndex].y = emitter.source.y;
particles[emitIndex].vx = emitter.vx + random16(emitter.var) - (emitter.var >> 1); //random16 is good enough for fire and much faster
particles[emitIndex].vy = emitter.vy + random16(emitter.var) - (emitter.var >> 1);
particles[emitIndex].ttl = random16(emitter.maxLife - emitter.minLife) + emitter.minLife + emitter.source.ttl; // flame intensity dies down with emitter TTL
// fire uses ttl and not hue for heat, so no need to set the hue
break; // done
}
}
}
//todo: idee: man könnte einen emitter machen, wo die anzahl emittierten partikel von seinem alter abhängt. benötigt aber einen counter
//idee2: source einen counter hinzufügen, dann setting für emitstärke, dann müsste man das nicht immer in den FX animationen handeln
// Emits a particle at given angle and speed, angle is from 0-65535 (=0-360deg), speed is also affected by emitter->var
// angle = 0 means in positive x-direction (i.e. to the right)
void ParticleSystem::angleEmit(PSsource &emitter, uint16_t angle, uint32_t speed)
{
emitter.vx = ((int32_t)cos16(angle) * speed) / 32767; // cos16() and sin16() return signed 16bit
emitter.vy = ((int32_t)sin16(angle) * speed) / 32767; // note: cannot use bit shifts as bit shifting is asymmetrical for positive and negative numbers and this needs to be accurate!
sprayEmit(emitter);
}
// particle moves, decays and dies, if killoutofbounds is set, out of bounds particles are set to ttl=0
// uses passed settings to set bounce or wrap, if useGravity is set, it will never bounce at the top and killoutofbounds is not applied over the top
void ParticleSystem::particleMoveUpdate(PSparticle &part, PSsettings &options)
{
if (part.ttl > 0)
{
// age
part.ttl--;
if (particlesettings.colorByAge)
part.hue = part.ttl > 255 ? 255 : part.ttl; //set color to ttl
int32_t newX = part.x + (int16_t)part.vx;
int32_t newY = part.y + (int16_t)part.vy;
part.outofbounds = 0; // reset out of bounds (in case particle was created outside the matrix and is now moving into view)
//if wall collisions are enabled, bounce them before they reach the edge, it looks much nicer if the particle is not half out of vew
if (options.bounceX)
{
if ((newX < PS_P_RADIUS) || (newX > maxX - PS_P_RADIUS)) // reached a wall
{
part.vx = -part.vx; // invert speed
part.vx = (part.vx * wallHardness) / 255; // reduce speed as energy is lost on non-hard surface
if (newX < PS_P_RADIUS)
newX = PS_P_RADIUS; // fast particles will never reach the edge if position is inverted
else
newX = maxX - PS_P_RADIUS;
}
}
if ((newX < 0) || (newX > maxX)) // check if particle reached an edge
{
if (options.wrapX)
{
newX = wraparound(newX, maxX);
}
else if (((newX <= -PS_P_HALFRADIUS) || (newX > maxX + PS_P_HALFRADIUS))) // particle is leaving, set out of bounds if it has fully left
{
part.outofbounds = 1;
if (options.killoutofbounds)
part.ttl = 0;
}
}
if (options.bounceY)
{
if ((newY < PS_P_RADIUS) || (newY > maxY - PS_P_RADIUS)) // reached floor / ceiling
{
if (newY < PS_P_RADIUS) // bounce at bottom
{
part.vy = -part.vy; // invert speed
part.vy = (part.vy * wallHardness) / 255; // reduce speed as energy is lost on non-hard surface
newY = PS_P_RADIUS;
}
else
{
if (options.useGravity) // do not bounce on top if using gravity (open container) if this is needed implement it in the FX
{
if (newY > maxY + PS_P_HALFRADIUS)
part.outofbounds = 1; // set out of bounds, kill out of bounds over the top does not apply if gravity is used (user can implement it in FX if needed)
}
else
{
part.vy = -part.vy; // invert speed
part.vy = (part.vy * wallHardness) / 255; // reduce speed as energy is lost on non-hard surface
newY = maxY - PS_P_RADIUS;
}
}
}
}
if (((newY < 0) || (newY > maxY))) // check if particle reached an edge
{
if (options.wrapY)
{
newY = wraparound(newY, maxY);
}
else if (((newY <= -PS_P_HALFRADIUS) || (newY > maxY + PS_P_HALFRADIUS))) // particle is leaving, set out of bounds if it has fully left
{
part.outofbounds = 1;
if (options.killoutofbounds)
{
if (newY < 0) // if gravity is enabled, only kill particles below ground
part.ttl = 0;
else if (!options.useGravity)
part.ttl = 0;
}
}
}
part.x = (int16_t)newX; // set new position
part.y = (int16_t)newY; // set new position
}
}
// apply a force in x,y direction to particles
// caller needs to provide a 8bit counter that holds its value between calls for each group (numparticles can be 1 for single particle)
// force is in 3.4 fixed point notation so force=16 means apply v+1 each frame default of 8 is every other frame (gives good results)
void ParticleSystem::applyForce(PSparticle *part, uint32_t numparticles, int8_t xforce, int8_t yforce, uint8_t *counter)
{
// for small forces, need to use a delay counter
uint8_t xcounter = (*counter) & 0x0F; // lower four bits
uint8_t ycounter = (*counter) >> 4; // upper four bits
// velocity increase
int32_t dvx = calcForce_dV(xforce, &xcounter);
int32_t dvy = calcForce_dV(yforce, &ycounter);
// save counter values back
*counter |= xcounter & 0x0F; // write lower four bits, make sure not to write more than 4 bits
*counter |= (ycounter << 4) & 0xF0; // write upper four bits
// apply the force to particle:
int32_t i = 0;
if (dvx != 0)
{
if (numparticles == 1) // for single particle, skip the for loop to make it faster
{
part[0].vx = part[0].vx + dvx > PS_P_MAXSPEED ? PS_P_MAXSPEED : part[0].vx + dvx; // limit the force, this is faster than min or if/else
}
else
{
for (i = 0; i < numparticles; i++)
{
// note: not checking if particle is dead is faster as most are usually alive and if few are alive, rendering is faster so no speed penalty
part[i].vx = part[i].vx + dvx > PS_P_MAXSPEED ? PS_P_MAXSPEED : part[i].vx + dvx;
}
}
}
if (dvy != 0)
{
if (numparticles == 1) // for single particle, skip the for loop to make it faster
part[0].vy = part[0].vy + dvy > PS_P_MAXSPEED ? PS_P_MAXSPEED : part[0].vy + dvy;
else
{
for (i = 0; i < numparticles; i++)
{
part[i].vy = part[i].vy + dvy > PS_P_MAXSPEED ? PS_P_MAXSPEED : part[i].vy + dvy;
}
}
}
}
// apply a force in x,y direction to particles directly (no counter required but no 'sub 1' force supported)
void ParticleSystem::applyForce(PSparticle *part, uint32_t numparticles, int8_t xforce, int8_t yforce)
{
//note: could make this faster for single particles by adding an if statement, but it is fast enough as is
for (uint i = 0; i < numparticles; i++)
{
// note: not checking if particle is dead is faster as most are usually alive and if few are alive, rendering is faster so no speed penalty
part[i].vx = part[i].vx + xforce > PS_P_MAXSPEED ? PS_P_MAXSPEED : part[i].vx + xforce;
part[i].vy = part[i].vy + yforce > PS_P_MAXSPEED ? PS_P_MAXSPEED : part[i].vy + yforce;
}
}
// apply a force in angular direction to group of particles //TODO: actually test if this works as expected, this is untested code
// caller needs to provide a 8bit counter that holds its value between calls for each group (numparticles can be 1 for single particle)
// angle is from 0-65535 (=0-360deg) angle = 0 means in positive x-direction (i.e. to the right)
void ParticleSystem::applyAngleForce(PSparticle *part, uint32_t numparticles, uint8_t force, uint16_t angle, uint8_t *counter)
{
int8_t xforce = ((int32_t)force * cos16(angle)) / 32767; // force is +/- 127
int8_t yforce = ((int32_t)force * sin16(angle)) / 32767; // note: cannot use bit shifts as bit shifting is asymmetrical for positive and negative numbers and this needs to be accurate!
// note: sin16 is 10% faster than sin8() on ESP32 but on ESP8266 it is 9% slower
// force is in 3.4 fixed point notation so force=16 means apply v+1 each frame (useful force range is +/- 127)
applyForce(part, numparticles, xforce, yforce, counter);
}
// apply a force in angular direction to particles directly (no counter required but no 'sub 1' force supported)
// angle is from 0-65535 (=0-360deg) angle = 0 means in positive x-direction (i.e. to the right)
void ParticleSystem::applyAngleForce(PSparticle *part, uint32_t numparticles, uint8_t force, uint16_t angle)
{
int8_t xforce = ((int32_t)force * cos16(angle)) / 32767; // force is +/- 127
int8_t yforce = ((int32_t)force * sin16(angle)) / 32767; // note: cannot use bit shifts as bit shifting is asymmetrical for positive and negative numbers and this needs to be accurate!
applyForce(part, numparticles, xforce, yforce);
}
// apply gravity to a group of particles
// faster than apply force since direction is always down and counter is fixed for all particles
// caller needs to provide a 8bit counter that holds its value between calls
// force is in 4.4 fixed point notation so force=16 means apply v+1 each frame default of 8 is every other frame (gives good results), force above 127 are VERY strong
void ParticleSystem::applyGravity(PSparticle *part, uint32_t numarticles, uint8_t force, uint8_t *counter)
{
int32_t dv; // velocity increase
if (force > 15)
dv = (force >> 4); // apply the 4 MSBs
else
dv = 1;
*counter += force;
if (*counter > 15)
{
*counter -= 16;
// apply force to all used particles
for (uint32_t i = 0; i < numarticles; i++)
{
// note: not checking if particle is dead is faster as most are usually alive and if few are alive, rendering is fast anyways
particles[i].vy = particles[i].vy - dv > PS_P_MAXSPEED ? PS_P_MAXSPEED : particles[i].vy - dv; // limit the force, this is faster than min or if/else
}
}
}
//apply gravity using PS global gforce
void ParticleSystem::applyGravity(PSparticle *part, uint32_t numarticles, uint8_t *counter)
{
applyGravity(part, numarticles, gforce, counter);
}
//apply gravity to single particle using system settings (use this for sources)
void ParticleSystem::applyGravity(PSparticle *part)
{
int32_t dv; // velocity increase
if (gforce > 15)
dv = (gforce >> 4); // apply the 4 MSBs
else
dv = 1;
if (gforcecounter + gforce > 15) //counter is updated in global update when applying gravity
{
part->vy = part->vy - dv > PS_P_MAXSPEED ? PS_P_MAXSPEED : part->vy - dv; // limit the force, this is faster than min or if/else
}
}
// slow down particles by friction, the higher the speed, the higher the friction. a high friction coefficient slows them more (255 means instant stop)
void ParticleSystem::applyFriction(PSparticle *part, uint8_t coefficient)
{
int32_t friction = 255 - coefficient;
// note: not checking if particle is dead is faster as most are usually alive and if few are alive, rendering is faster
// note2: cannot use right shifts as bit shifting in right direction is asymmetrical for positive and negative numbers and this needs to be accurate or things start to go to the left side.
part->vx = ((int16_t)part->vx * friction) / 255;
part->vy = ((int16_t)part->vy * friction) / 255;
}
// apply friction to all particles
void ParticleSystem::applyFriction(uint8_t coefficient)
{
int32_t friction = 255 - coefficient;
for (uint32_t i = 0; i < usedParticles; i++)
{
// note: not checking if particle is dead is faster as most are usually alive and if few are alive, rendering is faster
// note2: cannot use right shifts as bit shifting in right direction is asymmetrical for positive and negative numbers and this needs to be accurate or things start to go to the left side.
particles[i].vx = ((int16_t)particles[i].vx * friction) / 255;
particles[i].vy = ((int16_t)particles[i].vy * friction) / 255;
}
}
// attracts a particle to an attractor particle using the inverse square-law
void ParticleSystem::attract(PSparticle *part, PSparticle *attractor, uint8_t *counter, uint8_t strength, bool swallow)
{
// Calculate the distance between the particle and the attractor
int32_t dx = attractor->x - part->x;
int32_t dy = attractor->y - part->y;
// Calculate the force based on inverse square law
int32_t distanceSquared = dx * dx + dy * dy + 1;
if (distanceSquared < 8192)
{
if (swallow) // particle is close, age it fast so it fades out, do not attract further
{
if (part->ttl > 7)
part->ttl -= 8;
else
{
part->ttl = 0;
return;
}
}
distanceSquared = 4 * PS_P_RADIUS * PS_P_RADIUS; // limit the distance to avoid very high forces
}
int32_t force = ((int32_t)strength << 16) / distanceSquared;
int8_t xforce = (force * dx) / 1024; // scale to a lower value, found by experimenting
int8_t yforce = (force * dy) / 1024; // note: cannot use bit shifts as bit shifting is asymmetrical for positive and negative numbers and this needs to be accurate!
applyForce(part, 1, xforce, yforce, counter);
}
// render particles to the LED buffer (uses palette to render the 8bit particle color value)
// if wrap is set, particles half out of bounds are rendered to the other side of the matrix
// warning: do not render out of bounds particles or system will crash! rendering does not check if particle is out of bounds
// fireintensity and firemode are optional arguments (fireintensity is only used in firemode)
void ParticleSystem::ParticleSys_render(bool firemode, uint32_t fireintensity)
{
int32_t pixco[4][2]; //physical pixel coordinates of the four pixels a particle is rendered to. x,y pairs
CRGB baseRGB;
bool useLocalBuffer = true;
CRGB **colorbuffer;
uint32_t i;
uint32_t brightness; // particle brightness, fades if dying
//CRGB colorbuffer[maxXpixel+1][maxYpixel+1] = {0}; //put buffer on stack (not a good idea, can cause crashes on large segments if other function run the stack into the heap)
if (useLocalBuffer)
{
// allocate memory for the local renderbuffer
colorbuffer = allocate2Dbuffer(maxXpixel + 1, maxYpixel + 1);
if (colorbuffer == NULL)
useLocalBuffer = false; //render to segment pixels directly if not enough memory
}
// go over particles and render them to the buffer
for (i = 0; i < usedParticles; i++)
{
if (particles[i].outofbounds || particles[i].ttl == 0)
continue;
// generate RGB values for particle
if(firemode)
{
//brightness = (uint32_t)particles[i].ttl * (1 + (fireintensity >> 4)) + (fireintensity >> 2); //this is good
//brightness = (uint32_t)particles[i].ttl * (fireintensity >> 3) + (fireintensity >> 1); // this is experimental, also works, flamecolor is more even, does not look as good (but less puffy at lower speeds)
//brightness = (((uint32_t)particles[i].ttl * (maxY + PS_P_RADIUS - particles[i].y)) >> 7) + (uint32_t)particles[i].ttl * (fireintensity >> 4) + (fireintensity >> 1); // this is experimental //multiplikation mit weniger als >>4 macht noch mehr puffs bei low speed
//brightness = (((uint32_t)particles[i].ttl * (maxY + PS_P_RADIUS - particles[i].y)) >> 7) + particles[i].ttl + (fireintensity>>1); // this is experimental
//brightness = (((uint32_t)particles[i].ttl * (maxY + PS_P_RADIUS - particles[i].y)) >> 7) + ((particles[i].ttl * fireintensity) >> 5); // this is experimental TODO: test this -> testing... ok but not the best, bit sparky
brightness = (((uint32_t)particles[i].ttl * (maxY + PS_P_RADIUS - particles[i].y)) >> 7) + (fireintensity >> 1); // this is experimental TODO: test this -> testing... does not look too bad!
brightness > 255 ? 255 : brightness; // faster then using min()
baseRGB = ColorFromPalette(SEGPALETTE, brightness, 255, LINEARBLEND);
}
else{
brightness = particles[i].ttl > 255 ? 255 : particles[i].ttl; //faster then using min()
baseRGB = ColorFromPalette(SEGPALETTE, particles[i].hue, 255, LINEARBLEND);
if (particles[i].sat < 255)
{
CHSV baseHSV = rgb2hsv_approximate(baseRGB); //convert to hsv
baseHSV.s = particles[i].sat; //desaturate
baseRGB = (CRGB)baseHSV; //convert back to RGB
}
}
int32_t pxlbrightness[4] = {0}; //note: pxlbrightness needs to be set to 0 or checking in rendering function does not work (if values persist), this is faster then setting it to 0 there
// calculate brightness values for all four pixels representing a particle using linear interpolation and calculate the coordinates of the phyiscal pixels to add the color to
renderParticle(&particles[i], brightness, pxlbrightness, pixco);
/*
//debug: check coordinates if out of buffer boundaries print out some info
for(uint32_t d; d<4; d++)
{
if (pixco[d][0] < 0 || pixco[d][0] > maxXpixel)
{
pxlbrightness[d] = -1; //do not render
Serial.print("uncought out of bounds: x=");
Serial.print(pixco[d][0]);
Serial.print("particle x=");
Serial.print(particles[i].x);
Serial.print(" y=");
Serial.println(particles[i].y);
useLocalBuffer = false;
free(colorbuffer); // free buffer memory
}
if (pixco[d][1] < 0 || pixco[d][1] > maxYpixel)
{
pxlbrightness[d] = -1; // do not render
Serial.print("uncought out of bounds: y=");
Serial.print(pixco[d][1]);
Serial.print("particle x=");
Serial.print(particles[i].x);
Serial.print(" y=");
Serial.println(particles[i].y);
useLocalBuffer = false;
free(colorbuffer); // free buffer memory
}
}*/
if (useLocalBuffer)
{
if (pxlbrightness[0] > 0)
colorbuffer[pixco[0][0]][pixco[0][1]] = fast_color_add(colorbuffer[pixco[0][0]][pixco[0][1]], baseRGB, pxlbrightness[0]); // bottom left
if (pxlbrightness[1] > 0)
colorbuffer[pixco[1][0]][pixco[1][1]] = fast_color_add(colorbuffer[pixco[1][0]][pixco[1][1]], baseRGB, pxlbrightness[1]); // bottom right
if (pxlbrightness[2] > 0)
colorbuffer[pixco[2][0]][pixco[2][1]] = fast_color_add(colorbuffer[pixco[2][0]][pixco[2][1]], baseRGB, pxlbrightness[2]); // top right
if (pxlbrightness[3] > 0)
colorbuffer[pixco[3][0]][pixco[3][1]] = fast_color_add(colorbuffer[pixco[3][0]][pixco[3][1]], baseRGB, pxlbrightness[3]); // top left
}
else
{
SEGMENT.fill(BLACK); // clear the matrix
if (pxlbrightness[0] > 0)
SEGMENT.addPixelColorXY(pixco[0][0], maxYpixel - pixco[0][1], baseRGB.scale8((uint8_t)pxlbrightness[0])); // bottom left
if (pxlbrightness[1] > 0)
SEGMENT.addPixelColorXY(pixco[1][0], maxYpixel - pixco[1][1], baseRGB.scale8((uint8_t)pxlbrightness[1])); // bottom right
if (pxlbrightness[2] > 0)
SEGMENT.addPixelColorXY(pixco[2][0], maxYpixel - pixco[2][1], baseRGB.scale8((uint8_t)pxlbrightness[2])); // top right
if (pxlbrightness[3] > 0)
SEGMENT.addPixelColorXY(pixco[3][0], maxYpixel - pixco[3][1], baseRGB.scale8((uint8_t)pxlbrightness[3])); // top left
// test to render larger pixels with minimal effort (not working yet, need to calculate coordinate from actual dx position but brightness seems right), could probably be extended to 3x3
// SEGMENT.addPixelColorXY(pixco[1][0] + 1, maxYpixel - pixco[1][1], baseRGB.scale8((uint8_t)((brightness>>1) - pxlbrightness[0])), fastcoloradd);
// SEGMENT.addPixelColorXY(pixco[2][0] + 1, maxYpixel - pixco[2][1], baseRGB.scale8((uint8_t)((brightness>>1) -pxlbrightness[3])), fastcoloradd);
}
}
if (useLocalBuffer)
{
uint32_t yflipped;
for (int y = 0; y <= maxYpixel; y++)
{
yflipped = maxYpixel - y;
for (int x = 0; x <= maxXpixel; x++)
{
SEGMENT.setPixelColorXY(x, yflipped, colorbuffer[x][y]);
}
}
free(colorbuffer); // free buffer memory
}
}
// calculate pixel positions and brightness distribution for rendering function
// pixelpositions are the physical positions in the matrix that the particle renders to (4x2 array for the four positions)
void ParticleSystem::renderParticle(PSparticle* particle, uint32_t brightess, int32_t *pixelvalues, int32_t (*pixelpositions)[2])
{
// subtract half a radius as the rendering algorithm always starts at the bottom left, this makes calculations more efficient
int32_t xoffset = particle->x - PS_P_HALFRADIUS;
int32_t yoffset = particle->y - PS_P_HALFRADIUS;
int32_t dx = xoffset % PS_P_RADIUS; //relativ particle position in subpixel space
int32_t dy = yoffset % PS_P_RADIUS;
int32_t x = xoffset >> PS_P_RADIUS_SHIFT; // divide by PS_P_RADIUS which is 64, so can bitshift (compiler may not optimize automatically)
int32_t y = yoffset >> PS_P_RADIUS_SHIFT;
// set the four raw pixel coordinates, the order is bottom left [0], bottom right[1], top right [2], top left [3]
pixelpositions[0][0] = pixelpositions[3][0] = x; // bottom left & top left
pixelpositions[0][1] = pixelpositions[1][1] = y; // bottom left & bottom right
pixelpositions[1][0] = pixelpositions[2][0] = x + 1; // bottom right & top right
pixelpositions[2][1] = pixelpositions[3][1] = y + 1; // top right & top left
// now check if any are out of frame. set values to -1 if they are so they can be easily checked after (no value calculation, no setting of pixelcolor if value < 0)
if (x < 0) // left pixels out of frame
{
dx = PS_P_RADIUS + dx; // if x<0, xoffset becomes negative (and so does dx), must adjust dx as modulo will flip its value (really old bug now finally fixed)
//note: due to inverted shift math, a particel at position -32 (xoffset = -64, dx = 64) is rendered at the wrong pixel position (it should be out of frame)
//checking this above makes this algorithm slower (in frame pixels do not have to be checked), so just correct for it here:
if (dx == PS_P_RADIUS)
{
pixelvalues[1] = pixelvalues[2] = -1; // pixel is actually out of matrix boundaries, do not render
}
if (particlesettings.wrapX) // wrap x to the other side if required
pixelpositions[0][0] = pixelpositions[3][0] = maxXpixel;
else
pixelvalues[0] = pixelvalues[3] = -1; // pixel is out of matrix boundaries, do not render
}
else if (pixelpositions[1][0] > maxXpixel) // right pixels, only has to be checkt if left pixels did not overflow
{
if (particlesettings.wrapX) // wrap y to the other side if required
pixelpositions[1][0] = pixelpositions[2][0] = 0;
else
pixelvalues[1] = pixelvalues[2] = -1;
}
if (y < 0) // bottom pixels out of frame
{
dy = PS_P_RADIUS + dy; //see note above
if (dy == PS_P_RADIUS)
{
pixelvalues[2] = pixelvalues[3] = -1; // pixel is actually out of matrix boundaries, do not render
}
if (particlesettings.wrapY) // wrap y to the other side if required
pixelpositions[0][1] = pixelpositions[1][1] = maxYpixel;
else
pixelvalues[0] = pixelvalues[1] = -1;
}
else if (pixelpositions[2][1] > maxYpixel) // top pixels
{
if (particlesettings.wrapY) // wrap y to the other side if required
pixelpositions[2][1] = pixelpositions[3][1] = 0;
else
pixelvalues[2] = pixelvalues[3] = -1;
}
// calculate brightness values for all four pixels representing a particle using linear interpolation
// precalculate values for speed optimization
int32_t precal1 = (int32_t)PS_P_RADIUS - dx;
int32_t precal2 = ((int32_t)PS_P_RADIUS - dy) * brightess;
int32_t precal3 = dy * brightess;
//calculate the values for pixels that are in frame
if (pixelvalues[0] >= 0)
pixelvalues[0] = (precal1 * precal2) >> PS_P_SURFACE; // bottom left value equal to ((PS_P_RADIUS - dx) * (PS_P_RADIUS-dy) * brightess) >> PS_P_SURFACE
if (pixelvalues[1] >= 0)
pixelvalues[1] = (dx * precal2) >> PS_P_SURFACE; // bottom right value equal to (dx * (PS_P_RADIUS-dy) * brightess) >> PS_P_SURFACE
if (pixelvalues[2] >= 0)
pixelvalues[2] = (dx * precal3) >> PS_P_SURFACE; // top right value equal to (dx * dy * brightess) >> PS_P_SURFACE
if (pixelvalues[3] >= 0)
pixelvalues[3] = (precal1 * precal3) >> PS_P_SURFACE; // top left value equal to ((PS_P_RADIUS-dx) * dy * brightess) >> PS_P_SURFACE
/*
Serial.print("x:");
Serial.print(particle->x);
Serial.print(" y:");
Serial.print(particle->y);
//Serial.print(" xo");
//Serial.print(xoffset);
//Serial.print(" dx");
//Serial.print(dx);
//Serial.print(" ");
for(uint8_t t = 0; t<4; t++)
{
Serial.print(" v");
Serial.print(pixelvalues[t]);
Serial.print(" x");
Serial.print(pixelpositions[t][0]);
Serial.print(" y");
Serial.print(pixelpositions[t][1]);
Serial.print(" ");
}
Serial.println(" ");
*/
/*
// debug: check coordinates if out of buffer boundaries print out some info
for (uint32_t d = 0; d < 4; d++)
{
if (pixelpositions[d][0] < 0 || pixelpositions[d][0] > maxXpixel)
{
//Serial.print("<");
if (pixelvalues[d] >= 0)
{
Serial.print("uncought out of bounds: x:");
Serial.print(pixelpositions[d][0]);
Serial.print(" y:");
Serial.print(pixelpositions[d][1]);
Serial.print("particle x=");
Serial.print(particle->x);
Serial.print(" y=");
Serial.println(particle->y);
pixelvalues[d] = -1; // do not render
}
}
if (pixelpositions[d][1] < 0 || pixelpositions[d][1] > maxYpixel)
{
//Serial.print("^");
if (pixelvalues[d] >= 0)
{
Serial.print("uncought out of bounds: x:");
Serial.print(pixelpositions[d][0]);
Serial.print(" y:");
Serial.print(pixelpositions[d][1]);
Serial.print("particle x=");
Serial.print(particle->x);
Serial.print(" y=");
Serial.println(particle->y);
pixelvalues[d] = -1; // do not render
}
}
}
*/
}
// update & move particle, wraps around left/right if settings.wrapX is true, wrap around up/down if settings.wrapY is true
// particles move upwards faster if ttl is high (i.e. they are hotter)
void ParticleSystem::fireParticleupdate()
{
//TODO: cleanup this function? check if normal move is much slower, change move function to check y first and check again
//todo: kill out of bounds funktioniert nicht?
uint32_t i = 0;
for (i = 0; i < usedParticles; i++)
{
if (particles[i].ttl > 0)
{
// age
particles[i].ttl--;
// apply velocity
particles[i].x = particles[i].x + (int32_t)particles[i].vx;
particles[i].y = particles[i].y + (int32_t)particles[i].vy + (particles[i].ttl >> 2); // younger particles move faster upward as they are hotter
//particles[i].y = particles[i].y + (int32_t)particles[i].vy;// + (particles[i].ttl >> 3); // younger particles move faster upward as they are hotter //!! shift ttl by 2 is the original value, this is experimental
particles[i].outofbounds = 0;
// check if particle is out of bounds, wrap x around to other side if wrapping is enabled
// as fire particles start below the frame, lots of particles are out of bounds in y direction. to improve speed, only check x direction if y is not out of bounds
// y-direction
if (particles[i].y < -PS_P_HALFRADIUS)
particles[i].outofbounds = 1;
else if (particles[i].y > maxY + PS_P_HALFRADIUS) // particle moved out at the top
particles[i].ttl = 0;
else // particle is in frame in y direction, also check x direction now
{
if ((particles[i].x < 0) || (particles[i].x > maxX))
{
if (particlesettings.wrapX)
{
particles[i].x = wraparound(particles[i].x, maxX);
}
else if ((particles[i].x < -PS_P_HALFRADIUS) || (particles[i].x > maxX + PS_P_HALFRADIUS)) //if fully out of view
{
particles[i].ttl = 0;
}
}
}
}
}
}
// detect collisions in an array of particles and handle them
void ParticleSystem::handleCollisions()
{
// detect and handle collisions
uint32_t i, j;
uint32_t startparticle = 0;
uint32_t endparticle = usedParticles >> 1; // do half the particles, significantly speeds things up
// every second frame, do other half of particles (helps to speed things up as not all collisions are handled each frame, less accurate but good enough)
// if m ore accurate collisions are needed, just call it twice in a row
if (collisioncounter & 0x01)
{
startparticle = endparticle;
endparticle = usedParticles;
}
collisioncounter++;
//startparticle = 0;//!!! test: do all collisions every frame, FPS goes from about 52 to
//endparticle = usedParticles;
for (i = startparticle; i < endparticle; i++)
{
// go though all 'higher number' particles and see if any of those are in close proximity and if they are, make them collide
if (particles[i].ttl > 0 && particles[i].outofbounds == 0 && particles[i].collide) // if particle is alive and does collide and is not out of view
{
int32_t dx, dy; // distance to other particles
for (j = i + 1; j < usedParticles; j++)
{ // check against higher number particles
if (particles[j].ttl > 0) // if target particle is alive
{
dx = particles[i].x - particles[j].x;
if (dx < PS_P_HARDRADIUS && dx > -PS_P_HARDRADIUS) // check x direction, if close, check y direction
{
dy = particles[i].y - particles[j].y;
if (dy < PS_P_HARDRADIUS && dy > -PS_P_HARDRADIUS) // particles are close
collideParticles(&particles[i], &particles[j]);
}
}
}
}
}
}
// handle a collision if close proximity is detected, i.e. dx and/or dy smaller than 2*PS_P_RADIUS
// takes two pointers to the particles to collide and the particle hardness (softer means more energy lost in collision, 255 means full hard)
void ParticleSystem::collideParticles(PSparticle *particle1, PSparticle *particle2) //TODO: dx,dy is calculated just above, can pass it over here to save a few CPU cycles
{
int32_t dx = particle2->x - particle1->x;
int32_t dy = particle2->y - particle1->y;
int32_t distanceSquared = dx * dx + dy * dy;
// Calculate relative velocity (if it is zero, could exit but extra check does not overall speed but deminish it)
int32_t relativeVx = (int16_t)particle2->vx - (int16_t)particle1->vx;
int32_t relativeVy = (int16_t)particle2->vy - (int16_t)particle1->vy;
// if dx and dy are zero (i.e. they meet at the center) give them an offset, if speeds are also zero, also offset them (pushes them apart if they are clumped before enabling collisions)
if (distanceSquared == 0)
{
// Adjust positions based on relative velocity direction
dx = -1;
if (relativeVx < 0) // if true, particle2 is on the right side
dx = 1;
else if(relativeVx == 0) //if true
{
relativeVx = 1;
}
dy = -1;
if (relativeVy < 0)
dy = 1;
else if (relativeVy == 0)
{
relativeVy = 1;
}
distanceSquared = 2; //1 + 1
}
// Calculate dot product of relative velocity and relative distance
int32_t dotProduct = (dx * relativeVx + dy * relativeVy); //is always negative if moving towards each other
int32_t notsorandom = dotProduct & 0x01; // random16(2); //dotprouct LSB should be somewhat random, so no need to calculate a random number
if (dotProduct < 0) // particles are moving towards each other
{
// integer math used to avoid floats.
// overflow check: dx/dy are 7bit, relativV are 8bit -> dotproduct is 15bit, dotproduct/distsquared ist 8b, multiplied by collisionhardness of 8bit. so a 16bit shift is ok, make it 15 to be sure no overflows happen
// note: cannot use right shifts as bit shifting in right direction is asymmetrical for positive and negative numbers and this needs to be accurate! the trick is: only shift positive numers
// Calculate new velocities after collision
uint32_t surfacehardness = collisionHardness < PS_P_MINSURFACEHARDNESS ? PS_P_MINSURFACEHARDNESS : collisionHardness; // if particles are soft, the impulse must stay above a limit or collisions slip through at higher speeds, 170 seems to be a good value
int32_t impulse = -(((((-dotProduct) << 15) / distanceSquared) * surfacehardness) >> 8); // note: inverting before bitshift corrects for asymmetry in right-shifts (and is slightly faster)
int32_t ximpulse = ((impulse) * dx) / 32767; //cannot use bit shifts here, it can be negative, use division by 2^bitshift
int32_t yimpulse = ((impulse) * dy) / 32767;
particle1->vx += ximpulse;
particle1->vy += yimpulse;
particle2->vx -= ximpulse;
particle2->vy -= yimpulse;
if (collisionHardness < surfacehardness) // if particles are soft, they become 'sticky' i.e. apply some friction
{
const uint32_t coeff = collisionHardness + (255 - PS_P_MINSURFACEHARDNESS);
particle1->vx = ((int32_t)particle1->vx * coeff) / 255;
particle1->vy = ((int32_t)particle1->vy * coeff) / 255;
particle2->vx = ((int32_t)particle2->vx * coeff) / 255;
particle2->vy = ((int32_t)particle2->vy * coeff) / 255;
if (collisionHardness < 10) // if they are very soft, stop slow particles completely to make them stick to each other
{
particle1->vx = (particle1->vx < 3 && particle1->vx > -3) ? 0 : particle1->vx;
particle1->vy = (particle1->vy < 3 && particle1->vy > -3) ? 0 : particle1->vy;
particle2->vx = (particle2->vx < 3 && particle2->vx > -3) ? 0 : particle2->vx;
particle2->vy = (particle2->vy < 3 && particle2->vy > -3) ? 0 : particle2->vy;
}
}
// this part is for particle piling: slow them down if they are close (they become sticky) and push them so they counteract gravity
// particles have volume, push particles apart if they are too close
// tried lots of configurations, it works best if not moved but given a little velocity, it tends to oscillate less this way
// a problem with giving velocity is, that on harder collisions, this adds up as it is not dampened enough, so add friction in the FX if required
if (dotProduct > -250) //this means particles are slow (or really really close) so push them apart.
{
/**
//only apply friction if particles are slow or else fast moving particles (as in explosions) get slowed a lot
relativeVy *= relativeVy; //square the speed, apply friction if speed is below 10
if (relativeVy < 100) //particles are slow in y direction -> this works but most animations look much nicer without this friction. add friction in FX if required.
{
//now check x as well (no need to check if y speed is high, this saves some computation time)
relativeVx *= relativeVx; // square the speed, apply friction if speed is below 10
if (relativeVx < 100) // particles are slow in x direction
{
particle1->vx = ((int32_t)particle1->vx * 254) / 256;
particle2->vx = ((int32_t)particle2->vx * 254) / 256;
particle1->vy = ((int32_t)particle1->vy * 254) / 256;
particle2->vy = ((int32_t)particle2->vy * 254) / 256;
}
}*/
// const int32_t HARDDIAMETER = 2 * PS_P_HARDRADIUS; // push beyond the hard radius, helps with keeping stuff fluffed up -> not really
// int32_t push = (2 * PS_P_HARDRADIUS * PS_P_HARDRADIUS - distanceSquared) >> 6; // push a small amount, if pushing too much, it becomse chaotic as waves of pushing run through piles
int32_t pushamount = 1 + ((250 + dotProduct) >> 6); // the closer dotproduct is to zero, the closer the particles are
int32_t push;
// if (dx < HARDDIAMETER && dx > -HARDDIAMETER) //this is always true as it is checked before ntering this function!
{ // distance is too small, push them apart
push = 0;
if (dx < 0) // particle 1 is on the right
push = pushamount; //(HARDDIAMETER + dx) / 4;
else if (dx > 0)
push = -pushamount; //-(HARDDIAMETER - dx) / 4;
else // on the same x coordinate, shift it a little so they do not stack
{
if (notsorandom)
particle1->x++; // move it so pile collapses
else
particle1->x--;
}
particle1->vx += push;
}
// if (dy < HARDDIAMETER && dy > -HARDDIAMETER) //dito
{
push = 0;
if (dy < 0)
push = pushamount; //(HARDDIAMETER + dy) / 4;
else if (dy > 0)
push = -pushamount; //-(HARDDIAMETER - dy) / 4;
else // dy==0
{
if (notsorandom)
particle1->y++; // move it so pile collapses
else
particle1->y--;
}
particle1->vy += push;
}
/*
if (dx < HARDDIAMETER && dx > -HARDDIAMETER)
{ // distance is too small, push them apart
push = 0;
if (dx < 0) // particle 1 is on the right
push = 2; //(HARDDIAMETER + dx) / 4;
else if (dx > 0)
push = -2; //-(HARDDIAMETER - dx) / 4;
else //on the same x coordinate, shift it a little so they do not stack
particle1->x += 2;
if (notsorandom) // chose one of the particles to push, avoids oscillations
{
if (!particle1->flag3)
{
particle1->vx += push;
particle1->flag3 = 1; // particle was pushed, is reset on next push request
}
else
particle1->flag3 = 0; //reset
}
else
{
if (!particle2->flag3)
{
particle2->vx -= push;
particle2->flag3 = 1; // particle was pushed, is reset on next push request
}
else
particle2->flag3 = 0; // reset
}
}
if (dy < HARDDIAMETER && dy > -HARDDIAMETER)
{
push = 0;
if (dy < 0)
push = 2; //(HARDDIAMETER + dy) / 4;
else if (dy > 0)
push = -2; //-(HARDDIAMETER - dy) / 4;
if (!notsorandom) // chose one of the particles to push, avoids oscillations
{
if (!particle1->flag3)
{
particle1->vy += push;
particle1->flag3 = 1; // particle was pushed, is reset on next push request
}
else
particle1->flag3 = 0; // reset
}
else
{
if (!particle2->flag3)
{
particle2->vy -= push;
particle2->flag3 = 1; // particle was pushed, is reset on next push request
}
else
particle2->flag3 = 0; // reset
}
}*/
// note: pushing may push particles out of frame, if bounce is active, it will move it back as position will be limited to within frame, if bounce is disabled: bye bye
}
}
}
//fast calculation of particle wraparound (modulo version takes 37 instructions, this only takes 28, other variants are slower on ESP8266)
//function assumes that out of bounds is checked before calling it
int32_t ParticleSystem::wraparound(int32_t p, int32_t maxvalue)
{
if (p < 0)
{
p += maxvalue + 1;
}
else //if (p > maxvalue)
{
p -= maxvalue + 1;
}
return p;
}
//calculate the delta speed (dV) value and update the counter for force calculation (is used several times, function saves on codesize)
//force is in 3.4 fixedpoint notation, +/-127
int32_t ParticleSystem::calcForce_dV(int8_t force, uint8_t* counter)
{
// for small forces, need to use a delay counter
int32_t force_abs = abs(force); // absolute value (faster than lots of if's only 7 instructions)
int32_t dv = 0;
// for small forces, need to use a delay counter, apply force only if it overflows
if (force_abs < 16)
{
*counter += force_abs;
if (*counter > 15)
{
*counter -= 16;
dv = (force < 0) ? -1 : ((force > 0) ? 1 : 0); // force is either, 1, 0 or -1 if it is small
}
}
else
{
dv = force >> 4; // MSBs
}
return dv;
}
// allocate memory for the 2D array in one contiguous block and set values to zero
CRGB **ParticleSystem::allocate2Dbuffer(uint32_t cols, uint32_t rows)
{
cli();//!!! test to see if anything messes with the allocation (flicker issues)
CRGB ** array2D = (CRGB **)malloc(cols * sizeof(CRGB *) + cols * rows * sizeof(CRGB));
sei();
if (array2D == NULL)
DEBUG_PRINT(F("PS buffer alloc failed"));
else
{
// assign pointers of 2D array
CRGB *start = (CRGB *)(array2D + cols);
for (uint i = 0; i < cols; i++)
{
array2D[i] = start + i * rows;
}
memset(start, 0, cols * rows * sizeof(CRGB)); // set all values to zero
}
return array2D;
}
//update size and pointers (memory location and size can change dynamically)
//note: do not access the PS class in FX befor running this function (or it messes up SEGMENT.data)
void ParticleSystem::updateSystem(void)
{
// update matrix size
uint32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1;
uint32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength();
setMatrixSize(cols, rows);
updatePSpointers();
}
// set the pointers for the class (this only has to be done once and not on every FX call, only the class pointer needs to be reassigned to SEGENV.data every time)
// function returns the pointer to the next byte available for the FX (if it assigned more memory for other stuff using the above allocate function)
// FX handles the PSsources, need to tell this function how many there are
void ParticleSystem::updatePSpointers()
{
//DEBUG_PRINT(F("*** PS pointers ***"));
//DEBUG_PRINTF_P(PSTR("this PS %p\n"), this);
particles = reinterpret_cast<PSparticle *>(this + 1); // pointer to particle array at data+sizeof(ParticleSystem)
sources = reinterpret_cast<PSsource *>(particles + numParticles); // pointer to source(s)
PSdataEnd = reinterpret_cast<uint8_t *>(sources + numSources); // pointer to first available byte after the PS
//DEBUG_PRINTF_P(PSTR("particles %p\n"), particles);
//DEBUG_PRINTF_P(PSTR("sources %p\n"), sources);
//DEBUG_PRINTF_P(PSTR("end %p\n"), PSdataEnd);
}
//non class functions to use for initialization
uint32_t calculateNumberOfParticles()
{
uint32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1;
uint32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength();
#ifdef ESP8266
uint numberofParticles = (cols * rows * 3) / 4; // 0.75 particle per pixel
uint particlelimit = ESP8266_MAXPARTICLES; // maximum number of paticles allowed (based on one segment of 16x16 and 4k effect ram)
#elif ARDUINO_ARCH_ESP32S2
uint numberofParticles = (cols * rows); // 1 particle per pixe
uint particlelimit = ESP32S2_MAXPARTICLES; // maximum number of paticles allowed (based on one segment of 32x32 and 24k effect ram)
#else
uint numberofParticles = (cols * rows); // 1 particle per pixel (for example 768 particles on 32x16)
uint particlelimit = ESP32_MAXPARTICLES; // maximum number of paticles allowed (based on two segments of 32x32 and 40k effect ram)
#endif
numberofParticles = max((uint)1, min(numberofParticles, particlelimit));
return numberofParticles;
}
uint32_t calculateNumberOfSources()
{
uint32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1;
uint32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength();
#ifdef ESP8266
int numberofSources = (cols * rows) / 8;
numberofSources = max(1, min(numberofSources, ESP8266_MAXSOURCES)); // limit to 1 - 16
#elif ARDUINO_ARCH_ESP32S2
int numberofSources = (cols * rows) / 6;
numberofSources = max(1, min(numberofSources, ESP32S2_MAXSOURCES)); // limit to 1 - 48
#else
int numberofSources = (cols * rows) / 4;
numberofSources = max(1, min(numberofSources, ESP32_MAXSOURCES)); // limit to 1 - 72
#endif
return numberofSources;
}
//allocate memory for particle system class, particles, sprays plus additional memory requested by FX
bool allocateParticleSystemMemory(uint16_t numparticles, uint16_t numsources, uint16_t additionalbytes)
{
uint32_t requiredmemory = sizeof(ParticleSystem);
requiredmemory += sizeof(PSparticle) * numparticles;
requiredmemory += sizeof(PSsource) * numsources;
requiredmemory += additionalbytes;
Serial.print("allocating: ");
Serial.print(requiredmemory);
Serial.println("Bytes");
Serial.print("allocating for segment at");
Serial.println((uintptr_t)SEGMENT.data);
return(SEGMENT.allocateData(requiredmemory));
}
// initialize Particle System, allocate additional bytes if needed (pointer to those bytes can be read from particle system class: PSdataEnd)
bool initParticleSystem(ParticleSystem *&PartSys, uint16_t additionalbytes)
{
Serial.println("PS init function");
uint32_t numparticles = calculateNumberOfParticles();
uint32_t numsources = calculateNumberOfSources();
if (!allocateParticleSystemMemory(numparticles, numsources, additionalbytes))
{
DEBUG_PRINT(F("PS init failed: memory depleted"));
return false;
}
Serial.print("segment.data ptr");
Serial.println((uintptr_t)(SEGMENT.data));
uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1;
uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength();
Serial.println("calling constructor");
PartSys = new (SEGMENT.data) ParticleSystem(cols, rows, numparticles, numsources); // particle system constructor TODO: why does VS studio thinkt this is bad?
Serial.print("PS pointer at ");
Serial.println((uintptr_t)PartSys);
return true;
}
// fastled color adding is very inaccurate in color preservation
// a better color add function is implemented in colors.cpp but it uses 32bit RGBW. to use it colors need to be shifted just to then be shifted back by that function, which is slow
// this is a fast version for RGB (no white channel, PS does not handle white) and with native CRGB including scaling of second color (fastled scale8 can be made faster using native 32bit on ESP)
CRGB fast_color_add(CRGB c1, CRGB c2, uint32_t scale)
{
CRGB result;
scale++; //add one to scale so 255 will not scale when shifting
uint32_t r = c1.r + ((c2.r * (scale)) >> 8);
uint32_t g = c1.g + ((c2.g * (scale)) >> 8);
uint32_t b = c1.b + ((c2.b * (scale)) >> 8);
uint32_t max = r;
if (g > max) //note: using ? operator would be slower by 2 cpu cycles
max = g;
if (b > max)
max = b;
if (max < 256)
{
result.r = r;
result.g = g;
result.b = b;
}
else
{
result.r = (r * 255) / max;
result.g = (g * 255) / max;
result.b = (b * 255) / max;
}
return result;
}