La plupart du temps, l’utilisation de techniques graphiques classiques est la meilleure solution. Parfois, cependant, l'expérimentation et la créativité aux niveaux fondamentaux d'un effet peuvent être bénéfiques pour le style du jeu, le rendant ainsi plus visible. Dans ce tutoriel, je vais vous montrer comment créer une rivière de lave 2D animée à l'aide de courbes de Bézier, d'une géométrie à texture personnalisée et de vertex shaders..
Remarque: Bien que ce tutoriel ait été écrit avec AS3 et Flash, vous devriez pouvoir utiliser les mêmes techniques et concepts dans presque tous les environnements de développement de jeux..
Cliquez sur le signe Plus pour ouvrir plus d'options: vous pouvez ajuster l'épaisseur et la vitesse de la rivière et faire glisser les points de contrôle et les points de position autour.
Pas de flash? Découvrez la vidéo YouTube à la place:
L'implémentation de démonstration ci-dessus utilise AS3 et Flash avec Starling Framework pour un rendu accéléré par GPU et la bibliothèque Feathers pour les éléments d'interface utilisateur. Dans notre scène initiale, nous allons placer une image au sol et une image de roche au premier plan. Plus tard, nous allons ajouter une rivière, en l'insérant entre ces deux couches.
Les rivières sont formées par des processus naturels complexes d’interaction entre une masse fluide et le sol situé en dessous. Il ne serait pas pratique de faire une simulation physiquement correcte pour un match. Nous voulons juste obtenir la bonne représentation visuelle et, pour ce faire, nous allons utiliser un modèle simplifié de rivière..
La modélisation de la rivière sous forme de courbe est l’une des solutions que nous pouvons utiliser, ce qui nous permet d’avoir un bon contrôle et d’obtenir un aspect sinueux. J'ai choisi d'utiliser des courbes de Bézier quadratiques pour garder les choses simples.
Les courbes de Bézier sont des courbes paramétriques souvent utilisées en infographie; dans les courbes de Bézier quadratiques, la courbe passe par deux points spécifiés et sa forme est déterminée par le troisième point, généralement appelé point de contrôle.
Comme indiqué ci-dessus, la courbe passe par les points de position tandis que le point de contrôle gère la trajectoire prise. Par exemple, placer le point de contrôle directement entre les points de position définit une ligne droite, tandis que les autres valeurs du point de contrôle "attirent" la courbe pour aller près de ce point..
Ce type de courbe est défini à l'aide de la formule mathématique suivante:
[latex] \ Large B (t) = (1 - t) ^ 2 P_0 + (2t - 2t ^ 2) C + t ^ 2 P_1 [/ latex]À t = 0, nous sommes au début de notre courbe; à t = 1 nous sommes à la fin.
Techniquement, nous allons utiliser plusieurs courbes de Bézier où la fin de l’une est le début de l’autre, formant une chaîne..
Nous devons maintenant résoudre le problème de l'affichage de notre rivière. Les courbes n'ont pas d'épaisseur, nous allons donc construire une primitive géométrique autour de celle-ci.
Nous avons d’abord besoin d’un moyen de prendre la courbe et de la convertir en segments. Pour ce faire, nous prenons nos points et les connectons à la définition mathématique de la courbe. La chose intéressante à propos de cela est que nous pouvons facilement ajouter un paramètre pour contrôler la qualité de cette opération..
Voici le code pour générer les points à partir de la définition de la courbe:
// Calculer le point à partir d'une expression de Bézier quadratique fonction privée quadraticBezier (P0: Point, P1: Point, C: Point, t: Nombre): Point var x = (1 - t) * (1 - t) * P0.x + (2 - 2 * t) * t * Cx + t * t * P1.x; var y = (1 - t) * (1 - t) * P0.y + (2 - 2 * t) * t * C.y + t * t * P1.y; renvoyer le nouveau point (x, y);
Et voici comment convertir la courbe en segments:
// Cette méthode utilise une liste de nœuds. // Chaque nœud est défini comme suit: position, control fonction publique convertToPoints (quality: Number = 10): Vector. points var: vecteur. = nouveau vecteur. (); précision var: Nombre = 1 / qualité; // Passez tous les nœuds pour générer des segments de droite pour (var i: int = 0; i < _nodes.length - 1; i++) var current:CurveNode = _nodes[i]; var next:CurveNode = _nodes[i + 1]; // Sample Bezier curve between two nodes // Number of steps is determined by quality parameter for (var step:Number = 0; step < 1; step += precision) var newPoint:Point = quadraticBezier(current.position, next.position, current.control, step); points.push(newPoint); return points;
Nous pouvons maintenant prendre une courbe arbitraire et la convertir en un nombre personnalisé de segments de ligne - plus il y a de segments, plus la qualité est élevée:
Pour arriver à la géométrie, nous allons générer deux nouvelles courbes basées sur celle d'origine. Leur position et leurs points de contrôle seront déplacés par une valeur de décalage vectoriel normale, que nous pouvons considérer comme l’épaisseur. La première courbe sera déplacée dans le sens négatif, tandis que la seconde sera déplacée dans le sens positif.
Nous allons maintenant utiliser la fonction définie précédemment pour créer des segments de droite à partir des courbes. Cela formera une limite autour de la courbe d'origine.
Comment fait-on cela dans le code? Nous devrons calculer les normales pour la position et les points de contrôle, les multiplier par le décalage et les ajouter aux valeurs d'origine. Pour les points de position, nous devrons interpoler normales formant des lignes aux points de contrôle adjacents.
// parcourir tous les points pour (var i: int = 0; i < _nodes.length; i++) var normal:Point; var surface:Point; // Normal formed by position points if (i == 0) // First point - take normal from first line segment normal = lineNormal(_nodes[i].position, _nodes[i].control); surface = lineNormal(_nodes[i].position, _nodes[i + 1].position); else if (i + 1 == _nodes.length) // Last point - take normal from last line segment normal = lineNormal(_nodes[i - 1].control, _nodes[i].position); surface = lineNormal(_nodes[i - 1].position, _nodes[i].position); else // Middle point - take 2 normals from segments // adjecent to the point, and interpolate them normal = lineNormal(_nodes[i].position, _nodes[i].control); normal = normal.add( lineSegmentNormal(_nodes[i - 1].control, _nodes[i].position)); normal.normalize(1); // This causes a slight visual issue for thicker rivers // It can be avoided by adding more nodes surface = lineNormal(_nodes[i].position, _nodes[i + 1].position); // Add offsets to the original node, forming a new one. nodesWithOffset.add( _nodes[i].position.x + normal.x * offset, _nodes[i].position.y + normal.y * offset, _nodes[i].control.x + surfaceNormal.x * offset, _nodes[i].control.y + surfaceNormal.y * offset );
Vous pouvez déjà voir que nous pouvons utiliser ces points pour définir de petits polygones à quatre côtés - des "quadruples". Notre implémentation utilise un Starling DisplayObject personnalisé, qui transmet directement nos données géométriques au GPU..
Un problème, selon l’implémentation, est que nous ne pouvons pas envoyer de quads directement; au lieu de cela, nous devons envoyer des triangles. Mais il est assez facile de choisir deux triangles en utilisant quatre points:
Résultat:
Le style géométrique propre est amusant, et il pourrait même être un bon style pour certains jeux expérimentaux. Mais, pour que notre rivière soit vraiment belle, nous pourrions faire quelques détails supplémentaires. Utiliser une texture est une bonne idée. Ce qui nous amène au problème de l'afficher sur une géométrie personnalisée créée précédemment.
Nous devrons ajouter des informations supplémentaires à nos sommets; les positions seules ne suffiront plus. Chaque sommet peut stocker des paramètres supplémentaires à notre convenance, et pour prendre en charge le mappage de texture, nous devrons définir des coordonnées de texture..
Les coordonnées de texture sont dans l'espace de texture et mappent les valeurs en pixels de l'image aux positions des sommets dans le monde. Pour chaque pixel qui apparaît à l'écran, nous calculons les coordonnées de texture interpolées et les utilisons pour rechercher les valeurs de pixel des positions dans la texture. Les valeurs 0 et 1 dans l'espace de texture correspondent aux bords de la texture; si les valeurs quittent cette plage, nous avons plusieurs options:
Ceux qui connaissent un peu le mappage de texture sont certainement conscients des complexités possibles de la technique. J'ai de bonnes nouvelles pour toi! Cette façon de représenter les rivières est facilement mappée à une texture.
À partir des côtés, la hauteur de la texture est cartographiée dans son intégralité, tandis que la longueur de la rivière est segmentée en petits morceaux de l'espace de texture, dimensionnés de manière appropriée à la largeur de la texture..
Maintenant, implémentons-le dans le code:
// _texture est une texture de Starling var distance: Number = 0; // parcourir tous les points pour (var i: int = 0; i < _points.length; i++) if (i > 0) // Distance dans l'espace de texture pour la distance du segment de ligne en cours + = Point.distance (lastPoint, _points [i]) / _texture.width; // Affectation des coordonnées de texture à la géométrie _vertexData.setTexCoords (vertexId ++, distance, 0); _vertexData.setTexCoords (vertexId ++, distance, 1);
Maintenant, cela ressemble plus à une rivière:
Notre rivière ressemble maintenant beaucoup plus à une vraie, à une exception près: elle est immobile!
Bon, il faut donc l'animer. La première chose à laquelle vous pouvez penser est d'utiliser l'animation de feuille de sprite. Et cela peut fonctionner, mais pour garder plus de flexibilité et économiser un peu de mémoire de texture, nous ferons quelque chose de plus intéressant..
Au lieu de changer la texture, nous pouvons changer la façon dont la texture correspond à la géométrie. Nous faisons cela en changeant les coordonnées de texture pour nos sommets. Cela ne fonctionnera que pour les textures tiled avec un mappage défini sur répéter
.
Une méthode simple consiste à modifier les coordonnées de la texture sur la CPU et à envoyer les résultats au GPU à chaque image. C'est généralement un bon moyen de démarrer une implémentation avec ce type de technique, car le débogage est beaucoup plus facile. Cependant, nous allons plonger directement dans la meilleure façon d'accomplir ceci: animer les coordonnées de texture à l'aide de vertex shaders.
Par expérience, je peux dire que les shaders intimident parfois les gens, probablement à cause de leur lien avec les effets graphiques avancés des jeux à succès. À vrai dire, le concept qui les sous-tend est extrêmement simple, et si vous pouvez écrire un programme, vous pouvez écrire un shader - c'est tout ce qu'ils sont, de petits programmes exécutés sur le GPU. Nous allons utiliser un vertex shader pour animer notre rivière. Il existe plusieurs autres types de shaders, mais nous pouvons nous en passer..
Comme son nom l'indique, les vertex shaders traitent les sommets. Ils fonctionnent pour chaque sommet et prennent comme attributs de sommet d’entrée: position, coordonnées de texture et couleur..
Notre objectif est de compenser la valeur X de la coordonnée de texture de la rivière pour simuler l'écoulement. Nous gardons un compteur de flux et l'augmentons chaque image par delta temporel. Nous pouvons spécifier un paramètre supplémentaire pour la vitesse de l'animation. La valeur de décalage doit être transmise au shader en tant que valeur uniforme (constante), afin de fournir au programme de shader plus d'informations que de simples sommets. Cette valeur est généralement un vecteur à quatre composantes; nous allons simplement utiliser le composant X pour stocker la valeur, tout en réglant Y, Z et W sur 0.
// décalage de texture à l'index 5, que nous référencerons plus tard dans le shader context.setProgramConstantsFromVector (Context3DProgramType.VERTEX, 5, nouveau [-_textureOffset, 0, 0, 0], 1);
Cette implémentation utilise le langage de shader AGAL. Cela peut être un peu difficile à comprendre car c'est un assemblage comme un langage. Vous pouvez en apprendre plus ici.
Vertex shader:
m44 op, va0, vc0 // Calculer la position du sommet du monde mul v0, va1, vc4 // Calculer la couleur du sommet // Ajouter la coordonnée de texture du sommet (va2) et notre constante de décalage de texture (vc5): add v1, va2, vc5
Animation en action:
Nous avons presque terminé, sauf que notre rivière n'a toujours pas l'air naturelle. La plaine coupée entre le fond et la rivière est une véritable horreur. Pour résoudre ce problème, vous pouvez utiliser une couche supplémentaire de la rivière, légèrement plus épaisse, et une texture spéciale, qui recouvrirait les berges de la rivière et couvrirait la transition laide..
Et comme la démo représente une rivière de lave en fusion, on ne peut pas aller sans un peu de lueur! Créez une autre instance de la géométrie de la rivière, en utilisant maintenant une texture luminescente et définissez son mode de fusion sur "ajouter". Pour encore plus de plaisir, ajoutez une animation fluide de la valeur alpha glow.
Démo finale:
Bien sûr, vous pouvez faire beaucoup plus que des rivières en utilisant ce type d’effet. Je l'ai vu utilisé pour des effets de particules fantômes, des cascades ou même pour animer des chaînes. Il reste encore beaucoup d’améliorations à apporter. Les performances finales présentées ci-dessus peuvent être réalisées à l’aide d’un seul appel à dessiner si les textures sont fusionnées dans un atlas. Les longues rivières doivent être scindées en plusieurs parties. Une extension majeure consisterait à mettre en œuvre le forgeage des nœuds de courbe pour permettre plusieurs trajets de rivière et simuler à son tour une bifurcation..
J'utilise cette technique dans notre dernier jeu et je suis très heureux de ce que nous pouvons en faire. Nous l'utilisons pour les rivières et les routes (sans animation, évidemment). Je pense utiliser un effet similaire pour les lacs.
J'espère vous avoir donné quelques idées sur la façon de penser en dehors des techniques graphiques habituelles, telles que l'utilisation de feuilles de sprite ou de jeux de mosaïques pour obtenir des effets comme celui-ci. Cela nécessite un peu plus de travail, un peu de calcul et quelques connaissances en programmation GPU, mais en contrepartie, vous bénéficiez d'une plus grande flexibilité..