Fée attractrice

Toutes les vidéos de @generartiv ne sont pas publiées ici, pour ne pas en rater, ca se passe sur Youtube, Twitter ou Facebook.

Un sketch Processing en trois parties….

Le sketch se decompose en trois parties:

Comme d’habitude, un Setup et un Draw…

ParticuleSystem PS;

void setup() {
  size(1280, 720);
  PS = new ParticuleSystem();
  background(0);
}


void draw() {
  PS.run();
}

int getNumPixel(int x, int y) {
  return y * width + x;
}

Pas grand chose de particulier ici.

On déclare PS comme instance de la Class « ParticuleSystem ».

Un Setup qui définit la taille de l’affichage, instancie le ParticuleSystem et efface l’écran.

Un Draw qui a chaque tour appellera la fonction « Run » du système.

La fonction « getNumPixel » renvoi le numéro du pixel sur lequel on veut agir. Ce pixel est stocké dans le tableau « pixels[] », il nous faut donc l’identifiant dans ce tableau, on y viendra un peu plus loin dans la Class « ParticuleSystem »

La Class « Particule »

Parce que notre système de particules, il faudra bien qu’il dispose de particules, voici la classe dont elle sont faites:

class Particule {
  PVector position;
  PVector positionInit;
  PVector velo;
 
  Particule() {
    positionInit = new PVector(random(width-1), random(height-1));
    init();
  }

  void init() {
    position = positionInit.copy();
    velo = new PVector(0, 0);
    }

  void init(Particule particuleCopy) {
    positionInit = particuleCopy.position.copy();
    position = positionInit.copy();
    
    //velo = particuleCopy.velo.copy();
    velo = new PVector(0, 0);
   }

  void majPos() {
    position.add(velo);
  }

  void display() {
   
    float alpha = 255 - pow(PVector.dist(position, PS.cible), 3) * 255/(pow(width, 3));
    if (alpha<0) {
      alpha  = 0;
    } else if (alpha>255) {
      alpha = 255;
    }
    stroke(255, alpha/115);
    strokeWeight(86 - alpha/3);
    point(position.x, position.y);
    stroke(255,alpha);
    strokeWeight(1);
    point(position.x, position.y);
  }
}

Chaque particule a une position (actuelle), une position initiale (on y reviendra quand on verra le système), une direction (« velo »)… On se sert de Pvector pour ce faire, ça va nous simplifier le boulot par la suite…

La fonction « Particule() » crée notre objet, initialise la position au hasard et appele « init() » pour initialiser le reste…

La fonction « init() » est déportée et existe en deux versions : Notre système va aussi l’appeler… Pour commencer a raconter la vie de notre système, quand une particule aura fait son boulot, notre système décidera soit de réinitialiser notre particule dans son état initial, soit que cette particule va prendre les caractéristiques d’une autre particule, soit de complètement éliminer cette particule…

La fonction « majPos() » ne fait qu’ajouter la direction de la particule a sa position actuelle… Puisqu’on se sert de PVectors, ca se fait en un appel de la fonction « add() ».

Et comme il faut bien qu’on puisse voir notre particule, la fonction « display() » est faite pour ça… Notre particule affichera un « point() » (qui prend comme argument la position de la particule), mais on le fera deux fois, en calculant auparavant la couleur, la transparence, la taille du point : on fait un gros point pas très visible pour faire un halo autour le la particule, puis un point de taille « 1 » pour la particule en elle-même. Plus la particule sera éloigné de l’attracteur du système plus le halo sera gros et visible, plus la particule sera proche de l’attracteur et plus le point sera visible….

Le système de particules…

On peux enfin passer au moteur de tout ça : La Classe « ParticuleSystem »….

Pour comprendre ce qui suit, il faut savoir que notre système a à sa disposition une carte, sous forme de tableau bidimesionnel et de la taille de l’écran qui contient des PVector.

Cette carte représente, pour chaque pixel affiché une direction vers laquelle notre particule va être délicatement orientée : On fera appel a la fonction « lerp() » et « normalize() » de la classe Pvector pour ajuster au fur et a mesure les valeurs de cette carte, tout comme les valeur de la direction de chaque particule.

C’est là que la magie opère : A chaque mouvement de la particule, la valeur de la carte a la position où elle se trouve sera délicatement dirigée dans la direction que la particule prend, mais aussi dans la direction de la cible (dont la position est aussi un PVector) …

On trouve en instanciation de notre système un tableau contenant toutes nos particules (un ArrayList), et notre cible. La position de celle-ci évoluant en cercle au milieu de l’écran, le plus simple est d’incrémenter a chaque tour un angle (on lui retire TWO_PI si on a fait un tour complet), la position en absissie sera le cos() de cet angle multiplié par le rayon qu’on desire, la position en ordonné pareil mais avec le sin().

class ParticuleSystem {
  PVector carteVelo[][];
  PVector carteVeloSuiv[][];
  ArrayList<Particule> particules = new ArrayList<Particule>();
  
  float angleCible = 0;
  PVector cible;


  ParticuleSystem() {
    cible = new PVector(width/2, height/2);
    carteVelo = new PVector[width][height];
    carteVeloSuiv = new PVector[width][height];
    for (int x=0; x<width; x++) {
      for (int y=0; y<height; y++) {
        carteVelo[x][y] = new PVector(random(-1, 1), random(-1, 1));
        carteVelo[x][y].normalize();
      }
    }
  }

