매트릭스 조립 과정을 시작하기 전에 아래의 메인 코드를 라즈베리 파이 피코에 업로드했습니다.
#include <Adafruit_Protomatter.h>
#include <math.h>
// ── Pins (Raspberry Pi Pico GPIO) ─────────────────────────────────
#define R1 2
#define G1 3
#define B1 4
#define R2 5
#define G2 6
#define B2 9
#define PIN_A 10
#define PIN_B 16
#define PIN_C 18
#define PIN_D 20
#define CLK 11
#define LAT 12
#define OE 13
// ── Buttons ───────────────────────────────────────────────────────
#define BTN_PREV 0
#define BTN_NEXT 14
#define BTN_WAVE 1
// ── Resolution for TWO Chained 64x32 Panels (64x64 total) ─────────
#define W 64
#define H 64
uint8_t rgbPins[] = { R1, G1, B1, R2, G2, B2 };
uint8_t addrPins[] = { PIN_A, PIN_B, PIN_C, PIN_D };
Adafruit_Protomatter matrix(
W, 3, 1, rgbPins, 4, addrPins, CLK, LAT, OE, true, 2
);
// ── Global Variables & Timing ─────────────────────────────────────
uint32_t frameCount = 0;
int currentAnim = 0;
const int NUM_ANIMS = 9;
unsigned long previousMillis = 0;
const int frameInterval = 16;
bool lastBtnPrev = HIGH;
bool lastBtnNext = HIGH;
bool lastBtnWave = HIGH;
unsigned long lastDebounceTime = 0;
const unsigned long debounceDelay = 50;
// ── Audio / Waveform Settings ─────────────────────────────────────
const int MIC_PIN = 28; // Using GPIO 28 (ADC2)
const int OVERSAMP = 128;
const int X_STEP = 2;
const int NUM_POINTS = W / X_STEP;
const float MAX_WAVE_FRACTION = 0.5f;
float rmsPeak = 200.0f;
float loudSmooth = 0.0f;
bool noiseInit = false;
float noiseFloor = 100.0f;
const float LOUD_GATE = 0.08f;
float waveY[NUM_POINTS];
bool waveMode = false;
inline uint16_t rgb(uint8_t r, uint8_t g, uint8_t b) {
return matrix.color565(r, g, b);
}
// ══════════════════════════════════════════════════════════════════
// ANIMATION 0: The Original Cyber-Ghost
// ══════════════════════════════════════════════════════════════════
void drawPacGhost() {
matrix.fillScreen(0);
float t = frameCount * 0.05f;
uint8_t wheelPos = (frameCount >> 1) & 0xFF;
uint8_t r, g, b;
if (wheelPos < 85) { r = 255 - wheelPos * 3; g = 0; b = wheelPos * 3; }
else if (wheelPos < 170) { wheelPos -= 85; r = 0; g = wheelPos * 3; b = 255 - wheelPos * 3; }
else { wheelPos -= 170; r = wheelPos * 3; g = 255 - wheelPos * 3; b = 0; }
uint16_t GHOST_COLOR = rgb(r, g, b);
float base_cx = 32.0f + sinf(t * 1.2f) * 16.0f;
float hover_y = sinf(t * 3.0f) * 2.5f;
for (int y = 8; y < 58; y++) {
for (int x = base_cx - 18; x < base_cx + 18; x++) {
if(x < 0 || x >= W || y < 0 || y >= H) continue;
float relX = x - base_cx; float relY = (y - 12) + hover_y;
bool drawPixel = false;
if (relY < 15.0f) {
if (sqrtf(relX*relX + (relY - 15.0f)*(relY - 15.0f)) < 15.0f) drawPixel = true;
} else if (relY >= 15.0f && relY < 39.0f) {
if (relX >= -15.0f && relX <= 15.0f) drawPixel = true;
} else if (relY >= 39.0f) {
float footRipple = 2.5f * sinf(relX * 0.7f - t * 4.0f);
if (relY < (43.0f + footRipple) && relX >= -15.0f && relX <= 15.0f) drawPixel = true;
}
if (drawPixel) matrix.drawPixel(x, y, GHOST_COLOR);
}
}
float lookX = sinf(t * 1.2f) * 3.0f; float lookY = cosf(t * 1.5f) * 1.5f;
auto drawEye = [&](float ex, float ey) {
for (int y = ey - 5; y < ey + 5; y++) {
for (int x = ex - 3; x < ex + 3; x++) {
if(x < 0 || x >= W || y < 0 || y >= H) continue;
float dx = x - ex; float dy = y - (ey + hover_y);
if((dx*dx / 9.0f) + (dy*dy / 25.0f) < 1.0f) matrix.drawPixel(x, y, rgb(240, 240, 240));
}
}
matrix.fillRect(ex + lookX - 1, (ey + hover_y) + lookY - 1, 2, 2, rgb(0, 50, 255));
};
drawEye(base_cx - 6.0f, 22.0f); drawEye(base_cx + 6.0f, 22.0f);
}
// ══════════════════════════════════════════════════════════════════
// ANIMATION 1: Cyberpunk 2077 Relic Glitch
// ══════════════════════════════════════════════════════════════════
void drawCyberpunkRelic() {
matrix.fillScreen(rgb(10, 0, 15)); // Dark corporate background
uint16_t neonCyan = rgb(0, 255, 255);
uint16_t neonYellow = rgb(255, 240, 0);
uint16_t neonRed = rgb(255, 0, 50);
// Intentional X-axis tearing/glitching for the entire logo
int glitchX = (random(100) > 85) ? random(-6, 6) : 0;
// Base coordinates for the logo
int x = 16 + glitchX;
int y = 8;
// ── Draw the Custom Relic Logo Geometry ──
// Outer Frame
matrix.fillRect(x, y, 32, 6, neonRed); // Top horizontal
matrix.fillRect(x + 26, y, 6, 32, neonRed); // Right vertical
matrix.fillRect(x + 14, y + 26, 18, 6, neonRed); // Bottom horizontal (partial)
matrix.fillRect(x, y, 6, 22, neonRed); // Left vertical
// Inner 'R' Mechanism
matrix.fillRect(x, y + 12, 16, 6, neonRed); // Middle horizontal
matrix.fillRect(x + 14, y + 12, 6, 9, neonRed); // Inner right vertical
// The iconic 45-degree angle cut
matrix.fillRect(x, y + 22, 10, 6, neonRed); // Bottom-left inward turn
for(int i = 0; i < 6; i++) {
matrix.drawLine(x + 8 + i, y + 27, x + 16 + i, y + 19, neonRed); // Thick diagonal line
}
// Draw "RELIC" Text below the logo
matrix.setCursor(17 + glitchX, 44);
matrix.setTextColor(neonRed);
matrix.print("RELIC");
// Glitching core data block inside the logo
matrix.fillRect(x + 12, y + 16, 4, 4, neonYellow);
// ── Glitch & Artifact Effects ──
// Digital barcode / hacking lines cutting across
for(int i = 0; i < 7; i++) {
int lineY = random(0, 64);
matrix.drawFastHLine(random(0, 32), lineY, random(10, 45), neonCyan);
}
// Occasional full-screen EMP tear (Yellow flash with a black void)
if (random(100) > 92) {
int tearY = random(10, 50);
matrix.fillRect(0, tearY, 64, random(2, 6), neonYellow);
matrix.fillRect(0, tearY + 2, 64, random(2, 5), 0); // Black tear slicing the screen
}
}
// ══════════════════════════════════════════════════════════════════
// ANIMATION 2: Retro Doom Fire (Integer Cellular Automata)
// ══════════════════════════════════════════════════════════════════
uint8_t firePixels[W * (H + 1)]; // 1D array for speed
void drawRetroFire() {
// Feed the bottom row with random bright intensity
for (int x = 0; x < W; x++) {
firePixels[(H - 1) * W + x] = random(160, 255);
}
// Propagate upwards
for (int y = 0; y < H - 1; y++) {
for (int x = 0; x < W; x++) {
int src = (y + 1) * W + x;
int decay = random(0, 3);
int dstX = x - decay + 1;
if (dstX < 0) dstX = 0; if (dstX > W - 1) dstX = W - 1;
int dst = y * W + dstX;
int val = firePixels[src] - decay * 2;
firePixels[dst] = (val > 0) ? val : 0;
// Fast color mapping (Black -> Red -> Orange -> Yellow -> White)
uint8_t intensity = firePixels[dst];
uint8_t r = (intensity > 128) ? 255 : intensity * 2;
uint8_t g = (intensity > 128) ? (intensity - 128) * 2 : 0;
uint8_t b = (intensity > 192) ? (intensity - 192) * 4 : 0;
matrix.drawPixel(x, y, rgb(r, g, b));
}
}
}
// ══════════════════════════════════════════════════════════════════
// ANIMATION 3: XOR Cyber-Fractal (Fast Integer Hypnosis)
// ══════════════════════════════════════════════════════════════════
void drawXORFractal() {
matrix.fillScreen(0);
uint16_t t = frameCount * 2;
for (int y = 0; y < H; y++) {
for (int x = 0; x < W; x++) {
// Pure mathematical bitwise alien pattern
uint8_t c = ((x * 4) ^ (y * 4)) - t;
uint8_t r = (c * 2) % 255;
uint8_t g = (c * 4) % 255;
uint8_t b = 255 - c;
matrix.drawPixel(x, y, rgb(r, g, b));
}
}
}
// ══════════════════════════════════════════════════════════════════
// ANIMATION 4: Synthwave Outrun Grid
// ══════════════════════════════════════════════════════════════════
void drawSynthwave() {
matrix.fillScreen(rgb(5, 0, 15)); // Dark retro-purple sky
// 1. Synthwave Sun (with scrolling scanline cutouts)
int sunY = 24;
for (int y = 10; y < 38; y++) {
int slice = (y - (frameCount >> 1)) % 6;
if (y > 24 && slice < 2) continue; // Scanline gaps
int dy = y - sunY;
if (dy * dy <= 196) { // Circle radius 14 (14^2 = 196)
int dx = sqrt(196 - dy * dy);
uint8_t g = (38 - y) * 7; // Gradient: Yellow top, Red bottom
matrix.drawFastHLine(32 - dx, y, dx * 2, rgb(255, g, 0));
}
}
// 2. Glowing Cyan Horizon
matrix.drawFastHLine(0, 38, W, rgb(0, 255, 255));
// 3. Perspective Grid (Floor)
// Radiating vertical lines
for (int x = -100; x < 164; x += 18) {
matrix.drawLine(32, 38, x, 63, rgb(200, 0, 255));
}
// Moving horizontal perspective lines (speed controlled by frameCount offset)
int offset = (frameCount >> 1) % 6;
for (int y = 39; y < H; y++) {
int depth = y - 38;
if ((depth + offset) % (depth / 3 + 2) == 0) {
matrix.drawFastHLine(0, y, W, rgb(255, 0, 255));
}
}
}
// ══════════════════════════════════════════════════════════════════
// ANIMATION 5: Neon Qix Trails (Mystify)
// ══════════════════════════════════════════════════════════════════
static float qx[4] = {10, 54, 10, 54};
static float qy[4] = {10, 10, 54, 54};
static float qvx[4] = {1.5, -1.2, 1.3, -1.6};
static float qvy[4] = {1.2, 1.7, -1.1, 1.4};
#define TAIL 8
static int hx[4][TAIL] = {0};
static int hy[4][TAIL] = {0};
void drawNeonQix() {
matrix.fillScreen(0);
// Shift trail history
for(int i = 0; i < 4; i++) {
for(int t = TAIL - 1; t > 0; t--) {
hx[i][t] = hx[i][t-1];
hy[i][t] = hy[i][t-1];
}
hx[i][0] = (int)qx[i];
hy[i][0] = (int)qy[i];
}
// Move head points and bounce off edges
for(int i = 0; i < 4; i++) {
qx[i] += qvx[i];
qy[i] += qvy[i];
if(qx[i] <= 0 || qx[i] >= W - 1) qvx[i] = -qvx[i];
if(qy[i] <= 0 || qy[i] >= H - 1) qvy[i] = -qvy[i];
}
// Draw fading geometric connecting lines
for(int t = 0; t < TAIL; t++) {
if (hx[0][t] == 0 && hy[0][t] == 0) continue; // Skip first frame
uint8_t fade = 255 - (t * (255 / TAIL));
uint16_t c1 = rgb(fade, 0, fade / 2); // Neon Pink
uint16_t c2 = rgb(0, fade, fade); // Neon Cyan
matrix.drawLine(hx[0][t], hy[0][t], hx[1][t], hy[1][t], c1);
matrix.drawLine(hx[1][t], hy[1][t], hx[2][t], hy[2][t], c2);
matrix.drawLine(hx[2][t], hy[2][t], hx[3][t], hy[3][t], c1);
matrix.drawLine(hx[3][t], hy[3][t], hx[0][t], hy[0][t], c2);
}
}
// ══════════════════════════════════════════════════════════════════
// ANIMATION 6: Pikachu "Pika Pika!"
// ══════════════════════════════════════════════════════════════════
void drawPikachu() {
matrix.fillScreen(rgb(50, 150, 255)); // Bright Pokémon-sky blue background
// "Pika Pika" speech cadence: open mouth twice quickly, then pause
int talkCycle = frameCount % 60;
bool mouthOpen = (talkCycle > 0 && talkCycle < 10) || (talkCycle > 15 && talkCycle < 25);
// Head bobs down slightly when talking to give it energy
int hover = (mouthOpen) ? 2 : 0;
int cx = 32;
int cy = 34 + hover;
uint16_t yellow = rgb(255, 235, 20);
uint16_t black = rgb(0, 0, 0);
uint16_t red = rgb(255, 30, 30);
uint16_t white = rgb(255, 255, 255);
uint16_t tongue = rgb(255, 100, 100);
// ── 1. Outlines & Black Ear Tips ──
// We draw slightly larger black shapes in the background to act as thick 8-bit outlines!
matrix.fillTriangle(cx - 10, cy - 8, cx - 2, cy - 18, cx - 28, cy - 32, black); // Left Ear
matrix.fillTriangle(cx + 10, cy - 8, cx + 2, cy - 18, cx + 28, cy - 32, black); // Right Ear
matrix.fillRoundRect(cx - 23, cy - 15, 46, 32, 15, black); // Head Outline
// ── 2. Yellow Body ──
// The yellow triangles stop shorter than the black ones to naturally create the black ear tips!
matrix.fillTriangle(cx - 10, cy - 8, cx - 4, cy - 16, cx - 18, cy - 20, yellow); // Left Ear
matrix.fillTriangle(cx + 10, cy - 8, cx + 4, cy - 16, cx + 18, cy - 20, yellow); // Right Ear
matrix.fillRoundRect(cx - 21, cy - 14, 42, 30, 14, yellow); // Head
// ── 3. Face Details ──
// Cheeks
matrix.fillCircle(cx - 15, cy + 5, 5, red);
matrix.fillCircle(cx + 15, cy + 5, 5, red);
// Eyes (Black base, white glint)
matrix.fillCircle(cx - 9, cy - 2, 4, black);
matrix.fillCircle(cx + 9, cy - 2, 4, black);
matrix.fillCircle(cx - 10, cy - 3, 1, white);
matrix.fillCircle(cx + 8, cy - 3, 1, white);
// Nose
matrix.fillRect(cx - 1, cy + 2, 3, 2, black);
// ── 4. Animated Mouth ──
if (mouthOpen) {
// Open Mouth "Pika!"
matrix.fillCircle(cx, cy + 7, 4, black);
matrix.fillCircle(cx, cy + 8, 2, tongue); // Red tongue
// Hide top half of the circle with yellow to make it a 'D' shape
matrix.fillRect(cx - 5, cy + 3, 10, 4, yellow);
} else {
// Closed Mouth 'w' shape
// Left curve
matrix.drawLine(cx - 4, cy + 5, cx - 2, cy + 7, black);
matrix.drawLine(cx - 2, cy + 7, cx, cy + 5, black);
// Right curve
matrix.drawLine(cx, cy + 5, cx + 2, cy + 7, black);
matrix.drawLine(cx + 2, cy + 7, cx + 4, cy + 5, black);
}
}
// ══════════════════════════════════════════════════════════════════
// ANIMATION 7: Exact 8-Bit Mario Block Jump
// ══════════════════════════════════════════════════════════════════
// Perfect 16x16 Pixel Map of the "?" Block
const char* blockSprite[16] = {
"KKKKKKKKKKKKKKKK",
"KYYYYYYYYYYYYYDK",
"KYYYYYYYYYYYYYDK",
"KYYYKKKKKKYYYYDK",
"KYYYKWWWWKYYYYDK",
"KYYKWWKKWWKYYYDK",
"KYYKWKYYKWWKKYDK",
"KYYYKKYYKWWKKYDK",
"KYYYYYYKWWKKYDKK",
"KYYYYYYKWWKKYDKK",
"KYYYYYYYKKYYYYDK",
"KYYYYYKWWKYYYYDK",
"KYYYYYKWWKYYYYDK",
"KYYYYYYKKYYYYYDK",
"KDDDDDDDDDDDDDDK",
"KKKKKKKKKKKKKKKK"
};
// Perfect 16x16 Pixel Map of Mario (Modern 8-Bit Style)
const char* marioSprite[16] = {
".....KKKKK......",
"....KRRRRRK.....",
"...KRRRRRRRRK...",
"...KBBBSKSK.K...",
"..KBSBSSSBSSSK..",
"..KBSBSSSSSSSK..",
"..KBBSSSSSSBBK..",
"...KSSSSSSS.K...",
"..KKKRLRRRKKK...",
".KSSKLLRLLRKSSK.",
".KSSKLLRLLRKSSK.",
".KSSKLLYLLRKSSK.",
"..KKKLLLLLLKKK..",
"...KKLLLLLLKK...",
"..KBBBK..KBBBK..",
".KBBBBK..KBBBBK."
};
// Extremely fast sprite renderer reading the character map
void drawSprite(int ox, int oy, const char* sprite[], int scale) {
for (int y = 0; y < 16; y++) {
for (int x = 0; x < 16; x++) {
char c = sprite[y][x];
if (c == '.') continue; // Skip transparency
uint16_t color = 0;
if (c == 'K') color = rgb(0,0,0); // Black Outline
else if (c == 'Y') color = rgb(255,255,0); // Yellow
else if (c == 'D') color = rgb(200,120,0); // Dark Orange Shading
else if (c == 'W') color = rgb(255,255,255); // White
else if (c == 'R') color = rgb(255,0,0); // Mario Red
else if (c == 'B') color = rgb(120,60,0); // Mario Brown
else if (c == 'S') color = rgb(255,200,140); // Mario Skin
else if (c == 'L') color = rgb(0,80,255); // Mario Blue
matrix.fillRect(ox + (x * scale), oy + (y * scale), scale, scale, color);
}
}
}
void drawMarioBlock() {
matrix.fillScreen(rgb(100, 180, 255)); // Super Mario Sky Blue
// Ground Base
matrix.fillRect(0, 56, 64, 8, rgb(200, 76, 12)); // Brick Red Ground
matrix.drawFastHLine(0, 56, 64, rgb(0,0,0)); // Ground Outline
int cycle = frameCount % 80;
int mY = 24; // Base Mario Y (Resting on the ground)
int bY = 8; // Base Block Y
// Mario Jump Physics (Sine Wave)
if (cycle >= 10 && cycle <= 40) {
float t = (cycle - 10) / 30.0f; // Arc from 0.0 to 1.0
mY = 24 - (int)(sinf(t * 3.14159f) * 16.0f); // 16px high jump
}
// Block Bump Collision
if (cycle >= 23 && cycle <= 27) {
bY = 4; // Block jumps up when hit by Mario's head!
}
// Coin Pop-up!
if (cycle >= 24 && cycle <= 36) {
float ct = (cycle - 24) / 12.0f;
int cY = 8 - (int)(sinf(ct * 3.14159f) * 16.0f);
matrix.fillRect(28, cY, 8, 12, rgb(255, 255, 0)); // Coin Body
matrix.drawRect(28, cY, 8, 12, rgb(200, 120, 0)); // Coin Outline
}
// Draw the entities (Scale = 2 makes them nice and chunky!)
drawSprite(16, bY, blockSprite, 2);
drawSprite(16, mY, marioSprite, 2);
}
// ══════════════════════════════════════════════════════════════════
// ANIMATION 8: The Matrix Digital Rain
// ══════════════════════════════════════════════════════════════════
int rainY[W];
void drawMatrixRain() {
// Setup arrays on the very first frame of the animation
if (frameCount == 0) {
for(int i=0; i<W; i++) rainY[i] = random(-60, 0);
}
matrix.fillScreen(0);
for (int x = 0; x < W; x++) {
rainY[x] += random(1, 3); // Fall speed
if (rainY[x] > H + 15) rainY[x] = random(-20, 0);
// Draw trail
for (int t = 0; t < 15; t++) {
int y = rainY[x] - t;
if (y >= 0 && y < H) {
if (t == 0) matrix.drawPixel(x, y, rgb(200, 255, 200)); // Bright head
else matrix.drawPixel(x, y, rgb(0, 255 - (t * 15), 0)); // Fading tail
}
}
}
}
// ══════════════════════════════════════════════════════════════════
// MODE: Audio Reactive Waveform (Standalone)
// ══════════════════════════════════════════════════════════════════
void drawWaveform() {
static float samples[OVERSAMP];
uint32_t sum = 0;
for (int i = 0; i < OVERSAMP; i++) {
int v = analogRead(MIC_PIN);
samples[i] = (float)v;
sum += v;
delayMicroseconds(100); // Slight speed up for better sampling
}
// DEBUG: Uncomment this line to check values in Serial Monitor (115200)
// Serial.println(sum / OVERSAMP);
float dc = (float)sum / (float)OVERSAMP;
float rmsSum = 0.0f;
for (int i = 0; i < OVERSAMP; i++) {
float centered = samples[i] - dc;
rmsSum += centered * centered;
}
float rms = sqrtf(rmsSum / (float)OVERSAMP);
if (!noiseInit) { noiseFloor = rms; noiseInit = true; }
noiseFloor = (0.95f * noiseFloor) + (0.05f * rms);
float rmsEffective = rms - (noiseFloor * 1.2f); // Slightly higher gate
if (rmsEffective < 0.0f) rmsEffective = 0.0f;
rmsPeak = (0.99f * rmsPeak) + (0.01f * rmsEffective);
float loud = (rmsPeak > 5.0f) ? (rmsEffective / rmsPeak) : 0.0f;
loudSmooth = (0.7f * loudSmooth) + (0.3f * loud);
// Build points
for (int i = 0; i < NUM_POINTS; i++) {
float s = samples[i * (OVERSAMP / NUM_POINTS)];
float amp = loudSmooth * (H / 2.0f);
float y = (H / 2.0f) - ((s - dc) * amp / 500.0f);
if (y < 0) y = 0; if (y >= H) y = H - 1;
waveY[i] = y;
}
uint16_t waveColor = rgb(0, 255, 0); // Solid green wave
for (int i = 1; i < NUM_POINTS; i++) {
matrix.drawLine((i - 1) * X_STEP, (int)waveY[i - 1], i * X_STEP, (int)waveY[i], waveColor);
}
}
void setup() {
Serial.begin(115200);
analogReadResolution(12);
pinMode(BTN_PREV, INPUT_PULLUP);
pinMode(BTN_NEXT, INPUT_PULLUP);
pinMode(BTN_WAVE, INPUT_PULLUP);
matrix.begin();
}
void handleButtons() {
unsigned long now = millis();
bool rN = digitalRead(BTN_NEXT), rP = digitalRead(BTN_PREV), rW = digitalRead(BTN_WAVE);
if (now - lastDebounceTime > debounceDelay) {
if (rN != lastBtnNext && rN == LOW) { currentAnim = (currentAnim + 1) % NUM_ANIMS; frameCount = 0; lastDebounceTime = now; }
if (rP != lastBtnPrev && rP == LOW) { currentAnim = (currentAnim - 1 + NUM_ANIMS) % NUM_ANIMS; frameCount = 0; lastDebounceTime = now; }
if (rW != lastBtnWave && rW == LOW) { waveMode = !waveMode; lastDebounceTime = now; }
}
lastBtnNext = rN; lastBtnPrev = rP; lastBtnWave = rW;
}
void loop() {
handleButtons();
if (millis() - previousMillis >= frameInterval) {
previousMillis = millis();
matrix.fillScreen(0);
if (waveMode) {
drawWaveform();
} else {
switch (currentAnim) {
case 0: drawPacGhost(); break; case 1: drawCyberpunkRelic(); break; case 2: drawRetroFire(); break;
case 3: drawXORFractal(); break; case 4: drawSynthwave(); break; case 5: drawNeonQix(); break;
case 6: drawPikachu(); break; case 7: drawMarioBlock(); break; case 8: drawMatrixRain(); break;
}
}
matrix.show(); frameCount++;
}
}
그리고 이 프로젝트의 최종 결과물인 ‘PROJECT GESTALT’를 소개합니다. 이 프로젝트는 애니메이션, 게임에서 영감을 받은 비주얼, 소리에 반응하는 WaveForm 모드를 모두 결합한 완전한 기능을 갖춘 RGB 매트릭스 디스플레이로, ‘NieR: Automata’의 ‘Pods’에서 영감을 받은 맞춤형 케이스 안에 담겨 있습니다.
기기를 켜려면 메인 전원 버튼을 누르면 되며, 그러면 시스템이 전원이 켜지고 디스플레이가 초기화됩니다.
'이전' 및 '다음' 버튼을 사용하면 기기에 저장된 다양한 애니메이션을 순차적으로 볼 수 있습니다. '다음' 버튼을 누르면 다음 애니메이션으로 이동하고, '이전' 버튼을 누르면 이전 애니메이션으로 돌아갑니다.
전용 웨이브폼 모드 버튼을 누르면 기기가 소리에 반응하는 모드로 전환됩니다. 이 모드에서는 마이크가 주변 소리의 크기를 감지하여, 이를 RGB 매트릭스 상의 애니메이션 파형으로 실시간으로 시각화합니다. 주변 소리가 커지면 파형이 확장되며 이에 따라 반응합니다.
Project Gestalt에는 현재 9개의 맞춤형 애니메이션이 포함되어 있으며, 각 애니메이션은 라즈베리 파이 피코에서 원활하게 실행될 수 있도록 절차적 그래픽과 경량 코드를 사용하여 제작되었습니다.