Demo En desarrollo

Sombra Predador

Demo interactivo: una cámara con Python segmenta tu silueta y la convierte en depredador para 350 boids que huyen en Unity en tiempo real.

Publicado: · Por Juanjo "Banyo" López

Tu silueta es el depredador. Trescientos cincuenta boids huyen en Unity mientras una cámara con Python segmenta tu cuerpo en tiempo real y lo transmite al juego como un campo de peligro dinámico.

El reto

Quería un demo para presentar en vivo que cumpliera tres cosas:

  1. Que se entendiera en cinco segundos sin explicación verbal.
  2. Que combinara varias técnicas de IA aplicada trabajando juntas, no aisladas.
  3. Que el espectador interactuara sin tocar nada — solo pararse frente a la cámara.

La idea cayó rápido: si la silueta de la persona fuera un depredador y los agentes en pantalla tuvieran que escapar, el demo se explica solo. La parte difícil es que se sienta vivo: no basta con que huyan, tiene que parecer una manada real reaccionando a una amenaza real.

Cómo funciona

Dos procesos separados conectados por ZeroMQ sobre TCP localhost:

Flujo de datos De webcam a fuerza de huida en menos de 4 ms — un solo frame end-to-end.
  1. Hardware
    Webcam Captura RGB a 30 fps. 30 fps
  2. Python
    OpenCV + MediaPipe Segmentación de silueta y distance transform. 160 × 90
  3. Transporte
    ZeroMQ PUB Publica el campo escalar como buffer binario. tcp://*:5555 · CONFLATE
  4. Unity
    NetMQ SUB Hilo background lee sin bloquear el render. HWM = 1
  5. Unity
    Texture2D global El buffer se sube como textura accesible a todos los boids. _DangerField
  6. Unity
    Boids Sample del campo, gradiente y fuerza repulsiva integrada al steer. 350 agentes

Lado Python.

  1. Captura la webcam a 30fps.
  2. Segmenta la silueta con MediaPipe Selfie Segmentation (modelo landscape, ~250 KB)
  3. Calcula un distance transform sobre la máscara invertida. El resultado es un campo escalar 160×90 donde cada celda representa la distancia al borde más cercano de la silueta.
  4. Publica el campo escalar como un buffer de 14,400 bytes/frame.

Lado Unity.

  1. Un hilo background lee el buffer ZMQ y lo carga en una Texture2D global _DangerField.
  2. Los boids leen ese campo cada frame:
  • Convierten su posición de mundo a UV (0..1)
  • Leen el valor central y los cuatro vecinos.
  • Calculan el gradiente y aplican una fuerza repulsiva proporcional.
  1. Todo el flocking — separación, alineación, cohesión — y la fuerza de huida se combinan en un único steer integrado por dt.

¿Por qué dos procesos y no todo en Unity?

MediaPipe da segmentación humana de calidad sin entrenar nada, y la iteración en Python es mucho más rápida. La latencia ZMQ localhost es 1–2 ms con la flag CONFLATE activa, que descarta frames viejos automáticamente. Cero acumulación de lag.

Stack técnico

Las piezas concretas que sostienen el flujo de arriba. Agrupadas por runtime:

Python

Python 3.10–3.12

opencv-python
Captura de webcam y distance transform sobre la máscara invertida.
mediapipe
Segmentación con selfie_segmenter_landscape.tflite (~250 KB).
pyzmq
Socket PUB con flag CONFLATE para descartar frames viejos.
numpy
Construcción de la máscara y serialización a buffer binario de 14,400 bytes.
Unity

Unity 2022.3 LTS · URP 2D

NetMQ
Port C# de ZeroMQ. Hilo SUB en background para no bloquear el render.
AsyncIO dep. de NetMQ
NaCl dep. de NetMQ
NuGetForUnity solo dev
Instalación de NetMQ y sus dependencias dentro del proyecto.
URP 2D Volume + Bloom
HDR > 1.0 por grupo hace el glow visible que vende el demo en vivo.

Tutoriales aplicados

Cada uno aporta una pieza concreta. Si quieres recrearlo, este es el orden recomendado:

Lo que aprendí haciéndolo

El gradiente del distance transform es cero adentro de la silueta.

Bug