  void run() {

    angleCible += 0.00220;
    if (angleCible>TWO_PI) {
      angleCible -=TWO_PI;
    }
    cible.x = cos(angleCible) * height/4  + width/2;
    cible.y = sin(angleCible) * height/4  + height/2;

    loadPixels();
    for (int x=0; x<width; x++) {
      for (int y=0; y<height; y++) {
        int numPixel = getNumPixel(x, y);
        color c = pixels[numPixel];
        pixels[numPixel] = color(red(c)*0.995, green(c)*0.996, blue(c)*0.999);
        carteVeloSuiv[x][y] = carteVelo[x][y].copy();
      }
    }
    updatePixels();

    if (particules.size()<158 ) {
        particules.add(new Particule());
    }

     for (int x=0; x<width; x++) {
      for (int y=0; y<height; y++) {
        carteVeloSuiv[x][y] = carteVelo[x][y].copy();
      }
    }

    PVector v2cible = new PVector(0, 0);
    for (int i=1; i<particules.size(); i++) {
      Particule particuleTmp = particules.get(i);

      v2cible = PVector.sub(cible, particuleTmp.position);
      v2cible.normalize();

      carteVeloSuiv[floor(particuleTmp.position.x)][floor(particuleTmp.position.y)].lerp(v2cible, 0.33);
      carteVeloSuiv[floor(particuleTmp.position.x)][floor(particuleTmp.position.y)].lerp(particuleTmp.velo, 0.24);

      carteVeloSuiv[floor(particuleTmp.position.x)][floor(particuleTmp.position.y)].normalize();

      particuleTmp.velo.lerp(carteVelo[floor(particuleTmp.position.x)][floor(particuleTmp.position.y)], 0.57);
      particuleTmp.velo.normalize();
      particuleTmp.majPos();

      particuleTmp.display();
      if (particuleTmp.position.x<0 ||
        particuleTmp.position.x>width-1||
        particuleTmp.position.y<0 ||
        particuleTmp.position.y>height-1) {

        particules.remove(particuleTmp);
      } else if (PVector.dist(particuleTmp.position, cible)<2) {
        if (random(100)<49) {
          particuleTmp.init();
        } else {
          if (random(100)>54) {
            particuleTmp.init(particules.get(floor(random(particules.size()-1))));
          } else {
            particules.remove(particuleTmp);
          }
        }
      }
    }

    for (int x=0; x<width; x++) {
      for (int y=0; y<height; y++) {
        carteVelo[x][y] = carteVeloSuiv[x][y].copy();
      }
    }
  }
}

A chaque tour, la fonction « run() » est appelée.

On commence par mettre a jour la position de la cible, puis on efface un peu l’écran:

C’est ici que la fonction « getNumPixel() » qu’on a vu au debut de cet article entre en jeu.

On commence par mettre a jour le tableau des pixels en appellant « loadPixels() ».

A ce moment pixels[] contient l’ensemble des pixels qui ont été affichés au tour précédent. pour chaque abscisse de l’écran, pour chaque ordonnée, on cherche le numéro du pixel correspondant, on récupéré sa couleur et on en réduit sa valeur (c’est ici que la teinte légèrement vert/bleue de la traînée des particules est crée). Pour une traînée plus importante, on laisse le pixel quasi inchangé, pour réduire la traînée, on réduit sa valeur…

Vient ensuite le moment ou l’on crée des particules: Si on a pas assez de particules (« particules.size() »)dans notre ArrayList, on en ajoute (« add(new Particule() »)

Le reste de la fonction « run() » est dédiée a l’actualisation de la carte et de la direction de chaque particule.

On dispose de deux versions de cette carte: le tableau « carteVelo[][] » et le tableau « carteVeloSuiv[][] » : Avant d’opérer des changements sur la carte on en fait une copie : les particules évolueront en fonction des valeurs de la carte, on modifiera les valeurs de la carte copiée. Quand (a la fin) on aura fait toutes nos opérations, on actualisera les nouvelles valeurs de la carte. Cela peux paraître un peu alambiqué mais en procédant ainsi, une modification de la carte copiée n’aura d’effet qu’au tour suivant et n’aura pas d’incidence sur le comportement des particules traitées sur ce tour.

Pour chaque particule, on calcule le PVector de sa direction a la cible (« PVector.sub(cible, particuleTmp.position) », la fonction « normalize() » ramene ce vecteur a une distance de 1.

La fonction « lerp() » de la classe PVector nous permet de modifier légèrement la direction de la particule et la direction indiquée par la carte (celle de la copie, bien sur…)

« lerp() » prend pour arguments le PVector initial, le PVector qui modifie celui-ci et un coefiscient de modification… En utilisant la fonction « tweak » de Processing, on peux facilement ajuster à la volée le comportement des particules, l’intensité avec laquelle la particule agit sur la carte et la radité avec laquelle la carte va s’orienter vers la cible… Là, c’est a vous de vous amuser…

Vient le moment de déplacer notre particule… on appelle la fonction « majPos() » et la fonction « display() » de la Classe « Particule »…

Dernier tour de magie qui ne compte pas pour du beurre : A quel moment une particule a-t-elle fait son boulot? Qu’est-ce qu’on en fait ensuite?

Premier cas de figure : Une particule est en dehors de la zone affichée (x<0 ou x>width ou y<0 ou y>height): on la supprime au moyen de « particules.remove(particuleTmp) », si on a placé nos particules dans un ArrayList c’est bien pour se simplifier les ajouts / suppressions, ne nous en privons pas.

Deuxieme cas de figure : Une particule est arrivée a rejoindre la cible (la distance calculée avec « PVector.dist() » est inferieure a 2 pixels), dans ce cas on aura trois possibilités (choisies au hasard grace a « random(100)>valeur »):

On finit par copier les valeur de la carte copiée dans le tableau de la carte « bonne », et on a finit.

Le code en entier

L’intégralité du code est disponible sur GitHub: https://github.com/generartiv/fee_attractrice