Systemede
Particules

schoeffer_tour_cybernetique

tutorial de programmation
du mouvement en processing

Anthony Mattox

 

Introduction

Dans ce cours, on reprend le code du cours précèdent sur le mouvement pour fabriquer une système de particule. Nous allons ensuite élargir ce code par différents techniques de la programmation orientée objet, notamment celui de la surcharge de constructeur, l’héritage de classe et le polymorphisme.

Référence / Bibliographie

Daniel Shifman: particle systems

Polymorphisme

Système de particule

Un système de particule est une collection d’objets indépendants, souvent représentés par une forme ou simplement un point. Il peut être utilisé pour modéliser de nombreux phénomènes naturels irréguliers, tels que les explosions, le feu, la fumée, des étincelles, des cascades, des nuages, de brouillard, de pétales, de l’herbe, des bulles, et ainsi de suite. Dans un système, chaque particule a son propre ensemble de propriétés relatives à son comportement (par exemple, vitesse, accélération, etc) ainsi que son apparence définit par la couleur, la forme, l'image.

Nous allons construire deux classe, l’une qui décrit les propriétés et comportement de la particule et un deuxième classe qui permet de gérer un grand nombre de particules dans son ensemble. Il y a rien de nouveau au niveau d’algorithme, nous portons l’attention plutôt à la construction du code. La première question qu’on se posera est quels sont les paramètres qu’on définit dans la classe de particules et lesquels dans la classe du système des particules. Alors, tous ce qui concerne l’apparence et comportement de la particule sera définit dans la classe de particule et ce qui concerne l’organisation et le comportement globale des particules dans la classe du système de particule.

Dans un premier temps, on reprend le code de la semaine passée en le simplifiant et en le renommant Particle. On a juste 4 paramètres pour le mouvement: la position, la vélocité et l’accélération ainsi qu’un variable 'bounce' qui définit la force de rebondissement. Ensuite la particule à un temps de vie en forme de boolean, quand la vie est terminé elle devient false. Les méthodes décrivent le comportement de mouvement.


class Particle {
  PVector pos;
  PVector vel;
  PVector acc;
  float max_vel = 40;
  int defaultLifeTime = 100;
  int lifeTime = defaultLifeTime;
  int taille = 10;
  float bounce;

  Particle (PVector p, PVector v, PVector a) {
    pos = p.get();
    vel = v.get();
    acc = a.get();
  }

  void update() {
    //border();
    vel.add(acc);
    vel.limit(max_vel);
    pos.add(vel);
    lifeTime--;
  }

  void render() {
    update();
    ellipse (pos.x, pos.y, taille, taille);// *5 pour le rendre un peu plus grand
  }

  boolean life() {
    if (lifeTime < 0) return false;
    else return true;
  }
}

Ensuite nous allons construire la classe qui permet de gérer un ensemble de particules qu’on nomme ParticleSystem. On utilise à nouveau le ArrayList, qu’on a abordé au cours trigoLines, qui nous permet d’une manière souple de rajouter et enlever des éléments de la liste. Pour l'instant, elle nous permet simplement de créer un certaine nombre des particules, de le dessiner jusqu’à son mort. Un autre méthode nous permet de savoir quand il n’y a plus des particules dans le système et puis une pour rajouter une particule dans le système. Dans le constructeur, nous avons mis le 'bounce' par défaut à -1;

class ParticleSystem {

  ArrayList particles; 

  ParticleSystem(PVector origine, int nbr) {
    particles = new ArrayList();
    for (int i = 0; i < nbr; i++) {
      particles.add( new Particle(origine) );
    }
  } 

  void update() {
    for (int i = particles.size()-1; i >= 0; i--) {
      Particle p = (Particle) particles.get(i);
      p.render();
      if (p.life() ==false) particles.remove(i);
    }
  }

  void addParticle(Particle p) {
    particles.add(p);
  }

