Mirko Pavleski
Published © GPL3+

ESP32 Fluid simulation on 16x16 Led Matrix

This is a simple visually interesting toy for describing fluid dynamics.

BeginnerFull instructions provided2 hours949
ESP32 Fluid simulation on 16x16 Led Matrix

Things used in this project

Hardware components

Espressif ESP32 Development Board - Developer Edition
Espressif ESP32 Development Board - Developer Edition
×1
6 DOF Sensor - MPU6050
DFRobot 6 DOF Sensor - MPU6050
×1
16x16 Led matrix with WS2812B chips
×1
Button
×1

Software apps and online services

Arduino IDE
Arduino IDE

Hand tools and fabrication machines

Soldering iron (generic)
Soldering iron (generic)
Solder Wire, Lead Free
Solder Wire, Lead Free

Story

Read more

Schematics

Schematic

...

Code

CODE Colors Button

C/C++
..
  // colors:   0 = Red, 32 = Orange, 64 = Yellow, 96 = Green, 128 = Aqua, 160 = Blue, 192 = Purple, 224 = Pink


#include <FastLED.h>
#include <Wire.h>
#include <MPU6050.h>

// Pin definitions
#define LED_PIN     5
#define SDA_PIN     21
#define SCL_PIN     22
#define BUTTON_PIN  4   // Button for color switching

#define NUM_LEDS    256
#define MATRIX_WIDTH 16
#define MATRIX_HEIGHT 16
#define FLUID_PARTICLES 64  //80/64
#define BRIGHTNESS  30
#define NUM_COLORS  3   // Number of color options

// Structures
struct Vector2D {
    float x;
    float y;
};

struct Particle {
    Vector2D position;
    Vector2D velocity;
};

// Global variables
CRGB leds[NUM_LEDS];
MPU6050 mpu;
Particle particles[FLUID_PARTICLES];
Vector2D acceleration = {0, 0};

// Color switching variables
uint8_t currentColorIndex = 0;
unsigned long lastDebounceTime = 0;
const unsigned long debounceDelay = 200;

// Define the colors (you can change these hue values)
const uint8_t COLORS[NUM_COLORS] = {
    160,  // Blue
    0,    // Red
    96    // Green
};

// Mutex for synchronization
portMUX_TYPE dataMux = portMUX_INITIALIZER_UNLOCKED;

// Constants for physics
const float GRAVITY = 0.08f;  //0.3f /098f
const float DAMPING = 0.92f;   //0.99f /0.9f
const float MAX_VELOCITY = 0.6f; //0.6f /2.9f

// Function prototypes
void initMPU6050();
void initLEDs();
void initParticles();
void updateParticles();
void drawParticles();
void MPUTask(void *parameter);
void LEDTask(void *parameter);
void checkButton();

// Function to convert x,y coordinates to LED index
int xy(int x, int y) {
    x = constrain(x, 0, MATRIX_WIDTH - 1);
    y = constrain(y, 0, MATRIX_HEIGHT - 1);
    return (y & 1) ? (y * MATRIX_WIDTH + (MATRIX_WIDTH - 1 - x)) : (y * MATRIX_WIDTH + x);
}

void checkButton() {
    static bool lastButtonState = HIGH;
    bool buttonState = digitalRead(BUTTON_PIN);

    if (buttonState == LOW && lastButtonState == HIGH) {  // Button pressed
        if ((millis() - lastDebounceTime) > debounceDelay) {
            currentColorIndex = (currentColorIndex + 1) % NUM_COLORS;
            lastDebounceTime = millis();
        }
    }
    lastButtonState = buttonState;
}

