Skip to content

Commit 85a636a

Browse files
author
jacob toft pedersen
committed
Add metaball blob demo effect
With a python test script using the DDP protocol
1 parent 666e951 commit 85a636a

File tree

4 files changed

+237
-0
lines changed

4 files changed

+237
-0
lines changed

blobs.py

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
#!/usr/bin/env python3
2+
import socket
3+
import numpy as np
4+
import time
5+
import math
6+
7+
# ==============================
8+
# Config
9+
# ==============================
10+
WIDTH, HEIGHT = 16, 16 # Display size
11+
SIM_SCALE = 8 # Simulation resolution multiplier
12+
SIM_W, SIM_H = WIDTH*SIM_SCALE, HEIGHT*SIM_SCALE
13+
14+
NUM_BALLS = 4
15+
RADIUS = 3.0 * SIM_SCALE # Radius in simulation pixels
16+
SPEED = 0.6 * SIM_SCALE # Speed in simulation pixels/frame
17+
GAMMA = 1.7
18+
CAP_VALUE = 3.0 # Max field value before tone-mapping
19+
20+
IP = "192.168.1.112"
21+
PORT = 4048
22+
FPS = 40
23+
24+
# ==============================
25+
# DDP sender
26+
# ==============================
27+
def create_packet(pixels=None):
28+
packet = bytearray([
29+
0x41, # Version 1
30+
0x00, # Flags
31+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
32+
])
33+
data = bytearray([0] * (WIDTH * HEIGHT * 3))
34+
if pixels:
35+
for x, y, brightness in pixels:
36+
if 0 <= x < WIDTH and 0 <= y < HEIGHT and 0 <= brightness <= 255:
37+
idx = (y * WIDTH + x) * 3
38+
data[idx:idx + 3] = [brightness] * 3
39+
packet.extend(data)
40+
return packet
41+
42+
def send_ddp_packet(ip, port, packet):
43+
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
44+
try:
45+
sock.sendto(packet, (ip, port))
46+
finally:
47+
sock.close()
48+
49+
# ==============================
50+
# Metaball simulation
51+
# ==============================
52+
balls = []
53+
for _ in range(NUM_BALLS):
54+
x = np.random.uniform(0, SIM_W)
55+
y = np.random.uniform(0, SIM_H)
56+
vx = np.random.uniform(-SPEED, SPEED)
57+
vy = np.random.uniform(-SPEED, SPEED)
58+
balls.append([x, y, vx, vy])
59+
60+
def attenuation_fn(d, radius=RADIUS):
61+
if d > radius:
62+
return 0.0
63+
return (1 - (d / radius)**2)**2
64+
65+
def tone_map(v, gamma=GAMMA):
66+
v_capped = min(v, CAP_VALUE)
67+
n = v_capped / CAP_VALUE
68+
return int(pow(n, 1 / gamma) * 255)
69+
70+
def update_positions():
71+
for b in balls:
72+
b[0] += b[2]
73+
b[1] += b[3]
74+
if b[0] < 0 or b[0] >= SIM_W:
75+
b[2] *= -1
76+
if b[1] < 0 or b[1] >= SIM_H:
77+
b[3] *= -1
78+
79+
def render_highres():
80+
grid = np.zeros((SIM_H, SIM_W), dtype=float)
81+
for y in range(SIM_H):
82+
for x in range(SIM_W):
83+
val = 0.0
84+
for bx, by, _, _ in balls:
85+
d = math.dist((x, y), (bx, by))
86+
val += attenuation_fn(d)
87+
grid[y, x] = val
88+
return grid
89+
90+
def downsample(grid):
91+
"""Average SIM_SCALE×SIM_SCALE blocks into one pixel"""
92+
pixels = []
93+
for y in range(HEIGHT):
94+
for x in range(WIDTH):
95+
block = grid[
96+
y*SIM_SCALE:(y+1)*SIM_SCALE,
97+
x*SIM_SCALE:(x+1)*SIM_SCALE
98+
]
99+
avg_val = block.mean()
100+
brightness = tone_map(avg_val)
101+
pixels.append((x, y, brightness))
102+
return pixels
103+
104+
# ==============================
105+
# Main loop
106+
# ==============================
107+
def main():
108+
try:
109+
while True:
110+
update_positions()
111+
highres = render_highres()
112+
pixels = downsample(highres)
113+
packet = create_packet(pixels)
114+
send_ddp_packet(IP, PORT, packet)
115+
time.sleep(1 / FPS)
116+
except KeyboardInterrupt:
117+
pass
118+
119+
if __name__ == "__main__":
120+
main()

