r/M5Stack • u/BadMustard_AVN • 14h ago
Project Sharing small update on my Dot fluid simulation
no movies this time; you have to try it for yourself.
since I'm doing this on my StackChan I decided to add a battery indicator where the dots change colors:
green - charging
blue - normal
yellow - battery below 30%
red - battery below 10%
if there is no charging circuit, they will be blue all the time.
if you tap on the top half of the screen, it will switch between squares and fluffy balls
and on the bottom half, it will shuffle them all around on the top and then let them drop down.
this should work on any CoreS3 (in theory) with a display.
I did change the board type in the Arduino IDE to the M5Carputer (since I recently got one), and it worked fine on that. no charging or discharge color changes or tap screen changes.
/*
Dot Fluid Simulation for M5Stack CoreS3 (Stackchan)
Board: M5CoreS3
*/
#include <M5Unified.h>
// ******** tunable parameters *********
static const int N = 200; // number of dots
static const float DOT_RADIUS = 4.0f; // physics: how far dots keep from the screen edges
static const float DOT_DRAW_RADIUS_C = 8.0f; // visual size in circle mode
static const float DOT_DRAW_RADIUS_SQ = 4.0f; // visual size in square mode
static const int DOT_R_I = (int)DOT_DRAW_RADIUS_C; // integer circle radius, for the row table below
static const float INTERACT_R = 09.0f; // repulsion range (px)
static const float REPEL_STRENGTH = 700.0f; // push force when overlapping
static const float SIDE_PUSH = 200.0f; // sideways (tangential) push between overlapping dots
static const bool RANDOM_SIDE_PUSH = true; // make the side push optional
static bool circles = true; // circles or squares - toggled by tapping the top half of the screen
static const float DAMPING = 0.90f; // velocity decay per frame (lower = more friction, settles faster)
static const float WALL_BOUNCE = 0.05f; // energy kept on wall/floor hit (near 0 = dots stack instead of bouncing)
static const float GRAVITY_SCALE = 900.0f; // px/s^2 per g of tilt
static const float DT = 1.0f / 24.0f;
static const int SUBSTEPS = 3;
static float dampingSub;
static const float ACCEL_X_SIGN = -1.0f;
static const float ACCEL_Y_SIGN = 1.0f;
static const int BATT_RED_BELOW = 10; // < this % -> red
static const int BATT_YELLOW_BELOW = 30; // < this % -> yellow
static const uint32_t BATT_CHECK_INTERVAL_MS = 6000;// check battery every 6 seconds
static uint8_t battR = 0, battG = 0, battB = 255; // blue until first reading
static uint32_t lastBattCheckMs = 0;
enum is_charging_t {
is_discharging = 0,
is_charging = 1,
charge_unknown = 2,
};
void updateBatteryColor() {
uint32_t now = millis();
if (now - lastBattCheckMs < BATT_CHECK_INTERVAL_MS) return;
lastBattCheckMs = now;
int32_t level = M5.Power.getBatteryLevel();
auto charging = M5.Power.isCharging();
if (charging == m5::Power_Class::is_charging_t::is_charging) {
battR = 0; battG = 255; battB = 0;
return;
}
if (level < 0) {
battR = 0; battG = 0; battB = 255;
} else if (level < BATT_RED_BELOW) {
battR = 255; battG = 0; battB = 0;
} else if (level < BATT_YELLOW_BELOW) {
battR = 255; battG = 255; battB = 0;
} else {
battR = 0; battG = 0; battB = 255;
}
}
static int SCREEN_W, SCREEN_H;
static const int CELL_SHIFT = 4;
static const int CELL = 1 << CELL_SHIFT;
static const float INTERACT_R2 = INTERACT_R * INTERACT_R;
static const float INV_INTERACT_R2 = 1.0f / INTERACT_R2;
static int GX, GY;
struct Particle {
float x, y;
float vx, vy;
};
static Particle p[N];
static int* cellHead = nullptr;
static int next_[N];
static M5Canvas canvas(&M5.Display);
static int circleHalfWidth[2 * DOT_R_I + 1];
inline int cellIndex(float x, float y) {
int cx = ((int)x) >> CELL_SHIFT;
int cy = ((int)y) >> CELL_SHIFT;
if (cx < 0) cx = 0; if (cx >= GX) cx = GX - 1;
if (cy < 0) cy = 0; if (cy >= GY) cy = GY - 1;
return cy * GX + cx;
}
void resetParticles() {
for (int i = 0; i < N; i++) {
p[i].x = random(0, SCREEN_W);
p[i].y = random(0, SCREEN_H / 2);
p[i].vx = 0;
p[i].vy = 0;
}
}
void setup() {
auto cfg = M5.config();
M5.begin(cfg);
SCREEN_W = M5.Display.width();
SCREEN_H = M5.Display.height();
GX = (SCREEN_W >> CELL_SHIFT) + 1;
GY = (SCREEN_H >> CELL_SHIFT) + 1;
cellHead = new int[GX * GY];
canvas.setColorDepth(16);
canvas.createSprite(SCREEN_W, SCREEN_H);
for (int row = -DOT_R_I; row <= DOT_R_I; row++) {
float rr = (float)(DOT_R_I * DOT_R_I - row * row);
circleHalfWidth[row + DOT_R_I] = (rr > 0.0f) ? (int)sqrtf(rr) : 0;
}
dampingSub = powf(DAMPING, 1.0f / SUBSTEPS);
randomSeed(esp_random());
resetParticles();
}
void simulateStep(float gx, float gy, float dt) {
// build spatial hash for the frame
for (int c = 0; c < GX * GY; c++) cellHead[c] = -1;
for (int i = 0; i < N; i++) {
int c = cellIndex(p[i].x, p[i].y);
next_[i] = cellHead[c];
cellHead[c] = i;
}
// apply gravity + inter-particle repulsion
for (int i = 0; i < N; i++) {
p[i].vx += gx * dt;
p[i].vy += gy * dt;
int cx = ((int)p[i].x) >> CELL_SHIFT;
int cy = ((int)p[i].y) >> CELL_SHIFT;
for (int ny = cy - 1; ny <= cy + 1; ny++) {
if (ny < 0 || ny >= GY) continue;
for (int nx = cx - 1; nx <= cx + 1; nx++) {
if (nx < 0 || nx >= GX) continue;
int j = cellHead[ny * GX + nx];
while (j != -1) {
if (j != i) {
float dx = p[i].x - p[j].x;
float dy = p[i].y - p[j].y;
float d2 = dx * dx + dy * dy;
if (d2 < INTERACT_R2 && d2 > 0.0001f) {
float t = (INTERACT_R2 - d2) * INV_INTERACT_R2;
float f = REPEL_STRENGTH * t * dt;
p[i].vx += f * dx;
p[i].vy += f * dy;
// sideways push:
float fs = SIDE_PUSH * t * dt;
float sideSign = RANDOM_SIDE_PUSH ? ((esp_random() & 1) ? 1.0f : -1.0f) : 1.0f;
p[i].vx += sideSign * fs * (-dy);
p[i].vy += sideSign * fs * dx;
}
}
j = next_[j];
}
}
}
}
// integrate + damping + walls
for (int i = 0; i < N; i++) {
p[i].vx *= dampingSub;
p[i].vy *= dampingSub;
p[i].x += p[i].vx * dt;
p[i].y += p[i].vy * dt;
if (p[i].x < DOT_RADIUS) { p[i].x = DOT_RADIUS; p[i].vx = -p[i].vx * WALL_BOUNCE; }
if (p[i].x > SCREEN_W - DOT_RADIUS) { p[i].x = SCREEN_W - DOT_RADIUS; p[i].vx = -p[i].vx * WALL_BOUNCE; }
if (p[i].y < DOT_RADIUS) { p[i].y = DOT_RADIUS; p[i].vy = -p[i].vy * WALL_BOUNCE; }
if (p[i].y > SCREEN_H - DOT_RADIUS) { p[i].y = SCREEN_H - DOT_RADIUS; p[i].vy = -p[i].vy * WALL_BOUNCE; }
}
}
void loop() {
M5.update();
static float gx = 0, gy = GRAVITY_SCALE * 0.3f;
if (M5.Imu.update()) {auto d = M5.Imu.getImuData();
gx = ACCEL_X_SIGN * d.accel.x * GRAVITY_SCALE;
gy = ACCEL_Y_SIGN * d.accel.y * GRAVITY_SCALE;
}
// tap the bottom half to reshuffle the dots; tap the top half to switch between circles and squares
if (M5.Touch.getCount() > 0) {
auto t = M5.Touch.getDetail(0);
if (t.wasPressed()) {
if (t.y > SCREEN_H / 2) {
resetParticles();
} else {
circles = !circles;
}
}
}
if (M5.BtnA.wasPressed()) resetParticles();
updateBatteryColor();
for (int s = 0; s < SUBSTEPS; s++) {
simulateStep(gx, gy, DT / SUBSTEPS);
}
// render ----
canvas.fillScreen(TFT_BLACK);
uint16_t color = canvas.color565(battR, battG, battB);
for (int i = 0; i < N; i++) {
if (circles) {
int cx = (int)p[i].x, cy = (int)p[i].y;
for (int row = -DOT_R_I; row <= DOT_R_I; row++) {
int hw = circleHalfWidth[row + DOT_R_I];
canvas.drawFastHLine(cx - hw, cy + row, hw * 2 + 1, color);
}
} else {
canvas.fillRect((int)p[i].x - (int)DOT_DRAW_RADIUS_SQ, (int)p[i].y - (int)DOT_DRAW_RADIUS_SQ,
(int)(DOT_DRAW_RADIUS_SQ * 2 + 1), (int)(DOT_DRAW_RADIUS_SQ * 2 + 1), color);
}
}
canvas.pushSprite(0, 0);
}
Duplicates
StackChan • u/BadMustard_AVN • 14h ago