void drawParticles() {
    FastLED.clear();
    
    bool occupied[MATRIX_WIDTH][MATRIX_HEIGHT] = {{false}};
    
    struct ParticleIndex {
        int index;
        float position;
    };
    
    ParticleIndex sortedParticles[FLUID_PARTICLES];
    for (int i = 0; i < FLUID_PARTICLES; i++) {
        sortedParticles[i].index = i;
        sortedParticles[i].position = particles[i].position.y * MATRIX_WIDTH + particles[i].position.x;
    }
    
    for (int i = 0; i < FLUID_PARTICLES - 1; i++) {
        for (int j = 0; j < FLUID_PARTICLES - i - 1; j++) {
            if (sortedParticles[j].position > sortedParticles[j + 1].position) {
                ParticleIndex temp = sortedParticles[j];
                sortedParticles[j] = sortedParticles[j + 1];
                sortedParticles[j + 1] = temp;
            }
        }
    }

    for (int i = 0; i < FLUID_PARTICLES; i++) {
        int particleIndex = sortedParticles[i].index;
        int x = round(particles[particleIndex].position.x);
        int y = round(particles[particleIndex].position.y);
        
        x = constrain(x, 0, MATRIX_WIDTH - 1);
        y = constrain(y, 0, MATRIX_HEIGHT - 1);
        
        if (!occupied[x][y]) {
            int index = xy(x, y);
            if (index >= 0 && index < NUM_LEDS) {
                float speed = sqrt(
                    particles[particleIndex].velocity.x * particles[particleIndex].velocity.x + 
                    particles[particleIndex].velocity.y * particles[particleIndex].velocity.y
                );
                
                uint8_t hue = COLORS[currentColorIndex];
                uint8_t sat = 255;
                uint8_t val = constrain(180 + (speed * 50), 180, 255);
                
                leds[index] = CHSV(hue, sat, val);
                occupied[x][y] = true;
            }
        } else {
            for (int r = 1; r < 3; r++) {
                for (int dx = -r; dx <= r; dx++) {
                    for (int dy = -r; dy <= r; dy++) {
                        if (abs(dx) + abs(dy) == r) {
                            int newX = x + dx;
                            int newY = y + dy;
                            if (newX >= 0 && newX < MATRIX_WIDTH && 
                                newY >= 0 && newY < MATRIX_HEIGHT && 
                                !occupied[newX][newY]) {
                                int index = xy(newX, newY);
                                if (index >= 0 && index < NUM_LEDS) {
                                    leds[index] = CHSV(COLORS[currentColorIndex], 255, 180);
                                    occupied[newX][newY] = true;
                                    goto nextParticle;
                                }
                            }
                        }
                    }
                }
            }
            nextParticle:
            continue;
        }
    }
    
    FastLED.show();
}

void updateParticles() {
    Vector2D currentAccel;
    portENTER_CRITICAL(&dataMux);
    currentAccel = acceleration;
    portEXIT_CRITICAL(&dataMux);

    currentAccel.x *= 0.3f;
    currentAccel.y *= 0.3f;

    for (int i = 0; i < FLUID_PARTICLES; i++) {
        particles[i].velocity.x = particles[i].velocity.x * 0.9f + (currentAccel.x * GRAVITY);
        particles[i].velocity.y = particles[i].velocity.y * 0.9f + (currentAccel.y * GRAVITY);

        particles[i].velocity.x = constrain(particles[i].velocity.x, -MAX_VELOCITY, MAX_VELOCITY);
        particles[i].velocity.y = constrain(particles[i].velocity.y, -MAX_VELOCITY, MAX_VELOCITY);

        float newX = particles[i].position.x + particles[i].velocity.x;
        float newY = particles[i].position.y + particles[i].velocity.y;

        if (newX < 0.0f) {
            newX = 0.0f;
            particles[i].velocity.x = fabs(particles[i].velocity.x) * DAMPING;
        } 
        else if (newX >= (MATRIX_WIDTH - 1)) {
            newX = MATRIX_WIDTH - 1;
            particles[i].velocity.x = -fabs(particles[i].velocity.x) * DAMPING;
        }

        if (newY < 0.0f) {
            newY = 0.0f;
            particles[i].velocity.y = fabs(particles[i].velocity.y) * DAMPING;
        } 
        else if (newY >= (MATRIX_HEIGHT - 1)) {
            newY = MATRIX_HEIGHT - 1;
            particles[i].velocity.y = -fabs(particles[i].velocity.y) * DAMPING;
        }

        particles[i].position.x = constrain(newX, 0.0f, MATRIX_WIDTH - 1);
        particles[i].position.y = constrain(newY, 0.0f, MATRIX_HEIGHT - 1);

        particles[i].velocity.x *= 0.95f;
        particles[i].velocity.y *= 0.95f;
    }

    for (int i = 0; i < FLUID_PARTICLES; i++) {
        for (int j = i + 1; j < FLUID_PARTICLES; j++) {
            float dx = particles[j].position.x - particles[i].position.x;
            float dy = particles[j].position.y - particles[i].position.y;
            float distanceSquared = dx * dx + dy * dy;

            if (distanceSquared < 1.0f) {
                float distance = sqrt(distanceSquared);
                float angle = atan2(dy, dx);
                
                float repulsionX = cos(angle) * 0.5f;
                float repulsionY = sin(angle) * 0.5f;

                particles[i].position.x -= repulsionX * 0.3f;
                particles[i].position.y -= repulsionY * 0.3f;
                particles[j].position.x += repulsionX * 0.3f;
                particles[j].position.y += repulsionY * 0.3f;

                Vector2D avgVel = {
                    (particles[i].velocity.x + particles[j].velocity.x) * 0.5f,
                    (particles[i].velocity.y + particles[j].velocity.y) * 0.5f
                };

                particles[i].velocity = avgVel;
                particles[j].velocity = avgVel;
            }
        }
    }
}