  boolean ParticleSystemIsEmpty() {
    if ( particles.size() <= 0 ) return true;
    else return false;
  }

Maintenant nous souhaitons de faire en sorte qu'on peut définir à quel coté et avec quelle force la particule rebond sur les cotés. Pour cela, nous allons créer 4 variables globales de la classe déterminants les 4 cotés

boolean ps_up, ps_right, ps_down, ps_left;

et ensuite nous allons créer une méthode, permettant de définir à l’extérieur de la classe à quel coté la particule va rebondir. Enfin on crée un autre méthode qui permet le rebond sur les cotés. Nous appelons cette méthode dans la méthode render. C’est pour cela nous créons un argument 'Particle'. Java nous permet d’utiliser n’importa quel type de donnée comme argument, même ceux qu’on a créé nous même. Avec la classe Particle, nous avons créé un nouveau type de donnée. Regardons ce partie du code:

  void update() {
    for (int i = particles.size()-1; i >= 0; i--) {
      Particle p = (Particle) particles.get(i);
      border(p);
      ...
    }
  }

  void setBorderBounce( boolean _up, boolean _right, boolean _down, boolean _left) {
    ps_up = _up;
    ps_right = _right;
    ps_down = _down;
    ps_left = _left;
  }

  void border(Particle p) {   
    if ( p.pos.x < 0 && ps_left ) p.vel.x*= p.bounce;
    if ( p.pos.x > width && ps_right ) p.vel.x*= p.bounce;
    if ( p.pos.y < 0 && ps_up ) p.vel.y*= p.bounce;
    if ( p.pos.y > height && ps_down ) p.vel.y*= p.bounce;
  }
}

 

Conception de code

Il y a quelques techniques dans la programmation orienté objet qui permet de garder un maximum de cohérence et au même temps une souplesse de son utilisation.

Surcharge de constructeur

Comme pour les méthodes, on peut aussi définir plusieurs constructeurs autant qu’ils se distinguent soit par leurs types de données au par le nombre d’arguments. Alors nous définissons plusieurs constructeurs pour la classe particule et plusieurs pour la classe.

 

  Particle (PVector p, PVector v, PVector a, float _bounce) {
    pos = p.get();
    vel = v.get();
    acc = a.get();
    bounce = _bounce;
  }
  Particle (PVector p, PVector v, PVector a) {
    pos = p.get();
    vel = v.get();
    acc = a.get();
  }

  Particle (PVector p, PVector v) {
    pos = p.get();
    vel = v.get();
    acc = new PVector(0, 0);
  }

  Particle (PVector p) {
    pos = p.get();
    vel = new PVector(0, 0);
    acc = new PVector(0, 0);
  }