Mi primera versión tenía un bug raro: cuando un boid wrappeaba toroidalmente y aparecía dentro del área de la silueta, no escapaba. Caminaba en línea recta hasta salir y volver a entrar. El motivo: el distanceTransform da valor 0 a todas las celdas dentro de la máscara, así que el gradiente local entre vecinos también es 0.

La solución fue agregar un probe-search en Unity: cuando el gradiente local es cero pero el boid está en zona de peligro, sondea píxeles a distancias 4, 10, 22 y 50 en 8 direcciones. Toma la dirección del píxel más “seguro” encontrado. Cubre desde silueta pequeña hasta cubriendo casi todo el campo.

Pool de visuales en lugar de Destroy/Create.

Performance

Cada boid es un GameObject con SpriteRenderer. El primer prototipo recreaba GameObjects al cambiar el slider de cantidad de agentes en runtime, y se notaba un freeze de medio segundo cada vez.

Migré a Stack<Transform> como pool: al bajar el slider los GameObjects pasan a SetActive(false) y se quedan en el pool; al subir, se sacan del pool en lugar de crear nuevos. Cero hitches, cero alocaciones después del primer pico.

APIs que cambian sin avisar.

Dependencias

MediaPipe 0.10.21+ removió el namespace legacy mp.solutions.selfie_segmentation (la única forma documentada en tutoriales y ejemplos antiguos). Toca migrar a mediapipe.tasks.vision.ImageSegmenter con un .tflite que se descarga en runtime.

Algo similar pasó con NetMQ en Unity: la propiedad socket.Options.Conflate no compila en versiones recientes. Fix: confiar en que Python ya hace conflate del lado del PUB, y combinar con ReceiveHighWatermark = 1 en el SUB. El comportamiento es prácticamente equivalente — máximo un frame stale en el peor caso.

Bloom + colores HDR es lo que vende el demo en vivo.

Visual

Sin post-processing, los boids son puntos blancos en un fondo. Con URP 2D + Bloom volume + colores HDR > 1.0 por grupo (cyan eléctrico, magenta neón, amarillo dorado), la simulación se siente como un banco de peces fosforescentes.

El “color de pánico” en HDR alto (intensidad ~4) hace flash visible cuando un boid entra a zona de peligro — eso es lo que hace que el espectador entienda la mecánica sin que nadie la explique.

Próximas iteraciones

Lo que está construido funciona, pero hay margen para hacer el demo más impactante. En orden de prioridad:

  1. P · Alta

    Estela de miedo residual con BFS aplicado. Cuando la silueta se mueve, dejar un rastro que se desvanece en ~2 segundos. Los boids evitan esa zona aunque la silueta ya no esté ahí. Hace que la manada se vea con “memoria”.

  2. P · Alta

    LOD para IA en boids lejos del peligro. Los boids muy lejos del campo activo pueden tickear su lógica de flocking a 5 Hz en vez de 60. Imperceptible visualmente, brutal en CPU si querés escalar a 1000+ agentes.

  3. P · Media

    Multi-persona. MediaPipe Selfie Segmentation no separa personas. Para una instalación con varios espectadores, migrar a ImageSegmenter con modelo de instancias o YOLOv8-seg.

  4. P · Baja

    Compute Shader para >5000 boids. El flocking actual está en un MonoBehaviour con arrays de structs. Llega bien hasta ~500 boids. Para masivo, mover el loop entero a GPU.

Verlo en acción

Pendiente: video de la demo corriendo en vivo.

Pendiente: foto del montaje del evento.

¿Quieres hacer algo así?

El camino más corto:

  1. Empieza por Flocking / Boids — sin esto no hay demo.
  2. Cuando empieces a poner cientos de agentes, pasa por Grids espaciales — la diferencia entre 18 fps y 60 fps estables.
  3. Para la fuerza de huida, Steering Behaviors II te da la base de evade.
  4. Crea tu “depredador” con un collider o un sprite y haz que evadan los agentes de el.
  5. Cambia tu depredador por un input de campo de enteros que se comunica mediante el buffer.

El resto (segmentación, ZMQ, post-processing) son piezas que conectas alrededor. Si quieres todo el camino guiado, revisa el mapa — está marcada la ruta para llegar hasta acá.


Proyecto de Juanjo “Banyo” López. DemoDay Amerike CDMX, mayo 2026. Sobre el autor →