void initMPU6050() {
    Serial.println("Initializing MPU6050...");
    mpu.initialize();
    
    if (!mpu.testConnection()) {
        Serial.println("MPU6050 connection failed!");
        while (1) {
            delay(100);
        }
    }
    
    mpu.setFullScaleAccelRange(MPU6050_ACCEL_FS_2);
    Serial.println("MPU6050 initialized");
}

void initLEDs() {
    Serial.println("Initializing LEDs...");
    FastLED.addLeds<WS2812B, LED_PIN, GRB>(leds, NUM_LEDS);
    FastLED.setBrightness(BRIGHTNESS);
    FastLED.clear(true);
    Serial.println("LEDs initialized");
}

void initParticles() {
    Serial.println("Initializing particles...");
    int index = 0;
    
    for (int y = MATRIX_HEIGHT - 4; y < MATRIX_HEIGHT; y++) {
        for (int x = 0; x < MATRIX_WIDTH && index < FLUID_PARTICLES; x++) {
            particles[index].position = {static_cast<float>(x), static_cast<float>(y)};
            particles[index].velocity = {0.0f, 0.0f};
            index++;
        }
    }
    
    Serial.printf("Total particles initialized: %d\n", index);
}

void MPUTask(void *parameter) {
    while (true) {
        int16_t ax, ay, az;
        mpu.getAcceleration(&ax, &ay, &az);
        
        portENTER_CRITICAL(&dataMux);
        acceleration.x = -constrain(ax / 16384.0f, -1.0f, 1.0f);
        acceleration.y = constrain(ay / 16384.0f, -1.0f, 1.0f);
        portEXIT_CRITICAL(&dataMux);
        
        vTaskDelay(pdMS_TO_TICKS(10));
    }
}

void LEDTask(void *parameter) {
    TickType_t xLastWakeTime = xTaskGetTickCount();
    const TickType_t xFrequency = pdMS_TO_TICKS(16);
    
    while (true) {
        checkButton();
        updateParticles();
        drawParticles();
        vTaskDelayUntil(&xLastWakeTime, xFrequency);
    }
}

void setup() {
    Serial.begin(115200);
    delay(1000);
    Serial.println("Starting initialization...");

    // Initialize button pin
    pinMode(BUTTON_PIN, INPUT_PULLUP);

    Wire.begin(SDA_PIN, SCL_PIN);
    Wire.setClock(400000);

    initMPU6050();
    initLEDs();
    initParticles();

    xTaskCreatePinnedToCore(
        MPUTask,
        "MPUTask",
        4096,
        NULL,
        2,
        NULL,
        0
    );

    xTaskCreatePinnedToCore(
        LEDTask,
        "LEDTask",
        4096,
        NULL,
        1,
        NULL,
        1
    );

    Serial.println("Setup complete");
}

void loop() {
    vTaskDelete(NULL);
}

CODE Color palette

C/C++
..
#include <FastLED.h>
#include <Wire.h>
#include <MPU6050.h>

// Pin definitions
#define LED_PIN     5
#define SDA_PIN     21
#define SCL_PIN     22

#define NUM_LEDS    256
#define MATRIX_WIDTH 16
#define MATRIX_HEIGHT 16
#define FLUID_PARTICLES 64
#define BRIGHTNESS  100