et

  ParticleSystem() {
    particles = new ArrayList();
  }

  ParticleSystem(PVector origine, int nbr) {
    particles = new ArrayList();
    for (int i = 0; i < nbr; i++) {
      particles.add( new Particle(origine) );
    }
  } 

  ParticleSystem( PVector origine, PVector vel, int nbr) {
    particles = new ArrayList();
    for (int i = 0; i < nbr; i++) {
      particles.add( new Particle(origine, vel) );
    }

    ParticleSystem( PVector origine, PVector vel, PVector acc, int nbr) {
      particles = new ArrayList();
      for (int i = 0; i < nbr; i++) {
        particles.add( new Particle(origine, vel, acc) );
      }
    } 

 

Heritage de classe

Voila, maintenant nous avons construit un système de particule relativement souple pour travailler avec. Il y a un exemple d’un système de particule relativement simple et assez joli sur le site de openprocessing.org.

Son auteur et Toshitaka AMAOKA, on trouve l'exemple à cette adresse

On regardant son code, on voit qu’il est construit quasi similaire, il n’utilise pas des vecteurs, mais la physique qu’il emploie est la même. Mais comment peut-on intégrer son code dans notre structure sans y toucher ? La réponse est :

L'héritage et polymorphisme.

Héritage (inheritance) est un concept essentiel de la programmation orientée objet. Elle permet de définir une classe -fille (ou sous-classe) qui hérite des propriétés d’une autre classe- mère (super -classe). La classe fille hérite donc toutes les propriétés de la classe mère mais peut avoir aussi ses propres propriétés et méthodes. Pour définir qu’une classe est la sous-classe d'un autre on utilise le mot-clé extends. Alors si on veut écrire une classe à part pour des particules propres au code de Toshitaka, on fait hériter celui de notre classe Particle. Ensuite on peut utiliser toutes les propriétés de Particule. Il y a encore une particularité à respecter. Le constructeur doit faire référence à un constructeur de notre classe mère et il faut expliciter qu'on va utiliser ce constructeur à qui on fait référence avec le mot-cle super et on passe les arguments du constructeur fille à la mère. Alors on peut écrire:

Particle_Line l_particle;

void setup() {
  l_particle = new Particle_Line(new PVector(0, 0), new PVector(0, 0), new PVector(0, 0), 10);
}

void draw() {
  l_particle.render();// on utilise la methode de la mère
}

class Particle_Line extends Particle {
  Particle_Line(PVector p, PVector v, PVector a, float b ) {
    super(p, v, a, b);
  }
}

Il reste encore un problème conceptuel, la classe ParticleSystem attend un objet de la classe Particle et non pas un objet de type Particle_Line. ( particles.add( new Particle(origine) ); ) C'est ici que le polymorphisme va nous aider. Le polymorphisme définit qu'on peut déclarer un objet de type mère et instancier une référence de type fille. Cela parait compliqué, mais dans la pratique ce n'est le pas. On peut changer les premiers deux lignes du code en haut en :

Particle l_particle;

void setup() {
  l_particle = new Particle_Line(new PVector(0, 0), new PVector(0, 0), new PVector(0, 0), 10);
}

On a donc déclarer un objet du type Particle, et lors qu’on instancie cet objet on lui passe une référence du type Particle_Line. Voila la partie conceptuelle le plus difficile.

On regardant le code, la principale différence est donne la façon comment les particules sont dessiné. Ce ne sont pas des ellipses mais des lignes qui prennent la position de la position du frame avant. Et ces lignes devient de plus en plus opaque avec l'age (de la particule). Il faut alors changer la méthode render, ce qui est très confortable avec l’héritage, car on peut simplement redéfinir une méthode de la classe mère. Mais dans notre classe mère "render" se trouve la méthode update qu’on veut utiliser tel quel, alors on fait appelle dans notre nouvelle méthode à la classe mère par le mot-clé "super". Voici le code de la classe hérité:

class Particle_Line extends Particle {
  PVector last_pos;
  color c; 
  
  Particle_Line (PVector p, PVector v, PVector a, float b ) {
    super(p, v, a, b);  
    last_pos = p.get();
    lifeTime = int (random(100, 150));
    c = color(random(50), 255, 255);
  }

  void render() {
    super.update();
    stroke(c, lifeTime);
    line(pos.x, pos.y, last_pos.x, last_pos.y);
    last_pos=pos.get();
  }
}

Pour instancier notre système des particules, nous avons deux possibilités: Soit on passe directement les paramètres directement dans le constructeur, ou bien on crée d’abord un système des particules vide, ensuite on crée les particules qu’on rajoute ensuite dans le système. Pour éviter d’avoir un constructeur avec bcp des arguments, j'ai opté pour la deuxième stratégie. Et voici le code :

ParticleSystem particles;
int nbrParticles = 2000;

void setup() {
  size(800, 600, P2D);
  colorMode(HSB);
  createParticleSystem(width/2, height/2);
  background(0);
}

void draw() {
  particles.update();
}

void mouseReleased() {
  background(0);
  createParticleSystem(mouseX, mouseY);
}

void createParticleSystem(int x, int y) {
  particles = new ParticleSystem ();
  particles.setBorderBounce(false, true, true, true);
  for (int i = 0; i < nbrParticles; i ++) {
    Particle particle = new Particle_Line(
    new PVector (x, y), 
    new PVector (random(-10, 10), random(-30, 0) ), 
    new PVector( 0, random(0.5, 0.8) ), 
    random( - 0.4, - 0.99 )                                         
      );
    particles.addParticle(particle);
  }
}

Voici le code complet >>

Ce qu'il faut connaître:
 

 

Exercice