Auras et boules de feu !

Suis ce tuto en vidéo !

Aujourd’hui nous inaugurons une nouvelle série d’articles, celle sur les effets graphiques ! La première partie est de niveau débutant car elle ne parle que de l’approche graphique du problème. La deuxième partie concernant l’approche programmatique et mathématique est plutôt destinée à un public confirmé sachant déjà coder un minimum en javascript. Pour démarrer cette série, quoi de mieux qu’un bon vieux retour dans les années 80-90 avec …. La génération de boules de feu et d’auras à la Dragonball ! C’est parti, Tadadadadaaaaaaaaaa !!

Le rendu final

C’est vrai que ça a l’air vendeur comme ça, mais toi lecteur, qu’es-tu en droit d’espérer comme rendu après avoir lu cet article ? Et bien sans plus tarder voici une liste de GIFs animés générés à l’aide du code que nous allons étudier:

Alors, ça vous tente ? Si oui, embarquez avec moi, et voyons comment recréer ces superbes effets !

Le problème

En entamant le processus de création du générateur d’aura, je me suis posé les questions suivantes:

– Comment font les gens qui bossent sur les jeux vidéos de la licence Dragonball ?
– Les auras sont-elles générées à la volée ou bien dessinées ?
– Quelles techniques graphiques proches puis-je récupérer et détourner afin d’atteindre mon but ?

En faisant des recherches, je suis tombé sur le projet ZEQ2, tenu par des passionnés, leur rendu est extrêmement beau et la technique utilisée reste assez classique, à savoir l’utilisation de systèmes de particules et de spritesheets. Voici un aperçu de leur travail:

Après plusieurs tentatives infructueuses, je suis arrivé à un rendu à peu près correct à base de particules, je voulais obtenir un effet plus proche du manga, moins « 3D » que celui de ZEQ2:

Mais le rendu ne me convenait pas vraiment, en effet, cela manquait de puissance, de force, et de vitesse à mon goût. De plus, il me fallait customiser les systèmes de particules de façon assez fine pour un rendu qui ne correspondait pas exactement à ce que je voulais. J’aurais bien sur pu persister un peu et explorer d’autres pistes afin d’affiner mon rendu, mais je sentais que j’étais en train de me lancer dans quelque chose qui à terme, ne me satisferait pas. J’ai donc entrepris d’aborder le problème sous une approche différente

L’approche graphique

Plus je passais du temps à regarder mes auras et mes boules de feu, plus j’arrivais à distinguer un motif récurrent et particulier. Les auras n’étaient en fait que des boules parfaitement rondes auxquelles on avait ajouté des traînées de feu possédant des angles et des longueurs différentes. Ces traînées possédaient toujours une base triangulaire.

Schéma de base d’une boule de feu

Une fois cette base posée, il était facile de remarquer ce qu’il me manquait, à savoir du bruit. En effet, les bases triangulaires ne suffisent pas, les bords d’une traînée de feu ne sont pas lisses, ils sont ponctués de petits « pics » sur les côtés. Ces petits pics, nous allons les ajouter à l’aide de ce que l’on appelle un « bruit », à savoir une suite de valeurs aléatoires comprises entre un maximum et un minimum. On peut constater sur le schéma suivant la différence entre un pic « avec bruit » et un pic « sans bruit ».

On se rapproche de l’effet voulu !

On se rapproche de l’effet voulu mais ça n’est pas encore parfait, il manque un petit quelque chose… Si tu fais bien attention, tu verras que l’aura de Goku ne possède pas des côtés vraiment droits mais plutôt légèrement arrondis. Qu’à cela ne tienne, nous allons rajouter un peu d’arrondi à ces lignes droites !

On y est presque, plus que deux étapes et on aura notre base de boule de feu / aura ! La prochaine étape consiste à ajouter plusieurs traînées de feu supplémentaires de tailles différentes les unes par dessus les autres !

It’s over 9000 !

Comme on peut le constater, le problème restant consiste à ne pas superposer les « bords » des différentes boules de feu, il nous faut garder uniquement les bordures extérieures. Pour ça, rien de plus simple, le HTML5 nous laisse la possibilité de changer le mode de fusion de la balise canvas à l’aide de la propriété globalCompositeOperation d’un objet de type CanvasRenderingContext2D. Le mode de fusion qui nous intéresse est « lighter ». Et voici le rendu final:

Yaaaaaahhh !!