// Structures
struct Vector2D {
    float x;
    float y;
};

struct Particle {
    Vector2D position;
    Vector2D velocity;
};

// Global variables
CRGB leds[NUM_LEDS];
MPU6050 mpu;
Particle particles[FLUID_PARTICLES];
Vector2D acceleration = {0, 0};

// Mutex for synchronization
portMUX_TYPE dataMux = portMUX_INITIALIZER_UNLOCKED;

// Adjusted constants for smoother motion
const float GRAVITY = 0.3f;    //0.03f
const float DAMPING = 0.60f;   //   0.98f
const float MAX_VELOCITY = 0.7f;  //0.3f
const float MIN_MOVEMENT = 0.001f;  //0.001f

// Function prototypes
void initMPU6050();
void initLEDs();
void initParticles();
void updateParticles();
void drawParticles();
void MPUTask(void *parameter);
void LEDTask(void *parameter);

// Function to convert x,y coordinates to LED index
int xy(int x, int y) {
    x = constrain(x, 0, MATRIX_WIDTH - 1);
    y = constrain(y, 0, MATRIX_HEIGHT - 1);
    return (y & 1) ? (y * MATRIX_WIDTH + (MATRIX_WIDTH - 1 - x)) : (y * MATRIX_WIDTH + x);
}

void drawParticles() {
    FastLED.clear();
    
    // Create occupancy grid
    bool occupied[MATRIX_WIDTH][MATRIX_HEIGHT] = {{false}};
    
    // Get and smooth gravity direction
    static Vector2D lastGravityDir = {0, 1};
    Vector2D currentGravityDir;
    
    portENTER_CRITICAL(&dataMux);
    currentGravityDir = acceleration;
    portEXIT_CRITICAL(&dataMux);
    
    const float GRAVITY_SMOOTHING = 0.95f;
    lastGravityDir.x = lastGravityDir.x * GRAVITY_SMOOTHING + currentGravityDir.x * (1 - GRAVITY_SMOOTHING);
    lastGravityDir.y = lastGravityDir.y * GRAVITY_SMOOTHING + currentGravityDir.y * (1 - GRAVITY_SMOOTHING);
    
    // Normalize gravity vector
    float gravMagnitude = sqrt(lastGravityDir.x * lastGravityDir.x + lastGravityDir.y * lastGravityDir.y);
    Vector2D gravityDir = {0, 1}; // Default down direction
    
    if (gravMagnitude > 0.1f) {
        gravityDir.x = lastGravityDir.x / gravMagnitude;
        gravityDir.y = lastGravityDir.y / gravMagnitude;
    }

    // Calculate heights and prepare for drawing
    float heights[FLUID_PARTICLES];
    float minHeight = 1000;
    float maxHeight = -1000;
    
    for (int i = 0; i < FLUID_PARTICLES; i++) {
        heights[i] = -(particles[i].position.x * gravityDir.x + 
                      particles[i].position.y * gravityDir.y);
        minHeight = min(minHeight, heights[i]);
        maxHeight = max(maxHeight, heights[i]);
    }
    
    float heightRange = max(maxHeight - minHeight, 1.0f);

    // Draw particles
    int visibleCount = 0;
    for (int i = 0; i < FLUID_PARTICLES; i++) {
        int x = round(constrain(particles[i].position.x, 0, MATRIX_WIDTH - 1));
        int y = round(constrain(particles[i].position.y, 0, MATRIX_HEIGHT - 1));
        
        if (!occupied[x][y]) {
            int index = xy(x, y);
            if (index >= 0 && index < NUM_LEDS) {
                float relativeHeight = (heights[i] - minHeight) / heightRange;
                 uint8_t hue = relativeHeight * 160;  // Map from 0 (red) to 160 (blue)
                  uint8_t sat = 255;  // Full saturation for vibrant colors
                   uint8_t val = 220 + (relativeHeight * 35);  // Slightly brighter at top;
                
                leds[index] = CHSV(hue, sat, val);
                occupied[x][y] = true;
                visibleCount++;
            }
        } else {
            // Find nearest empty position
            for (int dx = -1; dx <= 1; dx++) {
                for (int dy = -1; dy <= 1; dy++) {
                    if (dx == 0 && dy == 0) continue;
                    
                    int newX = x + dx;
                    int newY = y + dy;
                    
                    if (newX >= 0 && newX < MATRIX_WIDTH && 
                        newY >= 0 && newY < MATRIX_HEIGHT && 
                        !occupied[newX][newY]) {
                        int index = xy(newX, newY);
                        if (index >= 0 && index < NUM_LEDS) {
                            float relativeHeight = (heights[i] - minHeight) / heightRange;
                        uint8_t hue = relativeHeight * 160;  // Map from 0 (red) to 160 (blue)
                        uint8_t sat = 255;  // Full saturation for vibrant colors
                        uint8_t val = 220 + (relativeHeight * 35);  // Slightly brighter at top

                            
                            leds[index] = CHSV(hue, sat, val);
                            occupied[newX][newY] = true;
                            visibleCount++;
                            goto particleDrawn;
                        }
                    }
                }
            }
            particleDrawn: continue;
        }
    }
    
    // Debug output
    static unsigned long lastDebugTime = 0;
    if (millis() - lastDebugTime > 1000) {
        Serial.printf("Visible particles: %d of %d\n", visibleCount, FLUID_PARTICLES);
        lastDebugTime = millis();
    }
    
    FastLED.show();
}
    


