/ OozeMaster3000 / OozeMaster3000.ino
OozeMaster3000.ino
1 // SPDX-FileCopyrightText: 2019 Phillip Burgess for Adafruit Industries 2 // 3 // SPDX-License-Identifier: MIT 4 5 // OOZE MASTER 3000: NeoPixel simulated liquid physics. Up to 7 NeoPixel 6 // strands dribble light, while an 8th strand "catches the drips." 7 // Designed for the Adafruit Feather M0 or M4 with matching version of 8 // NeoPXL8 FeatherWing. This can be adapted for other M0 or M4 boards but 9 // you will need to do your own "pin sudoku" and level shifting 10 // (e.g. NeoPXL8 Friend breakout or similar). 11 // See here: https://learn.adafruit.com/adafruit-neopxl8-featherwing-and-library 12 // Requires Adafruit_NeoPixel, Adafruit_NeoPXL8 and Adafruit_ZeroDMA libraries. 13 14 #include <Adafruit_NeoPXL8.h> 15 16 uint8_t dripColor[] = { 0, 255, 0 }; // Bright green ectoplasm 17 #define PIXEL_PITCH (1.0 / 150.0) // 150 pixels/m 18 #define ICE_BRIGHTNESS 0 // Icycle effect Brightness (0 to <100%) 19 20 #define GAMMA 2.6 21 #define G_CONST 9.806 // Standard acceleration due to gravity 22 // While the above G_CONST is correct for "real time" drips, you can dial it back 23 // for a more theatric effect / to slow down the drips like they've still got a 24 // syrupy "drool string" attached (try much lower values like 2.0 to 3.0). 25 26 // NeoPXL8 pin numbers (these are default connections on NeoPXL8 M0 FeatherWing) 27 int8_t pins[8] = { PIN_SERIAL1_RX, PIN_SERIAL1_TX, MISO, 13, 5, SDA, A4, A3 }; 28 29 // If using an M4 Feather & NeoPXL8 FeatherWing, use these values instead: 30 //int8_t pins[8] = { 13, 12, 11, 10, SCK, 5, 9, 6 }; 31 32 33 typedef enum { 34 MODE_IDLE, 35 MODE_OOZING, 36 MODE_DRIBBLING_1, 37 MODE_DRIBBLING_2, 38 MODE_DRIPPING 39 } dropState; 40 41 struct { 42 uint16_t length; // Length of NeoPixel strip IN PIXELS 43 uint16_t dribblePixel; // Index of pixel where dribble pauses before drop (0 to length-1) 44 float height; // Height IN METERS of dribblePixel above ground 45 dropState mode; // One of the above states (MODE_IDLE, etc.) 46 uint32_t eventStartUsec; // Starting time of current event 47 uint32_t eventDurationUsec; // Duration of current event, in microseconds 48 float eventDurationReal; // Duration of current event, in seconds (float) 49 uint32_t splatStartUsec; // Starting time of most recent "splat" 50 uint32_t splatDurationUsec; // Fade duration of splat 51 float pos; // Position of drip on prior frame 52 } drip[] = { 53 // THIS TABLE CONTAINS INFO FOR UP TO 8 NEOPIXEL DRIPS 54 { 16, 7, 0.157 }, // NeoPXL8 output 0: 16 pixels long, drip pauses at index 7, 0.157 meters above ground 55 { 19, 6, 0.174 }, // NeoPXL8 output 1: 19 pixels long, pause at index 6, 0.174 meters up 56 { 18, 5, 0.195 }, // NeoPXL8 output 2: etc. 57 { 17, 6, 0.16 }, // NeoPXL8 output 3 58 { 16, 1, 0.21 }, // NeoPXL8 output 4 59 { 16, 1, 0.21 }, // NeoPXL8 output 5 60 { 21, 10, 0.143 }, // NeoPXL8 output 6 61 // NeoPXL8 output 7 is normally reserved for ground splats 62 // You CAN add an eighth drip here, but then will not get splats 63 }; 64 65 #define N_DRIPS (sizeof drip / sizeof drip[0]) 66 int longestStrand = (N_DRIPS < 8) ? N_DRIPS : 0; 67 Adafruit_NeoPXL8 *pixels; 68 69 void setup() { 70 Serial.begin(9600); 71 randomSeed(analogRead(A0) + analogRead(A5)); 72 73 for(int i=0; i<N_DRIPS; i++) { 74 drip[i].mode = MODE_IDLE; // Start all drips in idle mode 75 drip[i].eventStartUsec = 0; 76 drip[i].eventDurationUsec = random(500000, 2500000); // Initial idle 0.5-2.5 sec 77 drip[i].eventDurationReal = (float)drip[i].eventDurationUsec / 1000000.0; 78 drip[i].splatStartUsec = 0; 79 drip[i].splatDurationUsec = 0; 80 if(drip[i].length > longestStrand) longestStrand = drip[i].length; 81 } 82 83 pixels = new Adafruit_NeoPXL8(longestStrand, pins, NEO_GRB); 84 pixels->begin(); 85 } 86 87 void loop() { 88 uint32_t t = micros(); // Current time, in microseconds 89 90 float x; // multipurpose interim result 91 pixels->clear(); 92 93 for(int i=0; i<N_DRIPS; i++) { 94 uint32_t dtUsec = t - drip[i].eventStartUsec; // Elapsed time, in microseconds, since start of current event 95 float dtReal = (float)dtUsec / 1000000.0; // Elapsed time, in seconds 96 97 // Handle transitions between drip states (oozing, dribbling, dripping, etc.) 98 if(dtUsec >= drip[i].eventDurationUsec) { // Are we past end of current event? 99 drip[i].eventStartUsec += drip[i].eventDurationUsec; // Yes, next event starts here 100 dtUsec -= drip[i].eventDurationUsec; // We're already this far into next event 101 dtReal = (float)dtUsec / 1000000.0; 102 switch(drip[i].mode) { // Current mode...about to switch to next mode... 103 case MODE_IDLE: 104 drip[i].mode = MODE_OOZING; // Idle to oozing transition 105 drip[i].eventDurationUsec = random(800000, 1200000); // 0.8 to 1.2 sec ooze 106 drip[i].eventDurationReal = (float)drip[i].eventDurationUsec / 1000000.0; 107 break; 108 case MODE_OOZING: 109 if(drip[i].dribblePixel) { // If dribblePixel is nonzero... 110 drip[i].mode = MODE_DRIBBLING_1; // Oozing to dribbling transition 111 drip[i].pos = (float)drip[i].dribblePixel; 112 drip[i].eventDurationUsec = 250000 + drip[i].dribblePixel * random(30000, 40000); 113 drip[i].eventDurationReal = (float)drip[i].eventDurationUsec / 1000000.0; 114 } else { // No dribblePixel... 115 drip[i].pos = (float)drip[i].dribblePixel; // Oozing to dripping transition 116 drip[i].mode = MODE_DRIPPING; 117 drip[i].eventDurationReal = sqrt(drip[i].height * 2.0 / G_CONST); // SCIENCE 118 drip[i].eventDurationUsec = (uint32_t)(drip[i].eventDurationReal * 1000000.0); 119 } 120 break; 121 case MODE_DRIBBLING_1: 122 drip[i].mode = MODE_DRIBBLING_2; // Dripping 1st half to 2nd half transition 123 drip[i].eventDurationUsec = drip[i].eventDurationUsec * 3 / 2; // Second half is 1/3 slower 124 drip[i].eventDurationReal = (float)drip[i].eventDurationUsec / 1000000.0; 125 break; 126 case MODE_DRIBBLING_2: 127 drip[i].mode = MODE_DRIPPING; // Dribbling 2nd half to dripping transition 128 drip[i].pos = (float)drip[i].dribblePixel; 129 drip[i].eventDurationReal = sqrt(drip[i].height * 2.0 / G_CONST); // SCIENCE 130 drip[i].eventDurationUsec = (uint32_t)(drip[i].eventDurationReal * 1000000.0); 131 break; 132 case MODE_DRIPPING: 133 drip[i].mode = MODE_IDLE; // Dripping to idle transition 134 drip[i].eventDurationUsec = random(500000, 1200000); // Idle for 0.5 to 1.2 seconds 135 drip[i].eventDurationReal = (float)drip[i].eventDurationUsec / 1000000.0; 136 drip[i].splatStartUsec = drip[i].eventStartUsec; // Splat starts now! 137 drip[i].splatDurationUsec = random(900000, 1100000); 138 break; 139 } 140 } 141 142 // Render drip state to NeoPixels... 143 #if ICE_BRIGHTNESS > 0 144 // Draw icycles if ICE_BRIGHTNESS is set 145 x = pow((float)ICE_BRIGHTNESS * 0.01, GAMMA); 146 for(int d=0; d<=drip[i].dribblePixel; d++) { 147 set(i, d, x); 148 } 149 #endif 150 switch(drip[i].mode) { 151 case MODE_IDLE: 152 // Do nothing 153 break; 154 case MODE_OOZING: 155 x = dtReal / drip[i].eventDurationReal; // 0.0 to 1.0 over ooze interval 156 x = sqrt(x); // Perceived area increases linearly 157 #if ICE_BRIGHTNESS > 0 158 x = ((float)ICE_BRIGHTNESS * 0.01) + 159 x * (float)(100 - ICE_BRIGHTNESS) * 0.01; 160 #endif 161 x = pow(x, GAMMA); 162 set(i, 0, x); 163 break; 164 case MODE_DRIBBLING_1: 165 // Point b moves from first to second pixel over event time 166 x = dtReal / drip[i].eventDurationReal; // 0.0 to 1.0 during move 167 x = 3 * x * x - 2 * x * x * x; // Easing function: 3*x^2-2*x^3 0.0 to 1.0 168 dripDraw(i, 0.0, x * drip[i].dribblePixel, false); 169 break; 170 case MODE_DRIBBLING_2: 171 // Point a moves from first to second pixel over event time 172 x = dtReal / drip[i].eventDurationReal; // 0.0 to 1.0 during move 173 x = 3 * x * x - 2 * x * x * x; // Easing function: 3*x^2-2*x^3 0.0 to 1.0 174 dripDraw(i, x * drip[i].dribblePixel, drip[i].dribblePixel, false); 175 break; 176 case MODE_DRIPPING: 177 x = 0.5 * G_CONST * dtReal * dtReal; // Position in meters 178 x = drip[i].dribblePixel + x / PIXEL_PITCH; // Position in pixels 179 dripDraw(i, drip[i].pos, x, true); 180 drip[i].pos = x; 181 break; 182 } 183 184 if(N_DRIPS < 8) { // Do splats unless there's an 8th drip defined 185 dtUsec = t - drip[i].splatStartUsec; // Elapsed time, in microseconds, since start of splat 186 if(dtUsec < drip[i].splatDurationUsec) { 187 x = 1.0 - sqrt((float)dtUsec / (float)drip[i].splatDurationUsec); 188 x = pow(x, GAMMA); 189 set(7, i, x); 190 } 191 } 192 } 193 194 pixels->show(); 195 } 196 197 // This "draws" a drip in the NeoPixel buffer...zero to peak brightness 198 // at center and back to zero. Peak brightness diminishes with length, 199 // and drawn dimmer as pixels approach strand length. 200 void dripDraw(uint8_t dNum, float a, float b, bool fade) { 201 if(a > b) { // Sort a,b inputs if needed so a<=b 202 float t = a; 203 a = b; 204 b = t; 205 } 206 // Find range of pixels to draw. If first pixel is off end of strand, 207 // nothing to draw. If last pixel is off end of strand, clip to strand length. 208 int firstPixel = (int)a; 209 if(firstPixel >= drip[dNum].length) return; 210 int lastPixel = (int)b + 1; 211 if(lastPixel >= drip[dNum].length) lastPixel = drip[dNum].length - 1; 212 213 float center = (a + b) * 0.5; // Midpoint of a-to-b 214 float range = center - a + 1.0; // Distance from center to a, plus 1 pixel 215 for(int i=firstPixel; i<= lastPixel; i++) { 216 float x = fabs(center - (float)i); // Pixel distance from center point 217 if(x < range) { // Inside drip 218 x = (range - x) / range; // 0.0 (edge) to 1.0 (center) 219 if(fade) { 220 int dLen = drip[dNum].length - drip[dNum].dribblePixel; // Length of drip 221 if(dLen > 0) { // Scale x by 1.0 at top to 1/3 at bottom of drip 222 int dPixel = i - drip[dNum].dribblePixel; // Pixel position along drip 223 x *= 1.0 - ((float)dPixel / (float)dLen * 0.66); 224 } 225 } 226 } else { 227 x = 0.0; 228 } 229 #if ICE_BRIGHTNESS > 0 230 // Upper pixels may be partially lit for an icycle effect 231 if(i <= drip[dNum].dribblePixel) { 232 // Math because preprocessor doesn't allow float constant in #if. 233 // Optimizer will reduce the math to float constants, it's fine. 234 x = ((float)ICE_BRIGHTNESS * 0.01) + 235 x * (float)(100 - ICE_BRIGHTNESS) * 0.01; 236 } 237 #endif 238 x = pow(x, GAMMA); 239 set(dNum, i, x); 240 } 241 } 242 243 // Set one pixel to a given brightness level (0.0 to 1.0) 244 void set(uint8_t strand, uint8_t pixel, float brightness) { 245 pixels->setPixelColor(pixel + strand * longestStrand, 246 (int)((float)dripColor[0] * brightness + 0.5), 247 (int)((float)dripColor[1] * brightness + 0.5), 248 (int)((float)dripColor[2] * brightness + 0.5)); 249 }