L’approche mathématique

Une fois l’algorithme graphique posé, je me suis lancé à la recherche des informations mathématiques nécessaires à la mise en place des différentes étapes. La première était de trouver la formule mathématique qui me permettrait de créer une forme de pic « ou dent de scie », et j’ai trouvé la fonction suivante.

// fonction  de type "double dent de scie"
// le paramètre "t" représente le "temps courant"
// le paramètre "m" représente le "temps maximum"
// le paramètre "o" représente un décalage dans le temps "offset"

function doubleSawtooth(t, m, o = 0) {
    let x = (t + o) / m * 2;
    return 1 - Math.abs(x % 2 - 1);
}

À noter que cette fonction renverra toujours une valeur comprise entre 0 et 1 ! Si l’on trace cette fonction, on obtiendra la courbe suivante:

Double dent de scie

On doit ensuite ajouter du bruit à notre courbe, afin de la rendre moins lisse, comme vu précédemment. Pour cela je suis allé chercher du côté d’une fonction similaire à la précédente. Les valeurs renvoyées sont toujours comprises entre 0 et 1 mais la courbe ressemble à ceci:

Dent de scie simple
// fonction traçant une courbe de type "dent de scie" au cours du temps 
// le paramètre "t" représente le "temps courant" 
// le paramètre "m" représente le "temps maximum" 
// le paramètre "o" représente un décalage dans le temps "offset" 
function sawtooth(t, m, o = 0) { 
    let x = (t + o) / m; 
    return (1 - x - Math.floor(1 - x)); 
}

Il me fallait trouver un moyen de combiner ces deux fonctions afin d’ajouter des « pics » sur mon pic principal. Pour cela, j’ai divisé la longueur totale de ma courbe en 6, puis j’ai appelé la fonction « dent de scie » sur chacune de ces sous parties. J’ai aussi pondéré les résultats de chacune des fonctions, donnant une importance plus grande au résultat de la fonction principale ( double dent de scie ) et une importance moindre à celle de la fonction secondaire ( dent de scie ). Voici le code et sa représentation graphique.


/* 
fonction "double dent de scie avec bruit"
le paramètre "t" représente le "temps courant"
le paramètre "m" représente le "temps maximum"
le paramètre "o" représente un décalage dans le temps "offset"
*/

function compose(t, m, o = 0) {
    // on calcule la valeur "double dent de scie"
    let value1 = doubleSawtooth(t, m, 0);
    // puis on calcule la valeur "dent de scie" sur un 1/6ème
    // de la longueur totale à chaque fois
    let value2 = sawtooth(t % m, m / 6, 0);
    // on inverse le sens de la fonction "dent de scie"
    // lorsqu'on est situé en dessous de la moitié de la 
    // courbe afin d'obtenir la courbe dans le bon sens
    if (t < m / 2) {
        value2 = 1 - value2;
    }
    // on pondère le résultat de la première fonction en lui accordant 
    // un "poids" 9 fois plus important qu'au second résultat
    value1 *= 0.9;
    value2 *= 0.1;
    // on retourne la combinaison des deux valeurs
    return value1 + value2;
}
Electrique n’est-il pas ?

On voit bien qu’il nous manque encore un peu de bruit … Et si on ajoutait quelque chose d’un peu aléatoire là dedans, ou même mieux, de « pseudo aléatoire » ?

// fonction renvoyant toujours le même nombre en position "num"
// pour la graine "seed". Cette fonctionnalité nous sert à retourner
// une suite de nombres qui ont l'air aléatoire.

function pseudorandom(num, seed = 0) {

    let t = seed + ((num % 0xFFFF) * 0x6D2B79F5);

    t = Math.imul(t ^ t >>> 15, t | 1);
    t ^= t + Math.imul(t ^ t >>> 7, t | 61);
    return ((t ^ t >>> 14) >>> 0) / 4294967296;
}

// puis on met à jour le code de la fonction compose 
// fonction renvoyant toujours le même nombre en position "num"
// pour la graine "seed". Cette fonctionnalité nous sert à retourner
// une suite de nombres qui ont l'air aléatoire.
function pseudorandom(num, seed = 0) {

    let t = seed + ((num % 0xFFFF) * 0x6D2B79F5);

    t = Math.imul(t ^ t >>> 15, t | 1);
    t ^= t + Math.imul(t ^ t >>> 7, t | 61);
    return ((t ^ t >>> 14) >>> 0) / 4294967296;
}