void updateParticles() {
    Vector2D currentAccel;
    portENTER_CRITICAL(&dataMux);
    currentAccel = acceleration;
    portEXIT_CRITICAL(&dataMux);

    // Reduce acceleration sensitivity
    currentAccel.x *= 0.3f;
    currentAccel.y *= 0.3f;

    // Update and constrain each particle
    for (int i = 0; i < FLUID_PARTICLES; i++) {
        // Update velocity with acceleration
        particles[i].velocity.x = particles[i].velocity.x * 0.95f + (currentAccel.x * GRAVITY);
        particles[i].velocity.y = particles[i].velocity.y * 0.95f + (currentAccel.y * GRAVITY);

        // Hard constrain velocity
        particles[i].velocity.x = constrain(particles[i].velocity.x, -MAX_VELOCITY, MAX_VELOCITY);
        particles[i].velocity.y = constrain(particles[i].velocity.y, -MAX_VELOCITY, MAX_VELOCITY);

        // Calculate new position
        float newX = particles[i].position.x + particles[i].velocity.x;
        float newY = particles[i].position.y + particles[i].velocity.y;

        // Strict boundary checking with bounce
        if (newX < 0.0f) {
            newX = 0.0f;
            particles[i].velocity.x = fabs(particles[i].velocity.x) * DAMPING;
        } 
        else if (newX >= (MATRIX_WIDTH - 1.0f)) {
            newX = MATRIX_WIDTH - 1.0f;
            particles[i].velocity.x = -fabs(particles[i].velocity.x) * DAMPING;
        }

        if (newY < 0.0f) {
            newY = 0.0f;
            particles[i].velocity.y = fabs(particles[i].velocity.y) * DAMPING;
        } 
        else if (newY >= (MATRIX_HEIGHT - 1.0f)) {
            newY = MATRIX_HEIGHT - 1.0f;
            particles[i].velocity.y = -fabs(particles[i].velocity.y) * DAMPING;
        }

        // Ensure positions are always within bounds
        particles[i].position.x = constrain(newX, 0.0f, MATRIX_WIDTH - 1.0f);
        particles[i].position.y = constrain(newY, 0.0f, MATRIX_HEIGHT - 1.0f);

        // Additional safety check
        if (isnan(particles[i].position.x) || isnan(particles[i].position.y)) {
            particles[i].position.x = MATRIX_WIDTH / 2;
            particles[i].position.y = MATRIX_HEIGHT / 2;
            particles[i].velocity.x = 0;
            particles[i].velocity.y = 0;
        }
    }

    // Particle collision detection and resolution
    for (int i = 0; i < FLUID_PARTICLES; i++) {
        for (int j = i + 1; j < FLUID_PARTICLES; j++) {
            float dx = particles[j].position.x - particles[i].position.x;
            float dy = particles[j].position.y - particles[i].position.y;
            float distSquared = dx * dx + dy * dy;

            if (distSquared < 1.0f && distSquared > 0.0f) {
                float dist = sqrt(distSquared);
                float nx = dx / dist;
                float ny = dy / dist;

                // Push particles apart
                float pushDistance = (1.0f - dist) * 0.5f;
                float pushX = nx * pushDistance;
                float pushY = ny * pushDistance;

                // Update positions while ensuring they stay in bounds
                particles[i].position.x = constrain(particles[i].position.x - pushX, 0.0f, MATRIX_WIDTH - 1.0f);
                particles[i].position.y = constrain(particles[i].position.y - pushY, 0.0f, MATRIX_HEIGHT - 1.0f);
                particles[j].position.x = constrain(particles[j].position.x + pushX, 0.0f, MATRIX_WIDTH - 1.0f);
                particles[j].position.y = constrain(particles[j].position.y + pushY, 0.0f, MATRIX_HEIGHT - 1.0f);

                // Exchange velocities with damping
                float tempVelX = particles[i].velocity.x;
                float tempVelY = particles[i].velocity.y;
                particles[i].velocity.x = particles[j].velocity.x * DAMPING;
                particles[i].velocity.y = particles[j].velocity.y * DAMPING;
                particles[j].velocity.x = tempVelX * DAMPING;
                particles[j].velocity.y = tempVelY * DAMPING;
            }
        }
    }
}




