Hoe schokkend goede 2D-bliksemeffecten te genereren

Lightning heeft veel toepassingen in games, van achtergrondsfeer tijdens een storm tot de verwoestende bliksemaanvallen van een tovenaar. In deze tutorial leg ik uit hoe je op programmatische wijze geweldige 2D-bliksemeffecten genereert: bouten, takken en zelfs tekst.

Notitie: Hoewel deze tutorial geschreven is met C # en XNA, zou je in bijna elke game-ontwikkelomgeving dezelfde technieken en concepten moeten kunnen gebruiken.


Laatste videovoorbeeld


Stap 1: teken een gloeiende lijn

De basisbouwsteen die we nodig hebben om bliksem te maken, is een lijnsegment. Begin met het openen van je favoriete beeldbewerkingssoftware en teken een bliksemflits. Dit is hoe de mijn eruit ziet:

We willen lijnen van verschillende lengtes tekenen, dus we gaan het lijnsegment in drie delen knippen, zoals hieronder wordt weergegeven. Dit stelt ons in staat om het middensegment uit te rekken naar elke gewenste lengte. Omdat we het middensegment gaan rekken, kunnen we het opslaan als slechts één pixel dik. Omdat de linker en rechter stukken spiegelbeelden van elkaar zijn, hoeven we er maar een te redden. We kunnen het in de code omdraaien.

Laten we nu een nieuwe klasse declareren voor het verwerken van lijnsegmenten:

public class Line public Vector2 A; openbare Vector2 B; openbare zweef Dikte; public Line ()  openbare regel (Vector2 a, Vector2 b, float thickness = 1) A = a; B = b; Dikte = dikte; 

A en B zijn de eindpunten van de lijn. Door de stukken van de lijn te schalen en te draaien, kunnen we een lijn van elke dikte, lengte en richting tekenen. Voeg het volgende toe Trek() methode om de Lijn klasse:

public void Draw (SpriteBatch spriteBatch, Kleur) Vector2 tangent = B - A; zweven rotatie = (zweven) Math.Atan2 (tangent.Y, tangent.X); const float ImageThickness = 8; zwevende dikteScale = Dikte / ImageThickness; Vector2 capOrigin = new Vector2 (Art.HalfCircle.Width, Art.HalfCircle.Height / 2f); Vector2 middleOrigin = new Vector2 (0, Art.LightningSegment.Height / 2f); Vector2 middleScale = new Vector2 (tangent.Length (), thicknessScale); spriteBatch.Draw (Art.LightningSegment, A, nul, kleur, rotatie, middleOrigin, middleScale, SpriteEffects.None, 0f); spriteBatch.Draw (Art.HalfCircle, A, nul, kleur, rotatie, capOrigin, thicknessScale, SpriteEffects.None, 0f); spriteBatch.Draw (Art.HalfCircle, B, null, kleur, rotatie + MathHelper.Pi, capOrigin, thicknessScale, SpriteEffects.None, 0f); 

Hier, Art.LightningSegment en Art.HalfCircle zijn statisch Texture2D variabelen die de afbeeldingen van de stukken van het lijnsegment vasthouden. ImageThickness is ingesteld op de dikte van de lijn zonder de gloed. In mijn afbeelding is het 8 pixels. We plaatsen de oorsprong van de dop aan de rechterkant en de oorsprong van het middensegment aan de linkerkant. Hierdoor worden ze naadloos samengevoegd wanneer we ze allebei in punt A tekenen. Het middensegment wordt uitgerekt tot de gewenste breedte en er wordt een andere dop getrokken in punt B, 180 ° gedraaid.

XNA's SpriteBatch klasse geeft je de mogelijkheid om er een te halen SpriteSortMode in zijn constructor, die de volgorde aangeeft waarin hij de sprites moet tekenen. Wanneer u de lijn tekent, zorg er dan voor dat u deze passeert SpriteBatch met zijn SpriteSortMode ingesteld op SpriteSortMode.Texture. Dit is om de prestaties te verbeteren.

Grafische kaarten zijn geweldig in het vele keren tekenen van dezelfde textuur. Telkens wanneer ze van structuur wisselen, is er echter overhead. Als we een aantal regels tekenen zonder te sorteren, tekenen we onze texturen in deze volgorde:

LightningSegment, HalfCircle, HalfCircle, LightningSegment, HalfCircle, HalfCircle, ...

Dit betekent dat we twee keer van structuur wisselen voor elke lijn die we tekenen. SpriteSortMode.Texture vertelt SpriteBatch om het te sorteren Trek() roept op textuur zodat alle LightningSegments zal samen worden getrokken en alle HalfCircles zullen samen worden getrokken. Wanneer we deze lijnen gebruiken om bliksemschichten te maken, willen we bovendien additieve overgang gebruiken om het licht van overlappende bliksemschichten samen te voegen.

SpriteBatch.Begin (SpriteSortMode.Texture, BlendState.Additive); // tekenregels SpriteBatch.End ();

Stap 2: Jagged Lines

Bliksem heeft de neiging om gekartelde lijnen te vormen, dus we hebben een algoritme nodig om deze te genereren. We doen dit door willekeurig punten langs een lijn te selecteren en deze op willekeurige afstand van de lijn te verplaatsen. Het gebruik van een volledig willekeurige verplaatsing heeft de neiging om de lijn te gekarteld te maken, dus we zullen de resultaten gladstrijken door te beperken hoe ver van elkaar aangrenzende punten kunnen worden verplaatst.

De lijn wordt afgevlakt door punten op een vergelijkbare afstand ten opzichte van het vorige punt te plaatsen; hierdoor kan de hele lijn op en neer dwalen en voorkomen dat een deel ervan te gek is. Hier is de code:

beschermde statische lijst CreateBolt (Vector2-bron, Vector2 dest, float thickness) var results = new List(); Vector2 tangent = dest - source; Vector2 normal = Vector2.Normalize (nieuwe Vector2 (tangent.Y, -tangent.X)); vlotterlengte = tangent.Length (); Lijst posities = nieuwe lijst(); positions.Add (0); voor (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; zwevende verplaatsing = Rand (-Sway, Sway); verplaatsing - = (verplaatsing - prevVisplacement) * (1 - schaal); verplaatsing * = envelop; Vector2 punt = bron + pos * tangens + verplaatsing * normaal; results.Add (nieuwe regel (prevPoint, punt, dikte)); prevPoint = punt; prevDisplacement = verplaatsing;  results.Add (nieuwe regel (prevPoint, dest, thickness)); resultaten retourneren; 

De code ziet er misschien een beetje intimiderend uit, maar het is niet zo erg als je de logica begrijpt. We beginnen met het berekenen van de normale en tangentiële vectoren van de lijn, samen met de lengte. Vervolgens kiezen we willekeurig een aantal posities langs de lijn en slaan deze op in onze positieslijst. De posities worden geschaald tussen 0 en 1 zoals dat 0 staat voor het begin van de regel en 1 vertegenwoordigt het eindpunt. Deze posities worden vervolgens gesorteerd, zodat we gemakkelijk lijnsegmenten tussen deze posities kunnen toevoegen.

De lus gaat door de willekeurig gekozen punten en verplaatst ze langs de normaal met een willekeurige hoeveelheid. De schaalfactor is er om overdreven scherpe hoeken te voorkomen, en de envelop zorgt ervoor dat de bliksem daadwerkelijk naar het bestemmingspunt gaat door de verplaatsing te beperken wanneer we dicht bij het einde zijn.


Stap 3: Animatie

Bliksem moet helder knipperen en vervolgens vervagen. Om dit aan te pakken, maken we een Bliksemschicht klasse.

class LightningBolt openbare lijst Segments = nieuwe lijst(); openbare float Alpha get; vast te stellen;  openbare float FadeOutRate get; vast te stellen;  openbare kleurtint get; vast te stellen;  public bool IsComplete krijg 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 (Vector2-bron, Vector2 dest, float thickness) // ... // ...

Gebruik gewoon een nieuw om dit te gebruiken Bliksemschicht en bel Bijwerken() en Trek() elk frame. Roeping Bijwerken() maakt het vervagen. Is compleet zal u vertellen wanneer de bout volledig is vervaagd.

U kunt nu uw bouten tekenen met behulp van de volgende code in uw klasse Game:

LightningBolt-bout; MouseState mouseState, lastMouseState; protected override void Update (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;  beveiligde override void Draw (GameTime gameTime) GraphicsDevice.Clear (Color.Black); spriteBatch.Begin (SpriteSortMode.Texture, BlendState.Additive); if (bolt! = null) bolt.Draw (spriteBatch); spriteBatch.End (); 

Stap 4: Branch Bliksem

U kunt de Bliksemschicht Klasse als een bouwsteen om meer interessante bliksemeffecten te creëren. U kunt bijvoorbeeld de bouten vertakken zoals hieronder weergegeven:

Om de bliksemtak te maken, kiezen we willekeurige punten langs de bliksemflits en voegen we nieuwe bouten toe die uit deze punten vertakken. In de onderstaande code creëren we tussen drie en zes takken die zich scheiden van de hoofdbout in hoeken van 30 °.

class BranchLightning Lijst bouten = nieuwe lijst(); public bool IsComplete krijg return bolts.Count == 0;  public Vector2 End get; privé set;  private Vector2-richting; statisch Willekeurige rand = nieuw Willekeurig (); public BranchLightning (Vector2 start, Vector2 end) Einde = einde; richting = Vector2.Normaliseren (einde - begin); Maken (start, einde);  public void Update () bolts = bolts.Where (x =>! x.IsComplete) .ToList (); foreach (var bolt in bolts) bolt.Update ();  public void Draw (SpriteBatch spriteBatch) foreach (var bolt in bolts) bolt.Draw (spriteBatch);  private void Create (Vector2 start, Vector2 end) var mainBolt = new LightningBolt (start, einde); bolts.Add (mainBolt); int numBranches = rand.Volgende (3, 6); Vector2 diff = einde - begin; // kies een aantal willekeurige punten tussen 0 en 1 en sorteer ze float [] branchPoints = Enumerable.Range (0, numBranches) .Select (x => Rand (0, 1f)). OrderBy (x => x). ToArray (); voor (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;  

Stap 5: Bliksemtekst

Hieronder staat een video van een ander effect dat je kunt maken met de bliksemschichten:

Eerst moeten we de pixels in de tekst krijgen die we willen tekenen. We doen dit door onze tekst naar a te trekken RenderTarget2D en het lezen van de pixeldata met RenderTarget2D.GetData(). Als u meer wilt lezen over het maken van tekstdeeltjeseffecten, heb ik hier een meer gedetailleerde zelfstudie.

We slaan de coördinaten van de pixels in de tekst op als een Lijst. Vervolgens pakken we willekeurig elk frame van deze punten en maken een bliksemschicht daartussen. We willen het zo ontwerpen dat de twee dichter bij elkaar liggen, des te groter de kans dat we een bout tussen hen maken. Er is een eenvoudige techniek die we kunnen gebruiken om dit te bereiken: we kiezen het eerste punt willekeurig, en dan kiezen we willekeurig een vast aantal andere punten en kiezen we de dichtstbijzijnde.

Het aantal kandidaat-punten dat we testen zal het uiterlijk van de bliksem veranderen; Door een groter aantal punten te controleren, kunnen we zeer nabije punten vinden om bouten tussen te tekenen, waardoor de tekst heel netjes en leesbaar blijft, maar met minder lange bliksemschichten tussen letters. Kleinere cijfers zullen de bliksem tekst er geker uit laten zien, maar minder goed leesbaar.

public void Update () foreach (var particle in textParticles) float x = particle.X / 500f; if (rand.Next (50) == 0) Vector2 nearestParticle = Vector2.Zero; zwevende dichtstbijzijndeDist = zweven. MaxValue; voor (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) nearestDist = dist; dichtstbijzijnde Stuks = andere;  if (nearestDist < 200 * 200 && nearestDist > 10 * 10) bolts.Add (nieuwe LightningBolt (deeltje, dichtstbijzijnde Particle, Color.White));  voor (int i = bolts.Count - 1; i> = 0; i--) bolts [i] .Update (); if (bouten [i] .IsComplete) bolts.RemoveAt (i); 

Stap 6: Optimalisatie

De bliksemschicht, zoals hierboven weergegeven, kan probleemloos werken als je een computer van topkwaliteit hebt, maar het is zeker zeer belastend. Elke bout duurt meer dan 30 frames en we maken elk frame tientallen nieuwe bouten. Omdat elke bliksemschicht tot een paar honderd lijnsegmenten kan bevatten en elk lijnsegment drie delen heeft, trekken we veel sprites. Mijn demo tekent bijvoorbeeld meer dan 25.000 afbeeldingen per frame met optimalisaties uitgeschakeld. We kunnen het beter doen.

In plaats van elke bout te tekenen totdat deze uitloopt, kunnen we elke nieuwe bout naar een renderdoel trekken en het renderingsdoel elk frame laten vervagen. Dit betekent dat we in plaats van elke bout voor 30 of meer frames te tekenen, we er maar één keer tekenen. Het betekent ook dat er geen extra prestatiekosten zijn voor het langzamer vervagen en langer duren van onze bliksems.

Ten eerste zullen we de LightningText klasse om elke bout voor slechts één frame te tekenen. In uw Spel klasse, twee verklaren RenderTarget2D variabelen: currentFrame en lastFrame. In LoadContent (), initialiseer ze zoals zo:

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);

Merk op dat de oppervlakte-indeling is ingesteld op HdrBlendable. HDR staat voor High Dynamic Range en dit geeft aan dat ons HDR-oppervlak een groter kleurenbereik kan vertegenwoordigen. Dit is nodig omdat het weergavetarget kleuren kan hebben die helderder zijn dan wit. Wanneer meerdere bliksemschichten overlappen, hebben we het renderdoel nodig om de volledige som van hun kleuren op te slaan, wat kan oplopen tot voorbij het standaardkleurenbereik. Hoewel deze kleuren helderder dan wit nog steeds als wit op het scherm worden weergegeven, is het belangrijk om de volledige helderheid op te slaan om ze op de juiste manier uit te faden.

XNA-tip: Merk ook op dat voor HDR-menging werkt, u het XNA-projectprofiel op Hi-Def moet instellen. U kunt dit doen door met de rechtermuisknop te klikken op het project in de oplossingsverkenner, eigenschappen te kiezen en vervolgens het hi-def profiel te kiezen onder het tabblad XNA Game Studio.

Bij elk frame tekenen we eerst de inhoud van het laatste frame naar het huidige frame, maar enigszins verduisterd. Vervolgens voegen we alle nieuw gemaakte bouten toe aan het huidige frame. Ten slotte maken we ons huidige frame op het scherm en wisselen we de twee renderdoelen om, zodat dit voor ons volgende frame, lastFrame verwijst naar het frame dat we zojuist hebben weergegeven.

void DrawLightningText () GraphicsDevice.SetRenderTarget (currentFrame); GraphicsDevice.Clear (Color.Black); // teken het laatste frame met een helderheid van 96% spriteBatch.Begin (0, BlendState.Opaque, SamplerState.PointClamp, null, null); spriteBatch.Draw (lastFrame, Vector2.Zero, Color.White * 0.96f); spriteBatch.End (); // teken nieuwe bouten met additieve blending spriteBatch.Begin (SpriteSortMode.Texture, BlendState.Additive); lightningText.Draw (); spriteBatch.End (); // teken het hele ding naar de backbuffer GraphicsDevice.SetRenderTarget (null); spriteBatch.Begin (0, BlendState.Opaque, SamplerState.PointClamp, null, null); spriteBatch.Draw (currentFrame, Vector2.Zero, Color.White); spriteBatch.End (); Swap (ref currentFrame, ref lastFrame);  void Swap(ref T a, ref T b) T temp = a; a = b; b = temp; 

Stap 7: andere variaties

We hebben gesproken over het maken van takbliksem en bliksem, maar dat zijn zeker niet de enige effecten die je kunt maken. Laten we eens naar een paar andere variaties op bliksem kijken die je misschien nog kunt gebruiken.

Moving Lightning

Vaak wilt u misschien een bewegende bliksemschicht maken. U kunt dit doen door een nieuwe korte bout per frame toe te voegen aan het eindpunt van de bout van het vorige frame.

Vector2 lightningEnd = nieuwe Vector2 (100, 100); Vector2 lightningVelocity = new Vector2 (50, 0); ongeldige update (GameTime gameTime) Bolts.Add (nieuwe LightningBolt (bliksemgang, bliksem Einde + bliksemvochtigheid)); bliksem Einde + = bliksem Snelheid; // ...

Soepele bliksem

U hebt misschien gemerkt dat de bliksem helderder op de gewrichten gloeit. Dit komt door het additieve blenden. Misschien wilt u een gelijkmatiger, gelijkmatiger uiterlijk van uw bliksem. Dit kan worden bereikt door uw mengstatusfunctie te wijzigen om de maximale waarde van de bron- en doelkleur te kiezen, zoals hieronder wordt weergegeven.

private static readonly BlendState maxBlend = new BlendState () AlphaBlendFunction.max, AlphaDestinationBlend = Blend.One, AlphaSourceBlend = Blend.One, ColorDestinationBlend.One, ColorSourceBlend = Blend.One;

Vervolgens in uw Trek() functie, oproep SpriteBatch.Begin () met maxBlend als de BlendState in plaats van BlendState.Additive. De onderstaande afbeeldingen laten het verschil zien tussen additief mengen en maximaal mengen op een bliksemschicht.


Uiteraard zorgt max blending ervoor dat het licht van meerdere bouten of van de achtergrond niet goed kan optellen. Als u wilt dat de bout zelf er glad uitziet, maar ook additief wordt gemengd met andere bouten, kunt u eerst de bout renderen naar een renderdoel met maximale overvloeiing en vervolgens het renderdoel op het scherm tekenen met additieve menging. Zorg ervoor dat u niet te veel grote renderdoelen gebruikt, omdat dit de prestaties schaadt.

Een ander alternatief, dat beter werkt voor grote aantallen bouten, is het elimineren van de gloed die is ingebouwd in de lijnsegmentafbeeldingen en deze opnieuw toe te voegen met behulp van een nabewerking-gloei-effect. De details over het gebruik van shaders en het maken van gloedeffecten vallen buiten het bestek van deze zelfstudie, maar u kunt het XNA Bloom-voorbeeld gebruiken om te beginnen. Voor deze techniek zijn er geen extra renderdoelen nodig als u meer bouten toevoegt.


Conclusie

Bliksem is een geweldig speciaal effect voor het verfraaien van je games. De effecten die in deze zelfstudie worden beschreven, zijn een goed beginpunt, maar het is zeker niet alles wat u met bliksem kunt doen. Met een beetje fantasie kun je allerlei ontzagwekkende bliksemeffecten maken! Download de broncode en experimenteer met uw eigen code.

Als je dit artikel leuk vond, bekijk dan ook mijn tutorial over 2D-watereffecten.