Comment générer des effets de foudre 2D étonnamment bons

La foudre a de nombreuses utilisations dans les jeux, de l'ambiance de fond lors d'un orage aux attaques éclair dévastatrices d'un sorcier. Dans ce didacticiel, nous expliquerons comment générer par programmation de superbes effets d'éclairage 2D: des boulons, des branches et même du texte..

Remarque: Bien que ce tutoriel ait été écrit en C # et XNA, vous devriez pouvoir utiliser les mêmes techniques et concepts dans presque tous les environnements de développement de jeux..


Aperçu vidéo final


Étape 1: Tracez une ligne rougeoyante

L'élément de base dont nous avons besoin pour créer la foudre est un segment de ligne. Commencez par ouvrir votre logiciel de retouche d'images préféré et dessinez une ligne droite de la foudre. Voici à quoi ressemble le mien:

Nous voulons dessiner des lignes de différentes longueurs, nous allons donc couper le segment de ligne en trois parties, comme indiqué ci-dessous. Cela nous permettra d’étirer le segment du milieu à n’importe quelle longueur. Étant donné que nous allons étirer le segment du milieu, nous pouvons l’enregistrer sous la forme d’un pixel d’épaisseur. De plus, comme les pièces de gauche et de droite sont des images inversées l'une de l'autre, il suffit d'en sauvegarder une. On peut le retourner dans le code.

Maintenant, déclarons une nouvelle classe pour gérer les segments de ligne de dessin:

public class Line public Vector2 A; vecteur public B2; flotteur public Épaisseur; ligne publique ()  ligne publique (vecteur2a, vecteur2b, épaisseur du flottant = 1) A = a; B = b; Épaisseur = épaisseur; 

A et B sont les extrémités de la ligne. En redimensionnant et en faisant pivoter les morceaux de la ligne, vous pouvez tracer une ligne de n'importe quelle épaisseur, longueur et orientation. Ajouter ce qui suit Dessiner() méthode à la Ligne classe:

public void Draw (SpriteBatch spriteBatch, Couleur) Vecteur2 tangente = B - A; rotation float = (float) Math.Atan2 (tangent.Y, tangent.X); const float ImageThickness = 8; float widthScale = Thickness / ImageThickness; Vector2 capOrigin = nouveau Vector2 (Art.HalfCircle.Width, Art.HalfCircle.Height / 2f); Vector2 middleOrigin = new Vector2 (0, Art.LightningSegment.Height / 2f); Vector2 middleScale = new Vector2 (tangent.Length (), widthScale); spriteBatch.Draw (Art.LightningSegment, A, null, couleur, rotation, middleOrigin, middleScale, SpriteEffects.None, 0f); spriteBatch.Draw (Art.HalfCircle, A, null, couleur, rotation, capOrigin, width, scale, SpriteEffects.None, 0f); spriteBatch.Draw (Art.HalfCircle, B, null, couleur, rotation + MathHelper.Pi, capOrigin, width, Scale, SpriteEffects.None, 0f); 

Ici, Art.LightningSegment et Art.HalfCircle sont statiques Texture2D variables contenant les images des morceaux du segment de droite. Imageépaisseur est réglé à l'épaisseur de la ligne sans la lueur. Dans mon image, c'est 8 pixels. Nous définissons l'origine du capuchon sur le côté droit et l'origine du segment central sur son côté gauche. Cela les fera se joindre de façon transparente lorsque nous les dessinerons tous les deux au point A. Le segment du milieu est étiré à la largeur souhaitée et un autre capuchon est dessiné au point B, pivoté de 180 °.

XNA SpriteBatch la classe vous permet de passer un SpriteSortMode dans son constructeur, qui indique l'ordre dans lequel il doit dessiner les sprites. Lorsque vous tracez la ligne, assurez-vous de la passer SpriteBatch avec son SpriteSortMode mis à SpriteSortMode.Texture. C'est pour améliorer les performances.

Les cartes graphiques sont excellentes pour dessiner la même texture plusieurs fois. Cependant, chaque fois qu'ils changent de texture, il y a une surcharge. Si nous dessinons un tas de lignes sans trier, nous dessinerions nos textures dans cet ordre:

LightningSegment, Demi-Cercle, Demi-Cercle, LightningSegment, Demi-Cercle, Demi-Cercle,…