include/plugins/Blop.h

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
#pragma once
2+
3+
#include "PluginManager.h"
4+
#include <cmath>
5+
#include <cstdlib>
6+
7+
class BlobPlugin : public Plugin
8+
{
9+
public:
10+
static constexpr float aspect_ratio = 1.5f;
11+
static constexpr uint8_t X_MAX = 16;
12+
static constexpr uint8_t Y_MAX = static_cast<uint8_t>(X_MAX * aspect_ratio);
13+
14+
static constexpr uint8_t NUM_BALLS = 5;
15+
16+
BlobPlugin();
17+
virtual ~BlobPlugin() {}
18+
19+
void setup() override;
20+
void loop() override;
21+
const char* getName() const override;
22+
23+
private:
24+
struct Ball {
25+
float x, y;
26+
float vx, vy;
27+
};
28+
29+
Ball balls[NUM_BALLS];
30+
31+
static constexpr float RADIUS = 7.0f;
32+
static constexpr float SPEED = 0.2f;
33+
static constexpr float CAP_VALUE = 3.0f;
34+
static constexpr float GAMMA = 0.7f;
35+
36+
float attenuation(float d, float radius) const;
37+
uint8_t toneMap(float v) const;
38+
void updatePositions();
39+
};

src/main.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
#include "plugins/StarsPlugin.h"
3333
#include "plugins/TickingClockPlugin.h"
3434
#include "plugins/ArtNet.h"
35+
#include "plugins/Blop.h"
3536

3637
#ifdef ENABLE_SERVER
3738
#include "plugins/AnimationPlugin.h"
@@ -166,6 +167,7 @@ void baseSetup()
166167
pluginManager.addPlugin(new CirclePlugin());
167168
pluginManager.addPlugin(new RainPlugin());
168169
pluginManager.addPlugin(new FireworkPlugin());
170+
pluginManager.addPlugin(new BlobPlugin());
169171

170172
#ifdef ENABLE_SERVER
171173
pluginManager.addPlugin(new BigClockPlugin());

src/plugins/Blop.cpp

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
#include "plugins/Blop.h"
2+
#include "screen.h" // Uses global Screen
3+
4+
BlobPlugin::BlobPlugin()
5+
{
6+
// Nothing special here, setup() will handle init
7+
}
8+
9+
void BlobPlugin::setup()
10+
{
11+
Screen.clear();
12+
13+
for (auto &b : balls) {
14+
b.x = static_cast<float>(rand() % X_MAX);
15+
b.y = static_cast<float>(rand() % Y_MAX);
16+
b.vx = ((rand() % 200) / 100.0f - 1.0f) * SPEED * 2;
17+
b.vy = ((rand() % 200) / 100.0f - 1.0f) * SPEED * 2;
18+
}
19+
}
20+
21+
void BlobPlugin::loop()
22+
{
23+
Screen.clear();
24+
25+
for (uint8_t y = 0; y < ROWS; ++y) {
26+
for (uint8_t x = 0; x < COLS; ++x) {
27+
float value = 0.0f;
28+
29+
for (const auto &b : balls) {
30+
float dx = b.x - static_cast<float>(x);
31+
float dy = b.y - static_cast<float>(y*aspect_ratio);
32+
float dist = std::sqrt(dx * dx + dy * dy);
33+
value += attenuation(dist, RADIUS);
34+
}
35+
36+
uint8_t brightness = toneMap(value);
37+
Screen.setPixel(x, y, 1, brightness);
38+
}
39+
}
40+
41+
updatePositions();
42+
delay(50); // ~20 FPS
43+
}
44+
45+
const char* BlobPlugin::getName() const
46+
{
47+
return "Blobs";
48+
}
49+
50+
float BlobPlugin::attenuation(float d, float radius) const
51+
{
52+
if (d > radius) return 0.0f;
53+
float ratio = d / radius;
54+
return (1.0f - ratio * ratio) * (1.0f - ratio * ratio);
55+
}
56+
57+
uint8_t BlobPlugin::toneMap(float v) const
58+
{
59+
if (v > CAP_VALUE) v = CAP_VALUE;
60+
float n = v / CAP_VALUE;
61+
float corrected = std::pow(n, 1.0f / GAMMA);
62+
return static_cast<uint8_t>(corrected * 255.0f);
63+
}
64+
65+
void BlobPlugin::updatePositions()
66+
{
67+
for (auto &b : balls) {
68+
b.x += b.vx;
69+
b.y += b.vy;
70+
71+
if (b.x < 0) { b.x = 0; b.vx *= -1; }
72+
if (b.x >= X_MAX) { b.x = X_MAX - 1; b.vx *= -1; }
73+
if (b.y < 0) { b.y = 0; b.vy *= -1; }
74+
if (b.y >= Y_MAX) { b.y = Y_MAX - 1; b.vy *= -1; }
75+
}
76+
}

0 commit comments

Comments
 (0)