FITI
Creado junto a Ignacio Bustamante y Facundo Spotti para el Taller de Arte Digital 1 (Universidad ORT Uruguay), FITI explora la interacción física con el arte digital. El proyecto evolucionó desde una idea inicial de control de audio hacia un sistema enfocado en la manipulación gráfica. Para evitar problemas de latencia con software externo, se desarrolló un entorno a medida en Processing conectado mediante cámara virtual a Resolume Arena. El flujo de trabajo combinó la programación del firmware en Arduino —donde se solucionaron problemas de drift en el joystick mediante matrices de calibración— con el armado electrónico, la soldadura del circuito y el diseño de una carcasa ergonómica en Blender impresa en 3D. El resultado es una interfaz autónoma e intuitiva que elimina la distancia entre la interacción física y la creación visual en tiempo real.
- FORMATO
- Interactive Experience
- ESTADO
- Completed
- PROPÓSITO
- Experimental
- DIMENSIÓN
- Medium Project
- DISCIPLINAS
- Interactive Design
- TÉCNICAS
- Coding · Scripting · Projection Mapping · Generative Systems
- SOFTWARE
- React · Resolume Arena · Blender · Figma · Arduino
cargando…
La carcasa
El objeto que salió de todo eso
Modelada en Blender e impresa en 3D. La primera versión no encastraba: las medidas y los alojamientos se corrigieron contra la placa real hasta que todo entró adentro.
Se puede girar: los botones y el joystick están en caras distintas, y de frente no se ve.
El proceso
Ideación
El proyecto comenzó utilizando Arduino como base para desarrollar una interfaz física capaz de controlar efectos audiovisuales. Tras explorar distintas posibilidades y encontrar limitaciones de latencia y complejidad en el procesamiento de audio e interacción con otros programas, el enfoque se trasladó hacia una interfaz visual desarrollada íntegramente en Processing.
El resultado funciona como un lienzo digital interactivo, donde potenciómetros permiten modificar parámetros como tamaño y color, alternar entre pintar y borrar, y aplicar efectos visuales mediante Resolume Arena, logrando una experiencia más fluida y controlada.
Desarrollo
El desarrollo del proyecto se dividió en dos etapas. En la primera se trabajó sobre la programación y la interacción entre los componentes físicos y Processing. Arduino IDE se encargó de interpretar la información de los potenciómetros, botones y joystick, mientras que Processing concentró la lógica de interacción y el control de los parámetros visuales.
Durante las primeras pruebas aparecieron algunos problemas de funcionamiento, principalmente con el joystick, que continuaba registrando movimiento incluso cuando se encontraba en posición neutra. Para solucionarlo, se implementó una lógica que permitiera fijar el valor en cero tomando como referencia la última posición válida, logrando una interacción más estable y controlada.
Construcción del dispositivo
Una vez resuelta la interacción, el proyecto pasó de ser un circuito experimental a un dispositivo físico. Se soldaron las conexiones, se reorganizó el cableado y se comenzó a trabajar en una estructura que permitiera integrar todos los componentes.
La carcasa fue modelada en Blender y posteriormente fabricada mediante impresión 3D. La primera versión requirió algunos ajustes en las medidas y encastres, por lo que se realizaron modificaciones hasta conseguir una estructura en la que todas las piezas pudieran integrarse correctamente.
Durante este proceso también aparecieron falsos contactos y desconexiones en el circuito. La solución fue soldar definitivamente las conexiones y reorganizar el cableado interno, mejorando la estabilidad del dispositivo.
Resultado
El resultado final permitió integrar el dispositivo físico con Processing y Resolume, generando una interfaz capaz de controlar en tiempo real la pintura, la manipulación visual y distintos efectos aplicados al video mapping.
De esta manera, el proyecto pasó de ser una exploración de interacción con componentes electrónicos a convertirse en una herramienta de creación visual. El usuario puede intervenir directamente sobre lo que crea y visualiza, además de preestablecer diferentes efectos, reduciendo la distancia entre la acción física y el resultado final.
Una nueva forma de crear
El proyecto busca explorar una forma más directa e intuitiva de creación visual en tiempo real. La interfaz funciona como un puente entre el usuario y la imagen, permitiendo que la acción física se traduzca inmediatamente en una respuesta visual.
El código
Dos programas hablando por el puerto serie
El firmware
Corre en el Arduino. Lee los dos potenciómetros, el joystick y los cuatro botones, escribe el estado en el LCD y lo manda por serie. Acá está la corrección del drift: el joystick seguía marcando movimiento en reposo.
ver el códigoCodigo_Arduino_Controlador.ino · 110 líneas
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
// LCD
LiquidCrystal_I2C lcd(0x27, 16, 2);
// Pines
const int pot1Pin = A0; // Tamaño
const int pot2Pin = A1; // Color
const int joyXPin = A2;
const int joyYPin = A3;
const int button1Pin = 5; // Pintar
const int button2Pin = 6; // Borrar
const int button3Pin = 7; // Resolume 1
const int button4Pin = 8; // Resolume 2
// Variables
int pot1, pot2;
int joyX, joyY;
int b1, b2, b3, b4;
void setup() {
Serial.begin(9600);
// LCD
lcd.init();
lcd.backlight();
lcd.setCursor(0, 0);
lcd.print(" INICIANDO ");
delay(1000);
lcd.clear();
// Botones
pinMode(button1Pin, INPUT_PULLUP);
pinMode(button2Pin, INPUT_PULLUP);
pinMode(button3Pin, INPUT_PULLUP);
pinMode(button4Pin, INPUT_PULLUP);
}
void loop() {
// --- POTENCIÓMETROS ---
pot1 = analogRead(pot1Pin); // Tamaño
pot2 = analogRead(pot2Pin); // Color
// --- JOYSTICK (suavizado simple) ---
joyX = (analogRead(joyXPin) + analogRead(joyXPin)) / 2;
joyY = (analogRead(joyYPin) + analogRead(joyYPin)) / 2;
// --- BOTONES (pull-up invertidos) ---
b1 = !digitalRead(button1Pin);
b2 = !digitalRead(button2Pin);
b3 = !digitalRead(button3Pin);
b4 = !digitalRead(button4Pin);
// --- LCD ---
lcd.setCursor(0, 0);
lcd.print("T:");
lcd.print(map(pot1, 0, 1023, 1, 100));
lcd.print(" C:");
lcd.print(map(pot2, 0, 1023, 0, 255));
lcd.print(" "); // limpia resto
lcd.setCursor(0, 1);
lcd.print("P:");
lcd.print(b1);
lcd.print(" B:");
lcd.print(b2);
lcd.print(" R1:");
lcd.print(b3);
lcd.print(" R2:");
lcd.print(b4);
// --- SERIAL DEBUG LEGIBLE ---
Serial.print("P1: ");
Serial.print(pot1);
Serial.print(" P2: ");
Serial.println(pot2);
Serial.print("JX: ");
Serial.print(joyX);
Serial.print(" JY: ");
Serial.println(joyY);
Serial.print("B1:");
Serial.print(b1);
Serial.print(" B2:");
Serial.print(b2);
Serial.print(" B3:");
Serial.print(b3);
Serial.print(" B4:");
Serial.println(b4);
Serial.println("-------------------");
// --- CSV PARA PROCESSING ---
Serial.print(pot1); Serial.print(",");
Serial.print(pot2); Serial.print(",");
Serial.print(joyX); Serial.print(",");
Serial.print(joyY); Serial.print(",");
Serial.print(b1); Serial.print(",");
Serial.print(b2); Serial.print(",");
Serial.print(b3); Serial.print(",");
Serial.println(b4);
delay(50);
}El lienzo
Corre en Processing. Recibe esos valores y los convierte en pincel: tamaño, color, pintar o borrar. Lo que dibuja sale por cámara virtual hacia Resolume Arena.
ver el códigoProyecto_Taller_Arte_Digital_FITI.pde · 152 líneas
import processing.serial.*;
import java.awt.Robot;
import java.awt.event.KeyEvent;
Serial myPort;
float pcursorX = 640;
float pcursorY = 360;
// Variables del Arduino
int pot1, pot2;
int joyX, joyY;
int b1, b2, b3, b4;
// Cursor del pincel
float cursorX = 640;
float cursorY = 360;
// Offset para joystick
float offsetX = 0;
float offsetY = 0;
// Estado previo del botón de pintar
boolean wasPainting = false;
// Dead zone del joystick
int deadZone = 50;
Robot robot;
boolean prevB3 = false;
boolean prevB4 = false;
void setup() {
size(1280, 720); // 🎥 16:9
background(0);
try {
robot = new Robot();
} catch (Exception e) {
e.printStackTrace();
}
println("Puertos disponibles:");
println(Serial.list());
myPort = new Serial(this, Serial.list()[0], 9600);
myPort.bufferUntil('\n');
noStroke();
colorMode(HSB, 255);
}
void draw() {
float joyXadj = -(joyX - offsetX);
float joyYadj = joyY - offsetY;
float velX = 0;
float velY = 0;
// Intercambio de ejes + inversión correcta
if (abs(joyYadj) > deadZone) velX = map(joyYadj, -512, 511, -5, 5);
if (abs(joyXadj) > deadZone) velY = map(joyXadj, 512, -511, -5, 5);
pcursorX = cursorX;
pcursorY = cursorY;
cursorX += velX;
cursorY += velY;
cursorX = constrain(cursorX, 0, width);
cursorY = constrain(cursorY, 0, height);
// Tamaño y color
float brushSize = map(pot1, 0, 1023, 3, 90);
float hueValue = map(pot2, 0, 1023, 0, 255);
// Pintar
if (b1 == 1) {
fill(hueValue, 255, 255);
noStroke();
ellipse(cursorX, cursorY, brushSize, brushSize);
}
// Borrar
if (b2 == 1) {
fill(0);
noStroke();
ellipse(cursorX, cursorY, brushSize + 5, brushSize + 5);
}
pushStyle();
noStroke();
fill(0);
ellipse(pcursorX, pcursorY, 20, 20);
popStyle();
// Cursor visible
if (b1 == 0) {
noFill();
stroke(0, 0, 255);
strokeWeight(2);
ellipse(cursorX, cursorY, 14, 14);
}
noStroke();
fill(255, 255, 0);
ellipse(cursorX, cursorY, 6, 6);
// Shortcuts Resolume
if (b3 == 1 && !prevB3) {
robot.keyPress(KeyEvent.VK_Q);
robot.keyRelease(KeyEvent.VK_Q);
}
if (b4 == 1 && !prevB4) {
robot.keyPress(KeyEvent.VK_W);
robot.keyRelease(KeyEvent.VK_W);
}
prevB3 = (b3 == 1);
prevB4 = (b4 == 1);
// Actualizar offset al soltar pintar
if (!wasPainting && b1 == 1) {
wasPainting = true;
} else if (wasPainting && b1 == 0) {
offsetX = joyX;
offsetY = joyY;
wasPainting = false;
}
}
// Serial
void serialEvent(Serial p) {
String data = p.readStringUntil('\n');
if (data == null) return;
data = trim(data);
String[] vals = split(data, ',');
if (vals.length == 8) {
pot1 = int(vals[0]);
pot2 = int(vals[1]);
joyX = int(vals[2]);
joyY = int(vals[3]);
b1 = int(vals[4]);
b2 = int(vals[5]);
b3 = int(vals[6]);
b4 = int(vals[7]);
}
}