Cela signifie que nous changerions de texture deux fois pour chaque ligne que nous dessinons.. SpriteSortMode.Texture raconte SpriteBatch trier le Dessiner() appelle par texture de sorte que tous les LightningSegments seront rassemblés et tous les Demi-cercles seront rassemblés. En outre, lorsque nous utilisons ces lignes pour fabriquer des éclairs, nous souhaitons utiliser un mélange additif pour que la lumière des éclairs superposés s’additionne.

SpriteBatch.Begin (SpriteSortMode.Texture, BlendState.Additive); // trace des lignes SpriteBatch.End ();

Étape 2: Lignes dentelées

Lightning a tendance à former des lignes irrégulières, nous aurons donc besoin d'un algorithme pour les générer. Pour ce faire, nous sélectionnons des points au hasard le long d'une ligne et les déplaçons à une distance aléatoire de la ligne. L'utilisation d'un déplacement complètement aléatoire a tendance à rendre la ligne trop irrégulière. Nous allons donc lisser les résultats en limitant la distance qui sépare les points voisins..

La ligne est lissée en plaçant des points avec un décalage similaire au point précédent; Cela permet à la ligne dans son ensemble de se promener de haut en bas, tout en évitant qu'une partie de celle-ci ne soit trop irrégulière. Voici le code:

Liste statique protégée CreateBolt (source Vector2, destination Vector2, épaisseur de flottement) var results = new List(); Vecteur2 tangente = dest - source; Vector2 normal = Vector2.Normalize (nouveau Vector2 (tangent.Y, -tangent.X)); float length = tangent.Length (); liste positions = nouvelle liste(); positions.Add (0); pour (int i = 0; i < length / 4; i++) positions.Add(Rand(0, 1)); positions.Sort(); const float Sway = 80; const float Jaggedness = 1 / Sway; Vector2 prevPoint = source; float prevDisplacement = 0; for (int i = 1; i < positions.Count; i++)  float pos = positions[i]; // used to prevent sharp angles by ensuring very close positions also have small perpendicular variation. float scale = (length * Jaggedness) * (pos - positions[i - 1]); // defines an envelope. Points near the middle of the bolt can be further from the central line. float envelope = pos > 0.95f? 20 * (1 - pos): 1; déplacement du flotteur = Rand (-Sway, Sway); déplacement - = (déplacement - prevDisplacement) * (1 - échelle); déplacement * = enveloppe; Vecteur2 point = source + pos * tangente + déplacement * normal; results.Add (nouvelle ligne (prevPoint, point, épaisseur)); prevPoint = point; prevDisplacement = deplacement;  results.Add (nouvelle ligne (prevPoint, dest, épaisseur)); renvoyer les résultats; 

Le code peut sembler un peu intimidant, mais ce n’est pas si grave une fois que vous comprenez la logique. Nous commençons par calculer les vecteurs normal et tangent de la ligne, ainsi que la longueur. Ensuite, nous choisissons au hasard un certain nombre de positions le long de la ligne et les stockons dans notre liste de positions. Les positions sont mises à l'échelle entre 0 et 1 tel que 0 représente le début de la ligne et 1 représente le point final. Ces positions sont ensuite triées pour nous permettre d’ajouter facilement des segments de ligne entre elles..

La boucle passe par les points choisis au hasard et les déplace le long de la normale d'une quantité aléatoire. Le facteur d’échelle est là pour éviter les angles trop vifs, et l’enveloppe garantit que la foudre se dirige réellement vers le point de destination en limitant le déplacement lorsque nous nous approchons de la fin..


Étape 3: animation

La foudre devrait clignoter de façon éclatante, puis disparaître progressivement. Pour gérer cela, créons un Éclair classe.

classe LightningBolt liste publique Segments = nouvelle liste(); flottant public Alpha get; ensemble;  public float FadeOutRate get; ensemble;  public Color Tint get; ensemble;  public bool IsComplete get return Alpha <= 0;   public LightningBolt(Vector2 source, Vector2 dest) : this(source, dest, new Color(0.9f, 0.8f, 1f))   public LightningBolt(Vector2 source, Vector2 dest, Color color)  Segments = CreateBolt(source, dest, 2); Tint = color; Alpha = 1f; FadeOutRate = 0.03f;  public void Draw(SpriteBatch spriteBatch)  if (Alpha <= 0) return; foreach (var segment in Segments) segment.Draw(spriteBatch, Tint * (Alpha * 0.6f));  public virtual void Update()  Alpha -= FadeOutRate;  protected static List CreateBolt (source Vector2, destination Vector2, épaisseur de flottement) //… //…

