-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathFXparticleSystem.cpp
More file actions
1941 lines (1754 loc) · 97.4 KB
/
FXparticleSystem.cpp
File metadata and controls
1941 lines (1754 loc) · 97.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/*
FXparticleSystem.cpp
Particle system with functions for particle generation, particle movement and particle rendering to RGB matrix.
by DedeHai (Damian Schneider) 2013-2024
Copyright (c) 2025 Damian Schneider
Licensed under the EUPL v. 1.2 or later
*/
#ifdef WLED_DISABLE_2D
#define WLED_DISABLE_PARTICLESYSTEM2D
#endif
#if !(defined(WLED_DISABLE_PARTICLESYSTEM2D) && defined(WLED_DISABLE_PARTICLESYSTEM1D)) // not both disabled
#include "FXparticleSystem.h"
// 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, 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);
#endif
#ifndef WLED_DISABLE_PARTICLESYSTEM2D
ParticleSystem2D::ParticleSystem2D(uint32_t width, uint32_t height, uint32_t numberofparticles, uint32_t numberofsources, bool isadvanced, bool sizecontrol) {
PSPRINTLN("\n ParticleSystem2D constructor");
numSources = numberofsources; // number of sources allocated in init
numParticles = numberofparticles; // number of particles allocated in init
usedParticles = numParticles; // use all particles by default
advPartProps = nullptr; //make sure we start out with null pointers (just in case memory was not cleared)
advPartSize = nullptr;
setMatrixSize(width, height);
updatePSpointers(isadvanced, sizecontrol); // set the particle and sources pointer (call this before accessing sprays or particles)
setWallHardness(255); // set default wall hardness to max
setWallRoughness(0); // smooth walls by default
setGravity(0); //gravity disabled by default
setParticleSize(1); // 2x2 rendering size by default
motionBlur = 0; //no fading by default
smearBlur = 0; //no smearing by default
emitIndex = 0;
collisionStartIdx = 0;
//initialize some default non-zero values most FX use
for (uint32_t i = 0; i < numParticles; i++) {
particles[i].sat = 255; // full saturation
}
for (uint32_t i = 0; i < numSources; i++) {
sources[i].source.sat = 255; //set saturation to max by default
sources[i].source.ttl = 1; //set source alive
sources[i].sourceFlags.asByte = 0; // all flags disabled
}
}
// update function applies gravity, moves the particles, handles collisions and renders the particles
void ParticleSystem2D::update(void) {
//apply gravity globally if enabled
if (particlesettings.useGravity)
applyGravity();
//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
particles[i].ttl = 0; // kill particle
}
}
}
// 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 (uint32_t i = 0; i < usedParticles; i++) {
particleMoveUpdate(particles[i], particleFlags[i], nullptr, advPartProps ? &advPartProps[i] : nullptr); // note: splitting this into two loops is slower and uses more flash
}
render();
}
// update function for fire animation
void ParticleSystem2D::updateFire(const uint8_t intensity,const bool renderonly) {
if (!renderonly)
fireParticleupdate();
fireIntesity = intensity > 0 ? intensity : 1; // minimum of 1, zero checking is used in render function
render();
}
// set percentage of used particles as uint8_t i.e 127 means 50% for example
void ParticleSystem2D::setUsedParticles(uint8_t percentage) {
usedParticles = (numParticles * ((int)percentage+1)) >> 8; // number of particles to use (percentage is 0-255, 255 = 100%)
PSPRINT(" SetUsedpaticles: allocated particles: ");
PSPRINT(numParticles);
PSPRINT(" ,used particles: ");
PSPRINTLN(usedParticles);
}
void ParticleSystem2D::setWallHardness(uint8_t hardness) {
wallHardness = hardness;
}
void ParticleSystem2D::setWallRoughness(uint8_t roughness) {
wallRoughness = roughness;
}
void ParticleSystem2D::setCollisionHardness(uint8_t hardness) {
collisionHardness = (int)hardness + 1;
}
void ParticleSystem2D::setMatrixSize(uint32_t x, uint32_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 (also by FX) to calculate positions
}
void ParticleSystem2D::setWrapX(bool enable) {
particlesettings.wrapX = enable;
}
void ParticleSystem2D::setWrapY(bool enable) {
particlesettings.wrapY = enable;
}
void ParticleSystem2D::setBounceX(bool enable) {
particlesettings.bounceX = enable;
}
void ParticleSystem2D::setBounceY(bool enable) {
particlesettings.bounceY = enable;
}
void ParticleSystem2D::setKillOutOfBounds(bool enable) {
particlesettings.killoutofbounds = enable;
}
void ParticleSystem2D::setColorByAge(bool enable) {
particlesettings.colorByAge = enable;
}
void ParticleSystem2D::setMotionBlur(uint8_t bluramount) {
if (particlesize < 2) // only allow motion blurring on default particle sizes or advanced size (cannot combine motion blur with normal blurring used for particlesize, would require another buffer)
motionBlur = bluramount;
}
void ParticleSystem2D::setSmearBlur(uint8_t bluramount) {
smearBlur = bluramount;
}
// render size using smearing (see blur function)
void ParticleSystem2D::setParticleSize(uint8_t size) {
particlesize = size;
particleHardRadius = PS_P_MINHARDRADIUS; // ~1 pixel
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
}
else if (particlesize == 0)
particleHardRadius = particleHardRadius >> 1; // single pixel particles have half the radius (i.e. 1/2 pixel)
}
// enable/disable gravity, optionally, set the force (force=8 is default) can be -127 to +127, 0 is disable
// if enabled, gravity is applied to all particles in ParticleSystemUpdate()
// 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 ParticleSystem2D::setGravity(int8_t force) {
if (force) {
gforce = force;
particlesettings.useGravity = true;
} else {
particlesettings.useGravity = false;
}
}
void ParticleSystem2D::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 = (int)hardness + 1;
}
// emit one particle with variation, returns index of emitted particle (or -1 if no particle emitted)
int32_t ParticleSystem2D::sprayEmit(const PSsource &emitter) {
bool success = false;
for (uint32_t i = 0; i < usedParticles; i++) {
emitIndex++;
if (emitIndex >= usedParticles)
emitIndex = 0;
if (particles[emitIndex].ttl == 0) { // find a dead particle
success = true;
particles[emitIndex].vx = emitter.vx + hw_random16(emitter.var << 1) - emitter.var; // random(-var, var)
particles[emitIndex].vy = emitter.vy + hw_random16(emitter.var << 1) - emitter.var; // random(-var, var)
particles[emitIndex].x = emitter.source.x;
particles[emitIndex].y = emitter.source.y;
particles[emitIndex].hue = emitter.source.hue;
particles[emitIndex].sat = emitter.source.sat;
particleFlags[emitIndex].collide = emitter.sourceFlags.collide;
particles[emitIndex].ttl = hw_random16(emitter.minLife, emitter.maxLife);
if (advPartProps)
advPartProps[emitIndex].size = emitter.size;
break;
}
}
if (success)
return emitIndex;
else
return -1;
}
// 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;
}
// 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)
int32_t ParticleSystem2D::angleEmit(PSsource &emitter, const uint16_t angle, const int32_t speed) {
emitter.vx = ((int32_t)cos16_t(angle) * speed) / (int32_t)32600; // cos16_t() and sin16_t() return signed 16bit, division should be 32767 but 32600 gives slightly better rounding
emitter.vy = ((int32_t)sin16_t(angle) * speed) / (int32_t)32600; // note: cannot use bit shifts as bit shifting is asymmetrical for positive and negative numbers and this needs to be accurate!
return 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 enabled, it will never bounce at the top and killoutofbounds is not applied over the top
void ParticleSystem2D::particleMoveUpdate(PSparticle &part, PSparticleFlags &partFlags, PSsettings2D *options, PSadvancedParticle *advancedproperties) {
if (options == nullptr)
options = &particlesettings; //use PS system settings by default
if (part.ttl > 0) {
if (!partFlags.perpetual)
part.ttl--; // age
if (options->colorByAge)
part.hue = min(part.ttl, (uint16_t)255); //set color to ttl
int32_t renderradius = PS_P_HALFRADIUS; // used to check out of bounds
int32_t newX = part.x + (int32_t)part.vx;
int32_t newY = part.y + (int32_t)part.vy;
partFlags.outofbounds = false; // reset out of bounds (in case particle was created outside the matrix and is now moving into view) note: moving this to checks below adds code and is not faster
if (advancedproperties) { //using individual particle size?
setParticleSize(particlesize); // updates default particleHardRadius
if (advancedproperties->size > PS_P_MINHARDRADIUS) {
particleHardRadius += (advancedproperties->size - PS_P_MINHARDRADIUS); // update radius
renderradius = particleHardRadius;
}
}
// note: if wall collisions are enabled, bounce them before they reach the edge, it looks much nicer if the particle does not go half out of view
if (options->bounceY) {
if ((newY < (int32_t)particleHardRadius) || ((newY > (int32_t)(maxY - particleHardRadius)) && !options->useGravity)) { // reached floor / ceiling
bounce(part.vy, part.vx, newY, maxY);
}
}
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
part.ttl = 0;
else if (!options->useGravity)
part.ttl = 0;
}
}
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
partFlags.outofbounds = true;
if (options->killoutofbounds)
part.ttl = 0;
}
}
part.x = (int16_t)newX; // set new position
part.y = (int16_t)newY; // set new position
}
}
// move function for fire particles
void ParticleSystem2D::fireParticleupdate() {
for (uint32_t i = 0; i < usedParticles; i++) {
if (particles[i].ttl > 0)
{
particles[i].ttl--; // age
int32_t newY = particles[i].y + (int32_t)particles[i].vy + (particles[i].ttl >> 2); // younger particles move faster upward as they are hotter
int32_t newX = particles[i].x + (int32_t)particles[i].vx;
particleFlags[i].outofbounds = 0; // reset out of bounds flag note: moving this to checks below is not faster but adds code
// 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
if (newY < -PS_P_HALFRADIUS)
particleFlags[i].outofbounds = 1;
else if (newY > int32_t(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 Note: using checkBoundsAndWrap() is slower, only saves a few bytes
{
if ((newX < 0) || (newX > (int32_t)maxX)) { // handle out of bounds & wrap
if (particlesettings.wrapX) {
newX = newX % (maxX + 1);
if (newX < 0) // handle negative modulo
newX += maxX + 1;
}
else if ((newX < -PS_P_HALFRADIUS) || (newX > int32_t(maxX + PS_P_HALFRADIUS))) { //if fully out of view
particles[i].ttl = 0;
}
}
particles[i].x = newX;
}
particles[i].y = newY;
}
}
}
// update advanced particle size control, returns false if particle shrinks to 0 size
bool ParticleSystem2D::updateSize(PSadvancedParticle *advprops, PSsizeControl *advsize) {
if (advsize == nullptr) // safety check
return false;
// grow/shrink particle
int32_t newsize = advprops->size;
uint32_t counter = advsize->sizecounter;
uint32_t increment = 0;
// calculate grow speed using 0-8 for low speeds and 9-15 for higher speeds
if (advsize->grow) increment = advsize->growspeed;
else if (advsize->shrink) increment = advsize->shrinkspeed;
if (increment < 9) { // 8 means +1 every frame
counter += increment;
if (counter > 7) {
counter -= 8;
increment = 1;
} else
increment = 0;
advsize->sizecounter = counter;
} else {
increment = (increment - 8) << 1; // 9 means +2, 10 means +4 etc. 15 means +14
}
if (advsize->grow) {
if (newsize < advsize->maxsize) {
newsize += increment;
if (newsize >= advsize->maxsize) {
advsize->grow = false; // stop growing, shrink from now on if enabled
newsize = advsize->maxsize; // limit
if (advsize->pulsate) advsize->shrink = true;
}
}
} else if (advsize->shrink) {
if (newsize > advsize->minsize) {
newsize -= increment;
if (newsize <= advsize->minsize) {
if (advsize->minsize == 0)
return false; // particle shrunk to zero
advsize->shrink = false; // disable shrinking
newsize = advsize->minsize; // limit
if (advsize->pulsate) advsize->grow = true;
}
}
}
advprops->size = newsize;
// handle wobbling
if (advsize->wobble) {
advsize->asymdir += advsize->wobblespeed; // note: if need better wobblespeed control a counter is already in the struct
}
return true;
}
// calculate x and y size for asymmetrical particles (advanced size control)
void ParticleSystem2D::getParticleXYsize(PSadvancedParticle *advprops, PSsizeControl *advsize, uint32_t &xsize, uint32_t &ysize) {
if (advsize == nullptr) // if advsize is valid, also advanced properties pointer is valid (handled by updatePSpointers())
return;
int32_t size = advprops->size;
int32_t asymdir = advsize->asymdir;
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) >> 6;
} else if (asymdir < 192) {
deviation = ((128 - asymdir) * deviation) >> 6;
} else {
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);
ysize = min((size + deviation), (int32_t)255);;
}
// 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 + 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
position = maxposition - particleHardRadius;
if (wallRoughness) {
int32_t incomingspeed_abs = abs((int32_t)incomingspeed);
int32_t totalspeed = incomingspeed_abs + abs((int32_t)parallelspeed);
// transfer an amount of incomingspeed speed to parallel speed
int32_t donatespeed = ((hw_random16(incomingspeed_abs << 1) - incomingspeed_abs) * (int32_t)wallRoughness) / (int32_t)255; // take random portion of + or - perpendicular speed, scaled by roughness
parallelspeed = limitSpeed((int32_t)parallelspeed + donatespeed);
// give the remainder of the speed to perpendicular speed
donatespeed = int8_t(totalspeed - abs(parallelspeed)); // keep total speed the same
incomingspeed = incomingspeed > 0 ? donatespeed : -donatespeed;
}
}
// apply a force in x,y direction to individual particle
// caller needs to provide a 8bit counter (for each particle) that holds its value between calls
// 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 ParticleSystem2D::applyForce(PSparticle &part, const int8_t xforce, const 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
part.vx = limitSpeed((int32_t)part.vx + dvx);
part.vy = limitSpeed((int32_t)part.vy + dvy);
}
// apply a force in x,y direction to individual particle using advanced particle properties
void ParticleSystem2D::applyForce(const uint32_t particleindex, const int8_t xforce, const int8_t yforce) {
if (advPartProps == nullptr)
return; // no advanced properties available
applyForce(particles[particleindex], xforce, yforce, advPartProps[particleindex].forcecounter);
}
// apply a force in x,y direction to all particles
// force is in 3.4 fixed point notation (see above)
void ParticleSystem2D::applyForce(const int8_t xforce, const int8_t yforce) {
// for small forces, need to use a delay counter
uint8_t tempcounter;
// note: this is not the most computationally efficient way to do this, but it saves on duplicate code and is fast enough
for (uint32_t i = 0; i < usedParticles; i++) {
tempcounter = forcecounter;
applyForce(particles[i], xforce, yforce, tempcounter);
}
forcecounter = tempcounter; // save value back
}
// apply a force in angular direction to single particle
// caller needs to provide a 8bit counter that holds its value between calls (if using single particles, a counter for each particle is needed)
// angle is from 0-65535 (=0-360deg) angle = 0 means in positive x-direction (i.e. to the right)
// force is in 3.4 fixed point notation so force=16 means apply v+1 each frame (useful force range is +/- 127)
void ParticleSystem2D::applyAngleForce(PSparticle &part, const int8_t force, const uint16_t angle, uint8_t &counter) {
int8_t xforce = ((int32_t)force * cos16_t(angle)) / 32767; // force is +/- 127
int8_t yforce = ((int32_t)force * sin16_t(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, xforce, yforce, counter);
}
void ParticleSystem2D::applyAngleForce(const uint32_t particleindex, const int8_t force, const uint16_t angle) {
if (advPartProps == nullptr)
return; // no advanced properties available
applyAngleForce(particles[particleindex], force, angle, advPartProps[particleindex].forcecounter);
}
// apply a force in angular direction to all particles
// angle is from 0-65535 (=0-360deg) angle = 0 means in positive x-direction (i.e. to the right)
void ParticleSystem2D::applyAngleForce(const int8_t force, const uint16_t angle) {
int8_t xforce = ((int32_t)force * cos16_t(angle)) / 32767; // force is +/- 127
int8_t yforce = ((int32_t)force * sin16_t(angle)) / 32767; // note: cannot use bit shifts as bit shifting is asymmetrical for positive and negative numbers and this needs to be accurate!
applyForce(xforce, yforce);
}
// apply gravity to all particles using PS global gforce setting
// force is in 3.4 fixed point notation, see note above
// 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;
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);
}
}
// apply gravity to single particle using system settings (use this for sources)
// function does not increment gravity counter, if gravity setting is disabled, this cannot be used
void ParticleSystem2D::applyGravity(PSparticle &part) {
uint32_t counterbkp = gforcecounter; // backup PS gravity counter
int32_t dv = calcForce_dv(gforce, gforcecounter);
gforcecounter = counterbkp; //save it back
part.vy = limitSpeed((int32_t)part.vy - dv);
}
// slow down particle by friction, the higher the speed, the higher the friction. a high friction coefficient slows them more (255 means instant stop)
// note: a coefficient smaller than 0 will speed them up (this is a feature, not a bug), coefficient larger than 255 inverts the speed, so don't do that
void ParticleSystem2D::applyFriction(PSparticle &part, const int32_t coefficient) {
// note: not checking if particle is dead can be done by caller (or can be omitted)
#if defined(CONFIG_IDF_TARGET_ESP32C3) || defined(ESP8266) // use bitshifts with rounding instead of division (2x faster)
int32_t friction = 256 - coefficient;
part.vx = ((int32_t)part.vx * friction + (((int32_t)part.vx >> 31) & 0xFF)) >> 8; // note: (v>>31) & 0xFF)) extracts the sign and adds 255 if negative for correct rounding using shifts
part.vy = ((int32_t)part.vy * friction + (((int32_t)part.vy >> 31) & 0xFF)) >> 8;
#else // division is faster on ESP32, S2 and S3
int32_t friction = 255 - coefficient;
part.vx = ((int32_t)part.vx * friction) / 255;
part.vy = ((int32_t)part.vy * friction) / 255;
#endif
}
// apply friction to all particles
// note: not checking if particle is dead is faster as most are usually alive and if few are alive, rendering is fast anyways
void ParticleSystem2D::applyFriction(const int32_t coefficient) {
#if defined(CONFIG_IDF_TARGET_ESP32C3) || defined(ESP8266) // use bitshifts with rounding instead of division (2x faster)
int32_t friction = 256 - coefficient;
for (uint32_t i = 0; i < usedParticles; i++) {
particles[i].vx = ((int32_t)particles[i].vx * friction + (((int32_t)particles[i].vx >> 31) & 0xFF)) >> 8; // note: (v>>31) & 0xFF)) extracts the sign and adds 255 if negative for correct rounding using shifts
particles[i].vy = ((int32_t)particles[i].vy * friction + (((int32_t)particles[i].vy >> 31) & 0xFF)) >> 8;
}
#else // division is faster on ESP32, S2 and S3
int32_t friction = 255 - coefficient;
for (uint32_t i = 0; i < usedParticles; i++) {
particles[i].vx = ((int32_t)particles[i].vx * friction) / 255;
particles[i].vy = ((int32_t)particles[i].vy * friction) / 255;
}
#endif
}
// attracts a particle to an attractor particle using the inverse square-law
void ParticleSystem2D::pointAttractor(const uint32_t particleindex, PSparticle &attractor, const uint8_t strength, const bool swallow) {
if (advPartProps == nullptr)
return; // no advanced properties available
// Calculate the distance between the particle and the attractor
int32_t dx = attractor.x - particles[particleindex].x;
int32_t dy = attractor.y - particles[particleindex].y;
// Calculate the force based on inverse square law
int32_t distanceSquared = dx * dx + dy * dy;
if (distanceSquared < 8192) {
if (swallow) { // particle is close, age it fast so it fades out, do not attract further
if (particles[particleindex].ttl > 7)
particles[particleindex].ttl -= 8;
else {
particles[particleindex].ttl = 0;
return;
}
}
distanceSquared = 2 * 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(particleindex, xforce, yforce);
}
// 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
// firemode is only used for PS Fire FX
void ParticleSystem2D::render() {
CRGB baseRGB;
uint32_t brightness; // particle brightness, fades if dying
TBlendType blend = LINEARBLEND; // default color rendering: wrap palette
if (particlesettings.colorByAge) {
blend = LINEARBLEND_NOWRAP;
}
if (motionBlur) { // motion-blurring active
for (int32_t y = 0; y <= maxYpixel; y++) {
int index = y * (maxXpixel + 1);
for (int32_t x = 0; x <= maxXpixel; x++) {
fast_color_scale(framebuffer[index], motionBlur); // note: could skip if only globalsmear is active but usually they are both active and scaling is fast enough
index++;
}
}
}
else { // no blurring: clear buffer
memset(framebuffer, 0, (maxXpixel+1) * (maxYpixel+1) * sizeof(CRGB));
}
// go over particles and render them to the buffer
for (uint32_t i = 0; i < usedParticles; i++) {
if (particles[i].ttl == 0 || particleFlags[i].outofbounds)
continue;
// generate RGB values for particle
if (fireIntesity) { // fire mode
brightness = (uint32_t)particles[i].ttl * (3 + (fireIntesity >> 5)) + 20;
brightness = min(brightness, (uint32_t)255);
baseRGB = ColorFromPalette(SEGPALETTE, brightness, 255, LINEARBLEND_NOWRAP);
}
else {
brightness = min((particles[i].ttl << 1), (int)255);
baseRGB = ColorFromPalette(SEGPALETTE, particles[i].hue, 255, blend);
if (particles[i].sat < 255) {
CHSV baseHSV = rgb2hsv_approximate(baseRGB); // convert to HSV
baseHSV.s = particles[i].sat; // set the saturation
hsv2rgb_spectrum(baseHSV, baseRGB); // convert back to RGB
}
}
renderParticle(i, brightness, baseRGB, particlesettings.wrapX, particlesettings.wrapY);
}
// apply global size rendering
if (particlesize > 1) {
uint32_t passes = particlesize / 64 + 1; // number of blur passes, four passes max
uint32_t bluramount = particlesize;
uint32_t bitshift = 0;
for (uint32_t i = 0; i < passes; 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;
blur2D(framebuffer, maxXpixel + 1, maxYpixel + 1, bluramount << bitshift, bluramount << bitshift);
bluramount -= 64;
}
}
// apply 2D blur to rendered frame
if (smearBlur) {
blur2D(framebuffer, maxXpixel + 1, maxYpixel + 1, smearBlur, smearBlur);
}
// transfer the framebuffer to the segment
for (int y = 0; y <= maxYpixel; y++) {
int index = y * (maxXpixel + 1); // current row index for 1D buffer
for (int x = 0; x <= maxXpixel; x++) {
SEGMENT.setPixelColorXY(x, y, framebuffer[index++]);
}
}
}
// calculate pixel positions and brightness distribution and render the particle to local buffer or global buffer
__attribute__((optimize("O2"))) void ParticleSystem2D::renderParticle(const uint32_t particleindex, const uint8_t brightness, const CRGB& color, const bool wrapX, const bool wrapY) {
uint32_t size = particlesize;
if (advPartProps && advPartProps[particleindex].size > 0) // use advanced size properties (0 means use global size including single pixel rendering)
size = advPartProps[particleindex].size;
if (size == 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) {
fast_color_add(framebuffer[x + (maxYpixel - y) * (maxXpixel + 1)], color, brightness);
}
return;
}
uint8_t pxlbrightness[4]; // brightness values for the four pixels representing a particle
struct {
int32_t x,y;
} pixco[4]; // particle pixel coordinates, the order is bottom left [0], bottom right[1], top right [2], top left [3] (thx @blazoncek for improved readability struct)
bool pixelvalid[4] = {true, true, true, true}; // is set to false if pixel is out of bounds
// add half a radius as the rendering algorithm always starts at the bottom left, this leaves things positive, so shifts can be used, then shift coordinate by a full pixel (x--/y-- below)
int32_t xoffset = particles[particleindex].x + PS_P_HALFRADIUS;
int32_t yoffset = particles[particleindex].y + PS_P_HALFRADIUS;
int32_t dx = xoffset & (PS_P_RADIUS - 1); // relativ particle position in subpixel space
int32_t dy = yoffset & (PS_P_RADIUS - 1); // modulo replaced with bitwise AND, as radius is always a power of 2
int32_t x = (xoffset >> PS_P_RADIUS_SHIFT); // divide by PS_P_RADIUS which is 64, so can bitshift (compiler can not optimize integer)
int32_t y = (yoffset >> PS_P_RADIUS_SHIFT);
// set the four raw pixel coordinates
pixco[1].x = pixco[2].x = x; // bottom right & top right
pixco[2].y = pixco[3].y = y; // top right & top left
x--; // shift by a full pixel here, this is skipped above to not do -1 and then +1
y--;
pixco[0].x = pixco[3].x = x; // bottom left & top left
pixco[0].y = pixco[1].y = y; // bottom left & bottom right
// calculate brightness values for all four pixels representing a particle using linear interpolation
// could check for out of frame pixels here but calculating them is faster (very few are out)
// precalculate values for speed optimization
int32_t precal1 = (int32_t)PS_P_RADIUS - dx;
int32_t precal2 = ((int32_t)PS_P_RADIUS - dy) * brightness;
int32_t precal3 = dy * brightness;
pxlbrightness[0] = (precal1 * precal2) >> PS_P_SURFACE; // bottom left value equal to ((PS_P_RADIUS - dx) * (PS_P_RADIUS-dy) * brightness) >> PS_P_SURFACE
pxlbrightness[1] = (dx * precal2) >> PS_P_SURFACE; // bottom right value equal to (dx * (PS_P_RADIUS-dy) * brightness) >> PS_P_SURFACE
pxlbrightness[2] = (dx * precal3) >> PS_P_SURFACE; // top right value equal to (dx * dy * brightness) >> PS_P_SURFACE
pxlbrightness[3] = (precal1 * precal3) >> PS_P_SURFACE; // top left value equal to ((PS_P_RADIUS-dx) * dy * brightness) >> PS_P_SURFACE
if (advPartProps && advPartProps[particleindex].size > 1) { //render particle to a bigger size
CRGB renderbuffer[100]; // 10x10 pixel buffer
memset(renderbuffer, 0, sizeof(renderbuffer)); // clear buffer
//particle size to pixels: < 64 is 4x4, < 128 is 6x6, < 192 is 8x8, bigger is 10x10
//first, render the pixel to the center of the renderbuffer, then apply 2D blurring
fast_color_add(renderbuffer[4 + (4 * 10)], color, pxlbrightness[0]); // oCrder is: bottom left, bottom right, top right, top left
fast_color_add(renderbuffer[5 + (4 * 10)], color, pxlbrightness[1]);
fast_color_add(renderbuffer[5 + (5 * 10)], color, pxlbrightness[2]);
fast_color_add(renderbuffer[4 + (5 * 10)], color, pxlbrightness[3]);
uint32_t rendersize = 2; // initialize render size, minimum is 4x4 pixels, it is incremented int he loop below to start with 4
uint32_t offset = 4; // offset to zero coordinate to write/read data in renderbuffer (actually needs to be 3, is decremented in the loop below)
uint32_t maxsize = advPartProps[particleindex].size;
uint32_t xsize = maxsize;
uint32_t ysize = maxsize;
if (advPartSize) { // use advanced size control
if (advPartSize[particleindex].asymmetry > 0)
getParticleXYsize(&advPartProps[particleindex], &advPartSize[particleindex], xsize, ysize);
maxsize = (xsize > ysize) ? xsize : ysize; // choose the bigger of the two
}
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++) {
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;
offset--;
blur2D(renderbuffer, rendersize, rendersize, xsize << bitshift, ysize << bitshift, offset, offset, true);
xsize = xsize > 64 ? xsize - 64 : 0;
ysize = ysize > 64 ? ysize - 64 : 0;
}
// calculate origin coordinates to render the particle to in the framebuffer
uint32_t xfb_orig = x - (rendersize>>1) + 1 - offset;
uint32_t yfb_orig = y - (rendersize>>1) + 1 - offset;
uint32_t xfb, yfb; // coordinates in frame buffer to write to note: by making this uint, only overflow has to be checked (spits a warning though)
//note on y-axis flip: WLED has the y-axis defined from top to bottom, so y coordinates must be flipped. doing this in the buffer xfer clashes with 1D/2D combined rendering, which does not invert y
// transferring the 1D buffer in inverted fashion will flip the x-axis of overlaid 2D FX, so the y-axis flip is done here so the buffer is flipped in y, giving correct results
// transfer particle renderbuffer to framebuffer
for (uint32_t xrb = offset; xrb < rendersize + offset; xrb++) {
xfb = xfb_orig + xrb;
if (xfb > (uint32_t)maxXpixel) {
if (wrapX) { // wrap x to the other side if required
if (xfb > (uint32_t)maxXpixel << 1) // xfb is "negative", handle it
xfb = (maxXpixel + 1) + (int32_t)xfb; // this always overflows to within bounds
else
xfb = xfb % (maxXpixel + 1); // note: without the above "negative" check, this works only for powers of 2
}
else
continue;
}
for (uint32_t yrb = offset; yrb < rendersize + offset; yrb++) {
yfb = yfb_orig + yrb;
if (yfb > (uint32_t)maxYpixel) {
if (wrapY) {// wrap y to the other side if required
if (yfb > (uint32_t)maxYpixel << 1) // yfb is "negative", handle it
yfb = (maxYpixel + 1) + (int32_t)yfb; // this always overflows to within bounds
else
yfb = yfb % (maxYpixel + 1); // note: without the above "negative" check, this works only for powers of 2
}
else
continue;
}
fast_color_add(framebuffer[xfb + (maxYpixel - yfb) * (maxXpixel + 1)], renderbuffer[xrb + yrb * 10]);
}
}
} else { // standard rendering (2x2 pixels)
// check for out of frame pixels and wrap them if required: x,y is bottom left pixel coordinate of the particle
if (x < 0) { // left pixels out of frame
if (wrapX) { // wrap x to the other side if required
pixco[0].x = pixco[3].x = maxXpixel;
} else {
pixelvalid[0] = pixelvalid[3] = false; // out of bounds
}
}
else if (pixco[1].x > (int32_t)maxXpixel) { // right pixels, only has to be checked if left pixel is in frame
if (wrapX) { // wrap y to the other side if required
pixco[1].x = pixco[2].x = 0;
} else {
pixelvalid[1] = pixelvalid[2] = false; // out of bounds
}
}
if (y < 0) { // bottom pixels out of frame
if (wrapY) { // wrap y to the other side if required
pixco[0].y = pixco[1].y = maxYpixel;
} else {
pixelvalid[0] = pixelvalid[1] = false; // out of bounds
}
}
else if (pixco[2].y > maxYpixel) { // top pixels
if (wrapY) { // wrap y to the other side if required
pixco[2].y = pixco[3].y = 0;
} else {
pixelvalid[2] = pixelvalid[3] = false; // out of bounds
}
}
for (uint32_t i = 0; i < 4; i++) {
if (pixelvalid[i])
fast_color_add(framebuffer[pixco[i].x + (maxYpixel - pixco[i].y) * (maxXpixel + 1)], color, pxlbrightness[i]); // order is: bottom left, bottom right, top right, top left
}
}
}
// detect collisions in an array of particles and handle them
// uses binning by dividing the frame into slices in x direction which is efficient if using gravity in y direction (but less efficient for FX that use forces in x direction)
// for code simplicity, no y slicing is done, making very tall matrix configurations less efficient
// note: also tested adding y slicing, it gives diminishing returns, some FX even get slower. FX not using gravity would benefit with a 10% FPS improvement
void ParticleSystem2D::handleCollisions() {
uint32_t collDistSq = particleHardRadius << 1; // distance is double the radius note: particleHardRadius is updated when setting global particle size
collDistSq = collDistSq * collDistSq; // square it for faster comparison (square is one operation)
// note: partices are binned in x-axis, assumption is that no more than half of the particles are in the same bin
// if they are, collisionStartIdx is increased so each particle collides at least every second frame (which still gives decent collisions)
constexpr int BIN_WIDTH = 6 * PS_P_RADIUS; // width of a bin in sub-pixels
int32_t overlap = particleHardRadius << 1; // overlap bins to include edge particles to neighbouring bins
if (advPartProps) //may be using individual particle size
overlap += 512; // add 2 * max radius (approximately)
uint32_t maxBinParticles = max((uint32_t)50, (usedParticles + 1) / 2); // assume no more than half of the particles are in the same bin, do not bin small amounts of particles
uint32_t numBins = (maxX + (BIN_WIDTH - 1)) / BIN_WIDTH; // number of bins in x direction
uint16_t binIndices[maxBinParticles]; // creat array on stack for indices, 2kB max for 1024 particles (ESP32_MAXPARTICLES/2)
uint32_t binParticleCount; // number of particles in the current bin
uint16_t nextFrameStartIdx = hw_random16(usedParticles); // index of the first particle in the next frame (set to fixed value if bin overflow)
uint32_t pidx = collisionStartIdx; //start index in case a bin is full, process remaining particles next frame
// fill the binIndices array for this bin
for (uint32_t bin = 0; bin < numBins; bin++) {
binParticleCount = 0; // reset for this bin
int32_t binStart = bin * BIN_WIDTH - overlap; // note: first bin will extend to negative, but that is ok as out of bounds particles are ignored
int32_t binEnd = binStart + BIN_WIDTH + overlap; // note: last bin can be out of bounds, see above;
// fill the binIndices array for this bin
for (uint32_t i = 0; i < usedParticles; i++) {
if (particles[pidx].ttl > 0) { // is alive
if (particles[pidx].x >= binStart && particles[pidx].x <= binEnd) { // >= and <= to include particles on the edge of the bin (overlap to ensure boarder particles collide with adjacent bins)
if(particleFlags[pidx].outofbounds == 0 && particleFlags[pidx].collide) { // particle is in frame and does collide note: checking flags is quite slow and usually these are set, so faster to check here
if (binParticleCount >= maxBinParticles) { // bin is full, more particles in this bin so do the rest next frame
nextFrameStartIdx = pidx; // bin overflow can only happen once as bin size is at least half of the particles (or half +1)
break;
}
binIndices[binParticleCount++] = pidx;
}
}
}
pidx++;
if (pidx >= usedParticles) pidx = 0; // wrap around
}
for (uint32_t i = 0; i < binParticleCount; i++) { // go though all 'higher number' particles in this bin and see if any of those are in close proximity and if they are, make them collide
uint32_t idx_i = binIndices[i];
for (uint32_t j = i + 1; j < binParticleCount; j++) { // check against higher number particles
uint32_t idx_j = binIndices[j];
if (advPartProps) { //may be using individual particle size
setParticleSize(particlesize); // updates base particleHardRadius
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_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_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);
}
}
}
}
collisionStartIdx = nextFrameStartIdx; // set the start index for the next frame
}
// 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)
__attribute__((optimize("O2"))) void ParticleSystem2D::collideParticles(PSparticle &particle1, PSparticle &particle2, int32_t dx, int32_t dy, const uint32_t collDistSq) {
int32_t distanceSquared = dx * dx + dy * dy;
// 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;
// if dx and dy are zero (i.e. same position) give them an offset, if speeds are also zero, also offset them (pushes particles 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)
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
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
int32_t surfacehardness = 1 + max(collisionHardness, (int32_t)PS_P_MINSURFACEHARDNESS); // 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 (is slightly faster)
#if defined(CONFIG_IDF_TARGET_ESP32C3) || defined(ESP8266) // use bitshifts with rounding instead of division (2x faster)
int32_t ximpulse = (impulse * dx + ((dx >> 31) & 32767)) >> 15; // note: extracting sign bit and adding rounding value to correct for asymmetry in right shifts
int32_t yimpulse = (impulse * dy + ((dy >> 31) & 32767)) >> 15;
#else
int32_t ximpulse = (impulse * dx) / 32767;
int32_t yimpulse = (impulse * dy) / 32767;
#endif
particle1.vx -= ximpulse; // note: impulse is inverted, so subtracting it
particle1.vy -= yimpulse;
particle2.vx += ximpulse;
particle2.vy += yimpulse;
if (collisionHardness < PS_P_MINSURFACEHARDNESS && (SEGMENT.call & 0x07) == 0) { // if particles are soft, they become 'sticky' i.e. apply some friction (they do pile more nicely and stop sloshing around)
const uint32_t coeff = collisionHardness + (255 - PS_P_MINSURFACEHARDNESS);
// Note: could call applyFriction, but this is faster and speed is key here
#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
particle1.vy = ((int32_t)particle1.vy * coeff + (((int32_t)particle1.vy >> 31) & 0xFF)) >> 8;
particle2.vx = ((int32_t)particle2.vx * coeff + (((int32_t)particle2.vx >> 31) & 0xFF)) >> 8;
particle2.vy = ((int32_t)particle2.vy * coeff + (((int32_t)particle2.vy >> 31) & 0xFF)) >> 8;
#else // division is faster on ESP32, S2 and S3
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;
#endif
}
// 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
// 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
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;
if (dx < 0) // particle 1 is on the right
push = pushamount;
else if (dx > 0)
push = -pushamount;
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;
push = 0;
if (dy < 0)
push = pushamount;
else if (dy > 0)
push = -pushamount;
else { // dy==0
if (notsorandom)
particle1.y++; // move it so pile collapses
else
particle1.y--;
}
particle1.vy += push;
// 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
if (collisionHardness < 5) { // if they are very soft, stop slow particles completely to make them stick to each other
particle1.vx = 0;
particle1.vy = 0;
particle2.vx = 0;
particle2.vy = 0;
//push them apart
particle1.x += push;
particle1.y += push;
}
}
}
}
// 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 SEGENV.data)
void ParticleSystem2D::updateSystem(void) {
PSPRINTLN("updateSystem2D");
setMatrixSize(SEGMENT.virtualWidth(), SEGMENT.virtualHeight());
updatePSpointers(advPartProps != nullptr, advPartSize != nullptr); // update pointers to PS data, also updates availableParticles
PSPRINTLN("\n END update System2D, running FX...");
}
// 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 ParticleSystem2D::updatePSpointers(bool isadvanced, bool sizecontrol) {
PSPRINTLN("updatePSpointers");
// Note on memory alignment:
// a pointer MUST be 4 byte aligned. sizeof() in a struct/class is always aligned to the largest element. if it contains a 32bit, it will be padded to 4 bytes, 16bit is padded to 2byte alignment.
// The PS is aligned to 4 bytes, a PSparticle is aligned to 2 and a struct containing only byte sized variables is not aligned at all and may need to be padded when dividing the memoryblock.
// by making sure that the number of sources and particles is a multiple of 4, padding can be skipped here as alignent is ensured, independent of struct sizes.
particles = reinterpret_cast<PSparticle *>(this + 1); // pointer to particles
particleFlags = reinterpret_cast<PSparticleFlags *>(particles + numParticles); // pointer to particle flags
sources = reinterpret_cast<PSsource *>(particleFlags + numParticles); // pointer to source(s) at data+sizeof(ParticleSystem2D)
framebuffer = reinterpret_cast<CRGB *>(sources + numSources); // pointer to framebuffer
// align pointer after framebuffer
uintptr_t p = reinterpret_cast<uintptr_t>(framebuffer + (maxXpixel+1)*(maxYpixel+1));
p = (p + 3) & ~0x03; // align to 4-byte boundary
PSdataEnd = reinterpret_cast<uint8_t *>(p); // pointer to first available byte after the PS for FX additional data
if (isadvanced) {
advPartProps = reinterpret_cast<PSadvancedParticle *>(PSdataEnd);
PSdataEnd = reinterpret_cast<uint8_t *>(advPartProps + numParticles);
if (sizecontrol) {
advPartSize = reinterpret_cast<PSsizeControl *>(PSdataEnd);
PSdataEnd = reinterpret_cast<uint8_t *>(advPartSize + numParticles);
}
}
#ifdef DEBUG_PS
Serial.printf_P(PSTR(" particles %p "), particles);
Serial.printf_P(PSTR(" sources %p "), sources);
Serial.printf_P(PSTR(" adv. props %p "), advPartProps);
Serial.printf_P(PSTR(" adv. ctrl %p "), advPartSize);
Serial.printf_P(PSTR("end %p\n"), PSdataEnd);
#endif
}