// fonction "double dent de scie avec bruit"
// le paramètre "t" représente le "temps courant"
// le paramètre "m" représente le "temps maximum"
// le paramètre "o" représente un décalage dans le temps "offset"
function compose(t, m, o = 0) {

    // on calcule la valeur "double dent de scie"
    let value1 = doubleSawtooth(t, m, 0);

    // puis on calcule la valeur "dent de scie" sur un 1/6ème
    // de la longueur totale à chaque fois
    let value2 = sawtooth(t % m, m / 6, 0);

    // puis on ajoute un bruit pseudo aléatoire
    let value3 = pseudorandom(t * m);

    // on inverse le sens de la fonction "dent de scie"
    // lorsqu'on est situé en dessous de la moitié de la 
    // courbe afin d'obtenir la courbe dans le bon sens
    if (t < m >> 1) {
        value2 = 1 - value2;
    }

    // on pondère le résultat de la première fonction en lui accordant 
    // un "poids" plus important qu'au second et troisième résultats
    value1 *= 0.85;
    value2 *= 0.1;
    value3 *= 0.05;

    // on retourne la combinaison des deux valeurs
    return value1 + value2 + value3;
}
On pousse les watts !

Et maintenant, nous allons arrondir un peu cette pente principale, pour cela nous allons utiliser une méthode très simple qui consiste à élever le nombre retourné par la fonction à une certaine puissance. Voici le résultat élevé au carré puis puissance 1/2.

Au carré !
Puissance 1/2 !
// on met à jour le code de la fonction compose

// on retourne la combinaison des deux valeurs élevée à puissance 1/2
return Math.pow( value1 + value2 + value3, 0.5 );

Et pour finir, il faut animer cette courbe de sorte à avoir l’impression que l’énergie se déplace vers le sommet de notre courbe. Pour cela, nous allons utiliser le paramètre de décalage (offset) que nous avons mis de côté jusqu’ici. Ce paramètre va tout simplement décaler l’ensemble des valeurs d’une certaine durée dans le temps, ce qui va donner l’impression que l’énergie bouge, alors qu’en vérité, on fait seulement varier chacune des données au niveau local. Et voici la version finale du code de la fonction de composition:

// fonction "double dent de scie avec bruit"
// le paramètre "t" représente le "temps courant"
// le paramètre "m" représente le "temps maximum"
// le paramètre "o" représente un décalage dans le temps "offset"
function compose(t, m, o = 0) {

    t = t % m;

    // on inverse le décalage (offset)  lorsqu'on
    // est situé en dessous de la moitié de la courbe
    // afin que le décalage s'opère dans le bon sens
    let offset = ( (t < m / 2 ) < 0.5) ? -o : o;

    // on calcule la valeur "double dent de scie"
    let value1 = doubleSawtooth(t, m, 0);

    // puis on calcule la valeur "dent de scie" sur un 1/6ème
    // de la longueur totale à chaque fois
    let value2 = sawtooth(t, m / 6, -offset);

    // puis on ajoute un bruit pseudo aléatoire
    let value3 = pseudorandom(t * m);

    // on inverse le sens de la fonction "dent de scie"
    // lorsqu'on est situé en dessous de la moitié de la 
    // courbe afin d'obtenir la courbe dans le bon sens
    if (t < m / 2) {
        value2 = 1 - value2;
    }

    // on pondère le résultat de la première fonction en lui accordant 
    // un "poids" plus important qu'au second et troisième résultats
    value1 *= 0.85;
    value2 *= 0.1;
    value3 *= 0.05;

    // on retourne la combinaison des deux valeurs élevée à puissance 1/2
    return Math.pow(value1 + value2 + value3, 0.5);
}

Conclusion

Et voilà, tu possèdes maintenant le code principal pour générer une courbe qui ressemble à une aura de DBZ. Bien entendu il te reste quelques petites choses à faire, toutefois, si tu n’en as pas le courage ( ou que tu ne sais pas comment faire ), tu peux retrouver le code final sur ce dépôt github -> https://github.com/thetinyspark/node-aura-generator. Tu peux cliquer ici pour te rendre sur l’éditeur d’aura que j’ai crée à partir de ce tutoriel. Quand à moi je te souhaite de t’amuser avec ce tuto et je te dis à bientôt, devant un petit café :).

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *