Lab

Franc-Tamponnage et le creative coding

Pendant un mois, notre région Bourgogne-Franche-Comté était chamboulée : le festival de musique appelé Franc-Tamponnage, dédié aux genres alternatif, électronique et extrême, battait son plein. Nos amis de Magna Vox avaient prévu…

Pendant un mois, notre région Bourgogne-Franche-Comté a été chamboulée : le festival de musique Franc-Tamponnage, dédié aux genres alternatif, électronique et extrême, battait son plein. Nos amis de Magna Vox avaient prévu des dizaines de concerts dans toute la région.

Pour une série de concerts en particulier, nous étions chargés de créer une belle expérience visuelle pour les visiteurs. C’était l’occasion parfaite pour un peu de creative coding ! Nous étions 3 développeurs sur ce projet : Julien expérimentant pour la première fois avec openFrameworks, Tamsen s’amusant avec l’un de ses jouets favoris, Processing, et Aymeric faisant du bon vieux Flash !

Voici le récap de chacun d’entre nous :

Aymeric :

J’ai travaillé sur l’affiche conçue par Le Tâche Papier et utilisé un effet d’ondulations d’eau avec AS3.

Tamsen :

J’ai travaillé avec Processing 3 pour créer un « visualizer » de distorsion d’image piloté par la musique. Les images provenaient d’un artiste participant, Pierre Berthier (des scans de croquis traités en noir et blanc), donc le rendu de base de Processing était noir sur blanc, rien de spectaculaire. Le tout était ensuite composité en direct et manipulé par un technicien qui contrôlait quoi et comment projeter, à l’aide de Millumin.

En voici un exemple :

L’idée était de laisser la musique en direct contrôler la façon dont les images changeaient et se distordaient ; le sketch Processing était prêt pour cet usage, mais en raison de contraintes techniques et de temps, une vidéo du rendu Processing préenregistré était diffusée en boucle, utilisée comme le technicien le jugeait bon pendant le concert.

En utilisant la bibliothèque Processing appelée minim, de compartmental, une simple FFT directe est appliquée à l’audio en entrée. Cela nous permet d’obtenir des données spectrales du signal entrant : en résumé, on a un flux de valeurs, chacune variant selon que le son contient des fréquences basses, moyennes ou hautes (le nombre de « divisions » ou de bandes qu’on peut extraire du signal peut être déterminé à l’avance ; nous en avons utilisé 6).

C’est la façon la plus basique de réagir au son, et ce serait le même algorithme utilisé pour simplement afficher une visualisation spectrale comme celle-ci, que nous avons tous déjà vue :

https://aymcreations.deviantart.com/art/Equalizer-385871495

Vous voyez donc que pour chaque bande du spectre, on peut jouer avec certains paramètres.

Dans ce cas, les hautes fréquences changeaient l’image utilisée ; l’image elle-même est affichée sur un quad, dont les sommets sont déplacés dans l’espace, tout comme les uv eux-mêmes. Il y a aussi des effets comme un flou de mouvement simulé (obtenu en ne réinitialisant pas le fond du buffer).

Voici un patch simple pour démarrer :

import ddf.minim.*;
import ddf.minim.analysis.*;

Minim minim;
AudioInput input;
FFT fft;

int bands = 6; // number of 'bands' we wish to extract from the audio input

float[] spectrum = new float[bands];

void setup() {
  size(600,600);

  minim   = new Minim(this); //create minim
  input = minim.getLineIn(Minim.STEREO); //start line in (audio input)

  fft = new FFT(input.bufferSize(),input.sampleRate()); //create fft according to input
  fft.linAverages(bands); //setup for averages
  fft.window(FFT.GAUSS); //window algorithm

  background(255);
  smooth(4);

  colorMode(HSB);
  noStroke();
}

float getFFT(int i) {
  float val = spectrum[i];
  //Here, one could correct values, remap or scaled them based on index etc...
  return val;
}

void doSpectrum() {
  fft.forward(input.mix);

  for (int i = 0; i < bands; ++i)
    spectrum[i] = fft.getAvg(i);
}

void draw() {
  doSpectrum();

  background(255);

  float d = (float)width/(float)bands;
  float hueDiv = 255.0/(float)bands;

  for(int i = 0;i < bands; i++) {

    color c = color(i*hueDiv,255,255);
    fill(c);

    float h = getFFT(i) * height;

    rect(i*d,height - h,d,h);
  }

}

void stop()
{
  input.close(); minim.stop();
  super.stop();
}

Cet exemple se contente de dessiner un histogramme du spectre. Vous remarquerez également que plus les bandes sont hautes dans le domaine fréquentiel, plus les valeurs sont faibles ; il faudrait (dans getFFT par exemple) essayer de remapper ou de mettre à l’échelle les valeurs selon l’indice de la bande pour obtenir des valeurs plus exploitables. Le code pour le faire n’est pas inclus ici par souci de clarté, et il existe de nombreuses approches possibles.

Pour être clair aussi, on appelle « bandes » des moyennes de plusieurs bandes, car on ne veut pas des données brutes complètes que la FFT extrairait. Quoi qu’il en soit, c’était suffisant pour notre cas d’usage. Une analyse sonore plus complexe, comme l’analyse du timbre, pourrait être réalisée avec des logiciels plus orientés audio comme Pure Data et la bibliothèque timbreID, qui ne fait pas seulement de la FFT mais aussi une analyse plus fine de la « nature » du son, disons. Nous avons failli partir dans cette direction, mais cela aurait impliqué de la communication wifi et du matériel que nous n’avons malheureusement pas eu le temps de mettre en place.

Julien :

J’étais nouveau dans la programmation créative, et j’allais utiliser le C++ pour programmer des effets procéduraux sur les images de Pierre Berthier. Le célèbre et bien documenté OpenFrameworks a donc été mon choix pour le développement.

Comme les images étaient en noir et blanc, j’ai préféré y ajouter des couleurs et jouer avec. Dans le temps imparti, j’ai développé 3 effets : des meta-balls, un remplissage liquide avec émission de particules, et un effet de propagation de liquide.

En voici un exemple sur un dispositif holographique pendant le festival :

Les effets n’étaient pas pilotés par la musique, et nous voulions garder le contrôle sur la génération. Un séquenceur a été développé pour « randomiser » l’affichage avec des patterns (effets avec différentes couleurs, image de fond, et d’autres paramètres). Les patterns sont stockés dans un json.

Au départ, il était prévu de générer la vidéo en temps réel sur un appareil embarqué, donc nous avions prévu d’utiliser un Raspberry Pi 3. Le Raspberry Pi n’est pas un ordinateur puissant, et programmer des effets sous OpenFrameworks avec OpenGL n’est pas très efficace en haute résolution (720p ou 1080p). Le Raspberry affichait les effets entre 25 et 32 FPS en 720p. Pour améliorer les performances, j’ai donc écrit la plupart des effets en shaders (meta-balls et propagation de liquide).

Metaballs

Les metaballs sont de simples surfaces implicites constituées uniquement de sphères. Le développement du shader n’était donc pas compliqué. La partie délicate concernait les effets de traînée/remplissage. Des FBO (FrameBuffer objects) sont utilisés pour créer des couches : une pour le rendu des metaballs et une pour les traînées. Sur la couche des traînées, on rend les metaballs avec un alpha pour créer un effet d’accumulation. Pendant l’effet, la couleur des metaballs change, et c’est la même chose pour les traînées. Le blend entre l’ancienne et la nouvelle couleur a été difficile à trouver. Nous laissons ici le code des metaballs, ça vous fera peut-être gagner du temps.

ofSetColor(255);

int nbCircle = 10;
float _metaballsCenter[nbCircle * 2];
float _metaballsSquareRadius[nbCircle];

_fbo.begin();
metaballsShader.begin();
metaballsShader.setUniform2fv("metaballsCenter", _metaballsCenter, _nbCircle);
metaballsShader.setUniform1fv("metaballsSquareRadius", _metaballsSquareRadius, _nbCircle);
metaballsShader.setUniform3f("inputColor", _currentColor.r*.0039215, _currentColor.g*.0039215, _currentColor.b*.0039215);

_fbo.draw(0, 0);

metaballsShader.end();
_fbo.end();

ofEnableAlphaBlending();
glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_SRC_ALPHA, GL_ONE);

ofSetColor(220, 220, 220, 20);

_fbo2.begin();
_fbo.draw(0, 0);
_fbo2.end();

ofEnableAlphaBlending();
ofSetColor(ofColor::white);

_fbo2.draw(x, y);
_fbo.draw(x, y);

Remplissage liquide avec particules

L’effet combine 3 couches : les particules, les traînées, et le « liquide ».

Les particules sont de simples cercles avec une physique basique (vélocité + masse + gravitation). Les traînées sont similaires à celles des metaballs : un fbo avec une couleur semi-transparente appliquée à chaque frame. Le liquide est un polygone dont 3 bords sont alignés sur les limites de l’écran, le dernier bord étant dessiné par un bruit de Perlin.

Propagation de liquide

La propagation de liquide repose sur une physique de liquide très simplifiée. Elle s’appuie sur un article de JGallant : http://www.jgallant.com/2d-liquid-simulator-with-cellular-automaton-in-unity/. L’effet est influencé par une forme d’obstacle stockée dans une image. Le canal vert est utilisé, et la valeur du vert représente la « hauteur » de l’obstacle à ce pixel. Ensuite, sur une autre image, les sources de liquide coloré sont placées. Le liquide est stocké dans les canaux de pixel (r,g,b), ce qui permet de gérer trois liquides différents en même temps. À chaque frame, chaque pixel voisin d’un pixel contenant du liquide reçoit une quantité fixe de liquide (couleur). Dans ce processus, les sources ne perdent pas de liquide tant que le niveau maximal (255) n’est pas atteint. Ainsi, si un pixel est proche de plusieurs sources de couleurs différentes, les couleurs s’additionnent dans le pixel et se mélangent entre couleurs primaires. Quand un pixel se trouve sur un obstacle, on compare le niveau de liquide du pixel le plus proche au niveau de l’obstacle. Si le niveau de liquide est supérieur à celui de l’obstacle, alors la couleur du liquide est ajoutée au pixel. Enfin, pour chaque couleur primaire (rgb), une couleur de remplacement est définie, et l’effet est terminé.

AL
Aymeric Lamboley
Fondateur & Directeur technique