void initMPU6050() {
    Serial.println("Initializing MPU6050...");
    mpu.initialize();
    
    if (!mpu.testConnection()) {
        Serial.println("MPU6050 connection failed!");
        while (1) {
            delay(100);
        }
    }
    
    mpu.setFullScaleAccelRange(MPU6050_ACCEL_FS_2);
    Serial.println("MPU6050 initialized");
}

void initLEDs() {
    Serial.println("Initializing LEDs...");
    FastLED.addLeds<WS2812B, LED_PIN, GRB>(leds, NUM_LEDS);
    FastLED.setBrightness(BRIGHTNESS);
    FastLED.clear(true);
    Serial.println("LEDs initialized");
}

void initParticles() {
    Serial.println("Initializing particles...");
    int index = 0;
    
    for (int y = MATRIX_HEIGHT - 4; y < MATRIX_HEIGHT; y++) {
        for (int x = 0; x < MATRIX_WIDTH && index < FLUID_PARTICLES; x++) {
            Serial.printf("Initializing particle %d at x=%d, y=%d\n", index, x, y);
            particles[index].position = {static_cast<float>(x), static_cast<float>(y)};
            particles[index].velocity = {0.0f, 0.0f};
            index++;
        }
    }
    
    Serial.printf("Total particles initialized: %d\n", index);
    Serial.println("Particles initialized");
}

void MPUTask(void *parameter) {
    while (true) {
        int16_t ax, ay, az;
        mpu.getAcceleration(&ax, &ay, &az);
        
        portENTER_CRITICAL(&dataMux);
        acceleration.x = -constrain(ax / 16384.0f, -1.0f, 1.0f);
        acceleration.y = constrain(ay / 16384.0f, -1.0f, 1.0f);
        portEXIT_CRITICAL(&dataMux);
        
        vTaskDelay(pdMS_TO_TICKS(10));
    }
}

void LEDTask(void *parameter) {
    TickType_t xLastWakeTime = xTaskGetTickCount();
    const TickType_t xFrequency = pdMS_TO_TICKS(16);
    
    while (true) {
        updateParticles();
        drawParticles();
        vTaskDelayUntil(&xLastWakeTime, xFrequency);
    }
}

void setup() {
    Serial.begin(115200);
    delay(1000);
    Serial.println("Starting initialization...");

    Wire.begin(SDA_PIN, SCL_PIN);
    Wire.setClock(400000);

    initMPU6050();
    initLEDs();
    initParticles();

    xTaskCreatePinnedToCore(
        MPUTask,
        "MPUTask",
        4096,
        NULL,
        2,
        NULL,
        0
    );

    xTaskCreatePinnedToCore(
        LEDTask,
        "LEDTask",
        4096,
        NULL,
        1,
        NULL,
        1
    );

    Serial.println("Setup complete");
}

void loop() {
    vTaskDelete(NULL);
}

Libraries

C/C++
..
No preview (download only).

Credits

Mirko Pavleski
172 projects • 1389 followers
Contact

Comments

Please log in or sign up to comment.