Pour l'utiliser, créez simplement un nouveau Éclair et appeler Mettre à jour() et Dessiner() chaque image. Appel Mettre à jour() le fait disparaître. Est complet vous dira quand le boulon sera complètement effacé.

Vous pouvez maintenant dessiner vos boulons en utilisant le code suivant dans votre classe de jeu:

LightningBolt boulon; MouseState mouseState, lastMouseState; protégé annule la mise à jour (GameTime gameTime) lastMouseState = mouseState; mouseState = Mouse.GetState (); var screenSize = new Vector2 (GraphicsDevice.Viewport.Width, GraphicsDevice.Viewport.Height); var mousePosition = new Vector2 (mouseState.X, mouseState.Y); if (MouseWasClicked ()) bolt = new LightningBolt (screenSize / 2, mousePosition); if (bolt! = null) bolt.Update ();  private bool MouseWasClicked () return mouseState.LeftButton == ButtonState.Pressed && lastMouseState.LeftButton == ButtonState.Released;  protected override void Draw (GameTime gameTime) GraphicsDevice.Clear (Color.Black); spriteBatch.Begin (SpriteSortMode.Texture, BlendState.Additive); if (bolt! = null) bolt.Draw (spriteBatch); spriteBatch.End (); 

Étape 4: Foudre de branche

Vous pouvez utiliser le Éclair classe comme un bloc de construction pour créer des effets de foudre plus intéressants. Par exemple, vous pouvez faire brancher les boulons comme indiqué ci-dessous:

Pour créer la branche de foudre, nous sélectionnons des points aléatoires le long de la foudre et ajoutons de nouveaux boulons qui se ramifient à partir de ces points. Dans le code ci-dessous, nous créons entre trois et six branches qui se séparent du boulon principal à des angles de 30 °.

classe BranchLightning List boulons = nouvelle liste(); public bool IsComplete get return Bolts.Count == 0;  public Vector2 End get; ensemble privé;  direction privée Vector2; Random statique statique = new Random (); BranchLightning public (début Vecteur2, fin Vecteur2) End = end; direction = Vector2.Normalize (fin - début); Créer (début, fin);  public void Update () bolts = bolts.Where (x =>! x.IsComplete) .ToList (); foreach (boulon dans les boulons) bolt.Update ();  public void Draw (SpriteBatch spriteBatch) foreach (boulon var dans boulons) bolt.Draw (spriteBatch);  private void Create (Vecteur2 début, Vecteur2 fin) var mainBolt = new LightningBolt (début, fin); boulons.Add (mainBolt); int numBranches = rand.Next (3, 6); Vector2 diff = end - start; // prend un tas de points aléatoires entre 0 et 1 et les trie float [] branchPoints = Enumerable.Range (0, numBranches) .Select (x => Rand (0, 1f)) .OrderBy (x => x). ToArray (); pour (int i = 0; i < branchPoints.Length; i++)  // Bolt.GetPoint() gets the position of the lightning bolt at specified fraction (0 = start of bolt, 1 = end) Vector2 boltStart = mainBolt.GetPoint(branchPoints[i]); // rotate 30 degrees. Alternate between rotating left and right. Quaternion rot = Quaternion.CreateFromAxisAngle(Vector3.UnitZ, MathHelper.ToRadians(30 * ((i & 1) == 0 ? 1 : -1))); Vector2 boltEnd = Vector2.Transform(diff * (1 - branchPoints[i]), rot) + boltStart; bolts.Add(new LightningBolt(boltStart, boltEnd));   static float Rand(float min, float max)  return (float)rand.NextDouble() * (max - min) + min;  

Étape 5: Texte éclair

Ci-dessous, une vidéo d'un autre effet que vous pouvez faire avec les éclairs:

Nous devons d’abord obtenir les pixels dans le texte que nous aimerions dessiner. Nous faisons cela en attirant notre texte vers un RenderTarget2D et relire les données de pixel avec RenderTarget2D.GetData(). Si vous souhaitez en savoir plus sur la création d'effets de particules de texte, j'ai un tutoriel plus détaillé ici.

Nous stockons les coordonnées des pixels dans le texte en tant que liste. Ensuite, chaque image, nous sélectionnons au hasard des paires de ces points et créons un éclair entre eux. Nous voulons le concevoir de manière à ce que plus les points sont proches l'un de l'autre, plus nous avons de chances de créer un verrou entre eux. Il existe une technique simple que nous pouvons utiliser pour y parvenir: nous allons choisir le premier point au hasard, puis nous choisirons un nombre fixe d’autres points au hasard et choisirons le plus proche..

Le nombre de points candidats que nous testons affectera l’apparence du texte éclair; vérifier un plus grand nombre de points nous permettra de trouver des points très proches entre lesquels dessiner des verrous, ce qui rendra le texte très net et lisible, mais avec moins de longs éclairs entre les lettres. Les petits chiffres donneront au texte éclair un aspect plus fou mais moins lisible.

public void Update () foreach (particule var dans textParticles) float x = particule.X / 500f; if (rand.Next (50) == 0) Vector2 mostParticle = Vector2.Zero; float mostDist = float.MaxValue; pour (int i = 0; i < 50; i++)  var other = textParticles[rand.Next(textParticles.Count)]; var dist = Vector2.DistanceSquared(particle, other); if (dist < nearestDist && dist > 10 * 10) mostDist = dist; le plus procheParticule = autre;  if (mostDist < 200 * 200 && nearestDist > 10 * 10) boulons.Add (nouveau LightningBolt (particule, particule la plus proche, couleur.blanc));  pour (int i = bolts.Count - 1; i> = 0; i--) bolts [i] .Update (); si (boulons [i] .IsComplete) boulons.RemoveAt (i); 

Étape 6: optimisation

Comme illustré ci-dessus, le texte éclair peut fonctionner correctement si vous avez un ordinateur haut de gamme, mais il est certainement très éprouvant. Chaque boulon dure plus de 30 cadres et nous créons des dizaines de nouveaux boulons pour chaque cadre. Étant donné que chaque éclair peut comporter jusqu'à quelques centaines de segments de ligne, et que chaque segment de ligne comporte trois parties, nous finissons par dessiner de nombreux sprites. Ma démonstration, par exemple, dessine plus de 25 000 images par image avec optimisations désactivées. On peut faire mieux.

Au lieu de dessiner chaque boulon jusqu'à ce qu'il disparaisse, nous pouvons dessiner chaque nouveau boulon vers une cible de rendu et faire disparaître progressivement la cible de rendu de chaque image. Cela signifie qu'au lieu de devoir dessiner chaque boulon pendant 30 images ou plus, nous ne le dessinons qu'une fois. Cela signifie également qu'il n'y a pas de coût de performance supplémentaire pour que nos éclairs disparaissent plus lentement et durent plus longtemps.

Tout d'abord, nous allons modifier le LightningText classe à ne dessiner que chaque boulon pour un cadre. Dans ton Jeu classe, déclarer deux RenderTarget2D variables: cadre actuel et dernier cadre. Dans LoadContent (), initialisez-les comme suit:

lastFrame = new RenderTarget2D (GraphicsDevice, screenSize.X, screenSize.Y, false, SurfaceFormat.HdrBlendable, DepthFormat.None); currentFrame = new RenderTarget2D (GraphicsDevice, screenSize.X, screenSize.Y, false, SurfaceFormat.HdrBlendable, DepthFormat.None);

Notez que le format de surface est défini sur HdrBlendable. HDR signifie High Dynamic Range. Il indique que notre surface HDR peut représenter une plus grande gamme de couleurs. Cela est nécessaire car cela permet à la cible de rendu d'avoir des couleurs plus vives que le blanc. Lorsque plusieurs éclairs se chevauchent, la cible de rendu doit stocker la somme complète de leurs couleurs, ce qui peut s’ajouter au-delà de la plage de couleurs standard. Bien que ces couleurs plus claires que le blanc soient toujours affichées en blanc sur l'écran, il est important de conserver toute leur luminosité pour les faire disparaître correctement..

Astuce XNA: Notez également que pour que la fusion HDR fonctionne, vous devez définir le profil de projet XNA sur Hi-Def. Vous pouvez le faire en cliquant avec le bouton droit de la souris sur le projet dans l'explorateur de solutions, en sélectionnant les propriétés, puis en choisissant le profil haute définition sous l'onglet XNA Game Studio..

Chaque image, nous dessinons d’abord le contenu de la dernière image sur l’image actuelle, mais légèrement assombrie. Nous ajoutons ensuite les boulons nouvellement créés au cadre actuel. Enfin, nous rendons notre image actuelle à l’écran, puis nous échangeons les deux cibles de rendu de sorte que, dernier cadre fera référence à l'image que nous venons de rendre.

void DrawLightningText () GraphicsDevice.SetRenderTarget (currentFrame); GraphicsDevice.Clear (Color.Black); // dessine la dernière image avec une luminosité de 96% spriteBatch.Begin (0, BlendState.Opaque, SamplerState.PointClamp, null, null); spriteBatch.Draw (lastFrame, Vector2.Zero, Color.White * 0.96f); spriteBatch.End (); // dessiner de nouveaux boulons avec un mélange additif lightningText.Draw (); spriteBatch.End (); // dessine le tout dans le tampon de fond GraphicsDevice.SetRenderTarget (null); spriteBatch.Begin (0, BlendState.Opaque, SamplerState.PointClamp, null, null); spriteBatch.Draw (currentFrame, Vector2.Zero, Color.White); spriteBatch.End (); Swap (réf currentFrame, ref lastFrame);  void swap(ref T a, ref T b) T temp = a; a = b; b = temp; 

Étape 7: Autres variations

Nous avons discuté de la création de texte avec des branches et des éclairs de branches, mais ces effets ne sont certainement pas les seuls effets possibles. Regardons quelques autres variations de la foudre que vous pouvez utiliser.

Moving Lightning

Souvent, vous souhaiterez peut-être créer un éclair mobile. Vous pouvez le faire en ajoutant un nouveau boulon court chaque cadre au point final du boulon du cadre précédent.

Vector2 lightningEnd = nouveau Vector2 (100, 100); Vector2 lightningVelocity = nouveau Vector2 (50, 0); void Update (GameTime gameTime) Bolts.Add (nouveau LightningBolt (lightningEnd, lightningEnd + lightningVelocity)); lightningEnd + = lightningVelocity; //…

Éclair lisse

Vous avez peut-être remarqué que les éclairs brillaient plus fortement au niveau des articulations. Cela est dû au mélange additif. Vous voudrez peut-être un plus lisse, plus même chercher votre foudre. Pour ce faire, modifiez votre fonction d'état de mélange afin de choisir la valeur maximale des couleurs source et cible, comme indiqué ci-dessous..

lecture seule privée statique BlendState maxBlend = new BlendState () AlphaBlendFunction = BlendFunction.Max, ColorBlendFunction = BlendFunction.Max, AlphaDestinationBlend = Blend.One, AlphaSourceBlend = Blend.One, ColorDestinationBlend

Ensuite, dans ton Dessiner() fonction, appel SpriteBatch.Begin () avec maxBlend comme le BlendState au lieu de BlendState.Additive. Les images ci-dessous montrent la différence entre le mélange additif et le mélange maximal sur un éclair.


Bien sûr, la fusion maximale ne permettra pas à la lumière de plusieurs boulons ou de l'arrière-plan de s'additionner. Si vous voulez que le boulon lui-même ait une apparence lisse, mais aussi qu'il se mélange de manière additive à d'autres boulons, vous pouvez d'abord le rendre à une cible de rendu à l'aide de la fusion maximale, puis dessiner la cible de rendu à l'écran à l'aide de la fusion additive. Veillez à ne pas utiliser un trop grand nombre de cibles de rendu car cela nuirait aux performances..

Une autre alternative, qui fonctionnera mieux avec un grand nombre de boulons, consiste à éliminer la lueur intégrée dans les images de segment de ligne et à la rajouter en utilisant un effet de lueur après traitement. Les détails relatifs à l'utilisation de shaders et à la création d'effets de luminescence dépassent le cadre de ce didacticiel, mais vous pouvez utiliser XNA Bloom Sample pour commencer. Cette technique ne nécessitera plus de cibles de rendu lorsque vous ajouterez plus de boulons..


Conclusion

La foudre est un excellent effet spécial pour améliorer vos jeux. Les effets décrits dans ce didacticiel constituent un bon point de départ, mais ce n’est certainement pas tout ce que vous pouvez faire avec la foudre. Avec un peu d'imagination, vous pouvez créer toutes sortes d'effets de foudre impressionnants! Téléchargez le code source et expérimentez le vôtre.

Si vous avez aimé cet article, jetez un coup d'œil à mon tutoriel sur les effets de l'eau en 2D.